@optilogic/chat 1.0.0-beta.5 → 1.0.0-beta.7
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/README.md +99 -0
- package/dist/index.cjs +167 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +98 -6
- package/dist/index.d.ts +98 -6
- package/dist/index.js +168 -16
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/agent-response/AgentResponse.tsx +29 -4
- package/src/components/agent-response/components/ActivityIndicators.tsx +36 -4
- package/src/components/agent-response/components/MetadataRow.tsx +6 -2
- package/src/components/agent-response/components/ThinkingSection.tsx +101 -9
- package/src/components/agent-response/hooks/useAgentResponseAccumulator.ts +41 -0
- package/src/components/agent-response/index.ts +3 -0
- package/src/components/agent-response/types.ts +54 -1
- package/src/components/user-prompt-input/UserPromptInput.tsx +79 -18
- package/src/components/user-prompt-input/types.ts +10 -0
- package/src/index.ts +3 -0
|
@@ -1,33 +1,109 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Thinking Section Component
|
|
3
3
|
*
|
|
4
|
-
* Collapsible section for displaying agent thinking/reasoning content
|
|
4
|
+
* Collapsible section for displaying agent thinking/reasoning content.
|
|
5
|
+
* Supports both plain text and structured collapsible sub-sections.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import * as React from "react";
|
|
9
|
+
import { useState, useCallback } from "react";
|
|
10
|
+
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
8
11
|
import { cn } from "@optilogic/core";
|
|
12
|
+
import type { ThinkingStep } from "../types";
|
|
9
13
|
|
|
10
|
-
export interface ThinkingSectionProps
|
|
11
|
-
|
|
12
|
-
content
|
|
14
|
+
export interface ThinkingSectionProps
|
|
15
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "content"> {
|
|
16
|
+
/** The thinking content to display (string or structured steps) */
|
|
17
|
+
content: string | ThinkingStep[];
|
|
13
18
|
/** Whether the section is expanded */
|
|
14
19
|
isExpanded: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Custom markdown renderer for the thinking content.
|
|
22
|
+
* If not provided, the content will be rendered as plain preformatted text.
|
|
23
|
+
*/
|
|
24
|
+
renderMarkdown?: (content: string) => React.ReactNode;
|
|
15
25
|
}
|
|
16
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Internal component for rendering a collapsible thinking step
|
|
29
|
+
*/
|
|
30
|
+
interface ThinkingStepItemProps {
|
|
31
|
+
step: ThinkingStep;
|
|
32
|
+
renderMarkdown?: (content: string) => React.ReactNode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ThinkingStepItem: React.FC<ThinkingStepItemProps> = ({ step, renderMarkdown }) => {
|
|
36
|
+
const [isCollapsed, setIsCollapsed] = useState(step.isCollapsed ?? false);
|
|
37
|
+
|
|
38
|
+
const toggleCollapse = useCallback(() => {
|
|
39
|
+
setIsCollapsed((prev) => !prev);
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const indentPadding = step.depth * 16;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="border-b border-border/50 last:border-b-0">
|
|
46
|
+
<button
|
|
47
|
+
onClick={toggleCollapse}
|
|
48
|
+
className="w-full flex items-center gap-1.5 py-1.5 px-2 hover:bg-muted/50 transition-colors text-left"
|
|
49
|
+
style={{ paddingLeft: `${indentPadding + 8}px` }}
|
|
50
|
+
>
|
|
51
|
+
{isCollapsed ? (
|
|
52
|
+
<ChevronRight className="w-3 h-3 text-muted-foreground flex-shrink-0" />
|
|
53
|
+
) : (
|
|
54
|
+
<ChevronDown className="w-3 h-3 text-muted-foreground flex-shrink-0" />
|
|
55
|
+
)}
|
|
56
|
+
<span className="text-xs font-medium text-foreground/80">{step.label}</span>
|
|
57
|
+
</button>
|
|
58
|
+
|
|
59
|
+
{!isCollapsed && (
|
|
60
|
+
<div
|
|
61
|
+
className="pb-2 px-2"
|
|
62
|
+
style={{ paddingLeft: `${indentPadding + 28}px` }}
|
|
63
|
+
>
|
|
64
|
+
{renderMarkdown ? (
|
|
65
|
+
<div className="text-xs text-muted-foreground">
|
|
66
|
+
{renderMarkdown(step.content)}
|
|
67
|
+
</div>
|
|
68
|
+
) : (
|
|
69
|
+
<pre className="text-xs text-muted-foreground whitespace-pre-wrap font-mono">
|
|
70
|
+
{step.content}
|
|
71
|
+
</pre>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
17
79
|
/**
|
|
18
80
|
* ThinkingSection Component
|
|
19
81
|
*
|
|
20
82
|
* Displays the agent's thinking/reasoning content in a collapsible panel.
|
|
83
|
+
* Supports both plain text content and structured collapsible sub-sections.
|
|
21
84
|
*
|
|
22
85
|
* @example
|
|
86
|
+
* // Plain text content
|
|
23
87
|
* <ThinkingSection content={state.thinking} isExpanded={isExpanded} />
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* // Structured content with sub-sections
|
|
91
|
+
* <ThinkingSection
|
|
92
|
+
* content={[
|
|
93
|
+
* { id: "1", label: "Analysis", content: "...", depth: 0 },
|
|
94
|
+
* { id: "2", label: "Sub-analysis", content: "...", depth: 1 },
|
|
95
|
+
* ]}
|
|
96
|
+
* isExpanded={isExpanded}
|
|
97
|
+
* />
|
|
24
98
|
*/
|
|
25
99
|
const ThinkingSection = React.forwardRef<HTMLDivElement, ThinkingSectionProps>(
|
|
26
|
-
({ content, isExpanded, className, ...props }, ref) => {
|
|
27
|
-
if (!isExpanded || !content) {
|
|
100
|
+
({ content, isExpanded, renderMarkdown, className, ...props }, ref) => {
|
|
101
|
+
if (!isExpanded || !content || (Array.isArray(content) && content.length === 0)) {
|
|
28
102
|
return null;
|
|
29
103
|
}
|
|
30
104
|
|
|
105
|
+
const isStructured = Array.isArray(content);
|
|
106
|
+
|
|
31
107
|
return (
|
|
32
108
|
<div
|
|
33
109
|
ref={ref}
|
|
@@ -35,9 +111,25 @@ const ThinkingSection = React.forwardRef<HTMLDivElement, ThinkingSectionProps>(
|
|
|
35
111
|
{...props}
|
|
36
112
|
>
|
|
37
113
|
<div className="mt-2 max-h-[200px] overflow-y-auto">
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
114
|
+
{isStructured ? (
|
|
115
|
+
<div className="space-y-0">
|
|
116
|
+
{content.map((step) => (
|
|
117
|
+
<ThinkingStepItem
|
|
118
|
+
key={step.id}
|
|
119
|
+
step={step}
|
|
120
|
+
renderMarkdown={renderMarkdown}
|
|
121
|
+
/>
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
) : renderMarkdown ? (
|
|
125
|
+
<div className="text-xs text-muted-foreground">
|
|
126
|
+
{renderMarkdown(content)}
|
|
127
|
+
</div>
|
|
128
|
+
) : (
|
|
129
|
+
<pre className="text-xs text-muted-foreground whitespace-pre-wrap font-mono">
|
|
130
|
+
{content}
|
|
131
|
+
</pre>
|
|
132
|
+
)}
|
|
41
133
|
</div>
|
|
42
134
|
</div>
|
|
43
135
|
);
|
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
type ToolCall,
|
|
14
14
|
type KnowledgeItem,
|
|
15
15
|
type MemoryItem,
|
|
16
|
+
type StatusItem,
|
|
17
|
+
type ThinkingStep,
|
|
16
18
|
} from "../types";
|
|
17
19
|
|
|
18
20
|
export interface UseAgentResponseAccumulatorOptions {
|
|
@@ -86,6 +88,26 @@ export function useAgentResponseAccumulator(
|
|
|
86
88
|
return { ...prev, status: newStatus };
|
|
87
89
|
|
|
88
90
|
case "thinking": {
|
|
91
|
+
// Check if this is a structured thinking step
|
|
92
|
+
if (payload.thinkingStep) {
|
|
93
|
+
const newStep: ThinkingStep = {
|
|
94
|
+
id: payload.thinkingStep.id || `step-${Date.now()}`,
|
|
95
|
+
label: payload.thinkingStep.label,
|
|
96
|
+
content: payload.thinkingStep.content,
|
|
97
|
+
depth: payload.thinkingStep.depth ?? 0,
|
|
98
|
+
isCollapsed: payload.thinkingStep.isCollapsed,
|
|
99
|
+
};
|
|
100
|
+
const thinkingStartTime = prev.thinkingStartTime ?? Date.now();
|
|
101
|
+
return {
|
|
102
|
+
...prev,
|
|
103
|
+
status: newStatus,
|
|
104
|
+
thinkingSteps: [...(prev.thinkingSteps || []), newStep],
|
|
105
|
+
thinkingStartTime,
|
|
106
|
+
firstMessageTime,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Plain text thinking (existing behavior)
|
|
89
111
|
const newThinking = payload.message || payload.content || "";
|
|
90
112
|
// Add line break between thinking messages
|
|
91
113
|
const separator = prev.thinking && newThinking ? "\n\n" : "";
|
|
@@ -170,6 +192,25 @@ export function useAgentResponseAccumulator(
|
|
|
170
192
|
firstMessageTime: prev.firstMessageTime ?? Date.now(),
|
|
171
193
|
};
|
|
172
194
|
|
|
195
|
+
case "status_update": {
|
|
196
|
+
const statusMessage = payload.message || payload.statusUpdate?.message;
|
|
197
|
+
if (statusMessage) {
|
|
198
|
+
const newStatusItem: StatusItem = {
|
|
199
|
+
id: payload.statusUpdate?.id || `status-${Date.now()}`,
|
|
200
|
+
message: statusMessage,
|
|
201
|
+
agent: payload.statusUpdate?.agent,
|
|
202
|
+
timestamp: Date.now(),
|
|
203
|
+
};
|
|
204
|
+
return {
|
|
205
|
+
...prev,
|
|
206
|
+
status: newStatus,
|
|
207
|
+
statusUpdates: [...prev.statusUpdates, newStatusItem],
|
|
208
|
+
firstMessageTime,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return { ...prev, status: newStatus, firstMessageTime };
|
|
212
|
+
}
|
|
213
|
+
|
|
173
214
|
default:
|
|
174
215
|
return { ...prev, status: newStatus, firstMessageTime };
|
|
175
216
|
}
|
|
@@ -44,6 +44,40 @@ export interface MemoryItem {
|
|
|
44
44
|
timestamp: number;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Status update information from the agent
|
|
49
|
+
*/
|
|
50
|
+
export interface StatusItem {
|
|
51
|
+
id: string;
|
|
52
|
+
message: string;
|
|
53
|
+
timestamp: number;
|
|
54
|
+
/** Optional agent name if in multi-agent scenario */
|
|
55
|
+
agent?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A single step in structured thinking content
|
|
60
|
+
*/
|
|
61
|
+
export interface ThinkingStep {
|
|
62
|
+
/** Unique identifier for the step */
|
|
63
|
+
id: string;
|
|
64
|
+
/** Label/title shown in the collapsible header */
|
|
65
|
+
label: string;
|
|
66
|
+
/** Content of the thinking step */
|
|
67
|
+
content: string;
|
|
68
|
+
/** Nesting depth (0 = root level, 1 = first indent, etc.) */
|
|
69
|
+
depth: number;
|
|
70
|
+
/** Whether this step should start collapsed (default: false) */
|
|
71
|
+
isCollapsed?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Union type for thinking content
|
|
76
|
+
* - string: plain text (backward compatible)
|
|
77
|
+
* - ThinkingStep[]: structured with collapsible sub-sections
|
|
78
|
+
*/
|
|
79
|
+
export type ThinkingContent = string | ThinkingStep[];
|
|
80
|
+
|
|
47
81
|
/**
|
|
48
82
|
* State shape for the agent response component
|
|
49
83
|
*/
|
|
@@ -52,12 +86,16 @@ export interface AgentResponseState {
|
|
|
52
86
|
status: AgentResponseStatus;
|
|
53
87
|
/** Accumulated thinking/reasoning text */
|
|
54
88
|
thinking: string;
|
|
89
|
+
/** Structured thinking steps (if provided, takes precedence over thinking string) */
|
|
90
|
+
thinkingSteps?: ThinkingStep[];
|
|
55
91
|
/** Tool calls made during processing */
|
|
56
92
|
toolCalls: ToolCall[];
|
|
57
93
|
/** Knowledge items retrieved */
|
|
58
94
|
knowledge: KnowledgeItem[];
|
|
59
95
|
/** Memory items accessed */
|
|
60
96
|
memory: MemoryItem[];
|
|
97
|
+
/** Status updates from the agent */
|
|
98
|
+
statusUpdates: StatusItem[];
|
|
61
99
|
/** Final response text */
|
|
62
100
|
response: string;
|
|
63
101
|
/** Timestamp when first thinking message was received (for timer) */
|
|
@@ -72,7 +110,7 @@ export interface AgentResponseState {
|
|
|
72
110
|
* WebSocket message payload for agent responses
|
|
73
111
|
*/
|
|
74
112
|
export interface AgentMessage {
|
|
75
|
-
type: "status" | "thinking" | "tool_call" | "knowledge" | "memory" | "response";
|
|
113
|
+
type: "status" | "thinking" | "tool_call" | "knowledge" | "memory" | "response" | "status_update";
|
|
76
114
|
/** Message content - for simple string payloads */
|
|
77
115
|
message?: string;
|
|
78
116
|
/** Alternative content field */
|
|
@@ -97,6 +135,20 @@ export interface AgentMessage {
|
|
|
97
135
|
type: string;
|
|
98
136
|
content: string;
|
|
99
137
|
};
|
|
138
|
+
/** For status_update messages */
|
|
139
|
+
statusUpdate?: {
|
|
140
|
+
id: string;
|
|
141
|
+
message: string;
|
|
142
|
+
agent?: string;
|
|
143
|
+
};
|
|
144
|
+
/** For structured thinking step messages */
|
|
145
|
+
thinkingStep?: {
|
|
146
|
+
id?: string;
|
|
147
|
+
label: string;
|
|
148
|
+
content: string;
|
|
149
|
+
depth?: number;
|
|
150
|
+
isCollapsed?: boolean;
|
|
151
|
+
};
|
|
100
152
|
}
|
|
101
153
|
|
|
102
154
|
/**
|
|
@@ -117,6 +169,7 @@ export const initialAgentResponseState: AgentResponseState = {
|
|
|
117
169
|
toolCalls: [],
|
|
118
170
|
knowledge: [],
|
|
119
171
|
memory: [],
|
|
172
|
+
statusUpdates: [],
|
|
120
173
|
response: "",
|
|
121
174
|
thinkingStartTime: null,
|
|
122
175
|
responseCompleteTime: null,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { Send, Loader2 } from "lucide-react";
|
|
2
|
+
import { Send, Loader2, Square } from "lucide-react";
|
|
3
3
|
import { cn, IconButton } from "@optilogic/core";
|
|
4
4
|
import {
|
|
5
5
|
SlateEditor,
|
|
@@ -142,6 +142,11 @@ export const UserPromptInput = React.forwardRef<
|
|
|
142
142
|
placeholder = "Type your message...",
|
|
143
143
|
disabled = false,
|
|
144
144
|
isSubmitting = false,
|
|
145
|
+
onStop,
|
|
146
|
+
disableWhileSubmitting = true,
|
|
147
|
+
autoFocus = false,
|
|
148
|
+
refocusAfterSubmit = false,
|
|
149
|
+
onReady,
|
|
145
150
|
minRows = 1,
|
|
146
151
|
maxRows = 6,
|
|
147
152
|
renderActions,
|
|
@@ -155,17 +160,63 @@ export const UserPromptInput = React.forwardRef<
|
|
|
155
160
|
) => {
|
|
156
161
|
const editorRef = React.useRef<SlateEditorRef>(null);
|
|
157
162
|
const [internalValue, setInternalValue] = React.useState(value);
|
|
163
|
+
const prevIsSubmitting = React.useRef(isSubmitting);
|
|
164
|
+
const hasEmittedReady = React.useRef(false);
|
|
158
165
|
|
|
159
166
|
// Sync internal value with prop
|
|
160
167
|
React.useEffect(() => {
|
|
161
168
|
setInternalValue(value);
|
|
162
169
|
}, [value]);
|
|
163
170
|
|
|
171
|
+
// Handle autoFocus - use double RAF to ensure Slate is fully initialized
|
|
172
|
+
React.useEffect(() => {
|
|
173
|
+
if (autoFocus) {
|
|
174
|
+
requestAnimationFrame(() => {
|
|
175
|
+
requestAnimationFrame(() => {
|
|
176
|
+
editorRef.current?.focus();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}, [autoFocus]);
|
|
181
|
+
|
|
182
|
+
// Emit onReady callback when editor is initialized
|
|
183
|
+
React.useEffect(() => {
|
|
184
|
+
if (!hasEmittedReady.current && onReady) {
|
|
185
|
+
requestAnimationFrame(() => {
|
|
186
|
+
requestAnimationFrame(() => {
|
|
187
|
+
hasEmittedReady.current = true;
|
|
188
|
+
onReady();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}, [onReady]);
|
|
193
|
+
|
|
194
|
+
// Refocus after submit completes
|
|
195
|
+
React.useEffect(() => {
|
|
196
|
+
if (refocusAfterSubmit && prevIsSubmitting.current && !isSubmitting) {
|
|
197
|
+
requestAnimationFrame(() => {
|
|
198
|
+
editorRef.current?.focus();
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
prevIsSubmitting.current = isSubmitting;
|
|
202
|
+
}, [isSubmitting, refocusAfterSubmit]);
|
|
203
|
+
|
|
164
204
|
// Expose ref methods
|
|
165
205
|
React.useImperativeHandle(
|
|
166
206
|
ref,
|
|
167
207
|
() => ({
|
|
168
|
-
focus: () =>
|
|
208
|
+
focus: () => {
|
|
209
|
+
try {
|
|
210
|
+
editorRef.current?.focus();
|
|
211
|
+
} catch {
|
|
212
|
+
// Retry after Slate initializes (handles early calls)
|
|
213
|
+
requestAnimationFrame(() => {
|
|
214
|
+
requestAnimationFrame(() => {
|
|
215
|
+
editorRef.current?.focus();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
},
|
|
169
220
|
clear: () => {
|
|
170
221
|
editorRef.current?.clear();
|
|
171
222
|
setInternalValue("");
|
|
@@ -225,7 +276,7 @@ export const UserPromptInput = React.forwardRef<
|
|
|
225
276
|
onSubmit={handleSubmit}
|
|
226
277
|
clearOnSubmit={false}
|
|
227
278
|
placeholder={placeholder}
|
|
228
|
-
disabled={disabled || isSubmitting}
|
|
279
|
+
disabled={disabled || (disableWhileSubmitting && isSubmitting)}
|
|
229
280
|
enableTags={enableTags}
|
|
230
281
|
onTagCreate={onTagCreate}
|
|
231
282
|
onTagDelete={onTagDelete}
|
|
@@ -241,21 +292,31 @@ export const UserPromptInput = React.forwardRef<
|
|
|
241
292
|
{/* Left actions slot */}
|
|
242
293
|
<div className="flex items-center gap-1">{renderActions?.()}</div>
|
|
243
294
|
|
|
244
|
-
{/* Send button */}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
295
|
+
{/* Send/Stop button */}
|
|
296
|
+
{isSubmitting && onStop ? (
|
|
297
|
+
<IconButton
|
|
298
|
+
icon={<Square />}
|
|
299
|
+
variant="filled"
|
|
300
|
+
size="sm"
|
|
301
|
+
aria-label="Stop"
|
|
302
|
+
onClick={onStop}
|
|
303
|
+
/>
|
|
304
|
+
) : (
|
|
305
|
+
<IconButton
|
|
306
|
+
icon={
|
|
307
|
+
isSubmitting ? (
|
|
308
|
+
<Loader2 className="animate-spin" />
|
|
309
|
+
) : (
|
|
310
|
+
<Send />
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
variant="filled"
|
|
314
|
+
size="sm"
|
|
315
|
+
aria-label={isSubmitting ? "Sending..." : "Send message"}
|
|
316
|
+
disabled={!canSubmit}
|
|
317
|
+
onClick={handleSendClick}
|
|
318
|
+
/>
|
|
319
|
+
)}
|
|
259
320
|
</div>
|
|
260
321
|
</div>
|
|
261
322
|
);
|
|
@@ -16,6 +16,16 @@ export interface UserPromptInputProps
|
|
|
16
16
|
disabled?: boolean;
|
|
17
17
|
/** Whether a submission is in progress (shows loading state) */
|
|
18
18
|
isSubmitting?: boolean;
|
|
19
|
+
/** Called when user clicks Stop during submission */
|
|
20
|
+
onStop?: () => void;
|
|
21
|
+
/** Whether to disable input while submitting (default: true) */
|
|
22
|
+
disableWhileSubmitting?: boolean;
|
|
23
|
+
/** Auto-focus the editor when mounted (handles Slate initialization timing) */
|
|
24
|
+
autoFocus?: boolean;
|
|
25
|
+
/** Refocus the input after submission completes (default: false) */
|
|
26
|
+
refocusAfterSubmit?: boolean;
|
|
27
|
+
/** Called when the editor is fully initialized and ready for interaction */
|
|
28
|
+
onReady?: () => void;
|
|
19
29
|
/** Minimum number of rows for the editor */
|
|
20
30
|
minRows?: number;
|
|
21
31
|
/** Maximum number of rows before scrolling */
|