@hed-hog/core 0.0.349 → 0.0.350

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.
Files changed (126) hide show
  1. package/dist/auth/auth.controller.d.ts +3 -3
  2. package/dist/auth/auth.service.d.ts +8 -8
  3. package/dist/core.module.d.ts.map +1 -1
  4. package/dist/core.module.js +3 -0
  5. package/dist/core.module.js.map +1 -1
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +4 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/mcp/guards/mcp-auth.guard.d.ts +6 -1
  11. package/dist/mcp/guards/mcp-auth.guard.d.ts.map +1 -1
  12. package/dist/mcp/guards/mcp-auth.guard.js +29 -2
  13. package/dist/mcp/guards/mcp-auth.guard.js.map +1 -1
  14. package/dist/mcp/mcp-api-key.controller.d.ts +32 -0
  15. package/dist/mcp/mcp-api-key.controller.d.ts.map +1 -0
  16. package/dist/mcp/mcp-api-key.controller.js +64 -0
  17. package/dist/mcp/mcp-api-key.controller.js.map +1 -0
  18. package/dist/mcp/mcp-api-key.service.d.ts +27 -0
  19. package/dist/mcp/mcp-api-key.service.d.ts.map +1 -0
  20. package/dist/mcp/mcp-api-key.service.js +74 -0
  21. package/dist/mcp/mcp-api-key.service.js.map +1 -0
  22. package/dist/mcp/mcp-registry.service.d.ts +1 -2
  23. package/dist/mcp/mcp-registry.service.d.ts.map +1 -1
  24. package/dist/mcp/mcp-registry.service.js +6 -2
  25. package/dist/mcp/mcp-registry.service.js.map +1 -1
  26. package/dist/mcp/mcp.module.d.ts.map +1 -1
  27. package/dist/mcp/mcp.module.js +8 -1
  28. package/dist/mcp/mcp.module.js.map +1 -1
  29. package/dist/mcp-chat/mcp-chat.controller.d.ts +4 -0
  30. package/dist/mcp-chat/mcp-chat.controller.d.ts.map +1 -1
  31. package/dist/mcp-chat/mcp-chat.controller.js +46 -0
  32. package/dist/mcp-chat/mcp-chat.controller.js.map +1 -1
  33. package/dist/mcp-chat/mcp-chat.service.d.ts +6 -1
  34. package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -1
  35. package/dist/mcp-chat/mcp-chat.service.js +125 -17
  36. package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
  37. package/dist/notification/dto/create-notification.dto.d.ts +26 -0
  38. package/dist/notification/dto/create-notification.dto.d.ts.map +1 -0
  39. package/dist/notification/dto/create-notification.dto.js +86 -0
  40. package/dist/notification/dto/create-notification.dto.js.map +1 -0
  41. package/dist/notification/dto/update-progress.dto.d.ts +6 -0
  42. package/dist/notification/dto/update-progress.dto.d.ts.map +1 -0
  43. package/dist/notification/dto/update-progress.dto.js +33 -0
  44. package/dist/notification/dto/update-progress.dto.js.map +1 -0
  45. package/dist/notification/notification.controller.d.ts +108 -0
  46. package/dist/notification/notification.controller.d.ts.map +1 -0
  47. package/dist/notification/notification.controller.js +118 -0
  48. package/dist/notification/notification.controller.js.map +1 -0
  49. package/dist/notification/notification.module.d.ts +3 -0
  50. package/dist/notification/notification.module.d.ts.map +1 -0
  51. package/dist/notification/notification.module.js +29 -0
  52. package/dist/notification/notification.module.js.map +1 -0
  53. package/dist/notification/notification.service.d.ts +98 -0
  54. package/dist/notification/notification.service.d.ts.map +1 -0
  55. package/dist/notification/notification.service.js +123 -0
  56. package/dist/notification/notification.service.js.map +1 -0
  57. package/dist/profile/profile.controller.d.ts +1 -1
  58. package/dist/profile/profile.service.d.ts +1 -1
  59. package/dist/session/session.controller.d.ts +1 -1
  60. package/dist/session/session.service.d.ts +3 -3
  61. package/dist/setting/dto/update-setting-list.dto.d.ts +9 -0
  62. package/dist/setting/dto/update-setting-list.dto.d.ts.map +1 -0
  63. package/dist/setting/dto/update-setting-list.dto.js +44 -0
  64. package/dist/setting/dto/update-setting-list.dto.js.map +1 -0
  65. package/dist/setting/setting.controller.d.ts +19 -0
  66. package/dist/setting/setting.controller.d.ts.map +1 -1
  67. package/dist/setting/setting.controller.js +12 -0
  68. package/dist/setting/setting.controller.js.map +1 -1
  69. package/dist/setting/setting.service.d.ts +19 -0
  70. package/dist/setting/setting.service.d.ts.map +1 -1
  71. package/dist/setting/setting.service.js +79 -0
  72. package/dist/setting/setting.service.js.map +1 -1
  73. package/dist/task/task.service.d.ts +12 -0
  74. package/dist/task/task.service.d.ts.map +1 -1
  75. package/dist/task/task.service.js +60 -0
  76. package/dist/task/task.service.js.map +1 -1
  77. package/dist/user/user.controller.d.ts +2 -2
  78. package/dist/user/user.service.d.ts +6 -6
  79. package/dist/webhook-integration/webhook-integration.service.d.ts +4 -0
  80. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  81. package/dist/webhook-integration/webhook-integration.service.js +184 -34
  82. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  83. package/dist/webhook-integration/webhook-utils.d.ts +1 -0
  84. package/dist/webhook-integration/webhook-utils.d.ts.map +1 -1
  85. package/dist/webhook-integration/webhook-utils.js +38 -0
  86. package/dist/webhook-integration/webhook-utils.js.map +1 -1
  87. package/hedhog/data/menu.yaml +48 -4
  88. package/hedhog/data/route.yaml +133 -0
  89. package/hedhog/data/setting_group.yaml +45 -0
  90. package/hedhog/data/setting_subgroup.yaml +16 -0
  91. package/hedhog/frontend/app/account/components/mcp-tokens.tsx.ejs +285 -0
  92. package/hedhog/frontend/app/account/layout.tsx.ejs +32 -55
  93. package/hedhog/frontend/app/account/mcp-tokens/page.tsx.ejs +5 -0
  94. package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +2098 -1835
  95. package/hedhog/frontend/app/mcp_chat/chat-components.tsx.ejs +595 -0
  96. package/hedhog/frontend/app/mcp_chat/chat-types.ts.ejs +16 -0
  97. package/hedhog/frontend/app/mcp_chat/page.tsx.ejs +554 -383
  98. package/hedhog/frontend/app/setting-list/page.tsx.ejs +649 -0
  99. package/hedhog/frontend/app/users/page.tsx.ejs +445 -203
  100. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +16 -37
  101. package/hedhog/frontend/messages/en.json +241 -2
  102. package/hedhog/frontend/messages/pt.json +241 -2
  103. package/hedhog/table/mcp_api_key.yaml +30 -0
  104. package/hedhog/table/notification.yaml +72 -0
  105. package/hedhog/table/webhook_action_log.yaml +49 -0
  106. package/package.json +4 -4
  107. package/src/core.module.ts +3 -0
  108. package/src/index.ts +5 -0
  109. package/src/mcp/guards/mcp-auth.guard.ts +64 -32
  110. package/src/mcp/mcp-api-key.controller.ts +42 -0
  111. package/src/mcp/mcp-api-key.service.ts +66 -0
  112. package/src/mcp/mcp-registry.service.ts +75 -69
  113. package/src/mcp/mcp.module.ts +43 -36
  114. package/src/mcp-chat/mcp-chat.controller.ts +113 -60
  115. package/src/mcp-chat/mcp-chat.service.ts +494 -332
  116. package/src/notification/dto/create-notification.dto.ts +71 -0
  117. package/src/notification/dto/update-progress.dto.ts +16 -0
  118. package/src/notification/notification.controller.ts +71 -0
  119. package/src/notification/notification.module.ts +16 -0
  120. package/src/notification/notification.service.ts +124 -0
  121. package/src/setting/dto/update-setting-list.dto.ts +32 -0
  122. package/src/setting/setting.controller.ts +9 -0
  123. package/src/setting/setting.service.ts +91 -0
  124. package/src/task/task.service.ts +72 -0
  125. package/src/webhook-integration/webhook-integration.service.ts +934 -735
  126. package/src/webhook-integration/webhook-utils.ts +136 -79
@@ -0,0 +1,595 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@/components/ui/button';
4
+ import { ScrollArea } from '@/components/ui/scroll-area';
5
+ import { Spinner } from '@/components/ui/spinner';
6
+ import { Textarea } from '@/components/ui/textarea';
7
+ import { AnimatePresence, motion } from 'framer-motion';
8
+ import {
9
+ ArrowUp,
10
+ Bot,
11
+ ChevronDown,
12
+ ChevronRight,
13
+ Loader2,
14
+ MessageSquare,
15
+ Paperclip,
16
+ Plus,
17
+ Trash2,
18
+ Wrench,
19
+ X,
20
+ } from 'lucide-react';
21
+ import { RefObject, useRef, useState } from 'react';
22
+ import ReactMarkdown from 'react-markdown';
23
+ import remarkGfm from 'remark-gfm';
24
+ import { Conversation, Message } from './chat-types';
25
+
26
+ type MarkdownComponentProps = {
27
+ children?: React.ReactNode;
28
+ className?: string;
29
+ href?: string;
30
+ };
31
+
32
+ const markdownComponents: Record<
33
+ string,
34
+ (props: MarkdownComponentProps) => React.ReactNode
35
+ > = {
36
+ p({ children }) {
37
+ return <p className="mb-2 last:mb-0 leading-relaxed">{children}</p>;
38
+ },
39
+ ul({ children }) {
40
+ return <ul className="mb-2 list-disc pl-4 space-y-1">{children}</ul>;
41
+ },
42
+ ol({ children }) {
43
+ return <ol className="mb-2 list-decimal pl-4 space-y-1">{children}</ol>;
44
+ },
45
+ li({ children }) {
46
+ return <li className="leading-relaxed">{children}</li>;
47
+ },
48
+ strong({ children }) {
49
+ return <strong className="font-semibold">{children}</strong>;
50
+ },
51
+ em({ children }) {
52
+ return <em className="italic">{children}</em>;
53
+ },
54
+ code({ children, className }) {
55
+ const isBlock = className?.includes('language-');
56
+ return isBlock ? (
57
+ <code className="block rounded-md bg-black/10 dark:bg-white/10 px-3 py-2 text-xs font-mono whitespace-pre-wrap my-2">
58
+ {children}
59
+ </code>
60
+ ) : (
61
+ <code className="rounded bg-black/10 dark:bg-white/10 px-1 py-0.5 text-xs font-mono">
62
+ {children}
63
+ </code>
64
+ );
65
+ },
66
+ pre({ children }) {
67
+ return <pre className="my-2 overflow-x-auto rounded-md">{children}</pre>;
68
+ },
69
+ blockquote({ children }) {
70
+ return (
71
+ <blockquote className="border-l-2 border-current/30 pl-3 italic my-2 text-current/70">
72
+ {children}
73
+ </blockquote>
74
+ );
75
+ },
76
+ h1({ children }) {
77
+ return (
78
+ <h1 className="text-base font-bold mb-2 mt-3 first:mt-0">{children}</h1>
79
+ );
80
+ },
81
+ h2({ children }) {
82
+ return (
83
+ <h2 className="text-sm font-bold mb-2 mt-3 first:mt-0">{children}</h2>
84
+ );
85
+ },
86
+ h3({ children }) {
87
+ return (
88
+ <h3 className="text-sm font-semibold mb-1 mt-2 first:mt-0">{children}</h3>
89
+ );
90
+ },
91
+ a({ href, children }) {
92
+ return (
93
+ <a
94
+ href={href}
95
+ target="_blank"
96
+ rel="noopener noreferrer"
97
+ className="underline underline-offset-2 hover:opacity-80"
98
+ >
99
+ {children}
100
+ </a>
101
+ );
102
+ },
103
+ hr() {
104
+ return <hr className="my-3 border-current/20" />;
105
+ },
106
+ table({ children }) {
107
+ return (
108
+ <div className="my-2 overflow-x-auto">
109
+ <table className="text-xs border-collapse w-full">{children}</table>
110
+ </div>
111
+ );
112
+ },
113
+ th({ children }) {
114
+ return (
115
+ <th className="border border-current/20 px-2 py-1 font-semibold text-left bg-current/5">
116
+ {children}
117
+ </th>
118
+ );
119
+ },
120
+ td({ children }) {
121
+ return <td className="border border-current/20 px-2 py-1">{children}</td>;
122
+ },
123
+ };
124
+
125
+ export function ToolBadge({
126
+ message,
127
+ variant,
128
+ }: {
129
+ message: Message;
130
+ variant: 'call' | 'result';
131
+ }) {
132
+ const [expanded, setExpanded] = useState(false);
133
+ let parsed: unknown;
134
+ try {
135
+ parsed = JSON.parse(message.content);
136
+ } catch {
137
+ parsed = message.content;
138
+ }
139
+
140
+ const colorClass =
141
+ variant === 'call'
142
+ ? 'border-violet-200 bg-violet-50 text-violet-700 dark:border-violet-800 dark:bg-violet-950 dark:text-violet-300'
143
+ : 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-300';
144
+
145
+ const preClass =
146
+ variant === 'call'
147
+ ? 'border-violet-100 bg-violet-50/50 text-violet-800 dark:border-violet-900 dark:bg-violet-950/50 dark:text-violet-200'
148
+ : 'border-emerald-100 bg-emerald-50/50 text-emerald-800 dark:border-emerald-900 dark:bg-emerald-950/50 dark:text-emerald-200';
149
+
150
+ return (
151
+ <div className="my-1 flex flex-col items-start">
152
+ <button
153
+ type="button"
154
+ className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium transition-colors hover:opacity-80 cursor-pointer ${colorClass}`}
155
+ onClick={() => setExpanded((v) => !v)}
156
+ >
157
+ <Wrench className="h-3 w-3 shrink-0" />
158
+ <span className="font-mono">{message.tool_name}</span>
159
+ {expanded ? (
160
+ <ChevronDown className="h-3 w-3 shrink-0" />
161
+ ) : (
162
+ <ChevronRight className="h-3 w-3 shrink-0" />
163
+ )}
164
+ </button>
165
+ {expanded && (
166
+ <pre
167
+ className={`mt-1.5 w-full max-w-lg rounded-lg border px-3 py-2 text-xs font-mono overflow-x-auto whitespace-pre-wrap ${preClass}`}
168
+ >
169
+ {typeof parsed === 'string'
170
+ ? parsed
171
+ : JSON.stringify(parsed, null, 2)}
172
+ </pre>
173
+ )}
174
+ </div>
175
+ );
176
+ }
177
+
178
+ export function TypingIndicator() {
179
+ return (
180
+ <div className="flex items-start gap-3">
181
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted ring-2 ring-background">
182
+ <Bot className="h-4 w-4 text-muted-foreground" />
183
+ </div>
184
+ <div className="flex items-center gap-1 rounded-2xl rounded-tl-sm bg-muted px-4 py-3 shadow-sm">
185
+ <span
186
+ className="h-1.5 w-1.5 rounded-full bg-muted-foreground/60 animate-bounce"
187
+ style={{ animationDelay: '0ms' }}
188
+ />
189
+ <span
190
+ className="h-1.5 w-1.5 rounded-full bg-muted-foreground/60 animate-bounce"
191
+ style={{ animationDelay: '150ms' }}
192
+ />
193
+ <span
194
+ className="h-1.5 w-1.5 rounded-full bg-muted-foreground/60 animate-bounce"
195
+ style={{ animationDelay: '300ms' }}
196
+ />
197
+ </div>
198
+ </div>
199
+ );
200
+ }
201
+
202
+ type ConversationListProps = {
203
+ conversations: Conversation[];
204
+ selectedConvId: number | null;
205
+ isLoading: boolean;
206
+ emptyLabel: string;
207
+ onSelect: (id: number) => void;
208
+ onDelete: (conversation: Conversation) => void;
209
+ };
210
+
211
+ export function ConversationList({
212
+ conversations,
213
+ selectedConvId,
214
+ isLoading,
215
+ emptyLabel,
216
+ onSelect,
217
+ onDelete,
218
+ }: ConversationListProps) {
219
+ if (isLoading) {
220
+ return (
221
+ <div className="space-y-2 pt-2 px-2">
222
+ {Array.from({ length: 6 }).map((_, index) => (
223
+ <div
224
+ key={`conv-skeleton-${index}`}
225
+ className="h-9 rounded-xl bg-muted/60 animate-pulse"
226
+ />
227
+ ))}
228
+ </div>
229
+ );
230
+ }
231
+
232
+ if (conversations.length === 0) {
233
+ return (
234
+ <div className="flex flex-col items-center gap-2 py-8 px-4 text-center">
235
+ <MessageSquare className="h-8 w-8 text-muted-foreground/40" />
236
+ <p className="text-xs text-muted-foreground">{emptyLabel}</p>
237
+ </div>
238
+ );
239
+ }
240
+
241
+ return (
242
+ <div className="space-y-0.5 pt-1">
243
+ {conversations.map((conv) => (
244
+ <div
245
+ key={conv.id}
246
+ className={`group flex items-center justify-between rounded-xl px-3 py-2 cursor-pointer transition-colors text-sm ${
247
+ selectedConvId === conv.id
248
+ ? 'bg-primary text-primary-foreground'
249
+ : 'hover:bg-accent text-foreground'
250
+ }`}
251
+ onClick={() => {
252
+ onSelect(conv.id);
253
+ }}
254
+ >
255
+ <div className="flex items-center gap-2 min-w-0 flex-1">
256
+ <MessageSquare className="h-3.5 w-3.5 shrink-0 opacity-60" />
257
+ <span className="truncate text-xs font-medium">
258
+ {conv.title || `#${conv.id}`}
259
+ </span>
260
+ </div>
261
+ <Button
262
+ variant="ghost"
263
+ size="icon"
264
+ className={`h-6 w-6 opacity-100 md:opacity-0 md:group-hover:opacity-100 shrink-0 cursor-pointer transition-opacity ${
265
+ selectedConvId === conv.id
266
+ ? 'hover:bg-primary-foreground/20 text-primary-foreground'
267
+ : 'hover:bg-destructive/10 hover:text-destructive'
268
+ }`}
269
+ onClick={(e) => {
270
+ e.stopPropagation();
271
+ onDelete(conv);
272
+ }}
273
+ >
274
+ <Trash2 className="h-3 w-3" />
275
+ </Button>
276
+ </div>
277
+ ))}
278
+ </div>
279
+ );
280
+ }
281
+
282
+ type ChatInputProps = {
283
+ textareaRef: RefObject<HTMLTextAreaElement | null>;
284
+ submitOnEnter?: boolean;
285
+ input: string;
286
+ isSending: boolean;
287
+ isUploadingFile: boolean;
288
+ attachments: Array<{ id: number; name: string }>;
289
+ placeholder: string;
290
+ sendLabel: string;
291
+ sendingLabel: string;
292
+ attachLabel: string;
293
+ uploadingFileLabel: string;
294
+ removeAttachmentLabel: string;
295
+ onInputChange: (value: string) => void;
296
+ onSend: () => void;
297
+ onUploadFiles: (files: FileList) => void;
298
+ onRemoveAttachment: (id: number) => void;
299
+ };
300
+
301
+ export function ChatInput({
302
+ textareaRef,
303
+ submitOnEnter = true,
304
+ input,
305
+ isSending,
306
+ isUploadingFile,
307
+ attachments,
308
+ placeholder,
309
+ sendLabel,
310
+ sendingLabel,
311
+ attachLabel,
312
+ uploadingFileLabel,
313
+ removeAttachmentLabel,
314
+ onInputChange,
315
+ onSend,
316
+ onUploadFiles,
317
+ onRemoveAttachment,
318
+ }: ChatInputProps) {
319
+ const fileInputRef = useRef<HTMLInputElement>(null);
320
+ const [isDraggingFile, setIsDraggingFile] = useState(false);
321
+
322
+ const handleOpenFilePicker = () => {
323
+ if (fileInputRef.current) {
324
+ fileInputRef.current.value = '';
325
+ fileInputRef.current.click();
326
+ }
327
+ };
328
+
329
+ const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
330
+ event.preventDefault();
331
+ event.stopPropagation();
332
+ setIsDraggingFile(true);
333
+ };
334
+
335
+ const handleDragLeave = (event: React.DragEvent<HTMLDivElement>) => {
336
+ event.preventDefault();
337
+ event.stopPropagation();
338
+ setIsDraggingFile(false);
339
+ };
340
+
341
+ const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
342
+ event.preventDefault();
343
+ event.stopPropagation();
344
+ setIsDraggingFile(false);
345
+
346
+ const files = event.dataTransfer.files;
347
+ if (files && files.length > 0) {
348
+ onUploadFiles(files);
349
+ }
350
+ };
351
+
352
+ return (
353
+ <div
354
+ className={`relative rounded-[28px] border px-2.5 md:px-3 py-2.5 shadow-sm transition-colors duration-200 ${
355
+ isDraggingFile
356
+ ? 'border-primary/60 bg-primary/5'
357
+ : 'border-border/70 bg-muted/45'
358
+ }`}
359
+ onDragOver={handleDragOver}
360
+ onDragLeave={handleDragLeave}
361
+ onDrop={handleDrop}
362
+ >
363
+ <input
364
+ ref={fileInputRef}
365
+ type="file"
366
+ className="hidden"
367
+ multiple
368
+ onChange={(event) => {
369
+ const files = event.target.files;
370
+ if (files && files.length > 0) {
371
+ onUploadFiles(files);
372
+ }
373
+ }}
374
+ />
375
+
376
+ <AnimatePresence>
377
+ {attachments.length > 0 && (
378
+ <motion.div
379
+ className="mb-2 flex flex-wrap gap-1.5 px-1"
380
+ initial={{ opacity: 0, y: -6 }}
381
+ animate={{ opacity: 1, y: 0 }}
382
+ exit={{ opacity: 0, y: -6 }}
383
+ transition={{ duration: 0.18, ease: 'easeOut' }}
384
+ >
385
+ <AnimatePresence initial={false}>
386
+ {attachments.map((attachment) => (
387
+ <motion.div
388
+ key={attachment.id}
389
+ layout
390
+ initial={{ opacity: 0, y: -8, scale: 0.94 }}
391
+ animate={{ opacity: 1, y: 0, scale: 1 }}
392
+ exit={{ opacity: 0, y: -8, scale: 0.94 }}
393
+ transition={{ duration: 0.18, ease: 'easeOut' }}
394
+ className="inline-flex items-center gap-1 rounded-full border border-border/70 bg-background/75 px-2.5 py-1.5 text-[11px]"
395
+ >
396
+ <Paperclip className="h-3 w-3" />
397
+ <span className="max-w-45 truncate" title={attachment.name}>
398
+ {attachment.name}
399
+ </span>
400
+ <button
401
+ type="button"
402
+ className="rounded-full p-0.5 hover:bg-muted cursor-pointer"
403
+ onClick={() => onRemoveAttachment(attachment.id)}
404
+ aria-label={`${removeAttachmentLabel}: ${attachment.name}`}
405
+ >
406
+ <X className="h-3 w-3" />
407
+ </button>
408
+ </motion.div>
409
+ ))}
410
+ </AnimatePresence>
411
+ </motion.div>
412
+ )}
413
+ </AnimatePresence>
414
+
415
+ <div className="flex items-center gap-2">
416
+ <Button
417
+ type="button"
418
+ size="icon"
419
+ variant="ghost"
420
+ className="shrink-0 h-10 w-10 rounded-full cursor-pointer border border-border/60 bg-background/85 hover:bg-background"
421
+ aria-label={attachLabel}
422
+ onClick={handleOpenFilePicker}
423
+ disabled={isSending || isUploadingFile}
424
+ >
425
+ {isUploadingFile ? (
426
+ <Loader2 className="h-4 w-4 animate-spin" />
427
+ ) : (
428
+ <Plus className="h-4 w-4" />
429
+ )}
430
+ </Button>
431
+
432
+ <Textarea
433
+ ref={textareaRef}
434
+ className="resize-none flex-1 border-0! bg-transparent! shadow-none! focus-visible:ring-0! px-2.5 py-2.5 min-h-10 max-h-40 md:max-h-75 overflow-y-auto text-sm text-foreground leading-5 placeholder:text-sm placeholder:leading-5 placeholder:text-muted-foreground transition-[height] duration-150 ease-out"
435
+ placeholder={isUploadingFile ? uploadingFileLabel : placeholder}
436
+ value={input}
437
+ disabled={isSending}
438
+ onChange={(e) => onInputChange(e.target.value)}
439
+ onKeyDown={(e) => {
440
+ if (submitOnEnter && e.key === 'Enter' && !e.shiftKey) {
441
+ e.preventDefault();
442
+ onSend();
443
+ }
444
+ }}
445
+ rows={1}
446
+ />
447
+
448
+ <Button
449
+ size="icon"
450
+ className="shrink-0 h-10 w-10 rounded-full cursor-pointer"
451
+ aria-label={isSending ? sendingLabel : sendLabel}
452
+ disabled={isSending || (!input.trim() && attachments.length === 0)}
453
+ onClick={onSend}
454
+ >
455
+ {isSending ? (
456
+ <Spinner className="h-4 w-4" aria-label={sendingLabel} />
457
+ ) : (
458
+ <ArrowUp className="h-4 w-4" />
459
+ )}
460
+ </Button>
461
+ </div>
462
+
463
+ <AnimatePresence>
464
+ {isDraggingFile && (
465
+ <motion.div
466
+ className="pointer-events-none absolute inset-0 rounded-[28px] border-2 border-dashed border-primary/45"
467
+ initial={{ opacity: 0 }}
468
+ animate={{ opacity: 1 }}
469
+ exit={{ opacity: 0 }}
470
+ transition={{ duration: 0.15 }}
471
+ />
472
+ )}
473
+ </AnimatePresence>
474
+ </div>
475
+ );
476
+ }
477
+
478
+ type MessageFeedProps = {
479
+ messages: Message[];
480
+ isLoadingMessages: boolean;
481
+ isSending: boolean;
482
+ isStreamingResponse: boolean;
483
+ messagesEndRef: RefObject<HTMLDivElement | null>;
484
+ };
485
+
486
+ export function MessageFeed({
487
+ messages,
488
+ isLoadingMessages,
489
+ isSending,
490
+ isStreamingResponse,
491
+ messagesEndRef,
492
+ }: MessageFeedProps) {
493
+ return (
494
+ <div className="flex flex-col gap-4 max-w-3xl mx-auto">
495
+ {isLoadingMessages && (
496
+ <div className="space-y-3">
497
+ {Array.from({ length: 3 }).map((_, index) => (
498
+ <div
499
+ key={`msg-skeleton-${index}`}
500
+ className="h-14 rounded-2xl bg-muted/60 animate-pulse"
501
+ />
502
+ ))}
503
+ </div>
504
+ )}
505
+ {messages.map((msg, idx) => {
506
+ if (msg.role === 'tool_call') {
507
+ return <ToolBadge key={msg.id ?? idx} message={msg} variant="call" />;
508
+ }
509
+ if (msg.role === 'tool_result') {
510
+ return (
511
+ <ToolBadge key={msg.id ?? idx} message={msg} variant="result" />
512
+ );
513
+ }
514
+ if (msg.role === 'user') {
515
+ return (
516
+ <div key={msg.id ?? idx} className="flex justify-end">
517
+ <div className="max-w-[90%] whitespace-pre-wrap wrap-break-word rounded-2xl rounded-tr-md bg-primary px-4 py-2.5 text-sm text-primary-foreground shadow-sm md:max-w-[75%]">
518
+ {msg.content}
519
+ </div>
520
+ </div>
521
+ );
522
+ }
523
+ return (
524
+ <div key={msg.id ?? idx} className="flex items-start gap-3">
525
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted ring-2 ring-background shadow-sm">
526
+ <Bot className="h-4 w-4 text-muted-foreground" />
527
+ </div>
528
+ <div className="max-w-[90%] wrap-break-word rounded-2xl rounded-tl-md bg-muted px-4 py-2.5 text-sm shadow-sm prose-sm md:max-w-[75%]">
529
+ <ReactMarkdown
530
+ remarkPlugins={[remarkGfm]}
531
+ components={markdownComponents as never}
532
+ >
533
+ {msg.content}
534
+ </ReactMarkdown>
535
+ </div>
536
+ </div>
537
+ );
538
+ })}
539
+ {isSending && !isStreamingResponse && <TypingIndicator />}
540
+ <div ref={messagesEndRef} />
541
+ </div>
542
+ );
543
+ }
544
+
545
+ export function ConversationSidebar({
546
+ conversations,
547
+ selectedConvId,
548
+ isLoading,
549
+ emptyLabel,
550
+ conversationsLabel,
551
+ newConversationLabel,
552
+ onCreate,
553
+ onSelect,
554
+ onDelete,
555
+ }: {
556
+ conversations: Conversation[];
557
+ selectedConvId: number | null;
558
+ isLoading: boolean;
559
+ emptyLabel: string;
560
+ conversationsLabel: string;
561
+ newConversationLabel: string;
562
+ onCreate: () => void;
563
+ onSelect: (id: number) => void;
564
+ onDelete: (conversation: Conversation) => void;
565
+ }) {
566
+ return (
567
+ <>
568
+ <div className="p-3 border-b bg-background/50">
569
+ <Button
570
+ variant="outline"
571
+ className="w-full justify-start gap-2 cursor-pointer rounded-xl h-9 text-sm font-medium"
572
+ onClick={onCreate}
573
+ >
574
+ <Plus className="h-4 w-4" />
575
+ {newConversationLabel}
576
+ </Button>
577
+ </div>
578
+ <div className="px-4 pt-4 pb-1.5">
579
+ <p className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
580
+ {conversationsLabel}
581
+ </p>
582
+ </div>
583
+ <ScrollArea className="flex-1 px-2 pb-2">
584
+ <ConversationList
585
+ conversations={conversations}
586
+ selectedConvId={selectedConvId}
587
+ isLoading={isLoading}
588
+ emptyLabel={emptyLabel}
589
+ onSelect={onSelect}
590
+ onDelete={onDelete}
591
+ />
592
+ </ScrollArea>
593
+ </>
594
+ );
595
+ }
@@ -0,0 +1,16 @@
1
+ export type Conversation = {
2
+ id: number;
3
+ title: string | null;
4
+ created_at: string;
5
+ updated_at: string;
6
+ };
7
+
8
+ export type Message = {
9
+ id: number;
10
+ conversation_id: number;
11
+ role: 'user' | 'assistant' | 'tool_call' | 'tool_result';
12
+ content: string;
13
+ tool_name: string | null;
14
+ tool_call_id: string | null;
15
+ created_at: string;
16
+ };