@promptbook/cli 0.103.0-53 → 0.103.0-55
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/src/app/admin/api-tokens/ApiTokensClient.tsx +186 -0
- package/apps/agents-server/src/app/admin/api-tokens/page.tsx +13 -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 +10 -2
- 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 +10 -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]/links/page.tsx +218 -0
- package/apps/agents-server/src/app/agents/[agentName]/page.tsx +24 -3
- package/apps/agents-server/src/app/api/api-tokens/route.ts +76 -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/docs/[docId]/page.tsx +1 -0
- package/apps/agents-server/src/app/docs/page.tsx +1 -0
- 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/Header/Header.tsx +94 -33
- package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +2 -1
- package/apps/agents-server/src/database/migrations/2025-12-0010-llm-cache.sql +12 -0
- package/apps/agents-server/src/database/migrations/2025-12-0060-api-tokens.sql +13 -0
- package/apps/agents-server/src/database/schema.ts +51 -0
- package/apps/agents-server/src/middleware.ts +50 -2
- package/apps/agents-server/src/tools/$provideCdnForServer.ts +3 -7
- package/apps/agents-server/src/tools/$provideExecutionToolsForServer.ts +10 -1
- package/apps/agents-server/src/utils/cache/SupabaseCacheStorage.ts +55 -0
- package/apps/agents-server/src/utils/cdn/classes/VercelBlobStorage.ts +63 -0
- package/apps/agents-server/src/utils/convertToCsv.ts +31 -0
- package/apps/agents-server/src/utils/handleChatCompletion.ts +183 -0
- package/apps/agents-server/src/utils/resolveInheritedAgentSource.ts +93 -0
- package/esm/index.es.js +846 -131
- package/esm/index.es.js.map +1 -1
- package/esm/typings/src/_packages/core.index.d.ts +8 -6
- package/esm/typings/src/_packages/types.index.d.ts +1 -1
- package/esm/typings/src/book-2.0/agent-source/AgentModelRequirements.d.ts +4 -0
- package/esm/typings/src/commitments/ACTION/ACTION.d.ts +4 -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/DELETE/DELETE.d.ts +4 -0
- package/esm/typings/src/commitments/FORMAT/FORMAT.d.ts +4 -0
- package/esm/typings/src/commitments/FROM/FROM.d.ts +34 -0
- package/esm/typings/src/commitments/GOAL/GOAL.d.ts +4 -0
- package/esm/typings/src/commitments/IMPORTANT/IMPORTANT.d.ts +26 -0
- package/esm/typings/src/commitments/KNOWLEDGE/KNOWLEDGE.d.ts +4 -0
- package/esm/typings/src/commitments/LANGUAGE/LANGUAGE.d.ts +35 -0
- package/esm/typings/src/commitments/MEMORY/MEMORY.d.ts +4 -0
- package/esm/typings/src/commitments/MESSAGE/AgentMessageCommitmentDefinition.d.ts +4 -0
- package/esm/typings/src/commitments/MESSAGE/InitialMessageCommitmentDefinition.d.ts +4 -0
- package/esm/typings/src/commitments/MESSAGE/MESSAGE.d.ts +4 -0
- package/esm/typings/src/commitments/MESSAGE/UserMessageCommitmentDefinition.d.ts +4 -0
- package/esm/typings/src/commitments/META/META.d.ts +4 -0
- package/esm/typings/src/commitments/META_COLOR/META_COLOR.d.ts +4 -0
- package/esm/typings/src/commitments/META_IMAGE/META_IMAGE.d.ts +4 -0
- package/esm/typings/src/commitments/META_LINK/META_LINK.d.ts +4 -0
- package/esm/typings/src/commitments/MODEL/MODEL.d.ts +4 -0
- package/esm/typings/src/commitments/NOTE/NOTE.d.ts +4 -0
- package/esm/typings/src/commitments/OPEN/OPEN.d.ts +35 -0
- package/esm/typings/src/commitments/PERSONA/PERSONA.d.ts +4 -0
- package/esm/typings/src/commitments/RULE/RULE.d.ts +4 -0
- package/esm/typings/src/commitments/SAMPLE/SAMPLE.d.ts +4 -0
- package/esm/typings/src/commitments/SCENARIO/SCENARIO.d.ts +4 -0
- package/esm/typings/src/commitments/STYLE/STYLE.d.ts +4 -0
- package/esm/typings/src/commitments/_base/BaseCommitmentDefinition.d.ts +5 -0
- package/esm/typings/src/commitments/_base/CommitmentDefinition.d.ts +5 -0
- package/esm/typings/src/commitments/_base/NotYetImplementedCommitmentDefinition.d.ts +4 -0
- package/esm/typings/src/commitments/index.d.ts +1 -82
- package/esm/typings/src/commitments/registry.d.ts +68 -0
- package/esm/typings/src/version.d.ts +1 -1
- package/package.json +3 -3
- package/umd/index.umd.js +846 -131
- package/umd/index.umd.js.map +1 -1
- package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +0 -119
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { $getTableName } from '../../../../database/$getTableName';
|
|
3
|
+
import { $provideSupabaseForServer } from '../../../../database/$provideSupabaseForServer';
|
|
4
|
+
import { AgentsServerDatabase } from '../../../../database/schema';
|
|
5
|
+
import { hashPassword, verifyPassword } from '../../../../utils/auth';
|
|
6
|
+
import { getCurrentUser } from '../../../../utils/getCurrentUser';
|
|
7
|
+
|
|
8
|
+
export async function POST(request: Request) {
|
|
9
|
+
try {
|
|
10
|
+
const user = await getCurrentUser();
|
|
11
|
+
if (!user) {
|
|
12
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const body = await request.json();
|
|
16
|
+
const { currentPassword, newPassword } = body;
|
|
17
|
+
|
|
18
|
+
if (!currentPassword || !newPassword) {
|
|
19
|
+
return NextResponse.json({ error: 'Missing password fields' }, { status: 400 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Special check for environment admin
|
|
23
|
+
if (user.username === 'admin' && process.env.ADMIN_PASSWORD) {
|
|
24
|
+
// Environment admin cannot change password through this API
|
|
25
|
+
// They must change the env variable
|
|
26
|
+
return NextResponse.json(
|
|
27
|
+
{
|
|
28
|
+
error: 'You cannot change the admin password. Please update the `ADMIN_PASSWORD` environment variable.',
|
|
29
|
+
},
|
|
30
|
+
{ status: 403 },
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const supabase = $provideSupabaseForServer();
|
|
35
|
+
const { data: userData, error: fetchError } = await supabase
|
|
36
|
+
.from(await $getTableName('User'))
|
|
37
|
+
.select('*')
|
|
38
|
+
.eq('username', user.username)
|
|
39
|
+
.single();
|
|
40
|
+
|
|
41
|
+
if (fetchError || !userData) {
|
|
42
|
+
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const userRow = userData as AgentsServerDatabase['public']['Tables']['User']['Row'];
|
|
46
|
+
|
|
47
|
+
// Verify current password
|
|
48
|
+
const isValid = await verifyPassword(currentPassword, userRow.passwordHash);
|
|
49
|
+
if (!isValid) {
|
|
50
|
+
return NextResponse.json({ error: 'Invalid current password' }, { status: 401 });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Hash new password
|
|
54
|
+
const newPasswordHash = await hashPassword(newPassword);
|
|
55
|
+
|
|
56
|
+
// Update password
|
|
57
|
+
const { error: updateError } = await supabase
|
|
58
|
+
.from(await $getTableName('User'))
|
|
59
|
+
.update({
|
|
60
|
+
passwordHash: newPasswordHash,
|
|
61
|
+
updatedAt: new Date().toISOString(),
|
|
62
|
+
})
|
|
63
|
+
.eq('id', userRow.id);
|
|
64
|
+
|
|
65
|
+
if (updateError) {
|
|
66
|
+
console.error('Error updating password:', updateError);
|
|
67
|
+
return NextResponse.json({ error: 'Failed to update password' }, { status: 500 });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return NextResponse.json({ success: true });
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error('Change password error:', error);
|
|
73
|
+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { $getTableName } from '../../../../database/$getTableName';
|
|
3
|
+
import { $provideSupabase } from '../../../../database/$provideSupabase';
|
|
4
|
+
import { convertToCsv } from '../../../../utils/convertToCsv';
|
|
5
|
+
import { isUserAdmin } from '../../../../utils/isUserAdmin';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Export chat feedback as CSV.
|
|
9
|
+
*
|
|
10
|
+
* Query params:
|
|
11
|
+
* - agentName: filter by agent name (optional)
|
|
12
|
+
*/
|
|
13
|
+
export async function GET(request: NextRequest) {
|
|
14
|
+
if (!(await isUserAdmin())) {
|
|
15
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const searchParams = request.nextUrl.searchParams;
|
|
20
|
+
const agentName = searchParams.get('agentName');
|
|
21
|
+
|
|
22
|
+
const supabase = $provideSupabase();
|
|
23
|
+
const table = await $getTableName('ChatFeedback');
|
|
24
|
+
|
|
25
|
+
let query = supabase.from(table).select('*');
|
|
26
|
+
|
|
27
|
+
if (agentName) {
|
|
28
|
+
query = query.eq('agentName', agentName);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
query = query.order('createdAt', { ascending: false });
|
|
32
|
+
|
|
33
|
+
const { data, error } = await query;
|
|
34
|
+
|
|
35
|
+
if (error) {
|
|
36
|
+
console.error('Export chat feedback error:', error);
|
|
37
|
+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const csv = convertToCsv((data || []) as Record<string, unknown>[]);
|
|
41
|
+
const filename = agentName
|
|
42
|
+
? `chat-feedback-${agentName}-${new Date().toISOString()}.csv`
|
|
43
|
+
: `chat-feedback-${new Date().toISOString()}.csv`;
|
|
44
|
+
|
|
45
|
+
return new NextResponse(csv, {
|
|
46
|
+
headers: {
|
|
47
|
+
'Content-Type': 'text/csv',
|
|
48
|
+
'Content-Disposition': `attachment; filename="${filename}"`,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('Export chat feedback error:', error);
|
|
53
|
+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { $getTableName } from '../../../../database/$getTableName';
|
|
3
|
+
import { $provideSupabase } from '../../../../database/$provideSupabase';
|
|
4
|
+
import { convertToCsv } from '../../../../utils/convertToCsv';
|
|
5
|
+
import { isUserAdmin } from '../../../../utils/isUserAdmin';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Export chat history as CSV.
|
|
9
|
+
*
|
|
10
|
+
* Query params:
|
|
11
|
+
* - agentName: filter by agent name (optional)
|
|
12
|
+
*/
|
|
13
|
+
export async function GET(request: NextRequest) {
|
|
14
|
+
if (!(await isUserAdmin())) {
|
|
15
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const searchParams = request.nextUrl.searchParams;
|
|
20
|
+
const agentName = searchParams.get('agentName');
|
|
21
|
+
|
|
22
|
+
const supabase = $provideSupabase();
|
|
23
|
+
const table = await $getTableName('ChatHistory');
|
|
24
|
+
|
|
25
|
+
let query = supabase.from(table).select('*');
|
|
26
|
+
|
|
27
|
+
if (agentName) {
|
|
28
|
+
query = query.eq('agentName', agentName);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
query = query.order('createdAt', { ascending: false });
|
|
32
|
+
|
|
33
|
+
const { data, error } = await query;
|
|
34
|
+
|
|
35
|
+
if (error) {
|
|
36
|
+
console.error('Export chat history error:', error);
|
|
37
|
+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const csv = convertToCsv((data || []) as Record<string, unknown>[]);
|
|
41
|
+
const filename = agentName
|
|
42
|
+
? `chat-history-${agentName}-${new Date().toISOString()}.csv`
|
|
43
|
+
: `chat-history-${new Date().toISOString()}.csv`;
|
|
44
|
+
|
|
45
|
+
return new NextResponse(csv, {
|
|
46
|
+
headers: {
|
|
47
|
+
'Content-Type': 'text/csv',
|
|
48
|
+
'Content-Disposition': `attachment; filename="${filename}"`,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('Export chat history error:', error);
|
|
53
|
+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -30,6 +30,7 @@ export default async function DocPage(props: DocPageProps) {
|
|
|
30
30
|
<div className="p-8 border-b border-gray-100 bg-gray-50/50">
|
|
31
31
|
<div className="flex items-center gap-4 mb-4">
|
|
32
32
|
<h1 className="text-4xl font-bold text-gray-900 tracking-tight">
|
|
33
|
+
<span className="mr-3">{primary.icon}</span>
|
|
33
34
|
{primary.type}
|
|
34
35
|
{aliases.length > 0 && (
|
|
35
36
|
<span className="text-gray-400 font-normal ml-4 text-2xl">
|
|
@@ -14,6 +14,7 @@ export default function DocsPage() {
|
|
|
14
14
|
<Link key={primary.type} href={`/docs/${primary.type}`} className="block h-full group">
|
|
15
15
|
<Card className="h-full group-hover:border-blue-500 transition-colors">
|
|
16
16
|
<h3 className="text-xl font-semibold mb-2 group-hover:text-blue-600 transition-colors">
|
|
17
|
+
<span className="mr-2">{primary.icon}</span>
|
|
17
18
|
{primary.type}
|
|
18
19
|
{aliases.length > 0 && (
|
|
19
20
|
<span className="text-gray-400 font-normal text-lg">
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { X } from 'lucide-react';
|
|
4
|
+
import { ChangePasswordForm } from '../ChangePasswordForm/ChangePasswordForm';
|
|
5
|
+
import { Portal } from '../Portal/Portal';
|
|
6
|
+
|
|
7
|
+
type ChangePasswordDialogProps = {
|
|
8
|
+
isOpen: boolean;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function ChangePasswordDialog(props: ChangePasswordDialogProps) {
|
|
13
|
+
const { isOpen, onClose } = props;
|
|
14
|
+
|
|
15
|
+
if (!isOpen) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Portal>
|
|
21
|
+
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm animate-in fade-in duration-200">
|
|
22
|
+
<div className="relative w-full max-w-md bg-white rounded-lg shadow-lg border border-gray-200 p-6 animate-in zoom-in-95 duration-200">
|
|
23
|
+
<button
|
|
24
|
+
onClick={onClose}
|
|
25
|
+
className="absolute top-4 right-4 text-gray-400 hover:text-gray-500 transition-colors"
|
|
26
|
+
>
|
|
27
|
+
<X className="w-5 h-5" />
|
|
28
|
+
<span className="sr-only">Close</span>
|
|
29
|
+
</button>
|
|
30
|
+
|
|
31
|
+
<div className="mb-6">
|
|
32
|
+
<h2 className="text-xl font-semibold text-gray-900">Change Password</h2>
|
|
33
|
+
<p className="text-sm text-gray-500 mt-1">Update your password to keep your account secure</p>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<ChangePasswordForm onSuccess={onClose} />
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</Portal>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Loader2, Lock } from 'lucide-react';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
|
|
6
|
+
type ChangePasswordFormProps = {
|
|
7
|
+
onSuccess?: () => void;
|
|
8
|
+
className?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function ChangePasswordForm(props: ChangePasswordFormProps) {
|
|
12
|
+
const { onSuccess, className } = props;
|
|
13
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
14
|
+
const [error, setError] = useState<string | null>(null);
|
|
15
|
+
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
|
16
|
+
|
|
17
|
+
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
18
|
+
event.preventDefault();
|
|
19
|
+
setIsLoading(true);
|
|
20
|
+
setError(null);
|
|
21
|
+
setSuccessMessage(null);
|
|
22
|
+
|
|
23
|
+
const formData = new FormData(event.currentTarget);
|
|
24
|
+
const currentPassword = formData.get('currentPassword') as string;
|
|
25
|
+
const newPassword = formData.get('newPassword') as string;
|
|
26
|
+
const confirmNewPassword = formData.get('confirmNewPassword') as string;
|
|
27
|
+
|
|
28
|
+
if (newPassword !== confirmNewPassword) {
|
|
29
|
+
setError('New passwords do not match');
|
|
30
|
+
setIsLoading(false);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch('/api/auth/change-password', {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: {
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({ currentPassword, newPassword }),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const result = await response.json();
|
|
44
|
+
|
|
45
|
+
if (response.ok) {
|
|
46
|
+
setSuccessMessage('Password changed successfully');
|
|
47
|
+
// Reset form
|
|
48
|
+
event.currentTarget.reset();
|
|
49
|
+
if (onSuccess) {
|
|
50
|
+
setTimeout(onSuccess, 1500);
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
setError(result.error || 'An error occurred');
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
setError('An unexpected error occurred');
|
|
57
|
+
console.error(error);
|
|
58
|
+
} finally {
|
|
59
|
+
setIsLoading(false);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<form onSubmit={handleSubmit} className={`space-y-4 ${className || ''}`}>
|
|
65
|
+
<div className="space-y-2">
|
|
66
|
+
<label
|
|
67
|
+
htmlFor="currentPassword"
|
|
68
|
+
className="text-sm font-medium text-gray-700 block"
|
|
69
|
+
>
|
|
70
|
+
Current Password
|
|
71
|
+
</label>
|
|
72
|
+
<div className="relative">
|
|
73
|
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
|
|
74
|
+
<Lock className="w-4 h-4" />
|
|
75
|
+
</div>
|
|
76
|
+
<input
|
|
77
|
+
id="currentPassword"
|
|
78
|
+
name="currentPassword"
|
|
79
|
+
type="password"
|
|
80
|
+
required
|
|
81
|
+
className="block w-full pl-10 h-10 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-promptbook-blue focus:border-transparent disabled:opacity-50"
|
|
82
|
+
placeholder="Enter current password"
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div className="space-y-2">
|
|
88
|
+
<label
|
|
89
|
+
htmlFor="newPassword"
|
|
90
|
+
className="text-sm font-medium text-gray-700 block"
|
|
91
|
+
>
|
|
92
|
+
New Password
|
|
93
|
+
</label>
|
|
94
|
+
<div className="relative">
|
|
95
|
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
|
|
96
|
+
<Lock className="w-4 h-4" />
|
|
97
|
+
</div>
|
|
98
|
+
<input
|
|
99
|
+
id="newPassword"
|
|
100
|
+
name="newPassword"
|
|
101
|
+
type="password"
|
|
102
|
+
required
|
|
103
|
+
className="block w-full pl-10 h-10 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-promptbook-blue focus:border-transparent disabled:opacity-50"
|
|
104
|
+
placeholder="Enter new password"
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div className="space-y-2">
|
|
110
|
+
<label
|
|
111
|
+
htmlFor="confirmNewPassword"
|
|
112
|
+
className="text-sm font-medium text-gray-700 block"
|
|
113
|
+
>
|
|
114
|
+
Confirm New Password
|
|
115
|
+
</label>
|
|
116
|
+
<div className="relative">
|
|
117
|
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
|
|
118
|
+
<Lock className="w-4 h-4" />
|
|
119
|
+
</div>
|
|
120
|
+
<input
|
|
121
|
+
id="confirmNewPassword"
|
|
122
|
+
name="confirmNewPassword"
|
|
123
|
+
type="password"
|
|
124
|
+
required
|
|
125
|
+
className="block w-full pl-10 h-10 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-promptbook-blue focus:border-transparent disabled:opacity-50"
|
|
126
|
+
placeholder="Confirm new password"
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{error && (
|
|
132
|
+
<div className="p-3 text-sm text-red-500 bg-red-50 border border-red-200 rounded-md">
|
|
133
|
+
{error}
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
{successMessage && (
|
|
138
|
+
<div className="p-3 text-sm text-green-500 bg-green-50 border border-green-200 rounded-md">
|
|
139
|
+
{successMessage}
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
|
|
143
|
+
<button
|
|
144
|
+
type="submit"
|
|
145
|
+
disabled={isLoading}
|
|
146
|
+
className="w-full inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 bg-promptbook-blue-dark text-white hover:bg-promptbook-blue-dark/90 focus:outline-none focus:ring-2 focus:ring-promptbook-blue focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none transition-colors"
|
|
147
|
+
>
|
|
148
|
+
{isLoading ? (
|
|
149
|
+
<>
|
|
150
|
+
<Loader2 className="mr-2 w-4 h-4 animate-spin" />
|
|
151
|
+
Changing Password...
|
|
152
|
+
</>
|
|
153
|
+
) : (
|
|
154
|
+
'Change Password'
|
|
155
|
+
)}
|
|
156
|
+
</button>
|
|
157
|
+
</form>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import promptbookLogoBlueTransparent from '@/public/logo-blue-white-256.png';
|
|
4
4
|
import { $createAgentAction, logoutAction } from '@/src/app/actions';
|
|
5
|
-
import { ArrowRight, ChevronDown, LogIn, LogOut } from 'lucide-react';
|
|
5
|
+
import { ArrowRight, ChevronDown, Lock, LogIn, LogOut, User } from 'lucide-react';
|
|
6
6
|
import Image from 'next/image';
|
|
7
7
|
import Link from 'next/link';
|
|
8
8
|
import { useRouter } from 'next/navigation';
|
|
@@ -12,6 +12,7 @@ import { HamburgerMenu } from '../../../../../src/book-components/_common/Hambur
|
|
|
12
12
|
import { just } from '../../../../../src/utils/organization/just';
|
|
13
13
|
import type { UserInfo } from '../../utils/getCurrentUser';
|
|
14
14
|
import { getVisibleCommitmentDefinitions } from '../../utils/getVisibleCommitmentDefinitions';
|
|
15
|
+
import { ChangePasswordDialog } from '../ChangePasswordDialog/ChangePasswordDialog';
|
|
15
16
|
import { LoginDialog } from '../LoginDialog/LoginDialog';
|
|
16
17
|
import { useUsersAdmin } from '../UsersList/useUsersAdmin';
|
|
17
18
|
|
|
@@ -73,12 +74,16 @@ export function Header(props: HeaderProps) {
|
|
|
73
74
|
|
|
74
75
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
75
76
|
const [isLoginOpen, setIsLoginOpen] = useState(false);
|
|
77
|
+
const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false);
|
|
76
78
|
const [isAgentsOpen, setIsAgentsOpen] = useState(false);
|
|
77
79
|
const [isDocsOpen, setIsDocsOpen] = useState(false);
|
|
78
80
|
const [isUsersOpen, setIsUsersOpen] = useState(false);
|
|
81
|
+
const [isSystemOpen, setIsSystemOpen] = useState(false);
|
|
82
|
+
const [isProfileOpen, setIsProfileOpen] = useState(false);
|
|
79
83
|
const [isMobileAgentsOpen, setIsMobileAgentsOpen] = useState(false);
|
|
80
84
|
const [isMobileDocsOpen, setIsMobileDocsOpen] = useState(false);
|
|
81
85
|
const [isMobileUsersOpen, setIsMobileUsersOpen] = useState(false);
|
|
86
|
+
const [isMobileSystemOpen, setIsMobileSystemOpen] = useState(false);
|
|
82
87
|
const [isCreatingAgent, setIsCreatingAgent] = useState(false);
|
|
83
88
|
const router = useRouter();
|
|
84
89
|
|
|
@@ -131,7 +136,9 @@ export function Header(props: HeaderProps) {
|
|
|
131
136
|
label: (
|
|
132
137
|
<>
|
|
133
138
|
{primary.type}
|
|
134
|
-
{aliases.length > 0 &&
|
|
139
|
+
{aliases.length > 0 && (
|
|
140
|
+
<span className="text-gray-400 font-normal"> / {aliases.join(' / ')}</span>
|
|
141
|
+
)}
|
|
135
142
|
</>
|
|
136
143
|
),
|
|
137
144
|
href: `/docs/${primary.type}`,
|
|
@@ -210,19 +217,30 @@ export function Header(props: HeaderProps) {
|
|
|
210
217
|
],
|
|
211
218
|
},
|
|
212
219
|
{
|
|
213
|
-
type: '
|
|
214
|
-
label: '
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
220
|
+
type: 'dropdown' as const,
|
|
221
|
+
label: 'System',
|
|
222
|
+
isOpen: isSystemOpen,
|
|
223
|
+
setIsOpen: setIsSystemOpen,
|
|
224
|
+
isMobileOpen: isMobileSystemOpen,
|
|
225
|
+
setIsMobileOpen: setIsMobileSystemOpen,
|
|
226
|
+
items: [
|
|
227
|
+
{
|
|
228
|
+
label: 'API Tokens',
|
|
229
|
+
href: '/admin/api-tokens',
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
label: 'Metadata',
|
|
233
|
+
href: '/admin/metadata',
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
label: 'Chat history',
|
|
237
|
+
href: '/admin/chat-history',
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
label: 'Chat feedback',
|
|
241
|
+
href: '/admin/chat-feedback',
|
|
242
|
+
},
|
|
243
|
+
],
|
|
226
244
|
},
|
|
227
245
|
{
|
|
228
246
|
type: 'link' as const,
|
|
@@ -236,6 +254,7 @@ export function Header(props: HeaderProps) {
|
|
|
236
254
|
return (
|
|
237
255
|
<header className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-200 h-16">
|
|
238
256
|
<LoginDialog isOpen={isLoginOpen} onClose={() => setIsLoginOpen(false)} />
|
|
257
|
+
<ChangePasswordDialog isOpen={isChangePasswordOpen} onClose={() => setIsChangePasswordOpen(false)} />
|
|
239
258
|
<div className="container mx-auto px-4 h-full">
|
|
240
259
|
<div className="flex items-center justify-between h-full">
|
|
241
260
|
{/* Logo */}
|
|
@@ -365,24 +384,56 @@ export function Header(props: HeaderProps) {
|
|
|
365
384
|
|
|
366
385
|
{(currentUser || isAdmin) && (
|
|
367
386
|
<div className="hidden lg:flex items-center gap-3">
|
|
368
|
-
<
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
387
|
+
<div className="relative">
|
|
388
|
+
<button
|
|
389
|
+
onClick={() => setIsProfileOpen(!isProfileOpen)}
|
|
390
|
+
onBlur={() => setTimeout(() => setIsProfileOpen(false), 200)}
|
|
391
|
+
className="flex items-center gap-2 text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors px-3 py-2 rounded-md hover:bg-gray-50"
|
|
392
|
+
>
|
|
393
|
+
<div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center text-blue-600">
|
|
394
|
+
<User className="w-4 h-4" />
|
|
395
|
+
</div>
|
|
396
|
+
<div className="flex flex-col items-start">
|
|
397
|
+
<span className="leading-none">{currentUser?.username || 'Admin'}</span>
|
|
398
|
+
{(currentUser?.isAdmin || isAdmin) && (
|
|
399
|
+
<span className="text-xs text-blue-600">Admin</span>
|
|
400
|
+
)}
|
|
401
|
+
</div>
|
|
402
|
+
<ChevronDown className="w-4 h-4 ml-1 opacity-50" />
|
|
403
|
+
</button>
|
|
404
|
+
|
|
405
|
+
{isProfileOpen && (
|
|
406
|
+
<div className="absolute top-full right-0 mt-2 w-56 bg-white rounded-md shadow-lg border border-gray-100 py-1 z-50 animate-in fade-in zoom-in-95 duration-200">
|
|
407
|
+
<div className="px-4 py-3 border-b border-gray-100">
|
|
408
|
+
<p className="text-sm font-medium text-gray-900">
|
|
409
|
+
{currentUser?.username || 'Admin'}
|
|
410
|
+
</p>
|
|
411
|
+
{(currentUser?.isAdmin || isAdmin) && (
|
|
412
|
+
<p className="text-xs text-blue-600 mt-1">Administrator</p>
|
|
413
|
+
)}
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
<button
|
|
417
|
+
onClick={() => setIsChangePasswordOpen(true)}
|
|
418
|
+
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 hover:text-gray-900 flex items-center gap-2"
|
|
419
|
+
>
|
|
420
|
+
<Lock className="w-4 h-4" />
|
|
421
|
+
Change Password
|
|
422
|
+
</button>
|
|
423
|
+
|
|
424
|
+
<button
|
|
425
|
+
onClick={() => {
|
|
426
|
+
handleLogout();
|
|
427
|
+
setIsMenuOpen(false);
|
|
428
|
+
}}
|
|
429
|
+
className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 hover:text-red-700 flex items-center gap-2"
|
|
430
|
+
>
|
|
431
|
+
<LogOut className="w-4 h-4" />
|
|
432
|
+
Log out
|
|
433
|
+
</button>
|
|
434
|
+
</div>
|
|
374
435
|
)}
|
|
375
|
-
</
|
|
376
|
-
<button
|
|
377
|
-
onClick={() => {
|
|
378
|
-
handleLogout();
|
|
379
|
-
setIsMenuOpen(false);
|
|
380
|
-
}}
|
|
381
|
-
className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium h-10 px-4 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 transition-colors"
|
|
382
|
-
>
|
|
383
|
-
Log out
|
|
384
|
-
<LogOut className="ml-2 w-4 h-4" />
|
|
385
|
-
</button>
|
|
436
|
+
</div>
|
|
386
437
|
</div>
|
|
387
438
|
)}
|
|
388
439
|
|
|
@@ -398,7 +449,7 @@ export function Header(props: HeaderProps) {
|
|
|
398
449
|
{/* Mobile Navigation */}
|
|
399
450
|
{isMenuOpen && (
|
|
400
451
|
<div
|
|
401
|
-
className="lg:hidden absolute top-16 left-0 right-0 z-50 bg-white
|
|
452
|
+
className="lg:hidden absolute top-16 left-0 right-0 z-50 bg-white shadow-xl py-4 border-t border-gray-100 animate-in slide-in-from-top-2 h-[calc(100vh-4rem)] overflow-y-auto"
|
|
402
453
|
style={{
|
|
403
454
|
backdropFilter: 'blur(20px)',
|
|
404
455
|
WebkitBackdropFilter: 'blur(20px)',
|
|
@@ -430,6 +481,16 @@ export function Header(props: HeaderProps) {
|
|
|
430
481
|
</span>
|
|
431
482
|
)}
|
|
432
483
|
</div>
|
|
484
|
+
<button
|
|
485
|
+
onClick={() => {
|
|
486
|
+
setIsChangePasswordOpen(true);
|
|
487
|
+
setIsMenuOpen(false);
|
|
488
|
+
}}
|
|
489
|
+
className="flex items-center gap-2 text-base font-medium text-gray-600 hover:text-gray-900 w-full"
|
|
490
|
+
>
|
|
491
|
+
<Lock className="w-4 h-4" />
|
|
492
|
+
Change Password
|
|
493
|
+
</button>
|
|
433
494
|
<button
|
|
434
495
|
onClick={() => {
|
|
435
496
|
handleLogout();
|
|
@@ -31,6 +31,7 @@ export function LayoutWrapper({
|
|
|
31
31
|
const isAdminChatPage =
|
|
32
32
|
pathname?.startsWith('/admin/chat-history') || pathname?.startsWith('/admin/chat-feedback');
|
|
33
33
|
const isHeaderHidden = pathname?.includes('/chat') && !isAdminChatPage;
|
|
34
|
+
const isFooterHiddenOnPage = pathname ? /^\/agents\/[^/]+\/book(\+chat)?$/.test(pathname) : false;
|
|
34
35
|
|
|
35
36
|
if (isHeaderHidden) {
|
|
36
37
|
return <main className={`pt-0`}>{children}</main>;
|
|
@@ -46,7 +47,7 @@ export function LayoutWrapper({
|
|
|
46
47
|
agents={agents}
|
|
47
48
|
/>
|
|
48
49
|
<main className={`pt-[60px]`}>{children}</main>
|
|
49
|
-
{isFooterShown && <Footer extraLinks={footerLinks} />}
|
|
50
|
+
{isFooterShown && !isFooterHiddenOnPage && <Footer extraLinks={footerLinks} />}
|
|
50
51
|
</>
|
|
51
52
|
);
|
|
52
53
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS "prefix_LlmCache" (
|
|
2
|
+
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
3
|
+
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
4
|
+
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
5
|
+
|
|
6
|
+
"hash" TEXT NOT NULL,
|
|
7
|
+
"value" JSONB NOT NULL
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "prefix_LlmCache_hash_idx" ON "prefix_LlmCache" ("hash");
|
|
11
|
+
|
|
12
|
+
ALTER TABLE "prefix_LlmCache" ENABLE ROW LEVEL SECURITY;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS "prefix_ApiTokens" (
|
|
2
|
+
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
3
|
+
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
4
|
+
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
5
|
+
|
|
6
|
+
"token" TEXT NOT NULL,
|
|
7
|
+
"note" TEXT,
|
|
8
|
+
"isRevoked" BOOLEAN NOT NULL DEFAULT FALSE
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "prefix_ApiTokens_token_idx" ON "prefix_ApiTokens" ("token");
|
|
12
|
+
|
|
13
|
+
ALTER TABLE "prefix_ApiTokens" ENABLE ROW LEVEL SECURITY;
|