diff --git a/findings.md b/findings.md
index d353077..bbaf5df 100644
--- a/findings.md
+++ b/findings.md
@@ -12,3 +12,5 @@
- 2026-03-25:`config-panel` 将 `activeRuleConfig` 的同步逻辑从 `updated()` 调整到 `willUpdate()`,避免 Lit 在测试与运行时发出“update completed 后又触发 update”的低效更新警告。
- 2026-03-25:多语言文案已补齐到 `zh_CN` / `zh_TW` / `en`,并确认占位符必须使用 Chrome i18n 兼容的 `$1` 形式,不能写成 `{0}`。
- 2026-03-25:完整验证结果:`npm run typecheck` 通过,`npx vitest run` 通过(36 files / 263 tests);`npm run lint` 通过但仓库内仍存在大量历史 warning,本次未扩散为新的 lint error。
+- 2026-03-25:继续完成“剧集提取失败态增强”:`suggestedFailureFileId` 现在真正用于右侧预览的失败项高亮与自动滚动定位,避免“点了首个失败项但用户还要自己找”的断层体验。
+- 2026-03-25:为失败项增加内联引导文案 `episode_assist_focus_hint`,明确告诉用户可以直接点整段/片段继续修正,再次预览。
diff --git a/progress.md b/progress.md
index 949c563..4025efc 100644
--- a/progress.md
+++ b/progress.md
@@ -11,3 +11,5 @@
- 2026-03-25:完成 `preview-panel` / `virtual-preview-list` / `file-selector-panel` 的辅助回填接线,实现从预览项整段回填、片段点选、失败项快捷修正、执行成功后自动记录 recent preset。
- 2026-03-25:补齐 `zh_CN` / `zh_TW` / `en` 新文案,并新增 4 个单测文件覆盖 config-panel、preview-panel、file-selector-panel 的新交互。
- 2026-03-25:完成验证:`npm run typecheck` ✅,`npx vitest run` ✅,`npm run lint` ✅(仅剩历史 warning)。
+- 2026-03-25:完成下一批失败态增强:右侧预览支持根据 `suggestedFailureFileId` 自动高亮并滚动定位失败项,且在整段/片段回填成功后清除聚焦态,避免提示残留。
+- 2026-03-25:补充失败态引导文案与测试,验证通过:`typecheck` ✅,`vitest` 全量 266 tests ✅,`lint` ✅(仅历史 warning)。
diff --git a/src/content/components/file-selector-panel.ts b/src/content/components/file-selector-panel.ts
index f502ca1..7582403 100644
--- a/src/content/components/file-selector-panel.ts
+++ b/src/content/components/file-selector-panel.ts
@@ -570,12 +570,16 @@ export class FileSelectorPanel extends LitElement {
private handleAssistApplyFullName(e: CustomEvent<{ fileId: string; fileName: string }>): void {
this.applyAssistValue(this.episodeExtractAssistState.fillTarget, e.detail.fileName);
- this.setAssistState({ sampleFileId: e.detail.fileId, sampleFileName: e.detail.fileName });
+ this.setAssistState({
+ sampleFileId: e.detail.fileId,
+ sampleFileName: e.detail.fileName,
+ suggestedFailureFileId: null,
+ });
}
private handleAssistApplySegment(e: CustomEvent<{ fileId: string; segment: string }>): void {
this.applyAssistValue(this.episodeExtractAssistState.fillTarget, e.detail.segment);
- this.setAssistState({ sampleFileId: e.detail.fileId });
+ this.setAssistState({ sampleFileId: e.detail.fileId, suggestedFailureFileId: null });
}
private handleAssistUseItemAsSample(e: CustomEvent<{ fileId: string; fileName: string }>): void {
@@ -605,6 +609,7 @@ export class FileSelectorPanel extends LitElement {
sampleFileId: e.detail.fileId,
sampleFileName: raw,
fillTarget: 'prefix',
+ suggestedFailureFileId: null,
});
}
diff --git a/src/content/components/virtual-preview-list.ts b/src/content/components/virtual-preview-list.ts
index 49ec55a..fb765c6 100644
--- a/src/content/components/virtual-preview-list.ts
+++ b/src/content/components/virtual-preview-list.ts
@@ -44,6 +44,7 @@ export class VirtualPreviewList extends LitElement {
*/
private _virtualizerInitialized = false;
private _itemsKey: string | null = null;
+ private _lastFocusedFailureId: string | null = null;
private shouldVirtualize(): boolean {
return this.items.length > VirtualPreviewList.VIRTUALIZE_MIN_ITEMS;
@@ -124,11 +125,16 @@ export class VirtualPreviewList extends LitElement {
const conflictHint = item.conflict
? html`${I18nService.t('error_api_conflict')}`
: '';
- const previewItemClasses = ['preview-item', statusClass].filter(Boolean).join(' ');
+ const isSuggestedFailure =
+ this.episodeExtractAssistEnabled &&
+ this.episodeExtractAssistState.suggestedFailureFileId === item.file.id;
+ const previewItemClasses = ['preview-item', statusClass, isSuggestedFailure ? 'assist-focus' : '']
+ .filter(Boolean)
+ .join(' ');
const newNameClasses = ['new-name', 'new-name-primary', statusClass].filter(Boolean).join(' ');
return html`
-
+
@@ -145,6 +151,9 @@ export class VirtualPreviewList extends LitElement {
? html`
${errorText}
`
: ''
}
+ ${isSuggestedFailure
+ ? html`
${I18nService.t('episode_assist_focus_hint')}
`
+ : ''}
${this.renderAssistActions(item)}
@@ -251,6 +260,24 @@ export class VirtualPreviewList extends LitElement {
protected updated(changedProperties: PropertyValues
): void {
super.updated(changedProperties);
+ const suggestedFailureFileId = this.episodeExtractAssistState.suggestedFailureFileId;
+ if (
+ this.episodeExtractAssistEnabled &&
+ suggestedFailureFileId &&
+ suggestedFailureFileId !== this._lastFocusedFailureId &&
+ (changedProperties.has('episodeExtractAssistState') || changedProperties.has('items'))
+ ) {
+ this._lastFocusedFailureId = suggestedFailureFileId;
+ requestAnimationFrame(() => {
+ const target = this.renderRoot.querySelector(`[data-file-id="${suggestedFailureFileId}"]`);
+ target?.scrollIntoView({ block: 'center', behavior: 'smooth' });
+ });
+ }
+
+ if (!suggestedFailureFileId) {
+ this._lastFocusedFailureId = null;
+ }
+
if (changedProperties.has('items') && this.items.length > 0) {
const nextKey = this.computeItemsKey(this.items);
if (nextKey !== this._itemsKey) {
@@ -356,6 +383,10 @@ export class VirtualPreviewList extends LitElement {
border-left: 3px solid var(--cdr-border-strong, #d9d9d9);
}
+ .preview-item.assist-focus {
+ box-shadow: inset 0 0 0 1px var(--cdr-primary, #1890ff), 0 0 0 2px rgba(24, 144, 255, 0.12);
+ }
+
.preview-content {
flex: 1;
min-width: 0;
@@ -434,6 +465,16 @@ export class VirtualPreviewList extends LitElement {
word-break: break-word;
}
+ .assist-focus-hint {
+ margin-top: 6px;
+ font-size: 12px;
+ color: var(--cdr-info-text, #0958d9);
+ background: var(--cdr-info-bg, #e6f4ff);
+ border: 1px solid var(--cdr-info-border, #91caff);
+ border-radius: 8px;
+ padding: 6px 8px;
+ }
+
.assist-actions {
display: flex;
flex-wrap: wrap;
diff --git a/src/locales/en.json b/src/locales/en.json
index 6cad55a..a9ba247 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -240,5 +240,6 @@
"episode_assist_focus_segment_mode": "Switch to segment mode",
"rule_preset_invalid": "This template is incompatible with the current version. Please delete and save it again.",
"rule_preset_invalid_badge": "Invalid",
- "rule_preset_invalid_hint": "This template config is no longer valid and cannot be applied."
+ "rule_preset_invalid_hint": "This template config is no longer valid and cannot be applied.",
+ "episode_assist_focus_hint": "The failed item is highlighted. Fill from the full text or a segment, then preview again."
}
diff --git a/src/locales/zh_CN.json b/src/locales/zh_CN.json
index c984e0d..9735670 100644
--- a/src/locales/zh_CN.json
+++ b/src/locales/zh_CN.json
@@ -240,5 +240,6 @@
"episode_assist_focus_segment_mode": "切换到片段点选模式",
"rule_preset_invalid": "该模板与当前版本不兼容,请删除后重新保存。",
"rule_preset_invalid_badge": "已失效",
- "rule_preset_invalid_hint": "该模板配置已失效,暂时无法应用。"
+ "rule_preset_invalid_hint": "该模板配置已失效,暂时无法应用。",
+ "episode_assist_focus_hint": "已为你定位失败项,可直接点整段或片段回填后继续预览。"
}
diff --git a/src/locales/zh_TW.json b/src/locales/zh_TW.json
index 91f04b2..c0abdd2 100644
--- a/src/locales/zh_TW.json
+++ b/src/locales/zh_TW.json
@@ -240,5 +240,6 @@
"episode_assist_focus_segment_mode": "切換到片段點選模式",
"rule_preset_invalid": "此模板與目前版本不相容,請刪除後重新儲存。",
"rule_preset_invalid_badge": "已失效",
- "rule_preset_invalid_hint": "此模板設定已失效,暫時無法套用。"
+ "rule_preset_invalid_hint": "此模板設定已失效,暫時無法套用。",
+ "episode_assist_focus_hint": "已為你定位失敗項,可直接點整段或片段回填後繼續預覽。"
}
diff --git a/tests/unit/preview-panel-episode-assist.test.ts b/tests/unit/preview-panel-episode-assist.test.ts
index 9b1f792..ed729d0 100644
--- a/tests/unit/preview-panel-episode-assist.test.ts
+++ b/tests/unit/preview-panel-episode-assist.test.ts
@@ -71,6 +71,11 @@ describe('PreviewPanel episode assist bridge', () => {
expect(prefixEvents).toHaveLength(1);
expect(modeEvents).toHaveLength(1);
+ const focusedItem = previewList?.shadowRoot?.querySelector('[data-file-id="file-1"]');
+ const focusHint = previewList?.shadowRoot?.querySelector('[data-role="episode-assist-focus-hint"]');
+ expect(focusedItem?.className).toContain('assist-focus');
+ expect(focusHint?.textContent).toContain('定位');
+
panel.remove();
});
});
diff --git a/tests/unit/virtual-preview-list.test.ts b/tests/unit/virtual-preview-list.test.ts
index 2c55b6b..9a27c5c 100644
--- a/tests/unit/virtual-preview-list.test.ts
+++ b/tests/unit/virtual-preview-list.test.ts
@@ -151,6 +151,51 @@ describe('VirtualPreviewList pending preview mapping', () => {
element.remove();
});
+ it('highlights and scrolls to the suggested failure item in assist mode', async () => {
+ const originalScrollIntoView = HTMLElement.prototype.scrollIntoView;
+ const scrollSpy = vi.fn();
+ HTMLElement.prototype.scrollIntoView = scrollSpy;
+
+ const element = new VirtualPreviewList();
+ element.episodeExtractAssistEnabled = true;
+ element.episodeExtractAssistState = {
+ sampleFileId: 'file-error',
+ sampleFileName: 'Show.S01E01',
+ fillTarget: 'prefix',
+ fillMode: 'segment',
+ suggestedFailureFileId: 'file-error',
+ };
+ element.items = [
+ {
+ file: {
+ id: 'file-error',
+ name: 'Show.S01E01.mkv',
+ ext: '.mkv',
+ size: 1,
+ mtime: Date.now(),
+ isDir: false,
+ },
+ newName: 'Show.S01E01.mkv',
+ conflict: false,
+ error: 'extract_episode_not_found',
+ },
+ ];
+
+ document.body.appendChild(element);
+ await element.updateComplete;
+ await new Promise((resolve) => requestAnimationFrame(() => resolve(undefined)));
+
+ const focusedItem = element.shadowRoot?.querySelector('[data-file-id="file-error"]');
+ const hint = element.shadowRoot?.querySelector('[data-role="episode-assist-focus-hint"]');
+
+ expect(focusedItem?.className).toContain('assist-focus');
+ expect(hint?.textContent).toContain(I18nService.t('episode_assist_focus_hint'));
+ expect(scrollSpy).toHaveBeenCalled();
+
+ HTMLElement.prototype.scrollIntoView = originalScrollIntoView;
+ element.remove();
+ });
+
it('locks preview-item structure with semantic classes and target-specific truncation rules', async () => {
const element = new VirtualPreviewList();
element.items = [