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.
Files changed (62) hide show
  1. package/README.md +55 -17
  2. package/dist/evaluator.d.ts +1 -1
  3. package/dist/evaluator.d.ts.map +1 -1
  4. package/dist/evaluator.js +213 -150
  5. package/dist/evaluator.js.map +1 -1
  6. package/dist/main.js +51 -10
  7. package/dist/main.js.map +1 -1
  8. package/dist/prompts.d.ts +2 -0
  9. package/dist/prompts.d.ts.map +1 -0
  10. package/dist/prompts.js +21 -0
  11. package/dist/prompts.js.map +1 -0
  12. package/dist/providers.d.ts +1 -0
  13. package/dist/providers.d.ts.map +1 -1
  14. package/dist/providers.js +11 -5
  15. package/dist/providers.js.map +1 -1
  16. package/dist/tableOutput.html +5 -8
  17. package/dist/types.d.ts +27 -8
  18. package/dist/types.d.ts.map +1 -1
  19. package/dist/util.d.ts +5 -0
  20. package/dist/util.d.ts.map +1 -1
  21. package/dist/util.js +50 -1
  22. package/dist/util.js.map +1 -1
  23. package/dist/web/client/assets/index-710f1308.css +1 -0
  24. package/dist/web/client/assets/index-900b20c0.js +172 -0
  25. package/dist/web/client/favicon.ico +0 -0
  26. package/dist/web/client/index.html +15 -0
  27. package/dist/web/client/logo.svg +30 -0
  28. package/dist/web/server.d.ts +2 -0
  29. package/dist/web/server.d.ts.map +1 -0
  30. package/dist/web/server.js +74 -0
  31. package/dist/web/server.js.map +1 -0
  32. package/package.json +14 -3
  33. package/src/evaluator.ts +271 -174
  34. package/src/main.ts +52 -10
  35. package/src/prompts.ts +20 -0
  36. package/src/providers.ts +33 -15
  37. package/src/tableOutput.html +5 -8
  38. package/src/types.ts +32 -7
  39. package/src/util.ts +60 -1
  40. package/src/web/client/.eslintrc.cjs +14 -0
  41. package/src/web/client/index.html +13 -0
  42. package/src/web/client/package.json +37 -0
  43. package/src/web/client/public/favicon.ico +0 -0
  44. package/src/web/client/public/logo.svg +30 -0
  45. package/src/web/client/src/App.css +0 -0
  46. package/src/web/client/src/App.tsx +43 -0
  47. package/src/web/client/src/Logo.css +13 -0
  48. package/src/web/client/src/Logo.tsx +11 -0
  49. package/src/web/client/src/NavBar.css +3 -0
  50. package/src/web/client/src/NavBar.tsx +11 -0
  51. package/src/web/client/src/ResultsTable.css +133 -0
  52. package/src/web/client/src/ResultsTable.tsx +261 -0
  53. package/src/web/client/src/ResultsView.tsx +110 -0
  54. package/src/web/client/src/index.css +35 -0
  55. package/src/web/client/src/main.tsx +10 -0
  56. package/src/web/client/src/store.ts +13 -0
  57. package/src/web/client/src/types.ts +14 -0
  58. package/src/web/client/src/vite-env.d.ts +1 -0
  59. package/src/web/client/tsconfig.json +24 -0
  60. package/src/web/client/tsconfig.node.json +10 -0
  61. package/src/web/client/vite.config.ts +7 -0
  62. 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: 3,
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[0];
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(head.length).fill(Math.floor(maxWidth / head.length)),
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 the first column green if it's a success, red if it's a failure.
192
- for (const row of summary.table.slice(1)) {
193
- const color = row[0] === 'PASS' ? 'green' : row[0].startsWith('FAIL') ? 'red' : undefined;
194
- table.push(row.map((col, i) => (i === 0 && color ? chalk[color](col) : col)));
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
- logger.info('Evaluation complete');
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 fetch('https://api.openai.com/v1/completions', {
64
- method: 'POST',
65
- headers: {
66
- 'Content-Type': 'application/json',
67
- Authorization: `Bearer ${this.apiKey}`,
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
- body: JSON.stringify(body),
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 fetch('https://api.openai.com/v1/chat/completions', {
133
- method: 'POST',
134
- headers: {
135
- 'Content-Type': 'application/json',
136
- Authorization: `Bearer ${this.apiKey}`,
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
- body: JSON.stringify(body),
139
- });
156
+ REQUEST_TIMEOUT_MS,
157
+ );
140
158
  data = (await response.json()) as unknown as any;
141
159
  } catch (err) {
142
160
  return {
@@ -20,17 +20,14 @@
20
20
  th,
21
21
  td {
22
22
  padding: 5px;
23
+ min-width: 200px;
23
24
  }
24
- /* If data-content is exactly "PASS", set font color to green */
25
- tr > td[data-content='PASS']:first-child {
25
+
26
+ tr > td[data-content^='[PASS]'] {
26
27
  color: green;
27
- font-weight: bold;
28
28
  }
29
-
30
- /* If data-content starts with "FAIL", set font color to red */
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: string[][];
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(summary.table);
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
+ }
@@ -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;
@@ -0,0 +1,13 @@
1
+ .logo {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: 4px;
5
+ }
6
+
7
+ .logo img {
8
+ width: 30px;
9
+ }
10
+
11
+ .logo span {
12
+ margin-bottom: 6px;
13
+ }
@@ -0,0 +1,11 @@
1
+ import Box from '@mui/material/Box';
2
+
3
+ import './Logo.css';
4
+
5
+ export default function Logo() {
6
+ return (
7
+ <Box className="logo">
8
+ <img src="/logo.svg" alt="Promptfoo logo" /> <span>promptfoo</span>
9
+ </Box>
10
+ );
11
+ }
@@ -0,0 +1,3 @@
1
+ nav {
2
+ margin-bottom: 1rem;
3
+ }
@@ -0,0 +1,11 @@
1
+ import Logo from './Logo';
2
+
3
+ import './NavBar.css';
4
+
5
+ export default function NavBar() {
6
+ return (
7
+ <nav>
8
+ <Logo />
9
+ </nav>
10
+ );
11
+ }