Files
safe-os-ui/temp_output.txt

199 lines
8.7 KiB
Plaintext
Raw Permalink 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.

abortRef.current?.abort();
const fresh = { sessionId: makeId("sess"), userId: makeId("user") };
dispatch({ type: "reset", ...fresh });
setFiles([]);
};
return (
<div className="h-full relative overflow-hidden">
{/* ─── Sidebar Overlay ─── */}
{isHistoryOpen && (
<div
className="absolute inset-0 bg-black/20 z-40 transition-opacity duration-300"
onClick={() => setIsHistoryOpen(false)}
/>
)}
<div
className={`absolute top-0 left-0 h-full w-1/4 bg-white border-r border-border z-50 transform transition-transform duration-300 ease-in-out shadow-xl ${
isHistoryOpen ? "translate-x-0" : "-translate-x-full"
}`}
>
<div className="p-6 h-full flex flex-col">
<div className="flex items-center justify-between mb-8">
<h3 className="text-lg font-bold">历史对话</h3>
<button
onClick={() => setIsHistoryOpen(false)}
className="p-2 hover:bg-surface-muted rounded-full transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="flex-1 overflow-y-auto">
{state.historySessions.length === 0 ? (
<div className="text-center py-10 text-txt-muted text-sm italic">
暂无历史记录
</div>
) : (
<div className="flex flex-col gap-3 py-2">
{state.historySessions.map((session) => (
<div
key={session.id}
onClick={() => loadHistorySession(session.sessionId)}
className={`p-4 rounded-xl border border-border cursor-pointer transition-all hover:bg-surface-muted hover:border-magenta group ${
state.sessionId === session.sessionId ? 'bg-magenta/5 border-magenta ring-1 ring-magenta' : 'bg-white'
}`}
>
<div className="flex justify-between items-start mb-2">
<span className="text-[0.7rem] font-mono text-txt-muted truncate max-w-[120px]">
ID: {session.sessionId.slice(0, 8)}...
</span>
<span className="text-[0.65rem] text-txt-muted">
{new Date(session.ts).toLocaleDateString()}
</span>
</div>
<p className="text-sm text-txt leading-snug line-clamp-2 group-hover:text-magenta transition-colors">
{session.summary}
</p>
</div>
))}
</div>
)}
{loadingHistory && (
<div className="absolute inset-0 bg-white/60 flex items-center justify-center z-[60]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-magenta"></div>
</div>
)}
</div>
</div>
</div>
{/* ─── Main Chat ─── */}
<div className="h-full flex flex-col min-w-0">
<div className="flex-1 overflow-y-auto px-[72px] py-8">
{/* History Toggle Button */}
<button
onClick={() => setIsHistoryOpen(true)}
className="absolute top-6 left-6 p-2 bg-white border border-border rounded-lg shadow-sm hover:bg-surface-muted transition-colors z-30 group"
title="查看历史记录"
>
<svg className="w-5 h-5 text-txt-muted group-hover:text-magenta" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
<div className="flex flex-col gap-6 w-full">
{state.messages.length === 0 && (
<div className="text-center py-20 card">
<h2 className="text-2xl font-extrabold mb-2">规划委员会代理</h2>
<p className="text-txt-muted text-sm">
提供一个高层 Epic代理将从产品、架构与 RTE 视角进行规划。
</p>
</div>
)}
{state.messages.map((msg) => (
<div
key={msg.id}
className={`max-w-[78%] px-6 py-5 text-[0.95rem] leading-relaxed rounded-2xl shadow-[0_2px_14px_rgba(0,0,0,0.04)] ${
msg.role === "user"
? "self-end mr-1 bg-surface-muted text-txt border border-border whitespace-pre-wrap"
: "self-start bg-white border border-border border-l-4 border-l-magenta"
}`}
>
{msg.role === "assistant" && (
<span className="badge mb-3 block w-fit">系统 / 迭代闭环</span>
)}
{msg.role === "assistant" && state.reasoning[msg.id] && (
<details
className="mb-4 bg-surface-muted/50 rounded-lg border border-border overflow-hidden"
open={!msg.content}
>
<summary className="px-4 py-2 text-xs font-medium text-txt-muted cursor-pointer hover:bg-border/20 transition-colors list-none flex items-center gap-2">
<span className={`w-1.5 h-1.5 rounded-full bg-magenta ${!msg.content ? 'animate-pulse' : ''}`}></span>
查看思考过程
</summary>
<div className="px-4 py-3 bg-white/50 text-[0.8rem] font-mono whitespace-pre-wrap border-t border-border text-txt-muted">
{state.reasoning[msg.id]}
</div>
</details>
)}
{msg.role === "assistant" ? (
<div className="prose prose-sm max-w-none prose-p:my-0.5 prose-pre:bg-gray-800 prose-pre:text-gray-100 prose-code:text-magenta prose-code:bg-magenta/10 prose-code:px-1 prose-code:rounded prose-li:my-0">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
>
{msg.content || (state.chatting ? "思考中..." : "")}
</ReactMarkdown>
</div>
) : (
msg.content || "Thinking..."
)}
{msg.status === "failed" && (
<span className="text-red-500 text-xs block mt-2">发送失败</span>
)}
</div>
))}
<div ref={bottomRef} />
</div>
</div>
{/* Error */}
{state.error && (
<div className="mx-[72px] mb-2 px-4 py-2 bg-red-50 text-red-700 text-sm flex justify-between items-center">
{state.error}
<button
className="text-red-400 hover:text-red-600 font-bold"
onClick={() => dispatch({ type: "set-error" })}
>
</button>
</div>
)}
{/* Input area */}
<div className="px-10 pb-6 pt-4 border-t border-border">
{/* File upload row */}
<div className="flex items-center gap-3 mb-3 text-sm">
<label className="btn-outline text-xs px-3 py-1.5 cursor-pointer">
{uploading ? "上传中..." : "附加文件"}
<input type="file" className="hidden" onChange={handleUpload} disabled={uploading} />
</label>
{files.map((f) => (
<span key={f.id} className="badge text-[0.7rem]">{f.name}</span>
))}
<button
className="ml-auto text-xs text-txt-muted hover:text-magenta"
onClick={handleReset}
>
重置会话
</button>
</div>
<div className="flex gap-4 items-end">
<textarea
className="input-field flex-1 min-h-[88px] max-h-56 resize-y"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
send();
}
}}
placeholder="描述你的 Epic 或提供指引..."
disabled={state.chatting}
/>
<button className="btn-magenta" onClick={send} disabled={state.chatting}>
{state.chatting ? "规划中..." : "发送"}
</button>
</div>
</div>
</div>
</div>
);
}