118 lines
3.4 KiB
TypeScript
118 lines
3.4 KiB
TypeScript
|
|
/**
|
||
|
|
* StudioContextFiles - Context document upload and display
|
||
|
|
*/
|
||
|
|
|
||
|
|
import React, { useState, useRef } from 'react';
|
||
|
|
import { FileText, Upload, Trash2, Loader2 } from 'lucide-react';
|
||
|
|
import { intakeApi } from '../../api/intake';
|
||
|
|
|
||
|
|
interface StudioContextFilesProps {
|
||
|
|
draftId: string;
|
||
|
|
files: string[];
|
||
|
|
onUploadComplete: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const StudioContextFiles: React.FC<StudioContextFilesProps> = ({
|
||
|
|
draftId,
|
||
|
|
files,
|
||
|
|
onUploadComplete,
|
||
|
|
}) => {
|
||
|
|
const [isUploading, setIsUploading] = useState(false);
|
||
|
|
const [deleting, setDeleting] = useState<string | null>(null);
|
||
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||
|
|
|
||
|
|
const VALID_EXTENSIONS = ['.md', '.txt', '.pdf', '.json', '.csv', '.docx'];
|
||
|
|
|
||
|
|
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
|
|
const selectedFiles = Array.from(e.target.files || []);
|
||
|
|
if (selectedFiles.length === 0) return;
|
||
|
|
|
||
|
|
e.target.value = '';
|
||
|
|
setIsUploading(true);
|
||
|
|
|
||
|
|
try {
|
||
|
|
await intakeApi.uploadContextFiles(draftId, selectedFiles);
|
||
|
|
onUploadComplete();
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Failed to upload context files:', err);
|
||
|
|
} finally {
|
||
|
|
setIsUploading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const deleteFile = async (filename: string) => {
|
||
|
|
setDeleting(filename);
|
||
|
|
|
||
|
|
try {
|
||
|
|
await intakeApi.deleteContextFile(draftId, filename);
|
||
|
|
onUploadComplete();
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Failed to delete context file:', err);
|
||
|
|
} finally {
|
||
|
|
setDeleting(null);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const getFileIcon = (_filename: string) => {
|
||
|
|
return <FileText className="w-3.5 h-3.5 text-amber-400" />;
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-2">
|
||
|
|
{/* File List */}
|
||
|
|
{files.length > 0 && (
|
||
|
|
<div className="space-y-1">
|
||
|
|
{files.map((name) => (
|
||
|
|
<div
|
||
|
|
key={name}
|
||
|
|
className="flex items-center gap-2 px-2 py-1.5 rounded bg-dark-700/50 text-sm group"
|
||
|
|
>
|
||
|
|
{getFileIcon(name)}
|
||
|
|
<span className="text-dark-200 truncate flex-1">{name}</span>
|
||
|
|
<button
|
||
|
|
onClick={() => deleteFile(name)}
|
||
|
|
disabled={deleting === name}
|
||
|
|
className="p-1 opacity-0 group-hover:opacity-100 hover:bg-red-500/20 rounded text-red-400 transition-all"
|
||
|
|
>
|
||
|
|
{deleting === name ? (
|
||
|
|
<Loader2 className="w-3 h-3 animate-spin" />
|
||
|
|
) : (
|
||
|
|
<Trash2 className="w-3 h-3" />
|
||
|
|
)}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Upload Button */}
|
||
|
|
<button
|
||
|
|
onClick={() => fileInputRef.current?.click()}
|
||
|
|
disabled={isUploading}
|
||
|
|
className="w-full flex items-center justify-center gap-2 px-3 py-2 rounded-lg
|
||
|
|
border border-dashed border-dark-600 text-dark-400 text-sm
|
||
|
|
hover:border-primary-400/50 hover:text-primary-400 hover:bg-primary-400/5
|
||
|
|
disabled:opacity-50 transition-colors"
|
||
|
|
>
|
||
|
|
{isUploading ? (
|
||
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||
|
|
) : (
|
||
|
|
<Upload className="w-4 h-4" />
|
||
|
|
)}
|
||
|
|
{isUploading ? 'Uploading...' : 'Add context files'}
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<input
|
||
|
|
ref={fileInputRef}
|
||
|
|
type="file"
|
||
|
|
multiple
|
||
|
|
accept={VALID_EXTENSIONS.join(',')}
|
||
|
|
onChange={handleFileSelect}
|
||
|
|
className="hidden"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default StudioContextFiles;
|