/** * Mistral AI Chatbot Demo Page * Full-featured chatbot with streaming responses */ import { useState, useRef, useEffect } from 'react'; // React hooks: useState for typed state, useRef for DOM refs, useEffect for lifecycle import { Link } from 'react-router-dom'; // Router Link component for client-side navigation import { useMistralConversation, useMistralStream, type ChatMessage } from '@/hooks'; // Custom hooks/types from app hooks module // - ChatMessage type: { role: 'system' | 'user' | 'assistant'; content: string; /* optional fields may exist */ } // - useMistralConversation(systemPrompt: string) // returns: { // messages: ChatMessage[], // conversation history (server-backed in non-streaming mode) // sendMessage: (content: string) => Promise, // sends a user message and updates messages // loading: boolean, // loading state for non-streaming calls // error: string | null, // error string if request failed // clearHistory: () => void // clears server-side history // } // - useMistralStream() // returns: { // stream: (messages: ChatMessage[]) => Promise, // starts a streaming response given full message context, resolves to final assistant text or null // loading: boolean, // streaming in-progress flag // error: string | null, // streaming error // abort: () => void // aborts current streaming request // } const SYSTEM_PROMPTS = { // Preset system prompts to set assistant "persona" / behavior assistant: 'You are a helpful, friendly AI assistant. Be concise but thorough in your responses.', // default assistant guidance coder: 'You are an expert software engineer. Help with coding questions, debugging, and best practices. Use code examples when helpful.', // coder persona guidance tutor: 'You are a patient and encouraging tutor. Explain concepts clearly, use analogies, and check for understanding.', // tutor persona guidance creative: 'You are a creative writing assistant. Help with storytelling, brainstorming, and creative projects.', // creative persona guidance }; type PersonaKey = keyof typeof SYSTEM_PROMPTS; // Restrict persona state to keys from SYSTEM_PROMPTS export default function MistralChatDemo() { const [input, setInput] = useState(''); // Controlled textarea input value const [persona, setPersona] = useState('assistant'); // Current persona selection, affects system prompt const [useStreaming, setUseStreaming] = useState(true); // Toggle between streaming or non-streaming API modes const [streamingContent, setStreamingContent] = useState(''); // Partial assistant content while streaming const messagesEndRef = useRef(null); // DOM anchor used for scrollIntoView; typed as HTMLDivElement | null // Non-streaming conversation hook — initialize with system prompt string to set assistant role/context const { messages, sendMessage, loading: conversationLoading, error: conversationError, clearHistory, } = useMistralConversation(SYSTEM_PROMPTS[persona]); // argument: string system prompt from SYSTEM_PROMPTS[persona] // Streaming hook — provides stream() that accepts ChatMessage[] and returns Promise const { stream, loading: streamLoading, error: streamError, abort, } = useMistralStream(); // no args; hook returns streaming client functions (stream, abort) and statuses const loading = useStreaming ? streamLoading : conversationLoading; // unified loading indicator depending on mode const error = useStreaming ? streamError : conversationError; // unified error message depending on mode // Local messages used only in streaming mode; array items conform to ChatMessage type const [localMessages, setLocalMessages] = useState([]); // Scroll to bottom when messages or streaming content change — improves UX for new messages useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages, localMessages, streamingContent]); // Handle persona change: update selected persona and reset relevant state and history const handlePersonaChange = (newPersona: PersonaKey) => { // newPersona: one of the PersonaKey values (keys of SYSTEM_PROMPTS) setPersona(newPersona); setLocalMessages([]); // clear local streaming messages setStreamingContent(''); // clear any partial streaming text clearHistory(); // resets server conversation state tied to prior persona }; // Choose which message list to display depending on streaming toggle; exclude system messages from UI const displayMessages = useStreaming ? localMessages.filter(m => m.role !== 'system') : messages.filter(m => m.role !== 'system'); // Submit handler for both streaming and non-streaming modes const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); // e: React.FormEvent - used only to stop form submit default if (!input.trim() || loading) return; const userMessage = input.trim(); setInput(''); if (useStreaming) { const newUserMessage: ChatMessage = { role: 'user', content: userMessage }; // ChatMessage shape enforced here const newMessages = [...localMessages, newUserMessage]; setLocalMessages(newMessages); setStreamingContent(''); // reset streaming buffer // Build full context array for stream — stream(messages: ChatMessage[]) expects the full history including system prompt const allMessages: ChatMessage[] = [ { role: 'system', content: SYSTEM_PROMPTS[persona] }, // system message: string prompt guiding assistant behavior ...newMessages, ]; // stream() signature: (messages: ChatMessage[]) => Promise // The hook may additionally emit incremental deltas to update streamingContent (handled inside the hook) const result = await stream(allMessages); if (result) { // result: final aggregated assistant response (string) or null if aborted/failed setLocalMessages([ ...newMessages, { role: 'assistant', content: result }, ]); setStreamingContent(''); } } else { // sendMessage(content: string) returns Promise and updates messages via the conversation hook await sendMessage(userMessage); } }; const handleClear = () => { if (useStreaming) { setLocalMessages([]); // clear local streaming messages only setStreamingContent(''); } else { clearHistory(); // clear server-backed conversation history } }; // Submit on Enter when not holding Shift; allows Shift+Enter for newline const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSubmit(e); } }; return (
{/* page container with layout classes */} {/* back link */} ← Back to Home
{/* header row: title + controls */}

Mistral AI Chatbot

{/* page title */}

Full-featured chat with the Mistral AI SDK

{/* controls: streaming toggle + persona selector */} {/* Streaming toggle */} {/* Persona selector */}
{/* Chat container */}
{/* Messages area */}
{displayMessages.length === 0 && !streamingContent ? ( // empty state when there are no messages and nothing streaming
{/* decorative icon */}

Start a conversation with Mistral AI

Currently using: {persona} persona

{/* shows active persona */}
) : (
{/* message list */} {displayMessages.map((message, index) => (
{message.role === 'user' ? 'U' : 'M'} {/* avatar initial */}

{message.role === 'user' ? 'You' : 'Mistral AI'} {/* label for who sent the message */}

{message.content}

{/* message content preserves line breaks */}
))} {/* Streaming content */} {streamingContent && (
M

Mistral AI

{streamingContent}

{/* show partial streaming text */}
)} {/* Loading indicator */} {loading && !streamingContent && ( // when loading but no streaming buffer, show pulsing dots
M

Mistral AI

)}
{/* anchor used to scroll to bottom */}
)}
{/* Error display */} {error && (
{error} {/* surface API or hook errors to the user */}
)} {/* Input area */}
{/* form submission handled by handleSubmit */}