/** A shared invitation with private visitor drafts and an originator-only inbox. */ import { computed, handler, NAME, pattern, type PerSpace, type PerUser, type ReadonlyCell, type Stream, UI, type VNode, Writable, } 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"; /** The invitation reads a published shelf in its containing space. */ export interface InvitationInput { originatorProfile: ReadonlyCell; library: ReadonlyCell; } /** A title and author snapshot chosen by the visitor for explicit sharing. */ export interface SelectedBooks { books: Book[]; } /** A shelf snapshot released for readers in the invitation's space. */ export interface LibrarySlot { value?: LibrarySeed; } /** Private state is scoped independently from the shared invitation. */ export interface SharedInvitationOutput { [NAME]: string; [UI]: PerUser; originator: OriginatorIdentity; originatorProfile: ReadonlyCell; library: ReadonlyCell; 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 !== ""), }); }); /** 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>>; libraryCell: PerUser>; received: PerUser[]>>; recommended: PerUser[]>>; selected: PerUser>; review: PerUser>; clearSelection: PerUser>; }, Pick >( ( { originatorProfile, originator, libraryCell, 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: libraryCell.key("value").get(), })); const suggestions = computed( (): PerUser<{ value?: CandidateAgentOutput }> => { const published = libraryCell.key("value").get(); if (!published || isOwner !== false) return {}; return { value: SuggestBooks.asScope("user")({ books: libraryCell.key("value", "books"), favoriteAuthors: libraryCell.key("value", "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, library }) => { const originator = new Writable.perSpace({}); const received = new Writable.perSpace[]>>( [], ); const recommended = new Writable.perUser< ReaderPrivate[]> >([]); const selected = new Writable.perUser>({ books: [], }); const review = reviewSelection({ selected }); const clear = clearSelection({ selected }); const reader = ReaderInvitation.asScope("user")({ originatorProfile, originator, libraryCell: library, received, recommended, selected, review, clearSelection: clear, }); return { [NAME]: "Recommend me a book", [UI]: reader[UI], originator, originatorProfile, library, received, recommended, selected, review, clearSelection: clear, }; }, );