import { useState, useRef, useEffect, type KeyboardEvent } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { Tooltip } from "./Tooltip"; import { Send, Square, Plus, X, Sparkles, Loader } from "lucide-react"; import type { UIMessage } from "ai"; import { isToolPart, normalizeToolPart } from "../utils/ai-tool-parts"; export interface ModelOption { id: string; label: string; context: string; cost: string; } interface ChatPanelProps { messages: UIMessage[]; isLoading: boolean; error: Error | undefined; input: string; models: ModelOption[]; selectedModel: string; onModelChange: (modelId: string) => void; onSend: (content: string) => void; onSetInput: (value: string) => void; onStop: () => void; onNewChat: () => void; onClose?: () => void; } function TokenGauge({ tokens, ratio }: { tokens: number; ratio: number }) { const r = 8; const circ = 2 * Math.PI * r; const offset = circ * (1 - ratio); const color = ratio > 0.8 ? "var(--ed-error)" : ratio > 0.5 ? "#ff9800" : "var(--ed-text-disabled)"; const label = tokens < 1000 ? `${tokens}` : `${(tokens / 1000).toFixed(1)}k`; if (tokens === 0) return null; return (
); } export function ChatPanel({ messages, isLoading, error, input, models, selectedModel, onModelChange, onSend, onSetInput, onStop, onNewChat, onClose, }: ChatPanelProps) { const scrollRef = useRef(null); useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [messages]); const inputValue = input ?? ""; const handleSubmit = () => { if (!inputValue.trim() || isLoading) return; onSend(inputValue.trim()); onSetInput(""); }; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSubmit(); } }; const estimatedTokens = messages.reduce((sum, msg) => { const text = msg.parts ?.filter((p) => p.type === "text") .map((p) => (p as { type: "text"; text: string }).text) .join("") ?? ""; return sum + Math.ceil(text.length / 4); }, 0); const selectedModelInfo = models.find((m) => m.id === selectedModel); const contextLimit = selectedModelInfo ? parseFloat(selectedModelInfo.context) * 1000 : 200_000; const usageRatio = Math.min(estimatedTokens / contextLimit, 1); return (
{/* Header */}
AI Assistant
{models.length > 0 && ( )} {onClose && ( )}
{/* Messages */}
{messages.length === 0 && (
Ask me to write, edit, expand, or improve your article.
)} {messages.map((msg) => ( ))} {isLoading && messages.length > 0 && ( )} {error && (
{error.message}
)}
{/* Input */}