commit e7b1090a853b2e829497cc65aa95f1db1e7a7bab Author: rogueking Date: Tue May 6 13:31:09 2025 -0700 rewrite into go and nextjs diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f8b4450 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +junk2jive-server +.env +.direnv diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..28c5dff --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +env ?= development +port ?= 8080 + +junk2jive: + @go build ./cmd/junk2jive +run: + @go run ./cmd/junk2jive -e $(env) -p $(port) +test: + @go test ./... -v +test_coverage: + # @go test -cover ./... + @go test -coverprofile=coverage.out ./... + @go tool cover -func=coverage.out | grep total: | awk '{print "Total coverage: " $$3}' +clean: + @rm ./junk2jive diff --git a/cmd/junk2jive/main.go b/cmd/junk2jive/main.go new file mode 100644 index 0000000..0c7857e --- /dev/null +++ b/cmd/junk2jive/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "flag" + "fmt" + "log" + "net/http" + "os" + "path/filepath" + + "gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config" + "gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers" + "gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/routes" +) + +func main() { + // Parse command line flags + var configPath string + flag.StringVar(&configPath, "config", "", "Path to config file") + flag.Parse() + + // If no config file specified, look for it in default locations + if configPath == "" { + // Check current directory + if _, err := os.Stat("config.json"); err == nil { + configPath = "config.json" + } else { + // Check config directory relative to executable + exePath, err := os.Executable() + if err == nil { + potentialPath := filepath.Join(filepath.Dir(exePath), "../config/config.json") + if _, err := os.Stat(potentialPath); err == nil { + configPath = potentialPath + } + } + } + } + + // Load configuration + cfg, err := config.Load(configPath) + if err != nil { + log.Fatalf("Failed to load configuration: %v", err) + } + + // Initialize services + // This would initialize your OpenAI and Robowflow services + // based on your configuration + + // Set up the router + router := routes.SetupRoutes(cfg) + + // Start the server + addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port) + log.Printf("Starting server on %s", addr) + if err := http.ListenAndServe(addr, router); err != nil { + log.Fatalf("Failed to start server: %v", err) + } +} \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..3338b46 --- /dev/null +++ b/flake.lock @@ -0,0 +1,25 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1746232882, + "narHash": "sha256-MHmBH2rS8KkRRdoU/feC/dKbdlMkcNkB5mwkuipVHeQ=", + "rev": "7a2622e2c0dbad5c4493cb268aba12896e28b008", + "revCount": 793418, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.793418%2Brev-7a2622e2c0dbad5c4493cb268aba12896e28b008/0196974c-148c-7984-8656-db70973db21b/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/NixOS/nixpkgs/0.1" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..9f4eb1a --- /dev/null +++ b/flake.nix @@ -0,0 +1,38 @@ +{ + description = "A Nix-flake-based Go 1.22 development environment"; + + inputs.nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1"; + + outputs = inputs: + let + goVersion = 23; # Change this to update the whole stack + + supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; + forEachSupportedSystem = f: inputs.nixpkgs.lib.genAttrs supportedSystems (system: f { + pkgs = import inputs.nixpkgs { + inherit system; + overlays = [ inputs.self.overlays.default ]; + }; + }); + in + { + overlays.default = final: prev: { + go = final."go_1_${toString goVersion}"; + }; + + devShells = forEachSupportedSystem ({ pkgs }: { + default = pkgs.mkShell { + packages = with pkgs; [ + # go (version is specified by overlay) + go + + # goimports, godoc, etc. + gotools + + # https://github.com/golangci/golangci-lint + golangci-lint + ]; + }; + }); + }; +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..50b4cad --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitea.miguelmuniz.com/rogueking/junk2jive-server + +go 1.24.2 diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..32f1187 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,26 @@ +package config + +import ( + "os" +) + +type Config struct { + OpenAIKey string + RoboflowKey string + Port string +} + +func LoadConfig() *Config { + return &Config{ + OpenAIKey: os.Getenv("OPENAI_API_KEY"), + RoboflowKey: os.Getenv("ROBOFLOW_API_KEY"), + Port: getEnvWithDefault("PORT", "8080"), + } +} + +func getEnvWithDefault(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} \ No newline at end of file diff --git a/internal/handlers/home.go b/internal/handlers/home.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/handlers/text.go b/internal/handlers/text.go new file mode 100644 index 0000000..81b503a --- /dev/null +++ b/internal/handlers/text.go @@ -0,0 +1,43 @@ +package handlers + +import ( + "encoding/json" + "net/http" + + "gitea.miguelmuniz.com/junk2jive-server/internal/config" + "gitea.miguelmuniz.com/junk2jive-server/internal/services" +) + +type TextPromptRequest struct { + Query string `json:"query"` +} + +type DIYResponse struct { + Prompt string `json:"prompt"` + Result string `json:"result"` +} + +func TextPromptHandler(cfg *config.Config) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req TextPromptRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + openAI := services.NewOpenAIService(cfg.OpenAIKey) + result, err := openAI.GenerateDIY(req.Query) + if err != nil { + http.Error(w, "Failed to generate DIY ideas", http.StatusInternalServerError) + return + } + + response := DIYResponse{ + Prompt: "AI Suggestions for " + req.Query + " are:", + Result: result, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + } +} \ No newline at end of file diff --git a/internal/handlers/visual.go b/internal/handlers/visual.go new file mode 100644 index 0000000..9ef04d3 --- /dev/null +++ b/internal/handlers/visual.go @@ -0,0 +1,72 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + + "gitea.miguelmuniz.com/junk2jive-server/internal/config" + "gitea.miguelmuniz.com/junk2jive-server/internal/services" +) + +func VisualAIHandler(cfg *config.Config) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Parse multipart form with 10MB limit + if err := r.ParseMultipartForm(10 << 20); err != nil { + http.Error(w, "Unable to parse form", http.StatusBadRequest) + return + } + + file, handler, err := r.FormFile("file") + if err != nil { + http.Error(w, "Error retrieving the file", http.StatusBadRequest) + return + } + defer file.Close() + + // Create temporary file + tempFile, err := os.CreateTemp("", "upload-*"+filepath.Ext(handler.Filename)) + if err != nil { + http.Error(w, "Error creating temporary file", http.StatusInternalServerError) + return + } + defer os.Remove(tempFile.Name()) + defer tempFile.Close() + + // Copy uploaded file to temp file + if _, err = io.Copy(tempFile, file); err != nil { + http.Error(w, "Error saving the file", http.StatusInternalServerError) + return + } + + // Process with Roboflow + roboflowService := services.NewRoboflowService(cfg.RoboflowKey) + detectedObjects, err := roboflowService.DetectObjects(tempFile.Name()) + if err != nil { + http.Error(w, "Error detecting objects", http.StatusInternalServerError) + return + } + + // Generate DIY ideas based on detected objects + openAI := services.NewOpenAIService(cfg.OpenAIKey) + query := strings.Join(detectedObjects, ", ") + result, err := openAI.GenerateDIY(query) + if err != nil { + http.Error(w, "Failed to generate DIY ideas", http.StatusInternalServerError) + return + } + + // Prepare response + response := DIYResponse{ + Prompt: fmt.Sprintf("AI Suggestions for %s are:", query), + Result: result, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + } +} \ No newline at end of file diff --git a/internal/routes/routes.go b/internal/routes/routes.go new file mode 100644 index 0000000..07e349b --- /dev/null +++ b/internal/routes/routes.go @@ -0,0 +1,38 @@ +package routes + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/cors" + "gitea.miguelmuniz.com/junk2jive-server/internal/config" + "gitea.miguelmuniz.com/junk2jive-server/internal/handlers" +) + +func SetupRoutes(cfg *config.Config) *chi.Mux { + r := chi.NewRouter() + + // Middleware + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + r.Use(cors.Handler(cors.Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET", "POST", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Content-Type"}, + AllowCredentials: true, + })) + + // Static files + fileServer := http.FileServer(http.Dir("./static")) + r.Handle("/static/*", http.StripPrefix("/static", fileServer)) + + // API routes + r.Get("/", handlers.HomeHandler) + r.Route("/api", func(r chi.Router) { + r.Post("/text-prompt", handlers.TextPromptHandler(cfg)) + r.Post("/ai-prompt", handlers.VisualAIHandler(cfg)) + }) + + return r +} diff --git a/internal/services/openai.go b/internal/services/openai.go new file mode 100644 index 0000000..fce989f --- /dev/null +++ b/internal/services/openai.go @@ -0,0 +1,87 @@ +package services + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) + +type OpenAIService struct { + apiKey string +} + +type OpenAIRequest struct { + Model string `json:"model"` + Messages []Message `json:"messages"` + Temperature float64 `json:"temperature"` +} + +type Message struct { + Role string `json:"role"` + Content string `json:"content"` +} + +type OpenAIResponse struct { + Choices []struct { + Message struct { + Content string `json:"content"` + } `json:"message"` + } `json:"choices"` +} + +func NewOpenAIService(apiKey string) *OpenAIService { + return &OpenAIService{apiKey: apiKey} +} + +func (s *OpenAIService) GenerateDIY(query string) (string, error) { + url := "https://api.openai.com/v1/chat/completions" + + prompt := fmt.Sprintf("Generate creative DIY project ideas to repurpose or recycle the following: %s. Provide detailed instructions, materials needed, and steps to create each project.", query) + + requestBody := OpenAIRequest{ + Model: "gpt-4", // or "gpt-3.5-turbo" depending on your needs/access + Messages: []Message{ + { + Role: "system", + Content: "You are a creative DIY expert who helps people repurpose and recycle items into useful projects.", + }, + { + Role: "user", + Content: prompt, + }, + }, + Temperature: 0.7, + } + + jsonData, err := json.Marshal(requestBody) + if err != nil { + return "", err + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return "", err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.apiKey)) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + var response OpenAIResponse + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return "", err + } + + if len(response.Choices) == 0 { + return "", fmt.Errorf("no response from OpenAI") + } + + return response.Choices[0].Message.Content, nil +} \ No newline at end of file diff --git a/internal/services/robowflow.go b/internal/services/robowflow.go new file mode 100644 index 0000000..4d3c8e8 --- /dev/null +++ b/internal/services/robowflow.go @@ -0,0 +1,85 @@ +package services + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "os" + "path/filepath" +) + +type RoboflowService struct { + apiKey string +} + +type RoboflowResponse struct { + Predictions []struct { + Class string `json:"class"` + Confidence float64 `json:"confidence"` + } `json:"predictions"` +} + +func NewRoboflowService(apiKey string) *RoboflowService { + return &RoboflowService{apiKey: apiKey} +} + +func (s *RoboflowService) DetectObjects(imagePath string) ([]string, error) { + // Open the file + file, err := os.Open(imagePath) + if err != nil { + return nil, err + } + defer file.Close() + + // Create a new multipart writer + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + // Create a form file field + part, err := writer.CreateFormFile("file", filepath.Base(imagePath)) + if err != nil { + return nil, err + } + + // Copy the file content to the form field + if _, err = io.Copy(part, file); err != nil { + return nil, err + } + + // Close the writer to finalize the form + writer.Close() + + // Create the request + url := fmt.Sprintf("https://detect.roboflow.com/taco-puuof/1?api_key=%s&confidence=60&overlap=30", s.apiKey) + req, err := http.NewRequest("POST", url, body) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", writer.FormDataContentType()) + + // Send the request + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Parse the response + var response RoboflowResponse + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return nil, err + } + + // Extract object classes + var objects []string + for _, prediction := range response.Predictions { + objects = append(objects, prediction.Class) + } + + return objects, nil +} \ No newline at end of file