promptfoo 0.2.2 → 0.4.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 +55 -17
- package/dist/evaluator.d.ts +1 -1
- package/dist/evaluator.d.ts.map +1 -1
- package/dist/evaluator.js +213 -150
- package/dist/evaluator.js.map +1 -1
- package/dist/main.js +51 -10
- package/dist/main.js.map +1 -1
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +21 -0
- package/dist/prompts.js.map +1 -0
- package/dist/providers.d.ts +1 -0
- package/dist/providers.d.ts.map +1 -1
- package/dist/providers.js +11 -5
- package/dist/providers.js.map +1 -1
- package/dist/tableOutput.html +5 -8
- package/dist/types.d.ts +27 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/util.d.ts +5 -0
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +50 -1
- package/dist/util.js.map +1 -1
- package/dist/web/client/assets/index-710f1308.css +1 -0
- package/dist/web/client/assets/index-900b20c0.js +172 -0
- package/dist/web/client/favicon.ico +0 -0
- package/dist/web/client/index.html +15 -0
- package/dist/web/client/logo.svg +30 -0
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.d.ts.map +1 -0
- package/dist/web/server.js +74 -0
- package/dist/web/server.js.map +1 -0
- package/package.json +14 -3
- package/src/evaluator.ts +271 -174
- package/src/main.ts +52 -10
- package/src/prompts.ts +20 -0
- package/src/providers.ts +33 -15
- package/src/tableOutput.html +5 -8
- package/src/types.ts +32 -7
- package/src/util.ts +60 -1
- package/src/web/client/.eslintrc.cjs +14 -0
- package/src/web/client/index.html +13 -0
- package/src/web/client/package.json +37 -0
- package/src/web/client/public/favicon.ico +0 -0
- package/src/web/client/public/logo.svg +30 -0
- package/src/web/client/src/App.css +0 -0
- package/src/web/client/src/App.tsx +43 -0
- package/src/web/client/src/Logo.css +13 -0
- package/src/web/client/src/Logo.tsx +11 -0
- package/src/web/client/src/NavBar.css +3 -0
- package/src/web/client/src/NavBar.tsx +11 -0
- package/src/web/client/src/ResultsTable.css +133 -0
- package/src/web/client/src/ResultsTable.tsx +261 -0
- package/src/web/client/src/ResultsView.tsx +110 -0
- package/src/web/client/src/index.css +35 -0
- package/src/web/client/src/main.tsx +10 -0
- package/src/web/client/src/store.ts +13 -0
- package/src/web/client/src/types.ts +14 -0
- package/src/web/client/src/vite-env.d.ts +1 -0
- package/src/web/client/tsconfig.json +24 -0
- package/src/web/client/tsconfig.node.json +10 -0
- package/src/web/client/vite.config.ts +7 -0
- package/src/web/server.ts +96 -0
package/src/main.ts
CHANGED
|
@@ -9,8 +9,9 @@ import { Command } from 'commander';
|
|
|
9
9
|
import logger, { setLogLevel } from './logger.js';
|
|
10
10
|
import { loadApiProvider } from './providers.js';
|
|
11
11
|
import { evaluate } from './evaluator.js';
|
|
12
|
-
import { readPrompts, readVars, writeOutput } from './util.js';
|
|
12
|
+
import { readPrompts, readVars, writeLatestResults, writeOutput } from './util.js';
|
|
13
13
|
import { getDirectory } from './esm.js';
|
|
14
|
+
import { init } from './web/server.js';
|
|
14
15
|
|
|
15
16
|
import type { CommandLineOptions, EvaluateOptions, VarMapping } from './types.js';
|
|
16
17
|
|
|
@@ -37,7 +38,7 @@ These prompts are nunjucks templates, so you can use logic like this:
|
|
|
37
38
|
prompts: ['prompts.txt'],
|
|
38
39
|
providers: ['openai:gpt-3.5-turbo'],
|
|
39
40
|
vars: 'vars.csv',
|
|
40
|
-
maxConcurrency:
|
|
41
|
+
maxConcurrency: 4,
|
|
41
42
|
};`;
|
|
42
43
|
const readme = `To get started, set your OPENAI_API_KEY environment variable. Then run:
|
|
43
44
|
\`\`\`
|
|
@@ -96,6 +97,14 @@ async function main() {
|
|
|
96
97
|
createDummyFiles(directory);
|
|
97
98
|
});
|
|
98
99
|
|
|
100
|
+
program
|
|
101
|
+
.command('view')
|
|
102
|
+
.description('Start browser ui')
|
|
103
|
+
.option('-p, --port <number>', 'Port number', '15500')
|
|
104
|
+
.action((cmdObj: { port: number } & Command) => {
|
|
105
|
+
init(cmdObj.port);
|
|
106
|
+
});
|
|
107
|
+
|
|
99
108
|
program
|
|
100
109
|
.command('eval')
|
|
101
110
|
.description('Evaluate prompts')
|
|
@@ -129,7 +138,10 @@ async function main() {
|
|
|
129
138
|
'Maximum number of concurrent API calls',
|
|
130
139
|
String(defaultConfig.maxConcurrency),
|
|
131
140
|
)
|
|
141
|
+
.option('--no-write', 'Do not write results to promptfoo directory')
|
|
142
|
+
.option('--grader', 'Model that will grade outputs', defaultConfig.grader)
|
|
132
143
|
.option('--verbose', 'Show debug logs', defaultConfig.verbose)
|
|
144
|
+
.option('--view', 'View in browser ui')
|
|
133
145
|
.action(async (cmdObj: CommandLineOptions & Command) => {
|
|
134
146
|
if (cmdObj.verbose) {
|
|
135
147
|
setLogLevel('debug');
|
|
@@ -170,6 +182,12 @@ async function main() {
|
|
|
170
182
|
...config,
|
|
171
183
|
};
|
|
172
184
|
|
|
185
|
+
if (cmdObj.grader) {
|
|
186
|
+
options.grading = {
|
|
187
|
+
provider: await loadApiProvider(cmdObj.grader),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
173
191
|
const summary = await evaluate(options);
|
|
174
192
|
|
|
175
193
|
if (cmdObj.output) {
|
|
@@ -178,31 +196,55 @@ async function main() {
|
|
|
178
196
|
} else {
|
|
179
197
|
// Output table by default
|
|
180
198
|
const maxWidth = process.stdout.columns ? process.stdout.columns - 10 : 120;
|
|
181
|
-
const head = summary.table
|
|
199
|
+
const head = summary.table.head;
|
|
200
|
+
const headLength = head.prompts.length + head.vars.length;
|
|
182
201
|
const table = new Table({
|
|
183
|
-
head,
|
|
184
|
-
colWidths: Array(
|
|
202
|
+
head: [...head.prompts, ...head.vars],
|
|
203
|
+
colWidths: Array(headLength).fill(Math.floor(maxWidth / headLength)),
|
|
185
204
|
wordWrap: true,
|
|
186
205
|
wrapOnWordBoundary: true,
|
|
187
206
|
style: {
|
|
188
207
|
head: ['blue', 'bold'],
|
|
189
208
|
},
|
|
190
209
|
});
|
|
191
|
-
// Skip first row (header) and add the rest. Color
|
|
192
|
-
for (const row of summary.table.
|
|
193
|
-
|
|
194
|
-
|
|
210
|
+
// Skip first row (header) and add the rest. Color PASS/FAIL
|
|
211
|
+
for (const row of summary.table.body) {
|
|
212
|
+
table.push([
|
|
213
|
+
...row.outputs.map((col) => {
|
|
214
|
+
if (col.startsWith('[PASS]')) {
|
|
215
|
+
// color '[PASS]' green
|
|
216
|
+
return chalk.green.bold(col.slice(0, 6)) + col.slice(6);
|
|
217
|
+
} else if (col.startsWith('[FAIL]')) {
|
|
218
|
+
// color everything red up until '---'
|
|
219
|
+
return col
|
|
220
|
+
.split('---')
|
|
221
|
+
.map((c, idx) => (idx === 0 ? chalk.red.bold(c) : c))
|
|
222
|
+
.join('---');
|
|
223
|
+
}
|
|
224
|
+
return col;
|
|
225
|
+
}),
|
|
226
|
+
...row.vars,
|
|
227
|
+
]);
|
|
195
228
|
}
|
|
196
229
|
|
|
197
230
|
logger.info('\n' + table.toString());
|
|
198
231
|
}
|
|
199
|
-
|
|
232
|
+
if (cmdObj.noWrite) {
|
|
233
|
+
logger.info('Evaluation complete');
|
|
234
|
+
} else {
|
|
235
|
+
writeLatestResults(summary);
|
|
236
|
+
logger.info(`Evaluation complete. To use web viewer, run ${chalk.green('promptfoo view')}`);
|
|
237
|
+
}
|
|
200
238
|
logger.info(chalk.green.bold(`Successes: ${summary.stats.successes}`));
|
|
201
239
|
logger.info(chalk.red.bold(`Failures: ${summary.stats.failures}`));
|
|
202
240
|
logger.info(
|
|
203
241
|
`Token usage: Total ${summary.stats.tokenUsage.total} Prompt ${summary.stats.tokenUsage.prompt} Completion ${summary.stats.tokenUsage.completion}`,
|
|
204
242
|
);
|
|
205
243
|
logger.info('Done.');
|
|
244
|
+
|
|
245
|
+
if (cmdObj.view) {
|
|
246
|
+
init(15500);
|
|
247
|
+
}
|
|
206
248
|
});
|
|
207
249
|
|
|
208
250
|
program.parse(process.argv);
|
package/src/prompts.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const DEFAULT_GRADING_PROMPT = JSON.stringify([
|
|
2
|
+
{
|
|
3
|
+
role: 'system',
|
|
4
|
+
content: `You are grading content according to a user-specified rubric. If the statement in the rubric is true, then the content passes the test. You respond with a JSON object with this structure: {pass: boolean; reason: string;}.
|
|
5
|
+
|
|
6
|
+
Examples:
|
|
7
|
+
|
|
8
|
+
Content: Hello world
|
|
9
|
+
Rubric: Contains a greeting
|
|
10
|
+
{"pass": true, "reason": "the content contains the word 'world'"}
|
|
11
|
+
|
|
12
|
+
Content: Avast ye swabs, repel the invaders!
|
|
13
|
+
Rubric: Does not speak like a pirate
|
|
14
|
+
{"pass": false, "reason": "'avast ye' is a common pirate term"}`,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
role: 'user',
|
|
18
|
+
content: 'Content: {{ content }}\nRubric: {{ rubric }}',
|
|
19
|
+
},
|
|
20
|
+
]);
|
package/src/providers.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
import fetch from 'node-fetch';
|
|
2
1
|
import path from 'node:path';
|
|
3
2
|
|
|
4
3
|
import { ApiProvider, ProviderResponse } from './types.js';
|
|
4
|
+
import { fetchWithTimeout } from './util.js';
|
|
5
5
|
import logger from './logger.js';
|
|
6
6
|
|
|
7
|
+
const DEFAULT_OPENAI_HOST = 'api.openai.com';
|
|
8
|
+
|
|
9
|
+
const REQUEST_TIMEOUT_MS = process.env.REQUEST_TIMEOUT_MS
|
|
10
|
+
? parseInt(process.env.REQUEST_TIMEOUT_MS, 10)
|
|
11
|
+
: 60_000;
|
|
12
|
+
|
|
7
13
|
export class OpenAiGenericProvider implements ApiProvider {
|
|
8
14
|
modelName: string;
|
|
9
15
|
apiKey: string;
|
|
16
|
+
apiHost: string;
|
|
10
17
|
|
|
11
18
|
constructor(modelName: string, apiKey?: string) {
|
|
12
19
|
this.modelName = modelName;
|
|
@@ -18,6 +25,8 @@ export class OpenAiGenericProvider implements ApiProvider {
|
|
|
18
25
|
);
|
|
19
26
|
}
|
|
20
27
|
this.apiKey = key;
|
|
28
|
+
|
|
29
|
+
this.apiHost = process.env.OPENAI_API_HOST || DEFAULT_OPENAI_HOST;
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
id(): string {
|
|
@@ -56,18 +65,23 @@ export class OpenAiCompletionProvider extends OpenAiGenericProvider {
|
|
|
56
65
|
prompt,
|
|
57
66
|
max_tokens: process.env.OPENAI_MAX_TOKENS || 1024,
|
|
58
67
|
temperature: process.env.OPENAI_TEMPERATURE || 0,
|
|
68
|
+
stop: process.env.OPENAI_STOP ? JSON.parse(process.env.OPENAI_STOP) : undefined,
|
|
59
69
|
};
|
|
60
70
|
logger.debug(`Calling OpenAI API: ${JSON.stringify(body)}`);
|
|
61
71
|
let response, data;
|
|
62
72
|
try {
|
|
63
|
-
response = await
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
response = await fetchWithTimeout(
|
|
74
|
+
`https://${this.apiHost}/v1/completions`,
|
|
75
|
+
{
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': 'application/json',
|
|
79
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
80
|
+
},
|
|
81
|
+
body: JSON.stringify(body),
|
|
68
82
|
},
|
|
69
|
-
|
|
70
|
-
|
|
83
|
+
REQUEST_TIMEOUT_MS,
|
|
84
|
+
);
|
|
71
85
|
|
|
72
86
|
data = (await response.json()) as unknown as any;
|
|
73
87
|
} catch (err) {
|
|
@@ -129,14 +143,18 @@ export class OpenAiChatCompletionProvider extends OpenAiGenericProvider {
|
|
|
129
143
|
|
|
130
144
|
let response, data;
|
|
131
145
|
try {
|
|
132
|
-
response = await
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
146
|
+
response = await fetchWithTimeout(
|
|
147
|
+
`https://${this.apiHost}/v1/chat/completions`,
|
|
148
|
+
{
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: {
|
|
151
|
+
'Content-Type': 'application/json',
|
|
152
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
153
|
+
},
|
|
154
|
+
body: JSON.stringify(body),
|
|
137
155
|
},
|
|
138
|
-
|
|
139
|
-
|
|
156
|
+
REQUEST_TIMEOUT_MS,
|
|
157
|
+
);
|
|
140
158
|
data = (await response.json()) as unknown as any;
|
|
141
159
|
} catch (err) {
|
|
142
160
|
return {
|
package/src/tableOutput.html
CHANGED
|
@@ -20,17 +20,14 @@
|
|
|
20
20
|
th,
|
|
21
21
|
td {
|
|
22
22
|
padding: 5px;
|
|
23
|
+
min-width: 200px;
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
-
tr > td[data-content
|
|
25
|
+
|
|
26
|
+
tr > td[data-content^='[PASS]'] {
|
|
26
27
|
color: green;
|
|
27
|
-
font-weight: bold;
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
tr > td[data-content^='FAIL']:first-child {
|
|
32
|
-
color: red;
|
|
33
|
-
font-weight: bold;
|
|
29
|
+
tr > td[data-content^='[FAIL]'] {
|
|
30
|
+
color: #ad0000;
|
|
34
31
|
}
|
|
35
32
|
</style>
|
|
36
33
|
</head>
|
package/src/types.ts
CHANGED
|
@@ -6,6 +6,9 @@ export interface CommandLineOptions {
|
|
|
6
6
|
config?: string;
|
|
7
7
|
verbose?: boolean;
|
|
8
8
|
maxConcurrency?: number;
|
|
9
|
+
grader?: string;
|
|
10
|
+
view?: boolean;
|
|
11
|
+
noWrite?: boolean;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
export interface ApiProvider {
|
|
@@ -13,7 +16,7 @@ export interface ApiProvider {
|
|
|
13
16
|
callApi: (prompt: string) => Promise<ProviderResponse>;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
interface TokenUsage {
|
|
19
|
+
export interface TokenUsage {
|
|
17
20
|
total: number;
|
|
18
21
|
prompt: number;
|
|
19
22
|
completion: number;
|
|
@@ -31,6 +34,11 @@ export interface CsvRow {
|
|
|
31
34
|
|
|
32
35
|
export type VarMapping = Record<string, string>;
|
|
33
36
|
|
|
37
|
+
export interface GradingConfig {
|
|
38
|
+
prompt?: string;
|
|
39
|
+
provider: ApiProvider;
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
export interface EvaluateOptions {
|
|
35
43
|
providers: ApiProvider[];
|
|
36
44
|
prompts: string[];
|
|
@@ -38,6 +46,8 @@ export interface EvaluateOptions {
|
|
|
38
46
|
|
|
39
47
|
maxConcurrency?: number;
|
|
40
48
|
showProgressBar?: boolean;
|
|
49
|
+
|
|
50
|
+
grading?: GradingConfig;
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
export interface Prompt {
|
|
@@ -53,12 +63,27 @@ export interface EvaluateResult {
|
|
|
53
63
|
success: boolean;
|
|
54
64
|
}
|
|
55
65
|
|
|
66
|
+
export interface EvaluateTable {
|
|
67
|
+
head: {
|
|
68
|
+
prompts: string[];
|
|
69
|
+
vars: string[];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
body: {
|
|
73
|
+
outputs: string[];
|
|
74
|
+
vars: string[];
|
|
75
|
+
}[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface EvaluateStats {
|
|
79
|
+
successes: number;
|
|
80
|
+
failures: number;
|
|
81
|
+
tokenUsage: TokenUsage;
|
|
82
|
+
}
|
|
83
|
+
|
|
56
84
|
export interface EvaluateSummary {
|
|
85
|
+
version: number;
|
|
57
86
|
results: EvaluateResult[];
|
|
58
|
-
table:
|
|
59
|
-
stats:
|
|
60
|
-
successes: number;
|
|
61
|
-
failures: number;
|
|
62
|
-
tokenUsage: TokenUsage;
|
|
63
|
-
};
|
|
87
|
+
table: EvaluateTable;
|
|
88
|
+
stats: EvaluateStats;
|
|
64
89
|
}
|
package/src/util.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
2
4
|
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import fetch from 'node-fetch';
|
|
3
7
|
import yaml from 'js-yaml';
|
|
4
8
|
import nunjucks from 'nunjucks';
|
|
5
9
|
import { parse as parsePath } from 'path';
|
|
@@ -7,8 +11,11 @@ import { CsvRow } from './types.js';
|
|
|
7
11
|
import { parse as parseCsv } from 'csv-parse/sync';
|
|
8
12
|
import { stringify } from 'csv-stringify/sync';
|
|
9
13
|
|
|
14
|
+
import logger from './logger.js';
|
|
10
15
|
import { getDirectory } from './esm.js';
|
|
11
16
|
|
|
17
|
+
import type { RequestInfo, RequestInit, Response } from 'node-fetch';
|
|
18
|
+
|
|
12
19
|
import type { EvaluateSummary } from './types.js';
|
|
13
20
|
|
|
14
21
|
const PROMPT_DELIMITER = '---';
|
|
@@ -48,7 +55,10 @@ export function writeOutput(outputPath: string, summary: EvaluateSummary): void
|
|
|
48
55
|
const outputExtension = outputPath.split('.').pop()?.toLowerCase();
|
|
49
56
|
|
|
50
57
|
if (outputExtension === 'csv' || outputExtension === 'txt') {
|
|
51
|
-
const csvOutput = stringify(
|
|
58
|
+
const csvOutput = stringify([
|
|
59
|
+
[...summary.table.head.prompts, ...summary.table.head.vars],
|
|
60
|
+
...summary.table.body.map((row) => [...row.outputs, ...row.vars]),
|
|
61
|
+
]);
|
|
52
62
|
fs.writeFileSync(outputPath, csvOutput);
|
|
53
63
|
} else if (outputExtension === 'json') {
|
|
54
64
|
fs.writeFileSync(outputPath, JSON.stringify(summary, null, 2));
|
|
@@ -65,3 +75,52 @@ export function writeOutput(outputPath: string, summary: EvaluateSummary): void
|
|
|
65
75
|
throw new Error('Unsupported output file format. Use CSV, JSON, or YAML.');
|
|
66
76
|
}
|
|
67
77
|
}
|
|
78
|
+
|
|
79
|
+
export function fetchWithTimeout(
|
|
80
|
+
url: RequestInfo,
|
|
81
|
+
options: RequestInit = {},
|
|
82
|
+
timeout: number,
|
|
83
|
+
): Promise<Response> {
|
|
84
|
+
return new Promise(async (resolve, reject) => {
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
const { signal } = controller;
|
|
87
|
+
options.signal = signal;
|
|
88
|
+
|
|
89
|
+
const timeoutId = setTimeout(() => {
|
|
90
|
+
controller.abort();
|
|
91
|
+
reject(new Error(`Request timed out after ${timeout} ms`));
|
|
92
|
+
}, timeout);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const response = await fetch(url, options);
|
|
96
|
+
clearTimeout(timeoutId);
|
|
97
|
+
resolve(response);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
100
|
+
// Fetch request was aborted, no need to reject again
|
|
101
|
+
} else {
|
|
102
|
+
clearTimeout(timeoutId);
|
|
103
|
+
reject(error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function getConfigDirectoryPath(): string {
|
|
110
|
+
return path.join(os.homedir(), '.promptfoo');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function getLatestResultsPath(): string {
|
|
114
|
+
return path.join(getConfigDirectoryPath(), 'output', 'latest.json');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function writeLatestResults(results: EvaluateSummary) {
|
|
118
|
+
const latestResultsPath = getLatestResultsPath();
|
|
119
|
+
try {
|
|
120
|
+
fs.mkdirSync(path.dirname(latestResultsPath), { recursive: true });
|
|
121
|
+
fs.writeFileSync(latestResultsPath, JSON.stringify(results, null, 2));
|
|
122
|
+
logger.info(`Wrote latest results to ${latestResultsPath}.`);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
logger.error(`Failed to write latest results to ${latestResultsPath}:\n${err}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
env: { browser: true, es2020: true },
|
|
3
|
+
extends: [
|
|
4
|
+
'eslint:recommended',
|
|
5
|
+
'plugin:@typescript-eslint/recommended',
|
|
6
|
+
'plugin:react-hooks/recommended',
|
|
7
|
+
],
|
|
8
|
+
parser: '@typescript-eslint/parser',
|
|
9
|
+
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
|
10
|
+
plugins: ['react-refresh'],
|
|
11
|
+
rules: {
|
|
12
|
+
'react-refresh/only-export-components': 'warn',
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="favicon.ico" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>promptfoo web viewer</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "promptfoo-client",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc && vite build",
|
|
9
|
+
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
|
10
|
+
"preview": "vite preview"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@emotion/react": "^11.11.0",
|
|
14
|
+
"@emotion/styled": "^11.11.0",
|
|
15
|
+
"@mui/material": "^5.13.0",
|
|
16
|
+
"@tanstack/react-table": "^8.9.1",
|
|
17
|
+
"react": "^18.2.0",
|
|
18
|
+
"react-dnd": "^16.0.1",
|
|
19
|
+
"react-dnd-html5-backend": "^16.0.1",
|
|
20
|
+
"react-dom": "^18.2.0",
|
|
21
|
+
"socket.io-client": "^4.6.1",
|
|
22
|
+
"tiny-invariant": "^1.3.1",
|
|
23
|
+
"zustand": "^4.3.8"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/react": "^18.0.28",
|
|
27
|
+
"@types/react-dom": "^18.0.11",
|
|
28
|
+
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
|
29
|
+
"@typescript-eslint/parser": "^5.57.1",
|
|
30
|
+
"@vitejs/plugin-react-swc": "^3.0.0",
|
|
31
|
+
"eslint": "^8.38.0",
|
|
32
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
33
|
+
"eslint-plugin-react-refresh": "^0.3.4",
|
|
34
|
+
"typescript": "^5.0.2",
|
|
35
|
+
"vite": "^4.3.2"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<?xml version="1.0" standalone="no"?>
|
|
2
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
|
3
|
+
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
|
4
|
+
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
|
6
|
+
preserveAspectRatio="xMidYMid meet">
|
|
7
|
+
<metadata>
|
|
8
|
+
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
|
9
|
+
</metadata>
|
|
10
|
+
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
|
11
|
+
fill="#000000" stroke="none">
|
|
12
|
+
<path d="M597 6384 c-77 -13 -152 -51 -204 -103 -44 -43 -88 -101 -77 -101 3
|
|
13
|
+
0 1 -6 -5 -14 -6 -7 -18 -35 -26 -62 -13 -43 -15 -320 -12 -2194 l2 -2145 33
|
|
14
|
+
-67 c53 -109 147 -187 262 -218 33 -9 301 -12 1046 -12 922 0 1004 -1 1017
|
|
15
|
+
-16 8 -10 37 -49 64 -88 274 -400 435 -626 470 -660 124 -118 298 -141 458
|
|
16
|
+
-61 43 21 135 110 187 181 18 24 49 63 69 87 19 24 127 159 238 301 l203 256
|
|
17
|
+
1033 0 c906 0 1040 2 1086 16 130 37 230 136 269 264 20 63 20 95 20 2187 0
|
|
18
|
+
1909 -2 2127 -16 2176 -9 30 -22 61 -30 70 -8 8 -14 19 -14 23 0 17 -55 78
|
|
19
|
+
-99 110 -25 19 -75 44 -111 57 l-65 22 -2868 1 c-1966 1 -2888 -2 -2930 -10z
|
|
20
|
+
m3951 -954 c57 -26 379 -345 411 -408 30 -59 31 -133 3 -196 -26 -58 -220
|
|
21
|
+
-256 -251 -256 -25 0 -599 573 -607 607 -5 19 0 32 23 56 17 18 57 61 89 96
|
|
22
|
+
74 79 106 102 163 117 58 14 111 9 169 -16z m-351 -712 c156 -156 290 -294
|
|
23
|
+
297 -308 8 -15 11 -33 6 -45 -12 -33 -1398 -1412 -1425 -1419 -22 -6 -58 27
|
|
24
|
+
-315 285 -160 160 -295 299 -300 309 -13 24 -14 23 270 310 866 876 1142 1150
|
|
25
|
+
1161 1150 14 0 116 -94 306 -282z m-1617 -1658 c322 -321 325 -326 270 -381
|
|
26
|
+
-16 -16 -37 -29 -47 -30 -22 -1 -12 1 -198 -44 -410 -99 -479 -115 -504 -115
|
|
27
|
+
-16 0 -40 11 -55 25 -25 23 -27 31 -23 73 2 26 8 61 11 77 19 82 79 327 91
|
|
28
|
+
375 8 30 26 101 39 158 27 115 51 152 97 152 24 0 77 -48 319 -290z"/>
|
|
29
|
+
</g>
|
|
30
|
+
</svg>
|
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { io as SocketIOClient } from 'socket.io-client';
|
|
4
|
+
|
|
5
|
+
import ResultsView from './ResultsView.js';
|
|
6
|
+
import NavBar from './NavBar.js';
|
|
7
|
+
import { useStore } from './store.js';
|
|
8
|
+
|
|
9
|
+
import './App.css';
|
|
10
|
+
|
|
11
|
+
function App() {
|
|
12
|
+
const { table, setTable } = useStore();
|
|
13
|
+
const [loaded, setLoaded] = React.useState<boolean>(false);
|
|
14
|
+
|
|
15
|
+
React.useEffect(() => {
|
|
16
|
+
const socket = SocketIOClient(`http://${window.location.host}`);
|
|
17
|
+
//const socket = SocketIOClient(`http://localhost:15500`);
|
|
18
|
+
|
|
19
|
+
socket.on('init', (data) => {
|
|
20
|
+
console.log('Initialized socket connection');
|
|
21
|
+
setLoaded(true);
|
|
22
|
+
setTable(data.table);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
socket.on('update', (data) => {
|
|
26
|
+
console.log('Received data update');
|
|
27
|
+
setTable(data.table);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return () => {
|
|
31
|
+
socket.disconnect();
|
|
32
|
+
};
|
|
33
|
+
}, [loaded, setTable]);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
<NavBar />
|
|
38
|
+
{loaded && table ? <ResultsView /> : <div>Loading...</div>}
|
|
39
|
+
</>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default App;
|