diff --git a/web/src/app/components/history.tsx b/web/src/app/components/history.tsx new file mode 100644 index 0000000..5dd6646 --- /dev/null +++ b/web/src/app/components/history.tsx @@ -0,0 +1,35 @@ +"use client"; +import { historyQueryKey } from "@/app/utils/local-storage"; +import { LocalHistory } from "@/app/interfaces/history"; +import { Answer } from "@/app/components/answer"; +import { Sources } from "@/app/components/sources"; +import { Relates } from "@/app/components/relates"; +import { Title } from "@/app/components/title"; +import { Fragment } from "react"; + +export const HistoryResult = () => { + const history = window.localStorage.getItem(historyQueryKey); + if (!history) return null; + let historyRecord: LocalHistory[]; + try { + historyRecord = JSON.parse(history); + } catch { + historyRecord = []; + } + return historyRecord.map( + ({ query, rid, sources, markdown, relates, timestamp }) => { + return ( + +
+ + </div> + <div className="flex flex-col gap-8"> + <Answer markdown={markdown} sources={sources}></Answer> + <Sources sources={sources}></Sources> + <Relates relates={relates}></Relates> + </div> + </Fragment> + ); + }, + ); +}; diff --git a/web/src/app/components/result.tsx b/web/src/app/components/result.tsx index b6d86fb..f316c04 100644 --- a/web/src/app/components/result.tsx +++ b/web/src/app/components/result.tsx @@ -4,15 +4,33 @@ import { Relates } from "@/app/components/relates"; import { Sources } from "@/app/components/sources"; import { Relate } from "@/app/interfaces/relate"; import { Source } from "@/app/interfaces/source"; +import { LocalHistory } from "@/app/interfaces/history"; import { parseStreaming } from "@/app/utils/parse-streaming"; import { Annoyed } from "lucide-react"; -import { FC, useEffect, useState } from "react"; +import { FC, useCallback, useEffect, useState } from "react"; +import { historyQueryKey } from "@/app/utils/local-storage"; export const Result: FC<{ query: string; rid: string }> = ({ query, rid }) => { const [sources, setSources] = useState<Source[]>([]); const [markdown, setMarkdown] = useState<string>(""); const [relates, setRelates] = useState<Relate[] | null>(null); const [error, setError] = useState<number | null>(null); + const handleFinish = useCallback( + (result: LocalHistory) => { + const localHistory = window.localStorage.getItem(historyQueryKey); + let history: LocalHistory[]; + try { + history = JSON.parse(localHistory || "[]"); + } catch { + history = []; + } + window.localStorage.setItem( + historyQueryKey, + JSON.stringify([result, ...history]), + ); + }, + [rid, query], + ); useEffect(() => { const controller = new AbortController(); void parseStreaming( @@ -22,6 +40,7 @@ export const Result: FC<{ query: string; rid: string }> = ({ query, rid }) => { setSources, setMarkdown, setRelates, + handleFinish, setError, ); return () => { diff --git a/web/src/app/interfaces/history.ts b/web/src/app/interfaces/history.ts new file mode 100644 index 0000000..1e8a94d --- /dev/null +++ b/web/src/app/interfaces/history.ts @@ -0,0 +1,11 @@ +import { Relate } from "@/app/interfaces/relate"; +import { Source } from "@/app/interfaces/source"; + +export interface LocalHistory { + markdown: string; + relates: Relate[]; + sources: Source[]; + rid: string; + query: string; + timestamp: number; +} diff --git a/web/src/app/search/page.tsx b/web/src/app/search/page.tsx index 48800a8..ba5ea06 100644 --- a/web/src/app/search/page.tsx +++ b/web/src/app/search/page.tsx @@ -3,6 +3,7 @@ import { Result } from "@/app/components/result"; import { Search } from "@/app/components/search"; import { Title } from "@/app/components/title"; import { useSearchParams } from "next/navigation"; +import { HistoryResult } from "@/app/components/history"; export default function SearchPage() { const searchParams = useSearchParams(); const query = decodeURIComponent(searchParams.get("q") || ""); @@ -14,6 +15,7 @@ export default function SearchPage() { <div className="px-4 md:px-8 pt-6 pb-24 rounded-2xl ring-8 ring-zinc-300/20 border border-zinc-200 h-full overflow-auto"> <Title query={query}> +
diff --git a/web/src/app/utils/local-storage.ts b/web/src/app/utils/local-storage.ts new file mode 100644 index 0000000..1363c85 --- /dev/null +++ b/web/src/app/utils/local-storage.ts @@ -0,0 +1 @@ +export const historyQueryKey = "lepton_previous_query"; diff --git a/web/src/app/utils/parse-streaming.ts b/web/src/app/utils/parse-streaming.ts index 0667514..9b5338d 100644 --- a/web/src/app/utils/parse-streaming.ts +++ b/web/src/app/utils/parse-streaming.ts @@ -1,6 +1,7 @@ import { Relate } from "@/app/interfaces/relate"; import { Source } from "@/app/interfaces/source"; import { fetchStream } from "@/app/utils/fetch-stream"; +import { LocalHistory } from "@/app/interfaces/history"; const LLM_SPLIT = "__LLM_RESPONSE__"; const RELATED_SPLIT = "__RELATED_QUESTIONS__"; @@ -12,6 +13,7 @@ export const parseStreaming = async ( onSources: (value: Source[]) => void, onMarkdown: (value: string) => void, onRelates: (value: Relate[]) => void, + onFinish: (result: LocalHistory) => void, onError?: (status: number) => void, ) => { const decoder = new TextDecoder(); @@ -30,18 +32,19 @@ export const parseStreaming = async ( search_uuid, }), }); + let finalRelates: Relate[] = []; + let finalMarkdown: string = ""; + let finalSources: Source[] = []; if (response.status !== 200) { onError?.(response.status); return; } const markdownParse = (text: string) => { - onMarkdown( - text - .replace(/\[\[([cC])itation/g, "[citation") - .replace(/[cC]itation:(\d+)]]/g, "citation:$1]") - .replace(/\[\[([cC]itation:\d+)]](?!])/g, `[$1]`) - .replace(/\[[cC]itation:(\d+)]/g, "[citation]($1)"), - ); + return text + .replace(/\[\[([cC])itation/g, "[citation") + .replace(/[cC]itation:(\d+)]]/g, "citation:$1]") + .replace(/\[\[([cC]itation:\d+)]](?!])/g, `[$1]`) + .replace(/\[[cC]itation:(\d+)]/g, "[citation]($1)"); }; fetchStream( response, @@ -52,27 +55,38 @@ export const parseStreaming = async ( const [sources, rest] = chunks.split(LLM_SPLIT); if (!sourcesEmitted) { try { - onSources(JSON.parse(sources)); + finalSources = JSON.parse(sources); } catch (e) { - onSources([]); + finalSources = []; } + onSources(finalSources); } sourcesEmitted = true; if (rest.includes(RELATED_SPLIT)) { const [md] = rest.split(RELATED_SPLIT); - markdownParse(md); + finalMarkdown = markdownParse(md); } else { - markdownParse(rest); + finalMarkdown = markdownParse(rest); } + onMarkdown(finalMarkdown); } }, () => { const [_, relates] = chunks.split(RELATED_SPLIT); try { - onRelates(JSON.parse(relates)); + finalRelates = JSON.parse(relates); } catch (e) { - onRelates([]); + finalRelates = []; } + onRelates(finalRelates); + onFinish({ + markdown: finalMarkdown, + sources: finalSources, + relates: finalRelates, + rid: search_uuid, + query, + timestamp: new Date().valueOf(), + }); }, ); };