promptfoo 0.18.4 → 0.19.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 (156) hide show
  1. package/dist/package.json +10 -5
  2. package/dist/src/evaluator.d.ts.map +1 -1
  3. package/dist/src/evaluator.js +17 -9
  4. package/dist/src/evaluator.js.map +1 -1
  5. package/dist/src/index.d.ts +1 -0
  6. package/dist/src/index.d.ts.map +1 -1
  7. package/dist/src/index.js +3 -0
  8. package/dist/src/index.js.map +1 -1
  9. package/dist/src/main.js +2 -2
  10. package/dist/src/main.js.map +1 -1
  11. package/dist/src/providers.d.ts +1 -1
  12. package/dist/src/providers.d.ts.map +1 -1
  13. package/dist/src/providers.js +5 -0
  14. package/dist/src/providers.js.map +1 -1
  15. package/dist/src/share.d.ts.map +1 -1
  16. package/dist/src/share.js +8 -7
  17. package/dist/src/share.js.map +1 -1
  18. package/dist/src/types.d.ts +9 -1
  19. package/dist/src/types.d.ts.map +1 -1
  20. package/dist/src/web/nextui/404/index.html +1 -0
  21. package/dist/src/web/nextui/404.html +1 -0
  22. package/dist/src/web/nextui/_next/static/P9zzdx-rDJKPcGFq_qOXC/_buildManifest.js +1 -0
  23. package/dist/src/web/nextui/_next/static/P9zzdx-rDJKPcGFq_qOXC/_ssgManifest.js +1 -0
  24. package/dist/src/web/nextui/_next/static/chunks/121-54cee610700b4756.js +27 -0
  25. package/dist/src/web/nextui/_next/static/chunks/339-501c32916b785ef1.js +1 -0
  26. package/dist/src/web/nextui/_next/static/chunks/373-6a411db0b05027d3.js +1 -0
  27. package/dist/src/web/nextui/_next/static/chunks/583-507e6d8883bb85ff.js +1 -0
  28. package/dist/src/web/nextui/_next/static/chunks/596-9c29c47b8dee7a50.js +25 -0
  29. package/dist/src/web/nextui/_next/static/chunks/658-f8f9d18540505edc.js +15 -0
  30. package/dist/src/web/nextui/_next/static/chunks/858-7255df6dbc44dff9.js +125 -0
  31. package/dist/src/web/nextui/_next/static/chunks/97-64e11ce2b0607459.js +1 -0
  32. package/dist/src/web/nextui/_next/static/chunks/app/eval/[id]/not-found-366629541fd598e9.js +1 -0
  33. package/dist/src/web/nextui/_next/static/chunks/app/eval/[id]/page-655bc42ac68b25cc.js +1 -0
  34. package/dist/src/web/nextui/_next/static/chunks/app/eval/page-d5e8697859d6294e.js +1 -0
  35. package/dist/src/web/nextui/_next/static/chunks/app/layout-4c714b1a5a3a768d.js +1 -0
  36. package/dist/src/web/nextui/_next/static/chunks/app/page-4fe8a6342d24ca23.js +1 -0
  37. package/dist/src/web/nextui/_next/static/chunks/app/setup/page-cd35686fe6c12be8.js +1 -0
  38. package/dist/src/web/nextui/_next/static/chunks/fd9d1056-d8847af536b5787b.js +9 -0
  39. package/dist/src/web/nextui/_next/static/chunks/framework-8883d1e9be70c3da.js +25 -0
  40. package/dist/src/web/nextui/_next/static/chunks/main-0670de04b1c026b4.js +1 -0
  41. package/dist/src/web/nextui/_next/static/chunks/main-app-581ccf0003955b21.js +1 -0
  42. package/dist/src/web/nextui/_next/static/chunks/pages/_app-52924524f99094ab.js +1 -0
  43. package/dist/src/web/nextui/_next/static/chunks/pages/_error-c92d5c4bb2b49926.js +1 -0
  44. package/dist/src/web/nextui/_next/static/chunks/polyfills-78c92fac7aa8fdd8.js +1 -0
  45. package/dist/src/web/nextui/_next/static/chunks/webpack-a886dd767c2e76b7.js +1 -0
  46. package/dist/src/web/nextui/_next/static/css/48d388184a2f4ce3.css +1 -0
  47. package/dist/src/web/nextui/_next/static/css/7265c36d84346934.css +1 -0
  48. package/dist/src/web/nextui/_next/static/css/8119d8bd13a8adab.css +1 -0
  49. package/dist/src/web/nextui/_next/static/css/a35c840ac696f161.css +1 -0
  50. package/dist/src/web/nextui/_next/static/css/e388dd377baf25ec.css +1 -0
  51. package/dist/src/web/nextui/_next/static/css/fc460b8a7cadb952.css +1 -0
  52. package/dist/src/web/nextui/_next/static/media/0e4fe491bf84089c-s.p.woff2 +0 -0
  53. package/dist/src/web/nextui/_next/static/media/1c57ca6f5208a29b-s.woff2 +0 -0
  54. package/dist/src/web/nextui/_next/static/media/3dbd163d3bb09d47-s.woff2 +0 -0
  55. package/dist/src/web/nextui/_next/static/media/42d52f46a26971a3-s.woff2 +0 -0
  56. package/dist/src/web/nextui/_next/static/media/5647e4c23315a2d2-s.woff2 +0 -0
  57. package/dist/src/web/nextui/_next/static/media/627622453ef56b0d-s.p.woff2 +0 -0
  58. package/dist/src/web/nextui/_next/static/media/7be645d133f3ee22-s.woff2 +0 -0
  59. package/dist/src/web/nextui/_next/static/media/7c53f7419436e04b-s.woff2 +0 -0
  60. package/dist/src/web/nextui/_next/static/media/8fb72f69fba4e3d2-s.woff2 +0 -0
  61. package/dist/src/web/nextui/_next/static/media/912a9cfe43c928d9-s.woff2 +0 -0
  62. package/dist/src/web/nextui/_next/static/media/934c4b7cb736f2a3-s.p.woff2 +0 -0
  63. package/dist/src/web/nextui/_next/static/media/a5b77b63ef20339c-s.woff2 +0 -0
  64. package/dist/src/web/nextui/_next/static/media/a6d330d7873e7320-s.woff2 +0 -0
  65. package/dist/src/web/nextui/_next/static/media/baf12dd90520ae41-s.woff2 +0 -0
  66. package/dist/src/web/nextui/_next/static/media/bbdb6f0234009aba-s.woff2 +0 -0
  67. package/dist/src/web/nextui/_next/static/media/cff529cd86cc0276-s.woff2 +0 -0
  68. package/dist/src/web/nextui/_next/static/media/d117eea74e01de14-s.woff2 +0 -0
  69. package/dist/src/web/nextui/_next/static/media/dfa8b99978df7bbc-s.woff2 +0 -0
  70. package/dist/src/web/nextui/_next/static/media/e25729ca87cc7df9-s.woff2 +0 -0
  71. package/dist/src/web/nextui/_next/static/media/eb52b768f62eeeb4-s.woff2 +0 -0
  72. package/dist/src/web/nextui/_next/static/media/f06116e890b3dadb-s.woff2 +0 -0
  73. package/dist/src/web/nextui/api +1 -0
  74. package/dist/src/web/nextui/eval/index.html +1 -0
  75. package/dist/src/web/nextui/eval/index.txt +13 -0
  76. package/dist/src/web/nextui/index.html +1 -0
  77. package/dist/src/web/nextui/index.txt +13 -0
  78. package/dist/src/web/nextui/setup/index.html +1 -0
  79. package/dist/src/web/nextui/setup/index.txt +14 -0
  80. package/dist/src/web/server.d.ts +1 -1
  81. package/dist/src/web/server.d.ts.map +1 -1
  82. package/dist/src/web/server.js +47 -4
  83. package/dist/src/web/server.js.map +1 -1
  84. package/package.json +10 -5
  85. package/src/evaluator.ts +17 -9
  86. package/src/index.ts +7 -1
  87. package/src/main.ts +3 -3
  88. package/src/providers.ts +11 -2
  89. package/src/share.ts +10 -8
  90. package/src/types.ts +10 -1
  91. package/src/web/nextui/.eslintrc.json +3 -0
  92. package/src/web/nextui/next.config.js +14 -0
  93. package/src/web/nextui/package-lock.json +4615 -0
  94. package/src/web/nextui/package.json +45 -0
  95. package/src/web/nextui/src/app/Home.css +3 -0
  96. package/src/web/nextui/src/app/api/route.ts +6 -0
  97. package/src/web/{client/src/NavBar.css → nextui/src/app/components/DarkMode.css} +1 -0
  98. package/src/web/{client/src/NavBar.tsx → nextui/src/app/components/DarkMode.tsx} +4 -9
  99. package/src/web/nextui/src/app/components/Logo.css +32 -0
  100. package/src/web/nextui/src/app/components/PageShell.css +33 -0
  101. package/src/web/nextui/src/app/components/PageShell.tsx +87 -0
  102. package/src/web/{client/src → nextui/src/app/eval}/ConfigModal.tsx +8 -5
  103. package/src/web/nextui/src/app/eval/Eval.css +13 -0
  104. package/src/web/nextui/src/app/eval/Eval.tsx +79 -0
  105. package/src/web/{client/src → nextui/src/app/eval}/EvalOutputPromptDialog.tsx +2 -2
  106. package/src/web/{client/src → nextui/src/app/eval}/ResultsTable.css +10 -12
  107. package/src/web/{client/src → nextui/src/app/eval}/ResultsTable.tsx +57 -14
  108. package/src/web/{client/src → nextui/src/app/eval}/ResultsView.tsx +4 -4
  109. package/src/web/nextui/src/app/eval/[id]/not-found.tsx +5 -0
  110. package/src/web/nextui/src/app/eval/[id]/page.css +9 -0
  111. package/src/web/nextui/src/app/eval/[id]/page.tsx +20 -0
  112. package/src/web/nextui/src/app/eval/index.css +0 -0
  113. package/src/web/nextui/src/app/eval/page.tsx +8 -0
  114. package/src/web/{client/src → nextui/src/app/eval}/store.ts +2 -2
  115. package/src/web/nextui/src/app/eval/types.ts +20 -0
  116. package/src/web/{client/src/index.css → nextui/src/app/globals.css} +21 -3
  117. package/src/web/nextui/src/app/layout.tsx +25 -0
  118. package/src/web/nextui/src/app/page.tsx +7 -0
  119. package/src/web/nextui/src/app/setup/AssertsForm.tsx +118 -0
  120. package/src/web/nextui/src/app/setup/PromptDialog.tsx +77 -0
  121. package/src/web/nextui/src/app/setup/PromptsSection.tsx +190 -0
  122. package/src/web/nextui/src/app/setup/ProviderConfigDialog.tsx +99 -0
  123. package/src/web/nextui/src/app/setup/ProviderSelector.tsx +149 -0
  124. package/src/web/nextui/src/app/setup/RunTestSuiteButton.tsx +88 -0
  125. package/src/web/nextui/src/app/setup/TestCaseDialog.tsx +108 -0
  126. package/src/web/nextui/src/app/setup/TestCasesSection.tsx +154 -0
  127. package/src/web/nextui/src/app/setup/VarsForm.tsx +57 -0
  128. package/src/web/nextui/src/app/setup/page.css +3 -0
  129. package/src/web/nextui/src/app/setup/page.tsx +160 -0
  130. package/src/web/nextui/src/util/api.ts +1 -0
  131. package/src/web/nextui/src/util/store.ts +53 -0
  132. package/src/web/nextui/tsconfig.json +28 -0
  133. package/src/web/server.ts +56 -2
  134. package/dist/src/web/client/assets/index-6d2a3573.js +0 -200
  135. package/dist/src/web/client/assets/index-d2b6a160.css +0 -1
  136. package/dist/src/web/client/assets/js-yaml-8bbf9398.js +0 -32
  137. package/dist/src/web/client/index.html +0 -15
  138. package/src/web/client/.eslintrc.cjs +0 -14
  139. package/src/web/client/index.html +0 -13
  140. package/src/web/client/package-lock.json +0 -5726
  141. package/src/web/client/package.json +0 -39
  142. package/src/web/client/src/App.css +0 -4
  143. package/src/web/client/src/App.tsx +0 -120
  144. package/src/web/client/src/Logo.css +0 -18
  145. package/src/web/client/src/main.tsx +0 -10
  146. package/src/web/client/src/types.ts +0 -36
  147. package/src/web/client/src/vite-env.d.ts +0 -1
  148. package/src/web/client/tsconfig.json +0 -24
  149. package/src/web/client/tsconfig.node.json +0 -10
  150. package/src/web/client/vite.config.ts +0 -7
  151. /package/dist/src/web/{client → nextui}/favicon.ico +0 -0
  152. /package/dist/src/web/{client → nextui}/logo.svg +0 -0
  153. /package/src/web/{client → nextui}/public/favicon.ico +0 -0
  154. /package/src/web/{client → nextui}/public/logo.svg +0 -0
  155. /package/src/web/{client/src → nextui/src/app/components}/Logo.tsx +0 -0
  156. /package/src/web/{client/src → nextui/src/app/eval}/ShareModal.tsx +0 -0
@@ -0,0 +1,149 @@
1
+ import React from 'react';
2
+ import { Autocomplete, Box, Chip, TextField } from '@mui/material';
3
+ import { ProviderConfig } from '../../../../../types';
4
+ import ProviderConfigDialog from './ProviderConfigDialog';
5
+
6
+ const defaultProviders: ProviderConfig[] = [
7
+ {
8
+ id: 'replicate:replicate/llama70b-v2-chat:e951f18578850b652510200860fc4ea62b3b16fac280f83ff32282f87bbd2e48',
9
+ config: { temperature: 0.5 },
10
+ },
11
+ ]
12
+ .concat(
13
+ [
14
+ 'anthropic:claude-1',
15
+ 'anthropic:claude-1-100k',
16
+ 'anthropic:claude-instant-1',
17
+ 'anthropic:claude-instant-1-100k',
18
+ ].map((id) => ({ id, config: { temperature: 0.5 } })),
19
+ )
20
+ .concat(
21
+ [
22
+ 'openai:gpt-3.5-turbo',
23
+ 'openai:gpt-3.5-turbo-0301',
24
+ 'openai:gpt-3.5-turbo-0613',
25
+ 'openai:gpt-3.5-turbo-16k',
26
+ 'openai:gpt-3.5-turbo-16k-0613',
27
+ 'openai:gpt-4',
28
+ 'openai:gpt-4-0314',
29
+ 'openai:gpt-4-0613',
30
+ 'openai:gpt-4-32k',
31
+ 'openai:gpt-4-32k-0314',
32
+ ].map((id) => ({ id, config: { temperature: 0.5, max_tokens: 1024 } })),
33
+ )
34
+ .concat(
35
+ [
36
+ 'azureopenai:gpt-3.5-turbo',
37
+ 'azureopenai:gpt-3.5-turbo-0301',
38
+ 'azureopenai:gpt-3.5-turbo-0613',
39
+ 'azureopenai:gpt-3.5-turbo-16k',
40
+ 'azureopenai:gpt-3.5-turbo-16k-0613',
41
+ 'azureopenai:gpt-4',
42
+ 'azureopenai:gpt-4-0314',
43
+ 'azureopenai:gpt-4-0613',
44
+ 'azureopenai:gpt-4-32k',
45
+ 'azureopenai:gpt-4-32k-0314',
46
+ ].map((id) => ({ id, config: { temperature: 0.5, max_tokens: 1024 } })),
47
+ )
48
+ .sort((a, b) => a.id.localeCompare(b.id));
49
+
50
+ interface ProviderSelectorProps {
51
+ providers: ProviderConfig[];
52
+ onChange: (providers: ProviderConfig[]) => void;
53
+ }
54
+
55
+ const ProviderSelector: React.FC<ProviderSelectorProps> = ({ providers, onChange }) => {
56
+ const [selectedProvider, setSelectedProvider] = React.useState<ProviderConfig | null>(null);
57
+
58
+ const getProviderLabel = (provider: string | ProviderConfig) => {
59
+ if (typeof provider === 'string') {
60
+ return provider;
61
+ }
62
+ return provider.id || 'Unknown provider';
63
+ };
64
+
65
+ const getProviderKey = (provider: string | ProviderConfig, index: number) => {
66
+ if (typeof provider === 'string') {
67
+ return provider;
68
+ }
69
+ return provider.id || index;
70
+ };
71
+
72
+ const handleProviderClick = (provider: string | ProviderConfig) => {
73
+ if (typeof provider === 'string') {
74
+ alert('Cannot edit custom providers');
75
+ } else if (!provider.config) {
76
+ alert('There is no config for this provider');
77
+ } else {
78
+ setSelectedProvider(provider as ProviderConfig);
79
+ }
80
+ };
81
+
82
+ const handleSave = (config: ProviderConfig['config']) => {
83
+ if (selectedProvider) {
84
+ const updatedProviders = providers.map((provider) =>
85
+ provider.id === selectedProvider.id ? { ...provider, config } : provider,
86
+ );
87
+ onChange(updatedProviders);
88
+ setSelectedProvider(null);
89
+ }
90
+ };
91
+
92
+ return (
93
+ <Box mt={2}>
94
+ <Autocomplete
95
+ multiple
96
+ freeSolo
97
+ options={defaultProviders}
98
+ value={providers}
99
+ onChange={(event, newValue: (string | ProviderConfig)[]) => {
100
+ onChange(newValue.map((value) => (typeof value === 'string' ? { id: value } : value)));
101
+ }}
102
+ getOptionLabel={(option) => {
103
+ if (!option) {
104
+ return '';
105
+ }
106
+ if (typeof option === 'string') {
107
+ return option;
108
+ }
109
+ return (option as ProviderConfig).id || 'Unknown provider';
110
+ }}
111
+ renderTags={(value, getTagProps) =>
112
+ value.map((provider, index: number) => {
113
+ const label = getProviderLabel(provider);
114
+ const key = getProviderKey(provider, index);
115
+
116
+ return (
117
+ <Chip
118
+ variant="outlined"
119
+ label={label}
120
+ {...getTagProps({ index })}
121
+ key={key}
122
+ onClick={() => handleProviderClick(provider)}
123
+ />
124
+ );
125
+ })
126
+ }
127
+ renderInput={(params) => (
128
+ <TextField
129
+ {...params}
130
+ variant="outlined"
131
+ placeholder="Select LLM providers"
132
+ helperText={providers.length > 0 ? 'Click a provider to configure its settings.' : null}
133
+ />
134
+ )}
135
+ />
136
+ {selectedProvider && selectedProvider.id && (
137
+ <ProviderConfigDialog
138
+ open={!!selectedProvider}
139
+ providerId={selectedProvider.id}
140
+ config={selectedProvider.config}
141
+ onClose={() => setSelectedProvider(null)}
142
+ onSave={handleSave}
143
+ />
144
+ )}
145
+ </Box>
146
+ );
147
+ };
148
+
149
+ export default ProviderSelector;
@@ -0,0 +1,88 @@
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { Button, CircularProgress } from '@mui/material';
6
+
7
+ import { useStore } from '@/util/store';
8
+ import { API_BASE_URL } from '@/util/api';
9
+
10
+ const RunTestSuiteButton: React.FC = () => {
11
+ const router = useRouter();
12
+ const { description, providers, prompts, testCases } = useStore();
13
+ const [isRunning, setIsRunning] = useState(false);
14
+ const [progressPercent, setProgressPercent] = useState(0);
15
+
16
+ const runTestSuite = async () => {
17
+ setIsRunning(true);
18
+
19
+ const testSuite = {
20
+ description,
21
+ providers,
22
+ prompts,
23
+ tests: testCases,
24
+ };
25
+
26
+ try {
27
+ const response = await fetch(`${API_BASE_URL}/api/eval`, {
28
+ method: 'POST',
29
+ headers: {
30
+ 'Content-Type': 'application/json',
31
+ },
32
+ body: JSON.stringify(testSuite),
33
+ });
34
+
35
+ if (!response.ok) {
36
+ throw new Error(`HTTP error! status: ${response.status}`);
37
+ }
38
+
39
+ const job = await response.json();
40
+
41
+ const intervalId = setInterval(async () => {
42
+ const progressResponse = await fetch(`${API_BASE_URL}/api/eval/${job.id}`);
43
+
44
+ if (!progressResponse.ok) {
45
+ clearInterval(intervalId);
46
+ throw new Error(`HTTP error! status: ${progressResponse.status}`);
47
+ }
48
+
49
+ const progressData = await progressResponse.json();
50
+
51
+ if (progressData.status === 'completed') {
52
+ clearInterval(intervalId);
53
+ setIsRunning(false);
54
+ router.push('/eval');
55
+ } else if (progressData.status === 'failed') {
56
+ clearInterval(intervalId);
57
+ setIsRunning(false);
58
+ throw new Error('Job failed');
59
+ } else {
60
+ const percent =
61
+ progressData.total === 0
62
+ ? 0
63
+ : Math.round((progressData.progress / progressData.total) * 100);
64
+ setProgressPercent(percent);
65
+ }
66
+ }, 1000);
67
+ } catch (error) {
68
+ console.error(error);
69
+ setIsRunning(false);
70
+ alert(`An error occurred: ${(error as Error).message}`);
71
+ }
72
+ };
73
+
74
+ return (
75
+ <Button variant="contained" color="primary" onClick={runTestSuite} disabled={isRunning}>
76
+ {isRunning ? (
77
+ <>
78
+ <CircularProgress size={24} sx={{ marginRight: 2 }} />
79
+ {progressPercent.toFixed(0)}% complete
80
+ </>
81
+ ) : (
82
+ 'Run Evaluation'
83
+ )}
84
+ </Button>
85
+ );
86
+ };
87
+
88
+ export default RunTestSuiteButton;
@@ -0,0 +1,108 @@
1
+ import React, { useState } from 'react';
2
+ import {
3
+ Button,
4
+ TextField,
5
+ Box,
6
+ Dialog,
7
+ DialogTitle,
8
+ DialogContent,
9
+ DialogActions,
10
+ } from '@mui/material';
11
+ import VarsForm from './VarsForm';
12
+ import AssertsForm from './AssertsForm';
13
+ import type { TestCase } from '../../../../../types';
14
+
15
+ interface TestCaseFormProps {
16
+ open: boolean;
17
+ onAdd: (testCase: TestCase, shouldClose: boolean) => void;
18
+ varsList: string[];
19
+ initialValues?: TestCase;
20
+ onCancel: () => void;
21
+ }
22
+
23
+ const TestCaseForm: React.FC<TestCaseFormProps> = ({
24
+ open,
25
+ onAdd,
26
+ varsList,
27
+ initialValues,
28
+ onCancel,
29
+ }) => {
30
+ const [description, setDescription] = useState(initialValues?.description || '');
31
+ const [vars, setVars] = useState(initialValues?.vars || {});
32
+ const [asserts, setAsserts] = useState(initialValues?.assert || []);
33
+ const [assertsFormKey, setAssertsFormKey] = useState(0);
34
+
35
+ React.useEffect(() => {
36
+ if (initialValues) {
37
+ setDescription(initialValues.description || '');
38
+ setVars(initialValues.vars || {});
39
+ setAsserts(initialValues.assert || []);
40
+ } else {
41
+ setDescription('');
42
+ setVars({});
43
+ setAsserts([]);
44
+ }
45
+ }, [initialValues]);
46
+
47
+ const handleAdd = (close: boolean) => {
48
+ onAdd(
49
+ {
50
+ description,
51
+ vars,
52
+ assert: asserts,
53
+ },
54
+ close,
55
+ );
56
+ if (close) {
57
+ onCancel();
58
+ }
59
+ setDescription('');
60
+ setVars({});
61
+ setAsserts([]);
62
+ setAssertsFormKey((prevKey) => prevKey + 1);
63
+ };
64
+
65
+ return (
66
+ <Dialog open={open} onClose={onCancel} fullWidth maxWidth="md">
67
+ <DialogTitle>{initialValues ? 'Edit Test Case' : 'Add Test Case'}</DialogTitle>
68
+ <DialogContent>
69
+ <Box>
70
+ {/*
71
+ <TextField
72
+ label="Description"
73
+ value={description}
74
+ onChange={(e) => setDescription(e.target.value)}
75
+ fullWidth
76
+ margin="normal"
77
+ />
78
+ */}
79
+ <VarsForm
80
+ onAdd={(vars) => setVars(vars)}
81
+ varsList={varsList}
82
+ initialValues={initialValues?.vars as Record<string, string>}
83
+ />
84
+ <AssertsForm
85
+ key={assertsFormKey}
86
+ onAdd={(asserts) => setAsserts(asserts)}
87
+ initialValues={initialValues?.assert || []}
88
+ />
89
+ </Box>
90
+ </DialogContent>
91
+ <DialogActions>
92
+ <Button onClick={handleAdd.bind(this, true)} color="primary" variant="contained">
93
+ {initialValues ? 'Update Test Case' : 'Add Test Case'}
94
+ </Button>
95
+ {!initialValues && (
96
+ <Button onClick={handleAdd.bind(this, false)} color="primary" variant="contained">
97
+ Add Another
98
+ </Button>
99
+ )}
100
+ <Button onClick={onCancel} color="secondary">
101
+ Cancel
102
+ </Button>
103
+ </DialogActions>
104
+ </Dialog>
105
+ );
106
+ };
107
+
108
+ export default TestCaseForm;
@@ -0,0 +1,154 @@
1
+ import React from 'react';
2
+ import Button from '@mui/material/Button';
3
+ import Copy from '@mui/icons-material/ContentCopy';
4
+ import Delete from '@mui/icons-material/Delete';
5
+ import Edit from '@mui/icons-material/Edit';
6
+ import IconButton from '@mui/material/IconButton';
7
+ import Stack from '@mui/material/Stack';
8
+ import Table from '@mui/material/Table';
9
+ import TableBody from '@mui/material/TableBody';
10
+ import TableCell from '@mui/material/TableCell';
11
+ import TableContainer from '@mui/material/TableContainer';
12
+ import TableHead from '@mui/material/TableHead';
13
+ import TableRow from '@mui/material/TableRow';
14
+ import Typography from '@mui/material/Typography';
15
+
16
+ import TestCaseDialog from './TestCaseDialog';
17
+ import { useStore } from '../../util/store';
18
+
19
+ import type { TestCase } from '../../../../../types';
20
+
21
+ interface TestCasesSectionProps {
22
+ varsList: string[];
23
+ }
24
+
25
+ const TestCasesSection: React.FC<TestCasesSectionProps> = ({ varsList }) => {
26
+ const { testCases, setTestCases } = useStore();
27
+ const [editingTestCaseIndex, setEditingTestCaseIndex] = React.useState<number | null>(null);
28
+ const [testCaseDialogOpen, setTestCaseDialogOpen] = React.useState(false);
29
+
30
+ const handleAddTestCase = (testCase: TestCase, shouldClose: boolean) => {
31
+ if (editingTestCaseIndex === null) {
32
+ setTestCases([...testCases, testCase]);
33
+ } else {
34
+ const updatedTestCases = testCases.map((tc, index) =>
35
+ index === editingTestCaseIndex ? testCase : tc,
36
+ );
37
+ setTestCases(updatedTestCases);
38
+ setEditingTestCaseIndex(null);
39
+ }
40
+
41
+ if (shouldClose) {
42
+ setTestCaseDialogOpen(false);
43
+ }
44
+ };
45
+
46
+ const handleRemoveTestCase = (event: React.MouseEvent, index: number) => {
47
+ event.stopPropagation();
48
+
49
+ if (confirm('Are you sure you want to delete this test case?')) {
50
+ setTestCases(testCases.filter((_, i) => i !== index));
51
+ }
52
+ };
53
+
54
+ const handleDuplicateTestCase = (event: React.MouseEvent, index: number) => {
55
+ event.stopPropagation();
56
+ const duplicatedTestCase = JSON.parse(JSON.stringify(testCases[index]));
57
+ setTestCases([...testCases, duplicatedTestCase]);
58
+ };
59
+
60
+ return (
61
+ <>
62
+ <Stack direction="row" spacing={2} justifyContent="space-between">
63
+ <Typography variant="h5">Test Cases</Typography>
64
+ <Button color="primary" onClick={() => setTestCaseDialogOpen(true)} variant="contained">
65
+ Add Test Case
66
+ </Button>
67
+ </Stack>
68
+ <TableContainer>
69
+ <Table>
70
+ <TableHead>
71
+ <TableRow>
72
+ <TableCell>Description</TableCell>
73
+ <TableCell>Assertions</TableCell>
74
+ <TableCell>Variables</TableCell>
75
+ <TableCell align="right"></TableCell>
76
+ </TableRow>
77
+ </TableHead>
78
+ <TableBody>
79
+ {testCases.length === 0 ? (
80
+ <TableRow>
81
+ <TableCell colSpan={4} align="center">
82
+ No test cases added yet.
83
+ </TableCell>
84
+ </TableRow>
85
+ ) : (
86
+ testCases.map((testCase, index) => (
87
+ <TableRow
88
+ key={index}
89
+ sx={{
90
+ '&:hover': {
91
+ backgroundColor: 'rgba(0, 0, 0, 0.04)',
92
+ cursor: 'pointer',
93
+ },
94
+ }}
95
+ onClick={() => {
96
+ setEditingTestCaseIndex(index);
97
+ setTestCaseDialogOpen(true);
98
+ }}
99
+ >
100
+ <TableCell>
101
+ <Typography variant="body2">
102
+ {testCase.description || `Test Case #${index + 1}`}
103
+ </Typography>
104
+ </TableCell>
105
+ <TableCell>{testCase.assert?.length || 0} assertions</TableCell>
106
+ <TableCell>
107
+ {Object.entries(testCase.vars || {})
108
+ .map(([k, v]) => k + '=' + v)
109
+ .join(', ')}
110
+ </TableCell>
111
+ <TableCell align="right" sx={{ minWidth: 150 }}>
112
+ <IconButton
113
+ onClick={() => {
114
+ setEditingTestCaseIndex(index);
115
+ setTestCaseDialogOpen(true);
116
+ }}
117
+ size="small"
118
+ >
119
+ <Edit />
120
+ </IconButton>
121
+ <IconButton
122
+ onClick={(event) => handleDuplicateTestCase(event, index)}
123
+ size="small"
124
+ >
125
+ <Copy />
126
+ </IconButton>
127
+ <IconButton
128
+ onClick={(event) => handleRemoveTestCase(event, index)}
129
+ size="small"
130
+ >
131
+ <Delete />
132
+ </IconButton>
133
+ </TableCell>
134
+ </TableRow>
135
+ ))
136
+ )}
137
+ </TableBody>
138
+ </Table>
139
+ </TableContainer>
140
+ <TestCaseDialog
141
+ open={testCaseDialogOpen}
142
+ onAdd={handleAddTestCase}
143
+ varsList={varsList}
144
+ initialValues={editingTestCaseIndex !== null ? testCases[editingTestCaseIndex] : undefined}
145
+ onCancel={() => {
146
+ setEditingTestCaseIndex(null);
147
+ setTestCaseDialogOpen(false);
148
+ }}
149
+ />
150
+ </>
151
+ );
152
+ };
153
+
154
+ export default TestCasesSection;
@@ -0,0 +1,57 @@
1
+ import React, { useEffect } from 'react';
2
+ import { Box, TextField, Typography, Stack } from '@mui/material';
3
+
4
+ interface VarsFormProps {
5
+ onAdd: (vars: Record<string, string>) => void;
6
+ varsList: string[];
7
+ initialValues?: Record<string, string>;
8
+ }
9
+
10
+ const VarsForm: React.FC<VarsFormProps> = ({ onAdd, varsList, initialValues }) => {
11
+ const [vars, setVars] = React.useState<Record<string, string>>(initialValues || {});
12
+
13
+ useEffect(() => {
14
+ const newVars: Record<string, string> = {};
15
+ varsList.forEach((v) => {
16
+ newVars[v] = initialValues?.[v] || '';
17
+ });
18
+ setVars(newVars);
19
+ }, [varsList, initialValues]);
20
+
21
+ return (
22
+ <Box my={2}>
23
+ <Typography variant="h6" mb={2}>
24
+ Vars
25
+ </Typography>
26
+ {varsList.length > 0 ? (
27
+ <Stack direction="row" spacing={2} alignItems="center">
28
+ {Object.keys(vars).map((varName, index) => (
29
+ <Stack key={index} direction="row" spacing={2} alignItems="center">
30
+ <TextField
31
+ placeholder={varName}
32
+ label={varName}
33
+ value={vars[varName]}
34
+ fullWidth
35
+ onChange={(e) => {
36
+ const newValue = e.target.value;
37
+ const newVars = {
38
+ ...vars,
39
+ [varName]: newValue,
40
+ };
41
+ setVars(newVars);
42
+ onAdd(newVars);
43
+ }}
44
+ />
45
+ </Stack>
46
+ ))}
47
+ </Stack>
48
+ ) : (
49
+ <Typography variant="subtitle1" gutterBottom>
50
+ Add variables to your prompt using the {'{{varname}}'} syntax.
51
+ </Typography>
52
+ )}
53
+ </Box>
54
+ );
55
+ };
56
+
57
+ export default VarsForm;
@@ -0,0 +1,3 @@
1
+ .yaml-config {
2
+ font-size: 10px;
3
+ }