@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,1744 @@
1
+ /**
2
+ * ESI Format Components (React)
3
+ *
4
+ * Output transformation wrappers for ESI components.
5
+ * These components wrap other ESI components and transform their output.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * // Render inference output as markdown
10
+ * <ESI.Markdown>
11
+ * <ESI.Infer>Generate documentation for this API endpoint</ESI.Infer>
12
+ * </ESI.Markdown>
13
+ *
14
+ * // Render LaTeX math expressions
15
+ * <ESI.Latex>
16
+ * <ESI.Infer>Write the quadratic formula</ESI.Infer>
17
+ * </ESI.Latex>
18
+ *
19
+ * // Pretty-print JSON output
20
+ * <ESI.Json>
21
+ * <ESI.Structured schema={mySchema}>Analyze this data</ESI.Structured>
22
+ * </ESI.Json>
23
+ * ```
24
+ */
25
+
26
+ import {
27
+ Children,
28
+ cloneElement,
29
+ isValidElement,
30
+ useState,
31
+ useEffect,
32
+ useMemo,
33
+ useCallback,
34
+ useContext,
35
+ createElement,
36
+ createContext,
37
+ type ReactNode,
38
+ type FC,
39
+ type ReactElement,
40
+ type ElementType,
41
+ } from 'react';
42
+
43
+ // Try to import useESI, but provide fallback if not in ESI context
44
+ let useESIContext:
45
+ | (() => {
46
+ process: (
47
+ directive: unknown,
48
+ ) => Promise<{ success: boolean; output?: string }>;
49
+ })
50
+ | null = null;
51
+ try {
52
+ // Dynamic import to avoid circular dependency
53
+ const esiReact = require('./esi-react');
54
+ useESIContext = esiReact.useESI;
55
+ } catch {
56
+ // ESI not available - code generation features won't work
57
+ }
58
+
59
+ // ============================================================================
60
+ // Types
61
+ // ============================================================================
62
+
63
+ /** Supported wrapper element types */
64
+ type WrapperElement =
65
+ | 'div'
66
+ | 'span'
67
+ | 'pre'
68
+ | 'code'
69
+ | 'section'
70
+ | 'article'
71
+ | 'aside';
72
+
73
+ export interface ESIFormatProps {
74
+ /** ESI component(s) to wrap */
75
+ children: ReactNode;
76
+ /** CSS class for the output container */
77
+ className?: string;
78
+ /** Custom wrapper element */
79
+ as?: WrapperElement;
80
+ /** Fallback content if transformation fails */
81
+ fallback?: ReactNode;
82
+ /** Whether to sanitize output (default: true for HTML-producing formats) */
83
+ sanitize?: boolean;
84
+ }
85
+
86
+ export interface ESIMarkdownProps extends ESIFormatProps {
87
+ /** GitHub Flavored Markdown support */
88
+ gfm?: boolean;
89
+ /** Enable syntax highlighting for code blocks */
90
+ syntaxHighlight?: boolean;
91
+ /** Theme for syntax highlighting */
92
+ syntaxTheme?: 'light' | 'dark' | 'auto';
93
+ /** Allow raw HTML in markdown (default: false for security) */
94
+ allowHtml?: boolean;
95
+ /** Custom link target */
96
+ linkTarget?: '_blank' | '_self' | '_parent' | '_top';
97
+ }
98
+
99
+ export interface ESILatexProps extends ESIFormatProps {
100
+ /** Rendering mode */
101
+ mode?: 'inline' | 'block' | 'auto';
102
+ /** Display mode (larger, centered equations) */
103
+ displayMode?: boolean;
104
+ /** Error color for invalid LaTeX */
105
+ errorColor?: string;
106
+ /** Trust user input (allow dangerous commands) */
107
+ trust?: boolean;
108
+ }
109
+
110
+ export interface ESIJsonProps extends Omit<ESIFormatProps, 'as'> {
111
+ /** Custom wrapper element */
112
+ as?: WrapperElement;
113
+ /** Indentation spaces */
114
+ indent?: number;
115
+ /** Syntax highlighting */
116
+ syntaxHighlight?: boolean;
117
+ /** Theme for syntax highlighting */
118
+ theme?: 'light' | 'dark' | 'auto';
119
+ /** Collapse objects/arrays by default */
120
+ collapsed?: boolean;
121
+ /** Max depth before collapsing */
122
+ collapseDepth?: number;
123
+ /** Enable copy button */
124
+ copyable?: boolean;
125
+ }
126
+
127
+ export interface ESIPlaintextProps extends Omit<ESIFormatProps, 'as'> {
128
+ /** Custom wrapper element */
129
+ as?: WrapperElement;
130
+ /** Preserve whitespace */
131
+ preserveWhitespace?: boolean;
132
+ /** Word wrap */
133
+ wordWrap?: boolean;
134
+ /** Max width (characters) before wrapping */
135
+ maxWidth?: number;
136
+ }
137
+
138
+ /** Supported code-specialized models */
139
+ export type CodeModel =
140
+ | 'codestral' // Mistral Codestral
141
+ | 'deepseek' // DeepSeek Coder
142
+ | 'starcoder' // StarCoder
143
+ | 'codellama' // Code Llama
144
+ | 'qwen-coder' // Qwen Coder
145
+ | 'claude' // Claude (general but excellent at code)
146
+ | 'gpt-4' // GPT-4 (general but excellent at code)
147
+ | 'llm'; // Default LLM
148
+
149
+ export interface ESICodeProps extends Omit<ESIFormatProps, 'as'> {
150
+ /** Custom wrapper element */
151
+ as?: WrapperElement;
152
+ /** Programming language for syntax highlighting */
153
+ language?: string;
154
+ /** Auto-detect language using AI inference */
155
+ autoDetect?: boolean;
156
+ /** Generate code from natural language description (text-to-code) */
157
+ generateFrom?: string;
158
+ /** Code model to use for generation/detection */
159
+ model?: CodeModel;
160
+ /** Show line numbers */
161
+ lineNumbers?: boolean;
162
+ /** Starting line number */
163
+ startLine?: number;
164
+ /** Highlight specific lines */
165
+ highlightLines?: number[];
166
+ /** Theme */
167
+ theme?: 'light' | 'dark' | 'auto';
168
+ /** Enable copy button */
169
+ copyable?: boolean;
170
+ /** Temperature for code generation (lower = more deterministic) */
171
+ temperature?: number;
172
+ /** Callback when language is detected */
173
+ onLanguageDetect?: (language: string) => void;
174
+ /** Callback when code is generated */
175
+ onGenerate?: (code: string, language: string) => void;
176
+ }
177
+
178
+ // ============================================================================
179
+ // Utility: Extract text from children
180
+ // ============================================================================
181
+
182
+ /**
183
+ * Intercept child component output and extract text content
184
+ */
185
+ function useChildOutput(children: ReactNode): {
186
+ output: string | null;
187
+ isLoading: boolean;
188
+ error: string | null;
189
+ } {
190
+ const [output, setOutput] = useState<string | null>(null);
191
+ const [isLoading, setIsLoading] = useState(true);
192
+ const [error, setError] = useState<string | null>(null);
193
+
194
+ // We need to render children and capture their output
195
+ // This is done by providing a custom render prop
196
+ useEffect(() => {
197
+ // Simple case: children is just a string
198
+ if (typeof children === 'string') {
199
+ setOutput(children);
200
+ setIsLoading(false);
201
+ return;
202
+ }
203
+
204
+ // For React elements, we need to intercept the output
205
+ // This will be handled by the wrapper logic below
206
+ }, [children]);
207
+
208
+ return { output, isLoading, error };
209
+ }
210
+
211
+ /** Props that ESI components typically have */
212
+ interface ESIComponentProps {
213
+ render?: (result: unknown) => ReactNode;
214
+ onComplete?: (result: unknown) => void;
215
+ [key: string]: unknown;
216
+ }
217
+
218
+ /**
219
+ * Wrap child ESI components to intercept their output
220
+ */
221
+ function wrapChildren(
222
+ children: ReactNode,
223
+ onOutput: (text: string) => void,
224
+ ): ReactNode {
225
+ return Children.map(children, (child) => {
226
+ if (!isValidElement(child)) {
227
+ // Plain text or null
228
+ if (typeof child === 'string') {
229
+ onOutput(child);
230
+ }
231
+ return null; // Don't render the raw child, we'll render transformed output
232
+ }
233
+
234
+ // Cast to our expected props shape
235
+ const childProps = child.props as ESIComponentProps;
236
+ const originalRender = childProps.render;
237
+ const originalOnComplete = childProps.onComplete;
238
+
239
+ // Clone with new props
240
+ const newProps: ESIComponentProps = {
241
+ ...childProps,
242
+ render: (result: unknown) => {
243
+ // Extract text from result
244
+ const text =
245
+ typeof result === 'string'
246
+ ? result
247
+ : typeof result === 'object' &&
248
+ result !== null &&
249
+ 'output' in result
250
+ ? String((result as { output: unknown }).output)
251
+ : JSON.stringify(result);
252
+
253
+ onOutput(text);
254
+
255
+ // Still call original render if provided (for side effects)
256
+ return originalRender ? originalRender(result) : null;
257
+ },
258
+ onComplete: (result: unknown) => {
259
+ // Also capture from onComplete
260
+ if (result && typeof result === 'object' && 'output' in result) {
261
+ onOutput(String((result as { output: unknown }).output));
262
+ }
263
+ originalOnComplete?.(result);
264
+ },
265
+ };
266
+
267
+ return cloneElement(child as ReactElement<ESIComponentProps>, newProps);
268
+ });
269
+ }
270
+
271
+ // ============================================================================
272
+ // Simple Markdown Parser (no dependencies)
273
+ // ============================================================================
274
+
275
+ /**
276
+ * Simple markdown to HTML converter
277
+ * For production, consider using a library like marked or remark
278
+ */
279
+ function parseMarkdown(
280
+ text: string,
281
+ options: {
282
+ gfm?: boolean;
283
+ allowHtml?: boolean;
284
+ linkTarget?: string;
285
+ } = {},
286
+ ): string {
287
+ const { gfm = true, allowHtml = false, linkTarget = '_blank' } = options;
288
+
289
+ let html = text;
290
+
291
+ // Escape HTML if not allowed
292
+ if (!allowHtml) {
293
+ html = html
294
+ .replace(/&/g, '&amp;')
295
+ .replace(/</g, '&lt;')
296
+ .replace(/>/g, '&gt;');
297
+ }
298
+
299
+ // Code blocks (fenced)
300
+ html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
301
+ const langClass = lang ? ` class="language-${lang}"` : '';
302
+ return `<pre><code${langClass}>${code.trim()}</code></pre>`;
303
+ });
304
+
305
+ // Inline code
306
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
307
+
308
+ // Headers
309
+ html = html.replace(/^######\s+(.+)$/gm, '<h6>$1</h6>');
310
+ html = html.replace(/^#####\s+(.+)$/gm, '<h5>$1</h5>');
311
+ html = html.replace(/^####\s+(.+)$/gm, '<h4>$1</h4>');
312
+ html = html.replace(/^###\s+(.+)$/gm, '<h3>$1</h3>');
313
+ html = html.replace(/^##\s+(.+)$/gm, '<h2>$1</h2>');
314
+ html = html.replace(/^#\s+(.+)$/gm, '<h1>$1</h1>');
315
+
316
+ // Bold and italic
317
+ html = html.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
318
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
319
+ html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
320
+ html = html.replace(/___(.+?)___/g, '<strong><em>$1</em></strong>');
321
+ html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
322
+ html = html.replace(/_(.+?)_/g, '<em>$1</em>');
323
+
324
+ // Strikethrough (GFM)
325
+ if (gfm) {
326
+ html = html.replace(/~~(.+?)~~/g, '<del>$1</del>');
327
+ }
328
+
329
+ // Links
330
+ html = html.replace(
331
+ /\[([^\]]+)\]\(([^)]+)\)/g,
332
+ `<a href="$2" target="${linkTarget}" rel="noopener noreferrer">$1</a>`,
333
+ );
334
+
335
+ // Images
336
+ html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" />');
337
+
338
+ // Blockquotes
339
+ html = html.replace(/^>\s+(.+)$/gm, '<blockquote>$1</blockquote>');
340
+
341
+ // Horizontal rules
342
+ html = html.replace(/^(---|\*\*\*|___)$/gm, '<hr />');
343
+
344
+ // Unordered lists
345
+ html = html.replace(/^[\*\-\+]\s+(.+)$/gm, '<li>$1</li>');
346
+ html = html.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>');
347
+
348
+ // Ordered lists
349
+ html = html.replace(/^\d+\.\s+(.+)$/gm, '<li>$1</li>');
350
+
351
+ // Task lists (GFM)
352
+ if (gfm) {
353
+ html = html.replace(
354
+ /<li>\[ \]\s*(.+)<\/li>/g,
355
+ '<li><input type="checkbox" disabled /> $1</li>',
356
+ );
357
+ html = html.replace(
358
+ /<li>\[x\]\s*(.+)<\/li>/gi,
359
+ '<li><input type="checkbox" disabled checked /> $1</li>',
360
+ );
361
+ }
362
+
363
+ // Tables (GFM) - simplified
364
+ if (gfm) {
365
+ const tableRegex = /^\|(.+)\|$/gm;
366
+ const rows = html.match(tableRegex);
367
+ if (rows && rows.length >= 2) {
368
+ // Check for separator row
369
+ const separatorIdx = rows.findIndex((row) => /^\|[\s\-:|]+\|$/.test(row));
370
+ if (separatorIdx === 1) {
371
+ const headerRow = rows[0];
372
+ const dataRows = rows.slice(2);
373
+
374
+ const headerCells = headerRow.split('|').filter((c) => c.trim());
375
+ const headerHtml = `<thead><tr>${headerCells.map((c) => `<th>${c.trim()}</th>`).join('')}</tr></thead>`;
376
+
377
+ const bodyHtml = dataRows
378
+ .map((row) => {
379
+ const cells = row.split('|').filter((c) => c.trim());
380
+ return `<tr>${cells.map((c) => `<td>${c.trim()}</td>`).join('')}</tr>`;
381
+ })
382
+ .join('');
383
+
384
+ const tableHtml = `<table>${headerHtml}<tbody>${bodyHtml}</tbody></table>`;
385
+
386
+ // Replace the original table markdown
387
+ const tableMarkdown = rows
388
+ .slice(0, separatorIdx + 1 + dataRows.length)
389
+ .join('\n');
390
+ html = html.replace(tableMarkdown, tableHtml);
391
+ }
392
+ }
393
+ }
394
+
395
+ // Paragraphs - wrap loose text
396
+ html = html.replace(/^(?!<[a-z]|$)(.+)$/gm, '<p>$1</p>');
397
+
398
+ // Clean up extra paragraph tags around block elements
399
+ html = html.replace(
400
+ /<p>(<(?:h[1-6]|ul|ol|li|blockquote|pre|table|hr)[^>]*>)/g,
401
+ '$1',
402
+ );
403
+ html = html.replace(
404
+ /(<\/(?:h[1-6]|ul|ol|li|blockquote|pre|table|hr)>)<\/p>/g,
405
+ '$1',
406
+ );
407
+
408
+ // Line breaks
409
+ html = html.replace(/\n\n/g, '</p><p>');
410
+ html = html.replace(/\n/g, '<br />');
411
+
412
+ return html;
413
+ }
414
+
415
+ // ============================================================================
416
+ // Simple LaTeX to HTML (basic support)
417
+ // ============================================================================
418
+
419
+ /**
420
+ * Simple LaTeX to HTML converter
421
+ * For production, use KaTeX or MathJax
422
+ */
423
+ function parseLatex(
424
+ text: string,
425
+ options: {
426
+ mode?: 'inline' | 'block' | 'auto';
427
+ displayMode?: boolean;
428
+ } = {},
429
+ ): string {
430
+ const { mode = 'auto', displayMode = false } = options;
431
+
432
+ // Check if we should use display mode
433
+ const isBlock =
434
+ mode === 'block' ||
435
+ (mode === 'auto' && (text.includes('\\[') || text.includes('$$')));
436
+
437
+ // Convert common LaTeX to HTML entities/CSS
438
+ let html = text;
439
+
440
+ // Display math delimiters
441
+ html = html.replace(
442
+ /\$\$([\s\S]+?)\$\$/g,
443
+ '<div class="math-block">$1</div>',
444
+ );
445
+ html = html.replace(
446
+ /\\\[([\s\S]+?)\\\]/g,
447
+ '<div class="math-block">$1</div>',
448
+ );
449
+
450
+ // Inline math delimiters
451
+ html = html.replace(/\$([^$]+)\$/g, '<span class="math-inline">$1</span>');
452
+ html = html.replace(/\\\((.+?)\\\)/g, '<span class="math-inline">$1</span>');
453
+
454
+ // Common symbols
455
+ const symbols: Record<string, string> = {
456
+ '\\alpha': 'α',
457
+ '\\beta': 'β',
458
+ '\\gamma': 'γ',
459
+ '\\delta': 'δ',
460
+ '\\epsilon': 'ε',
461
+ '\\zeta': 'ζ',
462
+ '\\eta': 'η',
463
+ '\\theta': 'θ',
464
+ '\\iota': 'ι',
465
+ '\\kappa': 'κ',
466
+ '\\lambda': 'λ',
467
+ '\\mu': 'μ',
468
+ '\\nu': 'ν',
469
+ '\\xi': 'ξ',
470
+ '\\pi': 'π',
471
+ '\\rho': 'ρ',
472
+ '\\sigma': 'σ',
473
+ '\\tau': 'τ',
474
+ '\\upsilon': 'υ',
475
+ '\\phi': 'φ',
476
+ '\\chi': 'χ',
477
+ '\\psi': 'ψ',
478
+ '\\omega': 'ω',
479
+ '\\Gamma': 'Γ',
480
+ '\\Delta': 'Δ',
481
+ '\\Theta': 'Θ',
482
+ '\\Lambda': 'Λ',
483
+ '\\Xi': 'Ξ',
484
+ '\\Pi': 'Π',
485
+ '\\Sigma': 'Σ',
486
+ '\\Phi': 'Φ',
487
+ '\\Psi': 'Ψ',
488
+ '\\Omega': 'Ω',
489
+ '\\infty': '∞',
490
+ '\\pm': '±',
491
+ '\\mp': '∓',
492
+ '\\times': '×',
493
+ '\\div': '÷',
494
+ '\\cdot': '·',
495
+ '\\leq': '≤',
496
+ '\\geq': '≥',
497
+ '\\neq': '≠',
498
+ '\\approx': '≈',
499
+ '\\equiv': '≡',
500
+ '\\subset': '⊂',
501
+ '\\supset': '⊃',
502
+ '\\in': '∈',
503
+ '\\notin': '∉',
504
+ '\\cup': '∪',
505
+ '\\cap': '∩',
506
+ '\\emptyset': '∅',
507
+ '\\forall': '∀',
508
+ '\\exists': '∃',
509
+ '\\nabla': '∇',
510
+ '\\partial': '∂',
511
+ '\\sum': '∑',
512
+ '\\prod': '∏',
513
+ '\\int': '∫',
514
+ '\\oint': '∮',
515
+ '\\sqrt': '√',
516
+ '\\therefore': '∴',
517
+ '\\because': '∵',
518
+ '\\angle': '∠',
519
+ '\\perp': '⊥',
520
+ '\\parallel': '∥',
521
+ '\\rightarrow': '→',
522
+ '\\leftarrow': '←',
523
+ '\\Rightarrow': '⇒',
524
+ '\\Leftarrow': '⇐',
525
+ '\\leftrightarrow': '↔',
526
+ '\\Leftrightarrow': '⇔',
527
+ };
528
+
529
+ for (const [latex, symbol] of Object.entries(symbols)) {
530
+ html = html.replace(new RegExp(latex.replace(/\\/g, '\\\\'), 'g'), symbol);
531
+ }
532
+
533
+ // Fractions: \frac{a}{b}
534
+ html = html.replace(
535
+ /\\frac\{([^}]+)\}\{([^}]+)\}/g,
536
+ '<span class="frac"><span class="num">$1</span><span class="den">$2</span></span>',
537
+ );
538
+
539
+ // Superscript: ^{x} or ^x
540
+ html = html.replace(/\^{([^}]+)}/g, '<sup>$1</sup>');
541
+ html = html.replace(/\^(\w)/g, '<sup>$1</sup>');
542
+
543
+ // Subscript: _{x} or _x
544
+ html = html.replace(/_{([^}]+)}/g, '<sub>$1</sub>');
545
+ html = html.replace(/_(\w)/g, '<sub>$1</sub>');
546
+
547
+ // Square root with argument
548
+ html = html.replace(/\\sqrt\{([^}]+)\}/g, '√($1)');
549
+
550
+ // Bold/text commands
551
+ html = html.replace(/\\textbf\{([^}]+)\}/g, '<strong>$1</strong>');
552
+ html = html.replace(/\\textit\{([^}]+)\}/g, '<em>$1</em>');
553
+ html = html.replace(/\\text\{([^}]+)\}/g, '$1');
554
+
555
+ // Remove remaining backslash commands we don't handle
556
+ html = html.replace(/\\[a-zA-Z]+/g, '');
557
+
558
+ // Wrap in display container if needed
559
+ if (displayMode || isBlock) {
560
+ html = `<div class="math-display">${html}</div>`;
561
+ }
562
+
563
+ return html;
564
+ }
565
+
566
+ // ============================================================================
567
+ // JSON Formatter
568
+ // ============================================================================
569
+
570
+ /**
571
+ * Format and optionally highlight JSON
572
+ */
573
+ function formatJson(
574
+ text: string,
575
+ options: {
576
+ indent?: number;
577
+ syntaxHighlight?: boolean;
578
+ theme?: 'light' | 'dark' | 'auto';
579
+ } = {},
580
+ ): string {
581
+ const { indent = 2, syntaxHighlight = true } = options;
582
+
583
+ try {
584
+ // Try to parse as JSON
585
+ const parsed = JSON.parse(text);
586
+ const formatted = JSON.stringify(parsed, null, indent);
587
+
588
+ if (!syntaxHighlight) {
589
+ return `<pre><code>${escapeHtml(formatted)}</code></pre>`;
590
+ }
591
+
592
+ // Simple syntax highlighting
593
+ let highlighted = escapeHtml(formatted);
594
+
595
+ // Strings (but not inside already highlighted)
596
+ highlighted = highlighted.replace(
597
+ /("(?:[^"\\]|\\.)*")\s*:/g,
598
+ '<span class="json-key">$1</span>:',
599
+ );
600
+ highlighted = highlighted.replace(
601
+ /:\s*("(?:[^"\\]|\\.)*")/g,
602
+ ': <span class="json-string">$1</span>',
603
+ );
604
+
605
+ // Numbers
606
+ highlighted = highlighted.replace(
607
+ /:\s*(-?\d+\.?\d*(?:[eE][+-]?\d+)?)/g,
608
+ ': <span class="json-number">$1</span>',
609
+ );
610
+
611
+ // Booleans and null
612
+ highlighted = highlighted.replace(
613
+ /:\s*(true|false|null)/g,
614
+ ': <span class="json-$1">$1</span>',
615
+ );
616
+
617
+ return `<pre class="json-highlight"><code>${highlighted}</code></pre>`;
618
+ } catch {
619
+ // Not valid JSON, return as-is
620
+ return `<pre><code>${escapeHtml(text)}</code></pre>`;
621
+ }
622
+ }
623
+
624
+ function escapeHtml(text: string): string {
625
+ return text
626
+ .replace(/&/g, '&amp;')
627
+ .replace(/</g, '&lt;')
628
+ .replace(/>/g, '&gt;')
629
+ .replace(/"/g, '&quot;')
630
+ .replace(/'/g, '&#039;');
631
+ }
632
+
633
+ // ============================================================================
634
+ // Code Formatter
635
+ // ============================================================================
636
+
637
+ /**
638
+ * Format code with optional syntax highlighting
639
+ */
640
+ function formatCode(
641
+ text: string,
642
+ options: {
643
+ language?: string;
644
+ lineNumbers?: boolean;
645
+ startLine?: number;
646
+ highlightLines?: number[];
647
+ } = {},
648
+ ): string {
649
+ const {
650
+ language,
651
+ lineNumbers = false,
652
+ startLine = 1,
653
+ highlightLines = [],
654
+ } = options;
655
+
656
+ const lines = text.split('\n');
657
+ const langClass = language ? ` language-${language}` : '';
658
+
659
+ if (!lineNumbers) {
660
+ return `<pre><code class="code-block${langClass}">${escapeHtml(text)}</code></pre>`;
661
+ }
662
+
663
+ const lineHtml = lines
664
+ .map((line, i) => {
665
+ const lineNum = startLine + i;
666
+ const isHighlighted = highlightLines.includes(lineNum);
667
+ const highlightClass = isHighlighted ? ' highlighted' : '';
668
+ return `<span class="line${highlightClass}"><span class="line-number">${lineNum}</span><span class="line-content">${escapeHtml(line)}</span></span>`;
669
+ })
670
+ .join('\n');
671
+
672
+ return `<pre class="code-with-lines${langClass}"><code>${lineHtml}</code></pre>`;
673
+ }
674
+
675
+ // ============================================================================
676
+ // ESI.Markdown Component
677
+ // ============================================================================
678
+
679
+ /**
680
+ * Render ESI output as markdown
681
+ *
682
+ * @example
683
+ * ```tsx
684
+ * <ESI.Markdown gfm>
685
+ * <ESI.Infer>Generate API documentation for the /users endpoint</ESI.Infer>
686
+ * </ESI.Markdown>
687
+ * ```
688
+ */
689
+ export const ESIMarkdown: FC<ESIMarkdownProps> = ({
690
+ children,
691
+ className,
692
+ as: Wrapper = 'div',
693
+ fallback,
694
+ gfm = true,
695
+ syntaxHighlight = false,
696
+ allowHtml = false,
697
+ linkTarget = '_blank',
698
+ }) => {
699
+ const [output, setOutput] = useState<string | null>(null);
700
+ const [isLoading, setIsLoading] = useState(true);
701
+
702
+ // Handle string children directly
703
+ if (typeof children === 'string') {
704
+ const html = parseMarkdown(children, { gfm, allowHtml, linkTarget });
705
+ return (
706
+ <Wrapper
707
+ className={className}
708
+ dangerouslySetInnerHTML={{ __html: html }}
709
+ />
710
+ );
711
+ }
712
+
713
+ // For ESI components, wrap and intercept
714
+ const wrappedChildren = useMemo(() => {
715
+ return wrapChildren(children, (text) => {
716
+ setOutput(text);
717
+ setIsLoading(false);
718
+ });
719
+ }, [children]);
720
+
721
+ if (isLoading) {
722
+ return (
723
+ <Wrapper className={className}>
724
+ {wrappedChildren}
725
+ {/* Hidden wrapper to intercept output */}
726
+ </Wrapper>
727
+ );
728
+ }
729
+
730
+ if (!output) {
731
+ return <Wrapper className={className}>{fallback}</Wrapper>;
732
+ }
733
+
734
+ const html = parseMarkdown(output, { gfm, allowHtml, linkTarget });
735
+
736
+ return (
737
+ <Wrapper
738
+ className={`esi-markdown ${className || ''}`}
739
+ dangerouslySetInnerHTML={{ __html: html }}
740
+ />
741
+ );
742
+ };
743
+
744
+ // ============================================================================
745
+ // ESI.Latex Component
746
+ // ============================================================================
747
+
748
+ /**
749
+ * Render ESI output as LaTeX math
750
+ *
751
+ * @example
752
+ * ```tsx
753
+ * <ESI.Latex displayMode>
754
+ * <ESI.Infer>Write the quadratic formula in LaTeX</ESI.Infer>
755
+ * </ESI.Latex>
756
+ * ```
757
+ */
758
+ export const ESILatex: FC<ESILatexProps> = ({
759
+ children,
760
+ className,
761
+ as: Wrapper = 'div',
762
+ fallback,
763
+ mode = 'auto',
764
+ displayMode = false,
765
+ }) => {
766
+ const [output, setOutput] = useState<string | null>(null);
767
+ const [isLoading, setIsLoading] = useState(true);
768
+
769
+ // Handle string children directly
770
+ if (typeof children === 'string') {
771
+ const html = parseLatex(children, { mode, displayMode });
772
+ return (
773
+ <Wrapper
774
+ className={`esi-latex ${className || ''}`}
775
+ dangerouslySetInnerHTML={{ __html: html }}
776
+ />
777
+ );
778
+ }
779
+
780
+ const wrappedChildren = useMemo(() => {
781
+ return wrapChildren(children, (text) => {
782
+ setOutput(text);
783
+ setIsLoading(false);
784
+ });
785
+ }, [children]);
786
+
787
+ if (isLoading) {
788
+ return <Wrapper className={className}>{wrappedChildren}</Wrapper>;
789
+ }
790
+
791
+ if (!output) {
792
+ return <Wrapper className={className}>{fallback}</Wrapper>;
793
+ }
794
+
795
+ const html = parseLatex(output, { mode, displayMode });
796
+
797
+ return (
798
+ <Wrapper
799
+ className={`esi-latex ${className || ''}`}
800
+ dangerouslySetInnerHTML={{ __html: html }}
801
+ />
802
+ );
803
+ };
804
+
805
+ // ============================================================================
806
+ // ESI.Json Component
807
+ // ============================================================================
808
+
809
+ /**
810
+ * Render ESI output as formatted JSON
811
+ *
812
+ * @example
813
+ * ```tsx
814
+ * <ESI.Json indent={4} syntaxHighlight>
815
+ * <ESI.Structured schema={dataSchema}>Analyze this data</ESI.Structured>
816
+ * </ESI.Json>
817
+ * ```
818
+ */
819
+ export const ESIJson: FC<ESIJsonProps> = ({
820
+ children,
821
+ className,
822
+ as: Wrapper = 'div',
823
+ fallback,
824
+ indent = 2,
825
+ syntaxHighlight = true,
826
+ theme = 'auto',
827
+ copyable = false,
828
+ }) => {
829
+ const [output, setOutput] = useState<string | null>(null);
830
+ const [isLoading, setIsLoading] = useState(true);
831
+ const [copied, setCopied] = useState(false);
832
+
833
+ const handleCopy = () => {
834
+ if (output) {
835
+ navigator.clipboard.writeText(output);
836
+ setCopied(true);
837
+ setTimeout(() => setCopied(false), 2000);
838
+ }
839
+ };
840
+
841
+ // Handle string children directly
842
+ if (typeof children === 'string') {
843
+ const html = formatJson(children, { indent, syntaxHighlight, theme });
844
+ return (
845
+ <Wrapper className={`esi-json ${className || ''}`}>
846
+ {copyable && (
847
+ <button
848
+ className="esi-json-copy"
849
+ onClick={handleCopy}
850
+ aria-label="Copy JSON"
851
+ >
852
+ {copied ? '✓' : '⎘'}
853
+ </button>
854
+ )}
855
+ <div dangerouslySetInnerHTML={{ __html: html }} />
856
+ </Wrapper>
857
+ );
858
+ }
859
+
860
+ const wrappedChildren = useMemo(() => {
861
+ return wrapChildren(children, (text) => {
862
+ setOutput(text);
863
+ setIsLoading(false);
864
+ });
865
+ }, [children]);
866
+
867
+ if (isLoading) {
868
+ return <Wrapper className={className}>{wrappedChildren}</Wrapper>;
869
+ }
870
+
871
+ if (!output) {
872
+ return <Wrapper className={className}>{fallback}</Wrapper>;
873
+ }
874
+
875
+ const html = formatJson(output, { indent, syntaxHighlight, theme });
876
+
877
+ return (
878
+ <Wrapper className={`esi-json ${className || ''}`}>
879
+ {copyable && (
880
+ <button
881
+ className="esi-json-copy"
882
+ onClick={handleCopy}
883
+ aria-label="Copy JSON"
884
+ >
885
+ {copied ? '✓' : '⎘'}
886
+ </button>
887
+ )}
888
+ <div dangerouslySetInnerHTML={{ __html: html }} />
889
+ </Wrapper>
890
+ );
891
+ };
892
+
893
+ // ============================================================================
894
+ // ESI.Plaintext Component
895
+ // ============================================================================
896
+
897
+ /**
898
+ * Render ESI output as plain text (strips formatting)
899
+ *
900
+ * @example
901
+ * ```tsx
902
+ * <ESI.Plaintext preserveWhitespace>
903
+ * <ESI.Infer>Generate ASCII art</ESI.Infer>
904
+ * </ESI.Plaintext>
905
+ * ```
906
+ */
907
+ export const ESIPlaintext: FC<ESIPlaintextProps> = ({
908
+ children,
909
+ className,
910
+ as: Wrapper = 'div',
911
+ fallback,
912
+ preserveWhitespace = true,
913
+ wordWrap = true,
914
+ maxWidth,
915
+ }) => {
916
+ const [output, setOutput] = useState<string | null>(null);
917
+ const [isLoading, setIsLoading] = useState(true);
918
+
919
+ const style = useMemo(
920
+ () => ({
921
+ whiteSpace: preserveWhitespace
922
+ ? ('pre-wrap' as const)
923
+ : ('normal' as const),
924
+ wordWrap: wordWrap ? ('break-word' as const) : ('normal' as const),
925
+ maxWidth: maxWidth ? `${maxWidth}ch` : undefined,
926
+ fontFamily: 'monospace',
927
+ }),
928
+ [preserveWhitespace, wordWrap, maxWidth],
929
+ );
930
+
931
+ // Handle string children directly
932
+ if (typeof children === 'string') {
933
+ return (
934
+ <Wrapper className={`esi-plaintext ${className || ''}`} style={style}>
935
+ {children}
936
+ </Wrapper>
937
+ );
938
+ }
939
+
940
+ const wrappedChildren = useMemo(() => {
941
+ return wrapChildren(children, (text) => {
942
+ setOutput(text);
943
+ setIsLoading(false);
944
+ });
945
+ }, [children]);
946
+
947
+ if (isLoading) {
948
+ return <Wrapper className={className}>{wrappedChildren}</Wrapper>;
949
+ }
950
+
951
+ if (!output) {
952
+ return <Wrapper className={className}>{fallback}</Wrapper>;
953
+ }
954
+
955
+ return (
956
+ <Wrapper className={`esi-plaintext ${className || ''}`} style={style}>
957
+ {output}
958
+ </Wrapper>
959
+ );
960
+ };
961
+
962
+ // ============================================================================
963
+ // ESI.Code Component
964
+ // ============================================================================
965
+
966
+ /**
967
+ * Render ESI output as code with optional syntax highlighting and AI generation
968
+ *
969
+ * @example
970
+ * ```tsx
971
+ * // Wrap inference output as code
972
+ * <ESI.Code language="typescript" lineNumbers>
973
+ * <ESI.Infer>Write a TypeScript function to sort an array</ESI.Infer>
974
+ * </ESI.Code>
975
+ *
976
+ * // Generate code from natural language (text-to-code)
977
+ * <ESI.Code
978
+ * generateFrom="A React hook that fetches user data"
979
+ * language="typescript"
980
+ * model="codestral"
981
+ * />
982
+ *
983
+ * // Auto-detect language
984
+ * <ESI.Code autoDetect model="deepseek">
985
+ * {someCodeString}
986
+ * </ESI.Code>
987
+ * ```
988
+ */
989
+ export const ESICode: FC<ESICodeProps> = ({
990
+ children,
991
+ className,
992
+ as: Wrapper = 'div',
993
+ fallback,
994
+ language,
995
+ autoDetect = false,
996
+ generateFrom,
997
+ model = 'codestral',
998
+ lineNumbers = false,
999
+ startLine = 1,
1000
+ highlightLines = [],
1001
+ copyable = false,
1002
+ temperature = 0.2,
1003
+ onLanguageDetect,
1004
+ onGenerate,
1005
+ }) => {
1006
+ const [output, setOutput] = useState<string | null>(null);
1007
+ const [detectedLang, setDetectedLang] = useState<string | undefined>(
1008
+ language,
1009
+ );
1010
+ const [isLoading, setIsLoading] = useState(true);
1011
+ const [isGenerating, setIsGenerating] = useState(false);
1012
+ const [copied, setCopied] = useState(false);
1013
+
1014
+ const handleCopy = useCallback(() => {
1015
+ if (output) {
1016
+ navigator.clipboard.writeText(output);
1017
+ setCopied(true);
1018
+ setTimeout(() => setCopied(false), 2000);
1019
+ }
1020
+ }, [output]);
1021
+
1022
+ // Extract code from markdown code blocks if present
1023
+ const extractCode = useCallback(
1024
+ (text: string): { code: string; lang?: string } => {
1025
+ const match = text.match(/```(\w*)\n?([\s\S]*?)```/);
1026
+ if (match) {
1027
+ return { code: match[2].trim(), lang: match[1] || undefined };
1028
+ }
1029
+ return { code: text };
1030
+ },
1031
+ [],
1032
+ );
1033
+
1034
+ // Generate code from natural language using coding model
1035
+ useEffect(() => {
1036
+ if (!generateFrom || !useESIContext) return;
1037
+
1038
+ setIsGenerating(true);
1039
+ setIsLoading(true);
1040
+
1041
+ const generateCode = async () => {
1042
+ try {
1043
+ const esi = useESIContext!();
1044
+ const langHint = language ? ` in ${language}` : '';
1045
+ const prompt = `Generate clean, production-ready code${langHint} for the following requirement. Output ONLY the code, no explanations:\n\n${generateFrom}`;
1046
+
1047
+ const result = await esi.process({
1048
+ id: `esi-code-gen-${Date.now()}`,
1049
+ params: {
1050
+ model: 'llm',
1051
+ variant: model,
1052
+ temperature,
1053
+ system: `You are an expert programmer. Generate clean, well-documented code. Output ONLY code wrapped in a markdown code block with the language specified. No explanations before or after.`,
1054
+ },
1055
+ content: {
1056
+ type: 'text',
1057
+ value: prompt,
1058
+ },
1059
+ });
1060
+
1061
+ if (result.success && result.output) {
1062
+ const { code, lang } = extractCode(result.output);
1063
+ setOutput(code);
1064
+ const finalLang = language || lang;
1065
+ setDetectedLang(finalLang);
1066
+ onGenerate?.(code, finalLang || 'text');
1067
+ }
1068
+ } catch (err) {
1069
+ console.error('Code generation failed:', err);
1070
+ } finally {
1071
+ setIsLoading(false);
1072
+ setIsGenerating(false);
1073
+ }
1074
+ };
1075
+
1076
+ generateCode();
1077
+ }, [generateFrom, model, language, temperature, extractCode, onGenerate]);
1078
+
1079
+ // Auto-detect language using AI
1080
+ useEffect(() => {
1081
+ if (!autoDetect || !output || detectedLang || !useESIContext) return;
1082
+
1083
+ const detectLanguage = async () => {
1084
+ try {
1085
+ const esi = useESIContext!();
1086
+ const result = await esi.process({
1087
+ id: `esi-lang-detect-${Date.now()}`,
1088
+ params: {
1089
+ model: 'llm',
1090
+ variant: model,
1091
+ temperature: 0,
1092
+ maxTokens: 20,
1093
+ system:
1094
+ 'You are a code language detector. Respond with ONLY the programming language name, nothing else.',
1095
+ },
1096
+ content: {
1097
+ type: 'text',
1098
+ value: `What programming language is this code written in?\n\n${output.slice(0, 500)}`,
1099
+ },
1100
+ });
1101
+
1102
+ if (result.success && result.output) {
1103
+ const lang = result.output.trim().toLowerCase();
1104
+ setDetectedLang(lang);
1105
+ onLanguageDetect?.(lang);
1106
+ }
1107
+ } catch (err) {
1108
+ console.error('Language detection failed:', err);
1109
+ }
1110
+ };
1111
+
1112
+ detectLanguage();
1113
+ }, [autoDetect, output, detectedLang, model, onLanguageDetect]);
1114
+
1115
+ // Handle string children directly
1116
+ if (typeof children === 'string' && !generateFrom) {
1117
+ const { code, lang } = extractCode(children);
1118
+ const finalLang = language || lang || detectedLang;
1119
+ const html = formatCode(code, {
1120
+ language: finalLang,
1121
+ lineNumbers,
1122
+ startLine,
1123
+ highlightLines,
1124
+ });
1125
+ return (
1126
+ <Wrapper className={`esi-code ${className || ''}`}>
1127
+ {copyable && (
1128
+ <button
1129
+ className="esi-code-copy"
1130
+ onClick={handleCopy}
1131
+ aria-label="Copy code"
1132
+ >
1133
+ {copied ? '✓' : '⎘'}
1134
+ </button>
1135
+ )}
1136
+ <div dangerouslySetInnerHTML={{ __html: html }} />
1137
+ </Wrapper>
1138
+ );
1139
+ }
1140
+
1141
+ // If generating from prompt, show loading state
1142
+ if (generateFrom && isGenerating) {
1143
+ return (
1144
+ <Wrapper className={`esi-code esi-code-generating ${className || ''}`}>
1145
+ <div className="esi-code-loading">Generating code...</div>
1146
+ </Wrapper>
1147
+ );
1148
+ }
1149
+
1150
+ const wrappedChildren = useMemo(() => {
1151
+ if (generateFrom) return null; // No children when generating
1152
+ return wrapChildren(children, (text) => {
1153
+ setOutput(text);
1154
+ setIsLoading(false);
1155
+ });
1156
+ }, [children, generateFrom]);
1157
+
1158
+ if (isLoading && !generateFrom) {
1159
+ return <Wrapper className={className}>{wrappedChildren}</Wrapper>;
1160
+ }
1161
+
1162
+ if (!output) {
1163
+ return <Wrapper className={className}>{fallback}</Wrapper>;
1164
+ }
1165
+
1166
+ const { code, lang } = extractCode(output);
1167
+ const finalLang = language || lang || detectedLang;
1168
+ const html = formatCode(code, {
1169
+ language: finalLang,
1170
+ lineNumbers,
1171
+ startLine,
1172
+ highlightLines,
1173
+ });
1174
+
1175
+ return (
1176
+ <Wrapper className={`esi-code ${className || ''}`}>
1177
+ {copyable && (
1178
+ <button
1179
+ className="esi-code-copy"
1180
+ onClick={handleCopy}
1181
+ aria-label="Copy code"
1182
+ >
1183
+ {copied ? '✓' : '⎘'}
1184
+ </button>
1185
+ )}
1186
+ <div dangerouslySetInnerHTML={{ __html: html }} />
1187
+ </Wrapper>
1188
+ );
1189
+ };
1190
+
1191
+ // ============================================================================
1192
+ // ESI.Semantic Component - Embeddings to Structured HTML
1193
+ // ============================================================================
1194
+
1195
+ /** Schema.org types for microdata */
1196
+ export type SchemaOrgType =
1197
+ | 'Article'
1198
+ | 'BlogPosting'
1199
+ | 'CreativeWork'
1200
+ | 'Event'
1201
+ | 'HowTo'
1202
+ | 'NewsArticle'
1203
+ | 'Organization'
1204
+ | 'Person'
1205
+ | 'Place'
1206
+ | 'Product'
1207
+ | 'Recipe'
1208
+ | 'Review'
1209
+ | 'Thing'
1210
+ | 'WebPage';
1211
+
1212
+ /** Extracted semantic topic */
1213
+ export interface SemanticTopic {
1214
+ /** Topic label */
1215
+ label: string;
1216
+ /** Confidence score 0-1 */
1217
+ confidence: number;
1218
+ /** Schema.org type */
1219
+ schemaType?: SchemaOrgType;
1220
+ /** Related keywords */
1221
+ keywords?: string[];
1222
+ /** Embedding vector (if requested) */
1223
+ embedding?: number[];
1224
+ }
1225
+
1226
+ /** Extracted entity */
1227
+ export interface SemanticEntity {
1228
+ /** Entity text */
1229
+ text: string;
1230
+ /** Entity type (person, place, org, etc.) */
1231
+ type:
1232
+ | 'person'
1233
+ | 'place'
1234
+ | 'organization'
1235
+ | 'date'
1236
+ | 'money'
1237
+ | 'product'
1238
+ | 'event'
1239
+ | 'other';
1240
+ /** Schema.org type */
1241
+ schemaType?: SchemaOrgType;
1242
+ /** Start position in text */
1243
+ start?: number;
1244
+ /** End position in text */
1245
+ end?: number;
1246
+ }
1247
+
1248
+ /** Extracted emotion from text */
1249
+ export interface SemanticEmotion {
1250
+ /** Primary emotion */
1251
+ primary: string;
1252
+ /** Valence: negative (-1) to positive (1) */
1253
+ valence: number;
1254
+ /** Arousal: calm (0) to excited (1) */
1255
+ arousal: number;
1256
+ /** Confidence 0-1 */
1257
+ confidence: number;
1258
+ }
1259
+
1260
+ export interface ESISemanticProps extends Omit<ESIFormatProps, 'as'> {
1261
+ /** Custom wrapper element */
1262
+ as?: WrapperElement;
1263
+ /** Text to analyze (or children) */
1264
+ text?: string;
1265
+ /** Output format */
1266
+ format?: 'microdata' | 'jsonld' | 'rdfa' | 'tags';
1267
+ /** Include embeddings in output */
1268
+ includeEmbeddings?: boolean;
1269
+ /** Maximum topics to extract */
1270
+ maxTopics?: number;
1271
+ /** Minimum confidence threshold */
1272
+ minConfidence?: number;
1273
+ /** Schema.org type hint */
1274
+ schemaType?: SchemaOrgType;
1275
+ /** Extract named entities using entity model */
1276
+ extractEntities?: boolean;
1277
+ /** Extract emotion using emotion model */
1278
+ extractEmotion?: boolean;
1279
+ /** Custom topic vocabulary (for domain-specific topics) */
1280
+ vocabulary?: string[];
1281
+ /** Callback with extracted data */
1282
+ onExtract?: (data: {
1283
+ topics: SemanticTopic[];
1284
+ entities: SemanticEntity[];
1285
+ emotion?: SemanticEmotion;
1286
+ }) => void;
1287
+ }
1288
+
1289
+ /**
1290
+ * Extract semantic topics and generate structured HTML with microdata
1291
+ *
1292
+ * @example
1293
+ * ```tsx
1294
+ * // Extract topics as microdata
1295
+ * <ESI.Semantic format="microdata" maxTopics={5}>
1296
+ * <ESI.Infer>Summarize this article about climate change</ESI.Infer>
1297
+ * </ESI.Semantic>
1298
+ *
1299
+ * // Generate JSON-LD structured data
1300
+ * <ESI.Semantic format="jsonld" schemaType="Article">
1301
+ * {articleText}
1302
+ * </ESI.Semantic>
1303
+ *
1304
+ * // Extract as tags for display
1305
+ * <ESI.Semantic format="tags" extractEntities>
1306
+ * {content}
1307
+ * </ESI.Semantic>
1308
+ * ```
1309
+ */
1310
+ export const ESISemantic: FC<ESISemanticProps> = ({
1311
+ children,
1312
+ text,
1313
+ className,
1314
+ as: Wrapper = 'div',
1315
+ fallback,
1316
+ format = 'microdata',
1317
+ includeEmbeddings = false,
1318
+ maxTopics = 5,
1319
+ minConfidence = 0.5,
1320
+ schemaType = 'Thing',
1321
+ extractEntities = false,
1322
+ extractEmotion = false,
1323
+ vocabulary,
1324
+ onExtract,
1325
+ }) => {
1326
+ const [output, setOutput] = useState<string | null>(null);
1327
+ const [topics, setTopics] = useState<SemanticTopic[]>([]);
1328
+ const [entities, setEntities] = useState<SemanticEntity[]>([]);
1329
+ const [emotion, setEmotion] = useState<SemanticEmotion | undefined>();
1330
+ const [isLoading, setIsLoading] = useState(true);
1331
+ const [structuredHtml, setStructuredHtml] = useState<string>('');
1332
+
1333
+ const inputText = text || (typeof children === 'string' ? children : null);
1334
+
1335
+ // Extract semantics using ESI
1336
+ useEffect(() => {
1337
+ if (!inputText && !output) return;
1338
+ if (!useESIContext) {
1339
+ setIsLoading(false);
1340
+ return;
1341
+ }
1342
+
1343
+ const extractSemantics = async () => {
1344
+ try {
1345
+ const esi = useESIContext!();
1346
+ const textToAnalyze = inputText || output || '';
1347
+
1348
+ // Build prompt for semantic extraction
1349
+ const vocabHint = vocabulary?.length
1350
+ ? `Focus on these topics: ${vocabulary.join(', ')}`
1351
+ : '';
1352
+
1353
+ const entityHint = extractEntities
1354
+ ? 'Also extract named entities (people, places, organizations, dates).'
1355
+ : '';
1356
+
1357
+ const prompt = `Analyze this text and extract semantic topics and metadata.
1358
+ ${vocabHint}
1359
+ ${entityHint}
1360
+
1361
+ Return JSON in this exact format:
1362
+ {
1363
+ "topics": [
1364
+ { "label": "topic name", "confidence": 0.95, "keywords": ["kw1", "kw2"], "schemaType": "Article" }
1365
+ ],
1366
+ "entities": [
1367
+ { "text": "entity text", "type": "person|place|organization|date|money|product|event|other" }
1368
+ ],
1369
+ "suggestedSchema": "Article|BlogPosting|etc"
1370
+ }
1371
+
1372
+ Text to analyze:
1373
+ ${textToAnalyze.slice(0, 2000)}`;
1374
+
1375
+ const result = await esi.process({
1376
+ id: `esi-semantic-${Date.now()}`,
1377
+ params: {
1378
+ model: 'llm',
1379
+ temperature: 0.1,
1380
+ maxTokens: 1000,
1381
+ system:
1382
+ 'You are a semantic analysis expert. Extract topics, entities, and suggest Schema.org types. Always respond with valid JSON.',
1383
+ },
1384
+ content: {
1385
+ type: 'text',
1386
+ value: prompt,
1387
+ },
1388
+ });
1389
+
1390
+ if (result.success && result.output) {
1391
+ try {
1392
+ // Parse the JSON response
1393
+ const jsonMatch = result.output.match(/\{[\s\S]*\}/);
1394
+ if (jsonMatch) {
1395
+ const parsed = JSON.parse(jsonMatch[0]);
1396
+ const extractedTopics: SemanticTopic[] = (parsed.topics || [])
1397
+ .filter((t: SemanticTopic) => t.confidence >= minConfidence)
1398
+ .slice(0, maxTopics);
1399
+ const extractedEntities: SemanticEntity[] = parsed.entities || [];
1400
+
1401
+ setTopics(extractedTopics);
1402
+
1403
+ // Use dedicated entity extraction model if available
1404
+ if (extractEntities) {
1405
+ try {
1406
+ const entityResult = await esi.process({
1407
+ id: `esi-semantic-entities-${Date.now()}`,
1408
+ params: { model: 'classify' }, // Entity extraction model
1409
+ content: {
1410
+ type: 'text',
1411
+ value: textToAnalyze.slice(0, 2000),
1412
+ },
1413
+ });
1414
+ if (entityResult.success && entityResult.output) {
1415
+ try {
1416
+ const entityParsed = JSON.parse(entityResult.output);
1417
+ if (Array.isArray(entityParsed)) {
1418
+ extractedEntities.push(...entityParsed);
1419
+ }
1420
+ } catch {
1421
+ // Use LLM-extracted entities as fallback
1422
+ }
1423
+ }
1424
+ } catch {
1425
+ // Entity model not available, use LLM-extracted entities
1426
+ }
1427
+ }
1428
+ setEntities(extractedEntities);
1429
+
1430
+ // Use dedicated emotion model if requested
1431
+ let extractedEmotion: SemanticEmotion | undefined;
1432
+ if (extractEmotion) {
1433
+ try {
1434
+ const emotionResult = await esi.process({
1435
+ id: `esi-semantic-emotion-${Date.now()}`,
1436
+ params: { model: 'emotion' }, // Emotion detection model
1437
+ content: {
1438
+ type: 'text',
1439
+ value: textToAnalyze.slice(0, 1000),
1440
+ },
1441
+ });
1442
+ if (emotionResult.success && emotionResult.output) {
1443
+ try {
1444
+ extractedEmotion = JSON.parse(emotionResult.output);
1445
+ setEmotion(extractedEmotion);
1446
+ } catch {
1447
+ // Parse emotion from string format
1448
+ const match = emotionResult.output.match(/(\w+)/);
1449
+ if (match) {
1450
+ extractedEmotion = {
1451
+ primary: match[1],
1452
+ valence: 0,
1453
+ arousal: 0.5,
1454
+ confidence: 0.8,
1455
+ };
1456
+ setEmotion(extractedEmotion);
1457
+ }
1458
+ }
1459
+ }
1460
+ } catch {
1461
+ // Emotion model not available
1462
+ }
1463
+ }
1464
+
1465
+ onExtract?.({
1466
+ topics: extractedTopics,
1467
+ entities: extractedEntities,
1468
+ emotion: extractedEmotion,
1469
+ });
1470
+
1471
+ // Generate structured HTML
1472
+ const html = generateStructuredHtml(
1473
+ textToAnalyze,
1474
+ extractedTopics,
1475
+ extractedEntities,
1476
+ format,
1477
+ parsed.suggestedSchema || schemaType,
1478
+ extractedEmotion,
1479
+ );
1480
+ setStructuredHtml(html);
1481
+ }
1482
+ } catch (parseErr) {
1483
+ console.error('Failed to parse semantic extraction:', parseErr);
1484
+ }
1485
+ }
1486
+
1487
+ // Get embeddings if requested
1488
+ if (includeEmbeddings) {
1489
+ const embedResult = await esi.process({
1490
+ id: `esi-semantic-embed-${Date.now()}`,
1491
+ params: { model: 'embed' },
1492
+ content: { type: 'text', value: textToAnalyze.slice(0, 1000) },
1493
+ });
1494
+
1495
+ const embeddingResult = embedResult as {
1496
+ success: boolean;
1497
+ embedding?: number[];
1498
+ };
1499
+ if (embeddingResult.success && embeddingResult.embedding) {
1500
+ const embedding = embeddingResult.embedding;
1501
+ setTopics((prev) =>
1502
+ prev.map((t, i) => (i === 0 ? { ...t, embedding } : t)),
1503
+ );
1504
+ }
1505
+ }
1506
+ } catch (err) {
1507
+ console.error('Semantic extraction failed:', err);
1508
+ } finally {
1509
+ setIsLoading(false);
1510
+ }
1511
+ };
1512
+
1513
+ extractSemantics();
1514
+ }, [
1515
+ inputText,
1516
+ output,
1517
+ maxTopics,
1518
+ minConfidence,
1519
+ schemaType,
1520
+ extractEntities,
1521
+ vocabulary,
1522
+ format,
1523
+ includeEmbeddings,
1524
+ onExtract,
1525
+ ]);
1526
+
1527
+ // Handle ESI children
1528
+ const wrappedChildren = useMemo(() => {
1529
+ if (inputText) return null;
1530
+ return wrapChildren(children, (text) => {
1531
+ setOutput(text);
1532
+ });
1533
+ }, [children, inputText]);
1534
+
1535
+ if (isLoading) {
1536
+ return (
1537
+ <Wrapper
1538
+ className={`esi-semantic esi-semantic-loading ${className || ''}`}
1539
+ >
1540
+ {wrappedChildren}
1541
+ {!wrappedChildren && <span>Analyzing...</span>}
1542
+ </Wrapper>
1543
+ );
1544
+ }
1545
+
1546
+ if (!structuredHtml && !topics.length) {
1547
+ return <Wrapper className={className}>{fallback}</Wrapper>;
1548
+ }
1549
+
1550
+ // For 'tags' format, render as interactive tag list
1551
+ if (format === 'tags') {
1552
+ return (
1553
+ <Wrapper className={`esi-semantic esi-semantic-tags ${className || ''}`}>
1554
+ {emotion && (
1555
+ <div
1556
+ className={`esi-semantic-emotion esi-semantic-emotion-${emotion.primary}`}
1557
+ data-valence={emotion.valence}
1558
+ data-arousal={emotion.arousal}
1559
+ >
1560
+ <span className="esi-semantic-emotion-label">
1561
+ {emotion.primary}
1562
+ </span>
1563
+ <span className="esi-semantic-emotion-confidence">
1564
+ {(emotion.confidence * 100).toFixed(0)}%
1565
+ </span>
1566
+ </div>
1567
+ )}
1568
+ <div className="esi-semantic-topics">
1569
+ {topics.map((topic, i) => (
1570
+ <span
1571
+ key={i}
1572
+ className="esi-semantic-tag"
1573
+ data-confidence={topic.confidence.toFixed(2)}
1574
+ data-schema={topic.schemaType}
1575
+ >
1576
+ {topic.label}
1577
+ </span>
1578
+ ))}
1579
+ </div>
1580
+ {entities.length > 0 && (
1581
+ <div className="esi-semantic-entities">
1582
+ {entities.map((entity, i) => (
1583
+ <span
1584
+ key={i}
1585
+ className={`esi-semantic-entity esi-semantic-entity-${entity.type}`}
1586
+ data-type={entity.type}
1587
+ >
1588
+ {entity.text}
1589
+ </span>
1590
+ ))}
1591
+ </div>
1592
+ )}
1593
+ </Wrapper>
1594
+ );
1595
+ }
1596
+
1597
+ // For structured formats, render the HTML
1598
+ return (
1599
+ <Wrapper
1600
+ className={`esi-semantic ${className || ''}`}
1601
+ dangerouslySetInnerHTML={{ __html: structuredHtml }}
1602
+ />
1603
+ );
1604
+ };
1605
+
1606
+ /**
1607
+ * Generate structured HTML with microdata, JSON-LD, or RDFa
1608
+ */
1609
+ function generateStructuredHtml(
1610
+ text: string,
1611
+ topics: SemanticTopic[],
1612
+ entities: SemanticEntity[],
1613
+ format: 'microdata' | 'jsonld' | 'rdfa' | 'tags',
1614
+ schemaType: SchemaOrgType,
1615
+ emotion?: SemanticEmotion,
1616
+ ): string {
1617
+ const keywords = topics.flatMap((t) => t.keywords || [t.label]).join(', ');
1618
+
1619
+ switch (format) {
1620
+ case 'jsonld': {
1621
+ const jsonLd: Record<string, unknown> = {
1622
+ '@context': 'https://schema.org',
1623
+ '@type': schemaType,
1624
+ name: topics[0]?.label || 'Content',
1625
+ keywords,
1626
+ about: topics.map((t) => ({
1627
+ '@type': t.schemaType || 'Thing',
1628
+ name: t.label,
1629
+ })),
1630
+ mentions: entities.map((e) => ({
1631
+ '@type': e.schemaType || entityTypeToSchema(e.type),
1632
+ name: e.text,
1633
+ })),
1634
+ };
1635
+ // Add emotion as custom property (using schema.org extension pattern)
1636
+ if (emotion) {
1637
+ jsonLd['emotionalTone'] = {
1638
+ '@type': 'PropertyValue',
1639
+ name: 'emotionalTone',
1640
+ value: emotion.primary,
1641
+ additionalProperty: [
1642
+ {
1643
+ '@type': 'PropertyValue',
1644
+ name: 'valence',
1645
+ value: emotion.valence,
1646
+ },
1647
+ {
1648
+ '@type': 'PropertyValue',
1649
+ name: 'arousal',
1650
+ value: emotion.arousal,
1651
+ },
1652
+ {
1653
+ '@type': 'PropertyValue',
1654
+ name: 'confidence',
1655
+ value: emotion.confidence,
1656
+ },
1657
+ ],
1658
+ };
1659
+ }
1660
+ return `<script type="application/ld+json">${JSON.stringify(jsonLd, null, 2)}</script>
1661
+ <div class="esi-semantic-content">${escapeHtml(text)}</div>`;
1662
+ }
1663
+
1664
+ case 'rdfa': {
1665
+ const topicSpans = topics
1666
+ .map(
1667
+ (t) =>
1668
+ `<span property="about" typeof="${t.schemaType || 'Thing'}"><span property="name">${escapeHtml(t.label)}</span></span>`,
1669
+ )
1670
+ .join(' ');
1671
+ const emotionSpan = emotion
1672
+ ? `<span property="emotionalTone" content="${escapeHtml(emotion.primary)}" data-valence="${emotion.valence}" data-arousal="${emotion.arousal}"></span>`
1673
+ : '';
1674
+ return `<div vocab="https://schema.org/" typeof="${schemaType}">
1675
+ <meta property="keywords" content="${escapeHtml(keywords)}" />
1676
+ ${emotionSpan}
1677
+ <div property="articleBody">${escapeHtml(text)}</div>
1678
+ <div class="esi-semantic-about">${topicSpans}</div>
1679
+ </div>`;
1680
+ }
1681
+
1682
+ case 'microdata':
1683
+ default: {
1684
+ const topicSpans = topics
1685
+ .map(
1686
+ (t) =>
1687
+ `<span itemprop="about" itemscope itemtype="https://schema.org/${t.schemaType || 'Thing'}">
1688
+ <span itemprop="name">${escapeHtml(t.label)}</span>
1689
+ </span>`,
1690
+ )
1691
+ .join('\n');
1692
+ const entitySpans = entities
1693
+ .map(
1694
+ (e) =>
1695
+ `<span itemprop="mentions" itemscope itemtype="https://schema.org/${entityTypeToSchema(e.type)}">
1696
+ <span itemprop="name">${escapeHtml(e.text)}</span>
1697
+ </span>`,
1698
+ )
1699
+ .join('\n');
1700
+ const emotionMeta = emotion
1701
+ ? `<meta itemprop="emotionalTone" content="${escapeHtml(emotion.primary)}" data-valence="${emotion.valence}" data-arousal="${emotion.arousal}" data-confidence="${emotion.confidence}" />`
1702
+ : '';
1703
+ return `<div itemscope itemtype="https://schema.org/${schemaType}">
1704
+ <meta itemprop="keywords" content="${escapeHtml(keywords)}" />
1705
+ ${emotionMeta}
1706
+ <div itemprop="articleBody">${escapeHtml(text)}</div>
1707
+ <div class="esi-semantic-about">${topicSpans}</div>
1708
+ <div class="esi-semantic-mentions">${entitySpans}</div>
1709
+ </div>`;
1710
+ }
1711
+ }
1712
+ }
1713
+
1714
+ /**
1715
+ * Map entity type to Schema.org type
1716
+ */
1717
+ function entityTypeToSchema(type: SemanticEntity['type']): SchemaOrgType {
1718
+ const map: Record<SemanticEntity['type'], SchemaOrgType> = {
1719
+ person: 'Person',
1720
+ place: 'Place',
1721
+ organization: 'Organization',
1722
+ date: 'Thing',
1723
+ money: 'Thing',
1724
+ product: 'Product',
1725
+ event: 'Event',
1726
+ other: 'Thing',
1727
+ };
1728
+ return map[type] || 'Thing';
1729
+ }
1730
+
1731
+ // ============================================================================
1732
+ // ESI Format Namespace Export
1733
+ // ============================================================================
1734
+
1735
+ export const ESIFormat = {
1736
+ Markdown: ESIMarkdown,
1737
+ Latex: ESILatex,
1738
+ Json: ESIJson,
1739
+ Plaintext: ESIPlaintext,
1740
+ Code: ESICode,
1741
+ Semantic: ESISemantic,
1742
+ };
1743
+
1744
+ export default ESIFormat;