Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_update_guide.js
T
DemonGatanjieuandClaude Opus 5.5 9f45fef85b feat(audio): explicit drop targets, drag hints and an in-app usage guide
- audio_node_targets.js is the only list of nodes the audio studio writes
  into: F5-TTS (Audio / Advanced / FromModel) takes a whole character, so the
  card header sets its main voice; ComfyUI's Load Audio takes one exact clip,
  so a row sets that file. Unlisted nodes are refused instead of matched by
  widget name, and a single emotion clip is never written into F5-TTS's
  sample (it would break {emotion} tags).
- While dragging, the hint says what releasing will do, or why a hovered node
  is refused and what to do instead (shared bindMaterialDrag targetHint /
  rejectHint).
- Script Director uses the same table and refuses nodes whose sample_audio is
  linked.
- In the audio domain the header "!" opens a five-step usage guide;
  docs/guides/audio-studio.md is the full guide (docs/guides is now tracked).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W411Z69FC3em9CzpeQEub5
2026-09-24 11:43:31 +00:00

121 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { CURRENT_UPDATE_GUIDE, validateUpdateGuide } from './update_guide_data.js';
import { i18n, translate as t } from './locales.js';
import { createViewScope } from './ui_lifecycle.js';
import { text } from './ui_dom.js';
import { startSpotlightTour, ensureTourStyles } from './ui_spotlight_tour.js';
import { createVersionLine } from './ui_version_manager.js';
const acknowledgedThisSession = new Set();
let activeGuide = null;
const storageKey = id => `anomalous_update_guide_seen:${id}`;
export function hasAcknowledged(id = CURRENT_UPDATE_GUIDE.id) {
if (acknowledgedThisSession.has(id)) return true;
try { return localStorage.getItem(storageKey(id)) === '1'; }
catch (error) { return false; } // Storage may be unavailable; session state still works.
}
export function closeUpdateGuide(owner, acknowledge = false) {
if (!activeGuide || activeGuide.owner !== owner) return;
const current = activeGuide;
activeGuide = null;
if (acknowledge) {
acknowledgedThisSession.add(current.id);
try { localStorage.setItem(storageKey(current.id), '1'); }
catch (error) { console.warn('[AMB] Update guide dismissal is saved for this session only.', error); }
}
current.scope.dispose();
}
/** Opened only by explicit user action in the header or Help. No graph or file writes. */
export function showUpdateGuide(owner, { force = false, guide = CURRENT_UPDATE_GUIDE } = {}) {
if (!owner.modal?.classList.contains('visible')) return false;
if (!validateUpdateGuide(guide, i18n)) {
console.warn('[AMB] Invalid update guide configuration; guide skipped.');
return false;
}
if (activeGuide?.owner === owner && activeGuide.id === guide.id) return false;
if (!force && hasAcknowledged(guide.id)) return false;
if (activeGuide) closeUpdateGuide(activeGuide.owner);
ensureTourStyles();
const scope = createViewScope();
const previousFocus = document.activeElement;
const dialog = document.createElement('dialog');
dialog.className = 'anomalous-update-guide';
dialog.setAttribute('aria-labelledby', 'anomalous-update-guide-title');
dialog.setAttribute('aria-describedby', 'anomalous-update-guide-body');
activeGuide = { owner, id: guide.id, scope };
scope.onDispose(() => {
if (dialog.open) dialog.close();
dialog.remove();
if (previousFocus?.isConnected) previousFocus.focus({ preventScroll: true });
});
const header = text(dialog, 'div', '', 'anomalous-update-guide-header');
text(header, 'span', t(guide.titleKey || 'updateGuideTitle'));
const dismiss = text(header, 'button', '×', 'anomalous-update-guide-close');
dismiss.type = 'button';
dismiss.setAttribute('aria-label', t('close'));
dismiss.onclick = () => closeUpdateGuide(owner, true);
dialog.appendChild(createVersionLine(owner, { beforeOpen: () => closeUpdateGuide(owner, true) }));
const icon = text(dialog, 'div', '', 'anomalous-update-guide-icon');
icon.setAttribute('aria-hidden', 'true');
const title = text(dialog, 'h2', '');
title.id = 'anomalous-update-guide-title';
const body = text(dialog, 'p', '');
body.id = 'anomalous-update-guide-body';
const tourBanner = text(dialog, 'button', t('updateGuideTourBanner') || '💡 想在主界面实地体验?点击开启按键遮罩导览 ›', 'anomalous-update-guide-tour-banner');
tourBanner.type = 'button';
tourBanner.id = 'anomalous-update-guide-tour-btn';
tourBanner.hidden = guide.tour === false;
tourBanner.onclick = () => {
closeUpdateGuide(owner, true);
startSpotlightTour(owner);
};
const progress = text(dialog, 'p', '', 'anomalous-update-guide-progress');
progress.setAttribute('aria-live', 'polite');
const footer = text(dialog, 'div', '', 'anomalous-update-guide-footer');
const skip = text(footer, 'button', t('updateGuideSkip'), 'anomalous-btn-ghost');
const navGroup = text(footer, 'div', '', 'anomalous-update-guide-nav-group');
navGroup.style.display = 'flex';
navGroup.style.gap = '8px';
navGroup.style.alignItems = 'center';
const back = text(navGroup, 'button', t('updateGuideBack'), 'anomalous-btn-ghost');
const next = text(navGroup, 'button', '', 'anomalous-btn-primary');
for (const button of [skip, back, next, tourBanner]) button.type = 'button';
let index = 0;
const render = () => {
const step = guide.steps[index];
icon.textContent = step.icon;
title.textContent = t(step.titleKey);
body.textContent = t(step.bodyKey);
tourBanner.textContent = t('updateGuideTourBanner') || '💡 想在主界面实地体验?点击开启按键遮罩导览 ›';
progress.textContent = t('updateGuideProgress', { current: index + 1, total: guide.steps.length });
back.disabled = index === 0;
next.textContent = t(index === guide.steps.length - 1 ? 'updateGuideDone' : 'updateGuideNext');
};
skip.onclick = () => closeUpdateGuide(owner, true);
back.onclick = () => { if (index > 0) { index--; render(); } };
next.onclick = () => {
if (index === guide.steps.length - 1) closeUpdateGuide(owner, true);
else { index++; render(); }
};
scope.listen(dialog, 'keydown', event => {
if (event.key === 'Escape') {
event.preventDefault();
event.stopPropagation();
closeUpdateGuide(owner, true);
}
});
scope.listen(dialog, 'cancel', event => { event.preventDefault(); closeUpdateGuide(owner, true); });
scope.listen(dialog, 'close', () => closeUpdateGuide(owner));
scope.listen(dialog, 'click', event => { if (event.target === dialog) closeUpdateGuide(owner, true); });
render();
document.body.appendChild(dialog);
dialog.showModal();
next.focus();
return true;
}