Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
38b20bf
Remove the custom node classifier
ArtyomVancyan Feb 19, 2026
8e5b799
Implement a callback-based tracer
ArtyomVancyan Feb 19, 2026
eee2b4f
Remove the deprecated tests
ArtyomVancyan Feb 19, 2026
1c1299d
Integrate new message handling
ArtyomVancyan Feb 19, 2026
fb460e4
Remove the deprecated web tests
ArtyomVancyan Feb 19, 2026
4fde456
Refactor BroadcastingTracer to simplify logic and improve node handling
ArtyomVancyan Feb 19, 2026
19c6d86
Fix the node kind identification issue
ArtyomVancyan Feb 19, 2026
6eac446
Refactor message handling and previews in BroadcastingTracer
ArtyomVancyan Feb 20, 2026
9acb506
Target ESNext and adjust library settings
ArtyomVancyan Feb 20, 2026
090ad82
Refactor tree computation logic - optimize code and remove redundancies
ArtyomVancyan Feb 20, 2026
760b076
Autoadjust the inspect tree pane size
ArtyomVancyan Feb 20, 2026
650b650
Inline NodeDetail component
ArtyomVancyan Feb 20, 2026
7bc6acf
Apply suggestions from code review
ArtyomVancyan Feb 20, 2026
173c3db
Remove unused properties
ArtyomVancyan Feb 20, 2026
ed20736
Rename SVG files and update NodeKind type to include new node types
ArtyomVancyan Feb 20, 2026
d1d17df
Fix error handling
ArtyomVancyan Feb 20, 2026
d4f4f68
Refactor streamer methods to improve async handling and add synchrono…
ArtyomVancyan Feb 20, 2026
26318db
Send error as output when it's present
ArtyomVancyan Feb 20, 2026
ba7516d
Update focus handling to trigger view fitting on error nodes
ArtyomVancyan Feb 20, 2026
022b2a0
Apply review fixes: recursive tree, correct auto-selection, remove no…
github-actions[bot] Feb 20, 2026
74d9263
Improve tree data handling and optimize child node retrieval
ArtyomVancyan Feb 20, 2026
c58889f
Rollback broken changes of `ainvoke`
ArtyomVancyan Feb 20, 2026
673de53
Add missing node kinds
ArtyomVancyan Feb 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions langgraphics-web/public/icons/chat_model.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 0 additions & 3 deletions langgraphics-web/public/icons/runnable.svg

This file was deleted.

191 changes: 69 additions & 122 deletions langgraphics-web/src/components/InspectPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,131 +1,69 @@
import Tree from "antd/es/tree";
import {type ReactNode} from "react";
import type {Node} from "@xyflow/react";
import type {GraphMessage, NodeData, NodeOutputEntry, NodeStepEntry} from "../types";
import {useInspectTree} from "../hooks/useInspectTree.tsx";
import type {TreeDataNode} from "antd";
import {useCallback, useEffect, useMemo, useState} from "react";
import type {NodeEntry} from "../types";

interface InspectPanelProps {
topology: GraphMessage | null;
nodes: Node<NodeData>[];
nodeOutputLog: NodeOutputEntry[];
nodeStepLog: NodeStepEntry[];
}

function DetailSection({title, children}: { title: string; children: ReactNode }) {
return (
<div className="inspect-detail-section">
<span className="inspect-section-label">{title}</span>
{children}
</div>
);
}
export function InspectPanel({nodeEntries}: { nodeEntries: NodeEntry[] }) {
const [selectedKey, setSelectedKey] = useState<string>("");

function NodeDetail({entry, isStart, isEnd, stepStart, stepEnd}: {
entry: NodeOutputEntry | null;
isStart: boolean;
isEnd: boolean;
stepStart: NodeStepEntry | null;
stepEnd: NodeStepEntry | null;
}) {
if (!entry) return null;
const expandedKeys = useMemo(() => {
return nodeEntries.map(({run_id}) => run_id);
}, [nodeEntries]);

if (stepStart !== null) {
let input = stepStart.data;
let output = stepEnd !== null ? stepEnd.data : stepEnd;
const toString = (d: any) => typeof d === "string" ? d : JSON.stringify(d, null, 2);
if (typeof stepStart.data === "object") {
const messages = stepStart.data.messages;
input = Array.isArray(messages) ? messages[messages.length - 1].content : stepStart.data;
}
if (stepEnd !== null && typeof stepEnd.data === "object") {
const messages = stepEnd.data.messages;
output = Array.isArray(messages) ? messages[messages.length - 1].content : stepEnd.data;
}
return (
<>
<DetailSection title="Input">
<pre className="inspect-detail-json">{toString(input)}</pre>
</DetailSection>
{stepEnd !== null && (
<DetailSection title="Output">
<pre className="inspect-detail-json">{toString(output)}</pre>
</DetailSection>
)}
</>
);
}
const selectedEntry = useMemo(() => {
return nodeEntries.find(({run_id}) => run_id === selectedKey);
}, [nodeEntries, selectedKey]);

if (isStart) {
const allMessages = entry.data.messages ?? [];
const promptMsg = allMessages.find((m) => m.type === "system") ?? allMessages[0];
return (
<DetailSection title="System prompt">
{promptMsg
? <div className="inspect-detail-text">{promptMsg.content as string}</div>
: <pre className="inspect-detail-json">{JSON.stringify(entry.data, null, 2)}</pre>
}
</DetailSection>
);
}
const getChildren = useCallback((parent: NodeEntry) => {
return nodeEntries.filter(({parent_run_id}) => parent_run_id === parent.run_id).map(child => {
const children: TreeDataNode[] = getChildren(child);
return {
children,
selectable: true,
key: child.run_id,
isLeaf: children.length === 0,
title: (
<span className="inspect-step-label">
{child.node_kind
? <img
alt={child.node_kind}
className="inspect-step-icon"
src={`/icons/${child.node_kind}.svg`}
/>
: <span className={`inspect-step-status${child.status === "error" ? " error" : ""}`}/>
}
<span className="inspect-step-name">{child.node_id ?? "step"}</span>
</span>
),
}
})
}, [nodeEntries])

if (isEnd) {
const allMessages = entry.data.messages ?? [];
const lastMsg = allMessages.length > 0 ? allMessages[allMessages.length - 1] : null;
return (
<DetailSection title="Final answer">
{lastMsg
? <div className="inspect-detail-text">{lastMsg.content as string}</div>
: <pre className="inspect-detail-json">{JSON.stringify(entry.data, null, 2)}</pre>
}
</DetailSection>
);
}
const treeData = useMemo((): TreeDataNode[] => {
return nodeEntries.filter(({parent_run_id}) => !parent_run_id).map(entry => ({
key: entry.run_id,
children: getChildren(entry),
title: (
<span className="inspect-node-label">
{entry.node_kind && <img src={`/icons/${entry.node_kind}.svg`} alt={entry.node_kind}/>}
{entry.node_id}
</span>
),
}))
}, [nodeEntries, getChildren]);

const outputMessages = entry.data.messages ?? [];
const inputMessages = entry.input?.messages ?? [];
const lastInput = inputMessages.length > 0 ? inputMessages.slice(-1) : null;

return (
<>
{entry.input !== null && (
<DetailSection title="Input">
{lastInput
? lastInput.map((msg, i) => <div key={i}
className="inspect-detail-text">{msg.content as string}</div>)
: <pre className="inspect-detail-json">{JSON.stringify(entry.input, null, 2)}</pre>
}
</DetailSection>
)}
<DetailSection title="Output">
{outputMessages.length > 0
? outputMessages.map((msg, i) => (
<div key={i} className="inspect-detail-text">
{msg.content as string}
</div>
))
: Object.keys(entry.data).length > 0
? <pre className="inspect-detail-json">{JSON.stringify(entry.data, null, 2)}</pre>
: null
}
</DetailSection>
</>
);
}

export function InspectPanel({topology, nodes, nodeOutputLog, nodeStepLog}: InspectPanelProps) {
const {
treeData, expandedKeys, visibleLog,
selectedKey, setSelectedKey,
selectedEntry, selectedMeta,
stepStart, stepEnd,
} = useInspectTree(topology, nodes, nodeOutputLog, nodeStepLog);
useEffect(() => {
if (nodeEntries.length > 0 && !selectedKey) {
setSelectedKey(nodeEntries.find(e => !e.parent_run_id)?.run_id ?? nodeEntries[0].run_id);
}
}, [nodeEntries, selectedKey]);

return (
<div className="inspect-panel">
<div className="inspect-panel-header">Trace Inspector</div>
<div className="inspect-panel-body">
<div className="inspect-tree-pane">
{visibleLog.length !== 0 && (
{nodeEntries.length !== 0 && (
<Tree
switcherIcon={<span className="ant-tree-switcher-leaf-line"/>}
onSelect={([key]) => key && setSelectedKey(key as string)}
Expand All @@ -138,13 +76,22 @@ export function InspectPanel({topology, nodes, nodeOutputLog, nodeStepLog}: Insp
)}
</div>
<div className="inspect-detail-pane">
<NodeDetail
isStart={selectedMeta?.isStart ?? false}
isEnd={selectedMeta?.isEnd ?? false}
entry={selectedEntry}
stepStart={stepStart}
stepEnd={stepEnd}
/>
{selectedEntry && (
<>
{selectedEntry.input && (
<div className="inspect-detail-section">
<span className="inspect-section-label">Input</span>
<div className="inspect-detail-text">{selectedEntry.input}</div>
</div>
)}
{selectedEntry.output && (
<div className="inspect-detail-section">
<span className="inspect-section-label">Output</span>
<div className="inspect-detail-text">{selectedEntry.output}</div>
</div>
)}
</>
)}
</div>
</div>
</div>
Expand Down
4 changes: 3 additions & 1 deletion langgraphics-web/src/hooks/useFocus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ export function useFocus({nodes, edges, activeNodeId, rankDir = "TB"}: UseFocusO

if (mode !== "auto") return;

if (activeNodeId && activeNodeId !== prevFocusId.current) {
if (nodes.some((n) => n.className === "error")) {
fitView({duration: FIT_VIEW_DURATION}).then();
} else if (activeNodeId && activeNodeId !== prevFocusId.current) {
prevFocusId.current = activeNodeId;

const activeNode = nodes.find((n) => n.id === activeNodeId);
Expand Down
Loading
Loading