Files
DemonGatanjieuandClaude Opus 5.5 cae7fd2a4a Audio gallery becomes a view of Voices; the rail fits the number keys
With the AI workbench the rail had ten entries for nine number keys, so
the audio gallery had none. It is now the third tab of Voices (Characters,
Voice-over, Audio); 9 reopens Voices on the view used last, and a test
keeps the rail within nine entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 20:26:31 +08:00

58 lines
4.3 KiB
JavaScript

// Content only. Maintenance checklist: docs/architecture/update-guide.md.
// Change the ID only when users should see a new guide, never for typo fixes.
export const CURRENT_UPDATE_GUIDE = Object.freeze({
id: '2026-10-quick-search-and-canvas-menu',
steps: Object.freeze([
{ id: 'quick-search', icon: '⌨️', titleKey: 'updateGuideQuickSearchTitle', bodyKey: 'updateGuideQuickSearchBody' },
{ id: 'canvas-menu', icon: '🖱️', titleKey: 'updateGuideCanvasMenuTitle', bodyKey: 'updateGuideCanvasMenuBody' },
{ id: 'keep-combo', icon: '📌', titleKey: 'updateGuideKeepComboTitle', bodyKey: 'updateGuideKeepComboBody' },
{ id: 'sidecars', icon: '🗂️', titleKey: 'updateGuideSidecarsTitle', bodyKey: 'updateGuideSidecarsBody' },
{ id: 'voice-over', icon: '🎭', titleKey: 'updateGuideVoiceOverTitle', bodyKey: 'updateGuideVoiceOverBody' },
]),
});
// Spotlight tour of the audio studio. Steps whose target is not on screen (no characters
// yet, the audio gallery open, sidebar collapsed) are skipped.
export const AUDIO_TOUR_STEPS = Object.freeze([
{ targetSelector: '[data-tour="audio-import"]', icon: '📥', titleKey: 'audioTourImportTitle', bodyKey: 'audioTourImportBody', position: 'bottom' },
{ targetSelector: '.anomalous-character-voice-header.is-draggable', icon: '🧲', titleKey: 'audioTourDragTitle', bodyKey: 'audioTourDragBody', position: 'bottom' },
{ targetSelector: '.anomalous-character-voice-actions', icon: '🎭', titleKey: 'audioTourEditTitle', bodyKey: 'audioTourEditBody', position: 'bottom' },
{ targetSelector: '.anomalous-voice-copy-btn', icon: '🏷️', titleKey: 'audioTourTagTitle', bodyKey: 'audioTourTagBody', position: 'bottom' },
{ targetSelector: '[data-tour="voice-tab-script"]', icon: '📜', titleKey: 'audioTourScriptTitle', bodyKey: 'audioTourScriptBody', position: 'bottom' },
{ targetSelector: '[data-tour="voice-tab-audio-gallery"]', icon: '🎧', titleKey: 'audioTourGalleryTitle', bodyKey: 'audioTourGalleryBody', position: 'bottom' },
{ targetSelector: '.anomalous-audio-sidebar-settings', icon: '⚙️', titleKey: 'audioTourSettingsTitle', bodyKey: 'audioTourSettingsBody', position: 'top' },
]);
// How-to for the audio studio; the "!" button opens it while the audio domain is active.
// Optional fields: titleKey (dialog title), tour: false (no spotlight tour banner) or the
// guide's own spotlight steps (default: the browser's tour).
export const AUDIO_USAGE_GUIDE = Object.freeze({
id: 'audio-studio-usage',
titleKey: 'audioGuideTitle',
tour: AUDIO_TOUR_STEPS,
steps: Object.freeze([
{ id: 'files', icon: '🎙️', titleKey: 'audioGuideFilesTitle', bodyKey: 'audioGuideFilesBody' },
{ id: 'drag', icon: '🧲', titleKey: 'audioGuideDragTitle', bodyKey: 'audioGuideDragBody' },
{ id: 'tags', icon: '🏷️', titleKey: 'audioGuideTagsTitle', bodyKey: 'audioGuideTagsBody' },
{ id: 'script', icon: '📜', titleKey: 'audioGuideScriptTitle', bodyKey: 'audioGuideScriptBody' },
{ id: 'generate', icon: '🎧', titleKey: 'audioGuideGenerateTitle', bodyKey: 'audioGuideGenerateBody' },
]),
});
export function validateUpdateGuide(guide, locales) {
if (!guide || typeof guide.id !== 'string' || !/^[a-z0-9][a-z0-9-]{0,79}$/.test(guide.id)) return false;
if (!Array.isArray(guide.steps) || guide.steps.length < 1 || guide.steps.length > 5) return false;
if (guide.titleKey !== undefined && !['zh', 'en'].every(locale => typeof locales[locale]?.[guide.titleKey] === 'string')) return false;
if (Array.isArray(guide.tour) && !guide.tour.every(step => typeof step?.targetSelector === 'string'
&& ['titleKey', 'bodyKey'].every(field => ['zh', 'en'].every(locale => typeof locales[locale]?.[step[field]] === 'string')))) return false;
const ids = new Set();
return guide.steps.every(step => {
if (!step || typeof step.id !== 'string' || !step.id || ids.has(step.id)) return false;
ids.add(step.id);
if (typeof step.icon !== 'string' || !step.icon || step.icon.length > 8) return false;
return ['titleKey', 'bodyKey'].every(field => typeof step[field] === 'string'
&& ['zh', 'en'].every(locale => typeof locales[locale]?.[step[field]] === 'string'
&& locales[locale][step[field]].trim().length > 0));
});
}