@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.
- package/CHANGELOG.md +112 -0
- package/README.md +625 -0
- package/examples/basic/aeon.config.ts +39 -0
- package/examples/basic/components/Cursor.tsx +86 -0
- package/examples/basic/components/OfflineIndicator.tsx +103 -0
- package/examples/basic/components/PresenceBar.tsx +77 -0
- package/examples/basic/package.json +20 -0
- package/examples/basic/pages/index.tsx +80 -0
- package/package.json +101 -0
- package/packages/analytics/README.md +309 -0
- package/packages/analytics/build.ts +35 -0
- package/packages/analytics/package.json +50 -0
- package/packages/analytics/src/click-tracker.ts +368 -0
- package/packages/analytics/src/context-bridge.ts +319 -0
- package/packages/analytics/src/data-layer.ts +302 -0
- package/packages/analytics/src/gtm-loader.ts +239 -0
- package/packages/analytics/src/index.ts +230 -0
- package/packages/analytics/src/merkle-tree.ts +489 -0
- package/packages/analytics/src/provider.tsx +300 -0
- package/packages/analytics/src/types.ts +320 -0
- package/packages/analytics/src/use-analytics.ts +296 -0
- package/packages/analytics/tsconfig.json +19 -0
- package/packages/benchmarks/src/benchmark.test.ts +691 -0
- package/packages/cli/dist/index.js +61899 -0
- package/packages/cli/package.json +43 -0
- package/packages/cli/src/commands/build.test.ts +682 -0
- package/packages/cli/src/commands/build.ts +890 -0
- package/packages/cli/src/commands/dev.ts +473 -0
- package/packages/cli/src/commands/init.ts +409 -0
- package/packages/cli/src/commands/start.ts +297 -0
- package/packages/cli/src/index.ts +105 -0
- package/packages/directives/src/use-aeon.ts +272 -0
- package/packages/mcp-server/package.json +51 -0
- package/packages/mcp-server/src/index.ts +178 -0
- package/packages/mcp-server/src/resources.ts +346 -0
- package/packages/mcp-server/src/tools/index.ts +36 -0
- package/packages/mcp-server/src/tools/navigation.ts +545 -0
- package/packages/mcp-server/tsconfig.json +21 -0
- package/packages/react/package.json +40 -0
- package/packages/react/src/Link.tsx +388 -0
- package/packages/react/src/components/InstallPrompt.tsx +286 -0
- package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
- package/packages/react/src/components/PushNotifications.tsx +453 -0
- package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
- package/packages/react/src/hooks/useConflicts.ts +277 -0
- package/packages/react/src/hooks/useNetworkState.ts +209 -0
- package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
- package/packages/react/src/hooks/useServiceWorker.ts +278 -0
- package/packages/react/src/hooks.ts +195 -0
- package/packages/react/src/index.ts +151 -0
- package/packages/react/src/provider.tsx +467 -0
- package/packages/react/tsconfig.json +19 -0
- package/packages/runtime/README.md +399 -0
- package/packages/runtime/build.ts +48 -0
- package/packages/runtime/package.json +71 -0
- package/packages/runtime/schema.sql +40 -0
- package/packages/runtime/src/api-routes.ts +465 -0
- package/packages/runtime/src/benchmark.ts +171 -0
- package/packages/runtime/src/cache.ts +479 -0
- package/packages/runtime/src/durable-object.ts +1341 -0
- package/packages/runtime/src/index.ts +360 -0
- package/packages/runtime/src/navigation.test.ts +421 -0
- package/packages/runtime/src/navigation.ts +422 -0
- package/packages/runtime/src/nextjs-adapter.ts +272 -0
- package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
- package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
- package/packages/runtime/src/offline/encryption.test.ts +412 -0
- package/packages/runtime/src/offline/encryption.ts +397 -0
- package/packages/runtime/src/offline/types.ts +465 -0
- package/packages/runtime/src/predictor.ts +371 -0
- package/packages/runtime/src/registry.ts +351 -0
- package/packages/runtime/src/router/context-extractor.ts +661 -0
- package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
- package/packages/runtime/src/router/esi-control.ts +541 -0
- package/packages/runtime/src/router/esi-cyrano.ts +779 -0
- package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
- package/packages/runtime/src/router/esi-react.tsx +1065 -0
- package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
- package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
- package/packages/runtime/src/router/esi-translate.ts +503 -0
- package/packages/runtime/src/router/esi.ts +666 -0
- package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
- package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
- package/packages/runtime/src/router/index.ts +298 -0
- package/packages/runtime/src/router/merkle-capability.ts +473 -0
- package/packages/runtime/src/router/speculation.ts +451 -0
- package/packages/runtime/src/router/types.ts +630 -0
- package/packages/runtime/src/router.test.ts +470 -0
- package/packages/runtime/src/router.ts +302 -0
- package/packages/runtime/src/server.ts +481 -0
- package/packages/runtime/src/service-worker-push.ts +319 -0
- package/packages/runtime/src/service-worker.ts +553 -0
- package/packages/runtime/src/skeleton-hydrate.ts +237 -0
- package/packages/runtime/src/speculation.test.ts +389 -0
- package/packages/runtime/src/speculation.ts +486 -0
- package/packages/runtime/src/storage.test.ts +1297 -0
- package/packages/runtime/src/storage.ts +1048 -0
- package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
- package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
- package/packages/runtime/src/sync/coordinator.test.ts +608 -0
- package/packages/runtime/src/sync/coordinator.ts +596 -0
- package/packages/runtime/src/tree-compiler.ts +295 -0
- package/packages/runtime/src/types.ts +728 -0
- package/packages/runtime/src/worker.ts +327 -0
- package/packages/runtime/tsconfig.json +20 -0
- package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
- package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
- package/packages/runtime/wasm/package.json +21 -0
- package/packages/runtime/wrangler.toml +41 -0
- package/packages/runtime-wasm/Cargo.lock +436 -0
- package/packages/runtime-wasm/Cargo.toml +29 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
- package/packages/runtime-wasm/pkg/package.json +21 -0
- package/packages/runtime-wasm/src/hydrate.rs +352 -0
- package/packages/runtime-wasm/src/lib.rs +191 -0
- package/packages/runtime-wasm/src/render.rs +629 -0
- package/packages/runtime-wasm/src/router.rs +298 -0
- package/packages/runtime-wasm/src/skeleton.rs +430 -0
- 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, '&')
|
|
295
|
+
.replace(/</g, '<')
|
|
296
|
+
.replace(/>/g, '>');
|
|
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, '&')
|
|
627
|
+
.replace(/</g, '<')
|
|
628
|
+
.replace(/>/g, '>')
|
|
629
|
+
.replace(/"/g, '"')
|
|
630
|
+
.replace(/'/g, ''');
|
|
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;
|