import { useEffect, useMemo, useRef, useState } from "react"; import type { SessionEvent, SkillRecord } from "@opengeni/sdk"; import { OpenGeniApiError } from "@opengeni/sdk/browser"; import type { SkillReviewReference } from "@opengeni/contracts"; import type { AppContextValue } from "../../app-context"; import { Button } from "../../../ui/button"; import { userErrorText } from "../../../ui/api-error"; import { hasAccountPermission, hasWorkspacePermission } from "../../lib/permissions"; import { sessionSkillReviews } from "../../lib/session-skill-reviews"; type ReviewReference = Omit; export function SessionSkillReviews({ context, workspaceId, sessionId, events, }: { context: AppContextValue; workspaceId: string; sessionId: string; events: readonly SessionEvent[]; }) { const receipts = useMemo(() => sessionSkillReviews(events), [events]); const receiptIdentity = JSON.stringify(receipts); const [catalog, setCatalog] = useState([]); const [cursor, setCursor] = useState(null); const [error, setError] = useState(false); const [reload, setReload] = useState(0); const [loading, setLoading] = useState(false); const [pagination, setPagination] = useState<{ identity: string; cursor: string }>(); // A new receipt invalidates every page of the previous discovery snapshot. const page = pagination?.identity === receiptIdentity ? pagination.cursor : undefined; useEffect(() => { let live = true; setLoading(true); setError(false); void context.client .listWorkspaceSkills(workspaceId, { sessionId, limit: 100, ...(page ? { cursor: page } : {}), }) .then((result) => { if (!live) return; const pending = result.skills.flatMap((skill) => skill.pendingRevisionIds.map((revisionId) => ({ skillId: skill.id, revisionId, expectedRevisionId: skill.activeRevisionId, expectedScopeVersion: skill.scopeVersion, ...(skill.removalOperationId ? { removalOperationId: skill.removalOperationId } : {}), })), ); setCatalog((previous) => (page ? [...previous, ...pending] : pending)); setCursor(result.nextCursor); }) .catch(() => { if (live) setError(true); }) .finally(() => { if (live) setLoading(false); }); return () => { live = false; }; }, [context.client, workspaceId, sessionId, receiptIdentity, reload, page]); useEffect(() => { const refresh = () => { setPagination(undefined); setReload((value) => value + 1); }; window.addEventListener("focus", refresh); return () => window.removeEventListener("focus", refresh); }, []); const reviews = [ ...new Map(catalog.map((reference) => [reference.revisionId, reference])).values(), ]; return ( <> {reviews.map((reference) => ( ))} {error ? (

Could not load pending Skill reviews.{" "}

) : null} {cursor ? ( ) : null} ); } function SkillReview({ context, workspaceId, reference, }: { context: AppContextValue; workspaceId: string; reference: ReviewReference; }) { const [record, setRecord] = useState(null); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const [settled, setSettled] = useState(false); const [reload, setReload] = useState(0); // Uncertain network outcomes must replay the same immutable operation. const operationId = useRef(null); const { client } = context; useEffect(() => { let live = true; setRecord(null); setError(null); setSettled(false); void client .readWorkspaceSkill(workspaceId, reference.skillId, reference.revisionId) .then((value) => { if (!live) return; // Never silently substitute a newer head, scope, or removal proposal. if ( value.id !== reference.skillId || value.revisionId !== reference.revisionId || value.scopeVersion !== reference.expectedScopeVersion || value.activeRevisionId !== reference.expectedRevisionId || (value.removalOperationId ?? null) !== (reference.removalOperationId ?? null) || !value.pendingRevisionIds.includes(reference.revisionId) ) { setSettled(true); return; } setRecord(value); }) .catch((reason) => { if (!live) return; if (reason instanceof OpenGeniApiError && reason.status === 404) setSettled(true); else setError("Could not load this Skill review."); }); return () => { live = false; }; }, [ client, workspaceId, reference.skillId, reference.revisionId, reference.expectedRevisionId, reference.expectedScopeVersion, reference.removalOperationId, reload, ]); if (settled) return null; const grant = context.accessContext.workspaceGrants.find( (entry) => entry.workspaceId === workspaceId, ); const human = grant?.principalKind === "human_session" || context.authSession != null; const canManage = Boolean( record && human && (record.scope === "user" || (record.scope === "workspace" && hasWorkspacePermission(context.accessContext, workspaceId, "documents:manage")) || (record.scope === "organization" && grant && hasAccountPermission(context.accessContext, grant.accountId, "account:admin"))), ); async function approve() { if (!record || !canManage || busy) return; setBusy(true); setError(null); try { const receipt = await client.approveWorkspaceSkill(workspaceId, reference.skillId, { operationId: (operationId.current ??= crypto.randomUUID()), revisionId: reference.revisionId, expectedRevisionId: reference.expectedRevisionId, expectedScopeVersion: reference.expectedScopeVersion, ...(reference.removalOperationId ? { removalOperationId: reference.removalOperationId } : {}), reason: "Approve exact Skill revision from its session", }); if (receipt.outcome !== "applied") throw new Error("Reload its review, then try again."); setSettled(true); } catch (reason) { setError(`Couldn't approve this Skill. ${userErrorText(reason)}`); } finally { setBusy(false); } } return (
Review {record?.title || "pending Skill"} {record ? (
{reference.removalOperationId ? (

Permanently deletes this Skill and all stored revisions. This cannot be undone; conversations remain unchanged.

) : (

Approval activates these exact files.

)} {record.files.map((file) => (
{file.path}
                  {file.content}
                
))} {canManage ? ( ) : (

You do not have permission to approve this Skill.

)}
) : !error ? (

Loading Skill…

) : null}
{error ? (
{error}{" "}
) : null}
); }