@tomny-dev/uzi 0.7.1 → 0.8.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 (41) hide show
  1. package/README.md +8 -0
  2. package/dist/components/ai-chat/AIChat.d.ts +164 -0
  3. package/dist/components/ai-chat/AIChat.d.ts.map +1 -0
  4. package/dist/components/ai-chat/AIChat.test.d.ts +2 -0
  5. package/dist/components/ai-chat/AIChat.test.d.ts.map +1 -0
  6. package/dist/components/ai-chat/AIChatDock.d.ts +33 -0
  7. package/dist/components/ai-chat/AIChatDock.d.ts.map +1 -0
  8. package/dist/components/floating-action-stack/FloatingActionStack.d.ts +19 -0
  9. package/dist/components/floating-action-stack/FloatingActionStack.d.ts.map +1 -0
  10. package/dist/components/floating-action-stack/FloatingActionStack.test.d.ts +2 -0
  11. package/dist/components/floating-action-stack/FloatingActionStack.test.d.ts.map +1 -0
  12. package/dist/index.cjs +9 -0
  13. package/dist/index.d.ts +7 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +4 -1
  16. package/dist/index75.js +91 -0
  17. package/dist/index75.js.map +1 -0
  18. package/dist/index76.cjs +133 -0
  19. package/dist/index76.cjs.map +1 -0
  20. package/dist/index76.js +667 -0
  21. package/dist/index76.js.map +1 -0
  22. package/dist/index77.cjs +669 -0
  23. package/dist/index77.cjs.map +1 -0
  24. package/dist/index77.js +35 -0
  25. package/dist/index77.js.map +1 -0
  26. package/dist/index78.cjs +49 -0
  27. package/dist/index78.cjs.map +1 -0
  28. package/dist/index78.js +231 -0
  29. package/dist/index78.js.map +1 -0
  30. package/dist/index79.cjs +231 -0
  31. package/dist/index79.cjs.map +1 -0
  32. package/dist/index79.js +19 -0
  33. package/dist/index79.js.map +1 -0
  34. package/dist/index80.cjs +25 -0
  35. package/dist/index80.cjs.map +1 -0
  36. package/dist/index80.js +36 -0
  37. package/dist/index80.js.map +1 -0
  38. package/dist/index81.cjs +37 -0
  39. package/dist/index81.cjs.map +1 -0
  40. package/dist/style.css +631 -0
  41. package/package.json +3 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index76.cjs","names":[],"sources":["../src/components/ai-chat/ai-chat.module.css"],"sourcesContent":[".container {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n max-height: none;\n overflow: hidden;\n position: relative;\n}\n\n.scope {\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n padding: 0.75rem 3rem 0.75rem 1rem;\n background: var(--uzi-surface-1, var(--muted));\n border-bottom: 1px solid var(--border);\n}\n\n.scopeLabel {\n font-size: 0.6875rem;\n font-weight: 650;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--muted-foreground);\n}\n\n.scope strong {\n font-size: 0.875rem;\n}\n\n.scopeHelp {\n margin: 0;\n font-size: 0.75rem;\n color: var(--muted-foreground);\n line-height: 1.5;\n}\n\n.closeBtn {\n position: absolute;\n top: 0.5rem;\n right: 0.5rem;\n z-index: 1;\n display: grid;\n place-items: center;\n width: 2rem;\n height: 2rem;\n border: 0;\n border-radius: 50%;\n background: transparent;\n color: var(--muted-foreground);\n cursor: pointer;\n font-size: 0.875rem;\n}\n\n.closeBtn:hover {\n background: var(--uzi-surface-2, var(--muted));\n color: var(--foreground);\n}\n\n.closeBtn:focus-visible,\n.sendButton:focus-visible,\n.errorActions button:focus-visible,\n.historyLimit button:focus-visible {\n outline: 2px solid var(--primary);\n outline-offset: 2px;\n}\n\n.thread {\n display: flex;\n flex: 1;\n flex-direction: column;\n min-height: 0;\n overflow: hidden;\n}\n\n.viewport {\n flex: 1;\n display: flex;\n flex-direction: column;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n gap: 0.75rem;\n}\n\n.chatToolbar {\n position: sticky;\n top: 0;\n z-index: 1;\n display: flex;\n justify-content: flex-end;\n padding: 0.5rem 1rem;\n background: var(--uzi-surface-1, var(--background));\n border-bottom: 1px solid var(--border);\n}\n\n.chatToolbar button {\n border: 0;\n border-radius: 0.375rem;\n padding: 0.35rem 0.5rem;\n background: transparent;\n color: var(--muted-foreground);\n font: inherit;\n font-size: 0.75rem;\n cursor: pointer;\n}\n\n.chatToolbar button:hover {\n color: var(--foreground);\n background: var(--uzi-surface-2, var(--muted));\n}\n\n.chatToolbar button:focus-visible {\n outline: 2px solid var(--primary);\n outline-offset: 2px;\n}\n\n.messageList {\n display: flex;\n flex: 1 0 auto;\n flex-direction: column;\n gap: 0.75rem;\n padding: 1rem;\n}\n\n.userMessage,\n.assistantMessage {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n padding: 0.75rem 1rem;\n border-radius: 0.75rem;\n color: var(--foreground);\n overflow-wrap: anywhere;\n}\n\n.userMessage {\n background: color-mix(in srgb, var(--primary) 10%, transparent);\n border: 1px solid color-mix(in srgb, var(--primary) 20%, transparent);\n}\n\n.assistantMessage {\n background: var(--muted);\n border: 1px solid var(--border);\n}\n\n.textPart {\n white-space: pre-wrap;\n line-height: 1.6;\n}\n\n.reasoning,\n.toolPart {\n border: 1px solid var(--border);\n border-radius: 0.5rem;\n padding: 0.5rem 0.75rem;\n background: var(--uzi-surface-1, transparent);\n}\n\n.reasoningSummary,\n.toolPart summary {\n cursor: pointer;\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--muted-foreground);\n}\n\n.reasoningText {\n margin: 0.5rem 0 0;\n font-size: 0.8125rem;\n line-height: 1.6;\n color: var(--muted-foreground);\n white-space: pre-wrap;\n}\n\n.sources {\n padding-top: 0.5rem;\n border-top: 1px solid var(--border);\n}\n\n.sourcesLabel {\n display: block;\n font-size: 0.6875rem;\n font-weight: 650;\n color: var(--muted-foreground);\n margin-bottom: 0.25rem;\n}\n\n.sources ul {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n margin: 0;\n padding-left: 1.25rem;\n}\n\n.sources li {\n font-size: 0.75rem;\n overflow-wrap: anywhere;\n}\n\n.data {\n padding: 0.5rem;\n background: var(--uzi-surface-2, var(--muted));\n border-radius: 0.5rem;\n overflow-x: auto;\n}\n\n.structuredData {\n margin: 0;\n padding: 0.5rem 0;\n max-width: 100%;\n overflow-x: auto;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n font-size: 0.75rem;\n line-height: 1.5;\n}\n\n.footer {\n position: sticky;\n bottom: 0;\n z-index: 1;\n flex-shrink: 0;\n background: var(--background);\n border-top: 1px solid var(--border);\n}\n\n.composerForm {\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto;\n align-items: end;\n gap: 0.75rem;\n padding: 0.75rem 1rem;\n}\n\n.input {\n width: 100%;\n min-height: 2.5rem;\n padding: 0.5rem 0.75rem;\n border: 1px solid var(--border);\n border-radius: 0.5rem;\n background: var(--background);\n color: var(--foreground);\n font: inherit;\n font-size: 0.875rem;\n line-height: 1.5;\n resize: vertical;\n}\n\n.input:focus {\n outline: none;\n border-color: var(--primary);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 30%, transparent);\n}\n\n.input::placeholder {\n color: var(--muted-foreground);\n}\n\n.input:disabled,\n.sendButton:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.sendButton {\n min-height: 2.5rem;\n padding: 0.5rem 0.875rem;\n border: 1px solid var(--border);\n border-radius: 0.5rem;\n background: var(--primary);\n color: var(--primary-foreground);\n font: inherit;\n font-size: 0.875rem;\n font-weight: 600;\n cursor: pointer;\n}\n\n.sendButton:not(:disabled):hover {\n filter: brightness(0.95);\n}\n\n.status {\n margin: 0;\n padding: 0.5rem 1rem;\n text-align: center;\n font-size: 0.8125rem;\n color: var(--muted-foreground);\n font-style: italic;\n}\n\n.error {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n padding: 0.75rem 1rem;\n border: 1px solid var(--destructive);\n border-radius: 0.5rem;\n margin: 0.75rem 1rem;\n background: color-mix(in srgb, var(--destructive) 10%, transparent);\n}\n\n.error p {\n margin: 0;\n color: var(--destructive);\n font-size: 0.8125rem;\n}\n\n.errorActions {\n display: flex;\n gap: 0.5rem;\n}\n\n.errorActions button,\n.historyLimit button {\n padding: 0.375rem 0.75rem;\n border: 1px solid var(--border);\n border-radius: 0.375rem;\n background: transparent;\n color: var(--foreground);\n font: inherit;\n font-size: 0.75rem;\n cursor: pointer;\n}\n\n.errorActions button:hover,\n.historyLimit button:hover {\n background: var(--uzi-surface-2, var(--muted));\n}\n\n.historyLimit {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n align-items: center;\n padding: 0.75rem 1rem;\n margin: 0.75rem 1rem;\n background: var(--uzi-surface-1, var(--muted));\n border: 1px solid var(--border);\n border-radius: 0.5rem;\n}\n\n.historyLimit span {\n font-size: 0.75rem;\n color: var(--muted-foreground);\n}\n\n.emptyState {\n display: grid;\n place-items: center;\n flex: 1;\n min-height: 12rem;\n color: var(--muted-foreground);\n text-align: center;\n}\n\n.emptyState p {\n margin: 0;\n font-size: 0.875rem;\n}\n\n@media (max-width: 640px) {\n .composerForm {\n grid-template-columns: minmax(0, 1fr) auto;\n }\n\n .errorActions {\n flex-wrap: wrap;\n }\n}\n\n/* Optional integration controls use the same Uzi surface and layout tokens. */\n.composerExtras {\n grid-column: 1 / -1;\n min-width: 0;\n}\n\n.composerActions {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n.approvalActions { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; padding-top: 0.5rem; }\n.approvalActions button, .attachmentList button {\n background: var(--background); color: var(--foreground); border: 1px solid var(--border);\n border-radius: 0.375rem; padding: 0.3rem 0.55rem; cursor: pointer;\n}\n.approvalActions button:disabled { opacity: 0.5; cursor: not-allowed; }\n.attachmentError { color: var(--destructive); padding: 0.5rem 1rem; margin: 0; }\n.attachmentList { display: flex; flex-wrap: wrap; gap: 0.5rem; padding: 0.4rem 0; margin: 0; list-style: none; }\n.attachmentList li { display: inline-flex; gap: 0.5rem; align-items: center; font-size: 0.75rem; }\n\n/* Role-aware message layout shared by the default renderer and consumer apps. */\n.messageBubble { width: fit-content; min-width: 0; max-width: 100%; }\n.messageBubble.userMessage { align-self: flex-end; max-width: min(90%, 42rem); }\n.messageBubble.assistantMessage { align-self: flex-start; max-width: 100%; }\n.messageLabel { font-size: 0.6875rem; font-weight: 650; color: var(--muted-foreground); }\n\n/* Shared compact composer: same visual grammar as Betty, without app-owned behavior. */\n.composerSurface {\n grid-column: 1 / -1;\n min-width: 0;\n border: 1px solid var(--border);\n border-radius: 0.875rem;\n background: var(--background);\n}\n.composerSurface:focus-within { outline: 2px solid var(--ring); outline-offset: 2px; }\n.composerRow { display: flex; align-items: end; min-width: 0; gap: 0.375rem; padding: 0.375rem; }\n.composerRow .input {\n display: block; flex: 1; width: 0; min-width: 0; min-height: 2.75rem;\n max-height: 11rem; padding: 0.625rem 0.25rem; border: 0;\n background: transparent; resize: none; font-size: 1rem;\n}\n.composerRow .input:focus { outline: none; box-shadow: none; }\n.composerIconButton, .composerRow .sendButton {\n display: grid; place-items: center; flex-shrink: 0;\n width: 2.75rem; height: 2.75rem; min-height: 2.75rem;\n padding: 0; border-radius: 0.625rem; cursor: pointer;\n}\n.composerIconButton {\n color: var(--foreground); border: 0; background: transparent;\n}\n.composerIconButton:hover:not(:disabled) { background: var(--muted); }\n.composerRow .sendButton { border: 0; background: var(--primary); color: var(--primary-foreground); }\n.composerIconButton:focus-visible, .composerRow .sendButton:focus-visible {\n outline: 2px solid var(--ring); outline-offset: 2px;\n}\n.composerIconButton:disabled { opacity: 0.5; cursor: not-allowed; }\n.visuallyHidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }\n\n/* Thumbnail treatment matches the Betty attachment shelf. */\n.attachmentList {\n gap: 0.5rem;\n padding: 0;\n align-items: center;\n}\n.attachmentList li {\n max-width: 100%;\n position: relative;\n min-height: 3rem;\n border-radius: 0.625rem;\n border: 1px solid var(--border);\n background: var(--uzi-surface-1, var(--muted));\n padding: 0.125rem;\n}\n.attachmentThumbnail {\n width: 3rem;\n height: 3rem;\n border-radius: 0.5rem;\n object-fit: cover;\n}\n.attachmentFilename {\n max-width: 11rem;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n padding: 0.5rem;\n}\n.attachmentList li button {\n flex-shrink: 0;\n border: 0;\n background: transparent;\n color: var(--muted-foreground);\n border-radius: 0.4rem;\n}\n.attachmentList li button:hover { color: var(--foreground); background: var(--muted); }\n"],"mappings":""}
@@ -0,0 +1,667 @@
1
+ "use client";
2
+ import ai_chat_module_default from "./index75.js";
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ import { AuiConfig, AuiProvider, ThreadPrimitive } from "@assistant-ui/react";
6
+ import { AISDKChat, AssistantChatTransport, useAISDKChat } from "@assistant-ui/ai-sdk";
7
+ //#region src/components/ai-chat/AIChat.tsx
8
+ function isRecord(value) {
9
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10
+ }
11
+ function asString(value) {
12
+ return typeof value === "string" ? value : void 0;
13
+ }
14
+ function safeHttpUrl(value) {
15
+ const url = asString(value);
16
+ return url && /^https?:\/\//i.test(url) ? url : void 0;
17
+ }
18
+ function AIChatMessageBubble({ role, label, children, className }) {
19
+ return /* @__PURE__ */ jsxs("div", {
20
+ className: [
21
+ ai_chat_module_default.messageBubble,
22
+ role === "user" ? ai_chat_module_default.userMessage : ai_chat_module_default.assistantMessage,
23
+ className
24
+ ].filter(Boolean).join(" "),
25
+ "data-role": role,
26
+ children: [label && /* @__PURE__ */ jsx("span", {
27
+ className: ai_chat_module_default.messageLabel,
28
+ children: label
29
+ }), children]
30
+ });
31
+ }
32
+ function DefaultMessageRenderer({ message, renderMessagePart }) {
33
+ return /* @__PURE__ */ jsx(AIChatMessageBubble, {
34
+ role: message.role,
35
+ children: message.parts.map((part, index) => /* @__PURE__ */ jsx("div", { children: isRecord(part) ? renderMessagePart(part, index) : null }, isRecord(part) && typeof part.id === "string" ? part.id : index))
36
+ });
37
+ }
38
+ function DefaultReasoningRenderer({ children, title = "Reasoning" }) {
39
+ return /* @__PURE__ */ jsxs("details", {
40
+ className: ai_chat_module_default.reasoning,
41
+ children: [/* @__PURE__ */ jsx("summary", {
42
+ className: ai_chat_module_default.reasoningSummary,
43
+ children: title
44
+ }), /* @__PURE__ */ jsx("p", {
45
+ className: ai_chat_module_default.reasoningText,
46
+ children
47
+ })]
48
+ });
49
+ }
50
+ function DefaultSourceRenderer({ sources, renderSource }) {
51
+ return /* @__PURE__ */ jsxs("div", {
52
+ className: ai_chat_module_default.sources,
53
+ children: [/* @__PURE__ */ jsx("span", {
54
+ className: ai_chat_module_default.sourcesLabel,
55
+ children: "Sources"
56
+ }), /* @__PURE__ */ jsx("ul", { children: sources.map((source, index) => /* @__PURE__ */ jsx("li", { children: renderSource(source, index) }, source.id)) })]
57
+ });
58
+ }
59
+ function DefaultDataRenderer({ data, renderDataItem }) {
60
+ return /* @__PURE__ */ jsx("div", {
61
+ className: ai_chat_module_default.data,
62
+ children: data.map((item, index) => /* @__PURE__ */ jsx("div", { children: renderDataItem(item, index) }, index))
63
+ });
64
+ }
65
+ function DefaultErrorRenderer({ error, onRetry, onClear }) {
66
+ return /* @__PURE__ */ jsxs("div", {
67
+ className: ai_chat_module_default.error,
68
+ role: "alert",
69
+ children: [/* @__PURE__ */ jsx("p", { children: error.message }), /* @__PURE__ */ jsxs("div", {
70
+ className: ai_chat_module_default.errorActions,
71
+ children: [/* @__PURE__ */ jsx("button", {
72
+ type: "button",
73
+ onClick: onRetry,
74
+ children: "Retry response"
75
+ }), /* @__PURE__ */ jsx("button", {
76
+ type: "button",
77
+ onClick: onClear,
78
+ children: "Start new chat"
79
+ })]
80
+ })]
81
+ });
82
+ }
83
+ function renderPart(part, index, components, onToolApproval, pendingApprovalId) {
84
+ const type = asString(part.type) ?? "";
85
+ if (type === "step-start") return null;
86
+ if (type === "text") return /* @__PURE__ */ jsx("span", {
87
+ className: ai_chat_module_default.textPart,
88
+ children: asString(part.text) ?? ""
89
+ });
90
+ if (type === "reasoning") return /* @__PURE__ */ jsx(components?.ReasoningRenderer ?? DefaultReasoningRenderer, { children: asString(part.text) ?? "" });
91
+ if (type === "source-url" || type === "source-document") return /* @__PURE__ */ jsx(components?.SourceRenderer ?? DefaultSourceRenderer, {
92
+ sources: [{
93
+ id: asString(part.sourceId) ?? asString(part.id) ?? `source-${index}`,
94
+ title: asString(part.title) ?? asString(part.filename),
95
+ url: safeHttpUrl(part.url)
96
+ }],
97
+ renderSource: (item) => item.url ? /* @__PURE__ */ jsx("a", {
98
+ href: item.url,
99
+ target: "_blank",
100
+ rel: "noopener noreferrer",
101
+ children: item.title ?? item.url
102
+ }) : /* @__PURE__ */ jsx("span", { children: item.title ?? item.id })
103
+ });
104
+ if (type.startsWith("data-")) return /* @__PURE__ */ jsx(components?.DataRenderer ?? DefaultDataRenderer, {
105
+ data: [part],
106
+ renderDataItem: (item) => /* @__PURE__ */ jsx("pre", {
107
+ className: ai_chat_module_default.structuredData,
108
+ children: JSON.stringify(item.data ?? item, null, 2)
109
+ })
110
+ });
111
+ if (type.startsWith("tool-") || type === "dynamic-tool") {
112
+ const name = asString(part.toolName) ?? type.replace(/^tool-/, "");
113
+ const approval = isRecord(part.approval) ? asString(part.approval.id) : void 0;
114
+ const approvalId = part.state === "approval-requested" ? approval : void 0;
115
+ const controlsDisabled = pendingApprovalId !== null && pendingApprovalId !== void 0;
116
+ const ToolRenderer = components?.ToolRenderer;
117
+ const props = {
118
+ part,
119
+ name,
120
+ approvalId,
121
+ approvalPending: controlsDisabled,
122
+ ...approvalId && onToolApproval ? {
123
+ onApprove: () => {
124
+ if (!controlsDisabled) onToolApproval(approvalId, true);
125
+ },
126
+ onDeny: () => {
127
+ if (!controlsDisabled) onToolApproval(approvalId, false);
128
+ }
129
+ } : {}
130
+ };
131
+ if (ToolRenderer) return /* @__PURE__ */ jsx(ToolRenderer, { ...props });
132
+ return /* @__PURE__ */ jsxs("div", {
133
+ className: ai_chat_module_default.toolPart,
134
+ children: [/* @__PURE__ */ jsxs("details", { children: [/* @__PURE__ */ jsx("summary", { children: name }), /* @__PURE__ */ jsx("pre", {
135
+ className: ai_chat_module_default.structuredData,
136
+ children: JSON.stringify(part.output ?? part.input ?? part, null, 2)
137
+ })] }), approvalId && /* @__PURE__ */ jsx("div", {
138
+ className: ai_chat_module_default.approvalActions,
139
+ "aria-label": `Approval required for ${name}`,
140
+ children: props.onApprove && props.onDeny ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
141
+ type: "button",
142
+ disabled: controlsDisabled,
143
+ onClick: props.onApprove,
144
+ children: "Approve"
145
+ }), /* @__PURE__ */ jsx("button", {
146
+ type: "button",
147
+ disabled: controlsDisabled,
148
+ onClick: props.onDeny,
149
+ children: "Deny"
150
+ })] }) : /* @__PURE__ */ jsx("span", { children: "Approval required in application" })
151
+ })]
152
+ });
153
+ }
154
+ if (type === "file" || type === "image") {
155
+ const url = safeHttpUrl(part.url);
156
+ return url ? /* @__PURE__ */ jsx("a", {
157
+ href: url,
158
+ target: "_blank",
159
+ rel: "noopener noreferrer",
160
+ children: asString(part.filename) ?? "View attachment"
161
+ }) : /* @__PURE__ */ jsx("span", { children: "Attachment" });
162
+ }
163
+ return /* @__PURE__ */ jsx("pre", {
164
+ className: ai_chat_module_default.structuredData,
165
+ children: JSON.stringify(part, null, 2)
166
+ });
167
+ }
168
+ var DEFAULT_MESSAGE_LIMIT = 8;
169
+ function AIChat({ api, scope, scopeLabel, fetch: requestFetch, headers, credentials, prepareSendMessagesRequest, sessionKey, messageLimit = DEFAULT_MESSAGE_LIMIT, inputPlaceholder = "Ask a question...", inputAriaLabel = "Message", inputMaxLength = 2e3, components, attachments, history, sendAutomaticallyWhen, onClose, className, style }) {
170
+ const [session, setSession] = useState(0);
171
+ const transport = useMemo(() => new AssistantChatTransport({
172
+ api,
173
+ ...scope ? { body: { scope } } : {},
174
+ ...requestFetch ? { fetch: requestFetch } : {},
175
+ ...headers ? { headers } : {},
176
+ ...credentials ? { credentials } : {},
177
+ ...prepareSendMessagesRequest ? { prepareSendMessagesRequest } : {}
178
+ }), [
179
+ api,
180
+ scope,
181
+ requestFetch,
182
+ headers,
183
+ credentials,
184
+ prepareSendMessagesRequest
185
+ ]);
186
+ const config = useMemo(() => AuiConfig({ threads: AISDKChat({
187
+ transport,
188
+ ...sendAutomaticallyWhen ? { sendAutomaticallyWhen } : {}
189
+ }) }), [
190
+ transport,
191
+ session,
192
+ sendAutomaticallyWhen
193
+ ]);
194
+ return /* @__PURE__ */ jsxs("div", {
195
+ className: [ai_chat_module_default.container, className].filter(Boolean).join(" "),
196
+ style,
197
+ children: [
198
+ scopeLabel && /* @__PURE__ */ jsxs("div", {
199
+ className: ai_chat_module_default.scope,
200
+ children: [/* @__PURE__ */ jsx("span", {
201
+ className: ai_chat_module_default.scopeLabel,
202
+ children: "Current scope"
203
+ }), /* @__PURE__ */ jsx("strong", { children: scopeLabel })]
204
+ }),
205
+ onClose && /* @__PURE__ */ jsx("button", {
206
+ className: ai_chat_module_default.closeBtn,
207
+ type: "button",
208
+ onClick: onClose,
209
+ "aria-label": "Close chat",
210
+ children: "✕"
211
+ }),
212
+ /* @__PURE__ */ jsx(AuiProvider, {
213
+ config,
214
+ children: /* @__PURE__ */ jsx(ThreadPrimitive.Root, {
215
+ className: ai_chat_module_default.thread,
216
+ children: /* @__PURE__ */ jsx(ChatViewport, {
217
+ messageLimit: Number.isFinite(messageLimit) ? Math.max(1, Math.floor(messageLimit)) : DEFAULT_MESSAGE_LIMIT,
218
+ inputPlaceholder,
219
+ inputAriaLabel,
220
+ inputMaxLength,
221
+ components,
222
+ attachmentOptions: attachments,
223
+ history,
224
+ onNewChat: () => setSession((previous) => previous + 1)
225
+ })
226
+ })
227
+ }, JSON.stringify([
228
+ api,
229
+ scope,
230
+ sessionKey,
231
+ session
232
+ ]))
233
+ ]
234
+ });
235
+ }
236
+ function ChatViewport({ messageLimit, inputPlaceholder, inputAriaLabel, inputMaxLength, components, attachmentOptions, history, onNewChat }) {
237
+ const chat = useAISDKChat();
238
+ const [draft, setDraft] = useState("");
239
+ const [selectedFiles, setSelectedFiles] = useState([]);
240
+ const [attachmentError, setAttachmentError] = useState(null);
241
+ const [attachmentPreviews, setAttachmentPreviews] = useState([]);
242
+ const [hydrated, setHydrated] = useState(!history);
243
+ const expectedRestoredId = useRef(null);
244
+ const saveQueue = useRef(Promise.resolve());
245
+ const [approvalBusy, setApprovalBusy] = useState(false);
246
+ useEffect(() => {
247
+ const urls = selectedFiles.map((file) => file.type.startsWith("image/") && typeof URL.createObjectURL === "function" ? URL.createObjectURL(file) : "");
248
+ setAttachmentPreviews(urls);
249
+ return () => urls.forEach((url) => {
250
+ if (url && typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
251
+ });
252
+ }, [selectedFiles]);
253
+ const messages = chat?.messages ?? [];
254
+ const status = chat?.status ?? "ready";
255
+ const isBusy = status === "submitted" || status === "streaming";
256
+ const historyFull = messages.filter((message) => message.role === "user").length >= messageLimit;
257
+ const reportHistoryError = (operation, error) => {
258
+ if (history?.onError) history.onError(operation, error);
259
+ else console.warn(`[Uzi AIChat] History ${operation} failed`);
260
+ };
261
+ useEffect(() => {
262
+ if (!history) return;
263
+ let active = true;
264
+ Promise.resolve().then(() => history.load()).then((restored) => {
265
+ if (!active) return;
266
+ if (Array.isArray(restored)) {
267
+ const valid = restored.filter((message) => typeof message?.id === "string" && (message.role === "user" || message.role === "assistant") && Array.isArray(message.parts));
268
+ expectedRestoredId.current = valid.length ? valid[0].id : null;
269
+ if (valid.length) chat?.setMessages(valid);
270
+ }
271
+ setHydrated(true);
272
+ }).catch((error) => {
273
+ if (active) {
274
+ reportHistoryError("load", error);
275
+ setHydrated(true);
276
+ }
277
+ });
278
+ return () => {
279
+ active = false;
280
+ };
281
+ }, [chat?.setMessages, history]);
282
+ useEffect(() => {
283
+ if (!history || !hydrated || isBusy) return;
284
+ const snapshot = messages.filter((message) => message.role === "user" || message.role === "assistant").map((message) => ({
285
+ id: message.id,
286
+ role: message.role,
287
+ parts: message.parts,
288
+ ...isRecord(message.metadata) ? { metadata: message.metadata } : {}
289
+ }));
290
+ if (expectedRestoredId.current) {
291
+ if (!snapshot.some((message) => message.id === expectedRestoredId.current)) return;
292
+ expectedRestoredId.current = null;
293
+ }
294
+ saveQueue.current = saveQueue.current.then(() => history.save(snapshot), () => history.save(snapshot)).then(() => {}, (error) => {
295
+ reportHistoryError("save", error);
296
+ });
297
+ }, [
298
+ history,
299
+ hydrated,
300
+ isBusy,
301
+ messages
302
+ ]);
303
+ const handleSelectedFiles = (files) => {
304
+ if (!attachmentOptions) return;
305
+ const selected = Array.from(files);
306
+ if (selected.length + selectedFiles.length > (attachmentOptions.maxFiles ?? 3)) {
307
+ setAttachmentError("Too many attachments.");
308
+ return;
309
+ }
310
+ if (selected.some((file) => file.size > (attachmentOptions.maxBytesPerFile ?? 5 * 1024 * 1024))) {
311
+ setAttachmentError("An attachment exceeds the size limit.");
312
+ return;
313
+ }
314
+ const allowedTypes = attachmentOptions.accept.split(",").map((item) => item.trim());
315
+ if (selected.some((file) => !allowedTypes.some((allow) => allow === file.type || allow.endsWith("/*") && file.type.startsWith(allow.slice(0, -1))))) {
316
+ setAttachmentError("Unsupported attachment type.");
317
+ return;
318
+ }
319
+ setAttachmentError(null);
320
+ setSelectedFiles((existing) => [...existing, ...selected]);
321
+ };
322
+ const handleSubmit = () => {
323
+ const text = draft.trim();
324
+ if (!chat || !text && selectedFiles.length === 0 || isBusy || historyFull || chat.error || !hydrated || approvalBusy) return;
325
+ const files = selectedFiles.length ? new DataTransfer() : null;
326
+ selectedFiles.forEach((file) => files?.items.add(file));
327
+ const previousDraft = draft;
328
+ const previousFiles = selectedFiles;
329
+ setDraft("");
330
+ setSelectedFiles([]);
331
+ setAttachmentError(null);
332
+ Promise.resolve(chat.sendMessage({
333
+ text,
334
+ ...files ? { files: files.files } : {}
335
+ })).catch(() => {
336
+ setDraft((current) => current || previousDraft);
337
+ setSelectedFiles((current) => current.length ? current : previousFiles);
338
+ });
339
+ };
340
+ const handleNewChat = () => {
341
+ (async () => {
342
+ await chat?.stop();
343
+ if (history) try {
344
+ await saveQueue.current;
345
+ await history.clear();
346
+ } catch (error) {
347
+ reportHistoryError("clear", error);
348
+ return;
349
+ }
350
+ onNewChat();
351
+ })();
352
+ };
353
+ const handleRetry = () => {
354
+ if (!chat || isBusy) return;
355
+ chat.clearError();
356
+ chat.regenerate();
357
+ };
358
+ return /* @__PURE__ */ jsx(AIChatView, {
359
+ messages: messages.filter((message) => message.role === "user" || message.role === "assistant").map((message) => ({
360
+ id: message.id,
361
+ role: message.role,
362
+ parts: message.parts,
363
+ ...isRecord(message.metadata) ? { metadata: message.metadata } : {}
364
+ })),
365
+ status,
366
+ error: chat?.error,
367
+ draft,
368
+ onDraftChange: setDraft,
369
+ onSend: handleSubmit,
370
+ onStop: () => {
371
+ chat?.stop();
372
+ },
373
+ onRetry: handleRetry,
374
+ onNewChat: handleNewChat,
375
+ messageLimit,
376
+ attachments: selectedFiles.map((file, index) => ({
377
+ name: file.name,
378
+ size: file.size,
379
+ ...attachmentPreviews[index] ? { previewUrl: attachmentPreviews[index] } : {}
380
+ })),
381
+ onSelectAttachments: attachmentOptions ? handleSelectedFiles : void 0,
382
+ onRemoveAttachment: attachmentOptions ? (index) => setSelectedFiles((files) => files.filter((_, i) => i !== index)) : void 0,
383
+ attachmentAccept: attachmentOptions?.accept,
384
+ attachmentError,
385
+ onToolApproval: chat ? async (id, approved) => {
386
+ if (approvalBusy) return;
387
+ setApprovalBusy(true);
388
+ try {
389
+ await chat.addToolApprovalResponse({
390
+ id,
391
+ approved,
392
+ ...!approved ? { reason: "User denied approval" } : {}
393
+ });
394
+ } finally {
395
+ setApprovalBusy(false);
396
+ }
397
+ } : void 0,
398
+ inputPlaceholder,
399
+ inputAriaLabel,
400
+ inputMaxLength,
401
+ components
402
+ });
403
+ }
404
+ /**
405
+ * Controlled chat presentation for apps with their own AI SDK/assistant-ui
406
+ * runtime. It does not create providers, transports or chat state. Callbacks
407
+ * are the only way messages/actions leave the view.
408
+ */
409
+ function AIChatView({ messages, status = "ready", error, draft = "", onDraftChange, onSend, onStop, onRetry, onNewChat, messageLimit, inputPlaceholder = "Ask a question...", inputAriaLabel = "Message", inputMaxLength = 2e3, components, attachments, onSelectAttachments, onRemoveAttachment, attachmentAccept, attachmentError, onToolApproval, toolbarActions, header, composerLeading, composerTrailing, composer, emptyState, className, style }) {
410
+ const viewportRef = useRef(null);
411
+ const pinnedToBottom = useRef(true);
412
+ const fileInputRef = useRef(null);
413
+ const approvalRef = useRef(null);
414
+ const [pendingApprovalId, setPendingApprovalId] = useState(null);
415
+ const [approvalError, setApprovalError] = useState(null);
416
+ const respondToApproval = async (id, approved) => {
417
+ if (!onToolApproval || approvalRef.current) return;
418
+ approvalRef.current = id;
419
+ setPendingApprovalId(id);
420
+ setApprovalError(null);
421
+ try {
422
+ await onToolApproval(id, approved);
423
+ } catch {
424
+ setApprovalError("Could not submit approval. Please try again.");
425
+ } finally {
426
+ approvalRef.current = null;
427
+ setPendingApprovalId(null);
428
+ }
429
+ };
430
+ const isBusy = status === "submitted" || status === "streaming";
431
+ const limit = messageLimit != null && Number.isFinite(messageLimit) ? Math.max(1, Math.floor(messageLimit)) : null;
432
+ const historyFull = limit !== null && messages.filter((message) => message.role === "user").length >= limit;
433
+ const MessageRenderer = components?.MessageRenderer ?? DefaultMessageRenderer;
434
+ const ErrorRenderer = components?.ErrorRenderer ?? DefaultErrorRenderer;
435
+ useEffect(() => {
436
+ const viewport = viewportRef.current;
437
+ if (viewport && pinnedToBottom.current) viewport.scrollTop = viewport.scrollHeight;
438
+ });
439
+ const handleKeyDown = (event) => {
440
+ if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
441
+ event.preventDefault();
442
+ if (!isBusy && !historyFull && !error && (draft.trim() || (attachments?.length ?? 0) > 0)) onSend?.();
443
+ }
444
+ };
445
+ const handleFormSubmit = (event) => {
446
+ event.preventDefault();
447
+ if (!isBusy && !historyFull && !error && (draft.trim() || (attachments?.length ?? 0) > 0)) onSend?.();
448
+ };
449
+ return /* @__PURE__ */ jsxs("div", {
450
+ className: [ai_chat_module_default.thread, className].filter(Boolean).join(" "),
451
+ style,
452
+ children: [
453
+ header,
454
+ /* @__PURE__ */ jsxs("div", {
455
+ ref: viewportRef,
456
+ className: ai_chat_module_default.viewport,
457
+ onScroll: (event) => {
458
+ const viewport = event.currentTarget;
459
+ pinnedToBottom.current = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight < 80;
460
+ },
461
+ children: [(messages.length > 0 || toolbarActions) && (onNewChat || toolbarActions) && /* @__PURE__ */ jsxs("div", {
462
+ className: ai_chat_module_default.chatToolbar,
463
+ children: [toolbarActions, onNewChat && messages.length > 0 && /* @__PURE__ */ jsx("button", {
464
+ type: "button",
465
+ onClick: onNewChat,
466
+ children: "New chat"
467
+ })]
468
+ }), /* @__PURE__ */ jsxs("div", {
469
+ className: ai_chat_module_default.messageList,
470
+ role: "log",
471
+ "aria-label": "Chat history",
472
+ "aria-live": "polite",
473
+ children: [
474
+ messages.length === 0 && /* @__PURE__ */ jsx("div", {
475
+ className: ai_chat_module_default.emptyState,
476
+ children: emptyState ?? /* @__PURE__ */ jsx("p", { children: "Start a conversation" })
477
+ }),
478
+ messages.map((message) => /* @__PURE__ */ jsx(MessageRenderer, {
479
+ message,
480
+ renderMessagePart: (part, index) => renderPart(part, index, components, onToolApproval ? respondToApproval : void 0, pendingApprovalId),
481
+ onToolApproval: onToolApproval ? respondToApproval : void 0
482
+ }, message.id)),
483
+ isBusy && /* @__PURE__ */ jsx("p", {
484
+ className: ai_chat_module_default.status,
485
+ children: "Thinking..."
486
+ })
487
+ ]
488
+ })]
489
+ }),
490
+ /* @__PURE__ */ jsxs("div", {
491
+ className: ai_chat_module_default.footer,
492
+ children: [
493
+ approvalError && /* @__PURE__ */ jsx("p", {
494
+ role: "alert",
495
+ className: ai_chat_module_default.attachmentError,
496
+ children: approvalError
497
+ }),
498
+ attachmentError && /* @__PURE__ */ jsx("p", {
499
+ role: "alert",
500
+ className: ai_chat_module_default.attachmentError,
501
+ children: attachmentError
502
+ }),
503
+ error && (onRetry && onNewChat ? /* @__PURE__ */ jsx(ErrorRenderer, {
504
+ error,
505
+ onRetry,
506
+ onClear: onNewChat
507
+ }) : /* @__PURE__ */ jsxs("div", {
508
+ className: ai_chat_module_default.error,
509
+ role: "alert",
510
+ children: [/* @__PURE__ */ jsx("p", { children: error.message }), (onRetry || onNewChat) && /* @__PURE__ */ jsxs("div", {
511
+ className: ai_chat_module_default.errorActions,
512
+ children: [onRetry && /* @__PURE__ */ jsx("button", {
513
+ type: "button",
514
+ onClick: onRetry,
515
+ children: "Retry response"
516
+ }), onNewChat && /* @__PURE__ */ jsx("button", {
517
+ type: "button",
518
+ onClick: onNewChat,
519
+ children: "Start new chat"
520
+ })]
521
+ })]
522
+ })),
523
+ historyFull && !error && !isBusy && onNewChat && /* @__PURE__ */ jsxs("div", {
524
+ className: ai_chat_module_default.historyLimit,
525
+ children: [/* @__PURE__ */ jsxs("span", { children: [
526
+ "This chat has reached its ",
527
+ limit,
528
+ "-question limit."
529
+ ] }), /* @__PURE__ */ jsx("button", {
530
+ type: "button",
531
+ onClick: onNewChat,
532
+ children: "Start new chat"
533
+ })]
534
+ }),
535
+ (!historyFull || isBusy) && !error && (composer !== void 0 ? composer : /* @__PURE__ */ jsxs("form", {
536
+ className: ai_chat_module_default.composerForm,
537
+ onSubmit: handleFormSubmit,
538
+ children: [
539
+ composerLeading && /* @__PURE__ */ jsx("div", {
540
+ className: ai_chat_module_default.composerExtras,
541
+ children: composerLeading
542
+ }),
543
+ onSelectAttachments && /* @__PURE__ */ jsxs("div", {
544
+ className: ai_chat_module_default.composerExtras,
545
+ children: [/* @__PURE__ */ jsx("input", {
546
+ ref: fileInputRef,
547
+ className: ai_chat_module_default.visuallyHidden,
548
+ type: "file",
549
+ "aria-label": "Add attachments",
550
+ accept: attachmentAccept,
551
+ multiple: true,
552
+ disabled: isBusy,
553
+ onChange: (event) => {
554
+ if (event.target.files) onSelectAttachments(event.target.files);
555
+ event.target.value = "";
556
+ }
557
+ }), attachments && attachments.length > 0 && /* @__PURE__ */ jsx("ul", {
558
+ className: ai_chat_module_default.attachmentList,
559
+ children: attachments.map((attachment, index) => /* @__PURE__ */ jsxs("li", { children: [attachment.previewUrl ? /* @__PURE__ */ jsx("img", {
560
+ className: ai_chat_module_default.attachmentThumbnail,
561
+ src: attachment.previewUrl,
562
+ alt: attachment.name
563
+ }) : /* @__PURE__ */ jsx("span", {
564
+ className: ai_chat_module_default.attachmentFilename,
565
+ children: attachment.name
566
+ }), onRemoveAttachment && /* @__PURE__ */ jsx("button", {
567
+ type: "button",
568
+ onClick: () => onRemoveAttachment(index),
569
+ "aria-label": `Remove ${attachment.name}`,
570
+ title: `Remove ${attachment.name}`,
571
+ children: "×"
572
+ })] }, index))
573
+ })]
574
+ }),
575
+ /* @__PURE__ */ jsx("div", {
576
+ className: ai_chat_module_default.composerSurface,
577
+ children: /* @__PURE__ */ jsxs("div", {
578
+ className: ai_chat_module_default.composerRow,
579
+ children: [
580
+ onSelectAttachments && /* @__PURE__ */ jsx("button", {
581
+ type: "button",
582
+ className: ai_chat_module_default.composerIconButton,
583
+ "aria-label": "Attach file",
584
+ title: "Attach file",
585
+ disabled: isBusy,
586
+ onClick: () => fileInputRef.current?.click(),
587
+ children: /* @__PURE__ */ jsx("svg", {
588
+ width: "20",
589
+ height: "20",
590
+ viewBox: "0 0 24 24",
591
+ fill: "none",
592
+ stroke: "currentColor",
593
+ strokeWidth: "1.8",
594
+ strokeLinecap: "round",
595
+ strokeLinejoin: "round",
596
+ "aria-hidden": "true",
597
+ children: /* @__PURE__ */ jsx("path", { d: "m21 11-8.5 8.5a6 6 0 0 1-8.5-8.5l9-9a4 4 0 0 1 5.7 5.7l-9 9a2 2 0 0 1-2.8-2.8l8.5-8.5" })
598
+ })
599
+ }),
600
+ /* @__PURE__ */ jsx("textarea", {
601
+ className: ai_chat_module_default.input,
602
+ "aria-label": inputAriaLabel,
603
+ rows: 1,
604
+ value: draft,
605
+ onChange: (event) => onDraftChange?.(event.target.value),
606
+ onKeyDown: handleKeyDown,
607
+ placeholder: inputPlaceholder,
608
+ maxLength: inputMaxLength,
609
+ disabled: isBusy || !onSend || !onDraftChange,
610
+ readOnly: !onDraftChange
611
+ }),
612
+ /* @__PURE__ */ jsxs("div", {
613
+ className: ai_chat_module_default.composerActions,
614
+ children: [composerTrailing, isBusy ? onStop && /* @__PURE__ */ jsx("button", {
615
+ type: "button",
616
+ className: ai_chat_module_default.composerIconButton,
617
+ onClick: onStop,
618
+ "aria-label": "Stop response",
619
+ title: "Stop response",
620
+ children: /* @__PURE__ */ jsx("svg", {
621
+ width: "18",
622
+ height: "18",
623
+ viewBox: "0 0 24 24",
624
+ fill: "currentColor",
625
+ "aria-hidden": "true",
626
+ children: /* @__PURE__ */ jsx("rect", {
627
+ x: "5",
628
+ y: "5",
629
+ width: "14",
630
+ height: "14",
631
+ rx: "2"
632
+ })
633
+ })
634
+ }) : /* @__PURE__ */ jsx("button", {
635
+ type: "submit",
636
+ className: ai_chat_module_default.sendButton,
637
+ "aria-label": "Send message",
638
+ title: "Send message",
639
+ disabled: !onSend || !draft.trim() && !attachments?.length,
640
+ children: /* @__PURE__ */ jsx("svg", {
641
+ width: "20",
642
+ height: "20",
643
+ viewBox: "0 0 24 24",
644
+ fill: "none",
645
+ stroke: "currentColor",
646
+ strokeWidth: "2",
647
+ strokeLinecap: "round",
648
+ strokeLinejoin: "round",
649
+ "aria-hidden": "true",
650
+ children: /* @__PURE__ */ jsx("path", { d: "M12 19V5m-7 7 7-7 7 7" })
651
+ })
652
+ })]
653
+ })
654
+ ]
655
+ })
656
+ })
657
+ ]
658
+ }))
659
+ ]
660
+ })
661
+ ]
662
+ });
663
+ }
664
+ //#endregion
665
+ export { AIChat, AIChatMessageBubble, AIChatView };
666
+
667
+ //# sourceMappingURL=index76.js.map