Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 3.8k
feat(integrations): suggest curated skills per integration with one-click add#4912
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
1feeb8cc85de13e3e235268ea5a0e448e6304ca966File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { SkillTile } from './skill-tile' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { AgentSkillsIcon } from '@/components/icons' | ||
| /** | ||
| * Square tile bearing the agent-skills glyph. Shared chrome for any surface | ||
| * that lists a skill (the Skills page and integration detail pages) so the two | ||
| * do not drift. | ||
| */ | ||
| export function SkillTile() { | ||
| return ( | ||
| <div className='size-9 flex-shrink-0'> | ||
| <div className='flex size-full items-center justify-center rounded-xl border border-[var(--border-1)] bg-[var(--surface-4)] dark:bg-[var(--surface-5)]'> | ||
| <AgentSkillsIcon className='size-5 text-[var(--text-icon)]' /> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| 'use client' | ||
| import { useMemo, useRef, useState } from 'react' | ||
| import { Check, Plus } from 'lucide-react' | ||
| import { usePostHog } from 'posthog-js/react' | ||
| import { Chip, toast } from '@/components/emcn' | ||
| import { captureEvent } from '@/lib/posthog/client' | ||
| import { SkillTile } from '@/app/workspace/[workspaceId]/components' | ||
| import type { SuggestedSkill } from '@/blocks/types' | ||
| import { useCreateSkill, useSkills } from '@/hooks/queries/skills' | ||
| interface IntegrationSkillsSectionProps { | ||
| skills: readonly SuggestedSkill[] | ||
| workspaceId: string | ||
| integrationType: string | ||
| } | ||
| interface SkillRowProps { | ||
| skill: SuggestedSkill | ||
| added: boolean | ||
| pending: boolean | ||
| disabled: boolean | ||
| onAdd: () => void | ||
| } | ||
| function SkillRow({ skill, added, pending, disabled, onAdd }: SkillRowProps) { | ||
| return ( | ||
| <div className='flex items-center gap-2.5 rounded-lg p-2'> | ||
| <SkillTile /> | ||
| <div className='flex min-w-0 flex-1 flex-col'> | ||
| <span className='truncate text-[14px] text-[var(--text-body)]'>{skill.name}</span> | ||
| <span className='truncate text-[12px] text-[var(--text-muted)]'>{skill.description}</span> | ||
| </div> | ||
| {added ? ( | ||
| <Chip variant='filled' leftIcon={Check} disabled flush> | ||
| Added | ||
| </Chip> | ||
| ) : ( | ||
| <Chip variant='primary' leftIcon={Plus} onClick={onAdd} disabled={disabled} flush> | ||
| {pending ? 'Adding...' : 'Add'} | ||
| </Chip> | ||
| )} | ||
| </div> | ||
| ) | ||
| } | ||
| /** | ||
| * Curated, research-backed skills for an integration. Each row adds the skill | ||
| * to the workspace via the same `useCreateSkill` mutation the Skills page uses; | ||
| * `useSkills` is the single source of truth for the "Added" state, so a skill | ||
| * removed elsewhere correctly reverts to "Add". | ||
| */ | ||
| export function IntegrationSkillsSection({ | ||
| skills, | ||
| workspaceId, | ||
| integrationType, | ||
| }: IntegrationSkillsSectionProps) { | ||
| const posthog = usePostHog() | ||
| const { data: existingSkills = [], isPending, isPlaceholderData } = useSkills(workspaceId) | ||
| const createSkill = useCreateSkill() | ||
| const skillsReady = !isPending && !isPlaceholderData | ||
| const [pendingNames, setPendingNames] = useState<ReadonlySet<string>>(new Set()) | ||
| const inFlightRef = useRef<Set<string>>(new Set()) | ||
| const existingNames = useMemo(() => new Set(existingSkills.map((s) => s.name)), [existingSkills]) | ||
| const handleAdd = async (skill: SuggestedSkill, position: number) => { | ||
| if (inFlightRef.current.has(skill.name)) return | ||
| inFlightRef.current.add(skill.name) | ||
| setPendingNames((prev) => new Set(prev).add(skill.name)) | ||
| try { | ||
| await createSkill.mutateAsync({ workspaceId, skill }) | ||
| captureEvent(posthog, 'integration_skill_added', { | ||
| workspace_id: workspaceId, | ||
| integration_type: integrationType, | ||
| skill_name: skill.name, | ||
| position, | ||
| skill_count: skills.length, | ||
| }) | ||
| } catch { | ||
| toast.error(`Failed to add "${skill.name}" — please try again`) | ||
| } finally { | ||
| inFlightRef.current.delete(skill.name) | ||
| setPendingNames((prev) => { | ||
| const next = new Set(prev) | ||
| next.delete(skill.name) | ||
| return next | ||
| }) | ||
| } | ||
| } | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| return ( | ||
| <section className='flex flex-col'> | ||
| <span className='pl-0.5 text-[var(--text-muted)] text-small'>Skills</span> | ||
| <div className='mt-[9px] mb-3 h-px bg-[var(--border)]' /> | ||
| <div className='-mx-2 flex flex-col gap-y-0.5'> | ||
| {skills.map((skill, index) => ( | ||
| <SkillRow | ||
| key={skill.name} | ||
| skill={skill} | ||
| added={skillsReady && existingNames.has(skill.name)} | ||
| pending={pendingNames.has(skill.name)} | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| disabled={pendingNames.has(skill.name) || !skillsReady} | ||
| onAdd={() => handleAdd(skill, index)} | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| /> | ||
| ))} | ||
| </div> | ||
| </section> | ||
| ) | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.