Files
markdown-converter/src/plugins/built-in/ai-assistant/prompts.js
T
amitwh c4dcbd8caf feat: v4.6.0 — AI assistant, collaboration, knowledge base, and 15 more features
- AI Assistant plugin: multi-provider chat (OpenAI/Anthropic/Ollama/LM Studio),
  summarize/improve/translate commands, proofread via ai:analyze; calls
  proxied through main so API keys stay out of the renderer
- Collaboration plugin: anchor-based comments in .comments/ sidecars with
  drift detection and F8 navigation
- Local knowledge base: [[wiki-links]] with click-to-create + Backlinks panel
- Crash recovery: debounced session snapshots with restore prompt on launch
- Version history: pre-save snapshots, History panel with restore/diff/delete
- Real PDF encryption: swap pdf-lib for @cantoo/pdf-lib (probe-driven UI)
- XLSX export (native workbooks via JSZip), ODT headers/footers + page size
- Offline KaTeX (bundled CSS+fonts), local-first PlantUML rendering
- Editor: vim mode toggle, snippet Tab-expansion, zen word-goal setter,
  writing heatmap, writing-studio panels wired with rail icons
- Quick Note global scratchpad (Ctrl+Alt+Q), markdownconverter:// deep links,
  REPL first-run confirmation
- Fix: Ctrl+Shift+P collision, pandoc converter availability check, CLI
  dangling --css/--reference-doc flags, dead converter button

8 new test suites; 613 tests green; lint clean
2026-09-05 20:48:39 +05:30

143 lines
5.2 KiB
JavaScript

/**
* Prompt builders and output parsers for the AI Assistant plugin.
*
* Kept as a pure module (no DOM, no IPC) so prompt construction and the
* proofread-issue parser can be unit-tested directly.
*
* @module AiPrompts
*/
/**
* Task prompt templates. Each entry maps an assistant action to a system
* prompt (behavior contract) and a user-prompt builder over the document
* text. Markdown output is requested for editor-facing actions so results
* can be inserted straight into the document.
*/
const TASKS = {
summarize: {
system:
'You are a concise writing assistant. Summarize the user text in clear markdown. ' +
'Use a short paragraph followed by 3-5 bullet points with the key ideas.',
user: (text, extra) => `Summarize the following${extra ? ` (${extra})` : ''}:\n\n${text}`,
},
improve: {
system:
'You are a professional editor. Improve the user text for clarity, flow, and correctness. ' +
'Return ONLY the rewritten markdown — no preamble, no explanations, no code fences ' +
'around the whole answer.',
user: (text) => `Rewrite and improve this text:\n\n${text}`,
},
explain: {
system:
'You are a patient technical explainer. Explain the user text in plain language, ' +
'using short markdown sections and examples where helpful.',
user: (text) => `Explain the following:\n\n${text}`,
},
translate: {
system:
'You are a careful translator. Translate the user text, preserving markdown formatting, ' +
'tone, and technical terminology. Return ONLY the translation.',
user: (text, targetLanguage) =>
`Translate the following to ${targetLanguage || 'English'}:\n\n${text}`,
},
chat: {
// Free-form conversation; the panel supplies its own message history
system:
'You are a helpful writing and markdown assistant inside a desktop editor. ' +
'Answer in markdown. Be concise unless asked for detail.',
user: (text) => text,
},
};
/**
* Build a {system, user} prompt pair for a known task action.
*
* @param {string} action Task id (summarize|improve|explain|translate|chat)
* @param {string} text Document text or selection
* @param {string} [extra] e.g. target language for translate
* @returns {{system: string, user: string}}
* @throws {Error} on unknown action
*/
function buildTaskPrompt(action, text, extra) {
const task = TASKS[action];
if (!task) throw new Error(`Unknown AI task "${action}"`);
return { system: task.system, user: task.user(String(text || ''), extra) };
}
/**
* System+user prompts for grammar proofreading. The writing-studio proofread
* panel expects a callback with `{issues: [{type, message, suggestion}]}`,
* so the model is asked for strict JSON.
*/
const PROOFREAD_SYSTEM =
'You are a strict proofreader. Find grammar, spelling, and punctuation issues in the text. ' +
'Respond with ONLY a JSON array — no prose, no code fences. Each element must be an object: ' +
'{"type": "grammar"|"spelling"|"punctuation"|"style", "message": string, "suggestion": string}. ' +
'The message should quote or describe the problematic fragment; the suggestion is the fix. ' +
'If there are no issues, respond with [].';
/** @returns {{system: string, user: string}} */
function buildProofreadPrompt(text) {
return {
system: PROOFREAD_SYSTEM,
user: `Proofread the following text and list its issues as the JSON array described:\n\n${text}`,
};
}
/**
* Parse a model's proofread answer into an issues array. Tolerates the usual
* LLM quirks: code fences around the JSON, leading prose, trailing commas,
* and single-quoted keys. Returns [] when nothing parseable is found rather
* than throwing — a chatty model must not break the panel.
*
* @param {string} modelOutput Raw assistant text
* @returns {Array<{type: string, message: string, suggestion: string}>}
*/
function parseProofreadIssues(modelOutput) {
const raw = String(modelOutput || '').trim();
if (!raw) return [];
// Strip markdown code fences the model may have added despite instructions
const unfenced = raw
.replace(/^```(?:json)?\s*/i, '')
.replace(/\s*```$/i, '')
.trim();
// Grab the outermost [...] block; ignores any leading/trailing prose
const start = unfenced.indexOf('[');
const end = unfenced.lastIndexOf(']');
if (start === -1 || end === -1 || end <= start) return [];
let jsonSlice = unfenced.slice(start, end + 1);
let parsed;
try {
parsed = JSON.parse(jsonSlice);
} catch {
try {
// Retry after trimming trailing commas (a common LLM artifact)
jsonSlice = jsonSlice.replace(/,\s*([\]}])/g, '$1');
parsed = JSON.parse(jsonSlice);
} catch {
return [];
}
}
if (!Array.isArray(parsed)) return [];
// Normalize/whitelist fields so the panel always gets a stable shape
return parsed
.filter((item) => item && typeof item === 'object' && (item.message || item.suggestion))
.slice(0, 100)
.map((item) => ({
type: typeof item.type === 'string' ? item.type.toLowerCase() : 'grammar',
message: String(item.message || item.suggestion || ''),
suggestion: item.suggestion === undefined ? '' : String(item.suggestion),
}));
}
module.exports = {
buildTaskPrompt,
buildProofreadPrompt,
parseProofreadIssues,
TASKS,
};