@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
@@ -0,0 +1,191 @@
1
+ // minisearch-idb-chinese-search.ts
2
+
3
+ import MiniSearch, { AsPlainObject, type Options, type SearchOptions, type SearchResult } from "minisearch";
4
+ import { openDB, type IDBPDatabase } from "idb";
5
+ import { BaseDB, BaseDBConfig } from "./BaseDB";
6
+ import { MemoryRecord } from "./db";
7
+
8
+ /**
9
+ * 搜索服务配置接口
10
+ */
11
+ export interface FullTextSearchConfig extends BaseDBConfig {
12
+ miniSearchOptions?: Partial<Options<MemoryRecord>>;
13
+ }
14
+
15
+ // 默认配置
16
+ const DEFAULT_CONFIG: Required<FullTextSearchConfig> = {
17
+ dbName: "minisearch_memory_db",
18
+ dbVersion: 1,
19
+ storeName: "memory",
20
+ miniSearchOptions: {
21
+ fields: ["text", "path", "tags", "referencePath"],
22
+ storeFields: ["text", "path", "tags", "referencePath"],
23
+ tokenize: chineseWordSegmenter,
24
+ },
25
+ };
26
+
27
+ // --- 1. 中文分词器 ---
28
+ /**
29
+ * 中文分词器,使用 Intl.Segmenter 将中文文本分割成词语。
30
+ * @param text 需要分词的中文文本。
31
+ * @returns 词语数组。
32
+ */
33
+ function chineseWordSegmenter(text: string): string[] {
34
+ // 移除所有空格,因为中文词语之间通常没有空格
35
+ const cleanText = text.replace(/ /g, "");
36
+
37
+ // 使用 Intl.Segmenter 进行词语分词
38
+ // 'zh-CN' 或 'cn' 都可以表示中文(中国大陆)
39
+ const segmenter = new Intl.Segmenter("zh-CN", { granularity: "word" });
40
+
41
+ // 获取分词结果并只保留词语内容
42
+ const segments = Array.from(segmenter.segment(cleanText)).map((item) => item.segment);
43
+
44
+ return segments;
45
+ }
46
+
47
+ export class FullTextSearchService extends BaseDB<MemoryRecord> {
48
+ private miniSearch!: MiniSearch<MemoryRecord>;
49
+ protected config: Required<FullTextSearchConfig>;
50
+ protected db!: IDBPDatabase;
51
+ constructor(config?: FullTextSearchConfig) {
52
+ const mergedConfig = {
53
+ ...DEFAULT_CONFIG,
54
+ ...config,
55
+ miniSearchOptions: {
56
+ ...DEFAULT_CONFIG.miniSearchOptions,
57
+ ...(config?.miniSearchOptions || {}),
58
+ },
59
+ };
60
+ super(mergedConfig);
61
+ this.config = mergedConfig;
62
+ }
63
+
64
+ public async initialize(): Promise<void> {
65
+ if (this.isInitialized) {
66
+ return;
67
+ }
68
+
69
+ const { storeName } = this.config;
70
+ this.db = await openDB(this.config.dbName, this.config.dbVersion, {
71
+ upgrade(db) {
72
+ if (!db.objectStoreNames.contains(storeName)) {
73
+ db.createObjectStore(storeName, { keyPath: "id", autoIncrement: true });
74
+ }
75
+ if (!db.objectStoreNames.contains("minisearch_index")) {
76
+ db.createObjectStore("minisearch_index");
77
+ }
78
+ },
79
+ });
80
+
81
+ // 确保 miniSearchOptions 包含所有必需的字段
82
+ const miniSearchOptions: Options<MemoryRecord> = {
83
+ fields: this.config.miniSearchOptions.fields || ["text"],
84
+ storeFields: this.config.miniSearchOptions.storeFields || ["text"],
85
+ tokenize: this.config.miniSearchOptions.tokenize || chineseWordSegmenter,
86
+ };
87
+
88
+ this.miniSearch = new MiniSearch(miniSearchOptions);
89
+
90
+ try {
91
+ const savedIndex = await this.loadMiniSearchIndex();
92
+ if (savedIndex) {
93
+ console.log(savedIndex);
94
+ this.miniSearch = await MiniSearch.loadJSAsync(savedIndex, miniSearchOptions);
95
+ console.log("MiniSearch index loaded from IndexedDB.");
96
+ } else {
97
+ console.log("No saved index found, building from documents...");
98
+ const allDocs = await this.getAll();
99
+ if (allDocs.length > 0) {
100
+ this.miniSearch.addAll(allDocs);
101
+ await this.saveMiniSearchIndex(this.miniSearch.toJSON());
102
+ console.log(`MiniSearch index built from ${allDocs.length} documents and saved.`);
103
+ } else {
104
+ console.log("No documents found, index is empty.");
105
+ }
106
+ }
107
+ this.isInitialized = true;
108
+ } catch (error) {
109
+ console.error("Failed to initialize FullTextSearchService:", error);
110
+ throw error;
111
+ }
112
+ }
113
+
114
+ public async insert(doc: Partial<MemoryRecord>): Promise<number> {
115
+ const transaction = this.db.transaction(this.config.storeName, "readwrite");
116
+ const store = transaction.objectStore(this.config.storeName);
117
+ const newDoc: MemoryRecord = { ...doc } as MemoryRecord;
118
+
119
+ const id = await store.add(newDoc);
120
+ newDoc.id = Number(id);
121
+
122
+ await transaction.done;
123
+
124
+ this.miniSearch.add(newDoc);
125
+ await this.saveMiniSearchIndex(this.miniSearch.toJSON());
126
+
127
+ return newDoc.id;
128
+ }
129
+
130
+ public async update(id: number, doc: Partial<MemoryRecord>): Promise<void> {
131
+ const transaction = this.db.transaction(this.config.storeName, "readwrite");
132
+ const store = transaction.objectStore(this.config.storeName);
133
+ const updatedDoc = { ...doc, id } as MemoryRecord;
134
+ await store.put(updatedDoc);
135
+
136
+ // MiniSearch 没有 update 方法,我们需要先删除再添加
137
+ const existingDoc = await store.get(id);
138
+ if (existingDoc) {
139
+ this.miniSearch.remove(existingDoc);
140
+ }
141
+ this.miniSearch.add(updatedDoc);
142
+ await this.saveMiniSearchIndex(this.miniSearch.toJSON());
143
+ }
144
+
145
+ public async delete(id: number): Promise<void> {
146
+ const transaction = this.db.transaction(this.config.storeName, "readwrite");
147
+ const store = transaction.objectStore(this.config.storeName);
148
+ const doc = await store.get(id);
149
+ if (doc) {
150
+ this.miniSearch.remove(doc);
151
+ }
152
+ await store.delete(id);
153
+ await this.saveMiniSearchIndex(this.miniSearch.toJSON());
154
+ }
155
+
156
+ public async query(query: string, options?: SearchOptions & { limit?: number }): Promise<MemoryRecord[]> {
157
+ const results = this.miniSearch.search(query, options);
158
+ // 将 SearchResult 转换为 MemoryRecord
159
+ return results.slice(0, options?.limit ?? 5).map((i) => {
160
+ const { id, text, ...rest } = i;
161
+ return {
162
+ id,
163
+ text,
164
+ tags: rest.tags,
165
+ path: rest.path,
166
+ type: rest.type,
167
+ score: Math.round(i.score),
168
+ referencePath: rest.referencePath,
169
+ } as unknown as MemoryRecord;
170
+ });
171
+ }
172
+
173
+ public async get(id: number): Promise<MemoryRecord | undefined> {
174
+ return this.db.transaction(this.config.storeName, "readonly").objectStore(this.config.storeName).get(id);
175
+ }
176
+
177
+ public async getAll(): Promise<MemoryRecord[]> {
178
+ return this.db.transaction(this.config.storeName, "readonly").objectStore(this.config.storeName).getAll();
179
+ }
180
+
181
+ private async loadMiniSearchIndex(): Promise<AsPlainObject | null> {
182
+ return this.db.transaction("minisearch_index", "readonly").objectStore("minisearch_index").get("indexData");
183
+ }
184
+
185
+ private async saveMiniSearchIndex(indexData: AsPlainObject): Promise<void> {
186
+ const tx = this.db.transaction("minisearch_index", "readwrite");
187
+ const store = tx.objectStore("minisearch_index");
188
+ await store.put(indexData, "indexData");
189
+ await tx.done;
190
+ }
191
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./tools";
2
+ export * from "./db";
3
+ export * from "./fulltext-search";
4
+ export * from "./BaseDB";
@@ -0,0 +1,170 @@
1
+ import { createToolUI } from "@langgraph-js/sdk";
2
+ import type { BaseDB, MemoryRecord } from "./BaseDB";
3
+
4
+ // 获取命名空间(这里简化为标识符)
5
+ function getNamespace(userId?: string): string {
6
+ return userId || "default";
7
+ }
8
+
9
+ // 确保 JSON 可序列化
10
+ function ensureJsonSerializable(content: any): any {
11
+ if (typeof content === "string" || typeof content === "number" || typeof content === "boolean" || content === null) {
12
+ return content;
13
+ }
14
+ if (Array.isArray(content) || typeof content === "object") {
15
+ return content;
16
+ }
17
+ return String(content);
18
+ }
19
+
20
+ export const createMemoryTool = (store: BaseDB<MemoryRecord>) => {
21
+ return {
22
+ manageMemory: createToolUI({
23
+ name: "manage_memory",
24
+ description: `这是一个**结构化**工具,用于**管理跨对话的持久化记忆**,以保留上下文、用户偏好和学习到的见解。它支持**创建、更新、检索和删除记忆条目**,从而在互动中保持连续性和个性化。
25
+
26
+ ### 你需要记录记忆的场景
27
+
28
+ 1. 请你第一时间敏锐记录下用户的细节偏好,包括对话风格、习惯、内容、方式等。
29
+ 2. 记录下高价值的问答,比如用户对你的纠正、任务过程中的关键信息
30
+
31
+
32
+ ### 核心功能
33
+
34
+ 该工具具备以下关键能力:
35
+
36
+ * **记忆生命周期管理:** 对记忆条目进行**全生命周期管理**,包括创建、更新、检索和删除。
37
+ * **分段与标签支持:** 支持**分段或打标签的记忆条目**,方便分类和高效检索。
38
+ * **存储选项灵活:** 提供**临时和永久两种存储选项**,以适应不同类型信息的存储需求。
39
+ `,
40
+ parameters: [
41
+ {
42
+ name: "path",
43
+ type: "string",
44
+ description: "记忆路径(创建时需要)",
45
+ },
46
+ {
47
+ name: "tags",
48
+ type: "string[]",
49
+ description: "记忆标签(创建或更新时需要)",
50
+ },
51
+ {
52
+ name: "content",
53
+ type: "string",
54
+ description: "记忆内容(创建或更新时需要)",
55
+ },
56
+ {
57
+ name: "action",
58
+ type: "string",
59
+ enum: ["create", "update", "delete"],
60
+ description: "操作类型:create(创建)、update(更新)、delete(删除)",
61
+ },
62
+ {
63
+ name: "id",
64
+ type: "string",
65
+ description: "记忆ID(更新或删除时需要,创建时会自动生成)",
66
+ required: false,
67
+ },
68
+ {
69
+ name: "referencePath",
70
+ type: "string",
71
+ description: "引用文档的 URL",
72
+ required: false,
73
+ },
74
+ ],
75
+ handler: async (args) => {
76
+ const { content, action, id, tags, path, referencePath } = args;
77
+
78
+ if ((action === "delete" || action === "update") && !id) {
79
+ throw new Error("删除或更新记忆时必须提供记忆ID");
80
+ }
81
+
82
+ if ((action === "create" || action === "update") && !content) {
83
+ throw new Error("创建或更新记忆时必须提供内容");
84
+ }
85
+
86
+ try {
87
+ if (action === "delete") {
88
+ await store.delete(Number(id));
89
+ return `已删除记忆 ${id}`;
90
+ }
91
+
92
+ const memoryData: MemoryRecord = {
93
+ text: content as string,
94
+ content: ensureJsonSerializable(content),
95
+ createdAt: new Date().toISOString(),
96
+ updatedAt: new Date().toISOString(),
97
+ namespace: getNamespace(),
98
+ tags: tags as string[],
99
+ path: path as string,
100
+ type: "document",
101
+ referencePath: referencePath as string,
102
+ };
103
+
104
+ if (action === "create") {
105
+ const key = await store.insert(memoryData);
106
+ return `已创建记忆 ${key}`;
107
+ } else {
108
+ await store.update(Number(id), memoryData);
109
+ return `已更新记忆 ${id}`;
110
+ }
111
+ } catch (error) {
112
+ throw new Error(`记忆操作失败: ${error}`);
113
+ }
114
+ },
115
+ }),
116
+ searchMemory: createToolUI({
117
+ name: "search_memory",
118
+ description: `搜索长期记忆中的相关信息,帮助维护对话上下文`,
119
+ parameters: [
120
+ {
121
+ name: "query",
122
+ type: "string",
123
+ description: "请使用中文关键词进行搜索",
124
+ },
125
+ {
126
+ name: "tags",
127
+ type: "string[]",
128
+ description: "记忆标签",
129
+ required: false,
130
+ },
131
+ {
132
+ name: "path",
133
+ type: "string",
134
+ description: "记忆路径,memory://user_id/path/to/memory_name",
135
+ required: false,
136
+ },
137
+ {
138
+ name: "limit",
139
+ type: "number",
140
+ description: "返回结果的最大数量(默认10)",
141
+ required: false,
142
+ },
143
+ ],
144
+ handler: async (args) => {
145
+ const { query, limit = 10, tags, path } = args;
146
+
147
+ if (!query || (typeof query === "string" && query.trim().length === 0)) {
148
+ throw new Error("搜索查询不能为空");
149
+ }
150
+
151
+ try {
152
+ const results = await store.query(query.toString(), {
153
+ limit: limit as number,
154
+ });
155
+
156
+ return {
157
+ query: query.toString(),
158
+ results: results,
159
+ total: results.length,
160
+ limit: Number(limit),
161
+ };
162
+ } catch (error) {
163
+ throw new Error(`搜索记忆失败: ${error}`);
164
+ }
165
+ },
166
+ }),
167
+ };
168
+ };
169
+
170
+ export { getNamespace, ensureJsonSerializable };
package/test/main.tsx CHANGED
@@ -1,5 +1,5 @@
1
- import React from "react";
2
1
  import ReactDOM from "react-dom/client";
3
2
  import App from "./App";
4
-
3
+ import "@unocss/reset/tailwind-compat.css";
4
+ import "virtual:uno.css";
5
5
  ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
package/vite.config.ts CHANGED
@@ -2,11 +2,12 @@ import react from "@vitejs/plugin-react";
2
2
  import { defineConfig } from "vite";
3
3
  import basicSsl from "@vitejs/plugin-basic-ssl";
4
4
  import path from "node:path";
5
+ import UnoCSS from "unocss/vite";
5
6
  // https://vitejs.dev/config/
6
7
  export default defineConfig(({ mode }) => {
7
8
  const isHttps = mode === "https";
8
9
  return {
9
- plugins: [react(), isHttps ? basicSsl() : undefined],
10
+ plugins: [UnoCSS(), react(), isHttps ? basicSsl() : undefined],
10
11
  resolve: {
11
12
  alias: {
12
13
  "@langgraph-js/sdk": new URL("../langgraph-client/src", import.meta.url).pathname,
@@ -16,6 +17,11 @@ export default defineConfig(({ mode }) => {
16
17
  exclude: ["@langgraph-js/ui", "@langgraph-js/sdk"],
17
18
  },
18
19
  server: {
20
+ headers: {
21
+ "Cross-Origin-Opener-Policy": "same-origin",
22
+ "Cross-Origin-Embedder-Policy": "require-corp",
23
+ "cross-origin-resource-policy": "cross-origin",
24
+ },
19
25
  port: 1111,
20
26
  },
21
27
  };