@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.
- package/dist/auth/auth.controller.d.ts +3 -3
- package/dist/auth/auth.service.d.ts +8 -8
- package/dist/core.module.d.ts.map +1 -1
- package/dist/core.module.js +3 -0
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/guards/mcp-auth.guard.d.ts +6 -1
- package/dist/mcp/guards/mcp-auth.guard.d.ts.map +1 -1
- package/dist/mcp/guards/mcp-auth.guard.js +29 -2
- package/dist/mcp/guards/mcp-auth.guard.js.map +1 -1
- package/dist/mcp/mcp-api-key.controller.d.ts +32 -0
- package/dist/mcp/mcp-api-key.controller.d.ts.map +1 -0
- package/dist/mcp/mcp-api-key.controller.js +64 -0
- package/dist/mcp/mcp-api-key.controller.js.map +1 -0
- package/dist/mcp/mcp-api-key.service.d.ts +27 -0
- package/dist/mcp/mcp-api-key.service.d.ts.map +1 -0
- package/dist/mcp/mcp-api-key.service.js +74 -0
- package/dist/mcp/mcp-api-key.service.js.map +1 -0
- package/dist/mcp/mcp-registry.service.d.ts +1 -2
- package/dist/mcp/mcp-registry.service.d.ts.map +1 -1
- package/dist/mcp/mcp-registry.service.js +6 -2
- package/dist/mcp/mcp-registry.service.js.map +1 -1
- package/dist/mcp/mcp.module.d.ts.map +1 -1
- package/dist/mcp/mcp.module.js +8 -1
- package/dist/mcp/mcp.module.js.map +1 -1
- package/dist/mcp-chat/mcp-chat.controller.d.ts +4 -0
- package/dist/mcp-chat/mcp-chat.controller.d.ts.map +1 -1
- package/dist/mcp-chat/mcp-chat.controller.js +46 -0
- package/dist/mcp-chat/mcp-chat.controller.js.map +1 -1
- package/dist/mcp-chat/mcp-chat.service.d.ts +6 -1
- package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -1
- package/dist/mcp-chat/mcp-chat.service.js +125 -17
- package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
- package/dist/notification/dto/create-notification.dto.d.ts +26 -0
- package/dist/notification/dto/create-notification.dto.d.ts.map +1 -0
- package/dist/notification/dto/create-notification.dto.js +86 -0
- package/dist/notification/dto/create-notification.dto.js.map +1 -0
- package/dist/notification/dto/update-progress.dto.d.ts +6 -0
- package/dist/notification/dto/update-progress.dto.d.ts.map +1 -0
- package/dist/notification/dto/update-progress.dto.js +33 -0
- package/dist/notification/dto/update-progress.dto.js.map +1 -0
- package/dist/notification/notification.controller.d.ts +108 -0
- package/dist/notification/notification.controller.d.ts.map +1 -0
- package/dist/notification/notification.controller.js +118 -0
- package/dist/notification/notification.controller.js.map +1 -0
- package/dist/notification/notification.module.d.ts +3 -0
- package/dist/notification/notification.module.d.ts.map +1 -0
- package/dist/notification/notification.module.js +29 -0
- package/dist/notification/notification.module.js.map +1 -0
- package/dist/notification/notification.service.d.ts +98 -0
- package/dist/notification/notification.service.d.ts.map +1 -0
- package/dist/notification/notification.service.js +123 -0
- package/dist/notification/notification.service.js.map +1 -0
- package/dist/profile/profile.controller.d.ts +1 -1
- package/dist/profile/profile.service.d.ts +1 -1
- package/dist/session/session.controller.d.ts +1 -1
- package/dist/session/session.service.d.ts +3 -3
- package/dist/setting/dto/update-setting-list.dto.d.ts +9 -0
- package/dist/setting/dto/update-setting-list.dto.d.ts.map +1 -0
- package/dist/setting/dto/update-setting-list.dto.js +44 -0
- package/dist/setting/dto/update-setting-list.dto.js.map +1 -0
- package/dist/setting/setting.controller.d.ts +19 -0
- package/dist/setting/setting.controller.d.ts.map +1 -1
- package/dist/setting/setting.controller.js +12 -0
- package/dist/setting/setting.controller.js.map +1 -1
- package/dist/setting/setting.service.d.ts +19 -0
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +79 -0
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/task/task.service.d.ts +12 -0
- package/dist/task/task.service.d.ts.map +1 -1
- package/dist/task/task.service.js +60 -0
- package/dist/task/task.service.js.map +1 -1
- package/dist/user/user.controller.d.ts +2 -2
- package/dist/user/user.service.d.ts +6 -6
- package/dist/webhook-integration/webhook-integration.service.d.ts +4 -0
- package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.js +184 -34
- package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
- package/dist/webhook-integration/webhook-utils.d.ts +1 -0
- package/dist/webhook-integration/webhook-utils.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-utils.js +38 -0
- package/dist/webhook-integration/webhook-utils.js.map +1 -1
- package/hedhog/data/menu.yaml +48 -4
- package/hedhog/data/route.yaml +133 -0
- package/hedhog/data/setting_group.yaml +45 -0
- package/hedhog/data/setting_subgroup.yaml +16 -0
- package/hedhog/frontend/app/account/components/mcp-tokens.tsx.ejs +285 -0
- package/hedhog/frontend/app/account/layout.tsx.ejs +32 -55
- package/hedhog/frontend/app/account/mcp-tokens/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +2098 -1835
- package/hedhog/frontend/app/mcp_chat/chat-components.tsx.ejs +595 -0
- package/hedhog/frontend/app/mcp_chat/chat-types.ts.ejs +16 -0
- package/hedhog/frontend/app/mcp_chat/page.tsx.ejs +554 -383
- package/hedhog/frontend/app/setting-list/page.tsx.ejs +649 -0
- package/hedhog/frontend/app/users/page.tsx.ejs +445 -203
- package/hedhog/frontend/app/users/user-sheet.tsx.ejs +16 -37
- package/hedhog/frontend/messages/en.json +241 -2
- package/hedhog/frontend/messages/pt.json +241 -2
- package/hedhog/table/mcp_api_key.yaml +30 -0
- package/hedhog/table/notification.yaml +72 -0
- package/hedhog/table/webhook_action_log.yaml +49 -0
- package/package.json +4 -4
- package/src/core.module.ts +3 -0
- package/src/index.ts +5 -0
- package/src/mcp/guards/mcp-auth.guard.ts +64 -32
- package/src/mcp/mcp-api-key.controller.ts +42 -0
- package/src/mcp/mcp-api-key.service.ts +66 -0
- package/src/mcp/mcp-registry.service.ts +75 -69
- package/src/mcp/mcp.module.ts +43 -36
- package/src/mcp-chat/mcp-chat.controller.ts +113 -60
- package/src/mcp-chat/mcp-chat.service.ts +494 -332
- package/src/notification/dto/create-notification.dto.ts +71 -0
- package/src/notification/dto/update-progress.dto.ts +16 -0
- package/src/notification/notification.controller.ts +71 -0
- package/src/notification/notification.module.ts +16 -0
- package/src/notification/notification.service.ts +124 -0
- package/src/setting/dto/update-setting-list.dto.ts +32 -0
- package/src/setting/setting.controller.ts +9 -0
- package/src/setting/setting.service.ts +91 -0
- package/src/task/task.service.ts +72 -0
- package/src/webhook-integration/webhook-integration.service.ts +934 -735
- 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
|
+
};
|