@autobe/ui 0.30.0-dev.20260315 → 0.30.0
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/LICENSE +661 -661
- package/lib/components/AutoBeChatMain.js +5 -5
- package/lib/components/AutoBeConfigModal.js +9 -9
- package/package.json +2 -2
- package/src/components/AutoBeAssistantMessageMovie.tsx +22 -22
- package/src/components/AutoBeChatMain.tsx +376 -376
- package/src/components/AutoBeChatSidebar.tsx +414 -414
- package/src/components/AutoBeConfigButton.tsx +83 -83
- package/src/components/AutoBeConfigModal.tsx +443 -443
- package/src/components/AutoBeStatusButton.tsx +75 -75
- package/src/components/AutoBeStatusModal.tsx +486 -486
- package/src/components/AutoBeUserMessageMovie.tsx +27 -27
- package/src/components/common/ActionButton.tsx +205 -205
- package/src/components/common/ActionButtonGroup.tsx +80 -80
- package/src/components/common/AutoBeConfigInput.tsx +185 -185
- package/src/components/common/ChatBubble.tsx +119 -119
- package/src/components/common/Collapsible.tsx +95 -95
- package/src/components/common/CompactSessionIndicator.tsx +73 -73
- package/src/components/common/CompactSessionList.tsx +82 -82
- package/src/components/common/index.ts +8 -8
- package/src/components/common/openai/OpenAIContent.tsx +53 -53
- package/src/components/common/openai/OpenAIUserAudioContent.tsx +70 -70
- package/src/components/common/openai/OpenAIUserFileContent.tsx +76 -76
- package/src/components/common/openai/OpenAIUserImageContent.tsx +34 -34
- package/src/components/common/openai/OpenAIUserTextContent.tsx +15 -15
- package/src/components/common/openai/index.ts +5 -5
- package/src/components/events/AutoBeCompleteEventMovie.tsx +402 -402
- package/src/components/events/AutoBeCorrectEventMovie.tsx +354 -354
- package/src/components/events/AutoBeEventGroupMovie.tsx +18 -18
- package/src/components/events/AutoBeEventMovie.tsx +158 -158
- package/src/components/events/AutoBeProgressEventMovie.tsx +217 -217
- package/src/components/events/AutoBeScenarioEventMovie.tsx +135 -135
- package/src/components/events/AutoBeStartEventMovie.tsx +82 -82
- package/src/components/events/AutoBeValidateEventMovie.tsx +249 -249
- package/src/components/events/README.md +300 -300
- package/src/components/events/common/CollapsibleEventGroup.tsx +211 -211
- package/src/components/events/common/EventCard.tsx +61 -61
- package/src/components/events/common/EventContent.tsx +31 -31
- package/src/components/events/common/EventHeader.tsx +85 -85
- package/src/components/events/common/EventIcon.tsx +82 -82
- package/src/components/events/common/ProgressBar.tsx +64 -64
- package/src/components/events/common/index.ts +13 -13
- package/src/components/events/groups/CorrectEventGroup.tsx +183 -183
- package/src/components/events/groups/ValidateEventGroup.tsx +143 -143
- package/src/components/events/groups/index.ts +8 -8
- package/src/components/events/index.ts +16 -16
- package/src/components/events/utils/eventGrouper.tsx +116 -116
- package/src/components/events/utils/index.ts +1 -1
- package/src/components/index.ts +13 -13
- package/src/components/upload/AutoBeChatUploadBox.tsx +425 -425
- package/src/components/upload/AutoBeChatUploadSendButton.tsx +66 -66
- package/src/components/upload/AutoBeFileUploadBox.tsx +123 -123
- package/src/components/upload/AutoBeUploadConfig.ts +5 -5
- package/src/components/upload/AutoBeVoiceRecoderButton.tsx +100 -100
- package/src/components/upload/index.ts +5 -5
- package/src/constant/color.ts +28 -28
- package/src/context/AutoBeAgentContext.tsx +245 -245
- package/src/context/AutoBeAgentSessionList.tsx +58 -58
- package/src/context/SearchParamsContext.tsx +49 -49
- package/src/hooks/index.ts +3 -3
- package/src/hooks/useEscapeKey.ts +24 -24
- package/src/hooks/useIsomorphicLayoutEffect.ts +8 -8
- package/src/hooks/useMediaQuery.ts +73 -73
- package/src/hooks/useSessionStorage.ts +10 -10
- package/src/icons/Receipt.tsx +74 -74
- package/src/index.ts +9 -9
- package/src/strategy/AutoBeAgentSessionStorageStrategy.ts +127 -127
- package/src/structure/AutoBeListener.ts +373 -373
- package/src/structure/AutoBeListenerState.ts +53 -53
- package/src/structure/IAutoBeAgentSessionStorageStrategy.ts +87 -87
- package/src/structure/IAutoBeEventGroup.ts +6 -6
- package/src/structure/index.ts +4 -4
- package/src/types/config.ts +44 -44
- package/src/types/index.ts +1 -1
- package/src/utils/AutoBeFileUploader.ts +279 -279
- package/src/utils/AutoBeVoiceRecorder.ts +95 -95
- package/src/utils/__tests__/crypto.test.ts +286 -286
- package/src/utils/__tests__/storage.test.ts +229 -229
- package/src/utils/crypto.ts +95 -95
- package/src/utils/index.ts +6 -6
- package/src/utils/number.ts +17 -17
- package/src/utils/storage.ts +96 -96
- package/src/utils/time.ts +14 -14
- package/tsconfig.json +9 -9
- package/vitest.config.ts +15 -15
- package/README.md +0 -261
|
@@ -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
|
+
};
|