{"version":3,"file":"ai-chat-turns.cjs","names":[],"sources":["../../../src/components/AIChat/ai-chat-turns.ts"],"sourcesContent":["/** Who produced a turn. */\nexport type AIChatRole = \"user\" | \"assistant\" | \"system\";\n\n/** Rating an app can collect on an assistant turn. */\nexport type AIChatVote = \"up\" | \"down\";\n\n/** A file carried by a turn — an upload on the way in, a document on the way out. */\nexport interface AIChatAttachment {\n    /** Stable identity. Used as the React key. */\n    id: string;\n    /** Name shown in the chip. */\n    name: string;\n    /** Size in bytes. Formatted for display when given. */\n    size?: number;\n    /** Image URL. When set the attachment renders as a thumbnail instead of a chip. */\n    url?: string;\n    /** MIME type. Used as the chip's secondary label when there is no size. */\n    mimeType?: string;\n}\n\n/** One turn of a conversation with a model. */\nexport interface AIChatMessage {\n    /** Stable identity. Used as the React key and by every callback. */\n    id: string;\n    role: AIChatRole;\n    /**\n     * The text of the turn.\n     *\n     * An assistant turn is rendered as Markdown; a user turn is rendered as plain\n     * text with newlines preserved. That asymmetry is deliberate: a model emits\n     * Markdown by contract, while a person typing `2 * 3 * 4` did not mean to open\n     * an emphasis span.\n     */\n    content: string;\n    /**\n     * Reasoning the model exposed before answering — extended thinking, a\n     * chain-of-thought trace.\n     *\n     * Rendered in its own collapsible block above the answer, so a long trace never\n     * pushes the answer off screen.\n     */\n    reasoning?: string;\n    /**\n     * The turn is still arriving.\n     *\n     * Shows the caret, marks the block `aria-busy`, and hides the action row —\n     * copying or rating half an answer is never what somebody meant to do.\n     */\n    streaming?: boolean;\n    /** Generation failed. Shown under whatever streamed, with the retry control. */\n    error?: string;\n    /** Epoch milliseconds. */\n    createdAt?: number;\n    /** Model that produced the turn. Shown in the meta row of an assistant turn. */\n    model?: string;\n    attachments?: readonly AIChatAttachment[];\n    /** Anything the app wants to carry through to its own renderers. */\n    data?: Record<string, unknown>;\n}\n\n/**\n * Turns to render, in order.\n *\n * System turns are dropped unless asked for: a system prompt is configuration, and\n * an app that shows it by default leaks its own instructions into the transcript.\n *\n * @param params.messages - The thread, oldest first. Never reordered.\n * @param params.showSystem - Keep `\"system\"` turns. Default `false`.\n * @returns The visible turns, in the given order.\n */\nexport function visibleTurns({\n    messages,\n    showSystem = false,\n}: {\n    messages: readonly AIChatMessage[];\n    showSystem?: boolean;\n}): AIChatMessage[] {\n    return messages.filter((message) => showSystem || message.role !== \"system\");\n}\n\n/** Whether any turn in the thread is still streaming. */\nexport function isGenerating(messages: readonly AIChatMessage[]): boolean {\n    return messages.some((message) => message.streaming === true);\n}\n\n/**\n * Id of the newest assistant turn, or `null` when there is none.\n *\n * Only that turn gets the regenerate control: re-asking an older one would throw\n * away every turn after it, which is a different operation (\"branch here\") and\n * needs its own confirmation.\n */\nexport function lastAssistantId(messages: readonly AIChatMessage[]): string | null {\n    for (let index = messages.length - 1; index >= 0; index -= 1) {\n        if (messages[index].role === \"assistant\") return messages[index].id;\n    }\n    return null;\n}\n\n/**\n * A value that changes whenever the tail of the thread grows.\n *\n * The scroll effect cannot depend on the `messages` array alone. Streaming appends\n * to the **last** turn, and an app that mutates that object in place — or that\n * re-renders from a store holding the same array identity — would keep the same\n * dependency while the text grows, so the view would stop following the answer.\n * Length of the array, identity of the tail and length of its text together cover\n * both shapes.\n *\n * @param messages - The thread, oldest first.\n * @returns An opaque signature; compare with `===`.\n */\nexport function tailSignature(messages: readonly AIChatMessage[]): string {\n    const tail = messages[messages.length - 1];\n    if (!tail) return \"0\";\n    const reasoning = tail.reasoning?.length ?? 0;\n    return `${messages.length}:${tail.id}:${tail.content.length}:${reasoning}:${tail.streaming ? 1 : 0}`;\n}\n\n/** Labels the conversation needs, per locale. */\nexport interface AIChatStrings {\n    thread: string;\n    empty: string;\n    emptyHint: string;\n    placeholder: string;\n    send: string;\n    stop: string;\n    regenerate: string;\n    copy: string;\n    copied: string;\n    edit: string;\n    save: string;\n    cancel: string;\n    editing: string;\n    good: string;\n    bad: string;\n    reasoning: string;\n    thinking: string;\n    generating: string;\n    done: string;\n    stopped: string;\n    you: string;\n    assistant: string;\n    system: string;\n    retry: string;\n    jumpToLatest: string;\n    attachment: string;\n    turnActions: string;\n}\n\nconst PT_BR: AIChatStrings = {\n    thread: \"Conversa\",\n    empty: \"Comece a conversa\",\n    emptyHint: \"Pergunte qualquer coisa.\",\n    placeholder: \"Pergunte alguma coisa…\",\n    send: \"Enviar\",\n    stop: \"Parar\",\n    regenerate: \"Gerar de novo\",\n    copy: \"Copiar\",\n    copied: \"Copiado\",\n    edit: \"Editar\",\n    save: \"Enviar edição\",\n    cancel: \"Cancelar\",\n    editing: \"Editando a mensagem\",\n    good: \"Boa resposta\",\n    bad: \"Resposta ruim\",\n    reasoning: \"Raciocínio\",\n    thinking: \"Pensando…\",\n    generating: \"Gerando resposta\",\n    done: \"Resposta concluída\",\n    stopped: \"Geração interrompida\",\n    you: \"Você\",\n    assistant: \"Assistente\",\n    system: \"Sistema\",\n    retry: \"Tentar de novo\",\n    jumpToLatest: \"Ir para a última mensagem\",\n    attachment: \"Anexo\",\n    turnActions: \"Ações da mensagem\",\n};\n\nconst EN: AIChatStrings = {\n    thread: \"Conversation\",\n    empty: \"Start the conversation\",\n    emptyHint: \"Ask anything.\",\n    placeholder: \"Ask anything…\",\n    send: \"Send\",\n    stop: \"Stop\",\n    regenerate: \"Regenerate\",\n    copy: \"Copy\",\n    copied: \"Copied\",\n    edit: \"Edit\",\n    save: \"Send edit\",\n    cancel: \"Cancel\",\n    editing: \"Editing the message\",\n    good: \"Good response\",\n    bad: \"Bad response\",\n    reasoning: \"Reasoning\",\n    thinking: \"Thinking…\",\n    generating: \"Generating a response\",\n    done: \"Response complete\",\n    stopped: \"Generation stopped\",\n    you: \"You\",\n    assistant: \"Assistant\",\n    system: \"System\",\n    retry: \"Try again\",\n    jumpToLatest: \"Jump to the latest message\",\n    attachment: \"Attachment\",\n    turnActions: \"Message actions\",\n};\n\n/** Locale strings for the conversation. */\nexport function aiChatStrings(locale: \"pt-BR\" | \"en\"): AIChatStrings {\n    return locale === \"en\" ? EN : PT_BR;\n}\n\n/** Role label used in the turn header and by screen readers. */\nexport function roleLabel(role: AIChatRole, strings: AIChatStrings): string {\n    if (role === \"user\") return strings.you;\n    if (role === \"assistant\") return strings.assistant;\n    return strings.system;\n}\n\n/**\n * Clock label for a turn — the time, not a relative phrase.\n *\n * A transcript is read top to bottom in one sitting, so \"há 2 minutos\" on every turn\n * is noise that also has to be re-rendered on a timer. The wall clock is stable and\n * enough to answer the only question anyone asks of it (\"was this today?\").\n */\nexport function turnTime(timestamp: number, locale: \"pt-BR\" | \"en\" = \"pt-BR\"): string {\n    return new Date(timestamp).toLocaleTimeString(locale === \"en\" ? \"en-US\" : \"pt-BR\", {\n        hour: \"2-digit\",\n        minute: \"2-digit\",\n    });\n}\n"],"mappings":"AAsEA,SAAgB,EAAa,CACzB,WACA,aAAa,IAIG,CAChB,OAAO,EAAS,OAAQ,GAAY,GAAc,EAAQ,OAAS,QAAQ,CAC/E,CAGA,SAAgB,EAAa,EAA6C,CACtE,OAAO,EAAS,KAAM,GAAY,EAAQ,YAAc,EAAI,CAChE,CASA,SAAgB,EAAgB,EAAmD,CAC/E,IAAK,IAAI,EAAQ,EAAS,OAAS,EAAG,GAAS,EAAG,IAC9C,GAAI,EAAS,EAAM,CAAC,OAAS,YAAa,OAAO,EAAS,EAAM,CAAC,GAErE,OAAO,IACX,CAeA,SAAgB,EAAc,EAA4C,CACtE,IAAM,EAAO,EAAS,EAAS,OAAS,GACxC,GAAI,CAAC,EAAM,MAAO,IAClB,IAAM,EAAY,EAAK,WAAW,QAAU,EAC5C,MAAO,GAAG,EAAS,OAAO,GAAG,EAAK,GAAG,GAAG,EAAK,QAAQ,OAAO,GAAG,EAAU,GAAG,KAAK,WACrF,CAiCA,IAAM,EAAuB,CACzB,OAAQ,WACR,MAAO,oBACP,UAAW,2BACX,YAAa,yBACb,KAAM,SACN,KAAM,QACN,WAAY,gBACZ,KAAM,SACN,OAAQ,UACR,KAAM,SACN,KAAM,gBACN,OAAQ,WACR,QAAS,sBACT,KAAM,eACN,IAAK,gBACL,UAAW,aACX,SAAU,YACV,WAAY,mBACZ,KAAM,qBACN,QAAS,uBACT,IAAK,OACL,UAAW,aACX,OAAQ,UACR,MAAO,iBACP,aAAc,4BACd,WAAY,QACZ,YAAa,mBACjB,EAEM,EAAoB,CACtB,OAAQ,eACR,MAAO,yBACP,UAAW,gBACX,YAAa,gBACb,KAAM,OACN,KAAM,OACN,WAAY,aACZ,KAAM,OACN,OAAQ,SACR,KAAM,OACN,KAAM,YACN,OAAQ,SACR,QAAS,sBACT,KAAM,gBACN,IAAK,eACL,UAAW,YACX,SAAU,YACV,WAAY,wBACZ,KAAM,oBACN,QAAS,qBACT,IAAK,MACL,UAAW,YACX,OAAQ,SACR,MAAO,YACP,aAAc,6BACd,WAAY,aACZ,YAAa,iBACjB,EAGA,SAAgB,EAAc,EAAuC,CACjE,OAAO,IAAW,KAAO,EAAK,CAClC,CAGA,SAAgB,EAAU,EAAkB,EAAgC,CAGxE,OAFI,IAAS,OAAe,EAAQ,IAChC,IAAS,YAAoB,EAAQ,UAClC,EAAQ,MACnB,CASA,SAAgB,EAAS,EAAmB,EAAyB,QAAiB,CAClF,OAAO,IAAI,KAAK,CAAS,CAAC,CAAC,mBAAmB,IAAW,KAAO,QAAU,QAAS,CAC/E,KAAM,UACN,OAAQ,SACZ,CAAC,CACL"}