@promptbook/cli 0.112.0-97 → 0.112.0-98
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/README.md +3 -3
- package/apps/agents-server/src/app/admin/cli-access/CliAccessClient.tsx +99 -0
- package/apps/agents-server/src/app/admin/cli-access/page.tsx +14 -0
- package/apps/agents-server/src/app/admin/code-runners/CodeRunnersClient.tsx +76 -325
- package/apps/agents-server/src/app/admin/database/page.tsx +1 -2
- package/apps/agents-server/src/app/agents/[agentName]/chat/CanonicalAgentChatSurface.tsx +24 -0
- package/apps/agents-server/src/app/api/admin/cli-access/route.ts +137 -0
- package/apps/agents-server/src/app/api/admin/code-runners/authentication/route.ts +7 -64
- package/apps/agents-server/src/app/api/admin/servers/[serverId]/route.ts +3 -3
- package/apps/agents-server/src/app/api/admin/servers/route.ts +4 -4
- package/apps/agents-server/src/app/api/chat/export/pdf/route.ts +63 -0
- package/apps/agents-server/src/components/AdminTerminal/AdminTerminalCard.tsx +279 -0
- package/apps/agents-server/src/components/AdminTerminal/useAdminTerminalSession.ts +336 -0
- package/apps/agents-server/src/components/Header/buildHeaderSystemMenuItems.ts +4 -0
- package/apps/agents-server/src/database/$provideClientSql.ts +17 -4
- package/apps/agents-server/src/database/$provideDatabaseAdminExecutor.ts +24 -3
- package/apps/agents-server/src/database/$provideSupabaseForServer.ts +1 -11
- package/apps/agents-server/src/database/agentsServerDatabaseMode.ts +1 -20
- package/apps/agents-server/src/languages/ServerTranslationKeys.ts +1 -0
- package/apps/agents-server/src/languages/translations/czech.yaml +1 -0
- package/apps/agents-server/src/languages/translations/english.yaml +1 -0
- package/apps/agents-server/src/tools/$provideServer.ts +2 -2
- package/apps/agents-server/src/tools/BrowserConnectionProvider.ts +1 -1
- package/apps/agents-server/src/utils/chatExport/downloadChatPdfFromServer.ts +59 -0
- package/apps/agents-server/src/utils/chatExport/renderHtmlToPdfOnServer.ts +37 -0
- package/apps/agents-server/src/utils/codeRunnerAuthentication.ts +77 -237
- package/apps/agents-server/src/utils/createInteractiveTerminalEventStream.ts +84 -0
- package/apps/agents-server/src/utils/interactiveTerminalSession.ts +442 -0
- package/apps/agents-server/src/utils/serverCliAccess.ts +221 -0
- package/apps/agents-server/src/utils/serverRegistry.ts +4 -4
- package/apps/agents-server/src/utils/vpsConfiguration.ts +2 -0
- package/esm/apps/agents-server/src/database/agentsServerDatabaseMode.d.ts +1 -9
- package/esm/index.es.js +2 -2
- package/esm/src/book-components/Chat/Chat/ChatActionsBar.d.ts +2 -0
- package/esm/src/book-components/Chat/Chat/ChatProps.d.ts +6 -0
- package/esm/src/book-components/Chat/save/_common/ChatSaveFormatHandler.d.ts +35 -0
- package/esm/src/book-components/Chat/save/_common/createChatExportFilename.d.ts +11 -0
- package/esm/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/book-components/Chat/Chat/Chat.tsx +2 -0
- package/src/book-components/Chat/Chat/ChatActionsBar.tsx +17 -9
- package/src/book-components/Chat/Chat/ChatProps.tsx +7 -0
- package/src/book-components/Chat/save/_common/ChatSaveFormatHandler.ts +40 -0
- package/src/book-components/Chat/save/_common/createChatExportFilename.ts +20 -0
- package/src/cli/cli-commands/agents-server/ensureAgentsServerEnvFile.ts +1 -1
- package/src/other/templates/getTemplatesPipelineCollection.ts +721 -736
- package/src/version.ts +2 -2
- package/src/versions.txt +1 -0
- package/umd/apps/agents-server/src/database/agentsServerDatabaseMode.d.ts +1 -9
- package/umd/index.umd.js +2 -2
- package/umd/src/book-components/Chat/Chat/ChatActionsBar.d.ts +2 -0
- package/umd/src/book-components/Chat/Chat/ChatProps.d.ts +6 -0
- package/umd/src/book-components/Chat/save/_common/ChatSaveFormatHandler.d.ts +35 -0
- package/umd/src/book-components/Chat/save/_common/createChatExportFilename.d.ts +11 -0
- package/umd/src/version.d.ts +1 -1
- package/apps/agents-server/src/database/$providePostgresPool.ts +0 -27
- package/apps/agents-server/src/database/postgres/$provideLocalPostgresSupabase.ts +0 -1261
- package/src/conversion/validation/_importPipeline.ts +0 -88
- /package/esm/src/conversion/validation/{_importPipeline.d.ts → _importPipeline.test.d.ts} +0 -0
- /package/umd/src/conversion/validation/{_importPipeline.d.ts → _importPipeline.test.d.ts} +0 -0
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
import { Chat } from '@promptbook-local/components';
|
|
4
4
|
import { useCallback, useMemo, type CSSProperties, type ReactNode } from 'react';
|
|
5
5
|
import type { ChatParticipant } from '../../../../../../../src/book-components/Chat/types/ChatParticipant';
|
|
6
|
+
import type { ChatSaveFormatHandlerOptions } from '../../../../../../../src/book-components/Chat/save/_common/ChatSaveFormatHandler';
|
|
6
7
|
import { useAgentBackground } from '../../../../components/AgentProfile/useAgentBackground';
|
|
7
8
|
import { useChatEnterBehaviorPreferences } from '../../../../components/ChatEnterBehavior/ChatEnterBehaviorPreferencesProvider';
|
|
9
|
+
import { notifyError } from '../../../../components/Notifications/notifications';
|
|
8
10
|
import { useChatVisualMode } from '../../../../components/ChatVisualMode/ChatVisualModeProvider';
|
|
9
11
|
import { useServerLanguage } from '../../../../components/ServerLanguage/ServerLanguageProvider';
|
|
10
12
|
import { ChatThreadLoadingSkeleton } from '../../../../components/Skeleton/ChatThreadLoadingSkeleton';
|
|
11
13
|
import { useSoundSystem } from '../../../../components/SoundSystemProvider/SoundSystemProvider';
|
|
12
14
|
import { usePromptbookTheme } from '../../../../components/ThemeMode/usePromptbookTheme';
|
|
13
15
|
import { createDefaultChatEffects } from '../../../../utils/chat/createDefaultChatEffects';
|
|
16
|
+
import { downloadChatPdfFromServer } from '../../../../utils/chatExport/downloadChatPdfFromServer';
|
|
14
17
|
import { executeQuickActionButton } from '../../../../utils/chat/executeQuickActionButton';
|
|
15
18
|
import {
|
|
16
19
|
isChatFeedbackEnabled,
|
|
@@ -175,6 +178,26 @@ export function CanonicalAgentChatSurface({
|
|
|
175
178
|
</button>
|
|
176
179
|
);
|
|
177
180
|
}, [cancellableJob, isReadOnly, onCancelActiveJob, translateText]);
|
|
181
|
+
const handlePdfSaveFormat = useCallback(
|
|
182
|
+
async ({ title, messages, participants }: ChatSaveFormatHandlerOptions) => {
|
|
183
|
+
try {
|
|
184
|
+
await downloadChatPdfFromServer({
|
|
185
|
+
title,
|
|
186
|
+
messages,
|
|
187
|
+
participants,
|
|
188
|
+
});
|
|
189
|
+
} catch (error) {
|
|
190
|
+
notifyError(error instanceof Error ? error.message : 'Failed to export chat as PDF.');
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
[],
|
|
194
|
+
);
|
|
195
|
+
const saveFormatHandlers = useMemo(
|
|
196
|
+
() => ({
|
|
197
|
+
pdf: handlePdfSaveFormat,
|
|
198
|
+
}),
|
|
199
|
+
[handlePdfSaveFormat],
|
|
200
|
+
);
|
|
178
201
|
const extraActionNodes = useMemo(
|
|
179
202
|
() => (
|
|
180
203
|
<>
|
|
@@ -235,6 +258,7 @@ export function CanonicalAgentChatSurface({
|
|
|
235
258
|
elevenLabsVoiceId={state.elevenLabsVoiceId}
|
|
236
259
|
teamAgentProfiles={state.teamAgentProfiles}
|
|
237
260
|
extraActions={extraActionNodes}
|
|
261
|
+
saveFormatHandlers={saveFormatHandlers}
|
|
238
262
|
theme={promptbookTheme}
|
|
239
263
|
>
|
|
240
264
|
{isReadOnly && frozenChatBannerLabel && (
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { isUserGlobalAdmin } from '@/src/utils/isUserGlobalAdmin';
|
|
3
|
+
import { createInteractiveTerminalEventStream } from '@/src/utils/createInteractiveTerminalEventStream';
|
|
4
|
+
import {
|
|
5
|
+
getLatestServerCliAccessSession,
|
|
6
|
+
getServerCliAccessSession,
|
|
7
|
+
startServerCliAccessSession,
|
|
8
|
+
stopServerCliAccessSession,
|
|
9
|
+
subscribeToServerCliAccessSession,
|
|
10
|
+
writeServerCliAccessSessionInput,
|
|
11
|
+
} from '@/src/utils/serverCliAccess';
|
|
12
|
+
|
|
13
|
+
export const runtime = 'nodejs';
|
|
14
|
+
export const dynamic = 'force-dynamic';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Loads the latest CLI access session or streams a specific terminal session.
|
|
18
|
+
*/
|
|
19
|
+
export async function GET(request: Request) {
|
|
20
|
+
if (!(await isUserGlobalAdmin())) {
|
|
21
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const { searchParams } = new URL(request.url);
|
|
26
|
+
const sessionId = searchParams.get('sessionId')?.trim() || '';
|
|
27
|
+
const isStreamRequested = searchParams.get('stream') === '1';
|
|
28
|
+
|
|
29
|
+
if (isStreamRequested) {
|
|
30
|
+
if (!sessionId) {
|
|
31
|
+
return NextResponse.json({ error: 'CLI access session id is required.' }, { status: 400 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const session = getServerCliAccessSession(sessionId);
|
|
35
|
+
if (!session) {
|
|
36
|
+
return NextResponse.json({ error: 'CLI access session was not found.' }, { status: 404 });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return createInteractiveTerminalEventStream(
|
|
40
|
+
request,
|
|
41
|
+
sessionId,
|
|
42
|
+
session,
|
|
43
|
+
subscribeToServerCliAccessSession,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return NextResponse.json({
|
|
48
|
+
session: getLatestServerCliAccessSession(),
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return NextResponse.json(
|
|
52
|
+
{ error: error instanceof Error ? error.message : 'Failed to load the CLI access session.' },
|
|
53
|
+
{ status: 500 },
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Starts or reconnects to the raw server shell exposed in the browser.
|
|
60
|
+
*/
|
|
61
|
+
export async function POST() {
|
|
62
|
+
if (!(await isUserGlobalAdmin())) {
|
|
63
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
return NextResponse.json({
|
|
68
|
+
session: await startServerCliAccessSession(),
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return NextResponse.json(
|
|
72
|
+
{ error: error instanceof Error ? error.message : 'Failed to start the CLI access session.' },
|
|
73
|
+
{ status: 500 },
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sends raw input to the running CLI access shell.
|
|
80
|
+
*/
|
|
81
|
+
export async function PATCH(request: Request) {
|
|
82
|
+
if (!(await isUserGlobalAdmin())) {
|
|
83
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const body = (await request.json().catch(() => null)) as
|
|
88
|
+
| {
|
|
89
|
+
readonly sessionId?: string;
|
|
90
|
+
readonly input?: string;
|
|
91
|
+
}
|
|
92
|
+
| null;
|
|
93
|
+
|
|
94
|
+
if (!body?.sessionId || typeof body.input !== 'string') {
|
|
95
|
+
return NextResponse.json({ error: 'CLI access session input is required.' }, { status: 400 });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return NextResponse.json({
|
|
99
|
+
session: writeServerCliAccessSessionInput(body.sessionId, body.input),
|
|
100
|
+
});
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return NextResponse.json(
|
|
103
|
+
{ error: error instanceof Error ? error.message : 'Failed to send CLI access input.' },
|
|
104
|
+
{ status: 500 },
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Stops one running CLI access shell session.
|
|
111
|
+
*/
|
|
112
|
+
export async function DELETE(request: Request) {
|
|
113
|
+
if (!(await isUserGlobalAdmin())) {
|
|
114
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const body = (await request.json().catch(() => null)) as
|
|
119
|
+
| {
|
|
120
|
+
readonly sessionId?: string;
|
|
121
|
+
}
|
|
122
|
+
| null;
|
|
123
|
+
|
|
124
|
+
if (!body?.sessionId) {
|
|
125
|
+
return NextResponse.json({ error: 'CLI access session id is required.' }, { status: 400 });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return NextResponse.json({
|
|
129
|
+
session: stopServerCliAccessSession(body.sessionId),
|
|
130
|
+
});
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return NextResponse.json(
|
|
133
|
+
{ error: error instanceof Error ? error.message : 'Failed to stop the CLI access session.' },
|
|
134
|
+
{ status: 500 },
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
2
|
import { isUserGlobalAdmin } from '@/src/utils/isUserGlobalAdmin';
|
|
3
|
+
import { createInteractiveTerminalEventStream } from '@/src/utils/createInteractiveTerminalEventStream';
|
|
3
4
|
import {
|
|
4
5
|
getCodeRunnerAuthenticationSession,
|
|
5
6
|
getLatestCodeRunnerAuthenticationSession,
|
|
@@ -36,7 +37,12 @@ export async function GET(request: Request) {
|
|
|
36
37
|
return NextResponse.json({ error: 'Authentication session was not found.' }, { status: 404 });
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
return
|
|
40
|
+
return createInteractiveTerminalEventStream(
|
|
41
|
+
request,
|
|
42
|
+
sessionId,
|
|
43
|
+
session,
|
|
44
|
+
subscribeToCodeRunnerAuthenticationSession,
|
|
45
|
+
);
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
const { agent } = await readConfiguredCodeRunner();
|
|
@@ -132,66 +138,3 @@ export async function DELETE(request: Request) {
|
|
|
132
138
|
);
|
|
133
139
|
}
|
|
134
140
|
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Creates one SSE response that replays buffered output and then streams live terminal events.
|
|
138
|
-
*
|
|
139
|
-
* @param request - Browser stream request.
|
|
140
|
-
* @param sessionId - Authentication session id.
|
|
141
|
-
* @param session - Existing session snapshot.
|
|
142
|
-
* @returns Event stream response.
|
|
143
|
-
*/
|
|
144
|
-
function createCodeRunnerAuthenticationEventStream(
|
|
145
|
-
request: Request,
|
|
146
|
-
sessionId: string,
|
|
147
|
-
session: NonNullable<ReturnType<typeof getCodeRunnerAuthenticationSession>>,
|
|
148
|
-
): Response {
|
|
149
|
-
const encoder = new TextEncoder();
|
|
150
|
-
|
|
151
|
-
return new Response(
|
|
152
|
-
new ReadableStream({
|
|
153
|
-
start(controller) {
|
|
154
|
-
const emitEvent = (event: string, payload: unknown) => {
|
|
155
|
-
controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`));
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
emitEvent('snapshot', session);
|
|
159
|
-
|
|
160
|
-
if (!session.isRunning) {
|
|
161
|
-
controller.close();
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const unsubscribe = subscribeToCodeRunnerAuthenticationSession(sessionId, {
|
|
166
|
-
onOutput: ({ chunk }) => emitEvent('output', { chunk }),
|
|
167
|
-
onExit: ({ snapshot: nextSession }) => {
|
|
168
|
-
emitEvent('exit', nextSession);
|
|
169
|
-
unsubscribe?.();
|
|
170
|
-
controller.close();
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
if (!unsubscribe) {
|
|
175
|
-
controller.error(new Error('Authentication session was not found.'));
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
request.signal.addEventListener(
|
|
180
|
-
'abort',
|
|
181
|
-
() => {
|
|
182
|
-
unsubscribe();
|
|
183
|
-
controller.close();
|
|
184
|
-
},
|
|
185
|
-
{ once: true },
|
|
186
|
-
);
|
|
187
|
-
},
|
|
188
|
-
}),
|
|
189
|
-
{
|
|
190
|
-
headers: {
|
|
191
|
-
'Content-Type': 'text/event-stream',
|
|
192
|
-
'Cache-Control': 'no-cache, no-transform',
|
|
193
|
-
Connection: 'keep-alive',
|
|
194
|
-
},
|
|
195
|
-
},
|
|
196
|
-
);
|
|
197
|
-
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
|
-
import {
|
|
2
|
+
import { isAgentsServerSqliteMode } from '../../../../../database/agentsServerDatabaseMode';
|
|
3
3
|
import { resolveCurrentServerRegistryContext } from '../../../../../utils/currentServerRegistryContext';
|
|
4
4
|
import { isUserGlobalAdmin } from '../../../../../utils/isUserGlobalAdmin';
|
|
5
5
|
import {
|
|
@@ -37,7 +37,7 @@ export async function PATCH(request: Request, context: { params: Promise<{ serve
|
|
|
37
37
|
const body = (await request.json()) as Omit<UpdateServerInput, 'id'>;
|
|
38
38
|
const parsedServerId = parseManagedServerId(serverId);
|
|
39
39
|
|
|
40
|
-
if (
|
|
40
|
+
if (isAgentsServerSqliteMode()) {
|
|
41
41
|
const updatedServer = await updateStandaloneVpsServerDomain(parsedServerId, body.domain);
|
|
42
42
|
await applyStandaloneVpsServerMetadata({
|
|
43
43
|
tablePrefix: updatedServer.tablePrefix,
|
|
@@ -76,7 +76,7 @@ export async function DELETE(_request: Request, context: { params: Promise<{ ser
|
|
|
76
76
|
const { serverId } = await context.params;
|
|
77
77
|
const parsedServerId = parseManagedServerId(serverId);
|
|
78
78
|
|
|
79
|
-
if (
|
|
79
|
+
if (isAgentsServerSqliteMode()) {
|
|
80
80
|
await deleteStandaloneVpsServerDomain(parsedServerId);
|
|
81
81
|
return NextResponse.json({
|
|
82
82
|
success: true,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
2
|
import { spaceTrim } from 'spacetrim';
|
|
3
3
|
import { DatabaseError } from '../../../../../../../src/errors/DatabaseError';
|
|
4
|
-
import {
|
|
4
|
+
import { isAgentsServerSqliteMode } from '../../../../database/agentsServerDatabaseMode';
|
|
5
5
|
import { resolveCurrentServerRegistryContext } from '../../../../utils/currentServerRegistryContext';
|
|
6
6
|
import { isUserAdmin } from '../../../../utils/isUserAdmin';
|
|
7
7
|
import { isUserGlobalAdmin } from '../../../../utils/isUserGlobalAdmin';
|
|
@@ -41,7 +41,7 @@ export async function GET() {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
const context = await resolveCurrentServerRegistryContext();
|
|
44
|
-
const servers =
|
|
44
|
+
const servers = isAgentsServerSqliteMode()
|
|
45
45
|
? await createStandaloneVpsServersResponse(context.registeredServers)
|
|
46
46
|
: context.registeredServers;
|
|
47
47
|
|
|
@@ -49,7 +49,7 @@ export async function GET() {
|
|
|
49
49
|
servers,
|
|
50
50
|
currentServerId: context.currentServer?.id ?? null,
|
|
51
51
|
canEdit: await isUserGlobalAdmin(),
|
|
52
|
-
isStandaloneVps:
|
|
52
|
+
isStandaloneVps: isAgentsServerSqliteMode(),
|
|
53
53
|
});
|
|
54
54
|
} catch (error) {
|
|
55
55
|
return NextResponse.json(
|
|
@@ -96,7 +96,7 @@ export async function POST(request: Request) {
|
|
|
96
96
|
assertGlobalAdminAccess(await isUserGlobalAdmin());
|
|
97
97
|
|
|
98
98
|
const body = withEnvironmentAdminUser((await request.json()) as CreateServerInput);
|
|
99
|
-
if (
|
|
99
|
+
if (isAgentsServerSqliteMode()) {
|
|
100
100
|
const normalizedDomain = normalizeServerDomain(body.domain);
|
|
101
101
|
if (!normalizedDomain) {
|
|
102
102
|
return NextResponse.json({ error: 'A valid domain is required.' }, { status: 400 });
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { buildChatHtml } from '../../../../../../../../src/book-components/Chat/save/html/htmlSaveFormatDefinition';
|
|
2
|
+
import type { ChatMessage } from '../../../../../../../../src/book-components/Chat/types/ChatMessage';
|
|
3
|
+
import type { ChatParticipant } from '../../../../../../../../src/book-components/Chat/types/ChatParticipant';
|
|
4
|
+
import { createChatExportFilename } from '../../../../../../../../src/book-components/Chat/save/_common/createChatExportFilename';
|
|
5
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
6
|
+
import { renderHtmlToPdfOnServer } from '@/src/utils/chatExport/renderHtmlToPdfOnServer';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* PDF export requires the Node.js runtime because it depends on Playwright.
|
|
10
|
+
*/
|
|
11
|
+
export const runtime = 'nodejs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Minimal request payload accepted by the chat PDF export endpoint.
|
|
15
|
+
*
|
|
16
|
+
* @private internal type for POST /api/chat/export/pdf
|
|
17
|
+
*/
|
|
18
|
+
type ChatPdfExportRequestBody = {
|
|
19
|
+
readonly title: string;
|
|
20
|
+
readonly messages: ReadonlyArray<ChatMessage>;
|
|
21
|
+
readonly participants: ReadonlyArray<ChatParticipant>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Builds a server-rendered PDF from the standalone HTML chat export.
|
|
26
|
+
*/
|
|
27
|
+
export async function POST(request: NextRequest) {
|
|
28
|
+
let requestBody: ChatPdfExportRequestBody;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
requestBody = (await request.json()) as ChatPdfExportRequestBody;
|
|
32
|
+
} catch {
|
|
33
|
+
return NextResponse.json({ error: 'Invalid JSON payload.' }, { status: 400 });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (
|
|
37
|
+
typeof requestBody?.title !== 'string' ||
|
|
38
|
+
!Array.isArray(requestBody.messages) ||
|
|
39
|
+
!Array.isArray(requestBody.participants)
|
|
40
|
+
) {
|
|
41
|
+
return NextResponse.json(
|
|
42
|
+
{ error: 'Expected `title`, `messages`, and `participants` in the PDF export payload.' },
|
|
43
|
+
{ status: 400 },
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const pdfBuffer = await renderHtmlToPdfOnServer(
|
|
49
|
+
buildChatHtml(requestBody.title, requestBody.messages, requestBody.participants),
|
|
50
|
+
);
|
|
51
|
+
const filename = createChatExportFilename(requestBody.title, 'pdf');
|
|
52
|
+
|
|
53
|
+
return new NextResponse(pdfBuffer, {
|
|
54
|
+
headers: {
|
|
55
|
+
'Content-Type': 'application/pdf',
|
|
56
|
+
'Content-Disposition': `attachment; filename="${filename}"`,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Failed to export chat PDF:', error);
|
|
61
|
+
return NextResponse.json({ error: 'Failed to export chat as PDF.' }, { status: 500 });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Loader2, Play, Send, SquareTerminal } from 'lucide-react';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
import { useEffect, useRef } from 'react';
|
|
6
|
+
import { Card } from '../Homepage/Card';
|
|
7
|
+
import type { AdminTerminalSession } from './useAdminTerminalSession';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* One quick terminal action rendered next to the input box.
|
|
11
|
+
*/
|
|
12
|
+
type AdminTerminalQuickAction = {
|
|
13
|
+
/**
|
|
14
|
+
* Visible button label.
|
|
15
|
+
*/
|
|
16
|
+
readonly label: string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Raw terminal input sent when the button is clicked.
|
|
20
|
+
*/
|
|
21
|
+
readonly input: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Props accepted by the shared admin terminal card.
|
|
26
|
+
*/
|
|
27
|
+
type AdminTerminalCardProps<TSession extends AdminTerminalSession> = {
|
|
28
|
+
/**
|
|
29
|
+
* Title shown above the terminal card.
|
|
30
|
+
*/
|
|
31
|
+
readonly title: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Short description of what the terminal is for.
|
|
35
|
+
*/
|
|
36
|
+
readonly description: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Optional extra hint shown below the description.
|
|
40
|
+
*/
|
|
41
|
+
readonly hint?: string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Active or latest terminal session snapshot.
|
|
45
|
+
*/
|
|
46
|
+
readonly session: TSession | null;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Controlled terminal input value.
|
|
50
|
+
*/
|
|
51
|
+
readonly input: string;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Updates the controlled terminal input.
|
|
55
|
+
*/
|
|
56
|
+
readonly onInputChange: (value: string) => void;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Starts or reconnects to the terminal session.
|
|
60
|
+
*/
|
|
61
|
+
readonly onStart: () => void;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Stops the active terminal session.
|
|
65
|
+
*/
|
|
66
|
+
readonly onStop: () => void;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Sends one raw input chunk to the terminal.
|
|
70
|
+
*/
|
|
71
|
+
readonly onSend: (input: string) => void;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether the surrounding page is still loading the initial terminal state.
|
|
75
|
+
*/
|
|
76
|
+
readonly isLoading?: boolean;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Whether a session start request is currently pending.
|
|
80
|
+
*/
|
|
81
|
+
readonly isStarting?: boolean;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Whether a terminal input request is currently pending.
|
|
85
|
+
*/
|
|
86
|
+
readonly isSending?: boolean;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Whether a session stop request is currently pending.
|
|
90
|
+
*/
|
|
91
|
+
readonly isStopping?: boolean;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Label used for the start button while the session is idle.
|
|
95
|
+
*/
|
|
96
|
+
readonly startLabel: string;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Label used for the start button while the session is already running.
|
|
100
|
+
*/
|
|
101
|
+
readonly runningLabel?: string;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Label used for the stop button.
|
|
105
|
+
*/
|
|
106
|
+
readonly stopLabel: string;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Label shown above the terminal output.
|
|
110
|
+
*/
|
|
111
|
+
readonly outputLabel: string;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Empty-state text shown before any terminal output exists.
|
|
115
|
+
*/
|
|
116
|
+
readonly outputEmptyState: string;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Placeholder used by the terminal input control.
|
|
120
|
+
*/
|
|
121
|
+
readonly inputPlaceholder: string;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Optional shortcut buttons that send raw terminal input.
|
|
125
|
+
*/
|
|
126
|
+
readonly quickActions?: ReadonlyArray<AdminTerminalQuickAction>;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Optional extra content inserted between the description and the terminal output.
|
|
130
|
+
*/
|
|
131
|
+
readonly children?: ReactNode;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const INPUT_CLASS_NAME =
|
|
135
|
+
'w-full rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-900 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 disabled:bg-gray-50 disabled:text-gray-500';
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Shared terminal card used by super-admin pages that expose interactive browser terminals.
|
|
139
|
+
*/
|
|
140
|
+
export function AdminTerminalCard<TSession extends AdminTerminalSession>({
|
|
141
|
+
title,
|
|
142
|
+
description,
|
|
143
|
+
hint,
|
|
144
|
+
session,
|
|
145
|
+
input,
|
|
146
|
+
onInputChange,
|
|
147
|
+
onStart,
|
|
148
|
+
onStop,
|
|
149
|
+
onSend,
|
|
150
|
+
isLoading = false,
|
|
151
|
+
isStarting = false,
|
|
152
|
+
isSending = false,
|
|
153
|
+
isStopping = false,
|
|
154
|
+
startLabel,
|
|
155
|
+
runningLabel,
|
|
156
|
+
stopLabel,
|
|
157
|
+
outputLabel,
|
|
158
|
+
outputEmptyState,
|
|
159
|
+
inputPlaceholder,
|
|
160
|
+
quickActions = [],
|
|
161
|
+
children,
|
|
162
|
+
}: AdminTerminalCardProps<TSession>) {
|
|
163
|
+
const outputReference = useRef<HTMLPreElement | null>(null);
|
|
164
|
+
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (!outputReference.current) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
outputReference.current.scrollTop = outputReference.current.scrollHeight;
|
|
171
|
+
}, [session?.output]);
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<Card className="hover:border-gray-200 hover:shadow-md">
|
|
175
|
+
<div className="space-y-4">
|
|
176
|
+
<div className="space-y-2">
|
|
177
|
+
<h2 className="text-lg font-semibold text-slate-900">{title}</h2>
|
|
178
|
+
<p className="text-sm text-slate-600">{description}</p>
|
|
179
|
+
{hint ? <p className="text-sm text-slate-600">{hint}</p> : null}
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
183
|
+
<button
|
|
184
|
+
type="button"
|
|
185
|
+
onClick={onStart}
|
|
186
|
+
disabled={isLoading || isStarting || session?.isRunning}
|
|
187
|
+
className="inline-flex items-center gap-2 rounded-md bg-slate-900 px-4 py-2 text-sm font-semibold text-white hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-60"
|
|
188
|
+
>
|
|
189
|
+
{isStarting ? <Loader2 className="h-4 w-4 animate-spin" /> : <Play className="h-4 w-4" />}
|
|
190
|
+
{session?.isRunning ? runningLabel || startLabel : startLabel}
|
|
191
|
+
</button>
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
onClick={onStop}
|
|
195
|
+
disabled={!session?.isRunning || isStopping}
|
|
196
|
+
className="inline-flex items-center gap-2 rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-60"
|
|
197
|
+
>
|
|
198
|
+
{isStopping ? (
|
|
199
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
200
|
+
) : (
|
|
201
|
+
<SquareTerminal className="h-4 w-4" />
|
|
202
|
+
)}
|
|
203
|
+
{stopLabel}
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
{children}
|
|
208
|
+
|
|
209
|
+
<div className="space-y-2">
|
|
210
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
211
|
+
<h3 className="text-sm font-semibold text-slate-700">{outputLabel}</h3>
|
|
212
|
+
{session ? (
|
|
213
|
+
<span className="text-xs text-slate-500">
|
|
214
|
+
{session.isRunning
|
|
215
|
+
? 'Running'
|
|
216
|
+
: session.exitCode === 0
|
|
217
|
+
? 'Finished successfully'
|
|
218
|
+
: 'Finished with an error'}
|
|
219
|
+
</span>
|
|
220
|
+
) : (
|
|
221
|
+
<span className="text-xs text-slate-500">No session started yet.</span>
|
|
222
|
+
)}
|
|
223
|
+
</div>
|
|
224
|
+
<pre
|
|
225
|
+
ref={outputReference}
|
|
226
|
+
className="max-h-96 overflow-auto rounded-xl border border-slate-200 bg-slate-950 p-4 text-xs text-slate-100"
|
|
227
|
+
>
|
|
228
|
+
{session?.output || outputEmptyState}
|
|
229
|
+
</pre>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<form
|
|
233
|
+
className="flex flex-col gap-3 md:flex-row"
|
|
234
|
+
onSubmit={(event) => {
|
|
235
|
+
event.preventDefault();
|
|
236
|
+
|
|
237
|
+
if (!input.trim()) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const formattedInput = input.endsWith('\n') ? input : `${input}\n`;
|
|
242
|
+
onSend(formattedInput);
|
|
243
|
+
onInputChange('');
|
|
244
|
+
}}
|
|
245
|
+
>
|
|
246
|
+
<input
|
|
247
|
+
type="text"
|
|
248
|
+
value={input}
|
|
249
|
+
onChange={(event) => onInputChange(event.target.value)}
|
|
250
|
+
disabled={!session?.isRunning || isSending}
|
|
251
|
+
placeholder={inputPlaceholder}
|
|
252
|
+
className={INPUT_CLASS_NAME}
|
|
253
|
+
/>
|
|
254
|
+
<div className="flex flex-wrap gap-3">
|
|
255
|
+
<button
|
|
256
|
+
type="submit"
|
|
257
|
+
disabled={!session?.isRunning || isSending || input.trim() === ''}
|
|
258
|
+
className="inline-flex items-center justify-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60"
|
|
259
|
+
>
|
|
260
|
+
{isSending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
|
261
|
+
Send
|
|
262
|
+
</button>
|
|
263
|
+
{quickActions.map((quickAction) => (
|
|
264
|
+
<button
|
|
265
|
+
key={`${quickAction.label}:${quickAction.input}`}
|
|
266
|
+
type="button"
|
|
267
|
+
onClick={() => onSend(quickAction.input)}
|
|
268
|
+
disabled={!session?.isRunning || isSending}
|
|
269
|
+
className="inline-flex items-center justify-center rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-60"
|
|
270
|
+
>
|
|
271
|
+
{quickAction.label}
|
|
272
|
+
</button>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
275
|
+
</form>
|
|
276
|
+
</div>
|
|
277
|
+
</Card>
|
|
278
|
+
);
|
|
279
|
+
}
|