- Remove card container (no more rounded-2xl, ring, 600px height) - Chat fills the full layout space naturally - Avatars: 28x28 rounded-7 squares (black for Atlas, warm gray for user) - Both sides use same avatar+label layout (no right-aligned bubbles) - Sender labels: tiny uppercase ATLAS / YOU above each message - Input bar: white pill with border, Send button, Stop for streaming - User initial pulled from session (name or email first letter) Made-with: Cursor
313 lines
11 KiB
TypeScript
313 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
ActionBarPrimitive,
|
|
BranchPickerPrimitive,
|
|
ComposerPrimitive,
|
|
MessagePrimitive,
|
|
ThreadPrimitive,
|
|
} from "@assistant-ui/react";
|
|
import {
|
|
CheckIcon,
|
|
ChevronLeftIcon,
|
|
ChevronRightIcon,
|
|
CopyIcon,
|
|
RefreshCwIcon,
|
|
SquareIcon,
|
|
} from "lucide-react";
|
|
import type { FC } from "react";
|
|
import { MarkdownText } from "./markdown-text";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Thread root — Stackless style: flat on beige, no card
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const Thread: FC<{ userInitial?: string }> = ({ userInitial = "Y" }) => (
|
|
<ThreadPrimitive.Root
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
height: "100%",
|
|
background: "#f6f4f0",
|
|
fontFamily: "Outfit, sans-serif",
|
|
}}
|
|
>
|
|
{/* Empty state */}
|
|
<ThreadPrimitive.Empty>
|
|
<div style={{
|
|
display: "flex", height: "100%",
|
|
flexDirection: "column", alignItems: "center", justifyContent: "center",
|
|
gap: 12, padding: "40px 32px",
|
|
}}>
|
|
<div style={{
|
|
width: 44, height: 44, borderRadius: 11, background: "#1a1a1a",
|
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|
fontFamily: "Newsreader, serif", fontSize: "1.2rem", fontWeight: 500,
|
|
color: "#fff",
|
|
}}>
|
|
A
|
|
</div>
|
|
<div style={{ textAlign: "center" }}>
|
|
<p style={{ fontSize: "0.88rem", fontWeight: 600, color: "#1a1a1a", marginBottom: 4 }}>Atlas</p>
|
|
<p style={{ fontSize: "0.78rem", color: "#a09a90", maxWidth: 260, lineHeight: 1.5 }}>
|
|
Your product strategist. Let's define what you're building.
|
|
</p>
|
|
</div>
|
|
<div style={{ width: "100%", maxWidth: 600 }}>
|
|
<Composer userInitial={userInitial} />
|
|
</div>
|
|
</div>
|
|
</ThreadPrimitive.Empty>
|
|
|
|
{/* Messages */}
|
|
<ThreadPrimitive.Viewport style={{ flex: 1, overflowY: "auto", padding: "28px 32px" }}>
|
|
<ThreadPrimitive.Messages
|
|
components={{
|
|
UserMessage: (props) => <UserMessage {...props} userInitial={userInitial} />,
|
|
AssistantMessage,
|
|
}}
|
|
/>
|
|
</ThreadPrimitive.Viewport>
|
|
|
|
{/* Input bar */}
|
|
<div style={{ padding: "14px 32px 22px", flexShrink: 0 }}>
|
|
<Composer userInitial={userInitial} />
|
|
</div>
|
|
</ThreadPrimitive.Root>
|
|
);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Composer — Stackless white pill input bar
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const Composer: FC<{ userInitial?: string }> = () => (
|
|
<ComposerPrimitive.Root style={{ width: "100%" }}>
|
|
<div style={{
|
|
display: "flex", gap: 8,
|
|
padding: "5px 5px 5px 16px",
|
|
background: "#fff",
|
|
border: "1px solid #e0dcd4",
|
|
borderRadius: 10,
|
|
alignItems: "center",
|
|
boxShadow: "0 1px 4px #1a1a1a06",
|
|
}}>
|
|
<ComposerPrimitive.Input
|
|
placeholder="Describe your thinking..."
|
|
rows={1}
|
|
autoFocus
|
|
style={{
|
|
flex: 1,
|
|
border: "none",
|
|
background: "none",
|
|
fontSize: "0.86rem",
|
|
fontFamily: "Outfit, sans-serif",
|
|
color: "#1a1a1a",
|
|
padding: "8px 0",
|
|
resize: "none",
|
|
outline: "none",
|
|
minHeight: 24,
|
|
maxHeight: 120,
|
|
}}
|
|
/>
|
|
<ThreadPrimitive.If running={false}>
|
|
<ComposerPrimitive.Send asChild>
|
|
<button
|
|
style={{
|
|
padding: "9px 16px",
|
|
borderRadius: 7,
|
|
border: "none",
|
|
background: "#1a1a1a",
|
|
color: "#fff",
|
|
fontSize: "0.78rem",
|
|
fontWeight: 600,
|
|
fontFamily: "Outfit, sans-serif",
|
|
cursor: "pointer",
|
|
transition: "opacity 0.15s",
|
|
flexShrink: 0,
|
|
}}
|
|
onMouseEnter={(e) => (e.currentTarget.style.opacity = "0.8")}
|
|
onMouseLeave={(e) => (e.currentTarget.style.opacity = "1")}
|
|
>
|
|
Send
|
|
</button>
|
|
</ComposerPrimitive.Send>
|
|
</ThreadPrimitive.If>
|
|
<ThreadPrimitive.If running>
|
|
<ComposerPrimitive.Cancel asChild>
|
|
<button
|
|
style={{
|
|
padding: "9px 16px",
|
|
borderRadius: 7,
|
|
border: "none",
|
|
background: "#eae6de",
|
|
color: "#8a8478",
|
|
fontSize: "0.78rem",
|
|
fontWeight: 600,
|
|
fontFamily: "Outfit, sans-serif",
|
|
cursor: "pointer",
|
|
flexShrink: 0,
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 6,
|
|
}}
|
|
>
|
|
<SquareIcon style={{ width: 10, height: 10 }} />
|
|
Stop
|
|
</button>
|
|
</ComposerPrimitive.Cancel>
|
|
</ThreadPrimitive.If>
|
|
</div>
|
|
</ComposerPrimitive.Root>
|
|
);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Assistant message — black avatar, "Atlas" label, plain text
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const AssistantMessage: FC = () => (
|
|
<MessagePrimitive.Root style={{ display: "flex", gap: 12, marginBottom: 22 }} className="group">
|
|
{/* Avatar */}
|
|
<div style={{
|
|
width: 28, height: 28, borderRadius: 7, flexShrink: 0, marginTop: 2,
|
|
background: "#1a1a1a",
|
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|
fontSize: "0.68rem", fontWeight: 700, color: "#fff",
|
|
fontFamily: "Newsreader, serif",
|
|
}}>
|
|
A
|
|
</div>
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
{/* Sender label */}
|
|
<div style={{
|
|
fontSize: "0.68rem", fontWeight: 600, color: "#a09a90",
|
|
marginBottom: 5, textTransform: "uppercase", letterSpacing: "0.04em",
|
|
}}>
|
|
Atlas
|
|
</div>
|
|
{/* Message content */}
|
|
<div style={{ fontSize: "0.88rem", color: "#2a2824", lineHeight: 1.72 }}>
|
|
<MessagePrimitive.Content components={{ Text: AssistantText }} />
|
|
</div>
|
|
<AssistantActionBar />
|
|
<BranchPicker />
|
|
</div>
|
|
</MessagePrimitive.Root>
|
|
);
|
|
|
|
const AssistantText: FC = () => <MarkdownText />;
|
|
|
|
const AssistantActionBar: FC = () => (
|
|
<ActionBarPrimitive.Root
|
|
hideWhenRunning
|
|
autohide="not-last"
|
|
className="group-hover:opacity-100"
|
|
style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 6, opacity: 0, transition: "opacity 0.15s" }}
|
|
>
|
|
<ActionBarPrimitive.Copy asChild>
|
|
<button style={{
|
|
display: "flex", alignItems: "center", gap: 4,
|
|
fontSize: "0.68rem", color: "#b5b0a6",
|
|
background: "none", border: "none", cursor: "pointer",
|
|
fontFamily: "Outfit, sans-serif", padding: "2px 6px", borderRadius: 4,
|
|
}}>
|
|
<MessagePrimitive.If copied>
|
|
<CheckIcon style={{ width: 10, height: 10 }} /> Copied
|
|
</MessagePrimitive.If>
|
|
<MessagePrimitive.If copied={false}>
|
|
<CopyIcon style={{ width: 10, height: 10 }} /> Copy
|
|
</MessagePrimitive.If>
|
|
</button>
|
|
</ActionBarPrimitive.Copy>
|
|
<ActionBarPrimitive.Reload asChild>
|
|
<button style={{
|
|
display: "flex", alignItems: "center", gap: 4,
|
|
fontSize: "0.68rem", color: "#b5b0a6",
|
|
background: "none", border: "none", cursor: "pointer",
|
|
fontFamily: "Outfit, sans-serif", padding: "2px 6px", borderRadius: 4,
|
|
}}>
|
|
<RefreshCwIcon style={{ width: 10, height: 10 }} /> Retry
|
|
</button>
|
|
</ActionBarPrimitive.Reload>
|
|
</ActionBarPrimitive.Root>
|
|
);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// User message — warm avatar, "You" label, same layout as Atlas
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const UserMessage: FC<{ userInitial?: string }> = ({ userInitial = "Y" }) => (
|
|
<MessagePrimitive.Root style={{ display: "flex", gap: 12, marginBottom: 22 }} className="group">
|
|
{/* Avatar */}
|
|
<div style={{
|
|
width: 28, height: 28, borderRadius: 7, flexShrink: 0, marginTop: 2,
|
|
background: "#e8e4dc",
|
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|
fontSize: "0.68rem", fontWeight: 700, color: "#8a8478",
|
|
fontFamily: "Outfit, sans-serif",
|
|
}}>
|
|
{userInitial}
|
|
</div>
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
{/* Sender label */}
|
|
<div style={{
|
|
fontSize: "0.68rem", fontWeight: 600, color: "#a09a90",
|
|
marginBottom: 5, textTransform: "uppercase", letterSpacing: "0.04em",
|
|
}}>
|
|
You
|
|
</div>
|
|
{/* Message content */}
|
|
<div style={{ fontSize: "0.88rem", color: "#2a2824", lineHeight: 1.72, whiteSpace: "pre-wrap" }}>
|
|
<MessagePrimitive.Content components={{ Text: UserText }} />
|
|
</div>
|
|
<UserActionBar />
|
|
</div>
|
|
</MessagePrimitive.Root>
|
|
);
|
|
|
|
const UserText: FC<{ text: string }> = ({ text }) => <span>{text}</span>;
|
|
|
|
const UserActionBar: FC = () => (
|
|
<ActionBarPrimitive.Root
|
|
hideWhenRunning
|
|
autohide="not-last"
|
|
className="group-hover:opacity-100"
|
|
style={{ display: "flex", alignItems: "center", gap: 4, marginTop: 4, opacity: 0, transition: "opacity 0.15s" }}
|
|
>
|
|
<ActionBarPrimitive.Edit asChild>
|
|
<button style={{
|
|
fontSize: "0.68rem", color: "#b5b0a6",
|
|
background: "none", border: "none", cursor: "pointer",
|
|
fontFamily: "Outfit, sans-serif", padding: "2px 6px", borderRadius: 4,
|
|
}}>
|
|
Edit
|
|
</button>
|
|
</ActionBarPrimitive.Edit>
|
|
</ActionBarPrimitive.Root>
|
|
);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Branch picker
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const BranchPicker: FC = () => (
|
|
<BranchPickerPrimitive.Root
|
|
hideWhenSingleBranch
|
|
className="group-hover:opacity-100"
|
|
style={{ display: "flex", alignItems: "center", gap: 4, marginTop: 4, opacity: 0, transition: "opacity 0.15s" }}
|
|
>
|
|
<BranchPickerPrimitive.Previous asChild>
|
|
<button style={{ background: "none", border: "none", cursor: "pointer", color: "#b5b0a6" }}>
|
|
<ChevronLeftIcon style={{ width: 12, height: 12 }} />
|
|
</button>
|
|
</BranchPickerPrimitive.Previous>
|
|
<span style={{ fontSize: "0.68rem", color: "#b5b0a6" }}>
|
|
<BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
|
|
</span>
|
|
<BranchPickerPrimitive.Next asChild>
|
|
<button style={{ background: "none", border: "none", cursor: "pointer", color: "#b5b0a6" }}>
|
|
<ChevronRightIcon style={{ width: 12, height: 12 }} />
|
|
</button>
|
|
</BranchPickerPrimitive.Next>
|
|
</BranchPickerPrimitive.Root>
|
|
);
|