Skip to content

Enhance chat and markdown content rendering experience. #68

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 1, 2025
Merged
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
50 changes: 35 additions & 15 deletions app/chat/PersonaModal.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,58 @@
import React, { useContext, useEffect } from 'react'
import { Button, Dialog, Flex, TextArea, TextField } from '@radix-ui/themes'
import { useForm } from 'react-hook-form'
import { ChatContext, Persona } from '@/components'
import ChatContext from '@/components/Chat/chatContext'
import type { Persona } from '@/components/Chat/interface'

const PersonaModal = () => {
const {
isOpenPersonaModal: open,
editPersona: detail,
onCreatePersona,
onCreateOrUpdatePersona,
onClosePersonaModal
} = useContext(ChatContext)

const { register, handleSubmit, setValue } = useForm()

const formSubmit = handleSubmit((values: any) => {
onCreatePersona?.(values as Persona)
})
const { register, handleSubmit, setValue, reset } = useForm<{
name: string
prompt: string
role: string
}>()

useEffect(() => {
if (detail) {
setValue('name', detail.name, { shouldTouch: true })
setValue('prompt', detail.prompt, { shouldTouch: true })
if (open && detail) {
setValue('name', detail.name ?? '')
setValue('prompt', detail.prompt ?? '')
} else if (open && !detail) {
reset({ name: '', prompt: '' })
}
if (!open) {
reset({ name: '', prompt: '' })
}
}, [detail, setValue])
}, [open, detail, setValue, reset])

const formSubmit = handleSubmit(({ name, prompt }) => {
const persona: Persona = { name, prompt, role: 'assistant' }
onCreateOrUpdatePersona?.(persona)
})

return (
<Dialog.Root open={open!}>
<Dialog.Root open={!!open}>
<Dialog.Content size="4">
<Dialog.Title>Create or Edit Persona Prompt</Dialog.Title>
<Dialog.Title>{detail ? 'Edit Persona Prompt' : 'Create Persona Prompt'}</Dialog.Title>
<Dialog.Description size="2" mb="4"></Dialog.Description>
<form onSubmit={formSubmit}>
<Flex direction="column" gap="3">
<TextField.Root placeholder="Name" {...register('name', { required: true })} />
<TextArea placeholder="Prompt" rows={7} {...register('prompt', { required: true })} />
<TextField.Root
placeholder="Name"
{...register('name', { required: true })}
autoComplete="off"
/>
<TextArea
placeholder="Prompt"
rows={7}
{...register('prompt', { required: true })}
autoComplete="off"
/>
</Flex>
<Flex gap="3" mt="4" justify="end">
<Dialog.Close>
Expand Down
19 changes: 4 additions & 15 deletions app/chat/PersonaPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { ChatContext, Persona } from '@/components'

const PersonaPanel = () => {
const {
personaPanelType,
DefaultPersonas,
personas,
openPersonaPanel,
Expand All @@ -35,27 +34,17 @@ const PersonaPanel = () => {
const [searchText, setSearchText] = useState('')

const handleSearch = useCallback(
debounce((type: string, list: Persona[], searchText: string) => {
debounce((list: Persona[], searchText: string) => {
setPromptList(
list.filter((item) => {
if (type === 'chat') {
return (
!item.key && (item.prompt?.includes(searchText) || item.name?.includes(searchText))
)
} else {
return (
item.key && (item.prompt?.includes(searchText) || item.name?.includes(searchText))
)
}
})
list.filter((item) => item.prompt?.includes(searchText) || item.name?.includes(searchText))
)
}, 350),
[]
)

useEffect(() => {
handleSearch(personaPanelType, [...DefaultPersonas, ...personas], searchText)
}, [personaPanelType, searchText, DefaultPersonas, personas, handleSearch])
handleSearch([...DefaultPersonas, ...personas], searchText)
}, [searchText, DefaultPersonas, personas, handleSearch])

return openPersonaPanel ? (
<Flex
Expand Down
2 changes: 1 addition & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const metadata: Metadata = {

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className="light" style={{ colorScheme: 'light' }}>
<html lang="en" suppressHydrationWarning>
<body>
<ThemesProvider>
<Header />
Expand Down
59 changes: 40 additions & 19 deletions components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
useRef,
useState
} from 'react'
import { Flex, Heading, IconButton, ScrollArea, Tooltip } from '@radix-ui/themes'
import { Flex, Heading, IconButton, ScrollArea, Tooltip, Button } from '@radix-ui/themes'
import ContentEditable from 'react-contenteditable'
import { AiOutlineClear, AiOutlineLoading3Quarters, AiOutlineUnorderedList } from 'react-icons/ai'
import { FiSend } from 'react-icons/fi'
Expand Down Expand Up @@ -205,39 +205,43 @@ const Chat = (props: ChatProps, ref: any) => {
return (
<Flex direction="column" height="100%" className="relative" gap="3">
<Flex
justify="between"
justify="center"
align="center"
py="3"
px="4"
style={{ backgroundColor: 'var(--gray-a2)' }}
style={{
backgroundColor: 'var(--gray-a2)',
borderBottom: '1px solid var(--gray-a4)',
position: 'relative'
}}
>
<Heading size="4">{currentChatRef?.current?.persona?.name || 'No Persona'}</Heading>
<Flex gap="2" align="center">
<Tooltip content="Clear History">
<IconButton
variant="soft"
color="gray"
size="2"
className="rounded-xl cursor-pointer"
disabled={isLoading}
onClick={clearMessages}
>
<AiOutlineClear className="size-5" />
</IconButton>
</Tooltip>
<Flex gap="2" align="center" className="absolute left-4 top-1/2 -translate-y-1/2 md:hidden">
<Tooltip content="Toggle Sidebar">
<IconButton
variant="soft"
color="gray"
size="2"
className="rounded-lg md:hidden cursor-pointer"
className="rounded-lg cursor-pointer"
disabled={isLoading}
onClick={onToggleSidebar}
>
<AiOutlineUnorderedList className="size-5" />
</IconButton>
</Tooltip>
</Flex>
<Flex align="center" width="100%" justify="center" gap="1">
<Heading
size="4"
style={{
flex: 'none',
textAlign: 'center',
fontWeight: 600,
letterSpacing: 0.5
}}
>
{currentChatRef?.current?.persona?.name || 'No Persona'}
</Heading>
</Flex>
</Flex>
<ScrollArea
className="flex-1 px-4"
Expand All @@ -252,6 +256,23 @@ const Chat = (props: ChatProps, ref: any) => {
<div ref={bottomOfChatRef}></div>
</ScrollArea>
<div className="px-4 pb-3">
{conversation.current.length > 0 && (
<Flex justify="start" mb="2">
<Button
variant="soft"
color="gray"
size="2"
className="rounded-xl cursor-pointer"
disabled={isLoading}
onClick={clearMessages}
tabIndex={0}
style={{ gap: 8, display: 'flex', alignItems: 'center' }}
>
<AiOutlineClear className="size-5" />
Clear Chat History
</Button>
</Flex>
)}
<Flex align="end" justify="between" gap="3" className="relative">
<div className="rt-TextAreaRoot rt-r-size-1 rt-variant-surface flex-1 rounded-3xl chat-textarea">
<ContentEditable
Expand Down Expand Up @@ -287,7 +308,7 @@ const Chat = (props: ChatProps, ref: any) => {
)}
<Tooltip content="Send Message">
<IconButton
variant="surface"
variant="soft"
disabled={isLoading}
color="gray"
size="2"
Expand Down
20 changes: 13 additions & 7 deletions components/Chat/ChatSideBar.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
'use client'

import React, { useContext } from 'react'
import { Box, Flex, IconButton, ScrollArea, Text } from '@radix-ui/themes'
import { Avatar, Box, Flex, IconButton, ScrollArea, Text } from '@radix-ui/themes'
import clsx from 'clsx'
import { AiOutlineCloseCircle } from 'react-icons/ai'
import { BiMessageDetail } from 'react-icons/bi'
import { FiPlus } from 'react-icons/fi'
import { BiEdit, BiMessageDetail } from 'react-icons/bi'
import { RiRobot2Line } from 'react-icons/ri'
import ChatContext from './chatContext'

Expand All @@ -20,18 +19,22 @@ export const ChatSideBar = () => {
onDeleteChat,
onChangeChat,
onCreateChat,
onOpenPersonaPanel
onOpenPersonaPanel,
onToggleSidebar
} = useContext(ChatContext)

return (
<Flex direction="column" className={clsx('chat-side-bar', { show: toggleSidebar })}>
<Flex className="p-2 h-full overflow-hidden w-64" direction="column" gap="3">
<Box
width="auto"
onClick={() => onCreateChat?.(DefaultPersonas[0])}
onClick={() => {
onCreateChat?.(DefaultPersonas[0])
onToggleSidebar?.()
}}
className="bg-token-surface-primary active:scale-95 cursor-pointer"
>
<FiPlus className="size-4" />
<Avatar fallback={<BiEdit className="size-6" />} />
<Text>New Chat</Text>
</Box>
<ScrollArea className="flex-1 " style={{ width: '100%' }} type="auto">
Expand All @@ -43,7 +46,10 @@ export const ChatSideBar = () => {
className={clsx('bg-token-surface active:scale-95 truncate cursor-pointer', {
active: currentChatRef?.current?.id === chat.id
})}
onClick={() => onChangeChat?.(chat)}
onClick={() => {
onChangeChat?.(chat)
onToggleSidebar?.()
}}
>
<Flex gap="2" align="center" className="overflow-hidden whitespace-nowrap">
<BiMessageDetail className="size-4" />
Expand Down
49 changes: 19 additions & 30 deletions components/Chat/Message.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import { useCallback, useState } from 'react'
import { Avatar, Flex, IconButton, Tooltip } from '@radix-ui/themes'
import { Avatar, Flex, Tooltip, Text, Button } from '@radix-ui/themes'
import { FaRegCopy } from 'react-icons/fa6'
import { HiUser } from 'react-icons/hi'
import { RiRobot2Line } from 'react-icons/ri'
Expand All @@ -18,18 +18,15 @@ const Message = (props: MessageProps) => {
const { role, content } = props.message
const isUser = role === 'user'
const copy = useCopyToClipboard()
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false)
const [tooltipText, setTooltipText] = useState<string>('Copy response')
const [copied, setCopied] = useState<boolean>(false)

const onCopy = useCallback(() => {
copy(content, (isSuccess) => {
if (isSuccess) {
setTooltipText('Copied!')
setTooltipOpen(true)
setCopied(true)
setTimeout(() => {
setTooltipText('Copy response')
setTooltipOpen(false)
}, 1000)
setCopied(false)
}, 1500)
}
})
}, [content, copy])
Expand All @@ -45,10 +42,9 @@ const Message = (props: MessageProps) => {
<div className="flex-1 pt-1 break-all">
{isUser ? (
<div
className="userMessage"
dangerouslySetInnerHTML={{
__html: sanitizeHtml(content, {
allowedTags: ['br', 'img', 'table', 'thead', 'tbody', 'tr', 'td', 'th'],
allowedTags: ['br', 'img', 'table', 'thead', 'tbody', 'tr', 'td', 'th', 'div'],
allowedAttributes: {
img: ['src', 'alt'],
'*': ['class']
Expand All @@ -59,28 +55,21 @@ const Message = (props: MessageProps) => {
) : (
<Flex direction="column" gap="4">
<Markdown>{content}</Markdown>
<Flex gap="4" align="center">
<Tooltip
open={tooltipOpen}
content={tooltipText}
onOpenChange={setTooltipOpen}
delayDuration={200}
>
<IconButton
className="cursor-pointer"
variant="outline"
color="gray"
<Flex gap="2" align="center" className="copy-btn-group">
<Tooltip content={copied ? 'Copied!' : 'Copy to clipboard'} delayDuration={200}>
<Button
variant="soft"
color={copied ? 'green' : 'gray'}
size="2"
className="rounded-xl cursor-pointer"
disabled={copied}
onClick={onCopy}
onMouseEnter={() => {
setTooltipText('Copy response')
setTooltipOpen(true)
}}
onMouseLeave={() => {
setTooltipOpen(false)
}}
tabIndex={0}
style={{ gap: 8, display: 'flex', alignItems: 'center' }}
>
<FaRegCopy />
</IconButton>
<FaRegCopy className="size-5" />
{copied ? 'Copied' : 'Copy'}
</Button>
</Tooltip>
</Flex>
</Flex>
Expand Down
4 changes: 1 addition & 3 deletions components/Chat/chatContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { Chat, ChatMessage, Persona } from './interface'

const ChatContext = createContext<{
debug?: boolean
personaPanelType: string
DefaultPersonas: Persona[]
currentChatRef?: MutableRefObject<Chat | undefined>
chatList: Chat[]
Expand All @@ -18,7 +17,7 @@ const ChatContext = createContext<{
onOpenPersonaModal?: () => void
onClosePersonaModal?: () => void
setCurrentChat?: (chat: Chat) => void
onCreatePersona?: (persona: Persona) => void
onCreateOrUpdatePersona?: (persona: Persona) => void
onDeleteChat?: (chat: Chat) => void
onDeletePersona?: (persona: Persona) => void
onEditPersona?: (persona: Persona) => void
Expand All @@ -30,7 +29,6 @@ const ChatContext = createContext<{
onToggleSidebar?: () => void
forceUpdate?: () => void
}>({
personaPanelType: 'chat',
DefaultPersonas: [],
chatList: [],
personas: []
Expand Down
Loading