{"version":3,"file":"AIChatTurn.cjs","names":[],"sources":["../../../src/components/AIChat/AIChatTurn.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — one turn renders six\n * things that each have their own state: Markdown body, collapsible reasoning,\n * attachments, streaming caret, inline edit form and the action row. The 13 props\n * are the callbacks those actions need plus the render slots, and the body is long\n * because the edit form and the action row share the turn's message and its pending\n * state.\n */\nimport { ChevronRight, Copy, Pencil, RefreshCw, ThumbsDown, ThumbsUp } from \"lucide-react\";\nimport { useMemo, useState, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { formatBytes } from \"@/utils/numbers\";\n\nimport { Collapsible } from \"../Collapsible\";\nimport { CopyButton } from \"../CopyButton\";\nimport { Markdown } from \"../Markdown\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n    aiChatStrings,\n    roleLabel,\n    turnTime,\n    type AIChatMessage,\n    type AIChatVote,\n} from \"./ai-chat-turns\";\nimport styles from \"./AIChat.module.css\";\n\nexport interface AIChatTurnProps {\n    /** The turn to render. */\n    message: AIChatMessage;\n    /** Locale for the labels. Default `\"pt-BR\"`. */\n    locale?: \"pt-BR\" | \"en\";\n    /**\n     * Offer the regenerate control.\n     *\n     * Only the newest assistant turn should get it — re-asking an older one throws\n     * away every turn after it, which is a different operation and needs its own\n     * confirmation.\n     */\n    canRegenerate?: boolean;\n    onRegenerate?: (message: AIChatMessage) => void;\n    onFeedback?: (message: AIChatMessage, vote: AIChatVote) => void;\n    /** Enables the edit control on a user turn. Receives the edited prompt. */\n    onEditSubmit?: (message: AIChatMessage, text: string) => void | Promise<void>;\n    /**\n     * Called when `onEditSubmit` rejects. The editor stays open with the draft either\n     * way.\n     *\n     * Without it the rejection is swallowed after the draft is preserved: re-throwing\n     * out of a click handler surfaces as an unhandled promise rejection, which is\n     * console noise for the developer and nothing the user can act on.\n     */\n    onEditError?: (error: unknown) => void;\n    /** Enables the retry control on a turn that carries an `error`. */\n    onRetry?: (message: AIChatMessage) => void;\n    renderAvatar?: (message: AIChatMessage) => ReactNode;\n    /** Render the body yourself — a tool-call card, a chart, a citation list. */\n    renderContent?: (message: AIChatMessage) => ReactNode;\n    /**\n     * Rating to show as pressed.\n     *\n     * Pass it to keep votes in app state (persisted across a reload); leave it out\n     * and the pressed state is kept locally, which is enough for a fire-and-forget\n     * `onFeedback`.\n     */\n    vote?: AIChatVote;\n    /** Reasoning blocks start expanded. Default `false`. */\n    defaultReasoningOpen?: boolean;\n    /** Show line numbers in fenced code. Default `false`. */\n    showLineNumbers?: boolean;\n}\n\n/**\n * One turn of a conversation with a model.\n *\n * Exported for apps that build their own transcript layout (a split view, a diff of\n * two answers) but still want the SDK's turn: Markdown body, reasoning block,\n * attachments, streaming caret, error state and the action row.\n *\n * @example\n * <AIChatTurn message={turn} canRegenerate onRegenerate={(m) => reask(m)} />\n */\nexport function AIChatTurn({\n    message,\n    locale = \"pt-BR\",\n    canRegenerate = false,\n    onRegenerate,\n    onFeedback,\n    onEditSubmit,\n    onEditError,\n    onRetry,\n    renderAvatar,\n    renderContent,\n    vote,\n    defaultReasoningOpen = false,\n    showLineNumbers = false,\n}: AIChatTurnProps) {\n    const strings = aiChatStrings(locale);\n    const { role, content, reasoning, streaming, error, model, createdAt, attachments } = message;\n    const [localVote, setLocalVote] = useState<AIChatVote | null>(null);\n    const [editing, setEditing] = useState(false);\n    const [draft, setDraft] = useState(content);\n    const [saving, setSaving] = useState(false);\n\n    const currentVote = vote ?? localVote;\n    const isUser = role === \"user\";\n\n    /**\n     * Memoised so a streaming answer re-parses **only the turn that is growing**.\n     *\n     * `Markdown` parses in its own render, and React skips re-rendering a child whose\n     * element is referentially identical to the previous one. Holding the element\n     * across renders is therefore what keeps a fifty-turn transcript from re-parsing\n     * every finished answer on every token of the newest one.\n     */\n    const markdown = useMemo(\n        () => (\n            <Markdown\n                source={content}\n                showLineNumbers={showLineNumbers}\n                className={styles.markdownRoot}\n            />\n        ),\n        [content, showLineNumbers],\n    );\n\n    /**\n     * Body of the turn.\n     *\n     * A user turn is plain text with newlines preserved, an assistant turn is\n     * Markdown. A model emits Markdown by contract; a person typing `2 * 3 * 4` did\n     * not mean to open an emphasis span.\n     */\n    const body = ((): ReactNode => {\n        if (renderContent) return renderContent(message);\n        if (isUser) return <div className={styles.plain}>{content}</div>;\n        if (content === \"\") return null;\n        return markdown;\n    })();\n\n    const submitEdit = async (): Promise<void> => {\n        const text = draft.trim();\n        if (!text || saving || !onEditSubmit) return;\n        setSaving(true);\n        try {\n            await onEditSubmit(message, text);\n            setEditing(false);\n        } catch (error) {\n            onEditError?.(error);\n        } finally {\n            setSaving(false);\n        }\n    };\n\n    const rate = (next: AIChatVote): void => {\n        if (currentVote === next) return;\n        setLocalVote(next);\n        onFeedback?.(message, next);\n    };\n\n    const showActions = !streaming && !editing;\n    const hasAssistantActions =\n        onFeedback !== undefined || (canRegenerate && onRegenerate !== undefined);\n\n    return (\n        <article\n            className={cn(\n                styles.turn,\n                isUser && styles.userTurn,\n                role === \"system\" && styles.systemTurn,\n                streaming && styles.streamingTurn,\n            )}\n            aria-busy={streaming || undefined}\n            data-role={role}\n        >\n            <header className={styles.turnHeader}>\n                {renderAvatar && (\n                    <span className={styles.avatar} aria-hidden=\"true\">\n                        {renderAvatar(message)}\n                    </span>\n                )}\n                <span className={cn(styles.role, isUser && styles.visuallyHiddenRole)}>\n                    {roleLabel(role, strings)}\n                </span>\n                {model && !isUser && <span className={styles.model}>{model}</span>}\n                {createdAt !== undefined && (\n                    <time className={styles.time} dateTime={new Date(createdAt).toISOString()}>\n                        {turnTime(createdAt, locale)}\n                    </time>\n                )}\n            </header>\n\n            {attachments && attachments.length > 0 && (\n                <ul className={styles.attachments}>\n                    {attachments.map((attachment) => (\n                        <li key={attachment.id} className={styles.attachment}>\n                            {attachment.url ? (\n                                <img\n                                    className={styles.thumb}\n                                    src={attachment.url}\n                                    alt={attachment.name}\n                                />\n                            ) : (\n                                <>\n                                    <span className={styles.attachmentName}>{attachment.name}</span>\n                                    <span className={styles.attachmentMeta}>\n                                        {attachment.size !== undefined\n                                            ? formatBytes(attachment.size)\n                                            : attachment.mimeType}\n                                    </span>\n                                </>\n                            )}\n                        </li>\n                    ))}\n                </ul>\n            )}\n\n            {reasoning && (\n                <Collapsible\n                    className={styles.reasoning}\n                    defaultOpen={defaultReasoningOpen || (streaming === true && content === \"\")}\n                    trigger={\n                        <>\n                            <ChevronRight\n                                className={styles.reasoningChevron}\n                                size={14}\n                                aria-hidden\n                            />\n                            {strings.reasoning}\n                        </>\n                    }\n                >\n                    <div className={styles.reasoningBody}>{reasoning}</div>\n                </Collapsible>\n            )}\n\n            {editing ? (\n                <div className={styles.editor}>\n                    <VisuallyHidden>\n                        <label htmlFor={`${message.id}-edit`}>{strings.editing}</label>\n                    </VisuallyHidden>\n                    <textarea\n                        id={`${message.id}-edit`}\n                        className={styles.editField}\n                        value={draft}\n                        rows={Math.min(draft.split(\"\\n\").length + 1, 10)}\n                        onChange={(event) => setDraft(event.target.value)}\n                    />\n                    <div className={styles.editActions}>\n                        <button\n                            type=\"button\"\n                            className={styles.ghost}\n                            onClick={() => {\n                                setDraft(content);\n                                setEditing(false);\n                            }}\n                        >\n                            {strings.cancel}\n                        </button>\n                        <button\n                            type=\"button\"\n                            className={styles.primary}\n                            disabled={saving || draft.trim() === \"\"}\n                            onClick={() => void submitEdit()}\n                        >\n                            {strings.save}\n                        </button>\n                    </div>\n                </div>\n            ) : (\n                <div className={cn(styles.body, streaming && styles.streamingBody)}>\n                    {body}\n                    {streaming === true && content === \"\" && (\n                        <span className={styles.dots} aria-hidden=\"true\">\n                            <span />\n                            <span />\n                            <span />\n                        </span>\n                    )}\n                </div>\n            )}\n\n            {error && (\n                <p className={styles.error} role=\"alert\">\n                    <span>{error}</span>\n                    {onRetry && (\n                        <button\n                            type=\"button\"\n                            className={styles.retry}\n                            onClick={() => onRetry(message)}\n                        >\n                            {strings.retry}\n                        </button>\n                    )}\n                </p>\n            )}\n\n            {showActions && (\n                <div className={styles.actions} aria-label={strings.turnActions} role=\"group\">\n                    <CopyButton\n                        className={styles.action}\n                        value={content}\n                        aria-label={strings.copy}\n                        title={strings.copy}\n                    >\n                        <Copy size={14} aria-hidden />\n                    </CopyButton>\n\n                    {isUser && onEditSubmit && (\n                        <button\n                            type=\"button\"\n                            className={styles.action}\n                            aria-label={strings.edit}\n                            title={strings.edit}\n                            onClick={() => {\n                                setDraft(content);\n                                setEditing(true);\n                            }}\n                        >\n                            <Pencil size={14} aria-hidden />\n                        </button>\n                    )}\n\n                    {!isUser && hasAssistantActions && (\n                        <>\n                            {canRegenerate && onRegenerate && (\n                                <button\n                                    type=\"button\"\n                                    className={styles.action}\n                                    aria-label={strings.regenerate}\n                                    title={strings.regenerate}\n                                    onClick={() => onRegenerate(message)}\n                                >\n                                    <RefreshCw size={14} aria-hidden />\n                                </button>\n                            )}\n                            {onFeedback && (\n                                <>\n                                    <button\n                                        type=\"button\"\n                                        className={cn(\n                                            styles.action,\n                                            currentVote === \"up\" && styles.actionActive,\n                                        )}\n                                        aria-label={strings.good}\n                                        title={strings.good}\n                                        aria-pressed={currentVote === \"up\"}\n                                        onClick={() => rate(\"up\")}\n                                    >\n                                        <ThumbsUp size={14} aria-hidden />\n                                    </button>\n                                    <button\n                                        type=\"button\"\n                                        className={cn(\n                                            styles.action,\n                                            currentVote === \"down\" && styles.actionActive,\n                                        )}\n                                        aria-label={strings.bad}\n                                        title={strings.bad}\n                                        aria-pressed={currentVote === \"down\"}\n                                        onClick={() => rate(\"down\")}\n                                    >\n                                        <ThumbsDown size={14} aria-hidden />\n                                    </button>\n                                </>\n                            )}\n                        </>\n                    )}\n                </div>\n            )}\n        </article>\n    );\n}\n"],"mappings":"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"}