|
@@ -1,350 +0,0 @@
|
|
|
-<!doctype html>
|
|
|
|
|
-<html lang="zh">
|
|
|
|
|
-<head>
|
|
|
|
|
-<meta charset="utf-8" />
|
|
|
|
|
-<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
|
|
|
-<title>创作知识 · 流水线可视化</title>
|
|
|
|
|
-<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
|
|
|
|
-<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
|
|
|
|
-<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
|
|
|
-<style>
|
|
|
|
|
- :root{
|
|
|
|
|
- --bg:#f3efe4; /* 暖黄页面背景 */
|
|
|
|
|
- --panel:#ffffff; /* 白卡 */
|
|
|
|
|
- --panel2:#faf7ef; /* 略暖的内卡 */
|
|
|
|
|
- --line:#e7e2d4; /* 浅暖描边 */
|
|
|
|
|
- --line2:#ece7d9;
|
|
|
|
|
- --fg:#2c2c2a; /* 近黑正文 */
|
|
|
|
|
- --mut:#6f6e68; /* 次要文字 */
|
|
|
|
|
- --tert:#9a978d; /* 提示文字 */
|
|
|
|
|
- --ac:#185fa5; /* 蓝 */
|
|
|
|
|
- --what:#185fa5; --why:#534ab7; --how:#0f6e56;
|
|
|
|
|
- --ok:#0f6e56; --no:#a32d2d; --warn:#854f0b;
|
|
|
|
|
- }
|
|
|
|
|
- *{box-sizing:border-box}
|
|
|
|
|
- body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.6 -apple-system,"PingFang SC",Segoe UI,Roboto,sans-serif}
|
|
|
|
|
- header{padding:14px 20px;background:var(--panel);border-bottom:1px solid var(--line);font-weight:600;display:flex;gap:10px;align-items:center}
|
|
|
|
|
- header .sub{color:var(--mut);font-weight:400;font-size:13px}
|
|
|
|
|
- .wrap{display:flex;height:calc(100vh - 51px)}
|
|
|
|
|
- .side{width:300px;background:var(--panel);border-right:1px solid var(--line);overflow:auto;flex:none}
|
|
|
|
|
- .main{flex:1;overflow:auto;padding:20px}
|
|
|
|
|
- .pitem{padding:12px 16px;border-bottom:1px solid var(--line);cursor:pointer}
|
|
|
|
|
- .pitem:hover{background:#f7f3ea}
|
|
|
|
|
- .pitem.sel{background:#eef4fb;border-left:3px solid var(--ac)}
|
|
|
|
|
- .pitem .id{font-size:12px;color:var(--mut);word-break:break-all}
|
|
|
|
|
- .pitem .row{display:flex;gap:8px;align-items:center;margin-top:4px}
|
|
|
|
|
- .badge{display:inline-block;padding:1px 9px;border-radius:10px;font-size:12px;background:#efeadd;color:#5f5e5a}
|
|
|
|
|
- .badge.ok{background:#e1f5ee;color:var(--ok)}
|
|
|
|
|
- .badge.no{background:#fcebeb;color:var(--no)}
|
|
|
|
|
- .badge.ac{background:#e6f1fb;color:var(--ac)}
|
|
|
|
|
- .badge.warn{background:#faeeda;color:var(--warn)}
|
|
|
|
|
- .plhead{position:sticky;top:0;background:var(--panel);border-bottom:1px solid var(--line);padding:8px 12px 8px;z-index:2}
|
|
|
|
|
- .plsum{font-size:12px;color:var(--mut);margin:2px 2px 6px} .plsum b{color:var(--fg)}
|
|
|
|
|
- .plf-g{display:flex;align-items:center;gap:4px;flex-wrap:wrap;margin:4px 0}
|
|
|
|
|
- .plf-l{font-size:11px;color:var(--tert);width:28px;flex:none}
|
|
|
|
|
- .plf{border:1px solid var(--line);background:#fff;border-radius:12px;padding:2px 9px;font-size:11.5px;cursor:pointer;font-family:inherit;color:var(--fg)}
|
|
|
|
|
- .plf.on{background:#2c2c2a;color:#fff;border-color:#2c2c2a}
|
|
|
|
|
- .pl-r1{display:flex;align-items:center;gap:6px}
|
|
|
|
|
- .pbg{font-size:10.5px;font-weight:700;padding:1px 6px;border-radius:3px}
|
|
|
|
|
- .pb-xhs{background:#ffe4e6;color:#be123c}.pb-dy{background:#e5e7eb;color:#111}.pb-ks{background:#ffedd5;color:#c2410c}.pb-bili{background:#fce7f3;color:#be185d}.pb-sph{background:#dcfce7;color:#15803d}.pb-x{background:#eee;color:#555}
|
|
|
|
|
- .pl-ty{font-size:12px;color:var(--mut)}
|
|
|
|
|
- .pl-dot{width:8px;height:8px;border-radius:50%;margin-left:auto}
|
|
|
|
|
- .pl-dot.done{background:#16a34a}.pl-dot.skip{background:#d97706}.pl-dot.fail{background:#dc2626}.pl-dot.other{background:#cbd5e1}
|
|
|
|
|
- .pl-ti{font-size:13px;font-weight:550;line-height:1.4;margin-top:3px}
|
|
|
|
|
- .pl-mt{font-size:11px;color:var(--tert);margin-top:2px}
|
|
|
|
|
- .stage{background:var(--panel);border:1px solid var(--line);border-radius:12px;margin-bottom:16px;overflow:hidden}
|
|
|
|
|
- .stage>h3{margin:0;padding:11px 16px;background:#f7f3ea;border-bottom:1px solid var(--line);font-size:13px;font-weight:600;display:flex;gap:8px;align-items:center}
|
|
|
|
|
- .stage>h3 .n{width:20px;height:20px;border-radius:50%;background:#3f86d6;color:#fff;display:flex;align-items:center;justify-content:center;font-size:12px}
|
|
|
|
|
- .stage .body{padding:14px 16px}
|
|
|
|
|
- .kv{color:var(--mut);font-size:12px;margin:0 0 4px}
|
|
|
|
|
- .imgs{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
|
|
|
|
|
- .segwrap{display:flex;flex-direction:column;gap:3px;margin-top:4px}
|
|
|
|
|
- .segvid{width:240px;max-width:100%;border-radius:6px;border:1px solid var(--line);background:#000}
|
|
|
|
|
- .segcap{font-size:11px;color:var(--mut)}
|
|
|
|
|
- .imgs img{width:80px;height:80px;object-fit:cover;border-radius:6px;border:1px solid var(--line);cursor:zoom-in;transition:transform .1s}
|
|
|
|
|
- .imgs img:hover{transform:scale(1.05);border-color:var(--ac)}
|
|
|
|
|
- .card{background:var(--panel2);border:1px solid var(--line2);border-radius:10px;padding:12px 14px;margin-bottom:10px}
|
|
|
|
|
- .kptitle{margin:0;font-size:15px}
|
|
|
|
|
- .part{margin-top:12px}
|
|
|
|
|
- .part ~ .part{border-top:1px solid var(--line2);padding-top:12px}
|
|
|
|
|
- .phead{font-size:12px;color:var(--mut);font-weight:600;margin-bottom:8px;display:flex;align-items:center;gap:8px}
|
|
|
|
|
- .pn{display:inline-flex;width:18px;height:18px;border-radius:50%;background:#e6f1fb;color:#185fa5;align-items:center;justify-content:center;font-size:11px;flex:none}
|
|
|
|
|
- .ktype{font-size:12px;padding:2px 9px;border-radius:8px;background:#e6f1fb;color:#0c447c;white-space:nowrap;margin-left:auto}
|
|
|
|
|
- .chips{display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin:0}
|
|
|
|
|
- pre{white-space:pre-wrap;word-break:break-word;background:#f6f2e8;border:1px solid var(--line);border-radius:6px;padding:10px;font-size:12px;color:#444441;margin:6px 0 0;max-height:260px;overflow:auto}
|
|
|
|
|
- details summary{cursor:pointer;color:var(--mut);font-size:12px}
|
|
|
|
|
- .whh{margin:8px 0;padding-left:10px;border-left:2px solid var(--line)}
|
|
|
|
|
- .whh.w{border-color:var(--what)} .whh.y{border-color:var(--why)} .whh.h{border-color:var(--how)}
|
|
|
|
|
- .whh b{font-size:12px}
|
|
|
|
|
- .whh.w b{color:var(--what)} .whh.y b{color:var(--why)} .whh.h b{color:var(--how)}
|
|
|
|
|
- .sect{border-top:1px solid var(--line2);margin-top:12px;padding-top:10px;display:flex;flex-direction:column;gap:10px}
|
|
|
|
|
- .srow{display:flex;align-items:flex-start;gap:10px}
|
|
|
|
|
- .slabel{font-size:13px;color:var(--mut);min-width:62px;flex:none;padding-top:2px}
|
|
|
|
|
- .scope{display:flex;align-items:baseline;gap:8px}
|
|
|
|
|
- .scn{font-size:12px;padding:2px 8px;border-radius:8px;flex:none}
|
|
|
|
|
- .foot{border-top:1px solid var(--line2);margin-top:10px;padding-top:8px;display:flex;gap:18px;flex-wrap:wrap}
|
|
|
|
|
- .link{display:inline-flex;align-items:center;gap:6px;padding:5px 12px;background:#e6f1fb;border:1px solid #9cc3ee;border-radius:8px;color:var(--ac);text-decoration:none;font-size:13px;margin-bottom:10px}
|
|
|
|
|
- .crit{background:var(--panel2);border:1px solid var(--line2);border-radius:8px;padding:10px 14px;margin-bottom:12px}
|
|
|
|
|
- .crit .t{color:var(--mut);font-size:12px;margin-bottom:6px}
|
|
|
|
|
- .crit ul{margin:0;padding-left:18px} .crit li{margin:3px 0;font-size:13px}
|
|
|
|
|
- .lb{position:fixed;inset:0;background:rgba(0,0,0,.8);display:flex;align-items:center;justify-content:center;z-index:50;cursor:zoom-out}
|
|
|
|
|
- .lb img{max-width:92vw;max-height:92vh;border-radius:8px}
|
|
|
|
|
- a{color:var(--ac)}
|
|
|
|
|
- .empty{color:var(--mut);padding:40px;text-align:center}
|
|
|
|
|
- .sub2{color:var(--mut);font-size:12px;margin:-2px 0 8px}
|
|
|
|
|
- .sx{margin-left:auto;display:flex;gap:6px}
|
|
|
|
|
- .pbtn{font-size:12px;padding:2px 10px;border-radius:7px;background:#fff;border:1px solid var(--line);color:var(--mut);cursor:pointer;font-family:inherit}
|
|
|
|
|
- .pbtn:hover{border-color:var(--ac);color:var(--ac)}
|
|
|
|
|
- .pmodal{background:var(--panel);border:1px solid var(--line);border-radius:12px;max-width:760px;width:90vw;max-height:86vh;overflow:auto;padding:16px 18px;cursor:auto}
|
|
|
|
|
- .pmhead{display:flex;align-items:center;gap:10px;margin-bottom:6px}
|
|
|
|
|
- .pmhead b{font-size:15px}
|
|
|
|
|
- .pmmodel{font-size:12px;color:var(--ac);background:#eef4fb;padding:1px 8px;border-radius:7px}
|
|
|
|
|
- .pmx{margin-left:auto;cursor:pointer;color:var(--mut);font-size:16px;line-height:1}
|
|
|
|
|
- .pmlabel{font-size:12px;color:var(--mut);font-weight:600;margin:12px 0 4px}
|
|
|
|
|
- .pmnote{font-size:12px;color:var(--warn);background:#faeeda;padding:6px 10px;border-radius:6px;margin-top:4px}
|
|
|
|
|
- .srcrow{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:8px 0}
|
|
|
|
|
- .srcrow .lbl{font-size:12px;color:var(--mut);flex:none}
|
|
|
|
|
- .srcthumb{position:relative}
|
|
|
|
|
- .srcthumb img{width:56px;height:56px;object-fit:cover;border-radius:6px;border:1px solid var(--line);cursor:zoom-in}
|
|
|
|
|
- .srcthumb img:hover{border-color:var(--ac)}
|
|
|
|
|
- .srcthumb .cn{position:absolute;left:2px;bottom:2px;font-size:10px;background:rgba(0,0,0,.6);color:#fff;padding:0 4px;border-radius:4px}
|
|
|
|
|
- .cardthumb{position:relative}
|
|
|
|
|
- .cardthumb .cn{position:absolute;left:3px;bottom:3px;font-size:10px;background:rgba(0,0,0,.6);color:#fff;padding:0 5px;border-radius:4px}
|
|
|
|
|
- .evrow{display:flex;gap:8px;align-items:baseline;margin:5px 0;font-size:13px}
|
|
|
|
|
- .evcard{font-size:11px;padding:1px 7px;border-radius:6px;background:#e6f1fb;color:#185fa5;cursor:zoom-in;flex:none}
|
|
|
|
|
- .segchip{display:inline-flex;align-items:center;font-size:12px;padding:4px 10px;border-radius:8px;background:#eef4fb;color:#185fa5;border:1px solid #cfe0f5;flex:none}
|
|
|
|
|
-</style>
|
|
|
|
|
-</head>
|
|
|
|
|
-<body>
|
|
|
|
|
-<div id="root"></div>
|
|
|
|
|
-<script type="text/babel">
|
|
|
|
|
-const {useState,useEffect} = React;
|
|
|
|
|
-const j = (u)=>fetch(u).then(r=>r.json());
|
|
|
|
|
-const SCOPE_CN = {substance:"实质",form:"形式",feeling:"感受",effect:"作用",intent:"意图"};
|
|
|
|
|
-const SCOPE_COLOR = {
|
|
|
|
|
- substance:["#e6f1fb","#0c447c"], form:["#e1f5ee","#0f6e56"],
|
|
|
|
|
- feeling:["#eeedfe","#3c3489"], effect:["#faeeda","#854f0b"],
|
|
|
|
|
- intent:["#faece7","#993c1d"]};
|
|
|
|
|
-
|
|
|
|
|
-function Badge({kind,children}){return <span className={"badge "+(kind||"")}>{children}</span>;}
|
|
|
|
|
-
|
|
|
|
|
-const PLAT={xiaohongshu:["小红书","pb-xhs"],douyin:["抖音","pb-dy"],kuaishou:["快手","pb-ks"],bilibili:["B站","pb-bili"],shipinhao:["视频号","pb-sph"]};
|
|
|
|
|
-const statBucket=s=>s==="done"?"done":s==="skipped"?"skip":(s==="failed"||s==="rejected")?"fail":"other";
|
|
|
|
|
-function PostList({posts,sel,onSel}){
|
|
|
|
|
- const [fp,setFp]=useState("all"),[fs,setFs]=useState("all");
|
|
|
|
|
- const plats=[...new Set(posts.map(p=>p.platform))];
|
|
|
|
|
- const shown=posts.filter(p=>(fp==="all"||p.platform===fp)&&(fs==="all"||statBucket(p.stage)===fs));
|
|
|
|
|
- const Chip=({on,onClick,children})=><button className={"plf"+(on?" on":"")} onClick={onClick}>{children}</button>;
|
|
|
|
|
- return <div className="side">
|
|
|
|
|
- <div className="plhead">
|
|
|
|
|
- <div className="plsum">显示 <b>{shown.length}</b> / {posts.length} 条</div>
|
|
|
|
|
- <div className="plf-g"><span className="plf-l">平台</span>
|
|
|
|
|
- <Chip on={fp==="all"} onClick={()=>setFp("all")}>全部</Chip>
|
|
|
|
|
- {plats.map(p=><Chip key={p} on={fp===p} onClick={()=>setFp(p)}>{(PLAT[p]||[p])[0]}</Chip>)}</div>
|
|
|
|
|
- <div className="plf-g"><span className="plf-l">状态</span>
|
|
|
|
|
- <Chip on={fs==="all"} onClick={()=>setFs("all")}>全部</Chip>
|
|
|
|
|
- {[["done","已完成"],["skip","已跳过"],["fail","失败"]].map(([v,l])=><Chip key={v} on={fs===v} onClick={()=>setFs(v)}>{l}</Chip>)}</div>
|
|
|
|
|
- </div>
|
|
|
|
|
- {shown.map(p=>{
|
|
|
|
|
- const pb=PLAT[p.platform]||[p.platform,"pb-x"], st=statBucket(p.stage);
|
|
|
|
|
- const vid=(p.content_type||"").toLowerCase()==="video";
|
|
|
|
|
- return <div key={p.id} className={"pitem"+(sel===p.id?" sel":"")} onClick={()=>onSel(p.id)}>
|
|
|
|
|
- <div className="pl-r1"><span className={"pbg "+pb[1]}>{pb[0]}</span>
|
|
|
|
|
- <span className="pl-ty" title={vid?"视频":"图文"}>{vid?"▶":"▤"}</span>
|
|
|
|
|
- <span className={"pl-dot "+st} title={p.stage}></span></div>
|
|
|
|
|
- <div className="pl-ti">{p.title||p.id}</div>
|
|
|
|
|
- <div className="pl-mt">{p.item_count} 个知识点 · {p.run||"—"}</div>
|
|
|
|
|
- </div>;
|
|
|
|
|
- })}
|
|
|
|
|
- </div>;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function Stage({n,title,sub,extra,children}){
|
|
|
|
|
- return <div className="stage"><h3><span className="n">{n}</span>{title}{extra&&<span className="sx">{extra}</span>}</h3>
|
|
|
|
|
- <div className="body">{sub&&<div className="sub2">{sub}</div>}{children}</div></div>;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function PromptBtn({label,onClick}){
|
|
|
|
|
- return <button className="pbtn" onClick={onClick}>📄 {label||"提示词"}</button>;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function PromptModal({item,onClose}){
|
|
|
|
|
- if(!item) return null;
|
|
|
|
|
- return <div className="lb" onClick={onClose}>
|
|
|
|
|
- <div className="pmodal" onClick={e=>e.stopPropagation()}>
|
|
|
|
|
- <div className="pmhead"><b>{item.label} · 提示词</b><span className="pmmodel">{item.model}</span><span className="pmx" onClick={onClose}>✕</span></div>
|
|
|
|
|
- {item.note&&<div className="pmnote">{item.note}</div>}
|
|
|
|
|
- <div className="pmlabel">System(角色设定)</div><pre>{item.system}</pre>
|
|
|
|
|
- <div className="pmlabel">User(输入模板)</div><pre>{item.user}</pre>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function fmtTs(s){s=Math.floor(s||0);return String(Math.floor(s/60)).padStart(2,'0')+":"+String(s%60).padStart(2,'0');}
|
|
|
|
|
-function cardTime(c){
|
|
|
|
|
- if(!c) return "";
|
|
|
|
|
- if(c.kind==="segment"&&c.start!=null) return fmtTs(c.start)+"–"+fmtTs(c.end);
|
|
|
|
|
- if(c.kind==="frame"&&c.timestamp!=null) return fmtTs(c.timestamp);
|
|
|
|
|
- return "";
|
|
|
|
|
-}
|
|
|
|
|
-// 段卡:图片用缩略图,视频段卡用内联 <video> 只播 [start,end] 区间
|
|
|
|
|
-const isSeg=c=>!!c&&c.kind==="segment";
|
|
|
|
|
-const segSrc=c=>c.url+"#t="+(c.start||0)+","+(c.end||0);
|
|
|
|
|
-function SegVideo({c}){
|
|
|
|
|
- const end=c.end;
|
|
|
|
|
- return <video className="segvid" src={segSrc(c)} controls preload="metadata"
|
|
|
|
|
- onTimeUpdate={e=>{ if(end!=null && e.target.currentTime>=end) e.target.pause(); }}/>;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function KPCard({it,idx,cardMap,onZoom}){
|
|
|
|
|
- const item=it.item||{}, deco=it.deconstruction||{}, payload=it.ingest_payload||{};
|
|
|
|
|
- const types=(item.knowledge_types||[]).map(t=>t.toUpperCase()).join(" + ");
|
|
|
|
|
- const stages=deco.stages||[], scopes=deco.scopes||[];
|
|
|
|
|
- const srcCards=(item.source_cards||[]).map(n=>cardMap[n]).filter(Boolean);
|
|
|
|
|
- const evidence=item.evidence||[];
|
|
|
|
|
- return <div className="card">
|
|
|
|
|
- <h4 className="kptitle">{idx}. {item.title||"(无标题)"}</h4>
|
|
|
|
|
-
|
|
|
|
|
- {srcCards.length>0 && <div className="srcrow">
|
|
|
|
|
- <span className="lbl">来源卡片</span>
|
|
|
|
|
- {srcCards.map(c=> (c.url&&!isSeg(c))
|
|
|
|
|
- ? <span className="srcthumb" key={c.index}>
|
|
|
|
|
- <img src={c.url} title={"卡片"+c.index} onClick={()=>onZoom(c.url)} onError={e=>e.target.style.display='none'}/>
|
|
|
|
|
- <span className="cn">{cardTime(c)||("卡片"+c.index)}</span></span>
|
|
|
|
|
- : <span className="segchip" key={c.index}>片段{c.index}{cardTime(c)?" · "+cardTime(c):""}</span>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>}
|
|
|
|
|
-
|
|
|
|
|
- <div className="part">
|
|
|
|
|
- <div className="phead"><span className="pn">1</span>知识类型拆解<span className="ktype">{types||"—"}</span></div>
|
|
|
|
|
- {item.what&&<div className="whh w"><b>What · 是什么</b><div>{item.what}</div></div>}
|
|
|
|
|
- {item.why&&<div className="whh y"><b>Why · 为什么</b><div>{item.why}</div></div>}
|
|
|
|
|
- {item.how&&<div className="whh h"><b>How · 怎么做</b><div>{item.how}</div></div>}
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div className="part">
|
|
|
|
|
- <div className="phead"><span className="pn">2</span>解构 · 阶段</div>
|
|
|
|
|
- <div className="chips">
|
|
|
|
|
- {stages.length? stages.map(s=><Badge key={s}>{s}</Badge>) : <span className="kv">—</span>}
|
|
|
|
|
- {stages.length>1 && <span className="kv">({stages.length} 个阶段都 cover)</span>}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div className="part">
|
|
|
|
|
- <div className="phead"><span className="pn">3</span>解构 · 作用域</div>
|
|
|
|
|
- <div style={{display:'flex',flexDirection:'column',gap:6}}>
|
|
|
|
|
- {scopes.length? scopes.map((s,i)=>{
|
|
|
|
|
- const c=SCOPE_COLOR[s.scope_type]||["#efeadd","#5f5e5a"];
|
|
|
|
|
- return <div className="scope" key={i}>
|
|
|
|
|
- <span className="scn" style={{background:c[0],color:c[1]}}>{SCOPE_CN[s.scope_type]||s.scope_type}</span>
|
|
|
|
|
- <span>{s.value}</span></div>;
|
|
|
|
|
- }) : <span className="kv">—</span>}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div className="foot">
|
|
|
|
|
- {evidence.length>0 &&
|
|
|
|
|
- <details><summary>原文证据({evidence.length})</summary>
|
|
|
|
|
- <div>{evidence.map((e,i)=>{
|
|
|
|
|
- const text=typeof e==="string"?e:(e.text||"");
|
|
|
|
|
- const card=(typeof e==="object"&&e)?e.card:null;
|
|
|
|
|
- const c=card!=null?cardMap[card]:null;
|
|
|
|
|
- return <div className="evrow" key={i}>
|
|
|
|
|
- {c&&((c.url&&!isSeg(c))
|
|
|
|
|
- ? <span className="evcard" onClick={()=>onZoom(c.url)}>卡片{card}</span>
|
|
|
|
|
- : <span className="evcard" style={{cursor:"default"}} title="视频片段">片段{card}{cardTime(c)?" · "+cardTime(c):""}</span>)}
|
|
|
|
|
- <span>{text}</span></div>;
|
|
|
|
|
- })}</div>
|
|
|
|
|
- </details>}
|
|
|
|
|
- <details><summary>入库数据(ingest payload)</summary><pre>{JSON.stringify(payload,null,2)}</pre></details>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function Pipeline({postId,onZoom,prompts,onPrompt}){
|
|
|
|
|
- const [post,setPost]=useState(null);
|
|
|
|
|
- const [items,setItems]=useState([]);
|
|
|
|
|
- useEffect(()=>{ if(!postId)return; setPost(null);setItems([]);
|
|
|
|
|
- j("/api/posts/"+postId).then(setPost);
|
|
|
|
|
- j("/api/posts/"+postId+"/items").then(setItems);
|
|
|
|
|
- },[postId]);
|
|
|
|
|
- if(!postId) return <div className="empty">← 左边选一个帖子</div>;
|
|
|
|
|
- if(!post) return <div className="empty">加载中…</div>;
|
|
|
|
|
- const inner=(post.raw&&post.raw.data&&post.raw.data.data)||{};
|
|
|
|
|
- const scr=post.screening||{};
|
|
|
|
|
- const rawImgs=(inner.image_url_list||[]).map(x=>x.image_url||x);
|
|
|
|
|
- // 统一卡片:优先 post.cards(含图/帧),旧数据回退 image_url_list
|
|
|
|
|
- const cards=(post.cards&&post.cards.length)? post.cards
|
|
|
|
|
- : rawImgs.map((u,i)=>({index:i+1,kind:"image",url:u}));
|
|
|
|
|
- const cardMap={}; cards.forEach(c=>{cardMap[c.index]=c;});
|
|
|
|
|
- return <div>
|
|
|
|
|
- <Stage n="1" title="原文">
|
|
|
|
|
- <a className="link" href={post.url} target="_blank" rel="noreferrer">🔗 打开原帖</a>
|
|
|
|
|
- <p className="kv">作者 {inner.channel_account_name||"—"} · 类型 {inner.content_type||"—"} · {cards.length} 张卡片</p>
|
|
|
|
|
- <div><b>{inner.title||"(无标题)"}</b></div>
|
|
|
|
|
- <pre>{inner.body_text||"(正文为空,知识在卡片里)"}</pre>
|
|
|
|
|
- <div className="imgs">{cards.map(c=>
|
|
|
|
|
- (isSeg(c)&&c.url)
|
|
|
|
|
- ? <div className="segwrap" key={c.index}>
|
|
|
|
|
- <SegVideo c={c}/>
|
|
|
|
|
- <span className="segcap">片段{c.index} · {cardTime(c)}</span></div>
|
|
|
|
|
- : c.url
|
|
|
|
|
- ? <span className="cardthumb" key={c.index}>
|
|
|
|
|
- <img src={c.url} title={"卡片"+c.index} onClick={()=>onZoom(c.url)} onError={e=>e.target.style.display='none'}/>
|
|
|
|
|
- <span className="cn">{c.index}{cardTime(c)?" · "+cardTime(c):""}</span></span>
|
|
|
|
|
- : <span className="segchip" key={c.index}>片段{c.index}{cardTime(c)?" · "+cardTime(c):""}</span>
|
|
|
|
|
- )}</div>
|
|
|
|
|
- </Stage>
|
|
|
|
|
-
|
|
|
|
|
- <Stage n="2" title="筛选" sub="判断这篇帖子值不值得提取成创作知识"
|
|
|
|
|
- extra={prompts.screen && <PromptBtn label="提示词" onClick={()=>onPrompt('screen')}/>}>
|
|
|
|
|
- <div className="crit">
|
|
|
|
|
- <div className="t">筛选标准(4 条都满足才通过)</div>
|
|
|
|
|
- <ul>
|
|
|
|
|
- <li>内容不是空的</li>
|
|
|
|
|
- <li>讲的是"怎么做内容"的方法或经验,不是一条作品本身(比如一段文案、一张图)</li>
|
|
|
|
|
- <li>至少包含一种:是什么(What)/ 为什么(Why)/ 怎么做(How)</li>
|
|
|
|
|
- <li>至少能拆出一条能用的创作知识</li>
|
|
|
|
|
- </ul>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="chips">
|
|
|
|
|
- <Badge kind={scr.passed?"ok":"no"}>{scr.passed?"✓ 通过":"✕ 淘汰"}</Badge>
|
|
|
|
|
- <Badge kind="ac">评分 {scr.score}/10</Badge>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="kv" style={{marginTop:6}}>判断理由</div>
|
|
|
|
|
- <div>{scr.reason||"—"}</div>
|
|
|
|
|
- </Stage>
|
|
|
|
|
-
|
|
|
|
|
- <Stage n="3" title={"知识点("+items.length+" 条)"}
|
|
|
|
|
- sub="每条 = 知识类型 + What/Why/How + 对应阶段 + 作用域"
|
|
|
|
|
- extra={<React.Fragment>
|
|
|
|
|
- {prompts.split && <PromptBtn label="拆分提示词" onClick={()=>onPrompt('split')}/>}
|
|
|
|
|
- {prompts.deconstruct && <PromptBtn label="解构提示词" onClick={()=>onPrompt('deconstruct')}/>}
|
|
|
|
|
- </React.Fragment>}>
|
|
|
|
|
- {items.length? items.map((it,i)=><KPCard key={i} it={it} idx={i+1} cardMap={cardMap} onZoom={onZoom}/>) : <div className="kv">无(被淘汰或未拆出)</div>}
|
|
|
|
|
- </Stage>
|
|
|
|
|
- </div>;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function App(){
|
|
|
|
|
- const [posts,setPosts]=useState([]);
|
|
|
|
|
- const [sel,setSel]=useState(null);
|
|
|
|
|
- const [zoom,setZoom]=useState(null);
|
|
|
|
|
- const [prompts,setPrompts]=useState({});
|
|
|
|
|
- const [pm,setPm]=useState(null);
|
|
|
|
|
- useEffect(()=>{ j("/api/posts").then(ps=>{setPosts(ps); if(ps[0])setSel(ps[0].id);}); },[]);
|
|
|
|
|
- useEffect(()=>{ j("/api/prompts").then(r=>{const m={};(r.items||[]).forEach(it=>m[it.key]=it);setPrompts(m);}); },[]);
|
|
|
|
|
- return <div>
|
|
|
|
|
- <header>创作知识的搜索和解构</header>
|
|
|
|
|
- <div className="wrap">
|
|
|
|
|
- <PostList posts={posts} sel={sel} onSel={setSel}/>
|
|
|
|
|
- <div className="main"><Pipeline postId={sel} onZoom={setZoom} prompts={prompts} onPrompt={setPm}/></div>
|
|
|
|
|
- </div>
|
|
|
|
|
- {zoom&&<div className="lb" onClick={()=>setZoom(null)}><img src={zoom}/></div>}
|
|
|
|
|
- {pm&&<PromptModal item={prompts[pm]} onClose={()=>setPm(null)}/>}
|
|
|
|
|
- </div>;
|
|
|
|
|
-}
|
|
|
|
|
-ReactDOM.createRoot(document.getElementById("root")).render(<App/>);
|
|
|
|
|
-</script>
|
|
|
|
|
-</body>
|
|
|
|
|
-</html>
|
|
|