{"version":3,"file":"AIChat.cjs","names":[],"sources":["../../../src/components/AIChat/AIChat.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the widest surface in\n * the SDK on purpose: it is a whole chat page, and its 25 props are four groups —\n * the transcript (messages, votes, pending, showSystem), the turn actions an app has\n * to own (onSend, onStop, onRegenerate, onEditSubmit, onFeedback, onRetry,\n * onSendError), render slots (renderAvatar, renderContent, header, emptyState,\n * suggestions, composerActions, composerFooter) and composer settings (composerRef,\n * composerDisabled, maxRows, placeholder, locale, defaultReasoningOpen,\n * showLineNumbers). The parts already are separate components — AIChatTurn and\n * AIChatComposer are exported — and this is the assembly whose entire job is to\n * accept the union of their configuration.\n */\nimport { ArrowDown } from \"lucide-react\";\nimport {\n    useEffect,\n    useLayoutEffect,\n    useRef,\n    useState,\n    type HTMLAttributes,\n    type ReactNode,\n    type Ref,\n} from \"react\";\n\nimport { useAnnounce } from \"@/hooks/use-announce\";\nimport { cn } from \"@/utils/cn\";\n\nimport { EmptyState } from \"../EmptyState\";\nimport {\n    aiChatStrings,\n    isGenerating,\n    lastAssistantId,\n    tailSignature,\n    visibleTurns,\n    type AIChatMessage,\n    type AIChatVote,\n} from \"./ai-chat-turns\";\nimport { AIChatComposer, type AIChatComposerHandle } from \"./AIChatComposer\";\nimport { AIChatTurn } from \"./AIChatTurn\";\nimport styles from \"./AIChat.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onSubmit\";\n\nexport interface AIChatProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n    /** The transcript, **oldest first**. Never reordered by the component. */\n    messages: readonly AIChatMessage[];\n    /** Renders the composer when given. Receives the trimmed prompt. */\n    onSend?: (text: string) => void | Promise<void>;\n    /** Abort the turn in flight. Shows the stop button while generating. */\n    onStop?: () => void;\n    /** Ask again for the newest assistant turn. */\n    onRegenerate?: (message: AIChatMessage) => void;\n    /** Re-submit an edited user turn. The app decides what to drop after it. */\n    onEditSubmit?: (message: AIChatMessage, text: string) => void | Promise<void>;\n    /** Rating on an assistant turn. */\n    onFeedback?: (message: AIChatMessage, vote: AIChatVote) => void;\n    /** Retry a turn that carries an `error`. */\n    onRetry?: (message: AIChatMessage) => void;\n    /**\n     * The request is out and nothing has arrived yet.\n     *\n     * Distinct from a turn with `streaming: true`: apps that only push a message\n     * once the first token lands need somewhere to say \"we asked\", and without it the\n     * screen is frozen for however long the model takes to start.\n     */\n    pending?: boolean;\n    /** Prompts offered on an empty transcript. Clicking one sends it. */\n    suggestions?: readonly string[];\n    /** Avatar for a turn — an `<Avatar>`, an `<Icon>`, a logo. */\n    renderAvatar?: (message: AIChatMessage) => ReactNode;\n    /** Render a body yourself — a tool-call card, a chart, a citation list. */\n    renderContent?: (message: AIChatMessage) => ReactNode;\n    /** Ratings to show as pressed, by message id. Omit to keep them local. */\n    votes?: Readonly<Record<string, AIChatVote>>;\n    /** Rendered above the transcript, inside the panel. */\n    header?: ReactNode;\n    /** Shown when there are no turns and no suggestions. */\n    emptyState?: ReactNode;\n    /** Show `\"system\"` turns. Default `false`. */\n    showSystem?: boolean;\n    /** Reasoning blocks start expanded. Default `false`. */\n    defaultReasoningOpen?: boolean;\n    /** Show line numbers in fenced code. Default `false`. */\n    showLineNumbers?: boolean;\n    /** Locale for labels. Default `\"pt-BR\"`. */\n    locale?: \"pt-BR\" | \"en\";\n    /** Placeholder for the composer. */\n    placeholder?: string;\n    /** Extra controls inside the composer, before the send button. */\n    composerActions?: ReactNode;\n    /**\n     * Reach the composer imperatively — `focus()`, `getValue()`, `setValue()`.\n     *\n     * What makes dictation (or a slash-command menu, or \"edit and resend\") possible\n     * without this component knowing anything about them: pair it with\n     * `composerActions` and the button you put in the composer can write into the\n     * field. Speech recognition is **not** wired in here on purpose — it would make\n     * every consumer of `AIChat` pay for an API that streams audio to a third party.\n     */\n    composerRef?: Ref<AIChatComposerHandle>;\n    /** Under the composer field — token count, model name, a disclaimer. */\n    composerFooter?: ReactNode;\n    /** Disable the composer — no credits, conversation archived, offline. */\n    composerDisabled?: boolean;\n    /** Largest height the composer grows to, in lines. Default 8. */\n    maxRows?: number;\n    /**\n     * Called when `onSend` **or** `onEditSubmit` rejects. The draft stays in the field\n     * either way.\n     */\n    onSendError?: (error: unknown) => void;\n}\n\n/** How close to the bottom still counts as \"reading the newest turn\", in pixels. */\nconst BOTTOM_SLACK = 48;\n\n/**\n * A conversation with a model: role-based turns, Markdown answers, a reasoning\n * block, a streaming caret, per-turn actions and a composer that turns into a stop\n * button while a turn is generating.\n *\n * This is the shape ChatGPT, Claude and DeepSeek converged on, and it is a different\n * component from {@link Chat}, not a variant of it. A human thread is addressed by\n * author and cares about delivery state; a model transcript is addressed by role,\n * has no delivery state at all, and needs three things a human thread never does —\n * partial output, reasoning separate from the answer, and re-asking.\n *\n * Presentational and controlled, like the rest of the SDK: it takes a list and emits\n * intent (`onSend`, `onStop`, `onRegenerate`, `onEditSubmit`, `onFeedback`). The\n * transport stays with the app, because \"how do I stream from my backend\" has a\n * different answer per provider — the SDK's `createEventStream` covers SSE, `fetch`\n * with a `ReadableStream` covers the rest, and either way the app owns the\n * `AbortController` it hands to `onStop`.\n *\n * @example\n * <AIChat\n *     messages={turns}\n *     pending={pending}\n *     onSend={(text) => ask(text)}\n *     onStop={() => controller.current?.abort()}\n *     onRegenerate={(turn) => reask(turn)}\n *     onFeedback={(turn, vote) => track(\"answer_rated\", { id: turn.id, vote })}\n *     suggestions={[\"Resuma o último relatório\", \"Quais pedidos atrasaram?\"]}\n * />\n */\nexport function AIChat({\n    messages,\n    onSend,\n    onStop,\n    onRegenerate,\n    onEditSubmit,\n    onFeedback,\n    onRetry,\n    pending = false,\n    suggestions = [],\n    renderAvatar,\n    renderContent,\n    votes,\n    header,\n    emptyState,\n    showSystem = false,\n    defaultReasoningOpen = false,\n    showLineNumbers = false,\n    locale = \"pt-BR\",\n    placeholder,\n    composerActions,\n    composerRef,\n    composerFooter,\n    composerDisabled,\n    maxRows,\n    onSendError,\n    className,\n    ...rest\n}: AIChatProps) {\n    const strings = aiChatStrings(locale);\n    const turns = visibleTurns({ messages, showSystem });\n    const generating = pending || isGenerating(messages);\n    const newestAssistant = lastAssistantId(messages);\n    const signature = tailSignature(messages);\n\n    const composer = useRef<AIChatComposerHandle | null>(null);\n    const thread = useRef<HTMLDivElement | null>(null);\n    const stuckToBottom = useRef(true);\n    const wasGenerating = useRef(false);\n    const [atBottom, setAtBottom] = useState(true);\n    const announce = useAnnounce();\n\n    /**\n     * Remember whether the reader is at the bottom, before the next tokens land.\n     *\n     * A transcript that always scrolls to the newest text yanks somebody out of the\n     * answer they were re-reading — and with a streaming answer that would happen\n     * dozens of times per second. So the jump only happens when they were already at\n     * the bottom. The slack covers a partially visible last line.\n     */\n    const trackPosition = (): void => {\n        const node = thread.current;\n        if (!node) return;\n        const next = node.scrollHeight - node.scrollTop - node.clientHeight < BOTTOM_SLACK;\n        stuckToBottom.current = next;\n        setAtBottom(next);\n    };\n\n    const jumpToLatest = (): void => {\n        const node = thread.current;\n        if (!node) return;\n        stuckToBottom.current = true;\n        setAtBottom(true);\n        node.scrollTop = node.scrollHeight;\n    };\n\n    useLayoutEffect(() => {\n        const node = thread.current;\n        if (!node || !stuckToBottom.current) return;\n        node.scrollTop = node.scrollHeight;\n    }, [signature, pending]);\n\n    useEffect(() => {\n        const node = thread.current;\n        if (!node) return;\n        node.scrollTop = node.scrollHeight;\n        // Mount lands on the newest turn; from then on `trackPosition` decides.\n    }, []);\n\n    /**\n     * Announce the start and the end of a generation, and nothing in between.\n     *\n     * The transcript is a `role=\"log\"` **without** `aria-live`: a live region over\n     * streaming text makes a screen reader read the answer again on every token,\n     * which is unusable. The two moments that matter are announced here instead —\n     * through the shared `useAnnounce` region rather than a private one, so a page\n     * holding a chat plus a table plus toasts still has exactly one live region per\n     * politeness — and the finished answer is read from the log at the reader's own\n     * pace.\n     */\n    useEffect(() => {\n        if (generating) {\n            announce(strings.generating);\n            wasGenerating.current = true;\n            return;\n        }\n        if (wasGenerating.current) {\n            announce(strings.done);\n            wasGenerating.current = false;\n        }\n    }, [generating, strings.generating, strings.done, announce]);\n\n    const showSuggestions = turns.length === 0 && suggestions.length > 0 && onSend !== undefined;\n    const detached = !atBottom && turns.length > 0;\n\n    return (\n        <div className={cn(styles.panel, className)} {...rest}>\n            {header && <header className={styles.header}>{header}</header>}\n\n            <div className={styles.threadWrapper}>\n                <div\n                    ref={thread}\n                    className={styles.thread}\n                    onScroll={trackPosition}\n                    role=\"log\"\n                    tabIndex={0}\n                    aria-label={strings.thread}\n                >\n                    {turns.length === 0 && !showSuggestions\n                        ? (emptyState ?? (\n                              <EmptyState title={strings.empty} description={strings.emptyHint} />\n                          ))\n                        : turns.map((message) => (\n                              <AIChatTurn\n                                  key={message.id}\n                                  message={message}\n                                  locale={locale}\n                                  canRegenerate={\n                                      message.id === newestAssistant && message.streaming !== true\n                                  }\n                                  onRegenerate={onRegenerate}\n                                  onFeedback={onFeedback}\n                                  onEditSubmit={onEditSubmit}\n                                  onEditError={onSendError}\n                                  onRetry={onRetry}\n                                  renderAvatar={renderAvatar}\n                                  renderContent={renderContent}\n                                  vote={votes?.[message.id]}\n                                  defaultReasoningOpen={defaultReasoningOpen}\n                                  showLineNumbers={showLineNumbers}\n                              />\n                          ))}\n\n                    {showSuggestions && (\n                        <div className={styles.suggestions}>\n                            <p className={styles.suggestionsTitle}>{strings.emptyHint}</p>\n                            {suggestions.map((suggestion) => (\n                                <button\n                                    key={suggestion}\n                                    type=\"button\"\n                                    className={styles.suggestion}\n                                    onClick={() => void onSend?.(suggestion)}\n                                >\n                                    {suggestion}\n                                </button>\n                            ))}\n                        </div>\n                    )}\n\n                    {pending && !isGenerating(messages) && (\n                        <p className={styles.thinking}>\n                            <span className={styles.dots} aria-hidden=\"true\">\n                                <span />\n                                <span />\n                                <span />\n                            </span>\n                            {strings.thinking}\n                        </p>\n                    )}\n                </div>\n\n                {detached && (\n                    <button\n                        type=\"button\"\n                        className={styles.jump}\n                        onClick={jumpToLatest}\n                        aria-label={strings.jumpToLatest}\n                        title={strings.jumpToLatest}\n                    >\n                        <ArrowDown size={16} aria-hidden />\n                    </button>\n                )}\n            </div>\n\n            {onSend && (\n                <AIChatComposer\n                    ref={composerRef ?? composer}\n                    onSend={onSend}\n                    onStop={onStop}\n                    generating={generating}\n                    locale={locale}\n                    placeholder={placeholder}\n                    actions={composerActions}\n                    footer={composerFooter}\n                    disabled={composerDisabled}\n                    maxRows={maxRows}\n                    onError={onSendError}\n                />\n            )}\n        </div>\n    );\n}\n"],"mappings":"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"}