@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.
- package/apps/agents-server/config.ts +0 -2
- package/apps/agents-server/package-lock.json +1163 -0
- package/apps/agents-server/package.json +6 -0
- package/apps/agents-server/src/app/admin/chat-feedback/ChatFeedbackClient.tsx +79 -6
- package/apps/agents-server/src/app/admin/chat-history/ChatHistoryClient.tsx +171 -69
- package/apps/agents-server/src/app/agents/[agentName]/AgentChatWrapper.tsx +3 -1
- package/apps/agents-server/src/app/agents/[agentName]/AgentOptionsMenu.tsx +216 -0
- package/apps/agents-server/src/app/agents/[agentName]/AgentProfileChat.tsx +78 -0
- package/apps/agents-server/src/app/agents/[agentName]/AgentProfileView.tsx +233 -0
- package/apps/agents-server/src/app/agents/[agentName]/CloneAgentButton.tsx +4 -4
- package/apps/agents-server/src/app/agents/[agentName]/InstallPwaButton.tsx +2 -2
- package/apps/agents-server/src/app/agents/[agentName]/QrCodeModal.tsx +90 -0
- package/apps/agents-server/src/app/agents/[agentName]/agentLinks.tsx +80 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/book/route.ts +3 -1
- package/apps/agents-server/src/app/agents/[agentName]/api/chat/route.ts +11 -1
- package/apps/agents-server/src/app/agents/[agentName]/api/mcp/route.ts +203 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/route.ts +3 -1
- package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/systemMessage/route.ts +3 -1
- package/apps/agents-server/src/app/agents/[agentName]/api/openai/chat/completions/route.ts +3 -169
- package/apps/agents-server/src/app/agents/[agentName]/api/openai/models/route.ts +93 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/openai/v1/chat/completions/route.ts +10 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/openai/v1/models/route.ts +93 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/openrouter/chat/completions/route.ts +10 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/voice/route.ts +4 -0
- package/apps/agents-server/src/app/agents/[agentName]/chat/page.tsx +9 -2
- package/apps/agents-server/src/app/agents/[agentName]/integration/SdkCodeTabs.tsx +31 -0
- package/apps/agents-server/src/app/agents/[agentName]/integration/page.tsx +271 -30
- package/apps/agents-server/src/app/agents/[agentName]/links/page.tsx +182 -0
- package/apps/agents-server/src/app/agents/[agentName]/page.tsx +108 -165
- package/apps/agents-server/src/app/agents/[agentName]/website-integration/page.tsx +61 -0
- package/apps/agents-server/src/app/api/auth/change-password/route.ts +75 -0
- package/apps/agents-server/src/app/api/chat-feedback/export/route.ts +55 -0
- package/apps/agents-server/src/app/api/chat-history/export/route.ts +55 -0
- package/apps/agents-server/src/app/api/openai/v1/chat/completions/route.ts +6 -0
- package/apps/agents-server/src/app/api/openai/v1/models/route.ts +65 -0
- package/apps/agents-server/src/app/docs/[docId]/page.tsx +12 -32
- package/apps/agents-server/src/app/docs/page.tsx +42 -17
- package/apps/agents-server/src/app/globals.css +129 -0
- package/apps/agents-server/src/app/layout.tsx +8 -2
- package/apps/agents-server/src/app/manifest.ts +1 -1
- package/apps/agents-server/src/components/ChangePasswordDialog/ChangePasswordDialog.tsx +41 -0
- package/apps/agents-server/src/components/ChangePasswordForm/ChangePasswordForm.tsx +159 -0
- package/apps/agents-server/src/components/DocumentationContent/DocumentationContent.tsx +87 -0
- package/apps/agents-server/src/components/Header/Header.tsx +94 -38
- package/apps/agents-server/src/components/OpenMojiIcon/OpenMojiIcon.tsx +20 -0
- package/apps/agents-server/src/components/PrintButton/PrintButton.tsx +18 -0
- package/apps/agents-server/src/components/PrintHeader/PrintHeader.tsx +18 -0
- package/apps/agents-server/src/database/migrations/2025-12-0070-chat-history-source.sql +2 -0
- package/apps/agents-server/src/database/schema.ts +6 -0
- package/apps/agents-server/src/middleware.ts +1 -1
- package/apps/agents-server/src/utils/convertToCsv.ts +31 -0
- package/apps/agents-server/src/utils/handleChatCompletion.ts +355 -0
- package/apps/agents-server/src/utils/resolveInheritedAgentSource.ts +100 -0
- package/apps/agents-server/src/utils/validateApiKey.ts +128 -0
- package/apps/agents-server/tailwind.config.ts +1 -1
- package/esm/index.es.js +1188 -175
- package/esm/index.es.js.map +1 -1
- package/esm/typings/src/book-2.0/agent-source/AgentModelRequirements.d.ts +4 -0
- package/esm/typings/src/book-components/Chat/LlmChat/LlmChatProps.d.ts +5 -0
- package/esm/typings/src/commitments/CLOSED/CLOSED.d.ts +35 -0
- package/esm/typings/src/commitments/COMPONENT/COMPONENT.d.ts +28 -0
- package/esm/typings/src/commitments/FROM/FROM.d.ts +34 -0
- package/esm/typings/src/commitments/LANGUAGE/LANGUAGE.d.ts +35 -0
- package/esm/typings/src/commitments/META_COLOR/META_COLOR.d.ts +6 -0
- package/esm/typings/src/commitments/META_FONT/META_FONT.d.ts +42 -0
- package/esm/typings/src/commitments/OPEN/OPEN.d.ts +35 -0
- package/esm/typings/src/commitments/USE/USE.d.ts +53 -0
- package/esm/typings/src/commitments/USE_BROWSER/USE_BROWSER.d.ts +38 -0
- package/esm/typings/src/commitments/USE_BROWSER/USE_BROWSER.test.d.ts +1 -0
- package/esm/typings/src/commitments/USE_MCP/USE_MCP.d.ts +37 -0
- package/esm/typings/src/commitments/USE_SEARCH_ENGINE/USE_SEARCH_ENGINE.d.ts +38 -0
- package/esm/typings/src/commitments/index.d.ts +12 -1
- package/esm/typings/src/playground/playground.d.ts +3 -0
- package/esm/typings/src/utils/color/Color.d.ts +8 -0
- package/esm/typings/src/utils/color/css-colors.d.ts +1 -0
- package/esm/typings/src/version.d.ts +1 -1
- package/package.json +2 -2
- package/umd/index.umd.js +1180 -167
- package/umd/index.umd.js.map +1 -1
- 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="
|
|
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="
|
|
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="
|
|
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="
|
|
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
|
+
}
|