ai smart auto-documentation and text rewrite
- Store captureMetadata (OCR text, window/app/element info) with each step at capture time so AI always has the original rich context. - Add buildCaptureContext() to TextIntelService; capture.js uses it instead of buildCaptureTitle() so both title and metadata come from one pass. - generateStepPatch() prefers stored captureMetadata over re-running OCR, giving the AI the best possible context when the user clicks an AI button later. - Add autoDoc setting: when enabled every capture (shoot, region, and session hotkey/click) is automatically documented by AI. Manual captures await AI before returning; session captures fire-and-forget and push a step:updated event so the renderer reloads seamlessly. - Add ai:rewriteText IPC and rewriteText() method for plain-text polishing via a separate callOllamaText() that skips JSON mode. - Add "AI Rewrite" section in the editor right panel: textarea + AI button that rewrites whatever the user types in place. - Improve buildAiPrompt() rules: action-focused title instructions, explicit anti-junk rules (no "Capture the screen / OCR" blocks), and a context-quality gate that suppresses blocks when context is thin. - Add autoDoc checkbox to AI settings dialog. - Renderer handles step:updated to reload the selected step after background auto-doc finishes. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
@@ -80,6 +80,18 @@ class StepForgeApp {
|
||||
|
||||
api.capture.onAdded((payload) => this.onCaptureAdded(payload));
|
||||
api.capture.onState((payload) => this.updateCaptureState(payload));
|
||||
api.capture.onStepUpdated((payload) => this.onStepUpdated(payload));
|
||||
}
|
||||
|
||||
async onStepUpdated(payload) {
|
||||
if (!payload || !payload.guideId || !payload.step) return;
|
||||
if (this.state.view === 'editor' && this.editor.guideId === payload.guideId) {
|
||||
const currentStep = this.editor.currentStep;
|
||||
if (currentStep && currentStep.stepId === payload.step.stepId) {
|
||||
await this.editor.reload(payload.step.stepId);
|
||||
toast('Documentation generated.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async onCaptureAdded(payload) {
|
||||
|
||||
@@ -315,6 +315,7 @@ function showSettingsDialog({
|
||||
const confirmSimple = el('input', { type: 'checkbox', checked: Boolean(settings.capture?.confirmSimpleCapture) });
|
||||
const keepLast = makeInput(settings.backups?.keepLast ?? 10, 'number', { min: 0, step: 1 });
|
||||
const aiEnabled = el('input', { type: 'checkbox', checked: Boolean(settings.ai?.enabled) });
|
||||
const aiAutoDoc = el('input', { type: 'checkbox', checked: Boolean(settings.ai?.autoDoc) });
|
||||
const ollamaHost = makeInput(settings.ai?.ollama?.host || 'http://127.0.0.1:11434');
|
||||
const ollamaModel = makeInput(settings.ai?.ollama?.model || 'llama3.2:1b');
|
||||
const aiStatus = el('div', { className: 'muted ai-status' }, 'AI stays local through Ollama. Nothing is sent to the cloud.');
|
||||
@@ -408,7 +409,8 @@ function showSettingsDialog({
|
||||
),
|
||||
el('fieldset', {},
|
||||
el('legend', {}, 'AI'),
|
||||
labeledRow('Enable AI text filling', aiEnabled),
|
||||
labeledRow('Enable AI', aiEnabled),
|
||||
labeledRow('Auto-document captures', aiAutoDoc),
|
||||
labeledRow('Ollama host', ollamaHost),
|
||||
labeledRow('Ollama model', ollamaModel),
|
||||
el('div.row', { style: { justifyContent: 'space-between' } },
|
||||
@@ -416,7 +418,7 @@ function showSettingsDialog({
|
||||
testAiBtn,
|
||||
),
|
||||
el('div.muted', {},
|
||||
'AI generation is manual only. Captures stay deterministic until you click a generate button.',
|
||||
'When auto-document is on, each capture is automatically documented by AI. Turn it off to use AI manually only.',
|
||||
),
|
||||
),
|
||||
el('fieldset', {},
|
||||
@@ -465,6 +467,7 @@ function showSettingsDialog({
|
||||
ai: {
|
||||
...settings.ai,
|
||||
enabled: aiEnabled.checked,
|
||||
autoDoc: aiAutoDoc.checked,
|
||||
ollama: {
|
||||
...(settings.ai?.ollama || {}),
|
||||
host: ollamaHost.value.trim(),
|
||||
|
||||
@@ -167,6 +167,7 @@ class GuideEditor {
|
||||
const buttons = [
|
||||
this.dom?.titleAiBtn,
|
||||
this.dom?.descAiBtn,
|
||||
this.dom?.rewriteAiBtn,
|
||||
...(this.dom?.blocksList ? [...this.dom.blocksList.querySelectorAll('button[data-ai-action]')] : []),
|
||||
].filter(Boolean);
|
||||
for (const button of buttons) {
|
||||
@@ -222,6 +223,36 @@ class GuideEditor {
|
||||
return this.runAiGeneration('all', { button });
|
||||
}
|
||||
|
||||
async runTextRewrite(button = null) {
|
||||
if (!this.isAiEnabled()) {
|
||||
this.onToast('Enable AI in Settings first.', { error: true });
|
||||
return;
|
||||
}
|
||||
const text = this.dom?.rewriteInput?.value?.trim();
|
||||
if (!text) {
|
||||
this.onToast('Type some text to rewrite first.', { error: true });
|
||||
return;
|
||||
}
|
||||
if (button) setButtonLoading(button, true, 'Rewriting…');
|
||||
try {
|
||||
const result = await api.ai.rewriteText({
|
||||
text,
|
||||
guideTitle: this.guide?.title || '',
|
||||
stepTitle: this.currentStep?.title || '',
|
||||
});
|
||||
if (!result || !result.ok) {
|
||||
this.onToast(result?.reason || 'Rewrite failed.', { error: true });
|
||||
return;
|
||||
}
|
||||
if (this.dom?.rewriteInput) this.dom.rewriteInput.value = result.text;
|
||||
this.onToast('Text rewritten.');
|
||||
} catch (err) {
|
||||
this.onToast(err.message || 'Rewrite failed.', { error: true });
|
||||
} finally {
|
||||
if (button) setButtonLoading(button, false);
|
||||
}
|
||||
}
|
||||
|
||||
async generateBlockWithAi(kind, block, button = null) {
|
||||
if (!block) return null;
|
||||
return this.runAiGeneration('block', { blockId: block.id, button });
|
||||
@@ -411,6 +442,21 @@ class GuideEditor {
|
||||
this.dom.addTableBlockBtn = el('button', { type: 'button' }, '+ Table'),
|
||||
),
|
||||
),
|
||||
el('section', {},
|
||||
el('div.row', { style: { justifyContent: 'space-between', alignItems: 'center' } },
|
||||
el('h3', { style: { margin: 0 } }, 'AI Rewrite'),
|
||||
this.dom.rewriteAiBtn = el('button.ai', {
|
||||
type: 'button',
|
||||
title: 'Rewrite the text below with AI',
|
||||
dataset: { aiAction: 'rewrite', aiTitle: 'Rewrite with AI' },
|
||||
}, 'Rewrite'),
|
||||
),
|
||||
this.dom.rewriteInput = el('textarea', {
|
||||
rows: 3,
|
||||
placeholder: 'Type something to improve…',
|
||||
style: { width: '100%', resize: 'vertical' },
|
||||
}),
|
||||
),
|
||||
el('section', {},
|
||||
el('h3', {}, 'Guide'),
|
||||
this.dom.guideSummary = el('div.muted', {}),
|
||||
@@ -583,6 +629,8 @@ class GuideEditor {
|
||||
});
|
||||
this.dom.descAiBtn.addEventListener('click', () => this.generateDescriptionWithAi(this.dom.descAiBtn));
|
||||
|
||||
this.dom.rewriteAiBtn.addEventListener('click', () => this.runTextRewrite(this.dom.rewriteAiBtn));
|
||||
|
||||
this.dom.descEditor.addEventListener('paste', (e) => {
|
||||
// Keep pasted text simple; backend sanitization will handle the rest.
|
||||
e.preventDefault();
|
||||
|
||||
Reference in New Issue
Block a user