Skip to content

Commit e84b8f7

Browse files
authored
Merge pull request #348 from traceroot-ai/xinwei_agent_ui_improve_v3
[UI][Agent] Make the UI of agent chat more aligned with the trace and log section [1/n]
2 parents 99759b9 + 699b9b9 commit e84b8f7

File tree

9 files changed

+279
-270
lines changed

9 files changed

+279
-270
lines changed

ui/src/app/explore/page.tsx

Lines changed: 75 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,40 @@ import AgentPanel from "@/components/agent-panel/AgentPanel";
1616
import { Span, Trace as TraceType } from "@/models/trace";
1717
import { initializeProviders } from "@/utils/provider";
1818

19+
// Custom hook for persistent state with localStorage
20+
function usePersistentState<T>(
21+
key: string,
22+
defaultValue: T,
23+
): [T, (value: T | ((prev: T) => T)) => void] {
24+
const [state, setState] = useState<T>(() => {
25+
if (typeof window === "undefined") return defaultValue;
26+
try {
27+
const saved = localStorage.getItem(key);
28+
return saved !== null ? JSON.parse(saved) : defaultValue;
29+
} catch {
30+
return defaultValue;
31+
}
32+
});
33+
34+
const setPersistentState = useCallback(
35+
(value: T | ((prev: T) => T)) => {
36+
setState((prev) => {
37+
const newValue =
38+
typeof value === "function" ? (value as (prev: T) => T)(prev) : value;
39+
try {
40+
localStorage.setItem(key, JSON.stringify(newValue));
41+
} catch {
42+
// Ignore localStorage errors
43+
}
44+
return newValue;
45+
});
46+
},
47+
[key],
48+
);
49+
50+
return [state, setPersistentState];
51+
}
52+
1953
export default function Explore() {
2054
const [selectedTraceIds, setSelectedTraceIds] = useState<string[]>([]);
2155
const [selectedSpanIds, setSelectedSpanIds] = useState<string[]>([]);
@@ -39,6 +73,13 @@ export default function Explore() {
3973
const [loading, setLoading] = useState(true);
4074
const [hasTraceIdInUrl, setHasTraceIdInUrl] = useState(false);
4175

76+
// Agent panel state - persisted to localStorage
77+
const [agentOpen, setAgentOpen] = usePersistentState("agentPanelOpen", false);
78+
79+
const handleAgentToggle = useCallback(() => {
80+
setAgentOpen((prev) => !prev);
81+
}, [setAgentOpen]);
82+
4283
// Initialize providers
4384
useEffect(() => {
4485
initializeProviders();
@@ -181,32 +222,38 @@ export default function Explore() {
181222

182223
// TODO (xinwei): Add ProtectedRoute
183224
return (
184-
<AgentPanel
185-
traceId={selectedTraceIds.length === 1 ? selectedTraceIds[0] : undefined}
186-
traceIds={selectedTraceIds}
187-
spanIds={selectedSpanIds}
188-
queryStartTime={timeRange?.start}
189-
queryEndTime={timeRange?.end}
190-
onSpanSelect={(spanId) => handleSpanSelect([spanId])}
191-
>
192-
<div className="h-screen flex flex-col">
193-
<ExploreHeader
194-
onSearch={handleSearch}
195-
onClearSearch={handleClearSearch}
196-
onLogSearchValueChange={handleLogSearchValueChange}
197-
onMetadataSearchTermsChange={handleMetadataSearchTermsChange}
198-
searchDisabled={loading || hasTraceIdInUrl}
199-
selectedTimeRange={selectedTimeRange}
200-
onTimeRangeSelect={handleTimeRangeSelect}
201-
onCustomTimeRangeSelect={handleCustomTimeRangeSelect}
202-
currentTimezone={timezone}
203-
timeDisabled={loading || hasTraceIdInUrl}
204-
onRefresh={handleRefresh}
205-
refreshDisabled={loading || hasTraceIdInUrl}
206-
viewType={viewType}
207-
onViewTypeChange={setViewType}
208-
/>
209-
<div className="flex-1 overflow-hidden">
225+
<div className="h-screen flex flex-col">
226+
<ExploreHeader
227+
onSearch={handleSearch}
228+
onClearSearch={handleClearSearch}
229+
onLogSearchValueChange={handleLogSearchValueChange}
230+
onMetadataSearchTermsChange={handleMetadataSearchTermsChange}
231+
searchDisabled={loading || hasTraceIdInUrl}
232+
selectedTimeRange={selectedTimeRange}
233+
onTimeRangeSelect={handleTimeRangeSelect}
234+
onCustomTimeRangeSelect={handleCustomTimeRangeSelect}
235+
currentTimezone={timezone}
236+
timeDisabled={loading || hasTraceIdInUrl}
237+
onRefresh={handleRefresh}
238+
refreshDisabled={loading || hasTraceIdInUrl}
239+
viewType={viewType}
240+
onViewTypeChange={setViewType}
241+
agentOpen={agentOpen}
242+
onAgentToggle={handleAgentToggle}
243+
/>
244+
<div className="flex-1 overflow-hidden">
245+
<AgentPanel
246+
traceId={
247+
selectedTraceIds.length === 1 ? selectedTraceIds[0] : undefined
248+
}
249+
traceIds={selectedTraceIds}
250+
spanIds={selectedSpanIds}
251+
queryStartTime={timeRange?.start}
252+
queryEndTime={timeRange?.end}
253+
onSpanSelect={(spanId) => handleSpanSelect([spanId])}
254+
isOpen={agentOpen}
255+
onToggle={handleAgentToggle}
256+
>
210257
<ResizablePanel
211258
leftPanel={
212259
<Trace
@@ -243,8 +290,8 @@ export default function Explore() {
243290
maxLeftWidth={60}
244291
defaultLeftWidth={46}
245292
/>
246-
</div>
293+
</AgentPanel>
247294
</div>
248-
</AgentPanel>
295+
</div>
249296
);
250297
}

ui/src/components/agent-panel/AgentPanel.tsx

Lines changed: 14 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
"use client";
22

33
import React, { useState, useRef, useEffect } from "react";
4-
import { RiRobot2Line } from "react-icons/ri";
5-
import { X } from "lucide-react";
64
import Agent from "@/components/right-panel/agent/Agent";
75

86
interface AgentPanelProps {
@@ -13,6 +11,8 @@ interface AgentPanelProps {
1311
queryEndTime?: Date;
1412
onSpanSelect?: (spanId: string) => void;
1513
onViewTypeChange?: (viewType: "log" | "trace") => void;
14+
isOpen: boolean;
15+
onToggle: () => void;
1616
children: React.ReactNode;
1717
}
1818

@@ -24,33 +24,34 @@ export default function AgentPanel({
2424
queryEndTime,
2525
onSpanSelect,
2626
onViewTypeChange,
27+
isOpen,
28+
onToggle,
2729
children,
2830
}: AgentPanelProps) {
29-
const [isOpen, setIsOpen] = useState(false);
30-
const [isHovered, setIsHovered] = useState(false);
3131
const [panelWidth, setPanelWidth] = useState(30); // Track panel width percentage (default 30%)
3232
const [hasBeenOpened, setHasBeenOpened] = useState(false); // Track if panel has ever been opened
3333
const panelRef = useRef<HTMLDivElement>(null);
3434

3535
const MIN_WIDTH = 20;
3636
const MAX_WIDTH = 35;
3737

38-
// Ensure panel stays closed unless explicitly opened
38+
// Track when panel has been opened at least once
3939
useEffect(() => {
40-
// Do NOT auto-open when traceId changes
41-
// Panel should only open via user click on the robot icon
42-
}, [traceId, traceIds]);
40+
if (isOpen && !hasBeenOpened) {
41+
setHasBeenOpened(true);
42+
}
43+
}, [isOpen, hasBeenOpened]);
4344

4445
// Handle ESC key to close panel
4546
useEffect(() => {
4647
const handleEsc = (e: KeyboardEvent) => {
4748
if (e.key === "Escape" && isOpen) {
48-
setIsOpen(false);
49+
onToggle();
4950
}
5051
};
5152
window.addEventListener("keydown", handleEsc);
5253
return () => window.removeEventListener("keydown", handleEsc);
53-
}, [isOpen]);
54+
}, [isOpen, onToggle]);
5455

5556
// Handle Cmd+Shift+I (Mac) or Ctrl+Shift+I (Windows) to toggle panel
5657
useEffect(() => {
@@ -63,39 +64,13 @@ export default function AgentPanel({
6364
) {
6465
e.preventDefault();
6566
e.stopPropagation();
66-
if (!isOpen) {
67-
setHasBeenOpened(true);
68-
}
69-
setIsOpen((prev) => !prev);
67+
onToggle();
7068
}
7169
};
7270
window.addEventListener("keydown", handleToggleShortcut, true); // Use capture phase
7371
return () =>
7472
window.removeEventListener("keydown", handleToggleShortcut, true);
75-
}, [isOpen]);
76-
77-
// Calculate the icon position based on panel width when open
78-
useEffect(() => {
79-
if (isOpen && panelRef.current) {
80-
const updateIconPosition = () => {
81-
const windowWidth = window.innerWidth;
82-
const panelPixelWidth = (windowWidth * panelWidth) / 100;
83-
setPanelWidth(panelWidth);
84-
};
85-
updateIconPosition();
86-
window.addEventListener("resize", updateIconPosition);
87-
return () => window.removeEventListener("resize", updateIconPosition);
88-
}
89-
}, [isOpen, panelWidth]);
90-
91-
// Calculate icon right position: when closed = 0 (50% hidden), when open = at panel left edge
92-
const getIconRightPosition = () => {
93-
if (!isOpen) {
94-
return isHovered ? "4px" : "-12px"; // Half-hidden when closed, centered on edge
95-
}
96-
// When open, position at the panel's left edge (border)
97-
return `calc(${panelWidth}% - 16px)`; // Centered on the border line
98-
};
73+
}, [onToggle]);
9974

10075
return (
10176
<div className="relative w-full h-full flex overflow-hidden">
@@ -119,7 +94,7 @@ export default function AgentPanel({
11994
ref={panelRef}
12095
>
12196
{/* Agent content area */}
122-
<div className="h-full overflow-hidden pt-2">
97+
<div className="h-full overflow-hidden pt-1">
12398
{/* Only render Agent when opened at least once */}
12499
{hasBeenOpened && (
125100
<Agent
@@ -171,20 +146,6 @@ export default function AgentPanel({
171146
}}
172147
/>
173148
)}
174-
175-
{/* Robot icon trigger - only visible when closed */}
176-
{!isOpen && (
177-
<div
178-
className="fixed bottom-4 right-4 z-50 cursor-pointer"
179-
onClick={(e) => {
180-
e.stopPropagation();
181-
setHasBeenOpened(true);
182-
setIsOpen(true);
183-
}}
184-
>
185-
<RiRobot2Line className="h-7 w-7 text-black dark:text-white drop-shadow-lg" />
186-
</div>
187-
)}
188149
</div>
189150
);
190151
}

ui/src/components/explore/ExploreHeader.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ interface ExploreHeaderProps {
3434
// Mode toggle props
3535
viewType: ViewType;
3636
onViewTypeChange: (type: ViewType) => void;
37+
38+
// Agent panel props
39+
agentOpen?: boolean;
40+
onAgentToggle?: () => void;
3741
}
3842

3943
export default function ExploreHeader({
@@ -51,6 +55,8 @@ export default function ExploreHeader({
5155
refreshDisabled = false,
5256
viewType,
5357
onViewTypeChange,
58+
agentOpen,
59+
onAgentToggle,
5460
}: ExploreHeaderProps) {
5561
return (
5662
<div className="sticky top-0 z-10 bg-white dark:bg-zinc-950 pt-1 pl-6 pr-2 pb-1 border-b border-zinc-200 dark:border-zinc-700">
@@ -73,7 +79,12 @@ export default function ExploreHeader({
7379
currentTimezone={currentTimezone}
7480
disabled={timeDisabled}
7581
/>
76-
<ModeToggle viewType={viewType} onViewTypeChange={onViewTypeChange} />
82+
<ModeToggle
83+
viewType={viewType}
84+
onViewTypeChange={onViewTypeChange}
85+
agentOpen={agentOpen}
86+
onAgentToggle={onAgentToggle}
87+
/>
7788
</div>
7889
</div>
7990
</div>

ui/src/components/right-panel/ModeToggle.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,27 @@
22

33
import React from "react";
44
import { Telescope, FileCode2 } from "lucide-react";
5+
import { RiRobot2Line } from "react-icons/ri";
56
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
7+
import { Toggle } from "@/components/ui/toggle";
68

79
export type ViewType = "log" | "trace";
810

911
interface ModeToggleProps {
1012
viewType: ViewType;
1113
onViewTypeChange: (type: ViewType) => void;
14+
agentOpen?: boolean;
15+
onAgentToggle?: () => void;
1216
}
1317

1418
export default function ModeToggle({
1519
viewType,
1620
onViewTypeChange,
21+
agentOpen = false,
22+
onAgentToggle,
1723
}: ModeToggleProps) {
1824
return (
19-
<div className="flex justify-end p-4">
25+
<div className="flex justify-end items-center p-4 gap-3">
2026
<ToggleGroup
2127
type="single"
2228
value={viewType}
@@ -33,6 +39,17 @@ export default function ModeToggle({
3339
<Telescope size={22} />
3440
</ToggleGroupItem>
3541
</ToggleGroup>
42+
{onAgentToggle && (
43+
<Toggle
44+
pressed={agentOpen}
45+
onPressedChange={onAgentToggle}
46+
aria-label="Toggle agent panel"
47+
variant="outline"
48+
size="lg"
49+
>
50+
<RiRobot2Line size={22} />
51+
</Toggle>
52+
)}
3653
</div>
3754
);
3855
}

0 commit comments

Comments
 (0)