promptfoo 0.10.0 → 0.12.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 (148) hide show
  1. package/README.md +45 -35
  2. package/dist/package.json +87 -0
  3. package/dist/src/__mocks__/esm.d.ts.map +1 -0
  4. package/dist/src/__mocks__/esm.js.map +1 -0
  5. package/dist/{assertions.d.ts → src/assertions.d.ts} +1 -1
  6. package/dist/src/assertions.d.ts.map +1 -0
  7. package/dist/src/assertions.js +374 -0
  8. package/dist/src/assertions.js.map +1 -0
  9. package/dist/src/cache.d.ts.map +1 -0
  10. package/dist/src/cache.js.map +1 -0
  11. package/dist/src/esm.d.ts.map +1 -0
  12. package/dist/src/esm.js.map +1 -0
  13. package/dist/src/evaluator.d.ts.map +1 -0
  14. package/dist/{evaluator.js → src/evaluator.js} +3 -1
  15. package/dist/src/evaluator.js.map +1 -0
  16. package/dist/src/index.d.ts.map +1 -0
  17. package/dist/{index.js → src/index.js} +10 -7
  18. package/dist/src/index.js.map +1 -0
  19. package/dist/src/logger.d.ts.map +1 -0
  20. package/dist/src/logger.js.map +1 -0
  21. package/dist/src/main.d.ts.map +1 -0
  22. package/dist/{main.js → src/main.js} +35 -13
  23. package/dist/src/main.js.map +1 -0
  24. package/dist/src/onboarding.d.ts.map +1 -0
  25. package/dist/src/onboarding.js.map +1 -0
  26. package/dist/src/prompts.d.ts.map +1 -0
  27. package/dist/src/prompts.js.map +1 -0
  28. package/dist/src/providers/localai.d.ts.map +1 -0
  29. package/dist/src/providers/localai.js.map +1 -0
  30. package/dist/src/providers/openai.d.ts.map +1 -0
  31. package/dist/src/providers/openai.js.map +1 -0
  32. package/dist/src/providers/shared.d.ts.map +1 -0
  33. package/dist/src/providers/shared.js.map +1 -0
  34. package/dist/src/providers.d.ts.map +1 -0
  35. package/dist/src/providers.js.map +1 -0
  36. package/dist/src/suggestions.d.ts.map +1 -0
  37. package/dist/src/suggestions.js.map +1 -0
  38. package/dist/src/telemetry.d.ts +10 -0
  39. package/dist/src/telemetry.d.ts.map +1 -0
  40. package/dist/src/telemetry.js +48 -0
  41. package/dist/src/telemetry.js.map +1 -0
  42. package/dist/{types.d.ts → src/types.d.ts} +6 -2
  43. package/dist/src/types.d.ts.map +1 -0
  44. package/dist/src/types.js.map +1 -0
  45. package/dist/src/updates.d.ts +3 -0
  46. package/dist/src/updates.d.ts.map +1 -0
  47. package/dist/src/updates.js +36 -0
  48. package/dist/src/updates.js.map +1 -0
  49. package/dist/{util.d.ts → src/util.d.ts} +3 -3
  50. package/dist/src/util.d.ts.map +1 -0
  51. package/dist/{util.js → src/util.js} +12 -5
  52. package/dist/src/util.js.map +1 -0
  53. package/dist/src/web/client/assets/index-87905193.css +1 -0
  54. package/dist/src/web/client/assets/index-eb6d3769.js +199 -0
  55. package/dist/src/web/client/assets/js-yaml-8bbf9398.js +32 -0
  56. package/dist/{web → src/web}/client/index.html +2 -2
  57. package/dist/src/web/server.d.ts.map +1 -0
  58. package/dist/{web → src/web}/server.js +3 -4
  59. package/dist/src/web/server.js.map +1 -0
  60. package/package.json +13 -9
  61. package/src/assertions.ts +247 -41
  62. package/src/evaluator.ts +5 -2
  63. package/src/index.ts +7 -4
  64. package/src/main.ts +50 -13
  65. package/src/telemetry.ts +57 -0
  66. package/src/types.ts +23 -2
  67. package/src/updates.ts +37 -0
  68. package/src/util.ts +28 -6
  69. package/src/web/client/package-lock.json +3 -6
  70. package/src/web/client/package.json +1 -0
  71. package/src/web/client/src/App.tsx +32 -12
  72. package/src/web/client/src/ConfigModal.tsx +81 -0
  73. package/src/web/client/src/ResultsTable.css +18 -6
  74. package/src/web/client/src/ResultsTable.tsx +101 -35
  75. package/src/web/client/src/ResultsView.tsx +148 -12
  76. package/src/web/client/src/ShareModal.tsx +70 -0
  77. package/src/web/client/src/index.css +6 -0
  78. package/src/web/client/src/store.ts +6 -1
  79. package/src/web/client/src/types.ts +4 -0
  80. package/src/web/server.ts +3 -7
  81. package/dist/__mocks__/esm.d.ts.map +0 -1
  82. package/dist/__mocks__/esm.js.map +0 -1
  83. package/dist/assertions.d.ts.map +0 -1
  84. package/dist/assertions.js +0 -233
  85. package/dist/assertions.js.map +0 -1
  86. package/dist/cache.d.ts.map +0 -1
  87. package/dist/cache.js.map +0 -1
  88. package/dist/esm.d.ts.map +0 -1
  89. package/dist/esm.js.map +0 -1
  90. package/dist/evaluator.d.ts.map +0 -1
  91. package/dist/evaluator.js.map +0 -1
  92. package/dist/index.d.ts.map +0 -1
  93. package/dist/index.js.map +0 -1
  94. package/dist/logger.d.ts.map +0 -1
  95. package/dist/logger.js.map +0 -1
  96. package/dist/main.d.ts.map +0 -1
  97. package/dist/main.js.map +0 -1
  98. package/dist/onboarding.d.ts.map +0 -1
  99. package/dist/onboarding.js.map +0 -1
  100. package/dist/prompts.d.ts.map +0 -1
  101. package/dist/prompts.js.map +0 -1
  102. package/dist/providers/localai.d.ts.map +0 -1
  103. package/dist/providers/localai.js.map +0 -1
  104. package/dist/providers/openai.d.ts.map +0 -1
  105. package/dist/providers/openai.js.map +0 -1
  106. package/dist/providers/shared.d.ts.map +0 -1
  107. package/dist/providers/shared.js.map +0 -1
  108. package/dist/providers.d.ts.map +0 -1
  109. package/dist/providers.js.map +0 -1
  110. package/dist/suggestions.d.ts.map +0 -1
  111. package/dist/suggestions.js.map +0 -1
  112. package/dist/types.d.ts.map +0 -1
  113. package/dist/types.js.map +0 -1
  114. package/dist/util.d.ts.map +0 -1
  115. package/dist/util.js.map +0 -1
  116. package/dist/web/client/assets/index-9a9ba400.css +0 -1
  117. package/dist/web/client/assets/index-b72d3ca9.js +0 -172
  118. package/dist/web/server.d.ts.map +0 -1
  119. package/dist/web/server.js.map +0 -1
  120. /package/dist/{__mocks__ → src/__mocks__}/esm.d.ts +0 -0
  121. /package/dist/{__mocks__ → src/__mocks__}/esm.js +0 -0
  122. /package/dist/{cache.d.ts → src/cache.d.ts} +0 -0
  123. /package/dist/{cache.js → src/cache.js} +0 -0
  124. /package/dist/{esm.d.ts → src/esm.d.ts} +0 -0
  125. /package/dist/{esm.js → src/esm.js} +0 -0
  126. /package/dist/{evaluator.d.ts → src/evaluator.d.ts} +0 -0
  127. /package/dist/{index.d.ts → src/index.d.ts} +0 -0
  128. /package/dist/{logger.d.ts → src/logger.d.ts} +0 -0
  129. /package/dist/{logger.js → src/logger.js} +0 -0
  130. /package/dist/{main.d.ts → src/main.d.ts} +0 -0
  131. /package/dist/{onboarding.d.ts → src/onboarding.d.ts} +0 -0
  132. /package/dist/{onboarding.js → src/onboarding.js} +0 -0
  133. /package/dist/{prompts.d.ts → src/prompts.d.ts} +0 -0
  134. /package/dist/{prompts.js → src/prompts.js} +0 -0
  135. /package/dist/{providers → src/providers}/localai.d.ts +0 -0
  136. /package/dist/{providers → src/providers}/localai.js +0 -0
  137. /package/dist/{providers → src/providers}/openai.d.ts +0 -0
  138. /package/dist/{providers → src/providers}/openai.js +0 -0
  139. /package/dist/{providers → src/providers}/shared.d.ts +0 -0
  140. /package/dist/{providers → src/providers}/shared.js +0 -0
  141. /package/dist/{providers.d.ts → src/providers.d.ts} +0 -0
  142. /package/dist/{providers.js → src/providers.js} +0 -0
  143. /package/dist/{suggestions.d.ts → src/suggestions.d.ts} +0 -0
  144. /package/dist/{suggestions.js → src/suggestions.js} +0 -0
  145. /package/dist/{types.js → src/types.js} +0 -0
  146. /package/dist/{web → src/web}/client/favicon.ico +0 -0
  147. /package/dist/{web → src/web}/client/logo.svg +0 -0
  148. /package/dist/{web → src/web}/server.d.ts +0 -0
@@ -9,12 +9,14 @@ import {
9
9
  getCoreRowModel,
10
10
  useReactTable,
11
11
  } from '@tanstack/react-table';
12
+ import Checkbox from '@mui/material/Checkbox';
13
+ import FormControlLabel from '@mui/material/FormControlLabel';
12
14
 
13
15
  import { useStore } from './store.js';
14
16
 
15
17
  import type { CellContext, VisibilityState } from '@tanstack/table-core';
16
18
 
17
- import type { EvalRow } from './types.js';
19
+ import type { EvalRow, FilterMode } from './types.js';
18
20
 
19
21
  import './ResultsTable.css';
20
22
 
@@ -114,12 +116,23 @@ function TableHeader({ text, maxLength, smallText }: TruncatedTextProps & { smal
114
116
  );
115
117
  }
116
118
 
117
- interface ResultsViewProps {
119
+ interface ResultsTableProps {
118
120
  maxTextLength: number;
119
121
  columnVisibility: VisibilityState;
122
+ wordBreak: 'break-word' | 'break-all';
123
+ filterMode: FilterMode;
124
+ failureFilter: { [key: string]: boolean };
125
+ onFailureFilterToggle: (columnId: string, checked: boolean) => void;
120
126
  }
121
127
 
122
- export default function ResultsTable({ maxTextLength, columnVisibility }: ResultsViewProps) {
128
+ export default function ResultsTable({
129
+ maxTextLength,
130
+ columnVisibility,
131
+ wordBreak,
132
+ filterMode,
133
+ failureFilter,
134
+ onFailureFilterToggle,
135
+ }: ResultsTableProps) {
123
136
  const { table, setTable } = useStore();
124
137
  invariant(table, 'Table should be defined');
125
138
  const { head, body } = table;
@@ -146,6 +159,10 @@ export default function ResultsTable({ maxTextLength, columnVisibility }: Result
146
159
  });
147
160
  };
148
161
 
162
+ const highestPassingIndex = numGood.reduce((maxIndex, currentPassCount, currentIndex, array) => {
163
+ return currentPassCount > array[maxIndex] ? currentIndex : maxIndex;
164
+ }, 0);
165
+ const highestPassingCount = numGood[highestPassingIndex];
149
166
  const columnHelper = createColumnHelper<EvalRow>();
150
167
  const columns = [
151
168
  columnHelper.group({
@@ -164,6 +181,8 @@ export default function ResultsTable({ maxTextLength, columnVisibility }: Result
164
181
  cell: (info: CellContext<EvalRow, string>) => (
165
182
  <TruncatedText text={info.getValue()} maxLength={maxTextLength} />
166
183
  ),
184
+ // Minimize the size of Variable columns.
185
+ size: 50,
167
186
  }),
168
187
  ),
169
188
  }),
@@ -173,16 +192,41 @@ export default function ResultsTable({ maxTextLength, columnVisibility }: Result
173
192
  columns: head.prompts.map((prompt, idx) =>
174
193
  columnHelper.accessor((row: EvalRow) => row.outputs[idx], {
175
194
  id: `Prompt ${idx + 1}`,
176
- header: () => (
177
- <>
178
- <TableHeader
179
- smallText={`Prompt ${idx + 1}`}
180
- text={prompt}
181
- maxLength={maxTextLength}
182
- />
183
- {numGood[idx]} / {body.length} 👍
184
- </>
185
- ),
195
+ header: () => {
196
+ const pct = ((numGood[idx] / body.length) * 100.0).toFixed(2);
197
+ const isHighestPassing =
198
+ numGood[idx] === highestPassingCount && highestPassingCount !== 0;
199
+ const columnId = `Prompt ${idx + 1}`;
200
+ const isChecked = failureFilter[columnId] || false;
201
+ return (
202
+ <>
203
+ <TableHeader
204
+ smallText={`Prompt ${idx + 1}`}
205
+ text={prompt}
206
+ maxLength={maxTextLength}
207
+ />
208
+ {filterMode === 'failures' && (
209
+ <FormControlLabel
210
+ sx={{
211
+ '& .MuiFormControlLabel-label': {
212
+ fontSize: '0.75rem',
213
+ },
214
+ }}
215
+ control={
216
+ <Checkbox
217
+ checked={isChecked}
218
+ onChange={(event) => onFailureFilterToggle(columnId, event.target.checked)}
219
+ />
220
+ }
221
+ label="Show failures"
222
+ />
223
+ )}
224
+ <div className={`summary ${isHighestPassing ? 'highlight' : ''}`}>
225
+ Passing: <strong>{pct}%</strong> ({numGood[idx]} / {body.length})
226
+ </div>
227
+ </>
228
+ );
229
+ },
186
230
  cell: (info: CellContext<EvalRow, string>) => (
187
231
  <PromptOutput
188
232
  text={info.getValue()}
@@ -197,8 +241,24 @@ export default function ResultsTable({ maxTextLength, columnVisibility }: Result
197
241
  }),
198
242
  ];
199
243
 
244
+ const filteredBody = React.useMemo(() => {
245
+ if (filterMode === 'failures') {
246
+ if (Object.values(failureFilter).every((v) => !v)) {
247
+ return body;
248
+ }
249
+ return body.filter((row) => {
250
+ return row.outputs.some((output, idx) => {
251
+ const columnId = `Prompt ${idx + 1}`;
252
+ const isFail = output.startsWith('[FAIL] ');
253
+ return failureFilter[columnId] && isFail;
254
+ });
255
+ });
256
+ }
257
+ return body;
258
+ }, [body, failureFilter, filterMode]);
259
+
200
260
  const reactTable = useReactTable({
201
- data: body,
261
+ data: filteredBody,
202
262
  columns,
203
263
  columnResizeMode: 'onChange',
204
264
  getCoreRowModel: getCoreRowModel(),
@@ -209,32 +269,38 @@ export default function ResultsTable({ maxTextLength, columnVisibility }: Result
209
269
  });
210
270
 
211
271
  return (
212
- <table>
272
+ <table
273
+ style={{
274
+ wordBreak,
275
+ }}
276
+ >
213
277
  <thead>
214
278
  {reactTable.getHeaderGroups().map((headerGroup) => (
215
279
  <tr key={headerGroup.id} className="header">
216
- {headerGroup.headers.map((header) => (
217
- <th
218
- {...{
219
- key: header.id,
220
- colSpan: header.colSpan,
221
- style: {
222
- width: header.getSize(),
223
- },
224
- }}
225
- >
226
- {header.isPlaceholder
227
- ? null
228
- : flexRender(header.column.columnDef.header, header.getContext())}
229
- <div
280
+ {headerGroup.headers.map((header) => {
281
+ return (
282
+ <th
230
283
  {...{
231
- onMouseDown: header.getResizeHandler(),
232
- onTouchStart: header.getResizeHandler(),
233
- className: `resizer ${header.column.getIsResizing() ? 'isResizing' : ''}`,
284
+ key: header.id,
285
+ colSpan: header.colSpan,
286
+ style: {
287
+ width: header.getSize(),
288
+ },
234
289
  }}
235
- />
236
- </th>
237
- ))}
290
+ >
291
+ {header.isPlaceholder
292
+ ? null
293
+ : flexRender(header.column.columnDef.header, header.getContext())}
294
+ <div
295
+ {...{
296
+ onMouseDown: header.getResizeHandler(),
297
+ onTouchStart: header.getResizeHandler(),
298
+ className: `resizer ${header.column.getIsResizing() ? 'isResizing' : ''}`,
299
+ }}
300
+ />
301
+ </th>
302
+ );
303
+ })}
238
304
  </tr>
239
305
  ))}
240
306
  </thead>
@@ -1,23 +1,41 @@
1
1
  import * as React from 'react';
2
2
 
3
3
  import invariant from 'tiny-invariant';
4
+ import Button from '@mui/material/Button';
4
5
  import Box from '@mui/material/Box';
5
- import Paper from '@mui/material/Box';
6
- import Stack from '@mui/material/Stack';
7
- import Slider from '@mui/material/Slider';
8
- import Typography from '@mui/material/Typography';
9
- import OutlinedInput from '@mui/material/OutlinedInput';
10
- import InputLabel from '@mui/material/InputLabel';
11
- import MenuItem from '@mui/material/MenuItem';
6
+ import Checkbox from '@mui/material/Checkbox';
7
+ import CircularProgress from '@mui/material/CircularProgress';
12
8
  import FormControl from '@mui/material/FormControl';
9
+ import FormControlLabel from '@mui/material/FormControlLabel';
10
+ import InputLabel from '@mui/material/InputLabel';
13
11
  import ListItemText from '@mui/material/ListItemText';
12
+ import MenuItem from '@mui/material/MenuItem';
13
+ import OutlinedInput from '@mui/material/OutlinedInput';
14
+ import Paper from '@mui/material/Box';
14
15
  import Select, { SelectChangeEvent } from '@mui/material/Select';
15
- import Checkbox from '@mui/material/Checkbox';
16
+ import Slider from '@mui/material/Slider';
17
+ import Stack from '@mui/material/Stack';
18
+ import Tooltip from '@mui/material/Tooltip';
19
+ import Typography from '@mui/material/Typography';
20
+ import ShareIcon from '@mui/icons-material/Share';
21
+ import VisibilityIcon from '@mui/icons-material/Visibility';
22
+ import { styled } from '@mui/system';
16
23
 
17
24
  import ResultsTable from './ResultsTable.js';
25
+ import ConfigModal from './ConfigModal';
26
+ import ShareModal from './ShareModal';
18
27
  import { useStore } from './store.js';
19
28
 
20
29
  import type { VisibilityState } from '@tanstack/table-core';
30
+ import type { FilterMode } from './types.js';
31
+
32
+ const ResponsiveStack = styled(Stack)(({ theme }) => ({
33
+ maxWidth: '100%',
34
+ flexWrap: 'wrap',
35
+ [theme.breakpoints.down('sm')]: {
36
+ flexDirection: 'column',
37
+ },
38
+ }));
21
39
 
22
40
  export default function ResultsView() {
23
41
  const { table } = useStore();
@@ -25,6 +43,62 @@ export default function ResultsView() {
25
43
  const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
26
44
  const [selectedColumns, setSelectedColumns] = React.useState<string[]>([]);
27
45
 
46
+ const [failureFilter, setFailureFilter] = React.useState<{ [key: string]: boolean }>({});
47
+ const handleFailureFilterToggle = (columnId: string, checked: boolean) => {
48
+ setFailureFilter((prevFailureFilter) => ({ ...prevFailureFilter, [columnId]: checked }));
49
+ };
50
+
51
+ const [filterMode, setFilterMode] = React.useState<FilterMode>('all');
52
+ const handleFilterModeChange = (event: SelectChangeEvent<unknown>) => {
53
+ const mode = event.target.value as FilterMode;
54
+ setFilterMode(mode);
55
+
56
+ const newFailureFilter: { [key: string]: boolean } = {};
57
+ head.prompts.forEach((_, idx) => {
58
+ const columnId = `Prompt ${idx + 1}`;
59
+ newFailureFilter[columnId] = mode === 'failures';
60
+ });
61
+ setFailureFilter(newFailureFilter);
62
+ };
63
+
64
+ const [wordBreak, setWordBreak] = React.useState<'break-word' | 'break-all'>('break-all');
65
+ const handleWordBreakChange = (event: React.ChangeEvent<HTMLInputElement>) => {
66
+ setWordBreak(event.target.checked ? 'break-all' : 'break-word');
67
+ };
68
+
69
+ const [shareModalOpen, setShareModalOpen] = React.useState(false);
70
+ const [shareUrl, setShareUrl] = React.useState('');
71
+ const [shareLoading, setShareLoading] = React.useState(false);
72
+
73
+ const handleShareButtonClick = async () => {
74
+ setShareLoading(true);
75
+ try {
76
+ const response = await fetch('https://api.promptfoo.dev/eval', {
77
+ method: 'POST',
78
+ headers: {
79
+ 'Content-Type': 'application/json',
80
+ },
81
+ body: JSON.stringify({
82
+ data: {
83
+ version: 1,
84
+ table,
85
+ },
86
+ }),
87
+ });
88
+
89
+ const { id } = await response.json();
90
+ const shareUrl = `https://app.promptfoo.dev/eval/${id}`;
91
+ setShareUrl(shareUrl);
92
+ setShareModalOpen(true);
93
+ } catch {
94
+ alert('Sorry, something went wrong.');
95
+ } finally {
96
+ setShareLoading(false);
97
+ }
98
+ };
99
+
100
+ const [configModalOpen, setConfigModalOpen] = React.useState(false);
101
+
28
102
  invariant(table, 'Table data must be loaded before rendering ResultsView');
29
103
  const { head } = table;
30
104
 
@@ -71,9 +145,9 @@ export default function ResultsView() {
71
145
  return (
72
146
  <div>
73
147
  <Paper py="md">
74
- <Stack direction="row" spacing={2} alignItems="center">
148
+ <ResponsiveStack direction="row" spacing={8} alignItems="center">
75
149
  <Box>
76
- <FormControl sx={{ m: 1, minWidth: 300 }} size="small">
150
+ <FormControl sx={{ m: 1, minWidth: 200 }} size="small">
77
151
  <InputLabel id="visible-columns-label">Visible columns</InputLabel>
78
152
  <Select
79
153
  labelId="visible-columns-label"
@@ -93,6 +167,21 @@ export default function ResultsView() {
93
167
  </Select>
94
168
  </FormControl>
95
169
  </Box>
170
+ <Box>
171
+ <FormControl sx={{ minWidth: 180 }} size="small">
172
+ <InputLabel id="failure-filter-mode-label">Filter</InputLabel>
173
+ <Select
174
+ labelId="filter-mode-label"
175
+ id="filter-mode"
176
+ value={filterMode}
177
+ onChange={handleFilterModeChange}
178
+ label="Filter"
179
+ >
180
+ <MenuItem value="all">Show all results</MenuItem>
181
+ <MenuItem value="failures">Show only failures</MenuItem>
182
+ </Select>
183
+ </FormControl>
184
+ </Box>
96
185
  <Box>
97
186
  <Typography mt={2}>Max text length: {maxTextLength}</Typography>
98
187
  <Slider
@@ -102,9 +191,56 @@ export default function ResultsView() {
102
191
  onChange={(_, val: number | number[]) => setMaxTextLength(val as number)}
103
192
  />
104
193
  </Box>
105
- </Stack>
194
+ <Box>
195
+ <Tooltip title="Forcing line breaks makes it easier to adjust column widths to your liking">
196
+ <FormControlLabel
197
+ control={
198
+ <Checkbox checked={wordBreak === 'break-all'} onChange={handleWordBreakChange} />
199
+ }
200
+ label="Force line breaks"
201
+ />
202
+ </Tooltip>
203
+ </Box>
204
+ <Box flexGrow={1} />
205
+ <Box display="flex" justifyContent="flex-end">
206
+ <ResponsiveStack direction="row" spacing={2}>
207
+ <Tooltip title="View config">
208
+ <Button
209
+ color="primary"
210
+ onClick={() => setConfigModalOpen(true)}
211
+ startIcon={<VisibilityIcon />}
212
+ >
213
+ Config
214
+ </Button>
215
+ </Tooltip>
216
+ <Tooltip title="Generate a unique URL that others can access">
217
+ <Button
218
+ color="primary"
219
+ onClick={handleShareButtonClick}
220
+ disabled={shareLoading}
221
+ startIcon={shareLoading ? <CircularProgress size={16} /> : <ShareIcon />}
222
+ >
223
+ Share
224
+ </Button>
225
+ </Tooltip>
226
+ </ResponsiveStack>
227
+ </Box>
228
+ </ResponsiveStack>
106
229
  </Paper>
107
- <ResultsTable maxTextLength={maxTextLength} columnVisibility={columnVisibility} />
230
+ <ResultsTable
231
+ maxTextLength={maxTextLength}
232
+ columnVisibility={columnVisibility}
233
+ wordBreak={wordBreak}
234
+ filterMode={filterMode}
235
+ failureFilter={failureFilter}
236
+ onFailureFilterToggle={handleFailureFilterToggle}
237
+ />
238
+ <ConfigModal open={configModalOpen} onClose={() => setConfigModalOpen(false)} />
239
+ <ShareModal
240
+ open={shareModalOpen}
241
+ onClose={() => setShareModalOpen(false)}
242
+ shareUrl={shareUrl}
243
+ />
108
244
  </div>
109
245
  );
110
246
  }
@@ -0,0 +1,70 @@
1
+ import React, { useRef, useState } from 'react';
2
+ import Dialog from '@mui/material/Dialog';
3
+ import DialogTitle from '@mui/material/DialogTitle';
4
+ import DialogContent from '@mui/material/DialogContent';
5
+ import DialogContentText from '@mui/material/DialogContentText';
6
+ import DialogActions from '@mui/material/DialogActions';
7
+ import Button from '@mui/material/Button';
8
+ import TextField from '@mui/material/TextField';
9
+ import IconButton from '@mui/material/IconButton';
10
+ import FileCopyIcon from '@mui/icons-material/FileCopy';
11
+ import CheckIcon from '@mui/icons-material/Check';
12
+
13
+ interface ShareModalProps {
14
+ open: boolean;
15
+ onClose: () => void;
16
+ shareUrl: string;
17
+ }
18
+
19
+ const ShareModal: React.FC<ShareModalProps> = ({ open, onClose, shareUrl }) => {
20
+ const inputRef = useRef<HTMLInputElement>(null);
21
+ const [copied, setCopied] = useState(false);
22
+
23
+ const handleCopyClick = () => {
24
+ if (inputRef.current) {
25
+ inputRef.current.select();
26
+ document.execCommand('copy');
27
+ setCopied(true);
28
+ }
29
+ };
30
+
31
+ const handleClose = () => {
32
+ onClose();
33
+ setCopied(false);
34
+ };
35
+
36
+ return (
37
+ <Dialog
38
+ open={open}
39
+ onClose={handleClose}
40
+ PaperProps={{ style: { minWidth: 'min(660px, 100%)' } }}
41
+ >
42
+ <DialogTitle>Your eval is ready to share</DialogTitle>
43
+ <DialogContent>
44
+ <TextField
45
+ inputRef={inputRef}
46
+ value={shareUrl}
47
+ fullWidth
48
+ InputProps={{
49
+ readOnly: true,
50
+ endAdornment: (
51
+ <IconButton onClick={handleCopyClick}>
52
+ {copied ? <CheckIcon /> : <FileCopyIcon />}
53
+ </IconButton>
54
+ ),
55
+ }}
56
+ />
57
+ <DialogContentText sx={{ fontSize: '0.75rem' }}>
58
+ Shared URLs are deleted after 1 week.
59
+ </DialogContentText>
60
+ </DialogContent>
61
+ <DialogActions>
62
+ <Button onClick={handleClose} color="primary">
63
+ Close
64
+ </Button>
65
+ </DialogActions>
66
+ </Dialog>
67
+ );
68
+ };
69
+
70
+ export default ShareModal;
@@ -15,6 +15,9 @@
15
15
  --pass-color: green;
16
16
  --fail-color: #ad0000;
17
17
  --smalltext-color: gray;
18
+ --success-background-color: #d1ffd7;
19
+ --variable-background-color: #f7f7f7;
20
+ --header-background-color: #fffdf7;
18
21
  }
19
22
 
20
23
  /* Dark mode colors */
@@ -38,6 +41,9 @@
38
41
  --pass-color: #4caf50;
39
42
  --fail-color: #f44336;
40
43
  --smalltext-color: #888888;
44
+ --success-background-color: #216d2b;
45
+ --variable-background-color: #333;
46
+ --header-background-color: #333;
41
47
  }
42
48
 
43
49
  html {
@@ -1,13 +1,18 @@
1
1
  import create from 'zustand';
2
2
 
3
- import type { EvalTable } from './types.js';
3
+ import type { EvalTable, UnifiedConfig } from './types.js';
4
4
 
5
5
  interface TableState {
6
6
  table: EvalTable | null;
7
7
  setTable: (table: EvalTable | null) => void;
8
+
9
+ config: Partial<UnifiedConfig> | null;
10
+ setConfig: (config: Partial<UnifiedConfig> | null) => void;
8
11
  }
9
12
 
10
13
  export const useStore = create<TableState>((set) => ({
11
14
  table: null,
12
15
  setTable: (table: EvalTable | null) => set(() => ({ table })),
16
+ config: null,
17
+ setConfig: (config: Partial<UnifiedConfig> | null) => set(() => ({ config })),
13
18
  }));
@@ -12,3 +12,7 @@ export type EvalTable = {
12
12
  head: EvalHead;
13
13
  body: EvalRow[];
14
14
  };
15
+
16
+ export type FilterMode = 'all' | 'failures';
17
+
18
+ export type { UnifiedConfig } from '../../../types';
package/src/web/server.ts CHANGED
@@ -9,13 +9,10 @@ import cors from 'cors';
9
9
  import opener from 'opener';
10
10
  import { Server as SocketIOServer } from 'socket.io';
11
11
 
12
- import promptfoo from '../index.js';
13
12
  import logger from '../logger';
14
13
  import { getDirectory } from '../esm';
15
14
  import { getLatestResultsPath } from '../util';
16
15
 
17
- import type { Request, Response } from 'express';
18
-
19
16
  export function init(port = 15500) {
20
17
  const app = express();
21
18
 
@@ -35,20 +32,19 @@ export function init(port = 15500) {
35
32
  const latestJsonPath = getLatestResultsPath();
36
33
  const readLatestJson = () => {
37
34
  const data = fs.readFileSync(latestJsonPath, 'utf8');
38
- const jsonData = JSON.parse(data);
39
- return jsonData.table;
35
+ return JSON.parse(data);
40
36
  };
41
37
 
42
38
  io.on('connection', (socket) => {
43
39
  // Send the initial table data when a client connects
44
- socket.emit('init', { table: readLatestJson() });
40
+ socket.emit('init', readLatestJson());
45
41
 
46
42
  // Watch for changes to latest.json and emit the update event
47
43
  fs.watch(
48
44
  latestJsonPath,
49
45
  debounce((event: string) => {
50
46
  if (event === 'change') {
51
- socket.emit('update', { table: readLatestJson() });
47
+ socket.emit('update', readLatestJson);
52
48
  }
53
49
  }, 250),
54
50
  );
@@ -1 +0,0 @@
1
- {"version":3,"file":"esm.d.ts","sourceRoot":"","sources":["../../src/__mocks__/esm.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,WAE3B"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"esm.js","sourceRoot":"","sources":["../../src/__mocks__/esm.ts"],"names":[],"mappings":";;;AAAA,SAAgB,YAAY;IAC1B,OAAO,WAAW,CAAC;AACrB,CAAC;AAFD,oCAEC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"assertions.d.ts","sourceRoot":"","sources":["../src/assertions.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAY,aAAa,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAMjG,wBAAsB,aAAa,CAAC,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAyBhG;AAED,wBAAsB,YAAY,CAChC,SAAS,EAAE,SAAS,EACpB,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,aAAa,CAAC,CA0DxB;AAoBD,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,aAAa,CAAC,CA0CxB;AAED,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,aAAa,GACtB,OAAO,CAAC,aAAa,CAAC,CAgDxB;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAmC/D;;;;;AAED,wBAGE"}