const STATE_ORDER = ["running", "success", "empty", "error", "exported"]; const QUERY_ORDER = ["success", "empty", "error", "export"]; const workspaceFixtures = { "pg-prod-readonly": { defaultSelection: "public.orders", catalog: [ { name: "public", items: [ { name: "orders", kind: "Table", descriptor: "table · 12 columns", columns: 12, primaryKey: "id", search: "orders public table created_at status customer_id", }, { name: "customers", kind: "Table", descriptor: "table · 9 columns", columns: 9, primaryKey: "id", search: "customers public table email country", }, { name: "payments", kind: "Table", descriptor: "table · 7 columns", columns: 7, primaryKey: "id", search: "payments public table status amount", }, ], }, { name: "analytics", items: [ { name: "daily_orders", kind: "View", descriptor: "view · materialized summary", columns: 6, primaryKey: "—", search: "daily_orders analytics view", }, { name: "top_customers", kind: "View", descriptor: "view · ranking output", columns: 5, primaryKey: "—", search: "top_customers analytics view", }, ], }, { name: "ops", items: [ { name: "job_runs", kind: "Table", descriptor: "table · execution log", columns: 8, primaryKey: "id", search: "job_runs ops table status finished_at", }, { name: "incident_log", kind: "Table", descriptor: "table · support evidence", columns: 10, primaryKey: "id", search: "incident_log ops table created_at severity", }, ], }, ], queries: { success: { label: "orders audit.sql", selection: "public.orders", sql: `select id, customer_id, status, created_at from public.orders where created_at > now() - interval '7 days' order by created_at desc limit 50;`, settledState: "success", columns: ["id", "customer_id", "status", "created_at"], successRows: [ ["8ec1...", "7cb3...", "paid", "2026-03-25 13:55:01 UTC"], ["80f8...", "5d10...", "processing", "2026-03-25 13:44:15 UTC"], ["5aa2...", "9ab1...", "failed", "2026-03-25 13:12:48 UTC"], ["44fe...", "7cb3...", "paid", "2026-03-25 12:58:22 UTC"], ], successRowCount: 50, successDuration: "182 ms", emptyDuration: "95 ms", errorDuration: "21 ms", errorMessage: "Syntax error near `form`", exportFormat: "csv", exportPath: "/tmp/orders-last-7-days.csv", }, empty: { label: "empty result.sql", selection: "public.orders", sql: `select id, status, created_at from public.orders where created_at > now() - interval '5 minutes' and status = 'refunded' order by created_at desc;`, settledState: "empty", columns: ["id", "status", "created_at"], successRows: [], successRowCount: 0, successDuration: "95 ms", emptyDuration: "95 ms", errorDuration: "18 ms", errorMessage: "No result envelope available", exportFormat: "csv", exportPath: "/tmp/orders-empty.csv", }, error: { label: "bad syntax.sql", selection: "public.orders", sql: `select id, status form public.orders where status = 'paid';`, settledState: "error", columns: ["id", "status"], successRows: [], successRowCount: 0, successDuration: "—", emptyDuration: "—", errorDuration: "21 ms", errorMessage: "Syntax error near `form`", exportFormat: "csv", exportPath: "/tmp/orders-error.csv", }, export: { label: "export review.sql", selection: "public.orders", sql: `select id, customer_id, status, created_at from public.orders where created_at >= date_trunc('day', now()) order by created_at desc;`, settledState: "exported", columns: ["id", "customer_id", "status", "created_at"], successRows: [ ["8ec1...", "7cb3...", "paid", "2026-03-25 13:55:01 UTC"], ["80f8...", "5d10...", "processing", "2026-03-25 13:44:15 UTC"], ["5aa2...", "9ab1...", "failed", "2026-03-25 13:12:48 UTC"], ["44fe...", "7cb3...", "paid", "2026-03-25 12:58:22 UTC"], ], successRowCount: 50, successDuration: "182 ms", emptyDuration: "95 ms", errorDuration: "21 ms", errorMessage: "Export source query failed", exportFormat: "csv", exportPath: "/tmp/orders-last-7-days.csv", }, }, }, "mysql-staging": { defaultSelection: "qa_demo.orders", catalog: [ { name: "qa_demo", items: [ { name: "orders", kind: "Table", descriptor: "table · 11 columns", columns: 11, primaryKey: "id", search: "orders qa_demo table created_at status region", }, { name: "order_audit", kind: "Table", descriptor: "table · change history", columns: 8, primaryKey: "audit_id", search: "order_audit qa_demo table before after actor", }, { name: "refunds", kind: "Table", descriptor: "table · finance review", columns: 6, primaryKey: "id", search: "refunds qa_demo table amount processed_at", }, ], }, { name: "reporting", items: [ { name: "daily_orders", kind: "View", descriptor: "view · daily summary", columns: 5, primaryKey: "—", search: "daily_orders reporting view", }, { name: "refund_watchlist", kind: "View", descriptor: "view · issue triage", columns: 4, primaryKey: "—", search: "refund_watchlist reporting view", }, ], }, { name: "ops", items: [ { name: "job_runs", kind: "Table", descriptor: "table · pipeline status", columns: 7, primaryKey: "id", search: "job_runs ops table status finished_at", }, { name: "dead_letter_queue", kind: "Table", descriptor: "table · retry backlog", columns: 5, primaryKey: "id", search: "dead_letter_queue ops table retry backlog", }, ], }, ], queries: { success: { label: "staging diff.sql", selection: "qa_demo.orders", sql: `select id, region, status, updated_at from qa_demo.orders where updated_at >= now() - interval 1 day order by updated_at desc limit 50;`, settledState: "success", columns: ["id", "region", "status", "updated_at"], successRows: [ ["1024", "us-east", "paid", "2026-03-25 13:55:01 UTC"], ["1021", "eu-west", "processing", "2026-03-25 13:44:15 UTC"], ["1015", "ap-south", "failed", "2026-03-25 13:12:48 UTC"], ["1007", "us-west", "paid", "2026-03-25 12:58:22 UTC"], ], successRowCount: 37, successDuration: "164 ms", emptyDuration: "88 ms", errorDuration: "17 ms", errorMessage: "Unknown column `statsu` in `field list`", exportFormat: "json", exportPath: "/tmp/mysql-staging-diff.json", }, empty: { label: "no drift.sql", selection: "reporting.daily_orders", sql: `select order_date, failed_count from reporting.daily_orders where order_date = current_date() and failed_count > 50;`, settledState: "empty", columns: ["order_date", "failed_count"], successRows: [], successRowCount: 0, successDuration: "88 ms", emptyDuration: "88 ms", errorDuration: "16 ms", errorMessage: "No result envelope available", exportFormat: "json", exportPath: "/tmp/mysql-no-drift.json", }, error: { label: "bad filter.sql", selection: "qa_demo.orders", sql: `select id, statsu from qa_demo.orders where status = 'paid';`, settledState: "error", columns: ["id", "status"], successRows: [], successRowCount: 0, successDuration: "—", emptyDuration: "—", errorDuration: "17 ms", errorMessage: "Unknown column `statsu` in `field list`", exportFormat: "json", exportPath: "/tmp/mysql-error.json", }, export: { label: "handoff export.sql", selection: "qa_demo.order_audit", sql: `select audit_id, order_id, actor, changed_at from qa_demo.order_audit order by changed_at desc limit 100;`, settledState: "exported", columns: ["audit_id", "order_id", "actor", "changed_at"], successRows: [ ["5008", "1024", "api", "2026-03-25 13:55:01 UTC"], ["5007", "1021", "worker", "2026-03-25 13:44:15 UTC"], ["5006", "1015", "ops", "2026-03-25 13:12:48 UTC"], ["5005", "1007", "api", "2026-03-25 12:58:22 UTC"], ], successRowCount: 100, successDuration: "201 ms", emptyDuration: "112 ms", errorDuration: "22 ms", errorMessage: "Export source query failed", exportFormat: "json", exportPath: "/tmp/mysql-audit-export.json", }, }, }, "sqlite-local": { defaultSelection: "main.audit_events", catalog: [ { name: "main", items: [ { name: "audit_events", kind: "Table", descriptor: "table · local evidence log", columns: 7, primaryKey: "id", search: "audit_events main table action actor event_time", }, { name: "attachments", kind: "Table", descriptor: "table · exported evidence", columns: 5, primaryKey: "id", search: "attachments main table file_path created_at", }, { name: "query_history", kind: "Table", descriptor: "table · recent drafts", columns: 4, primaryKey: "id", search: "query_history main table sql_label saved_at", }, ], }, { name: "temp", items: [ { name: "staged_rows", kind: "Table", descriptor: "table · transient compare set", columns: 4, primaryKey: "—", search: "staged_rows temp table transient compare", }, ], }, ], queries: { success: { label: "evidence review.sql", selection: "main.audit_events", sql: `select id, actor, action, event_time from main.audit_events order by event_time desc limit 50;`, settledState: "success", columns: ["id", "actor", "action", "event_time"], successRows: [ ["91", "qa-bot", "attach_export", "2026-03-25 13:55:01 UTC"], ["90", "frontend", "query_ok", "2026-03-25 13:44:15 UTC"], ["89", "backend", "inspect_table", "2026-03-25 13:12:48 UTC"], ["88", "qa-bot", "open_file", "2026-03-25 12:58:22 UTC"], ], successRowCount: 24, successDuration: "14 ms", emptyDuration: "8 ms", errorDuration: "4 ms", errorMessage: "SQLite error: no such column: acton", exportFormat: "csv", exportPath: "/tmp/sqlite-audit-events.csv", }, empty: { label: "no incidents.sql", selection: "main.audit_events", sql: `select id, action, event_time from main.audit_events where action = 'incident_opened' and event_time >= datetime('now', '-5 minutes');`, settledState: "empty", columns: ["id", "action", "event_time"], successRows: [], successRowCount: 0, successDuration: "8 ms", emptyDuration: "8 ms", errorDuration: "4 ms", errorMessage: "No result envelope available", exportFormat: "csv", exportPath: "/tmp/sqlite-empty.csv", }, error: { label: "bad column.sql", selection: "main.audit_events", sql: `select id, acton from main.audit_events where actor = 'qa-bot';`, settledState: "error", columns: ["id", "action"], successRows: [], successRowCount: 0, successDuration: "—", emptyDuration: "—", errorDuration: "4 ms", errorMessage: "SQLite error: no such column: acton", exportFormat: "csv", exportPath: "/tmp/sqlite-error.csv", }, export: { label: "attachment export.sql", selection: "main.attachments", sql: `select id, file_path, created_at from main.attachments order by created_at desc limit 25;`, settledState: "exported", columns: ["id", "file_path", "created_at"], successRows: [ ["17", "/tmp/evidence-1.csv", "2026-03-25 13:55:01 UTC"], ["16", "/tmp/evidence-2.csv", "2026-03-25 13:44:15 UTC"], ["15", "/tmp/evidence-3.csv", "2026-03-25 13:12:48 UTC"], ["14", "/tmp/evidence-4.csv", "2026-03-25 12:58:22 UTC"], ], successRowCount: 25, successDuration: "11 ms", emptyDuration: "6 ms", errorDuration: "4 ms", errorMessage: "Export source query failed", exportFormat: "csv", exportPath: "/tmp/sqlite-attachments.csv", }, }, }, }; const connectionCards = document.querySelectorAll(".connection-card"); const bottomTabs = document.querySelectorAll(".bottom-tab"); const tabContents = document.querySelectorAll(".tab-content"); const activeConnection = document.getElementById("active-connection"); const driverPill = document.getElementById("driver-pill"); const detailTarget = document.getElementById("detail-target"); const detailMode = document.getElementById("detail-mode"); const detailExport = document.getElementById("detail-export"); const detailTheme = document.getElementById("detail-theme"); const contextTarget = document.getElementById("context-target"); const contextMode = document.getElementById("context-mode"); const contextSelection = document.getElementById("context-selection"); const contextValidated = document.getElementById("context-validated"); const inspectorObject = document.getElementById("inspector-object"); const selectionKind = document.getElementById("selection-kind"); const selectionPrimaryKey = document.getElementById("selection-primary-key"); const selectionColumns = document.getElementById("selection-columns"); const summaryWorkspace = document.getElementById("summary-workspace"); const summaryWorkspaceCopy = document.getElementById("summary-workspace-copy"); const summarySelection = document.getElementById("summary-selection"); const summarySelectionCopy = document.getElementById("summary-selection-copy"); const summaryRun = document.getElementById("summary-run"); const summaryRunCopy = document.getElementById("summary-run-copy"); const summaryExport = document.getElementById("summary-export"); const summaryExportCopy = document.getElementById("summary-export-copy"); const executionBanner = document.getElementById("execution-banner"); const executionTitle = document.getElementById("execution-title"); const executionCopy = document.getElementById("execution-copy"); const durationValue = document.getElementById("duration-value"); const rowCount = document.getElementById("row-count"); const stateLabel = document.getElementById("state-label"); const editorCode = document.getElementById("editor-code"); const editorGutter = document.getElementById("editor-gutter"); const toolbarTabsContainer = document.getElementById("toolbar-tabs"); const schemaTree = document.getElementById("schema-tree"); const resultsHead = document.getElementById("results-head"); const resultsBody = document.getElementById("results-body"); const historyList = document.getElementById("history-list"); const resultSummary = document.getElementById("result-summary"); const resultCopy = document.getElementById("result-copy"); const exportTitle = document.getElementById("export-title"); const exportCopy = document.getElementById("export-copy"); const problemTitle = document.getElementById("problem-title"); const problemCopy = document.getElementById("problem-copy"); const contractTitle = document.getElementById("contract-title"); const contractCopy = document.getElementById("contract-copy"); const contractJson = document.getElementById("contract-json"); const statusMessage = document.getElementById("status-message"); const themeToggle = document.getElementById("theme-toggle"); const schemaSearch = document.getElementById("schema-search"); const clearSearch = document.getElementById("clear-search"); const runQueryButton = document.getElementById("run-query"); const cycleStateButton = document.getElementById("cycle-state"); const exportCurrentButton = document.getElementById("export-current"); const exportCsvButton = document.getElementById("export-csv"); const exportJsonButton = document.getElementById("export-json"); const commandButton = document.getElementById("command-button"); const commandPalette = document.getElementById("command-palette"); const commandBackdrop = document.getElementById("command-backdrop"); const commandCloseButton = document.getElementById("command-close"); const commandStateButtons = document.querySelectorAll("[data-command-state]"); const commandConnectionButtons = document.querySelectorAll("[data-command-connection]"); const commandActionButtons = document.querySelectorAll("[data-command-action]"); const appState = { connection: "pg-prod-readonly", query: "success", selection: "public.orders", activeState: "success", activeTab: "results", runTimer: null, }; function getActiveConnectionCard() { return document.querySelector(`.connection-card[data-connection="${appState.connection}"]`); } function getActiveConnectionData() { return getActiveConnectionCard().dataset; } function getWorkspaceFixture() { return workspaceFixtures[appState.connection]; } function getQueryPreset(queryKey = appState.query) { return getWorkspaceFixture().queries[queryKey]; } function getSelectionMeta(selection = appState.selection) { const fixture = getWorkspaceFixture(); for (const group of fixture.catalog) { for (const item of group.items) { if (`${group.name}.${item.name}` === selection) { return { schema: group.name, selection, ...item, }; } } } return null; } function getTreeItems() { return schemaTree.querySelectorAll(".tree-item"); } function getSchemaGroups() { return schemaTree.querySelectorAll(".tree-group"); } function setActiveTab(tabName) { appState.activeTab = tabName; bottomTabs.forEach((tab) => { tab.classList.toggle("is-active", tab.dataset.tab === tabName); }); tabContents.forEach((content) => { content.classList.toggle("is-active", content.dataset.content === tabName); }); } function renderToolbarTabs() { const fixture = getWorkspaceFixture(); toolbarTabsContainer.innerHTML = QUERY_ORDER.map((queryKey) => { const preset = fixture.queries[queryKey]; const isActive = queryKey === appState.query ? " is-active" : ""; return ``; }).join(""); } function renderSchemaTree() { const fixture = getWorkspaceFixture(); schemaTree.innerHTML = fixture.catalog .map((group) => { const groupSearch = [group.name, ...group.items.map((item) => item.search)].join(" ").toLowerCase(); const items = group.items .map((item) => { const selection = `${group.name}.${item.name}`; const isSelected = selection === appState.selection ? " is-selected" : ""; return ` `; }) .join(""); return `
${group.name} ${items}
`; }) .join(""); applySchemaFilter(); } function renderResults(columns, rows, tableMessage) { const activeColumns = columns.length > 0 ? columns : ["message"]; resultsHead.innerHTML = activeColumns.map((column) => `${column}`).join(""); if (tableMessage) { resultsBody.innerHTML = `${tableMessage}`; return; } resultsBody.innerHTML = rows .map( (row) => ` ${row.map((cell) => `${cell}`).join("")} `, ) .join(""); } function renderHistory(items) { historyList.innerHTML = items .map( ([title, meta]) => `
${title} ${meta}
`, ) .join(""); } function renderEditor(sql) { editorCode.textContent = sql; const lineCount = sql.split("\n").length; editorGutter.innerHTML = Array.from({ length: lineCount }, (_, index) => `${index + 1}`).join(""); } function normalizeDriver(driver) { const driverMap = { PostgreSQL: "postgres", MySQL: "mysql", SQLite: "sqlite", }; return driverMap[driver] || driver.toLowerCase(); } function buildHistoryMeta(queryKey, state = getQueryPreset(queryKey).settledState) { const query = getQueryPreset(queryKey); const connection = getActiveConnectionData(); if (state === "running") { return `Running · executing against ${connection.connection}`; } if (state === "success") { return `Success · ${query.successDuration} · ${query.successRowCount} rows`; } if (state === "empty") { return `Success · ${query.emptyDuration} · 0 rows`; } if (state === "error") { return `Error · ${query.errorMessage}`; } return `Exported · ${query.exportFormat.toUpperCase()} · ${query.successRowCount} rows`; } function buildHistory() { const orderedKeys = [appState.query, ...QUERY_ORDER.filter((queryKey) => queryKey !== appState.query)]; return orderedKeys.map((queryKey) => { const preset = getQueryPreset(queryKey); const state = queryKey === appState.query ? appState.activeState : preset.settledState; return [preset.label, buildHistoryMeta(queryKey, state)]; }); } function buildContractSnapshot() { const connection = getActiveConnectionData(); const query = getQueryPreset(); const selection = getSelectionMeta(); const baseTarget = { profile_name: connection.connection, driver: normalizeDriver(connection.driver), }; if (appState.activeState === "running") { return { title: "UI transient execution preview", copy: "`running` 仅是界面暂态;共享契约仍等待 `QueryResponse` 或 `AppError` 返回。", snapshot: { ui_state: "running", pending_operation: "query", target: baseTarget, source: { kind: "inline", label: query.label, }, context_object: selection?.selection || appState.selection, note: "Transient frontend state before the settled backend envelope is available.", }, }; } if (appState.activeState === "error") { return { title: "Query error envelope preview", copy: "失败态映射到 `status: error`,重点看 `error.kind` 与 `error.message`,而不是把失败伪装成空结果。", snapshot: { status: "error", operation: "query", error: { operation: "query", kind: "query", message: query.errorMessage, }, context_object: selection?.selection || appState.selection, }, }; } if (appState.activeState === "exported") { return { title: "Export success envelope preview", copy: "导出完成后切换到 `operation: export`;QA 可重点核对 `export.output_path` 与 `row_count`。", snapshot: { status: "success", operation: "export", data: { target: baseTarget, source: { kind: "inline", label: query.label, }, context_object: selection?.selection || appState.selection, export: { format: query.exportFormat, output_path: query.exportPath, overwrite: false, }, row_count: query.successRowCount, }, }, }; } if (appState.activeState === "empty") { return { title: "Query empty-result envelope preview", copy: "空结果仍然属于 `status: success`;用 `data.result.row_count: 0` 与问题态区分。", snapshot: { status: "success", operation: "query", data: { target: baseTarget, source: { kind: "inline", label: query.label, }, context_object: selection?.selection || appState.selection, result: { columns: query.columns, rows: [], row_count: 0, }, }, }, }; } return { title: "Query success envelope preview", copy: "成功态对齐 `operation: query`,重点确认 `data.result.columns`、`rows` 与 `row_count` 能驱动工作区渲染。", snapshot: { status: "success", operation: "query", data: { target: baseTarget, source: { kind: "inline", label: query.label, }, context_object: selection?.selection || appState.selection, result: { columns: query.columns, rows: query.successRows.slice(0, 2), row_count: query.successRowCount, }, }, }, }; } function buildStatePreset() { const query = getQueryPreset(); const connection = getActiveConnectionData(); const activeLabel = query.label; if (appState.activeState === "running") { return { title: "Query is running", copy: `${activeLabel} stays visible in-place while the workspace keeps target, selection, and export context readable.`, duration: "—", rows: "—", label: "Running", resultSummary: `Execution in progress · ${activeLabel}`, resultCopy: "执行中时不隐藏结果区,而是保留当前 SQL、目标连接和对象浏览上下文。", exportTitle: "Export is unavailable while the query is running", exportCopy: "等待当前执行完成后,再决定是否导出当前结果集。", problemTitle: "No blocking problem yet", problemCopy: "如果执行时间异常增长,未来正式 GUI 应提供 cancel / timeout 反馈。", tableMessage: `${activeLabel} is running… results will appear here when the execution finishes.`, rowsData: [], runSummary: "Running · waiting for rows", runCopy: `${connection.connection} 正在执行当前草稿。`, lastExport: "Waiting for current run", statusMessage: `Running preview active for ${activeLabel}.`, columns: query.columns, }; } if (appState.activeState === "success") { return { title: "Last run succeeded", copy: `${query.successRowCount} rows returned in ${query.successDuration}. Export is available.`, duration: query.successDuration, rows: String(query.successRowCount), label: "Success", resultSummary: `${query.successRowCount} rows · ${activeLabel}`, resultCopy: "结果区保留紧邻执行反馈的上下文,不让表格单独承担状态信息。", exportTitle: "Ready to export the current result set", exportCopy: `\`${query.exportPath}\` is available as the default target.`, problemTitle: "No blocking problem", problemCopy: "当前成功态下,问题区保留错误说明预留位,但不制造多余警告。", rowsData: query.successRows, runSummary: `Success · ${query.successRowCount} rows`, runCopy: `${query.successDuration},结果可导出。`, lastExport: "Not started", statusMessage: `Success preview active for ${activeLabel}.`, columns: query.columns, }; } if (appState.activeState === "empty") { return { title: "Query returned no rows", copy: `Execution still succeeded in ${query.emptyDuration}. Empty result should not look like a failure.`, duration: query.emptyDuration, rows: "0", label: "Empty", resultSummary: `0 rows · ${activeLabel}`, resultCopy: "空结果是可解释的完成态,仍保留 SQL、耗时和连接上下文。", exportTitle: "No rows to export from the current result", exportCopy: "保持导出入口可见,但要清楚说明当前不会产出文件。", problemTitle: "No blocking problem", problemCopy: "如需下一步,可建议用户放宽过滤条件或切换对象,而不是显示错误。", tableMessage: "No rows in the current result set.", rowsData: [], runSummary: "Success · 0 rows", runCopy: `${query.emptyDuration},空结果但执行成功。`, lastExport: "Skipped · no rows", statusMessage: `Empty-result preview active for ${activeLabel}.`, columns: query.columns, }; } if (appState.activeState === "error") { return { title: "Last run failed", copy: `${query.errorMessage}. The error summary should stay visible in the workspace.`, duration: query.errorDuration, rows: "0", label: "Error", resultSummary: `No table output · ${activeLabel}`, resultCopy: "错误信息应与 SQL 片段、目标连接和恢复动作放在同一工作区上下文中。", exportTitle: "Export disabled until the query succeeds", exportCopy: "失败态下导出区不隐藏,而是明确说明为何当前不可导出。", problemTitle: query.errorMessage, problemCopy: "建议动作:回到编辑区修正关键字、重试执行,或切换到最近一次成功结果。", tableMessage: "Execution failed before a result set was returned.", rowsData: [], runSummary: "Error · query failed", runCopy: `${query.errorDuration},错误摘要持续可见。`, lastExport: "Blocked by query error", statusMessage: `Error preview active for ${activeLabel}.`, columns: query.columns, }; } return { title: "Export completed", copy: `${query.successRowCount} rows exported to \`${query.exportPath}\`. The result state remains reviewable.`, duration: query.successDuration, rows: String(query.successRowCount), label: "Exported", resultSummary: `${query.successRowCount} rows · ${activeLabel}`, resultCopy: "导出完成后仍保留结果上下文,避免把确认信息丢到瞬时 toast。", exportTitle: "Export completed successfully", exportCopy: `\`${query.exportPath}\` was prepared as the last output path in this prototype.`, problemTitle: "No blocking problem", problemCopy: "导出成功是工作流完成态的一部分,应与执行成功和结果表格形成闭环。", rowsData: query.successRows, runSummary: `Exported · ${query.successRowCount} rows`, runCopy: `${query.exportFormat.toUpperCase()} 导出反馈已留在工作台中。`, lastExport: query.exportPath, statusMessage: `Export preview active for ${activeLabel}.`, columns: query.columns, }; } function renderSelection() { const selection = getSelectionMeta(); if (!selection) { return; } contextSelection.textContent = selection.selection; inspectorObject.textContent = selection.selection; selectionKind.textContent = selection.kind; selectionPrimaryKey.textContent = selection.primaryKey; selectionColumns.textContent = String(selection.columns); summarySelection.textContent = selection.selection; summarySelectionCopy.textContent = `${selection.descriptor} · schema browser 与 inspector 保持同步。`; } function renderConnection() { const connection = getActiveConnectionData(); activeConnection.textContent = connection.connection; driverPill.textContent = connection.driver; detailTarget.textContent = connection.connection; detailMode.textContent = connection.mode; contextTarget.textContent = connection.connection; contextMode.textContent = connection.mode; contextValidated.textContent = connection.lastValidated; summaryWorkspace.textContent = connection.workspace; summaryWorkspaceCopy.textContent = connection.workspaceCopy; } function renderThemeLabel() { const theme = document.documentElement.dataset.theme; const isDark = theme === "dark"; themeToggle.textContent = isDark ? "切换到 Light" : "切换到 Dark"; detailTheme.textContent = isDark ? "Dark" : "Light"; } function renderState() { const preset = buildStatePreset(); const contract = buildContractSnapshot(); const exportBlocked = appState.activeState === "running" || appState.activeState === "error"; executionBanner.className = `execution-banner is-${appState.activeState}`; executionTitle.textContent = preset.title; executionCopy.textContent = preset.copy; durationValue.textContent = preset.duration; rowCount.textContent = preset.rows; stateLabel.textContent = preset.label; resultSummary.textContent = preset.resultSummary; resultCopy.textContent = preset.resultCopy; exportTitle.textContent = preset.exportTitle; exportCopy.textContent = preset.exportCopy; problemTitle.textContent = preset.problemTitle; problemCopy.textContent = preset.problemCopy; contractTitle.textContent = contract.title; contractCopy.textContent = contract.copy; contractJson.textContent = JSON.stringify(contract.snapshot, null, 2); summaryRun.textContent = preset.runSummary; summaryRunCopy.textContent = preset.runCopy; summaryExport.textContent = preset.lastExport === "Not started" ? getQueryPreset().exportPath : preset.lastExport; summaryExportCopy.textContent = preset.lastExport === "Not started" ? "当前展示默认导出目标,不执行真实写盘。" : "导出反馈保留在当前工作台,便于 QA 与 PM 直接复核。"; detailExport.textContent = preset.lastExport; statusMessage.textContent = preset.statusMessage; runQueryButton.disabled = appState.activeState === "running"; runQueryButton.textContent = appState.activeState === "running" ? "Running…" : "Run Query"; exportCurrentButton.disabled = exportBlocked; exportCsvButton.disabled = exportBlocked; exportJsonButton.disabled = exportBlocked; renderResults(preset.columns, preset.rowsData, preset.tableMessage); renderHistory(buildHistory()); if (appState.activeState === "running") { setActiveTab("results"); } if (appState.activeState === "error") { setActiveTab("problems"); } if (appState.activeState === "exported") { setActiveTab("export"); } } function renderQuery() { const preset = getQueryPreset(); renderEditor(preset.sql); renderState(); } function applySchemaFilter() { const keyword = schemaSearch.value.trim().toLowerCase(); const items = getTreeItems(); const groups = getSchemaGroups(); items.forEach((item) => { const matches = item.dataset.search.includes(keyword); item.hidden = keyword.length > 0 && !matches; }); groups.forEach((group) => { const groupMatch = group.dataset.search.includes(keyword); const hasVisibleChildren = Array.from(group.querySelectorAll(".tree-item")).some((item) => !item.hidden); group.hidden = keyword.length > 0 && !groupMatch && !hasVisibleChildren; if (!group.hidden && keyword.length > 0) { group.open = true; } }); } function renderAll() { renderToolbarTabs(); renderSchemaTree(); renderConnection(); renderSelection(); renderThemeLabel(); renderQuery(); setActiveTab(appState.activeTab); } function clearRunTimer() { if (appState.runTimer) { clearTimeout(appState.runTimer); appState.runTimer = null; } } function closeCommandPalette() { commandPalette.hidden = true; document.body.classList.remove("has-modal"); } function openCommandPalette() { commandPalette.hidden = false; document.body.classList.add("has-modal"); statusMessage.textContent = "Quick actions open. Choose a state, connection, or workflow shortcut."; commandStateButtons[0]?.focus(); } function syncSelection(selection) { appState.selection = selection; renderSchemaTree(); renderSelection(); } function setQuery(queryKey, syncState = true) { const preset = getQueryPreset(queryKey); appState.query = queryKey; appState.selection = preset.selection; if (syncState) { appState.activeState = preset.settledState; } renderAll(); } function startRunPreview() { clearRunTimer(); appState.activeState = "running"; setActiveTab("results"); renderState(); appState.runTimer = setTimeout(() => { appState.runTimer = null; appState.activeState = getQueryPreset().settledState; renderState(); }, 1100); } connectionCards.forEach((card) => { card.addEventListener("click", () => { clearRunTimer(); connectionCards.forEach((item) => item.classList.remove("is-active")); card.classList.add("is-active"); appState.connection = card.dataset.connection; appState.query = "success"; appState.selection = getWorkspaceFixture().defaultSelection; appState.activeState = getQueryPreset().settledState; schemaSearch.value = ""; setActiveTab("results"); renderAll(); statusMessage.textContent = `${card.dataset.connection} loaded. Catalog, draft labels, and inspector now reflect the active target.`; }); }); schemaTree.addEventListener("click", (event) => { const item = event.target.closest(".tree-item"); if (!item) { return; } syncSelection(item.dataset.selection); statusMessage.textContent = `Selection changed to ${item.dataset.selection}. Inspector and workspace stay aligned.`; }); toolbarTabsContainer.addEventListener("click", (event) => { const tab = event.target.closest(".toolbar-tab"); if (!tab) { return; } clearRunTimer(); setActiveTab("results"); setQuery(tab.dataset.query, true); }); bottomTabs.forEach((tab) => { tab.addEventListener("click", () => { setActiveTab(tab.dataset.tab); }); }); runQueryButton.addEventListener("click", () => { startRunPreview(); }); cycleStateButton.addEventListener("click", () => { clearRunTimer(); const currentIndex = STATE_ORDER.indexOf(appState.activeState); const nextState = STATE_ORDER[(currentIndex + 1) % STATE_ORDER.length]; appState.activeState = nextState; renderState(); }); function activateExport(format) { clearRunTimer(); if (appState.activeState === "running") { renderState(); statusMessage.textContent = `${format} export is locked until the running query finishes.`; return; } if (appState.activeState === "error") { renderState(); statusMessage.textContent = `${format} export is blocked because the current query failed.`; return; } setActiveTab("export"); if (appState.activeState === "empty") { renderState(); statusMessage.textContent = `${format} export preview open. Current result is empty, so no file would be written.`; return; } appState.activeState = "exported"; renderState(); statusMessage.textContent = `${format} export preview active. No file is written in this prototype.`; } exportCurrentButton.addEventListener("click", () => activateExport("Workspace")); exportCsvButton.addEventListener("click", () => activateExport("CSV")); exportJsonButton.addEventListener("click", () => activateExport("JSON")); themeToggle.addEventListener("click", () => { document.documentElement.dataset.theme = document.documentElement.dataset.theme === "dark" ? "light" : "dark"; renderThemeLabel(); }); commandButton.addEventListener("click", () => { if (commandPalette.hidden) { openCommandPalette(); return; } closeCommandPalette(); }); commandBackdrop.addEventListener("click", closeCommandPalette); commandCloseButton.addEventListener("click", closeCommandPalette); commandStateButtons.forEach((button) => { button.addEventListener("click", () => { clearRunTimer(); appState.activeState = button.dataset.commandState; renderState(); closeCommandPalette(); }); }); commandConnectionButtons.forEach((button) => { button.addEventListener("click", () => { const card = document.querySelector(`.connection-card[data-connection="${button.dataset.commandConnection}"]`); card?.click(); closeCommandPalette(); }); }); commandActionButtons.forEach((button) => { button.addEventListener("click", () => { closeCommandPalette(); if (button.dataset.commandAction === "run") { startRunPreview(); return; } if (button.dataset.commandAction === "search") { schemaSearch.focus(); statusMessage.textContent = "Schema search focused."; return; } if (button.dataset.commandAction === "theme") { themeToggle.click(); } }); }); schemaSearch.addEventListener("input", () => { applySchemaFilter(); }); clearSearch.addEventListener("click", () => { schemaSearch.value = ""; applySchemaFilter(); statusMessage.textContent = "Schema search cleared."; }); document.addEventListener("keydown", (event) => { if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { event.preventDefault(); if (commandPalette.hidden) { openCommandPalette(); return; } closeCommandPalette(); return; } if (!commandPalette.hidden && event.key === "Escape") { event.preventDefault(); closeCommandPalette(); statusMessage.textContent = "Quick actions closed."; return; } if (event.target === schemaSearch) { return; } if (event.key === "/") { event.preventDefault(); schemaSearch.focus(); statusMessage.textContent = "Schema search focused."; return; } if (event.key.toLowerCase() === "t") { event.preventDefault(); themeToggle.click(); statusMessage.textContent = "Theme toggled."; return; } if (event.key.toLowerCase() === "r") { event.preventDefault(); startRunPreview(); return; } const hotkeyMap = { 1: "running", 2: "success", 3: "empty", 4: "error", 5: "exported", }; if (hotkeyMap[event.key]) { event.preventDefault(); clearRunTimer(); appState.activeState = hotkeyMap[event.key]; renderState(); } }); renderAll();