167 lines
5.8 KiB
TypeScript
167 lines
5.8 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { useLayoutEffect, useRef } from "react";
|
||
|
|
import { useChat } from "@ai-sdk/react";
|
||
|
|
import { AlertTriangle, Bot, Loader2, User } from "lucide-react";
|
||
|
|
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import { cn } from "@/lib/utils";
|
||
|
|
import { WorkspaceComposer } from "./workspace-composer";
|
||
|
|
import { createChatScrollMemory } from "./workspace-interaction";
|
||
|
|
|
||
|
|
export function ChatMode({
|
||
|
|
workspaceSlug,
|
||
|
|
active,
|
||
|
|
}: {
|
||
|
|
workspaceSlug: string;
|
||
|
|
active: boolean;
|
||
|
|
}) {
|
||
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||
|
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||
|
|
const scrollMemory = useRef(createChatScrollMemory());
|
||
|
|
const { messages, input, handleSubmit, status, error, setInput, stop } =
|
||
|
|
useChat({
|
||
|
|
api: "/api/chat",
|
||
|
|
body: { workspace: workspaceSlug },
|
||
|
|
});
|
||
|
|
const isBusy = status === "submitted" || status === "streaming";
|
||
|
|
|
||
|
|
useLayoutEffect(() => {
|
||
|
|
const container = scrollRef.current;
|
||
|
|
if (container) scrollMemory.current.update(container, active);
|
||
|
|
}, [messages, status, active]);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="flex h-full min-h-0 flex-col">
|
||
|
|
<div
|
||
|
|
ref={scrollRef}
|
||
|
|
onScroll={(event) => {
|
||
|
|
scrollMemory.current.record(event.currentTarget, active);
|
||
|
|
}}
|
||
|
|
className="min-h-0 flex-1 overflow-y-auto"
|
||
|
|
>
|
||
|
|
{messages.length === 0 ? (
|
||
|
|
<div className="flex h-full min-h-[12rem] flex-col items-center justify-center gap-4 px-4 text-muted-foreground">
|
||
|
|
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary/10">
|
||
|
|
<Bot className="size-8 text-primary" aria-hidden="true" />
|
||
|
|
</div>
|
||
|
|
<div className="text-center">
|
||
|
|
<p className="text-base font-medium">How can I help you today?</p>
|
||
|
|
<p className="mt-1 text-sm">
|
||
|
|
Ask me to break down a task, draft an outline, or summarize what
|
||
|
|
you have open.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div className="mt-2 flex flex-wrap justify-center gap-2">
|
||
|
|
{[
|
||
|
|
"Break this project into subtasks",
|
||
|
|
"Summarize my open tasks",
|
||
|
|
"Draft a sprint plan for this week",
|
||
|
|
].map((suggestion) => (
|
||
|
|
<Button
|
||
|
|
key={suggestion}
|
||
|
|
type="button"
|
||
|
|
variant="outline"
|
||
|
|
size="sm"
|
||
|
|
disabled={isBusy}
|
||
|
|
onClick={() => {
|
||
|
|
setInput(suggestion);
|
||
|
|
inputRef.current?.focus();
|
||
|
|
}}
|
||
|
|
className="h-auto rounded-full px-3 py-1.5 text-xs"
|
||
|
|
>
|
||
|
|
{suggestion}
|
||
|
|
</Button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div
|
||
|
|
role="log"
|
||
|
|
aria-label="Chat messages"
|
||
|
|
aria-live="polite"
|
||
|
|
className="mx-auto max-w-3xl space-y-4 px-4 py-6"
|
||
|
|
>
|
||
|
|
{messages.map((msg) => (
|
||
|
|
<div
|
||
|
|
key={msg.id}
|
||
|
|
className={cn(
|
||
|
|
"flex gap-3",
|
||
|
|
msg.role === "user" && "flex-row-reverse",
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
<div
|
||
|
|
className={cn(
|
||
|
|
"flex h-7 w-7 shrink-0 items-center justify-center rounded-full",
|
||
|
|
msg.role === "user"
|
||
|
|
? "bg-primary text-primary-foreground"
|
||
|
|
: "bg-muted",
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
{msg.role === "user" ? (
|
||
|
|
<User className="size-3.5" aria-hidden="true" />
|
||
|
|
) : (
|
||
|
|
<Bot className="size-3.5" aria-hidden="true" />
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<div
|
||
|
|
className={cn(
|
||
|
|
"max-w-[85%] rounded-xl px-4 py-3 text-sm leading-relaxed",
|
||
|
|
msg.role === "user"
|
||
|
|
? "bg-primary text-primary-foreground"
|
||
|
|
: "bg-muted",
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
<span className="sr-only">
|
||
|
|
{msg.role === "user" ? "You" : "Assistant"}:{" "}
|
||
|
|
</span>
|
||
|
|
<p className="whitespace-pre-wrap break-words">
|
||
|
|
{msg.content}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
{isBusy && messages[messages.length - 1]?.role === "user" && (
|
||
|
|
<div role="status" className="flex gap-3">
|
||
|
|
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-muted">
|
||
|
|
<Bot className="size-3.5" aria-hidden="true" />
|
||
|
|
</div>
|
||
|
|
<div className="rounded-xl bg-muted px-4 py-3">
|
||
|
|
<Loader2
|
||
|
|
className="size-4 animate-spin text-muted-foreground"
|
||
|
|
aria-hidden="true"
|
||
|
|
/>
|
||
|
|
<span className="sr-only">Generating response…</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{error && (
|
||
|
|
<div role="alert" className="mx-auto max-w-3xl px-4 pb-4">
|
||
|
|
<div className="flex items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/5 px-4 py-3 text-sm text-destructive">
|
||
|
|
<AlertTriangle
|
||
|
|
className="mt-0.5 size-4 shrink-0"
|
||
|
|
aria-hidden="true"
|
||
|
|
/>
|
||
|
|
<p>
|
||
|
|
{error.message || "Something went wrong. Please try again."}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<WorkspaceComposer
|
||
|
|
mode="chat"
|
||
|
|
active={active}
|
||
|
|
input={input}
|
||
|
|
onInputChange={setInput}
|
||
|
|
onSubmit={handleSubmit}
|
||
|
|
busy={isBusy}
|
||
|
|
onStop={stop}
|
||
|
|
inputRef={inputRef}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|