Skip to content

Commit 373b6dc

Browse files
committed
fix(platform): give live suggested skills Emir's skill editor
A live app page's suggested skill opened a Description detail and an instructions box, and an added one went disabled. It opens his editor again: the skill's name as the title, editable Name, Description and Instructions with his placeholders, and Add to Skills. An added skill stays openable, as on his page, with Save changes and a confirmed Remove from Skills, through the workspace's own skill operations.
1 parent 8176795 commit 373b6dc

1 file changed

Lines changed: 156 additions & 41 deletions

File tree

‎apps/sim/app/(product)/o/[organizationId]/integrations/_components/integration-capabilities/workspace-capabilities.tsx‎

Lines changed: 156 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,25 @@
22

33
import { useState } from 'react'
44
import { 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'
76
import { Icon } from '@sim/emcn/icon'
87
import { Modal } from '@sim/emcn/modal'
98
import { getErrorMessage } from '@sim/utils/errors'
109
import { IntegrationTemplates } from '@/app/(product)/o/[organizationId]/integrations/_components/integration-capabilities/integration-capabilities'
1110
import 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'
1320
import type { IntegrationCatalogEntry } from '@/platform/lib/integrations/catalog-model'
1421

1522
type CatalogSkill = IntegrationCatalogEntry['skills'][number]
23+
type SkillDraft = { name: string; description: string; content: string }
1624

1725
interface 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
*/
2836
export 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

Comments
 (0)