1 Commits

Author SHA1 Message Date
b3ba450f63 feat: deleting column from frontend 2025-10-05 14:10:25 +01:00
9 changed files with 117 additions and 207 deletions

View File

@ -21,8 +21,6 @@ and add a good description for each one.
You can add fields if you think they make a lot of sense. You can add fields if you think they make a lot of sense.
You can remove fields if they are not correct, but be sure before you do this. You can remove fields if they are not correct, but be sure before you do this.
You must respond in json format
` `
const listJsonSchema = ` const listJsonSchema = `
@ -78,10 +76,10 @@ type createNewListArguments struct {
type CreateListAgent struct { type CreateListAgent struct {
client client.AgentClient client client.AgentClient
stackModel models.StackModel listModel models.StackModel
} }
func (agent *CreateListAgent) CreateList(log *log.Logger, userID uuid.UUID, stackID uuid.UUID, title string, userReq string) error { func (agent *CreateListAgent) CreateList(log *log.Logger, userID uuid.UUID, userReq string) error {
request := client.AgentRequestBody{ request := client.AgentRequestBody{
Model: "policy/images", Model: "policy/images",
Temperature: 0.3, Temperature: 0.3,
@ -95,10 +93,7 @@ func (agent *CreateListAgent) CreateList(log *log.Logger, userID uuid.UUID, stac
} }
request.Chat.AddSystem(agent.client.Options.SystemPrompt) request.Chat.AddSystem(agent.client.Options.SystemPrompt)
request.Chat.AddUser(userReq)
req := fmt.Sprintf("List title: %s | Users list description: %s", title, userReq)
request.Chat.AddUser(req)
resp, err := agent.client.Request(&request) resp, err := agent.client.Request(&request)
if err != nil { if err != nil {
@ -109,8 +104,6 @@ func (agent *CreateListAgent) CreateList(log *log.Logger, userID uuid.UUID, stac
structuredOutput := resp.Choices[0].Message.Content structuredOutput := resp.Choices[0].Message.Content
log.Info("", "res", structuredOutput)
var createListArgs createNewListArguments var createListArgs createNewListArguments
err = json.Unmarshal([]byte(structuredOutput), &createListArgs) err = json.Unmarshal([]byte(structuredOutput), &createListArgs)
if err != nil { if err != nil {
@ -120,8 +113,6 @@ func (agent *CreateListAgent) CreateList(log *log.Logger, userID uuid.UUID, stac
schemaItems := make([]model.SchemaItems, 0) schemaItems := make([]model.SchemaItems, 0)
for _, field := range createListArgs.Fields { for _, field := range createListArgs.Fields {
schemaItems = append(schemaItems, model.SchemaItems{ schemaItems = append(schemaItems, model.SchemaItems{
StackID: stackID,
Item: field.Name, Item: field.Name,
Description: field.Description, Description: field.Description,
@ -129,7 +120,12 @@ func (agent *CreateListAgent) CreateList(log *log.Logger, userID uuid.UUID, stac
}) })
} }
err = agent.stackModel.SaveItems(ctx, schemaItems) _, err = agent.listModel.Save(ctx, userID, createListArgs.Title, createListArgs.Description, model.Progress_Complete)
if err != nil {
return fmt.Errorf("creating list agent, saving list: %w", err)
}
err = agent.listModel.SaveItems(ctx, schemaItems)
if err != nil { if err != nil {
return fmt.Errorf("creating list agent, saving items: %w", err) return fmt.Errorf("creating list agent, saving items: %w", err)
} }

View File

@ -176,7 +176,7 @@ type addToListArguments struct {
Schema []models.IDValue Schema []models.IDValue
} }
func NewStackAgent(log *log.Logger, stackModel models.StackModel, limitsMethods limits.LimitsManagerMethods) client.AgentClient { func NewListAgent(log *log.Logger, stackModel models.StackModel, limitsMethods limits.LimitsManagerMethods) client.AgentClient {
agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{ agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: listPrompt, SystemPrompt: listPrompt,
JsonTools: listTools, JsonTools: listTools,

View File

@ -142,20 +142,6 @@ func (m StackModel) SaveSchemaItems(ctx context.Context, imageID uuid.UUID, item
return err return err
} }
// ========================================
// UPDATE methods
// ========================================
func (m StackModel) UpdateProcess(ctx context.Context, stackID uuid.UUID, process model.Progress) error {
updateStackProgressStmt := Stacks.UPDATE(Stacks.Status).
SET(process).
WHERE(Stacks.ID.EQ(UUID(stackID)))
_, err := updateStackProgressStmt.ExecContext(ctx, m.dbPool)
return err
}
// ======================================== // ========================================
// DELETE methods // DELETE methods
// ======================================== // ========================================

View File

@ -9,7 +9,7 @@ import (
const ( const (
IMAGE_TYPE = "image" IMAGE_TYPE = "image"
STACK_TYPE = "stack" LIST_TYPE = "list"
) )
type ImageNotification struct { type ImageNotification struct {
@ -21,18 +21,18 @@ type ImageNotification struct {
Status string Status string
} }
type StackNotification struct { type ListNotification struct {
Type string Type string
StackID uuid.UUID ListID uuid.UUID
Name string Name string
Status string Status string
} }
type Notification struct { type Notification struct {
image *ImageNotification image *ImageNotification
stack *StackNotification list *ListNotification
} }
func GetImageNotification(image ImageNotification) Notification { func GetImageNotification(image ImageNotification) Notification {
@ -41,9 +41,9 @@ func GetImageNotification(image ImageNotification) Notification {
} }
} }
func GetStackNotification(list StackNotification) Notification { func GetListNotification(list ListNotification) Notification {
return Notification{ return Notification{
stack: &list, list: &list,
} }
} }
@ -52,8 +52,8 @@ func (n Notification) MarshalJSON() ([]byte, error) {
return json.Marshal(n.image) return json.Marshal(n.image)
} }
if n.stack != nil { if n.list != nil {
return json.Marshal(n.stack) return json.Marshal(n.list)
} }
return nil, fmt.Errorf("no image or list present") return nil, fmt.Errorf("no image or list present")

View File

@ -23,8 +23,11 @@ type ImageProcessor struct {
descriptionAgent agents.DescriptionAgent descriptionAgent agents.DescriptionAgent
stackAgent client.AgentClient stackAgent client.AgentClient
// TODO: add the notifier here
Processor *Processor[model.Image] Processor *Processor[model.Image]
notifier *notifications.Notifier[notifications.Notification]
notifier *notifications.Notifier[notifications.Notification]
} }
func (p *ImageProcessor) setImageToProcess(ctx context.Context, image model.Image) { func (p *ImageProcessor) setImageToProcess(ctx context.Context, image model.Image) {
@ -125,7 +128,7 @@ func NewImageProcessor(
} }
descriptionAgent := agents.NewDescriptionAgent(logger, imageModel) descriptionAgent := agents.NewDescriptionAgent(logger, imageModel)
stackAgent := agents.NewStackAgent(logger, listModel, limitsManager) stackAgent := agents.NewListAgent(logger, listModel, limitsManager)
imageProcessor := ImageProcessor{ imageProcessor := ImageProcessor{
imageModel: imageModel, imageModel: imageModel,

View File

@ -1,127 +0,0 @@
package processor
import (
"context"
"fmt"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents"
"screenmark/screenmark/models"
"screenmark/screenmark/notifications"
"sync"
"github.com/charmbracelet/log"
)
const STACK_PROCESS_AT_A_TIME = 10
// TODO:
// This processor contains a lot of shared stuff.
// If we ever want to do more generic stuff with "in-progress" and stuff
// we can extract that into a common thing
//
// However, this will require a pretty big DB shuffle.
type StackProcessor struct {
stackModel models.StackModel
logger *log.Logger
stackAgent agents.CreateListAgent
Processor *Processor[model.Stacks]
notifier *notifications.Notifier[notifications.Notification]
}
func (p *StackProcessor) setStackToProcess(ctx context.Context, stack model.Stacks) {
err := p.stackModel.UpdateProcess(ctx, stack.ID, model.Progress_InProgress)
if err != nil {
// TODO: what can we actually do here for the errors?
// We can't stop the work for the others
p.logger.Error("failed to update stack", "err", err)
// TODO: we can use context here to actually pass some information through
return
}
}
func (p *StackProcessor) extractInfo(ctx context.Context, stack model.Stacks) {
err := p.stackAgent.CreateList(p.logger, stack.UserID, stack.ID, stack.Name, stack.Description)
if err != nil {
// Again, wtf do we do?
// Although i think the agent actually returns an error when it's finished
p.logger.Error("failed to process image", "err", err)
return
}
}
func (p *StackProcessor) processImage(stack model.Stacks) {
p.logger.Info("Processing image", "ID", stack.ID)
ctx := context.Background()
p.setStackToProcess(ctx, stack)
var wg sync.WaitGroup
// Future proofing!
wg.Add(1)
stackNotification := notifications.GetStackNotification(notifications.StackNotification{
Type: notifications.STACK_TYPE,
Status: string(model.Progress_InProgress),
StackID: stack.ID,
Name: stack.Name,
})
err := p.notifier.SendAndCreate(stack.UserID.String(), stackNotification)
if err != nil {
p.logger.Error("sending in progress notification", "err", err)
return
}
go func() {
p.extractInfo(ctx, stack)
wg.Done()
}()
wg.Wait()
// TODO: there is some repeated code here. The ergonomicts of the notifications,
// isn't the best.
stackNotification = notifications.GetStackNotification(notifications.StackNotification{
Type: notifications.STACK_TYPE,
Status: string(model.Progress_Complete),
StackID: stack.ID,
Name: stack.Name,
})
err = p.notifier.SendAndCreate(stack.UserID.String(), stackNotification)
if err != nil {
p.logger.Error("sending done notification", "err", err)
return
}
}
func NewStackProcessor(
logger *log.Logger,
stackModel models.StackModel,
notifier *notifications.Notifier[notifications.Notification],
) (StackProcessor, error) {
if notifier == nil {
return StackProcessor{}, fmt.Errorf("notifier is nil")
}
stackAgent := agents.NewCreateListAgent(logger, stackModel)
imageProcessor := StackProcessor{
logger: logger,
stackModel: stackModel,
stackAgent: stackAgent,
notifier: notifier,
}
imageProcessor.Processor = NewProcessor(int(IMAGE_PROCESS_AT_A_TIME), imageProcessor.processImage)
return imageProcessor, nil
}

View File

@ -41,16 +41,9 @@ func setupRouter(db *sql.DB, jwtManager *ourmiddleware.JwtManager) (chi.Router,
return nil, fmt.Errorf("processor: %w", err) return nil, fmt.Errorf("processor: %w", err)
} }
stackProcessorLog := createLogger("Stack Processor", os.Stdout)
stackProcessor, err := processor.NewStackProcessor(stackProcessorLog, stackModel, &notifier)
if err != nil {
return nil, fmt.Errorf("processor: %w", err)
}
go imageProcessor.Processor.Work() go imageProcessor.Processor.Work()
go stackProcessor.Processor.Work()
stackHandler := stacks.CreateStackHandler(db, limitsManager, jwtManager, stackProcessor.Processor) stackHandler := stacks.CreateStackHandler(db, limitsManager, jwtManager)
authHandler := auth.CreateAuthHandler(db, jwtManager) authHandler := auth.CreateAuthHandler(db, jwtManager)
imageHandler := images.CreateImageHandler(db, limitsManager, jwtManager, imageProcessor.Processor) imageHandler := images.CreateImageHandler(db, limitsManager, jwtManager, imageProcessor.Processor)

View File

@ -8,7 +8,6 @@ import (
"screenmark/screenmark/limits" "screenmark/screenmark/limits"
"screenmark/screenmark/middleware" "screenmark/screenmark/middleware"
"screenmark/screenmark/models" "screenmark/screenmark/models"
"screenmark/screenmark/processor"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
@ -24,8 +23,6 @@ type StackHandler struct {
limitsManager limits.LimitsManagerMethods limitsManager limits.LimitsManagerMethods
jwtManager *middleware.JwtManager jwtManager *middleware.JwtManager
processor *processor.Processor[model.Stacks]
} }
func (h *StackHandler) getAllStacks(w http.ResponseWriter, r *http.Request) { func (h *StackHandler) getAllStacks(w http.ResponseWriter, r *http.Request) {
@ -212,16 +209,14 @@ func (h *StackHandler) createStack(body CreateStackBody, w http.ResponseWriter,
} }
// TODO: Add the stack processor here // TODO: Add the stack processor here
stack, err := h.stackModel.Save(ctx, userID, body.Title, body.Description, model.Progress_NotStarted) _, err = h.stackModel.Save(ctx, userID, body.Title, body.Description, model.Progress_NotStarted)
if err != nil { if err != nil {
h.logger.Warn("could not save stack", "err", err) h.logger.Warn("could not save stack", "err", err)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
return return
} }
h.processor.Add(stack) w.WriteHeader(http.StatusOK)
middleware.WriteJsonOrError(h.logger, stack, w)
} }
func (h *StackHandler) CreateRoutes(r chi.Router) { func (h *StackHandler) CreateRoutes(r chi.Router) {
@ -242,12 +237,7 @@ func (h *StackHandler) CreateRoutes(r chi.Router) {
}) })
} }
func CreateStackHandler( func CreateStackHandler(db *sql.DB, limitsManager limits.LimitsManagerMethods, jwtManager *middleware.JwtManager) StackHandler {
db *sql.DB,
limitsManager limits.LimitsManagerMethods,
jwtManager *middleware.JwtManager,
processor *processor.Processor[model.Stacks],
) StackHandler {
stackModel := models.NewStackModel(db) stackModel := models.NewStackModel(db)
imageModel := models.NewImageModel(db) imageModel := models.NewImageModel(db)
logger := log.New(os.Stdout).WithPrefix("Stacks") logger := log.New(os.Stdout).WithPrefix("Stacks")
@ -258,6 +248,5 @@ func CreateStackHandler(
stackModel: stackModel, stackModel: stackModel,
limitsManager: limitsManager, limitsManager: limitsManager,
jwtManager: jwtManager, jwtManager: jwtManager,
processor: processor,
} }
} }

View File

@ -57,6 +57,59 @@ const DeleteButton: Component<{ onDelete: () => void }> = (props) => {
); );
}; };
const DeleteSchemaItemButton: Component<{
onDelete: () => void;
itemName: string;
}> = (props) => {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<button
aria-label="Delete schema item"
class="text-red-600 hover:text-red-700 ml-2"
onClick={(e) => {
e.stopPropagation();
setIsOpen(true);
}}
>
×
</button>
<Dialog.Root open={isOpen()} onOpenChange={setIsOpen}>
<Dialog.Portal>
<Dialog.Overlay class="fixed inset-0 bg-black bg-opacity-50" />
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
<Dialog.Content class="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
<Dialog.Title class="text-lg font-bold mb-2">
Delete Column
</Dialog.Title>
<Dialog.Description class="mb-4">
Are you sure you want to delete the column "
{props.itemName}"? This will remove this column
and all its data from the list.
</Dialog.Description>
<div class="flex justify-end gap-2">
<Dialog.CloseButton>
<button class="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400">
Cancel
</button>
</Dialog.CloseButton>
<button
class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
onClick={props.onDelete}
>
Delete Column
</button>
</div>
</Dialog.Content>
</div>
</Dialog.Portal>
</Dialog.Root>
</>
);
};
const DeleteListButton: Component<{ onDelete: () => void }> = (props) => { const DeleteListButton: Component<{ onDelete: () => void }> = (props) => {
const [isOpen, setIsOpen] = createSignal(false); const [isOpen, setIsOpen] = createSignal(false);
@ -107,17 +160,22 @@ export const List: Component = () => {
const { listId } = useParams(); const { listId } = useParams();
const nav = useNavigate(); const nav = useNavigate();
const { stacks, onDeleteImageFromStack } = useSearchImageContext(); const { stacks, onDeleteImageFromStack, onDeleteStackItem } =
useSearchImageContext();
const [accessToken] = createResource(getAccessToken); const [accessToken] = createResource(getAccessToken);
const list = () => stacks().find((l) => l.ID === listId); const list = () => stacks().find((l) => l.ID === listId);
const handleDeleteList = async () => { const handleDeleteList = async () => {
await deleteList(listId) await deleteList(listId);
nav("/"); nav("/");
}; };
const handleDeleteSchemaItem = (schemaItemId: string) => {
onDeleteStackItem(listId, schemaItemId);
};
return ( return (
<Suspense> <Suspense>
<Show when={list()} fallback="List could not be found"> <Show when={list()} fallback="List could not be found">
@ -151,15 +209,25 @@ export const List: Component = () => {
<For each={l().SchemaItems}> <For each={l().SchemaItems}>
{(item, index) => ( {(item, index) => (
<th <th
class={`px-6 py-4 text-left text-sm font-semibold text-neutral-900 min-w-32 ${index() < class={`px-6 py-4 text-left text-sm font-semibold text-neutral-900 min-w-32 ${
l().SchemaItems index() <
.length - l().SchemaItems.length -
1 1
? "border-r border-neutral-200" ? "border-r border-neutral-200"
: "" : ""
}`} }`}
> >
{item.Item} <div class="flex items-center">
{item.Item}
<DeleteSchemaItemButton
onDelete={() =>
handleDeleteSchemaItem(
item.ID,
)
}
itemName={item.Item}
/>
</div>
</th> </th>
)} )}
</For> </For>
@ -169,10 +237,11 @@ export const List: Component = () => {
<For each={l().Images}> <For each={l().Images}>
{(image, rowIndex) => ( {(image, rowIndex) => (
<tr <tr
class={`hover:bg-neutral-50 transition-colors ${rowIndex() % 2 === 0 class={`hover:bg-neutral-50 transition-colors ${
? "bg-white" rowIndex() % 2 === 0
: "bg-neutral-25" ? "bg-white"
}`} : "bg-neutral-25"
}`}
> >
<td class="px-6 py-4 border-r border-neutral-200"> <td class="px-6 py-4 border-r border-neutral-200">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@ -199,13 +268,14 @@ export const List: Component = () => {
<For each={image.Items}> <For each={image.Items}>
{(item, colIndex) => ( {(item, colIndex) => (
<td <td
class={`px-6 py-4 text-sm text-neutral-700 ${colIndex() < class={`px-6 py-4 text-sm text-neutral-700 ${
colIndex() <
image.Items image.Items
.length - .length -
1 1
? "border-r border-neutral-200" ? "border-r border-neutral-200"
: "" : ""
}`} }`}
> >
<div <div
class="max-w-xs truncate" class="max-w-xs truncate"