@hed-hog/core 0.0.223 → 0.0.233

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 (63) hide show
  1. package/dist/ai/ai.controller.d.ts +90 -0
  2. package/dist/ai/ai.controller.d.ts.map +1 -0
  3. package/dist/ai/ai.controller.js +124 -0
  4. package/dist/ai/ai.controller.js.map +1 -0
  5. package/dist/ai/ai.module.d.ts +3 -0
  6. package/dist/ai/ai.module.d.ts.map +1 -0
  7. package/dist/ai/ai.module.js +31 -0
  8. package/dist/ai/ai.module.js.map +1 -0
  9. package/dist/ai/ai.service.d.ts +76 -0
  10. package/dist/ai/ai.service.d.ts.map +1 -0
  11. package/dist/ai/ai.service.js +467 -0
  12. package/dist/ai/ai.service.js.map +1 -0
  13. package/dist/ai/dto/chat-agent.dto.d.ts +5 -0
  14. package/dist/ai/dto/chat-agent.dto.d.ts.map +1 -0
  15. package/dist/ai/dto/chat-agent.dto.js +29 -0
  16. package/dist/ai/dto/chat-agent.dto.js.map +1 -0
  17. package/dist/ai/dto/chat.dto.d.ts +8 -0
  18. package/dist/ai/dto/chat.dto.d.ts.map +1 -0
  19. package/dist/ai/dto/chat.dto.js +45 -0
  20. package/dist/ai/dto/chat.dto.js.map +1 -0
  21. package/dist/ai/dto/create-agent.dto.d.ts +7 -0
  22. package/dist/ai/dto/create-agent.dto.d.ts.map +1 -0
  23. package/dist/ai/dto/create-agent.dto.js +38 -0
  24. package/dist/ai/dto/create-agent.dto.js.map +1 -0
  25. package/dist/ai/dto/update-agent.dto.d.ts +7 -0
  26. package/dist/ai/dto/update-agent.dto.d.ts.map +1 -0
  27. package/dist/ai/dto/update-agent.dto.js +38 -0
  28. package/dist/ai/dto/update-agent.dto.js.map +1 -0
  29. package/dist/auth/auth.controller.d.ts +1 -1
  30. package/dist/auth/auth.service.d.ts +1 -1
  31. package/dist/core.module.d.ts.map +1 -1
  32. package/dist/core.module.js +3 -0
  33. package/dist/core.module.js.map +1 -1
  34. package/dist/dashboard/dashboard-user/dashboard-user.controller.d.ts +3 -3
  35. package/dist/dashboard/dashboard-user/dashboard-user.service.d.ts +3 -3
  36. package/dist/file/file.service.d.ts +2 -0
  37. package/dist/file/file.service.d.ts.map +1 -1
  38. package/dist/file/file.service.js +17 -2
  39. package/dist/file/file.service.js.map +1 -1
  40. package/dist/index.d.ts +2 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +2 -0
  43. package/dist/index.js.map +1 -1
  44. package/dist/menu/menu.controller.d.ts +3 -3
  45. package/dist/menu/menu.service.d.ts +3 -3
  46. package/dist/user/user.controller.d.ts +2 -2
  47. package/dist/user/user.service.d.ts +3 -3
  48. package/hedhog/data/menu.yaml +16 -2
  49. package/hedhog/data/route.yaml +64 -0
  50. package/hedhog/data/setting_group.yaml +33 -1
  51. package/hedhog/frontend/app/ai_agent/page.tsx.ejs +465 -0
  52. package/hedhog/table/ai_agent.yaml +24 -0
  53. package/package.json +3 -3
  54. package/src/ai/ai.controller.ts +68 -0
  55. package/src/ai/ai.module.ts +18 -0
  56. package/src/ai/ai.service.ts +646 -0
  57. package/src/ai/dto/chat-agent.dto.ts +13 -0
  58. package/src/ai/dto/chat.dto.ts +26 -0
  59. package/src/ai/dto/create-agent.dto.ts +20 -0
  60. package/src/ai/dto/update-agent.dto.ts +20 -0
  61. package/src/core.module.ts +3 -0
  62. package/src/file/file.service.ts +34 -8
  63. package/src/index.ts +3 -0
@@ -0,0 +1,465 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Page,
5
+ PageHeader,
6
+ PaginationFooter,
7
+ SearchBar,
8
+ } from '@/components/entity-list';
9
+ import {
10
+ AlertDialog,
11
+ AlertDialogAction,
12
+ AlertDialogCancel,
13
+ AlertDialogContent,
14
+ AlertDialogDescription,
15
+ AlertDialogFooter,
16
+ AlertDialogHeader,
17
+ AlertDialogTitle,
18
+ } from '@/components/ui/alert-dialog';
19
+ import { Button } from '@/components/ui/button';
20
+ import { Card, CardContent } from '@/components/ui/card';
21
+ import {
22
+ Form,
23
+ FormControl,
24
+ FormField,
25
+ FormItem,
26
+ FormLabel,
27
+ FormMessage,
28
+ } from '@/components/ui/form';
29
+ import { Input } from '@/components/ui/input';
30
+ import {
31
+ Select,
32
+ SelectContent,
33
+ SelectItem,
34
+ SelectTrigger,
35
+ SelectValue,
36
+ } from '@/components/ui/select';
37
+ import {
38
+ Sheet,
39
+ SheetContent,
40
+ SheetDescription,
41
+ SheetFooter,
42
+ SheetHeader,
43
+ SheetTitle,
44
+ } from '@/components/ui/sheet';
45
+ import {
46
+ Table,
47
+ TableBody,
48
+ TableCell,
49
+ TableHead,
50
+ TableHeader,
51
+ TableRow,
52
+ } from '@/components/ui/table';
53
+ import { Textarea } from '@/components/ui/textarea';
54
+ import { useDebounce } from '@/hooks/use-debounce';
55
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
56
+ import { zodResolver } from '@hookform/resolvers/zod';
57
+ import { Bot, Pencil, Plus, Trash2 } from 'lucide-react';
58
+ import { useTranslations } from 'next-intl';
59
+ import { useEffect, useState } from 'react';
60
+ import { useForm } from 'react-hook-form';
61
+ import { toast } from 'sonner';
62
+ import { z } from 'zod';
63
+
64
+ type Agent = {
65
+ id: number;
66
+ slug: string;
67
+ provider: 'openai' | 'gemini';
68
+ model: string | null;
69
+ instructions: string | null;
70
+ external_agent_id: string | null;
71
+ created_at: string;
72
+ updated_at: string;
73
+ };
74
+
75
+ type PaginatedResponse<T> = {
76
+ data: T[];
77
+ total: number;
78
+ page: number;
79
+ pageSize: number;
80
+ totalPages: number;
81
+ };
82
+
83
+ type FormValues = {
84
+ slug: string;
85
+ provider: 'openai' | 'gemini';
86
+ model?: string;
87
+ instructions?: string;
88
+ };
89
+
90
+ export default function AiAgentPage() {
91
+ const { request } = useApp();
92
+ const t = useTranslations('core.AiAgentPage');
93
+ const formSchema = z.object({
94
+ slug: z.string().min(2, t('validationSlugMinLength')),
95
+ provider: z.enum(['openai', 'gemini']),
96
+ model: z.string().optional(),
97
+ instructions: z.string().optional(),
98
+ });
99
+ const [page, setPage] = useState(1);
100
+ const [pageSize, setPageSize] = useState(10);
101
+ const [searchQuery, setSearchQuery] = useState('');
102
+ const debouncedSearch = useDebounce(searchQuery, 400);
103
+
104
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
105
+ const [editingAgent, setEditingAgent] = useState<Agent | null>(null);
106
+ const [agentToDelete, setAgentToDelete] = useState<Agent | null>(null);
107
+
108
+ const form = useForm<FormValues>({
109
+ resolver: zodResolver(formSchema),
110
+ defaultValues: {
111
+ slug: '',
112
+ provider: 'openai',
113
+ model: '',
114
+ instructions: '',
115
+ },
116
+ });
117
+
118
+ const { data, isLoading, refetch } = useQuery<PaginatedResponse<Agent>>({
119
+ queryKey: ['ai-agents', page, pageSize, debouncedSearch],
120
+ queryFn: async () => {
121
+ const params = new URLSearchParams();
122
+ params.set('page', String(page));
123
+ params.set('pageSize', String(pageSize));
124
+ if (debouncedSearch) params.set('search', debouncedSearch);
125
+
126
+ const response = await request<PaginatedResponse<Agent>>({
127
+ url: `/ai/agent?${params.toString()}`,
128
+ method: 'GET',
129
+ });
130
+
131
+ return response.data;
132
+ },
133
+ initialData: {
134
+ data: [],
135
+ total: 0,
136
+ page: 1,
137
+ pageSize: 10,
138
+ totalPages: 1,
139
+ },
140
+ });
141
+
142
+ useEffect(() => {
143
+ if (editingAgent) {
144
+ form.reset({
145
+ slug: editingAgent.slug,
146
+ provider: editingAgent.provider,
147
+ model: editingAgent.model || '',
148
+ instructions: editingAgent.instructions || '',
149
+ });
150
+ return;
151
+ }
152
+
153
+ form.reset({
154
+ slug: '',
155
+ provider: 'openai',
156
+ model: '',
157
+ instructions: '',
158
+ });
159
+ }, [editingAgent, form]);
160
+
161
+ const handleOpenCreate = () => {
162
+ setEditingAgent(null);
163
+ setIsDialogOpen(true);
164
+ };
165
+
166
+ const handleOpenEdit = (agent: Agent) => {
167
+ setEditingAgent(agent);
168
+ setIsDialogOpen(true);
169
+ };
170
+
171
+ const onSubmit = async (values: FormValues) => {
172
+ try {
173
+ if (editingAgent) {
174
+ await request({
175
+ url: `/ai/agent/${editingAgent.id}`,
176
+ method: 'PATCH',
177
+ data: {
178
+ slug: values.slug,
179
+ provider: values.provider,
180
+ model: values.model || undefined,
181
+ instructions: values.instructions || undefined,
182
+ },
183
+ });
184
+ toast.success(t('toastUpdatedSuccess'));
185
+ } else {
186
+ await request({
187
+ url: '/ai/agent',
188
+ method: 'POST',
189
+ data: {
190
+ slug: values.slug,
191
+ provider: values.provider,
192
+ model: values.model || undefined,
193
+ instructions: values.instructions || undefined,
194
+ },
195
+ });
196
+ toast.success(t('toastCreatedSuccess'));
197
+ }
198
+
199
+ setIsDialogOpen(false);
200
+ setEditingAgent(null);
201
+ await refetch();
202
+ } catch (error: any) {
203
+ const message =
204
+ error?.response?.data?.message ||
205
+ error?.response?.data?.error ||
206
+ error?.message ||
207
+ t('toastSaveError');
208
+ toast.error(String(message));
209
+ }
210
+ };
211
+
212
+ const handleDelete = async () => {
213
+ if (!agentToDelete) return;
214
+
215
+ try {
216
+ await request({
217
+ url: '/ai/agent',
218
+ method: 'DELETE',
219
+ data: { ids: [agentToDelete.id] },
220
+ });
221
+
222
+ toast.success(t('toastDeletedSuccess'));
223
+ setAgentToDelete(null);
224
+ await refetch();
225
+ } catch (error: any) {
226
+ const message =
227
+ error?.response?.data?.message ||
228
+ error?.response?.data?.error ||
229
+ error?.message ||
230
+ t('toastDeleteError');
231
+ toast.error(String(message));
232
+ }
233
+ };
234
+
235
+ return (
236
+ <Page>
237
+ <PageHeader
238
+ breadcrumbs={[
239
+ { label: t('breadcrumbDashboard'), href: '/' },
240
+ { label: t('breadcrumbManagement'), href: '/core/management' },
241
+ { label: t('breadcrumbCurrent') },
242
+ ]}
243
+ title={t('title')}
244
+ description={t('description')}
245
+ actions={[
246
+ {
247
+ label: t('newAgent'),
248
+ onClick: handleOpenCreate,
249
+ icon: <Plus className="h-4 w-4" />,
250
+ },
251
+ ]}
252
+ />
253
+
254
+ <SearchBar
255
+ searchQuery={searchQuery}
256
+ onSearchChange={(value) => {
257
+ setSearchQuery(value);
258
+ setPage(1);
259
+ }}
260
+ onSearch={() => setPage(1)}
261
+ placeholder={t('searchPlaceholder')}
262
+ />
263
+
264
+ <Card>
265
+ <CardContent className="p-0">
266
+ <Table>
267
+ <TableHeader>
268
+ <TableRow>
269
+ <TableHead>Slug</TableHead>
270
+ <TableHead>{t('columnProvider')}</TableHead>
271
+ <TableHead>{t('columnModel')}</TableHead>
272
+ <TableHead>{t('columnExternalId')}</TableHead>
273
+ <TableHead className="text-right">
274
+ {t('columnActions')}
275
+ </TableHead>
276
+ </TableRow>
277
+ </TableHeader>
278
+ <TableBody>
279
+ {isLoading ? (
280
+ <TableRow>
281
+ <TableCell colSpan={5}>{t('loading')}</TableCell>
282
+ </TableRow>
283
+ ) : data.data.length === 0 ? (
284
+ <TableRow>
285
+ <TableCell colSpan={5}>{t('empty')}</TableCell>
286
+ </TableRow>
287
+ ) : (
288
+ data.data.map((agent) => (
289
+ <TableRow key={agent.id}>
290
+ <TableCell>{agent.slug}</TableCell>
291
+ <TableCell className="uppercase">
292
+ {agent.provider}
293
+ </TableCell>
294
+ <TableCell>{agent.model || '-'}</TableCell>
295
+ <TableCell>{agent.external_agent_id || '-'}</TableCell>
296
+ <TableCell className="text-right">
297
+ <div className="flex items-center justify-end gap-2">
298
+ <Button
299
+ variant="outline"
300
+ size="icon"
301
+ onClick={() => handleOpenEdit(agent)}
302
+ >
303
+ <Pencil className="h-4 w-4" />
304
+ </Button>
305
+ <Button
306
+ variant="destructive"
307
+ size="icon"
308
+ onClick={() => setAgentToDelete(agent)}
309
+ >
310
+ <Trash2 className="h-4 w-4" />
311
+ </Button>
312
+ </div>
313
+ </TableCell>
314
+ </TableRow>
315
+ ))
316
+ )}
317
+ </TableBody>
318
+ </Table>
319
+ </CardContent>
320
+ </Card>
321
+
322
+ <PaginationFooter
323
+ currentPage={page}
324
+ pageSize={pageSize}
325
+ totalItems={data.total}
326
+ onPageChange={setPage}
327
+ onPageSizeChange={(value) => {
328
+ setPageSize(value);
329
+ setPage(1);
330
+ }}
331
+ />
332
+
333
+ <Sheet open={isDialogOpen} onOpenChange={setIsDialogOpen}>
334
+ <SheetContent side="right" className="sm:max-w-xl overflow-y-auto">
335
+ <SheetHeader>
336
+ <SheetTitle>
337
+ {editingAgent ? t('editTitle') : t('createTitle')}
338
+ </SheetTitle>
339
+ <SheetDescription>{t('dialogDescription')}</SheetDescription>
340
+ </SheetHeader>
341
+
342
+ <Form {...form}>
343
+ <form
344
+ className="flex flex-col px-4 gap-4"
345
+ onSubmit={form.handleSubmit(onSubmit)}
346
+ >
347
+ <FormField
348
+ control={form.control}
349
+ name="slug"
350
+ render={({ field }) => (
351
+ <FormItem>
352
+ <FormLabel>{t('fieldSlug')}</FormLabel>
353
+ <FormControl>
354
+ <Input
355
+ placeholder={t('fieldSlugPlaceholder')}
356
+ {...field}
357
+ />
358
+ </FormControl>
359
+ <FormMessage />
360
+ </FormItem>
361
+ )}
362
+ />
363
+
364
+ <FormField
365
+ control={form.control}
366
+ name="provider"
367
+ render={({ field }) => (
368
+ <FormItem>
369
+ <FormLabel>{t('fieldProvider')}</FormLabel>
370
+ <FormControl>
371
+ <Select
372
+ value={field.value}
373
+ onValueChange={field.onChange}
374
+ >
375
+ <SelectTrigger className="w-full">
376
+ <SelectValue
377
+ placeholder={t('fieldProviderPlaceholder')}
378
+ />
379
+ </SelectTrigger>
380
+ <SelectContent>
381
+ <SelectItem value="openai">
382
+ {t('providerOpenai')}
383
+ </SelectItem>
384
+ <SelectItem value="gemini">
385
+ {t('providerGemini')}
386
+ </SelectItem>
387
+ </SelectContent>
388
+ </Select>
389
+ </FormControl>
390
+ <FormMessage />
391
+ </FormItem>
392
+ )}
393
+ />
394
+
395
+ <FormField
396
+ control={form.control}
397
+ name="model"
398
+ render={({ field }) => (
399
+ <FormItem>
400
+ <FormLabel>{t('fieldModel')}</FormLabel>
401
+ <FormControl>
402
+ <Input
403
+ placeholder={t('fieldModelPlaceholder')}
404
+ {...field}
405
+ />
406
+ </FormControl>
407
+ <FormMessage />
408
+ </FormItem>
409
+ )}
410
+ />
411
+
412
+ <FormField
413
+ control={form.control}
414
+ name="instructions"
415
+ render={({ field }) => (
416
+ <FormItem>
417
+ <FormLabel>{t('fieldInstructions')}</FormLabel>
418
+ <FormControl>
419
+ <Textarea
420
+ placeholder={t('fieldInstructionsPlaceholder')}
421
+ rows={10}
422
+ className="min-h-24"
423
+ {...field}
424
+ />
425
+ </FormControl>
426
+ <FormMessage />
427
+ </FormItem>
428
+ )}
429
+ />
430
+
431
+ <SheetFooter className="p-0">
432
+ <Button type="submit">
433
+ <Bot className="h-4 w-4" />
434
+ {editingAgent ? t('saveChanges') : t('createAgent')}
435
+ </Button>
436
+ </SheetFooter>
437
+ </form>
438
+ </Form>
439
+ </SheetContent>
440
+ </Sheet>
441
+
442
+ <AlertDialog
443
+ open={Boolean(agentToDelete)}
444
+ onOpenChange={(open) => {
445
+ if (!open) setAgentToDelete(null);
446
+ }}
447
+ >
448
+ <AlertDialogContent>
449
+ <AlertDialogHeader>
450
+ <AlertDialogTitle>{t('deleteTitle')}</AlertDialogTitle>
451
+ <AlertDialogDescription>
452
+ {t('deleteDescription')}
453
+ </AlertDialogDescription>
454
+ </AlertDialogHeader>
455
+ <AlertDialogFooter>
456
+ <AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
457
+ <AlertDialogAction onClick={handleDelete}>
458
+ {t('delete')}
459
+ </AlertDialogAction>
460
+ </AlertDialogFooter>
461
+ </AlertDialogContent>
462
+ </AlertDialog>
463
+ </Page>
464
+ );
465
+ }
@@ -0,0 +1,24 @@
1
+ columns:
2
+ - type: pk
3
+ - type: slug
4
+ - name: provider
5
+ type: enum
6
+ enum:
7
+ - openai
8
+ - gemini
9
+ default: openai
10
+ - name: model
11
+ isNullable: true
12
+ length: 127
13
+ - name: instructions
14
+ type: text
15
+ isNullable: true
16
+ - name: external_agent_id
17
+ isNullable: true
18
+ length: 127
19
+ - type: created_at
20
+ - type: updated_at
21
+ indices:
22
+ - columns:
23
+ - slug
24
+ isUnique: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/core",
3
- "version": "0.0.223",
3
+ "version": "0.0.233",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -30,10 +30,10 @@
30
30
  "speakeasy": "^2.0.0",
31
31
  "uuid": "^11.1.0",
32
32
  "@hed-hog/api": "0.0.3",
33
- "@hed-hog/api-types": "0.0.1",
34
- "@hed-hog/api-locale": "0.0.11",
35
33
  "@hed-hog/api-prisma": "0.0.4",
34
+ "@hed-hog/api-types": "0.0.1",
36
35
  "@hed-hog/api-pagination": "0.0.5",
36
+ "@hed-hog/api-locale": "0.0.11",
37
37
  "@hed-hog/api-mail": "0.0.7"
38
38
  },
39
39
  "exports": {
@@ -0,0 +1,68 @@
1
+ import { Role } from '@hed-hog/api';
2
+ import { Pagination } from '@hed-hog/api-pagination';
3
+ import { Body, Controller, Delete, Get, Inject, Param, ParseIntPipe, Patch, Post, UploadedFile, UseInterceptors, forwardRef } from '@nestjs/common';
4
+ import { FileInterceptor } from '@nestjs/platform-express';
5
+ import { DeleteDTO } from '../dto/delete.dto';
6
+ import { AiService } from './ai.service';
7
+ import { ChatAgentDTO } from './dto/chat-agent.dto';
8
+ import { ChatDTO } from './dto/chat.dto';
9
+ import { CreateAgentDTO } from './dto/create-agent.dto';
10
+ import { UpdateAgentDTO } from './dto/update-agent.dto';
11
+
12
+ @Role()
13
+ @Controller('ai')
14
+ export class AiController {
15
+ constructor(
16
+ @Inject(forwardRef(() => AiService))
17
+ private readonly aiService: AiService,
18
+ ) {}
19
+
20
+ @Post('chat')
21
+ @UseInterceptors(FileInterceptor('file'))
22
+ async chat(@Body() data: ChatDTO, @UploadedFile() file?: MulterFile) {
23
+ return this.aiService.chat(data, file);
24
+ }
25
+
26
+ @Post('agent')
27
+ async createAgent(@Body() data: CreateAgentDTO) {
28
+ return this.aiService.createAgent(data);
29
+ }
30
+
31
+ @Get('agent')
32
+ async listAgents(@Pagination() paginationParams) {
33
+ return this.aiService.listAgents(paginationParams);
34
+ }
35
+
36
+ @Get('agent/id/:agentId')
37
+ async getAgentById(@Param('agentId', ParseIntPipe) agentId: number) {
38
+ return this.aiService.getAgentById(agentId);
39
+ }
40
+
41
+ @Get('agent/:slug')
42
+ async getAgent(@Param('slug') slug: string) {
43
+ return this.aiService.getAgentBySlug(slug);
44
+ }
45
+
46
+ @Patch('agent/:agentId')
47
+ async updateAgent(
48
+ @Param('agentId', ParseIntPipe) agentId: number,
49
+ @Body() data: UpdateAgentDTO,
50
+ ) {
51
+ return this.aiService.updateAgent(agentId, data);
52
+ }
53
+
54
+ @Delete('agent')
55
+ async deleteAgents(@Body() data: DeleteDTO) {
56
+ return this.aiService.deleteAgents(data);
57
+ }
58
+
59
+ @Post('agent/:slug/chat')
60
+ @UseInterceptors(FileInterceptor('file'))
61
+ async chatWithAgent(
62
+ @Param('slug') slug: string,
63
+ @Body() data: ChatAgentDTO,
64
+ @UploadedFile() file?: MulterFile,
65
+ ) {
66
+ return this.aiService.chatWithAgent(slug, data, file);
67
+ }
68
+ }
@@ -0,0 +1,18 @@
1
+ import { PrismaModule } from '@hed-hog/api-prisma';
2
+ import { Module, forwardRef } from '@nestjs/common';
3
+ import { FileModule } from '../file/file.module';
4
+ import { SettingModule } from '../setting/setting.module';
5
+ import { AiController } from './ai.controller';
6
+ import { AiService } from './ai.service';
7
+
8
+ @Module({
9
+ imports: [
10
+ forwardRef(() => PrismaModule),
11
+ forwardRef(() => SettingModule),
12
+ forwardRef(() => FileModule),
13
+ ],
14
+ controllers: [AiController],
15
+ providers: [AiService],
16
+ exports: [AiService],
17
+ })
18
+ export class AiModule {}