@canmingir/link 1.2.54 → 1.2.56

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.
@@ -0,0 +1,221 @@
1
+ import { Iconify } from "@canmingir/link/platform/components";
2
+ import { MessageList } from "../ChatMessage";
3
+ import { Scrollbar } from "@canmingir/link/platform/components";
4
+ import Stack from "@mui/material/Stack";
5
+ import { StoredSession } from "./types";
6
+ import { alpha } from "@mui/material/styles";
7
+ import { cleanIconName } from "./cleanIconName";
8
+
9
+ import { Box, IconButton, Popover, Tooltip, Typography } from "@mui/material";
10
+ import React, { memo, useEffect, useRef } from "react";
11
+
12
+ interface SessionPopoverProps {
13
+ anchorEl: HTMLElement | null;
14
+ onClose: () => void;
15
+ onOpenFullChat: () => void;
16
+ sessions: StoredSession[];
17
+ activeSessionId: string;
18
+ currentSessionId?: string;
19
+ selectedConversationId?: string;
20
+ messagesEndRef: React.RefObject<HTMLDivElement>;
21
+ highlightedMessage: React.RefObject<HTMLDivElement>;
22
+ }
23
+
24
+ const SessionPopover = ({
25
+ anchorEl,
26
+ onClose,
27
+ onOpenFullChat,
28
+ sessions,
29
+ activeSessionId,
30
+ currentSessionId,
31
+ selectedConversationId,
32
+ messagesEndRef,
33
+ highlightedMessage,
34
+ }: SessionPopoverProps) => {
35
+ const session = sessions.find((s) => s.sessionId === activeSessionId);
36
+ const isCurrent = activeSessionId === currentSessionId;
37
+ const popoverEndRef = useRef<HTMLDivElement>(null);
38
+
39
+ useEffect(() => {
40
+ if (anchorEl) {
41
+ setTimeout(() => {
42
+ popoverEndRef.current?.scrollIntoView({ behavior: "smooth" });
43
+ }, 80);
44
+ }
45
+ }, [anchorEl, activeSessionId]);
46
+
47
+ return (
48
+ <Popover
49
+ open={Boolean(anchorEl)}
50
+ anchorEl={anchorEl}
51
+ onClose={onClose}
52
+ anchorOrigin={{ vertical: "center", horizontal: "left" }}
53
+ transformOrigin={{ vertical: "center", horizontal: "right" }}
54
+ slotProps={{
55
+ paper: {
56
+ sx: {
57
+ width: 360,
58
+ height: 500,
59
+ display: "flex",
60
+ flexDirection: "column",
61
+ overflow: "hidden",
62
+ borderRadius: "16px",
63
+ bgcolor: (theme) =>
64
+ alpha(
65
+ theme.palette.mode === "dark"
66
+ ? theme.palette.grey[900]
67
+ : theme.palette.common.white,
68
+ 0.88
69
+ ),
70
+ backdropFilter: "blur(20px) saturate(180%)",
71
+ WebkitBackdropFilter: "blur(20px) saturate(180%)",
72
+ border: (theme) =>
73
+ `1px solid ${alpha(theme.palette.divider, 0.12)}`,
74
+ boxShadow: (theme) =>
75
+ `0 8px 40px ${alpha(theme.palette.common.black, 0.22)}`,
76
+ mr: 1,
77
+ },
78
+ },
79
+ }}
80
+ >
81
+ <Box
82
+ sx={{
83
+ flexShrink: 0,
84
+ px: 2,
85
+ py: 1.5,
86
+ display: "flex",
87
+ alignItems: "center",
88
+ justifyContent: "space-between",
89
+ background: (theme) =>
90
+ `linear-gradient(135deg, ${alpha(
91
+ theme.palette.grey[900],
92
+ 0.97
93
+ )} 0%, ${alpha(theme.palette.grey[800], 0.97)} 100%)`,
94
+ borderBottom: (theme) =>
95
+ `1px solid ${alpha(theme.palette.common.white, 0.06)}`,
96
+ }}
97
+ >
98
+ <Stack direction="row" alignItems="center" spacing={1}>
99
+ <Box
100
+ sx={{
101
+ width: 32,
102
+ height: 32,
103
+ borderRadius: "9px",
104
+ bgcolor: (theme) => (theme.palette.primary.main, 0.2),
105
+ border: (theme) =>
106
+ `1px solid ${alpha(theme.palette.primary.main, 0.3)}`,
107
+ display: "flex",
108
+ alignItems: "center",
109
+ justifyContent: "center",
110
+ flexShrink: 0,
111
+ }}
112
+ >
113
+ {session?.agentIcon ? (
114
+ <Iconify
115
+ icon={cleanIconName(session.agentIcon)}
116
+ sx={{ width: 18, height: 18 }}
117
+ />
118
+ ) : (
119
+ <Typography
120
+ sx={{
121
+ fontSize: "0.72rem",
122
+ fontWeight: 800,
123
+ color: (theme) =>
124
+ isCurrent
125
+ ? theme.palette.primary.light
126
+ : theme.palette.warning.light,
127
+ }}
128
+ >
129
+ {session?.agentName?.[0] ?? "?"}
130
+ </Typography>
131
+ )}
132
+ </Box>
133
+ <Box>
134
+ <Typography
135
+ variant="subtitle2"
136
+ sx={{ color: "white", fontWeight: 700, lineHeight: 1.2 }}
137
+ >
138
+ {session?.agentName}
139
+ </Typography>
140
+ <Typography
141
+ variant="caption"
142
+ sx={{
143
+ color: (theme) => alpha(theme.palette.common.white, 0.5),
144
+ lineHeight: 1,
145
+ }}
146
+ >
147
+ {isCurrent ? "Current · " : "Session · "}
148
+ {session?.messages.length ?? 0} msg
149
+ {session ? ` · ${session.agentName}` : ""}
150
+ </Typography>
151
+ </Box>
152
+ </Stack>
153
+
154
+ <Stack direction="row" spacing={0.5}>
155
+ <Tooltip title="Open full chat" placement="top">
156
+ <IconButton
157
+ size="small"
158
+ onClick={onOpenFullChat}
159
+ sx={{
160
+ color: (theme) => alpha(theme.palette.common.white, 0.7),
161
+ "&:hover": { bgcolor: alpha("#fff", 0.08), color: "white" },
162
+ }}
163
+ >
164
+ <Iconify
165
+ icon="solar:maximize-square-bold"
166
+ sx={{ width: 16, height: 16 }}
167
+ />
168
+ </IconButton>
169
+ </Tooltip>
170
+ <IconButton
171
+ size="small"
172
+ onClick={onClose}
173
+ sx={{
174
+ color: (theme) => alpha(theme.palette.common.white, 0.7),
175
+ "&:hover": { bgcolor: alpha("#fff", 0.08), color: "white" },
176
+ }}
177
+ >
178
+ <Iconify icon="mdi:close" sx={{ width: 16, height: 16 }} />
179
+ </IconButton>
180
+ </Stack>
181
+ </Box>
182
+
183
+ <Box sx={{ flex: 1, minHeight: 0, overflow: "hidden" }}>
184
+ {session ? (
185
+ <Scrollbar sx={{ height: "100%", px: 1.5, py: 1 }}>
186
+ <MessageList
187
+ messages={
188
+ session.messages as {
189
+ id: string;
190
+ content: string;
191
+ role: string;
192
+ }[]
193
+ }
194
+ selectedId={isCurrent ? selectedConversationId : undefined}
195
+ messagesEndRef={messagesEndRef}
196
+ highlightedMessage={highlightedMessage}
197
+ />
198
+ <div ref={popoverEndRef} />
199
+ </Scrollbar>
200
+ ) : (
201
+ <Box
202
+ sx={{
203
+ height: "100%",
204
+ display: "flex",
205
+ flexDirection: "column",
206
+ alignItems: "center",
207
+ justifyContent: "center",
208
+ gap: 1,
209
+ opacity: 0.4,
210
+ }}
211
+ >
212
+ <Iconify icon="solar:history-bold" sx={{ width: 32, height: 32 }} />
213
+ <Typography variant="caption">No messages yet</Typography>
214
+ </Box>
215
+ )}
216
+ </Box>
217
+ </Popover>
218
+ );
219
+ };
220
+
221
+ export default memo(SessionPopover);
@@ -0,0 +1,222 @@
1
+ import ChatDrawer from "./ChatDrawer";
2
+ import SessionPopover from "./SessionPopover";
3
+ import SidebarSessionList from "./SidebarSessionList";
4
+ import { StoredSession } from "./types";
5
+ import { ToolDecision, ToolRenderers } from "../ChatMessage/ToolMessage";
6
+ import { useEvent } from "@nucleoidai/react-event";
7
+ import useSound from "use-sound";
8
+
9
+ import MessageSfx from "./messageSFX.mp3";
10
+ import React, { memo, useCallback, useEffect, useRef, useState } from "react";
11
+
12
+ type Message = { id?: string; content: string; role: string };
13
+
14
+ interface SidebarChatProps {
15
+ selectedConversationId?: string;
16
+ sessionId?: string;
17
+ title: string;
18
+ open: boolean;
19
+ handleToggle: () => void;
20
+ handleNewUserMessage: (message: string) => void;
21
+ history?: Message[];
22
+ readOnly?: boolean;
23
+ sound?: boolean;
24
+ agent?: { id: string; name: string; icon: string };
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
+ Presets?: any[];
27
+ selectedPreset?: string;
28
+ onPresetChange?: (preset: string) => void;
29
+ onSessionSelect?: (sessionId: string, cachedMessages?: Message[]) => void;
30
+ onNewSession?: () => void;
31
+ beta?: boolean;
32
+ storedSessions?: StoredSession[];
33
+ refreshSessions?: () => void;
34
+ clearAllSessions?: () => void;
35
+ toolRenderers?: ToolRenderers;
36
+ onToolDecision?: (toolCallId: string, decision: ToolDecision) => void;
37
+ }
38
+
39
+ const SidebarChat = ({
40
+ selectedConversationId,
41
+ sessionId,
42
+ title,
43
+ open,
44
+ handleToggle,
45
+ handleNewUserMessage,
46
+ history = [],
47
+ readOnly,
48
+ sound,
49
+ agent,
50
+ Presets = [],
51
+ selectedPreset,
52
+ onPresetChange,
53
+ onSessionSelect,
54
+ onNewSession,
55
+ beta,
56
+ storedSessions = [],
57
+ refreshSessions = () => {},
58
+ clearAllSessions = () => {},
59
+ toolRenderers,
60
+ onToolDecision,
61
+ }: SidebarChatProps) => {
62
+ const [aiResponded] = useEvent("AI_RESPONDED", null);
63
+ const [conversationSent] = useEvent("CONVERSATION_SENT", null);
64
+
65
+ const currentSessionId = sessionId || selectedConversationId;
66
+
67
+ const [mute, setMute] = useState(false);
68
+ const [loading, setLoading] = useState(false);
69
+ const [unreadCount, setUnreadCount] = useState(0);
70
+ const [play] = useSound(MessageSfx);
71
+
72
+ const messagesEndRef = useRef(null);
73
+ const highlightedMessage = useRef(null);
74
+ const lastSeenCountRef = useRef(0);
75
+ const sidebarRef = useRef<HTMLDivElement>(null);
76
+
77
+ const [popoverAnchor, setPopoverAnchor] = useState<HTMLElement | null>(null);
78
+ const [activeLogSessionId, setActiveLogSessionId] = useState("");
79
+
80
+ const scrollToBottom = useCallback(() => {
81
+ highlightedMessage.current?.scrollIntoView({
82
+ behavior: "smooth",
83
+ block: "center",
84
+ });
85
+ }, []);
86
+
87
+ useEffect(() => {
88
+ if (open) {
89
+ lastSeenCountRef.current = history.length;
90
+ setUnreadCount(0);
91
+ setPopoverAnchor(null);
92
+ }
93
+ }, [open, history.length]);
94
+
95
+ useEffect(() => {
96
+ if (!open && history.length > lastSeenCountRef.current) {
97
+ setUnreadCount(history.length - lastSeenCountRef.current);
98
+ }
99
+ }, [history, open]);
100
+
101
+ useEffect(() => {
102
+ if (open) {
103
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
104
+ }
105
+ }, [history, open]);
106
+
107
+ useEffect(() => {
108
+ scrollToBottom();
109
+ }, [aiResponded, scrollToBottom]);
110
+
111
+ useEffect(() => {
112
+ if (selectedConversationId) scrollToBottom();
113
+ }, [selectedConversationId, scrollToBottom]);
114
+
115
+ useEffect(() => {
116
+ if (!sound) {
117
+ setLoading(false);
118
+ return;
119
+ } else if (aiResponded !== null) {
120
+ setLoading(false);
121
+ !mute && play();
122
+ }
123
+ // eslint-disable-next-line
124
+ }, [aiResponded, mute, play]);
125
+
126
+ const handleSend = useCallback(
127
+ (content: string) => {
128
+ if (!content.trim()) return;
129
+ handleNewUserMessage(content);
130
+ !mute && play();
131
+ setLoading(true);
132
+ },
133
+ [handleNewUserMessage, mute, play]
134
+ );
135
+
136
+ const showLoading = useCallback(() => {
137
+ if (conversationSent?.createdAt > aiResponded?.createdAt) return true;
138
+ if (loading) return true;
139
+ if (conversationSent && aiResponded === null) return true;
140
+ return false;
141
+ }, [conversationSent, aiResponded, loading]);
142
+
143
+ const handleSessionClick = useCallback(
144
+ (event: React.MouseEvent<HTMLElement>, sid: string) => {
145
+ refreshSessions();
146
+ setActiveLogSessionId(sid);
147
+ setPopoverAnchor(event.currentTarget);
148
+ },
149
+ [refreshSessions]
150
+ );
151
+
152
+ const handleOpenFromPopover = useCallback(() => {
153
+ setPopoverAnchor(null);
154
+ if (activeLogSessionId && activeLogSessionId !== currentSessionId) {
155
+ const cached = storedSessions.find(
156
+ (s) => s.sessionId === activeLogSessionId
157
+ );
158
+ onSessionSelect?.(activeLogSessionId, cached?.messages);
159
+ }
160
+ if (!open) handleToggle();
161
+ }, [
162
+ handleToggle,
163
+ open,
164
+ activeLogSessionId,
165
+ currentSessionId,
166
+ onSessionSelect,
167
+ storedSessions,
168
+ ]);
169
+
170
+ return (
171
+ <>
172
+ {!open && (
173
+ <SidebarSessionList
174
+ sessions={storedSessions}
175
+ currentSessionId={currentSessionId}
176
+ unreadCount={unreadCount}
177
+ onToggleChat={handleToggle}
178
+ onSessionClick={handleSessionClick}
179
+ onClearAll={clearAllSessions}
180
+ wrapperRef={sidebarRef}
181
+ beta={beta}
182
+ onNewSession={onNewSession}
183
+ />
184
+ )}
185
+
186
+ <SessionPopover
187
+ anchorEl={popoverAnchor}
188
+ onClose={() => setPopoverAnchor(null)}
189
+ onOpenFullChat={handleOpenFromPopover}
190
+ sessions={storedSessions}
191
+ activeSessionId={activeLogSessionId}
192
+ currentSessionId={currentSessionId}
193
+ selectedConversationId={selectedConversationId}
194
+ messagesEndRef={messagesEndRef}
195
+ highlightedMessage={highlightedMessage}
196
+ />
197
+
198
+ <ChatDrawer
199
+ title={title}
200
+ open={open}
201
+ onClose={handleToggle}
202
+ history={history}
203
+ selectedConversationId={selectedConversationId}
204
+ readOnly={readOnly}
205
+ mute={mute}
206
+ onMuteToggle={() => setMute((prev) => !prev)}
207
+ showLoading={showLoading()}
208
+ onSend={handleSend}
209
+ Presets={Presets}
210
+ selectedPreset={selectedPreset}
211
+ onPresetChange={onPresetChange}
212
+ messagesEndRef={messagesEndRef}
213
+ highlightedMessage={highlightedMessage}
214
+ onNewSession={onNewSession}
215
+ toolRenderers={toolRenderers}
216
+ onToolDecision={onToolDecision}
217
+ />
218
+ </>
219
+ );
220
+ };
221
+
222
+ export default memo(SidebarChat);