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/components/AssistantView.svelte | 436 +++++++++++++++++++++++++++++ ui/src/components/ConfigEditorModal.svelte | 364 ++++++++++++++++++++++++ ui/src/components/CustomSelect.svelte | 43 ++- ui/src/components/Sidebar.svelte | 5 +- 4 files changed, 843 insertions(+), 5 deletions(-) create mode 100644 ui/src/components/AssistantView.svelte create mode 100644 ui/src/components/ConfigEditorModal.svelte (limited to 'ui/src/components') 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}