@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,81 @@
1
+ import { AIMessage } from "./AIMessage";
2
+ import { ErrorMessage } from "./ErrorMessage";
3
+ import { HumanMessage } from "./HumanMessage";
4
+ import { ToolDecision, ToolMessage, ToolRenderers } from "./ToolMessage";
5
+
6
+ import React, { memo } from "react";
7
+
8
+ const MessageList = memo(
9
+ ({
10
+ error,
11
+ messages,
12
+ selectedId,
13
+ messagesEndRef,
14
+ highlightedMessage,
15
+ onErrorClose,
16
+ toolRenderers,
17
+ onToolDecision,
18
+ }: {
19
+ error?: string;
20
+ messages: { id: string; content: string; role: string }[];
21
+ selectedId: string;
22
+ messagesEndRef: { current: HTMLDivElement | null };
23
+ highlightedMessage: { current: HTMLDivElement | null };
24
+ onErrorClose?: () => void;
25
+ toolRenderers?: ToolRenderers;
26
+ onToolDecision?: (toolCallId: string, decision: ToolDecision) => void;
27
+ }) => (
28
+ <>
29
+ {messages.map((item, index) => {
30
+ const isLastMessage = index === messages.length - 1;
31
+ const isSelected = item.id === selectedId;
32
+
33
+ if (item.role === "USER") {
34
+ return (
35
+ <HumanMessage
36
+ key={item.id || index}
37
+ message={item}
38
+ selectedId={selectedId}
39
+ messageRef={
40
+ isSelected
41
+ ? highlightedMessage
42
+ : isLastMessage
43
+ ? messagesEndRef
44
+ : undefined
45
+ }
46
+ />
47
+ );
48
+ }
49
+
50
+ if (item.role === "TOOL") {
51
+ return (
52
+ <ToolMessage
53
+ key={item.id || index}
54
+ content={item.content}
55
+ messageRef={isLastMessage ? messagesEndRef : undefined}
56
+ renderers={toolRenderers}
57
+ onDecision={onToolDecision}
58
+ />
59
+ );
60
+ }
61
+
62
+ return (
63
+ <AIMessage
64
+ key={item.id || index}
65
+ content={item.content}
66
+ messageRef={isLastMessage ? messagesEndRef : undefined}
67
+ />
68
+ );
69
+ })}
70
+ {error && (
71
+ <ErrorMessage
72
+ key="error-message"
73
+ content={error}
74
+ onClose={onErrorClose}
75
+ />
76
+ )}
77
+ </>
78
+ )
79
+ );
80
+
81
+ export { MessageList };
@@ -0,0 +1,346 @@
1
+ import { Iconify } from "@canmingir/link/platform/components";
2
+ import { alpha } from "@mui/material/styles";
3
+
4
+ import { Box, Button, Collapse, Stack, Typography } from "@mui/material";
5
+ import React, { memo, useMemo, useState } from "react";
6
+
7
+ export type ToolStatus =
8
+ | "awaiting_approval"
9
+ | "pending"
10
+ | "success"
11
+ | "error"
12
+ | "declined";
13
+
14
+ export type ToolDecision = "approve" | "decline" | "approve_all";
15
+
16
+ export interface ToolMessageContent {
17
+ id: string;
18
+ name: string;
19
+ input?: Record<string, unknown>;
20
+ output?: string;
21
+ status: ToolStatus;
22
+ }
23
+
24
+ export interface ToolRendererProps {
25
+ name: string;
26
+ input?: Record<string, unknown>;
27
+ output?: string;
28
+ status: ToolStatus;
29
+ }
30
+
31
+ export type ToolRenderers = Record<
32
+ string,
33
+ React.ComponentType<ToolRendererProps>
34
+ >;
35
+
36
+ function formatLabel(name: string): string {
37
+ return name
38
+ .split("_")
39
+ .filter(Boolean)
40
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
41
+ .join(" ");
42
+ }
43
+
44
+ function formatValue(value: unknown): string {
45
+ if (value === null || value === undefined || value === "") return "—";
46
+ if (typeof value === "string") return value;
47
+ if (typeof value === "number" || typeof value === "boolean") {
48
+ return String(value);
49
+ }
50
+ if (Array.isArray(value)) {
51
+ return value.length ? value.map(formatValue).join(", ") : "—";
52
+ }
53
+ if (typeof value === "object") {
54
+ return Object.entries(value as Record<string, unknown>)
55
+ .map(([key, val]) => `${formatLabel(key)}: ${formatValue(val)}`)
56
+ .join(", ");
57
+ }
58
+ return String(value);
59
+ }
60
+
61
+ function statusIcon(status: ToolStatus): string {
62
+ if (status === "success") return "solar:check-circle-bold";
63
+ if (status === "error") return "solar:close-circle-bold";
64
+ if (status === "declined") return "solar:forbidden-circle-bold";
65
+ if (status === "awaiting_approval") return "solar:shield-warning-bold";
66
+ return "svg-spinners:tadpole";
67
+ }
68
+
69
+ function statusColor(
70
+ status: ToolStatus
71
+ ): "success.main" | "error.main" | "warning.main" | "text.secondary" {
72
+ if (status === "success") return "success.main";
73
+ if (status === "error") return "error.main";
74
+ if (status === "awaiting_approval") return "warning.main";
75
+ return "text.secondary";
76
+ }
77
+
78
+ function FieldRow({ label, value }: { label: string; value: unknown }) {
79
+ return (
80
+ <Stack direction="row" spacing={1} sx={{ py: 0.4 }}>
81
+ <Typography
82
+ sx={{
83
+ fontSize: "0.72rem",
84
+ fontWeight: 600,
85
+ color: "text.secondary",
86
+ minWidth: 90,
87
+ flexShrink: 0,
88
+ }}
89
+ >
90
+ {label}
91
+ </Typography>
92
+ <Typography sx={{ fontSize: "0.78rem", wordBreak: "break-word" }}>
93
+ {formatValue(value)}
94
+ </Typography>
95
+ </Stack>
96
+ );
97
+ }
98
+
99
+ function FieldList({ data }: { data: Record<string, unknown> }) {
100
+ const entries = Object.entries(data);
101
+ if (!entries.length) {
102
+ return (
103
+ <Typography sx={{ fontSize: "0.78rem", color: "text.secondary" }}>
104
+ No fields
105
+ </Typography>
106
+ );
107
+ }
108
+
109
+ return (
110
+ <Stack>
111
+ {entries.map(([key, value]) => (
112
+ <FieldRow key={key} label={formatLabel(key)} value={value} />
113
+ ))}
114
+ </Stack>
115
+ );
116
+ }
117
+
118
+ function ResultView({ output }: { output: string }) {
119
+ const parsed = useMemo(() => {
120
+ try {
121
+ return JSON.parse(output);
122
+ } catch {
123
+ return undefined;
124
+ }
125
+ }, [output]);
126
+
127
+ if (parsed === undefined) {
128
+ return (
129
+ <Typography sx={{ fontSize: "0.78rem", whiteSpace: "pre-wrap" }}>
130
+ {output}
131
+ </Typography>
132
+ );
133
+ }
134
+
135
+ if (Array.isArray(parsed)) {
136
+ if (!parsed.length) {
137
+ return (
138
+ <Typography sx={{ fontSize: "0.78rem", color: "text.secondary" }}>
139
+ Empty result
140
+ </Typography>
141
+ );
142
+ }
143
+
144
+ return (
145
+ <Stack spacing={1}>
146
+ {parsed.map((item, index) =>
147
+ item && typeof item === "object" ? (
148
+ <Box
149
+ key={index}
150
+ sx={{
151
+ pl: 1,
152
+ borderLeft: (theme) =>
153
+ `2px solid ${alpha(theme.palette.grey[500], 0.3)}`,
154
+ }}
155
+ >
156
+ <FieldList data={item as Record<string, unknown>} />
157
+ </Box>
158
+ ) : (
159
+ <Typography key={index} sx={{ fontSize: "0.78rem" }}>
160
+ {formatValue(item)}
161
+ </Typography>
162
+ )
163
+ )}
164
+ </Stack>
165
+ );
166
+ }
167
+
168
+ if (parsed && typeof parsed === "object") {
169
+ return <FieldList data={parsed as Record<string, unknown>} />;
170
+ }
171
+
172
+ return (
173
+ <Typography sx={{ fontSize: "0.78rem" }}>{formatValue(parsed)}</Typography>
174
+ );
175
+ }
176
+
177
+ function ApprovalActions({
178
+ toolCallId,
179
+ onDecision,
180
+ }: {
181
+ toolCallId: string;
182
+ onDecision: (toolCallId: string, decision: ToolDecision) => void;
183
+ }) {
184
+ return (
185
+ <Stack direction="row" spacing={1} sx={{ mt: 1 }}>
186
+ <Button
187
+ size="small"
188
+ variant="contained"
189
+ color="success"
190
+ onClick={() => onDecision(toolCallId, "approve")}
191
+ sx={{ fontSize: "0.7rem", py: 0.25 }}
192
+ >
193
+ Approve
194
+ </Button>
195
+ <Button
196
+ size="small"
197
+ variant="outlined"
198
+ color="error"
199
+ onClick={() => onDecision(toolCallId, "decline")}
200
+ sx={{ fontSize: "0.7rem", py: 0.25 }}
201
+ >
202
+ Decline
203
+ </Button>
204
+ <Button
205
+ size="small"
206
+ variant="text"
207
+ onClick={() => onDecision(toolCallId, "approve_all")}
208
+ sx={{ fontSize: "0.7rem", py: 0.25 }}
209
+ >
210
+ Approve All
211
+ </Button>
212
+ </Stack>
213
+ );
214
+ }
215
+
216
+ const ToolMessage: React.FC<{
217
+ content: string;
218
+ messageRef?: React.RefObject<HTMLDivElement>;
219
+ renderers?: ToolRenderers;
220
+ onDecision?: (toolCallId: string, decision: ToolDecision) => void;
221
+ }> = memo(({ content, messageRef, renderers, onDecision }) => {
222
+ const [expanded, setExpanded] = useState(false);
223
+
224
+ const tool = useMemo<ToolMessageContent | null>(() => {
225
+ try {
226
+ const parsed = JSON.parse(content);
227
+ if (parsed && typeof parsed.name === "string") return parsed;
228
+ return null;
229
+ } catch {
230
+ return null;
231
+ }
232
+ }, [content]);
233
+
234
+ if (!tool) return null;
235
+
236
+ const CustomBody = renderers?.[tool.name];
237
+ const awaitingApproval = tool.status === "awaiting_approval";
238
+ const alwaysShowBody = CustomBody || awaitingApproval;
239
+
240
+ return (
241
+ <Stack
242
+ ref={messageRef}
243
+ sx={{
244
+ p: 1.5,
245
+ mt: 1.5,
246
+ borderRadius: 2,
247
+ backgroundColor: (theme) => alpha(theme.palette.grey[800], 0.4),
248
+ border: (theme) =>
249
+ `1px solid ${alpha(
250
+ awaitingApproval ? theme.palette.warning.main : theme.palette.grey[500],
251
+ awaitingApproval ? 0.4 : 0.2
252
+ )}`,
253
+ }}
254
+ >
255
+ <Stack
256
+ direction="row"
257
+ spacing={1}
258
+ alignItems="center"
259
+ onClick={alwaysShowBody ? undefined : () => setExpanded((e) => !e)}
260
+ sx={{ cursor: alwaysShowBody ? "default" : "pointer" }}
261
+ >
262
+ <Iconify
263
+ icon="solar:widget-bold-duotone"
264
+ sx={{ width: 18, height: 18, color: "text.secondary" }}
265
+ />
266
+ <Typography sx={{ fontSize: "0.8rem", fontWeight: 600, flex: 1 }}>
267
+ {formatLabel(tool.name)}
268
+ </Typography>
269
+ <Iconify
270
+ icon={statusIcon(tool.status)}
271
+ sx={{ width: 16, height: 16, color: statusColor(tool.status) }}
272
+ />
273
+ {!alwaysShowBody && (
274
+ <Iconify
275
+ icon={expanded ? "mdi:chevron-up" : "mdi:chevron-down"}
276
+ sx={{ width: 16, height: 16, color: "text.secondary" }}
277
+ />
278
+ )}
279
+ </Stack>
280
+
281
+ {CustomBody ? (
282
+ <Box sx={{ mt: 1 }}>
283
+ <CustomBody
284
+ name={tool.name}
285
+ input={tool.input}
286
+ output={tool.output}
287
+ status={tool.status}
288
+ />
289
+ </Box>
290
+ ) : awaitingApproval ? (
291
+ <Box sx={{ mt: 1 }}>
292
+ {tool.input && <FieldList data={tool.input} />}
293
+ </Box>
294
+ ) : (
295
+ <Collapse in={expanded}>
296
+ <Box sx={{ mt: 1 }}>
297
+ {tool.input && (
298
+ <>
299
+ <Typography
300
+ variant="caption"
301
+ sx={{
302
+ display: "block",
303
+ color: (theme) => alpha(theme.palette.text.secondary, 0.7),
304
+ fontSize: "0.65rem",
305
+ fontWeight: 600,
306
+ textTransform: "uppercase",
307
+ letterSpacing: 0.5,
308
+ }}
309
+ >
310
+ Arguments
311
+ </Typography>
312
+ <FieldList data={tool.input} />
313
+ </>
314
+ )}
315
+
316
+ {tool.output !== undefined && (
317
+ <>
318
+ <Typography
319
+ variant="caption"
320
+ sx={{
321
+ display: "block",
322
+ mt: 1,
323
+ color: (theme) => alpha(theme.palette.text.secondary, 0.7),
324
+ fontSize: "0.65rem",
325
+ fontWeight: 600,
326
+ textTransform: "uppercase",
327
+ letterSpacing: 0.5,
328
+ }}
329
+ >
330
+ Result
331
+ </Typography>
332
+ <ResultView output={tool.output} />
333
+ </>
334
+ )}
335
+ </Box>
336
+ </Collapse>
337
+ )}
338
+
339
+ {awaitingApproval && onDecision && (
340
+ <ApprovalActions toolCallId={tool.id} onDecision={onDecision} />
341
+ )}
342
+ </Stack>
343
+ );
344
+ });
345
+
346
+ export { ToolMessage };
@@ -0,0 +1,6 @@
1
+ export { AIMessage } from "./AIMessage";
2
+ export { ErrorMessage } from "./ErrorMessage";
3
+ export { HumanMessage } from "./HumanMessage";
4
+ export { LoadingMessage } from "./LoadingMessage";
5
+ export { MessageList } from "./MessageList";
6
+ export { ToolMessage } from "./ToolMessage";
@@ -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";