package launcher import ( "fmt" "log" "net/url" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/widget" ) // EnvVar represents an environment variable type EnvVar struct { Key string Value string } // LauncherUI handles the user interface type LauncherUI struct { // Status display statusLabel *widget.Label versionLabel *widget.Label // Control buttons startStopButton *widget.Button webUIButton *widget.Button updateButton *widget.Button downloadButton *widget.Button // Configuration modelsPathEntry *widget.Entry backendsPathEntry *widget.Entry addressEntry *widget.Entry logLevelSelect *widget.Select startOnBootCheck *widget.Check // Environment Variables envVarsData []EnvVar newEnvKeyEntry *widget.Entry newEnvValueEntry *widget.Entry updateEnvironmentDisplay func() // Logs logText *widget.Entry // Progress progressBar *widget.ProgressBar // Update management latestVersion string // Reference to launcher launcher *Launcher } // NewLauncherUI creates a new UI instance func NewLauncherUI() *LauncherUI { return &LauncherUI{ statusLabel: widget.NewLabel("Initializing..."), versionLabel: widget.NewLabel("Version: Unknown"), startStopButton: widget.NewButton("Start LocalAI", nil), webUIButton: widget.NewButton("Open WebUI", nil), updateButton: widget.NewButton("Check for Updates", nil), modelsPathEntry: widget.NewEntry(), backendsPathEntry: widget.NewEntry(), addressEntry: widget.NewEntry(), logLevelSelect: widget.NewSelect([]string{"error", "warn", "info", "debug", "trace"}, nil), startOnBootCheck: widget.NewCheck("Start LocalAI on system boot", nil), logText: widget.NewMultiLineEntry(), progressBar: widget.NewProgressBar(), envVarsData: []EnvVar{}, // Initialize the environment variables slice } } // CreateMainUI creates the main UI layout func (ui *LauncherUI) CreateMainUI(launcher *Launcher) *fyne.Container { ui.launcher = launcher ui.setupBindings() // Main tab with status and controls // Configuration is now the main content configTab := ui.createConfigTab() // Create a simple container instead of tabs since we only have settings tabs := container.NewVBox( widget.NewCard("LocalAI Launcher Settings", "", configTab), ) return tabs } // createConfigTab creates the configuration tab func (ui *LauncherUI) createConfigTab() *fyne.Container { // Path configuration pathsCard := widget.NewCard("Paths", "", container.NewGridWithColumns(2, widget.NewLabel("Models Path:"), ui.modelsPathEntry, widget.NewLabel("Backends Path:"), ui.backendsPathEntry, )) // Server configuration serverCard := widget.NewCard("Server", "", container.NewVBox( container.NewGridWithColumns(2, widget.NewLabel("Address:"), ui.addressEntry, widget.NewLabel("Log Level:"), ui.logLevelSelect, ), ui.startOnBootCheck, )) // Save button saveButton := widget.NewButton("Save Configuration", func() { ui.saveConfiguration() }) // Environment Variables section envCard := ui.createEnvironmentSection() return container.NewVBox( pathsCard, serverCard, envCard, saveButton, ) } // createEnvironmentSection creates the environment variables section for the config tab func (ui *LauncherUI) createEnvironmentSection() *fyne.Container { // Initialize environment variables widgets ui.newEnvKeyEntry = widget.NewEntry() ui.newEnvKeyEntry.SetPlaceHolder("Environment Variable Name") ui.newEnvValueEntry = widget.NewEntry() ui.newEnvValueEntry.SetPlaceHolder("Environment Variable Value") // Add button addButton := widget.NewButton("Add Environment Variable", func() { ui.addEnvironmentVariable() }) // Environment variables list with delete buttons ui.envVarsData = []EnvVar{} // Create container for environment variables envVarsContainer := container.NewVBox() // Update function to rebuild the environment variables display ui.updateEnvironmentDisplay = func() { envVarsContainer.Objects = nil for i, envVar := range ui.envVarsData { index := i // Capture index for closure // Create row with label and delete button envLabel := widget.NewLabel(fmt.Sprintf("%s = %s", envVar.Key, envVar.Value)) deleteBtn := widget.NewButton("Delete", func() { ui.confirmDeleteEnvironmentVariable(index) }) deleteBtn.Importance = widget.DangerImportance row := container.NewBorder(nil, nil, nil, deleteBtn, envLabel) envVarsContainer.Add(row) } envVarsContainer.Refresh() } // Create a scrollable container for the environment variables envScroll := container.NewScroll(envVarsContainer) envScroll.SetMinSize(fyne.NewSize(400, 150)) // Input section for adding new environment variables inputSection := container.NewVBox( container.NewGridWithColumns(2, ui.newEnvKeyEntry, ui.newEnvValueEntry, ), addButton, ) // Environment variables card envCard := widget.NewCard("Environment Variables", "", container.NewVBox( inputSection, widget.NewSeparator(), envScroll, )) return container.NewVBox(envCard) } // addEnvironmentVariable adds a new environment variable func (ui *LauncherUI) addEnvironmentVariable() { key := ui.newEnvKeyEntry.Text value := ui.newEnvValueEntry.Text log.Printf("addEnvironmentVariable: attempting to add %s=%s", key, value) log.Printf("addEnvironmentVariable: current ui.envVarsData has %d items: %v", len(ui.envVarsData), ui.envVarsData) if key == "" { log.Printf("addEnvironmentVariable: key is empty, showing error") dialog.ShowError(fmt.Errorf("environment variable name cannot be empty"), ui.launcher.window) return } // Check if key already exists for _, envVar := range ui.envVarsData { if envVar.Key == key { log.Printf("addEnvironmentVariable: key %s already exists, showing error", key) dialog.ShowError(fmt.Errorf("environment variable '%s' already exists", key), ui.launcher.window) return } } log.Printf("addEnvironmentVariable: adding new env var %s=%s", key, value) ui.envVarsData = append(ui.envVarsData, EnvVar{Key: key, Value: value}) log.Printf("addEnvironmentVariable: after adding, ui.envVarsData has %d items: %v", len(ui.envVarsData), ui.envVarsData) fyne.Do(func() { if ui.updateEnvironmentDisplay != nil { ui.updateEnvironmentDisplay() } // Clear input fields ui.newEnvKeyEntry.SetText("") ui.newEnvValueEntry.SetText("") }) log.Printf("addEnvironmentVariable: calling saveEnvironmentVariables") // Save to configuration ui.saveEnvironmentVariables() } // removeEnvironmentVariable removes an environment variable by index func (ui *LauncherUI) removeEnvironmentVariable(index int) { if index >= 0 && index < len(ui.envVarsData) { ui.envVarsData = append(ui.envVarsData[:index], ui.envVarsData[index+1:]...) fyne.Do(func() { if ui.updateEnvironmentDisplay != nil { ui.updateEnvironmentDisplay() } }) ui.saveEnvironmentVariables() } } // saveEnvironmentVariables saves environment variables to the configuration func (ui *LauncherUI) saveEnvironmentVariables() { if ui.launcher == nil { log.Printf("saveEnvironmentVariables: launcher is nil") return } config := ui.launcher.GetConfig() log.Printf("saveEnvironmentVariables: before - Environment vars: %v", config.EnvironmentVars) config.EnvironmentVars = make(map[string]string) for _, envVar := range ui.envVarsData { config.EnvironmentVars[envVar.Key] = envVar.Value log.Printf("saveEnvironmentVariables: adding %s=%s", envVar.Key, envVar.Value) } log.Printf("saveEnvironmentVariables: after - Environment vars: %v", config.EnvironmentVars) log.Printf("saveEnvironmentVariables: calling SetConfig with %d environment variables", len(config.EnvironmentVars)) err := ui.launcher.SetConfig(config) if err != nil { log.Printf("saveEnvironmentVariables: failed to save config: %v", err) } else { log.Printf("saveEnvironmentVariables: config saved successfully") } } // confirmDeleteEnvironmentVariable shows confirmation dialog for deleting an environment variable func (ui *LauncherUI) confirmDeleteEnvironmentVariable(index int) { if index >= 0 && index < len(ui.envVarsData) { envVar := ui.envVarsData[index] dialog.ShowConfirm("Remove Environment Variable", fmt.Sprintf("Remove environment variable '%s'?", envVar.Key), func(remove bool) { if remove { ui.removeEnvironmentVariable(index) } }, ui.launcher.window) } } // setupBindings sets up event handlers for UI elements func (ui *LauncherUI) setupBindings() { // Start/Stop button ui.startStopButton.OnTapped = func() { if ui.launcher.IsRunning() { ui.stopLocalAI() } else { ui.startLocalAI() } } // WebUI button ui.webUIButton.OnTapped = func() { ui.openWebUI() } ui.webUIButton.Disable() // Disabled until LocalAI is running // Update button ui.updateButton.OnTapped = func() { ui.checkForUpdates() } // Log level selection ui.logLevelSelect.OnChanged = func(selected string) { if ui.launcher != nil { config := ui.launcher.GetConfig() config.LogLevel = selected ui.launcher.SetConfig(config) } } } // startLocalAI starts the LocalAI service func (ui *LauncherUI) startLocalAI() { fyne.Do(func() { ui.startStopButton.Disable() }) ui.UpdateStatus("Starting LocalAI...") go func() { err := ui.launcher.StartLocalAI() if err != nil { ui.UpdateStatus("Failed to start: " + err.Error()) fyne.DoAndWait(func() { dialog.ShowError(err, ui.launcher.window) }) } else { fyne.Do(func() { ui.startStopButton.SetText("Stop LocalAI") ui.webUIButton.Enable() }) } fyne.Do(func() { ui.startStopButton.Enable() }) }() } // stopLocalAI stops the LocalAI service func (ui *LauncherUI) stopLocalAI() { fyne.Do(func() { ui.startStopButton.Disable() }) ui.UpdateStatus("Stopping LocalAI...") go func() { err := ui.launcher.StopLocalAI() if err != nil { fyne.DoAndWait(func() { dialog.ShowError(err, ui.launcher.window) }) } else { fyne.Do(func() { ui.startStopButton.SetText("Start LocalAI") ui.webUIButton.Disable() }) } fyne.Do(func() { ui.startStopButton.Enable() }) }() } // openWebUI opens the LocalAI WebUI in the default browser func (ui *LauncherUI) openWebUI() { webURL := ui.launcher.GetWebUIURL() parsedURL, err := url.Parse(webURL) if err != nil { dialog.ShowError(err, ui.launcher.window) return } // Open URL in default browser fyne.CurrentApp().OpenURL(parsedURL) } // saveConfiguration saves the current configuration func (ui *LauncherUI) saveConfiguration() { log.Printf("saveConfiguration: starting to save configuration") config := ui.launcher.GetConfig() log.Printf("saveConfiguration: current config Environment vars: %v", config.EnvironmentVars) log.Printf("saveConfiguration: ui.envVarsData has %d items: %v", len(ui.envVarsData), ui.envVarsData) config.ModelsPath = ui.modelsPathEntry.Text config.BackendsPath = ui.backendsPathEntry.Text config.Address = ui.addressEntry.Text config.LogLevel = ui.logLevelSelect.Selected config.StartOnBoot = ui.startOnBootCheck.Checked // Ensure environment variables are included in the configuration config.EnvironmentVars = make(map[string]string) for _, envVar := range ui.envVarsData { config.EnvironmentVars[envVar.Key] = envVar.Value log.Printf("saveConfiguration: adding env var %s=%s", envVar.Key, envVar.Value) } log.Printf("saveConfiguration: final config Environment vars: %v", config.EnvironmentVars) err := ui.launcher.SetConfig(config) if err != nil { log.Printf("saveConfiguration: failed to save config: %v", err) dialog.ShowError(err, ui.launcher.window) } else { log.Printf("saveConfiguration: config saved successfully") dialog.ShowInformation("Configuration", "Configuration saved successfully", ui.launcher.window) } } // checkForUpdates checks for available updates func (ui *LauncherUI) checkForUpdates() { fyne.Do(func() { ui.updateButton.Disable() }) ui.UpdateStatus("Checking for updates...") go func() { available, version, err := ui.launcher.CheckForUpdates() if err != nil { ui.UpdateStatus("Failed to check updates: " + err.Error()) fyne.DoAndWait(func() { dialog.ShowError(err, ui.launcher.window) }) } else if available { ui.latestVersion = version // Store the latest version ui.UpdateStatus("Update available: " + version) fyne.Do(func() { if ui.downloadButton != nil { ui.downloadButton.Enable() } }) ui.NotifyUpdateAvailable(version) } else { ui.UpdateStatus("No updates available") fyne.DoAndWait(func() { dialog.ShowInformation("Updates", "You are running the latest version", ui.launcher.window) }) } fyne.Do(func() { ui.updateButton.Enable() }) }() } // downloadUpdate downloads the latest update func (ui *LauncherUI) downloadUpdate() { // Use stored version or check for updates version := ui.latestVersion if version == "" { _, v, err := ui.launcher.CheckForUpdates() if err != nil { dialog.ShowError(err, ui.launcher.window) return } version = v ui.latestVersion = version } if version == "" { dialog.ShowError(fmt.Errorf("no version information available"), ui.launcher.window) return } // Disable buttons during download if ui.downloadButton != nil { fyne.Do(func() { ui.downloadButton.Disable() }) } fyne.Do(func() { ui.progressBar.Show() ui.progressBar.SetValue(0) }) ui.UpdateStatus("Downloading update " + version + "...") go func() { err := ui.launcher.DownloadUpdate(version, func(progress float64) { // Update progress bar fyne.Do(func() { ui.progressBar.SetValue(progress) }) // Update status with percentage percentage := int(progress * 100) ui.UpdateStatus(fmt.Sprintf("Downloading update %s... %d%%", version, percentage)) }) fyne.Do(func() { ui.progressBar.Hide() }) // Re-enable buttons after download if ui.downloadButton != nil { fyne.Do(func() { ui.downloadButton.Enable() }) } if err != nil { fyne.DoAndWait(func() { ui.UpdateStatus("Failed to download update: " + err.Error()) dialog.ShowError(err, ui.launcher.window) }) } else { fyne.DoAndWait(func() { ui.UpdateStatus("Update downloaded successfully") dialog.ShowInformation("Update", "Update downloaded successfully. Please restart the launcher to use the new version.", ui.launcher.window) }) } }() } // UpdateStatus updates the status label func (ui *LauncherUI) UpdateStatus(status string) { if ui.statusLabel != nil { fyne.Do(func() { ui.statusLabel.SetText(status) }) } } // OnLogUpdate handles new log content func (ui *LauncherUI) OnLogUpdate(logLine string) { if ui.logText != nil { fyne.Do(func() { currentText := ui.logText.Text ui.logText.SetText(currentText + logLine) // Auto-scroll to bottom (simplified) ui.logText.CursorRow = len(ui.logText.Text) }) } } // NotifyUpdateAvailable shows an update notification func (ui *LauncherUI) NotifyUpdateAvailable(version string) { if ui.launcher != nil && ui.launcher.window != nil { fyne.DoAndWait(func() { dialog.ShowConfirm("Update Available", "A new version ("+version+") is available. Would you like to download it?", func(confirmed bool) { if confirmed { ui.downloadUpdate() } }, ui.launcher.window) }) } } // LoadConfiguration loads the current configuration into UI elements func (ui *LauncherUI) LoadConfiguration() { if ui.launcher == nil { log.Printf("UI LoadConfiguration: launcher is nil") return } config := ui.launcher.GetConfig() log.Printf("UI LoadConfiguration: loading config - ModelsPath=%s, BackendsPath=%s, Address=%s, LogLevel=%s", config.ModelsPath, config.BackendsPath, config.Address, config.LogLevel) log.Printf("UI LoadConfiguration: Environment vars: %v", config.EnvironmentVars) ui.modelsPathEntry.SetText(config.ModelsPath) ui.backendsPathEntry.SetText(config.BackendsPath) ui.addressEntry.SetText(config.Address) ui.logLevelSelect.SetSelected(config.LogLevel) ui.startOnBootCheck.SetChecked(config.StartOnBoot) // Load environment variables ui.envVarsData = []EnvVar{} for key, value := range config.EnvironmentVars { ui.envVarsData = append(ui.envVarsData, EnvVar{Key: key, Value: value}) } if ui.updateEnvironmentDisplay != nil { fyne.Do(func() { ui.updateEnvironmentDisplay() }) } // Update version display version := ui.launcher.GetCurrentVersion() ui.versionLabel.SetText("Version: " + version) log.Printf("UI LoadConfiguration: configuration loaded successfully") } // showDownloadProgress shows a progress window for downloading LocalAI func (ui *LauncherUI) showDownloadProgress(version, title string) { fyne.DoAndWait(func() { // Create progress window using the launcher's app progressWindow := ui.launcher.app.NewWindow("Downloading LocalAI") progressWindow.Resize(fyne.NewSize(400, 250)) progressWindow.CenterOnScreen() // Progress bar progressBar := widget.NewProgressBar() progressBar.SetValue(0) // Status label statusLabel := widget.NewLabel("Preparing download...") // Release notes button releaseNotesButton := widget.NewButton("View Release Notes", func() { releaseNotesURL, err := ui.launcher.githubReleaseNotesURL(version) if err != nil { log.Printf("Failed to parse URL: %v", err) return } ui.launcher.app.OpenURL(releaseNotesURL) }) // Progress container progressContainer := container.NewVBox( widget.NewLabel(title), progressBar, statusLabel, widget.NewSeparator(), releaseNotesButton, ) progressWindow.SetContent(progressContainer) progressWindow.Show() // Start download in background go func() { err := ui.launcher.DownloadUpdate(version, func(progress float64) { // Update progress bar fyne.Do(func() { progressBar.SetValue(progress) percentage := int(progress * 100) statusLabel.SetText(fmt.Sprintf("Downloading... %d%%", percentage)) }) }) // Handle completion fyne.Do(func() { if err != nil { statusLabel.SetText(fmt.Sprintf("Download failed: %v", err)) // Show error dialog dialog.ShowError(err, progressWindow) } else { statusLabel.SetText("Download completed successfully!") progressBar.SetValue(1.0) // Show success dialog dialog.ShowConfirm("Installation Complete", "LocalAI has been downloaded and installed successfully. You can now start LocalAI from the launcher.", func(close bool) { progressWindow.Close() // Update status ui.UpdateStatus("LocalAI installed successfully") }, progressWindow) } }) }() }) } // UpdateRunningState updates UI based on LocalAI running state func (ui *LauncherUI) UpdateRunningState(isRunning bool) { fyne.Do(func() { if isRunning { ui.startStopButton.SetText("Stop LocalAI") ui.webUIButton.Enable() } else { ui.startStopButton.SetText("Start LocalAI") ui.webUIButton.Disable() } }) } // ShowWelcomeWindow displays the welcome window with helpful information func (ui *LauncherUI) ShowWelcomeWindow() { if ui.launcher == nil || ui.launcher.window == nil { log.Printf("Cannot show welcome window: launcher or window is nil") return } fyne.DoAndWait(func() { // Create welcome window welcomeWindow := ui.launcher.app.NewWindow("Welcome to LocalAI Launcher") welcomeWindow.Resize(fyne.NewSize(600, 500)) welcomeWindow.CenterOnScreen() welcomeWindow.SetCloseIntercept(func() { welcomeWindow.Close() }) // Title titleLabel := widget.NewLabel("Welcome to LocalAI Launcher!") titleLabel.TextStyle = fyne.TextStyle{Bold: true} titleLabel.Alignment = fyne.TextAlignCenter // Welcome message welcomeText := `LocalAI Launcher makes it easy to run LocalAI on your system. What you can do: • Start and stop LocalAI server • Configure models and backends paths • Set environment variables • Check for updates automatically • Access LocalAI WebUI when running Getting Started: 1. Configure your models and backends paths 2. Click "Start LocalAI" to begin 3. Use "Open WebUI" to access the interface 4. Check the system tray for quick access` welcomeLabel := widget.NewLabel(welcomeText) welcomeLabel.Wrapping = fyne.TextWrapWord // Useful links section linksTitle := widget.NewLabel("Useful Links:") linksTitle.TextStyle = fyne.TextStyle{Bold: true} // Create link buttons docsButton := widget.NewButton("📚 Documentation", func() { ui.openURL("https://localai.io/docs/") }) githubButton := widget.NewButton("🐙 GitHub Repository", func() { ui.openURL("https://github.com/mudler/LocalAI") }) modelsButton := widget.NewButton("🤖 Model Gallery", func() { ui.openURL("https://localai.io/models/") }) communityButton := widget.NewButton("💬 Community", func() { ui.openURL("https://discord.gg/XgwjKptP7Z") }) // Checkbox to disable welcome window dontShowAgainCheck := widget.NewCheck("Don't show this welcome window again", func(checked bool) { if ui.launcher != nil { config := ui.launcher.GetConfig() v := !checked config.ShowWelcome = &v ui.launcher.SetConfig(config) } }) config := ui.launcher.GetConfig() if config.ShowWelcome != nil { dontShowAgainCheck.SetChecked(*config.ShowWelcome) } // Close button closeButton := widget.NewButton("Get Started", func() { welcomeWindow.Close() }) closeButton.Importance = widget.HighImportance // Layout linksContainer := container.NewVBox( linksTitle, docsButton, githubButton, modelsButton, communityButton, ) content := container.NewVBox( titleLabel, widget.NewSeparator(), welcomeLabel, widget.NewSeparator(), linksContainer, widget.NewSeparator(), dontShowAgainCheck, widget.NewSeparator(), closeButton, ) welcomeWindow.SetContent(content) welcomeWindow.Show() }) } // openURL opens a URL in the default browser func (ui *LauncherUI) openURL(urlString string) { parsedURL, err := url.Parse(urlString) if err != nil { log.Printf("Failed to parse URL %s: %v", urlString, err) return } fyne.CurrentApp().OpenURL(parsedURL) }