Fix frontend imports and track frontend lib files

This commit is contained in:
2026-03-21 09:22:10 +08:00
parent 83f7a66e32
commit 190cee20bd
8 changed files with 285 additions and 19 deletions

4
.gitignore vendored
View File

@@ -108,8 +108,8 @@ develop-eggs/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
lib/ /lib/
lib64/ /lib64/
parts/ parts/
sdist/ sdist/
var/ var/

View File

@@ -1,4 +1,4 @@
"use client"; "use client";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { import {
@@ -8,10 +8,10 @@ import {
useState, useState,
type MouseEvent as ReactMouseEvent, type MouseEvent as ReactMouseEvent,
} from "react"; } from "react";
import MindmapCanvas from "@/components/MindmapCanvas"; import MindmapCanvas from "../../../components/MindmapCanvas";
import ChatPanel from "@/components/ChatPanel"; import ChatPanel from "../../../components/ChatPanel";
import { getMindmap } from "@/lib/api"; import { getMindmap } from "../../../lib/api";
import type { Mindmap } from "@/types/mindmap"; import type { Mindmap } from "../../../types/mindmap";
type TriggerRequest = { type TriggerRequest = {
id: number; id: number;

View File

@@ -1,15 +1,15 @@
"use client"; "use client";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import MarkdownMessage from "@/components/MarkdownMessage"; import MarkdownMessage from "./MarkdownMessage";
import { sendChatMessage } from "@/lib/api"; import { sendChatMessage } from "../lib/api";
import { import {
parseJsonSafely, parseJsonSafely,
splitSseEvents, splitSseEvents,
type TencentErrorEvent, type TencentErrorEvent,
type TencentReplyEvent, type TencentReplyEvent,
} from "@/lib/tencentSse"; } from "../lib/tencentSse";
import type { ChatMessage } from "@/types/mindmap"; import type { ChatMessage } from "../types/mindmap";
type TriggerRequest = { type TriggerRequest = {
id: number; id: number;

View File

@@ -1,11 +1,11 @@
"use client"; "use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import ReactFlow, { Background, Controls, MiniMap, type ReactFlowInstance } from "reactflow"; import ReactFlow, { Background, Controls, MiniMap, type ReactFlowInstance } from "reactflow";
import "reactflow/dist/style.css"; import "reactflow/dist/style.css";
import MindmapNodeCard from "@/components/MindmapNodeCard"; import MindmapNodeCard from "./MindmapNodeCard";
import { treeToGraph } from "@/lib/treeToGraph"; import { treeToGraph } from "../lib/treeToGraph";
import type { MindmapNode } from "@/types/mindmap"; import type { MindmapNode } from "../types/mindmap";
type MindmapCanvasProps = { type MindmapCanvasProps = {
tree: MindmapNode; tree: MindmapNode;

View File

@@ -1,8 +1,8 @@
"use client"; "use client";
import { memo, useCallback, type MouseEvent } from "react"; import { memo, useCallback, type MouseEvent } from "react";
import { Handle, Position, type NodeProps } from "reactflow"; import { Handle, Position, type NodeProps } from "reactflow";
import type { GraphNodeData } from "@/types/mindmap"; import type { GraphNodeData } from "../types/mindmap";
function MindmapNodeCard({ data }: NodeProps<GraphNodeData>) { function MindmapNodeCard({ data }: NodeProps<GraphNodeData>) {
const isRoot = data.level === 0; const isRoot = data.level === 0;

60
frontend/lib/api.ts Normal file
View File

@@ -0,0 +1,60 @@
import type { CreateMindmapPayload, Mindmap } from "../types/mindmap";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://127.0.0.1:8000";
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const response = await fetch(`${API_BASE_URL}${path}`, {
...init,
headers: {
"Content-Type": "application/json",
...(init?.headers ?? {}),
},
cache: "no-store",
});
if (!response.ok) {
const message = await response.text();
throw new Error(message || "请求失败");
}
return (await response.json()) as T;
}
export async function createMindmap(
payload: CreateMindmapPayload,
): Promise<Mindmap> {
return request<Mindmap>("/api/mindmaps", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function getMindmap(uniqueId: string): Promise<Mindmap> {
return request<Mindmap>(`/api/mindmaps/${uniqueId}`);
}
export async function sendChatMessage(
sessionId: string,
content: string,
visitorBizId: string = "default_visitor",
signal?: AbortSignal,
): Promise<Response> {
const response = await fetch(`${API_BASE_URL}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
signal,
body: JSON.stringify({
session_id: sessionId,
content,
visitor_biz_id: visitorBizId,
}),
});
if (!response.ok) {
const message = await response.text();
throw new Error(message || "请求失败");
}
return response;
}

View File

@@ -0,0 +1,77 @@
export type SseEvent = {
event: string;
data: string;
};
export type TencentReplyEvent = {
type?: "reply";
payload?: {
content?: string;
is_evil?: boolean;
is_final?: boolean;
is_from_self?: boolean;
is_llm_generated?: boolean;
};
};
export type TencentErrorEvent = {
type?: "error";
error?: {
code?: number;
message?: string;
};
message?: string;
status?: number;
};
export function parseJsonSafely<T>(value: string): T | null {
try {
return JSON.parse(value) as T;
} catch {
return null;
}
}
export function splitSseEvents(buffer: string): {
events: SseEvent[];
rest: string;
} {
const blocks = buffer.split(/\r?\n\r?\n/);
const rest = blocks.pop() ?? "";
const events = blocks
.map(parseSseEventBlock)
.filter((event): event is SseEvent => event !== null);
return { events, rest };
}
function parseSseEventBlock(block: string): SseEvent | null {
const lines = block.split(/\r?\n/);
let eventName = "message";
const dataLines: string[] = [];
for (const rawLine of lines) {
const line = rawLine.trimEnd();
if (!line || line.startsWith(":")) {
continue;
}
if (line.startsWith("event:")) {
eventName = line.slice(6).trim();
continue;
}
if (line.startsWith("data:")) {
dataLines.push(line.slice(5).trimStart());
}
}
if (dataLines.length === 0) {
return null;
}
return {
event: eventName,
data: dataLines.join("\n"),
};
}

129
frontend/lib/treeToGraph.ts Normal file
View File

@@ -0,0 +1,129 @@
import type { Edge, Node } from "reactflow";
import { Position } from "reactflow";
import type { GraphData, GraphNodeData, MindmapNode } from "../types/mindmap";
const LEVEL_GAP = 320;
const SIBLING_GAP = 120;
const PADDING_X = 80;
const PADDING_Y = 100;
type PositionMap = Map<string, { x: number; y: number }>;
type TreeToGraphOptions = {
expandedNodeIds?: Set<string>;
onToggleNode?: (nodeId: string) => void;
onOpenNodeChat?: (nodeLabel: string) => void;
};
function getVisibleChildren(
node: MindmapNode,
expandedNodeIds: Set<string>,
): MindmapNode[] {
if (node.children.length === 0) {
return [];
}
return expandedNodeIds.has(node.id) ? node.children : [];
}
function assignPositions(
node: MindmapNode,
positions: PositionMap,
leafIndexRef: { value: number },
expandedNodeIds: Set<string>,
): number {
const visibleChildren = getVisibleChildren(node, expandedNodeIds);
if (visibleChildren.length === 0) {
const y = leafIndexRef.value * SIBLING_GAP;
leafIndexRef.value += 1;
positions.set(node.id, { x: node.level * LEVEL_GAP, y });
return y;
}
const childYs = visibleChildren.map((child) =>
assignPositions(child, positions, leafIndexRef, expandedNodeIds),
);
const y =
childYs.reduce((total, current) => total + current, 0) / childYs.length;
positions.set(node.id, { x: node.level * LEVEL_GAP, y });
return y;
}
function buildGraph(
node: MindmapNode,
positions: PositionMap,
nodes: Node<GraphNodeData>[],
edges: Edge[],
expandedNodeIds: Set<string>,
options: TreeToGraphOptions,
): void {
const position = positions.get(node.id);
if (!position) {
return;
}
const hasChildren = node.children.length > 0;
const isExpanded = expandedNodeIds.has(node.id);
const onToggleNode = options.onToggleNode;
const onOpenNodeChat = options.onOpenNodeChat;
nodes.push({
id: node.id,
type: "mindmapNode",
position: {
x: position.x + PADDING_X,
y: position.y + PADDING_Y,
},
style: {
cursor: "default",
},
data: {
label: node.label,
level: node.level,
isLeaf: node.is_leaf,
hasChildren,
isExpanded,
onToggle: hasChildren && onToggleNode ? () => onToggleNode(node.id) : undefined,
onOpenChat: onOpenNodeChat ? () => onOpenNodeChat(node.label) : undefined,
},
sourcePosition: Position.Right,
targetPosition: Position.Left,
draggable: false,
selectable: false,
});
getVisibleChildren(node, expandedNodeIds).forEach((child) => {
edges.push({
id: `${node.id}-${child.id}`,
source: node.id,
target: child.id,
type: "smoothstep",
animated: false,
style: {
stroke: "#60a5fa",
strokeWidth: 2,
},
});
buildGraph(child, positions, nodes, edges, expandedNodeIds, options);
});
}
export function treeToGraph(
tree: MindmapNode,
options: TreeToGraphOptions = {},
): GraphData {
const positions: PositionMap = new Map();
const leafIndexRef = { value: 0 };
const nodes: Node<GraphNodeData>[] = [];
const edges: Edge[] = [];
const expandedNodeIds = options.expandedNodeIds ?? new Set([tree.id]);
assignPositions(tree, positions, leafIndexRef, expandedNodeIds);
buildGraph(tree, positions, nodes, edges, expandedNodeIds, options);
return { nodes, edges };
}