@toolr/ui-design 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 +63 -0
- package/components/content/info-panel-primitives.tsx +297 -0
- package/components/diagrams/diagram-utils.tsx +908 -0
- package/components/hooks/use-click-outside.ts +27 -0
- package/components/hooks/use-dropdown-max-height.ts +20 -0
- package/components/hooks/use-navigation-history.ts +94 -0
- package/components/lib/ai-tools.tsx +44 -0
- package/components/lib/cn.ts +6 -0
- package/components/lib/form-colors.ts +32 -0
- package/components/lib/theme-engine.ts +97 -0
- package/components/lib/toolr-brand.tsx +31 -0
- package/components/sections/ai-tools-paths/index.ts +37 -0
- package/components/sections/ai-tools-paths/tools-paths-panel.tsx +212 -0
- package/components/sections/ai-tools-paths/types.ts +111 -0
- package/components/sections/ai-tools-paths/use-tools-paths.ts +159 -0
- package/components/sections/captured-issues/captured-issues-panel.tsx +214 -0
- package/components/sections/captured-issues/index.ts +38 -0
- package/components/sections/captured-issues/types.ts +113 -0
- package/components/sections/captured-issues/use-captured-issues.ts +111 -0
- package/components/sections/golden-snapshots/file-diff-viewer.tsx +420 -0
- package/components/sections/golden-snapshots/golden-sync-panel.tsx +223 -0
- package/components/sections/golden-snapshots/index.ts +145 -0
- package/components/sections/golden-snapshots/snapshot-manager.tsx +200 -0
- package/components/sections/golden-snapshots/status-overview.tsx +305 -0
- package/components/sections/golden-snapshots/types.ts +288 -0
- package/components/sections/golden-snapshots/use-golden-sync.ts +477 -0
- package/components/sections/golden-snapshots/version-manager.tsx +186 -0
- package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +210 -0
- package/components/sections/prompt-editor/index.ts +121 -0
- package/components/sections/prompt-editor/simulator-prompt-editor.tsx +276 -0
- package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +514 -0
- package/components/sections/prompt-editor/types.ts +101 -0
- package/components/sections/prompt-editor/use-prompt-editor.ts +131 -0
- package/components/sections/report-bug/error-logger.ts +392 -0
- package/components/sections/report-bug/index.ts +59 -0
- package/components/sections/report-bug/issue-reporter-api.ts +83 -0
- package/components/sections/report-bug/report-bug-form.tsx +282 -0
- package/components/sections/report-bug/screenshot-uploader.tsx +228 -0
- package/components/sections/report-bug/use-report-bug.ts +170 -0
- package/components/sections/snapshot-browser/index.ts +53 -0
- package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +147 -0
- package/components/sections/snapshot-browser/snapshot-tree.tsx +451 -0
- package/components/sections/snapshot-browser/types.ts +106 -0
- package/components/sections/snapshot-browser/use-snapshot-browser.ts +125 -0
- package/components/sections/snippets-editor/index.ts +31 -0
- package/components/sections/snippets-editor/snippets-editor.tsx +381 -0
- package/components/sections/snippets-editor/types.ts +48 -0
- package/components/sections/snippets-editor/use-snippets-editor.ts +217 -0
- package/components/ui/action-dialog.tsx +309 -0
- package/components/ui/ai-action-button.tsx +137 -0
- package/components/ui/ai-execution-action-buttons.tsx +106 -0
- package/components/ui/badge.tsx +67 -0
- package/components/ui/bottom-panel-header.tsx +240 -0
- package/components/ui/breadcrumb.tsx +168 -0
- package/components/ui/checkbox.tsx +102 -0
- package/components/ui/collapsible-section.tsx +100 -0
- package/components/ui/confirm-badge.tsx +71 -0
- package/components/ui/detail-section.tsx +67 -0
- package/components/ui/detail-view-wrapper.tsx +55 -0
- package/components/ui/editor-placeholder-card.tsx +197 -0
- package/components/ui/editor-toolbar.tsx +123 -0
- package/components/ui/execution-details-panel.tsx +93 -0
- package/components/ui/extension-list-card.tsx +105 -0
- package/components/ui/file-structure-section.tsx +373 -0
- package/components/ui/file-tree.tsx +171 -0
- package/components/ui/files-panel.tsx +251 -0
- package/components/ui/filter-dropdown.tsx +173 -0
- package/components/ui/form-actions.tsx +127 -0
- package/components/ui/frontmatter-form-header.tsx +80 -0
- package/components/ui/icon-button.tsx +388 -0
- package/components/ui/input.tsx +211 -0
- package/components/ui/label.tsx +159 -0
- package/components/ui/layout-tab-bar.tsx +289 -0
- package/components/ui/modal.tsx +194 -0
- package/components/ui/nav-card.tsx +81 -0
- package/components/ui/navigation-bar.tsx +285 -0
- package/components/ui/number-input.tsx +165 -0
- package/components/ui/registry-browser.tsx +261 -0
- package/components/ui/registry-card.tsx +710 -0
- package/components/ui/registry-detail.tsx +224 -0
- package/components/ui/resizable-textarea.tsx +290 -0
- package/components/ui/scope-badge.tsx +67 -0
- package/components/ui/segmented-toggle.tsx +133 -0
- package/components/ui/select.tsx +172 -0
- package/components/ui/selection-grid.tsx +313 -0
- package/components/ui/setting-row.tsx +97 -0
- package/components/ui/snapshot-card.tsx +107 -0
- package/components/ui/snippets-panel.tsx +161 -0
- package/components/ui/sort-dropdown.tsx +109 -0
- package/components/ui/status-card.tsx +96 -0
- package/components/ui/tab-bar.tsx +340 -0
- package/components/ui/toggle.tsx +142 -0
- package/components/ui/tooltip.tsx +326 -0
- package/dist/content.d.ts +110 -0
- package/dist/content.js +195 -0
- package/dist/diagrams.d.ts +371 -0
- package/dist/diagrams.js +702 -0
- package/dist/index.d.ts +2714 -0
- package/dist/index.js +11220 -0
- package/dist/preset.d.ts +24 -0
- package/dist/preset.js +17 -0
- package/dist/tokens/tokens/primitives.css +45 -0
- package/dist/tokens/tokens/semantic.css +46 -0
- package/dist/tokens/tokens/theme.css +11 -0
- package/dist/tokens/tokens/tokens.json +65 -0
- package/index.ts +123 -0
- package/package.json +63 -0
- package/tailwind-preset.ts +22 -0
- package/tokens/primitives.css +45 -0
- package/tokens/semantic.css +46 -0
- package/tokens/theme.css +11 -0
- package/tokens/tokens.json +65 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TabbedPromptEditor — Core Monaco editor with AI tool tabs
|
|
3
|
+
*
|
|
4
|
+
* Part of: Sections > Prompt Editor
|
|
5
|
+
*
|
|
6
|
+
* This is the base editor component. It provides:
|
|
7
|
+
* - AI tool tabs along the top (icon + name, active tab highlighted)
|
|
8
|
+
* - Monaco editor (markdown) with {{VARIABLE}} highlighting and autocomplete
|
|
9
|
+
* - Variables sidebar on the right (resizable, searchable, click-to-copy)
|
|
10
|
+
* - Editor toolbar with save/reset/dirty indicator
|
|
11
|
+
* - Cmd/Ctrl+S to save, validation for checklist section
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* <TabbedPromptEditor
|
|
15
|
+
* prompts={{ claude: '...', gemini: '...' }}
|
|
16
|
+
* onPromptChange={(tool, value) => save(tool, value)}
|
|
17
|
+
* tools={[{ id: 'claude', name: 'Claude Code', icon: <AiToolIcon tool="claude" /> }]}
|
|
18
|
+
* variables={[{ name: 'FILE_PATH', description: 'Path to the file' }]}
|
|
19
|
+
* />
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { useState, useRef, useCallback, useEffect, useMemo } from 'react'
|
|
23
|
+
import Editor, { type Monaco } from '@monaco-editor/react'
|
|
24
|
+
import type { editor, languages } from 'monaco-editor'
|
|
25
|
+
import { Variable, Info, Search, X, AlertTriangle } from 'lucide-react'
|
|
26
|
+
import { IconButton } from '../../ui/icon-button.tsx'
|
|
27
|
+
import { Input } from '../../ui/input.tsx'
|
|
28
|
+
import { EditorToolbar } from '../../ui/editor-toolbar.tsx'
|
|
29
|
+
import { EditorPlaceholderCard } from '../../ui/editor-placeholder-card.tsx'
|
|
30
|
+
import type { ToolTab, PromptPlaceholder } from './types.ts'
|
|
31
|
+
import { AiToolIcon } from '../../lib/ai-tools.tsx'
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Constants
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
const THEME_NAME = 'prompt-editor-dark'
|
|
38
|
+
let themeRegistered = false
|
|
39
|
+
|
|
40
|
+
const MIN_SIDEBAR_WIDTH = 220
|
|
41
|
+
const MAX_SIDEBAR_WIDTH = 400
|
|
42
|
+
const DEFAULT_SIDEBAR_WIDTH = 280
|
|
43
|
+
const DEFAULT_EDITOR_HEIGHT = 400
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Props
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
export interface TabbedPromptEditorProps {
|
|
50
|
+
/** Prompt content keyed by tool id */
|
|
51
|
+
prompts: Record<string, string>
|
|
52
|
+
/** Called when a prompt is saved */
|
|
53
|
+
onPromptChange: (tool: string, value: string) => void
|
|
54
|
+
/** Tool tabs to display */
|
|
55
|
+
tools: ToolTab[]
|
|
56
|
+
/** Default/reset prompts keyed by tool id */
|
|
57
|
+
defaultPrompts?: Record<string, string>
|
|
58
|
+
/** Available template variables */
|
|
59
|
+
variables?: PromptPlaceholder[]
|
|
60
|
+
/** Called when reset is triggered */
|
|
61
|
+
onReset?: (tool: string) => void
|
|
62
|
+
/** Called when save is triggered */
|
|
63
|
+
onSave?: (tool: string, content: string) => void
|
|
64
|
+
/** When true, validates that prompt contains "## Verification Checklist" */
|
|
65
|
+
validateChecklist?: boolean
|
|
66
|
+
/** When true, adds border and rounding for standalone usage */
|
|
67
|
+
standalone?: boolean
|
|
68
|
+
className?: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Component
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
export function TabbedPromptEditor({
|
|
76
|
+
prompts,
|
|
77
|
+
onPromptChange,
|
|
78
|
+
tools,
|
|
79
|
+
defaultPrompts,
|
|
80
|
+
variables,
|
|
81
|
+
onReset,
|
|
82
|
+
onSave,
|
|
83
|
+
validateChecklist = false,
|
|
84
|
+
standalone = false,
|
|
85
|
+
className = '',
|
|
86
|
+
}: TabbedPromptEditorProps) {
|
|
87
|
+
const [activeTab, setActiveTab] = useState(tools[0]?.id ?? '')
|
|
88
|
+
const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_SIDEBAR_WIDTH)
|
|
89
|
+
const [variableSearch, setVariableSearch] = useState('')
|
|
90
|
+
const [localContent, setLocalContent] = useState<Record<string, string>>(prompts)
|
|
91
|
+
const [isDirty, setIsDirty] = useState(false)
|
|
92
|
+
|
|
93
|
+
const isDraggingSidebarRef = useRef(false)
|
|
94
|
+
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
|
|
95
|
+
const monacoRef = useRef<Monaco | null>(null)
|
|
96
|
+
const decorationsRef = useRef<string[]>([])
|
|
97
|
+
const completionProviderRef = useRef<{ dispose: () => void } | null>(null)
|
|
98
|
+
|
|
99
|
+
// Sync local content when prompts change externally
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
setLocalContent(prompts)
|
|
102
|
+
setIsDirty(false)
|
|
103
|
+
}, [prompts])
|
|
104
|
+
|
|
105
|
+
// --- Decorations: highlight {{VARIABLE}} patterns ---
|
|
106
|
+
|
|
107
|
+
const updateDecorations = useCallback(() => {
|
|
108
|
+
if (!editorRef.current || !monacoRef.current) return
|
|
109
|
+
const model = editorRef.current.getModel()
|
|
110
|
+
if (!model) return
|
|
111
|
+
|
|
112
|
+
const content = model.getValue()
|
|
113
|
+
const matches: RegExpExecArray[] = Array.from(content.matchAll(/\{\{(\w+)\}\}/g) as IterableIterator<RegExpExecArray>)
|
|
114
|
+
const newDecorations: editor.IModelDeltaDecoration[] = []
|
|
115
|
+
|
|
116
|
+
for (const match of matches) {
|
|
117
|
+
if (match.index === undefined) continue
|
|
118
|
+
const startPos = model.getPositionAt(match.index)
|
|
119
|
+
const endPos = model.getPositionAt(match.index + match[0].length)
|
|
120
|
+
|
|
121
|
+
const varName = match[1] as string
|
|
122
|
+
const varInfo = variables?.find((v) => v.name === varName)
|
|
123
|
+
const hoverContent = varInfo
|
|
124
|
+
? `**{{${varName}}}**\n\n${varInfo.description}${varInfo.example ? `\n\n*Value: ${varInfo.example}*` : ''}`
|
|
125
|
+
: `**Template Variable**: ${varName}`
|
|
126
|
+
|
|
127
|
+
newDecorations.push({
|
|
128
|
+
range: new monacoRef.current.Range(
|
|
129
|
+
startPos.lineNumber, startPos.column,
|
|
130
|
+
endPos.lineNumber, endPos.column,
|
|
131
|
+
),
|
|
132
|
+
options: {
|
|
133
|
+
inlineClassName: 'template-variable-highlight',
|
|
134
|
+
hoverMessage: { value: hoverContent },
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
decorationsRef.current = editorRef.current.deltaDecorations(
|
|
140
|
+
decorationsRef.current,
|
|
141
|
+
newDecorations,
|
|
142
|
+
)
|
|
143
|
+
}, [variables])
|
|
144
|
+
|
|
145
|
+
// --- Completion provider: {{ trigger ---
|
|
146
|
+
|
|
147
|
+
const registerCompletionProvider = useCallback((monaco: Monaco) => {
|
|
148
|
+
if (completionProviderRef.current) {
|
|
149
|
+
completionProviderRef.current.dispose()
|
|
150
|
+
}
|
|
151
|
+
if (!variables || variables.length === 0) return
|
|
152
|
+
|
|
153
|
+
completionProviderRef.current = monaco.languages.registerCompletionItemProvider('markdown', {
|
|
154
|
+
triggerCharacters: ['{'],
|
|
155
|
+
provideCompletionItems: (model: editor.ITextModel, position: { lineNumber: number; column: number }) => {
|
|
156
|
+
const textUntilPosition = model.getValueInRange({
|
|
157
|
+
startLineNumber: position.lineNumber,
|
|
158
|
+
startColumn: Math.max(1, position.column - 2),
|
|
159
|
+
endLineNumber: position.lineNumber,
|
|
160
|
+
endColumn: position.column,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
if (!textUntilPosition.endsWith('{{')) {
|
|
164
|
+
return { suggestions: [] }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const lineContent = model.getLineContent(position.lineNumber)
|
|
168
|
+
const textAfterCursor = lineContent.substring(position.column - 1)
|
|
169
|
+
const closingBracesMatch = textAfterCursor.match(/^(\}+)/)
|
|
170
|
+
const existingClosingBraces = closingBracesMatch ? closingBracesMatch[1].length : 0
|
|
171
|
+
|
|
172
|
+
const suggestions: languages.CompletionItem[] = variables.map((v) => ({
|
|
173
|
+
label: v.name,
|
|
174
|
+
kind: monaco.languages.CompletionItemKind.Variable,
|
|
175
|
+
detail: v.required ? '(required)' : undefined,
|
|
176
|
+
documentation: {
|
|
177
|
+
value: `${v.description}${v.example ? `\n\n*Value: ${v.example}*` : ''}`,
|
|
178
|
+
},
|
|
179
|
+
insertText: `${v.name}}}`,
|
|
180
|
+
range: {
|
|
181
|
+
startLineNumber: position.lineNumber,
|
|
182
|
+
startColumn: position.column,
|
|
183
|
+
endLineNumber: position.lineNumber,
|
|
184
|
+
endColumn: position.column + Math.min(existingClosingBraces, 2),
|
|
185
|
+
},
|
|
186
|
+
}))
|
|
187
|
+
|
|
188
|
+
return { suggestions }
|
|
189
|
+
},
|
|
190
|
+
})
|
|
191
|
+
}, [variables])
|
|
192
|
+
|
|
193
|
+
// --- Editor mount ---
|
|
194
|
+
|
|
195
|
+
const handleEditorDidMount = useCallback(
|
|
196
|
+
(editorInstance: editor.IStandaloneCodeEditor, monaco: Monaco) => {
|
|
197
|
+
editorRef.current = editorInstance
|
|
198
|
+
monacoRef.current = monaco
|
|
199
|
+
|
|
200
|
+
// Inject CSS for variable highlighting
|
|
201
|
+
const styleId = 'template-variable-styles'
|
|
202
|
+
if (!document.getElementById(styleId)) {
|
|
203
|
+
const style = document.createElement('style')
|
|
204
|
+
style.id = styleId
|
|
205
|
+
style.textContent = `
|
|
206
|
+
.template-variable-highlight {
|
|
207
|
+
background-color: rgba(180, 190, 254, 0.2);
|
|
208
|
+
border: 1px solid rgba(180, 190, 254, 0.4);
|
|
209
|
+
border-radius: 3px;
|
|
210
|
+
color: #b4befe !important;
|
|
211
|
+
font-weight: 500;
|
|
212
|
+
}
|
|
213
|
+
`
|
|
214
|
+
document.head.appendChild(style)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
registerCompletionProvider(monaco)
|
|
218
|
+
updateDecorations()
|
|
219
|
+
|
|
220
|
+
editorInstance.onDidChangeModelContent(() => {
|
|
221
|
+
updateDecorations()
|
|
222
|
+
})
|
|
223
|
+
},
|
|
224
|
+
[updateDecorations, registerCompletionProvider],
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
// Re-register completion provider when variables change
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
if (monacoRef.current) {
|
|
230
|
+
registerCompletionProvider(monacoRef.current)
|
|
231
|
+
}
|
|
232
|
+
return () => {
|
|
233
|
+
if (completionProviderRef.current) {
|
|
234
|
+
completionProviderRef.current.dispose()
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}, [variables, registerCompletionProvider])
|
|
238
|
+
|
|
239
|
+
// --- Sidebar resize ---
|
|
240
|
+
|
|
241
|
+
const handleSidebarMouseDown = useCallback((e: React.PointerEvent) => {
|
|
242
|
+
e.preventDefault()
|
|
243
|
+
e.stopPropagation()
|
|
244
|
+
isDraggingSidebarRef.current = true
|
|
245
|
+
const target = e.currentTarget as HTMLElement
|
|
246
|
+
target.setPointerCapture(e.pointerId)
|
|
247
|
+
|
|
248
|
+
const startX = e.clientX
|
|
249
|
+
const startWidth = sidebarWidth
|
|
250
|
+
|
|
251
|
+
const handlePointerMove = (moveEvent: PointerEvent) => {
|
|
252
|
+
if (!isDraggingSidebarRef.current) return
|
|
253
|
+
const deltaX = startX - moveEvent.clientX
|
|
254
|
+
const newWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, startWidth + deltaX))
|
|
255
|
+
setSidebarWidth(newWidth)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const handlePointerUp = () => {
|
|
259
|
+
isDraggingSidebarRef.current = false
|
|
260
|
+
target.removeEventListener('pointermove', handlePointerMove)
|
|
261
|
+
target.removeEventListener('pointerup', handlePointerUp)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
target.addEventListener('pointermove', handlePointerMove)
|
|
265
|
+
target.addEventListener('pointerup', handlePointerUp)
|
|
266
|
+
}, [sidebarWidth])
|
|
267
|
+
|
|
268
|
+
// --- Content change ---
|
|
269
|
+
|
|
270
|
+
const handleEditorChange = useCallback(
|
|
271
|
+
(value: string | undefined) => {
|
|
272
|
+
const newValue = value ?? ''
|
|
273
|
+
setLocalContent((prev: Record<string, string>) => {
|
|
274
|
+
const updated: Record<string, string> = { ...prev, [activeTab]: newValue }
|
|
275
|
+
const hasDiff = Object.keys(prompts).some(
|
|
276
|
+
(tool) => updated[tool] !== prompts[tool],
|
|
277
|
+
)
|
|
278
|
+
setIsDirty(hasDiff)
|
|
279
|
+
return updated
|
|
280
|
+
})
|
|
281
|
+
},
|
|
282
|
+
[activeTab, prompts],
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
// --- Save ---
|
|
286
|
+
|
|
287
|
+
const handleSave = useCallback(() => {
|
|
288
|
+
for (const tool of Object.keys(prompts)) {
|
|
289
|
+
if (localContent[tool] !== prompts[tool]) {
|
|
290
|
+
onPromptChange(tool, localContent[tool])
|
|
291
|
+
onSave?.(tool, localContent[tool])
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
setIsDirty(false)
|
|
295
|
+
}, [localContent, prompts, onPromptChange, onSave])
|
|
296
|
+
|
|
297
|
+
// Keyboard shortcut: Cmd/Ctrl + S
|
|
298
|
+
useEffect(() => {
|
|
299
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
300
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
|
|
301
|
+
e.preventDefault()
|
|
302
|
+
if (isDirty) handleSave()
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
window.addEventListener('keydown', handleKeyDown)
|
|
306
|
+
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
307
|
+
}, [isDirty, handleSave])
|
|
308
|
+
|
|
309
|
+
// --- Reset ---
|
|
310
|
+
|
|
311
|
+
const handleReset = useCallback(() => {
|
|
312
|
+
onReset?.(activeTab)
|
|
313
|
+
}, [activeTab, onReset])
|
|
314
|
+
|
|
315
|
+
// --- Derived state ---
|
|
316
|
+
|
|
317
|
+
const currentPrompt = localContent[activeTab] ?? ''
|
|
318
|
+
const canReset = !!onReset && !!defaultPrompts && currentPrompt !== defaultPrompts[activeTab]
|
|
319
|
+
const hasVariables = variables && variables.length > 0
|
|
320
|
+
|
|
321
|
+
const checklistMissing = useMemo(() => {
|
|
322
|
+
if (!validateChecklist) return false
|
|
323
|
+
if (!currentPrompt || currentPrompt.trim().length === 0) return true
|
|
324
|
+
return !/#+\s*Verification Checklist/i.test(currentPrompt)
|
|
325
|
+
}, [validateChecklist, currentPrompt])
|
|
326
|
+
|
|
327
|
+
const filteredVariables = useMemo((): PromptPlaceholder[] => {
|
|
328
|
+
if (!variables) return []
|
|
329
|
+
if (!variableSearch.trim()) return variables
|
|
330
|
+
const search = variableSearch.toLowerCase()
|
|
331
|
+
return variables.filter(
|
|
332
|
+
(v) =>
|
|
333
|
+
v.name.toLowerCase().includes(search) ||
|
|
334
|
+
v.description.toLowerCase().includes(search) ||
|
|
335
|
+
(v.example && v.example.toLowerCase().includes(search)),
|
|
336
|
+
)
|
|
337
|
+
}, [variables, variableSearch])
|
|
338
|
+
|
|
339
|
+
return (
|
|
340
|
+
<div className={`flex w-full h-full bg-[#181825] overflow-hidden ${standalone ? 'border border-[#313244] rounded-lg' : ''} ${className}`}>
|
|
341
|
+
{/* Main content area */}
|
|
342
|
+
<div className="flex-1 min-w-0 w-0 flex flex-col overflow-hidden">
|
|
343
|
+
{/* Tool Tabs */}
|
|
344
|
+
<div className="relative flex h-[26px] bg-[#11111b] shrink-0">
|
|
345
|
+
{tools.map((tool) => {
|
|
346
|
+
const isActive = activeTab === tool.id
|
|
347
|
+
const activeColor = tool.activeColor ?? 'text-[#89b4fa]'
|
|
348
|
+
|
|
349
|
+
return (
|
|
350
|
+
<button
|
|
351
|
+
key={tool.id}
|
|
352
|
+
onClick={() => setActiveTab(tool.id)}
|
|
353
|
+
className={`flex-1 flex items-center justify-center gap-1.5 px-2 text-xs font-medium border-b-2 transition-colors ${
|
|
354
|
+
isActive
|
|
355
|
+
? `border-current ${activeColor} bg-[#1e1e2e]`
|
|
356
|
+
: 'border-[#313244] text-[#6c7086] hover:text-[#a6adc8] hover:bg-[#1e1e2e]/50'
|
|
357
|
+
}`}
|
|
358
|
+
title={tool.name}
|
|
359
|
+
>
|
|
360
|
+
{tool.icon ?? <AiToolIcon tool={tool.id} size={14} />}
|
|
361
|
+
<span className="hidden sm:inline">{tool.shortName ?? tool.name}</span>
|
|
362
|
+
</button>
|
|
363
|
+
)
|
|
364
|
+
})}
|
|
365
|
+
</div>
|
|
366
|
+
|
|
367
|
+
{/* Checklist validation warning */}
|
|
368
|
+
{checklistMissing && (
|
|
369
|
+
<div className="flex items-start gap-2 px-3 py-2 bg-[#f38ba8]/10 border-b border-[#f38ba8]/30 shrink-0">
|
|
370
|
+
<AlertTriangle className="w-4 h-4 text-[#f38ba8] shrink-0 mt-0.5" />
|
|
371
|
+
<div className="text-xs text-[#f38ba8]">
|
|
372
|
+
<span className="font-medium">Missing "# Verification Checklist" section.</span>
|
|
373
|
+
{' '}When verification is enabled, only content under this heading will be sent to the AI for output validation.
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
)}
|
|
377
|
+
|
|
378
|
+
{/* Editor Toolbar */}
|
|
379
|
+
<EditorToolbar
|
|
380
|
+
isDirty={isDirty}
|
|
381
|
+
onSave={handleSave}
|
|
382
|
+
canReset={canReset}
|
|
383
|
+
onReset={handleReset}
|
|
384
|
+
resetTooltip={{
|
|
385
|
+
title: 'Reset Prompt',
|
|
386
|
+
description: `Restore default prompt for ${tools.find((t) => t.id === activeTab)?.name ?? activeTab}.`,
|
|
387
|
+
}}
|
|
388
|
+
/>
|
|
389
|
+
|
|
390
|
+
{/* Monaco Editor */}
|
|
391
|
+
<div className="flex-1 min-h-0" style={{ minHeight: DEFAULT_EDITOR_HEIGHT }}>
|
|
392
|
+
<Editor
|
|
393
|
+
key={activeTab}
|
|
394
|
+
height="100%"
|
|
395
|
+
defaultLanguage="markdown"
|
|
396
|
+
theme={THEME_NAME}
|
|
397
|
+
value={currentPrompt}
|
|
398
|
+
onChange={handleEditorChange}
|
|
399
|
+
onMount={handleEditorDidMount}
|
|
400
|
+
beforeMount={(monaco: Monaco) => {
|
|
401
|
+
if (!themeRegistered) {
|
|
402
|
+
monaco.editor.defineTheme(THEME_NAME, {
|
|
403
|
+
base: 'vs-dark',
|
|
404
|
+
inherit: true,
|
|
405
|
+
rules: [],
|
|
406
|
+
colors: {
|
|
407
|
+
'editor.background': '#181825',
|
|
408
|
+
'editorGutter.background': '#181825',
|
|
409
|
+
'minimap.background': '#181825',
|
|
410
|
+
'scrollbar.shadow': '#00000000',
|
|
411
|
+
},
|
|
412
|
+
})
|
|
413
|
+
themeRegistered = true
|
|
414
|
+
}
|
|
415
|
+
}}
|
|
416
|
+
options={{
|
|
417
|
+
minimap: { enabled: false },
|
|
418
|
+
fontSize: 13,
|
|
419
|
+
lineNumbers: 'on',
|
|
420
|
+
wordWrap: 'on',
|
|
421
|
+
padding: { top: 8, bottom: 16 },
|
|
422
|
+
scrollBeyondLastLine: false,
|
|
423
|
+
automaticLayout: true,
|
|
424
|
+
quickSuggestions: { strings: true, other: true, comments: true },
|
|
425
|
+
}}
|
|
426
|
+
/>
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
{/* Variables Sidebar */}
|
|
431
|
+
{hasVariables && (
|
|
432
|
+
<div
|
|
433
|
+
className="relative shrink-0 bg-[#11111b] overflow-hidden flex flex-col border-l border-[#313244]"
|
|
434
|
+
style={{ width: sidebarWidth, minWidth: MIN_SIDEBAR_WIDTH, maxWidth: MAX_SIDEBAR_WIDTH }}
|
|
435
|
+
>
|
|
436
|
+
{/* Resize handle on left edge */}
|
|
437
|
+
<div
|
|
438
|
+
onPointerDown={handleSidebarMouseDown}
|
|
439
|
+
className="absolute left-0 top-0 bottom-0 w-1 cursor-col-resize hover:bg-[#b4befe]/30 transition-colors z-10"
|
|
440
|
+
/>
|
|
441
|
+
|
|
442
|
+
{/* Header */}
|
|
443
|
+
<div className="h-[52px] px-3 flex items-center justify-between border-b border-[#313244] shrink-0">
|
|
444
|
+
<div className="flex items-center gap-2">
|
|
445
|
+
<Variable className="w-4 h-4 text-neutral-500 shrink-0" />
|
|
446
|
+
<div className="min-w-0">
|
|
447
|
+
<div className="text-xs font-medium text-neutral-400">Placeholders</div>
|
|
448
|
+
<div className="text-xs text-neutral-600 truncate">Available placeholders</div>
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
<span className="px-1.5 py-0.5 bg-[#b4befe]/20 text-[#b4befe] rounded text-xs">
|
|
452
|
+
{variableSearch.trim() ? `${filteredVariables.length}/${variables!.length}` : variables!.length}
|
|
453
|
+
</span>
|
|
454
|
+
</div>
|
|
455
|
+
|
|
456
|
+
{/* Search */}
|
|
457
|
+
<div className="px-2.5 py-2 border-b border-[#313244] shrink-0">
|
|
458
|
+
<div className="relative">
|
|
459
|
+
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-[#6c7086]" />
|
|
460
|
+
<Input
|
|
461
|
+
value={variableSearch}
|
|
462
|
+
onChange={setVariableSearch}
|
|
463
|
+
placeholder="Search placeholders..."
|
|
464
|
+
size="sm"
|
|
465
|
+
className="pl-7 pr-7"
|
|
466
|
+
/>
|
|
467
|
+
{variableSearch && (
|
|
468
|
+
<IconButton
|
|
469
|
+
icon={<X className="w-3 h-3" />}
|
|
470
|
+
onClick={() => setVariableSearch('')}
|
|
471
|
+
color="neutral"
|
|
472
|
+
size="xss"
|
|
473
|
+
className="absolute right-2 top-1/2 -translate-y-1/2"
|
|
474
|
+
tooltip={{ title: 'Clear', description: 'Clear search' }}
|
|
475
|
+
/>
|
|
476
|
+
)}
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
{/* Autocomplete tip */}
|
|
481
|
+
<div className="px-3 py-2 border-b border-[#313244] shrink-0">
|
|
482
|
+
<div className="flex items-center gap-1.5 text-xs text-[#6c7086]">
|
|
483
|
+
<Info className="w-3 h-3 flex-shrink-0" />
|
|
484
|
+
<span>Type <code className="text-[#b4befe]">{'{{'}</code> in editor for autocomplete</span>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
|
|
488
|
+
{/* Variable list */}
|
|
489
|
+
<div className="flex-1 overflow-y-auto p-2.5 space-y-2">
|
|
490
|
+
{filteredVariables.length > 0 ? (
|
|
491
|
+
filteredVariables.map((variable: PromptPlaceholder) => (
|
|
492
|
+
<div key={variable.name} className="bg-[#1e1e2e] border border-[#313244] rounded-lg">
|
|
493
|
+
<EditorPlaceholderCard
|
|
494
|
+
name={variable.name}
|
|
495
|
+
description={variable.description}
|
|
496
|
+
value={variable.example}
|
|
497
|
+
required={variable.required}
|
|
498
|
+
valueLabel={variable.valueLabel}
|
|
499
|
+
accentColor="purple"
|
|
500
|
+
showCopyPlaceholder
|
|
501
|
+
/>
|
|
502
|
+
</div>
|
|
503
|
+
))
|
|
504
|
+
) : variableSearch.trim() ? (
|
|
505
|
+
<div className="text-center py-4 text-xs text-[#6c7086]">
|
|
506
|
+
No placeholders match "{variableSearch}"
|
|
507
|
+
</div>
|
|
508
|
+
) : null}
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
)}
|
|
512
|
+
</div>
|
|
513
|
+
)
|
|
514
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Editor — Shared type definitions
|
|
3
|
+
*
|
|
4
|
+
* Part of: Sections > Prompt Editor
|
|
5
|
+
*
|
|
6
|
+
* These types define the shape of prompt data and the API contract
|
|
7
|
+
* between the UI layer and whatever backend stores prompts.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ReactNode } from 'react'
|
|
11
|
+
|
|
12
|
+
// Re-export the AI tool key type from the shared lib
|
|
13
|
+
export type { AiToolKey } from '../../lib/ai-tools.tsx'
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Tool definition (generic — not tied to specific AI tools)
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/** A tool tab shown in the editor (e.g. Claude, Gemini, Copilot) */
|
|
20
|
+
export interface ToolTab {
|
|
21
|
+
id: string
|
|
22
|
+
name: string
|
|
23
|
+
icon?: ReactNode
|
|
24
|
+
/** Short name for narrow layouts */
|
|
25
|
+
shortName?: string
|
|
26
|
+
/** Tailwind text color class for the active state (e.g. "text-orange-400") */
|
|
27
|
+
activeColor?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Prompt template variables
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/** A template variable available for insertion into prompts */
|
|
35
|
+
export interface PromptPlaceholder {
|
|
36
|
+
name: string
|
|
37
|
+
description: string
|
|
38
|
+
example?: string
|
|
39
|
+
required?: boolean
|
|
40
|
+
/** Label for the value display (default: "Value:") */
|
|
41
|
+
valueLabel?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Prompt snapshots
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/** A saved snapshot of a prompt for version history */
|
|
49
|
+
export interface PromptSnapshot {
|
|
50
|
+
content: string
|
|
51
|
+
savedAt: string // ISO date string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// API adapter interface
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* PromptEditorApi — optional callback interface for persistence.
|
|
60
|
+
*
|
|
61
|
+
* When provided, the editor can save, reset, and manage snapshots.
|
|
62
|
+
* When omitted, the editor works in "controlled" mode where the
|
|
63
|
+
* parent manages all state via props.
|
|
64
|
+
*/
|
|
65
|
+
export interface PromptEditorApi {
|
|
66
|
+
/** Save the current prompt */
|
|
67
|
+
savePrompt: (prompt: string) => Promise<void>
|
|
68
|
+
/** Get the default/reset value */
|
|
69
|
+
getDefaultPrompt?: () => Promise<string>
|
|
70
|
+
/** Save a snapshot */
|
|
71
|
+
saveSnapshot?: (content: string) => Promise<void>
|
|
72
|
+
/** Get saved snapshots */
|
|
73
|
+
getSnapshots?: () => Promise<PromptSnapshot[]>
|
|
74
|
+
/** Delete a snapshot by timestamp */
|
|
75
|
+
deleteSnapshot?: (savedAt: string) => Promise<void>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// File type selector (for FileTypeTabbedPromptEditor)
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/** A file type option in the sidebar selector */
|
|
83
|
+
export interface FileTypeOption {
|
|
84
|
+
id: string
|
|
85
|
+
name: string
|
|
86
|
+
description?: string
|
|
87
|
+
icon?: ReactNode
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Scenario/step tree (for SimulatorPromptEditor)
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
/** A scenario with child steps for the tree sidebar */
|
|
95
|
+
export interface ScenarioOption {
|
|
96
|
+
id: string
|
|
97
|
+
name: string
|
|
98
|
+
description?: string
|
|
99
|
+
icon?: ReactNode
|
|
100
|
+
steps: Array<{ id: string; name: string }>
|
|
101
|
+
}
|