@runtypelabs/react-flow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +289 -0
- package/example/.env.example +3 -0
- package/example/index.html +25 -0
- package/example/node_modules/.bin/browserslist +21 -0
- package/example/node_modules/.bin/terser +21 -0
- package/example/node_modules/.bin/tsc +21 -0
- package/example/node_modules/.bin/tsserver +21 -0
- package/example/node_modules/.bin/vite +21 -0
- package/example/package.json +26 -0
- package/example/src/App.tsx +1744 -0
- package/example/src/main.tsx +11 -0
- package/example/tsconfig.json +21 -0
- package/example/vite.config.ts +13 -0
- package/package.json +65 -0
- package/src/components/RuntypeFlowEditor.tsx +528 -0
- package/src/components/nodes/BaseNode.tsx +357 -0
- package/src/components/nodes/CodeNode.tsx +252 -0
- package/src/components/nodes/ConditionalNode.tsx +264 -0
- package/src/components/nodes/FetchUrlNode.tsx +299 -0
- package/src/components/nodes/PromptNode.tsx +270 -0
- package/src/components/nodes/SendEmailNode.tsx +311 -0
- package/src/hooks/useFlowValidation.ts +424 -0
- package/src/hooks/useRuntypeFlow.ts +414 -0
- package/src/index.ts +28 -0
- package/src/types/index.ts +332 -0
- package/src/utils/adapter.ts +544 -0
- package/src/utils/layout.ts +284 -0
- package/tsconfig.json +29 -0
- package/tsup.config.ts +15 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import React, { memo, useCallback, useState } from 'react'
|
|
2
|
+
import type { NodeProps } from '@xyflow/react'
|
|
3
|
+
import { BaseNode, BrainIcon, NODE_HEADER_COLORS } from './BaseNode'
|
|
4
|
+
import type { RuntypeNodeData, PromptStepConfig, FlowStep } from '../../types'
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Styles
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
const styles = {
|
|
11
|
+
field: {
|
|
12
|
+
marginBottom: '12px',
|
|
13
|
+
},
|
|
14
|
+
label: {
|
|
15
|
+
display: 'block',
|
|
16
|
+
fontSize: '11px',
|
|
17
|
+
fontWeight: 600,
|
|
18
|
+
color: '#6b7280',
|
|
19
|
+
marginBottom: '4px',
|
|
20
|
+
textTransform: 'uppercase' as const,
|
|
21
|
+
letterSpacing: '0.03em',
|
|
22
|
+
},
|
|
23
|
+
input: {
|
|
24
|
+
width: '100%',
|
|
25
|
+
padding: '8px 10px',
|
|
26
|
+
fontSize: '12px',
|
|
27
|
+
border: '1px solid #e5e7eb',
|
|
28
|
+
borderRadius: '6px',
|
|
29
|
+
backgroundColor: '#f9fafb',
|
|
30
|
+
color: '#1f2937',
|
|
31
|
+
outline: 'none',
|
|
32
|
+
transition: 'border-color 0.15s ease, box-shadow 0.15s ease',
|
|
33
|
+
},
|
|
34
|
+
textarea: {
|
|
35
|
+
width: '100%',
|
|
36
|
+
padding: '8px 10px',
|
|
37
|
+
fontSize: '12px',
|
|
38
|
+
border: '1px solid #e5e7eb',
|
|
39
|
+
borderRadius: '6px',
|
|
40
|
+
backgroundColor: '#f9fafb',
|
|
41
|
+
color: '#1f2937',
|
|
42
|
+
outline: 'none',
|
|
43
|
+
resize: 'vertical' as const,
|
|
44
|
+
minHeight: '60px',
|
|
45
|
+
fontFamily: 'inherit',
|
|
46
|
+
transition: 'border-color 0.15s ease, box-shadow 0.15s ease',
|
|
47
|
+
},
|
|
48
|
+
select: {
|
|
49
|
+
width: '100%',
|
|
50
|
+
padding: '8px 10px',
|
|
51
|
+
fontSize: '12px',
|
|
52
|
+
border: '1px solid #e5e7eb',
|
|
53
|
+
borderRadius: '6px',
|
|
54
|
+
backgroundColor: '#f9fafb',
|
|
55
|
+
color: '#1f2937',
|
|
56
|
+
outline: 'none',
|
|
57
|
+
cursor: 'pointer',
|
|
58
|
+
},
|
|
59
|
+
row: {
|
|
60
|
+
display: 'flex',
|
|
61
|
+
gap: '8px',
|
|
62
|
+
},
|
|
63
|
+
modeBadge: (isAgent: boolean) => ({
|
|
64
|
+
display: 'inline-flex',
|
|
65
|
+
alignItems: 'center',
|
|
66
|
+
padding: '2px 8px',
|
|
67
|
+
borderRadius: '12px',
|
|
68
|
+
fontSize: '10px',
|
|
69
|
+
fontWeight: 600,
|
|
70
|
+
backgroundColor: isAgent ? '#dbeafe' : '#f3e8ff',
|
|
71
|
+
color: isAgent ? '#1d4ed8' : '#7c3aed',
|
|
72
|
+
textTransform: 'uppercase' as const,
|
|
73
|
+
letterSpacing: '0.05em',
|
|
74
|
+
}),
|
|
75
|
+
preview: {
|
|
76
|
+
fontSize: '11px',
|
|
77
|
+
color: '#6b7280',
|
|
78
|
+
backgroundColor: '#f3f4f6',
|
|
79
|
+
padding: '8px',
|
|
80
|
+
borderRadius: '6px',
|
|
81
|
+
fontFamily: 'monospace',
|
|
82
|
+
whiteSpace: 'pre-wrap' as const,
|
|
83
|
+
wordBreak: 'break-word' as const,
|
|
84
|
+
maxHeight: '80px',
|
|
85
|
+
overflow: 'auto',
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ============================================================================
|
|
90
|
+
// Prompt Node Component
|
|
91
|
+
// ============================================================================
|
|
92
|
+
|
|
93
|
+
export const PromptNode = memo(function PromptNode(props: NodeProps) {
|
|
94
|
+
const { data, selected, id } = props as NodeProps & { data: RuntypeNodeData }
|
|
95
|
+
const { step, onChange } = data
|
|
96
|
+
const config = step.config as PromptStepConfig
|
|
97
|
+
|
|
98
|
+
const [isExpanded, setIsExpanded] = useState(false)
|
|
99
|
+
|
|
100
|
+
const handleChange = useCallback(
|
|
101
|
+
(field: keyof PromptStepConfig, value: unknown) => {
|
|
102
|
+
onChange?.(id, {
|
|
103
|
+
config: {
|
|
104
|
+
...config,
|
|
105
|
+
[field]: value,
|
|
106
|
+
},
|
|
107
|
+
} as Partial<FlowStep>)
|
|
108
|
+
},
|
|
109
|
+
[id, config, onChange]
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
const handleNameChange = useCallback(
|
|
113
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
114
|
+
onChange?.(id, { name: e.target.value })
|
|
115
|
+
},
|
|
116
|
+
[id, onChange]
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const isAgentMode = config.mode === 'agent'
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<BaseNode
|
|
123
|
+
data={data}
|
|
124
|
+
selected={selected}
|
|
125
|
+
id={id}
|
|
126
|
+
typeLabel="AI Prompt"
|
|
127
|
+
icon={<BrainIcon />}
|
|
128
|
+
headerColor={NODE_HEADER_COLORS.prompt}
|
|
129
|
+
>
|
|
130
|
+
<div style={styles.field}>
|
|
131
|
+
<label style={styles.label}>Step Name</label>
|
|
132
|
+
<input
|
|
133
|
+
style={styles.input}
|
|
134
|
+
value={step.name}
|
|
135
|
+
onChange={handleNameChange}
|
|
136
|
+
placeholder="Enter step name"
|
|
137
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
138
|
+
onFocus={(e) => {
|
|
139
|
+
e.target.style.borderColor = '#6366f1'
|
|
140
|
+
e.target.style.boxShadow = '0 0 0 2px rgba(99, 102, 241, 0.1)'
|
|
141
|
+
}}
|
|
142
|
+
onBlur={(e) => {
|
|
143
|
+
e.target.style.borderColor = '#e5e7eb'
|
|
144
|
+
e.target.style.boxShadow = 'none'
|
|
145
|
+
}}
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div style={styles.field}>
|
|
150
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
|
|
151
|
+
<label style={styles.label}>Mode</label>
|
|
152
|
+
<span style={styles.modeBadge(isAgentMode)}>
|
|
153
|
+
{isAgentMode ? 'Agent' : 'Instruction'}
|
|
154
|
+
</span>
|
|
155
|
+
</div>
|
|
156
|
+
<select
|
|
157
|
+
style={styles.select}
|
|
158
|
+
value={config.mode || 'instruction'}
|
|
159
|
+
onChange={(e) => handleChange('mode', e.target.value)}
|
|
160
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
161
|
+
>
|
|
162
|
+
<option value="instruction">Instruction Mode</option>
|
|
163
|
+
<option value="agent">Agent Mode</option>
|
|
164
|
+
</select>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<div style={styles.field}>
|
|
168
|
+
<label style={styles.label}>Model</label>
|
|
169
|
+
<input
|
|
170
|
+
style={styles.input}
|
|
171
|
+
value={config.model || ''}
|
|
172
|
+
onChange={(e) => handleChange('model', e.target.value)}
|
|
173
|
+
placeholder="e.g., gpt-4, claude-3-sonnet"
|
|
174
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
175
|
+
/>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div style={styles.field}>
|
|
179
|
+
<label style={styles.label}>User Prompt</label>
|
|
180
|
+
<textarea
|
|
181
|
+
style={styles.textarea}
|
|
182
|
+
value={config.userPrompt || ''}
|
|
183
|
+
onChange={(e) => handleChange('userPrompt', e.target.value)}
|
|
184
|
+
placeholder="Enter your prompt..."
|
|
185
|
+
rows={3}
|
|
186
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
{isExpanded && (
|
|
191
|
+
<>
|
|
192
|
+
{config.systemPrompt !== undefined && (
|
|
193
|
+
<div style={styles.field}>
|
|
194
|
+
<label style={styles.label}>System Prompt</label>
|
|
195
|
+
<textarea
|
|
196
|
+
style={styles.textarea}
|
|
197
|
+
value={config.systemPrompt || ''}
|
|
198
|
+
onChange={(e) => handleChange('systemPrompt', e.target.value)}
|
|
199
|
+
placeholder="System instructions..."
|
|
200
|
+
rows={2}
|
|
201
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
202
|
+
/>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
<div style={styles.row}>
|
|
207
|
+
<div style={{ ...styles.field, flex: 1 }}>
|
|
208
|
+
<label style={styles.label}>Response Format</label>
|
|
209
|
+
<select
|
|
210
|
+
style={styles.select}
|
|
211
|
+
value={config.responseFormat || 'text'}
|
|
212
|
+
onChange={(e) => handleChange('responseFormat', e.target.value)}
|
|
213
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
214
|
+
>
|
|
215
|
+
<option value="text">Text</option>
|
|
216
|
+
<option value="json">JSON</option>
|
|
217
|
+
<option value="markdown">Markdown</option>
|
|
218
|
+
<option value="html">HTML</option>
|
|
219
|
+
</select>
|
|
220
|
+
</div>
|
|
221
|
+
<div style={{ ...styles.field, flex: 1 }}>
|
|
222
|
+
<label style={styles.label}>Temperature</label>
|
|
223
|
+
<input
|
|
224
|
+
style={styles.input}
|
|
225
|
+
type="number"
|
|
226
|
+
min="0"
|
|
227
|
+
max="2"
|
|
228
|
+
step="0.1"
|
|
229
|
+
value={config.temperature ?? 0.7}
|
|
230
|
+
onChange={(e) => handleChange('temperature', parseFloat(e.target.value))}
|
|
231
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
232
|
+
/>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<div style={styles.field}>
|
|
237
|
+
<label style={styles.label}>Output Variable</label>
|
|
238
|
+
<input
|
|
239
|
+
style={styles.input}
|
|
240
|
+
value={config.outputVariable || ''}
|
|
241
|
+
onChange={(e) => handleChange('outputVariable', e.target.value)}
|
|
242
|
+
placeholder="result"
|
|
243
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
244
|
+
/>
|
|
245
|
+
</div>
|
|
246
|
+
</>
|
|
247
|
+
)}
|
|
248
|
+
|
|
249
|
+
<button
|
|
250
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
251
|
+
style={{
|
|
252
|
+
width: '100%',
|
|
253
|
+
padding: '6px',
|
|
254
|
+
fontSize: '11px',
|
|
255
|
+
color: '#6366f1',
|
|
256
|
+
backgroundColor: 'transparent',
|
|
257
|
+
border: '1px dashed #e5e7eb',
|
|
258
|
+
borderRadius: '6px',
|
|
259
|
+
cursor: 'pointer',
|
|
260
|
+
marginTop: '4px',
|
|
261
|
+
}}
|
|
262
|
+
>
|
|
263
|
+
{isExpanded ? 'Show Less' : 'Show More Options'}
|
|
264
|
+
</button>
|
|
265
|
+
</BaseNode>
|
|
266
|
+
)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
export default PromptNode
|
|
270
|
+
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import React, { memo, useCallback, useState } from 'react'
|
|
2
|
+
import type { NodeProps } from '@xyflow/react'
|
|
3
|
+
import { BaseNode, MailIcon, NODE_HEADER_COLORS } from './BaseNode'
|
|
4
|
+
import type { RuntypeNodeData, SendEmailStepConfig, FlowStep } from '../../types'
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Styles
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
const styles = {
|
|
11
|
+
field: {
|
|
12
|
+
marginBottom: '12px',
|
|
13
|
+
},
|
|
14
|
+
label: {
|
|
15
|
+
display: 'block',
|
|
16
|
+
fontSize: '11px',
|
|
17
|
+
fontWeight: 600,
|
|
18
|
+
color: '#6b7280',
|
|
19
|
+
marginBottom: '4px',
|
|
20
|
+
textTransform: 'uppercase' as const,
|
|
21
|
+
letterSpacing: '0.03em',
|
|
22
|
+
},
|
|
23
|
+
input: {
|
|
24
|
+
width: '100%',
|
|
25
|
+
padding: '8px 10px',
|
|
26
|
+
fontSize: '12px',
|
|
27
|
+
border: '1px solid #e5e7eb',
|
|
28
|
+
borderRadius: '6px',
|
|
29
|
+
backgroundColor: '#f9fafb',
|
|
30
|
+
color: '#1f2937',
|
|
31
|
+
outline: 'none',
|
|
32
|
+
transition: 'border-color 0.15s ease, box-shadow 0.15s ease',
|
|
33
|
+
},
|
|
34
|
+
textarea: {
|
|
35
|
+
width: '100%',
|
|
36
|
+
padding: '8px 10px',
|
|
37
|
+
fontSize: '12px',
|
|
38
|
+
border: '1px solid #e5e7eb',
|
|
39
|
+
borderRadius: '6px',
|
|
40
|
+
backgroundColor: '#f9fafb',
|
|
41
|
+
color: '#1f2937',
|
|
42
|
+
outline: 'none',
|
|
43
|
+
resize: 'vertical' as const,
|
|
44
|
+
minHeight: '80px',
|
|
45
|
+
fontFamily: 'inherit',
|
|
46
|
+
transition: 'border-color 0.15s ease, box-shadow 0.15s ease',
|
|
47
|
+
},
|
|
48
|
+
select: {
|
|
49
|
+
width: '100%',
|
|
50
|
+
padding: '8px 10px',
|
|
51
|
+
fontSize: '12px',
|
|
52
|
+
border: '1px solid #e5e7eb',
|
|
53
|
+
borderRadius: '6px',
|
|
54
|
+
backgroundColor: '#f9fafb',
|
|
55
|
+
color: '#1f2937',
|
|
56
|
+
outline: 'none',
|
|
57
|
+
cursor: 'pointer',
|
|
58
|
+
},
|
|
59
|
+
row: {
|
|
60
|
+
display: 'flex',
|
|
61
|
+
gap: '8px',
|
|
62
|
+
},
|
|
63
|
+
emailPreview: {
|
|
64
|
+
backgroundColor: '#f8fafc',
|
|
65
|
+
border: '1px solid #e2e8f0',
|
|
66
|
+
borderRadius: '8px',
|
|
67
|
+
padding: '10px',
|
|
68
|
+
marginTop: '8px',
|
|
69
|
+
},
|
|
70
|
+
previewHeader: {
|
|
71
|
+
fontSize: '10px',
|
|
72
|
+
color: '#64748b',
|
|
73
|
+
marginBottom: '4px',
|
|
74
|
+
},
|
|
75
|
+
previewValue: {
|
|
76
|
+
fontSize: '12px',
|
|
77
|
+
color: '#1e293b',
|
|
78
|
+
fontWeight: 500,
|
|
79
|
+
marginBottom: '8px',
|
|
80
|
+
wordBreak: 'break-all' as const,
|
|
81
|
+
},
|
|
82
|
+
variableHint: {
|
|
83
|
+
display: 'inline-flex',
|
|
84
|
+
alignItems: 'center',
|
|
85
|
+
padding: '2px 6px',
|
|
86
|
+
borderRadius: '4px',
|
|
87
|
+
fontSize: '10px',
|
|
88
|
+
backgroundColor: '#f0f9ff',
|
|
89
|
+
color: '#0369a1',
|
|
90
|
+
fontFamily: 'monospace',
|
|
91
|
+
marginRight: '4px',
|
|
92
|
+
marginTop: '4px',
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Send Email Node Component
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
export const SendEmailNode = memo(function SendEmailNode(props: NodeProps) {
|
|
101
|
+
const { data, selected, id } = props as NodeProps & { data: RuntypeNodeData }
|
|
102
|
+
const { step, onChange } = data
|
|
103
|
+
const config = step.config as SendEmailStepConfig
|
|
104
|
+
|
|
105
|
+
const [isExpanded, setIsExpanded] = useState(false)
|
|
106
|
+
|
|
107
|
+
const handleChange = useCallback(
|
|
108
|
+
(field: keyof SendEmailStepConfig, value: unknown) => {
|
|
109
|
+
onChange?.(id, {
|
|
110
|
+
config: {
|
|
111
|
+
...config,
|
|
112
|
+
[field]: value,
|
|
113
|
+
},
|
|
114
|
+
} as Partial<FlowStep>)
|
|
115
|
+
},
|
|
116
|
+
[id, config, onChange]
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const handleNameChange = useCallback(
|
|
120
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
121
|
+
onChange?.(id, { name: e.target.value })
|
|
122
|
+
},
|
|
123
|
+
[id, onChange]
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
// Check for template variables in fields
|
|
127
|
+
const hasVariables = (value: string) => /\{\{[\w._]+\}\}/.test(value)
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<BaseNode
|
|
131
|
+
data={data}
|
|
132
|
+
selected={selected}
|
|
133
|
+
id={id}
|
|
134
|
+
typeLabel="Send Email"
|
|
135
|
+
icon={<MailIcon />}
|
|
136
|
+
headerColor={NODE_HEADER_COLORS['send-email']}
|
|
137
|
+
>
|
|
138
|
+
<div style={styles.field}>
|
|
139
|
+
<label style={styles.label}>Step Name</label>
|
|
140
|
+
<input
|
|
141
|
+
style={styles.input}
|
|
142
|
+
value={step.name}
|
|
143
|
+
onChange={handleNameChange}
|
|
144
|
+
placeholder="Enter step name"
|
|
145
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div style={styles.field}>
|
|
150
|
+
<label style={styles.label}>To</label>
|
|
151
|
+
<input
|
|
152
|
+
style={styles.input}
|
|
153
|
+
value={config.to || ''}
|
|
154
|
+
onChange={(e) => handleChange('to', e.target.value)}
|
|
155
|
+
placeholder="recipient@example.com or {{_record.metadata.email}}"
|
|
156
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
157
|
+
/>
|
|
158
|
+
{hasVariables(config.to || '') && (
|
|
159
|
+
<span style={styles.variableHint}>Uses variable</span>
|
|
160
|
+
)}
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div style={styles.field}>
|
|
164
|
+
<label style={styles.label}>Subject</label>
|
|
165
|
+
<input
|
|
166
|
+
style={styles.input}
|
|
167
|
+
value={config.subject || ''}
|
|
168
|
+
onChange={(e) => handleChange('subject', e.target.value)}
|
|
169
|
+
placeholder="Your subject line"
|
|
170
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
171
|
+
/>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<div style={styles.field}>
|
|
175
|
+
<label style={styles.label}>HTML Content</label>
|
|
176
|
+
<textarea
|
|
177
|
+
style={styles.textarea}
|
|
178
|
+
value={config.html || ''}
|
|
179
|
+
onChange={(e) => handleChange('html', e.target.value)}
|
|
180
|
+
placeholder="<p>Your email content...</p> Use {{variable}} for dynamic content"
|
|
181
|
+
rows={4}
|
|
182
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
183
|
+
/>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
{isExpanded && (
|
|
187
|
+
<>
|
|
188
|
+
<div style={styles.field}>
|
|
189
|
+
<label style={styles.label}>From</label>
|
|
190
|
+
<input
|
|
191
|
+
style={styles.input}
|
|
192
|
+
value={config.from || ''}
|
|
193
|
+
onChange={(e) => handleChange('from', e.target.value)}
|
|
194
|
+
placeholder="no-reply@messages.runtype.com"
|
|
195
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
196
|
+
/>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<div style={styles.row}>
|
|
200
|
+
<div style={{ ...styles.field, flex: 1 }}>
|
|
201
|
+
<label style={styles.label}>CC</label>
|
|
202
|
+
<input
|
|
203
|
+
style={styles.input}
|
|
204
|
+
value={config.cc || ''}
|
|
205
|
+
onChange={(e) => handleChange('cc', e.target.value)}
|
|
206
|
+
placeholder="cc@example.com"
|
|
207
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
<div style={{ ...styles.field, flex: 1 }}>
|
|
211
|
+
<label style={styles.label}>BCC</label>
|
|
212
|
+
<input
|
|
213
|
+
style={styles.input}
|
|
214
|
+
value={config.bcc || ''}
|
|
215
|
+
onChange={(e) => handleChange('bcc', e.target.value)}
|
|
216
|
+
placeholder="bcc@example.com"
|
|
217
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div style={styles.field}>
|
|
223
|
+
<label style={styles.label}>Reply To</label>
|
|
224
|
+
<input
|
|
225
|
+
style={styles.input}
|
|
226
|
+
value={config.replyTo || ''}
|
|
227
|
+
onChange={(e) => handleChange('replyTo', e.target.value)}
|
|
228
|
+
placeholder="reply@example.com"
|
|
229
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<div style={styles.field}>
|
|
234
|
+
<label style={styles.label}>Plain Text (Fallback)</label>
|
|
235
|
+
<textarea
|
|
236
|
+
style={styles.textarea}
|
|
237
|
+
value={config.text || ''}
|
|
238
|
+
onChange={(e) => handleChange('text', e.target.value)}
|
|
239
|
+
placeholder="Plain text version of your email..."
|
|
240
|
+
rows={2}
|
|
241
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
242
|
+
/>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<div style={styles.row}>
|
|
246
|
+
<div style={{ ...styles.field, flex: 1 }}>
|
|
247
|
+
<label style={styles.label}>On Error</label>
|
|
248
|
+
<select
|
|
249
|
+
style={styles.select}
|
|
250
|
+
value={config.errorHandling || 'fail'}
|
|
251
|
+
onChange={(e) => handleChange('errorHandling', e.target.value)}
|
|
252
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
253
|
+
>
|
|
254
|
+
<option value="fail">Fail</option>
|
|
255
|
+
<option value="continue">Continue</option>
|
|
256
|
+
<option value="default">Use Default</option>
|
|
257
|
+
</select>
|
|
258
|
+
</div>
|
|
259
|
+
<div style={{ ...styles.field, flex: 1 }}>
|
|
260
|
+
<label style={styles.label}>Output Variable</label>
|
|
261
|
+
<input
|
|
262
|
+
style={styles.input}
|
|
263
|
+
value={config.outputVariable || ''}
|
|
264
|
+
onChange={(e) => handleChange('outputVariable', e.target.value)}
|
|
265
|
+
placeholder="email_result"
|
|
266
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
267
|
+
/>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</>
|
|
271
|
+
)}
|
|
272
|
+
|
|
273
|
+
{/* Email Preview */}
|
|
274
|
+
{(config.to || config.subject) && (
|
|
275
|
+
<div style={styles.emailPreview}>
|
|
276
|
+
<div style={styles.previewHeader}>Preview</div>
|
|
277
|
+
{config.to && (
|
|
278
|
+
<div style={styles.previewValue}>
|
|
279
|
+
<strong>To:</strong> {config.to}
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
{config.subject && (
|
|
283
|
+
<div style={styles.previewValue}>
|
|
284
|
+
<strong>Subject:</strong> {config.subject}
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
|
|
290
|
+
<button
|
|
291
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
292
|
+
style={{
|
|
293
|
+
width: '100%',
|
|
294
|
+
padding: '6px',
|
|
295
|
+
fontSize: '11px',
|
|
296
|
+
color: '#6366f1',
|
|
297
|
+
backgroundColor: 'transparent',
|
|
298
|
+
border: '1px dashed #e5e7eb',
|
|
299
|
+
borderRadius: '6px',
|
|
300
|
+
cursor: 'pointer',
|
|
301
|
+
marginTop: '8px',
|
|
302
|
+
}}
|
|
303
|
+
>
|
|
304
|
+
{isExpanded ? 'Show Less' : 'Show More Options'}
|
|
305
|
+
</button>
|
|
306
|
+
</BaseNode>
|
|
307
|
+
)
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
export default SendEmailNode
|
|
311
|
+
|