From b4a072cc8e36bec3f4493f60aed015b2bd31d9ae Mon Sep 17 00:00:00 2001 From: Daniel O'Connell Date: Sun, 1 Oct 2023 16:49:07 +0200 Subject: [PATCH] extract chat component --- web/src/components/chat.tsx | 151 +++++++++++++++++++++++++++++++ web/src/components/controls.tsx | 2 +- web/src/pages/index.tsx | 155 ++------------------------------ web/src/types.ts | 2 + 4 files changed, 159 insertions(+), 151 deletions(-) create mode 100644 web/src/components/chat.tsx diff --git a/web/src/components/chat.tsx b/web/src/components/chat.tsx new file mode 100644 index 0000000..7701524 --- /dev/null +++ b/web/src/components/chat.tsx @@ -0,0 +1,151 @@ +import { useState, useEffect } from "react"; +import { queryLLM, getStampyContent, runSearch } from "../hooks/useSearch"; +import type { + CurrentSearch, + Citation, + Entry, + AssistantEntry as AssistantEntryType, + Mode, + Followup, +} from "../types"; +import { SearchBox } from "../components/searchbox"; +import { AssistantEntry } from "../components/assistant"; +import { Entry as EntryTag } from "../components/entry"; + +const MAX_FOLLOWUPS = 4; + +type State = + | { + state: "idle"; + } + | { + state: "loading"; + phase: "semantic" | "prompt" | "llm"; + citations: Citation[]; + } + | { + state: "streaming"; + response: AssistantEntryType; + }; + +// smooth-scroll to the bottom of the window if we're already less than 30% a screen away +// note: finicky interaction with "smooth" - maybe fix later. +function scroll30() { + if ( + document.documentElement.scrollHeight - window.scrollY > + window.innerHeight * 1.3 + ) + return; + window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" }); +} + +const Chat = ({ sessionId, mode }: { sessionId: string; mode: Mode }) => { + const [entries, setEntries] = useState([]); + const [current, setCurrent] = useState(); + const [citations, setCitations] = useState([]); + + const updateCurrent = (current: CurrentSearch) => { + setCurrent(current); + if (current?.phase === "streaming") { + scroll30(); + } + }; + + const updateCitations = ( + allCitations: Citation[], + current?: CurrentSearch + ) => { + if (!current) return; + + const entryCitations = Array.from(current.citationsMap.values()); + if (!entryCitations.some((c) => !c.index)) { + // All of the entries citations have indexes, so there weren't any changes since the last check + return; + } + + // Get a mapping of all known citations, so as to reuse them if they appear again + const citationsMapping = Object.fromEntries( + allCitations.map((c) => [c.title + c.url, c.index]) + ); + + entryCitations.forEach((c) => { + const hash = c.title + c.url; + const index = citationsMapping[hash]; + if (!index) { + c.index = allCitations.length + 1; + allCitations.push(c); + } else { + c.index = index; + } + }); + setCitations(allCitations); + setCurrent(current); + }; + + const search = async ( + query: string, + query_source: "search" | "followups", + disable: () => void, + enable: (f_set: Followup[] | ((fs: Followup[]) => Followup[])) => void + ) => { + // clear the query box, append to entries + const userEntry: Entry = { + role: "user", + content: query_source === "search" ? query : query.split("\n", 2)[1]!, + }; + setEntries((prev) => [...prev, userEntry]); + disable(); + + const { result, followups } = await runSearch( + query, + query_source, + mode, + entries, + updateCurrent, + sessionId + ); + setCurrent(undefined); + + setEntries((prev) => [...prev, result]); + enable(followups || []); + scroll30(); + }; + + var last_entry = <>; + switch (current?.phase) { + case "semantic": + last_entry =

Loading: Performing semantic search...

; + break; + case "prompt": + last_entry =

Loading: Creating prompt...

; + break; + case "llm": + last_entry =

Loading: Waiting for LLM...

; + break; + case "streaming": + updateCitations(citations, current); + last_entry = ; + break; + case "followups": + last_entry = ( + <> + +

Checking for followups...

+ + ); + break; + } + + return ( +
    + {entries.map((entry, i) => ( + + ))} + + + {last_entry} +
+ ); +}; + +export default Chat; diff --git a/web/src/components/controls.tsx b/web/src/components/controls.tsx index d7fc17c..a92ae63 100644 --- a/web/src/components/controls.tsx +++ b/web/src/components/controls.tsx @@ -1,4 +1,4 @@ -export type Mode = "rookie" | "concise" | "default"; +import type { Mode } from "../types"; export const Controls = ({ mode, diff --git a/web/src/pages/index.tsx b/web/src/pages/index.tsx index aa584f4..e34d029 100644 --- a/web/src/pages/index.tsx +++ b/web/src/pages/index.tsx @@ -1,61 +1,17 @@ import { type NextPage } from "next"; import { useState, useEffect } from "react"; import Link from "next/link"; -import Image from "next/image"; -import Page from "../components/page"; -import { API_URL } from "../settings"; import { queryLLM, getStampyContent, runSearch } from "../hooks/useSearch"; -import type { - CurrentSearch, - Citation, - Entry, - UserEntry, - AssistantEntry as AssistantEntryType, - ErrorMessage, - StampyMessage, - Followup, -} from "../types"; -import { SearchBox } from "../components/searchbox"; -import { GlossarySpan } from "../components/glossary"; -import { Controls, Mode } from "../components/controls"; -import { AssistantEntry } from "../components/assistant"; -import { Entry as EntryTag } from "../components/entry"; +import type { Mode } from "../types"; +import Page from "../components/page"; +import Chat from "../components/chat"; +import { Controls } from "../components/controls"; const MAX_FOLLOWUPS = 4; -type State = - | { - state: "idle"; - } - | { - state: "loading"; - phase: "semantic" | "prompt" | "llm"; - citations: Citation[]; - } - | { - state: "streaming"; - response: AssistantEntryType; - }; - -// smooth-scroll to the bottom of the window if we're already less than 30% a screen away -// note: finicky interaction with "smooth" - maybe fix later. -function scroll30() { - if ( - document.documentElement.scrollHeight - window.scrollY > - window.innerHeight * 1.3 - ) - return; - window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" }); -} - const Home: NextPage = () => { - const [entries, setEntries] = useState([]); - const [current, setCurrent] = useState(); const [sessionId, setSessionId] = useState(""); - const [citations, setCitations] = useState([]); - - // [state, ready to save to localstorage] const [mode, setMode] = useState<[Mode, boolean]>(["default", false]); // store mode in localstorage @@ -70,100 +26,6 @@ const Home: NextPage = () => { setSessionId(crypto.randomUUID()); }, []); - const updateCurrent = (current: CurrentSearch) => { - setCurrent(current); - if (current?.phase === "streaming") { - scroll30(); - } - }; - - const updateCitations = ( - allCitations: Citation[], - current: CurrentSearch - ) => { - if (!current) return; - - const entryCitations = Array.from( - current.citationsMap.values() - ) as Citation[]; - if (!entryCitations.some((c) => !c.index)) { - // All of the entries citations have indexes, so there weren't any changes since the last check - return; - } - - // Get a mapping of all known citations, so as to reuse them if they appear again - const citationsMapping = Object.fromEntries( - allCitations.map((c) => [c.title + c.url, c.index]) - ); - - entryCitations.forEach((c) => { - const hash = c.title + c.url; - const index = citationsMapping[hash]; - if (index !== undefined) { - c.index = index; - } else { - c.index = allCitations.length + 1; - allCitations.push(c); - } - }); - setCitations(allCitations); - setCurrent(current); - }; - - const search = async ( - query: string, - query_source: "search" | "followups", - disable: () => void, - enable: (f_set: Followup[] | ((fs: Followup[]) => Followup[])) => void - ) => { - // clear the query box, append to entries - const userEntry: Entry = { - role: "user", - content: query_source === "search" ? query : query.split("\n", 2)[1]!, - }; - setEntries((prev) => [...prev, userEntry]); - disable(); - - const { result, followups } = await runSearch( - query, - query_source, - mode[0], - entries, - updateCurrent, - sessionId - ); - setCurrent(undefined); - - setEntries((prev) => [...prev, result]); - enable(followups || []); - scroll30(); - }; - - var last_entry = <>; - switch (current?.phase) { - case "semantic": - last_entry =

Loading: Performing semantic search...

; - break; - case "prompt": - last_entry =

Loading: Creating prompt...

; - break; - case "llm": - last_entry =

Loading: Waiting for LLM...

; - break; - case "streaming": - updateCitations(citations, current); - last_entry = ; - break; - case "followups": - last_entry = ( - <> - -

Checking for followups...

- - ); - break; - } - return ( @@ -176,14 +38,7 @@ const Home: NextPage = () => { welcomed. -
    - {entries.map((entry, i) => ( - - ))} - - - {last_entry} -
+
); }; diff --git a/web/src/types.ts b/web/src/types.ts index b7fe0c4..9bfede9 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -42,3 +42,5 @@ export type SearchResult = { result: Entry; }; export type CurrentSearch = (AssistantEntry & { phase?: string }) | undefined; + +export type Mode = "rookie" | "concise" | "default";