mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
🔢 fix: Unescape LaTeX Numbers in Artifact Content Edit (#10476)
This commit is contained in:
parent
b8b1217c34
commit
3f62ce054f
4 changed files with 168 additions and 2 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const { unescapeLaTeX } = require('@librechat/api');
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { ContentTypes } = require('librechat-data-provider');
|
const { ContentTypes } = require('librechat-data-provider');
|
||||||
const {
|
const {
|
||||||
|
|
@ -134,17 +135,32 @@ router.post('/artifact/:messageId', async (req, res) => {
|
||||||
return res.status(400).json({ error: 'Artifact index out of bounds' });
|
return res.status(400).json({ error: 'Artifact index out of bounds' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unescape LaTeX preprocessing done by the frontend
|
||||||
|
// The frontend escapes $ signs for display, but the database has unescaped versions
|
||||||
|
const unescapedOriginal = unescapeLaTeX(original);
|
||||||
|
const unescapedUpdated = unescapeLaTeX(updated);
|
||||||
|
|
||||||
const targetArtifact = artifacts[index];
|
const targetArtifact = artifacts[index];
|
||||||
let updatedText = null;
|
let updatedText = null;
|
||||||
|
|
||||||
if (targetArtifact.source === 'content') {
|
if (targetArtifact.source === 'content') {
|
||||||
const part = message.content[targetArtifact.partIndex];
|
const part = message.content[targetArtifact.partIndex];
|
||||||
updatedText = replaceArtifactContent(part.text, targetArtifact, original, updated);
|
updatedText = replaceArtifactContent(
|
||||||
|
part.text,
|
||||||
|
targetArtifact,
|
||||||
|
unescapedOriginal,
|
||||||
|
unescapedUpdated,
|
||||||
|
);
|
||||||
if (updatedText) {
|
if (updatedText) {
|
||||||
part.text = updatedText;
|
part.text = updatedText;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updatedText = replaceArtifactContent(message.text, targetArtifact, original, updated);
|
updatedText = replaceArtifactContent(
|
||||||
|
message.text,
|
||||||
|
targetArtifact,
|
||||||
|
unescapedOriginal,
|
||||||
|
unescapedUpdated,
|
||||||
|
);
|
||||||
if (updatedText) {
|
if (updatedText) {
|
||||||
message.text = updatedText;
|
message.text = updatedText;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export * from './events';
|
||||||
export * from './files';
|
export * from './files';
|
||||||
export * from './generators';
|
export * from './generators';
|
||||||
export * from './key';
|
export * from './key';
|
||||||
|
export * from './latex';
|
||||||
export * from './llm';
|
export * from './llm';
|
||||||
export * from './math';
|
export * from './math';
|
||||||
export * from './openid';
|
export * from './openid';
|
||||||
|
|
|
||||||
122
packages/api/src/utils/latex.spec.ts
Normal file
122
packages/api/src/utils/latex.spec.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
import { unescapeLaTeX } from './latex';
|
||||||
|
|
||||||
|
describe('unescapeLaTeX', () => {
|
||||||
|
describe('currency dollar signs', () => {
|
||||||
|
it('should unescape single backslash dollar signs', () => {
|
||||||
|
const input = 'Price: \\$14';
|
||||||
|
const expected = 'Price: $14';
|
||||||
|
expect(unescapeLaTeX(input)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should unescape double backslash dollar signs', () => {
|
||||||
|
const input = 'Price: \\\\$14';
|
||||||
|
const expected = 'Price: $14';
|
||||||
|
expect(unescapeLaTeX(input)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should unescape multiple currency values', () => {
|
||||||
|
const input = '**Crispy Calamari** - *\\\\$14*\n**Truffle Fries** - *\\\\$12*';
|
||||||
|
const expected = '**Crispy Calamari** - *$14*\n**Truffle Fries** - *$12*';
|
||||||
|
expect(unescapeLaTeX(input)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle currency with commas and decimals', () => {
|
||||||
|
const input = 'Total: \\\\$1,234.56';
|
||||||
|
const expected = 'Total: $1,234.56';
|
||||||
|
expect(unescapeLaTeX(input)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mhchem notation', () => {
|
||||||
|
it('should unescape mhchem ce notation', () => {
|
||||||
|
const input = '$$\\\\ce{H2O}$$';
|
||||||
|
const expected = '$\\ce{H2O}$';
|
||||||
|
expect(unescapeLaTeX(input)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should unescape mhchem pu notation', () => {
|
||||||
|
const input = '$$\\\\pu{123 kJ/mol}$$';
|
||||||
|
const expected = '$\\pu{123 kJ/mol}$';
|
||||||
|
expect(unescapeLaTeX(input)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple mhchem expressions', () => {
|
||||||
|
const input = '$$\\\\ce{H2O}$$ and $$\\\\ce{CO2}$$';
|
||||||
|
const expected = '$\\ce{H2O}$ and $\\ce{CO2}$';
|
||||||
|
expect(unescapeLaTeX(input)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it('should handle empty string', () => {
|
||||||
|
expect(unescapeLaTeX('')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null', () => {
|
||||||
|
expect(unescapeLaTeX(null)).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle undefined', () => {
|
||||||
|
expect(unescapeLaTeX(undefined)).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle string with no dollar signs', () => {
|
||||||
|
const input = 'Hello world';
|
||||||
|
expect(unescapeLaTeX(input)).toBe(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed escaped and unescaped content', () => {
|
||||||
|
const input = 'Price \\\\$14 and some text';
|
||||||
|
const expected = 'Price $14 and some text';
|
||||||
|
expect(unescapeLaTeX(input)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('real-world example from bug report', () => {
|
||||||
|
it('should correctly unescape restaurant menu content', () => {
|
||||||
|
const input = `# The Golden Spoon
|
||||||
|
## *Contemporary American Cuisine*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### STARTERS
|
||||||
|
|
||||||
|
**Crispy Calamari** - *\\\\$14*
|
||||||
|
Lightly fried, served with marinara & lemon aioli
|
||||||
|
|
||||||
|
**Truffle Fries** - *\\\\$12*
|
||||||
|
Hand-cut fries, parmesan, truffle oil, fresh herbs
|
||||||
|
|
||||||
|
**Burrata & Heirloom Tomatoes** - *\\\\$16*
|
||||||
|
Fresh burrata, basil pesto, balsamic reduction, grilled sourdough
|
||||||
|
|
||||||
|
**Thai Chicken Lettuce Wraps** - *\\\\$13*
|
||||||
|
Spicy ground chicken, water chestnuts, ginger-soy glaze
|
||||||
|
|
||||||
|
**Soup of the Day** - *\\\\$9`;
|
||||||
|
|
||||||
|
const expected = `# The Golden Spoon
|
||||||
|
## *Contemporary American Cuisine*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### STARTERS
|
||||||
|
|
||||||
|
**Crispy Calamari** - *$14*
|
||||||
|
Lightly fried, served with marinara & lemon aioli
|
||||||
|
|
||||||
|
**Truffle Fries** - *$12*
|
||||||
|
Hand-cut fries, parmesan, truffle oil, fresh herbs
|
||||||
|
|
||||||
|
**Burrata & Heirloom Tomatoes** - *$16*
|
||||||
|
Fresh burrata, basil pesto, balsamic reduction, grilled sourdough
|
||||||
|
|
||||||
|
**Thai Chicken Lettuce Wraps** - *$13*
|
||||||
|
Spicy ground chicken, water chestnuts, ginger-soy glaze
|
||||||
|
|
||||||
|
**Soup of the Day** - *$9`;
|
||||||
|
|
||||||
|
expect(unescapeLaTeX(input)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
27
packages/api/src/utils/latex.ts
Normal file
27
packages/api/src/utils/latex.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* Unescapes LaTeX preprocessing done by the frontend preprocessLaTeX function.
|
||||||
|
* This reverses the escaping of currency dollar signs and other LaTeX transformations.
|
||||||
|
*
|
||||||
|
* The frontend escapes dollar signs for proper LaTeX rendering (e.g., $14 → \\$14),
|
||||||
|
* but the database stores the original unescaped versions. This function reverses
|
||||||
|
* that transformation to match database content.
|
||||||
|
*
|
||||||
|
* @param text - The escaped text from the frontend
|
||||||
|
* @returns The unescaped text matching the database format
|
||||||
|
*/
|
||||||
|
export function unescapeLaTeX(text: string | null | undefined): string | null | undefined {
|
||||||
|
if (!text || typeof text !== 'string') {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unescape currency dollar signs (\\$ or \$ → $)
|
||||||
|
// This is the main transformation done by preprocessLaTeX for currency
|
||||||
|
let result = text.replace(/\\\\?\$/g, '$');
|
||||||
|
|
||||||
|
// Unescape mhchem notation if present
|
||||||
|
// Convert $$\\ce{...}$$ back to $\ce{...}$
|
||||||
|
result = result.replace(/\$\$\\\\ce\{([^}]*)\}\$\$/g, '$\\ce{$1}$');
|
||||||
|
result = result.replace(/\$\$\\\\pu\{([^}]*)\}\$\$/g, '$\\pu{$1}$');
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue