feat(ui): improve discoverability of hidden interactions

- Expand onboarding tour from 8 to 11 steps: marquee drag-select,
  drag card to sidebar folder, and the three context menus
  (card / bulk / global); enrich bulk-mode step with range-select
  and exit tips
- Add Replay Tutorial button to help modal Getting Started tab
- Add Shortcuts cheat-sheet tab to help modal, opened directly via
  the '?' key when not typing
- Fix trigger-word tooltip to mention double-click to edit
- Keep checkpoint/embedding send tooltips truthful (no replace mode)

Sync new i18n keys to all locales (placeholders pending translation)
This commit is contained in:
Will Miao
2026-09-03 18:16:00 +08:00
parent b309becdf9
commit 8260bd022d
16 changed files with 816 additions and 54 deletions
+2
View File
@@ -607,9 +607,11 @@ export function createModelCard(model, modelType) {
sendTitle = translate('modelCard.actions.sendToWorkflow', {}, 'Send to ComfyUI (Click: Append, Shift+Click: Replace)');
copyTitle = translate('modelCard.actions.copyLoRASyntax', {}, 'Copy LoRA Syntax');
} else if (modelType === MODEL_TYPES.CHECKPOINT) {
// Checkpoint send sets the widget value directly; no append/replace modes.
sendTitle = translate('modelCard.actions.sendCheckpointToWorkflow', {}, 'Send to ComfyUI');
copyTitle = translate('modelCard.actions.copyCheckpointName', {}, 'Copy checkpoint name');
} else if (modelType === MODEL_TYPES.EMBEDDING) {
// Embedding send always appends to the prompt; no replace mode.
sendTitle = translate('modelCard.actions.sendEmbeddingToWorkflow', {}, 'Send to ComfyUI');
copyTitle = translate('modelCard.actions.copyEmbeddingName', {}, 'Copy embedding name');
} else {
+4 -4
View File
@@ -227,7 +227,7 @@ export function renderTriggerWords(words, filePath) {
const escapedWord = escapeHtml(word);
const escapedAttr = escapeAttribute(word);
return `
<div class="trigger-word-tag" data-word="${escapedAttr}" title="${translate('modals.model.triggerWords.copyWord')}">
<div class="trigger-word-tag" data-word="${escapedAttr}" title="${translate('modals.model.triggerWords.copyOrEditWord')}">
<span class="trigger-word-content">${escapedWord}</span>
<span class="trigger-word-copy">
<i class="fas fa-copy"></i>
@@ -455,7 +455,7 @@ function resetTriggerWordsUIState(section) {
// Restore click-to-copy functionality
tag.removeEventListener('click', startEditTriggerWord);
setupDisplayTriggerWordTag(tag);
tag.title = translate('modals.model.triggerWords.copyWord');
tag.title = translate('modals.model.triggerWords.copyOrEditWord');
// Show copy icon, hide delete button
if (copyIcon) copyIcon.style.display = '';
@@ -503,7 +503,7 @@ function createTriggerWordTag(word, isEditMode = false) {
const tag = document.createElement('div');
tag.className = 'trigger-word-tag';
tag.dataset.word = word;
tag.title = translate(isEditMode ? 'modals.model.triggerWords.editWord' : 'modals.model.triggerWords.copyWord');
tag.title = translate(isEditMode ? 'modals.model.triggerWords.editWord' : 'modals.model.triggerWords.copyOrEditWord');
const escapedWord = escapeHtml(word);
tag.innerHTML = `
@@ -537,7 +537,7 @@ function setupDisplayTriggerWordTag(tag) {
tag.addEventListener('click', handleDisplayTriggerWordClick);
tag.addEventListener('dblclick', handleDisplayTriggerWordDoubleClick);
tag.title = translate('modals.model.triggerWords.copyWord');
tag.title = translate('modals.model.triggerWords.copyOrEditWord');
}
/**
+63 -15
View File
@@ -1,4 +1,5 @@
import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
import { onboardingManager } from './OnboardingManager.js';
/**
* Manages help modal functionality and tutorial update notifications
@@ -55,31 +56,78 @@ export class HelpManager {
const tabButtons = document.querySelectorAll('.help-tabs .tab-btn');
tabButtons.forEach(button => {
button.addEventListener('click', (event) => {
// Remove active class from all buttons and panes
document.querySelectorAll('.help-tabs .tab-btn').forEach(btn => {
btn.classList.remove('active');
});
document.querySelectorAll('.help-content .tab-pane').forEach(pane => {
pane.classList.remove('active');
});
// Add active class to clicked button
event.currentTarget.classList.add('active');
// Show corresponding tab content
const tabId = event.currentTarget.getAttribute('data-tab');
document.getElementById(tabId).classList.add('active');
this.activateHelpTab(event.currentTarget.getAttribute('data-tab'));
});
});
// Replay tutorial button in the Getting Started tab
const replayTutorialBtn = document.getElementById('replayTutorialBtn');
if (replayTutorialBtn) {
replayTutorialBtn.addEventListener('click', () => {
// Close the help modal, then restart the onboarding tutorial
if (window.modalManager) {
window.modalManager.closeModal('helpModal');
}
onboardingManager.reset();
onboardingManager.startTutorial();
});
}
// Global "?" shortcut opens the help modal on the Shortcuts tab
document.addEventListener('keydown', (event) => {
if (event.key !== '?') return;
if (this.isTypingContext(event.target)) return;
if (window.modalManager?.isAnyModalOpen()) return;
event.preventDefault();
this.openHelpModal('shortcuts');
});
}
/**
* Check if the event target is a text entry context where "?" is literal input
*/
isTypingContext(target) {
if (!(target instanceof Element)) return false;
const tagName = target.tagName?.toLowerCase();
return target.isContentEditable || tagName === 'input' || tagName === 'textarea' || tagName === 'select';
}
/**
* Activate a specific help modal tab by its data-tab id
* @param {string} tabId - The tab id (matches data-tab and pane element id)
*/
activateHelpTab(tabId) {
const tabButton = document.querySelector(`.help-tabs .tab-btn[data-tab="${tabId}"]`);
const tabPane = document.getElementById(tabId);
if (!tabButton || !tabPane) return;
// Remove active class from all buttons and panes
document.querySelectorAll('.help-tabs .tab-btn').forEach(btn => {
btn.classList.remove('active');
});
document.querySelectorAll('.help-content .tab-pane').forEach(pane => {
pane.classList.remove('active');
});
// Activate the requested tab
tabButton.classList.add('active');
tabPane.classList.add('active');
}
/**
* Open the help modal
* @param {string} [tabId] - Optional tab id to activate after opening
*/
openHelpModal() {
openHelpModal(tabId) {
// Use modalManager to open the help modal
if (window.modalManager) {
window.modalManager.toggleModal('helpModal');
if (tabId) {
this.activateHelpTab(tabId);
}
// Add visual indicator to Documentation tab if there's new content
this.updateDocumentationTabIndicator();
+22 -2
View File
@@ -43,7 +43,7 @@ export class OnboardingManager {
{
target: '.controls .action-buttons [data-action="bulk"]',
title: () => translate('onboarding.steps.bulk.title', {}, 'Bulk Operations'),
content: () => translate('onboarding.steps.bulk.content', {}, 'Enter bulk mode by clicking this button or pressing <span class="onboarding-shortcut">B</span>. Select multiple models and perform batch operations. Use <span class="onboarding-shortcut">Ctrl+A</span> to select all visible models.'),
content: () => translate('onboarding.steps.bulk.content', {}, 'Enter bulk mode by clicking this button or pressing <span class="onboarding-shortcut">B</span> to select multiple models and perform batch operations.<br>• <span class="onboarding-shortcut">Ctrl/Cmd+A</span> select all visible models, <span class="onboarding-shortcut">Shift+Click</span> select a range.<br>• <span class="onboarding-shortcut">Esc</span> or clicking an empty area exits bulk mode.'),
position: 'bottom'
},
{
@@ -71,10 +71,30 @@ export class OnboardingManager {
position: 'top',
customPosition: { top: '20%', left: '50%' }
},
{
target: '.card-grid',
title: () => translate('onboarding.steps.marqueeSelect.title', {}, 'Drag to Select'),
content: () => translate('onboarding.steps.marqueeSelect.content', {}, 'Hold the <strong>left mouse button</strong> on an empty area of the grid and drag to draw a marquee that selects multiple cards at once.'),
position: 'top',
customPosition: { top: '20%', left: '50%' }
},
{
target: '#folderSidebar',
title: () => translate('onboarding.steps.dragToSidebar.title', {}, 'Organize by Dragging'),
content: () => translate('onboarding.steps.dragToSidebar.content', {}, 'Drag a model card onto a folder in the sidebar to move the file there. This also works with multiple selected cards in bulk mode.'),
position: 'right'
},
{
target: '.card-grid',
title: () => translate('onboarding.steps.contextMenu.title', {}, 'Context Menu'),
content: () => translate('onboarding.steps.contextMenu.content', {}, '<strong>Right-click</strong> any model card for a context menu with additional actions.'),
content: () => translate('onboarding.steps.contextMenu.content', {}, '<strong>Right-click</strong> any model card for a context menu with card actions like moving, deleting, or editing metadata.'),
position: 'top',
customPosition: { top: '20%', left: '50%' }
},
{
target: '.card-grid',
title: () => translate('onboarding.steps.contextMenus.title', {}, 'More Context Menus'),
content: () => translate('onboarding.steps.contextMenus.content', {}, 'In bulk mode, <strong>right-click a selected card</strong> for bulk actions. <strong>Right-click an empty area</strong> of the page for global actions like update checks and managing excluded models.'),
position: 'top',
customPosition: { top: '20%', left: '50%' }
}