@optilogic/chat 1.0.0-beta.1 → 1.0.0-beta.11

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.
Files changed (38) hide show
  1. package/README.md +235 -0
  2. package/dist/index.cjs +1292 -43
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +524 -6
  5. package/dist/index.d.ts +524 -6
  6. package/dist/index.js +1267 -33
  7. package/dist/index.js.map +1 -1
  8. package/package.json +15 -9
  9. package/src/components/agent-response/AgentResponse.tsx +99 -10
  10. package/src/components/agent-response/components/ActivityIndicators.tsx +36 -4
  11. package/src/components/agent-response/components/HITLSection.tsx +95 -0
  12. package/src/components/agent-response/components/MetadataRow.tsx +21 -6
  13. package/src/components/agent-response/components/ThinkingSection.tsx +102 -10
  14. package/src/components/agent-response/components/TruncatedMessage.tsx +52 -0
  15. package/src/components/agent-response/components/index.ts +6 -0
  16. package/src/components/agent-response/hooks/useAgentResponseAccumulator.ts +79 -4
  17. package/src/components/agent-response/index.ts +23 -0
  18. package/src/components/agent-response/types.ts +96 -1
  19. package/src/components/agent-timeline/AgentTimeline.tsx +256 -0
  20. package/src/components/agent-timeline/TimelineAgentBlock.tsx +84 -0
  21. package/src/components/agent-timeline/TimelineItem.tsx +97 -0
  22. package/src/components/agent-timeline/index.ts +14 -0
  23. package/src/components/agent-timeline/types.ts +49 -0
  24. package/src/components/agent-timeline/utils.ts +167 -0
  25. package/src/components/hitl-interactions/HITLInteractionRecord.tsx +139 -0
  26. package/src/components/hitl-interactions/HITLQuestionPanel.tsx +270 -0
  27. package/src/components/hitl-interactions/index.ts +18 -0
  28. package/src/components/inline-actions/ActionMarkdownRenderer.tsx +60 -0
  29. package/src/components/inline-actions/index.ts +18 -0
  30. package/src/components/inline-actions/parseResponseSegments.ts +66 -0
  31. package/src/components/inline-actions/prompts.ts +41 -0
  32. package/src/components/inline-actions/types.ts +57 -0
  33. package/src/components/user-prompt/UserPrompt.tsx +60 -0
  34. package/src/components/user-prompt/index.ts +1 -0
  35. package/src/components/user-prompt-input/UserPromptInput.tsx +326 -0
  36. package/src/components/user-prompt-input/index.ts +2 -0
  37. package/src/components/user-prompt-input/types.ts +52 -0
  38. package/src/index.ts +54 -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,2 @@
1
+ export { UserPromptInput } from "./UserPromptInput";
2
+ export type { UserPromptInputProps, UserPromptInputRef } from "./types";
@@ -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
 
@@ -43,4 +50,51 @@ export {
43
50
  // Utilities
44
51
  formatTime,
45
52
  formatTotalTime,
53
+
54
+ // Agent Timeline (replaces ThinkingSection for rich execution visibility)
55
+ AgentTimeline,
56
+ createTimelineUIState,
57
+ buildTimelineEntries,
58
+ groupIntoAgentRuns,
59
+ deduplicateEntries,
60
+ type TimelineUIState,
61
+ type TimelineEntry,
62
+ type TimelineEntryType,
63
+ type AgentRun,
64
+ type DisplayEntry,
46
65
  } from "./components/agent-response";
66
+
67
+ // User Prompt - Component for displaying user messages
68
+ export { UserPrompt, type UserPromptProps } from "./components/user-prompt";
69
+
70
+ // User Prompt Input - Component for user message input
71
+ export {
72
+ UserPromptInput,
73
+ type UserPromptInputProps,
74
+ type UserPromptInputRef,
75
+ } from "./components/user-prompt-input";
76
+
77
+ // HITL Interactions - Human-in-the-loop question/response components
78
+ export {
79
+ HITLQuestionPanel,
80
+ type HITLQuestionPanelProps,
81
+ HITLInteractionRecord,
82
+ type HITLInteractionRecordProps,
83
+ type HITLQuestion,
84
+ type HITLInteraction,
85
+ type HITLResponseData,
86
+ buildResponseString,
87
+ } from "./components/hitl-interactions";
88
+
89
+ // Inline Actions - Components for rendering interactive actions within markdown
90
+ export {
91
+ ActionMarkdownRenderer,
92
+ parseResponseSegments,
93
+ INLINE_ACTION_PROMPT,
94
+ type ResponseSegment,
95
+ type MarkdownSegment,
96
+ type ActionSegment,
97
+ type InlineActionProps,
98
+ type ActionComponentRegistry,
99
+ type ActionMarkdownRendererProps,
100
+ } from "./components/inline-actions";