@jxtools/promptline 1.0.0
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/README.md +45 -0
- package/bin/promptline.mjs +181 -0
- package/index.html +15 -0
- package/package.json +55 -0
- package/promptline-prompt-queue.sh +173 -0
- package/promptline-session-end.sh +58 -0
- package/promptline-session-register.sh +113 -0
- package/src/App.tsx +70 -0
- package/src/api/client.ts +55 -0
- package/src/backend/queue-store.ts +172 -0
- package/src/components/AddPromptForm.tsx +131 -0
- package/src/components/ProjectDetail.tsx +136 -0
- package/src/components/PromptCard.tsx +279 -0
- package/src/components/SessionSection.tsx +164 -0
- package/src/components/Sidebar.tsx +127 -0
- package/src/components/StatusBar.tsx +71 -0
- package/src/hooks/useQueue.ts +6 -0
- package/src/hooks/useQueues.ts +53 -0
- package/src/hooks/useSSE.ts +37 -0
- package/src/index.css +34 -0
- package/src/main.tsx +10 -0
- package/src/types/queue.ts +33 -0
- package/tsconfig.app.json +29 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite-plugin-api.ts +307 -0
- package/vite.config.ts +14 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import type { ProjectView } from '../types/queue';
|
|
3
|
+
import { api } from '../api/client';
|
|
4
|
+
import { useSSE } from './useSSE';
|
|
5
|
+
|
|
6
|
+
const FALLBACK_POLL_MS = 2000;
|
|
7
|
+
|
|
8
|
+
export function useProjects() {
|
|
9
|
+
const [projects, setProjects] = useState<ProjectView[]>([]);
|
|
10
|
+
const [loading, setLoading] = useState(true);
|
|
11
|
+
const [error, setError] = useState<string | null>(null);
|
|
12
|
+
const fallbackRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
13
|
+
|
|
14
|
+
const handleProjects = useCallback((data: ProjectView[]) => {
|
|
15
|
+
setProjects(data);
|
|
16
|
+
setError(null);
|
|
17
|
+
setLoading(false);
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
const { connected } = useSSE({ onProjects: handleProjects });
|
|
21
|
+
|
|
22
|
+
const refresh = useCallback(async () => {
|
|
23
|
+
try {
|
|
24
|
+
const data = await api.listProjects();
|
|
25
|
+
setProjects(data);
|
|
26
|
+
setError(null);
|
|
27
|
+
} catch (err: unknown) {
|
|
28
|
+
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
29
|
+
} finally {
|
|
30
|
+
setLoading(false);
|
|
31
|
+
}
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
// Fallback polling when SSE is disconnected (also handles initial load if SSE is slow)
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (connected) {
|
|
37
|
+
if (fallbackRef.current) {
|
|
38
|
+
clearInterval(fallbackRef.current);
|
|
39
|
+
fallbackRef.current = null;
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
fallbackRef.current = setInterval(refresh, FALLBACK_POLL_MS);
|
|
44
|
+
return () => {
|
|
45
|
+
if (fallbackRef.current) {
|
|
46
|
+
clearInterval(fallbackRef.current);
|
|
47
|
+
fallbackRef.current = null;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}, [connected, refresh]);
|
|
51
|
+
|
|
52
|
+
return { projects, loading, error, refresh };
|
|
53
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
2
|
+
import type { ProjectView } from '../types/queue';
|
|
3
|
+
|
|
4
|
+
interface UseSSEOptions {
|
|
5
|
+
onProjects: (projects: ProjectView[]) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function useSSE({ onProjects }: UseSSEOptions) {
|
|
9
|
+
const [connected, setConnected] = useState(false);
|
|
10
|
+
const callbackRef = useRef(onProjects);
|
|
11
|
+
callbackRef.current = onProjects;
|
|
12
|
+
|
|
13
|
+
const connect = useCallback(() => {
|
|
14
|
+
const es = new EventSource('/api/events');
|
|
15
|
+
|
|
16
|
+
es.addEventListener('projects', (event: MessageEvent) => {
|
|
17
|
+
try {
|
|
18
|
+
const data = JSON.parse(event.data) as ProjectView[];
|
|
19
|
+
callbackRef.current(data);
|
|
20
|
+
} catch {
|
|
21
|
+
// Ignore malformed events
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
es.onopen = () => setConnected(true);
|
|
26
|
+
es.onerror = () => setConnected(false);
|
|
27
|
+
|
|
28
|
+
return es;
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const es = connect();
|
|
33
|
+
return () => es.close();
|
|
34
|
+
}, [connect]);
|
|
35
|
+
|
|
36
|
+
return { connected };
|
|
37
|
+
}
|
package/src/index.css
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
@theme {
|
|
4
|
+
--color-bg: #0a0a0f;
|
|
5
|
+
--color-surface: #111118;
|
|
6
|
+
--color-border: #1e1e2e;
|
|
7
|
+
--color-text: #e2e8f0;
|
|
8
|
+
--color-muted: #64748b;
|
|
9
|
+
--color-active: #4ade80;
|
|
10
|
+
--color-running: #a78bfa;
|
|
11
|
+
--color-pending: #60a5fa;
|
|
12
|
+
--color-idle: #f472b6;
|
|
13
|
+
--color-completed: #34d399;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
html {
|
|
17
|
+
font-family: 'JetBrains Mono', monospace;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
body {
|
|
21
|
+
margin: 0;
|
|
22
|
+
background: var(--color-bg);
|
|
23
|
+
color: var(--color-text);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* Pulsing animation for active status dots */
|
|
27
|
+
@keyframes pulse-dot {
|
|
28
|
+
0%, 100% { opacity: 1; }
|
|
29
|
+
50% { opacity: 0.5; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.animate-pulse-dot {
|
|
33
|
+
animation: pulse-dot 2s ease-in-out infinite;
|
|
34
|
+
}
|
package/src/main.tsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type PromptStatus = 'pending' | 'running' | 'completed';
|
|
2
|
+
export type SessionStatus = 'active' | 'idle';
|
|
3
|
+
export type QueueStatus = 'active' | 'completed' | 'empty';
|
|
4
|
+
|
|
5
|
+
export interface Prompt {
|
|
6
|
+
id: string;
|
|
7
|
+
text: string;
|
|
8
|
+
status: PromptStatus;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
completedAt: string | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SessionQueue {
|
|
14
|
+
sessionId: string;
|
|
15
|
+
project: string;
|
|
16
|
+
directory: string;
|
|
17
|
+
sessionName: string | null;
|
|
18
|
+
prompts: Prompt[];
|
|
19
|
+
startedAt: string;
|
|
20
|
+
lastActivity: string;
|
|
21
|
+
currentPromptId: string | null;
|
|
22
|
+
completedAt: string | null;
|
|
23
|
+
closedAt: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type SessionWithStatus = SessionQueue & { status: SessionStatus };
|
|
27
|
+
|
|
28
|
+
export interface ProjectView {
|
|
29
|
+
project: string;
|
|
30
|
+
directory: string;
|
|
31
|
+
sessions: SessionWithStatus[];
|
|
32
|
+
queueStatus: QueueStatus;
|
|
33
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"types": ["vite/client"],
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
|
|
11
|
+
/* Bundler mode */
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"moduleDetection": "force",
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"jsx": "react-jsx",
|
|
18
|
+
|
|
19
|
+
/* Linting */
|
|
20
|
+
"strict": true,
|
|
21
|
+
"noUnusedLocals": true,
|
|
22
|
+
"noUnusedParameters": true,
|
|
23
|
+
"erasableSyntaxOnly": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true,
|
|
25
|
+
"noUncheckedSideEffectImports": true
|
|
26
|
+
},
|
|
27
|
+
"include": ["src"],
|
|
28
|
+
"exclude": ["src/backend"]
|
|
29
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"erasableSyntaxOnly": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noUncheckedSideEffectImports": true
|
|
24
|
+
},
|
|
25
|
+
"include": ["vite.config.ts", "vite-plugin-api.ts", "src/backend/**/*.ts", "src/types/**/*.ts"]
|
|
26
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import type { Plugin, Connect } from 'vite';
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
|
+
import { mkdirSync, watch } from 'node:fs';
|
|
4
|
+
import type { FSWatcher } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
8
|
+
import type { PromptStatus } from './src/types/queue.ts';
|
|
9
|
+
import {
|
|
10
|
+
listProjects,
|
|
11
|
+
getProject,
|
|
12
|
+
deleteProject,
|
|
13
|
+
readSession,
|
|
14
|
+
writeSession,
|
|
15
|
+
withComputedStatus,
|
|
16
|
+
deleteSession,
|
|
17
|
+
addPrompt,
|
|
18
|
+
updatePrompt,
|
|
19
|
+
deletePrompt,
|
|
20
|
+
reorderPrompts,
|
|
21
|
+
} from './src/backend/queue-store.ts';
|
|
22
|
+
|
|
23
|
+
const QUEUES_DIR = join(homedir(), '.promptline', 'queues');
|
|
24
|
+
|
|
25
|
+
function parseBody(req: IncomingMessage): Promise<Record<string, unknown>> {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
let body = '';
|
|
28
|
+
req.on('data', (chunk: Buffer) => {
|
|
29
|
+
body += chunk.toString();
|
|
30
|
+
});
|
|
31
|
+
req.on('end', () => {
|
|
32
|
+
try {
|
|
33
|
+
resolve(body ? (JSON.parse(body) as Record<string, unknown>) : {});
|
|
34
|
+
} catch {
|
|
35
|
+
reject(new Error('Invalid JSON body'));
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
req.on('error', reject);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function json(res: ServerResponse, status: number, data: unknown): void {
|
|
43
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
44
|
+
res.end(JSON.stringify(data));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function jsonError(res: ServerResponse, status: number, message: string): void {
|
|
48
|
+
json(res, status, { error: message });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --- SSE connection manager ---
|
|
52
|
+
const sseClients = new Set<ServerResponse>();
|
|
53
|
+
let fsWatcher: FSWatcher | null = null;
|
|
54
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
55
|
+
const DEBOUNCE_MS = 200;
|
|
56
|
+
|
|
57
|
+
function broadcastProjects(): void {
|
|
58
|
+
const data = JSON.stringify(listProjects(QUEUES_DIR));
|
|
59
|
+
const message = `event: projects\ndata: ${data}\n\n`;
|
|
60
|
+
for (const client of sseClients) {
|
|
61
|
+
client.write(message);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function startWatcher(): void {
|
|
66
|
+
if (fsWatcher) return;
|
|
67
|
+
mkdirSync(QUEUES_DIR, { recursive: true });
|
|
68
|
+
try {
|
|
69
|
+
fsWatcher = watch(QUEUES_DIR, { recursive: true }, (_event, filename) => {
|
|
70
|
+
if (!filename || !filename.endsWith('.json')) return;
|
|
71
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
72
|
+
debounceTimer = setTimeout(broadcastProjects, DEBOUNCE_MS);
|
|
73
|
+
});
|
|
74
|
+
} catch {
|
|
75
|
+
// fs.watch not supported — SSE will work but without auto-push
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function stopWatcher(): void {
|
|
80
|
+
if (fsWatcher) {
|
|
81
|
+
fsWatcher.close();
|
|
82
|
+
fsWatcher = null;
|
|
83
|
+
}
|
|
84
|
+
if (debounceTimer) {
|
|
85
|
+
clearTimeout(debounceTimer);
|
|
86
|
+
debounceTimer = null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const HEARTBEAT_MS = 25_000;
|
|
91
|
+
|
|
92
|
+
function handleSSE(_req: IncomingMessage, res: ServerResponse): void {
|
|
93
|
+
res.writeHead(200, {
|
|
94
|
+
'Content-Type': 'text/event-stream',
|
|
95
|
+
'Cache-Control': 'no-cache',
|
|
96
|
+
'Connection': 'keep-alive',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Send initial state
|
|
100
|
+
const data = JSON.stringify(listProjects(QUEUES_DIR));
|
|
101
|
+
res.write(`event: projects\ndata: ${data}\n\n`);
|
|
102
|
+
|
|
103
|
+
const heartbeat = setInterval(() => {
|
|
104
|
+
res.write(': heartbeat\n\n');
|
|
105
|
+
}, HEARTBEAT_MS);
|
|
106
|
+
|
|
107
|
+
sseClients.add(res);
|
|
108
|
+
|
|
109
|
+
res.on('close', () => {
|
|
110
|
+
clearInterval(heartbeat);
|
|
111
|
+
sseClients.delete(res);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export default function apiPlugin(): Plugin {
|
|
116
|
+
return {
|
|
117
|
+
name: 'promptline-api',
|
|
118
|
+
configureServer(server) {
|
|
119
|
+
startWatcher();
|
|
120
|
+
|
|
121
|
+
server.httpServer?.on('close', () => {
|
|
122
|
+
stopWatcher();
|
|
123
|
+
for (const client of sseClients) {
|
|
124
|
+
client.end();
|
|
125
|
+
}
|
|
126
|
+
sseClients.clear();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
server.middlewares.use(((req: IncomingMessage, res: ServerResponse, next: Connect.NextFunction) => {
|
|
130
|
+
const url = req.url ?? '';
|
|
131
|
+
const method = req.method ?? 'GET';
|
|
132
|
+
|
|
133
|
+
if (!url.startsWith('/api/')) {
|
|
134
|
+
next();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// SSE endpoint
|
|
139
|
+
if (url === '/api/events' && method === 'GET') {
|
|
140
|
+
handleSSE(req, res);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
handleApi(url, method, req, res).catch((err: unknown) => {
|
|
145
|
+
const message = err instanceof Error ? err.message : 'Internal server error';
|
|
146
|
+
jsonError(res, 500, message);
|
|
147
|
+
});
|
|
148
|
+
}) as Connect.NextHandleFunction);
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function handleApi(
|
|
154
|
+
url: string,
|
|
155
|
+
method: string,
|
|
156
|
+
req: IncomingMessage,
|
|
157
|
+
res: ServerResponse,
|
|
158
|
+
): Promise<void> {
|
|
159
|
+
// Parse URL segments: /api/projects/:project/sessions/:sessionId/prompts/:promptId
|
|
160
|
+
const segments = url.replace(/^\/api\//, '').split('/').map(decodeURIComponent);
|
|
161
|
+
|
|
162
|
+
// GET /api/projects
|
|
163
|
+
if (segments[0] === 'projects' && segments.length === 1 && method === 'GET') {
|
|
164
|
+
return json(res, 200, listProjects(QUEUES_DIR));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// /api/projects/:project
|
|
168
|
+
if (segments[0] === 'projects' && segments.length === 2) {
|
|
169
|
+
const project = segments[1];
|
|
170
|
+
|
|
171
|
+
if (method === 'GET') {
|
|
172
|
+
const pv = getProject(QUEUES_DIR, project);
|
|
173
|
+
if (!pv) return jsonError(res, 404, `Project "${project}" not found`);
|
|
174
|
+
return json(res, 200, pv);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (method === 'DELETE') {
|
|
178
|
+
try {
|
|
179
|
+
deleteProject(QUEUES_DIR, project);
|
|
180
|
+
} catch (err: unknown) {
|
|
181
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
182
|
+
return jsonError(res, 404, `Project "${project}" not found`);
|
|
183
|
+
}
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
186
|
+
return json(res, 200, { deleted: project });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return jsonError(res, 405, `Method ${method} not allowed`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// /api/projects/:project/sessions/:sessionId
|
|
193
|
+
if (segments[0] === 'projects' && segments[2] === 'sessions' && segments.length === 4) {
|
|
194
|
+
const project = segments[1];
|
|
195
|
+
const sessionId = segments[3];
|
|
196
|
+
|
|
197
|
+
if (method === 'GET') {
|
|
198
|
+
const session = readSession(QUEUES_DIR, project, sessionId);
|
|
199
|
+
if (!session) return jsonError(res, 404, 'Session not found');
|
|
200
|
+
return json(res, 200, withComputedStatus(session));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (method === 'DELETE') {
|
|
204
|
+
try {
|
|
205
|
+
deleteSession(QUEUES_DIR, project, sessionId);
|
|
206
|
+
} catch (err: unknown) {
|
|
207
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
208
|
+
return jsonError(res, 404, 'Session not found');
|
|
209
|
+
}
|
|
210
|
+
throw err;
|
|
211
|
+
}
|
|
212
|
+
return json(res, 200, { deleted: sessionId });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return jsonError(res, 405, `Method ${method} not allowed`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// /api/projects/:project/sessions/:sessionId/prompts/reorder
|
|
219
|
+
if (
|
|
220
|
+
segments[0] === 'projects' && segments[2] === 'sessions' &&
|
|
221
|
+
segments[4] === 'prompts' && segments[5] === 'reorder' &&
|
|
222
|
+
segments.length === 6 && method === 'PUT'
|
|
223
|
+
) {
|
|
224
|
+
const project = segments[1];
|
|
225
|
+
const sessionId = segments[3];
|
|
226
|
+
const session = readSession(QUEUES_DIR, project, sessionId);
|
|
227
|
+
if (!session) return jsonError(res, 404, 'Session not found');
|
|
228
|
+
|
|
229
|
+
const body = await parseBody(req);
|
|
230
|
+
const order = body.order as string[] | undefined;
|
|
231
|
+
if (!Array.isArray(order)) {
|
|
232
|
+
return jsonError(res, 400, 'Field "order" (string[]) is required');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const error = reorderPrompts(session, order);
|
|
236
|
+
if (error) return jsonError(res, 400, error);
|
|
237
|
+
|
|
238
|
+
writeSession(QUEUES_DIR, project, session);
|
|
239
|
+
return json(res, 200, session);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// /api/projects/:project/sessions/:sessionId/prompts/:promptId
|
|
243
|
+
if (
|
|
244
|
+
segments[0] === 'projects' && segments[2] === 'sessions' &&
|
|
245
|
+
segments[4] === 'prompts' && segments.length === 6
|
|
246
|
+
) {
|
|
247
|
+
const project = segments[1];
|
|
248
|
+
const sessionId = segments[3];
|
|
249
|
+
const promptId = segments[5];
|
|
250
|
+
const session = readSession(QUEUES_DIR, project, sessionId);
|
|
251
|
+
if (!session) return jsonError(res, 404, 'Session not found');
|
|
252
|
+
|
|
253
|
+
if (method === 'PATCH') {
|
|
254
|
+
const body = await parseBody(req);
|
|
255
|
+
const updates: { text?: string; status?: PromptStatus } = {};
|
|
256
|
+
|
|
257
|
+
if (body.text !== undefined) {
|
|
258
|
+
updates.text = body.text as string;
|
|
259
|
+
}
|
|
260
|
+
if (body.status !== undefined) {
|
|
261
|
+
const validStatuses: PromptStatus[] = ['pending', 'running', 'completed'];
|
|
262
|
+
if (!validStatuses.includes(body.status as PromptStatus)) {
|
|
263
|
+
return jsonError(res, 400, `Invalid status. Must be one of: ${validStatuses.join(', ')}`);
|
|
264
|
+
}
|
|
265
|
+
updates.status = body.status as PromptStatus;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const updated = updatePrompt(session, promptId, updates);
|
|
269
|
+
if (!updated) return jsonError(res, 404, `Prompt "${promptId}" not found`);
|
|
270
|
+
|
|
271
|
+
writeSession(QUEUES_DIR, project, session);
|
|
272
|
+
return json(res, 200, updated);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (method === 'DELETE') {
|
|
276
|
+
const removed = deletePrompt(session, promptId);
|
|
277
|
+
if (!removed) return jsonError(res, 404, `Prompt "${promptId}" not found`);
|
|
278
|
+
|
|
279
|
+
writeSession(QUEUES_DIR, project, session);
|
|
280
|
+
return json(res, 200, removed);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return jsonError(res, 405, `Method ${method} not allowed`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// /api/projects/:project/sessions/:sessionId/prompts
|
|
287
|
+
if (
|
|
288
|
+
segments[0] === 'projects' && segments[2] === 'sessions' &&
|
|
289
|
+
segments[4] === 'prompts' && segments.length === 5 && method === 'POST'
|
|
290
|
+
) {
|
|
291
|
+
const project = segments[1];
|
|
292
|
+
const sessionId = segments[3];
|
|
293
|
+
const session = readSession(QUEUES_DIR, project, sessionId);
|
|
294
|
+
if (!session) return jsonError(res, 404, 'Session not found');
|
|
295
|
+
|
|
296
|
+
const body = await parseBody(req);
|
|
297
|
+
if (!body.text || typeof body.text !== 'string') {
|
|
298
|
+
return jsonError(res, 400, 'Field "text" (string) is required');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const prompt = addPrompt(session, uuidv4(), body.text as string);
|
|
302
|
+
writeSession(QUEUES_DIR, project, session);
|
|
303
|
+
return json(res, 201, prompt);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return jsonError(res, 404, 'Not found');
|
|
307
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
4
|
+
import apiPlugin from './vite-plugin-api.ts'
|
|
5
|
+
|
|
6
|
+
const randomPort = 3000 + Math.floor(Math.random() * 7000);
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
plugins: [react(), tailwindcss(), apiPlugin()],
|
|
10
|
+
server: {
|
|
11
|
+
port: randomPort,
|
|
12
|
+
open: true,
|
|
13
|
+
},
|
|
14
|
+
})
|