|
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