Skip to content

Commit 8a1f678

Browse files
Implement modes for inspect panel (GH-7)
2 parents b3fbed6 + d6be4da commit 8a1f678

File tree

7 files changed

+170
-113
lines changed

7 files changed

+170
-113
lines changed

langgraphics-web/src/components/Controls.tsx

Lines changed: 82 additions & 45 deletions
Large diffs are not rendered by default.

langgraphics-web/src/components/GraphCanvas.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {Background, type ColorMode, type Edge, type Node, type NodeTypes, ReactF
33
import {Controls} from "./Controls";
44
import {CustomNode} from "./CustomNode";
55
import {useFocus} from "../hooks/useFocus";
6-
import type {EdgeData, ExecutionEvent, NodeData} from "../types";
6+
import type {EdgeData, ExecutionEvent, InspectorMode, NodeData, ViewMode} from "../types";
77
import type {RankDir} from "../layout";
88

99
const nodeTypes: NodeTypes = {custom: CustomNode as NodeTypes[string]};
@@ -14,15 +14,18 @@ interface GraphCanvasProps {
1414
events: ExecutionEvent[];
1515
activeNodeId: string | null;
1616
inspect: ReactNode;
17+
initialMode: ViewMode;
1718
initialRankDir?: RankDir;
1819
initialColorMode?: ColorMode;
20+
initialInspect?: InspectorMode;
1921
onRankDirChange?: (v: RankDir) => void;
2022
}
2123

22-
export function GraphCanvas({nodes, edges, events, activeNodeId, inspect, initialColorMode = "system", initialRankDir = "TB", onRankDirChange}: GraphCanvasProps) {
24+
export function GraphCanvas({nodes, edges, events, activeNodeId, inspect, initialMode = "auto", initialInspect = "off", initialColorMode = "system", initialRankDir = "TB", onRankDirChange}: GraphCanvasProps) {
2325
const [rankDir, setRankDir] = useState<RankDir>(initialRankDir);
2426
const [colorMode, setColorMode] = useState<ColorMode>(initialColorMode);
25-
const {isManual, goAuto, goManual, fitContent} = useFocus({nodes, edges, activeNodeId, rankDir});
27+
const [inspectorMode, setInspectorMode] = useState<InspectorMode>(initialInspect);
28+
const {isManual, goAuto, goManual, fitContent} = useFocus({nodes, edges, activeNodeId, rankDir, initialMode});
2629

2730
const handleRankDirChange = useCallback(async (v: RankDir) => {
2831
setRankDir(v);
@@ -43,6 +46,7 @@ export function GraphCanvas({nodes, edges, events, activeNodeId, inspect, initia
4346
colorMode={colorMode}
4447
nodeTypes={nodeTypes}
4548
proOptions={{hideAttribution: true}}
49+
className={`inspector-${inspectorMode}`}
4650
zoomOnDoubleClick={false} nodesDraggable={false}
4751
nodesConnectable={false} elementsSelectable={false}
4852
panOnDrag={isManual} zoomOnScroll={isManual} zoomOnPinch={isManual}
@@ -53,11 +57,16 @@ export function GraphCanvas({nodes, edges, events, activeNodeId, inspect, initia
5357
goManual={goManual}
5458
isManual={isManual}
5559
colorMode={colorMode}
60+
fitContent={fitContent}
5661
setColorMode={setColorMode}
62+
inspectorMode={inspectorMode}
5763
setRankDir={handleRankDirChange}
64+
setInspectorMode={setInspectorMode}
5865
/>
5966
<Background/>
60-
{inspect}
67+
<div className={`inspect-wrapper-${inspectorMode}`}>
68+
{inspect}
69+
</div>
6170
</ReactFlow>
6271
);
6372
}

langgraphics-web/src/hooks/useFocus.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import {useCallback, useEffect, useMemo, useRef, useState} from "react";
22
import type {Edge, Node} from "@xyflow/react";
33
import {useReactFlow} from "@xyflow/react";
4-
import type {EdgeData, NodeData} from "../types";
4+
import type {EdgeData, NodeData, ViewMode} from "../types";
55
import type {RankDir} from "../layout";
66

77
interface UseFocusOptions {
88
nodes: Node<NodeData>[];
99
edges: Edge<EdgeData>[];
1010
activeNodeId: string | null;
1111
rankDir?: RankDir;
12+
initialMode?: ViewMode;
1213
}
1314

1415
const FIT_VIEW_DURATION = 1500;
@@ -36,9 +37,9 @@ function getNeighbourIds(nodeId: string, nodes: Node<NodeData>[], edges: Edge<Ed
3637
return [before?.id, after?.id].filter((id): id is string => id !== undefined).map((id) => ({id}));
3738
}
3839

39-
export function useFocus({nodes, edges, activeNodeId, rankDir = "TB"}: UseFocusOptions) {
40+
export function useFocus({nodes, edges, activeNodeId, rankDir = "TB", initialMode = "auto"}: UseFocusOptions) {
4041
const {fitView} = useReactFlow();
41-
const [mode, setMode] = useState<"auto" | "manual">("auto");
42+
const [mode, setMode] = useState<"auto" | "manual">(initialMode);
4243
const prevMode = useRef<"auto" | "manual">(mode);
4344
const initialDone = useRef(false);
4445
const prevFocusId = useRef<string | null>(null);

langgraphics-web/src/index.css

Lines changed: 46 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ html, body, #root {
5858
width: 50% !important;
5959
}
6060

61+
.react-flow.inspector-tree .react-flow__renderer {
62+
width: calc(100% - 220px) !important;
63+
}
64+
65+
.react-flow.inspector-off .react-flow__renderer {
66+
width: 100% !important;
67+
}
68+
6169
@keyframes node-pulse {
6270
0% {
6371
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.4);
@@ -115,83 +123,51 @@ html, body, #root {
115123
right: calc(50% + 10px);
116124
}
117125

126+
.react-flow.inspector-tree .canvas-controls {
127+
right: calc(220px + 10px);
128+
}
129+
130+
.react-flow.inspector-off .canvas-controls {
131+
right: 10px;
132+
}
133+
118134
.mode-toggle {
119135
display: flex;
120136
overflow: hidden;
121137
border-radius: 2px;
138+
border: 1px solid #3b82f6;
122139
}
123140

124141
.mode-toggle button {
125142
flex: 1;
143+
border: none;
144+
display: flex;
126145
color: #3b82f6;
127146
cursor: pointer;
128147
font-size: 11px;
129148
font-weight: 400;
130149
padding: 4px 10px;
131-
border: 1px solid #3b82f6;
150+
align-items: center;
151+
justify-content: center;
132152
background: var(--xy-node-background-color-default);
133153
transition: opacity 0.15s, background 0.15s, color 0.15s, border-color 0.15s;
134154
}
135155

156+
.mode-toggle button:not(:last-child) {
157+
border-right: 1px solid #3b82f6;
158+
}
159+
136160
.mode-toggle button.active {
137161
background: #3b82f6;
138162
color: #fff;
139163
}
140164

141-
.theme-select {
142-
width: 100%;
143-
position: relative;
144-
}
145-
146-
.theme-select-trigger {
147-
width: 100%;
148-
color: #3b82f6;
149-
cursor: pointer;
150-
font-size: 11px;
151-
font-weight: 400;
152-
text-align: left;
153-
padding: 4px 10px;
154-
border-radius: 2px;
155-
border: 1px solid #3b82f6;
156-
background: var(--xy-node-background-color-default);
157-
transition: opacity 0.15s, background 0.15s, color 0.15s, border-color 0.15s;
158-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%233b82f6'/%3E%3C/svg%3E");
159-
background-repeat: no-repeat;
160-
background-position: right 8px center;
161-
}
162-
163-
.theme-select-popup {
164-
left: 0;
165-
right: 0;
166-
display: flex;
167-
overflow: hidden;
168-
position: absolute;
169-
border-radius: 2px;
170-
top: calc(100% + 4px);
171-
flex-direction: column;
172-
border: 1px solid #3b82f6;
173-
background: var(--xy-node-background-color-default);
174-
}
175-
176-
.theme-select-popup button {
177-
border: none;
178-
color: #3b82f6;
179-
cursor: pointer;
180-
font-size: 11px;
181-
font-weight: 400;
182-
text-align: left;
183-
padding: 4px 10px;
184-
background: transparent;
185-
transition: background 0.1s;
186-
}
187-
188-
.theme-select-popup button:hover {
189-
background: rgba(59, 130, 246, 0.25);
165+
.mode-toggle button svg {
166+
fill: #3b82f6;
190167
}
191168

192-
.theme-select-popup button.selected {
193-
background: #3b82f6;
194-
color: #fff;
169+
.mode-toggle button.active svg {
170+
fill: #fff;
195171
}
196172

197173
.inspect-panel {
@@ -378,3 +354,20 @@ html, body, #root {
378354
.inspect-detail-section.chat_model .inspect-detail-text:not(.error) {
379355
color: #4f46e5cc;
380356
}
357+
358+
.inspect-wrapper-off {
359+
opacity: 0;
360+
pointer-events: none;
361+
}
362+
363+
.inspect-wrapper-tree .inspect-detail-pane {
364+
display: none;
365+
}
366+
367+
.inspect-wrapper-tree .inspect-panel {
368+
width: 220px;
369+
}
370+
371+
.inspect-wrapper-tree .inspect-tree-pane {
372+
border-right: none;
373+
}

langgraphics-web/src/main.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,31 @@ import {useWebSocket} from "./hooks/useWebSocket";
66
import {useGraphState} from "./hooks/useGraphState";
77
import {GraphCanvas} from "./components/GraphCanvas";
88
import {InspectPanel} from "./components/InspectPanel";
9+
import type {ViewMode, InspectorMode} from "./types.ts";
910
import type {RankDir} from "./layout";
1011
import "@xyflow/react/dist/style.css";
1112
import "./index.css";
1213

1314
const WS_URL = "ws://localhost:8765";
1415

15-
function parseParams(): {colorMode: ColorMode; rankDir: RankDir} {
16+
function parseParams(): {theme: ColorMode; direction: RankDir, mode: ViewMode, inspect: InspectorMode} {
1617
const p = new URLSearchParams(window.location.search);
18+
const mode = p.get("mode") ?? "auto";
1719
const theme = p.get("theme") ?? "system";
20+
const inspect = p.get("inspect") ?? "off";
1821
const direction = p.get("direction") ?? "TB";
1922
return {
20-
colorMode: (["system", "light", "dark"].includes(theme) ? theme : "system") as ColorMode,
21-
rankDir: (["TB", "LR"].includes(direction) ? direction : "TB") as RankDir,
23+
inspect: (["off", "tree", "full"].includes(inspect) ? inspect : "off") as InspectorMode,
24+
theme: (["system", "light", "dark"].includes(theme) ? theme : "system") as ColorMode,
25+
direction: (["TB", "LR"].includes(direction) ? direction : "TB") as RankDir,
26+
mode: (["auto", "manual"].includes(mode) ? mode : "auto") as ViewMode,
2227
};
2328
}
2429

25-
const {colorMode: initialColorMode, rankDir: initialRankDir} = parseParams();
30+
const {theme, mode, inspect, direction} = parseParams();
2631

2732
function Index() {
28-
const [rankDir, setRankDir] = useState<RankDir>(initialRankDir);
33+
const [rankDir, setRankDir] = useState<RankDir>(direction);
2934
const {topology, events, nodeEntries} = useWebSocket(WS_URL);
3035
const {nodes, edges, activeNodeId} = useGraphState(topology, events, rankDir);
3136

@@ -35,10 +40,12 @@ function Index() {
3540
nodes={nodes}
3641
edges={edges}
3742
events={events}
43+
initialMode={mode}
44+
initialInspect={inspect}
45+
initialColorMode={theme}
46+
initialRankDir={direction}
3847
activeNodeId={activeNodeId}
3948
onRankDirChange={setRankDir}
40-
initialRankDir={initialRankDir}
41-
initialColorMode={initialColorMode}
4249
inspect={<InspectPanel nodeEntries={nodeEntries}/>}
4350
/>
4451
</ReactFlowProvider>

langgraphics-web/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import {Position} from "@xyflow/react";
22

3+
export type ViewMode = "auto" | "manual";
4+
export type InspectorMode = "off" | "tree" | "full";
5+
36
export type NodeStatus = "idle" | "active" | "completed" | "error";
47
export type EdgeStatus = "idle" | "active" | "traversed" | "error";
58

langgraphics/watch.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ def watch(
4444
ws_port: int = 8765,
4545
open_browser: bool = True,
4646
direction: Literal["TB", "LR"] = "TB",
47+
mode: Literal["auto", "manual"] = "auto",
48+
inspect: Literal["off", "tree", "full"] = "off",
4749
theme: Literal["system", "dark", "light"] = "system",
4850
) -> Viewport:
4951
topology = extract(graph)
@@ -54,7 +56,12 @@ def watch(
5456
start_ws_server(manager, host, ws_port)
5557

5658
if open_browser:
57-
defaults = (("theme", theme, "system"), ("direction", direction, "TB"))
59+
defaults = (
60+
("mode", mode, "auto"),
61+
("theme", theme, "system"),
62+
("inspect", inspect, "off"),
63+
("direction", direction, "TB"),
64+
)
5865
params = [f"{k}={v}" for k, v, default in defaults if v != default]
5966
query = ("?" + "&".join(params)) if params else ""
6067
webbrowser.open(f"http://{host}:{port}{query}")

0 commit comments

Comments
 (0)