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,92 @@
import { create } from "zustand";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export type NotificationType = "success" | "error" | "warning" | "info";
export interface Notification {
id: string;
type: NotificationType;
title: string;
description?: string;
}
// ---------------------------------------------------------------------------
// Store
// ---------------------------------------------------------------------------
interface NotificationState {
notifications: Notification[];
addNotification: (
type: NotificationType,
title: string,
description?: string,
) => string;
removeNotification: (id: string) => void;
/** Convenience wrappers */
success: (title: string, description?: string) => string;
error: (title: string, description?: string) => string;
warning: (title: string, description?: string) => string;
info: (title: string, description?: string) => string;
}
let _nextId = 0;
function nextId(): string {
return `notif-${++_nextId}-${Date.now()}`;
}
/**
* Simple toast-notification system backed by Zustand.
*
* Components can call `useNotification().success("Done!")` and render the
* current `notifications` array however they like (e.g. a shadcn Toast list).
*
* Notifications are auto-dismissed after 5 seconds.
*/
export const useNotification = create<NotificationState>()((set, get) => {
const AUTO_DISMISS_MS = 5_000;
const addNotification: NotificationState["addNotification"] = (
type,
title,
description,
) => {
const id = nextId();
const notification: Notification = { id, type, title, description };
set((state) => ({
notifications: [...state.notifications, notification],
}));
// Auto-dismiss
setTimeout(() => {
get().removeNotification(id);
}, AUTO_DISMISS_MS);
return id;
};
return {
notifications: [],
addNotification,
removeNotification: (id) =>
set((state) => ({
notifications: state.notifications.filter((n) => n.id !== id),
})),
success: (title, description) =>
addNotification("success", title, description),
error: (title, description) =>
addNotification("error", title, description),
warning: (title, description) =>
addNotification("warning", title, description),
info: (title, description) => addNotification("info", title, description),
};
});

View file

@ -0,0 +1,110 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface FeatureSwitches {
flowClasses: boolean;
submissions: boolean;
tokenCost: boolean;
schemas: boolean;
structuredQuery: boolean;
ontologyEditor: boolean;
agentTools: boolean;
mcpTools: boolean;
llmModels: boolean;
}
export interface Settings {
/** Display name / identifier sent with every request */
user: string;
/** Optional API key for gateway authentication */
apiKey: string;
/** Active knowledge-graph collection */
collection: string;
/** Gateway base URL (used when building the WebSocket URL) */
gatewayUrl: string;
/** Toggle optional sections of the UI */
featureSwitches: FeatureSwitches;
}
// ---------------------------------------------------------------------------
// Defaults
// ---------------------------------------------------------------------------
const DEFAULT_FEATURE_SWITCHES: FeatureSwitches = {
flowClasses: false,
submissions: false,
tokenCost: false,
schemas: false,
structuredQuery: false,
ontologyEditor: false,
agentTools: false,
mcpTools: false,
llmModels: false,
};
const DEFAULT_SETTINGS: Settings = {
user: "user",
apiKey: "",
collection: "default",
gatewayUrl: "",
featureSwitches: DEFAULT_FEATURE_SWITCHES,
};
// ---------------------------------------------------------------------------
// Store
// ---------------------------------------------------------------------------
interface SettingsState {
settings: Settings;
isLoaded: boolean;
/** Replace the entire settings object */
setSettings: (settings: Settings) => void;
/** Update a single top-level key */
updateSetting: <K extends keyof Settings>(
key: K,
value: Settings[K],
) => void;
/** Merge partial feature-switch overrides */
updateFeatureSwitches: (partial: Partial<FeatureSwitches>) => void;
}
export const useSettings = create<SettingsState>()(
persist(
(set) => ({
settings: DEFAULT_SETTINGS,
isLoaded: true,
setSettings: (settings) => set({ settings }),
updateSetting: (key, value) =>
set((state) => ({
settings: { ...state.settings, [key]: value },
})),
updateFeatureSwitches: (partial) =>
set((state) => ({
settings: {
...state.settings,
featureSwitches: {
...state.settings.featureSwitches,
...partial,
},
},
})),
}),
{
name: "trustgraph-settings",
// Mark loaded once rehydration completes
onRehydrateStorage: () => (state) => {
if (state) state.isLoaded = true;
},
},
),
);

View file

@ -0,0 +1,125 @@
import {
createContext,
useContext,
useEffect,
useRef,
useState,
useSyncExternalStore,
type ReactNode,
} from "react";
import { BaseApi, type ConnectionState } from "@trustgraph/client";
// ---------------------------------------------------------------------------
// Context
// ---------------------------------------------------------------------------
interface SocketContextValue {
api: BaseApi;
}
const SocketContext = createContext<SocketContextValue | null>(null);
// ---------------------------------------------------------------------------
// Provider
// ---------------------------------------------------------------------------
export interface SocketProviderProps {
/** Username sent with every API request */
user: string;
/** Optional API key for authenticated connections */
apiKey?: string;
/** WebSocket URL (defaults to "/api/socket", proxied by Vite in dev) */
socketUrl?: string;
children: ReactNode;
}
/**
* SocketProvider creates a single BaseApi instance that lives for the
* lifetime of the provider and tears down the WebSocket on unmount.
*
* The BaseApi is recreated if `user`, `apiKey`, or `socketUrl` change.
*/
export function SocketProvider({
user,
apiKey,
socketUrl,
children,
}: SocketProviderProps) {
const apiRef = useRef<BaseApi | null>(null);
// Re-create the API instance when connection parameters change.
// We track a serial number so downstream consumers re-render.
const [serial, setSerial] = useState(0);
useEffect(() => {
// Close the previous socket if it exists
apiRef.current?.close();
const api = new BaseApi(user, apiKey, socketUrl);
apiRef.current = api;
setSerial((s) => s + 1);
return () => {
api.close();
if (apiRef.current === api) {
apiRef.current = null;
}
};
}, [user, apiKey, socketUrl]);
// Don't render children until the first API instance is ready
if (!apiRef.current) return null;
return (
<SocketContext.Provider
// eslint-disable-next-line react/no-children-prop
key={serial}
value={{ api: apiRef.current }}
>
{children}
</SocketContext.Provider>
);
}
// ---------------------------------------------------------------------------
// Hooks
// ---------------------------------------------------------------------------
/**
* Returns the shared BaseApi instance.
*
* Must be called inside a `<SocketProvider>`.
*/
export function useSocket(): BaseApi {
const ctx = useContext(SocketContext);
if (!ctx) {
throw new Error("useSocket must be used within a <SocketProvider>");
}
return ctx.api;
}
/**
* Subscribes to connection-state changes emitted by BaseApi.
*
* Uses `useSyncExternalStore` for tear-free reads.
*/
export function useConnectionState(): ConnectionState {
const api = useSocket();
// We store the latest snapshot in a ref so the getSnapshot function is stable.
const stateRef = useRef<ConnectionState>({
status: "connecting",
hasApiKey: false,
});
const subscribe = (onStoreChange: () => void) => {
return api.onConnectionStateChange((next) => {
stateRef.current = next;
onStoreChange();
});
};
const getSnapshot = () => stateRef.current;
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}