'use client' import { type FormEvent, useEffect, useMemo, useRef, useState } from 'react' import { useChat } from '@ai-sdk/react' import { DefaultChatTransport } from 'ai' import { ArrowUp, MessageCircle, Square, X } from 'lucide-react' import { Streamdown } from 'streamdown' import { cn } from '@/lib/utils' import 'streamdown/styles.css' interface DocSource { title: string url: string } /** Pull the deduped doc sources surfaced by the searchDocs tool out of a message's parts. */ function getSources(parts: ReadonlyArray<{ type: string; [key: string]: unknown }>): DocSource[] { const seen = new Set() const sources: DocSource[] = [] for (const part of parts) { if (part.type !== 'tool-searchDocs') continue const output = (part as { output?: unknown }).output if (!Array.isArray(output)) continue for (const item of output as DocSource[]) { if (!item?.url || seen.has(item.url)) continue seen.add(item.url) sources.push({ title: item.title, url: item.url }) } } return sources } /** Concatenate the streamed text parts of a message. */ function getText(parts: ReadonlyArray<{ type: string; [key: string]: unknown }>): string { return parts .filter((part) => part.type === 'text') .map((part) => (part as unknown as { text: string }).text) .join('') } interface AskAIProps { /** Active docs locale, forwarded so retrieval is scoped to the reader's language. */ locale: string } export function AskAI({ locale }: AskAIProps) { const [open, setOpen] = useState(false) const [input, setInput] = useState('') const scrollRef = useRef(null) // Stable transport; the locale is sent per-message (below) so it stays current // after a language switch instead of being frozen into the transport. const transport = useMemo(() => new DefaultChatTransport({ api: '/api/chat' }), []) const { messages, sendMessage, status, stop, error } = useChat({ transport }) const isBusy = status === 'submitted' || status === 'streaming' // Jump to the bottom instantly when the panel opens (a mount transition). useEffect(() => { if (!open) return scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight }) }, [open]) // Smooth-scroll as new messages stream in (an explicit re-orientation cue). useEffect(() => { scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' }) }, [messages]) const handleSubmit = (event: FormEvent) => { event.preventDefault() const text = input.trim() if (!text || isBusy) return sendMessage({ text }, { body: { locale } }) setInput('') } return ( <> {!open && ( )} {open && (
Ask Sim
{messages.length === 0 && (

Ask anything about building, deploying, and managing AI agents in Sim.

)} {messages.map((message, index) => { const text = getText(message.parts) // Only the in-progress (last) message should show the loading state. const isStreaming = isBusy && index === messages.length - 1 const sources = message.role === 'assistant' ? getSources(message.parts) : [] return (
{message.role === 'user' ? (
{text}
) : (
{text ? ( {text} ) : isStreaming ? ( '…' ) : sources.length === 0 ? ( No answer returned. ) : null}
)} {sources.length > 0 && (
{sources.map((source) => ( {source.title || source.url} ))}
)}
) })} {error && (

Something went wrong. Please try again.

)}