index.html 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  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;--what:#5b9dff;--why:#c084fc;--how:#36c08a;}
  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:80px;height:80px;object-fit:cover;border-radius:6px;border:1px solid var(--line);cursor:zoom-in;transition:transform .1s}
  36. .imgs img:hover{transform:scale(1.05);border-color:var(--ac)}
  37. .card{background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:12px 14px;margin-bottom:10px}
  38. .kphead{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px}
  39. .kphead h4{margin:0;font-size:14px}
  40. .ktype{font-size:12px;padding:3px 10px;border-radius:8px;background:rgba(91,157,255,.15);color:var(--ac);white-space:nowrap;flex:none}
  41. .chips{display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin:0}
  42. 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}
  43. details summary{cursor:pointer;color:var(--mut);font-size:12px;margin-top:8px}
  44. .whh{margin:8px 0;padding-left:10px;border-left:2px solid var(--line)}
  45. .whh.w{border-color:var(--what)} .whh.y{border-color:var(--why)} .whh.h{border-color:var(--how)}
  46. .whh b{font-size:12px}
  47. .whh.w b{color:var(--what)} .whh.y b{color:var(--why)} .whh.h b{color:var(--how)}
  48. .sect{border-top:1px solid var(--line);margin-top:12px;padding-top:10px;display:flex;flex-direction:column;gap:10px}
  49. .srow{display:flex;align-items:flex-start;gap:10px}
  50. .slabel{font-size:13px;color:var(--mut);min-width:62px;flex:none;padding-top:2px}
  51. .scope{display:flex;align-items:baseline;gap:8px}
  52. .scn{font-size:12px;padding:2px 8px;border-radius:8px;flex:none}
  53. .foot{border-top:1px solid var(--line);margin-top:10px;padding-top:6px;display:flex;gap:18px;flex-wrap:wrap}
  54. .link{display:inline-flex;align-items:center;gap:6px;padding:5px 12px;background:rgba(91,157,255,.12);border:1px solid var(--ac);border-radius:8px;color:var(--ac);text-decoration:none;font-size:13px;margin-bottom:10px}
  55. .crit{background:#0c0e12;border:1px solid var(--line);border-radius:8px;padding:10px 14px;margin-bottom:12px}
  56. .crit .t{color:var(--mut);font-size:12px;margin-bottom:6px}
  57. .crit ul{margin:0;padding-left:18px} .crit li{margin:3px 0;font-size:13px}
  58. .lb{position:fixed;inset:0;background:rgba(0,0,0,.85);display:flex;align-items:center;justify-content:center;z-index:50;cursor:zoom-out}
  59. .lb img{max-width:92vw;max-height:92vh;border-radius:8px}
  60. a{color:var(--ac)}
  61. .empty{color:var(--mut);padding:40px;text-align:center}
  62. .sub2{color:var(--mut);font-size:12px;margin:-2px 0 8px}
  63. </style>
  64. </head>
  65. <body>
  66. <div id="root"></div>
  67. <script type="text/babel">
  68. const {useState,useEffect} = React;
  69. const j = (u)=>fetch(u).then(r=>r.json());
  70. const SCOPE_CN = {substance:"实质",form:"形式",feeling:"感受",effect:"作用",intent:"意图"};
  71. const SCOPE_COLOR = {
  72. substance:["rgba(91,157,255,.16)","#9ec3ff"], form:["rgba(54,192,138,.16)","#6fe0b5"],
  73. feeling:["rgba(192,132,252,.16)","#d8b6ff"], effect:["rgba(224,162,58,.16)","#f0c97a"],
  74. intent:["rgba(224,85,107,.16)","#f0a0ad"]};
  75. function Badge({kind,children}){return <span className={"badge "+(kind||"")}>{children}</span>;}
  76. function PostList({posts,sel,onSel}){
  77. return <div className="side">
  78. {posts.map(p=>{
  79. const k = p.stage==="done"?"ok":p.stage==="rejected"?"no":p.stage==="failed"?"no":"warn";
  80. return <div key={p.id} className={"pitem"+(sel===p.id?" sel":"")} onClick={()=>onSel(p.id)}>
  81. <div className="id">{p.id}</div>
  82. <div className="row"><Badge kind={k}>{p.stage}</Badge><Badge kind="ac">{p.item_count} 条知识</Badge></div>
  83. </div>;
  84. })}
  85. </div>;
  86. }
  87. function Stage({n,title,sub,children}){
  88. return <div className="stage"><h3><span className="n">{n}</span>{title}</h3>
  89. <div className="body">{sub&&<div className="sub2">{sub}</div>}{children}</div></div>;
  90. }
  91. function KPCard({it,idx}){
  92. const item=it.item||{}, deco=it.deconstruction||{}, payload=it.ingest_payload||{};
  93. const types=(item.knowledge_types||[]).map(t=>t.toUpperCase()).join(" + ");
  94. const stages=deco.stages||[], scopes=deco.scopes||[];
  95. return <div className="card">
  96. <div className="kphead">
  97. <h4>{idx}. {item.title||"(无标题)"}</h4>
  98. <span className="ktype">知识类型 · {types||"—"}</span>
  99. </div>
  100. {item.what&&<div className="whh w"><b>What · 是什么</b><div>{item.what}</div></div>}
  101. {item.why&&<div className="whh y"><b>Why · 为什么</b><div>{item.why}</div></div>}
  102. {item.how&&<div className="whh h"><b>How · 怎么做</b><div>{item.how}</div></div>}
  103. <div className="sect">
  104. <div className="srow">
  105. <span className="slabel">对应阶段</span>
  106. <div className="chips">
  107. {stages.length? stages.map(s=><Badge key={s} kind="ac">{s}</Badge>) : <span className="kv">—</span>}
  108. {stages.length>1 && <span className="kv">({stages.length} 个阶段都 cover)</span>}
  109. </div>
  110. </div>
  111. <div className="srow">
  112. <span className="slabel">作用域</span>
  113. <div style={{display:'flex',flexDirection:'column',gap:6}}>
  114. {scopes.length? scopes.map((s,i)=>{
  115. const c=SCOPE_COLOR[s.scope_type]||["var(--panel)","var(--mut)"];
  116. return <div className="scope" key={i}>
  117. <span className="scn" style={{background:c[0],color:c[1]}}>{SCOPE_CN[s.scope_type]||s.scope_type}</span>
  118. <span>{s.value}</span></div>;
  119. }) : <span className="kv">—</span>}
  120. </div>
  121. </div>
  122. </div>
  123. <div className="foot">
  124. {(item.evidence&&item.evidence.length>0)&&
  125. <details><summary>原文证据({item.evidence.length})</summary><pre>{item.evidence.join("\n")}</pre></details>}
  126. <details><summary>入库数据(ingest payload)</summary><pre>{JSON.stringify(payload,null,2)}</pre></details>
  127. </div>
  128. </div>;
  129. }
  130. function Pipeline({postId,onZoom}){
  131. const [post,setPost]=useState(null);
  132. const [items,setItems]=useState([]);
  133. useEffect(()=>{ if(!postId)return; setPost(null);setItems([]);
  134. j("/api/posts/"+postId).then(setPost);
  135. j("/api/posts/"+postId+"/items").then(setItems);
  136. },[postId]);
  137. if(!postId) return <div className="empty">← 左边选一个帖子</div>;
  138. if(!post) return <div className="empty">加载中…</div>;
  139. const inner=(post.raw&&post.raw.data&&post.raw.data.data)||{};
  140. const scr=post.screening||{};
  141. const imgs=(inner.image_url_list||[]).map(x=>x.image_url||x);
  142. return <div>
  143. <Stage n="1" title="原文">
  144. <a className="link" href={post.url} target="_blank" rel="noreferrer">🔗 打开原帖</a>
  145. <p className="kv">作者 {inner.channel_account_name||"—"} · 类型 {inner.content_type||"—"}</p>
  146. <div><b>{inner.title||"(无标题)"}</b></div>
  147. <pre>{inner.body_text||"(正文为空,知识在图片里)"}</pre>
  148. <div className="imgs">{imgs.map((u,i)=><img key={i} src={u} title="点击放大"
  149. onClick={()=>onZoom(u)} onError={e=>e.target.style.display='none'}/>)}</div>
  150. </Stage>
  151. <Stage n="2" title="筛选" sub="判断这篇帖子值不值得提取成创作知识">
  152. <div className="crit">
  153. <div className="t">筛选标准(4 条都满足才通过)</div>
  154. <ul>
  155. <li>内容不是空的</li>
  156. <li>讲的是"怎么做内容"的方法或经验,不是一条作品本身(比如一段文案、一张图)</li>
  157. <li>至少包含一种:是什么(What)/ 为什么(Why)/ 怎么做(How)</li>
  158. <li>至少能拆出一条能用的创作知识</li>
  159. </ul>
  160. </div>
  161. <div className="chips">
  162. <Badge kind={scr.passed?"ok":"no"}>{scr.passed?"✓ 通过":"✕ 淘汰"}</Badge>
  163. <Badge kind="ac">评分 {scr.score}/10</Badge>
  164. </div>
  165. <div className="kv" style={{marginTop:6}}>判断理由</div>
  166. <div>{scr.reason||"—"}</div>
  167. </Stage>
  168. <Stage n="3" title={"知识点("+items.length+" 条)"}
  169. sub="每条 = 知识类型 + What/Why/How + 对应阶段 + 作用域">
  170. {items.length? items.map((it,i)=><KPCard key={i} it={it} idx={i+1}/>) : <div className="kv">无(被淘汰或未拆出)</div>}
  171. </Stage>
  172. </div>;
  173. }
  174. function App(){
  175. const [posts,setPosts]=useState([]);
  176. const [sel,setSel]=useState(null);
  177. const [zoom,setZoom]=useState(null);
  178. useEffect(()=>{ j("/api/posts").then(ps=>{setPosts(ps); if(ps[0])setSel(ps[0].id);}); },[]);
  179. return <div>
  180. <header>创作知识 <span className="sub">· 帖子知识流水线({posts.length} 帖)</span></header>
  181. <div className="wrap">
  182. <PostList posts={posts} sel={sel} onSel={setSel}/>
  183. <div className="main"><Pipeline postId={sel} onZoom={setZoom}/></div>
  184. </div>
  185. {zoom&&<div className="lb" onClick={()=>setZoom(null)}><img src={zoom}/></div>}
  186. </div>;
  187. }
  188. ReactDOM.createRoot(document.getElementById("root")).render(<App/>);
  189. </script>
  190. </body>
  191. </html>