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
@@ -1,6 +1,10 @@
1
- :root {
2
- font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
1
+ /* This CSS is common to all pages */
2
+
3
+ * {
4
+ box-sizing: border-box;
5
+ }
3
6
 
7
+ :root {
4
8
  font-synthesis: none;
5
9
  text-rendering: optimizeLegibility;
6
10
  -webkit-font-smoothing: antialiased;
@@ -18,6 +22,8 @@
18
22
  --success-background-color: #d1ffd7;
19
23
  --variable-background-color: #f7f7f7;
20
24
  --header-background-color: #fffdf7;
25
+ --insert-highlight-color: #d4fcbc;
26
+ --delete-highlight-color: #fbb6c2;
21
27
  }
22
28
 
23
29
  /* Dark mode colors */
@@ -33,8 +39,20 @@
33
39
  --success-background-color: #216d2b;
34
40
  --variable-background-color: #333;
35
41
  --header-background-color: #333;
42
+ --insert-highlight-color: #4f8a34;
43
+ --delete-highlight-color: #8a3434;
36
44
  }
37
45
 
38
46
  html {
39
- font-size: calc(14px + (18 - 14) * ((100vw - 300px) / (1600 - 300)));
47
+ font-size: 16px;
48
+ background-color: var(--background-color);
49
+ color: var(--text-color);
50
+ }
51
+
52
+ body {
53
+ margin: 0;
54
+ }
55
+
56
+ * {
57
+ box-sizing: border-box;
40
58
  }
@@ -0,0 +1,25 @@
1
+ import './globals.css';
2
+ import type { Metadata } from 'next';
3
+ import { Roboto } from 'next/font/google';
4
+ import { PageShell } from './components/PageShell';
5
+
6
+ const roboto = Roboto({
7
+ weight: ['400', '500', '700'],
8
+ style: ['normal'],
9
+ subsets: ['latin'],
10
+ });
11
+
12
+ export const metadata: Metadata = {
13
+ title: 'promptfoo',
14
+ description: 'LLM testing and evaluation',
15
+ };
16
+
17
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
18
+ return (
19
+ <html lang="en">
20
+ <body className={roboto.className}>
21
+ <PageShell>{children}</PageShell>
22
+ </body>
23
+ </html>
24
+ );
25
+ }
@@ -0,0 +1,7 @@
1
+ import { redirect } from 'next/navigation';
2
+
3
+ import './Home.css';
4
+
5
+ export default function Page() {
6
+ redirect('/eval');
7
+ }
@@ -0,0 +1,118 @@
1
+ // src/components/AssertsForm.tsx
2
+ import React, { useState } from 'react';
3
+ import Autocomplete from '@mui/material/Autocomplete';
4
+ import Box from '@mui/material/Box';
5
+ import Button from '@mui/material/Button';
6
+ import Delete from '@mui/icons-material/Delete';
7
+ import IconButton from '@mui/material/IconButton';
8
+ import Stack from '@mui/material/Stack';
9
+ import TextField from '@mui/material/TextField';
10
+ import Typography from '@mui/material/Typography';
11
+ import type { Assertion, AssertionType } from '../../../../../types';
12
+
13
+ interface AssertsFormProps {
14
+ onAdd: (asserts: Assertion[]) => void;
15
+ initialValues: Assertion[];
16
+ }
17
+
18
+ const assertTypes: AssertionType[] = [
19
+ 'equals',
20
+ 'contains',
21
+ 'icontains',
22
+ 'contains-all',
23
+ 'contains-any',
24
+ 'starts-with',
25
+ 'regex',
26
+ 'is-json',
27
+ 'contains-json',
28
+ 'javascript',
29
+ 'python',
30
+ 'similar',
31
+ 'llm-rubric',
32
+ 'webhook',
33
+ 'rouge-n',
34
+ 'rouge-s',
35
+ 'rouge-l',
36
+ 'not-equals',
37
+ 'not-contains',
38
+ 'not-icontains',
39
+ 'not-contains-all',
40
+ 'not-contains-any',
41
+ 'not-starts-with',
42
+ 'not-regex',
43
+ 'not-is-json',
44
+ 'not-contains-json',
45
+ 'not-javascript',
46
+ 'not-python',
47
+ 'not-similar',
48
+ 'not-llm-rubric',
49
+ 'not-webhook',
50
+ 'not-rouge-n',
51
+ 'not-rouge-s',
52
+ 'not-rouge-l',
53
+ ];
54
+
55
+ const AssertsForm: React.FC<AssertsFormProps> = ({ onAdd, initialValues }) => {
56
+ const [asserts, setAsserts] = useState<Assertion[]>(initialValues || []);
57
+
58
+ const handleAdd = () => {
59
+ const newAsserts = [...asserts, { type: 'equals' as AssertionType, value: '' }];
60
+ setAsserts(newAsserts);
61
+ onAdd(newAsserts);
62
+ };
63
+
64
+ const handleRemoveAssert = (indexToRemove: number) => {
65
+ const newAsserts = asserts.filter((_, index) => index !== indexToRemove);
66
+ setAsserts(newAsserts);
67
+ onAdd(newAsserts);
68
+ };
69
+
70
+ return (
71
+ <>
72
+ <Typography variant="h6">Asserts</Typography>
73
+ <Box my={asserts.length > 0 ? 2 : 0}>
74
+ <Stack direction="column" spacing={2}>
75
+ {asserts.map((assert, index) => (
76
+ <Stack key={index} direction="row" spacing={2} alignItems="center">
77
+ <Autocomplete
78
+ value={assert.type}
79
+ options={assertTypes}
80
+ sx={{ minWidth: 200 }}
81
+ onChange={(event, newValue) => {
82
+ const newType = newValue;
83
+ const newAsserts = asserts.map((a, i) =>
84
+ i === index ? { ...a, type: newType as AssertionType } : a,
85
+ );
86
+ setAsserts(newAsserts);
87
+ onAdd(newAsserts);
88
+ }}
89
+ renderInput={(params) => <TextField {...params} label="Type" />}
90
+ />
91
+ <TextField
92
+ label="Value"
93
+ value={assert.value}
94
+ fullWidth
95
+ onChange={(e) => {
96
+ const newValue = e.target.value;
97
+ const newAsserts = asserts.map((a, i) =>
98
+ i === index ? { ...a, value: newValue } : a,
99
+ );
100
+ setAsserts(newAsserts);
101
+ onAdd(newAsserts);
102
+ }}
103
+ />
104
+ <IconButton onClick={() => handleRemoveAssert(index)} size="small">
105
+ <Delete />
106
+ </IconButton>
107
+ </Stack>
108
+ ))}
109
+ </Stack>
110
+ </Box>
111
+ <Button color="primary" onClick={handleAdd}>
112
+ Add Assert
113
+ </Button>
114
+ </>
115
+ );
116
+ };
117
+
118
+ export default AssertsForm;
@@ -0,0 +1,77 @@
1
+ import React from 'react';
2
+ import {
3
+ Dialog,
4
+ DialogTitle,
5
+ DialogContent,
6
+ DialogActions,
7
+ TextField,
8
+ Button,
9
+ } from '@mui/material';
10
+
11
+ interface PromptDialogProps {
12
+ open: boolean;
13
+ prompt: string;
14
+ index: number;
15
+ onAdd: (prompt: string) => void;
16
+ onCancel: () => void;
17
+ }
18
+
19
+ const PromptDialog: React.FC<PromptDialogProps> = ({ open, prompt, index, onAdd, onCancel }) => {
20
+ const [editingPrompt, setEditingPrompt] = React.useState(prompt);
21
+ const textFieldRef = React.useRef<HTMLInputElement>(null);
22
+
23
+ React.useEffect(() => {
24
+ setEditingPrompt(prompt);
25
+ }, [prompt]);
26
+
27
+ const handleAdd = (close: boolean) => {
28
+ onAdd(editingPrompt);
29
+ setEditingPrompt('');
30
+ if (close) {
31
+ onCancel();
32
+ } else if (textFieldRef.current) {
33
+ textFieldRef.current.focus();
34
+ }
35
+ };
36
+
37
+ return (
38
+ <Dialog open={open} onClose={onCancel} fullWidth maxWidth="md">
39
+ <DialogTitle>{`Edit Prompt ${index + 1}`}</DialogTitle>
40
+ <DialogContent>
41
+ <TextField
42
+ value={editingPrompt}
43
+ onChange={(e) => setEditingPrompt(e.target.value)}
44
+ fullWidth
45
+ margin="normal"
46
+ multiline
47
+ placeholder="The quick brown {{animal1}} jumps over the lazy {{animal2}}."
48
+ helperText="Tip: use the {{varname}} syntax to add variables to your prompt."
49
+ inputRef={textFieldRef}
50
+ />
51
+ </DialogContent>
52
+ <DialogActions>
53
+ <Button
54
+ onClick={handleAdd.bind(null, true)}
55
+ color="primary"
56
+ variant="contained"
57
+ disabled={!editingPrompt.length}
58
+ >
59
+ Add
60
+ </Button>
61
+ <Button
62
+ onClick={handleAdd.bind(null, false)}
63
+ color="primary"
64
+ variant="contained"
65
+ disabled={!editingPrompt.length}
66
+ >
67
+ Add Another
68
+ </Button>
69
+ <Button onClick={onCancel} color="secondary">
70
+ Cancel
71
+ </Button>
72
+ </DialogActions>
73
+ </Dialog>
74
+ );
75
+ };
76
+
77
+ export default PromptDialog;
@@ -0,0 +1,190 @@
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import Button from '@mui/material/Button';
3
+ import Typography from '@mui/material/Typography';
4
+ import IconButton from '@mui/material/IconButton';
5
+ import Table from '@mui/material/Table';
6
+ import TableBody from '@mui/material/TableBody';
7
+ import TableCell from '@mui/material/TableCell';
8
+ import TableContainer from '@mui/material/TableContainer';
9
+ import TableRow from '@mui/material/TableRow';
10
+ import Tooltip from '@mui/material/Tooltip';
11
+ import Stack from '@mui/material/Stack';
12
+ import Edit from '@mui/icons-material/Edit';
13
+ import Delete from '@mui/icons-material/Delete';
14
+ import Publish from '@mui/icons-material/Publish';
15
+ import Copy from '@mui/icons-material/ContentCopy';
16
+
17
+ import PromptDialog from './PromptDialog';
18
+ import { useStore } from '../../util/store';
19
+
20
+ const PromptsSection: React.FC = () => {
21
+ const [promptDialogOpen, setPromptDialogOpen] = useState(false);
22
+ const [editingPromptIndex, setEditingPromptIndex] = useState<number | null>(null);
23
+
24
+ const { prompts, setPrompts } = useStore();
25
+ const newPromptInputRef = useRef<HTMLInputElement>(null);
26
+
27
+ useEffect(() => {
28
+ if (editingPromptIndex !== null && editingPromptIndex > 0 && newPromptInputRef.current) {
29
+ newPromptInputRef.current.focus();
30
+ }
31
+ }, [editingPromptIndex]);
32
+
33
+ const handleEditPrompt = (index: number) => {
34
+ setEditingPromptIndex(index);
35
+ setPromptDialogOpen(true);
36
+ };
37
+
38
+ const handleAddPromptFromFile = (event: React.ChangeEvent<HTMLInputElement>) => {
39
+ event.stopPropagation();
40
+ event.preventDefault();
41
+
42
+ const file = event.target.files?.[0];
43
+ if (file) {
44
+ const reader = new FileReader();
45
+ reader.onload = (e) => {
46
+ const text = e.target?.result?.toString();
47
+ if (text) {
48
+ setPrompts([...prompts, text]);
49
+ }
50
+ };
51
+ reader.readAsText(file);
52
+ }
53
+ };
54
+
55
+ const handleDuplicatePrompt = (event: React.MouseEvent, index: number) => {
56
+ event.stopPropagation();
57
+ const duplicatedPrompt = prompts[index];
58
+ setPrompts([...prompts, duplicatedPrompt]);
59
+ };
60
+
61
+ const handleChangePrompt = (index: number, newPrompt: string) => {
62
+ setPrompts(prompts.map((p, i) => (i === index ? newPrompt : p)));
63
+ };
64
+
65
+ const handleRemovePrompt = (event: React.MouseEvent, indexToRemove: number) => {
66
+ event.stopPropagation();
67
+
68
+ if (confirm('Are you sure you want to remove this prompt?')) {
69
+ setPrompts(prompts.filter((_, index) => index !== indexToRemove));
70
+ }
71
+ };
72
+
73
+ return (
74
+ <div>
75
+ <Stack direction="row" spacing={2} justifyContent="space-between">
76
+ <Typography variant="h5">Prompts</Typography>
77
+ <div>
78
+ <label htmlFor={`file-input-add-prompt`}>
79
+ <Tooltip title="Upload prompt from file">
80
+ <span>
81
+ <IconButton component="span">
82
+ <Publish />
83
+ </IconButton>
84
+ <input
85
+ id={`file-input-add-prompt`}
86
+ type="file"
87
+ accept=".txt,.md"
88
+ onChange={handleAddPromptFromFile}
89
+ style={{ display: 'none' }}
90
+ />
91
+ </span>
92
+ </Tooltip>
93
+ </label>
94
+ <Button
95
+ color="primary"
96
+ onClick={() => {
97
+ setPromptDialogOpen(true);
98
+ }}
99
+ variant="contained"
100
+ >
101
+ Add Prompt
102
+ </Button>
103
+ </div>
104
+ </Stack>
105
+ <TableContainer>
106
+ <Table>
107
+ <TableBody>
108
+ {prompts.length === 0 ? (
109
+ <TableRow>
110
+ <TableCell colSpan={2} align="center">
111
+ No prompts added yet.
112
+ </TableCell>
113
+ </TableRow>
114
+ ) : (
115
+ prompts.map((prompt, index) => (
116
+ <TableRow
117
+ key={index}
118
+ sx={{
119
+ '&:hover': {
120
+ backgroundColor: 'rgba(0, 0, 0, 0.04)',
121
+ cursor: 'pointer',
122
+ },
123
+ }}
124
+ onClick={() => handleEditPrompt(index)}
125
+ >
126
+ <TableCell>
127
+ <Typography variant="body2">
128
+ {`Prompt #${index + 1}: `}
129
+ {(prompt.length > 250 ? prompt.slice(0, 250) + ' ...' : prompt)
130
+ .split(/({{\w+}})/g)
131
+ .map((part, i) =>
132
+ /{{\w+}}/g.test(part) ? (
133
+ <span
134
+ key={i}
135
+ style={{
136
+ backgroundColor: 'linen',
137
+ padding: '0.25rem',
138
+ borderRadius: '4px',
139
+ }}
140
+ >
141
+ {part}
142
+ </span>
143
+ ) : (
144
+ part
145
+ ),
146
+ )}
147
+ </Typography>
148
+ </TableCell>
149
+ <TableCell align="right" sx={{ minWidth: 150 }}>
150
+ <IconButton onClick={() => handleEditPrompt(index)} size="small">
151
+ <Edit />
152
+ </IconButton>
153
+ <IconButton
154
+ onClick={(event) => handleDuplicatePrompt(event, index)}
155
+ size="small"
156
+ >
157
+ <Copy />
158
+ </IconButton>
159
+ <IconButton onClick={(event) => handleRemovePrompt(event, index)} size="small">
160
+ <Delete />
161
+ </IconButton>
162
+ </TableCell>
163
+ </TableRow>
164
+ ))
165
+ )}
166
+ </TableBody>
167
+ </Table>
168
+ </TableContainer>
169
+ <PromptDialog
170
+ open={promptDialogOpen}
171
+ prompt={editingPromptIndex !== null ? prompts[editingPromptIndex] : ''}
172
+ index={editingPromptIndex !== null ? editingPromptIndex : 0}
173
+ onAdd={(newPrompt) => {
174
+ if (editingPromptIndex !== null) {
175
+ handleChangePrompt(editingPromptIndex, newPrompt);
176
+ } else {
177
+ setPrompts([...prompts, newPrompt]);
178
+ }
179
+ setEditingPromptIndex(null);
180
+ }}
181
+ onCancel={() => {
182
+ setEditingPromptIndex(null);
183
+ setPromptDialogOpen(false);
184
+ }}
185
+ />
186
+ </div>
187
+ );
188
+ };
189
+
190
+ export default PromptsSection;
@@ -0,0 +1,99 @@
1
+ import React from 'react';
2
+ import {
3
+ Box,
4
+ Dialog,
5
+ DialogTitle,
6
+ DialogContent,
7
+ TextField,
8
+ DialogActions,
9
+ Button,
10
+ } from '@mui/material';
11
+ import { ProviderConfig } from '../../../../../types';
12
+
13
+ interface ProviderConfigDialogProps {
14
+ open: boolean;
15
+ providerId: string;
16
+ config: ProviderConfig['config'];
17
+ onClose: () => void;
18
+ onSave: (config: ProviderConfig['config']) => void;
19
+ }
20
+
21
+ const ProviderConfigDialog: React.FC<ProviderConfigDialogProps> = ({
22
+ open,
23
+ providerId,
24
+ config,
25
+ onClose,
26
+ onSave,
27
+ }) => {
28
+ const [localConfig, setLocalConfig] = React.useState(config);
29
+
30
+ React.useEffect(() => {
31
+ setLocalConfig(config);
32
+ }, [config]);
33
+
34
+ const handleSave = () => {
35
+ onSave(localConfig);
36
+ };
37
+
38
+ return (
39
+ <Dialog open={open} onClose={onClose}>
40
+ <DialogTitle>Edit {providerId}</DialogTitle>
41
+ <DialogContent>
42
+ {Object.keys(localConfig).map((key) => {
43
+ const value = localConfig[key];
44
+ let handleChange;
45
+
46
+ if (
47
+ typeof value === 'number' ||
48
+ typeof value === 'boolean' ||
49
+ typeof value === 'string'
50
+ ) {
51
+ if (typeof value === 'number') {
52
+ handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
53
+ setLocalConfig({ ...localConfig, [key]: parseFloat(e.target.value) });
54
+ } else if (typeof value === 'boolean') {
55
+ handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
56
+ setLocalConfig({ ...localConfig, [key]: e.target.value === 'true' });
57
+ } else {
58
+ handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
59
+ setLocalConfig({ ...localConfig, [key]: e.target.value });
60
+ }
61
+
62
+ return (
63
+ <Box key={key} my={2}>
64
+ <TextField
65
+ label={key}
66
+ value={value}
67
+ onChange={handleChange}
68
+ fullWidth
69
+ type={typeof value === 'number' ? 'number' : 'text'}
70
+ />
71
+ </Box>
72
+ );
73
+ } else {
74
+ return (
75
+ <Box key={key} my={2}>
76
+ <TextField
77
+ label={key}
78
+ value={JSON.stringify(value)}
79
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
80
+ setLocalConfig({ ...localConfig, [key]: JSON.parse(e.target.value) })
81
+ }
82
+ fullWidth
83
+ multiline
84
+ minRows={3}
85
+ />
86
+ </Box>
87
+ );
88
+ }
89
+ })}
90
+ </DialogContent>
91
+ <DialogActions>
92
+ <Button onClick={onClose}>Cancel</Button>
93
+ <Button onClick={handleSave}>Save</Button>
94
+ </DialogActions>
95
+ </Dialog>
96
+ );
97
+ };
98
+
99
+ export default ProviderConfigDialog;