@codrstudio/openclaude-chat 0.1.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 (171) hide show
  1. package/dist/components/Chat.d.ts +23 -0
  2. package/dist/components/Chat.js +12 -0
  3. package/dist/components/ErrorNote.d.ts +6 -0
  4. package/dist/components/ErrorNote.js +6 -0
  5. package/dist/components/LazyRender.d.ts +8 -0
  6. package/dist/components/LazyRender.js +22 -0
  7. package/dist/components/Markdown.d.ts +5 -0
  8. package/dist/components/Markdown.js +65 -0
  9. package/dist/components/MessageBubble.d.ts +9 -0
  10. package/dist/components/MessageBubble.js +45 -0
  11. package/dist/components/MessageInput.d.ts +19 -0
  12. package/dist/components/MessageInput.js +214 -0
  13. package/dist/components/MessageList.d.ts +13 -0
  14. package/dist/components/MessageList.js +72 -0
  15. package/dist/components/StreamingIndicator.d.ts +1 -0
  16. package/dist/components/StreamingIndicator.js +9 -0
  17. package/dist/display/AlertRenderer.d.ts +2 -0
  18. package/dist/display/AlertRenderer.js +13 -0
  19. package/dist/display/CarouselRenderer.d.ts +2 -0
  20. package/dist/display/CarouselRenderer.js +41 -0
  21. package/dist/display/ChartRenderer.d.ts +2 -0
  22. package/dist/display/ChartRenderer.js +76 -0
  23. package/dist/display/ChoiceButtonsRenderer.d.ts +6 -0
  24. package/dist/display/ChoiceButtonsRenderer.js +23 -0
  25. package/dist/display/CodeBlockRenderer.d.ts +2 -0
  26. package/dist/display/CodeBlockRenderer.js +17 -0
  27. package/dist/display/ComparisonTableRenderer.d.ts +2 -0
  28. package/dist/display/ComparisonTableRenderer.js +26 -0
  29. package/dist/display/DataTableRenderer.d.ts +2 -0
  30. package/dist/display/DataTableRenderer.js +74 -0
  31. package/dist/display/DisplayReactRenderer.d.ts +26 -0
  32. package/dist/display/DisplayReactRenderer.js +192 -0
  33. package/dist/display/FileCardRenderer.d.ts +2 -0
  34. package/dist/display/FileCardRenderer.js +31 -0
  35. package/dist/display/GalleryRenderer.d.ts +2 -0
  36. package/dist/display/GalleryRenderer.js +11 -0
  37. package/dist/display/ImageViewerRenderer.d.ts +2 -0
  38. package/dist/display/ImageViewerRenderer.js +15 -0
  39. package/dist/display/LinkPreviewRenderer.d.ts +2 -0
  40. package/dist/display/LinkPreviewRenderer.js +20 -0
  41. package/dist/display/MapViewRenderer.d.ts +2 -0
  42. package/dist/display/MapViewRenderer.js +20 -0
  43. package/dist/display/MetricCardRenderer.d.ts +2 -0
  44. package/dist/display/MetricCardRenderer.js +12 -0
  45. package/dist/display/PriceHighlightRenderer.d.ts +2 -0
  46. package/dist/display/PriceHighlightRenderer.js +30 -0
  47. package/dist/display/ProductCardRenderer.d.ts +2 -0
  48. package/dist/display/ProductCardRenderer.js +23 -0
  49. package/dist/display/ProgressStepsRenderer.d.ts +2 -0
  50. package/dist/display/ProgressStepsRenderer.js +14 -0
  51. package/dist/display/SourcesListRenderer.d.ts +2 -0
  52. package/dist/display/SourcesListRenderer.js +5 -0
  53. package/dist/display/SpreadsheetRenderer.d.ts +2 -0
  54. package/dist/display/SpreadsheetRenderer.js +32 -0
  55. package/dist/display/StepTimelineRenderer.d.ts +2 -0
  56. package/dist/display/StepTimelineRenderer.js +21 -0
  57. package/dist/display/index.d.ts +21 -0
  58. package/dist/display/index.js +20 -0
  59. package/dist/display/react-sandbox/bootstrap.d.ts +1 -0
  60. package/dist/display/react-sandbox/bootstrap.js +154 -0
  61. package/dist/display/registry.d.ts +5 -0
  62. package/dist/display/registry.js +52 -0
  63. package/dist/display/sdk-types.d.ts +187 -0
  64. package/dist/display/sdk-types.js +4 -0
  65. package/dist/hooks/ChatProvider.d.ts +9 -0
  66. package/dist/hooks/ChatProvider.js +14 -0
  67. package/dist/hooks/useIsMobile.d.ts +1 -0
  68. package/dist/hooks/useIsMobile.js +12 -0
  69. package/dist/hooks/useOpenClaudeChat.d.ts +36 -0
  70. package/dist/hooks/useOpenClaudeChat.js +361 -0
  71. package/dist/index.d.ts +47 -0
  72. package/dist/index.js +42 -0
  73. package/dist/lib/utils.d.ts +2 -0
  74. package/dist/lib/utils.js +5 -0
  75. package/dist/parts/PartErrorBoundary.d.ts +21 -0
  76. package/dist/parts/PartErrorBoundary.js +27 -0
  77. package/dist/parts/PartRenderer.d.ts +8 -0
  78. package/dist/parts/PartRenderer.js +99 -0
  79. package/dist/parts/ReasoningBlock.d.ts +6 -0
  80. package/dist/parts/ReasoningBlock.js +18 -0
  81. package/dist/parts/ToolActivity.d.ts +11 -0
  82. package/dist/parts/ToolActivity.js +52 -0
  83. package/dist/parts/ToolResult.d.ts +7 -0
  84. package/dist/parts/ToolResult.js +38 -0
  85. package/dist/styles.css +2 -0
  86. package/dist/types.d.ts +40 -0
  87. package/dist/types.js +4 -0
  88. package/dist/ui/alert.d.ts +12 -0
  89. package/dist/ui/alert.js +28 -0
  90. package/dist/ui/badge.d.ts +9 -0
  91. package/dist/ui/badge.js +20 -0
  92. package/dist/ui/button.d.ts +11 -0
  93. package/dist/ui/button.js +31 -0
  94. package/dist/ui/card.d.ts +8 -0
  95. package/dist/ui/card.js +21 -0
  96. package/dist/ui/collapsible.d.ts +1 -0
  97. package/dist/ui/collapsible.js +2 -0
  98. package/dist/ui/dialog.d.ts +19 -0
  99. package/dist/ui/dialog.js +23 -0
  100. package/dist/ui/dropdown-menu.d.ts +11 -0
  101. package/dist/ui/dropdown-menu.js +15 -0
  102. package/dist/ui/input.d.ts +3 -0
  103. package/dist/ui/input.js +6 -0
  104. package/dist/ui/progress.d.ts +7 -0
  105. package/dist/ui/progress.js +9 -0
  106. package/dist/ui/scroll-area.d.ts +5 -0
  107. package/dist/ui/scroll-area.js +12 -0
  108. package/dist/ui/separator.d.ts +4 -0
  109. package/dist/ui/separator.js +8 -0
  110. package/dist/ui/skeleton.d.ts +3 -0
  111. package/dist/ui/skeleton.js +6 -0
  112. package/dist/ui/table.d.ts +10 -0
  113. package/dist/ui/table.js +27 -0
  114. package/package.json +61 -0
  115. package/src/components/Chat.tsx +107 -0
  116. package/src/components/ErrorNote.tsx +35 -0
  117. package/src/components/LazyRender.tsx +42 -0
  118. package/src/components/Markdown.tsx +114 -0
  119. package/src/components/MessageBubble.tsx +107 -0
  120. package/src/components/MessageInput.tsx +421 -0
  121. package/src/components/MessageList.tsx +153 -0
  122. package/src/components/StreamingIndicator.tsx +19 -0
  123. package/src/display/AlertRenderer.tsx +23 -0
  124. package/src/display/CarouselRenderer.tsx +141 -0
  125. package/src/display/ChartRenderer.tsx +195 -0
  126. package/src/display/ChoiceButtonsRenderer.tsx +114 -0
  127. package/src/display/CodeBlockRenderer.tsx +49 -0
  128. package/src/display/ComparisonTableRenderer.tsx +132 -0
  129. package/src/display/DataTableRenderer.tsx +144 -0
  130. package/src/display/DisplayReactRenderer.tsx +269 -0
  131. package/src/display/FileCardRenderer.tsx +55 -0
  132. package/src/display/GalleryRenderer.tsx +65 -0
  133. package/src/display/ImageViewerRenderer.tsx +114 -0
  134. package/src/display/LinkPreviewRenderer.tsx +74 -0
  135. package/src/display/MapViewRenderer.tsx +75 -0
  136. package/src/display/MetricCardRenderer.tsx +29 -0
  137. package/src/display/PriceHighlightRenderer.tsx +62 -0
  138. package/src/display/ProductCardRenderer.tsx +112 -0
  139. package/src/display/ProgressStepsRenderer.tsx +59 -0
  140. package/src/display/SourcesListRenderer.tsx +47 -0
  141. package/src/display/SpreadsheetRenderer.tsx +86 -0
  142. package/src/display/StepTimelineRenderer.tsx +75 -0
  143. package/src/display/index.ts +21 -0
  144. package/src/display/react-sandbox/bootstrap.ts +155 -0
  145. package/src/display/registry.ts +84 -0
  146. package/src/display/sdk-types.ts +217 -0
  147. package/src/hooks/ChatProvider.tsx +21 -0
  148. package/src/hooks/useIsMobile.ts +15 -0
  149. package/src/hooks/useOpenClaudeChat.ts +476 -0
  150. package/src/index.ts +76 -0
  151. package/src/lib/utils.ts +6 -0
  152. package/src/parts/PartErrorBoundary.tsx +51 -0
  153. package/src/parts/PartRenderer.tsx +145 -0
  154. package/src/parts/ReasoningBlock.tsx +41 -0
  155. package/src/parts/ToolActivity.tsx +78 -0
  156. package/src/parts/ToolResult.tsx +79 -0
  157. package/src/styles.css +2 -0
  158. package/src/types.ts +41 -0
  159. package/src/ui/alert.tsx +77 -0
  160. package/src/ui/badge.tsx +36 -0
  161. package/src/ui/button.tsx +54 -0
  162. package/src/ui/card.tsx +68 -0
  163. package/src/ui/collapsible.tsx +7 -0
  164. package/src/ui/dialog.tsx +122 -0
  165. package/src/ui/dropdown-menu.tsx +76 -0
  166. package/src/ui/input.tsx +24 -0
  167. package/src/ui/progress.tsx +36 -0
  168. package/src/ui/scroll-area.tsx +48 -0
  169. package/src/ui/separator.tsx +31 -0
  170. package/src/ui/skeleton.tsx +9 -0
  171. package/src/ui/table.tsx +114 -0
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import type { DisplayRendererMap } from "../display/registry.js";
3
+ import type { Message } from "../types.js";
4
+ export interface ChatProps {
5
+ /** Base URL do servico openclaude (ex: http://localhost:9500/api/v1/ai). */
6
+ endpoint: string;
7
+ token?: string;
8
+ /** Sessao live existente. Se omitida, o hook cria uma via POST /sessions. */
9
+ sessionId?: string;
10
+ initialMessages?: Message[];
11
+ sessionOptions?: Record<string, unknown>;
12
+ turnOptions?: Record<string, unknown>;
13
+ displayRenderers?: DisplayRendererMap;
14
+ placeholder?: string;
15
+ header?: React.ReactNode;
16
+ footer?: React.ReactNode;
17
+ className?: string;
18
+ enableAttachments?: boolean;
19
+ enableVoice?: boolean;
20
+ fetcher?: typeof fetch;
21
+ emptyState?: React.ReactNode;
22
+ }
23
+ export declare function Chat({ endpoint, token, sessionId, initialMessages, sessionOptions, turnOptions, displayRenderers, placeholder, header, footer, className, enableAttachments, enableVoice, fetcher, emptyState, }: ChatProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../lib/utils.js";
3
+ import { ChatProvider, useChatContext } from "../hooks/ChatProvider.js";
4
+ import { MessageList } from "./MessageList.js";
5
+ import { MessageInput } from "./MessageInput.js";
6
+ function ChatContent({ displayRenderers, placeholder, enableAttachments = true, enableVoice = true, emptyState }) {
7
+ const { messages, input, setInput, handleSubmit, isLoading, stop, error, reload } = useChatContext();
8
+ return (_jsxs(_Fragment, { children: [_jsx(MessageList, { messages: messages, isLoading: isLoading, displayRenderers: displayRenderers, error: error ?? undefined, onRetry: reload, emptyState: emptyState }), _jsx("div", { className: "px-4 pb-4", children: _jsx(MessageInput, { input: input, setInput: setInput, handleSubmit: handleSubmit, isLoading: isLoading, stop: stop, placeholder: placeholder, enableAttachments: enableAttachments, enableVoice: enableVoice }) })] }));
9
+ }
10
+ export function Chat({ endpoint, token, sessionId, initialMessages, sessionOptions, turnOptions, displayRenderers, placeholder, header, footer, className, enableAttachments, enableVoice, fetcher, emptyState, }) {
11
+ return (_jsx(ChatProvider, { endpoint: endpoint, token: token, sessionId: sessionId, initialMessages: initialMessages, sessionOptions: sessionOptions, turnOptions: turnOptions, fetcher: fetcher, children: _jsxs("div", { className: cn("flex flex-col h-full bg-background text-foreground", className), children: [header, _jsx(ChatContent, { displayRenderers: displayRenderers, placeholder: placeholder, enableAttachments: enableAttachments, enableVoice: enableVoice, emptyState: emptyState }), footer] }) }, sessionId));
12
+ }
@@ -0,0 +1,6 @@
1
+ export interface ErrorNoteProps {
2
+ message?: string;
3
+ onRetry?: () => void;
4
+ className?: string;
5
+ }
6
+ export declare function ErrorNote({ message, onRetry, className }: ErrorNoteProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { AlertTriangle, RotateCcw } from "lucide-react";
3
+ import { cn } from "../lib/utils.js";
4
+ export function ErrorNote({ message, onRetry, className }) {
5
+ return (_jsxs("div", { role: "alert", className: cn("flex items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2 text-sm text-destructive", className), children: [_jsx(AlertTriangle, { className: "size-4 shrink-0 mt-0.5" }), _jsx("span", { className: "flex-1 min-w-0 break-words", children: message ?? "Falha ao processar mensagem" }), onRetry && (_jsxs("button", { type: "button", onClick: onRetry, className: "shrink-0 inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium text-destructive hover:bg-destructive/10 transition-colors", children: [_jsx(RotateCcw, { className: "size-3.5" }), "Tentar novamente"] }))] }));
6
+ }
@@ -0,0 +1,8 @@
1
+ import { type ReactNode } from "react";
2
+ interface LazyRenderProps {
3
+ children: ReactNode;
4
+ minHeight?: number;
5
+ rootMargin?: string;
6
+ }
7
+ export declare function LazyRender({ children, minHeight, rootMargin }: LazyRenderProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
@@ -0,0 +1,22 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useRef, useState, useEffect } from "react";
3
+ export function LazyRender({ children, minHeight = 120, rootMargin = "200px" }) {
4
+ const ref = useRef(null);
5
+ const [visible, setVisible] = useState(false);
6
+ useEffect(() => {
7
+ const el = ref.current;
8
+ if (!el)
9
+ return;
10
+ const observer = new IntersectionObserver(([entry]) => {
11
+ if (entry.isIntersecting) {
12
+ setVisible(true);
13
+ observer.disconnect();
14
+ }
15
+ }, { rootMargin });
16
+ observer.observe(el);
17
+ return () => observer.disconnect();
18
+ }, [rootMargin]);
19
+ if (visible)
20
+ return _jsx(_Fragment, { children: children });
21
+ return (_jsx("div", { ref: ref, className: "flex items-center justify-center text-muted-foreground text-xs rounded-md bg-muted/20 animate-pulse", style: { minHeight }, children: "Carregando..." }));
22
+ }
@@ -0,0 +1,5 @@
1
+ interface MarkdownProps {
2
+ children: string;
3
+ }
4
+ export declare const Markdown: import("react").NamedExoticComponent<MarkdownProps>;
5
+ export {};
@@ -0,0 +1,65 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { memo } from "react";
3
+ import ReactMarkdown from "react-markdown";
4
+ import remarkGfm from "remark-gfm";
5
+ import rehypeHighlight from "rehype-highlight";
6
+ import { cn } from "../lib/utils.js";
7
+ const REMARK_PLUGINS = [remarkGfm];
8
+ const REHYPE_PLUGINS = [rehypeHighlight];
9
+ const components = {
10
+ h1({ children }) {
11
+ return _jsx("h1", { className: "text-xl font-semibold", style: { marginTop: "20px", marginBottom: "8px" }, children: children });
12
+ },
13
+ h2({ children }) {
14
+ return _jsx("h2", { className: "text-lg font-semibold", style: { marginTop: "20px", marginBottom: "8px" }, children: children });
15
+ },
16
+ h3({ children }) {
17
+ return _jsx("h3", { className: "text-base font-semibold", style: { marginTop: "20px", marginBottom: "8px" }, children: children });
18
+ },
19
+ h4({ children }) {
20
+ return _jsx("h4", { className: "font-semibold", style: { marginTop: "20px", marginBottom: "8px" }, children: children });
21
+ },
22
+ p({ children }) {
23
+ return _jsx("p", { className: "mb-4 last:mb-0", children: children });
24
+ },
25
+ ul({ children }) {
26
+ return _jsx("ul", { style: { paddingLeft: "24px", marginTop: "8px", marginBottom: "8px", listStyleType: "disc" }, children: children });
27
+ },
28
+ ol({ children }) {
29
+ return _jsx("ol", { style: { paddingLeft: "24px", marginTop: "8px", marginBottom: "8px", listStyleType: "decimal" }, children: children });
30
+ },
31
+ li({ children }) {
32
+ return _jsx("li", { style: { marginTop: "4px", marginBottom: "4px" }, children: children });
33
+ },
34
+ hr() {
35
+ return _jsx("hr", { className: "border-border", style: { marginTop: "16px", marginBottom: "16px" } });
36
+ },
37
+ pre({ children }) {
38
+ return (_jsx("pre", { className: "bg-muted border border-border rounded-md overflow-hidden", style: { marginTop: "12px", marginBottom: "12px" }, children: children }));
39
+ },
40
+ code({ className, children }) {
41
+ const isBlock = className?.startsWith("language-");
42
+ if (isBlock) {
43
+ return (_jsx("code", { className: cn("block p-4 overflow-x-auto font-mono text-sm", className), children: children }));
44
+ }
45
+ return (_jsx("code", { className: "bg-muted border border-border rounded-sm px-1.5 py-0.5 font-mono text-sm", children: children }));
46
+ },
47
+ a({ href, children }) {
48
+ return (_jsx("a", { href: href, target: "_blank", rel: "noopener noreferrer", className: "text-primary underline underline-offset-2 hover:opacity-80", children: children }));
49
+ },
50
+ blockquote({ children }) {
51
+ return (_jsx("blockquote", { className: "border-l-[3px] border-border py-1 px-3 text-muted-foreground", style: { marginTop: "12px", marginBottom: "12px" }, children: children }));
52
+ },
53
+ table({ children }) {
54
+ return (_jsx("div", { className: "overflow-x-auto", children: _jsx("table", { className: "w-full border-collapse text-sm", children: children }) }));
55
+ },
56
+ th({ children }) {
57
+ return (_jsx("th", { className: "border border-border px-3 py-1.5 text-left font-semibold bg-muted", children: children }));
58
+ },
59
+ td({ children }) {
60
+ return (_jsx("td", { className: "border border-border px-3 py-1.5 text-left", children: children }));
61
+ },
62
+ };
63
+ export const Markdown = memo(function Markdown({ children }) {
64
+ return (_jsx("div", { className: "text-foreground text-sm", style: { lineHeight: "1.625em" }, children: _jsx(ReactMarkdown, { remarkPlugins: REMARK_PLUGINS, rehypePlugins: REHYPE_PLUGINS, components: components, children: children }) }));
65
+ });
@@ -0,0 +1,9 @@
1
+ import type { Message } from "../types.js";
2
+ import type { DisplayRendererMap } from "../display/registry.js";
3
+ export interface MessageBubbleProps {
4
+ message: Message;
5
+ isStreaming?: boolean;
6
+ displayRenderers?: DisplayRendererMap;
7
+ className?: string;
8
+ }
9
+ export declare const MessageBubble: import("react").NamedExoticComponent<MessageBubbleProps>;
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { memo, useState, useCallback } from "react";
3
+ import { cn } from "../lib/utils.js";
4
+ import { Markdown } from "./Markdown.js";
5
+ import { StreamingIndicator } from "./StreamingIndicator.js";
6
+ import { PartRenderer } from "../parts/PartRenderer.js";
7
+ import { PartErrorBoundary } from "../parts/PartErrorBoundary.js";
8
+ import { Copy, Check } from "lucide-react";
9
+ function extractText(message) {
10
+ if (message.content)
11
+ return message.content;
12
+ if (!Array.isArray(message.parts))
13
+ return "";
14
+ return message.parts
15
+ .filter((p) => p.type === "text")
16
+ .map((p) => p.text)
17
+ .join("\n");
18
+ }
19
+ function CopyButton({ text }) {
20
+ const [copied, setCopied] = useState(false);
21
+ const handleCopy = useCallback(async () => {
22
+ await navigator.clipboard.writeText(text);
23
+ setCopied(true);
24
+ setTimeout(() => setCopied(false), 2000);
25
+ }, [text]);
26
+ return (_jsx("button", { type: "button", onClick: handleCopy, className: cn("h-7 w-7 flex items-center justify-center rounded-lg transition-all", "text-muted-foreground/50 opacity-0 group-hover/bubble:opacity-100", "hover:bg-muted/50 hover:text-muted-foreground"), "aria-label": copied ? "Copiado" : "Copiar mensagem", children: copied ? _jsx(Check, { className: "h-3.5 w-3.5" }) : _jsx(Copy, { className: "h-3.5 w-3.5" }) }));
27
+ }
28
+ export const MessageBubble = memo(function MessageBubble({ message, isStreaming, displayRenderers, className }) {
29
+ const isUser = message.role === "user";
30
+ const hasParts = Array.isArray(message.parts) && message.parts.length > 0;
31
+ const hasText = typeof message.content === "string" && message.content.length > 0;
32
+ const isEmptyAssistant = !isUser && !hasParts && !hasText && !isStreaming;
33
+ return (_jsxs("div", { className: cn("group/bubble", isUser ? "flex flex-col items-end" : "flex flex-col items-start"), children: [_jsxs("div", { className: cn("min-w-0 overflow-hidden", isUser
34
+ ? "max-w-[80%] rounded-lg rounded-br-sm bg-muted text-foreground px-4 py-2.5"
35
+ : "w-full text-foreground py-1", className), children: [hasParts
36
+ ? _jsx("div", { className: "flex flex-col gap-3", children: message.parts.map((part, i) => (_jsx(PartErrorBoundary, { label: `part:${part.type}`, children: _jsx(PartRenderer, { part: part, isStreaming: isStreaming, displayRenderers: displayRenderers }) }, i))) })
37
+ : hasText
38
+ ? _jsx(Markdown, { children: message.content })
39
+ : isEmptyAssistant
40
+ ? _jsx("span", { className: "text-xs italic text-muted-foreground", children: "(resposta vazia)" })
41
+ : null, isStreaming && !isUser && _jsx(StreamingIndicator, {})] }), !isStreaming && (_jsx("div", { className: cn("flex items-center gap-0.5 mt-0.5", isUser ? "justify-end" : "justify-start"), children: _jsx(CopyButton, { text: extractText(message) }) }))] }));
42
+ }, (prev, next) => prev.message === next.message
43
+ && prev.isStreaming === next.isStreaming
44
+ && prev.displayRenderers === next.displayRenderers
45
+ && prev.className === next.className);
@@ -0,0 +1,19 @@
1
+ export interface Attachment {
2
+ id: string;
3
+ file: File;
4
+ preview?: string;
5
+ type: "image" | "file" | "audio";
6
+ }
7
+ export interface MessageInputProps {
8
+ input: string;
9
+ setInput: (value: string) => void;
10
+ handleSubmit: (e: React.FormEvent, attachments?: Attachment[]) => void;
11
+ isLoading?: boolean;
12
+ isUploading?: boolean;
13
+ stop?: () => void;
14
+ placeholder?: string;
15
+ className?: string;
16
+ enableAttachments?: boolean;
17
+ enableVoice?: boolean;
18
+ }
19
+ export declare function MessageInput({ input, setInput, handleSubmit, isLoading, isUploading, stop, placeholder, className, enableAttachments, enableVoice, }: MessageInputProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,214 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef, useEffect, useState, useCallback } from "react";
3
+ import { Send, Square, Plus, Mic, X, Camera, Paperclip, Image as ImageIcon, CircleStop, Loader2 } from "lucide-react";
4
+ import { Button } from "../ui/button.js";
5
+ import { cn } from "../lib/utils.js";
6
+ // ── Constants ──
7
+ const LINE_HEIGHT_PX = 24;
8
+ const MAX_ROWS = 10;
9
+ const MULTILINE_THRESHOLD_PX = LINE_HEIGHT_PX * 1.5; // 36px — above this = multiline
10
+ // ── Recording Hook ──
11
+ function useAudioRecording(onComplete) {
12
+ const [isRecording, setIsRecording] = useState(false);
13
+ const [elapsed, setElapsed] = useState(0);
14
+ const mediaRecorderRef = useRef(null);
15
+ const chunksRef = useRef([]);
16
+ const timerRef = useRef(undefined);
17
+ const start = useCallback(async () => {
18
+ try {
19
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
20
+ const recorder = new MediaRecorder(stream);
21
+ mediaRecorderRef.current = recorder;
22
+ chunksRef.current = [];
23
+ recorder.ondataavailable = (e) => { if (e.data.size > 0)
24
+ chunksRef.current.push(e.data); };
25
+ recorder.onstop = () => {
26
+ const blob = new Blob(chunksRef.current, { type: "audio/webm" });
27
+ const file = new File([blob], `audio-${Date.now()}.webm`, { type: "audio/webm" });
28
+ stream.getTracks().forEach((t) => t.stop());
29
+ onComplete(file);
30
+ };
31
+ recorder.start();
32
+ setIsRecording(true);
33
+ setElapsed(0);
34
+ timerRef.current = setInterval(() => setElapsed((s) => s + 1), 1000);
35
+ }
36
+ catch { /* permission denied */ }
37
+ }, [onComplete]);
38
+ const stop = useCallback(() => {
39
+ mediaRecorderRef.current?.stop();
40
+ setIsRecording(false);
41
+ clearInterval(timerRef.current);
42
+ }, []);
43
+ const cancel = useCallback(() => {
44
+ if (mediaRecorderRef.current) {
45
+ mediaRecorderRef.current.onstop = null;
46
+ mediaRecorderRef.current.stop();
47
+ mediaRecorderRef.current.stream.getTracks().forEach((t) => t.stop());
48
+ }
49
+ setIsRecording(false);
50
+ setElapsed(0);
51
+ clearInterval(timerRef.current);
52
+ }, []);
53
+ useEffect(() => () => clearInterval(timerRef.current), []);
54
+ return { isRecording, elapsed, start, stop, cancel };
55
+ }
56
+ function formatTime(seconds) {
57
+ const m = Math.floor(seconds / 60);
58
+ const s = seconds % 60;
59
+ return `${m}:${s.toString().padStart(2, "0")}`;
60
+ }
61
+ // ── Attachment Preview ──
62
+ function AttachmentPreview({ attachment, onRemove }) {
63
+ return (_jsxs("div", { className: "relative group shrink-0", children: [attachment.type === "image" && attachment.preview ? (_jsx("div", { className: "relative h-16 w-16 rounded-lg overflow-hidden border border-border/50", children: _jsx("img", { src: attachment.preview, alt: attachment.file.name, className: "h-full w-full object-cover" }) })) : (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-border/50 bg-background/50 px-3 py-2", children: [_jsx(Paperclip, { className: "h-4 w-4 text-muted-foreground shrink-0" }), _jsx("span", { className: "text-xs text-muted-foreground truncate max-w-[120px]", children: attachment.file.name })] })), _jsx("button", { type: "button", onClick: onRemove, className: "absolute -top-1.5 -right-1.5 h-5 w-5 rounded-full bg-foreground text-background flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity", "aria-label": "Remover", children: _jsx(X, { className: "h-3 w-3" }) })] }));
64
+ }
65
+ // ── Plus Menu ──
66
+ function PlusMenu({ onFile, onCamera, onGallery, onClose }) {
67
+ const menuRef = useRef(null);
68
+ useEffect(() => {
69
+ function handleClick(e) {
70
+ if (menuRef.current && !menuRef.current.contains(e.target))
71
+ onClose();
72
+ }
73
+ document.addEventListener("mousedown", handleClick);
74
+ return () => document.removeEventListener("mousedown", handleClick);
75
+ }, [onClose]);
76
+ return (_jsx("div", { ref: menuRef, className: "absolute bottom-full left-0 mb-2 rounded-xl border border-border bg-popover text-popover-foreground shadow-lg py-1 min-w-[160px] z-10", children: [
77
+ { icon: Paperclip, label: "Arquivo", onClick: onFile },
78
+ { icon: Camera, label: "Camera", onClick: onCamera },
79
+ { icon: ImageIcon, label: "Galeria", onClick: onGallery },
80
+ ].map((item) => (_jsxs("button", { type: "button", onClick: () => { item.onClick(); onClose(); }, className: "flex w-full items-center gap-3 px-4 py-2.5 text-sm hover:bg-muted/50 transition-colors", children: [_jsx(item.icon, { className: "h-4 w-4 text-muted-foreground" }), item.label] }, item.label))) }));
81
+ }
82
+ // ── Main Component ──
83
+ export function MessageInput({ input, setInput, handleSubmit, isLoading, isUploading = false, stop, placeholder = "Caixa de mensagem...", className, enableAttachments = true, enableVoice = true, }) {
84
+ const textareaRef = useRef(null);
85
+ const fileInputRef = useRef(null);
86
+ const imageInputRef = useRef(null);
87
+ const containerRef = useRef(null);
88
+ const [attachments, setAttachments] = useState([]);
89
+ const [showMenu, setShowMenu] = useState(false);
90
+ const [isDragging, setIsDragging] = useState(false);
91
+ const [isMultiline, setIsMultiline] = useState(false);
92
+ const historyRef = useRef([]);
93
+ const historyPosRef = useRef(-1);
94
+ const savedInputRef = useRef("");
95
+ const onRecordingComplete = useCallback((file) => {
96
+ const att = { id: crypto.randomUUID(), file, type: "audio" };
97
+ setAttachments((prev) => [...prev, att]);
98
+ }, []);
99
+ const { isRecording, elapsed, start: startRecording, stop: stopRecording, cancel: cancelRecording } = useAudioRecording(onRecordingComplete);
100
+ const hasContent = input.trim().length > 0 || attachments.length > 0;
101
+ // Auto-focus
102
+ useEffect(() => { textareaRef.current?.focus(); }, []);
103
+ // Auto-expand — stable measurement with height:auto
104
+ useEffect(() => {
105
+ const el = textareaRef.current;
106
+ if (!el)
107
+ return;
108
+ el.style.height = "auto";
109
+ const scrollH = el.scrollHeight;
110
+ const maxH = MAX_ROWS * LINE_HEIGHT_PX;
111
+ const clampedH = Math.min(scrollH, maxH);
112
+ el.style.height = `${clampedH}px`;
113
+ setIsMultiline(scrollH > MULTILINE_THRESHOLD_PX);
114
+ }, [input]);
115
+ // ── Attachments ──
116
+ const addFiles = useCallback((files) => {
117
+ const newAttachments = Array.from(files).map((file) => {
118
+ const isImage = file.type.startsWith("image/");
119
+ const att = { id: crypto.randomUUID(), file, type: isImage ? "image" : "file" };
120
+ if (isImage) {
121
+ const reader = new FileReader();
122
+ reader.onload = (e) => {
123
+ setAttachments((prev) => prev.map((a) => (a.id === att.id ? { ...a, preview: e.target?.result } : a)));
124
+ };
125
+ reader.readAsDataURL(file);
126
+ }
127
+ return att;
128
+ });
129
+ setAttachments((prev) => [...prev, ...newAttachments]);
130
+ }, []);
131
+ const removeAttachment = useCallback((id) => {
132
+ setAttachments((prev) => prev.filter((a) => a.id !== id));
133
+ }, []);
134
+ // ── Drag & Drop ──
135
+ const handleDragOver = useCallback((e) => { e.preventDefault(); setIsDragging(true); }, []);
136
+ const handleDragLeave = useCallback((e) => {
137
+ if (containerRef.current && !containerRef.current.contains(e.relatedTarget))
138
+ setIsDragging(false);
139
+ }, []);
140
+ const handleDrop = useCallback((e) => {
141
+ e.preventDefault();
142
+ setIsDragging(false);
143
+ if (e.dataTransfer.files.length)
144
+ addFiles(e.dataTransfer.files);
145
+ }, [addFiles]);
146
+ // ── Clipboard Paste ──
147
+ const handlePaste = useCallback((e) => {
148
+ const files = [];
149
+ for (let i = 0; i < e.clipboardData.items.length; i++) {
150
+ if (e.clipboardData.items[i].kind === "file") {
151
+ const file = e.clipboardData.items[i].getAsFile();
152
+ if (file)
153
+ files.push(file);
154
+ }
155
+ }
156
+ if (files.length) {
157
+ e.preventDefault();
158
+ addFiles(files);
159
+ }
160
+ }, [addFiles]);
161
+ // ── Submit ──
162
+ function onSubmit(e) {
163
+ e.preventDefault();
164
+ if (!hasContent || isLoading)
165
+ return;
166
+ if (input.trim()) {
167
+ historyRef.current.unshift(input);
168
+ if (historyRef.current.length > 50)
169
+ historyRef.current.length = 50;
170
+ }
171
+ historyPosRef.current = -1;
172
+ savedInputRef.current = "";
173
+ handleSubmit(e, attachments.length > 0 ? attachments : undefined);
174
+ setAttachments([]);
175
+ }
176
+ function handleKeyDown(e) {
177
+ // History navigation — only when cursor is at start/end and not multiline content
178
+ const el = textareaRef.current;
179
+ if (e.key === "ArrowUp" && historyRef.current.length > 0 && el) {
180
+ const atTop = el.selectionStart === 0 && el.selectionEnd === 0;
181
+ const isEmpty = input === "";
182
+ if (atTop || isEmpty) {
183
+ e.preventDefault();
184
+ if (historyPosRef.current === -1)
185
+ savedInputRef.current = input;
186
+ const nextPos = Math.min(historyPosRef.current + 1, historyRef.current.length - 1);
187
+ if (nextPos !== historyPosRef.current) {
188
+ historyPosRef.current = nextPos;
189
+ setInput(historyRef.current[nextPos]);
190
+ }
191
+ return;
192
+ }
193
+ }
194
+ if (e.key === "ArrowDown" && historyPosRef.current >= 0 && el) {
195
+ const atBottom = el.selectionStart === input.length;
196
+ if (atBottom) {
197
+ e.preventDefault();
198
+ const nextPos = historyPosRef.current - 1;
199
+ historyPosRef.current = nextPos;
200
+ setInput(nextPos < 0 ? savedInputRef.current : historyRef.current[nextPos]);
201
+ return;
202
+ }
203
+ }
204
+ if (e.key === "Enter" && !e.shiftKey) {
205
+ e.preventDefault();
206
+ if (hasContent && !isLoading)
207
+ onSubmit(e);
208
+ }
209
+ }
210
+ // ── Render ──
211
+ return (_jsxs("div", { ref: containerRef, onDragOver: enableAttachments ? handleDragOver : undefined, onDragLeave: enableAttachments ? handleDragLeave : undefined, onDrop: enableAttachments ? handleDrop : undefined, className: cn("relative border border-border/50 bg-muted transition-[border-radius] duration-200", isMultiline || attachments.length > 0 ? "rounded-2xl" : "rounded-full", isDragging && "ring-2 ring-primary/50", className), children: [isDragging && (_jsx("div", { className: "absolute inset-0 z-10 flex items-center justify-center rounded-[inherit] bg-primary/5 border-2 border-dashed border-primary/30", children: _jsx("span", { className: "text-sm text-primary font-medium", children: "Solte aqui" }) })), isRecording && (_jsxs("div", { className: "flex items-center gap-3 px-3 py-2", children: [_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: cancelRecording, className: "h-8 w-8 rounded-full shrink-0 text-muted-foreground", "aria-label": "Cancelar", children: _jsx(X, { className: "h-4 w-4" }) }), _jsxs("div", { className: "flex items-center gap-2 flex-1", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-destructive animate-pulse" }), _jsx("span", { className: "text-sm font-medium tabular-nums", children: formatTime(elapsed) }), _jsx("div", { className: "flex-1 flex items-center gap-0.5 px-2", children: Array.from({ length: 20 }, (_, i) => (_jsx("span", { className: "w-1 bg-foreground/30 rounded-full", style: { height: `${4 + Math.random() * 12}px` } }, i))) })] }), _jsx(Button, { type: "button", size: "icon", onClick: stopRecording, className: "h-8 w-8 rounded-full shrink-0", "aria-label": "Parar", children: _jsx(CircleStop, { className: "h-4 w-4" }) })] })), _jsxs("div", { className: cn(isRecording && "hidden"), children: [(attachments.length > 0 || isUploading) && (_jsxs("div", { className: "flex flex-wrap gap-2 px-4 pt-3 pb-1", children: [attachments.map((att) => (_jsx(AttachmentPreview, { attachment: att, onRemove: () => removeAttachment(att.id) }, att.id))), isUploading && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-border/50 bg-background/50 px-3 py-2 text-xs text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin shrink-0" }), _jsx("span", { children: "Enviando arquivos..." })] }))] })), _jsxs("div", { className: cn("flex gap-1 p-1.5", isMultiline ? "items-end" : "items-center"), children: [enableAttachments && (_jsxs("div", { className: "relative shrink-0", children: [_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "h-8 w-8 rounded-full", onClick: () => setShowMenu(!showMenu), "aria-label": "Adicionar", children: _jsx(Plus, { className: "h-4 w-4" }) }), showMenu && (_jsx(PlusMenu, { onFile: () => fileInputRef.current?.click(), onCamera: () => imageInputRef.current?.click(), onGallery: () => imageInputRef.current?.click(), onClose: () => setShowMenu(false) }))] })), _jsx("textarea", { ref: textareaRef, value: input, onChange: (e) => setInput(e.target.value), onKeyDown: handleKeyDown, onPaste: enableAttachments ? handlePaste : undefined, placeholder: placeholder, rows: 1, disabled: isLoading, "aria-label": "Mensagem", className: "flex-1 min-w-0 bg-transparent text-foreground text-sm resize-none outline-none placeholder:text-muted-foreground leading-6 py-1 px-2" }), _jsxs("div", { className: "flex items-center gap-0.5 shrink-0", children: [enableVoice && !hasContent && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "h-8 w-8 rounded-full text-muted-foreground", onClick: startRecording, "aria-label": "Gravar audio", children: _jsx(Mic, { className: "h-4 w-4" }) })), isLoading && stop ? (_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: stop, className: "h-8 w-8 rounded-full", "aria-label": "Parar gera\u00E7\u00E3o", children: _jsx(Square, { className: "h-4 w-4" }) })) : (_jsx(Button, { type: "button", size: "icon", onClick: onSubmit, disabled: !hasContent || !!isLoading, className: "h-8 w-8 rounded-full", "aria-label": "Enviar mensagem", children: _jsx(Send, { className: "h-4 w-4" }) }))] })] })] }), _jsx("input", { ref: fileInputRef, type: "file", multiple: true, className: "hidden", onChange: (e) => { if (e.target.files)
212
+ addFiles(e.target.files); e.target.value = ""; } }), _jsx("input", { ref: imageInputRef, type: "file", accept: "image/*", multiple: true, className: "hidden", onChange: (e) => { if (e.target.files)
213
+ addFiles(e.target.files); e.target.value = ""; } })] }));
214
+ }
@@ -0,0 +1,13 @@
1
+ import { type ReactNode } from "react";
2
+ import type { Message } from "../types.js";
3
+ import type { DisplayRendererMap } from "../display/registry.js";
4
+ export interface MessageListProps {
5
+ messages: Message[];
6
+ isLoading?: boolean;
7
+ displayRenderers?: DisplayRendererMap;
8
+ className?: string;
9
+ error?: Error;
10
+ onRetry?: () => void;
11
+ emptyState?: ReactNode;
12
+ }
13
+ export declare function MessageList({ messages, isLoading, displayRenderers, className, error, onRetry, emptyState }: MessageListProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,72 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
3
+ import { useRef, useEffect, useMemo, useCallback } from "react";
4
+ import { useVirtualizer } from "@tanstack/react-virtual";
5
+ import { Sparkles } from "lucide-react";
6
+ import { MessageBubble } from "./MessageBubble.js";
7
+ import { StreamingIndicator } from "./StreamingIndicator.js";
8
+ import { ErrorNote } from "./ErrorNote.js";
9
+ import { ScrollBar } from "../ui/scroll-area.js";
10
+ import { cn } from "../lib/utils.js";
11
+ function DefaultWelcome() {
12
+ return (_jsxs("div", { className: "flex h-full flex-col items-center justify-center gap-4 px-6 py-16 text-center", children: [_jsx("div", { className: "flex size-14 items-center justify-center rounded-2xl bg-primary/10 text-primary ring-1 ring-primary/20", children: _jsx(Sparkles, { className: "size-7" }) }), _jsxs("div", { className: "space-y-1.5", children: [_jsx("h2", { className: "text-xl font-semibold tracking-tight", children: "Como posso ajudar?" }), _jsx("p", { className: "max-w-sm text-sm text-muted-foreground", children: "Envie uma mensagem para comecar a conversa. Voce pode pedir respostas, acionar ferramentas ou colar conteudo para analise." })] })] }));
13
+ }
14
+ export function MessageList({ messages, isLoading, displayRenderers, className, error, onRetry, emptyState }) {
15
+ const viewportRef = useRef(null);
16
+ const isFollowingRef = useRef(true);
17
+ const lastAssistantIndex = useMemo(() => messages.reduceRight((found, msg, i) => {
18
+ if (found !== -1)
19
+ return found;
20
+ return msg.role === "assistant" ? i : -1;
21
+ }, -1), [messages]);
22
+ const virtualizer = useVirtualizer({
23
+ count: messages.length,
24
+ getScrollElement: () => viewportRef.current,
25
+ estimateSize: () => 80,
26
+ overscan: 5,
27
+ paddingStart: 16,
28
+ });
29
+ // Track scroll position to detect if user is following
30
+ const handleScroll = useCallback(() => {
31
+ const viewport = viewportRef.current;
32
+ if (!viewport)
33
+ return;
34
+ const distanceFromBottom = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
35
+ isFollowingRef.current = distanceFromBottom <= 100;
36
+ }, []);
37
+ useEffect(() => {
38
+ const viewport = viewportRef.current;
39
+ if (!viewport)
40
+ return;
41
+ viewport.addEventListener("scroll", handleScroll, { passive: true });
42
+ return () => viewport.removeEventListener("scroll", handleScroll);
43
+ }, [handleScroll]);
44
+ // Auto-scroll to bottom when following
45
+ useEffect(() => {
46
+ if (messages.length > 0 && isFollowingRef.current) {
47
+ virtualizer.scrollToIndex(messages.length - 1, { align: "end" });
48
+ }
49
+ }, [messages, virtualizer]);
50
+ // Auto-scroll when error appears
51
+ useEffect(() => {
52
+ if (error && isFollowingRef.current) {
53
+ const viewport = viewportRef.current;
54
+ if (viewport)
55
+ viewport.scrollTop = viewport.scrollHeight;
56
+ }
57
+ }, [error]);
58
+ const virtualItems = virtualizer.getVirtualItems();
59
+ if (messages.length === 0) {
60
+ return (_jsxs(ScrollAreaPrimitive.Root, { className: cn("flex-1 relative overflow-hidden", className), children: [_jsx(ScrollAreaPrimitive.Viewport, { ref: viewportRef, className: "h-full w-full rounded-[inherit]", children: emptyState ?? _jsx(DefaultWelcome, {}) }), _jsx(ScrollBar, {}), _jsx(ScrollAreaPrimitive.Corner, {})] }));
61
+ }
62
+ return (_jsxs(ScrollAreaPrimitive.Root, { className: cn("flex-1 relative overflow-hidden", className), children: [_jsxs(ScrollAreaPrimitive.Viewport, { ref: viewportRef, className: "h-full w-full rounded-[inherit]", children: [_jsx("div", { className: "relative w-full", style: { height: virtualizer.getTotalSize() + 16 }, children: _jsx("div", { children: virtualItems.map((virtualRow) => {
63
+ const message = messages[virtualRow.index];
64
+ return (_jsx("div", { "data-index": virtualRow.index, ref: virtualizer.measureElement, className: "pb-3 px-4", style: {
65
+ position: "absolute",
66
+ top: 0,
67
+ left: 0,
68
+ right: 0,
69
+ transform: `translateY(${virtualRow.start}px)`,
70
+ }, children: _jsx(MessageBubble, { message: message, isStreaming: virtualRow.index === lastAssistantIndex && isLoading && messages[messages.length - 1]?.role === "assistant", displayRenderers: displayRenderers }) }, message.id ?? virtualRow.index));
71
+ }) }) }), isLoading && messages[messages.length - 1]?.role !== "assistant" && (_jsx("div", { className: "px-4 pb-3", children: _jsx(StreamingIndicator, {}) })), !isLoading && error && (_jsx("div", { className: "px-4 pb-3", children: _jsx(ErrorNote, { message: error.message, onRetry: onRetry }) }))] }), _jsx(ScrollBar, {}), _jsx(ScrollAreaPrimitive.Corner, {})] }));
72
+ }
@@ -0,0 +1 @@
1
+ export declare function StreamingIndicator(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function StreamingIndicator() {
3
+ return (_jsxs("span", { className: "inline-flex items-end gap-1.5 py-1 mt-4", "aria-label": "Gerando resposta...", role: "status", children: [_jsx("span", { className: "size-1 rounded-full bg-primary animate-[streaming-bounce_1.2s_ease-in-out_infinite_0ms]" }), _jsx("span", { className: "size-1 rounded-full bg-primary animate-[streaming-bounce_1.2s_ease-in-out_infinite_150ms]" }), _jsx("span", { className: "size-1 rounded-full bg-primary animate-[streaming-bounce_1.2s_ease-in-out_infinite_300ms]" }), _jsx("style", { children: `
4
+ @keyframes streaming-bounce {
5
+ 0%, 60%, 100% { transform: translateY(0); }
6
+ 30% { transform: translateY(-4px); }
7
+ }
8
+ ` })] }));
9
+ }
@@ -0,0 +1,2 @@
1
+ import type { DisplayAlert } from "./sdk-types.js";
2
+ export declare function AlertRenderer({ variant, title, message }: DisplayAlert): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { AlertCircle, AlertTriangle, CheckCircle, Info } from "lucide-react";
3
+ import { Alert, AlertDescription, AlertTitle } from "../ui/alert.js";
4
+ const VARIANT_ICON = {
5
+ info: Info,
6
+ warning: AlertTriangle,
7
+ error: AlertCircle,
8
+ success: CheckCircle,
9
+ };
10
+ export function AlertRenderer({ variant = "info", title, message }) {
11
+ const Icon = VARIANT_ICON[variant] ?? Info;
12
+ return (_jsxs(Alert, { variant: variant === "error" ? "destructive" : "default", children: [_jsx(Icon, {}), title && _jsx(AlertTitle, { children: title }), _jsx(AlertDescription, { children: message })] }));
13
+ }
@@ -0,0 +1,2 @@
1
+ import type { DisplayCarousel } from "./sdk-types.js";
2
+ export declare function CarouselRenderer({ title, items }: DisplayCarousel): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import useEmblaCarousel from "embla-carousel-react";
3
+ import { ChevronLeft, ChevronRight } from "lucide-react";
4
+ import { useCallback, useEffect, useState } from "react";
5
+ import { Badge } from "../ui/badge";
6
+ import { Button } from "../ui/button";
7
+ import { Card, CardContent } from "../ui/card";
8
+ import { cn } from "../lib/utils";
9
+ function formatPrice(value, currency) {
10
+ return new Intl.NumberFormat("pt-BR", { style: "currency", currency }).format(value);
11
+ }
12
+ export function CarouselRenderer({ title, items }) {
13
+ const [emblaRef, emblaApi] = useEmblaCarousel({ loop: false, dragFree: false });
14
+ const [selectedIndex, setSelectedIndex] = useState(0);
15
+ const [canScrollPrev, setCanScrollPrev] = useState(false);
16
+ const [canScrollNext, setCanScrollNext] = useState(false);
17
+ const onSelect = useCallback(() => {
18
+ if (!emblaApi)
19
+ return;
20
+ setSelectedIndex(emblaApi.selectedScrollSnap());
21
+ setCanScrollPrev(emblaApi.canScrollPrev());
22
+ setCanScrollNext(emblaApi.canScrollNext());
23
+ }, [emblaApi]);
24
+ useEffect(() => {
25
+ if (!emblaApi)
26
+ return;
27
+ onSelect();
28
+ emblaApi.on("select", onSelect);
29
+ emblaApi.on("reInit", onSelect);
30
+ return () => {
31
+ emblaApi.off("select", onSelect);
32
+ emblaApi.off("reInit", onSelect);
33
+ };
34
+ }, [emblaApi, onSelect]);
35
+ const scrollPrev = useCallback(() => emblaApi?.scrollPrev(), [emblaApi]);
36
+ const scrollNext = useCallback(() => emblaApi?.scrollNext(), [emblaApi]);
37
+ return (_jsxs("div", { className: "flex flex-col gap-3", children: [title && _jsx("p", { className: "text-sm font-medium text-foreground", children: title }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", size: "icon", className: "rounded-full shrink-0", onClick: scrollPrev, disabled: !canScrollPrev, "aria-label": "Anterior", type: "button", children: _jsx(ChevronLeft, { className: "h-4 w-4" }) }), _jsx("div", { className: "overflow-hidden flex-1", ref: emblaRef, children: _jsx("div", { className: "flex", children: items.map((item, index) => (_jsx("div", { className: "flex-[0_0_80%] min-w-0 pl-3 first:pl-0", children: item.url ? (_jsx("a", { href: item.url, target: "_blank", rel: "noopener noreferrer", className: "block", children: _jsx(CarouselCard, { item: item }) })) : (_jsx(CarouselCard, { item: item })) }, index))) }) }), _jsx(Button, { variant: "outline", size: "icon", className: "rounded-full shrink-0", onClick: scrollNext, disabled: !canScrollNext, "aria-label": "Pr\u00F3ximo", type: "button", children: _jsx(ChevronRight, { className: "h-4 w-4" }) })] }), items.length > 1 && (_jsx("div", { className: "flex items-center justify-center gap-1.5", role: "tablist", "aria-label": "Slides", children: items.map((_, index) => (_jsx("button", { className: cn("w-2 h-2 rounded-full transition-colors", index === selectedIndex ? "bg-primary" : "bg-muted"), onClick: () => emblaApi?.scrollTo(index), role: "tab", "aria-selected": index === selectedIndex, "aria-label": `Slide ${index + 1}`, type: "button" }, index))) }))] }));
38
+ }
39
+ function CarouselCard({ item }) {
40
+ return (_jsxs(Card, { className: "overflow-hidden", children: [item.image && (_jsx("div", { className: "aspect-video overflow-hidden", children: _jsx("img", { src: item.image, alt: item.title, loading: "lazy", className: "w-full h-full object-cover" }) })), _jsxs(CardContent, { className: "p-3 space-y-1", children: [_jsx("p", { className: "font-medium text-sm text-foreground", children: item.title }), item.subtitle && (_jsx("p", { className: "text-xs text-muted-foreground", children: item.subtitle })), item.price && (_jsx("p", { className: "text-sm font-bold text-foreground", children: formatPrice(item.price.value, item.price.currency) })), item.badges && item.badges.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-1 pt-1", children: item.badges.map((badge, i) => (_jsx(Badge, { variant: badge.variant === "destructive" ? "destructive" : badge.variant === "secondary" ? "secondary" : "default", children: badge.label }, i))) }))] })] }));
41
+ }