import { useMemo, useRef, useState } from "react"; import { generateId } from "ai"; import { ActionIcon, Affix, Alert, Box, Group, Paper, ScrollArea, Stack, Text, Textarea, Tooltip, } from "@mantine/core"; import { IconAlertTriangle, IconArrowUp, IconSparkles, IconX, } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; import { useChat, type UIMessage } from "@ai-sdk/react"; import { DefaultChatTransport } from "ai"; interface ShareAiWidgetProps { /** The share id (or key) the assistant is scoped to. */ shareId: string; /** The page the reader currently has open (context for "this page"). */ pageId: string; } /** Concatenate the visible text parts of a UIMessage. */ function messageText(message: UIMessage): string { return (message.parts ?? []) .filter( (p): p is { type: "text"; text: string } => p?.type === "text" && typeof (p as { text?: string }).text === "string", ) .map((p) => p.text) .join(""); } /** * Lightweight, EPHEMERAL "Ask AI" widget for a public shared page. * * A stripped version of the authenticated chat: text input only, no chat list, * no history, no persistence, no voice input. The transcript lives only in * memory (this component's `useChat` store) and is sent with `credentials: * "omit"` to the anonymous `/api/shares/ai/stream` endpoint. The server stores * nothing. */ export default function ShareAiWidget({ shareId, pageId }: ShareAiWidgetProps) { const { t } = useTranslation(); const [open, setOpen] = useState(false); const [input, setInput] = useState(""); // Stable per-mount store key (see ai-chat ChatThread for the rationale on why // useChat needs a stable, non-undefined id to avoid re-creating its store). const storeIdRef = useRef(`share-ai-${generateId()}`); const transport = useMemo( () => new DefaultChatTransport({ api: "/api/shares/ai/stream", // Anonymous endpoint: never send cookies/credentials. credentials: "omit", prepareSendMessagesRequest: ({ messages, body }) => ({ body: { ...body, shareId, pageId, messages, }, }), }), [shareId, pageId], ); const { messages, sendMessage, status, stop, error } = useChat({ id: storeIdRef.current, transport, }); const isStreaming = status === "submitted" || status === "streaming"; const handleSend = () => { const text = input.trim(); if (!text || isStreaming) return; setInput(""); void sendMessage({ text }); }; if (!open) { return ( setOpen(true)} > ); } return ( {t("Ask AI")} setOpen(false)} > {messages.length === 0 ? ( {t("Ask a question about this documentation.")} ) : ( {messages.map((message) => ( {messageText(message) || (isStreaming ? t("Thinking…") : "")} ))} )} {error && ( } mt="sm" title={t("Something went wrong")} > {t("The assistant is unavailable right now. Please try again.")} )}