"use client" import { useAuth } from "@lib/auth-context" import { cn } from "@lib/utils" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@ui/components/alert-dialog" import { Sheet, SheetContent, SheetDescription, SheetTitle, } from "@ui/components/sheet" import { FileUp, Loader2, Plus, RotateCcw } from "lucide-react" import { useEffect, useRef, useState } from "react" import { toast } from "sonner" import { type BrainSkill, useBrainSkills, useUploadBrainSkill, } from "@/hooks/use-brain-skills" import { dmSans125ClassName } from "@/lib/fonts" import { emptySkillDraft, parseSkillMarkdown, skillDraftForRole, type SkillDraft, type SkillScope, } from "./company-brain-skills/domain" import { SkillEditor } from "./company-brain-skills/skill-editor" import { SkillRow } from "./company-brain-skills/skill-row" type EditorTarget = | { mode: "existing"; skillId: string } | { mode: "new" | "upload"; key: number; draft: SkillDraft } type PendingIntent = { type: "close" } | { type: "open"; target: EditorTarget } type ScopeFilter = "all" | "org" | "personal" const SCOPE_HINTS: Record = { all: "Org-wide skills are shared with the workspace and managed by admins and owners. Personal skills stay private to you.", org: "Shared with the workspace and managed by admins and owners.", personal: "Private to you and managed only by you.", } const EMPTY_MESSAGES: Record = { all: "No skills yet. Create one or upload a .md playbook to get started.", org: "No org-wide skills yet.", personal: "No personal skills yet.", } function draftFromSkill(skill: BrainSkill): SkillDraft { return { name: skill.name, description: skill.description, body: skill.body, scope: skill.scope, } } function uploadedDraft( local: ReturnType, server: Partial, ): SkillDraft { return { name: typeof server.name === "string" ? server.name : local.name, description: typeof server.description === "string" ? server.description : local.description, body: typeof server.body === "string" ? server.body : local.body, scope: "personal", } } function errorMessage(error: unknown, fallback: string) { return error instanceof Error ? error.message : fallback } export default function CompanyBrainSkills({ onUnsavedChangesChange, }: { onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void }) { const { org, user } = useAuth() return ( ) } function CompanyBrainSkillsContent({ userId, onUnsavedChangesChange, }: { userId: string onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void }) { const skillsQuery = useBrainSkills() const upload = useUploadBrainSkill() const [editing, setEditing] = useState(null) const [editorDirty, setEditorDirty] = useState(false) const [pending, setPending] = useState(null) const [scopeFilter, setScopeFilter] = useState("all") const [uploadError, setUploadError] = useState(null) const fileInput = useRef(null) const draftKey = useRef(0) // An uploaded draft holds real content the user hasn't saved, so it guards too. const hasUnsavedChanges = editorDirty || editing?.mode === "upload" useEffect(() => { onUnsavedChangesChange?.(hasUnsavedChanges) }, [hasUnsavedChanges, onUnsavedChangesChange]) useEffect( () => () => { onUnsavedChangesChange?.(false) }, [onUnsavedChangesChange], ) const data = skillsQuery.data const viewerId = data?.viewerId ?? userId const isAdmin = data?.isAdmin ?? false const openEditor = (target: EditorTarget) => { setEditing(target) setEditorDirty(false) } const closeEditor = () => { setEditing(null) setEditorDirty(false) setPending(null) } const requestOpen = (target: EditorTarget) => { if (editing && hasUnsavedChanges) { setPending({ type: "open", target }) return } openEditor(target) } const requestClose = () => { if (editing && hasUnsavedChanges) { setPending({ type: "close" }) return } closeEditor() } const confirmPending = () => { if (!pending) return if (pending.type === "close") closeEditor() else openEditor(pending.target) setPending(null) } const openDraft = (kind: "new" | "upload", draft: SkillDraft) => { requestOpen({ mode: kind, key: draftKey.current++, draft: skillDraftForRole(draft, isAdmin), }) } const onUpload = async (file: File | undefined) => { if (!file || upload.isPending) return setUploadError(null) upload.reset() if (!file.name.toLowerCase().endsWith(".md")) { setUploadError("Choose a Markdown file ending in .md.") return } try { const content = await file.text() // Client parsing gives immediate, deterministic feedback; the harness runs // the same validation and remains authoritative before the draft opens. const local = parseSkillMarkdown(content) const result = await upload.mutateAsync(content) openDraft("upload", uploadedDraft(local, result.draft)) toast.success("Skill file is ready to review.") } catch (error) { setUploadError(errorMessage(error, "Couldn't read this skill file.")) } } const orgSkills = (data?.skills ?? []).filter( (skill) => skill.scope === "org", ) const personalSkills = (data?.skills ?? []).filter( (skill) => skill.scope === "personal" && skill.creatorUserId === viewerId, ) const visibleSkills = scopeFilter === "org" ? orgSkills : scopeFilter === "personal" ? personalSkills : [...orgSkills, ...personalSkills] const newSkillScope: SkillScope = scopeFilter === "org" && isAdmin ? "org" : "personal" const target = editing const editingSkill = target?.mode === "existing" ? ((data?.skills ?? []).find((skill) => skill.id === target.skillId) ?? null) : null const renderSkill = (skill: BrainSkill) => ( requestOpen({ mode: "existing", skillId: skill.id })} /> ) return (
{skillsQuery.isLoading ? (
Loading skills…
) : skillsQuery.isError ? (

{errorMessage(skillsQuery.error, "Couldn't load skills.")}

) : (
{( [ { id: "all", label: "All", count: orgSkills.length + personalSkills.length, }, { id: "org", label: "Org-wide", count: orgSkills.length }, { id: "personal", label: "Personal", count: personalSkills.length, }, ] as const ).map((tab) => ( ))}
{ const file = event.currentTarget.files?.[0] event.currentTarget.value = "" void onUpload(file) }} />

{SCOPE_HINTS[scopeFilter]} Skills created through Company Brain appear here automatically.

{uploadError ? (
{uploadError}
) : null} {visibleSkills.length > 0 ? (
{visibleSkills.map(renderSkill)}
) : (

{EMPTY_MESSAGES[scopeFilter]}

)}
)} { if (!open) requestClose() }} > Skill editor Edit the name, scope, description and Markdown instructions for this skill. {target?.mode === "existing" ? ( editingSkill ? ( ) : (

This skill is no longer available.

) ) : target ? ( ) : null}
{ if (!open) setPending(null) }} > Discard unsaved changes? {pending?.type === "open" ? "Opening another skill will discard the edits in this skill." : "Closing the editor will discard the edits in this skill."} Keep editing {pending?.type === "open" ? "Discard and open" : "Discard"}
) }