@promptbook/cli 0.104.0-10 → 0.104.0-11

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 (47) hide show
  1. package/apps/agents-server/src/app/admin/files/FilesGalleryClient.tsx +263 -0
  2. package/apps/agents-server/src/app/admin/files/actions.ts +61 -0
  3. package/apps/agents-server/src/app/admin/files/page.tsx +13 -0
  4. package/apps/agents-server/src/app/admin/image-generator-test/ImageGeneratorTestClient.tsx +169 -0
  5. package/apps/agents-server/src/app/admin/image-generator-test/page.tsx +13 -0
  6. package/apps/agents-server/src/app/admin/images/ImagesGalleryClient.tsx +256 -0
  7. package/apps/agents-server/src/app/admin/images/actions.ts +60 -0
  8. package/apps/agents-server/src/app/admin/images/page.tsx +13 -0
  9. package/apps/agents-server/src/app/admin/search-engine-test/SearchEngineTestClient.tsx +109 -0
  10. package/apps/agents-server/src/app/admin/search-engine-test/actions.ts +17 -0
  11. package/apps/agents-server/src/app/admin/search-engine-test/page.tsx +13 -0
  12. package/apps/agents-server/src/app/api/images/[filename]/route.ts +22 -1
  13. package/apps/agents-server/src/components/AgentProfile/AgentCapabilityChips.tsx +38 -0
  14. package/apps/agents-server/src/components/AgentProfile/AgentProfile.tsx +3 -0
  15. package/apps/agents-server/src/components/Header/Header.tsx +16 -0
  16. package/apps/agents-server/src/components/Homepage/AgentCard.tsx +13 -8
  17. package/apps/agents-server/src/database/$getTableName.ts +1 -0
  18. package/apps/agents-server/src/database/migrations/2025-12-0830-image-purpose.sql +5 -0
  19. package/apps/agents-server/src/database/migrations/2025-12-0890-file-agent-id.sql +5 -0
  20. package/apps/agents-server/src/database/schema.ts +14 -1
  21. package/esm/index.es.js +53 -2
  22. package/esm/index.es.js.map +1 -1
  23. package/esm/typings/src/_packages/types.index.d.ts +6 -0
  24. package/esm/typings/src/book-2.0/agent-source/AgentBasicInformation.d.ts +23 -0
  25. package/esm/typings/src/book-components/Chat/types/ChatMessage.d.ts +2 -2
  26. package/esm/typings/src/book-components/_common/Dropdown/Dropdown.d.ts +1 -1
  27. package/esm/typings/src/book-components/_common/HamburgerMenu/HamburgerMenu.d.ts +1 -1
  28. package/esm/typings/src/book-components/icons/AboutIcon.d.ts +1 -1
  29. package/esm/typings/src/book-components/icons/AttachmentIcon.d.ts +1 -1
  30. package/esm/typings/src/book-components/icons/CameraIcon.d.ts +1 -1
  31. package/esm/typings/src/book-components/icons/DownloadIcon.d.ts +1 -1
  32. package/esm/typings/src/book-components/icons/MenuIcon.d.ts +1 -1
  33. package/esm/typings/src/book-components/icons/SaveIcon.d.ts +1 -1
  34. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +2 -2
  35. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentsDatabaseSchema.d.ts +0 -54
  36. package/esm/typings/src/llm-providers/_common/utils/count-total-usage/countUsage.d.ts +1 -1
  37. package/esm/typings/src/llm-providers/agent/Agent.d.ts +6 -1
  38. package/esm/typings/src/remote-server/ui/ServerApp.d.ts +1 -1
  39. package/esm/typings/src/search-engines/SearchEngine.d.ts +9 -0
  40. package/esm/typings/src/search-engines/SearchResult.d.ts +18 -0
  41. package/esm/typings/src/search-engines/bing/BingSearchEngine.d.ts +15 -0
  42. package/esm/typings/src/search-engines/dummy/DummySearchEngine.d.ts +15 -0
  43. package/esm/typings/src/utils/random/$randomAgentPersona.d.ts +3 -2
  44. package/esm/typings/src/version.d.ts +1 -1
  45. package/package.json +1 -1
  46. package/umd/index.umd.js +53 -2
  47. package/umd/index.umd.js.map +1 -1
@@ -0,0 +1,256 @@
1
+ 'use client';
2
+
3
+ import { ChevronLeft, ChevronRight, Grid, LayoutList, Loader2 } from 'lucide-react';
4
+ import Link from 'next/link';
5
+ import { useEffect, useRef, useState } from 'react';
6
+ import { ImageWithAgent, listImages } from './actions';
7
+
8
+ type ViewMode = 'TABLE' | 'GRID';
9
+
10
+ export function ImagesGalleryClient() {
11
+ const [viewMode, setViewMode] = useState<ViewMode>('GRID');
12
+ const [images, setImages] = useState<ImageWithAgent[]>([]);
13
+ const [total, setTotal] = useState(0);
14
+ const [isLoading, setIsLoading] = useState(false);
15
+ const [page, setPage] = useState(1);
16
+ const [limit] = useState(20);
17
+ const [hasMore, setHasMore] = useState(true);
18
+
19
+ const loadImages = async (pageNum: number, isNewView: boolean) => {
20
+ setIsLoading(true);
21
+ try {
22
+ const result = await listImages({ page: pageNum, limit });
23
+ if (isNewView) {
24
+ setImages(result.images);
25
+ } else {
26
+ setImages((prev) => [...prev, ...result.images]);
27
+ }
28
+ setTotal(result.total);
29
+ setHasMore(result.images.length === limit);
30
+ } catch (error) {
31
+ console.error(error);
32
+ } finally {
33
+ setIsLoading(false);
34
+ }
35
+ };
36
+
37
+ useEffect(() => {
38
+ loadImages(1, true);
39
+ setPage(1);
40
+ setHasMore(true);
41
+ }, [viewMode]);
42
+
43
+ const handleLoadMore = () => {
44
+ if (!isLoading && hasMore) {
45
+ const nextPage = page + 1;
46
+ setPage(nextPage);
47
+ loadImages(nextPage, false);
48
+ }
49
+ };
50
+
51
+ // Table view pagination
52
+ const handlePageChange = (newPage: number) => {
53
+ setPage(newPage);
54
+ loadImages(newPage, true);
55
+ };
56
+
57
+ const observerTarget = useRef<HTMLDivElement>(null);
58
+
59
+ useEffect(() => {
60
+ const observer = new IntersectionObserver(
61
+ (entries) => {
62
+ if (entries[0].isIntersecting && hasMore && !isLoading && viewMode === 'GRID') {
63
+ handleLoadMore();
64
+ }
65
+ },
66
+ { threshold: 0.1 }
67
+ );
68
+
69
+ if (observerTarget.current) {
70
+ observer.observe(observerTarget.current);
71
+ }
72
+
73
+ return () => {
74
+ // eslint-disable-next-line react-hooks/exhaustive-deps
75
+ if (observerTarget.current) {
76
+ observer.unobserve(observerTarget.current);
77
+ }
78
+ };
79
+ }, [hasMore, isLoading, viewMode, page]);
80
+
81
+ return (
82
+ <div className="container mx-auto px-4 py-8 space-y-6">
83
+ <div className="flex justify-between items-center mt-20 mb-4">
84
+ <h1 className="text-3xl text-gray-900 font-light">Images Gallery</h1>
85
+ <div className="flex items-center gap-2 bg-gray-100 p-1 rounded-lg">
86
+ <button
87
+ onClick={() => setViewMode('TABLE')}
88
+ className={`p-2 rounded-md transition-colors ${
89
+ viewMode === 'TABLE' ? 'bg-white shadow-sm text-blue-600' : 'text-gray-500 hover:text-gray-900'
90
+ }`}
91
+ title="Table View"
92
+ >
93
+ <LayoutList className="w-5 h-5" />
94
+ </button>
95
+ <button
96
+ onClick={() => setViewMode('GRID')}
97
+ className={`p-2 rounded-md transition-colors ${
98
+ viewMode === 'GRID' ? 'bg-white shadow-sm text-blue-600' : 'text-gray-500 hover:text-gray-900'
99
+ }`}
100
+ title="Grid View"
101
+ >
102
+ <Grid className="w-5 h-5" />
103
+ </button>
104
+ </div>
105
+ </div>
106
+
107
+ {viewMode === 'TABLE' ? (
108
+ <div className="bg-white border rounded-lg overflow-hidden shadow-sm">
109
+ <div className="overflow-x-auto">
110
+ <table className="w-full text-sm text-left text-gray-500">
111
+ <thead className="text-xs text-gray-700 uppercase bg-gray-50 border-b">
112
+ <tr>
113
+ <th className="px-6 py-3 w-32">Image</th>
114
+ <th className="px-6 py-3">Prompt</th>
115
+ <th className="px-6 py-3">Agent</th>
116
+ <th className="px-6 py-3">Purpose</th>
117
+ <th className="px-6 py-3">Created At</th>
118
+ </tr>
119
+ </thead>
120
+ <tbody>
121
+ {images.map((image) => (
122
+ <tr key={image.id} className="bg-white border-b hover:bg-gray-50">
123
+ <td className="px-6 py-4">
124
+ <a href={image.cdnUrl} target="_blank" rel="noopener noreferrer" className="block w-20 h-20 relative rounded overflow-hidden border bg-gray-100">
125
+ {/* eslint-disable-next-line @next/next/no-img-element */}
126
+ <img src={image.cdnUrl} alt={image.prompt} className="object-cover w-full h-full" />
127
+ </a>
128
+ </td>
129
+ <td className="px-6 py-4">
130
+ <div className="max-w-md truncate" title={image.prompt}>
131
+ {image.prompt}
132
+ </div>
133
+ <div className="text-xs text-gray-400 mt-1">{image.filename}</div>
134
+ </td>
135
+ <td className="px-6 py-4">
136
+ {image.agent ? (
137
+ <Link href={`/${image.agent.agentName}`} className="text-blue-600 hover:underline">
138
+ {image.agent.agentName}
139
+ </Link>
140
+ ) : (
141
+ <span className="text-gray-400">-</span>
142
+ )}
143
+ </td>
144
+ <td className="px-6 py-4">
145
+ {image.purpose ? (
146
+ <span className={`px-2 py-1 rounded-full text-xs font-medium ${
147
+ image.purpose === 'AVATAR' ? 'bg-purple-100 text-purple-800' : 'bg-yellow-100 text-yellow-800'
148
+ }`}>
149
+ {image.purpose}
150
+ </span>
151
+ ) : (
152
+ <span className="text-gray-400">-</span>
153
+ )}
154
+ </td>
155
+ <td className="px-6 py-4 whitespace-nowrap">
156
+ {new Date(image.createdAt).toLocaleString()}
157
+ </td>
158
+ </tr>
159
+ ))}
160
+ {images.length === 0 && !isLoading && (
161
+ <tr>
162
+ <td colSpan={5} className="px-6 py-4 text-center text-gray-500">
163
+ No images found.
164
+ </td>
165
+ </tr>
166
+ )}
167
+ </tbody>
168
+ </table>
169
+ </div>
170
+ {/* Pagination for Table */}
171
+ <div className="flex items-center justify-between px-6 py-4 border-t bg-gray-50">
172
+ <span className="text-sm text-gray-700">
173
+ Showing <span className="font-medium">{(page - 1) * limit + 1}</span> to <span className="font-medium">{Math.min(page * limit, total)}</span> of <span className="font-medium">{total}</span> results
174
+ </span>
175
+ <div className="flex gap-2">
176
+ <button
177
+ onClick={() => handlePageChange(page - 1)}
178
+ disabled={page === 1 || isLoading}
179
+ className="p-2 border rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
180
+ >
181
+ <ChevronLeft className="w-4 h-4" />
182
+ </button>
183
+ <button
184
+ onClick={() => handlePageChange(page + 1)}
185
+ disabled={page * limit >= total || isLoading}
186
+ className="p-2 border rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
187
+ >
188
+ <ChevronRight className="w-4 h-4" />
189
+ </button>
190
+ </div>
191
+ </div>
192
+ </div>
193
+ ) : (
194
+ <>
195
+ <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
196
+ {images.map((image) => (
197
+ <div key={image.id} className="group relative border rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-md transition-shadow">
198
+ <a href={image.cdnUrl} target="_blank" rel="noopener noreferrer" className="block aspect-square relative bg-gray-100 overflow-hidden">
199
+ {/* eslint-disable-next-line @next/next/no-img-element */}
200
+ <img
201
+ src={image.cdnUrl}
202
+ alt={image.prompt}
203
+ className="object-cover w-full h-full transition-transform duration-300 group-hover:scale-105"
204
+ loading="lazy"
205
+ />
206
+ {image.purpose && (
207
+ <div className="absolute top-2 right-2">
208
+ <span className={`px-2 py-0.5 rounded-full text-[10px] font-bold shadow-sm ${
209
+ image.purpose === 'AVATAR' ? 'bg-purple-100 text-purple-800' : 'bg-yellow-100 text-yellow-800'
210
+ }`}>
211
+ {image.purpose}
212
+ </span>
213
+ </div>
214
+ )}
215
+ </a>
216
+ <div className="p-3">
217
+ <div className="flex items-center justify-between gap-2 mb-1">
218
+ {image.agent ? (
219
+ <Link href={`/${image.agent.agentName}`} className="text-xs font-medium text-blue-600 hover:underline truncate">
220
+ {image.agent.agentName}
221
+ </Link>
222
+ ) : (
223
+ <span className="text-xs text-gray-400">No agent</span>
224
+ )}
225
+ <span className="text-[10px] text-gray-400 whitespace-nowrap">
226
+ {new Date(image.createdAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}
227
+ </span>
228
+ </div>
229
+ <p className="text-xs text-gray-600 line-clamp-2" title={image.prompt}>
230
+ {image.prompt}
231
+ </p>
232
+ </div>
233
+ </div>
234
+ ))}
235
+ </div>
236
+
237
+ {images.length === 0 && !isLoading && (
238
+ <div className="text-center text-gray-500 py-12">
239
+ No images found.
240
+ </div>
241
+ )}
242
+
243
+ {/* Infinite Scroll Loader */}
244
+ <div className="py-8 flex justify-center" ref={observerTarget}>
245
+ {isLoading && (
246
+ <Loader2 className="w-8 h-8 animate-spin text-blue-500" />
247
+ )}
248
+ {!isLoading && !hasMore && images.length > 0 && (
249
+ <p className="text-gray-400 text-sm">No more images</p>
250
+ )}
251
+ </div>
252
+ </>
253
+ )}
254
+ </div>
255
+ );
256
+ }
@@ -0,0 +1,60 @@
1
+ 'use server';
2
+
3
+ import { $getTableName } from '@/src/database/$getTableName';
4
+ import { TODO_any } from '@promptbook-local/types';
5
+ import { $provideSupabaseForServer } from '../../../database/$provideSupabaseForServer';
6
+
7
+ export type ImageWithAgent = {
8
+ id: number;
9
+ createdAt: string;
10
+ updatedAt: string;
11
+ filename: string;
12
+ prompt: string;
13
+ cdnUrl: string;
14
+ cdnKey: string;
15
+ agentId: number | null;
16
+ purpose: 'AVATAR' | 'TESTING' | null;
17
+ agent: {
18
+ id: number;
19
+ agentName: string;
20
+ } | null;
21
+ };
22
+
23
+ export async function listImages(options: {
24
+ page: number;
25
+ limit: number;
26
+ }): Promise<{ images: ImageWithAgent[]; total: number }> {
27
+ const { page, limit } = options;
28
+ const offset = (page - 1) * limit;
29
+
30
+ const supabase = $provideSupabaseForServer();
31
+
32
+ const {
33
+ data: images,
34
+ error,
35
+ count,
36
+ } = await supabase
37
+ .from(await $getTableName('Image'))
38
+ .select(
39
+ `
40
+ *,
41
+ agent:agentId (
42
+ id,
43
+ agentName
44
+ )
45
+ `,
46
+ { count: 'exact' },
47
+ )
48
+ .range(offset, offset + limit - 1)
49
+ .order('createdAt', { ascending: false });
50
+
51
+ if (error) {
52
+ console.error('Error fetching images:', error);
53
+ throw new Error(error.message);
54
+ }
55
+
56
+ return {
57
+ images: (images as TODO_any[]) || [],
58
+ total: count || 0,
59
+ };
60
+ }
@@ -0,0 +1,13 @@
1
+ import { ForbiddenPage } from '../../../components/ForbiddenPage/ForbiddenPage';
2
+ import { isUserAdmin } from '../../../utils/isUserAdmin';
3
+ import { ImagesGalleryClient } from './ImagesGalleryClient';
4
+
5
+ export default async function ImagesGalleryPage() {
6
+ const isAdmin = await isUserAdmin();
7
+
8
+ if (!isAdmin) {
9
+ return <ForbiddenPage />;
10
+ }
11
+
12
+ return <ImagesGalleryClient />;
13
+ }
@@ -0,0 +1,109 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Card } from '../../../components/Homepage/Card';
5
+ import { SearchResult } from '../../../../../../src/search-engines/SearchResult';
6
+ import { search } from './actions';
7
+
8
+ export function SearchEngineTestClient() {
9
+ const [query, setQuery] = useState('');
10
+ const [provider, setProvider] = useState('dummy');
11
+ const [results, setResults] = useState<SearchResult[] | null>(null);
12
+ const [isLoading, setIsLoading] = useState(false);
13
+ const [error, setError] = useState<string | null>(null);
14
+
15
+ const handleSearch = async () => {
16
+ if (!query) return;
17
+
18
+ setIsLoading(true);
19
+ setError(null);
20
+ setResults(null);
21
+
22
+ try {
23
+ const results = await search(query, provider);
24
+ setResults(results);
25
+ } catch (err) {
26
+ setError(String(err));
27
+ } finally {
28
+ setIsLoading(false);
29
+ }
30
+ };
31
+
32
+ return (
33
+ <div className="container mx-auto px-4 py-8 space-y-6">
34
+ <div className="mt-20 mb-4 flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
35
+ <div>
36
+ <h1 className="text-3xl text-gray-900 font-light">Search Engine Test</h1>
37
+ <p className="mt-1 text-sm text-gray-500">
38
+ Test the search engine capabilities by providing a query.
39
+ </p>
40
+ </div>
41
+ </div>
42
+
43
+ <Card>
44
+ <div className="mb-4 space-y-4">
45
+ <div className="space-y-2">
46
+ <label className="block text-sm font-medium text-gray-700">Query</label>
47
+ <input
48
+ type="text"
49
+ value={query}
50
+ onChange={(e) => setQuery(e.target.value)}
51
+ placeholder="e.g., Cat"
52
+ className="w-full p-2 border border-gray-300 rounded"
53
+ disabled={isLoading}
54
+ onKeyDown={(e) => {
55
+ if (e.key === 'Enter') {
56
+ handleSearch();
57
+ }
58
+ }}
59
+ />
60
+ </div>
61
+ <div className="space-y-2">
62
+ <label className="block text-sm font-medium text-gray-700">Provider</label>
63
+ <select
64
+ value={provider}
65
+ onChange={(e) => setProvider(e.target.value)}
66
+ className="w-full p-2 border border-gray-300 rounded"
67
+ disabled={isLoading}
68
+ >
69
+ <option value="dummy">Dummy</option>
70
+ <option value="bing">Bing</option>
71
+ </select>
72
+ </div>
73
+
74
+ <div className="flex justify-end">
75
+ <button
76
+ onClick={handleSearch}
77
+ disabled={isLoading || !query}
78
+ className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50"
79
+ >
80
+ {isLoading ? 'Searching...' : 'Search'}
81
+ </button>
82
+ </div>
83
+ </div>
84
+
85
+ {error && (
86
+ <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4" role="alert">
87
+ <strong className="font-bold">Error: </strong>
88
+ <span className="block sm:inline">{error}</span>
89
+ </div>
90
+ )}
91
+
92
+ {results && (
93
+ <div className="space-y-4">
94
+ <h2 className="text-xl font-semibold">Results</h2>
95
+ {results.map((result, index) => (
96
+ <div key={index} className="border p-4 rounded bg-gray-50">
97
+ <a href={result.url} target="_blank" rel="noopener noreferrer" className="text-blue-600 font-bold hover:underline">
98
+ {result.title}
99
+ </a>
100
+ <div className="text-sm text-green-700">{result.url}</div>
101
+ <p className="text-gray-700">{result.snippet}</p>
102
+ </div>
103
+ ))}
104
+ </div>
105
+ )}
106
+ </Card>
107
+ </div>
108
+ );
109
+ }
@@ -0,0 +1,17 @@
1
+ 'use server';
2
+
3
+ import { BingSearchEngine } from '../../../../../../src/search-engines/bing/BingSearchEngine';
4
+ import { DummySearchEngine } from '../../../../../../src/search-engines/dummy/DummySearchEngine';
5
+ import { SearchResult } from '../../../../../../src/search-engines/SearchResult';
6
+
7
+ export async function search(query: string, provider: string): Promise<SearchResult[]> {
8
+ if (provider === 'dummy') {
9
+ const searchEngine = new DummySearchEngine();
10
+ return searchEngine.search(query);
11
+ } else if (provider === 'bing') {
12
+ const searchEngine = new BingSearchEngine();
13
+ await searchEngine.checkConfiguration();
14
+ return searchEngine.search(query);
15
+ }
16
+ throw new Error(`Unknown provider: ${provider}`);
17
+ }
@@ -0,0 +1,13 @@
1
+ import { ForbiddenPage } from '../../../components/ForbiddenPage/ForbiddenPage';
2
+ import { isUserAdmin } from '../../../utils/isUserAdmin';
3
+ import { SearchEngineTestClient } from './SearchEngineTestClient';
4
+
5
+ export default async function SearchEngineTestPage() {
6
+ const isAdmin = await isUserAdmin();
7
+
8
+ if (!isAdmin) {
9
+ return <ForbiddenPage />;
10
+ }
11
+
12
+ return <SearchEngineTestClient />;
13
+ }
@@ -13,6 +13,9 @@ import { filenameToPrompt } from '../../../../utils/normalization/filenameToProm
13
13
  export async function GET(request: NextRequest, { params }: { params: Promise<{ filename: string }> }) {
14
14
  try {
15
15
  const { filename } = await params;
16
+ const searchParams = request.nextUrl.searchParams;
17
+ const modelName = searchParams.get('modelName');
18
+ const isRaw = searchParams.get('raw') === 'true';
16
19
 
17
20
  if (!filename) {
18
21
  return NextResponse.json({ error: 'Filename is required' }, { status: 400 });
@@ -33,6 +36,13 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
33
36
  }
34
37
 
35
38
  if (existingImage) {
39
+ if (isRaw) {
40
+ return NextResponse.json({
41
+ source: 'cache',
42
+ filename,
43
+ cdnUrl: existingImage.cdnUrl,
44
+ });
45
+ }
36
46
  // Image exists, redirect to CDN
37
47
  return NextResponse.redirect(existingImage.cdnUrl as string_url);
38
48
  }
@@ -53,7 +63,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
53
63
  parameters: {},
54
64
  modelRequirements: {
55
65
  modelVariant: 'IMAGE_GENERATION',
56
- modelName: 'dall-e-3', // Use DALL-E 3 for high quality
66
+ modelName: modelName || 'dall-e-3', // Use DALL-E 3 for high quality
57
67
  },
58
68
  });
59
69
 
@@ -92,6 +102,17 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
92
102
  throw insertError;
93
103
  }
94
104
 
105
+ if (isRaw) {
106
+ return NextResponse.json({
107
+ source: 'generated',
108
+ filename,
109
+ prompt,
110
+ modelName: modelName || 'dall-e-3',
111
+ cdnUrl: cdnUrl.href,
112
+ imageResult,
113
+ });
114
+ }
115
+
95
116
  // Redirect to the newly created image
96
117
  return NextResponse.redirect(cdnUrl.href as string_url);
97
118
  } catch (error) {
@@ -0,0 +1,38 @@
1
+ import { AgentBasicInformation } from '@promptbook-local/types';
2
+ import { Book, FileText, Globe, Search } from 'lucide-react';
3
+
4
+ type AgentCapabilityChipsProps = {
5
+ readonly agent: AgentBasicInformation;
6
+ readonly className?: string;
7
+ };
8
+
9
+ export function AgentCapabilityChips({ agent, className }: AgentCapabilityChipsProps) {
10
+ if (!agent.capabilities || agent.capabilities.length === 0) {
11
+ return null;
12
+ }
13
+
14
+ return (
15
+ <div className={`flex flex-wrap gap-2 ${className || ''}`}>
16
+ {agent.capabilities.map((capability, i) => {
17
+ const Icon =
18
+ {
19
+ Globe,
20
+ Search,
21
+ Book,
22
+ FileText,
23
+ }[capability.iconName] || Book;
24
+
25
+ return (
26
+ <div
27
+ key={i}
28
+ className="flex items-center gap-1.5 bg-white/50 backdrop-blur-sm px-2.5 py-1 rounded-full text-xs font-semibold text-gray-800 border border-white/20 shadow-sm"
29
+ title={capability.label}
30
+ >
31
+ <Icon className="w-3.5 h-3.5 opacity-70" />
32
+ <span className="truncate max-w-[150px]">{capability.label}</span>
33
+ </div>
34
+ );
35
+ })}
36
+ </div>
37
+ );
38
+ }
@@ -5,6 +5,7 @@ import { generatePlaceholderAgentProfileImageUrl } from '@promptbook-local/core'
5
5
  import { AgentBasicInformation, string_agent_permanent_id } from '@promptbook-local/types';
6
6
  import { RepeatIcon } from 'lucide-react';
7
7
  import { useState } from 'react';
8
+ import { AgentCapabilityChips } from './AgentCapabilityChips';
8
9
  import { AgentProfileImage } from './AgentProfileImage';
9
10
  import { AgentQrCode } from './AgentQrCode';
10
11
  import { QrCodeModal } from './QrCodeModal';
@@ -222,6 +223,8 @@ export function AgentProfile(props: AgentProfileProps) {
222
223
  <p className="text-sm md:text-xl text-gray-700 max-w-lg leading-relaxed font-medium line-clamp-3 md:line-clamp-none">
223
224
  {personaDescription}
224
225
  </p>
226
+
227
+ <AgentCapabilityChips agent={agent} />
225
228
  </div>
226
229
 
227
230
  {/* Chat Area */}
@@ -309,6 +309,22 @@ export function Header(props: HeaderProps) {
309
309
  label: 'Browser',
310
310
  href: '/admin/browser-test',
311
311
  },
312
+ {
313
+ label: 'Image Generator Test',
314
+ href: '/admin/image-generator-test',
315
+ },
316
+ {
317
+ label: 'Search Engine Test',
318
+ href: '/admin/search-engine-test',
319
+ },
320
+ {
321
+ label: 'Images gallery',
322
+ href: '/admin/images',
323
+ },
324
+ {
325
+ label: 'Files',
326
+ href: '/admin/files',
327
+ },
312
328
  ],
313
329
  },
314
330
  {