setModal("completePrompt");
}catch(e:any){ notify("读取提示词失败: "+e.message); }
};
+ const viewCompleteCubesPrompt=async()=>{
+ if(!site?.id) { notify("请先创建或打开一个站点"); return; }
+ try{
+ const qs=new URLSearchParams({name:site.name||"",domain:site.domain||"",description:site.description||"",supplement:site.supplement||""}).toString();
+ const data:{prompt:string}=await fetchDb(`/sites/${site.id}/complete-cubes-prompt?${qs}`);
+ setPromptData({name:"AI Cube 页面补全", text:data.prompt});
+ setModal("completePrompt");
+ }catch(e:any){ notify("读取提示词失败: "+e.message); }
+ };
const openFeedback=(cube:Cube)=>{
if(!cube.id){ notify("请先生成该页面再提出修改意见"); return; }
setFeedbackCube(cube);
{page==="styleLibrary"&&<PageStyleLibrary pageStyles={pageStyles} loadPageStyles={loadPageStyles} notify={notify}/>}
{page==="basic"&&<Basic site={site} domains={domains} onChange={saveSiteBasic} onAddDomain={addDomain} onUpload={uploadFile} saving={saving} notify={notify} onViewCompletePrompt={viewCompletePrompt}/>}
{page==="assets"&&<Assets site={site} onChange={saveAssets} saving={saving} notify={notify} onViewCompletePrompt={viewCompleteAssetsPrompt}/>}
- {page==="functions"&&<Functions site={site} muTree={muTree} onMuChange={saveMu} onSiteChange={setSite} pageStyles={pageStyles} notify={notify}/>}
+ {page==="functions"&&<Functions site={site} muTree={muTree} onMuChange={saveMu} onSiteChange={setSite} pageStyles={pageStyles} notify={notify} onViewCompletePrompt={viewCompleteCubesPrompt}/>}
{page==="generate"&&<Generate site={site} onSiteChange={setSite} notify={notify} viewPrompt={viewPrompt} viewSitePrompt={viewSitePrompt}/>}
{page==="result"&&<Result site={site} go={go} setModal={setModal} viewPrompt={viewPrompt} notify={notify} openFeedback={openFeedback} viewSitePrompt={viewSitePrompt}/>}
{page==="preview"&&<Preview site={site} tab={previewTab} setTab={setPreviewTab} setModal={setModal} notify={notify}/>}
</div>;
}
-function Functions({site,muTree,onMuChange,onSiteChange,pageStyles,notify}:{site:SiteDetail|null;muTree:MuNode[];onMuChange:(s:string[])=>void;onSiteChange:(s:SiteDetail)=>void;pageStyles:PageStyle[];notify:(m:string)=>void}){
+function Functions({site,muTree,onMuChange,onSiteChange,pageStyles,notify,onViewCompletePrompt}:{site:SiteDetail|null;muTree:MuNode[];onMuChange:(s:string[])=>void;onSiteChange:(s:SiteDetail)=>void;pageStyles:PageStyle[];notify:(m:string)=>void;onViewCompletePrompt:()=>void}){
const [tab,setTab]=useState<"mu"|"cube"|"menu">("mu");
- const selectedMu = site?.mu || [];
const leaves = useMemo(()=>buildMuLeaves(muTree),[muTree]);
const grouped = useMemo(()=>{
const map = new Map<string,{parentMenu:string, items:typeof leaves}>();
}
return Array.from(map.values());
},[leaves]);
+ const allMuKeys = useMemo(()=>leaves.map(l=>l.key),[leaves]);
+ const selectedMu = site?.mu || [];
+ // 默认全选中:site.mu 为空时按全部叶子键显示,首次勾选才落地保存
+ const effSelected = selectedMu.length ? selectedMu : allMuKeys;
if(!site) return <Panel><div className="empty-state">加载中…</div></Panel>;
const toggleMu=(key:string)=>{
- onMuChange(selectedMu.includes(key)?selectedMu.filter(k=>k!==key):[...selectedMu,key]);
+ const base = selectedMu.length ? selectedMu : allMuKeys;
+ onMuChange(base.includes(key)? base.filter(k=>k!==key) : [...base, key]);
};
const updateCube=(idx:number, patch:Partial<Cube>)=>{
const next = site.cubes.map((c,i)=>i===idx?{...c,...patch}:c);
notify(`预览 ${cube.name}`);
};
- // menu editor state
- const [menu, setMenu] = useState<MenuItem[]>([]);
- useEffect(()=>{ setMenu(site.menu.length?site.menu:[{parent_id:null,label:"站点监控",page_type:"",ref_id:"",sort_order:0}]); },[site?.menu]);
- const saveMenu=async()=>{
+ // ===== AI Cube 页面补全 =====
+ const [completing,setCompleting]=useState(false);
+ const [completeLog,setCompleteLog]=useState("");
+ const completeConsoleRef=useRef<HTMLPreElement>(null);
+ useEffect(()=>{ if(completeConsoleRef.current) completeConsoleRef.current.scrollTop=completeConsoleRef.current.scrollHeight; },[completeLog]);
+ const appendCubes=(items:Array<{name:string;content:string}>)=>{
+ const base = site?.cubes || [];
+ const next = [...base, ...items.map((it,i)=>({name:it.name, content:it.content||"", status:"pending", sort_order: base.length+i}))];
+ onSiteChange({...site, cubes:next});
+ notify(`AI 已补充 ${items.length} 个 Cube 页面(位于您已填项下方)`);
+ };
+ const runCompleteCubes=async()=>{
+ if(!site?.id || completing) return;
+ setCompleting(true); setCompleteLog("");
+ const watchdog=setTimeout(()=>{ setCompleting(false); notify("AI Cube 补全超时(>6 分钟),请重试"); }, 360000);
try{
- await fetchDb(`/sites/${site.id}/menu`, {method:"PUT", headers:{"Content-Type":"application/json"}, body:JSON.stringify({menu})});
- onSiteChange({...site, menu});
- notify("菜单结构已保存");
- }catch(e:any){ notify("菜单保存失败: "+e.message); }
+ const res=await fetch(`/api/db/sites/${site.id}/complete-cubes`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:site.name,domain:site.domain,description:site.description,supplement:site.supplement,assets:site.assets})});
+ if(!res.ok||!res.body) throw new Error("HTTP "+res.status);
+ const reader=res.body.getReader(); const dec=new TextDecoder(); let buf="";
+ for(;;){
+ const {done,value}=await reader.read(); if(done) break;
+ buf+=dec.decode(value,{stream:true});
+ const lines=buf.split("\n"); buf=lines.pop()||"";
+ for(const line of lines){
+ if(!line.trim()) continue;
+ try{
+ const ev=JSON.parse(line);
+ if(ev.log) setCompleteLog(prev=>prev+ev.log);
+ if(Array.isArray(ev.result)) appendCubes(ev.result);
+ }catch{}
+ }
+ }
+ notify("AI Cube 页面补全已完成");
+ }catch(e:any){ notify("AI Cube 页面补全失败:"+(e?.message||e)); }
+ finally{ clearTimeout(watchdog); setCompleting(false); }
};
- return <Panel><div className="tabs"><button className={tab==="mu"?"active":""} onClick={()=>setTab("mu")}>MU 标准页面 <i>{selectedMu.length}</i></button><button className={tab==="cube"?"active":""} onClick={()=>setTab("cube")}>Cube 扩展页面 <i>{site.cubes.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=><label key={item.key} className={item.disabled?"disabled":""} title={item.note}><input type="checkbox" disabled={item.disabled} checked={selectedMu.includes(item.key)} onChange={()=>toggleMu(item.key)}/><span>{item.label}{item.note&&<small>{item.note}</small>}</span></label>)}</div></div>)}</div>}
- {tab==="cube"&&<div className="cube-editor"><table className="data-table editable"><thead><tr><th>页面名称 <small>最多6字</small></th><th>页面内容</th><th>生成文件</th><th>操作</th></tr></thead><tbody>{site.cubes.map((cube,i)=><tr key={i}>
- <td><input value={cube.name} maxLength={6} placeholder="例如:运行总览" onChange={e=>updateCube(i,{name:e.target.value})}/></td>
- <td><input value={cube.content} placeholder="描述该页面要展示的内容和指标" onChange={e=>updateCube(i,{content:e.target.value})}/></td>
- <td>{cube.status==="generated"? <button className="link" onClick={()=>downloadCube(cube)}>下载 {cube.gen_filename}</button> : <span className="muted">未生成</span>}</td>
- <td className="cube-actions"><button onClick={()=>previewCube(cube)}>预览</button><button onClick={()=>generateCube(i)}>生成</button><button onClick={()=>saveCube(i)}>保存</button><button className="trash" onClick={()=>removeCube(i)}>删除</button></td>
- </tr>)}</tbody></table><Button onClick={addCube}>+ 添加 Cube 页面</Button></div>}
- {tab==="cube"&&<div className="style-picker">
- <h4>站点页面样式 <small>站点级 · 全部 Cube 页面共用</small></h4>
- <p className="hint">选择一个样式库方案作为本站点所有 Cube 页面的视觉风格(生成时参考其 JSON 主题参数)。</p>
- <div className="style-pick-list">
- <button className={"style-pick"+(!site.style_id?" active":"")} onClick={()=>selectStyle(null)}>
- <div className="style-pick-thumb none">无</div><span>不使用样式</span>
- </button>
- {pageStyles.map(s=><button key={s.id} className={"style-pick"+(site.style_id===s.id?" active":"")} onClick={()=>selectStyle(s.id)}>
- <div className="style-pick-thumb"><img src={`/api/db/page-styles/${s.id}/image`} alt={s.name} loading="lazy"/></div>
- <span>{s.name}</span>
- </button>)}
+ 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>{site.cubes.length}</i></button><button className={tab==="menu"?"active":""} onClick={()=>setTab("menu")}>菜单结构</button></div>
+ {/* MU 标准页面:树状上下结构,仅保留勾选框,默认全选中 */}
+ {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=><label key={item.key} className={item.disabled?"disabled":""}><input type="checkbox" disabled={item.disabled} checked={effSelected.includes(item.key)} onChange={()=>toggleMu(item.key)}/><span>{item.label}</span></label>)}</div></div>)}</div>}
+ {/* Cube 扩展页面:左右两栏,右栏为填写建议 + AI 智能补全 */}
+ {tab==="cube"&&<div className="two-column">
+ <div>
+ <div className="cube-editor"><table className="data-table editable"><thead><tr><th>页面名称 <small>最多6字</small></th><th>页面内容</th><th>生成文件</th><th>操作</th></tr></thead><tbody>{site.cubes.map((cube,i)=><tr key={i}>
+ <td><input value={cube.name} maxLength={6} placeholder="例如:运行总览" onChange={e=>updateCube(i,{name:e.target.value})}/></td>
+ <td><input value={cube.content} placeholder="描述该页面要展示的内容和指标" onChange={e=>updateCube(i,{content:e.target.value})}/></td>
+ <td>{cube.status==="generated"? <button className="link" onClick={()=>downloadCube(cube)}>下载 {cube.gen_filename}</button> : <span className="muted">未生成</span>}</td>
+ <td className="cube-actions"><button onClick={()=>previewCube(cube)}>预览</button><button onClick={()=>generateCube(i)}>生成</button><button onClick={()=>saveCube(i)}>保存</button><button className="trash" onClick={()=>removeCube(i)}>删除</button></td>
+ </tr>)}</tbody></table><Button onClick={addCube}>+ 添加 Cube 页面</Button></div>
+ <div className="style-picker">
+ <h4>站点页面样式 <small>站点级 · 全部 Cube 页面共用</small></h4>
+ <p className="hint">选择一个样式库方案作为本站点所有 Cube 页面的视觉风格(生成时参考其 JSON 主题参数)。</p>
+ <div className="style-pick-list">
+ <button className={"style-pick"+(!site.style_id?" active":"")} onClick={()=>selectStyle(null)}>
+ <div className="style-pick-thumb none">无</div><span>不使用样式</span>
+ </button>
+ {pageStyles.map(s=><button key={s.id} className={"style-pick"+(site.style_id===s.id?" active":"")} onClick={()=>selectStyle(s.id)}>
+ <div className="style-pick-thumb"><img src={`/api/db/page-styles/${s.id}/image`} alt={s.name} loading="lazy"/></div>
+ <span>{s.name}</span>
+ </button>)}
+ </div>
+ </div>
+ </div>
+ <div className="stack">
+ <Panel title="填写建议"><ul className="tips"><li>从站点业务与设备资产出发,规划需要的监控维度(综合总览、能耗能效、设备健康、报警事件、环境监测、生产产量等)。</li><li>每个 Cube 页面写明:页面目标、关键指标/图表、数据来源与模拟数据构造思路。</li><li>页面名称简洁,不超过 6 个汉字。</li><li>AI 补充的页面会追加在您已创建页面下方,不会改动您原有的内容。</li></ul></Panel>
+ <Panel title="AI 智能补全">
+ <p className="hint">基于站点名称 / 领域 / 描述 / 补充要求,以及设备资产层级,由 AI 分析并补充站点所需的 Cube 扩展页面(页面名称 + 详细内容),追加在您已建页面下方。</p>
+ <div style={{display:"flex",gap:"8px",alignItems:"center"}}>
+ <button className="button" disabled={completing} onClick={runCompleteCubes}>{completing?"补全中…":"开始补全"}</button>
+ <button className="text-button" onClick={onViewCompletePrompt}>查看提示词</button>
+ </div>
+ {(completing||completeLog)&&<div className="modal-console"><div className="modal-console-head">AI 智能补全</div><pre className="modal-console-body" ref={completeConsoleRef}>{completeLog||"(等待 AI 分析…)"}</pre></div>}
+ </Panel>
+ </div>
+ </div>}
+ {/* 菜单结构:只读默认预览(MU 在上,Cube 全部归入「综合监控」一级菜单) */}
+ {tab==="menu"&&<div className="menu-preview">
+ <p className="hint">默认菜单结构(展示用,无需编辑):MU 标准页面在上,所有 Cube 扩展页面归入「综合监控」一级菜单下。</p>
+ <div className="menu-tree">
+ <div className="menu-root">⌄ {site.name || "未命名站点"}</div>
+ <div className="menu-section">
+ <div className="menu-section-title">MU 标准页面</div>
+ {leaves.filter(l=>effSelected.includes(l.key)).map(l=><div key={l.key} className="menu-leaf">· {l.parentMenu}/{l.label}</div>)}
+ {leaves.filter(l=>effSelected.includes(l.key)).length===0&&<div className="menu-leaf muted">(未选择 MU 页面)</div>}
+ </div>
+ <div className="menu-section">
+ <div className="menu-section-title">综合监控</div>
+ {site.cubes.filter(c=>c.name&&c.name.trim()).map(c=><div key={c.id||c.name} className="menu-leaf cube">· {c.name}</div>)}
+ {site.cubes.filter(c=>c.name&&c.name.trim()).length===0&&<div className="menu-leaf muted">(暂无 Cube 页面)</div>}
+ </div>
</div>
</div>}
- {tab==="menu"&&<div className="menu-editor"><p className="hint">配置站点菜单结构,可选择引用 MU 标准页或 Cube 扩展页。</p>{menu.map((m,i)=><div key={i} className="menu-row"><input value={m.label} placeholder="菜单名称" onChange={e=>{const next=[...menu]; next[i]={...next[i],label:e.target.value}; setMenu(next);}}/>
- <select value={m.page_type||""} onChange={e=>{const next=[...menu]; next[i]={...next[i],page_type:e.target.value as any}; setMenu(next);}}><option value="">分组</option><option value="mu">MU 页面</option><option value="cube">Cube 页面</option></select>
- {m.page_type==="mu"&&<select value={String(m.ref_id)} onChange={e=>{const next=[...menu]; next[i]={...next[i],ref_id:e.target.value}; setMenu(next);}}><option value="">选择 MU 页面</option>{leaves.filter(l=>!l.disabled).map(l=><option key={l.key} value={l.key}>{l.parentMenu}/{l.label}</option>)}</select>}
- {m.page_type==="cube"&&<select value={String(m.ref_id)} onChange={e=>{const next=[...menu]; next[i]={...next[i],ref_id:Number(e.target.value)}; setMenu(next);}}><option value="">选择 Cube 页面</option>{site.cubes.map(c=><option key={c.id||c.name} value={c.id||""}>{c.name}</option>)}</select>}
- <button className="trash" onClick={()=>setMenu(menu.filter((_,idx)=>idx!==i))}>删除</button>
- </div>)}<Button secondary onClick={()=>setMenu([...menu,{parent_id:null,label:"",page_type:"",ref_id:"",sort_order:menu.length}])}>+ 添加菜单项</Button><Button onClick={saveMenu}>保存菜单</Button></div>}
</Panel>;
}
const MU_TREE = [
{
key: "设备监控",
- children: [
- { key: "设备总览", tabs: ["列表式", "卡片式"] },
- {
- key: "设备详情",
- tabs: [
- "设备工况", "告警记录",
- { label: "维修记录", disabled: true, note: "第一阶段暂不开发" },
- { label: "保养记录", disabled: true, note: "第一阶段暂不开发" },
- { label: "巡检记录", disabled: true, note: "第一阶段暂不开发" },
- "上下线记录", "设备文档", "区域监控",
- { label: "设备视频", disabled: true, note: "第一阶段暂不开发" },
- "远程快速控制", "远程控制计划", "设备轨迹",
- ],
- },
- ],
+ tabs: ["设备总览", "设备详情"],
},
{
key: "数据分析",
- children: [
- { key: "数据分析" },
- { key: "报表分析" },
- { key: "用量分析" },
- ],
+ tabs: ["数据分析", "报表分析", "用量分析"],
},
{
key: "智能控制",
- children: [
- { key: "远程控制计划" },
- { key: "执行记录" },
- { key: "控制模板" },
- ],
+ tabs: ["远程控制计划", "执行记录", "控制模板"],
},
];
return respondJson(res, 200, { prompt: buildCompleteAssetsPrompt(ctx, ctx.assets) });
}
+ // ===== AI Cube 页面补全:基于站点信息 + 资产层级,推断并补充 Cube 扩展页面 =====
+ function buildCompleteCubesPrompt(site) {
+ const assets = (site.assets || []).filter(a => a && a.type && a.type.trim());
+ const assetText = assets.length
+ ? assets.map(a => `- ${a.type.trim()} 实例数量:${Number(a.count) || 0}${a.rule ? `(命名规则:${a.rule.trim()})` : ""}`).join("\n")
+ : "(无,用户尚未配置设备资产)";
+ const existingCubes = (site.cubes || []).filter(c => c && c.name && c.name.trim()).map(c => c.name.trim());
+ const existingText = existingCubes.length ? existingCubes.join("、") : "(无,用户尚未创建)";
+ return `你是一名资深的工业站点数字化大屏方案架构师,擅长根据站点业务背景与设备资产,规划所需的可视化监控页面(Cube 扩展页)。
+请根据以下信息,推断该站点监控大屏所需的 Cube 扩展页面,并为每个页面撰写详尽的页面内容说明,用于补充「Cube 扩展页面」列表(位于用户已建页面下方)。
+
+站点信息:
+- 站点名称:${site.name || "未命名站点"}
+- 所属领域:${site.domain || "未指定"}
+- 站点基本情况描述:${site.description || "无"}
+- 补充要求:${site.supplement || "无"}
+- 设备资产层级(站点 → 设备类型 → 实例):
+${assetText}
+- 用户已手工创建的 Cube 页面(请勿重复、不要修改,仅补充新增):${existingText}
+
+要求:
+1. 先简要说明分析思路(思考过程):从站点业务与设备资产出发,推断需要哪些监控维度(如综合总览、能耗能效、设备健康、报警事件、环境监测、生产产量等),以及每个页面应覆盖的核心内容。
+2. 思考过程结束后,另起一段,以「【Cube页面】」开头,给出 JSON 数组(仅数组本身,不要多余文字与代码围栏),每个元素为 {"name":"页面名称(最多6个汉字)","content":"页面内容详述"}。
+3. content 需详实,至少覆盖:① 页面目标;② 关键展示要素(图表/指标卡/模块);③ 数据范围(来自哪些设备资产);④ 模拟数据构造思路(指标量级、单位、典型值)。
+4. 仅补充用户尚未创建的页面;页面名称简洁且不超过 6 个汉字。
+
+请直接输出你的分析与【Cube页面】JSON。`;
+ }
+
+ function extractCubes(text) {
+ const marker = "【Cube页面】";
+ const idx = text.lastIndexOf(marker);
+ let body = idx >= 0 ? text.slice(idx + marker.length) : text;
+ body = body.replace(/```(?:json)?/gi, "").trim();
+ const s = body.indexOf("[");
+ const e = body.lastIndexOf("]");
+ if (s < 0 || e <= s) throw new Error("未在输出中找到 Cube 页面 JSON");
+ const arr = JSON.parse(body.slice(s, e + 1));
+ if (!Array.isArray(arr)) throw new Error("Cube 页面不是数组");
+ return arr.filter(x => x && x.name).map(x => ({
+ name: String(x.name).trim().slice(0, 6),
+ content: x.content != null ? String(x.content).trim() : "",
+ }));
+ }
+
+ async function handleCompleteCubes(req, res, siteId) {
+ res.writeHead(200, { "Content-Type": "application/x-ndjson; charset=utf-8", "Cache-Control": "no-cache" });
+ const emit = (obj) => streamEvent(res, obj);
+ const fail = (message) => { emit({ task: "系统", status: "failed", message }); res.end(); };
+ if (genLocks.has(`complete-cubes-${siteId}`)) return fail("AI Cube 补全正在进行中");
+ genLocks.add(`complete-cubes-${siteId}`);
+ try {
+ const site = hydrateSite(siteId);
+ if (!site) return fail("站点不存在");
+ const body = await parseBody(req).catch(() => ({}));
+ const ctx = {
+ name: (body.name ?? site.name) || "",
+ domain: (body.domain ?? site.domain) || "",
+ description: (body.description ?? site.description) || "",
+ supplement: (body.supplement ?? site.supplement) || "",
+ assets: body.assets || site.assets || [],
+ cubes: site.cubes || [],
+ };
+ const prompt = buildCompleteCubesPrompt(ctx);
+ emit({ prompt });
+ emit({ log: `\n▶ 开始 AI Cube 补全:基于站点信息与设备资产,推断并补充 Cube 扩展页面…\n` });
+ emit({ task: "AI Cube 补全", status: "running" });
+ const onEvent = (o) => {
+ if (o && o.type === "stream_event" && o.event?.type === "content_block_delta" && o.event.delta?.type === "text_delta" && o.event.delta.text) {
+ emit({ log: o.event.delta.text });
+ }
+ };
+ const text = await runCodebuddy(prompt, `stationbuilder-complete-cubes-${siteId}`, onEvent);
+ const items = extractCubes(text);
+ emit({ log: `\n✓ 已生成 ${items.length} 个 Cube 页面\n` });
+ emit({ result: items });
+ emit({ task: "AI Cube 补全", status: "done" });
+ res.end();
+ } catch (e) {
+ emit({ log: `✗ AI Cube 补全失败:${e.message}\n` });
+ emit({ task: "系统", status: "failed", message: e.message });
+ res.end();
+ } finally {
+ genLocks.delete(`complete-cubes-${siteId}`);
+ }
+ }
+
+ async function handleCompleteCubesPrompt(req, res, siteId) {
+ const site = hydrateSite(siteId);
+ if (!site) return respondError(res, 404, "站点不存在");
+ const u = new URL(req.url, "http://localhost");
+ const ctx = {
+ name: u.searchParams.get("name") ?? site.name ?? "",
+ domain: u.searchParams.get("domain") ?? site.domain ?? "",
+ description: u.searchParams.get("description") ?? site.description ?? "",
+ supplement: u.searchParams.get("supplement") ?? site.supplement ?? "",
+ assets: site.assets || [],
+ cubes: site.cubes || [],
+ };
+ return respondJson(res, 200, { prompt: buildCompleteCubesPrompt(ctx) });
+ }
+
async function handleGenerateAll(req, res, siteId) {
res.writeHead(200, { "Content-Type": "application/x-ndjson; charset=utf-8", "Cache-Control": "no-cache" });
if (genLocks.has(`site-${siteId}`)) {
const mSiteCompletePrompt = path.match(/^\/api\/sites\/(\d+)\/complete-prompt$/);
const mSiteCompleteAssets = path.match(/^\/api\/sites\/(\d+)\/complete-assets$/);
const mSiteCompleteAssetsPrompt = path.match(/^\/api\/sites\/(\d+)\/complete-assets-prompt$/);
+ const mSiteCompleteCubes = path.match(/^\/api\/sites\/(\d+)\/complete-cubes$/);
+ const mSiteCompleteCubesPrompt = path.match(/^\/api\/sites\/(\d+)\/complete-cubes-prompt$/);
const mCubePromptNew = path.match(/^\/api\/sites\/(\d+)\/cubes\/prompt$/);
const mSiteMenu = path.match(/^\/api\/sites\/(\d+)\/menu$/);
const mSiteFiles = path.match(/^\/api\/sites\/(\d+)\/files$/);
if (mSiteCompletePrompt) return handleCompletePrompt(req, res, Number(mSiteCompletePrompt[1]));
if (mSiteCompleteAssets) return handleCompleteAssets(req, res, Number(mSiteCompleteAssets[1]));
if (mSiteCompleteAssetsPrompt) return handleCompleteAssetsPrompt(req, res, Number(mSiteCompleteAssetsPrompt[1]));
+ if (mSiteCompleteCubes) return handleCompleteCubes(req, res, Number(mSiteCompleteCubes[1]));
+ if (mSiteCompleteCubesPrompt) return handleCompleteCubesPrompt(req, res, Number(mSiteCompleteCubesPrompt[1]));
if (mSiteCubes) {
const siteId = Number(mSiteCubes[1]);
if (req.method === "GET") return respondJson(res, 200, db.prepare("SELECT c.*, c.prompt, gf.filename as gen_filename FROM cubes c LEFT JOIN gen_files gf ON gf.id=c.gen_file_id WHERE c.site_id=? ORDER BY c.sort_order,c.id").all(siteId));