import { AlertTriangle, Bot, Brain, ChevronDown, Loader2, RefreshCw, Send, Settings, Trash2, } from "lucide-react"; import { marked } from "marked"; import { useCallback, useEffect, useRef, useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { Textarea } from "@/components/ui/textarea"; import { toNumber } from "@/lib/tsrs-utils"; import { type Message, useAssistantStore } from "../stores/assistant-store"; import { useSettingsStore } from "../stores/settings-store"; import { useUiStore } from "../stores/ui-store"; interface ParsedMessage { thinking: string | null; content: string; isThinking: boolean; } function parseMessageContent(content: string): ParsedMessage { if (!content) return { thinking: null, content: "", isThinking: false }; // Support both and (DeepSeek uses ) let startTag = ""; let endTag = ""; let startIndex = content.indexOf(startTag); if (startIndex === -1) { startTag = ""; endTag = ""; startIndex = content.indexOf(startTag); } // Also check for encoded tags if they weren't decoded properly if (startIndex === -1) { startTag = "\u003cthink\u003e"; endTag = "\u003c/think\u003e"; startIndex = content.indexOf(startTag); } if (startIndex !== -1) { const endIndex = content.indexOf(endTag, startIndex); if (endIndex !== -1) { // Completed thinking block const before = content.substring(0, startIndex); const thinking = content .substring(startIndex + startTag.length, endIndex) .trim(); const after = content.substring(endIndex + endTag.length); return { thinking, content: (before + after).trim(), isThinking: false, }; } else { // Incomplete thinking block (still streaming) const before = content.substring(0, startIndex); const thinking = content.substring(startIndex + startTag.length).trim(); return { thinking, content: before.trim(), isThinking: true, }; } } return { thinking: null, content, isThinking: false }; } function renderMarkdown(content: string): string { if (!content) return ""; try { return marked(content, { breaks: true, gfm: true }) as string; } catch { return content; } } export function AssistantView() { const { messages, isProcessing, isProviderHealthy, streamingContent, init, checkHealth, sendMessage, clearHistory, } = useAssistantStore(); const { settings } = useSettingsStore(); const { setView } = useUiStore(); const [input, setInput] = useState(""); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const provider = settings.assistant.llmProvider; const endpoint = provider === "ollama" ? settings.assistant.ollamaEndpoint : settings.assistant.openaiEndpoint; const model = provider === "ollama" ? settings.assistant.ollamaModel : settings.assistant.openaiModel; const getProviderName = (): string => { if (provider === "ollama") { return `Ollama (${model})`; } else if (provider === "openai") { return `OpenAI (${model})`; } return provider; }; const getProviderHelpText = (): string => { if (provider === "ollama") { return `Please ensure Ollama is installed and running at ${endpoint}.`; } else if (provider === "openai") { return "Please check your OpenAI API key in Settings > AI Assistant."; } return ""; }; const scrollToBottom = useCallback(() => { if (messagesContainerRef.current) { setTimeout(() => { if (messagesContainerRef.current) { messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight; } }, 0); } }, []); useEffect(() => { init(); }, [init]); useEffect(() => { if (messages.length > 0 || isProcessing) { scrollToBottom(); } }, [messages.length, isProcessing, scrollToBottom]); const handleSubmit = async () => { if (!input.trim() || isProcessing) return; const text = input; setInput(""); await sendMessage(text, settings.assistant.enabled, provider, endpoint); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSubmit(); } }; const renderMessage = (message: Message, index: number) => { const isUser = message.role === "user"; const parsed = parseMessageContent(message.content); return (
{!isUser && parsed.thinking && (
Thinking Process
{parsed.thinking} {parsed.isThinking && ( )}
)}
{!isUser && message.stats && (
{message.stats.evalCount} tokens ·{" "} {Math.round(toNumber(message.stats.totalDuration) / 1000000)} ms
)}
); }; return (

Game Assistant

Powered by {getProviderName()}

{!settings.assistant.enabled ? ( Disabled ) : !isProviderHealthy ? ( Offline ) : (
Online )}
{/* Chat Area */}
{/* Warning when assistant is disabled */} {!settings.assistant.enabled && (
Assistant is disabled. Enable it in Settings > AI Assistant.
)} {/* Provider offline warning */} {settings.assistant.enabled && !isProviderHealthy && (
Assistant is offline {getProviderHelpText()}
)} {/* Messages Container */} {messages.length === 0 ? (

How can I help you today?

I can analyze your game logs, diagnose crashes, or explain mod features. {!settings.assistant.enabled && ( Assistant is disabled. Enable it in{" "} . )}

) : ( <> {messages.map((message, index) => renderMessage(message, index))} {isProcessing && streamingContent && (
Assistant is typing...
)} )}
{/* Input Area */}