Commit 88e42620 authored by Kantz's avatar Kantz
Browse files

first implementation with tiptap

parent 9f6573c0
import { Editor, Node, mergeAttributes } from "@tiptap/core";
import StarterKit from "@tiptap/starter-kit";
import katex from "katex";
import "mathlive";
const editorElement = document.querySelector("#editor");
const dialog = document.querySelector("#mathDialog");
const mathField = document.querySelector("#mathField");
const status = document.querySelector("#status");
const jsonOutput = document.querySelector("#json");
const markdownOutput = document.querySelector("#markdown");
let saveMath = null;
function openMathEditor(latex, onSave) {
mathField.value = latex || "";
saveMath = onSave;
dialog.showModal();
requestAnimationFrame(() => mathField.focus());
}
dialog.addEventListener("close", () => {
if (dialog.returnValue === "save" && saveMath) saveMath(mathField.value);
saveMath = null;
});
function renderLatex(dom, latex) {
try {
katex.render(latex || "\\square", dom, { throwOnError: false });
} catch {
dom.textContent = latex || "\\square";
}
}
const MathNode = Node.create({
name: "math",
group: "inline",
inline: true,
atom: true,
selectable: true,
addAttributes() {
return {
latex: { default: "" },
display: { default: "inline" },
};
},
parseHTML() {
return [{ tag: "span[data-math]" }];
},
renderHTML({ HTMLAttributes }) {
return ["span", mergeAttributes(HTMLAttributes, { "data-math": "" })];
},
addCommands() {
return {
insertMath:
(latex = "") =>
({ commands }) =>
commands.insertContent({ type: this.name, attrs: { latex } }),
};
},
addNodeView() {
return ({ node, editor, getPos }) => {
const dom = document.createElement("span");
dom.className = "math-chip";
dom.tabIndex = 0;
dom.dataset.math = "";
const edit = () => {
openMathEditor(node.attrs.latex, (latex) => {
editor
.chain()
.focus()
.command(({ tr }) => {
tr.setNodeMarkup(getPos(), undefined, { ...node.attrs, latex });
return true;
})
.run();
});
};
dom.addEventListener("click", edit);
dom.addEventListener("keydown", (event) => {
if (event.key !== "Enter" && event.key !== " ") return;
event.preventDefault();
edit();
});
renderLatex(dom, node.attrs.latex);
return {
dom,
update(updatedNode) {
if (updatedNode.type.name !== node.type.name) return false;
node = updatedNode;
renderLatex(dom, node.attrs.latex);
return true;
},
};
};
},
});
const editor = new Editor({
element: editorElement,
extensions: [StarterKit.configure({ heading: false }), MathNode],
content: "<p>Explain why <span data-math latex=\"\\frac{d}{dx}x^2 = 2x\"></span> is true.</p>",
editorProps: {
attributes: {
class: "tiptap",
},
handleKeyDown(view, event) {
if (event.key !== "$") return false;
event.preventDefault();
openMathEditor("", (latex) => editor.chain().focus().insertMath(latex).run());
return true;
},
},
onUpdate: updatePreview,
});
document.querySelector("#insertMath").addEventListener("click", () => {
openMathEditor("", (latex) => editor.chain().focus().insertMath(latex).run());
});
document.querySelector("#imageUpload").addEventListener("change", async (event) => {
const file = event.target.files[0];
if (!file) return;
status.textContent = "Reading image...";
try {
const form = new FormData();
form.append("image", file);
// ponytail: static demo expects the app backend to proxy Mathpix at this endpoint.
const response = await fetch("/api/mathpix", { method: "POST", body: form });
const result = await response.json();
if (!response.ok) throw new Error(result.error || `Mathpix failed: ${response.status}`);
const { latex } = result;
editor.chain().focus().insertMath(latex || "").run();
status.textContent = "Formula inserted";
} catch (error) {
status.textContent = error.message;
} finally {
event.target.value = "";
}
});
document.querySelector("#send").addEventListener("click", () => {
updatePreview();
status.textContent = "Serialized";
});
function toChatContent(doc) {
const content = [];
function walk(node) {
if (node.type === "text") content.push({ type: "text", text: node.text });
if (node.type === "math") {
content.push({
type: "math",
latex: node.attrs.latex,
display: node.attrs.display || "inline",
});
}
(node.content || []).forEach(walk);
}
walk(doc);
return content;
}
function toMarkdown(parts) {
return parts
.map((part) => (part.type === "math" ? `$${part.latex}$` : part.text))
.join("");
}
function updatePreview() {
const parts = toChatContent(editor.getJSON());
jsonOutput.textContent = JSON.stringify(parts, null, 2);
markdownOutput.textContent = toMarkdown(parts);
}
updatePreview();
const sample = toMarkdown([
{ type: "text", text: "x = " },
{ type: "math", latex: "1", display: "inline" },
]);
console.assert(sample === "x = $1$", "Markdown math serialization failed");
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Tiptap MathLive Chat Input</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css"
/>
<link rel="stylesheet" href="./styles.css" />
<script type="importmap">
{
"imports": {
"@tiptap/core": "https://esm.sh/@tiptap/core@2.11.5",
"@tiptap/starter-kit": "https://esm.sh/@tiptap/starter-kit@2.11.5",
"katex": "https://esm.sh/katex@0.16.10",
"mathlive": "https://esm.sh/mathlive@0.101.1"
}
}
</script>
<script type="module" src="./app.js"></script>
</head>
<body>
<main class="app">
<section class="composer" aria-label="Chat composer">
<div class="toolbar">
<button id="insertMath" type="button" title="Insert formula">f(x)</button>
<label class="upload" title="Read handwriting with Mathpix">
<input id="imageUpload" type="file" accept="image/*" />
Image
</label>
<button id="send" type="button">Send</button>
<span id="status" class="status" role="status"></span>
</div>
<div id="editor" class="editor" aria-label="Message input"></div>
<output id="json" class="output" aria-label="Structured JSON"></output>
<output id="markdown" class="output" aria-label="Markdown"></output>
</section>
</main>
<dialog id="mathDialog" class="math-dialog">
<form method="dialog">
<math-field id="mathField"></math-field>
<div class="dialog-actions">
<button id="cancelMath" value="cancel" type="submit">Cancel</button>
<button id="saveMath" value="save" type="submit">Save</button>
</div>
</form>
</dialog>
</body>
</html>
const http = require("http");
const fs = require("fs");
const path = require("path");
const root = __dirname;
const types = {
".css": "text/css; charset=utf-8",
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
};
function loadEnv(file = path.join(root, ".env")) {
if (!fs.existsSync(file)) return;
for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
const match = line.match(/^\s*([\w.-]+)\s*=\s*(.*)\s*$/);
if (!match || process.env[match[1]]) continue;
process.env[match[1]] = match[2].replace(/^['"]|['"]$/g, "");
}
}
function readBody(req, maxBytes = 10 * 1024 * 1024) {
return new Promise((resolve, reject) => {
const chunks = [];
let size = 0;
req.on("data", (chunk) => {
size += chunk.length;
if (size > maxBytes) {
req.destroy();
reject(new Error("Image is too large"));
return;
}
chunks.push(chunk);
});
req.on("end", () => resolve(Buffer.concat(chunks)));
req.on("error", reject);
});
}
function fileFromMultipart(contentType, body) {
const boundary = contentType.match(/boundary=(?:"([^"]+)"|([^;]+))/)?.slice(1).find(Boolean);
if (!boundary) throw new Error("Missing multipart boundary");
const raw = body.toString("binary");
const part = raw
.split(`--${boundary}`)
.find((chunk) => /name="image"/.test(chunk) && /filename=/.test(chunk));
if (!part) throw new Error("Missing image file");
const splitAt = part.indexOf("\r\n\r\n");
if (splitAt === -1) throw new Error("Invalid image upload");
const headers = part.slice(0, splitAt);
const data = part.slice(splitAt + 4).replace(/\r\n$/, "");
const mime = headers.match(/content-type:\s*([^\r\n]+)/i)?.[1] || "image/jpeg";
return { mime, buffer: Buffer.from(data, "binary") };
}
async function callMathpix(file) {
const response = await fetch("https://api.mathpix.com/v3/latex", {
method: "POST",
headers: {
"content-type": "application/json",
app_id: process.env.MATHPIX_APP_ID || "",
app_key: process.env.MATHPIX_APP_KEY || "",
},
body: JSON.stringify({
src: `data:${file.mime};base64,${file.buffer.toString("base64")}`,
formats: ["latex_normal"],
}),
});
const result = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(result.error || `Mathpix failed: ${response.status}`);
return result;
}
function sendJson(res, status, data) {
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
res.end(JSON.stringify(data));
}
async function handleMathpix(req, res) {
if (!process.env.MATHPIX_APP_ID || !process.env.MATHPIX_APP_KEY) {
sendJson(res, 500, { error: "Missing MATHPIX_APP_ID or MATHPIX_APP_KEY" });
return;
}
try {
const file = fileFromMultipart(req.headers["content-type"] || "", await readBody(req));
const raw = await callMathpix(file);
sendJson(res, 200, { latex: raw.latex || raw.latex_normal || "", raw });
} catch (error) {
sendJson(res, 400, { error: error.message });
}
}
function serveStatic(req, res) {
const urlPath = decodeURIComponent(new URL(req.url, "http://localhost").pathname);
const file = path.join(root, urlPath === "/" ? "index.html" : urlPath);
const relative = path.relative(root, file);
if (relative.startsWith("..") || path.isAbsolute(relative) || path.basename(file) === ".env") {
res.writeHead(404);
res.end("Not found");
return;
}
fs.readFile(file, (error, data) => {
if (error) {
res.writeHead(404);
res.end("Not found");
return;
}
res.writeHead(200, { "content-type": types[path.extname(file)] || "application/octet-stream" });
res.end(data);
});
}
function createServer() {
loadEnv();
return http.createServer((req, res) => {
if (req.method === "POST" && req.url === "/api/mathpix") {
handleMathpix(req, res);
return;
}
if (req.method === "GET" || req.method === "HEAD") {
serveStatic(req, res);
return;
}
res.writeHead(405);
res.end("Method not allowed");
});
}
function selfTest() {
const body = Buffer.from(
'--x\r\ncontent-disposition: form-data; name="image"; filename="a.png"\r\ncontent-type: image/png\r\n\r\nabc\r\n--x--\r\n',
"binary"
);
const file = fileFromMultipart("multipart/form-data; boundary=x", body);
console.assert(file.mime === "image/png", "multipart mime parse failed");
console.assert(file.buffer.toString() === "abc", "multipart body parse failed");
}
if (process.argv.includes("--self-test")) {
selfTest();
} else {
const port = Number(process.env.PORT || 5174);
createServer().listen(port, "127.0.0.1", () => {
console.log(`Listening on http://127.0.0.1:${port}`);
});
}
:root {
color-scheme: light;
font-family: Inter, "Segoe UI", Arial, sans-serif;
background: #f4f6f8;
color: #17202a;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
}
button,
.upload {
min-height: 40px;
border: 1px solid #c8d0d8;
border-radius: 8px;
padding: 0 14px;
background: #fff;
color: #17202a;
font: inherit;
font-weight: 650;
cursor: pointer;
}
button:hover,
.upload:hover {
border-color: #576b80;
}
.app {
display: grid;
min-height: 100vh;
align-items: center;
padding: 24px;
}
.composer {
width: min(860px, 100%);
margin: 0 auto;
}
.toolbar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
#send {
margin-left: auto;
border-color: #145c47;
background: #17785b;
color: #fff;
}
.upload {
display: inline-flex;
align-items: center;
}
.upload input {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
}
.status {
min-width: 140px;
color: #526170;
font-size: 0.9rem;
}
.editor {
min-height: 132px;
border: 1px solid #b9c3cd;
border-radius: 8px;
background: #fff;
}
.tiptap {
min-height: 132px;
padding: 16px;
font-size: 1.08rem;
line-height: 1.65;
outline: none;
}
.tiptap p {
margin: 0;
}
.math-chip {
display: inline-block;
min-width: 1.4em;
margin: 0 0.12em;
padding: 1px 5px;
border: 1px solid #b9d4cc;
border-radius: 6px;
background: #edf8f4;
vertical-align: baseline;
cursor: text;
}
.math-chip:focus {
outline: 3px solid rgba(23, 120, 91, 0.22);
outline-offset: 2px;
}
.output {
display: block;
width: 100%;
margin-top: 12px;
padding: 12px;
border: 1px solid #d8dee4;
border-radius: 8px;
background: #101820;
color: #d8f3dc;
white-space: pre-wrap;
overflow-wrap: anywhere;
font: 0.86rem/1.45 Consolas, "Liberation Mono", monospace;
}
.math-dialog {
width: min(560px, calc(100vw - 32px));
border: 1px solid #b9c3cd;
border-radius: 8px;
padding: 16px;
}
.math-dialog::backdrop {
background: rgba(9, 20, 31, 0.44);
}
math-field {
width: 100%;
min-height: 72px;
padding: 10px;
border: 1px solid #b9c3cd;
border-radius: 8px;
font-size: 1.4rem;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 12px;
}
@media (max-width: 640px) {
.app {
padding: 14px;
}
.toolbar {
flex-wrap: wrap;
}
#send {
margin-left: 0;
}
.status {
flex-basis: 100%;
}
}
...@@ -11,9 +11,12 @@ ...@@ -11,9 +11,12 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@tiptap/core": "^3.27.1",
"@tiptap/react": "^3.27.1",
"@tiptap/starter-kit": "^3.27.1",
"iconoir-react": "^7.11.0", "iconoir-react": "^7.11.0",
"jquery": "1.12.4", "katex": "^0.17.0",
"mathquill": "0.10.1-a", "mathlive": "^0.110.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
......
...@@ -8,15 +8,24 @@ importers: ...@@ -8,15 +8,24 @@ importers:
.: .:
dependencies: dependencies:
'@tiptap/core':
specifier: ^3.27.1
version: 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/react':
specifier: ^3.27.1
version: 3.27.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@tiptap/starter-kit':
specifier: ^3.27.1
version: 3.27.1
iconoir-react: iconoir-react:
specifier: ^7.11.0 specifier: ^7.11.0
version: 7.11.0(react@19.2.3) version: 7.11.0(react@19.2.3)
jquery: katex:
specifier: 1.12.4 specifier: ^0.17.0
version: 1.12.4 version: 0.17.0
mathquill: mathlive:
specifier: 0.10.1-a specifier: ^0.110.0
version: 0.10.1-a version: 0.110.0
react: react:
specifier: ^19.2.0 specifier: ^19.2.0
version: 19.2.3 version: 19.2.3
...@@ -69,6 +78,9 @@ importers: ...@@ -69,6 +78,9 @@ importers:
packages: packages:
'@arnog/colors@0.5.0':
resolution: {integrity: sha512-NB7yqrO7qvInsqJdqz6C6mDU+2oiKmbBbk3czMS0E7KkGxLRy538tEHCsB5TKqmaxxXpi6/U120TnGl8NsDePg==}
'@babel/code-frame@7.28.6': '@babel/code-frame@7.28.6':
resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
...@@ -152,6 +164,10 @@ packages: ...@@ -152,6 +164,10 @@ packages:
resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@cortex-js/compute-engine@0.58.0':
resolution: {integrity: sha512-N0+vXjVZfKwJS7ZIGvq41mojI55m5TrdDXveAXzNjzjZ9iNiNrdEzXL2EmrcP+9GCWCwTonrEeZWAIG78f3INg==}
engines: {node: '>=21.7.3', npm: '>=10.5.0'}
'@emnapi/core@1.8.1': '@emnapi/core@1.8.1':
resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
...@@ -199,6 +215,15 @@ packages: ...@@ -199,6 +215,15 @@ packages:
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@floating-ui/core@1.7.5':
resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
'@floating-ui/dom@1.7.6':
resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==}
'@floating-ui/utils@0.2.11':
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
'@humanfs/core@0.19.1': '@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'} engines: {node: '>=18.18.0'}
...@@ -334,6 +359,155 @@ packages: ...@@ -334,6 +359,155 @@ packages:
'@rolldown/pluginutils@1.0.0-beta.53': '@rolldown/pluginutils@1.0.0-beta.53':
resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
'@tiptap/core@3.27.1':
resolution: {integrity: sha512-rV6Qn4wmC6BxfF+4mu6bqGWj9vA4oXXhsrpXaJL2uhjxeHAGofjwcHof2X84VYzeyXgdlsGmqKie4TAppVXZUQ==}
peerDependencies:
'@tiptap/pm': 3.27.1
'@tiptap/extension-blockquote@3.27.1':
resolution: {integrity: sha512-VMF7xJx6qEGiX6DTKNiL31NLqypOcd/4sNjFSe8rb41PwejBJh/nOqVIbBvWkiT6NMGFLxMhj7zJ8/zPo1hXeg==}
peerDependencies:
'@tiptap/core': 3.27.1
'@tiptap/extension-bold@3.27.1':
resolution: {integrity: sha512-TlC5bsS+pqETTrlz4CZz9RO/cKBYtELGIxwtKeivUn3eNfnOxQbbu4WDsiwIfzRFyd0OMnKl6BPM2KnYEehoEQ==}
peerDependencies:
'@tiptap/core': 3.27.1
'@tiptap/extension-bubble-menu@3.27.1':
resolution: {integrity: sha512-j/j8Qp9Z5nViade2m7zjrO/CYH/Ca80Qj7aqo0eUaei6FZQ5izlF9o4XQU5EFMAutV6mwynsPUp8FVo5sCuYfw==}
peerDependencies:
'@tiptap/core': 3.27.1
'@tiptap/pm': 3.27.1
'@tiptap/extension-bullet-list@3.27.1':
resolution: {integrity: sha512-faCUHnRP47o9Zh9VZZX6EX/569udw9Vopm2PgEKPWuKLE2qaS5WBuUVU0iItdJmKUqaWiOZkpoW4jvnDmj0dfg==}
peerDependencies:
'@tiptap/extension-list': 3.27.1
'@tiptap/extension-code-block@3.27.1':
resolution: {integrity: sha512-pHlzmZx2OlHfyQ0yRlT5UL4mGokz947DthZuYefN1OleVqOkHpWBG+2JQwqoNq6bmzMne92zbH32rhcJUEYSjA==}
peerDependencies:
'@tiptap/core': 3.27.1
'@tiptap/pm': 3.27.1
'@tiptap/extension-code@3.27.1':
resolution: {integrity: sha512-epOUpFfEmBzjvnqvjv2qHX7NAuLo5dlOGV690lWu+sAYMjibuJBeVvAiKPyFCfRCCTUxdbDB3jbaOA1yEcEJ7w==}
peerDependencies:
'@tiptap/core': 3.27.1
'@tiptap/extension-document@3.27.1':
resolution: {integrity: sha512-8FbBTkfnRP4iVaoj+2h3iWa+H0eGDD3yTyVCwrmue/sQTkqUNUoSuAZa3GDG4Sd41xdPwTJxl9nUWGgM1qDCnw==}
peerDependencies:
'@tiptap/core': 3.27.1
'@tiptap/extension-dropcursor@3.27.1':
resolution: {integrity: sha512-blFf9x9RG0Qr7P3FoAH/033ffa+mMLZn34trVs8Vi0Ppk6FmJAg5HpYFOtmYoeREdNDJ5rHJKV7SoACbOHgskQ==}
peerDependencies:
'@tiptap/extensions': 3.27.1
'@tiptap/extension-floating-menu@3.27.1':
resolution: {integrity: sha512-BmJF1VqB7dSJkgAalrpVFj88WLhxKjcWPuWHOqf2ITrUU2832BhKLXKmxjWUy1gqV8PfNNVWtGfIERy7I0y0+Q==}
peerDependencies:
'@floating-ui/dom': ^1.0.0
'@tiptap/core': 3.27.1
'@tiptap/pm': 3.27.1
'@tiptap/extension-gapcursor@3.27.1':
resolution: {integrity: sha512-QoezN0wdvXIwLQ4ee2ccWDaX3RG0lzgQpIMpMz55oPDhpUVax1+19ApsS53LkcktpS4EbnPL4xO4DaJk0Vp7PQ==}
peerDependencies:
'@tiptap/extensions': 3.27.1
'@tiptap/extension-hard-break@3.27.1':
resolution: {integrity: sha512-iv/m9hzl6jfSj9Q8UEjAxONvCoUDaP7M9SRCPx3PaLNxA230TTD6RE0Ye4zFJ8ze7ZVoJJMAqg9Qpq1iYg2JOQ==}
peerDependencies:
'@tiptap/core': 3.27.1
'@tiptap/extension-heading@3.27.1':
resolution: {integrity: sha512-SrC4l1kEIyv9ZXFaI/8LQqU2MyMmjczw7XXsWUQOTN4YXv0JyVgMNR3cI/wz0d2xsTfBdZ1N85Tdng+Ga1t0Sg==}
peerDependencies:
'@tiptap/core': 3.27.1
'@tiptap/extension-horizontal-rule@3.27.1':
resolution: {integrity: sha512-QlKE7qn5qMnIGVGhXQlvYedvLtNJ9z0dmit5w8vPb8tKzW4Spk6M7N2kruprrDA8GBwHfeR5wmF+njfUm34qxg==}
peerDependencies:
'@tiptap/core': 3.27.1
'@tiptap/pm': 3.27.1
'@tiptap/extension-italic@3.27.1':
resolution: {integrity: sha512-jGGeyn9uRUnNjSTHpbqhiGsp6KaYTSbV09jDXPJI9cDwfV9hpugLvpaCZd0BMBbhU1B1W6kOfX0BE15qX/HQfA==}
peerDependencies:
'@tiptap/core': 3.27.1
'@tiptap/extension-link@3.27.1':
resolution: {integrity: sha512-/2jBfsxBZUDGJmpZifqRQPz7f1E5qpS1BckTZ39TADzUJX+feKy7RJ3DtQ02+8y6SSMzvP9loGVjrk6zEMTk4g==}
peerDependencies:
'@tiptap/core': 3.27.1
'@tiptap/pm': 3.27.1
'@tiptap/extension-list-item@3.27.1':
resolution: {integrity: sha512-zwRl01ETfCkWUvtvK5fw9bXtAajMPkvlkE3Cq6JvH3LF7XXJwDtNj5Tj7exacMpCaSZmlNc43vFb2rAYnrnwMA==}
peerDependencies:
'@tiptap/extension-list': 3.27.1
'@tiptap/extension-list-keymap@3.27.1':
resolution: {integrity: sha512-OIMZNlzPSO8WRd4ic73Fxckzl4N1tesjjLL2XApaNA/uMpO0LoF6WSRPAWv+Z24Wp92ARRJAnRP7iZoI5+Jxig==}
peerDependencies:
'@tiptap/extension-list': 3.27.1
'@tiptap/extension-list@3.27.1':
resolution: {integrity: sha512-c2Upru7lj0/ZV/Ibww6cNz6sUS8m6Dp/9uygFhYcZOd3X8M0xBIEk42c6m6SQehkPziVA8QOgNJz7sMqsbz1OQ==}
peerDependencies:
'@tiptap/core': 3.27.1
'@tiptap/pm': 3.27.1
'@tiptap/extension-ordered-list@3.27.1':
resolution: {integrity: sha512-GYrKqD//9nHJ2r80uXqbDMzRnFpGzbaEQRTSGaO/SH7DvXWFMow8evkOdjQ7PCQO07jNjJo75+A85Jwu3Ov3AA==}
peerDependencies:
'@tiptap/extension-list': 3.27.1
'@tiptap/extension-paragraph@3.27.1':
resolution: {integrity: sha512-7K7eo1gruOgAsnbK+GCV23AUVUI0cL1bTig8HaPneoFMVbig7vddk8jNLKBWO8TXVbG7TuHdnDN4F98vdtwh5Q==}
peerDependencies:
'@tiptap/core': 3.27.1
'@tiptap/extension-strike@3.27.1':
resolution: {integrity: sha512-Y3DW1jlSlCNCyMGHP3+3qBNNPS83wuFz4RTYGjZtvRRTCRh7apZme9XRWMq1rN5mJ2Cr7fKocA2/5Bs13KgN6Q==}
peerDependencies:
'@tiptap/core': 3.27.1
'@tiptap/extension-text@3.27.1':
resolution: {integrity: sha512-6ZwaZwSrDh+KFFv6V1J79oO37yPs7y1bFxvk1/9Ih2rn3Xr5AWz+eMS+n8RpH3djBVVAQpdIAeYQgcn+VCSsTg==}
peerDependencies:
'@tiptap/core': 3.27.1
'@tiptap/extension-underline@3.27.1':
resolution: {integrity: sha512-N889J4nXN/TPfVt8uF9N1A0SY82E90zwc1y26lqOcw6KWNLmQrlhMh/9OD4ikLDbekmFpOBq/UicpHf/6S8hbQ==}
peerDependencies:
'@tiptap/core': 3.27.1
'@tiptap/extensions@3.27.1':
resolution: {integrity: sha512-1Tdx9faw8k0/83V6X+xCDVhV8yElGt95JxeW3YMkKQJI56QdlPz0xOdJPlMiSGJKinPyVier+x9LJD/YZUZIaw==}
peerDependencies:
'@tiptap/core': 3.27.1
'@tiptap/pm': 3.27.1
'@tiptap/pm@3.27.1':
resolution: {integrity: sha512-Ffjx+vimmBU7zH/KrpXzJid3+pziCe/VL2aexSTP63cyQwKQ65LkFkCKaIsSpFdQQuakVZBGWjCA5RoBV852pw==}
'@tiptap/react@3.27.1':
resolution: {integrity: sha512-/Wn2fc9zMtX08MXYScDFsm4wJ8lzfhfPEdbtls7WCDlbtrop48PWlkHDBBJrywARfAQTB2mFs9KiFy9yrQm5Lg==}
peerDependencies:
'@tiptap/core': 3.27.1
'@tiptap/pm': 3.27.1
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
'@types/react-dom': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
'@tiptap/starter-kit@3.27.1':
resolution: {integrity: sha512-vfxRsqW8rCc0k4pzo0ilU3wobVi2wqVj88VZI2SlgZlNnUAkrDGDIAph7CTa9k9fshV+O1ivpEgPC5yC046jow==}
'@tybys/wasm-util@0.10.1': '@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
...@@ -387,6 +561,9 @@ packages: ...@@ -387,6 +561,9 @@ packages:
'@types/unist@3.0.3': '@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
'@types/use-sync-external-store@0.0.6':
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
'@typescript-eslint/eslint-plugin@8.53.0': '@typescript-eslint/eslint-plugin@8.53.0':
resolution: {integrity: sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==} resolution: {integrity: sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
...@@ -532,6 +709,14 @@ packages: ...@@ -532,6 +709,14 @@ packages:
comma-separated-tokens@2.0.3: comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
commander@8.3.0:
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
engines: {node: '>= 12'}
complex-esm@2.1.1-esm1:
resolution: {integrity: sha512-IShBEWHILB9s7MnfyevqNGxV0A1cfcSnewL/4uPFiSxkcQL4Mm3FxJ0pXMtCXuWLjYz3lRRyk6OfkeDZcjD6nw==}
engines: {node: '>=16.14.2', npm: '>=8.5.0'}
concat-map@0.0.1: concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
...@@ -558,6 +743,9 @@ packages: ...@@ -558,6 +743,9 @@ packages:
supports-color: supports-color:
optional: true optional: true
decimal.js@10.6.0:
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
decode-named-character-reference@1.3.0: decode-named-character-reference@1.3.0:
resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
...@@ -648,6 +836,10 @@ packages: ...@@ -648,6 +836,10 @@ packages:
fast-deep-equal@3.1.3: fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-equals@5.4.0:
resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==}
engines: {node: '>=6.0.0'}
fast-json-stable-stringify@2.1.0: fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
...@@ -769,10 +961,6 @@ packages: ...@@ -769,10 +961,6 @@ packages:
isexe@2.0.0: isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
jquery@1.12.4:
resolution: {integrity: sha512-UEVp7PPK9xXYSk8xqXCJrkXnKZtlgWkd2GsAQbMRFK6S/ePU2JN5G2Zum8hIVjzR3CpdfSqdqAzId/xd4TJHeg==}
deprecated: This version is deprecated. Please upgrade to the latest version or find support at https://www.herodevs.com/support/jquery-nes.
js-tokens@4.0.0: js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
...@@ -799,6 +987,10 @@ packages: ...@@ -799,6 +987,10 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
hasBin: true hasBin: true
katex@0.17.0:
resolution: {integrity: sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw==}
hasBin: true
keyv@4.5.4: keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
...@@ -880,6 +1072,9 @@ packages: ...@@ -880,6 +1072,9 @@ packages:
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
linkifyjs@4.3.3:
resolution: {integrity: sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==}
locate-path@6.0.0: locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'} engines: {node: '>=10'}
...@@ -893,8 +1088,8 @@ packages: ...@@ -893,8 +1088,8 @@ packages:
lru-cache@5.1.1: lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
mathquill@0.10.1-a: mathlive@0.110.0:
resolution: {integrity: sha512-snSAEwAtwdwBFSor+nVBnWWQtTw67kgAgKMyAIxuz4ZPboy0qkWZmd7BL3lfOXp/INihhRlU1PcfaAtDaRhmzA==} resolution: {integrity: sha512-UOpJsQ6h1eeN0xZULGTl1MwUB/lZLCDuzKeHvKEh7Zra8U/rtgDNrssj5PZdJtBTIV48AfEhtv6rv47AeMOxJA==}
mdast-util-from-markdown@2.0.2: mdast-util-from-markdown@2.0.2:
resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==}
...@@ -1008,6 +1203,9 @@ packages: ...@@ -1008,6 +1203,9 @@ packages:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
orderedmap@2.1.1:
resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==}
p-limit@3.1.0: p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
...@@ -1049,6 +1247,45 @@ packages: ...@@ -1049,6 +1247,45 @@ packages:
property-information@7.1.0: property-information@7.1.0:
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
prosemirror-changeset@2.4.1:
resolution: {integrity: sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==}
prosemirror-commands@1.7.1:
resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==}
prosemirror-dropcursor@1.8.2:
resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==}
prosemirror-gapcursor@1.4.1:
resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==}
prosemirror-history@1.5.0:
resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==}
prosemirror-inputrules@1.5.1:
resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==}
prosemirror-keymap@1.2.3:
resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==}
prosemirror-model@1.25.9:
resolution: {integrity: sha512-pRTklkDDMMRopyoAcrr9wV/8g/RYgrLHBuJAb5hlEuYZRdm5yqmPjWId83fpBwPpSFqEdja0H7Dfd7z1X/npcA==}
prosemirror-schema-list@1.5.1:
resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==}
prosemirror-state@1.4.4:
resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==}
prosemirror-tables@1.8.5:
resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==}
prosemirror-transform@1.12.0:
resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==}
prosemirror-view@1.41.9:
resolution: {integrity: sha512-clTunTX+eaLbr87L1V1QPheRlEQJyTlL3gXe9x3jQIk3rL0RVWxviDGz8tFaydwIVm+hKhYCyr+R/zBtWr9s6A==}
punycode@2.3.1: punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
...@@ -1144,6 +1381,9 @@ packages: ...@@ -1144,6 +1381,9 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true hasBin: true
rope-sequence@1.3.4:
resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==}
scheduler@0.27.0: scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
...@@ -1256,12 +1496,20 @@ packages: ...@@ -1256,12 +1496,20 @@ packages:
uri-js@4.4.1: uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
use-sync-external-store@1.6.0:
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
vfile-message@4.0.3: vfile-message@4.0.3:
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
vfile@6.0.3: vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
which@2.0.2: which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
...@@ -1292,6 +1540,8 @@ packages: ...@@ -1292,6 +1540,8 @@ packages:
snapshots: snapshots:
'@arnog/colors@0.5.0': {}
'@babel/code-frame@7.28.6': '@babel/code-frame@7.28.6':
dependencies: dependencies:
'@babel/helper-validator-identifier': 7.28.5 '@babel/helper-validator-identifier': 7.28.5
...@@ -1404,6 +1654,12 @@ snapshots: ...@@ -1404,6 +1654,12 @@ snapshots:
'@babel/helper-string-parser': 7.27.1 '@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5 '@babel/helper-validator-identifier': 7.28.5
'@cortex-js/compute-engine@0.58.0':
dependencies:
'@arnog/colors': 0.5.0
complex-esm: 2.1.1-esm1
decimal.js: 10.6.0
'@emnapi/core@1.8.1': '@emnapi/core@1.8.1':
dependencies: dependencies:
'@emnapi/wasi-threads': 1.1.0 '@emnapi/wasi-threads': 1.1.0
...@@ -1466,6 +1722,20 @@ snapshots: ...@@ -1466,6 +1722,20 @@ snapshots:
'@eslint/core': 0.17.0 '@eslint/core': 0.17.0
levn: 0.4.1 levn: 0.4.1
'@floating-ui/core@1.7.5':
dependencies:
'@floating-ui/utils': 0.2.11
optional: true
'@floating-ui/dom@1.7.6':
dependencies:
'@floating-ui/core': 1.7.5
'@floating-ui/utils': 0.2.11
optional: true
'@floating-ui/utils@0.2.11':
optional: true
'@humanfs/core@0.19.1': {} '@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.7': '@humanfs/node@0.16.7':
...@@ -1555,6 +1825,178 @@ snapshots: ...@@ -1555,6 +1825,178 @@ snapshots:
'@rolldown/pluginutils@1.0.0-beta.53': {} '@rolldown/pluginutils@1.0.0-beta.53': {}
'@tiptap/core@3.27.1(@tiptap/pm@3.27.1)':
dependencies:
'@tiptap/pm': 3.27.1
'@tiptap/extension-blockquote@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))':
dependencies:
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/extension-bold@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))':
dependencies:
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/extension-bubble-menu@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)':
dependencies:
'@floating-ui/dom': 1.7.6
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/pm': 3.27.1
optional: true
'@tiptap/extension-bullet-list@3.27.1(@tiptap/extension-list@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1))':
dependencies:
'@tiptap/extension-list': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)
'@tiptap/extension-code-block@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)':
dependencies:
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/pm': 3.27.1
'@tiptap/extension-code@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))':
dependencies:
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/extension-document@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))':
dependencies:
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/extension-dropcursor@3.27.1(@tiptap/extensions@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1))':
dependencies:
'@tiptap/extensions': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)
'@tiptap/extension-floating-menu@3.27.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)':
dependencies:
'@floating-ui/dom': 1.7.6
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/pm': 3.27.1
optional: true
'@tiptap/extension-gapcursor@3.27.1(@tiptap/extensions@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1))':
dependencies:
'@tiptap/extensions': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)
'@tiptap/extension-hard-break@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))':
dependencies:
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/extension-heading@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))':
dependencies:
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/extension-horizontal-rule@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)':
dependencies:
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/pm': 3.27.1
'@tiptap/extension-italic@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))':
dependencies:
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/extension-link@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)':
dependencies:
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/pm': 3.27.1
linkifyjs: 4.3.3
'@tiptap/extension-list-item@3.27.1(@tiptap/extension-list@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1))':
dependencies:
'@tiptap/extension-list': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)
'@tiptap/extension-list-keymap@3.27.1(@tiptap/extension-list@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1))':
dependencies:
'@tiptap/extension-list': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)
'@tiptap/extension-list@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)':
dependencies:
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/pm': 3.27.1
'@tiptap/extension-ordered-list@3.27.1(@tiptap/extension-list@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1))':
dependencies:
'@tiptap/extension-list': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)
'@tiptap/extension-paragraph@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))':
dependencies:
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/extension-strike@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))':
dependencies:
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/extension-text@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))':
dependencies:
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/extension-underline@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))':
dependencies:
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/extensions@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)':
dependencies:
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/pm': 3.27.1
'@tiptap/pm@3.27.1':
dependencies:
prosemirror-changeset: 2.4.1
prosemirror-commands: 1.7.1
prosemirror-dropcursor: 1.8.2
prosemirror-gapcursor: 1.4.1
prosemirror-history: 1.5.0
prosemirror-inputrules: 1.5.1
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.9
prosemirror-schema-list: 1.5.1
prosemirror-state: 1.4.4
prosemirror-tables: 1.8.5
prosemirror-transform: 1.12.0
prosemirror-view: 1.41.9
'@tiptap/react@3.27.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/pm': 3.27.1
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
'@types/use-sync-external-store': 0.0.6
fast-equals: 5.4.0
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
use-sync-external-store: 1.6.0(react@19.2.3)
optionalDependencies:
'@tiptap/extension-bubble-menu': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)
'@tiptap/extension-floating-menu': 3.27.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)
transitivePeerDependencies:
- '@floating-ui/dom'
'@tiptap/starter-kit@3.27.1':
dependencies:
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/extension-blockquote': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))
'@tiptap/extension-bold': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))
'@tiptap/extension-bullet-list': 3.27.1(@tiptap/extension-list@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1))
'@tiptap/extension-code': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))
'@tiptap/extension-code-block': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)
'@tiptap/extension-document': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))
'@tiptap/extension-dropcursor': 3.27.1(@tiptap/extensions@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1))
'@tiptap/extension-gapcursor': 3.27.1(@tiptap/extensions@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1))
'@tiptap/extension-hard-break': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))
'@tiptap/extension-heading': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))
'@tiptap/extension-horizontal-rule': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)
'@tiptap/extension-italic': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))
'@tiptap/extension-link': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)
'@tiptap/extension-list': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)
'@tiptap/extension-list-item': 3.27.1(@tiptap/extension-list@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1))
'@tiptap/extension-list-keymap': 3.27.1(@tiptap/extension-list@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1))
'@tiptap/extension-ordered-list': 3.27.1(@tiptap/extension-list@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1))
'@tiptap/extension-paragraph': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))
'@tiptap/extension-strike': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))
'@tiptap/extension-text': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))
'@tiptap/extension-underline': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))
'@tiptap/extensions': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)
'@tiptap/pm': 3.27.1
'@tybys/wasm-util@0.10.1': '@tybys/wasm-util@0.10.1':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
...@@ -1619,6 +2061,8 @@ snapshots: ...@@ -1619,6 +2061,8 @@ snapshots:
'@types/unist@3.0.3': {} '@types/unist@3.0.3': {}
'@types/use-sync-external-store@0.0.6': {}
'@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)': '@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)':
dependencies: dependencies:
'@eslint-community/regexpp': 4.12.2 '@eslint-community/regexpp': 4.12.2
...@@ -1793,6 +2237,10 @@ snapshots: ...@@ -1793,6 +2237,10 @@ snapshots:
comma-separated-tokens@2.0.3: {} comma-separated-tokens@2.0.3: {}
commander@8.3.0: {}
complex-esm@2.1.1-esm1: {}
concat-map@0.0.1: {} concat-map@0.0.1: {}
convert-source-map@2.0.0: {} convert-source-map@2.0.0: {}
...@@ -1811,6 +2259,8 @@ snapshots: ...@@ -1811,6 +2259,8 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
decimal.js@10.6.0: {}
decode-named-character-reference@1.3.0: decode-named-character-reference@1.3.0:
dependencies: dependencies:
character-entities: 2.0.2 character-entities: 2.0.2
...@@ -1918,6 +2368,8 @@ snapshots: ...@@ -1918,6 +2368,8 @@ snapshots:
fast-deep-equal@3.1.3: {} fast-deep-equal@3.1.3: {}
fast-equals@5.4.0: {}
fast-json-stable-stringify@2.1.0: {} fast-json-stable-stringify@2.1.0: {}
fast-levenshtein@2.0.6: {} fast-levenshtein@2.0.6: {}
...@@ -2027,8 +2479,6 @@ snapshots: ...@@ -2027,8 +2479,6 @@ snapshots:
isexe@2.0.0: {} isexe@2.0.0: {}
jquery@1.12.4: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
js-yaml@4.1.1: js-yaml@4.1.1:
...@@ -2045,6 +2495,10 @@ snapshots: ...@@ -2045,6 +2495,10 @@ snapshots:
json5@2.2.3: {} json5@2.2.3: {}
katex@0.17.0:
dependencies:
commander: 8.3.0
keyv@4.5.4: keyv@4.5.4:
dependencies: dependencies:
json-buffer: 3.0.1 json-buffer: 3.0.1
...@@ -2103,6 +2557,8 @@ snapshots: ...@@ -2103,6 +2557,8 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-arm64-msvc: 1.30.2
lightningcss-win32-x64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2
linkifyjs@4.3.3: {}
locate-path@6.0.0: locate-path@6.0.0:
dependencies: dependencies:
p-locate: 5.0.0 p-locate: 5.0.0
...@@ -2115,9 +2571,9 @@ snapshots: ...@@ -2115,9 +2571,9 @@ snapshots:
dependencies: dependencies:
yallist: 3.1.1 yallist: 3.1.1
mathquill@0.10.1-a: mathlive@0.110.0:
dependencies: dependencies:
jquery: 1.12.4 '@cortex-js/compute-engine': 0.58.0
mdast-util-from-markdown@2.0.2: mdast-util-from-markdown@2.0.2:
dependencies: dependencies:
...@@ -2366,6 +2822,8 @@ snapshots: ...@@ -2366,6 +2822,8 @@ snapshots:
type-check: 0.4.0 type-check: 0.4.0
word-wrap: 1.2.5 word-wrap: 1.2.5
orderedmap@2.1.1: {}
p-limit@3.1.0: p-limit@3.1.0:
dependencies: dependencies:
yocto-queue: 0.1.0 yocto-queue: 0.1.0
...@@ -2406,6 +2864,80 @@ snapshots: ...@@ -2406,6 +2864,80 @@ snapshots:
property-information@7.1.0: {} property-information@7.1.0: {}
prosemirror-changeset@2.4.1:
dependencies:
prosemirror-transform: 1.12.0
prosemirror-commands@1.7.1:
dependencies:
prosemirror-model: 1.25.9
prosemirror-state: 1.4.4
prosemirror-transform: 1.12.0
prosemirror-dropcursor@1.8.2:
dependencies:
prosemirror-state: 1.4.4
prosemirror-transform: 1.12.0
prosemirror-view: 1.41.9
prosemirror-gapcursor@1.4.1:
dependencies:
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.9
prosemirror-state: 1.4.4
prosemirror-view: 1.41.9
prosemirror-history@1.5.0:
dependencies:
prosemirror-state: 1.4.4
prosemirror-transform: 1.12.0
prosemirror-view: 1.41.9
rope-sequence: 1.3.4
prosemirror-inputrules@1.5.1:
dependencies:
prosemirror-state: 1.4.4
prosemirror-transform: 1.12.0
prosemirror-keymap@1.2.3:
dependencies:
prosemirror-state: 1.4.4
w3c-keyname: 2.2.8
prosemirror-model@1.25.9:
dependencies:
orderedmap: 2.1.1
prosemirror-schema-list@1.5.1:
dependencies:
prosemirror-model: 1.25.9
prosemirror-state: 1.4.4
prosemirror-transform: 1.12.0
prosemirror-state@1.4.4:
dependencies:
prosemirror-model: 1.25.9
prosemirror-transform: 1.12.0
prosemirror-view: 1.41.9
prosemirror-tables@1.8.5:
dependencies:
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.9
prosemirror-state: 1.4.4
prosemirror-transform: 1.12.0
prosemirror-view: 1.41.9
prosemirror-transform@1.12.0:
dependencies:
prosemirror-model: 1.25.9
prosemirror-view@1.41.9:
dependencies:
prosemirror-model: 1.25.9
prosemirror-state: 1.4.4
prosemirror-transform: 1.12.0
punycode@2.3.1: {} punycode@2.3.1: {}
react-dom@19.2.3(react@19.2.3): react-dom@19.2.3(react@19.2.3):
...@@ -2501,6 +3033,8 @@ snapshots: ...@@ -2501,6 +3033,8 @@ snapshots:
'@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.50 '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.50
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.50 '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.50
rope-sequence@1.3.4: {}
scheduler@0.27.0: {} scheduler@0.27.0: {}
semver@6.3.1: {} semver@6.3.1: {}
...@@ -2616,6 +3150,10 @@ snapshots: ...@@ -2616,6 +3150,10 @@ snapshots:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
use-sync-external-store@1.6.0(react@19.2.3):
dependencies:
react: 19.2.3
vfile-message@4.0.3: vfile-message@4.0.3:
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
...@@ -2626,6 +3164,8 @@ snapshots: ...@@ -2626,6 +3164,8 @@ snapshots:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
vfile-message: 4.0.3 vfile-message: 4.0.3
w3c-keyname@2.2.8: {}
which@2.0.2: which@2.0.2:
dependencies: dependencies:
isexe: 2.0.0 isexe: 2.0.0
......
...@@ -15,6 +15,8 @@ type ChatWindowProps = { ...@@ -15,6 +15,8 @@ type ChatWindowProps = {
onHistoryNavigate?: (direction: "older" | "newer") => boolean; onHistoryNavigate?: (direction: "older" | "newer") => boolean;
onToggleCanvas?: () => void; onToggleCanvas?: () => void;
onUploadSolution?: (file: File) => void | Promise<void>; onUploadSolution?: (file: File) => void | Promise<void>;
pendingFormulaLatex?: string | null;
onPendingFormulaHandled?: () => void;
onInspectDoc?: (doc: RetrievedDoc) => void; onInspectDoc?: (doc: RetrievedDoc) => void;
docIndex?: Record<string, RetrievedDoc>; docIndex?: Record<string, RetrievedDoc>;
docSlugIndex?: Record<string, RetrievedDoc>; docSlugIndex?: Record<string, RetrievedDoc>;
...@@ -31,6 +33,8 @@ export default function ChatWindow({ ...@@ -31,6 +33,8 @@ export default function ChatWindow({
onHistoryNavigate, onHistoryNavigate,
onToggleCanvas, onToggleCanvas,
onUploadSolution, onUploadSolution,
pendingFormulaLatex,
onPendingFormulaHandled,
onInspectDoc, onInspectDoc,
docIndex, docIndex,
docSlugIndex, docSlugIndex,
...@@ -54,6 +58,8 @@ export default function ChatWindow({ ...@@ -54,6 +58,8 @@ export default function ChatWindow({
onHistoryNavigate={onHistoryNavigate} onHistoryNavigate={onHistoryNavigate}
onToggleCanvas={onToggleCanvas} onToggleCanvas={onToggleCanvas}
onUploadSolution={onUploadSolution} onUploadSolution={onUploadSolution}
pendingFormulaLatex={pendingFormulaLatex}
onPendingFormulaHandled={onPendingFormulaHandled}
/> />
</div> </div>
); );
......
import { useEffect, useLayoutEffect, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef } from "react";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import { Node, mergeAttributes, type Editor, type JSONContent } from "@tiptap/core";
import { EditorContent, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import katex from "katex";
import "katex/dist/katex.min.css";
import "mathlive";
import { EditPencil, Send, Upload } from "iconoir-react"; import { EditPencil, Send, Upload } from "iconoir-react";
import { t } from "../../i18n"; import { t } from "../../i18n";
import "mathquill/build/mathquill.css";
type MessageInputProps = { type MessageInputProps = {
value: string; value: string;
...@@ -13,238 +19,186 @@ type MessageInputProps = { ...@@ -13,238 +19,186 @@ type MessageInputProps = {
onHistoryNavigate?: (direction: "older" | "newer") => boolean; onHistoryNavigate?: (direction: "older" | "newer") => boolean;
onToggleCanvas?: () => void; onToggleCanvas?: () => void;
onUploadSolution?: (file: File) => void | Promise<void>; onUploadSolution?: (file: File) => void | Promise<void>;
pendingFormulaLatex?: string | null;
onPendingFormulaHandled?: () => void;
}; };
type MathFieldLike = { type MathFieldElement = HTMLElement & {
el: () => HTMLElement;
latex: {
(): string;
(value: string): void;
};
focus: () => void;
revert: () => void;
};
type MathQuillInterface = {
MathField: (
element: HTMLElement,
config: {
handlers?: {
edit?: (mathField: MathFieldLike) => void;
enter?: (mathField: MathFieldLike) => void;
};
}
) => MathFieldLike;
StaticMath: (element: HTMLElement) => unknown;
};
type MathQuillGlobal = {
getInterface: (version: number) => MathQuillInterface;
};
type TextPart = {
type: "text";
value: string; value: string;
focus: () => void;
}; };
type MathPart = { type MathFieldProps = DetailedHTMLProps<HTMLAttributes<MathFieldElement>, MathFieldElement>;
type: "math";
latex: string;
};
type Part = TextPart | MathPart;
type CaretPosition = {
index: number;
offset: number;
};
declare global { declare module "@tiptap/core" {
interface Window { interface Commands<ReturnType> {
$?: unknown; math: {
jQuery?: unknown; insertMath: (latex?: string) => ReturnType;
MathQuill?: MathQuillGlobal; };
} }
} }
const MATH_SEGMENT_PATTERN = /(\$[^$\n]+\$)/g; declare module "react" {
namespace JSX {
let mathQuillLoader: Promise<MathQuillInterface> | null = null; interface IntrinsicElements {
"math-field": MathFieldProps;
const emptyParts = (): Part[] => [{ type: "text", value: "" }]; }
const loadMathQuill = async (): Promise<MathQuillInterface> => {
if (!mathQuillLoader) {
mathQuillLoader = (async () => {
const jqueryModule = await import("jquery");
const jquery = jqueryModule.default ?? jqueryModule;
window.$ = jquery;
window.jQuery = jquery;
await import("mathquill/build/mathquill.js");
const mathQuill = window.MathQuill;
if (!mathQuill) {
throw new Error("MathQuill failed to load");
}
return mathQuill.getInterface(2);
})();
} }
}
return mathQuillLoader; const stripMathDelimiters = (value: string) => {
}; const trimmed = value.trim();
if (!trimmed) {
const stringifyParts = (parts: Part[]) => return "";
parts
.map((part) => (part.type === "math" ? `$${part.latex}$` : part.value))
.join("");
const normalizeParts = (parts: Part[]): Part[] => {
if (!parts.length) {
return emptyParts();
} }
const normalized: Part[] = []; if (trimmed.startsWith("$$") && trimmed.endsWith("$$")) {
return trimmed.slice(2, -2).trim();
const pushText = (value: string) => {
const last = normalized[normalized.length - 1];
if (last?.type === "text") {
last.value += value;
return;
}
normalized.push({ type: "text", value });
};
parts.forEach((part, partIndex) => {
if (part.type === "text") {
pushText(part.value);
return;
}
const previous = normalized[normalized.length - 1];
if (!previous || previous.type !== "text") {
normalized.push({ type: "text", value: "" });
}
normalized.push(part);
const nextInput = parts[partIndex + 1];
if (!nextInput || nextInput.type !== "text") {
normalized.push({ type: "text", value: "" });
}
});
if (!normalized.length) {
return emptyParts();
} }
if (trimmed.startsWith("$") && trimmed.endsWith("$")) {
if (normalized[0].type !== "text") { return trimmed.slice(1, -1).trim();
normalized.unshift({ type: "text", value: "" });
} }
if (normalized[normalized.length - 1].type !== "text") { if (trimmed.startsWith("\\(") && trimmed.endsWith("\\)")) {
normalized.push({ type: "text", value: "" }); return trimmed.slice(2, -2).trim();
} }
if (trimmed.startsWith("\\[") && trimmed.endsWith("\\]")) {
return normalized; return trimmed.slice(2, -2).trim();
};
const partsFromValue = (value: string): Part[] => {
const parts: Part[] = [];
let lastIndex = 0;
for (const match of value.matchAll(MATH_SEGMENT_PATTERN)) {
const fullMatch = match[0];
const index = match.index ?? 0;
parts.push({ type: "text", value: value.slice(lastIndex, index) });
parts.push({ type: "math", latex: fullMatch.slice(1, -1) });
lastIndex = index + fullMatch.length;
} }
parts.push({ type: "text", value: value.slice(lastIndex) }); return trimmed;
return normalizeParts(parts);
}; };
const getSelectionOffsets = (element: HTMLElement) => { const renderLatex = (element: HTMLElement, latex: string) => {
const selection = window.getSelection(); try {
if (!selection || selection.rangeCount === 0 || !element.contains(selection.anchorNode)) { katex.render(latex || "\\square", element, { throwOnError: false });
const length = element.textContent?.length ?? 0; } catch {
return { start: length, end: length }; element.textContent = latex || "\\square";
} }
const range = selection.getRangeAt(0);
const startRange = range.cloneRange();
startRange.selectNodeContents(element);
startRange.setEnd(range.startContainer, range.startOffset);
const endRange = range.cloneRange();
endRange.selectNodeContents(element);
endRange.setEnd(range.endContainer, range.endOffset);
return {
start: startRange.toString().length,
end: endRange.toString().length,
};
}; };
const setCaretOffset = (element: HTMLElement, offset: number) => { const markdownToDoc = (value: string): JSONContent => ({
const selection = window.getSelection(); type: "doc",
if (!selection) { content: [
return; {
} type: "paragraph",
content:
const range = document.createRange(); value
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT); .split(/(\$[^$\n]+\$)/g)
let remaining = offset; .filter(Boolean)
let currentNode = walker.nextNode(); .map((part) =>
part.startsWith("$") && part.endsWith("$")
while (currentNode) { ? { type: "math", attrs: { latex: part.slice(1, -1) } }
const textLength = currentNode.textContent?.length ?? 0; : { type: "text", text: part }
if (remaining <= textLength) { ) || [],
range.setStart(currentNode, remaining); },
range.collapse(true); ],
selection.removeAllRanges(); });
selection.addRange(range);
const docToMarkdown = (doc: JSONContent) => {
const parts: string[] = [];
const walk = (node?: JSONContent) => {
if (!node) {
return; return;
} }
remaining -= textLength; if (node.type === "text") {
currentNode = walker.nextNode(); parts.push(node.text || "");
} return;
range.selectNodeContents(element);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
};
const findNearestTextIndex = (parts: Part[], start: number, direction: -1 | 1) => {
let index = start;
while (index >= 0 && index < parts.length) {
if (parts[index]?.type === "text") {
return index;
} }
index += direction; if (node.type === "math") {
} parts.push(`$${node.attrs?.latex || ""}$`);
return direction > 0 ? parts.length - 1 : 0; return;
}; }
node.content?.forEach(walk);
const clampCaret = (parts: Part[], caret: CaretPosition): CaretPosition => {
const index = findNearestTextIndex(parts, caret.index, caret.index >= parts.length ? -1 : 1);
const part = parts[index];
const maxOffset = part?.type === "text" ? part.value.length : 0;
return {
index,
offset: Math.max(0, Math.min(caret.offset, maxOffset)),
}; };
};
const isCaretAtStart = (parts: Part[], index: number, element: HTMLElement) => { walk(doc);
const { start, end } = getSelectionOffsets(element); return parts.join("");
if (start !== 0 || end !== 0) {
return false;
}
return stringifyParts(parts.slice(0, index)).length === 0;
}; };
const createMathNode = (
openMathEditor: (latex: string, onSave: (latex: string) => void) => void
) =>
Node.create({
name: "math",
group: "inline",
inline: true,
atom: true,
selectable: true,
addAttributes() {
return {
latex: { default: "" },
};
},
parseHTML() {
return [{ tag: "span[data-math]" }];
},
renderHTML({ HTMLAttributes }) {
return ["span", mergeAttributes(HTMLAttributes, { "data-math": "" })];
},
addCommands() {
return {
insertMath:
(latex = "") =>
({ commands }) =>
commands.insertContent({ type: this.name, attrs: { latex } }),
};
},
addNodeView() {
return ({ node, editor, getPos }) => {
const element = document.createElement("span");
element.className = "math-chip";
element.tabIndex = 0;
element.dataset.math = "";
const edit = () => {
openMathEditor(node.attrs.latex, (latex) => {
editor
.chain()
.focus()
.command(({ tr }) => {
const pos = getPos();
if (typeof pos !== "number") {
return false;
}
tr.setNodeMarkup(pos, undefined, { latex });
return true;
})
.run();
});
};
element.addEventListener("click", edit);
element.addEventListener("keydown", (event) => {
if (event.key !== "Enter" && event.key !== " ") {
return;
}
event.preventDefault();
edit();
});
renderLatex(element, node.attrs.latex);
return {
dom: element,
update(updatedNode) {
if (updatedNode.type.name !== node.type.name) {
return false;
}
node = updatedNode;
renderLatex(element, node.attrs.latex);
return true;
},
};
};
},
});
export default function MessageInput({ export default function MessageInput({
value, value,
onChange, onChange,
...@@ -255,240 +209,108 @@ export default function MessageInput({ ...@@ -255,240 +209,108 @@ export default function MessageInput({
onHistoryNavigate, onHistoryNavigate,
onToggleCanvas, onToggleCanvas,
onUploadSolution, onUploadSolution,
pendingFormulaLatex,
onPendingFormulaHandled,
}: MessageInputProps) { }: MessageInputProps) {
const uploadInputRef = useRef<HTMLInputElement | null>(null); const uploadInputRef = useRef<HTMLInputElement | null>(null);
const editorRef = useRef<HTMLDivElement | null>(null); const dialogRef = useRef<HTMLDialogElement | null>(null);
const textPartRefs = useRef(new Map<number, HTMLSpanElement>()); const mathFieldRef = useRef<MathFieldElement | null>(null);
const staticMathRefs = useRef(new Map<number, HTMLSpanElement>()); const saveMathRef = useRef<((latex: string) => void) | null>(null);
const activeMathHostRef = useRef<HTMLSpanElement | null>(null); const editorRef = useRef<Editor | null>(null);
const activeFieldRef = useRef<MathFieldLike | null>(null);
const mqRef = useRef<MathQuillInterface | null>(null); const openMathEditor = useCallback((latex: string, onSave: (latex: string) => void) => {
const wasSendingRef = useRef(isSending); saveMathRef.current = onSave;
const shouldRestoreFocusRef = useRef(false); if (mathFieldRef.current) {
const sendButtonStartedFromEditorRef = useRef(false); mathFieldRef.current.value = stripMathDelimiters(latex);
const pendingCaretRef = useRef<CaretPosition | null>(null);
const selectionRef = useRef<CaretPosition>({ index: 0, offset: 0 });
const activeMathIndexRef = useRef<number | null>(null);
const partsRef = useRef<Part[]>(partsFromValue(value));
const [parts, setParts] = useState<Part[]>(partsRef.current);
const [activeMathIndex, setActiveMathIndex] = useState<number | null>(null);
const message = stringifyParts(parts);
const canSend = message.trim().length > 0;
const commitParts = (
nextPartsInput: Part[],
options: {
emit?: boolean;
caret?: CaretPosition | null;
activeMath?: number | null;
} = {}
) => {
const previousMessage = stringifyParts(partsRef.current);
const nextParts = normalizeParts(nextPartsInput);
const nextMessage = stringifyParts(nextParts);
partsRef.current = nextParts;
setParts(nextParts);
if (options.caret) {
pendingCaretRef.current = clampCaret(nextParts, options.caret);
}
if (options.activeMath !== undefined) {
activeMathIndexRef.current = options.activeMath;
setActiveMathIndex(options.activeMath);
}
if (options.emit !== false && nextMessage !== previousMessage) {
onChange(nextMessage);
}
};
const finalizeActiveMathField = () => {
const field = activeFieldRef.current;
const index = activeMathIndexRef.current;
if (!field || index === null) {
return;
} }
dialogRef.current?.showModal();
const latex = field.latex(); requestAnimationFrame(() => mathFieldRef.current?.focus());
const nextParts = [...partsRef.current]; }, []);
nextParts[index] = latex ? { type: "math", latex } : { type: "text", value: "" };
const MathNode = useMemo(() => createMathNode(openMathEditor), [openMathEditor]);
const caret = latex ? { index: index + 1, offset: 0 } : { index, offset: 0 };
activeFieldRef.current = null; const editor = useEditor({
commitParts(nextParts, { caret, activeMath: null }); extensions: [StarterKit.configure({ heading: false }), MathNode],
}; content: markdownToDoc(value),
editable: !isSending,
useEffect(() => { immediatelyRender: false,
activeMathIndexRef.current = activeMathIndex; editorProps: {
}, [activeMathIndex]); attributes: {
class: "tiptap composer-editor",
useEffect(() => { "aria-label": t("typeQuestionOrLatex"),
partsRef.current = parts; },
}, [parts]); handleKeyDown(_view, event) {
const currentEditor = editorRef.current;
useEffect(() => {
const editor = editorRef.current; if (event.key === "$") {
if (!editor) { event.preventDefault();
return; openMathEditor("", (latex) => {
} editorRef.current?.chain().focus().insertMath(stripMathDelimiters(latex)).run();
});
if (value === stringifyParts(partsRef.current)) { return true;
return;
}
if (activeMathIndexRef.current !== null || editor.contains(document.activeElement)) {
return;
}
const nextParts = partsFromValue(value);
partsRef.current = nextParts;
setParts(nextParts);
}, [value]);
useEffect(() => {
let cancelled = false;
const renderStaticMath = async () => {
const MQ = mqRef.current ?? (await loadMathQuill());
if (cancelled) {
return;
}
mqRef.current = MQ;
parts.forEach((part, index) => {
if (part.type !== "math" || index === activeMathIndex) {
return;
} }
const node = staticMathRefs.current.get(index); if (event.key === "Enter" && !event.shiftKey) {
if (!node || node.dataset.renderedLatex === part.latex) { event.preventDefault();
return; const message = currentEditor ? docToMarkdown(currentEditor.getJSON()).trim() : "";
if (message) {
onSend(message);
}
return true;
} }
node.dataset.latex = part.latex; if (event.key === "ArrowUp" && currentEditor?.state.selection.from === 1) {
node.dataset.renderedLatex = part.latex; return onHistoryNavigate?.("older") ?? false;
node.textContent = part.latex || "\\square"; }
MQ.StaticMath(node);
});
};
void renderStaticMath();
return () => {
cancelled = true;
};
}, [parts, activeMathIndex]);
useEffect(() => {
if (activeMathIndex === null) {
return;
}
const host = activeMathHostRef.current;
if (!host) {
return;
}
let disposed = false;
let field: MathFieldLike | null = null;
const handleFocusOut = () => { if (
requestAnimationFrame(() => { event.key === "ArrowDown" &&
if (host.contains(document.activeElement)) { currentEditor &&
return; currentEditor.state.selection.to === currentEditor.state.doc.content.size
) {
return onHistoryNavigate?.("newer") ?? false;
} }
finalizeActiveMathField();
});
};
const handleKeyDown = (event: KeyboardEvent) => { return false;
if (event.key !== "Enter" && event.key !== "$") { },
return; },
} onUpdate({ editor }) {
event.preventDefault(); onChange(docToMarkdown(editor.getJSON()));
event.stopPropagation(); },
finalizeActiveMathField(); });
};
void (async () => { editorRef.current = editor;
const MQ = mqRef.current ?? (await loadMathQuill());
if (disposed) {
return;
}
mqRef.current = MQ;
field = MQ.MathField(host, {
handlers: {
edit: (mathField) => {
const nextLatex = mathField.latex();
mathField.el().dataset.empty = nextLatex ? "false" : "true";
mathField.el().dataset.latex = nextLatex;
},
enter: () => {
finalizeActiveMathField();
},
},
});
activeFieldRef.current = field;
const part = partsRef.current[activeMathIndex];
const latex = part?.type === "math" ? part.latex : "";
field.latex(latex);
host.dataset.empty = latex ? "false" : "true";
host.dataset.latex = latex;
host.addEventListener("focusout", handleFocusOut);
host.addEventListener("keydown", handleKeyDown);
requestAnimationFrame(() => field?.focus());
})();
return () => {
disposed = true;
activeFieldRef.current = null;
if (field) {
host.removeEventListener("focusout", handleFocusOut);
host.removeEventListener("keydown", handleKeyDown);
field.revert();
}
};
}, [activeMathIndex]);
useLayoutEffect(() => { const handleSend = useCallback(() => {
if (activeMathIndex !== null) { const currentEditor = editorRef.current;
if (!currentEditor || isSending) {
return; return;
} }
const pendingCaret = pendingCaretRef.current; const message = docToMarkdown(currentEditor.getJSON()).trim();
if (!pendingCaret) { if (message) {
return; onSend(message);
} }
}, [isSending, onSend]);
useEffect(() => {
editor?.setEditable(!isSending);
}, [editor, isSending]);
const target = textPartRefs.current.get(pendingCaret.index); useEffect(() => {
if (!target) { if (!editor || value === docToMarkdown(editor.getJSON())) {
return; return;
} }
editor.commands.setContent(markdownToDoc(value), { emitUpdate: false });
target.focus(); }, [editor, value]);
setCaretOffset(target, pendingCaret.offset);
selectionRef.current = pendingCaret;
pendingCaretRef.current = null;
}, [parts, activeMathIndex]);
useEffect(() => { useEffect(() => {
if (wasSendingRef.current && !isSending) { if (!editor || !pendingFormulaLatex) {
if (shouldRestoreFocusRef.current) { return;
const target = textPartRefs.current.get(selectionRef.current.index);
target?.focus({ preventScroll: true });
}
shouldRestoreFocusRef.current = false;
} }
editor.chain().focus().insertMath(stripMathDelimiters(pendingFormulaLatex)).run();
wasSendingRef.current = isSending; onPendingFormulaHandled?.();
}, [isSending]); }, [editor, pendingFormulaLatex, onPendingFormulaHandled]);
const handleUploadClick = () => { const handleUploadClick = () => {
uploadInputRef.current?.click(); uploadInputRef.current?.click();
...@@ -497,87 +319,14 @@ export default function MessageInput({ ...@@ -497,87 +319,14 @@ export default function MessageInput({
const handleUploadChange = async (event: React.ChangeEvent<HTMLInputElement>) => { const handleUploadChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]; const file = event.target.files?.[0];
event.target.value = ""; event.target.value = "";
if (!file) { if (file) {
return; await onUploadSolution?.(file);
} }
await onUploadSolution?.(file);
};
const updateSelection = (index: number, element: HTMLElement) => {
const { end } = getSelectionOffsets(element);
selectionRef.current = { index, offset: end };
}; };
const handleTextInput = (index: number, element: HTMLElement) => { const insertMath = () => {
const nextParts = [...partsRef.current]; openMathEditor("", (latex) => {
const current = nextParts[index]; editor?.chain().focus().insertMath(stripMathDelimiters(latex)).run();
if (!current || current.type !== "text") {
return;
}
const { end } = getSelectionOffsets(element);
current.value = element.textContent ?? "";
commitParts(nextParts, { caret: { index, offset: end } });
};
const replaceTextSelection = (index: number, text: string) => {
const element = textPartRefs.current.get(index);
const current = partsRef.current[index];
if (!element || !current || current.type !== "text") {
return;
}
const { start, end } = getSelectionOffsets(element);
const nextValue = `${current.value.slice(0, start)}${text}${current.value.slice(end)}`;
const nextParts = [...partsRef.current];
nextParts[index] = { type: "text", value: nextValue };
commitParts(nextParts, { caret: { index, offset: start + text.length } });
};
const insertMathAtSelection = (index?: number) => {
const fallbackIndex = findNearestTextIndex(partsRef.current, partsRef.current.length - 1, -1);
const targetIndex = index ?? selectionRef.current.index ?? fallbackIndex;
const element = textPartRefs.current.get(targetIndex);
const current = partsRef.current[targetIndex];
if (!current || current.type !== "text") {
return;
}
const selection = element ? getSelectionOffsets(element) : { start: current.value.length, end: current.value.length };
const before = current.value.slice(0, selection.start);
const after = current.value.slice(selection.end);
const nextParts = [
...partsRef.current.slice(0, targetIndex),
{ type: "text", value: before } as TextPart,
{ type: "math", latex: "" } as MathPart,
{ type: "text", value: after } as TextPart,
...partsRef.current.slice(targetIndex + 1),
];
commitParts(nextParts, { activeMath: targetIndex + 1, emit: false });
};
const activateMathPart = (index: number) => {
if (isSending) {
return;
}
commitParts(partsRef.current, { activeMath: index, emit: false });
};
const handleSend = () => {
finalizeActiveMathField();
const nextMessage = stringifyParts(partsRef.current);
if (!nextMessage.trim()) {
return;
}
shouldRestoreFocusRef.current = editorRef.current?.contains(document.activeElement) ?? false;
onSend(nextMessage);
commitParts(emptyParts(), {
caret: { index: 0, offset: 0 },
emit: true,
activeMath: null,
}); });
}; };
...@@ -635,31 +384,21 @@ export default function MessageInput({ ...@@ -635,31 +384,21 @@ export default function MessageInput({
{t("revealSolution")} {t("revealSolution")}
</button> </button>
) : null} ) : null}
{activeMathIndex === null ? ( <button
<button className="btn btn-formula"
className="btn btn-formula" type="button"
type="button" onClick={insertMath}
onClick={() => { disabled={isSending}
insertMathAtSelection(); aria-label={t("formula")}
}} title={t("formula")}
disabled={isSending} >
aria-label={t("formula")} f(x)
title={t("formula")} </button>
>
f (x)
</button>
) : null}
<button <button
className="btn primary" className="btn primary"
type="button" type="button"
onPointerDown={() => { onClick={handleSend}
sendButtonStartedFromEditorRef.current = editorRef.current?.contains(document.activeElement) ?? false; disabled={!value.trim() || isSending}
}}
onClick={() => {
handleSend();
sendButtonStartedFromEditorRef.current = false;
}}
disabled={!canSend || isSending}
aria-label={t("send")} aria-label={t("send")}
title={t("send")} title={t("send")}
> >
...@@ -674,128 +413,32 @@ export default function MessageInput({ ...@@ -674,128 +413,32 @@ export default function MessageInput({
)} )}
</button> </button>
</div> </div>
<div
ref={editorRef}
className={`composer-input composer-rich-input${isSending ? " composer-input-disabled" : ""}`}
data-placeholder={t("typeQuestionOrLatex")}
data-empty={message.length === 0 ? "true" : "false"}
aria-label={t("typeQuestionOrLatex")}
role="textbox"
aria-multiline="true"
>
{parts.map((part, index) => {
if (part.type === "text") {
return (
<span
key={`text-${index}`}
ref={(node) => {
if (node) {
textPartRefs.current.set(index, node);
return;
}
textPartRefs.current.delete(index);
}}
className="composer-text-part"
contentEditable={!isSending && activeMathIndex === null}
suppressContentEditableWarning
data-part-index={index}
spellCheck={false}
onFocus={(event) => {
updateSelection(index, event.currentTarget);
}}
onMouseUp={(event) => {
updateSelection(index, event.currentTarget);
}}
onKeyUp={(event) => {
updateSelection(index, event.currentTarget);
}}
onInput={(event) => {
handleTextInput(index, event.currentTarget);
}}
onKeyDown={(event) => {
if (isSending) {
event.preventDefault();
return;
}
if (event.key === "$") {
event.preventDefault();
insertMathAtSelection(index);
return;
}
if (event.key === "ArrowUp" && isCaretAtStart(partsRef.current, index, event.currentTarget)) {
if (onHistoryNavigate?.("older")) {
event.preventDefault();
return;
}
}
if (event.key === "ArrowDown" && isCaretAtStart(partsRef.current, index, event.currentTarget)) {
if (onHistoryNavigate?.("newer")) {
event.preventDefault();
return;
}
}
if (event.key === "Enter" && event.shiftKey) {
event.preventDefault();
replaceTextSelection(index, "\n");
return;
}
if (event.key === "Enter") {
event.preventDefault();
handleSend();
}
}}
>
{part.value}
</span>
);
}
if (index === activeMathIndex) { <EditorContent editor={editor} />
return (
<span
key={`math-active-${index}`}
ref={activeMathHostRef}
className="inline-math"
data-empty={part.latex ? "false" : "true"}
data-latex={part.latex}
contentEditable={false}
/>
);
}
return ( <dialog
<span ref={dialogRef}
key={`math-${index}`} className="math-dialog"
ref={(node) => { onClose={(event) => {
if (node) { const dialog = event.currentTarget;
staticMathRefs.current.set(index, node); if (dialog.returnValue === "save") {
return; saveMathRef.current?.(mathFieldRef.current?.value || "");
} }
staticMathRefs.current.delete(index); saveMathRef.current = null;
}} }}
className="inline-math-render" >
data-latex={part.latex} <form method="dialog">
contentEditable={false} <math-field ref={mathFieldRef} />
tabIndex={isSending ? -1 : 0} <div className="dialog-actions">
onClick={() => { <button className="btn" value="cancel" type="submit">
activateMathPart(index); {t("hide")}
}} </button>
onKeyDown={(event) => { <button className="btn primary" value="save" type="submit">
if (event.key !== "Enter" && event.key !== " ") { {t("formula")}
return; </button>
} </div>
event.preventDefault(); </form>
activateMathPart(index); </dialog>
}}
/>
);
})}
</div>
</div> </div>
); );
} }
...@@ -94,6 +94,28 @@ const sourceIdToPath = (source: ContextSource) => { ...@@ -94,6 +94,28 @@ const sourceIdToPath = (source: ContextSource) => {
return parts.length ? parts.join(" / ") : null; return parts.length ? parts.join(" / ") : null;
}; };
const stripMathDelimiters = (value: string) => {
const trimmed = value.trim();
if (!trimmed) {
return "";
}
if (trimmed.startsWith("$$") && trimmed.endsWith("$$")) {
return trimmed.slice(2, -2).trim();
}
if (trimmed.startsWith("$") && trimmed.endsWith("$")) {
return trimmed.slice(1, -1).trim();
}
if (trimmed.startsWith("\\(") && trimmed.endsWith("\\)")) {
return trimmed.slice(2, -2).trim();
}
if (trimmed.startsWith("\\[") && trimmed.endsWith("\\]")) {
return trimmed.slice(2, -2).trim();
}
return trimmed;
};
const toRetrievedDoc = (source: ContextSource, index: number): RetrievedDoc => ({ const toRetrievedDoc = (source: ContextSource, index: number): RetrievedDoc => ({
uid: sourceIdToUid(source, index), uid: sourceIdToUid(source, index),
sourceKey: sourceIdToKey(source.source_id), sourceKey: sourceIdToKey(source.source_id),
...@@ -216,6 +238,7 @@ export default function ChatPage() { ...@@ -216,6 +238,7 @@ export default function ChatPage() {
const [retrievalError, setRetrievalError] = useState<string | null>(null); const [retrievalError, setRetrievalError] = useState<string | null>(null);
const [deepLinkError, setDeepLinkError] = useState<string | null>(null); const [deepLinkError, setDeepLinkError] = useState<string | null>(null);
const [deepLinkRevision, setDeepLinkRevision] = useState(0); const [deepLinkRevision, setDeepLinkRevision] = useState(0);
const [pendingFormulaLatex, setPendingFormulaLatex] = useState<string | null>(null);
const [canvasStatus, setCanvasStatus] = useState<{ const [canvasStatus, setCanvasStatus] = useState<{
kind: "info" | "error" | "ok"; kind: "info" | "error" | "ok";
message: string; message: string;
...@@ -365,6 +388,7 @@ export default function ChatPage() { ...@@ -365,6 +388,7 @@ export default function ChatPage() {
setSections([]); setSections([]);
setRetrievalLoading(false); setRetrievalLoading(false);
setRetrievalError(null); setRetrievalError(null);
setPendingFormulaLatex(null);
setCanvasStatus(null); setCanvasStatus(null);
setIsCanvasVisible(false); setIsCanvasVisible(false);
...@@ -902,6 +926,7 @@ export default function ChatPage() { ...@@ -902,6 +926,7 @@ export default function ChatPage() {
setSections([]); setSections([]);
setRetrievalLoading(false); setRetrievalLoading(false);
setRetrievalError(null); setRetrievalError(null);
setPendingFormulaLatex(null);
setCanvasStatus(null); setCanvasStatus(null);
setIsCanvasVisible(false); setIsCanvasVisible(false);
}; };
...@@ -1150,7 +1175,7 @@ export default function ChatPage() { ...@@ -1150,7 +1175,7 @@ export default function ChatPage() {
} }
const latex = payload.latex; const latex = payload.latex;
if (typeof latex === "string" && latex.length > 0) { if (typeof latex === "string" && latex.length > 0) {
setDraft((prev) => (prev ? `${prev} ${latex}` : latex)); setPendingFormulaLatex(stripMathDelimiters(latex));
setCanvasStatus({ kind: "ok", message: t("canvasSaved") }); setCanvasStatus({ kind: "ok", message: t("canvasSaved") });
setIsCanvasVisible(false); setIsCanvasVisible(false);
} }
...@@ -1328,6 +1353,10 @@ export default function ChatPage() { ...@@ -1328,6 +1353,10 @@ export default function ChatPage() {
onHistoryNavigate={handleHistoryNavigate} onHistoryNavigate={handleHistoryNavigate}
onToggleCanvas={handleToggleCanvas} onToggleCanvas={handleToggleCanvas}
onUploadSolution={handleCanvasUpload} onUploadSolution={handleCanvasUpload}
pendingFormulaLatex={pendingFormulaLatex}
onPendingFormulaHandled={() => {
setPendingFormulaLatex(null);
}}
onInspectDoc={handleInspectDoc} onInspectDoc={handleInspectDoc}
docIndex={docIndexes.bySourceKey} docIndex={docIndexes.bySourceKey}
docSlugIndex={docIndexes.bySlug} docSlugIndex={docIndexes.bySlug}
......
...@@ -570,124 +570,80 @@ body { ...@@ -570,124 +570,80 @@ body {
animation-delay: 0.3s; animation-delay: 0.3s;
} }
.composer-input { .composer-editor {
min-height: 92px;
width: 100%; width: 100%;
border-radius: 12px; border-radius: 12px;
padding: 10px 12px; padding: 10px 12px;
border: 1px solid #d8d1c4; border: 1px solid #d8d1c4;
font-family: inherit;
background: #fff; background: #fff;
} font-family: inherit;
.composer-input:disabled {
background: #f7f3ec;
color: #8b857b;
cursor: not-allowed;
}
.composer-rich-input {
position: relative;
min-height: 92px;
line-height: 1.6; line-height: 1.6;
white-space: pre-wrap;
overflow-wrap: anywhere;
outline: none; outline: none;
cursor: text;
} }
.composer-rich-input[data-empty="true"]::before, .composer-editor p {
.composer-rich-input:empty::before { margin: 0;
content: attr(data-placeholder);
color: #8b857b;
pointer-events: none;
}
.composer-text-part {
display: inline;
outline: none;
}
.composer-text-part:empty::before {
content: "\200b";
} }
.composer-rich-input:focus { .composer-editor:focus {
border-color: #1d1b16; border-color: #1d1b16;
box-shadow: 0 0 0 2px rgba(29, 27, 22, 0.08); box-shadow: 0 0 0 2px rgba(29, 27, 22, 0.08);
} }
.composer-input-disabled { .composer-editor[contenteditable="false"] {
background: #f7f3ec; background: #f7f3ec;
color: #8b857b; color: #8b857b;
cursor: not-allowed; cursor: not-allowed;
} }
.btn-formula { .math-chip {
font-weight: 400;
}
.inline-math {
display: inline-block; display: inline-block;
min-width: 1.6em; min-width: 1.4em;
margin: 0 0.18em; margin: 0 0.12em;
padding: 4px 8px; padding: 1px 5px;
vertical-align: middle;
border: 1px solid #d8d1c4; border: 1px solid #d8d1c4;
border-radius: 10px; border-radius: 8px;
background: #fffaf0; background: #fffaf0;
font-size: 1rem; vertical-align: baseline;
} cursor: text;
.inline-math .mq-root-block {
min-width: 1em;
}
.inline-math[data-empty="true"] .mq-root-block::before {
content: "x";
opacity: 0.35;
} }
.inline-math:focus-within, .math-chip:focus {
.inline-math.mq-focused { outline: 2px solid rgba(29, 27, 22, 0.18);
border-color: #1d1b16; outline-offset: 2px;
box-shadow: 0 0 0 2px rgba(29, 27, 22, 0.08);
} }
.inline-math-render { .btn-formula {
display: inline-block; font-weight: 400;
min-width: 1em; white-space: nowrap;
margin: 0 0.18em;
padding: 2px 4px;
border-radius: 8px;
vertical-align: middle;
cursor: text;
} }
.inline-math-render .mq-root-block { .math-dialog {
display: inline-block; width: min(560px, calc(100vw - 32px));
border: 1px solid #d8d1c4;
border-radius: 12px;
padding: 16px;
} }
.inline-math-render:focus { .math-dialog::backdrop {
outline: 2px solid rgba(29, 27, 22, 0.18); background: rgba(29, 27, 22, 0.35);
outline-offset: 2px;
} }
/* ponytail: MathQuill's bundled Symbola font is malformed in current browsers. math-field {
Override the font face and stacks so the browser never fetches that asset. */ width: 100%;
@font-face { min-height: 72px;
font-family: "Symbola"; padding: 10px;
src: local("Times New Roman"); border: 1px solid #d8d1c4;
border-radius: 10px;
font-size: 1.4rem;
} }
.mq-math-mode var, .dialog-actions {
.mq-math-mode .mq-text-mode, display: flex;
.mq-math-mode .mq-nonSymbola, justify-content: flex-end;
.mq-math-mode .mq-font, gap: 8px;
.mq-math-mode .mq-sans-serif, margin-top: 12px;
.mq-math-mode .mq-monospace,
.mq-math-mode var.mq-operator-name,
.mq-math-mode .mq-binary-operator {
font-family: "Times New Roman", serif !important;
} }
.btn { .btn {
......
declare module "jquery";
declare module "mathquill/build/mathquill.js";
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment