From 32a4d85af937e4fd882fa671aee8b72878cc564f Mon Sep 17 00:00:00 2001 From: 苏向夜 Date: Sun, 29 Mar 2026 00:58:15 +0800 Subject: feat(ui): remove all stores --- packages/ui/src/models/assistant-store.ts.bk | 201 ++++++++++ packages/ui/src/models/logs-store.ts.bk | 200 ++++++++++ packages/ui/src/models/settings-store.ts.bk | 568 +++++++++++++++++++++++++++ packages/ui/src/pages/assistant-view.tsx.bk | 485 ----------------------- packages/ui/src/stores/assistant-store.ts | 201 ---------- packages/ui/src/stores/auth-store.ts | 296 -------------- packages/ui/src/stores/logs-store.ts | 200 ---------- packages/ui/src/stores/releases-store.ts | 63 --- packages/ui/src/stores/settings-store.ts | 568 --------------------------- packages/ui/src/stores/ui-store.ts | 42 -- 10 files changed, 969 insertions(+), 1855 deletions(-) create mode 100644 packages/ui/src/models/assistant-store.ts.bk create mode 100644 packages/ui/src/models/logs-store.ts.bk create mode 100644 packages/ui/src/models/settings-store.ts.bk delete mode 100644 packages/ui/src/pages/assistant-view.tsx.bk delete mode 100644 packages/ui/src/stores/assistant-store.ts delete mode 100644 packages/ui/src/stores/auth-store.ts delete mode 100644 packages/ui/src/stores/logs-store.ts delete mode 100644 packages/ui/src/stores/releases-store.ts delete mode 100644 packages/ui/src/stores/settings-store.ts delete mode 100644 packages/ui/src/stores/ui-store.ts (limited to 'packages') diff --git a/packages/ui/src/models/assistant-store.ts.bk b/packages/ui/src/models/assistant-store.ts.bk new file mode 100644 index 0000000..180031b --- /dev/null +++ b/packages/ui/src/models/assistant-store.ts.bk @@ -0,0 +1,201 @@ +import { invoke } from "@tauri-apps/api/core"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +import { create } from "zustand"; +import type { GenerationStats, StreamChunk } from "@/types/bindings/assistant"; + +export interface Message { + role: "user" | "assistant" | "system"; + content: string; + stats?: GenerationStats; +} + +interface AssistantState { + // State + messages: Message[]; + isProcessing: boolean; + isProviderHealthy: boolean | undefined; + streamingContent: string; + initialized: boolean; + streamUnlisten: UnlistenFn | null; + + // Actions + init: () => Promise; + checkHealth: () => Promise; + sendMessage: ( + content: string, + isEnabled: boolean, + provider: string, + endpoint: string, + ) => Promise; + finishStreaming: () => void; + clearHistory: () => void; + setMessages: (messages: Message[]) => void; + setIsProcessing: (isProcessing: boolean) => void; + setIsProviderHealthy: (isProviderHealthy: boolean | undefined) => void; + setStreamingContent: (streamingContent: string) => void; +} + +export const useAssistantStore = create((set, get) => ({ + // Initial state + messages: [], + isProcessing: false, + isProviderHealthy: false, + streamingContent: "", + initialized: false, + streamUnlisten: null, + + // Actions + init: async () => { + const { initialized } = get(); + if (initialized) return; + set({ initialized: true }); + await get().checkHealth(); + }, + + checkHealth: async () => { + try { + const isHealthy = await invoke("assistant_check_health"); + set({ isProviderHealthy: isHealthy }); + } catch (e) { + console.error("Failed to check provider health:", e); + set({ isProviderHealthy: false }); + } + }, + + finishStreaming: () => { + const { streamUnlisten } = get(); + set({ isProcessing: false, streamingContent: "" }); + + if (streamUnlisten) { + streamUnlisten(); + set({ streamUnlisten: null }); + } + }, + + sendMessage: async (content, isEnabled, provider, endpoint) => { + if (!content.trim()) return; + + const { messages } = get(); + + if (!isEnabled) { + const newMessage: Message = { + role: "assistant", + content: "Assistant is disabled. Enable it in Settings > AI Assistant.", + }; + set({ messages: [...messages, { role: "user", content }, newMessage] }); + return; + } + + // Add user message + const userMessage: Message = { role: "user", content }; + const updatedMessages = [...messages, userMessage]; + set({ + messages: updatedMessages, + isProcessing: true, + streamingContent: "", + }); + + // Add empty assistant message for streaming + const assistantMessage: Message = { role: "assistant", content: "" }; + const withAssistantMessage = [...updatedMessages, assistantMessage]; + set({ messages: withAssistantMessage }); + + try { + // Set up stream listener + const unlisten = await listen( + "assistant-stream", + (event) => { + const chunk = event.payload; + const currentState = get(); + + if (chunk.content) { + const newStreamingContent = + currentState.streamingContent + chunk.content; + const currentMessages = [...currentState.messages]; + const lastIdx = currentMessages.length - 1; + + if (lastIdx >= 0 && currentMessages[lastIdx].role === "assistant") { + currentMessages[lastIdx] = { + ...currentMessages[lastIdx], + content: newStreamingContent, + }; + set({ + streamingContent: newStreamingContent, + messages: currentMessages, + }); + } + } + + if (chunk.done) { + const finalMessages = [...currentState.messages]; + const lastIdx = finalMessages.length - 1; + + if ( + chunk.stats && + lastIdx >= 0 && + finalMessages[lastIdx].role === "assistant" + ) { + finalMessages[lastIdx] = { + ...finalMessages[lastIdx], + stats: chunk.stats, + }; + set({ messages: finalMessages }); + } + + get().finishStreaming(); + } + }, + ); + + set({ streamUnlisten: unlisten }); + + // Start streaming chat + await invoke("assistant_chat_stream", { + messages: withAssistantMessage.slice(0, -1), // Exclude the empty assistant message + }); + } catch (e) { + console.error("Failed to send message:", e); + const errorMessage = e instanceof Error ? e.message : String(e); + + let helpText = ""; + if (provider === "ollama") { + helpText = `\n\nPlease ensure Ollama is running at ${endpoint}.`; + } else if (provider === "openai") { + helpText = "\n\nPlease check your OpenAI API key in Settings."; + } + + // Update the last message with error + const currentMessages = [...get().messages]; + const lastIdx = currentMessages.length - 1; + if (lastIdx >= 0 && currentMessages[lastIdx].role === "assistant") { + currentMessages[lastIdx] = { + role: "assistant", + content: `Error: ${errorMessage}${helpText}`, + }; + set({ messages: currentMessages }); + } + + get().finishStreaming(); + } + }, + + clearHistory: () => { + set({ messages: [], streamingContent: "" }); + }, + + setMessages: (messages) => { + set({ messages }); + }, + + setIsProcessing: (isProcessing) => { + set({ isProcessing }); + }, + + setIsProviderHealthy: (isProviderHealthy) => { + set({ isProviderHealthy }); + }, + + setStreamingContent: (streamingContent) => { + set({ streamingContent }); + }, +})); diff --git a/packages/ui/src/models/logs-store.ts.bk b/packages/ui/src/models/logs-store.ts.bk new file mode 100644 index 0000000..b19f206 --- /dev/null +++ b/packages/ui/src/models/logs-store.ts.bk @@ -0,0 +1,200 @@ +import { listen } from "@tauri-apps/api/event"; +import { create } from "zustand"; + +export interface LogEntry { + id: number; + timestamp: string; + level: "info" | "warn" | "error" | "debug" | "fatal"; + source: string; + message: string; +} + +// Parse Minecraft/Java log format: [HH:MM:SS] [Thread/LEVEL]: message +// or: [HH:MM:SS] [Thread/LEVEL] [Source]: message +const GAME_LOG_REGEX = + /^\[[\d:]+\]\s*\[([^\]]+)\/(\w+)\](?:\s*\[([^\]]+)\])?:\s*(.*)$/; + +function parseGameLogLevel(levelStr: string): LogEntry["level"] { + const upper = levelStr.toUpperCase(); + if (upper === "INFO") return "info"; + if (upper === "WARN" || upper === "WARNING") return "warn"; + if (upper === "ERROR" || upper === "SEVERE") return "error"; + if ( + upper === "DEBUG" || + upper === "TRACE" || + upper === "FINE" || + upper === "FINER" || + upper === "FINEST" + ) + return "debug"; + if (upper === "FATAL") return "fatal"; + return "info"; +} + +interface LogsState { + // State + logs: LogEntry[]; + sources: Set; + nextId: number; + maxLogs: number; + initialized: boolean; + + // Actions + addLog: (level: LogEntry["level"], source: string, message: string) => void; + addGameLog: (rawLine: string, isStderr: boolean) => void; + clear: () => void; + exportLogs: (filteredLogs: LogEntry[]) => string; + init: () => Promise; + setLogs: (logs: LogEntry[]) => void; + setSources: (sources: Set) => void; +} + +export const useLogsStore = create((set, get) => ({ + // Initial state + logs: [], + sources: new Set(["Launcher"]), + nextId: 0, + maxLogs: 5000, + initialized: false, + + // Actions + addLog: (level, source, message) => { + const { nextId, logs, maxLogs, sources } = get(); + const now = new Date(); + const timestamp = + now.toLocaleTimeString() + + "." + + now.getMilliseconds().toString().padStart(3, "0"); + + const newLog: LogEntry = { + id: nextId, + timestamp, + level, + source, + message, + }; + + const newLogs = [...logs, newLog]; + const newSources = new Set(sources); + + // Track source + if (!newSources.has(source)) { + newSources.add(source); + } + + // Trim logs if exceeding max + const trimmedLogs = + newLogs.length > maxLogs ? newLogs.slice(-maxLogs) : newLogs; + + set({ + logs: trimmedLogs, + sources: newSources, + nextId: nextId + 1, + }); + }, + + addGameLog: (rawLine, isStderr) => { + const match = rawLine.match(GAME_LOG_REGEX); + + if (match) { + const [, thread, levelStr, extraSource, message] = match; + const level = parseGameLogLevel(levelStr); + // Use extraSource if available, otherwise use thread name as source hint + const source = extraSource || `Game/${thread.split("-")[0]}`; + get().addLog(level, source, message); + } else { + // Fallback: couldn't parse, use stderr as error indicator + const level = isStderr ? "error" : "info"; + get().addLog(level, "Game", rawLine); + } + }, + + clear: () => { + set({ + logs: [], + sources: new Set(["Launcher"]), + }); + get().addLog("info", "Launcher", "Logs cleared"); + }, + + exportLogs: (filteredLogs) => { + return filteredLogs + .map( + (l) => + `[${l.timestamp}] [${l.source}/${l.level.toUpperCase()}] ${l.message}`, + ) + .join("\n"); + }, + + init: async () => { + const { initialized } = get(); + if (initialized) return; + + set({ initialized: true }); + + // Initial log + get().addLog("info", "Launcher", "Logs initialized"); + + // General Launcher Logs + await listen("launcher-log", (e) => { + get().addLog("info", "Launcher", e.payload); + }); + + // Game Stdout - parse log level + await listen("game-stdout", (e) => { + get().addGameLog(e.payload, false); + }); + + // Game Stderr - parse log level, default to error + await listen("game-stderr", (e) => { + get().addGameLog(e.payload, true); + }); + + // Download Events (Summarized) + await listen("download-start", (e: any) => { + get().addLog( + "info", + "Downloader", + `Starting batch download of ${e.payload} files...`, + ); + }); + + await listen("download-complete", () => { + get().addLog("info", "Downloader", "All downloads completed."); + }); + + // Listen to file download progress to log finished files + await listen("download-progress", (e) => { + const p = e.payload; + if (p.status === "Finished") { + if (p.file.endsWith(".jar")) { + get().addLog("info", "Downloader", `Downloaded ${p.file}`); + } + } + }); + + // Java Download + await listen("java-download-progress", (e) => { + const p = e.payload; + if (p.status === "Downloading" && p.percentage === 0) { + get().addLog( + "info", + "JavaInstaller", + `Downloading Java: ${p.file_name}`, + ); + } else if (p.status === "Completed") { + get().addLog("info", "JavaInstaller", `Java installed: ${p.file_name}`); + } else if (p.status === "Error") { + get().addLog("error", "JavaInstaller", `Java download error`); + } + }); + }, + + setLogs: (logs) => { + set({ logs }); + }, + + setSources: (sources) => { + set({ sources }); + }, +})); diff --git a/packages/ui/src/models/settings-store.ts.bk b/packages/ui/src/models/settings-store.ts.bk new file mode 100644 index 0000000..0bfc1e1 --- /dev/null +++ b/packages/ui/src/models/settings-store.ts.bk @@ -0,0 +1,568 @@ +import { convertFileSrc, invoke } from "@tauri-apps/api/core"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +import { toast } from "sonner"; +import { create } from "zustand"; +import { downloadAdoptiumJava } from "@/client"; +import type { ModelInfo } from "../types/bindings/assistant"; +import type { LauncherConfig } from "../types/bindings/config"; +import type { + JavaDownloadProgress, + PendingJavaDownload, +} from "../types/bindings/downloader"; +import type { + JavaCatalog, + JavaInstallation, + JavaReleaseInfo, +} from "../types/bindings/java"; + +type JavaDownloadSource = "adoptium" | "mojang" | "azul"; + +/** + * State shape for settings store. + * + * Note: Uses camelCase naming to match ts-rs generated bindings (which now use + * `serde(rename_all = "camelCase")`). When reading raw binding objects from + * invoke, convert/mapping should be applied where necessary. + */ +interface SettingsState { + // State + settings: LauncherConfig; + javaInstallations: JavaInstallation[]; + isDetectingJava: boolean; + showJavaDownloadModal: boolean; + selectedDownloadSource: JavaDownloadSource; + javaCatalog: JavaCatalog | null; + isLoadingCatalog: boolean; + catalogError: string; + selectedMajorVersion: number | null; + selectedImageType: "jre" | "jdk"; + showOnlyRecommended: boolean; + searchQuery: string; + isDownloadingJava: boolean; + downloadProgress: JavaDownloadProgress | null; + javaDownloadStatus: string; + pendingDownloads: PendingJavaDownload[]; + ollamaModels: ModelInfo[]; + openaiModels: ModelInfo[]; + isLoadingOllamaModels: boolean; + isLoadingOpenaiModels: boolean; + ollamaModelsError: string; + openaiModelsError: string; + showConfigEditor: boolean; + rawConfigContent: string; + configFilePath: string; + configEditorError: string; + + // Computed / derived + backgroundUrl: string | undefined; + filteredReleases: JavaReleaseInfo[]; + availableMajorVersions: number[]; + installStatus: ( + version: number, + imageType: string, + ) => "installed" | "downloading" | "available"; + selectedRelease: JavaReleaseInfo | null; + currentModelOptions: Array<{ + value: string; + label: string; + details?: string; + }>; + + // Actions + loadSettings: () => Promise; + saveSettings: () => Promise; + // compatibility helper to mirror the older set({ key: value }) usage + set: (patch: Partial>) => void; + + detectJava: () => Promise; + selectJava: (path: string) => void; + + openJavaDownloadModal: () => Promise; + closeJavaDownloadModal: () => void; + loadJavaCatalog: (forceRefresh: boolean) => Promise; + refreshCatalog: () => Promise; + loadPendingDownloads: () => Promise; + selectMajorVersion: (version: number) => void; + downloadJava: () => Promise; + cancelDownload: () => Promise; + resumeDownloads: () => Promise; + + openConfigEditor: () => Promise; + closeConfigEditor: () => void; + saveRawConfig: () => Promise; + + loadOllamaModels: () => Promise; + loadOpenaiModels: () => Promise; + + setSetting: ( + key: K, + value: LauncherConfig[K], + ) => void; + setAssistantSetting: ( + key: K, + value: LauncherConfig["assistant"][K], + ) => void; + setFeatureFlag: ( + key: K, + value: LauncherConfig["featureFlags"][K], + ) => void; + + // Private + progressUnlisten: UnlistenFn | null; +} + +/** + * Default settings (camelCase) — lightweight defaults used until `get_settings` + * returns real values. + */ +const defaultSettings: LauncherConfig = { + minMemory: 1024, + maxMemory: 2048, + javaPath: "java", + width: 854, + height: 480, + downloadThreads: 32, + enableGpuAcceleration: false, + enableVisualEffects: true, + activeEffect: "constellation", + theme: "dark", + customBackgroundPath: null, + logUploadService: "paste.rs", + pastebinApiKey: null, + assistant: { + enabled: true, + llmProvider: "ollama", + ollamaEndpoint: "http://localhost:11434", + ollamaModel: "llama3", + openaiApiKey: null, + openaiEndpoint: "https://api.openai.com/v1", + openaiModel: "gpt-3.5-turbo", + systemPrompt: + "You are a helpful Minecraft expert assistant. You help players with game issues, mod installation, performance optimization, and gameplay tips. Analyze any game logs provided and give concise, actionable advice.", + responseLanguage: "auto", + ttsEnabled: false, + ttsProvider: "disabled", + }, + useSharedCaches: false, + keepLegacyPerInstanceStorage: true, + featureFlags: { + demoUser: false, + quickPlayEnabled: false, + quickPlayPath: null, + quickPlaySingleplayer: true, + quickPlayMultiplayerServer: null, + }, +}; + +export const useSettingsStore = create((set, get) => ({ + // initial state + settings: defaultSettings, + javaInstallations: [], + isDetectingJava: false, + showJavaDownloadModal: false, + selectedDownloadSource: "adoptium", + javaCatalog: null, + isLoadingCatalog: false, + catalogError: "", + selectedMajorVersion: null, + selectedImageType: "jre", + showOnlyRecommended: true, + searchQuery: "", + isDownloadingJava: false, + downloadProgress: null, + javaDownloadStatus: "", + pendingDownloads: [], + ollamaModels: [], + openaiModels: [], + isLoadingOllamaModels: false, + isLoadingOpenaiModels: false, + ollamaModelsError: "", + openaiModelsError: "", + showConfigEditor: false, + rawConfigContent: "", + configFilePath: "", + configEditorError: "", + progressUnlisten: null, + + // derived getters + get backgroundUrl() { + const { settings } = get(); + if (settings.customBackgroundPath) { + return convertFileSrc(settings.customBackgroundPath); + } + return undefined; + }, + + get filteredReleases() { + const { + javaCatalog, + selectedMajorVersion, + selectedImageType, + showOnlyRecommended, + searchQuery, + } = get(); + + if (!javaCatalog) return []; + + let releases = javaCatalog.releases; + + if (selectedMajorVersion !== null) { + releases = releases.filter( + (r) => r.majorVersion === selectedMajorVersion, + ); + } + + releases = releases.filter((r) => r.imageType === selectedImageType); + + if (showOnlyRecommended) { + releases = releases.filter((r) => r.isLts); + } + + if (searchQuery.trim() !== "") { + const q = searchQuery.toLowerCase(); + releases = releases.filter( + (r) => + r.version.toLowerCase().includes(q) || + (r.releaseName ?? "").toLowerCase().includes(q), + ); + } + + // sort newest-first by parsed version number + return releases.sort((a, b) => { + const aVer = parseFloat(a.version.split("-")[0]); + const bVer = parseFloat(b.version.split("-")[0]); + return bVer - aVer; + }); + }, + + get availableMajorVersions() { + return get().javaCatalog?.availableMajorVersions || []; + }, + + installStatus: (version: number, imageType: string) => { + const { + javaInstallations, + pendingDownloads, + isDownloadingJava, + downloadProgress, + } = get(); + + const installed = javaInstallations.some( + (inst) => parseInt(inst.version.split(".")[0], 10) === version, + ); + if (installed) return "installed"; + + if ( + isDownloadingJava && + downloadProgress?.fileName?.includes(`${version}`) + ) { + return "downloading"; + } + + const pending = pendingDownloads.some( + (d) => d.majorVersion === version && d.imageType === imageType, + ); + if (pending) return "downloading"; + + return "available"; + }, + + get selectedRelease() { + const { javaCatalog, selectedMajorVersion, selectedImageType } = get(); + if (!javaCatalog || selectedMajorVersion === null) return null; + return ( + javaCatalog.releases.find( + (r) => + r.majorVersion === selectedMajorVersion && + r.imageType === selectedImageType, + ) || null + ); + }, + + get currentModelOptions() { + const { settings, ollamaModels, openaiModels } = get(); + const provider = settings.assistant.llmProvider; + if (provider === "ollama") { + return ollamaModels.map((m) => ({ + value: m.id, + label: m.name, + details: m.details || m.size || "", + })); + } else { + return openaiModels.map((m) => ({ + value: m.id, + label: m.name, + details: m.details || "", + })); + } + }, + + // actions + loadSettings: async () => { + try { + const result = await invoke("get_settings"); + // result already uses camelCase fields from bindings + set({ settings: result }); + + // enforce dark theme at app-level if necessary + if (result.theme !== "dark") { + const updated = { ...result, theme: "dark" } as LauncherConfig; + set({ settings: updated }); + await invoke("save_settings", { config: updated }); + } + + // ensure customBackgroundPath is undefined rather than null for reactiveness + if (!result.customBackgroundPath) { + set((s) => ({ + settings: { ...s.settings, customBackgroundPath: null }, + })); + } + } catch (e) { + console.error("Failed to load settings:", e); + } + }, + + saveSettings: async () => { + try { + const { settings } = get(); + + // Clean up empty strings to null where appropriate + if ((settings.customBackgroundPath ?? "") === "") { + set((state) => ({ + settings: { ...state.settings, customBackgroundPath: null }, + })); + } + + await invoke("save_settings", { config: settings }); + toast.success("Settings saved!"); + } catch (e) { + console.error("Failed to save settings:", e); + toast.error(`Error saving settings: ${String(e)}`); + } + }, + + set: (patch: Partial>) => { + set(patch); + }, + + detectJava: async () => { + set({ isDetectingJava: true }); + try { + const installs = await invoke("detect_java"); + set({ javaInstallations: installs }); + if (installs.length === 0) toast.info("No Java installations found"); + else toast.success(`Found ${installs.length} Java installation(s)`); + } catch (e) { + console.error("Failed to detect Java:", e); + toast.error(`Error detecting Java: ${String(e)}`); + } finally { + set({ isDetectingJava: false }); + } + }, + + selectJava: (path: string) => { + set((s) => ({ settings: { ...s.settings, javaPath: path } })); + }, + + openJavaDownloadModal: async () => { + set({ + showJavaDownloadModal: true, + javaDownloadStatus: "", + catalogError: "", + downloadProgress: null, + }); + + // attach event listener for download progress + const state = get(); + if (state.progressUnlisten) { + state.progressUnlisten(); + } + + const unlisten = await listen( + "java-download-progress", + (event) => { + set({ downloadProgress: event.payload }); + }, + ); + + set({ progressUnlisten: unlisten }); + + // load catalog and pending downloads + await get().loadJavaCatalog(false); + await get().loadPendingDownloads(); + }, + + closeJavaDownloadModal: () => { + const { isDownloadingJava, progressUnlisten } = get(); + + if (!isDownloadingJava) { + set({ showJavaDownloadModal: false }); + if (progressUnlisten) { + try { + progressUnlisten(); + } catch { + // ignore + } + set({ progressUnlisten: null }); + } + } + }, + + loadJavaCatalog: async (forceRefresh: boolean) => { + set({ isLoadingCatalog: true, catalogError: "" }); + try { + const cmd = forceRefresh ? "refresh_java_catalog" : "get_java_catalog"; + const result = await invoke(cmd); + set({ javaCatalog: result, isLoadingCatalog: false }); + } catch (e) { + console.error("Failed to load Java catalog:", e); + set({ catalogError: String(e), isLoadingCatalog: false }); + } + }, + + refreshCatalog: async () => { + await get().loadJavaCatalog(true); + }, + + loadPendingDownloads: async () => { + try { + const pending = await invoke( + "get_pending_java_downloads", + ); + set({ pendingDownloads: pending }); + } catch (e) { + console.error("Failed to load pending downloads:", e); + } + }, + + selectMajorVersion: (version: number) => { + set({ selectedMajorVersion: version }); + }, + + downloadJava: async () => { + const { selectedMajorVersion, selectedImageType, selectedDownloadSource } = + get(); + if (!selectedMajorVersion) return; + set({ isDownloadingJava: true, javaDownloadStatus: "Starting..." }); + try { + const result = await downloadAdoptiumJava( + selectedMajorVersion, + selectedImageType, + selectedDownloadSource, + ); + set({ + javaDownloadStatus: `Java ${selectedMajorVersion} download started: ${result.path}`, + }); + toast.success("Download started"); + } catch (e) { + console.error("Failed to download Java:", e); + toast.error(`Failed to start Java download: ${String(e)}`); + } finally { + set({ isDownloadingJava: false }); + } + }, + + cancelDownload: async () => { + try { + await invoke("cancel_java_download"); + toast.success("Cancelled Java download"); + set({ isDownloadingJava: false, javaDownloadStatus: "" }); + } catch (e) { + console.error("Failed to cancel download:", e); + toast.error(`Failed to cancel download: ${String(e)}`); + } + }, + + resumeDownloads: async () => { + try { + const installed = await invoke("resume_java_downloads"); + if (installed) toast.success("Resumed Java downloads"); + else toast.info("No downloads to resume"); + } catch (e) { + console.error("Failed to resume downloads:", e); + toast.error(`Failed to resume downloads: ${String(e)}`); + } + }, + + openConfigEditor: async () => { + try { + const path = await invoke("get_config_path"); + const content = await invoke("read_config_raw"); + set({ + configFilePath: path, + rawConfigContent: content, + showConfigEditor: true, + }); + } catch (e) { + console.error("Failed to open config editor:", e); + set({ configEditorError: String(e) }); + } + }, + + closeConfigEditor: () => { + set({ + showConfigEditor: false, + rawConfigContent: "", + configFilePath: "", + configEditorError: "", + }); + }, + + saveRawConfig: async () => { + try { + await invoke("write_config_raw", { content: get().rawConfigContent }); + toast.success("Config saved"); + set({ showConfigEditor: false }); + } catch (e) { + console.error("Failed to save config:", e); + set({ configEditorError: String(e) }); + toast.error(`Failed to save config: ${String(e)}`); + } + }, + + loadOllamaModels: async () => { + set({ isLoadingOllamaModels: true, ollamaModelsError: "" }); + try { + const models = await invoke("get_ollama_models"); + set({ ollamaModels: models, isLoadingOllamaModels: false }); + } catch (e) { + console.error("Failed to load Ollama models:", e); + set({ isLoadingOllamaModels: false, ollamaModelsError: String(e) }); + } + }, + + loadOpenaiModels: async () => { + set({ isLoadingOpenaiModels: true, openaiModelsError: "" }); + try { + const models = await invoke("get_openai_models"); + set({ openaiModels: models, isLoadingOpenaiModels: false }); + } catch (e) { + console.error("Failed to load OpenAI models:", e); + set({ isLoadingOpenaiModels: false, openaiModelsError: String(e) }); + } + }, + + setSetting: (key, value) => { + set((s) => ({ + settings: { ...s.settings, [key]: value } as unknown as LauncherConfig, + })); + }, + + setAssistantSetting: (key, value) => { + set((s) => ({ + settings: { + ...s.settings, + assistant: { ...s.settings.assistant, [key]: value }, + } as LauncherConfig, + })); + }, + + setFeatureFlag: (key, value) => { + set((s) => ({ + settings: { + ...s.settings, + featureFlags: { ...s.settings.featureFlags, [key]: value }, + } as LauncherConfig, + })); + }, +})); diff --git a/packages/ui/src/pages/assistant-view.tsx.bk b/packages/ui/src/pages/assistant-view.tsx.bk deleted file mode 100644 index 56f827b..0000000 --- a/packages/ui/src/pages/assistant-view.tsx.bk +++ /dev/null @@ -1,485 +0,0 @@ -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 */} -
-
-