@promptbook/cli 0.103.0-66 → 0.103.0-68

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 (43) hide show
  1. package/apps/agents-server/TODO.txt +1 -5
  2. package/apps/agents-server/config.ts +3 -1
  3. package/apps/agents-server/package-lock.json +8 -2317
  4. package/apps/agents-server/package.json +0 -9
  5. package/apps/agents-server/src/app/agents/[agentName]/AgentOptionsMenu.tsx +34 -2
  6. package/apps/agents-server/src/app/agents/[agentName]/AgentProfileChat.tsx +1 -1
  7. package/apps/agents-server/src/app/agents/[agentName]/AgentProfileWrapper.tsx +4 -0
  8. package/apps/agents-server/src/app/humans.txt/route.ts +15 -0
  9. package/apps/agents-server/src/app/layout.tsx +31 -0
  10. package/apps/agents-server/src/app/robots.txt/route.ts +15 -0
  11. package/apps/agents-server/src/app/security.txt/route.ts +15 -0
  12. package/apps/agents-server/src/app/sitemap.xml/route.ts +37 -0
  13. package/apps/agents-server/src/components/AgentProfile/AgentProfile.tsx +28 -23
  14. package/apps/agents-server/src/components/DocumentationContent/DocumentationContent.tsx +19 -18
  15. package/apps/agents-server/src/components/Footer/Footer.tsx +13 -13
  16. package/apps/agents-server/src/components/Header/Header.tsx +95 -20
  17. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +3 -0
  18. package/apps/agents-server/src/components/_utils/generateMetaTxt.ts +28 -0
  19. package/apps/agents-server/src/components/_utils/headlessParam.tsx +36 -0
  20. package/apps/agents-server/src/middleware.ts +6 -2
  21. package/apps/agents-server/src/tools/$provideCdnForServer.ts +13 -1
  22. package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +119 -0
  23. package/apps/agents-server/src/utils/cdn/classes/VercelBlobStorage.ts +2 -1
  24. package/esm/index.es.js +191 -14
  25. package/esm/index.es.js.map +1 -1
  26. package/esm/typings/src/_packages/components.index.d.ts +2 -0
  27. package/esm/typings/src/_packages/types.index.d.ts +6 -0
  28. package/esm/typings/src/book-components/BookEditor/BookEditor.d.ts +10 -0
  29. package/esm/typings/src/book-components/BookEditor/BookEditorActionbar.d.ts +4 -0
  30. package/esm/typings/src/book-components/icons/CameraIcon.d.ts +11 -0
  31. package/esm/typings/src/execution/LlmExecutionTools.d.ts +5 -1
  32. package/esm/typings/src/execution/PromptResult.d.ts +7 -1
  33. package/esm/typings/src/llm-providers/ollama/OllamaExecutionTools.d.ts +4 -0
  34. package/esm/typings/src/llm-providers/openai/OpenAiCompatibleExecutionTools.d.ts +13 -1
  35. package/esm/typings/src/llm-providers/openai/OpenAiExecutionTools.d.ts +4 -0
  36. package/esm/typings/src/llm-providers/openai/createOpenAiCompatibleExecutionTools.d.ts +6 -6
  37. package/esm/typings/src/types/ModelRequirements.d.ts +13 -1
  38. package/esm/typings/src/types/ModelVariant.d.ts +1 -1
  39. package/esm/typings/src/types/Prompt.d.ts +13 -1
  40. package/esm/typings/src/version.d.ts +1 -1
  41. package/package.json +1 -1
  42. package/umd/index.umd.js +191 -14
  43. package/umd/index.umd.js.map +1 -1
@@ -4,7 +4,7 @@ import promptbookLogoBlueTransparent from '@/public/logo-blue-white-256.png';
4
4
  import { $createAgentAction, logoutAction } from '@/src/app/actions';
5
5
  import { ArrowRight, ChevronDown, Lock, LogIn, LogOut, User } from 'lucide-react';
6
6
  import Image from 'next/image';
7
- import Link from 'next/link';
7
+ import { HeadlessLink, useIsHeadless, pushWithHeadless } from '../_utils/headlessParam';
8
8
  import { useRouter } from 'next/navigation';
9
9
  import { ReactNode, useState } from 'react';
10
10
  import { AgentBasicInformation } from '../../../../../src/book-2.0/agent-source/AgentBasicInformation';
@@ -41,6 +41,11 @@ type HeaderProps = {
41
41
  * List of agents
42
42
  */
43
43
  agents: Array<AgentBasicInformation>;
44
+
45
+ /**
46
+ * List of federated servers for navigation dropdown
47
+ */
48
+ federatedServers: Array<{ url: string; title: string; logoUrl?: string | null }>;
44
49
  };
45
50
 
46
51
  /* TODO: [🐱‍🚀] Make this Agents server native */
@@ -70,7 +75,7 @@ type MenuItem =
70
75
  };
71
76
 
72
77
  export function Header(props: HeaderProps) {
73
- const { isAdmin = false, currentUser = null, serverName, serverLogoUrl, agents } = props;
78
+ const { isAdmin = false, currentUser = null, serverName, serverLogoUrl, agents, federatedServers } = props;
74
79
 
75
80
  const [isMenuOpen, setIsMenuOpen] = useState(false);
76
81
  const [isLoginOpen, setIsLoginOpen] = useState(false);
@@ -86,6 +91,7 @@ export function Header(props: HeaderProps) {
86
91
  const [isMobileSystemOpen, setIsMobileSystemOpen] = useState(false);
87
92
  const [isCreatingAgent, setIsCreatingAgent] = useState(false);
88
93
  const router = useRouter();
94
+ const isHeadless = useIsHeadless();
89
95
 
90
96
  const { users: adminUsers } = useUsersAdmin();
91
97
 
@@ -98,7 +104,7 @@ export function Header(props: HeaderProps) {
98
104
  const agentName = await $createAgentAction();
99
105
 
100
106
  if (agentName) {
101
- router.push(`/agents/${agentName}`);
107
+ pushWithHeadless(router, `/agents/${agentName}`, isHeadless);
102
108
  setIsAgentsOpen(false);
103
109
  setIsMenuOpen(false);
104
110
  } else {
@@ -109,6 +115,37 @@ export function Header(props: HeaderProps) {
109
115
  }
110
116
  };
111
117
 
118
+ // Federated servers dropdown items (respect logo, only current is not clickable)
119
+ const [isFederatedOpen, setIsFederatedOpen] = useState(false);
120
+ const [isMobileFederatedOpen, setIsMobileFederatedOpen] = useState(false);
121
+
122
+ const federatedDropdownItems: SubMenuItem[] = federatedServers.map(server => {
123
+ const isCurrent = server.url === (typeof window !== 'undefined' ? window.location.origin : '');
124
+ return isCurrent
125
+ ? {
126
+ label: (
127
+ <span className="flex items-center gap-2">
128
+ {/* eslint-disable-next-line @next/next/no-img-element */}
129
+ <img src={server.logoUrl || serverLogoUrl || promptbookLogoBlueTransparent.src} alt={server.title} width={20} height={20} className="w-5 h-5 object-contain rounded-full" />
130
+ <span className="font-semibold">{server.title.replace(/^Federated: /, '')}</span>
131
+ <span className="ml-1 text-xs text-blue-600">(current)</span>
132
+ </span>
133
+ ),
134
+ isBold: true,
135
+ isBordered: true,
136
+ }
137
+ : {
138
+ label: (
139
+ <span className="flex items-center gap-2">
140
+ {/* eslint-disable-next-line @next/next/no-img-element */}
141
+ <img src={server.logoUrl || promptbookLogoBlueTransparent.src} alt={server.title} width={20} height={20} className="w-5 h-5 object-contain rounded-full" />
142
+ <span>{server.title.replace(/^Federated: /, '')}</span>
143
+ </span>
144
+ ),
145
+ href: server.url,
146
+ };
147
+ });
148
+
112
149
  // Menu items configuration (DRY principle)
113
150
  const menuItems: MenuItem[] = [
114
151
  {
@@ -257,8 +294,8 @@ export function Header(props: HeaderProps) {
257
294
  <ChangePasswordDialog isOpen={isChangePasswordOpen} onClose={() => setIsChangePasswordOpen(false)} />
258
295
  <div className="container mx-auto px-4 h-full">
259
296
  <div className="flex items-center justify-between h-full">
260
- {/* Logo */}
261
- <Link href="/" className="flex items-center gap-3 hover:opacity-80 transition-opacity">
297
+ {/* Logo and heading */}
298
+ <HeadlessLink href="/" className="flex items-center gap-3 hover:opacity-80 transition-opacity">
262
299
  {serverLogoUrl ? (
263
300
  // Note: `next/image` does not load external images well without extra config
264
301
  // eslint-disable-next-line @next/next/no-img-element
@@ -279,20 +316,58 @@ export function Header(props: HeaderProps) {
279
316
  />
280
317
  )}
281
318
  <h1 className="text-xl font-bold tracking-tight text-gray-900">{serverName}</h1>
282
- </Link>
319
+ </HeadlessLink>
283
320
 
284
321
  {/* Desktop Navigation */}
285
322
  <nav className="hidden lg:flex items-center gap-8">
323
+ {/* Federated servers dropdown */}
324
+ <div className="relative">
325
+ <button
326
+ className="flex items-center gap-1 text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors cursor-pointer"
327
+ onClick={() => setIsFederatedOpen(!isFederatedOpen)}
328
+ onBlur={() => setTimeout(() => setIsFederatedOpen(false), 200)}
329
+ >
330
+ <ChevronDown className="w-4 h-4" />
331
+ <span>Switch server</span>
332
+ </button>
333
+ {isFederatedOpen && (
334
+ <div className="absolute top-full left-0 mt-2 w-56 bg-white rounded-md shadow-lg border border-gray-100 py-1 z-50 animate-in fade-in zoom-in-95 duration-200 max-h-[80vh] overflow-y-auto">
335
+ {federatedDropdownItems.map((subItem, subIndex) => {
336
+ const className = `block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 hover:text-gray-900 ${
337
+ subItem.isBold ? 'font-medium' : ''
338
+ } ${subItem.isBordered ? 'border-b border-gray-100' : ''}`;
339
+
340
+ if (subItem.href) {
341
+ return (
342
+ <HeadlessLink
343
+ key={subIndex}
344
+ href={subItem.href}
345
+ className={className}
346
+ onClick={() => setIsFederatedOpen(false)}
347
+ >
348
+ {subItem.label}
349
+ </HeadlessLink>
350
+ );
351
+ }
352
+ return (
353
+ <span key={subIndex} className={className}>
354
+ {subItem.label}
355
+ </span>
356
+ );
357
+ })}
358
+ </div>
359
+ )}
360
+ </div>
286
361
  {menuItems.map((item, index) => {
287
362
  if (item.type === 'link') {
288
363
  return (
289
- <Link
364
+ <HeadlessLink
290
365
  key={index}
291
366
  href={item.href}
292
367
  className="text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors cursor-pointer"
293
368
  >
294
369
  {item.label}
295
- </Link>
370
+ </HeadlessLink>
296
371
  );
297
372
  }
298
373
 
@@ -328,14 +403,14 @@ export function Header(props: HeaderProps) {
328
403
  }
329
404
 
330
405
  return (
331
- <Link
406
+ <HeadlessLink
332
407
  key={subIndex}
333
408
  href={subItem.href!}
334
409
  className={className}
335
410
  onClick={() => item.setIsOpen(false)}
336
411
  >
337
412
  {subItem.label}
338
- </Link>
413
+ </HeadlessLink>
339
414
  );
340
415
  })}
341
416
  </div>
@@ -348,25 +423,25 @@ export function Header(props: HeaderProps) {
348
423
  })}
349
424
 
350
425
  {just(false /* TODO: [🧠] Figure out what to do with theese links */) && (
351
- <Link
426
+ <a
352
427
  href="https://ptbk.io/"
353
428
  target="_blank"
354
429
  className="text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors cursor-pointer"
355
430
  >
356
431
  Create your server
357
- </Link>
432
+ </a>
358
433
  )}
359
434
  </nav>
360
435
 
361
436
  {/* CTA Button & Mobile Menu Toggle */}
362
437
  <div className="flex items-center gap-4">
363
438
  {just(false /* TODO: [🧠] Figure out what to do with call to action */) && (
364
- <Link href="https://ptbk.io/?modal=get-started" target="_blank" className="hidden md:block">
439
+ <a href="https://ptbk.io/?modal=get-started" target="_blank" className="hidden md:block">
365
440
  <button className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-10 px-4 py-2 bg-promptbook-blue-dark text-white hover:bg-promptbook-blue-dark/90">
366
441
  Get Started
367
442
  <ArrowRight className="ml-2 w-4 h-4" />
368
443
  </button>
369
- </Link>
444
+ </a>
370
445
  )}
371
446
 
372
447
  {!currentUser && !isAdmin && (
@@ -508,14 +583,14 @@ export function Header(props: HeaderProps) {
508
583
  {menuItems.map((item, index) => {
509
584
  if (item.type === 'link') {
510
585
  return (
511
- <Link
586
+ <HeadlessLink
512
587
  key={index}
513
588
  href={item.href}
514
589
  className="block text-base font-medium text-gray-600 hover:text-gray-900 py-2"
515
590
  onClick={() => setIsMenuOpen(false)}
516
591
  >
517
592
  {item.label}
518
- </Link>
593
+ </HeadlessLink>
519
594
  );
520
595
  }
521
596
 
@@ -555,14 +630,14 @@ export function Header(props: HeaderProps) {
555
630
  }
556
631
 
557
632
  return (
558
- <Link
633
+ <HeadlessLink
559
634
  key={subIndex}
560
635
  href={subItem.href!}
561
636
  className={className}
562
637
  onClick={() => setIsMenuOpen(false)}
563
638
  >
564
639
  {subItem.label}
565
- </Link>
640
+ </HeadlessLink>
566
641
  );
567
642
  })}
568
643
  </div>
@@ -575,14 +650,14 @@ export function Header(props: HeaderProps) {
575
650
  })}
576
651
 
577
652
  {just(false /* TODO: [🧠] Figure out what to do with these links */) && (
578
- <Link
653
+ <a
579
654
  href="https://ptbk.io/"
580
655
  target="_blank"
581
656
  className="text-base font-medium text-gray-600 hover:text-gray-900 py-2"
582
657
  onClick={() => setIsMenuOpen(false)}
583
658
  >
584
659
  Create your server
585
- </Link>
660
+ </a>
586
661
  )}
587
662
  </nav>
588
663
  </div>
@@ -15,6 +15,7 @@ type LayoutWrapperProps = {
15
15
  agents: Array<AgentBasicInformation>;
16
16
  isFooterShown: boolean;
17
17
  footerLinks: Array<FooterLink>;
18
+ federatedServers: Array<{ url: string; title: string }>;
18
19
  };
19
20
 
20
21
  export function LayoutWrapper({
@@ -26,6 +27,7 @@ export function LayoutWrapper({
26
27
  agents,
27
28
  isFooterShown,
28
29
  footerLinks,
30
+ federatedServers,
29
31
  }: LayoutWrapperProps) {
30
32
  const pathname = usePathname();
31
33
  const searchParams = useSearchParams();
@@ -46,6 +48,7 @@ export function LayoutWrapper({
46
48
  serverName={serverName}
47
49
  serverLogoUrl={serverLogoUrl}
48
50
  agents={agents}
51
+ federatedServers={federatedServers}
49
52
  />
50
53
  <main className={`pt-[60px]`}>{children}</main>
51
54
  {isFooterShown && !isFooterHiddenOnPage && <Footer extraLinks={footerLinks} />}
@@ -0,0 +1,28 @@
1
+ // Utility to generate content for robots.txt, security.txt, and humans.txt [DRY]
2
+
3
+ import { NEXT_PUBLIC_SITE_URL } from '@/config';
4
+
5
+ // Get base URL from environment or config
6
+ const baseUrl = NEXT_PUBLIC_SITE_URL?.href || process.env.PUBLIC_URL || 'https://ptbk.io';
7
+
8
+ export function generateRobotsTxt(): string {
9
+ return ['User-agent: *', `Sitemap: ${baseUrl}sitemap.xml`, ''].join('\n');
10
+ }
11
+
12
+ export function generateSecurityTxt(): string {
13
+ // See https://securitytxt.org/ for more fields
14
+ return [
15
+ `Contact: mailto:security@ptbk.io`,
16
+ `Expires: ${new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]}`,
17
+ `Canonical: ${baseUrl}security.txt`,
18
+ '',
19
+ ].join('\n');
20
+ }
21
+
22
+ export function generateHumansTxt(): string {
23
+ return ['/* TEAM */', 'Developer: Promptbook Team', `Site: https://ptbk.io`, `Instance: ${baseUrl}`].join('\n');
24
+ }
25
+
26
+ /**
27
+ * TODO: Use `spaceTrim`
28
+ */
@@ -0,0 +1,36 @@
1
+ // Utility to append ?headless param if present in current URL
2
+ import { usePathname, useSearchParams } from 'next/navigation';
3
+ import Link, { LinkProps } from 'next/link';
4
+ import { useMemo } from 'react';
5
+
6
+ // Returns true if ?headless is present in current search params
7
+ export function useIsHeadless() {
8
+ const searchParams = useSearchParams();
9
+ return searchParams.has('headless');
10
+ }
11
+
12
+ // Appends ?headless to a given href if needed
13
+ export function appendHeadlessParam(href: string, isHeadless: boolean): string {
14
+ if (!isHeadless) return href;
15
+ if (href.includes('headless')) return href;
16
+ const hasQuery = href.includes('?');
17
+ return hasQuery ? `${href}&headless` : `${href}?headless`;
18
+ }
19
+
20
+ // Custom Link that preserves headless param
21
+ export function HeadlessLink({ href, children, ...rest }: LinkProps & { children: React.ReactNode } & React.AnchorHTMLAttributes<HTMLAnchorElement>) {
22
+ const isHeadless = useIsHeadless();
23
+ const finalHref = useMemo(() => appendHeadlessParam(String(href), isHeadless), [href, isHeadless]);
24
+ return (
25
+ <Link href={finalHref} {...rest}>
26
+ {children}
27
+ </Link>
28
+ );
29
+ }
30
+
31
+ import { useRouter } from "next/navigation";
32
+
33
+ // Helper for router.push
34
+ export function pushWithHeadless(router: ReturnType<typeof useRouter>, href: string, isHeadless: boolean) {
35
+ router.push(appendHeadlessParam(href, isHeadless));
36
+ }
@@ -191,12 +191,16 @@ export async function middleware(req: NextRequest) {
191
191
  'api',
192
192
  'admin',
193
193
  'docs',
194
- 'manifest.webmanifest',
195
- 'sw.js',
196
194
  'test',
197
195
  'embed',
198
196
  '_next',
197
+ 'manifest.webmanifest',
198
+ 'sw.js',
199
199
  'favicon.ico',
200
+ 'sitemap.xml',
201
+ 'robots.txt',
202
+ 'security.txt',
203
+ 'humans.txt',
200
204
  ].includes(potentialAgentName) &&
201
205
  !potentialAgentName.startsWith('.') &&
202
206
  // Note: Other static files are excluded by the matcher configuration below
@@ -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.BLOB_READ_WRITE_TOKEN!,
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
  }