Skip to content

Commit

Permalink
✨ Automatically parse markdown from variables in text bubbles
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno authored and jmgoncalves97 committed Jan 17, 2025
1 parent b7ab77e commit 1c5b3af
Show file tree
Hide file tree
Showing 12 changed files with 1,191 additions and 127 deletions.
102 changes: 96 additions & 6 deletions packages/bot-engine/executeGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '@typebot.io/schemas'
import {
isBubbleBlock,
isEmpty,
isInputBlock,
isIntegrationBlock,
isLogicBlock,
Expand All @@ -26,6 +27,12 @@ import { getPrefilledInputValue } from './getPrefilledValue'
import { parseDateInput } from './blocks/inputs/date/parseDateInput'
import { deepParseVariables } from './variables/deepParseVariables'
import { parseVideoUrl } from '@typebot.io/lib/parseVideoUrl'
import { TDescendant, createPlateEditor } from '@udecode/plate-common'
import {
createDeserializeMdPlugin,
deserializeMd,
} from '@udecode/plate-serializer-md'
import { getVariablesToParseInfoInText } from './variables/parseVariables'

export const executeGroup =
(
Expand Down Expand Up @@ -158,12 +165,19 @@ const parseBubbleBlock =
(variables: Variable[]) =>
(block: BubbleBlock): ChatReply['messages'][0] => {
switch (block.type) {
case BubbleBlockType.TEXT:
return deepParseVariables(
variables,
{},
{ takeLatestIfList: true }
)(block)
case BubbleBlockType.TEXT: {
return {
...block,
content: {
...block.content,
richText: parseVariablesInRichText(
block.content.richText,
variables
),
},
}
}

case BubbleBlockType.EMBED: {
const message = deepParseVariables(variables)(block)
return {
Expand All @@ -189,6 +203,82 @@ const parseBubbleBlock =
}
}

const parseVariablesInRichText = (
elements: TDescendant[],
variables: Variable[]
): TDescendant[] => {
const parsedElements: TDescendant[] = []
for (const element of elements) {
if ('text' in element) {
const text = element.text as string
if (isEmpty(text)) {
parsedElements.push(element)
continue
}
const variablesInText = getVariablesToParseInfoInText(text, variables)
if (variablesInText.length === 0) {
parsedElements.push(element)
continue
}
for (const variableInText of variablesInText) {
const textBeforeVariable = text.substring(0, variableInText.startIndex)
const textAfterVariable = text.substring(variableInText.endIndex)
const isStandaloneElement =
isEmpty(textBeforeVariable) && isEmpty(textAfterVariable)
const variableElements = convertMarkdownToRichText(
isStandaloneElement
? variableInText.value
: variableInText.value.replace(/[\n]+/g, ' ')
)
if (isStandaloneElement) {
parsedElements.push(...variableElements)
continue
}
const children: TDescendant[] = []
if (isNotEmpty(textBeforeVariable))
children.push({
text: textBeforeVariable,
})
children.push({
type: 'inline-variable',
children: variableElements,
})
if (isNotEmpty(textAfterVariable))
children.push({
...element,
text: textAfterVariable,
})
parsedElements.push(...children)
}
continue
}

const type =
element.children.length === 1 &&
'text' in element.children[0] &&
(element.children[0].text as string).startsWith('{{') &&
(element.children[0].text as string).endsWith('}}')
? 'variable'
: element.type

parsedElements.push({
...element,
type,
children: parseVariablesInRichText(
element.children as TDescendant[],
variables
),
})
}
return parsedElements
}

const convertMarkdownToRichText = (text: string): TDescendant[] => {
const plugins = [createDeserializeMdPlugin()]
//@ts-ignore
return deserializeMd(createPlateEditor({ plugins }), text)
}

export const parseInput =
(state: SessionState) =>
async (block: InputBlock): Promise<ChatReply['input']> => {
Expand Down
1 change: 1 addition & 0 deletions packages/bot-engine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@typebot.io/schemas": "workspace:*",
"@typebot.io/tsconfig": "workspace:*",
"@udecode/plate-common": "^21.1.5",
"@udecode/plate-serializer-md": "^24.4.0",
"ai": "2.1.32",
"chrono-node": "2.6.6",
"date-fns": "^2.30.0",
Expand Down
29 changes: 29 additions & 0 deletions packages/bot-engine/variables/parseVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,35 @@ export const parseVariables =
)
}

type VariableToParseInformation = {
startIndex: number
endIndex: number
textToReplace: string
value: string
}

export const getVariablesToParseInfoInText = (
text: string,
variables: Variable[]
): VariableToParseInformation[] => {
const pattern = /\{\{([^{}]+)\}\}|(\$)\{\{([^{}]+)\}\}/g
const variablesToParseInfo: VariableToParseInformation[] = []
let match
while ((match = pattern.exec(text)) !== null) {
const matchedVarName = match[1] ?? match[3]
const variable = variables.find((variable) => {
return matchedVarName === variable.name && isDefined(variable.value)
}) as VariableWithValue | undefined
variablesToParseInfo.push({
startIndex: match.index,
endIndex: match.index + match[0].length,
textToReplace: match[0],
value: safeStringify(variable?.value) ?? '',
})
}
return variablesToParseInfo
}

const parseVariableValueInJson = (value: VariableWithValue['value']) => {
const stringifiedValue = JSON.stringify(value)
if (typeof value === 'string') return stringifiedValue.slice(1, -1)
Expand Down
9 changes: 6 additions & 3 deletions packages/embeds/js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@typebot.io/js",
"version": "0.1.33",
"version": "0.1.34",
"description": "Javascript library to display typebots on your website",
"type": "module",
"main": "dist/index.js",
Expand All @@ -14,7 +14,9 @@
"dependencies": {
"@stripe/stripe-js": "1.54.1",
"@udecode/plate-common": "^21.1.5",
"dompurify": "^3.0.6",
"eventsource-parser": "^1.0.0",
"marked": "^9.0.3",
"solid-element": "1.7.1",
"solid-js": "1.7.8"
},
Expand All @@ -24,11 +26,12 @@
"@rollup/plugin-node-resolve": "15.1.0",
"@rollup/plugin-terser": "0.4.3",
"@rollup/plugin-typescript": "11.1.2",
"@typebot.io/lib": "workspace:*",
"@typebot.io/bot-engine": "workspace:*",
"@typebot.io/env": "workspace:*",
"@typebot.io/lib": "workspace:*",
"@typebot.io/schemas": "workspace:*",
"@typebot.io/tsconfig": "workspace:*",
"@typebot.io/bot-engine": "workspace:*",
"@types/dompurify": "^3.0.3",
"autoprefixer": "10.4.14",
"babel-preset-solid": "1.7.7",
"clsx": "2.0.0",
Expand Down
6 changes: 1 addition & 5 deletions packages/embeds/js/src/assets/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,10 @@ textarea {
font-weight: 300;
}

.slate-a {
a {
text-decoration: underline;
}

.slate-html-container > div {
min-height: 24px;
}

.slate-bold {
font-weight: bold;
}
Expand Down
24 changes: 17 additions & 7 deletions packages/embeds/js/src/components/bubbles/StreamingBubble.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
import { streamingMessage } from '@/utils/streamingMessageSignal'
import { createEffect, createSignal } from 'solid-js'
import { marked } from 'marked'
import domPurify from 'dompurify'

type Props = {
streamingMessageId: string
}

marked.use({
renderer: {
link: (href, _title, text) => {
return `<a href="${href}" target="_blank" rel="noopener noreferrer">${text}</a>`
},
},
})

export const StreamingBubble = (props: Props) => {
let ref: HTMLDivElement | undefined
const [content, setContent] = createSignal<string>('')

createEffect(() => {
if (streamingMessage()?.id === props.streamingMessageId)
setContent(streamingMessage()?.content ?? '')
setContent(
domPurify.sanitize(marked.parse(streamingMessage()?.content ?? ''))
)
})

return (
<div class="flex flex-col animate-fade-in" ref={ref}>
<div class="flex flex-col animate-fade-in">
<div class="flex w-full items-center">
<div class="flex relative items-start typebot-host-bubble">
<div
Expand All @@ -28,11 +39,10 @@ export const StreamingBubble = (props: Props) => {
/>
<div
class={
'overflow-hidden text-fade-in mx-4 my-2 whitespace-pre-wrap slate-html-container relative text-ellipsis opacity-100 h-full'
'flex flex-col overflow-hidden text-fade-in mx-4 my-2 relative text-ellipsis h-full gap-6'
}
>
{content()}
</div>
innerHTML={content()}
/>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TypingBubble } from '@/components'
import type { TextBubbleContent, TypingEmulation } from '@typebot.io/schemas'
import { For, createSignal, onCleanup, onMount } from 'solid-js'
import { PlateBlock } from './plate/PlateBlock'
import { PlateElement } from './plate/PlateBlock'
import { computePlainText } from '../helpers/convertRichTextToPlainText'
import { clsx } from 'clsx'
import { isMobile } from '@/utils/isMobileSignal'
Expand Down Expand Up @@ -70,7 +70,7 @@ export const TextBubble = (props: Props) => {
}}
>
<For each={props.content.richText}>
{(element) => <PlateBlock element={element} />}
{(element) => <PlateElement element={element} />}
</For>
</div>
</div>
Expand Down
Loading

0 comments on commit 1c5b3af

Please sign in to comment.