mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-06 11:52:10 +02:00
init
This commit is contained in:
parent
c386f68743
commit
b6536eca38
100 changed files with 17680 additions and 377 deletions
|
|
@ -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),
|
||||
};
|
||||
});
|
||||
110
ts/packages/workbench/src/providers/settings-provider.tsx
Normal file
110
ts/packages/workbench/src/providers/settings-provider.tsx
Normal 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;
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
125
ts/packages/workbench/src/providers/socket-provider.tsx
Normal file
125
ts/packages/workbench/src/providers/socket-provider.tsx
Normal 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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue