4 Commits

8 changed files with 158 additions and 58 deletions

View File

@ -3,8 +3,10 @@ package agents
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"screenmark/screenmark/.gen/haystack/haystack/model" "screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents/client" "screenmark/screenmark/agents/client"
"screenmark/screenmark/limits"
"screenmark/screenmark/models" "screenmark/screenmark/models"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
@ -174,7 +176,7 @@ type addToListArguments struct {
Schema []models.IDValue 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{ agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: listPrompt, SystemPrompt: listPrompt,
JsonTools: listTools, JsonTools: listTools,
@ -193,6 +195,16 @@ func NewListAgent(log *log.Logger, listModel models.ListModel) client.AgentClien
return "", err 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() ctx := context.Background()
savedList, err := listModel.Save(ctx, info.UserId, args.Name, args.Desription, args.Schema) 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 return "", err
} }
log.Debug(savedList)
return savedList, nil return savedList, nil
}) })

View File

@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"os" "os"
"screenmark/screenmark/agents" "screenmark/screenmark/agents"
"screenmark/screenmark/limits"
"screenmark/screenmark/middleware" "screenmark/screenmark/middleware"
"screenmark/screenmark/models" "screenmark/screenmark/models"
"strconv" "strconv"
@ -86,6 +87,8 @@ func ListenNewImageEvents(db *sql.DB) {
imageModel := models.NewImageModel(db) imageModel := models.NewImageModel(db)
listModel := models.NewListModel(db) listModel := models.NewListModel(db)
limits := limits.CreateLimitsManager(db)
databaseEventLog := createLogger("Database Events 🤖", os.Stdout) databaseEventLog := createLogger("Database Events 🤖", os.Stdout)
databaseEventLog.SetLevel(log.DebugLevel) databaseEventLog.SetLevel(log.DebugLevel)
@ -116,7 +119,7 @@ func ListenNewImageEvents(db *sql.DB) {
} }
descriptionAgent := agents.NewDescriptionAgent(createLogger("Description 📝", splitWriter), imageModel) 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 var wg sync.WaitGroup
wg.Add(2) wg.Add(2)

View File

@ -111,7 +111,9 @@ $$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION notify_new_processing_image_status() CREATE OR REPLACE FUNCTION notify_new_processing_image_status()
RETURNS TRIGGER AS $$ RETURNS TRIGGER AS $$
BEGIN BEGIN
PERFORM pg_notify('new_processing_image_status', NEW.id::text || NEW.status::text); IF NEW.status <> 'not-started' THEN
PERFORM pg_notify('new_processing_image_status', NEW.id::text || NEW.status::text);
END IF;
RETURN NEW; RETURN NEW;
END END
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;

View File

@ -1,13 +1,13 @@
import { Navigate, Route, Router } from "@solidjs/router"; import { Navigate, Route, Router } from "@solidjs/router";
import { onAndroidMount } from "./mobile"; import { onAndroidMount } from "./mobile";
import { import {
FrontPage, FrontPage,
ImagePage, ImagePage,
Login, Login,
Settings, Settings,
SearchPage, SearchPage,
AllImages, AllImages,
List, List,
} from "./pages"; } from "./pages";
import { SearchImageContextProvider } from "@contexts/SearchImageContext"; import { SearchImageContextProvider } from "@contexts/SearchImageContext";
import { WithNotifications } from "@contexts/Notifications"; import { WithNotifications } from "@contexts/Notifications";
@ -15,32 +15,46 @@ import { ProtectedRoute } from "@components/protected-route";
import { AppWrapper } from "@components/app-wrapper"; import { AppWrapper } from "@components/app-wrapper";
import { WithTopbarAndDock } from "@components/app-wrapper/with-topbar-and-dock"; import { WithTopbarAndDock } from "@components/app-wrapper/with-topbar-and-dock";
import { onSendImage } from "@contexts/send-image"; import { onSendImage } from "@contexts/send-image";
import { Toast } from "@kobalte/core/toast";
import { Portal } from "solid-js/web";
export const App = () => { export const App = () => {
onAndroidMount(); onAndroidMount();
onSendImage(); onSendImage();
return ( return (
<SearchImageContextProvider> <SearchImageContextProvider>
<Router> <Router>
<Route path="/" component={AppWrapper}> <Route path="/" component={AppWrapper}>
<Route path="/login" component={Login} /> <Route path="/login" component={Login} />
<Route path="/" component={ProtectedRoute}> <Route path="/" component={ProtectedRoute}>
<Route path="/" component={WithNotifications}> <Route path="/" component={WithNotifications}>
<Route path="/" component={WithTopbarAndDock}> <Route path="/" component={WithTopbarAndDock}>
<Route path="/" component={FrontPage} /> <Route path="/" component={FrontPage} />
<Route path="/search" component={SearchPage} /> <Route path="/search" component={SearchPage} />
<Route path="/all-images" component={AllImages} /> <Route
<Route path="/image/:imageId" component={ImagePage} /> path="/all-images"
<Route path="/list/:listId" component={List} /> component={AllImages}
<Route path="/settings" component={Settings} /> />
</Route> <Route
</Route> path="/image/:imageId"
</Route> component={ImagePage}
</Route> />
<Route path="*" component={() => <Navigate href="/" />} /> <Route path="/list/:listId" component={List} />
</Router> <Route path="/settings" component={Settings} />
</SearchImageContextProvider> </Route>
); </Route>
</Route>
</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>
);
}; };

View File

@ -1,32 +1,42 @@
import { createEffect } from "solid-js"; import { createEffect } from "solid-js";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { getCurrentWindow } from "@tauri-apps/api/window"; 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 = () => { export const onSendImage = () => {
let sentImage = ""; let sentImage = "";
createEffect(async () => { createEffect(async () => {
// Listen for PNG processing events // Listen for PNG processing events
const unlisten = listen("png-processed", async (event) => { const unlisten = listen("png-processed", async (event) => {
const base64Data = event.payload as string; const base64Data = event.payload as string;
if (base64Data === sentImage) { if (base64Data === sentImage) {
return; return;
} }
sentImage = base64Data; sentImage = base64Data;
const appWindow = getCurrentWindow(); const appWindow = getCurrentWindow();
appWindow.show(); appWindow.show();
appWindow.setFocus(); appWindow.setFocus();
await sendImage("test-image.png", base64Data); 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 () => { return () => {
unlisten.then((fn) => fn()); // Cleanup listener unlisten.then((fn) => fn()); // Cleanup listener
}; };
}); });
}; };

View File

@ -68,6 +68,12 @@ export const sendImageFile = async (
return parse(sendImageResponseValidator, res); return parse(sendImageResponseValidator, res);
}; };
export class ImageLimitReached extends Error {
constructor() {
super();
}
}
export const sendImage = async ( export const sendImage = async (
imageName: string, imageName: string,
base64Image: string, base64Image: string,
@ -80,7 +86,12 @@ export const sendImage = async (
request.headers.set("Content-Type", "application/base64"); 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); return parse(sendImageResponseValidator, res);
}; };
@ -218,6 +229,12 @@ export const postCode = async (
return parse(codeValidator, res); return parse(codeValidator, res);
}; };
export class ReachedListLimit extends Error {
constructor() {
super();
}
}
export const createList = async ( export const createList = async (
title: string, title: string,
description: string, description: string,
@ -230,5 +247,8 @@ export const createList = async (
request.headers.set("Content-Type", "application/json"); request.headers.set("Content-Type", "application/json");
await fetch(request); const res = await fetch(request);
if (!res.ok && res.status == 429) {
throw new ReachedListLimit();
}
}; };

View File

@ -3,7 +3,8 @@ import { useSearchImageContext } from "@contexts/SearchImageContext";
import { ListCard } from "@components/list-card"; import { ListCard } from "@components/list-card";
import { Button } from "@kobalte/core/button"; import { Button } from "@kobalte/core/button";
import { Dialog } from "@kobalte/core/dialog"; 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 = () => { export const Categories: Component = () => {
const { lists, onRefetchImages } = useSearchImageContext(); const { lists, onRefetchImages } = useSearchImageContext();
@ -27,6 +28,9 @@ export const Categories: Component = () => {
onRefetchImages(); // Refresh the lists onRefetchImages(); // Refresh the lists
} catch (error) { } catch (error) {
console.error("Failed to create list:", error); console.error("Failed to create list:", error);
if (error instanceof ReachedListLimit) {
createToast("Reached limit!", "You've reached your limit for new lists");
}
} finally { } finally {
setIsCreating(false); setIsCreating(false);
} }

View 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>
));
};