nolo-cli 0.1.21 → 0.1.23

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 (72) hide show
  1. package/agent-runtime/agentRecordConfig.ts +4 -0
  2. package/agent-runtime/hostAdapter.ts +2 -0
  3. package/agent-runtime/index.ts +7 -0
  4. package/agent-runtime/localLoop.ts +2 -0
  5. package/agent-runtime/platformChatProvider.ts +3 -0
  6. package/agent-runtime/runtimeToolPolicy.ts +92 -0
  7. package/agent-runtime/types.ts +42 -0
  8. package/agentRunCommand.ts +74 -1
  9. package/agentRuntimeCommands.ts +17 -89
  10. package/ai/agent/streamAgentChatTurn.ts +104 -20
  11. package/ai/chat/fetchUtils.native.ts +2 -0
  12. package/ai/chat/fetchUtils.ts +2 -0
  13. package/ai/chat/sendOpenAICompletionsRequest.ts +56 -0
  14. package/ai/chat/sendOpenAIResponseRequest.ts +64 -0
  15. package/ai/llm/kimi.ts +1 -1
  16. package/ai/llm/providers.ts +3 -0
  17. package/ai/llm/reasoningModels.ts +1 -0
  18. package/ai/skills/skillDocProtocol.ts +95 -3
  19. package/ai/taskRun/taskRunProtocol.ts +1 -0
  20. package/ai/tools/agent/agentTools.ts +17 -0
  21. package/ai/tools/agent/startAgentDialogTool.ts +53 -0
  22. package/ai/tools/modelUsageTools.ts +5 -0
  23. package/client/agentRun.test.ts +257 -7
  24. package/client/agentRun.ts +133 -34
  25. package/client/localRuntimeAdapter.test.ts +2 -0
  26. package/client/localRuntimeAdapter.ts +15 -2
  27. package/database/actions/common.ts +4 -3
  28. package/database/config.ts +19 -0
  29. package/machineCommands.ts +400 -45
  30. package/package.json +4 -2
  31. package/render/canvas/canvasEditContext.ts +127 -0
  32. package/render/canvas/canvasRuntime.ts +57 -0
  33. package/render/canvas/canvasSnapshotParser.ts +76 -0
  34. package/render/canvas/canvasTree.ts +308 -0
  35. package/render/canvas/types.ts +46 -0
  36. package/render/layout/deleteBehavior.ts +52 -0
  37. package/render/layout/mainLayoutSidebar.ts +17 -0
  38. package/render/layout/mainLayoutViewMode.ts +56 -0
  39. package/render/layout/topbarUtils.ts +87 -0
  40. package/render/layout/useDevReloadPending.ts +30 -0
  41. package/render/page/createPageAction.ts +183 -0
  42. package/render/page/docSlice.ts +468 -0
  43. package/render/page/server/createPage.ts +174 -0
  44. package/render/page/server/handleCreatePage.ts +91 -0
  45. package/render/page/server/index.ts +4 -0
  46. package/render/page/types.ts +17 -0
  47. package/render/page/useKeyboardSave.ts +48 -0
  48. package/render/styles/zIndex.ts +12 -0
  49. package/render/surf/WeatherIconStyles.ts +17 -0
  50. package/render/surf/color.ts +9 -0
  51. package/render/surf/config.ts +46 -0
  52. package/render/surf/screens/style.ts +1 -0
  53. package/render/surf/styles/ToggleButtonStyles.ts +8 -0
  54. package/render/surf/utils/groupedWeatherData.ts +32 -0
  55. package/render/surf/weatherUtils.ts +50 -0
  56. package/render/table/activityColumns.ts +6 -0
  57. package/render/table/createTableAction.ts +270 -0
  58. package/render/table/deleteTableAction.ts +129 -0
  59. package/render/table/fetchAndCacheTableRows.ts +174 -0
  60. package/render/table/tableSlice.ts +1106 -0
  61. package/render/table/tableView.ts +289 -0
  62. package/render/table/toolValueUtils.ts +363 -0
  63. package/render/table/types.ts +252 -0
  64. package/render/table/useCreateTable.ts +72 -0
  65. package/render/table/useTable.ts +61 -0
  66. package/render/table/utils/tableSerialization.ts +50 -0
  67. package/render/web/elements/artifactPreviewCode.ts +43 -0
  68. package/render/web/elements/artifactRuntimePreload.ts +52 -0
  69. package/render/web/elements/codeBlockAutoPreview.ts +10 -0
  70. package/render/web/elements/mermaidPreview.ts +21 -0
  71. package/render/web/ui/useInlineEdit.ts +135 -0
  72. package/tableCommands.ts +42 -5
@@ -0,0 +1,252 @@
1
+ // 文件: render/table/types.ts
2
+
3
+ import { DataType } from "create/types";
4
+
5
+ /* --------------------------------------------------------------------------
6
+ * 1. 列类型(TableColumn)
7
+ * ------------------------------------------------------------------------*/
8
+
9
+ export type TableColumnType =
10
+ | "text"
11
+ | "number"
12
+ | "boolean"
13
+ | "date"
14
+ | "datetime"
15
+ | "select"
16
+ | "multi_select";
17
+ // 将来可以扩展: "relation" | "json" | "file" 等
18
+
19
+ /**
20
+ * 表字段定义(列)
21
+ *
22
+ * 说明:
23
+ * - id:列自身的稳定 ID,供公式 / 视图 / 触发器 / Agent 配置引用
24
+ * - name:行数据中的 key(machine name)
25
+ * - label:展示名,可中文;不填时 UI 回退为 name
26
+ * - type + required + options:构成字段的“域约束”
27
+ * - description:给人和 AI 的语义说明
28
+ * - isPrimary:标记“这一行是谁”的主字段(行标题)
29
+ */
30
+ export interface TableColumn {
31
+ /** 列内部 ID(稳定,不随 name 改动) */
32
+ id: string;
33
+
34
+ /** 行中的字段名(机器名),建议英文/拼音,无空格 */
35
+ name: string;
36
+
37
+ /** 字段显示名,可中文;不填时 UI 用 name */
38
+ label?: string;
39
+
40
+ /** 字段类型(默认 text) */
41
+ type?: TableColumnType;
42
+
43
+ /** 字段说明:给人和 AI 看的描述 */
44
+ description?: string;
45
+
46
+ /** 是否为主字段(行标题),类似 Notion 的 Name 列 */
47
+ isPrimary?: boolean;
48
+
49
+ /** 是否必填 */
50
+ required?: boolean;
51
+
52
+ /**
53
+ * 离散取值集合,仅在 type 为 select / multi_select 时有意义。
54
+ * 先用 string[] 简化,将来可以扩展为对象数组(带颜色/顺序等)。
55
+ */
56
+ options?: string[];
57
+
58
+ /**
59
+ * 自定义列宽(像素),仅影响 UI 展示。
60
+ * - 不设置时由浏览器根据内容自适应宽度;
61
+ * - 设置后在所有视图中保持一致宽度(后续可以细化到 view 级别)。
62
+ */
63
+ width?: number;
64
+ }
65
+
66
+ /**
67
+ * 创建表时,传入的列配置:
68
+ * - id 由系统生成,外部一般不传
69
+ */
70
+ export type CreateTableColumnInput = Omit<TableColumn, "id">;
71
+
72
+ /* --------------------------------------------------------------------------
73
+ * 2. 视图(TableView)
74
+ * ------------------------------------------------------------------------*/
75
+
76
+ export type TableViewType = "grid" | "kanban" | "calendar";
77
+
78
+ export type SortDirection = "asc" | "desc";
79
+
80
+ /**
81
+ * 简化版排序规则
82
+ */
83
+ export interface TableViewSortRule {
84
+ columnId: string; // 引用 TableColumn.id
85
+ direction: SortDirection;
86
+ }
87
+
88
+ /**
89
+ * 过滤规则:
90
+ * - 这里先保留占位,后续可以演进为一套 DSL / JSON Schema
91
+ */
92
+ export interface TableViewFilter {
93
+ // 将来可以改成 { op: "and" | "or"; conditions: Condition[] } 等
94
+ [key: string]: any;
95
+ }
96
+
97
+ /**
98
+ * 分组配置(例如看板分组)
99
+ */
100
+ export interface TableViewGroup {
101
+ columnId: string; // 按哪个列分组
102
+ }
103
+
104
+ /**
105
+ * 视图定义:
106
+ * - 用来描述“这一张表在某个视角下如何展示/筛选/排序”
107
+ * - 未来 UI 和 Agent 都可以基于 View 工作
108
+ */
109
+ export interface TableView {
110
+ id: string;
111
+ name: string;
112
+ type: TableViewType;
113
+
114
+ /** 是否为默认视图 */
115
+ isDefault?: boolean;
116
+
117
+ /** 当前视图中可见的列(按列 id 排序) */
118
+ visibleColumnIds: string[];
119
+
120
+ /** 排序规则(可选) */
121
+ sort?: TableViewSortRule[];
122
+
123
+ /** 过滤规则(可选) */
124
+ filter?: TableViewFilter;
125
+
126
+ /** 分组(看板/分组表格) */
127
+ group?: TableViewGroup | null;
128
+
129
+ /**
130
+ * 给 AI 的附加说明:
131
+ * - 例如「本视图只展示未完成任务」「按项目分组」等
132
+ */
133
+ aiHint?: string;
134
+ }
135
+
136
+ /* --------------------------------------------------------------------------
137
+ * 3. 触发器(TableTrigger):行变更 → Agent / Webhook 等
138
+ * ------------------------------------------------------------------------*/
139
+
140
+ // 目前 Trigger 还没有真正落库和实现逻辑,可以仅保留类型占位,
141
+ // 后续如做通用 Trigger 中心建议迁移到 core/events/types.ts。
142
+
143
+ export type TableTriggerEvent = "row_created" | "row_updated" | "row_deleted";
144
+
145
+ export type TableTriggerActionType = "agent" | "webhook" | "custom";
146
+
147
+ export interface TableTriggerCondition {
148
+ kind: "match_column_value";
149
+ columnId: string; // TableColumn.id
150
+ operator: "eq" | "ne" | "in" | "not_in" | "contains";
151
+ value: any;
152
+ }
153
+
154
+ export interface TableTriggerActionConfig {
155
+ agentKey?: string;
156
+ url?: string;
157
+ method?: "GET" | "POST" | "PUT" | "DELETE";
158
+ [key: string]: any;
159
+ }
160
+
161
+ export interface TableTrigger {
162
+ id: string;
163
+ name: string;
164
+ description?: string;
165
+ enabled: boolean;
166
+
167
+ event: TableTriggerEvent;
168
+ viewId?: string;
169
+ condition?: TableTriggerCondition;
170
+
171
+ actionType: TableTriggerActionType;
172
+ actionConfig?: TableTriggerActionConfig;
173
+ }
174
+
175
+ /* --------------------------------------------------------------------------
176
+ * 4. 表 AI 配置(TableAiConfig)
177
+ * ------------------------------------------------------------------------*/
178
+
179
+ export interface TableAiConfig {
180
+ enabled: boolean;
181
+ purpose?: string;
182
+ allowedOperations?: {
183
+ createRow?: boolean;
184
+ updateRow?: boolean;
185
+ deleteRow?: boolean;
186
+ modifySchema?: boolean;
187
+ };
188
+ privacy?: {
189
+ maskColumnIds?: string[];
190
+ };
191
+ }
192
+
193
+ /* --------------------------------------------------------------------------
194
+ * 5. 表 Meta(TableMeta)
195
+ * ------------------------------------------------------------------------*/
196
+
197
+ /**
198
+ * 表的元信息:
199
+ * - 跟具体存储键(meta-{tenantId}-{tableId})一一对应
200
+ * - 是 schema + 视图 + 触发器 + AI 配置的集中描述
201
+ *
202
+ * Invariant:
203
+ * - 对任意 TableMeta(dbKey = meta-{tenantId}-{tableId}),
204
+ * 所有行必须满足 row.tenantId === tenantId 且 row.tableId === tableId。
205
+ */
206
+ export interface TableMeta {
207
+ /** meta dbKey: meta-{tenantId}-{tableId} */
208
+ dbKey: string;
209
+
210
+ tenantId: string;
211
+ tableId: string;
212
+ spaceId?: string | null;
213
+
214
+ /** 表显示名(可中文) */
215
+ displayName?: string;
216
+
217
+ /**
218
+ * 表用途说明,给人和 AI 都看的版本:
219
+ * 建议包括:
220
+ * - 每一行代表什么
221
+ * - 这张表大致用来干什么
222
+ */
223
+ description?: string;
224
+
225
+ /**
226
+ * 任意关键词标签,仅供 AI / 系统使用:
227
+ * 例如 ["记账","交易","财务"] / ["衣服","穿搭","旅行"]。
228
+ * 用户不必手动维护,可由 Agent 在创建表时自动填入。
229
+ */
230
+ tags?: string[];
231
+
232
+ /** schema 版本号,用于将来迁移,起步可以固定为 1 */
233
+ schemaVersion?: number;
234
+
235
+ /** 列定义 */
236
+ columns: TableColumn[];
237
+
238
+ /** 视图列表(可选) */
239
+ views?: TableView[];
240
+
241
+ /** 表级触发器列表(可选) */
242
+ triggers?: TableTrigger[];
243
+
244
+ /** AI 配置(可选) */
245
+ aiConfig?: TableAiConfig;
246
+
247
+ createdAt: string;
248
+ updatedAt: string;
249
+
250
+ /** 类型标记:table */
251
+ type: DataType.TABLE;
252
+ }
@@ -0,0 +1,72 @@
1
+ import { useCallback, useState } from "react";
2
+ import { useNavigate } from "react-router-dom";
3
+ import { useTranslation } from "react-i18next";
4
+ import { toast } from "react-hot-toast";
5
+ import { useAppDispatch, useAppSelector } from "app/store";
6
+ import { selectUserId } from "auth/authSlice";
7
+ import { createTable, addRow } from "./tableSlice";
8
+
9
+ interface UseCreateTableOptions {
10
+ onSuccess?: () => void;
11
+ }
12
+
13
+ interface CreateNewTableParams {
14
+ spaceId?: string;
15
+ }
16
+
17
+ export const useCreateTable = (options?: UseCreateTableOptions) => {
18
+ const { onSuccess } = options || {};
19
+ const dispatch = useAppDispatch();
20
+ const navigate = useNavigate();
21
+ const { t } = useTranslation();
22
+ const userId = useAppSelector(selectUserId);
23
+ const [isCreating, setIsCreating] = useState(false);
24
+
25
+ const createNewTable = useCallback(async ({ spaceId }: CreateNewTableParams = {}) => {
26
+ if (!userId) {
27
+ toast.error(t("table:userNotFound", "未找到用户信息,无法创建表格"));
28
+ return;
29
+ }
30
+
31
+ setIsCreating(true);
32
+ try {
33
+ // 1. 创建表格 Meta
34
+ const dbKey = await dispatch(
35
+ createTable({
36
+ spaceId,
37
+ title: t("table:newTable", "新建表格"),
38
+ withDefaultRows: false,
39
+ })
40
+ ).unwrap();
41
+
42
+ // 从 dbKey (meta-tenantId-tableId) 中提取 tableId
43
+ const parts = dbKey.split("-");
44
+ const tableId = parts[parts.length - 1];
45
+
46
+ // 2. 添加初始行
47
+ await dispatch(
48
+ addRow({
49
+ tenantId: userId,
50
+ tableId,
51
+ values: { title: "示例数据", note: "这是自动生成的记录" },
52
+ })
53
+ ).unwrap();
54
+
55
+ // 3. 成功回调(关闭菜单等)
56
+ onSuccess?.();
57
+
58
+ // 4. 导航
59
+ navigate(`/${dbKey}?edit=true`);
60
+ } catch (error) {
61
+ console.error("Failed to create table:", error);
62
+ toast.error(t("table:createFailed", "创建表格失败"));
63
+ } finally {
64
+ setIsCreating(false);
65
+ }
66
+ }, [dispatch, navigate, userId, t, onSuccess]);
67
+
68
+ return {
69
+ createNewTable,
70
+ isCreating,
71
+ };
72
+ };
@@ -0,0 +1,61 @@
1
+ // packages/render/table/useTable.ts
2
+
3
+ import { useEffect, useMemo } from "react";
4
+ import { useAppDispatch, useAppSelector } from "app/store";
5
+ import {
6
+ initTable,
7
+ loadTableRows,
8
+ selectCurrentTable,
9
+ selectTableIsLoading,
10
+ selectTableError,
11
+ selectTableRows,
12
+ } from "./tableSlice";
13
+ import { SEPARATOR } from "database/keys";
14
+
15
+ export interface UseTableOptions {
16
+ enabled?: boolean;
17
+ }
18
+
19
+ export const useTable = (tableKey: string | undefined, options: UseTableOptions = {}) => {
20
+ const { enabled = true } = options;
21
+ const dispatch = useAppDispatch();
22
+
23
+ // 1. Parse Key
24
+ const { tenantId, tableId, valid } = useMemo(() => {
25
+ if (!tableKey) return { tenantId: "", tableId: "", valid: false };
26
+ const parts = tableKey.split(SEPARATOR);
27
+ // meta-{tenantId}-{tableId}
28
+ if (parts[0] !== "meta" || parts.length < 3) {
29
+ return { tenantId: "", tableId: "", valid: false };
30
+ }
31
+ const tableId = parts[parts.length - 1];
32
+ const tenantId = parts.slice(1, parts.length - 1).join(SEPARATOR);
33
+
34
+ return { tenantId, tableId, valid: true };
35
+ }, [tableKey]);
36
+
37
+ // 2. Load Data
38
+ useEffect(() => {
39
+ if (enabled && valid && tenantId && tableId) {
40
+ void dispatch(initTable({ tenantId, tableId }));
41
+ void dispatch(loadTableRows({ tenantId, tableId }));
42
+ }
43
+ }, [dispatch, enabled, valid, tenantId, tableId]);
44
+
45
+ // 3. Selectors
46
+ const tableMeta = useAppSelector(selectCurrentTable);
47
+ const isLoading = useAppSelector(selectTableIsLoading);
48
+ const error = useAppSelector(selectTableError);
49
+ const rows = useAppSelector(selectTableRows);
50
+
51
+ return {
52
+ tenantId,
53
+ tableId,
54
+ valid,
55
+ tableMeta,
56
+ isLoading,
57
+ error,
58
+ rows,
59
+ dispatch,
60
+ };
61
+ };
@@ -0,0 +1,50 @@
1
+ // packages/render/table/utils/tableSerialization.ts
2
+
3
+ import { TableMeta } from "render/table/types";
4
+ import { fetchAndCacheTableRows } from "../fetchAndCacheTableRows";
5
+
6
+ /**
7
+ * Fetches all rows for a given table from the database and serializes the metadata
8
+ * and rows into a structured Markdown format.
9
+ */
10
+ export const fetchAndSerializeTable = async (
11
+ tableMeta: TableMeta,
12
+ db: any,
13
+ options: {
14
+ token?: string | null;
15
+ remoteServers?: string[];
16
+ } = {}
17
+ ): Promise<{ rows: any[]; markdown: string }> => {
18
+ const tenantId = tableMeta.tenantId;
19
+ const tableId = tableMeta.tableId;
20
+ const results = await fetchAndCacheTableRows({
21
+ db,
22
+ tenantId,
23
+ tableId,
24
+ token: options.token,
25
+ remoteServers: options.remoteServers ?? [],
26
+ });
27
+
28
+ // Format as Markdown table
29
+ const columns = tableMeta.columns || [];
30
+ const headerRow = columns.map((c) => c.label || c.name);
31
+ const separatorRow = columns.map(() => "---");
32
+
33
+ let tableMd = "";
34
+ if (columns.length > 0) {
35
+ tableMd = `| ${headerRow.join(" | ")} |\n| ${separatorRow.join(" | ")} |\n`;
36
+ results.forEach((row: any) => {
37
+ const rowData = columns.map((col) => {
38
+ const val = row[col.name];
39
+ return val === undefined || val === null
40
+ ? ""
41
+ : String(val).replace(/\|/g, "\\|");
42
+ });
43
+ tableMd += `| ${rowData.join(" | ")} |\n`;
44
+ });
45
+ } else {
46
+ tableMd = "(No columns defined for this table)";
47
+ }
48
+
49
+ return { rows: results, markdown: tableMd };
50
+ };
@@ -0,0 +1,43 @@
1
+ export type ArtifactPreviewBuildResult = {
2
+ code: string | null;
3
+ error: string | null;
4
+ };
5
+
6
+ const BLOCKED_IMPORT_RE =
7
+ /import\s+[^;]*\sfrom\s*['"](react|echarts-for-react|react-icons\/lu)['"];?/g;
8
+
9
+ function stripLightweightTypeScript(code: string): string {
10
+ const stripParamList = (params: string) =>
11
+ params.replace(
12
+ /(^|,)(\s*[A-Za-z_$][\w$]*)\s*:\s*[^,)=]+/g,
13
+ "$1$2"
14
+ );
15
+
16
+ return code
17
+ .replace(
18
+ /(function\s+[A-Za-z_$][\w$]*\s*)\(([^)]*)\)/g,
19
+ (_match, prefix: string, params: string) =>
20
+ `${prefix}(${stripParamList(params)})`
21
+ )
22
+ .replace(
23
+ /(function\s+[A-Za-z_$][\w$]*\([^)]*\))\s*:\s*[A-Za-z_$][\w$<>,\s.[\]|&]*(?=\s*\{)/g,
24
+ "$1"
25
+ )
26
+ .replace(
27
+ /\(([^()\n]*)\)\s*=>/g,
28
+ (_match, params: string) => `(${stripParamList(params)}) =>`
29
+ )
30
+ .replace(
31
+ /(const|let|var)\s+([A-Za-z_$][\w$]*)\s*:\s*[^=;]+(?=\s*=)/g,
32
+ "$1 $2"
33
+ )
34
+ .replace(/\b(useState|useMemo|useRef|useReducer)<[^>(]+>\s*\(/g, "$1(");
35
+ }
36
+
37
+ export function sanitizeArtifactCode(rawCode: string): string {
38
+ return stripLightweightTypeScript(rawCode)
39
+ .replace(BLOCKED_IMPORT_RE, "")
40
+ .replace(/export\s+default\s+\w+;?/g, "")
41
+ .replace(/export\s+(const|let|var|function|class)\s+/g, "$1 ")
42
+ .trim();
43
+ }
@@ -0,0 +1,52 @@
1
+ const ARTIFACT_RUNTIME_SCRIPT_URL = "/artifact-runtime-script";
2
+
3
+ const preloadedRuntimeUrls = new Set<string>();
4
+
5
+ declare global {
6
+ interface Window {
7
+ __NOLO_ASSETS__?: {
8
+ artifactRuntimeJs?: string;
9
+ artifactRuntimePreloads?: string[];
10
+ };
11
+ }
12
+ }
13
+
14
+ export function resolveArtifactRuntimeScriptUrl() {
15
+ if (typeof window === "undefined") return ARTIFACT_RUNTIME_SCRIPT_URL;
16
+ return window.__NOLO_ASSETS__?.artifactRuntimeJs || ARTIFACT_RUNTIME_SCRIPT_URL;
17
+ }
18
+
19
+ function appendPreloadLink(href: string, rel: "preload" | "modulepreload", as?: string) {
20
+ if (!href || preloadedRuntimeUrls.has(`${rel}:${href}`)) return;
21
+ if (document.head.querySelector(`link[rel="${rel}"][href="${href}"]`)) {
22
+ preloadedRuntimeUrls.add(`${rel}:${href}`);
23
+ return;
24
+ }
25
+
26
+ const link = document.createElement("link");
27
+ link.rel = rel;
28
+ link.href = href;
29
+ if (as) link.as = as;
30
+ link.crossOrigin = "anonymous";
31
+ (link as HTMLLinkElement & { fetchPriority?: string }).fetchPriority = "high";
32
+ document.head.appendChild(link);
33
+ preloadedRuntimeUrls.add(`${rel}:${href}`);
34
+ }
35
+
36
+ export function preloadArtifactRuntimeResources() {
37
+ if (typeof window === "undefined") return;
38
+
39
+ appendPreloadLink(
40
+ new URL("/artifact-runtime", window.location.origin).toString(),
41
+ "preload",
42
+ "document"
43
+ );
44
+
45
+ const urls = window.__NOLO_ASSETS__?.artifactRuntimePreloads?.length
46
+ ? window.__NOLO_ASSETS__.artifactRuntimePreloads
47
+ : [resolveArtifactRuntimeScriptUrl()];
48
+
49
+ for (const url of urls) {
50
+ appendPreloadLink(new URL(url, window.location.origin).toString(), "modulepreload");
51
+ }
52
+ }
@@ -0,0 +1,10 @@
1
+ export function canPreviewJson(rawCode: string) {
2
+ if (!rawCode.trim()) return false;
3
+
4
+ try {
5
+ JSON.parse(rawCode);
6
+ return true;
7
+ } catch {
8
+ return false;
9
+ }
10
+ }
@@ -0,0 +1,21 @@
1
+ export type MermaidParse = (content: string) => Promise<unknown>;
2
+
3
+ const parseWithMermaid: MermaidParse = async (input) => {
4
+ const { default: mermaid } = await import("mermaid");
5
+ return mermaid.parse(input);
6
+ };
7
+
8
+ export async function canRenderMermaid(
9
+ content: string,
10
+ parseMermaid: MermaidParse = parseWithMermaid
11
+ ) {
12
+ const trimmed = content.trim();
13
+ if (!trimmed) return false;
14
+
15
+ try {
16
+ await parseMermaid(trimmed);
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
@@ -0,0 +1,135 @@
1
+ import React, { useState, useEffect, useRef, useCallback } from "react";
2
+
3
+ interface UseInlineEditOptions {
4
+ onSave: (newValue: string) => void | Promise<void>;
5
+ initialValue: string;
6
+ placeholder?: string;
7
+ ariaLabel?: string;
8
+ disabled?: boolean;
9
+ }
10
+
11
+ interface UseInlineEditReturn {
12
+ isEditing: boolean;
13
+ startEditing: () => void;
14
+ cancelEdit: () => void; // Usually handled internally by blur/escape
15
+ inputRef: React.RefObject<HTMLInputElement | null>;
16
+ inputProps: {
17
+ value: string;
18
+ onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
19
+ onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void;
20
+ onBlur: () => void;
21
+ placeholder?: string;
22
+ "aria-label"?: string;
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Hook to manage the state and logic for inline editing of a text value.
28
+ * @param initialValue - The initial value to display and edit.
29
+ * @param onSave - Callback function triggered when editing is successfully completed (Enter or Blur with changes).
30
+ * @param placeholder - Optional placeholder text for the input.
31
+ * @param ariaLabel - Optional aria-label for the input.
32
+ */
33
+ export const useInlineEdit = ({
34
+ initialValue,
35
+ onSave,
36
+ placeholder = "输入内容...",
37
+ ariaLabel = "编辑内容",
38
+ }: UseInlineEditOptions): UseInlineEditReturn => {
39
+ const [isEditing, setIsEditing] = useState(false);
40
+ const [currentValue, setCurrentValue] = useState(initialValue);
41
+ const inputRef = useRef<HTMLInputElement>(null);
42
+ // Store the value before editing started to check for actual changes on blur/save
43
+ const previousValueRef = useRef(initialValue);
44
+
45
+ // Update internal state if the initialValue prop changes externally while not editing
46
+ useEffect(() => {
47
+ if (!isEditing) {
48
+ setCurrentValue(initialValue);
49
+ previousValueRef.current = initialValue; // Keep track of the non-editing value
50
+ }
51
+ }, [initialValue, isEditing]);
52
+
53
+ // Focus and select text when editing starts
54
+ useEffect(() => {
55
+ if (isEditing && inputRef.current) {
56
+ inputRef.current.focus();
57
+ inputRef.current.select();
58
+ }
59
+ }, [isEditing]);
60
+
61
+ const startEditing = useCallback(() => {
62
+ previousValueRef.current = currentValue; // Store current value before editing
63
+ setIsEditing(true);
64
+ }, [currentValue]);
65
+
66
+ const handleSave = useCallback(() => {
67
+ const trimmedValue = currentValue.trim();
68
+ // Only save if the value is non-empty and actually changed
69
+ if (trimmedValue && trimmedValue !== previousValueRef.current) {
70
+ Promise.resolve(onSave(trimmedValue)).finally(() => {
71
+ setIsEditing(false);
72
+ });
73
+ } else if (!trimmedValue) {
74
+ // If empty, revert to the value before editing started
75
+ console.warn("Input cannot be empty. Reverting changes.");
76
+ setCurrentValue(previousValueRef.current);
77
+ setIsEditing(false);
78
+ } else {
79
+ // If unchanged or only whitespace difference, just exit editing
80
+ setCurrentValue(previousValueRef.current); // Ensure display reverts if only whitespace changed
81
+ setIsEditing(false);
82
+ }
83
+ }, [currentValue, onSave]);
84
+
85
+ const cancelEdit = useCallback(() => {
86
+ // Revert to the value before editing started
87
+ setCurrentValue(previousValueRef.current);
88
+ setIsEditing(false);
89
+ }, []);
90
+
91
+ const handleInputChange = useCallback(
92
+ (event: React.ChangeEvent<HTMLInputElement>) => {
93
+ setCurrentValue(event.target.value);
94
+ },
95
+ []
96
+ );
97
+
98
+ const handleKeyDown = useCallback(
99
+ (event: React.KeyboardEvent<HTMLInputElement>) => {
100
+ if (event.key === "Enter") {
101
+ event.preventDefault();
102
+ handleSave();
103
+ } else if (event.key === "Escape") {
104
+ cancelEdit();
105
+ }
106
+ },
107
+ [handleSave, cancelEdit]
108
+ );
109
+
110
+ // Save on blur
111
+ const handleBlur = useCallback(() => {
112
+ // Timeout helps prevent issues where blur triggers before a click outside (e.g., on a save button)
113
+ // If handleSave is already called by Enter, setIsEditing(false) will prevent double execution logic
114
+ // if (isEditing) { // Check if still in editing mode before saving on blur
115
+ setTimeout(handleSave, 0);
116
+ // }
117
+ }, [handleSave]);
118
+
119
+ const inputProps = {
120
+ value: currentValue,
121
+ onChange: handleInputChange,
122
+ onKeyDown: handleKeyDown,
123
+ onBlur: handleBlur,
124
+ placeholder: placeholder,
125
+ "aria-label": ariaLabel,
126
+ };
127
+
128
+ return {
129
+ isEditing,
130
+ startEditing,
131
+ cancelEdit, // Although primarily internal, can be exposed if needed
132
+ inputRef,
133
+ inputProps,
134
+ };
135
+ };