@optilogic/chat 1.0.0-beta.6 → 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 ADDED
@@ -0,0 +1,99 @@
1
+ # @optilogic/chat
2
+
3
+ Chat UI components for opti-ui - Components for displaying LLM/AI agent interactions.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @optilogic/chat @optilogic/core @optilogic/editor slate slate-react slate-history
9
+ ```
10
+
11
+ ## Setup
12
+
13
+ Make sure you have configured `@optilogic/core` with the Tailwind preset and CSS variables.
14
+
15
+ ## Usage
16
+
17
+ ### AgentResponse
18
+
19
+ Display AI agent responses with thinking indicators, tool calls, and feedback actions:
20
+
21
+ ```tsx
22
+ import { AgentResponse, useAgentResponseAccumulator } from '@optilogic/chat';
23
+
24
+ function ChatView() {
25
+ const { state, handlers } = useAgentResponseAccumulator({
26
+ onComplete: (response) => console.log('Response complete:', response),
27
+ });
28
+
29
+ return (
30
+ <AgentResponse
31
+ {...state}
32
+ onCopy={() => navigator.clipboard.writeText(state.content)}
33
+ onFeedback={(value) => console.log('Feedback:', value)}
34
+ />
35
+ );
36
+ }
37
+ ```
38
+
39
+ ### UserPrompt
40
+
41
+ Display user messages in the chat:
42
+
43
+ ```tsx
44
+ import { UserPrompt } from '@optilogic/chat';
45
+
46
+ function ChatMessage() {
47
+ return (
48
+ <UserPrompt
49
+ content="What is the weather today?"
50
+ timestamp={new Date()}
51
+ />
52
+ );
53
+ }
54
+ ```
55
+
56
+ ### UserPromptInput
57
+
58
+ Input component for user messages:
59
+
60
+ ```tsx
61
+ import { UserPromptInput } from '@optilogic/chat';
62
+
63
+ function ChatInput() {
64
+ return (
65
+ <UserPromptInput
66
+ onSubmit={(text) => console.log('Submitted:', text)}
67
+ placeholder="Type your message..."
68
+ />
69
+ );
70
+ }
71
+ ```
72
+
73
+ ## Components
74
+
75
+ ### AgentResponse
76
+ Main component for displaying AI agent responses with:
77
+ - Streaming content support
78
+ - Thinking/reasoning indicators
79
+ - Tool call visualization
80
+ - Copy and feedback actions
81
+ - Metadata display (tokens, timing)
82
+
83
+ ### UserPrompt
84
+ Component for displaying user messages in the chat interface.
85
+
86
+ ### UserPromptInput
87
+ Input component for composing user messages with tag support.
88
+
89
+ ## Hooks
90
+
91
+ ### useAgentResponseAccumulator
92
+ Manages state for streaming agent responses, handling incremental updates and message accumulation.
93
+
94
+ ### useThinkingTimer
95
+ Tracks elapsed time during agent thinking phases.
96
+
97
+ ## License
98
+
99
+ MIT
package/dist/index.cjs CHANGED
@@ -28,10 +28,30 @@ var React7__namespace = /*#__PURE__*/_interopNamespace(React7);
28
28
 
29
29
  // src/components/agent-response/AgentResponse.tsx
30
30
  var ActivityIndicators = React7__namespace.forwardRef(
31
- ({ toolCalls, knowledge, memory, className, ...props }, ref) => {
32
- const hasAnyActivity = toolCalls.length > 0 || knowledge.length > 0 || memory.length > 0;
31
+ ({ toolCalls, knowledge, memory, statusUpdates = [], className, ...props }, ref) => {
32
+ const hasAnyActivity = toolCalls.length > 0 || knowledge.length > 0 || memory.length > 0 || statusUpdates.length > 0;
33
33
  if (!hasAnyActivity) return null;
34
34
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref, className: core.cn("flex items-center gap-2", className), ...props, children: [
35
+ statusUpdates.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(core.Popover, { children: [
36
+ /* @__PURE__ */ jsxRuntime.jsx(core.PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
37
+ "button",
38
+ {
39
+ className: "flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors",
40
+ onClick: (e) => e.stopPropagation(),
41
+ children: [
42
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Activity, { className: "w-3.5 h-3.5" }),
43
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs", children: statusUpdates.length })
44
+ ]
45
+ }
46
+ ) }),
47
+ /* @__PURE__ */ jsxRuntime.jsx(core.PopoverContent, { className: "w-80", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
48
+ /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "font-medium text-sm", children: "Status Updates" }),
49
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2 max-h-60 overflow-auto", children: statusUpdates.map((item) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-2 bg-muted rounded text-xs", children: [
50
+ item.agent && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-medium text-muted-foreground", children: item.agent }),
51
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: item.agent ? "mt-1" : "", children: item.message })
52
+ ] }, item.id)) })
53
+ ] }) })
54
+ ] }),
35
55
  toolCalls.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(core.Popover, { children: [
36
56
  /* @__PURE__ */ jsxRuntime.jsx(core.PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
37
57
  "button",
@@ -134,6 +154,7 @@ var MetadataRow = React7__namespace.forwardRef(
134
154
  toolCalls,
135
155
  knowledge,
136
156
  memory,
157
+ statusUpdates = [],
137
158
  status,
138
159
  elapsedTime,
139
160
  className,
@@ -141,7 +162,7 @@ var MetadataRow = React7__namespace.forwardRef(
141
162
  }, ref) => {
142
163
  const isProcessing = status === "processing";
143
164
  const isComplete = status === "complete";
144
- const hasActivity = toolCalls.length > 0 || knowledge.length > 0 || memory.length > 0;
165
+ const hasActivity = toolCalls.length > 0 || knowledge.length > 0 || memory.length > 0 || statusUpdates.length > 0;
145
166
  const renderLeftContent = () => {
146
167
  if (hasThinking) {
147
168
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
@@ -181,7 +202,8 @@ var MetadataRow = React7__namespace.forwardRef(
181
202
  {
182
203
  toolCalls,
183
204
  knowledge,
184
- memory
205
+ memory,
206
+ statusUpdates
185
207
  }
186
208
  )
187
209
  ]
@@ -190,18 +212,55 @@ var MetadataRow = React7__namespace.forwardRef(
190
212
  }
191
213
  );
192
214
  MetadataRow.displayName = "MetadataRow";
215
+ var ThinkingStepItem = ({ step, renderMarkdown }) => {
216
+ const [isCollapsed, setIsCollapsed] = React7.useState(step.isCollapsed ?? false);
217
+ const toggleCollapse = React7.useCallback(() => {
218
+ setIsCollapsed((prev) => !prev);
219
+ }, []);
220
+ const indentPadding = step.depth * 16;
221
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-b border-border/50 last:border-b-0", children: [
222
+ /* @__PURE__ */ jsxRuntime.jsxs(
223
+ "button",
224
+ {
225
+ onClick: toggleCollapse,
226
+ className: "w-full flex items-center gap-1.5 py-1.5 px-2 hover:bg-muted/50 transition-colors text-left",
227
+ style: { paddingLeft: `${indentPadding + 8}px` },
228
+ children: [
229
+ isCollapsed ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { className: "w-3 h-3 text-muted-foreground flex-shrink-0" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { className: "w-3 h-3 text-muted-foreground flex-shrink-0" }),
230
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-foreground/80", children: step.label })
231
+ ]
232
+ }
233
+ ),
234
+ !isCollapsed && /* @__PURE__ */ jsxRuntime.jsx(
235
+ "div",
236
+ {
237
+ className: "pb-2 px-2",
238
+ style: { paddingLeft: `${indentPadding + 28}px` },
239
+ children: renderMarkdown ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-muted-foreground", children: renderMarkdown(step.content) }) : /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "text-xs text-muted-foreground whitespace-pre-wrap font-mono", children: step.content })
240
+ }
241
+ )
242
+ ] });
243
+ };
193
244
  var ThinkingSection = React7__namespace.forwardRef(
194
- ({ content, isExpanded, className, ...props }, ref) => {
195
- if (!isExpanded || !content) {
245
+ ({ content, isExpanded, renderMarkdown, className, ...props }, ref) => {
246
+ if (!isExpanded || !content || Array.isArray(content) && content.length === 0) {
196
247
  return null;
197
248
  }
249
+ const isStructured = Array.isArray(content);
198
250
  return /* @__PURE__ */ jsxRuntime.jsx(
199
251
  "div",
200
252
  {
201
253
  ref,
202
254
  className: core.cn("px-3 pb-3 border-t border-border", className),
203
255
  ...props,
204
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 max-h-[200px] overflow-y-auto", children: /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "text-xs text-muted-foreground whitespace-pre-wrap font-mono", children: content }) })
256
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 max-h-[200px] overflow-y-auto", children: isStructured ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-0", children: content.map((step) => /* @__PURE__ */ jsxRuntime.jsx(
257
+ ThinkingStepItem,
258
+ {
259
+ step,
260
+ renderMarkdown
261
+ },
262
+ step.id
263
+ )) }) : renderMarkdown ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-muted-foreground", children: renderMarkdown(content) }) : /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "text-xs text-muted-foreground whitespace-pre-wrap font-mono", children: content }) })
205
264
  }
206
265
  );
207
266
  }
@@ -330,6 +389,7 @@ var initialAgentResponseState = {
330
389
  toolCalls: [],
331
390
  knowledge: [],
332
391
  memory: [],
392
+ statusUpdates: [],
333
393
  response: "",
334
394
  thinkingStartTime: null,
335
395
  responseCompleteTime: null,
@@ -364,6 +424,23 @@ function useAgentResponseAccumulator(options) {
364
424
  }
365
425
  return { ...prev, status: newStatus };
366
426
  case "thinking": {
427
+ if (payload.thinkingStep) {
428
+ const newStep = {
429
+ id: payload.thinkingStep.id || `step-${Date.now()}`,
430
+ label: payload.thinkingStep.label,
431
+ content: payload.thinkingStep.content,
432
+ depth: payload.thinkingStep.depth ?? 0,
433
+ isCollapsed: payload.thinkingStep.isCollapsed
434
+ };
435
+ const thinkingStartTime2 = prev.thinkingStartTime ?? Date.now();
436
+ return {
437
+ ...prev,
438
+ status: newStatus,
439
+ thinkingSteps: [...prev.thinkingSteps || [], newStep],
440
+ thinkingStartTime: thinkingStartTime2,
441
+ firstMessageTime
442
+ };
443
+ }
367
444
  const newThinking = payload.message || payload.content || "";
368
445
  const separator = prev.thinking && newThinking ? "\n\n" : "";
369
446
  const thinkingStartTime = prev.thinkingStartTime ?? (newThinking ? Date.now() : null);
@@ -437,6 +514,24 @@ function useAgentResponseAccumulator(options) {
437
514
  responseCompleteTime: Date.now(),
438
515
  firstMessageTime: prev.firstMessageTime ?? Date.now()
439
516
  };
517
+ case "status_update": {
518
+ const statusMessage = payload.message || payload.statusUpdate?.message;
519
+ if (statusMessage) {
520
+ const newStatusItem = {
521
+ id: payload.statusUpdate?.id || `status-${Date.now()}`,
522
+ message: statusMessage,
523
+ agent: payload.statusUpdate?.agent,
524
+ timestamp: Date.now()
525
+ };
526
+ return {
527
+ ...prev,
528
+ status: newStatus,
529
+ statusUpdates: [...prev.statusUpdates, newStatusItem],
530
+ firstMessageTime
531
+ };
532
+ }
533
+ return { ...prev, status: newStatus, firstMessageTime };
534
+ }
440
535
  default:
441
536
  return { ...prev, status: newStatus, firstMessageTime };
442
537
  }
@@ -462,6 +557,7 @@ var AgentResponse = React7__namespace.forwardRef(
462
557
  onThinkingExpandedChange,
463
558
  actionsVisible = "hover",
464
559
  renderMarkdown,
560
+ renderThinkingMarkdown,
465
561
  className,
466
562
  ...props
467
563
  }, ref) => {
@@ -486,8 +582,9 @@ var AgentResponse = React7__namespace.forwardRef(
486
582
  if (!state.firstMessageTime || !state.responseCompleteTime) return 0;
487
583
  return (state.responseCompleteTime - state.firstMessageTime) / 1e3;
488
584
  }, [state.firstMessageTime, state.responseCompleteTime]);
489
- const hasAnyContent = state.thinking || state.toolCalls.length > 0 || state.knowledge.length > 0 || state.memory.length > 0 || state.response;
490
- const showMetadataRow = state.thinking || state.toolCalls.length > 0 || state.knowledge.length > 0 || state.memory.length > 0 || state.status === "processing";
585
+ const hasThinkingContent = !!state.thinking || state.thinkingSteps && state.thinkingSteps.length > 0 || false;
586
+ const hasAnyContent = hasThinkingContent || state.toolCalls.length > 0 || state.knowledge.length > 0 || state.memory.length > 0 || state.statusUpdates.length > 0 || state.response;
587
+ const showMetadataRow = hasThinkingContent || state.toolCalls.length > 0 || state.knowledge.length > 0 || state.memory.length > 0 || state.statusUpdates.length > 0 || state.status === "processing";
491
588
  const showActionBar = state.status === "complete" && state.response;
492
589
  const isActionBarVisible = actionsVisible === true || actionsVisible === "hover" && isHovered;
493
590
  if (!hasAnyContent) {
@@ -507,12 +604,13 @@ var AgentResponse = React7__namespace.forwardRef(
507
604
  /* @__PURE__ */ jsxRuntime.jsx(
508
605
  MetadataRow,
509
606
  {
510
- hasThinking: !!state.thinking,
607
+ hasThinking: hasThinkingContent,
511
608
  isExpanded: thinkingExpanded,
512
609
  onToggle: toggleThinking,
513
610
  toolCalls: state.toolCalls,
514
611
  knowledge: state.knowledge,
515
612
  memory: state.memory,
613
+ statusUpdates: state.statusUpdates,
516
614
  status: state.status,
517
615
  elapsedTime
518
616
  }
@@ -520,8 +618,9 @@ var AgentResponse = React7__namespace.forwardRef(
520
618
  /* @__PURE__ */ jsxRuntime.jsx(
521
619
  ThinkingSection,
522
620
  {
523
- content: state.thinking,
524
- isExpanded: thinkingExpanded
621
+ content: state.thinkingSteps && state.thinkingSteps.length > 0 ? state.thinkingSteps : state.thinking,
622
+ isExpanded: thinkingExpanded,
623
+ renderMarkdown: renderThinkingMarkdown
525
624
  }
526
625
  )
527
626
  ] }),
@@ -661,6 +760,11 @@ var UserPromptInput = React7__namespace.forwardRef(
661
760
  placeholder = "Type your message...",
662
761
  disabled = false,
663
762
  isSubmitting = false,
763
+ onStop,
764
+ disableWhileSubmitting = true,
765
+ autoFocus = false,
766
+ refocusAfterSubmit = false,
767
+ onReady,
664
768
  minRows = 1,
665
769
  maxRows = 6,
666
770
  renderActions,
@@ -672,13 +776,52 @@ var UserPromptInput = React7__namespace.forwardRef(
672
776
  }, ref) => {
673
777
  const editorRef = React7__namespace.useRef(null);
674
778
  const [internalValue, setInternalValue] = React7__namespace.useState(value);
779
+ const prevIsSubmitting = React7__namespace.useRef(isSubmitting);
780
+ const hasEmittedReady = React7__namespace.useRef(false);
675
781
  React7__namespace.useEffect(() => {
676
782
  setInternalValue(value);
677
783
  }, [value]);
784
+ React7__namespace.useEffect(() => {
785
+ if (autoFocus) {
786
+ requestAnimationFrame(() => {
787
+ requestAnimationFrame(() => {
788
+ editorRef.current?.focus();
789
+ });
790
+ });
791
+ }
792
+ }, [autoFocus]);
793
+ React7__namespace.useEffect(() => {
794
+ if (!hasEmittedReady.current && onReady) {
795
+ requestAnimationFrame(() => {
796
+ requestAnimationFrame(() => {
797
+ hasEmittedReady.current = true;
798
+ onReady();
799
+ });
800
+ });
801
+ }
802
+ }, [onReady]);
803
+ React7__namespace.useEffect(() => {
804
+ if (refocusAfterSubmit && prevIsSubmitting.current && !isSubmitting) {
805
+ requestAnimationFrame(() => {
806
+ editorRef.current?.focus();
807
+ });
808
+ }
809
+ prevIsSubmitting.current = isSubmitting;
810
+ }, [isSubmitting, refocusAfterSubmit]);
678
811
  React7__namespace.useImperativeHandle(
679
812
  ref,
680
813
  () => ({
681
- focus: () => editorRef.current?.focus(),
814
+ focus: () => {
815
+ try {
816
+ editorRef.current?.focus();
817
+ } catch {
818
+ requestAnimationFrame(() => {
819
+ requestAnimationFrame(() => {
820
+ editorRef.current?.focus();
821
+ });
822
+ });
823
+ }
824
+ },
682
825
  clear: () => {
683
826
  editorRef.current?.clear();
684
827
  setInternalValue("");
@@ -732,7 +875,7 @@ var UserPromptInput = React7__namespace.forwardRef(
732
875
  onSubmit: handleSubmit,
733
876
  clearOnSubmit: false,
734
877
  placeholder,
735
- disabled: disabled || isSubmitting,
878
+ disabled: disabled || disableWhileSubmitting && isSubmitting,
736
879
  enableTags,
737
880
  onTagCreate,
738
881
  onTagDelete,
@@ -744,7 +887,16 @@ var UserPromptInput = React7__namespace.forwardRef(
744
887
  ) }),
745
888
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between pl-2 pr-1 pb-1 pt-1", children: [
746
889
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-1", children: renderActions?.() }),
747
- /* @__PURE__ */ jsxRuntime.jsx(
890
+ isSubmitting && onStop ? /* @__PURE__ */ jsxRuntime.jsx(
891
+ core.IconButton,
892
+ {
893
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Square, {}),
894
+ variant: "filled",
895
+ size: "sm",
896
+ "aria-label": "Stop",
897
+ onClick: onStop
898
+ }
899
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
748
900
  core.IconButton,
749
901
  {
750
902
  icon: isSubmitting ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Send, {}),