mirror of
https://github.com/mudler/LocalAI.git
synced 2026-06-18 21:58:58 -04:00
fix(launcher): truncate download status labels to stop dialog blowout The download progress windows place a ProgressBar and a status Label in the same VBox. On failure the status label is set to "Download failed: <error>", and the error commonly contains a long, unbreakable URL/path. A Fyne label with default settings reports its MinSize as the full single-line text width, so a long message stretches the window — and the progress bar sharing the VBox — arbitrarily wide (fixes #10355). Set Truncation = fyne.TextTruncateEllipsis on the four affected status labels (the main-window status label plus the status label in each of the three showDownloadProgress implementations). Truncation collapses the label's MinSize to roughly one character plus the ellipsis regardless of content, so the window keeps its intended size. TextWrapWord is not enough because it cannot break a spaceless URL. The full error text remains visible via the dialog.ShowError call already present in each path. Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
807 lines
22 KiB
Go
807 lines
22 KiB
Go
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 {
|
|
// Truncate the status text with an ellipsis. Status messages can carry a
|
|
// download error containing a long, unbreakable URL/path; without this the
|
|
// label demands the full single-line width and stretches the window (and
|
|
// the progress bar) arbitrarily wide. The full error is still shown in the
|
|
// error dialog.
|
|
statusLabel := widget.NewLabel("Initializing...")
|
|
statusLabel.Truncation = fyne.TextTruncateEllipsis
|
|
|
|
return &LauncherUI{
|
|
statusLabel: statusLabel,
|
|
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. Truncate with an ellipsis so a long "Download failed:
|
|
// <url>" message can't stretch the window (and progress bar) to fit the
|
|
// whole error on one line; the full error is shown in the dialog below.
|
|
statusLabel := widget.NewLabel("Preparing download...")
|
|
statusLabel.Truncation = fyne.TextTruncateEllipsis
|
|
|
|
// 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)
|
|
}
|