mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
✏️ feat: LaTeX parsing for Messages (#1585)
* feat: Beta features tab in Settings and LaTeX Parsing toggle * feat: LaTex parsing with spec
This commit is contained in:
parent
638f9242e5
commit
a8d6bfde7a
17 changed files with 291 additions and 22 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import { useRecoilValue } from 'recoil';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
|
|
@ -8,9 +9,10 @@ import rehypeKatex from 'rehype-katex';
|
|||
import remarkMath from 'remark-math';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import { langSubset, validateIframe } from '~/utils';
|
||||
import CodeBlock from '~/components/Messages/Content/CodeBlock';
|
||||
import { langSubset, validateIframe, processLaTeX } from '~/utils';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import store from '~/store';
|
||||
|
||||
type TCodeProps = {
|
||||
inline: boolean;
|
||||
|
|
@ -42,11 +44,15 @@ const p = React.memo(({ children }: { children: React.ReactNode }) => {
|
|||
const Markdown = React.memo(({ content, message, showCursor }: TContentProps) => {
|
||||
const [cursor, setCursor] = useState('█');
|
||||
const { isSubmitting, latestMessage } = useChatContext();
|
||||
const LaTeXParsing = useRecoilValue<boolean>(store.LaTeXParsing);
|
||||
|
||||
const isInitializing = content === '<span className="result-streaming">█</span>';
|
||||
|
||||
const { isEdited, messageId } = message ?? {};
|
||||
const isLatestMessage = messageId === latestMessage?.messageId;
|
||||
const currentContent = content?.replace('z-index: 1;', '') ?? '';
|
||||
|
||||
const _content = content?.replace('z-index: 1;', '') ?? '';
|
||||
const currentContent = LaTeXParsing ? processLaTeX(_content) : _content;
|
||||
|
||||
useEffect(() => {
|
||||
let timer1: NodeJS.Timeout, timer2: NodeJS.Timeout;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import * as Tabs from '@radix-ui/react-tabs';
|
||||
import { SettingsTabValues } from 'librechat-data-provider';
|
||||
import type { TDialogProps } from '~/common';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui';
|
||||
import { GearIcon, DataIcon, UserIcon } from '~/components/svg';
|
||||
import { General, Data, Account } from './SettingsTabs';
|
||||
import { GearIcon, DataIcon, UserIcon, ExperimentIcon } from '~/components/svg';
|
||||
import { General, Beta, Data, Account } from './SettingsTabs';
|
||||
import { useMediaQuery, useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
|
|
@ -23,7 +24,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
|||
</DialogHeader>
|
||||
<div className="px-6">
|
||||
<Tabs.Root
|
||||
defaultValue="general"
|
||||
defaultValue={SettingsTabValues.GENERAL}
|
||||
className="flex flex-col gap-10 md:flex-row"
|
||||
orientation="vertical"
|
||||
>
|
||||
|
|
@ -44,7 +45,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
|||
? 'flex-1 items-center justify-center text-sm dark:text-gray-500 dark:radix-state-active:text-white'
|
||||
: '',
|
||||
)}
|
||||
value="general"
|
||||
value={SettingsTabValues.GENERAL}
|
||||
>
|
||||
<GearIcon />
|
||||
{localize('com_nav_setting_general')}
|
||||
|
|
@ -56,7 +57,19 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
|||
? 'flex-1 items-center justify-center text-sm dark:text-gray-500 dark:radix-state-active:text-white'
|
||||
: '',
|
||||
)}
|
||||
value="data"
|
||||
value={SettingsTabValues.BETA}
|
||||
>
|
||||
<ExperimentIcon />
|
||||
{localize('com_nav_setting_beta')}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
className={cn(
|
||||
'group my-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-gray-100 radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-800',
|
||||
isSmallScreen
|
||||
? 'flex-1 items-center justify-center text-sm dark:text-gray-500 dark:radix-state-active:text-white'
|
||||
: '',
|
||||
)}
|
||||
value={SettingsTabValues.DATA}
|
||||
>
|
||||
<DataIcon />
|
||||
{localize('com_nav_setting_data')}
|
||||
|
|
@ -68,13 +81,14 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
|||
? 'flex-1 items-center justify-center text-sm dark:text-gray-500 dark:radix-state-active:text-white'
|
||||
: '',
|
||||
)}
|
||||
value="account"
|
||||
value={SettingsTabValues.ACCOUNT}
|
||||
>
|
||||
<UserIcon />
|
||||
{localize('com_nav_setting_account')}
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<General />
|
||||
<Beta />
|
||||
<Data />
|
||||
<Account />
|
||||
</Tabs.Root>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import * as Tabs from '@radix-ui/react-tabs';
|
||||
import Avatar from './Avatar';
|
||||
import React from 'react';
|
||||
import * as Tabs from '@radix-ui/react-tabs';
|
||||
import { SettingsTabValues } from 'librechat-data-provider';
|
||||
import Avatar from './Avatar';
|
||||
|
||||
function Account() {
|
||||
return (
|
||||
<Tabs.Content value="account" role="tabpanel" className="w-full md:min-h-[300px]">
|
||||
<Tabs.Content
|
||||
value={SettingsTabValues.ACCOUNT}
|
||||
role="tabpanel"
|
||||
className="w-full md:min-h-[300px]"
|
||||
>
|
||||
<div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-300">
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<Avatar />
|
||||
|
|
|
|||
26
client/src/components/Nav/SettingsTabs/Beta/Beta.tsx
Normal file
26
client/src/components/Nav/SettingsTabs/Beta/Beta.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { memo } from 'react';
|
||||
import * as Tabs from '@radix-ui/react-tabs';
|
||||
import { SettingsTabValues } from 'librechat-data-provider';
|
||||
import LaTeXParsing from './LaTeXParsing';
|
||||
import ModularChat from './ModularChat';
|
||||
|
||||
function Beta() {
|
||||
return (
|
||||
<Tabs.Content
|
||||
value={SettingsTabValues.BETA}
|
||||
role="tabpanel"
|
||||
className="w-full md:min-h-[300px]"
|
||||
>
|
||||
<div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-300">
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<ModularChat />
|
||||
</div>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<LaTeXParsing />
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Beta);
|
||||
33
client/src/components/Nav/SettingsTabs/Beta/LaTeXParsing.tsx
Normal file
33
client/src/components/Nav/SettingsTabs/Beta/LaTeXParsing.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { useRecoilState } from 'recoil';
|
||||
import { Switch } from '~/components/ui';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
export default function LaTeXParsingSwitch({
|
||||
onCheckedChange,
|
||||
}: {
|
||||
onCheckedChange?: (value: boolean) => void;
|
||||
}) {
|
||||
const [LaTeXParsing, setLaTeXParsing] = useRecoilState<boolean>(store.LaTeXParsing);
|
||||
const localize = useLocalize();
|
||||
|
||||
const handleCheckedChange = (value: boolean) => {
|
||||
setLaTeXParsing(value);
|
||||
if (onCheckedChange) {
|
||||
onCheckedChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>{localize('com_nav_latex_parsing')} </div>
|
||||
<Switch
|
||||
id="LaTeXParsing"
|
||||
checked={LaTeXParsing}
|
||||
onCheckedChange={handleCheckedChange}
|
||||
className="ml-4 mt-2"
|
||||
data-testid="LaTeXParsing"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -20,9 +20,7 @@ export default function ModularChatSwitch({
|
|||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
{`[${localize('com_ui_experimental')}]`} {localize('com_nav_modular_chat')}{' '}
|
||||
</div>
|
||||
<div>{localize('com_nav_modular_chat')} </div>
|
||||
<Switch
|
||||
id="modularChat"
|
||||
checked={modularChat}
|
||||
|
|
@ -3,6 +3,7 @@ import {
|
|||
useRevokeAllUserKeysMutation,
|
||||
useRevokeUserKeyMutation,
|
||||
} from 'librechat-data-provider/react-query';
|
||||
import { SettingsTabValues } from 'librechat-data-provider';
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import { useOnClickOutside } from '~/hooks';
|
||||
import DangerButton from '../DangerButton';
|
||||
|
|
@ -66,7 +67,11 @@ export const RevokeKeysButton = ({
|
|||
|
||||
function Data() {
|
||||
return (
|
||||
<Tabs.Content value="data" role="tabpanel" className="w-full md:min-h-[300px]">
|
||||
<Tabs.Content
|
||||
value={SettingsTabValues.DATA}
|
||||
role="tabpanel"
|
||||
className="w-full md:min-h-[300px]"
|
||||
>
|
||||
<div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-300">
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<RevokeKeysButton all={true} />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useRecoilState } from 'recoil';
|
||||
import * as Tabs from '@radix-ui/react-tabs';
|
||||
import { SettingsTabValues } from 'librechat-data-provider';
|
||||
import React, { useState, useContext, useCallback, useRef } from 'react';
|
||||
import { useClearConversationsMutation } from 'librechat-data-provider/react-query';
|
||||
import {
|
||||
|
|
@ -14,7 +15,6 @@ import type { TDangerButtonProps } from '~/common';
|
|||
import AutoScrollSwitch from './AutoScrollSwitch';
|
||||
import { Dropdown } from '~/components/ui';
|
||||
import DangerButton from '../DangerButton';
|
||||
import ModularChat from './ModularChat';
|
||||
import store from '~/store';
|
||||
|
||||
export const ThemeSelector = ({
|
||||
|
|
@ -167,7 +167,7 @@ function General() {
|
|||
|
||||
return (
|
||||
<Tabs.Content
|
||||
value="general"
|
||||
value={SettingsTabValues.GENERAL}
|
||||
role="tabpanel"
|
||||
className="w-full md:min-h-[300px]"
|
||||
ref={contentRef}
|
||||
|
|
@ -190,9 +190,6 @@ function General() {
|
|||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<AutoScrollSwitch />
|
||||
</div>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<ModularChat />
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export { default as General } from './General/General';
|
||||
export { ClearChatsButton } from './General/General';
|
||||
export { default as Data } from './Data/Data';
|
||||
export { default as Beta } from './Beta/Beta';
|
||||
export { RevokeKeysButton } from './Data/Data';
|
||||
export { default as Account } from './Account/Account';
|
||||
|
|
|
|||
27
client/src/components/svg/ExperimentIcon.tsx
Normal file
27
client/src/components/svg/ExperimentIcon.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
export default function ExperimentIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="icon-sm"
|
||||
>
|
||||
<path
|
||||
d="M9 3H15M9 3V9.2759C9 9.74377 8.83597 10.1968 8.53644 10.5563L4.85085 14.979C4.30108 15.6387 4 16.4703 4 17.3291V17.3291C4 19.3565 5.64353 21 7.67094 21H16.3291C18.3565 21 20 19.3565 20 17.3291V17.3291C20 16.4703 19.6989 15.6387 19.1492 14.979L15.4636 10.5563C15.164 10.1968 15 9.74377 15 9.2759V3M9 3H8M15 3H16"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M5 14.774C11.5 12.839 12.15 16.7089 18 14"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
@ -42,3 +42,4 @@ export { default as GoogleMinimalIcon } from './GoogleMinimalIcon';
|
|||
export { default as AnthropicMinimalIcon } from './AnthropicMinimalIcon';
|
||||
export { default as SendMessageIcon } from './SendMessageIcon';
|
||||
export { default as UserIcon } from './UserIcon';
|
||||
export { default as ExperimentIcon } from './ExperimentIcon';
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export default {
|
|||
com_ui_limitation_harmful_biased:
|
||||
'May occasionally produce harmful instructions or biased content',
|
||||
com_ui_limitation_limited_2021: 'Limited knowledge of world and events after 2021',
|
||||
com_ui_experimental: 'Experimental',
|
||||
com_ui_experimental: 'Experimental Features',
|
||||
com_ui_input: 'Input',
|
||||
com_ui_close: 'Close',
|
||||
com_ui_model: 'Model',
|
||||
|
|
@ -265,6 +265,8 @@ export default {
|
|||
com_nav_welcome_message: 'How can I help you today?',
|
||||
com_nav_auto_scroll: 'Auto-scroll to Newest on Open',
|
||||
com_nav_modular_chat: 'Enable switching Endpoints mid-conversation',
|
||||
com_nav_latex_parsing:
|
||||
'Toggle parsing LaTeX in messages. Enabled by default but may affect performance on mobile or longer conversations.',
|
||||
com_nav_profile_picture: 'Profile Picture',
|
||||
com_nav_change_picture: 'Change picture',
|
||||
com_nav_plugin_store: 'Plugin store',
|
||||
|
|
@ -303,6 +305,7 @@ export default {
|
|||
com_nav_settings: 'Settings',
|
||||
com_nav_search_placeholder: 'Search messages',
|
||||
com_nav_setting_general: 'General',
|
||||
com_nav_setting_beta: 'Beta features',
|
||||
com_nav_setting_data: 'Data controls',
|
||||
com_nav_setting_account: 'Account',
|
||||
com_nav_language: 'Language',
|
||||
|
|
|
|||
|
|
@ -69,6 +69,25 @@ const modularChat = atom<boolean>({
|
|||
] as const,
|
||||
});
|
||||
|
||||
const LaTeXParsing = atom<boolean>({
|
||||
key: 'LaTeXParsing',
|
||||
default: true,
|
||||
effects: [
|
||||
({ setSelf, onSet }) => {
|
||||
const savedValue = localStorage.getItem('LaTeXParsing');
|
||||
if (savedValue != null) {
|
||||
setSelf(savedValue === 'true');
|
||||
}
|
||||
|
||||
onSet((newValue: unknown) => {
|
||||
if (typeof newValue === 'boolean') {
|
||||
localStorage.setItem('LaTeXParsing', newValue.toString());
|
||||
}
|
||||
});
|
||||
},
|
||||
] as const,
|
||||
});
|
||||
|
||||
export default {
|
||||
abortScroll,
|
||||
optionSettings,
|
||||
|
|
@ -78,4 +97,5 @@ export default {
|
|||
showPopover,
|
||||
autoScroll,
|
||||
modularChat,
|
||||
LaTeXParsing,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export * from './json';
|
||||
export * from './files';
|
||||
export * from './latex';
|
||||
export * from './presets';
|
||||
export * from './languages';
|
||||
export * from './endpoints';
|
||||
|
|
|
|||
86
client/src/utils/latex.spec.ts
Normal file
86
client/src/utils/latex.spec.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { processLaTeX } from './latex';
|
||||
|
||||
describe('processLaTeX', () => {
|
||||
test('returns the same string if no LaTeX patterns are found', () => {
|
||||
const content = 'This is a test string without LaTeX';
|
||||
expect(processLaTeX(content)).toBe(content);
|
||||
});
|
||||
|
||||
test('converts inline LaTeX expressions correctly', () => {
|
||||
const content = 'This is an inline LaTeX expression: \\(x^2 + y^2 = z^2\\)';
|
||||
const expected = 'This is an inline LaTeX expression: $x^2 + y^2 = z^2$';
|
||||
expect(processLaTeX(content)).toBe(expected);
|
||||
});
|
||||
|
||||
test('converts block LaTeX expressions correctly', () => {
|
||||
const content = 'This is a block LaTeX expression: \\[E = mc^2\\]';
|
||||
const expected = 'This is a block LaTeX expression: $$E = mc^2$$';
|
||||
expect(processLaTeX(content)).toBe(expected);
|
||||
});
|
||||
|
||||
test('converts mixed LaTeX expressions correctly', () => {
|
||||
const content = 'Inline \\(a + b = c\\) and block \\[x^2 + y^2 = z^2\\]';
|
||||
const expected = 'Inline $a + b = c$ and block $$x^2 + y^2 = z^2$$';
|
||||
expect(processLaTeX(content)).toBe(expected);
|
||||
});
|
||||
|
||||
test('escapes dollar signs followed by a digit or space and digit', () => {
|
||||
const content = 'Price is $50 and $ 100';
|
||||
const expected = 'Price is \\$50 and \\$ 100';
|
||||
expect(processLaTeX(content)).toBe(expected);
|
||||
});
|
||||
|
||||
test('handles strings with no content', () => {
|
||||
const content = '';
|
||||
expect(processLaTeX(content)).toBe('');
|
||||
});
|
||||
|
||||
test('does not alter already valid inline Markdown LaTeX', () => {
|
||||
const content = 'This is a valid inline LaTeX: $x^2 + y^2 = z^2$';
|
||||
expect(processLaTeX(content)).toBe(content);
|
||||
});
|
||||
|
||||
test('does not alter already valid block Markdown LaTeX', () => {
|
||||
const content = 'This is a valid block LaTeX: $$E = mc^2$$';
|
||||
expect(processLaTeX(content)).toBe(content);
|
||||
});
|
||||
|
||||
test('correctly processes a mix of valid Markdown LaTeX and LaTeX patterns', () => {
|
||||
const content = 'Valid $a + b = c$ and LaTeX to convert \\(x^2 + y^2 = z^2\\)';
|
||||
const expected = 'Valid $a + b = c$ and LaTeX to convert $x^2 + y^2 = z^2$';
|
||||
expect(processLaTeX(content)).toBe(expected);
|
||||
});
|
||||
|
||||
test('correctly handles strings with LaTeX and non-LaTeX dollar signs', () => {
|
||||
const content = 'Price $100 and LaTeX \\(x^2 + y^2 = z^2\\)';
|
||||
const expected = 'Price \\$100 and LaTeX $x^2 + y^2 = z^2$';
|
||||
expect(processLaTeX(content)).toBe(expected);
|
||||
});
|
||||
|
||||
test('ignores non-LaTeX content enclosed in dollar signs', () => {
|
||||
const content = 'This is not LaTeX: $This is just text$';
|
||||
expect(processLaTeX(content)).toBe(content);
|
||||
});
|
||||
|
||||
test('correctly processes complex block LaTeX with line breaks', () => {
|
||||
const complexBlockLatex = `Certainly! Here's an example of a mathematical formula written in LaTeX:
|
||||
|
||||
\\[
|
||||
\\sum_{i=1}^{n} \\left( \\frac{x_i}{y_i} \\right)^2
|
||||
\\]
|
||||
|
||||
This formula represents the sum of the squares of the ratios of \\(x\\) to \\(y\\) for \\(n\\) terms, where \\(x_i\\) and \\(y_i\\) represent the values of \\(x\\) and \\(y\\) for each term.
|
||||
|
||||
LaTeX is a typesetting system commonly used for mathematical and scientific documents. It provides a wide range of formatting options and symbols for expressing mathematical expressions.`;
|
||||
const expectedOutput = `Certainly! Here's an example of a mathematical formula written in LaTeX:
|
||||
|
||||
$$
|
||||
\\sum_{i=1}^{n} \\left( \\frac{x_i}{y_i} \\right)^2
|
||||
$$
|
||||
|
||||
This formula represents the sum of the squares of the ratios of $x$ to $y$ for $n$ terms, where $x_i$ and $y_i$ represent the values of $x$ and $y$ for each term.
|
||||
|
||||
LaTeX is a typesetting system commonly used for mathematical and scientific documents. It provides a wide range of formatting options and symbols for expressing mathematical expressions.`;
|
||||
expect(processLaTeX(complexBlockLatex)).toBe(expectedOutput);
|
||||
});
|
||||
});
|
||||
24
client/src/utils/latex.ts
Normal file
24
client/src/utils/latex.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Regex to check if the processed content contains any potential LaTeX patterns
|
||||
const containsLatexRegex =
|
||||
/\\\(.*?\\\)|\\\[.*?\\\]|\$.*?\$|\\begin\{equation\}.*?\\end\{equation\}/;
|
||||
// Regex for inline and block LaTeX expressions
|
||||
const inlineLatex = new RegExp(/\\\((.+?)\\\)/, 'g');
|
||||
// const blockLatex = new RegExp(/\\\[(.*?)\\\]/, 'gs');
|
||||
const blockLatex = new RegExp(/\\\[(.*?[^\\])\\\]/, 'gs');
|
||||
|
||||
export const processLaTeX = (content: string) => {
|
||||
// Escape dollar signs followed by a digit or space and digit
|
||||
let processedContent = content.replace(/(\$)(?=\s?\d)/g, '\\$');
|
||||
|
||||
// If no LaTeX patterns are found, return the processed content
|
||||
if (!containsLatexRegex.test(processedContent)) {
|
||||
return processedContent;
|
||||
}
|
||||
|
||||
// Convert LaTeX expressions to a markdown compatible format
|
||||
processedContent = processedContent
|
||||
.replace(inlineLatex, (match: string, equation: string) => `$${equation}$`) // Convert inline LaTeX
|
||||
.replace(blockLatex, (match: string, equation: string) => `$$${equation}$$`); // Convert block LaTeX
|
||||
|
||||
return processedContent;
|
||||
};
|
||||
|
|
@ -212,3 +212,25 @@ export enum ImageDetailCost {
|
|||
*/
|
||||
ADDITIONAL = 85,
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab values for Settings Dialog
|
||||
*/
|
||||
export enum SettingsTabValues {
|
||||
/**
|
||||
* Tab for General Settings
|
||||
*/
|
||||
GENERAL = 'general',
|
||||
/**
|
||||
* Tab for Beta Features
|
||||
*/
|
||||
BETA = 'beta',
|
||||
/**
|
||||
* Tab for Data Controls
|
||||
*/
|
||||
DATA = 'data',
|
||||
/**
|
||||
* Tab for Account Settings
|
||||
*/
|
||||
ACCOUNT = 'account',
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue