|
1 | | -import { useSnapshot } from "valtio"; |
2 | | -import RMarkdown from "react-markdown"; |
3 | | -import remarkGfm from "remark-gfm"; |
4 | | -import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; |
5 | | -import { a11yDark } from "react-syntax-highlighter/dist/esm/styles/prism"; |
6 | 1 | import { |
7 | 2 | FormEvent, |
8 | 3 | KeyboardEvent, |
9 | | - useCallback, |
| 4 | + Suspense, |
10 | 5 | useEffect, |
11 | 6 | useRef, |
12 | 7 | useState, |
13 | 8 | } from "react"; |
14 | | -import { ArrowDownCircleFill, ArrowUpSquareFill } from "react-bootstrap-icons"; |
15 | | - |
16 | | -const Mardown = ({ children }: { children: string }) => ( |
17 | | - <RMarkdown |
18 | | - remarkPlugins={[remarkGfm]} |
19 | | - components={{ |
20 | | - code: Code, |
21 | | - }} |
22 | | - > |
23 | | - {children} |
24 | | - </RMarkdown> |
25 | | -); |
26 | | - |
27 | | -const Code = ( |
28 | | - props: React.DetailedHTMLProps< |
29 | | - React.HTMLAttributes<HTMLElement>, |
30 | | - HTMLElement |
31 | | - >, |
32 | | -) => { |
33 | | - // eslint-disable-next-line @typescript-eslint/no-unused-vars |
34 | | - const { children, className, ref, ...rest } = props; |
35 | | - const match = /language-(\w+)/.exec(className || ""); |
36 | | - |
37 | | - if (!match) { |
38 | | - return ( |
39 | | - <div className="p-3"> |
40 | | - <code {...rest}>{children}</code> |
41 | | - </div> |
42 | | - ); |
43 | | - } |
44 | | - |
45 | | - return ( |
46 | | - <div> |
47 | | - <SyntaxHighlighter |
48 | | - {...rest} |
49 | | - PreTag="div" |
50 | | - customStyle={{ margin: 0 }} |
51 | | - language={match[1]} |
52 | | - style={a11yDark} |
53 | | - > |
54 | | - {String(children).replace(/\n$/, "")} |
55 | | - </SyntaxHighlighter> |
56 | | - </div> |
57 | | - ); |
58 | | -}; |
59 | | - |
60 | | -const History = ({ history }: { history: any[] }) => { |
61 | | - const scrollRef = useRef<HTMLDivElement>(null); |
62 | | - const [displayScrollButton, setDisplayScrollButton] = useState(false); |
63 | | - |
64 | | - const onScroll = (e: React.UIEvent<HTMLDivElement>) => { |
65 | | - const element = e.currentTarget; |
66 | | - if (element.scrollTop + element.clientHeight < element.scrollHeight) { |
67 | | - setDisplayScrollButton(true); |
68 | | - } else { |
69 | | - setDisplayScrollButton(false); |
70 | | - } |
71 | | - }; |
72 | | - |
73 | | - const scrollToBottom = useCallback( |
74 | | - (smooth = true) => { |
75 | | - if (scrollRef.current) { |
76 | | - scrollRef.current.scrollTo({ |
77 | | - top: scrollRef.current.scrollHeight, |
78 | | - behavior: smooth ? "smooth" : "instant", |
79 | | - }); |
80 | | - } |
81 | | - }, |
82 | | - [scrollRef], |
83 | | - ); |
84 | | - |
85 | | - // Start at bottom on mount |
86 | | - useEffect(() => { |
87 | | - scrollToBottom(false); |
88 | | - }, []); |
89 | | - |
90 | | - // Scroll to bottom when history changes |
91 | | - useEffect(() => { |
92 | | - scrollToBottom(); |
93 | | - }, [history, scrollToBottom]); |
94 | | - |
95 | | - return ( |
96 | | - <div |
97 | | - className="scroll relative flex flex-1 flex-col overflow-y-scroll px-3" |
98 | | - onScroll={onScroll} |
99 | | - ref={scrollRef} |
100 | | - > |
101 | | - {history.map((item, index) => { |
102 | | - switch (item.type) { |
103 | | - case "system": |
104 | | - return ( |
105 | | - <div key={index} className="pt-4 text-lg font-bold"> |
106 | | - <div className="sticky top-0 flex items-center gap-2 bg-white"> |
107 | | - <div className="text-l text-primary">Assistant</div> |
108 | | - <div className="flex-1 border-b-2" /> |
109 | | - </div> |
110 | | - <div className="prose p-2"> |
111 | | - <Mardown>{item.message}</Mardown> |
112 | | - </div> |
113 | | - </div> |
114 | | - ); |
115 | | - case "user": |
116 | | - return ( |
117 | | - <div key={index} className="pt-4 text-lg font-bold"> |
118 | | - <div className="sticky top-0 flex items-center gap-2 bg-white"> |
119 | | - <div className="text-l text-secondary">You</div> |
120 | | - <div className="flex-1 border-b-2" /> |
121 | | - </div> |
122 | | - <div className="prose p-2"> |
123 | | - <Mardown>{item.message}</Mardown> |
124 | | - </div> |
125 | | - </div> |
126 | | - ); |
127 | | - } |
128 | | - })} |
129 | | - {displayScrollButton && ( |
130 | | - <div className="sticky bottom-2 flex w-full justify-center "> |
131 | | - <button |
132 | | - onClick={() => scrollToBottom()} |
133 | | - className="opacity-40 hover:opacity-100" |
134 | | - > |
135 | | - <ArrowDownCircleFill size="2rem" /> |
136 | | - </button> |
137 | | - </div> |
138 | | - )} |
139 | | - </div> |
140 | | - ); |
141 | | -}; |
| 9 | +import { ArrowUpSquareFill } from "react-bootstrap-icons"; |
| 10 | +import { useAssistant } from "./use-assistant"; |
| 11 | +import { History } from "./History"; |
142 | 12 |
|
143 | 13 | interface InputProps { |
144 | 14 | onSubmit: (message: string) => void; |
@@ -199,17 +69,19 @@ const Input = ({ onSubmit }: InputProps) => { |
199 | 69 | ); |
200 | 70 | }; |
201 | 71 |
|
202 | | -export const Chat = () => { |
203 | | - const historySnap = useSnapshot(state); |
| 72 | +export const Chat = () => ( |
| 73 | + <Suspense fallback={<span>waiting...</span>}> |
| 74 | + <ChatSuspense /> |
| 75 | + </Suspense> |
| 76 | +); |
| 77 | + |
| 78 | +const ChatSuspense = () => { |
| 79 | + const state = useAssistant(); |
204 | 80 |
|
205 | 81 | return ( |
206 | 82 | <div className="flex h-full max-h-full flex-col gap-3 bg-white"> |
207 | | - <History history={historySnap.history} /> |
208 | | - <Input |
209 | | - onSubmit={(data) => { |
210 | | - state.history.push({ type: "user", message: data }); |
211 | | - }} |
212 | | - /> |
| 83 | + <History history={state.history} /> |
| 84 | + <Input onSubmit={state.sendMessage} /> |
213 | 85 | </div> |
214 | 86 | ); |
215 | 87 | }; |
0 commit comments