Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions apps/site/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ 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=
Comment on lines +15 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Warn that the analytics endpoint is client-visible.

index.astro emits this value into the page, so tokens embedded in the URL would be exposed to every visitor.

Proposed doc tweak
 # Generic analytics collector endpoint that accepts JSON POST payloads.
+# This value is exposed to browsers; do not include secrets or private write tokens.
 # Leave empty to disable the built-in pageview and click tracking.
 PUBLIC_ANALYTICS_ENDPOINT=
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Generic analytics collector endpoint that accepts JSON POST payloads.
# Leave empty to disable the built-in pageview and click tracking.
PUBLIC_ANALYTICS_ENDPOINT=
# Generic analytics collector endpoint that accepts JSON POST payloads.
# This value is exposed to browsers; do not include secrets or private write tokens.
# Leave empty to disable the built-in pageview and click tracking.
PUBLIC_ANALYTICS_ENDPOINT=
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/site/.env.example` around lines 15 - 17, The analytics endpoint value is
emitted into the page by index.astro, so the .env.example entry should
explicitly warn that PUBLIC_ANALYTICS_ENDPOINT is client-visible and must not
contain secrets or tokens. Update the comment near PUBLIC_ANALYTICS_ENDPOINT to
call out that anything embedded in the URL will be exposed to visitors, and keep
the guidance aligned with how index.astro consumes this setting.

1 change: 1 addition & 0 deletions apps/site/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default defineConfig({
react(),
starlight({
title: 'TouchAI',
disable404Route: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Astro custom 404 route candidates:"
fd -a '^404\.(astro|md|mdx)$' apps/site/src/pages || true

echo
echo "Starlight docs 404 candidates:"
fd -a '^404\.(md|mdx)$' apps/site/src/content/docs || true

Repository: TouchAI-org/TouchAI

Length of output: 277


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Astro/Starlight config and 404-related files:"
git ls-files 'apps/site/**' | rg '(^|/)(astro\.config\.mjs|404\.(astro|md|mdx)|starlight|pages/404|content/docs/404)' || true

echo
echo "Search for disable404Route usage:"
rg -n "disable404Route|404\\.md|404\\.astro" apps/site || true

echo
echo "Inspect the site astro config:"
cat -n apps/site/astro.config.mjs

Repository: TouchAI-org/TouchAI

Length of output: 972


Keep Starlight’s 404 route enabled
apps/site/src/content/docs/404.md only customizes the injected Starlight 404; there’s no src/pages/404.* in this tree, so disable404Route: true removes the only 404 route. Remove this flag or add a real Astro 404 page.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/site/astro.config.mjs` at line 13, The site config is disabling the only
available 404 route, since `apps/site/src/content/docs/404.md` is just the
Starlight-injected 404 and there is no real `src/pages/404.*` page. Update
`astro.config.mjs` by removing `disable404Route` from the Starlight config, or
alternatively add an actual Astro 404 page if you want to keep the flag; use the
`disable404Route` setting and the existing `404.md` customization as the key
places to fix.

social: [{ icon: 'github', label: 'GitHub', href: 'https://github.com/TouchAI-org/TouchAI' }],
sidebar: [
],
Expand Down
Binary file added apps/site/public/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
253 changes: 253 additions & 0 deletions apps/site/public/demo-utils/touchai-lite-math.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
(function () {
const renderer = window.TouchAILiteRenderer;

function escapeHtml(value) {
return renderer ? renderer.escapeHtml(value) : String(value);
}
Comment on lines +4 to +6

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Keep escaping safe when the base renderer is unavailable.

The fallback currently returns raw String(value), so loading this utility before touchai-lite-renderer.js turns formula/text rendering into raw HTML insertion.

🛡️ Proposed fix
     function escapeHtml(value) {
-        return renderer ? renderer.escapeHtml(value) : String(value);
+        if (renderer) return renderer.escapeHtml(value);
+        return String(value)
+            .replace(/&/g, '&')
+            .replace(/</g, '&lt;')
+            .replace(/>/g, '&gt;')
+            .replace(/"/g, '&quot;');
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function escapeHtml(value) {
return renderer ? renderer.escapeHtml(value) : String(value);
}
function escapeHtml(value) {
if (renderer) return renderer.escapeHtml(value);
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/site/public/demo-utils/touchai-lite-math.js` around lines 4 - 6, The
escapeHtml helper currently falls back to raw String(value) when renderer is
unavailable, which bypasses HTML escaping in touchai-lite-math.js. Update
escapeHtml so the no-renderer path still safely escapes the value instead of
returning unescaped text, and keep the renderer.escapeHtml branch unchanged; use
the escapeHtml function in touchai-lite-math.js as the target for the fix.


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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Make the KaTeX fallback catch lint-clean.

The empty catch with an unused error binding is reported by ESLint. Prefer catch { /* fall back to lightweight renderer */ }.

🧰 Tools
🪛 ESLint

[error] 29-29: 'error' is defined but never used.

(@typescript-eslint/no-unused-vars)


[error] 29-29: Empty block statement.

(no-empty)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/site/public/demo-utils/touchai-lite-math.js` at line 29, The KaTeX
fallback path has an empty catch block with an unused error binding, which
triggers the lint warning. Update the catch in the lightweight renderer fallback
so it uses a binding-free catch form, and keep the fallback behavior in place
within the same KaTeX rendering logic.

Source: Linters/SAST tools

}

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 `<span class="op">${escapeHtml(readRawGroup())}</span>`;
}
if (command === 'frac') {
const num = readArgument();
const den = readArgument();
return `<span class="frac"><span class="num">${num}</span><span class="den">${den}</span></span>`;
}
if (command === 'sqrt') {
const radicand = readArgument();
return `<span class="root"><span class="radicand">${radicand}</span></span>`;
}
if (command === 'boxed') {
return `<span class="boxed-formula">${readArgument()}</span>`;
}

const mapped = commandMap[command] ?? command;
return `<span class="${/^[A-Za-z]+$/.test(mapped) ? 'op' : 'punct'}">${escapeHtml(mapped)}</span>`;
}

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 += `<span class="frac"><span class="num">${num}</span><span class="den">${den}</span></span>`;
continue;
}

if (command === 'sqrt') {
const radicand = readArgument();
html += `<span class="root"><span class="radicand">${radicand}</span></span>`;
continue;
}

if (command === 'boxed') {
html += `<span class="boxed-formula">${readArgument()}</span>`;
continue;
}

if (command === 'text') {
html += `<span class="op">${escapeHtml(readRawGroup())}</span>`;
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 += `<span class="${className}">${escapeHtml(mapped)}</span>`;
continue;
}

html += escapeHtml(char);
index += 1;
}

return html;
};

const inner = parse();
const className = displayMode
? 'formula-module'
: 'formula-module inline-formula';

return `<span class="${className}">${inner}</span>`;
}

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;
});

let html = renderer
? renderer.renderMarkdownContent(protectedMarkdown)
: escapeHtml(protectedMarkdown);

mathTokens.forEach(({ token, formula, display }) => {
const rendered = renderFormula(formula, display);
const replacement = display
? `<div class="math-block"><span class="math-display-frame">${rendered}</span></div>`
: `<span class="katex-formula">${rendered}</span>`;
html = html
.replace(
new RegExp(`<p class="paragraph-node">${token}</p>`, 'g'),
replacement
)
.replace(new RegExp(token, 'g'), replacement);
});

return html;
}

window.TouchAILiteMathRenderer = {
renderFormula,
renderMarkdownWithMath,
};
})();
102 changes: 102 additions & 0 deletions apps/site/public/demo-utils/touchai-lite-renderer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
(function () {
function escapeHtml(value) {
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}

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 `<span class="kbd">${escapeHtml(segment.slice(1, -1))}</span>`;
}

return escapeHtml(segment).replace(
/\*\*([^*]+)\*\*/g,
'<strong>$1</strong>'
);
Comment on lines +26 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Apply the formatter to satisfy Prettier.

Static analysis reports Prettier errors in these two wrapped expressions; this can fail lint even though behavior is correct.

Also applies to: 51-53

🧰 Tools
🪛 ESLint

[error] 26-29: Replace ⏎····················/\*\*([^*]+)\*\*/g,⏎····················'<strong>$1</strong>'⏎················ with /\*\*([^*]+)\*\*/g,·'<strong>$1</strong>'

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/site/public/demo-utils/touchai-lite-renderer.js` around lines 26 - 29,
Prettier is flagging the wrapped replace expressions in the renderer, so update
the formatting in touchai-lite-renderer’s markdown handling to match the
project’s style without changing behavior. Reflow the chained
escapeHtml(...).replace(...) call and the similar wrapped expression later in
the file so they satisfy Prettier, keeping the same regex and replacement logic.

Source: Linters/SAST tools

})
.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(
`<p class="paragraph-node">${paragraphLines.map(renderInline).join('<br>')}</p>`
);
paragraphLines = [];
};

const flushList = () => {
if (!listItems.length) return;
html.push(
`<ul>${listItems
.map((item) => `<li>${renderInline(item)}</li>`)
.join('')}</ul>`
);
listItems = [];
};

lines.forEach((line) => {
const trimmed = stripImageMarkdown(line).trim();

if (!trimmed) {
flushParagraph();
flushList();
return;
}

if (trimmed === '---') {
flushParagraph();
flushList();
html.push(`<p class="response-divider" aria-hidden="true"></p>`);
return;
}

if (trimmed.startsWith('## ')) {
flushParagraph();
flushList();
html.push(`<h2>${renderInline(trimmed.slice(3))}</h2>`);
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,
};
})();
Loading