@optilogic/chat 1.0.0-beta.1 → 1.0.0-beta.10
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 +235 -0
- package/dist/index.cjs +725 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +319 -6
- package/dist/index.d.ts +319 -6
- package/dist/index.js +708 -25
- package/dist/index.js.map +1 -1
- package/package.json +15 -9
- package/src/components/agent-response/AgentResponse.tsx +76 -5
- package/src/components/agent-response/components/ActivityIndicators.tsx +36 -4
- package/src/components/agent-response/components/HITLSection.tsx +95 -0
- package/src/components/agent-response/components/MetadataRow.tsx +21 -6
- package/src/components/agent-response/components/ThinkingSection.tsx +101 -9
- package/src/components/agent-response/components/TruncatedMessage.tsx +52 -0
- package/src/components/agent-response/components/index.ts +6 -0
- package/src/components/agent-response/hooks/useAgentResponseAccumulator.ts +41 -0
- package/src/components/agent-response/index.ts +7 -0
- package/src/components/agent-response/types.ts +54 -1
- package/src/components/hitl-interactions/HITLInteractionRecord.tsx +139 -0
- package/src/components/hitl-interactions/HITLQuestionPanel.tsx +263 -0
- package/src/components/hitl-interactions/index.ts +18 -0
- package/src/components/user-prompt/UserPrompt.tsx +60 -0
- package/src/components/user-prompt/index.ts +1 -0
- package/src/components/user-prompt-input/UserPromptInput.tsx +326 -0
- package/src/components/user-prompt-input/index.ts +2 -0
- package/src/components/user-prompt-input/types.ts +52 -0
- package/src/index.ts +28 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Send, Loader2, Square } from "lucide-react";
|
|
3
|
+
import { cn, IconButton } from "@optilogic/core";
|
|
4
|
+
import {
|
|
5
|
+
SlateEditor,
|
|
6
|
+
Text,
|
|
7
|
+
type SlateEditorRef,
|
|
8
|
+
type NodeEntry,
|
|
9
|
+
type DecoratedRange,
|
|
10
|
+
type RenderLeafProps,
|
|
11
|
+
} from "@optilogic/editor";
|
|
12
|
+
import type { UserPromptInputProps, UserPromptInputRef } from "./types";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates a decorate function that highlights code blocks.
|
|
16
|
+
* Handles both complete (```...```) and unclosed (```...) code blocks.
|
|
17
|
+
*/
|
|
18
|
+
function createCodeBlockDecorate(entry: NodeEntry): DecoratedRange[] {
|
|
19
|
+
const [node, path] = entry;
|
|
20
|
+
const ranges: DecoratedRange[] = [];
|
|
21
|
+
|
|
22
|
+
if (!Text.isText(node)) {
|
|
23
|
+
return ranges;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { text } = node;
|
|
27
|
+
|
|
28
|
+
// Find all ``` positions
|
|
29
|
+
const backtickPositions: number[] = [];
|
|
30
|
+
let searchStart = 0;
|
|
31
|
+
while (true) {
|
|
32
|
+
const pos = text.indexOf("```", searchStart);
|
|
33
|
+
if (pos === -1) break;
|
|
34
|
+
backtickPositions.push(pos);
|
|
35
|
+
searchStart = pos + 3;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Process pairs of backticks (and handle unclosed blocks)
|
|
39
|
+
let i = 0;
|
|
40
|
+
while (i < backtickPositions.length) {
|
|
41
|
+
const openPos = backtickPositions[i];
|
|
42
|
+
const closePos = backtickPositions[i + 1];
|
|
43
|
+
|
|
44
|
+
// Mark opening delimiter
|
|
45
|
+
ranges.push({
|
|
46
|
+
anchor: { path, offset: openPos },
|
|
47
|
+
focus: { path, offset: openPos + 3 },
|
|
48
|
+
codeDelimiter: true,
|
|
49
|
+
} as DecoratedRange);
|
|
50
|
+
|
|
51
|
+
if (closePos !== undefined) {
|
|
52
|
+
// Complete code block - mark content and closing delimiter
|
|
53
|
+
if (closePos > openPos + 3) {
|
|
54
|
+
ranges.push({
|
|
55
|
+
anchor: { path, offset: openPos + 3 },
|
|
56
|
+
focus: { path, offset: closePos },
|
|
57
|
+
codeBlock: true,
|
|
58
|
+
} as DecoratedRange);
|
|
59
|
+
}
|
|
60
|
+
ranges.push({
|
|
61
|
+
anchor: { path, offset: closePos },
|
|
62
|
+
focus: { path, offset: closePos + 3 },
|
|
63
|
+
codeDelimiter: true,
|
|
64
|
+
} as DecoratedRange);
|
|
65
|
+
i += 2; // Move past both opening and closing
|
|
66
|
+
} else {
|
|
67
|
+
// Unclosed code block - mark everything to end as code
|
|
68
|
+
if (text.length > openPos + 3) {
|
|
69
|
+
ranges.push({
|
|
70
|
+
anchor: { path, offset: openPos + 3 },
|
|
71
|
+
focus: { path, offset: text.length },
|
|
72
|
+
codeBlock: true,
|
|
73
|
+
} as DecoratedRange);
|
|
74
|
+
}
|
|
75
|
+
i += 1; // Move past opening only
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return ranges;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Custom leaf renderer for code block styling
|
|
84
|
+
*/
|
|
85
|
+
function CodeBlockLeaf({ attributes, children, leaf }: RenderLeafProps) {
|
|
86
|
+
const leafAny = leaf as { codeBlock?: boolean; codeDelimiter?: boolean };
|
|
87
|
+
|
|
88
|
+
if (leafAny.codeBlock) {
|
|
89
|
+
return (
|
|
90
|
+
<span
|
|
91
|
+
{...attributes}
|
|
92
|
+
className="bg-muted/50 text-muted-foreground font-mono text-sm rounded px-1"
|
|
93
|
+
>
|
|
94
|
+
{children}
|
|
95
|
+
</span>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (leafAny.codeDelimiter) {
|
|
100
|
+
return (
|
|
101
|
+
<span
|
|
102
|
+
{...attributes}
|
|
103
|
+
className="text-muted-foreground/50 font-mono text-sm"
|
|
104
|
+
>
|
|
105
|
+
{children}
|
|
106
|
+
</span>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return <span {...attributes}>{children}</span>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* UserPromptInput Component
|
|
115
|
+
*
|
|
116
|
+
* A rich text input for user messages that wraps SlateEditor.
|
|
117
|
+
* Features:
|
|
118
|
+
* - Code block styling with triple backticks
|
|
119
|
+
* - Send button with loading state
|
|
120
|
+
* - Action slot for additional buttons
|
|
121
|
+
* - Tag support (optional)
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* <UserPromptInput
|
|
125
|
+
* placeholder="Type your message..."
|
|
126
|
+
* onSubmit={(text) => sendMessage(text)}
|
|
127
|
+
* renderActions={() => (
|
|
128
|
+
* <IconButton icon={<Paperclip />} aria-label="Attach file" />
|
|
129
|
+
* )}
|
|
130
|
+
* />
|
|
131
|
+
*/
|
|
132
|
+
export const UserPromptInput = React.forwardRef<
|
|
133
|
+
UserPromptInputRef,
|
|
134
|
+
UserPromptInputProps
|
|
135
|
+
>(
|
|
136
|
+
(
|
|
137
|
+
{
|
|
138
|
+
value = "",
|
|
139
|
+
onChange,
|
|
140
|
+
onSubmit,
|
|
141
|
+
clearOnSubmit = true,
|
|
142
|
+
placeholder = "Type your message...",
|
|
143
|
+
disabled = false,
|
|
144
|
+
isSubmitting = false,
|
|
145
|
+
onStop,
|
|
146
|
+
disableWhileSubmitting = true,
|
|
147
|
+
autoFocus = false,
|
|
148
|
+
refocusAfterSubmit = false,
|
|
149
|
+
onReady,
|
|
150
|
+
minRows = 1,
|
|
151
|
+
maxRows = 6,
|
|
152
|
+
renderActions,
|
|
153
|
+
enableTags = false,
|
|
154
|
+
onTagCreate,
|
|
155
|
+
onTagDelete,
|
|
156
|
+
className,
|
|
157
|
+
...props
|
|
158
|
+
},
|
|
159
|
+
ref
|
|
160
|
+
) => {
|
|
161
|
+
const editorRef = React.useRef<SlateEditorRef>(null);
|
|
162
|
+
const [internalValue, setInternalValue] = React.useState(value);
|
|
163
|
+
const prevIsSubmitting = React.useRef(isSubmitting);
|
|
164
|
+
const hasEmittedReady = React.useRef(false);
|
|
165
|
+
|
|
166
|
+
// Sync internal value with prop
|
|
167
|
+
React.useEffect(() => {
|
|
168
|
+
setInternalValue(value);
|
|
169
|
+
}, [value]);
|
|
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
|
+
|
|
204
|
+
// Expose ref methods
|
|
205
|
+
React.useImperativeHandle(
|
|
206
|
+
ref,
|
|
207
|
+
() => ({
|
|
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
|
+
},
|
|
220
|
+
clear: () => {
|
|
221
|
+
editorRef.current?.clear();
|
|
222
|
+
setInternalValue("");
|
|
223
|
+
},
|
|
224
|
+
getText: () => editorRef.current?.getText() ?? "",
|
|
225
|
+
insertText: (text: string) => editorRef.current?.insertText(text),
|
|
226
|
+
}),
|
|
227
|
+
[]
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const handleChange = React.useCallback(
|
|
231
|
+
(newValue: string) => {
|
|
232
|
+
setInternalValue(newValue);
|
|
233
|
+
onChange?.(newValue);
|
|
234
|
+
},
|
|
235
|
+
[onChange]
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const handleSubmit = React.useCallback(
|
|
239
|
+
(text: string) => {
|
|
240
|
+
if (disabled || isSubmitting) return;
|
|
241
|
+
if (!text.trim()) return;
|
|
242
|
+
|
|
243
|
+
onSubmit?.(text.trim());
|
|
244
|
+
|
|
245
|
+
if (clearOnSubmit) {
|
|
246
|
+
editorRef.current?.clear();
|
|
247
|
+
setInternalValue("");
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
[disabled, isSubmitting, onSubmit, clearOnSubmit]
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const handleSendClick = React.useCallback(() => {
|
|
254
|
+
const text = editorRef.current?.getText() ?? "";
|
|
255
|
+
handleSubmit(text);
|
|
256
|
+
}, [handleSubmit]);
|
|
257
|
+
|
|
258
|
+
const hasContent = internalValue.trim().length > 0;
|
|
259
|
+
const canSubmit = hasContent && !disabled && !isSubmitting;
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<div
|
|
263
|
+
className={cn(
|
|
264
|
+
"rounded-lg border border-input bg-background",
|
|
265
|
+
disabled && "opacity-50 cursor-not-allowed",
|
|
266
|
+
className
|
|
267
|
+
)}
|
|
268
|
+
{...props}
|
|
269
|
+
>
|
|
270
|
+
{/* Editor area */}
|
|
271
|
+
<div className="pl-2 pr-0 pt-1 pb-1">
|
|
272
|
+
<SlateEditor
|
|
273
|
+
ref={editorRef}
|
|
274
|
+
value={internalValue}
|
|
275
|
+
onChange={handleChange}
|
|
276
|
+
onSubmit={handleSubmit}
|
|
277
|
+
clearOnSubmit={false}
|
|
278
|
+
placeholder={placeholder}
|
|
279
|
+
disabled={disabled || (disableWhileSubmitting && isSubmitting)}
|
|
280
|
+
enableTags={enableTags}
|
|
281
|
+
onTagCreate={onTagCreate}
|
|
282
|
+
onTagDelete={onTagDelete}
|
|
283
|
+
minRows={minRows}
|
|
284
|
+
maxRows={maxRows}
|
|
285
|
+
decorate={createCodeBlockDecorate}
|
|
286
|
+
renderLeaf={CodeBlockLeaf}
|
|
287
|
+
/>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
{/* Actions row */}
|
|
291
|
+
<div className="flex items-center justify-between pl-2 pr-1 pb-1 pt-1">
|
|
292
|
+
{/* Left actions slot */}
|
|
293
|
+
<div className="flex items-center gap-1">{renderActions?.()}</div>
|
|
294
|
+
|
|
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
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
UserPromptInput.displayName = "UserPromptInput";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
export interface UserPromptInputProps
|
|
4
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange" | "onSubmit"> {
|
|
5
|
+
/** Current text value */
|
|
6
|
+
value?: string;
|
|
7
|
+
/** Callback when text changes */
|
|
8
|
+
onChange?: (value: string) => void;
|
|
9
|
+
/** Callback when user submits (Enter key or send button) */
|
|
10
|
+
onSubmit?: (text: string) => void;
|
|
11
|
+
/** Clear input after submit (default: true) */
|
|
12
|
+
clearOnSubmit?: boolean;
|
|
13
|
+
/** Placeholder text */
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
/** Whether the input is disabled */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Whether a submission is in progress (shows loading state) */
|
|
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;
|
|
29
|
+
/** Minimum number of rows for the editor */
|
|
30
|
+
minRows?: number;
|
|
31
|
+
/** Maximum number of rows before scrolling */
|
|
32
|
+
maxRows?: number;
|
|
33
|
+
/** Render function for additional action buttons (left side) */
|
|
34
|
+
renderActions?: () => React.ReactNode;
|
|
35
|
+
/** Enable tag detection with # character */
|
|
36
|
+
enableTags?: boolean;
|
|
37
|
+
/** Callback when a tag is created */
|
|
38
|
+
onTagCreate?: (tag: string) => void;
|
|
39
|
+
/** Callback when a tag is deleted */
|
|
40
|
+
onTagDelete?: (tag: string) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface UserPromptInputRef {
|
|
44
|
+
/** Focus the editor */
|
|
45
|
+
focus: () => void;
|
|
46
|
+
/** Clear the editor content */
|
|
47
|
+
clear: () => void;
|
|
48
|
+
/** Get the current text content */
|
|
49
|
+
getText: () => string;
|
|
50
|
+
/** Insert text at cursor position */
|
|
51
|
+
insertText: (text: string) => void;
|
|
52
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -15,10 +15,14 @@ export {
|
|
|
15
15
|
MetadataRow,
|
|
16
16
|
ThinkingSection,
|
|
17
17
|
ActionBar,
|
|
18
|
+
HITLSection,
|
|
19
|
+
TruncatedMessage,
|
|
18
20
|
type ActivityIndicatorsProps,
|
|
19
21
|
type MetadataRowProps,
|
|
20
22
|
type ThinkingSectionProps,
|
|
21
23
|
type ActionBarProps,
|
|
24
|
+
type HITLSectionProps,
|
|
25
|
+
type TruncatedMessageProps,
|
|
22
26
|
|
|
23
27
|
// Hooks
|
|
24
28
|
useAgentResponseAccumulator,
|
|
@@ -34,6 +38,9 @@ export {
|
|
|
34
38
|
type ToolCall,
|
|
35
39
|
type KnowledgeItem,
|
|
36
40
|
type MemoryItem,
|
|
41
|
+
type StatusItem,
|
|
42
|
+
type ThinkingStep,
|
|
43
|
+
type ThinkingContent,
|
|
37
44
|
type AgentMessage,
|
|
38
45
|
type GenericWebSocketMessage,
|
|
39
46
|
|
|
@@ -44,3 +51,24 @@ export {
|
|
|
44
51
|
formatTime,
|
|
45
52
|
formatTotalTime,
|
|
46
53
|
} from "./components/agent-response";
|
|
54
|
+
|
|
55
|
+
// User Prompt - Component for displaying user messages
|
|
56
|
+
export { UserPrompt, type UserPromptProps } from "./components/user-prompt";
|
|
57
|
+
|
|
58
|
+
// User Prompt Input - Component for user message input
|
|
59
|
+
export {
|
|
60
|
+
UserPromptInput,
|
|
61
|
+
type UserPromptInputProps,
|
|
62
|
+
type UserPromptInputRef,
|
|
63
|
+
} from "./components/user-prompt-input";
|
|
64
|
+
|
|
65
|
+
// HITL Interactions - Human-in-the-loop question/response components
|
|
66
|
+
export {
|
|
67
|
+
HITLQuestionPanel,
|
|
68
|
+
type HITLQuestionPanelProps,
|
|
69
|
+
HITLInteractionRecord,
|
|
70
|
+
type HITLInteractionRecordProps,
|
|
71
|
+
type HITLQuestion,
|
|
72
|
+
type HITLInteraction,
|
|
73
|
+
buildResponseString,
|
|
74
|
+
} from "./components/hitl-interactions";
|