"use client"; import { Attachment, AttachmentPreview, AttachmentRemove, Attachments, } from "@/components/ai-elements/attachments"; import { ModelSelector, ModelSelectorContent, ModelSelectorEmpty, ModelSelectorGroup, ModelSelectorInput, ModelSelectorItem, ModelSelectorList, ModelSelectorLogo, ModelSelectorLogoGroup, ModelSelectorName, ModelSelectorTrigger, } from "@/components/ai-elements/model-selector"; import type { PromptInputMessage } from "@/components/ai-elements/prompt-input"; import { PromptInput, PromptInputActionAddAttachments, PromptInputActionMenu, PromptInputActionMenuContent, PromptInputActionMenuTrigger, PromptInputBody, PromptInputButton, PromptInputFooter, PromptInputHeader, PromptInputSubmit, PromptInputTextarea, PromptInputTools, usePromptInputAttachments, } from "@/components/ai-elements/prompt-input"; import type { QueueTodo } from "@/components/ai-elements/queue"; import { Queue, QueueItem, QueueItemAction, QueueItemActions, QueueItemContent, QueueItemDescription, QueueItemIndicator, QueueSection, QueueSectionContent, } from "@/components/ai-elements/queue"; import { CheckIcon, GlobeIcon, Trash2 } from "lucide-react"; import { memo, useCallback, useRef, useState } from "react"; const models = [ { chef: "OpenAI", chefSlug: "openai", id: "gpt-4o", name: "GPT-4o", providers: ["openai", "azure"], }, { chef: "OpenAI", chefSlug: "openai", id: "gpt-4o-mini", name: "GPT-4o Mini", providers: ["openai", "azure"], }, { chef: "Anthropic", chefSlug: "anthropic", id: "claude-opus-4-20250514", name: "Claude 4 Opus", providers: ["anthropic", "azure", "google", "amazon-bedrock"], }, { chef: "Anthropic", chefSlug: "anthropic", id: "claude-sonnet-4-20250514", name: "Claude 4 Sonnet", providers: ["anthropic", "azure", "google", "amazon-bedrock"], }, { chef: "Google", chefSlug: "google", id: "gemini-2.0-flash-exp", name: "Gemini 2.0 Flash", providers: ["google"], }, ]; const SUBMITTING_TIMEOUT = 200; const STREAMING_TIMEOUT = 2000; interface AttachmentItemProps { attachment: { id: string; type: "file"; filename?: string; mediaType?: string; url: string; }; onRemove: (id: string) => void; } const AttachmentItem = memo(({ attachment, onRemove }: AttachmentItemProps) => { const handleRemove = useCallback( () => onRemove(attachment.id), [onRemove, attachment.id] ); return ( ); }); AttachmentItem.displayName = "AttachmentItem"; interface TodoItemProps { todo: QueueTodo; onRemove: (id: string) => void; } const TodoItem = memo(({ todo, onRemove }: TodoItemProps) => { const isCompleted = todo.status === "completed"; const handleRemove = useCallback( () => onRemove(todo.id), [onRemove, todo.id] ); return ( {todo.title} {todo.description && ( {todo.description} )} ); }); TodoItem.displayName = "TodoItem"; interface ModelItemProps { m: (typeof models)[0]; selectedModel: string; onSelect: (id: string) => void; } const ModelItem = memo(({ m, selectedModel, onSelect }: ModelItemProps) => { const handleSelect = useCallback(() => onSelect(m.id), [onSelect, m.id]); return ( {m.name} {m.providers.map((provider) => ( ))} {selectedModel === m.id ? ( ) : ( )} ); }); ModelItem.displayName = "ModelItem"; const sampleTodos: QueueTodo[] = [ { description: "Complete the README and API docs", id: "todo-1", status: "completed", title: "Write project documentation", }, { id: "todo-2", status: "pending", title: "Implement authentication", }, { description: "Resolve crash on settings page", id: "todo-3", status: "pending", title: "Fix bug #42", }, { description: "Unify queue and todo state management", id: "todo-4", status: "pending", title: "Refactor queue logic", }, { description: "Increase test coverage for hooks", id: "todo-5", status: "pending", title: "Add unit tests", }, ]; const PromptInputAttachmentsDisplay = () => { const attachments = usePromptInputAttachments(); const handleRemove = useCallback( (id: string) => attachments.remove(id), [attachments] ); if (attachments.files.length === 0) { return null; } return ( {attachments.files.map((attachment) => ( ))} ); }; const Example = () => { const [todos, setTodos] = useState(sampleTodos); const handleRemoveTodo = useCallback((id: string) => { setTodos((prev) => prev.filter((todo) => todo.id !== id)); }, []); const [text, setText] = useState(""); const [model, setModel] = useState(models[0].id); const [modelSelectorOpen, setModelSelectorOpen] = useState(false); const [status, setStatus] = useState< "submitted" | "streaming" | "ready" | "error" >("ready"); const timeoutRef = useRef(null); const handleTextChange = useCallback( (e: React.ChangeEvent) => setText(e.target.value), [] ); const handleModelSelect = useCallback((id: string) => { setModel(id); setModelSelectorOpen(false); }, []); const selectedModelData = models.find((m) => m.id === model); const stop = () => { console.log("Stopping request..."); // Clear any pending timeouts if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } setStatus("ready"); }; const handleSubmit = useCallback( (message: PromptInputMessage) => { // If currently streaming or submitted, stop instead of submitting if (status === "streaming" || status === "submitted") { stop(); return; } const hasText = Boolean(message.text); const hasAttachments = Boolean(message.files?.length); if (!(hasText || hasAttachments)) { return; } setStatus("submitted"); console.log("Submitting message:", message); setTimeout(() => { setStatus("streaming"); }, SUBMITTING_TIMEOUT); timeoutRef.current = setTimeout(() => { setStatus("ready"); timeoutRef.current = null; }, STREAMING_TIMEOUT); }, [status] ); return ( {todos.length > 0 && ( {todos.map((todo) => ( ))} )} Search {selectedModelData?.chefSlug && ( )} {selectedModelData?.name && ( {selectedModelData.name} )} No models found. {["OpenAI", "Anthropic", "Google"].map((chef) => ( {models .filter((m) => m.chef === chef) .map((m) => ( ))} ))} ); }; export default Example;