diff --git a/app/renderer/editor.js b/app/renderer/editor.js index 6db5c4d..4825afd 100644 --- a/app/renderer/editor.js +++ b/app/renderer/editor.js @@ -167,7 +167,6 @@ 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) { @@ -223,36 +222,6 @@ 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 }); @@ -442,21 +411,6 @@ 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', {}), @@ -629,8 +583,6 @@ 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(); diff --git a/app/text-intel.js b/app/text-intel.js index 4ef136f..3206670 100644 --- a/app/text-intel.js +++ b/app/text-intel.js @@ -5,6 +5,7 @@ const path = require('node:path'); const { execFileSync } = require('node:child_process'); const { + DEFAULT_CAPTURE_TITLES, buildCaptureTitle, normalizeOllamaHost, normalizeAiPatch, @@ -14,6 +15,8 @@ const { normalizeWhitespace, } = require('../core/text-intel'); +const DEFAULT_TITLE_VALUES = new Set(Object.values(DEFAULT_CAPTURE_TITLES).concat(['Capture'])); + const OCR_CROP = { width: 420, height: 220, @@ -430,18 +433,20 @@ public static class Win32 { // Use stored capture metadata when available (best context, from capture time). // Fall back to re-running OCR on the stored image only when metadata is absent. if (step.captureMetadata) { + const rawCandidate = buildCaptureTitle({ + mode: step.captureMetadata.mode || 'fullscreen', + metadata: { + windowTitle: step.captureMetadata.windowTitle, + appName: step.captureMetadata.appName, + elementLabel: step.captureMetadata.elementLabel, + elementRole: step.captureMetadata.elementRole, + }, + ocrText: step.captureMetadata.ocrText, + }); captureContext = { ...step.captureMetadata, - titleCandidate: buildCaptureTitle({ - mode: step.captureMetadata.mode || 'fullscreen', - metadata: { - windowTitle: step.captureMetadata.windowTitle, - appName: step.captureMetadata.appName, - elementLabel: step.captureMetadata.elementLabel, - elementRole: step.captureMetadata.elementRole, - }, - ocrText: step.captureMetadata.ocrText, - }), + // Don't suggest a generic fallback title — leave it blank so AI generates from context. + titleCandidate: DEFAULT_TITLE_VALUES.has(rawCandidate) ? '' : rawCandidate, }; } else if (step.image) { const imagePath = this.store.stepImagePath(guideId, stepId, 'working') || this.store.stepImagePath(guideId, stepId, 'original'); @@ -454,7 +459,7 @@ public static class Win32 { this.collectForegroundWindowContext(), this.ocrAroundClick({ image, size: image.getSize(), display: { bounds: { x: 0, y: 0, width: image.getSize().width, height: image.getSize().height } } }, clickPoint), ]); - const titleCandidate = buildCaptureTitle({ + const rawCandidate2 = buildCaptureTitle({ mode: step.kind === 'image' ? 'fullscreen' : 'window', metadata, ocrText: ocr.text, @@ -462,7 +467,7 @@ public static class Win32 { captureContext = { ...metadata, ocrText: ocr.text, - titleCandidate, + titleCandidate: DEFAULT_TITLE_VALUES.has(rawCandidate2) ? '' : rawCandidate2, mode: step.kind === 'image' ? 'fullscreen' : 'content', }; } diff --git a/core/text-intel.js b/core/text-intel.js index 823cacf..0f0f698 100644 --- a/core/text-intel.js +++ b/core/text-intel.js @@ -386,10 +386,22 @@ function summarizeBlocks(step = {}) { return parts.length ? parts.join('\n') : '(none)'; } +const DEFAULT_PLACEHOLDER_TITLES = new Set( + Object.values(DEFAULT_CAPTURE_TITLES).concat(['Capture', 'Untitled step']), +); + +function isPlaceholderTitle(title) { + return !title || DEFAULT_PLACEHOLDER_TITLES.has(title); +} + function summarizeStepForAi(step = {}) { + const titleLine = isPlaceholderTitle(step.title) + ? 'Step title: (not set — generate a specific action title from the capture context)' + : `Step title: ${step.title}`; + const descText = htmlToText(step.descriptionHtml || ''); return [ - `Step title: ${step.title || '(empty)'}`, - `Step description: ${htmlToText(step.descriptionHtml || '') || '(empty)'}`, + titleLine, + `Step description: ${descText || '(empty)'}`, `Step status: ${step.status || 'todo'}`, `Blocks:\n${summarizeBlocks(step)}`, ].join('\n'); @@ -407,7 +419,9 @@ function hasRichCaptureContext(captureContext) { const ocr = normalizeWhitespace(captureContext.ocrText || ''); const win = normalizeWhitespace(captureContext.windowTitle || ''); const app = normalizeWhitespace(captureContext.appName || ''); - return ocr.length > 3 || (win.length > 2 && !isBrowserNoise(win)) || app.length > 1; + const element = normalizeWhitespace(captureContext.elementLabel || ''); + // Any non-trivial context signal is enough — even just an app name. + return ocr.length > 3 || win.length > 2 || app.length > 1 || element.length > 1; } function buildAiPrompt({ @@ -417,11 +431,18 @@ function buildAiPrompt({ captureContext = null, block = null, } = {}) { + const hasDraftTitle = step && !isPlaceholderTitle(step.title); + const hasDraftDesc = step && Boolean(htmlToText(step.descriptionHtml || '')); + const targetText = { - title: 'rewrite only the step title', - description: 'rewrite only the step description', - block: 'rewrite only one block', - all: 'rewrite the step title, description, and any useful blocks', + title: hasDraftTitle + ? 'improve the user\'s draft step title — keep their intent, make it read like professional documentation' + : 'write a specific action title for this step using the capture context', + description: hasDraftDesc + ? 'improve the user\'s draft description — keep their intent, make it read like professional documentation' + : 'write a 1–2 sentence description of what the user does in this step, using the capture context', + block: 'rewrite only the target block', + all: 'write the step title, description, and any useful blocks from the capture context', }[target] || 'rewrite the step'; const richContext = hasRichCaptureContext(captureContext); @@ -474,13 +495,18 @@ function buildAiPrompt({ block ? `Target block:\n${JSON.stringify(block, null, 2)}` : null, '', 'Rules:', - '- Write titles as short imperative actions: "Click Save", "Select New document", "Open Settings".', - '- If the suggested title is already a good imperative action, use it as-is or refine it slightly.', - '- Write descriptions in 1–2 sentences describing exactly what the user does in this step.', + '- Titles must be short imperative actions: "Click Save", "Select New document", "Open Settings".', + '- NEVER output "Screen capture", "Window capture", "Region capture", or "Capture" as a title — always produce something specific.', + hasDraftTitle + ? '- The user has a draft title. Improve its wording without changing their intent.' + : '- No title yet. Use the capture context (OCR text, window, app) to write a specific action title.', + hasDraftDesc + ? '- The user has a draft description. Polish it to read like professional documentation.' + : '- No description yet. Write 1–2 sentences describing exactly what the user does.', '- Only include blocks that provide genuinely useful supplemental information (warnings, tips, code).', richContext - ? '- You have rich context: use the OCR text, window title, and element info to write specific documentation.' - : '- Context is limited: write a minimal title. Leave description empty and return no blocks unless the step already has meaningful content.', + ? '- Use the OCR text, window title, app name, and element info to make the documentation specific.' + : '- Context is limited. Use the app name or window title if available; generate a reasonable action title.', '- Do NOT generate blocks that describe the technical capture process or mention OCR.', '- Do NOT invent details not supported by the capture context.', '- If the target is one block, only rewrite that block.',