From 9430bee86fbf943283eb5a6f63bd750b875ff433 Mon Sep 17 00:00:00 2001
From: 苏向夜 <fu050409@163.com>
Date: Fri, 23 Jan 2026 20:51:28 +0800
Subject: feat(ui): add new ui project

---
 packages/ui-new/src/pages/settings-view.tsx | 1158 +++++++++++++++++++++++++++
 1 file changed, 1158 insertions(+)
 create mode 100644 packages/ui-new/src/pages/settings-view.tsx

(limited to 'packages/ui-new/src/pages/settings-view.tsx')

diff --git a/packages/ui-new/src/pages/settings-view.tsx b/packages/ui-new/src/pages/settings-view.tsx
new file mode 100644
index 0000000..ac43d9b
--- /dev/null
+++ b/packages/ui-new/src/pages/settings-view.tsx
@@ -0,0 +1,1158 @@
+import { open } from "@tauri-apps/plugin-dialog";
+import {
+  Coffee,
+  Download,
+  FileJson,
+  Loader2,
+  RefreshCw,
+  Upload,
+} from "lucide-react";
+import { useEffect, useState } from "react";
+import { toast } from "sonner";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Checkbox } from "@/components/ui/checkbox";
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from "@/components/ui/select";
+import { Separator } from "@/components/ui/separator";
+import { Switch } from "@/components/ui/switch";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Textarea } from "@/components/ui/textarea";
+import { useSettingsStore } from "../stores/settings-store";
+
+const effectOptions = [
+  { value: "saturn", label: "Saturn" },
+  { value: "constellation", label: "Network (Constellation)" },
+];
+
+const logServiceOptions = [
+  { value: "paste.rs", label: "paste.rs (Free, No Account)" },
+  { value: "pastebin.com", label: "pastebin.com (Requires API Key)" },
+];
+
+const llmProviderOptions = [
+  { value: "ollama", label: "Ollama (Local)" },
+  { value: "openai", label: "OpenAI (Remote)" },
+];
+
+const languageOptions = [
+  { value: "auto", label: "Auto (Match User)" },
+  { value: "English", label: "English" },
+  { value: "Chinese", label: "中文" },
+  { value: "Japanese", label: "日本語" },
+  { value: "Korean", label: "한국어" },
+  { value: "Spanish", label: "Español" },
+  { value: "French", label: "Français" },
+  { value: "German", label: "Deutsch" },
+  { value: "Russian", label: "Русский" },
+];
+
+const ttsProviderOptions = [
+  { value: "disabled", label: "Disabled" },
+  { value: "piper", label: "Piper TTS (Local)" },
+  { value: "edge", label: "Edge TTS (Online)" },
+];
+
+const personas = [
+  {
+    value: "default",
+    label: "Minecraft Expert (Default)",
+    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.",
+  },
+  {
+    value: "technical",
+    label: "Technical Debugger",
+    prompt:
+      "You are a technical support specialist for Minecraft. Focus strictly on analyzing logs, identifying crash causes, and providing technical solutions. Be precise and avoid conversational filler.",
+  },
+  {
+    value: "concise",
+    label: "Concise Helper",
+    prompt:
+      "You are a direct and concise assistant. Provide answers in as few words as possible while remaining accurate. Use bullet points for lists.",
+  },
+  {
+    value: "explain",
+    label: "Teacher / Explainer",
+    prompt:
+      "You are a patient teacher. Explain Minecraft concepts, redstone mechanics, and mod features in simple, easy-to-understand terms suitable for beginners.",
+  },
+  {
+    value: "pirate",
+    label: "Pirate Captain",
+    prompt:
+      "You are a salty Minecraft Pirate Captain! Yarr! Speak like a pirate while helping the crew (the user) with their blocky adventures. Use terms like 'matey', 'landlubber', and 'treasure'.",
+  },
+];
+
+export function SettingsView() {
+  const {
+    settings,
+    backgroundUrl,
+    javaInstallations,
+    isDetectingJava,
+    showJavaDownloadModal,
+    selectedDownloadSource,
+    javaCatalog,
+    isLoadingCatalog,
+    catalogError,
+    selectedMajorVersion,
+    selectedImageType,
+    showOnlyRecommended,
+    searchQuery,
+    isDownloadingJava,
+    downloadProgress,
+    javaDownloadStatus,
+    pendingDownloads,
+    ollamaModels,
+    openaiModels,
+    isLoadingOllamaModels,
+    isLoadingOpenaiModels,
+    ollamaModelsError,
+    openaiModelsError,
+    showConfigEditor,
+    rawConfigContent,
+    configFilePath,
+    configEditorError,
+    filteredReleases,
+    availableMajorVersions,
+    installStatus,
+    selectedRelease,
+    currentModelOptions,
+    loadSettings,
+    saveSettings,
+    detectJava,
+    selectJava,
+    openJavaDownloadModal,
+    closeJavaDownloadModal,
+    loadJavaCatalog,
+    refreshCatalog,
+    loadPendingDownloads,
+    selectMajorVersion,
+    downloadJava,
+    cancelDownload,
+    resumeDownloads,
+    openConfigEditor,
+    closeConfigEditor,
+    saveRawConfig,
+    loadOllamaModels,
+    loadOpenaiModels,
+    set,
+    setSetting,
+    setAssistantSetting,
+    setFeatureFlag,
+  } = useSettingsStore();
+
+  // Mark potentially-unused variables as referenced so TypeScript does not report
+  // them as unused in this file (they are part of the store API and used elsewhere).
+  // This is a no-op but satisfies the compiler.
+  void selectedDownloadSource;
+  void javaCatalog;
+  void javaDownloadStatus;
+  void pendingDownloads;
+  void ollamaModels;
+  void openaiModels;
+  void isLoadingOllamaModels;
+  void isLoadingOpenaiModels;
+  void ollamaModelsError;
+  void openaiModelsError;
+  void selectedRelease;
+  void loadJavaCatalog;
+  void loadPendingDownloads;
+  void cancelDownload;
+  void resumeDownloads;
+  void setFeatureFlag;
+  const [selectedPersona, setSelectedPersona] = useState("default");
+  const [migrating, setMigrating] = useState(false);
+  const [activeTab, setActiveTab] = useState("appearance");
+
+  useEffect(() => {
+    loadSettings();
+    detectJava();
+  }, [loadSettings, detectJava]);
+
+  useEffect(() => {
+    if (activeTab === "assistant") {
+      if (settings.assistant.llmProvider === "ollama") {
+        loadOllamaModels();
+      } else if (settings.assistant.llmProvider === "openai") {
+        loadOpenaiModels();
+      }
+    }
+  }, [
+    activeTab,
+    settings.assistant.llmProvider,
+    loadOllamaModels,
+    loadOpenaiModels,
+  ]);
+
+  const handleSelectBackground = async () => {
+    try {
+      const selected = await open({
+        multiple: false,
+        filters: [
+          {
+            name: "Images",
+            extensions: ["png", "jpg", "jpeg", "webp", "gif"],
+          },
+        ],
+      });
+
+      if (selected && typeof selected === "string") {
+        setSetting("customBackgroundPath", selected);
+        saveSettings();
+      }
+    } catch (e) {
+      console.error("Failed to select background:", e);
+      toast.error("Failed to select background");
+    }
+  };
+
+  const handleClearBackground = () => {
+    setSetting("customBackgroundPath", null);
+    saveSettings();
+  };
+
+  const handleApplyPersona = (value: string) => {
+    const persona = personas.find((p) => p.value === value);
+    if (persona) {
+      setAssistantSetting("systemPrompt", persona.prompt);
+      setSelectedPersona(value);
+      saveSettings();
+    }
+  };
+
+  const handleResetSystemPrompt = () => {
+    const defaultPersona = personas.find((p) => p.value === "default");
+    if (defaultPersona) {
+      setAssistantSetting("systemPrompt", defaultPersona.prompt);
+      setSelectedPersona("default");
+      saveSettings();
+    }
+  };
+
+  const handleRunMigration = async () => {
+    if (migrating) return;
+    setMigrating(true);
+    try {
+      await new Promise((resolve) => setTimeout(resolve, 2000));
+      toast.success("Migration complete! Files migrated successfully");
+    } catch (e) {
+      console.error("Migration failed:", e);
+      toast.error(`Migration failed: ${e}`);
+    } finally {
+      setMigrating(false);
+    }
+  };
+
+  return (
+    <div className="h-full flex flex-col p-6 overflow-hidden">
+      <div className="flex items-center justify-between mb-6">
+        <h2 className="text-3xl font-black bg-clip-text text-transparent bg-linear-to-r dark:from-white dark:to-white/60 from-gray-900 to-gray-600">
+          Settings
+        </h2>
+
+        <Button
+          variant="outline"
+          size="sm"
+          onClick={openConfigEditor}
+          className="gap-2"
+        >
+          <FileJson className="h-4 w-4" />
+          <span className="hidden sm:inline">Open JSON</span>
+        </Button>
+      </div>
+
+      <Tabs
+        value={activeTab}
+        onValueChange={setActiveTab}
+        className="flex-1 overflow-hidden"
+      >
+        <TabsList className="grid grid-cols-4 mb-6">
+          <TabsTrigger value="appearance">Appearance</TabsTrigger>
+          <TabsTrigger value="java">Java</TabsTrigger>
+          <TabsTrigger value="advanced">Advanced</TabsTrigger>
+          <TabsTrigger value="assistant">Assistant</TabsTrigger>
+        </TabsList>
+
+        <ScrollArea className="flex-1 pr-2">
+          <TabsContent value="appearance" className="space-y-6">
+            <Card className="border-border">
+              <CardHeader>
+                <CardTitle className="text-lg">Appearance</CardTitle>
+              </CardHeader>
+              <CardContent className="space-y-6">
+                <div>
+                  <Label className="mb-3">Custom Background Image</Label>
+                  <div className="flex items-center gap-6">
+                    <div className="w-40 h-24 rounded-xl overflow-hidden bg-secondary border relative group shadow-lg">
+                      {backgroundUrl ? (
+                        <img
+                          src={backgroundUrl}
+                          alt="Background Preview"
+                          className="w-full h-full object-cover"
+                          onError={(e) => {
+                            console.error("Failed to load image");
+                            e.currentTarget.style.display = "none";
+                          }}
+                        />
+                      ) : (
+                        <div className="w-full h-full bg-linear-to-br from-emerald-900 via-zinc-900 to-indigo-950" />
+                      )}
+                      {!backgroundUrl && (
+                        <div className="absolute inset-0 flex items-center justify-center text-xs text-white/50 bg-black/20">
+                          Default Gradient
+                        </div>
+                      )}
+                    </div>
+
+                    <div className="flex flex-col gap-2">
+                      <Button
+                        variant="outline"
+                        onClick={handleSelectBackground}
+                      >
+                        Select Image
+                      </Button>
+                      {backgroundUrl && (
+                        <Button
+                          variant="ghost"
+                          className="text-red-500"
+                          onClick={handleClearBackground}
+                        >
+                          Reset to Default
+                        </Button>
+                      )}
+                    </div>
+                  </div>
+                  <p className="text-sm text-muted-foreground mt-3">
+                    Select an image from your computer to replace the default
+                    gradient background.
+                  </p>
+                </div>
+
+                <Separator />
+
+                <div className="space-y-4">
+                  <div className="flex items-center justify-between">
+                    <div>
+                      <Label className="text-base">Visual Effects</Label>
+                      <p className="text-sm text-muted-foreground">
+                        Enable particle effects and animated gradients.
+                      </p>
+                    </div>
+                    <Switch
+                      checked={settings.enableVisualEffects}
+                      onCheckedChange={(checked) => {
+                        setSetting("enableVisualEffects", checked);
+                        saveSettings();
+                      }}
+                    />
+                  </div>
+
+                  {settings.enableVisualEffects && (
+                    <div className="pl-4 border-l-2 border-border">
+                      <div className="space-y-2">
+                        <Label>Theme Effect</Label>
+                        <Select
+                          value={settings.activeEffect}
+                          onValueChange={(value) => {
+                            setSetting("activeEffect", value);
+                            saveSettings();
+                          }}
+                        >
+                          <SelectTrigger className="w-52">
+                            <SelectValue />
+                          </SelectTrigger>
+                          <SelectContent>
+                            {effectOptions.map((option) => (
+                              <SelectItem
+                                key={option.value}
+                                value={option.value}
+                              >
+                                {option.label}
+                              </SelectItem>
+                            ))}
+                          </SelectContent>
+                        </Select>
+                        <p className="text-sm text-muted-foreground">
+                          Select the active visual theme.
+                        </p>
+                      </div>
+                    </div>
+                  )}
+
+                  <div className="flex items-center justify-between">
+                    <div>
+                      <Label className="text-base">GPU Acceleration</Label>
+                      <p className="text-sm text-muted-foreground">
+                        Enable GPU acceleration for the interface.
+                      </p>
+                    </div>
+                    <Switch
+                      checked={settings.enableGpuAcceleration}
+                      onCheckedChange={(checked) => {
+                        setSetting("enableGpuAcceleration", checked);
+                        saveSettings();
+                      }}
+                    />
+                  </div>
+                </div>
+              </CardContent>
+            </Card>
+          </TabsContent>
+
+          <TabsContent value="java" className="space-y-6">
+            <Card className="border-border">
+              <CardHeader>
+                <CardTitle className="text-lg">Java Environment</CardTitle>
+              </CardHeader>
+              <CardContent className="space-y-6">
+                <div>
+                  <Label className="mb-2">Java Path</Label>
+                  <div className="flex gap-2">
+                    <Input
+                      value={settings.javaPath}
+                      onChange={(e) => setSetting("javaPath", e.target.value)}
+                      className="flex-1"
+                      placeholder="java or full path to java executable"
+                    />
+                    <Button
+                      variant="outline"
+                      onClick={() => detectJava()}
+                      disabled={isDetectingJava}
+                    >
+                      {isDetectingJava ? (
+                        <Loader2 className="h-4 w-4 animate-spin" />
+                      ) : (
+                        "Detect"
+                      )}
+                    </Button>
+                  </div>
+                  <p className="text-sm text-muted-foreground mt-2">
+                    Path to Java executable.
+                  </p>
+                </div>
+
+                <div>
+                  <Label className="mb-2">Memory Settings (MB)</Label>
+                  <div className="grid grid-cols-2 gap-4">
+                    <div>
+                      <Label htmlFor="min-memory" className="text-sm">
+                        Minimum Memory
+                      </Label>
+                      <Input
+                        id="min-memory"
+                        type="number"
+                        value={settings.minMemory}
+                        onChange={(e) =>
+                          setSetting(
+                            "minMemory",
+                            parseInt(e.target.value, 10) || 1024,
+                          )
+                        }
+                        min={512}
+                        step={256}
+                      />
+                    </div>
+                    <div>
+                      <Label htmlFor="max-memory" className="text-sm">
+                        Maximum Memory
+                      </Label>
+                      <Input
+                        id="max-memory"
+                        type="number"
+                        value={settings.maxMemory}
+                        onChange={(e) =>
+                          setSetting(
+                            "maxMemory",
+                            parseInt(e.target.value, 10) || 2048,
+                          )
+                        }
+                        min={1024}
+                        step={256}
+                      />
+                    </div>
+                  </div>
+                  <p className="text-sm text-muted-foreground mt-2">
+                    Memory allocation for Minecraft.
+                  </p>
+                </div>
+
+                <Separator />
+
+                <div>
+                  <div className="flex items-center justify-between mb-4">
+                    <Label className="text-base">
+                      Detected Java Installations
+                    </Label>
+                    <Button
+                      variant="outline"
+                      size="sm"
+                      onClick={() => detectJava()}
+                      disabled={isDetectingJava}
+                    >
+                      <RefreshCw
+                        className={`h-4 w-4 mr-2 ${isDetectingJava ? "animate-spin" : ""}`}
+                      />
+                      Rescan
+                    </Button>
+                  </div>
+
+                  {javaInstallations.length === 0 ? (
+                    <div className="text-center py-8 text-muted-foreground border rounded-lg">
+                      <Coffee className="h-12 w-12 mx-auto mb-4 opacity-30" />
+                      <p>No Java installations detected</p>
+                    </div>
+                  ) : (
+                    <div className="space-y-2">
+                      {javaInstallations.map((installation) => (
+                        <Card
+                          key={installation.path}
+                          className={`p-3 cursor-pointer transition-colors ${
+                            settings.javaPath === installation.path
+                              ? "border-primary bg-primary/5"
+                              : ""
+                          }`}
+                          onClick={() => selectJava(installation.path)}
+                        >
+                          <div className="flex items-center justify-between">
+                            <div>
+                              <div className="font-medium flex items-center gap-2">
+                                <Coffee className="h-4 w-4" />
+                                {installation.version}
+                              </div>
+                              <div className="text-sm text-muted-foreground font-mono">
+                                {installation.path}
+                              </div>
+                            </div>
+                            {settings.javaPath === installation.path && (
+                              <div className="h-5 w-5 text-primary">✓</div>
+                            )}
+                          </div>
+                        </Card>
+                      ))}
+                    </div>
+                  )}
+
+                  <div className="mt-4">
+                    <Button
+                      variant="default"
+                      className="w-full"
+                      onClick={openJavaDownloadModal}
+                    >
+                      <Download className="h-4 w-4 mr-2" />
+                      Download Java
+                    </Button>
+                  </div>
+                </div>
+              </CardContent>
+            </Card>
+          </TabsContent>
+
+          <TabsContent value="advanced" className="space-y-6">
+            <Card className="border-border">
+              <CardHeader>
+                <CardTitle className="text-lg">Advanced Settings</CardTitle>
+              </CardHeader>
+              <CardContent className="space-y-6">
+                <div>
+                  <Label className="mb-2">Download Threads</Label>
+                  <Input
+                    type="number"
+                    value={settings.downloadThreads}
+                    onChange={(e) =>
+                      setSetting(
+                        "downloadThreads",
+                        parseInt(e.target.value, 10) || 32,
+                      )
+                    }
+                    min={1}
+                    max={64}
+                  />
+                  <p className="text-sm text-muted-foreground mt-2">
+                    Number of concurrent downloads.
+                  </p>
+                </div>
+
+                <div>
+                  <Label className="mb-2">Log Upload Service</Label>
+                  <Select
+                    value={settings.logUploadService}
+                    onValueChange={(value) => {
+                      setSetting("logUploadService", value as any);
+                      saveSettings();
+                    }}
+                  >
+                    <SelectTrigger>
+                      <SelectValue />
+                    </SelectTrigger>
+                    <SelectContent>
+                      {logServiceOptions.map((option) => (
+                        <SelectItem key={option.value} value={option.value}>
+                          {option.label}
+                        </SelectItem>
+                      ))}
+                    </SelectContent>
+                  </Select>
+                </div>
+
+                {settings.logUploadService === "pastebin.com" && (
+                  <div>
+                    <Label className="mb-2">Pastebin API Key</Label>
+                    <Input
+                      type="password"
+                      value={settings.pastebinApiKey || ""}
+                      onChange={(e) =>
+                        setSetting("pastebinApiKey", e.target.value || null)
+                      }
+                      placeholder="Enter your Pastebin API key"
+                    />
+                  </div>
+                )}
+
+                <Separator />
+
+                <div className="space-y-4">
+                  <div className="flex items-center justify-between">
+                    <div>
+                      <Label className="text-base">Use Shared Caches</Label>
+                      <p className="text-sm text-muted-foreground">
+                        Share downloaded assets between instances.
+                      </p>
+                    </div>
+                    <Switch
+                      checked={settings.useSharedCaches}
+                      onCheckedChange={(checked) => {
+                        setSetting("useSharedCaches", checked);
+                        saveSettings();
+                      }}
+                    />
+                  </div>
+
+                  {!settings.useSharedCaches && (
+                    <div className="flex items-center justify-between">
+                      <div>
+                        <Label className="text-base">
+                          Keep Legacy Per-Instance Storage
+                        </Label>
+                        <p className="text-sm text-muted-foreground">
+                          Maintain separate cache folders for compatibility.
+                        </p>
+                      </div>
+                      <Switch
+                        checked={settings.keepLegacyPerInstanceStorage}
+                        onCheckedChange={(checked) => {
+                          setSetting("keepLegacyPerInstanceStorage", checked);
+                          saveSettings();
+                        }}
+                      />
+                    </div>
+                  )}
+
+                  {settings.useSharedCaches && (
+                    <div className="mt-4">
+                      <Button
+                        variant="outline"
+                        className="w-full"
+                        onClick={handleRunMigration}
+                        disabled={migrating}
+                      >
+                        {migrating ? (
+                          <Loader2 className="h-4 w-4 mr-2 animate-spin" />
+                        ) : (
+                          <Upload className="h-4 w-4 mr-2" />
+                        )}
+                        {migrating
+                          ? "Migrating..."
+                          : "Migrate to Shared Caches"}
+                      </Button>
+                    </div>
+                  )}
+                </div>
+              </CardContent>
+            </Card>
+          </TabsContent>
+
+          <TabsContent value="assistant" className="space-y-6">
+            <Card className="border-border">
+              <CardHeader>
+                <CardTitle className="text-lg">AI Assistant</CardTitle>
+              </CardHeader>
+              <CardContent className="space-y-6">
+                <div className="flex items-center justify-between">
+                  <div>
+                    <Label className="text-base">Enable Assistant</Label>
+                    <p className="text-sm text-muted-foreground">
+                      Enable the AI assistant for help with Minecraft issues.
+                    </p>
+                  </div>
+                  <Switch
+                    checked={settings.assistant.enabled}
+                    onCheckedChange={(checked) => {
+                      setAssistantSetting("enabled", checked);
+                      saveSettings();
+                    }}
+                  />
+                </div>
+
+                {settings.assistant.enabled && (
+                  <>
+                    <div>
+                      <Label className="mb-2">LLM Provider</Label>
+                      <Select
+                        value={settings.assistant.llmProvider}
+                        onValueChange={(value) => {
+                          setAssistantSetting("llmProvider", value as any);
+                          saveSettings();
+                        }}
+                      >
+                        <SelectTrigger>
+                          <SelectValue />
+                        </SelectTrigger>
+                        <SelectContent>
+                          {llmProviderOptions.map((option) => (
+                            <SelectItem key={option.value} value={option.value}>
+                              {option.label}
+                            </SelectItem>
+                          ))}
+                        </SelectContent>
+                      </Select>
+                    </div>
+
+                    <div>
+                      <Label className="mb-2">Model</Label>
+                      <Select
+                        value={
+                          settings.assistant.llmProvider === "ollama"
+                            ? settings.assistant.ollamaModel
+                            : settings.assistant.openaiModel
+                        }
+                        onValueChange={(value) => {
+                          if (settings.assistant.llmProvider === "ollama") {
+                            setAssistantSetting("ollamaModel", value);
+                          } else {
+                            setAssistantSetting("openaiModel", value);
+                          }
+                          saveSettings();
+                        }}
+                      >
+                        <SelectTrigger>
+                          <SelectValue />
+                        </SelectTrigger>
+                        <SelectContent>
+                          {currentModelOptions.map((model) => (
+                            <SelectItem key={model.value} value={model.value}>
+                              {model.label}
+                            </SelectItem>
+                          ))}
+                        </SelectContent>
+                      </Select>
+                    </div>
+
+                    {settings.assistant.llmProvider === "ollama" && (
+                      <div>
+                        <Label className="mb-2">Ollama Endpoint</Label>
+                        <Input
+                          value={settings.assistant.ollamaEndpoint}
+                          onChange={(e) => {
+                            setAssistantSetting(
+                              "ollamaEndpoint",
+                              e.target.value,
+                            );
+                            saveSettings();
+                          }}
+                          placeholder="http://localhost:11434"
+                        />
+                      </div>
+                    )}
+
+                    {settings.assistant.llmProvider === "openai" && (
+                      <>
+                        <div>
+                          <Label className="mb-2">OpenAI API Key</Label>
+                          <Input
+                            type="password"
+                            value={settings.assistant.openaiApiKey || ""}
+                            onChange={(e) => {
+                              setAssistantSetting(
+                                "openaiApiKey",
+                                e.target.value || null,
+                              );
+                              saveSettings();
+                            }}
+                            placeholder="Enter your OpenAI API key"
+                          />
+                        </div>
+                        <div>
+                          <Label className="mb-2">OpenAI Endpoint</Label>
+                          <Input
+                            value={settings.assistant.openaiEndpoint}
+                            onChange={(e) => {
+                              setAssistantSetting(
+                                "openaiEndpoint",
+                                e.target.value,
+                              );
+                              saveSettings();
+                            }}
+                            placeholder="https://api.openai.com/v1"
+                          />
+                        </div>
+                      </>
+                    )}
+
+                    <div>
+                      <Label className="mb-2">Response Language</Label>
+                      <Select
+                        value={settings.assistant.responseLanguage}
+                        onValueChange={(value) => {
+                          setAssistantSetting("responseLanguage", value);
+                          saveSettings();
+                        }}
+                      >
+                        <SelectTrigger>
+                          <SelectValue />
+                        </SelectTrigger>
+                        <SelectContent>
+                          {languageOptions.map((option) => (
+                            <SelectItem key={option.value} value={option.value}>
+                              {option.label}
+                            </SelectItem>
+                          ))}
+                        </SelectContent>
+                      </Select>
+                    </div>
+
+                    <div>
+                      <Label className="mb-2">Assistant Persona</Label>
+                      <Select
+                        value={selectedPersona}
+                        onValueChange={handleApplyPersona}
+                      >
+                        <SelectTrigger>
+                          <SelectValue />
+                        </SelectTrigger>
+                        <SelectContent>
+                          {personas.map((persona) => (
+                            <SelectItem
+                              key={persona.value}
+                              value={persona.value}
+                            >
+                              {persona.label}
+                            </SelectItem>
+                          ))}
+                        </SelectContent>
+                      </Select>
+                      <div className="mt-2">
+                        <Button
+                          variant="outline"
+                          size="sm"
+                          onClick={handleResetSystemPrompt}
+                        >
+                          Reset to Default
+                        </Button>
+                      </div>
+                    </div>
+
+                    <div>
+                      <Label className="mb-2">System Prompt</Label>
+
+                      <Textarea
+                        value={settings.assistant.systemPrompt}
+                        onChange={(e) => {
+                          setAssistantSetting("systemPrompt", e.target.value);
+                          saveSettings();
+                        }}
+                        rows={6}
+                        className="font-mono text-sm"
+                      />
+                    </div>
+
+                    <div>
+                      <Label className="mb-2">Text-to-Speech</Label>
+
+                      <Select
+                        value={settings.assistant.ttsProvider}
+                        onValueChange={(value) => {
+                          setAssistantSetting("ttsProvider", value);
+                          saveSettings();
+                        }}
+                      >
+                        <SelectTrigger>
+                          <SelectValue />
+                        </SelectTrigger>
+
+                        <SelectContent>
+                          {ttsProviderOptions.map((option) => (
+                            <SelectItem key={option.value} value={option.value}>
+                              {option.label}
+                            </SelectItem>
+                          ))}
+                        </SelectContent>
+                      </Select>
+                    </div>
+                  </>
+                )}
+              </CardContent>
+            </Card>
+          </TabsContent>
+        </ScrollArea>
+      </Tabs>
+
+      {/* Java Download Modal */}
+      <Dialog
+        open={showJavaDownloadModal}
+        onOpenChange={closeJavaDownloadModal}
+      >
+        <DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden">
+          <DialogHeader>
+            <DialogTitle>Download Java</DialogTitle>
+            <DialogDescription>
+              Download and install Java for Minecraft.
+            </DialogDescription>
+          </DialogHeader>
+
+          <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
+            <div className="space-y-4">
+              <div>
+                <Label className="mb-2">Java Version</Label>
+                <Select
+                  value={selectedMajorVersion?.toString() || ""}
+                  onValueChange={(v) => selectMajorVersion(parseInt(v, 10))}
+                >
+                  <SelectTrigger>
+                    <SelectValue placeholder="Select version" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    {availableMajorVersions.map((version) => (
+                      <SelectItem key={version} value={version.toString()}>
+                        Java {version}
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+              </div>
+
+              <div>
+                <Label className="mb-2">Type</Label>
+                <Select
+                  value={selectedImageType}
+                  onValueChange={(v) => set({ selectedImageType: v as any })}
+                >
+                  <SelectTrigger>
+                    <SelectValue />
+                  </SelectTrigger>
+                  <SelectContent>
+                    <SelectItem value="jre">JRE (Runtime)</SelectItem>
+                    <SelectItem value="jdk">JDK (Development)</SelectItem>
+                  </SelectContent>
+                </Select>
+              </div>
+
+              <div className="flex items-center space-x-2">
+                <Checkbox
+                  id="recommended"
+                  checked={showOnlyRecommended}
+                  onCheckedChange={(checked) =>
+                    set({ showOnlyRecommended: !!checked })
+                  }
+                />
+                <Label htmlFor="recommended">Show only LTS/Recommended</Label>
+              </div>
+
+              <div>
+                <Label className="mb-2">Search</Label>
+                <Input
+                  placeholder="Search versions..."
+                  value={searchQuery}
+                  onChange={(e) => set({ searchQuery: e.target.value })}
+                />
+              </div>
+
+              <Button
+                variant="outline"
+                size="sm"
+                onClick={refreshCatalog}
+                disabled={isLoadingCatalog}
+              >
+                <RefreshCw
+                  className={`h-4 w-4 mr-2 ${isLoadingCatalog ? "animate-spin" : ""}`}
+                />
+                Refresh Catalog
+              </Button>
+            </div>
+
+            <div className="md:col-span-2">
+              <ScrollArea className="h-75 pr-4">
+                {isLoadingCatalog ? (
+                  <div className="flex items-center justify-center h-full">
+                    <Loader2 className="h-8 w-8 animate-spin" />
+                  </div>
+                ) : catalogError ? (
+                  <div className="text-red-500 p-4">{catalogError}</div>
+                ) : filteredReleases.length === 0 ? (
+                  <div className="text-muted-foreground p-4 text-center">
+                    No Java versions found
+                  </div>
+                ) : (
+                  <div className="space-y-2">
+                    {filteredReleases.map((release) => {
+                      const status = installStatus(
+                        release.majorVersion,
+                        release.imageType,
+                      );
+                      return (
+                        <Card
+                          key={`${release.majorVersion}-${release.imageType}`}
+                          className="p-3 cursor-pointer hover:bg-accent"
+                          onClick={() =>
+                            selectMajorVersion(release.majorVersion)
+                          }
+                        >
+                          <div className="flex items-center justify-between">
+                            <div>
+                              <div className="font-medium">
+                                Java {release.majorVersion}{" "}
+                                {release.imageType.toUpperCase()}
+                              </div>
+                              <div className="text-sm text-muted-foreground">
+                                {release.releaseName} • {release.architecture}{" "}
+                                {release.architecture}
+                              </div>
+                            </div>
+                            <div className="flex items-center gap-2">
+                              {release.isLts && (
+                                <Badge variant="secondary">LTS</Badge>
+                              )}
+                              {status === "installed" && (
+                                <Badge variant="default">Installed</Badge>
+                              )}
+                              {status === "available" && (
+                                <Button
+                                  variant="ghost"
+                                  size="sm"
+                                  onClick={(e) => {
+                                    e.stopPropagation();
+                                    selectMajorVersion(release.majorVersion);
+                                    downloadJava();
+                                  }}
+                                >
+                                  <Download className="h-3 w-3 mr-1" />
+                                  Download
+                                </Button>
+                              )}
+                            </div>
+                          </div>
+                        </Card>
+                      );
+                    })}
+                  </div>
+                )}
+              </ScrollArea>
+            </div>
+          </div>
+
+          {isDownloadingJava && downloadProgress && (
+            <div className="mt-4 p-4 border rounded-lg">
+              <div className="flex justify-between items-center mb-2">
+                <span className="text-sm font-medium">
+                  {downloadProgress.fileName}
+                </span>
+                <span className="text-sm text-muted-foreground">
+                  {Math.round(downloadProgress.percentage)}%
+                </span>
+              </div>
+              <div className="w-full bg-secondary h-2 rounded-full overflow-hidden">
+                <div
+                  className="bg-primary h-full transition-all duration-300"
+                  style={{ width: `${downloadProgress.percentage}%` }}
+                />
+              </div>
+            </div>
+          )}
+
+          <DialogFooter>
+            <Button
+              variant="outline"
+              onClick={closeJavaDownloadModal}
+              disabled={isDownloadingJava}
+            >
+              Cancel
+            </Button>
+            {selectedMajorVersion && (
+              <Button
+                onClick={() => downloadJava()}
+                disabled={isDownloadingJava}
+              >
+                {isDownloadingJava ? (
+                  <>
+                    <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                    Downloading...
+                  </>
+                ) : (
+                  <>
+                    <Download className="mr-2 h-4 w-4" />
+                    Download Java {selectedMajorVersion}
+                  </>
+                )}
+              </Button>
+            )}
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+
+      {/* Config Editor Modal */}
+      <Dialog open={showConfigEditor} onOpenChange={closeConfigEditor}>
+        <DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden">
+          <DialogHeader>
+            <DialogTitle>Edit Configuration</DialogTitle>
+            <DialogDescription>
+              Edit the raw JSON configuration file.
+            </DialogDescription>
+          </DialogHeader>
+
+          <div className="text-sm text-muted-foreground mb-2">
+            File: {configFilePath}
+          </div>
+
+          {configEditorError && (
+            <div className="text-red-500 p-3 bg-red-50 dark:bg-red-950/30 rounded-md">
+              {configEditorError}
+            </div>
+          )}
+
+          <Textarea
+            value={rawConfigContent}
+            onChange={(e) => set({ rawConfigContent: e.target.value })}
+            className="font-mono text-sm h-100 resize-none"
+            spellCheck={false}
+          />
+
+          <DialogFooter>
+            <Button variant="outline" onClick={closeConfigEditor}>
+              Cancel
+            </Button>
+            <Button onClick={() => saveRawConfig()}>Save Changes</Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+    </div>
+  );
+}
-- 
cgit v1.2.3-70-g09d2

