promptfoo 0.3.0 → 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 (53) hide show
  1. package/README.md +26 -7
  2. package/dist/evaluator.d.ts.map +1 -1
  3. package/dist/evaluator.js +31 -39
  4. package/dist/evaluator.js.map +1 -1
  5. package/dist/main.js +43 -20
  6. package/dist/main.js.map +1 -1
  7. package/dist/providers.d.ts.map +1 -1
  8. package/dist/providers.js +2 -1
  9. package/dist/providers.js.map +1 -1
  10. package/dist/types.d.ts +14 -1
  11. package/dist/types.d.ts.map +1 -1
  12. package/dist/util.d.ts +3 -0
  13. package/dist/util.d.ts.map +1 -1
  14. package/dist/util.js +24 -1
  15. package/dist/util.js.map +1 -1
  16. package/dist/web/client/assets/index-710f1308.css +1 -0
  17. package/dist/web/client/assets/index-900b20c0.js +172 -0
  18. package/dist/web/client/favicon.ico +0 -0
  19. package/dist/web/client/index.html +15 -0
  20. package/dist/web/client/logo.svg +30 -0
  21. package/dist/web/server.d.ts +2 -0
  22. package/dist/web/server.d.ts.map +1 -0
  23. package/dist/web/server.js +74 -0
  24. package/dist/web/server.js.map +1 -0
  25. package/package.json +14 -3
  26. package/src/evaluator.ts +38 -44
  27. package/src/main.ts +31 -9
  28. package/src/providers.ts +2 -1
  29. package/src/types.ts +16 -1
  30. package/src/util.ts +27 -1
  31. package/src/web/client/.eslintrc.cjs +14 -0
  32. package/src/web/client/index.html +13 -0
  33. package/src/web/client/package.json +37 -0
  34. package/src/web/client/public/favicon.ico +0 -0
  35. package/src/web/client/public/logo.svg +30 -0
  36. package/src/web/client/src/App.css +0 -0
  37. package/src/web/client/src/App.tsx +43 -0
  38. package/src/web/client/src/Logo.css +13 -0
  39. package/src/web/client/src/Logo.tsx +11 -0
  40. package/src/web/client/src/NavBar.css +3 -0
  41. package/src/web/client/src/NavBar.tsx +11 -0
  42. package/src/web/client/src/ResultsTable.css +133 -0
  43. package/src/web/client/src/ResultsTable.tsx +261 -0
  44. package/src/web/client/src/ResultsView.tsx +110 -0
  45. package/src/web/client/src/index.css +35 -0
  46. package/src/web/client/src/main.tsx +10 -0
  47. package/src/web/client/src/store.ts +13 -0
  48. package/src/web/client/src/types.ts +14 -0
  49. package/src/web/client/src/vite-env.d.ts +1 -0
  50. package/src/web/client/tsconfig.json +24 -0
  51. package/src/web/client/tsconfig.node.json +10 -0
  52. package/src/web/client/vite.config.ts +7 -0
  53. package/src/web/server.ts +96 -0
@@ -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
+ }
@@ -0,0 +1,133 @@
1
+ * {
2
+ box-sizing: border-box;
3
+ }
4
+
5
+ html {
6
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif,
7
+ 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
8
+ font-size: 16px;
9
+ background-color: var(--background-color);
10
+ color: var(--text-color);
11
+ }
12
+
13
+ table,
14
+ .divTable {
15
+ border: 1px solid var(--table-border-color);
16
+ border-collapse: collapse;
17
+ width: 100%;
18
+
19
+ margin: 1rem 0;
20
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
21
+ }
22
+
23
+ .tr {
24
+ display: flex;
25
+ }
26
+
27
+ tr,
28
+ .tr {
29
+ width: fit-content;
30
+ }
31
+
32
+ tr:hover,
33
+ .tr:hover {
34
+ background-color: rgba(0, 0, 0, 0.05);
35
+ }
36
+
37
+ th,
38
+ .th,
39
+ td,
40
+ .td {
41
+ position: relative;
42
+ box-shadow: inset 0 0 0 1px var(--border-color);
43
+ word-break: break-all;
44
+ vertical-align: top;
45
+
46
+ padding: 1.5rem;
47
+ }
48
+
49
+ th,
50
+ .th {
51
+ padding: 1rem;
52
+ position: relative;
53
+ text-align: center;
54
+ font-weight: semi-bold;
55
+ }
56
+
57
+ tr .cell {
58
+ }
59
+
60
+ tr .cell-rating {
61
+ visibility: hidden;
62
+ position: absolute;
63
+ bottom: 1.25rem;
64
+ right: -1rem;
65
+ line-height: 0;
66
+ font-size: 1.75rem;
67
+ }
68
+
69
+ tr:hover .cell-rating {
70
+ visibility: visible;
71
+ }
72
+
73
+ tr .cell-rating .rating {
74
+ cursor: pointer;
75
+ margin-right: 1rem;
76
+ }
77
+
78
+ th .smalltext {
79
+ visibility: hidden;
80
+ font-weight: normal;
81
+ font-size: 0.75rem;
82
+ color: var(--smalltext-color);
83
+ }
84
+
85
+ th:hover .smalltext {
86
+ visibility: visible;
87
+ }
88
+
89
+ td,
90
+ .td {
91
+ }
92
+
93
+ td .status {
94
+ margin-bottom: 0.5rem;
95
+ font-weight: bold;
96
+ }
97
+
98
+ td .pass {
99
+ color: var(--pass-color);
100
+ }
101
+
102
+ td .fail {
103
+ color: var(--fail-color);
104
+ }
105
+
106
+ .resizer {
107
+ position: absolute;
108
+ right: 0;
109
+ top: 0;
110
+ height: 100%;
111
+ width: 5px;
112
+ cursor: col-resize;
113
+ user-select: none;
114
+ touch-action: none;
115
+
116
+ background: var(--text-color);
117
+ opacity: 0.5;
118
+ }
119
+
120
+ .resizer.isResizing {
121
+ background: var(--text-color);
122
+ opacity: 1;
123
+ }
124
+
125
+ @media (hover: hover) {
126
+ .resizer {
127
+ opacity: 0;
128
+ }
129
+
130
+ *:hover > .resizer {
131
+ opacity: 1;
132
+ }
133
+ }
@@ -0,0 +1,261 @@
1
+ import * as React from 'react';
2
+
3
+ import './index.css';
4
+
5
+ import invariant from 'tiny-invariant';
6
+ import {
7
+ createColumnHelper,
8
+ flexRender,
9
+ getCoreRowModel,
10
+ useReactTable,
11
+ } from '@tanstack/react-table';
12
+
13
+ import { useStore } from './store.js';
14
+
15
+ import type { CellContext, VisibilityState } from '@tanstack/table-core';
16
+
17
+ import type { EvalRow } from './types.js';
18
+
19
+ import './ResultsTable.css';
20
+
21
+ interface TruncatedTextProps {
22
+ text: string;
23
+ maxLength: number;
24
+ }
25
+
26
+ function TruncatedText({ text, maxLength }: TruncatedTextProps) {
27
+ const [isTruncated, setIsTruncated] = React.useState<boolean>(true);
28
+
29
+ const toggleTruncate = () => {
30
+ setIsTruncated(!isTruncated);
31
+ };
32
+
33
+ const renderTruncatedText = () => {
34
+ if (text.length <= maxLength) {
35
+ return text;
36
+ }
37
+ if (isTruncated) {
38
+ return (
39
+ <>
40
+ <span style={{ cursor: 'pointer' }} onClick={toggleTruncate}>
41
+ {text.substring(0, maxLength)} ...
42
+ </span>
43
+ </>
44
+ );
45
+ } else {
46
+ return (
47
+ <>
48
+ <span style={{ cursor: 'pointer' }} onClick={toggleTruncate}>
49
+ {text}
50
+ </span>
51
+ </>
52
+ );
53
+ }
54
+ };
55
+
56
+ return <div>{renderTruncatedText()}</div>;
57
+ }
58
+
59
+ interface PromptOutputProps {
60
+ text: string;
61
+ maxTextLength: number;
62
+ rowIndex: number;
63
+ promptIndex: number;
64
+ onRating: (rowIndex: number, promptIndex: number, isPass: boolean) => void;
65
+ }
66
+
67
+ function PromptOutput({ text, maxTextLength, rowIndex, promptIndex, onRating }: PromptOutputProps) {
68
+ const isPass = text.startsWith('[PASS] ');
69
+ const isFail = text.startsWith('[FAIL] ');
70
+ let chunks: string[] = [];
71
+ if (isPass) {
72
+ text = text.substring(7);
73
+ } else if (isFail) {
74
+ if (text.includes('---')) {
75
+ chunks = text.split('---');
76
+ text = chunks.slice(1).join('---');
77
+ } else {
78
+ chunks = ['[FAIL]'];
79
+ if (text.startsWith('[FAIL] ')) {
80
+ text = text.substring(7);
81
+ }
82
+ }
83
+ }
84
+
85
+ const handleClick = (isPass: boolean) => {
86
+ onRating(rowIndex, promptIndex, isPass);
87
+ };
88
+
89
+ return (
90
+ <>
91
+ <div className="cell">
92
+ {isPass && <div className="status pass">[PASS]</div>}
93
+ {isFail && <div className="status fail">{chunks[0]}</div>}{' '}
94
+ <TruncatedText text={text} maxLength={maxTextLength} />
95
+ </div>
96
+ <div className="cell-rating">
97
+ <span className="rating" onClick={() => handleClick(true)}>
98
+ 👍
99
+ </span>
100
+ <span className="rating" onClick={() => handleClick(false)}>
101
+ 👎
102
+ </span>
103
+ </div>
104
+ </>
105
+ );
106
+ }
107
+
108
+ function TableHeader({ text, maxLength, smallText }: TruncatedTextProps & { smallText: string }) {
109
+ return (
110
+ <div>
111
+ <TruncatedText text={text} maxLength={maxLength} />
112
+ <span className="smalltext">{smallText}</span>
113
+ </div>
114
+ );
115
+ }
116
+
117
+ interface ResultsViewProps {
118
+ maxTextLength: number;
119
+ columnVisibility: VisibilityState;
120
+ }
121
+
122
+ export default function ResultsTable({ maxTextLength, columnVisibility }: ResultsViewProps) {
123
+ const { table, setTable } = useStore();
124
+ invariant(table, 'Table should be defined');
125
+ const { head, body } = table;
126
+ // TODO(ian): Correctly plumb through the results instead of parsing the string.
127
+ const numGood = head.prompts.map((_, idx) =>
128
+ body.reduce((acc, row) => {
129
+ return acc + (row.outputs[idx].startsWith('[PASS]') ? 1 : 0);
130
+ }, 0),
131
+ );
132
+
133
+ const handleRating = (rowIndex: number, promptIndex: number, isPass: boolean) => {
134
+ const updatedData = [...body];
135
+ const updatedRow = { ...updatedData[rowIndex] };
136
+ const updatedOutputs = [...updatedRow.outputs];
137
+ const updatedOutput = isPass
138
+ ? `[PASS] ${updatedOutputs[promptIndex].replace(/^\[(PASS|FAIL)\] /, '')}`
139
+ : `[FAIL] ${updatedOutputs[promptIndex].replace(/^\[(PASS|FAIL)\] /, '')}`;
140
+ updatedOutputs[promptIndex] = updatedOutput;
141
+ updatedRow.outputs = updatedOutputs;
142
+ updatedData[rowIndex] = updatedRow;
143
+ setTable({
144
+ head,
145
+ body: updatedData,
146
+ });
147
+ };
148
+
149
+ const columnHelper = createColumnHelper<EvalRow>();
150
+ const columns = [
151
+ columnHelper.group({
152
+ id: 'prompts',
153
+ header: () => <span>Prompts</span>,
154
+ columns: head.prompts.map((prompt, idx) =>
155
+ columnHelper.accessor((row: EvalRow) => row.outputs[idx], {
156
+ id: `Prompt ${idx + 1}`,
157
+ header: () => (
158
+ <>
159
+ <TableHeader
160
+ smallText={`Prompt ${idx + 1}`}
161
+ text={prompt}
162
+ maxLength={maxTextLength}
163
+ />
164
+ {numGood[idx]} / {body.length} 👍
165
+ </>
166
+ ),
167
+ cell: (info: CellContext<EvalRow, string>) => (
168
+ <PromptOutput
169
+ text={info.getValue()}
170
+ maxTextLength={maxTextLength}
171
+ rowIndex={info.row.index}
172
+ promptIndex={idx}
173
+ onRating={handleRating}
174
+ />
175
+ ),
176
+ }),
177
+ ),
178
+ }),
179
+ columnHelper.group({
180
+ id: 'vars',
181
+ header: () => <span>Variables</span>,
182
+ columns: head.vars.map((varName, idx) =>
183
+ columnHelper.accessor((row: EvalRow) => row.vars[idx], {
184
+ id: `Variable ${idx + 1}`,
185
+ header: () => (
186
+ <TableHeader
187
+ smallText={`Variable ${idx + 1}`}
188
+ text={varName}
189
+ maxLength={maxTextLength}
190
+ />
191
+ ),
192
+ cell: (info: CellContext<EvalRow, string>) => (
193
+ <TruncatedText text={info.getValue()} maxLength={maxTextLength} />
194
+ ),
195
+ }),
196
+ ),
197
+ }),
198
+ ];
199
+
200
+ const reactTable = useReactTable({
201
+ data: body,
202
+ columns,
203
+ columnResizeMode: 'onChange',
204
+ getCoreRowModel: getCoreRowModel(),
205
+
206
+ state: {
207
+ columnVisibility,
208
+ },
209
+ });
210
+
211
+ return (
212
+ <table>
213
+ <thead>
214
+ {reactTable.getHeaderGroups().map((headerGroup) => (
215
+ <tr key={headerGroup.id}>
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
230
+ {...{
231
+ onMouseDown: header.getResizeHandler(),
232
+ onTouchStart: header.getResizeHandler(),
233
+ className: `resizer ${header.column.getIsResizing() ? 'isResizing' : ''}`,
234
+ }}
235
+ />
236
+ </th>
237
+ ))}
238
+ </tr>
239
+ ))}
240
+ </thead>
241
+ <tbody>
242
+ {reactTable.getRowModel().rows.map((row) => (
243
+ <tr key={row.id}>
244
+ {row.getVisibleCells().map((cell) => (
245
+ <td
246
+ {...{
247
+ key: cell.id,
248
+ style: {
249
+ width: cell.column.getSize(),
250
+ },
251
+ }}
252
+ >
253
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
254
+ </td>
255
+ ))}
256
+ </tr>
257
+ ))}
258
+ </tbody>
259
+ </table>
260
+ );
261
+ }
@@ -0,0 +1,110 @@
1
+ import * as React from 'react';
2
+
3
+ import invariant from 'tiny-invariant';
4
+ 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';
12
+ import FormControl from '@mui/material/FormControl';
13
+ import ListItemText from '@mui/material/ListItemText';
14
+ import Select, { SelectChangeEvent } from '@mui/material/Select';
15
+ import Checkbox from '@mui/material/Checkbox';
16
+
17
+ import ResultsTable from './ResultsTable.js';
18
+ import { useStore } from './store.js';
19
+
20
+ import type { VisibilityState } from '@tanstack/table-core';
21
+
22
+ export default function ResultsView() {
23
+ const { table } = useStore();
24
+ const [maxTextLength, setMaxTextLength] = React.useState(250);
25
+ const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
26
+ const [selectedColumns, setSelectedColumns] = React.useState<string[]>([]);
27
+
28
+ invariant(table, 'Table data must be loaded before rendering ResultsView');
29
+ const { head } = table;
30
+
31
+ const handleChange = (event: SelectChangeEvent<typeof selectedColumns>) => {
32
+ const {
33
+ target: { value },
34
+ } = event;
35
+ setSelectedColumns(typeof value === 'string' ? value.split(',') : value);
36
+
37
+ const allColumns = [
38
+ ...head.prompts.map((_, idx) => `Prompt ${idx + 1}`),
39
+ ...head.vars.map((_, idx) => `Variable ${idx + 1}`),
40
+ ];
41
+ const newColumnVisibility: VisibilityState = {};
42
+ allColumns.forEach((col) => {
43
+ newColumnVisibility[col] = (typeof value === 'string' ? value.split(',') : value).includes(
44
+ col,
45
+ );
46
+ });
47
+ setColumnVisibility(newColumnVisibility);
48
+ };
49
+
50
+ const columnData = [
51
+ ...head.prompts.map((_, idx) => ({
52
+ value: `Prompt ${idx + 1}`,
53
+ label: `Prompt ${idx + 1}`,
54
+ group: 'Prompts',
55
+ })),
56
+ ...head.vars.map((_, idx) => ({
57
+ value: `Variable ${idx + 1}`,
58
+ label: `Variable ${idx + 1}`,
59
+ group: 'Variables',
60
+ })),
61
+ ];
62
+
63
+ // Set all columns as selected by default
64
+ React.useEffect(() => {
65
+ setSelectedColumns([
66
+ ...head.prompts.map((_, idx) => `Prompt ${idx + 1}`),
67
+ ...head.vars.map((_, idx) => `Variable ${idx + 1}`),
68
+ ]);
69
+ }, [head]);
70
+
71
+ return (
72
+ <div>
73
+ <Paper py="md">
74
+ <Stack direction="row" spacing={2} alignItems="center">
75
+ <Box>
76
+ <FormControl sx={{ m: 1, minWidth: 300 }} size="small">
77
+ <InputLabel id="visible-columns-label">Visible columns</InputLabel>
78
+ <Select
79
+ labelId="visible-columns-label"
80
+ id="visible-columns"
81
+ multiple
82
+ value={selectedColumns}
83
+ onChange={handleChange}
84
+ input={<OutlinedInput label="Visible columns" />}
85
+ renderValue={(selected: string[]) => selected.join(', ')}
86
+ >
87
+ {columnData.map((column) => (
88
+ <MenuItem dense key={column.value} value={column.value}>
89
+ <Checkbox checked={selectedColumns.indexOf(column.value) > -1} />
90
+ <ListItemText primary={column.label} />
91
+ </MenuItem>
92
+ ))}
93
+ </Select>
94
+ </FormControl>
95
+ </Box>
96
+ <Box>
97
+ <Typography mt={2}>Max text length: {maxTextLength}</Typography>
98
+ <Slider
99
+ min={25}
100
+ max={1000}
101
+ value={maxTextLength}
102
+ onChange={(_, val: number | number[]) => setMaxTextLength(val as number)}
103
+ />
104
+ </Box>
105
+ </Stack>
106
+ </Paper>
107
+ <ResultsTable maxTextLength={maxTextLength} columnVisibility={columnVisibility} />
108
+ </div>
109
+ );
110
+ }
@@ -0,0 +1,35 @@
1
+ :root {
2
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
3
+
4
+ font-synthesis: none;
5
+ text-rendering: optimizeLegibility;
6
+ -webkit-font-smoothing: antialiased;
7
+ -moz-osx-font-smoothing: grayscale;
8
+ -webkit-text-size-adjust: 100%;
9
+
10
+ /* Light mode colors */
11
+ --background-color: #ffffff;
12
+ --text-color: #404040;
13
+ --border-color: lightgray;
14
+ --table-border-color: lightgray;
15
+ --pass-color: green;
16
+ --fail-color: #ad0000;
17
+ --smalltext-color: gray;
18
+ }
19
+
20
+ /* Dark mode colors */
21
+ @media (prefers-color-scheme: dark) {
22
+ :root {
23
+ --background-color: #1a1a1a;
24
+ --text-color: #f0f0f0;
25
+ --border-color: #444444;
26
+ --table-border-color: #444444;
27
+ --pass-color: #4caf50;
28
+ --fail-color: #f44336;
29
+ --smalltext-color: #888888;
30
+ }
31
+ }
32
+
33
+ html {
34
+ font-size: calc(14px + (18 - 14) * ((100vw - 300px) / (1600 - 300)));
35
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App.tsx';
4
+ import './index.css';
5
+
6
+ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ );
@@ -0,0 +1,13 @@
1
+ import create from 'zustand';
2
+
3
+ import type { EvalTable } from './types.js';
4
+
5
+ interface TableState {
6
+ table: EvalTable | null;
7
+ setTable: (table: EvalTable | null) => void;
8
+ }
9
+
10
+ export const useStore = create<TableState>((set) => ({
11
+ table: null,
12
+ setTable: (table: EvalTable | null) => set(() => ({ table })),
13
+ }));
@@ -0,0 +1,14 @@
1
+ export type EvalHead = {
2
+ prompts: string[];
3
+ vars: string[];
4
+ };
5
+
6
+ export type EvalRow = {
7
+ outputs: string[]; // var inputs
8
+ vars: string[]; // model outputs
9
+ };
10
+
11
+ export type EvalTable = {
12
+ head: EvalHead;
13
+ body: EvalRow[];
14
+ };
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
5
+ "module": "ESNext",
6
+ "skipLibCheck": true,
7
+
8
+ /* Bundler mode */
9
+ "moduleResolution": "bundler",
10
+ "allowImportingTsExtensions": true,
11
+ "resolveJsonModule": true,
12
+ "isolatedModules": true,
13
+ "noEmit": true,
14
+ "jsx": "react-jsx",
15
+
16
+ /* Linting */
17
+ "strict": true,
18
+ "noUnusedLocals": true,
19
+ "noUnusedParameters": true,
20
+ "noFallthroughCasesInSwitch": true
21
+ },
22
+ "include": ["src"],
23
+ "references": [{ "path": "./tsconfig.node.json" }]
24
+ }