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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions common/types/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export type CustomUI = {
chatQuoteColor: string
chatQuoteEmphasisColor: string
chatQuoteEmphasisWeight: string
chatEmphasisEmphasisColor: string
chatEmphasisEmphasisWeight: string
}

export type MessageOption =
Expand Down Expand Up @@ -101,6 +103,8 @@ const customUiGuard = {
chatQuoteColor: 'string',
chatQuoteEmphasisColor: 'string',
chatQuoteEmphasisWeight: 'string',
chatEmphasisEmphasisColor: 'string',
chatEmphasisEmphasisWeight: 'string',
} as const

export const uiGuard = {
Expand Down Expand Up @@ -152,6 +156,8 @@ export const defaultUIsettings: UISettings = {
chatQuoteColor: '--text-800',
chatQuoteEmphasisColor: '--text-800',
chatQuoteEmphasisWeight: 'unset',
chatEmphasisEmphasisColor: '--text-800',
chatEmphasisEmphasisWeight: 'unset',
},

dark: {
Expand All @@ -162,6 +168,8 @@ export const defaultUIsettings: UISettings = {
chatQuoteColor: '--text-800',
chatQuoteEmphasisColor: '--text-800',
chatQuoteEmphasisWeight: 'unset',
chatEmphasisEmphasisColor: '--text-800',
chatEmphasisEmphasisWeight: 'unset',
},

msgOptsInline: {
Expand Down
8 changes: 8 additions & 0 deletions web/pages/Chat/components/Message.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ qem {
font-weight: var(--text-quote-emphasis-weight);
}

eem {
/* emphasis emphasis; *inner* emphasis within <em> tags */
color: var(--text-800);
color: var(--text-emphasis-emphasis-color);
font-style: italic;
font-weight: var(--text-emphasis-emphasis-weight);
}

.rendered-markdown img {
display: inline-block; /* markdown images are expected to be inline-block */
}
Expand Down
135 changes: 87 additions & 48 deletions web/pages/Chat/components/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1046,70 +1046,109 @@ function retryJsonSchema(original: AppSchema.ChatMessage, split: SplitMessage) {
}

function renderMessage(ctx: ContextState, text: string, isUser: boolean, adapter?: string) {
// Iam getting pissed of by showdown effing up Asteriks, I just do this myself - @Aldimensch
// Escape single asterisks but not double asterisks
text = text.replace(/(?<!\*)\*(?!\*)/g, '\\*')

// Address unfortunate Showdown bug where spaces in code blocks are replaced with nbsp, except
// it also encodes the ampersand, which results in them actually being rendered as `&amp;nbsp;`
// https://github.com/showdownjs/showdown/issues/669

// we sanizize user input to prevent XSS attacks
// DomPurify has an implicit list of allowed Tags, when we add our own we have to use ADD_TAGS
const html = Purify.sanitize(
wrapWithQuoteElement(
markdown.makeHtml(parseMessage(text, ctx, isUser, adapter)).replace(/&amp;nbsp;/g, '&nbsp;')
),
{
ADD_TAGS: ['qem'],
}
let html = makeTextLookNice(
markdown
.makeHtml(parseMessage(text, ctx, isUser, adapter))
.replace(/&amp;nbsp;/g, '&nbsp;')
)

// Now, after HTML is generated, process asterisks in text nodes only
html = asterikPairing(html)

html = Purify.sanitize(html, {
ADD_TAGS: ['qem', 'eem'],
})

return html
}

/**
* Markup beautification. Lets us control color of diffrent HTML tags, expands on the markdown functionality
* Especially useful for quotes, which are wrapped in <q> tags
* and emphasis, which is wrapped in <qem> tags.
*/
function wrapWithQuoteElement(str: string) {
// Replace all non-regular double quotes with double regular quotes
// Unicode double quote characters: https://en.wikipedia.org/wiki/Quotation_mark#Unicode_code_point_table
str = str.replace(/[\u201C\u201D\u201E\u201F]/g, '"')

return str.replace(
/*
Regex magic explained:
<[\s\S]*?> - skip all HTML tags eg. <sumting>
```[\s\S]*?``` - skip all code blocks eg. <pre>/``` markdown transform <pre><code> to ```
``[\s\S]*?`` - skip all inline code eg. <code>/`` markdown transform <code> to `` | this is a non standard markup
`[\s\S]*?` - skip all inline code eg. <code>/` markdown transform <code> to `

(\".+?\") - capture all regular double quotes, which are not part of HTML tags or code blocks
All captured groups are passed to the wrapCaptureGroupQuotes function
*/
/<[\s\S]*?>|```[\s\S]*?```|``[\s\S]*?``|`[\s\S]*?`|(\".+?\")/gm,
wrapCaptureGroupQuotes
)
function asterikPairing(text: string) {
const lines = text.split(/((?:<\/p>|<\/pre>|<br\s*\/?>|<q>|<\/q>))/i);
let accum = '';
let tempLine = '';
for (let line of lines) {
if (/(?:<\/p>|<\/pre>|<br\s*\/?>|<q>|<\/q>)/i.test(line)) {
accum += line;
} else {
tempLine = outerAsterikPairing(line);
accum += innerAsterikPairing(tempLine);
}
}
return accum;
}

/** Processes capture group from above */
function wrapCaptureGroupQuotes(match: string, regularQuoted?: string) {
if (regularQuoted) {
/*If we have a valid string then we are within a quote
([\s\S]*?) - we ignore all characters between <em> and </em>
a valid capure will look like this: "lets have some <em>fun</em>"
we then pass the capture group to wrapCaptureGroupEmphasis function, which will replace <em> with <qem>
*/
regularQuoted = regularQuoted.replace(/<em>([\s\S]*?)<\/em>/gm, wrapCaptureGroupEmphasis)
return '<q>"' + regularQuoted.replace(/\"/g, '') + '"</q>'
function outerAsterikPairing(line: string) {
// Check if the line contains any <pre> or <code> tags, we dont mess with the content of these tags.
if (/<(?:pre|code)(?:\s[^>]*)?>/i.test(line)) return line

const firstAsterisk = line.indexOf('*')
const lastAsterisk = line.lastIndexOf('*')
if (firstAsterisk !== -1 && lastAsterisk !== -1 && firstAsterisk !== lastAsterisk) {
return line.slice(0, firstAsterisk) + '<em>' + line.slice(firstAsterisk + 1, lastAsterisk) + '</em>' + line.slice(lastAsterisk + 1)
}
return match
return line
}

/** Replaces all <em> tags within a <q> tag with <qem> tags */
function wrapCaptureGroupEmphasis(match: string, emphasisQuote?: string) {
if (emphasisQuote) {
return '<qem>' + emphasisQuote.replace(/\"/g, '') + '</qem>'
}
return match
function innerAsterikPairing(line: string) {
// Check if the line contains any <pre> or <code> tags, we dont mess with the content of these tags.
if (/<(?:pre|code)(?:\s[^>]*)?>/i.test(line)) return line

// Count asterisks in the line
const asteriskCount = (line.match(/\*/g) || []).length

// If odd number of asterisks, we can't properly pair them
if (asteriskCount % 2 !== 0) return line

// If even number of asterisks, replace all * pairs with <em> tags
return line.replace(/\*(.*?)\*/g, '<eem>$1</eem>')
}

/**
* Beautifies the markup by handling custom quote and emphasis formatting in a single pass.
* This function replaces a series of sequential replacements with a single, more efficient
* regular expression. It processes:
* - Escaped asterisks inside `<code>` blocks.
* - Double-quoted sections `"..."` into `<q>...</q>` tags.
* - Asterisk-based emphasis inside quotes into `<qem>...</qem>` tags.
* (Asterisk emphasis is now handled after HTML generation)
*/
function makeTextLookNice(str: string) {
// First, normalize all Unicode double quotes to standard double quotes.
// Unicode double quote characters: https://en.wikipedia.org/wiki/Quotation_mark#Unicode_code_point_table
const normalizedStr = str.replace(/[\u201C\u201D\u201E\u201F]/g, '"')

// This regex handles two cases, in order of priority:
// 1. `<code>...</code>` blocks, 2. quoted sections `"..."`
return normalizedStr.replace(
/(<code>[\s\S]*?<\/code>)|(".*?")/gm,
(match, codeBlock, quoted) => {
// Case 1: A code block was matched.
if (codeBlock) {
// Un-escape any `\*` back to `*` inside a code block.
return codeBlock.replace(/\\\*/g, '*')
}

// Case 2: A quoted string was matched.
if (quoted) {
// Find any asterisk-wrapped text *inside* the quote and
// convert it to a custom emphasis tag `<qem>`.
const innerContent = quoted.slice(1, -1).replace(/\*(.*?)\*/g, '<qem>$1</qem>')
return `<q>"${innerContent}"</q>`
}

return match
}
)
}

function sendAction(_send: MessageProps['sendMessage'], action: AppSchema.ChatAction) {
Expand Down
30 changes: 30 additions & 0 deletions web/pages/Settings/UISettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,36 @@ const UISettings: Component<{}> = () => {
value={state.current.chatQuoteEmphasisWeight || 'unset'}
/>

<ColorPicker
label="Chat Emphasis Emphasis Color"
fieldName="chatEmphasisEmphasisColor"
helperText={
<span
class="link"
onClick={() => userStore.saveCustomUI({ chatEmphasisEmphasisColor: 'text-800' })}
>
Reset to Default
</span>
}
onInput={(color) => tryCustomUI({ chatEmphasisEmphasisColor: color })}
onChange={(color) => userStore.saveCustomUI({ chatEmphasisEmphasisColor: color })}
value={state.current.chatEmphasisEmphasisColor || '--text-800'}
/>

<Select
fieldName="chatEmphasisEmphasisWeight"
label="Chat Emphasis Emphasis Weight"
inline
items={[
{ label: 'None', value: 'unset' },
{ label: 'Bold', value: 'bold' },
]}
onChange={(item) =>
userStore.saveCustomUI({ chatEmphasisEmphasisWeight: item.value as string })
}
value={state.current.chatEmphasisEmphasisWeight || 'unset'}
/>

<Select
fieldName="chatWidth"
label="Content Width"
Expand Down
8 changes: 8 additions & 0 deletions web/store/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1022,12 +1022,20 @@ async function updateTheme(ui: UI.UISettings) {
'text-quote-emphasis-color',
getSettingColor(mode.chatQuoteEmphasisColor || 'text-800')
)
setRootVariable(
'text-emphasis-emphasis-color',
getSettingColor(mode.chatEmphasisEmphasisColor || 'text-800')
)
setRootVariable('bot-background', getSettingColor(mode.botBackground || 'bg-800'))

setRootVariable(
'text-quote-emphasis-weight',
mode.chatQuoteEmphasisWeight
) /*Controls the thickness of the font. default: 'unset'*/
setRootVariable(
'text-emphasis-emphasis-weight',
mode.chatEmphasisEmphasisWeight
) /*Controls the thickness of the font. default: 'unset'*/
root.style.setProperty(`--sitewide-font`, fontFaces[ui.font])
}

Expand Down
2 changes: 2 additions & 0 deletions web/variables.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
--text-quote-color: unset;
--text-quote-emphasis-color: unset;
--text-quote-emphasis-weight: unset;
--text-emphasis-emphasis-color: unset;
--text-emphasis-emphasis-weight: unset;

--tooltip-x: 0;
--tooltip-y: 0;
Expand Down
Loading