feat(ui): build session viewer to read raw AI transcripts and tool calls
This commit is contained in:
@@ -411,8 +411,15 @@ function SectionTile({
|
||||
// Vision
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
function BriefPanel({ plan, projectId, onChange }: { plan: Plan; projectId: string; onChange: (p: Plan) => void; }) {
|
||||
function BriefPanel({
|
||||
plan,
|
||||
projectId,
|
||||
onChange,
|
||||
}: {
|
||||
plan: Plan;
|
||||
projectId: string;
|
||||
onChange: (p: Plan) => void;
|
||||
}) {
|
||||
const [draft, setDraft] = useState(plan.brief ?? "");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [editing, setEditing] = useState(!plan.brief);
|
||||
@@ -445,7 +452,7 @@ function BriefPanel({ plan, projectId, onChange }: { plan: Plan; projectId: stri
|
||||
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
|
||||
// For now we just read text files (txt, md, csv).
|
||||
// In the future you can send PDFs to an extraction API.
|
||||
const reader = new FileReader();
|
||||
@@ -463,20 +470,106 @@ function BriefPanel({ plan, projectId, onChange }: { plan: Plan; projectId: stri
|
||||
<div className="vibn-enter" style={{ position: "relative" }}>
|
||||
<button
|
||||
onClick={() => setEditing(true)}
|
||||
style={{ position: "absolute", top: 0, right: 0, padding: "6px 12px", borderRadius: 6, background: "transparent", border: "1px solid #e8e4dc", fontSize: "0.75rem", color: "#6b6560", cursor: "pointer", display: "flex", alignItems: "center", gap: 6 }}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
padding: "6px 12px",
|
||||
borderRadius: 6,
|
||||
background: "transparent",
|
||||
border: "1px solid #e8e4dc",
|
||||
fontSize: "0.75rem",
|
||||
color: "#6b6560",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<Pencil style={{ width: 12, height: 12 }} /> Edit
|
||||
</button>
|
||||
<div style={{ padding: "8px 0" }}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={{
|
||||
p: ({ node, ...props }) => <p style={{ fontSize: "0.95rem", lineHeight: 1.6, color: "#1a1a1a", marginBottom: 16 }} {...props} />,
|
||||
h1: ({ node, ...props }) => <h1 style={{ fontSize: "1.4rem", fontWeight: 600, color: "#1a1a1a", marginTop: 24, marginBottom: 16 }} {...props} />,
|
||||
h2: ({ node, ...props }) => <h2 style={{ fontSize: "1.2rem", fontWeight: 600, color: "#1a1a1a", marginTop: 20, marginBottom: 12 }} {...props} />,
|
||||
h3: ({ node, ...props }) => <h3 style={{ fontSize: "1.05rem", fontWeight: 600, color: "#1a1a1a", marginTop: 16, marginBottom: 10 }} {...props} />,
|
||||
ul: ({ node, ...props }) => <ul style={{ fontSize: "0.95rem", lineHeight: 1.6, color: "#1a1a1a", paddingLeft: 24, marginBottom: 16, listStyleType: "disc" }} {...props} />,
|
||||
ol: ({ node, ...props }) => <ol style={{ fontSize: "0.95rem", lineHeight: 1.6, color: "#1a1a1a", paddingLeft: 24, marginBottom: 16, listStyleType: "decimal" }} {...props} />,
|
||||
li: ({ node, ...props }) => <li style={{ marginBottom: 6 }} {...props} />,
|
||||
}}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
p: ({ node, ...props }) => (
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.95rem",
|
||||
lineHeight: 1.6,
|
||||
color: "#1a1a1a",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h1: ({ node, ...props }) => (
|
||||
<h1
|
||||
style={{
|
||||
fontSize: "1.4rem",
|
||||
fontWeight: 600,
|
||||
color: "#1a1a1a",
|
||||
marginTop: 24,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h2: ({ node, ...props }) => (
|
||||
<h2
|
||||
style={{
|
||||
fontSize: "1.2rem",
|
||||
fontWeight: 600,
|
||||
color: "#1a1a1a",
|
||||
marginTop: 20,
|
||||
marginBottom: 12,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h3: ({ node, ...props }) => (
|
||||
<h3
|
||||
style={{
|
||||
fontSize: "1.05rem",
|
||||
fontWeight: 600,
|
||||
color: "#1a1a1a",
|
||||
marginTop: 16,
|
||||
marginBottom: 10,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
ul: ({ node, ...props }) => (
|
||||
<ul
|
||||
style={{
|
||||
fontSize: "0.95rem",
|
||||
lineHeight: 1.6,
|
||||
color: "#1a1a1a",
|
||||
paddingLeft: 24,
|
||||
marginBottom: 16,
|
||||
listStyleType: "disc",
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
ol: ({ node, ...props }) => (
|
||||
<ol
|
||||
style={{
|
||||
fontSize: "0.95rem",
|
||||
lineHeight: 1.6,
|
||||
color: "#1a1a1a",
|
||||
paddingLeft: 24,
|
||||
marginBottom: 16,
|
||||
listStyleType: "decimal",
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
li: ({ node, ...props }) => (
|
||||
<li style={{ marginBottom: 6 }} {...props} />
|
||||
),
|
||||
}}
|
||||
>
|
||||
{plan.brief}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
@@ -488,15 +581,48 @@ function BriefPanel({ plan, projectId, onChange }: { plan: Plan; projectId: stri
|
||||
<div className="vibn-enter">
|
||||
{!plan.brief && (
|
||||
<div style={emptyVisionBox}>
|
||||
<p style={{ margin: 0, marginBottom: 12, fontSize: "0.9rem", color: "#444441", lineHeight: 1.55 }}>
|
||||
No project brief uploaded. You can paste your PRD, requirements document, or notes below. The AI will read this to understand the full scope of your project.
|
||||
<p
|
||||
style={{
|
||||
margin: 0,
|
||||
marginBottom: 12,
|
||||
fontSize: "0.9rem",
|
||||
color: "#444441",
|
||||
lineHeight: 1.55,
|
||||
}}
|
||||
>
|
||||
No project brief uploaded. You can paste your PRD, requirements
|
||||
document, or notes below. The AI will read this to understand the
|
||||
full scope of your project.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
<label style={{ ...primaryBtn, cursor: 'pointer', display: 'inline-flex' }}>
|
||||
<div style={{ display: "flex", gap: "12px" }}>
|
||||
<label
|
||||
style={{
|
||||
...primaryBtn,
|
||||
cursor: "pointer",
|
||||
display: "inline-flex",
|
||||
}}
|
||||
>
|
||||
Upload Text File (.md, .txt)
|
||||
<input type="file" accept=".txt,.md,.csv" style={{ display: 'none' }} onChange={handleFileUpload} />
|
||||
<input
|
||||
type="file"
|
||||
accept=".txt,.md,.csv"
|
||||
style={{ display: "none" }}
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
</label>
|
||||
<button onClick={() => setEditing(true)} style={{ padding: "8px 16px", borderRadius: 7, background: "transparent", color: "#1a1a1a", border: "1px solid #1a1a1a", fontSize: "0.82rem", fontWeight: 600, cursor: "pointer" }}>
|
||||
<button
|
||||
onClick={() => setEditing(true)}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
borderRadius: 7,
|
||||
background: "transparent",
|
||||
color: "#1a1a1a",
|
||||
border: "1px solid #1a1a1a",
|
||||
fontSize: "0.82rem",
|
||||
fontWeight: 600,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Paste Manually
|
||||
</button>
|
||||
</div>
|
||||
@@ -504,12 +630,52 @@ function BriefPanel({ plan, projectId, onChange }: { plan: Plan; projectId: stri
|
||||
)}
|
||||
|
||||
{editing && (
|
||||
<div style={{ background: "#fff", border: "1px solid #e8e4dc", borderRadius: 10, overflow: "hidden", marginTop: plan.brief ? 0 : 24 }}>
|
||||
<div style={{ display: "flex", borderBottom: "1px solid #e8e4dc", background: "#faf8f5" }}>
|
||||
<button onClick={() => setEditorView("write")} style={{ ...tabStyle(editorView === "write" ? "write" : ""), borderRadius: 0, border: "none", borderRight: "1px solid #e8e4dc", borderBottom: editorView === "write" ? "1px solid transparent" : "1px solid #e8e4dc", background: editorView === "write" ? "#fff" : "transparent" }}>
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e8e4dc",
|
||||
borderRadius: 10,
|
||||
overflow: "hidden",
|
||||
marginTop: plan.brief ? 0 : 24,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
borderBottom: "1px solid #e8e4dc",
|
||||
background: "#faf8f5",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={() => setEditorView("write")}
|
||||
style={{
|
||||
...tabStyle(editorView === "write" ? "write" : ""),
|
||||
borderRadius: 0,
|
||||
border: "none",
|
||||
borderRight: "1px solid #e8e4dc",
|
||||
borderBottom:
|
||||
editorView === "write"
|
||||
? "1px solid transparent"
|
||||
: "1px solid #e8e4dc",
|
||||
background: editorView === "write" ? "#fff" : "transparent",
|
||||
}}
|
||||
>
|
||||
Write
|
||||
</button>
|
||||
<button onClick={() => setEditorView("preview")} style={{ ...tabStyle(editorView === "preview" ? "preview" : ""), borderRadius: 0, border: "none", borderRight: "1px solid #e8e4dc", borderBottom: editorView === "preview" ? "1px solid transparent" : "1px solid #e8e4dc", background: editorView === "preview" ? "#fff" : "transparent" }}>
|
||||
<button
|
||||
onClick={() => setEditorView("preview")}
|
||||
style={{
|
||||
...tabStyle(editorView === "preview" ? "preview" : ""),
|
||||
borderRadius: 0,
|
||||
border: "none",
|
||||
borderRight: "1px solid #e8e4dc",
|
||||
borderBottom:
|
||||
editorView === "preview"
|
||||
? "1px solid transparent"
|
||||
: "1px solid #e8e4dc",
|
||||
background: editorView === "preview" ? "#fff" : "transparent",
|
||||
}}
|
||||
>
|
||||
Preview
|
||||
</button>
|
||||
<div style={{ flex: 1, borderBottom: "1px solid #e8e4dc" }} />
|
||||
@@ -518,34 +684,142 @@ function BriefPanel({ plan, projectId, onChange }: { plan: Plan; projectId: stri
|
||||
{editorView === "write" ? (
|
||||
<textarea
|
||||
value={draft}
|
||||
onChange={(e) => { setDraft(e.target.value); setDirty(true); }}
|
||||
onChange={(e) => {
|
||||
setDraft(e.target.value);
|
||||
setDirty(true);
|
||||
}}
|
||||
placeholder="Paste your PRD or project scope here..."
|
||||
style={{ width: "100%", minHeight: 300, padding: 20, border: "none", outline: "none", fontSize: "0.9rem", lineHeight: 1.6, fontFamily: "var(--font-inter), sans-serif", color: "#1a1a1a", resize: "vertical" }}
|
||||
style={{
|
||||
width: "100%",
|
||||
minHeight: 300,
|
||||
padding: 20,
|
||||
border: "none",
|
||||
outline: "none",
|
||||
fontSize: "0.9rem",
|
||||
lineHeight: 1.6,
|
||||
fontFamily: "var(--font-inter), sans-serif",
|
||||
color: "#1a1a1a",
|
||||
resize: "vertical",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ padding: 20, minHeight: 300, maxHeight: 500, overflowY: "auto" }}>
|
||||
<div
|
||||
style={{
|
||||
padding: 20,
|
||||
minHeight: 300,
|
||||
maxHeight: 500,
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
{draft ? (
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={{
|
||||
p: ({ node, ...props }) => <p style={{ fontSize: "0.95rem", lineHeight: 1.6, color: "#1a1a1a", marginBottom: 16 }} {...props} />,
|
||||
h1: ({ node, ...props }) => <h1 style={{ fontSize: "1.4rem", fontWeight: 600, color: "#1a1a1a", marginTop: 24, marginBottom: 16 }} {...props} />,
|
||||
h2: ({ node, ...props }) => <h2 style={{ fontSize: "1.2rem", fontWeight: 600, color: "#1a1a1a", marginTop: 20, marginBottom: 12 }} {...props} />,
|
||||
ul: ({ node, ...props }) => <ul style={{ fontSize: "0.95rem", lineHeight: 1.6, color: "#1a1a1a", paddingLeft: 24, marginBottom: 16, listStyleType: "disc" }} {...props} />,
|
||||
li: ({ node, ...props }) => <li style={{ marginBottom: 6 }} {...props} />,
|
||||
}}>{draft}</ReactMarkdown>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
p: ({ node, ...props }) => (
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.95rem",
|
||||
lineHeight: 1.6,
|
||||
color: "#1a1a1a",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h1: ({ node, ...props }) => (
|
||||
<h1
|
||||
style={{
|
||||
fontSize: "1.4rem",
|
||||
fontWeight: 600,
|
||||
color: "#1a1a1a",
|
||||
marginTop: 24,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h2: ({ node, ...props }) => (
|
||||
<h2
|
||||
style={{
|
||||
fontSize: "1.2rem",
|
||||
fontWeight: 600,
|
||||
color: "#1a1a1a",
|
||||
marginTop: 20,
|
||||
marginBottom: 12,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
ul: ({ node, ...props }) => (
|
||||
<ul
|
||||
style={{
|
||||
fontSize: "0.95rem",
|
||||
lineHeight: 1.6,
|
||||
color: "#1a1a1a",
|
||||
paddingLeft: 24,
|
||||
marginBottom: 16,
|
||||
listStyleType: "disc",
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
li: ({ node, ...props }) => (
|
||||
<li style={{ marginBottom: 6 }} {...props} />
|
||||
),
|
||||
}}
|
||||
>
|
||||
{draft}
|
||||
</ReactMarkdown>
|
||||
) : (
|
||||
<p style={{ color: "#a09a90", fontStyle: "italic", margin: 0 }}>Nothing to preview.</p>
|
||||
<p style={{ color: "#a09a90", fontStyle: "italic", margin: 0 }}>
|
||||
Nothing to preview.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", gap: 12, padding: "12px 20px", borderTop: "1px solid #e8e4dc", background: "#faf8f5" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
gap: 12,
|
||||
padding: "12px 20px",
|
||||
borderTop: "1px solid #e8e4dc",
|
||||
background: "#faf8f5",
|
||||
}}
|
||||
>
|
||||
{plan.brief && (
|
||||
<button onClick={() => { setDraft(plan.brief ?? ""); setDirty(false); setEditing(false); }} style={{ padding: "8px 16px", borderRadius: 6, background: "transparent", border: "1px solid #e8e4dc", color: "#6b6560", fontSize: "0.82rem", fontWeight: 500, cursor: "pointer" }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setDraft(plan.brief ?? "");
|
||||
setDirty(false);
|
||||
setEditing(false);
|
||||
}}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
borderRadius: 6,
|
||||
background: "transparent",
|
||||
border: "1px solid #e8e4dc",
|
||||
color: "#6b6560",
|
||||
fontSize: "0.82rem",
|
||||
fontWeight: 500,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
<button onClick={handleSave} disabled={!dirty || saving} style={{ ...primaryBtn, opacity: !dirty || saving ? 0.5 : 1 }}>
|
||||
{saving ? <Loader2 style={{ width: 14, height: 14 }} className="animate-spin" /> : null}
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={!dirty || saving}
|
||||
style={{ ...primaryBtn, opacity: !dirty || saving ? 0.5 : 1 }}
|
||||
>
|
||||
{saving ? (
|
||||
<Loader2
|
||||
style={{ width: 14, height: 14 }}
|
||||
className="animate-spin"
|
||||
/>
|
||||
) : null}
|
||||
{saving ? "Saving..." : "Save Brief"}
|
||||
</button>
|
||||
</div>
|
||||
@@ -555,7 +829,6 @@ function BriefPanel({ plan, projectId, onChange }: { plan: Plan; projectId: stri
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function VisionPanel({
|
||||
plan,
|
||||
projectId,
|
||||
@@ -817,23 +1090,44 @@ function SessionsPanel({
|
||||
) : (
|
||||
<ul style={list}>
|
||||
{sessions.map((s) => (
|
||||
<li key={s.id} style={sessionRow}>
|
||||
<MessageSquare
|
||||
size={14}
|
||||
style={{ color: INK.mid, marginTop: 2, flexShrink: 0 }}
|
||||
/>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={sessionTitle}>{s.title}</div>
|
||||
{s.summary && <div style={sessionSummary}>{s.summary}</div>}
|
||||
<div style={sessionMeta}>
|
||||
<span>
|
||||
{s.messageCount}{" "}
|
||||
{s.messageCount === 1 ? "message" : "messages"}
|
||||
</span>
|
||||
<span style={{ color: INK.muted }}>·</span>
|
||||
<span>{relativeTime(s.updatedAt)}</span>
|
||||
<li key={s.id} style={{ ...sessionRow, padding: 0 }}>
|
||||
<a
|
||||
href={`/${workspace}/project/${projectId}/sessions/${s.id}`}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: 12,
|
||||
textDecoration: "none",
|
||||
color: "inherit",
|
||||
width: "100%",
|
||||
padding: "12px 14px",
|
||||
borderRadius: 8,
|
||||
transition: "background 0.15s",
|
||||
}}
|
||||
onMouseEnter={(e) =>
|
||||
(e.currentTarget.style.background = "rgba(0,0,0,0.03)")
|
||||
}
|
||||
onMouseLeave={(e) =>
|
||||
(e.currentTarget.style.background = "transparent")
|
||||
}
|
||||
>
|
||||
<MessageSquare
|
||||
size={14}
|
||||
style={{ color: INK.mid, marginTop: 2, flexShrink: 0 }}
|
||||
/>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={sessionTitle}>{s.title}</div>
|
||||
{s.summary && <div style={sessionSummary}>{s.summary}</div>}
|
||||
<div style={sessionMeta}>
|
||||
<span>
|
||||
{s.messageCount}{" "}
|
||||
{s.messageCount === 1 ? "message" : "messages"}
|
||||
</span>
|
||||
<span style={{ color: INK.muted }}>·</span>
|
||||
<span>{relativeTime(s.updatedAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { Loader2, ArrowLeft, Bot, User, Code2, Wrench } from "lucide-react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
interface ToolCall {
|
||||
id: string;
|
||||
name: string;
|
||||
args: any;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
role: "user" | "assistant";
|
||||
content?: string;
|
||||
toolCalls?: ToolCall[];
|
||||
textSegments?: string[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export default function SessionViewer() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const workspace = params.workspace as string;
|
||||
const projectId = params.projectId as string;
|
||||
const sessionId = params.sessionId as string;
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [thread, setThread] = useState<any>(null);
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/chat/threads/${sessionId}`)
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
setThread(d.thread);
|
||||
setMessages(d.messages || []);
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => setLoading(false));
|
||||
}, [sessionId]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center bg-[#faf8f5]">
|
||||
<Loader2 className="w-5 h-5 animate-spin text-zinc-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!thread) {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col items-center justify-center bg-[#faf8f5]">
|
||||
<p className="text-zinc-500 mb-4">Session not found.</p>
|
||||
<button
|
||||
onClick={() => router.push(`/${workspace}/project/${projectId}/plan`)}
|
||||
className="text-indigo-600 font-medium text-sm hover:underline"
|
||||
>
|
||||
← Back to Plan
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 bg-[#faf8f5] overflow-y-auto flex flex-col">
|
||||
<header className="sticky top-0 bg-white border-b border-zinc-200 px-6 py-4 flex items-center gap-4 z-10">
|
||||
<button
|
||||
onClick={() => router.push(`/${workspace}/project/${projectId}/plan`)}
|
||||
className="w-8 h-8 flex items-center justify-center rounded-md hover:bg-zinc-100 text-zinc-500 transition-colors"
|
||||
title="Back to Sessions"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold text-zinc-900 leading-none mb-1">{thread.title}</h1>
|
||||
<p className="text-xs text-zinc-500 font-mono">{new Date(thread.createdAt).toLocaleString()}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 max-w-4xl mx-auto w-full p-6 pb-20 space-y-8">
|
||||
{messages.map((m, i) => (
|
||||
<div key={m.id || i} className="flex gap-4">
|
||||
<div className="w-8 h-8 rounded-full flex-shrink-0 flex items-center justify-center border mt-1">
|
||||
{m.role === "user" ? (
|
||||
<div className="bg-zinc-100 border-zinc-200 w-full h-full rounded-full flex items-center justify-center">
|
||||
<User className="w-4 h-4 text-zinc-600" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-indigo-600 border-indigo-700 w-full h-full rounded-full flex items-center justify-center">
|
||||
<Bot className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 space-y-3">
|
||||
{m.role === "user" && m.content && (
|
||||
<div className="text-[15px] text-zinc-800 leading-relaxed font-medium whitespace-pre-wrap">
|
||||
{m.content}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{m.role === "assistant" && (
|
||||
<>
|
||||
{m.toolCalls && m.toolCalls.length > 0 && (
|
||||
<div className="space-y-1.5 mb-4">
|
||||
{m.toolCalls.map((tc, idx) => (
|
||||
<div key={idx} className="bg-zinc-100 border border-zinc-200 rounded-md p-2.5 text-xs font-mono overflow-x-auto text-zinc-600">
|
||||
<div className="flex items-center gap-2 text-zinc-800 font-semibold mb-1">
|
||||
<Wrench className="w-3.5 h-3.5" />
|
||||
{tc.name}
|
||||
</div>
|
||||
<pre className="text-[10px] leading-relaxed text-zinc-500 mt-1">
|
||||
{JSON.stringify(tc.args, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{m.content && (
|
||||
<div className="prose prose-sm prose-zinc max-w-none bg-white border border-zinc-200 p-4 rounded-xl shadow-sm">
|
||||
<ReactMarkdown>{m.content}</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Fallback for when textSegments exists but content is empty */}
|
||||
{!m.content && m.textSegments && m.textSegments.length > 0 && (
|
||||
<div className="prose prose-sm prose-zinc max-w-none bg-white border border-zinc-200 p-4 rounded-xl shadow-sm">
|
||||
<ReactMarkdown>{m.textSegments.join("\n\n")}</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{messages.length === 0 && (
|
||||
<p className="text-center text-zinc-500 italic mt-10">Empty session.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user