22
33import { useState } from 'react'
44import { ActivityRow } from '@sim/emcn/activity-row'
5- import { TextAreaField } from '@sim/emcn/field'
6- import { DetailsField } from '@sim/emcn/field/details-field'
5+ import { TextAreaField , TextField } from '@sim/emcn/field'
76import { Icon } from '@sim/emcn/icon'
87import { Modal } from '@sim/emcn/modal'
98import { getErrorMessage } from '@sim/utils/errors'
109import { IntegrationTemplates } from '@/app/(product)/o/[organizationId]/integrations/_components/integration-capabilities/integration-capabilities'
1110import styles from '@/app/(product)/o/[organizationId]/integrations/_components/integration-capabilities/integration-capabilities.module.css'
12- import { isSkillNameConflictError , useCreateSkill , useSkills } from '@/platform/lib/data/skills'
11+ import {
12+ isSkillNameConflictError ,
13+ SKILL_DESCRIPTION_MAX_LENGTH ,
14+ type SkillDefinition ,
15+ useCreateSkill ,
16+ useDeleteSkill ,
17+ useSkills ,
18+ useUpdateSkill ,
19+ } from '@/platform/lib/data/skills'
1320import type { IntegrationCatalogEntry } from '@/platform/lib/integrations/catalog-model'
1421
1522type CatalogSkill = IntegrationCatalogEntry [ 'skills' ] [ number ]
23+ type SkillDraft = { name : string ; description : string ; content : string }
1624
1725interface WorkspaceCapabilitiesProps {
1826 workspaceId : string
@@ -22,19 +30,20 @@ interface WorkspaceCapabilitiesProps {
2230
2331/**
2432 * An app's suggested skills and agent templates in one workspace, as legacy's workspace app page
25- * offered them. A skill is added to the workspace through the Skills tab's own create operation,
26- * and the workspace's skill list decides which already read Added .
33+ * offered them. A suggestion is added to the workspace through the Skills tab's own create
34+ * operation; one already added, by name, opens for editing or removal .
2735 */
2836export function WorkspaceCapabilities ( {
2937 workspaceId,
3038 skills,
3139 templates,
3240} : WorkspaceCapabilitiesProps ) {
3341 const existing = useSkills ( workspaceId )
34- const createSkill = useCreateSkill ( )
35- const [ adding , setAdding ] = useState < { skill : CatalogSkill ; instructions : string } | null > ( null )
42+ const [ open , setOpen ] = useState < { skill : CatalogSkill ; installed ?: SkillDefinition } | null > (
43+ null
44+ )
3645 const listed = existing . isPlaceholderData ? undefined : existing . data
37- const added = new Set ( ( listed ?? [ ] ) . map ( ( skill ) => skill . name ) )
46+ const installed = new Map ( ( listed ?? [ ] ) . map ( ( skill ) => [ skill . name , skill ] ) )
3847
3948 return (
4049 < >
@@ -48,53 +57,159 @@ export function WorkspaceCapabilities({
4857 leading = { < Icon name = 'I044' /> }
4958 title = { entry . name }
5059 detail = { entry . description }
51- textMetadata = { added . has ( entry . name ) ? 'Added' : undefined }
52- disabled = { ! listed || added . has ( entry . name ) }
53- onOpen = { ( ) => setAdding ( { skill : entry , instructions : entry . content } ) }
60+ textMetadata = { installed . has ( entry . name ) ? 'Added' : undefined }
61+ onOpen = { ( ) => setOpen ( { skill : entry , installed : installed . get ( entry . name ) } ) }
5462 />
5563 ) ) }
5664 </ ul >
5765 </ section >
5866 ) }
5967 < IntegrationTemplates templates = { templates } />
60- { adding && (
68+ { open && (
69+ < SuggestedSkillEditor
70+ key = { open . installed ?. id ?? open . skill . name }
71+ workspaceId = { workspaceId }
72+ skill = { open . skill }
73+ installed = { open . installed }
74+ onClose = { ( ) => setOpen ( null ) }
75+ />
76+ ) }
77+ </ >
78+ )
79+ }
80+
81+ interface SuggestedSkillEditorProps {
82+ workspaceId : string
83+ skill : CatalogSkill
84+ /** The workspace's copy of the suggestion, which the editor saves to or removes. */
85+ installed ?: SkillDefinition
86+ onClose : ( ) => void
87+ }
88+
89+ /** A suggested skill's editable fields before adding it, or its added copy's. */
90+ function SuggestedSkillEditor ( {
91+ workspaceId,
92+ skill,
93+ installed,
94+ onClose,
95+ } : SuggestedSkillEditorProps ) {
96+ const createSkill = useCreateSkill ( )
97+ const updateSkill = useUpdateSkill ( )
98+ const deleteSkill = useDeleteSkill ( )
99+ const saved : SkillDraft = installed
100+ ? { name : installed . name , description : installed . description , content : installed . content }
101+ : { name : skill . name , description : skill . description , content : skill . content }
102+ const [ draft , setDraft ] = useState ( saved )
103+ const [ removing , setRemoving ] = useState ( false )
104+ const editable = ! installed || ( installed . canEdit && ! installed . readOnly )
105+ const dirty =
106+ draft . name !== saved . name ||
107+ draft . description !== saved . description ||
108+ draft . content !== saved . content
109+ const complete = Boolean ( draft . name . trim ( ) && draft . description . trim ( ) && draft . content . trim ( ) )
110+
111+ async function save ( ) {
112+ const next = {
113+ name : draft . name . trim ( ) ,
114+ description : draft . description . trim ( ) ,
115+ content : draft . content . trim ( ) ,
116+ }
117+ try {
118+ if ( ! installed ) {
119+ await createSkill . mutateAsync ( { workspaceId, skill : next } )
120+ return
121+ }
122+ await updateSkill . mutateAsync ( {
123+ workspaceId,
124+ skillId : installed . id ,
125+ updates : {
126+ ...( next . name !== installed . name ? { name : next . name } : { } ) ,
127+ ...( next . description !== installed . description ? { description : next . description } : { } ) ,
128+ ...( next . content !== installed . content ? { content : next . content } : { } ) ,
129+ } ,
130+ } )
131+ } catch ( error ) {
132+ throw new Error (
133+ isSkillNameConflictError ( error )
134+ ? `"${ next . name } " is already in this workspace`
135+ : getErrorMessage ( error , `Could not save "${ next . name } ". Try again.` )
136+ )
137+ }
138+ }
139+
140+ return (
141+ < >
142+ < Modal
143+ open
144+ title = { skill . name }
145+ confirmLabel = { installed ? 'Save changes' : 'Add to Skills' }
146+ confirmDisabled = { ! editable || ! complete || Boolean ( installed && ! dirty ) }
147+ onOpenChange = { ( next ) => {
148+ if ( ! next ) onClose ( )
149+ } }
150+ onConfirm = { save }
151+ footerStartAction = {
152+ installed && editable
153+ ? {
154+ label : 'Remove from Skills' ,
155+ variant : 'quiet' ,
156+ destructive : true ,
157+ onClick : ( ) => setRemoving ( true ) ,
158+ }
159+ : undefined
160+ }
161+ >
162+ < TextField
163+ label = 'Name'
164+ placeholder = 'Skill name'
165+ name = 'name'
166+ value = { draft . name }
167+ onChange = { ( name ) => setDraft ( { ...draft , name } ) }
168+ viewOnly = { ! editable }
169+ required
170+ maxLength = { 64 }
171+ />
172+ < TextAreaField
173+ label = 'Description'
174+ placeholder = 'Describe what this is for'
175+ name = 'description'
176+ value = { draft . description }
177+ onChange = { ( description ) => setDraft ( { ...draft , description } ) }
178+ viewOnly = { ! editable }
179+ required
180+ maxLength = { SKILL_DESCRIPTION_MAX_LENGTH }
181+ />
182+ < TextAreaField
183+ label = 'Instructions'
184+ placeholder = 'Describe what this skill should do'
185+ name = 'instructions'
186+ defaultSize = 'large'
187+ value = { draft . content }
188+ onChange = { ( content ) => setDraft ( { ...draft , content } ) }
189+ viewOnly = { ! editable }
190+ required
191+ />
192+ </ Modal >
193+ { removing && installed && (
61194 < Modal
62195 open
63- onOpenChange = { ( open ) => {
64- if ( ! open ) setAdding ( null )
196+ title = 'Remove from Skills?'
197+ description = { `This removes ${ installed . name } from your integrations.` }
198+ variant = 'confirm'
199+ destructive
200+ confirmLabel = 'Remove from Skills'
201+ onOpenChange = { ( next ) => {
202+ if ( ! next ) setRemoving ( false )
65203 } }
66- title = { adding . skill . name }
67- confirmLabel = 'Add to Skills'
68- confirmDisabled = { ! adding . instructions . trim ( ) }
69204 onConfirm = { async ( ) => {
70205 try {
71- await createSkill . mutateAsync ( {
72- workspaceId,
73- skill : {
74- name : adding . skill . name ,
75- description : adding . skill . description ,
76- content : adding . instructions . trim ( ) ,
77- } ,
78- } )
206+ await deleteSkill . mutateAsync ( { workspaceId, skillId : installed . id } )
79207 } catch ( error ) {
80- throw new Error (
81- isSkillNameConflictError ( error )
82- ? `"${ adding . skill . name } " is already in this workspace`
83- : getErrorMessage ( error , `Could not add "${ adding . skill . name } ". Try again.` )
84- )
208+ throw new Error ( getErrorMessage ( error , `Could not remove "${ installed . name } ".` ) )
85209 }
210+ onClose ( )
86211 } }
87- >
88- < DetailsField label = 'Description' > { adding . skill . description } </ DetailsField >
89- < TextAreaField
90- label = 'Instructions'
91- placeholder = 'What should the agent do with this skill?'
92- value = { adding . instructions }
93- onChange = { ( instructions ) => setAdding ( { ...adding , instructions } ) }
94- defaultSize = 'large'
95- required
96- />
97- </ Modal >
212+ />
98213 ) }
99214 </ >
100215 )
0 commit comments