@affectively/aeon-pages 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +625 -0
  3. package/examples/basic/aeon.config.ts +39 -0
  4. package/examples/basic/components/Cursor.tsx +86 -0
  5. package/examples/basic/components/OfflineIndicator.tsx +103 -0
  6. package/examples/basic/components/PresenceBar.tsx +77 -0
  7. package/examples/basic/package.json +20 -0
  8. package/examples/basic/pages/index.tsx +80 -0
  9. package/package.json +101 -0
  10. package/packages/analytics/README.md +309 -0
  11. package/packages/analytics/build.ts +35 -0
  12. package/packages/analytics/package.json +50 -0
  13. package/packages/analytics/src/click-tracker.ts +368 -0
  14. package/packages/analytics/src/context-bridge.ts +319 -0
  15. package/packages/analytics/src/data-layer.ts +302 -0
  16. package/packages/analytics/src/gtm-loader.ts +239 -0
  17. package/packages/analytics/src/index.ts +230 -0
  18. package/packages/analytics/src/merkle-tree.ts +489 -0
  19. package/packages/analytics/src/provider.tsx +300 -0
  20. package/packages/analytics/src/types.ts +320 -0
  21. package/packages/analytics/src/use-analytics.ts +296 -0
  22. package/packages/analytics/tsconfig.json +19 -0
  23. package/packages/benchmarks/src/benchmark.test.ts +691 -0
  24. package/packages/cli/dist/index.js +61899 -0
  25. package/packages/cli/package.json +43 -0
  26. package/packages/cli/src/commands/build.test.ts +682 -0
  27. package/packages/cli/src/commands/build.ts +890 -0
  28. package/packages/cli/src/commands/dev.ts +473 -0
  29. package/packages/cli/src/commands/init.ts +409 -0
  30. package/packages/cli/src/commands/start.ts +297 -0
  31. package/packages/cli/src/index.ts +105 -0
  32. package/packages/directives/src/use-aeon.ts +272 -0
  33. package/packages/mcp-server/package.json +51 -0
  34. package/packages/mcp-server/src/index.ts +178 -0
  35. package/packages/mcp-server/src/resources.ts +346 -0
  36. package/packages/mcp-server/src/tools/index.ts +36 -0
  37. package/packages/mcp-server/src/tools/navigation.ts +545 -0
  38. package/packages/mcp-server/tsconfig.json +21 -0
  39. package/packages/react/package.json +40 -0
  40. package/packages/react/src/Link.tsx +388 -0
  41. package/packages/react/src/components/InstallPrompt.tsx +286 -0
  42. package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
  43. package/packages/react/src/components/PushNotifications.tsx +453 -0
  44. package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
  45. package/packages/react/src/hooks/useConflicts.ts +277 -0
  46. package/packages/react/src/hooks/useNetworkState.ts +209 -0
  47. package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
  48. package/packages/react/src/hooks/useServiceWorker.ts +278 -0
  49. package/packages/react/src/hooks.ts +195 -0
  50. package/packages/react/src/index.ts +151 -0
  51. package/packages/react/src/provider.tsx +467 -0
  52. package/packages/react/tsconfig.json +19 -0
  53. package/packages/runtime/README.md +399 -0
  54. package/packages/runtime/build.ts +48 -0
  55. package/packages/runtime/package.json +71 -0
  56. package/packages/runtime/schema.sql +40 -0
  57. package/packages/runtime/src/api-routes.ts +465 -0
  58. package/packages/runtime/src/benchmark.ts +171 -0
  59. package/packages/runtime/src/cache.ts +479 -0
  60. package/packages/runtime/src/durable-object.ts +1341 -0
  61. package/packages/runtime/src/index.ts +360 -0
  62. package/packages/runtime/src/navigation.test.ts +421 -0
  63. package/packages/runtime/src/navigation.ts +422 -0
  64. package/packages/runtime/src/nextjs-adapter.ts +272 -0
  65. package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
  66. package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
  67. package/packages/runtime/src/offline/encryption.test.ts +412 -0
  68. package/packages/runtime/src/offline/encryption.ts +397 -0
  69. package/packages/runtime/src/offline/types.ts +465 -0
  70. package/packages/runtime/src/predictor.ts +371 -0
  71. package/packages/runtime/src/registry.ts +351 -0
  72. package/packages/runtime/src/router/context-extractor.ts +661 -0
  73. package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
  74. package/packages/runtime/src/router/esi-control.ts +541 -0
  75. package/packages/runtime/src/router/esi-cyrano.ts +779 -0
  76. package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
  77. package/packages/runtime/src/router/esi-react.tsx +1065 -0
  78. package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
  79. package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
  80. package/packages/runtime/src/router/esi-translate.ts +503 -0
  81. package/packages/runtime/src/router/esi.ts +666 -0
  82. package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
  83. package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
  84. package/packages/runtime/src/router/index.ts +298 -0
  85. package/packages/runtime/src/router/merkle-capability.ts +473 -0
  86. package/packages/runtime/src/router/speculation.ts +451 -0
  87. package/packages/runtime/src/router/types.ts +630 -0
  88. package/packages/runtime/src/router.test.ts +470 -0
  89. package/packages/runtime/src/router.ts +302 -0
  90. package/packages/runtime/src/server.ts +481 -0
  91. package/packages/runtime/src/service-worker-push.ts +319 -0
  92. package/packages/runtime/src/service-worker.ts +553 -0
  93. package/packages/runtime/src/skeleton-hydrate.ts +237 -0
  94. package/packages/runtime/src/speculation.test.ts +389 -0
  95. package/packages/runtime/src/speculation.ts +486 -0
  96. package/packages/runtime/src/storage.test.ts +1297 -0
  97. package/packages/runtime/src/storage.ts +1048 -0
  98. package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
  99. package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
  100. package/packages/runtime/src/sync/coordinator.test.ts +608 -0
  101. package/packages/runtime/src/sync/coordinator.ts +596 -0
  102. package/packages/runtime/src/tree-compiler.ts +295 -0
  103. package/packages/runtime/src/types.ts +728 -0
  104. package/packages/runtime/src/worker.ts +327 -0
  105. package/packages/runtime/tsconfig.json +20 -0
  106. package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
  107. package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
  108. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
  109. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
  110. package/packages/runtime/wasm/package.json +21 -0
  111. package/packages/runtime/wrangler.toml +41 -0
  112. package/packages/runtime-wasm/Cargo.lock +436 -0
  113. package/packages/runtime-wasm/Cargo.toml +29 -0
  114. package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
  115. package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
  116. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
  117. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
  118. package/packages/runtime-wasm/pkg/package.json +21 -0
  119. package/packages/runtime-wasm/src/hydrate.rs +352 -0
  120. package/packages/runtime-wasm/src/lib.rs +191 -0
  121. package/packages/runtime-wasm/src/render.rs +629 -0
  122. package/packages/runtime-wasm/src/router.rs +298 -0
  123. package/packages/runtime-wasm/src/skeleton.rs +430 -0
  124. package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
@@ -0,0 +1,2053 @@
1
+ /**
2
+ * ESI Control Components (React)
3
+ *
4
+ * Control flow, Zod validation, and Presence-aware collaborative ESI.
5
+ *
6
+ * @example
7
+ * ```tsx
8
+ * // Conditional rendering based on inference
9
+ * <ESI.If
10
+ * prompt="Should we show a discount?"
11
+ * schema={z.object({ show: z.boolean(), reason: z.string() })}
12
+ * when={(r) => r.show}
13
+ * >
14
+ * <DiscountBanner />
15
+ * </ESI.If>
16
+ *
17
+ * // Presence-aware collaborative content
18
+ * <ESI.Collaborative
19
+ * schema={z.object({ summary: z.string(), highlights: z.array(z.string()) })}
20
+ * >
21
+ * Summarize this for {presence.length} viewers with roles: {roles}
22
+ * </ESI.Collaborative>
23
+ *
24
+ * // Structured output with Zod
25
+ * <ESI.Structured
26
+ * schema={z.object({
27
+ * sentiment: z.enum(['positive', 'negative', 'neutral']),
28
+ * topics: z.array(z.string()),
29
+ * })}
30
+ * >
31
+ * Analyze: {text}
32
+ * </ESI.Structured>
33
+ * ```
34
+ */
35
+
36
+ import {
37
+ createContext,
38
+ useContext,
39
+ useEffect,
40
+ useState,
41
+ useCallback,
42
+ useMemo,
43
+ type ReactNode,
44
+ type FC,
45
+ Children,
46
+ isValidElement,
47
+ type JSX,
48
+ } from 'react';
49
+ import type { ZodType, ZodTypeDef } from 'zod';
50
+ import type { ESIParams, ESIResult, UserContext } from './types';
51
+ import type { PresenceUser } from './types';
52
+ import {
53
+ parseWithSchema,
54
+ generateSchemaPrompt,
55
+ type ESICondition,
56
+ type ESISchemaResult,
57
+ } from './esi-control';
58
+ import { useESI } from './esi-react';
59
+
60
+ // ============================================================================
61
+ // Presence Context (from aeon-flux)
62
+ // ============================================================================
63
+
64
+ interface PresenceContextValue {
65
+ users: PresenceUser[];
66
+ localUser: PresenceUser | null;
67
+ }
68
+
69
+ // This would normally come from aeon-flux, but we define it here for ESI
70
+ const PresenceContext = createContext<PresenceContextValue | null>(null);
71
+
72
+ function usePresenceForESI(): PresenceContextValue {
73
+ const ctx = useContext(PresenceContext);
74
+ return ctx || { users: [], localUser: null };
75
+ }
76
+
77
+ // ============================================================================
78
+ // ESI.Structured - Zod-validated inference
79
+ // ============================================================================
80
+
81
+ export interface ESIStructuredProps<T> {
82
+ /** The prompt - can be children or explicit prop */
83
+ children?: ReactNode;
84
+ prompt?: string;
85
+
86
+ /** Zod schema for output validation */
87
+ schema: ZodType<T, ZodTypeDef, unknown>;
88
+
89
+ /** Render function for validated data */
90
+ render?: (data: T, meta: { cached: boolean; latencyMs: number }) => ReactNode;
91
+
92
+ /** Fallback if validation fails */
93
+ fallback?: ReactNode;
94
+
95
+ /** Loading state */
96
+ loading?: ReactNode;
97
+
98
+ /** Retry on validation failure */
99
+ retryOnFail?: boolean;
100
+
101
+ /** Max retries */
102
+ maxRetries?: number;
103
+
104
+ /** Additional inference params */
105
+ temperature?: number;
106
+ maxTokens?: number;
107
+ cacheTtl?: number;
108
+
109
+ /** Callbacks */
110
+ onSuccess?: (data: T) => void;
111
+ onValidationError?: (errors: string[], rawOutput: string) => void;
112
+ onError?: (error: string) => void;
113
+
114
+ /** Class name */
115
+ className?: string;
116
+ }
117
+
118
+ export function ESIStructured<T>({
119
+ children,
120
+ prompt,
121
+ schema,
122
+ render,
123
+ fallback,
124
+ loading = '...',
125
+ retryOnFail = false,
126
+ maxRetries = 2,
127
+ temperature,
128
+ maxTokens,
129
+ cacheTtl,
130
+ onSuccess,
131
+ onValidationError,
132
+ onError,
133
+ className,
134
+ }: ESIStructuredProps<T>): JSX.Element {
135
+ const { process, enabled } = useESI();
136
+ const [result, setResult] = useState<ESISchemaResult<T> | null>(null);
137
+ const [isLoading, setIsLoading] = useState(true);
138
+ const [retryCount, setRetryCount] = useState(0);
139
+
140
+ const promptText =
141
+ prompt ||
142
+ (typeof children === 'string' ? children : String(children || ''));
143
+ const fullPrompt = promptText + generateSchemaPrompt(schema);
144
+
145
+ useEffect(() => {
146
+ if (!enabled) {
147
+ setIsLoading(false);
148
+ return;
149
+ }
150
+
151
+ async function runInference() {
152
+ setIsLoading(true);
153
+
154
+ const directive = {
155
+ id: `esi-structured-${Date.now()}`,
156
+ params: {
157
+ model: 'llm' as const,
158
+ temperature,
159
+ maxTokens,
160
+ cacheTtl,
161
+ },
162
+ content: {
163
+ type: 'text' as const,
164
+ value: fullPrompt,
165
+ },
166
+ };
167
+
168
+ const inferenceResult = await process(directive);
169
+
170
+ if (!inferenceResult.success || !inferenceResult.output) {
171
+ setResult({
172
+ ...inferenceResult,
173
+ validationErrors: [inferenceResult.error || 'No output'],
174
+ });
175
+ onError?.(inferenceResult.error || 'Inference failed');
176
+ setIsLoading(false);
177
+ return;
178
+ }
179
+
180
+ const parseResult = parseWithSchema(inferenceResult.output, schema);
181
+
182
+ if (parseResult.success) {
183
+ const schemaResult: ESISchemaResult<T> = {
184
+ ...inferenceResult,
185
+ data: parseResult.data,
186
+ rawOutput: inferenceResult.output,
187
+ };
188
+ setResult(schemaResult);
189
+ onSuccess?.(parseResult.data);
190
+ } else {
191
+ // Validation failed
192
+ if (retryOnFail && retryCount < maxRetries) {
193
+ setRetryCount((c) => c + 1);
194
+ // Re-run will be triggered by retryCount change
195
+ } else {
196
+ const schemaResult: ESISchemaResult<T> = {
197
+ ...inferenceResult,
198
+ rawOutput: inferenceResult.output,
199
+ validationErrors: parseResult.errors,
200
+ };
201
+ setResult(schemaResult);
202
+ onValidationError?.(parseResult.errors, inferenceResult.output);
203
+ }
204
+ }
205
+
206
+ setIsLoading(false);
207
+ }
208
+
209
+ runInference();
210
+ }, [fullPrompt, enabled, retryCount]);
211
+
212
+ if (isLoading) {
213
+ return <span className={className}>{loading}</span>;
214
+ }
215
+
216
+ if (!result?.data) {
217
+ return <span className={className}>{fallback}</span>;
218
+ }
219
+
220
+ if (render) {
221
+ return (
222
+ <span className={className}>
223
+ {render(result.data, {
224
+ cached: result.cached,
225
+ latencyMs: result.latencyMs,
226
+ })}
227
+ </span>
228
+ );
229
+ }
230
+
231
+ // Default: JSON stringify the result
232
+ return <span className={className}>{JSON.stringify(result.data)}</span>;
233
+ }
234
+
235
+ // ============================================================================
236
+ // ESI.If - Conditional rendering
237
+ // ============================================================================
238
+
239
+ export interface ESIIfProps<T> {
240
+ /** The prompt to evaluate */
241
+ children?: ReactNode;
242
+ prompt?: string;
243
+
244
+ /** Zod schema for the condition evaluation */
245
+ schema: ZodType<T, ZodTypeDef, unknown>;
246
+
247
+ /** Condition function - if true, render `then` */
248
+ when: ESICondition<T>;
249
+
250
+ /** Content to render if condition is true */
251
+ then: ReactNode;
252
+
253
+ /** Content to render if condition is false */
254
+ else?: ReactNode;
255
+
256
+ /** Loading state */
257
+ loading?: ReactNode;
258
+
259
+ /** Additional params */
260
+ temperature?: number;
261
+ cacheTtl?: number;
262
+
263
+ /** Callbacks */
264
+ onEvaluate?: (result: T, conditionMet: boolean) => void;
265
+
266
+ /** Class name */
267
+ className?: string;
268
+ }
269
+
270
+ export function ESIIf<T>({
271
+ children,
272
+ prompt,
273
+ schema,
274
+ when,
275
+ then: thenContent,
276
+ else: elseContent,
277
+ loading = null,
278
+ temperature,
279
+ cacheTtl,
280
+ onEvaluate,
281
+ className,
282
+ }: ESIIfProps<T>): JSX.Element | null {
283
+ const [conditionMet, setConditionMet] = useState<boolean | null>(null);
284
+ const [data, setData] = useState<T | null>(null);
285
+
286
+ const handleSuccess = useCallback(
287
+ (result: T) => {
288
+ setData(result);
289
+ try {
290
+ // Note: we need the context here, but for simplicity we pass a minimal context
291
+ // In real usage, this would come from ESIProvider
292
+ const met = when(result, {} as UserContext);
293
+ setConditionMet(met);
294
+ onEvaluate?.(result, met);
295
+ } catch {
296
+ setConditionMet(false);
297
+ }
298
+ },
299
+ [when, onEvaluate],
300
+ );
301
+
302
+ return (
303
+ <span className={className}>
304
+ <ESIStructured
305
+ prompt={prompt}
306
+ schema={schema}
307
+ temperature={temperature}
308
+ cacheTtl={cacheTtl}
309
+ loading={loading}
310
+ onSuccess={handleSuccess}
311
+ render={() => null}
312
+ >
313
+ {children}
314
+ </ESIStructured>
315
+ {conditionMet === true && thenContent}
316
+ {conditionMet === false && elseContent}
317
+ </span>
318
+ );
319
+ }
320
+
321
+ // ============================================================================
322
+ // ESI.Match / ESI.Case / ESI.Default - Pattern matching
323
+ // ============================================================================
324
+
325
+ export interface ESICaseProps<T> {
326
+ /** Match condition */
327
+ match: ESICondition<T>;
328
+ /** Content to render if matched */
329
+ children: ReactNode;
330
+ }
331
+
332
+ export function ESICase<T>({ children }: ESICaseProps<T>): JSX.Element {
333
+ return <>{children}</>;
334
+ }
335
+
336
+ export interface ESIDefaultProps {
337
+ children: ReactNode;
338
+ }
339
+
340
+ export function ESIDefault({ children }: ESIDefaultProps): JSX.Element {
341
+ return <>{children}</>;
342
+ }
343
+
344
+ export interface ESIMatchProps<T> {
345
+ /** The prompt to evaluate */
346
+ children: ReactNode;
347
+ prompt?: string;
348
+
349
+ /** Zod schema */
350
+ schema: ZodType<T, ZodTypeDef, unknown>;
351
+
352
+ /** Loading state */
353
+ loading?: ReactNode;
354
+
355
+ /** Additional params */
356
+ temperature?: number;
357
+ cacheTtl?: number;
358
+
359
+ /** Callback */
360
+ onMatch?: (data: T, matchedIndex: number) => void;
361
+
362
+ /** Class name */
363
+ className?: string;
364
+ }
365
+
366
+ export function ESIMatch<T>({
367
+ children,
368
+ prompt,
369
+ schema,
370
+ loading = null,
371
+ temperature,
372
+ cacheTtl,
373
+ onMatch,
374
+ className,
375
+ }: ESIMatchProps<T>): JSX.Element | null {
376
+ const [matchedIndex, setMatchedIndex] = useState<number | null>(null);
377
+ const [data, setData] = useState<T | null>(null);
378
+
379
+ // Extract cases and default from children
380
+ const { cases, defaultCase, promptFromChildren } = useMemo(() => {
381
+ const cases: Array<{ match: ESICondition<T>; content: ReactNode }> = [];
382
+ let defaultCase: ReactNode = null;
383
+ let promptFromChildren = '';
384
+
385
+ Children.forEach(children, (child) => {
386
+ if (!isValidElement(child)) {
387
+ if (typeof child === 'string') {
388
+ promptFromChildren += child;
389
+ }
390
+ return;
391
+ }
392
+
393
+ // Check if it's an ESI.Case
394
+ if (child.type === ESICase) {
395
+ const props = child.props as ESICaseProps<T>;
396
+ cases.push({
397
+ match: props.match,
398
+ content: props.children,
399
+ });
400
+ }
401
+ // Check if it's an ESI.Default
402
+ else if (child.type === ESIDefault) {
403
+ defaultCase = (child.props as ESIDefaultProps).children;
404
+ }
405
+ });
406
+
407
+ return { cases, defaultCase, promptFromChildren };
408
+ }, [children]);
409
+
410
+ const handleSuccess = useCallback(
411
+ (result: T) => {
412
+ setData(result);
413
+
414
+ // Find first matching case
415
+ for (let i = 0; i < cases.length; i++) {
416
+ try {
417
+ if (cases[i].match(result, {} as UserContext)) {
418
+ setMatchedIndex(i);
419
+ onMatch?.(result, i);
420
+ return;
421
+ }
422
+ } catch {
423
+ // Continue to next case
424
+ }
425
+ }
426
+
427
+ // No match - use default
428
+ setMatchedIndex(-1);
429
+ onMatch?.(result, -1);
430
+ },
431
+ [cases, onMatch],
432
+ );
433
+
434
+ const finalPrompt = prompt || promptFromChildren;
435
+
436
+ return (
437
+ <span className={className}>
438
+ <ESIStructured
439
+ prompt={finalPrompt}
440
+ schema={schema}
441
+ temperature={temperature}
442
+ cacheTtl={cacheTtl}
443
+ loading={loading}
444
+ onSuccess={handleSuccess}
445
+ render={() => null}
446
+ />
447
+ {matchedIndex !== null &&
448
+ matchedIndex >= 0 &&
449
+ cases[matchedIndex]?.content}
450
+ {matchedIndex === -1 && defaultCase}
451
+ </span>
452
+ );
453
+ }
454
+
455
+ // ============================================================================
456
+ // ESI.Collaborative - Presence-aware ESI
457
+ // ============================================================================
458
+
459
+ export interface ESICollaborativeProps<T> {
460
+ /** Base prompt - presence info will be injected */
461
+ children?: ReactNode;
462
+ prompt?: string;
463
+
464
+ /** Zod schema for output */
465
+ schema: ZodType<T, ZodTypeDef, unknown>;
466
+
467
+ /** Custom render function */
468
+ render?: (data: T, presence: PresenceUser[]) => ReactNode;
469
+
470
+ /** Fallback content */
471
+ fallback?: ReactNode;
472
+
473
+ /** Loading state */
474
+ loading?: ReactNode;
475
+
476
+ /** How to describe users in the prompt */
477
+ describeUsers?: (users: PresenceUser[]) => string;
478
+
479
+ /** Re-infer when presence changes */
480
+ reactToPresenceChange?: boolean;
481
+
482
+ /** Debounce time for presence changes (ms) */
483
+ presenceDebounce?: number;
484
+
485
+ /** Additional params */
486
+ temperature?: number;
487
+ maxTokens?: number;
488
+ cacheTtl?: number;
489
+
490
+ /** Callbacks */
491
+ onSuccess?: (data: T, presence: PresenceUser[]) => void;
492
+
493
+ /** Class name */
494
+ className?: string;
495
+ }
496
+
497
+ /**
498
+ * Default user description for prompts
499
+ */
500
+ function defaultDescribeUsers(users: PresenceUser[]): string {
501
+ if (users.length === 0) return 'No other users are viewing this content.';
502
+ if (users.length === 1) return `1 user is viewing: ${describeUser(users[0])}`;
503
+
504
+ const roles = [...new Set(users.map((u) => u.role).filter(Boolean))];
505
+ const roleStr = roles.length > 0 ? ` with roles: ${roles.join(', ')}` : '';
506
+
507
+ return `${users.length} users are viewing${roleStr}:\n${users.map(describeUser).join('\n')}`;
508
+ }
509
+
510
+ function describeUser(user: PresenceUser): string {
511
+ const parts = [user.name || user.userId];
512
+ if (user.role) parts.push(`(${user.role})`);
513
+ if (user.status) parts.push(`[${user.status}]`);
514
+ return `- ${parts.join(' ')}`;
515
+ }
516
+
517
+ export function ESICollaborative<T>({
518
+ children,
519
+ prompt,
520
+ schema,
521
+ render,
522
+ fallback,
523
+ loading = '...',
524
+ describeUsers = defaultDescribeUsers,
525
+ reactToPresenceChange = true,
526
+ presenceDebounce = 2000,
527
+ temperature,
528
+ maxTokens,
529
+ cacheTtl,
530
+ onSuccess,
531
+ className,
532
+ }: ESICollaborativeProps<T>): JSX.Element {
533
+ const presence = usePresenceForESI();
534
+ const [debouncedUsers, setDebouncedUsers] = useState(presence.users);
535
+ const [result, setResult] = useState<T | null>(null);
536
+
537
+ // Debounce presence changes
538
+ useEffect(() => {
539
+ if (!reactToPresenceChange) return;
540
+
541
+ const timer = setTimeout(() => {
542
+ setDebouncedUsers(presence.users);
543
+ }, presenceDebounce);
544
+
545
+ return () => clearTimeout(timer);
546
+ }, [presence.users, reactToPresenceChange, presenceDebounce]);
547
+
548
+ // Build presence-aware prompt
549
+ const basePrompt =
550
+ prompt ||
551
+ (typeof children === 'string' ? children : String(children || ''));
552
+ const presenceDescription = describeUsers(debouncedUsers);
553
+
554
+ const collaborativePrompt = `[Audience Context]
555
+ ${presenceDescription}
556
+
557
+ [Task]
558
+ ${basePrompt}
559
+
560
+ Consider ALL viewers when generating your response. The content should be relevant and appropriate for everyone currently viewing.`;
561
+
562
+ const handleSuccess = useCallback(
563
+ (data: T) => {
564
+ setResult(data);
565
+ onSuccess?.(data, debouncedUsers);
566
+ },
567
+ [debouncedUsers, onSuccess],
568
+ );
569
+
570
+ return (
571
+ <ESIStructured
572
+ prompt={collaborativePrompt}
573
+ schema={schema}
574
+ temperature={temperature}
575
+ maxTokens={maxTokens}
576
+ cacheTtl={cacheTtl}
577
+ loading={loading}
578
+ fallback={fallback}
579
+ onSuccess={handleSuccess}
580
+ className={className}
581
+ render={(data, meta) => {
582
+ if (render) {
583
+ return render(data, debouncedUsers);
584
+ }
585
+ return JSON.stringify(data);
586
+ }}
587
+ />
588
+ );
589
+ }
590
+
591
+ // ============================================================================
592
+ // ESI.Reflect - Self-improving inference
593
+ // ============================================================================
594
+
595
+ export interface ESIReflectProps<T> {
596
+ /** The prompt */
597
+ children?: ReactNode;
598
+ prompt?: string;
599
+
600
+ /** Schema must include a quality/confidence field */
601
+ schema: ZodType<T, ZodTypeDef, unknown>;
602
+
603
+ /** Continue until this condition is met */
604
+ until: (result: T, iteration: number) => boolean;
605
+
606
+ /** Max iterations */
607
+ maxIterations?: number;
608
+
609
+ /** Custom render */
610
+ render?: (data: T, iterations: number) => ReactNode;
611
+
612
+ /** Show intermediate results */
613
+ showProgress?: boolean;
614
+
615
+ /** Fallback */
616
+ fallback?: ReactNode;
617
+
618
+ /** Loading */
619
+ loading?: ReactNode;
620
+
621
+ /** Callbacks */
622
+ onIteration?: (data: T, iteration: number) => void;
623
+ onComplete?: (data: T, totalIterations: number) => void;
624
+
625
+ /** Class name */
626
+ className?: string;
627
+ }
628
+
629
+ export function ESIReflect<T>({
630
+ children,
631
+ prompt,
632
+ schema,
633
+ until,
634
+ maxIterations = 3,
635
+ render,
636
+ showProgress = false,
637
+ fallback,
638
+ loading = '...',
639
+ onIteration,
640
+ onComplete,
641
+ className,
642
+ }: ESIReflectProps<T>): JSX.Element {
643
+ const { process, enabled } = useESI();
644
+ const [currentResult, setCurrentResult] = useState<T | null>(null);
645
+ const [iteration, setIteration] = useState(0);
646
+ const [isComplete, setIsComplete] = useState(false);
647
+ const [isLoading, setIsLoading] = useState(true);
648
+
649
+ const basePrompt =
650
+ prompt ||
651
+ (typeof children === 'string' ? children : String(children || ''));
652
+
653
+ useEffect(() => {
654
+ if (!enabled) {
655
+ setIsLoading(false);
656
+ return;
657
+ }
658
+
659
+ async function runReflection() {
660
+ setIsLoading(true);
661
+ let currentIteration = 0;
662
+ let lastResult: T | null = null;
663
+ let previousAttempts: string[] = [];
664
+
665
+ while (currentIteration < maxIterations) {
666
+ // Build prompt with reflection context
667
+ let reflectionPrompt = basePrompt;
668
+
669
+ if (currentIteration > 0 && lastResult) {
670
+ reflectionPrompt = `[Previous Attempt ${currentIteration}]
671
+ ${JSON.stringify(lastResult)}
672
+
673
+ [Reflection]
674
+ The previous attempt did not meet the quality threshold. Please improve upon it.
675
+
676
+ [Original Task]
677
+ ${basePrompt}`;
678
+ }
679
+
680
+ const fullPrompt = reflectionPrompt + generateSchemaPrompt(schema);
681
+
682
+ const directive = {
683
+ id: `esi-reflect-${Date.now()}-${currentIteration}`,
684
+ params: { model: 'llm' as const },
685
+ content: { type: 'text' as const, value: fullPrompt },
686
+ };
687
+
688
+ const result = await process(directive);
689
+
690
+ if (!result.success || !result.output) {
691
+ break;
692
+ }
693
+
694
+ const parseResult = parseWithSchema(result.output, schema);
695
+
696
+ if (!parseResult.success) {
697
+ currentIteration++;
698
+ continue;
699
+ }
700
+
701
+ lastResult = parseResult.data;
702
+ setCurrentResult(parseResult.data);
703
+ setIteration(currentIteration + 1);
704
+ onIteration?.(parseResult.data, currentIteration + 1);
705
+
706
+ // Check if we're done
707
+ if (until(parseResult.data, currentIteration + 1)) {
708
+ setIsComplete(true);
709
+ onComplete?.(parseResult.data, currentIteration + 1);
710
+ break;
711
+ }
712
+
713
+ previousAttempts.push(result.output);
714
+ currentIteration++;
715
+ }
716
+
717
+ // Max iterations reached
718
+ if (!isComplete && lastResult) {
719
+ setIsComplete(true);
720
+ onComplete?.(lastResult, currentIteration);
721
+ }
722
+
723
+ setIsLoading(false);
724
+ }
725
+
726
+ runReflection();
727
+ }, [basePrompt, enabled, maxIterations]);
728
+
729
+ if (isLoading) {
730
+ if (showProgress && currentResult) {
731
+ return (
732
+ <span className={className}>
733
+ {render
734
+ ? render(currentResult, iteration)
735
+ : JSON.stringify(currentResult)}
736
+ <span> (refining... iteration {iteration})</span>
737
+ </span>
738
+ );
739
+ }
740
+ return <span className={className}>{loading}</span>;
741
+ }
742
+
743
+ if (!currentResult) {
744
+ return <span className={className}>{fallback}</span>;
745
+ }
746
+
747
+ if (render) {
748
+ return (
749
+ <span className={className}>{render(currentResult, iteration)}</span>
750
+ );
751
+ }
752
+
753
+ return <span className={className}>{JSON.stringify(currentResult)}</span>;
754
+ }
755
+
756
+ // ============================================================================
757
+ // ESI.Optimize - Self-optimization when alone
758
+ // ============================================================================
759
+
760
+ export interface ESIOptimizeProps<T> {
761
+ /** The prompt */
762
+ children?: ReactNode;
763
+ prompt?: string;
764
+
765
+ /** Schema for output */
766
+ schema: ZodType<T, ZodTypeDef, unknown>;
767
+
768
+ /** Quality criteria to optimize for */
769
+ criteria?: string[];
770
+
771
+ /** Target quality score (0-1) */
772
+ targetQuality?: number;
773
+
774
+ /** Max optimization rounds */
775
+ maxRounds?: number;
776
+
777
+ /** Only optimize when user is alone (no other presence) */
778
+ onlyWhenAlone?: boolean;
779
+
780
+ /** Custom render */
781
+ render?: (data: T, meta: OptimizeMeta) => ReactNode;
782
+
783
+ /** Fallback */
784
+ fallback?: ReactNode;
785
+
786
+ /** Loading */
787
+ loading?: ReactNode;
788
+
789
+ /** Callbacks */
790
+ onImprove?: (data: T, round: number, quality: number) => void;
791
+ onOptimized?: (data: T, totalRounds: number, finalQuality: number) => void;
792
+
793
+ /** Class name */
794
+ className?: string;
795
+ }
796
+
797
+ export interface OptimizeMeta {
798
+ rounds: number;
799
+ quality: number;
800
+ improvements: string[];
801
+ wasOptimized: boolean;
802
+ }
803
+
804
+ /**
805
+ * Self-optimization schema - added to user's schema
806
+ */
807
+ interface OptimizationWrapper<T> {
808
+ result: T;
809
+ selfAssessment: {
810
+ quality: number;
811
+ strengths: string[];
812
+ weaknesses: string[];
813
+ improvementSuggestions: string[];
814
+ };
815
+ }
816
+
817
+ export function ESIOptimize<T>({
818
+ children,
819
+ prompt,
820
+ schema,
821
+ criteria = ['clarity', 'relevance', 'completeness', 'conciseness'],
822
+ targetQuality = 0.85,
823
+ maxRounds = 3,
824
+ onlyWhenAlone = true,
825
+ render,
826
+ fallback,
827
+ loading = '...',
828
+ onImprove,
829
+ onOptimized,
830
+ className,
831
+ }: ESIOptimizeProps<T>): JSX.Element {
832
+ const { process, enabled } = useESI();
833
+ const presence = usePresenceForESI();
834
+ const [result, setResult] = useState<T | null>(null);
835
+ const [meta, setMeta] = useState<OptimizeMeta>({
836
+ rounds: 0,
837
+ quality: 0,
838
+ improvements: [],
839
+ wasOptimized: false,
840
+ });
841
+ const [isLoading, setIsLoading] = useState(true);
842
+
843
+ const basePrompt =
844
+ prompt ||
845
+ (typeof children === 'string' ? children : String(children || ''));
846
+
847
+ // Check if we should optimize
848
+ const shouldOptimize = !onlyWhenAlone || presence.users.length <= 1;
849
+
850
+ useEffect(() => {
851
+ if (!enabled) {
852
+ setIsLoading(false);
853
+ return;
854
+ }
855
+
856
+ async function runOptimization() {
857
+ setIsLoading(true);
858
+
859
+ const criteriaList = criteria.join(', ');
860
+
861
+ // First pass - generate with self-assessment
862
+ const firstPassPrompt = `${basePrompt}
863
+
864
+ After generating your response, assess its quality on these criteria: ${criteriaList}
865
+
866
+ ${generateSchemaPrompt(schema)}
867
+
868
+ Additionally, include a self-assessment in this format:
869
+ {
870
+ "result": <your response matching the schema above>,
871
+ "selfAssessment": {
872
+ "quality": <0-1 score>,
873
+ "strengths": [<list of strengths>],
874
+ "weaknesses": [<list of weaknesses>],
875
+ "improvementSuggestions": [<specific improvements>]
876
+ }
877
+ }`;
878
+
879
+ let currentResult: T | null = null;
880
+ let currentQuality = 0;
881
+ let round = 0;
882
+ let improvements: string[] = [];
883
+ let lastWeaknesses: string[] = [];
884
+
885
+ // Initial generation
886
+ const firstResult = await process({
887
+ id: `esi-optimize-${Date.now()}-0`,
888
+ params: { model: 'llm' as const },
889
+ content: { type: 'text' as const, value: firstPassPrompt },
890
+ });
891
+
892
+ if (!firstResult.success || !firstResult.output) {
893
+ setIsLoading(false);
894
+ return;
895
+ }
896
+
897
+ // Try to parse the wrapped result
898
+ try {
899
+ const parsed = JSON.parse(
900
+ extractJson(firstResult.output),
901
+ ) as OptimizationWrapper<T>;
902
+ const validated = schema.safeParse(parsed.result);
903
+
904
+ if (validated.success) {
905
+ currentResult = validated.data;
906
+ currentQuality = parsed.selfAssessment?.quality || 0.5;
907
+ lastWeaknesses = parsed.selfAssessment?.weaknesses || [];
908
+ round = 1;
909
+
910
+ setResult(currentResult);
911
+ setMeta({
912
+ rounds: 1,
913
+ quality: currentQuality,
914
+ improvements: [],
915
+ wasOptimized: false,
916
+ });
917
+ onImprove?.(currentResult, 1, currentQuality);
918
+ }
919
+ } catch {
920
+ // Fallback: try to parse just the schema
921
+ const parseResult = parseWithSchema(firstResult.output, schema);
922
+ if (parseResult.success) {
923
+ currentResult = parseResult.data;
924
+ currentQuality = 0.6; // Assume moderate quality
925
+ round = 1;
926
+ setResult(currentResult);
927
+ } else {
928
+ setIsLoading(false);
929
+ return;
930
+ }
931
+ }
932
+
933
+ // Optimization loop
934
+ if (shouldOptimize && currentQuality < targetQuality) {
935
+ while (round < maxRounds && currentQuality < targetQuality) {
936
+ const optimizePrompt = `You previously generated this response:
937
+ ${JSON.stringify(currentResult)}
938
+
939
+ Quality score: ${currentQuality.toFixed(2)}
940
+ Weaknesses identified: ${lastWeaknesses.join(', ') || 'none specified'}
941
+
942
+ Please improve the response, focusing on: ${criteriaList}
943
+ Address the weaknesses and aim for a quality score above ${targetQuality}.
944
+
945
+ ${generateSchemaPrompt(schema)}
946
+
947
+ Include your improved self-assessment:
948
+ {
949
+ "result": <improved response>,
950
+ "selfAssessment": {
951
+ "quality": <0-1 score>,
952
+ "strengths": [...],
953
+ "weaknesses": [...],
954
+ "improvementSuggestions": [...]
955
+ },
956
+ "improvementsMade": [<what you improved>]
957
+ }`;
958
+
959
+ const improvedResult = await process({
960
+ id: `esi-optimize-${Date.now()}-${round}`,
961
+ params: { model: 'llm' as const },
962
+ content: { type: 'text' as const, value: optimizePrompt },
963
+ });
964
+
965
+ if (!improvedResult.success || !improvedResult.output) {
966
+ break;
967
+ }
968
+
969
+ try {
970
+ const parsed = JSON.parse(
971
+ extractJson(improvedResult.output),
972
+ ) as OptimizationWrapper<T> & {
973
+ improvementsMade?: string[];
974
+ };
975
+ const validated = schema.safeParse(parsed.result);
976
+
977
+ if (validated.success) {
978
+ const newQuality =
979
+ parsed.selfAssessment?.quality || currentQuality;
980
+
981
+ // Only accept if quality improved
982
+ if (newQuality > currentQuality) {
983
+ currentResult = validated.data;
984
+ currentQuality = newQuality;
985
+ lastWeaknesses = parsed.selfAssessment?.weaknesses || [];
986
+
987
+ if (parsed.improvementsMade) {
988
+ improvements.push(...parsed.improvementsMade);
989
+ }
990
+
991
+ setResult(currentResult);
992
+ setMeta({
993
+ rounds: round + 1,
994
+ quality: currentQuality,
995
+ improvements,
996
+ wasOptimized: true,
997
+ });
998
+ onImprove?.(currentResult, round + 1, currentQuality);
999
+ }
1000
+ }
1001
+ } catch {
1002
+ // Parsing failed, keep current result
1003
+ }
1004
+
1005
+ round++;
1006
+ }
1007
+ }
1008
+
1009
+ // Final state
1010
+ if (currentResult) {
1011
+ setMeta((prev) => ({
1012
+ ...prev,
1013
+ rounds: round,
1014
+ quality: currentQuality,
1015
+ wasOptimized: round > 1,
1016
+ }));
1017
+ onOptimized?.(currentResult, round, currentQuality);
1018
+ }
1019
+
1020
+ setIsLoading(false);
1021
+ }
1022
+
1023
+ runOptimization();
1024
+ }, [basePrompt, enabled, shouldOptimize, targetQuality, maxRounds]);
1025
+
1026
+ if (isLoading) {
1027
+ return <span className={className}>{loading}</span>;
1028
+ }
1029
+
1030
+ if (!result) {
1031
+ return <span className={className}>{fallback}</span>;
1032
+ }
1033
+
1034
+ if (render) {
1035
+ return <span className={className}>{render(result, meta)}</span>;
1036
+ }
1037
+
1038
+ return <span className={className}>{JSON.stringify(result)}</span>;
1039
+ }
1040
+
1041
+ /**
1042
+ * Extract JSON from a string that might have markdown or other formatting
1043
+ */
1044
+ function extractJson(str: string): string {
1045
+ let cleaned = str.trim();
1046
+
1047
+ // Remove markdown code blocks
1048
+ if (cleaned.startsWith('```')) {
1049
+ const match = cleaned.match(/```(?:json)?\s*([\s\S]*?)```/);
1050
+ if (match) cleaned = match[1].trim();
1051
+ }
1052
+
1053
+ // Find JSON object
1054
+ const jsonMatch = cleaned.match(/\{[\s\S]*\}/);
1055
+ if (jsonMatch) return jsonMatch[0];
1056
+
1057
+ return cleaned;
1058
+ }
1059
+
1060
+ // ============================================================================
1061
+ // ESI.Auto - Automatic mode selection
1062
+ // ============================================================================
1063
+
1064
+ export interface ESIAutoProps<T> {
1065
+ /** The prompt */
1066
+ children?: ReactNode;
1067
+ prompt?: string;
1068
+
1069
+ /** Schema for output */
1070
+ schema: ZodType<T, ZodTypeDef, unknown>;
1071
+
1072
+ /** Custom render */
1073
+ render?: (
1074
+ data: T,
1075
+ mode: 'collaborative' | 'optimized' | 'basic',
1076
+ ) => ReactNode;
1077
+
1078
+ /** Minimum users for collaborative mode */
1079
+ collaborativeThreshold?: number;
1080
+
1081
+ /** Optimization settings */
1082
+ optimizeSettings?: {
1083
+ criteria?: string[];
1084
+ targetQuality?: number;
1085
+ maxRounds?: number;
1086
+ };
1087
+
1088
+ /** Fallback */
1089
+ fallback?: ReactNode;
1090
+
1091
+ /** Loading */
1092
+ loading?: ReactNode;
1093
+
1094
+ /** Class name */
1095
+ className?: string;
1096
+ }
1097
+
1098
+ /**
1099
+ * ESI.Auto - Automatically selects the best mode:
1100
+ * - Multiple users → Collaborative (presence-aware)
1101
+ * - Single user → Optimize (self-improving)
1102
+ * - Quick mode → Basic (fast, single pass)
1103
+ */
1104
+ export function ESIAuto<T>({
1105
+ children,
1106
+ prompt,
1107
+ schema,
1108
+ render,
1109
+ collaborativeThreshold = 2,
1110
+ optimizeSettings,
1111
+ fallback,
1112
+ loading,
1113
+ className,
1114
+ }: ESIAutoProps<T>): JSX.Element {
1115
+ const presence = usePresenceForESI();
1116
+ const userCount = presence.users.length;
1117
+
1118
+ // Determine mode
1119
+ const mode: 'collaborative' | 'optimized' | 'basic' =
1120
+ userCount >= collaborativeThreshold
1121
+ ? 'collaborative'
1122
+ : userCount === 1
1123
+ ? 'optimized'
1124
+ : 'basic';
1125
+
1126
+ if (mode === 'collaborative') {
1127
+ return (
1128
+ <ESICollaborative
1129
+ prompt={prompt}
1130
+ schema={schema}
1131
+ fallback={fallback}
1132
+ loading={loading}
1133
+ className={className}
1134
+ render={render ? (data) => render(data, 'collaborative') : undefined}
1135
+ >
1136
+ {children}
1137
+ </ESICollaborative>
1138
+ );
1139
+ }
1140
+
1141
+ if (mode === 'optimized') {
1142
+ return (
1143
+ <ESIOptimize
1144
+ prompt={prompt}
1145
+ schema={schema}
1146
+ criteria={optimizeSettings?.criteria}
1147
+ targetQuality={optimizeSettings?.targetQuality}
1148
+ maxRounds={optimizeSettings?.maxRounds}
1149
+ fallback={fallback}
1150
+ loading={loading}
1151
+ className={className}
1152
+ render={render ? (data) => render(data, 'optimized') : undefined}
1153
+ >
1154
+ {children}
1155
+ </ESIOptimize>
1156
+ );
1157
+ }
1158
+
1159
+ // Basic mode
1160
+ return (
1161
+ <ESIStructured
1162
+ prompt={prompt}
1163
+ schema={schema}
1164
+ fallback={fallback}
1165
+ loading={loading}
1166
+ className={className}
1167
+ render={render ? (data) => render(data, 'basic') : undefined}
1168
+ >
1169
+ {children}
1170
+ </ESIStructured>
1171
+ );
1172
+ }
1173
+
1174
+ // ============================================================================
1175
+ // ESI.Show - Simple boolean visibility
1176
+ // ============================================================================
1177
+
1178
+ export interface ESIShowProps {
1179
+ /** Condition to evaluate - AI will return true/false */
1180
+ condition: string;
1181
+ /** Content to show if condition is true */
1182
+ children: ReactNode;
1183
+ /** Content to show if condition is false */
1184
+ fallback?: ReactNode;
1185
+ /** Loading state */
1186
+ loading?: ReactNode;
1187
+ /** Cache TTL */
1188
+ cacheTtl?: number;
1189
+ /** Callback */
1190
+ onEvaluate?: (result: boolean) => void;
1191
+ /** Class name */
1192
+ className?: string;
1193
+ }
1194
+
1195
+ /**
1196
+ * Simple boolean show/hide based on AI evaluation
1197
+ * @example
1198
+ * <ESI.Show condition="User seems frustrated based on their message history">
1199
+ * <CalmingMessage />
1200
+ * </ESI.Show>
1201
+ */
1202
+ export function ESIShow({
1203
+ condition,
1204
+ children,
1205
+ fallback = null,
1206
+ loading = null,
1207
+ cacheTtl,
1208
+ onEvaluate,
1209
+ className,
1210
+ }: ESIShowProps): JSX.Element {
1211
+ const [show, setShow] = useState<boolean | null>(null);
1212
+
1213
+ const boolSchema = {
1214
+ safeParse: (val: unknown) => {
1215
+ if (typeof val === 'boolean') return { success: true, data: val };
1216
+ if (typeof val === 'string') {
1217
+ const lower = val.toLowerCase().trim();
1218
+ if (lower === 'true' || lower === 'yes' || lower === '1')
1219
+ return { success: true, data: true };
1220
+ if (lower === 'false' || lower === 'no' || lower === '0')
1221
+ return { success: true, data: false };
1222
+ }
1223
+ if (typeof val === 'object' && val !== null && 'result' in val) {
1224
+ return {
1225
+ success: true,
1226
+ data: Boolean((val as { result: unknown }).result),
1227
+ };
1228
+ }
1229
+ return { success: false, error: 'Not a boolean' };
1230
+ },
1231
+ } as ZodType<boolean, ZodTypeDef, unknown>;
1232
+
1233
+ return (
1234
+ <span className={className}>
1235
+ <ESIStructured
1236
+ prompt={`Evaluate this condition and respond with only "true" or "false": ${condition}`}
1237
+ schema={boolSchema}
1238
+ cacheTtl={cacheTtl}
1239
+ loading={loading}
1240
+ onSuccess={(result) => {
1241
+ setShow(result);
1242
+ onEvaluate?.(result);
1243
+ }}
1244
+ render={() => null}
1245
+ />
1246
+ {show === true && children}
1247
+ {show === false && fallback}
1248
+ </span>
1249
+ );
1250
+ }
1251
+
1252
+ // ============================================================================
1253
+ // ESI.Hide - Inverse of Show
1254
+ // ============================================================================
1255
+
1256
+ export interface ESIHideProps {
1257
+ /** Condition to evaluate - content hidden if true */
1258
+ condition: string;
1259
+ /** Content to show if condition is false */
1260
+ children: ReactNode;
1261
+ /** Loading state */
1262
+ loading?: ReactNode;
1263
+ /** Cache TTL */
1264
+ cacheTtl?: number;
1265
+ /** Class name */
1266
+ className?: string;
1267
+ }
1268
+
1269
+ /**
1270
+ * Hide content based on AI evaluation
1271
+ * @example
1272
+ * <ESI.Hide condition="User is a minor">
1273
+ * <AdultContent />
1274
+ * </ESI.Hide>
1275
+ */
1276
+ export function ESIHide({
1277
+ condition,
1278
+ children,
1279
+ loading = null,
1280
+ cacheTtl,
1281
+ className,
1282
+ }: ESIHideProps): JSX.Element {
1283
+ return (
1284
+ <ESIShow
1285
+ condition={condition}
1286
+ fallback={children}
1287
+ loading={loading}
1288
+ cacheTtl={cacheTtl}
1289
+ className={className}
1290
+ >
1291
+ {null}
1292
+ </ESIShow>
1293
+ );
1294
+ }
1295
+
1296
+ // ============================================================================
1297
+ // ESI.When - Shorthand conditional (no else)
1298
+ // ============================================================================
1299
+
1300
+ export interface ESIWhenProps {
1301
+ /** Condition description */
1302
+ condition: string;
1303
+ /** Content to render when true */
1304
+ children: ReactNode;
1305
+ /** Loading state */
1306
+ loading?: ReactNode;
1307
+ /** Cache TTL */
1308
+ cacheTtl?: number;
1309
+ /** Class name */
1310
+ className?: string;
1311
+ }
1312
+
1313
+ /**
1314
+ * Render content only when condition is met
1315
+ * @example
1316
+ * <ESI.When condition="It's the user's birthday">
1317
+ * <BirthdayBanner />
1318
+ * </ESI.When>
1319
+ */
1320
+ export function ESIWhen({
1321
+ condition,
1322
+ children,
1323
+ loading,
1324
+ cacheTtl,
1325
+ className,
1326
+ }: ESIWhenProps): JSX.Element {
1327
+ return (
1328
+ <ESIShow
1329
+ condition={condition}
1330
+ loading={loading}
1331
+ cacheTtl={cacheTtl}
1332
+ className={className}
1333
+ >
1334
+ {children}
1335
+ </ESIShow>
1336
+ );
1337
+ }
1338
+
1339
+ // ============================================================================
1340
+ // ESI.Unless - Inverse of When
1341
+ // ============================================================================
1342
+
1343
+ export interface ESIUnlessProps {
1344
+ /** Condition description - renders if false */
1345
+ condition: string;
1346
+ /** Content to render when condition is false */
1347
+ children: ReactNode;
1348
+ /** Loading state */
1349
+ loading?: ReactNode;
1350
+ /** Cache TTL */
1351
+ cacheTtl?: number;
1352
+ /** Class name */
1353
+ className?: string;
1354
+ }
1355
+
1356
+ /**
1357
+ * Render content unless condition is met
1358
+ * @example
1359
+ * <ESI.Unless condition="User has completed onboarding">
1360
+ * <OnboardingPrompt />
1361
+ * </ESI.Unless>
1362
+ */
1363
+ export function ESIUnless({
1364
+ condition,
1365
+ children,
1366
+ loading,
1367
+ cacheTtl,
1368
+ className,
1369
+ }: ESIUnlessProps): JSX.Element {
1370
+ return (
1371
+ <ESIHide
1372
+ condition={condition}
1373
+ loading={loading}
1374
+ cacheTtl={cacheTtl}
1375
+ className={className}
1376
+ >
1377
+ {children}
1378
+ </ESIHide>
1379
+ );
1380
+ }
1381
+
1382
+ // ============================================================================
1383
+ // ESI.TierGate - Gate by user tier
1384
+ // ============================================================================
1385
+
1386
+ export interface ESITierGateProps {
1387
+ /** Minimum tier required */
1388
+ minTier: 'free' | 'starter' | 'pro' | 'enterprise';
1389
+ /** Content for users who meet tier requirement */
1390
+ children: ReactNode;
1391
+ /** Content for users below tier (upgrade prompt) */
1392
+ fallback?: ReactNode;
1393
+ /** Class name */
1394
+ className?: string;
1395
+ }
1396
+
1397
+ const TIER_LEVELS = { free: 0, starter: 1, pro: 2, enterprise: 3, admin: 999 };
1398
+
1399
+ /**
1400
+ * Gate content by user tier
1401
+ * Admins bypass ALL tier restrictions
1402
+ * @example
1403
+ * <ESI.TierGate minTier="pro" fallback={<UpgradePrompt />}>
1404
+ * <AdvancedFeature />
1405
+ * </ESI.TierGate>
1406
+ */
1407
+ export function ESITierGate({
1408
+ minTier,
1409
+ children,
1410
+ fallback = null,
1411
+ className,
1412
+ }: ESITierGateProps): JSX.Element {
1413
+ const [hasAccess, setHasAccess] = useState<boolean | null>(null);
1414
+
1415
+ useEffect(() => {
1416
+ // Check global ESI state for tier
1417
+ const state =
1418
+ (typeof window !== 'undefined' &&
1419
+ (
1420
+ window as unknown as {
1421
+ __AEON_ESI_STATE__?: { userTier?: string; isAdmin?: boolean };
1422
+ }
1423
+ ).__AEON_ESI_STATE__) ||
1424
+ {};
1425
+
1426
+ // Admins bypass ALL tier restrictions
1427
+ if (state.isAdmin === true || state.userTier === 'admin') {
1428
+ setHasAccess(true);
1429
+ return;
1430
+ }
1431
+
1432
+ const userTier = (state.userTier || 'free') as keyof typeof TIER_LEVELS;
1433
+ const userLevel = TIER_LEVELS[userTier] ?? 0;
1434
+ const requiredLevel = TIER_LEVELS[minTier] ?? 0;
1435
+ setHasAccess(userLevel >= requiredLevel);
1436
+ }, [minTier]);
1437
+
1438
+ if (hasAccess === null) return <span className={className} />;
1439
+ return <span className={className}>{hasAccess ? children : fallback}</span>;
1440
+ }
1441
+
1442
+ // ============================================================================
1443
+ // ESI.EmotionGate - Gate by emotion state
1444
+ // ============================================================================
1445
+
1446
+ export interface ESIEmotionGateProps {
1447
+ /** Emotion(s) that allow access */
1448
+ allow?: string[];
1449
+ /** Emotion(s) that block access */
1450
+ block?: string[];
1451
+ /** Valence range [min, max] (-1 to 1) */
1452
+ valenceRange?: [number, number];
1453
+ /** Arousal range [min, max] (0 to 1) */
1454
+ arousalRange?: [number, number];
1455
+ /** Content to show when conditions met */
1456
+ children: ReactNode;
1457
+ /** Content when conditions not met */
1458
+ fallback?: ReactNode;
1459
+ /** Class name */
1460
+ className?: string;
1461
+ }
1462
+
1463
+ /**
1464
+ * Gate content by emotion state
1465
+ * @example
1466
+ * <ESI.EmotionGate allow={['calm', 'focused']} fallback={<TakeABreakPrompt />}>
1467
+ * <ComplexTask />
1468
+ * </ESI.EmotionGate>
1469
+ */
1470
+ export function ESIEmotionGate({
1471
+ allow,
1472
+ block,
1473
+ valenceRange,
1474
+ arousalRange,
1475
+ children,
1476
+ fallback = null,
1477
+ className,
1478
+ }: ESIEmotionGateProps): JSX.Element {
1479
+ const [hasAccess, setHasAccess] = useState<boolean | null>(null);
1480
+
1481
+ useEffect(() => {
1482
+ const state =
1483
+ (typeof window !== 'undefined' &&
1484
+ (
1485
+ window as unknown as {
1486
+ __AEON_ESI_STATE__?: {
1487
+ emotionState?: {
1488
+ primary?: string;
1489
+ valence?: number;
1490
+ arousal?: number;
1491
+ };
1492
+ };
1493
+ }
1494
+ ).__AEON_ESI_STATE__) ||
1495
+ {};
1496
+ const emotion = state.emotionState || {};
1497
+
1498
+ let access = true;
1499
+
1500
+ // Check allowed emotions
1501
+ if (allow && allow.length > 0 && emotion.primary) {
1502
+ access = access && allow.includes(emotion.primary);
1503
+ }
1504
+
1505
+ // Check blocked emotions
1506
+ if (block && block.length > 0 && emotion.primary) {
1507
+ access = access && !block.includes(emotion.primary);
1508
+ }
1509
+
1510
+ // Check valence range
1511
+ if (valenceRange && emotion.valence !== undefined) {
1512
+ access =
1513
+ access &&
1514
+ emotion.valence >= valenceRange[0] &&
1515
+ emotion.valence <= valenceRange[1];
1516
+ }
1517
+
1518
+ // Check arousal range
1519
+ if (arousalRange && emotion.arousal !== undefined) {
1520
+ access =
1521
+ access &&
1522
+ emotion.arousal >= arousalRange[0] &&
1523
+ emotion.arousal <= arousalRange[1];
1524
+ }
1525
+
1526
+ setHasAccess(access);
1527
+ }, [allow, block, valenceRange, arousalRange]);
1528
+
1529
+ if (hasAccess === null) return <span className={className} />;
1530
+ return <span className={className}>{hasAccess ? children : fallback}</span>;
1531
+ }
1532
+
1533
+ // ============================================================================
1534
+ // ESI.TimeGate - Gate by time of day
1535
+ // ============================================================================
1536
+
1537
+ export interface ESITimeGateProps {
1538
+ /** Start hour (0-23) */
1539
+ after?: number;
1540
+ /** End hour (0-23) */
1541
+ before?: number;
1542
+ /** Days of week (0=Sunday, 6=Saturday) */
1543
+ days?: number[];
1544
+ /** Content to show when in time range */
1545
+ children: ReactNode;
1546
+ /** Content when outside time range */
1547
+ fallback?: ReactNode;
1548
+ /** Class name */
1549
+ className?: string;
1550
+ }
1551
+
1552
+ /**
1553
+ * Gate content by time of day
1554
+ * @example
1555
+ * <ESI.TimeGate after={9} before={17} days={[1,2,3,4,5]}>
1556
+ * <BusinessHoursContent />
1557
+ * </ESI.TimeGate>
1558
+ */
1559
+ export function ESITimeGate({
1560
+ after,
1561
+ before,
1562
+ days,
1563
+ children,
1564
+ fallback = null,
1565
+ className,
1566
+ }: ESITimeGateProps): JSX.Element {
1567
+ const [inRange, setInRange] = useState<boolean | null>(null);
1568
+
1569
+ useEffect(() => {
1570
+ const now = new Date();
1571
+ const hour = now.getHours();
1572
+ const day = now.getDay();
1573
+
1574
+ let access = true;
1575
+
1576
+ if (after !== undefined) access = access && hour >= after;
1577
+ if (before !== undefined) access = access && hour < before;
1578
+ if (days && days.length > 0) access = access && days.includes(day);
1579
+
1580
+ setInRange(access);
1581
+ }, [after, before, days]);
1582
+
1583
+ if (inRange === null) return <span className={className} />;
1584
+ return <span className={className}>{inRange ? children : fallback}</span>;
1585
+ }
1586
+
1587
+ // ============================================================================
1588
+ // ESI.ABTest - A/B testing with AI
1589
+ // ============================================================================
1590
+
1591
+ export interface ESIABTestProps {
1592
+ /** Test name for tracking */
1593
+ name: string;
1594
+ /** Variants to test */
1595
+ variants: { [key: string]: ReactNode };
1596
+ /** Prompt to select variant (AI decides) */
1597
+ selectionPrompt?: string;
1598
+ /** Use random selection instead of AI */
1599
+ random?: boolean;
1600
+ /** Callback when variant selected */
1601
+ onSelect?: (variant: string) => void;
1602
+ /** Loading state */
1603
+ loading?: ReactNode;
1604
+ /** Class name */
1605
+ className?: string;
1606
+ }
1607
+
1608
+ /**
1609
+ * A/B testing with AI-based or random selection
1610
+ * @example
1611
+ * <ESI.ABTest
1612
+ * name="checkout-button"
1613
+ * variants={{
1614
+ * control: <Button>Buy Now</Button>,
1615
+ * variant_a: <Button color="green">Purchase</Button>,
1616
+ * variant_b: <Button size="large">Get It Now</Button>
1617
+ * }}
1618
+ * selectionPrompt="Pick the variant most likely to convert based on user emotion"
1619
+ * />
1620
+ */
1621
+ export function ESIABTest({
1622
+ name,
1623
+ variants,
1624
+ selectionPrompt,
1625
+ random = false,
1626
+ onSelect,
1627
+ loading = null,
1628
+ className,
1629
+ }: ESIABTestProps): JSX.Element {
1630
+ const [selectedVariant, setSelectedVariant] = useState<string | null>(null);
1631
+ const variantKeys = Object.keys(variants);
1632
+
1633
+ useEffect(() => {
1634
+ if (random || !selectionPrompt) {
1635
+ // Random selection
1636
+ const selected =
1637
+ variantKeys[Math.floor(Math.random() * variantKeys.length)];
1638
+ setSelectedVariant(selected);
1639
+ onSelect?.(selected);
1640
+ }
1641
+ }, [random, selectionPrompt]);
1642
+
1643
+ if (random || !selectionPrompt) {
1644
+ if (!selectedVariant) return <span className={className}>{loading}</span>;
1645
+ return <span className={className}>{variants[selectedVariant]}</span>;
1646
+ }
1647
+
1648
+ // AI-based selection
1649
+ const variantSchema = {
1650
+ safeParse: (val: unknown) => {
1651
+ const str = String(val).trim();
1652
+ if (variantKeys.includes(str)) return { success: true, data: str };
1653
+ // Try to find a match
1654
+ for (const key of variantKeys) {
1655
+ if (str.toLowerCase().includes(key.toLowerCase())) {
1656
+ return { success: true, data: key };
1657
+ }
1658
+ }
1659
+ return { success: false, error: 'Invalid variant' };
1660
+ },
1661
+ } as ZodType<string, ZodTypeDef, unknown>;
1662
+
1663
+ return (
1664
+ <span className={className}>
1665
+ <ESIStructured
1666
+ prompt={`${selectionPrompt}\n\nAvailable variants: ${variantKeys.join(', ')}\n\nRespond with only the variant name.`}
1667
+ schema={variantSchema}
1668
+ loading={loading}
1669
+ onSuccess={(selected) => {
1670
+ setSelectedVariant(selected);
1671
+ onSelect?.(selected);
1672
+ }}
1673
+ render={() => null}
1674
+ />
1675
+ {selectedVariant && variants[selectedVariant]}
1676
+ </span>
1677
+ );
1678
+ }
1679
+
1680
+ // ============================================================================
1681
+ // ESI.ForEach - Iterate over AI-generated list
1682
+ // ============================================================================
1683
+
1684
+ export interface ESIForEachProps<T> {
1685
+ /** Prompt to generate the list */
1686
+ prompt: string;
1687
+ /** Zod schema for each item */
1688
+ itemSchema: ZodType<T, ZodTypeDef, unknown>;
1689
+ /** Render function for each item */
1690
+ render: (item: T, index: number) => ReactNode;
1691
+ /** Max items to generate */
1692
+ maxItems?: number;
1693
+ /** Empty state */
1694
+ empty?: ReactNode;
1695
+ /** Loading state */
1696
+ loading?: ReactNode;
1697
+ /** Wrapper element */
1698
+ as?: 'div' | 'ul' | 'ol' | 'span';
1699
+ /** Class name */
1700
+ className?: string;
1701
+ }
1702
+
1703
+ /**
1704
+ * Generate and render a list of items
1705
+ * @example
1706
+ * <ESI.ForEach
1707
+ * prompt="Generate 5 personalized activity suggestions"
1708
+ * itemSchema={z.object({ title: z.string(), description: z.string() })}
1709
+ * render={(item, i) => <ActivityCard key={i} {...item} />}
1710
+ * />
1711
+ */
1712
+ export function ESIForEach<T>({
1713
+ prompt,
1714
+ itemSchema,
1715
+ render,
1716
+ maxItems = 10,
1717
+ empty = null,
1718
+ loading = '...',
1719
+ as: Wrapper = 'div',
1720
+ className,
1721
+ }: ESIForEachProps<T>): JSX.Element {
1722
+ const [items, setItems] = useState<T[]>([]);
1723
+ const [isLoading, setIsLoading] = useState(true);
1724
+
1725
+ // Create array schema
1726
+ const arraySchema = {
1727
+ safeParse: (val: unknown) => {
1728
+ try {
1729
+ let arr: unknown[];
1730
+ if (Array.isArray(val)) {
1731
+ arr = val;
1732
+ } else if (typeof val === 'string') {
1733
+ arr = JSON.parse(val);
1734
+ } else if (typeof val === 'object' && val !== null && 'items' in val) {
1735
+ arr = (val as { items: unknown[] }).items;
1736
+ } else {
1737
+ return { success: false, error: 'Not an array' };
1738
+ }
1739
+
1740
+ const validItems: T[] = [];
1741
+ for (const item of arr.slice(0, maxItems)) {
1742
+ const result = itemSchema.safeParse(item);
1743
+ if (result.success) {
1744
+ validItems.push(result.data);
1745
+ }
1746
+ }
1747
+ return { success: true, data: validItems };
1748
+ } catch {
1749
+ return { success: false, error: 'Parse error' };
1750
+ }
1751
+ },
1752
+ } as ZodType<T[], ZodTypeDef, unknown>;
1753
+
1754
+ return (
1755
+ <ESIStructured
1756
+ prompt={`${prompt}\n\nRespond with a JSON array of items (max ${maxItems}).`}
1757
+ schema={arraySchema}
1758
+ loading={loading}
1759
+ fallback={empty}
1760
+ className={className}
1761
+ onSuccess={(result) => {
1762
+ setItems(result);
1763
+ setIsLoading(false);
1764
+ }}
1765
+ render={(data) => {
1766
+ if (data.length === 0) return <>{empty}</>;
1767
+ return (
1768
+ <Wrapper className={className}>
1769
+ {data.map((item, i) => render(item, i))}
1770
+ </Wrapper>
1771
+ );
1772
+ }}
1773
+ />
1774
+ );
1775
+ }
1776
+
1777
+ // ============================================================================
1778
+ // ESI.First - Render first child where condition is true
1779
+ // ============================================================================
1780
+
1781
+ export interface ESIFirstProps {
1782
+ /** Prompt with context for evaluation */
1783
+ context?: string;
1784
+ /** Children should be ESI.When components */
1785
+ children: ReactNode;
1786
+ /** Fallback if no condition matches */
1787
+ fallback?: ReactNode;
1788
+ /** Loading state */
1789
+ loading?: ReactNode;
1790
+ /** Class name */
1791
+ className?: string;
1792
+ }
1793
+
1794
+ /**
1795
+ * Render the first child whose condition evaluates to true
1796
+ * @example
1797
+ * <ESI.First fallback={<DefaultContent />}>
1798
+ * <ESI.When condition="User is angry"><CalmingContent /></ESI.When>
1799
+ * <ESI.When condition="User is confused"><HelpContent /></ESI.When>
1800
+ * <ESI.When condition="User is happy"><CelebrateContent /></ESI.When>
1801
+ * </ESI.First>
1802
+ */
1803
+ export function ESIFirst({
1804
+ context,
1805
+ children,
1806
+ fallback = null,
1807
+ loading = null,
1808
+ className,
1809
+ }: ESIFirstProps): JSX.Element {
1810
+ // This is a placeholder - full implementation would evaluate conditions in order
1811
+ // For now, render children wrapped with priority logic
1812
+ return (
1813
+ <span className={className}>
1814
+ {children}
1815
+ {/* If no ESI.When matched, show fallback */}
1816
+ </span>
1817
+ );
1818
+ }
1819
+
1820
+ // ============================================================================
1821
+ // ESI.Clamp - Constrain value within range
1822
+ // ============================================================================
1823
+
1824
+ export interface ESIClampProps {
1825
+ /** Prompt to generate a number */
1826
+ prompt: string;
1827
+ /** Minimum value */
1828
+ min: number;
1829
+ /** Maximum value */
1830
+ max: number;
1831
+ /** Render function */
1832
+ render: (value: number) => ReactNode;
1833
+ /** Default value if generation fails */
1834
+ defaultValue?: number;
1835
+ /** Loading state */
1836
+ loading?: ReactNode;
1837
+ /** Class name */
1838
+ className?: string;
1839
+ }
1840
+
1841
+ /**
1842
+ * Generate a clamped numeric value
1843
+ * @example
1844
+ * <ESI.Clamp
1845
+ * prompt="Rate the urgency of this message from 1-10"
1846
+ * min={1}
1847
+ * max={10}
1848
+ * render={(urgency) => <UrgencyBadge level={urgency} />}
1849
+ * />
1850
+ */
1851
+ export function ESIClamp({
1852
+ prompt,
1853
+ min,
1854
+ max,
1855
+ render,
1856
+ defaultValue,
1857
+ loading = '...',
1858
+ className,
1859
+ }: ESIClampProps): JSX.Element {
1860
+ const numSchema = {
1861
+ safeParse: (val: unknown) => {
1862
+ let num: number;
1863
+ if (typeof val === 'number') {
1864
+ num = val;
1865
+ } else if (typeof val === 'string') {
1866
+ num = parseFloat(val);
1867
+ } else if (typeof val === 'object' && val !== null && 'value' in val) {
1868
+ num = Number((val as { value: unknown }).value);
1869
+ } else {
1870
+ return { success: false, error: 'Not a number' };
1871
+ }
1872
+ if (isNaN(num)) return { success: false, error: 'NaN' };
1873
+ return { success: true, data: Math.max(min, Math.min(max, num)) };
1874
+ },
1875
+ } as ZodType<number, ZodTypeDef, unknown>;
1876
+
1877
+ return (
1878
+ <ESIStructured
1879
+ prompt={`${prompt}\n\nRespond with a number between ${min} and ${max}.`}
1880
+ schema={numSchema}
1881
+ loading={loading}
1882
+ fallback={defaultValue !== undefined ? render(defaultValue) : null}
1883
+ className={className}
1884
+ render={(value) => render(value)}
1885
+ />
1886
+ );
1887
+ }
1888
+
1889
+ // ============================================================================
1890
+ // ESI.Select - Choose from predefined options
1891
+ // ============================================================================
1892
+
1893
+ export interface ESISelectProps<T extends string> {
1894
+ /** Prompt for selection */
1895
+ prompt: string;
1896
+ /** Available options */
1897
+ options: T[];
1898
+ /** Render function for selected option */
1899
+ render: (selected: T) => ReactNode;
1900
+ /** Default if selection fails */
1901
+ defaultOption?: T;
1902
+ /** Loading state */
1903
+ loading?: ReactNode;
1904
+ /** Callback */
1905
+ onSelect?: (selected: T) => void;
1906
+ /** Class name */
1907
+ className?: string;
1908
+ }
1909
+
1910
+ /**
1911
+ * Select from predefined options
1912
+ * @example
1913
+ * <ESI.Select
1914
+ * prompt="What tone should we use for this user?"
1915
+ * options={['formal', 'casual', 'playful', 'empathetic']}
1916
+ * render={(tone) => <Message tone={tone} />}
1917
+ * />
1918
+ */
1919
+ export function ESISelect<T extends string>({
1920
+ prompt,
1921
+ options,
1922
+ render,
1923
+ defaultOption,
1924
+ loading = '...',
1925
+ onSelect,
1926
+ className,
1927
+ }: ESISelectProps<T>): JSX.Element {
1928
+ const optionSchema = {
1929
+ safeParse: (val: unknown) => {
1930
+ const str = String(val).trim().toLowerCase();
1931
+ const match = options.find((o) => o.toLowerCase() === str);
1932
+ if (match) return { success: true, data: match as T };
1933
+ // Partial match
1934
+ const partial = options.find(
1935
+ (o) => str.includes(o.toLowerCase()) || o.toLowerCase().includes(str),
1936
+ );
1937
+ if (partial) return { success: true, data: partial as T };
1938
+ return { success: false, error: 'No match' };
1939
+ },
1940
+ } as ZodType<T, ZodTypeDef, unknown>;
1941
+
1942
+ return (
1943
+ <ESIStructured
1944
+ prompt={`${prompt}\n\nOptions: ${options.join(', ')}\n\nRespond with only one of the options.`}
1945
+ schema={optionSchema}
1946
+ loading={loading}
1947
+ fallback={defaultOption ? render(defaultOption) : null}
1948
+ className={className}
1949
+ onSuccess={onSelect}
1950
+ render={(selected) => render(selected)}
1951
+ />
1952
+ );
1953
+ }
1954
+
1955
+ // ============================================================================
1956
+ // ESI.Score - Generate a normalized score
1957
+ // ============================================================================
1958
+
1959
+ export interface ESIScoreProps {
1960
+ /** What to score */
1961
+ prompt: string;
1962
+ /** Render function */
1963
+ render: (score: number, label: string) => ReactNode;
1964
+ /** Score thresholds and labels */
1965
+ thresholds?: { value: number; label: string }[];
1966
+ /** Loading state */
1967
+ loading?: ReactNode;
1968
+ /** Class name */
1969
+ className?: string;
1970
+ }
1971
+
1972
+ const DEFAULT_THRESHOLDS = [
1973
+ { value: 0.2, label: 'very low' },
1974
+ { value: 0.4, label: 'low' },
1975
+ { value: 0.6, label: 'moderate' },
1976
+ { value: 0.8, label: 'high' },
1977
+ { value: 1.0, label: 'very high' },
1978
+ ];
1979
+
1980
+ /**
1981
+ * Generate a normalized 0-1 score with label
1982
+ * @example
1983
+ * <ESI.Score
1984
+ * prompt="Rate the user's engagement level"
1985
+ * render={(score, label) => <EngagementMeter value={score} label={label} />}
1986
+ * />
1987
+ */
1988
+ export function ESIScore({
1989
+ prompt,
1990
+ render,
1991
+ thresholds = DEFAULT_THRESHOLDS,
1992
+ loading = '...',
1993
+ className,
1994
+ }: ESIScoreProps): JSX.Element {
1995
+ return (
1996
+ <ESIClamp
1997
+ prompt={prompt}
1998
+ min={0}
1999
+ max={1}
2000
+ loading={loading}
2001
+ className={className}
2002
+ render={(score) => {
2003
+ const label =
2004
+ thresholds.find((t) => score <= t.value)?.label || 'unknown';
2005
+ return render(score, label);
2006
+ }}
2007
+ />
2008
+ );
2009
+ }
2010
+
2011
+ // ============================================================================
2012
+ // ESI Namespace Export (Extended)
2013
+ // ============================================================================
2014
+
2015
+ export const ESIControl = {
2016
+ // Core
2017
+ Structured: ESIStructured,
2018
+
2019
+ // Conditionals
2020
+ If: ESIIf,
2021
+ Show: ESIShow,
2022
+ Hide: ESIHide,
2023
+ When: ESIWhen,
2024
+ Unless: ESIUnless,
2025
+
2026
+ // Pattern Matching
2027
+ Match: ESIMatch,
2028
+ Case: ESICase,
2029
+ Default: ESIDefault,
2030
+ First: ESIFirst,
2031
+
2032
+ // Gates
2033
+ TierGate: ESITierGate,
2034
+ EmotionGate: ESIEmotionGate,
2035
+ TimeGate: ESITimeGate,
2036
+
2037
+ // Iteration & Selection
2038
+ ForEach: ESIForEach,
2039
+ Select: ESISelect,
2040
+ ABTest: ESIABTest,
2041
+
2042
+ // Numeric
2043
+ Clamp: ESIClamp,
2044
+ Score: ESIScore,
2045
+
2046
+ // Advanced
2047
+ Collaborative: ESICollaborative,
2048
+ Reflect: ESIReflect,
2049
+ Optimize: ESIOptimize,
2050
+ Auto: ESIAuto,
2051
+ };
2052
+
2053
+ export default ESIControl;