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.
Files changed (91) hide show
  1. package/client-v2.d.ts +2 -0
  2. package/client-v2.js +1 -0
  3. package/dist/client/index.js +2 -2
  4. package/dist/client-v2/376.bea427a4cf8864b4.js +10 -0
  5. package/dist/client-v2/index.js +10 -0
  6. package/dist/externalVersion.js +9 -8
  7. package/dist/node_modules/sanitize-html/index.js +2 -2
  8. package/dist/node_modules/sanitize-html/package.json +1 -1
  9. package/dist/server/actions/build.js +5 -2
  10. package/package.json +8 -4
  11. package/src/client/UserGuideManager.tsx +307 -138
  12. package/src/client-v2/index.tsx +1 -0
  13. package/src/client-v2/plugin.tsx +24 -0
  14. package/src/server/actions/build.ts +6 -2
  15. package/dist/node_modules/sanitize-html/node_modules/nanoid/async/index.browser.cjs +0 -34
  16. package/dist/node_modules/sanitize-html/node_modules/nanoid/async/index.browser.js +0 -34
  17. package/dist/node_modules/sanitize-html/node_modules/nanoid/async/index.cjs +0 -35
  18. package/dist/node_modules/sanitize-html/node_modules/nanoid/async/index.d.ts +0 -56
  19. package/dist/node_modules/sanitize-html/node_modules/nanoid/async/index.js +0 -35
  20. package/dist/node_modules/sanitize-html/node_modules/nanoid/async/index.native.js +0 -26
  21. package/dist/node_modules/sanitize-html/node_modules/nanoid/async/package.json +0 -12
  22. package/dist/node_modules/sanitize-html/node_modules/nanoid/bin/nanoid.cjs +0 -55
  23. package/dist/node_modules/sanitize-html/node_modules/nanoid/index.browser.cjs +0 -34
  24. package/dist/node_modules/sanitize-html/node_modules/nanoid/index.browser.js +0 -34
  25. package/dist/node_modules/sanitize-html/node_modules/nanoid/index.cjs +0 -45
  26. package/dist/node_modules/sanitize-html/node_modules/nanoid/index.d.cts +0 -91
  27. package/dist/node_modules/sanitize-html/node_modules/nanoid/index.d.ts +0 -91
  28. package/dist/node_modules/sanitize-html/node_modules/nanoid/index.js +0 -45
  29. package/dist/node_modules/sanitize-html/node_modules/nanoid/nanoid.js +0 -1
  30. package/dist/node_modules/sanitize-html/node_modules/nanoid/non-secure/index.cjs +0 -21
  31. package/dist/node_modules/sanitize-html/node_modules/nanoid/non-secure/index.d.ts +0 -33
  32. package/dist/node_modules/sanitize-html/node_modules/nanoid/non-secure/index.js +0 -21
  33. package/dist/node_modules/sanitize-html/node_modules/nanoid/non-secure/package.json +0 -6
  34. package/dist/node_modules/sanitize-html/node_modules/nanoid/package.json +0 -88
  35. package/dist/node_modules/sanitize-html/node_modules/nanoid/url-alphabet/index.cjs +0 -3
  36. package/dist/node_modules/sanitize-html/node_modules/nanoid/url-alphabet/index.js +0 -3
  37. package/dist/node_modules/sanitize-html/node_modules/nanoid/url-alphabet/package.json +0 -6
  38. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/at-rule.d.ts +0 -115
  39. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/at-rule.js +0 -25
  40. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/comment.d.ts +0 -67
  41. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/comment.js +0 -13
  42. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/container.d.ts +0 -452
  43. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/container.js +0 -439
  44. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/css-syntax-error.d.ts +0 -248
  45. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/css-syntax-error.js +0 -100
  46. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/declaration.d.ts +0 -148
  47. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/declaration.js +0 -24
  48. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/document.d.ts +0 -68
  49. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/document.js +0 -33
  50. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/fromJSON.d.ts +0 -9
  51. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/fromJSON.js +0 -54
  52. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/input.d.ts +0 -194
  53. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/input.js +0 -248
  54. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/lazy-result.d.ts +0 -190
  55. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/lazy-result.js +0 -550
  56. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/list.d.ts +0 -57
  57. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/list.js +0 -58
  58. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/map-generator.js +0 -359
  59. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/no-work-result.d.ts +0 -46
  60. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/no-work-result.js +0 -135
  61. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/node.d.ts +0 -536
  62. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/node.js +0 -381
  63. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/parse.d.ts +0 -9
  64. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/parse.js +0 -42
  65. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/parser.js +0 -610
  66. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/postcss.d.mts +0 -72
  67. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/postcss.d.ts +0 -441
  68. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/postcss.js +0 -101
  69. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/previous-map.d.ts +0 -81
  70. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/previous-map.js +0 -142
  71. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/processor.d.ts +0 -115
  72. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/processor.js +0 -67
  73. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/result.d.ts +0 -206
  74. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/result.js +0 -42
  75. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/root.d.ts +0 -86
  76. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/root.js +0 -61
  77. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/rule.d.ts +0 -113
  78. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/rule.js +0 -27
  79. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/stringifier.d.ts +0 -46
  80. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/stringifier.js +0 -353
  81. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/stringify.d.ts +0 -9
  82. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/stringify.js +0 -11
  83. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/symbols.js +0 -5
  84. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/terminal-highlight.js +0 -70
  85. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/tokenize.js +0 -266
  86. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/warn-once.js +0 -13
  87. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/warning.d.ts +0 -147
  88. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/warning.js +0 -37
  89. package/dist/node_modules/sanitize-html/node_modules/postcss/node_modules/.bin/nanoid +0 -15
  90. package/dist/node_modules/sanitize-html/node_modules/postcss/node_modules/.bin/nanoid.cmd +0 -7
  91. package/dist/node_modules/sanitize-html/node_modules/postcss/package.json +0 -88
@@ -1 +1 @@
1
- {"name":"sanitize-html","version":"2.17.4","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-05-22T04:55:04.489Z"}
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.6",
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, { useMemo } from 'react';
2
- import {
3
- SchemaComponent,
4
- useActionContext,
5
- useCollectionRecordData,
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
- import { LLMServiceSelect } from './components/LLMServiceSelect';
22
- import { ModelSelect } from './components/ModelSelect';
23
- import { StatusTag } from './components/StatusTag';
24
- import { BuildButton } from './components/BuildButton';
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
- const useCreateFormProps = () => {
36
- const form = useMemo(
37
- () =>
38
- createForm({
39
- initialValues: {
40
- targetChapterCount: DEFAULT_TARGET_CHAPTER_COUNT,
41
- },
42
- }),
43
- [],
44
- );
45
- return { form };
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 useEditFormProps = () => {
49
- const record = useCollectionRecordData();
50
- const form = useMemo(
51
- () =>
52
- createForm({
53
- initialValues: {
54
- ...record,
55
- targetChapterCount: normalizeTargetChapterCount(record?.targetChapterCount),
56
- },
57
- }),
58
- [record],
59
- );
60
- return { form };
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 useCancelActionProps = () => {
64
- const { setVisible } = useActionContext();
65
- return {
66
- type: 'default',
67
- onClick() {
68
- setVisible(false);
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
- return rest;
80
- };
81
-
82
- const useCreateActionProps = () => {
83
- const { setVisible } = useActionContext();
84
- const { message } = App.useApp();
85
- const resource = useDataBlockResource();
86
- const { refresh } = useDataBlockRequest();
87
- const form = useForm();
88
-
89
- return {
90
- type: 'primary',
91
- async onClick() {
92
- try {
93
- await form.submit();
94
- await resource.create({ values: normalizeValues(form.values) });
95
- refresh();
96
- message.success(t('Saved successfully'));
97
- setVisible(false);
98
- } catch (err: any) {
99
- if (err?.name !== 'ValidateError') {
100
- message.error(err?.message || t('Save failed'));
101
- }
102
- }
103
- },
104
- };
105
- };
106
-
107
- const useUpdateActionProps = () => {
108
- const { setVisible } = useActionContext();
109
- const { message } = App.useApp();
110
- const resource = useDataBlockResource();
111
- const { refresh } = useDataBlockRequest();
112
- const record = useCollectionRecordData();
113
- const form = useForm();
114
-
115
- return {
116
- type: 'primary',
117
- async onClick() {
118
- try {
119
- await form.submit();
120
- await resource.update({
121
- filterByTk: record.id,
122
- values: normalizeValues(form.values),
123
- });
124
- refresh();
125
- message.success(t('Saved successfully'));
126
- setVisible(false);
127
- } catch (err: any) {
128
- if (err?.name !== 'ValidateError') {
129
- message.error(err?.message || t('Save failed'));
130
- }
131
- }
132
- },
133
- };
134
- };
135
-
136
- return (
137
- <SchemaComponent
138
- schema={spacesSchema}
139
- components={{
140
- LLMServiceSelect,
141
- ModelSelect,
142
- StatusTag,
143
- BuildButton,
144
- }}
145
- scope={{
146
- t,
147
- useCreateFormProps,
148
- useEditFormProps,
149
- useCancelActionProps,
150
- useCreateActionProps,
151
- useUpdateActionProps,
152
- useDestroyActionProps,
153
- useTableBlockProps,
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 }