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