Fix frontend imports and track frontend lib files
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -108,8 +108,8 @@ develop-eggs/
|
|||||||
downloads/
|
downloads/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
lib/
|
/lib/
|
||||||
lib64/
|
/lib64/
|
||||||
parts/
|
parts/
|
||||||
sdist/
|
sdist/
|
||||||
var/
|
var/
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
60
frontend/lib/api.ts
Normal 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;
|
||||||
|
}
|
||||||
77
frontend/lib/tencentSse.ts
Normal file
77
frontend/lib/tencentSse.ts
Normal 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
129
frontend/lib/treeToGraph.ts
Normal 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 };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user