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
|
|
@ -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