package main import ( "encoding/base64" "encoding/json" "fmt" "image/color" "io" "net/http" "os" "path/filepath" "strings" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/driver/desktop" "fyne.io/fyne/v2/storage" "fyne.io/fyne/v2/widget" ) func main() { myApp := app.NewWithID("finder2e_foundry_converter") myWindow := myApp.NewWindow("Finder2e Foundry Converter") prefs := myApp.Preferences() // LM Studio Integration savedAddr := prefs.StringWithFallback("LMStudioAddress", "http://localhost:1234") addrEntry := widget.NewEntry() addrEntry.SetText(savedAddr) addrEntry.SetPlaceHolder("LM Studio URL (e.g. http://192.168.1.10:1234)") addrEntry.OnChanged = func(s string) { prefs.SetString("LMStudioAddress", s) } addrContainer := container.NewVBox( widget.NewLabel("LM Studio Address:"), addrEntry, ) addrContainer.Hide() // LLM Provider Selection var toggleAddrBtn *widget.Button savedProvider := prefs.StringWithFallback("SelectedProvider", "LM Studio") providerSelect := widget.NewSelect([]string{"LM Studio"}, func(s string) { fmt.Printf("Provider changed to: %s\n", s) prefs.SetString("SelectedProvider", s) if s == "LM Studio" { toggleAddrBtn.Show() } else { toggleAddrBtn.Hide() addrContainer.Hide() } }) addrManuallyOpened := false toggleAddrBtn = widget.NewButton("Address", func() { if addrContainer.Visible() { addrContainer.Hide() addrManuallyOpened = false } else { addrContainer.Show() addrManuallyOpened = true } }) providerSelect.SetSelected(savedProvider) errorLabel := canvas.NewText("Error: LM Studio not found. Retrying...", color.NRGBA{R: 255, G: 0, B: 0, A: 255}) errorLabel.Alignment = fyne.TextAlignCenter errorLabel.Hide() modelSelect := widget.NewSelect([]string{"Loading models..."}, func(s string) { if s != "" && !strings.HasPrefix(s, "Loading") && !strings.HasPrefix(s, "Error") && !strings.HasPrefix(s, "No models") { prefs.SetString("LastSelectedModel", s) } }) modelSelect.PlaceHolder = "Select Model" refreshModels := func() { if providerSelect.Selected != "LM Studio" { fyne.Do(func() { errorLabel.Hide() }) return } currentAddr := strings.TrimSuffix(addrEntry.Text, "/") resp, err := http.Get(fmt.Sprintf("%s/v1/models", currentAddr)) if err != nil { fyne.Do(func() { modelSelect.Options = []string{"Error: LM Studio not found"} modelSelect.Refresh() errorLabel.Show() errorLabel.Refresh() addrContainer.Show() // Auto-show on failure }) return } defer resp.Body.Close() var result struct { Data []struct { ID string `json:"id"` } `json:"data"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { fyne.Do(func() { modelSelect.Options = []string{"Error decoding models"} modelSelect.Refresh() }) return } var models []string for _, m := range result.Data { models = append(models, m.ID) } if len(models) == 0 { models = []string{"No models loaded in LM Studio"} } fyne.Do(func() { errorLabel.Hide() errorLabel.Refresh() modelSelect.Options = models modelSelect.Refresh() lastModel := prefs.String("LastSelectedModel") if lastModel != "" && (modelSelect.Selected == "" || modelSelect.Selected == "Loading models...") { for _, m := range models { if m == lastModel { modelSelect.SetSelected(m) break } } } if !addrManuallyOpened { addrContainer.Hide() } }) } // Retry logic: Refresh models every 3 seconds with countdown go func() { for { refreshModels() if errorLabel.Visible() { for i := 3; i > 0; i-- { fyne.Do(func() { errorLabel.Text = fmt.Sprintf("Error: LM Studio not found. Retrying in %d...", i) errorLabel.Refresh() }) time.Sleep(1 * time.Second) } } else { time.Sleep(3 * time.Second) } } }() textEntry := widget.NewMultiLineEntry() textEntry.SetPlaceHolder("Enter description here...") imagePathsContainer := container.NewVBox() addImageEntry := func(path string) { fyne.Do(func() { newEntry := widget.NewEntry() newEntry.SetText(path) imagePathsContainer.Add(newEntry) imagePathsContainer.Refresh() }) } isImage := func(ext string) bool { switch ext { case ".png", ".jpg", ".jpeg", ".webp": return true } return false } imagePickerButton := widget.NewButton("Select Image", func() { fd := dialog.NewFileOpen(func(reader fyne.URIReadCloser, err error) { if err != nil || reader == nil { return } defer reader.Close() addImageEntry(reader.URI().Path()) }, myWindow) fd.SetFilter(storage.NewExtensionFileFilter([]string{".png", ".jpg", ".jpeg", ".webp"})) fd.Show() }) // Drag and Drop support myWindow.SetOnDropped(func(pos fyne.Position, uris []fyne.URI) { for _, u := range uris { if isImage(u.Extension()) { addImageEntry(u.Path()) } } }) // Paste support (Ctrl+V) pasteShortcut := &desktop.CustomShortcut{KeyName: fyne.KeyV, Modifier: fyne.KeyModifierShortcutDefault} myWindow.Canvas().AddShortcut(pasteShortcut, func(shortcut fyne.Shortcut) { content := myWindow.Clipboard().Content() if content == "" { return } // Handle file URLs (sometimes copied from file managers) path := strings.TrimPrefix(content, "file://") path = strings.TrimSpace(path) ext := strings.ToLower(filepath.Ext(path)) if isImage(ext) { // Check if it's a valid local path if _, err := os.Stat(path); err == nil { addImageEntry(path) } } }) generateButton := widget.NewButton("Generate", nil) generateButton.OnTapped = func() { if modelSelect.Selected == "" { dialog.ShowInformation("Selection Required", "Please select a model first.", myWindow) return } if providerSelect.Selected == "LM Studio" { currentAddr := strings.TrimSuffix(addrEntry.Text, "/") fmt.Printf("Sending request to LM Studio at %s (%s)...\n", currentAddr, modelSelect.Selected) // Prepare messages type MessageContent struct { Type string `json:"type"` Text string `json:"text,omitempty"` ImageURL *struct { URL string `json:"url"` } `json:"image_url,omitempty"` } var content []MessageContent content = append(content, MessageContent{Type: "text", Text: textEntry.Text}) for _, obj := range imagePathsContainer.Objects { if entry, ok := obj.(*widget.Entry); ok && entry.Text != "" { imgData, err := os.ReadFile(entry.Text) if err != nil { fmt.Printf("Error reading image %s: %v\n", entry.Text, err) continue } base64Img := base64.StdEncoding.EncodeToString(imgData) mimeType := "image/png" if strings.HasSuffix(strings.ToLower(entry.Text), ".jpg") || strings.HasSuffix(strings.ToLower(entry.Text), ".jpeg") { mimeType = "image/jpeg" } else if strings.HasSuffix(strings.ToLower(entry.Text), ".webp") { mimeType = "image/webp" } content = append(content, MessageContent{ Type: "image_url", ImageURL: &struct { URL string `json:"url"` }{ URL: fmt.Sprintf("data:%s;base64,%s", mimeType, base64Img), }, }) } } requestBody, _ := json.Marshal(map[string]interface{}{ "model": modelSelect.Selected, "messages": []interface{}{map[string]interface{}{"role": "user", "content": content}}, }) resp, err := http.Post(fmt.Sprintf("%s/v1/chat/completions", currentAddr), "application/json", strings.NewReader(string(requestBody))) if err != nil { fmt.Printf("Error calling LM Studio: %v\n", err) return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var chatResp struct { Choices []struct { Message struct { Content string `json:"content"` } `json:"message"` } `json:"choices"` Error struct { Message string `json:"message"` } `json:"error"` } if err := json.Unmarshal(body, &chatResp); err != nil { fmt.Printf("Error decoding response: %v\n", err) fmt.Println("Raw response:", string(body)) return } if chatResp.Error.Message != "" { fmt.Printf("LM Studio Error: %s\n", chatResp.Error.Message) return } if len(chatResp.Choices) > 0 { fmt.Println("\nLM Studio Response:") fmt.Println(chatResp.Choices[0].Message.Content) } else { fmt.Println("No response choices returned.") } } } scrollContainer := container.NewVScroll(imagePathsContainer) scrollContainer.SetMinSize(fyne.NewSize(0, 150)) content := container.NewVBox( widget.NewLabel("Provider:"), container.NewBorder(nil, nil, nil, toggleAddrBtn, providerSelect), errorLabel, addrContainer, widget.NewLabel("Model:"), container.NewBorder(nil, nil, nil, widget.NewButton("Refresh", refreshModels), modelSelect), widget.NewLabel("Description:"), textEntry, widget.NewLabel("Images:"), scrollContainer, imagePickerButton, generateButton, ) myWindow.SetContent(content) myWindow.Resize(fyne.NewSize(500, 500)) myWindow.ShowAndRun() }