improve AI rewrite UX and title quality
**AI button: dynamic tooltip hints** - titleAiBtn and descAiBtn now show "Rewrite with AI" when the field already has user content, "Generate with AI" when empty. - updateAiButtonHints() fires on every title/description input event and whenever syncStepFields() runs to keep hints current. **Stronger rewrite prompt** - When the user has typed a draft title or description, the prompt now shows it explicitly: "User's draft title (rewrite this): '...'" - Rules changed from "improve its wording" to "Your only job is to polish its grammar and phrasing. Do NOT replace it with something different." — prevents the model from ignoring the user's text and generating fresh content from capture context. - The suggested-title hint is suppressed when a draft title exists so the model doesn't silently swap the user's text for the auto-title. **Title quality: generic window title filter** - GENERIC_WINDOW_TITLES Set filters "New Tab", "Untitled", "Loading" etc. from the window-title path so they no longer produce titles like "Open New Tab in Chrome". **Title quality: app name stripping for non-browser apps** - stripBrowserNameSuffix now accepts an optional appName; it strips the app's display name and process name from the window title suffix using the same pattern as browser names. - "Document1.docx - Word" with appName "winword" → "Document1.docx". - buildCaptureTitle passes metadata.appName into the strip call. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
@@ -227,6 +227,21 @@ class GuideEditor {
|
||||
return this.runAiGeneration('block', { blockId: block.id, button });
|
||||
}
|
||||
|
||||
updateAiButtonHints() {
|
||||
const PLACEHOLDER_TITLES = new Set([
|
||||
'', 'screen capture', 'window capture', 'region capture', 'capture',
|
||||
]);
|
||||
if (this.dom.titleAiBtn) {
|
||||
const val = (this.dom.titleInput?.value || '').trim();
|
||||
const hasDraft = Boolean(val) && !PLACEHOLDER_TITLES.has(val.toLowerCase());
|
||||
this.dom.titleAiBtn.title = hasDraft ? 'Rewrite step title with AI' : 'Generate step title with AI';
|
||||
}
|
||||
if (this.dom.descAiBtn) {
|
||||
const hasDesc = Boolean((this.dom.descEditor?.innerText || '').trim());
|
||||
this.dom.descAiBtn.title = hasDesc ? 'Rewrite description with AI' : 'Generate description with AI';
|
||||
}
|
||||
}
|
||||
|
||||
get currentStep() {
|
||||
return this.stepMap.get(this.selectedStepId) || null;
|
||||
}
|
||||
@@ -504,6 +519,7 @@ class GuideEditor {
|
||||
this.saveStepDebounced();
|
||||
this.renderStepList();
|
||||
this.emitMeta();
|
||||
this.updateAiButtonHints();
|
||||
});
|
||||
this.dom.titleAiBtn.addEventListener('click', () => this.generateTitleWithAi(this.dom.titleAiBtn));
|
||||
|
||||
@@ -575,6 +591,7 @@ class GuideEditor {
|
||||
this.saveStepDebounced();
|
||||
this.emitMeta();
|
||||
this.updateToolbarState();
|
||||
this.updateAiButtonHints();
|
||||
});
|
||||
this.dom.descEditor.addEventListener('keyup', () => this.updateToolbarState());
|
||||
this.dom.descEditor.addEventListener('mouseup', () => this.updateToolbarState());
|
||||
@@ -1019,6 +1036,7 @@ class GuideEditor {
|
||||
if (document.activeElement !== this.dom.titleInput) this.dom.titleInput.value = step.title || '';
|
||||
if (document.activeElement !== this.dom.descEditor) this.dom.descEditor.innerHTML = step.descriptionHtml || '';
|
||||
this.dom.statusSelect.value = step.status || 'todo';
|
||||
this.updateAiButtonHints();
|
||||
this.dom.hiddenToggle.querySelector('input').checked = Boolean(step.hidden);
|
||||
this.dom.skippedToggle.querySelector('input').checked = Boolean(step.skipped);
|
||||
this.dom.forceNewPageToggle.querySelector('input').checked = Boolean(step.forceNewPage);
|
||||
|
||||
+51
-19
@@ -44,6 +44,13 @@ const GENERIC_OCR_PHRASES = new Set([
|
||||
'type',
|
||||
]);
|
||||
|
||||
// Generic OS/browser chrome titles that tell us nothing about what the user did.
|
||||
const GENERIC_WINDOW_TITLES = new Set([
|
||||
'new tab', 'new window', 'new incognito window', 'new incognito tab',
|
||||
'new document', 'untitled', 'blank page', 'home page', 'homepage',
|
||||
'start page', 'speed dial', 'loading', 'loading…', 'loading...',
|
||||
]);
|
||||
|
||||
const BROWSER_NAME_PHRASES = new Set([
|
||||
'google chrome',
|
||||
'chrome',
|
||||
@@ -267,6 +274,7 @@ function isUsefulTitleCandidate(text, { source = 'ocr' } = {}) {
|
||||
if (BROWSER_NAME_PHRASES.has(lower)) return false;
|
||||
if (isPathOrUrlLike(clean)) return false;
|
||||
if ((source === 'window' || source === 'app') && isBrowserNoise(clean)) return false;
|
||||
if (source === 'window' && GENERIC_WINDOW_TITLES.has(lower)) return false;
|
||||
if (/^[\p{P}\p{S}0-9]+$/u.test(clean)) return false;
|
||||
return true;
|
||||
}
|
||||
@@ -287,14 +295,25 @@ function candidateWords(text) {
|
||||
return clean.split(/\s+/).filter((w) => /[a-zA-Z0-9]/.test(w));
|
||||
}
|
||||
|
||||
// Remove trailing "- Google Chrome", "| Firefox", etc. from a browser window title,
|
||||
// leaving just the page title portion: "oracle - Google Search - Google Chrome" → "oracle - Google Search".
|
||||
function stripBrowserNameSuffix(text) {
|
||||
// Remove trailing "- Google Chrome", "| Firefox", etc. from a window title.
|
||||
// When appName is supplied, also strips the specific app's display name suffix:
|
||||
// "Document1 - Word" → "Document1" when appName is "winword".
|
||||
function stripBrowserNameSuffix(text, appName) {
|
||||
let clean = normalizeWhitespace(text);
|
||||
// Always strip known browser names first.
|
||||
for (const name of BROWSER_NAME_PHRASES) {
|
||||
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
clean = clean.replace(new RegExp(`\\s*[-–—·|•]\\s*${escaped}\\s*$`, 'i'), '').trim();
|
||||
}
|
||||
// Also strip the specific app's display name when provided.
|
||||
if (appName) {
|
||||
const display = cleanAppName(appName);
|
||||
const raw = normalizeWhitespace(appName).replace(/\.exe$/i, '');
|
||||
for (const name of [display, raw].filter(Boolean)) {
|
||||
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
clean = clean.replace(new RegExp(`\\s*[-–—·|•]\\s*${escaped}\\s*$`, 'i'), '').trim();
|
||||
}
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
|
||||
@@ -476,8 +495,8 @@ function buildCaptureTitle({ mode = 'fullscreen', metadata = {}, ocrText = '', r
|
||||
return app ? qualifyTitleWithApp(elementPhrase, metadata.appName) : elementPhrase;
|
||||
}
|
||||
|
||||
// 6. Browser window title (suffix stripped) → page title or search query.
|
||||
const strippedWindowTitle = stripBrowserNameSuffix(metadata.windowTitle || '');
|
||||
// 6. Window title (browser suffix + app name stripped) → page title or search query.
|
||||
const strippedWindowTitle = stripBrowserNameSuffix(metadata.windowTitle || '', metadata.appName);
|
||||
if (strippedWindowTitle) {
|
||||
const searchQuery = extractSearchQuery(strippedWindowTitle);
|
||||
if (searchQuery) {
|
||||
@@ -670,16 +689,29 @@ function buildAiPrompt({
|
||||
'Use block.position values from [before-title, after-title, before-image, after-image, before-description, after-description].',
|
||||
].join(' ');
|
||||
|
||||
const contextLines = captureContext ? [
|
||||
captureContext.windowTitle ? `Active window: ${captureContext.windowTitle}` : null,
|
||||
captureContext.appName ? `App: ${captureContext.appName}` : null,
|
||||
captureContext.elementLabel ? `UI element: ${captureContext.elementLabel}${captureContext.elementRole ? ` (${captureContext.elementRole})` : ''}` : null,
|
||||
captureContext.elementValue ? `Element content (what was typed): ${captureContext.elementValue}` : null,
|
||||
captureContext.recentTyped ? `Keyboard input before this step: ${captureContext.recentTyped}` : null,
|
||||
captureContext.recentShortcut ? `Keyboard shortcut used: ${captureContext.recentShortcut}` : null,
|
||||
captureContext.ocrText ? `OCR text near click:\n${captureContext.ocrText}` : null,
|
||||
captureContext.titleCandidate ? `Suggested title: ${captureContext.titleCandidate}` : null,
|
||||
].filter(Boolean) : [];
|
||||
// When the user already has a draft, surface it prominently so the model
|
||||
// knows exactly what text to polish rather than generating from scratch.
|
||||
const descText = htmlToText(step?.descriptionHtml || '');
|
||||
const draftTitleLine = hasDraftTitle && (target === 'title' || target === 'all')
|
||||
? `User's draft title (rewrite this): "${step.title}"` : null;
|
||||
const draftDescLine = hasDraftDesc && (target === 'description' || target === 'all')
|
||||
? `User's draft description (rewrite this): "${descText}"` : null;
|
||||
|
||||
const contextLines = [
|
||||
...(captureContext ? [
|
||||
captureContext.windowTitle ? `Active window: ${captureContext.windowTitle}` : null,
|
||||
captureContext.appName ? `App: ${captureContext.appName}` : null,
|
||||
captureContext.elementLabel ? `UI element: ${captureContext.elementLabel}${captureContext.elementRole ? ` (${captureContext.elementRole})` : ''}` : null,
|
||||
captureContext.elementValue ? `Element content (what was typed): ${captureContext.elementValue}` : null,
|
||||
captureContext.recentTyped ? `Keyboard input before this step: ${captureContext.recentTyped}` : null,
|
||||
captureContext.recentShortcut ? `Keyboard shortcut used: ${captureContext.recentShortcut}` : null,
|
||||
captureContext.ocrText ? `OCR text near click:\n${captureContext.ocrText}` : null,
|
||||
(!hasDraftTitle || target === 'description') && captureContext.titleCandidate
|
||||
? `Suggested title: ${captureContext.titleCandidate}` : null,
|
||||
] : []),
|
||||
draftTitleLine,
|
||||
draftDescLine,
|
||||
].filter(Boolean);
|
||||
|
||||
const prompt = [
|
||||
'You write concise, action-focused step-by-step documentation for a desktop application guide.',
|
||||
@@ -716,11 +748,11 @@ function buildAiPrompt({
|
||||
'Rules:',
|
||||
'- 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.'
|
||||
hasDraftTitle && (target === 'title' || target === 'all')
|
||||
? '- The user wrote their own title (shown above). Your only job is to polish its grammar and phrasing. Do NOT replace it with something different. Do NOT change what action or subject it describes.'
|
||||
: '- 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.'
|
||||
hasDraftDesc && (target === 'description' || target === 'all')
|
||||
? '- The user wrote their own description (shown above). Polish the wording to sound professional but preserve every fact and intent they stated.'
|
||||
: '- 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
|
||||
|
||||
Reference in New Issue
Block a user