const basicAi=useStreamComplete();
const assetsAi=useStreamComplete();
const cubesAi=useStreamComplete();
+ const [genIds,setGenIds]=useState<Set<number>>(new Set());
+ const [genLog,setGenLog]=useState("");
const {confirm, el:confirmEl}=useConfirm();
const domainName = doms.find((d:any)=>d.id===m?.domain_id)?.name || "";
if(cubesAi.completing) return;
cubesAi.setCompleting(true); cubesAi.setLog("");
const wd=setTimeout(()=>{ cubesAi.setCompleting(false); notify("AI Cube 补全超时(>6 分钟),请重试"); },360000);
+ let failed=""; let items:any[]=[];
try{
const res=await fetch(`/api/db/domain-templates/${id}/complete-cubes`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:m.name,domain:domainName,description:m.description,supplement:m.supplement,assets:assets.map(a=>({type:a.device_name,count:a.count,rule:a.rule}))})});
if(!res.ok||!res.body) throw new Error("HTTP "+res.status);
- await streamInto(res, cubesAi.setLog, (ev)=>{ if(Array.isArray(ev.result)){ for(const it of ev.result) fetchDb(`/domain-templates/${id}/pages`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:it.name,description:it.content||"",json_text:"{}"})}).catch(()=>{}); } });
+ await streamInto(res, cubesAi.setLog, (ev)=>{ if(Array.isArray(ev.result)) items=ev.result; if(ev?.status==="failed") failed=ev?.message||"AI 分析失败"; });
+ if(failed){ notify("AI Cube 补全失败:"+failed); return; }
+ let added=0;
+ for(const it of items){ try{ await fetchDb(`/domain-templates/${id}/pages`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:it.name,description:it.content||"",json_text:"{}"})}); added++; }catch(e:any){ notify(`新增页面「${it.name||""}」失败:${e.message}`); } }
await load(true);
- notify("AI Cube 页面补全已完成");
+ notify(items.length? `AI Cube 页面补全已完成,已新增 ${added} 个页面` : "AI 未返回可用页面");
}catch(e:any){ notify("AI Cube 补全失败:"+(e?.message||e)); }
finally{ clearTimeout(wd); cubesAi.setCompleting(false); }
};
};
const savePageMeta=async(p:DomainPage)=>{ await fetchDb(`/domain-pages/${p.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:p.name,description:p.description,json_text:p.json_text})}); notify("页面已保存"); };
const deletePage=async(p:DomainPage)=>{ if(!(await confirm(`删除页面「${p.name}」?`))) return; await fetchDb(`/domain-pages/${p.id}`,{method:"DELETE"}); await load(true); };
- const generatePage=async(p:DomainPage)=>{ notify(`开始生成「${p.name}」…`); try{ await fetch(`/api/db/domain-templates/${id}/pages/${p.id}/generate`,{method:"POST"}); await load(true); notify(`「${p.name}」生成完成`); }catch(e:any){ notify("生成失败:"+e.message); await load(true); } };
+ const generatePage=async(p:DomainPage)=>{
+ if(genIds.has(p.id)) return;
+ setGenIds(prev=>new Set(prev).add(p.id)); setGenLog("");
+ notify(`开始生成「${p.name}」…`);
+ let failed="";
+ try{
+ const res=await fetch(`/api/db/domain-templates/${id}/pages/${p.id}/generate`,{method:"POST"});
+ if(!res.ok||!res.body) throw new Error("HTTP "+res.status);
+ await streamInto(res, setGenLog, (ev)=>{ if(ev?.status==="failed") failed=ev?.message||"生成失败"; });
+ if(failed) notify(`「${p.name}」生成失败:${failed}`);
+ else notify(`「${p.name}」生成完成`);
+ }catch(e:any){ notify("生成失败:"+(e?.message||e)); }
+ finally{ setGenIds(prev=>{ const n=new Set(prev); n.delete(p.id); return n; }); await load(true); }
+ };
const downloadPage=(p:DomainPage)=>{ const blob=new Blob([p.json_text||"{}"],{type:"application/json"}); downloadBlob(blob, p.gen_filename||`${p.name||"page"}.json`); };
if(loading||!t||!m) return <p>加载中…</p>;
{step==="functions" && <FunctionsStep
muTree={muTreeLocal} grouped={grouped} effSelected={effSelected} onToggleMu={toggleMu}
pages={pages} onAddPage={addPage} onUploadPageJson={uploadPageJson} onSavePageMeta={savePageMeta} onDeletePage={deletePage} onGeneratePage={generatePage} onDownloadPage={downloadPage} onPreviewPage={setPreviewPage}
+ genIds={genIds} genLog={genLog}
cubesAi={cubesAi} onRunCubesComplete={runCubesComplete} onViewCompleteCubesPrompt={()=>viewCompleteCubesPrompt(id,m.name,domainName,m.description,m.supplement||"",assets.map(a=>({type:a.device_name,count:a.count,rule:a.rule})))}
updatePageField={(i,patchRow)=>setPages(prev=>prev.map((p,idx)=>idx===i?{...p,...patchRow}:p))}
tName={m.name} saving={saving} notify={notify} />}
return <Panel title={title}><div className="placeholder-block"><span className="badge todo">待开发</span><p>{note}</p></div></Panel>;
}
-function FunctionsStep({muTree,grouped,effSelected,onToggleMu,pages,onAddPage,onUploadPageJson,onSavePageMeta,onDeletePage,onGeneratePage,onDownloadPage,onPreviewPage,cubesAi,onRunCubesComplete,onViewCompleteCubesPrompt,updatePageField,tName,saving,notify}:{muTree:MuNode[];grouped:any[];effSelected:string[];onToggleMu:(k:string)=>void;pages:DomainPage[];onAddPage:()=>Promise<void>;onUploadPageJson:(f:File)=>Promise<void>;onSavePageMeta:(p:DomainPage)=>Promise<void>;onDeletePage:(p:DomainPage)=>Promise<void>;onGeneratePage:(p:DomainPage)=>Promise<void>;onDownloadPage:(p:DomainPage)=>void;onPreviewPage:(p:DomainPage)=>void;cubesAi:ReturnType<typeof useStreamComplete>;onRunCubesComplete:()=>Promise<void>;onViewCompleteCubesPrompt:()=>void;updatePageField:(i:number,patch:Partial<DomainPage>)=>void;tName:string;saving:boolean;notify:(m:string)=>void}){
+function FunctionsStep({muTree,grouped,effSelected,onToggleMu,pages,onAddPage,onUploadPageJson,onSavePageMeta,onDeletePage,onGeneratePage,onDownloadPage,onPreviewPage,genIds,genLog,cubesAi,onRunCubesComplete,onViewCompleteCubesPrompt,updatePageField,tName,saving,notify}:{muTree:MuNode[];grouped:any[];effSelected:string[];onToggleMu:(k:string)=>void;pages:DomainPage[];onAddPage:()=>Promise<void>;onUploadPageJson:(f:File)=>Promise<void>;onSavePageMeta:(p:DomainPage)=>Promise<void>;onDeletePage:(p:DomainPage)=>Promise<void>;onGeneratePage:(p:DomainPage)=>Promise<void>;onDownloadPage:(p:DomainPage)=>void;onPreviewPage:(p:DomainPage)=>void;genIds:Set<number>;genLog:string;cubesAi:ReturnType<typeof useStreamComplete>;onRunCubesComplete:()=>Promise<void>;onViewCompleteCubesPrompt:()=>void;updatePageField:(i:number,patch:Partial<DomainPage>)=>void;tName:string;saving:boolean;notify:(m:string)=>void}){
const sourceBadge=(src?:string|null)=>{
if(src==="upload") return <span className="badge src-upload">↑ 上传</span>;
if(src==="ai") return <span className="badge src-ai">✦ AI生成</span>;
return <span className="badge src-manual">手动</span>;
};
const [tab,setTab]=useState<"mu"|"cube"|"menu">("mu");
+ const genLogRef=useRef<HTMLPreElement>(null);
+ useEffect(()=>{ if(genLogRef.current) genLogRef.current.scrollTop=genLogRef.current.scrollHeight; },[genLog]);
return <Panel><div className="tabs"><button className={tab==="mu"?"active":""} onClick={()=>setTab("mu")}>MU 标准页面 <i>{effSelected.length}</i></button><button className={tab==="cube"?"active":""} onClick={()=>setTab("cube")}>Cube 扩展页面 <i>{pages.length}</i></button><button className={tab==="menu"?"active":""} onClick={()=>setTab("menu")}>菜单结构</button></div>
{tab==="mu" && <div className="mu-tree">{grouped.map(g=><div key={g.parentMenu} className="mu-group"><b>{g.parentMenu}</b><div className="mu-items">{g.items.map((item:any)=><label key={item.key} className={item.disabled?"disabled":""}><input type="checkbox" disabled={item.disabled} checked={effSelected.includes(item.key)} onChange={()=>onToggleMu(item.key)}/><span>{item.label}</span></label>)}</div></div>)}</div>}
{tab==="cube" && <div className="two-column">
<td><input value={p.description} placeholder="描述该页面要展示的内容与指标" onChange={e=>updatePageField(i,{description:e.target.value})}/></td>
<td>{sourceBadge(p.source)}</td>
<td>{p.gen_filename? <button className="link" onClick={()=>onDownloadPage(p)}>下载 {p.gen_filename}</button> : <span className="muted">未生成</span>}</td>
- <td className="cube-actions"><button onClick={()=>onPreviewPage(p)}>预览</button><button onClick={()=>onGeneratePage(p)}>生成</button><button onClick={()=>onSavePageMeta(p)}>保存</button><button className="trash" onClick={()=>onDeletePage(p)}>删除</button></td>
+ <td className="cube-actions"><button onClick={()=>onPreviewPage(p)}>预览</button><button disabled={genIds.has(p.id)} onClick={()=>onGeneratePage(p)}>{genIds.has(p.id)?"生成中…":"生成"}</button><button onClick={()=>onSavePageMeta(p)}>保存</button><button className="trash" onClick={()=>onDeletePage(p)}>删除</button></td>
</tr>)}</tbody></table></div>
<div style={{display:"flex",gap:8,marginTop:8}}><Button onClick={onAddPage}>+ 添加 Cube 页面</Button><label className="button secondary" style={{cursor:"pointer"}}>↑ 上传页面 Json<input type="file" accept=".json,application/json" hidden onChange={e=>{const f=e.target.files?.[0]; if(f) onUploadPageJson(f); e.target.value="";}}/></label></div>
+ {(genIds.size>0||genLog)&&<div className="modal-console"><div className="modal-console-head">AI 生成页面</div><pre className="modal-console-body" ref={genLogRef}>{genLog||"(等待生成…)"}</pre></div>}
</div>
<div className="stack">
<Panel title="填写建议"><ul className="tips"><li>从方案业务与设备资产出发,规划监控维度(综合总览、能耗能效、设备健康、报警事件、环境监测等)。</li><li>页面名称简洁,不超过 6 个汉字;描述写清页面目标与关键指标。</li><li>每个 Cube 页面可「生成」(AI 创建 JSON)或「上传页面 Json」(使用已有 JSON)。</li><li>已生成页面可点击「预览」查看效果,并在预览中提交修改意见重新生成。</li></ul></Panel>