@aigne/doc-smith 0.9.7 → 0.9.8-beta
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/CHANGELOG.md +13 -0
- package/agents/create/analyze-diagram-type-llm.yaml +160 -0
- package/agents/create/analyze-diagram-type.mjs +297 -0
- package/agents/create/generate-diagram-image.yaml +60 -0
- package/agents/create/replace-d2-with-image.mjs +624 -0
- package/agents/create/utils/init-current-content.mjs +5 -9
- package/agents/evaluate/document.yaml +6 -0
- package/agents/evaluate/index.yaml +1 -0
- package/agents/init/index.mjs +16 -0
- package/agents/media/batch-generate-media-description.yaml +2 -0
- package/agents/media/generate-media-description.yaml +3 -0
- package/agents/media/load-media-description.mjs +44 -15
- package/agents/publish/publish-docs.mjs +1 -4
- package/agents/update/check-diagram-flag.mjs +116 -0
- package/agents/update/check-document.mjs +0 -1
- package/agents/update/check-generate-diagram.mjs +48 -30
- package/agents/update/check-sync-image-flag.mjs +55 -0
- package/agents/update/check-update-is-single.mjs +11 -0
- package/agents/update/generate-diagram.yaml +43 -9
- package/agents/update/generate-document.yaml +9 -0
- package/agents/update/handle-document-update.yaml +10 -8
- package/agents/update/index.yaml +16 -1
- package/agents/update/sync-images-and-exit.mjs +148 -0
- package/agents/update/update-single/update-single-document-detail.mjs +131 -17
- package/agents/utils/analyze-feedback-intent.mjs +136 -0
- package/agents/utils/choose-docs.mjs +183 -40
- package/agents/utils/generate-document-or-skip.mjs +41 -0
- package/agents/utils/handle-diagram-operations.mjs +263 -0
- package/agents/utils/load-all-document-content.mjs +30 -0
- package/agents/utils/load-sources.mjs +2 -2
- package/agents/utils/read-current-document-content.mjs +46 -0
- package/agents/utils/save-doc.mjs +42 -0
- package/agents/utils/skip-if-content-exists.mjs +27 -0
- package/aigne.yaml +6 -1
- package/assets/report-template/report.html +17 -17
- package/docs-mcp/read-doc-content.mjs +30 -1
- package/package.json +4 -4
- package/prompts/detail/diagram/generate-image-system.md +135 -0
- package/prompts/detail/diagram/generate-image-user.md +32 -0
- package/prompts/detail/generate/user-prompt.md +27 -13
- package/prompts/evaluate/document.md +23 -10
- package/prompts/media/media-description/system-prompt.md +10 -2
- package/prompts/media/media-description/user-prompt.md +9 -0
- package/utils/check-document-has-diagram.mjs +97 -0
- package/utils/constants/index.mjs +46 -0
- package/utils/d2-utils.mjs +114 -181
- package/utils/delete-diagram-images.mjs +103 -0
- package/utils/docs-finder-utils.mjs +34 -1
- package/utils/image-compress.mjs +75 -0
- package/utils/kroki-utils.mjs +2 -3
- package/utils/sync-diagram-to-translations.mjs +258 -0
- package/utils/utils.mjs +24 -0
- package/agents/create/check-diagram.mjs +0 -40
- package/agents/create/draw-diagram.yaml +0 -27
- package/agents/create/merge-diagram.yaml +0 -39
- package/agents/create/wrap-diagram-code.mjs +0 -35
|
@@ -5,7 +5,15 @@ import {
|
|
|
5
5
|
getActionText,
|
|
6
6
|
getMainLanguageFiles,
|
|
7
7
|
processSelectedFiles,
|
|
8
|
+
readFileContent,
|
|
8
9
|
} from "../../utils/docs-finder-utils.mjs";
|
|
10
|
+
import {
|
|
11
|
+
hasDiagramContent,
|
|
12
|
+
hasBananaImages,
|
|
13
|
+
getDiagramTypeLabels,
|
|
14
|
+
formatDiagramTypeSuffix,
|
|
15
|
+
} from "../../utils/check-document-has-diagram.mjs";
|
|
16
|
+
import { debug } from "../../utils/debug.mjs";
|
|
9
17
|
import { DOC_ACTION } from "../../utils/constants/index.mjs";
|
|
10
18
|
|
|
11
19
|
function getFeedbackMessage(action) {
|
|
@@ -27,6 +35,9 @@ export default async function chooseDocs(
|
|
|
27
35
|
reset = false,
|
|
28
36
|
requiredFeedback = true,
|
|
29
37
|
action,
|
|
38
|
+
shouldUpdateDiagrams = false,
|
|
39
|
+
shouldAutoSelectDiagrams = false,
|
|
40
|
+
shouldSyncImages = false,
|
|
30
41
|
},
|
|
31
42
|
options,
|
|
32
43
|
) {
|
|
@@ -48,55 +59,181 @@ export default async function chooseDocs(
|
|
|
48
59
|
);
|
|
49
60
|
}
|
|
50
61
|
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const flatName = file.replace(/\.md$/, "").replace(/\.\w+(-\w+)?$/, "");
|
|
55
|
-
const docItem = documentStructure.find((item) => {
|
|
56
|
-
const itemFlattenedPath = item.path.replace(/^\//, "").replace(/\//g, "-");
|
|
57
|
-
return itemFlattenedPath === flatName;
|
|
58
|
-
});
|
|
62
|
+
// If --diagram-sync flag is set, filter documents by banana images only
|
|
63
|
+
if (shouldSyncImages) {
|
|
64
|
+
debug("🔄 Filtering documents with banana images...");
|
|
59
65
|
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
// Read content for all files and filter by banana images only
|
|
67
|
+
const filesWithImages = [];
|
|
68
|
+
for (const fileName of mainLanguageFiles) {
|
|
69
|
+
const content = await readFileContent(docsDir, fileName);
|
|
70
|
+
if (content && hasBananaImages(content)) {
|
|
71
|
+
filesWithImages.push(fileName);
|
|
72
|
+
}
|
|
66
73
|
}
|
|
67
74
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
75
|
+
if (filesWithImages.length === 0) {
|
|
76
|
+
debug("ℹ️ No documents found with banana images (DIAGRAM_IMAGE_START markers).");
|
|
77
|
+
return {
|
|
78
|
+
selectedDocs: [],
|
|
79
|
+
feedback: "",
|
|
80
|
+
selectedPaths: [],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
debug(`✅ Found ${filesWithImages.length} document(s) with banana images.`);
|
|
85
|
+
debug("📋 Auto-selecting all documents with banana images...");
|
|
86
|
+
// Show diagram types for each document
|
|
87
|
+
for (const file of filesWithImages) {
|
|
88
|
+
const content = await readFileContent(docsDir, file);
|
|
89
|
+
const diagramLabels = content ? getDiagramTypeLabels(content) : [];
|
|
90
|
+
const diagramSuffix = formatDiagramTypeSuffix(diagramLabels);
|
|
91
|
+
debug(` • ${file}${diagramSuffix}`);
|
|
92
|
+
}
|
|
93
|
+
selectedFiles = filesWithImages;
|
|
94
|
+
}
|
|
95
|
+
// If --diagram flag is set, filter documents by diagram content
|
|
96
|
+
else if (shouldUpdateDiagrams) {
|
|
97
|
+
debug("🔄 Filtering documents with diagram content...");
|
|
73
98
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
return choices.filter((choice) => choice.name.toLowerCase().includes(term.toLowerCase()));
|
|
81
|
-
},
|
|
82
|
-
validate: (answer) => {
|
|
83
|
-
if (answer.length === 0) {
|
|
84
|
-
return "Please select at least one document";
|
|
99
|
+
// Read content for all files and filter by diagram content
|
|
100
|
+
const filesWithDiagrams = [];
|
|
101
|
+
for (const fileName of mainLanguageFiles) {
|
|
102
|
+
const content = await readFileContent(docsDir, fileName);
|
|
103
|
+
if (content && hasDiagramContent(content)) {
|
|
104
|
+
filesWithDiagrams.push(fileName);
|
|
85
105
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (filesWithDiagrams.length === 0) {
|
|
109
|
+
debug(
|
|
110
|
+
"ℹ️ No documents found with diagram content (d2 code blocks, placeholders, or diagram images).",
|
|
111
|
+
);
|
|
112
|
+
return {
|
|
113
|
+
selectedDocs: [],
|
|
114
|
+
feedback: "",
|
|
115
|
+
selectedPaths: [],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
debug(`✅ Found ${filesWithDiagrams.length} document(s) with diagram content.`);
|
|
120
|
+
|
|
121
|
+
// If --diagram-all, auto-select all; otherwise let user choose
|
|
122
|
+
if (shouldAutoSelectDiagrams) {
|
|
123
|
+
debug("📋 Auto-selecting all documents with diagrams...");
|
|
124
|
+
// Show diagram types for each document in auto-select mode
|
|
125
|
+
for (const file of filesWithDiagrams) {
|
|
126
|
+
const content = await readFileContent(docsDir, file);
|
|
127
|
+
const diagramLabels = content ? getDiagramTypeLabels(content) : [];
|
|
128
|
+
const diagramSuffix = formatDiagramTypeSuffix(diagramLabels);
|
|
129
|
+
debug(` • ${file}${diagramSuffix}`);
|
|
130
|
+
}
|
|
131
|
+
selectedFiles = filesWithDiagrams;
|
|
132
|
+
} else {
|
|
133
|
+
// --diagram mode: show only documents with diagrams, let user select
|
|
134
|
+
const choices = await Promise.all(
|
|
135
|
+
filesWithDiagrams.map(async (file) => {
|
|
136
|
+
// Convert filename to flat path to find corresponding documentation structure item
|
|
137
|
+
const flatName = file.replace(/\.md$/, "").replace(/\.\w+(-\w+)?$/, "");
|
|
138
|
+
const docItem = documentStructure.find((item) => {
|
|
139
|
+
const itemFlattenedPath = item.path.replace(/^\//, "").replace(/\//g, "-");
|
|
140
|
+
return itemFlattenedPath === flatName;
|
|
141
|
+
});
|
|
89
142
|
|
|
90
|
-
|
|
91
|
-
|
|
143
|
+
// Read content to detect diagram types
|
|
144
|
+
const content = await readFileContent(docsDir, file);
|
|
145
|
+
const diagramLabels = content ? getDiagramTypeLabels(content) : [];
|
|
146
|
+
const diagramSuffix = formatDiagramTypeSuffix(diagramLabels);
|
|
147
|
+
|
|
148
|
+
// Use title if available, otherwise fall back to filename
|
|
149
|
+
let displayName = docItem?.title;
|
|
150
|
+
if (displayName) {
|
|
151
|
+
displayName = `${displayName} (${file})${diagramSuffix}`;
|
|
152
|
+
} else {
|
|
153
|
+
displayName = `${file}${diagramSuffix}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
name: displayName,
|
|
158
|
+
value: file,
|
|
159
|
+
};
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Let user select multiple files from filtered list
|
|
164
|
+
selectedFiles = await options.prompts.checkbox({
|
|
165
|
+
message: getActionText("Select documents with diagrams to {action}:", docAction),
|
|
166
|
+
source: (term) => {
|
|
167
|
+
if (!term) return choices;
|
|
168
|
+
|
|
169
|
+
return choices.filter((choice) =>
|
|
170
|
+
choice.name.toLowerCase().includes(term.toLowerCase()),
|
|
171
|
+
);
|
|
172
|
+
},
|
|
173
|
+
validate: (answer) => {
|
|
174
|
+
if (answer.length === 0) {
|
|
175
|
+
return "Please select at least one document";
|
|
176
|
+
}
|
|
177
|
+
return true;
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (!selectedFiles || selectedFiles.length === 0) {
|
|
182
|
+
throw new Error("No documents selected");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
// Normal flow: let user select documents from all files
|
|
187
|
+
// Convert files to choices with titles
|
|
188
|
+
const choices = mainLanguageFiles.map((file) => {
|
|
189
|
+
// Convert filename to flat path to find corresponding documentation structure item
|
|
190
|
+
const flatName = file.replace(/\.md$/, "").replace(/\.\w+(-\w+)?$/, "");
|
|
191
|
+
const docItem = documentStructure.find((item) => {
|
|
192
|
+
const itemFlattenedPath = item.path.replace(/^\//, "").replace(/\//g, "-");
|
|
193
|
+
return itemFlattenedPath === flatName;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Use title if available, otherwise fall back to filename
|
|
197
|
+
let displayName = docItem?.title;
|
|
198
|
+
if (displayName) {
|
|
199
|
+
displayName = `${displayName} (${file})`;
|
|
200
|
+
} else {
|
|
201
|
+
displayName = file;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
name: displayName,
|
|
206
|
+
value: file,
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Let user select multiple files
|
|
211
|
+
selectedFiles = await options.prompts.checkbox({
|
|
212
|
+
message: getActionText("Select documents to {action}:", docAction),
|
|
213
|
+
source: (term) => {
|
|
214
|
+
if (!term) return choices;
|
|
215
|
+
|
|
216
|
+
return choices.filter((choice) =>
|
|
217
|
+
choice.name.toLowerCase().includes(term.toLowerCase()),
|
|
218
|
+
);
|
|
219
|
+
},
|
|
220
|
+
validate: (answer) => {
|
|
221
|
+
if (answer.length === 0) {
|
|
222
|
+
return "Please select at least one document";
|
|
223
|
+
}
|
|
224
|
+
return true;
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (!selectedFiles || selectedFiles.length === 0) {
|
|
229
|
+
throw new Error("No documents selected");
|
|
230
|
+
}
|
|
92
231
|
}
|
|
93
232
|
|
|
94
233
|
// Process selected files and convert to found items
|
|
95
234
|
foundItems = await processSelectedFiles(selectedFiles, documentStructure, docsDir);
|
|
96
235
|
} catch (error) {
|
|
97
|
-
|
|
98
|
-
getActionText(`\nFailed to select documents to {action}: ${error.message}`, docAction),
|
|
99
|
-
);
|
|
236
|
+
debug(getActionText(`\nFailed to select documents to {action}: ${error.message}`, docAction));
|
|
100
237
|
process.exit(0);
|
|
101
238
|
}
|
|
102
239
|
} else {
|
|
@@ -105,7 +242,7 @@ export default async function chooseDocs(
|
|
|
105
242
|
const foundItem = await findItemByPath(documentStructure, docPath, boardId, docsDir, locale);
|
|
106
243
|
|
|
107
244
|
if (!foundItem) {
|
|
108
|
-
|
|
245
|
+
debug(`⚠️ Item with path "${docPath}" not found in documentStructure`);
|
|
109
246
|
continue;
|
|
110
247
|
}
|
|
111
248
|
|
|
@@ -120,8 +257,14 @@ export default async function chooseDocs(
|
|
|
120
257
|
}
|
|
121
258
|
|
|
122
259
|
// Prompt for feedback if not provided
|
|
260
|
+
// Skip feedback prompt if --diagram, --diagram-all, or --diagram-sync flag is set
|
|
123
261
|
let userFeedback = feedback;
|
|
124
|
-
if (
|
|
262
|
+
if (
|
|
263
|
+
!userFeedback &&
|
|
264
|
+
(requiredFeedback || foundItems?.length > 1) &&
|
|
265
|
+
!shouldUpdateDiagrams &&
|
|
266
|
+
!shouldSyncImages
|
|
267
|
+
) {
|
|
125
268
|
const feedbackMessage = getFeedbackMessage(docAction);
|
|
126
269
|
|
|
127
270
|
userFeedback = await options.prompts.input({
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conditionally call generateDocument. If we already have content for diagram-only intents,
|
|
3
|
+
* skip LLM generation and pass through the existing content.
|
|
4
|
+
*/
|
|
5
|
+
export default async function generateDocumentOrSkip(input, options) {
|
|
6
|
+
const { intentType, content, skipGenerateDocument } = input;
|
|
7
|
+
|
|
8
|
+
const isDiagramIntent =
|
|
9
|
+
intentType && ["addDiagram", "updateDiagram", "deleteDiagram"].includes(intentType);
|
|
10
|
+
const shouldSkip = Boolean(skipGenerateDocument || (isDiagramIntent && content));
|
|
11
|
+
|
|
12
|
+
if (shouldSkip) {
|
|
13
|
+
// Return the existing content and mark the generation as skipped
|
|
14
|
+
return {
|
|
15
|
+
...input,
|
|
16
|
+
content,
|
|
17
|
+
documentContent: content,
|
|
18
|
+
originalContent: content,
|
|
19
|
+
reviewContent: content,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const generateAgent = options.context?.agents?.["generateDocument"];
|
|
24
|
+
if (!generateAgent) {
|
|
25
|
+
throw new Error("generateDocument agent not found");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const result = await options.context.invoke(generateAgent, input);
|
|
29
|
+
const generatedContent = result?.content ?? result;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
...input,
|
|
33
|
+
...result,
|
|
34
|
+
content: generatedContent,
|
|
35
|
+
documentContent: generatedContent,
|
|
36
|
+
originalContent: generatedContent,
|
|
37
|
+
reviewContent: generatedContent,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
generateDocumentOrSkip.task_render_mode = "hide";
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { AIAgent } from "@aigne/core";
|
|
2
|
+
import { pick } from "@aigne/core/utils/type-utils.js";
|
|
3
|
+
import z from "zod";
|
|
4
|
+
import {
|
|
5
|
+
DIAGRAM_PLACEHOLDER,
|
|
6
|
+
replaceD2WithPlaceholder,
|
|
7
|
+
replaceDiagramsWithPlaceholder,
|
|
8
|
+
} from "../../utils/d2-utils.mjs";
|
|
9
|
+
import { readFileContent } from "../../utils/docs-finder-utils.mjs";
|
|
10
|
+
import { getFileName, userContextAt } from "../../utils/utils.mjs";
|
|
11
|
+
import { debug } from "../../utils/debug.mjs";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Read current document content from file system
|
|
15
|
+
* Reuses the same logic as initCurrentContent but returns the content
|
|
16
|
+
* First checks userContext, then reads from file if not in context
|
|
17
|
+
*/
|
|
18
|
+
async function readCurrentContent(input, options) {
|
|
19
|
+
const { path, docsDir, locale = "en" } = input;
|
|
20
|
+
|
|
21
|
+
if (!path || !docsDir) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// First check if content is already in userContext
|
|
26
|
+
const contentContext = userContextAt(options, `currentContents.${path}`);
|
|
27
|
+
const existingContent = contentContext.get();
|
|
28
|
+
if (existingContent) {
|
|
29
|
+
return existingContent;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// If not in context, read from file (same logic as initCurrentContent)
|
|
33
|
+
try {
|
|
34
|
+
const fileName = getFileName(path, locale);
|
|
35
|
+
const content = await readFileContent(docsDir, fileName);
|
|
36
|
+
|
|
37
|
+
if (!content) {
|
|
38
|
+
console.warn(`⚠️ Could not read content from ${fileName}`);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Set in userContext for future use
|
|
43
|
+
contentContext.set(content);
|
|
44
|
+
|
|
45
|
+
return content;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.warn(`Failed to read current content for ${path}: ${error.message}`);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Save document content
|
|
54
|
+
*/
|
|
55
|
+
async function saveDoc(input, options, { content, intentType }) {
|
|
56
|
+
const saveAgent = options.context?.agents?.["saveDoc"];
|
|
57
|
+
if (!saveAgent) {
|
|
58
|
+
console.warn("saveDoc agent not found");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
await options.context.invoke(saveAgent, {
|
|
62
|
+
...pick(input, ["path", "docsDir", "labels", "locale"]),
|
|
63
|
+
content,
|
|
64
|
+
intentType, // Pass intentType so saveDoc can handle translation sync
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Handle addDiagram intent
|
|
70
|
+
*/
|
|
71
|
+
async function addDiagram(input, options) {
|
|
72
|
+
// readCurrentContent will check userContext first, then read from file if needed
|
|
73
|
+
const currentContent = await readCurrentContent(input, options);
|
|
74
|
+
if (!currentContent) {
|
|
75
|
+
throw new Error(`Could not read current content for ${input.path}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const contentContext = userContextAt(options, `currentContents.${input.path}`);
|
|
79
|
+
|
|
80
|
+
const generateDiagramAgent = options.context.agents["checkGenerateDiagram"];
|
|
81
|
+
if (!generateDiagramAgent) {
|
|
82
|
+
throw new Error("checkGenerateDiagram agent not found");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const generateDiagramResult = await options.context.invoke(generateDiagramAgent, {
|
|
86
|
+
...pick(input, ["locale", "diagramming", "feedback", "path", "docsDir"]),
|
|
87
|
+
documentContent: currentContent,
|
|
88
|
+
originalContent: currentContent,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const content = generateDiagramResult.content;
|
|
92
|
+
contentContext.set(content);
|
|
93
|
+
// Pass intentType to saveDoc so it can handle translation sync automatically
|
|
94
|
+
await saveDoc(input, options, { content, intentType: "addDiagram" });
|
|
95
|
+
return { content };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Handle updateDiagram intent
|
|
100
|
+
*/
|
|
101
|
+
async function updateDiagram(input, options) {
|
|
102
|
+
// readCurrentContent will check userContext first, then read from file if needed
|
|
103
|
+
const currentContent = await readCurrentContent(input, options);
|
|
104
|
+
if (!currentContent) {
|
|
105
|
+
throw new Error(`Could not read current content for ${input.path}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const contentContext = userContextAt(options, `currentContents.${input.path}`);
|
|
109
|
+
|
|
110
|
+
let [content] = replaceD2WithPlaceholder({
|
|
111
|
+
content: currentContent,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const generateAgent = options.context?.agents?.["generateDiagram"];
|
|
115
|
+
if (!generateAgent) {
|
|
116
|
+
throw new Error("generateDiagram agent not found");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = await options.context.invoke(generateAgent, {
|
|
120
|
+
documentContent: content,
|
|
121
|
+
locale: input.locale,
|
|
122
|
+
diagramming: input.diagramming || {},
|
|
123
|
+
feedback: input.feedback,
|
|
124
|
+
originalContent: currentContent, // Pass original content to find existing diagrams
|
|
125
|
+
path: input.path,
|
|
126
|
+
docsDir: input.docsDir,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// generateDiagram now returns { content } with image already inserted
|
|
130
|
+
// The image replaces DIAGRAM_PLACEHOLDER or D2 code blocks
|
|
131
|
+
if (result?.content) {
|
|
132
|
+
content = result.content;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
contentContext.set(content);
|
|
136
|
+
// Pass intentType to saveDoc so it can handle translation sync automatically
|
|
137
|
+
await saveDoc(input, options, { content, intentType: "updateDiagram" });
|
|
138
|
+
return { content };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Handle deleteDiagram intent
|
|
143
|
+
*/
|
|
144
|
+
async function deleteDiagram(input, options) {
|
|
145
|
+
// readCurrentContent will check userContext first, then read from file if needed
|
|
146
|
+
const currentContent = await readCurrentContent(input, options);
|
|
147
|
+
if (!currentContent) {
|
|
148
|
+
throw new Error(`Could not read current content for ${input.path}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const contentContext = userContextAt(options, `currentContents.${input.path}`);
|
|
152
|
+
|
|
153
|
+
// Extract diagram index from feedback if provided
|
|
154
|
+
// This allows deleting a specific diagram when multiple diagrams exist
|
|
155
|
+
let diagramIndex = input.diagramIndex;
|
|
156
|
+
if (diagramIndex === undefined && input.feedback) {
|
|
157
|
+
// Import extractDiagramIndexFromFeedback from replace-d2-with-image.mjs
|
|
158
|
+
const { extractDiagramIndexFromFeedback } = await import("../create/replace-d2-with-image.mjs");
|
|
159
|
+
const extractedIndex = extractDiagramIndexFromFeedback(input.feedback);
|
|
160
|
+
if (extractedIndex !== null) {
|
|
161
|
+
diagramIndex = extractedIndex;
|
|
162
|
+
debug(`Extracted diagram index ${diagramIndex} from feedback: "${input.feedback}"`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Replace all diagrams (D2 code blocks, generated images, Mermaid) with placeholder
|
|
167
|
+
// If diagramIndex is provided, only replace that specific diagram
|
|
168
|
+
// This ensures LLM can identify and remove the diagram regardless of its type
|
|
169
|
+
const documentContent = replaceDiagramsWithPlaceholder({
|
|
170
|
+
content: currentContent,
|
|
171
|
+
diagramIndex,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const instructions = `<role>
|
|
175
|
+
Your task is to remove ${DIAGRAM_PLACEHOLDER} and adjust the document context (based on the user's feedback) to make it easier to understand.
|
|
176
|
+
</role>
|
|
177
|
+
|
|
178
|
+
<document_content>
|
|
179
|
+
{{documentContent}}
|
|
180
|
+
</document_content>
|
|
181
|
+
|
|
182
|
+
<user_feedback>
|
|
183
|
+
{{feedback}}
|
|
184
|
+
</user_feedback>
|
|
185
|
+
|
|
186
|
+
<output_constraints>
|
|
187
|
+
- Do not provide any explanations; include only the document content itself
|
|
188
|
+
</output_constraints>`;
|
|
189
|
+
|
|
190
|
+
const deleteAgent = AIAgent.from({
|
|
191
|
+
name: "deleteDiagram",
|
|
192
|
+
description: "Remove a diagram from the document content",
|
|
193
|
+
task_render_mode: "hide",
|
|
194
|
+
instructions,
|
|
195
|
+
inputSchema: z.object({
|
|
196
|
+
documentContent: z.string().describe("Source content of the document"),
|
|
197
|
+
feedback: z.string().describe("User feedback for content modifications"),
|
|
198
|
+
}),
|
|
199
|
+
outputKey: "message",
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const { message: content } = await options.context.invoke(deleteAgent, {
|
|
203
|
+
documentContent,
|
|
204
|
+
feedback: input.feedback,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Delete associated diagram image files
|
|
208
|
+
if (input.docsDir) {
|
|
209
|
+
try {
|
|
210
|
+
const { deleteDiagramImages } = await import("../../utils/delete-diagram-images.mjs");
|
|
211
|
+
const { deleted } = await deleteDiagramImages(currentContent, input.path, input.docsDir);
|
|
212
|
+
if (deleted > 0) {
|
|
213
|
+
debug(`Deleted ${deleted} diagram image file(s) for ${input.path}`);
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
// Don't fail the operation if image deletion fails
|
|
217
|
+
console.warn(`Failed to delete diagram images: ${error.message}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
contentContext.set(content);
|
|
222
|
+
// Pass intentType to saveDoc so it can handle translation sync automatically
|
|
223
|
+
await saveDoc(input, options, { content, intentType: "deleteDiagram" });
|
|
224
|
+
|
|
225
|
+
return { content };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Handle diagram operations based on intent type
|
|
230
|
+
* Supports: addDiagram, updateDiagram, deleteDiagram
|
|
231
|
+
*/
|
|
232
|
+
export default async function handleDiagramOperations(
|
|
233
|
+
{ intentType, path, docsDir, locale, feedback, diagramming, labels },
|
|
234
|
+
options,
|
|
235
|
+
) {
|
|
236
|
+
if (!intentType || !["addDiagram", "updateDiagram", "deleteDiagram"].includes(intentType)) {
|
|
237
|
+
throw new Error(`Invalid intent type for diagram operations: ${intentType}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const input = {
|
|
241
|
+
path,
|
|
242
|
+
docsDir,
|
|
243
|
+
locale,
|
|
244
|
+
feedback,
|
|
245
|
+
diagramming,
|
|
246
|
+
labels,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const fnMap = {
|
|
250
|
+
addDiagram,
|
|
251
|
+
updateDiagram,
|
|
252
|
+
deleteDiagram,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
if (fnMap[intentType]) {
|
|
256
|
+
const result = await fnMap[intentType](input, options);
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
throw new Error(`Unsupported intent type: ${intentType}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
handleDiagramOperations.task_render_mode = "hide";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import pMap from "p-map";
|
|
2
|
+
import { findItemByPath } from "../../utils/docs-finder-utils.mjs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Loads all document content from the file system based on the provided document structure.
|
|
6
|
+
*
|
|
7
|
+
* @async
|
|
8
|
+
* @function loadAllDocumentContent
|
|
9
|
+
* @param {Object} params - The parameters object
|
|
10
|
+
* @param {string} params.docsDir - The root directory path where documents are located
|
|
11
|
+
* @param {Array<Object>} params.documentStructure - The document structure array containing items with path information
|
|
12
|
+
* @param {string} params.documentStructure[].path - The file path of each document item
|
|
13
|
+
* @returns {Promise<Array>} returns.allDocumentContentList - An array of document content items loaded from the file system
|
|
14
|
+
* @example
|
|
15
|
+
* const result = await loadAllDocumentContent({
|
|
16
|
+
* docsDir: './docs',
|
|
17
|
+
* documentStructure: [{ path: 'readme.md' }, { path: 'guide.md' }]
|
|
18
|
+
* });
|
|
19
|
+
*/
|
|
20
|
+
export default async function loadAllDocumentContent({ docsDir, documentStructure = [] }) {
|
|
21
|
+
const allDocumentContentList = await pMap(documentStructure, async (item) => {
|
|
22
|
+
const itemResult = await findItemByPath(documentStructure, item.path, null, docsDir);
|
|
23
|
+
return itemResult;
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
allDocumentContentList,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
loadAllDocumentContent.taskRenderMode = "hide";
|
|
@@ -157,8 +157,8 @@ export default async function loadSources(
|
|
|
157
157
|
mediaItem.width = dimensions.width;
|
|
158
158
|
mediaItem.height = dimensions.height;
|
|
159
159
|
|
|
160
|
-
// Filter out images with width less than minImageWidth
|
|
161
|
-
if (dimensions.width < minImageWidth) {
|
|
160
|
+
// Filter out images with width less than minImageWidth, but not SVG images
|
|
161
|
+
if (dimensions.width < minImageWidth && mediaItem.mimeType !== "image/svg+xml") {
|
|
162
162
|
filteredImageCount++;
|
|
163
163
|
console.log(
|
|
164
164
|
`Ignored image: ${fileName} (${dimensions.width}x${dimensions.height}px < ${minImageWidth}px minimum)`,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFileContent } from "../../utils/docs-finder-utils.mjs";
|
|
2
|
+
import { getFileName } from "../../utils/utils.mjs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Read current document content from file system
|
|
6
|
+
* Used when skipping document generation (e.g., for diagram-only updates)
|
|
7
|
+
* Only reads content if intentType is diagram-related, otherwise returns input unchanged
|
|
8
|
+
*/
|
|
9
|
+
export default async function readCurrentDocumentContent(input) {
|
|
10
|
+
const { path, docsDir, locale = "en", intentType } = input;
|
|
11
|
+
|
|
12
|
+
// Only read content if intentType is diagram-related
|
|
13
|
+
// Otherwise, return input unchanged to allow document generation to proceed
|
|
14
|
+
if (!intentType || !["addDiagram", "updateDiagram", "deleteDiagram"].includes(intentType)) {
|
|
15
|
+
return input;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!path || !docsDir) {
|
|
19
|
+
return input;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
// Read document content using the same utility as other parts of the system
|
|
24
|
+
const fileName = getFileName(path, locale);
|
|
25
|
+
const content = await readFileContent(docsDir, fileName);
|
|
26
|
+
|
|
27
|
+
if (!content) {
|
|
28
|
+
console.warn(`⚠️ Could not read content from ${fileName}`);
|
|
29
|
+
return input;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Return content as both content, documentContent, and originalContent
|
|
33
|
+
// This matches the structure expected by downstream agents
|
|
34
|
+
return {
|
|
35
|
+
...input,
|
|
36
|
+
content,
|
|
37
|
+
documentContent: content,
|
|
38
|
+
originalContent: content,
|
|
39
|
+
};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.warn(`Failed to read current content for ${path}: ${error.message}`);
|
|
42
|
+
return input;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
readCurrentDocumentContent.task_render_mode = "hide";
|