mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-01 09:29:38 +02:00
fix: comprehensive QA — resolve 13 bugs, add UX improvements across all services
Client SDK: add .catch() to graphRagStreaming/documentRagStreaming (silent timeout), null-guard JSON.parse in getPrompts/getSystemPrompt/getPrompt. Backend: implement "getvalues" config operation for token costs, null-check createTerm() in FalkorDB triples query, add knowledge-cores service entrypoint and Docker entry, return proper HTTP 400/404 for gateway error responses. Workbench: cancel button + elapsed timer for chat, clear agent spinner on error, flow dialog inline validation, responsive header wrapping, knowledge cores loading timeout, sidebar/page naming consistency, theme toggle indicator. Infrastructure: enable Grafana Explore for viewers, add gateway Prometheus scrape target, fix RAG pipeline dashboard layout (6 panels visible), filter Service Health to configured targets only. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
72870a7e2e
commit
3a80872482
22 changed files with 202 additions and 54 deletions
|
|
@ -3,5 +3,6 @@ node_modules
|
|||
deploy
|
||||
**/dist
|
||||
**/.turbo
|
||||
**/*.tsbuildinfo
|
||||
*.log
|
||||
.env*
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ RUN pnpm install --frozen-lockfile
|
|||
|
||||
# Copy source and build
|
||||
COPY packages/ packages/
|
||||
RUN pnpm build
|
||||
RUN pnpm build --filter=@trustgraph/base --filter=@trustgraph/client --filter=@trustgraph/flow
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 2: Runtime
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ services:
|
|||
networks:
|
||||
- trustgraph
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:6333/healthz"]
|
||||
test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/6333'"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
|
@ -179,6 +179,7 @@ services:
|
|||
- GF_AUTH_DISABLE_LOGIN_FORM=false
|
||||
- GF_USERS_DEFAULT_THEME=dark
|
||||
- GF_EXPLORE_ENABLED=true
|
||||
- GF_USERS_VIEWERS_CAN_EDIT=true
|
||||
- GF_FEATURE_TOGGLES_ENABLE=traceqlEditor tempoSearch tempoServiceGraph
|
||||
depends_on:
|
||||
prometheus:
|
||||
|
|
@ -323,6 +324,18 @@ services:
|
|||
- trustgraph
|
||||
restart: unless-stopped
|
||||
|
||||
knowledge-cores:
|
||||
image: trustgraph-ts:local
|
||||
command: ["node", "entrypoints/cores.mjs"]
|
||||
environment:
|
||||
- NATS_URL=nats://nats:4222
|
||||
depends_on:
|
||||
nats:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- trustgraph
|
||||
restart: unless-stopped
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Document Processing Pipeline
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
"id": 1,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "up",
|
||||
"expr": "up{job=~\"prometheus|otel-collector|gateway\"}",
|
||||
"legendFormat": "{{job}}",
|
||||
"refId": "A"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
"type": "prometheus",
|
||||
"uid": "tg-prometheus"
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 0 },
|
||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 },
|
||||
"id": 1,
|
||||
"targets": [
|
||||
{
|
||||
|
|
@ -92,7 +92,7 @@
|
|||
"type": "prometheus",
|
||||
"uid": "tg-prometheus"
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 },
|
||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 },
|
||||
"id": 2,
|
||||
"targets": [
|
||||
{
|
||||
|
|
@ -151,7 +151,7 @@
|
|||
"type": "prometheus",
|
||||
"uid": "tg-prometheus"
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 },
|
||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 },
|
||||
"id": 3,
|
||||
"targets": [
|
||||
{
|
||||
|
|
@ -210,7 +210,7 @@
|
|||
"type": "prometheus",
|
||||
"uid": "tg-prometheus"
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 },
|
||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 },
|
||||
"id": 4,
|
||||
"targets": [
|
||||
{
|
||||
|
|
@ -269,7 +269,7 @@
|
|||
"type": "prometheus",
|
||||
"uid": "tg-prometheus"
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 },
|
||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 },
|
||||
"id": 5,
|
||||
"targets": [
|
||||
{
|
||||
|
|
@ -328,7 +328,7 @@
|
|||
"type": "prometheus",
|
||||
"uid": "tg-prometheus"
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 24 },
|
||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 },
|
||||
"id": 6,
|
||||
"targets": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,13 +13,10 @@ scrape_configs:
|
|||
- targets:
|
||||
- "prometheus:9090"
|
||||
|
||||
# NATS monitoring
|
||||
- job_name: "nats"
|
||||
scrape_interval: 15s
|
||||
metrics_path: "/varz"
|
||||
static_configs:
|
||||
- targets:
|
||||
- "nats:8222"
|
||||
# NATS monitoring (uses nats-prometheus-exporter format)
|
||||
# NATS exposes JSON at /varz, not Prometheus format.
|
||||
# To get proper Prometheus metrics, deploy nats-exporter sidecar.
|
||||
# For now, we rely on NATS healthcheck and JetStream monitoring via /jsz.
|
||||
|
||||
# OpenTelemetry Collector (exposes Prometheus metrics from OTLP pipeline)
|
||||
- job_name: "otel-collector"
|
||||
|
|
@ -28,9 +25,10 @@ scrape_configs:
|
|||
- targets:
|
||||
- "otel-collector:8889"
|
||||
|
||||
# TrustGraph gateway (enabled when gateway container is running)
|
||||
# TrustGraph gateway metrics (prom-client)
|
||||
- job_name: "gateway"
|
||||
scrape_interval: 5s
|
||||
scrape_interval: 15s
|
||||
metrics_path: "/api/v1/metrics"
|
||||
static_configs:
|
||||
- targets:
|
||||
- "gateway:8000"
|
||||
- "gateway:8088"
|
||||
|
|
|
|||
6
ts/entrypoints/cores.mjs
Normal file
6
ts/entrypoints/cores.mjs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import("../packages/flow/dist/cores/service.js")
|
||||
.then((m) => m.run())
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -126,12 +126,13 @@ export interface DocumentEmbeddingsResponse {
|
|||
}
|
||||
|
||||
// Config
|
||||
export type ConfigOperation = "get" | "list" | "delete" | "put" | "config";
|
||||
export type ConfigOperation = "get" | "list" | "delete" | "put" | "config" | "getvalues";
|
||||
|
||||
export interface ConfigRequest {
|
||||
operation: ConfigOperation;
|
||||
keys?: string[];
|
||||
values?: Record<string, unknown>;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface ConfigResponse {
|
||||
|
|
|
|||
|
|
@ -1174,7 +1174,8 @@ export class FlowsApi {
|
|||
string,
|
||||
Record<string, Record<string, string>>
|
||||
>;
|
||||
return JSON.parse(config.config.prompt["template-index"]);
|
||||
const raw = config.config?.prompt?.["template-index"];
|
||||
return raw ? JSON.parse(raw) : [];
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1187,7 +1188,8 @@ export class FlowsApi {
|
|||
string,
|
||||
Record<string, Record<string, string>>
|
||||
>;
|
||||
return JSON.parse(config.config.prompt[`template.${id}`]);
|
||||
const raw = config.config?.prompt?.[`template.${id}`];
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1200,7 +1202,8 @@ export class FlowsApi {
|
|||
string,
|
||||
Record<string, Record<string, string>>
|
||||
>;
|
||||
return JSON.parse(config.config.prompt.system);
|
||||
const raw = config.config?.prompt?.system;
|
||||
return raw ? JSON.parse(raw) : "";
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1546,7 +1549,10 @@ export class FlowApi {
|
|||
60000,
|
||||
undefined,
|
||||
this.flowId,
|
||||
);
|
||||
).catch((err) => {
|
||||
const errorMessage = err instanceof Error ? err.message : err?.toString() || "Unknown error";
|
||||
onError(`Graph RAG request failed: ${errorMessage}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1617,7 +1623,10 @@ export class FlowApi {
|
|||
60000,
|
||||
undefined,
|
||||
this.flowId,
|
||||
);
|
||||
).catch((err) => {
|
||||
const errorMessage = err instanceof Error ? err.message : err?.toString() || "Unknown error";
|
||||
onError(`Document RAG request failed: ${errorMessage}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -129,6 +129,9 @@ export class ConfigService extends AsyncProcessor {
|
|||
case "config":
|
||||
return this.handleConfigDump();
|
||||
|
||||
case "getvalues":
|
||||
return this.handleGetValues(request);
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown config operation: ${op as string}`);
|
||||
}
|
||||
|
|
@ -237,6 +240,22 @@ export class ConfigService extends AsyncProcessor {
|
|||
};
|
||||
}
|
||||
|
||||
private handleGetValues(request: ConfigRequest): ConfigResponse {
|
||||
const type = request.type ?? "";
|
||||
|
||||
const values: { key: string; value: unknown }[] = [];
|
||||
|
||||
for (const [namespace, subMap] of this.store) {
|
||||
if (!type || namespace === type || namespace.startsWith(`${type}.`) || namespace.startsWith(`${type}/`)) {
|
||||
for (const [k, v] of subMap) {
|
||||
values.push({ key: `${namespace}.${k}`, value: v });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { version: this.version, values: values as unknown as Record<string, unknown> };
|
||||
}
|
||||
|
||||
private handleConfigDump(): ConfigResponse {
|
||||
const config: Record<string, unknown> = {};
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,12 @@ export async function createGateway(config: GatewayConfig) {
|
|||
const body = request.body as Record<string, unknown>;
|
||||
|
||||
try {
|
||||
const result = await dispatcher.dispatchGlobalService(kind, body);
|
||||
const result = await dispatcher.dispatchGlobalService(kind, body) as Record<string, unknown>;
|
||||
const err = result?.error as { type?: string; message?: string } | undefined;
|
||||
if (err) {
|
||||
const statusCode = err.type === "not-found" ? 404 : 400;
|
||||
return reply.code(statusCode).send(result);
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
reply.code(500).send({ error: { type: "internal", message: String(err) } });
|
||||
|
|
@ -62,7 +67,12 @@ export async function createGateway(config: GatewayConfig) {
|
|||
const body = request.body as Record<string, unknown>;
|
||||
|
||||
try {
|
||||
const result = await dispatcher.dispatchFlowService(flow, kind, body);
|
||||
const result = await dispatcher.dispatchFlowService(flow, kind, body) as Record<string, unknown>;
|
||||
const err = result?.error as { type?: string; message?: string } | undefined;
|
||||
if (err) {
|
||||
const statusCode = err.type === "not-found" ? 404 : 400;
|
||||
return reply.code(statusCode).send(result);
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
reply.code(500).send({ error: { type: "internal", message: String(err) } });
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ function termToValue(term: Term | undefined): string | null {
|
|||
}
|
||||
|
||||
function createTerm(value: string): Term {
|
||||
if (!value) {
|
||||
return { type: "LITERAL", value: "" };
|
||||
}
|
||||
if (value.startsWith("http://") || value.startsWith("https://")) {
|
||||
return { type: "IRI", iri: value };
|
||||
}
|
||||
|
|
@ -90,11 +93,14 @@ export class FalkorDBTriplesQuery {
|
|||
await this.matchAll(rawTriples, limit);
|
||||
}
|
||||
|
||||
return rawTriples.slice(0, limit).map(([s, p, o]) => ({
|
||||
s: createTerm(s),
|
||||
p: createTerm(p),
|
||||
o: createTerm(o),
|
||||
}));
|
||||
return rawTriples
|
||||
.filter(([s, p, o]) => s != null && p != null && o != null)
|
||||
.slice(0, limit)
|
||||
.map(([s, p, o]) => ({
|
||||
s: createTerm(s),
|
||||
p: createTerm(p),
|
||||
o: createTerm(o),
|
||||
}));
|
||||
}
|
||||
|
||||
private async matchPattern(
|
||||
|
|
|
|||
|
|
@ -16,7 +16,17 @@ server {
|
|||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# WebSocket proxy
|
||||
# WebSocket proxy (client connects to /api/socket, gateway listens on /api/v1/socket)
|
||||
location /api/socket {
|
||||
proxy_pass http://gateway:8088/api/v1/socket;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
|
||||
# WebSocket proxy (direct v1 path)
|
||||
location /api/v1/socket {
|
||||
proxy_pass http://gateway:8088;
|
||||
proxy_http_version 1.1;
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ export function Sidebar() {
|
|||
<NavItem to="/graph" icon={Rotate3d} label="Graph" />
|
||||
<NavItem to="/prompts" icon={MessageCircleCode} label="Prompts" />
|
||||
<NavItem to="/token-cost" icon={Coins} label="Token Cost" />
|
||||
<NavItem to="/knowledge-cores" icon={BrainCircuit} label="Knowledge" />
|
||||
<NavItem to="/knowledge-cores" icon={BrainCircuit} label="Knowledge Cores" />
|
||||
<NavItem to="/flows" icon={Workflow} label="Flows" />
|
||||
<NavItem to="/settings" icon={Settings} label="Settings" />
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback } from "react";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { useSocket } from "@/providers/socket-provider";
|
||||
import {
|
||||
useConversation,
|
||||
|
|
@ -16,6 +16,7 @@ import type { StreamingMetadata } from "@trustgraph/client";
|
|||
|
||||
export interface UseChatReturn {
|
||||
submitMessage: (opts: { input: string }) => void;
|
||||
cancelRequest: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -33,10 +34,32 @@ export function useChat(): UseChatReturn {
|
|||
const addActivity = useProgressStore((s) => s.addActivity);
|
||||
const removeActivity = useProgressStore((s) => s.removeActivity);
|
||||
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
const cancelRequest = useCallback(() => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
updateLastMessage((prev) => ({
|
||||
...prev,
|
||||
content: prev.content || "(Cancelled)",
|
||||
isStreaming: false,
|
||||
activePhase: undefined,
|
||||
}));
|
||||
removeActivity("Chat request");
|
||||
}, [updateLastMessage, removeActivity]);
|
||||
|
||||
const submitMessage = useCallback(
|
||||
({ input }: { input: string }) => {
|
||||
if (!input.trim()) return;
|
||||
|
||||
// Abort any in-flight request
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
abortControllerRef.current = new AbortController();
|
||||
|
||||
const activityLabel = "Chat request";
|
||||
|
||||
// 1. Add the user message
|
||||
|
|
@ -101,6 +124,7 @@ export function useChat(): UseChatReturn {
|
|||
...prev,
|
||||
content: prev.content || `Error: ${error}`,
|
||||
isStreaming: false,
|
||||
activePhase: undefined,
|
||||
}));
|
||||
removeActivity(activityLabel);
|
||||
};
|
||||
|
|
@ -211,5 +235,5 @@ export function useChat(): UseChatReturn {
|
|||
],
|
||||
);
|
||||
|
||||
return { submitMessage };
|
||||
return { submitMessage, cancelRequest };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
ChevronDown,
|
||||
ChevronRight,
|
||||
Loader2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import Markdown from "react-markdown";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
|
@ -189,12 +190,23 @@ export default function ChatPage() {
|
|||
const setInput = useConversation((s) => s.setInput);
|
||||
const setChatMode = useConversation((s) => s.setChatMode);
|
||||
const clearMessages = useConversation((s) => s.clearMessages);
|
||||
const { submitMessage } = useChat();
|
||||
const { submitMessage, cancelRequest } = useChat();
|
||||
const collection = useSettings((s) => s.settings.collection);
|
||||
const isLoading = useProgressStore((s) => s.isLoading);
|
||||
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Elapsed time counter while loading
|
||||
const [elapsed, setElapsed] = useState(0);
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
setElapsed(0);
|
||||
return;
|
||||
}
|
||||
const interval = setInterval(() => setElapsed((e) => e + 1), 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [isLoading]);
|
||||
|
||||
// Auto-scroll to bottom when messages change
|
||||
useEffect(() => {
|
||||
scrollRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
|
|
@ -219,7 +231,7 @@ export default function ChatPage() {
|
|||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<MessageSquareText className="h-6 w-6 text-brand-400" />
|
||||
<h1 className="text-2xl font-bold text-fg">Chat</h1>
|
||||
|
|
@ -228,7 +240,7 @@ export default function ChatPage() {
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{/* Mode selector */}
|
||||
<div className="flex rounded-lg border border-border bg-surface-100 p-0.5">
|
||||
{MODES.map((mode) => (
|
||||
|
|
@ -279,7 +291,14 @@ export default function ChatPage() {
|
|||
{isLoading && (
|
||||
<div className="flex items-center gap-2 pb-2 text-xs text-fg-subtle">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
<span>Processing...</span>
|
||||
<span>Processing... {elapsed}s</span>
|
||||
<button
|
||||
onClick={cancelRequest}
|
||||
className="flex items-center gap-1 rounded-lg px-3 py-1 text-xs text-red-400 hover:bg-surface-200"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ function StartFlowDialog({
|
|||
const [paramsJson, setParamsJson] = useState("{}");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [paramsError, setParamsError] = useState<string | null>(null);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
// Fetch blueprints when dialog opens
|
||||
useEffect(() => {
|
||||
|
|
@ -70,9 +71,13 @@ function StartFlowDialog({
|
|||
setParamsJson("{}");
|
||||
setParamsError(null);
|
||||
setSubmitting(false);
|
||||
setSubmitted(false);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setSubmitted(true);
|
||||
if (!isValid) return;
|
||||
|
||||
let params: Record<string, unknown> = {};
|
||||
try {
|
||||
params = JSON.parse(paramsJson);
|
||||
|
|
@ -140,6 +145,9 @@ function StartFlowDialog({
|
|||
placeholder="my-flow-id"
|
||||
className="w-full rounded-lg border border-border bg-surface-100 px-3 py-2 text-sm text-fg placeholder:text-fg-subtle focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500"
|
||||
/>
|
||||
{submitted && !id.trim() && (
|
||||
<p className="mt-1 text-xs text-red-400">Flow ID is required</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Blueprint name */}
|
||||
|
|
@ -167,6 +175,9 @@ function StartFlowDialog({
|
|||
))}
|
||||
</select>
|
||||
)}
|
||||
{submitted && !blueprint && (
|
||||
<p className="mt-1 text-xs text-red-400">Blueprint is required</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
|
|
@ -181,6 +192,9 @@ function StartFlowDialog({
|
|||
placeholder="Human-readable description"
|
||||
className="w-full rounded-lg border border-border bg-surface-100 px-3 py-2 text-sm text-fg placeholder:text-fg-subtle focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500"
|
||||
/>
|
||||
{submitted && !description.trim() && (
|
||||
<p className="mt-1 text-xs text-red-400">Description is required</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Parameters (JSON) */}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,13 @@ export default function KnowledgeCoresPage() {
|
|||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const ids = await socket.knowledge().getKnowledgeCores();
|
||||
const timeoutPromise = new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Request timed out")), 15000),
|
||||
);
|
||||
const ids = await Promise.race([
|
||||
socket.knowledge().getKnowledgeCores(),
|
||||
timeoutPromise,
|
||||
]);
|
||||
setCores(Array.isArray(ids) ? ids : []);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
|
|
@ -144,13 +150,15 @@ export default function KnowledgeCoresPage() {
|
|||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header */}
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<BrainCircuit className="h-6 w-6 text-brand-400" />
|
||||
<h1 className="text-2xl font-bold text-fg">Knowledge Cores</h1>
|
||||
<span className="ml-2 rounded bg-surface-200 px-2 py-0.5 text-xs text-fg-subtle">
|
||||
{cores.length} core{cores.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
{!loading && (
|
||||
<span className="ml-2 rounded bg-surface-200 px-2 py-0.5 text-xs text-fg-subtle">
|
||||
{cores.length} core{cores.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export default function PromptsPage() {
|
|||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header */}
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<MessageCircleCode className="h-6 w-6 text-brand-400" />
|
||||
<h1 className="text-2xl font-bold text-fg">Prompts</h1>
|
||||
|
|
|
|||
|
|
@ -300,6 +300,9 @@ export default function SettingsPage() {
|
|||
<p className="text-xs text-fg-subtle">
|
||||
Toggle between dark and light mode.
|
||||
</p>
|
||||
<p className="text-xs text-fg-subtle">
|
||||
Currently using {isDark ? "dark" : "light"} mode.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export default function TokenCostPage() {
|
|||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header */}
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Coins className="h-6 w-6 text-brand-400" />
|
||||
<h1 className="text-2xl font-bold text-fg">Token Cost</h1>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
|
|
@ -112,14 +113,20 @@ export function useConnectionState(): ConnectionState {
|
|||
hasApiKey: false,
|
||||
});
|
||||
|
||||
const subscribe = (onStoreChange: () => void) => {
|
||||
return api.onConnectionStateChange((next) => {
|
||||
stateRef.current = next;
|
||||
onStoreChange();
|
||||
});
|
||||
};
|
||||
// subscribe must be stable across renders to prevent useSyncExternalStore
|
||||
// from re-subscribing on every render (which would cause an infinite loop
|
||||
// because onConnectionStateChange immediately calls the listener).
|
||||
const subscribe = useCallback(
|
||||
(onStoreChange: () => void) => {
|
||||
return api.onConnectionStateChange((next) => {
|
||||
stateRef.current = next;
|
||||
onStoreChange();
|
||||
});
|
||||
},
|
||||
[api],
|
||||
);
|
||||
|
||||
const getSnapshot = () => stateRef.current;
|
||||
const getSnapshot = useCallback(() => stateRef.current, []);
|
||||
|
||||
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue