diff --git a/apps/site/.env.example b/apps/site/.env.example index 2ad925ae..e6e237d4 100644 --- a/apps/site/.env.example +++ b/apps/site/.env.example @@ -8,6 +8,6 @@ # Leave empty to disable analytics in this build. PUBLIC_CLARITY_ID= -# Waitlist subscribe endpoint (Cloudflare Worker URL + /subscribe). -# Leave empty to disable form submission gracefully. -PUBLIC_WAITLIST_ENDPOINT= +# Generic analytics collector endpoint that accepts JSON POST payloads. +# Leave empty to disable the built-in pageview and click tracking. +PUBLIC_ANALYTICS_ENDPOINT= diff --git a/apps/site/astro.config.mjs b/apps/site/astro.config.mjs index 0bd4df59..1d9f6c31 100644 --- a/apps/site/astro.config.mjs +++ b/apps/site/astro.config.mjs @@ -10,6 +10,7 @@ export default defineConfig({ react(), starlight({ title: 'TouchAI', + disable404Route: true, social: [{ icon: 'github', label: 'GitHub', href: 'https://github.com/TouchAI-org/TouchAI' }], sidebar: [ ], diff --git a/apps/site/public/apple-touch-icon.png b/apps/site/public/apple-touch-icon.png new file mode 100644 index 00000000..69e40dd4 Binary files /dev/null and b/apps/site/public/apple-touch-icon.png differ diff --git a/apps/site/public/demo-utils/touchai-lite-math.js b/apps/site/public/demo-utils/touchai-lite-math.js new file mode 100644 index 00000000..03b79cd6 --- /dev/null +++ b/apps/site/public/demo-utils/touchai-lite-math.js @@ -0,0 +1,262 @@ +(function () { + const getRenderer = () => window.TouchAILiteRenderer; + + function escapeHtml(value) { + const renderer = getRenderer(); + if (renderer?.escapeHtml) { + return renderer.escapeHtml(value); + } + + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/`/g, '`'); + } + + function normalizeFormula(source) { + return String(source) + .replace(/\\left/g, '') + .replace(/\\right/g, '') + .replace(/\\,/g, ' ') + .replace(/\\:/g, ' ') + .replace(/\\;/g, ' ') + .replace(/\\quad/g, ' ') + .replace(/\\qquad/g, ' ') + .replace(/\\!/g, ''); + } + + function renderFormula(source, displayMode) { + if (window.katex && typeof window.katex.renderToString === 'function') { + try { + return window.katex.renderToString(String(source), { + displayMode, + throwOnError: false, + strict: 'ignore', + trust: false, + }); + } catch { + // Fall back to the lightweight renderer below. + } + } + + const input = normalizeFormula(source); + let index = 0; + + const commandMap = { + theta: 'θ', + cos: 'cos', + sin: 'sin', + max: 'max', + min: 'min', + in: '∈', + Rightarrow: '⇒', + to: '→', + cdot: '·', + }; + + const readCommandName = () => { + index += 1; + let name = ''; + while (index < input.length && /[A-Za-z]/.test(input[index])) { + name += input[index]; + index += 1; + } + + if (!name && index < input.length) { + name = input[index]; + index += 1; + } + + return name; + }; + + const readRawGroup = () => { + if (input[index] !== '{') { + const start = index; + index += 1; + return input.slice(start, index); + } + + index += 1; + let depth = 1; + let value = ''; + + while (index < input.length && depth > 0) { + const char = input[index]; + index += 1; + + if (char === '{') { + depth += 1; + value += char; + continue; + } + + if (char === '}') { + depth -= 1; + if (depth > 0) value += char; + continue; + } + + value += char; + } + + return value; + }; + + const readArgument = () => { + while (input[index] === ' ') index += 1; + + if (index >= input.length) return ''; + + if (input[index] === '{') { + const raw = readRawGroup(); + return renderFormula(raw, false); + } + + if (input[index] === '\\') { + const command = readCommandName(); + if (command === 'text') { + return `${escapeHtml(readRawGroup())}`; + } + if (command === 'frac') { + const num = readArgument(); + const den = readArgument(); + return `${num}${den}`; + } + if (command === 'sqrt') { + const radicand = readArgument(); + return `${radicand}`; + } + if (command === 'boxed') { + return `${readArgument()}`; + } + + const mapped = commandMap[command] ?? command; + return `${escapeHtml(mapped)}`; + } + + const char = input[index]; + index += 1; + return escapeHtml(char); + }; + + const parse = (stopChar) => { + let html = ''; + + while (index < input.length) { + const char = input[index]; + + if (stopChar && char === stopChar) { + index += 1; + break; + } + + if (char === '{') { + index += 1; + html += parse('}'); + continue; + } + + if (char === '^' || char === '_') { + index += 1; + const tag = char === '^' ? 'sup' : 'sub'; + html += `<${tag}>${readArgument()}${tag}>`; + continue; + } + + if (char === '\\') { + const command = readCommandName(); + + if (command === 'frac') { + const num = readArgument(); + const den = readArgument(); + html += `${num}${den}`; + continue; + } + + if (command === 'sqrt') { + const radicand = readArgument(); + html += `${radicand}`; + continue; + } + + if (command === 'boxed') { + html += `${readArgument()}`; + continue; + } + + if (command === 'text') { + html += `${escapeHtml(readRawGroup())}`; + continue; + } + + if ( + command === ',' || + command === ':' || + command === ';' || + command === 'quad' || + command === 'qquad' + ) { + html += ' '; + continue; + } + + const mapped = commandMap[command] ?? command; + const className = /^[A-Za-z]+$/.test(mapped) ? 'op' : 'punct'; + html += `${escapeHtml(mapped)}`; + continue; + } + + html += escapeHtml(char); + index += 1; + } + + return html; + }; + + const inner = parse(); + const className = displayMode ? 'formula-module' : 'formula-module inline-formula'; + + return `${inner}`; + } + + function renderMarkdownWithMath(markdown) { + const mathTokens = []; + const protectedMarkdown = String(markdown) + .replace(/\$\$([\s\S]+?)\$\$/g, (_, formula) => { + const token = `@@MATH_BLOCK_${mathTokens.length}@@`; + mathTokens.push({ token, formula: formula.trim(), display: true }); + return `\n\n${token}\n\n`; + }) + .replace(/\$([^$\n]+?)\$/g, (_, formula) => { + const token = `@@MATH_INLINE_${mathTokens.length}@@`; + mathTokens.push({ token, formula: formula.trim(), display: false }); + return token; + }); + + const renderer = getRenderer(); + let html = renderer + ? renderer.renderMarkdownContent(protectedMarkdown) + : escapeHtml(protectedMarkdown); + + mathTokens.forEach(({ token, formula, display }) => { + const rendered = renderFormula(formula, display); + const replacement = display + ? `
${token}
`, 'g'), replacement) + .replace(new RegExp(token, 'g'), replacement); + }); + + return html; + } + + window.TouchAILiteMathRenderer = { + renderFormula, + renderMarkdownWithMath, + }; +})(); diff --git a/apps/site/public/demo-utils/touchai-lite-renderer.js b/apps/site/public/demo-utils/touchai-lite-renderer.js new file mode 100644 index 00000000..30c40325 --- /dev/null +++ b/apps/site/public/demo-utils/touchai-lite-renderer.js @@ -0,0 +1,97 @@ +(function () { + function escapeHtml(value) { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + function stripImageMarkdown(value) { + return String(value) + .replace(/\[!\[[^\]]*\]\([^)]*\)\]\([^)]*\)/g, '') + .replace(/!\[[^\]]*\]\([^)]*\)/g, ''); + } + + function renderInline(source) { + const segments = stripImageMarkdown(source).split(/(`[^`]+`)/g); + + return segments + .map((segment) => { + if (!segment) return ''; + if (segment.startsWith('`') && segment.endsWith('`')) { + return `${escapeHtml(segment.slice(1, -1))}`; + } + + return escapeHtml(segment).replace(/\*\*([^*]+)\*\*/g, '$1'); + }) + .join(''); + } + + function renderMarkdownContent(markdown) { + const lines = String(markdown).split(/\r?\n/); + const html = []; + let paragraphLines = []; + let listItems = []; + + const flushParagraph = () => { + if (!paragraphLines.length) return; + html.push( + `${paragraphLines.map(renderInline).join('
')}