264 lines
6.9 KiB
Go
264 lines
6.9 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/app"
|
|
"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.New()
|
|
myWindow := myApp.NewWindow("Finder2e Foundry Converter")
|
|
|
|
// LLM Provider Selection
|
|
providerSelect := widget.NewSelect([]string{"LM Studio"}, func(s string) {
|
|
fmt.Printf("Provider changed to: %s\n", s)
|
|
})
|
|
providerSelect.SetSelected("LM Studio")
|
|
|
|
// LM Studio Integration
|
|
modelSelect := widget.NewSelect([]string{"Loading models..."}, nil)
|
|
modelSelect.PlaceHolder = "Select Model"
|
|
|
|
refreshModels := func() {
|
|
if providerSelect.Selected != "LM Studio" {
|
|
return
|
|
}
|
|
resp, err := http.Get("http://localhost:1234/v1/models")
|
|
if err != nil {
|
|
modelSelect.Options = []string{"Error: LM Studio not found"}
|
|
modelSelect.Refresh()
|
|
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 {
|
|
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"}
|
|
}
|
|
modelSelect.Options = models
|
|
modelSelect.Refresh()
|
|
}
|
|
|
|
// Initial fetch
|
|
go refreshModels()
|
|
|
|
textEntry := widget.NewMultiLineEntry()
|
|
textEntry.SetPlaceHolder("Enter description here...")
|
|
|
|
imagePathsContainer := container.NewVBox()
|
|
|
|
addImageEntry := func(path string) {
|
|
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", func() {
|
|
if modelSelect.Selected == "" {
|
|
dialog.ShowInformation("Selection Required", "Please select a model first.", myWindow)
|
|
return
|
|
}
|
|
|
|
if providerSelect.Selected == "LM Studio" {
|
|
fmt.Printf("Sending request to LM Studio (%s)...\n", modelSelect.Selected)
|
|
// ... (rest of the existing LM Studio generation logic remains below)
|
|
}
|
|
})
|
|
|
|
// (I'll keep the actual generation logic block to keep the edit clean)
|
|
// Actually, I should include it to ensure the code is complete.
|
|
// RE-IMPLEMENTING the generate button with provider check:
|
|
|
|
generateButton.OnTapped = func() {
|
|
if modelSelect.Selected == "" {
|
|
dialog.ShowInformation("Selection Required", "Please select a model first.", myWindow)
|
|
return
|
|
}
|
|
|
|
if providerSelect.Selected == "LM Studio" {
|
|
fmt.Printf("Sending request to LM Studio (%s)...\n", 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("http://localhost:1234/v1/chat/completions", "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:"),
|
|
providerSelect,
|
|
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()
|
|
}
|