219 Commits

Author SHA1 Message Date
0940134d9e chore(platforms): seperating permissions and inits on different platforms 2025-04-22 15:26:48 +01:00
af24a8f6d4 fix: backend err 2025-04-22 15:26:48 +01:00
446e8bb2ee feat: sending image to the backend 2025-04-22 15:26:48 +01:00
6211945e3d feat(images): share target working and receiving images! 2025-04-22 15:26:48 +01:00
9f215bb3d1 feat: working android dev environment 2025-04-22 15:26:48 +01:00
1a84e0d6c5 wip: working release sdk 2025-04-22 15:26:48 +01:00
bd9d8b955a wip: sorting out various versioning problems 2025-04-22 15:26:48 +01:00
58f60bf053 BIGWIP(android): trying to make an android release
fucking stupid shit why is it so hard
2025-04-22 15:26:48 +01:00
55cd552724 fix(linux-tauri): window building on linux 2025-04-19 15:42:14 +01:00
981bca86e9 wip(logs): displaying image
WIP because we need to bypass authorization here
2025-04-19 14:06:05 +01:00
ac0bcfdae0 feat(logs): route to view the logs for each image 2025-04-19 14:03:18 +01:00
af2aa2c1b6 chore(code): cleaning 2025-04-19 12:16:48 +01:00
9f98a21532 feat(logging): split logging to stdout & database to allow us to view it on webbrowser 2025-04-19 12:14:04 +01:00
130bce86a1 fix: enabling note agent 2025-04-19 10:30:49 +01:00
4da37d1704 feat(events): a better prompt with good integration with location agent 2025-04-19 10:30:29 +01:00
016834ee7d feat(contacts): not creating duplicates 2025-04-19 10:07:51 +01:00
bae6a7eda9 feat(location): prompt tweak + going back to faster model 2025-04-18 15:36:51 +01:00
e706b6a976 feat(location): correctly updating an image if it contains a duplicate locatino 2025-04-18 15:32:07 +01:00
7e7f01447e feat(agents): improving rationality by adding tool to allow the models to think through choices.
This works pretty nicely actually. I'm starting to understand how to
demistify the system prompt and have the tools the agent needs to do a
good job.
2025-04-18 15:06:20 +01:00
dc202189ca wip 2025-04-18 14:21:23 +01:00
1cff278263 feat(location-agent): using createLocation instead of updateLocation to simplify 2025-04-18 13:26:42 +01:00
1e40390952 feat(contact-agent): using createContact with an ID field to provide updates 2025-04-17 18:57:13 +01:00
150a43a5dc feat(event-agent): update events function 2025-04-17 18:19:54 +01:00
2b7206c29e feat(location-agent): seperating the tool to allow for replying
This means it makes less mistakes and doesnt get as confused.
2025-04-17 18:09:00 +01:00
7002b05aae fix(location-events): adding location id to the database from agent call 2025-04-17 15:32:50 +01:00
8e73ad6f4e feat(prompts): adding better prompts & restoring tool_stop
Mistral's models seem to do something really strange if you allow for
`tool_choice` to be anything but `any`. They start putting the tool call
inside the `content` instead of an actual tool call. This means that I
need this `stop` mechanism using a tool call instead because I cannot
trust the model to do it by itself.

I quite like this model though, it's cheap, it's fast and it's open
source. And all the answers are pretty good!
2025-04-17 15:24:21 +01:00
4b0ef8b17f feat(orchestrator): removing the end tool call
fix
2025-04-17 13:00:39 +01:00
57c760e7f0 chore: removing unnecessary logging 2025-04-17 13:00:24 +01:00
f5fdaff7c1 wip(orchestrator): improving orchestrator system prompt and tool description 2025-04-17 12:52:54 +01:00
8fff043849 feat(event-location): communicating using tool calls correctly 2025-04-17 11:15:02 +01:00
d1fd2aeaf1 fix(logger): nil pointer error + log debug level clean 2025-04-17 11:07:37 +01:00
1e5028177f refactor(agents): not returning an error on factory method 2025-04-17 11:02:11 +01:00
c4569e925b refactor(agents): encapsulating prompt and calls inside factory method 2025-04-17 10:58:19 +01:00
8fed2f9b9a fix: using correct eventAgent instead of orchestrator bug + better logging 2025-04-17 10:48:30 +01:00
1651926c4d refactor(agents): no need to wrap them in another struct 2025-04-17 10:36:11 +01:00
fa127c2331 feat: event agent calling location agent about location ID
This is pretty nice. We can now have agents spawn other agents and
actually get super cool functionality from it.

The pattern might be a little fragile.
2025-04-16 14:43:07 +01:00
7be669e49e wip(agents): allowing event agent to call location agent 2025-04-15 16:44:00 +01:00
7b6bdf2c7b feat: Adding text message to describe an action3 2025-04-15 16:43:27 +01:00
7b40959125 fix 2025-04-14 20:08:07 +01:00
63201280bb rollback: not using link functions as they are very problematic 2025-04-14 10:59:08 +01:00
885f877ef0 fix(network): restore conditional base URL for development environment
- Reintroduced conditional logic for the base URL to switch between local and production endpoints based on the environment.
2025-04-14 11:41:29 +02:00
f38d44f5f5 Merge branch 'main' of https://git.johncosta.tech/JohnCosta27/Haystack 2025-04-14 11:41:11 +02:00
64f8aa032e ffix 2025-04-14 10:40:02 +01:00
4ba41258e0 prompt 2025-04-14 10:38:25 +01:00
49f5cd0afb Merge branch 'main' of https://git.johncosta.tech/JohnCosta27/Haystack 2025-04-14 11:37:34 +02:00
3ccb7ad7a1 refactor(network): simplify base URL and clean up validators
- Updated the base URL to a fixed production endpoint, removing the conditional logic for development.
- Commented out Location and Organizer validations in the eventValidator for future consideration.
- Added a console log in getUserImages to assist with backend response tracking.
2025-04-14 11:37:29 +02:00
2e092e5fe4 more prompt 2025-04-14 10:36:21 +01:00
60273c5782 fix more prompt 2025-04-14 10:33:56 +01:00
98799b01e6 stupid 2025-04-14 10:30:46 +01:00
1d0eb8ddaa horrible 2025-04-14 10:30:21 +01:00
8e7ee204ce fix: prompts 2025-04-14 10:28:31 +01:00
0ff541b7b6 debug 2025-04-14 10:22:54 +01:00
732d0cedd0 fix(network): update base URL for development and production environments
- Changed the base URL to use localhost for development and the production URL for other environments.
- Added Location validation back into the eventValidator and removed commented-out code for clarity.
- Cleaned up debugging logs in getUserImages function.
2025-04-14 10:56:32 +02:00
73dad6fd2d push 2025-04-14 09:55:50 +01:00
cb3b930c32 Merge branch 'main' of https://git.johncosta.tech/JohnCosta27/Haystack 2025-04-14 10:54:24 +02:00
cbf013aece feat(search): improve Search component with conditional rendering and debugging logs
- Added conditional rendering for the "No results found" message using the Show component.
- Introduced debugging logs in getUserImages and Search component to track data flow.
- Cleaned up the data mapping process in getUserImages for better readability.
2025-04-14 10:54:18 +02:00
edcba60c5a pushhh 2025-04-14 09:54:09 +01:00
ba940ae6fd debnug 2025-04-14 09:50:03 +01:00
efedb4e63c debug 2025-04-14 09:44:29 +01:00
9ac569359c debug 2025-04-14 09:44:19 +01:00
b13d3c1881 Merge branch 'main' of https://git.johncosta.tech/JohnCosta27/Haystack 2025-04-14 10:36:58 +02:00
e1857bd532 feat(login): implement logout functionality and redirect after login
- Added a logout function in the Settings component to clear user session data and redirect to the login page.
- Updated the Login component to redirect to the home page upon successful login.
- Adjusted styling in the Search component for better spacing in the "No results found" message.
2025-04-14 10:36:55 +02:00
b1b46ff7e5 fix 2025-04-14 09:31:27 +01:00
dbb98d1e48 feat: making all codes upper case + fetching fixes 2025-04-14 09:28:08 +01:00
d3fb92546f fix 2025-04-14 09:15:48 +01:00
313b764ec4 chore: removing SQL debug 2025-04-14 09:12:16 +01:00
43e63f739a feat(search): enhance Search component with shortcuts and item modal
- Added functionality to fetch and display global shortcuts in the Search component.
- Introduced ItemModal for displaying detailed information about selected items.
- Updated SearchCard components to improve layout and information presentation.
- Enhanced user experience with better styling and accessibility features.
2025-04-14 10:03:37 +02:00
ce9e27ec68 feat(image-viewer): integrate ImageViewer component and update FolderPicker layout
- Added ImageViewer component to the App for displaying processed images.
- Updated FolderPicker layout for improved user guidance and aesthetics.
- Refactored ShortcutItem and Shortcuts components for better structure and clarity.
- Introduced ItemModal component for future use.
2025-04-14 09:25:53 +02:00
76618d1124 Merge branch 'main' of https://git.johncosta.tech/JohnCosta27/Haystack 2025-04-14 08:55:42 +02:00
22a6ad9818 refactor(settings): reorganize FolderPicker component and update layout
- Moved FolderPicker to a new folder structure for better organization.
- Updated the Settings page layout to enhance visual hierarchy by increasing the title size.
- Removed the old FolderPicker component file after restructuring.
2025-04-14 08:55:36 +02:00
8af4f62492 feat(app): restructure routing and implement Search component
- Refactored the App component to streamline routing using the Router and Route components.
- Introduced a new Search component to handle search functionality, including input handling and result display.
- Removed inline search logic from the App component for better separation of concerns.
- Updated index.tsx to render the App component directly, simplifying the routing structure.
2025-04-14 08:47:57 +02:00
6e96eb53b4 feat: registering users if their email is not known 2025-04-13 22:29:25 +01:00
ca2e98e4b4 feat(app): refactor App component and add Settings page
- Refactored the App component to utilize a new SearchCard component for rendering search results.
- Introduced a Settings page with FolderPicker and Shortcuts components for user configuration.
- Removed the ImagePage component as it was no longer needed.
- Updated routing to include the new Settings page and adjusted imports accordingly.
- Added a settings button to the main interface for easy access to the new settings functionality.
2025-04-13 22:48:26 +02:00
43404aaf18 feat(search): add autofocus to search input and emit focus event on shortcut
- Added autofocus attribute to the search input field for improved user experience.
- Updated global shortcut handling to emit a "focus-search" event when the shortcut is triggered, enhancing the application's responsiveness to user actions.
- Updated dependencies in Cargo.toml to specific beta versions for better compatibility.
2025-04-13 21:57:36 +02:00
4196952178 feat(capabilities): add localhost URL to default permissions
- Updated the default capabilities configuration to allow access to http://localhost:3040 in addition to the existing https://haystack.johncosta.tech URL.
2025-04-13 21:28:14 +02:00
767ca20b4c chore: update dependencies and add new packages
- Updated several dependencies in Cargo.lock to their latest versions, including `anyhow`, `ashpd`, `bitflags`, `chrono`, and `http`.
- Added new dependencies such as `tauri-plugin-http`, `cookie_store`, and `h2`.
- Removed outdated dependencies related to `wayland` and updated the `windows` related packages for better compatibility.
2025-04-13 21:24:20 +02:00
bda733f8a5 Merge branch 'main' of https://git.johncosta.tech/JohnCosta27/Haystack 2025-04-13 21:21:43 +02:00
7d68f39bab refactor: using tauri http client 2025-04-13 19:34:02 +01:00
d687e86f86 fix 2025-04-13 19:18:07 +01:00
fca4a6445c chore: removing old agent that was messy and too coupled
chore
2025-04-13 16:30:20 +01:00
17cc12f0c9 feat(event): seperate event agent 2025-04-13 16:30:20 +01:00
b57968b938 feat(location): agent to create locations 2025-04-13 16:30:20 +01:00
f0477ed720 fix(email) 2025-04-13 16:28:40 +01:00
f09cc137a3 feat: add global shortcut functionality and update dependencies
- Introduced global shortcut management in the Tauri application, allowing users to set, change, and unregister shortcuts.
- Added new dependencies for global shortcut functionality in Cargo.toml and updated package.json.
- Enhanced the default capabilities to include global shortcut permissions.
- Refactored the main application logic to integrate the new shortcut features.
2025-04-13 16:40:04 +02:00
4e78d2e701 Merge branch 'main' of https://git.johncosta.tech/JohnCosta27/Haystack 2025-04-13 16:20:32 +02:00
0b97b2eed2 feat: update app description and enhance folder watching functionality
- Updated the app description in package.json and Cargo.toml to "Screenshots that organize themselves".
- Refactored the Tauri backend to introduce a new command for handling folder selection and watching for PNG file changes.
- Added utility functions for processing PNG files and managing the watcher state.
- Improved the frontend by integrating an ImageViewer component and setting up event listeners for search input focus.
2025-04-13 16:19:51 +02:00
0fcdd73a47 feat(sse): very rough events. Not used in the client yet
feat(sse): very rough events. Not used in the client yet
2025-04-13 14:27:59 +01:00
35072684de Revert "FIXUP wip: notifications on starting progress"
This reverts commit 391d0fdde2b69d067e30c2d3011046aab8b929c9.
2025-04-12 15:57:36 +01:00
391d0fdde2 FIXUP wip: notifications on starting progress 2025-04-12 15:55:58 +01:00
ab86bff35f chore: removing unused files 2025-04-12 14:44:16 +01:00
1560d1504e fix: tests 2025-04-12 14:43:01 +01:00
ae9ac1fe4f refactor(agent): main agent loop extracted away
Still not super sure how to represent these agents in code.
It doesn't make the most amount of sense to keep them in structs. A
curried function is more like it, with system prompt and tooling.

Maybe that's what I'll end up doing.
2025-04-12 14:39:16 +01:00
ca8583575e fix(event) 2025-04-12 14:15:07 +01:00
02490c6c84 fix(orchestrator): better describing the note taking agent 2025-04-12 07:53:43 +01:00
36e776789e fix(notes): improving note taking capabilities 2025-04-12 07:48:42 +01:00
df028aaedb fix(log): removing access token logging 2025-04-12 07:46:07 +01:00
70d4411270 feat(contact-agent): linking to existing instead of creating new ones 2025-04-12 07:29:29 +01:00
6b181aac9f fix: removing extra log line 2025-04-12 07:22:35 +01:00
324aac438b feat(log): pretty logging agent responses and tool calls 2025-04-12 07:16:30 +01:00
b0f4a45c40 feat(contact-agent): working contact agent
Built this in under 20 minutes. Getting some really good agents
2025-04-11 21:12:06 +01:00
c02ccfd274 feat: contacts working 2025-04-11 20:31:51 +01:00
7264c6ed32 feat(authorization): e2e working authorization 2025-04-11 19:58:25 +01:00
283265c8c5 feat: checking user authorization on image retrieval 2025-04-11 19:41:36 +01:00
5b03d38392 wip(token): verifying user when getting the image 2025-04-11 19:35:49 +01:00
207f263853 fix: not using cookies anymore
I think Tauri doesn't like it very much
2025-04-11 13:27:19 +01:00
5b7fdd9f3e feat(cookies): using HTTP setCookie instead of manually doing it 2025-04-11 11:34:32 +01:00
4dbe1508c2 feat: minimal UI for login 2025-04-11 11:08:33 +01:00
36d60d7985 feat(jwt): validating token 2025-04-10 15:49:53 +01:00
a86addc8b2 feat(jwt): adding access and refresh token generation 2025-04-10 15:35:35 +01:00
06d2f1db6e feat(email): endpoint for sending auth code 2025-04-09 18:24:26 +01:00
b209de5c5d wip(email-auth): auth and email modules 2025-04-09 18:13:49 +01:00
680003b626 refactor: moving image listener to own function 2025-04-09 17:20:27 +01:00
1a9b707533 feat(orchestrator): async processing and ending the loop3 2025-04-09 15:23:51 +01:00
c35951063a fix(tool-calls): ToolLoop 2025-04-09 15:15:31 +01:00
f294f9cdc0 fix(tools): testing and processing
fix
2025-04-09 13:56:30 +01:00
88fda32125 fix(types): agent processing stuff 2025-04-09 12:12:09 +01:00
5502fc6b19 feat(chat): more simplified chat messages and tool handling 2025-04-09 12:04:44 +01:00
28ee32e2ff fixup(chat): better way to organize agent messages and tool calls 2025-04-06 20:24:40 +01:00
f5f4008034 fix(tools): dont error if AI invested a tool 2025-04-05 15:04:09 +01:00
d474b1700a refactor(tools): removing pointer map
This is not needed
2025-04-05 14:59:50 +01:00
d78f34a7aa feat(tools): return error to agent if any happened 2025-04-05 14:58:38 +01:00
1cafc31e0a test(tools): more robust multiple tool call handling 2025-04-05 14:52:31 +01:00
a1ce96d2e3 test(tools): starting test suite for tools 2025-04-05 14:35:54 +01:00
03e7803467 feat(orchestrator): calling needed agents when it needs to 2025-04-05 11:01:43 +01:00
286a9a8472 fix(tool): raw text not scaling so well ey? 2025-04-04 22:50:19 +01:00
aa153de185 refactor(agents): working e2e now
I guess some repeated code doesnt hurt anyone, if it keeps things
simpler. Trying to be fancy with the interfaces didn't work so well.
2025-04-04 22:40:45 +01:00
cd27f1105a refactor(tool-calls): to be handled more generally 2025-04-04 22:17:58 +01:00
71d4581110 refactor(ai-client): moving tool handling and client into seperate folders 2025-04-04 22:03:46 +01:00
8a165c2042 wip(orchestrator): basic scaffolding for the agent 2025-04-04 20:40:31 +01:00
fe7c92b622 feat: sample data 2025-04-02 17:32:52 +00:00
745265773b feat(notes): allowing frontend to save 2025-04-01 20:54:15 +00:00
f72ee73020 feat(notes): saving the notes for any images for easy text searching 2025-04-01 20:45:43 +00:00
1cb6510465 feat(events): search through organizer 2025-04-01 19:59:17 +00:00
af485aec49 feat: adding rawData search 2025-04-01 19:48:06 +00:00
4320bd7fe9 fix(validators): allowing location in event fields 2025-04-01 19:33:31 +00:00
9652549b01 fix: adding location to general query 2025-04-01 19:32:55 +00:00
b56250c1f8 Merge branch 'integrating-frontend' 2025-04-01 19:30:51 +00:00
35f004752d fix 2025-03-31 20:17:14 +00:00
901f214f9d feat(contacts): events can now have organizers 2025-03-31 18:40:36 +00:00
0c78d741f0 feat(schema): basic contact tables 2025-03-31 18:10:04 +00:00
7c5d2f9433 chore(imports): organisation 2025-03-31 18:03:02 +00:00
e29f93bcd5 feat(cards): adjusting for backend data types 2025-03-31 17:49:17 +00:00
13e82334ca feat(events): adding start and end times 2025-03-31 17:32:55 +00:00
59737ca9ac feat(schema): removing coordinates and adding start times to events
.
2025-03-31 16:44:42 +00:00
b6969127eb fix(backend): SQL statements without returns 2025-03-26 16:51:46 +00:00
2645cbb1c2 refactor(validators): frontend to new schema 2025-03-26 16:51:35 +00:00
d716e66463 feat: attaching both to image 2025-03-26 16:22:23 +00:00
a576355e7c feat: creating events and attaching locations 2025-03-26 16:16:48 +00:00
2dcb59c19d feat(frontend): add new search card components and update styling
- Introduced new search card components for Contact, Event, Location, Note, Receipt, and Website.
- Updated the App component to utilize these new components for displaying search results.
- Changed the default font from Manrope to Switzer and updated related styles.
- Added a new dependency `solid-motionone` to package.json.
- Improved search functionality with a new sample data structure and enhanced search logic.
2025-03-23 21:56:09 +01:00
2c279bb68f refactor(frontend): clean up App component and improve search functionality 2025-03-23 19:10:18 +01:00
6f938a34e3 feat(tool-calling) Big refactor on how tool calling is handled
these commits are too big
2025-03-22 20:46:26 +00:00
7debe6bab2 feat(locations): allowing AI to attach it to the image 2025-03-22 17:47:02 +00:00
4c4bf7a9e4 feat(location): working e2e with tool calling 2025-03-22 12:22:31 +00:00
aad45fcf52 feat(tool-calls): listLocation tool call handling 2025-03-22 11:14:00 +00:00
7c473e054a feat: using tools for event loocation agent 2025-03-22 10:12:51 +00:00
7f96d2fc45 refactor(naming): using Agent instead of openai 2025-03-21 17:07:00 +00:00
df90eaa6ad fix: docker image 2025-03-21 14:49:51 +00:00
44d506bc69 wip(frontend): adding more information 2025-03-21 14:36:03 +00:00
45f3e11214 fix: app to re-include images 2025-03-21 14:23:38 +00:00
58f7afb521 fix: re-adding location to event
Figured it out
2025-03-21 14:08:06 +00:00
7b64563647 fix: simplifying query
I do have some learning to do about go-jet specifically.

I can't include the Location field on events, because the QRM will add
any location to the events
2025-03-21 14:01:21 +00:00
5b794b2e7f feat: frontend validation 2025-03-21 13:44:42 +00:00
72a3e58ef9 feat: returning events and locations from end point 2025-03-20 18:34:11 +00:00
f042c9dfcc feat: working e2e solution 2025-03-20 17:59:00 +00:00
d2dd43c6b2 merging 2025-03-19 09:46:52 +00:00
56d423d261 feat: adding events and locations to json schema 2025-03-19 09:46:42 +00:00
072eebc0bf feat(events): also working 2025-03-18 18:37:12 +00:00
28dd02a47d feat(locations): now working 2025-03-18 18:18:01 +00:00
2e1809aa27 refactor(text,tags,links): to foreign key to image instead of user_image 2025-03-18 17:48:38 +00:00
e76a4b901c wip: add sample data and types 2025-03-17 22:20:26 +01:00
b359e9d61d fix: linter and format issues
format
2025-03-17 21:16:40 +01:00
9b5113f428 Merge branch 'main' of https://git.johncosta.tech/JohnCosta27/Haystack 2025-03-17 21:01:15 +01:00
b29a013cde wip 2025-03-17 21:00:52 +01:00
32b5b08dcf fix: frontend parsers 2025-03-16 18:41:26 +00:00
439f729150 fix: returning whole tag object 2025-03-16 18:29:15 +00:00
bfd6d136dc fix: returns correct image ID 2025-03-16 18:20:29 +00:00
1d5d90c3b5 refactor(models): using more organised structure 2025-03-16 18:13:30 +00:00
b8ee0c2381 Merge branch 'main' of https://github.com/dimuuu/haystack-app 2025-03-13 17:25:16 +01:00
d9f972f674 wip 2025-03-13 17:24:53 +01:00
d4c6aa0310 feat(tags): correctly inserting new tags and adding them to images 2025-03-11 22:47:28 +00:00
6fcb1e9f26 feat(tags): creating and getting user tags 2025-03-11 21:23:41 +00:00
c215bf6909 feat: new schema to support user tags better 2025-03-11 20:29:56 +00:00
234988399d fix: actually searching properly 2025-03-08 15:50:21 +00:00
aee49c313b feat: better result display 2025-03-08 15:42:16 +00:00
9e3896a30f feat: super basic image search 2025-03-08 15:37:10 +00:00
e5ac7061f4 feat: sending images and receiving them is now working 2025-03-08 13:13:05 +00:00
03a4d49ee6 feat: sending base64 image to backend
This is silly, but binary is apparently hard to do????
2025-03-08 12:30:16 +00:00
c025eebea8 wip: dialog to choose folder to watch 2025-03-08 11:58:25 +00:00
53c4ec1869 fix: using json response header 2025-03-07 14:14:40 +00:00
5192aeb70f wip: Using mistral instead of OpenAi 2025-03-07 13:42:50 +00:00
3bd3b420c9 chore: running format 2025-02-26 21:27:43 +00:00
06df315ed0 feat: network file with validators for backend requests 2025-02-26 21:27:37 +00:00
a3dee09011 feat: making prompt be more generic with tags 2025-02-26 20:53:12 +00:00
a4136fe3f8 feat: using different prompt 2025-02-26 20:49:55 +00:00
0ec2ae65d2 fix: some spam from get images request 2025-02-26 20:48:52 +00:00
79ee5e23ca fix: getting user images 2025-02-26 20:09:19 +00:00
1f16cfb30b 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.
2025-02-26 20:01:56 +00:00
2a37e37c4b refactor: using chi router + bug fixes 2025-02-26 18:04:30 +00:00
b29f5b1bb5 fix: actually saving to the correct db table 2025-02-26 15:54:27 +00:00
111c6ac74f fix: actually returning all the user images
fix
2025-02-24 21:19:59 +00:00
24fc2d7c84 chore: documentation 2025-02-24 21:04:25 +00:00
c36fb1c0d6 feat: saving AI information to database 2025-02-24 21:00:05 +00:00
e26835861d feat: method for getting images 2025-02-24 20:05:56 +00:00
e69d7b5c08 feat: methods to get image 2025-02-24 20:02:58 +00:00
c0fe7e1853 feat: instructions for docker compose 2025-02-24 20:02:54 +00:00
c9cd0df9ca feat: working docker image and compose file 2025-02-24 19:44:19 +00:00
18ecfecd54 feat: using parsing on response 2025-02-24 19:05:11 +00:00
51d14b0eba chore: updating gitignore
fix
2025-02-24 19:01:18 +00:00
4a32e99a5c feat: parsing response from open ai
bruh
2025-02-24 19:01:18 +00:00
098fd05dfd build very basic ui 2025-02-23 22:16:41 +01:00
fe786690ad some updates 2025-02-23 20:11:58 +01:00
ac8d3387c5 messing around with ui 2025-02-23 20:02:06 +01:00
ec13e70024 added a bunch of frontend things 2025-02-23 19:30:11 +01:00
40debf1da3 Merge branch 'main' of https://github.com/dimuuu/haystack-app 2025-02-23 14:59:04 +01:00
b773abd51a some window ui tweaks 2025-02-23 14:59:01 +01:00
050126116c refactor: moving all files to backend 2025-02-22 23:30:59 +00:00
49680c00b2 Merge branch 'main' of github.com:dimuuu/haystack-app 2025-02-22 23:29:49 +00:00
cf452653a7 add biome 2025-02-22 16:41:52 +01:00
531b126cf5 v0 2025-02-22 16:30:41 +01:00
206 changed files with 16528 additions and 523 deletions

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer", "biomejs.biome", "golang.go", "bradlc.vscode-tailwindcss"]
}

12
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,12 @@
{
"biome.enabled": true,
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit"
},
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
},
"rust-analyzer.linkedProjects": ["./frontend/src-tauri/Cargo.toml"]
}

View File

@ -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 enum
import "github.com/go-jet/jet/v2/postgres"
var Progress = &struct {
NotStarted postgres.StringExpression
InProgress postgres.StringExpression
}{
NotStarted: postgres.NewEnumValue("not-started"),
InProgress: postgres.NewEnumValue("in-progress"),
}

View File

@ -0,0 +1,20 @@
//
// 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 Contacts struct {
ID uuid.UUID `sql:"primary_key"`
Name string
Description *string
PhoneNumber *string
Email *string
}

View File

@ -0,0 +1,23 @@
//
// 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"
"time"
)
type Events struct {
ID uuid.UUID `sql:"primary_key"`
Name string
Description *string
StartDateTime *time.Time
EndDateTime *time.Time
LocationID *uuid.UUID
OrganizerID *uuid.UUID
}

View File

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

View File

@ -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 ImageContacts struct {
ID uuid.UUID `sql:"primary_key"`
ImageID uuid.UUID
ContactID uuid.UUID
}

View File

@ -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 ImageEvents struct {
ID uuid.UUID `sql:"primary_key"`
EventID uuid.UUID
ImageID uuid.UUID
}

View File

@ -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 ImageLinks struct {
ID uuid.UUID `sql:"primary_key"`
Link string
ImageID uuid.UUID
}

View File

@ -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 ImageLocations struct {
ID uuid.UUID `sql:"primary_key"`
LocationID uuid.UUID
ImageID uuid.UUID
}

View File

@ -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 ImageNotes struct {
ID uuid.UUID `sql:"primary_key"`
ImageID uuid.UUID
NoteID uuid.UUID
}

View File

@ -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 ImageTags struct {
ID uuid.UUID `sql:"primary_key"`
TagID uuid.UUID
ImageID uuid.UUID
}

View File

@ -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 ImageText struct {
ID uuid.UUID `sql:"primary_key"`
ImageText string
ImageID uuid.UUID
}

View File

@ -0,0 +1,19 @@
//
// 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 Locations struct {
ID uuid.UUID `sql:"primary_key"`
Name string
Address *string
Description *string
}

View File

@ -0,0 +1,17 @@
//
// 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 Logs struct {
Log string
ImageID uuid.UUID
}

View File

@ -0,0 +1,19 @@
//
// 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 Notes struct {
ID uuid.UUID `sql:"primary_key"`
Name string
Description *string
Content string
}

View File

@ -0,0 +1,49 @@
//
// 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 "errors"
type Progress string
const (
Progress_NotStarted Progress = "not-started"
Progress_InProgress Progress = "in-progress"
)
var ProgressAllValues = []Progress{
Progress_NotStarted,
Progress_InProgress,
}
func (e *Progress) Scan(value interface{}) error {
var enumValue string
switch val := value.(type) {
case string:
enumValue = val
case []byte:
enumValue = string(val)
default:
return errors.New("jet: Invalid scan value for AllTypesEnum enum. Enum value has to be of type string or []byte")
}
switch enumValue {
case "not-started":
*e = Progress_NotStarted
case "in-progress":
*e = Progress_InProgress
default:
return errors.New("jet: Invalid scan value '" + enumValue + "' for Progress enum")
}
return nil
}
func (e Progress) String() string {
return string(e)
}

View File

@ -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 UserContacts struct {
ID uuid.UUID `sql:"primary_key"`
UserID uuid.UUID
ContactID uuid.UUID
}

View File

@ -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 UserEvents struct {
ID uuid.UUID `sql:"primary_key"`
EventID uuid.UUID
UserID uuid.UUID
}

View File

@ -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 UserImages struct {
ID uuid.UUID `sql:"primary_key"`
ImageID uuid.UUID
UserID uuid.UUID
}

View File

@ -0,0 +1,19 @@
//
// 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"`
Status Progress
ImageID uuid.UUID
UserID uuid.UUID
}

View File

@ -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 UserLocations struct {
ID uuid.UUID `sql:"primary_key"`
LocationID uuid.UUID
UserID uuid.UUID
}

View File

@ -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 UserNotes struct {
ID uuid.UUID `sql:"primary_key"`
UserID uuid.UUID
NoteID uuid.UUID
}

View File

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

View File

@ -0,0 +1,17 @@
//
// 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 Users struct {
ID uuid.UUID `sql:"primary_key"`
Email string
}

View File

@ -0,0 +1,87 @@
//
// 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 Contacts = newContactsTable("haystack", "contacts", "")
type contactsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
Name postgres.ColumnString
Description postgres.ColumnString
PhoneNumber postgres.ColumnString
Email postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type ContactsTable struct {
contactsTable
EXCLUDED contactsTable
}
// AS creates new ContactsTable with assigned alias
func (a ContactsTable) AS(alias string) *ContactsTable {
return newContactsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ContactsTable with assigned schema name
func (a ContactsTable) FromSchema(schemaName string) *ContactsTable {
return newContactsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ContactsTable with assigned table prefix
func (a ContactsTable) WithPrefix(prefix string) *ContactsTable {
return newContactsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ContactsTable with assigned table suffix
func (a ContactsTable) WithSuffix(suffix string) *ContactsTable {
return newContactsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newContactsTable(schemaName, tableName, alias string) *ContactsTable {
return &ContactsTable{
contactsTable: newContactsTableImpl(schemaName, tableName, alias),
EXCLUDED: newContactsTableImpl("", "excluded", ""),
}
}
func newContactsTableImpl(schemaName, tableName, alias string) contactsTable {
var (
IDColumn = postgres.StringColumn("id")
NameColumn = postgres.StringColumn("name")
DescriptionColumn = postgres.StringColumn("description")
PhoneNumberColumn = postgres.StringColumn("phone_number")
EmailColumn = postgres.StringColumn("email")
allColumns = postgres.ColumnList{IDColumn, NameColumn, DescriptionColumn, PhoneNumberColumn, EmailColumn}
mutableColumns = postgres.ColumnList{NameColumn, DescriptionColumn, PhoneNumberColumn, EmailColumn}
)
return contactsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Name: NameColumn,
Description: DescriptionColumn,
PhoneNumber: PhoneNumberColumn,
Email: EmailColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -0,0 +1,93 @@
//
// 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 Events = newEventsTable("haystack", "events", "")
type eventsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
Name postgres.ColumnString
Description postgres.ColumnString
StartDateTime postgres.ColumnTimestamp
EndDateTime postgres.ColumnTimestamp
LocationID postgres.ColumnString
OrganizerID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type EventsTable struct {
eventsTable
EXCLUDED eventsTable
}
// AS creates new EventsTable with assigned alias
func (a EventsTable) AS(alias string) *EventsTable {
return newEventsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new EventsTable with assigned schema name
func (a EventsTable) FromSchema(schemaName string) *EventsTable {
return newEventsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new EventsTable with assigned table prefix
func (a EventsTable) WithPrefix(prefix string) *EventsTable {
return newEventsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new EventsTable with assigned table suffix
func (a EventsTable) WithSuffix(suffix string) *EventsTable {
return newEventsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newEventsTable(schemaName, tableName, alias string) *EventsTable {
return &EventsTable{
eventsTable: newEventsTableImpl(schemaName, tableName, alias),
EXCLUDED: newEventsTableImpl("", "excluded", ""),
}
}
func newEventsTableImpl(schemaName, tableName, alias string) eventsTable {
var (
IDColumn = postgres.StringColumn("id")
NameColumn = postgres.StringColumn("name")
DescriptionColumn = postgres.StringColumn("description")
StartDateTimeColumn = postgres.TimestampColumn("start_date_time")
EndDateTimeColumn = postgres.TimestampColumn("end_date_time")
LocationIDColumn = postgres.StringColumn("location_id")
OrganizerIDColumn = postgres.StringColumn("organizer_id")
allColumns = postgres.ColumnList{IDColumn, NameColumn, DescriptionColumn, StartDateTimeColumn, EndDateTimeColumn, LocationIDColumn, OrganizerIDColumn}
mutableColumns = postgres.ColumnList{NameColumn, DescriptionColumn, StartDateTimeColumn, EndDateTimeColumn, LocationIDColumn, OrganizerIDColumn}
)
return eventsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Name: NameColumn,
Description: DescriptionColumn,
StartDateTime: StartDateTimeColumn,
EndDateTime: EndDateTimeColumn,
LocationID: LocationIDColumn,
OrganizerID: OrganizerIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

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

View File

@ -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 ImageContacts = newImageContactsTable("haystack", "image_contacts", "")
type imageContactsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
ImageID postgres.ColumnString
ContactID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type ImageContactsTable struct {
imageContactsTable
EXCLUDED imageContactsTable
}
// AS creates new ImageContactsTable with assigned alias
func (a ImageContactsTable) AS(alias string) *ImageContactsTable {
return newImageContactsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ImageContactsTable with assigned schema name
func (a ImageContactsTable) FromSchema(schemaName string) *ImageContactsTable {
return newImageContactsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ImageContactsTable with assigned table prefix
func (a ImageContactsTable) WithPrefix(prefix string) *ImageContactsTable {
return newImageContactsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ImageContactsTable with assigned table suffix
func (a ImageContactsTable) WithSuffix(suffix string) *ImageContactsTable {
return newImageContactsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newImageContactsTable(schemaName, tableName, alias string) *ImageContactsTable {
return &ImageContactsTable{
imageContactsTable: newImageContactsTableImpl(schemaName, tableName, alias),
EXCLUDED: newImageContactsTableImpl("", "excluded", ""),
}
}
func newImageContactsTableImpl(schemaName, tableName, alias string) imageContactsTable {
var (
IDColumn = postgres.StringColumn("id")
ImageIDColumn = postgres.StringColumn("image_id")
ContactIDColumn = postgres.StringColumn("contact_id")
allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, ContactIDColumn}
mutableColumns = postgres.ColumnList{ImageIDColumn, ContactIDColumn}
)
return imageContactsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
ImageID: ImageIDColumn,
ContactID: ContactIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -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 ImageEvents = newImageEventsTable("haystack", "image_events", "")
type imageEventsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
EventID postgres.ColumnString
ImageID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type ImageEventsTable struct {
imageEventsTable
EXCLUDED imageEventsTable
}
// AS creates new ImageEventsTable with assigned alias
func (a ImageEventsTable) AS(alias string) *ImageEventsTable {
return newImageEventsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ImageEventsTable with assigned schema name
func (a ImageEventsTable) FromSchema(schemaName string) *ImageEventsTable {
return newImageEventsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ImageEventsTable with assigned table prefix
func (a ImageEventsTable) WithPrefix(prefix string) *ImageEventsTable {
return newImageEventsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ImageEventsTable with assigned table suffix
func (a ImageEventsTable) WithSuffix(suffix string) *ImageEventsTable {
return newImageEventsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newImageEventsTable(schemaName, tableName, alias string) *ImageEventsTable {
return &ImageEventsTable{
imageEventsTable: newImageEventsTableImpl(schemaName, tableName, alias),
EXCLUDED: newImageEventsTableImpl("", "excluded", ""),
}
}
func newImageEventsTableImpl(schemaName, tableName, alias string) imageEventsTable {
var (
IDColumn = postgres.StringColumn("id")
EventIDColumn = postgres.StringColumn("event_id")
ImageIDColumn = postgres.StringColumn("image_id")
allColumns = postgres.ColumnList{IDColumn, EventIDColumn, ImageIDColumn}
mutableColumns = postgres.ColumnList{EventIDColumn, ImageIDColumn}
)
return imageEventsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
EventID: EventIDColumn,
ImageID: ImageIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -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 ImageLinks = newImageLinksTable("haystack", "image_links", "")
type imageLinksTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
Link postgres.ColumnString
ImageID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type ImageLinksTable struct {
imageLinksTable
EXCLUDED imageLinksTable
}
// AS creates new ImageLinksTable with assigned alias
func (a ImageLinksTable) AS(alias string) *ImageLinksTable {
return newImageLinksTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ImageLinksTable with assigned schema name
func (a ImageLinksTable) FromSchema(schemaName string) *ImageLinksTable {
return newImageLinksTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ImageLinksTable with assigned table prefix
func (a ImageLinksTable) WithPrefix(prefix string) *ImageLinksTable {
return newImageLinksTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ImageLinksTable with assigned table suffix
func (a ImageLinksTable) WithSuffix(suffix string) *ImageLinksTable {
return newImageLinksTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newImageLinksTable(schemaName, tableName, alias string) *ImageLinksTable {
return &ImageLinksTable{
imageLinksTable: newImageLinksTableImpl(schemaName, tableName, alias),
EXCLUDED: newImageLinksTableImpl("", "excluded", ""),
}
}
func newImageLinksTableImpl(schemaName, tableName, alias string) imageLinksTable {
var (
IDColumn = postgres.StringColumn("id")
LinkColumn = postgres.StringColumn("link")
ImageIDColumn = postgres.StringColumn("image_id")
allColumns = postgres.ColumnList{IDColumn, LinkColumn, ImageIDColumn}
mutableColumns = postgres.ColumnList{LinkColumn, ImageIDColumn}
)
return imageLinksTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Link: LinkColumn,
ImageID: ImageIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -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 ImageLocations = newImageLocationsTable("haystack", "image_locations", "")
type imageLocationsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
LocationID postgres.ColumnString
ImageID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type ImageLocationsTable struct {
imageLocationsTable
EXCLUDED imageLocationsTable
}
// AS creates new ImageLocationsTable with assigned alias
func (a ImageLocationsTable) AS(alias string) *ImageLocationsTable {
return newImageLocationsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ImageLocationsTable with assigned schema name
func (a ImageLocationsTable) FromSchema(schemaName string) *ImageLocationsTable {
return newImageLocationsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ImageLocationsTable with assigned table prefix
func (a ImageLocationsTable) WithPrefix(prefix string) *ImageLocationsTable {
return newImageLocationsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ImageLocationsTable with assigned table suffix
func (a ImageLocationsTable) WithSuffix(suffix string) *ImageLocationsTable {
return newImageLocationsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newImageLocationsTable(schemaName, tableName, alias string) *ImageLocationsTable {
return &ImageLocationsTable{
imageLocationsTable: newImageLocationsTableImpl(schemaName, tableName, alias),
EXCLUDED: newImageLocationsTableImpl("", "excluded", ""),
}
}
func newImageLocationsTableImpl(schemaName, tableName, alias string) imageLocationsTable {
var (
IDColumn = postgres.StringColumn("id")
LocationIDColumn = postgres.StringColumn("location_id")
ImageIDColumn = postgres.StringColumn("image_id")
allColumns = postgres.ColumnList{IDColumn, LocationIDColumn, ImageIDColumn}
mutableColumns = postgres.ColumnList{LocationIDColumn, ImageIDColumn}
)
return imageLocationsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
LocationID: LocationIDColumn,
ImageID: ImageIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -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 ImageNotes = newImageNotesTable("haystack", "image_notes", "")
type imageNotesTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
ImageID postgres.ColumnString
NoteID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type ImageNotesTable struct {
imageNotesTable
EXCLUDED imageNotesTable
}
// AS creates new ImageNotesTable with assigned alias
func (a ImageNotesTable) AS(alias string) *ImageNotesTable {
return newImageNotesTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ImageNotesTable with assigned schema name
func (a ImageNotesTable) FromSchema(schemaName string) *ImageNotesTable {
return newImageNotesTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ImageNotesTable with assigned table prefix
func (a ImageNotesTable) WithPrefix(prefix string) *ImageNotesTable {
return newImageNotesTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ImageNotesTable with assigned table suffix
func (a ImageNotesTable) WithSuffix(suffix string) *ImageNotesTable {
return newImageNotesTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newImageNotesTable(schemaName, tableName, alias string) *ImageNotesTable {
return &ImageNotesTable{
imageNotesTable: newImageNotesTableImpl(schemaName, tableName, alias),
EXCLUDED: newImageNotesTableImpl("", "excluded", ""),
}
}
func newImageNotesTableImpl(schemaName, tableName, alias string) imageNotesTable {
var (
IDColumn = postgres.StringColumn("id")
ImageIDColumn = postgres.StringColumn("image_id")
NoteIDColumn = postgres.StringColumn("note_id")
allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, NoteIDColumn}
mutableColumns = postgres.ColumnList{ImageIDColumn, NoteIDColumn}
)
return imageNotesTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
ImageID: ImageIDColumn,
NoteID: NoteIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -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 ImageTags = newImageTagsTable("haystack", "image_tags", "")
type imageTagsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
TagID postgres.ColumnString
ImageID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type ImageTagsTable struct {
imageTagsTable
EXCLUDED imageTagsTable
}
// AS creates new ImageTagsTable with assigned alias
func (a ImageTagsTable) AS(alias string) *ImageTagsTable {
return newImageTagsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ImageTagsTable with assigned schema name
func (a ImageTagsTable) FromSchema(schemaName string) *ImageTagsTable {
return newImageTagsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ImageTagsTable with assigned table prefix
func (a ImageTagsTable) WithPrefix(prefix string) *ImageTagsTable {
return newImageTagsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ImageTagsTable with assigned table suffix
func (a ImageTagsTable) WithSuffix(suffix string) *ImageTagsTable {
return newImageTagsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newImageTagsTable(schemaName, tableName, alias string) *ImageTagsTable {
return &ImageTagsTable{
imageTagsTable: newImageTagsTableImpl(schemaName, tableName, alias),
EXCLUDED: newImageTagsTableImpl("", "excluded", ""),
}
}
func newImageTagsTableImpl(schemaName, tableName, alias string) imageTagsTable {
var (
IDColumn = postgres.StringColumn("id")
TagIDColumn = postgres.StringColumn("tag_id")
ImageIDColumn = postgres.StringColumn("image_id")
allColumns = postgres.ColumnList{IDColumn, TagIDColumn, ImageIDColumn}
mutableColumns = postgres.ColumnList{TagIDColumn, ImageIDColumn}
)
return imageTagsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
TagID: TagIDColumn,
ImageID: ImageIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -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 ImageText = newImageTextTable("haystack", "image_text", "")
type imageTextTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
ImageText postgres.ColumnString
ImageID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type ImageTextTable struct {
imageTextTable
EXCLUDED imageTextTable
}
// AS creates new ImageTextTable with assigned alias
func (a ImageTextTable) AS(alias string) *ImageTextTable {
return newImageTextTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ImageTextTable with assigned schema name
func (a ImageTextTable) FromSchema(schemaName string) *ImageTextTable {
return newImageTextTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ImageTextTable with assigned table prefix
func (a ImageTextTable) WithPrefix(prefix string) *ImageTextTable {
return newImageTextTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ImageTextTable with assigned table suffix
func (a ImageTextTable) WithSuffix(suffix string) *ImageTextTable {
return newImageTextTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newImageTextTable(schemaName, tableName, alias string) *ImageTextTable {
return &ImageTextTable{
imageTextTable: newImageTextTableImpl(schemaName, tableName, alias),
EXCLUDED: newImageTextTableImpl("", "excluded", ""),
}
}
func newImageTextTableImpl(schemaName, tableName, alias string) imageTextTable {
var (
IDColumn = postgres.StringColumn("id")
ImageTextColumn = postgres.StringColumn("image_text")
ImageIDColumn = postgres.StringColumn("image_id")
allColumns = postgres.ColumnList{IDColumn, ImageTextColumn, ImageIDColumn}
mutableColumns = postgres.ColumnList{ImageTextColumn, ImageIDColumn}
)
return imageTextTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
ImageText: ImageTextColumn,
ImageID: ImageIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -0,0 +1,84 @@
//
// 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 Locations = newLocationsTable("haystack", "locations", "")
type locationsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
Name postgres.ColumnString
Address postgres.ColumnString
Description postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type LocationsTable struct {
locationsTable
EXCLUDED locationsTable
}
// AS creates new LocationsTable with assigned alias
func (a LocationsTable) AS(alias string) *LocationsTable {
return newLocationsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new LocationsTable with assigned schema name
func (a LocationsTable) FromSchema(schemaName string) *LocationsTable {
return newLocationsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new LocationsTable with assigned table prefix
func (a LocationsTable) WithPrefix(prefix string) *LocationsTable {
return newLocationsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new LocationsTable with assigned table suffix
func (a LocationsTable) WithSuffix(suffix string) *LocationsTable {
return newLocationsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newLocationsTable(schemaName, tableName, alias string) *LocationsTable {
return &LocationsTable{
locationsTable: newLocationsTableImpl(schemaName, tableName, alias),
EXCLUDED: newLocationsTableImpl("", "excluded", ""),
}
}
func newLocationsTableImpl(schemaName, tableName, alias string) locationsTable {
var (
IDColumn = postgres.StringColumn("id")
NameColumn = postgres.StringColumn("name")
AddressColumn = postgres.StringColumn("address")
DescriptionColumn = postgres.StringColumn("description")
allColumns = postgres.ColumnList{IDColumn, NameColumn, AddressColumn, DescriptionColumn}
mutableColumns = postgres.ColumnList{NameColumn, AddressColumn, DescriptionColumn}
)
return locationsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Name: NameColumn,
Address: AddressColumn,
Description: DescriptionColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -0,0 +1,78 @@
//
// 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 Logs = newLogsTable("haystack", "logs", "")
type logsTable struct {
postgres.Table
// Columns
Log postgres.ColumnString
ImageID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type LogsTable struct {
logsTable
EXCLUDED logsTable
}
// AS creates new LogsTable with assigned alias
func (a LogsTable) AS(alias string) *LogsTable {
return newLogsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new LogsTable with assigned schema name
func (a LogsTable) FromSchema(schemaName string) *LogsTable {
return newLogsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new LogsTable with assigned table prefix
func (a LogsTable) WithPrefix(prefix string) *LogsTable {
return newLogsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new LogsTable with assigned table suffix
func (a LogsTable) WithSuffix(suffix string) *LogsTable {
return newLogsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newLogsTable(schemaName, tableName, alias string) *LogsTable {
return &LogsTable{
logsTable: newLogsTableImpl(schemaName, tableName, alias),
EXCLUDED: newLogsTableImpl("", "excluded", ""),
}
}
func newLogsTableImpl(schemaName, tableName, alias string) logsTable {
var (
LogColumn = postgres.StringColumn("log")
ImageIDColumn = postgres.StringColumn("image_id")
allColumns = postgres.ColumnList{LogColumn, ImageIDColumn}
mutableColumns = postgres.ColumnList{LogColumn, ImageIDColumn}
)
return logsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
Log: LogColumn,
ImageID: ImageIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -0,0 +1,84 @@
//
// 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 Notes = newNotesTable("haystack", "notes", "")
type notesTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
Name postgres.ColumnString
Description postgres.ColumnString
Content postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type NotesTable struct {
notesTable
EXCLUDED notesTable
}
// AS creates new NotesTable with assigned alias
func (a NotesTable) AS(alias string) *NotesTable {
return newNotesTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new NotesTable with assigned schema name
func (a NotesTable) FromSchema(schemaName string) *NotesTable {
return newNotesTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new NotesTable with assigned table prefix
func (a NotesTable) WithPrefix(prefix string) *NotesTable {
return newNotesTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new NotesTable with assigned table suffix
func (a NotesTable) WithSuffix(suffix string) *NotesTable {
return newNotesTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newNotesTable(schemaName, tableName, alias string) *NotesTable {
return &NotesTable{
notesTable: newNotesTableImpl(schemaName, tableName, alias),
EXCLUDED: newNotesTableImpl("", "excluded", ""),
}
}
func newNotesTableImpl(schemaName, tableName, alias string) notesTable {
var (
IDColumn = postgres.StringColumn("id")
NameColumn = postgres.StringColumn("name")
DescriptionColumn = postgres.StringColumn("description")
ContentColumn = postgres.StringColumn("content")
allColumns = postgres.ColumnList{IDColumn, NameColumn, DescriptionColumn, ContentColumn}
mutableColumns = postgres.ColumnList{NameColumn, DescriptionColumn, ContentColumn}
)
return notesTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Name: NameColumn,
Description: DescriptionColumn,
Content: ContentColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -0,0 +1,34 @@
//
// 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
// 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) {
Contacts = Contacts.FromSchema(schema)
Events = Events.FromSchema(schema)
Image = Image.FromSchema(schema)
ImageContacts = ImageContacts.FromSchema(schema)
ImageEvents = ImageEvents.FromSchema(schema)
ImageLinks = ImageLinks.FromSchema(schema)
ImageLocations = ImageLocations.FromSchema(schema)
ImageNotes = ImageNotes.FromSchema(schema)
ImageTags = ImageTags.FromSchema(schema)
ImageText = ImageText.FromSchema(schema)
Locations = Locations.FromSchema(schema)
Logs = Logs.FromSchema(schema)
Notes = Notes.FromSchema(schema)
UserContacts = UserContacts.FromSchema(schema)
UserEvents = UserEvents.FromSchema(schema)
UserImages = UserImages.FromSchema(schema)
UserImagesToProcess = UserImagesToProcess.FromSchema(schema)
UserLocations = UserLocations.FromSchema(schema)
UserNotes = UserNotes.FromSchema(schema)
UserTags = UserTags.FromSchema(schema)
Users = Users.FromSchema(schema)
}

View File

@ -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 UserContacts = newUserContactsTable("haystack", "user_contacts", "")
type userContactsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
UserID postgres.ColumnString
ContactID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type UserContactsTable struct {
userContactsTable
EXCLUDED userContactsTable
}
// AS creates new UserContactsTable with assigned alias
func (a UserContactsTable) AS(alias string) *UserContactsTable {
return newUserContactsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new UserContactsTable with assigned schema name
func (a UserContactsTable) FromSchema(schemaName string) *UserContactsTable {
return newUserContactsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new UserContactsTable with assigned table prefix
func (a UserContactsTable) WithPrefix(prefix string) *UserContactsTable {
return newUserContactsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new UserContactsTable with assigned table suffix
func (a UserContactsTable) WithSuffix(suffix string) *UserContactsTable {
return newUserContactsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newUserContactsTable(schemaName, tableName, alias string) *UserContactsTable {
return &UserContactsTable{
userContactsTable: newUserContactsTableImpl(schemaName, tableName, alias),
EXCLUDED: newUserContactsTableImpl("", "excluded", ""),
}
}
func newUserContactsTableImpl(schemaName, tableName, alias string) userContactsTable {
var (
IDColumn = postgres.StringColumn("id")
UserIDColumn = postgres.StringColumn("user_id")
ContactIDColumn = postgres.StringColumn("contact_id")
allColumns = postgres.ColumnList{IDColumn, UserIDColumn, ContactIDColumn}
mutableColumns = postgres.ColumnList{UserIDColumn, ContactIDColumn}
)
return userContactsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
UserID: UserIDColumn,
ContactID: ContactIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -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 UserEvents = newUserEventsTable("haystack", "user_events", "")
type userEventsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
EventID postgres.ColumnString
UserID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type UserEventsTable struct {
userEventsTable
EXCLUDED userEventsTable
}
// AS creates new UserEventsTable with assigned alias
func (a UserEventsTable) AS(alias string) *UserEventsTable {
return newUserEventsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new UserEventsTable with assigned schema name
func (a UserEventsTable) FromSchema(schemaName string) *UserEventsTable {
return newUserEventsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new UserEventsTable with assigned table prefix
func (a UserEventsTable) WithPrefix(prefix string) *UserEventsTable {
return newUserEventsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new UserEventsTable with assigned table suffix
func (a UserEventsTable) WithSuffix(suffix string) *UserEventsTable {
return newUserEventsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newUserEventsTable(schemaName, tableName, alias string) *UserEventsTable {
return &UserEventsTable{
userEventsTable: newUserEventsTableImpl(schemaName, tableName, alias),
EXCLUDED: newUserEventsTableImpl("", "excluded", ""),
}
}
func newUserEventsTableImpl(schemaName, tableName, alias string) userEventsTable {
var (
IDColumn = postgres.StringColumn("id")
EventIDColumn = postgres.StringColumn("event_id")
UserIDColumn = postgres.StringColumn("user_id")
allColumns = postgres.ColumnList{IDColumn, EventIDColumn, UserIDColumn}
mutableColumns = postgres.ColumnList{EventIDColumn, UserIDColumn}
)
return userEventsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
EventID: EventIDColumn,
UserID: UserIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -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 UserImages = newUserImagesTable("haystack", "user_images", "")
type userImagesTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
ImageID postgres.ColumnString
UserID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type UserImagesTable struct {
userImagesTable
EXCLUDED userImagesTable
}
// AS creates new UserImagesTable with assigned alias
func (a UserImagesTable) AS(alias string) *UserImagesTable {
return newUserImagesTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new UserImagesTable with assigned schema name
func (a UserImagesTable) FromSchema(schemaName string) *UserImagesTable {
return newUserImagesTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new UserImagesTable with assigned table prefix
func (a UserImagesTable) WithPrefix(prefix string) *UserImagesTable {
return newUserImagesTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new UserImagesTable with assigned table suffix
func (a UserImagesTable) WithSuffix(suffix string) *UserImagesTable {
return newUserImagesTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newUserImagesTable(schemaName, tableName, alias string) *UserImagesTable {
return &UserImagesTable{
userImagesTable: newUserImagesTableImpl(schemaName, tableName, alias),
EXCLUDED: newUserImagesTableImpl("", "excluded", ""),
}
}
func newUserImagesTableImpl(schemaName, tableName, alias string) userImagesTable {
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 userImagesTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
ImageID: ImageIDColumn,
UserID: UserIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -0,0 +1,84 @@
//
// 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
Status 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")
StatusColumn = postgres.StringColumn("status")
ImageIDColumn = postgres.StringColumn("image_id")
UserIDColumn = postgres.StringColumn("user_id")
allColumns = postgres.ColumnList{IDColumn, StatusColumn, ImageIDColumn, UserIDColumn}
mutableColumns = postgres.ColumnList{StatusColumn, ImageIDColumn, UserIDColumn}
)
return userImagesToProcessTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Status: StatusColumn,
ImageID: ImageIDColumn,
UserID: UserIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -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 UserLocations = newUserLocationsTable("haystack", "user_locations", "")
type userLocationsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
LocationID postgres.ColumnString
UserID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type UserLocationsTable struct {
userLocationsTable
EXCLUDED userLocationsTable
}
// AS creates new UserLocationsTable with assigned alias
func (a UserLocationsTable) AS(alias string) *UserLocationsTable {
return newUserLocationsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new UserLocationsTable with assigned schema name
func (a UserLocationsTable) FromSchema(schemaName string) *UserLocationsTable {
return newUserLocationsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new UserLocationsTable with assigned table prefix
func (a UserLocationsTable) WithPrefix(prefix string) *UserLocationsTable {
return newUserLocationsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new UserLocationsTable with assigned table suffix
func (a UserLocationsTable) WithSuffix(suffix string) *UserLocationsTable {
return newUserLocationsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newUserLocationsTable(schemaName, tableName, alias string) *UserLocationsTable {
return &UserLocationsTable{
userLocationsTable: newUserLocationsTableImpl(schemaName, tableName, alias),
EXCLUDED: newUserLocationsTableImpl("", "excluded", ""),
}
}
func newUserLocationsTableImpl(schemaName, tableName, alias string) userLocationsTable {
var (
IDColumn = postgres.StringColumn("id")
LocationIDColumn = postgres.StringColumn("location_id")
UserIDColumn = postgres.StringColumn("user_id")
allColumns = postgres.ColumnList{IDColumn, LocationIDColumn, UserIDColumn}
mutableColumns = postgres.ColumnList{LocationIDColumn, UserIDColumn}
)
return userLocationsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
LocationID: LocationIDColumn,
UserID: UserIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -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 UserNotes = newUserNotesTable("haystack", "user_notes", "")
type userNotesTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
UserID postgres.ColumnString
NoteID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type UserNotesTable struct {
userNotesTable
EXCLUDED userNotesTable
}
// AS creates new UserNotesTable with assigned alias
func (a UserNotesTable) AS(alias string) *UserNotesTable {
return newUserNotesTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new UserNotesTable with assigned schema name
func (a UserNotesTable) FromSchema(schemaName string) *UserNotesTable {
return newUserNotesTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new UserNotesTable with assigned table prefix
func (a UserNotesTable) WithPrefix(prefix string) *UserNotesTable {
return newUserNotesTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new UserNotesTable with assigned table suffix
func (a UserNotesTable) WithSuffix(suffix string) *UserNotesTable {
return newUserNotesTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newUserNotesTable(schemaName, tableName, alias string) *UserNotesTable {
return &UserNotesTable{
userNotesTable: newUserNotesTableImpl(schemaName, tableName, alias),
EXCLUDED: newUserNotesTableImpl("", "excluded", ""),
}
}
func newUserNotesTableImpl(schemaName, tableName, alias string) userNotesTable {
var (
IDColumn = postgres.StringColumn("id")
UserIDColumn = postgres.StringColumn("user_id")
NoteIDColumn = postgres.StringColumn("note_id")
allColumns = postgres.ColumnList{IDColumn, UserIDColumn, NoteIDColumn}
mutableColumns = postgres.ColumnList{UserIDColumn, NoteIDColumn}
)
return userNotesTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
UserID: UserIDColumn,
NoteID: NoteIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

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

View File

@ -0,0 +1,78 @@
//
// 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 Users = newUsersTable("haystack", "users", "")
type usersTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
Email postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type UsersTable struct {
usersTable
EXCLUDED usersTable
}
// AS creates new UsersTable with assigned alias
func (a UsersTable) AS(alias string) *UsersTable {
return newUsersTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new UsersTable with assigned schema name
func (a UsersTable) FromSchema(schemaName string) *UsersTable {
return newUsersTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new UsersTable with assigned table prefix
func (a UsersTable) WithPrefix(prefix string) *UsersTable {
return newUsersTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new UsersTable with assigned table suffix
func (a UsersTable) WithSuffix(suffix string) *UsersTable {
return newUsersTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newUsersTable(schemaName, tableName, alias string) *UsersTable {
return &UsersTable{
usersTable: newUsersTableImpl(schemaName, tableName, alias),
EXCLUDED: newUsersTableImpl("", "excluded", ""),
}
}
func newUsersTableImpl(schemaName, tableName, alias string) usersTable {
var (
IDColumn = postgres.StringColumn("id")
EmailColumn = postgres.StringColumn("email")
allColumns = postgres.ColumnList{IDColumn, EmailColumn}
mutableColumns = postgres.ColumnList{EmailColumn}
)
return usersTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Email: EmailColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

4
backend/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
screenmark
.env.docker
.env
pgdata

15
backend/Dockerfile Normal file
View File

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

26
backend/README.md Normal file
View File

@ -0,0 +1,26 @@
# 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`
# 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.

View File

@ -0,0 +1,239 @@
package client
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"path/filepath"
)
type Chat struct {
Messages []ChatMessage `json:"messages"`
}
type ChatMessage interface {
IsResponse() bool
}
// TODO: the role could be inferred from the type.
// This would solve some bugs.
/*
Is there a world where this actually becomes the product?
Where we build such a resilient system of AI calls that we
can build some app builder, or even just an API system,
with a fancy UI?
Manage all the complexity for the user?
*/
// =============================================
// Messages from us to the AI.
// =============================================
type UserRole = string
const (
User UserRole = "user"
System UserRole = "system"
)
type ToolRole = string
const (
Tool ToolRole = "tool"
)
type ChatUserMessage struct {
Role UserRole `json:"role"`
MessageContent `json:"MessageContent"`
}
func (m ChatUserMessage) MarshalJSON() ([]byte, error) {
switch t := m.MessageContent.(type) {
case SingleMessage:
return json.Marshal(&struct {
Role UserRole `json:"role"`
Content string `json:"content"`
}{
Role: User,
Content: t.Content,
})
case ArrayMessage:
return json.Marshal(&struct {
Role UserRole `json:"role"`
Content []MessageContentMessage `json:"content"`
}{
Role: User,
Content: t.Content,
})
}
return []byte{}, errors.New("Unreachable")
}
func (r ChatUserMessage) IsResponse() bool {
return false
}
type ChatUserToolResponse struct {
Role ToolRole `json:"role"`
// The name of the function we are responding to.
Name string `json:"name"`
Content string `json:"content"`
ToolCallId string `json:"tool_call_id"`
}
func (r ChatUserToolResponse) IsResponse() bool {
return false
}
type ChatAiMessage struct {
Role string `json:"role"`
ToolCalls *[]ToolCall `json:"tool_calls,omitempty"`
Content string `json:"content"`
}
func (m ChatAiMessage) IsResponse() bool {
return true
}
// =============================================
// Unique interface for message content.
// =============================================
type MessageContent interface {
IsSingleMessage() bool
}
type SingleMessage struct {
Content string `json:"content"`
}
func (m SingleMessage) IsSingleMessage() bool {
return true
}
type ArrayMessage struct {
Content []MessageContentMessage `json:"content"`
}
func (m ArrayMessage) IsSingleMessage() bool {
return false
}
type MessageContentMessage interface {
IsImageMessage() bool
}
type TextMessageContent struct {
TextType string `json:"type"`
Text string `json:"text"`
}
func (m TextMessageContent) IsImageMessage() bool {
return false
}
type ImageMessageContent struct {
ImageType string `json:"type"`
ImageUrl string `json:"image_url"`
}
func (m ImageMessageContent) IsImageMessage() bool {
return true
}
type ImageContentUrl struct {
Url string `json:"url"`
}
// =============================================
// Adjacent interfaces.
// =============================================
type ToolCall struct {
Index int `json:"index"`
Id string `json:"id"`
Function FunctionCall `json:"function"`
}
type FunctionCall struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
}
// =============================================
// Chat methods
// =============================================
func (chat *Chat) AddSystem(prompt string) {
chat.Messages = append(chat.Messages, ChatUserMessage{
Role: System,
MessageContent: SingleMessage{
Content: prompt,
},
})
}
func (chat *Chat) AddImage(imageName string, image []byte, query *string) error {
extension := filepath.Ext(imageName)
if len(extension) == 0 {
// TODO: could also validate for image types we support.
return errors.New("Image does not have extension")
}
extension = extension[1:]
encodedString := base64.StdEncoding.EncodeToString(image)
contentLength := 1
if query != nil {
contentLength += 1
}
messageContent := ArrayMessage{
Content: make([]MessageContentMessage, contentLength),
}
index := 0
if query != nil {
messageContent.Content[index] = TextMessageContent{
TextType: "text",
Text: *query,
}
index += 1
}
messageContent.Content[index] = ImageMessageContent{
ImageType: "image_url",
ImageUrl: fmt.Sprintf("data:image/%s;base64,%s", extension, encodedString),
}
arrayMessage := ChatUserMessage{Role: User, MessageContent: messageContent}
chat.Messages = append(chat.Messages, arrayMessage)
return nil
}
func (chat *Chat) AddAiResponse(res ChatAiMessage) {
chat.Messages = append(chat.Messages, res)
}
func (chat *Chat) AddToolResponse(res ChatUserToolResponse) {
chat.Messages = append(chat.Messages, res)
}
func (chat Chat) GetLatest() (ChatMessage, error) {
if len(chat.Messages) == 0 {
return nil, errors.New("Not enough messages")
}
return chat.Messages[len(chat.Messages)-1], nil
}

View File

@ -0,0 +1,24 @@
package client
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
)
func TestFlatMarshallSingleMessage(t *testing.T) {
require := require.New(t)
message := ChatUserMessage{
Role: User,
MessageContent: SingleMessage{
Content: "Hello",
},
}
json, err := json.Marshal(message)
require.NoError(err)
require.Equal(string(json), "{\"role\":\"user\",\"content\":\"Hello\"}")
}

View File

@ -0,0 +1,267 @@
package client
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
"os"
"github.com/charmbracelet/log"
"github.com/google/uuid"
)
type ResponseFormat struct {
Type string `json:"type"`
JsonSchema any `json:"json_schema"`
}
type AgentRequestBody struct {
Model string `json:"model"`
Temperature float64 `json:"temperature"`
ResponseFormat ResponseFormat `json:"response_format"`
Tools *any `json:"tools,omitempty"`
ToolChoice *string `json:"tool_choice,omitempty"`
EndToolCall string `json:"-"`
Chat *Chat `json:"messages"`
}
func (req AgentRequestBody) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
Model string `json:"model"`
Temperature float64 `json:"temperature"`
ResponseFormat ResponseFormat `json:"response_format"`
Tools *any `json:"tools,omitempty"`
ToolChoice *string `json:"tool_choice,omitempty"`
Messages []ChatMessage `json:"messages"`
}{
Model: req.Model,
Temperature: req.Temperature,
ResponseFormat: req.ResponseFormat,
Tools: req.Tools,
ToolChoice: req.ToolChoice,
Messages: req.Chat.Messages,
})
}
type ResponseChoice struct {
Index int `json:"index"`
Message ChatAiMessage `json:"message"`
FinishReason string `json:"finish_reason"`
}
type AgentResponse struct {
Id string `json:"id"`
Object string `json:"object"`
Choices []ResponseChoice `json:"choices"`
Created int `json:"created"`
}
type AgentClient struct {
url string
apiKey string
responseFormat string
ToolHandler ToolsHandlers
Log *log.Logger
Reply string
Do func(req *http.Request) (*http.Response, error)
Options CreateAgentClientOptions
}
const OPENAI_API_KEY = "OPENAI_API_KEY"
type CreateAgentClientOptions struct {
Log *log.Logger
SystemPrompt string
JsonTools string
EndToolCall string
Query *string
}
func CreateAgentClient(options CreateAgentClientOptions) AgentClient {
apiKey := os.Getenv(OPENAI_API_KEY)
if len(apiKey) == 0 {
panic("No api key")
}
return AgentClient{
apiKey: apiKey,
url: "https://api.mistral.ai/v1/chat/completions",
Do: func(req *http.Request) (*http.Response, error) {
client := &http.Client{}
return client.Do(req)
},
Log: options.Log,
ToolHandler: ToolsHandlers{
handlers: map[string]ToolHandler{},
},
Options: options,
}
}
func (client AgentClient) getRequest(body []byte) (*http.Request, error) {
req, err := http.NewRequest("POST", client.url, bytes.NewBuffer(body))
if err != nil {
return req, err
}
req.Header.Add("Authorization", "Bearer "+client.apiKey)
req.Header.Add("Content-Type", "application/json")
return req, nil
}
func (client AgentClient) Request(req *AgentRequestBody) (AgentResponse, error) {
jsonAiRequest, err := json.Marshal(req)
if err != nil {
return AgentResponse{}, err
}
httpRequest, err := client.getRequest(jsonAiRequest)
if err != nil {
return AgentResponse{}, err
}
resp, err := client.Do(httpRequest)
if err != nil {
return AgentResponse{}, err
}
response, err := io.ReadAll(resp.Body)
if err != nil {
return AgentResponse{}, err
}
agentResponse := AgentResponse{}
err = json.Unmarshal(response, &agentResponse)
if err != nil {
return AgentResponse{}, err
}
if len(agentResponse.Choices) != 1 {
client.Log.Errorf("Received more than 1 choice from AI \n %s\n", string(response))
return AgentResponse{}, errors.New("Unsupported. We currently only accept 1 choice from AI.")
}
msg := agentResponse.Choices[0].Message
req.Chat.AddAiResponse(msg)
return agentResponse, nil
}
func (client *AgentClient) ToolLoop(info ToolHandlerInfo, req *AgentRequestBody) error {
for {
response, err := client.Request(req)
if err != nil {
return err
}
if response.Choices[0].FinishReason == "stop" {
client.Log.Debug("Agent is finished")
return nil
}
err = client.Process(info, req)
if err != nil {
if err == FinishedCall {
client.Log.Debug("Agent is finished")
}
return err
}
}
}
var FinishedCall = errors.New("Last tool tool was called")
func (client *AgentClient) Process(info ToolHandlerInfo, req *AgentRequestBody) error {
var err error
message, err := req.Chat.GetLatest()
if err != nil {
return err
}
aiMessage, ok := message.(ChatAiMessage)
if !ok {
return errors.New("Latest message isnt an AI message")
}
if aiMessage.ToolCalls == nil {
// Not an error, we just dont have any tool calls to process.
return nil
}
for _, toolCall := range *aiMessage.ToolCalls {
if toolCall.Function.Name == req.EndToolCall {
return FinishedCall
}
toolResponse := client.ToolHandler.Handle(info, toolCall)
if toolCall.Function.Name == "reply" {
client.Reply = toolCall.Function.Arguments
}
client.Log.Debug("Tool call", "name", toolCall.Function.Name, "arguments", toolCall.Function.Arguments, "response", toolResponse.Content)
req.Chat.AddToolResponse(toolResponse)
}
return err
}
func (client *AgentClient) RunAgent(userId uuid.UUID, imageId uuid.UUID, imageName string, imageData []byte) error {
var tools any
err := json.Unmarshal([]byte(client.Options.JsonTools), &tools)
if err != nil {
panic(err)
}
toolChoice := "any"
request := AgentRequestBody{
Tools: &tools,
ToolChoice: &toolChoice,
Model: "pixtral-12b-2409",
Temperature: 0.3,
EndToolCall: client.Options.EndToolCall,
ResponseFormat: ResponseFormat{
Type: "text",
},
Chat: &Chat{
Messages: make([]ChatMessage, 0),
},
}
request.Chat.AddSystem(client.Options.SystemPrompt)
request.Chat.AddImage(imageName, imageData, client.Options.Query)
toolHandlerInfo := ToolHandlerInfo{
ImageId: imageId,
ImageName: imageName,
UserId: userId,
Image: &imageData,
}
return client.ToolLoop(toolHandlerInfo, &request)
}

View File

@ -0,0 +1,74 @@
package client
import (
"encoding/json"
"errors"
"github.com/google/uuid"
)
type ToolHandlerInfo struct {
UserId uuid.UUID
ImageId uuid.UUID
ImageName string
// Pointer because we don't want to copy this around too much.
Image *[]byte
}
type ToolHandler struct {
Fn func(info ToolHandlerInfo, args string, call ToolCall) (string, error)
}
type ToolsHandlers struct {
handlers map[string]ToolHandler
}
var NoToolCallError = errors.New("An assistant tool call with no tool calls was provided.")
const NonExistantTool = "This tool does not exist"
func (handler ToolsHandlers) Handle(info ToolHandlerInfo, toolCallMessage ToolCall) ChatUserToolResponse {
fnName := toolCallMessage.Function.Name
arguments := toolCallMessage.Function.Arguments
responseMessage := ChatUserToolResponse{
Role: "tool",
Name: fnName,
ToolCallId: toolCallMessage.Id,
}
fnHandler, exists := handler.handlers[fnName]
if !exists {
responseMessage.Content = NonExistantTool
return responseMessage
}
res, err := fnHandler.Fn(info, arguments, toolCallMessage)
if err != nil {
responseMessage.Content = err.Error()
} else {
responseMessage.Content = res
}
return responseMessage
}
func (handler *ToolsHandlers) AddTool(name string, fn func(info ToolHandlerInfo, args string, call ToolCall) (any, error)) {
handler.handlers[name] = ToolHandler{
Fn: func(info ToolHandlerInfo, args string, call ToolCall) (string, error) {
res, err := fn(info, args, call)
if err != nil {
return "", err
}
marshalledRes, err := json.Marshal(res)
if err != nil {
return "", err
}
return string(marshalledRes), nil
},
}
}

View File

@ -0,0 +1,191 @@
package client
import (
"errors"
"os"
"testing"
"github.com/charmbracelet/log"
"github.com/google/uuid"
"github.com/stretchr/testify/suite"
)
type ToolTestSuite struct {
suite.Suite
handler ToolsHandlers
client AgentClient
}
func (suite *ToolTestSuite) SetupTest() {
suite.handler = ToolsHandlers{
handlers: map[string]ToolHandler{},
}
suite.handler.AddTool("a", func(info ToolHandlerInfo, args string, call ToolCall) (any, error) {
return args, nil
})
suite.handler.AddTool("error", func(info ToolHandlerInfo, args string, call ToolCall) (any, error) {
return false, errors.New("I will always error")
})
suite.client.Log = log.New(os.Stdout)
suite.client.ToolHandler = suite.handler
}
func (suite *ToolTestSuite) TestSingleToolCall() {
require := suite.Require()
response := suite.handler.Handle(
ToolHandlerInfo{
UserId: uuid.Nil,
ImageId: uuid.Nil,
},
ToolCall{
Index: 0,
Id: "1",
Function: FunctionCall{
Name: "a",
Arguments: "return",
},
})
require.EqualValues(response, ChatUserToolResponse{
Role: "tool",
Content: "\"return\"",
ToolCallId: "1",
Name: "a",
})
}
func (suite *ToolTestSuite) TestMultipleToolCalls() {
assert := suite.Assert()
require := suite.Require()
chat := Chat{
Messages: []ChatMessage{ChatAiMessage{
Role: "assistant",
Content: "",
ToolCalls: &[]ToolCall{
{
Index: 0,
Id: "1",
Function: FunctionCall{
Name: "a",
Arguments: "first-call",
},
},
{
Index: 1,
Id: "2",
Function: FunctionCall{
Name: "a",
Arguments: "second-call",
},
},
},
}},
}
err := suite.client.Process(
ToolHandlerInfo{
UserId: uuid.Nil,
ImageId: uuid.Nil,
},
&AgentRequestBody{
Chat: &chat,
})
require.NoError(err, "Tool call shouldnt return an error")
assert.EqualValues(chat.Messages[1:], []ChatMessage{
ChatUserToolResponse{
Role: "tool",
Content: "\"first-call\"",
ToolCallId: "1",
Name: "a",
},
ChatUserToolResponse{
Role: "tool",
Content: "\"second-call\"",
ToolCallId: "2",
Name: "a",
},
})
}
func (suite *ToolTestSuite) TestMultipleToolCallsWithErrors() {
assert := suite.Assert()
require := suite.Require()
chat := Chat{
Messages: []ChatMessage{ChatAiMessage{
Role: "assistant",
Content: "",
ToolCalls: &[]ToolCall{
{
Index: 0,
Id: "1",
Function: FunctionCall{
Name: "error",
Arguments: "",
},
},
{
Index: 1,
Id: "2",
Function: FunctionCall{
Name: "non-existant",
Arguments: "",
},
},
{
Index: 2,
Id: "3",
Function: FunctionCall{
Name: "a",
Arguments: "no-error",
},
},
},
}},
}
err := suite.client.Process(
ToolHandlerInfo{
UserId: uuid.Nil,
ImageId: uuid.Nil,
},
&AgentRequestBody{
Chat: &chat,
})
require.NoError(err, "Tool call shouldnt return an error")
assert.EqualValues(chat.Messages[1:], []ChatMessage{
ChatUserToolResponse{
Role: "tool",
Content: "I will always error",
ToolCallId: "1",
Name: "error",
},
ChatUserToolResponse{
Role: "tool",
Content: "This tool does not exist",
ToolCallId: "2",
Name: "non-existant",
},
ChatUserToolResponse{
Role: "tool",
Content: "\"no-error\"",
ToolCallId: "3",
Name: "a",
},
})
}
func TestToolSuite(t *testing.T) {
suite.Run(t, &ToolTestSuite{
client: AgentClient{},
})
}

View File

@ -0,0 +1,177 @@
package agents
import (
"context"
"encoding/json"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/models"
"github.com/charmbracelet/log"
"github.com/google/uuid"
)
const contactPrompt = `
**Role:** AI Contact Processor from Images.
**Goal:** Extract contacts from an image, check against existing list using listContacts, add *only* new contacts using createContact, and call stopAgent when finished. Avoid duplicates.
**Input:** Image potentially containing contact info (Name, Phone, Email, Address).
**Workflow:**
1. **Scan Image:** Extract all contact details. If none, call stopAgent.
2. **Think:** Using the think tool, you must layout your thoughts about the contacts on the image. If they are duplicates or not, and what your next action should be,
3. **Check Duplicates:** If contacts found, *first* call listContacts. Compare extracted info to list. If all found contacts already exist, call stopAgent.
4. **Add New:** If you detect a new contact on the image, call createContact to create a new contact.
5. **Finish:** Call stopAgent once all new contacts are created OR if steps 1 or 2 determined no action/creation was needed.
**Tools:**
* listContacts: Check existing contacts (Use first if contacts found).
* createContact: Add a NEW contact (Name required).
* stopAgent: Signal task completion.
`
const contactTools = `
[
{
"type": "function",
"function": {
"name": "think",
"description": "Use this tool to think through the image, evaluating the contact and whether or not it exists in the users listContacts.",
"parameters": {
"type": "object",
"properties": {
"thought": {
"type": "string",
"description": "A singular thought about the image"
}
},
"required": ["thought"]
}
}
},
{
"type": "function",
"function": {
"name": "listContacts",
"description": "Retrieves the complete list of the user's currently saved contacts (e.g., names, phone numbers, emails if available in the stored data). This tool is essential and **must** be called *before* attempting to create a new contact if potential contact info is found in the image, to check if the person already exists and prevent duplicate entries.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "createContact",
"description": "Saves a new contact to the user's contact list. Only use this function **after** confirming the contact does not already exist by checking the output of listContacts. Provide all available extracted information for the new contact. Process one new contact per call.",
"parameters": {
"type": "object",
"properties": {
"contactId": {
"type": "string",
"description": "The UUID of the contact. You should only provide this IF you believe the contact already exists, from listContacts."
},
"name": {
"type": "string",
"description": "The full name of the person being added as a contact. This field is mandatory."
},
"phoneNumber": {
"type": "string",
"description": "The contact's primary phone number, including area or country code if available. Provide this if extracted from the image."
},
"address": {
"type": "string",
"description": "The complete physical mailing address of the contact (e.g., street number, street name, city, state/province, postal code, country). Provide this if extracted from the image."
},
"email": {
"type": "string",
"description": "The contact's primary email address. Provide this if extracted from the image."
}
},
"required": ["name"]
}
}
},
{
"type": "function",
"function": {
"name": "stopAgent",
"description": "Use this tool to signal that the contact processing for the current image is complete. Call this *only* when: 1) No contact info was found initially, OR 2) All found contacts were confirmed to already exist after calling listContacts, OR 3) All necessary createContact calls for new individuals have been completed.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
}
]
`
type listContactsArguments struct{}
type createContactsArguments struct {
Name string `json:"name"`
ContactID *string `json:"contactId"`
PhoneNumber *string `json:"phoneNumber"`
Address *string `json:"address"`
Email *string `json:"email"`
}
func NewContactAgent(log *log.Logger, contactModel models.ContactModel) client.AgentClient {
agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: contactPrompt,
JsonTools: contactTools,
Log: log,
EndToolCall: "stopAgent",
})
agentClient.ToolHandler.AddTool("think", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return "Thought", nil
})
agentClient.ToolHandler.AddTool("listContacts", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return contactModel.List(context.Background(), info.UserId)
})
agentClient.ToolHandler.AddTool("createContact", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
args := createContactsArguments{}
err := json.Unmarshal([]byte(_args), &args)
if err != nil {
return model.Contacts{}, err
}
ctx := context.Background()
contactId := uuid.Nil
if args.ContactID != nil {
contactUuid, err := uuid.Parse(*args.ContactID)
if err != nil {
return model.Contacts{}, err
}
contactId = contactUuid
}
contact, err := contactModel.Save(ctx, info.UserId, model.Contacts{
ID: contactId,
Name: args.Name,
PhoneNumber: args.PhoneNumber,
Email: args.Email,
})
if err != nil {
return model.Contacts{}, err
}
_, err = contactModel.SaveToImage(ctx, info.ImageId, contact.ID)
if err != nil {
return model.Contacts{}, err
}
return contact, nil
})
return agentClient
}

View File

@ -0,0 +1,250 @@
package agents
import (
"context"
"encoding/json"
"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 AI processing events from images using internal thought.**
**Task:** Extract event details (Name, Date/Time, Location). Use think before deciding actions. Check duplicates with listEvents. Handle new events via getEventLocationId (if location exists) and createEvent. Use finish if no event or duplicate found.
1. **Analyze Image & Think:** Extract details. Use think to confirm if a valid event exists. If not -> stopAgent.
2. **Event Confirmed?** -> *Must* call listEvents, to check for existing events and prevent duplicates.
3. **Detect Duplicates** -> If the input contains an event that already exists from listEvents, then you should call stopAgent.
4. **New Events**
* If you think the input contains a location, then you can use getEventLocationId to retrieve the ID of the location. Only use this IF the input contains a location.
* Call createEvent.
5. **Multiple Events:** Process sequentially using this logic.
**Tools:**
* think: Internal reasoning/planning step.
* listEvents: Check for duplicates (mandatory first step for found events).
* getEventLocationId: Get ID for location text.
* createEvent: Add new event (Name req.). Terminal action for new events.
* stopAgent: Signal completion (no event/duplicate found). Terminal action.
`
const eventTools = `
[
{
"type": "function",
"function": {
"name": "think",
"description": "Use this tool to think through the image, evaluating the event and whether or not it exists in the users listEvents.",
"parameters": {
"type": "object",
"properties": {
"thought": {
"type": "string",
"description": "A singular thought about the image"
}
},
"required": ["thought"]
}
}
},
{
"type": "function",
"function": {
"name": "listEvents",
"description": "Retrieves the list of the user's currently scheduled events. Essential for checking if an event identified in the image already exists to prevent duplicates. Must be called before potentially creating an event.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "createEvent",
"description": "Creates a new event in the user's calendar or list. Use only after listEvents confirms the event is new. Provide all extracted details.",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name or title of the event. This field is mandatory."
},
"startDateTime": {
"type": "string",
"description": "The event's start date and time in ISO 8601 format (e.g., '2025-04-18T10:00:00Z'). Include if available."
},
"endDateTime": {
"type": "string",
"description": "The event's end date and time in ISO 8601 format. Optional, include if available and different from startDateTime."
},
"locationId": {
"type": "string",
"description": "The unique identifier (UUID or similar) for the event's location. Use this if available, do not invent it."
}
},
"required": ["name"]
}
}
},
{
"type": "function",
"function": {
"name": "updateEvent",
"description": "Updates an existing event record identified by its eventId. Use this tool when listEvents indicates a match for the event details found in the current input.",
"parameters": {
"type": "object",
"properties": {
"eventId": {
"type": "string",
"description": "The UUID of the existing event"
}
},
"required": ["eventId"]
}
}
},
{
"type": "function",
"function": {
"name": "getEventLocationId",
"description": "Retrieves a unique identifier for a location description associated with an event. Use this before createEvent if a new event specifies a location.",
"parameters": {
"type": "object",
"properties": {
"locationDescription": {
"type": "string",
"description": "The text describing the location extracted from the image (e.g., 'Conference Room B', '123 Main St, Anytown', 'Zoom Link details')."
}
},
"required": ["locationDescription"]
}
}
},
{
"type": "function",
"function": {
"name": "stopAgent",
"description": "Call this tool only when event processing for the current image is fully complete. This occurs if: 1) No event info was found, OR 2) The found event already exists, OR 3) A new event has been successfully created.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
}
]`
type listEventArguments struct{}
type createEventArguments struct {
Name string `json:"name"`
StartDateTime *string `json:"startDateTime"`
EndDateTime *string `json:"endDateTime"`
OrganizerName *string `json:"organizerName"`
LocationID *string `json:"locationId"`
}
type updateEventArguments struct {
EventID string `json:"eventId"`
}
func NewEventAgent(log *log.Logger, eventsModel models.EventModel, locationModel models.LocationModel) client.AgentClient {
agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: eventPrompt,
JsonTools: eventTools,
Log: log,
EndToolCall: "stopAgent",
})
locationAgent := NewLocationAgentWithComm(log.WithPrefix("Events 📅 > Locations 📍"), locationModel)
locationQuery := "Can you get me the ID of the location present in this image?"
locationAgent.Options.Query = &locationQuery
agentClient.ToolHandler.AddTool("think", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return "Thought", nil
})
agentClient.ToolHandler.AddTool("listEvents", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return 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"
// TODO: check for nil pointers.
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
}
locationId, err := uuid.Parse(*args.LocationID)
if err != nil {
return model.Events{}, err
}
events, err := eventsModel.Save(ctx, info.UserId, model.Events{
Name: args.Name,
StartDateTime: &startTime,
EndDateTime: &endTime,
LocationID: &locationId,
})
if err != nil {
return model.Events{}, err
}
_, err = eventsModel.SaveToImage(ctx, info.ImageId, events.ID)
if err != nil {
return model.Events{}, err
}
return events, nil
})
agentClient.ToolHandler.AddTool("updateEvent", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
args := updateEventArguments{}
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
}
eventsModel.SaveToImage(ctx, info.ImageId, contactUuid)
return "Saved", nil
})
agentClient.ToolHandler.AddTool("getEventLocationId", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
// TODO: reenable this when I'm creating the agent locally instead of getting it from above.
locationAgent.RunAgent(info.UserId, info.ImageId, info.ImageName, *info.Image)
log.Debugf("Reply from location %s\n", locationAgent.Reply)
return locationAgent.Reply, nil
})
return agentClient
}

View File

@ -0,0 +1,248 @@
package agents
import (
"context"
"encoding/json"
"fmt"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/models"
"github.com/charmbracelet/log"
"github.com/google/uuid"
)
const locationPrompt = `
Role: Location AI Assistant
Objective: Identify locations from images/text, manage a saved list, and answer user queries about saved locations using the provided tools.
The user does not want to have duplicate entries on their saved location list. So you should only create a new location if listLocation doesnt return
what would be a duplicate.
Core Logic:
**Extract Location Details:** Attempt to extract location details (like InputName, InputAddress) from the user's input.
* If no details can be extracted, inform the user and use stopAgent.
**Check for Existing Location:** If details *were* extracted:
* Use listLocations with the extracted InputName and/or InputAddress to search for potentially matching locations already saved in the list.
Action loop:
**Thinking**
* Use the think tool to analytise the image.
* You should think about whether listLocations already contains this location, or if it is a new location.
* You should always call this after listLocations.
* You must think about whether or not listLocations already has this location.
**Decide Action based on Search Results:**
* If no existing location looks like the location on the input. You should use createLocation.
* Do not use this tool if this location already exists.
* If the input contains a location that already exists, you should use createExistingLocation.
* If there is a similar location in listLocation, you should use this tool. It doesnt have to be an exact match.
* Lastly, if the user asked a specific question about a location. You must do all the actions but also always use the reply tool to answer the user.
* This is the only way you can communicate with the user if they asked a query.
You should repeat the action loop until all locations on the image are done.
Once you are done, use stopAgent.
`
const replyTool = `
{
"type": "function",
"function": {
"name": "reply",
"description": "Signals intent to provide information about a specific known location in response to a user's query. Use only if the user asked a question and the location's ID was found via listLocations.",
"parameters": {
"type": "object",
"properties": {
"locationId": {
"type": "string",
"description": "The unique identifier of the saved location that the user is asking about."
}
},
"required": ["locationId"]
}
}
},`
const locationTools = `
[
{
"type": "function",
"function": {
"name": "think",
"description": "Use this tool to think through the image, evaluating the location and whether or not it exists in the users listLocations. You should also ask yourself if the user has asked a query, and if you've used the correct tool to reply to them.",
"parameters": {
"type": "object",
"properties": {
"thought": {
"type": "string",
"description": "A singular thought about the image"
}
},
"required": ["thought"]
}
}
},
{
"type": "function",
"function": {
"name": "listLocations",
"description": "Retrieves the list of the user's currently saved locations (names, addresses, IDs). Use this first to check if a location from an image already exists, or to find the ID of a location the user is asking about.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "createLocation",
"description": "Creates a new location with as much information as you can extract. Be precise. You should only add the parameters you can actually see on the image.",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The primary name of the location"
},
"address": {
"type": "string",
"description": "The address of the location"
}
},
"required": ["name"]
}
}
},
{
"type": "function",
"function": {
"name": "createExistingLocation",
"description": "Called when a location already exists in the users list, from listLocations. Only call this to indicate this image contains a duplicate. And only after using the doesLocationExist tol",
"parameters": {
"type": "object",
"properties": {
"locationId": {
"type": "string",
"description": "The UUID of the location, from listLocations"
}
},
"required": ["locationId"]
}
}
},
%s
{
"type": "function",
"function": {
"name": "stopAgent",
"description": "Use this tool to signal that the contact processing for the current image is complete.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
}
]`
func getLocationAgentTools(allowReply bool) string {
if allowReply {
return fmt.Sprintf(locationTools, replyTool)
} else {
return fmt.Sprintf(locationTools, "")
}
}
type listLocationArguments struct{}
type createLocationArguments struct {
Name string `json:"name"`
Address *string `json:"address"`
}
type createExistingLocationArguments struct {
LocationID string `json:"locationId"`
}
func NewLocationAgentWithComm(log *log.Logger, locationModel models.LocationModel) client.AgentClient {
client := NewLocationAgent(log, locationModel)
client.Options.JsonTools = getLocationAgentTools(true)
return client
}
func NewLocationAgent(log *log.Logger, locationModel models.LocationModel) client.AgentClient {
agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: locationPrompt,
JsonTools: getLocationAgentTools(false),
Log: log,
EndToolCall: "stopAgent",
})
agentClient.ToolHandler.AddTool("listLocations", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return 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()
// TODO: this tool could be simplier, as the model could have a SaveToImage joined with the save.
location, err := locationModel.Save(ctx, info.UserId, model.Locations{
Name: args.Name,
Address: args.Address,
})
if err != nil {
return model.Locations{}, err
}
_, err = locationModel.SaveToImage(ctx, info.ImageId, location.ID)
if err != nil {
return model.Locations{}, err
}
return location, nil
})
agentClient.ToolHandler.AddTool("createExistingLocation", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
args := createExistingLocationArguments{}
err := json.Unmarshal([]byte(_args), &args)
if err != nil {
return "", err
}
ctx := context.Background()
locationId, err := uuid.Parse(args.LocationID)
if err != nil {
return "", err
}
_, err = locationModel.SaveToImage(ctx, info.ImageId, locationId)
if err != nil {
return "", err
}
return "", nil
})
agentClient.ToolHandler.AddTool("reply", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return "ok", nil
})
agentClient.ToolHandler.AddTool("think", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return "ok", nil
})
return agentClient
}

View File

@ -0,0 +1,83 @@
package agents
import (
"context"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/models"
"github.com/charmbracelet/log"
"github.com/google/uuid"
)
const noteAgentPrompt = `
You are a helpful agent, who's job is to extract notes from images.
Not all images contain notes, in such cases there's not need to create them.
An image can have more than one note.
You must return markdown, and adapt the text to best fit markdown.
Do not return anything except markdown.
If the image contains code, add this inside code blocks. You must try and correctly guess the language too.
`
type NoteAgent struct {
client client.AgentClient
noteModel models.NoteModel
}
func (agent NoteAgent) GetNotes(userId uuid.UUID, imageId uuid.UUID, imageName string, imageData []byte) error {
request := client.AgentRequestBody{
Model: "pixtral-12b-2409",
Temperature: 0.3,
ResponseFormat: client.ResponseFormat{
Type: "text",
},
Chat: &client.Chat{
Messages: make([]client.ChatMessage, 0),
},
}
request.Chat.AddSystem(noteAgentPrompt)
request.Chat.AddImage(imageName, imageData, nil)
resp, err := agent.client.Request(&request)
if err != nil {
return err
}
ctx := context.Background()
markdown := resp.Choices[0].Message.Content
note, err := agent.noteModel.Save(ctx, userId, model.Notes{
Name: "the note", // TODO: add some json schema
Content: markdown,
})
if err != nil {
return err
}
_, err = agent.noteModel.SaveToImage(ctx, imageId, note.ID)
if err != nil {
return err
}
return nil
}
func NewNoteAgent(log *log.Logger, noteModel models.NoteModel) NoteAgent {
client := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: noteAgentPrompt,
Log: log,
})
agent := NoteAgent{
client: client,
noteModel: noteModel,
}
return agent
}

View File

@ -0,0 +1,170 @@
package agents
import (
"screenmark/screenmark/agents/client"
"github.com/charmbracelet/log"
)
const orchestratorPrompt = `
**Role:** You are an Orchestrator AI responsible for analyzing images provided by the user.
**Primary Task:** Examine the input image and determine which specialized AI agent(s), available as tool calls, should be invoked to process the relevant information within the image, or if no specialized processing is needed. Your goal is to either extract and structure useful information for the user by selecting the most appropriate tool(s) or explicitly indicate that no specific action is required.
**Analysis Process & Decision Logic:**
1. **Analyze Image Content:** Scrutinize the image for distinct types of information:
* General text/writing (including code, formulas)
* Information about a person or contact details
* Information about a place, location, or address
* Information about an event
* Content that doesn't fit any specific category or lacks actionable information.
2. **Thinking**
* You should use the think tool to allow you to think your way through the image.
* You should call this as many times as you need to in order to describe and analyse the image correctly.
3. **Agent Selection - Determine ALL that apply:**
* **contactAgent:** Is there information specifically related to a person or their contact details (e.g., business card, name/email/phone)?
* **locationAgent:** Is there information specifically identifying a place, location, city, or address (e.g., map, street sign, address text)?
* **eventAgent:** Is there information specifically related to an event (e.g., invitation, poster with date/time, schedule)?
* **noteAgent** Does the image contain *any* text/writing (including code, formulas)?
* **noAgent**: Call this when you are done working on this image.
* Call all applicable specialized agents (noteAgent, contactAgent, locationAgent, eventAgent) simultaneously (in parallel).
`
const orchestratorTools = `
[
{
"type": "function",
"function": {
"name": "think",
"description": "Use to layout all your thoughts about the image, roughly describing it, and specially describing if the image contains anything relevant to your available agents",
"parameters": {
"type": "object",
"properties": {
"thought": {
"type": "string",
"description": "A singular thought about the image"
}
},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "noteAgent",
"description": "Extracts general textual content like handwritten notes, paragraphs in documents, presentation slides, code snippets, or mathematical formulas. Use this for significant text that isn't primarily contact details, an address, or specific event information.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "contactAgent",
"description": "Extracts personal contact information. Use when the image clearly shows details like names, phone numbers, email addresses, job titles, or company names, especially from sources like business cards, email signatures, or contact lists.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "locationAgent",
"description": "Use when the input has anything to do with a place. This could be a city, an address, a postcode, a virtual meeting location, or a geographical location.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "eventAgent",
"description": "Extracts details related to scheduled events, appointments, or specific occasions. Use when the image contains information like event titles, dates, times, venues, agendas, or descriptions, typically found on invitations, posters, calendar entries, or schedules.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "noAgent",
"description": "Extracts details related to scheduled events, appointments, or specific occasions. Use when the image contains information like event titles, dates, times, venues, agendas, or descriptions, typically found on invitations, posters, calendar entries, or schedules.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
}
]
`
type OrchestratorAgent struct {
Client client.AgentClient
log log.Logger
}
type Status struct {
Ok bool `json:"ok"`
}
func NewOrchestratorAgent(log *log.Logger, noteAgent NoteAgent, contactAgent client.AgentClient, locationAgent client.AgentClient, eventAgent client.AgentClient, imageName string, imageData []byte) client.AgentClient {
agent := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: orchestratorPrompt,
JsonTools: orchestratorTools,
Log: log,
EndToolCall: "noAgent",
})
agent.ToolHandler.AddTool("think", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return "Thought", 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)
return "noteAgent called successfully", nil
})
agent.ToolHandler.AddTool("contactAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
go contactAgent.RunAgent(info.UserId, info.ImageId, imageName, imageData)
return "contactAgent called successfully", nil
})
agent.ToolHandler.AddTool("locationAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
go locationAgent.RunAgent(info.UserId, info.ImageId, imageName, imageData)
return "locationAgent called successfully", nil
})
agent.ToolHandler.AddTool("eventAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
go eventAgent.RunAgent(info.UserId, info.ImageId, imageName, imageData)
return "eventAgent called successfully", nil
})
agent.ToolHandler.AddTool("noAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return "ok", nil
})
return agent
}

68
backend/auth.go Normal file
View File

@ -0,0 +1,68 @@
package main
import (
"errors"
"math/rand"
"time"
)
type Code struct {
Code string
Valid time.Time
}
type Auth struct {
codes map[string]Code
mailer Mailer
}
var letterRunes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
func randString(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return string(b)
}
func (a *Auth) CreateCode(email string) error {
code := randString(10)
if err := a.mailer.SendCode(email, code); err != nil {
return err
}
a.codes[email] = Code{
Code: code,
Valid: time.Now().Add(time.Minute),
}
return nil
}
func (a *Auth) IsCodeValid(email string, code string) bool {
existingCode, exists := a.codes[email]
if !exists {
return false
}
return existingCode.Valid.After(time.Now()) && existingCode.Code == code
}
func (a *Auth) UseCode(email string, code string) error {
if valid := a.IsCodeValid(email, code); !valid {
return errors.New("This code is invalid.")
}
delete(a.codes, email)
return nil
}
func CreateAuth(mailer Mailer) Auth {
return Auth{
codes: make(map[string]Code),
mailer: mailer,
}
}

30
backend/auth_test.go Normal file
View File

@ -0,0 +1,30 @@
package main
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
type TestMail struct{}
func (m TestMail) SendCode(to string, code string) error {
return nil
}
var testMailer = TestMail{}
func TestCreateCode(t *testing.T) {
require := require.New(t)
auth := CreateAuth(testMailer)
err := auth.CreateCode("test")
require.NoError(err)
code, exists := auth.codes["test"]
require.True(exists)
require.True(code.Valid.After(time.Now()))
require.True(auth.IsCodeValid("test", code.Code))
}

View File

@ -0,0 +1,31 @@
services:
database:
image: postgres
restart: always
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: haystack
volumes:
- ./schema.sql:/docker-entrypoint-initdb.d/schema.sql
- ./pgdata:/var/lib/postgresql/data
ports:
- 4321:5432
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d haystack"]
interval: 10s
retries: 5
start_period: 5s
timeout: 5s
backend:
build: .
restart: always
env_file: .env.docker
ports:
- 3040:3040
volumes:
- ./.env.docker:/app/.env
depends_on:
database:
condition: service_healthy
restart: true

72
backend/email.go Normal file
View File

@ -0,0 +1,72 @@
package main
import (
"fmt"
"os"
"github.com/wneessen/go-mail"
)
type MailClient struct {
client *mail.Client
}
type TestMailClient struct{}
type Mailer interface {
SendCode(to string, code string) error
}
func (m MailClient) getMessage() (*mail.Msg, error) {
message := mail.NewMsg()
if err := message.From("auth@johncosta.tech"); err != nil {
return nil, err
}
return message, nil
}
func (m MailClient) SendCode(to string, code string) error {
msg, err := m.getMessage()
if err != nil {
return err
}
if err := msg.To(to); err != nil {
return err
}
msg.Subject("Login to Haystack")
msg.SetBodyString(mail.TypeTextPlain, code)
return m.client.DialAndSend(msg)
}
func (m TestMailClient) SendCode(to string, code string) error {
fmt.Printf("Email: %s | Code %s\n", to, code)
return nil
}
func CreateMailClient() (Mailer, error) {
mode := os.Getenv("MODE")
if mode == "DEV" {
return TestMailClient{}, nil
}
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")),
)
if err != nil {
return MailClient{}, err
}
return MailClient{
client: client,
}, nil
}

121
backend/events.go Normal file
View File

@ -0,0 +1,121 @@
package main
import (
"context"
"database/sql"
"os"
"screenmark/screenmark/agents"
"screenmark/screenmark/models"
"time"
"github.com/charmbracelet/log"
"github.com/google/uuid"
"github.com/lib/pq"
)
func ListenNewImageEvents(db *sql.DB, eventManager *EventManager) {
listener := pq.NewListener(os.Getenv("DB_CONNECTION"), time.Second, time.Second, func(event pq.ListenerEventType, err error) {
if err != nil {
panic(err)
}
})
defer listener.Close()
locationModel := models.NewLocationModel(db)
eventModel := models.NewEventModel(db)
noteModel := models.NewNoteModel(db)
imageModel := models.NewImageModel(db)
contactModel := models.NewContactModel(db)
databaseEventLog := createLogger("Database Events 🤖", os.Stdout)
databaseEventLog.SetLevel(log.DebugLevel)
err := listener.Listen("new_image")
if err != nil {
panic(err)
}
for {
select {
case parameters := <-listener.Notify:
imageId := uuid.MustParse(parameters.Extra)
eventManager.listeners[parameters.Extra] = make(chan string)
databaseEventLog.Debug("Starting processing image", "ImageID", imageId)
ctx := context.Background()
go func() {
image, err := imageModel.GetToProcessWithData(ctx, imageId)
if err != nil {
databaseEventLog.Error("Failed to GetToProcessWithData", "error", err)
return
}
splitWriter := createDbStdoutWriter(db, image.ImageID)
noteAgent := agents.NewNoteAgent(createLogger("Notes 📝", splitWriter), noteModel)
contactAgent := agents.NewContactAgent(createLogger("Contacts 👥", splitWriter), contactModel)
locationAgent := agents.NewLocationAgent(createLogger("Locations 📍", splitWriter), locationModel)
eventAgent := agents.NewEventAgent(createLogger("Events 📅", splitWriter), eventModel, locationModel)
if err := imageModel.StartProcessing(ctx, image.ID); err != nil {
databaseEventLog.Error("Failed to FinishProcessing", "error", err)
return
}
orchestrator := agents.NewOrchestratorAgent(createLogger("Orchestrator 🎼", splitWriter), noteAgent, contactAgent, locationAgent, eventAgent, image.Image.ImageName, image.Image.Image)
orchestrator.RunAgent(image.UserID, image.ImageID, image.Image.ImageName, image.Image.Image)
_, err = imageModel.FinishProcessing(ctx, image.ID)
if err != nil {
databaseEventLog.Error("Failed to finish processing", "ImageID", imageId)
return
}
databaseEventLog.Debug("Starting processing image", "ImageID", imageId)
}()
}
}
}
type EventManager struct {
// Maps processing image UUID to a channel
listeners map[string]chan string
}
func NewEventManager() EventManager {
return EventManager{
listeners: make(map[string]chan string),
}
}
func ListenProcessingImageStatus(db *sql.DB, eventManager *EventManager) {
listener := pq.NewListener(os.Getenv("DB_CONNECTION"), time.Second, time.Second, func(event pq.ListenerEventType, err error) {
if err != nil {
panic(err)
}
})
defer listener.Close()
if err := listener.Listen("new_processing_image_status"); err != nil {
panic(err)
}
for {
select {
case data := <-listener.Notify:
stringUuid := data.Extra[0:36]
status := data.Extra[36:]
imageListener, exists := eventManager.listeners[stringUuid]
if !exists {
continue
}
imageListener <- status
close(imageListener)
delete(eventManager.listeners, stringUuid)
}
}
}

31
backend/go.mod Normal file
View File

@ -0,0 +1,31 @@
module screenmark/screenmark
go 1.24.0
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/lipgloss v1.0.0 // indirect
github.com/charmbracelet/log v0.4.1 // indirect
github.com/charmbracelet/x/ansi v0.4.2 // indirect
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/go-logfmt/logfmt v0.6.0 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/robert-nix/ansihtml v1.0.1 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/wneessen/go-mail v0.6.2 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

119
backend/go.sum Normal file
View File

@ -0,0 +1,119 @@
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
github.com/charmbracelet/log v0.4.1 h1:6AYnoHKADkghm/vt4neaNEXkxcXLSV2g1rdyFDOpTyk=
github.com/charmbracelet/log v0.4.1/go.mod h1:pXgyTsqsVu4N9hGdHmQ0xEA4RsXof402LX9ZgiITn2I=
github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk=
github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robert-nix/ansihtml v1.0.1 h1:VTiyQ6/+AxSJoSSLsMecnkh8i0ZqOEdiRl/odOc64fc=
github.com/robert-nix/ansihtml v1.0.1/go.mod h1:CJwclxYaTPc2RfcxtanEACsYuTksh4yDXcNeHHKZINE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/wneessen/go-mail v0.6.2 h1:c6V7c8D2mz868z9WJ+8zDKtUyLfZ1++uAZmo2GRFji8=
github.com/wneessen/go-mail v0.6.2/go.mod h1:L/PYjPK3/2ZlNb2/FjEBIn9n1rUWjW+Toy531oVmeb4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

96
backend/jwt.go Normal file
View File

@ -0,0 +1,96 @@
package main
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
type JwtType string
const (
Access JwtType = "access"
Refresh JwtType = "refresh"
)
type JwtClaims struct {
UserID string
Type JwtType
Expire time.Time
}
// obviously this is very not secure. TODO: extract to env
var JWT_SECRET = []byte("very secret")
func createToken(claims JwtClaims) *jwt.Token {
return jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"UserID": claims.UserID,
"Type": claims.Type,
"Expire": claims.Expire,
})
}
func CreateRefreshToken(userId uuid.UUID) string {
token := createToken(JwtClaims{
UserID: userId.String(),
Type: Refresh,
Expire: time.Now().Add(time.Hour * 24 * 7),
})
// TODO: bruh what is this
tokenString, err := token.SignedString(JWT_SECRET)
if err != nil {
panic(err)
}
return tokenString
}
func CreateAccessToken(userId uuid.UUID) string {
token := createToken(JwtClaims{
UserID: userId.String(),
Type: Access,
Expire: time.Now().Add(time.Hour),
})
// TODO: bruh what is this
tokenString, err := token.SignedString(JWT_SECRET)
if err != nil {
panic(err)
}
return tokenString
}
var NotValidToken = errors.New("Not a valid token")
func GetUserIdFromAccess(accessToken string) (uuid.UUID, error) {
token, err := jwt.Parse(accessToken, func(token *jwt.Token) (any, error) {
return JWT_SECRET, nil
}, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))
if err != nil {
return uuid.Nil, err
}
// Blah blah, check expiry and stuff
// this function is stupid
if claims, ok := token.Claims.(jwt.MapClaims); ok {
tokenType, ok := claims["Type"]
if !ok || tokenType.(string) != "access" {
return uuid.Nil, NotValidToken
}
userId, err := uuid.Parse(claims["UserID"].(string))
if err != nil {
return uuid.Nil, NotValidToken
}
return userId, nil
} else {
return uuid.Nil, NotValidToken
}
}

149
backend/logs.go Normal file
View File

@ -0,0 +1,149 @@
package main
import (
"context"
"database/sql"
"fmt"
"io"
"net/http"
"os"
"time"
"screenmark/screenmark/.gen/haystack/haystack/model"
. "screenmark/screenmark/.gen/haystack/haystack/table"
"github.com/go-chi/chi/v5"
. "github.com/go-jet/jet/v2/postgres"
"github.com/robert-nix/ansihtml"
"github.com/charmbracelet/log"
"github.com/google/uuid"
"github.com/muesli/termenv"
)
type DatabaseWriter struct {
dbPool *sql.DB
imageId uuid.UUID
}
func (w *DatabaseWriter) Write(p []byte) (n int, err error) {
if len(p) == 0 {
return 0, nil
}
insertLogStmt := Logs.
INSERT(Logs.Log, Logs.ImageID).
VALUES(string(p), w.imageId)
_, err = insertLogStmt.Exec(w.dbPool)
if err != nil {
return 0, err
} else {
return len(p), nil
}
}
func (w *DatabaseWriter) GetImageLogs(ctx context.Context, imageId uuid.UUID) ([]string, error) {
getImageLogsStmt := Logs.
SELECT(Logs.Log).
WHERE(Logs.ImageID.EQ(UUID(imageId)))
logs := []model.Logs{}
err := getImageLogsStmt.QueryContext(ctx, w.dbPool, &logs)
if err != nil {
return []string{}, err
}
stringLogs := make([]string, len(logs))
for i, log := range logs {
stringLogs[i] = log.Log
}
return stringLogs, nil
}
func createLogHandler(logWriter *DatabaseWriter) func(r chi.Router) {
return func(r chi.Router) {
r.Get("/{imageId}", func(w http.ResponseWriter, r *http.Request) {
stringImageId := r.PathValue("imageId")
imageId, err := uuid.Parse(stringImageId)
if err != nil {
w.WriteHeader(http.StatusBadGateway)
return
}
logs, err := logWriter.GetImageLogs(r.Context(), imageId)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
html := ""
imageTag := fmt.Sprintf(`<image src="http://localhost:3040/image/%s">`, stringImageId)
for _, log := range logs {
html += fmt.Sprintf("<div>%s</div>", string(ansihtml.ConvertToHTML([]byte(log)))+"\n")
}
css := `
<style>
body {
background-color: #1e1e1e;
color: #f0f0f0;
font-family: sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
}
/* Basic styling for code blocks often used for logs */
pre {
background-color: #2a2a2a;
padding: 1em;
border-radius: 4px;
overflow-x: auto;
border: 1px solid #444;
}
code {
font-family: monospace;
}
</style>
`
fullHtml := fmt.Sprintf("<html><head><title>Logs</title>%s</head><body>%s%s</body></html>", css, imageTag, html)
w.Header().Add("Content-Type", "text/html")
w.Write([]byte(fullHtml))
w.WriteHeader(http.StatusOK)
})
}
}
func newDatabaseWriter(dbPool *sql.DB, imageId uuid.UUID) *DatabaseWriter {
return &DatabaseWriter{
dbPool: dbPool,
imageId: imageId,
}
}
func createDbStdoutWriter(dbPool *sql.DB, imageId uuid.UUID) io.Writer {
return io.MultiWriter(os.Stdout, newDatabaseWriter(dbPool, imageId))
}
func createLogger(prefix string, writer io.Writer) *log.Logger {
logger := log.NewWithOptions(writer, log.Options{
ReportTimestamp: true,
TimeFormat: time.Kitchen,
Prefix: prefix,
Formatter: log.TextFormatter,
})
logger.SetColorProfile(termenv.TrueColor)
logger.SetLevel(log.DebugLevel)
return logger
}

407
backend/main.go Normal file
View File

@ -0,0 +1,407 @@
package main
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"path/filepath"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/models"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/google/uuid"
"github.com/joho/godotenv"
)
type TestAiClient struct {
ImageInfo client.ImageMessageContent
}
func (client TestAiClient) GetImageInfo(imageName string, imageData []byte) (client.ImageMessageContent, error) {
return client.ImageInfo, nil
}
func main() {
err := godotenv.Load()
if err != nil {
panic(err)
}
db, err := models.InitDatabase()
if err != nil {
panic(err)
}
imageModel := models.NewImageModel(db)
userModel := models.NewUserModel(db)
mail, err := CreateMailClient()
if err != nil {
panic(err)
}
auth := CreateAuth(mail)
eventManager := NewEventManager()
go ListenNewImageEvents(db, &eventManager)
go ListenProcessingImageStatus(db, &eventManager)
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(CorsMiddleware)
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.WriteHeader(http.StatusOK)
})
r.Group(func(r chi.Router) {
r.Use(ProtectedRoute)
r.Get("/image", func(w http.ResponseWriter, r *http.Request) {
userId := r.Context().Value(USER_ID).(uuid.UUID)
images, err := userModel.ListWithProperties(r.Context(), userId)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "Something went wrong")
return
}
type DataType struct {
Type string `json:"type"`
Data any `json:"data"`
}
dataTypes := make([]DataType, 0)
// lord
// forgive me
idMap := make(map[uuid.UUID]bool)
for _, image := range images {
for _, location := range image.Locations {
_, exists := idMap[location.ID]
if exists {
continue
}
dataTypes = append(dataTypes, DataType{
Type: "location",
Data: location,
})
idMap[location.ID] = true
}
for _, event := range image.Events {
_, exists := idMap[event.ID]
if exists {
continue
}
dataTypes = append(dataTypes, DataType{
Type: "event",
Data: event,
})
idMap[event.ID] = true
}
for _, note := range image.Notes {
dataTypes = append(dataTypes, DataType{
Type: "note",
Data: note,
})
idMap[note.ID] = true
}
for _, contact := range image.Contacts {
_, exists := idMap[contact.ID]
if exists {
continue
}
dataTypes = append(dataTypes, DataType{
Type: "contact",
Data: contact,
})
idMap[contact.ID] = true
}
}
jsonImages, err := json.Marshal(dataTypes)
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)
})
r.Get("/image/{id}", func(w http.ResponseWriter, r *http.Request) {
stringImageId := r.PathValue("id")
userId := r.Context().Value(USER_ID).(uuid.UUID)
imageId, err := uuid.Parse(stringImageId)
if err != nil {
w.WriteHeader(http.StatusForbidden)
fmt.Fprintf(w, "You cannot read this")
return
}
if authorized := imageModel.IsUserAuthorized(r.Context(), imageId, userId); !authorized {
w.WriteHeader(http.StatusForbidden)
fmt.Fprintf(w, "You cannot read this")
return
}
// TODO: really need authorization here!
image, err := imageModel.Get(r.Context(), 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.Image.ImageName)
extension = extension[1:]
w.Header().Add("Content-Type", "image/"+extension)
w.Write(image.Image.Image)
})
r.Post("/image/{name}", func(w http.ResponseWriter, r *http.Request) {
imageName := r.PathValue("name")
userId := r.Context().Value(USER_ID).(uuid.UUID)
if len(imageName) == 0 {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "You need to provide a name in the path")
return
}
contentType := r.Header.Get("Content-Type")
// TODO: length checks on body
// TODO: extract this shit out
image := make([]byte, 0)
if contentType == "application/base64" {
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
}
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 := imageModel.Process(r.Context(), userId, model.Image{
Image: image,
ImageName: imageName,
})
if err != nil {
log.Println("Second case")
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("Third case")
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "Could not create JSON response for this image")
return
}
w.WriteHeader(http.StatusCreated)
fmt.Fprint(w, string(jsonUserImage))
w.Header().Add("Content-Type", "application/json")
})
})
r.Get("/image-events/{id}", func(w http.ResponseWriter, r *http.Request) {
// TODO: authentication :)
id := r.PathValue("id")
imageNotifier, exists := eventManager.listeners[id]
if !exists {
fmt.Println("Not found!")
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.(http.Flusher).Flush()
ctx, cancel := context.WithCancel(r.Context())
for {
select {
case <-ctx.Done():
fmt.Fprint(w, "event: close\ndata: Connection closed\n\n")
w.(http.Flusher).Flush()
cancel()
return
case data := <-imageNotifier:
fmt.Printf("Status received: %s\n", data)
fmt.Fprintf(w, "data: %s-%s\n", data, time.Now().String())
w.(http.Flusher).Flush()
cancel()
}
}
})
r.Post("/login", func(w http.ResponseWriter, r *http.Request) {
type LoginBody struct {
Email string `json:"email"`
}
loginBody := LoginBody{}
err := json.NewDecoder(r.Body).Decode(&loginBody)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "Request body was not correct")
return
}
// TODO: validate it's an email
auth.CreateCode(loginBody.Email)
w.WriteHeader(http.StatusOK)
})
r.Post("/code", func(w http.ResponseWriter, r *http.Request) {
type CodeBody struct {
Email string `json:"email"`
Code string `json:"code"`
}
type CodeReturn struct {
Access string `json:"access"`
Refresh string `json:"refresh"`
}
codeBody := CodeBody{}
if err := json.NewDecoder(r.Body).Decode(&codeBody); err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "Request body was not correct")
return
}
if err := auth.UseCode(codeBody.Email, codeBody.Code); err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "email or code are incorrect")
return
}
if exists := userModel.DoesUserExist(r.Context(), codeBody.Email); !exists {
userModel.Save(r.Context(), model.Users{
Email: codeBody.Email,
})
}
uuid, err := userModel.GetUserIdFromEmail(r.Context(), codeBody.Email)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Something went wrong.")
return
}
refresh := CreateRefreshToken(uuid)
access := CreateAccessToken(uuid)
codeReturn := CodeReturn{
Access: access,
Refresh: refresh,
}
json, err := json.Marshal(codeReturn)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Something went wrong.")
return
}
w.WriteHeader(http.StatusOK)
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, string(json))
})
logWriter := DatabaseWriter{
dbPool: db,
}
r.Route("/logs", createLogHandler(&logWriter))
log.Println("Listening and serving on port 3040.")
if err := http.ListenAndServe(":3040", r); err != nil {
log.Println(err)
return
}
}

39
backend/middleware.go Normal file
View File

@ -0,0 +1,39 @@
package main
import (
"context"
"net/http"
)
func CorsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(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", "*")
next.ServeHTTP(w, r)
})
}
const USER_ID = "UserID"
func ProtectedRoute(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if len(token) < len("Bearer ") {
w.WriteHeader(http.StatusUnauthorized)
return
}
userId, err := GetUserIdFromAccess(token[len("Bearer "):])
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
contextWithUserId := context.WithValue(r.Context(), USER_ID, userId)
newR := r.WithContext(contextWithUserId)
next.ServeHTTP(w, newR)
})
}

117
backend/models/contacts.go Normal file
View File

@ -0,0 +1,117 @@
package models
import (
"context"
"database/sql"
"screenmark/screenmark/.gen/haystack/haystack/model"
. "screenmark/screenmark/.gen/haystack/haystack/table"
. "github.com/go-jet/jet/v2/postgres"
"github.com/google/uuid"
)
type ContactModel struct {
dbPool *sql.DB
}
func (m ContactModel) List(ctx context.Context, userId uuid.UUID) ([]model.Contacts, error) {
listContactsStmt := SELECT(Contacts.AllColumns).
FROM(
Contacts.
INNER_JOIN(UserContacts, UserContacts.ContactID.EQ(Contacts.ID)),
).
WHERE(UserContacts.UserID.EQ(UUID(userId)))
locations := []model.Contacts{}
err := listContactsStmt.QueryContext(ctx, m.dbPool, &locations)
return locations, err
}
func (m ContactModel) Get(ctx context.Context, contactId uuid.UUID) (model.Contacts, error) {
getContactStmt := Contacts.
SELECT(Contacts.AllColumns).
WHERE(Contacts.ID.EQ(UUID(contactId)))
contact := model.Contacts{}
err := getContactStmt.QueryContext(ctx, m.dbPool, &contact)
return contact, err
}
func (m ContactModel) Update(ctx context.Context, contact model.Contacts) (model.Contacts, error) {
existingContact, err := m.Get(ctx, contact.ID)
if err != nil {
return model.Contacts{}, err
}
existingContact.Name = contact.Name
if contact.Description != nil {
existingContact.Description = contact.Description
}
if contact.PhoneNumber != nil {
existingContact.PhoneNumber = contact.PhoneNumber
}
if contact.Email != nil {
existingContact.Email = contact.Email
}
updateContactStmt := Contacts.
UPDATE(Contacts.MutableColumns).
MODEL(existingContact).
WHERE(Contacts.ID.EQ(UUID(contact.ID))).
RETURNING(Contacts.AllColumns)
updatedContact := model.Contacts{}
err = updateContactStmt.QueryContext(ctx, m.dbPool, &updatedContact)
return updatedContact, err
}
func (m ContactModel) Save(ctx context.Context, userId uuid.UUID, contact model.Contacts) (model.Contacts, error) {
// TODO: make this a transaction
if contact.ID != uuid.Nil {
return m.Update(ctx, contact)
}
insertContactStmt := Contacts.
INSERT(Contacts.Name, Contacts.Description, Contacts.PhoneNumber, Contacts.Email).
VALUES(contact.Name, contact.Description, contact.PhoneNumber, contact.Email).
RETURNING(Contacts.AllColumns)
insertedContact := model.Contacts{}
err := insertContactStmt.QueryContext(ctx, m.dbPool, &insertedContact)
if err != nil {
return insertedContact, err
}
insertUserContactStmt := UserContacts.
INSERT(UserContacts.UserID, UserContacts.ContactID).
VALUES(userId, insertedContact.ID)
_, err = insertUserContactStmt.ExecContext(ctx, m.dbPool)
return insertedContact, err
}
func (m ContactModel) SaveToImage(ctx context.Context, imageId uuid.UUID, contactId uuid.UUID) (model.ImageContacts, error) {
insertImageContactStmt := ImageContacts.
INSERT(ImageContacts.ImageID, ImageContacts.ContactID).
VALUES(imageId, contactId).
RETURNING(ImageContacts.AllColumns)
imageContact := model.ImageContacts{}
err := insertImageContactStmt.QueryContext(ctx, m.dbPool, &imageContact)
return imageContact, err
}
func NewContactModel(db *sql.DB) ContactModel {
return ContactModel{dbPool: db}
}

View File

@ -0,0 +1,19 @@
package models
import (
"database/sql"
"errors"
"os"
_ "github.com/lib/pq"
)
func InitDatabase() (*sql.DB, error) {
connection := os.Getenv("DB_CONNECTION")
if len(connection) == 0 {
return nil, errors.New("DB_CONNECTION env was not found.")
}
return sql.Open("postgres", connection)
}

94
backend/models/events.go Normal file
View File

@ -0,0 +1,94 @@
package models
import (
"context"
"database/sql"
. "github.com/go-jet/jet/v2/postgres"
"screenmark/screenmark/.gen/haystack/haystack/model"
. "screenmark/screenmark/.gen/haystack/haystack/table"
"github.com/google/uuid"
)
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.
INSERT(Events.MutableColumns).
MODEL(event).
RETURNING(Events.AllColumns)
insertedEvent := model.Events{}
err := insertEventStmt.QueryContext(ctx, m.dbPool, &insertedEvent)
if err != nil {
return insertedEvent, err
}
insertUserEventStmt := UserEvents.
INSERT(UserEvents.UserID, UserEvents.EventID).
VALUES(userId, insertedEvent.ID)
_, err = insertUserEventStmt.ExecContext(ctx, m.dbPool)
return insertedEvent, err
}
func (m EventModel) SaveToImage(ctx context.Context, imageId uuid.UUID, eventId uuid.UUID) (model.ImageEvents, error) {
insertImageEventStmt := ImageEvents.
INSERT(ImageEvents.ImageID, ImageEvents.EventID).
VALUES(imageId, eventId).
RETURNING(ImageEvents.AllColumns)
imageEvent := model.ImageEvents{}
err := insertImageEventStmt.QueryContext(ctx, m.dbPool, &imageEvent)
return imageEvent, err
}
func (m EventModel) UpdateLocation(ctx context.Context, eventId uuid.UUID, locationId uuid.UUID) (model.Events, error) {
updateEventLocationStmt := Events.
UPDATE(Events.LocationID).
SET(locationId).
WHERE(Events.ID.EQ(UUID(eventId))).
RETURNING(Events.AllColumns)
updatedEvent := model.Events{}
err := updateEventLocationStmt.QueryContext(ctx, m.dbPool, &updatedEvent)
return updatedEvent, err
}
func (m EventModel) UpdateOrganizer(ctx context.Context, eventId uuid.UUID, organizerId uuid.UUID) (model.Events, error) {
updateEventContactStmt := Events.
UPDATE(Events.OrganizerID).
SET(organizerId).
WHERE(Events.ID.EQ(UUID(eventId))).
RETURNING(Events.AllColumns)
updatedEvent := model.Events{}
err := updateEventContactStmt.QueryContext(ctx, m.dbPool, &updatedEvent)
return updatedEvent, err
}
func NewEventModel(db *sql.DB) EventModel {
return EventModel{dbPool: db}
}

172
backend/models/image.go Normal file
View File

@ -0,0 +1,172 @@
package models
import (
"context"
"database/sql"
"errors"
"fmt"
"screenmark/screenmark/.gen/haystack/haystack/model"
. "screenmark/screenmark/.gen/haystack/haystack/table"
. "github.com/go-jet/jet/v2/postgres"
"github.com/google/uuid"
)
type ImageModel struct {
dbPool *sql.DB
}
type ImageData struct {
model.UserImages
Image model.Image
}
type ProcessingImageData struct {
model.UserImagesToProcess
Image model.Image
}
func (m ImageModel) Process(ctx context.Context, userId uuid.UUID, image model.Image) (model.UserImagesToProcess, error) {
tx, err := m.dbPool.BeginTx(ctx, nil)
if err != nil {
return model.UserImagesToProcess{}, err
}
insertImageStmt := Image.
INSERT(Image.ImageName, Image.Image).
VALUES(image.ImageName, image.Image).
RETURNING(Image.ID)
insertedImage := model.Image{}
err = insertImageStmt.QueryContext(ctx, tx, &insertedImage)
if err != nil {
return model.UserImagesToProcess{}, err
}
stmt := UserImagesToProcess.
INSERT(UserImagesToProcess.UserID, UserImagesToProcess.ImageID).
VALUES(userId, insertedImage.ID).
RETURNING(UserImagesToProcess.AllColumns)
userImage := model.UserImagesToProcess{}
err = stmt.QueryContext(ctx, tx, &userImage)
if err != nil {
return model.UserImagesToProcess{}, err
}
err = tx.Commit()
return userImage, err
}
func (m ImageModel) GetToProcess(ctx context.Context, imageId uuid.UUID) (model.UserImagesToProcess, error) {
getToProcessStmt := UserImagesToProcess.
SELECT(UserImagesToProcess.AllColumns).
WHERE(UserImagesToProcess.ID.EQ(UUID(imageId)))
images := []model.UserImagesToProcess{}
err := getToProcessStmt.QueryContext(ctx, m.dbPool, &images)
if len(images) != 1 {
return model.UserImagesToProcess{}, errors.New(fmt.Sprintf("Expected 1, got %d\n", len(images)))
}
return images[0], err
}
func (m ImageModel) GetToProcessWithData(ctx context.Context, imageId uuid.UUID) (ProcessingImageData, error) {
stmt := SELECT(UserImagesToProcess.AllColumns, Image.AllColumns).
FROM(
UserImagesToProcess.INNER_JOIN(
Image, Image.ID.EQ(UserImagesToProcess.ImageID),
),
).WHERE(UserImagesToProcess.ID.EQ(UUID(imageId)))
images := []ProcessingImageData{}
err := stmt.QueryContext(ctx, m.dbPool, &images)
if len(images) != 1 {
return ProcessingImageData{}, errors.New(fmt.Sprintf("Expected 1, got %d\n", len(images)))
}
return images[0], err
}
func (m ImageModel) FinishProcessing(ctx context.Context, imageId uuid.UUID) (model.UserImages, error) {
imageToProcess, err := m.GetToProcess(ctx, imageId)
if err != nil {
return model.UserImages{}, err
}
tx, err := m.dbPool.Begin()
if err != nil {
return model.UserImages{}, err
}
insertImageStmt := UserImages.
INSERT(UserImages.UserID, UserImages.ImageID).
VALUES(imageToProcess.UserID, imageToProcess.ImageID).
RETURNING(UserImages.ID, UserImages.UserID, UserImages.ImageID)
userImage := model.UserImages{}
err = insertImageStmt.QueryContext(ctx, tx, &userImage)
if err != nil {
return model.UserImages{}, err
}
removeProcessingStmt := UserImagesToProcess.
DELETE().
WHERE(UserImagesToProcess.ID.EQ(UUID(imageToProcess.ID)))
_, err = removeProcessingStmt.ExecContext(ctx, tx)
if err != nil {
return model.UserImages{}, err
}
err = tx.Commit()
return userImage, err
}
func (m ImageModel) StartProcessing(ctx context.Context, processingImageId uuid.UUID) error {
startProcessingStmt := UserImagesToProcess.
UPDATE(UserImagesToProcess.Status).
SET(model.Progress_InProgress).
WHERE(UserImagesToProcess.ID.EQ(UUID(processingImageId)))
_, err := startProcessingStmt.ExecContext(ctx, m.dbPool)
return err
}
func (m ImageModel) Get(ctx context.Context, imageId uuid.UUID) (ImageData, error) {
getImageStmt := SELECT(UserImages.AllColumns, Image.AllColumns).
FROM(
UserImages.INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID)),
).
WHERE(UserImages.ID.EQ(UUID(imageId)))
images := []ImageData{}
err := getImageStmt.QueryContext(ctx, m.dbPool, &images)
if len(images) != 1 {
return ImageData{}, errors.New(fmt.Sprintf("Expected 1, got %d\n", len(images)))
}
return images[0], err
}
func (m ImageModel) IsUserAuthorized(ctx context.Context, imageId uuid.UUID, userId uuid.UUID) bool {
getImageUserId := UserImages.SELECT(UserImages.UserID).WHERE(UserImages.ImageID.EQ(UUID(imageId)))
userImage := model.UserImages{}
err := getImageUserId.QueryContext(ctx, m.dbPool, &userImage)
return err != nil && userImage.UserID.String() == userId.String()
}
func NewImageModel(db *sql.DB) ImageModel {
return ImageModel{dbPool: db}
}

33
backend/models/links.go Normal file
View File

@ -0,0 +1,33 @@
package models
import (
"context"
"database/sql"
. "screenmark/screenmark/.gen/haystack/haystack/table"
"github.com/google/uuid"
)
type LinkModel struct {
dbPool *sql.DB
}
func (m LinkModel) Save(ctx context.Context, imageId uuid.UUID, links []string) error {
if len(links) == 0 {
return nil
}
stmt := ImageLinks.INSERT(ImageLinks.ImageID, ImageLinks.Link)
for _, link := range links {
stmt = stmt.VALUES(imageId, link)
}
_, err := stmt.ExecContext(ctx, m.dbPool)
return err
}
func NewLinkModel(db *sql.DB) LinkModel {
return LinkModel{dbPool: db}
}

130
backend/models/locations.go Normal file
View File

@ -0,0 +1,130 @@
package models
import (
"context"
"database/sql"
"screenmark/screenmark/.gen/haystack/haystack/model"
. "screenmark/screenmark/.gen/haystack/haystack/table"
. "github.com/go-jet/jet/v2/postgres"
"github.com/go-jet/jet/v2/qrm"
"github.com/google/uuid"
)
type LocationModel struct {
dbPool *sql.DB
}
func (m LocationModel) List(ctx context.Context, userId uuid.UUID) ([]model.Locations, error) {
listLocationsStmt := SELECT(Locations.AllColumns).
FROM(
Locations.
INNER_JOIN(UserLocations, UserLocations.LocationID.EQ(Locations.ID)),
).
WHERE(UserLocations.UserID.EQ(UUID(userId)))
locations := []model.Locations{}
err := listLocationsStmt.QueryContext(ctx, m.dbPool, &locations)
return locations, err
}
func (m LocationModel) Get(ctx context.Context, locationId uuid.UUID) (model.Locations, error) {
getLocationStmt := Locations.
SELECT(Locations.AllColumns).
WHERE(Locations.ID.EQ(UUID(locationId)))
location := model.Locations{}
err := getLocationStmt.QueryContext(ctx, m.dbPool, &location)
return location, err
}
func (m LocationModel) Update(ctx context.Context, location model.Locations) (model.Locations, error) {
existingLocation, err := m.Get(ctx, location.ID)
if err != nil {
return model.Locations{}, err
}
existingLocation.Name = location.Name
if location.Description != nil {
existingLocation.Description = location.Description
}
if location.Address != nil {
existingLocation.Address = location.Address
}
updateLocationStmt := Locations.
UPDATE(Locations.MutableColumns).
MODEL(existingLocation).
WHERE(Locations.ID.EQ(UUID(location.ID))).
RETURNING(Locations.AllColumns)
updatedLocation := model.Locations{}
err = updateLocationStmt.QueryContext(ctx, m.dbPool, &updatedLocation)
return updatedLocation, err
}
func (m LocationModel) Save(ctx context.Context, userId uuid.UUID, location model.Locations) (model.Locations, error) {
if location.ID != uuid.Nil {
return m.Update(ctx, location)
}
insertLocationStmt := Locations.
INSERT(Locations.Name, Locations.Address, Locations.Description).
VALUES(location.Name, location.Address, location.Description).
RETURNING(Locations.AllColumns)
insertedLocation := model.Locations{}
err := insertLocationStmt.QueryContext(ctx, m.dbPool, &insertedLocation)
if err != nil {
return model.Locations{}, err
}
insertUserLocationStmt := UserLocations.
INSERT(UserLocations.UserID, UserLocations.LocationID).
VALUES(userId, insertedLocation.ID)
_, err = insertUserLocationStmt.ExecContext(ctx, m.dbPool)
return insertedLocation, err
}
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)
err = insertImageLocationStmt.QueryContext(ctx, m.dbPool, &imageLocation)
return imageLocation, err
}
func NewLocationModel(db *sql.DB) LocationModel {
return LocationModel{dbPool: db}
}

67
backend/models/notes.go Normal file
View File

@ -0,0 +1,67 @@
package models
import (
"context"
"database/sql"
"screenmark/screenmark/.gen/haystack/haystack/model"
. "screenmark/screenmark/.gen/haystack/haystack/table"
. "github.com/go-jet/jet/v2/postgres"
"github.com/google/uuid"
)
type NoteModel struct {
dbPool *sql.DB
}
func (m NoteModel) List(ctx context.Context, userId uuid.UUID) ([]model.Notes, error) {
listNotesStmt := SELECT(Notes.AllColumns).
FROM(
Notes.
INNER_JOIN(UserNotes, UserNotes.NoteID.EQ(Notes.ID)),
).
WHERE(UserNotes.UserID.EQ(UUID(userId)))
locations := []model.Notes{}
err := listNotesStmt.QueryContext(ctx, m.dbPool, &locations)
return locations, err
}
func (m NoteModel) Save(ctx context.Context, userId uuid.UUID, note model.Notes) (model.Notes, error) {
insertNoteStmt := Notes.
INSERT(Notes.Name, Notes.Description, Notes.Content).
VALUES(note.Name, note.Description, note.Content).
RETURNING(Notes.AllColumns)
insertedNote := model.Notes{}
err := insertNoteStmt.QueryContext(ctx, m.dbPool, &insertedNote)
if err != nil {
return model.Notes{}, err
}
insertUserNoteStmt := UserNotes.
INSERT(UserNotes.UserID, UserNotes.NoteID).
VALUES(userId, insertedNote.ID)
_, err = insertUserNoteStmt.ExecContext(ctx, m.dbPool)
return insertedNote, err
}
func (m NoteModel) SaveToImage(ctx context.Context, imageId uuid.UUID, noteId uuid.UUID) (model.ImageNotes, error) {
insertImageNoteStmt := ImageNotes.
INSERT(ImageNotes.ImageID, ImageNotes.NoteID).
VALUES(imageId, noteId).
RETURNING(ImageNotes.AllColumns)
imageNote := model.ImageNotes{}
err := insertImageNoteStmt.QueryContext(ctx, m.dbPool, &imageNote)
return imageNote, err
}
func NewNoteModel(db *sql.DB) NoteModel {
return NoteModel{dbPool: db}
}

156
backend/models/tags.go Normal file
View File

@ -0,0 +1,156 @@
package models
import (
"context"
"database/sql"
"fmt"
"screenmark/screenmark/.gen/haystack/haystack/model"
. "screenmark/screenmark/.gen/haystack/haystack/table"
. "github.com/go-jet/jet/v2/postgres"
"github.com/google/uuid"
)
type TagModel struct {
dbPool *sql.DB
}
// Raw dogging SQL is kinda based though?
//
// | nO, usE OrM!!
//
// | RAW - RAW
// | SQL | \ SQL
// | GOOD | \ GOOD
// | - -
// | -- --
// | -- --
// | ---- IQ ----
func (m TagModel) getNonExistantTags(ctx context.Context, userId uuid.UUID, tags []string) ([]string, error) {
if len(tags) == 0 {
return tags, nil
}
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]
getNonExistingTags := 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 = given_tags.tag
where user_tags.tag is null`, counter)
getNonExistingTagsStmt, err := m.dbPool.PrepareContext(ctx, getNonExistingTags)
defer getNonExistingTagsStmt.Close()
if err != nil {
return []string{}, err
}
args := make([]any, counter)
for i, v := range tags {
args[i] = v
}
args[counter-1] = userId.String()
rows, err := getNonExistingTagsStmt.QueryContext(ctx, args...)
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 (m TagModel) Save(ctx context.Context, userId uuid.UUID, tags []string) error {
tagsToInsert, err := m.getNonExistantTags(ctx, userId, tags)
if err != nil {
return err
}
if len(tagsToInsert) == 0 {
return nil
}
stmt := UserTags.INSERT(UserTags.UserID, UserTags.Tag)
for _, tag := range tagsToInsert {
stmt = stmt.VALUES(UUID(userId), tag)
}
_, err = stmt.ExecContext(ctx, m.dbPool)
return err
}
func (m TagModel) List(ctx context.Context, userId uuid.UUID) ([]model.UserTags, error) {
listTagsStmt := UserTags.SELECT(UserTags.AllColumns).WHERE(UserTags.UserID.EQ(UUID(userId)))
userTags := []model.UserTags{}
err := listTagsStmt.QueryContext(ctx, m.dbPool, &userTags)
return userTags, err
}
func (m TagModel) SaveToImage(ctx context.Context, imageId uuid.UUID, tags []string) error {
if len(tags) == 0 {
return nil
}
userId, err := getUserIdFromImage(ctx, m.dbPool, imageId)
if err != nil {
return err
}
err = m.Save(ctx, userId, tags)
if err != nil {
return 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(m.dbPool, &userTags)
if err != nil {
return err
}
stmt := ImageTags.INSERT(ImageTags.ImageID, ImageTags.TagID)
for _, t := range userTags {
stmt = stmt.VALUES(imageId, t.ID)
}
_, err = stmt.ExecContext(ctx, m.dbPool)
return err
}
func NewTagModel(db *sql.DB) TagModel {
return TagModel{dbPool: db}
}

35
backend/models/text.go Normal file
View File

@ -0,0 +1,35 @@
package models
import (
"context"
"database/sql"
. "screenmark/screenmark/.gen/haystack/haystack/table"
"github.com/google/uuid"
)
type TextModel struct {
dbPool *sql.DB
}
func (m TextModel) Save(ctx context.Context, imageId uuid.UUID, texts []string) error {
if len(texts) == 0 {
return nil
}
saveImageTextStmt := ImageText.INSERT(ImageText.ImageID, ImageText.ImageText)
for _, t := range texts {
saveImageTextStmt = saveImageTextStmt.VALUES(imageId, t)
}
saveImageTextStmt.RETURNING(ImageText.AllColumns)
_, err := saveImageTextStmt.ExecContext(ctx, m.dbPool)
return err
}
func NewTextModel(db *sql.DB) TextModel {
return TextModel{dbPool: db}
}

128
backend/models/user.go Normal file
View File

@ -0,0 +1,128 @@
package models
import (
"context"
"database/sql"
"errors"
"log"
"screenmark/screenmark/.gen/haystack/haystack/model"
. "screenmark/screenmark/.gen/haystack/haystack/table"
. "github.com/go-jet/jet/v2/postgres"
"github.com/go-jet/jet/v2/qrm"
"github.com/google/uuid"
)
type UserModel struct {
dbPool *sql.DB
}
type ImageWithProperties struct {
ID uuid.UUID
Image model.Image
Tags []struct {
model.ImageTags
Tag model.UserTags
}
Links []model.ImageLinks
Text []model.ImageText
Locations []model.Locations
Events []model.Events
Notes []model.Notes
Contacts []model.Contacts
}
func getUserIdFromImage(ctx context.Context, dbPool *sql.DB, imageId uuid.UUID) (uuid.UUID, error) {
getUserIdStmt := UserImages.SELECT(UserImages.UserID).WHERE(UserImages.ImageID.EQ(UUID(imageId)))
log.Println(getUserIdStmt.DebugSql())
userImages := []model.UserImages{}
err := getUserIdStmt.QueryContext(ctx, dbPool, &userImages)
if err != nil {
return uuid.Nil, err
}
if len(userImages) != 1 {
return uuid.Nil, errors.New("Expected exactly one choice.")
}
return userImages[0].UserID, nil
}
func (m UserModel) ListWithProperties(ctx context.Context, userId uuid.UUID) ([]ImageWithProperties, error) {
listWithPropertiesStmt := SELECT(
UserImages.ID.AS("ImageWithProperties.ID"),
Image.ID,
Image.ImageName,
ImageTags.AllColumns,
UserTags.AllColumns,
ImageText.AllColumns,
ImageLinks.AllColumns,
ImageLocations.AllColumns,
Locations.AllColumns,
ImageEvents.AllColumns,
Events.AllColumns,
ImageContacts.AllColumns,
Contacts.AllColumns,
ImageNotes.AllColumns,
Notes.AllColumns,
).
FROM(
UserImages.INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID)).
LEFT_JOIN(ImageTags, ImageTags.ImageID.EQ(Image.ID)).
LEFT_JOIN(UserTags, UserTags.ID.EQ(ImageTags.TagID)).
LEFT_JOIN(ImageText, ImageText.ImageID.EQ(Image.ID)).
LEFT_JOIN(ImageLinks, ImageLinks.ImageID.EQ(Image.ID)).
LEFT_JOIN(ImageLocations, ImageLocations.ImageID.EQ(UserImages.ImageID)).
LEFT_JOIN(Locations, Locations.ID.EQ(ImageLocations.LocationID)).
LEFT_JOIN(ImageEvents, ImageEvents.ImageID.EQ(UserImages.ImageID)).
LEFT_JOIN(Events, Events.ID.EQ(ImageEvents.EventID)).
LEFT_JOIN(ImageContacts, ImageContacts.ImageID.EQ(UserImages.ImageID)).
LEFT_JOIN(Contacts, Contacts.ID.EQ(ImageContacts.ContactID)).
LEFT_JOIN(ImageNotes, ImageNotes.ImageID.EQ(UserImages.ImageID)).
LEFT_JOIN(Notes, Notes.ID.EQ(ImageNotes.NoteID))).
WHERE(UserImages.UserID.EQ(UUID(userId)))
images := []ImageWithProperties{}
err := listWithPropertiesStmt.QueryContext(ctx, m.dbPool, &images)
if err != nil {
return images, err
}
return images, err
}
func (m UserModel) GetUserIdFromEmail(ctx context.Context, email string) (uuid.UUID, error) {
getUserIdStmt := Users.SELECT(Users.ID).WHERE(Users.Email.EQ(String(email)))
user := model.Users{}
err := getUserIdStmt.QueryContext(ctx, m.dbPool, &user)
return user.ID, err
}
func (m UserModel) DoesUserExist(ctx context.Context, email string) bool {
getUserIdStmt := Users.SELECT(Users.ID).WHERE(Users.Email.EQ(String(email)))
user := model.Users{}
err := getUserIdStmt.QueryContext(ctx, m.dbPool, &user)
return err != qrm.ErrNoRows
}
func (m UserModel) Save(ctx context.Context, user model.Users) (model.Users, error) {
insertUserStmt := Users.INSERT(Users.Email).VALUES(user.Email).RETURNING(Users.AllColumns)
insertedUser := model.Users{}
err := insertUserStmt.QueryContext(ctx, m.dbPool, &insertedUser)
return insertedUser, err
}
func NewUserModel(db *sql.DB) UserModel {
return UserModel{dbPool: db}
}

287
backend/schema.sql Normal file
View File

@ -0,0 +1,287 @@
DROP SCHEMA IF EXISTS haystack CASCADE;
CREATE SCHEMA haystack;
/* -----| Enums |----- */
CREATE TYPE haystack.progress AS ENUM('not-started','in-progress');
/* -----| Schema tables |----- */
CREATE TABLE haystack.users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL
);
CREATE TABLE haystack.image (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
image_name TEXT NOT NULL,
image BYTEA NOT NULL
);
CREATE TABLE haystack.user_images_to_process (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
status haystack.progress NOT NULL DEFAULT 'not-started',
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)
);
CREATE TABLE haystack.user_tags (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tag VARCHAR(32) UNIQUE 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_id UUID NOT NULL REFERENCES haystack.user_tags (id),
image_id UUID NOT NULL REFERENCES haystack.image (id)
);
CREATE TABLE haystack.image_text (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
image_text TEXT NOT NULL,
image_id UUID NOT NULL REFERENCES haystack.image (id)
);
CREATE TABLE haystack.image_links (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
link TEXT NOT NULL,
image_id UUID NOT NULL REFERENCES haystack.image (id)
);
CREATE TABLE haystack.locations (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
address TEXT,
description TEXT
);
CREATE TABLE haystack.image_locations (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
location_id UUID NOT NULL REFERENCES haystack.locations (id),
image_id UUID NOT NULL REFERENCES haystack.image (id)
);
CREATE TABLE haystack.user_locations (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
location_id UUID NOT NULL REFERENCES haystack.locations (id),
user_id UUID NOT NULL REFERENCES haystack.users (id)
);
CREATE TABLE haystack.contacts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
-- It seems name and description are frequent. We could use table inheritance.
name TEXT NOT NULL,
description TEXT,
phone_number TEXT,
email TEXT
);
CREATE TABLE haystack.user_contacts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES haystack.users (id),
contact_id UUID NOT NULL REFERENCES haystack.contacts (id)
);
CREATE TABLE haystack.image_contacts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
image_id UUID NOT NULL REFERENCES haystack.image (id),
contact_id UUID NOT NULL REFERENCES haystack.contacts (id)
);
CREATE TABLE haystack.events (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
-- It seems name and description are frequent. We could use table inheritance.
name TEXT NOT NULL,
description TEXT,
start_date_time TIMESTAMP,
end_date_time TIMESTAMP,
location_id UUID REFERENCES haystack.locations (id),
organizer_id UUID REFERENCES haystack.contacts (id)
);
CREATE TABLE haystack.image_events (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
event_id UUID NOT NULL REFERENCES haystack.events (id),
image_id UUID NOT NULL REFERENCES haystack.image (id)
);
CREATE TABLE haystack.user_events (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
event_id UUID NOT NULL REFERENCES haystack.events (id),
user_id UUID NOT NULL REFERENCES haystack.users (id)
);
CREATE TABLE haystack.notes (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
-- It seems name and description are frequent. We could use table inheritance.
name TEXT NOT NULL,
description TEXT,
content TEXT NOT NULL
);
CREATE TABLE haystack.image_notes (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
image_id UUID NOT NULL REFERENCES haystack.image (id),
note_id UUID NOT NULL REFERENCES haystack.notes (id)
);
CREATE TABLE haystack.user_notes (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES haystack.users (id),
note_id UUID NOT NULL REFERENCES haystack.notes (id)
);
CREATE TABLE haystack.logs (
log TEXT NOT NULL,
image_id UUID NOT NULL REFERENCES haystack.image (id)
);
/* -----| Indexes |----- */
CREATE INDEX user_tags_index ON haystack.user_tags(tag);
/* -----| Stored Procedures |----- */
CREATE OR REPLACE FUNCTION notify_new_image()
RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify('new_image', NEW.id::texT);
RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION notify_new_processing_image_status()
RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify('new_processing_image_status', NEW.id::text || NEW.status::text);
RETURN NEW;
END
$$ LANGUAGE plpgsql;
/* -----| Triggers |----- */
CREATE OR REPLACE TRIGGER on_new_image AFTER INSERT
ON haystack.user_images_to_process
FOR EACH ROW
EXECUTE PROCEDURE notify_new_image();
CREATE OR REPLACE TRIGGER on_update_image_progress
AFTER UPDATE OF status
ON haystack.user_images_to_process
FOR EACH ROW
EXECUTE PROCEDURE notify_new_processing_image_status();
/* -----| Test Data |----- */
-- Insert a user
INSERT INTO haystack.users (id, email) VALUES ('1db09f34-b155-4bf2-b606-dda25365fc89', 'me@email.com');
-- Insert images
INSERT INTO haystack.image (id, image_name, image) VALUES
('3bd3fa04-e4b4-4ffb-b282-d573a092eb71', 'Sample Image 1', 'sample_image_1_bytes'),
('f4560a78-d5d3-433e-8d90-b75c66e25423', 'Sample Image 2', 'sample_image_2_bytes');
-- Insert user images to process
INSERT INTO haystack.user_images_to_process (id, image_id, user_id) VALUES
('abe3679c-e787-4670-b5da-570453938f18', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71', '1db09f34-b155-4bf2-b606-dda25365fc89'),
('8f3727e8-03fa-49bf-b0fe-ba8762df0902', 'f4560a78-d5d3-433e-8d90-b75c66e25423', '1db09f34-b155-4bf2-b606-dda25365fc89');
-- Insert user images
INSERT INTO haystack.user_images (id, image_id, user_id) VALUES
('28ade3a5-30c0-4f0a-93ff-5d062ba5c253', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71', '1db09f34-b155-4bf2-b606-dda25365fc89'),
('c9425f01-a496-4c0a-919e-54b58c8ba600', 'f4560a78-d5d3-433e-8d90-b75c66e25423', '1db09f34-b155-4bf2-b606-dda25365fc89');
-- Insert user tags
INSERT INTO haystack.user_tags (id, tag, user_id) VALUES
('118c9491-a1ea-4930-88ee-33edfbc61cd3', 'vacation', '1db09f34-b155-4bf2-b606-dda25365fc89'),
('c3e8c00a-4af6-45c6-acc3-53aa7ce2024a', 'family', '1db09f34-b155-4bf2-b606-dda25365fc89');
-- Insert image tags
INSERT INTO haystack.image_tags (id, tag_id, image_id) VALUES
('38ec5481-7b09-4e50-98b8-a85bbd5f6c6e', '118c9491-a1ea-4930-88ee-33edfbc61cd3', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71'),
('9d64f58e-1d61-4c97-ae8b-a38bc3519fe1', 'c3e8c00a-4af6-45c6-acc3-53aa7ce2024a', 'f4560a78-d5d3-433e-8d90-b75c66e25423');
-- Insert image text
INSERT INTO haystack.image_text (id, image_text, image_id) VALUES
('fdd7a9f4-2a9a-494e-89d2-a63df8e45d62', 'Sample text for image 1', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71'),
('95516f15-575c-485b-92ab-22eb18a306c1', 'Sample text for image 2', 'f4560a78-d5d3-433e-8d90-b75c66e25423');
-- Insert image links
INSERT INTO haystack.image_links (id, link, image_id) VALUES
('bbcc284f-c1f6-47ac-8d54-65b7729f03be', 'http://example.com/image1', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71'),
('7391b2d1-6141-4195-8a4c-9c8ba4491b5a', 'http://example.com/image2', 'f4560a78-d5d3-433e-8d90-b75c66e25423');
-- Insert locations
INSERT INTO haystack.locations (id, name, address, description) VALUES
('5ac6f116-c21a-408b-9d2b-e8227a9a8503', 'Sample Location 1', '123 Sample St', 'A sample location'),
('cd4b1815-5019-406d-9f1d-e9e5ac34c5f1', 'Sample Location 2', '456 Sample Ave', 'Another sample location');
-- Insert image locations
INSERT INTO haystack.image_locations (id, location_id, image_id) VALUES
('0e0c5cc2-b5b3-4b26-9d9c-2517b9358eb3', '5ac6f116-c21a-408b-9d2b-e8227a9a8503', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71'),
('98facc74-cfc0-41cd-87e1-5e3822ae3407', 'cd4b1815-5019-406d-9f1d-e9e5ac34c5f1', 'f4560a78-d5d3-433e-8d90-b75c66e25423');
-- Insert user locations
INSERT INTO haystack.user_locations (id, location_id, user_id) VALUES
('1427ca1c-293f-4fab-b813-2acf145715f5', '5ac6f116-c21a-408b-9d2b-e8227a9a8503', '1db09f34-b155-4bf2-b606-dda25365fc89'),
('343f9321-f63d-4248-aaab-3a1264d9cb5e', 'cd4b1815-5019-406d-9f1d-e9e5ac34c5f1', '1db09f34-b155-4bf2-b606-dda25365fc89');
-- Insert contacts
INSERT INTO haystack.contacts (id, name, description, phone_number, email) VALUES
('943be2ab-4db4-4e4e-bd1c-b78ad96df0d1', 'Contact 1', 'Sample contact description', '123-456-7890', 'contact1@example.com'),
('09e2bf18-09b7-4553-971e-45136bd5b12f', 'Contact 2', 'Another sample contact description', '098-765-4321', 'contact2@example.com');
-- Insert user contacts
INSERT INTO haystack.user_contacts (id, user_id, contact_id) VALUES
('d74125e4-cbe4-4b83-8432-e0a3206af91c', '1db09f34-b155-4bf2-b606-dda25365fc89', '943be2ab-4db4-4e4e-bd1c-b78ad96df0d1'),
('46e8cbd4-46a6-4499-9575-d3aad003fd1c', '1db09f34-b155-4bf2-b606-dda25365fc89', '09e2bf18-09b7-4553-971e-45136bd5b12f');
-- Insert image contacts
INSERT INTO haystack.image_contacts (id, image_id, contact_id) VALUES
('db075381-e89b-4582-800e-07561f9139e8', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71', '943be2ab-4db4-4e4e-bd1c-b78ad96df0d1'),
('7384970d-3d3c-4e29-b158-edf200c53169', 'f4560a78-d5d3-433e-8d90-b75c66e25423', '09e2bf18-09b7-4553-971e-45136bd5b12f');
-- Insert events
INSERT INTO haystack.events (id, name, description, start_date_time, end_date_time, location_id, organizer_id) VALUES
('24a9dcbc-f8dc-4fca-835b-7ea57850d0b7', 'Sample Event 1', 'A sample event description', '2023-01-01 10:00:00', '2023-01-01 12:00:00', '5ac6f116-c21a-408b-9d2b-e8227a9a8503', '943be2ab-4db4-4e4e-bd1c-b78ad96df0d1'),
('9cb6b0ae-3b02-4343-9858-5a07dd248562', 'Sample Event 2', 'Another sample event description', '2023-02-01 14:00:00', '2023-02-01 16:00:00', 'cd4b1815-5019-406d-9f1d-e9e5ac34c5f1', '09e2bf18-09b7-4553-971e-45136bd5b12f');
-- Insert image events
INSERT INTO haystack.image_events (id, event_id, image_id) VALUES
('5268a005-b3eb-4a30-8823-c8e9666507bb', '24a9dcbc-f8dc-4fca-835b-7ea57850d0b7', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71'),
('9d6d4d26-c2a2-427f-92ed-34dc8c2d3e5f', '9cb6b0ae-3b02-4343-9858-5a07dd248562', 'f4560a78-d5d3-433e-8d90-b75c66e25423');
-- Insert user events
INSERT INTO haystack.user_events (id, event_id, user_id) VALUES
('16d815e4-6387-4fe9-b31d-5baff0567345', '24a9dcbc-f8dc-4fca-835b-7ea57850d0b7', '1db09f34-b155-4bf2-b606-dda25365fc89'),
('43078366-d265-4ff9-9210-e11680bd6bcd', '9cb6b0ae-3b02-4343-9858-5a07dd248562', '1db09f34-b155-4bf2-b606-dda25365fc89');
-- Insert notes
INSERT INTO haystack.notes (id, name, description, content) VALUES
('6524f6b9-c659-409e-b2a0-abd3c3f5b5bb', 'Sample Note 1', 'A sample note description', 'This is the content of the sample note 1'),
('a274b9b3-024f-457d-b4a0-d4535c2cca54', 'Sample Note 2', 'Another sample note description', 'This is the content of the sample note 2');
-- Insert image notes
INSERT INTO haystack.image_notes (id, image_id, note_id) VALUES
('6062fceb-7b3f-41fb-8509-489218968204', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71', '6524f6b9-c659-409e-b2a0-abd3c3f5b5bb'),
('956dd3f6-4513-4cbc-9a5e-03dbec769402', 'f4560a78-d5d3-433e-8d90-b75c66e25423', 'a274b9b3-024f-457d-b4a0-d4535c2cca54');
-- Insert user notes
INSERT INTO haystack.user_notes (id, user_id, note_id) VALUES
('e3fa7a74-acbf-4aa9-930b-f10bd8a6ced5', '1db09f34-b155-4bf2-b606-dda25365fc89', '6524f6b9-c659-409e-b2a0-abd3c3f5b5bb'),
('ebaef76b-3b78-491c-93f7-19510080284d', '1db09f34-b155-4bf2-b606-dda25365fc89', 'a274b9b3-024f-457d-b4a0-d4535c2cca54');

17
biome.json Normal file
View File

@ -0,0 +1,17 @@
{
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 4
}
}

View File

@ -1,3 +1,5 @@
.env
db
screenmark
node_modules
dist

3
frontend/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

607
frontend/.idea/caches/deviceStreaming.xml generated Normal file
View File

@ -0,0 +1,607 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceStreaming">
<option name="deviceSelectionList">
<list>
<PersistentDeviceSelectionData>
<option name="api" value="27" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="F01L" />
<option name="id" value="F01L" />
<option name="labId" value="google" />
<option name="manufacturer" value="FUJITSU" />
<option name="name" value="F-01L" />
<option name="screenDensity" value="360" />
<option name="screenX" value="720" />
<option name="screenY" value="1280" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="OnePlus" />
<option name="codename" value="OP5552L1" />
<option name="id" value="OP5552L1" />
<option name="labId" value="google" />
<option name="manufacturer" value="OnePlus" />
<option name="name" value="CPH2415" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2412" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="OPPO" />
<option name="codename" value="OP573DL1" />
<option name="id" value="OP573DL1" />
<option name="labId" value="google" />
<option name="manufacturer" value="OPPO" />
<option name="name" value="CPH2557" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="28" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="SH-01L" />
<option name="id" value="SH-01L" />
<option name="labId" value="google" />
<option name="manufacturer" value="SHARP" />
<option name="name" value="AQUOS sense2 SH-01L" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="Lenovo" />
<option name="codename" value="TB370FU" />
<option name="formFactor" value="Tablet" />
<option name="id" value="TB370FU" />
<option name="labId" value="google" />
<option name="manufacturer" value="Lenovo" />
<option name="name" value="Tab P12" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1840" />
<option name="screenY" value="2944" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="a15" />
<option name="id" value="a15" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="A15" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="a35x" />
<option name="id" value="a35x" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="A35" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />
<option name="codename" value="a51" />
<option name="id" value="a51" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy A51" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="akita" />
<option name="id" value="akita" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="motorola" />
<option name="codename" value="arcfox" />
<option name="id" value="arcfox" />
<option name="labId" value="google" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="razr plus 2024" />
<option name="screenDensity" value="360" />
<option name="screenX" value="1080" />
<option name="screenY" value="1272" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="motorola" />
<option name="codename" value="austin" />
<option name="id" value="austin" />
<option name="labId" value="google" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="moto g 5G (2022)" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="b0q" />
<option name="id" value="b0q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S22 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="32" />
<option name="brand" value="google" />
<option name="codename" value="bluejay" />
<option name="id" value="bluejay" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="caiman" />
<option name="id" value="caiman" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro" />
<option name="screenDensity" value="360" />
<option name="screenX" value="960" />
<option name="screenY" value="2142" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="comet" />
<option name="default" value="true" />
<option name="id" value="comet" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro Fold" />
<option name="screenDensity" value="390" />
<option name="screenX" value="2076" />
<option name="screenY" value="2152" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="29" />
<option name="brand" value="samsung" />
<option name="codename" value="crownqlteue" />
<option name="id" value="crownqlteue" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Note9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2220" />
<option name="screenY" value="1080" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="dm2q" />
<option name="id" value="dm2q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="S23 Plus" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="dm3q" />
<option name="id" value="dm3q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S23 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="e1q" />
<option name="default" value="true" />
<option name="id" value="e1q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S24" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="e3q" />
<option name="id" value="e3q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S24 Ultra" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1440" />
<option name="screenY" value="3120" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="eos" />
<option name="id" value="eos" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Eos" />
<option name="screenDensity" value="320" />
<option name="screenX" value="384" />
<option name="screenY" value="384" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix_camera" />
<option name="id" value="felix_camera" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold (Camera-enabled)" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="motorola" />
<option name="codename" value="fogona" />
<option name="id" value="fogona" />
<option name="labId" value="google" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="moto g play - 2024" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="g0q" />
<option name="id" value="g0q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-S906U1" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gta9pwifi" />
<option name="id" value="gta9pwifi" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-X210" />
<option name="screenDensity" value="240" />
<option name="screenX" value="1200" />
<option name="screenY" value="1920" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts7xllite" />
<option name="id" value="gts7xllite" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-T738U" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="gts8uwifi" />
<option name="formFactor" value="Tablet" />
<option name="id" value="gts8uwifi" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S8 Ultra" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1848" />
<option name="screenY" value="2960" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts8wifi" />
<option name="formFactor" value="Tablet" />
<option name="id" value="gts8wifi" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S8" />
<option name="screenDensity" value="274" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts9fe" />
<option name="id" value="gts9fe" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S9 FE 5G" />
<option name="screenDensity" value="280" />
<option name="screenX" value="1440" />
<option name="screenY" value="2304" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="husky" />
<option name="id" value="husky" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8 Pro" />
<option name="screenDensity" value="390" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="motorola" />
<option name="codename" value="java" />
<option name="id" value="java" />
<option name="labId" value="google" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="G20" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="komodo" />
<option name="id" value="komodo" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro XL" />
<option name="screenDensity" value="360" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="lynx" />
<option name="id" value="lynx" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="motorola" />
<option name="codename" value="maui" />
<option name="id" value="maui" />
<option name="labId" value="google" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="moto g play - 2023" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="o1q" />
<option name="id" value="o1q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S21" />
<option name="screenDensity" value="421" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="google" />
<option name="codename" value="oriole" />
<option name="id" value="oriole" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="panther" />
<option name="id" value="panther" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q5q" />
<option name="id" value="q5q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold5" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1812" />
<option name="screenY" value="2176" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q6q" />
<option name="id" value="q6q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold6" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1856" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="r11" />
<option name="formFactor" value="Wear OS" />
<option name="id" value="r11" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Watch" />
<option name="screenDensity" value="320" />
<option name="screenX" value="384" />
<option name="screenY" value="384" />
<option name="type" value="WEAR_OS" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="r11q" />
<option name="id" value="r11q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-S711U" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="redfin" />
<option name="id" value="redfin" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 5" />
<option name="screenDensity" value="440" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="shiba" />
<option name="id" value="shiba" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="t2q" />
<option name="id" value="t2q" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S21 Plus" />
<option name="screenDensity" value="394" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="tangorpro" />
<option name="formFactor" value="Tablet" />
<option name="id" value="tangorpro" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Tablet" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="tokay" />
<option name="default" value="true" />
<option name="id" value="tokay" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="35" />
<option name="brand" value="google" />
<option name="codename" value="tokay" />
<option name="default" value="true" />
<option name="id" value="tokay" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
</list>
</option>
</component>
</project>

9
frontend/.idea/frontend.iml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
frontend/.idea/misc.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
frontend/.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/frontend.iml" filepath="$PROJECT_DIR$/.idea/frontend.iml" />
</modules>
</component>
</project>

6
frontend/.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

BIN
frontend/bun.lockb Executable file

Binary file not shown.

17
frontend/index.html Normal file
View File

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="icon" type="image/svg+xml" href="/src/assets/logo.svg" />
<title>Tauri + Solid + Typescript App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="/src/index.tsx" type="module"></script>
</body>
</html>

48
frontend/package.json Normal file
View File

@ -0,0 +1,48 @@
{
"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-fs": "~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-markdown": "^2.0.14",
"solid-motionone": "^1.0.3",
"solidjs-markdown": "^0.2.0",
"tailwind-scrollbar-hide": "^2.0.0",
"tauri-plugin-sharetarget-api": "^0.1.6",
"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"
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -0,0 +1,6 @@
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

1
frontend/public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

7
frontend/src-tauri/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

5925
frontend/src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,41 @@
[package]
name = "Haystack"
version = "0.1.0"
description = "Screenshots that organize themselves"
authors = ["Dmytro Kondakov", "John Costa"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "haystack_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.0.0-beta.12", features = [] }
[dependencies]
tauri = { version = "2.0.0-beta.12", features = ["macos-private-api"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
notify = "6.1.1"
base64 = "0.21.7"
tokio = { version = "1.36.0", features = ["full"] }
tauri-plugin-store = "2.0.0-beta.12"
tauri-plugin-http = "2.0.0-beta.12"
tauri-plugin-fs = "2"
tauri-plugin-dialog = "2.2.1"
tauri-plugin-opener = "2.2.6"
tauri-plugin-sharetarget = "0.1.6"
[target."cfg(target_os = \"macos\")".dependencies]
cocoa = "0.26"
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
tauri-plugin-global-shortcut = "2.0.0-beta.12"
[target."cfg(target_os = \"android\")".dependencies]
tauri-plugin-sharetarget = "0.1.6"

View File

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

Some files were not shown because too many files have changed in this diff Show More