Chat with me
Demo chat for testing.
React + TailwindCSS
Simple chat example written in React with Tailwindcss.
Uses marked for parsing markdown and github-markdown-css for styling it.
This example also uses lucide icons but feel free to replace them with any other you’d like.
Install dependencies.
npm i marked github-markdown-css dompurify clsx lucide-reactUsage:
chat.tsx
import React, { useRef, useState } from 'react';
import clsx from 'clsx';
import DOMPurify from 'dompurify';
import { marked } from 'marked';
import { ChevronDown, ChevronUp, Send } from 'lucide-react';
import 'github-markdown-css/github-markdown-dark.css';
const CLIENT_ID = 'CLIENT_ID'; // Replace with your Client ID.
interface TMessage {
role: 'user' | 'assistant';
content: string;
}
/* Component for displaying Markdown */
function Markdown({ content }: { content: string }) {
const markdownHtml = marked(content, { async: false });
return (
<span
className="markdown-body"
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(markdownHtml),
}}
/>
);
}
function Message({ message }: { message: TMessage }) {
return (
<div
className={clsx(
'flex w-full',
message.role === 'user' ? 'justify-end' : 'gap-2',
)}
>
<div
className={clsx(
'break-words',
message.role === 'assistant'
? 'w-full'
: 'max-w-[60%] bg-gray-500/50 rounded-lg p-4',
)}
>
<Markdown content={message.content} />
</div>
</div>
);
}
export function Chat() {
const [isCollapsed, setIsCollapsed] = useState(true);
const [messages, setMessages] = useState<TMessage[]>([]);
const [input, setInput] = useState('');
const [isBotTyping, setIsBotTyping] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const isDisabled =
isLoading || isBotTyping || !inputRef.current?.value.trim().length;
async function submit() {
if (!input.trim().length) return;
setIsLoading(true);
setIsBotTyping(true);
try {
setMessages((prev) => [
...prev,
{ role: 'user', content: input },
{ role: 'assistant', content: '' },
]);
const { body } = await fetch(
'https://chatfolio.me/api/v1/messages',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
messages,
message: input,
clientId: CLIENT_ID,
stream: true,
}),
},
);
const reader = body!.getReader();
const decoder = new TextDecoder('utf-8');
let contentBuffer = '';
inputRef.current!.value = '';
setInput('');
setIsLoading(false);
while (true) {
const { value, done } = await reader.read();
if (value) {
const chunk = decoder.decode(value);
contentBuffer += chunk;
setMessages((prev) => {
const updatedMessages = [...prev];
updatedMessages[messages.length + 1].content =
contentBuffer;
return updatedMessages;
});
}
if (done) break;
}
} catch (error) {
console.error(error);
setMessages((prev) => prev.slice(0, -1));
} finally {
setIsLoading(false);
setIsBotTyping(false);
}
}
return (
<div className="fixed bottom-2 right-2 z-50">
<div
className={clsx(
'w-96 rounded-lg flex flex-col border border-gray-700 bg-gray-800 bg-base-100 border-base-300 [&>div]:p-4',
isCollapsed ? 'h-min' : 'h-[500px]',
)}
style={{ width: '400px' }}
>
<div
className={clsx(
'py-2 px-4 mt-1 flex justify-between items-center w-full cursor-pointer',
!isCollapsed && 'border-b border-b-base-300',
)}
onClick={() => setIsCollapsed((prev) => !prev)}
>
<div className="space-y-2">
<h2 className="text-white font-bold text-2xl">
Chat with me
</h2>
<p className="text-gray-300 text-sm">
Demo chat for testing.
</p>
</div>
<div>{isCollapsed ? <ChevronUp /> : <ChevronDown />}</div>
</div>
<div
className={clsx(
'flex flex-col w-full h-full p-4 overflow-y-auto gap-2',
isCollapsed && 'hidden',
)}
ref={containerRef}
>
{messages
.filter((message) => message.content)
.map((message, index) => (
<Message key={index} message={message} />
))}
{isLoading && <p className="text-white">Thinking...</p>}
<div ref={messagesEndRef} />
</div>
<div
className={clsx(
'flex items-center relative w-full',
isCollapsed && 'hidden',
)}
>
<input
type="text"
className="flex h-10 w-full rounded-xl p-4 text-base bg-transparent outline-none"
placeholder="Ask me anything!"
ref={inputRef}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
submit();
}
}}
/>
<button
className="bg-transparent border-none absolute right-4 disabled:opacity-50"
disabled={isDisabled}
onClick={() => submit()}
>
<Send />
</button>
</div>
</div>
</div>
);
}