@walwal-harness/cli 7.1.39 → 7.1.40
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/HR-Resource/ops/SKILL.md +10 -0
- package/HR-Resource/ops-prompt-inspector/SKILL.md +100 -0
- package/HR-Resource/ops-prompt-inspector/agents/installation-agent.json +7 -0
- package/HR-Resource/ops-prompt-inspector/assets/PromptInspector.tsx +599 -0
- package/HR-Resource/ops-prompt-inspector/references/handler-templates.md +255 -0
- package/HR-Resource/ops-prompt-inspector/scripts/discover_apis.py +509 -0
- package/HR-Resource/ops-prompt-inspector/scripts/install.sh +32 -0
- package/HR-Resource/ops-prompt-inspector/scripts/setup.py +318 -0
- package/bin/init.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// Prompt Inspector v1.1.0 - API Search Feature
|
|
4
|
+
export const PROMPT_INSPECTOR_VERSION = '1.1.0';
|
|
5
|
+
|
|
6
|
+
import React, { useState, useCallback, useEffect } from 'react';
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Types
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
interface APIEndpoint {
|
|
13
|
+
id: string;
|
|
14
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
15
|
+
path: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface TriggerType {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
icon: string;
|
|
23
|
+
description: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface HandlerType {
|
|
27
|
+
id: string;
|
|
28
|
+
label: string;
|
|
29
|
+
description: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ErrorHandler {
|
|
33
|
+
id: number;
|
|
34
|
+
type: HandlerType;
|
|
35
|
+
message: string;
|
|
36
|
+
errorCode?: string;
|
|
37
|
+
redirectPath?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface Binding {
|
|
41
|
+
mode: 'comment' | 'api';
|
|
42
|
+
elementSelector: string;
|
|
43
|
+
elementDescription?: string;
|
|
44
|
+
comment?: string;
|
|
45
|
+
api?: APIEndpoint;
|
|
46
|
+
trigger?: TriggerType;
|
|
47
|
+
successHandler?: {
|
|
48
|
+
type: HandlerType;
|
|
49
|
+
message: string;
|
|
50
|
+
redirectPath?: string;
|
|
51
|
+
};
|
|
52
|
+
errorHandlers?: ErrorHandler[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface PromptInspectorProps {
|
|
56
|
+
apis?: APIEndpoint[];
|
|
57
|
+
onBindingComplete?: (binding: Binding) => void;
|
|
58
|
+
onExportMarkdown?: (markdown: string) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// Constants
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
const BINDING_MODES = [
|
|
66
|
+
{ id: 'comment', label: 'Comment Only', description: 'Leave a comment or annotation on this element' },
|
|
67
|
+
{ id: 'api', label: 'Connect API', description: 'Bind an API call to this element' },
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const TRIGGER_TYPES: TriggerType[] = [
|
|
71
|
+
{ id: 'onComponentMount', label: 'Mount', icon: '🔄', description: 'Component loaded' },
|
|
72
|
+
{ id: 'onPageLoad', label: 'Page Load', icon: '📄', description: 'Page initially loads' },
|
|
73
|
+
{ id: 'onClick', label: 'Click', icon: '👆', description: 'Element clicked' },
|
|
74
|
+
{ id: 'onSubmit', label: 'Submit', icon: '📤', description: 'Form submitted' },
|
|
75
|
+
{ id: 'onBlur', label: 'Blur', icon: '👁️', description: 'Input loses focus' },
|
|
76
|
+
{ id: 'onChange', label: 'Change', icon: '✏️', description: 'Value changes' },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const HANDLER_TYPES: HandlerType[] = [
|
|
80
|
+
{ id: 'toast', label: 'Toast Message', description: 'Brief notification' },
|
|
81
|
+
{ id: 'popup', label: 'Popup Dialog', description: 'Modal dialog' },
|
|
82
|
+
{ id: 'redirect', label: 'Page Redirect', description: 'Navigate to page' },
|
|
83
|
+
{ id: 'none', label: 'None', description: 'No feedback' },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
// ============================================================================
|
|
87
|
+
// Component
|
|
88
|
+
// ============================================================================
|
|
89
|
+
|
|
90
|
+
export function PromptInspector({
|
|
91
|
+
apis = [],
|
|
92
|
+
onBindingComplete,
|
|
93
|
+
onExportMarkdown,
|
|
94
|
+
}: PromptInspectorProps) {
|
|
95
|
+
const [isSelecting, setIsSelecting] = useState(false);
|
|
96
|
+
const [highlightRect, setHighlightRect] = useState<DOMRect | null>(null);
|
|
97
|
+
const [showPanel, setShowPanel] = useState(false);
|
|
98
|
+
const [panelStep, setPanelStep] = useState<'mode' | 'comment' | 'api'>('mode');
|
|
99
|
+
const [bindings, setBindings] = useState<Binding[]>([]);
|
|
100
|
+
const [badgePositions, setBadgePositions] = useState<Array<{ index: number; top: number; left: number; mode: string }>>([]);
|
|
101
|
+
const [routePath, setRoutePath] = useState(typeof window !== 'undefined' ? window.location.pathname : '/');
|
|
102
|
+
const [apiSearchQuery, setApiSearchQuery] = useState('');
|
|
103
|
+
const [isApiDropdownOpen, setIsApiDropdownOpen] = useState(false);
|
|
104
|
+
|
|
105
|
+
const [currentBinding, setCurrentBinding] = useState<Partial<Binding>>({
|
|
106
|
+
mode: undefined,
|
|
107
|
+
comment: '',
|
|
108
|
+
trigger: TRIGGER_TYPES[2],
|
|
109
|
+
successHandler: { type: HANDLER_TYPES[0], message: 'Successfully completed.' },
|
|
110
|
+
errorHandlers: [{ id: 1, type: HANDLER_TYPES[0], message: 'An error occurred.', errorCode: '' }],
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Update badge positions
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
const updatePositions = () => {
|
|
116
|
+
const positions = bindings.map((binding, index) => {
|
|
117
|
+
try {
|
|
118
|
+
const element = document.querySelector(binding.elementSelector);
|
|
119
|
+
if (element) {
|
|
120
|
+
const rect = element.getBoundingClientRect();
|
|
121
|
+
return { index: index + 1, top: rect.top + window.scrollY, left: rect.left + window.scrollX, mode: binding.mode };
|
|
122
|
+
}
|
|
123
|
+
} catch {}
|
|
124
|
+
return null;
|
|
125
|
+
}).filter(Boolean) as Array<{ index: number; top: number; left: number; mode: string }>;
|
|
126
|
+
setBadgePositions(positions);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
updatePositions();
|
|
130
|
+
window.addEventListener('scroll', updatePositions);
|
|
131
|
+
window.addEventListener('resize', updatePositions);
|
|
132
|
+
return () => {
|
|
133
|
+
window.removeEventListener('scroll', updatePositions);
|
|
134
|
+
window.removeEventListener('resize', updatePositions);
|
|
135
|
+
};
|
|
136
|
+
}, [bindings]);
|
|
137
|
+
|
|
138
|
+
// CSS selector generator - handles Tailwind CSS special characters
|
|
139
|
+
const getSelector = useCallback((element: HTMLElement): string => {
|
|
140
|
+
// 1. Try ID selector first (most reliable)
|
|
141
|
+
if (element.id) {
|
|
142
|
+
const escapedId = CSS.escape(element.id);
|
|
143
|
+
return `#${escapedId}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 2. Try data attributes (great for component identification)
|
|
147
|
+
const dataTestId = element.getAttribute('data-testid') || element.getAttribute('data-id');
|
|
148
|
+
if (dataTestId) {
|
|
149
|
+
return `[data-testid="${dataTestId}"]`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 3. Try clean class names (filter out Tailwind special chars)
|
|
153
|
+
const invalidChars = /[:\[\]\/&@!%]/;
|
|
154
|
+
const cleanClasses = Array.from(element.classList).filter(
|
|
155
|
+
c => !c.startsWith('_') && !invalidChars.test(c)
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
if (cleanClasses.length > 0) {
|
|
159
|
+
const escapedClasses = cleanClasses.map(c => CSS.escape(c)).join('.');
|
|
160
|
+
const selector = `${element.tagName.toLowerCase()}.${escapedClasses}`;
|
|
161
|
+
try {
|
|
162
|
+
if (document.querySelectorAll(selector).length === 1) return selector;
|
|
163
|
+
} catch {
|
|
164
|
+
// Invalid selector, continue to path-based approach
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 4. Try combining tag + text content for buttons/links
|
|
169
|
+
const tag = element.tagName.toLowerCase();
|
|
170
|
+
const text = element.textContent?.trim().slice(0, 30);
|
|
171
|
+
if (['button', 'a', 'label'].includes(tag) && text) {
|
|
172
|
+
// Use XPath-like text matching via attribute selector workaround
|
|
173
|
+
const ariaLabel = element.getAttribute('aria-label');
|
|
174
|
+
if (ariaLabel) {
|
|
175
|
+
return `${tag}[aria-label="${CSS.escape(ariaLabel)}"]`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 5. Fall back to path-based selector (most reliable)
|
|
180
|
+
const path: string[] = [];
|
|
181
|
+
let current: HTMLElement | null = element;
|
|
182
|
+
while (current && current !== document.body) {
|
|
183
|
+
let selector = current.tagName.toLowerCase();
|
|
184
|
+
|
|
185
|
+
if (current.id) {
|
|
186
|
+
path.unshift(`#${CSS.escape(current.id)}`);
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Add nth-child for disambiguation
|
|
191
|
+
const siblings = current.parentElement?.children;
|
|
192
|
+
if (siblings && siblings.length > 1) {
|
|
193
|
+
const sameTagSiblings = Array.from(siblings).filter(
|
|
194
|
+
s => s.tagName === current!.tagName
|
|
195
|
+
);
|
|
196
|
+
if (sameTagSiblings.length > 1) {
|
|
197
|
+
const index = sameTagSiblings.indexOf(current) + 1;
|
|
198
|
+
selector += `:nth-of-type(${index})`;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
path.unshift(selector);
|
|
203
|
+
current = current.parentElement;
|
|
204
|
+
}
|
|
205
|
+
return path.join(' > ');
|
|
206
|
+
}, []);
|
|
207
|
+
|
|
208
|
+
// Selection handlers
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
if (!isSelecting) return;
|
|
211
|
+
|
|
212
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
213
|
+
const target = e.target as HTMLElement;
|
|
214
|
+
if (target.closest('[data-prompt-inspector-ignore]')) return;
|
|
215
|
+
setHighlightRect(target.getBoundingClientRect());
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const handleClick = (e: MouseEvent) => {
|
|
219
|
+
const target = e.target as HTMLElement;
|
|
220
|
+
if (target.closest('[data-prompt-inspector-ignore]')) return;
|
|
221
|
+
e.preventDefault();
|
|
222
|
+
e.stopPropagation();
|
|
223
|
+
|
|
224
|
+
const selector = getSelector(target);
|
|
225
|
+
setCurrentBinding(prev => ({
|
|
226
|
+
...prev,
|
|
227
|
+
elementSelector: selector,
|
|
228
|
+
elementDescription: target.textContent?.slice(0, 50) || undefined,
|
|
229
|
+
}));
|
|
230
|
+
setIsSelecting(false);
|
|
231
|
+
setHighlightRect(null);
|
|
232
|
+
setPanelStep('mode');
|
|
233
|
+
setShowPanel(true);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
237
|
+
document.addEventListener('click', handleClick, true);
|
|
238
|
+
return () => {
|
|
239
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
240
|
+
document.removeEventListener('click', handleClick, true);
|
|
241
|
+
};
|
|
242
|
+
}, [isSelecting, getSelector]);
|
|
243
|
+
|
|
244
|
+
const selectMode = (mode: typeof BINDING_MODES[0]) => {
|
|
245
|
+
setCurrentBinding(prev => ({ ...prev, mode: mode.id as 'comment' | 'api' }));
|
|
246
|
+
setPanelStep(mode.id as 'comment' | 'api');
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const resetPanel = (continueSelecting = false) => {
|
|
250
|
+
setShowPanel(false);
|
|
251
|
+
setPanelStep('mode');
|
|
252
|
+
setCurrentBinding({
|
|
253
|
+
mode: undefined,
|
|
254
|
+
comment: '',
|
|
255
|
+
trigger: TRIGGER_TYPES[2],
|
|
256
|
+
successHandler: { type: HANDLER_TYPES[0], message: 'Successfully completed.' },
|
|
257
|
+
errorHandlers: [{ id: 1, type: HANDLER_TYPES[0], message: 'An error occurred.', errorCode: '' }],
|
|
258
|
+
});
|
|
259
|
+
setApiSearchQuery('');
|
|
260
|
+
setIsApiDropdownOpen(false);
|
|
261
|
+
if (continueSelecting) setIsSelecting(true);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Filter APIs based on search query
|
|
265
|
+
const filteredApis = apis.filter(api => {
|
|
266
|
+
if (!apiSearchQuery.trim()) return true;
|
|
267
|
+
const query = apiSearchQuery.toLowerCase();
|
|
268
|
+
return (
|
|
269
|
+
api.method.toLowerCase().includes(query) ||
|
|
270
|
+
api.path.toLowerCase().includes(query) ||
|
|
271
|
+
(api.description?.toLowerCase().includes(query) ?? false)
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Close API dropdown when clicking outside
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
if (!isApiDropdownOpen) return;
|
|
278
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
279
|
+
const target = e.target as HTMLElement;
|
|
280
|
+
if (!target.closest('[data-api-search]')) {
|
|
281
|
+
setIsApiDropdownOpen(false);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
285
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
286
|
+
}, [isApiDropdownOpen]);
|
|
287
|
+
|
|
288
|
+
const completeBinding = () => {
|
|
289
|
+
if (!currentBinding.elementSelector) return;
|
|
290
|
+
if (currentBinding.mode === 'api' && !currentBinding.api) return;
|
|
291
|
+
|
|
292
|
+
const binding = currentBinding as Binding;
|
|
293
|
+
setBindings(prev => [...prev, binding]);
|
|
294
|
+
onBindingComplete?.(binding);
|
|
295
|
+
resetPanel(true);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const addErrorHandler = () => {
|
|
299
|
+
setCurrentBinding(prev => ({
|
|
300
|
+
...prev,
|
|
301
|
+
errorHandlers: [...(prev.errorHandlers || []), { id: Date.now(), type: HANDLER_TYPES[0], message: '', errorCode: '' }],
|
|
302
|
+
}));
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const removeErrorHandler = (id: number) => {
|
|
306
|
+
setCurrentBinding(prev => ({
|
|
307
|
+
...prev,
|
|
308
|
+
errorHandlers: prev.errorHandlers?.filter(h => h.id !== id),
|
|
309
|
+
}));
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const updateErrorHandler = (id: number, field: string, value: any) => {
|
|
313
|
+
setCurrentBinding(prev => ({
|
|
314
|
+
...prev,
|
|
315
|
+
errorHandlers: prev.errorHandlers?.map(h => h.id === id ? { ...h, [field]: value } : h),
|
|
316
|
+
}));
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const exportMarkdown = () => {
|
|
320
|
+
const lines = [`# Route: ${routePath}`, ''];
|
|
321
|
+
bindings.forEach((binding, index) => {
|
|
322
|
+
const num = index + 1;
|
|
323
|
+
if (binding.mode === 'comment') {
|
|
324
|
+
lines.push(`## ${num}. Comment`);
|
|
325
|
+
lines.push(`- Selector: \`${binding.elementSelector}\``);
|
|
326
|
+
lines.push(`- Comment: ${binding.comment}`);
|
|
327
|
+
} else {
|
|
328
|
+
lines.push(`## ${num}. ${binding.api?.method} ${binding.api?.path}`);
|
|
329
|
+
lines.push(`- Selector: \`${binding.elementSelector}\``);
|
|
330
|
+
lines.push(`- Trigger: ${binding.trigger?.id}`);
|
|
331
|
+
if (binding.successHandler?.type.id !== 'none') {
|
|
332
|
+
lines.push(`- OnSuccess: ${binding.successHandler?.type.id}${binding.successHandler?.message ? ` ("${binding.successHandler.message}")` : ''}${binding.successHandler?.redirectPath ? ` → ${binding.successHandler.redirectPath}` : ''}`);
|
|
333
|
+
}
|
|
334
|
+
binding.errorHandlers?.forEach((handler, i) => {
|
|
335
|
+
if (handler.type.id !== 'none') {
|
|
336
|
+
const errorCodePart = handler.errorCode ? `[${handler.errorCode}] ` : '';
|
|
337
|
+
lines.push(`- OnError${(binding.errorHandlers?.length || 0) > 1 ? ` #${i + 1}` : ''}: ${errorCodePart}${handler.type.id}${handler.message ? ` ("${handler.message}")` : ''}${handler.redirectPath ? ` → ${handler.redirectPath}` : ''}`);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
lines.push('');
|
|
342
|
+
});
|
|
343
|
+
const markdown = lines.join('\n');
|
|
344
|
+
navigator.clipboard.writeText(markdown);
|
|
345
|
+
onExportMarkdown?.(markdown);
|
|
346
|
+
alert('Copied to clipboard!\n\n' + markdown);
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// Styles
|
|
350
|
+
const styles = {
|
|
351
|
+
overlay: { position: 'fixed' as const, inset: 0, backgroundColor: 'rgba(0,0,0,0.3)', zIndex: 9998, pointerEvents: 'none' as const },
|
|
352
|
+
toolbar: { position: 'fixed' as const, bottom: 20, right: 20, zIndex: 10000, display: 'flex', gap: 8, padding: 8, backgroundColor: '#1a1a2e', borderRadius: 12, boxShadow: '0 4px 20px rgba(0,0,0,0.3)' },
|
|
353
|
+
button: { padding: '10px 16px', border: 'none', borderRadius: 8, cursor: 'pointer', fontSize: 14, fontWeight: 500 },
|
|
354
|
+
highlight: { position: 'fixed' as const, pointerEvents: 'none' as const, border: '3px solid #6366f1', backgroundColor: 'rgba(99,102,241,0.2)', borderRadius: 4, zIndex: 9997 },
|
|
355
|
+
panel: { position: 'fixed' as const, top: '50%', left: '50%', transform: 'translate(-50%,-50%)', zIndex: 10001, width: 520, maxHeight: '85vh', overflow: 'auto', backgroundColor: '#1a1a2e', borderRadius: 16, boxShadow: '0 8px 32px rgba(0,0,0,0.4)', color: 'white' },
|
|
356
|
+
sectionTitle: { fontSize: 12, fontWeight: 600, color: '#9ca3af', marginBottom: 12, textTransform: 'uppercase' as const, letterSpacing: '0.5px' },
|
|
357
|
+
select: { width: '100%', padding: 12, backgroundColor: '#374151', border: '1px solid #4b5563', borderRadius: 8, color: 'white', fontSize: 14 },
|
|
358
|
+
input: { width: '100%', padding: 12, backgroundColor: '#374151', border: '1px solid #4b5563', borderRadius: 8, color: 'white', fontSize: 14, boxSizing: 'border-box' as const },
|
|
359
|
+
badge: { position: 'absolute' as const, width: 24, height: 24, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700, color: 'white', zIndex: 9990, boxShadow: '0 2px 8px rgba(0,0,0,0.3)', transform: 'translate(-50%,-50%)' },
|
|
360
|
+
modeCard: { display: 'flex', alignItems: 'center', gap: 16, padding: 16, backgroundColor: '#374151', borderRadius: 12, cursor: 'pointer', border: '2px solid transparent' },
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<div data-prompt-inspector-ignore>
|
|
365
|
+
{isSelecting && <div style={styles.overlay} />}
|
|
366
|
+
{isSelecting && highlightRect && (
|
|
367
|
+
<div style={{ ...styles.highlight, top: highlightRect.top, left: highlightRect.left, width: highlightRect.width, height: highlightRect.height }} />
|
|
368
|
+
)}
|
|
369
|
+
|
|
370
|
+
{/* Badges */}
|
|
371
|
+
{badgePositions.map((pos) => (
|
|
372
|
+
<div key={pos.index} style={{ ...styles.badge, top: pos.top, left: pos.left, backgroundColor: pos.mode === 'comment' ? '#f59e0b' : '#6366f1' }} title={`Binding #${pos.index}`}>
|
|
373
|
+
{pos.index}
|
|
374
|
+
</div>
|
|
375
|
+
))}
|
|
376
|
+
|
|
377
|
+
{/* Panel Backdrop */}
|
|
378
|
+
{showPanel && <div style={{ position: 'fixed', inset: 0, zIndex: 10000 }} onClick={() => resetPanel(true)} />}
|
|
379
|
+
|
|
380
|
+
{/* Toolbar */}
|
|
381
|
+
<div style={styles.toolbar}>
|
|
382
|
+
<input
|
|
383
|
+
style={{ padding: '8px 12px', backgroundColor: '#374151', border: '1px solid #4b5563', borderRadius: 8, color: 'white', fontSize: 13, width: 150 }}
|
|
384
|
+
value={routePath}
|
|
385
|
+
onChange={(e) => setRoutePath(e.target.value)}
|
|
386
|
+
placeholder="/route/path"
|
|
387
|
+
/>
|
|
388
|
+
<button
|
|
389
|
+
style={{ ...styles.button, backgroundColor: isSelecting ? '#22c55e' : '#6366f1', color: 'white' }}
|
|
390
|
+
onClick={() => setIsSelecting(!isSelecting)}
|
|
391
|
+
>
|
|
392
|
+
{isSelecting ? 'Selecting...' : 'Select Element'}
|
|
393
|
+
</button>
|
|
394
|
+
{bindings.length > 0 && (
|
|
395
|
+
<button style={{ ...styles.button, backgroundColor: '#374151', color: 'white' }} onClick={exportMarkdown}>
|
|
396
|
+
Export ({bindings.length})
|
|
397
|
+
</button>
|
|
398
|
+
)}
|
|
399
|
+
</div>
|
|
400
|
+
|
|
401
|
+
{/* Panel */}
|
|
402
|
+
{showPanel && (
|
|
403
|
+
<div style={styles.panel} onClick={(e) => e.stopPropagation()}>
|
|
404
|
+
<div style={{ padding: 20, borderBottom: '1px solid #374151' }}>
|
|
405
|
+
<h2 style={{ margin: 0, fontSize: 18 }}>
|
|
406
|
+
{panelStep === 'mode' && 'Choose Action'}
|
|
407
|
+
{panelStep === 'comment' && 'Add Comment'}
|
|
408
|
+
{panelStep === 'api' && 'Configure API Binding'}
|
|
409
|
+
</h2>
|
|
410
|
+
{currentBinding.elementSelector && (
|
|
411
|
+
<div style={{ display: 'inline-block', padding: '4px 8px', backgroundColor: '#6366f1', borderRadius: 4, fontSize: 12, fontFamily: 'monospace', marginTop: 8 }}>
|
|
412
|
+
{currentBinding.elementSelector}
|
|
413
|
+
</div>
|
|
414
|
+
)}
|
|
415
|
+
</div>
|
|
416
|
+
|
|
417
|
+
<div style={{ padding: 20 }}>
|
|
418
|
+
{/* Mode Selection */}
|
|
419
|
+
{panelStep === 'mode' && (
|
|
420
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
421
|
+
{BINDING_MODES.map(mode => (
|
|
422
|
+
<div key={mode.id} style={styles.modeCard} onClick={() => selectMode(mode)}>
|
|
423
|
+
<div style={{ width: 48, height: 48, borderRadius: 12, backgroundColor: mode.id === 'comment' ? '#f59e0b' : '#6366f1', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 24 }}>
|
|
424
|
+
{mode.id === 'comment' ? '💬' : '🔗'}
|
|
425
|
+
</div>
|
|
426
|
+
<div>
|
|
427
|
+
<div style={{ fontWeight: 600, fontSize: 16 }}>{mode.label}</div>
|
|
428
|
+
<div style={{ fontSize: 13, color: '#9ca3af', marginTop: 4 }}>{mode.description}</div>
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
))}
|
|
432
|
+
</div>
|
|
433
|
+
)}
|
|
434
|
+
|
|
435
|
+
{/* Comment Mode */}
|
|
436
|
+
{panelStep === 'comment' && (
|
|
437
|
+
<>
|
|
438
|
+
<div style={{ marginBottom: 24 }}>
|
|
439
|
+
<div style={styles.sectionTitle}>Your Comment</div>
|
|
440
|
+
<textarea
|
|
441
|
+
style={{ ...styles.input, minHeight: 120, resize: 'vertical' }}
|
|
442
|
+
placeholder="Enter your comment..."
|
|
443
|
+
value={currentBinding.comment || ''}
|
|
444
|
+
onChange={(e) => setCurrentBinding(prev => ({ ...prev, comment: e.target.value }))}
|
|
445
|
+
/>
|
|
446
|
+
</div>
|
|
447
|
+
<div style={{ display: 'flex', gap: 12 }}>
|
|
448
|
+
<button style={{ ...styles.button, backgroundColor: '#6366f1', color: 'white', flex: 1, opacity: currentBinding.comment ? 1 : 0.5 }} disabled={!currentBinding.comment} onClick={completeBinding}>Add Comment</button>
|
|
449
|
+
<button style={{ ...styles.button, backgroundColor: '#374151', color: 'white' }} onClick={() => setPanelStep('mode')}>Back</button>
|
|
450
|
+
<button style={{ ...styles.button, backgroundColor: '#374151', color: 'white' }} onClick={() => resetPanel(false)}>Cancel</button>
|
|
451
|
+
</div>
|
|
452
|
+
</>
|
|
453
|
+
)}
|
|
454
|
+
|
|
455
|
+
{/* API Mode */}
|
|
456
|
+
{panelStep === 'api' && (
|
|
457
|
+
<>
|
|
458
|
+
{/* API Selection with Search */}
|
|
459
|
+
<div style={{ marginBottom: 24, position: 'relative' }} data-api-search>
|
|
460
|
+
<div style={styles.sectionTitle}>Select API ({apis.length} available)</div>
|
|
461
|
+
<div style={{ position: 'relative' }}>
|
|
462
|
+
<input
|
|
463
|
+
style={styles.input}
|
|
464
|
+
placeholder="🔍 Search APIs by method, path, or description..."
|
|
465
|
+
value={apiSearchQuery}
|
|
466
|
+
onChange={(e) => {
|
|
467
|
+
setApiSearchQuery(e.target.value);
|
|
468
|
+
setIsApiDropdownOpen(true);
|
|
469
|
+
}}
|
|
470
|
+
onFocus={() => setIsApiDropdownOpen(true)}
|
|
471
|
+
/>
|
|
472
|
+
{apiSearchQuery && (
|
|
473
|
+
<button
|
|
474
|
+
style={{ position: 'absolute', right: 12, top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', color: '#9ca3af', cursor: 'pointer', fontSize: 16, padding: 4 }}
|
|
475
|
+
onClick={() => { setApiSearchQuery(''); setIsApiDropdownOpen(true); }}
|
|
476
|
+
title="Clear search"
|
|
477
|
+
>
|
|
478
|
+
✕
|
|
479
|
+
</button>
|
|
480
|
+
)}
|
|
481
|
+
</div>
|
|
482
|
+
{currentBinding.api && (
|
|
483
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 8, padding: '8px 12px', backgroundColor: '#6366f1', borderRadius: 8 }}>
|
|
484
|
+
<span style={{ fontWeight: 600, fontSize: 12 }}>{currentBinding.api.method}</span>
|
|
485
|
+
<span style={{ fontSize: 14 }}>{currentBinding.api.path}</span>
|
|
486
|
+
<button
|
|
487
|
+
style={{ marginLeft: 'auto', background: 'none', border: 'none', color: 'white', cursor: 'pointer', fontSize: 14, padding: 2 }}
|
|
488
|
+
onClick={() => setCurrentBinding(prev => ({ ...prev, api: undefined }))}
|
|
489
|
+
title="Remove selection"
|
|
490
|
+
>
|
|
491
|
+
✕
|
|
492
|
+
</button>
|
|
493
|
+
</div>
|
|
494
|
+
)}
|
|
495
|
+
{isApiDropdownOpen && (
|
|
496
|
+
<div style={{ position: 'absolute', top: '100%', left: 0, right: 0, maxHeight: 240, overflow: 'auto', backgroundColor: '#2d3748', borderRadius: 8, marginTop: 4, boxShadow: '0 4px 12px rgba(0,0,0,0.3)', zIndex: 10 }}>
|
|
497
|
+
{filteredApis.length === 0 ? (
|
|
498
|
+
<div style={{ padding: 16, textAlign: 'center', color: '#9ca3af' }}>
|
|
499
|
+
{apiSearchQuery ? `No APIs matching "${apiSearchQuery}"` : 'No APIs available'}
|
|
500
|
+
</div>
|
|
501
|
+
) : (
|
|
502
|
+
filteredApis.map(api => (
|
|
503
|
+
<div
|
|
504
|
+
key={api.id}
|
|
505
|
+
style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 12px', cursor: 'pointer', borderBottom: '1px solid #374151', backgroundColor: currentBinding.api?.id === api.id ? '#4b5563' : 'transparent' }}
|
|
506
|
+
onClick={() => {
|
|
507
|
+
setCurrentBinding(prev => ({ ...prev, api }));
|
|
508
|
+
setIsApiDropdownOpen(false);
|
|
509
|
+
setApiSearchQuery('');
|
|
510
|
+
}}
|
|
511
|
+
onMouseEnter={(e) => { (e.currentTarget as HTMLElement).style.backgroundColor = '#4b5563'; }}
|
|
512
|
+
onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.backgroundColor = currentBinding.api?.id === api.id ? '#4b5563' : 'transparent'; }}
|
|
513
|
+
>
|
|
514
|
+
<span style={{
|
|
515
|
+
padding: '2px 8px',
|
|
516
|
+
borderRadius: 4,
|
|
517
|
+
fontSize: 11,
|
|
518
|
+
fontWeight: 700,
|
|
519
|
+
backgroundColor: api.method === 'GET' ? '#10b981' : api.method === 'POST' ? '#3b82f6' : api.method === 'PUT' ? '#f59e0b' : api.method === 'DELETE' ? '#ef4444' : '#8b5cf6',
|
|
520
|
+
color: 'white',
|
|
521
|
+
minWidth: 50,
|
|
522
|
+
textAlign: 'center'
|
|
523
|
+
}}>
|
|
524
|
+
{api.method}
|
|
525
|
+
</span>
|
|
526
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
527
|
+
<div style={{ fontSize: 14, fontFamily: 'monospace', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{api.path}</div>
|
|
528
|
+
{api.description && <div style={{ fontSize: 12, color: '#9ca3af', marginTop: 2, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{api.description}</div>}
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
531
|
+
))
|
|
532
|
+
)}
|
|
533
|
+
</div>
|
|
534
|
+
)}
|
|
535
|
+
</div>
|
|
536
|
+
|
|
537
|
+
{/* Trigger */}
|
|
538
|
+
<div style={{ marginBottom: 24 }}>
|
|
539
|
+
<div style={styles.sectionTitle}>Trigger</div>
|
|
540
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 8 }}>
|
|
541
|
+
{TRIGGER_TYPES.map(trigger => (
|
|
542
|
+
<div key={trigger.id} onClick={() => setCurrentBinding(prev => ({ ...prev, trigger }))} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '12px 8px', backgroundColor: '#374151', borderRadius: 8, cursor: 'pointer', border: currentBinding.trigger?.id === trigger.id ? '2px solid #6366f1' : '2px solid transparent' }} title={trigger.description}>
|
|
543
|
+
<span style={{ fontSize: 20, marginBottom: 4 }}>{trigger.icon}</span>
|
|
544
|
+
<span style={{ fontSize: 11, fontWeight: 500, textAlign: 'center' }}>{trigger.label}</span>
|
|
545
|
+
</div>
|
|
546
|
+
))}
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
|
|
550
|
+
{/* Success Handler */}
|
|
551
|
+
<div style={{ marginBottom: 24 }}>
|
|
552
|
+
<div style={styles.sectionTitle}>On Success</div>
|
|
553
|
+
<select style={{ ...styles.select, marginBottom: 12 }} value={currentBinding.successHandler?.type.id || 'toast'} onChange={(e) => { const type = HANDLER_TYPES.find(h => h.id === e.target.value)!; setCurrentBinding(prev => ({ ...prev, successHandler: { ...prev.successHandler!, type } })); }}>
|
|
554
|
+
{HANDLER_TYPES.map(h => <option key={h.id} value={h.id}>{h.label}</option>)}
|
|
555
|
+
</select>
|
|
556
|
+
{currentBinding.successHandler?.type.id !== 'none' && <input style={styles.input} placeholder="Success message" value={currentBinding.successHandler?.message || ''} onChange={(e) => setCurrentBinding(prev => ({ ...prev, successHandler: { ...prev.successHandler!, message: e.target.value } }))} />}
|
|
557
|
+
{currentBinding.successHandler?.type.id === 'redirect' && <input style={{ ...styles.input, marginTop: 8 }} placeholder="Redirect path (e.g., /dashboard)" value={currentBinding.successHandler?.redirectPath || ''} onChange={(e) => setCurrentBinding(prev => ({ ...prev, successHandler: { ...prev.successHandler!, redirectPath: e.target.value } }))} />}
|
|
558
|
+
</div>
|
|
559
|
+
|
|
560
|
+
{/* Error Handlers */}
|
|
561
|
+
<div style={{ marginBottom: 24 }}>
|
|
562
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
|
563
|
+
<div style={styles.sectionTitle}>On Error</div>
|
|
564
|
+
<button style={{ ...styles.button, padding: '4px 12px', backgroundColor: '#4b5563', fontSize: 12 }} onClick={addErrorHandler}>+ Add Case</button>
|
|
565
|
+
</div>
|
|
566
|
+
{currentBinding.errorHandlers?.map((handler, idx) => (
|
|
567
|
+
<div key={handler.id} style={{ backgroundColor: '#2d3748', borderRadius: 8, padding: 12, marginBottom: 8 }}>
|
|
568
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
|
569
|
+
<span style={{ fontSize: 12, color: '#9ca3af' }}>Case #{idx + 1}</span>
|
|
570
|
+
{(currentBinding.errorHandlers?.length || 0) > 1 && <button style={{ background: 'none', border: 'none', color: '#ef4444', cursor: 'pointer', fontSize: 16, padding: 0 }} onClick={() => removeErrorHandler(handler.id)}>✕</button>}
|
|
571
|
+
</div>
|
|
572
|
+
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
|
|
573
|
+
<select style={{ ...styles.select, flex: 2 }} value={handler.type.id} onChange={(e) => updateErrorHandler(handler.id, 'type', HANDLER_TYPES.find(h => h.id === e.target.value))}>
|
|
574
|
+
{HANDLER_TYPES.map(h => <option key={h.id} value={h.id}>{h.label}</option>)}
|
|
575
|
+
</select>
|
|
576
|
+
<input style={{ ...styles.input, flex: 1 }} placeholder="Error code" value={handler.errorCode || ''} onChange={(e) => updateErrorHandler(handler.id, 'errorCode', e.target.value)} title="Optional: Business error code" />
|
|
577
|
+
</div>
|
|
578
|
+
{handler.type.id !== 'none' && <input style={styles.input} placeholder="Error message" value={handler.message || ''} onChange={(e) => updateErrorHandler(handler.id, 'message', e.target.value)} />}
|
|
579
|
+
{handler.type.id === 'redirect' && <input style={{ ...styles.input, marginTop: 8 }} placeholder="Redirect path" value={handler.redirectPath || ''} onChange={(e) => updateErrorHandler(handler.id, 'redirectPath', e.target.value)} />}
|
|
580
|
+
</div>
|
|
581
|
+
))}
|
|
582
|
+
</div>
|
|
583
|
+
|
|
584
|
+
{/* Actions */}
|
|
585
|
+
<div style={{ display: 'flex', gap: 12 }}>
|
|
586
|
+
<button style={{ ...styles.button, backgroundColor: '#6366f1', color: 'white', flex: 1, opacity: currentBinding.api ? 1 : 0.5 }} disabled={!currentBinding.api} onClick={completeBinding}>Add Binding</button>
|
|
587
|
+
<button style={{ ...styles.button, backgroundColor: '#374151', color: 'white' }} onClick={() => setPanelStep('mode')}>Back</button>
|
|
588
|
+
<button style={{ ...styles.button, backgroundColor: '#374151', color: 'white' }} onClick={() => resetPanel(false)}>Cancel</button>
|
|
589
|
+
</div>
|
|
590
|
+
</>
|
|
591
|
+
)}
|
|
592
|
+
</div>
|
|
593
|
+
</div>
|
|
594
|
+
)}
|
|
595
|
+
</div>
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
export default PromptInspector;
|