Compare commits
4 Commits
e45688d57e
...
95330c163b
Author | SHA1 | Date | |
---|---|---|---|
95330c163b | |||
84a0996be9 | |||
48579267b5 | |||
8b54d502f2 |
@ -3,8 +3,10 @@ package agents
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/limits"
|
||||
"screenmark/screenmark/models"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
@ -174,7 +176,7 @@ type addToListArguments struct {
|
||||
Schema []models.IDValue
|
||||
}
|
||||
|
||||
func NewListAgent(log *log.Logger, listModel models.ListModel) client.AgentClient {
|
||||
func NewListAgent(log *log.Logger, listModel models.ListModel, limitsMethods limits.LimitsManagerMethods) client.AgentClient {
|
||||
agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{
|
||||
SystemPrompt: listPrompt,
|
||||
JsonTools: listTools,
|
||||
@ -193,6 +195,16 @@ func NewListAgent(log *log.Logger, listModel models.ListModel) client.AgentClien
|
||||
return "", err
|
||||
}
|
||||
|
||||
hasReachedLimit, err := limitsMethods.HasReachedStackLimit(info.UserId)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error checking stack limits: %w", err)
|
||||
}
|
||||
|
||||
if hasReachedLimit {
|
||||
log.Warn("User has reached limits", "userID", info.UserId)
|
||||
return "", fmt.Errorf("reached stack limits")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
savedList, err := listModel.Save(ctx, info.UserId, args.Name, args.Desription, args.Schema)
|
||||
|
||||
@ -201,8 +213,6 @@ func NewListAgent(log *log.Logger, listModel models.ListModel) client.AgentClien
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Debug(savedList)
|
||||
|
||||
return savedList, nil
|
||||
})
|
||||
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"screenmark/screenmark/agents"
|
||||
"screenmark/screenmark/limits"
|
||||
"screenmark/screenmark/middleware"
|
||||
"screenmark/screenmark/models"
|
||||
"strconv"
|
||||
@ -86,6 +87,8 @@ func ListenNewImageEvents(db *sql.DB) {
|
||||
imageModel := models.NewImageModel(db)
|
||||
listModel := models.NewListModel(db)
|
||||
|
||||
limits := limits.CreateLimitsManager(db)
|
||||
|
||||
databaseEventLog := createLogger("Database Events 🤖", os.Stdout)
|
||||
databaseEventLog.SetLevel(log.DebugLevel)
|
||||
|
||||
@ -116,7 +119,7 @@ func ListenNewImageEvents(db *sql.DB) {
|
||||
}
|
||||
|
||||
descriptionAgent := agents.NewDescriptionAgent(createLogger("Description 📝", splitWriter), imageModel)
|
||||
listAgent := agents.NewListAgent(createLogger("Lists 🖋️", splitWriter), listModel)
|
||||
listAgent := agents.NewListAgent(createLogger("Lists 🖋️", splitWriter), listModel, limits)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
@ -111,7 +111,9 @@ $$ LANGUAGE plpgsql;
|
||||
CREATE OR REPLACE FUNCTION notify_new_processing_image_status()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.status <> 'not-started' THEN
|
||||
PERFORM pg_notify('new_processing_image_status', NEW.id::text || NEW.status::text);
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
@ -15,6 +15,8 @@ import { ProtectedRoute } from "@components/protected-route";
|
||||
import { AppWrapper } from "@components/app-wrapper";
|
||||
import { WithTopbarAndDock } from "@components/app-wrapper/with-topbar-and-dock";
|
||||
import { onSendImage } from "@contexts/send-image";
|
||||
import { Toast } from "@kobalte/core/toast";
|
||||
import { Portal } from "solid-js/web";
|
||||
|
||||
export const App = () => {
|
||||
onAndroidMount();
|
||||
@ -31,8 +33,14 @@ export const App = () => {
|
||||
<Route path="/" component={WithTopbarAndDock}>
|
||||
<Route path="/" component={FrontPage} />
|
||||
<Route path="/search" component={SearchPage} />
|
||||
<Route path="/all-images" component={AllImages} />
|
||||
<Route path="/image/:imageId" component={ImagePage} />
|
||||
<Route
|
||||
path="/all-images"
|
||||
component={AllImages}
|
||||
/>
|
||||
<Route
|
||||
path="/image/:imageId"
|
||||
component={ImagePage}
|
||||
/>
|
||||
<Route path="/list/:listId" component={List} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
</Route>
|
||||
@ -41,6 +49,12 @@ export const App = () => {
|
||||
</Route>
|
||||
<Route path="*" component={() => <Navigate href="/" />} />
|
||||
</Router>
|
||||
|
||||
<Portal>
|
||||
<Toast.Region class="fixed w-72 top-4 right-4 z-50 flex flex-col space-y-2">
|
||||
<Toast.List />
|
||||
</Toast.Region>
|
||||
</Portal>
|
||||
</SearchImageContextProvider>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { createEffect } from "solid-js";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { sendImage } from "@network/index";
|
||||
import { ImageLimitReached, sendImage } from "@network/index";
|
||||
import { createToast } from "../utils/show-toast";
|
||||
|
||||
export const onSendImage = () => {
|
||||
let sentImage = "";
|
||||
@ -22,7 +23,16 @@ export const onSendImage = () => {
|
||||
appWindow.show();
|
||||
appWindow.setFocus();
|
||||
|
||||
try {
|
||||
await sendImage("test-image.png", base64Data);
|
||||
} catch (e) {
|
||||
if (e instanceof ImageLimitReached) {
|
||||
createToast("Limits reached!", "You've reached your image limit")
|
||||
console.log("Reached image limits!");
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
|
@ -68,6 +68,12 @@ export const sendImageFile = async (
|
||||
return parse(sendImageResponseValidator, res);
|
||||
};
|
||||
|
||||
export class ImageLimitReached extends Error {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export const sendImage = async (
|
||||
imageName: string,
|
||||
base64Image: string,
|
||||
@ -80,7 +86,12 @@ export const sendImage = async (
|
||||
|
||||
request.headers.set("Content-Type", "application/base64");
|
||||
|
||||
const res = await fetch(request).then((res) => res.json());
|
||||
const rawRes = await fetch(request);
|
||||
if (!rawRes.ok && rawRes.status == 429) {
|
||||
throw new ImageLimitReached()
|
||||
}
|
||||
|
||||
const res = await rawRes.json();
|
||||
|
||||
return parse(sendImageResponseValidator, res);
|
||||
};
|
||||
@ -218,6 +229,12 @@ export const postCode = async (
|
||||
return parse(codeValidator, res);
|
||||
};
|
||||
|
||||
export class ReachedListLimit extends Error {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export const createList = async (
|
||||
title: string,
|
||||
description: string,
|
||||
@ -230,5 +247,8 @@ export const createList = async (
|
||||
|
||||
request.headers.set("Content-Type", "application/json");
|
||||
|
||||
await fetch(request);
|
||||
const res = await fetch(request);
|
||||
if (!res.ok && res.status == 429) {
|
||||
throw new ReachedListLimit();
|
||||
}
|
||||
};
|
||||
|
@ -3,7 +3,8 @@ import { useSearchImageContext } from "@contexts/SearchImageContext";
|
||||
import { ListCard } from "@components/list-card";
|
||||
import { Button } from "@kobalte/core/button";
|
||||
import { Dialog } from "@kobalte/core/dialog";
|
||||
import { createList } from "../../network";
|
||||
import { createList, ReachedListLimit } from "../../network";
|
||||
import { createToast } from "../../utils/show-toast";
|
||||
|
||||
export const Categories: Component = () => {
|
||||
const { lists, onRefetchImages } = useSearchImageContext();
|
||||
@ -27,6 +28,9 @@ export const Categories: Component = () => {
|
||||
onRefetchImages(); // Refresh the lists
|
||||
} catch (error) {
|
||||
console.error("Failed to create list:", error);
|
||||
if (error instanceof ReachedListLimit) {
|
||||
createToast("Reached limit!", "You've reached your limit for new lists");
|
||||
}
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
|
37
frontend/src/utils/show-toast.tsx
Normal file
37
frontend/src/utils/show-toast.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { Toast, toaster } from "@kobalte/core/toast";
|
||||
import { IconCircleDashedX } from "@tabler/icons-solidjs";
|
||||
|
||||
export const createToast = (title: string, text: string) => {
|
||||
console.log("creating toast")
|
||||
toaster.show((props) => (
|
||||
<Toast
|
||||
toastId={props.toastId}
|
||||
class="max-w-lg w-full bg-white shadow-lg rounded-lg pointer-events-auto flex ring-1 ring-black ring-opacity-5"
|
||||
>
|
||||
<div class="flex-1 w-0 p-4">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 pt-0.5">
|
||||
<IconCircleDashedX class="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<Toast.Title class="text-sm font-medium text-gray-900">
|
||||
{title}
|
||||
</Toast.Title>
|
||||
<Toast.Description class="mt-1 text-sm text-gray-500">
|
||||
{text}
|
||||
</Toast.Description>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex border-l border-gray-200">
|
||||
<Toast.CloseButton class="w-full border border-transparent rounded-none rounded-r-lg p-4 flex items-center justify-center text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
||||
<span class="sr-only">Close</span>
|
||||
<IconCircleDashedX class="h-5 w-5" aria-hidden="true" />
|
||||
</Toast.CloseButton>
|
||||
</div>
|
||||
<Toast.ProgressTrack class="absolute bottom-0 left-0 right-0 h-1 bg-gray-200 rounded-b-lg overflow-hidden">
|
||||
<Toast.ProgressFill class="h-full bg-indigo-600 transition-all duration-300" />
|
||||
</Toast.ProgressTrack>
|
||||
</Toast>
|
||||
));
|
||||
};
|
Reference in New Issue
Block a user