From 24ef31e00f8435c28d9cafe26a3ced602710c600 Mon Sep 17 00:00:00 2001 From: John Costa Date: Mon, 24 Feb 2025 18:59:38 +0000 Subject: [PATCH 01/30] feat: parsing response from open ai bruh --- backend/openai.go | 84 +++++++++++++++++++++++++++++++++++++++++- backend/openai_test.go | 66 ++++++++++++++++++++++++++++++++- 2 files changed, 146 insertions(+), 4 deletions(-) diff --git a/backend/openai.go b/backend/openai.go index 1bcd3aa..91ee9c4 100644 --- a/backend/openai.go +++ b/backend/openai.go @@ -210,10 +210,14 @@ func (client OpenAiClient) getRequest(body []byte) (*http.Request, error) { return req, nil } -func getCompletionsForImage(model string, temperature float64, prompt, imageName string, imageData []byte) (OpenAiRequestBody, error) { +func getCompletionsForImage(model string, temperature float64, prompt string, imageName string, jsonSchema string, imageData []byte) (OpenAiRequestBody, error) { request := OpenAiRequestBody{ Model: model, Temperature: temperature, + ResponseFormat: ResponseFormat{ + Type: "json_schema", + JsonSchema: jsonSchema, + }, } // TODO: Add build pattern here that deals with errors in some internal state? @@ -231,8 +235,84 @@ func getCompletionsForImage(model string, temperature float64, prompt, imageName return request, nil } +// { +// "id": "chatcmpl-B4XgiHcd7A2nyK7eyARdggSvfFuWQ", +// "object": "chat.completion", +// "created": 1740422508, +// "model": "gpt-4o-mini-2024-07-18", +// "choices": [ +// { +// "index": 0, +// "message": { +// "role": "assistant", +// "content": "{\"links\":[],\"tags\":[\"Git\",\"Programming\",\"Humor\",\"Meme\"],\"text\":[\"GIT FLOW\",\"JUST USE MAIN\",\"JUST USE MAIN\"]}", +// "refusal": null +// }, +// "logprobs": null, +// "finish_reason": "stop" +// } +// ], +// "usage": { +// "prompt_tokens": 775, +// "completion_tokens": 33, +// "total_tokens": 808, +// "prompt_tokens_details": { +// "cached_tokens": 0, +// "audio_tokens": 0 +// }, +// "completion_tokens_details": { +// "reasoning_tokens": 0, +// "audio_tokens": 0, +// "accepted_prediction_tokens": 0, +// "rejected_prediction_tokens": 0 +// } +// }, +// "service_tier": "default", +// "system_fingerprint": "fp_7fcd609668" +// } + +type ResponseChoiceMessage struct { + Role string `json:"role"` + Content string `json:"content"` +} + +type ResponseChoice struct { + Index int `json:"index"` + Message ResponseChoiceMessage `json:"message"` + FinishReason string `json:"finish_reason"` +} + +type OpenAiResponse struct { + Id string `json:"id"` + Object string `json:"object"` + Choices []ResponseChoice `json:"choices"` + Created int `json:"created"` +} + +// TODO: add usage parsing +func parseOpenAiResponse(jsonResponse []byte) (ImageInfo, error) { + response := OpenAiResponse{} + + err := json.Unmarshal(jsonResponse, &response) + if err != nil { + return ImageInfo{}, err + } + + if len(response.Choices) != 1 { + return ImageInfo{}, errors.New("Expected exactly one choice.") + } + + imageInfo := ImageInfo{} + err = json.Unmarshal([]byte(response.Choices[0].Message.Content), &imageInfo) + if err != nil { + return ImageInfo{}, errors.New("Could not parse content into image type.") + } + + return imageInfo, nil +} + func (client OpenAiClient) GetImageInfo(imageName string, imageData []byte) (ImageInfo, error) { - aiRequest, err := getCompletionsForImage("gpt-4o-mini", 1.0, client.systemPrompt, imageName, imageData) + aiRequest, err := getCompletionsForImage("gpt-4o-mini", 1.0, client.systemPrompt, imageName, RESPONSE_FORMAT, imageData) if err != nil { return ImageInfo{}, err } diff --git a/backend/openai_test.go b/backend/openai_test.go index 3a2f663..945f507 100644 --- a/backend/openai_test.go +++ b/backend/openai_test.go @@ -89,7 +89,7 @@ func TestMessageBuilderImage(t *testing.T) { } func TestFullImageRequest(t *testing.T) { - request, err := getCompletionsForImage("model", 0.1, "You are an assistant", "image.png", []byte("some data")) + request, err := getCompletionsForImage("model", 0.1, "You are an assistant", "image.png", "", []byte("some data")) if err != nil { t.Log(request) t.FailNow() @@ -101,7 +101,7 @@ func TestFullImageRequest(t *testing.T) { t.FailNow() } - expectedJson := `{"model":"model","temperature":0.1,"response_format":{"type":"","json_schema":""},"messages":[{"role":"system","content":"You are an assistant"},{"role":"user","content":[{"type":"image_url","image_url":{"url":"data:image/png;base64,c29tZSBkYXRh"}}]}]}` + expectedJson := `{"model":"model","temperature":0.1,"response_format":{"type":"json_schema","json_schema":""},"messages":[{"role":"system","content":"You are an assistant"},{"role":"user","content":[{"type":"image_url","image_url":{"url":"data:image/png;base64,c29tZSBkYXRh"}}]}]}` if string(jsonData) != expectedJson { t.Logf("Expected:\n%s\n Got:\n%s\n", expectedJson, string(jsonData)) @@ -149,3 +149,65 @@ func TestResponse(t *testing.T) { t.FailNow() } } + +func TestResponseParsing(t *testing.T) { + response := `{ + "id": "chatcmpl-B4XgiHcd7A2nyK7eyARdggSvfFuWQ", + "object": "chat.completion", + "created": 1740422508, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "{\"links\":[\"link\"],\"tags\":[\"tag\"],\"text\":[\"text\"]}", + "refusal": null + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 775, + "completion_tokens": 33, + "total_tokens": 808, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_7fcd609668" +}` + + imageParsed, err := parseOpenAiResponse([]byte(response)) + if err != nil { + t.Log(err) + t.FailNow() + } + + if len(imageParsed.Links) != 1 || imageParsed.Links[0] != "link" { + t.Log(imageParsed) + t.Log("Should have one link called 'link'.") + t.FailNow() + } + + if len(imageParsed.Tags) != 1 || imageParsed.Tags[0] != "tag" { + t.Log(imageParsed) + t.Log("Should have one tag called 'tag'.") + t.FailNow() + } + + if len(imageParsed.Text) != 1 || imageParsed.Text[0] != "text" { + t.Log(imageParsed) + t.Log("Should have one text called 'text'.") + t.FailNow() + } +} From 46e4043994a53bbc7fb68b905c973593c78bf5e8 Mon Sep 17 00:00:00 2001 From: John Costa Date: Mon, 24 Feb 2025 18:59:42 +0000 Subject: [PATCH 02/30] chore: updating gitignore fix --- backend/.gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 backend/.gitignore diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..0795065 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,2 @@ +screenmark +.env From 43092fa4f50c5be7c622d97000280c499cfb8799 Mon Sep 17 00:00:00 2001 From: John Costa Date: Mon, 24 Feb 2025 19:05:11 +0000 Subject: [PATCH 03/30] feat: using parsing on response --- backend/openai.go | 47 +---------------------------------------------- 1 file changed, 1 insertion(+), 46 deletions(-) diff --git a/backend/openai.go b/backend/openai.go index 91ee9c4..01ce639 100644 --- a/backend/openai.go +++ b/backend/openai.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "io" - "log" "net/http" "os" "path/filepath" @@ -235,42 +234,6 @@ func getCompletionsForImage(model string, temperature float64, prompt string, im return request, nil } -// { -// "id": "chatcmpl-B4XgiHcd7A2nyK7eyARdggSvfFuWQ", -// "object": "chat.completion", -// "created": 1740422508, -// "model": "gpt-4o-mini-2024-07-18", -// "choices": [ -// { -// "index": 0, -// "message": { -// "role": "assistant", -// "content": "{\"links\":[],\"tags\":[\"Git\",\"Programming\",\"Humor\",\"Meme\"],\"text\":[\"GIT FLOW\",\"JUST USE MAIN\",\"JUST USE MAIN\"]}", -// "refusal": null -// }, -// "logprobs": null, -// "finish_reason": "stop" -// } -// ], -// "usage": { -// "prompt_tokens": 775, -// "completion_tokens": 33, -// "total_tokens": 808, -// "prompt_tokens_details": { -// "cached_tokens": 0, -// "audio_tokens": 0 -// }, -// "completion_tokens_details": { -// "reasoning_tokens": 0, -// "audio_tokens": 0, -// "accepted_prediction_tokens": 0, -// "rejected_prediction_tokens": 0 -// } -// }, -// "service_tier": "default", -// "system_fingerprint": "fp_7fcd609668" -// } - type ResponseChoiceMessage struct { Role string `json:"role"` Content string `json:"content"` @@ -348,13 +311,5 @@ func (client OpenAiClient) GetImageInfo(imageName string, imageData []byte) (Ima return ImageInfo{}, err } - info := ImageInfo{} - err = json.Unmarshal(response, &info) - if err != nil { - return ImageInfo{}, err - } - - log.Println(string(response)) - - return info, nil + return parseOpenAiResponse(response) } From 2115da85b56c7008e738e83d9bbb6e893d675fcf Mon Sep 17 00:00:00 2001 From: John Costa Date: Mon, 24 Feb 2025 19:44:19 +0000 Subject: [PATCH 04/30] feat: working docker image and compose file --- backend/.gitignore | 1 + backend/Dockerfile | 15 +++++++++++++++ backend/docker-compose.yml | 27 +++++++++++++++++++++++++++ backend/main.go | 5 +++-- backend/models/database.go | 12 +++++++++--- backend/models/image.go | 6 +++++- backend/schema.sql | 9 ++++++++- 7 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 backend/Dockerfile create mode 100644 backend/docker-compose.yml diff --git a/backend/.gitignore b/backend/.gitignore index 0795065..fcbf23e 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,2 +1,3 @@ screenmark +.env.docker .env diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..1ab2a73 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,15 @@ +FROM golang + +WORKDIR /app + +# Dependency management +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN go build -o /app/haystack + +EXPOSE 3040 + +CMD ["/app/haystack"] diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..d8cd091 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,27 @@ +services: + database: + image: postgres + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + volumes: + - ./schema.sql:/docker-entrypoint-initdb.d/schema.sql + ports: + - 4321:5432 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d haystack_db"] + interval: 10s + retries: 5 + start_period: 5s + timeout: 5s + backend: + build: . + restart: always + env_file: .env.docker + ports: + - 3040:3040 + depends_on: + database: + condition: service_healthy + restart: true diff --git a/backend/main.go b/backend/main.go index 6d472b1..51bb0f2 100644 --- a/backend/main.go +++ b/backend/main.go @@ -5,6 +5,7 @@ import ( "io" "log" "net/http" + "os" "screenmark/screenmark/models" "time" @@ -23,7 +24,7 @@ func main() { panic(err) } - listener := pq.NewListener(models.CONNECTION, time.Second, time.Second, func(event pq.ListenerEventType, err error) { + listener := pq.NewListener(os.Getenv("DB_CONNECTION"), time.Second, time.Second, func(event pq.ListenerEventType, err error) { if err != nil { panic(err) } @@ -103,7 +104,7 @@ func main() { } }) - log.Println("Listening and serving.") + log.Println("Listening and serving on port 3040.") http.ListenAndServe(":3040", mux) } diff --git a/backend/models/database.go b/backend/models/database.go index 87edbb8..fb6f08b 100644 --- a/backend/models/database.go +++ b/backend/models/database.go @@ -2,16 +2,22 @@ package models import ( "database/sql" + "errors" + "os" _ "github.com/lib/pq" ) -const CONNECTION = "postgresql://localhost:5432/haystack?sslmode=disable" - var db *sql.DB func InitDatabase() error { - database, err := sql.Open("postgres", CONNECTION) + connection := os.Getenv("DB_CONNECTION") + + if len(connection) == 0 { + return errors.New("DB_CONNECTION env was not found.") + } + + database, err := sql.Open("postgres", connection) db = database diff --git a/backend/models/image.go b/backend/models/image.go index 39d91e9..891ec67 100644 --- a/backend/models/image.go +++ b/backend/models/image.go @@ -3,11 +3,13 @@ package models import ( "errors" "fmt" + "log" - . "github.com/go-jet/jet/v2/postgres" "screenmark/screenmark/.gen/haystack/haystack/model" . "screenmark/screenmark/.gen/haystack/haystack/table" + . "github.com/go-jet/jet/v2/postgres" + "github.com/google/uuid" ) @@ -22,6 +24,8 @@ func GetImage(imageId string) (model.UserImages, error) { id := uuid.MustParse(imageId) stmt := UserImages.SELECT(UserImages.ImageName, UserImages.Image).WHERE(UserImages.ID.EQ(UUID(id))) + log.Println(stmt.DebugSql()) + images := []model.UserImages{} err := stmt.Query(db, &images) diff --git a/backend/schema.sql b/backend/schema.sql index edbd83d..91dabf3 100644 --- a/backend/schema.sql +++ b/backend/schema.sql @@ -1,3 +1,6 @@ +DROP DATABASE IF EXISTS haystack_db; +CREATE DATABASE haystack_db; + DROP SCHEMA IF EXISTS haystack CASCADE; CREATE SCHEMA haystack; @@ -53,4 +56,8 @@ $$ LANGUAGE plpgsql; CREATE OR REPLACE TRIGGER on_new_image AFTER INSERT ON haystack.user_images FOR EACH ROW -EXECUTE PROCEDURE notify_new_image() +EXECUTE PROCEDURE notify_new_image(); + +/* -----| Test Data |----- */ + +INSERT INTO haystack.users VALUES ('fcc22dbb-7792-4595-be8e-d0439e13990a'); From f49589907a36d988edff71743f750148ed315a1c Mon Sep 17 00:00:00 2001 From: John Costa Date: Mon, 24 Feb 2025 20:02:54 +0000 Subject: [PATCH 05/30] feat: instructions for docker compose --- backend/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 backend/README.md diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..b71a08d --- /dev/null +++ b/backend/README.md @@ -0,0 +1,10 @@ +# Running the backend + +1. Create a `.env.docker` file, which must contain the following. + +OPENAI_API_KEY=openai_key +DB_CONNECTION=postgresql://postgres:password@database:5432/haystack_db?sslmode=disable + +2. Use `docker-compose up` to spin up the containers. + +You should be able to access the backend through port `3040` From 64f6bde6a9094f45d9b1814ca9c236a8d3716360 Mon Sep 17 00:00:00 2001 From: John Costa Date: Mon, 24 Feb 2025 20:02:58 +0000 Subject: [PATCH 06/30] feat: methods to get image --- backend/main.go | 37 ++++++++++++++++++++++++++++++++++++- backend/models/image.go | 16 +++++++--------- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/backend/main.go b/backend/main.go index 51bb0f2..ef5f2e9 100644 --- a/backend/main.go +++ b/backend/main.go @@ -1,11 +1,13 @@ package main import ( + "encoding/json" "fmt" "io" "log" "net/http" "os" + "path/filepath" "screenmark/screenmark/models" "time" @@ -72,6 +74,26 @@ func main() { w.Header().Add("Access-Control-Allow-Headers", "*") }) + mux.HandleFunc("GET /image/{id}", func(w http.ResponseWriter, r *http.Request) { + imageId := r.PathValue("id") + + // TODO: really need authorization here! + image, err := models.GetImage(imageId) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, "Could not get image") + return + } + + // TODO: this could be part of the db table + extension := filepath.Ext(image.ImageName) + extension = extension[1:] + + w.Header().Add("Content-Type", "image/"+extension) + w.Write(image.Image) + }) + mux.HandleFunc("POST /image/{name}", func(w http.ResponseWriter, r *http.Request) { imageName := r.PathValue("name") @@ -95,13 +117,26 @@ func main() { return } - err = models.SaveImage(userId, imageName, image) + userImage, err := models.SaveImage(userId, imageName, image) if err != nil { log.Println(err) w.WriteHeader(http.StatusBadRequest) fmt.Fprintf(w, "Could not save image to DB") return } + + jsonUserImage, err := json.Marshal(userImage) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Could not create JSON response for this image") + return + } + + fmt.Fprint(w, string(jsonUserImage)) + w.Header().Add("Content-Type", "application/json") + + w.WriteHeader(http.StatusCreated) }) log.Println("Listening and serving on port 3040.") diff --git a/backend/models/image.go b/backend/models/image.go index 891ec67..148d2dc 100644 --- a/backend/models/image.go +++ b/backend/models/image.go @@ -3,8 +3,6 @@ package models import ( "errors" "fmt" - "log" - "screenmark/screenmark/.gen/haystack/haystack/model" . "screenmark/screenmark/.gen/haystack/haystack/table" @@ -13,18 +11,18 @@ import ( "github.com/google/uuid" ) -func SaveImage(userId string, imageName string, imageData []byte) error { - stmt := UserImages.INSERT(UserImages.UserID, UserImages.ImageName, UserImages.Image).VALUES(userId, imageName, imageData) - _, err := stmt.Exec(db) +func SaveImage(userId string, imageName string, imageData []byte) (model.UserImages, error) { + stmt := UserImages.INSERT(UserImages.UserID, UserImages.ImageName, UserImages.Image).VALUES(userId, imageName, imageData).RETURNING(UserImages.ID, UserImages.UserID, UserImages.ImageName) - return err + userImage := model.UserImages{} + err := stmt.Query(db, &userImage) + + return userImage, err } func GetImage(imageId string) (model.UserImages, error) { id := uuid.MustParse(imageId) - stmt := UserImages.SELECT(UserImages.ImageName, UserImages.Image).WHERE(UserImages.ID.EQ(UUID(id))) - - log.Println(stmt.DebugSql()) + stmt := UserImages.SELECT(UserImages.AllColumns).WHERE(UserImages.ID.EQ(UUID(id))) images := []model.UserImages{} err := stmt.Query(db, &images) From ee0587a16b3418f3bdfc760ee144359ab1954bf9 Mon Sep 17 00:00:00 2001 From: John Costa Date: Mon, 24 Feb 2025 20:05:56 +0000 Subject: [PATCH 07/30] feat: method for getting images --- backend/main.go | 22 ++++++++++++++++++++++ backend/models/image.go | 10 ++++++++++ 2 files changed, 32 insertions(+) diff --git a/backend/main.go b/backend/main.go index ef5f2e9..7ad91e1 100644 --- a/backend/main.go +++ b/backend/main.go @@ -74,6 +74,28 @@ func main() { w.Header().Add("Access-Control-Allow-Headers", "*") }) + mux.HandleFunc("GET /image", func(w http.ResponseWriter, r *http.Request) { + userId := r.Header.Get("userId") + + images, err := models.GetUserImages(userId) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, "Something went wrong") + return + } + + jsonImages, err := json.Marshal(images) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Could not create JSON response for this image") + return + } + + w.Write(jsonImages) + }) + mux.HandleFunc("GET /image/{id}", func(w http.ResponseWriter, r *http.Request) { imageId := r.PathValue("id") diff --git a/backend/models/image.go b/backend/models/image.go index 148d2dc..9fed339 100644 --- a/backend/models/image.go +++ b/backend/models/image.go @@ -33,3 +33,13 @@ func GetImage(imageId string) (model.UserImages, error) { return images[0], err } + +func GetUserImages(userId string) ([]model.UserImages, error) { + id := uuid.MustParse(userId) + stmt := UserImages.SELECT(UserImages.ID, UserImages.ImageName).WHERE(UserImages.UserID.EQ(UUID(id))) + + images := []model.UserImages{} + err := stmt.Query(db, &images) + + return images, err +} From b99432c20239d0c78994bb372c457aefb79f93ac Mon Sep 17 00:00:00 2001 From: John Costa Date: Mon, 24 Feb 2025 21:00:05 +0000 Subject: [PATCH 08/30] feat: saving AI information to database --- .../haystack/haystack/model/image_tags.go | 3 +- .../.gen/haystack/haystack/model/user_tags.go | 18 ----- .../haystack/haystack/table/image_tags.go | 13 +-- .../haystack/table/table_use_schema.go | 1 - .../.gen/haystack/haystack/table/user_tags.go | 81 ------------------- backend/main.go | 10 ++- backend/models/image.go | 63 ++++++++++++++- backend/schema.sql | 9 +-- 8 files changed, 80 insertions(+), 118 deletions(-) delete mode 100644 backend/.gen/haystack/haystack/model/user_tags.go delete mode 100644 backend/.gen/haystack/haystack/table/user_tags.go diff --git a/backend/.gen/haystack/haystack/model/image_tags.go b/backend/.gen/haystack/haystack/model/image_tags.go index a3c666c..9fb990b 100644 --- a/backend/.gen/haystack/haystack/model/image_tags.go +++ b/backend/.gen/haystack/haystack/model/image_tags.go @@ -12,6 +12,7 @@ import ( ) type ImageTags struct { - TagID uuid.UUID + ID uuid.UUID `sql:"primary_key"` + Tag string ImageID uuid.UUID } diff --git a/backend/.gen/haystack/haystack/model/user_tags.go b/backend/.gen/haystack/haystack/model/user_tags.go deleted file mode 100644 index e2d12d7..0000000 --- a/backend/.gen/haystack/haystack/model/user_tags.go +++ /dev/null @@ -1,18 +0,0 @@ -// -// Code generated by go-jet DO NOT EDIT. -// -// WARNING: Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated -// - -package model - -import ( - "github.com/google/uuid" -) - -type UserTags struct { - ID uuid.UUID `sql:"primary_key"` - Tag string - UserID uuid.UUID -} diff --git a/backend/.gen/haystack/haystack/table/image_tags.go b/backend/.gen/haystack/haystack/table/image_tags.go index 9aaa4f8..7074850 100644 --- a/backend/.gen/haystack/haystack/table/image_tags.go +++ b/backend/.gen/haystack/haystack/table/image_tags.go @@ -17,7 +17,8 @@ type imageTagsTable struct { postgres.Table // Columns - TagID postgres.ColumnString + ID postgres.ColumnString + Tag postgres.ColumnString ImageID postgres.ColumnString AllColumns postgres.ColumnList @@ -59,17 +60,19 @@ func newImageTagsTable(schemaName, tableName, alias string) *ImageTagsTable { func newImageTagsTableImpl(schemaName, tableName, alias string) imageTagsTable { var ( - TagIDColumn = postgres.StringColumn("tag_id") + IDColumn = postgres.StringColumn("id") + TagColumn = postgres.StringColumn("tag") ImageIDColumn = postgres.StringColumn("image_id") - allColumns = postgres.ColumnList{TagIDColumn, ImageIDColumn} - mutableColumns = postgres.ColumnList{TagIDColumn, ImageIDColumn} + allColumns = postgres.ColumnList{IDColumn, TagColumn, ImageIDColumn} + mutableColumns = postgres.ColumnList{TagColumn, ImageIDColumn} ) return imageTagsTable{ Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), //Columns - TagID: TagIDColumn, + ID: IDColumn, + Tag: TagColumn, ImageID: ImageIDColumn, AllColumns: allColumns, diff --git a/backend/.gen/haystack/haystack/table/table_use_schema.go b/backend/.gen/haystack/haystack/table/table_use_schema.go index f7f65cd..4826795 100644 --- a/backend/.gen/haystack/haystack/table/table_use_schema.go +++ b/backend/.gen/haystack/haystack/table/table_use_schema.go @@ -14,6 +14,5 @@ func UseSchema(schema string) { ImageTags = ImageTags.FromSchema(schema) ImageText = ImageText.FromSchema(schema) UserImages = UserImages.FromSchema(schema) - UserTags = UserTags.FromSchema(schema) Users = Users.FromSchema(schema) } diff --git a/backend/.gen/haystack/haystack/table/user_tags.go b/backend/.gen/haystack/haystack/table/user_tags.go deleted file mode 100644 index 99dfc9e..0000000 --- a/backend/.gen/haystack/haystack/table/user_tags.go +++ /dev/null @@ -1,81 +0,0 @@ -// -// Code generated by go-jet DO NOT EDIT. -// -// WARNING: Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated -// - -package table - -import ( - "github.com/go-jet/jet/v2/postgres" -) - -var UserTags = newUserTagsTable("haystack", "user_tags", "") - -type userTagsTable struct { - postgres.Table - - // Columns - ID postgres.ColumnString - Tag postgres.ColumnString - UserID postgres.ColumnString - - AllColumns postgres.ColumnList - MutableColumns postgres.ColumnList -} - -type UserTagsTable struct { - userTagsTable - - EXCLUDED userTagsTable -} - -// AS creates new UserTagsTable with assigned alias -func (a UserTagsTable) AS(alias string) *UserTagsTable { - return newUserTagsTable(a.SchemaName(), a.TableName(), alias) -} - -// Schema creates new UserTagsTable with assigned schema name -func (a UserTagsTable) FromSchema(schemaName string) *UserTagsTable { - return newUserTagsTable(schemaName, a.TableName(), a.Alias()) -} - -// WithPrefix creates new UserTagsTable with assigned table prefix -func (a UserTagsTable) WithPrefix(prefix string) *UserTagsTable { - return newUserTagsTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) -} - -// WithSuffix creates new UserTagsTable with assigned table suffix -func (a UserTagsTable) WithSuffix(suffix string) *UserTagsTable { - return newUserTagsTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) -} - -func newUserTagsTable(schemaName, tableName, alias string) *UserTagsTable { - return &UserTagsTable{ - userTagsTable: newUserTagsTableImpl(schemaName, tableName, alias), - EXCLUDED: newUserTagsTableImpl("", "excluded", ""), - } -} - -func newUserTagsTableImpl(schemaName, tableName, alias string) userTagsTable { - var ( - IDColumn = postgres.StringColumn("id") - TagColumn = postgres.StringColumn("tag") - UserIDColumn = postgres.StringColumn("user_id") - allColumns = postgres.ColumnList{IDColumn, TagColumn, UserIDColumn} - mutableColumns = postgres.ColumnList{TagColumn, UserIDColumn} - ) - - return userTagsTable{ - Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), - - //Columns - ID: IDColumn, - Tag: TagColumn, - UserID: UserIDColumn, - - AllColumns: allColumns, - MutableColumns: mutableColumns, - } -} diff --git a/backend/main.go b/backend/main.go index 7ad91e1..a6cd088 100644 --- a/backend/main.go +++ b/backend/main.go @@ -61,7 +61,11 @@ func main() { return } - log.Printf("Info: %+v\n", imageInfo) + log.Println("Finished processing image " + parameters.Extra) + + models.SaveImageTags(parameters.Extra, imageInfo.Tags) + models.SaveImageLinks(parameters.Extra, imageInfo.Links) + models.SaveImageTexts(parameters.Extra, imageInfo.Tags) }() } }() @@ -155,10 +159,10 @@ func main() { return } + w.WriteHeader(http.StatusCreated) + fmt.Fprint(w, string(jsonUserImage)) w.Header().Add("Content-Type", "application/json") - - w.WriteHeader(http.StatusCreated) }) log.Println("Listening and serving on port 3040.") diff --git a/backend/models/image.go b/backend/models/image.go index 9fed339..5366b97 100644 --- a/backend/models/image.go +++ b/backend/models/image.go @@ -34,12 +34,71 @@ func GetImage(imageId string) (model.UserImages, error) { return images[0], err } -func GetUserImages(userId string) ([]model.UserImages, error) { +type UserImagesWithInfo struct { + model.UserImages + + Tags []model.ImageTags + Links []model.ImageLinks + Text []model.ImageText +} + +func GetUserImages(userId string) ([]UserImagesWithInfo, error) { id := uuid.MustParse(userId) stmt := UserImages.SELECT(UserImages.ID, UserImages.ImageName).WHERE(UserImages.UserID.EQ(UUID(id))) - images := []model.UserImages{} + images := []UserImagesWithInfo{} err := stmt.Query(db, &images) return images, err } + +func SaveImageTags(imageId string, tags []string) ([]model.ImageTags, error) { + id := uuid.MustParse(imageId) + + stmt := ImageTags.INSERT(ImageTags.ImageID, ImageTags.Tag) + + for _, t := range tags { + stmt = stmt.VALUES(id, t) + } + + stmt.RETURNING(ImageTags.AllColumns) + + imageTags := []model.ImageTags{} + err := stmt.Query(db, &imageTags) + + return imageTags, err +} + +func SaveImageLinks(imageId string, links []string) ([]model.ImageLinks, error) { + id := uuid.MustParse(imageId) + + stmt := ImageTags.INSERT(ImageLinks.ImageID, ImageLinks.Link) + + for _, t := range links { + stmt = stmt.VALUES(id, t) + } + + stmt.RETURNING(ImageLinks.AllColumns) + + imageLinks := []model.ImageLinks{} + err := stmt.Query(db, &imageLinks) + + return imageLinks, err +} + +func SaveImageTexts(imageId string, texts []string) ([]model.ImageText, error) { + id := uuid.MustParse(imageId) + + stmt := ImageTags.INSERT(ImageText.ImageID, ImageText.ImageText) + + for _, t := range texts { + stmt = stmt.VALUES(id, t) + } + + stmt.RETURNING(ImageText.AllColumns) + + imageTags := []model.ImageText{} + err := stmt.Query(db, &imageTags) + + return imageTags, err +} diff --git a/backend/schema.sql b/backend/schema.sql index 91dabf3..9d366e8 100644 --- a/backend/schema.sql +++ b/backend/schema.sql @@ -18,14 +18,9 @@ CREATE TABLE haystack.user_images ( user_id uuid NOT NULL REFERENCES haystack.users (id) ); -CREATE TABLE haystack.user_tags ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - tag TEXT NOT NULL, - user_id uuid NOT NULL REFERENCES haystack.users (id) -); - CREATE TABLE haystack.image_tags ( - tag_id UUID NOT NULL REFERENCES haystack.user_tags (id), + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + tag TEXT NOT NULL, image_id UUID NOT NULL REFERENCES haystack.user_images (id) ); From 13ebd80ce97d1d63295221109042ca42eff0dfcb Mon Sep 17 00:00:00 2001 From: John Costa Date: Mon, 24 Feb 2025 21:04:25 +0000 Subject: [PATCH 09/30] chore: documentation --- backend/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/backend/README.md b/backend/README.md index b71a08d..8619de6 100644 --- a/backend/README.md +++ b/backend/README.md @@ -8,3 +8,19 @@ DB_CONNECTION=postgresql://postgres:password@database:5432/haystack_db?sslmode=d 2. Use `docker-compose up` to spin up the containers. You should be able to access the backend through port `3040` + +# Methods + +For now, we cheat and add a `userId` header which if os type `UUID`. Use the auto generated test one (fcc22dbb-7792-4595-be8e-d0439e13990a). + +- `GET /image` | Returns all of the users image, including tags, links and text any image contains. +- `GET /image/{imageId}` | Returns the actual image, use this to display images in the UI. +- `POST /image/{imageNameWithExtension}` | Sends an image to the backend, saves it and sents it to open ai to later process. + +# Architecture + +1. The user posts an image, which gets saved on our database (all data, including images are saved on DB). +2. We listen for table event creation, and we can process this image by sending it to OpenAI. +3. After OpenAI responds, we write to the database. + +This means that for now, we don't have a notification system to tell the user when their image is done processing. But will do in the future. From 971f705288c4a7071414f13dfb285b2c7d95a1de Mon Sep 17 00:00:00 2001 From: John Costa Date: Mon, 24 Feb 2025 21:15:30 +0000 Subject: [PATCH 10/30] fix: actually returning all the user images fix --- backend/models/image.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/models/image.go b/backend/models/image.go index 5366b97..463f34a 100644 --- a/backend/models/image.go +++ b/backend/models/image.go @@ -44,7 +44,9 @@ type UserImagesWithInfo struct { func GetUserImages(userId string) ([]UserImagesWithInfo, error) { id := uuid.MustParse(userId) - stmt := UserImages.SELECT(UserImages.ID, UserImages.ImageName).WHERE(UserImages.UserID.EQ(UUID(id))) + stmt := SELECT(UserImages.ID, UserImages.ImageName, ImageTags.AllColumns, ImageText.AllColumns, ImageLinks.AllColumns).FROM(UserImages.LEFT_JOIN(ImageTags, ImageTags.ImageID.EQ(UserImages.ID)).LEFT_JOIN(ImageText, ImageText.ImageID.EQ(UserImages.ID)).LEFT_JOIN(ImageLinks, ImageLinks.ImageID.EQ(UserImages.ID))).WHERE(UserImages.UserID.EQ(UUID(id))) + + fmt.Println(stmt.DebugSql()) images := []UserImagesWithInfo{} err := stmt.Query(db, &images) From 5bec6c95909a4b3565878ffff29ee7844dc565c6 Mon Sep 17 00:00:00 2001 From: John Costa Date: Wed, 26 Feb 2025 15:54:27 +0000 Subject: [PATCH 11/30] fix: actually saving to the correct db table --- backend/models/image.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/models/image.go b/backend/models/image.go index 463f34a..733a607 100644 --- a/backend/models/image.go +++ b/backend/models/image.go @@ -91,7 +91,7 @@ func SaveImageLinks(imageId string, links []string) ([]model.ImageLinks, error) func SaveImageTexts(imageId string, texts []string) ([]model.ImageText, error) { id := uuid.MustParse(imageId) - stmt := ImageTags.INSERT(ImageText.ImageID, ImageText.ImageText) + stmt := ImageText.INSERT(ImageText.ImageID, ImageText.ImageText) for _, t := range texts { stmt = stmt.VALUES(id, t) From 410270e2178b8d99e2b6aff508f2a9c8c2fb23c2 Mon Sep 17 00:00:00 2001 From: John Costa Date: Wed, 26 Feb 2025 18:04:30 +0000 Subject: [PATCH 12/30] refactor: using chi router + bug fixes --- backend/go.mod | 1 + backend/go.sum | 2 + backend/main.go | 85 +++++++++++++++++++++++++++-------------- backend/models/image.go | 2 - backend/openai.go | 4 ++ 5 files changed, 63 insertions(+), 31 deletions(-) diff --git a/backend/go.mod b/backend/go.mod index c5dca0b..fcdc38a 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -4,6 +4,7 @@ go 1.24.0 require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-chi/chi/v5 v5.2.1 // indirect github.com/go-jet/jet/v2 v2.12.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/joho/godotenv v1.5.1 // indirect diff --git a/backend/go.sum b/backend/go.sum index 7ccdb24..ecd9db0 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,5 +1,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= +github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-jet/jet/v2 v2.12.0 h1:z2JfvBAZgsfxlQz6NXBYdZTXc7ep3jhbszTLtETv1JE= github.com/go-jet/jet/v2 v2.12.0/go.mod h1:ufQVRQeI1mbcO5R8uCEVcVf3Foej9kReBdwDx7YMWUM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= diff --git a/backend/main.go b/backend/main.go index a6cd088..7674819 100644 --- a/backend/main.go +++ b/backend/main.go @@ -11,16 +11,38 @@ import ( "screenmark/screenmark/models" "time" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" "github.com/joho/godotenv" "github.com/lib/pq" ) +type TestAiClient struct { + ImageInfo ImageInfo +} + +func (client TestAiClient) GetImageInfo(imageName string, imageData []byte) (ImageInfo, error) { + return client.ImageInfo, nil +} + +func GetAiClient() (AiClient, error) { + mode := os.Getenv("MODE") + if mode == "TESTING" { + return TestAiClient{}, nil + } + + return CreateOpenAiClient() +} + func main() { err := godotenv.Load() if err != nil { panic(err) } + mode := os.Getenv("MODE") + log.Printf("Mode: %s\n", mode) + err = models.InitDatabase() if err != nil { panic(err) @@ -39,46 +61,51 @@ func main() { panic(err) } - select { - case parameters := <-listener.Notify: - log.Println("received notification, new image available: " + parameters.Extra) + for { - go func() { - openAiClient, err := CreateOpenAiClient() - if err != nil { - panic(err) - } + select { + case parameters := <-listener.Notify: + log.Println("received notification, new image available: " + parameters.Extra) - image, err := models.GetImage(parameters.Extra) - if err != nil { - log.Println(err) - return - } + go func() { + openAiClient, err := GetAiClient() + if err != nil { + panic(err) + } - imageInfo, err := openAiClient.GetImageInfo(image.ImageName, image.Image) - if err != nil { - log.Println(err) - return - } + image, err := models.GetImage(parameters.Extra) + if err != nil { + log.Println(err) + return + } - log.Println("Finished processing image " + parameters.Extra) + imageInfo, err := openAiClient.GetImageInfo(image.ImageName, image.Image) + if err != nil { + log.Println(err) + return + } - models.SaveImageTags(parameters.Extra, imageInfo.Tags) - models.SaveImageLinks(parameters.Extra, imageInfo.Links) - models.SaveImageTexts(parameters.Extra, imageInfo.Tags) - }() + log.Println("Finished processing image " + parameters.Extra) + log.Printf("Image attributes: %+v\n", imageInfo) + + models.SaveImageTags(parameters.Extra, imageInfo.Tags) + models.SaveImageLinks(parameters.Extra, imageInfo.Links) + models.SaveImageTexts(parameters.Extra, imageInfo.Text) + }() + } } }() - mux := http.NewServeMux() + r := chi.NewRouter() + r.Use(middleware.Logger) - mux.HandleFunc("OPTIONS /image/{name}", func(w http.ResponseWriter, r *http.Request) { + r.Options("/*", func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Access-Control-Allow-Origin", "*") w.Header().Add("Access-Control-Allow-Credentials", "*") w.Header().Add("Access-Control-Allow-Headers", "*") }) - mux.HandleFunc("GET /image", func(w http.ResponseWriter, r *http.Request) { + r.Get("/image", func(w http.ResponseWriter, r *http.Request) { userId := r.Header.Get("userId") images, err := models.GetUserImages(userId) @@ -100,7 +127,7 @@ func main() { w.Write(jsonImages) }) - mux.HandleFunc("GET /image/{id}", func(w http.ResponseWriter, r *http.Request) { + r.Get("/image/{id}", func(w http.ResponseWriter, r *http.Request) { imageId := r.PathValue("id") // TODO: really need authorization here! @@ -120,7 +147,7 @@ func main() { w.Write(image.Image) }) - mux.HandleFunc("POST /image/{name}", func(w http.ResponseWriter, r *http.Request) { + r.Post("/image/{name}", func(w http.ResponseWriter, r *http.Request) { imageName := r.PathValue("name") userId := r.Header.Get("userId") @@ -167,5 +194,5 @@ func main() { log.Println("Listening and serving on port 3040.") - http.ListenAndServe(":3040", mux) + http.ListenAndServe(":3040", r) } diff --git a/backend/models/image.go b/backend/models/image.go index 733a607..9fdca5a 100644 --- a/backend/models/image.go +++ b/backend/models/image.go @@ -46,8 +46,6 @@ func GetUserImages(userId string) ([]UserImagesWithInfo, error) { id := uuid.MustParse(userId) stmt := SELECT(UserImages.ID, UserImages.ImageName, ImageTags.AllColumns, ImageText.AllColumns, ImageLinks.AllColumns).FROM(UserImages.LEFT_JOIN(ImageTags, ImageTags.ImageID.EQ(UserImages.ID)).LEFT_JOIN(ImageText, ImageText.ImageID.EQ(UserImages.ID)).LEFT_JOIN(ImageLinks, ImageLinks.ImageID.EQ(UserImages.ID))).WHERE(UserImages.UserID.EQ(UUID(id))) - fmt.Println(stmt.DebugSql()) - images := []UserImagesWithInfo{} err := stmt.Query(db, &images) diff --git a/backend/openai.go b/backend/openai.go index 01ce639..f7a114e 100644 --- a/backend/openai.go +++ b/backend/openai.go @@ -113,6 +113,10 @@ func (imageMessage OpenAiImage) ToJson() ([]byte, error) { return json.Marshal(imageMessage) } +type AiClient interface { + GetImageInfo(imageName string, imageData []byte) (ImageInfo, error) +} + type OpenAiClient struct { url string apiKey string From d8095b0c67860ce47d8218a325c2df401fa2f8ac Mon Sep 17 00:00:00 2001 From: John Costa Date: Wed, 26 Feb 2025 20:01:56 +0000 Subject: [PATCH 13/30] refactor: tables for `image` and `processing_image` This allows a single table to be used to process images, meaning if anything happens to the system we can always return to polling the database and process these images individually. Because of this we also want an `image` table to contain the actual binary data for the image, so we aren't selecting and writing it each time, as it is potentially a bottleneck. --- backend/.gen/haystack/haystack/model/image.go | 18 +++ .../haystack/haystack/model/user_images.go | 7 +- .../haystack/model/user_images_to_process.go | 18 +++ backend/.gen/haystack/haystack/table/image.go | 81 +++++++++++++ .../haystack/table/table_use_schema.go | 2 + .../haystack/haystack/table/user_images.go | 25 ++-- .../haystack/table/user_images_to_process.go | 81 +++++++++++++ backend/main.go | 42 +++++-- backend/models/image.go | 112 ++++++++++++++++-- backend/schema.sql | 17 ++- 10 files changed, 362 insertions(+), 41 deletions(-) create mode 100644 backend/.gen/haystack/haystack/model/image.go create mode 100644 backend/.gen/haystack/haystack/model/user_images_to_process.go create mode 100644 backend/.gen/haystack/haystack/table/image.go create mode 100644 backend/.gen/haystack/haystack/table/user_images_to_process.go diff --git a/backend/.gen/haystack/haystack/model/image.go b/backend/.gen/haystack/haystack/model/image.go new file mode 100644 index 0000000..e80c52a --- /dev/null +++ b/backend/.gen/haystack/haystack/model/image.go @@ -0,0 +1,18 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package model + +import ( + "github.com/google/uuid" +) + +type Image struct { + ID uuid.UUID `sql:"primary_key"` + ImageName string + Image []byte +} diff --git a/backend/.gen/haystack/haystack/model/user_images.go b/backend/.gen/haystack/haystack/model/user_images.go index 18c33ca..d6b4998 100644 --- a/backend/.gen/haystack/haystack/model/user_images.go +++ b/backend/.gen/haystack/haystack/model/user_images.go @@ -12,8 +12,7 @@ import ( ) type UserImages struct { - ID uuid.UUID `sql:"primary_key"` - ImageName string - Image []byte - UserID uuid.UUID + ID uuid.UUID `sql:"primary_key"` + ImageID uuid.UUID + UserID uuid.UUID } diff --git a/backend/.gen/haystack/haystack/model/user_images_to_process.go b/backend/.gen/haystack/haystack/model/user_images_to_process.go new file mode 100644 index 0000000..8b8ee4c --- /dev/null +++ b/backend/.gen/haystack/haystack/model/user_images_to_process.go @@ -0,0 +1,18 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package model + +import ( + "github.com/google/uuid" +) + +type UserImagesToProcess struct { + ID uuid.UUID `sql:"primary_key"` + ImageID uuid.UUID + UserID uuid.UUID +} diff --git a/backend/.gen/haystack/haystack/table/image.go b/backend/.gen/haystack/haystack/table/image.go new file mode 100644 index 0000000..9185561 --- /dev/null +++ b/backend/.gen/haystack/haystack/table/image.go @@ -0,0 +1,81 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package table + +import ( + "github.com/go-jet/jet/v2/postgres" +) + +var Image = newImageTable("haystack", "image", "") + +type imageTable struct { + postgres.Table + + // Columns + ID postgres.ColumnString + ImageName postgres.ColumnString + Image postgres.ColumnString + + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList +} + +type ImageTable struct { + imageTable + + EXCLUDED imageTable +} + +// AS creates new ImageTable with assigned alias +func (a ImageTable) AS(alias string) *ImageTable { + return newImageTable(a.SchemaName(), a.TableName(), alias) +} + +// Schema creates new ImageTable with assigned schema name +func (a ImageTable) FromSchema(schemaName string) *ImageTable { + return newImageTable(schemaName, a.TableName(), a.Alias()) +} + +// WithPrefix creates new ImageTable with assigned table prefix +func (a ImageTable) WithPrefix(prefix string) *ImageTable { + return newImageTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} + +// WithSuffix creates new ImageTable with assigned table suffix +func (a ImageTable) WithSuffix(suffix string) *ImageTable { + return newImageTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} + +func newImageTable(schemaName, tableName, alias string) *ImageTable { + return &ImageTable{ + imageTable: newImageTableImpl(schemaName, tableName, alias), + EXCLUDED: newImageTableImpl("", "excluded", ""), + } +} + +func newImageTableImpl(schemaName, tableName, alias string) imageTable { + var ( + IDColumn = postgres.StringColumn("id") + ImageNameColumn = postgres.StringColumn("image_name") + ImageColumn = postgres.StringColumn("image") + allColumns = postgres.ColumnList{IDColumn, ImageNameColumn, ImageColumn} + mutableColumns = postgres.ColumnList{ImageNameColumn, ImageColumn} + ) + + return imageTable{ + Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), + + //Columns + ID: IDColumn, + ImageName: ImageNameColumn, + Image: ImageColumn, + + AllColumns: allColumns, + MutableColumns: mutableColumns, + } +} diff --git a/backend/.gen/haystack/haystack/table/table_use_schema.go b/backend/.gen/haystack/haystack/table/table_use_schema.go index 4826795..72d718c 100644 --- a/backend/.gen/haystack/haystack/table/table_use_schema.go +++ b/backend/.gen/haystack/haystack/table/table_use_schema.go @@ -10,9 +10,11 @@ package table // UseSchema sets a new schema name for all generated table SQL builder types. It is recommended to invoke // this method only once at the beginning of the program. func UseSchema(schema string) { + Image = Image.FromSchema(schema) ImageLinks = ImageLinks.FromSchema(schema) ImageTags = ImageTags.FromSchema(schema) ImageText = ImageText.FromSchema(schema) UserImages = UserImages.FromSchema(schema) + UserImagesToProcess = UserImagesToProcess.FromSchema(schema) Users = Users.FromSchema(schema) } diff --git a/backend/.gen/haystack/haystack/table/user_images.go b/backend/.gen/haystack/haystack/table/user_images.go index 98bbd64..5e82a2b 100644 --- a/backend/.gen/haystack/haystack/table/user_images.go +++ b/backend/.gen/haystack/haystack/table/user_images.go @@ -17,10 +17,9 @@ type userImagesTable struct { postgres.Table // Columns - ID postgres.ColumnString - ImageName postgres.ColumnString - Image postgres.ColumnString - UserID postgres.ColumnString + ID postgres.ColumnString + ImageID postgres.ColumnString + UserID postgres.ColumnString AllColumns postgres.ColumnList MutableColumns postgres.ColumnList @@ -61,22 +60,20 @@ func newUserImagesTable(schemaName, tableName, alias string) *UserImagesTable { func newUserImagesTableImpl(schemaName, tableName, alias string) userImagesTable { var ( - IDColumn = postgres.StringColumn("id") - ImageNameColumn = postgres.StringColumn("image_name") - ImageColumn = postgres.StringColumn("image") - UserIDColumn = postgres.StringColumn("user_id") - allColumns = postgres.ColumnList{IDColumn, ImageNameColumn, ImageColumn, UserIDColumn} - mutableColumns = postgres.ColumnList{ImageNameColumn, ImageColumn, UserIDColumn} + IDColumn = postgres.StringColumn("id") + ImageIDColumn = postgres.StringColumn("image_id") + UserIDColumn = postgres.StringColumn("user_id") + allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, UserIDColumn} + mutableColumns = postgres.ColumnList{ImageIDColumn, UserIDColumn} ) return userImagesTable{ Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), //Columns - ID: IDColumn, - ImageName: ImageNameColumn, - Image: ImageColumn, - UserID: UserIDColumn, + ID: IDColumn, + ImageID: ImageIDColumn, + UserID: UserIDColumn, AllColumns: allColumns, MutableColumns: mutableColumns, diff --git a/backend/.gen/haystack/haystack/table/user_images_to_process.go b/backend/.gen/haystack/haystack/table/user_images_to_process.go new file mode 100644 index 0000000..b3df555 --- /dev/null +++ b/backend/.gen/haystack/haystack/table/user_images_to_process.go @@ -0,0 +1,81 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package table + +import ( + "github.com/go-jet/jet/v2/postgres" +) + +var UserImagesToProcess = newUserImagesToProcessTable("haystack", "user_images_to_process", "") + +type userImagesToProcessTable struct { + postgres.Table + + // Columns + ID postgres.ColumnString + ImageID postgres.ColumnString + UserID postgres.ColumnString + + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList +} + +type UserImagesToProcessTable struct { + userImagesToProcessTable + + EXCLUDED userImagesToProcessTable +} + +// AS creates new UserImagesToProcessTable with assigned alias +func (a UserImagesToProcessTable) AS(alias string) *UserImagesToProcessTable { + return newUserImagesToProcessTable(a.SchemaName(), a.TableName(), alias) +} + +// Schema creates new UserImagesToProcessTable with assigned schema name +func (a UserImagesToProcessTable) FromSchema(schemaName string) *UserImagesToProcessTable { + return newUserImagesToProcessTable(schemaName, a.TableName(), a.Alias()) +} + +// WithPrefix creates new UserImagesToProcessTable with assigned table prefix +func (a UserImagesToProcessTable) WithPrefix(prefix string) *UserImagesToProcessTable { + return newUserImagesToProcessTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} + +// WithSuffix creates new UserImagesToProcessTable with assigned table suffix +func (a UserImagesToProcessTable) WithSuffix(suffix string) *UserImagesToProcessTable { + return newUserImagesToProcessTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} + +func newUserImagesToProcessTable(schemaName, tableName, alias string) *UserImagesToProcessTable { + return &UserImagesToProcessTable{ + userImagesToProcessTable: newUserImagesToProcessTableImpl(schemaName, tableName, alias), + EXCLUDED: newUserImagesToProcessTableImpl("", "excluded", ""), + } +} + +func newUserImagesToProcessTableImpl(schemaName, tableName, alias string) userImagesToProcessTable { + var ( + IDColumn = postgres.StringColumn("id") + ImageIDColumn = postgres.StringColumn("image_id") + UserIDColumn = postgres.StringColumn("user_id") + allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, UserIDColumn} + mutableColumns = postgres.ColumnList{ImageIDColumn, UserIDColumn} + ) + + return userImagesToProcessTable{ + Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), + + //Columns + ID: IDColumn, + ImageID: ImageIDColumn, + UserID: UserIDColumn, + + AllColumns: allColumns, + MutableColumns: mutableColumns, + } +} diff --git a/backend/main.go b/backend/main.go index 7674819..f0b9c7a 100644 --- a/backend/main.go +++ b/backend/main.go @@ -28,7 +28,13 @@ func (client TestAiClient) GetImageInfo(imageName string, imageData []byte) (Ima func GetAiClient() (AiClient, error) { mode := os.Getenv("MODE") if mode == "TESTING" { - return TestAiClient{}, nil + return TestAiClient{ + ImageInfo: ImageInfo{ + Tags: []string{"tag"}, + Links: []string{"links"}, + Text: []string{"text"}, + }, + }, nil } return CreateOpenAiClient() @@ -65,7 +71,9 @@ func main() { select { case parameters := <-listener.Notify: - log.Println("received notification, new image available: " + parameters.Extra) + imageId := parameters.Extra + + log.Println("received notification, new image available: " + imageId) go func() { openAiClient, err := GetAiClient() @@ -73,24 +81,33 @@ func main() { panic(err) } - image, err := models.GetImage(parameters.Extra) + image, err := models.GetImageToProcessWithData(imageId) if err != nil { + log.Println("1") log.Println(err) return } - imageInfo, err := openAiClient.GetImageInfo(image.ImageName, image.Image) + imageInfo, err := openAiClient.GetImageInfo(image.Image.ImageName, image.Image.Image) if err != nil { + log.Println("2") log.Println(err) return } - log.Println("Finished processing image " + parameters.Extra) + savedImage, err := models.SaveImage(image.ID) + if err != nil { + log.Println("3") + log.Println(err) + return + } + + log.Println("Finished processing image " + imageId) log.Printf("Image attributes: %+v\n", imageInfo) - models.SaveImageTags(parameters.Extra, imageInfo.Tags) - models.SaveImageLinks(parameters.Extra, imageInfo.Links) - models.SaveImageTexts(parameters.Extra, imageInfo.Text) + models.SaveImageTags(savedImage.ID.String(), imageInfo.Tags) + models.SaveImageLinks(savedImage.ID.String(), imageInfo.Links) + models.SaveImageTexts(savedImage.ID.String(), imageInfo.Text) }() } } @@ -140,11 +157,11 @@ func main() { } // TODO: this could be part of the db table - extension := filepath.Ext(image.ImageName) + extension := filepath.Ext(image.Image.ImageName) extension = extension[1:] w.Header().Add("Content-Type", "image/"+extension) - w.Write(image.Image) + w.Write(image.Image.Image) }) r.Post("/image/{name}", func(w http.ResponseWriter, r *http.Request) { @@ -165,13 +182,15 @@ func main() { image, err := io.ReadAll(r.Body) if err != nil { + log.Println("First case") w.WriteHeader(http.StatusBadRequest) fmt.Fprintf(w, "Couldnt read the image from the request body") return } - userImage, err := models.SaveImage(userId, imageName, image) + userImage, err := models.SaveImageToProcess(userId, imageName, image) if err != nil { + log.Println("Second case") log.Println(err) w.WriteHeader(http.StatusBadRequest) fmt.Fprintf(w, "Could not save image to DB") @@ -180,6 +199,7 @@ func main() { jsonUserImage, err := json.Marshal(userImage) if err != nil { + log.Println("Third case") log.Println(err) w.WriteHeader(http.StatusBadRequest) fmt.Fprintf(w, "Could not create JSON response for this image") diff --git a/backend/models/image.go b/backend/models/image.go index 9fdca5a..29697f1 100644 --- a/backend/models/image.go +++ b/backend/models/image.go @@ -11,24 +11,113 @@ import ( "github.com/google/uuid" ) -func SaveImage(userId string, imageName string, imageData []byte) (model.UserImages, error) { - stmt := UserImages.INSERT(UserImages.UserID, UserImages.ImageName, UserImages.Image).VALUES(userId, imageName, imageData).RETURNING(UserImages.ID, UserImages.UserID, UserImages.ImageName) +func SaveImageToProcess(userId string, imageName string, imageData []byte) (model.UserImagesToProcess, error) { + insertImageStmt := Image.INSERT(Image.ImageName, Image.Image).VALUES(imageName, imageData).RETURNING(Image.ID) - userImage := model.UserImages{} - err := stmt.Query(db, &userImage) + // TODO: should be a transaction + + image := model.Image{} + err := insertImageStmt.Query(db, &image) + if err != nil { + return model.UserImagesToProcess{}, err + } + + stmt := UserImagesToProcess.INSERT(UserImagesToProcess.UserID, UserImagesToProcess.ImageID).VALUES(userId, image.ID).RETURNING(UserImagesToProcess.AllColumns) + + userImage := model.UserImagesToProcess{} + err = stmt.Query(db, &userImage) return userImage, err } -func GetImage(imageId string) (model.UserImages, error) { +func removeImageToProcess(imageId string) error { id := uuid.MustParse(imageId) - stmt := UserImages.SELECT(UserImages.AllColumns).WHERE(UserImages.ID.EQ(UUID(id))) - images := []model.UserImages{} + stmt := UserImagesToProcess.DELETE().WHERE(UserImagesToProcess.ID.EQ(UUID(id))) + + fmt.Println(stmt.DebugSql()) + + _, err := stmt.Exec(db) + + return err +} + +func SaveImage(imageId uuid.UUID) (model.UserImages, error) { + imageToProcess, err := GetImageToProcess(imageId.String()) + if err != nil { + return model.UserImages{}, err + } + + stmt := UserImages.INSERT(UserImages.UserID, UserImages.ImageID).VALUES(imageToProcess.UserID, imageToProcess.ImageID).RETURNING(UserImages.ID, UserImages.UserID, UserImages.ImageID) + + userImage := model.UserImages{} + err = stmt.Query(db, &userImage) + if err != nil { + return model.UserImages{}, err + } + + err = removeImageToProcess(imageId.String()) + if err != nil { + return model.UserImages{}, err + } + + return userImage, err +} + +type ImageData struct { + model.UserImages + + Image model.Image +} + +func GetImage(imageId string) (ImageData, error) { + id := uuid.MustParse(imageId) + stmt := SELECT(UserImages.AllColumns, Image.AllColumns).FROM(UserImages.INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID))).WHERE(UserImages.ID.EQ(UUID(id))) + + images := []ImageData{} err := stmt.Query(db, &images) if len(images) != 1 { - return model.UserImages{}, errors.New(fmt.Sprintf("Expected 1, got %d\n", len(images))) + return ImageData{}, errors.New(fmt.Sprintf("Expected 1, got %d\n", len(images))) + } + + return images[0], err +} + +type ImageToProcessData struct { + model.UserImagesToProcess + + Image model.Image +} + +func GetImageToProcessWithData(imageId string) (ImageToProcessData, error) { + id := uuid.MustParse(imageId) + // stmt := UserImagesToProcess.SELECT(UserImages.AllColumns).WHERE(UserImages.ID.EQ(UUID(id))) + + // TODO: Image should be `Images` + stmt := SELECT(UserImagesToProcess.AllColumns, Image.AllColumns).FROM(UserImagesToProcess.INNER_JOIN(Image, Image.ID.EQ(UserImagesToProcess.ImageID))).WHERE(UserImagesToProcess.ID.EQ(UUID(id))) + + images := []ImageToProcessData{} + err := stmt.Query(db, &images) + + if len(images) != 1 { + return ImageToProcessData{}, errors.New(fmt.Sprintf("Expected 1, got %d\n", len(images))) + } + + return images[0], err +} + +func GetImageToProcess(imageId string) (model.UserImagesToProcess, error) { + id := uuid.MustParse(imageId) + stmt := UserImagesToProcess.SELECT(UserImagesToProcess.AllColumns).WHERE(UserImagesToProcess.ID.EQ(UUID(id))) + + fmt.Println(stmt.DebugSql()) + + images := []model.UserImagesToProcess{} + err := stmt.Query(db, &images) + + if len(images) != 1 { + return model.UserImagesToProcess{}, errors.New(fmt.Sprintf("Expected 1, got %d\n", len(images))) } return images[0], err @@ -37,6 +126,9 @@ func GetImage(imageId string) (model.UserImages, error) { type UserImagesWithInfo struct { model.UserImages + // TODO: this shit + Image model.Image + Tags []model.ImageTags Links []model.ImageLinks Text []model.ImageText @@ -44,7 +136,7 @@ type UserImagesWithInfo struct { func GetUserImages(userId string) ([]UserImagesWithInfo, error) { id := uuid.MustParse(userId) - stmt := SELECT(UserImages.ID, UserImages.ImageName, ImageTags.AllColumns, ImageText.AllColumns, ImageLinks.AllColumns).FROM(UserImages.LEFT_JOIN(ImageTags, ImageTags.ImageID.EQ(UserImages.ID)).LEFT_JOIN(ImageText, ImageText.ImageID.EQ(UserImages.ID)).LEFT_JOIN(ImageLinks, ImageLinks.ImageID.EQ(UserImages.ID))).WHERE(UserImages.UserID.EQ(UUID(id))) + stmt := SELECT(UserImages.AllColumns, ImageTags.AllColumns, ImageText.AllColumns, ImageLinks.AllColumns).FROM(UserImages.LEFT_JOIN(ImageTags, ImageTags.ImageID.EQ(UserImages.ID)).LEFT_JOIN(ImageText, ImageText.ImageID.EQ(UserImages.ID)).LEFT_JOIN(ImageLinks, ImageLinks.ImageID.EQ(UserImages.ID))).WHERE(UserImages.UserID.EQ(UUID(id))) images := []UserImagesWithInfo{} err := stmt.Query(db, &images) @@ -63,6 +155,8 @@ func SaveImageTags(imageId string, tags []string) ([]model.ImageTags, error) { stmt.RETURNING(ImageTags.AllColumns) + fmt.Println(stmt.DebugSql()) + imageTags := []model.ImageTags{} err := stmt.Query(db, &imageTags) diff --git a/backend/schema.sql b/backend/schema.sql index 9d366e8..26b9dbc 100644 --- a/backend/schema.sql +++ b/backend/schema.sql @@ -11,10 +11,21 @@ CREATE TABLE haystack.users ( id uuid PRIMARY KEY DEFAULT gen_random_uuid() ); -CREATE TABLE haystack.user_images ( +CREATE TABLE haystack.image ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), image_name TEXT NOT NULL, - image BYTEA NOT NULL, + image BYTEA NOT NULL +); + +CREATE TABLE haystack.user_images_to_process ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + image_id uuid NOT NULL UNIQUE REFERENCES haystack.image (id), + user_id uuid NOT NULL REFERENCES haystack.users (id) +); + +CREATE TABLE haystack.user_images ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + image_id uuid NOT NULL UNIQUE REFERENCES haystack.image (id), user_id uuid NOT NULL REFERENCES haystack.users (id) ); @@ -49,7 +60,7 @@ $$ LANGUAGE plpgsql; /* -----| Triggers |----- */ CREATE OR REPLACE TRIGGER on_new_image AFTER INSERT -ON haystack.user_images +ON haystack.user_images_to_process FOR EACH ROW EXECUTE PROCEDURE notify_new_image(); From ad61b8e1fa18920aa11c9aac5d3d8d3fe1f009dd Mon Sep 17 00:00:00 2001 From: John Costa Date: Wed, 26 Feb 2025 20:09:19 +0000 Subject: [PATCH 14/30] fix: getting user images --- backend/models/image.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/models/image.go b/backend/models/image.go index 29697f1..6fadd0c 100644 --- a/backend/models/image.go +++ b/backend/models/image.go @@ -136,7 +136,9 @@ type UserImagesWithInfo struct { func GetUserImages(userId string) ([]UserImagesWithInfo, error) { id := uuid.MustParse(userId) - stmt := SELECT(UserImages.AllColumns, ImageTags.AllColumns, ImageText.AllColumns, ImageLinks.AllColumns).FROM(UserImages.LEFT_JOIN(ImageTags, ImageTags.ImageID.EQ(UserImages.ID)).LEFT_JOIN(ImageText, ImageText.ImageID.EQ(UserImages.ID)).LEFT_JOIN(ImageLinks, ImageLinks.ImageID.EQ(UserImages.ID))).WHERE(UserImages.UserID.EQ(UUID(id))) + stmt := SELECT(UserImages.AllColumns, Image.ID, Image.ImageName, ImageTags.AllColumns, ImageText.AllColumns, ImageLinks.AllColumns).FROM(UserImages.INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID)).LEFT_JOIN(ImageTags, ImageTags.ImageID.EQ(UserImages.ID)).LEFT_JOIN(ImageText, ImageText.ImageID.EQ(UserImages.ID)).LEFT_JOIN(ImageLinks, ImageLinks.ImageID.EQ(UserImages.ID))).WHERE(UserImages.UserID.EQ(UUID(id))) + + fmt.Println(stmt.DebugSql()) images := []UserImagesWithInfo{} err := stmt.Query(db, &images) From d1d6ee6762c78b3a9f9c55e79e4ad89339abb66e Mon Sep 17 00:00:00 2001 From: John Costa Date: Wed, 26 Feb 2025 20:48:52 +0000 Subject: [PATCH 15/30] fix: some spam from get images request --- backend/models/image.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/models/image.go b/backend/models/image.go index 6fadd0c..db533c9 100644 --- a/backend/models/image.go +++ b/backend/models/image.go @@ -124,7 +124,7 @@ func GetImageToProcess(imageId string) (model.UserImagesToProcess, error) { } type UserImagesWithInfo struct { - model.UserImages + ID uuid.UUID // TODO: this shit Image model.Image @@ -136,7 +136,7 @@ type UserImagesWithInfo struct { func GetUserImages(userId string) ([]UserImagesWithInfo, error) { id := uuid.MustParse(userId) - stmt := SELECT(UserImages.AllColumns, Image.ID, Image.ImageName, ImageTags.AllColumns, ImageText.AllColumns, ImageLinks.AllColumns).FROM(UserImages.INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID)).LEFT_JOIN(ImageTags, ImageTags.ImageID.EQ(UserImages.ID)).LEFT_JOIN(ImageText, ImageText.ImageID.EQ(UserImages.ID)).LEFT_JOIN(ImageLinks, ImageLinks.ImageID.EQ(UserImages.ID))).WHERE(UserImages.UserID.EQ(UUID(id))) + stmt := SELECT(UserImages.ID.AS("UserImagesWithInfo.ID"), Image.ID, Image.ImageName, ImageTags.AllColumns, ImageText.AllColumns, ImageLinks.AllColumns).FROM(UserImages.INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID)).LEFT_JOIN(ImageTags, ImageTags.ImageID.EQ(UserImages.ID)).LEFT_JOIN(ImageText, ImageText.ImageID.EQ(UserImages.ID)).LEFT_JOIN(ImageLinks, ImageLinks.ImageID.EQ(UserImages.ID))).WHERE(UserImages.UserID.EQ(UUID(id))) fmt.Println(stmt.DebugSql()) From 3fe48464e46294e155a98b30ddb0032369157536 Mon Sep 17 00:00:00 2001 From: John Costa Date: Wed, 26 Feb 2025 20:49:55 +0000 Subject: [PATCH 16/30] feat: using different prompt --- backend/openai.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/openai.go b/backend/openai.go index f7a114e..534189e 100644 --- a/backend/openai.go +++ b/backend/openai.go @@ -140,8 +140,7 @@ const IMAGE_TYPE = "image_url" const PROMPT = ` You are an image information extractor. The user will provide you with screenshots and your job is to extract any relevant links and text that the image might contain. You will also try your best to assign some tags to this image, avoid too many tags. - -This system is part of a bookmark manager, who's main goal is to allow the user to search through various screenshots. +Be sure to extract every link (URL) that you find. ` const RESPONSE_FORMAT = ` From 7e7f3ff7323c7926d71488ea794f96954c309479 Mon Sep 17 00:00:00 2001 From: John Costa Date: Wed, 26 Feb 2025 20:53:12 +0000 Subject: [PATCH 17/30] feat: making prompt be more generic with tags --- backend/openai.go | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/openai.go b/backend/openai.go index 534189e..cfab5fa 100644 --- a/backend/openai.go +++ b/backend/openai.go @@ -141,6 +141,7 @@ const PROMPT = ` You are an image information extractor. The user will provide you with screenshots and your job is to extract any relevant links and text that the image might contain. You will also try your best to assign some tags to this image, avoid too many tags. Be sure to extract every link (URL) that you find. +Use generic tags. ` const RESPONSE_FORMAT = ` From ee69d9c2fe5a0c8c7d3c560cd3dd3d289eaf2998 Mon Sep 17 00:00:00 2001 From: John Costa Date: Wed, 26 Feb 2025 21:27:37 +0000 Subject: [PATCH 18/30] feat: network file with validators for backend requests --- frontend/src/network/index.ts | 97 +++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 frontend/src/network/index.ts diff --git a/frontend/src/network/index.ts b/frontend/src/network/index.ts new file mode 100644 index 0000000..9a31eb3 --- /dev/null +++ b/frontend/src/network/index.ts @@ -0,0 +1,97 @@ +import { + array, + InferOutput, + null as Null, + nullable, + object, + parse, + pipe, + string, + uuid, +} from "valibot"; + +type BaseRequestParams = Partial<{ + path: string; + body: RequestInit["body"]; + method: "GET" | "POST"; +}>; + +const getBaseRequest = ({ path, body, method }: BaseRequestParams): Request => { + return new Request(`http://localhost:3040/${path}`, { + headers: { userId: "fcc22dbb-7792-4595-be8e-d0439e13990a" }, + body, + method, + }); +}; + +const sendImageResponseValidator = object({ + ID: pipe(string(), uuid()), + ImageID: pipe(string(), uuid()), + UserID: pipe(string(), uuid()), +}); + +export const sendImage = async ( + imageName: string, + image: BlobPart, +): Promise> => { + const data = new Blob([image]); + const request = getBaseRequest({ + path: `image/${imageName}`, + body: data, + method: "POST", + }); + + request.headers.set("Content-Type", "aplication/oclet-stream"); + + const res = await fetch(request).then((res) => res.json()); + + return parse(sendImageResponseValidator, res); +}; + +const getUserImagesResponseValidator = array( + object({ + ID: pipe(string(), uuid()), + Image: object({ + ID: pipe(string(), uuid()), + ImageName: string(), + Image: Null(), + }), + Tags: nullable( + array( + object({ + ID: pipe(string(), uuid()), + Tag: string(), + ImageID: pipe(string(), uuid()), + }), + ), + ), + Links: nullable( + array( + object({ + ID: pipe(string(), uuid()), + Links: string(), + ImageID: pipe(string(), uuid()), + }), + ), + ), + Text: nullable( + array( + object({ + ID: pipe(string(), uuid()), + ImageText: string(), + ImageID: pipe(string(), uuid()), + }), + ), + ), + }), +); + +export const getUserImages = async (): Promise< + InferOutput +> => { + const request = getBaseRequest({ path: "image" }); + + const res = await fetch(request).then((res) => res.json()); + + return parse(getUserImagesResponseValidator, res); +}; From 2df18869e5377fac4c45aa0277881b5a246fddcf Mon Sep 17 00:00:00 2001 From: John Costa Date: Wed, 26 Feb 2025 21:27:43 +0000 Subject: [PATCH 19/30] chore: running format --- frontend/biome.json | 20 +- frontend/bun.lockb | Bin 113120 -> 113507 bytes frontend/package.json | 73 ++++---- frontend/postcss.config.js | 8 +- frontend/src-tauri/capabilities/default.json | 20 +- frontend/src-tauri/src/lib.rs | 71 ++++---- frontend/src-tauri/tauri.conf.json | 56 +++--- frontend/src/App.tsx | 182 +++++++++---------- frontend/src/components/FolderPicker.tsx | 76 ++++---- frontend/src/components/ImageViewer.tsx | 54 +++--- frontend/src/index.css | 28 +-- frontend/tailwind.config.js | 24 +-- frontend/tsconfig.json | 44 ++--- frontend/tsconfig.node.json | 16 +- frontend/vite.config.ts | 54 +++--- 15 files changed, 362 insertions(+), 364 deletions(-) diff --git a/frontend/biome.json b/frontend/biome.json index decb4ed..f8702e4 100644 --- a/frontend/biome.json +++ b/frontend/biome.json @@ -1,12 +1,12 @@ { - "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", - "organizeImports": { - "enabled": true - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - } + "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + } } diff --git a/frontend/bun.lockb b/frontend/bun.lockb index d69237e6558f6fc863a4140e02ac7cf98527f3c3..66c8f4ec506356151f30af38822facd7c79cb34e 100755 GIT binary patch delta 19164 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 `x3EJiE;gtDH0oalyE!0d(yM4Cyz_0`K0 zS^cmE{$mNaa~X&Z{AbsaNU27a8bQ5&wZ;MLd4czp7Is`D4`yBh??A@O6}`3D{*QUE zhW3YYuAruyVB6~cxK#bE2|tMn=tLSD(4|Qy&wsso#%*+An08BTY*Dw*+j`>9o_F?W zolEz%@uK|z++~}+#JB;NYsO>soZ>4D7cTEFpoo=)E_24V`3rvT&r0t%^fro!lq$vU z0q7HBpM3b*3G>dBZYeOayL~mIo{CB+m>MUFUr>iBAW<|O$ih1Q^E>&!Z$%@vT+ABC zg2;Rftv6F$qH655j;w{}Hvf8bB5IC~)}g-R;C6m+zcJnrk2RZB0_LpTOAvr&QSBA#97C&kYLwT@y%fLjM`ieF(|pZ85h|-qA(fj(@AjxqRRi5ih4sZn#R8gxYzo@ z&HsuVvsg7jy<@&1tWYp%KbgG6%pthn#*Jj7q0BqVD_Ofu?>71B#0ATyv_!5j?blfg zq%=f7@6Fu#@%!#4n^KB)uMn0j#vre+@efW?KKiQVyqre%>KfJCQ}u2Ym3W$Q(bgxc zxHFUuq+f<1!`LKW<3AM44!T^h@?PNAY8C1aU-idvxXERhxI2u6`5OP?VD#G^zO)py z83@JBA^4Gsr$iz&1zl1s94Q!jO*oAIXb{uy-EYD+`rsmnIT1=O^7np4&884-yK(LITga*J`ddD>TH63%h2orx`cDzOEu_ zI17t1{@uZtvJMYlx%F+T3x4&5Q8AM74-xWaos2o(Zeh#n6!#Hg^Kf*v@h=p7pFDD| zc;7O-TBE8`nR$C(pESvm_kGJhmXAN$`I)mn!D delta 19056 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 diff --git a/frontend/package.json b/frontend/package.json index 68a9eed..a904fae 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,38 +1,39 @@ { - "name": "haystack", - "version": "0.1.0", - "description": "", - "type": "module", - "scripts": { - "start": "vite", - "dev": "vite", - "build": "vite build", - "serve": "vite preview", - "tauri": "tauri", - "lint": "bunx @biomejs/biome lint .", - "format": "bunx @biomejs/biome format . --write" - }, - "license": "MIT", - "dependencies": { - "@kobalte/core": "^0.13.9", - "@kobalte/tailwindcss": "^0.9.0", - "@tabler/icons-solidjs": "^3.30.0", - "@tauri-apps/api": "^2", - "@tauri-apps/plugin-dialog": "~2", - "@tauri-apps/plugin-opener": "^2", - "clsx": "^2.1.1", - "solid-js": "^1.9.3", - "tailwind-scrollbar-hide": "^2.0.0" - }, - "devDependencies": { - "@biomejs/biome": "^1.9.4", - "@tauri-apps/cli": "^2", - "autoprefixer": "^10.4.20", - "postcss": "^8.5.3", - "postcss-cli": "^11.0.0", - "tailwindcss": "3.4.0", - "typescript": "~5.6.2", - "vite": "^6.0.3", - "vite-plugin-solid": "^2.11.0" - } + "name": "haystack", + "version": "0.1.0", + "description": "", + "type": "module", + "scripts": { + "start": "vite", + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "tauri": "tauri", + "lint": "bunx @biomejs/biome lint .", + "format": "bunx @biomejs/biome format . --write" + }, + "license": "MIT", + "dependencies": { + "@kobalte/core": "^0.13.9", + "@kobalte/tailwindcss": "^0.9.0", + "@tabler/icons-solidjs": "^3.30.0", + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-dialog": "~2", + "@tauri-apps/plugin-opener": "^2", + "clsx": "^2.1.1", + "solid-js": "^1.9.3", + "tailwind-scrollbar-hide": "^2.0.0", + "valibot": "^1.0.0-rc.2" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@tauri-apps/cli": "^2", + "autoprefixer": "^10.4.20", + "postcss": "^8.5.3", + "postcss-cli": "^11.0.0", + "tailwindcss": "3.4.0", + "typescript": "~5.6.2", + "vite": "^6.0.3", + "vite-plugin-solid": "^2.11.0" + } } diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index 2aa7205..7b75c83 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -1,6 +1,6 @@ export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, }; diff --git a/frontend/src-tauri/capabilities/default.json b/frontend/src-tauri/capabilities/default.json index e8480e0..d8e24c2 100644 --- a/frontend/src-tauri/capabilities/default.json +++ b/frontend/src-tauri/capabilities/default.json @@ -1,12 +1,12 @@ { - "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "default", - "description": "Capability for the main window", - "windows": ["main"], - "permissions": [ - "core:default", - "opener:default", - "dialog:default", - "core:window:allow-start-dragging" - ] + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capability for the main window", + "windows": ["main"], + "permissions": [ + "core:default", + "opener:default", + "dialog:default", + "core:window:allow-start-dragging" + ] } diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index 1d4a06c..3920ce6 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -1,14 +1,13 @@ -use std::path::PathBuf; +use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher}; -use tauri::Emitter; -use std::sync::mpsc::channel; use std::fs; -use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; -use std::sync::Mutex; +use std::path::PathBuf; +use std::sync::mpsc::channel; use std::sync::Arc; +use std::sync::Mutex; use tauri::AppHandle; -use tauri::{TitleBarStyle, WebviewUrl, WebviewWindowBuilder}; - +use tauri::Emitter; +use tauri::{WebviewUrl, WebviewWindowBuilder}; struct WatcherState { watcher: Option, @@ -25,8 +24,7 @@ fn process_png_file(path: &PathBuf, app: AppHandle) -> Result<(), String> { println!("Processing PNG file: {}", path.display()); // Read the file - let contents = fs::read(path) - .map_err(|e| format!("Failed to read file: {}", e))?; + let contents = fs::read(path).map_err(|e| format!("Failed to read file: {}", e))?; // Convert to base64 let base64_string = BASE64.encode(&contents); @@ -47,26 +45,27 @@ async fn handle_selected_folder( app: AppHandle, ) -> Result { let path_buf = PathBuf::from(&path); - + if !path_buf.exists() || !path_buf.is_dir() { return Err("Invalid directory path".to_string()); } // Stop existing watcher if any - let mut state = state.lock().map_err(|_| "Failed to lock state".to_string())?; + let mut state = state + .lock() + .map_err(|_| "Failed to lock state".to_string())?; state.watcher = None; // Create a channel to receive file system events let (tx, rx) = channel(); // Create a new watcher - let mut watcher = RecommendedWatcher::new( - tx, - Config::default(), - ).map_err(|e| format!("Failed to create watcher: {}", e))?; + let mut watcher = RecommendedWatcher::new(tx, Config::default()) + .map_err(|e| format!("Failed to create watcher: {}", e))?; // Start watching the directory - watcher.watch(path_buf.as_ref(), RecursiveMode::Recursive) + watcher + .watch(path_buf.as_ref(), RecursiveMode::Recursive) .map_err(|e| format!("Failed to watch directory: {}", e))?; // Store the watcher in state @@ -114,38 +113,36 @@ pub fn run() { .manage(watcher_state) .invoke_handler(tauri::generate_handler![handle_selected_folder]) .setup(|app| { - let win_builder = - WebviewWindowBuilder::new(app, "main", WebviewUrl::default()) - .hidden_title(true) + let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default()) .inner_size(480.0, 360.0) .resizable(false); // set transparent title bar only when building for macOS #[cfg(target_os = "macos")] let win_builder = win_builder.title_bar_style(TitleBarStyle::Transparent); - + let window = win_builder.build().unwrap(); - + // set background color only when building for macOS #[cfg(target_os = "macos")] { - use cocoa::appkit::{NSColor, NSWindow}; - use cocoa::base::{id, nil}; - - let ns_window = window.ns_window().unwrap() as id; - unsafe { - let bg_color = NSColor::colorWithRed_green_blue_alpha_( - nil, - 245.0 / 255.0, - 245.0 / 255.0, - 245.0 / 255.0, - 1.0, - ); - ns_window.setBackgroundColor_(bg_color); - } + use cocoa::appkit::{NSColor, NSWindow}; + use cocoa::base::{id, nil}; + + let ns_window = window.ns_window().unwrap() as id; + unsafe { + let bg_color = NSColor::colorWithRed_green_blue_alpha_( + nil, + 245.0 / 255.0, + 245.0 / 255.0, + 245.0 / 255.0, + 1.0, + ); + ns_window.setBackgroundColor_(bg_color); + } } - + Ok(()) - }) + }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index 2aaa7b5..d056e73 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -1,30 +1,30 @@ { - "$schema": "https://schema.tauri.app/config/2", - "productName": "haystack", - "version": "0.1.0", - "identifier": "com.haystack.app", - "build": { - "beforeDevCommand": "bun run dev", - "devUrl": "http://localhost:1420", - "beforeBuildCommand": "bun run build", - "frontendDist": "../dist" - }, - "app": { - "windows": [], - "macOSPrivateApi": true, - "security": { - "csp": null - } - }, - "bundle": { - "active": true, - "targets": "all", - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.icns", - "icons/icon.ico" - ] - } + "$schema": "https://schema.tauri.app/config/2", + "productName": "haystack", + "version": "0.1.0", + "identifier": "com.haystack.app", + "build": { + "beforeDevCommand": "bun run dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "bun run build", + "frontendDist": "../dist" + }, + "app": { + "windows": [], + "macOSPrivateApi": true, + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2129247..cd87c9e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,105 +4,105 @@ import { IconSearch, IconRefresh } from "@tabler/icons-solidjs"; import clsx from "clsx"; type Emoji = { - emoji: string; - name: string; + emoji: string; + name: string; }; function App() { - const [options, setOptions] = createSignal([]); - const [emoji, setEmoji] = createSignal(null); + const [options, setOptions] = createSignal([]); + const [emoji, setEmoji] = createSignal(null); - const emojiData: Emoji[] = [ - { emoji: "😀", name: "Grinning Face" }, - { emoji: "😃", name: "Grinning Face with Big Eyes" }, - { emoji: "😄", name: "Grinning Face with Smiling Eyes" }, - { emoji: "😁", name: "Beaming Face with Smiling Eyes" }, - { emoji: "😆", name: "Grinning Squinting Face" }, - ]; + const emojiData: Emoji[] = [ + { emoji: "😀", name: "Grinning Face" }, + { emoji: "😃", name: "Grinning Face with Big Eyes" }, + { emoji: "😄", name: "Grinning Face with Smiling Eyes" }, + { emoji: "😁", name: "Beaming Face with Smiling Eyes" }, + { emoji: "😆", name: "Grinning Squinting Face" }, + ]; - const queryEmojiData = (query: string) => { - return emojiData.filter((emoji) => - emoji.name.toLowerCase().includes(query.toLowerCase()) - ); - }; + const queryEmojiData = (query: string) => { + return emojiData.filter((emoji) => + emoji.name.toLowerCase().includes(query.toLowerCase()), + ); + }; - return ( -
-
- setOptions(queryEmojiData(query))} - onChange={(result) => setEmoji(result)} - optionValue="name" - optionLabel="name" - placeholder="Search for stuff..." - itemComponent={(props) => ( - - - {props.item.rawValue.emoji} - - - )} - > - - - - - } - > - - - - - - - - e.preventDefault()} - > - - - 😬 No emoji found - - - - -
- {/*
+ return ( +
+
+ setOptions(queryEmojiData(query))} + onChange={(result) => setEmoji(result)} + optionValue="name" + optionLabel="name" + placeholder="Search for stuff..." + itemComponent={(props) => ( + + + {props.item.rawValue.emoji} + + + )} + > + + + + + } + > + + + + + + + + e.preventDefault()} + > + + + 😬 No emoji found + + + + +
+ {/*
Emoji selected: {emoji()?.emoji} {emoji()?.name}
*/} -
-
-
-
-
-
-
-
-
-
-
-
-
-
- footer -
-
- ); +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ footer +
+
+ ); } export default App; diff --git a/frontend/src/components/FolderPicker.tsx b/frontend/src/components/FolderPicker.tsx index 585fbbe..285a2bb 100644 --- a/frontend/src/components/FolderPicker.tsx +++ b/frontend/src/components/FolderPicker.tsx @@ -3,47 +3,47 @@ import { open } from "@tauri-apps/plugin-dialog"; import { invoke } from "@tauri-apps/api/core"; export function FolderPicker() { - const [selectedPath, setSelectedPath] = createSignal(""); - const [status, setStatus] = createSignal(""); + const [selectedPath, setSelectedPath] = createSignal(""); + const [status, setStatus] = createSignal(""); - const handleFolderSelect = async () => { - try { - const selected = await open({ - directory: true, - multiple: false, - }); + const handleFolderSelect = async () => { + try { + const selected = await open({ + directory: true, + multiple: false, + }); - if (selected) { - setSelectedPath(selected as string); - // Send the path to Rust - const response = await invoke("handle_selected_folder", { - path: selected, - }); - setStatus(`Folder processed: ${response}`); - } - } catch (error) { - setStatus(`Error: ${error}`); - } - }; + if (selected) { + setSelectedPath(selected as string); + // Send the path to Rust + const response = await invoke("handle_selected_folder", { + path: selected, + }); + setStatus(`Folder processed: ${response}`); + } + } catch (error) { + setStatus(`Error: ${error}`); + } + }; - return ( -
- + return ( +
+ - {selectedPath() && ( -
-

Selected folder:

-

{selectedPath()}

-
- )} + {selectedPath() && ( +
+

Selected folder:

+

{selectedPath()}

+
+ )} - {status() &&

{status()}

} -
- ); + {status() &&

{status()}

} +
+ ); } diff --git a/frontend/src/components/ImageViewer.tsx b/frontend/src/components/ImageViewer.tsx index 85fc08a..2fffb5c 100644 --- a/frontend/src/components/ImageViewer.tsx +++ b/frontend/src/components/ImageViewer.tsx @@ -3,35 +3,35 @@ import { listen } from "@tauri-apps/api/event"; import { FolderPicker } from "./FolderPicker"; export function ImageViewer() { - const [latestImage, setLatestImage] = createSignal(null); + const [latestImage, setLatestImage] = createSignal(null); - createEffect(() => { - // Listen for PNG processing events - const unlisten = listen("png-processed", (event) => { - console.log("Received processed PNG"); - const base64Data = event.payload as string; - setLatestImage(`data:image/png;base64,${base64Data}`); - }); + createEffect(() => { + // Listen for PNG processing events + const unlisten = listen("png-processed", (event) => { + console.log("Received processed PNG"); + const base64Data = event.payload as string; + setLatestImage(`data:image/png;base64,${base64Data}`); + }); - return () => { - unlisten.then((fn) => fn()); // Cleanup listener - }; - }); + return () => { + unlisten.then((fn) => fn()); // Cleanup listener + }; + }); - return ( -
- + return ( +
+ - {latestImage() && ( -
-

Latest Processed Image:

- Latest processed -
- )} -
- ); + {latestImage() && ( +
+

Latest Processed Image:

+ Latest processed +
+ )} +
+ ); } diff --git a/frontend/src/index.css b/frontend/src/index.css index 71bc21a..d61995c 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -3,21 +3,21 @@ @tailwind utilities; @font-face { - font-family: "Manrope"; - src: url("./assets/fonts/Manrope-VariableFont_wght.ttf") format("truetype"); - font-weight: 100 900; - font-display: swap; + font-family: "Manrope"; + src: url("./assets/fonts/Manrope-VariableFont_wght.ttf") format("truetype"); + font-weight: 100 900; + font-display: swap; } :root { - @apply bg-neutral-100 text-black rounded-xl; - font-family: Manrope, sans-serif; - font-size: 16px; - line-height: 24px; - font-weight: 500; - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; + @apply bg-neutral-100 text-black rounded-xl; + font-family: Manrope, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 500; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; } diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 97f179f..f749829 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1,15 +1,15 @@ /** @type {import('tailwindcss').Config} */ export default { - content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], - theme: { - extend: { - fontFamily: { - sans: ["Manrope", "sans-serif"], - }, - }, - }, - plugins: [ - require("@kobalte/tailwindcss"), - require("tailwind-scrollbar-hide"), - ], + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: { + fontFamily: { + sans: ["Manrope", "sans-serif"], + }, + }, + }, + plugins: [ + require("@kobalte/tailwindcss"), + require("tailwind-scrollbar-hide"), + ], }; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 3999958..f7f13c7 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,26 +1,26 @@ { - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "module": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "skipLibCheck": true, + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "preserve", - "jsxImportSource": "solid-js", + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json index 42872c5..eca6668 100644 --- a/frontend/tsconfig.node.json +++ b/frontend/tsconfig.node.json @@ -1,10 +1,10 @@ { - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts"] + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index af994b4..815ec0f 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -8,31 +8,31 @@ const host = process.env.TAURI_DEV_HOST; // https://vitejs.dev/config/ export default defineConfig(async () => ({ - plugins: [solid()], - css: { - postcss: { - plugins: [tailwindcss, autoprefixer], - }, - }, - // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` - // - // 1. prevent vite from obscuring rust errors - clearScreen: false, - // 2. tauri expects a fixed port, fail if that port is not available - server: { - port: 1420, - strictPort: true, - host: host || false, - hmr: host - ? { - protocol: "ws", - host, - port: 1421, - } - : undefined, - watch: { - // 3. tell vite to ignore watching `src-tauri` - ignored: ["**/src-tauri/**"], - }, - }, + plugins: [solid()], + css: { + postcss: { + plugins: [tailwindcss, autoprefixer], + }, + }, + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // + // 1. prevent vite from obscuring rust errors + clearScreen: false, + // 2. tauri expects a fixed port, fail if that port is not available + server: { + port: 1420, + strictPort: true, + host: host || false, + hmr: host + ? { + protocol: "ws", + host, + port: 1421, + } + : undefined, + watch: { + // 3. tell vite to ignore watching `src-tauri` + ignored: ["**/src-tauri/**"], + }, + }, })); From e595783d8987a0807b294a39e415e9a461fac7ec Mon Sep 17 00:00:00 2001 From: John Costa Date: Fri, 7 Mar 2025 13:42:50 +0000 Subject: [PATCH 20/30] wip: Using mistral instead of OpenAi --- backend/main.go | 3 +++ backend/openai.go | 15 ++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/backend/main.go b/backend/main.go index f0b9c7a..2c62899 100644 --- a/backend/main.go +++ b/backend/main.go @@ -88,6 +88,8 @@ func main() { return } + fmt.Printf("%+v\n", image) + imageInfo, err := openAiClient.GetImageInfo(image.Image.ImageName, image.Image.Image) if err != nil { log.Println("2") @@ -180,6 +182,7 @@ func main() { } image, err := io.ReadAll(r.Body) + fmt.Println(image) if err != nil { log.Println("First case") diff --git a/backend/openai.go b/backend/openai.go index cfab5fa..49644ff 100644 --- a/backend/openai.go +++ b/backend/openai.go @@ -72,9 +72,7 @@ func (content *OpenAiMessages) AddImage(imageName string, image []byte) error { arrayMessage := OpenAiArrayMessage{Role: ROLE_USER, Content: make([]OpenAiContent, 1)} arrayMessage.Content[0] = OpenAiImage{ ImageType: IMAGE_TYPE, - ImageUrl: ImageUrl{ - Url: fmt.Sprintf("data:image/%s;base64,%s", extension, encodedString), - }, + ImageUrl: fmt.Sprintf("data:image/%s;base64,%s", extension, encodedString), } content.Messages = append(content.Messages, arrayMessage) @@ -104,8 +102,8 @@ type ImageUrl struct { } type OpenAiImage struct { - ImageType string `json:"type"` - ImageUrl ImageUrl `json:"image_url"` + ImageType string `json:"type"` + ImageUrl string `json:"image_url"` } func (imageMessage OpenAiImage) ToJson() ([]byte, error) { @@ -192,7 +190,7 @@ func CreateOpenAiClient() (OpenAiClient, error) { return OpenAiClient{ apiKey: apiKey, - url: "https://api.openai.com/v1/chat/completions", + url: "https://api.mistral.ai/v1/chat/completions", systemPrompt: PROMPT, Do: func(req *http.Request) (*http.Response, error) { client := &http.Client{} @@ -279,7 +277,7 @@ func parseOpenAiResponse(jsonResponse []byte) (ImageInfo, error) { } func (client OpenAiClient) GetImageInfo(imageName string, imageData []byte) (ImageInfo, error) { - aiRequest, err := getCompletionsForImage("gpt-4o-mini", 1.0, client.systemPrompt, imageName, RESPONSE_FORMAT, imageData) + aiRequest, err := getCompletionsForImage("pixtral-12b-2409", 1.0, client.systemPrompt, imageName, RESPONSE_FORMAT, imageData) if err != nil { return ImageInfo{}, err } @@ -300,6 +298,8 @@ func (client OpenAiClient) GetImageInfo(imageName string, imageData []byte) (Ima return ImageInfo{}, err } + fmt.Println(string(jsonAiRequest)) + request, err := client.getRequest(jsonAiRequest) if err != nil { return ImageInfo{}, err @@ -315,5 +315,6 @@ func (client OpenAiClient) GetImageInfo(imageName string, imageData []byte) (Ima return ImageInfo{}, err } + fmt.Println(string(response)) return parseOpenAiResponse(response) } From 1424ec22f41d46d1f33ccec34a94d465cca36547 Mon Sep 17 00:00:00 2001 From: John Costa Date: Fri, 7 Mar 2025 14:14:40 +0000 Subject: [PATCH 21/30] fix: using json response header --- backend/main.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/main.go b/backend/main.go index 2c62899..c80aa1d 100644 --- a/backend/main.go +++ b/backend/main.go @@ -116,7 +116,15 @@ func main() { }() r := chi.NewRouter() + r.Use(middleware.Logger) + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + + next.ServeHTTP(w, r) + }) + }) r.Options("/*", func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Access-Control-Allow-Origin", "*") From d212584486fc3072a71ed182429882258040dc27 Mon Sep 17 00:00:00 2001 From: John Costa Date: Sat, 8 Mar 2025 11:58:25 +0000 Subject: [PATCH 22/30] wip: dialog to choose folder to watch --- frontend/bun.lockb | Bin 113507 -> 113510 bytes frontend/src-tauri/src/lib.rs | 2 +- frontend/src/App.tsx | 184 ++++++++++++------------ frontend/src/components/ImageViewer.tsx | 57 ++++---- 4 files changed, 124 insertions(+), 119 deletions(-) diff --git a/frontend/bun.lockb b/frontend/bun.lockb index 66c8f4ec506356151f30af38822facd7c79cb34e..e402c1c51e0d0e40ac66d671689f35e4d20ef6f3 100755 GIT binary patch delta 10458 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 G;eP?Vc%V!G delta 10488 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 P=|pbd_0a^rY0CcsuLhIl diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index 3920ce6..daa3d04 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -115,7 +115,7 @@ pub fn run() { .setup(|app| { let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default()) .inner_size(480.0, 360.0) - .resizable(false); + .resizable(true); // set transparent title bar only when building for macOS #[cfg(target_os = "macos")] let win_builder = win_builder.title_bar_style(TitleBarStyle::Transparent); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index cd87c9e..97c3a6f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,107 +2,109 @@ import { createSignal } from "solid-js"; import { Search } from "@kobalte/core/search"; import { IconSearch, IconRefresh } from "@tabler/icons-solidjs"; import clsx from "clsx"; +import { ImageViewer } from "./components/ImageViewer"; type Emoji = { - emoji: string; - name: string; + emoji: string; + name: string; }; function App() { - const [options, setOptions] = createSignal([]); - const [emoji, setEmoji] = createSignal(null); + const [options, setOptions] = createSignal([]); + const [emoji, setEmoji] = createSignal(null); - const emojiData: Emoji[] = [ - { emoji: "😀", name: "Grinning Face" }, - { emoji: "😃", name: "Grinning Face with Big Eyes" }, - { emoji: "😄", name: "Grinning Face with Smiling Eyes" }, - { emoji: "😁", name: "Beaming Face with Smiling Eyes" }, - { emoji: "😆", name: "Grinning Squinting Face" }, - ]; + const emojiData: Emoji[] = [ + { emoji: "😀", name: "Grinning Face" }, + { emoji: "😃", name: "Grinning Face with Big Eyes" }, + { emoji: "😄", name: "Grinning Face with Smiling Eyes" }, + { emoji: "😁", name: "Beaming Face with Smiling Eyes" }, + { emoji: "😆", name: "Grinning Squinting Face" }, + ]; - const queryEmojiData = (query: string) => { - return emojiData.filter((emoji) => - emoji.name.toLowerCase().includes(query.toLowerCase()), - ); - }; + const queryEmojiData = (query: string) => { + return emojiData.filter((emoji) => + emoji.name.toLowerCase().includes(query.toLowerCase()), + ); + }; - return ( -
-
- setOptions(queryEmojiData(query))} - onChange={(result) => setEmoji(result)} - optionValue="name" - optionLabel="name" - placeholder="Search for stuff..." - itemComponent={(props) => ( - - - {props.item.rawValue.emoji} - - - )} - > - - - - - } - > - - - - - - - - e.preventDefault()} - > - - - 😬 No emoji found - - - - -
- {/*
+ return ( +
+
+ setOptions(queryEmojiData(query))} + onChange={(result) => setEmoji(result)} + optionValue="name" + optionLabel="name" + placeholder="Search for stuff..." + itemComponent={(props) => ( + + + {props.item.rawValue.emoji} + + + )} + > + + + + + } + > + + + + + + + + e.preventDefault()} + > + + + 😬 No emoji found + + + + +
+ {/*
Emoji selected: {emoji()?.emoji} {emoji()?.name}
*/} -
-
-
-
-
-
-
-
-
-
-
-
-
-
- footer -
-
- ); + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ footer +
+
+ ); } export default App; diff --git a/frontend/src/components/ImageViewer.tsx b/frontend/src/components/ImageViewer.tsx index 2fffb5c..8efbbdb 100644 --- a/frontend/src/components/ImageViewer.tsx +++ b/frontend/src/components/ImageViewer.tsx @@ -1,37 +1,40 @@ import { createEffect, createSignal } from "solid-js"; import { listen } from "@tauri-apps/api/event"; import { FolderPicker } from "./FolderPicker"; +import { sendImage } from "../network"; export function ImageViewer() { - const [latestImage, setLatestImage] = createSignal(null); + const [latestImage, setLatestImage] = createSignal(null); - createEffect(() => { - // Listen for PNG processing events - const unlisten = listen("png-processed", (event) => { - console.log("Received processed PNG"); - const base64Data = event.payload as string; - setLatestImage(`data:image/png;base64,${base64Data}`); - }); + createEffect(() => { + // Listen for PNG processing events + const unlisten = listen("png-processed", (event) => { + console.log("Received processed PNG"); + const base64Data = event.payload as string; + setLatestImage(`data:image/png;base64,${base64Data}`); - return () => { - unlisten.then((fn) => fn()); // Cleanup listener - }; - }); + sendImage("test-image.png", base64Data); + }); - return ( -
- + return () => { + unlisten.then((fn) => fn()); // Cleanup listener + }; + }); - {latestImage() && ( -
-

Latest Processed Image:

- Latest processed -
- )} -
- ); + return ( +
+ + + {latestImage() && ( +
+

Latest Processed Image:

+ Latest processed +
+ )} +
+ ); } From bf07c18fd71d3d786385041a95931cae7ecd63b8 Mon Sep 17 00:00:00 2001 From: John Costa Date: Sat, 8 Mar 2025 12:30:16 +0000 Subject: [PATCH 23/30] feat: sending base64 image to backend This is silly, but binary is apparently hard to do???? --- backend/main.go | 20 +++- backend/models/image.go | 8 -- backend/openai.go | 5 +- frontend/src/components/ImageViewer.tsx | 8 +- frontend/src/network/index.ts | 140 ++++++++++++------------ 5 files changed, 93 insertions(+), 88 deletions(-) diff --git a/backend/main.go b/backend/main.go index c80aa1d..3f04e3d 100644 --- a/backend/main.go +++ b/backend/main.go @@ -1,6 +1,7 @@ package main import ( + "encoding/base64" "encoding/json" "fmt" "io" @@ -88,8 +89,6 @@ func main() { return } - fmt.Printf("%+v\n", image) - imageInfo, err := openAiClient.GetImageInfo(image.Image.ImageName, image.Image.Image) if err != nil { log.Println("2") @@ -189,8 +188,21 @@ func main() { return } - image, err := io.ReadAll(r.Body) - fmt.Println(image) + bodyData, err := io.ReadAll(r.Body) + // TODO: check headers + + contentType := r.Header.Get("Content-Type") + + image := make([]byte, 0) + if contentType == "application/base64" { + base64.StdEncoding.Decode(image, bodyData) + } else if contentType == "application/oclet-stream" { + image = bodyData + } else { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Bruh, you need oclet stream or base64") + return + } if err != nil { log.Println("First case") diff --git a/backend/models/image.go b/backend/models/image.go index db533c9..ce08240 100644 --- a/backend/models/image.go +++ b/backend/models/image.go @@ -35,8 +35,6 @@ func removeImageToProcess(imageId string) error { stmt := UserImagesToProcess.DELETE().WHERE(UserImagesToProcess.ID.EQ(UUID(id))) - fmt.Println(stmt.DebugSql()) - _, err := stmt.Exec(db) return err @@ -111,8 +109,6 @@ func GetImageToProcess(imageId string) (model.UserImagesToProcess, error) { id := uuid.MustParse(imageId) stmt := UserImagesToProcess.SELECT(UserImagesToProcess.AllColumns).WHERE(UserImagesToProcess.ID.EQ(UUID(id))) - fmt.Println(stmt.DebugSql()) - images := []model.UserImagesToProcess{} err := stmt.Query(db, &images) @@ -138,8 +134,6 @@ func GetUserImages(userId string) ([]UserImagesWithInfo, error) { id := uuid.MustParse(userId) stmt := SELECT(UserImages.ID.AS("UserImagesWithInfo.ID"), Image.ID, Image.ImageName, ImageTags.AllColumns, ImageText.AllColumns, ImageLinks.AllColumns).FROM(UserImages.INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID)).LEFT_JOIN(ImageTags, ImageTags.ImageID.EQ(UserImages.ID)).LEFT_JOIN(ImageText, ImageText.ImageID.EQ(UserImages.ID)).LEFT_JOIN(ImageLinks, ImageLinks.ImageID.EQ(UserImages.ID))).WHERE(UserImages.UserID.EQ(UUID(id))) - fmt.Println(stmt.DebugSql()) - images := []UserImagesWithInfo{} err := stmt.Query(db, &images) @@ -157,8 +151,6 @@ func SaveImageTags(imageId string, tags []string) ([]model.ImageTags, error) { stmt.RETURNING(ImageTags.AllColumns) - fmt.Println(stmt.DebugSql()) - imageTags := []model.ImageTags{} err := stmt.Query(db, &imageTags) diff --git a/backend/openai.go b/backend/openai.go index 49644ff..4a90a34 100644 --- a/backend/openai.go +++ b/backend/openai.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "log" "net/http" "os" "path/filepath" @@ -264,6 +265,7 @@ func parseOpenAiResponse(jsonResponse []byte) (ImageInfo, error) { } if len(response.Choices) != 1 { + log.Println(string(jsonResponse)) return ImageInfo{}, errors.New("Expected exactly one choice.") } @@ -298,8 +300,6 @@ func (client OpenAiClient) GetImageInfo(imageName string, imageData []byte) (Ima return ImageInfo{}, err } - fmt.Println(string(jsonAiRequest)) - request, err := client.getRequest(jsonAiRequest) if err != nil { return ImageInfo{}, err @@ -315,6 +315,5 @@ func (client OpenAiClient) GetImageInfo(imageName string, imageData []byte) (Ima return ImageInfo{}, err } - fmt.Println(string(response)) return parseOpenAiResponse(response) } diff --git a/frontend/src/components/ImageViewer.tsx b/frontend/src/components/ImageViewer.tsx index 8efbbdb..cd1a974 100644 --- a/frontend/src/components/ImageViewer.tsx +++ b/frontend/src/components/ImageViewer.tsx @@ -9,11 +9,13 @@ export function ImageViewer() { createEffect(() => { // Listen for PNG processing events const unlisten = listen("png-processed", (event) => { - console.log("Received processed PNG"); + console.log("Received processed PNG", event); const base64Data = event.payload as string; - setLatestImage(`data:image/png;base64,${base64Data}`); - sendImage("test-image.png", base64Data); + const urlImage = `data:image/png;base64,${base64Data}`; + setLatestImage(urlImage); + + sendImage("test-image.png", urlImage); }); return () => { diff --git a/frontend/src/network/index.ts b/frontend/src/network/index.ts index 9a31eb3..a6213c3 100644 --- a/frontend/src/network/index.ts +++ b/frontend/src/network/index.ts @@ -1,97 +1,97 @@ import { - array, - InferOutput, - null as Null, - nullable, - object, - parse, - pipe, - string, - uuid, + array, + InferOutput, + null as Null, + nullable, + object, + parse, + pipe, + string, + uuid, } from "valibot"; type BaseRequestParams = Partial<{ - path: string; - body: RequestInit["body"]; - method: "GET" | "POST"; + path: string; + body: RequestInit["body"]; + method: "GET" | "POST"; }>; const getBaseRequest = ({ path, body, method }: BaseRequestParams): Request => { - return new Request(`http://localhost:3040/${path}`, { - headers: { userId: "fcc22dbb-7792-4595-be8e-d0439e13990a" }, - body, - method, - }); + return new Request(`http://localhost:3040/${path}`, { + headers: { userId: "fcc22dbb-7792-4595-be8e-d0439e13990a" }, + body, + method, + }); }; const sendImageResponseValidator = object({ - ID: pipe(string(), uuid()), - ImageID: pipe(string(), uuid()), - UserID: pipe(string(), uuid()), + ID: pipe(string(), uuid()), + ImageID: pipe(string(), uuid()), + UserID: pipe(string(), uuid()), }); export const sendImage = async ( - imageName: string, - image: BlobPart, + imageName: string, + image: BlobPart, ): Promise> => { - const data = new Blob([image]); - const request = getBaseRequest({ - path: `image/${imageName}`, - body: data, - method: "POST", - }); + const data = new Blob([image]); + const request = getBaseRequest({ + path: `image/${imageName}`, + body: data, + method: "POST", + }); - request.headers.set("Content-Type", "aplication/oclet-stream"); + request.headers.set("Content-Type", "aplication/base64"); - const res = await fetch(request).then((res) => res.json()); + const res = await fetch(request).then((res) => res.json()); - return parse(sendImageResponseValidator, res); + return parse(sendImageResponseValidator, res); }; const getUserImagesResponseValidator = array( - object({ - ID: pipe(string(), uuid()), - Image: object({ - ID: pipe(string(), uuid()), - ImageName: string(), - Image: Null(), - }), - Tags: nullable( - array( - object({ - ID: pipe(string(), uuid()), - Tag: string(), - ImageID: pipe(string(), uuid()), - }), - ), - ), - Links: nullable( - array( - object({ - ID: pipe(string(), uuid()), - Links: string(), - ImageID: pipe(string(), uuid()), - }), - ), - ), - Text: nullable( - array( - object({ - ID: pipe(string(), uuid()), - ImageText: string(), - ImageID: pipe(string(), uuid()), - }), - ), - ), - }), + object({ + ID: pipe(string(), uuid()), + Image: object({ + ID: pipe(string(), uuid()), + ImageName: string(), + Image: Null(), + }), + Tags: nullable( + array( + object({ + ID: pipe(string(), uuid()), + Tag: string(), + ImageID: pipe(string(), uuid()), + }), + ), + ), + Links: nullable( + array( + object({ + ID: pipe(string(), uuid()), + Links: string(), + ImageID: pipe(string(), uuid()), + }), + ), + ), + Text: nullable( + array( + object({ + ID: pipe(string(), uuid()), + ImageText: string(), + ImageID: pipe(string(), uuid()), + }), + ), + ), + }), ); export const getUserImages = async (): Promise< - InferOutput + InferOutput > => { - const request = getBaseRequest({ path: "image" }); + const request = getBaseRequest({ path: "image" }); - const res = await fetch(request).then((res) => res.json()); + const res = await fetch(request).then((res) => res.json()); - return parse(getUserImagesResponseValidator, res); + return parse(getUserImagesResponseValidator, res); }; From 53ebbb6e8d2a3852697157a764a2334004afd0eb Mon Sep 17 00:00:00 2001 From: John Costa Date: Sat, 8 Mar 2025 13:13:05 +0000 Subject: [PATCH 24/30] feat: sending images and receiving them is now working --- backend/models/image.go | 2 ++ frontend/src/App.tsx | 15 ++++++++++++++- frontend/src/components/ImageViewer.tsx | 6 ++---- frontend/src/network/index.ts | 7 +++---- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/backend/models/image.go b/backend/models/image.go index ce08240..b3f0208 100644 --- a/backend/models/image.go +++ b/backend/models/image.go @@ -24,6 +24,8 @@ func SaveImageToProcess(userId string, imageName string, imageData []byte) (mode stmt := UserImagesToProcess.INSERT(UserImagesToProcess.UserID, UserImagesToProcess.ImageID).VALUES(userId, image.ID).RETURNING(UserImagesToProcess.AllColumns) + fmt.Println(stmt.DebugSql()) + userImage := model.UserImagesToProcess{} err = stmt.Query(db, &userImage) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 97c3a6f..007d8b5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,8 +1,10 @@ -import { createSignal } from "solid-js"; +import { createEffect, createResource, createSignal, For } from "solid-js"; import { Search } from "@kobalte/core/search"; import { IconSearch, IconRefresh } from "@tabler/icons-solidjs"; import clsx from "clsx"; import { ImageViewer } from "./components/ImageViewer"; +import { getUserImages } from "./network"; +import { image } from "@tauri-apps/api"; type Emoji = { emoji: string; @@ -13,6 +15,12 @@ function App() { const [options, setOptions] = createSignal([]); const [emoji, setEmoji] = createSignal(null); + const [images] = createResource(getUserImages); + + createEffect(() => { + console.log(images()?.map(image => image.ID)) + }); + const emojiData: Emoji[] = [ { emoji: "😀", name: "Grinning Face" }, { emoji: "😃", name: "Grinning Face with Big Eyes" }, @@ -97,6 +105,11 @@ function App() {
+ + {(image) => ( + + )} +
diff --git a/frontend/src/components/ImageViewer.tsx b/frontend/src/components/ImageViewer.tsx index cd1a974..50c3563 100644 --- a/frontend/src/components/ImageViewer.tsx +++ b/frontend/src/components/ImageViewer.tsx @@ -12,10 +12,8 @@ export function ImageViewer() { console.log("Received processed PNG", event); const base64Data = event.payload as string; - const urlImage = `data:image/png;base64,${base64Data}`; - setLatestImage(urlImage); - - sendImage("test-image.png", urlImage); + setLatestImage(`data:image/png;base64,${base64Data}`); + sendImage("test-image.png", base64Data); }); return () => { diff --git a/frontend/src/network/index.ts b/frontend/src/network/index.ts index a6213c3..8b22a4b 100644 --- a/frontend/src/network/index.ts +++ b/frontend/src/network/index.ts @@ -32,16 +32,15 @@ const sendImageResponseValidator = object({ export const sendImage = async ( imageName: string, - image: BlobPart, + base64Image: string, ): Promise> => { - const data = new Blob([image]); const request = getBaseRequest({ path: `image/${imageName}`, - body: data, + body: base64Image, method: "POST", }); - request.headers.set("Content-Type", "aplication/base64"); + request.headers.set("Content-Type", "application/base64"); const res = await fetch(request).then((res) => res.json()); From 863716c096e87d61558ed90963b6be0fc79a0366 Mon Sep 17 00:00:00 2001 From: John Costa Date: Sat, 8 Mar 2025 15:37:10 +0000 Subject: [PATCH 25/30] feat: super basic image search --- backend/main.go | 42 +++++++++++++++++++++++--- frontend/bun.lockb | Bin 113510 -> 114257 bytes frontend/package.json | 2 ++ frontend/src/App.tsx | 60 ++++++++++++++++++++----------------- frontend/src/ImagePage.tsx | 36 ++++++++++++++++++++++ frontend/src/index.tsx | 9 +++++- 6 files changed, 117 insertions(+), 32 deletions(-) create mode 100644 frontend/src/ImagePage.tsx diff --git a/backend/main.go b/backend/main.go index 3f04e3d..56584eb 100644 --- a/backend/main.go +++ b/backend/main.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "encoding/base64" "encoding/json" "fmt" @@ -134,6 +135,10 @@ func main() { r.Get("/image", func(w http.ResponseWriter, r *http.Request) { userId := r.Header.Get("userId") + w.Header().Add("Access-Control-Allow-Origin", "*") + w.Header().Add("Access-Control-Allow-Credentials", "*") + w.Header().Add("Access-Control-Allow-Headers", "*") + images, err := models.GetUserImages(userId) if err != nil { log.Println(err) @@ -156,6 +161,10 @@ func main() { r.Get("/image/{id}", func(w http.ResponseWriter, r *http.Request) { imageId := r.PathValue("id") + w.Header().Add("Access-Control-Allow-Origin", "*") + w.Header().Add("Access-Control-Allow-Credentials", "*") + w.Header().Add("Access-Control-Allow-Headers", "*") + // TODO: really need authorization here! image, err := models.GetImage(imageId) if err != nil { @@ -188,17 +197,42 @@ func main() { return } - bodyData, err := io.ReadAll(r.Body) - // TODO: check headers - contentType := r.Header.Get("Content-Type") + log.Println(contentType) + + // TODO: length checks on body + // TODO: extract this shit out image := make([]byte, 0) if contentType == "application/base64" { - base64.StdEncoding.Decode(image, bodyData) + decoder := base64.NewDecoder(base64.StdEncoding, r.Body) + buf := &bytes.Buffer{} + + decodedIamge, err := io.Copy(buf, decoder) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "bruh, base64 aint decoding") + return + } + + fmt.Println(string(image)) + fmt.Println(decodedIamge) + + image = buf.Bytes() } else if contentType == "application/oclet-stream" { + bodyData, err := io.ReadAll(r.Body) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "bruh, binary aint binaring") + return + } + // TODO: check headers + image = bodyData } else { + log.Println("bad stuff?") w.WriteHeader(http.StatusBadRequest) fmt.Fprintf(w, "Bruh, you need oclet stream or base64") return diff --git a/frontend/bun.lockb b/frontend/bun.lockb index e402c1c51e0d0e40ac66d671689f35e4d20ef6f3..e34de90cd66892fb9cd8123c6ccca40d666e1840 100755 GIT binary patch delta 18672 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�-e&d! z4@$D=Pa+9EW{lN`W8;mcqS}L>@HSPm~+RE@(7TSO>l)_BF5UXVE8G z$&s-73=1J8%Wb9+_yn3+9w4`5US%~K(vU{o$CCkuc`mmnTlDSmj60C$`NbNhL!zP^ z@fzF^wnL*k4z}}P++(28VwP{s`u5na2Fi1=6jBDiofOM%@Sp(}U5^x=Gr-K=% z!K#K=rdxDf2J=oCX8npGk~Ex$WW?%!fizT>Qijqhp}1Eejgck8FwHI-l45rh(jYmt z#c=s%jvRJKsdC&wNOCDUpAo!juvzatlGg9-!Lj;6Na?b45>lEhd5w~!6j>TyEA4~S zUyf@qT9T|J=?0AERYSX#x=ZrAmc+mw05Kk5WH|(^1F)IPSABLuOgPX0h>}zPGp2Ge z-pwT{2)mkG5Ij(l__<-83DgX@n0yhFj>(s=f5l{n?L}g|>$EUy-kRTq-BPXXK068Ngh~7cmt$TGkXM zJrkgYX90Lkk#YdK>NAac9zgb!RL%#}RmY`u5e*?uQB;|!f@!KwOa(p(P=PZ5y6Q8< z&y-^oru?&2y*`s2RZHwp>&JRgdh`^SE@EoX0)Q%B1W<qU@#eS`n9uGIgh6;dPHA)wti2uyR@S#5cJruY!V(?%2lCNDRs zcEscxabSv~uRe4UlU?^xHKIG1M0rDCo0?9=Ps7KSm zq?f4tB$zVHq)3%rz|TT&#BY!3B2OV#Q;DhH%T%41N?iq}{d9vG|L-uB^RjABOs#lD z)$4JoJo7u$g!y&on}7A-B5#ZT z%7B*B|7ZGNPc}57e?8g!^<+bS@PB*q`QwvK7p0(o`^jc^OkQ$_~u}Yzhy0$;Ll}w)iD0o5n9fTd>T9$1k(? zFHxE*hfWK6e^S$93(wlP=}@<^6pEMdy&UW0ruCD=Aj z8j*G8J-mF<_2$iIE?paUb@+gA9wNN>OUtZm2EV?{#%~B0-b+~7EWSk8`10j0 z{1<4YJYl(w_gUe>H!rucIlKniU1;emtoTm4VTFxvS?R*{E3NElp0?7)2d{GBd!fze zY?X~WuXf?ttE}u8~0l4!l$gUvgdgPv{TU9 zt+lcxykM=3PhaQ4KZeG6;5r+7k(VHRiC;jtj0df^F~R2`T+S~dT){&(*znQ#S%jwYGd1YCc^FfAi^Eob&HMd{C8xA3S%z3;zb%MIO4}#-k6o@R#;m+2{N^ zv>VWR9k8-1e8~ZL+(CF8v@dwVL3rFDc-%oNyUuH%-G!EZ$jZLr8xEm=Z=io~SlKr` z?G5zrF!~4WJI)THe@D>2!&X+w%b^{B=6=MA-yAZJpnq?of6#7o*Ei9>qv+q8R`xxw zfOZO6yQ5ZoN-j8x{=J3%LHmgZzJ>l(pnq>!*)RM8w9lYLR#@4ud`<=WcMSc5c8`Z1 zL;sGWf5)u&@$Wjc8_;?kx3UL($#L}WZS)V?BcAX!`ga2Td)umGjQ{v{2EPk6{e+b@ z;2Tb$gYTe&?^y8%7isUHgD25JXpJ~Ki4LAZ2Txj=ftN!&0L}fBl|9BYPoaaS(Lrb~ z-1Rg%cm^FjZN=XcRGhZ4X59OXjk)pygw6SBge`dByEf*=OAxyA3kW@U&{-Svu z@rww(dFVMC^Wo1T^ySwP`tg|eY^)_;f)IaUfG~h3yl-Qz_zHx9yar)w-uJwXwc#7i zf2c3SdqiViaPRb}Pc>CX-2SEKE`@;b+sFzd@{`uDI& zQy+fXee0`ROdn=b+DSh+JMqWA{aWuj0^ihN@Zs)L-wibJ$rr~o9vp~FxcamfZ39@e z`uhCvk#bRruY3zoJ{;87>wkvuQ*G#F>?x%{sihi6-%SE2BQF1XWp##5rU7akeURu; zN-?-vsgd;7S18Be@66OVx-B}?xYmfHO6g1CQvjvWXDa-oS&`#&uBR zrc)ufybvM$+Of7`dMlP(idm*VQIifM^&oHvcoU#+7e|0MfCIoTU?;E}C!0JH}>0&RgHAQgkI(BXE{Z~#AH$zPZ@B7PG|G;Gd(~l)9fRzAtCuuTJ0OSM30IlO3U?PwUM4=q| zL~H^w02?p}Fakk9cfbxzq{Wa6ArH8WjGqF1fknUqU=WZ2bO-1cnJK_jU=H8_W&?S^ zDCDOvulYbf=+6V`z;vJlm0v;G79Mr?h4Rn-&tVV zf#w0U!z=?{1nA35pAIm$uVgc`vu^@Qh@4v63`l^b*gy( zly)phS4uj88b3(|l|h}QI%#%jaHu2Ht~tO=fNZI)#AHkC0I0JBEsgp>13~?vK_NSW zDwk@)Kk#!fqM2C;JPS|*C=(S(1!#qm{wD>?u2ruhd6wu0JesXffEwUq+5kU<@HTK9 zpxYH?qS4z4(8$xsMgZb^N7mW!Xa~gYq&^tL83&A4buEtUKL9=gDgkt?R4)7mlrMqr z0L}PY$W#$EK&$K<$X^3r0kj?{&o$r@a1rByqRsaa zEu@ElC-4CH6`(1k-S{_vT0a=L58MOpih|Cph5VB_nYjRu1LO%4!4yaPauAsIY1+T3 zSlZBObEi$8ZUh9=wQ~Gc{ar&)Bf^V$3}KFhDI90rG=t2gn6@dVnZBj5h!Pq zy%+U5POm?SOi4K@&V{gW5AC#x_E*&?&SVS~=i`{0Xl7)M*)`#7WS!Zs zBFV_Y85c8*>{DOuz)`5><#sdYy80kjxG~&_K@SjlU6{A8c0#G##sBMP7ng;=Ak-LU zMBQOx0}NQ4VCk^aPD1&Xm431FyS8N! zL|_yODi-lwVfU0y`SbE2+uBsQJU#U=?4p!{UJwN^VC%&qqTOP1SM;NmxJVQrTteA+ zr7>;|L#f^f@qDOkR~8C`NKt^dg@$s4eZ{Y#tiP{zDr{5E)F~wohqXjaVa8BYCcP|1 zg(2%sQ4)r%H^h!G)}K|1-zajTa1Tdj?SxnQ=v$BOzmR!H&Kyql=80M1%sWy$>g6;z zY|y!eTTj3sQt5|w_^VTwKEK}`=Jq2}Oh~~<+!gO4tFLz2>uB(*OR?!*{a`?2j%AW6 z9)+Vt^3Z#Tz7fpCPK!AaXx3e^3gnA}Ve-L=`Hh0s_TTjSt8&&*V+=Lx{Rp;-rHYA> zu+vV8B{tplYVqRp3(1{~p~`Hmi)7)x!OfII4r8C&mtnuWwi*S6DUG}%Zc_<=Isjdb z*jd&2Oc${#kz@Tf}5nvuGw<7^ZQC!zNB7d2!)SUhF|Ws7zu z?9MxzE8S5N#oN)Wxq;l8{O>oh7?zRRNwWammm|}vt0$wx7&H{^(9Whk-E4Sn?mV_y z84L0aFY!6D`f5krwm$yJ!$&t)bmbMnFB3=m%+TcmdAZOGAT{pzc?JF52Z4ec15E$yTJBYr%6y{^W6A|wV) z($2+gDS4P5zTP*%Ngf}Xm=uvo28GxJFg1?5_h(EvHu%NOIyMY3sNeCFNQq_MDI1`| z2^?dG96z?U>3Y46J%<=~#Arw3(m%Y__R0KaLr{6NGUsPufsN7;+PLOsr`~Q(I(7gt zZis0i9#E|*P_c_T9^|rJlUpZVLe>Z+>r*1M8%AoVuoB6dhZA~wO_U-cQaiV|JwJ3$ zZgtjLWW?@5ex@DV8&l9WzWZZMqme>`imYwKIb`)!kN7zTbUC)s`^<4S7(^*`rU-_f zqB82RuXa>$C|mt`?_bxE1E?E@Vi0)^Q}Y-ae{1Z}^eb_1CBh&^*}}BLfJ5Gi8d%lu z-YukbMG9KCL#&~)wbOt1T(c)Wj5xOf2BAjuysLCYT%b1e5;uvC34I*vuWJ%02F0;3 zW#?8maBr3&-iTx2;+UCv=!{X~Rvhz-RL|}?GSfa^{pF?EI5&ZUBjxR*tumbp&RlTa zFD3^dB^*_vP4h&2Jldq4()*&(lJldl95Ts`j-jmEL;(!Ym_=mw6$&Cpkd|M4(7&{S zS+)x`M&nUQ9F1q;x{P+>=Xe&T8_`yDG^5p@ieYACzagHlBQn~GL|yX^;;NZ>MQLXT z$DaQ2hxy@Kv2n-^NAtAPg|>ueJj?ga9Ie)el$;J?rv;nVNDK4Psmed&DD9A8{Kk*^ z&WP2&Ef*7EG~ulV_iPVz=*rQ{tH+^z(uTurL|JXdfFNtei&setWY$R|Y)#TJCl< zb*dy-R3k5&FBz+up7Bk)`nTykZ{gDN-HNqNYSwu+{`u%5M(k@kaeTOu0f zee1-qU?ONc*3M3Tx2*Wb4VF7^)TLY#Bak&pJ8T)1e0JZ4rF)*NGtiD>cI~lp)c%G| zb#*D8MzOsY^YYUUa4vA#K4Fw=x9N2j@kVhC7E$V%Psgc7siOCdykm71V~xsI=5_ym z=)f~k*>x$?jl$R)z1Gf=em-|x@6ctJD(WoOi_G3w5_`l9kgs+QmB-#{le2i`f;zj) z;x!mVX$M!=`gT3^VP)3PIs<(dWe*OX=-KF}<2k+SQhd6IhiF8Ub{N*hGqG~*&Z#Hs zEVSdY+j`qa6k0m0u1gu-MIsSS%i7@PavPnat$ z??RE%2OgpwVU7M_MoN=r^DopHXop-EJ^x_!{x+^vbtzS14YEdQr((ZNXy^P(&&0Ru z4778zBTg6eO6)i5gSwQIXmN|$rJb~0I>&Fw?5V$Wth3-EpfB2`oz1m0N*h$uuo zxjWy?nemLFThuyb6^9$6q-ya!iqbVs5R+5!MDE{@`Rh^>gt;GfqlpRP1uIxh)JNiZ zXtP+<4|fzL%8UIhmJDQWjl!ak#CnP*F!%fO%E+Qw&t(HpUUpA09ER#6x>`-x-<8== zE)x%Hsqm94!Zo?Jm0E-TSi4fTSenct{Ir9%i5EA05;d{>IeA+PkHKb*ZS8Mq%tPEw zW@8#p?5*r7f9y=4T(NeBH|fjVUY#oTA5=!HUN_lh@os-i!vEIWNbOYc%^NFMKGEfP zGD=dP(zJ8K(zrL@zWqYMWjRH?&G#RGZl4w72Vi*wh*i*ZX-VRT47@AQO?O0+IFF%> z)DAM=+?Kt0?FZ}jDb1vb(vB*JFU@+gV$FIy92WLby1PiUOu??9oo(Llb>i~*^ABnN zmLGItr0pUt1=Y%LP;TAimlo|?e*W_Jo6cU4ESfQozVc^m{O1?Bx%Q%B=|{2a6Q7xZ z^wvO6QrLNASMg@Dhq*VTHfau}&O!rx&(h|Xo?0{y(5ar}TMf7MgZqxW_=9o4dY@-? z4A0on@LK7XI0C=WJQJ@^CdsM4_%RjtH0^Zt!;gyJbUb)Yd3wf6gnUoyo(2o;T=lHO zPdH*L=3l8BG^Jr)evK29=Sq(j-zIll(5FJSkZab~0@bKEzxY0l>3tWZC=UUTFYPtR zb>3%5y4pL`TG4bMHWBsEwxi{eev`7+^J#Ske_EOREUH!(K$ZP-E4dlry0|p)&Okg7 zC8vq|V0Cpxt;c(73%ZrwJHGd@)0ma$3cc^)7O$NM|E7J_rEeog&XqeUr(~yznNMJW z_7yKZ!S=EgVI2gA*_@^fO8ofqV`t9KX$$)>I0r_yT$I8fQagbD^_8@f_S-E-l7XBq zy@vb00gb5lh_Aj{Do^JAn4OeAdzP11gY1aou?+0#Y`m{_Abo1!xmnMB6L4Ft1s(Db zH`C#_OGQ8i3-{9wv=2WKe8YQIhXJtbs(eE5%@C6^FjDO^#NiC=M=JYiN7$qLov996 z<%7Gq{5Ao1Q4f0(;>ft$glk2 zzS_CQdr{h~a~=eQW3Oznm}Sm47^_ zNu5Q>5b**mqS8kwzW&X<@QGiyX@Gkz-jwOVPtuOkZ@=UbS#f6oZn9d+oDt$G^7?9r z?=2x^&N-L-zpt~{C0vJKoKA_(Am8h#!xtWNXl1rn(u+CA>+F6PqhQdu>u}}6)epnP zQ$tv9xOj1VDE{0pw_=OQywxuPoMF!ZCfjEovY%_ zb3D_VJaUV;+Qe0K8_LW+dsCJ8dC`$)+4Lq4Ta8faIe0`Y9m+N~!T;oAADdIur7(X= zadzSU@L_CppsV_kt4l~1RT8tOv3C3UOtzAVYjc_2yEX@Gl&m4jS2d9~ng#3+p3S<& F{|{p~k)= z>R0=(AK1?iZ8h7L)UjvxkgF?)6*o%g6nZu9$mI?{t_aD$HQ`o^^}T;BKDUcWbTuup z`WJfptAZqr%$Zs+DQD>DXL9oMo*p+zs)CA#B-x_C^}uTls_arnlIlYqT2PQ%FjSIC z5Z?&$_;Cf35GMuNNs<%fHi&lwkIfl7E^nA5IYX8qPt6&VPt_Meb}mFj0RmKDuErB{ z3&!O?lPj&Vmn470F9cITv%sV`GN)kNvtuRcd|kDo_FDS*yx~)+;2_AZ(6eggrGTje zi5lk>v>A~n9{v8 zeZ7ZuAyfRA+=(M{BP2<$_~BFNG2(Zj60%ss`X~l*wm~SZ0r)0oh88kMkcw~2!wvrI zS)OL_$jWG__F>Y<+_BV6lO`vFsma4#Bnkg*O2=RWFBrfbvJ9pw&wWuML8lE>cZ+uQc!F|M&d<5~-J|9oDWw*5Wt&pkocfiz&)n2N80hkPB z@m4tjGWGLw$mCL0V5-kfCrurnD-F#rm_lB)gRF`y z^TA|`Rhm2vOhvggQ@bz%GK_5N4PR_c zYS5$t%y-F7lD=ui9gTj4##xH=W{4pA5fyn?}G!ECevBt8- z_uHs?mo+}6a-nS(0yI6|(RhKzMH-I-ljR0#Y|*%##w|3SI4(bb@_0#F8YfBc7uyUl z{L3~BOor^H#fO5)AkJVvsZg@rij^c7$#xPPPDjM71a)X`fT@C0V9GEWOa(UW zr261oFwOcaU~>BbNGIQ#0GSFNrLNG@j86QKyuB>qz#WQ4-o(M*rDK|Et59$Q#5Uz=4*rE#daH$=0h8ru zeSLar%|ngWQ<6RVsd^2-Wa+#SW5-R*m82j0sv}d0{1iXDpiSf z@^cqMCKK<}>VZE}dZEqBsy3%Sm=x+GftvRqQ?ghhml0W$HjReGu433V{4A$_PiuHj?1oQHijp=n&mZh zB&jEK?YIkc@9;{bL~z;7%qH<<-1qTfH?!dfJAT~F#6r02Zf4m$+1)I!FiKJqGTQSr z_gHqBS0besmp#n#K6^|5RHr3%S-m&sXNR+oO<(0QUqCOfam;3}$ zdo>NFYR8Lx&GKY;48<9E6|AtC%Z<%U=E;rCaz{)IYMGIG$ENX0q@3cipIP>ULnR;u z_CcO8yx7mo%6O$8%v_&4`kPoFPxd#n0le5B;{(r&Qz~$294nuPL=7@oU% zDbIMr6lXri-%N5GKjjwB8gRLZnZ3f3asQkbH!&La;4)SlJ&dKG_V`haGdc+|0~&^WzTw${;d0GxZUTfYJLxicqx3NoH0{$iuet z%0M&o;&PB#?&qQAs;juqtB`0QWM1VLD}Mq>tqBb@wDRPRt|oa5<`Su3q=RDFAzs|V zEZbwAQJe$$k9=H{F#3=hc&omEDT7|7Ym({%g7l)WxDz6MN%d4=YQA!;$r|~AFuG|T} z)Sef&G0UR^Bng`YnyV;ti6$A9S$C8thnkJ`n`k+V{SboYb{BgkrQr-8lLCLv1Yj- zL^T%{SGQQh_aXdvtjXxx76#&{V&jdY5K2)(A5@17Fs~ZdvpTf2I`lO{=pOQhh3lc` z5bCbP9jFdDV&QHoyBt>(lgbsFCCI;|SQ@pWZJ4wn?LZcB%S3>&{Qd51}OHyAYZY)9r zmCy-<@{~|iyd>rDxuyU%)D9q&tAtu~l%&B*=;i9rHH6e0oiO#( zrYu89)q7MO*VimbR-T&J!2to(LW?HW!fTLV2l5xcSgdH3oy`WGz#WrJ%#kN2nT_KU z;krCEDc*Ptq5euJ93Gayos!}WD?9NXT}<+~h(Nb!iN=Z=2si3P$;Rgq>Odj60wIhV zO>21hJxEFcERt7t#k9tBpj0el@bwjt;CPsUnB<1aBtEB`i8=AgZf0W=c7iE9H8kG% zK0yPJ)U-S8Gptz#p(oNP91>c)E{n`ASd2>no&`fy(k2}6fgU=|rZ?#>;1 zn3xAo?qOzmyts#1em`05h*5b2d;RqdLp=ClXNk+HX5*XC@5AT%#>@8+qN4E(X&vW)-77T` zTcIdLp!tj^ZR1$un@H-T3_$}dQ5k%0_js1eD|?y^Ka z6a)D8QTyPaJc!aDVK!r9;etnYk(P!^u)>u=Qdc1tJhJXXq6c$b?h+8kEBlz`34PTy zR@E+tggIubujCEHa8X{Yhgcm)KqAkA(eWr;14*?L$~ArhskdVMz;snhR1N|_EVIdcZi{$Bc?NgPFv%^gFaVU$1nil{^5P7$ z;XNxqo?$Y&XGu~vKa~-0oQP0=C3F&@OeNI3za%}agkDBS)w_a_s@Guvd{IfAk5GmZ z`Whkh0p-PHYo!=OHlLGaGAf;9fIIbIvMMS}1x^8|z^Ph%ZKn9= zw0L65|GXyGX40Du5YN!kiAmq4)sJZWx`-)$E(y45GwHns5YGeX zA}0OU0gC4UUBnc>hy+}Pdf?BP6qf+h(q#Z$e}^gmN`M-$3ZUx`T&P5l!fHyzwHhFK z4L}vY3D8AM@oNDY*a%SiW{pcVE(6m=O!`{^()$1)=iLoZ{2stSR@}yf#yOFyd+92?vu`M`?P*Hiy(%0Nx8 zHd8t2IY^+2hiVGMB^IX#6agN}sCbB6foO zGMGxAtHu8r*WuHLwk@Pm7ek8{%axk;f5K#}HJUy#wc<@puFcenceVK1?1=cSOnY7+ zpj`hArX^;NR^I>3>U{aP4Ah!N8mKH+fS6{ZJMdrI&1m)iZrekqi~-1>+5?K?gX#Kj z?`GT*Q7HAS6P1pO*uXbt`~S0(QS4Db_5l9b$t+Urp>B1^CI8vU{Iiq!J6o-Pb}}@9 z{@Ka=vy*`v{3kn|zqga2T`sQbEl=6QF)1zhiiH+dz;76vST38{^ zS!(4DOP%;(NJZRbnU(K=G?`m3uCC;-5g8#ha|K@?(%@ zuCTCTUIl5&3MU@D(t_XcidI^A%au<28l<^Ae3g~|3(~?>7B-JxhBSMX6YsLx!b*6_ zYAcUh?Zodx;yiJUl`Y^)a9_yp;=YI{zhPy9ufcsWe~9}Mp7y4dE#>QRU&h&5D_hR9 za9_boabL;hx2$Xx&%u2)---Jg?((*ky}|Qvf0I|>zLtBvV`Xpg3An$_D{+5^H(6(8 z>-bdM*Yhge-{q~=Tk(r)5$^Bt^SE#1;qO}6CSHvDW_}s>_j$VwR#wVOaNojj;$FrR z-?Or`U_yKSY?mPHH+;{S{O;)yxug85iXPd2T56{AVFE7P?AD7>^ zvT~k-`+mL?_X_S(iV=AqBT{N%2YCggk);@sEf#i|PuPMH*@6*)bd)zK!-zndS!Tfx z=v9!Wlwm}+TG$C*v=t+=6(a)a6c68q5rMRDn}vPIFGHHW4I}b_g;nvA4=^GhU_>CD z<%!$jagg5HZegGByO0)dcjDC%)z&`gaihJ7i%GdDsV;9)i4H=VS!ux!@>P(gRHB2&Evz0dI*tw=M+YG}@$eJqAf$yS zEcoI4@(C+*=Iu^enF}w$y&=Dedn2BB%F0~%65QSRUEJMy@@Xr6!dQd5Cx3{$7f<`p z%Dnk{+O`q$L}#8IX6wao@lS~E?L-s4};;+ku5^&r2b#RY0{Q>YMJ&J=;mv4iN}l#MFf z3GoA9JFpv|2jeba2e1v;2y6h}2Q~qlf%kw?pbXdo(02v;=CBrc3wRrN2UrJ$qpnDR zjQAWd4R{`y4#0@`b2|izfER!lftP@m$Uxgwe*j*mShgA16Tn|v&|fr30YD(o90&rM z0xf`Gz#H%Zt^?PAs{nnj{2aIlTmrrTzNGITYY})0cpG>JSO=^J-UT)QYk+0Ia$p6J zfd*QEOk&9CKz|?;pf5M8fYktf)maKG16BYWC;`mSn+2W?yaE$QuL8vYeKeW_Btrpi zPQNrNzjky1cLM062d0+tO(GuL0muQK28IH|fLvfWFao#-{0i8k%-_Iv;Ge;D!H>Z| z0)09S+?0QCWKd>4TH+6{0A8Ul>~ zPk<&6O)Q!;}(^05n7BbtV&=u$oBm-T56d(~u0y+Vmfl0tb zpa3ufCZHKW-_o}L7l7ly5#SJTl*a!s0+qmaUIhy02w*WV0T>I61I7Ro0h-YxfjnRo5Q%c=dutTX7w8A1(fGFpf`E>|a9|{m2aE#f z<2!wr?*1f~P@!^;$4DzFe}1!MsWP>2N_3gjX@4QvBw?Rg1U0K5*&0}=wEfGaFS^l8e1 zqG`;@Kghov5dR}U>lry{2{0d^`lw!N9Cc>$_L(hMH$x$X3DjV+Fy*5TQav;xWDsh* z4Il$N2aq1MkC=3cX8_bcf}Te0roK}jiU86h=wWc-A0rfb4wPs#W&^VTY5--T0;vGK zP?Gh+Y8OoIR9mKzc?}@`7`OwR1*&KjJAgn1Ko2X*L>7AwcwO8KX0f7WYnJV_05S*m z15u)+HS1Qm3Bip3sp~#}5Me5UT>cP1@=@SJ;It;60G|RV&oQ79a0E#IBtUscXE;Es z49R*N>3;;AA;VoqfI35ke+_&ITmy9Fs|Zs?)BwG*uMoZhTn0V?z65Bo{ul5ma1OW# zd=7jDoCj!9Qv*ro3xLWyLcTyPrUuk*DOGqIxCKx{z6EXqHvrQ82KY`3Q@Y;R9}vC^ ze6PjTPK!hwEsyp9Etj-}I;Mi30KWmh0v^CG0IjJG0IjJH0cw3F@H6le@Bl~$G60IF zo;{Yt(l*Sys~rla`AF+8t;JLxt<$t#)A~&dJAq`f23g7pVOmzH9528@5{6LL)JV&2 z1JNp!`81^En3m=tA-Lh1k|lbEvf%B{hcZ7Vwzg&NY`!?rmU;QCLMjTeRjxj;w5|Qn z8wNHbLdhcS5Kr2&NRbl8yjhhP5yo1x8-j z^FF#fd(aOCHYg%AJQOMVL7Gx0zpKS_*SA4RcxXf@s%Rz(W0<>LH2!WuR75gYaVM1- z;rvY^m={YF@gN`loX?zzqxKH$aq|drM}~$`xmGa_3eoxjqp)!wE-CSt9!ds! zkg_&3`^IVJGkcH{gRJ40y<#twtsipg_3+@fJ$6qog+i37uvXlSKrdZHU?j*{WJR(O zOlhPS3lyJ4qJ4^rtB|9ZzmI-6>8;|`&4PQrVbTghBioB#(wUdYj$%=4mMDor#)V=7 z(JFB`3T2-c52IMB4;@>=VA>p4T)yy9-LE210+|nO%@#S)ER|gso1>XmjDA|F7G51LPLZpkSHuMXr(ezsce@x6$88ddqEMX3BxEg6byfg#s&%enbk5M z{fN}cmv?tw_gnq9;aD^>sE~^)(zA+hV^Mlz;U9;Q)K6xm54rW^(fphTO8a01?D$|B z7|8a!l(5h!-3eG%v8E3^2_+8{YvNGkaPdhT%=)a0dWhkhw4u{_ztVCBii87%w!>pq zbZm#N=tr&k6rbDVm)XM+3YrUKhzadjYk5^e%rsQ3>Wh8tn0JhRQfj|K1 z^P_1a4`eQQ%fM0)gC)>bATC28?{$dvAr8wrcwl9N)kXuGju^!0E&(mR(xrn-E7U_!~D92HH{9KuDyz^#1XX-ir;d#|ej5lM6_@JRW=+uN)-1Mf! zVb3n^LV*nt^%6j1MPLGpG@NZNoD-Rw7?;2r8&Lgg321dcQ9&xX;&!fT^+R>m#Fsrvw>>`u8Z_I{`s+dB%S0BjD2aI+wD4bY zSxfPLC*~QWAGNz!{`v9|A7xacG?YcloPHSZ%AwCvTHY-EKuN*0#6F;~$9-7XInx00Br_=s$w z3BLMav8@mNF3h*B+<{ibhDMNUoM|lry1+B^V`X>VbKZKf=aZ{SpW|p!=m*dSI#0b^ zb*1wGq|hoKi|Hc9bYYR+twU7TS!U~SesbiUYp%=%G2WQsBESq6_^=D}U`gArcVT80 zQ-P#2|lEn}&p%6j^NSj(Mk_zzlBKGLpUW)tVZG zaB&$5tc&=C^z;*)#~#@HpI@8KYxKs5w#n$>3!&;-9WvUZ?!AMfurJlErymOKWBRDI zZ)2BtYEm|aiW$l9cl{XYhOT)7Cz@Lw(*JLtufho%SKFEo`us4DGHD^4-Y?;=8bq4h^wi|)PH6vrD~ z)P2eCOINbo?;;+Ps@{dBBD^=|+3$P8>WD(bM(YPF6(c=|28+hva(bBE1qBk_6 z^`p9xuMd5$V#R7~7VmUZd!ZlZ{Wd-)r!u0ajJ)BY(V?-ClCM~SN?0qgJq=Z=uQ;yy zm-=ywKWs2#c8YLj_%YM74+~(*fc}-A)bawKKR!{g*N9bjZ?y-rW~3?u1`$roH-562=I=>1rQ_hfYz7$n1-!=xZtB)xPR@z707>KUh3{ zPhVSn#jJ~}^{B;v)zUU$D7SvUS{_a1D=zHCjAB4PEUNlx!iTK07~y3N3XvB#n- zT+D+4b?;KGB9ta-aq4Vv74_1YzhP%@5t5E(SN0Y|!0JF~FT^EcBiQQq_0POnGj247J|H;ndm%IoJgrfitKFnA|2&}jXz^ya(`pPX#8 z&mHR8`&&oxgcSZ{x2}zywv$%MbGL&N*P4~WW@SC$wD|FAgpYoN`k5xDroVd4U)`)J zTQTL6K_qJ+W@ch-*N9IIne4L4a(+^$$`e_eCmwu;bYpsRG zU#B2%hb-oq_eVcQUTgSwmQ?H?qjxwW3CfDZ&4K+W}Y>*@0n zUf9&*O0ychV_E7~tJ(d>; function App() { - const [options, setOptions] = createSignal([]); - const [emoji, setEmoji] = createSignal(null); + const [searchResults, setSearchResults] = createSignal([]); const [images] = createResource(getUserImages); + const nav = useNavigate(); + + let fuze = new Fuse([], { keys: ["Text.ImageText"] }); + + // TODO: there's probably a better way? createEffect(() => { - console.log(images()?.map(image => image.ID)) + const userImages = images(); + if (userImages == null) { + return; + } + + fuze = new Fuse(userImages, { keys: ["Text.ImageText"] }); }); - const emojiData: Emoji[] = [ - { emoji: "😀", name: "Grinning Face" }, - { emoji: "😃", name: "Grinning Face with Big Eyes" }, - { emoji: "😄", name: "Grinning Face with Smiling Eyes" }, - { emoji: "😁", name: "Beaming Face with Smiling Eyes" }, - { emoji: "😆", name: "Grinning Squinting Face" }, - ]; - - const queryEmojiData = (query: string) => { - return emojiData.filter((emoji) => - emoji.name.toLowerCase().includes(query.toLowerCase()), - ); - }; + const onInputChange = (query: string) => { + const searched = fuze.search(query).flatMap(s => s.item).flatMap(s => s.Text ?? []); + setSearchResults(searched); + } return (
setOptions(queryEmojiData(query))} - onChange={(result) => setEmoji(result)} - optionValue="name" - optionLabel="name" + options={searchResults() ?? []} + onInputChange={onInputChange} + onChange={(item) => { + if (item?.ImageID == null) { + console.error("ImageID was null"); + return; + } + + nav(`/image/${item.ImageID}`); + }} + optionValue="ID" + optionLabel="ImageText" placeholder="Search for stuff..." itemComponent={(props) => ( - {props.item.rawValue.emoji} + {props.item.rawValue.ImageText ?? ''} )} @@ -107,7 +113,7 @@ function App() {
{(image) => ( - + )}
diff --git a/frontend/src/ImagePage.tsx b/frontend/src/ImagePage.tsx new file mode 100644 index 0000000..2173ef7 --- /dev/null +++ b/frontend/src/ImagePage.tsx @@ -0,0 +1,36 @@ +import { A, useParams } from "@solidjs/router" +import { createEffect, createResource, For, Suspense } from "solid-js" +import { getUserImages } from "./network" + +export function ImagePage() { + const { imageId } = useParams<{ imageId: string }>() + + const [image] = createResource(async () => { + const userImages = await getUserImages(); + + const currentImage = userImages.find(image => image.ID === imageId); + if (currentImage == null) { + // TODO: this error handling. + throw new Error("must be valid"); + } + + return currentImage; + }); + + createEffect(() => { + console.log(image()); + }) + + return (Loading...}> + Back +

{image()?.Image.ImageName}

+ +
+ + {(tag) => ( +
{tag.Tag}
+ )} +
+
+
) +} diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index e02db44..13e8ce0 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -2,5 +2,12 @@ import { render } from "solid-js/web"; import App from "./App"; import "./index.css"; +import { Route, Router } from "@solidjs/router"; +import { ImagePage } from "./ImagePage"; -render(() => , document.getElementById("root") as HTMLElement); +render(() => ( + + + + +), document.getElementById("root") as HTMLElement); From 1bc1b79042b46fd91b64ede7252a3b81d4a89ae3 Mon Sep 17 00:00:00 2001 From: John Costa Date: Sat, 8 Mar 2025 15:42:16 +0000 Subject: [PATCH 26/30] feat: better result display --- frontend/src/App.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fba1292..496a46f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -30,6 +30,7 @@ function App() { }); const onInputChange = (query: string) => { + // TODO: we can migrate this searching to Rust, so we don't abuse the main thread. const searched = fuze.search(query).flatMap(s => s.item).flatMap(s => s.Text ?? []); setSearchResults(searched); } @@ -56,7 +57,7 @@ function App() { @@ -89,7 +90,7 @@ function App() { class="bg-white rounded-md border border-gray-200 shadow-md origin-[var(--kb-search-content-transform-origin)] w-[var(--kb-popper-anchor-width)] data-[expanded]:animate-contentShow" onCloseAutoFocus={(e) => e.preventDefault()} > - + 😬 No emoji found From 05263d10894e421a8a5b25829bbb37c813313248 Mon Sep 17 00:00:00 2001 From: John Costa Date: Sat, 8 Mar 2025 15:50:21 +0000 Subject: [PATCH 27/30] fix: actually searching properly --- frontend/src/App.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 496a46f..07eecb8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,7 +17,7 @@ function App() { const nav = useNavigate(); - let fuze = new Fuse([], { keys: ["Text.ImageText"] }); + let fuze = new Fuse[number]>([], { keys: ["Text.ImageText"] }); // TODO: there's probably a better way? createEffect(() => { @@ -26,13 +26,15 @@ function App() { return; } - fuze = new Fuse(userImages, { keys: ["Text.ImageText"] }); + const imageText = userImages.flatMap(i => i.Text ?? []); + + fuze = new Fuse(imageText, { keys: ["ImageText"], threshold: 0.3 }); }); const onInputChange = (query: string) => { // TODO: we can migrate this searching to Rust, so we don't abuse the main thread. - const searched = fuze.search(query).flatMap(s => s.item).flatMap(s => s.Text ?? []); - setSearchResults(searched); + // But, it's not too bad as is. + setSearchResults(fuze.search(query).flatMap(s => s.item)); } return ( From 5df6c67ee542edac2cf2f54295cb9b2017fe9dbd Mon Sep 17 00:00:00 2001 From: John Costa Date: Tue, 11 Mar 2025 20:29:56 +0000 Subject: [PATCH 28/30] feat: new schema to support user tags better --- backend/schema.sql | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/backend/schema.sql b/backend/schema.sql index 26b9dbc..5a27869 100644 --- a/backend/schema.sql +++ b/backend/schema.sql @@ -29,9 +29,15 @@ CREATE TABLE haystack.user_images ( user_id uuid NOT NULL REFERENCES haystack.users (id) ); +CREATE TABLE haystack.user_tags ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + tag VARCHAR(32) NOT NULL, + user_id uuid NOT NULL REFERENCES haystack.users (id) +); + CREATE TABLE haystack.image_tags ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - tag TEXT NOT NULL, + tag_id UUID NOT NULL REFERENCES haystack.user_tags (id), image_id UUID NOT NULL REFERENCES haystack.user_images (id) ); @@ -47,6 +53,10 @@ CREATE TABLE haystack.image_links ( image_id UUID NOT NULL REFERENCES haystack.user_images (id) ); +/* -----| Indexes |----- */ + +CREATE INDEX user_tags_index ON haystack.user_tags(tag); + /* -----| Stored Procedures |----- */ CREATE OR REPLACE FUNCTION notify_new_image() From 40e854fb87ca185d75eea285ccfc41d948790b49 Mon Sep 17 00:00:00 2001 From: John Costa Date: Tue, 11 Mar 2025 21:23:41 +0000 Subject: [PATCH 29/30] feat(tags): creating and getting user tags --- .../haystack/haystack/model/image_tags.go | 2 +- .../.gen/haystack/haystack/model/user_tags.go | 18 +++++ .../haystack/haystack/table/image_tags.go | 10 +-- .../haystack/table/table_use_schema.go | 1 + .../.gen/haystack/haystack/table/user_tags.go | 81 +++++++++++++++++++ backend/models/tags.go | 78 ++++++++++++++++++ backend/schema.sql | 2 +- 7 files changed, 185 insertions(+), 7 deletions(-) create mode 100644 backend/.gen/haystack/haystack/model/user_tags.go create mode 100644 backend/.gen/haystack/haystack/table/user_tags.go create mode 100644 backend/models/tags.go diff --git a/backend/.gen/haystack/haystack/model/image_tags.go b/backend/.gen/haystack/haystack/model/image_tags.go index 9fb990b..8b20f01 100644 --- a/backend/.gen/haystack/haystack/model/image_tags.go +++ b/backend/.gen/haystack/haystack/model/image_tags.go @@ -13,6 +13,6 @@ import ( type ImageTags struct { ID uuid.UUID `sql:"primary_key"` - Tag string + TagID uuid.UUID ImageID uuid.UUID } diff --git a/backend/.gen/haystack/haystack/model/user_tags.go b/backend/.gen/haystack/haystack/model/user_tags.go new file mode 100644 index 0000000..e2d12d7 --- /dev/null +++ b/backend/.gen/haystack/haystack/model/user_tags.go @@ -0,0 +1,18 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package model + +import ( + "github.com/google/uuid" +) + +type UserTags struct { + ID uuid.UUID `sql:"primary_key"` + Tag string + UserID uuid.UUID +} diff --git a/backend/.gen/haystack/haystack/table/image_tags.go b/backend/.gen/haystack/haystack/table/image_tags.go index 7074850..c3a30dd 100644 --- a/backend/.gen/haystack/haystack/table/image_tags.go +++ b/backend/.gen/haystack/haystack/table/image_tags.go @@ -18,7 +18,7 @@ type imageTagsTable struct { // Columns ID postgres.ColumnString - Tag postgres.ColumnString + TagID postgres.ColumnString ImageID postgres.ColumnString AllColumns postgres.ColumnList @@ -61,10 +61,10 @@ func newImageTagsTable(schemaName, tableName, alias string) *ImageTagsTable { func newImageTagsTableImpl(schemaName, tableName, alias string) imageTagsTable { var ( IDColumn = postgres.StringColumn("id") - TagColumn = postgres.StringColumn("tag") + TagIDColumn = postgres.StringColumn("tag_id") ImageIDColumn = postgres.StringColumn("image_id") - allColumns = postgres.ColumnList{IDColumn, TagColumn, ImageIDColumn} - mutableColumns = postgres.ColumnList{TagColumn, ImageIDColumn} + allColumns = postgres.ColumnList{IDColumn, TagIDColumn, ImageIDColumn} + mutableColumns = postgres.ColumnList{TagIDColumn, ImageIDColumn} ) return imageTagsTable{ @@ -72,7 +72,7 @@ func newImageTagsTableImpl(schemaName, tableName, alias string) imageTagsTable { //Columns ID: IDColumn, - Tag: TagColumn, + TagID: TagIDColumn, ImageID: ImageIDColumn, AllColumns: allColumns, diff --git a/backend/.gen/haystack/haystack/table/table_use_schema.go b/backend/.gen/haystack/haystack/table/table_use_schema.go index 72d718c..913cd2a 100644 --- a/backend/.gen/haystack/haystack/table/table_use_schema.go +++ b/backend/.gen/haystack/haystack/table/table_use_schema.go @@ -16,5 +16,6 @@ func UseSchema(schema string) { ImageText = ImageText.FromSchema(schema) UserImages = UserImages.FromSchema(schema) UserImagesToProcess = UserImagesToProcess.FromSchema(schema) + UserTags = UserTags.FromSchema(schema) Users = Users.FromSchema(schema) } diff --git a/backend/.gen/haystack/haystack/table/user_tags.go b/backend/.gen/haystack/haystack/table/user_tags.go new file mode 100644 index 0000000..99dfc9e --- /dev/null +++ b/backend/.gen/haystack/haystack/table/user_tags.go @@ -0,0 +1,81 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package table + +import ( + "github.com/go-jet/jet/v2/postgres" +) + +var UserTags = newUserTagsTable("haystack", "user_tags", "") + +type userTagsTable struct { + postgres.Table + + // Columns + ID postgres.ColumnString + Tag postgres.ColumnString + UserID postgres.ColumnString + + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList +} + +type UserTagsTable struct { + userTagsTable + + EXCLUDED userTagsTable +} + +// AS creates new UserTagsTable with assigned alias +func (a UserTagsTable) AS(alias string) *UserTagsTable { + return newUserTagsTable(a.SchemaName(), a.TableName(), alias) +} + +// Schema creates new UserTagsTable with assigned schema name +func (a UserTagsTable) FromSchema(schemaName string) *UserTagsTable { + return newUserTagsTable(schemaName, a.TableName(), a.Alias()) +} + +// WithPrefix creates new UserTagsTable with assigned table prefix +func (a UserTagsTable) WithPrefix(prefix string) *UserTagsTable { + return newUserTagsTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} + +// WithSuffix creates new UserTagsTable with assigned table suffix +func (a UserTagsTable) WithSuffix(suffix string) *UserTagsTable { + return newUserTagsTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} + +func newUserTagsTable(schemaName, tableName, alias string) *UserTagsTable { + return &UserTagsTable{ + userTagsTable: newUserTagsTableImpl(schemaName, tableName, alias), + EXCLUDED: newUserTagsTableImpl("", "excluded", ""), + } +} + +func newUserTagsTableImpl(schemaName, tableName, alias string) userTagsTable { + var ( + IDColumn = postgres.StringColumn("id") + TagColumn = postgres.StringColumn("tag") + UserIDColumn = postgres.StringColumn("user_id") + allColumns = postgres.ColumnList{IDColumn, TagColumn, UserIDColumn} + mutableColumns = postgres.ColumnList{TagColumn, UserIDColumn} + ) + + return userTagsTable{ + Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), + + //Columns + ID: IDColumn, + Tag: TagColumn, + UserID: UserIDColumn, + + AllColumns: allColumns, + MutableColumns: mutableColumns, + } +} diff --git a/backend/models/tags.go b/backend/models/tags.go new file mode 100644 index 0000000..a511384 --- /dev/null +++ b/backend/models/tags.go @@ -0,0 +1,78 @@ +package models + +import ( + "screenmark/screenmark/.gen/haystack/haystack/model" + . "screenmark/screenmark/.gen/haystack/haystack/table" + + . "github.com/go-jet/jet/v2/postgres" + "github.com/google/uuid" +) + +// Raw dogging SQL is kinda based though? +// +// | nO, usE OrM!! +// +// | RAW - RAW +// | SQL | \ SQL +// | GOOD | \ GOOD +// | - - +// | -- -- +// | -- -- +// | ---- IQ ---- +func getNonExistantTags(userId uuid.UUID, tags []string) ([]string, error) { + stmt, err := db.Prepare(`WITH given_tags + AS (SELECT given_tags.tag FROM (VALUES ('a'), ('b'), ('c')) AS given_tags (tag)) + SELECT given_tags.tag + FROM given_tags + LEFT OUTER JOIN haystack.user_tags ON haystack.user_tags.tag = a.tag + where user_tags.tag is null;`) + + if err != nil { + return []string{}, err + } + + defer stmt.Close() + + rows, err := stmt.Query() + if err != nil { + return []string{}, err + } + + nonExistantTags := make([]string, 0) + + for rows.Next() { + var tag string + rows.Scan(&tag) + + nonExistantTags = append(nonExistantTags, tag) + } + + return nonExistantTags, nil +} + +func CreateTags(userId uuid.UUID, tags []string) error { + tagsToInsert, err := getNonExistantTags(userId, tags) + if err != nil { + return err + } + + stmt := UserTags.INSERT(UserTags.UserID, UserTags.Tag) + + for _, tag := range tagsToInsert { + stmt = stmt.VALUES(UUID(userId), tag) + } + + _, err = stmt.Exec(db) + + return err +} + +func GetTags(userId uuid.UUID) ([]model.UserTags, error) { + stmt := UserTags.SELECT(UserTags.AllColumns).WHERE(UserTags.UserID.EQ(UUID(userId))) + + userTags := []model.UserTags{} + + err := stmt.Query(db, &userTags) + + return userTags, err +} diff --git a/backend/schema.sql b/backend/schema.sql index 5a27869..af3d517 100644 --- a/backend/schema.sql +++ b/backend/schema.sql @@ -31,7 +31,7 @@ CREATE TABLE haystack.user_images ( CREATE TABLE haystack.user_tags ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - tag VARCHAR(32) NOT NULL, + tag VARCHAR(32) UNIQUE NOT NULL, user_id uuid NOT NULL REFERENCES haystack.users (id) ); From 536a49fe1c094b9c67bed199c0d9bca4c820588c Mon Sep 17 00:00:00 2001 From: John Costa Date: Tue, 11 Mar 2025 22:47:28 +0000 Subject: [PATCH 30/30] feat(tags): correctly inserting new tags and adding them to images --- backend/main.go | 20 ++++++++++++--- backend/models/image.go | 54 ++++++++++++++++++++++++++++++++++++----- backend/models/tags.go | 48 ++++++++++++++++++++++++++++++++---- 3 files changed, 108 insertions(+), 14 deletions(-) diff --git a/backend/main.go b/backend/main.go index 56584eb..5f28569 100644 --- a/backend/main.go +++ b/backend/main.go @@ -107,9 +107,23 @@ func main() { log.Println("Finished processing image " + imageId) log.Printf("Image attributes: %+v\n", imageInfo) - models.SaveImageTags(savedImage.ID.String(), imageInfo.Tags) - models.SaveImageLinks(savedImage.ID.String(), imageInfo.Links) - models.SaveImageTexts(savedImage.ID.String(), imageInfo.Text) + _, err = models.SaveImageTags(savedImage.ID.String(), imageInfo.Tags) + if err != nil { + log.Println("1") + log.Println(err) + } + + _, err = models.SaveImageLinks(savedImage.ID.String(), imageInfo.Links) + if err != nil { + log.Println("2") + log.Println(err) + } + + _, err = models.SaveImageTexts(savedImage.ID.String(), imageInfo.Text) + if err != nil { + log.Println("3") + log.Println(err) + } }() } } diff --git a/backend/models/image.go b/backend/models/image.go index b3f0208..d0d5f61 100644 --- a/backend/models/image.go +++ b/backend/models/image.go @@ -42,6 +42,25 @@ func removeImageToProcess(imageId string) error { return err } +func getUserId(imageId uuid.UUID) (uuid.UUID, error) { + stmt := UserImages.SELECT(UserImages.UserID).WHERE(UserImages.ID.EQ(UUID(imageId))) + + fmt.Println(stmt.DebugSql()) + + userIds := make([]string, 0) + + err := stmt.Query(db, &userIds) + if err != nil { + return uuid.Nil, err + } + + if len(userIds) != 1 { + return uuid.Nil, errors.New("expect only one user id per image id") + } + + return uuid.Parse(userIds[0]) +} + func SaveImage(imageId uuid.UUID) (model.UserImages, error) { imageToProcess, err := GetImageToProcess(imageId.String()) if err != nil { @@ -145,16 +164,39 @@ func GetUserImages(userId string) ([]UserImagesWithInfo, error) { func SaveImageTags(imageId string, tags []string) ([]model.ImageTags, error) { id := uuid.MustParse(imageId) - stmt := ImageTags.INSERT(ImageTags.ImageID, ImageTags.Tag) + userId, err := getUserId(id) + if err != nil { + return []model.ImageTags{}, err + } - for _, t := range tags { - stmt = stmt.VALUES(id, t) + err = CreateTags(userId, tags) + if err != nil { + return []model.ImageTags{}, err + } + + userTagsExpression := make([]Expression, 0) + for _, tag := range tags { + userTagsExpression = append(userTagsExpression, String(tag)) + } + + userTags := make([]model.UserTags, 0) + + getTagsStmt := UserTags.SELECT(UserTags.ID, UserTags.Tag).WHERE(UserTags.Tag.IN(userTagsExpression...)) + err = getTagsStmt.Query(db, &userTags) + if err != nil { + return []model.ImageTags{}, err + } + + stmt := ImageTags.INSERT(ImageTags.ImageID, ImageTags.TagID) + + for _, t := range userTags { + stmt = stmt.VALUES(id, t.ID) } stmt.RETURNING(ImageTags.AllColumns) - imageTags := []model.ImageTags{} - err := stmt.Query(db, &imageTags) + imageTags := make([]model.ImageTags, 0) + err = stmt.Query(db, &imageTags) return imageTags, err } @@ -162,7 +204,7 @@ func SaveImageTags(imageId string, tags []string) ([]model.ImageTags, error) { func SaveImageLinks(imageId string, links []string) ([]model.ImageLinks, error) { id := uuid.MustParse(imageId) - stmt := ImageTags.INSERT(ImageLinks.ImageID, ImageLinks.Link) + stmt := ImageLinks.INSERT(ImageLinks.ImageID, ImageLinks.Link) for _, t := range links { stmt = stmt.VALUES(id, t) diff --git a/backend/models/tags.go b/backend/models/tags.go index a511384..e61b977 100644 --- a/backend/models/tags.go +++ b/backend/models/tags.go @@ -1,6 +1,7 @@ package models import ( + "fmt" "screenmark/screenmark/.gen/haystack/haystack/model" . "screenmark/screenmark/.gen/haystack/haystack/table" @@ -20,20 +21,53 @@ import ( // | -- -- // | ---- IQ ---- func getNonExistantTags(userId uuid.UUID, tags []string) ([]string, error) { - stmt, err := db.Prepare(`WITH given_tags - AS (SELECT given_tags.tag FROM (VALUES ('a'), ('b'), ('c')) AS given_tags (tag)) + values := "" + counter := 1 + // big big SQL injection problem here? + for counter = 1; counter <= len(tags); counter++ { + values += fmt.Sprintf("($%d),", counter) + } + values = values[0 : len(values)-1] + + /* + WITH given_tags + AS (SELECT given_tags.tag FROM (VALUES ('c')) AS given_tags (tag)), + this_user_tags as ( + SELECT id, tag + FROM haystack.user_tags + where user_tags.user_id = 'fcc22dbb-7792-4595-be8e-d0439e13990a' + ) + select given_tags.tag from given_tags + LEFT OUTER JOIN this_user_tags ON this_user_tags.tag = given_tags.tag + where this_user_tags.tag is null; + */ + + withStuff := fmt.Sprintf(`WITH given_tags + AS (SELECT given_tags.tag FROM (VALUES `+values+`) AS given_tags (tag)), + this_user_tags AS + (SELECT id, tag FROM haystack.user_tags WHERE user_tags.user_id = $%d) SELECT given_tags.tag FROM given_tags - LEFT OUTER JOIN haystack.user_tags ON haystack.user_tags.tag = a.tag - where user_tags.tag is null;`) + LEFT OUTER JOIN haystack.user_tags ON haystack.user_tags.tag = given_tags.tag + where user_tags.tag is null`, counter) + + stmt, err := db.Prepare(withStuff) + fmt.Println(withStuff) if err != nil { + fmt.Println("failing to prepare stmt") return []string{}, err } defer stmt.Close() - rows, err := stmt.Query() + args := make([]any, counter) + for i, v := range tags { + args[i] = v + } + args[counter-1] = userId.String() + + rows, err := stmt.Query(args...) if err != nil { return []string{}, err } @@ -56,6 +90,10 @@ func CreateTags(userId uuid.UUID, tags []string) error { return err } + if len(tagsToInsert) == 0 { + return nil + } + stmt := UserTags.INSERT(UserTags.UserID, UserTags.Tag) for _, tag := range tagsToInsert {