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.
- package/agent-runtime/agentRecordConfig.ts +4 -0
- package/agent-runtime/hostAdapter.ts +2 -0
- package/agent-runtime/index.ts +7 -0
- package/agent-runtime/localLoop.ts +2 -0
- package/agent-runtime/platformChatProvider.ts +3 -0
- package/agent-runtime/runtimeToolPolicy.ts +92 -0
- package/agent-runtime/types.ts +42 -0
- package/agentRunCommand.ts +74 -1
- package/agentRuntimeCommands.ts +17 -89
- package/ai/agent/streamAgentChatTurn.ts +104 -20
- package/ai/chat/fetchUtils.native.ts +2 -0
- package/ai/chat/fetchUtils.ts +2 -0
- package/ai/chat/sendOpenAICompletionsRequest.ts +56 -0
- package/ai/chat/sendOpenAIResponseRequest.ts +64 -0
- package/ai/llm/kimi.ts +1 -1
- package/ai/llm/providers.ts +3 -0
- package/ai/llm/reasoningModels.ts +1 -0
- package/ai/skills/skillDocProtocol.ts +95 -3
- package/ai/taskRun/taskRunProtocol.ts +1 -0
- package/ai/tools/agent/agentTools.ts +17 -0
- package/ai/tools/agent/startAgentDialogTool.ts +53 -0
- package/ai/tools/modelUsageTools.ts +5 -0
- package/client/agentRun.test.ts +257 -7
- package/client/agentRun.ts +133 -34
- package/client/localRuntimeAdapter.test.ts +2 -0
- package/client/localRuntimeAdapter.ts +15 -2
- package/database/actions/common.ts +4 -3
- package/database/config.ts +19 -0
- package/machineCommands.ts +400 -45
- package/package.json +4 -2
- package/render/canvas/canvasEditContext.ts +127 -0
- package/render/canvas/canvasRuntime.ts +57 -0
- package/render/canvas/canvasSnapshotParser.ts +76 -0
- package/render/canvas/canvasTree.ts +308 -0
- package/render/canvas/types.ts +46 -0
- package/render/layout/deleteBehavior.ts +52 -0
- package/render/layout/mainLayoutSidebar.ts +17 -0
- package/render/layout/mainLayoutViewMode.ts +56 -0
- package/render/layout/topbarUtils.ts +87 -0
- package/render/layout/useDevReloadPending.ts +30 -0
- package/render/page/createPageAction.ts +183 -0
- package/render/page/docSlice.ts +468 -0
- package/render/page/server/createPage.ts +174 -0
- package/render/page/server/handleCreatePage.ts +91 -0
- package/render/page/server/index.ts +4 -0
- package/render/page/types.ts +17 -0
- package/render/page/useKeyboardSave.ts +48 -0
- package/render/styles/zIndex.ts +12 -0
- package/render/surf/WeatherIconStyles.ts +17 -0
- package/render/surf/color.ts +9 -0
- package/render/surf/config.ts +46 -0
- package/render/surf/screens/style.ts +1 -0
- package/render/surf/styles/ToggleButtonStyles.ts +8 -0
- package/render/surf/utils/groupedWeatherData.ts +32 -0
- package/render/surf/weatherUtils.ts +50 -0
- package/render/table/activityColumns.ts +6 -0
- package/render/table/createTableAction.ts +270 -0
- package/render/table/deleteTableAction.ts +129 -0
- package/render/table/fetchAndCacheTableRows.ts +174 -0
- package/render/table/tableSlice.ts +1106 -0
- package/render/table/tableView.ts +289 -0
- package/render/table/toolValueUtils.ts +363 -0
- package/render/table/types.ts +252 -0
- package/render/table/useCreateTable.ts +72 -0
- package/render/table/useTable.ts +61 -0
- package/render/table/utils/tableSerialization.ts +50 -0
- package/render/web/elements/artifactPreviewCode.ts +43 -0
- package/render/web/elements/artifactRuntimePreload.ts +52 -0
- package/render/web/elements/codeBlockAutoPreview.ts +10 -0
- package/render/web/elements/mermaidPreview.ts +21 -0
- package/render/web/ui/useInlineEdit.ts +135 -0
- 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,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
|
+
};
|