11'use client'
22
33import type { ChangeEvent } from 'react'
4- import { useCallback , useRef , useState } from 'react'
4+ import { useCallback , useState } from 'react'
55import { getErrorMessage } from '@sim/utils/errors'
6- import { Chip , ChipInput , ChipTextarea , Loader } from '@/components/emcn'
7- import { Upload } from '@/components/emcn/icons'
6+ import { Chip , ChipInput , ChipModalField , ChipTextarea , Loader } from '@/components/emcn'
87import { requestJson } from '@/lib/api/client/request'
98import { importSkillContract } from '@/lib/api/contracts'
10- import { cn } from '@/lib/core/utils/cn'
119import {
1210 extractSkillFromZip ,
1311 parseSkillMarkdown ,
@@ -33,10 +31,6 @@ function isAcceptedFile(file: File): boolean {
3331}
3432
3533export function SkillImport ( { onImport } : SkillImportProps ) {
36- const fileInputRef = useRef < HTMLInputElement > ( null )
37-
38- const [ dragCounter , setDragCounter ] = useState ( 0 )
39- const isDragging = dragCounter > 0
4034 const [ fileState , setFileState ] = useState < ImportState > ( 'idle' )
4135 const [ fileError , setFileError ] = useState ( '' )
4236
@@ -84,39 +78,9 @@ export function SkillImport({ onImport }: SkillImportProps) {
8478 [ onImport ]
8579 )
8680
87- const handleFileChange = useCallback (
88- ( e : ChangeEvent < HTMLInputElement > ) => {
89- const file = e . target . files ?. [ 0 ]
90- if ( file ) processFile ( file )
91- if ( fileInputRef . current ) fileInputRef . current . value = ''
92- } ,
93- [ processFile ]
94- )
95-
96- const handleDragEnter = useCallback ( ( e : React . DragEvent ) => {
97- e . preventDefault ( )
98- e . stopPropagation ( )
99- setDragCounter ( ( prev ) => prev + 1 )
100- } , [ ] )
101-
102- const handleDragLeave = useCallback ( ( e : React . DragEvent ) => {
103- e . preventDefault ( )
104- e . stopPropagation ( )
105- setDragCounter ( ( prev ) => prev - 1 )
106- } , [ ] )
107-
108- const handleDragOver = useCallback ( ( e : React . DragEvent ) => {
109- e . preventDefault ( )
110- e . stopPropagation ( )
111- e . dataTransfer . dropEffect = 'copy'
112- } , [ ] )
113-
114- const handleDrop = useCallback (
115- ( e : React . DragEvent ) => {
116- e . preventDefault ( )
117- e . stopPropagation ( )
118- setDragCounter ( 0 )
119- const file = e . dataTransfer . files ?. [ 0 ]
81+ const handleFiles = useCallback (
82+ ( files : File [ ] ) => {
83+ const file = files [ 0 ]
12084 if ( file ) processFile ( file )
12185 } ,
12286 [ processFile ]
@@ -159,55 +123,20 @@ export function SkillImport({ onImport }: SkillImportProps) {
159123
160124 return (
161125 < div className = 'flex flex-col gap-4' >
162- { /* File drop zone */ }
163- < div className = 'flex flex-col gap-[9px]' >
164- < span className = 'pl-0.5 font-normal text-[var(--text-muted)] text-sm' > Upload File</ span >
165- < button
166- type = 'button'
167- onClick = { ( ) => fileInputRef . current ?. click ( ) }
168- onDragEnter = { handleDragEnter }
169- onDragOver = { handleDragOver }
170- onDragLeave = { handleDragLeave }
171- onDrop = { handleDrop }
172- disabled = { fileState === 'loading' }
173- className = { cn (
174- 'flex w-full cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border border-dashed px-4 py-6 transition-colors' ,
175- 'border-[var(--border-1)] bg-[var(--surface-5)] hover-hover:bg-[var(--surface-active)] dark:bg-[var(--surface-4)]' ,
176- isDragging && 'border-[var(--text-muted)] bg-[var(--surface-active)]' ,
177- fileState === 'loading' && 'pointer-events-none opacity-60'
178- ) }
179- >
180- < input
181- ref = { fileInputRef }
182- type = 'file'
183- accept = '.md,.zip'
184- onChange = { handleFileChange }
185- className = 'hidden'
186- />
187- { fileState === 'loading' ? (
188- < Loader className = 'size-[16px] text-[var(--text-tertiary)]' animate />
189- ) : (
190- < Upload className = 'size-[16px] text-[var(--text-tertiary)]' />
191- ) }
192- < div className = 'flex flex-col gap-0.5 text-center' >
193- < span className = 'text-[var(--text-primary)] text-sm' >
194- { isDragging ? 'Drop file here' : 'Drop file here or click to browse' }
195- </ span >
196- < span className = 'text-[11px] text-[var(--text-muted)]' >
197- .md file with YAML frontmatter, or .zip containing a SKILL.md
198- </ span >
199- </ div >
200- </ button >
201- { fileError && < p className = 'text-[12px] text-[var(--text-error)]' > { fileError } </ p > }
202- </ div >
126+ < ChipModalField
127+ type = 'file'
128+ title = 'Upload File'
129+ accept = '.md,.zip'
130+ onChange = { handleFiles }
131+ disabled = { fileState === 'loading' }
132+ label = { fileState === 'loading' ? 'Importing…' : undefined }
133+ description = '.md file with YAML frontmatter, or .zip containing a SKILL.md'
134+ error = { fileError || undefined }
135+ />
203136
204137 < ImportDivider />
205138
206- { /* GitHub URL */ }
207- < div className = 'flex flex-col gap-[9px]' >
208- < span className = 'pl-0.5 font-normal text-[var(--text-muted)] text-sm' >
209- Import from GitHub
210- </ span >
139+ < ChipModalField type = 'custom' title = 'Import from GitHub' error = { githubError || undefined } >
211140 < div className = 'flex gap-2' >
212141 < ChipInput
213142 placeholder = 'https://github.com/owner/repo/blob/main/SKILL.md'
@@ -217,7 +146,7 @@ export function SkillImport({ onImport }: SkillImportProps) {
217146 if ( githubError ) setGithubError ( '' )
218147 } }
219148 disabled = { githubState === 'loading' }
220- className = 'flex-1'
149+ className = 'min-w-0 flex-1'
221150 />
222151 < Chip
223152 flush
@@ -227,42 +156,43 @@ export function SkillImport({ onImport }: SkillImportProps) {
227156 { githubState === 'loading' ? < Loader className = 'size-[14px]' animate /> : 'Fetch' }
228157 </ Chip >
229158 </ div >
230- { githubError && < p className = 'text-[12px] text-[var(--text-error)]' > { githubError } </ p > }
231- </ div >
159+ </ ChipModalField >
232160
233161 < ImportDivider />
234162
235- { /* Paste content */ }
236- < div className = 'flex flex-col gap-[9px]' >
237- < span className = 'pl-0.5 font-normal text-[var(--text-muted)] text-sm' >
238- Paste SKILL.md Content
239- </ span >
240- < ChipTextarea
241- placeholder = {
242- '---\nname: my-skill\ndescription: What this skill does\n---\n\n# Instructions...'
243- }
244- value = { pasteContent }
245- onChange = { ( e : ChangeEvent < HTMLTextAreaElement > ) => {
246- setPasteContent ( e . target . value )
247- if ( pasteError ) setPasteError ( '' )
248- } }
249- resizable
250- className = 'min-h-[120px] font-mono leading-relaxed'
251- />
252- { pasteError && < p className = 'text-[12px] text-[var(--text-error)]' > { pasteError } </ p > }
253- < div className = 'flex justify-end' >
254- < Chip variant = 'primary' flush onClick = { handlePasteImport } disabled = { ! pasteContent . trim ( ) } >
255- Import
256- </ Chip >
163+ < ChipModalField type = 'custom' title = 'Paste SKILL.md Content' error = { pasteError || undefined } >
164+ < div className = 'flex flex-col gap-[9px]' >
165+ < ChipTextarea
166+ placeholder = {
167+ '---\nname: my-skill\ndescription: What this skill does\n---\n\n# Instructions...'
168+ }
169+ value = { pasteContent }
170+ onChange = { ( e : ChangeEvent < HTMLTextAreaElement > ) => {
171+ setPasteContent ( e . target . value )
172+ if ( pasteError ) setPasteError ( '' )
173+ } }
174+ resizable
175+ className = 'min-h-[120px]'
176+ />
177+ < div className = 'flex justify-end' >
178+ < Chip
179+ variant = 'primary'
180+ flush
181+ onClick = { handlePasteImport }
182+ disabled = { ! pasteContent . trim ( ) }
183+ >
184+ Import
185+ </ Chip >
186+ </ div >
257187 </ div >
258- </ div >
188+ </ ChipModalField >
259189 </ div >
260190 )
261191}
262192
263193function ImportDivider ( ) {
264194 return (
265- < div className = 'flex items-center gap-3 px-1 ' >
195+ < div className = 'flex items-center gap-3 px-2 ' >
266196 < div className = 'h-px flex-1 bg-[var(--border)]' />
267197 < span className = 'text-[11px] text-[var(--text-muted)]' > or</ span >
268198 < div className = 'h-px flex-1 bg-[var(--border)]' />
0 commit comments