// Renders the real page HTML with representative fixtures so the design can be looked at, not // guessed at. The render functions are pure, so this needs no worker, no auth and no network. // // bun run preview # writes .preview/*.html // bun run preview --shot # ...and screenshots them with chromium import {mkdirSync, writeFileSync} from 'node:fs' import {join} from 'node:path' import {renderDebugPage, type DebugState} from '../src/render/debug' import {renderPage} from '../src/render/page' import type {ActorCandidate} from '../src/candidates' import type {AppState, ManagementTarget, Recommendation, StoredFeature, UserList} from '../src/types' const OUT = join(import.meta.dir, '..', '.preview') function target(overrides: Partial = {}): ManagementTarget { return { actorDid: 'did:plc:a', actorHandle: 'a.example', actorName: 'Ay', sourceUris: [], currentUnreadCount: 0, recent24hCount: 0, recent7dCount: 0, allTimeCount: 0, reasons: {}, subscriptionPosts: false, subscriptionReplies: false, muted: false, blocked: false, examples: [], ...overrides, } } function recommendation(overrides: Partial = {}): Recommendation { return { description: 'Quiet likes from strangers', context: '', why: 'Likes are most of what reaches this account, and they come from people you do not follow. Limiting them to follows keeps the ones you care about.', code: 'async function run(agent, ctx) {\n await agent.app.bsky.notification.putPreferencesV2({\n like: {include: "follows", list: true, push: false}\n })\n}', sourceUris: [], actorDids: [], actors: [], evidence: [], ...overrides, } } const state: AppState = { activeAccount: {did: 'did:plc:me', handle: 'zzstoatzz.io', avatar: 'https://placehold.co/88/1b2735/7bb4ff/png?text=z'}, accounts: [ {did: 'did:plc:me', handle: 'zzstoatzz.io', avatar: 'https://placehold.co/88/1b2735/7bb4ff/png?text=z'}, {did: 'did:plc:2', handle: 'plyr.fm', avatar: 'https://placehold.co/88/101820/5ea3ff/png?text=p'}, {did: 'did:plc:3', handle: 'zat.dev', avatar: 'https://placehold.co/88/1c3326/9fd6a8/png?text=z'}, {did: 'did:plc:4', handle: 'find-bufo.com', avatar: 'https://placehold.co/88/2b2413/e0b23d/png?text=b'}, {did: 'did:plc:5', handle: 'prefect.io', avatar: 'https://placehold.co/88/171b2b/8f9bff/png?text=P'}, {did: 'did:plc:6', handle: 'waow.tech', needsReconnect: true}, ], canManageAccounts: true, isAdmin: true, groupNotifications: [ { sequence: 3, accountDid: 'did:plc:me', accountHandle: 'zzstoatzz.io', indexedAt: new Date(Date.now() - 3 * 60_000).toISOString(), observedAt: new Date().toISOString(), notification: { uri: 'at://did:plc:phi/app.bsky.feed.post/1', reason: 'reply', authorDid: 'did:plc:phi', authorHandle: 'phi.zzstoatzz.io', authorName: 'ϕ', indexedAt: new Date(Date.now() - 3 * 60_000).toISOString(), isRead: false, text: 'the labeler adapter landed — trust tiers are wired through the candidate ranker now', isReply: true, }, }, { sequence: 2, accountDid: 'did:plc:2', accountHandle: 'plyr.fm', indexedAt: new Date(Date.now() - 42 * 60_000).toISOString(), observedAt: new Date().toISOString(), notification: { uri: 'at://did:plc:x/app.bsky.feed.post/2', reason: 'like', authorDid: 'did:plc:x', authorHandle: 'brookie.blog', authorName: 'Brookie', indexedAt: new Date(Date.now() - 42 * 60_000).toISOString(), isRead: false, subjectText: 'shipped a thing', isReply: false, }, }, ], groupUnreadCount: 2, identityCard: { note: 'busy week, and @phi.zzstoatzz.io is most of it', statKeys: ['week', 'loudest', 'unread', 'subscriptions'], tone: 'busy', stats: [ {key: 'week', label: 'this week', value: '155', notable: true, iconName: 'bell'}, {key: 'loudest', label: 'most from', value: '@phi.zzstoatzz.io', notable: true, labelFirst: true}, {key: 'unread', label: 'unread now', value: '2'}, {key: 'subscriptions', label: 'subscribed', value: '6', iconName: 'subscription', detail: '@phi.zzstoatzz.io, @doodl.waow.tech, @zat.dev, @plyr.fm, @bufo.uk, @find-bufo.com'}, {key: 'muted', label: 'muted', value: '1', iconName: 'bellOff', detail: '@loud.example'}, ], entities: [{handle: 'phi.zzstoatzz.io', displayName: 'ϕ', relationship: 'subscribed'}], }, unreadCount: 2, recommendations: [ recommendation(), recommendation({ description: 'Turn off doodl post subscription', context: 'doodl', why: 'doodl has a posts subscription on but has not sent anything in the past week.', actorDids: ['did:plc:d'], actors: [{did: 'did:plc:d', handle: 'doodl.waow.tech', name: 'doodl', profileUrl: 'https://bsky.app/profile/doodl.waow.tech'}], code: 'async function run(agent, ctx) {\n await agent.app.bsky.notification.putActivitySubscription({subject: "did:plc:d", activitySubscription: {post: false, reply: false}})\n}', }), ], currentSubscriptions: [ target({actorHandle: 'phi.zzstoatzz.io', actorName: 'ϕ', subscriptionPosts: true, subscriptionReplies: true}), target({actorDid: 'did:plc:d', actorHandle: 'doodl.waow.tech', actorName: 'doodl', subscriptionPosts: true}), ], currentBlocks: [], placeholder: 'e.g. turn off replies from @phi.zzstoatzz.io', recommendationPrompt: 'system prompt', recommendationGuidance: '', customRecommendationPrompt: false, webhookIntegration: {configured: false, failureCount: 0, reasons: []}, queueState: 'ready', } // /me fixtures. The page is mostly derived from candidates, so the interesting axes are // "does this person have a rich evidence record" and "does the user keep any lists" — the // second one being the branch that renders an empty-state instead of rows. function candidate(overrides: Partial = {}): ActorCandidate { const evidence = { did: 'did:plc:a', handle: 'a.example', displayName: 'Ay', allTimeCount: 42, recent7dCount: 12, recent24hCount: 3, replyCount: 4, muted: false, blocked: false, follows: true, followedBy: true, mutual: true, subscribedPosts: true, subscribedReplies: false, labels: [], listCount: 1, ...overrides.evidence, } return { did: evidence.did, handle: evidence.handle, displayName: evidence.displayName, score: 30, reasons: ['3 notifications in the last 24h', 'mutual'], ...overrides, evidence, } } const meCandidates: ActorCandidate[] = [ candidate(), candidate({score: 22, evidence: {did: 'did:plc:phi', handle: 'phi.zzstoatzz.io', displayName: 'ϕ', mutual: false, follows: true, followedBy: false, recent7dCount: 55, recent24hCount: 9, replyCount: 1, labels: ['spam'], allTimeCount: 155, listCount: 0, blocked: false, muted: false, subscribedPosts: true, subscribedReplies: true}}), candidate({score: 8, evidence: {did: 'did:plc:d', handle: 'doodl.waow.tech', displayName: 'doodl', mutual: false, follows: false, followedBy: true, recent7dCount: 2, recent24hCount: 0, replyCount: 0, labels: [], allTimeCount: 9, listCount: 0, blocked: false, muted: false, subscribedPosts: true, subscribedReplies: false}}), ] const meLists: UserList[] = [ {uri: 'at://did:plc:self/app.bsky.graph.list/1', name: 'zig people', memberCount: 7}, {uri: 'at://did:plc:self/app.bsky.graph.list/2', name: 'atproto folks', memberCount: 23}, ] const meState: DebugState = { userDid: 'did:plc:self', unreadCount: 2, targets: state.currentSubscriptions, avatars: new Map(), // Mirrors the real shape that read as a contradiction: several subscribed labelers plus noti's // own default, which is why "checked" exceeds "subscribed" by exactly one. labelers: [ {did: 'did:plc:mod', name: 'bluesky moderation', tier: 'noti-default', subscribed: false}, {did: 'did:plc:l1', name: 'Bookmarks', tier: 'user-subscribed', subscribed: true}, {did: 'did:plc:l2', name: 'Alt Heroes', tier: 'user-subscribed', subscribed: true}, {did: 'did:plc:l3', name: 'Skywatch Blue', tier: 'user-subscribed', subscribed: true}, ], subscribedLabelers: [ {did: 'did:plc:l1', handle: 'bookmarks.example', name: 'Bookmarks', profileUrl: 'https://bsky.app/profile/bookmarks.example'}, {did: 'did:plc:l2', handle: 'alt.example', name: 'Alt Heroes', profileUrl: 'https://bsky.app/profile/alt.example'}, {did: 'did:plc:l3', handle: 'skywatch.example', name: 'Skywatch Blue', profileUrl: 'https://bsky.app/profile/skywatch.example'}, ], backfill: [ {collection: 'app.bsky.graph.block', status: 'complete', cursor: null, recordsSeen: 12, recordsWritten: 12, attemptCount: 1, nextAttemptAt: null, lastError: null}, {collection: 'app.bsky.graph.listitem', status: 'complete', cursor: '3lkzq7xk2ps2h', recordsSeen: 96, recordsWritten: 96, attemptCount: 1, nextAttemptAt: null, lastError: null}, {collection: 'app.bsky.graph.follow', status: 'error', cursor: null, recordsSeen: 210, recordsWritten: 180, attemptCount: 3, nextAttemptAt: new Date(Date.now() + 9 * 60_000).toISOString(), lastError: 'listRecords 502 from https://pds.example — upstream unavailable'}, ], parity: [ {featureKey: 'viewer.blocked', pdsCount: 14, appviewCount: 5, agreed: 5, onlyPds: Array.from({length: 9}, () => 'did:plc:x'), onlyAppview: [], inAgreement: true}, ], backfillReady: {ready: true, reasons: []}, candidates: meCandidates, candidateAvatars: new Map(), // Real provenance shapes: a contradiction between sources on the same key (so the shadowed row // renders), a multi-valued list membership keyed by list URI, and a labeler-sourced label. candidateFeatures: new Map([ [ 'did:plc:phi', [ {actorDid: 'did:plc:phi', key: 'viewer.follows', value: 'true', source: 'pds', sourceId: 'self-repo', observedAt: new Date(Date.now() - 3 * 3600_000).toISOString()}, {actorDid: 'did:plc:phi', key: 'viewer.follows', value: 'false', source: 'viewer', sourceId: 'bsky-appview-profile', observedAt: new Date(Date.now() - 5 * 60_000).toISOString()}, {actorDid: 'did:plc:phi', key: 'viewer.mutual', value: 'true', source: 'viewer', sourceId: 'bsky-appview-profile', observedAt: new Date(Date.now() - 5 * 60_000).toISOString()}, {actorDid: 'did:plc:phi', key: 'list.member', value: 'true', source: 'pds', sourceId: 'at://did:plc:self/app.bsky.graph.list/3labc', observedAt: new Date(Date.now() - 26 * 3600_000).toISOString()}, {actorDid: 'did:plc:phi', key: 'label.spam', value: '{"tier":"user-subscribed"}', source: 'labeler', sourceId: 'did:plc:ar7c4by46qjdydhdevvrndac', observedAt: new Date(Date.now() - 40 * 3600_000).toISOString(), expiresAt: new Date(Date.now() + 5 * 86400_000).toISOString()}, ], ], ]), lists: meLists, // Deliberately larger than the candidate list: these are totals over the whole history, and // the fixture should make it obvious that the strip is not summing the three balls below it. totals: {peopleNoticed: 48, pings7d: 315, directReplies: 167, mutuals: 22, labeled: 3}, advanced: false, } const pages = { home: renderPage(state), // Elicitation: the model asked one question and offered answers the user can click. clarify: renderPage({ ...state, placeholder: 'answer above, or type something else', proposal: { kind: 'clarify', request: 'mute alex', message: 'Which account do you mean? Two of the people you talk to go by “alex”.', examples: ['mute @alex.bsky.team', 'mute @alex.example.com'], }, // A conversation several turns deep, so the render is exercised as a thread rather than a pair. thread: [ {role: 'user', text: 'can you make a core atproto dev list', createdAt: ''}, {role: 'noti', text: 'I can create a public list called “Core atproto dev”. Applying the suggestion below will create it, empty — you can add people to it after that.', kind: 'action', createdAt: ''}, {role: 'user', text: 'mute alex', createdAt: ''}, {role: 'noti', text: 'Which account do you mean? Two of the people you talk to go by “alex”.', kind: 'clarify', createdAt: ''}, ], }), me: renderDebugPage(meState), 'me-advanced': renderDebugPage({...meState, advanced: true}), 'me-empty': renderDebugPage({...meState, lists: [], candidates: [], unreadCount: 0}), loading: renderPage({...state, queueState: 'loading', recommendations: []}), empty: renderPage({...state, recommendations: [], groupNotifications: [], groupUnreadCount: 0, unreadCount: 0, identityCard: { note: 'nothing needs attention', statKeys: ['unread'], tone: 'quiet', stats: [{key: 'unread', label: 'unread', value: '0'}, {key: 'age', label: 'on bluesky', value: '2 years'}], entities: [], }}), } satisfies Record mkdirSync(OUT, {recursive: true}) for (const [name, html] of Object.entries(pages)) { writeFileSync(join(OUT, `${name}.html`), html) console.log(`wrote .preview/${name}.html`) } if (process.argv.includes('--shot')) { const {chromium} = await import('playwright') const browser = await chromium.launch() for (const [name, width] of [['home', 1100], ['home-mobile', 420], ['loading', 1100], ['empty', 1100], ['me', 1100], ['me-mobile', 420], ['me-advanced', 1100], ['me-empty', 1100]] as const) { const file = name.replace('-mobile', '') const page = await browser.newPage({viewport: {width, height: 1000}, deviceScaleFactor: 2}) await page.goto(`file://${join(OUT, `${file}.html`)}`) await page.waitForTimeout(400) await page.screenshot({path: join(OUT, `${name}.png`), fullPage: true}) console.log(`shot .preview/${name}.png`) await page.close() } await browser.close() }