@optilogic/chat 1.0.0-beta.10 → 1.0.0-beta.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +616 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +223 -3
- package/dist/index.d.ts +223 -3
- package/dist/index.js +610 -18
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/agent-response/AgentResponse.tsx +31 -13
- package/src/components/agent-response/components/ThinkingSection.tsx +1 -1
- package/src/components/agent-response/hooks/useAgentResponseAccumulator.ts +65 -8
- package/src/components/agent-response/index.ts +17 -0
- package/src/components/agent-response/types.ts +61 -1
- package/src/components/agent-timeline/AgentTimeline.tsx +256 -0
- package/src/components/agent-timeline/TimelineAgentBlock.tsx +84 -0
- package/src/components/agent-timeline/TimelineItem.tsx +97 -0
- package/src/components/agent-timeline/index.ts +14 -0
- package/src/components/agent-timeline/types.ts +49 -0
- package/src/components/agent-timeline/utils.ts +189 -0
- package/src/components/hitl-interactions/HITLQuestionPanel.tsx +9 -2
- package/src/components/hitl-interactions/index.ts +1 -1
- package/src/components/inline-actions/ActionMarkdownRenderer.tsx +60 -0
- package/src/components/inline-actions/index.ts +18 -0
- package/src/components/inline-actions/parseResponseSegments.ts +66 -0
- package/src/components/inline-actions/prompts.ts +41 -0
- package/src/components/inline-actions/types.ts +57 -0
- package/src/index.ts +27 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optilogic/chat",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.12",
|
|
4
4
|
"description": "Chat UI components for Optilogic - AgentResponse and related components for LLM interactions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"README.md"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@optilogic/core": "1.0.0-beta.
|
|
28
|
-
"@optilogic/editor": "1.0.0-beta.
|
|
27
|
+
"@optilogic/core": "1.0.0-beta.12",
|
|
28
|
+
"@optilogic/editor": "1.0.0-beta.12"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import * as React from "react";
|
|
9
|
-
import { useState, useMemo, useCallback } from "react";
|
|
9
|
+
import { useState, useRef, useMemo, useCallback } from "react";
|
|
10
10
|
import { cn } from "@optilogic/core";
|
|
11
11
|
import { MetadataRow, ThinkingSection, ActionBar, HITLSection } from "./components";
|
|
12
12
|
import { useThinkingTimer } from "./hooks";
|
|
13
13
|
import type { AgentResponseState, FeedbackValue } from "./types";
|
|
14
14
|
import type { HITLInteraction } from "../hitl-interactions";
|
|
15
|
+
import { AgentTimeline, createTimelineUIState } from "../agent-timeline";
|
|
16
|
+
import type { TimelineUIState } from "../agent-timeline";
|
|
15
17
|
|
|
16
18
|
export interface AgentResponseProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
17
19
|
/** The response state to render */
|
|
@@ -147,6 +149,9 @@ const AgentResponse = React.forwardRef<HTMLDivElement, AgentResponseProps>(
|
|
|
147
149
|
},
|
|
148
150
|
ref
|
|
149
151
|
) => {
|
|
152
|
+
// Ref-backed timeline UI state (survives remounts during streaming)
|
|
153
|
+
const timelineUIStateRef = useRef<TimelineUIState>(createTimelineUIState());
|
|
154
|
+
|
|
150
155
|
// Uncontrolled thinking expanded state
|
|
151
156
|
const [uncontrolledExpanded, setUncontrolledExpanded] = useState(defaultThinkingExpanded);
|
|
152
157
|
|
|
@@ -182,9 +187,10 @@ const AgentResponse = React.forwardRef<HTMLDivElement, AgentResponseProps>(
|
|
|
182
187
|
return (state.responseCompleteTime - state.firstMessageTime) / 1000;
|
|
183
188
|
}, [state.firstMessageTime, state.responseCompleteTime]);
|
|
184
189
|
|
|
185
|
-
// Check if we have any thinking content (plain text or
|
|
190
|
+
// Check if we have any thinking content (plain text, structured, or timeline)
|
|
191
|
+
const hasTimelineEntries = !!(state.timelineEntries && state.timelineEntries.length > 0);
|
|
186
192
|
const hasThinkingContent =
|
|
187
|
-
!!state.thinking || (state.thinkingSteps && state.thinkingSteps.length > 0) || false;
|
|
193
|
+
!!state.thinking || (state.thinkingSteps && state.thinkingSteps.length > 0) || hasTimelineEntries || false;
|
|
188
194
|
|
|
189
195
|
const hasHITLInteractions =
|
|
190
196
|
hitlInteractions && hitlInteractions.length > 0;
|
|
@@ -245,16 +251,28 @@ const AgentResponse = React.forwardRef<HTMLDivElement, AgentResponseProps>(
|
|
|
245
251
|
elapsedTime={elapsedTime}
|
|
246
252
|
/>
|
|
247
253
|
|
|
248
|
-
{/* Thinking Content -
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
254
|
+
{/* Thinking Content - AgentTimeline when timeline entries exist, ThinkingSection otherwise */}
|
|
255
|
+
{hasTimelineEntries ? (
|
|
256
|
+
thinkingExpanded && (
|
|
257
|
+
<div className="px-3 pb-3 border-t border-border mt-2">
|
|
258
|
+
<AgentTimeline
|
|
259
|
+
entries={state.timelineEntries!}
|
|
260
|
+
renderMarkdown={renderThinkingMarkdown}
|
|
261
|
+
uiState={timelineUIStateRef.current}
|
|
262
|
+
/>
|
|
263
|
+
</div>
|
|
264
|
+
)
|
|
265
|
+
) : (
|
|
266
|
+
<ThinkingSection
|
|
267
|
+
content={
|
|
268
|
+
state.thinkingSteps && state.thinkingSteps.length > 0
|
|
269
|
+
? state.thinkingSteps
|
|
270
|
+
: state.thinking
|
|
271
|
+
}
|
|
272
|
+
isExpanded={thinkingExpanded}
|
|
273
|
+
renderMarkdown={renderThinkingMarkdown}
|
|
274
|
+
/>
|
|
275
|
+
)}
|
|
258
276
|
</>
|
|
259
277
|
)}
|
|
260
278
|
|
|
@@ -110,7 +110,7 @@ const ThinkingSection = React.forwardRef<HTMLDivElement, ThinkingSectionProps>(
|
|
|
110
110
|
className={cn("px-3 pb-3 border-t border-border", className)}
|
|
111
111
|
{...props}
|
|
112
112
|
>
|
|
113
|
-
<div className="mt-2 max-h-[200px] overflow-y-auto">
|
|
113
|
+
<div className="mt-2 max-h-[200px] overflow-y-auto scrollbar-thin">
|
|
114
114
|
{isStructured ? (
|
|
115
115
|
<div className="space-y-0">
|
|
116
116
|
{content.map((step) => (
|
|
@@ -15,7 +15,9 @@ import {
|
|
|
15
15
|
type MemoryItem,
|
|
16
16
|
type StatusItem,
|
|
17
17
|
type ThinkingStep,
|
|
18
|
+
type PotentialResponse,
|
|
18
19
|
} from "../types";
|
|
20
|
+
import { buildTimelineEntries } from "../../agent-timeline/utils";
|
|
19
21
|
|
|
20
22
|
export interface UseAgentResponseAccumulatorOptions {
|
|
21
23
|
/** WebSocket topic to filter messages (optional, for convenience) */
|
|
@@ -94,33 +96,50 @@ export function useAgentResponseAccumulator(
|
|
|
94
96
|
id: payload.thinkingStep.id || `step-${Date.now()}`,
|
|
95
97
|
label: payload.thinkingStep.label,
|
|
96
98
|
content: payload.thinkingStep.content,
|
|
97
|
-
depth: payload.thinkingStep.depth ?? 0,
|
|
99
|
+
depth: payload.thinkingStep.depth ?? payload.depth ?? 0,
|
|
98
100
|
isCollapsed: payload.thinkingStep.isCollapsed,
|
|
101
|
+
timestamp: Date.now(),
|
|
102
|
+
agentName: payload.agentName,
|
|
103
|
+
parentAgent: payload.parentAgent,
|
|
99
104
|
};
|
|
100
105
|
const thinkingStartTime = prev.thinkingStartTime ?? Date.now();
|
|
101
|
-
|
|
106
|
+
const next = {
|
|
102
107
|
...prev,
|
|
103
108
|
status: newStatus,
|
|
104
109
|
thinkingSteps: [...(prev.thinkingSteps || []), newStep],
|
|
105
110
|
thinkingStartTime,
|
|
106
111
|
firstMessageTime,
|
|
107
112
|
};
|
|
113
|
+
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
108
114
|
}
|
|
109
115
|
|
|
110
|
-
// Plain text thinking
|
|
116
|
+
// Plain text thinking — concatenate for backward compat AND
|
|
117
|
+
// push a ThinkingStep so the timeline gets individual entries.
|
|
111
118
|
const newThinking = payload.message || payload.content || "";
|
|
112
119
|
// Add line break between thinking messages
|
|
113
120
|
const separator = prev.thinking && newThinking ? "\n\n" : "";
|
|
114
121
|
// Set thinkingStartTime on first thinking message
|
|
115
122
|
const thinkingStartTime =
|
|
116
123
|
prev.thinkingStartTime ?? (newThinking ? Date.now() : null);
|
|
117
|
-
|
|
124
|
+
const prevSteps = prev.thinkingSteps || [];
|
|
125
|
+
const plainStep: ThinkingStep = {
|
|
126
|
+
id: `step-${prevSteps.length}`,
|
|
127
|
+
label: newThinking,
|
|
128
|
+
content: newThinking,
|
|
129
|
+
depth: payload.depth ?? 0,
|
|
130
|
+
timestamp: Date.now(),
|
|
131
|
+
agentName: payload.agentName,
|
|
132
|
+
parentAgent: payload.parentAgent,
|
|
133
|
+
};
|
|
134
|
+
const next = {
|
|
118
135
|
...prev,
|
|
119
136
|
status: newStatus,
|
|
120
137
|
thinking: prev.thinking + separator + newThinking,
|
|
138
|
+
thinkingSteps: [...prevSteps, plainStep],
|
|
121
139
|
thinkingStartTime,
|
|
122
140
|
firstMessageTime,
|
|
123
141
|
};
|
|
142
|
+
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
124
143
|
}
|
|
125
144
|
|
|
126
145
|
case "tool_call": {
|
|
@@ -132,13 +151,17 @@ export function useAgentResponseAccumulator(
|
|
|
132
151
|
name: toolName,
|
|
133
152
|
arguments: payload.tool?.arguments,
|
|
134
153
|
timestamp: Date.now(),
|
|
154
|
+
agentName: payload.agentName,
|
|
155
|
+
parentAgent: payload.parentAgent,
|
|
156
|
+
depth: payload.depth,
|
|
135
157
|
};
|
|
136
|
-
|
|
158
|
+
const next = {
|
|
137
159
|
...prev,
|
|
138
160
|
status: newStatus,
|
|
139
161
|
toolCalls: [...prev.toolCalls, newToolCall],
|
|
140
162
|
firstMessageTime,
|
|
141
163
|
};
|
|
164
|
+
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
142
165
|
}
|
|
143
166
|
return { ...prev, status: newStatus, firstMessageTime };
|
|
144
167
|
}
|
|
@@ -152,13 +175,17 @@ export function useAgentResponseAccumulator(
|
|
|
152
175
|
source: payload.knowledge?.source || "unknown",
|
|
153
176
|
content: knowledgeContent,
|
|
154
177
|
timestamp: Date.now(),
|
|
178
|
+
agentName: payload.agentName,
|
|
179
|
+
parentAgent: payload.parentAgent,
|
|
180
|
+
depth: payload.depth,
|
|
155
181
|
};
|
|
156
|
-
|
|
182
|
+
const next = {
|
|
157
183
|
...prev,
|
|
158
184
|
status: newStatus,
|
|
159
185
|
knowledge: [...prev.knowledge, newKnowledge],
|
|
160
186
|
firstMessageTime,
|
|
161
187
|
};
|
|
188
|
+
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
162
189
|
}
|
|
163
190
|
return { ...prev, status: newStatus, firstMessageTime };
|
|
164
191
|
}
|
|
@@ -172,13 +199,17 @@ export function useAgentResponseAccumulator(
|
|
|
172
199
|
type: payload.memory?.type || "unknown",
|
|
173
200
|
content: memoryContent,
|
|
174
201
|
timestamp: Date.now(),
|
|
202
|
+
agentName: payload.agentName,
|
|
203
|
+
parentAgent: payload.parentAgent,
|
|
204
|
+
depth: payload.depth,
|
|
175
205
|
};
|
|
176
|
-
|
|
206
|
+
const next = {
|
|
177
207
|
...prev,
|
|
178
208
|
status: newStatus,
|
|
179
209
|
memory: [...prev.memory, newMemory],
|
|
180
210
|
firstMessageTime,
|
|
181
211
|
};
|
|
212
|
+
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
182
213
|
}
|
|
183
214
|
return { ...prev, status: newStatus, firstMessageTime };
|
|
184
215
|
}
|
|
@@ -200,13 +231,39 @@ export function useAgentResponseAccumulator(
|
|
|
200
231
|
message: statusMessage,
|
|
201
232
|
agent: payload.statusUpdate?.agent,
|
|
202
233
|
timestamp: Date.now(),
|
|
234
|
+
agentName: payload.agentName,
|
|
235
|
+
parentAgent: payload.parentAgent,
|
|
236
|
+
depth: payload.depth,
|
|
203
237
|
};
|
|
204
|
-
|
|
238
|
+
const next = {
|
|
205
239
|
...prev,
|
|
206
240
|
status: newStatus,
|
|
207
241
|
statusUpdates: [...prev.statusUpdates, newStatusItem],
|
|
208
242
|
firstMessageTime,
|
|
209
243
|
};
|
|
244
|
+
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
245
|
+
}
|
|
246
|
+
return { ...prev, status: newStatus, firstMessageTime };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
case "potential_response": {
|
|
250
|
+
const respContent = payload.message || payload.content || "";
|
|
251
|
+
if (respContent) {
|
|
252
|
+
const newResp: PotentialResponse = {
|
|
253
|
+
id: `resp-${Date.now()}`,
|
|
254
|
+
content: respContent,
|
|
255
|
+
timestamp: Date.now(),
|
|
256
|
+
agentName: payload.agentName,
|
|
257
|
+
parentAgent: payload.parentAgent,
|
|
258
|
+
depth: payload.depth,
|
|
259
|
+
};
|
|
260
|
+
const next = {
|
|
261
|
+
...prev,
|
|
262
|
+
status: newStatus,
|
|
263
|
+
potentialResponses: [...(prev.potentialResponses || []), newResp],
|
|
264
|
+
firstMessageTime,
|
|
265
|
+
};
|
|
266
|
+
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
210
267
|
}
|
|
211
268
|
return { ...prev, status: newStatus, firstMessageTime };
|
|
212
269
|
}
|
|
@@ -45,6 +45,7 @@ export type {
|
|
|
45
45
|
StatusItem,
|
|
46
46
|
ThinkingStep,
|
|
47
47
|
ThinkingContent,
|
|
48
|
+
PotentialResponse,
|
|
48
49
|
AgentMessage,
|
|
49
50
|
GenericWebSocketMessage,
|
|
50
51
|
} from "./types";
|
|
@@ -53,3 +54,19 @@ export { initialAgentResponseState } from "./types";
|
|
|
53
54
|
|
|
54
55
|
// Utilities
|
|
55
56
|
export { formatTime, formatTotalTime } from "./utils";
|
|
57
|
+
|
|
58
|
+
// Agent Timeline (replaces ThinkingSection for rich execution visibility)
|
|
59
|
+
export {
|
|
60
|
+
AgentTimeline,
|
|
61
|
+
createTimelineUIState,
|
|
62
|
+
buildTimelineEntries,
|
|
63
|
+
groupIntoAgentRuns,
|
|
64
|
+
deduplicateEntries,
|
|
65
|
+
} from "../agent-timeline";
|
|
66
|
+
export type {
|
|
67
|
+
TimelineUIState,
|
|
68
|
+
TimelineEntry,
|
|
69
|
+
TimelineEntryType,
|
|
70
|
+
AgentRun,
|
|
71
|
+
DisplayEntry,
|
|
72
|
+
} from "../agent-timeline";
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Type definitions for the library-ready agent response component
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import type { TimelineEntry } from "../agent-timeline/types";
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Status of the agent response cycle
|
|
9
11
|
*/
|
|
@@ -22,6 +24,12 @@ export interface ToolCall {
|
|
|
22
24
|
name: string;
|
|
23
25
|
arguments?: Record<string, unknown>;
|
|
24
26
|
timestamp: number;
|
|
27
|
+
/** Agent that made this call (multi-agent scenarios) */
|
|
28
|
+
agentName?: string | null;
|
|
29
|
+
/** Parent agent name */
|
|
30
|
+
parentAgent?: string | null;
|
|
31
|
+
/** Nesting depth in agent hierarchy */
|
|
32
|
+
depth?: number;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
/**
|
|
@@ -32,6 +40,12 @@ export interface KnowledgeItem {
|
|
|
32
40
|
source: string;
|
|
33
41
|
content: string;
|
|
34
42
|
timestamp: number;
|
|
43
|
+
/** Agent that retrieved this (multi-agent scenarios) */
|
|
44
|
+
agentName?: string | null;
|
|
45
|
+
/** Parent agent name */
|
|
46
|
+
parentAgent?: string | null;
|
|
47
|
+
/** Nesting depth in agent hierarchy */
|
|
48
|
+
depth?: number;
|
|
35
49
|
}
|
|
36
50
|
|
|
37
51
|
/**
|
|
@@ -42,6 +56,24 @@ export interface MemoryItem {
|
|
|
42
56
|
type: string;
|
|
43
57
|
content: string;
|
|
44
58
|
timestamp: number;
|
|
59
|
+
/** Agent that accessed this (multi-agent scenarios) */
|
|
60
|
+
agentName?: string | null;
|
|
61
|
+
/** Parent agent name */
|
|
62
|
+
parentAgent?: string | null;
|
|
63
|
+
/** Nesting depth in agent hierarchy */
|
|
64
|
+
depth?: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Potential response (sub-agent intermediate AI response)
|
|
69
|
+
*/
|
|
70
|
+
export interface PotentialResponse {
|
|
71
|
+
id: string;
|
|
72
|
+
content: string;
|
|
73
|
+
timestamp: number;
|
|
74
|
+
agentName?: string | null;
|
|
75
|
+
parentAgent?: string | null;
|
|
76
|
+
depth?: number;
|
|
45
77
|
}
|
|
46
78
|
|
|
47
79
|
/**
|
|
@@ -53,6 +85,12 @@ export interface StatusItem {
|
|
|
53
85
|
timestamp: number;
|
|
54
86
|
/** Optional agent name if in multi-agent scenario */
|
|
55
87
|
agent?: string;
|
|
88
|
+
/** Agent that produced this (multi-agent scenarios) */
|
|
89
|
+
agentName?: string | null;
|
|
90
|
+
/** Parent agent name */
|
|
91
|
+
parentAgent?: string | null;
|
|
92
|
+
/** Nesting depth in agent hierarchy */
|
|
93
|
+
depth?: number;
|
|
56
94
|
}
|
|
57
95
|
|
|
58
96
|
/**
|
|
@@ -69,6 +107,12 @@ export interface ThinkingStep {
|
|
|
69
107
|
depth: number;
|
|
70
108
|
/** Whether this step should start collapsed (default: false) */
|
|
71
109
|
isCollapsed?: boolean;
|
|
110
|
+
/** Timestamp for timeline ordering */
|
|
111
|
+
timestamp?: number;
|
|
112
|
+
/** Agent that produced this (multi-agent scenarios) */
|
|
113
|
+
agentName?: string | null;
|
|
114
|
+
/** Parent agent name */
|
|
115
|
+
parentAgent?: string | null;
|
|
72
116
|
}
|
|
73
117
|
|
|
74
118
|
/**
|
|
@@ -96,8 +140,14 @@ export interface AgentResponseState {
|
|
|
96
140
|
memory: MemoryItem[];
|
|
97
141
|
/** Status updates from the agent */
|
|
98
142
|
statusUpdates: StatusItem[];
|
|
143
|
+
/** Potential responses (sub-agent intermediate AI responses) */
|
|
144
|
+
potentialResponses?: PotentialResponse[];
|
|
145
|
+
/** Custom timeline entries (consumer-provided) */
|
|
146
|
+
customTimelineEntries?: TimelineEntry[];
|
|
99
147
|
/** Final response text */
|
|
100
148
|
response: string;
|
|
149
|
+
/** Timeline entries derived from all accumulator arrays (for AgentTimeline) */
|
|
150
|
+
timelineEntries?: TimelineEntry[];
|
|
101
151
|
/** Timestamp when first thinking message was received (for timer) */
|
|
102
152
|
thinkingStartTime: number | null;
|
|
103
153
|
/** Timestamp when response was completed (for final timer display) */
|
|
@@ -110,13 +160,21 @@ export interface AgentResponseState {
|
|
|
110
160
|
* WebSocket message payload for agent responses
|
|
111
161
|
*/
|
|
112
162
|
export interface AgentMessage {
|
|
113
|
-
type: "status" | "thinking" | "tool_call" | "knowledge" | "memory" | "response" | "status_update";
|
|
163
|
+
type: "status" | "thinking" | "tool_call" | "knowledge" | "memory" | "response" | "status_update" | "potential_response";
|
|
114
164
|
/** Message content - for simple string payloads */
|
|
115
165
|
message?: string;
|
|
116
166
|
/** Alternative content field */
|
|
117
167
|
content?: string;
|
|
118
168
|
/** For status messages */
|
|
119
169
|
status?: string;
|
|
170
|
+
/** Agent name (multi-agent scenarios) */
|
|
171
|
+
agentName?: string | null;
|
|
172
|
+
/** Parent agent name (multi-agent scenarios) */
|
|
173
|
+
parentAgent?: string | null;
|
|
174
|
+
/** Agent nesting depth (0 = root) */
|
|
175
|
+
depth?: number;
|
|
176
|
+
/** Title/label for timeline display */
|
|
177
|
+
title?: string | null;
|
|
120
178
|
/** For tool_call messages */
|
|
121
179
|
tool?: {
|
|
122
180
|
id: string;
|
|
@@ -170,6 +228,8 @@ export const initialAgentResponseState: AgentResponseState = {
|
|
|
170
228
|
knowledge: [],
|
|
171
229
|
memory: [],
|
|
172
230
|
statusUpdates: [],
|
|
231
|
+
potentialResponses: [],
|
|
232
|
+
customTimelineEntries: [],
|
|
173
233
|
response: "",
|
|
174
234
|
thinkingStartTime: null,
|
|
175
235
|
responseCompleteTime: null,
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { useMemo, useRef, useState, useCallback, type ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Brain,
|
|
4
|
+
Wrench,
|
|
5
|
+
BookOpen,
|
|
6
|
+
HardDrive,
|
|
7
|
+
Activity,
|
|
8
|
+
MessageSquare,
|
|
9
|
+
AlertCircle,
|
|
10
|
+
ChevronsDownUp,
|
|
11
|
+
ChevronsUpDown,
|
|
12
|
+
} from "lucide-react";
|
|
13
|
+
import type { TimelineEntry, TimelineEntryType } from "./types";
|
|
14
|
+
import { groupIntoAgentRuns } from "./utils";
|
|
15
|
+
import { TimelineAgentBlock } from "./TimelineAgentBlock";
|
|
16
|
+
|
|
17
|
+
/** Externalized UI state that survives component remounts */
|
|
18
|
+
export interface TimelineUIState {
|
|
19
|
+
expandedItems: Set<string>;
|
|
20
|
+
collapsedRuns: Set<string>;
|
|
21
|
+
activeFilters: Set<TimelineEntryType>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createTimelineUIState(): TimelineUIState {
|
|
25
|
+
return {
|
|
26
|
+
expandedItems: new Set(),
|
|
27
|
+
collapsedRuns: new Set(),
|
|
28
|
+
activeFilters: new Set(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Icon + label config for each entry type */
|
|
33
|
+
const TYPE_CONFIG: {
|
|
34
|
+
type: TimelineEntryType;
|
|
35
|
+
icon: typeof Brain;
|
|
36
|
+
label: string;
|
|
37
|
+
}[] = [
|
|
38
|
+
{ type: "status_update", icon: Activity, label: "Status" },
|
|
39
|
+
{ type: "thinking", icon: Brain, label: "Thinking" },
|
|
40
|
+
{ type: "tool_call", icon: Wrench, label: "Tools" },
|
|
41
|
+
{ type: "knowledge", icon: BookOpen, label: "Knowledge" },
|
|
42
|
+
{ type: "memory", icon: HardDrive, label: "Memory" },
|
|
43
|
+
{ type: "ai_response", icon: MessageSquare, label: "AI" },
|
|
44
|
+
{ type: "error", icon: AlertCircle, label: "Errors" },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
interface AgentTimelineProps {
|
|
48
|
+
entries: TimelineEntry[];
|
|
49
|
+
renderMarkdown?: (content: string) => ReactNode;
|
|
50
|
+
/**
|
|
51
|
+
* External UI state store. When provided, expand/collapse/filter state
|
|
52
|
+
* is read from and written to this object (which should be ref-backed
|
|
53
|
+
* in the parent) so it survives component remounts during streaming.
|
|
54
|
+
*/
|
|
55
|
+
uiState?: TimelineUIState;
|
|
56
|
+
/**
|
|
57
|
+
* Maximum height of the scrollable timeline container.
|
|
58
|
+
* Defaults to "300px". Set to "none" to disable.
|
|
59
|
+
*/
|
|
60
|
+
maxHeight?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function AgentTimeline({ entries, renderMarkdown, uiState, maxHeight = "300px" }: AgentTimelineProps) {
|
|
64
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
65
|
+
|
|
66
|
+
// Render tick: forces re-renders AND invalidates useMemo deps when we mutate Sets in-place
|
|
67
|
+
const [renderTick, setRenderTick] = useState(0);
|
|
68
|
+
const forceRender = useCallback(() => setRenderTick((t) => t + 1), []);
|
|
69
|
+
|
|
70
|
+
// Resolve state: prefer external uiState (ref-backed, survives remounts)
|
|
71
|
+
// Fall back to internal state if no external store provided
|
|
72
|
+
const [internalExpandedItems] = useState<Set<string>>(() => new Set());
|
|
73
|
+
const [internalCollapsedRuns] = useState<Set<string>>(() => new Set());
|
|
74
|
+
const [internalActiveFilters] = useState<Set<TimelineEntryType>>(() => new Set());
|
|
75
|
+
|
|
76
|
+
const expandedItems = uiState?.expandedItems ?? internalExpandedItems;
|
|
77
|
+
const collapsedRuns = uiState?.collapsedRuns ?? internalCollapsedRuns;
|
|
78
|
+
const activeFilters = uiState?.activeFilters ?? internalActiveFilters;
|
|
79
|
+
|
|
80
|
+
// Compute which types actually exist in the entries
|
|
81
|
+
const availableTypes = useMemo(() => {
|
|
82
|
+
const types = new Set<TimelineEntryType>();
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
types.add(entry.type);
|
|
85
|
+
}
|
|
86
|
+
return types;
|
|
87
|
+
}, [entries]);
|
|
88
|
+
|
|
89
|
+
// Filter entries, then group into runs
|
|
90
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- renderTick invalidates when mutated Sets change
|
|
91
|
+
const filteredEntries = useMemo(
|
|
92
|
+
() =>
|
|
93
|
+
activeFilters.size === 0
|
|
94
|
+
? entries
|
|
95
|
+
: entries.filter((e) => activeFilters.has(e.type)),
|
|
96
|
+
[entries, activeFilters, renderTick],
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
100
|
+
const agentRuns = useMemo(() => groupIntoAgentRuns(filteredEntries), [filteredEntries, renderTick]);
|
|
101
|
+
|
|
102
|
+
// --- Mutators (mutate the set in-place + trigger re-render) ---
|
|
103
|
+
|
|
104
|
+
const toggleFilter = useCallback((type: TimelineEntryType) => {
|
|
105
|
+
if (activeFilters.has(type)) {
|
|
106
|
+
activeFilters.delete(type);
|
|
107
|
+
} else {
|
|
108
|
+
activeFilters.add(type);
|
|
109
|
+
}
|
|
110
|
+
forceRender();
|
|
111
|
+
}, [activeFilters, forceRender]);
|
|
112
|
+
|
|
113
|
+
const clearFilters = useCallback(() => {
|
|
114
|
+
activeFilters.clear();
|
|
115
|
+
forceRender();
|
|
116
|
+
}, [activeFilters, forceRender]);
|
|
117
|
+
|
|
118
|
+
const toggleItemExpanded = useCallback((entryId: string) => {
|
|
119
|
+
if (expandedItems.has(entryId)) {
|
|
120
|
+
expandedItems.delete(entryId);
|
|
121
|
+
} else {
|
|
122
|
+
expandedItems.add(entryId);
|
|
123
|
+
}
|
|
124
|
+
forceRender();
|
|
125
|
+
}, [expandedItems, forceRender]);
|
|
126
|
+
|
|
127
|
+
const collapseAll = useCallback(() => {
|
|
128
|
+
collapsedRuns.clear();
|
|
129
|
+
agentRuns.forEach((run, i) => {
|
|
130
|
+
collapsedRuns.add(`${run.agentName}-${i}`);
|
|
131
|
+
});
|
|
132
|
+
expandedItems.clear();
|
|
133
|
+
forceRender();
|
|
134
|
+
}, [agentRuns, collapsedRuns, expandedItems, forceRender]);
|
|
135
|
+
|
|
136
|
+
const expandAll = useCallback(() => {
|
|
137
|
+
collapsedRuns.clear();
|
|
138
|
+
agentRuns.forEach((run, i) => {
|
|
139
|
+
collapsedRuns.add(`${run.agentName}-${i}:expanded`);
|
|
140
|
+
});
|
|
141
|
+
forceRender();
|
|
142
|
+
}, [agentRuns, collapsedRuns, forceRender]);
|
|
143
|
+
|
|
144
|
+
if (entries.length === 0) return null;
|
|
145
|
+
|
|
146
|
+
const isSingle = agentRuns.length === 1;
|
|
147
|
+
const hasActiveFilter = activeFilters.size > 0;
|
|
148
|
+
|
|
149
|
+
const scrollStyle = maxHeight !== "none" ? { maxHeight } : undefined;
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<div
|
|
153
|
+
ref={containerRef}
|
|
154
|
+
className={`-mt-1 ${maxHeight !== "none" ? "overflow-y-auto scrollbar-thin" : ""}`}
|
|
155
|
+
style={scrollStyle}
|
|
156
|
+
>
|
|
157
|
+
{/* Filter + controls bar (sticky within scroll container) */}
|
|
158
|
+
<div className="sticky top-0 z-10 bg-background flex items-center gap-1 py-1.5 mb-1 border-b border-border/50 flex-wrap">
|
|
159
|
+
{/* Type filter chips */}
|
|
160
|
+
{TYPE_CONFIG.filter((tc) => availableTypes.has(tc.type)).map((tc) => {
|
|
161
|
+
const isActive = activeFilters.has(tc.type);
|
|
162
|
+
const count = entries.filter((e) => e.type === tc.type).length;
|
|
163
|
+
return (
|
|
164
|
+
<button
|
|
165
|
+
key={tc.type}
|
|
166
|
+
onClick={() => toggleFilter(tc.type)}
|
|
167
|
+
className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] transition-colors ${
|
|
168
|
+
isActive
|
|
169
|
+
? "bg-accent text-accent-foreground ring-1 ring-accent-foreground/20"
|
|
170
|
+
: "text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted/50"
|
|
171
|
+
}`}
|
|
172
|
+
title={`${isActive ? "Hide" : "Show only"} ${tc.label}`}
|
|
173
|
+
>
|
|
174
|
+
<tc.icon className="w-3 h-3" />
|
|
175
|
+
<span>{count}</span>
|
|
176
|
+
</button>
|
|
177
|
+
);
|
|
178
|
+
})}
|
|
179
|
+
|
|
180
|
+
{/* Clear filter button */}
|
|
181
|
+
{hasActiveFilter && (
|
|
182
|
+
<button
|
|
183
|
+
onClick={clearFilters}
|
|
184
|
+
className="text-[10px] text-muted-foreground/60 hover:text-muted-foreground px-1"
|
|
185
|
+
>
|
|
186
|
+
Clear
|
|
187
|
+
</button>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
{/* Spacer */}
|
|
191
|
+
<div className="flex-1" />
|
|
192
|
+
|
|
193
|
+
{/* Collapse/expand all */}
|
|
194
|
+
{!isSingle && (
|
|
195
|
+
<>
|
|
196
|
+
<button
|
|
197
|
+
onClick={collapseAll}
|
|
198
|
+
className="inline-flex items-center gap-0.5 text-[10px] text-muted-foreground/60 hover:text-muted-foreground px-1 py-0.5 rounded hover:bg-muted/50 transition-colors"
|
|
199
|
+
title="Collapse all"
|
|
200
|
+
>
|
|
201
|
+
<ChevronsDownUp className="w-3 h-3" />
|
|
202
|
+
</button>
|
|
203
|
+
<button
|
|
204
|
+
onClick={expandAll}
|
|
205
|
+
className="inline-flex items-center gap-0.5 text-[10px] text-muted-foreground/60 hover:text-muted-foreground px-1 py-0.5 rounded hover:bg-muted/50 transition-colors"
|
|
206
|
+
title="Expand all"
|
|
207
|
+
>
|
|
208
|
+
<ChevronsUpDown className="w-3 h-3" />
|
|
209
|
+
</button>
|
|
210
|
+
</>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
{/* Timeline content */}
|
|
215
|
+
{filteredEntries.length === 0 ? (
|
|
216
|
+
<div className="text-[10px] text-muted-foreground/50 py-2 text-center">
|
|
217
|
+
No entries match the selected filters
|
|
218
|
+
</div>
|
|
219
|
+
) : (
|
|
220
|
+
<div className="space-y-0.5">
|
|
221
|
+
{agentRuns.map((run, i) => {
|
|
222
|
+
const runKey = `${run.agentName}-${i}`;
|
|
223
|
+
const defaultCollapsed = run.depth > 0;
|
|
224
|
+
const isCollapsed = collapsedRuns.has(runKey)
|
|
225
|
+
? true
|
|
226
|
+
: collapsedRuns.has(`${runKey}:expanded`)
|
|
227
|
+
? false
|
|
228
|
+
: defaultCollapsed;
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<TimelineAgentBlock
|
|
232
|
+
key={runKey}
|
|
233
|
+
block={run}
|
|
234
|
+
renderMarkdown={renderMarkdown}
|
|
235
|
+
isSingleAgent={isSingle}
|
|
236
|
+
isCollapsed={isCollapsed}
|
|
237
|
+
onToggleCollapsed={() => {
|
|
238
|
+
if (isCollapsed) {
|
|
239
|
+
collapsedRuns.delete(runKey);
|
|
240
|
+
collapsedRuns.add(`${runKey}:expanded`);
|
|
241
|
+
} else {
|
|
242
|
+
collapsedRuns.delete(`${runKey}:expanded`);
|
|
243
|
+
collapsedRuns.add(runKey);
|
|
244
|
+
}
|
|
245
|
+
forceRender();
|
|
246
|
+
}}
|
|
247
|
+
expandedItems={expandedItems}
|
|
248
|
+
onToggleItemExpanded={toggleItemExpanded}
|
|
249
|
+
/>
|
|
250
|
+
);
|
|
251
|
+
})}
|
|
252
|
+
</div>
|
|
253
|
+
)}
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
}
|