Skip to content

Commit 4c6069a

Browse files
GeneAIclaude
authored andcommitted
feat: Add drag-and-drop file upload to debug wizard
- Drag & drop zone for source files - Browse button with multi-file support - File list with size display and remove buttons - Auto-fills file path from first uploaded file - Respects tier limits (file count and size) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 9d8686f commit 4c6069a

File tree

1 file changed

+160
-2
lines changed

1 file changed

+160
-2
lines changed

website/components/debug-wizard/DebugWizard.tsx

Lines changed: 160 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,59 @@ export default function DebugWizard() {
135135
setState((prev) => ({ ...prev, [field]: value }));
136136
};
137137

138+
const handleFileUpload = async (files: File[]) => {
139+
const maxFiles = limits.maxFiles ?? 100;
140+
const maxSizeMB = limits.maxFileSizeMB ?? 1;
141+
const maxSizeBytes = maxSizeMB * 1024 * 1024;
142+
143+
// Check file count limit
144+
const totalFiles = state.files.length + files.length;
145+
if (totalFiles > maxFiles) {
146+
setError(`Maximum ${maxFiles} files allowed. You have ${state.files.length} and tried to add ${files.length}.`);
147+
return;
148+
}
149+
150+
const newFiles: FileInput[] = [];
151+
152+
for (const file of files) {
153+
// Check file size
154+
if (file.size > maxSizeBytes) {
155+
setError(`File "${file.name}" exceeds the ${maxSizeMB}MB limit.`);
156+
continue;
157+
}
158+
159+
// Read file content
160+
try {
161+
const content = await file.text();
162+
newFiles.push({
163+
path: file.name,
164+
content: content,
165+
size_bytes: file.size,
166+
});
167+
} catch (err) {
168+
console.error('Error reading file:', err);
169+
setError(`Could not read file "${file.name}".`);
170+
}
171+
}
172+
173+
if (newFiles.length > 0) {
174+
setState((prev) => ({
175+
...prev,
176+
files: [...prev.files, ...newFiles],
177+
// Auto-fill file path from first file if not set
178+
file_path: prev.file_path || newFiles[0].path,
179+
}));
180+
setError(null);
181+
}
182+
};
183+
184+
const removeFile = (index: number) => {
185+
setState((prev) => ({
186+
...prev,
187+
files: prev.files.filter((_, i) => i !== index),
188+
}));
189+
};
190+
138191
const handleAnalyze = async () => {
139192
setError(null);
140193

@@ -305,10 +358,115 @@ logger = structlog.get_logger()`,
305358
/>
306359
</div>
307360

308-
{/* File Path and Line Number */}
361+
{/* File Upload Section */}
362+
<div>
363+
<label className="block text-sm font-medium text-gray-700 mb-2">
364+
Upload Files (optional)
365+
<span className="text-gray-500 font-normal ml-2">
366+
Max {limits.maxFiles === null ? 'unlimited' : limits.maxFiles} file{limits.maxFiles !== 1 ? 's' : ''}, {limits.maxFileSizeMB ? `${limits.maxFileSizeMB}MB each` : 'no size limit'}
367+
</span>
368+
</label>
369+
<div
370+
className={`border-2 border-dashed rounded-lg p-6 text-center transition-colors ${
371+
state.files.length > 0 ? 'border-purple-400 bg-purple-50' : 'border-gray-300 hover:border-purple-400'
372+
}`}
373+
onDragOver={(e) => {
374+
e.preventDefault();
375+
e.currentTarget.classList.add('border-purple-500', 'bg-purple-50');
376+
}}
377+
onDragLeave={(e) => {
378+
e.currentTarget.classList.remove('border-purple-500', 'bg-purple-50');
379+
}}
380+
onDrop={(e) => {
381+
e.preventDefault();
382+
e.currentTarget.classList.remove('border-purple-500', 'bg-purple-50');
383+
const droppedFiles = Array.from(e.dataTransfer.files);
384+
handleFileUpload(droppedFiles);
385+
}}
386+
>
387+
{state.files.length === 0 ? (
388+
<>
389+
<div className="text-4xl mb-2">📁</div>
390+
<p className="text-gray-600 mb-2">Drag & drop source files here</p>
391+
<p className="text-sm text-gray-500 mb-3">or</p>
392+
<label className="px-4 py-2 bg-purple-600 text-white rounded-lg cursor-pointer hover:bg-purple-700 inline-block">
393+
Browse Files
394+
<input
395+
type="file"
396+
multiple
397+
accept=".ts,.tsx,.js,.jsx,.py,.java,.go,.rs,.rb,.php,.c,.cpp,.h,.hpp,.cs,.swift,.kt,.scala,.vue,.svelte,.json,.yaml,.yml,.md,.txt"
398+
className="hidden"
399+
onChange={(e) => {
400+
if (e.target.files) {
401+
handleFileUpload(Array.from(e.target.files));
402+
}
403+
}}
404+
/>
405+
</label>
406+
<p className="text-xs text-gray-400 mt-3">
407+
Supports: .ts, .tsx, .js, .jsx, .py, .java, .go, .rs, .rb, .php, .c, .cpp, .cs, .swift, .kt, .scala, .vue, .svelte, .json, .yaml
408+
</p>
409+
</>
410+
) : (
411+
<div className="text-left">
412+
<div className="flex items-center justify-between mb-3">
413+
<span className="font-medium text-purple-700">
414+
{state.files.length} file{state.files.length !== 1 ? 's' : ''} selected
415+
</span>
416+
<button
417+
type="button"
418+
onClick={() => setState(prev => ({ ...prev, files: [] }))}
419+
className="text-sm text-red-600 hover:text-red-800"
420+
>
421+
Clear all
422+
</button>
423+
</div>
424+
<div className="space-y-2 max-h-40 overflow-y-auto">
425+
{state.files.map((file, idx) => (
426+
<div key={idx} className="flex items-center justify-between bg-white rounded p-2 border border-purple-200">
427+
<div className="flex items-center gap-2 min-w-0">
428+
<span className="text-lg">📄</span>
429+
<span className="text-sm font-mono truncate">{file.path}</span>
430+
<span className="text-xs text-gray-400">
431+
({Math.round(file.content.length / 1024 * 10) / 10}KB)
432+
</span>
433+
</div>
434+
<button
435+
type="button"
436+
onClick={() => removeFile(idx)}
437+
className="text-red-500 hover:text-red-700 ml-2"
438+
>
439+
440+
</button>
441+
</div>
442+
))}
443+
</div>
444+
<label className="mt-3 text-sm text-purple-600 hover:text-purple-800 cursor-pointer inline-block">
445+
+ Add more files
446+
<input
447+
type="file"
448+
multiple
449+
accept=".ts,.tsx,.js,.jsx,.py,.java,.go,.rs,.rb,.php,.c,.cpp,.h,.hpp,.cs,.swift,.kt,.scala,.vue,.svelte,.json,.yaml,.yml,.md,.txt"
450+
className="hidden"
451+
onChange={(e) => {
452+
if (e.target.files) {
453+
handleFileUpload(Array.from(e.target.files));
454+
}
455+
}}
456+
/>
457+
</label>
458+
</div>
459+
)}
460+
</div>
461+
</div>
462+
463+
{/* File Path and Line Number (manual entry fallback) */}
309464
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
310465
<div className="md:col-span-2">
311-
<label className="block text-sm font-medium text-gray-700 mb-2">File Path</label>
466+
<label className="block text-sm font-medium text-gray-700 mb-2">
467+
File Path
468+
<span className="text-gray-500 font-normal ml-2">(or enter manually if not uploading)</span>
469+
</label>
312470
<input
313471
type="text"
314472
value={state.file_path}

0 commit comments

Comments
 (0)