Skip to content

Commit

Permalink
Added Cosmos DB chat history feature to the frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
fujita-h committed Oct 21, 2024
1 parent 6846c4d commit 07fe2ae
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 17 deletions.
64 changes: 63 additions & 1 deletion app/frontend/src/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const BACKEND_URI = "";

import { ChatAppResponse, ChatAppResponseOrError, ChatAppRequest, Config, SimpleAPIResponse } from "./models";
import { ChatAppResponse, ChatAppResponseOrError, ChatAppRequest, Config, SimpleAPIResponse, HistoryListApiResponse, HistroyApiResponse } from "./models";
import { useLogin, getToken, isUsingAppServicesLogin } from "../authConfig";

export async function getHeaders(idToken: string | undefined): Promise<Record<string, string>> {
Expand Down Expand Up @@ -126,3 +126,65 @@ export async function listUploadedFilesApi(idToken: string): Promise<string[]> {
const dataResponse: string[] = await response.json();
return dataResponse;
}

export async function postChatHistoryApi(item: any, idToken: string): Promise<any> {
const headers = await getHeaders(idToken);
const response = await fetch("/chat_history", {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify(item)
});

if (!response.ok) {
throw new Error(`Posting chat history failed: ${response.statusText}`);
}

const dataResponse: any = await response.json();
return dataResponse;
}

export async function getChatHistoryListApi(count: number, continuationToken: string | undefined, idToken: string): Promise<HistoryListApiResponse> {
const headers = await getHeaders(idToken);
const response = await fetch("/chat_history/items", {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ count: count, continuation_token: continuationToken })
});

if (!response.ok) {
throw new Error(`Getting chat histories failed: ${response.statusText}`);
}

const dataResponse: HistoryListApiResponse = await response.json();
return dataResponse;
}

export async function getChatHistoryApi(id: string, idToken: string): Promise<HistroyApiResponse> {
const headers = await getHeaders(idToken);
const response = await fetch(`/chat_history/items/${id}`, {
method: "GET",
headers: { ...headers, "Content-Type": "application/json" }
});

if (!response.ok) {
throw new Error(`Getting chat history failed: ${response.statusText}`);
}

const dataResponse: HistroyApiResponse = await response.json();
return dataResponse;
}

export async function deleteChatHistoryApi(id: string, idToken: string): Promise<any> {
const headers = await getHeaders(idToken);
const response = await fetch(`/chat_history/items/${id}`, {
method: "DELETE",
headers: { ...headers, "Content-Type": "application/json" }
});

if (!response.ok) {
throw new Error(`Deleting chat history failed: ${response.statusText}`);
}

const dataResponse: any = await response.json();
return dataResponse;
}
19 changes: 19 additions & 0 deletions app/frontend/src/api/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export type Config = {
showSpeechOutputBrowser: boolean;
showSpeechOutputAzure: boolean;
showChatHistoryBrowser: boolean;
showChatHistoryCosmos: boolean;
};

export type SimpleAPIResponse = {
Expand All @@ -103,3 +104,21 @@ export interface SpeechConfig {
isPlaying: boolean;
setIsPlaying: (isPlaying: boolean) => void;
}

export type HistoryListApiResponse = {
items: {
id: string;
entra_id: string;
title?: string;
_ts: number;
}[];
continuation_token?: string;
};

export type HistroyApiResponse = {
id: string;
entra_id: string;
title?: string;
answers: any;
_ts: number;
};
18 changes: 13 additions & 5 deletions app/frontend/src/components/HistoryPanel/HistoryPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Panel, PanelType } from "@fluentui/react";
import { useMsal } from "@azure/msal-react";
import { getToken, useLogin } from "../../authConfig";
import { Panel, PanelType, Spinner } from "@fluentui/react";
import { useEffect, useMemo, useRef, useState } from "react";
import { HistoryData, HistoryItem } from "../HistoryItem";
import { Answers, HistoryProviderOptions } from "../HistoryProviders/IProvider";
Expand Down Expand Up @@ -26,6 +28,8 @@ export const HistoryPanel = ({
const [isLoading, setIsLoading] = useState(false);
const [hasMoreHistory, setHasMoreHistory] = useState(false);

const client = useLogin ? useMsal().instance : undefined;

useEffect(() => {
if (!isOpen) return;
if (notify) {
Expand All @@ -37,7 +41,8 @@ export const HistoryPanel = ({

const loadMoreHistory = async () => {
setIsLoading(() => true);
const items = await historyManager.getNextItems(HISTORY_COUNT_PER_LOAD);
const token = client ? await getToken(client) : undefined;
const items = await historyManager.getNextItems(HISTORY_COUNT_PER_LOAD, token);
if (items.length === 0) {
setHasMoreHistory(false);
}
Expand All @@ -46,14 +51,16 @@ export const HistoryPanel = ({
};

const handleSelect = async (id: string) => {
const item = await historyManager.getItem(id);
const token = client ? await getToken(client) : undefined;
const item = await historyManager.getItem(id, token);
if (item) {
onChatSelected(item);
}
};

const handleDelete = async (id: string) => {
await historyManager.deleteItem(id);
const token = client ? await getToken(client) : undefined;
await historyManager.deleteItem(id, token);
setHistory(prevHistory => prevHistory.filter(item => item.id !== id));
};

Expand Down Expand Up @@ -85,7 +92,8 @@ export const HistoryPanel = ({
))}
</div>
))}
{history.length === 0 && <p>{t("history.noHistory")}</p>}
{isLoading && <Spinner style={{ marginTop: "10px" }} />}
{history.length === 0 && !isLoading && <p>{t("history.noHistory")}</p>}
{hasMoreHistory && !isLoading && <InfiniteLoadingButton func={loadMoreHistory} />}
</div>
</Panel>
Expand Down
51 changes: 51 additions & 0 deletions app/frontend/src/components/HistoryProviders/CosmosDB.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { IHistoryProvider, Answers, HistoryProviderOptions, HistoryMetaData } from "./IProvider";
import { deleteChatHistoryApi, getChatHistoryApi, getChatHistoryListApi, postChatHistoryApi } from "../../api";

export class CosmosDBProvider implements IHistoryProvider {
getProviderName = () => HistoryProviderOptions.CosmosDB;

private continuationToken: string | undefined;
private isItemEnd: boolean = false;

resetContinuationToken() {
this.continuationToken = undefined;
this.isItemEnd = false;
}

async getNextItems(count: number, idToken?: string): Promise<HistoryMetaData[]> {
if (this.isItemEnd) {
return [];
}

try {
const response = await getChatHistoryListApi(count, this.continuationToken, idToken || "");
this.continuationToken = response.continuation_token;
if (!this.continuationToken) {
this.isItemEnd = true;
}
return response.items.map(item => ({
id: item.id,
title: item.title || "untitled",
timestamp: item._ts * 1000
}));
} catch (e) {
console.error(e);
return [];
}
}

async addItem(id: string, answers: Answers, idToken?: string): Promise<void> {
await postChatHistoryApi({ id, answers }, idToken || "");
return;
}

async getItem(id: string, idToken?: string): Promise<Answers | null> {
const response = await getChatHistoryApi(id, idToken || "");
return response.answers || null;
}

async deleteItem(id: string, idToken?: string): Promise<void> {
await deleteChatHistoryApi(id, idToken || "");
return;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import { useMemo } from "react";
import { IHistoryProvider, HistoryProviderOptions } from "../HistoryProviders/IProvider";
import { NoneProvider } from "../HistoryProviders/None";
import { IndexedDBProvider } from "../HistoryProviders/IndexedDB";
import { CosmosDBProvider } from "../HistoryProviders/CosmosDB";

export const useHistoryManager = (provider: HistoryProviderOptions): IHistoryProvider => {
const providerInstance = useMemo(() => {
switch (provider) {
case HistoryProviderOptions.IndexedDB:
return new IndexedDBProvider("chat-database", "chat-history");
case HistoryProviderOptions.CosmosDB:
return new CosmosDBProvider();
case HistoryProviderOptions.None:
default:
return new NoneProvider();
Expand Down
11 changes: 6 additions & 5 deletions app/frontend/src/components/HistoryProviders/IProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ export type Answers = [user: string, response: ChatAppResponse][];

export const enum HistoryProviderOptions {
None = "none",
IndexedDB = "indexedDB"
IndexedDB = "indexedDB",
CosmosDB = "cosmosDB"
}

export interface IHistoryProvider {
getProviderName(): HistoryProviderOptions;
resetContinuationToken(): void;
getNextItems(count: number): Promise<HistoryMetaData[]>;
addItem(id: string, answers: Answers): Promise<void>;
getItem(id: string): Promise<Answers | null>;
deleteItem(id: string): Promise<void>;
getNextItems(count: number, idToken?: string): Promise<HistoryMetaData[]>;
addItem(id: string, answers: Answers, idToken?: string): Promise<void>;
getItem(id: string, idToken?: string): Promise<Answers | null>;
deleteItem(id: string, idToken?: string): Promise<void>;
}
20 changes: 15 additions & 5 deletions app/frontend/src/pages/chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const Chat = () => {
const [showSpeechOutputBrowser, setShowSpeechOutputBrowser] = useState<boolean>(false);
const [showSpeechOutputAzure, setShowSpeechOutputAzure] = useState<boolean>(false);
const [showChatHistoryBrowser, setShowChatHistoryBrowser] = useState<boolean>(false);
const [showChatHistoryCosmos, setShowChatHistoryCosmos] = useState<boolean>(false);
const audio = useRef(new Audio()).current;
const [isPlaying, setIsPlaying] = useState(false);

Expand All @@ -111,6 +112,7 @@ const Chat = () => {
setShowSpeechOutputBrowser(config.showSpeechOutputBrowser);
setShowSpeechOutputAzure(config.showSpeechOutputAzure);
setShowChatHistoryBrowser(config.showChatHistoryBrowser);
setShowChatHistoryCosmos(config.showChatHistoryCosmos);
});
};

Expand Down Expand Up @@ -160,7 +162,11 @@ const Chat = () => {
const client = useLogin ? useMsal().instance : undefined;
const { loggedIn } = useContext(LoginContext);

const historyProvider: HistoryProviderOptions = showChatHistoryBrowser ? HistoryProviderOptions.IndexedDB : HistoryProviderOptions.None;
const historyProvider: HistoryProviderOptions = (() => {
if (useLogin && showChatHistoryCosmos) return HistoryProviderOptions.CosmosDB;
if (showChatHistoryBrowser) return HistoryProviderOptions.IndexedDB;
return HistoryProviderOptions.None;
})();
const historyManager = useHistoryManager(historyProvider);

const makeApiRequest = async (question: string) => {
Expand Down Expand Up @@ -217,7 +223,8 @@ const Chat = () => {
const parsedResponse: ChatAppResponse = await handleAsyncRequest(question, answers, response.body);
setAnswers([...answers, [question, parsedResponse]]);
if (typeof parsedResponse.session_state === "string" && parsedResponse.session_state !== "") {
historyManager.addItem(parsedResponse.session_state, [...answers, [question, parsedResponse]]);
const token = client ? await getToken(client) : undefined;
historyManager.addItem(parsedResponse.session_state, [...answers, [question, parsedResponse]], token);
}
} else {
const parsedResponse: ChatAppResponseOrError = await response.json();
Expand All @@ -226,7 +233,8 @@ const Chat = () => {
}
setAnswers([...answers, [question, parsedResponse as ChatAppResponse]]);
if (typeof parsedResponse.session_state === "string" && parsedResponse.session_state !== "") {
historyManager.addItem(parsedResponse.session_state, [...answers, [question, parsedResponse as ChatAppResponse]]);
const token = client ? await getToken(client) : undefined;
historyManager.addItem(parsedResponse.session_state, [...answers, [question, parsedResponse as ChatAppResponse]], token);
}
}
setSpeechUrls([...speechUrls, null]);
Expand Down Expand Up @@ -369,7 +377,9 @@ const Chat = () => {
</Helmet>
<div className={styles.commandsSplitContainer}>
<div className={styles.commandsContainer}>
{showChatHistoryBrowser && <HistoryButton className={styles.commandButton} onClick={() => setIsHistoryPanelOpen(!isHistoryPanelOpen)} />}
{((useLogin && showChatHistoryCosmos) || showChatHistoryBrowser) && (
<HistoryButton className={styles.commandButton} onClick={() => setIsHistoryPanelOpen(!isHistoryPanelOpen)} />
)}
</div>
<div className={styles.commandsContainer}>
<ClearChatButton className={styles.commandButton} onClick={clearChat} disabled={!lastQuestionRef.current || isLoading} />
Expand Down Expand Up @@ -478,7 +488,7 @@ const Chat = () => {
/>
)}

{showChatHistoryBrowser && (
{((useLogin && showChatHistoryCosmos) || showChatHistoryBrowser) && (
<HistoryPanel
provider={historyProvider}
isOpen={isHistoryPanelOpen}
Expand Down
3 changes: 2 additions & 1 deletion app/frontend/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ export default defineConfig({
"/config": "http://localhost:50505",
"/upload": "http://localhost:50505",
"/delete_uploaded": "http://localhost:50505",
"/list_uploaded": "http://localhost:50505"
"/list_uploaded": "http://localhost:50505",
"/chat_history": "http://localhost:50505"
}
}
});

0 comments on commit 07fe2ae

Please sign in to comment.