rewrite into go and nextjs

This commit is contained in:
2025-05-06 13:31:09 -07:00
commit e7b1090a85
14 changed files with 494 additions and 0 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
junk2jive-server
.env
.direnv

15
Makefile Normal file
View File

@@ -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

58
cmd/junk2jive/main.go Normal file
View File

@@ -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)
}
}

25
flake.lock generated Normal file
View File

@@ -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
}

38
flake.nix Normal file
View File

@@ -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
];
};
});
};
}

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module gitea.miguelmuniz.com/rogueking/junk2jive-server
go 1.24.2

26
internal/config/config.go Normal file
View File

@@ -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
}

View File

43
internal/handlers/text.go Normal file
View File

@@ -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)
}
}

View File

@@ -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)
}
}

38
internal/routes/routes.go Normal file
View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}