From e6ef14ea8a76d3ca99826105b5b39e720320bfc9 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 13 Jun 2026 10:34:15 -0700 Subject: [PATCH 1/2] fix(skills): reuse shared upload field in skill import modal; logo-only Quartr icon - Replace the hand-rolled drop zone in the skill import modal with the shared ChipModalField type='file' control (same component the Knowledge Base and Help & Support modals use), so the upload zone is visually consistent. - Migrate the GitHub-URL and paste-content rows to ChipModalField so every field shares the canonical px-4 gutter and error rendering, and align the 'or' dividers to match. - Drop the monospace font on the paste textarea so its text matches the rest of the modal. - Quartr icon now renders the logo mark only (no wordmark) as a black mark on a white rounded tile. --- .../components/skill-import/skill-import.tsx | 158 +++++------------- apps/sim/components/icons.tsx | 55 ++---- 2 files changed, 61 insertions(+), 152 deletions(-) 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..c216f7c57ae 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/icons.tsx b/apps/sim/components/icons.tsx index 162de3ad5f9..7d016ee4f53 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3707,44 +3707,23 @@ export function QdrantIcon(props: SVGProps) { export function QuartrIcon(props: SVGProps) { return ( - - - - - - - - - - - + + + + + + + + ) } From 460b304e491c3fe580b70e3030e719fe1ec6eea9 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 13 Jun 2026 10:42:34 -0700 Subject: [PATCH 2/2] fix(emcn): restore upload spinner via loading prop on ChipModalField file control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback — the shared file drop zone now accepts an optional loading prop that renders an animated spinner and blocks further picks while an async import is in flight, restoring the feedback the skill import modal lost when it migrated off its bespoke drop zone. --- .../components/skill-import/skill-import.tsx | 2 +- .../emcn/components/chip-modal/chip-modal.tsx | 21 +++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) 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 c216f7c57ae..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 @@ -128,7 +128,7 @@ export function SkillImport({ onImport }: SkillImportProps) { title='Upload File' accept='.md,.zip' onChange={handleFiles} - disabled={fileState === 'loading'} + loading={fileState === 'loading'} label={fileState === 'loading' ? 'Importing…' : undefined} description='.md file with YAML frontmatter, or .zip containing a SKILL.md' error={fileError || undefined} 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({ {isDragging ? 'Drop files here' : label}