@langgraph-js/ui 1.5.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 (47) 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 +98 -31
  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 +55 -8
  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 +25 -13
  27. package/src/chat/tools/create_artifacts.tsx +50 -0
  28. package/src/chat/tools/index.ts +3 -2
  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-CLyKQAUN.js +0 -214
  42. package/dist/assets/index-D80TEgwy.css +0 -1
  43. package/src/chat/chat.css +0 -552
  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/login/Login.css +0 -93
@@ -1,7 +1,10 @@
1
- import React, { JSX } 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
4
  import { useChat } from "../context/ChatContext";
5
+ import Markdown from "react-markdown";
6
+ import remarkGfm from "remark-gfm";
7
+
5
8
  interface MessageToolProps {
6
9
  message: ToolMessage & RenderMessage;
7
10
  client: LangGraphClient;
@@ -15,21 +18,21 @@ const MessageTool: React.FC<MessageToolProps> = ({ message, client, getMessageCo
15
18
  const { getToolUIRender } = useChat();
16
19
  const render = getToolUIRender(message.name!);
17
20
  return (
18
- <div className="message tool">
21
+ <div className="flex flex-col w-full">
19
22
  {render ? (
20
23
  (render(message) as JSX.Element)
21
24
  ) : (
22
- <div className="tool-message">
23
- <div className="tool-header" onClick={onToggleCollapse}>
24
- <div className="tool-title" onClick={() => console.log(message)}>
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)}>
25
28
  {message.node_name} | {message.name}
26
29
  </div>
27
30
  </div>
28
31
 
29
32
  {!isCollapsed && (
30
- <div className="tool-content">
31
- <div className="tool-input">{message.tool_input}</div>
32
- <div className="tool-output">{getMessageContent(message.content)}</div>
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)} />
33
36
  <UsageMetadata
34
37
  response_metadata={message.response_metadata as any}
35
38
  usage_metadata={message.usage_metadata || {}}
@@ -45,4 +48,48 @@ const MessageTool: React.FC<MessageToolProps> = ({ message, client, getMessageCo
45
48
  );
46
49
  };
47
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>
79
+
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>
86
+ </div>
87
+ ) : (
88
+ <pre className="whitespace-pre-wrap">{content}</pre>
89
+ )}
90
+ </div>
91
+ </div>
92
+ );
93
+ };
94
+
48
95
  export default MessageTool;
@@ -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,5 +1,8 @@
1
1
  import { createChatStore } from "@langgraph-js/sdk";
2
- import { ask_user_for_approve } from "../tools/index";
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
+
3
6
  const F =
4
7
  localStorage.getItem("withCredentials") === "true"
5
8
  ? (url: string, options: RequestInit) => {
@@ -19,6 +22,26 @@ export const setLocalConfig = (config: Partial<{ showHistory: boolean; showGraph
19
22
  localStorage.setItem(key, value.toString());
20
23
  });
21
24
  };
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([ask_user_for_approve]);
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
  );
@@ -1,5 +1,6 @@
1
1
  import { ToolManager, ToolRenderData, createToolUI } from "@langgraph-js/sdk";
2
2
  import { useState } from "react";
3
+ import { CheckCircle2, XCircle, Search } from "lucide-react";
3
4
 
4
5
  interface RenderResponse {
5
6
  approved: boolean;
@@ -18,9 +19,9 @@ export const ask_user_for_approve = createToolUI({
18
19
  ],
19
20
  onlyRender: true,
20
21
  handler: ToolManager.waitForUIDone,
21
- render(tool: ToolRenderData<RenderResponse>) {
22
- const data = tool.input || {};
23
- const [feedback, setFeedback] = useState("");
22
+ render(tool: ToolRenderData<{ title: string }, RenderResponse>) {
23
+ const data = tool.getInputRepaired();
24
+ const [feedback, setFeedback] = useState(tool.getJSONOutputSafe()?.feedback || "");
24
25
 
25
26
  const handleApprove = () => {
26
27
  const result = {
@@ -39,27 +40,38 @@ export const ask_user_for_approve = createToolUI({
39
40
  };
40
41
 
41
42
  return (
42
- <div className="approval-prompt-compact">
43
- <div className="approval-header-compact">
44
- <span>🔍请求审核批准</span>
45
- {data.title && <span className="approval-title">{data.title}</span>}
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>}
46
48
  </div>
47
49
 
48
- <div className="feedback-input-compact">
50
+ <div className="flex gap-2">
49
51
  <textarea
50
52
  disabled={tool.state === "done"}
51
- className="feedback-textarea-compact"
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"
52
54
  placeholder="反馈意见(可选)"
53
55
  value={feedback}
54
56
  onChange={(e) => setFeedback(e.target.value)}
55
57
  rows={1}
56
58
  />
57
59
 
58
- <button disabled={tool.state === "done"} className="approve-button-compact" onClick={handleApprove}>
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
+ 批准
60
67
  </button>
61
- <button disabled={tool.state === "done"} className="reject-button-compact" onClick={handleReject}>
62
- 拒绝
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
+ 拒绝
63
75
  </button>
64
76
  </div>
65
77
  </div>
@@ -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
+ });
@@ -1,4 +1,5 @@
1
1
  export { ask_user_for_approve } from "./ask_user_for_approve";
2
-
2
+ export { update_plan } from "./update_plan";
3
+ export { web_search_tool } from "./web_search_tool";
3
4
  // 在这里添加其他工具的导出
4
- // export { other_tool } from "./other_tool";
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";