plugin-build-guide-block 1.1.6 → 1.1.9
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/client-v2.d.ts +2 -0
- package/client-v2.js +1 -0
- package/dist/client/index.js +2 -2
- package/dist/client-v2/376.bea427a4cf8864b4.js +10 -0
- package/dist/client-v2/index.js +10 -0
- package/dist/externalVersion.js +9 -8
- package/dist/node_modules/sanitize-html/index.js +2 -2
- package/dist/node_modules/sanitize-html/package.json +1 -1
- package/dist/server/actions/build.js +5 -2
- package/package.json +8 -4
- package/src/client/UserGuideManager.tsx +307 -138
- package/src/client-v2/index.tsx +1 -0
- package/src/client-v2/plugin.tsx +24 -0
- package/src/server/actions/build.ts +6 -2
- package/dist/node_modules/sanitize-html/node_modules/nanoid/async/index.browser.cjs +0 -34
- package/dist/node_modules/sanitize-html/node_modules/nanoid/async/index.browser.js +0 -34
- package/dist/node_modules/sanitize-html/node_modules/nanoid/async/index.cjs +0 -35
- package/dist/node_modules/sanitize-html/node_modules/nanoid/async/index.d.ts +0 -56
- package/dist/node_modules/sanitize-html/node_modules/nanoid/async/index.js +0 -35
- package/dist/node_modules/sanitize-html/node_modules/nanoid/async/index.native.js +0 -26
- package/dist/node_modules/sanitize-html/node_modules/nanoid/async/package.json +0 -12
- package/dist/node_modules/sanitize-html/node_modules/nanoid/bin/nanoid.cjs +0 -55
- package/dist/node_modules/sanitize-html/node_modules/nanoid/index.browser.cjs +0 -34
- package/dist/node_modules/sanitize-html/node_modules/nanoid/index.browser.js +0 -34
- package/dist/node_modules/sanitize-html/node_modules/nanoid/index.cjs +0 -45
- package/dist/node_modules/sanitize-html/node_modules/nanoid/index.d.cts +0 -91
- package/dist/node_modules/sanitize-html/node_modules/nanoid/index.d.ts +0 -91
- package/dist/node_modules/sanitize-html/node_modules/nanoid/index.js +0 -45
- package/dist/node_modules/sanitize-html/node_modules/nanoid/nanoid.js +0 -1
- package/dist/node_modules/sanitize-html/node_modules/nanoid/non-secure/index.cjs +0 -21
- package/dist/node_modules/sanitize-html/node_modules/nanoid/non-secure/index.d.ts +0 -33
- package/dist/node_modules/sanitize-html/node_modules/nanoid/non-secure/index.js +0 -21
- package/dist/node_modules/sanitize-html/node_modules/nanoid/non-secure/package.json +0 -6
- package/dist/node_modules/sanitize-html/node_modules/nanoid/package.json +0 -88
- package/dist/node_modules/sanitize-html/node_modules/nanoid/url-alphabet/index.cjs +0 -3
- package/dist/node_modules/sanitize-html/node_modules/nanoid/url-alphabet/index.js +0 -3
- package/dist/node_modules/sanitize-html/node_modules/nanoid/url-alphabet/package.json +0 -6
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/at-rule.d.ts +0 -115
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/at-rule.js +0 -25
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/comment.d.ts +0 -67
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/comment.js +0 -13
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/container.d.ts +0 -452
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/container.js +0 -439
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/css-syntax-error.d.ts +0 -248
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/css-syntax-error.js +0 -100
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/declaration.d.ts +0 -148
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/declaration.js +0 -24
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/document.d.ts +0 -68
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/document.js +0 -33
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/fromJSON.d.ts +0 -9
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/fromJSON.js +0 -54
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/input.d.ts +0 -194
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/input.js +0 -248
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/lazy-result.d.ts +0 -190
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/lazy-result.js +0 -550
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/list.d.ts +0 -57
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/list.js +0 -58
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/map-generator.js +0 -359
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/no-work-result.d.ts +0 -46
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/no-work-result.js +0 -135
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/node.d.ts +0 -536
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/node.js +0 -381
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/parse.d.ts +0 -9
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/parse.js +0 -42
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/parser.js +0 -610
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/postcss.d.mts +0 -72
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/postcss.d.ts +0 -441
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/postcss.js +0 -101
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/previous-map.d.ts +0 -81
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/previous-map.js +0 -142
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/processor.d.ts +0 -115
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/processor.js +0 -67
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/result.d.ts +0 -206
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/result.js +0 -42
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/root.d.ts +0 -86
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/root.js +0 -61
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/rule.d.ts +0 -113
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/rule.js +0 -27
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/stringifier.d.ts +0 -46
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/stringifier.js +0 -353
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/stringify.d.ts +0 -9
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/stringify.js +0 -11
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/symbols.js +0 -5
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/terminal-highlight.js +0 -70
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/tokenize.js +0 -266
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/warn-once.js +0 -13
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/warning.d.ts +0 -147
- package/dist/node_modules/sanitize-html/node_modules/postcss/lib/warning.js +0 -37
- package/dist/node_modules/sanitize-html/node_modules/postcss/node_modules/.bin/nanoid +0 -15
- package/dist/node_modules/sanitize-html/node_modules/postcss/node_modules/.bin/nanoid.cmd +0 -7
- package/dist/node_modules/sanitize-html/node_modules/postcss/package.json +0 -88
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"sanitize-html","version":"2.17.
|
|
1
|
+
{"name":"sanitize-html","version":"2.17.5","description":"Clean up user-submitted HTML, preserving allowlisted elements and allowlisted attributes on a per-element basis","sideEffects":false,"main":"index.js","files":["index.js"],"repository":{"type":"git","url":"https://github.com/apostrophecms/apostrophe.git","directory":"packages/sanitize-html"},"homepage":"https://github.com/apostrophecms/apostrophe/tree/main/packages/sanitize-html#readme","keywords":["html","parser","sanitizer","sanitize"],"author":"Apostrophe Technologies, Inc.","license":"MIT","dependencies":{"deepmerge":"^4.2.2","escape-string-regexp":"^4.0.0","htmlparser2":"^10.1.0","is-plain-object":"^5.0.0","parse-srcset":"^1.0.2","postcss":"^8.3.11","launder":"^1.7.1"},"devDependencies":{"eslint":"^9.39.1","mocha":"^11.7.5","sinon":"^9.0.2","eslint-config-apostrophe":"^6.0.2"},"apostropheTestConfig":{"requiresMongo":false},"scripts":{"test":"npm run lint && mocha","lint":"eslint ."},"_lastModified":"2026-06-17T05:00:41.121Z"}
|
|
@@ -316,6 +316,9 @@ function toPlainText(value) {
|
|
|
316
316
|
}
|
|
317
317
|
return JSON.stringify(value);
|
|
318
318
|
}
|
|
319
|
+
function stripThink(text) {
|
|
320
|
+
return text.replace(/<think>[\s\S]*?(?:<\/think>|$)/gi, "").trim();
|
|
321
|
+
}
|
|
319
322
|
function stripFence(text) {
|
|
320
323
|
return text.replace(/^```(?:json|markdown|md|html)?\s*/i, "").replace(/```\s*$/i, "").trim();
|
|
321
324
|
}
|
|
@@ -374,7 +377,7 @@ function createFallbackPlan(guideTitle, targetChapterCount) {
|
|
|
374
377
|
}
|
|
375
378
|
function normalizePlan(rawText, guideTitle, targetChapterCount) {
|
|
376
379
|
const targetCount = clampChapterCount(targetChapterCount);
|
|
377
|
-
const cleanText = stripFence(rawText);
|
|
380
|
+
const cleanText = stripFence(stripThink(rawText));
|
|
378
381
|
const jsonStart = cleanText.indexOf("{");
|
|
379
382
|
const jsonEnd = cleanText.lastIndexOf("}");
|
|
380
383
|
const jsonText = jsonStart >= 0 && jsonEnd > jsonStart ? cleanText.slice(jsonStart, jsonEnd + 1) : cleanText;
|
|
@@ -479,7 +482,7 @@ Source documents:
|
|
|
479
482
|
${documentsText.slice(0, MAX_SOURCE_CHARS)}`)
|
|
480
483
|
);
|
|
481
484
|
const response = await provider.chatModel.invoke(messages);
|
|
482
|
-
return stripFence(toPlainText(response.content));
|
|
485
|
+
return stripFence(stripThink(toPlainText(response.content)));
|
|
483
486
|
}
|
|
484
487
|
async function markdownToCleanHtml(markdown) {
|
|
485
488
|
const renderedHtml = await import_marked.marked.parse(markdown, { async: true });
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"displayName.zh-CN": "构建指南区块",
|
|
6
6
|
"description": "Generate user guides, tutorials and help books from documents using AI, then render the result as a block (HTML or Markdown).",
|
|
7
7
|
"description.vi-VN": "Tạo hướng dẫn người dùng, tutorial và help book từ tài liệu bằng AI, hiển thị kết quả dưới dạng block (HTML hoặc Markdown).",
|
|
8
|
-
"version": "1.1.
|
|
8
|
+
"version": "1.1.9",
|
|
9
9
|
"license": "Apache-2.0",
|
|
10
10
|
"keywords": [
|
|
11
11
|
"ai",
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
"client.js",
|
|
22
22
|
"server.js",
|
|
23
23
|
"client.d.ts",
|
|
24
|
-
"server.d.ts"
|
|
24
|
+
"server.d.ts",
|
|
25
|
+
"client-v2.js",
|
|
26
|
+
"client-v2.d.ts"
|
|
25
27
|
],
|
|
26
28
|
"dependencies": {
|
|
27
29
|
"dompurify": "^3.1.2",
|
|
@@ -40,7 +42,9 @@
|
|
|
40
42
|
"@nocobase/plugin-ai": "2.x",
|
|
41
43
|
"@nocobase/plugin-file-manager": "2.x",
|
|
42
44
|
"@langchain/core": "*",
|
|
43
|
-
"axios": "*"
|
|
45
|
+
"axios": "*",
|
|
46
|
+
"@nocobase/client-v2": "2.x",
|
|
47
|
+
"@nocobase/flow-engine": "2.x"
|
|
44
48
|
},
|
|
45
49
|
"nocobase": {
|
|
46
50
|
"supportedVersions": [
|
|
@@ -48,4 +52,4 @@
|
|
|
48
52
|
],
|
|
49
53
|
"editionLevel": 0
|
|
50
54
|
}
|
|
51
|
-
}
|
|
55
|
+
}
|
|
@@ -1,27 +1,47 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
useDataBlockRequest,
|
|
7
|
-
useDataBlockResource,
|
|
8
|
-
useDestroyActionProps,
|
|
9
|
-
useTableBlockProps,
|
|
10
|
-
} from '@nocobase/client';
|
|
11
|
-
import { createForm } from '@formily/core';
|
|
12
|
-
import { useForm } from '@formily/react';
|
|
13
|
-
import { App } from 'antd';
|
|
14
|
-
import { useTranslation } from 'react-i18next';
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { App, Button, Drawer, Form, Input, InputNumber, Select, Space, Table, Tag } from 'antd';
|
|
3
|
+
import { PlayCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
|
4
|
+
import { useApp } from '@nocobase/client-v2';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
15
6
|
import {
|
|
16
7
|
DEFAULT_TARGET_CHAPTER_COUNT,
|
|
17
8
|
MAX_TARGET_CHAPTER_COUNT,
|
|
18
9
|
MIN_TARGET_CHAPTER_COUNT,
|
|
19
|
-
spacesSchema,
|
|
20
10
|
} from './schemas/spacesSchema';
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
11
|
+
|
|
12
|
+
const POLL_INTERVAL_MS = 3000;
|
|
13
|
+
const STILL_RUNNING_AFTER_MS = 5 * 60 * 1000;
|
|
14
|
+
const SLOW_POLL_INTERVAL_MS = 10000;
|
|
15
|
+
|
|
16
|
+
const STATUS_COLORS: Record<string, string> = {
|
|
17
|
+
draft: 'default',
|
|
18
|
+
building: 'blue',
|
|
19
|
+
completed: 'success',
|
|
20
|
+
error: 'error',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const OUTPUT_FORMAT_OPTIONS = [
|
|
24
|
+
{ label: 'HTML', value: 'html' },
|
|
25
|
+
{ label: 'Markdown', value: 'markdown' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
interface SpaceRecord {
|
|
29
|
+
id: number;
|
|
30
|
+
title?: string;
|
|
31
|
+
status?: string;
|
|
32
|
+
buildPhase?: string;
|
|
33
|
+
pageCount?: number;
|
|
34
|
+
buildLog?: string;
|
|
35
|
+
llmService?: string;
|
|
36
|
+
model?: string;
|
|
37
|
+
outputFormat?: string;
|
|
38
|
+
targetChapterCount?: number;
|
|
39
|
+
chapterGuidance?: string;
|
|
40
|
+
systemPrompt?: string;
|
|
41
|
+
generatedHtml?: string;
|
|
42
|
+
generatedMarkdown?: string;
|
|
43
|
+
documents?: any[];
|
|
44
|
+
}
|
|
25
45
|
|
|
26
46
|
const normalizeTargetChapterCount = (value: unknown) => {
|
|
27
47
|
const count = Number(value);
|
|
@@ -31,127 +51,276 @@ const normalizeTargetChapterCount = (value: unknown) => {
|
|
|
31
51
|
|
|
32
52
|
export const UserGuideManager = () => {
|
|
33
53
|
const { t } = useTranslation();
|
|
54
|
+
const api = useApp().apiClient;
|
|
55
|
+
const { message, modal } = App.useApp();
|
|
56
|
+
const [form] = Form.useForm();
|
|
57
|
+
const selectedService = Form.useWatch('llmService', form);
|
|
58
|
+
|
|
59
|
+
const [data, setData] = useState<SpaceRecord[]>([]);
|
|
60
|
+
const [loading, setLoading] = useState(false);
|
|
61
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
62
|
+
const [editingId, setEditingId] = useState<number | null>(null);
|
|
63
|
+
const [submitting, setSubmitting] = useState(false);
|
|
64
|
+
const [serviceOptions, setServiceOptions] = useState<{ label: string; value: string }[]>([]);
|
|
65
|
+
const [modelOptions, setModelOptions] = useState<{ label: string; value: string }[]>([]);
|
|
66
|
+
const [buildingId, setBuildingId] = useState<number | null>(null);
|
|
67
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
68
|
+
|
|
69
|
+
const loadList = useCallback(async () => {
|
|
70
|
+
setLoading(true);
|
|
71
|
+
try {
|
|
72
|
+
const res = await api.resource('aiBuildGuideSpaces').list({
|
|
73
|
+
appends: ['documents'],
|
|
74
|
+
sort: ['-createdAt'],
|
|
75
|
+
paginate: false,
|
|
76
|
+
});
|
|
77
|
+
setData(res?.data?.data || []);
|
|
78
|
+
} finally {
|
|
79
|
+
setLoading(false);
|
|
80
|
+
}
|
|
81
|
+
}, [api]);
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
loadList();
|
|
85
|
+
}, [loadList]);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
return () => {
|
|
89
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
90
|
+
};
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
const loadServices = useCallback(async () => {
|
|
94
|
+
try {
|
|
95
|
+
const res = await api.resource('ai').listLLMServices();
|
|
96
|
+
const list = res?.data?.data || [];
|
|
97
|
+
setServiceOptions(list.map((s: any) => ({ label: s.title || s.name, value: s.name })));
|
|
98
|
+
} catch {
|
|
99
|
+
setServiceOptions([]);
|
|
100
|
+
}
|
|
101
|
+
}, [api]);
|
|
102
|
+
|
|
103
|
+
const loadModels = useCallback(
|
|
104
|
+
async (service?: string) => {
|
|
105
|
+
if (!service) {
|
|
106
|
+
setModelOptions([]);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const res = await api.resource('ai').listModels({ llmService: service });
|
|
111
|
+
const list = res?.data?.data || [];
|
|
112
|
+
setModelOptions(list.map((m: any) => ({ label: m.id || m.name, value: m.id || m.name })));
|
|
113
|
+
} catch {
|
|
114
|
+
setModelOptions([]);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
[api],
|
|
118
|
+
);
|
|
34
119
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
loadModels(selectedService);
|
|
122
|
+
}, [selectedService, loadModels]);
|
|
123
|
+
|
|
124
|
+
const openCreate = () => {
|
|
125
|
+
setEditingId(null);
|
|
126
|
+
form.resetFields();
|
|
127
|
+
form.setFieldsValue({
|
|
128
|
+
outputFormat: 'html',
|
|
129
|
+
targetChapterCount: DEFAULT_TARGET_CHAPTER_COUNT,
|
|
130
|
+
systemPrompt:
|
|
131
|
+
'You are an expert technical writer. Generate a comprehensive user guide based on the provided documents.',
|
|
132
|
+
});
|
|
133
|
+
loadServices();
|
|
134
|
+
setDrawerOpen(true);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const openEdit = (record: SpaceRecord) => {
|
|
138
|
+
setEditingId(record.id);
|
|
139
|
+
form.resetFields();
|
|
140
|
+
form.setFieldsValue({
|
|
141
|
+
title: record.title,
|
|
142
|
+
llmService: record.llmService,
|
|
143
|
+
model: record.model,
|
|
144
|
+
outputFormat: record.outputFormat || 'html',
|
|
145
|
+
targetChapterCount: normalizeTargetChapterCount(record.targetChapterCount),
|
|
146
|
+
chapterGuidance: record.chapterGuidance,
|
|
147
|
+
systemPrompt: record.systemPrompt,
|
|
148
|
+
});
|
|
149
|
+
loadServices();
|
|
150
|
+
loadModels(record.llmService);
|
|
151
|
+
setDrawerOpen(true);
|
|
46
152
|
};
|
|
47
153
|
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
154
|
+
const handleSubmit = async () => {
|
|
155
|
+
const values = await form.validateFields();
|
|
156
|
+
values.targetChapterCount = normalizeTargetChapterCount(values.targetChapterCount);
|
|
157
|
+
setSubmitting(true);
|
|
158
|
+
try {
|
|
159
|
+
if (editingId) {
|
|
160
|
+
await api.resource('aiBuildGuideSpaces').update({ filterByTk: editingId, values });
|
|
161
|
+
} else {
|
|
162
|
+
await api.resource('aiBuildGuideSpaces').create({ values });
|
|
163
|
+
}
|
|
164
|
+
message.success(t('Saved successfully'));
|
|
165
|
+
setDrawerOpen(false);
|
|
166
|
+
await loadList();
|
|
167
|
+
} catch (err: any) {
|
|
168
|
+
if (err?.errorFields) return;
|
|
169
|
+
message.error(err?.response?.data?.error?.message || t('Save failed'));
|
|
170
|
+
} finally {
|
|
171
|
+
setSubmitting(false);
|
|
172
|
+
}
|
|
61
173
|
};
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const normalizeValues = (values: any) => {
|
|
74
|
-
const { documents, ...rest } = values;
|
|
75
|
-
rest.targetChapterCount = normalizeTargetChapterCount(rest.targetChapterCount);
|
|
76
|
-
if (Array.isArray(documents)) {
|
|
77
|
-
rest.documents = documents.map((doc: any) => (typeof doc === 'object' && doc?.id ? { id: doc.id } : doc));
|
|
174
|
+
|
|
175
|
+
const handleDelete = async (record: SpaceRecord) => {
|
|
176
|
+
await api.resource('aiBuildGuideSpaces').destroy({ filterByTk: record.id });
|
|
177
|
+
message.success(t('Deleted'));
|
|
178
|
+
await loadList();
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const stopPolling = useCallback(() => {
|
|
182
|
+
if (timerRef.current) {
|
|
183
|
+
clearTimeout(timerRef.current);
|
|
184
|
+
timerRef.current = null;
|
|
78
185
|
}
|
|
79
|
-
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
186
|
+
setBuildingId(null);
|
|
187
|
+
}, []);
|
|
188
|
+
|
|
189
|
+
const handleBuild = async (record: SpaceRecord) => {
|
|
190
|
+
setBuildingId(record.id);
|
|
191
|
+
try {
|
|
192
|
+
await api.resource('aiBuildGuideSpaces').build({ filterByTk: record.id });
|
|
193
|
+
message.success(t('Build started'));
|
|
194
|
+
const startedAt = Date.now();
|
|
195
|
+
let stillRunningNotified = false;
|
|
196
|
+
const poll = async () => {
|
|
197
|
+
try {
|
|
198
|
+
const res = await api.resource('aiBuildGuideSpaces').get({ filterByTk: record.id });
|
|
199
|
+
const status = res?.data?.data?.status;
|
|
200
|
+
if (status !== 'building') {
|
|
201
|
+
stopPolling();
|
|
202
|
+
await loadList();
|
|
203
|
+
if (status === 'completed') message.success(t('Build completed'));
|
|
204
|
+
else if (status === 'error') message.error(t('Build failed'));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const elapsed = Date.now() - startedAt;
|
|
208
|
+
if (elapsed >= STILL_RUNNING_AFTER_MS && !stillRunningNotified) {
|
|
209
|
+
stillRunningNotified = true;
|
|
210
|
+
message.info(t('Build is still running'));
|
|
211
|
+
}
|
|
212
|
+
const next = elapsed >= STILL_RUNNING_AFTER_MS ? SLOW_POLL_INTERVAL_MS : POLL_INTERVAL_MS;
|
|
213
|
+
timerRef.current = setTimeout(poll, next);
|
|
214
|
+
} catch {
|
|
215
|
+
stopPolling();
|
|
216
|
+
await loadList();
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
timerRef.current = setTimeout(poll, POLL_INTERVAL_MS);
|
|
220
|
+
} catch (err: any) {
|
|
221
|
+
message.error(err?.response?.data?.error?.message || t('Build failed'));
|
|
222
|
+
setBuildingId(null);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const columns = [
|
|
227
|
+
{ title: t('Title'), dataIndex: 'title' },
|
|
228
|
+
{
|
|
229
|
+
title: t('Status'),
|
|
230
|
+
dataIndex: 'status',
|
|
231
|
+
render: (v: string) => (v ? <Tag color={STATUS_COLORS[v] || 'default'}>{String(v).toUpperCase()}</Tag> : null),
|
|
232
|
+
},
|
|
233
|
+
{ title: t('Build Phase'), dataIndex: 'buildPhase' },
|
|
234
|
+
{ title: t('Chapters'), dataIndex: 'pageCount' },
|
|
235
|
+
{
|
|
236
|
+
title: t('Actions'),
|
|
237
|
+
key: 'actions',
|
|
238
|
+
render: (_: unknown, record: SpaceRecord) => (
|
|
239
|
+
<Space split="|">
|
|
240
|
+
<Button
|
|
241
|
+
type="link"
|
|
242
|
+
icon={<PlayCircleOutlined />}
|
|
243
|
+
loading={buildingId === record.id}
|
|
244
|
+
disabled={record.status === 'building'}
|
|
245
|
+
onClick={() => handleBuild(record)}
|
|
246
|
+
>
|
|
247
|
+
{t('Build')}
|
|
248
|
+
</Button>
|
|
249
|
+
<a onClick={() => openEdit(record)}>{t('Edit')}</a>
|
|
250
|
+
<a
|
|
251
|
+
onClick={() =>
|
|
252
|
+
modal.confirm({
|
|
253
|
+
title: t('Delete'),
|
|
254
|
+
content: t('Are you sure you want to delete this space?'),
|
|
255
|
+
onOk: () => handleDelete(record),
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
>
|
|
259
|
+
{t('Delete')}
|
|
260
|
+
</a>
|
|
261
|
+
</Space>
|
|
262
|
+
),
|
|
263
|
+
},
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<div>
|
|
268
|
+
<Space style={{ marginBottom: 16 }}>
|
|
269
|
+
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
|
270
|
+
{t('Create space')}
|
|
271
|
+
</Button>
|
|
272
|
+
<Button onClick={loadList}>{t('Refresh')}</Button>
|
|
273
|
+
</Space>
|
|
274
|
+
<Table rowKey="id" loading={loading} dataSource={data} columns={columns} />
|
|
275
|
+
<Drawer
|
|
276
|
+
title={editingId ? t('Edit space') : t('Create space')}
|
|
277
|
+
open={drawerOpen}
|
|
278
|
+
onClose={() => setDrawerOpen(false)}
|
|
279
|
+
width={640}
|
|
280
|
+
footer={
|
|
281
|
+
<Space style={{ float: 'right' }}>
|
|
282
|
+
<Button onClick={() => setDrawerOpen(false)}>{t('Cancel')}</Button>
|
|
283
|
+
<Button type="primary" loading={submitting} onClick={handleSubmit}>
|
|
284
|
+
{t('Submit')}
|
|
285
|
+
</Button>
|
|
286
|
+
</Space>
|
|
287
|
+
}
|
|
288
|
+
>
|
|
289
|
+
<Form
|
|
290
|
+
form={form}
|
|
291
|
+
layout="vertical"
|
|
292
|
+
onValuesChange={(changed) => {
|
|
293
|
+
if ('llmService' in changed) form.setFieldValue('model', undefined);
|
|
294
|
+
}}
|
|
295
|
+
>
|
|
296
|
+
<Form.Item name="title" label={t('Title')} rules={[{ required: true }]}>
|
|
297
|
+
<Input />
|
|
298
|
+
</Form.Item>
|
|
299
|
+
<Form.Item name="llmService" label={t('LLM Service')} rules={[{ required: true }]}>
|
|
300
|
+
<Select options={serviceOptions} onFocus={loadServices} showSearch optionFilterProp="label" />
|
|
301
|
+
</Form.Item>
|
|
302
|
+
<Form.Item name="model" label={t('Model')} rules={[{ required: true }]}>
|
|
303
|
+
<Select options={modelOptions} disabled={!selectedService} showSearch optionFilterProp="label" />
|
|
304
|
+
</Form.Item>
|
|
305
|
+
<Form.Item name="outputFormat" label={t('Output format')} rules={[{ required: true }]}>
|
|
306
|
+
<Select options={OUTPUT_FORMAT_OPTIONS} />
|
|
307
|
+
</Form.Item>
|
|
308
|
+
<Form.Item name="targetChapterCount" label={t('Target chapters')} rules={[{ required: true }]}>
|
|
309
|
+
<InputNumber
|
|
310
|
+
min={MIN_TARGET_CHAPTER_COUNT}
|
|
311
|
+
max={MAX_TARGET_CHAPTER_COUNT}
|
|
312
|
+
precision={0}
|
|
313
|
+
style={{ width: '100%' }}
|
|
314
|
+
/>
|
|
315
|
+
</Form.Item>
|
|
316
|
+
<Form.Item name="chapterGuidance" label={t('Chapter guidance')}>
|
|
317
|
+
<Input.TextArea rows={3} placeholder={t('Describe how the guide should be split into chapters')} />
|
|
318
|
+
</Form.Item>
|
|
319
|
+
<Form.Item name="systemPrompt" label={t('System Prompt')}>
|
|
320
|
+
<Input.TextArea rows={4} />
|
|
321
|
+
</Form.Item>
|
|
322
|
+
</Form>
|
|
323
|
+
</Drawer>
|
|
324
|
+
</div>
|
|
325
|
+
);
|
|
326
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './plugin';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Plugin, Application } from '@nocobase/client-v2';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
export class PluginBuildGuideBlockClient extends Plugin<Record<string, never>, Application> {
|
|
5
|
+
async load() {
|
|
6
|
+
this.pluginSettingsManager.addMenuItem({
|
|
7
|
+
key: 'ai-build-guide',
|
|
8
|
+
title: this.t('Build Guide Block'),
|
|
9
|
+
icon: 'ReadOutlined',
|
|
10
|
+
aclSnippet: 'pm.ai-build-guide',
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
14
|
+
menuKey: 'ai-build-guide',
|
|
15
|
+
key: 'index',
|
|
16
|
+
title: this.t('Build Guide Block'),
|
|
17
|
+
|
|
18
|
+
componentLoader: () => import('../client/UserGuideManager').then(m => ({ default: m.UserGuideManager })),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default PluginBuildGuideBlockClient;
|
|
@@ -328,6 +328,10 @@ function toPlainText(value: unknown) {
|
|
|
328
328
|
return JSON.stringify(value);
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
+
function stripThink(text: string) {
|
|
332
|
+
return text.replace(/<think>[\s\S]*?(?:<\/think>|$)/gi, '').trim();
|
|
333
|
+
}
|
|
334
|
+
|
|
331
335
|
function stripFence(text: string) {
|
|
332
336
|
return text
|
|
333
337
|
.replace(/^```(?:json|markdown|md|html)?\s*/i, '')
|
|
@@ -398,7 +402,7 @@ function createFallbackPlan(guideTitle: string, targetChapterCount: number): Gui
|
|
|
398
402
|
|
|
399
403
|
function normalizePlan(rawText: string, guideTitle: string, targetChapterCount: number): GuidePlan {
|
|
400
404
|
const targetCount = clampChapterCount(targetChapterCount);
|
|
401
|
-
const cleanText = stripFence(rawText);
|
|
405
|
+
const cleanText = stripFence(stripThink(rawText));
|
|
402
406
|
const jsonStart = cleanText.indexOf('{');
|
|
403
407
|
const jsonEnd = cleanText.lastIndexOf('}');
|
|
404
408
|
const jsonText = jsonStart >= 0 && jsonEnd > jsonStart ? cleanText.slice(jsonStart, jsonEnd + 1) : cleanText;
|
|
@@ -514,7 +518,7 @@ Source documents:
|
|
|
514
518
|
${documentsText.slice(0, MAX_SOURCE_CHARS)}`),
|
|
515
519
|
);
|
|
516
520
|
const response = await provider.chatModel.invoke(messages);
|
|
517
|
-
return stripFence(toPlainText(response.content));
|
|
521
|
+
return stripFence(stripThink(toPlainText(response.content)));
|
|
518
522
|
}
|
|
519
523
|
|
|
520
524
|
async function markdownToCleanHtml(markdown: string) {
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
let random = async bytes => crypto.getRandomValues(new Uint8Array(bytes))
|
|
2
|
-
let customAlphabet = (alphabet, defaultSize = 21) => {
|
|
3
|
-
let mask = (2 << (Math.log(alphabet.length - 1) / Math.LN2)) - 1
|
|
4
|
-
let step = -~((1.6 * mask * defaultSize) / alphabet.length)
|
|
5
|
-
return async (size = defaultSize) => {
|
|
6
|
-
let id = ''
|
|
7
|
-
while (true) {
|
|
8
|
-
let bytes = crypto.getRandomValues(new Uint8Array(step))
|
|
9
|
-
let i = step
|
|
10
|
-
while (i--) {
|
|
11
|
-
id += alphabet[bytes[i] & mask] || ''
|
|
12
|
-
if (id.length === size) return id
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
let nanoid = async (size = 21) => {
|
|
18
|
-
let id = ''
|
|
19
|
-
let bytes = crypto.getRandomValues(new Uint8Array(size))
|
|
20
|
-
while (size--) {
|
|
21
|
-
let byte = bytes[size] & 63
|
|
22
|
-
if (byte < 36) {
|
|
23
|
-
id += byte.toString(36)
|
|
24
|
-
} else if (byte < 62) {
|
|
25
|
-
id += (byte - 26).toString(36).toUpperCase()
|
|
26
|
-
} else if (byte < 63) {
|
|
27
|
-
id += '_'
|
|
28
|
-
} else {
|
|
29
|
-
id += '-'
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return id
|
|
33
|
-
}
|
|
34
|
-
module.exports = { nanoid, customAlphabet, random }
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
let random = async bytes => crypto.getRandomValues(new Uint8Array(bytes))
|
|
2
|
-
let customAlphabet = (alphabet, defaultSize = 21) => {
|
|
3
|
-
let mask = (2 << (Math.log(alphabet.length - 1) / Math.LN2)) - 1
|
|
4
|
-
let step = -~((1.6 * mask * defaultSize) / alphabet.length)
|
|
5
|
-
return async (size = defaultSize) => {
|
|
6
|
-
let id = ''
|
|
7
|
-
while (true) {
|
|
8
|
-
let bytes = crypto.getRandomValues(new Uint8Array(step))
|
|
9
|
-
let i = step
|
|
10
|
-
while (i--) {
|
|
11
|
-
id += alphabet[bytes[i] & mask] || ''
|
|
12
|
-
if (id.length === size) return id
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
let nanoid = async (size = 21) => {
|
|
18
|
-
let id = ''
|
|
19
|
-
let bytes = crypto.getRandomValues(new Uint8Array(size))
|
|
20
|
-
while (size--) {
|
|
21
|
-
let byte = bytes[size] & 63
|
|
22
|
-
if (byte < 36) {
|
|
23
|
-
id += byte.toString(36)
|
|
24
|
-
} else if (byte < 62) {
|
|
25
|
-
id += (byte - 26).toString(36).toUpperCase()
|
|
26
|
-
} else if (byte < 63) {
|
|
27
|
-
id += '_'
|
|
28
|
-
} else {
|
|
29
|
-
id += '-'
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return id
|
|
33
|
-
}
|
|
34
|
-
export { nanoid, customAlphabet, random }
|