Merge commit 'a8390532f7' as 'ai-context/workbench-ui'

This commit is contained in:
elpresidank 2026-04-05 21:08:02 -05:00
commit 1a72bfdec0
310 changed files with 56430 additions and 0 deletions

View file

@ -0,0 +1,734 @@
# 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

View file

@ -0,0 +1,156 @@
# Flow Class Definition Specification
## Overview
A flow class defines a complete dataflow pattern template in the TrustGraph system. When instantiated, it creates an interconnected network of processors that handle data ingestion, processing, storage, and querying as a unified system.
## Structure
A flow class definition consists of four main sections:
### 1. Class Section
Defines shared service processors that are instantiated once per flow class. These processors handle requests from all flow instances of this class.
```json
"class": {
"service-name:{class}": {
"request": "queue-pattern:{class}",
"response": "queue-pattern:{class}"
}
}
```
**Characteristics:**
- Shared across all flow instances of the same class
- Typically expensive or stateless services (LLMs, embedding models)
- Use `{class}` template variable for queue naming
- Examples: `embeddings:{class}`, `text-completion:{class}`, `graph-rag:{class}`
### 2. Flow Section
Defines flow-specific processors that are instantiated for each individual flow instance. Each flow gets its own isolated set of these processors.
```json
"flow": {
"processor-name:{id}": {
"input": "queue-pattern:{id}",
"output": "queue-pattern:{id}"
}
}
```
**Characteristics:**
- Unique instance per flow
- Handle flow-specific data and state
- Use `{id}` template variable for queue naming
- Examples: `chunker:{id}`, `pdf-decoder:{id}`, `kg-extract-relationships:{id}`
### 3. Interfaces Section
Defines the entry points and interaction contracts for the flow. These form the API surface for external systems and internal component communication.
Interfaces can take two forms:
**Fire-and-Forget Pattern** (single queue):
```json
"interfaces": {
"document-load": "persistent://tg/flow/document-load:{id}",
"triples-store": "persistent://tg/flow/triples-store:{id}"
}
```
**Request/Response Pattern** (object with request/response fields):
```json
"interfaces": {
"embeddings": {
"request": "non-persistent://tg/request/embeddings:{class}",
"response": "non-persistent://tg/response/embeddings:{class}"
}
}
```
**Types of Interfaces:**
- **Entry Points**: Where external systems inject data (`document-load`, `agent`)
- **Service Interfaces**: Request/response patterns for services (`embeddings`, `text-completion`)
- **Data Interfaces**: Fire-and-forget data flow connection points (`triples-store`, `entity-contexts-load`)
### 4. Metadata
Additional information about the flow class:
```json
"description": "Human-readable description",
"tags": ["capability-1", "capability-2"]
```
## Template Variables
### {id}
- Replaced with the unique flow instance identifier
- Creates isolated resources for each flow
- Example: `flow-123`, `customer-A-flow`
### {class}
- Replaced with the flow class name
- Creates shared resources across flows of the same class
- Example: `standard-rag`, `enterprise-rag`
## Queue Patterns (Pulsar)
Flow classes use Apache Pulsar for messaging. Queue names follow the Pulsar format:
```
<persistence>://<tenant>/<namespace>/<topic>
```
### Components:
- **persistence**: `persistent` or `non-persistent` (Pulsar persistence mode)
- **tenant**: `tg` for TrustGraph-supplied flow class definitions
- **namespace**: Indicates the messaging pattern
- `flow`: Fire-and-forget services
- `request`: Request portion of request/response services
- `response`: Response portion of request/response services
- **topic**: The specific queue/topic name with template variables
### Persistent Queues
- Pattern: `persistent://tg/flow/<topic>:{id}`
- Used for fire-and-forget services and durable data flow
- Data persists in Pulsar storage across restarts
- Example: `persistent://tg/flow/chunk-load:{id}`
### Non-Persistent Queues
- Pattern: `non-persistent://tg/request/<topic>:{class}` or `non-persistent://tg/response/<topic>:{class}`
- Used for request/response messaging patterns
- Ephemeral, not persisted to disk by Pulsar
- Lower latency, suitable for RPC-style communication
- Example: `non-persistent://tg/request/embeddings:{class}`
## Dataflow Architecture
The flow class creates a unified dataflow where:
1. **Document Processing Pipeline**: Flows from ingestion through transformation to storage
2. **Query Services**: Integrated processors that query the same data stores and services
3. **Shared Services**: Centralized processors that all flows can utilize
4. **Storage Writers**: Persist processed data to appropriate stores
All processors (both `{id}` and `{class}`) work together as a cohesive dataflow graph, not as separate systems.
## Example Flow Instantiation
Given:
- Flow Instance ID: `customer-A-flow`
- Flow Class: `standard-rag`
Template expansions:
- `persistent://tg/flow/chunk-load:{id}``persistent://tg/flow/chunk-load:customer-A-flow`
- `non-persistent://tg/request/embeddings:{class}``non-persistent://tg/request/embeddings:standard-rag`
This creates:
- Isolated document processing pipeline for `customer-A-flow`
- Shared embedding service for all `standard-rag` flows
- Complete dataflow from document ingestion through querying
## Benefits
1. **Resource Efficiency**: Expensive services are shared across flows
2. **Flow Isolation**: Each flow has its own data processing pipeline
3. **Scalability**: Can instantiate multiple flows from the same template
4. **Modularity**: Clear separation between shared and flow-specific components
5. **Unified Architecture**: Query and processing are part of the same dataflow

View file

@ -0,0 +1,910 @@
# Flow Class Visual Editor Technical Specification
## Overview
A React-based visual editor for creating and modifying TrustGraph flow class definitions using a node-and-edge graph interface. Built with React Flow, this component allows users to visually design dataflow patterns by dragging processors onto a canvas and connecting them with queues.
## Core Technologies
- **React Flow** - Node-based editor framework
- **TypeScript** - Type safety for flow definitions
- **Chakra UI v3** - UI components and theming
- **Zustand** - Editor state management
- **Zod** - Schema validation for flow class structure
## Component Architecture
### Directory Structure
```
src/components/flow-editor/
├── FlowClassEditor.tsx # Main editor component
├── nodes/ # Custom node components
│ ├── ClassProcessorNode.tsx # Shared service nodes
│ ├── FlowProcessorNode.tsx # Flow-specific nodes
│ └── InterfaceNode.tsx # Entry/exit point nodes
├── edges/ # Custom edge components
│ ├── PersistentQueueEdge.tsx # Persistent queue connections
│ └── RequestResponseEdge.tsx # Request/response pairs
├── panels/ # Editor UI panels
│ ├── NodePalette.tsx # Drag-and-drop processor library
│ ├── PropertiesPanel.tsx # Node/edge configuration
│ └── ValidationPanel.tsx # Real-time validation feedback
├── hooks/
│ ├── useFlowValidation.ts # Validation logic
│ ├── useFlowExport.ts # JSON export/import
│ └── useAutoLayout.ts # Automatic graph layout
└── types/
└── flowEditorTypes.ts # TypeScript definitions
```
## Visual Design
### Node Types
#### 1. Class Processor Node ({class})
```tsx
{
type: 'classProcessor',
data: {
processorName: string, // e.g., "embeddings"
queues: {
request?: string, // Queue pattern
response?: string, // Queue pattern
[key: string]: string // Additional queues
}
},
style: {
background: 'accent.subtle', // Shared service color
border: '2px solid accent.solid',
icon: <Share2 /> // Lucide icon indicating shared
}
}
```
#### 2. Flow Processor Node ({id})
```tsx
{
type: 'flowProcessor',
data: {
processorName: string, // e.g., "chunker"
queues: {
input?: string, // Input queue
output?: string, // Output queue
[key: string]: string // Additional queues
}
},
style: {
background: 'primary.subtle', // Flow-specific color
border: '2px solid primary.solid',
icon: <Box /> // Lucide icon for isolated
}
}
```
#### 3. Interface Node
```tsx
{
type: 'interfaceNode',
data: {
interfaceName: string, // e.g., "document-load"
interfaceType: 'fire-and-forget' | 'request-response',
queuePattern?: string, // For fire-and-forget
request?: string, // For request-response
response?: string // For request-response
},
style: {
background: 'bg.muted',
border: '2px dashed border.muted',
icon: <Plug /> // Entry/exit point indicator
}
}
```
### Edge Types
#### 1. Persistent Queue Edge
- **Visual**: Solid line with arrow
- **Color**: Based on namespace (flow: green, request: blue, response: purple)
- **Label**: Queue name displayed on hover
- **Validation**: Source/target compatibility checking
#### 2. Non-Persistent Queue Edge
- **Visual**: Dashed line with arrow
- **Color**: Lighter variant of namespace colors
- **Label**: Queue name with non-persistent indicator
- **Validation**: Request/response pairing validation
## User Edit Operations
### 1. Processor Management
#### Add Processor
- **Drag from palette**: Drag processor type from categorized library
- **Double-click canvas**: Quick-add with processor type selector
- **Context menu**: Right-click → Add Processor → Select type
- **Keyboard shortcut**: `A` key opens add processor dialog
#### Configure Processor
- **Rename**: Click processor name to edit inline
- **Change type**: Toggle between `{class}` and `{id}` via properties panel
- **Queue management**:
```tsx
// Add new queue to processor
addQueue(processorId, {
name: "custom-queue",
direction: "input" | "output" | "bidirectional",
pattern: "persistent://tg/flow/custom:{id}"
});
// Remove queue
removeQueue(processorId, queueName);
// Edit queue pattern
updateQueue(processorId, queueName, newPattern);
```
#### Delete Processor
- **Single**: Select + Delete key
- **Multiple**: Multi-select + Delete key
- **Context menu**: Right-click → Delete
- **Validation**: Warn if processor has connections
### 2. Connection Management
#### Create Connection
- **Drag connection**: From output handle to input handle
- **Validation rules**:
- Persistence compatibility (persistent ↔ persistent preferred)
- Namespace compatibility (flow/request/response)
- Template variable consistency ({class} ↔ {class}, {id} ↔ {id})
- No self-connections
- No duplicate connections
#### Configure Connection
- **Auto-naming**: Generate queue name from source/target processors
- **Custom naming**: Override auto-generated queue name
- **Persistence mode**: Toggle persistent/non-persistent
- **Queue pattern template**:
```tsx
generateQueuePattern({
persistence: "persistent" | "non-persistent",
tenant: "tg",
namespace: "flow" | "request" | "response",
topic: "document-embeddings",
template: "{id}" | "{class}"
});
// Result: "persistent://tg/flow/document-embeddings:{id}"
```
#### Delete Connection
- **Click to select** + Delete key
- **Context menu** on edge
- **Disconnect handle**: Drag connection away from handle
### 3. Interface Operations
#### Add Interface
- **Entry points**: Document load, text input, etc.
- **Exit points**: Response outputs, storage endpoints
- **Service interfaces**: Request/response pairs
#### Configure Interface Type
```tsx
// Fire-and-forget pattern
interface FireAndForgetInterface {
type: "fire-and-forget";
queue: string; // Single queue pattern
}
// Request/response pattern
interface RequestResponseInterface {
type: "request-response";
request: string; // Request queue pattern
response: string; // Response queue pattern
}
```
### 4. Bulk Operations
#### Multi-select Actions
- **Box select**: Click and drag to select multiple nodes
- **Shift-click**: Add to selection
- **Cmd-click**: Toggle selection
- **Select all**: Cmd+A
#### Group Operations
- **Move together**: Drag any selected node moves all
- **Delete together**: Delete key removes all selected
- **Duplicate**: Cmd+D duplicates selection
- **Copy/paste**: Cmd+C/Cmd+V for cross-flow copying
### 5. Layout Operations
#### Auto-layout
```tsx
const layoutStrategies = {
hierarchical: {
direction: "LR" | "TB", // Left-right or top-bottom
nodeSpacing: 150,
levelSpacing: 200
},
force: {
strength: -1000,
distance: 150
},
circular: {
radius: 300,
startAngle: 0
}
};
```
#### Manual Arrangement
- **Snap to grid**: Optional grid snapping (toggle with G key)
- **Alignment tools**: Align selected nodes (top/bottom/left/right/center)
- **Distribution**: Distribute nodes evenly (horizontal/vertical)
### 6. Template Operations
#### Apply Template
```tsx
const templates = {
"document-rag": {
description: "Document processing with RAG",
processors: [
{ type: "pdf-decoder", id: "{id}" },
{ type: "chunker", id: "{id}" },
{ type: "embeddings", id: "{class}" },
{ type: "de-write", id: "{id}" }
],
connections: [
{ from: "pdf-decoder.output", to: "chunker.input" },
{ from: "chunker.output", to: "embeddings.input" },
{ from: "embeddings.output", to: "de-write.input" }
]
}
};
```
#### Create Template
- Select nodes/edges → Right-click → "Save as template"
- Provide template name and description
- Template saved to library for reuse
### 7. Validation Operations
#### Real-time Validation
```tsx
interface ValidationRule {
id: string;
severity: "error" | "warning" | "info";
check: (flowClass: FlowClass) => ValidationResult;
}
const validationRules = [
{
id: "no-orphans",
severity: "warning",
check: (flow) => findOrphanedNodes(flow)
},
{
id: "queue-consistency",
severity: "error",
check: (flow) => validateQueuePatterns(flow)
},
{
id: "template-consistency",
severity: "error",
check: (flow) => validateTemplateVariables(flow)
}
];
```
#### Fix Suggestions
- **Auto-fix**: One-click fixes for common issues
- **Quick actions**: Context-aware suggestions
- **Validation overlay**: Visual indicators on invalid elements
### 8. History Management
#### Undo/Redo Stack
```tsx
interface HistoryAction {
type: "add" | "delete" | "update" | "connect" | "disconnect";
before: FlowState;
after: FlowState;
timestamp: number;
}
const historyStack: HistoryAction[] = [];
const redoStack: HistoryAction[] = [];
// Track all operations
const executeOperation = (operation: Operation) => {
const before = getCurrentState();
performOperation(operation);
const after = getCurrentState();
historyStack.push({
type: operation.type,
before,
after,
timestamp: Date.now()
});
redoStack.length = 0; // Clear redo on new operation
};
```
### 9. Import/Export Operations
#### Import Flow Class
- **From JSON file**: Upload or paste JSON
- **From Config API**: Select from existing flow classes
- **Validation**: Verify structure before import
- **Merge options**: Replace or merge with existing
#### Export Flow Class
- **To JSON**: Download as .json file
- **To Config API**: Save directly to backend
- **To clipboard**: Copy JSON for sharing
- **Format options**: Minified or pretty-printed
### 10. Metadata Operations
#### Edit Flow Properties
```tsx
interface FlowMetadata {
id: string; // Kebab-case identifier
name: string; // Human-readable name
description: string; // Detailed description
tags: string[]; // Categorization tags
version: string; // Semantic version
author: string; // Creator identity
created: Date; // Creation timestamp
modified: Date; // Last modification
}
```
#### Tag Management
- **Add tags**: Type or select from existing
- **Remove tags**: Click X on tag chips
- **Tag suggestions**: Based on processors used
- **Tag categories**: System tags vs user tags
## Core Features
### 1. Auto-Layout
```tsx
const handleAutoLayout = () => {
const layoutedElements = getLayoutedElements(nodes, edges, {
direction: 'LR', // Left to right
nodeSpacing: 150,
levelSpacing: 200,
animate: true
});
setNodes(layoutedElements.nodes);
setEdges(layoutedElements.edges);
};
```
### 2. Import/Export
```tsx
// Export to flow class JSON
const exportFlowClass = () => {
const flowClass = {
class: extractClassProcessors(nodes),
flow: extractFlowProcessors(nodes),
interfaces: extractInterfaces(nodes),
description: metadata.description,
tags: metadata.tags
};
return JSON.stringify(flowClass, null, 2);
};
// Import from JSON
const importFlowClass = (json: string) => {
const flowClass = JSON.parse(json);
const { nodes, edges } = convertToReactFlow(flowClass);
setNodes(nodes);
setEdges(edges);
};
```
### 3. Connection Validation
```tsx
const isValidConnection = (connection: Connection) => {
const sourceNode = getNode(connection.source);
const targetNode = getNode(connection.target);
// Validate queue compatibility
if (!areQueuesCompatible(sourceNode, targetNode)) {
return false;
}
// Prevent circular dependencies
if (createsCircularDependency(connection)) {
return false;
}
return true;
};
```
### 4. Smart Templates
Pre-built flow patterns users can instantiate:
- **Document RAG Pipeline**: PDF → Chunker → Embeddings → Storage
- **Graph RAG Pipeline**: Knowledge extraction → Graph embeddings → Query
- **Simple Q&A**: Prompt → LLM → Response
- **Custom Template**: User-defined reusable patterns
## State Management
```tsx
interface FlowEditorState {
// React Flow state
nodes: Node[];
edges: Edge[];
// Editor state
selectedElement: Node | Edge | null;
validationErrors: ValidationError[];
isDirty: boolean;
// Metadata
flowClassName: string;
description: string;
tags: string[];
// Actions
addNode: (type: NodeType, position: XYPosition) => void;
updateNode: (nodeId: string, data: NodeData) => void;
deleteNode: (nodeId: string) => void;
addEdge: (edge: Edge) => void;
deleteEdge: (edgeId: string) => void;
validateFlow: () => ValidationResult;
exportFlow: () => FlowClassDefinition;
importFlow: (definition: FlowClassDefinition) => void;
}
```
## Visual Indicators
### Node States
- **Normal**: Default appearance
- **Selected**: Blue glow/border
- **Invalid**: Red border with error icon
- **Connecting**: Pulse animation on handles
- **Hover**: Slight scale increase
### Edge States
- **Normal**: Default appearance
- **Selected**: Highlighted with thicker stroke
- **Invalid**: Red dashed line
- **Animated**: Flow animation for active connections
### Queue Handle Types
- **Input**: Left side of node, inward arrow
- **Output**: Right side of node, outward arrow
- **Bidirectional**: Both sides, for request/response
## Keyboard Shortcuts
- `Delete` - Delete selected elements
- `Cmd+Z` - Undo
- `Cmd+Shift+Z` - Redo
- `Cmd+S` - Save flow class
- `Cmd+O` - Open flow class
- `Cmd+E` - Export to JSON
- `Space` - Pan mode
- `Cmd+A` - Select all nodes
- `Cmd+D` - Duplicate selected nodes
## Integration Points
### Config API Integration
The flow class editor uses the existing Config API for all flow class operations:
#### State Management Hook
```tsx
// src/state/flow-classes.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useSocket } from "./socket";
export const useFlowClasses = () => {
const socket = useSocket();
return useQuery({
queryKey: ["flow-classes"],
queryFn: async () => {
const response = await socket.request({
operation: "get-config",
path: "flow-classes"
});
return response.configuration as FlowClassDefinition[];
}
});
};
export const useFlowClass = (flowClassId: string) => {
const socket = useSocket();
return useQuery({
queryKey: ["flow-class", flowClassId],
queryFn: async () => {
const response = await socket.request({
operation: "get-config",
path: `flow-classes/${flowClassId}`
});
return response.configuration as FlowClassDefinition;
}
});
};
export const useUpdateFlowClass = () => {
const socket = useSocket();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, flowClass }: {
id: string;
flowClass: FlowClassDefinition
}) => {
return await socket.request({
operation: "set-config",
path: `flow-classes/${id}`,
configuration: flowClass
});
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries(["flow-class", variables.id]);
queryClient.invalidateQueries(["flow-classes"]);
}
});
};
export const useDeleteFlowClass = () => {
const socket = useSocket();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
return await socket.request({
operation: "delete-config",
path: `flow-classes/${id}`
});
},
onSuccess: () => {
queryClient.invalidateQueries(["flow-classes"]);
}
});
};
```
#### Editor Component Integration
```tsx
// src/components/flow-editor/FlowClassEditor.tsx
import { useFlowClass, useUpdateFlowClass } from "../../state/flow-classes";
import { useActivity } from "../../state/activity";
import { useNotification } from "../../state/notify";
export const FlowClassEditor = ({ flowClassId }: { flowClassId?: string }) => {
const notify = useNotification();
// Load existing flow class if ID provided
const { data: flowClass, isLoading } = useFlowClass(flowClassId);
const updateMutation = useUpdateFlowClass();
// Track loading state
useActivity(isLoading, "Loading flow class");
useActivity(updateMutation.isPending, "Saving flow class");
// Initialize React Flow with loaded data
useEffect(() => {
if (flowClass) {
const { nodes, edges } = convertFlowClassToReactFlow(flowClass);
setNodes(nodes);
setEdges(edges);
setMetadata({
description: flowClass.description,
tags: flowClass.tags
});
}
}, [flowClass]);
// Save handler
const handleSave = async () => {
const flowClassData = exportFlowClass();
try {
await updateMutation.mutateAsync({
id: flowClassId || generateFlowClassId(),
flowClass: flowClassData
});
notify.success("Flow class saved successfully");
} catch (error) {
notify.error("Failed to save flow class");
}
};
return (
<ReactFlow
nodes={nodes}
edges={edges}
// ... rest of React Flow config
/>
);
};
```
#### Flow Class List Integration
```tsx
// src/components/flow-editor/FlowClassList.tsx
import { useFlowClasses, useDeleteFlowClass } from "../../state/flow-classes";
export const FlowClassList = () => {
const { data: flowClasses, isLoading } = useFlowClasses();
const deleteMutation = useDeleteFlowClass();
useActivity(isLoading, "Loading flow classes");
useActivity(deleteMutation.isPending, "Deleting flow class");
return (
<VStack>
{flowClasses?.map(flowClass => (
<Card key={flowClass.id}>
<HStack>
<Text>{flowClass.description}</Text>
<Button onClick={() => openEditor(flowClass.id)}>
Edit
</Button>
<Button onClick={() => deleteMutation.mutate(flowClass.id)}>
Delete
</Button>
</HStack>
</Card>
))}
</VStack>
);
};
```
#### Config API Request/Response Format
```typescript
// Request to get all flow classes
{
operation: "get-config",
path: "flow-classes"
}
// Response
{
configuration: [
{
id: "document-rag-flow",
class: { /* class processors */ },
flow: { /* flow processors */ },
interfaces: { /* interfaces */ },
description: "Document RAG pipeline",
tags: ["rag", "documents"]
}
]
}
// Request to update flow class
{
operation: "set-config",
path: "flow-classes/document-rag-flow",
configuration: {
class: { /* updated class processors */ },
flow: { /* updated flow processors */ },
interfaces: { /* updated interfaces */ },
description: "Updated description",
tags: ["rag", "documents", "v2"]
}
}
```
### Real-time Updates via WebSocket
The editor subscribes to configuration changes to handle external updates:
```tsx
useEffect(() => {
const subscription = socket.subscribe(
`config/flow-classes/${flowClassId}`,
(update) => {
// Handle external updates to the flow class
if (update.source !== currentSessionId) {
notify.warning("Flow class updated externally. Refreshing...");
queryClient.invalidateQueries(["flow-class", flowClassId]);
}
}
);
return () => subscription.unsubscribe();
}, [flowClassId]);
```
### With Existing UI
#### Page Integration
The Flow Class Editor is a separate page in the workbench, controlled by a feature toggle:
```tsx
// src/components/settings/FeatureSwitchesSection.tsx
// Add to existing feature switches:
<HStack justify="space-between" align="center">
<VStack gap={1} align="start">
<Text fontWeight="medium">Flow Class Editor</Text>
<HStack gap={2} align="center">
<Text fontSize="sm" color="fg.muted">
Enable the visual flow class editor for creating and modifying dataflow patterns
</Text>
<Tag.Root colorPalette="accent" size="sm">
<Tag.Label>experimental</Tag.Label>
</Tag.Root>
</HStack>
</VStack>
<Switch.Root
checked={flowClassEditor}
onCheckedChange={(details) =>
onFlowClassEditorChange(details.checked)
}
>
<Switch.HiddenInput />
<Switch.Control>
<Switch.Thumb />
</Switch.Control>
</Switch.Root>
</HStack>
```
#### Sidebar Navigation
```tsx
// src/components/Sidebar.tsx
// Add conditional menu item based on feature switch:
{settings.featureSwitches.flowClassEditor && (
<SidebarItem
icon={<GitBranch />}
label="Flow Class Editor"
path="/flow-class-editor"
isActive={location.pathname === "/flow-class-editor"}
/>
)}
```
#### Route Configuration
```tsx
// src/App.tsx
// Add route for the editor page:
{settings.featureSwitches.flowClassEditor && (
<Route path="/flow-class-editor" element={<FlowClassEditorPage />} />
)}
```
#### Page Component
```tsx
// src/pages/FlowClassEditorPage.tsx
import React from "react";
import PageHeader from "../components/common/PageHeader";
import FlowClassEditor from "../components/flow-editor/FlowClassEditor";
import { GitBranch } from "lucide-react";
const FlowClassEditorPage: React.FC = () => {
return (
<>
<PageHeader
icon={<GitBranch />}
title="Flow Class Editor"
description="Visual editor for creating and modifying TrustGraph dataflow patterns"
/>
<FlowClassEditor />
</>
);
};
export default FlowClassEditorPage;
```
#### Settings State Update
```tsx
// src/state/settings.ts
interface FeatureSwitches {
ontologyEditor: boolean;
submissions: boolean;
agentTools: boolean;
mcpTools: boolean;
schemas: boolean;
tokenCost: boolean;
flowClasses: boolean; // Existing flow classes management
flowClassEditor: boolean; // New visual editor
structuredQuery: boolean;
}
```
#### Integration Features
- Uses consistent Chakra UI theming
- Integrates with notification system via `useNotification`
- Progress indicators via `useActivity`
- Follows existing Config API patterns
- Respects user's feature toggle preferences
## Responsive Design
### Desktop (Primary)
- Full editor with all panels visible
- Optimal canvas size for complex flows
- Properties panel as sidebar
### Tablet
- Collapsible panels to maximize canvas
- Touch-friendly node manipulation
- Simplified toolbar
### Mobile (View-only)
- Read-only flow visualization
- Pan and zoom only
- Export functionality retained
## Performance Considerations
### Optimizations
- **Virtualization** for large flows (100+ nodes)
- **Debounced validation** during editing
- **Memoized node/edge components**
- **Lazy loading** of processor templates
- **Web Workers** for layout calculations
### Limits
- Max 500 nodes per flow class
- Max 1000 edges per flow class
- Auto-layout for flows under 100 nodes
- Real-time validation for flows under 50 nodes
## Error Handling
### Validation Errors
- Inline error indicators on invalid nodes/edges
- Validation panel with detailed error list
- Prevent export/save when errors exist
### Runtime Errors
- Connection rejection with toast notification
- Import failure with detailed parsing errors
- Auto-save recovery for browser crashes
## Future Enhancements
### Phase 2
- **Processor library management** - Add custom processors
- **Collaborative editing** - Real-time multi-user support
- **Version control** - Flow class versioning and diff view
- **Simulation mode** - Visualize data flow through the graph
### Phase 3
- **AI assistance** - Suggest connections and optimizations
- **Performance profiling** - Visualize bottlenecks
- **Template marketplace** - Share flow patterns
- **Code generation** - Generate processor stubs from flow
## Testing Strategy
### Unit Tests
- Node/edge component rendering
- Validation logic
- Import/export transformations
- State management actions
### Integration Tests
- Full editor workflow
- Save/load operations
- Template instantiation
- Keyboard shortcuts
### E2E Tests
- Create flow from scratch
- Import and modify existing flow
- Export and validate JSON
- Deploy flow instance

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,373 @@
# Gateway Authentication for TrustGraph UI
## Overview
This document specifies the implementation of gateway authentication for the TrustGraph UI application. The gateway authentication system will provide secure communication between the UI and the TrustGraph backend services through an authentication token mechanism.
## Requirements
The gateway authentication system should provide:
1. **Authentication Modes**
- **Unauthenticated Mode**: When no API key is entered (empty string), the system operates without authentication
- **Authenticated Mode**: When an API key is specified in settings, all communications include the authentication token
2. **Token Management**
- Secure storage and retrieval of authentication credentials via settings system
- Authentication is determined by presence/absence of API key
- Token persists across sessions (stored in localStorage via settings)
- Token should be masked in UI displays
3. **Integration Points**
- **WebSocket Connections**: Append `?token=<apiToken>` to WebSocket connection URL
- **REST API Calls**: Include token as Bearer token in Authorization header
- Settings page for token configuration
- Error handling for 401/403 responses
## Implementation Details
### Settings Integration
**Authentication Settings** (already exists in settings-types.ts):
```typescript
authentication: {
apiKey: string; // Gateway authentication token/secret
}
```
### Socket Layer Integration
**WebSocket Authentication**:
```typescript
// In createTrustGraphSocket or SocketProvider
const wsUrl = settings.authentication.apiKey
? `/api/socket?token=${settings.authentication.apiKey}`
: `/api/socket`;
```
**REST API Authentication**:
```typescript
// For any REST endpoints (if used)
const headers = settings.authentication.apiKey
? { 'Authorization': `Bearer ${settings.authentication.apiKey}` }
: {};
```
**Current State**:
- `useSocket()` hook now has access to settings via `useSettings()`
- Socket context created once at app initialization
- Settings changes require socket reconnection for auth updates
**Required Changes**:
1. Modify `createTrustGraphSocket` to accept optional token parameter
2. Update socket initialization to append token to WebSocket URL when present
3. Add Bearer token to any REST API calls (if applicable)
4. Handle authentication errors (401/403) gracefully
5. Consider socket reconnection when authentication settings change
### User Interface
**Settings Page**:
- Password input field for gateway secret
- Show/hide toggle for secret visibility
- Clear button to remove authentication
- Save confirmation with success/error feedback
**Authentication Status**:
- Optional status indicator in header/sidebar
- Error notifications for auth failures
- Redirect to settings on 401/403 errors
## Socket Initialization Timing
### Critical Requirements
1. **The socket MUST NOT be created until settings have been loaded from localStorage/backend**. Creating the socket too early will result in incorrect authentication state.
2. **The socket MUST reconnect when the API key changes**. This ensures authentication state stays synchronized with user settings.
### Initialization Scenarios
1. **Scenario 1: Token Already Configured**
- User has previously saved an API key in settings
- Settings load from localStorage → contains `apiKey: "token123"`
- Socket creation MUST wait for settings load
- Socket connects with `?token=token123` appended to URL
- **Risk if socket created early**: Connects without auth, requires reconnection
2. **Scenario 2: Explicitly Unauthenticated**
- User has explicitly chosen no authentication (saved empty token)
- Settings load from localStorage → contains `apiKey: ""`
- Socket creation MUST wait for settings load
- Socket connects WITHOUT token parameter
- **Risk if socket created early**: Might use stale token from previous session
3. **Scenario 3: First-Time User / No Settings**
- No settings have been saved yet
- Settings system returns defaults (empty apiKey)
- **Options**:
a. Wait for settings to initialize with defaults, then create socket (safest)
b. Create socket immediately without auth (assumes unauthenticated default)
c. Show setup wizard requiring auth decision before socket creation
- **Recommendation**: Option (a) - always wait for settings initialization
### Socket Reconnection Requirements
When the API key changes (user updates settings), the socket must:
1. **Detect the Change**
- Monitor `settings.authentication.apiKey` for changes
- Triggered when user saves new API key in settings
- Also triggered when user clears API key (switches to unauthenticated)
2. **Clean Disconnect**
- Close existing WebSocket connection gracefully
- Cancel any pending requests/subscriptions
- Clear any auth-related state
3. **Reconnect with New Auth**
- Create new socket with updated token (or no token)
- Re-establish WebSocket connection
- Show brief loading/reconnecting state to user
4. **Handle Edge Cases**
- API key changes from `""` to `"token123"` (unauthenticated → authenticated)
- API key changes from `"token123"` to `"token456"` (change tokens)
- API key changes from `"token123"` to `""` (authenticated → unauthenticated)
- Rapid API key changes (debounce or queue reconnections)
### Implementation Strategy
```typescript
// BAD - Socket created immediately, no reconnection
const socket = createTrustGraphSocket(); // ❌ No access to settings yet
export const SocketContext = createContext(socket);
// GOOD - Socket created after settings load, reconnects on auth change
const SocketProvider = ({ children }) => {
const { settings, isLoaded } = useSettings();
const [socket, setSocket] = useState(null);
const [isReconnecting, setIsReconnecting] = useState(false);
useEffect(() => {
if (!isLoaded) return; // Wait for settings
// Show reconnecting state during transitions
setIsReconnecting(true);
// Clean up old socket if it exists
if (socket) {
console.log("Closing existing socket for reconnection...");
socket.close();
}
// Create new socket with current auth settings
const newSocket = createTrustGraphSocket(settings.authentication.apiKey);
// Wait for connection to establish
newSocket.addEventListener('open', () => {
console.log("Socket connected with auth:",
settings.authentication.apiKey ? 'enabled' : 'disabled');
setIsReconnecting(false);
});
setSocket(newSocket);
return () => newSocket?.close();
}, [isLoaded, settings.authentication.apiKey]); // Re-run when apiKey changes
if (!socket || isReconnecting) {
return (
<Box>
<LoadingSpinner />
<Text>{isReconnecting ? 'Reconnecting...' : 'Initializing...'}</Text>
</Box>
);
}
return (
<SocketContext.Provider value={socket}>
{children}
</SocketContext.Provider>
);
};
```
## Technical Approach
### Phase 1: Deferred Socket Initialization
1. Convert static socket creation to dynamic SocketProvider
2. Wait for settings to load before creating socket
3. Show loading state while settings/socket initialize
4. Pass loaded auth token to socket creation
### Phase 2: Basic Authentication
1. Append `?token=<token>` to WebSocket URL if token exists
2. Add Bearer token to REST API headers if token exists
3. Handle basic auth success/failure
### Phase 3: Socket Reconnection on Auth Change
1. Detect when authentication settings change
2. Close existing socket connection gracefully
3. Create new socket with updated authentication
4. Handle in-flight requests during reconnection
5. Restore any active subscriptions/state (if needed)
### Phase 4: Enhanced Features (Future)
1. Token validation endpoint
2. Authentication status indicator
3. Auto-retry with exponential backoff on auth failures
4. Better error messages for authentication issues
## Security Considerations
1. **Token Storage**:
- Stored in localStorage via settings system
- Never logged to console in production
- Masked in UI displays
2. **Token Transmission**:
- Sent via secure headers
- HTTPS required in production
- No token in URL parameters
3. **Error Handling**:
- Generic error messages to users
- Detailed errors only in development mode
- Rate limiting on failed attempts
## Testing Strategy
1. **Unit Tests**:
- Settings storage and retrieval
- Header injection logic
- Error handling paths
2. **Integration Tests**:
- Full authentication flow
- Token persistence across sessions
- Error recovery scenarios
3. **Manual Testing**:
- UI interaction flows
- Network failure scenarios
- Token expiration handling
## Migration Path
1. **Backwards Compatibility**:
- Support unauthenticated mode (empty token)
- Graceful degradation for older backends
- Feature detection for auth requirements
2. **Rollout Strategy**:
- Deploy with auth disabled by default
- Enable per-user via settings
- Monitor error rates during rollout
## Implementation Example
```typescript
// In trustgraph-socket.ts
export const createTrustGraphSocket = (token?: string) => {
// Use relative URL for WebSocket connection
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host;
const baseUrl = `${protocol}//${host}/api/socket`;
const wsUrl = token ? `${baseUrl}?token=${token}` : baseUrl;
console.log(`Creating socket with auth: ${token ? 'enabled' : 'disabled'}`);
// Create WebSocket connection with authentication
const socket = new WebSocket(wsUrl);
// ... rest of socket implementation
};
// In SocketProvider.tsx (NEW FILE)
export const SocketProvider = ({ children }) => {
const { settings, isLoaded } = useSettings();
const [socket, setSocket] = useState(null);
const [isSocketReady, setIsSocketReady] = useState(false);
useEffect(() => {
// CRITICAL: Wait for settings to load
if (!isLoaded) {
console.log("Waiting for settings to load before creating socket...");
return;
}
console.log("Settings loaded, creating socket with auth:",
settings.authentication.apiKey ? 'enabled' : 'disabled');
// Clean up existing socket before creating new one
if (socket) {
console.log("API key changed, closing existing socket...");
socket.close();
setIsSocketReady(false);
}
// Create socket with current auth settings
const newSocket = createTrustGraphSocket(settings.authentication.apiKey);
// Mark socket as ready when connection opens
newSocket.addEventListener('open', () => {
console.log("Socket connected successfully");
setIsSocketReady(true);
});
setSocket(newSocket);
return () => {
newSocket?.close();
setIsSocketReady(false);
};
}, [isLoaded, settings.authentication.apiKey]); // Reconnects when API key changes
// Show loading state until both settings and socket are ready
if (!isSocketReady) {
return (
<Box>
<CenterSpinner />
<Text>Initializing connection...</Text>
</Box>
);
}
return (
<SocketContext.Provider value={socket}>
{children}
</SocketContext.Provider>
);
};
// In App.tsx or index.tsx
<QueryClientProvider client={queryClient}>
<SocketProvider> {/* Now waits for settings before creating socket */}
<ChakraProvider>
<App />
</ChakraProvider>
</SocketProvider>
</QueryClientProvider>
```
## Open Questions
1. **Socket Reconnection Strategy**:
- Should socket reconnect automatically when auth settings change?
- How to handle in-flight requests during reconnection?
- Should we show a loading state during reconnection?
2. **Error Handling**:
- How does the backend communicate auth failures (401 vs 403)?
- Should we automatically redirect to settings on auth failure?
- How to differentiate between network errors and auth errors?
3. **Token Security**:
- Should we support token rotation/refresh?
- How long should tokens be valid?
- Should we add CSRF protection for REST calls?
## References
- [Settings System Documentation](./SETTINGS.md)
- [Socket Implementation](../../src/api/trustgraph/socket.ts)
- [TrustGraph Socket API](../../src/api/trustgraph/trustgraph-socket.ts)

View file

@ -0,0 +1,185 @@
# LLM Models Editor Technical Specification
## Overview
This specification describes the LLM Models Editor in the TrustGraph UI. This feature allows administrators to manage the `llm-model` parameter type - the list of available LLM models that appear in dropdown menus when launching flows.
The LLM model list is stored as a parameter type definition in the configuration system with type `"parameter-types"` and key `"llm-model"`. The editor provides a simple table interface for managing model options (ID, Description, Default).
## Background
The `llm-model` parameter controls which models are available when configuring flows. It's stored as a parameter type definition with an `enum` field containing model options.
### Current State
- The llm-model parameter can be modified through direct config API calls or CLI commands
- The parameter type with its `enum` array renders as a dropdown in flow dialogs
- The default value determines which model is pre-selected
### Feature Switch
This feature is controlled by a feature switch in Settings:
- **Setting Name**: `llmModels`
- **Display Label**: "LLM Models"
- **Default**: `false` (off by default)
- **Location**: Settings page → Feature Switches section
## Goals
- **Simple Table Editor**: Editable table with ID, Description, and Default columns
- **Direct Editing**: Edit model options directly in table cells
- **Add/Delete Rows**: Add new models or delete existing ones
- **Default Selection**: Radio button to mark one model as default
- **Save Changes**: Manual save with "Save Changes" button
- **Auto-defaults**: First model automatically selected as default when adding to empty table
## Technical Design
### Architecture
Following CODEBOT-INSTRUCTIONS.md patterns:
**Component Structure:**
```
src/
├── pages/
│ └── LLMModelsPage.tsx # Main page with PageHeader
├── components/
│ └── llm-models/ # Domain-specific directory
│ ├── LLMModels.tsx # Container component
│ ├── ParameterTypeSelector.tsx # (unused - kept for future)
│ └── ModelsTable.tsx # Editable table with save
├── state/
│ └── llm-models.ts # API hooks
└── model/
└── llm-models.ts # TypeScript types
```
### Data Models
#### EnumOption (Model Option)
```typescript
interface EnumOption {
id: string; // Model ID (e.g., "gemini-2.5-flash")
description: string; // Display text (e.g., "Gemini 2.5 Flash")
}
```
#### LLMModelParameter
```typescript
interface LLMModelParameter {
name: string; // Parameter type key (always "llm-model")
type: string; // Always "string"
description: string; // Read-only (e.g., "LLM model to use")
default: string; // Default model ID
enum: EnumOption[]; // List of models
required: boolean; // Read-only
}
```
### Implementation Details
**Key Behavior:**
1. Page only handles the single `llm-model` parameter type
2. Table edits are local until "Save Changes" is clicked
3. Radio buttons use native HTML inputs (Chakra RadioGroup had issues in tables)
4. When adding first model to empty table, it's auto-selected as default
5. When editing ID of default model, default value updates to track changes
6. When deleting default model, first remaining model becomes default
7. Empty ID fields are allowed but disabled for default selection
**State Management:**
- Uses `getConfig([{type: "parameter-types", key: "llm-model"}])` to fetch single param
- Uses `putConfig()` to save changes, preserving read-only fields
- React Query handles caching and invalidation
### Routing and Navigation
#### Route (`src/App.tsx`)
```typescript
<Route path="/llm-models" element={<LLMModelsPage />} />
```
#### Sidebar Navigation (`src/components/Sidebar.tsx`)
```typescript
{settings.featureSwitches.llmModels && (
<NavItem to="/llm-models" icon={Bot} label="LLM Models" />
)}
```
### Feature Switch Integration
#### Settings Types (`src/model/settings-types.ts`)
```typescript
featureSwitches: {
llmModels: boolean; // Default: false
}
```
#### Feature Switches Section (`src/components/settings/FeatureSwitchesSection.tsx`)
Adds toggle UI with prop `llmModels` and handler `onLlmModelsChange`
## User Workflows
### Editing Model Options
1. Enable feature in Settings → Feature Switches → LLM Models
2. Navigate to LLM Models page from sidebar
3. View current models in table
4. Edit ID or Description fields directly
5. Click "Save Changes" to persist
6. Notification confirms success
### Setting Default Model
1. View models table
2. Click radio button in "Default" column for desired model
3. Click "Save Changes" to persist
### Adding New Model
1. Click "Add Model" button
2. New empty row appears
3. Enter Model ID and Description
4. If it's the only model, radio button is auto-selected
5. Click "Save Changes" to persist
### Deleting Model
1. Click trash icon next to model
2. Row is removed from local state
3. If deleted model was default, first remaining model becomes default
4. Click "Save Changes" to persist
## Implementation Checklist
- [x] Update `src/model/settings-types.ts` - Add `llmModels` feature switch
- [x] Update `src/components/settings/FeatureSwitchesSection.tsx` - Add LLM Models toggle
- [x] Update `src/components/settings/Settings.tsx` - Wire up llmModels prop
- [x] Create `src/model/llm-models.ts` - Type definitions
- [x] Create `src/state/llm-models.ts` - useLLMModels hook
- [x] Create `src/components/llm-models/LLMModels.tsx` - Container
- [x] Create `src/components/llm-models/ParameterTypeSelector.tsx` - (Created but unused)
- [x] Create `src/components/llm-models/ModelsTable.tsx` - Editable table with save
- [x] Create `src/pages/LLMModelsPage.tsx` - Main page with PageHeader
- [x] Update `src/App.tsx` - Add route
- [x] Update `src/components/Sidebar.tsx` - Add navigation item with Bot icon
- [x] Test CRUD operations
- [x] Test feature switch toggle
## Future Enhancements
1. **Multiple Parameter Types**: Support editing other parameter types with enum arrays (llm-rag-model, etc.)
2. **Import/Export**: Bulk import/export model lists from JSON
3. **Templates**: Pre-configured model lists for common providers
4. **Model Metadata**: Additional fields like context length, cost per token
5. **Reordering**: Drag-and-drop or up/down arrows to reorder models
## References
- Flow Configurable Parameters: `docs/tech-specs/flow-configurable-parameters.md`
- Parameter Inputs Component: `src/components/flows/ParameterInputs.tsx`
- Settings Feature Switches: `src/components/settings/FeatureSwitchesSection.tsx`
- CODEBOT Instructions: `CODEBOT-INSTRUCTIONS.md`

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,150 @@
# Schema Support for TrustGraph UI
## Overview
This document specifies the UI work needed to support structured data schemas in TrustGraph. Schemas enable the system to work with structured data (rows in tables/objects) alongside unstructured data processing.
## Schema Representation
Based on the STRUCTURED_DATA.md specification, schemas are stored in TrustGraph's configuration system with:
- **Type**: `schema` (fixed value for all structured data schemas)
- **Key**: Unique schema identifier (e.g., `customer_records`, `transaction_log`)
- **Value**: JSON schema definition
### Schema Structure Example:
```json
{
"name": "customer_records",
"description": "Customer information table",
"fields": [
{
"name": "customer_id",
"type": "string",
"primary_key": true
},
{
"name": "name",
"type": "string",
"required": true
},
{
"name": "email",
"type": "string",
"required": true
},
{
"name": "registration_date",
"type": "timestamp"
},
{
"name": "status",
"type": "string",
"enum": ["active", "inactive", "suspended"]
}
],
"indexes": ["email", "registration_date"]
}
```
### Field Types Supported:
- `string`
- `integer`
- `float`
- `boolean`
- `timestamp`
- `enum` (with predefined values)
### Field Properties:
- `name`: Field identifier
- `type`: Data type
- `primary_key`: Boolean flag for primary key fields
- `required`: Boolean flag for required fields
- `enum`: Array of allowed values for enum types
## Requirements
Based on the Prompts page implementation pattern, the Schema UI should provide:
1. **Schema Management Page**
- List all schemas in a table view
- Create new schemas via modal dialog
- Edit existing schemas
- Delete schemas with confirmation
- View schema details in a readable format
2. **UI Components Needed**
- Main schemas page with table listing
- Create/Edit schema dialog with form validation
- Schema field editor (add/remove/edit fields)
- Field type selector with appropriate options
- Primary key and index configuration
- Schema preview/viewer component
3. **State Management**
- Use React Query for data fetching and mutations
- Implement CRUD operations following the prompts pattern
- Handle loading states and error notifications
- Cache management and invalidation
## Implementation Details
### API Integration Pattern (from Prompts example)
1. **Configuration Keys**
- Individual schemas: `{ type: "schema", key: "{schema_id}" }`
- List all schemas by querying all keys with `type: "schema"`
2. **State Management Hook** (`useSchemas`)
- `getValues("schema")` to list all schemas (returns array of {key, value} objects)
- `putConfig()` to create/update schemas
- `deleteConfig()` to remove schemas
- No need for separate index management
3. **Component Structure**
- `SchemasPage.tsx` - Main page component
- `components/schemas/Schemas.tsx` - Container component
- `components/schemas/SchemasTable.tsx` - List view
- `components/schemas/SchemaControls.tsx` - Action buttons
- `components/schemas/EditSchemaDialog.tsx` - Create/Edit form
- `components/schemas/SchemaViewer.tsx` - Read-only schema display
- `state/schemas.ts` - React Query hooks
- `model/schemas-table.tsx` - TypeScript definitions
4. **Field Editor Requirements**
- Dynamic field list with add/remove capabilities
- Field property editors:
- Name (text input)
- Type (dropdown: string, integer, float, boolean, timestamp, enum)
- Primary key (checkbox)
- Required (checkbox)
- Enum values (list editor, shown only for enum type)
- Index configuration (multi-select from available fields)
5. **Validation Rules**
- Schema name: Required, unique
- At least one field required
- At least one primary key field
- Field names must be unique within schema
- Enum type requires at least one enum value
## Tasks
1. Create schema state management hook (`useSchemas`)
2. Implement SchemasPage and routing
3. Build SchemasTable component with sorting/filtering
4. Create EditSchemaDialog with field editor
5. Add schema validation logic
6. Implement schema viewer component
7. Add TypeScript models and table configurations
8. Integration testing with backend API
## Notes
- Follow the existing Prompts page pattern for consistency
- Use Chakra UI components matching current design system
- Implement proper error handling and user feedback
- Consider adding import/export functionality for schemas
- May need to handle schema versioning in the future
- Implementation is simpler than prompts since we use `getValues("schema")` instead of maintaining a separate index
- Reference the agent-tools implementation pattern which also uses `getValues()` directly

View file

@ -0,0 +1,232 @@
# Settings Page for TrustGraph UI
## Overview
This document specifies the implementation of a Settings page for the TrustGraph UI application. The Settings page will provide a centralized interface for configuring application preferences, user settings, and system-wide configurations.
## Requirements
The Settings page should provide:
1. **Settings Management Interface**
- Centralized location for all user and system settings
- Organized into logical sections/categories using visual grouping
- Real-time save functionality with visual feedback
- Reset to defaults capability
- Import/export settings configuration
2. **Settings Categories**
- **Authentication**: API key configuration for TrustGraph socket authentication
- **GraphRAG Configuration**: Entity limits, triple limits, and graph traversal settings
- **Feature Switches**: Toggle switches for advanced/experimental functionality
3. **Specific Settings**
**Authentication Section**:
- **API Key**: Text input field (password type for security)
- Default: empty string (no authentication)
- When set: used for TrustGraph socket authentication
- Should mask the key value when displayed
**GraphRAG Settings Section**:
- **Entity Limit**: Number input (default: 50)
- **Triple Limit**: Number input (default: 30)
- **Max Subgraph Size**: Number input (default: 1000)
- **Path Length**: Number input (default: 2)
**Feature Switches Section**:
- **Taxonomy Editor**: Boolean toggle (default: false)
- **Submissions**: Boolean toggle (default: false)
3. **Navigation Integration**
- Add settings route at the end of the sidebar navigation
- Use Settings icon from lucide-react
- Standard page structure with PageHeader
## Implementation Details
### Routing Integration
**Sidebar Addition** (src/components/Sidebar.tsx):
- Add import: `Settings` from lucide-react
- Add NavItem at end of VStack: `<NavItem to="/settings" icon={Settings} label="Settings" />`
**Route Configuration**:
- Add route in main router configuration
- Path: `/settings`
- Component: `SettingsPage`
### Component Structure
Following the established patterns from UI-TOOLKITS.md:
```
src/
├── pages/
│ └── SettingsPage.tsx # Main page with PageHeader
├── components/
│ └── settings/
│ ├── Settings.tsx # Main container component
│ ├── SettingsForm.tsx # Settings form management
│ ├── AuthenticationSection.tsx # API key configuration
│ ├── GraphRagSection.tsx # GraphRAG settings
│ ├── FeatureSwitchesSection.tsx # Feature toggles
│ └── SettingsControls.tsx # Action buttons (save, reset, import/export)
├── state/
│ └── settings.ts # Settings state management with localStorage
└── model/
└── settings-types.ts # TypeScript definitions for settings
```
### UI Framework Considerations
Based on UI-TOOLKITS.md guidelines:
**Chakra UI v3 Components**:
- Use `Field.Root` and `Field.Label` for form inputs
- Use common components: `TextField`, `SelectField`, `Card`
- Use `Alert.Root` for validation feedback
- Follow semantic color tokens (`primary`, `accent`, etc.)
**Icons**:
- Use `Settings` from lucide-react (already established pattern)
- Other icons as needed: `Save`, `RotateCcw`, `Download`, `Upload`
**Notifications**:
- Use `useNotification` hook (NOT direct toaster)
- Provide success/error feedback for save operations
### State Management Pattern
Following the established React Query pattern:
**Settings State Hook** (`useSettings`):
- `getSettings()` to retrieve current settings from localStorage
- `updateSetting()` to modify individual settings and persist to localStorage
- `resetSettings()` to restore defaults and clear localStorage
- `exportSettings()` and `importSettings()` for configuration management
- Handle localStorage serialization/deserialization
- Provide default values when localStorage is empty
**Data Storage**:
- **Browser localStorage**: All settings stored in browser's localStorage
- Settings persist across browser sessions
- Settings are client-side only (no server synchronization)
- Use structured key naming for organized storage
### Testing Strategy
Based on TEST_STRATEGY.md:
**Component Tests**:
- SettingsForm validation and state management
- Settings section rendering and interaction
- Import/export functionality
- Reset to defaults behavior
**Integration Tests**:
- Settings persistence across sessions
- Settings application to other components
- Route navigation and sidebar integration
**Test Data**:
```tsx
const mockSettings = {
authentication: {
apiKey: '' // Empty by default
},
graphrag: {
entityLimit: 50,
tripleLimit: 30,
maxSubgraphSize: 1000,
pathLength: 2
},
featureSwitches: {
taxonomyEditor: false,
submissions: false
}
};
```
## Tasks
1. **Foundation Setup**
- Create SettingsPage component with PageHeader
- Add routing integration and sidebar navigation
- Set up basic component structure
2. **State Management**
- Implement settings state hook with localStorage integration
- Define settings data model with typed interfaces
- Create default settings configuration
- Handle localStorage persistence and retrieval
3. **UI Implementation**
- Build AuthenticationSection with masked API key input
- Create GraphRagSection with NumberField components for limits
- Implement FeatureSwitchesSection with toggle switches
- Add visual grouping with Card components for each section
- Implement form validation and submission
- Add import/export functionality
- Create reset to defaults mechanism
4. **Integration & Testing**
- Add route configuration
- Implement component tests
- Add integration tests for settings persistence
- Verify UI consistency with design system
## Data Model
### Settings Structure
```tsx
interface Settings {
authentication: {
apiKey: string; // Default: ''
};
graphrag: {
entityLimit: number; // Default: 50
tripleLimit: number; // Default: 30
maxSubgraphSize: number; // Default: 1000
pathLength: number; // Default: 2
};
featureSwitches: {
taxonomyEditor: boolean; // Default: false
submissions: boolean; // Default: false
};
}
```
### LocalStorage Keys
- Main settings: `trustgraph-settings`
- Backup/versioning: Consider `trustgraph-settings-backup` for import/export
## Integration Points
### API Key Integration
- Settings API key should be used by TrustGraph socket authentication
- When API key is empty, no authentication is used
- When API key has value, it's passed to socket connection for authentication
### Feature Switches Integration
- **Taxonomy Editor**: Controls visibility of taxonomy-related routes/components
- **Submissions**: Controls visibility of submissions/processing routes/components
- Features should be conditionally rendered based on these settings
## Notes
- **Security**: API key should be masked in UI but stored as plaintext in localStorage
- **Visual Grouping**: Use Card components to separate the three main sections
- **Real-time Updates**: Settings changes should be immediately persisted to localStorage
- **Validation**: Number inputs should have min/max constraints and validation
- **Accessibility**: Ensure full keyboard navigation and screen reader support
- **Responsive**: Settings should work well on mobile and desktop layouts
## Future Considerations
- User-specific vs. system-wide settings
- Settings synchronization across devices
- Advanced settings with warnings/confirmations
- Settings search/filter capability
- Bulk settings operations
- Settings versioning and migration

View file

@ -0,0 +1,289 @@
# Socket Reliability Refactor
## Overview
This document outlines a comprehensive refactor to address critical issues in the TrustGraph UI WebSocket connection handling that are causing exponential retry storms and excessive logging.
## Current Problems
### Issue #1: Dual Retry System Conflict ⚠️ CRITICAL
**Problem**: Two independent retry mechanisms create multiplicative retry storms:
1. **BaseApi Socket-Level Reconnection** (`trustgraph-socket.ts`)
- Triggers on `onClose()` events
- 10 attempts with exponential backoff (2-60 seconds)
- Handles socket-level connection failures
2. **ServiceCall Request-Level Retries** (`service-call.ts`)
- Triggers on send failures and timeouts
- 3 retries per request with backoff
- **Calls `socket.reopen()` which triggers BaseApi reconnection**
**Result**: Single connection failure → 3 request retries × 10 socket reconnections = **30+ retry attempts**
```typescript
// service-call.ts:160, 174 - PROBLEM LINES
console.log("Reopen...");
this.socket.reopen(); // ← Triggers BaseApi reconnection
```
### Issue #2: SocketProvider Dependency Loop ✅ FIXED
**Status**: Resolved by removing `socket` from dependency array
### Issue #3: Inconsistent Request Retry Backoff ⚠️ MEDIUM
**Location**: `service-call.ts:170`
```typescript
// Inconsistent retry strategies:
setTimeout(this.attempt.bind(this), backoffDelay); // Exponential backoff ✅
setTimeout(this.attempt.bind(this), 500); // Fixed 500ms ❌ (spams)
setTimeout(this.attempt.bind(this), backoffDelay); // Exponential backoff ✅
```
### Issue #4: Concurrent Socket Reopen Calls ⚠️ MEDIUM
**Problem**: Multiple failed requests simultaneously call `socket.reopen()`:
- No coordination between ServiceCalls
- Redundant reconnection attempts
- Race conditions in connection state
## Proposed Solution: Centralized Retry Strategy
### Architectural Decision
**Adopt Option A: Let BaseApi handle ALL reconnection logic**
**Rationale**:
- ✅ Single source of truth for connection state
- ✅ BaseApi already has robust exponential backoff
- ✅ Eliminates retry system conflicts
- ✅ Cleaner separation of concerns
- ✅ Minimal code changes required
### Implementation Plan
#### Phase 1: Remove ServiceCall Reconnection Triggers
**File**: `src/api/trustgraph/service-call.ts`
**Changes**:
1. Remove `this.socket.reopen()` calls (lines 160, 174)
2. Replace with passive waiting for socket reconnection
3. Standardize backoff for all retry paths
```typescript
// BEFORE (service-call.ts:156-161)
console.log("Reopen...");
this.socket.reopen(); // ← REMOVE THIS
// AFTER
console.log("Message send failure, waiting for socket reconnection...");
// Let BaseApi handle reconnection, just retry the request
```
#### Phase 2: Improve Request Queueing Strategy
**Current Behavior**: ServiceCall attempts fail when socket is not ready
**New Behavior**: ServiceCall waits for socket to become available
```typescript
// Enhanced attempt() method logic
attempt() {
if (this.complete) return;
this.retries--;
if (this.retries < 0) {
// Give up after retries exhausted
this.error("Ran out of retries");
return;
}
if (this.socket.ws && this.socket.ws.readyState === WebSocket.OPEN) {
// Socket ready - send message
try {
this.socket.ws.send(JSON.stringify(this.msg));
this.timeoutId = setTimeout(this.onTimeout.bind(this), this.timeout);
} catch (e) {
// Send failed - wait and retry (no socket reopen)
setTimeout(this.attempt.bind(this), this.calculateBackoff());
}
} else {
// Socket not ready - wait for BaseApi to reconnect
console.log("Request", this.mid, "waiting for socket reconnection...");
setTimeout(this.attempt.bind(this), this.calculateBackoff());
}
}
calculateBackoff() {
return Math.min(
SOCKET_RECONNECTION_TIMEOUT * Math.pow(2, 3 - this.retries) + Math.random() * 1000,
30000
);
}
```
#### Phase 3: Enhanced BaseApi Connection Management
**File**: `src/api/trustgraph/trustgraph-socket.ts`
**Improvements**:
1. Add connection state tracking
2. Prevent redundant reconnection attempts
3. Improve logging for debugging
```typescript
class BaseApi {
reconnectionState: 'idle' | 'reconnecting' | 'failed' = 'idle';
scheduleReconnect() {
// Prevent concurrent reconnection attempts
if (this.reconnectionState === 'reconnecting') {
console.log("[socket] Reconnection already in progress, skipping");
return;
}
if (this.reconnectTimer) return;
this.reconnectionState = 'reconnecting';
// ... existing logic
}
onOpen() {
console.log("[socket open]");
this.reconnectAttempts = 0;
this.reconnectionState = 'idle'; // Reset state
// Clear any pending reconnect timer
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = undefined;
}
}
}
```
## Expected Benefits
### Immediate Impact
- **80-90% reduction in retry attempts** - eliminates dual retry system
- **Cleaner logs** - single source of reconnection messages
- **Predictable behavior** - one retry algorithm instead of two
### Log Message Changes
```
// BEFORE: Chaotic dual retry messages
[socket] Reconnecting in 2000ms (attempt 1)
Request test-123 timed out
Message send failure, retry...
Reopen...
[socket] Reconnecting in 4000ms (attempt 2)
Request test-123 ran out of retries
Request test-456 timed out
Reopen...
[socket] Reconnecting in 8000ms (attempt 3)
// AFTER: Clean, coordinated messages
[socket] Reconnecting in 2000ms (attempt 1)
Request test-123 waiting for socket reconnection...
Request test-456 waiting for socket reconnection...
[socket open]
Request test-123 sent successfully
Request test-456 sent successfully
```
### Performance Improvements
- **Reduced CPU usage** - fewer concurrent timers and retry loops
- **Less network spam** - coordinated reconnection attempts
- **Better user experience** - faster recovery from connection issues
## Risk Assessment
### Low Risk Changes
- ✅ Removing `socket.reopen()` calls from ServiceCall
- ✅ Standardizing backoff calculations
- ✅ Adding connection state tracking
### Potential Issues
- ⚠️ **Request timeout behavior may change** - requests may take longer to fail
- ⚠️ **Need to test edge cases** - rapid API key changes, server restarts
- ⚠️ **Verify inflight request cleanup** - ensure requests don't hang indefinitely
### Mitigation Strategies
1. **Preserve existing timeout behavior** - requests should still timeout appropriately
2. **Add circuit breaker** - stop retrying after socket reconnection gives up
3. **Comprehensive testing** - test connection failure scenarios
## Testing Strategy
### Unit Tests
- Mock WebSocket state transitions
- Verify ServiceCall doesn't trigger socket reopens
- Test backoff calculations are consistent
### Integration Tests
- Test connection failure and recovery scenarios
- Verify request queueing during reconnection
- Test concurrent request handling
### Manual Testing Scenarios
1. **Server shutdown** - verify clean reconnection behavior
2. **Network interruption** - test mobile/wifi scenarios
3. **API key changes** - ensure proper socket recreation
4. **High load** - multiple concurrent requests during connection issues
## Implementation Timeline
### Phase 1: Core Fixes (1-2 hours)
- Remove `socket.reopen()` calls from ServiceCall
- Standardize ServiceCall backoff calculations
- Add basic connection state tracking
### Phase 2: Enhanced Reliability (2-3 hours)
- Implement request queueing improvements
- Add comprehensive logging
- Enhanced error handling
### Phase 3: Testing & Validation (2-4 hours)
- Unit test coverage
- Integration testing
- Performance validation
**Total Estimated Effort**: 5-9 hours
## Success Metrics
### Quantitative Goals
- **Reduce retry attempts by 80%+** during connection failures
- **Eliminate concurrent socket reopen calls**
- **Standardize all retry backoff to exponential**
### Qualitative Goals
- **Cleaner, more understandable logs**
- **Predictable connection recovery behavior**
- **Better separation of concerns in codebase**
## Future Enhancements
### Potential Improvements (Out of Scope)
1. **Request prioritization** - critical requests retry faster
2. **Connection health monitoring** - proactive reconnection
3. **Metrics collection** - track connection reliability
4. **Advanced queueing** - persist important requests across sessions
### Monitoring Additions
```typescript
// Connection reliability metrics
interface SocketMetrics {
connectionAttempts: number;
successfulConnections: number;
averageReconnectionTime: number;
requestsLostDuringReconnection: number;
}
```
## Conclusion
This refactor addresses the root cause of socket retry storms by establishing BaseApi as the single authority for connection management. The changes are surgical and low-risk, focusing on removing the problematic dual retry system while preserving all existing functionality.
**Next Steps**: Implement Phase 1 changes and validate that retry storms are eliminated before proceeding with enhanced features.