aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages/ui/src/pages/assistant-view.tsx.bk
diff options
context:
space:
mode:
author苏向夜 <fu050409@163.com>2026-02-25 01:32:51 +0800
committer苏向夜 <fu050409@163.com>2026-02-25 01:32:51 +0800
commit66668d85d603c5841d755a6023aa1925559fc6d4 (patch)
tree485464148c76b0021efb55b7d2afd1c3004ceee0 /packages/ui/src/pages/assistant-view.tsx.bk
parenta6773bd092db654360c599ca6b0108ea0e456e8c (diff)
downloadDropOut-66668d85d603c5841d755a6023aa1925559fc6d4.tar.gz
DropOut-66668d85d603c5841d755a6023aa1925559fc6d4.zip
chore(workspace): replace legacy codes
Diffstat (limited to 'packages/ui/src/pages/assistant-view.tsx.bk')
-rw-r--r--packages/ui/src/pages/assistant-view.tsx.bk485
1 files changed, 485 insertions, 0 deletions
diff --git a/packages/ui/src/pages/assistant-view.tsx.bk b/packages/ui/src/pages/assistant-view.tsx.bk
new file mode 100644
index 0000000..56f827b
--- /dev/null
+++ b/packages/ui/src/pages/assistant-view.tsx.bk
@@ -0,0 +1,485 @@
+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 <thinking> and <think> (DeepSeek uses <think>)
+ let startTag = "<thinking>";
+ let endTag = "</thinking>";
+ let startIndex = content.indexOf(startTag);
+
+ if (startIndex === -1) {
+ startTag = "<think>";
+ endTag = "</think>";
+ 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<HTMLDivElement>(null);
+ const messagesContainerRef = useRef<HTMLDivElement>(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 (
+ <div
+ key={index}
+ className={`flex ${isUser ? "justify-end" : "justify-start"} mb-4`}
+ >
+ <div
+ className={`max-w-[80%] rounded-2xl px-4 py-3 ${
+ isUser
+ ? "bg-indigo-500 text-white rounded-br-none"
+ : "bg-zinc-800 text-zinc-100 rounded-bl-none"
+ }`}
+ >
+ {!isUser && parsed.thinking && (
+ <div className="mb-3 max-w-full overflow-hidden">
+ <details className="group" open={parsed.isThinking}>
+ <summary className="list-none cursor-pointer flex items-center gap-2 text-zinc-500 hover:text-zinc-300 transition-colors text-xs font-medium select-none bg-black/20 p-2 rounded-lg border border-white/5 w-fit mb-2 outline-none">
+ <Brain className="h-3 w-3" />
+ <span>Thinking Process</span>
+ <ChevronDown className="h-3 w-3 transition-transform duration-200 group-open:rotate-180" />
+ </summary>
+ <div className="pl-3 border-l-2 border-zinc-700 text-zinc-500 text-xs italic leading-relaxed whitespace-pre-wrap font-mono max-h-96 overflow-y-auto custom-scrollbar bg-black/10 p-2 rounded-r-md">
+ {parsed.thinking}
+ {parsed.isThinking && (
+ <span className="inline-block w-1.5 h-3 bg-zinc-500 ml-1 animate-pulse align-middle" />
+ )}
+ </div>
+ </details>
+ </div>
+ )}
+ <div
+ className="prose prose-invert max-w-none"
+ dangerouslySetInnerHTML={{
+ __html: renderMarkdown(parsed.content),
+ }}
+ />
+ {!isUser && message.stats && (
+ <div className="mt-2 pt-2 border-t border-zinc-700/50">
+ <div className="text-xs text-zinc-400">
+ {message.stats.evalCount} tokens ·{" "}
+ {Math.round(toNumber(message.stats.totalDuration) / 1000000)}
+ ms
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+ );
+ };
+
+ return (
+ <div className="h-full w-full flex flex-col gap-4 p-4 lg:p-8">
+ <div className="flex items-center justify-between mb-2">
+ <div className="flex items-center gap-3">
+ <div className="p-2 bg-indigo-500/20 rounded-lg text-indigo-400">
+ <Bot size={24} />
+ </div>
+ <div>
+ <h2 className="text-2xl font-bold">Game Assistant</h2>
+ <p className="text-zinc-400 text-sm">
+ Powered by {getProviderName()}
+ </p>
+ </div>
+ </div>
+
+ <div className="flex items-center gap-2">
+ {!settings.assistant.enabled ? (
+ <Badge
+ variant="outline"
+ className="bg-zinc-500/10 text-zinc-400 border-zinc-500/20"
+ >
+ <AlertTriangle className="h-3 w-3 mr-1" />
+ Disabled
+ </Badge>
+ ) : !isProviderHealthy ? (
+ <Badge
+ variant="outline"
+ className="bg-red-500/10 text-red-400 border-red-500/20"
+ >
+ <AlertTriangle className="h-3 w-3 mr-1" />
+ Offline
+ </Badge>
+ ) : (
+ <Badge
+ variant="outline"
+ className="bg-emerald-500/10 text-emerald-400 border-emerald-500/20"
+ >
+ <div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse mr-1" />
+ Online
+ </Badge>
+ )}
+
+ <Button
+ variant="ghost"
+ size="icon"
+ onClick={checkHealth}
+ title="Check Connection"
+ disabled={isProcessing}
+ >
+ <RefreshCw
+ className={`h-4 w-4 ${isProcessing ? "animate-spin" : ""}`}
+ />
+ </Button>
+
+ <Button
+ variant="ghost"
+ size="icon"
+ onClick={clearHistory}
+ title="Clear History"
+ disabled={isProcessing}
+ >
+ <Trash2 className="h-4 w-4" />
+ </Button>
+
+ <Button
+ variant="ghost"
+ size="icon"
+ onClick={() => setView("settings")}
+ title="Settings"
+ >
+ <Settings className="h-4 w-4" />
+ </Button>
+ </div>
+ </div>
+
+ {/* Chat Area */}
+ <div className="flex-1 bg-black/20 border border-white/5 rounded-xl overflow-hidden flex flex-col relative">
+ {/* Warning when assistant is disabled */}
+ {!settings.assistant.enabled && (
+ <div className="absolute top-4 left-1/2 transform -translate-x-1/2 z-10">
+ <Card className="bg-yellow-500/10 border-yellow-500/20">
+ <CardContent className="p-3 flex items-center gap-2">
+ <AlertTriangle className="h-4 w-4 text-yellow-500" />
+ <span className="text-yellow-500 text-sm font-medium">
+ Assistant is disabled. Enable it in Settings &gt; AI
+ Assistant.
+ </span>
+ </CardContent>
+ </Card>
+ </div>
+ )}
+
+ {/* Provider offline warning */}
+ {settings.assistant.enabled && !isProviderHealthy && (
+ <div className="absolute top-4 left-1/2 transform -translate-x-1/2 z-10">
+ <Card className="bg-red-500/10 border-red-500/20">
+ <CardContent className="p-3 flex items-center gap-2">
+ <AlertTriangle className="h-4 w-4 text-red-500" />
+ <div className="flex flex-col">
+ <span className="text-red-500 text-sm font-medium">
+ Assistant is offline
+ </span>
+ <span className="text-red-400 text-xs">
+ {getProviderHelpText()}
+ </span>
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ )}
+
+ {/* Messages Container */}
+ <ScrollArea className="flex-1 p-4 lg:p-6" ref={messagesContainerRef}>
+ {messages.length === 0 ? (
+ <div className="flex flex-col items-center justify-center h-full text-zinc-400 gap-4 mt-8">
+ <div className="p-4 bg-zinc-800/50 rounded-full">
+ <Bot className="h-12 w-12" />
+ </div>
+ <h3 className="text-xl font-medium">How can I help you today?</h3>
+ <p className="text-center max-w-md text-sm">
+ I can analyze your game logs, diagnose crashes, or explain mod
+ features.
+ {!settings.assistant.enabled && (
+ <span className="block mt-2 text-yellow-500">
+ Assistant is disabled. Enable it in{" "}
+ <button
+ type="button"
+ onClick={() => setView("settings")}
+ className="text-indigo-400 hover:underline"
+ >
+ Settings &gt; AI Assistant
+ </button>
+ .
+ </span>
+ )}
+ </p>
+ <div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-2 max-w-lg">
+ <Button
+ variant="outline"
+ className="text-left h-auto py-3"
+ onClick={() =>
+ setInput("How do I fix Minecraft crashing on launch?")
+ }
+ disabled={isProcessing}
+ >
+ <div className="text-sm">
+ How do I fix Minecraft crashing on launch?
+ </div>
+ </Button>
+ <Button
+ variant="outline"
+ className="text-left h-auto py-3"
+ onClick={() =>
+ setInput("What's the best way to improve FPS?")
+ }
+ disabled={isProcessing}
+ >
+ <div className="text-sm">
+ What's the best way to improve FPS?
+ </div>
+ </Button>
+ <Button
+ variant="outline"
+ className="text-left h-auto py-3"
+ onClick={() =>
+ setInput(
+ "Can you help me install Fabric for Minecraft 1.20.4?",
+ )
+ }
+ disabled={isProcessing}
+ >
+ <div className="text-sm">
+ Can you help me install Fabric for 1.20.4?
+ </div>
+ </Button>
+ <Button
+ variant="outline"
+ className="text-left h-auto py-3"
+ onClick={() =>
+ setInput("What mods do you recommend for performance?")
+ }
+ disabled={isProcessing}
+ >
+ <div className="text-sm">
+ What mods do you recommend for performance?
+ </div>
+ </Button>
+ </div>
+ </div>
+ ) : (
+ <>
+ {messages.map((message, index) => renderMessage(message, index))}
+ {isProcessing && streamingContent && (
+ <div className="flex justify-start mb-4">
+ <div className="max-w-[80%] bg-zinc-800 text-zinc-100 rounded-2xl rounded-bl-none px-4 py-3">
+ <div
+ className="prose prose-invert max-w-none"
+ dangerouslySetInnerHTML={{
+ __html: renderMarkdown(streamingContent),
+ }}
+ />
+ <div className="flex items-center gap-1 mt-2 text-xs text-zinc-400">
+ <Loader2 className="h-3 w-3 animate-spin" />
+ <span>Assistant is typing...</span>
+ </div>
+ </div>
+ </div>
+ )}
+ </>
+ )}
+ <div ref={messagesEndRef} />
+ </ScrollArea>
+
+ <Separator />
+
+ {/* Input Area */}
+ <div className="p-3 lg:p-4">
+ <div className="flex gap-2">
+ <Textarea
+ placeholder={
+ settings.assistant.enabled
+ ? "Ask about your game..."
+ : "Assistant is disabled. Enable it in Settings to use."
+ }
+ value={input}
+ onChange={(e) => setInput(e.target.value)}
+ onKeyDown={handleKeyDown}
+ className="min-h-11 max-h-50 resize-none border-zinc-700 bg-zinc-900/50 focus:bg-zinc-900/80"
+ disabled={!settings.assistant.enabled || isProcessing}
+ />
+ <Button
+ onClick={handleSubmit}
+ disabled={
+ !settings.assistant.enabled || !input.trim() || isProcessing
+ }
+ className="px-6 bg-indigo-600 hover:bg-indigo-700 text-white"
+ >
+ {isProcessing ? (
+ <Loader2 className="h-4 w-4 animate-spin" />
+ ) : (
+ <Send className="h-4 w-4" />
+ )}
+ </Button>
+ </div>
+ <div className="mt-2 flex items-center justify-between">
+ <div className="text-xs text-zinc-500">
+ {settings.assistant.enabled
+ ? "Press Enter to send, Shift+Enter for new line"
+ : "Enable the assistant in Settings to use"}
+ </div>
+ <div className="text-xs text-zinc-500">
+ Model: {model} • Provider: {provider}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}