6 Commits

13 changed files with 225 additions and 64 deletions

View File

@ -15,6 +15,9 @@ You are an AI agent who's job is to describe the image you see.
You should also add any text you see in the image, if no text exists, just add a description.
Be consise and don't add too much extra information or formatting characters, simple text.
You must write this text in Markdown. You can add extra information for the user.
You must organise this text nicely, not be all over the place.
`
type DescriptionAgent struct {

View File

@ -33,6 +33,8 @@ and extract some meaning about what the image is.
You must call "listLists" to see which available lists are already available.
Use "createList" only once, don't create multiple lists for one image.
You can add an image to multiple lists, this is also true if you already created a list. But only add to a list if it makes sense to do so.
**Tools:**
* think: Internal reasoning/planning step.
* listLists: Get existing lists

View File

@ -8,8 +8,10 @@ import (
"net/http"
"os"
"screenmark/screenmark/agents"
"screenmark/screenmark/middleware"
"screenmark/screenmark/models"
"strconv"
"sync"
"time"
"github.com/charmbracelet/log"
@ -64,14 +66,24 @@ func ListenNewImageEvents(db *sql.DB, notifier *Notifier[Notification]) {
}
descriptionAgent := agents.NewDescriptionAgent(createLogger("Description 📝", splitWriter), imageModel)
err = descriptionAgent.Describe(createLogger("Description 📓", splitWriter), image.Image.ID, image.Image.ImageName, image.Image.Image)
if err != nil {
log.Error(err)
}
listAgent := agents.NewListAgent(createLogger("Lists 🖋️", splitWriter), listModel)
listAgent.RunAgent(image.UserID, image.ImageID, image.Image.ImageName, image.Image.Image)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
descriptionAgent.Describe(createLogger("Description 📓", splitWriter), image.Image.ID, image.Image.ImageName, image.Image.Image)
}()
go func() {
defer wg.Done()
listAgent.RunAgent(image.UserID, image.ImageID, image.Image.ImageName, image.Image.Image)
}()
wg.Wait()
_, err = imageModel.FinishProcessing(ctx, image.ID)
if err != nil {
@ -140,7 +152,7 @@ func CreateEventsHandler(notifier *Notifier[Notification]) http.HandlerFunc {
userSplitters := make(map[string]*ChannelSplitter[Notification])
return func(w http.ResponseWriter, r *http.Request) {
_userId := r.Context().Value(USER_ID).(uuid.UUID)
_userId := r.Context().Value(middleware.USER_ID).(uuid.UUID)
if _userId == uuid.Nil {
w.WriteHeader(http.StatusUnauthorized)
return

View File

@ -12,6 +12,9 @@ import (
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/models"
"screenmark/screenmark/stacks"
ourmiddleware "screenmark/screenmark/middleware"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
@ -41,6 +44,8 @@ func main() {
imageModel := models.NewImageModel(db)
userModel := models.NewUserModel(db)
stackHandler := stacks.CreateStackHandler(db)
mail, err := CreateMailClient()
if err != nil {
panic(err)
@ -56,10 +61,9 @@ func main() {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(CorsMiddleware)
r.Options("/*", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
r.Use(ourmiddleware.CorsMiddleware)
r.Route("/stacks", stackHandler.CreateRoutes)
// Temporarily not in protect route because we aren't using cookies.
// Therefore they don't get automatically attached to the request.
@ -102,7 +106,7 @@ func main() {
})
r.Group(func(r chi.Router) {
r.Use(ProtectedRoute)
r.Use(ourmiddleware.ProtectedRoute)
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")
@ -112,7 +116,7 @@ func main() {
})
r.Get("/image", func(w http.ResponseWriter, r *http.Request) {
userId := r.Context().Value(USER_ID).(uuid.UUID)
userId := r.Context().Value(ourmiddleware.USER_ID).(uuid.UUID)
if err != nil {
w.WriteHeader(http.StatusForbidden)
fmt.Fprintf(w, "You cannot read this")
@ -168,7 +172,7 @@ func main() {
r.Post("/image/{name}", func(w http.ResponseWriter, r *http.Request) {
imageName := r.PathValue("name")
userId := r.Context().Value(USER_ID).(uuid.UUID)
userId := r.Context().Value(ourmiddleware.USER_ID).(uuid.UUID)
if len(imageName) == 0 {
w.WriteHeader(http.StatusBadRequest)
@ -256,7 +260,7 @@ func main() {
})
r.Route("/notifications", func(r chi.Router) {
r.Use(GetUserIdFromUrl)
r.Use(ourmiddleware.GetUserIdFromUrl)
r.Get("/", CreateEventsHandler(&notifier))
})
@ -322,8 +326,8 @@ func main() {
return
}
refresh := CreateRefreshToken(uuid)
access := CreateAccessToken(uuid)
refresh := ourmiddleware.CreateRefreshToken(uuid)
access := ourmiddleware.CreateAccessToken(uuid)
codeReturn := CodeReturn{
Access: access,
@ -355,8 +359,8 @@ func main() {
return
}
refresh := CreateRefreshToken(uuid)
access := CreateAccessToken(uuid)
refresh := ourmiddleware.CreateRefreshToken(uuid)
access := ourmiddleware.CreateAccessToken(uuid)
codeReturn := CodeReturn{
Access: access,

View File

@ -0,0 +1,11 @@
package middleware
import "net/http"
func SetJson(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)
})
}

View File

@ -1,4 +1,4 @@
package main
package middleware
import (
"errors"

View File

@ -1,22 +1,50 @@
package main
package middleware
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/google/uuid"
)
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", "*")
// Access-Control-Allow-Methods is often needed for preflight OPTIONS requests
w.Header().Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
// The client makes an OPTIONS preflight request before a complex request.
// We must handle this and respond with the appropriate headers.
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
const USER_ID = "UserID"
func GetUserID(ctx context.Context) (uuid.UUID, error) {
userId := ctx.Value(USER_ID)
if userId == nil {
return uuid.Nil, errors.New("context does not contain a user id")
}
userIdUuid, ok := userId.(uuid.UUID)
if !ok {
return uuid.Nil, fmt.Errorf("context user id is not of type uuid, got: %t", userId)
}
return userIdUuid, nil
}
func ProtectedRoute(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")

View File

@ -51,7 +51,10 @@ func (m UserModel) Save(ctx context.Context, user model.Users) (model.Users, err
type UserImageWithImage struct {
model.UserImages
Image model.Image
Image struct {
model.Image
ImageLists []model.ImageLists
}
}
func (m UserModel) GetUserImages(ctx context.Context, userId uuid.UUID) ([]UserImageWithImage, error) {
@ -60,8 +63,13 @@ func (m UserModel) GetUserImages(ctx context.Context, userId uuid.UUID) ([]UserI
Image.ID,
Image.ImageName,
Image.Description,
ImageLists.AllColumns,
).
FROM(UserImages.INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID))).
FROM(
UserImages.
INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID)).
INNER_JOIN(ImageLists, ImageLists.ImageID.EQ(UserImages.ImageID)),
).
WHERE(UserImages.UserID.EQ(UUID(userId)))
userImages := []UserImageWithImage{}

67
backend/stacks/handler.go Normal file
View File

@ -0,0 +1,67 @@
package stacks
import (
"database/sql"
"encoding/json"
"net/http"
"os"
"screenmark/screenmark/middleware"
"screenmark/screenmark/models"
"github.com/charmbracelet/log"
"github.com/go-chi/chi/v5"
)
type StackHandler struct {
logger *log.Logger
stackModel models.ListModel
}
func (h *StackHandler) getAllStacks(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
userId, err := middleware.GetUserID(ctx)
if err != nil {
h.logger.Warn("could not get users in get all stacks", "err", err)
w.WriteHeader(http.StatusUnauthorized)
return
}
lists, err := h.stackModel.List(ctx, userId)
if err != nil {
h.logger.Warn("could not get stacks", "err", err)
w.WriteHeader(http.StatusBadRequest)
return
}
jsonLists, err := json.Marshal(lists)
if err != nil {
h.logger.Warn("could not marshal json lists", "err", err)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Write(jsonLists)
w.WriteHeader(http.StatusOK)
}
func (h *StackHandler) CreateRoutes(r chi.Router) {
h.logger.Info("Mounting stack router")
r.Group(func(r chi.Router) {
r.Use(middleware.ProtectedRoute)
r.Use(middleware.SetJson)
r.Get("/", h.getAllStacks)
})
}
func CreateStackHandler(db *sql.DB) StackHandler {
stackModel := models.NewListModel(db)
logger := log.New(os.Stdout).WithPrefix("Stacks")
return StackHandler{
logger,
stackModel,
}
}

View File

@ -0,0 +1,35 @@
import { List } from "@network/index";
import { Component } from "solid-js";
import fastHashCode from "../../utils/hash";
import { A } from "@solidjs/router";
const colors = [
"bg-emerald-50",
"bg-lime-50",
"bg-indigo-50",
"bg-sky-50",
"bg-amber-50",
"bg-teal-50",
"bg-fuchsia-50",
"bg-pink-50",
];
export const ListCard: Component<{ list: List }> = (props) => {
return (
<A
href={`/list/${props.list.ID}`}
class={
"flex flex-col p-4 border border-neutral-200 rounded-lg " +
colors[
fastHashCode(props.list.Name, { forcePositive: true }) % colors.length
]
}
>
<p class="text-xl font-bold">{props.list.Name}</p>
<p class="text-lg">{props.list.Images.length}</p>
</A>
);
};

View File

@ -96,7 +96,16 @@ const userImageValidator = strictObject({
CreatedAt: pipe(string()),
ImageID: pipe(string(), uuid()),
UserID: pipe(string(), uuid()),
Image: imageMetaValidator,
Image: strictObject({
...imageMetaValidator.entries,
ImageLists: array(
strictObject({
ID: pipe(string(), uuid()),
ImageID: pipe(string(), uuid()),
ListID: pipe(string(), uuid()),
}),
),
}),
});
const userProcessingImageValidator = strictObject({

View File

@ -1,21 +1,6 @@
import { Component, For } from "solid-js";
import { A } from "@solidjs/router";
import { useSearchImageContext } from "@contexts/SearchImageContext";
import fastHashCode from "../../utils/hash";
const colors = [
"bg-emerald-50",
"bg-lime-50",
"bg-indigo-50",
"bg-sky-50",
"bg-amber-50",
"bg-teal-50",
"bg-fuchsia-50",
"bg-pink-50",
];
import { ListCard } from "@components/list-card";
export const Categories: Component = () => {
const { lists } = useSearchImageContext();
@ -24,23 +9,7 @@ export const Categories: Component = () => {
<div class="rounded-xl bg-white p-4 flex flex-col gap-2">
<h2 class="text-xl font-bold">Generated Lists</h2>
<div class="w-full grid grid-cols-3 auto-rows-[minmax(100px,1fr)] gap-4">
<For each={lists()}>
{(list) => (
<A
href={`/list/${list.ID}`}
class={
"flex flex-col p-4 border border-neutral-200 rounded-lg " +
colors[
fastHashCode(list.Name, { forcePositive: true }) %
colors.length
]
}
>
<p class="text-xl font-bold">{list.Name}</p>
<p class="text-lg">{list.Images.length}</p>
</A>
)}
</For>
<For each={lists()}>{(list) => <ListCard list={list} />}</For>
</div>
</div>
);

View File

@ -1,13 +1,15 @@
import { ImageComponent } from "@components/image";
import { useSearchImageContext } from "@contexts/SearchImageContext";
import { useParams } from "@solidjs/router";
import { type Component } from "solid-js";
import { For, type Component } from "solid-js";
import SolidjsMarkdown from "solidjs-markdown";
import { List } from "../list";
import { ListCard } from "@components/list-card";
export const ImagePage: Component = () => {
const { imageId } = useParams<{ imageId: string }>();
const { userImages } = useSearchImageContext();
const { userImages, lists } = useSearchImageContext();
const image = () => userImages().find((i) => i.ImageID === imageId);
@ -16,11 +18,22 @@ export const ImagePage: Component = () => {
<div class="w-full bg-white rounded-xl p-4">
<ImageComponent ID={imageId} />
</div>
<div>
<h2 class="font-bold text-xl">Description</h2>
<div class="w-full bg-white rounded-xl p-4 flex flex-col gap-4">
<h2 class="font-bold text-2xl">Description</h2>
<div class="grid grid-cols-3 gap-4">
<For each={image()?.Image.ImageLists}>
{(imageList) => (
<ListCard
list={lists().find((l) => l.ID === imageList.ListID)!}
/>
)}
</For>
</div>
</div>
<div class="w-full bg-white rounded-xl p-4">
<h2 class="font-bold text-2xl">Description</h2>
<SolidjsMarkdown>{image()?.Image.Description}</SolidjsMarkdown>
</div>
<div class="w-full grid grid-cols-3 gap-2 grid-flow-row-dense p-4 bg-white rounded-xl"></div>
</main>
);
};