@langgraph-js/ui 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.env +2 -0
  2. package/.env.example +2 -0
  3. package/dist/assets/index-7vem5Peg.css +1 -0
  4. package/dist/assets/index-CZ6k2dGe.js +235 -0
  5. package/dist/index.html +3 -5
  6. package/index.html +1 -3
  7. package/package.json +10 -2
  8. package/src/artifacts/ArtifactViewer.tsx +158 -0
  9. package/src/artifacts/ArtifactsContext.tsx +99 -0
  10. package/src/artifacts/SourceCodeViewer.tsx +15 -0
  11. package/src/chat/Chat.tsx +102 -27
  12. package/src/chat/FileUpload/index.ts +3 -7
  13. package/src/chat/components/FileList.tsx +16 -12
  14. package/src/chat/components/HistoryList.tsx +39 -137
  15. package/src/chat/components/JsonEditorPopup.tsx +57 -45
  16. package/src/chat/components/JsonToMessage/JsonToMessage.tsx +20 -14
  17. package/src/chat/components/JsonToMessage/JsonToMessageButton.tsx +9 -16
  18. package/src/chat/components/JsonToMessage/index.tsx +1 -1
  19. package/src/chat/components/MessageAI.tsx +5 -4
  20. package/src/chat/components/MessageBox.tsx +21 -19
  21. package/src/chat/components/MessageHuman.tsx +13 -10
  22. package/src/chat/components/MessageTool.tsx +71 -30
  23. package/src/chat/components/UsageMetadata.tsx +41 -22
  24. package/src/chat/context/ChatContext.tsx +14 -5
  25. package/src/chat/store/index.ts +25 -2
  26. package/src/chat/tools/ask_user_for_approve.tsx +80 -0
  27. package/src/chat/tools/create_artifacts.tsx +50 -0
  28. package/src/chat/tools/index.ts +5 -0
  29. package/src/chat/tools/update_plan.tsx +75 -0
  30. package/src/chat/tools/web_search_tool.tsx +89 -0
  31. package/src/graph/index.tsx +9 -6
  32. package/src/index.ts +1 -0
  33. package/src/login/Login.tsx +155 -47
  34. package/src/memory/BaseDB.ts +92 -0
  35. package/src/memory/db.ts +232 -0
  36. package/src/memory/fulltext-search.ts +191 -0
  37. package/src/memory/index.ts +4 -0
  38. package/src/memory/tools.ts +170 -0
  39. package/test/main.tsx +2 -2
  40. package/vite.config.ts +7 -1
  41. package/dist/assets/index-BWndsYW1.js +0 -214
  42. package/dist/assets/index-LcgERueJ.css +0 -1
  43. package/src/chat/chat.css +0 -406
  44. package/src/chat/components/FileList.css +0 -129
  45. package/src/chat/components/JsonEditorPopup.css +0 -81
  46. package/src/chat/components/JsonToMessage/JsonToMessage.css +0 -104
  47. package/src/chat/tools.ts +0 -33
  48. package/src/login/Login.css +0 -93
@@ -1,6 +1,10 @@
1
- import React from "react";
1
+ import React, { JSX, useState } from "react";
2
2
  import { LangGraphClient, RenderMessage, ToolMessage } from "@langgraph-js/sdk";
3
3
  import { UsageMetadata } from "./UsageMetadata";
4
+ import { useChat } from "../context/ChatContext";
5
+ import Markdown from "react-markdown";
6
+ import remarkGfm from "remark-gfm";
7
+
4
8
  interface MessageToolProps {
5
9
  message: ToolMessage & RenderMessage;
6
10
  client: LangGraphClient;
@@ -11,40 +15,77 @@ interface MessageToolProps {
11
15
  }
12
16
 
13
17
  const MessageTool: React.FC<MessageToolProps> = ({ message, client, getMessageContent, formatTokens, isCollapsed, onToggleCollapse }) => {
18
+ const { getToolUIRender } = useChat();
19
+ const render = getToolUIRender(message.name!);
14
20
  return (
15
- <div className="message tool">
16
- {message.name === "ask_user" && !message.additional_kwargs?.done && (
17
- <div>
18
- <div>询问 {message.tool_input}</div>
19
- <input
20
- type="text"
21
- onKeyDown={(e) => {
22
- if (e.key === "Enter") {
23
- client.doneFEToolWaiting(message.id!, (e.target as any).value);
24
- }
25
- }}
26
- />
27
- </div>
28
- )}
29
- <div className="tool-message">
30
- <div className="tool-header" onClick={onToggleCollapse}>
31
- <div className="tool-title">
32
- {message.node_name} | {message.name}
21
+ <div className="flex flex-col w-full">
22
+ {render ? (
23
+ (render(message) as JSX.Element)
24
+ ) : (
25
+ <div className="flex flex-col w-full bg-white rounded-lg shadow-sm border border-gray-200">
26
+ <div className="flex items-center justify-between p-3 cursor-pointer hover:bg-gray-50 transition-colors" onClick={onToggleCollapse}>
27
+ <div className="text-sm font-medium text-gray-700" onClick={() => console.log(message)}>
28
+ {message.node_name} | {message.name}
29
+ </div>
33
30
  </div>
31
+
32
+ {!isCollapsed && (
33
+ <div className="flex flex-col gap-4 p-4 border-t border-gray-100">
34
+ <Previewer content={message.tool_input || ""} />
35
+ <Previewer content={getMessageContent(message.content)} />
36
+ <UsageMetadata
37
+ response_metadata={message.response_metadata as any}
38
+ usage_metadata={message.usage_metadata || {}}
39
+ spend_time={message.spend_time}
40
+ id={message.id}
41
+ tool_call_id={message.tool_call_id}
42
+ />
43
+ </div>
44
+ )}
34
45
  </div>
46
+ )}
47
+ </div>
48
+ );
49
+ };
50
+
51
+ const Previewer = ({ content }: { content: string }) => {
52
+ const validJSON = () => {
53
+ try {
54
+ JSON.parse(content);
55
+ return true;
56
+ } catch (e) {
57
+ return false;
58
+ }
59
+ };
60
+ const isJSON = content.startsWith("{") && content.endsWith("}") && validJSON();
61
+ const isMarkdown = content.includes("#") || content.includes("```") || content.includes("*");
62
+ const [jsonMode, setJsonMode] = useState(false);
63
+ const [markdownMode, setMarkdownMode] = useState(false);
64
+
65
+ return (
66
+ <div className={`flex flex-col`}>
67
+ <div className="flex gap-2 mb-2">
68
+ {isJSON && (
69
+ <button onClick={() => setJsonMode(!jsonMode)} className="px-2 py-1 text-xs font-medium text-gray-600 bg-gray-100 rounded hover:bg-gray-200 transition-colors">
70
+ json
71
+ </button>
72
+ )}
73
+ {isMarkdown && (
74
+ <button onClick={() => setMarkdownMode(!markdownMode)} className="px-2 py-1 text-xs font-medium text-gray-600 bg-gray-100 rounded hover:bg-gray-200 transition-colors">
75
+ markdown
76
+ </button>
77
+ )}
78
+ </div>
35
79
 
36
- {!isCollapsed && (
37
- <div className="tool-content">
38
- <div className="tool-input">{message.tool_input}</div>
39
- <div className="tool-output">{getMessageContent(message.content)}</div>
40
- <UsageMetadata
41
- response_metadata={message.response_metadata as any}
42
- usage_metadata={message.usage_metadata || {}}
43
- spend_time={message.spend_time}
44
- id={message.id}
45
- tool_call_id={message.tool_call_id}
46
- />
80
+ <div className="flex flex-col max-h-[300px] overflow-auto border border-gray-200 rounded p-2 w-full text-xs font-mono whitespace-pre-wrap">
81
+ {jsonMode && isJSON ? (
82
+ <pre className="whitespace-pre-wrap">{JSON.stringify(JSON.parse(content), null, 2)}</pre>
83
+ ) : markdownMode && isMarkdown ? (
84
+ <div className="markdown-body">
85
+ <Markdown remarkPlugins={[remarkGfm]}>{content}</Markdown>
47
86
  </div>
87
+ ) : (
88
+ <pre className="whitespace-pre-wrap">{content}</pre>
48
89
  )}
49
90
  </div>
50
91
  </div>
@@ -12,31 +12,50 @@ interface UsageMetadataProps {
12
12
  id?: string;
13
13
  }
14
14
 
15
+ const formatTokens = (tokens: number) => {
16
+ return tokens.toString();
17
+ };
15
18
  export const UsageMetadata: React.FC<UsageMetadataProps> = ({ usage_metadata, spend_time, response_metadata, id, tool_call_id }) => {
16
- const formatTokens = (tokens: number) => {
17
- return tokens.toString();
18
- };
19
-
19
+ const speed = spend_time ? ((usage_metadata.output_tokens || 0) * 1000) / (spend_time || 1) : 0;
20
20
  return (
21
- <div className="message-meta">
22
- <div className="token-info">
23
- <span className="token-item">
24
- <span className="token-emoji">📥</span>
25
- {formatTokens(usage_metadata.input_tokens || 0)}
26
- </span>
27
- <span className="token-item">
28
- <span className="token-emoji">📤</span>
29
- {formatTokens(usage_metadata.output_tokens || 0)}
30
- </span>
31
- <span className="token-item">
32
- <span className="token-emoji">📊</span>
33
- {formatTokens(usage_metadata.total_tokens || 0)}
34
- </span>
21
+ <div className="flex items-center justify-between text-xs text-gray-500 mt-2">
22
+ <div className="flex items-center gap-3">
23
+ <TokenPanel usage_metadata={usage_metadata} />
24
+ {spend_time && <span className="text-gray-500">{(spend_time / 1000).toFixed(2)}s</span>}
25
+ {speed && <span className="text-gray-500">{speed.toFixed(2)} TPS</span>}
35
26
  </div>
36
- <div>{response_metadata?.model_name}</div>
37
- <span className="message-time">{spend_time ? `${(spend_time / 1000).toFixed(2)}s` : ""}</span>
38
- {tool_call_id && <span className="tool-call-id">Tool: {tool_call_id}</span>}
39
- {id && <span className="message-id">ID: {id}</span>}
27
+ <div className="flex items-center gap-2">
28
+ {response_metadata?.model_name && <span className="text-gray-600">{response_metadata.model_name}</span>}
29
+ {tool_call_id && <span className="text-gray-500">Tool: {tool_call_id}</span>}
30
+ {id && <span className="text-gray-500">ID: {id}</span>}
31
+ </div>
32
+ </div>
33
+ );
34
+ };
35
+
36
+ export const TokenPanel = ({ usage_metadata }: any) => {
37
+ return (
38
+ <div className="flex items-center gap-1">
39
+ <span className="flex items-center gap-1 group relative">
40
+ <span>📊</span>
41
+ {formatTokens(usage_metadata.total_tokens || 0)}
42
+ <div className="hidden group-hover:block absolute bottom-full ml-2 bg-gray-100 p-2 rounded text-xs shadow border">
43
+ <div className="flex flex-col gap-1">
44
+ <div className="flex flex-col gap-1">
45
+ <span className="flex items-center gap-1">
46
+ <span>📥</span>
47
+ {formatTokens(usage_metadata.input_tokens || 0)}
48
+ <div> {JSON.stringify(usage_metadata.prompt_tokens_details)}</div>
49
+ </span>
50
+ <span className="flex items-center gap-1">
51
+ <span>📤</span>
52
+ {formatTokens(usage_metadata.output_tokens || 0)}
53
+ <div> {JSON.stringify(usage_metadata.completion_tokens_details)}</div>
54
+ </span>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </span>
40
59
  </div>
41
60
  );
42
61
  };
@@ -20,11 +20,20 @@ import { useStore } from "@nanostores/react";
20
20
  export const ChatProvider: React.FC<ChatProviderProps> = ({ children }) => {
21
21
  const store = useUnionStore(globalChatStore, useStore);
22
22
  useEffect(() => {
23
- store.initClient().then((res) => {
24
- if (store.showHistory) {
25
- store.refreshHistoryList();
26
- }
27
- });
23
+ store
24
+ .initClient()
25
+ .then((res) => {
26
+ if (store.showHistory) {
27
+ store.refreshHistoryList();
28
+ }
29
+ console.log(res);
30
+ })
31
+ .catch((err) => {
32
+ console.error(err);
33
+ const agentName = prompt("Failed to initialize chat client: " + store.currentAgent + "\n请输入 agent 名称");
34
+ localStorage.setItem("agent_name", agentName!);
35
+ location.reload();
36
+ });
28
37
  }, []);
29
38
 
30
39
  return <ChatContext.Provider value={store}>{children}</ChatContext.Provider>;
@@ -1,4 +1,8 @@
1
1
  import { createChatStore } from "@langgraph-js/sdk";
2
+ import { ask_user_for_approve, update_plan, web_search_tool } from "../tools/index";
3
+ import { FullTextSearchService, OpenAIVectorizer, VecDB, createMemoryTool } from "../../memory/index";
4
+ import { create_artifacts } from "../tools/create_artifacts";
5
+
2
6
  const F =
3
7
  localStorage.getItem("withCredentials") === "true"
4
8
  ? (url: string, options: RequestInit) => {
@@ -18,7 +22,26 @@ export const setLocalConfig = (config: Partial<{ showHistory: boolean; showGraph
18
22
  localStorage.setItem(key, value.toString());
19
23
  });
20
24
  };
21
- import { askUserTool } from "../tools";
25
+
26
+ // const vectorizer = new OpenAIVectorizer("text-embedding-3-small", {
27
+ // apiKey: import.meta.env.VITE_MEMORY_API_KEY,
28
+ // apiEndpoint: import.meta.env.VITE_MEMORY_API_ENDPOINT,
29
+ // });
30
+ // const db = new VecDB({
31
+ // vectorizer,
32
+ // dbName: "memory_db",
33
+ // dbVersion: 1,
34
+ // storeName: "memory",
35
+ // });
36
+ const db = new FullTextSearchService({
37
+ dbName: "memory_fulltext_db",
38
+ dbVersion: 1,
39
+ storeName: "memory",
40
+ });
41
+ db.initialize();
42
+ console.log(db);
43
+ export const memoryTool = createMemoryTool(db);
44
+
22
45
  export const globalChatStore = createChatStore(
23
46
  localStorage.getItem("agent_name") || "",
24
47
  {
@@ -32,7 +55,7 @@ export const globalChatStore = createChatStore(
32
55
  {
33
56
  ...getLocalConfig(),
34
57
  onInit(client) {
35
- client.tools.bindTools([askUserTool]);
58
+ client.tools.bindTools([create_artifacts, web_search_tool, ask_user_for_approve, update_plan, memoryTool.manageMemory, memoryTool.searchMemory]);
36
59
  },
37
60
  }
38
61
  );
@@ -0,0 +1,80 @@
1
+ import { ToolManager, ToolRenderData, createToolUI } from "@langgraph-js/sdk";
2
+ import { useState } from "react";
3
+ import { CheckCircle2, XCircle, Search } from "lucide-react";
4
+
5
+ interface RenderResponse {
6
+ approved: boolean;
7
+ feedback: string | null;
8
+ }
9
+
10
+ export const ask_user_for_approve = createToolUI({
11
+ name: "ask_user_for_approve",
12
+ description: "Request user review and approval for plans or content, wait for user feedback before proceeding",
13
+ parameters: [
14
+ {
15
+ name: "title",
16
+ type: "string",
17
+ description: "Title or subject of the content to be reviewed",
18
+ },
19
+ ],
20
+ onlyRender: true,
21
+ handler: ToolManager.waitForUIDone,
22
+ render(tool: ToolRenderData<{ title: string }, RenderResponse>) {
23
+ const data = tool.getInputRepaired();
24
+ const [feedback, setFeedback] = useState(tool.getJSONOutputSafe()?.feedback || "");
25
+
26
+ const handleApprove = () => {
27
+ const result = {
28
+ approved: true,
29
+ feedback: feedback.trim() || null,
30
+ };
31
+ tool.response(result);
32
+ };
33
+
34
+ const handleReject = () => {
35
+ const result = {
36
+ approved: false,
37
+ feedback: feedback.trim() || "用户拒绝了请求",
38
+ };
39
+ tool.response(result);
40
+ };
41
+
42
+ return (
43
+ <div className="flex flex-col gap-3 p-4 bg-white rounded-lg border border-gray-200">
44
+ <div className="flex items-center gap-2 text-gray-700">
45
+ <Search className="w-4 h-4 text-blue-500" />
46
+ <span>请求审核批准</span>
47
+ {data.title && <span className="text-sm text-gray-500 ml-2 truncate max-w-[200px]">{data.title}</span>}
48
+ </div>
49
+
50
+ <div className="flex gap-2">
51
+ <textarea
52
+ disabled={tool.state === "done"}
53
+ className="flex-1 p-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-50 disabled:text-gray-500"
54
+ placeholder="反馈意见(可选)"
55
+ value={feedback}
56
+ onChange={(e) => setFeedback(e.target.value)}
57
+ rows={1}
58
+ />
59
+
60
+ <button
61
+ disabled={tool.state === "done"}
62
+ className="px-3 py-2 text-sm font-medium text-white bg-green-500 rounded-lg hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
63
+ onClick={handleApprove}
64
+ >
65
+ <CheckCircle2 className="w-4 h-4" />
66
+ 批准
67
+ </button>
68
+ <button
69
+ disabled={tool.state === "done"}
70
+ className="px-3 py-2 text-sm font-medium text-white bg-red-500 rounded-lg hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
71
+ onClick={handleReject}
72
+ >
73
+ <XCircle className="w-4 h-4" />
74
+ 拒绝
75
+ </button>
76
+ </div>
77
+ </div>
78
+ );
79
+ },
80
+ });
@@ -0,0 +1,50 @@
1
+ import { createToolUI, ToolRenderData } from "@langgraph-js/sdk";
2
+ import { FileIcon } from "lucide-react";
3
+ import { useState } from "react";
4
+ import { useArtifacts } from "../../artifacts/ArtifactsContext";
5
+
6
+ interface ArtifactsInput {
7
+ filename: string;
8
+ filetype: string;
9
+ code: string;
10
+ }
11
+
12
+ interface ArtifactsResponse {
13
+ success: boolean;
14
+ message: string;
15
+ artifactsPath?: string;
16
+ }
17
+
18
+ export const create_artifacts = createToolUI({
19
+ name: "create_artifacts",
20
+ description: "创建并保存代码文件到 artifacts 目录",
21
+ parameters: [],
22
+ onlyRender: true,
23
+ render(tool: ToolRenderData<ArtifactsInput, ArtifactsResponse>) {
24
+ const data = tool.getInputRepaired();
25
+ const { setCurrentArtifactById, currentArtifact } = useArtifacts();
26
+
27
+ const toggleExpand = () => {
28
+ setCurrentArtifactById(tool.message.id!);
29
+ };
30
+
31
+ return (
32
+ <div className="p-4 space-y-4">
33
+ <div className="text-sm text-gray-500">
34
+ 创建文件: {data.filename}.{data.filetype}
35
+ </div>
36
+ <div className="border rounded-lg p-2 hover:bg-gray-50">
37
+ <div className="flex items-center justify-between select-none cursor-pointer" onClick={toggleExpand}>
38
+ <div className="flex items-center gap-2">
39
+ <FileIcon className="w-4 h-4" />
40
+ <span className="font-xs">{data.filename}</span>
41
+ </div>
42
+ <span className="text-gray-400">
43
+ {data.filetype} {currentArtifact?.id === tool.message.id ? "▼" : "▶"}
44
+ </span>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ );
49
+ },
50
+ });
@@ -0,0 +1,5 @@
1
+ export { ask_user_for_approve } from "./ask_user_for_approve";
2
+ export { update_plan } from "./update_plan";
3
+ export { web_search_tool } from "./web_search_tool";
4
+ // 在这里添加其他工具的导出
5
+ // export { other_tool } from "./other_tool";
@@ -0,0 +1,75 @@
1
+ import { ToolManager, ToolRenderData, createToolUI } from "@langgraph-js/sdk";
2
+ import { FileEdit, Globe, Brain } from "lucide-react";
3
+
4
+ interface Step {
5
+ need_web_search: boolean;
6
+ title: string;
7
+ description: string;
8
+ step_type: "research" | "processing";
9
+ execution_res?: string;
10
+ }
11
+
12
+ interface Plan {
13
+ locale: string;
14
+ has_enough_context: boolean;
15
+ thought: string;
16
+ title?: string;
17
+ steps: Step[];
18
+ }
19
+
20
+ export const update_plan = createToolUI({
21
+ name: "update_plan",
22
+ description: "展示当前执行计划,等待用户确认",
23
+ parameters: [],
24
+ onlyRender: true,
25
+ render(tool: ToolRenderData<Plan, string>) {
26
+ const data = tool.getInputRepaired();
27
+ const plan = data || {
28
+ locale: "zh-CN",
29
+ has_enough_context: false,
30
+ thought: "",
31
+ steps: [],
32
+ };
33
+
34
+ return (
35
+ <div className="p-3 bg-white rounded-lg border border-gray-200">
36
+ <div className="flex items-center gap-1.5 text-gray-700 mb-2 font-bold">
37
+ <FileEdit className="w-3.5 h-3.5 text-blue-500" />
38
+ <span>执行计划</span>
39
+ </div>
40
+
41
+ <div className="space-y-2">
42
+ {plan.title && (
43
+ <div className="text-sm">
44
+ <span className="text-gray-500">标题:</span>
45
+ <span>{plan.title}</span>
46
+ </div>
47
+ )}
48
+
49
+ {plan.thought && (
50
+ <div className="text-sm">
51
+ <span className="text-gray-500">思考:</span>
52
+ <span className="whitespace-pre-wrap">{plan.thought}</span>
53
+ </div>
54
+ )}
55
+
56
+ {plan.steps && plan.steps.length > 0 && (
57
+ <div className="space-y-1.5">
58
+ <div className="text-sm text-gray-500">步骤:</div>
59
+ {plan.steps.map((step, index) => (
60
+ <div key={index} className="pl-2 border-l-2 border-gray-200">
61
+ <div className="flex items-center gap-1.5 text-sm">
62
+ {step!.step_type === "research" ? <Globe className="w-3.5 h-3.5 text-blue-500" /> : <Brain className="w-3.5 h-3.5 text-purple-500" />}
63
+ <span className="font-medium">{step!.title}</span>
64
+ {step!.need_web_search && <span className="text-xs text-blue-500">[搜索]</span>}
65
+ </div>
66
+ <div className="text-sm text-gray-600 pl-5">{step!.description}</div>
67
+ </div>
68
+ ))}
69
+ </div>
70
+ )}
71
+ </div>
72
+ </div>
73
+ );
74
+ },
75
+ });
@@ -0,0 +1,89 @@
1
+ // 入参 {"query":"Gemini Diffusion vs other diffusion models advantages disadvantages unique features"}
2
+
3
+ import { createToolUI, ToolRenderData } from "@langgraph-js/sdk";
4
+ import { LinkIcon } from "lucide-react";
5
+ import { useState } from "react";
6
+
7
+ interface SearchResult {
8
+ title: string;
9
+ url: string;
10
+ description: string;
11
+ updateTime: string;
12
+ metadata: {
13
+ engines: string[];
14
+ };
15
+ }
16
+
17
+ interface RenderResponse {
18
+ engine: string;
19
+ results: SearchResult[];
20
+ }
21
+
22
+ interface SearchInput {
23
+ query: string;
24
+ }
25
+
26
+ export const web_search_tool = createToolUI({
27
+ name: "web_search",
28
+ description:
29
+ "A powerful web search tool that provides comprehensive, real-time results using search engine. Returns relevant web content with customizable parameters for result count, content type, and domain filtering. Ideal for gathering current information, news, and detailed web content analysis.",
30
+ parameters: [],
31
+ onlyRender: true,
32
+ render(tool: ToolRenderData<SearchInput, RenderResponse[]>) {
33
+ const data = tool.getInputRepaired();
34
+ const feedback = tool.getJSONOutputSafe()?.flatMap((i) => i.results) || [];
35
+ const [expandedItems, setExpandedItems] = useState<Set<number>>(new Set());
36
+
37
+ const toggleExpand = (index: number) => {
38
+ const newExpanded = new Set(expandedItems);
39
+ if (newExpanded.has(index)) {
40
+ newExpanded.delete(index);
41
+ } else {
42
+ newExpanded.add(index);
43
+ }
44
+ setExpandedItems(newExpanded);
45
+ };
46
+
47
+ const openLink = (url: string) => {
48
+ window.open(url, "_blank", "noopener,noreferrer");
49
+ };
50
+
51
+ return (
52
+ <div className="p-4 space-y-4">
53
+ <div className="text-sm text-gray-500">
54
+ Search Query: {data.query};Get {feedback.length} results
55
+ </div>
56
+ <div className="space-y-3 max-h-[300px] overflow-y-auto">
57
+ {feedback.map((result, index) => (
58
+ <div key={index} className="border rounded-lg p-2 hover:bg-gray-50">
59
+ <div className="flex items-center justify-between select-none cursor-pointer" onClick={() => toggleExpand(index)}>
60
+ <span className="font-xs flex-1">{result.title}</span>
61
+ <div
62
+ onClick={(e) => {
63
+ e.stopPropagation();
64
+ openLink(result.url);
65
+ }}
66
+ className="px-3 py-1 text-sm"
67
+ >
68
+ <LinkIcon className="w-4 h-4" />
69
+ </div>
70
+ <span className="text-gray-400">{expandedItems.has(index) ? "▼" : "▶"}</span>
71
+ </div>
72
+
73
+ {expandedItems.has(index) && (
74
+ <div className="mt-3 space-y-2">
75
+ <p className="text-sm text-gray-600">{result.description}</p>
76
+ <div className="flex items-center gap-2 text-xs text-gray-500">
77
+ <span>{new Date(result.updateTime).toLocaleDateString()}</span>
78
+ <span>•</span>
79
+ <span>{result.metadata.engines.join(", ")}</span>
80
+ </div>
81
+ </div>
82
+ )}
83
+ </div>
84
+ ))}
85
+ </div>
86
+ </div>
87
+ );
88
+ },
89
+ });
@@ -8,7 +8,7 @@ import { AssistantGraph } from "@langgraph-js/sdk";
8
8
  import "./flow.css";
9
9
  const nodeTypes = {
10
10
  group: ({ data }: { data: any }) => (
11
- <div style={{ position: "absolute", bottom: "100%", left: 0 }}>
11
+ <div className="absolute bottom-full left-0">
12
12
  <span>{data.name}</span>
13
13
  </div>
14
14
  ),
@@ -24,7 +24,6 @@ const transformEdges = (edges: AssistantGraph["edges"], nodes: Node[]): Edge[] =
24
24
  id: `${sourceId}=${targetId}`,
25
25
  source: sourceId!,
26
26
  target: targetId!,
27
- // type: edge.conditional ? "smoothstep" : "straight",
28
27
  animated: edge.conditional,
29
28
  label: edge.data,
30
29
  style: {
@@ -84,13 +83,17 @@ const LayoutFlow = () => {
84
83
  }
85
84
  }, [currentNodeName]);
86
85
  return (
87
- <div style={{ width: "30%", height: "100%", position: "relative", overflow: "hidden" }}>
86
+ <div className="w-1/3 h-full relative overflow-hidden border-l">
88
87
  <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} fitView className="w-full h-full" nodeTypes={nodeTypes}>
89
88
  <Background />
90
89
  <Controls />
91
- <Panel position="top-right">
92
- <button onClick={() => onLayout("TB")}>垂直布局</button>
93
- <button onClick={() => onLayout("LR")}>水平布局</button>
90
+ <Panel position="top-right" className="flex gap-2">
91
+ <button onClick={() => onLayout("TB")} className="px-3 py-1.5 bg-white border border-gray-200 rounded-lg text-sm text-gray-700 hover:bg-gray-50 transition-colors">
92
+ 垂直布局
93
+ </button>
94
+ <button onClick={() => onLayout("LR")} className="px-3 py-1.5 bg-white border border-gray-200 rounded-lg text-sm text-gray-700 hover:bg-gray-50 transition-colors">
95
+ 水平布局
96
+ </button>
94
97
  </Panel>
95
98
  </ReactFlow>
96
99
  </div>
package/src/index.ts CHANGED
@@ -1 +1,2 @@
1
+ import "virtual:uno.css";
1
2
  export { default as Chat } from "./chat/Chat";