22 Box ,
33 Button ,
44 Chip ,
5+ ChipDelete ,
56 IconButton ,
67 Input ,
78 Modal ,
@@ -10,8 +11,8 @@ import {
1011 Stack ,
1112 Typography ,
1213} from '@mui/joy' ;
13- import { PencilIcon , XIcon } from 'lucide-react' ;
14- import { useState } from 'react' ;
14+ import { PencilIcon } from 'lucide-react' ;
15+ import { useEffect , useState } from 'react' ;
1516import * as chatAPI from 'renderer/lib/transformerlab-api-sdk' ;
1617import { fetcher } from 'renderer/lib/transformerlab-api-sdk' ;
1718import { parseTagInput } from './tagUtils' ;
@@ -33,19 +34,33 @@ export default function TagEditor({
3334 const [ error , setError ] = useState < string | null > ( null ) ;
3435 const [ busy , setBusy ] = useState ( false ) ;
3536 const [ open , setOpen ] = useState ( false ) ;
37+ const [ localTags , setLocalTags ] = useState < string [ ] > ( tags ) ;
38+
39+ // Sync local state when parent data refreshes or modal reopens.
40+ useEffect ( ( ) => {
41+ setLocalTags ( tags ) ;
42+ } , [ tags , open ] ) ;
3643
3744 const id = experimentId || experimentName ;
3845
39- async function callTagApi ( url : string , tagList : string [ ] ) {
46+ async function callTagApi (
47+ url : string ,
48+ tagList : string [ ] ,
49+ optimistic : string [ ] ,
50+ previous : string [ ] ,
51+ ) {
4052 setBusy ( true ) ;
4153 setError ( null ) ;
54+ setLocalTags ( optimistic ) ;
4255 try {
4356 await fetcher ( url , {
4457 method : 'POST' ,
4558 body : JSON . stringify ( { tags : tagList } ) ,
4659 } ) ;
47- await onChanged ( ) ;
60+ // Fire-and-forget parent refresh so UI stays snappy.
61+ void onChanged ( ) ;
4862 } catch ( e : any ) {
63+ setLocalTags ( previous ) ;
4964 const detail =
5065 e ?. response && typeof e . response === 'object' && 'detail' in e . response
5166 ? String ( e . response . detail )
@@ -61,12 +76,26 @@ export default function TagEditor({
6176 async function handleAdd ( ) {
6277 const parsed = parseTagInput ( draft ) ;
6378 if ( parsed . length === 0 ) return ;
64- await callTagApi ( chatAPI . Endpoints . Experiment . TagsAdd ( id ) , parsed ) ;
79+ const previous = localTags ;
80+ const merged = Array . from ( new Set ( [ ...previous , ...parsed ] ) ) ;
6581 setDraft ( '' ) ;
82+ await callTagApi (
83+ chatAPI . Endpoints . Experiment . TagsAdd ( id ) ,
84+ parsed ,
85+ merged ,
86+ previous ,
87+ ) ;
6688 }
6789
6890 async function handleRemove ( tag : string ) {
69- await callTagApi ( chatAPI . Endpoints . Experiment . TagsRemove ( id ) , [ tag ] ) ;
91+ const previous = localTags ;
92+ const next = previous . filter ( ( t ) => t !== tag ) ;
93+ await callTagApi (
94+ chatAPI . Endpoints . Experiment . TagsRemove ( id ) ,
95+ [ tag ] ,
96+ next ,
97+ previous ,
98+ ) ;
7099 }
71100
72101 return (
@@ -82,29 +111,25 @@ export default function TagEditor({
82111 < Modal open = { open } onClose = { ( ) => setOpen ( false ) } >
83112 < ModalDialog sx = { { minWidth : 360 } } >
84113 < ModalClose />
85- < Typography level = "title-md" > Edit tags — { experimentName } </ Typography >
114+ < Typography level = "title-md" > Edit tags ( { experimentName } ) </ Typography >
86115 < Stack spacing = { 1.5 } sx = { { mt : 1 } } >
87116 < Box sx = { { display : 'flex' , gap : 0.5 , flexWrap : 'wrap' } } >
88- { tags . length === 0 && (
117+ { localTags . length === 0 && (
89118 < Typography level = "body-sm" color = "neutral" >
90119 No tags yet.
91120 </ Typography >
92121 ) }
93- { tags . map ( ( t ) => (
122+ { localTags . map ( ( t ) => (
94123 < Chip
95124 key = { t }
96125 size = "sm"
97126 variant = "soft"
98127 color = "neutral"
99128 endDecorator = {
100- < IconButton
101- size = "sm"
102- variant = "plain"
103- onClick = { ( ) => handleRemove ( t ) }
129+ < ChipDelete
130+ onDelete = { ( ) => handleRemove ( t ) }
104131 disabled = { busy }
105- >
106- < XIcon size = { 10 } />
107- </ IconButton >
132+ />
108133 }
109134 >
110135 { t }
0 commit comments