@promptbook/cli 0.103.0-56 → 0.103.0-67
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/apps/agents-server/TODO.txt +5 -1
- package/apps/agents-server/config.ts +3 -1
- package/apps/agents-server/package-lock.json +1220 -47
- package/apps/agents-server/package.json +4 -1
- package/apps/agents-server/src/app/actions.ts +3 -1
- package/apps/agents-server/src/app/agents/[agentName]/AgentOptionsMenu.tsx +72 -6
- package/apps/agents-server/src/app/agents/[agentName]/AgentProfileChat.tsx +21 -8
- package/apps/agents-server/src/app/agents/[agentName]/AgentProfileWrapper.tsx +44 -0
- package/apps/agents-server/src/app/agents/[agentName]/generateAgentMetadata.ts +7 -3
- package/apps/agents-server/src/app/agents/[agentName]/layout.tsx +41 -0
- package/apps/agents-server/src/app/agents/[agentName]/page.tsx +47 -100
- package/apps/agents-server/src/app/agents/[agentName]/website-integration/page.tsx +11 -2
- package/apps/agents-server/src/app/embed/page.tsx +2 -2
- package/apps/agents-server/src/app/layout.tsx +8 -24
- package/apps/agents-server/src/app/manifest.ts +8 -3
- package/apps/agents-server/src/components/AgentProfile/AgentProfile.tsx +339 -0
- package/apps/agents-server/src/components/AgentProfile/AgentProfileFromSource.tsx +23 -0
- package/apps/agents-server/src/{app/agents/[agentName] → components/AgentProfile}/AgentQrCode.tsx +8 -1
- package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +7 -6
- package/apps/agents-server/src/database/metadataDefaults.ts +6 -0
- package/apps/agents-server/src/tools/$provideCdnForServer.ts +13 -1
- package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +119 -0
- package/apps/agents-server/src/utils/cdn/classes/VercelBlobStorage.ts +2 -1
- package/esm/index.es.js +65 -10
- package/esm/index.es.js.map +1 -1
- package/esm/typings/src/_packages/components.index.d.ts +2 -2
- package/esm/typings/src/_packages/types.index.d.ts +6 -0
- package/esm/typings/src/book-2.0/agent-source/AgentBasicInformation.d.ts +2 -1
- package/esm/typings/src/book-2.0/agent-source/createCommitmentRegex.d.ts +1 -1
- package/esm/typings/src/book-components/Chat/AgentChat/AgentChat.d.ts +3 -0
- package/esm/typings/src/book-components/Chat/Chat/ChatProps.d.ts +6 -0
- package/esm/typings/src/book-components/PromptbookAgent/PromptbookAgentIntegration.d.ts +52 -0
- package/esm/typings/src/book-components/PromptbookAgent/PromptbookAgentSeamlessIntegration.d.ts +14 -0
- package/esm/typings/src/book-components/icons/SendIcon.d.ts +3 -0
- package/esm/typings/src/commitments/CLOSED/CLOSED.d.ts +4 -0
- package/esm/typings/src/commitments/CLOSED/CLOSED.test.d.ts +4 -0
- package/esm/typings/src/commitments/USE_BROWSER/USE_BROWSER.d.ts +4 -0
- package/esm/typings/src/commitments/_base/BaseCommitmentDefinition.d.ts +6 -0
- package/esm/typings/src/llm-providers/agent/Agent.d.ts +3 -1
- package/esm/typings/src/other/templates/getTemplatesPipelineCollection.d.ts +1 -1
- package/esm/typings/src/types/typeAliases.d.ts +6 -0
- package/esm/typings/src/utils/color/Color.d.ts +1 -1
- package/esm/typings/src/utils/random/$generateBookBoilerplate.d.ts +6 -0
- package/esm/typings/src/utils/random/CzechNamePool.d.ts +7 -0
- package/esm/typings/src/utils/random/EnglishNamePool.d.ts +7 -0
- package/esm/typings/src/utils/random/NamePool.d.ts +17 -0
- package/esm/typings/src/utils/random/getNamePool.d.ts +10 -0
- package/esm/typings/src/version.d.ts +1 -1
- package/package.json +2 -2
- package/umd/index.umd.js +65 -10
- package/umd/index.umd.js.map +1 -1
- package/apps/agents-server/src/app/agents/[agentName]/AgentProfileView.tsx +0 -233
- package/esm/typings/src/book-components/PromptbookAgent/PromptbookAgent.d.ts +0 -29
- /package/apps/agents-server/src/{app/agents/[agentName] → components/AgentProfile}/QrCodeModal.tsx +0 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { AgentBasicInformation } from '@promptbook-local/types';
|
|
4
|
+
import { RepeatIcon } from 'lucide-react';
|
|
5
|
+
import { useMemo, useState } from 'react';
|
|
6
|
+
import spaceTrim from 'spacetrim';
|
|
7
|
+
import { Color } from '../../../../../src/utils/color/Color';
|
|
8
|
+
import { darken } from '../../../../../src/utils/color/operators/darken';
|
|
9
|
+
import { lighten } from '../../../../../src/utils/color/operators/lighten';
|
|
10
|
+
import { AgentQrCode } from './AgentQrCode';
|
|
11
|
+
import { QrCodeModal } from './QrCodeModal';
|
|
12
|
+
|
|
13
|
+
type AgentProfileProps = {
|
|
14
|
+
/**
|
|
15
|
+
* The agent to display
|
|
16
|
+
*/
|
|
17
|
+
readonly agent: AgentBasicInformation;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* URL of the agent page
|
|
21
|
+
*
|
|
22
|
+
* @default undefined - If not provided, some features like QR code for link might be disabled or use generic link
|
|
23
|
+
*/
|
|
24
|
+
readonly agentUrl?: string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Email of the agent
|
|
28
|
+
*/
|
|
29
|
+
readonly agentEmail?: string;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Content for the menu (top right)
|
|
33
|
+
*
|
|
34
|
+
* @param props.onShowQrCode - Function to open QR code modal
|
|
35
|
+
*/
|
|
36
|
+
readonly renderMenu?: (props: { onShowQrCode: () => void }) => React.ReactNode;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Content for the chat area
|
|
40
|
+
*/
|
|
41
|
+
readonly children?: React.ReactNode;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Content for the secondary actions (links)
|
|
45
|
+
*/
|
|
46
|
+
readonly actions?: React.ReactNode;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* If true, hides the menu and actions for fullscreen/embedded view
|
|
50
|
+
*/
|
|
51
|
+
readonly isHeadless?: boolean;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* CSS class name
|
|
55
|
+
*/
|
|
56
|
+
readonly className?: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export function AgentProfile(props: AgentProfileProps) {
|
|
60
|
+
const {
|
|
61
|
+
agent,
|
|
62
|
+
agentUrl = '',
|
|
63
|
+
agentEmail = '',
|
|
64
|
+
renderMenu,
|
|
65
|
+
children,
|
|
66
|
+
actions,
|
|
67
|
+
isHeadless = false,
|
|
68
|
+
className,
|
|
69
|
+
} = props;
|
|
70
|
+
const { meta, agentName } = agent;
|
|
71
|
+
const fullname = (meta.fullname as string) || agentName || 'Agent';
|
|
72
|
+
const personaDescription = agent.personaDescription || '';
|
|
73
|
+
const imageUrl = (meta.image as string) || null;
|
|
74
|
+
|
|
75
|
+
const [isQrModalOpen, setIsQrModalOpen] = useState(false);
|
|
76
|
+
const [isFlipped, setIsFlipped] = useState(false);
|
|
77
|
+
|
|
78
|
+
// Dynamic Font Loading
|
|
79
|
+
const fontString = meta.font;
|
|
80
|
+
let fontStyle: React.CSSProperties = {};
|
|
81
|
+
|
|
82
|
+
if (fontString) {
|
|
83
|
+
// [🧠] TODO: Properly parse font string to get family name
|
|
84
|
+
const primaryFont = fontString.split(',')[0].trim().replace(/['"]/g, '');
|
|
85
|
+
fontStyle = {
|
|
86
|
+
fontFamily: fontString,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Compute Colors and Background
|
|
91
|
+
const { brandColorHex, brandColorLightHex, brandColorDarkHex, backgroundImage } = useMemo(() => {
|
|
92
|
+
// [🧠] Default color should be imported constant, but for now hardcoded fallback
|
|
93
|
+
const PROMPTBOOK_COLOR_HEX = '#f15b24'; // TODO: Import PROMPTBOOK_COLOR
|
|
94
|
+
const brandColorString = meta.color || PROMPTBOOK_COLOR_HEX;
|
|
95
|
+
|
|
96
|
+
let brandColor;
|
|
97
|
+
try {
|
|
98
|
+
brandColor = Color.fromSafe(brandColorString.split(',')[0].trim());
|
|
99
|
+
} catch {
|
|
100
|
+
brandColor = Color.fromHex(PROMPTBOOK_COLOR_HEX);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const brandColorHex = brandColor.toHex();
|
|
104
|
+
const brandColorLightHex = brandColor.then(lighten(0.2)).toHex();
|
|
105
|
+
const brandColorDarkHex = brandColor.then(darken(0.15)).toHex();
|
|
106
|
+
|
|
107
|
+
// Generate Noisy SVG Background
|
|
108
|
+
const color1 = brandColor;
|
|
109
|
+
// const color2 = brandColors[1] || brandColors[0]!; // Use secondary color if available?
|
|
110
|
+
// For simplicity using primary color for now or derive second one
|
|
111
|
+
const color2 = brandColor;
|
|
112
|
+
|
|
113
|
+
// [🧠] Make colors much lighter for the background
|
|
114
|
+
const color1Light = color1.then(lighten(0.3)).toHex();
|
|
115
|
+
const color1Main = color1.toHex();
|
|
116
|
+
const color1Dark = color1.then(darken(0.3)).toHex();
|
|
117
|
+
|
|
118
|
+
const color2Light = color2.then(lighten(0.3)).toHex();
|
|
119
|
+
const color2Main = color2.toHex();
|
|
120
|
+
const color2Dark = color2.then(darken(0.3)).toHex();
|
|
121
|
+
|
|
122
|
+
const svgContent = spaceTrim(`
|
|
123
|
+
<svg xmlns="http://www.w3.org/2000/svg"
|
|
124
|
+
viewBox="0 0 1920 1080"
|
|
125
|
+
width="1920" height="1080"
|
|
126
|
+
preserveAspectRatio="xMidYMid slice">
|
|
127
|
+
<defs>
|
|
128
|
+
<!-- Bottom-left -->
|
|
129
|
+
<radialGradient id="grad1" cx="0%" cy="100%" r="90%">
|
|
130
|
+
<stop offset="0%" stop-color="${color1Light}" />
|
|
131
|
+
<stop offset="50%" stop-color="${color1Main}" />
|
|
132
|
+
<stop offset="100%" stop-color="${color1Dark}" />
|
|
133
|
+
</radialGradient>
|
|
134
|
+
|
|
135
|
+
<!-- Bottom-right -->
|
|
136
|
+
<radialGradient id="grad2" cx="100%" cy="100%" r="90%">
|
|
137
|
+
<stop offset="0%" stop-color="${color2Light}" />
|
|
138
|
+
<stop offset="50%" stop-color="${color2Main}" />
|
|
139
|
+
<stop offset="100%" stop-color="${color2Dark}" />
|
|
140
|
+
</radialGradient>
|
|
141
|
+
|
|
142
|
+
<!-- White top fade -->
|
|
143
|
+
<linearGradient id="whiteTopGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
144
|
+
<stop offset="0%" stop-color="#ffffff" stop-opacity="1" />
|
|
145
|
+
<stop offset="100%" stop-color="#ffffff" stop-opacity="0.3" />
|
|
146
|
+
</linearGradient>
|
|
147
|
+
|
|
148
|
+
<!-- Strong grain -->
|
|
149
|
+
<filter id="grain" x="-10%" y="-10%" width="120%" height="120%">
|
|
150
|
+
<feTurbulence type="fractalNoise" baseFrequency="3.5" numOctaves="3" seed="8" result="noise" />
|
|
151
|
+
<feComponentTransfer>
|
|
152
|
+
<feFuncR type="linear" slope="3.5" intercept="-1.2" />
|
|
153
|
+
<feFuncG type="linear" slope="3.5" intercept="-1.2" />
|
|
154
|
+
<feFuncB type="linear" slope="3.5" intercept="-1.2" />
|
|
155
|
+
<feFuncA type="table" tableValues="0 0.8" />
|
|
156
|
+
</feComponentTransfer>
|
|
157
|
+
</filter>
|
|
158
|
+
</defs>
|
|
159
|
+
|
|
160
|
+
<!-- White base -->
|
|
161
|
+
<rect width="100%" height="100%" fill="#ffffff" />
|
|
162
|
+
|
|
163
|
+
<!-- Gradients -->
|
|
164
|
+
<rect width="100%" height="100%" fill="url(#grad1)" />
|
|
165
|
+
<rect width="100%" height="100%" fill="url(#grad2)" style="mix-blend-mode:screen; opacity:0.85" />
|
|
166
|
+
|
|
167
|
+
<!-- White fade on top -->
|
|
168
|
+
<rect width="100%" height="100%" fill="url(#whiteTopGrad)" />
|
|
169
|
+
|
|
170
|
+
<!-- Strong visible noise -->
|
|
171
|
+
<rect width="100%" height="100%" filter="url(#grain)"
|
|
172
|
+
style="mix-blend-mode:soft-light; opacity:1.2" />
|
|
173
|
+
</svg>
|
|
174
|
+
`);
|
|
175
|
+
|
|
176
|
+
const backgroundImage = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`;
|
|
177
|
+
|
|
178
|
+
return { brandColorHex, brandColorLightHex, brandColorDarkHex, backgroundImage };
|
|
179
|
+
}, [meta.color]);
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<>
|
|
183
|
+
{fontString && (
|
|
184
|
+
<style jsx global>{`
|
|
185
|
+
@import url('https://fonts.googleapis.com/css2?family=${encodeURIComponent(
|
|
186
|
+
fontString.split(',')[0].trim().replace(/['"]/g, ''),
|
|
187
|
+
)}:wght@400;600;700&display=swap');
|
|
188
|
+
`}</style>
|
|
189
|
+
)}
|
|
190
|
+
|
|
191
|
+
{/* Full-screen background with agent color */}
|
|
192
|
+
<div
|
|
193
|
+
className={`w-full flex flex-col items-center justify-center p-6 md:p-12 relative overflow-hidden ${
|
|
194
|
+
isHeadless ? 'min-h-screen' : 'min-h-[calc(100vh-60px)]'
|
|
195
|
+
} ${className || ''}`}
|
|
196
|
+
style={{
|
|
197
|
+
background: `url("${backgroundImage}")`,
|
|
198
|
+
backgroundSize: 'cover',
|
|
199
|
+
backgroundPosition: 'center',
|
|
200
|
+
...fontStyle,
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
{/* Options menu in top right */}
|
|
204
|
+
{!isHeadless && renderMenu && (
|
|
205
|
+
<div className="absolute top-4 right-4 z-[9999]">
|
|
206
|
+
{renderMenu({ onShowQrCode: () => setIsQrModalOpen(true) })}
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
{/* Main profile content */}
|
|
211
|
+
<div className="relative z-10 grid grid-cols-[auto_1fr] gap-x-6 gap-y-4 md:gap-12 max-w-5xl w-full items-start">
|
|
212
|
+
{/* Agent image card (Flippable) */}
|
|
213
|
+
<div
|
|
214
|
+
className="flex-shrink-0 perspective-1000 group row-start-1 col-start-1 md:row-span-3"
|
|
215
|
+
style={{ perspective: '1000px' }}
|
|
216
|
+
>
|
|
217
|
+
<div
|
|
218
|
+
className="relative w-24 md:w-80 transition-all duration-700 transform-style-3d cursor-pointer"
|
|
219
|
+
style={{
|
|
220
|
+
aspectRatio: '1 / 1.618', // Golden Ratio
|
|
221
|
+
transformStyle: 'preserve-3d',
|
|
222
|
+
transform: isFlipped ? 'rotateY(180deg)' : 'rotateY(0deg)',
|
|
223
|
+
}}
|
|
224
|
+
onClick={() => setIsFlipped(!isFlipped)}
|
|
225
|
+
>
|
|
226
|
+
{/* Front of Card (Image) */}
|
|
227
|
+
<div
|
|
228
|
+
className="absolute inset-0 w-full h-full backface-hidden rounded-lg md:rounded-3xl shadow-lg md:shadow-2xl overflow-hidden backdrop-blur-sm"
|
|
229
|
+
style={{
|
|
230
|
+
backfaceVisibility: 'hidden',
|
|
231
|
+
backgroundColor: brandColorDarkHex,
|
|
232
|
+
boxShadow: `0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px ${brandColorLightHex}40`,
|
|
233
|
+
}}
|
|
234
|
+
>
|
|
235
|
+
{imageUrl ? (
|
|
236
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
237
|
+
<img src={imageUrl} alt={fullname} className="w-full h-full object-cover" />
|
|
238
|
+
) : (
|
|
239
|
+
<div
|
|
240
|
+
className="w-full h-full flex items-center justify-center text-3xl md:text-8xl font-bold text-white/80"
|
|
241
|
+
style={{ backgroundColor: brandColorDarkHex }}
|
|
242
|
+
>
|
|
243
|
+
{fullname.charAt(0).toUpperCase()}
|
|
244
|
+
</div>
|
|
245
|
+
)}
|
|
246
|
+
|
|
247
|
+
{/* Flip hint icon */}
|
|
248
|
+
<div className="absolute bottom-2 md:bottom-4 right-2 md:right-4 bg-black/30 p-1 md:p-2 rounded-full text-white/80 backdrop-blur-md opacity-0 group-hover:opacity-100 transition-opacity">
|
|
249
|
+
<RepeatIcon className="w-3 h-3 md:w-5 md:h-5" />
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
{/* Back of Card (QR Code) */}
|
|
254
|
+
<div
|
|
255
|
+
className="absolute inset-0 w-full h-full backface-hidden rounded-lg md:rounded-3xl shadow-lg md:shadow-2xl overflow-hidden backdrop-blur-sm flex flex-col items-center justify-center p-2 md:p-6"
|
|
256
|
+
style={{
|
|
257
|
+
backfaceVisibility: 'hidden',
|
|
258
|
+
transform: 'rotateY(180deg)',
|
|
259
|
+
background: `linear-gradient(135deg, ${brandColorLightHex} 0%, #ffffff 100%)`,
|
|
260
|
+
boxShadow: `0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px ${brandColorLightHex}40`,
|
|
261
|
+
}}
|
|
262
|
+
>
|
|
263
|
+
<div className="transform scale-90 md:scale-100">
|
|
264
|
+
<AgentQrCode
|
|
265
|
+
agentName={agentName}
|
|
266
|
+
agentUrl={agentUrl}
|
|
267
|
+
agentEmail={agentEmail}
|
|
268
|
+
personaDescription={personaDescription}
|
|
269
|
+
meta={meta}
|
|
270
|
+
isJustVcardShown
|
|
271
|
+
/>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
{/* Flip hint icon */}
|
|
275
|
+
<div className="absolute bottom-2 md:bottom-4 right-2 md:right-4 bg-black/10 p-1 md:p-2 rounded-full text-black/50 backdrop-blur-md">
|
|
276
|
+
<RepeatIcon className="w-3 h-3 md:w-5 md:h-5" />
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
{/* Agent info - Header Area */}
|
|
283
|
+
<div className="row-start-1 col-start-2 flex flex-col justify-center md:justify-start h-full md:h-auto gap-1 md:gap-6">
|
|
284
|
+
{/* Agent name with custom font */}
|
|
285
|
+
<h1
|
|
286
|
+
className="text-2xl md:text-5xl lg:text-6xl font-bold text-gray-900 tracking-tight leading-tight"
|
|
287
|
+
style={{
|
|
288
|
+
textShadow: '0 2px 20px rgba(255, 255, 255, 0.5)',
|
|
289
|
+
}}
|
|
290
|
+
>
|
|
291
|
+
{fullname}
|
|
292
|
+
</h1>
|
|
293
|
+
|
|
294
|
+
{/* Short description */}
|
|
295
|
+
<p className="text-sm md:text-xl text-gray-700 max-w-lg leading-relaxed font-medium line-clamp-3 md:line-clamp-none">
|
|
296
|
+
{personaDescription}
|
|
297
|
+
</p>
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
{/* Chat Area */}
|
|
301
|
+
<div className="col-span-2 md:col-span-1 md:col-start-2 w-full mt-2 md:mt-0">
|
|
302
|
+
{children}
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
{/* Secondary Actions */}
|
|
306
|
+
{!isHeadless && (
|
|
307
|
+
<div className="col-span-2 md:col-span-1 md:col-start-2 flex flex-wrap justify-center md:justify-start items-center gap-4 md:gap-6 mt-2">
|
|
308
|
+
{actions}
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
{/* Subtle gradient overlay at bottom */}
|
|
314
|
+
<div
|
|
315
|
+
className="absolute bottom-0 left-0 right-0 h-32 pointer-events-none"
|
|
316
|
+
style={{
|
|
317
|
+
background: `linear-gradient(to top, ${brandColorDarkHex}40, transparent)`,
|
|
318
|
+
}}
|
|
319
|
+
/>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
{/* QR Code Modal */}
|
|
323
|
+
<QrCodeModal
|
|
324
|
+
isOpen={isQrModalOpen}
|
|
325
|
+
onClose={() => setIsQrModalOpen(false)}
|
|
326
|
+
agentName={agentName}
|
|
327
|
+
meta={meta}
|
|
328
|
+
personaDescription={personaDescription}
|
|
329
|
+
agentUrl={agentUrl}
|
|
330
|
+
agentEmail={agentEmail}
|
|
331
|
+
brandColorHex={brandColorHex}
|
|
332
|
+
/>
|
|
333
|
+
</>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* TODO: !!!! Use 3D badge @see https://vercel.com/blog/building-an-interactive-3d-event-badge-with-react-three-fiber
|
|
339
|
+
*/
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { string_book } from '@promptbook-local/types';
|
|
4
|
+
import { useMemo } from 'react';
|
|
5
|
+
import { parseAgentSource } from '../../../../../src/book-2.0/agent-source/parseAgentSource';
|
|
6
|
+
import { AgentProfile } from './AgentProfile';
|
|
7
|
+
|
|
8
|
+
type AgentProfileFromSourceProps = Omit<React.ComponentProps<typeof AgentProfile>, 'agent'> & {
|
|
9
|
+
/**
|
|
10
|
+
* Source code of the agent (book)
|
|
11
|
+
*/
|
|
12
|
+
readonly source: string_book;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function AgentProfileFromSource(props: AgentProfileFromSourceProps) {
|
|
16
|
+
const { source, ...rest } = props;
|
|
17
|
+
|
|
18
|
+
const agent = useMemo(() => {
|
|
19
|
+
return parseAgentSource(source);
|
|
20
|
+
}, [source]);
|
|
21
|
+
|
|
22
|
+
return <AgentProfile agent={agent} {...rest} />;
|
|
23
|
+
}
|
package/apps/agents-server/src/{app/agents/[agentName] → components/AgentProfile}/AgentQrCode.tsx
RENAMED
|
@@ -8,9 +8,12 @@ import spaceTrim from 'spacetrim';
|
|
|
8
8
|
type AgentQrCodeProps = Pick<AgentBasicInformation, 'agentName' | 'personaDescription' | 'meta'> & {
|
|
9
9
|
agentUrl: string;
|
|
10
10
|
agentEmail: string;
|
|
11
|
+
|
|
12
|
+
isJustVcardShown?: boolean;
|
|
11
13
|
};
|
|
12
14
|
|
|
13
|
-
export function AgentQrCode(
|
|
15
|
+
export function AgentQrCode(props: AgentQrCodeProps) {
|
|
16
|
+
const { agentName, agentUrl, agentEmail, personaDescription, meta, isJustVcardShown } = props;
|
|
14
17
|
const [mode, setMode] = useState<'contact' | 'link'>('contact');
|
|
15
18
|
|
|
16
19
|
// TODO: [🧠] Should we include more info in VCARD?
|
|
@@ -27,6 +30,10 @@ export function AgentQrCode({ agentName, agentUrl, agentEmail, personaDescriptio
|
|
|
27
30
|
const qrValue = mode === 'contact' ? vcard : agentUrl;
|
|
28
31
|
const label = mode === 'contact' ? 'Scan to add contact' : 'Scan to open agent';
|
|
29
32
|
|
|
33
|
+
if (isJustVcardShown) {
|
|
34
|
+
return <PromptbookQrCode value={vcard} className="" size={250} />;
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
return (
|
|
31
38
|
<div className="flex flex-col items-center">
|
|
32
39
|
<div className="flex bg-gray-100 p-1 rounded-lg mb-4">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { usePathname } from 'next/navigation';
|
|
3
|
+
import { usePathname, useSearchParams } from 'next/navigation';
|
|
4
4
|
import { AgentBasicInformation } from '../../../../../src/book-2.0/agent-source/AgentBasicInformation';
|
|
5
5
|
import type { UserInfo } from '../../utils/getCurrentUser';
|
|
6
6
|
import { Footer, type FooterLink } from '../Footer/Footer';
|
|
@@ -28,12 +28,13 @@ export function LayoutWrapper({
|
|
|
28
28
|
footerLinks,
|
|
29
29
|
}: LayoutWrapperProps) {
|
|
30
30
|
const pathname = usePathname();
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
const
|
|
31
|
+
const searchParams = useSearchParams();
|
|
32
|
+
const isHeadless = searchParams.has('headless');
|
|
33
|
+
// const isAdminChatPage = pathname?.startsWith('/admin/chat-history') || pathname?.startsWith('/admin/chat-feedback');
|
|
34
|
+
const isHeaderHidden = false; // pathname?.includes('/chat') && !isAdminChatPage;
|
|
35
|
+
const isFooterHiddenOnPage = pathname ? /^\/agents\/[^/]+\/(book|chat|book\+chat)$/.test(pathname) : false;
|
|
35
36
|
|
|
36
|
-
if (isHeaderHidden) {
|
|
37
|
+
if (isHeaderHidden || isHeadless) {
|
|
37
38
|
return <main className={`pt-0`}>{children}</main>;
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -61,6 +61,12 @@ export const metadataDefaults = [
|
|
|
61
61
|
note: 'Maximum size of file that can be uploaded in MB.',
|
|
62
62
|
type: 'NUMBER',
|
|
63
63
|
},
|
|
64
|
+
{
|
|
65
|
+
key: 'NAME_POOL',
|
|
66
|
+
value: 'ENGLISH',
|
|
67
|
+
note: 'Language for generating new agent names. Possible values: ENGLISH, CZECH.',
|
|
68
|
+
type: 'TEXT_SINGLE_LINE',
|
|
69
|
+
},
|
|
64
70
|
] as const satisfies ReadonlyArray<{
|
|
65
71
|
key: string;
|
|
66
72
|
value: string;
|
|
@@ -14,10 +14,22 @@ let cdn: IIFilesStorageWithCdn | null = null;
|
|
|
14
14
|
export function $provideCdnForServer(): IIFilesStorageWithCdn {
|
|
15
15
|
if (!cdn) {
|
|
16
16
|
cdn = new VercelBlobStorage({
|
|
17
|
-
token: process.env.
|
|
17
|
+
token: process.env.VERCEL_BLOB_READ_WRITE_TOKEN!,
|
|
18
18
|
pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX!,
|
|
19
19
|
cdnPublicUrl: new URL(process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!),
|
|
20
20
|
});
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
cdn = new DigitalOceanSpaces({
|
|
24
|
+
bucket: process.env.CDN_BUCKET!,
|
|
25
|
+
pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX!,
|
|
26
|
+
endpoint: process.env.CDN_ENDPOINT!,
|
|
27
|
+
accessKeyId: process.env.CDN_ACCESS_KEY_ID!,
|
|
28
|
+
secretAccessKey: process.env.CDN_SECRET_ACCESS_KEY!,
|
|
29
|
+
cdnPublicUrl: new URL(process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!),
|
|
30
|
+
gzip: true,
|
|
31
|
+
});
|
|
32
|
+
*/
|
|
21
33
|
}
|
|
22
34
|
|
|
23
35
|
return cdn;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { GetObjectCommand, PutObjectCommand, PutObjectCommandInput, S3Client } from '@aws-sdk/client-s3';
|
|
2
|
+
import { NotYetImplementedError } from '@promptbook-local/core';
|
|
3
|
+
import { gzip, ungzip } from 'node-gzip';
|
|
4
|
+
import { TODO_USE } from '../../../../../../src/utils/organization/TODO_USE';
|
|
5
|
+
import { validateMimeType } from '../../validators/validateMimeType';
|
|
6
|
+
import type { IFile, IIFilesStorageWithCdn } from '../interfaces/IFilesStorage';
|
|
7
|
+
|
|
8
|
+
type IDigitalOceanSpacesConfig = {
|
|
9
|
+
readonly bucket: string;
|
|
10
|
+
readonly pathPrefix: string;
|
|
11
|
+
readonly endpoint: string;
|
|
12
|
+
readonly accessKeyId: string;
|
|
13
|
+
readonly secretAccessKey: string;
|
|
14
|
+
readonly cdnPublicUrl: URL;
|
|
15
|
+
readonly gzip: boolean;
|
|
16
|
+
|
|
17
|
+
// TODO: [⛳️] Probbably prefix should be in this config not on the consumer side
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export class DigitalOceanSpaces implements IIFilesStorageWithCdn {
|
|
21
|
+
public get cdnPublicUrl() {
|
|
22
|
+
return this.config.cdnPublicUrl;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private s3: S3Client;
|
|
26
|
+
|
|
27
|
+
public constructor(private readonly config: IDigitalOceanSpacesConfig) {
|
|
28
|
+
this.s3 = new S3Client({
|
|
29
|
+
region: 'auto',
|
|
30
|
+
endpoint: 'https://' + config.endpoint,
|
|
31
|
+
credentials: {
|
|
32
|
+
accessKeyId: config.accessKeyId,
|
|
33
|
+
secretAccessKey: config.secretAccessKey,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public getItemUrl(key: string): URL {
|
|
39
|
+
return new URL(this.config.pathPrefix + '/' + key, this.cdnPublicUrl);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public async getItem(key: string): Promise<IFile | null> {
|
|
43
|
+
const parameters = {
|
|
44
|
+
Bucket: this.config.bucket,
|
|
45
|
+
Key: this.config.pathPrefix + '/' + key,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const { Body, ContentType, ContentEncoding } = await this.s3.send(new GetObjectCommand(parameters));
|
|
50
|
+
|
|
51
|
+
// const blob = new Blob([await Body?.transformToByteArray()!]);
|
|
52
|
+
|
|
53
|
+
if (ContentEncoding === 'gzip') {
|
|
54
|
+
return {
|
|
55
|
+
type: validateMimeType(ContentType),
|
|
56
|
+
data: await ungzip(await Body!.transformToByteArray()),
|
|
57
|
+
};
|
|
58
|
+
} else {
|
|
59
|
+
return {
|
|
60
|
+
type: validateMimeType(ContentType),
|
|
61
|
+
data: (await Body!.transformToByteArray()) as Buffer,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error instanceof Error && error.name.match(/^NoSuchKey/)) {
|
|
66
|
+
return null;
|
|
67
|
+
} else {
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public async removeItem(key: string): Promise<void> {
|
|
74
|
+
TODO_USE(key);
|
|
75
|
+
throw new NotYetImplementedError(`DigitalOceanSpaces.removeItem is not implemented yet`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public async setItem(key: string, file: IFile): Promise<void> {
|
|
79
|
+
// TODO: Put putObjectRequestAdditional into processedFile
|
|
80
|
+
const putObjectRequestAdditional: Partial<PutObjectCommandInput> = {};
|
|
81
|
+
|
|
82
|
+
let processedFile: IFile;
|
|
83
|
+
if (this.config.gzip) {
|
|
84
|
+
const gzipped = await gzip(file.data);
|
|
85
|
+
const sizePercentageAfterCompression = gzipped.byteLength / file.data.byteLength;
|
|
86
|
+
if (sizePercentageAfterCompression < 0.7) {
|
|
87
|
+
// consolex.log(`Gzipping ${key} (${Math.floor(sizePercentageAfterCompression * 100)}%)`);
|
|
88
|
+
processedFile = { ...file, data: gzipped };
|
|
89
|
+
putObjectRequestAdditional.ContentEncoding = 'gzip';
|
|
90
|
+
} else {
|
|
91
|
+
processedFile = file;
|
|
92
|
+
// consolex.log(`NOT Gzipping ${key} (${Math.floor(sizePercentageAfterCompression * 100)}%)`);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
processedFile = file;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const uploadResult = await this.s3.send(
|
|
99
|
+
new PutObjectCommand({
|
|
100
|
+
Bucket: this.config.bucket,
|
|
101
|
+
Key: this.config.pathPrefix + '/' + key,
|
|
102
|
+
ContentType: processedFile.type,
|
|
103
|
+
...putObjectRequestAdditional,
|
|
104
|
+
Body: processedFile.data,
|
|
105
|
+
// TODO: Public read access / just private to extending class
|
|
106
|
+
ACL: 'public-read',
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (!uploadResult.ETag) {
|
|
111
|
+
throw new Error(`Upload result does not contain ETag`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* TODO: Implement Read-only mode
|
|
118
|
+
* TODO: [☹️] Unite with `PromptbookStorage` and move to `/src/...`
|
|
119
|
+
*/
|
|
@@ -51,12 +51,13 @@ export class VercelBlobStorage implements IIFilesStorageWithCdn {
|
|
|
51
51
|
|
|
52
52
|
public async setItem(key: string, file: IFile): Promise<void> {
|
|
53
53
|
const path = this.config.pathPrefix ? `${this.config.pathPrefix}/${key}` : key;
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
await put(path, file.data, {
|
|
56
56
|
access: 'public',
|
|
57
57
|
addRandomSuffix: false,
|
|
58
58
|
contentType: file.type,
|
|
59
59
|
token: this.config.token,
|
|
60
|
+
allowOverwrite: true, // <- TODO: This is inefficient, we should check first if the file exists and only then decide to overwrite or not
|
|
60
61
|
// Note: We rely on Vercel Blob for compression
|
|
61
62
|
});
|
|
62
63
|
}
|