@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,112 @@
1
+ import { Iconify } from "@canmingir/link/platform/components";
2
+ import React from "react";
3
+ import { alpha } from "@mui/material/styles";
4
+
5
+ import {
6
+ Box,
7
+ FormControl,
8
+ InputLabel,
9
+ MenuItem,
10
+ Select,
11
+ Typography,
12
+ } from "@mui/material";
13
+
14
+ const PresetSelector = ({
15
+ Presets,
16
+ selectedPreset,
17
+ onPresetChange,
18
+ }: {
19
+ Presets: Array<{
20
+ id: string;
21
+ title: string;
22
+ description?: string;
23
+ }>;
24
+ selectedPreset?: string;
25
+ onPresetChange: (presetId: string) => void;
26
+ }) => {
27
+ return (
28
+ <Box
29
+ className="noDrag"
30
+ sx={{
31
+ width: "100%",
32
+ p: 1.5,
33
+ bgcolor: (theme) => alpha(theme.palette.grey[900], 0.8),
34
+ boxShadow: 2,
35
+ }}
36
+ >
37
+ <FormControl fullWidth>
38
+ <InputLabel
39
+ sx={{
40
+ color: "rgba(255, 255, 255, 0.7)",
41
+ "&.Mui-focused": {
42
+ color: "white",
43
+ },
44
+ }}
45
+ >
46
+ Preset
47
+ </InputLabel>
48
+ <Select
49
+ value={selectedPreset || "Automatic"}
50
+ onChange={(e) => onPresetChange(e.target.value)}
51
+ label="Preset"
52
+ sx={{
53
+ color: "white",
54
+ "& .MuiOutlinedInput-notchedOutline": {
55
+ borderColor: "rgba(255, 255, 255, 0.3)",
56
+ },
57
+ "&:hover .MuiOutlinedInput-notchedOutline": {
58
+ borderColor: "rgba(255, 255, 255, 0.5)",
59
+ },
60
+ "&.Mui-focused .MuiOutlinedInput-notchedOutline": {
61
+ borderColor: "white",
62
+ },
63
+ "& .MuiSelect-icon": {
64
+ color: "rgba(255, 255, 255, 0.7)",
65
+ },
66
+ }}
67
+ MenuProps={{
68
+ PaperProps: {
69
+ sx: {
70
+ bgcolor: (theme) => alpha(theme.palette.grey[900], 0.95),
71
+ "& .MuiMenuItem-root": {
72
+ color: "white",
73
+ "&:hover": {
74
+ bgcolor: (theme) => alpha(theme.palette.primary.main, 0.2),
75
+ },
76
+ "&.Mui-selected": {
77
+ bgcolor: (theme) => alpha(theme.palette.primary.main, 0.3),
78
+ "&:hover": {
79
+ bgcolor: (theme) =>
80
+ alpha(theme.palette.primary.main, 0.4),
81
+ },
82
+ },
83
+ },
84
+ },
85
+ },
86
+ }}
87
+ >
88
+ <MenuItem value="Automatic">
89
+ <Typography sx={{ color: "white" }}>Automatic</Typography>
90
+ </MenuItem>
91
+ {Presets.map((Preset) => (
92
+ <MenuItem key={Preset.id} value={Preset.id}>
93
+ <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
94
+ <Iconify
95
+ icon="healthicons:crisis-response-center-person-outline"
96
+ width={20}
97
+ height={20}
98
+ sx={{ color: "white" }}
99
+ />
100
+ <Typography sx={{ color: "white" }}>
101
+ {Preset.title}
102
+ </Typography>
103
+ </Box>
104
+ </MenuItem>
105
+ ))}
106
+ </Select>
107
+ </FormControl>
108
+ </Box>
109
+ );
110
+ };
111
+
112
+ export default PresetSelector;
@@ -0,0 +1 @@
1
+ export { default } from "./PresetSelector";
@@ -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);