@autobe/ui 0.29.2 → 0.30.0-dev.20260315

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 (151) hide show
  1. package/LICENSE +661 -661
  2. package/README.md +261 -0
  3. package/lib/components/AutoBeChatMain.js +5 -5
  4. package/lib/components/AutoBeChatMain.js.map +1 -1
  5. package/lib/components/AutoBeConfigModal.js +9 -9
  6. package/lib/components/AutoBeStatusModal.js +4 -4
  7. package/lib/components/AutoBeStatusModal.js.map +1 -1
  8. package/lib/components/AutoBeUserMessageMovie.d.ts +2 -2
  9. package/lib/components/common/ChatBubble.d.ts +2 -2
  10. package/lib/components/common/openai/OpenAIContent.d.ts +2 -2
  11. package/lib/components/common/openai/OpenAIContent.js.map +1 -1
  12. package/lib/components/common/openai/OpenAIUserAudioContent.js +1 -1
  13. package/lib/components/common/openai/OpenAIUserAudioContent.js.map +1 -1
  14. package/lib/components/common/openai/OpenAIUserFileContent.js +1 -1
  15. package/lib/components/common/openai/OpenAIUserFileContent.js.map +1 -1
  16. package/lib/components/common/openai/OpenAIUserImageContent.d.ts +2 -2
  17. package/lib/components/events/AutoBeCompleteEventMovie.d.ts +2 -2
  18. package/lib/components/events/AutoBeCompleteEventMovie.js +5 -5
  19. package/lib/components/events/AutoBeCompleteEventMovie.js.map +1 -1
  20. package/lib/components/events/AutoBeCorrectEventMovie.d.ts +2 -2
  21. package/lib/components/events/AutoBeCorrectEventMovie.js +4 -4
  22. package/lib/components/events/AutoBeCorrectEventMovie.js.map +1 -1
  23. package/lib/components/events/AutoBeEventMovie.js +38 -17
  24. package/lib/components/events/AutoBeEventMovie.js.map +1 -1
  25. package/lib/components/events/AutoBeProgressEventMovie.js +73 -13
  26. package/lib/components/events/AutoBeProgressEventMovie.js.map +1 -1
  27. package/lib/components/events/AutoBeScenarioEventMovie.d.ts +2 -2
  28. package/lib/components/events/AutoBeScenarioEventMovie.js +18 -5
  29. package/lib/components/events/AutoBeScenarioEventMovie.js.map +1 -1
  30. package/lib/components/events/AutoBeStartEventMovie.d.ts +2 -2
  31. package/lib/components/events/AutoBeStartEventMovie.js +2 -2
  32. package/lib/components/events/AutoBeStartEventMovie.js.map +1 -1
  33. package/lib/components/events/AutoBeValidateEventMovie.d.ts +2 -2
  34. package/lib/components/events/AutoBeValidateEventMovie.js +3 -11
  35. package/lib/components/events/AutoBeValidateEventMovie.js.map +1 -1
  36. package/lib/components/events/groups/CorrectEventGroup.d.ts +2 -2
  37. package/lib/components/events/groups/CorrectEventGroup.js +1 -1
  38. package/lib/components/events/groups/CorrectEventGroup.js.map +1 -1
  39. package/lib/components/events/groups/ValidateEventGroup.d.ts +2 -2
  40. package/lib/components/events/groups/ValidateEventGroup.js +1 -2
  41. package/lib/components/events/groups/ValidateEventGroup.js.map +1 -1
  42. package/lib/components/events/utils/eventGrouper.js +1 -2
  43. package/lib/components/events/utils/eventGrouper.js.map +1 -1
  44. package/lib/components/upload/AutoBeChatUploadBox.d.ts +3 -4
  45. package/lib/components/upload/AutoBeChatUploadBox.js +2 -1
  46. package/lib/components/upload/AutoBeChatUploadBox.js.map +1 -1
  47. package/lib/components/upload/AutoBeChatUploadSendButton.js +1 -1
  48. package/lib/components/upload/AutoBeChatUploadSendButton.js.map +1 -1
  49. package/lib/context/AutoBeAgentContext.d.ts +1 -3
  50. package/lib/context/AutoBeAgentContext.js +0 -4
  51. package/lib/context/AutoBeAgentContext.js.map +1 -1
  52. package/lib/hooks/useSessionStorage.d.ts +4 -0
  53. package/lib/hooks/useSessionStorage.js +16 -0
  54. package/lib/hooks/useSessionStorage.js.map +1 -0
  55. package/lib/index.d.ts +1 -0
  56. package/lib/index.js +1 -0
  57. package/lib/index.js.map +1 -1
  58. package/lib/strategy/AutoBeAgentSessionStorageStrategy.d.ts +10 -0
  59. package/lib/strategy/AutoBeAgentSessionStorageStrategy.js +117 -0
  60. package/lib/strategy/AutoBeAgentSessionStorageStrategy.js.map +1 -0
  61. package/lib/structure/AutoBeListener.js +91 -23
  62. package/lib/structure/AutoBeListener.js.map +1 -1
  63. package/lib/structure/AutoBeListenerState.d.ts +3 -3
  64. package/lib/structure/AutoBeListenerState.js +4 -4
  65. package/lib/structure/AutoBeListenerState.js.map +1 -1
  66. package/lib/structure/IAutoBeAgentSessionStorageStrategy.js +1 -1
  67. package/lib/structure/IAutoBeAgentSessionStorageStrategy.js.map +1 -1
  68. package/lib/utils/AutoBeFileUploader.d.ts +2 -2
  69. package/lib/utils/AutoBeFileUploader.js.map +1 -1
  70. package/package.json +3 -4
  71. package/src/components/AutoBeAssistantMessageMovie.tsx +22 -22
  72. package/src/components/AutoBeChatMain.tsx +376 -376
  73. package/src/components/AutoBeChatSidebar.tsx +414 -414
  74. package/src/components/AutoBeConfigButton.tsx +83 -83
  75. package/src/components/AutoBeConfigModal.tsx +443 -443
  76. package/src/components/AutoBeStatusButton.tsx +75 -75
  77. package/src/components/AutoBeStatusModal.tsx +486 -484
  78. package/src/components/AutoBeUserMessageMovie.tsx +27 -27
  79. package/src/components/common/ActionButton.tsx +205 -205
  80. package/src/components/common/ActionButtonGroup.tsx +80 -80
  81. package/src/components/common/AutoBeConfigInput.tsx +185 -185
  82. package/src/components/common/ChatBubble.tsx +119 -119
  83. package/src/components/common/Collapsible.tsx +95 -95
  84. package/src/components/common/CompactSessionIndicator.tsx +73 -73
  85. package/src/components/common/CompactSessionList.tsx +82 -82
  86. package/src/components/common/index.ts +8 -8
  87. package/src/components/common/openai/OpenAIContent.tsx +53 -53
  88. package/src/components/common/openai/OpenAIUserAudioContent.tsx +70 -70
  89. package/src/components/common/openai/OpenAIUserFileContent.tsx +76 -76
  90. package/src/components/common/openai/OpenAIUserImageContent.tsx +34 -34
  91. package/src/components/common/openai/OpenAIUserTextContent.tsx +15 -15
  92. package/src/components/common/openai/index.ts +5 -5
  93. package/src/components/events/AutoBeCompleteEventMovie.tsx +402 -402
  94. package/src/components/events/AutoBeCorrectEventMovie.tsx +354 -368
  95. package/src/components/events/AutoBeEventGroupMovie.tsx +18 -18
  96. package/src/components/events/AutoBeEventMovie.tsx +158 -139
  97. package/src/components/events/AutoBeProgressEventMovie.tsx +217 -157
  98. package/src/components/events/AutoBeScenarioEventMovie.tsx +135 -95
  99. package/src/components/events/AutoBeStartEventMovie.tsx +82 -82
  100. package/src/components/events/AutoBeValidateEventMovie.tsx +249 -286
  101. package/src/components/events/README.md +300 -300
  102. package/src/components/events/common/CollapsibleEventGroup.tsx +211 -211
  103. package/src/components/events/common/EventCard.tsx +61 -61
  104. package/src/components/events/common/EventContent.tsx +31 -31
  105. package/src/components/events/common/EventHeader.tsx +85 -85
  106. package/src/components/events/common/EventIcon.tsx +82 -82
  107. package/src/components/events/common/ProgressBar.tsx +64 -64
  108. package/src/components/events/common/index.ts +13 -13
  109. package/src/components/events/groups/CorrectEventGroup.tsx +183 -183
  110. package/src/components/events/groups/ValidateEventGroup.tsx +143 -146
  111. package/src/components/events/groups/index.ts +8 -8
  112. package/src/components/events/index.ts +16 -16
  113. package/src/components/events/utils/eventGrouper.tsx +116 -117
  114. package/src/components/events/utils/index.ts +1 -1
  115. package/src/components/index.ts +13 -13
  116. package/src/components/upload/AutoBeChatUploadBox.tsx +425 -424
  117. package/src/components/upload/AutoBeChatUploadSendButton.tsx +66 -66
  118. package/src/components/upload/AutoBeFileUploadBox.tsx +123 -123
  119. package/src/components/upload/AutoBeUploadConfig.ts +5 -5
  120. package/src/components/upload/AutoBeVoiceRecoderButton.tsx +100 -100
  121. package/src/components/upload/index.ts +5 -5
  122. package/src/constant/color.ts +28 -28
  123. package/src/context/AutoBeAgentContext.tsx +245 -258
  124. package/src/context/AutoBeAgentSessionList.tsx +58 -58
  125. package/src/context/SearchParamsContext.tsx +49 -49
  126. package/src/hooks/index.ts +3 -3
  127. package/src/hooks/useEscapeKey.ts +24 -24
  128. package/src/hooks/useIsomorphicLayoutEffect.ts +8 -8
  129. package/src/hooks/useMediaQuery.ts +73 -73
  130. package/src/hooks/useSessionStorage.ts +10 -0
  131. package/src/icons/Receipt.tsx +74 -74
  132. package/src/index.ts +9 -8
  133. package/src/strategy/AutoBeAgentSessionStorageStrategy.ts +127 -0
  134. package/src/structure/AutoBeListener.ts +373 -304
  135. package/src/structure/AutoBeListenerState.ts +53 -53
  136. package/src/structure/IAutoBeAgentSessionStorageStrategy.ts +87 -87
  137. package/src/structure/IAutoBeEventGroup.ts +6 -6
  138. package/src/structure/index.ts +4 -4
  139. package/src/types/config.ts +44 -44
  140. package/src/types/index.ts +1 -1
  141. package/src/utils/AutoBeFileUploader.ts +279 -279
  142. package/src/utils/AutoBeVoiceRecorder.ts +95 -95
  143. package/src/utils/__tests__/crypto.test.ts +286 -286
  144. package/src/utils/__tests__/storage.test.ts +229 -229
  145. package/src/utils/crypto.ts +95 -95
  146. package/src/utils/index.ts +6 -6
  147. package/src/utils/number.ts +17 -17
  148. package/src/utils/storage.ts +96 -96
  149. package/src/utils/time.ts +14 -14
  150. package/tsconfig.json +9 -9
  151. package/vitest.config.ts +15 -15
@@ -1,414 +1,414 @@
1
- import { useCallback, useEffect, useState } from "react";
2
-
3
- import { useAutoBeAgentSessionList } from "../context/AutoBeAgentSessionList";
4
- import { useSearchParams } from "../context/SearchParamsContext";
5
- import {
6
- IAutoBeAgentSession,
7
- IAutoBeAgentSessionStorageStrategy,
8
- } from "../structure";
9
- import { ActionButtonGroup } from "./common/ActionButtonGroup";
10
- import { CompactSessionList } from "./common/CompactSessionList";
11
-
12
- /** Props interface for AutoBeChatSidebar component */
13
- export interface IAutoBeChatSidebarProps {
14
- storageStrategy: IAutoBeAgentSessionStorageStrategy;
15
- /** Whether the sidebar is collapsed (true) or expanded (false) */
16
- isCollapsed: boolean;
17
- /** Function to toggle sidebar collapsed/expanded */
18
- onToggle: () => void;
19
- /** Custom className */
20
- className?: string;
21
- /** Function to select a session */
22
- onSessionSelect?: (id: string) => Promise<void> | void;
23
- /** Function to delete a session */
24
- onDeleteSession?: (id: string) => Promise<void> | void;
25
- }
26
-
27
- const collapsedWidth = "60px";
28
- const expandedWidth = "320px";
29
- /** Beautiful and modern chat sidebar component as part of layout */
30
- export const AutoBeChatSidebar = (props: IAutoBeChatSidebarProps) => {
31
- const { sessionList, refreshSessionList } = useAutoBeAgentSessionList();
32
- const { searchParams, setSearchParams } = useSearchParams();
33
- const activeSessionId = searchParams.get("session-id") ?? null;
34
- const [currentSessionId, setCurrentSessionId] = useState(
35
- Array.isArray(activeSessionId) ? activeSessionId.at(0) : activeSessionId,
36
- );
37
-
38
- const handleOnSessionSelect = useCallback(
39
- (sessionId: string) => {
40
- props.onSessionSelect?.(sessionId);
41
- setCurrentSessionId(sessionId);
42
- },
43
- [props.onSessionSelect, setSearchParams],
44
- );
45
-
46
- useEffect(() => {
47
- setCurrentSessionId(activeSessionId);
48
- }, [searchParams]);
49
-
50
- return (
51
- <div
52
- className={props.className}
53
- style={{
54
- position: "relative",
55
- height: "100%",
56
- width: props.isCollapsed ? collapsedWidth : expandedWidth,
57
- backgroundColor: "#ffffff",
58
- borderRight: "1px solid #e5e7eb",
59
- boxShadow: "2px 0 4px rgba(0, 0, 0, 0.05)",
60
- transition: "width 0.3s ease",
61
- display: "flex",
62
- flexDirection: "column",
63
- overflow: "hidden",
64
- flexShrink: 0,
65
- }}
66
- >
67
- {/* Header section */}
68
- <div
69
- style={{
70
- padding: props.isCollapsed ? "1rem 0.75rem" : "1.5rem 1.25rem 1rem",
71
- borderBottom: "1px solid #f3f4f6",
72
- backgroundColor: "#fafafa",
73
- transition: "padding 0.3s ease",
74
- }}
75
- >
76
- {/* Toggle button and title */}
77
- <div
78
- style={{
79
- display: "flex",
80
- justifyContent: props.isCollapsed ? "center" : "space-between",
81
- alignItems: "center",
82
- marginBottom: props.isCollapsed ? "0" : "1rem",
83
- transition: "all 0.3s ease",
84
- }}
85
- >
86
- {!props.isCollapsed && (
87
- <h2
88
- style={{
89
- fontSize: "1.25rem",
90
- fontWeight: "600",
91
- color: "#1f2937",
92
- margin: 0,
93
- opacity: props.isCollapsed ? 0 : 1,
94
- transition: "opacity 0.3s ease",
95
- }}
96
- >
97
- Chat History
98
- </h2>
99
- )}
100
- <button
101
- onClick={props.onToggle}
102
- style={{
103
- background: "none",
104
- border: "none",
105
- cursor: "pointer",
106
- padding: "0.5rem",
107
- borderRadius: "0.5rem",
108
- display: "flex",
109
- alignItems: "center",
110
- justifyContent: "center",
111
- transition: "background-color 0.2s ease",
112
- }}
113
- onMouseEnter={(e) => {
114
- e.currentTarget.style.backgroundColor = "#f3f4f6";
115
- }}
116
- onMouseLeave={(e) => {
117
- e.currentTarget.style.backgroundColor = "transparent";
118
- }}
119
- title={props.isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
120
- >
121
- <svg
122
- width="20"
123
- height="20"
124
- viewBox="0 0 24 24"
125
- fill="none"
126
- stroke="currentColor"
127
- strokeWidth="2"
128
- strokeLinecap="round"
129
- strokeLinejoin="round"
130
- style={{
131
- transform: props.isCollapsed
132
- ? "rotate(0deg)"
133
- : "rotate(180deg)",
134
- transition: "transform 0.3s ease",
135
- }}
136
- >
137
- <path d="M15 18l-6-6 6-6" />
138
- </svg>
139
- </button>
140
- </div>
141
- </div>
142
-
143
- {/* Conversations list */}
144
- <div
145
- style={{
146
- flex: 1,
147
- overflowY: props.isCollapsed ? "visible" : "auto",
148
- padding: props.isCollapsed ? "0.25rem" : "0.5rem",
149
- transition: "padding 0.3s ease",
150
- }}
151
- >
152
- {props.isCollapsed ? (
153
- // Collapsed state - show compact conversation indicators
154
- <CompactSessionList
155
- sessions={sessionList}
156
- activeSessionId={currentSessionId}
157
- maxItems={8}
158
- onSessionSelect={handleOnSessionSelect}
159
- />
160
- ) : (
161
- // Expanded state - show full conversation list
162
- <>
163
- {sessionList.length === 0 ? (
164
- <div
165
- style={{
166
- padding: "2rem 1rem",
167
- textAlign: "center",
168
- color: "#6b7280",
169
- fontSize: "0.875rem",
170
- }}
171
- >
172
- <div style={{ marginBottom: "0.5rem" }}>
173
- <svg
174
- width="48"
175
- height="48"
176
- viewBox="0 0 24 24"
177
- fill="none"
178
- stroke="currentColor"
179
- strokeWidth="1.5"
180
- strokeLinecap="round"
181
- strokeLinejoin="round"
182
- style={{ margin: "0 auto", opacity: 0.5 }}
183
- >
184
- <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
185
- </svg>
186
- </div>
187
- <p style={{ margin: 0 }}>No conversations yet</p>
188
- <p style={{ margin: "0.25rem 0 0 0", fontSize: "0.75rem" }}>
189
- Start a new chat to see your history here
190
- </p>
191
- </div>
192
- ) : (
193
- sessionList.map((session) => (
194
- <SessionListItem
195
- key={session.id}
196
- session={session}
197
- isActive={currentSessionId === session.id}
198
- onSelect={handleOnSessionSelect}
199
- onDelete={async () => {
200
- await props.onDeleteSession?.(session.id);
201
- refreshSessionList();
202
- if (session.id === currentSessionId) {
203
- setSearchParams((sp) => {
204
- const newSp = new URLSearchParams(sp);
205
- newSp.delete("session-id");
206
- return newSp;
207
- });
208
- }
209
- }}
210
- onEditTitle={async (_: string, newTitle: string) => {
211
- // Update the session title through storage strategy
212
- await props.storageStrategy.editSessionTitle({
213
- id: session.id,
214
- title: newTitle,
215
- });
216
- refreshSessionList();
217
- }}
218
- />
219
- ))
220
- )}
221
- </>
222
- )}
223
- </div>
224
- </div>
225
- );
226
- };
227
-
228
- export default AutoBeChatSidebar;
229
-
230
- /** Props for ConversationListItem component */
231
- export interface IConversationListItemProps {
232
- /** Conversation session data */
233
- session: IAutoBeAgentSession;
234
- /** Whether this conversation is currently active */
235
- isActive: boolean;
236
- /** Callback when conversation is selected */
237
- onSelect: (sessionId: string) => void;
238
- /** Callback when conversation should be deleted */
239
- onDelete: (sessionId: string) => void;
240
- /** Callback when conversation should be edited */
241
- onEditTitle: (sessionId: string, title: string) => void;
242
- }
243
-
244
- /**
245
- * Individual conversation list item component Displays conversation title,
246
- * metadata, and actions
247
- */
248
- export const SessionListItem = (props: IConversationListItemProps) => {
249
- const { session, isActive, onSelect, onDelete, onEditTitle } = props;
250
- const [isHovered, setIsHovered] = useState(false);
251
- const [isEditing, setIsEditing] = useState(false);
252
- const [editingTitle, setEditingTitle] = useState(session.title ?? "");
253
- const lastMessage = session.history.at(-1);
254
-
255
- //----
256
- // EVENT HANDLERS
257
- //----
258
- const handleStartEditing = () => {
259
- setIsEditing(true);
260
- setEditingTitle(session.title ?? "Untitled");
261
- };
262
-
263
- const handleSaveTitle = () => {
264
- const trimmedTitle = editingTitle.trim();
265
- if (trimmedTitle && trimmedTitle !== session.title) {
266
- onEditTitle?.(session.id, trimmedTitle);
267
- }
268
- setIsEditing(false);
269
- };
270
-
271
- const handleCancelEditing = () => {
272
- setIsEditing(false);
273
- setEditingTitle(session.title ?? "Untitled");
274
- };
275
-
276
- const handleKeyPress = (e: React.KeyboardEvent) => {
277
- if (e.key === "Enter") {
278
- handleSaveTitle();
279
- } else if (e.key === "Escape") {
280
- handleCancelEditing();
281
- }
282
- };
283
-
284
- return (
285
- <div
286
- style={{
287
- marginBottom: "0.5rem",
288
- borderRadius: "0.75rem",
289
- padding: "0.75rem",
290
- cursor: "pointer",
291
- backgroundColor: isActive
292
- ? "#eff6ff"
293
- : isHovered
294
- ? "#f9fafb"
295
- : "transparent",
296
- border: isActive ? "1px solid #dbeafe" : "1px solid transparent",
297
- transition: "all 0.2s ease",
298
- display: "flex",
299
- flexDirection: "column",
300
- gap: "0.25rem",
301
- position: "relative",
302
- }}
303
- onClick={() => onSelect(session.id)}
304
- onMouseEnter={() => setIsHovered(true)}
305
- onMouseLeave={() => setIsHovered(false)}
306
- >
307
- {/* Conversation title and action buttons */}
308
- <div
309
- style={{
310
- display: "flex",
311
- alignItems: "center",
312
- justifyContent: "space-between",
313
- gap: "0.5rem",
314
- fontSize: "0.875rem",
315
- fontWeight: "500",
316
- color: isActive ? "#1d4ed8" : "#1f2937",
317
- lineHeight: "1.25",
318
- paddingRight: "0.5rem", // Space for buttons
319
- }}
320
- >
321
- {/* Title section */}
322
- <div
323
- style={{
324
- flex: 1,
325
- display: "flex",
326
- alignItems: "center",
327
- minWidth: 0, // Allow shrinking
328
- }}
329
- >
330
- {isEditing ? (
331
- <input
332
- type="text"
333
- value={editingTitle}
334
- onChange={(e) => setEditingTitle(e.target.value)}
335
- onKeyDown={handleKeyPress}
336
- onBlur={handleSaveTitle}
337
- style={{
338
- flex: 1,
339
- border: "1px solid #d1d5db",
340
- borderRadius: "0.25rem",
341
- padding: "0.25rem 0.5rem",
342
- fontSize: "0.875rem",
343
- fontWeight: "500",
344
- color: isActive ? "#1d4ed8" : "#1f2937",
345
- backgroundColor: "#ffffff",
346
- outline: "none",
347
- boxShadow: "0 0 0 2px rgba(59, 130, 246, 0.25)",
348
- }}
349
- autoFocus
350
- onClick={(e) => e.stopPropagation()}
351
- />
352
- ) : (
353
- <span
354
- style={{
355
- overflow: "hidden",
356
- textOverflow: "ellipsis",
357
- whiteSpace: "nowrap",
358
- flex: 1,
359
- }}
360
- >
361
- {session.title ?? "Untitled"}
362
- </span>
363
- )}
364
- </div>
365
-
366
- {/* Action buttons group */}
367
- {!isEditing ? (
368
- <div
369
- style={{
370
- visibility: isHovered ? "visible" : "hidden",
371
- opacity: isHovered ? 1 : 0,
372
- transition: "opacity 0.2s ease",
373
- }}
374
- >
375
- <ActionButtonGroup
376
- onEdit={handleStartEditing}
377
- onDelete={onDelete ? () => onDelete(session.id) : undefined}
378
- />
379
- </div>
380
- ) : (
381
- <ActionButtonGroup
382
- onSave={handleSaveTitle}
383
- onCancel={handleCancelEditing}
384
- />
385
- )}
386
- </div>
387
-
388
- {/* Conversation metadata */}
389
- <div
390
- style={{
391
- display: "flex",
392
- justifyContent: "space-between",
393
- alignItems: "center",
394
- fontSize: "0.75rem",
395
- color: "#6b7280",
396
- }}
397
- >
398
- <span>
399
- {session.history.length > 0 && lastMessage !== undefined
400
- ? new Date(lastMessage.created_at).toLocaleDateString("en-US", {
401
- month: "short",
402
- day: "numeric",
403
- hour: "2-digit",
404
- minute: "2-digit",
405
- })
406
- : "No messages"}
407
- </span>
408
- {session.history.length > 0 && (
409
- <span>{session.history.length} messages</span>
410
- )}
411
- </div>
412
- </div>
413
- );
414
- };
1
+ import { useCallback, useEffect, useState } from "react";
2
+
3
+ import { useAutoBeAgentSessionList } from "../context/AutoBeAgentSessionList";
4
+ import { useSearchParams } from "../context/SearchParamsContext";
5
+ import {
6
+ IAutoBeAgentSession,
7
+ IAutoBeAgentSessionStorageStrategy,
8
+ } from "../structure";
9
+ import { ActionButtonGroup } from "./common/ActionButtonGroup";
10
+ import { CompactSessionList } from "./common/CompactSessionList";
11
+
12
+ /** Props interface for AutoBeChatSidebar component */
13
+ export interface IAutoBeChatSidebarProps {
14
+ storageStrategy: IAutoBeAgentSessionStorageStrategy;
15
+ /** Whether the sidebar is collapsed (true) or expanded (false) */
16
+ isCollapsed: boolean;
17
+ /** Function to toggle sidebar collapsed/expanded */
18
+ onToggle: () => void;
19
+ /** Custom className */
20
+ className?: string;
21
+ /** Function to select a session */
22
+ onSessionSelect?: (id: string) => Promise<void> | void;
23
+ /** Function to delete a session */
24
+ onDeleteSession?: (id: string) => Promise<void> | void;
25
+ }
26
+
27
+ const collapsedWidth = "60px";
28
+ const expandedWidth = "320px";
29
+ /** Beautiful and modern chat sidebar component as part of layout */
30
+ export const AutoBeChatSidebar = (props: IAutoBeChatSidebarProps) => {
31
+ const { sessionList, refreshSessionList } = useAutoBeAgentSessionList();
32
+ const { searchParams, setSearchParams } = useSearchParams();
33
+ const activeSessionId = searchParams.get("session-id") ?? null;
34
+ const [currentSessionId, setCurrentSessionId] = useState(
35
+ Array.isArray(activeSessionId) ? activeSessionId.at(0) : activeSessionId,
36
+ );
37
+
38
+ const handleOnSessionSelect = useCallback(
39
+ (sessionId: string) => {
40
+ props.onSessionSelect?.(sessionId);
41
+ setCurrentSessionId(sessionId);
42
+ },
43
+ [props.onSessionSelect, setSearchParams],
44
+ );
45
+
46
+ useEffect(() => {
47
+ setCurrentSessionId(activeSessionId);
48
+ }, [searchParams]);
49
+
50
+ return (
51
+ <div
52
+ className={props.className}
53
+ style={{
54
+ position: "relative",
55
+ height: "100%",
56
+ width: props.isCollapsed ? collapsedWidth : expandedWidth,
57
+ backgroundColor: "#ffffff",
58
+ borderRight: "1px solid #e5e7eb",
59
+ boxShadow: "2px 0 4px rgba(0, 0, 0, 0.05)",
60
+ transition: "width 0.3s ease",
61
+ display: "flex",
62
+ flexDirection: "column",
63
+ overflow: "hidden",
64
+ flexShrink: 0,
65
+ }}
66
+ >
67
+ {/* Header section */}
68
+ <div
69
+ style={{
70
+ padding: props.isCollapsed ? "1rem 0.75rem" : "1.5rem 1.25rem 1rem",
71
+ borderBottom: "1px solid #f3f4f6",
72
+ backgroundColor: "#fafafa",
73
+ transition: "padding 0.3s ease",
74
+ }}
75
+ >
76
+ {/* Toggle button and title */}
77
+ <div
78
+ style={{
79
+ display: "flex",
80
+ justifyContent: props.isCollapsed ? "center" : "space-between",
81
+ alignItems: "center",
82
+ marginBottom: props.isCollapsed ? "0" : "1rem",
83
+ transition: "all 0.3s ease",
84
+ }}
85
+ >
86
+ {!props.isCollapsed && (
87
+ <h2
88
+ style={{
89
+ fontSize: "1.25rem",
90
+ fontWeight: "600",
91
+ color: "#1f2937",
92
+ margin: 0,
93
+ opacity: props.isCollapsed ? 0 : 1,
94
+ transition: "opacity 0.3s ease",
95
+ }}
96
+ >
97
+ Chat History
98
+ </h2>
99
+ )}
100
+ <button
101
+ onClick={props.onToggle}
102
+ style={{
103
+ background: "none",
104
+ border: "none",
105
+ cursor: "pointer",
106
+ padding: "0.5rem",
107
+ borderRadius: "0.5rem",
108
+ display: "flex",
109
+ alignItems: "center",
110
+ justifyContent: "center",
111
+ transition: "background-color 0.2s ease",
112
+ }}
113
+ onMouseEnter={(e) => {
114
+ e.currentTarget.style.backgroundColor = "#f3f4f6";
115
+ }}
116
+ onMouseLeave={(e) => {
117
+ e.currentTarget.style.backgroundColor = "transparent";
118
+ }}
119
+ title={props.isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
120
+ >
121
+ <svg
122
+ width="20"
123
+ height="20"
124
+ viewBox="0 0 24 24"
125
+ fill="none"
126
+ stroke="currentColor"
127
+ strokeWidth="2"
128
+ strokeLinecap="round"
129
+ strokeLinejoin="round"
130
+ style={{
131
+ transform: props.isCollapsed
132
+ ? "rotate(0deg)"
133
+ : "rotate(180deg)",
134
+ transition: "transform 0.3s ease",
135
+ }}
136
+ >
137
+ <path d="M15 18l-6-6 6-6" />
138
+ </svg>
139
+ </button>
140
+ </div>
141
+ </div>
142
+
143
+ {/* Conversations list */}
144
+ <div
145
+ style={{
146
+ flex: 1,
147
+ overflowY: props.isCollapsed ? "visible" : "auto",
148
+ padding: props.isCollapsed ? "0.25rem" : "0.5rem",
149
+ transition: "padding 0.3s ease",
150
+ }}
151
+ >
152
+ {props.isCollapsed ? (
153
+ // Collapsed state - show compact conversation indicators
154
+ <CompactSessionList
155
+ sessions={sessionList}
156
+ activeSessionId={currentSessionId}
157
+ maxItems={8}
158
+ onSessionSelect={handleOnSessionSelect}
159
+ />
160
+ ) : (
161
+ // Expanded state - show full conversation list
162
+ <>
163
+ {sessionList.length === 0 ? (
164
+ <div
165
+ style={{
166
+ padding: "2rem 1rem",
167
+ textAlign: "center",
168
+ color: "#6b7280",
169
+ fontSize: "0.875rem",
170
+ }}
171
+ >
172
+ <div style={{ marginBottom: "0.5rem" }}>
173
+ <svg
174
+ width="48"
175
+ height="48"
176
+ viewBox="0 0 24 24"
177
+ fill="none"
178
+ stroke="currentColor"
179
+ strokeWidth="1.5"
180
+ strokeLinecap="round"
181
+ strokeLinejoin="round"
182
+ style={{ margin: "0 auto", opacity: 0.5 }}
183
+ >
184
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
185
+ </svg>
186
+ </div>
187
+ <p style={{ margin: 0 }}>No conversations yet</p>
188
+ <p style={{ margin: "0.25rem 0 0 0", fontSize: "0.75rem" }}>
189
+ Start a new chat to see your history here
190
+ </p>
191
+ </div>
192
+ ) : (
193
+ sessionList.map((session) => (
194
+ <SessionListItem
195
+ key={session.id}
196
+ session={session}
197
+ isActive={currentSessionId === session.id}
198
+ onSelect={handleOnSessionSelect}
199
+ onDelete={async () => {
200
+ await props.onDeleteSession?.(session.id);
201
+ refreshSessionList();
202
+ if (session.id === currentSessionId) {
203
+ setSearchParams((sp) => {
204
+ const newSp = new URLSearchParams(sp);
205
+ newSp.delete("session-id");
206
+ return newSp;
207
+ });
208
+ }
209
+ }}
210
+ onEditTitle={async (_: string, newTitle: string) => {
211
+ // Update the session title through storage strategy
212
+ await props.storageStrategy.editSessionTitle({
213
+ id: session.id,
214
+ title: newTitle,
215
+ });
216
+ refreshSessionList();
217
+ }}
218
+ />
219
+ ))
220
+ )}
221
+ </>
222
+ )}
223
+ </div>
224
+ </div>
225
+ );
226
+ };
227
+
228
+ export default AutoBeChatSidebar;
229
+
230
+ /** Props for ConversationListItem component */
231
+ export interface IConversationListItemProps {
232
+ /** Conversation session data */
233
+ session: IAutoBeAgentSession;
234
+ /** Whether this conversation is currently active */
235
+ isActive: boolean;
236
+ /** Callback when conversation is selected */
237
+ onSelect: (sessionId: string) => void;
238
+ /** Callback when conversation should be deleted */
239
+ onDelete: (sessionId: string) => void;
240
+ /** Callback when conversation should be edited */
241
+ onEditTitle: (sessionId: string, title: string) => void;
242
+ }
243
+
244
+ /**
245
+ * Individual conversation list item component Displays conversation title,
246
+ * metadata, and actions
247
+ */
248
+ export const SessionListItem = (props: IConversationListItemProps) => {
249
+ const { session, isActive, onSelect, onDelete, onEditTitle } = props;
250
+ const [isHovered, setIsHovered] = useState(false);
251
+ const [isEditing, setIsEditing] = useState(false);
252
+ const [editingTitle, setEditingTitle] = useState(session.title ?? "");
253
+ const lastMessage = session.history.at(-1);
254
+
255
+ //----
256
+ // EVENT HANDLERS
257
+ //----
258
+ const handleStartEditing = () => {
259
+ setIsEditing(true);
260
+ setEditingTitle(session.title ?? "Untitled");
261
+ };
262
+
263
+ const handleSaveTitle = () => {
264
+ const trimmedTitle = editingTitle.trim();
265
+ if (trimmedTitle && trimmedTitle !== session.title) {
266
+ onEditTitle?.(session.id, trimmedTitle);
267
+ }
268
+ setIsEditing(false);
269
+ };
270
+
271
+ const handleCancelEditing = () => {
272
+ setIsEditing(false);
273
+ setEditingTitle(session.title ?? "Untitled");
274
+ };
275
+
276
+ const handleKeyPress = (e: React.KeyboardEvent) => {
277
+ if (e.key === "Enter") {
278
+ handleSaveTitle();
279
+ } else if (e.key === "Escape") {
280
+ handleCancelEditing();
281
+ }
282
+ };
283
+
284
+ return (
285
+ <div
286
+ style={{
287
+ marginBottom: "0.5rem",
288
+ borderRadius: "0.75rem",
289
+ padding: "0.75rem",
290
+ cursor: "pointer",
291
+ backgroundColor: isActive
292
+ ? "#eff6ff"
293
+ : isHovered
294
+ ? "#f9fafb"
295
+ : "transparent",
296
+ border: isActive ? "1px solid #dbeafe" : "1px solid transparent",
297
+ transition: "all 0.2s ease",
298
+ display: "flex",
299
+ flexDirection: "column",
300
+ gap: "0.25rem",
301
+ position: "relative",
302
+ }}
303
+ onClick={() => onSelect(session.id)}
304
+ onMouseEnter={() => setIsHovered(true)}
305
+ onMouseLeave={() => setIsHovered(false)}
306
+ >
307
+ {/* Conversation title and action buttons */}
308
+ <div
309
+ style={{
310
+ display: "flex",
311
+ alignItems: "center",
312
+ justifyContent: "space-between",
313
+ gap: "0.5rem",
314
+ fontSize: "0.875rem",
315
+ fontWeight: "500",
316
+ color: isActive ? "#1d4ed8" : "#1f2937",
317
+ lineHeight: "1.25",
318
+ paddingRight: "0.5rem", // Space for buttons
319
+ }}
320
+ >
321
+ {/* Title section */}
322
+ <div
323
+ style={{
324
+ flex: 1,
325
+ display: "flex",
326
+ alignItems: "center",
327
+ minWidth: 0, // Allow shrinking
328
+ }}
329
+ >
330
+ {isEditing ? (
331
+ <input
332
+ type="text"
333
+ value={editingTitle}
334
+ onChange={(e) => setEditingTitle(e.target.value)}
335
+ onKeyDown={handleKeyPress}
336
+ onBlur={handleSaveTitle}
337
+ style={{
338
+ flex: 1,
339
+ border: "1px solid #d1d5db",
340
+ borderRadius: "0.25rem",
341
+ padding: "0.25rem 0.5rem",
342
+ fontSize: "0.875rem",
343
+ fontWeight: "500",
344
+ color: isActive ? "#1d4ed8" : "#1f2937",
345
+ backgroundColor: "#ffffff",
346
+ outline: "none",
347
+ boxShadow: "0 0 0 2px rgba(59, 130, 246, 0.25)",
348
+ }}
349
+ autoFocus
350
+ onClick={(e) => e.stopPropagation()}
351
+ />
352
+ ) : (
353
+ <span
354
+ style={{
355
+ overflow: "hidden",
356
+ textOverflow: "ellipsis",
357
+ whiteSpace: "nowrap",
358
+ flex: 1,
359
+ }}
360
+ >
361
+ {session.title ?? "Untitled"}
362
+ </span>
363
+ )}
364
+ </div>
365
+
366
+ {/* Action buttons group */}
367
+ {!isEditing ? (
368
+ <div
369
+ style={{
370
+ visibility: isHovered ? "visible" : "hidden",
371
+ opacity: isHovered ? 1 : 0,
372
+ transition: "opacity 0.2s ease",
373
+ }}
374
+ >
375
+ <ActionButtonGroup
376
+ onEdit={handleStartEditing}
377
+ onDelete={onDelete ? () => onDelete(session.id) : undefined}
378
+ />
379
+ </div>
380
+ ) : (
381
+ <ActionButtonGroup
382
+ onSave={handleSaveTitle}
383
+ onCancel={handleCancelEditing}
384
+ />
385
+ )}
386
+ </div>
387
+
388
+ {/* Conversation metadata */}
389
+ <div
390
+ style={{
391
+ display: "flex",
392
+ justifyContent: "space-between",
393
+ alignItems: "center",
394
+ fontSize: "0.75rem",
395
+ color: "#6b7280",
396
+ }}
397
+ >
398
+ <span>
399
+ {session.history.length > 0 && lastMessage !== undefined
400
+ ? new Date(lastMessage.created_at).toLocaleDateString("en-US", {
401
+ month: "short",
402
+ day: "numeric",
403
+ hour: "2-digit",
404
+ minute: "2-digit",
405
+ })
406
+ : "No messages"}
407
+ </span>
408
+ {session.history.length > 0 && (
409
+ <span>{session.history.length} messages</span>
410
+ )}
411
+ </div>
412
+ </div>
413
+ );
414
+ };