diff --git a/apps/sim/app/workspace/[workspaceId]/skills/components/skill-import/skill-import.tsx b/apps/sim/app/workspace/[workspaceId]/skills/components/skill-import/skill-import.tsx index 968b63b3819..7b2323c13ad 100644 --- a/apps/sim/app/workspace/[workspaceId]/skills/components/skill-import/skill-import.tsx +++ b/apps/sim/app/workspace/[workspaceId]/skills/components/skill-import/skill-import.tsx @@ -1,13 +1,11 @@ 'use client' import type { ChangeEvent } from 'react' -import { useCallback, useRef, useState } from 'react' +import { useCallback, useState } from 'react' import { getErrorMessage } from '@sim/utils/errors' -import { Chip, ChipInput, ChipTextarea, Loader } from '@/components/emcn' -import { Upload } from '@/components/emcn/icons' +import { Chip, ChipInput, ChipModalField, ChipTextarea, Loader } from '@/components/emcn' import { requestJson } from '@/lib/api/client/request' import { importSkillContract } from '@/lib/api/contracts' -import { cn } from '@/lib/core/utils/cn' import { extractSkillFromZip, parseSkillMarkdown, @@ -33,10 +31,6 @@ function isAcceptedFile(file: File): boolean { } export function SkillImport({ onImport }: SkillImportProps) { - const fileInputRef = useRef(null) - - const [dragCounter, setDragCounter] = useState(0) - const isDragging = dragCounter > 0 const [fileState, setFileState] = useState('idle') const [fileError, setFileError] = useState('') @@ -84,39 +78,9 @@ export function SkillImport({ onImport }: SkillImportProps) { [onImport] ) - const handleFileChange = useCallback( - (e: ChangeEvent) => { - const file = e.target.files?.[0] - if (file) processFile(file) - if (fileInputRef.current) fileInputRef.current.value = '' - }, - [processFile] - ) - - const handleDragEnter = useCallback((e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setDragCounter((prev) => prev + 1) - }, []) - - const handleDragLeave = useCallback((e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setDragCounter((prev) => prev - 1) - }, []) - - const handleDragOver = useCallback((e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - e.dataTransfer.dropEffect = 'copy' - }, []) - - const handleDrop = useCallback( - (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setDragCounter(0) - const file = e.dataTransfer.files?.[0] + const handleFiles = useCallback( + (files: File[]) => { + const file = files[0] if (file) processFile(file) }, [processFile] @@ -159,55 +123,20 @@ export function SkillImport({ onImport }: SkillImportProps) { return (
- {/* File drop zone */} -
- Upload File - - {fileError &&

{fileError}

} -
+ - {/* GitHub URL */} -
- - Import from GitHub - +
: 'Fetch'}
- {githubError &&

{githubError}

} -
+
- {/* Paste content */} -
- - Paste SKILL.md Content - - ) => { - setPasteContent(e.target.value) - if (pasteError) setPasteError('') - }} - resizable - className='min-h-[120px] font-mono leading-relaxed' - /> - {pasteError &&

{pasteError}

} -
- - Import - + +
+ ) => { + setPasteContent(e.target.value) + if (pasteError) setPasteError('') + }} + resizable + className='min-h-[120px]' + /> +
+ + Import + +
-
+
) } function ImportDivider() { return ( -
+
or
diff --git a/apps/sim/components/emcn/components/chip-modal/chip-modal.tsx b/apps/sim/components/emcn/components/chip-modal/chip-modal.tsx index d2079583bb6..b15f2031ad1 100644 --- a/apps/sim/components/emcn/components/chip-modal/chip-modal.tsx +++ b/apps/sim/components/emcn/components/chip-modal/chip-modal.tsx @@ -52,6 +52,7 @@ import { ChipTextarea } from '@/components/emcn/components/chip-textarea/chip-te import { Label } from '@/components/emcn/components/label/label' import { Modal, ModalContent } from '@/components/emcn/components/modal/modal' import { TagInput, type TagItem } from '@/components/emcn/components/tag-input/tag-input' +import { Loader } from '@/components/emcn/icons' import { cn } from '@/lib/core/utils/cn' import { quickValidateEmail } from '@/lib/messaging/email/validation' @@ -377,6 +378,14 @@ interface ChipModalFileFieldProps extends ChipModalFieldBaseProps { * for a single-line zone. */ description?: React.ReactNode + /** + * Renders a spinner inside the drop zone and blocks further picks while an + * async import/upload is in flight. Use for slow selections (zip extraction, + * remote fetches) where the zone would otherwise look idle. Pair with a + * `label` such as `'Importing…'` for an explicit status line. + * @default false + */ + loading?: boolean } export interface ChipModalEmailsFieldProps extends ChipModalFieldBaseProps { @@ -692,6 +701,7 @@ function ChipModalFileControl({ multiple = false, label = 'Drop files here or click to browse', description, + loading = false, disabled, id, 'aria-required': ariaRequired, @@ -700,6 +710,7 @@ function ChipModalFileControl({ }: ChipModalFileFieldProps & { id: string } & React.AriaAttributes) { const inputRef = React.useRef(null) const [isDragging, setIsDragging] = React.useState(false) + const isInteractive = !disabled && !loading const emitFiles = React.useCallback( (files: FileList | null) => { @@ -713,7 +724,8 @@ function ChipModalFileControl({