Explorar o código

feat(frontend): match legacy decode detail layout

SamLee hai 3 semanas
pai
achega
4b6c598e3e

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 8
app/frontend/dist/assets/index-B8vY7s1D.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
app/frontend/dist/assets/index-BqVHeSuo.css


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 8 - 0
app/frontend/dist/assets/index-CmZtyzF7.js


+ 2 - 2
app/frontend/dist/index.html

@@ -4,8 +4,8 @@
     <meta charset="UTF-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>创作知识 · Query 正交 Demo</title>
-    <script type="module" crossorigin src="/app/assets/index-B8vY7s1D.js"></script>
-    <link rel="stylesheet" crossorigin href="/app/assets/index-BmczRVfS.css">
+    <script type="module" crossorigin src="/app/assets/index-CmZtyzF7.js"></script>
+    <link rel="stylesheet" crossorigin href="/app/assets/index-BqVHeSuo.css">
   </head>
   <body>
     <div id="root"></div>

+ 2 - 1
app/frontend/src/App.jsx

@@ -23,8 +23,9 @@ export default function App() {
     window.addEventListener('hashchange', onHash)
     return () => window.removeEventListener('hashchange', onHash)
   }, [])
+  const shellClass = route.name === 'decodeItem' ? 'decode-shell' : 'wrap'
   return (
-    <div className="wrap">
+    <div className={shellClass}>
       {route.name === 'query' ? (
         <CreationQueryDetail runId={route.runId} queryId={route.queryId} />
       ) : route.name === 'decodeItem' ? (

+ 240 - 179
app/frontend/src/pages/DecodeItemDetail.jsx

@@ -1,152 +1,249 @@
 import React, { useEffect, useMemo, useState } from 'react'
 
+const SCOPES = [
+  ['substance', '实质'],
+  ['form', '形式'],
+  ['feeling', '感受'],
+  ['effect', '作用'],
+  ['intent', '意图'],
+]
+const SC_CN = Object.fromEntries(SCOPES)
+const TYPE_CN = { how: 'How', what: 'What', why: 'Why' }
+const TYPE_LABEL = { how: '怎么做', what: '是什么', why: '为什么' }
+
+const STEPS = [
+  ['读懂帖子', '把图、视频读成文字'],
+  ['判断是否是创作知识', '不是就整帖跳过'],
+  ['拆出知识', '拆成 How / What / Why'],
+  ['校验 how 的严谨性', '假工序拆回 What / Why'],
+  ['给知识归类', '标作用域并定位分类树'],
+  ['生成 payload', '整理成统一入库格式'],
+]
+
 function mediaUrl(asset) {
   return asset?.cdn_url || asset?.oss_url || asset?.source_url || ''
 }
 
+function payloadTitle(payload) {
+  return payload?.title || payload?.source?.title || '未命名 payload'
+}
+
 function compactJson(value) {
   return JSON.stringify(value || {}, null, 2)
 }
 
-function particleTone(type) {
-  return {
-    what: 'what',
-    how: 'how',
-    why: 'why',
-  }[type] || 'what'
+function stageList(value) {
+  if (Array.isArray(value)) return value
+  return value ? [value] : []
 }
 
-function typeLabel(type) {
-  return {
-    what: 'What I know',
-    how: 'How I do',
-    why: 'Why it works',
-  }[type] || type || 'Knowledge'
+function sourceImages(item) {
+  return (item?.media_assets || [])
+    .filter((asset) => ['image', 'cover', 'frame'].includes(asset.media_type))
+    .map(mediaUrl)
+    .filter(Boolean)
 }
 
-function payloadTitle(payload) {
-  return payload?.title || payload?.source?.title || '未命名 payload'
+function sourceVideo(item) {
+  const asset = (item?.media_assets || []).find((row) => row.media_type === 'video')
+  return asset ? mediaUrl(asset) : ''
+}
+
+function scopeClass(scopeType) {
+  return `s-${scopeType || 'intent'}`
 }
 
-function PayloadCard({ draft, ingest }) {
-  const payload = draft.payload || {}
+function ScopeChip({ scope, showType = false }) {
   return (
-    <article className="payload-card">
-      <div className="payload-head">
-        <b>{payloadTitle(payload)}</b>
-        <span className={`pill ${draft.status}`}>{draft.status}</span>
-      </div>
-      <div className="payload-meta">
-        <span>{(payload.dim_attributes || []).join(' / ') || '未标属性'}</span>
-        <span>{draft.ingest_ready ? '可入库' : '待审核'}</span>
-        {ingest && <span>{ingest.target_system} · {ingest.status}</span>}
-      </div>
-      {payload.content && <pre className="payload-content">{payload.content}</pre>}
-      {(payload.scopes || []).length > 0 && (
-        <div className="scope-row">
-          {payload.scopes.map((scope, idx) => (
-            <span className="scope-chip" key={`${scope.scope_type}-${scope.value}-${idx}`}>
-              {scope.scope_type}: {scope.value}
-            </span>
-          ))}
-        </div>
-      )}
-    </article>
+    <span className={`sv ${scopeClass(scope.scope_type)}`}>
+      {showType && <span className="k">{SC_CN[scope.scope_type] || scope.scope_type}</span>}
+      {scope.scope_value || scope.value}
+      <span className="mk mk-new">新</span>
+    </span>
   )
 }
 
-function KnowledgeCard({ particle, scopes }) {
-  const content = particle.content || {}
-  const steps = content.steps || []
-  const body = content.body || []
+function KScopes({ scopes }) {
+  if (!scopes?.length) return null
   return (
-    <article className={`knowledge-card ${particleTone(particle.particle_type)}`}>
-      <div className="knowledge-head">
-        <span>{typeLabel(particle.particle_type)}</span>
-        <b>{particle.title || '未命名知识颗'}</b>
+    <div className="kscopes">
+      <span className="lab">作用域(5 棵树):</span>
+      {scopes.map((scope, idx) => <ScopeChip key={`${scope.scope_type}-${scope.scope_value}-${idx}`} scope={scope} showType />)}
+    </div>
+  )
+}
+
+function Ribbon() {
+  return (
+    <div className="ribbon">
+      <div className="rcap">这套系统怎么「从帖子拆解知识」——从真实单例跑出的状态回看</div>
+      <div className="rsteps">
+        {STEPS.map(([label, desc], idx) => (
+          <React.Fragment key={label}>
+            {idx > 0 && <span className="rarrow">→</span>}
+            <div className={`rstep ${idx === 5 ? 'auto' : ''}`}>
+              <div className="rtop"><span className="rn">{idx + 1}</span><span className="rlab">{label}</span></div>
+              <div className="rdesc">{desc}</div>
+              <div className="rbtn">{idx === 5 ? '⚙️ 程序自动' : '已完成'}</div>
+            </div>
+          </React.Fragment>
+        ))}
       </div>
-      <div className="knowledge-meta">
-        {particle.business_stage && <span>业务阶段:{particle.business_stage}</span>}
-        {particle.creation_stage && <span>创作阶段:{particle.creation_stage}</span>}
-        <span>{particle.status}</span>
+    </div>
+  )
+}
+
+function SourceDetails({ item, readText, onZoom }) {
+  const images = sourceImages(item)
+  const video = sourceVideo(item)
+  return (
+    <details className="src" open>
+      <summary>原帖:{item?.title || '无标题'}(这一帖的原料)· {item?.platform || 'unknown'}{video ? ' · 视频' : images.length ? ` · ${images.length} 图` : ''}</summary>
+      <div className="srcbody">
+        {item?.canonical_url && <a href={item.canonical_url} target="_blank" rel="noreferrer">{item.canonical_url}</a>}
+        {video ? (
+          <div className="srcvideo"><video src={video} controls /></div>
+        ) : (
+          <div className="srcimgs">
+            {images.map((url, idx) => <img key={url} src={url} title={url} onClick={() => onZoom(url)} alt={`原帖图 ${idx + 1}`} />)}
+          </div>
+        )}
+        {readText && <div className="srcread">① 读懂后的内容:{readText}</div>}
       </div>
-      {content.purpose && <p className="knowledge-purpose">{content.purpose}</p>}
-      {steps.length > 0 && (
-        <div className="step-list">
+    </details>
+  )
+}
+
+function HowTable({ knowledge }) {
+  const steps = knowledge.steps || []
+  const scOf = (step, type) => (step['作用域'] || []).filter((scope) => scope.scope_type === type)
+  return (
+    <div className="twrap">
+      <table className="proc">
+        <thead>
+          <tr>
+            <th className="c-idx" rowSpan="2">#</th>
+            <th className="g-frame" colSpan="5">② 拆框架</th>
+            <th className="g-scope" colSpan="5">③ 作用域</th>
+          </tr>
+          <tr>
+            <th className="c-intent">输入</th>
+            <th className="c-cstage cstage-h">创作<br />阶段 ▸</th>
+            <th className="c-act">动作<br />(模型推导)</th>
+            <th className="c-dir">方法</th>
+            <th className="c-out">产出</th>
+            {SCOPES.map(([key, label]) => <th key={key} className={`c-scope s-${key}`}>{label}</th>)}
+          </tr>
+        </thead>
+        <tbody>
           {steps.map((step, idx) => (
-            <div className="step-row" key={step.id || idx}>
-              <span className="step-no">{idx + 1}</span>
-              <div>
-                <div><b>输入</b>{step.input || '未写'}</div>
-                <div><b>指引</b>{step.directive || '未写'}</div>
-                <div><b>产出</b>{step.output || '未写'}</div>
-              </div>
-            </div>
+            <tr className="step" key={step.id || idx}>
+              <td className="idx">{idx + 1}</td>
+              <td className="intent"><span className="in-txt">{step.input || '未写'}</span></td>
+              <td className="cstage">{step['创作阶段'] && <span className="cstage-chip">{step['创作阶段']}</span>}</td>
+              <td className="act">{step['动作'] && <span className="act-chip">{step['动作']}</span>}</td>
+              <td className="dir"><span className="dir-txt">{step.directive || '未写'}</span></td>
+              <td className="out"><span className="out-txt">{step.output || '未写'}</span></td>
+              {SCOPES.map(([key]) => (
+                <td key={key} className={`scope s-${key}`}>
+                  {scOf(step, key).map((scope, scopeIdx) => <ScopeChip key={scopeIdx} scope={scope} />)}
+                </td>
+              ))}
+            </tr>
           ))}
+        </tbody>
+      </table>
+    </div>
+  )
+}
+
+function WhatCard({ knowledge, scopes }) {
+  const body = knowledge.body || []
+  return (
+    <div>
+      {(knowledge['概要'] || knowledge.kind) && (
+        <div className="def">
+          {knowledge.kind && <span className={`kindb kind-${knowledge.kind}`}>{knowledge.kind}</span>}
+          {knowledge['维度拆分规则'] && <span className="dimrule">按「{knowledge['维度拆分规则']}」拆</span>}
+          {knowledge['概要'] && <>概要:{knowledge['概要']}</>}
         </div>
       )}
       {body.length > 0 && (
-        <div className="what-body">
-          {body.map((row, idx) => (
-            <div className="what-row" key={`${row.item_name || 'row'}-${idx}`}>
-              <b>{row.item_name || `条目 ${idx + 1}`}</b>
-              <span>{row.item_desc || '暂无描述'}</span>
-            </div>
-          ))}
-        </div>
-      )}
-      {(content['阐述'] || content.desc) && (
-        <p className="knowledge-purpose">{content['阐述'] || content.desc}</p>
-      )}
-      {scopes.length > 0 && (
-        <div className="scope-row">
-          {scopes.map((scope) => (
-            <span className="scope-chip" key={scope.id}>
-              {scope.scope_type}: {scope.scope_value}
-            </span>
-          ))}
-        </div>
+        <table className="elem">
+          <tbody>
+            {body.map((row, idx) => (
+              <tr key={`${row.item_name || 'row'}-${idx}`}>
+                <td className="el">{row.item_name || `条目 ${idx + 1}`}</td>
+                <td>{row.item_desc || '暂无描述'}</td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
       )}
-      <details className="fold knfold">
-        <summary>原始知识颗 JSON</summary>
-        <pre className="jsonbox">{compactJson(content)}</pre>
-      </details>
-    </article>
+      <KScopes scopes={scopes} />
+    </div>
   )
 }
 
-function SourcePanel({ item }) {
-  const media = item?.media_assets || []
-  const images = media.filter((asset) => ['image', 'cover', 'frame'].includes(asset.media_type)).map(mediaUrl).filter(Boolean)
-  const video = media.find((asset) => asset.media_type === 'video')
+function WhyCard({ knowledge, scopes }) {
   return (
-    <section className="source-panel">
-      <div>
-        <h1>{item?.title || 'Decode 知识详情'}</h1>
-        <div className="sub">
-          {item?.platform || 'unknown'} · {item?.author_name || '未知作者'} · {item?.content_type || '未知类型'}
-        </div>
-        {item?.raw_summary && <p className="source-summary">{item.raw_summary}</p>}
-        {item?.canonical_url && <a className="btn ghost" href={item.canonical_url} target="_blank" rel="noreferrer">打开原帖</a>}
+    <div>
+      {(knowledge['阐述'] || knowledge.desc) && <div className="why-exp">{knowledge['阐述'] || knowledge.desc}</div>}
+      <KScopes scopes={scopes} />
+    </div>
+  )
+}
+
+function KnowledgeCard({ particle, idx, scopes, payload, onJson }) {
+  const knowledge = particle.content || {}
+  const type = particle.particle_type || knowledge.type || 'what'
+  return (
+    <div className="kcard" id={`k-${particle.id}`}>
+      <div className="khead">
+        <span className="kno">第{idx + 1}颗知识</span>
+        <span className={`ktype ty-${type}`}>{TYPE_CN[type] || type}</span>
+        <span className="ktitle">{particle.title || knowledge.title || '未命名知识'}</span>
+        <span className="krole">{TYPE_LABEL[type] || type}</span>
+        {stageList(particle.business_stage || knowledge['业务阶段']).map((stage) => <span className="kbiz" key={stage}>业务阶段:{stage}</span>)}
+        <span className="kacts">{payload && <button className="lbtn" onClick={() => onJson(payload.payload)}>⚙️ 入库 payload</button>}</span>
       </div>
-      <div className="source-media">
-        {video ? (
-          <video controls playsInline src={mediaUrl(video)} />
-        ) : images.length > 0 ? (
-          <div className="decode-thumbs">
-            {images.slice(0, 6).map((url, idx) => <img key={url} src={url} alt={`素材 ${idx + 1}`} loading="lazy" />)}
-          </div>
-        ) : (
-          <div className="media-empty">暂无媒体</div>
-        )}
+      <div className="kbody">
+        {type === 'how' && <HowTable knowledge={knowledge} />}
+        {type === 'what' && <WhatCard knowledge={knowledge} scopes={scopes} />}
+        {type === 'why' && <WhyCard knowledge={knowledge} scopes={scopes} />}
       </div>
-    </section>
+    </div>
+  )
+}
+
+function PayloadModal({ payload, onClose }) {
+  const [copied, setCopied] = useState(false)
+  if (!payload) return null
+  const copy = () => {
+    navigator.clipboard?.writeText(compactJson(payload)).then(() => {
+      setCopied(true)
+      setTimeout(() => setCopied(false), 1200)
+    })
+  }
+  return (
+    <div className="modal" onClick={onClose}>
+      <div className="mbox" onClick={(event) => event.stopPropagation()}>
+        <h2>
+          ⚙️ ingest payload(给机器看)
+          <button className="lbtn copybtn" onClick={copy}>{copied ? '✓ 已复制' : '📋 复制 JSON'}</button>
+          <span className="x" onClick={onClose}>×</span>
+        </h2>
+        <div className="body"><pre>{compactJson(payload)}</pre></div>
+      </div>
+    </div>
   )
 }
 
 export default function DecodeItemDetail({ itemId }) {
   const [data, setData] = useState(null)
   const [err, setErr] = useState('')
+  const [jsonPayload, setJsonPayload] = useState(null)
+  const [zoom, setZoom] = useState(null)
 
   useEffect(() => {
     setData(null)
@@ -170,88 +267,52 @@ export default function DecodeItemDetail({ itemId }) {
     return map
   }, [data])
 
-  const ingestByDraft = useMemo(() => {
+  const payloadByParticle = useMemo(() => {
     const map = new Map()
-    for (const record of data?.ingest_records || []) {
-      if (record.payload_draft_id) map.set(record.payload_draft_id, record)
+    for (const draft of data?.payload_drafts || []) {
+      if (draft.particle_id) map.set(draft.particle_id, draft)
     }
     return map
   }, [data])
 
-  if (err) return <div className="empty">{err}</div>
-  if (!data) return <div className="empty">加载中...</div>
+  if (err) return <div className="legacy-decode-page"><div className="empty">{err}</div></div>
+  if (!data) return <div className="legacy-decode-page"><div className="empty">加载中...</div></div>
 
-  const readText = data.decode_result?.read_result?.text || ''
   const particles = data.knowledge_particles || []
-  const drafts = data.payload_drafts || []
+  const readText = data.decode_result?.read_result?.text || ''
+  const pass = data.decode_result?.gate_result?.passed
 
   return (
-    <div>
-      <div className="detail-top">
-        <a className="back" href="#/">返回 Query 列表</a>
-        <span className="meta">item_id: {itemId}</span>
-      </div>
-      <SourcePanel item={data.item} />
-
-      <section className="decode-strip">
-        <div>
-          <span>Decode</span>
-          <b>{data.decode_result.status}</b>
-        </div>
-        <div>
-          <span>Knowledge</span>
-          <b>{particles.length}</b>
-        </div>
-        <div>
-          <span>Payload</span>
-          <b>{drafts.length}</b>
-        </div>
-        <div>
-          <span>Ingest</span>
-          <b>{data.ingest_records?.filter((row) => row.status === 'ingested').length || 0}</b>
-        </div>
-      </section>
-
-      <section className="decode-section">
-        <h2>单帖读懂</h2>
-        <div className="read-panel">{readText || '暂无读懂文本'}</div>
-        <div className="gate-panel">
-          <span className={`pill ${data.decode_result.gate_result?.passed ? 'decoded' : 'rejected'}`}>
-            {data.decode_result.gate_result?.passed ? '通过创作闸' : '未通过创作闸'}
-          </span>
-          <span>{data.decode_result.gate_result?.reason || '暂无判断理由'}</span>
-        </div>
-      </section>
-
-      <section className="decode-section">
-        <h2>知识颗</h2>
-        {particles.length === 0 ? (
-          <div className="row-empty">暂无 What/How/Why 知识颗</div>
-        ) : (
-          <div className="knowledge-grid">
-            {particles.map((particle) => (
-              <KnowledgeCard
-                key={particle.id}
-                particle={particle}
-                scopes={scopesByParticle.get(particle.id) || []}
-              />
-            ))}
+    <div className="legacy-decode-page">
+      <div className="app">
+        <header className="top">
+          <h1>创作知识的解构</h1>
+          <div className="picker">
+            <a className="pill" href="#/">返回 Query Demo</a>
+            <span className="pill on">{data.item?.title || '真实单例'}<span className="n">{particles.length}颗</span></span>
+            <span className="lab">状态:{data.decode_result?.status || 'unknown'} · {pass ? '通过创作闸' : '未通过创作闸'}</span>
           </div>
-        )}
-      </section>
-
-      <section className="decode-section">
-        <h2>Payload Draft</h2>
-        {drafts.length === 0 ? (
-          <div className="row-empty">暂无 payload draft</div>
-        ) : (
-          <div className="payload-grid">
-            {drafts.map((draft) => (
-              <PayloadCard key={draft.id} draft={draft} ingest={ingestByDraft.get(draft.id)} />
-            ))}
+        </header>
+        <div className="scroll">
+          <Ribbon />
+          <SourceDetails item={data.item} readText={readText} onZoom={setZoom} />
+          <div className="barhint">
+            这一帖拆出 <b>{particles.length}</b> 颗知识(how 工序表 / what 知识卡 / why 论点卡);每颗知识右上角可打开 payload。
           </div>
-        )}
-      </section>
+          {particles.map((particle, idx) => (
+            <KnowledgeCard
+              key={particle.id}
+              particle={particle}
+              idx={idx}
+              scopes={scopesByParticle.get(particle.id) || []}
+              payload={payloadByParticle.get(particle.id)}
+              onJson={setJsonPayload}
+            />
+          ))}
+        </div>
+      </div>
+      <PayloadModal payload={jsonPayload} onClose={() => setJsonPayload(null)} />
+      {zoom && <div className="lb" onClick={() => setZoom(null)}><img src={zoom} alt="" /></div>}
     </div>
   )
 }

+ 618 - 0
app/frontend/src/styles.css

@@ -518,6 +518,616 @@ td.q { font-weight: 600; }
   border: 1px solid var(--line);
 }
 
+.decode-shell { min-height: 100vh; }
+
+/* 老版 Decode 知识详情页视觉系统:只作用于 decode-item 路由 */
+.legacy-decode-page {
+  height: 100vh;
+  min-height: 100vh;
+  overflow: hidden;
+  background: #f5f5f7;
+  color: #111827;
+  font: 13px/1.5 -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
+}
+.legacy-decode-page * { box-sizing: border-box; }
+.legacy-decode-page a { color: #2563eb; text-decoration: none; }
+.legacy-decode-page a:hover { text-decoration: underline; }
+.legacy-decode-page .app {
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+}
+.legacy-decode-page header.top {
+  flex: none;
+  padding: 11px 18px 9px;
+  background: #fff;
+  border-bottom: 1px solid #e5e7eb;
+}
+.legacy-decode-page header.top h1 {
+  margin: 0 0 7px;
+  font-size: 16px;
+  line-height: 1.35;
+  font-weight: 600;
+  letter-spacing: 0;
+}
+.legacy-decode-page .picker {
+  display: flex;
+  gap: 6px;
+  flex-wrap: wrap;
+  align-items: center;
+}
+.legacy-decode-page .picker .lab {
+  font-size: 11.5px;
+  color: #9ca3af;
+  margin-right: 2px;
+}
+.legacy-decode-page .pill {
+  display: inline-flex;
+  align-items: center;
+  font-size: 12px;
+  font-weight: 400;
+  line-height: 1.4;
+  padding: 4px 11px;
+  border-radius: 14px;
+  border: 1px solid #e5e7eb;
+  background: #fff;
+  color: #6b7280;
+  cursor: pointer;
+}
+.legacy-decode-page .pill:hover {
+  background: #f3f4f6;
+  text-decoration: none;
+}
+.legacy-decode-page .pill.on {
+  background: #1e293b;
+  color: #fff;
+  border-color: #1e293b;
+  font-weight: 600;
+}
+.legacy-decode-page .pill .n {
+  font-size: 10px;
+  opacity: .7;
+  margin-left: 4px;
+}
+.legacy-decode-page .scroll {
+  flex: 1;
+  overflow: auto;
+  background: #f5f5f7;
+  padding-bottom: 40px;
+}
+.legacy-decode-page .empty {
+  color: #9ca3af;
+  padding: 50px;
+  text-align: center;
+}
+.legacy-decode-page details.src {
+  display: block;
+  margin: 10px 18px 0;
+  background: #fff;
+  border: 1px solid #e2e8f0;
+  border-radius: 6px;
+}
+.legacy-decode-page details.src summary {
+  padding: 8px 14px;
+  cursor: pointer;
+  font-size: 12px;
+  color: #475569;
+  list-style: none;
+}
+.legacy-decode-page details.src summary::-webkit-details-marker { display: none; }
+.legacy-decode-page details.src summary::before {
+  content: '▶ ';
+  color: #94a3b8;
+  font-size: 10px;
+}
+.legacy-decode-page details.src[open] summary::before { content: '▼ '; }
+.legacy-decode-page .srcbody {
+  padding: 4px 16px 12px;
+  font-size: 12px;
+}
+.legacy-decode-page .srcbody a {
+  color: #2563eb;
+  word-break: break-all;
+}
+.legacy-decode-page .srcimgs {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+  margin-top: 8px;
+}
+.legacy-decode-page .srcimgs img {
+  width: 80px;
+  height: 80px;
+  object-fit: cover;
+  border-radius: 4px;
+  border: 1px solid #e2e8f0;
+  cursor: zoom-in;
+}
+.legacy-decode-page .srcvideo {
+  margin-top: 8px;
+}
+.legacy-decode-page .srcvideo video {
+  width: min(720px, 100%);
+  max-height: 360px;
+  background: #000;
+  border-radius: 5px;
+}
+.legacy-decode-page .srcread {
+  font-size: 12.5px;
+  color: #1f2937;
+  line-height: 1.6;
+  margin-top: 6px;
+  background: #fff;
+  border-left: 3px solid #0e7490;
+  padding: 6px 9px;
+}
+.legacy-decode-page .barhint {
+  margin: 8px 18px 0;
+  font-size: 11.5px;
+  color: #6b7280;
+}
+.legacy-decode-page .ribbon {
+  margin: 11px 18px 0;
+  background: #fff;
+  border: 1px solid #e2e8f0;
+  border-radius: 8px;
+  padding: 10px 14px;
+}
+.legacy-decode-page .rcap {
+  font-size: 11.5px;
+  color: #6b7280;
+  margin-bottom: 8px;
+}
+.legacy-decode-page .rsteps {
+  display: flex;
+  align-items: stretch;
+  flex-wrap: wrap;
+}
+.legacy-decode-page .rstep {
+  flex: 1;
+  min-width: 118px;
+  border: 1px solid #e2e8f0;
+  border-radius: 8px;
+  padding: 8px 9px;
+  cursor: pointer;
+  background: #f8fafc;
+}
+.legacy-decode-page .rstep:hover {
+  background: #ecfeff;
+  border-color: #99d5d0;
+}
+.legacy-decode-page .rstep.auto {
+  cursor: default;
+  background: #f1f5f9;
+  opacity: .9;
+}
+.legacy-decode-page .rstep.auto:hover {
+  background: #f1f5f9;
+  border-color: #e2e8f0;
+}
+.legacy-decode-page .rtop {
+  display: flex;
+  align-items: center;
+  gap: 7px;
+}
+.legacy-decode-page .rn {
+  width: 18px;
+  height: 18px;
+  border-radius: 50%;
+  background: #0e7490;
+  color: #fff;
+  font-size: 11px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex: none;
+}
+.legacy-decode-page .rstep.auto .rn { background: #94a3b8; }
+.legacy-decode-page .rlab {
+  font-size: 13.5px;
+  font-weight: 600;
+}
+.legacy-decode-page .rdesc {
+  font-size: 11.5px;
+  color: #6b7280;
+  margin: 5px 0 7px;
+  line-height: 1.5;
+}
+.legacy-decode-page .rbtn {
+  font-size: 11.5px;
+  color: #0e7490;
+}
+.legacy-decode-page .rstep.auto .rbtn { color: #94a3b8; }
+.legacy-decode-page .rarrow {
+  display: flex;
+  align-items: center;
+  color: #cbd5e1;
+  font-size: 18px;
+  padding: 0 4px;
+  flex: none;
+}
+.legacy-decode-page .lbtn {
+  padding: 3px 9px;
+  border: 1px solid #cbd5e1;
+  border-radius: 5px;
+  background: #fff;
+  cursor: pointer;
+  font-size: 11.5px;
+  color: #334155;
+  font-family: inherit;
+}
+.legacy-decode-page .lbtn:hover {
+  background: #f1f5f9;
+  text-decoration: none;
+}
+.legacy-decode-page .kcard {
+  margin: 12px 18px 0;
+  background: #fff;
+  border: 1px solid #e2e8f0;
+  border-radius: 8px;
+  overflow: hidden;
+}
+.legacy-decode-page .khead {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 9px 14px;
+  border-bottom: 1px solid #eef2f7;
+  flex-wrap: wrap;
+}
+.legacy-decode-page .kno {
+  font-size: 11px;
+  font-weight: 700;
+  color: #fff;
+  background: #475569;
+  border-radius: 4px;
+  padding: 1px 7px;
+}
+.legacy-decode-page .ktype {
+  font-size: 11px;
+  font-weight: 700;
+  border-radius: 4px;
+  padding: 1px 8px;
+}
+.legacy-decode-page .ty-how { background: #cffafe; color: #155e75; }
+.legacy-decode-page .ty-what { background: #fae8ff; color: #86198f; }
+.legacy-decode-page .ty-why { background: #fef9c3; color: #854d0e; }
+.legacy-decode-page .ktitle {
+  font-size: 14px;
+  font-weight: 600;
+}
+.legacy-decode-page .krole {
+  font-size: 10.5px;
+  padding: 1px 6px;
+  border-radius: 3px;
+  background: #f1f5f9;
+  color: #64748b;
+}
+.legacy-decode-page .kbiz {
+  font-size: 11px;
+  padding: 1px 7px;
+  border-radius: 3px;
+  background: #fef3c7;
+  color: #92400e;
+}
+.legacy-decode-page .kacts {
+  margin-left: auto;
+  display: flex;
+  gap: 6px;
+}
+.legacy-decode-page .kbody {
+  padding: 6px 14px 13px;
+}
+.legacy-decode-page .twrap {
+  overflow: auto;
+  border: 1px solid #e5e7eb;
+  border-radius: 6px;
+  margin-top: 8px;
+}
+.legacy-decode-page table.proc {
+  width: max-content;
+  min-width: 100%;
+  border-collapse: collapse;
+  font-size: 12px;
+  background: #fff;
+  border: 0;
+  border-radius: 0;
+}
+.legacy-decode-page table.proc th,
+.legacy-decode-page table.proc td {
+  border: 1px solid #e5e7eb;
+  padding: 6px 8px;
+  vertical-align: top;
+  line-height: 1.5;
+}
+.legacy-decode-page table.proc thead th {
+  font-weight: 600;
+  text-align: center;
+  font-size: 12px;
+  position: sticky;
+  top: 0;
+  z-index: 3;
+}
+.legacy-decode-page table.proc thead tr:nth-child(2) th { top: 30px; }
+.legacy-decode-page td.idx {
+  width: 30px;
+  text-align: center;
+  color: #6b7280;
+  background: #f8fafc;
+}
+.legacy-decode-page th.c-intent,
+.legacy-decode-page td.intent {
+  width: 210px;
+  min-width: 210px;
+  max-width: 210px;
+  white-space: pre-wrap;
+}
+.legacy-decode-page th.c-cstage,
+.legacy-decode-page td.cstage {
+  width: 54px;
+  text-align: center;
+  background: #ecfeff;
+}
+.legacy-decode-page th.c-act,
+.legacy-decode-page td.act {
+  min-width: 110px;
+  max-width: 140px;
+  word-break: break-word;
+  background: #ecfeff;
+}
+.legacy-decode-page th.c-dir,
+.legacy-decode-page td.dir {
+  min-width: 230px;
+  max-width: 330px;
+  white-space: pre-wrap;
+  word-break: break-word;
+  background: #ecfeff;
+}
+.legacy-decode-page th.c-out,
+.legacy-decode-page td.out {
+  min-width: 160px;
+  max-width: 240px;
+  white-space: pre-wrap;
+  background: #ecfeff;
+}
+.legacy-decode-page th.c-scope,
+.legacy-decode-page td.scope {
+  width: 92px;
+  min-width: 92px;
+  max-width: 92px;
+  word-break: break-word;
+}
+.legacy-decode-page th.g-frame { background: #0e7490; color: #fff; }
+.legacy-decode-page th.g-scope { background: #7c3aed; color: #fff; }
+.legacy-decode-page th.c-idx {
+  background: #475569;
+  color: #fff;
+  vertical-align: middle;
+}
+.legacy-decode-page thead tr:nth-child(2) th.c-intent,
+.legacy-decode-page thead tr:nth-child(2) th.c-cstage,
+.legacy-decode-page thead tr:nth-child(2) th.c-act,
+.legacy-decode-page thead tr:nth-child(2) th.c-dir,
+.legacy-decode-page thead tr:nth-child(2) th.c-out {
+  background: #0891b2;
+  color: #fff;
+}
+.legacy-decode-page thead tr:nth-child(2) th.s-substance { background: #dc2626; color: #fff; }
+.legacy-decode-page thead tr:nth-child(2) th.s-form { background: #2563eb; color: #fff; }
+.legacy-decode-page thead tr:nth-child(2) th.s-feeling { background: #db2777; color: #fff; }
+.legacy-decode-page thead tr:nth-child(2) th.s-effect { background: #16a34a; color: #fff; }
+.legacy-decode-page thead tr:nth-child(2) th.s-intent { background: #d97706; color: #fff; }
+.legacy-decode-page td.scope.s-substance { background: #fef2f2; }
+.legacy-decode-page td.scope.s-form { background: #eff6ff; }
+.legacy-decode-page td.scope.s-feeling { background: #fdf2f8; }
+.legacy-decode-page td.scope.s-effect { background: #f0fdf4; }
+.legacy-decode-page td.scope.s-intent { background: #fffbeb; }
+.legacy-decode-page tr.step > td { border-top: 1.5px solid #cbd5e1; }
+.legacy-decode-page .cstage-chip {
+  display: inline-block;
+  font-size: 11px;
+  padding: 1px 6px;
+  border-radius: 10px;
+  background: #ede9fe;
+  color: #5b21b6;
+}
+.legacy-decode-page .act-chip {
+  display: inline-block;
+  font-size: 11.5px;
+  padding: 1px 7px;
+  border-radius: 4px;
+  background: #fff7ed;
+  color: #9a3412;
+  border: 1px solid #fed7aa;
+}
+.legacy-decode-page .dir-txt {
+  font-size: 11.5px;
+  color: #33312c;
+  line-height: 1.65;
+}
+.legacy-decode-page .out-txt {
+  font-size: 11.5px;
+  color: #166534;
+}
+.legacy-decode-page .in-txt {
+  font-size: 11.5px;
+  color: #1e40af;
+  line-height: 1.6;
+}
+.legacy-decode-page .kindb {
+  font-size: 10.5px;
+  font-weight: 700;
+  padding: 1px 8px;
+  border-radius: 9px;
+  margin-right: 8px;
+  vertical-align: middle;
+}
+.legacy-decode-page .kind-子集 { background: #dbeafe; color: #1e40af; }
+.legacy-decode-page .kind-多维关系 { background: #dcfce7; color: #166534; }
+.legacy-decode-page .kind-序列 { background: #fef3c7; color: #92400e; }
+.legacy-decode-page .dimrule {
+  font-size: 10.5px;
+  padding: 1px 8px;
+  border-radius: 9px;
+  background: #ecfccb;
+  color: #3f6212;
+  margin-right: 8px;
+  vertical-align: middle;
+}
+.legacy-decode-page .def {
+  font-size: 13px;
+  background: #fdf4ff;
+  border-left: 3px solid #a21caf;
+  border-radius: 0 6px 6px 0;
+  padding: 8px 12px;
+  margin: 6px 0 10px;
+}
+.legacy-decode-page table.elem {
+  border-collapse: collapse;
+  width: 100%;
+  font-size: 12.5px;
+  background: #fff;
+  border: 0;
+}
+.legacy-decode-page table.elem td {
+  border: 1px solid #f0e6f5;
+  padding: 6px 10px;
+  text-align: left;
+  vertical-align: top;
+}
+.legacy-decode-page table.elem td.el {
+  font-weight: 600;
+  color: #86198f;
+  background: #fdf4ff;
+  width: 130px;
+}
+.legacy-decode-page .why-exp {
+  font-size: 12.5px;
+  line-height: 1.72;
+  color: #1f2937;
+  background: #fdf4ff;
+  border-left: 3px solid #a21caf;
+  border-radius: 0 6px 6px 0;
+  padding: 9px 13px;
+  margin: 6px 0;
+}
+.legacy-decode-page .kscopes {
+  margin-top: 9px;
+  display: flex;
+  gap: 5px;
+  flex-wrap: wrap;
+  align-items: center;
+}
+.legacy-decode-page .kscopes .lab {
+  font-size: 11px;
+  color: #9ca3af;
+}
+.legacy-decode-page .sv {
+  display: inline-block;
+  cursor: pointer;
+  font-size: 11.5px;
+  padding: 1px 5px;
+  border-radius: 3px;
+  border: 1px solid rgba(0,0,0,.08);
+  background: rgba(255,255,255,.7);
+  margin: 1px 0;
+}
+.legacy-decode-page .sv:hover { box-shadow: 0 0 0 1px #1e293b; }
+.legacy-decode-page .sv .k {
+  font-size: 11.5px;
+  opacity: .85;
+  font-weight: 700;
+  margin-right: 4px;
+}
+.legacy-decode-page table.proc th.c-scope { font-size: 13.5px; }
+.legacy-decode-page .sv .mk {
+  font-size: 9px;
+  padding: 0 3px;
+  border-radius: 2px;
+  margin-left: 3px;
+  vertical-align: top;
+}
+.legacy-decode-page .mk-new { background: #fde68a; color: #92400e; }
+.legacy-decode-page .mk-reuse { background: #bbf7d0; color: #166534; }
+.legacy-decode-page .sv.s-substance { background: #fef2f2; }
+.legacy-decode-page .sv.s-form { background: #eff6ff; }
+.legacy-decode-page .sv.s-feeling { background: #fdf2f8; }
+.legacy-decode-page .sv.s-effect { background: #f0fdf4; }
+.legacy-decode-page .sv.s-intent { background: #fffbeb; }
+.legacy-decode-page .modal {
+  position: fixed;
+  inset: 0;
+  background: rgba(0,0,0,.45);
+  z-index: 110;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 20px;
+  width: auto;
+  max-height: none;
+  overflow: visible;
+  border-radius: 0;
+  box-shadow: none;
+}
+.legacy-decode-page .mbox {
+  background: #fff;
+  width: 760px;
+  max-width: 94vw;
+  max-height: 86vh;
+  border-radius: 10px;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+.legacy-decode-page .mbox h2 {
+  margin: 0;
+  padding: 13px 18px;
+  background: #0f766e;
+  color: #fff;
+  font-size: 14px;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  letter-spacing: 0;
+}
+.legacy-decode-page .mbox h2 .x {
+  margin-left: auto;
+  cursor: pointer;
+  font-size: 18px;
+}
+.legacy-decode-page .mbox .body {
+  padding: 15px 18px;
+  overflow: auto;
+}
+.legacy-decode-page .mbox pre {
+  margin: 0;
+  font: 12.5px/1.7 ui-monospace, Menlo, monospace;
+  white-space: pre-wrap;
+  color: #1f2937;
+}
+.legacy-decode-page .copybtn {
+  margin-left: auto;
+  background: rgba(255,255,255,.12);
+  color: #fff;
+  border-color: rgba(255,255,255,.28);
+}
+.legacy-decode-page .copybtn:hover { background: rgba(255,255,255,.2); }
+.legacy-decode-page .lb {
+  position: fixed;
+  inset: 0;
+  background: rgba(0,0,0,.85);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 120;
+  cursor: zoom-out;
+}
+.legacy-decode-page .lb img {
+  max-width: 92vw;
+  max-height: 92vh;
+  border-radius: 6px;
+}
+
 @media (max-width: 720px) {
   .lanes { grid-template-columns: 1fr; }
   .source-panel { grid-template-columns: 1fr; }
@@ -528,4 +1138,12 @@ td.q { font-weight: 600; }
   .lbnav { bottom: 10px; top: auto; transform: none; }
   .lbnav.prev { left: 26px; }
   .lbnav.next { right: 26px; }
+  .legacy-decode-page .rsteps { display: block; }
+  .legacy-decode-page .rstep { margin-top: 7px; }
+  .legacy-decode-page .rarrow { display: none; }
+  .legacy-decode-page .khead { align-items: flex-start; }
+  .legacy-decode-page .kacts {
+    margin-left: 0;
+    width: 100%;
+  }
 }

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio