@polpo-ai/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.
- package/README.md +255 -0
- package/dist/chunk-CCSIMOXD.js +230 -0
- package/dist/chunk-CCSIMOXD.js.map +1 -0
- package/dist/chunk-LTLIBITC.js +64 -0
- package/dist/chunk-LTLIBITC.js.map +1 -0
- package/dist/hooks/index.d.ts +17 -0
- package/dist/hooks/index.js +9 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index.d.ts +198 -0
- package/dist/index.js +737 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/index.d.ts +21 -0
- package/dist/tools/index.js +10 -0
- package/dist/tools/index.js.map +1 -0
- package/package.json +55 -0
- package/registry.json +188 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/chat.tsx","../src/components/chat-provider.tsx","../src/components/chat-messages.tsx","../src/components/chat-skeleton.tsx","../src/components/chat-scroll-button.tsx","../src/lib/get-text-content.ts","../src/components/chat-message.tsx","../src/lib/relative-time.ts","../src/components/chat-typing.tsx","../src/components/chat-input.tsx","../src/components/streamdown-code.tsx"],"sourcesContent":["\"use client\";\n\nimport { forwardRef, type ReactNode } from \"react\";\nimport { ChatProvider } from \"./chat-provider\";\nimport { ChatMessages, type ChatMessagesHandle } from \"./chat-messages\";\nimport { ChatMessage, type ChatMessageProps } from \"./chat-message\";\nimport { ChatInput } from \"./chat-input\";\n\n// ── Props ──\n\nexport interface ChatProps {\n /** Session ID — omit for new chats */\n sessionId?: string;\n /** Agent name for completions */\n agent?: string;\n /** Called when a new session is created */\n onSessionCreated?: (sessionId: string) => void;\n /** Called on each stream update (useful for external scroll control) */\n onUpdate?: () => void;\n /** Custom message renderer — if omitted, uses ChatMessage with defaults */\n renderMessage?: (msg: ChatMessageProps[\"msg\"], index: number, isLast: boolean, isStreaming: boolean) => ReactNode;\n /** Avatar ReactNode shown on assistant messages */\n avatar?: ReactNode;\n /** Agent display name shown on assistant messages */\n agentName?: string;\n /** Streamdown components override for code blocks etc. */\n streamdownComponents?: Record<string, unknown>;\n /** Number of skeleton items while loading */\n skeletonCount?: number;\n /** Placeholder text for the default input */\n inputPlaceholder?: string;\n /** Hint text below the default input */\n inputHint?: string;\n /** Whether file attachments are enabled on the default input (default: true) */\n allowAttachments?: boolean;\n /** Children rendered after the message list — replaces the default ChatInput */\n children?: ReactNode;\n /** Additional className on the outer container */\n className?: string;\n}\n\n// ── Component ──\n\nexport const Chat = forwardRef<ChatMessagesHandle, ChatProps>(function Chat(\n {\n sessionId,\n agent,\n onSessionCreated,\n onUpdate,\n renderMessage,\n avatar,\n agentName,\n streamdownComponents,\n skeletonCount,\n inputPlaceholder,\n inputHint,\n allowAttachments,\n children,\n className,\n },\n ref,\n) {\n const hasChildren = children !== undefined && children !== null;\n const defaultRender = (msg: ChatMessageProps[\"msg\"], _index: number, isLast: boolean, isStreaming: boolean) => (\n <ChatMessage\n msg={msg}\n isLast={isLast}\n isStreaming={isStreaming}\n avatar={avatar}\n agentName={agentName}\n streamdownComponents={streamdownComponents}\n />\n );\n\n return (\n <ChatProvider\n sessionId={sessionId}\n agent={agent}\n onSessionCreated={onSessionCreated}\n onUpdate={onUpdate}\n >\n <div className={`flex flex-col min-w-0 min-h-0 ${className || \"\"}`}>\n <ChatMessages\n ref={ref}\n renderItem={renderMessage || defaultRender}\n skeletonCount={skeletonCount}\n className=\"flex-1\"\n />\n {hasChildren ? children : (\n <ChatInput\n placeholder={inputPlaceholder}\n hint={inputHint}\n allowAttachments={allowAttachments}\n />\n )}\n </div>\n </ChatProvider>\n );\n});\n","\"use client\";\n\nimport {\n createContext,\n useContext,\n type ReactNode,\n} from \"react\";\nimport { useChat, type UseChatReturn } from \"@polpo-ai/react\";\nimport { useFiles } from \"@polpo-ai/react\";\n\n/* ------------------------------------------------------------------ */\n/* Types */\n/* ------------------------------------------------------------------ */\n\nexport interface ChatProviderProps {\n /** Resume an existing session by ID. */\n sessionId?: string;\n /** Target a specific agent for direct conversation. */\n agent?: string;\n /** Called when a new session is created (first message). */\n onSessionCreated?: (id: string) => void;\n /** Called after each stream update (e.g. scroll-to-bottom). */\n onUpdate?: () => void;\n children: ReactNode;\n}\n\nexport interface ChatContextValue extends UseChatReturn {\n /** Upload a file attachment (delegates to useFiles). */\n uploadFile: (\n destPath: string,\n file: File | Blob,\n filename: string,\n ) => Promise<{ uploaded: { name: string; size: number }[]; count: number }>;\n /** Whether a file upload is in progress. */\n isUploading: boolean;\n}\n\n/* ------------------------------------------------------------------ */\n/* Context */\n/* ------------------------------------------------------------------ */\n\nconst ChatContext = createContext<ChatContextValue | null>(null);\n\n/* ------------------------------------------------------------------ */\n/* Provider */\n/* ------------------------------------------------------------------ */\n\nexport function ChatProvider({\n sessionId,\n agent,\n onSessionCreated,\n onUpdate,\n children,\n}: ChatProviderProps) {\n const chat = useChat({\n sessionId,\n agent,\n onSessionCreated,\n onUpdate,\n });\n\n const { uploadFile, isUploading } = useFiles();\n\n const value: ChatContextValue = {\n ...chat,\n uploadFile,\n isUploading,\n };\n\n return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>;\n}\n\n/* ------------------------------------------------------------------ */\n/* Consumer hook */\n/* ------------------------------------------------------------------ */\n\nexport function useChatContext(): ChatContextValue {\n const ctx = useContext(ChatContext);\n if (!ctx) {\n throw new Error(\"useChatContext must be used within a <ChatProvider>\");\n }\n return ctx;\n}\n","\"use client\";\n\nimport {\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport { Virtuoso, type VirtuosoHandle } from \"react-virtuoso\";\nimport type { ChatMessage } from \"@polpo-ai/sdk\";\nimport { useChatContext } from \"./chat-provider\";\nimport { ChatSkeleton } from \"./chat-skeleton\";\nimport { ChatScrollButton } from \"./chat-scroll-button\";\nimport { getTextContent } from \"../lib/get-text-content\";\n\n/* ------------------------------------------------------------------ */\n/* Types */\n/* ------------------------------------------------------------------ */\n\nexport interface ChatMessagesHandle {\n /** Scroll the list to the bottom. */\n scrollToBottom: (behavior?: \"smooth\" | \"auto\") => void;\n}\n\nexport interface ChatMessagesProps {\n /**\n * Custom renderer for each message.\n * Receives the message, its index, whether it is the last item, and\n * whether the assistant is currently streaming.\n */\n renderItem?: (\n msg: ChatMessage,\n index: number,\n isLast: boolean,\n isStreaming: boolean,\n ) => ReactNode;\n /** Extra classes applied to the Virtuoso container. */\n className?: string;\n /** Number of skeleton pairs to show during the initial load. */\n skeletonCount?: number;\n}\n\n/* ------------------------------------------------------------------ */\n/* Default renderer (plain text fallback) */\n/* ------------------------------------------------------------------ */\n\nfunction DefaultMessageItem({ msg }: { msg: ChatMessage }) {\n const text = getTextContent(msg.content);\n return (\n <div className=\"w-full px-6 py-3\">\n <div className=\"max-w-3xl mx-auto\">\n <p className=\"text-xs font-medium opacity-50 mb-1\">\n {msg.role === \"user\" ? \"You\" : \"Assistant\"}\n </p>\n <p className=\"whitespace-pre-wrap\">{text}</p>\n </div>\n </div>\n );\n}\n\n/* ------------------------------------------------------------------ */\n/* Footer (keeps Virtuoso happy for follow-output) */\n/* ------------------------------------------------------------------ */\n\nfunction VirtuosoFooter() {\n return <div className=\"h-px\" aria-hidden=\"true\" />;\n}\n\n/* ------------------------------------------------------------------ */\n/* Component */\n/* ------------------------------------------------------------------ */\n\nexport const ChatMessages = forwardRef<ChatMessagesHandle, ChatMessagesProps>(\n function ChatMessages({ renderItem, className, skeletonCount = 3 }, ref) {\n const { messages, isStreaming, status } = useChatContext();\n\n /* ── Virtuoso ref & scroll state ────────────────────────────── */\n const virtuosoRef = useRef<VirtuosoHandle>(null);\n const [isAtBottom, setIsAtBottom] = useState(true);\n const [showNewMessage, setShowNewMessage] = useState(false);\n const prevMessageCountRef = useRef(0);\n const hasInitialScrollRef = useRef(false);\n\n /* ── Scroll helpers ─────────────────────────────────────────── */\n const scrollToBottom = useCallback(\n (behavior: \"smooth\" | \"auto\" = \"smooth\") => {\n virtuosoRef.current?.scrollToIndex({\n index: \"LAST\",\n align: \"end\",\n behavior,\n });\n setShowNewMessage(false);\n },\n [],\n );\n\n useImperativeHandle(ref, () => ({ scrollToBottom }), [scrollToBottom]);\n\n /* ── Bottom-state change handler ────────────────────────────── */\n const handleAtBottomStateChange = useCallback((atBottom: boolean) => {\n setIsAtBottom(atBottom);\n if (atBottom) setShowNewMessage(false);\n }, []);\n\n /* ── Initial scroll to bottom once messages load ────────────── */\n useEffect(() => {\n if (messages.length > 0 && !hasInitialScrollRef.current) {\n hasInitialScrollRef.current = true;\n requestAnimationFrame(() =>\n virtuosoRef.current?.scrollToIndex({\n index: \"LAST\",\n align: \"end\",\n behavior: \"auto\",\n }),\n );\n }\n }, [messages.length]);\n\n /* ── Reset scroll flag when data source changes ─────────────── */\n useEffect(() => {\n hasInitialScrollRef.current = false;\n }, [status]);\n\n /* ── Show \"new messages\" badge when not at bottom ───────────── */\n useEffect(() => {\n const cur = messages.length;\n const prev = prevMessageCountRef.current;\n if (cur > prev && !isAtBottom && prev > 0) {\n setShowNewMessage(true);\n }\n prevMessageCountRef.current = cur;\n }, [messages.length, isAtBottom]);\n\n /* ── Item renderer ──────────────────────────────────────────── */\n const itemContent = useCallback(\n (index: number, msg: ChatMessage) => {\n const isLast = index === messages.length - 1;\n if (renderItem) {\n return renderItem(msg, index, isLast, isStreaming);\n }\n return <DefaultMessageItem msg={msg} />;\n },\n [messages.length, isStreaming, renderItem],\n );\n\n /* ── Loading state ──────────────────────────────────────────── */\n if (status === \"loading\") {\n return (\n <div className={`flex-1 overflow-hidden ${className ?? \"\"}`}>\n <ChatSkeleton count={skeletonCount} />\n </div>\n );\n }\n\n /* ── Main list ──────────────────────────────────────────────── */\n return (\n <div className={`relative flex-1 min-h-0 ${className ?? \"\"}`}>\n <Virtuoso\n ref={virtuosoRef}\n data={messages}\n followOutput=\"auto\"\n atBottomStateChange={handleAtBottomStateChange}\n atBottomThreshold={100}\n defaultItemHeight={120}\n overscan={500}\n increaseViewportBy={{ top: 300, bottom: 300 }}\n skipAnimationFrameInResizeObserver\n itemContent={itemContent}\n className=\"h-full\"\n components={{ Footer: VirtuosoFooter }}\n />\n\n <ChatScrollButton\n isAtBottom={isAtBottom}\n showNewMessage={showNewMessage}\n onClick={() => scrollToBottom()}\n />\n </div>\n );\n },\n);\n","/**\n * Skeleton loaders for chat UI — mirrors the exact layout of ChatMessageItem\n * so the loading state feels native, not jarring.\n */\n\nfunction Bone({ width, height = 14 }: { width: string; height?: number }) {\n return (\n <div\n className=\"bg-p-line animate-pulse\"\n style={{ width, height, borderRadius: height > 20 ? 12 : 6 }}\n />\n );\n}\n\n/** Skeleton for an AI message: avatar + name + text lines */\nexport function MessageSkeleton({ lines = 3 }: { lines?: number }) {\n const widths = [\"85%\", \"70%\", \"55%\", \"90%\", \"40%\"];\n return (\n <div className=\"w-full px-6 pt-4 pb-6\">\n <div className=\"max-w-3xl mx-auto\">\n <div className=\"flex items-center gap-2 mb-2\">\n <div className=\"size-6 rounded-md bg-p-line animate-pulse shrink-0\" />\n <Bone width=\"80px\" height={13} />\n </div>\n <div className=\"flex flex-col gap-2\">\n {Array.from({ length: lines }, (_, i) => (\n <Bone key={i} width={widths[i % widths.length]} />\n ))}\n </div>\n </div>\n </div>\n );\n}\n\n/** Skeleton for a user message: right-aligned bubble */\nexport function UserMessageSkeleton() {\n return (\n <div className=\"w-full px-6 py-3\">\n <div className=\"max-w-3xl mx-auto flex justify-end\">\n <div className=\"w-[45%] min-w-[120px] h-[42px] rounded-[18px_18px_4px_18px] bg-p-line animate-pulse\" />\n </div>\n </div>\n );\n}\n\n/** Full conversation skeleton — alternating user/AI messages */\nexport function ChatSkeleton({ count = 3 }: { count?: number }) {\n return (\n <div className=\"py-2\">\n {Array.from({ length: count }, (_, i) => (\n <div key={i}>\n <UserMessageSkeleton />\n <MessageSkeleton lines={i === 0 ? 2 : i === 1 ? 4 : 3} />\n </div>\n ))}\n </div>\n );\n}\n","\"use client\";\n\nimport { ArrowDown } from \"lucide-react\";\n\n/* ------------------------------------------------------------------ */\n/* Scroll-to-bottom button with optional new-message indicator */\n/* ------------------------------------------------------------------ */\n\ninterface ChatScrollButtonProps {\n isAtBottom: boolean;\n showNewMessage?: boolean;\n onClick: () => void;\n className?: string;\n}\n\nexport function ChatScrollButton({\n isAtBottom,\n showNewMessage,\n onClick,\n className,\n}: ChatScrollButtonProps) {\n if (isAtBottom) return null;\n\n return (\n <button\n type=\"button\"\n aria-label=\"Scroll to bottom\"\n onClick={onClick}\n className={`absolute bottom-4 left-1/2 -translate-x-1/2 z-10 flex h-8 w-8 items-center justify-center rounded-full border border-[var(--polpo-border)] bg-[var(--polpo-bg,#fff)] shadow-md transition-colors hover:bg-[var(--polpo-bg-hover,#f5f5f5)] ${className ?? \"\"}`}\n >\n <ArrowDown className=\"h-4 w-4 text-[var(--polpo-fg,#333)]\" />\n\n {showNewMessage && (\n <span className=\"absolute -top-1 -right-1 flex h-3 w-3\">\n <span className=\"absolute inline-flex h-full w-full animate-ping rounded-full bg-[var(--polpo-accent,#3b82f6)] opacity-75\" />\n <span className=\"relative inline-flex h-3 w-3 rounded-full bg-[var(--polpo-accent,#3b82f6)]\" />\n </span>\n )}\n </button>\n );\n}\n","import type { ContentPart } from \"@polpo-ai/sdk\";\n\n/** Extract the concatenated text from a message content value (string or ContentPart[]). */\nexport function getTextContent(content: string | ContentPart[]): string {\n if (typeof content === \"string\") return content;\n return content\n .filter((p): p is { type: \"text\"; text: string } => p.type === \"text\")\n .map((p) => p.text)\n .join(\"\");\n}\n","\"use client\";\n\nimport {\n memo,\n useState,\n useCallback,\n type ReactNode,\n} from \"react\";\nimport type { ContentPart, ToolCallEvent } from \"@polpo-ai/sdk\";\nimport { Copy, Check, FileCode } from \"lucide-react\";\nimport { Streamdown } from \"streamdown\";\nimport { getTextContent } from \"../lib/get-text-content\";\nimport { relativeTime } from \"../lib/relative-time\";\nimport { ToolCallChip } from \"../tools\";\nimport { ChatTyping } from \"./chat-typing\";\n\n/** Components override accepted by Streamdown — use Record for flexibility. */\ntype StreamdownComponentsProp = Record<string, unknown>;\n\n/* ------------------------------------------------------------------ */\n/* Types */\n/* ------------------------------------------------------------------ */\n\nexport interface ChatMessageItemData {\n id?: string;\n role: \"user\" | \"assistant\";\n content: string | ContentPart[];\n ts?: string;\n toolCalls?: ToolCallEvent[];\n}\n\nexport interface ChatMessageProps {\n msg: ChatMessageItemData;\n isLast?: boolean;\n isStreaming?: boolean;\n avatar?: ReactNode;\n agentName?: string;\n streamdownComponents?: StreamdownComponentsProp;\n}\n\n/* ------------------------------------------------------------------ */\n/* CopyButton */\n/* ------------------------------------------------------------------ */\n\nfunction CopyButton({ text }: { text: string }) {\n const [copied, setCopied] = useState(false);\n\n const handleCopy = useCallback(() => {\n navigator.clipboard.writeText(text);\n setCopied(true);\n setTimeout(() => setCopied(false), 1500);\n }, [text]);\n\n return (\n <button\n type=\"button\"\n onClick={handleCopy}\n aria-label=\"Copy message\"\n className=\"inline-flex items-center justify-center rounded-md p-1 text-[var(--ink-3)] hover:text-[var(--ink)] hover:bg-[var(--warm)] transition-colors\"\n >\n {copied ? <Check className=\"size-3.5\" /> : <Copy className=\"size-3.5\" />}\n </button>\n );\n}\n\n/* ------------------------------------------------------------------ */\n/* File / image content parts */\n/* ------------------------------------------------------------------ */\n\nfunction ContentParts({\n parts,\n align,\n}: {\n parts: ContentPart[];\n align: \"start\" | \"end\";\n}) {\n const nonText = parts.filter((p) => p.type !== \"text\");\n if (nonText.length === 0) return null;\n\n return (\n <div\n className={`flex flex-wrap gap-1.5 mb-1 ${align === \"end\" ? \"justify-end\" : \"\"}`}\n >\n {nonText.map((part, i) => {\n if (part.type === \"image_url\") {\n return (\n <a\n key={i}\n href={(part as { type: \"image_url\"; image_url: { url: string } }).image_url.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"rounded-lg overflow-hidden max-w-[200px]\"\n >\n <img\n src={(part as { type: \"image_url\"; image_url: { url: string } }).image_url.url}\n alt=\"\"\n className=\"w-full h-auto block\"\n />\n </a>\n );\n }\n if (part.type === \"file\") {\n const fileId = (part as { type: \"file\"; file_id: string }).file_id;\n const fn = fileId.split(\"/\").pop() || fileId;\n return (\n <a\n key={i}\n href={`/api/polpo/files/read?path=${encodeURIComponent(fileId)}`}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-1.5 bg-[var(--warm)] border border-[var(--line)] rounded-lg px-2.5 py-1.5 text-xs text-[var(--ink)] hover:border-[var(--ink-3)] transition-colors\"\n >\n <FileCode size={13} />\n <span className=\"truncate max-w-[120px]\">{fn}</span>\n </a>\n );\n }\n return null;\n })}\n </div>\n );\n}\n\n/* ------------------------------------------------------------------ */\n/* ChatUserMessage */\n/* ------------------------------------------------------------------ */\n\nexport const ChatUserMessage = memo(\n function ChatUserMessage({\n msg,\n isLast,\n isStreaming,\n }: {\n msg: ChatMessageItemData;\n isLast?: boolean;\n isStreaming?: boolean;\n }) {\n const text = getTextContent(msg.content);\n\n return (\n <div className=\"w-full px-6 py-3\">\n <div className=\"max-w-3xl mx-auto\">\n <div className=\"group flex w-full flex-col gap-2 ml-auto justify-end\">\n {/* File/image parts */}\n {Array.isArray(msg.content) && (\n <ContentParts parts={msg.content} align=\"end\" />\n )}\n\n {/* Message bubble */}\n <div className=\"w-fit max-w-[80%] ml-auto rounded-[18px_18px_4px_18px] bg-[var(--warm)] px-4 py-3\">\n {text ? (\n <p className=\"whitespace-pre-wrap break-words text-[var(--ink)]\">\n {text}\n </p>\n ) : null}\n </div>\n\n {/* Hover actions: timestamp + copy */}\n {text && (!isLast || !isStreaming) && (\n <div className=\"flex items-center justify-end gap-1.5 h-6\">\n <span className=\"text-[11px] text-[var(--ink-3)] opacity-0 group-hover:opacity-100 transition-opacity\">\n {msg.ts ? relativeTime(msg.ts) : \"\"}\n </span>\n <div className=\"opacity-0 group-hover:opacity-100 transition-opacity\">\n <CopyButton text={text} />\n </div>\n </div>\n )}\n </div>\n </div>\n </div>\n );\n },\n (prev, next) =>\n prev.isLast === next.isLast &&\n prev.isStreaming === next.isStreaming &&\n prev.msg.id === next.msg.id &&\n prev.msg.content === next.msg.content,\n);\n\n/* ------------------------------------------------------------------ */\n/* ChatAssistantMessage */\n/* ------------------------------------------------------------------ */\n\nexport const ChatAssistantMessage = memo(\n function ChatAssistantMessage({\n msg,\n isLast,\n isStreaming,\n avatar,\n agentName,\n streamdownComponents: components,\n }: {\n msg: ChatMessageItemData;\n isLast?: boolean;\n isStreaming?: boolean;\n avatar?: ReactNode;\n agentName?: string;\n streamdownComponents?: StreamdownComponentsProp;\n }) {\n const text = getTextContent(msg.content);\n const filteredToolCalls = msg.toolCalls?.filter(\n (tc) => tc.name !== \"ask_user_question\",\n );\n\n return (\n <div className=\"w-full px-6 pt-4 pb-6\">\n <div className=\"max-w-3xl mx-auto\">\n <div className=\"group flex w-full flex-col gap-2\">\n {/* Avatar + name header */}\n {(avatar || agentName) && (\n <div className=\"flex items-center gap-2 mb-1\">\n {avatar}\n {agentName && (\n <span className=\"font-display text-[13px] font-semibold text-[var(--ink)]\">\n {agentName}\n </span>\n )}\n </div>\n )}\n\n {/* Tool calls */}\n {filteredToolCalls && filteredToolCalls.length > 0 && (\n <div className=\"flex flex-col gap-1 mb-1\">\n {filteredToolCalls.map((tc) => (\n <ToolCallChip key={tc.id} tool={tc} />\n ))}\n </div>\n )}\n\n {/* File/image parts */}\n {Array.isArray(msg.content) && (\n <ContentParts parts={msg.content} align=\"start\" />\n )}\n\n {/* Text content or typing dots */}\n <div className=\"w-full text-[var(--ink)]\">\n {text ? (\n components ? (\n <Streamdown\n className=\"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0\"\n components={components as any}\n >\n {text}\n </Streamdown>\n ) : (\n <p className=\"whitespace-pre-wrap break-words\">{text}</p>\n )\n ) : (\n !filteredToolCalls?.length && <ChatTyping className=\"pt-1\" />\n )}\n </div>\n\n {/* Hover action: copy */}\n {text && (!isLast || !isStreaming) && (\n <div className=\"h-6 flex items-center opacity-0 group-hover:opacity-100 transition-opacity\">\n <CopyButton text={text} />\n </div>\n )}\n </div>\n </div>\n </div>\n );\n },\n (prev, next) =>\n prev.avatar === next.avatar &&\n prev.agentName === next.agentName &&\n prev.isLast === next.isLast &&\n prev.isStreaming === next.isStreaming &&\n prev.streamdownComponents === next.streamdownComponents &&\n prev.msg.id === next.msg.id &&\n prev.msg.content === next.msg.content &&\n prev.msg.toolCalls?.length === next.msg.toolCalls?.length &&\n JSON.stringify(prev.msg.toolCalls?.map((t) => t.state)) ===\n JSON.stringify(next.msg.toolCalls?.map((t) => t.state)),\n);\n\n/* ------------------------------------------------------------------ */\n/* ChatMessage — dispatcher */\n/* ------------------------------------------------------------------ */\n\nexport const ChatMessage = memo(\n function ChatMessage({\n msg,\n isLast,\n isStreaming,\n avatar,\n agentName,\n streamdownComponents,\n }: ChatMessageProps) {\n if (msg.role === \"user\") {\n return (\n <ChatUserMessage msg={msg} isLast={isLast} isStreaming={isStreaming} />\n );\n }\n\n return (\n <ChatAssistantMessage\n msg={msg}\n isLast={isLast}\n isStreaming={isStreaming}\n avatar={avatar}\n agentName={agentName}\n streamdownComponents={streamdownComponents}\n />\n );\n },\n (prev, next) =>\n prev.avatar === next.avatar &&\n prev.agentName === next.agentName &&\n prev.isLast === next.isLast &&\n prev.isStreaming === next.isStreaming &&\n prev.streamdownComponents === next.streamdownComponents &&\n prev.msg.id === next.msg.id &&\n prev.msg.content === next.msg.content &&\n prev.msg.role === next.msg.role &&\n prev.msg.toolCalls?.length === next.msg.toolCalls?.length &&\n JSON.stringify(prev.msg.toolCalls?.map((t) => t.state)) ===\n JSON.stringify(next.msg.toolCalls?.map((t) => t.state)),\n);\n","/** Format an ISO timestamp as a human-readable relative time string. */\nexport function relativeTime(iso: string): string {\n const now = Date.now();\n const then = new Date(iso).getTime();\n const diff = Math.floor((now - then) / 1000);\n if (diff < 10) return \"Just now\";\n if (diff < 60) return `${diff}s ago`;\n const mins = Math.floor(diff / 60);\n if (mins < 60) return mins === 1 ? \"A minute ago\" : `${mins}m ago`;\n const hours = Math.floor(mins / 60);\n if (hours < 24) return hours === 1 ? \"An hour ago\" : `${hours}h ago`;\n return new Date(iso).toLocaleString(undefined, { month: \"short\", day: \"numeric\", hour: \"2-digit\", minute: \"2-digit\" });\n}\n","\"use client\";\n\n/* ------------------------------------------------------------------ */\n/* Typing indicator (three animated dots) */\n/* ------------------------------------------------------------------ */\n\nconst dotBase =\n \"inline-block h-1.5 w-1.5 rounded-full bg-current opacity-40 animate-[typing-dot_1.4s_ease-in-out_infinite]\";\n\nexport function ChatTyping({ className }: { className?: string }) {\n return (\n <span\n role=\"status\"\n aria-label=\"Typing\"\n className={`inline-flex items-center gap-1 ${className ?? \"\"}`}\n >\n <span className={dotBase} style={{ animationDelay: \"0ms\" }} />\n <span className={dotBase} style={{ animationDelay: \"200ms\" }} />\n <span className={dotBase} style={{ animationDelay: \"400ms\" }} />\n </span>\n );\n}\n","\"use client\";\n\nimport {\n useState,\n useRef,\n useCallback,\n useEffect,\n type KeyboardEvent,\n type ChangeEvent,\n type DragEvent,\n type ReactNode,\n} from \"react\";\nimport { ArrowUp, Square, Plus, X, Upload } from \"lucide-react\";\nimport { useChatContext } from \"./chat-provider\";\nimport { useSubmitHandler } from \"../hooks/use-submit-handler\";\nimport { useDocumentDrag } from \"../hooks/use-document-drag\";\n\n/* ------------------------------------------------------------------ */\n/* Types */\n/* ------------------------------------------------------------------ */\n\ninterface PendingFile {\n id: string;\n file: File;\n url: string;\n}\n\nexport interface ChatInputProps {\n /** Placeholder text for the textarea */\n placeholder?: string;\n /** Hint text below the input */\n hint?: string;\n /** Whether file attachments are enabled (default: true) */\n allowAttachments?: boolean;\n /** Additional className on the outer wrapper */\n className?: string;\n /** Custom submit button renderer */\n renderSubmit?: (props: { isStreaming: boolean; onStop: () => void }) => ReactNode;\n}\n\n/* ------------------------------------------------------------------ */\n/* ChatInput */\n/* ------------------------------------------------------------------ */\n\nexport function ChatInput({\n placeholder = \"Type a message…\",\n hint,\n allowAttachments = true,\n className,\n renderSubmit,\n}: ChatInputProps) {\n const { sendMessage, isStreaming, abort, uploadFile } = useChatContext();\n const handleSubmit = useSubmitHandler(sendMessage, uploadFile);\n const dragging = useDocumentDrag();\n\n const [text, setText] = useState(\"\");\n const [files, setFiles] = useState<PendingFile[]>([]);\n const [isSending, setIsSending] = useState(false);\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n const fileInputRef = useRef<HTMLInputElement>(null);\n\n // Auto-resize textarea\n useEffect(() => {\n const el = textareaRef.current;\n if (!el) return;\n el.style.height = \"auto\";\n el.style.height = `${Math.min(el.scrollHeight, 200)}px`;\n }, [text]);\n\n // Add files helper\n const addFiles = useCallback((incoming: FileList | File[]) => {\n const arr = Array.from(incoming);\n setFiles((prev) => {\n // Dedupe by filename\n const existing = new Set(prev.map((f) => f.file.name));\n const fresh = arr.filter((f) => !existing.has(f.name));\n return [\n ...prev,\n ...fresh.map((file) => ({\n id: Math.random().toString(36).slice(2),\n file,\n url: URL.createObjectURL(file),\n })),\n ];\n });\n }, []);\n\n const removeFile = useCallback((id: string) => {\n setFiles((prev) => {\n const f = prev.find((p) => p.id === id);\n if (f) URL.revokeObjectURL(f.url);\n return prev.filter((p) => p.id !== id);\n });\n }, []);\n\n // Cleanup blob URLs on unmount\n useEffect(\n () => () => {\n files.forEach((f) => URL.revokeObjectURL(f.url));\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [],\n );\n\n // Submit\n const submit = useCallback(async () => {\n if (isSending) return;\n const trimmed = text.trim();\n if (!trimmed && files.length === 0) return;\n\n setIsSending(true);\n try {\n await handleSubmit({\n text: trimmed,\n files: files.map((f) => ({ url: f.url, filename: f.file.name })),\n });\n setText(\"\");\n setFiles([]);\n } finally {\n setIsSending(false);\n }\n\n // Refocus textarea\n setTimeout(() => textareaRef.current?.focus(), 0);\n }, [text, files, handleSubmit, isSending]);\n\n // Keyboard: Enter to send, Shift+Enter for newline\n const onKeyDown = useCallback(\n (e: KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n submit();\n }\n },\n [submit],\n );\n\n // File input change\n const onFileChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n if (e.target.files) addFiles(e.target.files);\n e.target.value = \"\";\n },\n [addFiles],\n );\n\n // Drop handler\n const onDrop = useCallback(\n (e: DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n if (e.dataTransfer.files.length > 0) {\n addFiles(e.dataTransfer.files);\n }\n },\n [addFiles],\n );\n\n return (\n <div className={`shrink-0 ${className || \"\"}`}>\n <div className=\"w-full px-6 py-3\">\n <div className=\"max-w-3xl mx-auto relative\">\n {/* Drag overlay */}\n {allowAttachments && dragging && (\n <div className=\"absolute inset-0 z-10 bg-blue-50 border-2 border-dashed border-blue-400 rounded-2xl flex items-center justify-center gap-2 text-blue-600 text-sm font-medium pointer-events-none\">\n <Upload className=\"size-4\" /> Drop files to attach\n </div>\n )}\n\n {/* Input container */}\n <div\n className=\"rounded-2xl border border-gray-200 shadow-sm focus-within:border-blue-400 focus-within:shadow-md transition-all bg-white\"\n onDrop={allowAttachments ? onDrop : undefined}\n onDragOver={allowAttachments ? (e) => e.preventDefault() : undefined}\n >\n {/* Pending files */}\n {files.length > 0 && (\n <div className=\"flex flex-wrap gap-2 px-4 pt-3\">\n {files.map((f) => {\n const isImage = f.file.type.startsWith(\"image/\");\n return (\n <div\n key={f.id}\n className=\"relative flex items-center gap-2 bg-gray-50 border border-gray-200 rounded-lg px-2.5 py-1.5 text-xs\"\n >\n {isImage ? (\n <img src={f.url} alt={f.file.name} className=\"size-8 rounded object-cover\" />\n ) : (\n <div className=\"size-8 rounded bg-gray-200 flex items-center justify-center text-gray-500 text-[10px] font-mono uppercase\">\n {f.file.name.split(\".\").pop()?.slice(0, 4)}\n </div>\n )}\n <span className=\"truncate max-w-[120px]\">{f.file.name}</span>\n <button\n type=\"button\"\n onClick={() => removeFile(f.id)}\n className=\"size-4 rounded-full bg-gray-300/50 flex items-center justify-center hover:bg-red-500 hover:text-white transition-colors\"\n >\n <X className=\"size-2.5\" />\n </button>\n </div>\n );\n })}\n </div>\n )}\n\n {/* Textarea */}\n <textarea\n ref={textareaRef}\n value={text}\n onChange={(e) => setText(e.target.value)}\n onKeyDown={onKeyDown}\n placeholder={placeholder}\n rows={1}\n className=\"w-full resize-none bg-transparent px-5 pt-4 pb-2 text-sm outline-none placeholder:text-gray-400\"\n />\n\n {/* Footer */}\n <div className=\"flex items-center justify-between px-3 pb-3\">\n {/* Left: attach button */}\n {allowAttachments ? (\n <button\n type=\"button\"\n onClick={() => fileInputRef.current?.click()}\n className=\"flex items-center justify-center size-8 rounded-lg text-gray-400 hover:text-gray-700 hover:bg-gray-100 transition-colors\"\n aria-label=\"Attach file\"\n >\n <Plus className=\"size-4\" />\n </button>\n ) : (\n <div />\n )}\n\n {/* Right: submit / stop */}\n {renderSubmit ? (\n renderSubmit({ isStreaming, onStop: abort })\n ) : (\n <button\n type=\"button\"\n onClick={isStreaming ? abort : submit}\n disabled={isSending && !isStreaming}\n className=\"flex items-center justify-center size-8 rounded-lg bg-gray-900 text-white hover:bg-gray-700 disabled:opacity-40 transition-colors\"\n aria-label={isStreaming ? \"Stop\" : \"Send\"}\n >\n {isStreaming ? (\n <Square className=\"size-3.5\" />\n ) : (\n <ArrowUp className=\"size-4\" />\n )}\n </button>\n )}\n </div>\n </div>\n\n {/* Hidden file input */}\n {allowAttachments && (\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n onChange={onFileChange}\n className=\"hidden\"\n />\n )}\n\n {/* Hint */}\n {hint && (\n <p className=\"text-center text-xs text-gray-400 mt-2\">{hint}</p>\n )}\n </div>\n </div>\n </div>\n );\n}\n","\"use client\";\n\nimport type { ComponentType, HTMLAttributes } from \"react\";\n\n/* ------------------------------------------------------------------ */\n/* Types */\n/* ------------------------------------------------------------------ */\n\n/** Props that any code-block component must accept. */\nexport interface CodeBlockComponentProps {\n code: string;\n language: string;\n children?: React.ReactNode;\n}\n\n/** The shape returned by createStreamdownComponents / the default export. */\nexport interface StreamdownComponents {\n code: (\n props: HTMLAttributes<HTMLElement> & {\n node?: unknown;\n \"data-block\"?: string;\n },\n ) => React.ReactNode;\n}\n\n/* ------------------------------------------------------------------ */\n/* Fallback code block (simple <pre><code>) */\n/* ------------------------------------------------------------------ */\n\nfunction FallbackCodeBlock({ code, language }: CodeBlockComponentProps) {\n return (\n <pre\n data-language={language}\n style={{\n margin: \"0.75rem 0\",\n padding: \"0.875rem 1rem\",\n borderRadius: \"10px\",\n overflowX: \"auto\",\n border: \"1px solid var(--line, #e5e7eb)\",\n background: \"var(--bg, #f9fafb)\",\n color: \"var(--ink, inherit)\",\n fontSize: \"0.75rem\",\n lineHeight: \"1.625\",\n }}\n >\n <code className={`language-${language}`}>{code}</code>\n </pre>\n );\n}\n\n/* ------------------------------------------------------------------ */\n/* Factory */\n/* ------------------------------------------------------------------ */\n\n/**\n * Create a `streamdownComponents` override object for Streamdown.\n *\n * Pass your own `CodeBlock` component to get syntax-highlighted fenced\n * code blocks. If omitted, a plain `<pre><code>` fallback is used.\n *\n * @example\n * ```tsx\n * import { CodeBlock } from \"@/components/ai-elements/code-block\";\n * import { createStreamdownComponents } from \"@polpo-ai/chat\";\n *\n * const streamdownComponents = createStreamdownComponents(CodeBlock);\n * ```\n */\nexport function createStreamdownComponents(\n CodeBlockComponent?: ComponentType<CodeBlockComponentProps>,\n): StreamdownComponents {\n const Block = CodeBlockComponent ?? FallbackCodeBlock;\n\n function StreamdownCode(\n props: HTMLAttributes<HTMLElement> & {\n node?: unknown;\n \"data-block\"?: string;\n },\n ) {\n const {\n children,\n className,\n node: _,\n \"data-block\": dataBlock,\n ...rest\n } = props;\n\n // Fenced code blocks carry data-block; inline code does not.\n if (dataBlock !== undefined) {\n const match = /language-(\\w+)/.exec(className || \"\");\n const lang = match?.[1] || \"text\";\n const code = String(children).replace(/\\n$/, \"\");\n return <Block code={code} language={lang} />;\n }\n\n return (\n <code className={className} {...rest}>\n {children}\n </code>\n );\n }\n\n return { code: StreamdownCode };\n}\n\n/* ------------------------------------------------------------------ */\n/* Pre-built instance — uses the plain fallback */\n/* ------------------------------------------------------------------ */\n\nexport const streamdownComponents: StreamdownComponents =\n createStreamdownComponents();\n"],"mappings":"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","names":["forwardRef","jsx","jsx","jsxs","jsx","jsxs","ChatMessages","useState","useCallback","jsx","jsxs","jsx","jsxs","useState","useCallback","ChatUserMessage","ChatAssistantMessage","ChatMessage","streamdownComponents","useState","useRef","useCallback","useEffect","jsx","jsxs","useState","useRef","useEffect","useCallback","jsx","jsxs","forwardRef","Chat","streamdownComponents","jsx"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ToolCallEvent } from '@polpo-ai/sdk';
|
|
3
|
+
import { ReactNode } from 'react';
|
|
4
|
+
import { LucideIcon } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
interface ToolCallShellProps {
|
|
7
|
+
tool: ToolCallEvent;
|
|
8
|
+
icon: LucideIcon;
|
|
9
|
+
label: string;
|
|
10
|
+
/** One-line summary shown next to the label */
|
|
11
|
+
summary?: string | null;
|
|
12
|
+
/** Custom expanded content — replaces default raw result */
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
declare function ToolCallShell({ tool, icon: Icon, label, summary, children }: ToolCallShellProps): react_jsx_runtime.JSX.Element;
|
|
16
|
+
|
|
17
|
+
declare function ToolCallChip({ tool }: {
|
|
18
|
+
tool: ToolCallEvent;
|
|
19
|
+
}): react_jsx_runtime.JSX.Element;
|
|
20
|
+
|
|
21
|
+
export { ToolCallChip, ToolCallShell };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@polpo-ai/chat",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Composable chat UI components for Polpo AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
},
|
|
14
|
+
"./tools": {
|
|
15
|
+
"import": "./dist/tools/index.js",
|
|
16
|
+
"types": "./dist/tools/index.d.ts"
|
|
17
|
+
},
|
|
18
|
+
"./hooks": {
|
|
19
|
+
"import": "./dist/hooks/index.js",
|
|
20
|
+
"types": "./dist/hooks/index.d.ts"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup",
|
|
25
|
+
"dev": "tsup --watch"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"registry.json",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@polpo-ai/react": ">=0.5.0",
|
|
35
|
+
"@polpo-ai/sdk": ">=0.5.0",
|
|
36
|
+
"lucide-react": ">=0.400.0",
|
|
37
|
+
"react": ">=18.0.0",
|
|
38
|
+
"react-virtuoso": ">=4.0.0",
|
|
39
|
+
"streamdown": ">=0.1.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/react": "^19.2.14",
|
|
43
|
+
"@types/react-dom": "^19.2.3",
|
|
44
|
+
"tsup": "^8.5.1",
|
|
45
|
+
"typescript": "^6.0.2"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"polpo",
|
|
49
|
+
"chat",
|
|
50
|
+
"ai",
|
|
51
|
+
"react",
|
|
52
|
+
"components"
|
|
53
|
+
],
|
|
54
|
+
"license": "MIT"
|
|
55
|
+
}
|
package/registry.json
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "polpo-chat-lib",
|
|
4
|
+
"type": "registry:lib",
|
|
5
|
+
"title": "Polpo Chat Lib",
|
|
6
|
+
"description": "Utility functions for chat message content extraction and relative time formatting.",
|
|
7
|
+
"dependencies": ["@polpo-ai/sdk"],
|
|
8
|
+
"registryDependencies": [],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "lib/polpo-chat/relative-time.ts",
|
|
12
|
+
"type": "registry:lib",
|
|
13
|
+
"target": "lib/polpo-chat/relative-time.ts",
|
|
14
|
+
"content": "/** Format an ISO timestamp as a human-readable relative time string. */\nexport function relativeTime(iso: string): string {\n const now = Date.now();\n const then = new Date(iso).getTime();\n const diff = Math.floor((now - then) / 1000);\n if (diff < 10) return \"Just now\";\n if (diff < 60) return `${diff}s ago`;\n const mins = Math.floor(diff / 60);\n if (mins < 60) return mins === 1 ? \"A minute ago\" : `${mins}m ago`;\n const hours = Math.floor(mins / 60);\n if (hours < 24) return hours === 1 ? \"An hour ago\" : `${hours}h ago`;\n return new Date(iso).toLocaleString(undefined, { month: \"short\", day: \"numeric\", hour: \"2-digit\", minute: \"2-digit\" });\n}\n"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"path": "lib/polpo-chat/get-text-content.ts",
|
|
18
|
+
"type": "registry:lib",
|
|
19
|
+
"target": "lib/polpo-chat/get-text-content.ts",
|
|
20
|
+
"content": "import type { ContentPart } from \"@polpo-ai/sdk\";\n\n/** Extract the concatenated text from a message content value (string or ContentPart[]). */\nexport function getTextContent(content: string | ContentPart[]): string {\n if (typeof content === \"string\") return content;\n return content\n .filter((p): p is { type: \"text\"; text: string } => p.type === \"text\")\n .map((p) => p.text)\n .join(\"\");\n}\n"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "polpo-chat-hooks",
|
|
26
|
+
"type": "registry:hook",
|
|
27
|
+
"title": "Polpo Chat Hooks",
|
|
28
|
+
"description": "Hooks for chat submit handling and document-level drag detection.",
|
|
29
|
+
"dependencies": ["@polpo-ai/sdk"],
|
|
30
|
+
"registryDependencies": [],
|
|
31
|
+
"files": [
|
|
32
|
+
{
|
|
33
|
+
"path": "hooks/polpo-chat/use-submit-handler.ts",
|
|
34
|
+
"type": "registry:hook",
|
|
35
|
+
"target": "hooks/polpo-chat/use-submit-handler.ts",
|
|
36
|
+
"content": "\"use client\";\n\nimport { useCallback } from \"react\";\nimport type { ContentPart } from \"@polpo-ai/sdk\";\n\n/** Shape of the message emitted by the PromptInput component. */\nexport interface PromptInputMessage {\n text: string;\n files: { url: string; filename?: string }[];\n}\n\n/** Shared submit handler — uploads files via SDK then sends ContentPart[] */\nexport function useSubmitHandler(\n sendMessage: (content: string | ContentPart[]) => Promise<void>,\n uploadFile: (destPath: string, file: Blob, filename: string) => Promise<unknown>,\n) {\n return useCallback(async (message: PromptInputMessage) => {\n const text = message.text.trim();\n const files = message.files || [];\n if (!text && files.length === 0) return;\n\n if (files.length > 0) {\n const parts: ContentPart[] = [];\n if (text) parts.push({ type: \"text\", text });\n for (const f of files) {\n const name = f.filename || \"upload\";\n try {\n const res = await fetch(f.url);\n const blob = await res.blob();\n await uploadFile(\"workspace\", blob, name);\n parts.push({ type: \"file\", file_id: `workspace/${name}` });\n } catch { /* skip failed uploads */ }\n }\n if (parts.length > 0) sendMessage(parts);\n } else {\n sendMessage(text);\n }\n }, [sendMessage, uploadFile]);\n}\n"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"path": "hooks/polpo-chat/use-document-drag.ts",
|
|
40
|
+
"type": "registry:hook",
|
|
41
|
+
"target": "hooks/polpo-chat/use-document-drag.ts",
|
|
42
|
+
"content": "\"use client\";\n\nimport { useState, useRef, useEffect } from \"react\";\n\n/** Track document-level drag state for visual feedback */\nexport function useDocumentDrag() {\n const [dragging, setDragging] = useState(false);\n const counterRef = useRef(0);\n useEffect(() => {\n const onEnter = (e: DragEvent) => { if (e.dataTransfer?.types?.includes(\"Files\")) { counterRef.current++; setDragging(true); } };\n const onLeave = () => { counterRef.current--; if (counterRef.current === 0) setDragging(false); };\n const onDrop = () => { counterRef.current = 0; setDragging(false); };\n document.addEventListener(\"dragenter\", onEnter);\n document.addEventListener(\"dragleave\", onLeave);\n document.addEventListener(\"drop\", onDrop);\n return () => { document.removeEventListener(\"dragenter\", onEnter); document.removeEventListener(\"dragleave\", onLeave); document.removeEventListener(\"drop\", onDrop); };\n }, []);\n return dragging;\n}\n"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "polpo-chat-streamdown",
|
|
48
|
+
"type": "registry:ui",
|
|
49
|
+
"title": "Polpo Chat Streamdown",
|
|
50
|
+
"description": "Streamdown code block override component for rendering fenced code blocks in chat messages.",
|
|
51
|
+
"dependencies": ["streamdown"],
|
|
52
|
+
"registryDependencies": [],
|
|
53
|
+
"files": [
|
|
54
|
+
{
|
|
55
|
+
"path": "components/polpo-chat/streamdown-code.tsx",
|
|
56
|
+
"type": "registry:ui",
|
|
57
|
+
"target": "components/polpo-chat/streamdown-code.tsx",
|
|
58
|
+
"content": "\"use client\";\n\nimport type { ComponentType, HTMLAttributes } from \"react\";\n\n/* ------------------------------------------------------------------ */\n/* Types */\n/* ------------------------------------------------------------------ */\n\n/** Props that any code-block component must accept. */\nexport interface CodeBlockComponentProps {\n code: string;\n language: string;\n children?: React.ReactNode;\n}\n\n/** The shape returned by createStreamdownComponents / the default export. */\nexport interface StreamdownComponents {\n code: (\n props: HTMLAttributes<HTMLElement> & {\n node?: unknown;\n \"data-block\"?: string;\n },\n ) => React.ReactNode;\n}\n\n/* ------------------------------------------------------------------ */\n/* Fallback code block (simple <pre><code>) */\n/* ------------------------------------------------------------------ */\n\nfunction FallbackCodeBlock({ code, language }: CodeBlockComponentProps) {\n return (\n <pre\n data-language={language}\n style={{\n margin: \"0.75rem 0\",\n padding: \"0.875rem 1rem\",\n borderRadius: \"10px\",\n overflowX: \"auto\",\n border: \"1px solid var(--line, #e5e7eb)\",\n background: \"var(--bg, #f9fafb)\",\n color: \"var(--ink, inherit)\",\n fontSize: \"0.75rem\",\n lineHeight: \"1.625\",\n }}\n >\n <code className={`language-${language}`}>{code}</code>\n </pre>\n );\n}\n\n/* ------------------------------------------------------------------ */\n/* Factory */\n/* ------------------------------------------------------------------ */\n\n/**\n * Create a `streamdownComponents` override object for Streamdown.\n *\n * Pass your own `CodeBlock` component to get syntax-highlighted fenced\n * code blocks. If omitted, a plain `<pre><code>` fallback is used.\n *\n * @example\n * ```tsx\n * import { CodeBlock } from \"@/components/ai-elements/code-block\";\n * import { createStreamdownComponents } from \"@polpo-ai/chat\";\n *\n * const streamdownComponents = createStreamdownComponents(CodeBlock);\n * ```\n */\nexport function createStreamdownComponents(\n CodeBlockComponent?: ComponentType<CodeBlockComponentProps>,\n): StreamdownComponents {\n const Block = CodeBlockComponent ?? FallbackCodeBlock;\n\n function StreamdownCode(\n props: HTMLAttributes<HTMLElement> & {\n node?: unknown;\n \"data-block\"?: string;\n },\n ) {\n const {\n children,\n className,\n node: _,\n \"data-block\": dataBlock,\n ...rest\n } = props;\n\n // Fenced code blocks carry data-block; inline code does not.\n if (dataBlock !== undefined) {\n const match = /language-(\\w+)/.exec(className || \"\");\n const lang = match?.[1] || \"text\";\n const code = String(children).replace(/\\n$/, \"\");\n return <Block code={code} language={lang} />;\n }\n\n return (\n <code className={className} {...rest}>\n {children}\n </code>\n );\n }\n\n return { code: StreamdownCode };\n}\n\n/* ------------------------------------------------------------------ */\n/* Pre-built instance — uses the plain fallback */\n/* ------------------------------------------------------------------ */\n\nexport const streamdownComponents: StreamdownComponents =\n createStreamdownComponents();\n"
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "polpo-chat-tools",
|
|
64
|
+
"type": "registry:ui",
|
|
65
|
+
"title": "Polpo Chat Tools",
|
|
66
|
+
"description": "Tool call renderer components for chat messages: shell, dispatcher, and specialized renderers for bash, read, write, search, HTTP, email, and ask-user tools.",
|
|
67
|
+
"dependencies": ["@polpo-ai/sdk", "lucide-react"],
|
|
68
|
+
"registryDependencies": [],
|
|
69
|
+
"files": [
|
|
70
|
+
{
|
|
71
|
+
"path": "components/polpo-chat/tools/tool-call-shell.tsx",
|
|
72
|
+
"type": "registry:ui",
|
|
73
|
+
"target": "components/polpo-chat/tools/tool-call-shell.tsx",
|
|
74
|
+
"content": "\"use client\";\n\nimport { useState, type ReactNode } from \"react\";\nimport type { ToolCallEvent } from \"@polpo-ai/sdk\";\nimport { ChevronRight, Loader2, Check, AlertCircle, type LucideIcon } from \"lucide-react\";\n\n// ── Shell props ──\n\nexport interface ToolCallShellProps {\n tool: ToolCallEvent;\n icon: LucideIcon;\n label: string;\n /** One-line summary shown next to the label */\n summary?: string | null;\n /** Custom expanded content — replaces default raw result */\n children?: ReactNode;\n}\n\n// ── Shell ──\n\nexport function ToolCallShell({ tool, icon: Icon, label, summary, children }: ToolCallShellProps) {\n const [expanded, setExpanded] = useState(false);\n const isPending = tool.state === \"calling\" || tool.state === \"preparing\";\n const isError = tool.state === \"error\";\n const isDone = tool.state === \"completed\";\n const hasContent = isDone && (children || tool.result);\n\n return (\n <div className={`flex flex-col rounded-lg bg-p-warm border border-p-line text-[13px] text-p-ink-2 overflow-hidden ${isError ? \"border-destructive/20 bg-destructive/5\" : \"\"}`}>\n <button\n className={`flex items-center gap-2 px-3 py-2 border-none bg-transparent font-inherit text-inherit text-left w-full ${hasContent ? \"cursor-pointer\" : \"cursor-default\"}`}\n onClick={() => hasContent && setExpanded(!expanded)}\n >\n <Icon size={14} className=\"shrink-0\" />\n <span className=\"font-medium text-p-ink whitespace-nowrap\">{label}</span>\n {summary ? <span className=\"text-p-ink-3 text-xs overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]\">{summary}</span> : null}\n {isPending ? <Loader2 size={14} className=\"animate-spin text-p-accent shrink-0\" /> : null}\n {isDone ? <Check size={14} className=\"text-p-green shrink-0\" /> : null}\n {isError ? <AlertCircle size={14} className=\"text-destructive shrink-0\" /> : null}\n {hasContent ? (\n <ChevronRight size={12} className={`ml-auto text-p-ink-3 shrink-0 transition-transform duration-150 ${expanded ? \"rotate-90\" : \"\"}`} />\n ) : null}\n </button>\n {expanded ? (\n <div className=\"border-t border-p-line\">\n {children || (\n <pre className=\"m-0 px-2.5 py-2 text-[11px] leading-normal font-mono text-p-ink-2 bg-p-bg whitespace-pre-wrap break-all max-h-[180px] overflow-y-auto\">\n {tool.result}\n </pre>\n )}\n </div>\n ) : null}\n </div>\n );\n}\n"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"path": "components/polpo-chat/tools/tool-ask-user.tsx",
|
|
78
|
+
"type": "registry:ui",
|
|
79
|
+
"target": "components/polpo-chat/tools/tool-ask-user.tsx",
|
|
80
|
+
"content": "\"use client\";\n\nimport type { ToolCallEvent } from \"@polpo-ai/sdk\";\nimport { MessageSquareMore, Check } from \"lucide-react\";\nimport { ToolCallShell } from \"./tool-call-shell\";\n\ninterface AskUserQuestion {\n id: string;\n question: string;\n header?: string;\n options?: { label: string; description?: string }[];\n}\n\n/** Ask user question tool — shows the questions and answers after completion */\nexport function ToolAskUser({ tool }: { tool: ToolCallEvent }) {\n const questions = (tool.arguments?.questions || []) as AskUserQuestion[];\n const isAnswered = tool.state === \"completed\" || tool.state === \"interrupted\";\n\n // Try to parse the result as answers\n let answers: { questionId: string; selected: string[] }[] = [];\n if (tool.result) {\n try {\n const parsed = JSON.parse(tool.result);\n answers = parsed.answers || [];\n } catch {\n // result might be plain text — not structured\n }\n }\n\n return (\n <ToolCallShell\n tool={tool}\n icon={MessageSquareMore}\n label=\"Question\"\n summary={isAnswered ? `${questions.length} answered` : `${questions.length} question${questions.length > 1 ? \"s\" : \"\"}`}\n >\n <div className=\"bg-p-bg px-3 py-2 text-xs space-y-2\">\n {questions.map((q) => {\n const answer = answers.find((a) => a.questionId === q.id);\n return (\n <div key={q.id} className=\"flex items-start gap-2\">\n {isAnswered ? (\n <Check size={12} className=\"text-p-green shrink-0 mt-0.5\" />\n ) : (\n <MessageSquareMore size={12} className=\"text-p-accent shrink-0 mt-0.5\" />\n )}\n <div className=\"min-w-0\">\n <p className=\"text-p-ink font-medium\">{q.question}</p>\n {answer && answer.selected.length > 0 && (\n <p className=\"text-p-ink-3 mt-0.5\">{answer.selected.join(\", \")}</p>\n )}\n {!answer && isAnswered && (\n <p className=\"text-p-ink-3/50 italic mt-0.5\">Skipped</p>\n )}\n </div>\n </div>\n );\n })}\n </div>\n </ToolCallShell>\n );\n}\n"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"path": "components/polpo-chat/tools/tool-bash.tsx",
|
|
84
|
+
"type": "registry:ui",
|
|
85
|
+
"target": "components/polpo-chat/tools/tool-bash.tsx",
|
|
86
|
+
"content": "\"use client\";\n\nimport type { ToolCallEvent } from \"@polpo-ai/sdk\";\nimport { Terminal } from \"lucide-react\";\nimport { ToolCallShell } from \"./tool-call-shell\";\n\n/** Bash tool — shows command and output */\nexport function ToolBash({ tool }: { tool: ToolCallEvent }) {\n const command = (tool.arguments?.command) as string | undefined;\n const lines = tool.result?.split(\"\\n\") || [];\n const maxLines = 10;\n const truncated = lines.length > maxLines;\n\n return (\n <ToolCallShell tool={tool} icon={Terminal} label=\"Bash\" summary={command?.slice(0, 60)}>\n <div className=\"bg-[#1a1a1a] max-h-[220px] overflow-y-auto\">\n {command && (\n <div className=\"px-3 py-1.5 text-[11px] font-mono text-emerald-400 border-b border-white/10\">\n <span className=\"text-p-ink-3 select-none\">$ </span>{command}\n </div>\n )}\n {tool.result && (\n <pre className=\"m-0 px-3 py-1.5 text-[11px] leading-normal font-mono text-neutral-300 whitespace-pre-wrap break-all\">\n {lines.slice(0, maxLines).join(\"\\n\")}\n {truncated ? `\\n… +${lines.length - maxLines} lines` : \"\"}\n </pre>\n )}\n </div>\n </ToolCallShell>\n );\n}\n"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"path": "components/polpo-chat/tools/tool-read.tsx",
|
|
90
|
+
"type": "registry:ui",
|
|
91
|
+
"target": "components/polpo-chat/tools/tool-read.tsx",
|
|
92
|
+
"content": "\"use client\";\n\nimport type { ToolCallEvent } from \"@polpo-ai/sdk\";\nimport { FileText } from \"lucide-react\";\nimport { ToolCallShell } from \"./tool-call-shell\";\n\n/** Read tool — shows file path and truncated content with line numbers */\nexport function ToolRead({ tool }: { tool: ToolCallEvent }) {\n const path = (tool.arguments?.path || tool.arguments?.file_path) as string | undefined;\n const lines = tool.result?.split(\"\\n\") || [];\n const maxLines = 12;\n const truncated = lines.length > maxLines;\n\n return (\n <ToolCallShell tool={tool} icon={FileText} label=\"Read\" summary={path}>\n {tool.result && (\n <div className=\"bg-p-bg max-h-[220px] overflow-y-auto\">\n <table className=\"w-full text-[11px] leading-relaxed font-mono border-collapse\">\n <tbody>\n {lines.slice(0, maxLines).map((line, i) => (\n <tr key={i} className=\"hover:bg-p-warm/50\">\n <td className=\"text-right text-p-ink-3 select-none px-2.5 py-0 w-[1%] whitespace-nowrap\">{i + 1}</td>\n <td className=\"text-p-ink-2 px-2.5 py-0 whitespace-pre-wrap break-all\">{line}</td>\n </tr>\n ))}\n </tbody>\n </table>\n {truncated && (\n <div className=\"px-2.5 py-1 text-[10px] text-p-ink-3 border-t border-p-line\">\n +{lines.length - maxLines} more lines\n </div>\n )}\n </div>\n )}\n </ToolCallShell>\n );\n}\n"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"path": "components/polpo-chat/tools/tool-write.tsx",
|
|
96
|
+
"type": "registry:ui",
|
|
97
|
+
"target": "components/polpo-chat/tools/tool-write.tsx",
|
|
98
|
+
"content": "\"use client\";\n\nimport type { ToolCallEvent } from \"@polpo-ai/sdk\";\nimport { Pen } from \"lucide-react\";\nimport { ToolCallShell } from \"./tool-call-shell\";\n\n/** Write/Edit tool — shows file path and a diff-like preview */\nexport function ToolWrite({ tool }: { tool: ToolCallEvent }) {\n const path = (tool.arguments?.path || tool.arguments?.file_path) as string | undefined;\n const content = (tool.arguments?.content || tool.arguments?.new_string) as string | undefined;\n const preview = content?.slice(0, 200);\n\n return (\n <ToolCallShell tool={tool} icon={Pen} label={tool.name === \"edit\" ? \"Edit\" : \"Write\"} summary={path}>\n {preview && (\n <div className=\"bg-p-bg max-h-[180px] overflow-y-auto\">\n <pre className=\"m-0 px-2.5 py-2 text-[11px] leading-normal font-mono text-p-green whitespace-pre-wrap break-all\">\n {preview}{content && content.length > 200 ? \"\\n…\" : \"\"}\n </pre>\n </div>\n )}\n </ToolCallShell>\n );\n}\n"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"path": "components/polpo-chat/tools/tool-search.tsx",
|
|
102
|
+
"type": "registry:ui",
|
|
103
|
+
"target": "components/polpo-chat/tools/tool-search.tsx",
|
|
104
|
+
"content": "\"use client\";\n\nimport type { ToolCallEvent } from \"@polpo-ai/sdk\";\nimport { Search, FolderSearch } from \"lucide-react\";\nimport { ToolCallShell } from \"./tool-call-shell\";\n\n/** Grep/Glob/Search tool — shows query/pattern and matched results */\nexport function ToolSearch({ tool }: { tool: ToolCallEvent }) {\n const isGlob = tool.name === \"glob\";\n const pattern = (tool.arguments?.pattern || tool.arguments?.query || tool.arguments?.q) as string | undefined;\n const path = (tool.arguments?.path || tool.arguments?.root) as string | undefined;\n const summary = pattern ? `${pattern}${path ? ` in ${path}` : \"\"}` : path;\n\n const lines = tool.result?.split(\"\\n\").filter(Boolean) || [];\n const maxItems = 8;\n const truncated = lines.length > maxItems;\n\n return (\n <ToolCallShell tool={tool} icon={isGlob ? FolderSearch : Search} label={isGlob ? \"Glob\" : tool.name === \"grep\" ? \"Grep\" : \"Search\"} summary={summary}>\n {lines.length > 0 && (\n <div className=\"bg-p-bg max-h-[200px] overflow-y-auto\">\n <ul className=\"list-none m-0 p-0\">\n {lines.slice(0, maxItems).map((line, i) => (\n <li key={i} className=\"px-2.5 py-0.5 text-xs font-mono text-p-ink-2 border-b border-p-line/50 last:border-b-0 hover:bg-p-warm/50 truncate\">\n {line}\n </li>\n ))}\n </ul>\n {truncated && (\n <div className=\"px-2.5 py-1 text-[10px] text-p-ink-3 border-t border-p-line\">\n +{lines.length - maxItems} more results\n </div>\n )}\n </div>\n )}\n </ToolCallShell>\n );\n}\n"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"path": "components/polpo-chat/tools/tool-http.tsx",
|
|
108
|
+
"type": "registry:ui",
|
|
109
|
+
"target": "components/polpo-chat/tools/tool-http.tsx",
|
|
110
|
+
"content": "\"use client\";\n\nimport type { ToolCallEvent } from \"@polpo-ai/sdk\";\nimport { Globe } from \"lucide-react\";\nimport { ToolCallShell } from \"./tool-call-shell\";\n\n/** HTTP fetch/download/search_web tool — shows URL and response preview */\nexport function ToolHttp({ tool }: { tool: ToolCallEvent }) {\n const url = (tool.arguments?.url) as string | undefined;\n const method = (tool.arguments?.method as string)?.toUpperCase() || \"GET\";\n const summary = url ? `${method} ${url}` : null;\n\n return (\n <ToolCallShell tool={tool} icon={Globe} label={tool.name === \"search_web\" ? \"Search Web\" : \"HTTP\"} summary={summary}>\n {tool.result && (\n <pre className=\"m-0 px-2.5 py-2 text-[11px] leading-normal font-mono text-p-ink-2 bg-p-bg whitespace-pre-wrap break-all max-h-[180px] overflow-y-auto\">\n {tool.result.slice(0, 500)}{tool.result.length > 500 ? \"\\n…\" : \"\"}\n </pre>\n )}\n </ToolCallShell>\n );\n}\n"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"path": "components/polpo-chat/tools/tool-email.tsx",
|
|
114
|
+
"type": "registry:ui",
|
|
115
|
+
"target": "components/polpo-chat/tools/tool-email.tsx",
|
|
116
|
+
"content": "\"use client\";\n\nimport type { ToolCallEvent } from \"@polpo-ai/sdk\";\nimport { Mail } from \"lucide-react\";\nimport { ToolCallShell } from \"./tool-call-shell\";\n\n/** Email send tool — shows recipient, subject, and body preview */\nexport function ToolEmail({ tool }: { tool: ToolCallEvent }) {\n const to = (tool.arguments?.to || tool.arguments?.recipient) as string | undefined;\n const subject = (tool.arguments?.subject) as string | undefined;\n const body = (tool.arguments?.body || tool.arguments?.content) as string | undefined;\n const summary = to ? `→ ${to}` : null;\n\n return (\n <ToolCallShell tool={tool} icon={Mail} label=\"Email\" summary={summary}>\n <div className=\"bg-p-bg px-2.5 py-2 text-xs max-h-[180px] overflow-y-auto\">\n {subject && (\n <div className=\"mb-1\">\n <span className=\"text-p-ink-3\">Subject: </span>\n <span className=\"text-p-ink font-medium\">{subject}</span>\n </div>\n )}\n {to && (\n <div className=\"mb-1.5\">\n <span className=\"text-p-ink-3\">To: </span>\n <span className=\"text-p-ink-2\">{to}</span>\n </div>\n )}\n {body && (\n <p className=\"m-0 text-p-ink-2 leading-relaxed whitespace-pre-wrap\">\n {body.slice(0, 300)}{body.length > 300 ? \"…\" : \"\"}\n </p>\n )}\n </div>\n </ToolCallShell>\n );\n}\n"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"path": "components/polpo-chat/tools/index.tsx",
|
|
120
|
+
"type": "registry:ui",
|
|
121
|
+
"target": "components/polpo-chat/tools/index.tsx",
|
|
122
|
+
"content": "\"use client\";\n\nimport type { ToolCallEvent } from \"@polpo-ai/sdk\";\nimport { Wrench } from \"lucide-react\";\nimport { ToolCallShell } from \"./tool-call-shell\";\nimport { ToolRead } from \"./tool-read\";\nimport { ToolWrite } from \"./tool-write\";\nimport { ToolBash } from \"./tool-bash\";\nimport { ToolSearch } from \"./tool-search\";\nimport { ToolHttp } from \"./tool-http\";\nimport { ToolEmail } from \"./tool-email\";\nimport { ToolAskUser } from \"./tool-ask-user\";\n\n// ── Tool name → component map ──\n\nconst TOOL_COMPONENTS: Record<string, React.ComponentType<{ tool: ToolCallEvent }>> = {\n ask_user_question: ToolAskUser,\n read: ToolRead,\n read_attachment: ToolRead,\n write: ToolWrite,\n edit: ToolWrite,\n bash: ToolBash,\n grep: ToolSearch,\n glob: ToolSearch,\n search_web: ToolHttp,\n http_fetch: ToolHttp,\n http_download: ToolHttp,\n email_send: ToolEmail,\n};\n\n// Prefix-based matching for tools like browser_*, memory_*, etc.\nconst TOOL_PREFIX_COMPONENTS: { prefix: string; component: React.ComponentType<{ tool: ToolCallEvent }> }[] = [\n { prefix: \"browser_\", component: ToolHttp },\n { prefix: \"email_\", component: ToolEmail },\n { prefix: \"search_\", component: ToolSearch },\n];\n\nfunction getToolLabel(name: string) {\n return name.replace(/_/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\n// ── Dispatcher ──\n\nexport function ToolCallChip({ tool }: { tool: ToolCallEvent }) {\n // Exact match\n const Exact = TOOL_COMPONENTS[tool.name];\n if (Exact) return <Exact tool={tool} />;\n\n // Prefix match\n for (const { prefix, component: Prefixed } of TOOL_PREFIX_COMPONENTS) {\n if (tool.name.startsWith(prefix)) return <Prefixed tool={tool} />;\n }\n\n // Generic fallback\n const summary = tool.arguments\n ? Object.values(tool.arguments).find((v) => typeof v === \"string\" && v.length > 0) as string | undefined\n : undefined;\n\n return (\n <ToolCallShell\n tool={tool}\n icon={Wrench}\n label={getToolLabel(tool.name)}\n summary={summary?.slice(0, 80)}\n />\n );\n}\n\n// Re-export shell for custom usage\nexport { ToolCallShell } from \"./tool-call-shell\";\n"
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"name": "polpo-chat-message",
|
|
128
|
+
"type": "registry:ui",
|
|
129
|
+
"title": "Polpo Chat Message",
|
|
130
|
+
"description": "Chat message components: ChatMessage dispatcher, ChatUserMessage bubble, and ChatAssistantMessage with avatar, tool calls, and streamdown rendering.",
|
|
131
|
+
"dependencies": ["@polpo-ai/sdk", "lucide-react", "streamdown"],
|
|
132
|
+
"registryDependencies": ["polpo-chat-lib", "polpo-chat-tools", "polpo-chat-streamdown"],
|
|
133
|
+
"files": [
|
|
134
|
+
{
|
|
135
|
+
"path": "components/polpo-chat/chat-typing.tsx",
|
|
136
|
+
"type": "registry:ui",
|
|
137
|
+
"target": "components/polpo-chat/chat-typing.tsx",
|
|
138
|
+
"content": "\"use client\";\n\n/* ------------------------------------------------------------------ */\n/* Typing indicator (three animated dots) */\n/* ------------------------------------------------------------------ */\n\nconst dotBase =\n \"inline-block h-1.5 w-1.5 rounded-full bg-current opacity-40 animate-[typing-dot_1.4s_ease-in-out_infinite]\";\n\nexport function ChatTyping({ className }: { className?: string }) {\n return (\n <span\n role=\"status\"\n aria-label=\"Typing\"\n className={`inline-flex items-center gap-1 ${className ?? \"\"}`}\n >\n <span className={dotBase} style={{ animationDelay: \"0ms\" }} />\n <span className={dotBase} style={{ animationDelay: \"200ms\" }} />\n <span className={dotBase} style={{ animationDelay: \"400ms\" }} />\n </span>\n );\n}\n"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"path": "components/polpo-chat/chat-message.tsx",
|
|
142
|
+
"type": "registry:ui",
|
|
143
|
+
"target": "components/polpo-chat/chat-message.tsx",
|
|
144
|
+
"content": "\"use client\";\n\nimport {\n memo,\n useState,\n useCallback,\n type ReactNode,\n} from \"react\";\nimport type { ContentPart, ToolCallEvent } from \"@polpo-ai/sdk\";\nimport { Copy, Check, FileCode } from \"lucide-react\";\nimport { Streamdown } from \"streamdown\";\nimport { getTextContent } from \"@/lib/polpo-chat/get-text-content\";\nimport { relativeTime } from \"@/lib/polpo-chat/relative-time\";\nimport { ToolCallChip } from \"@/components/polpo-chat/tools\";\nimport { ChatTyping } from \"./chat-typing\";\n\n/** Components override accepted by Streamdown — use Record for flexibility. */\ntype StreamdownComponentsProp = Record<string, unknown>;\n\n/* ------------------------------------------------------------------ */\n/* Types */\n/* ------------------------------------------------------------------ */\n\nexport interface ChatMessageItemData {\n id?: string;\n role: \"user\" | \"assistant\";\n content: string | ContentPart[];\n ts?: string;\n toolCalls?: ToolCallEvent[];\n}\n\nexport interface ChatMessageProps {\n msg: ChatMessageItemData;\n isLast?: boolean;\n isStreaming?: boolean;\n avatar?: ReactNode;\n agentName?: string;\n streamdownComponents?: StreamdownComponentsProp;\n}\n\n/* ------------------------------------------------------------------ */\n/* CopyButton */\n/* ------------------------------------------------------------------ */\n\nfunction CopyButton({ text }: { text: string }) {\n const [copied, setCopied] = useState(false);\n\n const handleCopy = useCallback(() => {\n navigator.clipboard.writeText(text);\n setCopied(true);\n setTimeout(() => setCopied(false), 1500);\n }, [text]);\n\n return (\n <button\n type=\"button\"\n onClick={handleCopy}\n aria-label=\"Copy message\"\n className=\"inline-flex items-center justify-center rounded-md p-1 text-[var(--ink-3)] hover:text-[var(--ink)] hover:bg-[var(--warm)] transition-colors\"\n >\n {copied ? <Check className=\"size-3.5\" /> : <Copy className=\"size-3.5\" />}\n </button>\n );\n}\n\n/* ------------------------------------------------------------------ */\n/* File / image content parts */\n/* ------------------------------------------------------------------ */\n\nfunction ContentParts({\n parts,\n align,\n}: {\n parts: ContentPart[];\n align: \"start\" | \"end\";\n}) {\n const nonText = parts.filter((p) => p.type !== \"text\");\n if (nonText.length === 0) return null;\n\n return (\n <div\n className={`flex flex-wrap gap-1.5 mb-1 ${align === \"end\" ? \"justify-end\" : \"\"}`}\n >\n {nonText.map((part, i) => {\n if (part.type === \"image_url\") {\n return (\n <a\n key={i}\n href={(part as { type: \"image_url\"; image_url: { url: string } }).image_url.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"rounded-lg overflow-hidden max-w-[200px]\"\n >\n <img\n src={(part as { type: \"image_url\"; image_url: { url: string } }).image_url.url}\n alt=\"\"\n className=\"w-full h-auto block\"\n />\n </a>\n );\n }\n if (part.type === \"file\") {\n const fileId = (part as { type: \"file\"; file_id: string }).file_id;\n const fn = fileId.split(\"/\").pop() || fileId;\n return (\n <a\n key={i}\n href={`/api/polpo/files/read?path=${encodeURIComponent(fileId)}`}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-1.5 bg-[var(--warm)] border border-[var(--line)] rounded-lg px-2.5 py-1.5 text-xs text-[var(--ink)] hover:border-[var(--ink-3)] transition-colors\"\n >\n <FileCode size={13} />\n <span className=\"truncate max-w-[120px]\">{fn}</span>\n </a>\n );\n }\n return null;\n })}\n </div>\n );\n}\n\n/* ------------------------------------------------------------------ */\n/* ChatUserMessage */\n/* ------------------------------------------------------------------ */\n\nexport const ChatUserMessage = memo(\n function ChatUserMessage({\n msg,\n isLast,\n isStreaming,\n }: {\n msg: ChatMessageItemData;\n isLast?: boolean;\n isStreaming?: boolean;\n }) {\n const text = getTextContent(msg.content);\n\n return (\n <div className=\"w-full px-6 py-3\">\n <div className=\"max-w-3xl mx-auto\">\n <div className=\"group flex w-full flex-col gap-2 ml-auto justify-end\">\n {/* File/image parts */}\n {Array.isArray(msg.content) && (\n <ContentParts parts={msg.content} align=\"end\" />\n )}\n\n {/* Message bubble */}\n <div className=\"w-fit max-w-[80%] ml-auto rounded-[18px_18px_4px_18px] bg-[var(--warm)] px-4 py-3\">\n {text ? (\n <p className=\"whitespace-pre-wrap break-words text-[var(--ink)]\">\n {text}\n </p>\n ) : null}\n </div>\n\n {/* Hover actions: timestamp + copy */}\n {text && (!isLast || !isStreaming) && (\n <div className=\"flex items-center justify-end gap-1.5 h-6\">\n <span className=\"text-[11px] text-[var(--ink-3)] opacity-0 group-hover:opacity-100 transition-opacity\">\n {msg.ts ? relativeTime(msg.ts) : \"\"}\n </span>\n <div className=\"opacity-0 group-hover:opacity-100 transition-opacity\">\n <CopyButton text={text} />\n </div>\n </div>\n )}\n </div>\n </div>\n </div>\n );\n },\n (prev, next) =>\n prev.isLast === next.isLast &&\n prev.isStreaming === next.isStreaming &&\n prev.msg.id === next.msg.id &&\n prev.msg.content === next.msg.content,\n);\n\n/* ------------------------------------------------------------------ */\n/* ChatAssistantMessage */\n/* ------------------------------------------------------------------ */\n\nexport const ChatAssistantMessage = memo(\n function ChatAssistantMessage({\n msg,\n isLast,\n isStreaming,\n avatar,\n agentName,\n streamdownComponents: components,\n }: {\n msg: ChatMessageItemData;\n isLast?: boolean;\n isStreaming?: boolean;\n avatar?: ReactNode;\n agentName?: string;\n streamdownComponents?: StreamdownComponentsProp;\n }) {\n const text = getTextContent(msg.content);\n const filteredToolCalls = msg.toolCalls?.filter(\n (tc) => tc.name !== \"ask_user_question\",\n );\n\n return (\n <div className=\"w-full px-6 pt-4 pb-6\">\n <div className=\"max-w-3xl mx-auto\">\n <div className=\"group flex w-full flex-col gap-2\">\n {/* Avatar + name header */}\n {(avatar || agentName) && (\n <div className=\"flex items-center gap-2 mb-1\">\n {avatar}\n {agentName && (\n <span className=\"font-display text-[13px] font-semibold text-[var(--ink)]\">\n {agentName}\n </span>\n )}\n </div>\n )}\n\n {/* Tool calls */}\n {filteredToolCalls && filteredToolCalls.length > 0 && (\n <div className=\"flex flex-col gap-1 mb-1\">\n {filteredToolCalls.map((tc) => (\n <ToolCallChip key={tc.id} tool={tc} />\n ))}\n </div>\n )}\n\n {/* File/image parts */}\n {Array.isArray(msg.content) && (\n <ContentParts parts={msg.content} align=\"start\" />\n )}\n\n {/* Text content or typing dots */}\n <div className=\"w-full text-[var(--ink)]\">\n {text ? (\n components ? (\n <Streamdown\n className=\"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0\"\n components={components as any}\n >\n {text}\n </Streamdown>\n ) : (\n <p className=\"whitespace-pre-wrap break-words\">{text}</p>\n )\n ) : (\n !filteredToolCalls?.length && <ChatTyping className=\"pt-1\" />\n )}\n </div>\n\n {/* Hover action: copy */}\n {text && (!isLast || !isStreaming) && (\n <div className=\"h-6 flex items-center opacity-0 group-hover:opacity-100 transition-opacity\">\n <CopyButton text={text} />\n </div>\n )}\n </div>\n </div>\n </div>\n );\n },\n (prev, next) =>\n prev.avatar === next.avatar &&\n prev.agentName === next.agentName &&\n prev.isLast === next.isLast &&\n prev.isStreaming === next.isStreaming &&\n prev.streamdownComponents === next.streamdownComponents &&\n prev.msg.id === next.msg.id &&\n prev.msg.content === next.msg.content &&\n prev.msg.toolCalls?.length === next.msg.toolCalls?.length &&\n JSON.stringify(prev.msg.toolCalls?.map((t) => t.state)) ===\n JSON.stringify(next.msg.toolCalls?.map((t) => t.state)),\n);\n\n/* ------------------------------------------------------------------ */\n/* ChatMessage — dispatcher */\n/* ------------------------------------------------------------------ */\n\nexport const ChatMessage = memo(\n function ChatMessage({\n msg,\n isLast,\n isStreaming,\n avatar,\n agentName,\n streamdownComponents,\n }: ChatMessageProps) {\n if (msg.role === \"user\") {\n return (\n <ChatUserMessage msg={msg} isLast={isLast} isStreaming={isStreaming} />\n );\n }\n\n return (\n <ChatAssistantMessage\n msg={msg}\n isLast={isLast}\n isStreaming={isStreaming}\n avatar={avatar}\n agentName={agentName}\n streamdownComponents={streamdownComponents}\n />\n );\n },\n (prev, next) =>\n prev.avatar === next.avatar &&\n prev.agentName === next.agentName &&\n prev.isLast === next.isLast &&\n prev.isStreaming === next.isStreaming &&\n prev.streamdownComponents === next.streamdownComponents &&\n prev.msg.id === next.msg.id &&\n prev.msg.content === next.msg.content &&\n prev.msg.role === next.msg.role &&\n prev.msg.toolCalls?.length === next.msg.toolCalls?.length &&\n JSON.stringify(prev.msg.toolCalls?.map((t) => t.state)) ===\n JSON.stringify(next.msg.toolCalls?.map((t) => t.state)),\n);\n"
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"name": "polpo-chat",
|
|
150
|
+
"type": "registry:ui",
|
|
151
|
+
"title": "Polpo Chat",
|
|
152
|
+
"description": "The main Chat compound component with ChatProvider, ChatMessages (virtualized), ChatScrollButton, and ChatSkeleton. Drop-in chat interface for Polpo AI agents.",
|
|
153
|
+
"dependencies": ["@polpo-ai/sdk", "@polpo-ai/react", "react-virtuoso", "lucide-react"],
|
|
154
|
+
"registryDependencies": ["polpo-chat-message", "polpo-chat-lib"],
|
|
155
|
+
"files": [
|
|
156
|
+
{
|
|
157
|
+
"path": "components/polpo-chat/chat-provider.tsx",
|
|
158
|
+
"type": "registry:ui",
|
|
159
|
+
"target": "components/polpo-chat/chat-provider.tsx",
|
|
160
|
+
"content": "\"use client\";\n\nimport {\n createContext,\n useContext,\n type ReactNode,\n} from \"react\";\nimport { useChat, type UseChatReturn } from \"@polpo-ai/react\";\nimport { useFiles } from \"@polpo-ai/react\";\n\n/* ------------------------------------------------------------------ */\n/* Types */\n/* ------------------------------------------------------------------ */\n\nexport interface ChatProviderProps {\n /** Resume an existing session by ID. */\n sessionId?: string;\n /** Target a specific agent for direct conversation. */\n agent?: string;\n /** Called when a new session is created (first message). */\n onSessionCreated?: (id: string) => void;\n /** Called after each stream update (e.g. scroll-to-bottom). */\n onUpdate?: () => void;\n children: ReactNode;\n}\n\nexport interface ChatContextValue extends UseChatReturn {\n /** Upload a file attachment (delegates to useFiles). */\n uploadFile: (\n destPath: string,\n file: File | Blob,\n filename: string,\n ) => Promise<{ uploaded: { name: string; size: number }[]; count: number }>;\n /** Whether a file upload is in progress. */\n isUploading: boolean;\n}\n\n/* ------------------------------------------------------------------ */\n/* Context */\n/* ------------------------------------------------------------------ */\n\nconst ChatContext = createContext<ChatContextValue | null>(null);\n\n/* ------------------------------------------------------------------ */\n/* Provider */\n/* ------------------------------------------------------------------ */\n\nexport function ChatProvider({\n sessionId,\n agent,\n onSessionCreated,\n onUpdate,\n children,\n}: ChatProviderProps) {\n const chat = useChat({\n sessionId,\n agent,\n onSessionCreated,\n onUpdate,\n });\n\n const { uploadFile, isUploading } = useFiles();\n\n const value: ChatContextValue = {\n ...chat,\n uploadFile,\n isUploading,\n };\n\n return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>;\n}\n\n/* ------------------------------------------------------------------ */\n/* Consumer hook */\n/* ------------------------------------------------------------------ */\n\nexport function useChatContext(): ChatContextValue {\n const ctx = useContext(ChatContext);\n if (!ctx) {\n throw new Error(\"useChatContext must be used within a <ChatProvider>\");\n }\n return ctx;\n}\n"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"path": "components/polpo-chat/chat-skeleton.tsx",
|
|
164
|
+
"type": "registry:ui",
|
|
165
|
+
"target": "components/polpo-chat/chat-skeleton.tsx",
|
|
166
|
+
"content": "/**\n * Skeleton loaders for chat UI — mirrors the exact layout of ChatMessageItem\n * so the loading state feels native, not jarring.\n */\n\nfunction Bone({ width, height = 14 }: { width: string; height?: number }) {\n return (\n <div\n className=\"bg-p-line animate-pulse\"\n style={{ width, height, borderRadius: height > 20 ? 12 : 6 }}\n />\n );\n}\n\n/** Skeleton for an AI message: avatar + name + text lines */\nexport function MessageSkeleton({ lines = 3 }: { lines?: number }) {\n const widths = [\"85%\", \"70%\", \"55%\", \"90%\", \"40%\"];\n return (\n <div className=\"w-full px-6 pt-4 pb-6\">\n <div className=\"max-w-3xl mx-auto\">\n <div className=\"flex items-center gap-2 mb-2\">\n <div className=\"size-6 rounded-md bg-p-line animate-pulse shrink-0\" />\n <Bone width=\"80px\" height={13} />\n </div>\n <div className=\"flex flex-col gap-2\">\n {Array.from({ length: lines }, (_, i) => (\n <Bone key={i} width={widths[i % widths.length]} />\n ))}\n </div>\n </div>\n </div>\n );\n}\n\n/** Skeleton for a user message: right-aligned bubble */\nexport function UserMessageSkeleton() {\n return (\n <div className=\"w-full px-6 py-3\">\n <div className=\"max-w-3xl mx-auto flex justify-end\">\n <div className=\"w-[45%] min-w-[120px] h-[42px] rounded-[18px_18px_4px_18px] bg-p-line animate-pulse\" />\n </div>\n </div>\n );\n}\n\n/** Full conversation skeleton — alternating user/AI messages */\nexport function ChatSkeleton({ count = 3 }: { count?: number }) {\n return (\n <div className=\"py-2\">\n {Array.from({ length: count }, (_, i) => (\n <div key={i}>\n <UserMessageSkeleton />\n <MessageSkeleton lines={i === 0 ? 2 : i === 1 ? 4 : 3} />\n </div>\n ))}\n </div>\n );\n}\n"
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"path": "components/polpo-chat/chat-scroll-button.tsx",
|
|
170
|
+
"type": "registry:ui",
|
|
171
|
+
"target": "components/polpo-chat/chat-scroll-button.tsx",
|
|
172
|
+
"content": "\"use client\";\n\nimport { ArrowDown } from \"lucide-react\";\n\n/* ------------------------------------------------------------------ */\n/* Scroll-to-bottom button with optional new-message indicator */\n/* ------------------------------------------------------------------ */\n\ninterface ChatScrollButtonProps {\n isAtBottom: boolean;\n showNewMessage?: boolean;\n onClick: () => void;\n className?: string;\n}\n\nexport function ChatScrollButton({\n isAtBottom,\n showNewMessage,\n onClick,\n className,\n}: ChatScrollButtonProps) {\n if (isAtBottom) return null;\n\n return (\n <button\n type=\"button\"\n aria-label=\"Scroll to bottom\"\n onClick={onClick}\n className={`absolute bottom-4 left-1/2 -translate-x-1/2 z-10 flex h-8 w-8 items-center justify-center rounded-full border border-[var(--polpo-border)] bg-[var(--polpo-bg,#fff)] shadow-md transition-colors hover:bg-[var(--polpo-bg-hover,#f5f5f5)] ${className ?? \"\"}`}\n >\n <ArrowDown className=\"h-4 w-4 text-[var(--polpo-fg,#333)]\" />\n\n {showNewMessage && (\n <span className=\"absolute -top-1 -right-1 flex h-3 w-3\">\n <span className=\"absolute inline-flex h-full w-full animate-ping rounded-full bg-[var(--polpo-accent,#3b82f6)] opacity-75\" />\n <span className=\"relative inline-flex h-3 w-3 rounded-full bg-[var(--polpo-accent,#3b82f6)]\" />\n </span>\n )}\n </button>\n );\n}\n"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"path": "components/polpo-chat/chat-messages.tsx",
|
|
176
|
+
"type": "registry:ui",
|
|
177
|
+
"target": "components/polpo-chat/chat-messages.tsx",
|
|
178
|
+
"content": "\"use client\";\n\nimport {\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport { Virtuoso, type VirtuosoHandle } from \"react-virtuoso\";\nimport type { ChatMessage } from \"@polpo-ai/sdk\";\nimport { useChatContext } from \"./chat-provider\";\nimport { ChatSkeleton } from \"./chat-skeleton\";\nimport { ChatScrollButton } from \"./chat-scroll-button\";\nimport { getTextContent } from \"@/lib/polpo-chat/get-text-content\";\n\n/* ------------------------------------------------------------------ */\n/* Types */\n/* ------------------------------------------------------------------ */\n\nexport interface ChatMessagesHandle {\n /** Scroll the list to the bottom. */\n scrollToBottom: (behavior?: \"smooth\" | \"auto\") => void;\n}\n\nexport interface ChatMessagesProps {\n /**\n * Custom renderer for each message.\n * Receives the message, its index, whether it is the last item, and\n * whether the assistant is currently streaming.\n */\n renderItem?: (\n msg: ChatMessage,\n index: number,\n isLast: boolean,\n isStreaming: boolean,\n ) => ReactNode;\n /** Extra classes applied to the Virtuoso container. */\n className?: string;\n /** Number of skeleton pairs to show during the initial load. */\n skeletonCount?: number;\n}\n\n/* ------------------------------------------------------------------ */\n/* Default renderer (plain text fallback) */\n/* ------------------------------------------------------------------ */\n\nfunction DefaultMessageItem({ msg }: { msg: ChatMessage }) {\n const text = getTextContent(msg.content);\n return (\n <div className=\"w-full px-6 py-3\">\n <div className=\"max-w-3xl mx-auto\">\n <p className=\"text-xs font-medium opacity-50 mb-1\">\n {msg.role === \"user\" ? \"You\" : \"Assistant\"}\n </p>\n <p className=\"whitespace-pre-wrap\">{text}</p>\n </div>\n </div>\n );\n}\n\n/* ------------------------------------------------------------------ */\n/* Footer (keeps Virtuoso happy for follow-output) */\n/* ------------------------------------------------------------------ */\n\nfunction VirtuosoFooter() {\n return <div className=\"h-px\" aria-hidden=\"true\" />;\n}\n\n/* ------------------------------------------------------------------ */\n/* Component */\n/* ------------------------------------------------------------------ */\n\nexport const ChatMessages = forwardRef<ChatMessagesHandle, ChatMessagesProps>(\n function ChatMessages({ renderItem, className, skeletonCount = 3 }, ref) {\n const { messages, isStreaming, status } = useChatContext();\n\n /* ── Virtuoso ref & scroll state ────────────────────────────── */\n const virtuosoRef = useRef<VirtuosoHandle>(null);\n const [isAtBottom, setIsAtBottom] = useState(true);\n const [showNewMessage, setShowNewMessage] = useState(false);\n const prevMessageCountRef = useRef(0);\n const hasInitialScrollRef = useRef(false);\n\n /* ── Scroll helpers ─────────────────────────────────────────── */\n const scrollToBottom = useCallback(\n (behavior: \"smooth\" | \"auto\" = \"smooth\") => {\n virtuosoRef.current?.scrollToIndex({\n index: \"LAST\",\n align: \"end\",\n behavior,\n });\n setShowNewMessage(false);\n },\n [],\n );\n\n useImperativeHandle(ref, () => ({ scrollToBottom }), [scrollToBottom]);\n\n /* ── Bottom-state change handler ────────────────────────────── */\n const handleAtBottomStateChange = useCallback((atBottom: boolean) => {\n setIsAtBottom(atBottom);\n if (atBottom) setShowNewMessage(false);\n }, []);\n\n /* ── Initial scroll to bottom once messages load ────────────── */\n useEffect(() => {\n if (messages.length > 0 && !hasInitialScrollRef.current) {\n hasInitialScrollRef.current = true;\n requestAnimationFrame(() =>\n virtuosoRef.current?.scrollToIndex({\n index: \"LAST\",\n align: \"end\",\n behavior: \"auto\",\n }),\n );\n }\n }, [messages.length]);\n\n /* ── Reset scroll flag when data source changes ─────────────── */\n useEffect(() => {\n hasInitialScrollRef.current = false;\n }, [status]);\n\n /* ── Show \"new messages\" badge when not at bottom ───────────── */\n useEffect(() => {\n const cur = messages.length;\n const prev = prevMessageCountRef.current;\n if (cur > prev && !isAtBottom && prev > 0) {\n setShowNewMessage(true);\n }\n prevMessageCountRef.current = cur;\n }, [messages.length, isAtBottom]);\n\n /* ── Item renderer ──────────────────────────────────────────── */\n const itemContent = useCallback(\n (index: number, msg: ChatMessage) => {\n const isLast = index === messages.length - 1;\n if (renderItem) {\n return renderItem(msg, index, isLast, isStreaming);\n }\n return <DefaultMessageItem msg={msg} />;\n },\n [messages.length, isStreaming, renderItem],\n );\n\n /* ── Loading state ──────────────────────────────────────────── */\n if (status === \"loading\") {\n return (\n <div className={`flex-1 overflow-hidden ${className ?? \"\"}`}>\n <ChatSkeleton count={skeletonCount} />\n </div>\n );\n }\n\n /* ── Main list ──────────────────────────────────────────────── */\n return (\n <div className={`relative flex-1 min-h-0 ${className ?? \"\"}`}>\n <Virtuoso\n ref={virtuosoRef}\n data={messages}\n followOutput=\"auto\"\n atBottomStateChange={handleAtBottomStateChange}\n atBottomThreshold={100}\n defaultItemHeight={120}\n overscan={500}\n increaseViewportBy={{ top: 300, bottom: 300 }}\n skipAnimationFrameInResizeObserver\n itemContent={itemContent}\n className=\"h-full\"\n components={{ Footer: VirtuosoFooter }}\n />\n\n <ChatScrollButton\n isAtBottom={isAtBottom}\n showNewMessage={showNewMessage}\n onClick={() => scrollToBottom()}\n />\n </div>\n );\n },\n);\n"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"path": "components/polpo-chat/chat.tsx",
|
|
182
|
+
"type": "registry:ui",
|
|
183
|
+
"target": "components/polpo-chat/chat.tsx",
|
|
184
|
+
"content": "\"use client\";\n\nimport { forwardRef, type ReactNode } from \"react\";\nimport { ChatProvider } from \"./chat-provider\";\nimport { ChatMessages, type ChatMessagesHandle } from \"./chat-messages\";\nimport { ChatMessage, type ChatMessageProps } from \"./chat-message\";\n\n// ── Props ──\n\nexport interface ChatProps {\n /** Session ID — omit for new chats */\n sessionId?: string;\n /** Agent name for completions */\n agent?: string;\n /** Called when a new session is created */\n onSessionCreated?: (sessionId: string) => void;\n /** Called on each stream update (useful for external scroll control) */\n onUpdate?: () => void;\n /** Custom message renderer — if omitted, uses ChatMessage with defaults */\n renderMessage?: (msg: ChatMessageProps[\"msg\"], index: number, isLast: boolean, isStreaming: boolean) => ReactNode;\n /** Avatar ReactNode shown on assistant messages */\n avatar?: ReactNode;\n /** Agent display name shown on assistant messages */\n agentName?: string;\n /** Streamdown components override for code blocks etc. */\n streamdownComponents?: Record<string, unknown>;\n /** Number of skeleton items while loading */\n skeletonCount?: number;\n /** Children rendered after the message list (e.g. ChatInput) */\n children?: ReactNode;\n /** Additional className on the outer container */\n className?: string;\n}\n\n// ── Component ──\n\nexport const Chat = forwardRef<ChatMessagesHandle, ChatProps>(function Chat(\n {\n sessionId,\n agent,\n onSessionCreated,\n onUpdate,\n renderMessage,\n avatar,\n agentName,\n streamdownComponents,\n skeletonCount,\n children,\n className,\n },\n ref,\n) {\n const defaultRender = (msg: ChatMessageProps[\"msg\"], _index: number, isLast: boolean, isStreaming: boolean) => (\n <ChatMessage\n msg={msg}\n isLast={isLast}\n isStreaming={isStreaming}\n avatar={avatar}\n agentName={agentName}\n streamdownComponents={streamdownComponents}\n />\n );\n\n return (\n <ChatProvider\n sessionId={sessionId}\n agent={agent}\n onSessionCreated={onSessionCreated}\n onUpdate={onUpdate}\n >\n <div className={`flex flex-col min-w-0 min-h-0 ${className || \"\"}`}>\n <ChatMessages\n ref={ref}\n renderItem={renderMessage || defaultRender}\n skeletonCount={skeletonCount}\n className=\"flex-1\"\n />\n {children}\n </div>\n </ChatProvider>\n );\n});\n"
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
]
|