1import { COMPOSER_CONTEXT_KINDS, type KnownComposerContextKind } from "@t3tools/contracts";
2
3export interface ContextPresentationCapability {
4 details: "none" | "tooltip" | "popover";
5 expanded: "none" | "inline-block" | "modal";
6 defaultDraftView: "compact" | "expanded";
7}
8
9export interface ContextPresentationDefinition {
10 kind: KnownComposerContextKind;
11 capabilities: ContextPresentationCapability;
12}
13
14const DEFINITIONS = [
15 {
16 kind: "image",
17 capabilities: { details: "tooltip", expanded: "modal", defaultDraftView: "compact" },
18 },
19 {
20 kind: "file",
21 capabilities: { details: "tooltip", expanded: "modal", defaultDraftView: "compact" },
22 },
23 {
24 kind: "terminal",
25 capabilities: { details: "popover", expanded: "none", defaultDraftView: "compact" },
26 },
27 {
28 kind: "element",
29 capabilities: { details: "popover", expanded: "none", defaultDraftView: "compact" },
30 },
31 {
32 kind: "preview-annotation",
33 capabilities: { details: "popover", expanded: "modal", defaultDraftView: "compact" },
34 },
35 {
36 kind: "review-comment",
37 capabilities: { details: "popover", expanded: "none", defaultDraftView: "compact" },
38 },
39 {
40 kind: "mention",
41 capabilities: { details: "none", expanded: "none", defaultDraftView: "compact" },
42 },
43 {
44 kind: "skill",
45 capabilities: { details: "tooltip", expanded: "none", defaultDraftView: "compact" },
46 },
47 {
48 kind: "thread",
49 capabilities: { details: "tooltip", expanded: "none", defaultDraftView: "compact" },
50 },
51] as const satisfies ReadonlyArray<ContextPresentationDefinition>;
52
53function buildDefinitionRegistry(
54 definitions: ReadonlyArray<ContextPresentationDefinition>,
55): ReadonlyMap<KnownComposerContextKind, ContextPresentationDefinition> {
56 const registry = new Map<KnownComposerContextKind, ContextPresentationDefinition>();
57 for (const definition of definitions) {
58 if (registry.has(definition.kind)) {
59 throw new Error(`Duplicate context presentation definition: ${definition.kind}`);
60 }
61 registry.set(definition.kind, definition);
62 }
63 for (const kind of COMPOSER_CONTEXT_KINDS) {
64 if (!registry.has(kind)) {
65 throw new Error(`Missing context presentation definition: ${kind}`);
66 }
67 }
68 return registry;
69}
70
71export const CONTEXT_PRESENTATION_DEFINITIONS = buildDefinitionRegistry(DEFINITIONS);
72
73export function contextPresentationDefinition(
74 kind: KnownComposerContextKind,
75): ContextPresentationDefinition {
76 return CONTEXT_PRESENTATION_DEFINITIONS.get(kind)!;
77}
78
79export interface ContextPresentationHandler<TRecord, TRenderContext, TResult> {
80 kind: KnownComposerContextKind;
81 canRender?: (record: TRecord, context: TRenderContext) => boolean;
82 render: (
83 record: TRecord,
84 context: TRenderContext,
85 definition: ContextPresentationDefinition,
86 ) => TResult;
87}
88
89export interface ContextPresentationRegistry<TRecord, TRenderContext, TResult> {
90 definition(kind: KnownComposerContextKind): ContextPresentationDefinition;
91 render(kind: string, record: TRecord | undefined, context: TRenderContext): TResult;
92}
93
94/**
95 * Creates one checked dispatcher for a rendering surface. Definitions are shared; renderers stay
96 * surface-specific so web composer and transcript presentation can differ without drifting on
97 * capability semantics.
98 */
99export function createContextPresentationRegistry<TRecord, TRenderContext, TResult>(options: {
100 handlers: ReadonlyArray<ContextPresentationHandler<TRecord, TRenderContext, TResult>>;
101 requiredKinds?: ReadonlyArray<KnownComposerContextKind>;
102 fallback: (kind: string, record: TRecord | undefined, context: TRenderContext) => TResult;
103}): ContextPresentationRegistry<TRecord, TRenderContext, TResult> {
104 const handlers = new Map<
105 KnownComposerContextKind,
106 ContextPresentationHandler<TRecord, TRenderContext, TResult>
107 >();
108 for (const handler of options.handlers) {
109 if (handlers.has(handler.kind)) {
110 throw new Error(`Duplicate context presentation handler: ${handler.kind}`);
111 }
112 handlers.set(handler.kind, handler);
113 }
114 for (const kind of options.requiredKinds ?? []) {
115 if (!handlers.has(kind)) {
116 throw new Error(`Missing context presentation handler: ${kind}`);
117 }
118 }
119
120 return {
121 definition: contextPresentationDefinition,
122 render(kind, record, context) {
123 const handler = handlers.get(kind as KnownComposerContextKind);
124 if (!handler || record === undefined || handler.canRender?.(record, context) === false) {
125 return options.fallback(kind, record, context);
126 }
127 return handler.render(record, context, contextPresentationDefinition(handler.kind));
128 },
129 };
130}