fix ai title generation and remove separate rewrite section
- 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 <[email protected]>
This commit is contained in:
@@ -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();
|
||||
|
||||
+17
-12
@@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
+38
-12
@@ -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.',
|
||||
|
||||
Reference in New Issue
Block a user