From 66668d85d603c5841d755a6023aa1925559fc6d4 Mon Sep 17 00:00:00 2001 From: 苏向夜 Date: Wed, 25 Feb 2026 01:32:51 +0800 Subject: chore(workspace): replace legacy codes --- packages/ui/src/stores/assistant-store.ts | 201 ++++++++++ packages/ui/src/stores/assistant.svelte.ts | 166 --------- packages/ui/src/stores/auth-store.ts | 296 +++++++++++++++ packages/ui/src/stores/auth.svelte.ts | 192 ---------- packages/ui/src/stores/game-store.ts | 101 +++++ packages/ui/src/stores/game.svelte.ts | 78 ---- packages/ui/src/stores/instances.svelte.ts | 109 ------ packages/ui/src/stores/logs-store.ts | 200 ++++++++++ packages/ui/src/stores/logs.svelte.ts | 151 -------- packages/ui/src/stores/releases-store.ts | 63 ++++ packages/ui/src/stores/releases.svelte.ts | 36 -- packages/ui/src/stores/settings-store.ts | 568 ++++++++++++++++++++++++++++ packages/ui/src/stores/settings.svelte.ts | 570 ----------------------------- packages/ui/src/stores/ui-store.ts | 42 +++ packages/ui/src/stores/ui.svelte.ts | 32 -- 15 files changed, 1471 insertions(+), 1334 deletions(-) create mode 100644 packages/ui/src/stores/assistant-store.ts delete mode 100644 packages/ui/src/stores/assistant.svelte.ts create mode 100644 packages/ui/src/stores/auth-store.ts delete mode 100644 packages/ui/src/stores/auth.svelte.ts create mode 100644 packages/ui/src/stores/game-store.ts delete mode 100644 packages/ui/src/stores/game.svelte.ts delete mode 100644 packages/ui/src/stores/instances.svelte.ts create mode 100644 packages/ui/src/stores/logs-store.ts delete mode 100644 packages/ui/src/stores/logs.svelte.ts create mode 100644 packages/ui/src/stores/releases-store.ts delete mode 100644 packages/ui/src/stores/releases.svelte.ts create mode 100644 packages/ui/src/stores/settings-store.ts delete mode 100644 packages/ui/src/stores/settings.svelte.ts create mode 100644 packages/ui/src/stores/ui-store.ts delete mode 100644 packages/ui/src/stores/ui.svelte.ts (limited to 'packages/ui/src/stores') diff --git a/packages/ui/src/stores/assistant-store.ts b/packages/ui/src/stores/assistant-store.ts new file mode 100644 index 0000000..180031b --- /dev/null +++ b/packages/ui/src/stores/assistant-store.ts @@ -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/stores/assistant.svelte.ts b/packages/ui/src/stores/assistant.svelte.ts deleted file mode 100644 index a3f47ea..0000000 --- a/packages/ui/src/stores/assistant.svelte.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { invoke } from "@tauri-apps/api/core"; -import { listen, type UnlistenFn } from "@tauri-apps/api/event"; - -export interface GenerationStats { - total_duration: number; - load_duration: number; - prompt_eval_count: number; - prompt_eval_duration: number; - eval_count: number; - eval_duration: number; -} - -export interface Message { - role: "user" | "assistant" | "system"; - content: string; - stats?: GenerationStats; -} - -interface StreamChunk { - content: string; - done: boolean; - stats?: GenerationStats; -} - -// Module-level state using $state -let messages = $state([]); -let isProcessing = $state(false); -let isProviderHealthy = $state(false); -let streamingContent = ""; -let initialized = false; -let streamUnlisten: UnlistenFn | null = null; - -async function init() { - if (initialized) return; - initialized = true; - await checkHealth(); -} - -async function checkHealth() { - try { - isProviderHealthy = await invoke("assistant_check_health"); - } catch (e) { - console.error("Failed to check provider health:", e); - isProviderHealthy = false; - } -} - -function finishStreaming() { - isProcessing = false; - streamingContent = ""; - if (streamUnlisten) { - streamUnlisten(); - streamUnlisten = null; - } -} - -async function sendMessage( - content: string, - isEnabled: boolean, - provider: string, - endpoint: string, -) { - if (!content.trim()) return; - if (!isEnabled) { - messages = [ - ...messages, - { - role: "assistant", - content: "Assistant is disabled. Enable it in Settings > AI Assistant.", - }, - ]; - return; - } - - // Add user message - messages = [...messages, { role: "user", content }]; - isProcessing = true; - streamingContent = ""; - - // Add empty assistant message for streaming - messages = [...messages, { role: "assistant", content: "" }]; - - try { - // Set up stream listener - streamUnlisten = await listen("assistant-stream", (event) => { - const chunk = event.payload; - - if (chunk.content) { - streamingContent += chunk.content; - // Update the last message (assistant's response) - const lastIdx = messages.length - 1; - if (lastIdx >= 0 && messages[lastIdx].role === "assistant") { - messages[lastIdx] = { - ...messages[lastIdx], - content: streamingContent, - }; - // Trigger reactivity - messages = [...messages]; - } - } - - if (chunk.done) { - if (chunk.stats) { - const lastIdx = messages.length - 1; - if (lastIdx >= 0 && messages[lastIdx].role === "assistant") { - messages[lastIdx] = { - ...messages[lastIdx], - stats: chunk.stats, - }; - messages = [...messages]; - } - } - finishStreaming(); - } - }); - - // Start streaming chat - await invoke("assistant_chat_stream", { - messages: messages.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 lastIdx = messages.length - 1; - if (lastIdx >= 0 && messages[lastIdx].role === "assistant") { - messages[lastIdx] = { - role: "assistant", - content: `Error: ${errorMessage}${helpText}`, - }; - messages = [...messages]; - } - - finishStreaming(); - } -} - -function clearHistory() { - messages = []; - streamingContent = ""; -} - -// Export as an object with getters for reactive access -export const assistantState = { - get messages() { - return messages; - }, - get isProcessing() { - return isProcessing; - }, - get isProviderHealthy() { - return isProviderHealthy; - }, - init, - checkHealth, - sendMessage, - clearHistory, -}; diff --git a/packages/ui/src/stores/auth-store.ts b/packages/ui/src/stores/auth-store.ts new file mode 100644 index 0000000..bf7e3c5 --- /dev/null +++ b/packages/ui/src/stores/auth-store.ts @@ -0,0 +1,296 @@ +import { invoke } from "@tauri-apps/api/core"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +import { open } from "@tauri-apps/plugin-shell"; +import { toast } from "sonner"; +import { create } from "zustand"; +import type { Account, DeviceCodeResponse } from "../types/bindings/auth"; + +interface AuthState { + // State + currentAccount: Account | null; + isLoginModalOpen: boolean; + isLogoutConfirmOpen: boolean; + loginMode: "select" | "offline" | "microsoft"; + offlineUsername: string; + deviceCodeData: DeviceCodeResponse | null; + msLoginLoading: boolean; + msLoginStatus: string; + + // Private state + pollInterval: ReturnType | null; + isPollingRequestActive: boolean; + authProgressUnlisten: UnlistenFn | null; + + // Actions + checkAccount: () => Promise; + openLoginModal: () => void; + openLogoutConfirm: () => void; + cancelLogout: () => void; + confirmLogout: () => Promise; + closeLoginModal: () => void; + resetLoginState: () => void; + performOfflineLogin: () => Promise; + startMicrosoftLogin: () => Promise; + checkLoginStatus: (deviceCode: string) => Promise; + stopPolling: () => void; + cancelMicrosoftLogin: () => void; + setLoginMode: (mode: "select" | "offline" | "microsoft") => void; + setOfflineUsername: (username: string) => void; +} + +export const useAuthStore = create((set, get) => ({ + // Initial state + currentAccount: null, + isLoginModalOpen: false, + isLogoutConfirmOpen: false, + loginMode: "select", + offlineUsername: "", + deviceCodeData: null, + msLoginLoading: false, + msLoginStatus: "Waiting for authorization...", + + // Private state + pollInterval: null, + isPollingRequestActive: false, + authProgressUnlisten: null, + + // Actions + checkAccount: async () => { + try { + const acc = await invoke("get_active_account"); + set({ currentAccount: acc }); + } catch (error) { + console.error("Failed to check account:", error); + } + }, + + openLoginModal: () => { + const { currentAccount } = get(); + if (currentAccount) { + // Show custom logout confirmation dialog + set({ isLogoutConfirmOpen: true }); + return; + } + get().resetLoginState(); + set({ isLoginModalOpen: true }); + }, + + openLogoutConfirm: () => { + set({ isLogoutConfirmOpen: true }); + }, + + cancelLogout: () => { + set({ isLogoutConfirmOpen: false }); + }, + + confirmLogout: async () => { + set({ isLogoutConfirmOpen: false }); + try { + await invoke("logout"); + set({ currentAccount: null }); + } catch (error) { + console.error("Logout failed:", error); + } + }, + + closeLoginModal: () => { + get().stopPolling(); + set({ isLoginModalOpen: false }); + }, + + resetLoginState: () => { + set({ + loginMode: "select", + offlineUsername: "", + deviceCodeData: null, + msLoginLoading: false, + msLoginStatus: "Waiting for authorization...", + }); + }, + + performOfflineLogin: async () => { + const { offlineUsername } = get(); + if (!offlineUsername.trim()) return; + + try { + const account = await invoke("login_offline", { + username: offlineUsername, + }); + set({ + currentAccount: account, + isLoginModalOpen: false, + offlineUsername: "", + }); + } catch (error) { + // Keep UI-friendly behavior consistent with prior code + alert("Login failed: " + String(error)); + } + }, + + startMicrosoftLogin: async () => { + // Prepare UI state + set({ + msLoginLoading: true, + msLoginStatus: "Waiting for authorization...", + loginMode: "microsoft", + deviceCodeData: null, + }); + + // Listen to general launcher logs so we can display progress to the user. + // The backend emits logs via "launcher-log"; using that keeps this store decoupled + // from a dedicated auth event channel (backend may reuse launcher-log). + try { + const unlisten = await listen("launcher-log", (event) => { + const payload = event.payload; + // Normalize payload to string if possible + const message = + typeof payload === "string" + ? payload + : (payload?.toString?.() ?? JSON.stringify(payload)); + set({ msLoginStatus: message }); + }); + set({ authProgressUnlisten: unlisten }); + } catch (err) { + console.warn("Failed to attach launcher-log listener:", err); + } + + try { + const deviceCodeData = await invoke( + "start_microsoft_login", + ); + set({ deviceCodeData }); + + if (deviceCodeData) { + // Try to copy user code to clipboard for convenience (best-effort) + try { + await navigator.clipboard?.writeText(deviceCodeData.userCode ?? ""); + } catch (err) { + // ignore clipboard errors + console.debug("Clipboard copy failed:", err); + } + + // Open verification URI in default browser + try { + if (deviceCodeData.verificationUri) { + await open(deviceCodeData.verificationUri); + } + } catch (err) { + console.debug("Failed to open verification URI:", err); + } + + // Start polling for completion + // `interval` from the bindings is a bigint (seconds). Convert safely to number. + const intervalSeconds = + deviceCodeData.interval !== undefined && + deviceCodeData.interval !== null + ? Number(deviceCodeData.interval) + : 5; + const intervalMs = intervalSeconds * 1000; + const pollInterval = setInterval( + () => get().checkLoginStatus(deviceCodeData.deviceCode), + intervalMs, + ); + set({ pollInterval }); + } + } catch (error) { + toast.error(`Failed to start Microsoft login: ${error}`); + set({ loginMode: "select" }); + // cleanup listener if present + const { authProgressUnlisten } = get(); + if (authProgressUnlisten) { + authProgressUnlisten(); + set({ authProgressUnlisten: null }); + } + } finally { + set({ msLoginLoading: false }); + } + }, + + checkLoginStatus: async (deviceCode: string) => { + const { isPollingRequestActive } = get(); + if (isPollingRequestActive) return; + + set({ isPollingRequestActive: true }); + + try { + const account = await invoke("complete_microsoft_login", { + deviceCode, + }); + + // On success, stop polling and cleanup listener + get().stopPolling(); + const { authProgressUnlisten } = get(); + if (authProgressUnlisten) { + authProgressUnlisten(); + set({ authProgressUnlisten: null }); + } + + set({ + currentAccount: account, + isLoginModalOpen: false, + }); + } catch (error: unknown) { + const errStr = String(error); + if (errStr.includes("authorization_pending")) { + // Still waiting — keep polling + } else { + set({ msLoginStatus: "Error: " + errStr }); + + if ( + errStr.includes("expired_token") || + errStr.includes("access_denied") + ) { + // Terminal errors — stop polling and reset state + get().stopPolling(); + const { authProgressUnlisten } = get(); + if (authProgressUnlisten) { + authProgressUnlisten(); + set({ authProgressUnlisten: null }); + } + alert("Login failed: " + errStr); + set({ loginMode: "select" }); + } + } + } finally { + set({ isPollingRequestActive: false }); + } + }, + + stopPolling: () => { + const { pollInterval, authProgressUnlisten } = get(); + if (pollInterval) { + try { + clearInterval(pollInterval); + } catch (err) { + console.debug("Failed to clear poll interval:", err); + } + set({ pollInterval: null }); + } + if (authProgressUnlisten) { + try { + authProgressUnlisten(); + } catch (err) { + console.debug("Failed to unlisten auth progress:", err); + } + set({ authProgressUnlisten: null }); + } + }, + + cancelMicrosoftLogin: () => { + get().stopPolling(); + set({ + deviceCodeData: null, + msLoginLoading: false, + msLoginStatus: "", + loginMode: "select", + }); + }, + + setLoginMode: (mode: "select" | "offline" | "microsoft") => { + set({ loginMode: mode }); + }, + + setOfflineUsername: (username: string) => { + set({ offlineUsername: username }); + }, +})); diff --git a/packages/ui/src/stores/auth.svelte.ts b/packages/ui/src/stores/auth.svelte.ts deleted file mode 100644 index 1b613a7..0000000 --- a/packages/ui/src/stores/auth.svelte.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { invoke } from "@tauri-apps/api/core"; -import { open } from "@tauri-apps/plugin-shell"; -import { listen, type UnlistenFn } from "@tauri-apps/api/event"; -import type { Account, DeviceCodeResponse } from "../types"; -import { uiState } from "./ui.svelte"; -import { logsState } from "./logs.svelte"; - -export class AuthState { - currentAccount = $state(null); - isLoginModalOpen = $state(false); - isLogoutConfirmOpen = $state(false); - loginMode = $state<"select" | "offline" | "microsoft">("select"); - offlineUsername = $state(""); - deviceCodeData = $state(null); - msLoginLoading = $state(false); - msLoginStatus = $state("Waiting for authorization..."); - - private pollInterval: ReturnType | null = null; - private isPollingRequestActive = false; - private authProgressUnlisten: UnlistenFn | null = null; - - async checkAccount() { - try { - const acc = await invoke("get_active_account"); - this.currentAccount = acc as Account | null; - } catch (e) { - console.error("Failed to check account:", e); - } - } - - openLoginModal() { - if (this.currentAccount) { - // Show custom logout confirmation dialog - this.isLogoutConfirmOpen = true; - return; - } - this.resetLoginState(); - this.isLoginModalOpen = true; - } - - cancelLogout() { - this.isLogoutConfirmOpen = false; - } - - async confirmLogout() { - this.isLogoutConfirmOpen = false; - try { - await invoke("logout"); - this.currentAccount = null; - uiState.setStatus("Logged out successfully"); - } catch (e) { - console.error("Logout failed:", e); - } - } - - closeLoginModal() { - this.stopPolling(); - this.isLoginModalOpen = false; - } - - resetLoginState() { - this.loginMode = "select"; - this.offlineUsername = ""; - this.deviceCodeData = null; - this.msLoginLoading = false; - } - - async performOfflineLogin() { - if (!this.offlineUsername) return; - try { - this.currentAccount = (await invoke("login_offline", { - username: this.offlineUsername, - })) as Account; - this.isLoginModalOpen = false; - } catch (e) { - alert("Login failed: " + e); - } - } - - async startMicrosoftLogin() { - this.loginMode = "microsoft"; - this.msLoginLoading = true; - this.msLoginStatus = "Waiting for authorization..."; - this.stopPolling(); - - // Setup auth progress listener - this.setupAuthProgressListener(); - - try { - this.deviceCodeData = (await invoke("start_microsoft_login")) as DeviceCodeResponse; - - if (this.deviceCodeData) { - try { - await navigator.clipboard.writeText(this.deviceCodeData.user_code); - } catch (e) { - console.error("Clipboard failed", e); - } - - open(this.deviceCodeData.verification_uri); - logsState.addLog( - "info", - "Auth", - "Microsoft login started, waiting for browser authorization...", - ); - - console.log("Starting polling for token..."); - const intervalMs = (this.deviceCodeData.interval || 5) * 1000; - this.pollInterval = setInterval( - () => this.checkLoginStatus(this.deviceCodeData!.device_code), - intervalMs, - ); - } - } catch (e) { - logsState.addLog("error", "Auth", `Failed to start Microsoft login: ${e}`); - alert("Failed to start Microsoft login: " + e); - this.loginMode = "select"; - } finally { - this.msLoginLoading = false; - } - } - - private async setupAuthProgressListener() { - // Clean up previous listener if exists - if (this.authProgressUnlisten) { - this.authProgressUnlisten(); - this.authProgressUnlisten = null; - } - - this.authProgressUnlisten = await listen("auth-progress", (event) => { - const message = event.payload; - this.msLoginStatus = message; - logsState.addLog("info", "Auth", message); - }); - } - - private cleanupAuthListener() { - if (this.authProgressUnlisten) { - this.authProgressUnlisten(); - this.authProgressUnlisten = null; - } - } - - stopPolling() { - if (this.pollInterval) { - clearInterval(this.pollInterval); - this.pollInterval = null; - } - } - - async checkLoginStatus(deviceCode: string) { - if (this.isPollingRequestActive) return; - this.isPollingRequestActive = true; - - console.log("Polling Microsoft API..."); - try { - this.currentAccount = (await invoke("complete_microsoft_login", { - deviceCode, - })) as Account; - - console.log("Login Successful!", this.currentAccount); - this.stopPolling(); - this.cleanupAuthListener(); - this.isLoginModalOpen = false; - logsState.addLog( - "info", - "Auth", - `Login successful! Welcome, ${this.currentAccount.username}`, - ); - uiState.setStatus("Welcome back, " + this.currentAccount.username); - } catch (e: any) { - const errStr = e.toString(); - if (errStr.includes("authorization_pending")) { - console.log("Status: Waiting for user to authorize..."); - } else { - console.error("Polling Error:", errStr); - this.msLoginStatus = "Error: " + errStr; - logsState.addLog("error", "Auth", `Login error: ${errStr}`); - - if (errStr.includes("expired_token") || errStr.includes("access_denied")) { - this.stopPolling(); - this.cleanupAuthListener(); - alert("Login failed: " + errStr); - this.loginMode = "select"; - } - } - } finally { - this.isPollingRequestActive = false; - } - } -} - -export const authState = new AuthState(); diff --git a/packages/ui/src/stores/game-store.ts b/packages/ui/src/stores/game-store.ts new file mode 100644 index 0000000..fa0f9f8 --- /dev/null +++ b/packages/ui/src/stores/game-store.ts @@ -0,0 +1,101 @@ +import { toast } from "sonner"; +import { create } from "zustand"; +import { getVersions } from "@/client"; +import type { Version } from "@/types/bindings/manifest"; + +interface GameState { + // State + versions: Version[]; + selectedVersion: string; + + // Computed property + latestRelease: Version | undefined; + + // Actions + loadVersions: (instanceId?: string) => Promise; + startGame: ( + currentAccount: any, + openLoginModal: () => void, + activeInstanceId: string | null, + setView: (view: any) => void, + ) => Promise; + setSelectedVersion: (version: string) => void; + setVersions: (versions: Version[]) => void; +} + +export const useGameStore = create((set, get) => ({ + // Initial state + versions: [], + selectedVersion: "", + + // Computed property + get latestRelease() { + return get().versions.find((v) => v.type === "release"); + }, + + // Actions + loadVersions: async (instanceId?: string) => { + console.log("Loading versions for instance:", instanceId); + try { + // Ask the backend for known versions (optionally scoped to an instance). + // The Tauri command `get_versions` is expected to return an array of `Version`. + const versions = await getVersions(); + set({ versions: versions ?? [] }); + } catch (e) { + console.error("Failed to load versions:", e); + // Keep the store consistent on error by clearing versions. + set({ versions: [] }); + } + }, + + startGame: async ( + currentAccount, + openLoginModal, + activeInstanceId, + setView, + ) => { + const { selectedVersion } = get(); + + if (!currentAccount) { + alert("Please login first!"); + openLoginModal(); + return; + } + + if (!selectedVersion) { + alert("Please select a version!"); + return; + } + + if (!activeInstanceId) { + alert("Please select an instance first!"); + setView("instances"); + return; + } + + toast.info("Preparing to launch " + selectedVersion + "..."); + + try { + // Note: In production, this would call Tauri invoke + // const msg = await invoke("start_game", { + // instanceId: activeInstanceId, + // versionId: selectedVersion, + // }); + + // Simulate success + await new Promise((resolve) => setTimeout(resolve, 1000)); + toast.success("Game started successfully!"); + } catch (e) { + console.error(e); + toast.error(`Error: ${e}`); + } + }, + + setSelectedVersion: (version: string) => { + set({ selectedVersion: version }); + }, + + setVersions: (versions: Version[]) => { + set({ versions }); + }, +})); diff --git a/packages/ui/src/stores/game.svelte.ts b/packages/ui/src/stores/game.svelte.ts deleted file mode 100644 index 504d108..0000000 --- a/packages/ui/src/stores/game.svelte.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { invoke } from "@tauri-apps/api/core"; -import type { Version } from "../types"; -import { uiState } from "./ui.svelte"; -import { authState } from "./auth.svelte"; -import { instancesState } from "./instances.svelte"; - -export class GameState { - versions = $state([]); - selectedVersion = $state(""); - - constructor() { - // Constructor intentionally empty - // Instance switching handled in App.svelte with $effect - } - - get latestRelease() { - return this.versions.find((v) => v.type === "release"); - } - - async loadVersions(instanceId?: string) { - const id = instanceId || instancesState.activeInstanceId; - if (!id) { - this.versions = []; - return; - } - - try { - this.versions = await invoke("get_versions", { - instanceId: id, - }); - // Don't auto-select version here - let BottomBar handle version selection - // based on installed versions only - } catch (e) { - console.error("Failed to fetch versions:", e); - uiState.setStatus("Error fetching versions: " + e); - } - } - - async startGame() { - if (!authState.currentAccount) { - alert("Please login first!"); - authState.openLoginModal(); - return; - } - - if (!this.selectedVersion) { - alert("Please select a version!"); - return; - } - - if (!instancesState.activeInstanceId) { - alert("Please select an instance first!"); - uiState.setView("instances"); - return; - } - - uiState.setStatus("Preparing to launch " + this.selectedVersion + "..."); - console.log( - "Invoking start_game for version:", - this.selectedVersion, - "instance:", - instancesState.activeInstanceId, - ); - try { - const msg = await invoke("start_game", { - instanceId: instancesState.activeInstanceId, - versionId: this.selectedVersion, - }); - console.log("Response:", msg); - uiState.setStatus(msg); - } catch (e) { - console.error(e); - uiState.setStatus("Error: " + e); - } - } -} - -export const gameState = new GameState(); diff --git a/packages/ui/src/stores/instances.svelte.ts b/packages/ui/src/stores/instances.svelte.ts deleted file mode 100644 index f4ac4e9..0000000 --- a/packages/ui/src/stores/instances.svelte.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { invoke } from "@tauri-apps/api/core"; -import type { Instance } from "../types"; -import { uiState } from "./ui.svelte"; - -export class InstancesState { - instances = $state([]); - activeInstanceId = $state(null); - get activeInstance(): Instance | null { - if (!this.activeInstanceId) return null; - return this.instances.find((i) => i.id === this.activeInstanceId) || null; - } - - async loadInstances() { - try { - this.instances = await invoke("list_instances"); - const active = await invoke("get_active_instance"); - if (active) { - this.activeInstanceId = active.id; - } else if (this.instances.length > 0) { - // If no active instance but instances exist, set the first one as active - await this.setActiveInstance(this.instances[0].id); - } - } catch (e) { - console.error("Failed to load instances:", e); - uiState.setStatus("Error loading instances: " + e); - } - } - - async createInstance(name: string): Promise { - try { - const instance = await invoke("create_instance", { name }); - await this.loadInstances(); - uiState.setStatus(`Instance "${name}" created successfully`); - return instance; - } catch (e) { - console.error("Failed to create instance:", e); - uiState.setStatus("Error creating instance: " + e); - return null; - } - } - - async deleteInstance(id: string) { - try { - await invoke("delete_instance", { instanceId: id }); - await this.loadInstances(); - // If deleted instance was active, set another as active - if (this.activeInstanceId === id) { - if (this.instances.length > 0) { - await this.setActiveInstance(this.instances[0].id); - } else { - this.activeInstanceId = null; - } - } - uiState.setStatus("Instance deleted successfully"); - } catch (e) { - console.error("Failed to delete instance:", e); - uiState.setStatus("Error deleting instance: " + e); - } - } - - async updateInstance(instance: Instance) { - try { - await invoke("update_instance", { instance }); - await this.loadInstances(); - uiState.setStatus("Instance updated successfully"); - } catch (e) { - console.error("Failed to update instance:", e); - uiState.setStatus("Error updating instance: " + e); - } - } - - async setActiveInstance(id: string) { - try { - await invoke("set_active_instance", { instanceId: id }); - this.activeInstanceId = id; - uiState.setStatus("Active instance changed"); - } catch (e) { - console.error("Failed to set active instance:", e); - uiState.setStatus("Error setting active instance: " + e); - } - } - - async duplicateInstance(id: string, newName: string): Promise { - try { - const instance = await invoke("duplicate_instance", { - instanceId: id, - newName, - }); - await this.loadInstances(); - uiState.setStatus(`Instance duplicated as "${newName}"`); - return instance; - } catch (e) { - console.error("Failed to duplicate instance:", e); - uiState.setStatus("Error duplicating instance: " + e); - return null; - } - } - - async getInstance(id: string): Promise { - try { - return await invoke("get_instance", { instanceId: id }); - } catch (e) { - console.error("Failed to get instance:", e); - return null; - } - } -} - -export const instancesState = new InstancesState(); diff --git a/packages/ui/src/stores/logs-store.ts b/packages/ui/src/stores/logs-store.ts new file mode 100644 index 0000000..b19f206 --- /dev/null +++ b/packages/ui/src/stores/logs-store.ts @@ -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/stores/logs.svelte.ts b/packages/ui/src/stores/logs.svelte.ts deleted file mode 100644 index c9d4acc..0000000 --- a/packages/ui/src/stores/logs.svelte.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { listen } from "@tauri-apps/api/event"; - -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"; -} - -export class LogsState { - logs = $state([]); - private nextId = 0; - private maxLogs = 5000; - - // Track all unique sources for filtering - sources = $state>(new Set(["Launcher"])); - - constructor() { - this.addLog("info", "Launcher", "Logs initialized"); - } - - addLog(level: LogEntry["level"], source: string, message: string) { - const now = new Date(); - const timestamp = - now.toLocaleTimeString() + "." + now.getMilliseconds().toString().padStart(3, "0"); - - this.logs.push({ - id: this.nextId++, - timestamp, - level, - source, - message, - }); - - // Track source - if (!this.sources.has(source)) { - this.sources = new Set([...this.sources, source]); - } - - if (this.logs.length > this.maxLogs) { - this.logs.shift(); - } - } - - // Parse game output and extract level/source - addGameLog(rawLine: string, isStderr: boolean) { - 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]}`; - this.addLog(level, source, message); - } else { - // Fallback: couldn't parse, use stderr as error indicator - const level = isStderr ? "error" : "info"; - this.addLog(level, "Game", rawLine); - } - } - - clear() { - this.logs = []; - this.sources = new Set(["Launcher"]); - this.addLog("info", "Launcher", "Logs cleared"); - } - - // Export with filter support - exportLogs(filteredLogs: LogEntry[]): string { - return filteredLogs - .map((l) => `[${l.timestamp}] [${l.source}/${l.level.toUpperCase()}] ${l.message}`) - .join("\n"); - } - - private initialized = false; - - async init() { - if (this.initialized) return; - this.initialized = true; - - // General Launcher Logs - await listen("launcher-log", (e) => { - this.addLog("info", "Launcher", e.payload); - }); - - // Game Stdout - parse log level - await listen("game-stdout", (e) => { - this.addGameLog(e.payload, false); - }); - - // Game Stderr - parse log level, default to error - await listen("game-stderr", (e) => { - this.addGameLog(e.payload, true); - }); - - // Download Events (Summarized) - await listen("download-start", (e) => { - this.addLog("info", "Downloader", `Starting batch download of ${e.payload} files...`); - }); - - await listen("download-complete", () => { - this.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")) { - this.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) { - this.addLog("info", "JavaInstaller", `Downloading Java: ${p.file_name}`); - } else if (p.status === "Completed") { - this.addLog("info", "JavaInstaller", `Java installed: ${p.file_name}`); - } else if (p.status === "Error") { - this.addLog("error", "JavaInstaller", `Java download error`); - } - }); - } -} - -export const logsState = new LogsState(); diff --git a/packages/ui/src/stores/releases-store.ts b/packages/ui/src/stores/releases-store.ts new file mode 100644 index 0000000..56afa08 --- /dev/null +++ b/packages/ui/src/stores/releases-store.ts @@ -0,0 +1,63 @@ +import { invoke } from "@tauri-apps/api/core"; +import { create } from "zustand"; +import type { GithubRelease } from "@/types/bindings/core"; + +interface ReleasesState { + // State + releases: GithubRelease[]; + isLoading: boolean; + isLoaded: boolean; + error: string | null; + + // Actions + loadReleases: () => Promise; + setReleases: (releases: GithubRelease[]) => void; + setIsLoading: (isLoading: boolean) => void; + setIsLoaded: (isLoaded: boolean) => void; + setError: (error: string | null) => void; +} + +export const useReleasesStore = create((set, get) => ({ + // Initial state + releases: [], + isLoading: false, + isLoaded: false, + error: null, + + // Actions + loadReleases: async () => { + const { isLoaded, isLoading } = get(); + + // If already loaded or currently loading, skip to prevent duplicate requests + if (isLoaded || isLoading) return; + + set({ isLoading: true, error: null }); + + try { + const releases = await invoke("get_github_releases"); + set({ releases, isLoaded: true }); + } catch (e) { + const error = e instanceof Error ? e.message : String(e); + console.error("Failed to load releases:", e); + set({ error }); + } finally { + set({ isLoading: false }); + } + }, + + setReleases: (releases) => { + set({ releases }); + }, + + setIsLoading: (isLoading) => { + set({ isLoading }); + }, + + setIsLoaded: (isLoaded) => { + set({ isLoaded }); + }, + + setError: (error) => { + set({ error }); + }, +})); diff --git a/packages/ui/src/stores/releases.svelte.ts b/packages/ui/src/stores/releases.svelte.ts deleted file mode 100644 index c858abb..0000000 --- a/packages/ui/src/stores/releases.svelte.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { invoke } from "@tauri-apps/api/core"; - -export interface GithubRelease { - tag_name: string; - name: string; - published_at: string; - body: string; - html_url: string; -} - -export class ReleasesState { - releases = $state([]); - isLoading = $state(false); - isLoaded = $state(false); - error = $state(null); - - async loadReleases() { - // If already loaded or currently loading, skip to prevent duplicate requests - if (this.isLoaded || this.isLoading) return; - - this.isLoading = true; - this.error = null; - - try { - this.releases = await invoke("get_github_releases"); - this.isLoaded = true; - } catch (e) { - console.error("Failed to load releases:", e); - this.error = String(e); - } finally { - this.isLoading = false; - } - } -} - -export const releasesState = new ReleasesState(); diff --git a/packages/ui/src/stores/settings-store.ts b/packages/ui/src/stores/settings-store.ts new file mode 100644 index 0000000..0bfc1e1 --- /dev/null +++ b/packages/ui/src/stores/settings-store.ts @@ -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/stores/settings.svelte.ts b/packages/ui/src/stores/settings.svelte.ts deleted file mode 100644 index 5d20050..0000000 --- a/packages/ui/src/stores/settings.svelte.ts +++ /dev/null @@ -1,570 +0,0 @@ -import { invoke } from "@tauri-apps/api/core"; -import { convertFileSrc } from "@tauri-apps/api/core"; -import { listen, type UnlistenFn } from "@tauri-apps/api/event"; -import type { - JavaCatalog, - JavaDownloadProgress, - JavaDownloadSource, - JavaInstallation, - JavaReleaseInfo, - LauncherConfig, - ModelInfo, - PendingJavaDownload, -} from "../types"; -import { uiState } from "./ui.svelte"; - -export class SettingsState { - settings = $state({ - min_memory: 1024, - max_memory: 2048, - java_path: "java", - width: 854, - height: 480, - download_threads: 32, - enable_gpu_acceleration: false, - enable_visual_effects: true, - active_effect: "constellation", - theme: "dark", - custom_background_path: undefined, - log_upload_service: "paste.rs", - pastebin_api_key: undefined, - assistant: { - enabled: true, - llm_provider: "ollama", - ollama_endpoint: "http://localhost:11434", - ollama_model: "llama3", - openai_api_key: undefined, - openai_endpoint: "https://api.openai.com/v1", - openai_model: "gpt-3.5-turbo", - system_prompt: - "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.", - response_language: "auto", - tts_enabled: false, - tts_provider: "disabled", - }, - use_shared_caches: false, - keep_legacy_per_instance_storage: true, - feature_flags: { - demo_user: false, - quick_play_enabled: false, - quick_play_path: undefined, - quick_play_singleplayer: true, - quick_play_multiplayer_server: undefined, - }, - }); - - // Convert background path to proper asset URL - get backgroundUrl(): string | undefined { - if (this.settings.custom_background_path) { - return convertFileSrc(this.settings.custom_background_path); - } - return undefined; - } - javaInstallations = $state([]); - isDetectingJava = $state(false); - - // Java download modal state - showJavaDownloadModal = $state(false); - selectedDownloadSource = $state("adoptium"); - - // Java catalog state - javaCatalog = $state(null); - isLoadingCatalog = $state(false); - catalogError = $state(""); - - // Version selection state - selectedMajorVersion = $state(null); - selectedImageType = $state<"jre" | "jdk">("jre"); - showOnlyRecommended = $state(true); - searchQuery = $state(""); - - // Download progress state - isDownloadingJava = $state(false); - downloadProgress = $state(null); - javaDownloadStatus = $state(""); - - // Pending downloads - pendingDownloads = $state([]); - - // AI Model lists - ollamaModels = $state([]); - openaiModels = $state([]); - isLoadingOllamaModels = $state(false); - isLoadingOpenaiModels = $state(false); - ollamaModelsError = $state(""); - openaiModelsError = $state(""); - - // Config Editor state - showConfigEditor = $state(false); - rawConfigContent = $state(""); - configFilePath = $state(""); - configEditorError = $state(""); - - // Event listener cleanup - private progressUnlisten: UnlistenFn | null = null; - - async openConfigEditor() { - this.configEditorError = ""; - try { - const path = await invoke("get_config_path"); - const content = await invoke("read_raw_config"); - this.configFilePath = path; - this.rawConfigContent = content; - this.showConfigEditor = true; - } catch (e) { - console.error("Failed to open config editor:", e); - uiState.setStatus(`Failed to open config: ${e}`); - } - } - - async saveRawConfig(content: string, closeAfterSave = true) { - try { - await invoke("save_raw_config", { content }); - // Reload settings to ensure UI is in sync - await this.loadSettings(); - if (closeAfterSave) { - this.showConfigEditor = false; - } - uiState.setStatus("Configuration saved successfully!"); - } catch (e) { - console.error("Failed to save config:", e); - this.configEditorError = String(e); - } - } - - closeConfigEditor() { - this.showConfigEditor = false; - this.rawConfigContent = ""; - this.configEditorError = ""; - } - - // Computed: filtered releases based on selection - get filteredReleases(): JavaReleaseInfo[] { - if (!this.javaCatalog) return []; - - let releases = this.javaCatalog.releases; - - // Filter by major version if selected - if (this.selectedMajorVersion !== null) { - releases = releases.filter((r) => r.major_version === this.selectedMajorVersion); - } - - // Filter by image type - releases = releases.filter((r) => r.image_type === this.selectedImageType); - - // Filter by recommended (LTS) versions - if (this.showOnlyRecommended) { - releases = releases.filter((r) => r.is_lts); - } - - // Filter by search query - if (this.searchQuery.trim()) { - const query = this.searchQuery.toLowerCase(); - releases = releases.filter( - (r) => - r.release_name.toLowerCase().includes(query) || - r.version.toLowerCase().includes(query) || - r.major_version.toString().includes(query), - ); - } - - return releases; - } - - // Computed: available major versions for display - get availableMajorVersions(): number[] { - if (!this.javaCatalog) return []; - let versions = [...this.javaCatalog.available_major_versions]; - - // Filter by LTS if showOnlyRecommended is enabled - if (this.showOnlyRecommended) { - versions = versions.filter((v) => this.javaCatalog!.lts_versions.includes(v)); - } - - // Sort descending (newest first) - return versions.sort((a, b) => b - a); - } - - // Get installation status for a release: 'installed' | 'download' - getInstallStatus(release: JavaReleaseInfo): "installed" | "download" { - // Find installed Java that matches the major version and image type (by path pattern) - const matchingInstallations = this.javaInstallations.filter((inst) => { - // Check if this is a DropOut-managed Java (path contains temurin-XX-jre/jdk pattern) - const pathLower = inst.path.toLowerCase(); - const pattern = `temurin-${release.major_version}-${release.image_type}`; - return pathLower.includes(pattern); - }); - - // If any matching installation exists, it's installed - return matchingInstallations.length > 0 ? "installed" : "download"; - } - - // Computed: selected release details - get selectedRelease(): JavaReleaseInfo | null { - if (!this.javaCatalog || this.selectedMajorVersion === null) return null; - return ( - this.javaCatalog.releases.find( - (r) => - r.major_version === this.selectedMajorVersion && r.image_type === this.selectedImageType, - ) || null - ); - } - - async loadSettings() { - try { - const result = await invoke("get_settings"); - this.settings = result; - // Force dark mode - if (this.settings.theme !== "dark") { - this.settings.theme = "dark"; - this.saveSettings(); - } - // Ensure custom_background_path is reactive - if (!this.settings.custom_background_path) { - this.settings.custom_background_path = undefined; - } - } catch (e) { - console.error("Failed to load settings:", e); - } - } - - async saveSettings() { - try { - // Ensure we clean up any invalid paths before saving - if (this.settings.custom_background_path === "") { - this.settings.custom_background_path = undefined; - } - - await invoke("save_settings", { config: this.settings }); - uiState.setStatus("Settings saved!"); - } catch (e) { - console.error("Failed to save settings:", e); - uiState.setStatus("Error saving settings: " + e); - } - } - - async detectJava() { - this.isDetectingJava = true; - try { - this.javaInstallations = await invoke("detect_java"); - if (this.javaInstallations.length === 0) { - uiState.setStatus("No Java installations found"); - } else { - uiState.setStatus(`Found ${this.javaInstallations.length} Java installation(s)`); - } - } catch (e) { - console.error("Failed to detect Java:", e); - uiState.setStatus("Error detecting Java: " + e); - } finally { - this.isDetectingJava = false; - } - } - - selectJava(path: string) { - this.settings.java_path = path; - } - - async openJavaDownloadModal() { - this.showJavaDownloadModal = true; - this.javaDownloadStatus = ""; - this.catalogError = ""; - this.downloadProgress = null; - - // Setup progress event listener - await this.setupProgressListener(); - - // Load catalog - await this.loadJavaCatalog(false); - - // Check for pending downloads - await this.loadPendingDownloads(); - } - - async closeJavaDownloadModal() { - if (!this.isDownloadingJava) { - this.showJavaDownloadModal = false; - // Cleanup listener - if (this.progressUnlisten) { - this.progressUnlisten(); - this.progressUnlisten = null; - } - } - } - - private async setupProgressListener() { - if (this.progressUnlisten) { - this.progressUnlisten(); - } - - this.progressUnlisten = await listen( - "java-download-progress", - (event) => { - this.downloadProgress = event.payload; - this.javaDownloadStatus = event.payload.status; - - if (event.payload.status === "Completed") { - this.isDownloadingJava = false; - setTimeout(async () => { - await this.detectJava(); - uiState.setStatus(`Java installed successfully!`); - }, 500); - } else if (event.payload.status === "Error") { - this.isDownloadingJava = false; - } - }, - ); - } - - async loadJavaCatalog(forceRefresh: boolean) { - this.isLoadingCatalog = true; - this.catalogError = ""; - - try { - const command = forceRefresh ? "refresh_java_catalog" : "fetch_java_catalog"; - this.javaCatalog = await invoke(command); - - // Auto-select first LTS version - if (this.selectedMajorVersion === null && this.javaCatalog.lts_versions.length > 0) { - // Select most recent LTS (21 or highest) - const ltsVersions = [...this.javaCatalog.lts_versions].sort((a, b) => b - a); - this.selectedMajorVersion = ltsVersions[0]; - } - } catch (e) { - console.error("Failed to load Java catalog:", e); - this.catalogError = `Failed to load Java catalog: ${e}`; - } finally { - this.isLoadingCatalog = false; - } - } - - async refreshCatalog() { - await this.loadJavaCatalog(true); - uiState.setStatus("Java catalog refreshed"); - } - - async loadPendingDownloads() { - try { - this.pendingDownloads = await invoke("get_pending_java_downloads"); - } catch (e) { - console.error("Failed to load pending downloads:", e); - } - } - - selectMajorVersion(version: number) { - this.selectedMajorVersion = version; - } - - async downloadJava() { - if (!this.selectedRelease || !this.selectedRelease.is_available) { - uiState.setStatus("Selected Java version is not available for this platform"); - return; - } - - this.isDownloadingJava = true; - this.javaDownloadStatus = "Starting download..."; - this.downloadProgress = null; - - try { - const result: JavaInstallation = await invoke("download_adoptium_java", { - majorVersion: this.selectedMajorVersion, - imageType: this.selectedImageType, - customPath: null, - }); - - this.settings.java_path = result.path; - await this.detectJava(); - - setTimeout(() => { - this.showJavaDownloadModal = false; - uiState.setStatus(`Java ${this.selectedMajorVersion} is ready to use!`); - }, 1500); - } catch (e) { - console.error("Failed to download Java:", e); - this.javaDownloadStatus = `Download failed: ${e}`; - } finally { - this.isDownloadingJava = false; - } - } - - async cancelDownload() { - try { - await invoke("cancel_java_download"); - this.isDownloadingJava = false; - this.javaDownloadStatus = "Download cancelled"; - this.downloadProgress = null; - await this.loadPendingDownloads(); - } catch (e) { - console.error("Failed to cancel download:", e); - } - } - - async resumeDownloads() { - if (this.pendingDownloads.length === 0) return; - - this.isDownloadingJava = true; - this.javaDownloadStatus = "Resuming download..."; - - try { - const installed = await invoke("resume_java_downloads"); - if (installed.length > 0) { - this.settings.java_path = installed[0].path; - await this.detectJava(); - uiState.setStatus(`Resumed and installed ${installed.length} Java version(s)`); - } - await this.loadPendingDownloads(); - } catch (e) { - console.error("Failed to resume downloads:", e); - this.javaDownloadStatus = `Resume failed: ${e}`; - } finally { - this.isDownloadingJava = false; - } - } - - // Format bytes to human readable - formatBytes(bytes: number): string { - if (bytes === 0) return "0 B"; - const k = 1024; - const sizes = ["B", "KB", "MB", "GB"]; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]; - } - - // Format seconds to human readable - formatTime(seconds: number): string { - if (seconds === 0 || !isFinite(seconds)) return "--"; - if (seconds < 60) return `${Math.round(seconds)}s`; - if (seconds < 3600) { - const mins = Math.floor(seconds / 60); - const secs = Math.round(seconds % 60); - return `${mins}m ${secs}s`; - } - const hours = Math.floor(seconds / 3600); - const mins = Math.floor((seconds % 3600) / 60); - return `${hours}h ${mins}m`; - } - - // Format date string - formatDate(dateStr: string | null): string { - if (!dateStr) return "--"; - try { - const date = new Date(dateStr); - return date.toLocaleDateString("en-US", { - year: "2-digit", - month: "2-digit", - day: "2-digit", - }); - } catch { - return "--"; - } - } - - // Legacy compatibility - get availableJavaVersions(): number[] { - return this.availableMajorVersions; - } - - // AI Model loading methods - async loadOllamaModels() { - this.isLoadingOllamaModels = true; - this.ollamaModelsError = ""; - - try { - const models = await invoke("list_ollama_models", { - endpoint: this.settings.assistant.ollama_endpoint, - }); - this.ollamaModels = models; - - // If no model is selected or selected model isn't available, select the first one - if (models.length > 0) { - const currentModel = this.settings.assistant.ollama_model; - const modelExists = models.some((m) => m.id === currentModel); - if (!modelExists) { - this.settings.assistant.ollama_model = models[0].id; - } - } - } catch (e) { - console.error("Failed to load Ollama models:", e); - this.ollamaModelsError = String(e); - this.ollamaModels = []; - } finally { - this.isLoadingOllamaModels = false; - } - } - - async loadOpenaiModels() { - if (!this.settings.assistant.openai_api_key) { - this.openaiModelsError = "API key required"; - this.openaiModels = []; - return; - } - - this.isLoadingOpenaiModels = true; - this.openaiModelsError = ""; - - try { - const models = await invoke("list_openai_models"); - this.openaiModels = models; - - // If no model is selected or selected model isn't available, select the first one - if (models.length > 0) { - const currentModel = this.settings.assistant.openai_model; - const modelExists = models.some((m) => m.id === currentModel); - if (!modelExists) { - this.settings.assistant.openai_model = models[0].id; - } - } - } catch (e) { - console.error("Failed to load OpenAI models:", e); - this.openaiModelsError = String(e); - this.openaiModels = []; - } finally { - this.isLoadingOpenaiModels = false; - } - } - - // Computed: get model options for current provider - get currentModelOptions(): { value: string; label: string; details?: string }[] { - const provider = this.settings.assistant.llm_provider; - - if (provider === "ollama") { - if (this.ollamaModels.length === 0) { - // Return fallback options if no models loaded - return [ - { value: "llama3", label: "Llama 3" }, - { value: "llama3.1", label: "Llama 3.1" }, - { value: "llama3.2", label: "Llama 3.2" }, - { value: "mistral", label: "Mistral" }, - { value: "gemma2", label: "Gemma 2" }, - { value: "qwen2.5", label: "Qwen 2.5" }, - { value: "phi3", label: "Phi-3" }, - { value: "codellama", label: "Code Llama" }, - ]; - } - return this.ollamaModels.map((m) => ({ - value: m.id, - label: m.name, - details: m.size ? `${m.size}${m.details ? ` - ${m.details}` : ""}` : m.details, - })); - } else if (provider === "openai") { - if (this.openaiModels.length === 0) { - // Return fallback options if no models loaded - return [ - { value: "gpt-4o", label: "GPT-4o" }, - { value: "gpt-4o-mini", label: "GPT-4o Mini" }, - { value: "gpt-4-turbo", label: "GPT-4 Turbo" }, - { value: "gpt-4", label: "GPT-4" }, - { value: "gpt-3.5-turbo", label: "GPT-3.5 Turbo" }, - ]; - } - return this.openaiModels.map((m) => ({ - value: m.id, - label: m.name, - details: m.details, - })); - } - - return []; - } -} - -export const settingsState = new SettingsState(); diff --git a/packages/ui/src/stores/ui-store.ts b/packages/ui/src/stores/ui-store.ts new file mode 100644 index 0000000..89b9191 --- /dev/null +++ b/packages/ui/src/stores/ui-store.ts @@ -0,0 +1,42 @@ +import { create } from "zustand"; + +export type ViewType = "home" | "versions" | "settings" | "guide" | "instances"; + +interface UIState { + // State + currentView: ViewType; + showConsole: boolean; + appVersion: string; + + // Actions + toggleConsole: () => void; + setView: (view: ViewType) => void; + setAppVersion: (version: string) => void; +} + +export const useUIStore = create((set) => ({ + // Initial state + currentView: "home", + showConsole: false, + appVersion: "...", + + // Actions + toggleConsole: () => { + set((state) => ({ showConsole: !state.showConsole })); + }, + + setView: (view: ViewType) => { + set({ currentView: view }); + }, + + setAppVersion: (version: string) => { + set({ appVersion: version }); + }, +})); + +// Provide lowercase alias for compatibility with existing imports. +// Use a function wrapper to ensure the named export exists as a callable value +// at runtime (some bundlers/tree-shakers may remove simple aliases). +export function useUiStore() { + return useUIStore(); +} diff --git a/packages/ui/src/stores/ui.svelte.ts b/packages/ui/src/stores/ui.svelte.ts deleted file mode 100644 index e88f6b4..0000000 --- a/packages/ui/src/stores/ui.svelte.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { type ViewType } from "../types"; - -export class UIState { - currentView: ViewType = $state("home"); - status = $state("Ready"); - showConsole = $state(false); - appVersion = $state("..."); - - private statusTimeout: ReturnType | null = null; - - setStatus(msg: string) { - if (this.statusTimeout) clearTimeout(this.statusTimeout); - - this.status = msg; - - if (msg !== "Ready") { - this.statusTimeout = setTimeout(() => { - this.status = "Ready"; - }, 5000); - } - } - - toggleConsole() { - this.showConsole = !this.showConsole; - } - - setView(view: ViewType) { - this.currentView = view; - } -} - -export const uiState = new UIState(); -- cgit v1.2.3-70-g09d2