plugin-build-guide-block 1.1.2 → 1.1.3

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.
@@ -1,26 +1,17 @@
1
- /**
2
- * This file is part of the NocoBase (R) project.
3
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
- * Authors: NocoBase Team.
5
- *
6
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
- * For more information, please refer to: https://www.nocobase.com/agreement.
8
- */
9
-
10
1
  module.exports = {
11
- "@nocobase/client": "2.0.45",
2
+ "@nocobase/client": "2.0.46",
12
3
  "react": "18.2.0",
13
4
  "@formily/react": "2.3.7",
14
5
  "antd": "5.24.2",
15
6
  "react-i18next": "11.18.6",
16
7
  "@ant-design/icons": "5.6.1",
17
8
  "@formily/core": "2.3.7",
18
- "@nocobase/server": "2.0.45",
19
- "@nocobase/flow-engine": "2.0.45",
20
- "@nocobase/actions": "2.0.45",
21
- "@nocobase/database": "2.0.45",
22
- "@nocobase/plugin-ai": "2.0.45",
23
- "@nocobase/plugin-file-manager": "2.0.45",
9
+ "@nocobase/server": "2.0.46",
10
+ "@nocobase/flow-engine": "2.0.46",
11
+ "@nocobase/actions": "2.0.46",
12
+ "@nocobase/database": "2.0.46",
13
+ "@nocobase/plugin-ai": "2.0.46",
14
+ "@nocobase/plugin-file-manager": "2.0.46",
24
15
  "@langchain/core": "1.1.24",
25
16
  "axios": "1.7.7"
26
17
  };
package/dist/index.js CHANGED
@@ -1,12 +1,3 @@
1
- /**
2
- * This file is part of the NocoBase (R) project.
3
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
- * Authors: NocoBase Team.
5
- *
6
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
- * For more information, please refer to: https://www.nocobase.com/agreement.
8
- */
9
-
10
1
  var __create = Object.create;
11
2
  var __defProp = Object.defineProperty;
12
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -1,12 +1,3 @@
1
- /**
2
- * This file is part of the NocoBase (R) project.
3
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
- * Authors: NocoBase Team.
5
- *
6
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
- * For more information, please refer to: https://www.nocobase.com/agreement.
8
- */
9
-
10
1
  var __defProp = Object.defineProperty;
11
2
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -15,10 +15,18 @@
15
15
  "Actions": "Hành động",
16
16
  "Edit": "Sửa",
17
17
  "Edit space": "Sửa Space",
18
- "Generated HTML": "HTML Đã tạo",
19
- "Generated Markdown": "Markdown Đã tạo",
20
- "Output format": "Định dạng đầu ra",
21
- "Select Space": "Chọn Space",
18
+ "Generated HTML": "HTML Đã tạo",
19
+ "Generated Markdown": "Markdown Đã tạo",
20
+ "Output format": "Định dạng đầu ra",
21
+ "Target chapters": "Số chapter cần tạo",
22
+ "Chapter guidance": "Hướng dẫn chia chapter",
23
+ "Describe how the guide should be split into chapters": "Mô tả cách chia hướng dẫn thành các chapter",
24
+ "Build Phase": "Giai đoạn Build",
25
+ "Chapters": "Chapter",
26
+ "Breakdown Plan": "Kế hoạch chia nội dung",
27
+ "Contents": "Mục lục",
28
+ "No guide content available": "Chưa có nội dung hướng dẫn",
29
+ "Select Space": "Chọn Space",
22
30
  "Build Log": "Log quá trình Build",
23
31
  "Delete": "Xóa",
24
32
  "Saved successfully": "Lưu thành công",
@@ -15,10 +15,18 @@
15
15
  "Actions": "操作",
16
16
  "Edit": "编辑",
17
17
  "Edit space": "编辑空间",
18
- "Generated HTML": "生成的 HTML",
19
- "Generated Markdown": "生成的 Markdown",
20
- "Output format": "输出格式",
21
- "Select Space": "选择空间",
18
+ "Generated HTML": "生成的 HTML",
19
+ "Generated Markdown": "生成的 Markdown",
20
+ "Output format": "输出格式",
21
+ "Target chapters": "目标章节数",
22
+ "Chapter guidance": "章节拆分说明",
23
+ "Describe how the guide should be split into chapters": "描述应如何将指南拆分为章节",
24
+ "Build Phase": "构建阶段",
25
+ "Chapters": "章节",
26
+ "Breakdown Plan": "拆分计划",
27
+ "Contents": "目录",
28
+ "No guide content available": "暂无指南内容",
29
+ "Select Space": "选择空间",
22
30
  "Build Log": "构建日志",
23
31
  "Delete": "删除",
24
32
  "Saved successfully": "保存成功",
@@ -1,12 +1,3 @@
1
- /**
2
- * This file is part of the NocoBase (R) project.
3
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
- * Authors: NocoBase Team.
5
- *
6
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
- * For more information, please refer to: https://www.nocobase.com/agreement.
8
- */
9
-
10
1
  var __create = Object.create;
11
2
  var __defProp = Object.defineProperty;
12
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -47,7 +38,7 @@ var import_path = __toESM(require("path"));
47
38
  var import_crypto = __toESM(require("crypto"));
48
39
  var import_marked = require("marked");
49
40
  const MAX_SOURCE_CHARS = 9e4;
50
- const MIN_CHAPTERS = 3;
41
+ const MIN_CHAPTERS = 1;
51
42
  const MAX_CHAPTERS = 12;
52
43
  const DEFAULT_TARGET_CHAPTERS = 5;
53
44
  const SANITIZE_OPTIONS = {
@@ -192,6 +183,7 @@ function createFallbackPlan(guideTitle, targetChapterCount) {
192
183
  };
193
184
  }
194
185
  function normalizePlan(rawText, guideTitle, targetChapterCount) {
186
+ const targetCount = clampChapterCount(targetChapterCount);
195
187
  const cleanText = stripFence(rawText);
196
188
  const jsonStart = cleanText.indexOf("{");
197
189
  const jsonEnd = cleanText.lastIndexOf("}");
@@ -200,19 +192,25 @@ function normalizePlan(rawText, guideTitle, targetChapterCount) {
200
192
  try {
201
193
  parsed = JSON.parse(jsonText);
202
194
  } catch {
203
- return createFallbackPlan(guideTitle, targetChapterCount);
195
+ return createFallbackPlan(guideTitle, targetCount);
204
196
  }
205
197
  const rawChapters = Array.isArray(parsed == null ? void 0 : parsed.chapters) ? parsed.chapters : [];
206
- const chapters = rawChapters.slice(0, MAX_CHAPTERS).map((item, index) => ({
207
- title: String((item == null ? void 0 : item.title) || `Section ${index + 1}`),
208
- goal: (item == null ? void 0 : item.goal) ? String(item.goal) : "",
209
- sourceHints: Array.isArray(item == null ? void 0 : item.sourceHints) ? item.sourceHints.map((hint) => String(hint)) : []
210
- })).filter((item) => item.title);
211
- if (chapters.length < MIN_CHAPTERS) {
212
- return createFallbackPlan((parsed == null ? void 0 : parsed.title) ? String(parsed.title) : guideTitle, targetChapterCount);
213
- }
198
+ const planTitle = (parsed == null ? void 0 : parsed.title) ? String(parsed.title) : guideTitle;
199
+ const fallbackPlan = createFallbackPlan(planTitle, targetCount);
200
+ const chapters = fallbackPlan.chapters.map((fallback, index) => {
201
+ const item = rawChapters[index];
202
+ if (!item) return fallback;
203
+ const title = typeof item === "string" ? item : item == null ? void 0 : item.title;
204
+ const goal = typeof item === "object" ? item == null ? void 0 : item.goal : void 0;
205
+ const sourceHints = typeof item === "object" && Array.isArray(item == null ? void 0 : item.sourceHints) ? item.sourceHints : void 0;
206
+ return {
207
+ title: String(title || fallback.title),
208
+ goal: goal ? String(goal) : fallback.goal,
209
+ sourceHints: sourceHints ? sourceHints.map((hint) => String(hint)) : fallback.sourceHints
210
+ };
211
+ });
214
212
  return {
215
- title: (parsed == null ? void 0 : parsed.title) ? String(parsed.title) : guideTitle,
213
+ title: planTitle,
216
214
  chapters
217
215
  };
218
216
  }
@@ -247,8 +245,8 @@ Return ONLY valid JSON with this shape:
247
245
  }
248
246
 
249
247
  Rules:
250
- - Create exactly ${targetCount} chapters.
251
- - Each chapter must cover a distinct user goal. Do not collapse the guide into a single chapter.
248
+ - Create exactly ${targetCount} chapter${targetCount === 1 ? "" : "s"}.
249
+ - If more than one chapter is requested, each chapter must cover a distinct user goal.
252
250
  - Keep chapter titles user-facing and action-oriented.
253
251
  - Do not include markdown fences or explanations.
254
252
 
@@ -1,12 +1,3 @@
1
- /**
2
- * This file is part of the NocoBase (R) project.
3
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
- * Authors: NocoBase Team.
5
- *
6
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
- * For more information, please refer to: https://www.nocobase.com/agreement.
8
- */
9
-
10
1
  var __defProp = Object.defineProperty;
11
2
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -1,12 +1,3 @@
1
- /**
2
- * This file is part of the NocoBase (R) project.
3
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
- * Authors: NocoBase Team.
5
- *
6
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
- * For more information, please refer to: https://www.nocobase.com/agreement.
8
- */
9
-
10
1
  var __defProp = Object.defineProperty;
11
2
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -1,12 +1,3 @@
1
- /**
2
- * This file is part of the NocoBase (R) project.
3
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
- * Authors: NocoBase Team.
5
- *
6
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
- * For more information, please refer to: https://www.nocobase.com/agreement.
8
- */
9
-
10
1
  var __defProp = Object.defineProperty;
11
2
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -1,12 +1,3 @@
1
- /**
2
- * This file is part of the NocoBase (R) project.
3
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
- * Authors: NocoBase Team.
5
- *
6
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
- * For more information, please refer to: https://www.nocobase.com/agreement.
8
- */
9
-
10
1
  var __defProp = Object.defineProperty;
11
2
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -1,12 +1,3 @@
1
- /**
2
- * This file is part of the NocoBase (R) project.
3
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
- * Authors: NocoBase Team.
5
- *
6
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
- * For more information, please refer to: https://www.nocobase.com/agreement.
8
- */
9
-
10
1
  var __create = Object.create;
11
2
  var __defProp = Object.defineProperty;
12
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -1,12 +1,3 @@
1
- /**
2
- * This file is part of the NocoBase (R) project.
3
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
- * Authors: NocoBase Team.
5
- *
6
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
- * For more information, please refer to: https://www.nocobase.com/agreement.
8
- */
9
-
10
1
  var __defProp = Object.defineProperty;
11
2
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
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.2",
8
+ "version": "1.1.3",
9
9
  "license": "Apache-2.0",
10
10
  "keywords": [
11
11
  "ai",
@@ -128,7 +128,7 @@ const guideStyles = `
128
128
  .user-guide-shell {
129
129
  display: grid;
130
130
  grid-template-columns: minmax(0, 1fr);
131
- max-width: 1180px;
131
+ max-width: 95%;
132
132
  margin: 0 auto;
133
133
  }
134
134
 
@@ -175,7 +175,7 @@ const guideStyles = `
175
175
  }
176
176
 
177
177
  .user-guide-content {
178
- max-width: 920px;
178
+ max-width: 95%;
179
179
  margin: 0 auto;
180
180
  padding: 32px;
181
181
  color: rgba(0, 0, 0, 0.82);
@@ -12,25 +12,53 @@ import { createForm } from '@formily/core';
12
12
  import { useForm } from '@formily/react';
13
13
  import { App } from 'antd';
14
14
  import { useTranslation } from 'react-i18next';
15
- import { spacesSchema } from './schemas/spacesSchema';
15
+ import {
16
+ DEFAULT_TARGET_CHAPTER_COUNT,
17
+ MAX_TARGET_CHAPTER_COUNT,
18
+ MIN_TARGET_CHAPTER_COUNT,
19
+ spacesSchema,
20
+ } from './schemas/spacesSchema';
16
21
  import { LLMServiceSelect } from './components/LLMServiceSelect';
17
- import { ModelSelect } from './components/ModelSelect';
18
- import { StatusTag } from './components/StatusTag';
19
- import { BuildButton } from './components/BuildButton';
20
-
21
- export const UserGuideManager = () => {
22
- const { t } = useTranslation();
23
-
24
- const useCreateFormProps = () => {
25
- const form = useMemo(() => createForm(), []);
26
- return { form };
27
- };
28
-
29
- const useEditFormProps = () => {
30
- const record = useCollectionRecordData();
31
- const form = useMemo(() => createForm({ initialValues: record }), [record]);
32
- return { form };
33
- };
22
+ import { ModelSelect } from './components/ModelSelect';
23
+ import { StatusTag } from './components/StatusTag';
24
+ import { BuildButton } from './components/BuildButton';
25
+
26
+ const normalizeTargetChapterCount = (value: unknown) => {
27
+ const count = Number(value);
28
+ if (!Number.isFinite(count)) return DEFAULT_TARGET_CHAPTER_COUNT;
29
+ return Math.max(MIN_TARGET_CHAPTER_COUNT, Math.min(MAX_TARGET_CHAPTER_COUNT, Math.round(count)));
30
+ };
31
+
32
+ export const UserGuideManager = () => {
33
+ const { t } = useTranslation();
34
+
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 };
46
+ };
47
+
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 };
61
+ };
34
62
 
35
63
  const useCancelActionProps = () => {
36
64
  const { setVisible } = useActionContext();
@@ -42,11 +70,12 @@ export const UserGuideManager = () => {
42
70
  };
43
71
  };
44
72
 
45
- const normalizeValues = (values: any) => {
46
- const { documents, ...rest } = values;
47
- if (Array.isArray(documents)) {
48
- rest.documents = documents.map((doc: any) => (typeof doc === 'object' && doc?.id ? { id: doc.id } : doc));
49
- }
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));
78
+ }
50
79
  return rest;
51
80
  };
52
81
 
@@ -1,3 +1,7 @@
1
+ export const MIN_TARGET_CHAPTER_COUNT = 1;
2
+ export const MAX_TARGET_CHAPTER_COUNT = 12;
3
+ export const DEFAULT_TARGET_CHAPTER_COUNT = 5;
4
+
1
5
  export const spacesSchema = {
2
6
  type: 'void',
3
7
  name: 'ai-build-guide-spaces',
@@ -91,12 +95,12 @@ export const spacesSchema = {
91
95
  type: 'number',
92
96
  title: '{{t("Target chapters")}}',
93
97
  required: true,
94
- default: 5,
98
+ default: DEFAULT_TARGET_CHAPTER_COUNT,
95
99
  'x-decorator': 'FormItem',
96
100
  'x-component': 'InputNumber',
97
101
  'x-component-props': {
98
- min: 3,
99
- max: 12,
102
+ min: MIN_TARGET_CHAPTER_COUNT,
103
+ max: MAX_TARGET_CHAPTER_COUNT,
100
104
  precision: 0,
101
105
  style: {
102
106
  width: '100%',
@@ -310,11 +314,12 @@ export const spacesSchema = {
310
314
  type: 'number',
311
315
  title: '{{t("Target chapters")}}',
312
316
  required: true,
317
+ default: DEFAULT_TARGET_CHAPTER_COUNT,
313
318
  'x-decorator': 'FormItem',
314
319
  'x-component': 'InputNumber',
315
320
  'x-component-props': {
316
- min: 3,
317
- max: 12,
321
+ min: MIN_TARGET_CHAPTER_COUNT,
322
+ max: MAX_TARGET_CHAPTER_COUNT,
318
323
  precision: 0,
319
324
  style: {
320
325
  width: '100%',
@@ -15,10 +15,18 @@
15
15
  "Actions": "Hành động",
16
16
  "Edit": "Sửa",
17
17
  "Edit space": "Sửa Space",
18
- "Generated HTML": "HTML Đã tạo",
19
- "Generated Markdown": "Markdown Đã tạo",
20
- "Output format": "Định dạng đầu ra",
21
- "Select Space": "Chọn Space",
18
+ "Generated HTML": "HTML Đã tạo",
19
+ "Generated Markdown": "Markdown Đã tạo",
20
+ "Output format": "Định dạng đầu ra",
21
+ "Target chapters": "Số chapter cần tạo",
22
+ "Chapter guidance": "Hướng dẫn chia chapter",
23
+ "Describe how the guide should be split into chapters": "Mô tả cách chia hướng dẫn thành các chapter",
24
+ "Build Phase": "Giai đoạn Build",
25
+ "Chapters": "Chapter",
26
+ "Breakdown Plan": "Kế hoạch chia nội dung",
27
+ "Contents": "Mục lục",
28
+ "No guide content available": "Chưa có nội dung hướng dẫn",
29
+ "Select Space": "Chọn Space",
22
30
  "Build Log": "Log quá trình Build",
23
31
  "Delete": "Xóa",
24
32
  "Saved successfully": "Lưu thành công",
@@ -15,10 +15,18 @@
15
15
  "Actions": "操作",
16
16
  "Edit": "编辑",
17
17
  "Edit space": "编辑空间",
18
- "Generated HTML": "生成的 HTML",
19
- "Generated Markdown": "生成的 Markdown",
20
- "Output format": "输出格式",
21
- "Select Space": "选择空间",
18
+ "Generated HTML": "生成的 HTML",
19
+ "Generated Markdown": "生成的 Markdown",
20
+ "Output format": "输出格式",
21
+ "Target chapters": "目标章节数",
22
+ "Chapter guidance": "章节拆分说明",
23
+ "Describe how the guide should be split into chapters": "描述应如何将指南拆分为章节",
24
+ "Build Phase": "构建阶段",
25
+ "Chapters": "章节",
26
+ "Breakdown Plan": "拆分计划",
27
+ "Contents": "目录",
28
+ "No guide content available": "暂无指南内容",
29
+ "Select Space": "选择空间",
22
30
  "Build Log": "构建日志",
23
31
  "Delete": "删除",
24
32
  "Saved successfully": "保存成功",
@@ -13,7 +13,7 @@ import crypto from 'crypto';
13
13
  import { marked } from 'marked';
14
14
 
15
15
  const MAX_SOURCE_CHARS = 90000;
16
- const MIN_CHAPTERS = 3;
16
+ const MIN_CHAPTERS = 1;
17
17
  const MAX_CHAPTERS = 12;
18
18
  const DEFAULT_TARGET_CHAPTERS = 5;
19
19
 
@@ -167,6 +167,7 @@ function createFallbackPlan(guideTitle: string, targetChapterCount: number): Gui
167
167
  }
168
168
 
169
169
  function normalizePlan(rawText: string, guideTitle: string, targetChapterCount: number): GuidePlan {
170
+ const targetCount = clampChapterCount(targetChapterCount);
170
171
  const cleanText = stripFence(rawText);
171
172
  const jsonStart = cleanText.indexOf('{');
172
173
  const jsonEnd = cleanText.lastIndexOf('}');
@@ -175,24 +176,27 @@ function normalizePlan(rawText: string, guideTitle: string, targetChapterCount:
175
176
  try {
176
177
  parsed = JSON.parse(jsonText);
177
178
  } catch {
178
- return createFallbackPlan(guideTitle, targetChapterCount);
179
+ return createFallbackPlan(guideTitle, targetCount);
179
180
  }
180
181
  const rawChapters = Array.isArray(parsed?.chapters) ? parsed.chapters : [];
181
- const chapters = rawChapters
182
- .slice(0, MAX_CHAPTERS)
183
- .map((item: any, index: number) => ({
184
- title: String(item?.title || `Section ${index + 1}`),
185
- goal: item?.goal ? String(item.goal) : '',
186
- sourceHints: Array.isArray(item?.sourceHints) ? item.sourceHints.map((hint: any) => String(hint)) : [],
187
- }))
188
- .filter((item: GuidePlanItem) => item.title);
189
-
190
- if (chapters.length < MIN_CHAPTERS) {
191
- return createFallbackPlan(parsed?.title ? String(parsed.title) : guideTitle, targetChapterCount);
192
- }
182
+ const planTitle = parsed?.title ? String(parsed.title) : guideTitle;
183
+ const fallbackPlan = createFallbackPlan(planTitle, targetCount);
184
+ const chapters = fallbackPlan.chapters.map((fallback, index) => {
185
+ const item = rawChapters[index];
186
+ if (!item) return fallback;
187
+ const title = typeof item === 'string' ? item : item?.title;
188
+ const goal = typeof item === 'object' ? item?.goal : undefined;
189
+ const sourceHints = typeof item === 'object' && Array.isArray(item?.sourceHints) ? item.sourceHints : undefined;
190
+
191
+ return {
192
+ title: String(title || fallback.title),
193
+ goal: goal ? String(goal) : fallback.goal,
194
+ sourceHints: sourceHints ? sourceHints.map((hint: any) => String(hint)) : fallback.sourceHints,
195
+ };
196
+ });
193
197
 
194
198
  return {
195
- title: parsed?.title ? String(parsed.title) : guideTitle,
199
+ title: planTitle,
196
200
  chapters,
197
201
  };
198
202
  }
@@ -229,8 +233,8 @@ Return ONLY valid JSON with this shape:
229
233
  }
230
234
 
231
235
  Rules:
232
- - Create exactly ${targetCount} chapters.
233
- - Each chapter must cover a distinct user goal. Do not collapse the guide into a single chapter.
236
+ - Create exactly ${targetCount} chapter${targetCount === 1 ? '' : 's'}.
237
+ - If more than one chapter is requested, each chapter must cover a distinct user goal.
234
238
  - Keep chapter titles user-facing and action-oriented.
235
239
  - Do not include markdown fences or explanations.
236
240