promptfoo 0.18.4 → 0.19.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.
Files changed (156) hide show
  1. package/dist/package.json +10 -5
  2. package/dist/src/evaluator.d.ts.map +1 -1
  3. package/dist/src/evaluator.js +17 -9
  4. package/dist/src/evaluator.js.map +1 -1
  5. package/dist/src/index.d.ts +1 -0
  6. package/dist/src/index.d.ts.map +1 -1
  7. package/dist/src/index.js +3 -0
  8. package/dist/src/index.js.map +1 -1
  9. package/dist/src/main.js +2 -2
  10. package/dist/src/main.js.map +1 -1
  11. package/dist/src/providers.d.ts +1 -1
  12. package/dist/src/providers.d.ts.map +1 -1
  13. package/dist/src/providers.js +5 -0
  14. package/dist/src/providers.js.map +1 -1
  15. package/dist/src/share.d.ts.map +1 -1
  16. package/dist/src/share.js +8 -7
  17. package/dist/src/share.js.map +1 -1
  18. package/dist/src/types.d.ts +9 -1
  19. package/dist/src/types.d.ts.map +1 -1
  20. package/dist/src/web/nextui/404/index.html +1 -0
  21. package/dist/src/web/nextui/404.html +1 -0
  22. package/dist/src/web/nextui/_next/static/P9zzdx-rDJKPcGFq_qOXC/_buildManifest.js +1 -0
  23. package/dist/src/web/nextui/_next/static/P9zzdx-rDJKPcGFq_qOXC/_ssgManifest.js +1 -0
  24. package/dist/src/web/nextui/_next/static/chunks/121-54cee610700b4756.js +27 -0
  25. package/dist/src/web/nextui/_next/static/chunks/339-501c32916b785ef1.js +1 -0
  26. package/dist/src/web/nextui/_next/static/chunks/373-6a411db0b05027d3.js +1 -0
  27. package/dist/src/web/nextui/_next/static/chunks/583-507e6d8883bb85ff.js +1 -0
  28. package/dist/src/web/nextui/_next/static/chunks/596-9c29c47b8dee7a50.js +25 -0
  29. package/dist/src/web/nextui/_next/static/chunks/658-f8f9d18540505edc.js +15 -0
  30. package/dist/src/web/nextui/_next/static/chunks/858-7255df6dbc44dff9.js +125 -0
  31. package/dist/src/web/nextui/_next/static/chunks/97-64e11ce2b0607459.js +1 -0
  32. package/dist/src/web/nextui/_next/static/chunks/app/eval/[id]/not-found-366629541fd598e9.js +1 -0
  33. package/dist/src/web/nextui/_next/static/chunks/app/eval/[id]/page-655bc42ac68b25cc.js +1 -0
  34. package/dist/src/web/nextui/_next/static/chunks/app/eval/page-d5e8697859d6294e.js +1 -0
  35. package/dist/src/web/nextui/_next/static/chunks/app/layout-4c714b1a5a3a768d.js +1 -0
  36. package/dist/src/web/nextui/_next/static/chunks/app/page-4fe8a6342d24ca23.js +1 -0
  37. package/dist/src/web/nextui/_next/static/chunks/app/setup/page-cd35686fe6c12be8.js +1 -0
  38. package/dist/src/web/nextui/_next/static/chunks/fd9d1056-d8847af536b5787b.js +9 -0
  39. package/dist/src/web/nextui/_next/static/chunks/framework-8883d1e9be70c3da.js +25 -0
  40. package/dist/src/web/nextui/_next/static/chunks/main-0670de04b1c026b4.js +1 -0
  41. package/dist/src/web/nextui/_next/static/chunks/main-app-581ccf0003955b21.js +1 -0
  42. package/dist/src/web/nextui/_next/static/chunks/pages/_app-52924524f99094ab.js +1 -0
  43. package/dist/src/web/nextui/_next/static/chunks/pages/_error-c92d5c4bb2b49926.js +1 -0
  44. package/dist/src/web/nextui/_next/static/chunks/polyfills-78c92fac7aa8fdd8.js +1 -0
  45. package/dist/src/web/nextui/_next/static/chunks/webpack-a886dd767c2e76b7.js +1 -0
  46. package/dist/src/web/nextui/_next/static/css/48d388184a2f4ce3.css +1 -0
  47. package/dist/src/web/nextui/_next/static/css/7265c36d84346934.css +1 -0
  48. package/dist/src/web/nextui/_next/static/css/8119d8bd13a8adab.css +1 -0
  49. package/dist/src/web/nextui/_next/static/css/a35c840ac696f161.css +1 -0
  50. package/dist/src/web/nextui/_next/static/css/e388dd377baf25ec.css +1 -0
  51. package/dist/src/web/nextui/_next/static/css/fc460b8a7cadb952.css +1 -0
  52. package/dist/src/web/nextui/_next/static/media/0e4fe491bf84089c-s.p.woff2 +0 -0
  53. package/dist/src/web/nextui/_next/static/media/1c57ca6f5208a29b-s.woff2 +0 -0
  54. package/dist/src/web/nextui/_next/static/media/3dbd163d3bb09d47-s.woff2 +0 -0
  55. package/dist/src/web/nextui/_next/static/media/42d52f46a26971a3-s.woff2 +0 -0
  56. package/dist/src/web/nextui/_next/static/media/5647e4c23315a2d2-s.woff2 +0 -0
  57. package/dist/src/web/nextui/_next/static/media/627622453ef56b0d-s.p.woff2 +0 -0
  58. package/dist/src/web/nextui/_next/static/media/7be645d133f3ee22-s.woff2 +0 -0
  59. package/dist/src/web/nextui/_next/static/media/7c53f7419436e04b-s.woff2 +0 -0
  60. package/dist/src/web/nextui/_next/static/media/8fb72f69fba4e3d2-s.woff2 +0 -0
  61. package/dist/src/web/nextui/_next/static/media/912a9cfe43c928d9-s.woff2 +0 -0
  62. package/dist/src/web/nextui/_next/static/media/934c4b7cb736f2a3-s.p.woff2 +0 -0
  63. package/dist/src/web/nextui/_next/static/media/a5b77b63ef20339c-s.woff2 +0 -0
  64. package/dist/src/web/nextui/_next/static/media/a6d330d7873e7320-s.woff2 +0 -0
  65. package/dist/src/web/nextui/_next/static/media/baf12dd90520ae41-s.woff2 +0 -0
  66. package/dist/src/web/nextui/_next/static/media/bbdb6f0234009aba-s.woff2 +0 -0
  67. package/dist/src/web/nextui/_next/static/media/cff529cd86cc0276-s.woff2 +0 -0
  68. package/dist/src/web/nextui/_next/static/media/d117eea74e01de14-s.woff2 +0 -0
  69. package/dist/src/web/nextui/_next/static/media/dfa8b99978df7bbc-s.woff2 +0 -0
  70. package/dist/src/web/nextui/_next/static/media/e25729ca87cc7df9-s.woff2 +0 -0
  71. package/dist/src/web/nextui/_next/static/media/eb52b768f62eeeb4-s.woff2 +0 -0
  72. package/dist/src/web/nextui/_next/static/media/f06116e890b3dadb-s.woff2 +0 -0
  73. package/dist/src/web/nextui/api +1 -0
  74. package/dist/src/web/nextui/eval/index.html +1 -0
  75. package/dist/src/web/nextui/eval/index.txt +13 -0
  76. package/dist/src/web/nextui/index.html +1 -0
  77. package/dist/src/web/nextui/index.txt +13 -0
  78. package/dist/src/web/nextui/setup/index.html +1 -0
  79. package/dist/src/web/nextui/setup/index.txt +14 -0
  80. package/dist/src/web/server.d.ts +1 -1
  81. package/dist/src/web/server.d.ts.map +1 -1
  82. package/dist/src/web/server.js +47 -4
  83. package/dist/src/web/server.js.map +1 -1
  84. package/package.json +10 -5
  85. package/src/evaluator.ts +17 -9
  86. package/src/index.ts +7 -1
  87. package/src/main.ts +3 -3
  88. package/src/providers.ts +11 -2
  89. package/src/share.ts +10 -8
  90. package/src/types.ts +10 -1
  91. package/src/web/nextui/.eslintrc.json +3 -0
  92. package/src/web/nextui/next.config.js +14 -0
  93. package/src/web/nextui/package-lock.json +4615 -0
  94. package/src/web/nextui/package.json +45 -0
  95. package/src/web/nextui/src/app/Home.css +3 -0
  96. package/src/web/nextui/src/app/api/route.ts +6 -0
  97. package/src/web/{client/src/NavBar.css → nextui/src/app/components/DarkMode.css} +1 -0
  98. package/src/web/{client/src/NavBar.tsx → nextui/src/app/components/DarkMode.tsx} +4 -9
  99. package/src/web/nextui/src/app/components/Logo.css +32 -0
  100. package/src/web/nextui/src/app/components/PageShell.css +33 -0
  101. package/src/web/nextui/src/app/components/PageShell.tsx +87 -0
  102. package/src/web/{client/src → nextui/src/app/eval}/ConfigModal.tsx +8 -5
  103. package/src/web/nextui/src/app/eval/Eval.css +13 -0
  104. package/src/web/nextui/src/app/eval/Eval.tsx +79 -0
  105. package/src/web/{client/src → nextui/src/app/eval}/EvalOutputPromptDialog.tsx +2 -2
  106. package/src/web/{client/src → nextui/src/app/eval}/ResultsTable.css +10 -12
  107. package/src/web/{client/src → nextui/src/app/eval}/ResultsTable.tsx +57 -14
  108. package/src/web/{client/src → nextui/src/app/eval}/ResultsView.tsx +4 -4
  109. package/src/web/nextui/src/app/eval/[id]/not-found.tsx +5 -0
  110. package/src/web/nextui/src/app/eval/[id]/page.css +9 -0
  111. package/src/web/nextui/src/app/eval/[id]/page.tsx +20 -0
  112. package/src/web/nextui/src/app/eval/index.css +0 -0
  113. package/src/web/nextui/src/app/eval/page.tsx +8 -0
  114. package/src/web/{client/src → nextui/src/app/eval}/store.ts +2 -2
  115. package/src/web/nextui/src/app/eval/types.ts +20 -0
  116. package/src/web/{client/src/index.css → nextui/src/app/globals.css} +21 -3
  117. package/src/web/nextui/src/app/layout.tsx +25 -0
  118. package/src/web/nextui/src/app/page.tsx +7 -0
  119. package/src/web/nextui/src/app/setup/AssertsForm.tsx +118 -0
  120. package/src/web/nextui/src/app/setup/PromptDialog.tsx +77 -0
  121. package/src/web/nextui/src/app/setup/PromptsSection.tsx +190 -0
  122. package/src/web/nextui/src/app/setup/ProviderConfigDialog.tsx +99 -0
  123. package/src/web/nextui/src/app/setup/ProviderSelector.tsx +149 -0
  124. package/src/web/nextui/src/app/setup/RunTestSuiteButton.tsx +88 -0
  125. package/src/web/nextui/src/app/setup/TestCaseDialog.tsx +108 -0
  126. package/src/web/nextui/src/app/setup/TestCasesSection.tsx +154 -0
  127. package/src/web/nextui/src/app/setup/VarsForm.tsx +57 -0
  128. package/src/web/nextui/src/app/setup/page.css +3 -0
  129. package/src/web/nextui/src/app/setup/page.tsx +160 -0
  130. package/src/web/nextui/src/util/api.ts +1 -0
  131. package/src/web/nextui/src/util/store.ts +53 -0
  132. package/src/web/nextui/tsconfig.json +28 -0
  133. package/src/web/server.ts +56 -2
  134. package/dist/src/web/client/assets/index-6d2a3573.js +0 -200
  135. package/dist/src/web/client/assets/index-d2b6a160.css +0 -1
  136. package/dist/src/web/client/assets/js-yaml-8bbf9398.js +0 -32
  137. package/dist/src/web/client/index.html +0 -15
  138. package/src/web/client/.eslintrc.cjs +0 -14
  139. package/src/web/client/index.html +0 -13
  140. package/src/web/client/package-lock.json +0 -5726
  141. package/src/web/client/package.json +0 -39
  142. package/src/web/client/src/App.css +0 -4
  143. package/src/web/client/src/App.tsx +0 -120
  144. package/src/web/client/src/Logo.css +0 -18
  145. package/src/web/client/src/main.tsx +0 -10
  146. package/src/web/client/src/types.ts +0 -36
  147. package/src/web/client/src/vite-env.d.ts +0 -1
  148. package/src/web/client/tsconfig.json +0 -24
  149. package/src/web/client/tsconfig.node.json +0 -10
  150. package/src/web/client/vite.config.ts +0 -7
  151. /package/dist/src/web/{client → nextui}/favicon.ico +0 -0
  152. /package/dist/src/web/{client → nextui}/logo.svg +0 -0
  153. /package/src/web/{client → nextui}/public/favicon.ico +0 -0
  154. /package/src/web/{client → nextui}/public/logo.svg +0 -0
  155. /package/src/web/{client/src → nextui/src/app/components}/Logo.tsx +0 -0
  156. /package/src/web/{client/src → nextui/src/app/eval}/ShareModal.tsx +0 -0
@@ -0,0 +1,160 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useEffect } from 'react';
4
+ import Link from 'next/link';
5
+ import yaml from 'js-yaml';
6
+ import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
7
+ import { docco } from 'react-syntax-highlighter/dist/cjs/styles/hljs';
8
+ import Button from '@mui/material/Button';
9
+ import Container from '@mui/material/Container';
10
+ import Typography from '@mui/material/Typography';
11
+ import Box from '@mui/material/Box';
12
+ import Stack from '@mui/material/Stack';
13
+ import Dialog from '@mui/material/Dialog';
14
+ import DialogActions from '@mui/material/DialogActions';
15
+ import DialogContent from '@mui/material/DialogContent';
16
+ import DialogContentText from '@mui/material/DialogContentText';
17
+ import DialogTitle from '@mui/material/DialogTitle';
18
+
19
+ import RunTestSuiteButton from './RunTestSuiteButton';
20
+ import PromptsSection from './PromptsSection';
21
+ import TestCasesSection from './TestCasesSection';
22
+ import ProviderSelector from './ProviderSelector';
23
+ import { useStore } from '../../util/store';
24
+
25
+ import './page.css';
26
+
27
+ const EvaluateTestSuiteCreator: React.FC = () => {
28
+ const [yamlString, setYamlString] = useState('');
29
+ const [resetDialogOpen, setResetDialogOpen] = useState(false);
30
+
31
+ const {
32
+ description,
33
+ setDescription,
34
+ providers,
35
+ setProviders,
36
+ prompts,
37
+ setPrompts,
38
+ testCases,
39
+ setTestCases,
40
+ } = useStore();
41
+
42
+ useEffect(() => {
43
+ useStore.persist.rehydrate();
44
+ }, []);
45
+
46
+ useEffect(() => {
47
+ const testSuite = {
48
+ description,
49
+ providers,
50
+ prompts,
51
+ tests: testCases,
52
+ };
53
+ setYamlString(yaml.dump(testSuite));
54
+ }, [description, providers, prompts, testCases]);
55
+
56
+ if (process.env.NEXT_PUBLIC_NO_BROWSING) {
57
+ return null;
58
+ }
59
+
60
+ const extractVarsFromPrompts = (prompts: string[]): string[] => {
61
+ const varRegex = /{{(\w+)}}/g;
62
+ const varsSet = new Set<string>();
63
+
64
+ prompts.forEach((prompt) => {
65
+ let match;
66
+ while ((match = varRegex.exec(prompt)) !== null) {
67
+ varsSet.add(match[1]);
68
+ }
69
+ });
70
+
71
+ return Array.from(varsSet);
72
+ };
73
+
74
+ const varsList = extractVarsFromPrompts(prompts);
75
+
76
+ const handleReset = () => {
77
+ setDescription('');
78
+ setProviders([]);
79
+ setPrompts([]);
80
+ setTestCases([]);
81
+ setYamlString('');
82
+ setResetDialogOpen(false);
83
+ };
84
+
85
+ return (
86
+ <Container maxWidth="lg" sx={{ marginTop: '2rem' }}>
87
+ <Stack direction="row" spacing={2} justifyContent="space-between">
88
+ <Typography variant="h4">Set up an evaluation</Typography>
89
+ <Stack direction="row" spacing={2}>
90
+ <RunTestSuiteButton />
91
+ <Button variant="outlined" color="primary" onClick={() => setResetDialogOpen(true)}>
92
+ Reset
93
+ </Button>
94
+ </Stack>
95
+ </Stack>
96
+ <Box mt={4} />
97
+ {/*
98
+ <Box mt={4}>
99
+ <TextField
100
+ label="Description"
101
+ value={description}
102
+ onChange={(e) => {
103
+ setDescription(e.target.value);
104
+ }}
105
+ fullWidth
106
+ margin="normal"
107
+ />
108
+ </Box>
109
+ */}
110
+ <Box mt={2}>
111
+ <Stack direction="column" spacing={2} justifyContent="space-between">
112
+ <Typography variant="h5">Providers</Typography>
113
+ <ProviderSelector providers={providers} onChange={setProviders} />
114
+ </Stack>
115
+ </Box>
116
+ <Box mt={4} />
117
+ <PromptsSection />
118
+ <Box mt={6} />
119
+ <TestCasesSection varsList={varsList} />
120
+ <Box mt={8}>
121
+ {yamlString && (
122
+ <Box mt={4}>
123
+ <Typography variant="h5" gutterBottom>
124
+ YAML config
125
+ </Typography>
126
+ <Typography variant="body1" gutterBottom>
127
+ This is the evaluation config that is run by promptfoo. See{' '}
128
+ <Link href="https://promptfoo.dev/docs/configuration/guide">configuration docs</Link>{' '}
129
+ to learn more.
130
+ </Typography>
131
+ <SyntaxHighlighter className="yaml-config" language="yaml" style={docco}>
132
+ {yamlString}
133
+ </SyntaxHighlighter>
134
+ </Box>
135
+ )}
136
+ </Box>
137
+ <Dialog
138
+ open={resetDialogOpen}
139
+ onClose={() => setResetDialogOpen(false)}
140
+ aria-labelledby="alert-dialog-title"
141
+ aria-describedby="alert-dialog-description"
142
+ >
143
+ <DialogTitle id="alert-dialog-title">{'Confirm Reset'}</DialogTitle>
144
+ <DialogContent>
145
+ <DialogContentText id="alert-dialog-description">
146
+ Are you sure you want to reset all the fields? This action cannot be undone.
147
+ </DialogContentText>
148
+ </DialogContent>
149
+ <DialogActions>
150
+ <Button onClick={() => setResetDialogOpen(false)}>Cancel</Button>
151
+ <Button onClick={handleReset} autoFocus>
152
+ Reset
153
+ </Button>
154
+ </DialogActions>
155
+ </Dialog>
156
+ </Container>
157
+ );
158
+ };
159
+
160
+ export default EvaluateTestSuiteCreator;
@@ -0,0 +1 @@
1
+ export const API_BASE_URL = `http://localhost:15500`;
@@ -0,0 +1,53 @@
1
+ import { create } from 'zustand';
2
+ import { persist } from 'zustand/middleware';
3
+
4
+ import type { Assertion, ProviderConfig, TestCase } from '../../../../types';
5
+
6
+ export interface State {
7
+ asserts: Assertion[];
8
+ testCases: TestCase[];
9
+ description: string;
10
+ providers: ProviderConfig[];
11
+ prompts: string[];
12
+ setAsserts: (asserts: Assertion[]) => void;
13
+ setTestCases: (testCases: TestCase[]) => void;
14
+ setDescription: (description: string) => void;
15
+ setProviders: (providers: ProviderConfig[]) => void;
16
+ setPrompts: (prompts: string[]) => void;
17
+ }
18
+
19
+ export const useStore = create<State>()(
20
+ persist(
21
+ (set) => ({
22
+ asserts: [],
23
+ testCases: [],
24
+ description: '',
25
+ providers: [],
26
+ prompts: [],
27
+ setAsserts: (asserts) => set({ asserts }),
28
+ setTestCases: (testCases) => set({ testCases }),
29
+ setDescription: (description) => set({ description }),
30
+ setProviders: (providers) => set({ providers }),
31
+ setPrompts: (prompts) => set({ prompts }),
32
+ }),
33
+ {
34
+ name: 'promptfoo',
35
+ skipHydration: true,
36
+ },
37
+ ),
38
+ );
39
+
40
+ /*
41
+ export const useStore = create<State>((set) => ({
42
+ asserts: [],
43
+ testCases: [],
44
+ description: '',
45
+ providers: [],
46
+ prompts: [],
47
+ setAsserts: (asserts) => set({ asserts }),
48
+ setTestCases: (testCases) => set({ testCases }),
49
+ setDescription: (description) => set({ description }),
50
+ setProviders: (providers) => set({ providers }),
51
+ setPrompts: (prompts) => set({ prompts }),
52
+ }));
53
+ */
@@ -0,0 +1,28 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es5",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "forceConsistentCasingInFileNames": true,
9
+ "noEmit": true,
10
+ "esModuleInterop": true,
11
+ "module": "esnext",
12
+ "moduleResolution": "bundler",
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "jsx": "preserve",
16
+ "incremental": true,
17
+ "plugins": [
18
+ {
19
+ "name": "next"
20
+ }
21
+ ],
22
+ "paths": {
23
+ "@/*": ["./src/*"]
24
+ }
25
+ },
26
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27
+ "exclude": ["node_modules"]
28
+ }
package/src/web/server.ts CHANGED
@@ -2,23 +2,37 @@ import fs, { Stats } from 'fs';
2
2
  import path from 'node:path';
3
3
  import readline from 'node:readline';
4
4
  import http from 'node:http';
5
+ import invariant from 'tiny-invariant';
6
+ import { v4 as uuidv4 } from 'uuid';
5
7
 
6
8
  import debounce from 'debounce';
7
9
  import express from 'express';
8
10
  import cors from 'cors';
11
+ import compression from 'compression';
9
12
  import opener from 'opener';
10
13
  import { Server as SocketIOServer } from 'socket.io';
14
+ import promptfoo, { EvaluateSummary } from '../index';
11
15
 
12
16
  import logger from '../logger';
13
17
  import { getDirectory } from '../esm';
14
18
  import { getLatestResultsPath, listPreviousResults, readResult } from '../util';
15
19
 
16
- export function init(port = 15500) {
20
+ interface Job {
21
+ status: 'in-progress' | 'completed';
22
+ progress: number;
23
+ total: number;
24
+ result: EvaluateSummary | null;
25
+ }
26
+
27
+ const evalJobs = new Map<string, Job>();
28
+
29
+ export function startServer(port = 15500) {
17
30
  const app = express();
18
31
 
19
- const staticDir = path.join(getDirectory(), 'web', 'client');
32
+ const staticDir = path.join(getDirectory(), 'web', 'nextui');
20
33
 
21
34
  app.use(cors());
35
+ app.use(compression());
22
36
  app.use(express.json());
23
37
  app.use(express.static(staticDir));
24
38
 
@@ -55,9 +69,49 @@ export function init(port = 15500) {
55
69
 
56
70
  app.get('/results', (req, res) => {
57
71
  const previousResults = listPreviousResults();
72
+ previousResults.reverse();
58
73
  res.json({ data: previousResults });
59
74
  });
60
75
 
76
+ app.post('/api/eval', (req, res) => {
77
+ const testSuite = req.body;
78
+ const id = uuidv4();
79
+ evalJobs.set(id, { status: 'in-progress', progress: 0, total: 0, result: null });
80
+
81
+ promptfoo
82
+ .evaluate(Object.assign({}, testSuite, { writeLatestResults: true }), {
83
+ progressCallback: (progress, total) => {
84
+ const job = evalJobs.get(id);
85
+ invariant(job, 'Job not found');
86
+ job.progress = progress;
87
+ job.total = total;
88
+ console.log(`Progress: ${progress}/${total}`);
89
+ },
90
+ })
91
+ .then((result) => {
92
+ const job = evalJobs.get(id);
93
+ invariant(job, 'Job not found');
94
+ job.status = 'completed';
95
+ job.result = result;
96
+ });
97
+
98
+ res.json({ id });
99
+ });
100
+
101
+ app.get('/api/eval/:id', (req, res) => {
102
+ const id = req.params.id;
103
+ const job = evalJobs.get(id);
104
+ if (!job) {
105
+ res.status(404).json({ error: 'Job not found' });
106
+ return;
107
+ }
108
+ if (job.status === 'completed') {
109
+ res.json({ status: 'completed', result: job.result });
110
+ } else {
111
+ res.json({ status: 'in-progress', progress: job.progress, total: job.total });
112
+ }
113
+ });
114
+
61
115
  app.get('/results/:filename', (req, res) => {
62
116
  const filename = req.params.filename;
63
117
  const safeFilename = path.basename(filename);