tempest-react-sdk 0.33.2 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +3 -3
  2. package/dist/components/AIChat/AIChat.cjs +2 -0
  3. package/dist/components/AIChat/AIChat.cjs.map +1 -0
  4. package/dist/components/AIChat/AIChat.js +141 -0
  5. package/dist/components/AIChat/AIChat.js.map +1 -0
  6. package/dist/components/AIChat/AIChat.module.cjs +2 -0
  7. package/dist/components/AIChat/AIChat.module.cjs.map +1 -0
  8. package/dist/components/AIChat/AIChat.module.js +58 -0
  9. package/dist/components/AIChat/AIChat.module.js.map +1 -0
  10. package/dist/components/AIChat/AIChatComposer.cjs +2 -0
  11. package/dist/components/AIChat/AIChatComposer.cjs.map +1 -0
  12. package/dist/components/AIChat/AIChatComposer.js +87 -0
  13. package/dist/components/AIChat/AIChatComposer.js.map +1 -0
  14. package/dist/components/AIChat/AIChatTurn.cjs +3 -0
  15. package/dist/components/AIChat/AIChatTurn.cjs.map +1 -0
  16. package/dist/components/AIChat/AIChatTurn.js +217 -0
  17. package/dist/components/AIChat/AIChatTurn.js.map +1 -0
  18. package/dist/components/AIChat/ai-chat-turns.cjs +2 -0
  19. package/dist/components/AIChat/ai-chat-turns.cjs.map +1 -0
  20. package/dist/components/AIChat/ai-chat-turns.js +90 -0
  21. package/dist/components/AIChat/ai-chat-turns.js.map +1 -0
  22. package/dist/styles.css +1 -1
  23. package/dist/tempest-react-sdk.cjs +1 -1
  24. package/dist/tempest-react-sdk.d.ts +415 -9
  25. package/dist/tempest-react-sdk.js +118 -114
  26. package/dist/ws/create-web-socket.cjs +1 -1
  27. package/dist/ws/create-web-socket.cjs.map +1 -1
  28. package/dist/ws/create-web-socket.js +54 -37
  29. package/dist/ws/create-web-socket.js.map +1 -1
  30. package/dist/ws/use-web-socket.cjs +1 -1
  31. package/dist/ws/use-web-socket.cjs.map +1 -1
  32. package/dist/ws/use-web-socket.js +49 -18
  33. package/dist/ws/use-web-socket.js.map +1 -1
  34. package/package.json +1 -1
  35. package/template/src/lib/api.ts +27 -5
  36. package/template/src/stores/auth.ts +14 -0
package/README.md CHANGED
@@ -1835,9 +1835,9 @@ npm run clean # rm -rf dist coverage
1835
1835
 
1836
1836
  Snapshot of current health:
1837
1837
 
1838
- - 2488 tests / 377 files — 98.5% lines, 97.1% statements, 96.6% functions, 95.0% branches (CI floors: 98/97/96/94).
1839
- - What your app actually pays (brotli, tree-shaken slices measured by `npm run size`): `cn` alone 153 B · one `Button` 794 B · a typical app shell (5 components + router + providers + HTTP + auth + a hook) 6.83 KB · offline/PWA surface 4.44 KB · `styles.css` 21.68 KB · the opt-in `utilities.css` 1.13 KB.
1840
- - Full-barrel ceiling — nobody imports this, it is the no-tree-shaking worst case: 70.21 KB ESM / 85.52 KB CJS.
1838
+ - 3969 tests / 451 files — 98.6% lines, 97.2% statements, 97.0% functions, 94.3% branches (CI floors: 98/97/96/94).
1839
+ - What your app actually pays (brotli, tree-shaken slices measured by `npm run size`): `cn` alone 153 B · one `Button` 794 B · a typical app shell (5 components + router + providers + HTTP + auth + a hook) 6.88 KB · offline/PWA surface 4.34 KB · `AIChat` (LLM transcript, pulls `Markdown` + `CodeBlock`) 9.41 KB · `styles.css` 26.33 KB · the opt-in `utilities.css` 1.33 KB.
1840
+ - Full-barrel ceiling — nobody imports this, it is the no-tree-shaking worst case: 94.27 KB ESM / 113.46 KB CJS.
1841
1841
  - Husky pre-commit runs `lint-staged` (eslint --fix + prettier --write) on staged files.
1842
1842
 
1843
1843
  The demo gallery lives in `examples/gallery` and consumes the local SDK via `file:../..`:
@@ -0,0 +1,2 @@
1
+ const e=require("../../utils/cn.cjs"),t=require("../EmptyState/EmptyState.cjs"),n=require("../VisuallyHidden/VisuallyHidden.cjs"),r=require("./ai-chat-turns.cjs"),i=require("./AIChat.module.cjs"),a=require("./AIChatComposer.cjs"),o=require("./AIChatTurn.cjs");let s=require("react"),c=require("react/jsx-runtime"),l=require("lucide-react");var u=48;function d({messages:d,onSend:f,onStop:p,onRegenerate:m,onEditSubmit:h,onFeedback:g,onRetry:_,pending:v=!1,suggestions:y=[],renderAvatar:b,renderContent:x,votes:S,header:C,emptyState:w,showSystem:T=!1,defaultReasoningOpen:E=!1,showLineNumbers:D=!1,locale:O=`pt-BR`,placeholder:k,composerActions:A,composerFooter:j,composerDisabled:M,maxRows:N,onSendError:P,className:F,...I}){let L=r.aiChatStrings(O),R=r.visibleTurns({messages:d,showSystem:T}),z=v||r.isGenerating(d),B=r.lastAssistantId(d),V=r.tailSignature(d),H=(0,s.useRef)(null),U=(0,s.useRef)(null),W=(0,s.useRef)(!0),G=(0,s.useRef)(!1),[K,q]=(0,s.useState)(!0),[J,Y]=(0,s.useState)(``),X=()=>{let e=U.current;if(!e)return;let t=e.scrollHeight-e.scrollTop-e.clientHeight<u;W.current=t,q(t)},Z=()=>{let e=U.current;e&&(W.current=!0,q(!0),e.scrollTop=e.scrollHeight)};(0,s.useLayoutEffect)(()=>{let e=U.current;!e||!W.current||(e.scrollTop=e.scrollHeight)},[V,v]),(0,s.useEffect)(()=>{let e=U.current;e&&(e.scrollTop=e.scrollHeight)},[]),(0,s.useEffect)(()=>{if(z){Y(L.generating),G.current=!0;return}G.current&&=(Y(L.done),!1)},[z,L.generating,L.done]);let Q=R.length===0&&y.length>0&&f!==void 0,$=!K&&R.length>0;return(0,c.jsxs)(`div`,{className:e.cn(i.default.panel,F),...I,children:[C&&(0,c.jsx)(`header`,{className:i.default.header,children:C}),(0,c.jsxs)(`div`,{className:i.default.threadWrapper,children:[(0,c.jsxs)(`div`,{ref:U,className:i.default.thread,onScroll:X,role:`log`,tabIndex:0,"aria-label":L.thread,children:[R.length===0&&!Q?w??(0,c.jsx)(t.EmptyState,{title:L.empty,description:L.emptyHint}):R.map(e=>(0,c.jsx)(o.AIChatTurn,{message:e,locale:O,canRegenerate:e.id===B&&e.streaming!==!0,onRegenerate:m,onFeedback:g,onEditSubmit:h,onEditError:P,onRetry:_,renderAvatar:b,renderContent:x,vote:S?.[e.id],defaultReasoningOpen:E,showLineNumbers:D},e.id)),Q&&(0,c.jsxs)(`div`,{className:i.default.suggestions,children:[(0,c.jsx)(`p`,{className:i.default.suggestionsTitle,children:L.emptyHint}),y.map(e=>(0,c.jsx)(`button`,{type:`button`,className:i.default.suggestion,onClick:()=>void f?.(e),children:e},e))]}),v&&!r.isGenerating(d)&&(0,c.jsxs)(`p`,{className:i.default.thinking,children:[(0,c.jsxs)(`span`,{className:i.default.dots,"aria-hidden":`true`,children:[(0,c.jsx)(`span`,{}),(0,c.jsx)(`span`,{}),(0,c.jsx)(`span`,{})]}),L.thinking]})]}),$&&(0,c.jsx)(`button`,{type:`button`,className:i.default.jump,onClick:Z,"aria-label":L.jumpToLatest,title:L.jumpToLatest,children:(0,c.jsx)(l.ArrowDown,{size:16,"aria-hidden":!0})})]}),(0,c.jsx)(n.VisuallyHidden,{children:(0,c.jsx)(`div`,{role:`status`,"aria-live":`polite`,children:J})}),f&&(0,c.jsx)(a.AIChatComposer,{ref:H,onSend:f,onStop:p,generating:z,locale:O,placeholder:k,actions:A,footer:j,disabled:M,maxRows:N,onError:P})]})}exports.AIChat=d;
2
+ //# sourceMappingURL=AIChat.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AIChat.cjs","names":[],"sources":["../../../src/components/AIChat/AIChat.tsx"],"sourcesContent":["import { ArrowDown } from \"lucide-react\";\nimport {\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type HTMLAttributes,\n type ReactNode,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { EmptyState } from \"../EmptyState\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\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 /** 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 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 [announcement, setAnnouncement] = useState(\"\");\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, and\n * the finished answer is read from the log at the reader's own pace.\n */\n useEffect(() => {\n if (generating) {\n setAnnouncement(strings.generating);\n wasGenerating.current = true;\n return;\n }\n if (wasGenerating.current) {\n setAnnouncement(strings.done);\n wasGenerating.current = false;\n }\n }, [generating, strings.generating, strings.done]);\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 <VisuallyHidden>\n <div role=\"status\" aria-live=\"polite\">\n {announcement}\n </div>\n </VisuallyHidden>\n\n {onSend && (\n <AIChatComposer\n ref={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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"}
@@ -0,0 +1,141 @@
1
+ import { cn as e } from "../../utils/cn.js";
2
+ import { EmptyState as t } from "../EmptyState/EmptyState.js";
3
+ import { VisuallyHidden as n } from "../VisuallyHidden/VisuallyHidden.js";
4
+ import { aiChatStrings as r, isGenerating as i, lastAssistantId as a, tailSignature as o, visibleTurns as ee } from "./ai-chat-turns.js";
5
+ import s from "./AIChat.module.js";
6
+ import { AIChatComposer as c } from "./AIChatComposer.js";
7
+ import { AIChatTurn as l } from "./AIChatTurn.js";
8
+ import { useEffect as u, useLayoutEffect as d, useRef as f, useState as p } from "react";
9
+ import { jsx as m, jsxs as h } from "react/jsx-runtime";
10
+ import { ArrowDown as g } from "lucide-react";
11
+ //#region src/components/AIChat/AIChat.tsx
12
+ var _ = 48;
13
+ function v({ messages: v, onSend: y, onStop: b, onRegenerate: x, onEditSubmit: te, onFeedback: ne, onRetry: re, pending: S = !1, suggestions: C = [], renderAvatar: w, renderContent: T, votes: E, header: D, emptyState: O, showSystem: k = !1, defaultReasoningOpen: A = !1, showLineNumbers: j = !1, locale: M = "pt-BR", placeholder: N, composerActions: P, composerFooter: F, composerDisabled: I, maxRows: L, onSendError: R, className: z, ...B }) {
14
+ let V = r(M), H = ee({
15
+ messages: v,
16
+ showSystem: k
17
+ }), U = S || i(v), W = a(v), G = o(v), K = f(null), q = f(null), J = f(!0), Y = f(!1), [X, Z] = p(!0), [ie, Q] = p(""), ae = () => {
18
+ let e = q.current;
19
+ if (!e) return;
20
+ let t = e.scrollHeight - e.scrollTop - e.clientHeight < _;
21
+ J.current = t, Z(t);
22
+ }, oe = () => {
23
+ let e = q.current;
24
+ e && (J.current = !0, Z(!0), e.scrollTop = e.scrollHeight);
25
+ };
26
+ d(() => {
27
+ let e = q.current;
28
+ !e || !J.current || (e.scrollTop = e.scrollHeight);
29
+ }, [G, S]), u(() => {
30
+ let e = q.current;
31
+ e && (e.scrollTop = e.scrollHeight);
32
+ }, []), u(() => {
33
+ if (U) {
34
+ Q(V.generating), Y.current = !0;
35
+ return;
36
+ }
37
+ Y.current &&= (Q(V.done), !1);
38
+ }, [
39
+ U,
40
+ V.generating,
41
+ V.done
42
+ ]);
43
+ let $ = H.length === 0 && C.length > 0 && y !== void 0, se = !X && H.length > 0;
44
+ return /* @__PURE__ */ h("div", {
45
+ className: e(s.panel, z),
46
+ ...B,
47
+ children: [
48
+ D && /* @__PURE__ */ m("header", {
49
+ className: s.header,
50
+ children: D
51
+ }),
52
+ /* @__PURE__ */ h("div", {
53
+ className: s.threadWrapper,
54
+ children: [/* @__PURE__ */ h("div", {
55
+ ref: q,
56
+ className: s.thread,
57
+ onScroll: ae,
58
+ role: "log",
59
+ tabIndex: 0,
60
+ "aria-label": V.thread,
61
+ children: [
62
+ H.length === 0 && !$ ? O ?? /* @__PURE__ */ m(t, {
63
+ title: V.empty,
64
+ description: V.emptyHint
65
+ }) : H.map((e) => /* @__PURE__ */ m(l, {
66
+ message: e,
67
+ locale: M,
68
+ canRegenerate: e.id === W && e.streaming !== !0,
69
+ onRegenerate: x,
70
+ onFeedback: ne,
71
+ onEditSubmit: te,
72
+ onEditError: R,
73
+ onRetry: re,
74
+ renderAvatar: w,
75
+ renderContent: T,
76
+ vote: E?.[e.id],
77
+ defaultReasoningOpen: A,
78
+ showLineNumbers: j
79
+ }, e.id)),
80
+ $ && /* @__PURE__ */ h("div", {
81
+ className: s.suggestions,
82
+ children: [/* @__PURE__ */ m("p", {
83
+ className: s.suggestionsTitle,
84
+ children: V.emptyHint
85
+ }), C.map((e) => /* @__PURE__ */ m("button", {
86
+ type: "button",
87
+ className: s.suggestion,
88
+ onClick: () => void y?.(e),
89
+ children: e
90
+ }, e))]
91
+ }),
92
+ S && !i(v) && /* @__PURE__ */ h("p", {
93
+ className: s.thinking,
94
+ children: [/* @__PURE__ */ h("span", {
95
+ className: s.dots,
96
+ "aria-hidden": "true",
97
+ children: [
98
+ /* @__PURE__ */ m("span", {}),
99
+ /* @__PURE__ */ m("span", {}),
100
+ /* @__PURE__ */ m("span", {})
101
+ ]
102
+ }), V.thinking]
103
+ })
104
+ ]
105
+ }), se && /* @__PURE__ */ m("button", {
106
+ type: "button",
107
+ className: s.jump,
108
+ onClick: oe,
109
+ "aria-label": V.jumpToLatest,
110
+ title: V.jumpToLatest,
111
+ children: /* @__PURE__ */ m(g, {
112
+ size: 16,
113
+ "aria-hidden": !0
114
+ })
115
+ })]
116
+ }),
117
+ /* @__PURE__ */ m(n, { children: /* @__PURE__ */ m("div", {
118
+ role: "status",
119
+ "aria-live": "polite",
120
+ children: ie
121
+ }) }),
122
+ y && /* @__PURE__ */ m(c, {
123
+ ref: K,
124
+ onSend: y,
125
+ onStop: b,
126
+ generating: U,
127
+ locale: M,
128
+ placeholder: N,
129
+ actions: P,
130
+ footer: F,
131
+ disabled: I,
132
+ maxRows: L,
133
+ onError: R
134
+ })
135
+ ]
136
+ });
137
+ }
138
+ //#endregion
139
+ export { v as AIChat };
140
+
141
+ //# sourceMappingURL=AIChat.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AIChat.js","names":[],"sources":["../../../src/components/AIChat/AIChat.tsx"],"sourcesContent":["import { ArrowDown } from \"lucide-react\";\nimport {\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type HTMLAttributes,\n type ReactNode,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { EmptyState } from \"../EmptyState\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\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 /** 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 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 [announcement, setAnnouncement] = useState(\"\");\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, and\n * the finished answer is read from the log at the reader's own pace.\n */\n useEffect(() => {\n if (generating) {\n setAnnouncement(strings.generating);\n wasGenerating.current = true;\n return;\n }\n if (wasGenerating.current) {\n setAnnouncement(strings.done);\n wasGenerating.current = false;\n }\n }, [generating, strings.generating, strings.done]);\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 <VisuallyHidden>\n <div role=\"status\" aria-live=\"polite\">\n {announcement}\n </div>\n </VisuallyHidden>\n\n {onSend && (\n <AIChatComposer\n ref={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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"}
@@ -0,0 +1,2 @@
1
+ var e=`tempest_panel_W0CCy`,t=`tempest_header_KYy5l`,n=`tempest_threadWrapper_LjoF6`,r=`tempest_thread_RqELz`,i=`tempest_jump_oBKxQ`,a=`tempest_turn_k7h7J`,o=`tempest_userTurn_7Csx0`,s=`tempest_systemTurn_xpQmC`,c=`tempest_turnHeader_l7lv3`,l=`tempest_avatar_XqQ8R`,u=`tempest_role_Vz6Dn`,d=`tempest_visuallyHiddenRole_raKmR`,f=`tempest_model_Z55Ho`,p=`tempest_time_o07s8`,m=`tempest_body_GnhC7`,h=`tempest_plain_MRQet`,g=`tempest_markdownRoot_nem2U`,_=`tempest_streamingBody_YMx1-`,v=`tempest_caretBlink_IpIIw`,y=`tempest_dots_FtZUe`,b=`tempest_dotPulse_nZJHU`,x=`tempest_thinking_Dk96l`,S=`tempest_reasoning_kJof1`,C=`tempest_reasoningChevron_TObJ2`,w=`tempest_reasoningBody_ZR910`,T=`tempest_attachments_qQH3a`,E=`tempest_attachment_z4T-R`,D=`tempest_attachmentName_2DCGR`,O=`tempest_attachmentMeta_xBy5U`,k=`tempest_thumb_w70uR`,A=`tempest_error_CS0Gp`,j=`tempest_retry_8a6Vm`,M=`tempest_actions_SZnxe`,N=`tempest_action_YPvQH`,P=`tempest_actionActive_DW5O1`,F=`tempest_editor_rk4hA`,I=`tempest_editField_Z9neJ`,L=`tempest_editActions_vfC0b`,R=`tempest_ghost_S1yuC`,z=`tempest_primary_wZ-Wu`,B=`tempest_suggestions_V7Zhh`,V=`tempest_suggestionsTitle_Z-wWe`,H=`tempest_suggestion_4NYhq`,U=`tempest_composer_AaB1Q`,W=`tempest_composerBox_Fz-GG`,G=`tempest_field_7m1Pa`,K=`tempest_composerActions_z1oj9`,q=`tempest_composerFooter_WZ1Fo`,J=`tempest_send_eVkd6`,Y=`tempest_stop_KWS9o`,X=`tempest_stopGlyph_Nc19c`,Z={panel:e,header:t,threadWrapper:n,thread:r,jump:i,turn:a,userTurn:o,systemTurn:s,turnHeader:c,avatar:l,role:u,visuallyHiddenRole:d,model:f,time:p,body:m,plain:h,markdownRoot:g,streamingBody:_,caretBlink:v,dots:y,dotPulse:b,thinking:x,reasoning:S,reasoningChevron:C,reasoningBody:w,attachments:T,attachment:E,attachmentName:D,attachmentMeta:O,thumb:k,error:A,retry:j,actions:M,action:N,actionActive:P,editor:F,editField:I,editActions:L,ghost:R,primary:z,suggestions:B,suggestionsTitle:V,suggestion:H,composer:U,composerBox:W,field:G,composerActions:K,composerFooter:q,send:J,stop:Y,stopGlyph:X};exports.action=N,exports.actionActive=P,exports.actions=M,exports.attachment=E,exports.attachmentMeta=O,exports.attachmentName=D,exports.attachments=T,exports.avatar=l,exports.body=m,exports.caretBlink=v,exports.composer=U,exports.composerActions=K,exports.composerBox=W,exports.composerFooter=q,exports.default=Z,exports.dotPulse=b,exports.dots=y,exports.editActions=L,exports.editField=I,exports.editor=F,exports.error=A,exports.field=G,exports.ghost=R,exports.header=t,exports.jump=i,exports.markdownRoot=g,exports.model=f,exports.panel=e,exports.plain=h,exports.primary=z,exports.reasoning=S,exports.reasoningBody=w,exports.reasoningChevron=C,exports.retry=j,exports.role=u,exports.send=J,exports.stop=Y,exports.stopGlyph=X,exports.streamingBody=_,exports.suggestion=H,exports.suggestions=B,exports.suggestionsTitle=V,exports.systemTurn=s,exports.thinking=x,exports.thread=r,exports.threadWrapper=n,exports.thumb=k,exports.time=p,exports.turn=a,exports.turnHeader=c,exports.userTurn=o,exports.visuallyHiddenRole=d;
2
+ //# sourceMappingURL=AIChat.module.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AIChat.module.cjs","names":[],"sources":["../../../src/components/AIChat/AIChat.module.css"],"sourcesContent":[".panel {\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n height: var(--tempest-ai-chat-height, 100%);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font-family: var(--tempest-font-sans);\n}\n\n.header {\n flex: 0 0 auto;\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-bottom: 1px solid var(--tempest-border);\n}\n\n/*\n * The wrapper exists only so the jump-to-latest button can be positioned against\n * the scroll area without being scrolled away with it.\n */\n.threadWrapper {\n position: relative;\n display: flex;\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n}\n\n/*\n * `min-width: 0` is load-bearing, not defensive.\n *\n * A flex item defaults to `min-width: auto`, which means \"at least as wide as my\n * content wants\". An answer carrying a wide table or a long line of SQL would make\n * the scroll area itself grow past the panel — and the table's own `overflow-x`\n * inside `Markdown` never gets a chance to kick in, because it is already as wide as\n * it asked to be. Zeroing the floor here is what pushes the overflow back down to\n * the table and the code block, where it scrolls in its own box.\n */\n.thread {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--tempest-space-5);\n min-width: 0;\n min-height: 0;\n padding: var(--tempest-space-4);\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n\n.thread:focus-visible {\n outline: 2px solid var(--tempest-primary);\n outline-offset: -2px;\n}\n\n.jump {\n position: absolute;\n right: var(--tempest-space-4);\n bottom: var(--tempest-space-3);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 2rem;\n height: 2rem;\n padding: 0;\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-full);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n box-shadow: var(--tempest-shadow-md);\n cursor: pointer;\n}\n\n.jump:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 2px;\n}\n\n/* Same 44×44 hit-slop as the turn actions: 2rem is a small target for a thumb. */\n@media (pointer: coarse) {\n .jump::after {\n content: \"\";\n position: absolute;\n inset: -6px;\n }\n}\n\n/* --- turn ------------------------------------------------------------------ */\n\n/*\n * Turns, the thinking row, the suggestions and the composer all share one reading\n * column — `--tempest-ai-chat-width`, 48rem by default — centred in whatever width\n * the panel got.\n *\n * A capped column is the right answer from a phone up to a 1920 desktop: prose past\n * roughly 90 characters per line is measurably harder to track back to the start of\n * the next line, and letting an answer run the full width of a wide monitor makes it\n * worse, not more useful. It is deliberately a **knob** rather than a constant\n * because at 2560 and above the trade flips — a 768px ribbon in the middle of a\n * living-room screen is mostly empty room, and only the app knows how far away its\n * user is sitting. Widen it at the root: `--tempest-ai-chat-width: 72rem`.\n *\n * Type size is **not** solved here. Scaling the font for a TV is a decision for\n * `typography.css` and `density.css` — a component-local font ramp would fight the\n * tokens every app themes through.\n */\n.turn {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-2);\n min-width: 0;\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin-inline: auto;\n}\n\n/*\n * An assistant turn is full width with no bubble, a user turn is a narrow bubble\n * pushed to the end of the row.\n *\n * That asymmetry is the whole reading model of an LLM transcript: the answer is the\n * document being read, and wrapping it in a bubble would cap its width, fight the\n * code blocks and tables inside it, and make a long answer look like a shouted\n * message. The prompt is short and needs to be told apart at a glance, which a\n * bubble does better than anything else.\n */\n.userTurn {\n align-items: flex-end;\n}\n\n.systemTurn {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px dashed var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n.turnHeader {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n min-width: 0;\n}\n\n.avatar {\n display: inline-flex;\n flex: 0 0 auto;\n}\n\n.role {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n}\n\n/*\n * The role of a user turn is announced but not drawn: the bubble on the other side\n * already says who wrote it, and a \"Você\" label over every prompt is noise. Screen\n * readers still get it, because side and colour are not information for them.\n */\n.visuallyHiddenRole {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n border: 0;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n\n/*\n * The meta row uses the muted token, not the subtle one.\n *\n * `--tempest-text-subtle` at `--tempest-text-2xs` measures **4.37:1** against\n * `--tempest-bg` in the dark theme — under the 4.5:1 floor for text. Measured in a\n * real browser; jsdom's `axe` turns `color-contrast` off because nothing is painted.\n * Size is what de-emphasises this row; colour still has to pass.\n */\n.model,\n.time {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-2xs);\n font-variant-numeric: tabular-nums;\n}\n\n.body {\n min-width: 0;\n overflow-wrap: anywhere;\n}\n\n.userTurn .body {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n max-width: min(85%, 36rem);\n}\n\n.plain {\n white-space: pre-wrap;\n}\n\n.markdownRoot {\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-relaxed);\n}\n\n/*\n * The caret rides the last block of the answer, so it sits at the end of the text\n * instead of on a line of its own.\n *\n * `::after` on the final child is inline content, which is exactly the behaviour\n * wanted: it flows after the last word and moves with it as more text arrives. When\n * the answer starts with a fenced block the caret lands inside the code area — which\n * is where the text is actually being appended, so it is still telling the truth.\n */\n.streamingBody > .markdownRoot > :last-child::after {\n content: \"▍\";\n margin-left: 0.125em;\n color: var(--tempest-primary);\n animation: caretBlink 1s steps(2, jump-none) infinite;\n}\n\n@keyframes caretBlink {\n to {\n opacity: 0.2;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .streamingBody > .markdownRoot > :last-child::after {\n animation: none;\n }\n}\n\n.dots {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n}\n\n.dots > span {\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background-color: var(--tempest-text-subtle);\n animation: dotPulse 1.2s var(--tempest-ease-in-out) infinite;\n}\n\n.dots > span:nth-child(2) {\n animation-delay: 0.15s;\n}\n\n.dots > span:nth-child(3) {\n animation-delay: 0.3s;\n}\n\n@keyframes dotPulse {\n 0%,\n 60%,\n 100% {\n opacity: 0.25;\n }\n\n 30% {\n opacity: 1;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dots > span {\n animation: none;\n opacity: 0.6;\n }\n}\n\n.thinking {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin: 0 auto;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n/* --- reasoning ------------------------------------------------------------- */\n\n.reasoning {\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n}\n\n/*\n * Class plus attribute, so this beats `Collapsible`'s own single-class `.trigger`\n * rule whatever order the two modules land in the built stylesheet.\n */\n.reasoning button[aria-expanded] {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-medium);\n}\n\n.reasoning button[aria-expanded=\"true\"] .reasoningChevron {\n transform: rotate(90deg);\n}\n\n.reasoningChevron {\n flex: 0 0 auto;\n transition: transform var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .reasoningChevron {\n transition: none;\n }\n}\n\n.reasoningBody {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n line-height: var(--tempest-leading-relaxed);\n white-space: pre-wrap;\n}\n\n/* --- attachments ----------------------------------------------------------- */\n\n.attachments {\n display: flex;\n flex-wrap: wrap;\n gap: var(--tempest-space-2);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.attachment {\n display: inline-flex;\n align-items: center;\n gap: var(--tempest-space-2);\n max-width: 16rem;\n padding: var(--tempest-space-1) var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n font-size: var(--tempest-text-xs);\n}\n\n.attachmentName {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.attachmentMeta {\n flex: 0 0 auto;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n}\n\n.thumb {\n display: block;\n width: 4rem;\n height: 4rem;\n border-radius: var(--tempest-radius-sm);\n object-fit: cover;\n}\n\n/* --- error ----------------------------------------------------------------- */\n\n.error {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n margin: 0;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-danger);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-danger-bg);\n color: var(--tempest-text);\n font-size: var(--tempest-text-sm);\n}\n\n.retry {\n padding: 0;\n border: none;\n background: none;\n color: var(--tempest-danger);\n font: inherit;\n font-weight: var(--tempest-weight-medium);\n text-decoration: underline;\n cursor: pointer;\n}\n\n/* --- actions --------------------------------------------------------------- */\n\n.actions {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-1);\n opacity: 0;\n transition: opacity var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n/*\n * The row is revealed on hover and on keyboard focus, and is **always** visible\n * where there is no hover at all.\n *\n * Hiding a control behind `:hover` on a touch screen hides it for good — there is no\n * hover event to trigger it, and the first tap would land on whatever is underneath.\n */\n.turn:hover .actions,\n.actions:focus-within {\n opacity: 1;\n}\n\n@media (hover: none) {\n .actions {\n opacity: 1;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .actions {\n transition: none;\n }\n}\n\n/*\n * Two classes so the ghost styling wins over `CopyButton`'s own `.button` rule\n * regardless of the order the two CSS modules land in `styles.css`.\n */\n.actions .action {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.75rem;\n height: 1.75rem;\n padding: 0;\n border: none;\n border-radius: var(--tempest-radius-sm);\n background: none;\n color: var(--tempest-text-muted);\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n/*\n * Touch hit-slop — grows the tap target to 44×44 on a coarse pointer without moving\n * a pixel of the visible button, the same trick `Button` uses for its icon-only\n * sizes.\n *\n * 28×28 clears the 24×24 floor of WCAG 2.5.8 but sits well under the 44×44 of 2.5.5,\n * and four of these sit side by side: on a phone, \"copy\" and \"thumbs down\" are two\n * thumb-widths apart. Padding the button instead would space the row out on desktop,\n * where the pointer is precise and the row should stay quiet.\n */\n@media (pointer: coarse) {\n .actions .action::after {\n content: \"\";\n position: absolute;\n inset: -8px;\n }\n}\n\n@media (hover: hover) and (pointer: fine) {\n .actions .action:hover {\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n }\n}\n\n.actions .action:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 1px;\n}\n\n.actions .action[data-copied],\n.actions .actionActive {\n background: none;\n color: var(--tempest-primary);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .actions .action {\n transition: none;\n }\n}\n\n/* --- edit ------------------------------------------------------------------ */\n\n.editor {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-2);\n width: 100%;\n max-width: min(85%, 36rem);\n}\n\n.editField {\n width: 100%;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-primary);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n resize: vertical;\n}\n\n.editField:focus-visible {\n outline: 2px solid var(--tempest-primary);\n outline-offset: -1px;\n}\n\n.editActions {\n display: flex;\n justify-content: flex-end;\n gap: var(--tempest-space-2);\n}\n\n.ghost {\n padding: var(--tempest-space-1) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: transparent;\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n cursor: pointer;\n}\n\n.primary {\n padding: var(--tempest-space-1) var(--tempest-space-3);\n border: none;\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.primary:disabled {\n background-color: var(--tempest-surface-3);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n/* --- suggestions ----------------------------------------------------------- */\n\n.suggestions {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: var(--tempest-space-2);\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin: auto auto 0;\n}\n\n.suggestionsTitle {\n margin: 0;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n.suggestion {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n text-align: left;\n cursor: pointer;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .suggestion:hover {\n background-color: var(--tempest-surface-2);\n }\n}\n\n.suggestion:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 2px;\n}\n\n/* --- composer -------------------------------------------------------------- */\n\n.composer {\n display: flex;\n flex: 0 0 auto;\n flex-direction: column;\n gap: var(--tempest-space-1);\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-top: 1px solid var(--tempest-border);\n}\n\n/*\n * The field and the buttons share one bordered box, and the border is drawn here\n * rather than on the textarea: that is what makes the attach control and the send\n * button read as part of the prompt instead of a toolbar next to it.\n */\n.composerBox {\n display: flex;\n align-items: flex-end;\n gap: var(--tempest-space-2);\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin-inline: auto;\n padding: var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-surface);\n}\n\n.composerBox:focus-within {\n border-color: var(--tempest-primary);\n}\n\n.field {\n flex: 1 1 auto;\n min-width: 0;\n padding: var(--tempest-space-1) var(--tempest-space-2);\n border: none;\n background: none;\n color: inherit;\n font: inherit;\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n resize: none;\n}\n\n.field:focus-visible {\n outline: none;\n}\n\n.field:disabled {\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n.composerActions {\n display: flex;\n flex: 0 0 auto;\n align-items: center;\n gap: var(--tempest-space-2);\n}\n\n.composerFooter {\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin-inline: auto;\n padding-inline: var(--tempest-space-2);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n}\n\n.send {\n padding: var(--tempest-space-2) var(--tempest-space-4);\n border: none;\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.send:disabled {\n background-color: var(--tempest-surface-3);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n/*\n * Stop replaces send in the same slot, so the one button under the thumb is always\n * the one that does the thing you want next — send while idle, abort while the answer\n * is coming. Two buttons side by side would mean aiming at the right one mid-stream.\n */\n.stop {\n display: inline-flex;\n align-items: center;\n gap: var(--tempest-space-2);\n padding: var(--tempest-space-2) var(--tempest-space-4);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.stopGlyph {\n display: inline-block;\n width: 0.625rem;\n height: 0.625rem;\n border-radius: 2px;\n background-color: currentColor;\n}\n\n@media (max-width: 480px) {\n .thread {\n gap: var(--tempest-space-4);\n padding: var(--tempest-space-3);\n }\n\n .userTurn .body,\n .editor {\n max-width: 100%;\n }\n\n .composer {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n }\n}\n"],"mappings":""}
@@ -0,0 +1,58 @@
1
+ //#region src/components/AIChat/AIChat.module.css
2
+ var e = "tempest_panel_W0CCy", t = "tempest_header_KYy5l", n = "tempest_threadWrapper_LjoF6", r = "tempest_thread_RqELz", i = "tempest_jump_oBKxQ", a = "tempest_turn_k7h7J", o = "tempest_userTurn_7Csx0", s = "tempest_systemTurn_xpQmC", c = "tempest_turnHeader_l7lv3", l = "tempest_avatar_XqQ8R", u = "tempest_role_Vz6Dn", d = "tempest_visuallyHiddenRole_raKmR", f = "tempest_model_Z55Ho", p = "tempest_time_o07s8", m = "tempest_body_GnhC7", h = "tempest_plain_MRQet", g = "tempest_markdownRoot_nem2U", _ = "tempest_streamingBody_YMx1-", v = "tempest_caretBlink_IpIIw", y = "tempest_dots_FtZUe", b = "tempest_dotPulse_nZJHU", x = "tempest_thinking_Dk96l", S = "tempest_reasoning_kJof1", C = "tempest_reasoningChevron_TObJ2", w = "tempest_reasoningBody_ZR910", T = "tempest_attachments_qQH3a", E = "tempest_attachment_z4T-R", D = "tempest_attachmentName_2DCGR", O = "tempest_attachmentMeta_xBy5U", k = "tempest_thumb_w70uR", A = "tempest_error_CS0Gp", j = "tempest_retry_8a6Vm", M = "tempest_actions_SZnxe", N = "tempest_action_YPvQH", P = "tempest_actionActive_DW5O1", F = "tempest_editor_rk4hA", I = "tempest_editField_Z9neJ", L = "tempest_editActions_vfC0b", R = "tempest_ghost_S1yuC", z = "tempest_primary_wZ-Wu", B = "tempest_suggestions_V7Zhh", V = "tempest_suggestionsTitle_Z-wWe", H = "tempest_suggestion_4NYhq", U = "tempest_composer_AaB1Q", W = "tempest_composerBox_Fz-GG", G = "tempest_field_7m1Pa", K = "tempest_composerActions_z1oj9", q = "tempest_composerFooter_WZ1Fo", J = "tempest_send_eVkd6", Y = "tempest_stop_KWS9o", X = "tempest_stopGlyph_Nc19c", Z = {
3
+ panel: e,
4
+ header: t,
5
+ threadWrapper: n,
6
+ thread: r,
7
+ jump: i,
8
+ turn: a,
9
+ userTurn: o,
10
+ systemTurn: s,
11
+ turnHeader: c,
12
+ avatar: l,
13
+ role: u,
14
+ visuallyHiddenRole: d,
15
+ model: f,
16
+ time: p,
17
+ body: m,
18
+ plain: h,
19
+ markdownRoot: g,
20
+ streamingBody: _,
21
+ caretBlink: v,
22
+ dots: y,
23
+ dotPulse: b,
24
+ thinking: x,
25
+ reasoning: S,
26
+ reasoningChevron: C,
27
+ reasoningBody: w,
28
+ attachments: T,
29
+ attachment: E,
30
+ attachmentName: D,
31
+ attachmentMeta: O,
32
+ thumb: k,
33
+ error: A,
34
+ retry: j,
35
+ actions: M,
36
+ action: N,
37
+ actionActive: P,
38
+ editor: F,
39
+ editField: I,
40
+ editActions: L,
41
+ ghost: R,
42
+ primary: z,
43
+ suggestions: B,
44
+ suggestionsTitle: V,
45
+ suggestion: H,
46
+ composer: U,
47
+ composerBox: W,
48
+ field: G,
49
+ composerActions: K,
50
+ composerFooter: q,
51
+ send: J,
52
+ stop: Y,
53
+ stopGlyph: X
54
+ };
55
+ //#endregion
56
+ export { N as action, P as actionActive, M as actions, E as attachment, O as attachmentMeta, D as attachmentName, T as attachments, l as avatar, m as body, v as caretBlink, U as composer, K as composerActions, W as composerBox, q as composerFooter, Z as default, b as dotPulse, y as dots, L as editActions, I as editField, F as editor, A as error, G as field, R as ghost, t as header, i as jump, g as markdownRoot, f as model, e as panel, h as plain, z as primary, S as reasoning, w as reasoningBody, C as reasoningChevron, j as retry, u as role, J as send, Y as stop, X as stopGlyph, _ as streamingBody, H as suggestion, B as suggestions, V as suggestionsTitle, s as systemTurn, x as thinking, r as thread, n as threadWrapper, k as thumb, p as time, a as turn, c as turnHeader, o as userTurn, d as visuallyHiddenRole };
57
+
58
+ //# sourceMappingURL=AIChat.module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AIChat.module.js","names":[],"sources":["../../../src/components/AIChat/AIChat.module.css"],"sourcesContent":[".panel {\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n height: var(--tempest-ai-chat-height, 100%);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font-family: var(--tempest-font-sans);\n}\n\n.header {\n flex: 0 0 auto;\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-bottom: 1px solid var(--tempest-border);\n}\n\n/*\n * The wrapper exists only so the jump-to-latest button can be positioned against\n * the scroll area without being scrolled away with it.\n */\n.threadWrapper {\n position: relative;\n display: flex;\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n}\n\n/*\n * `min-width: 0` is load-bearing, not defensive.\n *\n * A flex item defaults to `min-width: auto`, which means \"at least as wide as my\n * content wants\". An answer carrying a wide table or a long line of SQL would make\n * the scroll area itself grow past the panel — and the table's own `overflow-x`\n * inside `Markdown` never gets a chance to kick in, because it is already as wide as\n * it asked to be. Zeroing the floor here is what pushes the overflow back down to\n * the table and the code block, where it scrolls in its own box.\n */\n.thread {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--tempest-space-5);\n min-width: 0;\n min-height: 0;\n padding: var(--tempest-space-4);\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n\n.thread:focus-visible {\n outline: 2px solid var(--tempest-primary);\n outline-offset: -2px;\n}\n\n.jump {\n position: absolute;\n right: var(--tempest-space-4);\n bottom: var(--tempest-space-3);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 2rem;\n height: 2rem;\n padding: 0;\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-full);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n box-shadow: var(--tempest-shadow-md);\n cursor: pointer;\n}\n\n.jump:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 2px;\n}\n\n/* Same 44×44 hit-slop as the turn actions: 2rem is a small target for a thumb. */\n@media (pointer: coarse) {\n .jump::after {\n content: \"\";\n position: absolute;\n inset: -6px;\n }\n}\n\n/* --- turn ------------------------------------------------------------------ */\n\n/*\n * Turns, the thinking row, the suggestions and the composer all share one reading\n * column — `--tempest-ai-chat-width`, 48rem by default — centred in whatever width\n * the panel got.\n *\n * A capped column is the right answer from a phone up to a 1920 desktop: prose past\n * roughly 90 characters per line is measurably harder to track back to the start of\n * the next line, and letting an answer run the full width of a wide monitor makes it\n * worse, not more useful. It is deliberately a **knob** rather than a constant\n * because at 2560 and above the trade flips — a 768px ribbon in the middle of a\n * living-room screen is mostly empty room, and only the app knows how far away its\n * user is sitting. Widen it at the root: `--tempest-ai-chat-width: 72rem`.\n *\n * Type size is **not** solved here. Scaling the font for a TV is a decision for\n * `typography.css` and `density.css` — a component-local font ramp would fight the\n * tokens every app themes through.\n */\n.turn {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-2);\n min-width: 0;\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin-inline: auto;\n}\n\n/*\n * An assistant turn is full width with no bubble, a user turn is a narrow bubble\n * pushed to the end of the row.\n *\n * That asymmetry is the whole reading model of an LLM transcript: the answer is the\n * document being read, and wrapping it in a bubble would cap its width, fight the\n * code blocks and tables inside it, and make a long answer look like a shouted\n * message. The prompt is short and needs to be told apart at a glance, which a\n * bubble does better than anything else.\n */\n.userTurn {\n align-items: flex-end;\n}\n\n.systemTurn {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px dashed var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n.turnHeader {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n min-width: 0;\n}\n\n.avatar {\n display: inline-flex;\n flex: 0 0 auto;\n}\n\n.role {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n}\n\n/*\n * The role of a user turn is announced but not drawn: the bubble on the other side\n * already says who wrote it, and a \"Você\" label over every prompt is noise. Screen\n * readers still get it, because side and colour are not information for them.\n */\n.visuallyHiddenRole {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n border: 0;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n\n/*\n * The meta row uses the muted token, not the subtle one.\n *\n * `--tempest-text-subtle` at `--tempest-text-2xs` measures **4.37:1** against\n * `--tempest-bg` in the dark theme — under the 4.5:1 floor for text. Measured in a\n * real browser; jsdom's `axe` turns `color-contrast` off because nothing is painted.\n * Size is what de-emphasises this row; colour still has to pass.\n */\n.model,\n.time {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-2xs);\n font-variant-numeric: tabular-nums;\n}\n\n.body {\n min-width: 0;\n overflow-wrap: anywhere;\n}\n\n.userTurn .body {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n max-width: min(85%, 36rem);\n}\n\n.plain {\n white-space: pre-wrap;\n}\n\n.markdownRoot {\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-relaxed);\n}\n\n/*\n * The caret rides the last block of the answer, so it sits at the end of the text\n * instead of on a line of its own.\n *\n * `::after` on the final child is inline content, which is exactly the behaviour\n * wanted: it flows after the last word and moves with it as more text arrives. When\n * the answer starts with a fenced block the caret lands inside the code area — which\n * is where the text is actually being appended, so it is still telling the truth.\n */\n.streamingBody > .markdownRoot > :last-child::after {\n content: \"▍\";\n margin-left: 0.125em;\n color: var(--tempest-primary);\n animation: caretBlink 1s steps(2, jump-none) infinite;\n}\n\n@keyframes caretBlink {\n to {\n opacity: 0.2;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .streamingBody > .markdownRoot > :last-child::after {\n animation: none;\n }\n}\n\n.dots {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n}\n\n.dots > span {\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background-color: var(--tempest-text-subtle);\n animation: dotPulse 1.2s var(--tempest-ease-in-out) infinite;\n}\n\n.dots > span:nth-child(2) {\n animation-delay: 0.15s;\n}\n\n.dots > span:nth-child(3) {\n animation-delay: 0.3s;\n}\n\n@keyframes dotPulse {\n 0%,\n 60%,\n 100% {\n opacity: 0.25;\n }\n\n 30% {\n opacity: 1;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dots > span {\n animation: none;\n opacity: 0.6;\n }\n}\n\n.thinking {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin: 0 auto;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n/* --- reasoning ------------------------------------------------------------- */\n\n.reasoning {\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n}\n\n/*\n * Class plus attribute, so this beats `Collapsible`'s own single-class `.trigger`\n * rule whatever order the two modules land in the built stylesheet.\n */\n.reasoning button[aria-expanded] {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-medium);\n}\n\n.reasoning button[aria-expanded=\"true\"] .reasoningChevron {\n transform: rotate(90deg);\n}\n\n.reasoningChevron {\n flex: 0 0 auto;\n transition: transform var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .reasoningChevron {\n transition: none;\n }\n}\n\n.reasoningBody {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n line-height: var(--tempest-leading-relaxed);\n white-space: pre-wrap;\n}\n\n/* --- attachments ----------------------------------------------------------- */\n\n.attachments {\n display: flex;\n flex-wrap: wrap;\n gap: var(--tempest-space-2);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.attachment {\n display: inline-flex;\n align-items: center;\n gap: var(--tempest-space-2);\n max-width: 16rem;\n padding: var(--tempest-space-1) var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n font-size: var(--tempest-text-xs);\n}\n\n.attachmentName {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.attachmentMeta {\n flex: 0 0 auto;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n}\n\n.thumb {\n display: block;\n width: 4rem;\n height: 4rem;\n border-radius: var(--tempest-radius-sm);\n object-fit: cover;\n}\n\n/* --- error ----------------------------------------------------------------- */\n\n.error {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n margin: 0;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-danger);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-danger-bg);\n color: var(--tempest-text);\n font-size: var(--tempest-text-sm);\n}\n\n.retry {\n padding: 0;\n border: none;\n background: none;\n color: var(--tempest-danger);\n font: inherit;\n font-weight: var(--tempest-weight-medium);\n text-decoration: underline;\n cursor: pointer;\n}\n\n/* --- actions --------------------------------------------------------------- */\n\n.actions {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-1);\n opacity: 0;\n transition: opacity var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n/*\n * The row is revealed on hover and on keyboard focus, and is **always** visible\n * where there is no hover at all.\n *\n * Hiding a control behind `:hover` on a touch screen hides it for good — there is no\n * hover event to trigger it, and the first tap would land on whatever is underneath.\n */\n.turn:hover .actions,\n.actions:focus-within {\n opacity: 1;\n}\n\n@media (hover: none) {\n .actions {\n opacity: 1;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .actions {\n transition: none;\n }\n}\n\n/*\n * Two classes so the ghost styling wins over `CopyButton`'s own `.button` rule\n * regardless of the order the two CSS modules land in `styles.css`.\n */\n.actions .action {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.75rem;\n height: 1.75rem;\n padding: 0;\n border: none;\n border-radius: var(--tempest-radius-sm);\n background: none;\n color: var(--tempest-text-muted);\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n/*\n * Touch hit-slop — grows the tap target to 44×44 on a coarse pointer without moving\n * a pixel of the visible button, the same trick `Button` uses for its icon-only\n * sizes.\n *\n * 28×28 clears the 24×24 floor of WCAG 2.5.8 but sits well under the 44×44 of 2.5.5,\n * and four of these sit side by side: on a phone, \"copy\" and \"thumbs down\" are two\n * thumb-widths apart. Padding the button instead would space the row out on desktop,\n * where the pointer is precise and the row should stay quiet.\n */\n@media (pointer: coarse) {\n .actions .action::after {\n content: \"\";\n position: absolute;\n inset: -8px;\n }\n}\n\n@media (hover: hover) and (pointer: fine) {\n .actions .action:hover {\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n }\n}\n\n.actions .action:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 1px;\n}\n\n.actions .action[data-copied],\n.actions .actionActive {\n background: none;\n color: var(--tempest-primary);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .actions .action {\n transition: none;\n }\n}\n\n/* --- edit ------------------------------------------------------------------ */\n\n.editor {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-2);\n width: 100%;\n max-width: min(85%, 36rem);\n}\n\n.editField {\n width: 100%;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-primary);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n resize: vertical;\n}\n\n.editField:focus-visible {\n outline: 2px solid var(--tempest-primary);\n outline-offset: -1px;\n}\n\n.editActions {\n display: flex;\n justify-content: flex-end;\n gap: var(--tempest-space-2);\n}\n\n.ghost {\n padding: var(--tempest-space-1) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: transparent;\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n cursor: pointer;\n}\n\n.primary {\n padding: var(--tempest-space-1) var(--tempest-space-3);\n border: none;\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.primary:disabled {\n background-color: var(--tempest-surface-3);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n/* --- suggestions ----------------------------------------------------------- */\n\n.suggestions {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: var(--tempest-space-2);\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin: auto auto 0;\n}\n\n.suggestionsTitle {\n margin: 0;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n.suggestion {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n text-align: left;\n cursor: pointer;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .suggestion:hover {\n background-color: var(--tempest-surface-2);\n }\n}\n\n.suggestion:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 2px;\n}\n\n/* --- composer -------------------------------------------------------------- */\n\n.composer {\n display: flex;\n flex: 0 0 auto;\n flex-direction: column;\n gap: var(--tempest-space-1);\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-top: 1px solid var(--tempest-border);\n}\n\n/*\n * The field and the buttons share one bordered box, and the border is drawn here\n * rather than on the textarea: that is what makes the attach control and the send\n * button read as part of the prompt instead of a toolbar next to it.\n */\n.composerBox {\n display: flex;\n align-items: flex-end;\n gap: var(--tempest-space-2);\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin-inline: auto;\n padding: var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-surface);\n}\n\n.composerBox:focus-within {\n border-color: var(--tempest-primary);\n}\n\n.field {\n flex: 1 1 auto;\n min-width: 0;\n padding: var(--tempest-space-1) var(--tempest-space-2);\n border: none;\n background: none;\n color: inherit;\n font: inherit;\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n resize: none;\n}\n\n.field:focus-visible {\n outline: none;\n}\n\n.field:disabled {\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n.composerActions {\n display: flex;\n flex: 0 0 auto;\n align-items: center;\n gap: var(--tempest-space-2);\n}\n\n.composerFooter {\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin-inline: auto;\n padding-inline: var(--tempest-space-2);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n}\n\n.send {\n padding: var(--tempest-space-2) var(--tempest-space-4);\n border: none;\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.send:disabled {\n background-color: var(--tempest-surface-3);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n/*\n * Stop replaces send in the same slot, so the one button under the thumb is always\n * the one that does the thing you want next — send while idle, abort while the answer\n * is coming. Two buttons side by side would mean aiming at the right one mid-stream.\n */\n.stop {\n display: inline-flex;\n align-items: center;\n gap: var(--tempest-space-2);\n padding: var(--tempest-space-2) var(--tempest-space-4);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.stopGlyph {\n display: inline-block;\n width: 0.625rem;\n height: 0.625rem;\n border-radius: 2px;\n background-color: currentColor;\n}\n\n@media (max-width: 480px) {\n .thread {\n gap: var(--tempest-space-4);\n padding: var(--tempest-space-3);\n }\n\n .userTurn .body,\n .editor {\n max-width: 100%;\n }\n\n .composer {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n }\n}\n"],"mappings":""}
@@ -0,0 +1,2 @@
1
+ const e=require("../../utils/cn.cjs"),t=require("./ai-chat-turns.cjs"),n=require("./AIChat.module.cjs");let r=require("react"),i=require("react/jsx-runtime");var a=(0,r.forwardRef)(function({onSend:a,onStop:o,generating:s=!1,locale:c=`pt-BR`,actions:l,footer:u,maxRows:d=8,onError:f,className:p,disabled:m,placeholder:h,onKeyDown:g,onChange:_,...v},y){let b=t.aiChatStrings(c),x=(0,r.useRef)(null),[S,C]=(0,r.useState)(``),[w,T]=(0,r.useState)(!1);(0,r.useImperativeHandle)(y,()=>({focus:()=>x.current?.focus(),setValue:e=>{C(e),x.current?.focus()}})),(0,r.useLayoutEffect)(()=>{let e=x.current;if(!e)return;e.style.height=`auto`;let t=(Number.parseFloat(getComputedStyle(e).lineHeight)||20)*d;e.style.height=`${Math.min(e.scrollHeight,t)}px`,e.style.overflowY=e.scrollHeight>t?`auto`:`hidden`},[S,d]);let E=async()=>{let e=S.trim();if(!(!e||w||m||s)){T(!0);try{await a(e),C(``)}catch(e){f?.(e)}finally{T(!1)}}},D=e=>{e.preventDefault(),E()},O=e=>{if(g?.(e),!e.defaultPrevented){if(e.key===`Escape`&&s&&o){e.preventDefault(),o();return}e.key!==`Enter`||e.shiftKey||e.nativeEvent.isComposing||e.keyCode===229||(e.preventDefault(),E())}},k=s&&o!==void 0;return(0,i.jsxs)(`form`,{className:e.cn(n.default.composer,p),onSubmit:D,children:[(0,i.jsxs)(`div`,{className:n.default.composerBox,children:[(0,i.jsx)(`textarea`,{...v,ref:x,className:n.default.field,rows:1,value:S,disabled:m,placeholder:h??b.placeholder,onChange:e=>{C(e.target.value),_?.(e)},onKeyDown:O}),(0,i.jsxs)(`div`,{className:n.default.composerActions,children:[l,k?(0,i.jsxs)(`button`,{type:`button`,className:n.default.stop,onClick:o,children:[(0,i.jsx)(`span`,{"aria-hidden":`true`,className:n.default.stopGlyph}),b.stop]}):(0,i.jsx)(`button`,{type:`submit`,className:n.default.send,disabled:m||w||s||S.trim()===``,children:b.send})]})]}),u&&(0,i.jsx)(`div`,{className:n.default.composerFooter,children:u})]})});exports.AIChatComposer=a;
2
+ //# sourceMappingURL=AIChatComposer.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AIChatComposer.cjs","names":[],"sources":["../../../src/components/AIChat/AIChatComposer.tsx"],"sourcesContent":["import {\n forwardRef,\n useImperativeHandle,\n useLayoutEffect,\n useRef,\n useState,\n type FormEvent,\n type KeyboardEvent,\n type ReactNode,\n type TextareaHTMLAttributes,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { aiChatStrings } from \"./ai-chat-turns\";\nimport styles from \"./AIChat.module.css\";\n\n/** DOM attributes the composer redefines. */\ntype OverriddenDomProps = \"onSubmit\" | \"value\" | \"defaultValue\" | \"rows\";\n\nexport interface AIChatComposerProps extends Omit<\n TextareaHTMLAttributes<HTMLTextAreaElement>,\n OverriddenDomProps\n> {\n /** Called with the trimmed prompt. The field clears only when this does not throw. */\n onSend: (text: string) => void | Promise<void>;\n /**\n * Abort the turn in flight.\n *\n * When given together with `generating`, the send button becomes a stop button\n * and `Escape` aborts too.\n */\n onStop?: () => void;\n /** A turn is being generated. Replaces send with stop and refuses to send. */\n generating?: boolean;\n /** Locale for the placeholder and the button labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Left of the send button — an attach control, a model picker, a tool toggle. */\n actions?: ReactNode;\n /** Under the field — a token count, the model name, a disclaimer. */\n footer?: ReactNode;\n /** Largest height the field grows to, in lines. Default 8. */\n maxRows?: number;\n /**\n * Called when `onSend` rejects. The draft is kept either way.\n *\n * Without it the rejection is swallowed after the draft is preserved: re-throwing\n * out of a DOM event handler surfaces as an unhandled promise rejection, which is\n * console noise for the developer and nothing the user can act on. The visible\n * signal is the prompt still sitting in the field; wire this to a toast to say why.\n */\n onError?: (error: unknown) => void;\n}\n\n/** Imperative handle, so a thread can focus or refill the field. */\nexport interface AIChatComposerHandle {\n focus: () => void;\n /** Replace the draft — used to put a prompt back in the field. */\n setValue: (text: string) => void;\n}\n\n/**\n * The prompt field of a conversation with a model: a textarea that grows with its\n * content, sends on `Enter`, keeps `Shift+Enter` for a newline, and turns into a\n * stop button while a turn is streaming.\n *\n * Uncontrolled on purpose. A draft changes on every keystroke, and lifting that into\n * app state re-renders the whole transcript per character — with a streaming answer\n * above, that is the one place where \"controlled by default\" costs something\n * visible. Apps that need the draft (a persisted composer, a slash-command menu)\n * read it from `onChange` or drive it through the ref.\n *\n * @example\n * <AIChatComposer\n * generating={generating}\n * onSend={(text) => ask(text)}\n * onStop={() => controller.abort()}\n * footer={<small>Claude Opus 5 · pode errar</small>}\n * />\n */\nexport const AIChatComposer = forwardRef<AIChatComposerHandle, AIChatComposerProps>(\n function AIChatComposer(\n {\n onSend,\n onStop,\n generating = false,\n locale = \"pt-BR\",\n actions,\n footer,\n maxRows = 8,\n onError,\n className,\n disabled,\n placeholder,\n onKeyDown,\n onChange,\n ...rest\n },\n ref,\n ) {\n const strings = aiChatStrings(locale);\n const textarea = useRef<HTMLTextAreaElement | null>(null);\n const [value, setValue] = useState(\"\");\n const [busy, setBusy] = useState(false);\n\n useImperativeHandle(ref, () => ({\n focus: () => textarea.current?.focus(),\n setValue: (text: string) => {\n setValue(text);\n textarea.current?.focus();\n },\n }));\n\n /**\n * Grow the field to fit its content, up to `maxRows`.\n *\n * Measured from `scrollHeight` after resetting the height, because\n * `scrollHeight` on an element that is already tall enough reports the current\n * height and the field would never shrink back.\n */\n useLayoutEffect(() => {\n const node = textarea.current;\n if (!node) return;\n node.style.height = \"auto\";\n const lineHeight = Number.parseFloat(getComputedStyle(node).lineHeight) || 20;\n const max = lineHeight * maxRows;\n node.style.height = `${Math.min(node.scrollHeight, max)}px`;\n node.style.overflowY = node.scrollHeight > max ? \"auto\" : \"hidden\";\n }, [value, maxRows]);\n\n const submit = async (): Promise<void> => {\n const text = value.trim();\n if (!text || busy || disabled || generating) return;\n setBusy(true);\n try {\n await onSend(text);\n setValue(\"\");\n } catch (error) {\n onError?.(error);\n } finally {\n setBusy(false);\n }\n };\n\n const handleSubmit = (event: FormEvent): void => {\n event.preventDefault();\n void submit();\n };\n\n /**\n * `Enter` sends, `Shift+Enter` breaks the line, `Escape` aborts a turn in\n * flight.\n *\n * The IME check is not optional: while composing Japanese or Korean, `Enter`\n * confirms the candidate word and `keyCode === 229` marks that keystroke.\n * Sending there would post half a word and eat the confirmation.\n */\n const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>): void => {\n onKeyDown?.(event);\n if (event.defaultPrevented) return;\n if (event.key === \"Escape\" && generating && onStop) {\n event.preventDefault();\n onStop();\n return;\n }\n if (event.key !== \"Enter\" || event.shiftKey) return;\n if (event.nativeEvent.isComposing || event.keyCode === 229) return;\n event.preventDefault();\n void submit();\n };\n\n const showStop = generating && onStop !== undefined;\n\n return (\n <form className={cn(styles.composer, className)} onSubmit={handleSubmit}>\n <div className={styles.composerBox}>\n <textarea\n {...rest}\n ref={textarea}\n className={styles.field}\n rows={1}\n value={value}\n disabled={disabled}\n placeholder={placeholder ?? strings.placeholder}\n onChange={(event) => {\n setValue(event.target.value);\n onChange?.(event);\n }}\n onKeyDown={handleKeyDown}\n />\n <div className={styles.composerActions}>\n {actions}\n {showStop ? (\n <button type=\"button\" className={styles.stop} onClick={onStop}>\n <span aria-hidden=\"true\" className={styles.stopGlyph} />\n {strings.stop}\n </button>\n ) : (\n <button\n type=\"submit\"\n className={styles.send}\n disabled={disabled || busy || generating || value.trim() === \"\"}\n >\n {strings.send}\n </button>\n )}\n </div>\n </div>\n {footer && <div className={styles.composerFooter}>{footer}</div>}\n </form>\n );\n },\n);\n"],"mappings":"8JAgFA,IAAa,GAAA,EAAA,EAAA,WAAA,CACT,SACI,CACI,SACA,SACA,aAAa,GACb,SAAS,QACT,UACA,SACA,UAAU,EACV,UACA,YACA,WACA,cACA,YACA,WACA,GAAG,GAEP,EACF,CACE,IAAM,EAAU,EAAA,cAAc,CAAM,EAC9B,GAAA,EAAA,EAAA,OAAA,CAA8C,IAAI,EAClD,CAAC,EAAO,IAAA,EAAA,EAAA,SAAA,CAAqB,EAAE,EAC/B,CAAC,EAAM,IAAA,EAAA,EAAA,SAAA,CAAoB,EAAK,GAEtC,EAAA,EAAA,oBAAA,CAAoB,OAAY,CAC5B,UAAa,EAAS,SAAS,MAAM,EACrC,SAAW,GAAiB,CACxB,EAAS,CAAI,EACb,EAAS,SAAS,MAAM,CAC5B,CACJ,EAAE,GASF,EAAA,EAAA,gBAAA,KAAsB,CAClB,IAAM,EAAO,EAAS,QACtB,GAAI,CAAC,EAAM,OACX,EAAK,MAAM,OAAS,OAEpB,IAAM,GADa,OAAO,WAAW,iBAAiB,CAAI,CAAC,CAAC,UAAU,GAAK,IAClD,EACzB,EAAK,MAAM,OAAS,GAAG,KAAK,IAAI,EAAK,aAAc,CAAG,EAAE,IACxD,EAAK,MAAM,UAAY,EAAK,aAAe,EAAM,OAAS,QAC9D,EAAG,CAAC,EAAO,CAAO,CAAC,EAEnB,IAAM,EAAS,SAA2B,CACtC,IAAM,EAAO,EAAM,KAAK,EACpB,MAAC,GAAQ,GAAQ,GAAY,GACjC,GAAQ,EAAI,EACZ,GAAI,CACA,MAAM,EAAO,CAAI,EACjB,EAAS,EAAE,CACf,OAAS,EAAO,CACZ,IAAU,CAAK,CACnB,QAAU,CACN,EAAQ,EAAK,CACjB,CARY,CAShB,EAEM,EAAgB,GAA2B,CAC7C,EAAM,eAAe,EACrB,EAAY,CAChB,EAUM,EAAiB,GAAoD,CACvE,OAAY,CAAK,EACb,GAAM,iBACV,IAAI,EAAM,MAAQ,UAAY,GAAc,EAAQ,CAChD,EAAM,eAAe,EACrB,EAAO,EACP,MACJ,CACI,EAAM,MAAQ,SAAW,EAAM,UAC/B,EAAM,YAAY,aAAe,EAAM,UAAY,MACvD,EAAM,eAAe,EACrB,EAAY,EAJZ,CAKJ,EAEM,EAAW,GAAc,IAAW,IAAA,GAE1C,OACI,EAAA,EAAA,KAAA,CAAC,OAAD,CAAM,UAAW,EAAA,GAAG,EAAA,QAAO,SAAU,CAAS,EAAG,SAAU,WAA3D,EACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,qBAAvB,EACI,EAAA,EAAA,IAAA,CAAC,WAAD,CACI,GAAI,EACJ,IAAK,EACL,UAAW,EAAA,QAAO,MAClB,KAAM,EACC,QACG,WACV,YAAa,GAAe,EAAQ,YACpC,SAAW,GAAU,CACjB,EAAS,EAAM,OAAO,KAAK,EAC3B,IAAW,CAAK,CACpB,EACA,UAAW,CACd,CAAA,GACD,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,yBAAvB,CACK,EACA,GACG,EAAA,EAAA,KAAA,CAAC,SAAD,CAAQ,KAAK,SAAS,UAAW,EAAA,QAAO,KAAM,QAAS,WAAvD,EACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,cAAY,OAAO,UAAW,EAAA,QAAO,SAAY,CAAA,EACtD,EAAQ,IACL,KAER,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,KAClB,SAAU,GAAY,GAAQ,GAAc,EAAM,KAAK,IAAM,YAE5D,EAAQ,IACL,CAAA,CAEX,GACJ,IACJ,IAAU,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,wBAAiB,CAAY,CAAA,CAC7D,GAEd,CACJ"}
@@ -0,0 +1,87 @@
1
+ import { cn as e } from "../../utils/cn.js";
2
+ import { aiChatStrings as t } from "./ai-chat-turns.js";
3
+ import n from "./AIChat.module.js";
4
+ import { forwardRef as r, useImperativeHandle as i, useLayoutEffect as a, useRef as o, useState as s } from "react";
5
+ import { jsx as c, jsxs as l } from "react/jsx-runtime";
6
+ //#region src/components/AIChat/AIChatComposer.tsx
7
+ var u = r(function({ onSend: r, onStop: u, generating: d = !1, locale: f = "pt-BR", actions: p, footer: m, maxRows: h = 8, onError: g, className: _, disabled: v, placeholder: y, onKeyDown: b, onChange: x, ...S }, C) {
8
+ let w = t(f), T = o(null), [E, D] = s(""), [O, k] = s(!1);
9
+ i(C, () => ({
10
+ focus: () => T.current?.focus(),
11
+ setValue: (e) => {
12
+ D(e), T.current?.focus();
13
+ }
14
+ })), a(() => {
15
+ let e = T.current;
16
+ if (!e) return;
17
+ e.style.height = "auto";
18
+ let t = (Number.parseFloat(getComputedStyle(e).lineHeight) || 20) * h;
19
+ e.style.height = `${Math.min(e.scrollHeight, t)}px`, e.style.overflowY = e.scrollHeight > t ? "auto" : "hidden";
20
+ }, [E, h]);
21
+ let A = async () => {
22
+ let e = E.trim();
23
+ if (!(!e || O || v || d)) {
24
+ k(!0);
25
+ try {
26
+ await r(e), D("");
27
+ } catch (e) {
28
+ g?.(e);
29
+ } finally {
30
+ k(!1);
31
+ }
32
+ }
33
+ }, j = (e) => {
34
+ e.preventDefault(), A();
35
+ }, M = (e) => {
36
+ if (b?.(e), !e.defaultPrevented) {
37
+ if (e.key === "Escape" && d && u) {
38
+ e.preventDefault(), u();
39
+ return;
40
+ }
41
+ e.key !== "Enter" || e.shiftKey || e.nativeEvent.isComposing || e.keyCode === 229 || (e.preventDefault(), A());
42
+ }
43
+ }, N = d && u !== void 0;
44
+ return /* @__PURE__ */ l("form", {
45
+ className: e(n.composer, _),
46
+ onSubmit: j,
47
+ children: [/* @__PURE__ */ l("div", {
48
+ className: n.composerBox,
49
+ children: [/* @__PURE__ */ c("textarea", {
50
+ ...S,
51
+ ref: T,
52
+ className: n.field,
53
+ rows: 1,
54
+ value: E,
55
+ disabled: v,
56
+ placeholder: y ?? w.placeholder,
57
+ onChange: (e) => {
58
+ D(e.target.value), x?.(e);
59
+ },
60
+ onKeyDown: M
61
+ }), /* @__PURE__ */ l("div", {
62
+ className: n.composerActions,
63
+ children: [p, N ? /* @__PURE__ */ l("button", {
64
+ type: "button",
65
+ className: n.stop,
66
+ onClick: u,
67
+ children: [/* @__PURE__ */ c("span", {
68
+ "aria-hidden": "true",
69
+ className: n.stopGlyph
70
+ }), w.stop]
71
+ }) : /* @__PURE__ */ c("button", {
72
+ type: "submit",
73
+ className: n.send,
74
+ disabled: v || O || d || E.trim() === "",
75
+ children: w.send
76
+ })]
77
+ })]
78
+ }), m && /* @__PURE__ */ c("div", {
79
+ className: n.composerFooter,
80
+ children: m
81
+ })]
82
+ });
83
+ });
84
+ //#endregion
85
+ export { u as AIChatComposer };
86
+
87
+ //# sourceMappingURL=AIChatComposer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AIChatComposer.js","names":[],"sources":["../../../src/components/AIChat/AIChatComposer.tsx"],"sourcesContent":["import {\n forwardRef,\n useImperativeHandle,\n useLayoutEffect,\n useRef,\n useState,\n type FormEvent,\n type KeyboardEvent,\n type ReactNode,\n type TextareaHTMLAttributes,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { aiChatStrings } from \"./ai-chat-turns\";\nimport styles from \"./AIChat.module.css\";\n\n/** DOM attributes the composer redefines. */\ntype OverriddenDomProps = \"onSubmit\" | \"value\" | \"defaultValue\" | \"rows\";\n\nexport interface AIChatComposerProps extends Omit<\n TextareaHTMLAttributes<HTMLTextAreaElement>,\n OverriddenDomProps\n> {\n /** Called with the trimmed prompt. The field clears only when this does not throw. */\n onSend: (text: string) => void | Promise<void>;\n /**\n * Abort the turn in flight.\n *\n * When given together with `generating`, the send button becomes a stop button\n * and `Escape` aborts too.\n */\n onStop?: () => void;\n /** A turn is being generated. Replaces send with stop and refuses to send. */\n generating?: boolean;\n /** Locale for the placeholder and the button labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Left of the send button — an attach control, a model picker, a tool toggle. */\n actions?: ReactNode;\n /** Under the field — a token count, the model name, a disclaimer. */\n footer?: ReactNode;\n /** Largest height the field grows to, in lines. Default 8. */\n maxRows?: number;\n /**\n * Called when `onSend` rejects. The draft is kept either way.\n *\n * Without it the rejection is swallowed after the draft is preserved: re-throwing\n * out of a DOM event handler surfaces as an unhandled promise rejection, which is\n * console noise for the developer and nothing the user can act on. The visible\n * signal is the prompt still sitting in the field; wire this to a toast to say why.\n */\n onError?: (error: unknown) => void;\n}\n\n/** Imperative handle, so a thread can focus or refill the field. */\nexport interface AIChatComposerHandle {\n focus: () => void;\n /** Replace the draft — used to put a prompt back in the field. */\n setValue: (text: string) => void;\n}\n\n/**\n * The prompt field of a conversation with a model: a textarea that grows with its\n * content, sends on `Enter`, keeps `Shift+Enter` for a newline, and turns into a\n * stop button while a turn is streaming.\n *\n * Uncontrolled on purpose. A draft changes on every keystroke, and lifting that into\n * app state re-renders the whole transcript per character — with a streaming answer\n * above, that is the one place where \"controlled by default\" costs something\n * visible. Apps that need the draft (a persisted composer, a slash-command menu)\n * read it from `onChange` or drive it through the ref.\n *\n * @example\n * <AIChatComposer\n * generating={generating}\n * onSend={(text) => ask(text)}\n * onStop={() => controller.abort()}\n * footer={<small>Claude Opus 5 · pode errar</small>}\n * />\n */\nexport const AIChatComposer = forwardRef<AIChatComposerHandle, AIChatComposerProps>(\n function AIChatComposer(\n {\n onSend,\n onStop,\n generating = false,\n locale = \"pt-BR\",\n actions,\n footer,\n maxRows = 8,\n onError,\n className,\n disabled,\n placeholder,\n onKeyDown,\n onChange,\n ...rest\n },\n ref,\n ) {\n const strings = aiChatStrings(locale);\n const textarea = useRef<HTMLTextAreaElement | null>(null);\n const [value, setValue] = useState(\"\");\n const [busy, setBusy] = useState(false);\n\n useImperativeHandle(ref, () => ({\n focus: () => textarea.current?.focus(),\n setValue: (text: string) => {\n setValue(text);\n textarea.current?.focus();\n },\n }));\n\n /**\n * Grow the field to fit its content, up to `maxRows`.\n *\n * Measured from `scrollHeight` after resetting the height, because\n * `scrollHeight` on an element that is already tall enough reports the current\n * height and the field would never shrink back.\n */\n useLayoutEffect(() => {\n const node = textarea.current;\n if (!node) return;\n node.style.height = \"auto\";\n const lineHeight = Number.parseFloat(getComputedStyle(node).lineHeight) || 20;\n const max = lineHeight * maxRows;\n node.style.height = `${Math.min(node.scrollHeight, max)}px`;\n node.style.overflowY = node.scrollHeight > max ? \"auto\" : \"hidden\";\n }, [value, maxRows]);\n\n const submit = async (): Promise<void> => {\n const text = value.trim();\n if (!text || busy || disabled || generating) return;\n setBusy(true);\n try {\n await onSend(text);\n setValue(\"\");\n } catch (error) {\n onError?.(error);\n } finally {\n setBusy(false);\n }\n };\n\n const handleSubmit = (event: FormEvent): void => {\n event.preventDefault();\n void submit();\n };\n\n /**\n * `Enter` sends, `Shift+Enter` breaks the line, `Escape` aborts a turn in\n * flight.\n *\n * The IME check is not optional: while composing Japanese or Korean, `Enter`\n * confirms the candidate word and `keyCode === 229` marks that keystroke.\n * Sending there would post half a word and eat the confirmation.\n */\n const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>): void => {\n onKeyDown?.(event);\n if (event.defaultPrevented) return;\n if (event.key === \"Escape\" && generating && onStop) {\n event.preventDefault();\n onStop();\n return;\n }\n if (event.key !== \"Enter\" || event.shiftKey) return;\n if (event.nativeEvent.isComposing || event.keyCode === 229) return;\n event.preventDefault();\n void submit();\n };\n\n const showStop = generating && onStop !== undefined;\n\n return (\n <form className={cn(styles.composer, className)} onSubmit={handleSubmit}>\n <div className={styles.composerBox}>\n <textarea\n {...rest}\n ref={textarea}\n className={styles.field}\n rows={1}\n value={value}\n disabled={disabled}\n placeholder={placeholder ?? strings.placeholder}\n onChange={(event) => {\n setValue(event.target.value);\n onChange?.(event);\n }}\n onKeyDown={handleKeyDown}\n />\n <div className={styles.composerActions}>\n {actions}\n {showStop ? (\n <button type=\"button\" className={styles.stop} onClick={onStop}>\n <span aria-hidden=\"true\" className={styles.stopGlyph} />\n {strings.stop}\n </button>\n ) : (\n <button\n type=\"submit\"\n className={styles.send}\n disabled={disabled || busy || generating || value.trim() === \"\"}\n >\n {strings.send}\n </button>\n )}\n </div>\n </div>\n {footer && <div className={styles.composerFooter}>{footer}</div>}\n </form>\n );\n },\n);\n"],"mappings":";;;;;;AAgFA,IAAa,IAAiB,EAC1B,SACI,EACI,WACA,WACA,gBAAa,IACb,YAAS,SACT,YACA,WACA,aAAU,GACV,YACA,cACA,aACA,gBACA,cACA,aACA,GAAG,KAEP,GACF;CACE,IAAM,IAAU,EAAc,CAAM,GAC9B,IAAW,EAAmC,IAAI,GAClD,CAAC,GAAO,KAAY,EAAS,EAAE,GAC/B,CAAC,GAAM,KAAW,EAAS,EAAK;CAiBtC,AAfA,EAAoB,UAAY;EAC5B,aAAa,EAAS,SAAS,MAAM;EACrC,WAAW,MAAiB;GAExB,AADA,EAAS,CAAI,GACb,EAAS,SAAS,MAAM;EAC5B;CACJ,EAAE,GASF,QAAsB;EAClB,IAAM,IAAO,EAAS;EACtB,IAAI,CAAC,GAAM;EACX,EAAK,MAAM,SAAS;EAEpB,IAAM,KADa,OAAO,WAAW,iBAAiB,CAAI,CAAC,CAAC,UAAU,KAAK,MAClD;EAEzB,AADA,EAAK,MAAM,SAAS,GAAG,KAAK,IAAI,EAAK,cAAc,CAAG,EAAE,KACxD,EAAK,MAAM,YAAY,EAAK,eAAe,IAAM,SAAS;CAC9D,GAAG,CAAC,GAAO,CAAO,CAAC;CAEnB,IAAM,IAAS,YAA2B;EACtC,IAAM,IAAO,EAAM,KAAK;EACpB,OAAC,KAAQ,KAAQ,KAAY,IACjC;KAAQ,EAAI;GACZ,IAAI;IAEA,AADA,MAAM,EAAO,CAAI,GACjB,EAAS,EAAE;GACf,SAAS,GAAO;IACZ,IAAU,CAAK;GACnB,UAAU;IACN,EAAQ,EAAK;GACjB;EARY;CAShB,GAEM,KAAgB,MAA2B;EAE7C,AADA,EAAM,eAAe,GACrB,EAAY;CAChB,GAUM,KAAiB,MAAoD;EACvE,QAAY,CAAK,GACb,GAAM,kBACV;OAAI,EAAM,QAAQ,YAAY,KAAc,GAAQ;IAEhD,AADA,EAAM,eAAe,GACrB,EAAO;IACP;GACJ;GACI,EAAM,QAAQ,WAAW,EAAM,YAC/B,EAAM,YAAY,eAAe,EAAM,YAAY,QACvD,EAAM,eAAe,GACrB,EAAY;EAJZ;CAKJ,GAEM,IAAW,KAAc,MAAW,KAAA;CAE1C,OACI,kBAAC,QAAD;EAAM,WAAW,EAAG,EAAO,UAAU,CAAS;EAAG,UAAU;YAA3D,CACI,kBAAC,OAAD;GAAK,WAAW,EAAO;aAAvB,CACI,kBAAC,YAAD;IACI,GAAI;IACJ,KAAK;IACL,WAAW,EAAO;IAClB,MAAM;IACC;IACG;IACV,aAAa,KAAe,EAAQ;IACpC,WAAW,MAAU;KAEjB,AADA,EAAS,EAAM,OAAO,KAAK,GAC3B,IAAW,CAAK;IACpB;IACA,WAAW;GACd,CAAA,GACD,kBAAC,OAAD;IAAK,WAAW,EAAO;cAAvB,CACK,GACA,IACG,kBAAC,UAAD;KAAQ,MAAK;KAAS,WAAW,EAAO;KAAM,SAAS;eAAvD,CACI,kBAAC,QAAD;MAAM,eAAY;MAAO,WAAW,EAAO;KAAY,CAAA,GACtD,EAAQ,IACL;SAER,kBAAC,UAAD;KACI,MAAK;KACL,WAAW,EAAO;KAClB,UAAU,KAAY,KAAQ,KAAc,EAAM,KAAK,MAAM;eAE5D,EAAQ;IACL,CAAA,CAEX;KACJ;MACJ,KAAU,kBAAC,OAAD;GAAK,WAAW,EAAO;aAAiB;EAAY,CAAA,CAC7D;;AAEd,CACJ"}
@@ -0,0 +1,3 @@
1
+ const e=require("../../utils/cn.cjs"),t=require("../../utils/numbers.cjs"),n=require("../CopyButton/CopyButton.cjs"),r=require("../VisuallyHidden/VisuallyHidden.cjs"),i=require("../Collapsible/Collapsible.cjs"),a=require("./ai-chat-turns.cjs"),o=require("./AIChat.module.cjs"),s=require("../Markdown/Markdown.cjs");let c=require("react"),l=require("react/jsx-runtime"),u=require("lucide-react");function d({message:d,locale:f=`pt-BR`,canRegenerate:p=!1,onRegenerate:m,onFeedback:h,onEditSubmit:g,onEditError:_,onRetry:v,renderAvatar:y,renderContent:b,vote:x,defaultReasoningOpen:S=!1,showLineNumbers:C=!1}){let w=a.aiChatStrings(f),{role:T,content:E,reasoning:D,streaming:O,error:k,model:A,createdAt:j,attachments:M}=d,[N,P]=(0,c.useState)(null),[F,I]=(0,c.useState)(!1),[L,R]=(0,c.useState)(E),[z,B]=(0,c.useState)(!1),V=x??N,H=T===`user`,U=(0,c.useMemo)(()=>(0,l.jsx)(s.Markdown,{source:E,showLineNumbers:C,className:o.default.markdownRoot}),[E,C]),W=b?b(d):H?(0,l.jsx)(`div`,{className:o.default.plain,children:E}):E===``?null:U,G=async()=>{let e=L.trim();if(!(!e||z||!g)){B(!0);try{await g(d,e),I(!1)}catch(e){_?.(e)}finally{B(!1)}}},K=e=>{V!==e&&(P(e),h?.(d,e))},q=!O&&!F,J=h!==void 0||p&&m!==void 0;return(0,l.jsxs)(`article`,{className:e.cn(o.default.turn,H&&o.default.userTurn,T===`system`&&o.default.systemTurn,O&&o.default.streamingTurn),"aria-busy":O||void 0,"data-role":T,children:[(0,l.jsxs)(`header`,{className:o.default.turnHeader,children:[y&&(0,l.jsx)(`span`,{className:o.default.avatar,"aria-hidden":`true`,children:y(d)}),(0,l.jsx)(`span`,{className:e.cn(o.default.role,H&&o.default.visuallyHiddenRole),children:a.roleLabel(T,w)}),A&&!H&&(0,l.jsx)(`span`,{className:o.default.model,children:A}),j!==void 0&&(0,l.jsx)(`time`,{className:o.default.time,dateTime:new Date(j).toISOString(),children:a.turnTime(j,f)})]}),M&&M.length>0&&(0,l.jsx)(`ul`,{className:o.default.attachments,children:M.map(e=>(0,l.jsx)(`li`,{className:o.default.attachment,children:e.url?(0,l.jsx)(`img`,{className:o.default.thumb,src:e.url,alt:e.name}):(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(`span`,{className:o.default.attachmentName,children:e.name}),(0,l.jsx)(`span`,{className:o.default.attachmentMeta,children:e.size===void 0?e.mimeType:t.formatBytes(e.size)})]})},e.id))}),D&&(0,l.jsx)(i.Collapsible,{className:o.default.reasoning,defaultOpen:S||O===!0&&E===``,trigger:(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(u.ChevronRight,{className:o.default.reasoningChevron,size:14,"aria-hidden":!0}),w.reasoning]}),children:(0,l.jsx)(`div`,{className:o.default.reasoningBody,children:D})}),F?(0,l.jsxs)(`div`,{className:o.default.editor,children:[(0,l.jsx)(r.VisuallyHidden,{children:(0,l.jsx)(`label`,{htmlFor:`${d.id}-edit`,children:w.editing})}),(0,l.jsx)(`textarea`,{id:`${d.id}-edit`,className:o.default.editField,value:L,rows:Math.min(L.split(`
2
+ `).length+1,10),onChange:e=>R(e.target.value)}),(0,l.jsxs)(`div`,{className:o.default.editActions,children:[(0,l.jsx)(`button`,{type:`button`,className:o.default.ghost,onClick:()=>{R(E),I(!1)},children:w.cancel}),(0,l.jsx)(`button`,{type:`button`,className:o.default.primary,disabled:z||L.trim()===``,onClick:()=>void G(),children:w.save})]})]}):(0,l.jsxs)(`div`,{className:e.cn(o.default.body,O&&o.default.streamingBody),children:[W,O===!0&&E===``&&(0,l.jsxs)(`span`,{className:o.default.dots,"aria-hidden":`true`,children:[(0,l.jsx)(`span`,{}),(0,l.jsx)(`span`,{}),(0,l.jsx)(`span`,{})]})]}),k&&(0,l.jsxs)(`p`,{className:o.default.error,role:`alert`,children:[(0,l.jsx)(`span`,{children:k}),v&&(0,l.jsx)(`button`,{type:`button`,className:o.default.retry,onClick:()=>v(d),children:w.retry})]}),q&&(0,l.jsxs)(`div`,{className:o.default.actions,"aria-label":w.turnActions,role:`group`,children:[(0,l.jsx)(n.CopyButton,{className:o.default.action,value:E,"aria-label":w.copy,title:w.copy,children:(0,l.jsx)(u.Copy,{size:14,"aria-hidden":!0})}),H&&g&&(0,l.jsx)(`button`,{type:`button`,className:o.default.action,"aria-label":w.edit,title:w.edit,onClick:()=>{R(E),I(!0)},children:(0,l.jsx)(u.Pencil,{size:14,"aria-hidden":!0})}),!H&&J&&(0,l.jsxs)(l.Fragment,{children:[p&&m&&(0,l.jsx)(`button`,{type:`button`,className:o.default.action,"aria-label":w.regenerate,title:w.regenerate,onClick:()=>m(d),children:(0,l.jsx)(u.RefreshCw,{size:14,"aria-hidden":!0})}),h&&(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(`button`,{type:`button`,className:e.cn(o.default.action,V===`up`&&o.default.actionActive),"aria-label":w.good,title:w.good,"aria-pressed":V===`up`,onClick:()=>K(`up`),children:(0,l.jsx)(u.ThumbsUp,{size:14,"aria-hidden":!0})}),(0,l.jsx)(`button`,{type:`button`,className:e.cn(o.default.action,V===`down`&&o.default.actionActive),"aria-label":w.bad,title:w.bad,"aria-pressed":V===`down`,onClick:()=>K(`down`),children:(0,l.jsx)(u.ThumbsDown,{size:14,"aria-hidden":!0})})]})]})]})]})}exports.AIChatTurn=d;
3
+ //# sourceMappingURL=AIChatTurn.cjs.map