Skip to content

Commit

Permalink
feat(chat): add support for code blocks
Browse files Browse the repository at this point in the history
  • Loading branch information
Mati365 committed Feb 23, 2025
1 parent f50e1f4 commit c122a9c
Show file tree
Hide file tree
Showing 11 changed files with 325 additions and 49 deletions.
2 changes: 2 additions & 0 deletions apps/chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@llm/commons-front": "*",
"@llm/sdk": "*",
"@tailwindcss/typography": "^0.5.16",
"@types/react-syntax-highlighter": "^15.5.3",
"@types/sanitize-html": "^2.13.0",
"@types/uikit": "^3.14.5",
"@under-control/forms": "^2.0.0",
Expand All @@ -24,6 +25,7 @@
"postcss-sort-media-queries": "^5.2.0",
"react-helmet-async": "^2.0.5",
"react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.6.1",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.0",
"sanitize-html": "^2.13.1",
Expand Down
6 changes: 6 additions & 0 deletions apps/chat/src/i18n/packs/i18n-lang-en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,12 @@ export const I18N_PACK_EN = {
webSearch: 'Web search',
saved: 'Saved for later',
},
widgets: {
code: {
copy: 'Copy',
copied: 'Copied!',
},
},
generating: {
title: 'Generating title...',
description: 'Generating description...',
Expand Down
6 changes: 6 additions & 0 deletions apps/chat/src/i18n/packs/i18n-lang-pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,12 @@ export const I18N_PACK_PL: I18nLangPack = {
webSearch: 'Wyszukiwanie w sieci',
saved: 'Zapisane na potem',
},
widgets: {
code: {
copy: 'Kopiuj',
copied: 'Skopiowano!',
},
},
generating: {
title: 'Generowanie tytułu...',
description: 'Generowanie opisu...',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export function ChatMessage(
{(!isYou || isAI) && (
<div
className={clsx(
'flex items-center gap-2 mb-1 text-sm',
'flex items-center gap-2 mb-2 text-sm',
{
'flex-row-reverse': !isAI && isYou,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { createStoreSubscriber, truncateText } from '@llm/commons';

import type { AIStreamContent, AIStreamObservable } from '../../hooks';

import { MessageMarkdown, useContentHydration } from './parser';
import { ChatMessageMarkdown } from './chat-message-markdown';
import { useContentHydration } from './parser';

type Props = {
content: string | AIStreamObservable;
Expand Down Expand Up @@ -76,7 +77,7 @@ export const ChatMessageContent = memo((
{!truncate && showToolbars && hydrationResult.prependToolbars}

<div className={clsx('max-w-[800px]', textClassName)}>
<MessageMarkdown
<ChatMessageMarkdown
content={hydrationResult.content}
inlinedReactComponents={hydrationResult.inlinedReactComponents}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import clsx from 'clsx';
import { Children, memo } from 'react';
import Markdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';

import { ChatCodeBlock } from './widgets';

type Props = {
content: string;
inlinedReactComponents?: Record<string, React.ReactNode>;
};

export const ChatMessageMarkdown = memo(({ content, inlinedReactComponents = {} }: Props) => {
return (
<Markdown
className={clsx(
'prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-gray-300 prose-th:border-gray-300 prose-table:border-collapse',
'prose-a:underline prose-code:overflow-auto',
'prose-ol:list-decimal chat-markdown prose-sm prose-ul:list-disc',
'prose-hr:my-3',
'[&_.footnotes]:text-xs [&_.footnotes]:text-gray-600',
'[&_pre]:!p-0 [&_pre]:!m-0 [&_pre]:!bg-transparent',
)}
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
components={{
blockquote: ({ children }) => (
<blockquote className="my-2 pl-4 border-gray-300 border-l-4 italic">{children}</blockquote>
),
a: ({ children, ...props }) => {
if (typeof children === 'string' && children === '$embed') {
const key = props.href?.replace(/^react\$/, '');

if (key && inlinedReactComponents[key]) {
return inlinedReactComponents[key];
}
}

return <a {...props}>{children}</a>;
},
pre: ({ children }) => {
const hasCodeTag = (
Children
// eslint-disable-next-line react/no-children-to-array
.toArray(children)
.some((child: any) => child.props?.className?.startsWith('language-'))
);

if (hasCodeTag) {
return <>{children}</>;
}

return <pre className="bg-gray-100 p-2 rounded-md">{children}</pre>;
},
code({ node, inline, className, children, ...props }: any) {
const match = /language-(\w+)/.exec(className || '');

return !inline && match
? (
<ChatCodeBlock language={match[1]}>
{String(children).replace(/\n$/, '')}
</ChatCodeBlock>
)
: (
<code className={className} {...props}>
{children}
</code>
);
},
}}
>
{content}
</Markdown>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ export * from './hydrate-with-app-chat-badges';
export * from './hydrate-with-chat-actions';
export * from './hydrate-with-projects-embeddings-badges';
export * from './hydrate-with-web-search-sources';
export * from './message-markdown';
export * from './use-content-hydration';

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Check, Copy } from 'lucide-react';
import { useState } from 'react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneLight } from 'react-syntax-highlighter/dist/cjs/styles/prism';

import { useI18n } from '~/i18n';

type ChatCodeBlockProps = {
language?: string;
children: string;
};

export function ChatCodeBlock({ language, children }: ChatCodeBlockProps) {
const t = useI18n().pack.chat.widgets.code;
const [copied, setCopied] = useState(false);

const handleCopy = async () => {
await navigator.clipboard.writeText(children);

setCopied(true);
setTimeout(() => setCopied(false), 2000);
};

return (
<div className="relative bg-[#f7f7f8] border border-gray-200 rounded-md">
<div className="flex justify-between items-center px-4 pt-2">
<span className="text-gray-600 text-xs">{language || 'plaintext'}</span>
<button
type="button"
onClick={handleCopy}
className="flex items-center gap-1 text-gray-600 hover:text-gray-900 text-xs transition-colors"
>
{copied ? <Check className="w-3 h-3" /> : <Copy className="w-3 h-3" />}
{copied ? t.copied : t.copy}
</button>
</div>

<SyntaxHighlighter
language={language || 'plaintext'}
style={oneLight}
customStyle={{
margin: 0,
padding: '1rem',
background: 'transparent',
}}
PreTag="div"
>
{children}
</SyntaxHighlighter>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './chat-code-block';
Loading

0 comments on commit c122a9c

Please sign in to comment.