@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.
@@ -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>&#10;&#10;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
+