Ver código fonte

feat: 问数思考过程收成 Codex 式一行摘要(运行中展开/完成收起)+「应用到报表」替换为「导出为报告(HTML)」自包含导出

KenZ1117 2 semanas atrás
pai
commit
f7fc252aa2
2 arquivos alterados com 64 adições e 11 exclusões
  1. 55 11
      public/ask.js
  2. 9 0
      public/style.css

+ 55 - 11
public/ask.js

@@ -435,6 +435,50 @@
     stream.scrollTop = stream.scrollHeight;
   }
 
+  /** 把一次问数导出为自包含 HTML 报告(问题/元信息 + markdown 正文),纯前端生成。 */
+  function exportRunHtml(rec) {
+    const result = rec.result || {};
+    const entity = (state.entities.find((e) => e.key === (rec.entity || state.entity)) || {});
+    const styles = [
+      'body{font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;color:#2b2a28;margin:0;background:#f4f2ef;}',
+      '.wrap{max-width:860px;margin:0 auto;padding:40px 32px 56px;}',
+      'h1{font-size:22px;margin:0 0 8px;line-height:1.4;}',
+      '.meta{color:#6b6560;font-size:12.5px;margin-bottom:24px;}',
+      '.card{background:#fff;border:1px solid #e8e4df;border-radius:14px;padding:26px 30px;}',
+      '.answer{font-size:14px;line-height:1.8;}',
+      '.answer>*:first-child{margin-top:0}.answer>*:last-child{margin-bottom:0}',
+      '.answer p{margin:10px 0;}',
+      '.answer h3,.answer h4,.answer h5,.answer h6{margin:18px 0 8px;font-size:14px;font-weight:700;}',
+      '.answer h3{font-size:16px;}',
+      '.answer ul,.answer ol{margin:10px 0;padding-left:22px;display:grid;gap:5px;}',
+      '.answer blockquote{margin:10px 0;padding:8px 14px;border-left:3px solid #e8e4df;color:#6b6560;background:#f4f2ef;border-radius:0 8px 8px 0;}',
+      '.answer code{padding:1px 5px;border-radius:5px;background:#f0ede8;font-size:12.5px;font-family:ui-monospace,Menlo,Consolas,monospace;}',
+      '.answer pre.md-code{margin:10px 0;padding:12px 14px;border-radius:8px;background:#1d1c1a;color:#f4f2ef;font-size:12.5px;overflow:auto;line-height:1.6;}',
+      '.answer pre.md-code code{background:transparent;color:inherit;padding:0;}',
+      '.answer table{margin:12px 0;border-collapse:collapse;font-size:13px;width:100%;}',
+      '.answer th,.answer td{border:1px solid #e8e4df;padding:7px 11px;text-align:left;vertical-align:top;}',
+      '.answer th{background:#f0ede8;font-weight:650;white-space:nowrap;}',
+      '.answer hr{border:0;border-top:1px solid #e8e4df;margin:14px 0;}',
+      '.answer a{color:#ff6b35;}',
+      'footer{margin-top:22px;color:#a5a09a;font-size:12px;text-align:center;}',
+      '@media print{body{background:#fff;}.card{border:none;padding:0;}}',
+    ].join('\n');
+    const doc = '<!doctype html>\n<html lang="zh-CN">\n<head>\n<meta charset="utf-8">\n'
+      + '<meta name="viewport" content="width=device-width, initial-scale=1">\n'
+      + `<title>${esc(rec.question || '问数报告')}</title>\n<style>${styles}</style>\n</head>\n<body>\n<div class="wrap">\n`
+      + `<h1>${esc(rec.question || '')}</h1>\n`
+      + `<div class="meta">智能问数 · ${esc(entity.name || rec.entity || state.entity || '—')} · ${esc(fmtClock(rec.startedAt))}${rec.skill ? ` · 技能「${esc(rec.skill)}」` : ''}</div>\n`
+      + `<div class="card"><div class="answer">${renderMarkdown(result.answer || '')}</div></div>\n`
+      + '<footer>由 炘和数智智慧平台 · 智能问数 生成</footer>\n</div>\n</body>\n</html>';
+    const blob = new Blob([doc], { type: 'text/html;charset=utf-8' });
+    const a = document.createElement('a');
+    a.href = URL.createObjectURL(blob);
+    a.download = `问数报告-${String(rec.question || '报告').slice(0, 16).replace(/[\\/:*?"<>|\s]+/g, '-')}-${Date.now()}.html`;
+    a.click();
+    URL.revokeObjectURL(a.href);
+    showToast('报告已导出(.html,可直接打印为 PDF)', 'success');
+  }
+
   /** 把一次运行渲染进容器(轮询时整卡重绘,数据量小无压力)。 */
   function renderRunInto(wrap, rec) {
     const TOOL_LABEL = {
@@ -493,7 +537,14 @@ ${esc(step.summary || step.error || '')}</pre>
     parts.push(`<div class="ask-q">${esc(rec.question)}</div>`);
     parts.push('<div class="ask-card">');
     parts.push(head.join(''));
-    if (steps.length) parts.push(`<div class="ask-steps">${steps.join('')}</div>`);
+    // 思考过程:Codex 式一行摘要,运行中自动展开、完成后收起,点开才看逐步明细
+    if (steps.length) {
+      const running = rec.status === 'running';
+      const label = running
+        ? `思考中 · 已 ${steps.length} 步 · ${toolCount} 个工具`
+        : `思考过程 · ${steps.length} 步${totalMs != null ? ` · ${fmtDuration(totalMs)}` : ''} · ${toolCount} 个工具`;
+      parts.push(`<details class="ask-loop"${running ? ' open' : ''}><summary><span class="ask-dot ${running ? 'running' : 'done'}"></span>${esc(label)}</summary><div class="ask-steps">${steps.join('')}</div></details>`);
+    }
     // verdict 卡:数据体检/层间核对/恒等式/完整性等校验结论
     const verdicts = rec.verdicts || [];
     if (verdicts.length) {
@@ -521,9 +572,7 @@ ${esc(step.summary || step.error || '')}</pre>
         parts.push(`<div class="ask-chart" data-run="${esc(rec.runId)}"></div>`);
         if (chart.title) parts.push(`<div class="ask-chart-title">${esc(chart.title)}</div>`);
       }
-      if (result.appliedFrom) {
-        parts.push('<div class="ask-actions"><button type="button" class="btn btn-ghost btn-small" data-apply="' + esc(rec.runId) + '">应用到报表</button></div>');
-      }
+      parts.push('<div class="ask-actions"><button type="button" class="btn btn-ghost btn-small" data-export="' + esc(rec.runId) + '">导出为报告(HTML)</button></div>');
       if (result.reportUrl) {
         parts.push(`<div class="ask-actions"><a class="btn btn-ghost btn-small" href="${esc(result.reportUrl)}" target="_blank" rel="noopener">查看分析报告</a></div>`);
       }
@@ -549,13 +598,8 @@ ${esc(step.summary || step.error || '')}</pre>
       if (wrapEl && rows && rows.length) renderChart(wrapEl, result.chart, rows);
       else if (wrapEl) wrapEl.parentNode.removeChild(wrapEl);
     }
-    const applyBtn = wrap.querySelector('[data-apply]');
-    if (applyBtn) {
-      applyBtn.addEventListener('click', () => {
-        const applied = rec.result && rec.result.appliedFrom;
-        if (applied && window.shanReportsApplyQuery) window.shanReportsApplyQuery(applied);
-      });
-    }
+    const exportBtn = wrap.querySelector('[data-export]');
+    if (exportBtn) exportBtn.addEventListener('click', () => exportRunHtml(rec));
     wrap.querySelectorAll('[data-fb]').forEach((btn) => {
       btn.addEventListener('click', async () => {
         const kind = btn.dataset.fb;

+ 9 - 0
public/style.css

@@ -1095,6 +1095,15 @@ main.content { padding: var(--page-padding-y) var(--page-padding-x) var(--page-p
 .ask-answer a { color: var(--accent); }
 .ask-answer strong { font-weight: 700; }
 
+/* 思考过程:Codex 式一行摘要,默认收起 */
+.ask-loop { margin-top: 10px; }
+.ask-loop > summary { display: flex; align-items: center; gap: 8px; cursor: pointer; user-select: none; list-style: none; font-size: 12px; color: var(--text-2); padding: 5px 10px; border-radius: 8px; background: var(--border-light); }
+.ask-loop > summary::-webkit-details-marker { display: none; }
+.ask-loop > summary:hover { color: var(--text); }
+.ask-loop > summary .ask-dot { flex: 0 0 auto; }
+.ask-loop[open] > summary { margin-bottom: 10px; border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
+.ask-loop .ask-steps { padding: 2px 10px 4px; border: 1px solid var(--border-light); border-top: none; border-radius: 0 0 8px 8px; margin: 0; }
+
 /* 运行过程时间线:状态点 + 元信息胶囊 + 思考/工具卡 */
 .ask-step { padding-left: 22px; }
 .ask-step .ask-dot { top: 5px; }