'use client'; import { type ComponentProps, createContext, type ReactNode, type SyntheticEvent, use, useCallback, useEffect, useEffectEvent, useMemo, useRef, useState, } from 'react'; import { flushSync } from 'react-dom'; import { ArrowUp, FileText, Loader2, RefreshCw, SearchIcon, Sparkles, Square, ThumbsDown, ThumbsUp, X, } from 'lucide-react'; import { cn } from '../../lib/cn'; import { buttonVariants } from '../ui/button'; import { useChat, type UseChatHelpers } from '@ai-sdk/react'; import { DefaultChatTransport, type UIMessage } from 'ai'; import { Markdown } from '../markdown'; import { getVisitorId, randomId, sendFeedback, type Vote } from '../../lib/analytics/client'; export type ChatUIMessage = UIMessage< never, { client: { location: string; }; } >; const Context = createContext<{ open: boolean; setOpen: (open: boolean) => void; chat: UseChatHelpers; /** empties the chat and starts a new analytics thread */ clear: () => void; votes: Record; vote: (messageId: string, vote: Vote) => void; } | null>(null); export function AISearchPanelHeader({ className, ...props }: ComponentProps<'div'>) { const { setOpen } = useAISearchContext(); return (

Ask Pangolin AI

Answers are generated from the docs and can be wrong. Check the linked pages. Don't send any sensitive information.

); } export function AISearchInputActions() { const { clear } = useAISearchContext(); const { messages, status, regenerate } = useChatContext(); const isLoading = status === 'streaming'; if (messages.length === 0) return null; return ( <> {!isLoading && messages.at(-1)?.role === 'assistant' && ( )} ); } const suggestions = [ 'How do I expose a web app running on my home server?', 'What is the difference between a site and a client?', 'How do I self-host Pangolin with Docker Compose?', 'How do I connect Claude Code to the AI Gateway?', ]; function sendText(send: UseChatHelpers['sendMessage'], text: string) { void send({ role: 'user', parts: [ { type: 'data-client', data: { location: location.href } }, { type: 'text', text }, ], }); } /** open the panel and immediately ask `text` */ export function useAskAI() { const { setOpen, chat } = useAISearchContext(); return (text: string) => { setOpen(true); if (chat.status === 'streaming' || chat.status === 'submitted') return; sendText(chat.sendMessage, text); }; } const StorageKeyInput = '__ai_search_input'; export function AISearchInput(props: ComponentProps<'form'>) { const { status, sendMessage, stop } = useChatContext(); const [input, setInput] = useState(() => { try { return localStorage.getItem(StorageKeyInput) ?? ''; } catch { return ''; } }); const isLoading = status === 'streaming' || status === 'submitted'; const onStart = (e?: SyntheticEvent) => { e?.preventDefault(); const message = input.trim(); if (message.length === 0) return; sendText(sendMessage, message); setInput(''); try { localStorage.removeItem(StorageKeyInput); } catch { // storage unavailable } }; useEffect(() => { if (isLoading) document.getElementById('nd-ai-input')?.focus(); }, [isLoading]); return (
{ setInput(e.target.value); try { localStorage.setItem(StorageKeyInput, e.target.value); } catch { // storage unavailable } }} onKeyDown={(event) => { // keyCode 229: Safari fires `compositionend` before this keydown, `isComposing` is already false if (event.nativeEvent.isComposing || event.keyCode === 229) return; if (!event.shiftKey && event.key === 'Enter') { onStart(event); } }} /> {isLoading ? ( ) : ( )}
); } function List(props: Omit, 'dir'>) { const containerRef = useRef(null); useEffect(() => { if (!containerRef.current) return; function callback() { const container = containerRef.current; if (!container) return; container.scrollTo({ top: container.scrollHeight, behavior: 'instant', }); } const observer = new ResizeObserver(callback); callback(); const element = containerRef.current?.firstElementChild; if (element) { observer.observe(element); } return () => { observer.disconnect(); }; }, []); return (
{props.children}
); } function Input(props: ComponentProps<'textarea'>) { const ref = useRef(null); const shared = cn('pg-chat-field col-start-1 row-start-1', props.className); return (