@promptbook/cli 0.104.0-8 → 0.104.0-9
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/src/app/AddAgentButton.tsx +45 -19
- package/apps/agents-server/src/app/actions.ts +5 -0
- package/apps/agents-server/src/app/admin/browser-test/BrowserTestClient.tsx +23 -0
- package/apps/agents-server/src/app/agents/[agentName]/_utils.ts +2 -1
- package/apps/agents-server/src/app/agents/[agentName]/api/book/route.ts +4 -2
- package/apps/agents-server/src/app/agents/[agentName]/history/page.tsx +10 -3
- package/apps/agents-server/src/app/agents/[agentName]/images/default-avatar.png/route.ts +7 -3
- package/apps/agents-server/src/app/api/agents/[agentName]/clone/route.ts +3 -2
- package/apps/agents-server/src/app/api/agents/[agentName]/restore/route.ts +2 -1
- package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +2 -1
- package/apps/agents-server/src/app/api/browser-test/scroll-facebook/route.ts +62 -0
- package/apps/agents-server/src/components/AgentProfile/AgentProfile.tsx +8 -11
- package/apps/agents-server/src/components/AgentProfile/AgentProfileImage.tsx +79 -0
- package/apps/agents-server/src/components/NewAgentDialog/NewAgentDialog.tsx +88 -0
- package/apps/agents-server/src/database/migrations/2025-12-0820-agent-history-permanent-id.sql +29 -0
- package/apps/agents-server/src/database/schema.ts +6 -3
- package/esm/index.es.js +7 -2
- package/esm/index.es.js.map +1 -1
- package/esm/typings/src/book-2.0/agent-source/createAgentModelRequirementsWithCommitments.closed.test.d.ts +1 -0
- package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +10 -6
- package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentsDatabaseSchema.d.ts +6 -3
- package/esm/typings/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/umd/index.umd.js +7 -2
- package/umd/index.umd.js.map +1 -1
|
@@ -1,41 +1,67 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { string_book } from '@promptbook-local/types';
|
|
3
4
|
import { useRouter } from 'next/navigation';
|
|
4
5
|
import { useState } from 'react';
|
|
5
6
|
import { Card } from '../components/Homepage/Card';
|
|
6
|
-
import {
|
|
7
|
+
import { NewAgentDialog } from '../components/NewAgentDialog/NewAgentDialog';
|
|
8
|
+
import { $createAgentFromBookAction, $generateAgentBoilerplateAction } from './actions';
|
|
7
9
|
|
|
8
10
|
export function AddAgentButton() {
|
|
9
11
|
const router = useRouter();
|
|
10
12
|
const [isLoading, setIsLoading] = useState(false);
|
|
13
|
+
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
14
|
+
const [agentSource, setAgentSource] = useState<string_book>('' as string_book);
|
|
11
15
|
|
|
12
16
|
const handleAddAgent = async () => {
|
|
13
17
|
setIsLoading(true);
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
try {
|
|
19
|
+
const boilerplate = await $generateAgentBoilerplateAction();
|
|
20
|
+
setAgentSource(boilerplate);
|
|
21
|
+
setIsDialogOpen(true);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('Failed to generate agent boilerplate', error);
|
|
24
|
+
// TODO: Add proper error handling and UI feedback
|
|
25
|
+
} finally {
|
|
26
|
+
setIsLoading(false);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const handleCreate = async (source: string_book) => {
|
|
31
|
+
// Note: [🧠] Logic for creation is now handled inside the dialog (waiting for promise), here we just handle navigation
|
|
32
|
+
const { permanentId } = await $createAgentFromBookAction(source);
|
|
33
|
+
|
|
16
34
|
if (permanentId) {
|
|
17
35
|
router.push(`/agents/${permanentId}`);
|
|
18
36
|
} else {
|
|
19
37
|
router.refresh();
|
|
20
|
-
setIsLoading(false);
|
|
21
38
|
}
|
|
22
39
|
};
|
|
23
40
|
|
|
24
41
|
return (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
<>
|
|
43
|
+
<div
|
|
44
|
+
onClick={isLoading ? undefined : handleAddAgent}
|
|
45
|
+
className={`cursor-pointer h-full group ${isLoading ? 'pointer-events-none' : ''}`}
|
|
46
|
+
>
|
|
47
|
+
<Card className="flex items-center justify-center text-lg font-medium text-gray-500 group-hover:text-blue-500 group-hover:border-blue-400 border-dashed border-2">
|
|
48
|
+
{isLoading ? (
|
|
49
|
+
<>
|
|
50
|
+
<span className="mr-2 inline-block h-4 w-4 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
|
51
|
+
Preparing...
|
|
52
|
+
</>
|
|
53
|
+
) : (
|
|
54
|
+
'+ Add New Agent'
|
|
55
|
+
)}
|
|
56
|
+
</Card>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<NewAgentDialog
|
|
60
|
+
isOpen={isDialogOpen}
|
|
61
|
+
onClose={() => setIsDialogOpen(false)}
|
|
62
|
+
initialAgentSource={agentSource}
|
|
63
|
+
onCreate={handleCreate}
|
|
64
|
+
/>
|
|
65
|
+
</>
|
|
40
66
|
);
|
|
41
67
|
}
|
|
@@ -25,6 +25,11 @@ export async function $createAgentAction(): Promise<{ agentName: string_agent_na
|
|
|
25
25
|
return { agentName, permanentId };
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export async function $generateAgentBoilerplateAction(): Promise<string_book> {
|
|
29
|
+
const namePool = (await getMetadata('NAME_POOL')) || 'ENGLISH';
|
|
30
|
+
return $generateBookBoilerplate({ namePool });
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
export async function $createAgentFromBookAction(bookContent: string_book): Promise<{ agentName: string_agent_name; permanentId: string_agent_permanent_id }> {
|
|
29
34
|
// TODO: [👹] Check permissions here
|
|
30
35
|
if (!(await isUserAdmin())) {
|
|
@@ -5,6 +5,7 @@ import { Card } from '../../../components/Homepage/Card';
|
|
|
5
5
|
|
|
6
6
|
export function BrowserTestClient() {
|
|
7
7
|
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
|
8
|
+
const [facebookStreamUrl, setFacebookStreamUrl] = useState<string | null>(null);
|
|
8
9
|
|
|
9
10
|
useEffect(() => {
|
|
10
11
|
return () => {
|
|
@@ -17,6 +18,7 @@ export function BrowserTestClient() {
|
|
|
17
18
|
const [error, setError] = useState<string | null>(null);
|
|
18
19
|
|
|
19
20
|
const handleTakeScreenshot = async () => {
|
|
21
|
+
setFacebookStreamUrl(null);
|
|
20
22
|
setIsLoading(true);
|
|
21
23
|
setError(null);
|
|
22
24
|
try {
|
|
@@ -42,6 +44,12 @@ export function BrowserTestClient() {
|
|
|
42
44
|
}
|
|
43
45
|
};
|
|
44
46
|
|
|
47
|
+
const handleScrollFacebook = () => {
|
|
48
|
+
setImageUrl(null);
|
|
49
|
+
setError(null);
|
|
50
|
+
setFacebookStreamUrl(`/api/browser-test/scroll-facebook?t=${Date.now()}`);
|
|
51
|
+
};
|
|
52
|
+
|
|
45
53
|
return (
|
|
46
54
|
<div className="container mx-auto px-4 py-8 space-y-6">
|
|
47
55
|
<div className="mt-20 mb-4 flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
|
|
@@ -63,6 +71,13 @@ export function BrowserTestClient() {
|
|
|
63
71
|
>
|
|
64
72
|
{isLoading ? 'Taking Screenshot...' : 'Take Screenshot'}
|
|
65
73
|
</button>
|
|
74
|
+
<button
|
|
75
|
+
onClick={handleScrollFacebook}
|
|
76
|
+
disabled={isLoading}
|
|
77
|
+
className="ml-2 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50"
|
|
78
|
+
>
|
|
79
|
+
Scroll Facebook
|
|
80
|
+
</button>
|
|
66
81
|
</div>
|
|
67
82
|
|
|
68
83
|
{error && (
|
|
@@ -79,6 +94,14 @@ export function BrowserTestClient() {
|
|
|
79
94
|
<img src={imageUrl} alt="Screenshot of ptbk.io" className="w-full h-auto" />
|
|
80
95
|
</div>
|
|
81
96
|
)}
|
|
97
|
+
|
|
98
|
+
{facebookStreamUrl && (
|
|
99
|
+
<div className="border rounded shadow-lg overflow-hidden">
|
|
100
|
+
<h2 className="text-xl font-semibold p-2 bg-gray-100">Facebook Scroll Stream</h2>
|
|
101
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
102
|
+
<img src={facebookStreamUrl} alt="Live stream of Facebook scrolling" className="w-full h-auto" />
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
82
105
|
</Card>
|
|
83
106
|
</div>
|
|
84
107
|
);
|
|
@@ -12,7 +12,8 @@ export async function getAgentName(params: Promise<{ agentName: string }>) {
|
|
|
12
12
|
|
|
13
13
|
export async function getAgentProfile(agentName: string) {
|
|
14
14
|
const collection = await $provideAgentCollectionForServer();
|
|
15
|
-
const
|
|
15
|
+
const agentId = await collection.getAgentPermanentId(agentName);
|
|
16
|
+
const agentSource = await collection.getAgentSource(agentId);
|
|
16
17
|
const agentProfile = parseAgentSource(agentSource);
|
|
17
18
|
return agentProfile;
|
|
18
19
|
}
|
|
@@ -13,7 +13,8 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
|
|
|
13
13
|
|
|
14
14
|
try {
|
|
15
15
|
const collection = await $provideAgentCollectionForServer();
|
|
16
|
-
const
|
|
16
|
+
const agentId = await collection.getAgentPermanentId(agentName);
|
|
17
|
+
const agentSource = await collection.getAgentSource(agentId);
|
|
17
18
|
const effectiveAgentSource = await resolveInheritedAgentSource(agentSource, collection);
|
|
18
19
|
|
|
19
20
|
return new Response(effectiveAgentSource, {
|
|
@@ -52,7 +53,8 @@ export async function PUT(request: Request, { params }: { params: Promise<{ agen
|
|
|
52
53
|
let agentSource = validateBook(agentSourceUnchecked);
|
|
53
54
|
agentSource = padBook(agentSource);
|
|
54
55
|
|
|
55
|
-
await collection.
|
|
56
|
+
const agentId = await collection.getAgentPermanentId(agentName);
|
|
57
|
+
await collection.updateAgentSource(agentId, agentSource);
|
|
56
58
|
// <- TODO: [🐱🚀] Properly type as string_book
|
|
57
59
|
|
|
58
60
|
return new Response(
|
|
@@ -10,7 +10,8 @@ export const metadata = {
|
|
|
10
10
|
export default async function AgentHistoryPage({ params }: { params: Promise<{ agentName: string }> }) {
|
|
11
11
|
const { agentName } = await params;
|
|
12
12
|
const collection = await $provideAgentCollectionForServer();
|
|
13
|
-
const
|
|
13
|
+
const agentId = await collection.getAgentPermanentId(agentName);
|
|
14
|
+
const history = await collection.listAgentHistory(agentId);
|
|
14
15
|
|
|
15
16
|
return (
|
|
16
17
|
<div className="container mx-auto p-6 max-w-4xl">
|
|
@@ -21,7 +22,10 @@ export default async function AgentHistoryPage({ params }: { params: Promise<{ a
|
|
|
21
22
|
<div>
|
|
22
23
|
<h1 className="text-3xl font-bold text-gray-900">History: {agentName}</h1>
|
|
23
24
|
<p className="text-gray-600">
|
|
24
|
-
Previous versions of this agent.
|
|
25
|
+
Previous versions of this agent.{' '}
|
|
26
|
+
<Link href={`/agents/${agentName}`} className="text-blue-600 hover:underline">
|
|
27
|
+
Back to agent
|
|
28
|
+
</Link>
|
|
25
29
|
</p>
|
|
26
30
|
</div>
|
|
27
31
|
</header>
|
|
@@ -47,7 +51,10 @@ export default async function AgentHistoryPage({ params }: { params: Promise<{ a
|
|
|
47
51
|
Version {history.length - index}
|
|
48
52
|
</h3>
|
|
49
53
|
<p className="text-sm text-gray-500">
|
|
50
|
-
Hash:
|
|
54
|
+
Hash:{' '}
|
|
55
|
+
<code className="bg-gray-100 px-1 rounded">
|
|
56
|
+
{item.agentHash.substring(0, 8)}
|
|
57
|
+
</code>
|
|
51
58
|
</p>
|
|
52
59
|
</div>
|
|
53
60
|
<RestoreVersionButton agentName={agentName} historyId={item.id} />
|
|
@@ -27,9 +27,13 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|
|
27
27
|
try {
|
|
28
28
|
agentSource = await collection.getAgentSource(agentName);
|
|
29
29
|
} catch (error) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return NextResponse.
|
|
30
|
+
assertsError(error);
|
|
31
|
+
|
|
32
|
+
return NextResponse.json({ error: serializeError(error) }, { status: 500 });
|
|
33
|
+
|
|
34
|
+
//> // If agent not found, redirect to pravatar with the agent name as unique identifier
|
|
35
|
+
//> const pravaratUrl = `https://i.pravatar.cc/1024?u=${encodeURIComponent(agentName)}`;
|
|
36
|
+
//> return NextResponse.redirect(pravaratUrl);
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
const agentProfile = parseAgentSource(agentSource);
|
|
@@ -8,7 +8,8 @@ export async function POST(request: Request, { params }: { params: Promise<{ age
|
|
|
8
8
|
const collection = await $provideAgentCollectionForServer();
|
|
9
9
|
|
|
10
10
|
try {
|
|
11
|
-
const
|
|
11
|
+
const agentId = await collection.getAgentPermanentId(agentName);
|
|
12
|
+
const source = await collection.getAgentSource(agentId);
|
|
12
13
|
|
|
13
14
|
// Generate new name
|
|
14
15
|
// TODO: [🧠] Better naming strategy, maybe check for collisions
|
|
@@ -18,7 +19,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ age
|
|
|
18
19
|
// eslint-disable-next-line no-constant-condition
|
|
19
20
|
while (true) {
|
|
20
21
|
try {
|
|
21
|
-
await collection.
|
|
22
|
+
await collection.getAgentPermanentId(newAgentName);
|
|
22
23
|
// If success, it means it exists, so we try next one
|
|
23
24
|
counter++;
|
|
24
25
|
newAgentName = `${agentName} (Copy ${counter})`;
|
|
@@ -8,7 +8,8 @@ export async function POST(request: Request, { params }: { params: Promise<{ age
|
|
|
8
8
|
const collection = await $provideAgentCollectionForServer();
|
|
9
9
|
|
|
10
10
|
try {
|
|
11
|
-
await collection.
|
|
11
|
+
const agentId = await collection.getAgentPermanentId(agentName);
|
|
12
|
+
await collection.restoreAgent(agentId);
|
|
12
13
|
return NextResponse.json({ success: true });
|
|
13
14
|
} catch (error) {
|
|
14
15
|
return NextResponse.json(
|
|
@@ -49,7 +49,8 @@ export async function DELETE(request: Request, { params }: { params: Promise<{ a
|
|
|
49
49
|
const collection = await $provideAgentCollectionForServer();
|
|
50
50
|
|
|
51
51
|
try {
|
|
52
|
-
await collection.
|
|
52
|
+
const agentId = await collection.getAgentPermanentId(agentName);
|
|
53
|
+
await collection.deleteAgent(agentId);
|
|
53
54
|
return NextResponse.json({ success: true });
|
|
54
55
|
} catch (error) {
|
|
55
56
|
return NextResponse.json(
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { $provideBrowserForServer } from '@/src/tools/$provideBrowserForServer';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { just } from '../../../../../../../src/utils/organization/just';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
export async function GET() {
|
|
8
|
+
const encoder = new TextEncoder();
|
|
9
|
+
const stream = new ReadableStream({
|
|
10
|
+
async start(controller) {
|
|
11
|
+
try {
|
|
12
|
+
console.log('Starting Facebook scroll session');
|
|
13
|
+
const browserContext = await $provideBrowserForServer();
|
|
14
|
+
const page = await browserContext.newPage();
|
|
15
|
+
|
|
16
|
+
await page.goto('https://www.facebook.com/');
|
|
17
|
+
console.log('Navigated to Facebook');
|
|
18
|
+
|
|
19
|
+
// Loop for streaming
|
|
20
|
+
while (just(true)) {
|
|
21
|
+
// Check for login (simple check for now)
|
|
22
|
+
// Facebook uses role="banner" for the top navigation bar when logged in
|
|
23
|
+
const isLoggedIn = await page.$('div[role="banner"]');
|
|
24
|
+
|
|
25
|
+
if (isLoggedIn) {
|
|
26
|
+
// Scroll down
|
|
27
|
+
await page.evaluate(() => window.scrollBy(0, 500));
|
|
28
|
+
// console.log('Scrolled');
|
|
29
|
+
} else {
|
|
30
|
+
// console.log('Waiting for login...');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const buffer = await page.screenshot({ type: 'jpeg', quality: 50 });
|
|
34
|
+
|
|
35
|
+
const boundary = '--myboundary';
|
|
36
|
+
const header = `\r\n${boundary}\r\nContent-Type: image/jpeg\r\nContent-Length: ${buffer.length}\r\n\r\n`;
|
|
37
|
+
|
|
38
|
+
controller.enqueue(encoder.encode(header));
|
|
39
|
+
controller.enqueue(buffer);
|
|
40
|
+
|
|
41
|
+
// Short delay
|
|
42
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('Stream error:', error);
|
|
46
|
+
try {
|
|
47
|
+
controller.close();
|
|
48
|
+
} catch (e) {
|
|
49
|
+
// Ignore error if controller is already closed
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return new NextResponse(stream, {
|
|
56
|
+
headers: {
|
|
57
|
+
'Content-Type': 'multipart/x-mixed-replace; boundary=myboundary',
|
|
58
|
+
'Cache-Control': 'no-cache',
|
|
59
|
+
Connection: 'keep-alive',
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -5,6 +5,7 @@ import { generatePlaceholderAgentProfileImageUrl } from '@promptbook-local/core'
|
|
|
5
5
|
import { AgentBasicInformation, string_agent_permanent_id } from '@promptbook-local/types';
|
|
6
6
|
import { RepeatIcon } from 'lucide-react';
|
|
7
7
|
import { useState } from 'react';
|
|
8
|
+
import { AgentProfileImage } from './AgentProfileImage';
|
|
8
9
|
import { AgentQrCode } from './AgentQrCode';
|
|
9
10
|
import { QrCodeModal } from './QrCodeModal';
|
|
10
11
|
import { useAgentBackground } from './useAgentBackground';
|
|
@@ -132,14 +133,14 @@ export function AgentProfile(props: AgentProfileProps) {
|
|
|
132
133
|
)}
|
|
133
134
|
|
|
134
135
|
{/* Main profile content */}
|
|
135
|
-
<div className="relative z-10 grid grid-cols-[auto_1fr] gap-
|
|
136
|
+
<div className="relative z-10 flex flex-col md:grid md:grid-cols-[auto_1fr] gap-y-6 md:gap-y-4 md:gap-x-12 max-w-5xl w-full items-center md:items-start">
|
|
136
137
|
{/* Agent image card (Flippable) */}
|
|
137
138
|
<div
|
|
138
|
-
className="flex-shrink-0 perspective-1000 group row-start-1 col-start-1 md:row-span-3"
|
|
139
|
+
className="flex-shrink-0 perspective-1000 group w-full md:w-auto md:row-start-1 md:col-start-1 md:row-span-3"
|
|
139
140
|
style={{ perspective: '1000px' }}
|
|
140
141
|
>
|
|
141
142
|
<div
|
|
142
|
-
className="relative w-
|
|
143
|
+
className="relative w-full md:w-80 transition-all duration-700 transform-style-3d cursor-pointer max-w-sm mx-auto md:max-w-none md:mx-0"
|
|
143
144
|
style={{
|
|
144
145
|
aspectRatio: '1 / 1.618', // Golden Ratio
|
|
145
146
|
transformStyle: 'preserve-3d',
|
|
@@ -160,14 +161,10 @@ export function AgentProfile(props: AgentProfileProps) {
|
|
|
160
161
|
// ['cornerShape' as really_any /* <- Note: `cornerShape` is non standard CSS property */]: 'squircle ',
|
|
161
162
|
}}
|
|
162
163
|
>
|
|
163
|
-
|
|
164
|
-
<img
|
|
164
|
+
<AgentProfileImage
|
|
165
165
|
src={imageUrl}
|
|
166
166
|
alt={fullname}
|
|
167
167
|
className="w-full h-full object-cover"
|
|
168
|
-
// width={1024}
|
|
169
|
-
// height={1792}
|
|
170
|
-
// <- TODO: [🤐] DRY
|
|
171
168
|
style={{
|
|
172
169
|
objectFit: 'cover',
|
|
173
170
|
backgroundImage: `url(${colorToDataUrl(brandColorLightHex)})`,
|
|
@@ -210,7 +207,7 @@ export function AgentProfile(props: AgentProfileProps) {
|
|
|
210
207
|
</div>
|
|
211
208
|
|
|
212
209
|
{/* Agent info - Header Area */}
|
|
213
|
-
<div className="row-start-1 col-start-2 flex flex-col justify-center md:justify-start
|
|
210
|
+
<div className="w-full md:w-auto md:row-start-1 md:col-start-2 flex flex-col justify-center md:justify-start items-center md:items-start h-auto gap-4 md:gap-6 text-center md:text-left">
|
|
214
211
|
{/* Agent name with custom font */}
|
|
215
212
|
<h1
|
|
216
213
|
className="text-2xl md:text-5xl lg:text-6xl font-bold text-gray-900 tracking-tight leading-tight"
|
|
@@ -228,11 +225,11 @@ export function AgentProfile(props: AgentProfileProps) {
|
|
|
228
225
|
</div>
|
|
229
226
|
|
|
230
227
|
{/* Chat Area */}
|
|
231
|
-
<div className="
|
|
228
|
+
<div className="w-full md:col-span-1 md:col-start-2 mt-4 md:mt-0">{children}</div>
|
|
232
229
|
|
|
233
230
|
{/* Secondary Actions */}
|
|
234
231
|
{!isHeadless && (
|
|
235
|
-
<div className="
|
|
232
|
+
<div className="w-full md:col-span-1 md:col-start-2 flex flex-wrap justify-center md:justify-start items-center gap-4 md:gap-6 mt-4 md:mt-2">
|
|
236
233
|
{actions}
|
|
237
234
|
</div>
|
|
238
235
|
)}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Loader2 } from 'lucide-react';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
type AgentProfileImageProps = {
|
|
5
|
+
readonly src: string;
|
|
6
|
+
readonly alt: string;
|
|
7
|
+
readonly className?: string;
|
|
8
|
+
readonly style?: React.CSSProperties;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function AgentProfileImage({ src, alt, className, style }: AgentProfileImageProps) {
|
|
12
|
+
const [imageSrc, setImageSrc] = useState<string | null>(null);
|
|
13
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
let isMounted = true;
|
|
17
|
+
let timeoutId: NodeJS.Timeout;
|
|
18
|
+
let objectUrl: string | null = null;
|
|
19
|
+
|
|
20
|
+
const fetchImage = async () => {
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetch(src);
|
|
23
|
+
if (response.ok) {
|
|
24
|
+
const blob = await response.blob();
|
|
25
|
+
if (!isMounted) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
objectUrl = URL.createObjectURL(blob);
|
|
30
|
+
setImageSrc(objectUrl);
|
|
31
|
+
setIsLoading(false);
|
|
32
|
+
} else {
|
|
33
|
+
// Retry on non-200
|
|
34
|
+
if (isMounted) {
|
|
35
|
+
timeoutId = setTimeout(fetchImage, 2000);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.warn('Failed to fetch image, retrying...', error);
|
|
40
|
+
if (isMounted) {
|
|
41
|
+
timeoutId = setTimeout(fetchImage, 2000);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
setIsLoading(true);
|
|
47
|
+
fetchImage();
|
|
48
|
+
|
|
49
|
+
return () => {
|
|
50
|
+
isMounted = false;
|
|
51
|
+
clearTimeout(timeoutId);
|
|
52
|
+
if (objectUrl) {
|
|
53
|
+
URL.revokeObjectURL(objectUrl);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}, [src]);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className={`relative ${className}`} style={style}>
|
|
60
|
+
{/* Note: We apply style to the container, so backgroundImage works here */}
|
|
61
|
+
|
|
62
|
+
{isLoading && (
|
|
63
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
64
|
+
<Loader2 className="w-8 h-8 animate-spin text-white/50" />
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
|
|
68
|
+
{imageSrc && (
|
|
69
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
70
|
+
<img
|
|
71
|
+
src={imageSrc}
|
|
72
|
+
alt={alt}
|
|
73
|
+
className="w-full h-full object-cover"
|
|
74
|
+
// We don't pass style here because it is applied to container
|
|
75
|
+
/>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { string_book } from '@promptbook-local/types';
|
|
4
|
+
import { X } from 'lucide-react';
|
|
5
|
+
import { useEffect, useState } from 'react';
|
|
6
|
+
import { BookEditor } from '../../../../../src/book-components/BookEditor/BookEditor';
|
|
7
|
+
import { Portal } from '../Portal/Portal';
|
|
8
|
+
|
|
9
|
+
type NewAgentDialogProps = {
|
|
10
|
+
isOpen: boolean;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
initialAgentSource: string_book;
|
|
13
|
+
onCreate: (agentSource: string_book) => Promise<void>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function NewAgentDialog(props: NewAgentDialogProps) {
|
|
17
|
+
const { isOpen, onClose, initialAgentSource, onCreate } = props;
|
|
18
|
+
const [agentSource, setAgentSource] = useState(initialAgentSource);
|
|
19
|
+
const [isCreating, setIsCreating] = useState(false);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
setAgentSource(initialAgentSource);
|
|
23
|
+
}, [initialAgentSource]);
|
|
24
|
+
|
|
25
|
+
if (!isOpen) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const handleCreate = async () => {
|
|
30
|
+
setIsCreating(true);
|
|
31
|
+
try {
|
|
32
|
+
await onCreate(agentSource);
|
|
33
|
+
} finally {
|
|
34
|
+
setIsCreating(false);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Portal>
|
|
40
|
+
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm animate-in fade-in duration-200">
|
|
41
|
+
<div className="relative w-full max-w-4xl h-[80vh] bg-white rounded-lg shadow-lg border border-gray-200 flex flex-col animate-in zoom-in-95 duration-200">
|
|
42
|
+
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
|
43
|
+
<h2 className="text-xl font-semibold text-gray-900">Create New Agent</h2>
|
|
44
|
+
<button
|
|
45
|
+
onClick={onClose}
|
|
46
|
+
className="text-gray-400 hover:text-gray-500 transition-colors"
|
|
47
|
+
>
|
|
48
|
+
<X className="w-5 h-5" />
|
|
49
|
+
<span className="sr-only">Close</span>
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div className="flex-1 overflow-hidden p-4">
|
|
54
|
+
<BookEditor
|
|
55
|
+
agentSource={agentSource}
|
|
56
|
+
onChange={(source) => setAgentSource(source)}
|
|
57
|
+
height="100%"
|
|
58
|
+
isVerbose={false}
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div className="flex items-center justify-end gap-3 p-4 border-t border-gray-200 bg-gray-50 rounded-b-lg">
|
|
63
|
+
<button
|
|
64
|
+
onClick={onClose}
|
|
65
|
+
className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded-md transition-colors"
|
|
66
|
+
>
|
|
67
|
+
Cancel
|
|
68
|
+
</button>
|
|
69
|
+
<button
|
|
70
|
+
onClick={handleCreate}
|
|
71
|
+
disabled={isCreating}
|
|
72
|
+
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
73
|
+
>
|
|
74
|
+
{isCreating ? (
|
|
75
|
+
<>
|
|
76
|
+
<span className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
|
77
|
+
Creating...
|
|
78
|
+
</>
|
|
79
|
+
) : (
|
|
80
|
+
'Create Agent'
|
|
81
|
+
)}
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</Portal>
|
|
87
|
+
);
|
|
88
|
+
}
|
package/apps/agents-server/src/database/migrations/2025-12-0820-agent-history-permanent-id.sql
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- Backfill permanentId in Agent table if missing
|
|
2
|
+
UPDATE "prefix_Agent"
|
|
3
|
+
SET "permanentId" = gen_random_uuid()::text
|
|
4
|
+
WHERE "permanentId" IS NULL;
|
|
5
|
+
|
|
6
|
+
-- Add permanentId column to AgentHistory
|
|
7
|
+
ALTER TABLE "prefix_AgentHistory" ADD COLUMN "permanentId" TEXT;
|
|
8
|
+
|
|
9
|
+
-- Backfill permanentId from Agent table
|
|
10
|
+
UPDATE "prefix_AgentHistory" ah
|
|
11
|
+
SET "permanentId" = a."permanentId"
|
|
12
|
+
FROM "prefix_Agent" a
|
|
13
|
+
WHERE ah."agentName" = a."agentName";
|
|
14
|
+
|
|
15
|
+
-- Make permanentId NOT NULL
|
|
16
|
+
ALTER TABLE "prefix_AgentHistory" ALTER COLUMN "permanentId" SET NOT NULL;
|
|
17
|
+
|
|
18
|
+
-- Drop old foreign key on agentName
|
|
19
|
+
ALTER TABLE "prefix_AgentHistory" DROP CONSTRAINT "prefix_AgentHistory_agentName_fkey";
|
|
20
|
+
|
|
21
|
+
-- Add new foreign key on permanentId referencing Agent(permanentId)
|
|
22
|
+
ALTER TABLE "prefix_AgentHistory"
|
|
23
|
+
ADD CONSTRAINT "prefix_AgentHistory_permanentId_fkey"
|
|
24
|
+
FOREIGN KEY ("permanentId")
|
|
25
|
+
REFERENCES "prefix_Agent"("permanentId")
|
|
26
|
+
ON DELETE CASCADE;
|
|
27
|
+
|
|
28
|
+
-- Add index for permanentId
|
|
29
|
+
CREATE INDEX "prefix_AgentHistory_permanentId_idx" ON "prefix_AgentHistory" ("permanentId");
|
|
@@ -97,6 +97,7 @@ export type AgentsServerDatabase = {
|
|
|
97
97
|
id: number;
|
|
98
98
|
createdAt: string;
|
|
99
99
|
agentName: string;
|
|
100
|
+
permanentId: string;
|
|
100
101
|
agentHash: string;
|
|
101
102
|
previousAgentHash: string | null;
|
|
102
103
|
agentSource: string;
|
|
@@ -106,6 +107,7 @@ export type AgentsServerDatabase = {
|
|
|
106
107
|
id?: number;
|
|
107
108
|
createdAt: string;
|
|
108
109
|
agentName: string;
|
|
110
|
+
permanentId: string;
|
|
109
111
|
agentHash: string;
|
|
110
112
|
previousAgentHash?: string | null;
|
|
111
113
|
agentSource: string;
|
|
@@ -115,6 +117,7 @@ export type AgentsServerDatabase = {
|
|
|
115
117
|
id?: number;
|
|
116
118
|
createdAt?: string;
|
|
117
119
|
agentName?: string;
|
|
120
|
+
permanentId?: string;
|
|
118
121
|
agentHash?: string;
|
|
119
122
|
previousAgentHash?: string | null;
|
|
120
123
|
agentSource?: string;
|
|
@@ -122,10 +125,10 @@ export type AgentsServerDatabase = {
|
|
|
122
125
|
};
|
|
123
126
|
Relationships: [
|
|
124
127
|
{
|
|
125
|
-
foreignKeyName: '
|
|
126
|
-
columns: ['
|
|
128
|
+
foreignKeyName: 'AgentHistory_permanentId_fkey';
|
|
129
|
+
columns: ['permanentId'];
|
|
127
130
|
referencedRelation: 'Agent';
|
|
128
|
-
referencedColumns: ['
|
|
131
|
+
referencedColumns: ['permanentId'];
|
|
129
132
|
},
|
|
130
133
|
];
|
|
131
134
|
};
|