@@ -3,47 +3,13 @@ import "./tailwind.css";
33
44import clsx from "clsx" ;
55import type React from "react" ;
6- import { memo , useCallback , useEffect , useRef , useState } from "react" ;
6+ import { memo , useCallback , useEffect , useRef } from "react" ;
77import Markdown from "react-markdown" ;
8- import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" ;
9- import {
10- oneLight ,
11- vscDarkPlus ,
12- } from "react-syntax-highlighter/dist/esm/styles/prism" ;
138import remarkGfm from "remark-gfm" ;
149import remarkMath from "remark-math" ;
1510import { twMerge } from "tailwind-merge" ;
16- import type { MessageParam } from "./utils" ;
17-
18- const useTheme = ( ) => {
19- const [ isDark , setDark ] = useState ( false ) ;
20-
21- useEffect ( ( ) => {
22- const checkDarkMode = ( ) => {
23- setDark ( document . documentElement . classList . contains ( "dark" ) ) ;
24- } ;
25- checkDarkMode ( ) ;
26-
27- const observer = new MutationObserver ( checkDarkMode ) ;
28- observer . observe ( document . documentElement , {
29- attributes : true ,
30- attributeFilter : [ "class" ] ,
31- } ) ;
32-
33- const mediaQuery = window . matchMedia ( "(prefers-color-scheme: dark)" ) ;
34- const handleSystemThemeChange = ( event : MediaQueryListEvent ) => {
35- setDark ( event . matches ) ;
36- } ;
37- mediaQuery . addEventListener ( "change" , handleSystemThemeChange ) ;
38-
39- return ( ) => {
40- observer . disconnect ( ) ;
41- mediaQuery . removeEventListener ( "change" , handleSystemThemeChange ) ;
42- } ;
43- } , [ ] ) ;
44-
45- return { isDark } ;
46- } ;
11+ import type { MessageParam } from "../utils" ;
12+ import { BlockQuote , CodeBlock , Heading , Link } from "./markdown" ;
4713
4814const bubbleVariants = cva (
4915 "flex flex-col gap-1 justify-center rounded-lg dark:text-gray-200 text-gray-800 max-w-full overflow-x-auto" ,
@@ -111,7 +77,6 @@ export function Bubble({
11177 isPending = false ,
11278 ...props
11379} : BubbleProps ) {
114- const { isDark } = useTheme ( ) ;
11580
11681 const defaultPending = (
11782 < div className = "flex items-center space-x-1 py-1" >
@@ -149,130 +114,15 @@ export function Bubble({
149114 < Markdown
150115 remarkPlugins = { [ remarkGfm , remarkMath ] }
151116 components = { {
152- h1 ( props ) {
153- const { children, className, node : _node , ...rest } = props ;
154- return (
155- < h1
156- { ...rest }
157- className = { clsx ( "my-3 text-2xl font-bold" , className ) }
158- >
159- { children }
160- </ h1 >
161- ) ;
162- } ,
163- h2 ( props ) {
164- const { children, className, node : _node , ...rest } = props ;
165- return (
166- < h2
167- { ...rest }
168- className = { clsx ( "my-2 text-xl font-bold" , className ) }
169- >
170- { children }
171- </ h2 >
172- ) ;
173- } ,
174- h3 ( props ) {
175- const { children, className, node : _node , ...rest } = props ;
176- return (
177- < h3
178- { ...rest }
179- className = { clsx ( "my-1 text-lg font-bold" , className ) }
180- >
181- { children }
182- </ h3 >
183- ) ;
184- } ,
185- code ( props ) {
186- const { children, className, ref : _ref , ...rest } = props ;
187- const match = / l a n g u a g e - ( \w + ) / . exec ( className || "" ) ;
188-
189- const [ copied , setCopied ] = useState ( false ) ;
190- const handleCopy = ( ) => {
191- navigator . clipboard . writeText ( String ( children ) . replace ( / \n $ / , '' ) ) ;
192- setCopied ( true ) ;
193- setTimeout ( ( ) => setCopied ( false ) , 2000 ) ;
194- } ;
195-
196- return match ? (
197- < div
198- className = { clsx (
199- "w-full overflow-x-auto rounded-lg" ,
200- "bg-gray-50 dark:bg-gray-800" ,
201- ) }
202- >
203- < div className = "inline-flex w-full justify-between bg-gray-100 p-2" >
204- < div className = "px-2 py-1 text-xs text-gray-900 dark:text-gray-400" >
205- { match [ 1 ] }
206- </ div >
207- < div
208- className = "px-2 py-1 text-xs text-gray-900 dark:text-gray-400 cursor-pointer"
209- onClick = { handleCopy }
210- >
211- { copied ? "Copied" : "Copy" }
212- </ div >
213- </ div >
214- < SyntaxHighlighter
215- { ...rest }
216- PreTag = "div"
217- language = { match [ 1 ] }
218- style = { isDark ? vscDarkPlus : oneLight }
219- customStyle = { {
220- background : "transparent" ,
221- margin : 0 ,
222- padding : "1rem" ,
223- borderRadius : "0.5rem" ,
224- overflowX : "auto" ,
225- } }
226- codeTagProps = { {
227- style : {
228- fontFamily : "monospace" ,
229- fontSize : "0.875rem" ,
230- } ,
231- } }
232- >
233- { String ( children ) . replace ( / \n $ / , "" ) }
234- </ SyntaxHighlighter >
235- </ div >
236- ) : (
237- < code
238- { ...rest }
239- className = { clsx (
240- "rounded-md px-1 py-0.5 text-[85%]" ,
241- "bg-gray-100 dark:bg-gray-800" ,
242- ) }
243- >
244- { children }
245- </ code >
246- ) ;
247- } ,
248- blockquote ( props ) {
249- const { children, className, ...rest } = props ;
250- return (
251- < blockquote
252- { ...rest }
253- className = { clsx (
254- "border-l-4 border-gray-300 pl-4 italic" ,
255- className ,
256- ) }
257- >
258- { children }
259- </ blockquote >
260- ) ;
261- } ,
262- a ( props ) {
263- const { children, className, ref : _ref , ...rest } = props ;
264- return (
265- < a
266- { ...rest }
267- className = { clsx (
268- "text-blue-600 dark:text-blue-400 hover:underline underline-offset-1" ,
269- className ,
270- ) }
271- >
272- { children }
273- </ a >
274- ) ;
275- } ,
117+ a : Link ,
118+ code : CodeBlock ,
119+ blockquote : BlockQuote ,
120+ h1 : ( props ) => < Heading { ...props } level = { 1 } /> ,
121+ h2 : ( props ) => < Heading { ...props } level = { 2 } /> ,
122+ h3 : ( props ) => < Heading { ...props } level = { 3 } /> ,
123+ h4 : ( props ) => < Heading { ...props } level = { 4 } /> ,
124+ h5 : ( props ) => < Heading { ...props } level = { 5 } /> ,
125+ h6 : ( props ) => < Heading { ...props } level = { 6 } /> ,
276126 } }
277127 >
278128 { text }
0 commit comments