@@ -8,6 +8,13 @@ import { getAllTags } from '../../services/bookmarks'
88import { useHotkeys } from '../../hooks/useHotkeys'
99import { useContentSuggestion } from '../../hooks/useContentSuggestion'
1010
11+ // Helper to detect if a string looks like a URL
12+ function looksLikeUrl ( str ) {
13+ if ( ! str ) return false
14+ const trimmed = str . trim ( )
15+ return / ^ h t t p s ? : \/ \/ / i. test ( trimmed ) || / ^ w w w \. / i. test ( trimmed )
16+ }
17+
1118export function BookmarkForm ( { isOpen, onClose, onSave, initialData = null } ) {
1219 const isEditing = Boolean ( initialData )
1320 const formRef = useRef ( null )
@@ -23,8 +30,9 @@ export function BookmarkForm({ isOpen, onClose, onSave, initialData = null }) {
2330 const [ errors , setErrors ] = useState ( { } )
2431 const [ loading , setLoading ] = useState ( false )
2532 const [ allTags , setAllTags ] = useState ( [ ] )
33+ const [ suggestedTags , setSuggestedTags ] = useState ( [ ] ) // Tags from suggestions, not yet applied
2634
27- const { suggestions, loading : suggesting , error : suggestError , suggest, clear : clearSuggestions , enabled : suggestionsEnabled } = useContentSuggestion ( )
35+ const { suggestions, loading : suggesting , error : suggestError , suggest, clear : clearSuggestions , cancel : cancelSuggestion , enabled : suggestionsEnabled } = useContentSuggestion ( )
2836
2937 useEffect ( ( ) => {
3038 if ( initialData ) {
@@ -45,6 +53,7 @@ export function BookmarkForm({ isOpen, onClose, onSave, initialData = null }) {
4553 } )
4654 }
4755 setErrors ( { } )
56+ setSuggestedTags ( [ ] )
4857 clearSuggestions ( )
4958 } , [ initialData , isOpen , clearSuggestions ] )
5059
@@ -58,16 +67,26 @@ export function BookmarkForm({ isOpen, onClose, onSave, initialData = null }) {
5867 }
5968 } , [ isOpen ] )
6069
61- // Apply suggestions to empty fields when they arrive
70+ // Apply suggestions to empty fields when they arrive (except tags - show those separately)
6271 useEffect ( ( ) => {
6372 if ( ! suggestions ) return
6473 setFormData ( ( prev ) => ( {
6574 ...prev ,
6675 title : prev . title || suggestions . title || prev . title ,
6776 description : prev . description || suggestions . description || prev . description ,
68- tags : prev . tags . length > 0 ? prev . tags : suggestions . suggestedTags || prev . tags ,
77+ // Don't auto-apply tags - let user pick from suggestedTags
6978 } ) )
70- } , [ suggestions ] )
79+ // Show suggested tags that aren't already in the form
80+ if ( suggestions . suggestedTags ?. length > 0 ) {
81+ setSuggestedTags ( ( prev ) => {
82+ // Filter out tags already in formData
83+ const newSuggested = suggestions . suggestedTags . filter (
84+ ( tag ) => ! formData . tags . includes ( tag )
85+ )
86+ return newSuggested
87+ } )
88+ }
89+ } , [ suggestions , formData . tags ] )
7190
7291 const submitForm = useCallback ( ( ) => {
7392 if ( formRef . current && ! loading ) {
@@ -153,6 +172,43 @@ export function BookmarkForm({ isOpen, onClose, onSave, initialData = null }) {
153172 }
154173 }
155174
175+ // Auto-suggest when URL is pasted
176+ const handleUrlPaste = ( e ) => {
177+ const pastedText = e . clipboardData ?. getData ( 'text' )
178+ if ( pastedText && looksLikeUrl ( pastedText ) && suggestionsEnabled && ! isEditing ) {
179+ // Small delay to let the input update first
180+ setTimeout ( ( ) => {
181+ const url = pastedText . trim ( ) . startsWith ( 'http' )
182+ ? pastedText . trim ( )
183+ : `https://${ pastedText . trim ( ) } `
184+ suggest ( url )
185+ } , 100 )
186+ }
187+ }
188+
189+ // Add a single suggested tag
190+ const addSuggestedTag = ( tag ) => {
191+ setFormData ( ( prev ) => ( {
192+ ...prev ,
193+ tags : [ ...prev . tags , tag ] ,
194+ } ) )
195+ setSuggestedTags ( ( prev ) => prev . filter ( ( t ) => t !== tag ) )
196+ }
197+
198+ // Add all suggested tags
199+ const addAllSuggestedTags = ( ) => {
200+ setFormData ( ( prev ) => ( {
201+ ...prev ,
202+ tags : [ ...prev . tags , ...suggestedTags ] ,
203+ } ) )
204+ setSuggestedTags ( [ ] )
205+ }
206+
207+ // Dismiss all suggested tags
208+ const dismissSuggestedTags = ( ) => {
209+ setSuggestedTags ( [ ] )
210+ }
211+
156212 return (
157213 < Modal
158214 isOpen = { isOpen }
@@ -167,6 +223,7 @@ export function BookmarkForm({ isOpen, onClose, onSave, initialData = null }) {
167223 type = "url"
168224 value = { formData . url }
169225 onChange = { ( value ) => updateField ( 'url' , value ) }
226+ onPaste = { handleUrlPaste }
170227 placeholder = "https://example.com"
171228 required
172229 error = { errors . url }
@@ -178,17 +235,24 @@ export function BookmarkForm({ isOpen, onClose, onSave, initialData = null }) {
178235 < Button
179236 type = "button"
180237 variant = "ghost"
181- onClick = { handleSuggest }
182- disabled = { loading || suggesting || ! formData . url . trim ( ) }
238+ onClick = { suggesting ? cancelSuggestion : handleSuggest }
239+ disabled = { loading || ! formData . url . trim ( ) }
183240 className = "text-xs whitespace-nowrap"
184241 >
185- { suggesting ? 'Fetching... ' : 'Suggest' }
242+ { suggesting ? 'Cancel ' : 'Suggest' }
186243 </ Button >
187244 </ div >
188245 ) }
189246 </ div >
190247
191- { suggestError && (
248+ { suggesting && (
249+ < p className = "text-xs text-muted-foreground -mt-2 mb-3 flex items-center gap-1.5" >
250+ < span className = "inline-block w-3 h-3 border-2 border-muted-foreground/30 border-t-muted-foreground rounded-full animate-spin" />
251+ Fetching suggestions...
252+ </ p >
253+ ) }
254+
255+ { suggestError && ! suggesting && (
192256 < p className = "text-xs text-muted-foreground -mt-2 mb-3" >
193257 Could not fetch suggestions
194258 </ p >
@@ -199,7 +263,7 @@ export function BookmarkForm({ isOpen, onClose, onSave, initialData = null }) {
199263 type = "text"
200264 value = { formData . title }
201265 onChange = { ( value ) => updateField ( 'title' , value ) }
202- placeholder = " Bookmark title"
266+ placeholder = { suggesting ? 'Loading...' : ' Bookmark title' }
203267 required
204268 error = { errors . title }
205269 disabled = { loading }
@@ -233,13 +297,51 @@ export function BookmarkForm({ isOpen, onClose, onSave, initialData = null }) {
233297 { errors . tags }
234298 </ p >
235299 ) }
300+
301+ { suggestedTags . length > 0 && (
302+ < div className = "mt-3 p-2.5 rounded-md bg-muted/50 border border-border/50" >
303+ < div className = "flex items-center justify-between mb-2" >
304+ < span className = "text-xs font-medium text-muted-foreground" > Suggested tags</ span >
305+ < div className = "flex gap-1.5" >
306+ < button
307+ type = "button"
308+ onClick = { addAllSuggestedTags }
309+ className = "text-xs text-primary hover:underline"
310+ >
311+ Add all
312+ </ button >
313+ < span className = "text-muted-foreground/50" > ·</ span >
314+ < button
315+ type = "button"
316+ onClick = { dismissSuggestedTags }
317+ className = "text-xs text-muted-foreground hover:text-foreground"
318+ >
319+ Dismiss
320+ </ button >
321+ </ div >
322+ </ div >
323+ < div className = "flex flex-wrap gap-1" >
324+ { suggestedTags . map ( ( tag ) => (
325+ < button
326+ key = { tag }
327+ type = "button"
328+ onClick = { ( ) => addSuggestedTag ( tag ) }
329+ className = "inline-flex items-center gap-1 px-2 py-0.5 text-xs rounded-md bg-background border border-border hover:border-primary hover:text-primary transition-colors cursor-pointer"
330+ >
331+ < span className = "text-muted-foreground" > +</ span >
332+ { tag }
333+ </ button >
334+ ) ) }
335+ </ div >
336+ </ div >
337+ ) }
236338 </ div >
237339
238340 < TextArea
239341 label = "Description"
240342 value = { formData . description }
241343 onChange = { ( value ) => updateField ( 'description' , value ) }
242- placeholder = " Optional description..."
344+ placeholder = { suggesting ? 'Loading...' : ' Optional description...' }
243345 rows = { 3 }
244346 disabled = { loading }
245347 />
0 commit comments