Skip to content

Commit b7e448b

Browse files
authored
Enhance chat and markdown content rendering experience. (#68)
1 parent c366f7a commit b7e448b

File tree

14 files changed

+263
-255
lines changed

14 files changed

+263
-255
lines changed

app/chat/PersonaModal.tsx

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,58 @@
11
import React, { useContext, useEffect } from 'react'
22
import { Button, Dialog, Flex, TextArea, TextField } from '@radix-ui/themes'
33
import { useForm } from 'react-hook-form'
4-
import { ChatContext, Persona } from '@/components'
4+
import ChatContext from '@/components/Chat/chatContext'
5+
import type { Persona } from '@/components/Chat/interface'
56

67
const PersonaModal = () => {
78
const {
89
isOpenPersonaModal: open,
910
editPersona: detail,
10-
onCreatePersona,
11+
onCreateOrUpdatePersona,
1112
onClosePersonaModal
1213
} = useContext(ChatContext)
1314

14-
const { register, handleSubmit, setValue } = useForm()
15-
16-
const formSubmit = handleSubmit((values: any) => {
17-
onCreatePersona?.(values as Persona)
18-
})
15+
const { register, handleSubmit, setValue, reset } = useForm<{
16+
name: string
17+
prompt: string
18+
role: string
19+
}>()
1920

2021
useEffect(() => {
21-
if (detail) {
22-
setValue('name', detail.name, { shouldTouch: true })
23-
setValue('prompt', detail.prompt, { shouldTouch: true })
22+
if (open && detail) {
23+
setValue('name', detail.name ?? '')
24+
setValue('prompt', detail.prompt ?? '')
25+
} else if (open && !detail) {
26+
reset({ name: '', prompt: '' })
27+
}
28+
if (!open) {
29+
reset({ name: '', prompt: '' })
2430
}
25-
}, [detail, setValue])
31+
}, [open, detail, setValue, reset])
32+
33+
const formSubmit = handleSubmit(({ name, prompt }) => {
34+
const persona: Persona = { name, prompt, role: 'assistant' }
35+
onCreateOrUpdatePersona?.(persona)
36+
})
2637

2738
return (
28-
<Dialog.Root open={open!}>
39+
<Dialog.Root open={!!open}>
2940
<Dialog.Content size="4">
30-
<Dialog.Title>Create or Edit Persona Prompt</Dialog.Title>
41+
<Dialog.Title>{detail ? 'Edit Persona Prompt' : 'Create Persona Prompt'}</Dialog.Title>
3142
<Dialog.Description size="2" mb="4"></Dialog.Description>
3243
<form onSubmit={formSubmit}>
3344
<Flex direction="column" gap="3">
34-
<TextField.Root placeholder="Name" {...register('name', { required: true })} />
35-
<TextArea placeholder="Prompt" rows={7} {...register('prompt', { required: true })} />
45+
<TextField.Root
46+
placeholder="Name"
47+
{...register('name', { required: true })}
48+
autoComplete="off"
49+
/>
50+
<TextArea
51+
placeholder="Prompt"
52+
rows={7}
53+
{...register('prompt', { required: true })}
54+
autoComplete="off"
55+
/>
3656
</Flex>
3757
<Flex gap="3" mt="4" justify="end">
3858
<Dialog.Close>

app/chat/PersonaPanel.tsx

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import { ChatContext, Persona } from '@/components'
2020

2121
const PersonaPanel = () => {
2222
const {
23-
personaPanelType,
2423
DefaultPersonas,
2524
personas,
2625
openPersonaPanel,
@@ -35,27 +34,17 @@ const PersonaPanel = () => {
3534
const [searchText, setSearchText] = useState('')
3635

3736
const handleSearch = useCallback(
38-
debounce((type: string, list: Persona[], searchText: string) => {
37+
debounce((list: Persona[], searchText: string) => {
3938
setPromptList(
40-
list.filter((item) => {
41-
if (type === 'chat') {
42-
return (
43-
!item.key && (item.prompt?.includes(searchText) || item.name?.includes(searchText))
44-
)
45-
} else {
46-
return (
47-
item.key && (item.prompt?.includes(searchText) || item.name?.includes(searchText))
48-
)
49-
}
50-
})
39+
list.filter((item) => item.prompt?.includes(searchText) || item.name?.includes(searchText))
5140
)
5241
}, 350),
5342
[]
5443
)
5544

5645
useEffect(() => {
57-
handleSearch(personaPanelType, [...DefaultPersonas, ...personas], searchText)
58-
}, [personaPanelType, searchText, DefaultPersonas, personas, handleSearch])
46+
handleSearch([...DefaultPersonas, ...personas], searchText)
47+
}, [searchText, DefaultPersonas, personas, handleSearch])
5948

6049
return openPersonaPanel ? (
6150
<Flex

app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const metadata: Metadata = {
2222

2323
export default function RootLayout({ children }: { children: React.ReactNode }) {
2424
return (
25-
<html lang="en" className="light" style={{ colorScheme: 'light' }}>
25+
<html lang="en" suppressHydrationWarning>
2626
<body>
2727
<ThemesProvider>
2828
<Header />

components/Chat/Chat.tsx

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
useRef,
1010
useState
1111
} from 'react'
12-
import { Flex, Heading, IconButton, ScrollArea, Tooltip } from '@radix-ui/themes'
12+
import { Flex, Heading, IconButton, ScrollArea, Tooltip, Button } from '@radix-ui/themes'
1313
import ContentEditable from 'react-contenteditable'
1414
import { AiOutlineClear, AiOutlineLoading3Quarters, AiOutlineUnorderedList } from 'react-icons/ai'
1515
import { FiSend } from 'react-icons/fi'
@@ -205,39 +205,43 @@ const Chat = (props: ChatProps, ref: any) => {
205205
return (
206206
<Flex direction="column" height="100%" className="relative" gap="3">
207207
<Flex
208-
justify="between"
208+
justify="center"
209209
align="center"
210210
py="3"
211211
px="4"
212-
style={{ backgroundColor: 'var(--gray-a2)' }}
212+
style={{
213+
backgroundColor: 'var(--gray-a2)',
214+
borderBottom: '1px solid var(--gray-a4)',
215+
position: 'relative'
216+
}}
213217
>
214-
<Heading size="4">{currentChatRef?.current?.persona?.name || 'No Persona'}</Heading>
215-
<Flex gap="2" align="center">
216-
<Tooltip content="Clear History">
217-
<IconButton
218-
variant="soft"
219-
color="gray"
220-
size="2"
221-
className="rounded-xl cursor-pointer"
222-
disabled={isLoading}
223-
onClick={clearMessages}
224-
>
225-
<AiOutlineClear className="size-5" />
226-
</IconButton>
227-
</Tooltip>
218+
<Flex gap="2" align="center" className="absolute left-4 top-1/2 -translate-y-1/2 md:hidden">
228219
<Tooltip content="Toggle Sidebar">
229220
<IconButton
230221
variant="soft"
231222
color="gray"
232223
size="2"
233-
className="rounded-lg md:hidden cursor-pointer"
224+
className="rounded-lg cursor-pointer"
234225
disabled={isLoading}
235226
onClick={onToggleSidebar}
236227
>
237228
<AiOutlineUnorderedList className="size-5" />
238229
</IconButton>
239230
</Tooltip>
240231
</Flex>
232+
<Flex align="center" width="100%" justify="center" gap="1">
233+
<Heading
234+
size="4"
235+
style={{
236+
flex: 'none',
237+
textAlign: 'center',
238+
fontWeight: 600,
239+
letterSpacing: 0.5
240+
}}
241+
>
242+
{currentChatRef?.current?.persona?.name || 'No Persona'}
243+
</Heading>
244+
</Flex>
241245
</Flex>
242246
<ScrollArea
243247
className="flex-1 px-4"
@@ -252,6 +256,23 @@ const Chat = (props: ChatProps, ref: any) => {
252256
<div ref={bottomOfChatRef}></div>
253257
</ScrollArea>
254258
<div className="px-4 pb-3">
259+
{conversation.current.length > 0 && (
260+
<Flex justify="start" mb="2">
261+
<Button
262+
variant="soft"
263+
color="gray"
264+
size="2"
265+
className="rounded-xl cursor-pointer"
266+
disabled={isLoading}
267+
onClick={clearMessages}
268+
tabIndex={0}
269+
style={{ gap: 8, display: 'flex', alignItems: 'center' }}
270+
>
271+
<AiOutlineClear className="size-5" />
272+
Clear Chat History
273+
</Button>
274+
</Flex>
275+
)}
255276
<Flex align="end" justify="between" gap="3" className="relative">
256277
<div className="rt-TextAreaRoot rt-r-size-1 rt-variant-surface flex-1 rounded-3xl chat-textarea">
257278
<ContentEditable
@@ -287,7 +308,7 @@ const Chat = (props: ChatProps, ref: any) => {
287308
)}
288309
<Tooltip content="Send Message">
289310
<IconButton
290-
variant="surface"
311+
variant="soft"
291312
disabled={isLoading}
292313
color="gray"
293314
size="2"

components/Chat/ChatSideBar.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
'use client'
22

33
import React, { useContext } from 'react'
4-
import { Box, Flex, IconButton, ScrollArea, Text } from '@radix-ui/themes'
4+
import { Avatar, Box, Flex, IconButton, ScrollArea, Text } from '@radix-ui/themes'
55
import clsx from 'clsx'
66
import { AiOutlineCloseCircle } from 'react-icons/ai'
7-
import { BiMessageDetail } from 'react-icons/bi'
8-
import { FiPlus } from 'react-icons/fi'
7+
import { BiEdit, BiMessageDetail } from 'react-icons/bi'
98
import { RiRobot2Line } from 'react-icons/ri'
109
import ChatContext from './chatContext'
1110

@@ -20,18 +19,22 @@ export const ChatSideBar = () => {
2019
onDeleteChat,
2120
onChangeChat,
2221
onCreateChat,
23-
onOpenPersonaPanel
22+
onOpenPersonaPanel,
23+
onToggleSidebar
2424
} = useContext(ChatContext)
2525

2626
return (
2727
<Flex direction="column" className={clsx('chat-side-bar', { show: toggleSidebar })}>
2828
<Flex className="p-2 h-full overflow-hidden w-64" direction="column" gap="3">
2929
<Box
3030
width="auto"
31-
onClick={() => onCreateChat?.(DefaultPersonas[0])}
31+
onClick={() => {
32+
onCreateChat?.(DefaultPersonas[0])
33+
onToggleSidebar?.()
34+
}}
3235
className="bg-token-surface-primary active:scale-95 cursor-pointer"
3336
>
34-
<FiPlus className="size-4" />
37+
<Avatar fallback={<BiEdit className="size-6" />} />
3538
<Text>New Chat</Text>
3639
</Box>
3740
<ScrollArea className="flex-1 " style={{ width: '100%' }} type="auto">
@@ -43,7 +46,10 @@ export const ChatSideBar = () => {
4346
className={clsx('bg-token-surface active:scale-95 truncate cursor-pointer', {
4447
active: currentChatRef?.current?.id === chat.id
4548
})}
46-
onClick={() => onChangeChat?.(chat)}
49+
onClick={() => {
50+
onChangeChat?.(chat)
51+
onToggleSidebar?.()
52+
}}
4753
>
4854
<Flex gap="2" align="center" className="overflow-hidden whitespace-nowrap">
4955
<BiMessageDetail className="size-4" />

components/Chat/Message.tsx

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

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

2423
const onCopy = useCallback(() => {
2524
copy(content, (isSuccess) => {
2625
if (isSuccess) {
27-
setTooltipText('Copied!')
28-
setTooltipOpen(true)
26+
setCopied(true)
2927
setTimeout(() => {
30-
setTooltipText('Copy response')
31-
setTooltipOpen(false)
32-
}, 1000)
28+
setCopied(false)
29+
}, 1500)
3330
}
3431
})
3532
}, [content, copy])
@@ -45,10 +42,9 @@ const Message = (props: MessageProps) => {
4542
<div className="flex-1 pt-1 break-all">
4643
{isUser ? (
4744
<div
48-
className="userMessage"
4945
dangerouslySetInnerHTML={{
5046
__html: sanitizeHtml(content, {
51-
allowedTags: ['br', 'img', 'table', 'thead', 'tbody', 'tr', 'td', 'th'],
47+
allowedTags: ['br', 'img', 'table', 'thead', 'tbody', 'tr', 'td', 'th', 'div'],
5248
allowedAttributes: {
5349
img: ['src', 'alt'],
5450
'*': ['class']
@@ -59,28 +55,21 @@ const Message = (props: MessageProps) => {
5955
) : (
6056
<Flex direction="column" gap="4">
6157
<Markdown>{content}</Markdown>
62-
<Flex gap="4" align="center">
63-
<Tooltip
64-
open={tooltipOpen}
65-
content={tooltipText}
66-
onOpenChange={setTooltipOpen}
67-
delayDuration={200}
68-
>
69-
<IconButton
70-
className="cursor-pointer"
71-
variant="outline"
72-
color="gray"
58+
<Flex gap="2" align="center" className="copy-btn-group">
59+
<Tooltip content={copied ? 'Copied!' : 'Copy to clipboard'} delayDuration={200}>
60+
<Button
61+
variant="soft"
62+
color={copied ? 'green' : 'gray'}
63+
size="2"
64+
className="rounded-xl cursor-pointer"
65+
disabled={copied}
7366
onClick={onCopy}
74-
onMouseEnter={() => {
75-
setTooltipText('Copy response')
76-
setTooltipOpen(true)
77-
}}
78-
onMouseLeave={() => {
79-
setTooltipOpen(false)
80-
}}
67+
tabIndex={0}
68+
style={{ gap: 8, display: 'flex', alignItems: 'center' }}
8169
>
82-
<FaRegCopy />
83-
</IconButton>
70+
<FaRegCopy className="size-5" />
71+
{copied ? 'Copied' : 'Copy'}
72+
</Button>
8473
</Tooltip>
8574
</Flex>
8675
</Flex>

components/Chat/chatContext.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { Chat, ChatMessage, Persona } from './interface'
55

66
const ChatContext = createContext<{
77
debug?: boolean
8-
personaPanelType: string
98
DefaultPersonas: Persona[]
109
currentChatRef?: MutableRefObject<Chat | undefined>
1110
chatList: Chat[]
@@ -18,7 +17,7 @@ const ChatContext = createContext<{
1817
onOpenPersonaModal?: () => void
1918
onClosePersonaModal?: () => void
2019
setCurrentChat?: (chat: Chat) => void
21-
onCreatePersona?: (persona: Persona) => void
20+
onCreateOrUpdatePersona?: (persona: Persona) => void
2221
onDeleteChat?: (chat: Chat) => void
2322
onDeletePersona?: (persona: Persona) => void
2423
onEditPersona?: (persona: Persona) => void
@@ -30,7 +29,6 @@ const ChatContext = createContext<{
3029
onToggleSidebar?: () => void
3130
forceUpdate?: () => void
3231
}>({
33-
personaPanelType: 'chat',
3432
DefaultPersonas: [],
3533
chatList: [],
3634
personas: []

0 commit comments

Comments
 (0)