index.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import { useRef, useState, useEffect } from 'react';
  2. import Layout from '@/components/layout';
  3. import styles from '@/styles/Home.module.css';
  4. import { Message } from '@/types/chat';
  5. import Image from 'next/image';
  6. import ReactMarkdown from 'react-markdown';
  7. import LoadingDots from '@/components/ui/LoadingDots';
  8. import { Document } from 'langchain/document';
  9. import {
  10. Accordion,
  11. AccordionContent,
  12. AccordionItem,
  13. AccordionTrigger,
  14. } from '@/components/ui/accordion';
  15. // require('dotenv') .config()
  16. // import {run} from '@/scripts/ingest-data';
  17. export default function Home() {
  18. // run();
  19. //console.log(process.env.OPENAI_API_KEY)
  20. const [query, setQuery] = useState<string>('');
  21. const [loading, setLoading] = useState<boolean>(false);
  22. const [error, setError] = useState<string | null>(null);
  23. const [messageState, setMessageState] = useState<{
  24. messages: Message[];
  25. pending?: string;
  26. history: [string, string][];
  27. pendingSourceDocs?: Document[];
  28. }>({
  29. messages: [
  30. {
  31. message: 'Hi, what would you like to learn about this legal case?',
  32. type: 'apiMessage',
  33. },
  34. ],
  35. history: [],
  36. });
  37. const { messages, history } = messageState;
  38. const messageListRef = useRef<HTMLDivElement>(null);
  39. const textAreaRef = useRef<HTMLTextAreaElement>(null);
  40. useEffect(() => {
  41. textAreaRef.current?.focus();
  42. }, []);
  43. //handle form submission
  44. async function handleSubmit(e: any) {
  45. e.preventDefault();
  46. setError(null);
  47. if (!query) {
  48. alert('Please input a question');
  49. return;
  50. }
  51. const question = query.trim();
  52. setMessageState((state) => ({
  53. ...state,
  54. messages: [
  55. ...state.messages,
  56. {
  57. type: 'userMessage',
  58. message: question,
  59. },
  60. ],
  61. }));
  62. setLoading(true);
  63. setQuery('');
  64. try {
  65. const response = await fetch('/api/chat', {
  66. method: 'POST',
  67. headers: {
  68. 'Content-Type': 'application/json',
  69. },
  70. body: JSON.stringify({
  71. question,
  72. history,
  73. }),
  74. });
  75. const data = await response.json();
  76. console.log('data', data);
  77. if (data.error) {
  78. setError(data.error);
  79. } else {
  80. setMessageState((state) => ({
  81. ...state,
  82. messages: [
  83. ...state.messages,
  84. {
  85. type: 'apiMessage',
  86. message: data.text,
  87. sourceDocs: data.sourceDocuments,
  88. },
  89. ],
  90. history: [...state.history, [question, data.text]],
  91. }));
  92. }
  93. console.log('messageState', messageState);
  94. setLoading(false);
  95. //scroll to bottom
  96. messageListRef.current?.scrollTo(0, messageListRef.current.scrollHeight);
  97. } catch (error) {
  98. setLoading(false);
  99. setError('An error occurred while fetching the data. Please try again.');
  100. console.log('error', error);
  101. }
  102. }
  103. //prevent empty submissions
  104. const handleEnter = (e: any) => {
  105. if (e.key === 'Enter' && query) {
  106. handleSubmit(e);
  107. } else if (e.key == 'Enter') {
  108. e.preventDefault();
  109. }
  110. };
  111. return (
  112. <>
  113. <Layout>
  114. <div className="mx-auto flex flex-col gap-4">
  115. <h1 className="text-2xl font-bold leading-[1.1] tracking-tighter text-center">
  116. Chat With Your Legal Docs
  117. </h1>
  118. <main className={styles.main}>
  119. <div className={styles.cloud}>
  120. <div ref={messageListRef} className={styles.messagelist}>
  121. {messages.map((message, index) => {
  122. let icon;
  123. let className;
  124. if (message.type === 'apiMessage') {
  125. icon = (
  126. <Image
  127. key={index}
  128. src="/bot-image.png"
  129. alt="AI"
  130. width="40"
  131. height="40"
  132. className={styles.boticon}
  133. priority
  134. />
  135. );
  136. className = styles.apimessage;
  137. } else {
  138. icon = (
  139. <Image
  140. key={index}
  141. src="/usericon.png"
  142. alt="Me"
  143. width="30"
  144. height="30"
  145. className={styles.usericon}
  146. priority
  147. />
  148. );
  149. // The latest message sent by the user will be animated while waiting for a response
  150. className =
  151. loading && index === messages.length - 1
  152. ? styles.usermessagewaiting
  153. : styles.usermessage;
  154. }
  155. return (
  156. <>
  157. <div key={`chatMessage-${index}`} className={className}>
  158. {icon}
  159. <div className={styles.markdownanswer}>
  160. <ReactMarkdown linkTarget="_blank">
  161. {message.message}
  162. </ReactMarkdown>
  163. </div>
  164. </div>
  165. {message.sourceDocs && (
  166. <div
  167. className="p-5"
  168. key={`sourceDocsAccordion-${index}`}
  169. >
  170. <Accordion
  171. type="single"
  172. collapsible
  173. className="flex-col"
  174. >
  175. {message.sourceDocs.map((doc, index) => (
  176. <div key={`messageSourceDocs-${index}`}>
  177. <AccordionItem value={`item-${index}`}>
  178. <AccordionTrigger>
  179. <h3>Source {index + 1}</h3>
  180. </AccordionTrigger>
  181. <AccordionContent>
  182. <ReactMarkdown linkTarget="_blank">
  183. {doc.pageContent}
  184. </ReactMarkdown>
  185. <p className="mt-2">
  186. <b>Source:</b> {doc.metadata.source}
  187. </p>
  188. </AccordionContent>
  189. </AccordionItem>
  190. </div>
  191. ))}
  192. </Accordion>
  193. </div>
  194. )}
  195. </>
  196. );
  197. })}
  198. </div>
  199. </div>
  200. <div className={styles.center}>
  201. <div className={styles.cloudform}>
  202. <form onSubmit={handleSubmit}>
  203. <textarea
  204. disabled={loading}
  205. onKeyDown={handleEnter}
  206. ref={textAreaRef}
  207. autoFocus={false}
  208. rows={1}
  209. maxLength={512}
  210. id="userInput"
  211. name="userInput"
  212. placeholder={
  213. loading
  214. ? 'Waiting for response...'
  215. : 'What is this legal case about?'
  216. }
  217. value={query}
  218. onChange={(e) => setQuery(e.target.value)}
  219. className={styles.textarea}
  220. />
  221. <button
  222. type="submit"
  223. disabled={loading}
  224. className={styles.generatebutton}
  225. >
  226. {loading ? (
  227. <div className={styles.loadingwheel}>
  228. <LoadingDots color="#000" />
  229. </div>
  230. ) : (
  231. // Send icon SVG in input field
  232. <svg
  233. viewBox="0 0 20 20"
  234. className={styles.svgicon}
  235. xmlns="http://www.w3.org/2000/svg"
  236. >
  237. <path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z"></path>
  238. </svg>
  239. )}
  240. </button>
  241. </form>
  242. </div>
  243. </div>
  244. {error && (
  245. <div className="border border-red-400 rounded-md p-4">
  246. <p className="text-red-500">{error}</p>
  247. </div>
  248. )}
  249. </main>
  250. </div>
  251. <footer className="m-auto p-4">
  252. <a href="https://twitter.com/mayowaoshin">
  253. Powered by LangChainAI. Demo built by Mayo (Twitter: @mayowaoshin).
  254. </a>
  255. </footer>
  256. </Layout>
  257. </>
  258. );
  259. }