@promptbook/cli 0.103.0-52 → 0.103.0-53

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 (107) hide show
  1. package/apps/agents-server/README.md +1 -1
  2. package/apps/agents-server/config.ts +3 -3
  3. package/apps/agents-server/next.config.ts +1 -1
  4. package/apps/agents-server/public/sw.js +16 -0
  5. package/apps/agents-server/src/app/AddAgentButton.tsx +24 -4
  6. package/apps/agents-server/src/app/actions.ts +15 -13
  7. package/apps/agents-server/src/app/admin/chat-feedback/ChatFeedbackClient.tsx +541 -0
  8. package/apps/agents-server/src/app/admin/chat-feedback/page.tsx +22 -0
  9. package/apps/agents-server/src/app/admin/chat-history/ChatHistoryClient.tsx +532 -0
  10. package/apps/agents-server/src/app/admin/chat-history/page.tsx +21 -0
  11. package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +241 -27
  12. package/apps/agents-server/src/app/admin/models/page.tsx +22 -0
  13. package/apps/agents-server/src/app/admin/users/[userId]/UserDetailClient.tsx +131 -0
  14. package/apps/agents-server/src/app/admin/users/[userId]/page.tsx +21 -0
  15. package/apps/agents-server/src/app/admin/users/page.tsx +18 -0
  16. package/apps/agents-server/src/app/agents/[agentName]/ClearAgentChatFeedbackButton.tsx +63 -0
  17. package/apps/agents-server/src/app/agents/[agentName]/ClearAgentChatHistoryButton.tsx +63 -0
  18. package/apps/agents-server/src/app/agents/[agentName]/CloneAgentButton.tsx +41 -0
  19. package/apps/agents-server/src/app/agents/[agentName]/InstallPwaButton.tsx +74 -0
  20. package/apps/agents-server/src/app/agents/[agentName]/ServiceWorkerRegister.tsx +24 -0
  21. package/apps/agents-server/src/app/agents/[agentName]/_utils.ts +19 -0
  22. package/apps/agents-server/src/app/agents/[agentName]/api/agents/route.ts +67 -0
  23. package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +3 -0
  24. package/apps/agents-server/src/app/agents/[agentName]/api/voice/route.ts +177 -0
  25. package/apps/agents-server/src/app/agents/[agentName]/book/page.tsx +3 -0
  26. package/apps/agents-server/src/app/agents/[agentName]/book+chat/AgentBookAndChat.tsx +53 -1
  27. package/apps/agents-server/src/app/agents/[agentName]/generateAgentMetadata.ts +11 -11
  28. package/apps/agents-server/src/app/agents/[agentName]/history/RestoreVersionButton.tsx +46 -0
  29. package/apps/agents-server/src/app/agents/[agentName]/history/actions.ts +12 -0
  30. package/apps/agents-server/src/app/agents/[agentName]/history/page.tsx +62 -0
  31. package/apps/agents-server/src/app/agents/[agentName]/images/icon-256.png/route.tsx +80 -0
  32. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-fullhd.png/route.tsx +92 -0
  33. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-phone.png/route.tsx +92 -0
  34. package/apps/agents-server/src/app/agents/[agentName]/integration/page.tsx +61 -0
  35. package/apps/agents-server/src/app/agents/[agentName]/opengraph-image.tsx +102 -0
  36. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +41 -22
  37. package/apps/agents-server/src/app/api/agents/[agentName]/clone/route.ts +47 -0
  38. package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +19 -0
  39. package/apps/agents-server/src/app/api/agents/route.ts +22 -13
  40. package/apps/agents-server/src/app/api/auth/login/route.ts +6 -44
  41. package/apps/agents-server/src/app/api/chat-feedback/[id]/route.ts +38 -0
  42. package/apps/agents-server/src/app/api/chat-feedback/route.ts +157 -0
  43. package/apps/agents-server/src/app/api/chat-history/[id]/route.ts +37 -0
  44. package/apps/agents-server/src/app/api/chat-history/route.ts +147 -0
  45. package/apps/agents-server/src/app/api/federated-agents/route.ts +17 -0
  46. package/apps/agents-server/src/app/api/upload/route.ts +9 -1
  47. package/apps/agents-server/src/app/docs/[docId]/page.tsx +62 -0
  48. package/apps/agents-server/src/app/docs/page.tsx +33 -0
  49. package/apps/agents-server/src/app/layout.tsx +29 -3
  50. package/apps/agents-server/src/app/manifest.ts +109 -0
  51. package/apps/agents-server/src/app/page.tsx +8 -45
  52. package/apps/agents-server/src/app/recycle-bin/RestoreAgentButton.tsx +40 -0
  53. package/apps/agents-server/src/app/recycle-bin/actions.ts +27 -0
  54. package/apps/agents-server/src/app/recycle-bin/page.tsx +58 -0
  55. package/apps/agents-server/src/app/restricted/page.tsx +33 -0
  56. package/apps/agents-server/src/app/test/og-image/README.md +1 -0
  57. package/apps/agents-server/src/app/test/og-image/opengraph-image.tsx +37 -0
  58. package/apps/agents-server/src/app/test/og-image/page.tsx +22 -0
  59. package/apps/agents-server/src/components/Footer/Footer.tsx +175 -0
  60. package/apps/agents-server/src/components/Header/Header.tsx +445 -79
  61. package/apps/agents-server/src/components/Homepage/AgentCard.tsx +46 -14
  62. package/apps/agents-server/src/components/Homepage/AgentsList.tsx +58 -0
  63. package/apps/agents-server/src/components/Homepage/Card.tsx +1 -1
  64. package/apps/agents-server/src/components/Homepage/ExternalAgentsSection.tsx +21 -0
  65. package/apps/agents-server/src/components/Homepage/ExternalAgentsSectionClient.tsx +183 -0
  66. package/apps/agents-server/src/components/Homepage/ModelsSection.tsx +75 -0
  67. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +28 -3
  68. package/apps/agents-server/src/components/LoginDialog/LoginDialog.tsx +18 -17
  69. package/apps/agents-server/src/components/Portal/Portal.tsx +38 -0
  70. package/apps/agents-server/src/components/UsersList/UsersList.tsx +82 -131
  71. package/apps/agents-server/src/components/UsersList/useUsersAdmin.ts +139 -0
  72. package/apps/agents-server/src/database/metadataDefaults.ts +38 -6
  73. package/apps/agents-server/src/middleware.ts +146 -93
  74. package/apps/agents-server/src/tools/$provideServer.ts +2 -2
  75. package/apps/agents-server/src/utils/authenticateUser.ts +42 -0
  76. package/apps/agents-server/src/utils/chatFeedbackAdmin.ts +96 -0
  77. package/apps/agents-server/src/utils/chatHistoryAdmin.ts +96 -0
  78. package/apps/agents-server/src/utils/getEffectiveFederatedServers.ts +22 -0
  79. package/apps/agents-server/src/utils/getFederatedAgents.ts +31 -8
  80. package/apps/agents-server/src/utils/getFederatedServersFromMetadata.ts +10 -0
  81. package/apps/agents-server/src/utils/getVisibleCommitmentDefinitions.ts +12 -0
  82. package/apps/agents-server/src/utils/isUserAdmin.ts +2 -2
  83. package/apps/agents-server/vercel.json +7 -0
  84. package/esm/index.es.js +153 -2
  85. package/esm/index.es.js.map +1 -1
  86. package/esm/typings/servers.d.ts +8 -1
  87. package/esm/typings/src/_packages/components.index.d.ts +2 -0
  88. package/esm/typings/src/_packages/core.index.d.ts +6 -0
  89. package/esm/typings/src/_packages/types.index.d.ts +2 -0
  90. package/esm/typings/src/_packages/utils.index.d.ts +2 -0
  91. package/esm/typings/src/book-2.0/agent-source/AgentModelRequirements.d.ts +7 -0
  92. package/esm/typings/src/book-components/Chat/Chat/ChatProps.d.ts +4 -0
  93. package/esm/typings/src/book-components/_common/HamburgerMenu/HamburgerMenu.d.ts +12 -0
  94. package/esm/typings/src/book-components/icons/MicIcon.d.ts +8 -0
  95. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +17 -0
  96. package/esm/typings/src/commitments/MESSAGE/AgentMessageCommitmentDefinition.d.ts +28 -0
  97. package/esm/typings/src/commitments/MESSAGE/UserMessageCommitmentDefinition.d.ts +28 -0
  98. package/esm/typings/src/commitments/index.d.ts +20 -1
  99. package/esm/typings/src/execution/LlmExecutionTools.d.ts +9 -0
  100. package/esm/typings/src/llm-providers/agent/AgentLlmExecutionTools.d.ts +2 -1
  101. package/esm/typings/src/llm-providers/agent/RemoteAgent.d.ts +10 -1
  102. package/esm/typings/src/utils/normalization/normalizeMessageText.d.ts +9 -0
  103. package/esm/typings/src/utils/normalization/normalizeMessageText.test.d.ts +1 -0
  104. package/esm/typings/src/version.d.ts +1 -1
  105. package/package.json +1 -1
  106. package/umd/index.umd.js +153 -2
  107. package/umd/index.umd.js.map +1 -1
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import { CopyIcon } from 'lucide-react';
4
+ import { useRouter } from 'next/navigation';
5
+
6
+ type CloneAgentButtonProps = {
7
+ agentName: string;
8
+ };
9
+
10
+ export function CloneAgentButton({ agentName }: CloneAgentButtonProps) {
11
+ const router = useRouter();
12
+
13
+ const handleClone = async () => {
14
+ if (!window.confirm(`Clone agent "${agentName}"?`)) return;
15
+
16
+ try {
17
+ const response = await fetch(`/api/agents/${encodeURIComponent(agentName)}/clone`, { method: 'POST' });
18
+
19
+ if (!response.ok) {
20
+ throw new Error('Failed to clone agent');
21
+ }
22
+
23
+ const newAgent = await response.json();
24
+ router.push(`/${newAgent.agentName}`);
25
+ router.refresh();
26
+ } catch (error) {
27
+ alert('Failed to clone agent');
28
+ console.error(error);
29
+ }
30
+ };
31
+
32
+ return (
33
+ <button
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"
36
+ >
37
+ <CopyIcon className="ml-2 w-4 h-4 mr-2" />
38
+ Clone
39
+ </button>
40
+ );
41
+ }
@@ -0,0 +1,74 @@
1
+ 'use client';
2
+
3
+ import { TODO_any } from '@promptbook-local/types';
4
+ import { ShoppingBagIcon } from 'lucide-react';
5
+ import { useCallback, useEffect, useState } from 'react';
6
+
7
+ type BeforeInstallPromptEvent = Event & {
8
+ prompt: () => Promise<void>;
9
+ userChoice: Promise<{ outcome: 'accepted' | 'dismissed'; platform: string }>;
10
+ };
11
+
12
+ export function InstallPwaButton() {
13
+ const [installPromptEvent, setInstallPromptEvent] = useState<BeforeInstallPromptEvent | null>(null);
14
+ const [isInstalled, setIsInstalled] = useState(false);
15
+ const [hasPrompted, setHasPrompted] = useState(false);
16
+
17
+ useEffect(() => {
18
+ function handleBeforeInstallPrompt(e: Event) {
19
+ // Some browsers (Chrome) fire this event when PWA is installable
20
+ e.preventDefault();
21
+ setInstallPromptEvent(e as BeforeInstallPromptEvent);
22
+ }
23
+
24
+ function updateInstalledStatus() {
25
+ const mediaMatch = window.matchMedia('(display-mode: standalone)');
26
+ const standalone = mediaMatch.matches || (window.navigator as TODO_any).standalone === true;
27
+ setIsInstalled(standalone);
28
+ }
29
+
30
+ window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
31
+ updateInstalledStatus();
32
+ window.matchMedia('(display-mode: standalone)').addEventListener('change', updateInstalledStatus);
33
+
34
+ return () => {
35
+ window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
36
+ window.matchMedia('(display-mode: standalone)').removeEventListener('change', updateInstalledStatus);
37
+ };
38
+ }, []);
39
+
40
+ const onInstall = useCallback(async () => {
41
+ if (!installPromptEvent) return;
42
+ try {
43
+ installPromptEvent.prompt();
44
+ setHasPrompted(true);
45
+ const choice = await installPromptEvent.userChoice.catch(() => null);
46
+ if (choice?.outcome === 'accepted') {
47
+ setIsInstalled(true);
48
+ }
49
+ } finally {
50
+ // Clear stored event so button hides if dismissed
51
+ setInstallPromptEvent(null);
52
+ }
53
+ }, [installPromptEvent]);
54
+
55
+ if (isInstalled || (!installPromptEvent && hasPrompted)) return null;
56
+
57
+ return (
58
+ <button
59
+ type="button"
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"
62
+ style={{
63
+ opacity: installPromptEvent ? 1 : 0.5,
64
+ cursor: installPromptEvent ? 'pointer' : 'wait',
65
+ }}
66
+ aria-label="Install App"
67
+ disabled={!installPromptEvent}
68
+ >
69
+ {/* Simple icon substitute: download arrow */}
70
+ <ShoppingBagIcon className="ml-2 w-4 h-4 mr-2" />
71
+ Install
72
+ </button>
73
+ );
74
+ }
@@ -0,0 +1,24 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+
5
+ type ServiceWorkerRegisterProps = {
6
+ scope: string;
7
+ };
8
+
9
+ export function ServiceWorkerRegister({ scope }: ServiceWorkerRegisterProps) {
10
+ useEffect(() => {
11
+ if ('serviceWorker' in navigator) {
12
+ navigator.serviceWorker
13
+ .register('/sw.js', { scope })
14
+ .then((registration) => {
15
+ console.log('Service Worker registered with scope:', registration.scope);
16
+ })
17
+ .catch((error) => {
18
+ console.error('Service Worker registration failed:', error);
19
+ });
20
+ }
21
+ }, [scope]);
22
+
23
+ return null;
24
+ }
@@ -0,0 +1,19 @@
1
+ import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
2
+ import { parseAgentSource } from '@promptbook-local/core';
3
+
4
+ export const AGENT_ACTIONS = ['Emails', 'Web chat', 'Read documents', 'Browser', 'WhatsApp', '<Coding/>'];
5
+
6
+ export async function getAgentName(params: Promise<{ agentName: string }>) {
7
+ const { agentName } = await params;
8
+ return decodeURIComponent(agentName);
9
+ }
10
+
11
+ export async function getAgentProfile(agentName: string) {
12
+ const collection = await $provideAgentCollectionForServer();
13
+ const agentSource = await collection.getAgentSource(agentName);
14
+ return parseAgentSource(agentSource);
15
+ }
16
+
17
+ /**
18
+ * TODO: Split to multiple files
19
+ */
@@ -0,0 +1,67 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ export const dynamic = 'force-dynamic';
4
+
5
+ export async function GET(
6
+ request: NextRequest,
7
+ { params }: { params: Promise<{ agentName: string }> }
8
+ ) {
9
+ try {
10
+ const { agentName } = await params;
11
+ // agentName is likely the federated server URL (e.g., "https://s6.ptbk.io")
12
+ // It comes decoded from the URL params if it was encoded in the request path
13
+
14
+ let serverUrl = agentName;
15
+
16
+ // If the serverUrl doesn't look like a URL, it might be just a hostname or something else
17
+ // But the requirement says we look for /agents/[federated-server]/api/agents
18
+ // The client will likely pass the full URL or hostname.
19
+ // We'll assume if it doesn't start with http, we might need to prepend it, or it's invalid.
20
+ // However, the current federated servers list contains full URLs.
21
+
22
+ // If it was somehow double encoded or something, we might need to handle it, but standard Next.js behavior is single decode.
23
+
24
+ if (!serverUrl.startsWith('http')) {
25
+ // Maybe it is just a hostname?
26
+ // Let's try to assume https if missing
27
+ if (serverUrl.includes('.')) {
28
+ serverUrl = `https://${serverUrl}`;
29
+ } else {
30
+ return NextResponse.json(
31
+ { error: 'Invalid federated server URL' },
32
+ { status: 400 }
33
+ );
34
+ }
35
+ }
36
+
37
+ // Normalize URL (remove trailing slash)
38
+ serverUrl = serverUrl.replace(/\/$/, '');
39
+
40
+ const response = await fetch(`${serverUrl}/api/agents`, {
41
+ // Forward relevant headers if necessary, or just basic fetch
42
+ headers: {
43
+ 'Content-Type': 'application/json',
44
+ // Add any other needed headers
45
+ },
46
+ next: { revalidate: 600 }, // Cache for 10 minutes
47
+ });
48
+
49
+ if (!response.ok) {
50
+ console.warn(`Proxy failed to fetch agents from ${serverUrl}: ${response.status} ${response.statusText}`);
51
+ return NextResponse.json(
52
+ { error: `Failed to fetch from ${serverUrl}` },
53
+ { status: response.status }
54
+ );
55
+ }
56
+
57
+ const data = await response.json();
58
+ return NextResponse.json(data);
59
+
60
+ } catch (error) {
61
+ console.error('Proxy error fetching federated agents:', error);
62
+ return NextResponse.json(
63
+ { error: 'Internal Server Error' },
64
+ { status: 500 }
65
+ );
66
+ }
67
+ }
@@ -1,3 +1,4 @@
1
+ import { getMetadata } from '@/src/database/getMetadata';
1
2
  import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
2
3
  import { computeAgentHash, parseAgentSource } from '@promptbook-local/core';
3
4
  import { serializeError } from '@promptbook-local/utils';
@@ -26,6 +27,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
26
27
  const agentSource = await collection.getAgentSource(agentName);
27
28
  const agentProfile = parseAgentSource(agentSource);
28
29
  const agentHash = computeAgentHash(agentSource);
30
+ const isVoiceCallingEnabled = (await getMetadata('IS_VOICE_CALLING_ENABLED')) === 'true';
29
31
 
30
32
  return new Response(
31
33
  JSON.stringify(
@@ -33,6 +35,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
33
35
  ...agentProfile,
34
36
  agentHash,
35
37
  parameters: [], // <- TODO: [😰] Implement parameters
38
+ isVoiceCallingEnabled, // [✨✷] Add voice calling status
36
39
  },
37
40
  // <- TODO: [🐱‍🚀] Rename `serializeError` to `errorToJson`
38
41
  null,
@@ -0,0 +1,177 @@
1
+ import { $getTableName } from '@/src/database/$getTableName';
2
+ import { $provideSupabaseForServer } from '@/src/database/$provideSupabaseForServer';
3
+ import { getMetadata } from '@/src/database/getMetadata';
4
+ import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
5
+ import { $provideOpenAiAssistantExecutionToolsForServer } from '@/src/tools/$provideOpenAiAssistantExecutionToolsForServer';
6
+ import { Agent, computeAgentHash, PROMPTBOOK_ENGINE_VERSION } from '@promptbook-local/core';
7
+ import { computeHash, serializeError } from '@promptbook-local/utils';
8
+ import { assertsError } from '../../../../../../../../src/errors/assertsError';
9
+
10
+ export const maxDuration = 300;
11
+
12
+ export async function OPTIONS(request: Request) {
13
+ return new Response(null, {
14
+ status: 200,
15
+ headers: {
16
+ 'Access-Control-Allow-Origin': '*',
17
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
18
+ 'Access-Control-Allow-Headers': 'Content-Type',
19
+ },
20
+ });
21
+ }
22
+
23
+ export async function POST(request: Request, { params }: { params: Promise<{ agentName: string }> }) {
24
+ // Check if voice calling is enabled
25
+ const isVoiceCallingEnabled = (await getMetadata('IS_VOICE_CALLING_ENABLED')) === 'true';
26
+ if (!isVoiceCallingEnabled) {
27
+ return new Response(JSON.stringify({ error: 'Voice calling is disabled on this server' }), {
28
+ status: 403,
29
+ headers: { 'Content-Type': 'application/json' },
30
+ });
31
+ }
32
+
33
+ let { agentName } = await params;
34
+ agentName = decodeURIComponent(agentName);
35
+
36
+ // Note: Parse FormData for audio file
37
+ const formData = await request.formData();
38
+ const audioFile = formData.get('audio') as File | null;
39
+ const threadString = formData.get('thread') as string | null;
40
+ const thread = threadString ? JSON.parse(threadString) : undefined;
41
+ const messageContext = formData.get('message') as string | null; // Optional text context or previous message?
42
+
43
+ if (!audioFile) {
44
+ return new Response(JSON.stringify({ error: 'No audio file provided' }), {
45
+ status: 400,
46
+ headers: { 'Content-Type': 'application/json' },
47
+ });
48
+ }
49
+
50
+ try {
51
+ const collection = await $provideAgentCollectionForServer();
52
+ const openAiAssistantExecutionTools = await $provideOpenAiAssistantExecutionToolsForServer();
53
+ const agentSource = await collection.getAgentSource(agentName);
54
+ const agent = new Agent({
55
+ isVerbose: true,
56
+ executionTools: {
57
+ llm: openAiAssistantExecutionTools,
58
+ },
59
+ agentSource,
60
+ });
61
+
62
+ // 1. Transcribe Audio (STT)
63
+ const client = await openAiAssistantExecutionTools.getClient();
64
+ const transcription = await client.audio.transcriptions.create({
65
+ file: audioFile,
66
+ model: 'whisper-1',
67
+ });
68
+ const message = transcription.text;
69
+
70
+ // --- Common Chat Logic Start (TODO: Extract) ---
71
+
72
+ const agentHash = computeAgentHash(agentSource);
73
+ const userAgent = request.headers.get('user-agent');
74
+ const ip =
75
+ request.headers.get('x-forwarded-for') ||
76
+ request.headers.get('x-real-ip') ||
77
+ request.headers.get('x-client-ip');
78
+ const language = request.headers.get('accept-language');
79
+ const platform = userAgent ? userAgent.match(/\(([^)]+)\)/)?.[1] : undefined;
80
+
81
+ // Identify and Record User Message
82
+ const userMessageContent = {
83
+ role: 'USER',
84
+ content: message,
85
+ isVoiceCall: true, // Mark as voice call
86
+ };
87
+ const supabase = $provideSupabaseForServer();
88
+ await supabase.from(await $getTableName('ChatHistory')).insert({
89
+ createdAt: new Date().toISOString(),
90
+ messageHash: computeHash(userMessageContent),
91
+ previousMessageHash: null,
92
+ agentName,
93
+ agentHash,
94
+ message: userMessageContent,
95
+ promptbookEngineVersion: PROMPTBOOK_ENGINE_VERSION,
96
+ url: request.url,
97
+ ip,
98
+ userAgent,
99
+ language,
100
+ platform,
101
+ });
102
+
103
+ // Call Agent
104
+ const response = await agent.callChatModel({
105
+ title: `Voice Chat with agent ${agentName}`,
106
+ parameters: {},
107
+ modelRequirements: {
108
+ modelVariant: 'CHAT',
109
+ },
110
+ content: message,
111
+ thread,
112
+ });
113
+
114
+ const agentMessageContent = {
115
+ role: 'MODEL',
116
+ content: response.content,
117
+ isVoiceCall: true,
118
+ };
119
+
120
+ // Record Agent Message
121
+ await supabase.from(await $getTableName('ChatHistory')).insert({
122
+ createdAt: new Date().toISOString(),
123
+ messageHash: computeHash(agentMessageContent),
124
+ previousMessageHash: computeHash(userMessageContent),
125
+ agentName,
126
+ agentHash,
127
+ message: agentMessageContent,
128
+ promptbookEngineVersion: PROMPTBOOK_ENGINE_VERSION,
129
+ url: request.url,
130
+ ip,
131
+ userAgent,
132
+ language,
133
+ platform,
134
+ });
135
+
136
+ // Learning
137
+ const newAgentSource = agent.agentSource.value;
138
+ if (newAgentSource !== agentSource) {
139
+ await collection.updateAgentSource(agentName, newAgentSource);
140
+ }
141
+
142
+ // --- Common Chat Logic End ---
143
+
144
+ // 2. Synthesize Audio (TTS)
145
+ const mp3 = await client.audio.speech.create({
146
+ model: 'tts-1',
147
+ voice: 'alloy',
148
+ input: response.content,
149
+ });
150
+
151
+ const buffer = Buffer.from(await mp3.arrayBuffer());
152
+ const base64Audio = buffer.toString('base64');
153
+
154
+ return new Response(
155
+ JSON.stringify({
156
+ userMessage: message,
157
+ agentMessage: response.content,
158
+ audio: base64Audio,
159
+ audioFormat: 'mp3',
160
+ }),
161
+ {
162
+ status: 200,
163
+ headers: {
164
+ 'Content-Type': 'application/json',
165
+ 'Access-Control-Allow-Origin': '*',
166
+ },
167
+ },
168
+ );
169
+ } catch (error) {
170
+ assertsError(error);
171
+ console.error(error);
172
+ return new Response(JSON.stringify(serializeError(error), null, 4), {
173
+ status: 400,
174
+ headers: { 'Content-Type': 'application/json' },
175
+ });
176
+ }
177
+ }
@@ -5,8 +5,11 @@ import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentColle
5
5
  import { isUserAdmin } from '@/src/utils/isUserAdmin';
6
6
  import { headers } from 'next/headers';
7
7
  import { $sideEffect } from '../../../../../../../src/utils/organization/$sideEffect';
8
+ import { generateAgentMetadata } from '../generateAgentMetadata';
8
9
  import { BookEditorWrapper } from './BookEditorWrapper';
9
10
 
11
+ export const generateMetadata = generateAgentMetadata;
12
+
10
13
  export default async function AgentBookPage({ params }: { params: Promise<{ agentName: string }> }) {
11
14
  $sideEffect(headers());
12
15
 
@@ -2,8 +2,10 @@
2
2
 
3
3
  import { ResizablePanelsAuto } from '@common/components/ResizablePanelsAuto/ResizablePanelsAuto';
4
4
  import { string_agent_url, string_book } from '@promptbook-local/types';
5
- import { BookEditorWrapper } from '../book/BookEditorWrapper';
5
+ import { Book, MessageSquare } from 'lucide-react';
6
+ import { useEffect, useState } from 'react';
6
7
  import { AgentChatWrapper } from '../AgentChatWrapper';
8
+ import { BookEditorWrapper } from '../book/BookEditorWrapper';
7
9
 
8
10
  type AgentBookAndChatProps = {
9
11
  agentName: string;
@@ -13,6 +15,56 @@ type AgentBookAndChatProps = {
13
15
 
14
16
  export function AgentBookAndChat(props: AgentBookAndChatProps) {
15
17
  const { agentName, initialAgentSource, agentUrl } = props;
18
+ const [isMobile, setIsMobile] = useState(false);
19
+ const [activeTab, setActiveTab] = useState<'book' | 'chat'>('chat');
20
+ const [isMounted, setIsMounted] = useState(false);
21
+
22
+ useEffect(() => {
23
+ setIsMounted(true);
24
+ const checkMobile = () => setIsMobile(window.innerWidth < 1024);
25
+ checkMobile();
26
+ window.addEventListener('resize', checkMobile);
27
+ return () => window.removeEventListener('resize', checkMobile);
28
+ }, []);
29
+
30
+ if (!isMounted) {
31
+ return <div className="w-full h-full bg-white" />;
32
+ }
33
+
34
+ if (isMobile) {
35
+ return (
36
+ <div className="flex flex-col h-full w-full bg-white">
37
+ <div className="flex-grow overflow-hidden relative">
38
+ <div className={`w-full h-full ${activeTab === 'book' ? 'block' : 'hidden'}`}>
39
+ <BookEditorWrapper agentName={agentName} initialAgentSource={initialAgentSource} />
40
+ </div>
41
+ <div className={`w-full h-full ${activeTab === 'chat' ? 'block' : 'hidden'}`}>
42
+ <AgentChatWrapper agentUrl={agentUrl} />
43
+ </div>
44
+ </div>
45
+ <div className="flex-shrink-0 h-16 bg-white border-t border-gray-200 flex shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)] z-10">
46
+ <button
47
+ onClick={() => setActiveTab('book')}
48
+ className={`flex-1 flex flex-col items-center justify-center gap-1 transition-colors ${
49
+ activeTab === 'book' ? 'text-blue-600 bg-blue-50/50' : 'text-gray-500 hover:bg-gray-50'
50
+ }`}
51
+ >
52
+ <Book className="w-5 h-5" />
53
+ <span className="text-xs font-medium">Info</span>
54
+ </button>
55
+ <button
56
+ onClick={() => setActiveTab('chat')}
57
+ className={`flex-1 flex flex-col items-center justify-center gap-1 transition-colors ${
58
+ activeTab === 'chat' ? 'text-blue-600 bg-blue-50/50' : 'text-gray-500 hover:bg-gray-50'
59
+ }`}
60
+ >
61
+ <MessageSquare className="w-5 h-5" />
62
+ <span className="text-xs font-medium">Chat</span>
63
+ </button>
64
+ </div>
65
+ </div>
66
+ );
67
+ }
16
68
 
17
69
  return (
18
70
  <ResizablePanelsAuto name={`agent-book-and-chat-${agentName}`} className="w-full h-full">
@@ -1,15 +1,13 @@
1
- import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
2
- import { parseAgentSource } from '@promptbook-local/core';
1
+ import { $provideServer } from '@/src/tools/$provideServer';
3
2
  import { Metadata } from 'next';
3
+ import { getAgentName, getAgentProfile } from './_utils';
4
4
 
5
5
  export async function generateAgentMetadata({ params }: { params: Promise<{ agentName: string }> }): Promise<Metadata> {
6
- let { agentName } = await params;
7
- agentName = decodeURIComponent(agentName);
6
+ const { publicUrl } = await $provideServer();
7
+ const agentName = await getAgentName(params);
8
8
 
9
9
  try {
10
- const collection = await $provideAgentCollectionForServer();
11
- const agentSource = await collection.getAgentSource(agentName);
12
- const agentProfile = parseAgentSource(agentSource);
10
+ const agentProfile = await getAgentProfile(agentName);
13
11
 
14
12
  const title = agentProfile.meta.fullname || agentProfile.agentName;
15
13
  const description = agentProfile.meta.description || agentProfile.personaDescription || undefined;
@@ -17,22 +15,24 @@ export async function generateAgentMetadata({ params }: { params: Promise<{ agen
17
15
  // Extract image from meta
18
16
  const image = agentProfile.meta.image;
19
17
 
20
- return {
18
+ const metadata = {
19
+ metadataBase: publicUrl,
21
20
  title,
22
21
  description,
22
+ icons: image ? { icon: image } : undefined,
23
23
  openGraph: {
24
24
  title,
25
25
  description,
26
26
  type: 'website',
27
- images: image ? [{ url: image }] : undefined,
28
27
  },
29
28
  twitter: {
30
29
  card: 'summary_large_image',
31
30
  title,
32
31
  description,
33
- images: image ? [image] : undefined,
34
32
  },
35
- };
33
+ } satisfies Metadata;
34
+
35
+ return metadata;
36
36
  } catch (error) {
37
37
  console.warn(`Failed to generate metadata for agent ${agentName}`, error);
38
38
  return {
@@ -0,0 +1,46 @@
1
+ 'use client';
2
+
3
+ import { RefreshCcwIcon } from 'lucide-react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { useState } from 'react';
6
+ import { restoreAgentVersion } from './actions';
7
+
8
+ type RestoreVersionButtonProps = {
9
+ agentName: string;
10
+ historyId: number;
11
+ };
12
+
13
+ export function RestoreVersionButton({ agentName, historyId }: RestoreVersionButtonProps) {
14
+ const router = useRouter();
15
+ const [isRestoring, setIsRestoring] = useState(false);
16
+
17
+ const handleRestore = async () => {
18
+ if (!confirm('Are you sure you want to restore this version? Current changes will be saved to history.')) {
19
+ return;
20
+ }
21
+
22
+ try {
23
+ setIsRestoring(true);
24
+ await restoreAgentVersion(agentName, historyId);
25
+ router.refresh();
26
+ router.push(`/agents/${agentName}`);
27
+ } catch (error) {
28
+ console.error('Failed to restore version:', error);
29
+ alert('Failed to restore version');
30
+ } finally {
31
+ setIsRestoring(false);
32
+ }
33
+ };
34
+
35
+ return (
36
+ <button
37
+ onClick={handleRestore}
38
+ disabled={isRestoring}
39
+ className="flex items-center gap-2 px-3 py-1 text-sm bg-white border border-gray-300 rounded hover:bg-gray-50 text-gray-700 disabled:opacity-50"
40
+ title="Restore this version"
41
+ >
42
+ <RefreshCcwIcon className={`w-3 h-3 ${isRestoring ? 'animate-spin' : ''}`} />
43
+ {isRestoring ? 'Restoring...' : 'Restore'}
44
+ </button>
45
+ );
46
+ }
@@ -0,0 +1,12 @@
1
+ 'use server';
2
+
3
+ import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
4
+ import { revalidatePath } from 'next/cache';
5
+
6
+ export async function restoreAgentVersion(agentName: string, historyId: number) {
7
+ const collection = await $provideAgentCollectionForServer();
8
+ await collection.restoreAgent(historyId);
9
+
10
+ revalidatePath(`/agents/${agentName}`);
11
+ revalidatePath(`/agents/${agentName}/history`);
12
+ }