This commit is contained in:
elpresidank 2026-04-05 22:44:45 -05:00
parent c386f68743
commit b6536eca38
100 changed files with 17680 additions and 377 deletions

View file

@ -0,0 +1,215 @@
import { useCallback } from "react";
import { useSocket } from "@/providers/socket-provider";
import {
useConversation,
nextMessageId,
type ChatMessage,
} from "./use-conversation";
import { useSessionStore } from "./use-session-store";
import { useProgressStore } from "./use-progress-store";
import { useSettings } from "@/providers/settings-provider";
import type { StreamingMetadata } from "@trustgraph/client";
// ---------------------------------------------------------------------------
// Hook
// ---------------------------------------------------------------------------
export interface UseChatReturn {
submitMessage: (opts: { input: string }) => void;
}
/**
* Orchestrates sending a chat message through the selected RAG / agent
* pipeline and accumulates streamed chunks into the conversation store.
*/
export function useChat(): UseChatReturn {
const socket = useSocket();
const flowId = useSessionStore((s) => s.flowId);
const chatMode = useConversation((s) => s.chatMode);
const addMessage = useConversation((s) => s.addMessage);
const updateLastMessage = useConversation((s) => s.updateLastMessage);
const setInput = useConversation((s) => s.setInput);
const collection = useSettings((s) => s.settings.collection);
const addActivity = useProgressStore((s) => s.addActivity);
const removeActivity = useProgressStore((s) => s.removeActivity);
const submitMessage = useCallback(
({ input }: { input: string }) => {
if (!input.trim()) return;
const activityLabel = "Chat request";
// 1. Add the user message
const userMsg: ChatMessage = {
id: nextMessageId(),
role: "user",
content: input,
timestamp: Date.now(),
};
addMessage(userMsg);
setInput("");
// 2. Add a placeholder assistant message for streaming
const assistantId = nextMessageId();
const isAgent = chatMode === "agent";
const assistantMsg: ChatMessage = {
id: assistantId,
role: "assistant",
content: "",
timestamp: Date.now(),
isStreaming: true,
...(isAgent
? {
agentPhases: { think: "", observe: "", answer: "" },
activePhase: "think" as const,
}
: {}),
};
addMessage(assistantMsg);
addActivity(activityLabel);
const flow = socket.flow(flowId);
// Shared handler for streaming responses (graph-rag / document-rag)
const onChunk = (
chunk: string,
complete: boolean,
metadata?: StreamingMetadata,
) => {
updateLastMessage((prev) => ({
...prev,
content: prev.content + chunk,
isStreaming: !complete,
...(complete && metadata
? {
metadata: {
model: metadata.model,
inTokens: metadata.in_token,
outTokens: metadata.out_token,
},
}
: {}),
}));
if (complete) {
removeActivity(activityLabel);
}
};
const onError = (error: string) => {
updateLastMessage((prev) => ({
...prev,
content: prev.content || `Error: ${error}`,
isStreaming: false,
}));
removeActivity(activityLabel);
};
// 3. Dispatch based on chat mode
switch (chatMode) {
case "graph-rag":
flow.graphRagStreaming(input, onChunk, onError, undefined, collection);
break;
case "document-rag":
flow.documentRagStreaming(input, onChunk, onError, undefined, collection);
break;
case "agent": {
// Agent has separate think / observe / answer streams.
// We track each phase in agentPhases and display the answer
// as the main content.
flow.agent(
input,
// think
(chunk, complete) => {
updateLastMessage((prev) => {
const phases = prev.agentPhases ?? {
think: "",
observe: "",
answer: "",
};
return {
...prev,
agentPhases: {
...phases,
think: phases.think + chunk,
},
activePhase: complete ? prev.activePhase : "think",
};
});
},
// observe
(chunk, complete) => {
updateLastMessage((prev) => {
const phases = prev.agentPhases ?? {
think: "",
observe: "",
answer: "",
};
return {
...prev,
agentPhases: {
...phases,
observe: phases.observe + chunk,
},
activePhase: complete ? prev.activePhase : "observe",
};
});
},
// answer
(chunk, complete, metadata) => {
updateLastMessage((prev) => {
const phases = prev.agentPhases ?? {
think: "",
observe: "",
answer: "",
};
const newAnswer = phases.answer + chunk;
return {
...prev,
content: newAnswer,
agentPhases: {
...phases,
answer: newAnswer,
},
activePhase: complete ? undefined : "answer",
isStreaming: !complete,
...(complete && metadata
? {
metadata: {
model: metadata.model,
inTokens: metadata.in_token,
outTokens: metadata.out_token,
},
}
: {}),
};
});
if (complete) {
removeActivity(activityLabel);
}
},
// error
onError,
);
break;
}
}
},
[
socket,
flowId,
chatMode,
collection,
addMessage,
updateLastMessage,
setInput,
addActivity,
removeActivity,
],
);
return { submitMessage };
}

View file

@ -0,0 +1,91 @@
import { create } from "zustand";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export type ChatMode = "graph-rag" | "document-rag" | "agent";
export type MessageRole = "user" | "assistant" | "system";
/** Phase labels for agent-mode messages */
export type AgentPhase = "think" | "observe" | "answer";
export interface ChatMessage {
id: string;
role: MessageRole;
content: string;
/** Timestamp (epoch ms) */
timestamp: number;
/** If true the message is still being streamed */
isStreaming?: boolean;
/** Optional metadata attached on completion */
metadata?: {
model?: string;
inTokens?: number;
outTokens?: number;
};
/** Agent-mode phases with their accumulated content */
agentPhases?: {
think: string;
observe: string;
answer: string;
};
/** Indicates the current active phase during streaming */
activePhase?: AgentPhase;
}
// ---------------------------------------------------------------------------
// Store
// ---------------------------------------------------------------------------
interface ConversationState {
messages: ChatMessage[];
input: string;
chatMode: ChatMode;
setInput: (value: string) => void;
setChatMode: (mode: ChatMode) => void;
addMessage: (message: ChatMessage) => void;
/**
* Update the last message in the list (used during streaming to append
* chunks). The `updater` receives the current last message and must
* return the replacement.
*/
updateLastMessage: (
updater: (prev: ChatMessage) => ChatMessage,
) => void;
clearMessages: () => void;
}
let _nextMsgId = 0;
export function nextMessageId(): string {
return `msg-${++_nextMsgId}-${Date.now()}`;
}
export const useConversation = create<ConversationState>()((set) => ({
messages: [],
input: "",
chatMode: "graph-rag",
setInput: (value) => set({ input: value }),
setChatMode: (mode) => set({ chatMode: mode }),
addMessage: (message) =>
set((state) => ({ messages: [...state.messages, message] })),
updateLastMessage: (updater) =>
set((state) => {
if (state.messages.length === 0) return state;
const last = state.messages[state.messages.length - 1]!;
const updated = updater(last);
return {
messages: [...state.messages.slice(0, -1), updated],
};
}),
clearMessages: () => set({ messages: [] }),
}));

View file

@ -0,0 +1,130 @@
import { useCallback, useEffect, useState } from "react";
import { useSocket } from "@/providers/socket-provider";
import { useConnectionState } from "@/providers/socket-provider";
import { useProgressStore } from "./use-progress-store";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface FlowSummary {
id: string;
description?: string;
[key: string]: unknown;
}
export interface UseFlowsReturn {
flows: FlowSummary[];
loading: boolean;
error: string | null;
/** Refresh the flow list from the server */
getFlows: () => Promise<void>;
/** Start a new flow */
startFlow: (
id: string,
blueprintName: string,
description: string,
parameters?: Record<string, unknown>,
) => Promise<void>;
/** Stop a running flow */
stopFlow: (id: string) => Promise<void>;
/** Fetch a single flow definition */
getFlow: (id: string) => Promise<FlowSummary>;
}
// ---------------------------------------------------------------------------
// Hook
// ---------------------------------------------------------------------------
export function useFlows(): UseFlowsReturn {
const socket = useSocket();
const connectionState = useConnectionState();
const addActivity = useProgressStore((s) => s.addActivity);
const removeActivity = useProgressStore((s) => s.removeActivity);
const [flows, setFlows] = useState<FlowSummary[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const getFlows = useCallback(async () => {
const act = "Load flows";
try {
setLoading(true);
setError(null);
addActivity(act);
const ids: string[] = await socket.flows().getFlows();
const results = await Promise.all(
ids.map(async (id) => {
const def = await socket.flows().getFlow(id);
return { id, ...def } as FlowSummary;
}),
);
setFlows(results);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
setError(msg);
console.error("useFlows.getFlows error:", err);
} finally {
setLoading(false);
removeActivity(act);
}
}, [socket, addActivity, removeActivity]);
const startFlow = useCallback(
async (
id: string,
blueprintName: string,
description: string,
parameters?: Record<string, unknown>,
) => {
const act = `Start flow ${id}`;
try {
addActivity(act);
await socket.flows().startFlow(id, blueprintName, description, parameters);
// Refresh list after starting
await getFlows();
} finally {
removeActivity(act);
}
},
[socket, addActivity, removeActivity, getFlows],
);
const stopFlow = useCallback(
async (id: string) => {
const act = `Stop flow ${id}`;
try {
addActivity(act);
await socket.flows().stopFlow(id);
await getFlows();
} finally {
removeActivity(act);
}
},
[socket, addActivity, removeActivity, getFlows],
);
const getFlow = useCallback(
async (id: string): Promise<FlowSummary> => {
const def = await socket.flows().getFlow(id);
return { id, ...def } as FlowSummary;
},
[socket],
);
// Auto-load flows when the connection becomes ready
useEffect(() => {
if (
connectionState.status === "connected" ||
connectionState.status === "authenticated" ||
connectionState.status === "unauthenticated"
) {
getFlows();
}
}, [connectionState.status, getFlows]);
return { flows, loading, error, getFlows, startFlow, stopFlow, getFlow };
}

View file

@ -0,0 +1,134 @@
import { useCallback, useState } from "react";
import { useSocket } from "@/providers/socket-provider";
import { useProgressStore } from "./use-progress-store";
import type { DocumentMetadata } from "@trustgraph/client";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface ProcessingMetadata {
id: string;
"document-id": string;
flow: string;
collection: string;
[key: string]: unknown;
}
export interface UseLibraryReturn {
documents: DocumentMetadata[];
processing: ProcessingMetadata[];
loading: boolean;
error: string | null;
/** Refresh the documents list */
getDocuments: () => Promise<void>;
/** Upload a new document */
uploadDocument: (
document: string,
mimeType: string,
title: string,
comments: string,
tags: string[],
id?: string,
) => Promise<void>;
/** Remove a document */
removeDocument: (id: string, collection?: string) => Promise<void>;
/** Get the list of currently-processing documents */
getProcessing: () => Promise<void>;
}
// ---------------------------------------------------------------------------
// Hook
// ---------------------------------------------------------------------------
export function useLibrary(): UseLibraryReturn {
const socket = useSocket();
const addActivity = useProgressStore((s) => s.addActivity);
const removeActivity = useProgressStore((s) => s.removeActivity);
const [documents, setDocuments] = useState<DocumentMetadata[]>([]);
const [processing, setProcessing] = useState<ProcessingMetadata[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const getDocuments = useCallback(async () => {
const act = "Load documents";
try {
setLoading(true);
setError(null);
addActivity(act);
const docs = await socket.librarian().getDocuments();
setDocuments(docs);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
setError(msg);
console.error("useLibrary.getDocuments error:", err);
} finally {
setLoading(false);
removeActivity(act);
}
}, [socket, addActivity, removeActivity]);
const uploadDocument = useCallback(
async (
document: string,
mimeType: string,
title: string,
comments: string,
tags: string[],
id?: string,
) => {
const act = "Upload document";
try {
addActivity(act);
await socket
.librarian()
.loadDocument(document, mimeType, title, comments, tags, id);
// Refresh list after upload
await getDocuments();
} finally {
removeActivity(act);
}
},
[socket, addActivity, removeActivity, getDocuments],
);
const removeDocument = useCallback(
async (id: string, collection?: string) => {
const act = "Remove document";
try {
addActivity(act);
await socket.librarian().removeDocument(id, collection);
await getDocuments();
} finally {
removeActivity(act);
}
},
[socket, addActivity, removeActivity, getDocuments],
);
const getProcessing = useCallback(async () => {
const act = "Load processing";
try {
addActivity(act);
const procs = await socket.librarian().getProcessing();
setProcessing(procs as ProcessingMetadata[]);
} catch (err) {
console.error("useLibrary.getProcessing error:", err);
} finally {
removeActivity(act);
}
}, [socket, addActivity, removeActivity]);
return {
documents,
processing,
loading,
error,
getDocuments,
uploadDocument,
removeDocument,
getProcessing,
};
}

View file

@ -0,0 +1,39 @@
import { create } from "zustand";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
interface ProgressState {
/** Set of currently-running activity labels */
activities: Set<string>;
/** Derived: true when at least one activity is running */
isLoading: boolean;
addActivity: (label: string) => void;
removeActivity: (label: string) => void;
}
// ---------------------------------------------------------------------------
// Store
// ---------------------------------------------------------------------------
export const useProgressStore = create<ProgressState>()((set) => ({
activities: new Set<string>(),
isLoading: false,
addActivity: (label) =>
set((state) => {
const next = new Set(state.activities);
next.add(label);
return { activities: next, isLoading: next.size > 0 };
}),
removeActivity: (label) =>
set((state) => {
const next = new Set(state.activities);
next.delete(label);
return { activities: next, isLoading: next.size > 0 };
}),
}));

View file

@ -0,0 +1,34 @@
import { create } from "zustand";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
/** Minimal flow description kept in session state after selection. */
export interface FlowInfo {
id: string;
description?: string;
[key: string]: unknown;
}
interface SessionState {
/** Currently-selected flow id */
flowId: string;
/** Cached flow definition for the selected flow */
flow: FlowInfo | null;
setFlowId: (id: string) => void;
setFlow: (flow: FlowInfo | null) => void;
}
// ---------------------------------------------------------------------------
// Store
// ---------------------------------------------------------------------------
export const useSessionStore = create<SessionState>()((set) => ({
flowId: "default",
flow: null,
setFlowId: (id) => set({ flowId: id }),
setFlow: (flow) => set({ flow }),
}));