diff --git a/backend/agents/event_agent.go b/backend/agents/event_agent.go new file mode 100644 index 0000000..a375ea1 --- /dev/null +++ b/backend/agents/event_agent.go @@ -0,0 +1,200 @@ +package agents + +import ( + "context" + "encoding/json" + "os" + "screenmark/screenmark/.gen/haystack/haystack/model" + "screenmark/screenmark/agents/client" + "screenmark/screenmark/models" + "time" + + "github.com/charmbracelet/log" + "github.com/google/uuid" +) + +const eventPrompt = ` +You are an agent. + +The user will send you images and you have to identify if they have any events or a place. +This could be a friend suggesting to meet, a conference, or anything that looks like an event. + +There are various tools you can use to perform this task. + +listEvents +Lists the users already existing events, you should do this before using createEvents to avoid creating duplicates. + +createEvent +Use this to create a new events. + +linkEvent +Links an image to a events. + +finish +Call when there is nothing else to do. +` + +const eventTools = ` +[ + { + "type": "function", + "function": { + "name": "listEvents", + "description": "List the events the user already has.", + "parameters": { + "type": "object", + "properties": {}, + "required": [] + } + } + }, + { + "type": "function", + "function": { + "name": "createEvent", + "description": "Use to create a new events", + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "startDateTime": { + "type": "string", + "description": "The start time as an ISO string" + }, + "endDateTime": { + "type": "string", + "description": "The end time as an ISO string" + } + }, + "required": ["name"] + } + } + }, + { + "type": "function", + "function": { + "name": "linkEvent", + "description": "Use to link an already existing events to the image you were sent", + "parameters": { + "type": "object", + "properties": { + "eventId": { + "type": "string" + } + }, + "required": ["eventsId"] + } + } + }, + { + "type": "function", + "function": { + "name": "finish", + "description": "Call this when there is nothing left to do.", + "parameters": { + "type": "object", + "properties": {}, + "required": [] + } + } + } +]` + +type EventAgent struct { + client client.AgentClient + + eventsModel models.EventModel +} + +type listEventArguments struct{} +type createEventArguments struct { + Name string `json:"name"` + StartDateTime *string `json:"startDateTime"` + EndDateTime *string `json:"endDateTime"` + OrganizerName *string `json:"organizerName"` +} +type linkEventArguments struct { + EventID string `json:"eventId"` +} + +func NewEventAgent(eventsModel models.EventModel) (EventAgent, error) { + agentClient, err := client.CreateAgentClient(log.NewWithOptions(os.Stdout, log.Options{ + ReportTimestamp: true, + TimeFormat: time.Kitchen, + Prefix: "Events 📍", + })) + + if err != nil { + return EventAgent{}, err + } + + agent := EventAgent{ + client: agentClient, + eventsModel: eventsModel, + } + + agentClient.ToolHandler.AddTool("listEvents", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) { + return agent.eventsModel.List(context.Background(), info.UserId) + }) + + agentClient.ToolHandler.AddTool("createEvent", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) { + args := createEventArguments{} + err := json.Unmarshal([]byte(_args), &args) + if err != nil { + return model.Events{}, err + } + + ctx := context.Background() + + layout := "2006-01-02T15:04:05Z" + + startTime, err := time.Parse(layout, *args.StartDateTime) + if err != nil { + return model.Events{}, err + } + + endTime, err := time.Parse(layout, *args.EndDateTime) + if err != nil { + return model.Events{}, err + } + + events, err := agent.eventsModel.Save(ctx, info.UserId, model.Events{ + Name: args.Name, + StartDateTime: &startTime, + EndDateTime: &endTime, + }) + + if err != nil { + return model.Events{}, err + } + + _, err = agent.eventsModel.SaveToImage(ctx, info.ImageId, events.ID) + if err != nil { + return model.Events{}, err + } + + return events, nil + }) + + agentClient.ToolHandler.AddTool("linkEvent", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) { + args := linkEventArguments{} + err := json.Unmarshal([]byte(_args), &args) + if err != nil { + return "", err + } + + ctx := context.Background() + + contactUuid, err := uuid.Parse(args.EventID) + if err != nil { + return "", err + } + + agent.eventsModel.SaveToImage(ctx, info.ImageId, contactUuid) + return "Saved", nil + }) + + return agent, nil +} diff --git a/backend/agents/event_location_agent.go b/backend/agents/event_location_agent.go deleted file mode 100644 index 68a07b0..0000000 --- a/backend/agents/event_location_agent.go +++ /dev/null @@ -1,267 +0,0 @@ -package agents - -import ( - "context" - "encoding/json" - "os" - "screenmark/screenmark/.gen/haystack/haystack/model" - "screenmark/screenmark/agents/client" - "screenmark/screenmark/models" - "time" - - "github.com/charmbracelet/log" - "github.com/google/uuid" -) - -// This prompt is probably shit. -const eventLocationPrompt = ` -You are an agent that extracts events, locations, and organizers from an image. Your primary tasks are to identify and create locations and organizers before creating events. Follow these steps: - -Identify and Create Locations: - -Check if the image contains a location. -If a location is found, check if it exists in the listLocations. -If the location does not exist, create it first. -Always reuse existing locations from listLocations to avoid duplicates. - -Identify and Create Events: - -Check if the image contains an event. An event should have a name and a date. -If an event is found, ensure you have a location (from step 1) and an organizer (from step 2) before creating the event. -Events must have an associated location and organizer. Do not create an event without these. -If possible, return a start time and an end time as ISO datetime strings. -Handling Images Without Events or Locations: - -It is possible that the image does not contain an event or a location. In such cases, do not create an event. -Always prioritize the creation of locations and organizers before events. Ensure that all events have an associated location and organizer. -` - -// TODO: this should be read directly from a file on load. -const eventLocationTools = ` -[ - { - "type": "function", - "function": { - "name": "createLocation", - "description": "Creates a location. No not use if you think an existing location is suitable!", - "parameters": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "address": { - "type": "string" - } - }, - "required": ["name"] - } - } - }, - { - "type": "function", - "function": { - "name": "listLocations", - "description": "Lists the locations available", - "parameters": { - "type": "object", - "properties": {} - } - } - }, - { - "type": "function", - "function": { - "name": "createEvent", - "description": "Creates a new event", - "parameters": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "startDateTime": { - "type": "string", - "description": "The start time as an ISO string" - }, - "endDateTime": { - "type": "string", - "description": "The end time as an ISO string" - }, - "locationId": { - "type": "string", - "description": "The ID of the location, available by listLocations" - }, - "organizerName": { - "type": "string", - "description": "The name of the organizer" - } - }, - "required": ["name"] - } - } - }, - { - "type": "function", - "function": { - "name": "finish", - "description": "Nothing else to do. call this function.", - "parameters": {} - } - } -] -` - -type EventLocationAgent struct { - client client.AgentClient - - eventModel models.EventModel - locationModel models.LocationModel - contactModel models.ContactModel - - toolHandler client.ToolsHandlers -} - -// TODO make these private - -type ListLocationArguments struct{} -type ListOrganizerArguments struct{} - -type CreateLocationArguments struct { - Name string `json:"name"` - Address *string `json:"address,omitempty"` - Coordinates *string `json:"coordinates,omitempty"` -} - -type CreateOrganizerArguments struct { - Name string `json:"name"` - PhoneNumber *string `json:"phoneNumber,omitempty"` - Email *string `json:"email,omitempty"` -} - -type AttachImageLocationArguments struct { - LocationId string `json:"locationId"` -} - -type CreateEventArguments struct { - Name string `json:"name"` - StartDateTime string `json:"startDateTime"` - EndDateTime string `json:"endDateTime"` - LocationId string `json:"locationId"` - OrganizerName string `json:"organizerName"` -} - -func NewLocationEventAgent(locationModel models.LocationModel, eventModel models.EventModel, contactModel models.ContactModel) (EventLocationAgent, error) { - agentClient, err := client.CreateAgentClient(log.NewWithOptions(os.Stdout, log.Options{ - ReportTimestamp: true, - TimeFormat: time.Kitchen, - Prefix: "Location & Events 📅", - })) - if err != nil { - return EventLocationAgent{}, err - } - - agent := EventLocationAgent{ - client: agentClient, - locationModel: locationModel, - eventModel: eventModel, - contactModel: contactModel, - } - - agentClient.ToolHandler.AddTool("listLocations", - func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) { - return agent.locationModel.List(context.Background(), info.UserId) - }, - ) - - agentClient.ToolHandler.AddTool("createLocation", - func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) { - args := CreateLocationArguments{} - err := json.Unmarshal([]byte(_args), &args) - if err != nil { - return model.Locations{}, err - } - - ctx := context.Background() - - location, err := agent.locationModel.Save(ctx, info.UserId, model.Locations{ - Name: args.Name, - Address: args.Address, - }) - - if err != nil { - return location, err - } - - _, err = agent.locationModel.SaveToImage(ctx, info.ImageId, location.ID) - - return location, err - }, - ) - - agentClient.ToolHandler.AddTool("createEvent", - func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) { - args := CreateEventArguments{} - err := json.Unmarshal([]byte(_args), &args) - if err != nil { - return model.Locations{}, err - } - - ctx := context.Background() - - layout := "2006-01-02T15:04:05Z" - - startTime, err := time.Parse(layout, args.StartDateTime) - if err != nil { - return model.Events{}, err - } - - endTime, err := time.Parse(layout, args.EndDateTime) - if err != nil { - return model.Events{}, err - } - - event, err := agent.eventModel.Save(ctx, info.UserId, model.Events{ - Name: args.Name, - StartDateTime: &startTime, - EndDateTime: &endTime, - }) - - if err != nil { - return event, err - } - - organizer, err := agent.contactModel.Save(ctx, info.UserId, model.Contacts{ - Name: args.Name, - }) - - if err != nil { - return event, err - } - - _, err = agent.eventModel.SaveToImage(ctx, info.ImageId, event.ID) - if err != nil { - return event, err - } - - _, err = agent.contactModel.SaveToImage(ctx, info.ImageId, organizer.ID) - if err != nil { - return event, err - } - - locationId, err := uuid.Parse(args.LocationId) - if err != nil { - return event, err - } - - event, err = agent.eventModel.UpdateLocation(ctx, event.ID, locationId) - if err != nil { - return event, err - } - - return agent.eventModel.UpdateOrganizer(ctx, event.ID, organizer.ID) - }, - ) - - return agent, nil -} diff --git a/backend/agents/location_agent.go b/backend/agents/location_agent.go new file mode 100644 index 0000000..25da2e0 --- /dev/null +++ b/backend/agents/location_agent.go @@ -0,0 +1,179 @@ +package agents + +import ( + "context" + "encoding/json" + "os" + "screenmark/screenmark/.gen/haystack/haystack/model" + "screenmark/screenmark/agents/client" + "screenmark/screenmark/models" + "time" + + "github.com/charmbracelet/log" + "github.com/google/uuid" +) + +const locationPrompt = ` +You are an agent. + +The user will send you images and you have to identify if they have any location or a place. This could a picture of a real place, an address, or it's name. + +There are various tools you can use to perform this task. + +listLocations +Lists the users already existing locations, you should do this before using createLocation to avoid creating duplicates. + +createLocation +Use this to create a new location. Avoid making duplicates and only create a new location if listLocations doesnt contain the location on the image. + +linkLocation +Links an image to a location. + +finish +Call when there is nothing else to do. +` + +const locationTools = ` +[ + { + "type": "function", + "function": { + "name": "listLocations", + "description": "List the locations the user already has.", + "parameters": { + "type": "object", + "properties": {}, + "required": [] + } + } + }, + { + "type": "function", + "function": { + "name": "createLocation", + "description": "Use to create a new location", + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "address": { + "type": "string" + } + }, + "required": ["name"] + } + } + }, + { + "type": "function", + "function": { + "name": "linkLocation", + "description": "Use to link an already existing location to the image you were sent", + "parameters": { + "type": "object", + "properties": { + "locationId": { + "type": "string" + } + }, + "required": ["locationId"] + } + } + }, + { + "type": "function", + "function": { + "name": "finish", + "description": "Call this when there is nothing left to do.", + "parameters": { + "type": "object", + "properties": {}, + "required": [] + } + } + } +]` + +type LocationAgent struct { + client client.AgentClient + + locationModel models.LocationModel +} + +type listLocationArguments struct{} +type createLocationArguments struct { + Name string `json:"name"` + Address *string `json:"address"` +} +type linkLocationArguments struct { + LocationID string `json:"locationId"` +} + +func NewLocationAgent(locationModel models.LocationModel) (LocationAgent, error) { + agentClient, err := client.CreateAgentClient(log.NewWithOptions(os.Stdout, log.Options{ + ReportTimestamp: true, + TimeFormat: time.Kitchen, + Prefix: "Locations 📍", + })) + + if err != nil { + return LocationAgent{}, err + } + + agent := LocationAgent{ + client: agentClient, + locationModel: locationModel, + } + + agentClient.ToolHandler.AddTool("listLocations", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) { + return agent.locationModel.List(context.Background(), info.UserId) + }) + + agentClient.ToolHandler.AddTool("createLocation", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) { + args := createLocationArguments{} + err := json.Unmarshal([]byte(_args), &args) + if err != nil { + return model.Locations{}, err + } + + ctx := context.Background() + + location, err := agent.locationModel.Save(ctx, info.UserId, model.Locations{ + Name: args.Name, + Address: args.Address, + }) + + if err != nil { + return model.Locations{}, err + } + + _, err = agent.locationModel.SaveToImage(ctx, info.ImageId, location.ID) + if err != nil { + return model.Locations{}, err + } + + return location, nil + }) + + agentClient.ToolHandler.AddTool("linkLocation", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) { + args := linkLocationArguments{} + err := json.Unmarshal([]byte(_args), &args) + if err != nil { + return "", err + } + + ctx := context.Background() + + contactUuid, err := uuid.Parse(args.LocationID) + if err != nil { + return "", err + } + + agent.locationModel.SaveToImage(ctx, info.ImageId, contactUuid) + return "Saved", nil + }) + + return agent, nil +} diff --git a/backend/agents/orchestrator.go b/backend/agents/orchestrator.go index 9868bef..7c4caca 100644 --- a/backend/agents/orchestrator.go +++ b/backend/agents/orchestrator.go @@ -20,17 +20,18 @@ The agents are available as tool calls. Agents available: -eventLocationAgent -Use it when you think the image contains an event or a location of any sort. This can be an event page, a map, an address or a date. -This could also be a conversation describing an event. - noteAgent Use when there is ANY text on the image. contactAgent - Use it when the image contains information relating a person. +locationAgent +Use it when the image contains some address or a place. + +eventAgent +Use it when the image contains an event, this can be a date, a message suggesting an event. + noAction When you think there is no more information to extract from the image. @@ -41,18 +42,6 @@ Do not call the agent if you do not think it is relevant for the image. const OrchestratorTools = ` [ - { - "type": "function", - "function": { - "name": "eventLocationAgent", - "description": "Use when there is an event or location on the image. This could be in writing form", - "parameters": { - "type": "object", - "properties": {}, - "required": [] - } - } - }, { "type": "function", "function": { @@ -76,6 +65,30 @@ const OrchestratorTools = ` "required": [] } } + }, + { + "type": "function", + "function": { + "name": "locationAgent", + "description": "Use when then image contains some place, location or address", + "parameters": { + "type": "object", + "properties": {}, + "required": [] + } + } + }, + { + "type": "function", + "function": { + "name": "eventAgent", + "description": "Use when then image contains some event", + "parameters": { + "type": "object", + "properties": {}, + "required": [] + } + } }, { "type": "function", @@ -101,7 +114,7 @@ type Status struct { Ok bool `json:"ok"` } -func NewOrchestratorAgent(eventLocationAgent EventLocationAgent, noteAgent NoteAgent, contactAgent ContactAgent, imageName string, imageData []byte) (OrchestratorAgent, error) { +func NewOrchestratorAgent(noteAgent NoteAgent, contactAgent ContactAgent, locationAgent LocationAgent, eventAgent EventAgent, imageName string, imageData []byte) (OrchestratorAgent, error) { agent, err := client.CreateAgentClient(log.NewWithOptions(os.Stdout, log.Options{ ReportTimestamp: true, TimeFormat: time.Kitchen, @@ -112,17 +125,6 @@ func NewOrchestratorAgent(eventLocationAgent EventLocationAgent, noteAgent NoteA return OrchestratorAgent{}, err } - agent.ToolHandler.AddTool("eventLocationAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) { - // We need a way to keep track of this async? - // Probably just a DB, because we don't want to wait. The orchistrator shouldnt wait for this stuff to finish. - - go eventLocationAgent.client.RunAgent(eventLocationPrompt, eventLocationTools, "finish", info.UserId, info.ImageId, imageName, imageData) - - return Status{ - Ok: true, - }, nil - }) - agent.ToolHandler.AddTool("noteAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) { go noteAgent.GetNotes(info.UserId, info.ImageId, imageName, imageData) @@ -139,6 +141,22 @@ func NewOrchestratorAgent(eventLocationAgent EventLocationAgent, noteAgent NoteA }, nil }) + agent.ToolHandler.AddTool("locationAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) { + go locationAgent.client.RunAgent(locationPrompt, locationTools, "finish", info.UserId, info.ImageId, imageName, imageData) + + return Status{ + Ok: true, + }, nil + }) + + agent.ToolHandler.AddTool("eventAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) { + go eventAgent.client.RunAgent(eventPrompt, eventTools, "finish", info.UserId, info.ImageId, imageName, imageData) + + return Status{ + Ok: true, + }, nil + }) + agent.ToolHandler.AddTool("noAction", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) { // To nothing diff --git a/backend/email.go b/backend/email.go index 03c6fb9..fc91505 100644 --- a/backend/email.go +++ b/backend/email.go @@ -56,6 +56,7 @@ func CreateMailClient() (Mailer, error) { client, err := mail.NewClient( "smtp.mailbox.org", + mail.WithTLSPortPolicy(mail.TLSMandatory), mail.WithSMTPAuth(mail.SMTPAuthPlain), mail.WithUsername(os.Getenv("EMAIL_USERNAME")), mail.WithPassword(os.Getenv("EMAIL_PASSWORD")), diff --git a/backend/events.go b/backend/events.go index 39edff8..9f7aead 100644 --- a/backend/events.go +++ b/backend/events.go @@ -42,11 +42,6 @@ func ListenNewImageEvents(db *sql.DB, eventManager *EventManager) { ctx := context.Background() go func() { - locationAgent, err := agents.NewLocationEventAgent(locationModel, eventModel, contactModel) - if err != nil { - panic(err) - } - noteAgent, err := agents.NewNoteAgent(noteModel) if err != nil { panic(err) @@ -57,6 +52,16 @@ func ListenNewImageEvents(db *sql.DB, eventManager *EventManager) { panic(err) } + locationAgent, err := agents.NewLocationAgent(locationModel) + if err != nil { + panic(err) + } + + eventAgent, err := agents.NewEventAgent(eventModel) + if err != nil { + panic(err) + } + image, err := imageModel.GetToProcessWithData(ctx, imageId) if err != nil { log.Println("Failed to GetToProcessWithData") @@ -70,7 +75,7 @@ func ListenNewImageEvents(db *sql.DB, eventManager *EventManager) { return } - orchestrator, err := agents.NewOrchestratorAgent(locationAgent, noteAgent, contactAgent, image.Image.ImageName, image.Image.Image) + orchestrator, err := agents.NewOrchestratorAgent(noteAgent, contactAgent, locationAgent, eventAgent, image.Image.ImageName, image.Image.Image) if err != nil { panic(err) } diff --git a/backend/models/events.go b/backend/models/events.go index c1ef38a..d473605 100644 --- a/backend/models/events.go +++ b/backend/models/events.go @@ -14,6 +14,20 @@ type EventModel struct { dbPool *sql.DB } +func (m EventModel) List(ctx context.Context, userId uuid.UUID) ([]model.Events, error) { + listEventsStmt := SELECT(Events.AllColumns). + FROM( + Events. + INNER_JOIN(UserEvents, UserEvents.EventID.EQ(Events.ID)), + ). + WHERE(UserEvents.UserID.EQ(UUID(userId))) + + events := []model.Events{} + + err := listEventsStmt.QueryContext(ctx, m.dbPool, &events) + return events, err +} + func (m EventModel) Save(ctx context.Context, userId uuid.UUID, event model.Events) (model.Events, error) { // TODO tx here insertEventStmt := Events. diff --git a/backend/models/locations.go b/backend/models/locations.go index 7f874c8..b3bfd6c 100644 --- a/backend/models/locations.go +++ b/backend/models/locations.go @@ -7,6 +7,7 @@ import ( . "screenmark/screenmark/.gen/haystack/haystack/table" . "github.com/go-jet/jet/v2/postgres" + "github.com/go-jet/jet/v2/qrm" "github.com/google/uuid" ) @@ -51,13 +52,32 @@ func (m LocationModel) Save(ctx context.Context, userId uuid.UUID, location mode } func (m LocationModel) SaveToImage(ctx context.Context, imageId uuid.UUID, locationId uuid.UUID) (model.ImageLocations, error) { + imageLocation := model.ImageLocations{} + + checkExistingStmt := ImageLocations. + SELECT(ImageLocations.AllColumns). + WHERE( + ImageLocations.ImageID.EQ(UUID(imageId)). + AND(ImageLocations.LocationID.EQ(UUID(locationId))), + ) + + err := checkExistingStmt.QueryContext(ctx, m.dbPool, &imageLocation) + if err != nil && err != qrm.ErrNoRows { + // A real error + return model.ImageLocations{}, err + } + + if err == nil { + // Already exists. + return imageLocation, nil + } + insertImageLocationStmt := ImageLocations. INSERT(ImageLocations.ImageID, ImageLocations.LocationID). VALUES(imageId, locationId). RETURNING(ImageLocations.AllColumns) - imageLocation := model.ImageLocations{} - err := insertImageLocationStmt.QueryContext(ctx, m.dbPool, &imageLocation) + err = insertImageLocationStmt.QueryContext(ctx, m.dbPool, &imageLocation) return imageLocation, err } diff --git a/frontend/package.json b/frontend/package.json index dc290f7..10e7e33 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,45 +1,44 @@ { - "name": "haystack", - "version": "0.1.0", - "description": "Screenshots that organize themselves", - "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", - "@solidjs/router": "^0.15.3", - "@tabler/icons-solidjs": "^3.30.0", - "@tauri-apps/api": "^2", - "@tauri-apps/plugin-dialog": "~2", - "@tauri-apps/plugin-global-shortcut": "~2", - "@tauri-apps/plugin-opener": "^2", - "@tauri-apps/plugin-store": "~2", - "clsx": "^2.1.1", - "fuse.js": "^7.1.0", - "jwt-decode": "^4.0.0", - "solid-js": "^1.9.3", - "solid-motionone": "^1.0.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" - } + "name": "haystack", + "version": "0.1.0", + "description": "Screenshots that organize themselves", + "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", + "@solidjs/router": "^0.15.3", + "@tabler/icons-solidjs": "^3.30.0", + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-dialog": "~2", + "@tauri-apps/plugin-http": "~2", + "@tauri-apps/plugin-opener": "^2", + "clsx": "^2.1.1", + "fuse.js": "^7.1.0", + "jwt-decode": "^4.0.0", + "solid-js": "^1.9.3", + "solid-motionone": "^1.0.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/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index 7bf2860..dcd73e6 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -27,6 +27,7 @@ notify = "6.1.1" base64 = "0.21.7" tokio = { version = "1.36.0", features = ["full"] } tauri-plugin-store = "2" +tauri-plugin-http = "2" [target."cfg(target_os = \"macos\")".dependencies] cocoa = "0.26" diff --git a/frontend/src-tauri/capabilities/default.json b/frontend/src-tauri/capabilities/default.json index e7874eb..d97ab54 100644 --- a/frontend/src-tauri/capabilities/default.json +++ b/frontend/src-tauri/capabilities/default.json @@ -11,6 +11,11 @@ "global-shortcut:allow-is-registered", "global-shortcut:allow-register", "global-shortcut:allow-unregister", - "global-shortcut:allow-unregister-all" + "global-shortcut:allow-unregister-all", + "http:default", + { + "identifier": "http:default", + "allow": [{ "url": "https://haystack.johncosta.tech" }] + } ] } diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index 8fd4e3c..5de6bb4 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -13,6 +13,7 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_store::Builder::new().build()) + .plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_opener::init()) .manage(watcher_state) diff --git a/frontend/src/Login.tsx b/frontend/src/Login.tsx index 344d8cd..25d3c46 100644 --- a/frontend/src/Login.tsx +++ b/frontend/src/Login.tsx @@ -1,7 +1,7 @@ import { Button } from "@kobalte/core/button"; import { TextField } from "@kobalte/core/text-field"; import { createSignal, Show, type Component } from "solid-js"; -import { postCode, postLogin } from "./network"; +import { base, postCode, postLogin } from "./network"; import { isTokenValid } from "./ProtectedRoute"; import { Navigate } from "@solidjs/router"; @@ -40,20 +40,23 @@ export const Login: Component = () => { const isAuthorized = isTokenValid(); return ( - }> -
- - Email - - - - - Code + <> + {base} + }> + + + Email - - - -
+ + + Code + + + + + +
+ ); }; diff --git a/frontend/src/network/index.ts b/frontend/src/network/index.ts index 067f3e7..0cd1cbd 100644 --- a/frontend/src/network/index.ts +++ b/frontend/src/network/index.ts @@ -1,3 +1,5 @@ +import { fetch } from "@tauri-apps/plugin-http"; + import { type InferOutput, array, @@ -17,8 +19,12 @@ type BaseRequestParams = Partial<{ method: "GET" | "POST"; }>; +export const base = import.meta.env.DEV + ? "http://localhost:3040" + : "https://haystack.johncosta.tech"; + const getBaseRequest = ({ path, body, method }: BaseRequestParams): Request => { - return new Request(`http://localhost:3040/${path}`, { + return new Request(`${base}/${path}`, { body, method, }); @@ -29,7 +35,7 @@ const getBaseAuthorizedRequest = ({ body, method, }: BaseRequestParams): Request => { - return new Request(`http://localhost:3040/${path}`, { + return new Request(`${base}/${path}`, { headers: { Authorization: `Bearer ${localStorage.getItem("access")?.toString()}`, },