@canmingir/link 1.2.55 → 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,407 @@
1
+ import PresetSelector from "../PresetSelector/PresetSelector";
2
+ import Editor from "@monaco-editor/react";
3
+ import { Iconify } from "@canmingir/link/platform/components";
4
+ import { Scrollbar } from "@canmingir/link/platform/components";
5
+ import Stack from "@mui/material/Stack";
6
+ import { alpha } from "@mui/material/styles";
7
+
8
+ import {
9
+ Box,
10
+ Drawer,
11
+ IconButton,
12
+ TextField,
13
+ ToggleButton,
14
+ ToggleButtonGroup,
15
+ Tooltip,
16
+ Typography,
17
+ } from "@mui/material";
18
+ import { LoadingMessage, MessageList } from "../ChatMessage";
19
+ import { ToolDecision, ToolRenderers } from "../ChatMessage/ToolMessage";
20
+ import React, { memo, useCallback, useRef, useState } from "react";
21
+
22
+ const DRAWER_WIDTH = 400;
23
+
24
+ type InputMode = "chat" | "json";
25
+
26
+ interface ChatDrawerProps {
27
+ title: string;
28
+ open: boolean;
29
+ onClose: () => void;
30
+ history: { id?: string; content: string; role: string }[];
31
+ selectedConversationId?: string;
32
+ readOnly?: boolean;
33
+ mute: boolean;
34
+ onMuteToggle: () => void;
35
+ showLoading: boolean;
36
+ onSend: (content: string) => void;
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ Presets?: any[];
39
+ selectedPreset?: string;
40
+ onPresetChange?: (preset: string) => void;
41
+ messagesEndRef: React.RefObject<HTMLDivElement>;
42
+ highlightedMessage: React.RefObject<HTMLDivElement>;
43
+ onNewSession?: () => void;
44
+ toolRenderers?: ToolRenderers;
45
+ onToolDecision?: (toolCallId: string, decision: ToolDecision) => void;
46
+ }
47
+
48
+ const DEFAULT_JSON = `{\n \n}`;
49
+
50
+ const ChatDrawer = ({
51
+ title,
52
+ open,
53
+ onClose,
54
+ history,
55
+ selectedConversationId,
56
+ readOnly,
57
+ mute,
58
+ onMuteToggle,
59
+ showLoading,
60
+ onSend,
61
+ Presets = [],
62
+ selectedPreset,
63
+ onPresetChange,
64
+ messagesEndRef,
65
+ highlightedMessage,
66
+ toolRenderers,
67
+ onToolDecision,
68
+ }: ChatDrawerProps) => {
69
+ const inputRef = useRef(null);
70
+ const [inputMode, setInputMode] = useState<InputMode>("chat");
71
+ const [jsonValue, setJsonValue] = useState(DEFAULT_JSON);
72
+ const [jsonError, setJsonError] = useState<string | null>(null);
73
+
74
+ const handleModeChange = useCallback(
75
+ (_: React.MouseEvent<HTMLElement>, newMode: InputMode | null) => {
76
+ if (newMode) setInputMode(newMode);
77
+ },
78
+ []
79
+ );
80
+
81
+ const handleKeyDown = useCallback(
82
+ (event: React.KeyboardEvent) => {
83
+ if (event.key === "Enter" && !event.shiftKey) {
84
+ event.preventDefault();
85
+ const content = inputRef.current?.value?.trim();
86
+ if (content) {
87
+ onSend(content);
88
+ inputRef.current.value = "";
89
+ }
90
+ }
91
+ },
92
+ [onSend]
93
+ );
94
+
95
+ const handleJsonChange = useCallback((value: string | undefined) => {
96
+ const v = value ?? "";
97
+ setJsonValue(v);
98
+ try {
99
+ JSON.parse(v);
100
+ setJsonError(null);
101
+ } catch {
102
+ setJsonError("Invalid JSON");
103
+ }
104
+ }, []);
105
+
106
+ const handleJsonSend = useCallback(() => {
107
+ try {
108
+ const parsed = JSON.parse(jsonValue);
109
+ onSend(JSON.stringify(parsed));
110
+ setJsonValue(DEFAULT_JSON);
111
+ setJsonError(null);
112
+ } catch {
113
+ setJsonError("Invalid JSON — fix before sending");
114
+ }
115
+ }, [jsonValue, onSend]);
116
+
117
+ const handleJsonKeyDown = useCallback(
118
+ (event: React.KeyboardEvent) => {
119
+ if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
120
+ event.preventDefault();
121
+ handleJsonSend();
122
+ }
123
+ },
124
+ [handleJsonSend]
125
+ );
126
+
127
+ return (
128
+ <Drawer
129
+ anchor="right"
130
+ open={open}
131
+ onClose={onClose}
132
+ variant="persistent"
133
+ sx={{
134
+ width: open ? DRAWER_WIDTH : 0,
135
+ flexShrink: 0,
136
+ "& .MuiDrawer-paper": {
137
+ width: DRAWER_WIDTH,
138
+ boxSizing: "border-box",
139
+ border: "none",
140
+ boxShadow: (theme) => theme.shadows[8],
141
+ },
142
+ }}
143
+ >
144
+ <Box
145
+ sx={{
146
+ display: "flex",
147
+ flexDirection: "column",
148
+ height: "100%",
149
+ bgcolor: (theme) => theme.palette.background.default,
150
+ }}
151
+ >
152
+ {/* Header */}
153
+ <Box
154
+ sx={{
155
+ bgcolor: (theme) => alpha(theme.palette.grey[900], 0.9),
156
+ display: "flex",
157
+ justifyContent: "space-between",
158
+ alignItems: "center",
159
+ p: 2,
160
+ borderBottom: (theme) =>
161
+ `1px solid ${alpha(theme.palette.divider, 0.1)}`,
162
+ }}
163
+ >
164
+ <Typography variant="h6" sx={{ color: "white", fontWeight: 600 }}>
165
+ {title}
166
+ </Typography>
167
+ <Stack direction="row" spacing={1}>
168
+ {!readOnly && (
169
+ <IconButton onClick={onMuteToggle} size="small">
170
+ <Iconify
171
+ icon={
172
+ mute
173
+ ? "solar:volume-cross-bold-duotone"
174
+ : "solar:volume-loud-bold-duotone"
175
+ }
176
+ sx={{ width: 20, height: 20, color: "white" }}
177
+ />
178
+ </IconButton>
179
+ )}
180
+ <IconButton onClick={onClose} size="small">
181
+ <Iconify
182
+ icon="mdi:chevron-right"
183
+ sx={{ width: 24, height: 24, color: "white" }}
184
+ />
185
+ </IconButton>
186
+ </Stack>
187
+ </Box>
188
+
189
+ {Presets.length > 0 && !readOnly && (
190
+ <Box sx={{ px: 2, pt: 1 }}>
191
+ <PresetSelector
192
+ Presets={Presets}
193
+ selectedPreset={selectedPreset}
194
+ onPresetChange={onPresetChange}
195
+ />
196
+ </Box>
197
+ )}
198
+
199
+ {/* Messages */}
200
+ <Box sx={{ flex: 1, overflow: "hidden", p: 2 }}>
201
+ <Scrollbar sx={{ height: "100%" }}>
202
+ <MessageList
203
+ messages={
204
+ history as { id: string; content: string; role: string }[]
205
+ }
206
+ selectedId={selectedConversationId}
207
+ messagesEndRef={messagesEndRef}
208
+ highlightedMessage={highlightedMessage}
209
+ toolRenderers={toolRenderers}
210
+ onToolDecision={onToolDecision}
211
+ />
212
+ {showLoading && <LoadingMessage messagesEndRef={messagesEndRef} />}
213
+ </Scrollbar>
214
+ </Box>
215
+
216
+ {/* Input Area */}
217
+ {!readOnly && (
218
+ <Box
219
+ sx={{
220
+ borderTop: (theme) =>
221
+ `1px solid ${alpha(theme.palette.divider, 0.1)}`,
222
+ bgcolor: (theme) => alpha(theme.palette.grey[900], 0.5),
223
+ }}
224
+ >
225
+ {/* Mode Toggle */}
226
+ <Box
227
+ sx={{
228
+ display: "flex",
229
+ alignItems: "center",
230
+ px: 2,
231
+ pt: 1.5,
232
+ pb: 1,
233
+ gap: 1,
234
+ }}
235
+ >
236
+ <ToggleButtonGroup
237
+ value={inputMode}
238
+ exclusive
239
+ onChange={handleModeChange}
240
+ size="small"
241
+ sx={{
242
+ "& .MuiToggleButton-root": {
243
+ px: 1.5,
244
+ py: 0.5,
245
+ fontSize: "0.7rem",
246
+ fontWeight: 600,
247
+ letterSpacing: 0.5,
248
+ textTransform: "uppercase",
249
+ color: (theme) => alpha(theme.palette.text.secondary, 0.7),
250
+ borderColor: (theme) => alpha(theme.palette.divider, 0.3),
251
+ "&.Mui-selected": {
252
+ color: (theme) => theme.palette.primary.light,
253
+ bgcolor: (theme) =>
254
+ alpha(theme.palette.primary.main, 0.15),
255
+ borderColor: (theme) =>
256
+ alpha(theme.palette.primary.main, 0.4),
257
+ },
258
+ },
259
+ }}
260
+ >
261
+ <ToggleButton value="chat">
262
+ <Iconify
263
+ icon="solar:chat-round-line-bold-duotone"
264
+ sx={{ width: 14, height: 14, mr: 0.5 }}
265
+ />
266
+ Chat
267
+ </ToggleButton>
268
+ <ToggleButton value="json">
269
+ <Iconify
270
+ icon="solar:code-bold-duotone"
271
+ sx={{ width: 14, height: 14, mr: 0.5 }}
272
+ />
273
+ JSON
274
+ </ToggleButton>
275
+ </ToggleButtonGroup>
276
+ </Box>
277
+
278
+ {/* Chat Input */}
279
+ {inputMode === "chat" && (
280
+ <Box sx={{ px: 2, pb: 2 }}>
281
+ <TextField
282
+ variant="outlined"
283
+ autoComplete="off"
284
+ fullWidth
285
+ placeholder="Type a message..."
286
+ inputRef={inputRef}
287
+ onKeyDown={handleKeyDown}
288
+ size="small"
289
+ sx={{
290
+ "& .MuiOutlinedInput-root": {
291
+ bgcolor: (theme) =>
292
+ alpha(theme.palette.background.paper, 0.8),
293
+ },
294
+ }}
295
+ slotProps={{
296
+ input: {
297
+ endAdornment: (
298
+ <IconButton
299
+ onClick={() => {
300
+ const content = inputRef.current?.value?.trim();
301
+ if (content) {
302
+ onSend(content);
303
+ inputRef.current.value = "";
304
+ }
305
+ }}
306
+ size="small"
307
+ >
308
+ <Iconify
309
+ icon="material-symbols:send"
310
+ sx={{ width: 20, height: 20 }}
311
+ />
312
+ </IconButton>
313
+ ),
314
+ },
315
+ }}
316
+ />
317
+ </Box>
318
+ )}
319
+
320
+ {/* JSON Input */}
321
+ {inputMode === "json" && (
322
+ <Box sx={{ px: 2, pb: 2 }} onKeyDown={handleJsonKeyDown}>
323
+ <Box
324
+ sx={{
325
+ border: (theme) =>
326
+ `1px solid ${
327
+ jsonError
328
+ ? theme.palette.error.main
329
+ : alpha(theme.palette.primary.main, 0.3)
330
+ }`,
331
+ borderRadius: 1,
332
+ overflow: "hidden",
333
+ bgcolor: "#1e1e1e",
334
+ }}
335
+ >
336
+ <Editor
337
+ height="160px"
338
+ defaultLanguage="json"
339
+ value={jsonValue}
340
+ onChange={handleJsonChange}
341
+ theme="vs-dark"
342
+ options={{
343
+ minimap: { enabled: false },
344
+ fontSize: 12,
345
+ lineNumbers: "off",
346
+ scrollBeyondLastLine: false,
347
+ wordWrap: "on",
348
+ tabSize: 2,
349
+ folding: false,
350
+ glyphMargin: false,
351
+ lineDecorationsWidth: 0,
352
+ lineNumbersMinChars: 0,
353
+ padding: { top: 8, bottom: 8 },
354
+ }}
355
+ />
356
+ </Box>
357
+
358
+ <Box
359
+ sx={{
360
+ display: "flex",
361
+ alignItems: "center",
362
+ justifyContent: "space-between",
363
+ mt: 1,
364
+ }}
365
+ >
366
+ <Typography
367
+ variant="caption"
368
+ sx={{
369
+ color: jsonError
370
+ ? "error.main"
371
+ : (theme) => alpha(theme.palette.text.secondary, 0.5),
372
+ fontSize: "0.7rem",
373
+ }}
374
+ >
375
+ {jsonError ?? "⌘↵ to send"}
376
+ </Typography>
377
+ <Tooltip title="Send JSON (⌘↵)" placement="top">
378
+ <span>
379
+ <IconButton
380
+ onClick={handleJsonSend}
381
+ size="small"
382
+ disabled={!!jsonError}
383
+ sx={{
384
+ color: (theme) =>
385
+ jsonError
386
+ ? theme.palette.action.disabled
387
+ : theme.palette.primary.light,
388
+ }}
389
+ >
390
+ <Iconify
391
+ icon="material-symbols:send"
392
+ sx={{ width: 18, height: 18 }}
393
+ />
394
+ </IconButton>
395
+ </span>
396
+ </Tooltip>
397
+ </Box>
398
+ </Box>
399
+ )}
400
+ </Box>
401
+ )}
402
+ </Box>
403
+ </Drawer>
404
+ );
405
+ };
406
+
407
+ export default memo(ChatDrawer);
@@ -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);