import { useCallback, useEffect, useRef, useState } from "react"; import { FloatingPortal } from "@floating-ui/react"; import { X, CornerDownLeft, Loader2, RefreshCw } from "lucide-react"; import { summarizePage, streamAskPage, AiStreamError } from "@/client/lib/ai/api"; import type { AiAskHistoryMessage } from "@/shared/types"; import "./summarize-sheet.css"; interface SummarizeSheetProps { workspaceId: string; pageId: string; canSummarize: boolean; canAsk: boolean; open: boolean; onClose: () => void; } interface SummaryState { status: "idle" | "loading" | "ready" | "error"; text: string; error: string | null; } interface ChatMessage { id: string; role: "user" | "assistant"; content: string; streaming?: boolean; error?: boolean; } const EMPTY_SUMMARY_COPY = "The model returned an empty summary. Try again."; const EMPTY_ANSWER_COPY = "The model returned no answer. Try again."; function describeAiError(err: unknown, fallback: string): string { if (err instanceof AiStreamError && err.code === "page_empty") { return "Add something to this page before asking the model about it."; } if (err instanceof AiStreamError || err instanceof Error) return err.message; return fallback; } function SummarySection({ summary, onRetry }: { summary: SummaryState; onRetry: () => void }) { const isError = summary.status === "error"; const isLoading = summary.status === "loading"; const retryLabel = isError ? "Retry summary" : "Regenerate summary"; return (

Summary

{summary.status === "loading" && (
)} {summary.status === "error" && (

{summary.error}

)} {summary.status === "ready" && (

{summary.text}

)}
); } function ChatMessageRow({ message }: { message: ChatMessage }) { // Note: sheet surface is bg-zinc-800. To keep contrast comfortable for // astigmatic reading on longer passages, body text sits at zinc-200 // (not zinc-100) against both the neutral and accent-tinted fills. const style = message.error ? "rounded border border-red-500/30 bg-red-500/10 px-3 py-2 text-red-200" : message.role === "user" ? "rounded bg-accent-500/14 px-3 py-2 text-zinc-100" : "rounded bg-zinc-700/45 px-3 py-2 text-zinc-200"; const testid = message.role !== "assistant" ? undefined : message.error ? "summarize-sheet-answer-error" : "summarize-sheet-answer"; return (

{message.content || (message.streaming ? Thinking… : "")}

); } function PageChatList({ messages, chatEndRef, }: { messages: ChatMessage[]; chatEndRef: React.RefObject; }) { return (

Dig deeper

{messages.map((m) => ( ))}
); } function ChatComposer({ input, onChange, onSubmit, sending, inputRef, }: { input: string; onChange: (next: string) => void; onSubmit: () => void; sending: boolean; inputRef: React.RefObject; }) { return (
{ e.preventDefault(); onSubmit(); }} >