index.html 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. <!doctype html>
  2. <html lang="zh">
  3. <head>
  4. <meta charset="utf-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1" />
  6. <title>创作知识 · 流水线可视化</title>
  7. <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
  8. <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
  9. <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  10. <style>
  11. :root{--bg:#0f1115;--panel:#171a21;--panel2:#1e222b;--line:#2a2f3a;--fg:#e6e8ec;--mut:#8b93a1;--ac:#5b9dff;--ok:#36c08a;--no:#e0556b;--warn:#e0a23a;}
  12. *{box-sizing:border-box}
  13. body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.5 -apple-system,"PingFang SC",Segoe UI,Roboto,sans-serif}
  14. header{padding:14px 20px;border-bottom:1px solid var(--line);font-weight:600;display:flex;gap:10px;align-items:center}
  15. header .sub{color:var(--mut);font-weight:400;font-size:13px}
  16. .wrap{display:flex;height:calc(100vh - 51px)}
  17. .side{width:300px;border-right:1px solid var(--line);overflow:auto;flex:none}
  18. .main{flex:1;overflow:auto;padding:20px}
  19. .pitem{padding:12px 16px;border-bottom:1px solid var(--line);cursor:pointer}
  20. .pitem:hover{background:var(--panel)}
  21. .pitem.sel{background:var(--panel2);border-left:3px solid var(--ac)}
  22. .pitem .id{font-size:12px;color:var(--mut);word-break:break-all}
  23. .pitem .row{display:flex;gap:8px;align-items:center;margin-top:4px}
  24. .badge{display:inline-block;padding:1px 8px;border-radius:10px;font-size:12px;background:var(--panel2);color:var(--mut);border:1px solid var(--line)}
  25. .badge.ok{background:rgba(54,192,138,.15);color:var(--ok);border-color:transparent}
  26. .badge.no{background:rgba(224,85,107,.15);color:var(--no);border-color:transparent}
  27. .badge.ac{background:rgba(91,157,255,.15);color:var(--ac);border-color:transparent}
  28. .badge.warn{background:rgba(224,162,58,.15);color:var(--warn);border-color:transparent}
  29. .stage{background:var(--panel);border:1px solid var(--line);border-radius:10px;margin-bottom:16px;overflow:hidden}
  30. .stage>h3{margin:0;padding:10px 16px;background:var(--panel2);font-size:13px;display:flex;gap:8px;align-items:center}
  31. .stage>h3 .n{width:20px;height:20px;border-radius:50%;background:var(--ac);color:#fff;display:flex;align-items:center;justify-content:center;font-size:12px}
  32. .stage .body{padding:14px 16px}
  33. .kv{color:var(--mut);font-size:12px;margin:0 0 4px}
  34. .imgs{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
  35. .imgs img{width:72px;height:72px;object-fit:cover;border-radius:6px;border:1px solid var(--line)}
  36. .card{background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:12px;margin-bottom:10px}
  37. .card h4{margin:0 0 8px;font-size:14px}
  38. .chips{display:flex;gap:6px;flex-wrap:wrap;margin:6px 0}
  39. pre{white-space:pre-wrap;word-break:break-word;background:#0c0e12;border:1px solid var(--line);border-radius:6px;padding:10px;font-size:12px;color:#c8cdd6;margin:6px 0 0;max-height:260px;overflow:auto}
  40. details summary{cursor:pointer;color:var(--mut);font-size:12px;margin-top:8px}
  41. .whh{margin:8px 0}
  42. .whh b{color:var(--ac);font-size:12px}
  43. a{color:var(--ac)}
  44. .empty{color:var(--mut);padding:40px;text-align:center}
  45. </style>
  46. </head>
  47. <body>
  48. <div id="root"></div>
  49. <script type="text/babel">
  50. const {useState,useEffect} = React;
  51. const j = (u)=>fetch(u).then(r=>r.json());
  52. function Badge({kind,children}){return <span className={"badge "+(kind||"")}>{children}</span>;}
  53. function PostList({posts,sel,onSel}){
  54. return <div className="side">
  55. {posts.map(p=>{
  56. const stageKind = p.stage==="done"?"ok":p.stage==="rejected"?"no":p.stage==="failed"?"no":"warn";
  57. return <div key={p.id} className={"pitem"+(sel===p.id?" sel":"")} onClick={()=>onSel(p.id)}>
  58. <div className="id">{p.id}</div>
  59. <div className="row">
  60. <Badge kind={stageKind}>{p.stage}</Badge>
  61. <Badge kind="ac">{p.item_count} 片段</Badge>
  62. </div>
  63. </div>;
  64. })}
  65. </div>;
  66. }
  67. function Stage({n,title,children}){
  68. return <div className="stage"><h3><span className="n">{n}</span>{title}</h3><div className="body">{children}</div></div>;
  69. }
  70. function ItemCard({it}){
  71. const item=it.item||{}, deco=it.deconstruction||{}, payload=it.ingest_payload||{};
  72. const ist=it.ingest_status;
  73. return <div className="card">
  74. <h4>{item.title||"(无标题)"}</h4>
  75. <div className="chips">
  76. {(item.knowledge_types||[]).map(t=><Badge key={t} kind="ac">{t}</Badge>)}
  77. {(deco.stages||[]).map(s=><Badge key={s}>{s}</Badge>)}
  78. <Badge kind={ist==="ingested"?"ok":ist==="failed"?"no":"warn"}>{ist}</Badge>
  79. </div>
  80. {item.what&&<div className="whh"><b>What</b><div>{item.what}</div></div>}
  81. {item.why&&<div className="whh"><b>Why</b><div>{item.why}</div></div>}
  82. {item.how&&<div className="whh"><b>How</b><div>{item.how}</div></div>}
  83. <div className="chips">
  84. {(deco.scopes||[]).map((s,i)=><Badge key={i}>{s.scope_type}: {s.value}</Badge>)}
  85. </div>
  86. <details><summary>ingest payload</summary>
  87. <pre>{JSON.stringify(payload,null,2)}</pre>
  88. </details>
  89. </div>;
  90. }
  91. function Pipeline({postId}){
  92. const [post,setPost]=useState(null);
  93. const [items,setItems]=useState([]);
  94. useEffect(()=>{ if(!postId)return;
  95. setPost(null);setItems([]);
  96. j("/api/posts/"+postId).then(setPost);
  97. j("/api/posts/"+postId+"/items").then(setItems);
  98. },[postId]);
  99. if(!postId) return <div className="empty">← 左边选一个帖子</div>;
  100. if(!post) return <div className="empty">加载中…</div>;
  101. const inner=(post.raw&&post.raw.data&&post.raw.data.data)||{};
  102. const ex=post.extracted||{}, scr=post.screening||{};
  103. const imgs=(inner.image_url_list||[]).map(x=>x.image_url||x).slice(0,9);
  104. return <div>
  105. <Stage n="1" title="原文(拉取)">
  106. <p className="kv">作者 {inner.channel_account_name} · 类型 {inner.content_type} · <a href={post.url} target="_blank">原链接</a></p>
  107. <div><b>{inner.title||"(无标题)"}</b></div>
  108. <pre>{inner.body_text||"(正文为空)"}</pre>
  109. <div className="imgs">{imgs.map((u,i)=><img key={i} src={u} onError={e=>e.target.style.display='none'}/>)}</div>
  110. </Stage>
  111. <Stage n="2" title="多模态提取(Gemini)">
  112. <div className="chips"><Badge kind={ex.is_empty?"no":"ok"}>is_empty: {String(ex.is_empty)}</Badge></div>
  113. {ex.from_image&&<div className="whh"><b>图片提取</b><div>{ex.from_image}</div></div>}
  114. {ex.from_video&&<div className="whh"><b>视频提取</b><div>{ex.from_video}</div></div>}
  115. {ex.text&&<pre>{ex.text}</pre>}
  116. </Stage>
  117. <Stage n="3" title="筛选">
  118. <div className="chips">
  119. <Badge kind={scr.passed?"ok":"no"}>{scr.passed?"passed":"rejected"}</Badge>
  120. <Badge kind="ac">score {scr.score}</Badge>
  121. </div>
  122. <div>{scr.reason}</div>
  123. </Stage>
  124. <Stage n="4" title={"知识片段("+items.length+")"}>
  125. {items.length? items.map((it,i)=><ItemCard key={i} it={it}/>) : <div className="kv">无(被筛选淘汰或未拆出)</div>}
  126. </Stage>
  127. </div>;
  128. }
  129. function App(){
  130. const [posts,setPosts]=useState([]);
  131. const [sel,setSel]=useState(null);
  132. useEffect(()=>{ j("/api/posts").then(ps=>{setPosts(ps); if(ps[0])setSel(ps[0].id);}); },[]);
  133. return <div>
  134. <header>创作知识 <span className="sub">· 帖子知识流水线可视化({posts.length} 帖)</span></header>
  135. <div className="wrap">
  136. <PostList posts={posts} sel={sel} onSel={setSel}/>
  137. <div className="main"><Pipeline postId={sel}/></div>
  138. </div>
  139. </div>;
  140. }
  141. ReactDOM.createRoot(document.getElementById("root")).render(<App/>);
  142. </script>
  143. </body>
  144. </html>