diff --git a/src/pages/AIChatPage.tsx b/src/pages/AIChatPage.tsx deleted file mode 100644 index 4ef656f1..00000000 --- a/src/pages/AIChatPage.tsx +++ /dev/null @@ -1,534 +0,0 @@ -import { useMemo, useRef, useState, useEffect, useCallback } from 'react'; -import { useSeoMeta } from '@unhead/react'; -import Markdown from 'react-markdown'; -import rehypeSanitize from 'rehype-sanitize'; -import { Bot, Loader2, Send, Square, Trash2 } from 'lucide-react'; - -import { PageHeader } from '@/components/PageHeader'; -import { useCurrentUser } from '@/hooks/useCurrentUser'; -import { useAppContext } from '@/hooks/useAppContext'; -import { useAIChatSession } from '@/hooks/useAIChatSession'; -import { LoginArea } from '@/components/auth/LoginArea'; -import { Button } from '@/components/ui/button'; -import { Textarea } from '@/components/ui/textarea'; -import { ScrollArea } from '@/components/ui/scroll-area'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; -import { cn } from '@/lib/utils'; -import { useLayoutOptions } from '@/contexts/LayoutContext'; - -import type { DisplayMessage, ToolCall } from '@/lib/aiChatTools'; - -// ─── Slash Commands ─── - -const SLASH_COMMANDS = [ - { command: '/clear', description: 'Clear conversation history' }, - { command: '/new', description: 'Start a new conversation' }, - { command: '/tools', description: 'List available tools' }, -]; - -// ─── Page Component ─── - -export function AIChatPage() { - const { config } = useAppContext(); - const { user } = useCurrentUser(); - - useSeoMeta({ - title: `Agent | ${config.appName}`, - description: 'Chat with your AI agent', - }); - - useLayoutOptions({ noOverscroll: true }); - - if (!user) { - return ( -
-
- -
-

Agent

-

Log in with your Nostr account to start using the Agent.

-
- -
-
- ); - } - - return ; -} - -// ─── Chat View ─── - -function AgentChatView() { - const { - messages, - input, - setInput, - isStreaming, - streamingText, - selectedModel, - apiLoading, - apiError, - messagesEndRef, - capacity, - lastPromptTokens, - contextWindow, - storageBytes, - maxStorageBytes, - handleSend, - handleStop, - handleKeyDown, - handleClear, - } = useAIChatSession(); - - return ( -
- {/* Header */} - - -

Agent

- - }> -
- - -
-
- - {/* Messages Area */} - {messages.length === 0 && !streamingText ? ( -
- -
- ) : ( - -
- {messages.map((msg) => ( - msg.role !== 'tool_result' && - ))} - - {/* Streaming text */} - {streamingText && ( -
-
-
-
- - {streamingText} - -
-
-
-
- )} - - {/* Loading indicator */} - {(isStreaming || apiLoading) && !streamingText && } - - {/* Error display */} - {apiError && ( - apiError.includes('run out of credits') ? ( - - ) : apiError.includes('Rate limited') ? ( - - ) : null - )} - -
-
- - )} - - {/* Input Area */} -
-
- - {isStreaming ? ( - - ) : ( - - )} -
-
-
- ); -} - -// ─── Sub-Components ─── - -function ThinkingIndicator() { - return ( -
-
- - Thinking... -
-
- ); -} - -function ErrorBanner({ heading, body }: { heading: string; body: string }) { - return ( -
-

{heading}

-

{body}

-
- ); -} - -const AGENT_GREETINGS = [ - "How can I help you today?", - "What would you like to know?", - "Ready when you are.", -]; - -const SUGGESTIONS = [ - "What are my friends talking about?", - "What's happening in the world?", -]; - -function EmptyState({ onSuggestion }: { onSuggestion: (text: string) => void }) { - const greeting = useMemo(() => AGENT_GREETINGS[Math.floor(Math.random() * AGENT_GREETINGS.length)], []); - - return ( -
- -
-

Agent

-

{greeting}

-
- -
- {SUGGESTIONS.map((s) => ( - - ))} -
-
- ); -} - -function MessageBubble({ message }: { message: DisplayMessage }) { - const isUser = message.role === 'user'; - - // System notices — info vs error styling - if (message.noticeVariant) { - const isError = message.noticeVariant === 'error'; - return ( -
-
-
-
- - {message.content} - -
-
-
-
- ); - } - - return ( -
-
- {/* Hide the bubble entirely when the assistant message is empty (tool-only turn) */} - {(isUser || message.content.trim()) && ( -
- {isUser ? ( -

{message.content}

- ) : ( -
- - {message.content} - -
- )} -
- )} - - {/* Tool call indicators */} - {message.toolCalls && message.toolCalls.length > 0 && ( -
- {message.toolCalls.map((tc) => ( - - ))} -
- )} - - - {message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} - -
-
- ); -} - -/** Text input with a slash-command autocomplete dropdown. */ -function SlashCommandInput({ value, onChange, onKeyDown, onSend, placeholder, disabled }: { - value: string; - onChange: (v: string) => void; - onKeyDown: (e: React.KeyboardEvent) => void; - onSend: (override?: string) => void; - placeholder?: string; - disabled?: boolean; -}) { - const wrapperRef = useRef(null); - const [selectedIndex, setSelectedIndex] = useState(0); - const [menuDismissed, setMenuDismissed] = useState(false); - - // Filter commands based on input - const matches = useMemo(() => { - if (!value.startsWith('/') || menuDismissed) return []; - const typed = value.toLowerCase(); - return SLASH_COMMANDS.filter((c) => c.command.startsWith(typed)); - }, [value, menuDismissed]); - - const showMenu = matches.length > 0 && !disabled; - - // Reset selection when matches change - useEffect(() => { - setSelectedIndex(0); - }, [matches.length]); - - // Un-dismiss when input stops being a slash command or is cleared - useEffect(() => { - if (!value.startsWith('/')) setMenuDismissed(false); - }, [value]); - - // Close menu on outside click - useEffect(() => { - if (!showMenu) return; - const handler = (e: MouseEvent) => { - if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) { - setMenuDismissed(true); - } - }; - document.addEventListener('mousedown', handler); - return () => document.removeEventListener('mousedown', handler); - }, [showMenu]); - - const selectCommand = useCallback((cmd: string) => { - onChange(cmd); - setMenuDismissed(true); - // Auto-send slash commands immediately - onSend(cmd); - }, [onChange, onSend]); - - const handleKeyDown = useCallback((e: React.KeyboardEvent) => { - if (showMenu) { - if (e.key === 'ArrowUp') { - e.preventDefault(); - setSelectedIndex((i) => (i - 1 + matches.length) % matches.length); - return; - } - if (e.key === 'ArrowDown') { - e.preventDefault(); - setSelectedIndex((i) => (i + 1) % matches.length); - return; - } - if (e.key === 'Tab' || (e.key === 'Enter' && !e.shiftKey)) { - e.preventDefault(); - selectCommand(matches[selectedIndex].command); - return; - } - if (e.key === 'Escape') { - e.preventDefault(); - setMenuDismissed(true); - return; - } - } - // Fall through to parent handler (Enter → send, etc.) - onKeyDown(e); - }, [showMenu, matches, selectedIndex, selectCommand, onKeyDown]); - - return ( -
- {/* Autocomplete menu */} - {showMenu && ( -
- {matches.map((cmd, i) => ( - - ))} -
- )} -