From 68ceee83ef004cef639c9c5aaa67d6066735796c Mon Sep 17 00:00:00 2001 From: Twest2 Date: Wed, 24 Jun 2026 09:27:23 -0500 Subject: [PATCH] fix ai title generation and remove separate rewrite section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove the standalone "AI Rewrite" section from the editor panel. The existing title and description AI buttons already flush the step first, so they rewrite whatever the user has typed in those fields. - Never pass a generic fallback title ("Screen capture", "Window capture", "Region capture", "Capture") as the AI title candidate or as step content. It is now treated as "(not set — generate a specific action title)" so the AI always produces something real. - hasRichCaptureContext now counts any non-trivial app name or window title as sufficient context, instead of requiring non-browser noise. - Prompt rules updated: "NEVER output Screen/Window/Region capture", separate paths for improving a user draft vs generating from context, and clearer guidance when context is limited (use app/window name). - isPlaceholderTitle helper guards summarizeStepForAi so a default-titled step presents itself as empty to the AI. Co-Authored-By: Claude Sonnet 4.6 --- app/renderer/editor.js | 48 ---------------------------------------- app/text-intel.js | 29 ++++++++++++++---------- core/text-intel.js | 50 ++++++++++++++++++++++++++++++++---------- 3 files changed, 55 insertions(+), 72 deletions(-) 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.',