@promptbook/cli 0.103.0-54 → 0.103.0-56

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 (80) hide show
  1. package/apps/agents-server/config.ts +0 -2
  2. package/apps/agents-server/package-lock.json +1163 -0
  3. package/apps/agents-server/package.json +6 -0
  4. package/apps/agents-server/src/app/admin/chat-feedback/ChatFeedbackClient.tsx +79 -6
  5. package/apps/agents-server/src/app/admin/chat-history/ChatHistoryClient.tsx +171 -69
  6. package/apps/agents-server/src/app/agents/[agentName]/AgentChatWrapper.tsx +3 -1
  7. package/apps/agents-server/src/app/agents/[agentName]/AgentOptionsMenu.tsx +216 -0
  8. package/apps/agents-server/src/app/agents/[agentName]/AgentProfileChat.tsx +78 -0
  9. package/apps/agents-server/src/app/agents/[agentName]/AgentProfileView.tsx +233 -0
  10. package/apps/agents-server/src/app/agents/[agentName]/CloneAgentButton.tsx +4 -4
  11. package/apps/agents-server/src/app/agents/[agentName]/InstallPwaButton.tsx +2 -2
  12. package/apps/agents-server/src/app/agents/[agentName]/QrCodeModal.tsx +90 -0
  13. package/apps/agents-server/src/app/agents/[agentName]/agentLinks.tsx +80 -0
  14. package/apps/agents-server/src/app/agents/[agentName]/api/book/route.ts +3 -1
  15. package/apps/agents-server/src/app/agents/[agentName]/api/chat/route.ts +11 -1
  16. package/apps/agents-server/src/app/agents/[agentName]/api/mcp/route.ts +203 -0
  17. package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/route.ts +3 -1
  18. package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/systemMessage/route.ts +3 -1
  19. package/apps/agents-server/src/app/agents/[agentName]/api/openai/chat/completions/route.ts +3 -169
  20. package/apps/agents-server/src/app/agents/[agentName]/api/openai/models/route.ts +93 -0
  21. package/apps/agents-server/src/app/agents/[agentName]/api/openai/v1/chat/completions/route.ts +10 -0
  22. package/apps/agents-server/src/app/agents/[agentName]/api/openai/v1/models/route.ts +93 -0
  23. package/apps/agents-server/src/app/agents/[agentName]/api/openrouter/chat/completions/route.ts +10 -0
  24. package/apps/agents-server/src/app/agents/[agentName]/api/voice/route.ts +4 -0
  25. package/apps/agents-server/src/app/agents/[agentName]/chat/page.tsx +9 -2
  26. package/apps/agents-server/src/app/agents/[agentName]/integration/SdkCodeTabs.tsx +31 -0
  27. package/apps/agents-server/src/app/agents/[agentName]/integration/page.tsx +271 -30
  28. package/apps/agents-server/src/app/agents/[agentName]/links/page.tsx +182 -0
  29. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +108 -165
  30. package/apps/agents-server/src/app/agents/[agentName]/website-integration/page.tsx +61 -0
  31. package/apps/agents-server/src/app/api/auth/change-password/route.ts +75 -0
  32. package/apps/agents-server/src/app/api/chat-feedback/export/route.ts +55 -0
  33. package/apps/agents-server/src/app/api/chat-history/export/route.ts +55 -0
  34. package/apps/agents-server/src/app/api/openai/v1/chat/completions/route.ts +6 -0
  35. package/apps/agents-server/src/app/api/openai/v1/models/route.ts +65 -0
  36. package/apps/agents-server/src/app/docs/[docId]/page.tsx +12 -32
  37. package/apps/agents-server/src/app/docs/page.tsx +42 -17
  38. package/apps/agents-server/src/app/globals.css +129 -0
  39. package/apps/agents-server/src/app/layout.tsx +8 -2
  40. package/apps/agents-server/src/app/manifest.ts +1 -1
  41. package/apps/agents-server/src/components/ChangePasswordDialog/ChangePasswordDialog.tsx +41 -0
  42. package/apps/agents-server/src/components/ChangePasswordForm/ChangePasswordForm.tsx +159 -0
  43. package/apps/agents-server/src/components/DocumentationContent/DocumentationContent.tsx +87 -0
  44. package/apps/agents-server/src/components/Header/Header.tsx +94 -38
  45. package/apps/agents-server/src/components/OpenMojiIcon/OpenMojiIcon.tsx +20 -0
  46. package/apps/agents-server/src/components/PrintButton/PrintButton.tsx +18 -0
  47. package/apps/agents-server/src/components/PrintHeader/PrintHeader.tsx +18 -0
  48. package/apps/agents-server/src/database/migrations/2025-12-0070-chat-history-source.sql +2 -0
  49. package/apps/agents-server/src/database/schema.ts +6 -0
  50. package/apps/agents-server/src/middleware.ts +1 -1
  51. package/apps/agents-server/src/utils/convertToCsv.ts +31 -0
  52. package/apps/agents-server/src/utils/handleChatCompletion.ts +355 -0
  53. package/apps/agents-server/src/utils/resolveInheritedAgentSource.ts +100 -0
  54. package/apps/agents-server/src/utils/validateApiKey.ts +128 -0
  55. package/apps/agents-server/tailwind.config.ts +1 -1
  56. package/esm/index.es.js +1188 -175
  57. package/esm/index.es.js.map +1 -1
  58. package/esm/typings/src/book-2.0/agent-source/AgentModelRequirements.d.ts +4 -0
  59. package/esm/typings/src/book-components/Chat/LlmChat/LlmChatProps.d.ts +5 -0
  60. package/esm/typings/src/commitments/CLOSED/CLOSED.d.ts +35 -0
  61. package/esm/typings/src/commitments/COMPONENT/COMPONENT.d.ts +28 -0
  62. package/esm/typings/src/commitments/FROM/FROM.d.ts +34 -0
  63. package/esm/typings/src/commitments/LANGUAGE/LANGUAGE.d.ts +35 -0
  64. package/esm/typings/src/commitments/META_COLOR/META_COLOR.d.ts +6 -0
  65. package/esm/typings/src/commitments/META_FONT/META_FONT.d.ts +42 -0
  66. package/esm/typings/src/commitments/OPEN/OPEN.d.ts +35 -0
  67. package/esm/typings/src/commitments/USE/USE.d.ts +53 -0
  68. package/esm/typings/src/commitments/USE_BROWSER/USE_BROWSER.d.ts +38 -0
  69. package/esm/typings/src/commitments/USE_BROWSER/USE_BROWSER.test.d.ts +1 -0
  70. package/esm/typings/src/commitments/USE_MCP/USE_MCP.d.ts +37 -0
  71. package/esm/typings/src/commitments/USE_SEARCH_ENGINE/USE_SEARCH_ENGINE.d.ts +38 -0
  72. package/esm/typings/src/commitments/index.d.ts +12 -1
  73. package/esm/typings/src/playground/playground.d.ts +3 -0
  74. package/esm/typings/src/utils/color/Color.d.ts +8 -0
  75. package/esm/typings/src/utils/color/css-colors.d.ts +1 -0
  76. package/esm/typings/src/version.d.ts +1 -1
  77. package/package.json +2 -2
  78. package/umd/index.umd.js +1180 -167
  79. package/umd/index.umd.js.map +1 -1
  80. package/esm/typings/src/playground/playground1.d.ts +0 -2
@@ -0,0 +1,216 @@
1
+ 'use client';
2
+
3
+ import {
4
+ CopyIcon,
5
+ CopyPlusIcon,
6
+ DownloadIcon,
7
+ MailIcon,
8
+ MessageCircleQuestionIcon,
9
+ MessageSquareIcon,
10
+ MessageSquareShareIcon,
11
+ MoreHorizontalIcon,
12
+ QrCodeIcon,
13
+ SquareSplitHorizontalIcon,
14
+ } from 'lucide-react';
15
+ import { useEffect, useRef, useState } from 'react';
16
+ import { string_data_url, string_url_image } from '../../../../../../src/types/typeAliases';
17
+ import { getAgentLinks } from './agentLinks';
18
+
19
+ type AgentOptionsMenuProps = {
20
+ agentName: string;
21
+ agentUrl: string;
22
+ agentEmail: string;
23
+ brandColorHex: string;
24
+ isAdmin?: boolean;
25
+ backgroundImage: string_url_image & string_data_url;
26
+ onShowQrCode?: () => void;
27
+ };
28
+
29
+ export function AgentOptionsMenu({
30
+ agentName,
31
+ agentUrl,
32
+ agentEmail,
33
+ brandColorHex,
34
+ isAdmin = false,
35
+ backgroundImage,
36
+ onShowQrCode,
37
+ }: AgentOptionsMenuProps) {
38
+ const [isOpen, setIsOpen] = useState(false);
39
+ const [copyFeedback, setCopyFeedback] = useState<string | null>(null);
40
+ const menuRef = useRef<HTMLDivElement>(null);
41
+
42
+ useEffect(() => {
43
+ function handleClickOutside(event: MouseEvent) {
44
+ if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
45
+ setIsOpen(false);
46
+ }
47
+ }
48
+
49
+ document.addEventListener('mousedown', handleClickOutside);
50
+ return () => document.removeEventListener('mousedown', handleClickOutside);
51
+ }, []);
52
+
53
+ const handleCopy = async (value: string, label: string) => {
54
+ try {
55
+ await navigator.clipboard.writeText(value);
56
+ setCopyFeedback(label);
57
+ setTimeout(() => setCopyFeedback(null), 2000);
58
+ } catch (error) {
59
+ console.error('Failed to copy:', error);
60
+ }
61
+ };
62
+
63
+ const links = getAgentLinks(agentName);
64
+ const editBookLink = links.find((l) => l.title === 'Edit Book')!;
65
+ const integrationLink = links.find((l) => l.title === 'Integration')!;
66
+ const historyLink = links.find((l) => l.title === 'History & Feedback')!;
67
+ const allLinksLink = links.find((l) => l.title === 'All Links')!;
68
+
69
+ const menuItems = [
70
+ {
71
+ type: 'link' as const,
72
+ href: `/agents/${encodeURIComponent(agentName)}/chat`,
73
+ icon: MessageSquareShareIcon,
74
+ label: 'Standalone Chat',
75
+ },
76
+ {
77
+ type: 'link' as const,
78
+ href: `/agents/${encodeURIComponent(agentName)}/book+chat`,
79
+ icon: SquareSplitHorizontalIcon,
80
+ label: 'Edit Book & Chat',
81
+ },
82
+ {
83
+ type: 'link' as const,
84
+ href: editBookLink.href,
85
+ icon: editBookLink.icon,
86
+ label: editBookLink.title,
87
+ },
88
+ { type: 'divider' as const },
89
+ {
90
+ type: 'link' as const,
91
+ href: integrationLink.href,
92
+ icon: integrationLink.icon,
93
+ label: integrationLink.title,
94
+ },
95
+ {
96
+ type: 'link' as const,
97
+ href: historyLink.href,
98
+ icon: historyLink.icon,
99
+ label: historyLink.title, // 'History & Feedback'
100
+ },
101
+ {
102
+ type: 'link' as const,
103
+ href: allLinksLink.href,
104
+ icon: allLinksLink.icon,
105
+ label: allLinksLink.title,
106
+ },
107
+ { type: 'divider' as const },
108
+ {
109
+ type: 'action' as const,
110
+ icon: CopyIcon,
111
+ label: copyFeedback === 'URL' ? 'Copied!' : 'Copy Agent URL',
112
+ onClick: () => handleCopy(agentUrl, 'URL'),
113
+ },
114
+ {
115
+ type: 'action' as const,
116
+ icon: MailIcon,
117
+ label: copyFeedback === 'Email' ? 'Copied!' : 'Copy Agent Email',
118
+ onClick: () => handleCopy(agentEmail, 'Email'),
119
+ },
120
+ {
121
+ type: 'action' as const,
122
+ icon: QrCodeIcon,
123
+ label: 'Show QR Code',
124
+ onClick: onShowQrCode,
125
+ },
126
+ // Admin-only items
127
+ ...(isAdmin
128
+ ? [
129
+ { type: 'divider' as const },
130
+ {
131
+ type: 'link' as const,
132
+ href: `/admin/chat-history?agentName=${encodeURIComponent(agentName)}`,
133
+ icon: MessageSquareIcon,
134
+ label: 'Chat History',
135
+ },
136
+ {
137
+ type: 'link' as const,
138
+ href: `/admin/chat-feedback?agentName=${encodeURIComponent(agentName)}`,
139
+ icon: MessageCircleQuestionIcon,
140
+ label: 'Chat Feedback',
141
+ },
142
+ {
143
+ type: 'link' as const,
144
+ href: `/agents/${encodeURIComponent(agentName)}/clone`,
145
+ icon: CopyPlusIcon,
146
+ label: 'Clone Agent',
147
+ },
148
+ {
149
+ type: 'link' as const,
150
+ href: `/agents/${encodeURIComponent(agentName)}/export`,
151
+ icon: DownloadIcon,
152
+ label: 'Export Agent',
153
+ },
154
+ // {
155
+ // type: 'link' as const,
156
+ // href: backgroundImage,
157
+ // icon: DownloadIcon,
158
+ // label: 'Download Background Image',
159
+ // },
160
+ ]
161
+ : []),
162
+ ];
163
+
164
+ return (
165
+ <div ref={menuRef} className="relative">
166
+ <button
167
+ onClick={() => setIsOpen(!isOpen)}
168
+ className="p-3 rounded-full bg-white/20 hover:bg-white/30 transition-all duration-200 backdrop-blur-sm"
169
+ style={{ backgroundColor: brandColorHex }}
170
+ aria-label="More options"
171
+ >
172
+ <MoreHorizontalIcon className="w-5 h-5 text-white" />
173
+ </button>
174
+
175
+ {isOpen && (
176
+ <div className="absolute right-0 top-full mt-2 w-56 bg-white rounded-xl shadow-2xl border border-gray-100 py-2 z-50 animate-in fade-in slide-in-from-top-2 duration-200">
177
+ {menuItems.map((item, index) => {
178
+ if (item.type === 'divider') {
179
+ return <div key={index} className="h-px bg-gray-100 my-2" />;
180
+ }
181
+
182
+ if (item.type === 'link') {
183
+ return (
184
+ <a
185
+ key={index}
186
+ href={item.href}
187
+ className="flex items-center gap-3 px-4 py-2.5 text-gray-700 hover:bg-gray-50 transition-colors"
188
+ onClick={() => setIsOpen(false)}
189
+ >
190
+ <item.icon className="w-4 h-4 text-gray-500" />
191
+ <span className="text-sm font-medium">{item.label}</span>
192
+ </a>
193
+ );
194
+ }
195
+
196
+ return (
197
+ <button
198
+ key={index}
199
+ onClick={() => {
200
+ item.onClick?.();
201
+ if (item.label !== 'Show QR Code') {
202
+ // Keep menu open for copy feedback
203
+ }
204
+ }}
205
+ className="flex items-center gap-3 px-4 py-2.5 text-gray-700 hover:bg-gray-50 transition-colors w-full text-left"
206
+ >
207
+ <item.icon className="w-4 h-4 text-gray-500" />
208
+ <span className="text-sm font-medium">{item.label}</span>
209
+ </button>
210
+ );
211
+ })}
212
+ </div>
213
+ )}
214
+ </div>
215
+ );
216
+ }
@@ -0,0 +1,78 @@
1
+ 'use client';
2
+
3
+ import { Chat } from '@promptbook-local/components';
4
+ import { RemoteAgent } from '@promptbook-local/core';
5
+ import spaceTrim from 'spacetrim';
6
+ import { useCallback, useMemo } from 'react';
7
+ import { usePromise } from '@common/hooks/usePromise';
8
+ import { useRouter } from 'next/navigation';
9
+ import { string_agent_url } from '../../../../../../src/types/typeAliases';
10
+
11
+ type AgentProfileChatProps = {
12
+ agentUrl: string_agent_url;
13
+ agentName: string;
14
+ fullname: string;
15
+ };
16
+
17
+ export function AgentProfileChat({ agentUrl, agentName, fullname }: AgentProfileChatProps) {
18
+ const router = useRouter();
19
+
20
+ const agentPromise = useMemo(
21
+ () =>
22
+ RemoteAgent.connect({
23
+ agentUrl,
24
+ isVerbose: true,
25
+ }),
26
+ [agentUrl],
27
+ );
28
+
29
+ const { value: agent } = usePromise(agentPromise, [agentPromise]);
30
+
31
+ const handleMessage = useCallback(
32
+ async (message: string) => {
33
+ // Redirect to chat page with the message
34
+ router.push(`/agents/${encodeURIComponent(agentName)}/chat?message=${encodeURIComponent(message)}`);
35
+ },
36
+ [agentName, router],
37
+ );
38
+
39
+ const initialMessage = useMemo(() => {
40
+ if (!agent) {
41
+ return 'Loading...';
42
+ }
43
+ return (
44
+ agent.initialMessage ||
45
+ spaceTrim(`
46
+ Hello! I am ${fullname || agentName || 'an AI Agent'}.
47
+
48
+ [Hello](?message=Hello, can you tell me about yourself?)
49
+ `)
50
+ );
51
+ }, [agent, fullname, agentName]);
52
+
53
+ // If agent is not loaded yet, we can show a skeleton or just the default Chat structure
54
+ // But to match "same initial message", we need the agent loaded or at least the default fallback.
55
+ // The fallback above matches AgentChat.tsx default.
56
+
57
+ return (
58
+ <div className="w-full h-[400px] md:h-[500px]">
59
+ <Chat
60
+ title={`Chat with ${fullname}`}
61
+ messages={[
62
+ {
63
+ from: 'AGENT',
64
+ content: initialMessage,
65
+ date: new Date(),
66
+ id: 'initial-message',
67
+ isComplete: true
68
+ },
69
+ ]}
70
+ onMessage={handleMessage}
71
+ isSaveButtonEnabled={false}
72
+ isCopyButtonEnabled={false}
73
+ className="bg-transparent"
74
+ style={{ background: 'transparent' }}
75
+ />
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,233 @@
1
+ 'use client';
2
+
3
+ import { AgentBasicInformation, string_data_url, string_url_image } from '@promptbook-local/types';
4
+ import { RepeatIcon } from 'lucide-react';
5
+ import { useState } from 'react';
6
+ import { getAgentLinks } from './agentLinks';
7
+ import { AgentOptionsMenu } from './AgentOptionsMenu';
8
+ import { AgentProfileChat } from './AgentProfileChat';
9
+ import { AgentQrCode } from './AgentQrCode';
10
+ import { QrCodeModal } from './QrCodeModal';
11
+
12
+ type AgentProfileViewProps = {
13
+ agentName: string;
14
+ fullname: string;
15
+ personaDescription: string;
16
+ imageUrl: string | null;
17
+ agentUrl: string;
18
+ agentEmail: string;
19
+ brandColorHex: string;
20
+ brandColorLightHex: string;
21
+ brandColorDarkHex: string;
22
+ brandColorsHex: string[];
23
+ backgroundImage: string_url_image & string_data_url;
24
+ meta: AgentBasicInformation['meta'];
25
+ isAdmin?: boolean;
26
+ };
27
+
28
+ export function AgentProfileView({
29
+ agentName,
30
+ fullname,
31
+ personaDescription,
32
+ imageUrl,
33
+ agentUrl,
34
+ agentEmail,
35
+ brandColorHex,
36
+ brandColorLightHex,
37
+ brandColorDarkHex,
38
+ brandColorsHex,
39
+ backgroundImage,
40
+ meta,
41
+ isAdmin = false,
42
+ }: AgentProfileViewProps) {
43
+ const [isQrModalOpen, setIsQrModalOpen] = useState(false);
44
+ const [isFlipped, setIsFlipped] = useState(false);
45
+
46
+ // Dynamic Font Loading
47
+ const fontString = meta.font;
48
+ let fontStyle: React.CSSProperties = {};
49
+
50
+ if (fontString) {
51
+ const primaryFont = fontString.split(',')[0].trim().replace(/['"]/g, '');
52
+ const googleFontUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(
53
+ primaryFont,
54
+ )}:wght@400;600;700&display=swap`;
55
+
56
+ fontStyle = {
57
+ fontFamily: fontString,
58
+ };
59
+
60
+ // TODO: [🧠] Is this the best way to load fonts? Maybe use next/font/google?
61
+ // But next/font/google requires known font at build time or generic loader which might be tricky dynamically.
62
+ // Inserting a link tag is a simple dynamic solution.
63
+ }
64
+
65
+ return (
66
+ <>
67
+ {fontString && (
68
+ <style jsx global>{`
69
+ @import url('https://fonts.googleapis.com/css2?family=${encodeURIComponent(
70
+ fontString.split(',')[0].trim().replace(/['"]/g, ''),
71
+ )}:wght@400;600;700&display=swap');
72
+ `}</style>
73
+ )}
74
+
75
+ {/* Full-screen background with agent color */}
76
+ <div
77
+ className="min-h-[calc(100vh-60px)] w-full flex flex-col items-center justify-center p-6 md:p-12 relative overflow-hidden"
78
+ style={{
79
+ background: `url("${backgroundImage}")`,
80
+ backgroundSize: 'cover',
81
+ backgroundPosition: 'center',
82
+ ...fontStyle,
83
+ }}
84
+ >
85
+ {/* Options menu in top right */}
86
+ <div className="absolute top-4 right-4 z-10">
87
+ <AgentOptionsMenu
88
+ agentName={agentName}
89
+ agentUrl={agentUrl}
90
+ agentEmail={agentEmail}
91
+ brandColorHex={brandColorHex}
92
+ isAdmin={isAdmin}
93
+ onShowQrCode={() => setIsQrModalOpen(true)}
94
+ backgroundImage={backgroundImage}
95
+ />
96
+ </div>
97
+
98
+ {/* Main profile content */}
99
+ <div className="relative z-10 flex flex-col md:flex-row items-center gap-8 md:gap-12 max-w-5xl w-full">
100
+ {/* Agent image card (Flippable) */}
101
+ <div className="flex-shrink-0 perspective-1000 group" style={{ perspective: '1000px' }}>
102
+ <div
103
+ className="relative w-72 md:w-80 transition-all duration-700 transform-style-3d cursor-pointer"
104
+ style={{
105
+ aspectRatio: '1 / 1.62', // Golden Ratio
106
+ transformStyle: 'preserve-3d',
107
+ transform: isFlipped ? 'rotateY(180deg)' : 'rotateY(0deg)',
108
+ }}
109
+ onClick={() => setIsFlipped(!isFlipped)}
110
+ >
111
+ {/* Front of Card (Image) */}
112
+ <div
113
+ className="absolute inset-0 w-full h-full backface-hidden rounded-3xl shadow-2xl overflow-hidden border-4 border-white/20 backdrop-blur-sm"
114
+ style={{
115
+ backfaceVisibility: 'hidden',
116
+ backgroundColor: brandColorDarkHex,
117
+ boxShadow: `0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px ${brandColorLightHex}40`,
118
+ }}
119
+ >
120
+ {imageUrl ? (
121
+ // eslint-disable-next-line @next/next/no-img-element
122
+ <img src={imageUrl} alt={fullname} className="w-full h-full object-cover" />
123
+ ) : (
124
+ <div
125
+ className="w-full h-full flex items-center justify-center text-8xl font-bold text-white/80"
126
+ style={{ backgroundColor: brandColorDarkHex }}
127
+ >
128
+ {fullname.charAt(0).toUpperCase()}
129
+ </div>
130
+ )}
131
+
132
+ {/* Flip hint icon */}
133
+ <div className="absolute bottom-4 right-4 bg-black/30 p-2 rounded-full text-white/80 backdrop-blur-md opacity-0 group-hover:opacity-100 transition-opacity">
134
+ <RepeatIcon className="w-5 h-5" />
135
+ </div>
136
+ </div>
137
+
138
+ {/* Back of Card (QR Code) */}
139
+ <div
140
+ className="absolute inset-0 w-full h-full backface-hidden rounded-3xl shadow-2xl overflow-hidden border-4 border-white/20 backdrop-blur-sm flex flex-col items-center justify-center p-6"
141
+ style={{
142
+ backfaceVisibility: 'hidden',
143
+ transform: 'rotateY(180deg)',
144
+ background: `linear-gradient(135deg, ${brandColorLightHex} 0%, #ffffff 100%)`,
145
+ boxShadow: `0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px ${brandColorLightHex}40`,
146
+ }}
147
+ >
148
+ <div className="transform scale-90 md:scale-100">
149
+ <AgentQrCode
150
+ agentName={agentName}
151
+ agentUrl={agentUrl}
152
+ agentEmail={agentEmail}
153
+ personaDescription={personaDescription}
154
+ meta={meta}
155
+ />
156
+ </div>
157
+
158
+ {/* Flip hint icon */}
159
+ <div className="absolute bottom-4 right-4 bg-black/10 p-2 rounded-full text-black/50 backdrop-blur-md">
160
+ <RepeatIcon className="w-5 h-5" />
161
+ </div>
162
+ </div>
163
+ </div>
164
+ </div>
165
+
166
+ {/* Agent info */}
167
+ <div className="flex flex-col items-center md:items-start text-center md:text-left gap-6">
168
+ {/* Agent name with custom font */}
169
+ <h1
170
+ className="text-4xl md:text-5xl lg:text-6xl font-bold text-gray-900 tracking-tight"
171
+ style={{
172
+ // fontFamily: 'var(--font-poppins), Poppins, sans-serif', // <- [🧠] Should we keep this fallback or just use inherited font?
173
+ // Using inherited font from the wrapper div which has dynamic font
174
+ textShadow: '0 2px 20px rgba(255, 255, 255, 0.5)',
175
+ }}
176
+ >
177
+ {fullname}
178
+ </h1>
179
+
180
+ {/* Short description */}
181
+ <p className="text-lg md:text-xl text-gray-700 max-w-lg leading-relaxed font-medium">
182
+ {personaDescription}
183
+ </p>
184
+
185
+ {/* Chat */}
186
+ <div className="w-full">
187
+ <AgentProfileChat agentUrl={agentUrl} agentName={agentName} fullname={fullname} />
188
+ </div>
189
+
190
+ {/* Secondary Actions */}
191
+ <div className="flex flex-wrap justify-center md:justify-start items-center gap-4 md:gap-6 mt-2">
192
+ {getAgentLinks(agentName)
193
+ .filter((link) => ['Edit Book', 'Integration', 'All Links'].includes(link.title))
194
+ .map((link) => (
195
+ <a
196
+ key={link.href}
197
+ href={link.href}
198
+ className="flex items-center gap-2 text-gray-600 hover:text-gray-900 transition-colors group"
199
+ title={link.title}
200
+ >
201
+ <div className="p-2 rounded-full bg-white/40 group-hover:bg-white/60 transition-colors shadow-sm">
202
+ <link.icon className="w-5 h-5" />
203
+ </div>
204
+ <span className="font-medium text-sm">{link.title}</span>
205
+ </a>
206
+ ))}
207
+ </div>
208
+ </div>
209
+ </div>
210
+
211
+ {/* Subtle gradient overlay at bottom */}
212
+ <div
213
+ className="absolute bottom-0 left-0 right-0 h-32 pointer-events-none"
214
+ style={{
215
+ background: `linear-gradient(to top, ${brandColorDarkHex}40, transparent)`,
216
+ }}
217
+ />
218
+ </div>
219
+
220
+ {/* QR Code Modal */}
221
+ <QrCodeModal
222
+ isOpen={isQrModalOpen}
223
+ onClose={() => setIsQrModalOpen(false)}
224
+ agentName={agentName}
225
+ meta={meta}
226
+ personaDescription={personaDescription}
227
+ agentUrl={agentUrl}
228
+ agentEmail={agentEmail}
229
+ brandColorHex={brandColorHex}
230
+ />
231
+ </>
232
+ );
233
+ }
@@ -12,10 +12,10 @@ export function CloneAgentButton({ agentName }: CloneAgentButtonProps) {
12
12
 
13
13
  const handleClone = async () => {
14
14
  if (!window.confirm(`Clone agent "${agentName}"?`)) return;
15
-
15
+
16
16
  try {
17
17
  const response = await fetch(`/api/agents/${encodeURIComponent(agentName)}/clone`, { method: 'POST' });
18
-
18
+
19
19
  if (!response.ok) {
20
20
  throw new Error('Failed to clone agent');
21
21
  }
@@ -32,9 +32,9 @@ export function CloneAgentButton({ agentName }: CloneAgentButtonProps) {
32
32
  return (
33
33
  <button
34
34
  onClick={handleClone}
35
- className="flex-1 inline-flex items-center justify-center whitespace-nowrap bg-white hover:bg-gray-100 text-gray-800 px-4 py-2 rounded shadow font-semibold transition border border-gray-200"
35
+ className="inline-flex items-center justify-center whitespace-nowrap rounded-md border border-gray-300 bg-white px-3 py-1.5 text-xs font-semibold text-gray-700 shadow-sm hover:bg-gray-50"
36
36
  >
37
- <CopyIcon className="ml-2 w-4 h-4 mr-2" />
37
+ <CopyIcon className="mr-2 w-3 h-3" />
38
38
  Clone
39
39
  </button>
40
40
  );
@@ -58,7 +58,7 @@ export function InstallPwaButton() {
58
58
  <button
59
59
  type="button"
60
60
  onClick={onInstall}
61
- className="flex-1 inline-flex items-center justify-center whitespace-nowrap bg-white hover:bg-gray-100 text-gray-800 px-4 py-2 rounded shadow font-semibold transition border border-gray-200"
61
+ className="inline-flex items-center justify-center whitespace-nowrap rounded-md border border-gray-300 bg-white px-3 py-1.5 text-xs font-semibold text-gray-700 shadow-sm hover:bg-gray-50"
62
62
  style={{
63
63
  opacity: installPromptEvent ? 1 : 0.5,
64
64
  cursor: installPromptEvent ? 'pointer' : 'wait',
@@ -67,7 +67,7 @@ export function InstallPwaButton() {
67
67
  disabled={!installPromptEvent}
68
68
  >
69
69
  {/* Simple icon substitute: download arrow */}
70
- <ShoppingBagIcon className="ml-2 w-4 h-4 mr-2" />
70
+ <ShoppingBagIcon className="mr-2 w-3 h-3" />
71
71
  Install
72
72
  </button>
73
73
  );
@@ -0,0 +1,90 @@
1
+ 'use client';
2
+
3
+ import { AgentBasicInformation } from '@promptbook-local/types';
4
+ import { XIcon } from 'lucide-react';
5
+ import { useEffect } from 'react';
6
+ import { AgentQrCode } from './AgentQrCode';
7
+
8
+ type QrCodeModalProps = {
9
+ isOpen: boolean;
10
+ onClose: () => void;
11
+ agentName: string;
12
+ meta: AgentBasicInformation['meta'];
13
+ personaDescription: string;
14
+ agentUrl: string;
15
+ agentEmail: string;
16
+ brandColorHex: string;
17
+ };
18
+
19
+ export function QrCodeModal({
20
+ isOpen,
21
+ onClose,
22
+ agentName,
23
+ meta,
24
+ personaDescription,
25
+ agentUrl,
26
+ agentEmail,
27
+ brandColorHex,
28
+ }: QrCodeModalProps) {
29
+ useEffect(() => {
30
+ if (isOpen) {
31
+ document.body.style.overflow = 'hidden';
32
+ } else {
33
+ document.body.style.overflow = '';
34
+ }
35
+ return () => {
36
+ document.body.style.overflow = '';
37
+ };
38
+ }, [isOpen]);
39
+
40
+ useEffect(() => {
41
+ function handleEscape(event: KeyboardEvent) {
42
+ if (event.key === 'Escape') {
43
+ onClose();
44
+ }
45
+ }
46
+
47
+ if (isOpen) {
48
+ document.addEventListener('keydown', handleEscape);
49
+ }
50
+ return () => document.removeEventListener('keydown', handleEscape);
51
+ }, [isOpen, onClose]);
52
+
53
+ if (!isOpen) return null;
54
+
55
+ return (
56
+ <div
57
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
58
+ onClick={onClose}
59
+ >
60
+ <div
61
+ className="relative bg-white rounded-2xl shadow-2xl p-8 max-w-sm w-full mx-4 animate-in zoom-in-95 duration-200"
62
+ onClick={(e) => e.stopPropagation()}
63
+ >
64
+ <button
65
+ onClick={onClose}
66
+ className="absolute top-4 right-4 p-2 rounded-full hover:bg-gray-100 transition-colors"
67
+ aria-label="Close"
68
+ >
69
+ <XIcon className="w-5 h-5 text-gray-500" />
70
+ </button>
71
+
72
+ <h3 className="text-xl font-semibold text-gray-900 mb-6 text-center">Scan to Chat</h3>
73
+
74
+ <div className="flex justify-center">
75
+ <AgentQrCode
76
+ agentName={agentName}
77
+ meta={meta}
78
+ personaDescription={personaDescription}
79
+ agentUrl={agentUrl}
80
+ agentEmail={agentEmail}
81
+ />
82
+ </div>
83
+
84
+ <p className="mt-6 text-sm text-gray-500 text-center">
85
+ Scan this QR code to start chatting with {(meta.fullname as string) || agentName}
86
+ </p>
87
+ </div>
88
+ </div>
89
+ );
90
+ }