@promptbook/cli 0.103.0-53 → 0.103.0-54
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/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/agents/[agentName]/AgentChatWrapper.tsx +10 -2
- package/apps/agents-server/src/app/agents/[agentName]/api/openai/chat/completions/route.ts +176 -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/docs/[docId]/page.tsx +1 -0
- package/apps/agents-server/src/app/docs/page.tsx +1 -0
- package/apps/agents-server/src/components/Header/Header.tsx +5 -0
- 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 +49 -1
- 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/esm/index.es.js +127 -1
- package/esm/index.es.js.map +1 -1
- package/esm/typings/src/commitments/ACTION/ACTION.d.ts +4 -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/GOAL/GOAL.d.ts +4 -0
- package/esm/typings/src/commitments/KNOWLEDGE/KNOWLEDGE.d.ts +4 -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/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/version.d.ts +1 -1
- package/package.json +2 -2
- package/umd/index.umd.js +127 -1
- package/umd/index.umd.js.map +1 -1
- package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +0 -119
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Copy, Plus, Trash } from 'lucide-react';
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
type ApiTokenEntry = {
|
|
7
|
+
id: number;
|
|
8
|
+
token: string;
|
|
9
|
+
note: string | null;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
updatedAt: string;
|
|
12
|
+
isRevoked: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function ApiTokensClient() {
|
|
16
|
+
const [tokens, setTokens] = useState<ApiTokenEntry[]>([]);
|
|
17
|
+
const [loading, setLoading] = useState(true);
|
|
18
|
+
const [error, setError] = useState<string | null>(null);
|
|
19
|
+
const [note, setNote] = useState('');
|
|
20
|
+
const [isCreating, setIsCreating] = useState(false);
|
|
21
|
+
|
|
22
|
+
const fetchTokens = async () => {
|
|
23
|
+
try {
|
|
24
|
+
setLoading(true);
|
|
25
|
+
const response = await fetch('/api/api-tokens');
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error('Failed to fetch tokens');
|
|
28
|
+
}
|
|
29
|
+
const data: ApiTokenEntry[] = await response.json();
|
|
30
|
+
setTokens(data);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
setError(err instanceof Error ? err.message : 'An error occurred');
|
|
33
|
+
} finally {
|
|
34
|
+
setLoading(false);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
fetchTokens();
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const handleCreate = async (e: React.FormEvent) => {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
setError(null);
|
|
45
|
+
setIsCreating(true);
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch('/api/api-tokens', {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Content-Type': 'application/json' },
|
|
51
|
+
body: JSON.stringify({ note }),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
const data = await response.json();
|
|
56
|
+
throw new Error(data.error || 'Failed to create token');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setNote('');
|
|
60
|
+
fetchTokens();
|
|
61
|
+
} catch (err) {
|
|
62
|
+
setError(err instanceof Error ? err.message : 'An error occurred');
|
|
63
|
+
} finally {
|
|
64
|
+
setIsCreating(false);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const handleDelete = async (id: number) => {
|
|
69
|
+
if (!confirm('Are you sure you want to delete this token?')) return;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const response = await fetch(`/api/api-tokens?id=${id}`, {
|
|
73
|
+
method: 'DELETE',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
const data = await response.json();
|
|
78
|
+
throw new Error(data.error || 'Failed to delete token');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fetchTokens();
|
|
82
|
+
} catch (err) {
|
|
83
|
+
setError(err instanceof Error ? err.message : 'An error occurred');
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const copyToClipboard = (text: string) => {
|
|
88
|
+
navigator.clipboard.writeText(text);
|
|
89
|
+
// You could add a toast notification here
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (loading && tokens.length === 0) {
|
|
93
|
+
return <div className="p-8 text-center">Loading tokens...</div>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="container mx-auto p-8 max-w-4xl">
|
|
98
|
+
<h1 className="text-3xl font-bold mb-8">API Tokens</h1>
|
|
99
|
+
|
|
100
|
+
{error && (
|
|
101
|
+
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-6">{error}</div>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
<div className="bg-white shadow rounded-lg p-6 mb-8">
|
|
105
|
+
<h2 className="text-xl font-semibold mb-4">Create New Token</h2>
|
|
106
|
+
<form onSubmit={handleCreate} className="flex gap-4">
|
|
107
|
+
<input
|
|
108
|
+
type="text"
|
|
109
|
+
value={note}
|
|
110
|
+
onChange={(e) => setNote(e.target.value)}
|
|
111
|
+
className="flex-grow px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
112
|
+
placeholder="Description (optional)"
|
|
113
|
+
/>
|
|
114
|
+
<button
|
|
115
|
+
type="submit"
|
|
116
|
+
disabled={isCreating}
|
|
117
|
+
className="bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 transition-colors flex items-center gap-2 whitespace-nowrap"
|
|
118
|
+
>
|
|
119
|
+
<Plus className="w-4 h-4" />
|
|
120
|
+
{isCreating ? 'Creating...' : 'Create Token'}
|
|
121
|
+
</button>
|
|
122
|
+
</form>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div className="bg-white shadow rounded-lg overflow-hidden">
|
|
126
|
+
<table className="min-w-full divide-y divide-gray-200">
|
|
127
|
+
<thead className="bg-gray-50">
|
|
128
|
+
<tr>
|
|
129
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
130
|
+
Token
|
|
131
|
+
</th>
|
|
132
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
133
|
+
Note
|
|
134
|
+
</th>
|
|
135
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
136
|
+
Created At
|
|
137
|
+
</th>
|
|
138
|
+
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
139
|
+
Actions
|
|
140
|
+
</th>
|
|
141
|
+
</tr>
|
|
142
|
+
</thead>
|
|
143
|
+
<tbody className="bg-white divide-y divide-gray-200">
|
|
144
|
+
{tokens.length === 0 ? (
|
|
145
|
+
<tr>
|
|
146
|
+
<td colSpan={4} className="px-6 py-4 text-center text-gray-500">
|
|
147
|
+
No tokens found.
|
|
148
|
+
</td>
|
|
149
|
+
</tr>
|
|
150
|
+
) : (
|
|
151
|
+
tokens.map((entry) => (
|
|
152
|
+
<tr key={entry.id}>
|
|
153
|
+
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 font-mono">
|
|
154
|
+
<div className="flex items-center gap-2">
|
|
155
|
+
<span>{entry.token}</span>
|
|
156
|
+
<button
|
|
157
|
+
onClick={() => copyToClipboard(entry.token)}
|
|
158
|
+
className="text-gray-400 hover:text-gray-600"
|
|
159
|
+
title="Copy to clipboard"
|
|
160
|
+
>
|
|
161
|
+
<Copy className="w-4 h-4" />
|
|
162
|
+
</button>
|
|
163
|
+
</div>
|
|
164
|
+
</td>
|
|
165
|
+
<td className="px-6 py-4 text-sm text-gray-500">{entry.note || '-'}</td>
|
|
166
|
+
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
167
|
+
{new Date(entry.createdAt).toLocaleDateString()}
|
|
168
|
+
</td>
|
|
169
|
+
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
170
|
+
<button
|
|
171
|
+
onClick={() => handleDelete(entry.id)}
|
|
172
|
+
className="text-red-600 hover:text-red-900"
|
|
173
|
+
title="Delete token"
|
|
174
|
+
>
|
|
175
|
+
<Trash className="w-4 h-4" />
|
|
176
|
+
</button>
|
|
177
|
+
</td>
|
|
178
|
+
</tr>
|
|
179
|
+
))
|
|
180
|
+
)}
|
|
181
|
+
</tbody>
|
|
182
|
+
</table>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ForbiddenPage } from '../../../components/ForbiddenPage/ForbiddenPage';
|
|
2
|
+
import { isUserAdmin } from '../../../utils/isUserAdmin';
|
|
3
|
+
import { ApiTokensClient } from './ApiTokensClient';
|
|
4
|
+
|
|
5
|
+
export default async function ApiTokensPage() {
|
|
6
|
+
const isAdmin = await isUserAdmin();
|
|
7
|
+
|
|
8
|
+
if (!isAdmin) {
|
|
9
|
+
return <ForbiddenPage />;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return <ApiTokensClient />;
|
|
13
|
+
}
|
|
@@ -8,12 +8,13 @@ import { string_agent_url } from '../../../../../../src/types/typeAliases';
|
|
|
8
8
|
|
|
9
9
|
type AgentChatWrapperProps = {
|
|
10
10
|
agentUrl: string_agent_url;
|
|
11
|
+
defaultMessage?: string;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
// TODO: [🐱🚀] Rename to AgentChatSomethingWrapper
|
|
14
15
|
|
|
15
16
|
export function AgentChatWrapper(props: AgentChatWrapperProps) {
|
|
16
|
-
const { agentUrl } = props;
|
|
17
|
+
const { agentUrl, defaultMessage } = props;
|
|
17
18
|
|
|
18
19
|
const agentPromise = useMemo(
|
|
19
20
|
() =>
|
|
@@ -60,7 +61,14 @@ export function AgentChatWrapper(props: AgentChatWrapperProps) {
|
|
|
60
61
|
return <>{/* <- TODO: [🐱🚀] <PromptbookLoading /> */}</>;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
return
|
|
64
|
+
return (
|
|
65
|
+
<AgentChat
|
|
66
|
+
className={`w-full h-full`}
|
|
67
|
+
agent={agent}
|
|
68
|
+
onFeedback={handleFeedback}
|
|
69
|
+
defaultMessage={defaultMessage}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
/**
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
|
|
2
|
+
import { $provideExecutionToolsForServer } from '@/src/tools/$provideExecutionToolsForServer';
|
|
3
|
+
import { Agent } from '@promptbook-local/core';
|
|
4
|
+
import { ChatMessage, ChatPromptResult, Prompt, TODO_any } from '@promptbook-local/types';
|
|
5
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
6
|
+
|
|
7
|
+
export async function POST(
|
|
8
|
+
request: NextRequest,
|
|
9
|
+
{ params }: { params: Promise<{ agentName: string }> },
|
|
10
|
+
) {
|
|
11
|
+
const { agentName } = await params;
|
|
12
|
+
|
|
13
|
+
// Note: Authentication is handled by middleware
|
|
14
|
+
// If we are here, the request is either authenticated or public access is allowed (but middleware blocks it if not)
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const body = await request.json();
|
|
18
|
+
const { messages, stream, model } = body;
|
|
19
|
+
|
|
20
|
+
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
|
21
|
+
return NextResponse.json(
|
|
22
|
+
{ error: { message: 'Messages array is required and cannot be empty.', type: 'invalid_request_error' } },
|
|
23
|
+
{ status: 400 },
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const collection = await $provideAgentCollectionForServer();
|
|
28
|
+
let agentSource;
|
|
29
|
+
try {
|
|
30
|
+
agentSource = await collection.getAgentSource(agentName);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return NextResponse.json(
|
|
33
|
+
{ error: { message: `Agent '${agentName}' not found.`, type: 'invalid_request_error' } },
|
|
34
|
+
{ status: 404 },
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!agentSource) {
|
|
39
|
+
return NextResponse.json(
|
|
40
|
+
{ error: { message: `Agent '${agentName}' not found.`, type: 'invalid_request_error' } },
|
|
41
|
+
{ status: 404 },
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const executionTools = await $provideExecutionToolsForServer();
|
|
46
|
+
const agent = new Agent({
|
|
47
|
+
agentSource,
|
|
48
|
+
executionTools,
|
|
49
|
+
isVerbose: true, // or false
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Prepare thread and content
|
|
53
|
+
const lastMessage = messages[messages.length - 1];
|
|
54
|
+
const previousMessages = messages.slice(0, -1);
|
|
55
|
+
|
|
56
|
+
const thread: ChatMessage[] = previousMessages.map((msg: TODO_any, index: number) => ({
|
|
57
|
+
id: `msg-${index}`, // Placeholder ID
|
|
58
|
+
from: msg.role === 'assistant' ? 'agent' : 'user', // Mapping standard OpenAI roles
|
|
59
|
+
content: msg.content,
|
|
60
|
+
isComplete: true,
|
|
61
|
+
date: new Date(), // We don't have the real date, using current
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
const prompt: Prompt = {
|
|
65
|
+
title: 'OpenAI API Chat Completion',
|
|
66
|
+
content: lastMessage.content,
|
|
67
|
+
modelRequirements: {
|
|
68
|
+
modelVariant: 'CHAT',
|
|
69
|
+
// We could pass 'model' from body if we wanted to enforce it, but Agent usually has its own config
|
|
70
|
+
},
|
|
71
|
+
parameters: {},
|
|
72
|
+
thread,
|
|
73
|
+
} as Prompt;
|
|
74
|
+
// Note: Casting as Prompt because the type definition might require properties we don't strictly use or that are optional but TS complains
|
|
75
|
+
|
|
76
|
+
if (stream) {
|
|
77
|
+
const encoder = new TextEncoder();
|
|
78
|
+
const readableStream = new ReadableStream({
|
|
79
|
+
async start(controller) {
|
|
80
|
+
const runId = `chatcmpl-${Math.random().toString(36).substring(2, 15)}`;
|
|
81
|
+
const created = Math.floor(Date.now() / 1000);
|
|
82
|
+
|
|
83
|
+
let previousContent = '';
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await agent.callChatModelStream(prompt, (chunk: ChatPromptResult) => {
|
|
87
|
+
const fullContent = chunk.content;
|
|
88
|
+
const deltaContent = fullContent.substring(previousContent.length);
|
|
89
|
+
previousContent = fullContent;
|
|
90
|
+
|
|
91
|
+
if (deltaContent) {
|
|
92
|
+
const chunkData = {
|
|
93
|
+
id: runId,
|
|
94
|
+
object: 'chat.completion.chunk',
|
|
95
|
+
created,
|
|
96
|
+
model: model || 'promptbook-agent',
|
|
97
|
+
choices: [
|
|
98
|
+
{
|
|
99
|
+
index: 0,
|
|
100
|
+
delta: {
|
|
101
|
+
content: deltaContent,
|
|
102
|
+
},
|
|
103
|
+
finish_reason: null,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunkData)}\n\n`));
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const doneChunkData = {
|
|
112
|
+
id: runId,
|
|
113
|
+
object: 'chat.completion.chunk',
|
|
114
|
+
created,
|
|
115
|
+
model: model || 'promptbook-agent',
|
|
116
|
+
choices: [
|
|
117
|
+
{
|
|
118
|
+
index: 0,
|
|
119
|
+
delta: {},
|
|
120
|
+
finish_reason: 'stop',
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(doneChunkData)}\n\n`));
|
|
125
|
+
controller.enqueue(encoder.encode('[DONE]'));
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error('Error during streaming:', error);
|
|
128
|
+
// OpenAI stream doesn't usually send error JSON in stream, just closes or sends error text?
|
|
129
|
+
// But we should try to close gracefully or error.
|
|
130
|
+
controller.error(error);
|
|
131
|
+
}
|
|
132
|
+
controller.close();
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return new Response(readableStream, {
|
|
137
|
+
headers: {
|
|
138
|
+
'Content-Type': 'text/event-stream',
|
|
139
|
+
'Cache-Control': 'no-cache',
|
|
140
|
+
Connection: 'keep-alive',
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
} else {
|
|
144
|
+
const result = await agent.callChatModel(prompt);
|
|
145
|
+
|
|
146
|
+
return NextResponse.json({
|
|
147
|
+
id: `chatcmpl-${Math.random().toString(36).substring(2, 15)}`,
|
|
148
|
+
object: 'chat.completion',
|
|
149
|
+
created: Math.floor(Date.now() / 1000),
|
|
150
|
+
model: model || 'promptbook-agent',
|
|
151
|
+
choices: [
|
|
152
|
+
{
|
|
153
|
+
index: 0,
|
|
154
|
+
message: {
|
|
155
|
+
role: 'assistant',
|
|
156
|
+
content: result.content,
|
|
157
|
+
},
|
|
158
|
+
finish_reason: 'stop',
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
usage: {
|
|
162
|
+
prompt_tokens: result.usage?.input?.tokensCount?.value || 0,
|
|
163
|
+
completion_tokens: result.usage?.output?.tokensCount?.value || 0,
|
|
164
|
+
total_tokens: (result.usage?.input?.tokensCount?.value || 0) + (result.usage?.output?.tokensCount?.value || 0),
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error('Error in OpenAI API handler:', error);
|
|
171
|
+
return NextResponse.json(
|
|
172
|
+
{ error: { message: (error as Error).message || 'Internal Server Error', type: 'server_error' } },
|
|
173
|
+
{ status: 500 },
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// import { BookEditor } from '@promptbook-local/components';
|
|
4
4
|
import { $provideServer } from '@/src/tools/$provideServer';
|
|
5
5
|
import { PROMPTBOOK_COLOR } from '@promptbook-local/core';
|
|
6
|
-
import { CodeIcon, HistoryIcon, NotebookPenIcon } from 'lucide-react';
|
|
6
|
+
import { CodeIcon, HistoryIcon, MessageCircleQuestionIcon, MessageSquareIcon, NotebookPenIcon } from 'lucide-react';
|
|
7
7
|
import { headers } from 'next/headers';
|
|
8
8
|
import { notFound } from 'next/navigation';
|
|
9
9
|
import { Color } from '../../../../../../src/utils/color/Color';
|
|
@@ -26,13 +26,20 @@ import { isUserAdmin } from '../../../utils/isUserAdmin';
|
|
|
26
26
|
|
|
27
27
|
export const generateMetadata = generateAgentMetadata;
|
|
28
28
|
|
|
29
|
-
export default async function AgentPage({
|
|
29
|
+
export default async function AgentPage({
|
|
30
|
+
params,
|
|
31
|
+
searchParams,
|
|
32
|
+
}: {
|
|
33
|
+
params: Promise<{ agentName: string }>;
|
|
34
|
+
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
|
35
|
+
}) {
|
|
30
36
|
// const [apiKey, setApiKey] = useStateInLocalStorage<string>('openai-apiKey', () => '');
|
|
31
37
|
// const [isApiKeyVisible, setIsApiKeyVisible] = useState(false);
|
|
32
38
|
// const [isApiKeySectionCollapsed, setIsApiKeySectionCollapsed] = useState(!!apiKey);
|
|
33
39
|
|
|
34
40
|
$sideEffect(headers());
|
|
35
41
|
|
|
42
|
+
const { message } = await searchParams;
|
|
36
43
|
const agentName = await getAgentName(params);
|
|
37
44
|
const isAdmin = await isUserAdmin();
|
|
38
45
|
|
|
@@ -155,6 +162,20 @@ export default async function AgentPage({ params }: { params: Promise<{ agentNam
|
|
|
155
162
|
Maintenance
|
|
156
163
|
</h2>
|
|
157
164
|
<div className="flex flex-col gap-2">
|
|
165
|
+
<a
|
|
166
|
+
href={`/admin/chat-history?agentName=${encodeURIComponent(agentName)}`}
|
|
167
|
+
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"
|
|
168
|
+
>
|
|
169
|
+
<MessageSquareIcon className="mr-2 w-3 h-3" />
|
|
170
|
+
Chat history
|
|
171
|
+
</a>
|
|
172
|
+
<a
|
|
173
|
+
href={`/admin/chat-feedback?agentName=${encodeURIComponent(agentName)}`}
|
|
174
|
+
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"
|
|
175
|
+
>
|
|
176
|
+
<MessageCircleQuestionIcon className="mr-2 w-3 h-3" />
|
|
177
|
+
Chat feedback
|
|
178
|
+
</a>
|
|
158
179
|
<ClearAgentChatHistoryButton agentName={agentName} />
|
|
159
180
|
<ClearAgentChatFeedbackButton agentName={agentName} />
|
|
160
181
|
</div>
|
|
@@ -181,7 +202,7 @@ export default async function AgentPage({ params }: { params: Promise<{ agentNam
|
|
|
181
202
|
|
|
182
203
|
{/* Main content: Chat */}
|
|
183
204
|
<div className="flex-1 relative h-full bg-white">
|
|
184
|
-
<AgentChatWrapper agentUrl={agentUrl} />
|
|
205
|
+
<AgentChatWrapper agentUrl={agentUrl} defaultMessage={message as string} />
|
|
185
206
|
</div>
|
|
186
207
|
</div>
|
|
187
208
|
);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { $getTableName } from '../../../database/$getTableName';
|
|
2
|
+
import { $provideSupabase } from '../../../database/$provideSupabase';
|
|
3
|
+
import { isUserAdmin } from '../../../utils/isUserAdmin';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
6
|
+
|
|
7
|
+
export async function GET(request: NextRequest) {
|
|
8
|
+
if (!(await isUserAdmin())) {
|
|
9
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const supabase = $provideSupabase();
|
|
13
|
+
const table = await $getTableName('ApiTokens');
|
|
14
|
+
|
|
15
|
+
const { data, error } = await supabase.from(table).select('*').order('createdAt', { ascending: false });
|
|
16
|
+
|
|
17
|
+
if (error) {
|
|
18
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return NextResponse.json(data);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function POST(request: NextRequest) {
|
|
25
|
+
if (!(await isUserAdmin())) {
|
|
26
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const body = await request.json();
|
|
31
|
+
const { note } = body;
|
|
32
|
+
|
|
33
|
+
const token = `ptbk_${randomUUID().replace(/-/g, '')}`;
|
|
34
|
+
|
|
35
|
+
const supabase = $provideSupabase();
|
|
36
|
+
const table = await $getTableName('ApiTokens');
|
|
37
|
+
|
|
38
|
+
const { data, error } = await supabase
|
|
39
|
+
.from(table)
|
|
40
|
+
.insert({ token, note })
|
|
41
|
+
.select()
|
|
42
|
+
.single();
|
|
43
|
+
|
|
44
|
+
if (error) {
|
|
45
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return NextResponse.json(data);
|
|
49
|
+
} catch (e) {
|
|
50
|
+
return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function DELETE(request: NextRequest) {
|
|
55
|
+
if (!(await isUserAdmin())) {
|
|
56
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const searchParams = request.nextUrl.searchParams;
|
|
60
|
+
const id = searchParams.get('id');
|
|
61
|
+
|
|
62
|
+
if (!id) {
|
|
63
|
+
return NextResponse.json({ error: 'ID is required' }, { status: 400 });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const supabase = $provideSupabase();
|
|
67
|
+
const table = await $getTableName('ApiTokens');
|
|
68
|
+
|
|
69
|
+
const { error } = await supabase.from(table).delete().eq('id', parseInt(id, 10));
|
|
70
|
+
|
|
71
|
+
if (error) {
|
|
72
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return NextResponse.json({ success: true });
|
|
76
|
+
}
|
|
@@ -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">
|
|
@@ -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;
|