trustgraph/ai-context/workbench-ui/docs/tech-specs/collections.md

735 lines
20 KiB
Markdown
Raw Normal View History

# Collections Support for TrustGraph UI
## Overview
This document specifies the implementation of collections support in the TrustGraph UI. Collections provide a way to organize and manage groups of documents with metadata including name, description, and tags. The feature adds collection management capabilities to the Library page through a tabbed interface.
## API Integration
### Backend API Summary
The TrustGraph API provides three collection operations via the `collection-management` endpoint:
#### 1. List Collections
- **Operation**: `list-collections`
- **Request**:
```json
{
"operation": "list-collections",
"user": "username",
"tag_filter": ["tag1", "tag2"] // optional
}
```
- **Response**: Array of collection metadata objects
- **Fields**: user, collection, name, description, tags, created_at, updated_at
#### 2. Update Collection (also creates)
- **Operation**: `update-collection`
- **Request**:
```json
{
"operation": "update-collection",
"user": "username",
"collection": "collection-id",
"name": "Display Name", // optional
"description": "Description", // optional
"tags": ["tag1", "tag2"] // optional
}
```
- **Response**: Single collection metadata object in `collections` array
- **Note**: Creates collection if it doesn't exist; updates if it does
#### 3. Delete Collection
- **Operation**: `delete-collection`
- **Request**:
```json
{
"operation": "delete-collection",
"user": "username",
"collection": "collection-id"
}
```
- **Response**: Empty object `{}`
### Collection Metadata Structure
```typescript
interface CollectionMetadata {
user: string;
collection: string; // Collection ID (unique identifier)
name: string; // Display name
description: string; // Description text
tags: string[]; // Array of tags
created_at: string; // ISO timestamp
updated_at: string; // ISO timestamp
}
```
## Implementation Plan
### Phase 1: Socket Layer Integration
**File**: `src/api/trustgraph/trustgraph-socket.ts`
Add collection management methods to the socket interface:
```typescript
// Add to Socket interface
export interface Socket {
// ... existing methods ...
// Collection management
collectionManagement: () => CollectionManagement;
}
// New CollectionManagement interface
export interface CollectionManagement {
listCollections: (
user: string,
tagFilter?: string[]
) => Promise<CollectionMetadata[]>;
updateCollection: (
user: string,
collection: string,
name?: string,
description?: string,
tags?: string[]
) => Promise<CollectionMetadata>;
deleteCollection: (
user: string,
collection: string
) => Promise<void>;
}
// Implementation in BaseApi class
class BaseApi {
// ... existing methods ...
collectionManagement(): CollectionManagement {
return {
listCollections: async (user, tagFilter) => {
const request = {
operation: "list-collections",
user,
...(tagFilter && { tag_filter: tagFilter }),
};
const response = await this.request("collection-management", request);
return response.collections || [];
},
updateCollection: async (user, collection, name, description, tags) => {
const request = {
operation: "update-collection",
user,
collection,
...(name !== undefined && { name }),
...(description !== undefined && { description }),
...(tags !== undefined && { tags }),
};
const response = await this.request("collection-management", request);
return response.collections[0];
},
deleteCollection: async (user, collection) => {
const request = {
operation: "delete-collection",
user,
collection,
};
await this.request("collection-management", request);
},
};
}
}
```
### Phase 2: State Management Hook
**File**: `src/state/collections.ts` (new file)
Create a React Query-based state management hook following the library.ts pattern:
```typescript
import { useQueryClient, useQuery, useMutation } from "@tanstack/react-query";
import { useSocket, useConnectionState } from "../api/trustgraph/socket";
import { useNotification } from "./notify";
import { useActivity } from "./activity";
import { useSettings } from "./settings";
export interface CollectionMetadata {
user: string;
collection: string;
name: string;
description: string;
tags: string[];
created_at: string;
updated_at: string;
}
export const useCollections = () => {
const socket = useSocket();
const connectionState = useConnectionState();
const queryClient = useQueryClient();
const notify = useNotification();
const { settings } = useSettings();
const isSocketReady =
connectionState?.status === "authenticated" ||
connectionState?.status === "unauthenticated";
// Query for fetching all collections
const collectionsQuery = useQuery({
queryKey: ["collections", settings.user],
enabled: isSocketReady && !!settings.user,
queryFn: () => {
return socket.collectionManagement().listCollections(settings.user);
},
});
// Mutation for creating/updating a collection
const updateCollectionMutation = useMutation({
mutationFn: ({ collection, name, description, tags, onSuccess }) => {
return socket
.collectionManagement()
.updateCollection(
settings.user,
collection,
name,
description,
tags
)
.then(() => {
if (onSuccess) onSuccess();
});
},
onError: (err) => {
console.log("Error:", err);
notify.error(err.toString());
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["collections"] });
notify.success("Collection saved successfully");
},
});
// Mutation for deleting collections
const deleteCollectionsMutation = useMutation({
mutationFn: ({ collections, onSuccess }) => {
return Promise.all(
collections.map((collection) =>
socket
.collectionManagement()
.deleteCollection(settings.user, collection)
)
).then(() => {
if (onSuccess) onSuccess();
});
},
onError: (err) => {
console.log("Error:", err);
notify.error(err.toString());
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["collections"] });
notify.success("Collections deleted successfully");
},
});
// Activity indicators
useActivity(collectionsQuery.isLoading, "Loading collections");
useActivity(updateCollectionMutation.isPending, "Saving collection");
useActivity(deleteCollectionsMutation.isPending, "Deleting collections");
return {
// Collection data and query state
collections: collectionsQuery.data || [],
isLoading: collectionsQuery.isLoading,
isError: collectionsQuery.isError,
error: collectionsQuery.error,
// Update/create collection operations
updateCollection: updateCollectionMutation.mutate,
isUpdating: updateCollectionMutation.isPending,
updateError: updateCollectionMutation.error,
// Delete collection operations
deleteCollections: deleteCollectionsMutation.mutate,
isDeleting: deleteCollectionsMutation.isPending,
deleteError: deleteCollectionsMutation.error,
// Manual refetch
refetch: collectionsQuery.refetch,
};
};
```
### Phase 3: Data Model
**File**: `src/model/collection-table.tsx` (new file)
Define table columns for collections display:
```typescript
import { createColumnHelper } from "@tanstack/react-table";
import { Tag } from "@chakra-ui/react";
import { Checkbox } from "../components/ui/checkbox";
import { selectionState } from "../components/common/SelectableTable";
import { CollectionMetadata } from "../state/collections";
export const columnHelper = createColumnHelper<CollectionMetadata>();
export const columns = [
// Selection column
columnHelper.display({
id: "select",
header: ({ table }) => (
<Checkbox.Root
checked={selectionState(table)}
onChange={table.getToggleAllRowsSelectedHandler()}
>
<Checkbox.HiddenInput />
<Checkbox.Control />
</Checkbox.Root>
),
cell: ({ row }) => (
<Checkbox.Root
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
>
<Checkbox.HiddenInput />
<Checkbox.Control />
</Checkbox.Root>
),
}),
// Collection ID column
columnHelper.accessor("collection", {
header: "Collection ID",
cell: (info) => info.getValue(),
}),
// Name column
columnHelper.accessor("name", {
header: "Name",
cell: (info) => info.getValue(),
}),
// Description column
columnHelper.accessor("description", {
header: "Description",
cell: (info) => info.getValue(),
}),
// Tags column
columnHelper.accessor("tags", {
header: "Tags",
cell: (info) =>
info.getValue()?.map((tag) => (
<Tag.Root key={tag} mr={2} size="sm">
<Tag.Label>{tag}</Tag.Label>
</Tag.Root>
)),
}),
// Created column
columnHelper.accessor("created_at", {
header: "Created",
cell: (info) => new Date(info.getValue()).toLocaleString(),
}),
// Updated column
columnHelper.accessor("updated_at", {
header: "Updated",
cell: (info) => new Date(info.getValue()).toLocaleString(),
}),
];
```
### Phase 4: UI Components
#### 4.1 Collections Component
**File**: `src/components/library/Collections.tsx` (new file)
Main collections management component following the Documents.tsx pattern:
```typescript
import React, { useState } from "react";
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { columns } from "../../model/collection-table";
import { useCollections } from "../../state/collections";
import { useNotification } from "../../state/notify";
import CollectionActions from "./CollectionActions";
import CollectionDialog from "./CollectionDialog";
import SelectableTable from "../common/SelectableTable";
import CollectionControls from "./CollectionControls";
const Collections = () => {
const [dialogOpen, setDialogOpen] = useState(false);
const [editingCollection, setEditingCollection] = useState(null);
const notify = useNotification();
const collectionsState = useCollections();
const collections = collectionsState.collections || [];
const table = useReactTable({
data: collections,
columns: columns,
getCoreRowModel: getCoreRowModel(),
});
const selected = table.getSelectedRowModel().rows.map((x) => x.original.collection);
const onCreateNew = () => {
setEditingCollection(null);
setDialogOpen(true);
};
const onEdit = () => {
if (selected.length !== 1) {
notify.info("Please select exactly one collection to edit");
return;
}
const collection = collections.find(c => c.collection === selected[0]);
setEditingCollection(collection);
setDialogOpen(true);
};
const onDelete = () => {
collectionsState.deleteCollections({
collections: selected,
onSuccess: () => {
table.setRowSelection({});
},
});
};
const onSaveCollection = (collection, name, description, tags) => {
collectionsState.updateCollection({
collection,
name,
description,
tags,
onSuccess: () => {
setDialogOpen(false);
table.setRowSelection({});
},
});
};
return (
<>
<CollectionActions
selectedCount={selected.length}
onEdit={onEdit}
onDelete={onDelete}
/>
<CollectionDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
onSave={onSaveCollection}
editingCollection={editingCollection}
/>
<SelectableTable table={table} />
<CollectionControls onCreate={onCreateNew} />
</>
);
};
export default Collections;
```
#### 4.2 Collection Actions Bar
**File**: `src/components/library/CollectionActions.tsx` (new file)
Action buttons for bulk operations on selected collections:
```typescript
import React from "react";
import { HStack, Button, Text } from "@chakra-ui/react";
import { Edit, Trash2 } from "lucide-react";
interface CollectionActionsProps {
selectedCount: number;
onEdit: () => void;
onDelete: () => void;
}
const CollectionActions = ({ selectedCount, onEdit, onDelete }: CollectionActionsProps) => {
if (selectedCount === 0) return null;
return (
<HStack mb={4} p={4} bg="bg.muted" borderRadius="md">
<Text flex={1}>
{selectedCount} collection{selectedCount !== 1 ? "s" : ""} selected
</Text>
<Button onClick={onEdit} size="sm">
<Edit /> Edit
</Button>
<Button onClick={onDelete} colorPalette="red" size="sm">
<Trash2 /> Delete
</Button>
</HStack>
);
};
export default CollectionActions;
```
#### 4.3 Collection Dialog
**File**: `src/components/library/CollectionDialog.tsx` (new file)
Dialog for creating/editing collections:
```typescript
import React, { useState, useEffect } from "react";
import { Dialog } from "@chakra-ui/react";
import { Portal } from "@chakra-ui/react";
import TextField from "../common/TextField";
import TextAreaField from "../common/TextAreaField";
import ChipInputField from "../common/ChipInputField";
import ProgressSubmitButton from "../common/ProgressSubmitButton";
interface CollectionDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSave: (collection: string, name: string, description: string, tags: string[]) => void;
editingCollection?: any;
}
const CollectionDialog = ({
open,
onOpenChange,
onSave,
editingCollection,
}: CollectionDialogProps) => {
const [collection, setCollection] = useState("");
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [tags, setTags] = useState<string[]>([]);
useEffect(() => {
if (editingCollection) {
setCollection(editingCollection.collection);
setName(editingCollection.name);
setDescription(editingCollection.description);
setTags(editingCollection.tags || []);
} else {
setCollection("");
setName("");
setDescription("");
setTags([]);
}
}, [editingCollection, open]);
const handleSubmit = () => {
onSave(collection, name, description, tags);
};
const isValid = collection.trim() !== "" && name.trim() !== "";
return (
<Dialog.Root open={open} onOpenChange={(e) => onOpenChange(e.open)}>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>
{editingCollection ? "Edit Collection" : "Create Collection"}
</Dialog.Title>
</Dialog.Header>
<Dialog.Body>
<TextField
label="Collection ID"
value={collection}
onValueChange={setCollection}
disabled={!!editingCollection}
required
helperText={editingCollection ? "ID cannot be changed" : "Unique identifier for the collection"}
/>
<TextField
label="Name"
value={name}
onValueChange={setName}
required
helperText="Display name for the collection"
/>
<TextAreaField
label="Description"
value={description}
onValueChange={setDescription}
helperText="Brief description of the collection"
/>
<ChipInputField
label="Tags"
value={tags}
onValueChange={setTags}
helperText="Press Enter to add tags"
/>
</Dialog.Body>
<Dialog.Footer>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<ProgressSubmitButton onClick={handleSubmit} disabled={!isValid}>
{editingCollection ? "Update" : "Create"}
</ProgressSubmitButton>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
);
};
export default CollectionDialog;
```
#### 4.4 Collection Controls
**File**: `src/components/library/CollectionControls.tsx` (new file)
Control buttons for collection operations:
```typescript
import React from "react";
import { HStack, Button } from "@chakra-ui/react";
import { Plus } from "lucide-react";
interface CollectionControlsProps {
onCreate: () => void;
}
const CollectionControls = ({ onCreate }: CollectionControlsProps) => {
return (
<HStack mt={4} justify="flex-end">
<Button onClick={onCreate} colorPalette="primary">
<Plus /> Create Collection
</Button>
</HStack>
);
};
export default CollectionControls;
```
### Phase 5: Update Library Page with Tabs
**File**: `src/pages/LibraryPage.tsx`
Update the library page to use tabs for Documents and Collections:
```typescript
import React from "react";
import { LibraryBig } from "lucide-react";
import { Tabs } from "@chakra-ui/react";
import PageHeader from "../components/common/PageHeader";
import Documents from "../components/library/Documents";
import Collections from "../components/library/Collections";
const LibraryPage = () => {
return (
<>
<PageHeader
icon={<LibraryBig />}
title="Library"
description="Managing documents and collections"
/>
<Tabs.Root defaultValue="documents">
<Tabs.List>
<Tabs.Trigger value="documents">Documents</Tabs.Trigger>
<Tabs.Trigger value="collections">Collections</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="documents">
<Documents />
</Tabs.Content>
<Tabs.Content value="collections">
<Collections />
</Tabs.Content>
</Tabs.Root>
</>
);
};
export default LibraryPage;
```
## Type Definitions
**File**: `src/api/trustgraph/messages.ts`
Add collection-related message types:
```typescript
// Collection management request
export interface CollectionRequest extends RequestMessage {
operation: "list-collections" | "update-collection" | "delete-collection";
user: string;
collection?: string;
name?: string;
description?: string;
tags?: string[];
tag_filter?: string[];
}
// Collection management response
export interface CollectionResponse {
collections?: Array<{
user: string;
collection: string;
name: string;
description: string;
tags: string[];
created_at: string;
updated_at: string;
}>;
}
```
## Testing Checklist
- [ ] List collections displays all collections for the user
- [ ] Tag filter works when listing collections
- [ ] Create new collection with ID, name, description, and tags
- [ ] Edit existing collection (ID is disabled, other fields editable)
- [ ] Delete single collection
- [ ] Delete multiple collections
- [ ] Selection state persists correctly
- [ ] Loading indicators show during operations
- [ ] Error notifications display for failures
- [ ] Success notifications display for completed operations
- [ ] Tab switching preserves state
- [ ] Collections table sorts correctly
- [ ] Empty state displays when no collections exist
- [ ] Validation prevents creating collections with empty ID or name
## Future Enhancements
1. **Collection Assignment**: Allow assigning documents to collections from the Documents tab
2. **Collection Filtering**: Filter documents by collection
3. **Bulk Collection Operations**: Move multiple documents between collections
4. **Collection Statistics**: Show document count per collection
5. **Collection Search**: Search collections by name, description, or tags
6. **Collection Export**: Export collection metadata
## Migration Notes
- No breaking changes to existing functionality
- Documents tab functionality remains unchanged
- New collections functionality is additive
- Follows established patterns from library.ts and Documents.tsx
- Uses consistent Chakra v3 components and patterns