mirror of
https://github.com/wassname/stampy-chat.git
synced 2026-09-09 11:36:23 +08:00
extract chat component
This commit is contained in:
@@ -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<Entry[]>([]);
|
||||
const [current, setCurrent] = useState<CurrentSearch>();
|
||||
const [citations, setCitations] = useState<Citation[]>([]);
|
||||
|
||||
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 = <p>Loading: Performing semantic search...</p>;
|
||||
break;
|
||||
case "prompt":
|
||||
last_entry = <p>Loading: Creating prompt...</p>;
|
||||
break;
|
||||
case "llm":
|
||||
last_entry = <p>Loading: Waiting for LLM...</p>;
|
||||
break;
|
||||
case "streaming":
|
||||
updateCitations(citations, current);
|
||||
last_entry = <AssistantEntry entry={current} />;
|
||||
break;
|
||||
case "followups":
|
||||
last_entry = (
|
||||
<>
|
||||
<AssistantEntry entry={current} />
|
||||
<p>Checking for followups...</p>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{entries.map((entry, i) => (
|
||||
<EntryTag entry={entry} key={i} />
|
||||
))}
|
||||
<SearchBox search={search} />
|
||||
|
||||
{last_entry}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chat;
|
||||
@@ -1,4 +1,4 @@
|
||||
export type Mode = "rookie" | "concise" | "default";
|
||||
import type { Mode } from "../types";
|
||||
|
||||
export const Controls = ({
|
||||
mode,
|
||||
|
||||
+5
-150
@@ -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<Entry[]>([]);
|
||||
const [current, setCurrent] = useState<CurrentSearch>();
|
||||
const [sessionId, setSessionId] = useState("");
|
||||
const [citations, setCitations] = useState<Citation[]>([]);
|
||||
|
||||
// [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 = <p>Loading: Performing semantic search...</p>;
|
||||
break;
|
||||
case "prompt":
|
||||
last_entry = <p>Loading: Creating prompt...</p>;
|
||||
break;
|
||||
case "llm":
|
||||
last_entry = <p>Loading: Waiting for LLM...</p>;
|
||||
break;
|
||||
case "streaming":
|
||||
updateCitations(citations, current);
|
||||
last_entry = <AssistantEntry entry={current} />;
|
||||
break;
|
||||
case "followups":
|
||||
last_entry = (
|
||||
<>
|
||||
<AssistantEntry entry={current} />
|
||||
<p>Checking for followups...</p>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page page="index">
|
||||
<Controls mode={mode} setMode={setMode} />
|
||||
@@ -176,14 +38,7 @@ const Home: NextPage = () => {
|
||||
welcomed.
|
||||
</h2>
|
||||
|
||||
<ul>
|
||||
{entries.map((entry, i) => (
|
||||
<EntryTag entry={entry} key={i} />
|
||||
))}
|
||||
<SearchBox search={search} />
|
||||
|
||||
{last_entry}
|
||||
</ul>
|
||||
<Chat sessionId={sessionId} mode={mode[0]} />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -42,3 +42,5 @@ export type SearchResult = {
|
||||
result: Entry;
|
||||
};
|
||||
export type CurrentSearch = (AssistantEntry & { phase?: string }) | undefined;
|
||||
|
||||
export type Mode = "rookie" | "concise" | "default";
|
||||
|
||||
Reference in New Issue
Block a user