From 6c6cd5052a157b658f50e04ca7c350a00c2dbd60 Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Fri, 16 Jan 2026 14:17:17 +0800 Subject: feat: add assistant view and configuration editor components Introduced a new AssistantView component for enhanced interaction with the AI assistant, allowing users to send messages and receive responses. Implemented a ConfigEditorModal for editing configuration files with JSON validation and history management. Updated the App component to integrate these new features, improving user experience and functionality in managing AI settings. --- ui/src/App.svelte | 21 +- ui/src/components/AssistantView.svelte | 436 +++++++++++++++++++++++++++++ ui/src/components/ConfigEditorModal.svelte | 364 ++++++++++++++++++++++++ ui/src/components/CustomSelect.svelte | 43 ++- ui/src/components/Sidebar.svelte | 5 +- 5 files changed, 853 insertions(+), 16 deletions(-) create mode 100644 ui/src/components/AssistantView.svelte create mode 100644 ui/src/components/ConfigEditorModal.svelte diff --git a/ui/src/App.svelte b/ui/src/App.svelte index 760a15f..2b78892 100644 --- a/ui/src/App.svelte +++ b/ui/src/App.svelte @@ -10,6 +10,7 @@ import LoginModal from "./components/LoginModal.svelte"; import ParticleBackground from "./components/ParticleBackground.svelte"; import SettingsView from "./components/SettingsView.svelte"; + import AssistantView from "./components/AssistantView.svelte"; import Sidebar from "./components/Sidebar.svelte"; import StatusToast from "./components/StatusToast.svelte"; import VersionsView from "./components/VersionsView.svelte"; @@ -18,6 +19,7 @@ import { gameState } from "./stores/game.svelte"; import { settingsState } from "./stores/settings.svelte"; import { uiState } from "./stores/ui.svelte"; + import { logsState } from "./stores/logs.svelte"; import { convertFileSrc } from "@tauri-apps/api/core"; let mouseX = $state(0); @@ -29,24 +31,19 @@ } onMount(async () => { + // ENFORCE DARK MODE: Always add 'dark' class and attribute + document.documentElement.classList.add('dark'); + document.documentElement.setAttribute('data-theme', 'dark'); + document.documentElement.classList.remove('light'); + authState.checkAccount(); await settingsState.loadSettings(); + logsState.init(); await settingsState.detectJava(); gameState.loadVersions(); getVersion().then((v) => (uiState.appVersion = v)); window.addEventListener("mousemove", handleMouseMove); }); - - $effect(() => { - // ENFORCE DARK MODE: Always add 'dark' class and attribute - // This combined with the @variant dark in app.css ensures dark mode is always active - // regardless of system preference settings. - document.documentElement.classList.add('dark'); - document.documentElement.setAttribute('data-theme', 'dark'); - - // Ensure 'light' class is never present - document.documentElement.classList.remove('light'); - }); onDestroy(() => { if (typeof window !== 'undefined') @@ -120,6 +117,8 @@ {:else if uiState.currentView === "settings"} + {:else if uiState.currentView === "guide"} + {/if} diff --git a/ui/src/components/AssistantView.svelte b/ui/src/components/AssistantView.svelte new file mode 100644 index 0000000..54509a5 --- /dev/null +++ b/ui/src/components/AssistantView.svelte @@ -0,0 +1,436 @@ + + +
+
+
+
+ +
+
+

Game Assistant

+

Powered by {getProviderName()}

+
+
+ +
+ {#if !settingsState.settings.assistant.enabled} +
+ + Disabled +
+ {:else if !assistantState.isProviderHealthy} +
+ + Offline +
+ {:else} +
+
+ Online +
+ {/if} + + + + + + +
+
+ + +
+ {#if assistantState.messages.length === 0} +
+ +
+

How can I help you today?

+

I can analyze your game logs, diagnose crashes, or explain mod features.

+
+ {#if !settingsState.settings.assistant.enabled} +
+ Assistant is disabled. Enable it in . +
+ {:else if !assistantState.isProviderHealthy} +
+ {getProviderHelpText()} +
+ {/if} +
+ {/if} + +
+ {#each assistantState.messages as msg, idx} +
+ {#if msg.role === 'assistant'} +
+ +
+ {/if} + +
+ {#if msg.role === 'user'} +
+ {msg.content} +
+ {:else} + {@const parsed = parseMessageContent(msg.content)} + + + {#if parsed.thinking} +
+
+ + + Thinking Process + + +
+ {parsed.thinking} + {#if parsed.isThinking} + + {/if} +
+
+
+ {/if} + + +
+ {#if parsed.content} + {@html renderMarkdown(parsed.content)} + {:else if assistantState.isProcessing && idx === assistantState.messages.length - 1 && !parsed.isThinking} + + + + + + {/if} +
+ + + {#if msg.stats} +
+
+ Eval: + {msg.stats.eval_count} tokens +
+
+ Time: + {(msg.stats.total_duration / 1e9).toFixed(2)}s +
+ {#if msg.stats.eval_duration > 0} +
+ Speed: + {(msg.stats.eval_count / (msg.stats.eval_duration / 1e9)).toFixed(1)} t/s +
+ {/if} +
+ {/if} + {/if} +
+
+ {/each} +
+ + +
+
+ + + +
+
+
+
+ + diff --git a/ui/src/components/ConfigEditorModal.svelte b/ui/src/components/ConfigEditorModal.svelte new file mode 100644 index 0000000..87a7d67 --- /dev/null +++ b/ui/src/components/ConfigEditorModal.svelte @@ -0,0 +1,364 @@ + + +
+ +
+ + diff --git a/ui/src/components/CustomSelect.svelte b/ui/src/components/CustomSelect.svelte index 2e89c75..0767471 100644 --- a/ui/src/components/CustomSelect.svelte +++ b/ui/src/components/CustomSelect.svelte @@ -13,6 +13,7 @@ placeholder?: string; disabled?: boolean; class?: string; + allowCustom?: boolean; // New prop to allow custom input onchange?: (value: string) => void; } @@ -22,17 +23,25 @@ placeholder = "Select...", disabled = false, class: className = "", + allowCustom = false, onchange }: Props = $props(); let isOpen = $state(false); let containerRef: HTMLDivElement; + let customInput = $state(""); // State for custom input let selectedOption = $derived(options.find(o => o.value === value)); + // Display label: if option exists use its label, otherwise if custom is allowed use raw value, else placeholder + let displayLabel = $derived(selectedOption ? selectedOption.label : (allowCustom && value ? value : placeholder)); function toggle() { if (!disabled) { isOpen = !isOpen; + // When opening, if current value is custom (not in options), pre-fill input + if (isOpen && allowCustom && !selectedOption) { + customInput = value; + } } } @@ -43,6 +52,13 @@ onchange?.(option.value); } + function handleCustomSubmit() { + if (!customInput.trim()) return; + value = customInput.trim(); + isOpen = false; + onchange?.(value); + } + function handleKeydown(e: KeyboardEvent) { if (disabled) return; @@ -98,8 +114,8 @@ transition-colors cursor-pointer outline-none disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:border-zinc-700" > - - {selectedOption?.label || placeholder} + + {displayLabel} + {#if allowCustom} +
+
+ e.key === 'Enter' && handleCustomSubmit()} + onclick={(e) => e.stopPropagation()} + /> + +
+
+ {/if} + {#each options as option}