webui: Add file paste upload support
- Add new /upload endpoint in loophttp.go to save pasted files to /tmp
- Make the implementation generic to handle any file type, not just images
- Implement paste event handling in sketch-chat-input.ts to detect files
- Add logic to upload files and insert file paths in chat input
- Improve random filename generation with better comments
Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: sff2e40b9b3e4c05ak
webui: Improve file upload UI experience
- Use relative path for upload endpoint
- Add loading indicator during file upload
- Show error message if upload fails
- Improve cursor position handling
diff --git a/loop/server/loophttp.go b/loop/server/loophttp.go
index 1cd486a..20e1629 100644
--- a/loop/server/loophttp.go
+++ b/loop/server/loophttp.go
@@ -3,7 +3,9 @@
import (
"context"
+ "crypto/rand"
"encoding/base64"
+ "encoding/hex"
"encoding/json"
"fmt"
"html"
@@ -14,6 +16,7 @@
"net/http/pprof"
"os"
"os/exec"
+ "path/filepath"
"strconv"
"strings"
"sync"
@@ -594,6 +597,62 @@
w.WriteHeader(http.StatusOK)
})
+ // Handler for POST /upload - uploads a file to /tmp
+ s.mux.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ // Limit to 10MB file size
+ r.Body = http.MaxBytesReader(w, r.Body, 10*1024*1024)
+
+ // Parse the multipart form
+ if err := r.ParseMultipartForm(10 * 1024 * 1024); err != nil {
+ http.Error(w, "Failed to parse form: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ // Get the file from the multipart form
+ file, handler, err := r.FormFile("file")
+ if err != nil {
+ http.Error(w, "Failed to get uploaded file: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+ defer file.Close()
+
+ // Generate a unique ID (8 random bytes converted to 16 hex chars)
+ randBytes := make([]byte, 8)
+ if _, err := rand.Read(randBytes); err != nil {
+ http.Error(w, "Failed to generate random filename: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ // Get file extension from the original filename
+ ext := filepath.Ext(handler.Filename)
+
+ // Create a unique filename in the /tmp directory
+ filename := fmt.Sprintf("/tmp/sketch_file_%s%s", hex.EncodeToString(randBytes), ext)
+
+ // Create the destination file
+ destFile, err := os.Create(filename)
+ if err != nil {
+ http.Error(w, "Failed to create destination file: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+ defer destFile.Close()
+
+ // Copy the file contents to the destination file
+ if _, err := io.Copy(destFile, file); err != nil {
+ http.Error(w, "Failed to save file: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ // Return the path to the saved file
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{"path": filename})
+ })
+
// Handler for /cancel - cancels the current inner loop in progress
s.mux.HandleFunc("/cancel", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {