Refresh exact frontend lanes and relocated host contracts while preserving Desktop settings behavior. Recognize first-party Switch nodes and optional branches without suppressing required-input or broken-link diagnostics. Revalidate canvas, workflow and target identity across focus navigation; cancel stale requests on newer actions and sidebar teardown. Keep legacy positioning and nested graph traversal covered by action-level regressions. Validation: 75 source contracts, 84 frontend unit tests, Windows full gate with 871 backend tests and 125 subtests, and 149 mocked browser E2E tests. Seven existing backend skips remain. No live-host certification is claimed.
686 lines
35 KiB
JavaScript
686 lines
35 KiB
JavaScript
/**
|
|
* ComfyUI-Doctor Extension Entry Point
|
|
* Registers settings with ComfyUI's settings panel and initializes the Doctor UI.
|
|
*/
|
|
import { app } from "../../../scripts/app.js";
|
|
import { api } from "../../../scripts/api.js";
|
|
import { DoctorUI } from "./doctor_ui.js";
|
|
import { DoctorAPI } from "./doctor_api.js";
|
|
import { isPreactEnabled, loadPreact, getLoadError } from "./preact-loader.js";
|
|
import { tabRegistry, TabManager } from "./doctor_tabs.js";
|
|
import {
|
|
enforceDoctorSidebarMinWidth,
|
|
} from "./doctor_sidebar_layout.js";
|
|
import * as ChatTab from "./tabs/chat_tab.js";
|
|
import * as StatsTab from "./tabs/stats_tab.js";
|
|
import * as SettingsTab from "./tabs/settings_tab.js";
|
|
// R5: Error Boundaries
|
|
import { installGlobalErrorHandlers } from "./global_error_handler.js";
|
|
import { getRuntimeApiKey, setRuntimeApiKey, migrateLegacyApiKeyFromSettings } from "./llm_key_store.js";
|
|
import {
|
|
DOCTOR_DEFAULTS,
|
|
DOCTOR_EXTENSION_SETTINGS,
|
|
destroyDoctorSidebarMount,
|
|
ensureDoctorSettingsRegistered,
|
|
getDoctorRuntimeSettings,
|
|
getDoctorSetting,
|
|
isDoctorEnabled,
|
|
isDoctorErrorBoundariesEnabled,
|
|
registerDoctorSidebarTab,
|
|
} from "./comfyui_frontend_compat.js";
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// A7: PREACT ISLANDS FEATURE FLAG
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Controls whether Preact-based UI components (islands) are loaded.
|
|
// When disabled, the extension uses pure Vanilla JS fallback UI.
|
|
//
|
|
// How to disable: localStorage.setItem('doctor_preact_disabled', 'true')
|
|
// Last Modified: 2026-01-05 (A7 Phase 2)
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
const PREACT_ISLANDS_ENABLED = localStorage.getItem('doctor_preact_disabled') !== 'true' && isPreactEnabled();
|
|
|
|
// Provider default URLs (will be fetched from backend on init)
|
|
let PROVIDER_DEFAULTS = {
|
|
"openai": "https://api.openai.com/v1",
|
|
"anthropic": "https://api.anthropic.com",
|
|
"deepseek": "https://api.deepseek.com/v1",
|
|
"groq": "https://api.groq.com/openai/v1",
|
|
"gemini": "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
"xai": "https://api.x.ai/v1",
|
|
"openrouter": "https://openrouter.ai/api/v1",
|
|
"ollama": "http://127.0.0.1:11434",
|
|
"lmstudio": "http://localhost:1234/v1",
|
|
"custom": ""
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// R5: ERROR BOUNDARIES FEATURE FLAG
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
// Fetch provider defaults from backend (supports env vars)
|
|
async function loadProviderDefaults() {
|
|
try {
|
|
const response = await fetch('/doctor/provider_defaults');
|
|
if (response.ok) {
|
|
const defaults = await response.json();
|
|
PROVIDER_DEFAULTS = { ...PROVIDER_DEFAULTS, ...defaults };
|
|
console.log('[ComfyUI-Doctor] Provider defaults loaded:', PROVIDER_DEFAULTS);
|
|
}
|
|
} catch (error) {
|
|
console.warn('[ComfyUI-Doctor] Failed to load provider defaults, using fallback:', error);
|
|
}
|
|
}
|
|
|
|
// ComfyUI Settings → About panel badges (ComfyUI_frontend AboutPanel.vue)
|
|
const DOCTOR_REPO_URL = "https://github.com/rookiestar28/ComfyUI-Doctor";
|
|
const DOCTOR_ABOUT_BADGES = [
|
|
{
|
|
label: "ComfyUI Doctor",
|
|
url: DOCTOR_REPO_URL,
|
|
icon: "pi pi-github",
|
|
},
|
|
];
|
|
|
|
app.registerExtension({
|
|
name: "ComfyUI-Doctor",
|
|
aboutPageBadges: DOCTOR_ABOUT_BADGES,
|
|
settings: DOCTOR_EXTENSION_SETTINGS,
|
|
|
|
async setup() {
|
|
console.log("[ComfyUI-Doctor] 🟢 Frontend Extension Initialized");
|
|
|
|
// Load provider defaults from backend (supports env vars)
|
|
await loadProviderDefaults();
|
|
ensureDoctorSettingsRegistered(app);
|
|
|
|
if (!isDoctorEnabled()) {
|
|
console.log("[ComfyUI-Doctor] Extension disabled via settings");
|
|
return;
|
|
}
|
|
|
|
const migratedLegacyApiKey = migrateLegacyApiKeyFromSettings();
|
|
|
|
const {
|
|
language,
|
|
pollInterval,
|
|
autoOpenOnError,
|
|
enableNotifications,
|
|
} = getDoctorRuntimeSettings();
|
|
|
|
// Create Doctor UI instance with settings
|
|
const doctorUI = new DoctorUI({
|
|
language,
|
|
pollInterval,
|
|
autoOpenOnError,
|
|
enableNotifications,
|
|
api, // Pass ComfyUI API for event subscription
|
|
});
|
|
|
|
// Store reference on app for settings callbacks
|
|
app.Doctor = doctorUI;
|
|
app.Doctor.providerDefaults = PROVIDER_DEFAULTS;
|
|
app.Doctor.getRuntimeApiKey = getRuntimeApiKey;
|
|
app.Doctor.setRuntimeApiKey = setRuntimeApiKey;
|
|
app.Doctor.legacyApiKeyMigrated = migratedLegacyApiKey;
|
|
|
|
// NOTE (UI tooltip): Wait for UI text before sidebar registration to avoid "[Missing]" labels.
|
|
// Do not remove this await unless registerSidebarTab no longer uses getUIText().
|
|
if (doctorUI.uiTextReady) {
|
|
await doctorUI.uiTextReady;
|
|
}
|
|
|
|
// Populate Doctor version for ComfyUI Settings → About badge (best effort).
|
|
// Note: AboutPanel badges are computed and may not be reactive to later updates.
|
|
// Updating immediately after uiTextReady should be early enough for typical usage.
|
|
try {
|
|
const meta = doctorUI?.meta;
|
|
if (meta?.repository) DOCTOR_ABOUT_BADGES[0].url = meta.repository;
|
|
if (meta?.version && meta.version !== "unknown") {
|
|
DOCTOR_ABOUT_BADGES[0].label = `ComfyUI Doctor v${meta.version}`;
|
|
}
|
|
} catch (e) {
|
|
// no-op: keep fallback badge label/url
|
|
}
|
|
|
|
// ========================================
|
|
// Register Sidebar Tab
|
|
// ========================================
|
|
const sidebarRegistrationSource = registerDoctorSidebarTab(app, {
|
|
id: "comfyui-doctor",
|
|
icon: "pi pi-heart-fill", // PrimeVue icon
|
|
title: doctorUI.getUIText('sidebar_doctor_title'),
|
|
tooltip: doctorUI.getUIText('sidebar_doctor_tooltip'),
|
|
type: "custom",
|
|
destroy: () => destroyDoctorSidebarMount(doctorUI),
|
|
render: (container) => {
|
|
destroyDoctorSidebarMount(doctorUI);
|
|
let restoreLayout = () => {};
|
|
let manager = null;
|
|
let doctorRoot = null;
|
|
let ownershipObserver = null;
|
|
let sidebarDestroyed = false;
|
|
// CRITICAL: install this cleanup before the first registry, mount, or layout
|
|
// mutation. Moving it below geometry/render setup makes early exceptions leak
|
|
// Doctor markers or PrimeVue widths; the outer catch must invoke it and rethrow.
|
|
const cleanup = ({ hostTakeover = false } = {}) => {
|
|
if (sidebarDestroyed) return;
|
|
sidebarDestroyed = true;
|
|
let cleanupError = null;
|
|
const runCleanupStep = (step) => {
|
|
try {
|
|
step();
|
|
} catch (error) {
|
|
cleanupError ??= error;
|
|
}
|
|
};
|
|
runCleanupStep(() => ownershipObserver?.disconnect());
|
|
runCleanupStep(() => doctorUI.cancelPendingNodeFocus());
|
|
runCleanupStep(() => manager?.destroy());
|
|
runCleanupStep(() => tabRegistry.clear());
|
|
if (!hostTakeover) {
|
|
runCleanupStep(() => container.replaceChildren());
|
|
}
|
|
runCleanupStep(() => restoreLayout({
|
|
preserveExternalChanges: hostTakeover,
|
|
}));
|
|
doctorRoot = null;
|
|
doctorUI.sidebarTabContainer = null;
|
|
doctorUI.tabManager = null;
|
|
if (doctorUI.sidebarCleanup === cleanup) {
|
|
doctorUI.sidebarCleanup = null;
|
|
}
|
|
if (cleanupError) throw cleanupError;
|
|
};
|
|
doctorUI.sidebarCleanup = cleanup;
|
|
|
|
try {
|
|
tabRegistry.clear();
|
|
container.replaceChildren();
|
|
doctorRoot = document.createElement('div');
|
|
doctorRoot.className = 'doctor-sidebar-content';
|
|
doctorRoot.style.cssText = 'display: flex; flex-direction: column; height: 100%; background: var(--bg-color, #1a1a2e);';
|
|
container.appendChild(doctorRoot);
|
|
restoreLayout = enforceDoctorSidebarMinWidth(container);
|
|
|
|
// CRITICAL: current ExtensionSlot reuses one custom mount when its extension
|
|
// prop changes and only calls destroy on component unmount. Keep Doctor in an
|
|
// owned root and observe its removal; clearing the shared mount during takeover
|
|
// deletes the incoming extension, while unconditional restoration overwrites it.
|
|
if (typeof MutationObserver === 'function') {
|
|
ownershipObserver = new MutationObserver(() => {
|
|
if (
|
|
!sidebarDestroyed
|
|
&& doctorRoot
|
|
&& !container.contains(doctorRoot)
|
|
) {
|
|
cleanup({ hostTakeover: true });
|
|
}
|
|
});
|
|
ownershipObserver.observe(container, { childList: true });
|
|
}
|
|
|
|
// Add styles for the sidebar content if not already added
|
|
if (!document.getElementById('doctor-sidebar-styles')) {
|
|
const style = document.createElement('style');
|
|
style.id = 'doctor-sidebar-styles';
|
|
style.textContent = `
|
|
.doctor-sidebar-content {
|
|
box-sizing: border-box;
|
|
padding: 15px;
|
|
height: 100%;
|
|
min-height: 0;
|
|
min-width: 560px;
|
|
overflow-y: auto;
|
|
background: var(--bg-color, #1a1a2e);
|
|
color: var(--fg-color, #eee);
|
|
font-family: system-ui, -apple-system, sans-serif;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.sidebar-content-container :has(> .doctor-sidebar-content) {
|
|
height: 100%;
|
|
min-height: 0;
|
|
}
|
|
/* CRITICAL: keep this selector and 560px aligned with doctor_sidebar_layout.js.
|
|
The host splitter and content wrapper must share the active Doctor minimum;
|
|
an inner-only rule overflows into a narrower, clipped SplitterPanel. */
|
|
.side-bar-panel:has(.doctor-sidebar-content),
|
|
.p-splitterpanel:has(.doctor-sidebar-content),
|
|
.sidebar-content-container:has(.doctor-sidebar-content) {
|
|
min-width: 560px !important;
|
|
}
|
|
.doctor-sidebar-content h3 {
|
|
margin: 0 0 15px 0;
|
|
font-size: 18px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.doctor-sidebar-content .status-indicator {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
background: #4caf50;
|
|
display: inline-block;
|
|
}
|
|
.doctor-sidebar-content .status-indicator.error {
|
|
background: #ff4444;
|
|
animation: pulse 1s infinite;
|
|
}
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.5; }
|
|
}
|
|
.doctor-sidebar-content .error-card {
|
|
background: rgba(255, 68, 68, 0.1);
|
|
border: 1px solid #ff4444;
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
margin-bottom: 12px;
|
|
}
|
|
.doctor-sidebar-content .error-card .error-type {
|
|
font-weight: bold;
|
|
color: #ff6b6b;
|
|
margin-bottom: 5px;
|
|
}
|
|
.doctor-sidebar-content .error-card .error-message {
|
|
font-size: 13px;
|
|
color: #ddd;
|
|
word-break: break-word;
|
|
}
|
|
.doctor-sidebar-content .error-card .error-time {
|
|
font-size: 11px;
|
|
color: #888;
|
|
margin-top: 8px;
|
|
}
|
|
.doctor-sidebar-content .node-context {
|
|
background: rgba(0,0,0,0.2);
|
|
border-radius: 4px;
|
|
padding: 8px;
|
|
margin-top: 8px;
|
|
font-size: 12px;
|
|
}
|
|
.doctor-sidebar-content .node-context span {
|
|
display: block;
|
|
margin-bottom: 3px;
|
|
}
|
|
.doctor-sidebar-content .action-btn {
|
|
background: #333;
|
|
color: #eee;
|
|
border: 1px solid #555;
|
|
padding: 8px 12px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
margin-top: 8px;
|
|
width: 100%;
|
|
text-align: center;
|
|
transition: background 0.2s;
|
|
}
|
|
.doctor-sidebar-content .action-btn:hover {
|
|
background: #444;
|
|
}
|
|
.doctor-sidebar-content .action-btn.primary {
|
|
background: #2563eb;
|
|
border-color: #3b82f6;
|
|
}
|
|
.doctor-sidebar-content .action-btn.primary:hover {
|
|
background: #1d4ed8;
|
|
}
|
|
.doctor-sidebar-content .no-errors {
|
|
text-align: center;
|
|
padding: 40px 20px;
|
|
color: #888;
|
|
}
|
|
.doctor-sidebar-content .no-errors .icon {
|
|
font-size: 48px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.doctor-sidebar-content .ai-response {
|
|
background: rgba(37, 99, 235, 0.1);
|
|
border: 1px solid #2563eb;
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
margin-top: 12px;
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
}
|
|
.doctor-sidebar-content .ai-response h4 {
|
|
margin: 0 0 8px 0;
|
|
color: #60a5fa;
|
|
}
|
|
/* F4: Statistics Dashboard Styles */
|
|
.doctor-sidebar-content .stats-panel {
|
|
background: rgba(0,0,0,0.2);
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
margin: 10px 0;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
flex-shrink: 0;
|
|
}
|
|
.doctor-sidebar-content .stats-panel summary {
|
|
cursor: pointer;
|
|
font-weight: bold;
|
|
font-size: 13px;
|
|
color: #ddd;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.doctor-sidebar-content .stats-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 8px;
|
|
margin-top: 10px;
|
|
}
|
|
.doctor-sidebar-content .stat-card {
|
|
background: rgba(255,255,255,0.05);
|
|
border: 1px solid #444;
|
|
border-radius: 6px;
|
|
padding: 10px;
|
|
text-align: center;
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
}
|
|
.doctor-sidebar-content .stat-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
|
}
|
|
.doctor-sidebar-content .stat-card .stat-value {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
color: #f44;
|
|
}
|
|
.doctor-sidebar-content .stat-card .stat-label {
|
|
font-size: 11px;
|
|
color: #888;
|
|
margin-top: 4px;
|
|
}
|
|
.doctor-sidebar-content .top-patterns {
|
|
margin-top: 10px;
|
|
}
|
|
.doctor-sidebar-content .top-patterns h5 {
|
|
margin: 0 0 8px 0;
|
|
font-size: 12px;
|
|
color: #aaa;
|
|
}
|
|
.doctor-sidebar-content .pattern-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 6px 8px;
|
|
background: rgba(255,68,68,0.1);
|
|
border-radius: 4px;
|
|
margin-bottom: 4px;
|
|
font-size: 12px;
|
|
}
|
|
.doctor-sidebar-content .pattern-item .pattern-name {
|
|
color: #ff6b6b;
|
|
flex: 1;
|
|
}
|
|
.doctor-sidebar-content .pattern-item .pattern-count {
|
|
background: #f44;
|
|
color: white;
|
|
padding: 2px 6px;
|
|
border-radius: 10px;
|
|
font-size: 10px;
|
|
font-weight: bold;
|
|
}
|
|
.doctor-sidebar-content .category-bar {
|
|
margin-bottom: 6px;
|
|
}
|
|
.doctor-sidebar-content .category-bar .bar-label {
|
|
font-size: 11px;
|
|
color: #aaa;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 2px;
|
|
}
|
|
.doctor-sidebar-content .category-bar .bar-track {
|
|
height: 6px;
|
|
background: #333;
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
}
|
|
.doctor-sidebar-content .category-bar .bar-fill {
|
|
height: 100%;
|
|
border-radius: 3px;
|
|
transition: width 0.3s ease;
|
|
}
|
|
.doctor-sidebar-content .stats-empty {
|
|
text-align: center;
|
|
padding: 20px;
|
|
color: #666;
|
|
font-size: 12px;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
|
|
// F13: Tab System Styles
|
|
const tabStyle = document.createElement('style');
|
|
tabStyle.textContent = `
|
|
.doctor-tab-bar {
|
|
display: flex;
|
|
border-bottom: 1px solid var(--border-color, #444);
|
|
background: rgba(0,0,0,0.2);
|
|
}
|
|
.doctor-tab-button {
|
|
flex: 1;
|
|
text-align: center;
|
|
padding: 10px;
|
|
cursor: pointer;
|
|
color: #888;
|
|
border-bottom: 2px solid transparent;
|
|
transition: all 0.2s;
|
|
}
|
|
.doctor-tab-button:hover {
|
|
background: rgba(255,255,255,0.05);
|
|
color: #ddd;
|
|
}
|
|
.doctor-tab-button.active {
|
|
color: #4caf50;
|
|
border-bottom-color: #4caf50;
|
|
background: rgba(76, 175, 80, 0.1);
|
|
}
|
|
.doctor-tab-pane {
|
|
height: 100%;
|
|
display: none;
|
|
overflow: hidden;
|
|
/* Ensure flex children can fill height */
|
|
box-sizing: border-box;
|
|
}
|
|
.doctor-tab-pane[style*="display: block"] {
|
|
display: flex !important;
|
|
flex-direction: column;
|
|
}
|
|
`;
|
|
document.head.appendChild(tabStyle);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// LEFT SIDEBAR PANEL - Doctor Sidebar (Main UI)
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// LOCATION: Left side of ComfyUI (accessed via 🏥 Doctor icon)
|
|
// DO NOT CONFUSE WITH: Right error panel (doctor_ui.js)
|
|
//
|
|
// STRUCTURE:
|
|
// 1. Header (🏥 Doctor + ⚙️ Settings toggle)
|
|
// 2. Settings Panel (collapsible)
|
|
// 3. Error Context Card (shows suggestion when error occurs)
|
|
// 4. Messages Area (AI chat interface)
|
|
// 5. Input Area (sticky at bottom)
|
|
//
|
|
// IMPORTANT NOTES:
|
|
// - Error Context Card displays CONCISE SUGGESTION ONLY
|
|
// - Suggestion extraction logic in doctor_ui.js:updateSidebarTab()
|
|
// - Full error details sent to LLM, only actionable advice shown here
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
// Create sidebar content - TAB BASED STRUCTURE (F13)
|
|
// 1. Header
|
|
const header = document.createElement('div');
|
|
header.style.cssText = 'padding: 12px 15px; border-bottom: 1px solid var(--border-color, #444); display: flex; align-items: center; justify-content: space-between; flex-shrink: 0;';
|
|
header.innerHTML = `
|
|
<div style="display: flex; align-items: center; gap: 8px;">
|
|
<span class="status-indicator" id="doctor-tab-status" style="width: 10px; height: 10px; border-radius: 50%; background: #4caf50; display: inline-block;"></span>
|
|
<span style="font-size: 16px; font-weight: bold; color: var(--fg-color, #eee);">🏥 ${app.Doctor.getUIText('sidebar_doctor_title')}</span>
|
|
</div>
|
|
<div style="display: flex; align-items: center; gap: 10px;">
|
|
<span id="doctor-sidebar-version" style="font-size: 13px; color: #c8ccd4;">v?</span>
|
|
<a id="doctor-sidebar-github-link" href="https://github.com/rookiestar28/ComfyUI-Doctor" target="_blank" rel="noopener noreferrer" style="display: inline-block; padding: 4px 10px; border: 1px solid var(--border-color, #4a4f5a); border-radius: 999px; font-size: 12px; font-weight: 600; text-decoration: none; color: var(--fg-color, #f1f5f9); background: rgba(255,255,255,0.03);">View on GitHub</a>
|
|
</div>
|
|
`;
|
|
doctorRoot.appendChild(header);
|
|
|
|
const applySidebarMeta = (meta) => {
|
|
const versionEl = header.querySelector('#doctor-sidebar-version');
|
|
const githubEl = header.querySelector('#doctor-sidebar-github-link');
|
|
if (!versionEl || !githubEl) return;
|
|
|
|
const version = (typeof meta?.version === 'string' && meta.version.trim() && meta.version.trim() !== 'unknown')
|
|
? meta.version.trim().replace(/^v/i, '')
|
|
: null;
|
|
const repository = (typeof meta?.repository === 'string' && meta.repository.trim())
|
|
? meta.repository.trim()
|
|
: 'https://github.com/rookiestar28/ComfyUI-Doctor';
|
|
|
|
versionEl.textContent = version ? `v${version}` : 'v?';
|
|
githubEl.href = repository;
|
|
};
|
|
|
|
const loadSidebarMeta = async () => {
|
|
applySidebarMeta(app.Doctor?.meta);
|
|
if (app.Doctor?.meta?.version && app.Doctor.meta.version !== 'unknown') return;
|
|
try {
|
|
const currentLang = getDoctorSetting('Doctor.General.Language', DOCTOR_DEFAULTS.LANGUAGE);
|
|
const endpoint = `/doctor/ui_text?lang=${encodeURIComponent(currentLang)}`;
|
|
const response = (typeof window !== 'undefined' && window.api && typeof window.api.fetchApi === 'function')
|
|
? await window.api.fetchApi(endpoint)
|
|
: await fetch(endpoint);
|
|
if (!response.ok) return;
|
|
const data = await response.json();
|
|
if (!data?.meta) return;
|
|
app.Doctor.meta = data.meta;
|
|
applySidebarMeta(data.meta);
|
|
} catch (_) {
|
|
// Keep best-effort fallback in header.
|
|
}
|
|
};
|
|
void loadSidebarMeta();
|
|
|
|
// 2. Tab Bar
|
|
const tabBar = document.createElement('div');
|
|
tabBar.id = 'doctor-tab-bar';
|
|
tabBar.className = 'doctor-tab-bar';
|
|
doctorRoot.appendChild(tabBar);
|
|
|
|
// 3. Tab Content
|
|
const content = document.createElement('div');
|
|
content.id = 'doctor-tab-content';
|
|
// ⚠️ CRITICAL: min-height: 0 required for nested flex scrolling
|
|
content.style.cssText = 'flex: 1 1 0; overflow: hidden; position: relative; min-height: 0;';
|
|
doctorRoot.appendChild(content);
|
|
|
|
// Register Tabs
|
|
const getTxt = (k, f) => app.Doctor.getUIText(k) || f;
|
|
|
|
try {
|
|
console.log("[ComfyUI-Doctor] Registering Chat tab...");
|
|
tabRegistry.register({
|
|
id: 'chat',
|
|
icon: '💬',
|
|
label: getTxt('tab_chat', 'Chat'),
|
|
order: 10,
|
|
render: ChatTab.render,
|
|
onActivate: ChatTab.onActivate
|
|
});
|
|
|
|
console.log("[ComfyUI-Doctor] Registering Stats tab...");
|
|
tabRegistry.register({
|
|
id: 'stats',
|
|
icon: '📊',
|
|
label: getTxt('tab_stats', 'Stats'),
|
|
order: 20,
|
|
render: StatsTab.render,
|
|
onActivate: StatsTab.onActivate
|
|
});
|
|
|
|
console.log("[ComfyUI-Doctor] Registering Settings tab...");
|
|
tabRegistry.register({
|
|
id: 'settings',
|
|
icon: '⚙️',
|
|
label: getTxt('tab_settings', 'Settings'),
|
|
order: 30,
|
|
render: SettingsTab.render
|
|
});
|
|
|
|
console.log("[ComfyUI-Doctor] Initializing TabManager...");
|
|
// Init Manager - pass DOM elements directly (not IDs)
|
|
// ⚠️ CRITICAL: document.getElementById() fails in ComfyUI's Vue context
|
|
manager = new TabManager(tabRegistry, content, tabBar);
|
|
manager.init();
|
|
console.log("[ComfyUI-Doctor] ✅ TabManager initialized successfully");
|
|
|
|
// Store references
|
|
doctorUI.sidebarTabContainer = content;
|
|
doctorUI.tabManager = manager;
|
|
// Update status dot if error exists
|
|
if (doctorUI.lastErrorData) {
|
|
const statusDot = header.querySelector('#doctor-tab-status');
|
|
}
|
|
} catch (tabError) {
|
|
console.error("[ComfyUI-Doctor] ❌ Tab initialization failed:", tabError);
|
|
// Show error message in sidebar content
|
|
content.innerHTML = `
|
|
<div style="padding: 20px; color: #ff5555;">
|
|
<h4>⚠️ Tab Initialization Error</h4>
|
|
<p style="font-size: 12px; white-space: pre-wrap;">${tabError.message}</p>
|
|
<p style="font-size: 10px; color: #888;">Check browser console for details</p>
|
|
</div>
|
|
`;
|
|
}
|
|
} catch (mountError) {
|
|
destroyDoctorSidebarMount(doctorUI);
|
|
throw mountError;
|
|
}
|
|
}
|
|
});
|
|
if (sidebarRegistrationSource) {
|
|
console.log(`[ComfyUI-Doctor] ✅ Sidebar tab registered via ${sidebarRegistrationSource}`);
|
|
} else {
|
|
console.log("[ComfyUI-Doctor] Sidebar API not available, using legacy menu button");
|
|
}
|
|
|
|
// Sync initial language with backend
|
|
try {
|
|
await DoctorAPI.setLanguage(language);
|
|
} catch (e) {
|
|
console.error("[ComfyUI-Doctor] Failed to sync initial language:", e);
|
|
}
|
|
|
|
console.log("[ComfyUI-Doctor] Settings initialized successfully");
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// R5: ERROR BOUNDARIES INITIALIZATION
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
if (isDoctorErrorBoundariesEnabled()) {
|
|
// 1. Inject Error Boundary CSS
|
|
if (!document.getElementById('doctor-error-boundary-styles')) {
|
|
const link = document.createElement('link');
|
|
link.id = 'doctor-error-boundary-styles';
|
|
link.rel = 'stylesheet';
|
|
link.href = new URL('./error_boundary.css', import.meta.url).href;
|
|
document.head.appendChild(link);
|
|
}
|
|
|
|
// 2. Install global error handlers
|
|
installGlobalErrorHandlers();
|
|
|
|
console.log('[ComfyUI-Doctor] ✅ Error boundaries active');
|
|
} else {
|
|
console.log('[ComfyUI-Doctor] Error boundaries disabled by user setting');
|
|
}
|
|
}
|
|
});
|