promptfoo 0.5.1 → 0.6.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 +20 -248
- package/dist/__mocks__/esm.js +5 -1
- package/dist/__mocks__/esm.js.map +1 -1
- package/dist/assertions.d.ts +18 -0
- package/dist/assertions.d.ts.map +1 -0
- package/dist/assertions.js +128 -0
- package/dist/assertions.js.map +1 -0
- package/dist/esm.d.ts.map +1 -1
- package/dist/esm.js +10 -3
- package/dist/esm.js.map +1 -1
- package/dist/evaluator.d.ts.map +1 -1
- package/dist/evaluator.js +88 -117
- package/dist/evaluator.js.map +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -5
- package/dist/index.js.map +1 -1
- package/dist/logger.js +18 -11
- package/dist/logger.js.map +1 -1
- package/dist/main.js +95 -53
- package/dist/main.js.map +1 -1
- package/dist/prompts.d.ts +4 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +12 -1
- package/dist/prompts.js.map +1 -1
- package/dist/providers/localai.js +21 -13
- package/dist/providers/localai.js.map +1 -1
- package/dist/providers/openai.d.ts +9 -4
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +39 -29
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/shared.d.ts.map +1 -1
- package/dist/providers/shared.js +5 -2
- package/dist/providers/shared.js.map +1 -1
- package/dist/providers.d.ts +10 -0
- package/dist/providers.d.ts.map +1 -1
- package/dist/providers.js +51 -14
- package/dist/providers.js.map +1 -1
- package/dist/suggestions.d.ts +9 -0
- package/dist/suggestions.d.ts.map +1 -0
- package/dist/suggestions.js +54 -0
- package/dist/suggestions.js.map +1 -0
- package/dist/types.d.ts +11 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -1
- package/dist/util.d.ts +1 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +86 -31
- package/dist/util.js.map +1 -1
- package/dist/web/client/assets/index-207192fc.css +1 -0
- package/dist/web/client/assets/index-8751749f.js +172 -0
- package/dist/web/client/index.html +2 -2
- package/dist/web/server.js +38 -31
- package/dist/web/server.js.map +1 -1
- package/package.json +14 -4
- package/src/assertions.ts +154 -0
- package/src/esm.ts +5 -2
- package/src/evaluator.ts +61 -139
- package/src/index.ts +12 -0
- package/src/main.ts +28 -3
- package/src/prompts.ts +9 -0
- package/src/providers/openai.ts +16 -9
- package/src/providers/shared.ts +1 -1
- package/src/providers.ts +8 -0
- package/src/suggestions.ts +63 -0
- package/src/types.ts +14 -2
- package/src/util.ts +24 -3
- package/src/web/client/package.json +1 -0
- package/src/web/client/src/App.css +4 -0
- package/src/web/client/src/App.tsx +29 -5
- package/src/web/client/src/Logo.css +5 -0
- package/src/web/client/src/NavBar.css +18 -0
- package/src/web/client/src/NavBar.tsx +12 -1
- package/src/web/client/src/index.css +10 -0
- package/src/web/server.ts +2 -2
- package/dist/web/client/assets/index-710f1308.css +0 -1
- package/dist/web/client/assets/index-900b20c0.js +0 -172
package/src/main.ts
CHANGED
|
@@ -34,7 +34,7 @@ These prompts are nunjucks templates, so you can use logic like this:
|
|
|
34
34
|
{% endif %}`;
|
|
35
35
|
const dummyVars =
|
|
36
36
|
'var1,var2,var3\nvalue1,value2,value3\nanother value1,another value2,another value3';
|
|
37
|
-
const dummyConfig = `
|
|
37
|
+
const dummyConfig = `module.exports = {
|
|
38
38
|
prompts: ['prompts.txt'],
|
|
39
39
|
providers: ['openai:gpt-3.5-turbo'],
|
|
40
40
|
vars: 'vars.csv',
|
|
@@ -79,6 +79,10 @@ async function main() {
|
|
|
79
79
|
defaultConfig = (await import(pathJoin(process.cwd(), './promptfooconfig.js'))).default;
|
|
80
80
|
logger.info('Loaded default config from promptfooconfig.js');
|
|
81
81
|
}
|
|
82
|
+
if (existsSync('promptfooconfig.json')) {
|
|
83
|
+
defaultConfig = JSON.parse(readFileSync('promptfooconfig.json', 'utf-8'));
|
|
84
|
+
logger.info('Loaded default config from promptfooconfig.json');
|
|
85
|
+
}
|
|
82
86
|
|
|
83
87
|
const program = new Command();
|
|
84
88
|
|
|
@@ -143,10 +147,24 @@ async function main() {
|
|
|
143
147
|
'Truncate console table cells to this length',
|
|
144
148
|
'250',
|
|
145
149
|
)
|
|
150
|
+
.option(
|
|
151
|
+
'--suggest-prompts <number>',
|
|
152
|
+
'Generate N new prompts and append them to the prompt list',
|
|
153
|
+
)
|
|
154
|
+
.option(
|
|
155
|
+
'--prompt-prefix <path>',
|
|
156
|
+
'This prefix is prepended to every prompt',
|
|
157
|
+
defaultConfig.promptPrefix,
|
|
158
|
+
)
|
|
159
|
+
.option(
|
|
160
|
+
'--prompt-suffix <path>',
|
|
161
|
+
'This suffix is append to every prompt',
|
|
162
|
+
defaultConfig.promptSuffix,
|
|
163
|
+
)
|
|
146
164
|
.option('--no-write', 'Do not write results to promptfoo directory')
|
|
147
165
|
.option('--grader', 'Model that will grade outputs', defaultConfig.grader)
|
|
148
166
|
.option('--verbose', 'Show debug logs', defaultConfig.verbose)
|
|
149
|
-
.option('--view', 'View in browser ui')
|
|
167
|
+
.option('--view [port]', 'View in browser ui')
|
|
150
168
|
.action(async (cmdObj: CommandLineOptions & Command) => {
|
|
151
169
|
if (cmdObj.verbose) {
|
|
152
170
|
setLogLevel('debug');
|
|
@@ -184,6 +202,10 @@ async function main() {
|
|
|
184
202
|
providers,
|
|
185
203
|
showProgressBar: true,
|
|
186
204
|
maxConcurrency: !isNaN(maxConcurrency) && maxConcurrency > 0 ? maxConcurrency : undefined,
|
|
205
|
+
prompt: {
|
|
206
|
+
prefix: cmdObj.promptPrefix,
|
|
207
|
+
suffix: cmdObj.promptSuffix,
|
|
208
|
+
},
|
|
187
209
|
...config,
|
|
188
210
|
};
|
|
189
211
|
|
|
@@ -192,6 +214,9 @@ async function main() {
|
|
|
192
214
|
provider: await loadApiProvider(cmdObj.grader),
|
|
193
215
|
};
|
|
194
216
|
}
|
|
217
|
+
if (cmdObj.generateSuggestions) {
|
|
218
|
+
options.prompt!.generateSuggestions = true;
|
|
219
|
+
}
|
|
195
220
|
|
|
196
221
|
const summary = await evaluate(options);
|
|
197
222
|
|
|
@@ -252,7 +277,7 @@ async function main() {
|
|
|
252
277
|
logger.info('Done.');
|
|
253
278
|
|
|
254
279
|
if (cmdObj.view) {
|
|
255
|
-
init(15500);
|
|
280
|
+
init(parseInt(cmdObj.view, 10) || 15500);
|
|
256
281
|
}
|
|
257
282
|
});
|
|
258
283
|
|
package/src/prompts.ts
CHANGED
|
@@ -18,3 +18,12 @@ Rubric: Does not speak like a pirate
|
|
|
18
18
|
content: 'Content: {{ content }}\nRubric: {{ rubric }}',
|
|
19
19
|
},
|
|
20
20
|
]);
|
|
21
|
+
|
|
22
|
+
export const SUGGEST_PROMPTS_SYSTEM_MESSAGE = {
|
|
23
|
+
role: 'system',
|
|
24
|
+
content: `You're helping a scientist who is tuning a prompt for a large language model. You will receive messages, and each message is a full prompt. Generate a candidate variation of the given prompt. This variation will be tested for quality in order to select a winner.
|
|
25
|
+
|
|
26
|
+
Substantially revise the prompt, revising its structure and content however necessary to make it perform better, while preserving the original intent and including important details.
|
|
27
|
+
|
|
28
|
+
Your output is going to be copied directly into the program. It should contain the prompt ONLY`,
|
|
29
|
+
};
|
package/src/providers/openai.ts
CHANGED
|
@@ -12,6 +12,10 @@ const embeddingsCache = new LRUCache<string, ProviderEmbeddingResponse>({
|
|
|
12
12
|
max: 1000,
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
+
interface OpenAiCompletionOptions {
|
|
16
|
+
temperature: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
15
19
|
class OpenAiGenericProvider implements ApiProvider {
|
|
16
20
|
modelName: string;
|
|
17
21
|
apiKey?: string;
|
|
@@ -34,7 +38,7 @@ class OpenAiGenericProvider implements ApiProvider {
|
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
// @ts-ignore: Prompt is not used in this implementation
|
|
37
|
-
async callApi(prompt: string): Promise<ProviderResponse> {
|
|
41
|
+
async callApi(prompt: string, options?: OpenAiCompletionOptions): Promise<ProviderResponse> {
|
|
38
42
|
throw new Error('Not implemented');
|
|
39
43
|
}
|
|
40
44
|
}
|
|
@@ -110,8 +114,6 @@ export class OpenAiEmbeddingProvider extends OpenAiGenericProvider {
|
|
|
110
114
|
}
|
|
111
115
|
}
|
|
112
116
|
|
|
113
|
-
export const DefaultEmbeddingProvider = new OpenAiEmbeddingProvider('text-embedding-ada-002');
|
|
114
|
-
|
|
115
117
|
export class OpenAiCompletionProvider extends OpenAiGenericProvider {
|
|
116
118
|
static OPENAI_COMPLETION_MODELS = [
|
|
117
119
|
'text-davinci-003',
|
|
@@ -126,20 +128,20 @@ export class OpenAiCompletionProvider extends OpenAiGenericProvider {
|
|
|
126
128
|
logger.warn(`Using unknown OpenAI completion model: ${modelName}`);
|
|
127
129
|
}
|
|
128
130
|
super(modelName, apiKey);
|
|
131
|
+
}
|
|
129
132
|
|
|
133
|
+
async callApi(prompt: string, options?: OpenAiCompletionOptions): Promise<ProviderResponse> {
|
|
130
134
|
if (!this.apiKey) {
|
|
131
135
|
throw new Error(
|
|
132
136
|
'OpenAI API key is not set. Set OPENAI_API_KEY environment variable or pass it as an argument to the constructor.',
|
|
133
137
|
);
|
|
134
138
|
}
|
|
135
|
-
}
|
|
136
139
|
|
|
137
|
-
async callApi(prompt: string): Promise<ProviderResponse> {
|
|
138
140
|
const body = {
|
|
139
141
|
model: this.modelName,
|
|
140
142
|
prompt,
|
|
141
143
|
max_tokens: process.env.OPENAI_MAX_TOKENS || 1024,
|
|
142
|
-
temperature: process.env.
|
|
144
|
+
temperature: options?.temperature ?? (process.env.OPENAI_MAX_TEMPERATURE || 0),
|
|
143
145
|
stop: process.env.OPENAI_STOP ? JSON.parse(process.env.OPENAI_STOP) : undefined,
|
|
144
146
|
};
|
|
145
147
|
logger.debug(`Calling OpenAI API: ${JSON.stringify(body)}`);
|
|
@@ -197,15 +199,16 @@ export class OpenAiChatCompletionProvider extends OpenAiGenericProvider {
|
|
|
197
199
|
logger.warn(`Using unknown OpenAI chat model: ${modelName}`);
|
|
198
200
|
}
|
|
199
201
|
super(modelName, apiKey);
|
|
202
|
+
}
|
|
200
203
|
|
|
204
|
+
// TODO(ian): support passing in `messages` directly
|
|
205
|
+
async callApi(prompt: string, options?: OpenAiCompletionOptions): Promise<ProviderResponse> {
|
|
201
206
|
if (!this.apiKey) {
|
|
202
207
|
throw new Error(
|
|
203
208
|
'OpenAI API key is not set. Set OPENAI_API_KEY environment variable or pass it as an argument to the constructor.',
|
|
204
209
|
);
|
|
205
210
|
}
|
|
206
|
-
}
|
|
207
211
|
|
|
208
|
-
async callApi(prompt: string): Promise<ProviderResponse> {
|
|
209
212
|
let messages: { role: string; content: string }[];
|
|
210
213
|
try {
|
|
211
214
|
// User can specify `messages` payload as JSON, or we'll just put the
|
|
@@ -218,7 +221,7 @@ export class OpenAiChatCompletionProvider extends OpenAiGenericProvider {
|
|
|
218
221
|
model: this.modelName,
|
|
219
222
|
messages: messages,
|
|
220
223
|
max_tokens: process.env.OPENAI_MAX_TOKENS || 1024,
|
|
221
|
-
temperature: process.env.OPENAI_MAX_TEMPERATURE || 0,
|
|
224
|
+
temperature: options?.temperature ?? (process.env.OPENAI_MAX_TEMPERATURE || 0),
|
|
222
225
|
};
|
|
223
226
|
logger.debug(`Calling OpenAI API: ${JSON.stringify(body)}`);
|
|
224
227
|
|
|
@@ -260,3 +263,7 @@ export class OpenAiChatCompletionProvider extends OpenAiGenericProvider {
|
|
|
260
263
|
}
|
|
261
264
|
}
|
|
262
265
|
}
|
|
266
|
+
|
|
267
|
+
export const DefaultEmbeddingProvider = new OpenAiEmbeddingProvider('text-embedding-ada-002');
|
|
268
|
+
export const DefaultGradingProvider = new OpenAiChatCompletionProvider('gpt-4');
|
|
269
|
+
export const DefaultSuggestionsProvider = new OpenAiChatCompletionProvider('gpt-4');
|
package/src/providers/shared.ts
CHANGED
package/src/providers.ts
CHANGED
|
@@ -45,3 +45,11 @@ export async function loadApiProvider(providerPath: string): Promise<ApiProvider
|
|
|
45
45
|
const CustomApiProvider = (await import(path.join(process.cwd(), providerPath))).default;
|
|
46
46
|
return new CustomApiProvider();
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
export default {
|
|
50
|
+
OpenAiCompletionProvider,
|
|
51
|
+
OpenAiChatCompletionProvider,
|
|
52
|
+
LocalAiCompletionProvider,
|
|
53
|
+
LocalAiChatProvider,
|
|
54
|
+
loadApiProvider,
|
|
55
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { SUGGEST_PROMPTS_SYSTEM_MESSAGE } from './prompts';
|
|
2
|
+
import { DefaultSuggestionsProvider } from './providers/openai';
|
|
3
|
+
|
|
4
|
+
import type { TokenUsage } from './types';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TEMPERATURE = 0.9;
|
|
7
|
+
|
|
8
|
+
interface GeneratePromptsOutput {
|
|
9
|
+
prompts?: string[];
|
|
10
|
+
error?: string;
|
|
11
|
+
tokensUsed: TokenUsage;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function generatePrompts(prompt: string, num: number): Promise<GeneratePromptsOutput> {
|
|
15
|
+
const provider = DefaultSuggestionsProvider;
|
|
16
|
+
|
|
17
|
+
const resp = await provider.callApi(
|
|
18
|
+
JSON.stringify([
|
|
19
|
+
SUGGEST_PROMPTS_SYSTEM_MESSAGE,
|
|
20
|
+
{
|
|
21
|
+
role: 'user',
|
|
22
|
+
content: 'Generate a variant for the following prompt:',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
role: 'user',
|
|
26
|
+
content: prompt,
|
|
27
|
+
},
|
|
28
|
+
]),
|
|
29
|
+
{
|
|
30
|
+
temperature: DEFAULT_TEMPERATURE,
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
if (resp.error || !resp.output) {
|
|
34
|
+
return {
|
|
35
|
+
error: resp.error || 'Unknown error',
|
|
36
|
+
tokensUsed: {
|
|
37
|
+
total: resp.tokenUsage?.total || 0,
|
|
38
|
+
prompt: resp.tokenUsage?.prompt || 0,
|
|
39
|
+
completion: resp.tokenUsage?.completion || 0,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
return {
|
|
46
|
+
prompts: [resp.output],
|
|
47
|
+
tokensUsed: {
|
|
48
|
+
total: resp.tokenUsage?.total || 0,
|
|
49
|
+
prompt: resp.tokenUsage?.prompt || 0,
|
|
50
|
+
completion: resp.tokenUsage?.completion || 0,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return {
|
|
55
|
+
error: `Output is not valid JSON: ${resp.output}`,
|
|
56
|
+
tokensUsed: {
|
|
57
|
+
total: resp.tokenUsage?.total || 0,
|
|
58
|
+
prompt: resp.tokenUsage?.prompt || 0,
|
|
59
|
+
completion: resp.tokenUsage?.completion || 0,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -7,9 +7,13 @@ export interface CommandLineOptions {
|
|
|
7
7
|
verbose?: boolean;
|
|
8
8
|
maxConcurrency?: string;
|
|
9
9
|
grader?: string;
|
|
10
|
-
view?:
|
|
10
|
+
view?: string;
|
|
11
11
|
noWrite?: boolean;
|
|
12
12
|
tableCellMaxLength?: string;
|
|
13
|
+
|
|
14
|
+
generateSuggestions?: boolean;
|
|
15
|
+
promptPrefix?: string;
|
|
16
|
+
promptSuffix?: string;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
export interface ApiProvider {
|
|
@@ -43,7 +47,13 @@ export type VarMapping = Record<string, string>;
|
|
|
43
47
|
|
|
44
48
|
export interface GradingConfig {
|
|
45
49
|
prompt?: string;
|
|
46
|
-
provider
|
|
50
|
+
provider?: string | ApiProvider;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface PromptConfig {
|
|
54
|
+
prefix?: string;
|
|
55
|
+
suffix?: string;
|
|
56
|
+
generateSuggestions?: boolean;
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
export interface EvaluateOptions {
|
|
@@ -55,6 +65,8 @@ export interface EvaluateOptions {
|
|
|
55
65
|
showProgressBar?: boolean;
|
|
56
66
|
|
|
57
67
|
grading?: GradingConfig;
|
|
68
|
+
|
|
69
|
+
prompt?: PromptConfig;
|
|
58
70
|
}
|
|
59
71
|
|
|
60
72
|
export interface Prompt {
|
package/src/util.ts
CHANGED
|
@@ -5,6 +5,7 @@ import * as os from 'node:os';
|
|
|
5
5
|
import fetch from 'node-fetch';
|
|
6
6
|
import yaml from 'js-yaml';
|
|
7
7
|
import nunjucks from 'nunjucks';
|
|
8
|
+
import { globSync } from 'glob';
|
|
8
9
|
import { parse as parsePath } from 'path';
|
|
9
10
|
import { CsvRow } from './types.js';
|
|
10
11
|
import { parse as parseCsv } from 'csv-parse/sync';
|
|
@@ -27,8 +28,24 @@ function parseJson(json: string): any | undefined {
|
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
export function readPrompts(
|
|
31
|
-
|
|
31
|
+
export function readPrompts(promptPathsOrGlobs: string[]): string[] {
|
|
32
|
+
const promptPaths = promptPathsOrGlobs.flatMap((pathOrGlob) => globSync(pathOrGlob));
|
|
33
|
+
let promptContents: string[] = [];
|
|
34
|
+
|
|
35
|
+
for (const promptPath of promptPaths) {
|
|
36
|
+
const stat = fs.statSync(promptPath);
|
|
37
|
+
if (stat.isDirectory()) {
|
|
38
|
+
const filesInDirectory = fs.readdirSync(promptPath);
|
|
39
|
+
const fileContents = filesInDirectory.map((fileName) =>
|
|
40
|
+
fs.readFileSync(path.join(promptPath, fileName), 'utf-8'),
|
|
41
|
+
);
|
|
42
|
+
promptContents.push(...fileContents);
|
|
43
|
+
} else {
|
|
44
|
+
const fileContent = fs.readFileSync(promptPath, 'utf-8');
|
|
45
|
+
promptContents.push(fileContent);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
if (promptContents.length === 1) {
|
|
33
50
|
promptContents = promptContents[0].split(PROMPT_DELIMITER).map((p) => p.trim());
|
|
34
51
|
}
|
|
@@ -65,8 +82,12 @@ export function writeOutput(outputPath: string, summary: EvaluateSummary): void
|
|
|
65
82
|
fs.writeFileSync(outputPath, yaml.dump(summary));
|
|
66
83
|
} else if (outputExtension === 'html') {
|
|
67
84
|
const template = fs.readFileSync(`${getDirectory()}/tableOutput.html`, 'utf-8');
|
|
85
|
+
const table = [
|
|
86
|
+
[...summary.table.head.prompts, ...summary.table.head.vars],
|
|
87
|
+
...summary.table.body.map((row) => [...row.outputs, ...row.vars]),
|
|
88
|
+
];
|
|
68
89
|
const htmlOutput = nunjucks.renderString(template, {
|
|
69
|
-
table
|
|
90
|
+
table,
|
|
70
91
|
results: summary.results,
|
|
71
92
|
});
|
|
72
93
|
fs.writeFileSync(outputPath, htmlOutput);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
|
+
import useMediaQuery from '@mui/material/useMediaQuery';
|
|
4
|
+
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
|
3
5
|
import { io as SocketIOClient } from 'socket.io-client';
|
|
4
6
|
|
|
5
7
|
import ResultsView from './ResultsView.js';
|
|
@@ -12,9 +14,31 @@ function App() {
|
|
|
12
14
|
const { table, setTable } = useStore();
|
|
13
15
|
const [loaded, setLoaded] = React.useState<boolean>(false);
|
|
14
16
|
|
|
17
|
+
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
|
18
|
+
const [darkMode, setDarkMode] = React.useState(prefersDarkMode);
|
|
19
|
+
|
|
20
|
+
const theme = React.useMemo(
|
|
21
|
+
() =>
|
|
22
|
+
createTheme({
|
|
23
|
+
palette: {
|
|
24
|
+
mode: darkMode ? 'dark' : 'light',
|
|
25
|
+
},
|
|
26
|
+
}),
|
|
27
|
+
[darkMode],
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const toggleDarkMode = () => {
|
|
31
|
+
setDarkMode(!darkMode);
|
|
32
|
+
if (!darkMode) {
|
|
33
|
+
document.documentElement.setAttribute('data-theme', 'dark');
|
|
34
|
+
} else {
|
|
35
|
+
document.documentElement.removeAttribute('data-theme');
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
15
39
|
React.useEffect(() => {
|
|
16
|
-
const socket = SocketIOClient(`http://${window.location.host}`);
|
|
17
|
-
|
|
40
|
+
//const socket = SocketIOClient(`http://${window.location.host}`);
|
|
41
|
+
const socket = SocketIOClient(`http://localhost:15500`);
|
|
18
42
|
|
|
19
43
|
socket.on('init', (data) => {
|
|
20
44
|
console.log('Initialized socket connection');
|
|
@@ -33,10 +57,10 @@ function App() {
|
|
|
33
57
|
}, [loaded, setTable]);
|
|
34
58
|
|
|
35
59
|
return (
|
|
36
|
-
|
|
37
|
-
<NavBar />
|
|
60
|
+
<ThemeProvider theme={theme}>
|
|
61
|
+
<NavBar darkMode={darkMode} onToggleDarkMode={toggleDarkMode} />
|
|
38
62
|
{loaded && table ? <ResultsView /> : <div>Loading...</div>}
|
|
39
|
-
|
|
63
|
+
</ThemeProvider>
|
|
40
64
|
);
|
|
41
65
|
}
|
|
42
66
|
|
|
@@ -1,3 +1,21 @@
|
|
|
1
1
|
nav {
|
|
2
|
+
display: flex;
|
|
3
|
+
justify-content: space-between;
|
|
4
|
+
align-items: center;
|
|
2
5
|
margin-bottom: 1rem;
|
|
6
|
+
color: var(--text-color);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.dark-mode-toggle {
|
|
10
|
+
background-color: transparent;
|
|
11
|
+
border: none;
|
|
12
|
+
color: var(--text-color);
|
|
13
|
+
cursor: pointer;
|
|
14
|
+
font-size: 16px;
|
|
15
|
+
padding: 8px;
|
|
16
|
+
transition: color 0.3s;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.dark-mode-toggle:hover {
|
|
20
|
+
color: var(--pass-color);
|
|
3
21
|
}
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import Logo from './Logo';
|
|
2
2
|
|
|
3
|
+
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
|
4
|
+
import LightModeIcon from '@mui/icons-material/LightMode';
|
|
5
|
+
|
|
3
6
|
import './NavBar.css';
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
interface NavbarProps {
|
|
9
|
+
darkMode: boolean;
|
|
10
|
+
onToggleDarkMode: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function NavBar({ darkMode, onToggleDarkMode }: NavbarProps) {
|
|
6
14
|
return (
|
|
7
15
|
<nav>
|
|
8
16
|
<Logo />
|
|
17
|
+
<div className="dark-mode-toggle" onClick={onToggleDarkMode}>
|
|
18
|
+
{darkMode ? <DarkModeIcon /> : <LightModeIcon />}
|
|
19
|
+
</div>
|
|
9
20
|
</nav>
|
|
10
21
|
);
|
|
11
22
|
}
|
|
@@ -30,6 +30,16 @@
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
[data-theme='dark'] {
|
|
34
|
+
--background-color: #1a1a1a;
|
|
35
|
+
--text-color: #f0f0f0;
|
|
36
|
+
--border-color: #444444;
|
|
37
|
+
--table-border-color: #444444;
|
|
38
|
+
--pass-color: #4caf50;
|
|
39
|
+
--fail-color: #f44336;
|
|
40
|
+
--smalltext-color: #888888;
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
html {
|
|
34
44
|
font-size: calc(14px + (18 - 14) * ((100vw - 300px) / (1600 - 300)));
|
|
35
45
|
}
|
package/src/web/server.ts
CHANGED
|
@@ -4,9 +4,9 @@ import readline from 'node:readline';
|
|
|
4
4
|
import http from 'node:http';
|
|
5
5
|
|
|
6
6
|
import debounce from 'debounce';
|
|
7
|
-
import open from 'open';
|
|
8
7
|
import express from 'express';
|
|
9
8
|
import cors from 'cors';
|
|
9
|
+
import opener from 'opener';
|
|
10
10
|
import { Server as SocketIOServer } from 'socket.io';
|
|
11
11
|
|
|
12
12
|
import promptfoo from '../index.js';
|
|
@@ -83,7 +83,7 @@ export function init(port = 15500) {
|
|
|
83
83
|
rl.question('Do you want to open the browser to the URL? (y/N): ', async (answer) => {
|
|
84
84
|
if (answer.toLowerCase().startsWith('y')) {
|
|
85
85
|
try {
|
|
86
|
-
await
|
|
86
|
+
await opener(url);
|
|
87
87
|
logger.info(`Opening browser to: ${url}`);
|
|
88
88
|
} catch (err) {
|
|
89
89
|
logger.error(`Failed to open browser: ${String(err)}`);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
:root{font-family:system-ui,Avenir,Helvetica,Arial,sans-serif;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-text-size-adjust:100%;--background-color: #ffffff;--text-color: #404040;--border-color: lightgray;--table-border-color: lightgray;--pass-color: green;--fail-color: #ad0000;--smalltext-color: gray}@media (prefers-color-scheme: dark){:root{--background-color: #1a1a1a;--text-color: #f0f0f0;--border-color: #444444;--table-border-color: #444444;--pass-color: #4caf50;--fail-color: #f44336;--smalltext-color: #888888}}html{font-size:calc(14px + (18 - 14) * ((100vw - 300px) / (1600 - 300)))}*{box-sizing:border-box}html{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol;font-size:16px;background-color:var(--background-color);color:var(--text-color)}table,.divTable{border:1px solid var(--table-border-color);border-collapse:collapse;width:100%;margin:1rem 0;box-shadow:0 2px 4px #0000001a}.tr{display:flex}tr,.tr{width:fit-content}tr:hover,.tr:hover{background-color:#0000000d}th,.th,td,.td{position:relative;box-shadow:inset 0 0 0 1px var(--border-color);word-break:break-all;vertical-align:top;padding:1.5rem}th,.th{padding:1rem;position:relative;text-align:center;font-weight:semi-bold}tr .cell-rating{visibility:hidden;position:absolute;bottom:1.25rem;right:-1rem;line-height:0;font-size:1.75rem}tr:hover .cell-rating{visibility:visible}tr .cell-rating .rating{cursor:pointer;margin-right:1rem}th .smalltext{visibility:hidden;font-weight:400;font-size:.75rem;color:var(--smalltext-color)}th:hover .smalltext{visibility:visible}td .status{margin-bottom:.5rem;font-weight:700}td .pass{color:var(--pass-color)}td .fail{color:var(--fail-color)}.resizer{position:absolute;right:0;top:0;height:100%;width:5px;cursor:col-resize;user-select:none;touch-action:none;background:var(--text-color);opacity:.5}.resizer.isResizing{background:var(--text-color);opacity:1}@media (hover: hover){.resizer{opacity:0}*:hover>.resizer{opacity:1}}.logo{display:flex;align-items:center;gap:4px}.logo img{width:30px}.logo span{margin-bottom:6px}nav{margin-bottom:1rem}
|