Преглед изворни кода

前端各环节加「提示词」按钮 + 弹窗

- api.py 新增 GET /api/prompts:返回 extract/screen/split/deconstruct 的 system+user+model
- web:筛选环节加「提示词」按钮;知识点环节加「拆分提示词」「解构提示词」按钮
- 点击弹窗展示该环节 system 角色 + user 输入模板(多模态提取标注未外置)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lisihan пре 1 месец
родитељ
комит
20d8e47cdf
2 измењених фајлова са 77 додато и 9 уклоњено
  1. 36 3
      creation_knowledge/api.py
  2. 41 6
      web/index.html

+ 36 - 3
creation_knowledge/api.py

@@ -14,16 +14,29 @@ from fastapi import FastAPI, HTTPException
 from fastapi.middleware.cors import CORSMiddleware
 from fastapi.staticfiles import StaticFiles
 
-from creation_knowledge.config import PgConfig
+from creation_knowledge.config import PgConfig, Settings
+from creation_knowledge.integrations import extractor as extractor_mod
 from creation_knowledge.integrations.db import CkStore
+from creation_knowledge.prompts import PROMPT_VERSION, load_prompt
+from creation_knowledge.stages import deconstruct as decon_stage
+from creation_knowledge.stages import screen as screen_stage
+from creation_knowledge.stages import split as split_stage
 
 WEB_DIR = Path(__file__).resolve().parent.parent / "web"
 
 
+def _env_file() -> str:
+    return os.getenv("CK_ENV_FILE", ".env")
+
+
 @lru_cache(maxsize=1)
 def _store() -> CkStore:
-    env_file = os.getenv("CK_ENV_FILE", ".env")
-    return CkStore(PgConfig.from_env(env_file))
+    return CkStore(PgConfig.from_env(_env_file()))
+
+
+@lru_cache(maxsize=1)
+def _settings() -> Settings:
+    return Settings.from_env(_env_file())
 
 
 app = FastAPI(title="创作知识")
@@ -50,6 +63,26 @@ def get_items(post_id: str) -> list[dict]:
     return _store().read_items(post_id)
 
 
+@app.get("/api/prompts")
+def get_prompts() -> dict:
+    """各环节用到的提示词(system + user 模板),供前端弹窗查看。"""
+    s = _settings()
+    return {
+        "version": PROMPT_VERSION,
+        "items": [
+            {"key": "extract", "label": "多模态提取", "model": s.video_model,
+             "system": extractor_mod._SYSTEM_PROMPT, "user": extractor_mod._USER_TMPL,
+             "note": "文本+图片+视频提取知识;提示词写在 extractor.py,未外置到 prompts/"},
+            {"key": "screen", "label": "筛选", "model": s.llm_model,
+             "system": screen_stage.SYSTEM, "user": load_prompt("screen")},
+            {"key": "split", "label": "拆分", "model": s.llm_model,
+             "system": split_stage.SYSTEM, "user": load_prompt("split")},
+            {"key": "deconstruct", "label": "解构", "model": s.llm_model,
+             "system": decon_stage.SYSTEM, "user": load_prompt("deconstruct")},
+        ],
+    }
+
+
 # 单页前端挂在最后(catch-all),不影响上面的 /api 路由
 if WEB_DIR.exists():
     app.mount("/", StaticFiles(directory=str(WEB_DIR), html=True), name="web")

+ 41 - 6
web/index.html

@@ -75,6 +75,16 @@
   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}
 </style>
 </head>
 <body>
@@ -102,11 +112,27 @@ function PostList({posts,sel,onSel}){
   </div>;
 }
 
-function Stage({n,title,sub,children}){
-  return <div className="stage"><h3><span className="n">{n}</span>{title}</h3>
+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 KPCard({it,idx}){
   const item=it.item||{}, deco=it.deconstruction||{}, payload=it.ingest_payload||{};
   const types=(item.knowledge_types||[]).map(t=>t.toUpperCase()).join(" + ");
@@ -149,7 +175,7 @@ function KPCard({it,idx}){
   </div>;
 }
 
-function Pipeline({postId,onZoom}){
+function Pipeline({postId,onZoom,prompts,onPrompt}){
   const [post,setPost]=useState(null);
   const [items,setItems]=useState([]);
   useEffect(()=>{ if(!postId)return; setPost(null);setItems([]);
@@ -171,7 +197,8 @@ function Pipeline({postId,onZoom}){
         onClick={()=>onZoom(u)} onError={e=>e.target.style.display='none'}/>)}</div>
     </Stage>
 
-    <Stage n="2" title="筛选" sub="判断这篇帖子值不值得提取成创作知识">
+    <Stage n="2" title="筛选" sub="判断这篇帖子值不值得提取成创作知识"
+      extra={prompts.screen && <PromptBtn label="提示词" onClick={()=>onPrompt('screen')}/>}>
       <div className="crit">
         <div className="t">筛选标准(4 条都满足才通过)</div>
         <ul>
@@ -190,7 +217,11 @@ function Pipeline({postId,onZoom}){
     </Stage>
 
     <Stage n="3" title={"知识点("+items.length+" 条)"}
-      sub="每条 = 知识类型 + What/Why/How + 对应阶段 + 作用域">
+      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}/>) : <div className="kv">无(被淘汰或未拆出)</div>}
     </Stage>
   </div>;
@@ -200,14 +231,18 @@ 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>创作知识 <span className="sub">· 帖子知识流水线({posts.length} 帖)</span></header>
     <div className="wrap">
       <PostList posts={posts} sel={sel} onSel={setSel}/>
-      <div className="main"><Pipeline postId={sel} onZoom={setZoom}/></div>
+      <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/>);