@@ -35,17 +35,24 @@ import { Textarea } from '@/components/ui/textarea';
3535import useChatStoreAdapter from '@/hooks/useChatStoreAdapter' ;
3636import { INIT_PROVODERS } from '@/lib/llm' ;
3737import { useAuthStore , useWorkerList } from '@/store/authStore' ;
38+ import {
39+ hasGlobalAgentTemplatesApi ,
40+ useGlobalAgentTemplatesStore ,
41+ } from '@/store/globalAgentTemplatesStore' ;
3842import {
3943 Bot ,
4044 ChevronDown ,
4145 ChevronUp ,
46+ Download ,
4247 Edit ,
4348 Eye ,
4449 EyeOff ,
50+ FileUp ,
4551 Plus ,
4652} from 'lucide-react' ;
47- import { useRef , useState } from 'react' ;
53+ import { useCallback , useEffect , useRef , useState } from 'react' ;
4854import { useTranslation } from 'react-i18next' ;
55+ import { toast } from 'sonner' ;
4956import ToolSelect from './ToolSelect' ;
5057
5158interface EnvValue {
@@ -110,6 +117,121 @@ export function AddWorker({
110117 const [ customModelPlatform , setCustomModelPlatform ] = useState ( '' ) ;
111118 const [ customModelType , setCustomModelType ] = useState ( '' ) ;
112119
120+ // Global template and export/import
121+ const [ saveAsGlobalTemplate , setSaveAsGlobalTemplate ] = useState ( false ) ;
122+ const [ selectedTemplateId , setSelectedTemplateId ] = useState < string > ( '' ) ;
123+ const importFileRef = useRef < HTMLInputElement > ( null ) ;
124+ const {
125+ templates : globalTemplates ,
126+ loadTemplates : loadGlobalTemplates ,
127+ addTemplate : addGlobalTemplate ,
128+ getTemplate : getGlobalTemplate ,
129+ } = useGlobalAgentTemplatesStore ( ) ;
130+ const hasGlobalTemplatesApi = hasGlobalAgentTemplatesApi ( ) ;
131+
132+ useEffect ( ( ) => {
133+ if ( hasGlobalTemplatesApi && dialogOpen ) loadGlobalTemplates ( ) ;
134+ } , [ hasGlobalTemplatesApi , dialogOpen , loadGlobalTemplates ] ) ;
135+
136+ useEffect ( ( ) => {
137+ if ( selectedTemplateId && dialogOpen ) {
138+ const tpl = getGlobalTemplate ( selectedTemplateId ) ;
139+ if ( tpl ) {
140+ setWorkerName ( tpl . name ) ;
141+ setWorkerDescription ( tpl . description ) ;
142+ if ( tpl . custom_model_config ) {
143+ setUseCustomModel ( true ) ;
144+ setShowModelConfig ( true ) ;
145+ setCustomModelPlatform ( tpl . custom_model_config . model_platform ?? '' ) ;
146+ setCustomModelType ( tpl . custom_model_config . model_type ?? '' ) ;
147+ }
148+ }
149+ }
150+ } , [ selectedTemplateId , dialogOpen , getGlobalTemplate ] ) ;
151+
152+ const handleExportConfig = useCallback ( ( ) => {
153+ const localTool : string [ ] = [ ] ;
154+ const mcpList : string [ ] = [ ] ;
155+ selectedTools . forEach ( ( tool : McpItem ) => {
156+ if ( tool . isLocal ) {
157+ localTool . push ( tool . toolkit as string ) ;
158+ } else {
159+ mcpList . push ( tool ?. key || tool ?. mcp_name ) ;
160+ }
161+ } ) ;
162+ let mcpLocal : Record < string , unknown > = { mcpServers : { } } ;
163+ selectedTools . forEach ( ( tool : McpItem ) => {
164+ if ( ! tool . isLocal && tool . key ) {
165+ ( mcpLocal . mcpServers as Record < string , unknown > ) [ tool . key ] = { } ;
166+ }
167+ } ) ;
168+ const custom_model_config =
169+ useCustomModel && customModelPlatform
170+ ? {
171+ model_platform : customModelPlatform ,
172+ model_type : customModelType || undefined ,
173+ }
174+ : undefined ;
175+ const blob = new Blob (
176+ [
177+ JSON . stringify (
178+ {
179+ name : workerName ,
180+ description : workerDescription ,
181+ tools : localTool ,
182+ mcp_tools : mcpLocal ,
183+ custom_model_config,
184+ } ,
185+ null ,
186+ 2
187+ ) ,
188+ ] ,
189+ { type : 'application/json' }
190+ ) ;
191+ const url = URL . createObjectURL ( blob ) ;
192+ const a = document . createElement ( 'a' ) ;
193+ a . href = url ;
194+ a . download = `agent-${ workerName || 'config' } .json` ;
195+ a . click ( ) ;
196+ URL . revokeObjectURL ( url ) ;
197+ toast . success ( t ( 'workforce.save-changes' ) ) ;
198+ } , [
199+ selectedTools ,
200+ workerName ,
201+ workerDescription ,
202+ useCustomModel ,
203+ customModelPlatform ,
204+ customModelType ,
205+ t ,
206+ ] ) ;
207+
208+ const handleImportConfig = useCallback (
209+ ( e : React . ChangeEvent < HTMLInputElement > ) => {
210+ const file = e . target . files ?. [ 0 ] ;
211+ e . target . value = '' ;
212+ if ( ! file ) return ;
213+ file . text ( ) . then ( ( text ) => {
214+ try {
215+ const data = JSON . parse ( text ) ;
216+ setWorkerName ( data . name ?? '' ) ;
217+ setWorkerDescription ( data . description ?? '' ) ;
218+ if ( data . custom_model_config ) {
219+ setUseCustomModel ( true ) ;
220+ setShowModelConfig ( true ) ;
221+ setCustomModelPlatform (
222+ data . custom_model_config . model_platform ?? ''
223+ ) ;
224+ setCustomModelType ( data . custom_model_config . model_type ?? '' ) ;
225+ }
226+ toast . success ( t ( 'workforce.save-changes' ) ) ;
227+ } catch {
228+ toast . error ( t ( 'agents.skill-add-error' ) ) ;
229+ }
230+ } ) ;
231+ } ,
232+ [ t ]
233+ ) ;
234+
113235 // environment variable management
114236 const initializeEnvValues = ( mcp : McpItem ) => {
115237 console . log ( mcp ) ;
@@ -261,6 +383,8 @@ export function AddWorker({
261383 setUseCustomModel ( false ) ;
262384 setCustomModelPlatform ( '' ) ;
263385 setCustomModelType ( '' ) ;
386+ setSaveAsGlobalTemplate ( false ) ;
387+ setSelectedTemplateId ( '' ) ;
264388 } ;
265389
266390 // tool function
@@ -401,6 +525,24 @@ export function AddWorker({
401525 setWorkerList ( [ ...workerList , worker ] ) ;
402526 }
403527
528+ if ( saveAsGlobalTemplate && hasGlobalTemplatesApi ) {
529+ const customModelConfig =
530+ useCustomModel && customModelPlatform
531+ ? {
532+ model_platform : customModelPlatform ,
533+ model_type : customModelType || undefined ,
534+ }
535+ : undefined ;
536+ await addGlobalTemplate ( {
537+ name : workerName ,
538+ description : workerDescription ,
539+ tools : localTool ,
540+ mcp_tools : mcpLocal ,
541+ custom_model_config : customModelConfig ,
542+ } ) ;
543+ toast . success ( t ( 'agents.skill-added-success' ) ) ;
544+ }
545+
404546 setDialogOpen ( false ) ;
405547
406548 // reset form
@@ -568,6 +710,34 @@ export function AddWorker({
568710 // default add interface
569711 < >
570712 < DialogContentSection className = "flex flex-col gap-3 bg-white-100% p-md" >
713+ { hasGlobalTemplatesApi &&
714+ globalTemplates . length > 0 &&
715+ ! edit && (
716+ < div className = "flex flex-col gap-1" >
717+ < label className = "text-xs text-text-body" >
718+ { t ( 'agents.global-agent-create-from-template' ) }
719+ </ label >
720+ < Select
721+ value = { selectedTemplateId }
722+ onValueChange = { setSelectedTemplateId }
723+ >
724+ < SelectTrigger className = "w-full" >
725+ < SelectValue placeholder = { t ( 'agents.no-templates' ) } />
726+ </ SelectTrigger >
727+ < SelectContent >
728+ < SelectItem value = "" >
729+ { t ( 'agents.no-templates' ) }
730+ </ SelectItem >
731+ { globalTemplates . map ( ( tpl ) => (
732+ < SelectItem key = { tpl . id } value = { tpl . id } >
733+ { tpl . name }
734+ </ SelectItem >
735+ ) ) }
736+ </ SelectContent >
737+ </ Select >
738+ </ div >
739+ ) }
740+
571741 < div className = "flex flex-col gap-4" >
572742 < div className = "flex items-center gap-sm" >
573743 < div className = "flex h-16 w-16 items-center justify-center" >
@@ -597,15 +767,80 @@ export function AddWorker({
597767 placeholder = { t ( 'layout.im-an-agent-specially-designed-for' ) }
598768 value = { workerDescription }
599769 onChange = { ( e ) => setWorkerDescription ( e . target . value ) }
770+ className = "min-h-[120px] resize-y"
600771 />
601772
773+ < div className = "flex flex-row flex-wrap items-center gap-2" >
774+ < Button
775+ type = "button"
776+ variant = "ghost"
777+ size = "sm"
778+ onClick = { handleExportConfig }
779+ >
780+ < Download className = "mr-1 h-4 w-4" />
781+ { t ( 'workforce.export-agent' ) }
782+ </ Button >
783+ < Button
784+ type = "button"
785+ variant = "ghost"
786+ size = "sm"
787+ onClick = { ( ) => importFileRef . current ?. click ( ) }
788+ >
789+ < FileUp className = "mr-1 h-4 w-4" />
790+ { t ( 'workforce.import-agent' ) }
791+ </ Button >
792+ < input
793+ ref = { importFileRef }
794+ type = "file"
795+ accept = ".json,application/json"
796+ className = "hidden"
797+ onChange = { handleImportConfig }
798+ />
799+ </ div >
800+
602801 < ToolSelect
603802 onShowEnvConfig = { handleShowEnvConfig }
604803 onSelectedToolsChange = { handleSelectedToolsChange }
605804 initialSelectedTools = { selectedTools }
606805 ref = { toolSelectRef }
607806 />
608807
808+ { selectedTools . length > 0 && (
809+ < div className = "rounded-lg border border-border-subtle-strong bg-surface-tertiary-subtle px-3 py-2" >
810+ < div className = "text-body-xs font-medium text-text-body" >
811+ { t ( 'workforce.agent-tool' ) } ({ selectedTools . length } )
812+ </ div >
813+ < div className = "mt-1 flex flex-wrap gap-1" >
814+ { selectedTools . map ( ( tool , idx ) => (
815+ < span
816+ key = { idx }
817+ className = "rounded bg-surface-primary px-1.5 py-0.5 text-body-xs text-text-body"
818+ title = { tool . description || tool . name || tool . key }
819+ >
820+ { tool . name ||
821+ tool . mcp_name ||
822+ tool . key ||
823+ `Tool ${ idx + 1 } ` }
824+ </ span >
825+ ) ) }
826+ </ div >
827+ </ div >
828+ ) }
829+
830+ { hasGlobalTemplatesApi && (
831+ < label className = "flex cursor-pointer items-center gap-2 text-body-sm text-text-body" >
832+ < input
833+ type = "checkbox"
834+ checked = { saveAsGlobalTemplate }
835+ onChange = { ( e ) =>
836+ setSaveAsGlobalTemplate ( e . target . checked )
837+ }
838+ className = "rounded border-border-subtle-strong"
839+ />
840+ { t ( 'agents.global-agent-save-as-template' ) }
841+ </ label >
842+ ) }
843+
609844 { /* Model Configuration Section */ }
610845 < div className = "mt-2 flex flex-col gap-2" >
611846 < button
0 commit comments