Files
mini-openclaw-v2/src/components/xingbang/BusinessAssistant.tsx
T

60 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import {ChangeEvent,FormEvent,useEffect,useMemo,useState} from "react";
import {ArrowUpRight,Bot,FilePlus2,Paperclip,Send,Sparkles,UserRound} from "lucide-react";
import AgentNav from "./AgentNav";
import IdentitySelector,{ROLE_LABEL} from "./IdentitySelector";
import {newRequestId} from "@/lib/request-id";
import "./business-assistant.css";
const ROOT="/api/risk-agent/project-assistant";
type Message={id:string;side:"assistant"|"user";text:string;url?:string;objectId?:string};
function headers(json=true){
const token=sessionStorage.getItem("xingbang-access-token"),role=sessionStorage.getItem("xingbang-demo-role");
return {...(json?{"Content-Type":"application/json"}:{}),...(token?{Authorization:`Bearer ${token}`}:{}) ,...(role?{"X-Xingbang-Demo-Role":role}:{})};
}
export default function BusinessAssistant(){
const [projects,setProjects]=useState<any[]>([]),[identity,setIdentity]=useState<any>(null),[projectId,setProjectId]=useState("");
const [input,setInput]=useState(""),[busy,setBusy]=useState(false),[error,setError]=useState("");
const [messages,setMessages]=useState<Message[]>([{id:"hello",side:"assistant",text:"你好,我是兴邦对公业务助手。选择项目后,你可以直接告诉我查状态、发起AI预审、生成报告或补充材料。"}]);
const role=identity?.role||"manager",project=projects.find(p=>p.id===projectId);
const quick=useMemo(()=>role==="reviewer"?["查看项目状态","发起独立评审","生成评审报告"]:role==="admin"?["查看项目状态"]:["查看项目状态","我有几个 Skills?","哪些金融相关 Skills?","发起AI预审","生成预审报告"],[role]);
async function load(){
setError("");
try{const response=await fetch(`${ROOT}/projects`,{headers:headers()});const body=await response.json();if(!response.ok)throw Error(body.detail||"身份或项目读取失败");setProjects(body.projects);setIdentity(body.identity);const requested=new URLSearchParams(location.search).get("project");setProjectId(current=>current||(requested&&body.projects.some((p:any)=>p.id===requested)?requested:body.projects[0]?.id||""));}
catch(e:any){setError(e.message)}
}
useEffect(()=>{load()},[]);
async function send(text:string){
const message=text.trim();if(!message||!projectId||busy)return;
if(message.includes("提交")&&message.includes("评审")&&!window.confirm(`确认将 ${projectId} 的AI预审报告提交正式评审?提交后保留当前版本。`))return;
setMessages(old=>[...old,{id:newRequestId(),side:"user",text:message}]);setInput("");setBusy(true);setError("");
try{const catalogQuery=/skill|rules?|技能|规则|数据卡|模板/i.test(message);const response=await fetch(`${ROOT}/${catalogQuery?"library/inventory":"commands"}`,{method:"POST",headers:{...headers(),"Idempotency-Key":newRequestId()},body:JSON.stringify({project_id:projectId,message})});const body=await response.json();if(!response.ok)throw Error(body.detail||"业务指令执行失败");setMessages(old=>[...old,{id:newRequestId(),side:"assistant",text:body.message,url:body.url,objectId:body.object_id}]);await load();}
catch(e:any){setError(e.message);setMessages(old=>[...old,{id:newRequestId(),side:"assistant",text:`未执行:${e.message}`}]);}
finally{setBusy(false)}
}
async function attach(event:ChangeEvent<HTMLInputElement>){
const file=event.target.files?.[0];event.target.value="";if(!file||!projectId||busy)return;
setMessages(old=>[...old,{id:newRequestId(),side:"user",text:`把材料“${file.name}”补充到 ${projectId}`}]);setBusy(true);setError("");
try{const form=new FormData();form.append("file",file);const response=await fetch(`${ROOT}/projects/${encodeURIComponent(projectId)}/documents`,{method:"POST",headers:headers(false),body:form});const body=await response.json();if(!response.ok)throw Error(body.detail||"材料上传失败");setMessages(old=>[...old,{id:newRequestId(),side:"assistant",text:`材料已归档并解析出 ${body.candidates?.length||0} 个候选字段。为避免误改事实,请到项目页确认来源。`,url:`/xingbang-workbench?project=${encodeURIComponent(projectId)}&tab=findings`,objectId:body.id}]);await load();}
catch(e:any){setError(e.message);setMessages(old=>[...old,{id:newRequestId(),side:"assistant",text:`材料未补充:${e.message}`}]);}
finally{setBusy(false)}
}
function submit(event:FormEvent){event.preventDefault();send(input)}
const conversation=<div className="ba-chat"><div className="ba-messages">{messages.map(message=><article key={message.id} className={message.side}><div>{message.side==="assistant"&&<Bot size={16}/>}<p style={{whiteSpace:"pre-line"}}>{message.text}</p>{message.side==="user"&&<span className="ba-user-icon" aria-label="本人"><UserRound size={17}/></span>}</div>{message.objectId&&<small>操作凭证 · {message.objectId}</small>}{message.url&&<a href={message.url}>查看项目中的结果<ArrowUpRight size={14}/></a>}</article>)}{busy&&<article className="assistant pending"><div><Bot size={16}/><p>正在执行并生成可追溯记录…</p></div></article>}</div>
{error&&<div role="alert" className="ba-error">{error}</div>}
<form onSubmit={submit}><label className="ba-attach" title="补充项目材料"><Paperclip size={18}/><input aria-label="补充项目材料" type="file" accept=".txt,.csv,.docx,.xlsx,.pdf,.png,.jpg,.jpeg" onChange={attach}/></label><input aria-label="业务指令" value={input} onChange={e=>setInput(e.target.value)} placeholder={`例如:帮我做一次 ${projectId||"当前项目"} 风险研判`}/><button aria-label="发送业务指令" disabled={busy||!input.trim()||!projectId}><Send size={18}/></button></form><footer><FilePlus2 size={13}/>当前身份只会调用本项目授权的 Skills、规则和业务工具,并保留操作凭证。</footer>
</div>;
return <div className="ba-shell"><AgentNav active="assistant" project={projectId} identity={identity}/><main className="ba-main">
<header className="ba-heading"><div><p>XINGBANG BUSINESS ASSISTANT</p><h1>一句话,推进对公业务</h1><span>对话和页面按钮共用同一套 Skills、规则、权限与留痕。</span></div><IdentitySelector identity={identity}/></header>
<section className="ba-context"><div><span>正在处理</span><select aria-label="选择业务项目" value={projectId} onChange={e=>setProjectId(e.target.value)}>{projects.map(p=><option key={p.id} value={p.id}>{p.id} · {p.customer}</option>)}</select></div>{project&&<><div><span>当前阶段</span><b>{project.stage}</b></div><div><span>下一步</span><b>{project.next_action}</b></div><a href={`/xingbang-workbench?project=${encodeURIComponent(projectId)}`}>进入项目<ArrowUpRight size={15}/></a></>}</section>
<section className="ba-workspace"><aside><Sparkles size={18}/><h2>{ROLE_LABEL[role]}快捷操作</h2><p>{role==="reviewer"?"独立查看材料和历史,再形成评审意见。":role==="admin"?"配置能力与数据边界,不替业务人员作判断。":"补证、预审、成稿、提交,一条主线完成。"}</p>{quick.map(item=><button key={item} disabled={busy||!projectId} onClick={()=>send(item)}>{item}<ArrowUpRight size={14}/></button>)}<small><Bot size={13}/>每次执行都会生成对象编号,可回到项目中追溯。</small></aside>
{conversation}
</section>
</main></div>;
}