@langgraph-js/ui 1.5.0 → 1.7.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-C0dczJ0v.css +1 -0
  4. package/dist/assets/index-JlVlMqZ-.js +248 -0
  5. package/dist/index.html +3 -5
  6. package/index.html +1 -3
  7. package/package.json +11 -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 +100 -32
  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 +196 -48
  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 +20 -19
  21. package/src/chat/components/MessageHuman.tsx +13 -10
  22. package/src/chat/components/MessageTool.tsx +85 -8
  23. package/src/chat/components/UsageMetadata.tsx +41 -22
  24. package/src/chat/context/ChatContext.tsx +14 -5
  25. package/src/chat/index.css +4 -0
  26. package/src/chat/store/index.ts +25 -2
  27. package/src/chat/tools/ask_user_for_approve.tsx +25 -13
  28. package/src/chat/tools/create_artifacts.tsx +50 -0
  29. package/src/chat/tools/index.ts +3 -2
  30. package/src/chat/tools/update_plan.tsx +75 -0
  31. package/src/chat/tools/web_search_tool.tsx +89 -0
  32. package/src/graph/index.tsx +9 -6
  33. package/src/index.ts +2 -0
  34. package/src/login/Login.tsx +155 -47
  35. package/src/memory/BaseDB.ts +92 -0
  36. package/src/memory/db.ts +232 -0
  37. package/src/memory/fulltext-search.ts +191 -0
  38. package/src/memory/index.ts +4 -0
  39. package/src/memory/tools.ts +170 -0
  40. package/test/main.tsx +2 -2
  41. package/vite.config.ts +7 -1
  42. package/dist/assets/index-CLyKQAUN.js +0 -214
  43. package/dist/assets/index-D80TEgwy.css +0 -1
  44. package/src/chat/chat.css +0 -552
  45. package/src/chat/components/FileList.css +0 -129
  46. package/src/chat/components/JsonEditorPopup.css +0 -81
  47. package/src/chat/components/JsonToMessage/JsonToMessage.css +0 -104
  48. package/src/login/Login.css +0 -93
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+
2
3
  interface MessageHumanProps {
3
4
  content: string | any[];
4
5
  }
@@ -6,7 +7,7 @@ interface MessageHumanProps {
6
7
  const MessageHuman: React.FC<MessageHumanProps> = ({ content }) => {
7
8
  const renderContent = () => {
8
9
  if (typeof content === "string") {
9
- return <div className="message-text">{content}</div>;
10
+ return <div className="text-white whitespace-pre-wrap">{content}</div>;
10
11
  }
11
12
 
12
13
  if (Array.isArray(content)) {
@@ -14,27 +15,27 @@ const MessageHuman: React.FC<MessageHumanProps> = ({ content }) => {
14
15
  switch (item.type) {
15
16
  case "text":
16
17
  return (
17
- <div key={index} className="message-text">
18
+ <div key={index} className="text-white">
18
19
  {item.text}
19
20
  </div>
20
21
  );
21
22
  case "image_url":
22
23
  return (
23
- <div key={index} className="message-image">
24
- <img src={item.image_url.url} alt="用户上传的图片" style={{ maxWidth: "200px", borderRadius: "4px" }} />
24
+ <div key={index} className="mt-2">
25
+ <img src={item.image_url.url} alt="用户上传的图片" className="max-w-[200px] rounded" />
25
26
  </div>
26
27
  );
27
28
  case "audio":
28
29
  return (
29
- <div key={index} className="message-audio">
30
- <audio controls src={item.audio_url}>
30
+ <div key={index} className="mt-2">
31
+ <audio controls src={item.audio_url} className="w-full">
31
32
  您的浏览器不支持音频播放
32
33
  </audio>
33
34
  </div>
34
35
  );
35
36
  default:
36
37
  return (
37
- <div key={index} className="message-text">
38
+ <div key={index} className="text-white whitespace-pre-wrap">
38
39
  {JSON.stringify(item)}
39
40
  </div>
40
41
  );
@@ -42,12 +43,14 @@ const MessageHuman: React.FC<MessageHumanProps> = ({ content }) => {
42
43
  });
43
44
  }
44
45
 
45
- return <div className="message-text">{JSON.stringify(content)}</div>;
46
+ return <div className="text-white whitespace-pre-wrap">{JSON.stringify(content)}</div>;
46
47
  };
47
48
 
48
49
  return (
49
- <div className="message human">
50
- <div className="message-content">{renderContent()}</div>
50
+ <div className="flex flex-row w-full justify-end">
51
+ <div className="flex flex-col w-fit bg-blue-500 rounded-lg text-white border border-blue-100">
52
+ <div className="flex flex-col p-4 ">{renderContent()}</div>
53
+ </div>
51
54
  </div>
52
55
  );
53
56
  };
@@ -1,7 +1,13 @@
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
+ import { Highlight, themes } from "prism-react-renderer";
8
+
9
+ const TOOL_COLORS = ["border-red-400", "border-blue-400", "border-green-500", "border-yellow-400", "border-purple-400", "border-pink-400", "border-indigo-400"];
10
+
5
11
  interface MessageToolProps {
6
12
  message: ToolMessage & RenderMessage;
7
13
  client: LangGraphClient;
@@ -11,25 +17,35 @@ interface MessageToolProps {
11
17
  onToggleCollapse: () => void;
12
18
  }
13
19
 
20
+ const getToolColorClass = (tool_name: string) => {
21
+ let hash = 0;
22
+ for (let i = 0; i < tool_name.length; i++) {
23
+ hash = tool_name.charCodeAt(i) + ((hash << 5) - hash);
24
+ }
25
+ const index = Math.abs(hash % TOOL_COLORS.length);
26
+ return TOOL_COLORS[index];
27
+ };
28
+
14
29
  const MessageTool: React.FC<MessageToolProps> = ({ message, client, getMessageContent, formatTokens, isCollapsed, onToggleCollapse }) => {
15
30
  const { getToolUIRender } = useChat();
16
31
  const render = getToolUIRender(message.name!);
32
+ const borderColorClass = getToolColorClass(message.name!);
17
33
  return (
18
- <div className="message tool">
34
+ <div className="flex flex-col w-full">
19
35
  {render ? (
20
36
  (render(message) as JSX.Element)
21
37
  ) : (
22
- <div className="tool-message">
23
- <div className="tool-header" onClick={onToggleCollapse}>
24
- <div className="tool-title" onClick={() => console.log(message)}>
38
+ <div className={`flex flex-col w-full bg-white rounded-lg shadow-sm border-2 ${borderColorClass} overflow-hidden`}>
39
+ <div className="flex items-center justify-between p-3 cursor-pointer hover:bg-gray-100 transition-colors" onClick={onToggleCollapse}>
40
+ <div className="text-sm font-medium text-gray-700" onClick={() => console.log(message)}>
25
41
  {message.node_name} | {message.name}
26
42
  </div>
27
43
  </div>
28
44
 
29
45
  {!isCollapsed && (
30
- <div className="tool-content">
31
- <div className="tool-input">{message.tool_input}</div>
32
- <div className="tool-output">{getMessageContent(message.content)}</div>
46
+ <div className="flex flex-col gap-4 p-4 border-t border-gray-100">
47
+ <Previewer content={message.tool_input || ""} />
48
+ <Previewer content={getMessageContent(message.content)} />
33
49
  <UsageMetadata
34
50
  response_metadata={message.response_metadata as any}
35
51
  usage_metadata={message.usage_metadata || {}}
@@ -45,4 +61,65 @@ const MessageTool: React.FC<MessageToolProps> = ({ message, client, getMessageCo
45
61
  );
46
62
  };
47
63
 
64
+ const Previewer = ({ content }: { content: string }) => {
65
+ const validJSON = () => {
66
+ try {
67
+ JSON.parse(content);
68
+ return true;
69
+ } catch (e) {
70
+ return false;
71
+ }
72
+ };
73
+ const isJSON = content.startsWith("{") && content.endsWith("}") && validJSON();
74
+ const isMarkdown = content.includes("#") || content.includes("```") || content.includes("*");
75
+ const [jsonMode, setJsonMode] = useState(isJSON);
76
+ const [markdownMode, setMarkdownMode] = useState(false);
77
+ const copyToClipboard = () => {
78
+ navigator.clipboard.writeText(content);
79
+ };
80
+ return (
81
+ <div className={`flex flex-col`}>
82
+ <div className="flex gap-2 mb-2">
83
+ <button onClick={copyToClipboard} className="px-2 py-1 text-xs font-medium text-gray-600 bg-green-100 rounded hover:bg-green-200 transition-colors">
84
+ copy
85
+ </button>
86
+ {isJSON && (
87
+ <button onClick={() => setJsonMode(!jsonMode)} className="px-2 py-1 text-xs font-medium text-gray-600 bg-orange-100 rounded hover:bg-orange-200 transition-colors">
88
+ json
89
+ </button>
90
+ )}
91
+ {isMarkdown && (
92
+ <button onClick={() => setMarkdownMode(!markdownMode)} className="px-2 py-1 text-xs font-medium text-gray-600 bg-blue-100 rounded hover:bg-blue-200 transition-colors">
93
+ markdown
94
+ </button>
95
+ )}
96
+ </div>
97
+
98
+ <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">
99
+ {jsonMode && isJSON ? (
100
+ <Highlight code={JSON.stringify(JSON.parse(content), null, 2)} language="json" theme={themes.oneLight}>
101
+ {({ className, style, tokens, getLineProps, getTokenProps }) => (
102
+ <pre style={style}>
103
+ {tokens.map((line, i) => (
104
+ <div key={i} {...getLineProps({ line })}>
105
+ {line.map((token, key) => (
106
+ <span key={key} {...getTokenProps({ token })} />
107
+ ))}
108
+ </div>
109
+ ))}
110
+ </pre>
111
+ )}
112
+ </Highlight>
113
+ ) : markdownMode && isMarkdown ? (
114
+ <div className="markdown-body">
115
+ <Markdown remarkPlugins={[remarkGfm]}>{content}</Markdown>
116
+ </div>
117
+ ) : (
118
+ <pre className="whitespace-pre-wrap">{content}</pre>
119
+ )}
120
+ </div>
121
+ </div>
122
+ );
123
+ };
124
+
48
125
  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>;
@@ -0,0 +1,4 @@
1
+ .langgraph-chat-container .token-line {
2
+ white-space: pre-wrap;
3
+ overflow: hidden;
4
+ }
@@ -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
+ });