109 lines
3.9 KiB
TypeScript
109 lines
3.9 KiB
TypeScript
import { projectWorkNotices } from "@code/primitives/work";
|
|
import {
|
|
Conversation,
|
|
ConversationContent,
|
|
} from "@code/ui/components/ai-elements/conversation";
|
|
import { MessageSquareText } from "lucide-react";
|
|
import { useMemo } from "react";
|
|
|
|
import { ChatMessage } from "@/components/chat/chat-message";
|
|
import { ChatThinkingResponse } from "@/components/chat/chat-thinking-response";
|
|
import { buildWorkspaceTimeline } from "@/lib/workspace/presentation";
|
|
import type { WorkRecord, WorkspaceState } from "@/lib/workspace/types";
|
|
|
|
import { WorkCard } from "./work-card";
|
|
|
|
const ConversationLoading = () => (
|
|
<output
|
|
aria-label="Loading conversation"
|
|
className="mx-auto flex min-h-[55vh] w-full max-w-md flex-col justify-center gap-4"
|
|
>
|
|
<span className="h-16 w-4/5 animate-pulse rounded-sm bg-[#e3e0d5]" />
|
|
<span className="ml-auto h-12 w-3/5 animate-pulse rounded-sm bg-[#dedbd0]" />
|
|
<span className="h-24 w-full animate-pulse rounded-sm bg-[#e3e0d5]" />
|
|
<span className="sr-only">Loading conversation…</span>
|
|
</output>
|
|
);
|
|
|
|
const ConversationEmptyState = () => (
|
|
<div className="grid min-h-[55vh] place-items-center text-center">
|
|
<div>
|
|
<MessageSquareText className="mx-auto size-7 text-[#8a887f]" />
|
|
<h1 className="mt-4 text-xl font-semibold">What should move forward?</h1>
|
|
<p className="mx-auto mt-2 max-w-sm text-sm leading-6 text-[#69675f]">
|
|
Describe an outcome or problem. Casual conversation stays conversation.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
export const ConversationFeed = ({
|
|
highlightedMessageId,
|
|
onSourceSelect,
|
|
workspace,
|
|
}: {
|
|
readonly highlightedMessageId?: string;
|
|
readonly onSourceSelect: (rawText: string) => void;
|
|
readonly workspace: WorkspaceState;
|
|
}) => {
|
|
const works = workspace.works ?? [];
|
|
const workById = new Map(
|
|
works.map((work) => [String(work._id), work] as const)
|
|
);
|
|
const notices = projectWorkNotices(works);
|
|
const timeline = useMemo(
|
|
() => buildWorkspaceTimeline(workspace.agent.messages, notices),
|
|
[notices, workspace.agent.messages]
|
|
);
|
|
|
|
return (
|
|
<Conversation className="min-h-0 flex-1">
|
|
<ConversationContent className="mx-auto min-h-full w-full max-w-2xl gap-4 px-4 py-5 sm:px-6">
|
|
{!workspace.agent.historyReady && timeline.length === 0 ? (
|
|
<ConversationLoading />
|
|
) : null}
|
|
{workspace.agent.historyReady && timeline.length === 0 ? (
|
|
<ConversationEmptyState />
|
|
) : null}
|
|
{timeline.map((item) => {
|
|
if (item.kind === "work") {
|
|
const work = workById.get(item.notice.workId) as
|
|
| WorkRecord
|
|
| undefined;
|
|
return work ? (
|
|
<div
|
|
className="chat-message ml-7"
|
|
key={`notice-${item.notice.eventId}`}
|
|
>
|
|
<p className="mb-2 text-[10px] font-semibold uppercase text-[#65713a]">
|
|
Work proposed from this conversation
|
|
</p>
|
|
<div className="rounded-sm">
|
|
<span className="sr-only">Proposed Work</span>
|
|
<WorkCard
|
|
onSourceSelect={onSourceSelect}
|
|
work={work}
|
|
workspace={workspace}
|
|
/>
|
|
</div>
|
|
</div>
|
|
) : null;
|
|
}
|
|
return (
|
|
<div
|
|
className={`rounded-sm transition-colors duration-300 ${highlightedMessageId === item.message.id ? "bg-[#dcff68]/70 ring-2 ring-[#7f9130] ring-offset-4 ring-offset-[#f2f0e7]" : ""}`}
|
|
id={`workspace-message-${item.message.id}`}
|
|
key={item.message.id}
|
|
>
|
|
<ChatMessage message={item.message} />
|
|
</div>
|
|
);
|
|
})}
|
|
{workspace.agent.status === "submitted" ? (
|
|
<ChatThinkingResponse />
|
|
) : null}
|
|
</ConversationContent>
|
|
</Conversation>
|
|
);
|
|
};
|