/** A shared invitation with private visitor drafts and an originator-only inbox. */ import { type Cfc, computed, type CurrentPrincipal, handler, NAME, pattern, type PerSpace, type PerUser, type ReadonlyCell, type RepresentsCurrentUser, type Stream, UI, type VNode, Writable, type WriteAuthorizedBy, } from "commonfabric"; import { type Book, type CandidateAgentOutput, type CandidateBooks, type LibrarySeed, prioritizeCandidateBooks, SuggestBooks, } from "./agents.tsx"; import { type OriginatorIdentity, type ReaderPrivate } from "./privacy.tsx"; import { InvitationView, type Profile } from "./views.tsx"; /** Published library and creator profile carried into the invitation. */ export interface InvitationInput { originatorProfile: ReadonlyCell; } /** A title and author snapshot chosen by the visitor for explicit sharing. */ export interface SelectedBooks { books: Book[]; } /** Published library pointer; the creator alone may replace it. */ export interface LibrarySlot { value?: ReadonlyCell; } /** Binds a host-approved library snapshot to this invitation. */ export const publishLibrary = handler<{ library: ReadonlyCell }, { library: Writable; }>((event, { library }) => library.set({ value: event.library })); /** Library pointer whose authenticated creator alone may publish a snapshot. */ export type PublishedLibrary = RepresentsCurrentUser< Cfc< WriteAuthorizedBy, { ownerPrincipal: CurrentPrincipal } > >; /** Private state is scoped independently from the shared invitation. */ export interface InvitationOutput { [NAME]: string; [UI]: PerUser; originator: OriginatorIdentity; originatorProfile: ReadonlyCell; library: PublishedLibrary; publish: Stream<{ library: ReadonlyCell }>; reviewedLibrary: PerUser>; publishReviewed: Stream; received: PerSpace[]>>; recommended: PerUser[]>>; selected: PerUser>; review: Stream; clearSelection: Stream; } /** Stages only the book fields the visitor has chosen to review. */ const reviewSelection = handler>; }>(({ books }, { selected }) => { selected.set({ books: books.map(({ title, author }) => ({ title: title.trim(), author: author.trim(), })).filter((book) => book.title !== ""), }); }); /** Publishes a reviewed shelf within the invitation's transaction space. */ const publishReviewedLibrary = handler>; publish: Stream<{ library: ReadonlyCell }>; }>((_, { reviewedLibrary, publish }) => { if (!reviewedLibrary.get()?.value) return; publish.send({ library: reviewedLibrary.get().value! }); reviewedLibrary.set({}); }); /** Clears the visitor's draft after the host commits its reviewed copy. */ const clearSelection = handler>; }>((_, { selected }) => { selected.set({ books: [] }); }); /** Per-reader computations and UI cannot share their derived private values. */ const ReaderInvitation = pattern< { originatorProfile: PerUser>; originator: PerUser>>; library: PerUser>; received: PerUser[]>>; recommended: PerUser[]>>; selected: PerUser>; review: PerUser>; clearSelection: PerUser>; }, Pick >( ( { originatorProfile, originator, library, received, recommended, selected, review, clearSelection, }, ) => { const ownerState = new Writable.perUser(null); const isOwner: PerUser | null = computed( (): PerUser | null => ownerState.get(), ); const reading = computed((): PerUser<{ value?: LibrarySeed }> => ({ value: library.get().value?.get(), })); const suggestions = computed( (): PerUser<{ value?: CandidateAgentOutput }> => { const published = library.get().value; if (!published || isOwner !== false) return {}; return { value: SuggestBooks.asScope("user")({ books: published.key("books"), favoriteAuthors: published.key("favoriteAuthors"), }), }; }, ); const hasSelected: PerUser = computed((): PerUser => (selected.get()?.books?.length ?? 0) > 0 ); const view = InvitationView.asScope("user")({ originator: originatorProfile, isOwner, originatorBooks: computed((): PerUser => reading.value?.books ?? [] ), favoriteAuthors: computed((): PerUser => reading.value?.favoriteAuthors ?? [] ), candidates: computed((): PerUser => prioritizeCandidateBooks( suggestions.value?.state.result?.books ?? [], reading.value, ) ), agentStatus: computed((): PerUser => suggestions.value?.state.pending ? "Finding books you might recommend…" : "" ), agentError: computed((): PerUser => suggestions.value?.state.error ?? "" ), receivedRecommendations: computed((): PerUser => { if (isOwner !== true) return []; return received.get().map((book) => book.get()); }), myRecommendations: computed((): PerUser => (recommended.get() ?? []).map((book) => book.get()) ), recommend: review, }); return { [UI]: ( {view} {hasSelected ? ( ) : null} ), }; }, ); export default pattern( ({ originatorProfile }) => { const originator = new Writable.perSpace({}); const library = new Writable.perSpace({}); const received = new Writable.perSpace[]>>( [], ); const reviewedLibrary = new Writable.perUser< LibrarySlot >({}); const recommended = new Writable.perUser< ReaderPrivate[]> >([]); const selected = new Writable.perUser>({ books: [], }); const publish = publishLibrary({ library }); const review = reviewSelection({ selected }); const clear = clearSelection({ selected }); const publishReviewed = publishReviewedLibrary({ reviewedLibrary, publish, }); const reader = ReaderInvitation.asScope("user")({ originatorProfile, originator, library, received, recommended, selected, review, clearSelection: clear, }); return { [NAME]: "Recommend me a book", [UI]: reader[UI], originator, originatorProfile, library, publish, reviewedLibrary, publishReviewed, received, recommended, selected, review, clearSelection: clear, }; }, );