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()}`; + 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 + ? `
${rendered}
` + : `${rendered}`; + html = html + .replace(new RegExp(`

${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('
')}

` + ); + paragraphLines = []; + }; + + const flushList = () => { + if (!listItems.length) return; + html.push( + `` + ); + listItems = []; + }; + + lines.forEach((line) => { + const trimmed = stripImageMarkdown(line).trim(); + + if (!trimmed) { + flushParagraph(); + flushList(); + return; + } + + if (trimmed === '---') { + flushParagraph(); + flushList(); + html.push(``); + return; + } + + if (trimmed.startsWith('## ')) { + flushParagraph(); + flushList(); + html.push(`

${renderInline(trimmed.slice(3))}

`); + return; + } + + if (trimmed.startsWith('- ')) { + flushParagraph(); + listItems.push(trimmed.slice(2)); + return; + } + + flushList(); + paragraphLines.push(trimmed); + }); + + flushParagraph(); + flushList(); + + return html.join('\n'); + } + + window.TouchAILiteRenderer = { + escapeHtml, + renderInline, + renderMarkdownContent, + }; +})(); diff --git a/apps/site/public/feature-reminder-en/touchai-components.html b/apps/site/public/feature-reminder-en/touchai-components.html index 6458f3bf..8066f7a3 100644 --- a/apps/site/public/feature-reminder-en/touchai-components.html +++ b/apps/site/public/feature-reminder-en/touchai-components.html @@ -1,5 +1,5 @@  - + @@ -83,6 +83,10 @@ margin-bottom 640ms cubic-bezier(0.22, 1, 0.36, 1); } + body.is-scroll-driven .chat-panel { + will-change: auto; + } + body.is-idle .chat-panel { max-height: 56px; min-height: 56px; @@ -148,20 +152,20 @@ flex: 1 1 auto; min-height: 0; overflow-y: auto; + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; scrollbar-width: none; padding-top: 0; padding-bottom: 20px; } - body.is-answering .conversation-content { - flex: 0 0 auto; - overflow: visible; - padding-bottom: 0; - } - + body.is-answering .conversation-content, body.is-answering.is-scrolling .conversation-content { flex: 1 1 auto; + min-height: 0; overflow-y: auto; + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; padding-top: 0; padding-bottom: 20px; } @@ -463,6 +467,16 @@ flex: 0 0 auto; } + body.is-complete .conversation-content, + body.is-complete.is-scrolling .conversation-content { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; + padding-bottom: 20px; + } + body.is-answering .composer { margin-top: 0; } @@ -471,6 +485,37 @@ margin-top: auto; } + body.is-complete .composer { + margin-top: 0; + } + + body.is-complete.is-scrolling .composer { + margin-top: auto; + } + + body.is-scroll-driven.is-answering .conversation-content, + body.is-scroll-driven.is-complete .conversation-content { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; + padding-bottom: 20px; + } + + body.is-scroll-driven.is-answering .composer, + body.is-scroll-driven.is-complete .composer { + margin-top: auto; + } + + @media (min-width: 841px) { + body.is-scroll-driven.is-complete .chat-panel { + min-height: var(--window-min-height); + max-height: var(--window-min-height); + height: var(--window-min-height); + } + } + .response > p, .response > ul, .response > .math-block, @@ -708,12 +753,13 @@ margin-top: var(--response-item-gap); } - .response li > span:last-child { + .response li > span:last-child:not(.kbd) { display: block; text-indent: var(--paragraph-indent); } body.is-answering .response p:not(.is-visible), + body.is-answering .response h2:not(.is-visible), body.is-answering .response li:not(.is-visible), body.is-answering .response ul:not(.is-visible), body.is-answering .response .tool-call-list:not(.is-visible), @@ -1100,14 +1146,14 @@
-
-
- - - -
+
-
-
- + diff --git a/apps/site/public/feature-reminder/touchai-components.html b/apps/site/public/feature-reminder/touchai-components.html index b7551f06..63bbcb65 100644 --- a/apps/site/public/feature-reminder/touchai-components.html +++ b/apps/site/public/feature-reminder/touchai-components.html @@ -83,6 +83,10 @@ margin-bottom 640ms cubic-bezier(0.22, 1, 0.36, 1); } + body.is-scroll-driven .chat-panel { + will-change: auto; + } + body.is-idle .chat-panel { max-height: 56px; min-height: 56px; @@ -148,20 +152,20 @@ flex: 1 1 auto; min-height: 0; overflow-y: auto; + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; scrollbar-width: none; padding-top: 0; padding-bottom: 20px; } - body.is-answering .conversation-content { - flex: 0 0 auto; - overflow: visible; - padding-bottom: 0; - } - + body.is-answering .conversation-content, body.is-answering.is-scrolling .conversation-content { flex: 1 1 auto; + min-height: 0; overflow-y: auto; + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; padding-top: 0; padding-bottom: 20px; } @@ -463,6 +467,16 @@ flex: 0 0 auto; } + body.is-complete .conversation-content, + body.is-complete.is-scrolling .conversation-content { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; + padding-bottom: 20px; + } + body.is-answering .composer { margin-top: 0; } @@ -471,6 +485,37 @@ margin-top: auto; } + body.is-complete .composer { + margin-top: 0; + } + + body.is-complete.is-scrolling .composer { + margin-top: auto; + } + + body.is-scroll-driven.is-answering .conversation-content, + body.is-scroll-driven.is-complete .conversation-content { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; + padding-bottom: 20px; + } + + body.is-scroll-driven.is-answering .composer, + body.is-scroll-driven.is-complete .composer { + margin-top: auto; + } + + @media (min-width: 841px) { + body.is-scroll-driven.is-complete .chat-panel { + min-height: var(--window-min-height); + max-height: var(--window-min-height); + height: var(--window-min-height); + } + } + .response > p, .response > ul, .response > .math-block, @@ -708,12 +753,13 @@ margin-top: var(--response-item-gap); } - .response li > span:last-child { + .response li > span:last-child:not(.kbd) { display: block; text-indent: var(--paragraph-indent); } body.is-answering .response p:not(.is-visible), + body.is-answering .response h2:not(.is-visible), body.is-answering .response li:not(.is-visible), body.is-answering .response ul:not(.is-visible), body.is-answering .response .tool-call-list:not(.is-visible), @@ -1148,7 +1194,12 @@ -
+
- -
-
+

Here is the short solution:

- 设过 - - M - ( - 0 - , - 1 - ) - - 的直线为 - - y - = - kx - + - 1 - - ,与椭圆 + Let the line through + M(0, 1) + be + y = kx + 1 + , and let it intersect the ellipse

x 2 - + + + y @@ -1036,171 +1087,135 @@ 4 - = - 1 + = 1

- 交于 + at (A,B) - 。 + .

-

代入得

+

Substituting gives

- ( - k + (k 2 - + - 4 - ) - x + + 4)x 2 - + - 2kx - - 3 - = - 0 + + 2kx − 3 = 0

- 设两根为 + If the two roots are x 1 - , - x + , x 2 - ,则 + , then

x 1 - + - x + + x 2 - = - + = − 2k k 2 - + - 4 + + 4 - , -  y + , y 1 - + - y + + y 2 - = - k - ( - x + = k(x 1 - + - x + + x 2 - ) - + - 2 - = + ) + 2 = 8 k 2 - + - 4 + + 4

-

+

Also,

- P - ( + P( x 1 - + - x + + x 2 2 - , + , y 1 - + - y + + y 2 2 - ) + )

-

所以

+

so

- x - = - + x = − k k 2 - + - 4 + + 4 - , -  y - = + , y = 4 k 2 - + - 4 + + 4

- 消去 + Eliminating k - 得 + , we get

4x 2 - + - ( - y - + + (y − 1 2 - ) + ) 2 - = + = 1 4 @@ -1209,68 +1224,41 @@

-

再求

+

Now find the distance from

- N - ( + N( 1 2 - , + , 1 2 - ) + )

- 到轨迹上的点 + to a point P(x,y) - 的距离。 + on the locus.

-

由轨迹方程可设

+

From the locus equation, set

- 4x - 2 - + - ( - y - - - 1 - 2 - - ) - 2 - = + x = 1 4 - -

-

-

- - x - = - - 1 - 4 - - cosθ - , -  y - + cosθ, y − 1 2 - = + = 1 2 @@ -1278,46 +1266,41 @@ sinθ

-

+

Then

|NP| 2 - = - ( + = ( 1 4 - cosθ - + cosθ − 1 2 - ) + ) 2 - + - ( + + ( 1 2 - sinθ - ) + sinθ) 2 - = + = 1 2 - + − 1 4 - cosθ - + cosθ − 3 16 @@ -1328,36 +1311,25 @@

- 设 - - t - = - cosθ - - [ - −1 - , - 1 - ] - - ,则 + Let + t = cosθ ∈ [−1, 1] + , then

|NP| 2 - = + = 1 2 - + − 1 4 - t - + t − 3 16 @@ -1368,33 +1340,31 @@

- 当 + The maximum occurs when - t - = - + t = − 2 3 - 时取最大值: + :

|NP| 2 max - = + = 7 12 - + ⇒ |NP| max - = + = @@ -1408,43 +1378,33 @@

- 比较端点: + Checking the endpoints:

- t - = - 1 - - |NP| + t = 1 ⇒ |NP| 2 - = + = 1 16 - , -  t - = - - 1 - - |NP| + , t = −1 ⇒ |NP| 2 - = + = 9 16

-

所以

+

So

|NP| min - = + = 1 4 @@ -1453,23 +1413,20 @@

-

答案:

+

Answer:

- P的轨迹方程 4x + Locus of P: 4x 2 - + - ( - y - + + (y − 1 2 - ) + ) 2 - = + = 1 4 @@ -1482,15 +1439,14 @@ |NP| min - = + = 1 4 - , -  |NP| + , |NP| max - = + = @@ -1502,8 +1458,8 @@

-
- +
+ @@ -1511,7 +1467,7 @@
-
-