60 lines
7.2 KiB
TypeScript
60 lines
7.2 KiB
TypeScript
"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>;
|
||
}
|