|
|
@@ -1,279 +1,243 @@
|
|
|
<!doctype html>
|
|
|
<html lang="zh">
|
|
|
<head>
|
|
|
-<meta charset="utf-8" />
|
|
|
-<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
|
-<title>创作知识 · 流水线可视化</title>
|
|
|
+<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;
|
|
|
+ --bg:#f3efe4; --panel:#fff; --panel2:#faf7ef; --line:#e7e2d4; --line2:#ece7d9;
|
|
|
+ --fg:#2c2c2a; --mut:#6f6e68; --tert:#9a978d; --ac:#185fa5;
|
|
|
}
|
|
|
*{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}
|
|
|
+ header{padding:12px 20px;background:var(--panel);border-bottom:1px solid var(--line);display:flex;gap:10px;align-items:baseline}
|
|
|
+ header h0{font-size:16px;font-weight:650;margin:0}
|
|
|
+ header .sub{color:var(--mut);font-weight:400;font-size:12.5px}
|
|
|
+ .pbtn{margin-left:auto;background:#1e293b;color:#fff;border:0;border-radius:7px;padding:6px 13px;font-size:12.5px;cursor:pointer;font-family:inherit}
|
|
|
+ .pbtn:hover{background:#334155}
|
|
|
+ .legend{background:var(--panel2);border-bottom:1px solid var(--line);padding:8px 20px;font-size:11.5px;color:var(--mut);display:flex;gap:18px;flex-wrap:wrap;align-items:center}
|
|
|
+ .legend .grp{display:flex;gap:6px;align-items:center;flex-wrap:wrap}
|
|
|
+ .legend .gl{font-weight:700;color:var(--fg)}
|
|
|
+ .legend .it{white-space:nowrap}
|
|
|
+ .wrap{display:flex;height:calc(100vh - 50px - 38px)}
|
|
|
+ .side{width:320px;background:var(--panel);border-right:1px solid var(--line);overflow:auto;flex:none}
|
|
|
+ .sidecap{padding:9px 14px;font-size:11px;color:var(--tert);font-weight:600;border-bottom:1px solid var(--line);background:var(--panel2);position:sticky;top:0}
|
|
|
+ .main{flex:1;overflow:auto;padding:18px 26px}
|
|
|
+ .pitem{padding:11px 14px;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)}
|
|
|
- .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}
|
|
|
- .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}
|
|
|
+ .pitem.sel{background:#eef4fb;box-shadow:inset 3px 0 0 var(--ac)}
|
|
|
+ .pitem .ftitle{font-size:13.5px;font-weight:600;line-height:1.45}
|
|
|
+ .pitem .r{display:flex;gap:5px;align-items:center;margin-top:5px;flex-wrap:wrap}
|
|
|
+ .pbg{font-size:10.5px;font-weight:700;padding:1px 6px;border-radius:3px;background:#ffe4e6;color:#be123c}
|
|
|
+ .pitem .src{font-size:11px;color:var(--tert);margin-top:3px}
|
|
|
+ .chip{font-size:11px;padding:1px 8px;border-radius:10px;white-space:nowrap}
|
|
|
+ .c-stage{background:#ede9fe;color:#5b21b6}
|
|
|
+ .c-type{background:#e6f1fb;color:#185fa5}
|
|
|
+ .c-step{font-size:11px;padding:0 7px;border-radius:4px;background:#eef2f7;color:#475569;font-weight:600}
|
|
|
+ .sc{font-size:11px;padding:1px 8px;border-radius:4px;margin:0 4px 4px 0;display:inline-block}
|
|
|
+ .sc-实质{background:#fee2e2;color:#991b1b}.sc-形式{background:#dbeafe;color:#1e40af}
|
|
|
+ .sc-感受{background:#fce7f3;color:#9d174d}.sc-作用{background:#dcfce7;color:#166534}.sc-意图{background:#fef3c7;color:#92400e}
|
|
|
+ .dh{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:2px}
|
|
|
+ .dh h1{font-size:20px;margin:0;font-weight:700}
|
|
|
+ .dh .acts{margin-left:auto;display:flex;gap:8px;align-items:center}
|
|
|
+ .dh a{color:var(--ac);text-decoration:none;font-size:12.5px}
|
|
|
+ .minib{background:#eef2f7;color:#334155;border:1px solid var(--line);border-radius:6px;padding:4px 10px;font-size:12px;cursor:pointer;font-family:inherit}
|
|
|
+ .minib:hover{background:#e2e8f0}
|
|
|
+ .ptitle{color:var(--mut);font-size:12.5px;margin-bottom:12px}
|
|
|
+ .meta{display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin-bottom:16px}
|
|
|
+ /* 下游字段名作主标签 */
|
|
|
+ .flab{display:inline-block;font-size:11px;font-weight:700;color:#fff;background:#0f766e;border-radius:5px;padding:1px 9px;margin-bottom:7px}
|
|
|
+ .block{margin-bottom:18px}
|
|
|
+ .purpose{background:var(--panel);border:1px solid var(--line);border-left:3px solid #0f766e;border-radius:8px;padding:11px 14px;font-size:14.5px;font-weight:600;line-height:1.55}
|
|
|
+ .want{background:var(--panel2);border:1px dashed var(--line);border-radius:8px;padding:8px 12px;font-size:13px}
|
|
|
+ .want b{color:var(--ac)}
|
|
|
+ .step{background:var(--panel);border:1px solid var(--line);border-radius:10px;margin-bottom:12px;overflow:hidden}
|
|
|
+ .step-h{display:flex;align-items:center;gap:9px;padding:9px 14px;background:var(--panel2);border-bottom:1px solid var(--line2)}
|
|
|
+ .step-n{background:#0f766e;color:#fff;width:auto;min-width:46px;height:22px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:11px;flex:none;padding:0 8px;font-weight:600}
|
|
|
+ .step-intent{font-size:14px;font-weight:600;flex:1}
|
|
|
+ .step-intent .vlabel{color:var(--tert);font-weight:400;font-size:12px}
|
|
|
+ .step-tags{padding:8px 14px 0;display:flex;gap:5px;flex-wrap:wrap;align-items:center}
|
|
|
+ .rowlab{display:flex;gap:8px;padding:9px 14px 0}
|
|
|
+ .rl{font-size:10px;font-weight:700;border-radius:4px;padding:1px 7px;flex:none;height:18px;line-height:16px}
|
|
|
+ .rl-how{background:#e6f1fb;color:#185fa5}
|
|
|
+ .rl-out{background:#dcfce7;color:#15803d}
|
|
|
+ .directive{font-size:13px;line-height:1.72;white-space:pre-wrap;color:#33312c;flex:1}
|
|
|
+ .outtxt{font-size:12.5px;line-height:1.6;color:#166534;flex:1}
|
|
|
+ .from{font-size:11px;color:var(--tert);margin-left:auto}
|
|
|
+ details.box{margin-top:14px;border:1px solid var(--line);border-radius:8px;background:var(--panel)}
|
|
|
+ details.box>summary{padding:9px 14px;cursor:pointer;font-size:12.5px;color:var(--mut);list-style:none}
|
|
|
+ details.box>summary::before{content:'▶ ';color:var(--tert)}
|
|
|
+ details.box[open]>summary::before{content:'▼ '}
|
|
|
+ .box .bd{padding:4px 14px 12px}
|
|
|
+ .drop{font-size:12px;color:var(--mut);margin:4px 0;line-height:1.6}
|
|
|
+ .imgs{display:flex;flex-wrap:wrap;gap:7px;padding:4px 14px 12px}
|
|
|
+ .imgs img{width:96px;height:96px;object-fit:cover;border-radius:6px;border:1px solid var(--line);cursor:zoom-in}
|
|
|
+ .lb{position:fixed;inset:0;background:rgba(0,0,0,.82);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}
|
|
|
+ .empty{color:var(--tert);padding:40px;text-align:center}
|
|
|
+ .ov{position:fixed;inset:0;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;z-index:60}
|
|
|
+ .modal{background:#fff;width:780px;max-width:92vw;max-height:86vh;border-radius:10px;overflow:hidden;display:flex;flex-direction:column}
|
|
|
+ .modal h2{margin:0;padding:13px 18px;background:#1e293b;color:#fff;font-size:14px;display:flex;align-items:center}
|
|
|
+ .modal h2 .x{margin-left:auto;cursor:pointer;font-size:18px}
|
|
|
+ .modal .mbody{padding:14px 18px;overflow:auto}
|
|
|
+ .modal pre{margin:0;font:12px/1.62 ui-monospace,"SF Mono",Menlo,monospace;color:#1f2937;white-space:pre-wrap}
|
|
|
+ .modal .fk{font-size:11px;color:var(--tert);margin:10px 0 3px;font-weight:700}
|
|
|
+ .modal .fv{font-size:13px;color:#1f2937;margin-bottom:4px}
|
|
|
+ .modal .cwrap{background:#f7f7f5;border:1px solid var(--line);border-radius:7px;padding:11px 13px;margin-top:4px}
|
|
|
</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"]};
|
|
|
+const j = u => fetch(u).then(r=>r.json());
|
|
|
|
|
|
-function Badge({kind,children}){return <span className={"badge "+(kind||"")}>{children}</span>;}
|
|
|
+const PROMPT = `# 创作知识提取(需求驱动 · 完整框架粒度)
|
|
|
|
|
|
-function PostList({posts,sel,onSel}){
|
|
|
- return <div className="side">
|
|
|
- {posts.map(p=>{
|
|
|
- const k = p.stage==="done"?"ok":p.stage==="rejected"?"no":p.stage==="failed"?"no":"warn";
|
|
|
- return <div key={p.id} className={"pitem"+(sel===p.id?" sel":"")} onClick={()=>onSel(p.id)}>
|
|
|
- <div className="id">{p.id}</div>
|
|
|
- <div className="row"><Badge kind={k}>{p.stage}</Badge><Badge kind="ac">{p.item_count} 条知识</Badge></div>
|
|
|
- </div>;
|
|
|
- })}
|
|
|
- </div>;
|
|
|
-}
|
|
|
+产物:一颗知识 = 一个完整创作框架
|
|
|
+= 一个 purpose(把某种输入一路做成一个可交付成品)+ 一条不可跳步的 steps 链。
|
|
|
+一帖通常只出 1-2 颗。绝不把框架内部的步骤/要点拆成独立知识。
|
|
|
|
|
|
-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>;
|
|
|
-}
|
|
|
+## 提取四步
|
|
|
+1. 数闭环:找帖里所有“输入→可交付成品”的完整链,几条算几颗。
|
|
|
+2. 去重叠:若某链成品其实是另一条链的中间产物,并入后者。
|
|
|
+3. 压清单:所有“N选1 / 并列要素”压进所属 step,不按项数拆颗、不拆成多步。
|
|
|
+4. 定步数:每颗按“几次不可省的形态转换”定 steps(2-6),动词只用 决策/分析/构建/撰写/打磨。
|
|
|
+
|
|
|
+## 一颗的结构
|
|
|
+- title:6-12字名词短语,以“框架/创作/写作”收尾。
|
|
|
+- purpose:「按「A→B→C」N步,将X写成可直接交付的Y」。
|
|
|
+- 每 step:intent="<双字动词>,<任务>";directive=把怎么做的全部细节压进来(清单+判据+正反例+阈值+反模式);output=交付物验收规格。
|
|
|
+
|
|
|
+## 只要创作,剔除制作(按“设计决策 vs 工艺执行”判,不看词面)
|
|
|
+保留:景别/角度/运镜/构图/字幕/配乐/结构/时长。剔除:器材/打光/剪辑软件操作/调色参数/导出。
|
|
|
|
|
|
-function PromptBtn({label,onClick}){
|
|
|
- return <button className="pbtn" onClick={onClick}>📄 {label||"提示词"}</button>;
|
|
|
+## 标签
|
|
|
+知识类型(What/Why/How)、阶段(灵感/选题/脚本)、作用域(实质/形式/感受/作用/意图,可多选)。`;
|
|
|
+
|
|
|
+// 把一颗框架拍平成下游消费的 content 文本(与 id 1005 同款)
|
|
|
+function buildContent(fw){
|
|
|
+ let s = "目标:" + (fw.purpose||"") + "\n";
|
|
|
+ (fw.steps||[]).forEach((st,i)=>{
|
|
|
+ s += "步骤"+(i+1)+"(目的:"+(st.intent||"")+")\n";
|
|
|
+ s += " 指引:"+(st.directive||"")+"\n";
|
|
|
+ s += " 产出:"+(st.output||"")+"\n";
|
|
|
+ });
|
|
|
+ return s.replace(/\n+$/,"");
|
|
|
}
|
|
|
|
|
|
-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>
|
|
|
+function Scopes({list}){ return (list||[]).map((s,i)=><span key={i} className={"sc sc-"+s}>{s}</span>); }
|
|
|
+function Tags({stage,type,scopes}){ return <React.Fragment>
|
|
|
+ {type&&<span className="chip c-type">类型·{type}</span>}
|
|
|
+ {stage&&<span className="chip c-stage">阶段·{stage}</span>}
|
|
|
+ <Scopes list={scopes}/>
|
|
|
+</React.Fragment>; }
|
|
|
+
|
|
|
+function Legend(){
|
|
|
+ return <div className="legend">
|
|
|
+ <div className="grp"><span className="gl">怎么读:</span><span className="it">左边一颗知识 → 右边是它下发给下游的格式:<b>目标</b> + <b>步骤</b>(目的/指引/产出)。点右上「下游记录」看实际 content。</span></div>
|
|
|
+ <div className="grp"><span className="gl">类型</span><span className="it"><b>What</b>是什么</span><span className="it"><b>Why</b>为什么</span><span className="it"><b>How</b>怎么做</span></div>
|
|
|
+ <div className="grp"><span className="gl">阶段</span><span className="it">灵感·找方向</span><span className="it">选题·定写什么</span><span className="it">脚本·组织表达</span></div>
|
|
|
+ <div className="grp"><span className="gl">作用域</span><span className="sc sc-实质">实质讲什么</span><span className="sc sc-形式">形式怎么呈现</span><span className="sc sc-感受">感受勾情绪</span><span className="sc sc-作用">作用起功能</span><span className="sc sc-意图">意图创作者目的</span></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 "";
|
|
|
+function PostList({posts,sel,onSel}){
|
|
|
+ return <div className="side">
|
|
|
+ <div className="sidecap">知识列表 · 每条 = 一颗完整创作框架</div>
|
|
|
+ {posts.map(p=>{ const fw=p.framework||{};
|
|
|
+ return <div key={p.post_id} className={"pitem"+(sel===p.post_id?" sel":"")} onClick={()=>onSel(p.post_id)}>
|
|
|
+ <div className="ftitle">{fw.title||p.title}</div>
|
|
|
+ <div className="r"><span className="pbg">小红书</span><span className="chip c-type">{fw["知识类型"]}</span><span className="chip c-stage">{fw["阶段跨度"]}</span><span className="c-step">{(fw.steps||[]).length}步</span></div>
|
|
|
+ <div className="src">原帖:{p.title}</div>
|
|
|
+ </div>;
|
|
|
+ })}
|
|
|
+ </div>;
|
|
|
}
|
|
|
|
|
|
-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
|
|
|
- ? <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>}
|
|
|
+function Step({s,i}){
|
|
|
+ return <div className="step">
|
|
|
+ <div className="step-h">
|
|
|
+ <span className="step-n">步骤{i+1}</span>
|
|
|
+ <span className="step-intent"><span className="vlabel">目的:</span>{s.intent}</span>
|
|
|
+ </div>
|
|
|
+ <div className="step-tags">
|
|
|
+ {s["阶段"]&&<span className="chip c-stage">阶段·{s["阶段"]}</span>}
|
|
|
+ {s["类型"]&&<span className="chip c-type">类型·{s["类型"]}</span>}
|
|
|
+ <Scopes list={s["作用域"]}/>
|
|
|
+ {s["出处"]&&s["出处"].length>0 && <span className="from">来源 {s["出处"].join(" · ")}</span>}
|
|
|
</div>
|
|
|
+ <div className="rowlab"><span className="rl rl-how">指引</span><div className="directive">{s.directive}</div></div>
|
|
|
+ {s.output && <div className="rowlab" style={{paddingBottom:12}}><span className="rl rl-out">产出</span><div className="outtxt">{s.output}</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>}
|
|
|
+function Detail({post,onZoom,onRecord}){
|
|
|
+ if(!post) return <div className="empty">← 左边选一颗知识</div>;
|
|
|
+ const fw=post.framework||{};
|
|
|
+ const imgs=[]; for(let n=1;n<=(post.img_count||0);n++) imgs.push(post.img_base+"/image_"+n+".webp");
|
|
|
+ return <div>
|
|
|
+ <div className="dh">
|
|
|
+ <h1>{fw.title}</h1>
|
|
|
+ <div className="acts">
|
|
|
+ <button className="minib" onClick={()=>onRecord(post)}>📥 下游记录</button>
|
|
|
+ <a href={post.url} target="_blank" rel="noreferrer">🔗 原帖</a>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ <div className="ptitle">原帖:{post.title} · 小红书</div>
|
|
|
+ <div className="meta"><Tags stage={fw["阶段跨度"]} type={fw["知识类型"]} scopes={fw["作用域并集"]}/></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 className="block">
|
|
|
+ <span className="flab">目标</span>
|
|
|
+ <div className="purpose">{fw.purpose}</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
|
|
|
- ? <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>
|
|
|
+ {fw["想要"] && <div className="block"><span className="flab">想要</span><div className="want">{fw["想要"]}</div></div>}
|
|
|
+
|
|
|
+ <div className="block">
|
|
|
+ <span className="flab">步骤({(fw.steps||[]).length})· 目的 / 指引 / 产出</span>
|
|
|
+ {(fw.steps||[]).map((s,i)=><Step key={i} s={s} i={i}/>)}
|
|
|
</div>
|
|
|
+
|
|
|
+ {post.dropped&&post.dropped.length>0 && <details className="box">
|
|
|
+ <summary>被剔除的「制作 / 素材」({post.dropped.length})— 不算创作知识</summary>
|
|
|
+ <div className="bd">{post.dropped.map((d,i)=><div className="drop" key={i}>• {typeof d==="string"?d:(d.item||JSON.stringify(d))}{d.reason?(" — "+d.reason):""}</div>)}</div>
|
|
|
+ </details>}
|
|
|
+
|
|
|
+ <details className="box">
|
|
|
+ <summary>原帖图片({imgs.length})· 知识从这里提炼</summary>
|
|
|
+ <div className="imgs">{imgs.map((u,i)=><img key={i} src={u} onClick={()=>onZoom(u)} onError={e=>e.target.style.display='none'}/>)}</div>
|
|
|
+ </details>
|
|
|
</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=> 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>
|
|
|
+function RecordModal({post,onClose}){
|
|
|
+ const fw=post.framework||{};
|
|
|
+ const content=buildContent(fw);
|
|
|
+ const record={ title: fw.title, purpose: fw.purpose, content };
|
|
|
+ return <div className="ov" onClick={onClose}>
|
|
|
+ <div className="modal" onClick={e=>e.stopPropagation()}>
|
|
|
+ <h2>下游消费记录(这颗知识下发给「脚本构建 Agent」的样子)<span className="x" onClick={onClose}>×</span></h2>
|
|
|
+ <div className="mbody">
|
|
|
+ <div className="fk">title</div><div className="fv">{record.title}</div>
|
|
|
+ <div className="fk">purpose</div><div className="fv">{record.purpose}</div>
|
|
|
+ <div className="fk">content(拍平后的全文,下游逐字消费)</div>
|
|
|
+ <div className="cwrap"><pre>{content}</pre></div>
|
|
|
</div>
|
|
|
- <div className="kv" style={{marginTop:6}}>判断理由</div>
|
|
|
- <div>{scr.reason||"—"}</div>
|
|
|
- </Stage>
|
|
|
+ </div>
|
|
|
+ </div>;
|
|
|
+}
|
|
|
|
|
|
- <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>
|
|
|
+function PromptModal({onClose}){
|
|
|
+ return <div className="ov" onClick={onClose}>
|
|
|
+ <div className="modal" onClick={e=>e.stopPropagation()}>
|
|
|
+ <h2>这套知识是怎么提取出来的(提示词)<span className="x" onClick={onClose}>×</span></h2>
|
|
|
+ <div className="mbody"><pre>{PROMPT}</pre></div>
|
|
|
+ </div>
|
|
|
</div>;
|
|
|
}
|
|
|
|
|
|
@@ -281,18 +245,24 @@ 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);}); },[]);
|
|
|
+ const [pm,setPm]=useState(false);
|
|
|
+ const [rec,setRec]=useState(null);
|
|
|
+ useEffect(()=>{ j("/frameworks.json").then(d=>{const ps=d.posts||[];setPosts(ps); if(ps[0])setSel(ps[0].post_id);}); },[]);
|
|
|
+ const post=posts.find(p=>p.post_id===sel);
|
|
|
return <div>
|
|
|
- <header>创作知识 <span className="sub">· 帖子知识流水线({posts.length} 帖)</span></header>
|
|
|
+ <header>
|
|
|
+ <h0>创作知识的搜索和解构</h0>
|
|
|
+ <span className="sub">· 一帖一颗完整框架({posts.length} 颗)· 已按下游消费格式呈现</span>
|
|
|
+ <button className="pbtn" onClick={()=>setPm(true)}>📄 提取提示词</button>
|
|
|
+ </header>
|
|
|
+ <Legend/>
|
|
|
<div className="wrap">
|
|
|
<PostList posts={posts} sel={sel} onSel={setSel}/>
|
|
|
- <div className="main"><Pipeline postId={sel} onZoom={setZoom} prompts={prompts} onPrompt={setPm}/></div>
|
|
|
+ <div className="main">{posts.length? <Detail post={post} onZoom={setZoom} onRecord={setRec}/> : <div className="empty">加载中…</div>}</div>
|
|
|
</div>
|
|
|
{zoom&&<div className="lb" onClick={()=>setZoom(null)}><img src={zoom}/></div>}
|
|
|
- {pm&&<PromptModal item={prompts[pm]} onClose={()=>setPm(null)}/>}
|
|
|
+ {pm&&<PromptModal onClose={()=>setPm(false)}/>}
|
|
|
+ {rec&&<RecordModal post={rec} onClose={()=>setRec(null)}/>}
|
|
|
</div>;
|
|
|
}
|
|
|
ReactDOM.createRoot(document.getElementById("root")).render(<App/>);
|