Skip to content

Commit 07fe2ae

Browse files
committed
Added Cosmos DB chat history feature to the frontend
1 parent 6846c4d commit 07fe2ae

File tree

8 files changed

+172
-17
lines changed

8 files changed

+172
-17
lines changed

app/frontend/src/api/api.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const BACKEND_URI = "";
22

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

66
export async function getHeaders(idToken: string | undefined): Promise<Record<string, string>> {
@@ -126,3 +126,65 @@ export async function listUploadedFilesApi(idToken: string): Promise<string[]> {
126126
const dataResponse: string[] = await response.json();
127127
return dataResponse;
128128
}
129+
130+
export async function postChatHistoryApi(item: any, idToken: string): Promise<any> {
131+
const headers = await getHeaders(idToken);
132+
const response = await fetch("/chat_history", {
133+
method: "POST",
134+
headers: { ...headers, "Content-Type": "application/json" },
135+
body: JSON.stringify(item)
136+
});
137+
138+
if (!response.ok) {
139+
throw new Error(`Posting chat history failed: ${response.statusText}`);
140+
}
141+
142+
const dataResponse: any = await response.json();
143+
return dataResponse;
144+
}
145+
146+
export async function getChatHistoryListApi(count: number, continuationToken: string | undefined, idToken: string): Promise<HistoryListApiResponse> {
147+
const headers = await getHeaders(idToken);
148+
const response = await fetch("/chat_history/items", {
149+
method: "POST",
150+
headers: { ...headers, "Content-Type": "application/json" },
151+
body: JSON.stringify({ count: count, continuation_token: continuationToken })
152+
});
153+
154+
if (!response.ok) {
155+
throw new Error(`Getting chat histories failed: ${response.statusText}`);
156+
}
157+
158+
const dataResponse: HistoryListApiResponse = await response.json();
159+
return dataResponse;
160+
}
161+
162+
export async function getChatHistoryApi(id: string, idToken: string): Promise<HistroyApiResponse> {
163+
const headers = await getHeaders(idToken);
164+
const response = await fetch(`/chat_history/items/${id}`, {
165+
method: "GET",
166+
headers: { ...headers, "Content-Type": "application/json" }
167+
});
168+
169+
if (!response.ok) {
170+
throw new Error(`Getting chat history failed: ${response.statusText}`);
171+
}
172+
173+
const dataResponse: HistroyApiResponse = await response.json();
174+
return dataResponse;
175+
}
176+
177+
export async function deleteChatHistoryApi(id: string, idToken: string): Promise<any> {
178+
const headers = await getHeaders(idToken);
179+
const response = await fetch(`/chat_history/items/${id}`, {
180+
method: "DELETE",
181+
headers: { ...headers, "Content-Type": "application/json" }
182+
});
183+
184+
if (!response.ok) {
185+
throw new Error(`Deleting chat history failed: ${response.statusText}`);
186+
}
187+
188+
const dataResponse: any = await response.json();
189+
return dataResponse;
190+
}

app/frontend/src/api/models.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export type Config = {
9090
showSpeechOutputBrowser: boolean;
9191
showSpeechOutputAzure: boolean;
9292
showChatHistoryBrowser: boolean;
93+
showChatHistoryCosmos: boolean;
9394
};
9495

9596
export type SimpleAPIResponse = {
@@ -103,3 +104,21 @@ export interface SpeechConfig {
103104
isPlaying: boolean;
104105
setIsPlaying: (isPlaying: boolean) => void;
105106
}
107+
108+
export type HistoryListApiResponse = {
109+
items: {
110+
id: string;
111+
entra_id: string;
112+
title?: string;
113+
_ts: number;
114+
}[];
115+
continuation_token?: string;
116+
};
117+
118+
export type HistroyApiResponse = {
119+
id: string;
120+
entra_id: string;
121+
title?: string;
122+
answers: any;
123+
_ts: number;
124+
};

app/frontend/src/components/HistoryPanel/HistoryPanel.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { Panel, PanelType } from "@fluentui/react";
1+
import { useMsal } from "@azure/msal-react";
2+
import { getToken, useLogin } from "../../authConfig";
3+
import { Panel, PanelType, Spinner } from "@fluentui/react";
24
import { useEffect, useMemo, useRef, useState } from "react";
35
import { HistoryData, HistoryItem } from "../HistoryItem";
46
import { Answers, HistoryProviderOptions } from "../HistoryProviders/IProvider";
@@ -26,6 +28,8 @@ export const HistoryPanel = ({
2628
const [isLoading, setIsLoading] = useState(false);
2729
const [hasMoreHistory, setHasMoreHistory] = useState(false);
2830

31+
const client = useLogin ? useMsal().instance : undefined;
32+
2933
useEffect(() => {
3034
if (!isOpen) return;
3135
if (notify) {
@@ -37,7 +41,8 @@ export const HistoryPanel = ({
3741

3842
const loadMoreHistory = async () => {
3943
setIsLoading(() => true);
40-
const items = await historyManager.getNextItems(HISTORY_COUNT_PER_LOAD);
44+
const token = client ? await getToken(client) : undefined;
45+
const items = await historyManager.getNextItems(HISTORY_COUNT_PER_LOAD, token);
4146
if (items.length === 0) {
4247
setHasMoreHistory(false);
4348
}
@@ -46,14 +51,16 @@ export const HistoryPanel = ({
4651
};
4752

4853
const handleSelect = async (id: string) => {
49-
const item = await historyManager.getItem(id);
54+
const token = client ? await getToken(client) : undefined;
55+
const item = await historyManager.getItem(id, token);
5056
if (item) {
5157
onChatSelected(item);
5258
}
5359
};
5460

5561
const handleDelete = async (id: string) => {
56-
await historyManager.deleteItem(id);
62+
const token = client ? await getToken(client) : undefined;
63+
await historyManager.deleteItem(id, token);
5764
setHistory(prevHistory => prevHistory.filter(item => item.id !== id));
5865
};
5966

@@ -85,7 +92,8 @@ export const HistoryPanel = ({
8592
))}
8693
</div>
8794
))}
88-
{history.length === 0 && <p>{t("history.noHistory")}</p>}
95+
{isLoading && <Spinner style={{ marginTop: "10px" }} />}
96+
{history.length === 0 && !isLoading && <p>{t("history.noHistory")}</p>}
8997
{hasMoreHistory && !isLoading && <InfiniteLoadingButton func={loadMoreHistory} />}
9098
</div>
9199
</Panel>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { IHistoryProvider, Answers, HistoryProviderOptions, HistoryMetaData } from "./IProvider";
2+
import { deleteChatHistoryApi, getChatHistoryApi, getChatHistoryListApi, postChatHistoryApi } from "../../api";
3+
4+
export class CosmosDBProvider implements IHistoryProvider {
5+
getProviderName = () => HistoryProviderOptions.CosmosDB;
6+
7+
private continuationToken: string | undefined;
8+
private isItemEnd: boolean = false;
9+
10+
resetContinuationToken() {
11+
this.continuationToken = undefined;
12+
this.isItemEnd = false;
13+
}
14+
15+
async getNextItems(count: number, idToken?: string): Promise<HistoryMetaData[]> {
16+
if (this.isItemEnd) {
17+
return [];
18+
}
19+
20+
try {
21+
const response = await getChatHistoryListApi(count, this.continuationToken, idToken || "");
22+
this.continuationToken = response.continuation_token;
23+
if (!this.continuationToken) {
24+
this.isItemEnd = true;
25+
}
26+
return response.items.map(item => ({
27+
id: item.id,
28+
title: item.title || "untitled",
29+
timestamp: item._ts * 1000
30+
}));
31+
} catch (e) {
32+
console.error(e);
33+
return [];
34+
}
35+
}
36+
37+
async addItem(id: string, answers: Answers, idToken?: string): Promise<void> {
38+
await postChatHistoryApi({ id, answers }, idToken || "");
39+
return;
40+
}
41+
42+
async getItem(id: string, idToken?: string): Promise<Answers | null> {
43+
const response = await getChatHistoryApi(id, idToken || "");
44+
return response.answers || null;
45+
}
46+
47+
async deleteItem(id: string, idToken?: string): Promise<void> {
48+
await deleteChatHistoryApi(id, idToken || "");
49+
return;
50+
}
51+
}

app/frontend/src/components/HistoryProviders/HistoryManager.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import { useMemo } from "react";
22
import { IHistoryProvider, HistoryProviderOptions } from "../HistoryProviders/IProvider";
33
import { NoneProvider } from "../HistoryProviders/None";
44
import { IndexedDBProvider } from "../HistoryProviders/IndexedDB";
5+
import { CosmosDBProvider } from "../HistoryProviders/CosmosDB";
56

67
export const useHistoryManager = (provider: HistoryProviderOptions): IHistoryProvider => {
78
const providerInstance = useMemo(() => {
89
switch (provider) {
910
case HistoryProviderOptions.IndexedDB:
1011
return new IndexedDBProvider("chat-database", "chat-history");
12+
case HistoryProviderOptions.CosmosDB:
13+
return new CosmosDBProvider();
1114
case HistoryProviderOptions.None:
1215
default:
1316
return new NoneProvider();

app/frontend/src/components/HistoryProviders/IProvider.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ export type Answers = [user: string, response: ChatAppResponse][];
55

66
export const enum HistoryProviderOptions {
77
None = "none",
8-
IndexedDB = "indexedDB"
8+
IndexedDB = "indexedDB",
9+
CosmosDB = "cosmosDB"
910
}
1011

1112
export interface IHistoryProvider {
1213
getProviderName(): HistoryProviderOptions;
1314
resetContinuationToken(): void;
14-
getNextItems(count: number): Promise<HistoryMetaData[]>;
15-
addItem(id: string, answers: Answers): Promise<void>;
16-
getItem(id: string): Promise<Answers | null>;
17-
deleteItem(id: string): Promise<void>;
15+
getNextItems(count: number, idToken?: string): Promise<HistoryMetaData[]>;
16+
addItem(id: string, answers: Answers, idToken?: string): Promise<void>;
17+
getItem(id: string, idToken?: string): Promise<Answers | null>;
18+
deleteItem(id: string, idToken?: string): Promise<void>;
1819
}

app/frontend/src/pages/chat/Chat.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ const Chat = () => {
8585
const [showSpeechOutputBrowser, setShowSpeechOutputBrowser] = useState<boolean>(false);
8686
const [showSpeechOutputAzure, setShowSpeechOutputAzure] = useState<boolean>(false);
8787
const [showChatHistoryBrowser, setShowChatHistoryBrowser] = useState<boolean>(false);
88+
const [showChatHistoryCosmos, setShowChatHistoryCosmos] = useState<boolean>(false);
8889
const audio = useRef(new Audio()).current;
8990
const [isPlaying, setIsPlaying] = useState(false);
9091

@@ -111,6 +112,7 @@ const Chat = () => {
111112
setShowSpeechOutputBrowser(config.showSpeechOutputBrowser);
112113
setShowSpeechOutputAzure(config.showSpeechOutputAzure);
113114
setShowChatHistoryBrowser(config.showChatHistoryBrowser);
115+
setShowChatHistoryCosmos(config.showChatHistoryCosmos);
114116
});
115117
};
116118

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

163-
const historyProvider: HistoryProviderOptions = showChatHistoryBrowser ? HistoryProviderOptions.IndexedDB : HistoryProviderOptions.None;
165+
const historyProvider: HistoryProviderOptions = (() => {
166+
if (useLogin && showChatHistoryCosmos) return HistoryProviderOptions.CosmosDB;
167+
if (showChatHistoryBrowser) return HistoryProviderOptions.IndexedDB;
168+
return HistoryProviderOptions.None;
169+
})();
164170
const historyManager = useHistoryManager(historyProvider);
165171

166172
const makeApiRequest = async (question: string) => {
@@ -217,7 +223,8 @@ const Chat = () => {
217223
const parsedResponse: ChatAppResponse = await handleAsyncRequest(question, answers, response.body);
218224
setAnswers([...answers, [question, parsedResponse]]);
219225
if (typeof parsedResponse.session_state === "string" && parsedResponse.session_state !== "") {
220-
historyManager.addItem(parsedResponse.session_state, [...answers, [question, parsedResponse]]);
226+
const token = client ? await getToken(client) : undefined;
227+
historyManager.addItem(parsedResponse.session_state, [...answers, [question, parsedResponse]], token);
221228
}
222229
} else {
223230
const parsedResponse: ChatAppResponseOrError = await response.json();
@@ -226,7 +233,8 @@ const Chat = () => {
226233
}
227234
setAnswers([...answers, [question, parsedResponse as ChatAppResponse]]);
228235
if (typeof parsedResponse.session_state === "string" && parsedResponse.session_state !== "") {
229-
historyManager.addItem(parsedResponse.session_state, [...answers, [question, parsedResponse as ChatAppResponse]]);
236+
const token = client ? await getToken(client) : undefined;
237+
historyManager.addItem(parsedResponse.session_state, [...answers, [question, parsedResponse as ChatAppResponse]], token);
230238
}
231239
}
232240
setSpeechUrls([...speechUrls, null]);
@@ -369,7 +377,9 @@ const Chat = () => {
369377
</Helmet>
370378
<div className={styles.commandsSplitContainer}>
371379
<div className={styles.commandsContainer}>
372-
{showChatHistoryBrowser && <HistoryButton className={styles.commandButton} onClick={() => setIsHistoryPanelOpen(!isHistoryPanelOpen)} />}
380+
{((useLogin && showChatHistoryCosmos) || showChatHistoryBrowser) && (
381+
<HistoryButton className={styles.commandButton} onClick={() => setIsHistoryPanelOpen(!isHistoryPanelOpen)} />
382+
)}
373383
</div>
374384
<div className={styles.commandsContainer}>
375385
<ClearChatButton className={styles.commandButton} onClick={clearChat} disabled={!lastQuestionRef.current || isLoading} />
@@ -478,7 +488,7 @@ const Chat = () => {
478488
/>
479489
)}
480490

481-
{showChatHistoryBrowser && (
491+
{((useLogin && showChatHistoryCosmos) || showChatHistoryBrowser) && (
482492
<HistoryPanel
483493
provider={historyProvider}
484494
isOpen={isHistoryPanelOpen}

app/frontend/vite.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ export default defineConfig({
3434
"/config": "http://localhost:50505",
3535
"/upload": "http://localhost:50505",
3636
"/delete_uploaded": "http://localhost:50505",
37-
"/list_uploaded": "http://localhost:50505"
37+
"/list_uploaded": "http://localhost:50505",
38+
"/chat_history": "http://localhost:50505"
3839
}
3940
}
4041
});

0 commit comments

Comments
 (0)