Skip to content

Commit

Permalink
Add chat history feature (#1988)
Browse files Browse the repository at this point in the history
* Add chat history feature

This commit adds the following new files:
- HistoryItem: A new component for displaying chat history items.
- HistoryPanel: A new component for displaying the chat history panel.
- HistoryButton: A new component for opening the chat history.
- HistoryProviders: A new module containing history provider classes.

These changes include functionality to support future enhancements to the chat history feature.

* Add translations for chat history in Spanish and French

* Update e2e test

* Refactor chat history configuration variables
Changed variable names to make their functions clearer.

* Refactor session state handling
Changed to generate session state on the server side.

* Add support for chat history feature to bicep

* Update workflows, docs, typing

* Apply suggestions from code review

Co-authored-by: Wassim Chegham <[email protected]>

* Revert e2e test change

---------

Co-authored-by: Pamela Fox <[email protected]>
Co-authored-by: Wassim Chegham <[email protected]>
Co-authored-by: Pamela Fox <[email protected]>
  • Loading branch information
4 people authored Oct 4, 2024
1 parent f4f6896 commit be26d31
Show file tree
Hide file tree
Showing 33 changed files with 708 additions and 9 deletions.
2 changes: 1 addition & 1 deletion .azdo/pipelines/azure-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ steps:
AZURE_ADLS_GEN2_FILESYSTEM: $(AZURE_ADLS_GEN2_FILESYSTEM)
DEPLOYMENT_TARGET: $(DEPLOYMENT_TARGET)
AZURE_CONTAINER_APPS_WORKLOAD_PROFILE: $(AZURE_CONTAINER_APPS_WORKLOAD_PROFILE)

USE_CHAT_HISTORY_BROWSER: $(USE_CHAT_HISTORY_BROWSER)
- task: AzureCLI@2
displayName: Deploy Application
inputs:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/azure-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ jobs:
AZURE_ADLS_GEN2_FILESYSTEM: ${{ vars.AZURE_ADLS_GEN2_FILESYSTEM }}
DEPLOYMENT_TARGET: ${{ vars.DEPLOYMENT_TARGET }}
AZURE_CONTAINER_APPS_WORKLOAD_PROFILE: ${{ vars.AZURE_CONTAINER_APPS_WORKLOAD_PROFILE }}

USE_CHAT_HISTORY_BROWSER: ${{ vars.USE_CHAT_HISTORY_BROWSER }}
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ If you followed the steps above to install the pre-commit hooks, then you can ju
## Adding new azd environment variables

When adding new azd environment variables, please remember to update:

1. App Service's [azure.yaml](./azure.yaml)
1. [ADO pipeline](.azdo/pipelines/azure-dev.yml).
1. [Github workflows](.github/workflows/azure-dev.yml)
19 changes: 17 additions & 2 deletions app/backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
CONFIG_AUTH_CLIENT,
CONFIG_BLOB_CONTAINER_CLIENT,
CONFIG_CHAT_APPROACH,
CONFIG_CHAT_HISTORY_BROWSER_ENABLED,
CONFIG_CHAT_VISION_APPROACH,
CONFIG_CREDENTIAL,
CONFIG_GPT4V_DEPLOYED,
Expand All @@ -79,6 +80,7 @@
CONFIG_VECTOR_SEARCH_ENABLED,
)
from core.authentication import AuthenticationHelper
from core.sessionhelper import create_session_id
from decorators import authenticated, authenticated_path
from error import error_dict, error_response
from prepdocs import (
Expand Down Expand Up @@ -218,10 +220,15 @@ async def chat(auth_claims: Dict[str, Any]):
else:
approach = cast(Approach, current_app.config[CONFIG_CHAT_APPROACH])

# If session state is provided, persists the session state,
# else creates a new session_id depending on the chat history options enabled.
session_state = request_json.get("session_state")
if session_state is None:
session_state = create_session_id(current_app.config[CONFIG_CHAT_HISTORY_BROWSER_ENABLED])
result = await approach.run(
request_json["messages"],
context=context,
session_state=request_json.get("session_state"),
session_state=session_state,
)
return jsonify(result)
except Exception as error:
Expand All @@ -244,10 +251,15 @@ async def chat_stream(auth_claims: Dict[str, Any]):
else:
approach = cast(Approach, current_app.config[CONFIG_CHAT_APPROACH])

# If session state is provided, persists the session state,
# else creates a new session_id depending on the chat history options enabled.
session_state = request_json.get("session_state")
if session_state is None:
session_state = create_session_id(current_app.config[CONFIG_CHAT_HISTORY_BROWSER_ENABLED])
result = await approach.run_stream(
request_json["messages"],
context=context,
session_state=request_json.get("session_state"),
session_state=session_state,
)
response = await make_response(format_as_ndjson(result))
response.timeout = None # type: ignore
Expand Down Expand Up @@ -276,6 +288,7 @@ def config():
"showSpeechInput": current_app.config[CONFIG_SPEECH_INPUT_ENABLED],
"showSpeechOutputBrowser": current_app.config[CONFIG_SPEECH_OUTPUT_BROWSER_ENABLED],
"showSpeechOutputAzure": current_app.config[CONFIG_SPEECH_OUTPUT_AZURE_ENABLED],
"showChatHistoryBrowser": current_app.config[CONFIG_CHAT_HISTORY_BROWSER_ENABLED],
}
)

Expand Down Expand Up @@ -439,6 +452,7 @@ async def setup_clients():
USE_SPEECH_INPUT_BROWSER = os.getenv("USE_SPEECH_INPUT_BROWSER", "").lower() == "true"
USE_SPEECH_OUTPUT_BROWSER = os.getenv("USE_SPEECH_OUTPUT_BROWSER", "").lower() == "true"
USE_SPEECH_OUTPUT_AZURE = os.getenv("USE_SPEECH_OUTPUT_AZURE", "").lower() == "true"
USE_CHAT_HISTORY_BROWSER = os.getenv("USE_CHAT_HISTORY_BROWSER", "").lower() == "true"

# WEBSITE_HOSTNAME is always set by App Service, RUNNING_IN_PRODUCTION is set in main.bicep
RUNNING_ON_AZURE = os.getenv("WEBSITE_HOSTNAME") is not None or os.getenv("RUNNING_IN_PRODUCTION") is not None
Expand Down Expand Up @@ -609,6 +623,7 @@ async def setup_clients():
current_app.config[CONFIG_SPEECH_INPUT_ENABLED] = USE_SPEECH_INPUT_BROWSER
current_app.config[CONFIG_SPEECH_OUTPUT_BROWSER_ENABLED] = USE_SPEECH_OUTPUT_BROWSER
current_app.config[CONFIG_SPEECH_OUTPUT_AZURE_ENABLED] = USE_SPEECH_OUTPUT_AZURE
current_app.config[CONFIG_CHAT_HISTORY_BROWSER_ENABLED] = USE_CHAT_HISTORY_BROWSER

# Various approaches to integrate GPT and external knowledge, most applications will use a single one of these patterns
# or some derivative, here we include several for exploration purposes
Expand Down
1 change: 1 addition & 0 deletions app/backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@
CONFIG_SPEECH_SERVICE_LOCATION = "speech_service_location"
CONFIG_SPEECH_SERVICE_TOKEN = "speech_service_token"
CONFIG_SPEECH_SERVICE_VOICE = "speech_service_voice"
CONFIG_CHAT_HISTORY_BROWSER_ENABLED = "chat_history_browser_enabled"
8 changes: 8 additions & 0 deletions app/backend/core/sessionhelper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import uuid
from typing import Union


def create_session_id(config_chat_history_browser_enabled: bool) -> Union[str, None]:
if config_chat_history_browser_enabled:
return str(uuid.uuid4())
return None
6 changes: 6 additions & 0 deletions app/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"i18next": "^23.12.2",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.5.2",
"idb": "^8.0.0",
"ndjson-readablestream": "^1.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
1 change: 1 addition & 0 deletions app/frontend/src/api/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export type Config = {
showSpeechInput: boolean;
showSpeechOutputBrowser: boolean;
showSpeechOutputAzure: boolean;
showChatHistoryBrowser: boolean;
};

export type SimpleAPIResponse = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.container {
display: flex;
align-items: center;
gap: 0.375em;
cursor: pointer;
padding: 0.5rem;
}
22 changes: 22 additions & 0 deletions app/frontend/src/components/HistoryButton/HistoryButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { History24Regular } from "@fluentui/react-icons";
import { Button } from "@fluentui/react-components";
import { useTranslation } from "react-i18next";

import styles from "./HistoryButton.module.css";

interface Props {
className?: string;
onClick: () => void;
disabled?: boolean;
}

export const HistoryButton = ({ className, disabled, onClick }: Props) => {
const { t } = useTranslation();
return (
<div className={`${styles.container} ${className ?? ""}`}>
<Button icon={<History24Regular />} disabled={disabled} onClick={onClick}>
{t("history.openChatHistory")}
</Button>
</div>
);
};
1 change: 1 addition & 0 deletions app/frontend/src/components/HistoryButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./HistoryButton";
120 changes: 120 additions & 0 deletions app/frontend/src/components/HistoryItem/HistoryItem.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
.historyItem {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 8px;
border-radius: 6px;
transition: background-color 0.2s;
}

.historyItem:hover {
background-color: #f3f4f6;
}

.historyItemButton {
flex-grow: 1;
text-align: left;
padding: 0;
margin-right: 4px;
background: none;
border: none;
cursor: pointer;
}

.historyItemTitle {
font-size: 1rem;
}

.deleteIcon {
width: 20px;
height: 20px;
}

.deleteButton {
opacity: 0;
transition: opacity 0.2s;
background: none;
border: none;
cursor: pointer;
padding: 4px;
border-radius: 9999px;
color: #6b7280;
}

.historyItem:hover .deleteButton,
.deleteButton:focus {
opacity: 1;
}

.deleteButton:hover {
color: #111827;
}

.modalOverlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
}

.modalContent {
background-color: white;
padding: 24px;
border-radius: 8px;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
max-width: 400px;
width: 100%;
}

.modalTitle {
font-size: 20px;
font-weight: 600;
margin-top: 0px;
margin-bottom: 16px;
}

.modalDescription {
margin-top: 0px;
margin-bottom: 16px;
}

.modalActions {
display: flex;
justify-content: flex-end;
gap: 16px;
}

.modalCancelButton,
.modalConfirmButton {
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
}

.modalCancelButton {
background-color: #f3f4f6;
color: #374151;
}

.modalConfirmButton {
background-color: #ef4444;
color: white;
}

.modalCancelButton:hover {
background-color: #e5e7eb;
}

.modalConfirmButton:hover {
background-color: #dc2626;
}
59 changes: 59 additions & 0 deletions app/frontend/src/components/HistoryItem/HistoryItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useState, useCallback } from "react";
import { useTranslation } from "react-i18next";
import styles from "./HistoryItem.module.css";
import { DefaultButton } from "@fluentui/react";
import { Delete24Regular } from "@fluentui/react-icons";

export interface HistoryData {
id: string;
title: string;
timestamp: number;
}

interface HistoryItemProps {
item: HistoryData;
onSelect: (id: string) => void;
onDelete: (id: string) => void;
}

export function HistoryItem({ item, onSelect, onDelete }: HistoryItemProps) {
const [isModalOpen, setIsModalOpen] = useState(false);

const handleDelete = useCallback(() => {
setIsModalOpen(false);
onDelete(item.id);
}, [item.id, onDelete]);

return (
<div className={styles.historyItem}>
<button onClick={() => onSelect(item.id)} className={styles.historyItemButton}>
<div className={styles.historyItemTitle}>{item.title}</div>
</button>
<button onClick={() => setIsModalOpen(true)} className={styles.deleteButton} aria-label="delete this chat history">
<Delete24Regular className={styles.deleteIcon} />
</button>
<DeleteHistoryModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} onConfirm={handleDelete} />
</div>
);
}

function DeleteHistoryModal({ isOpen, onClose, onConfirm }: { isOpen: boolean; onClose: () => void; onConfirm: () => void }) {
if (!isOpen) return null;
const { t } = useTranslation();
return (
<div className={styles.modalOverlay}>
<div className={styles.modalContent}>
<h2 className={styles.modalTitle}>{t("history.deleteModalTitle")}</h2>
<p className={styles.modalDescription}>{t("history.deleteModalDescription")}</p>
<div className={styles.modalActions}>
<DefaultButton onClick={onClose} className={styles.modalCancelButton}>
{t("history.cancelLabel")}
</DefaultButton>
<DefaultButton onClick={onConfirm} className={styles.modalConfirmButton}>
{t("history.deleteLabel")}
</DefaultButton>
</div>
</div>
</div>
);
}
1 change: 1 addition & 0 deletions app/frontend/src/components/HistoryItem/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./HistoryItem";
14 changes: 14 additions & 0 deletions app/frontend/src/components/HistoryPanel/HistoryPanel.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.group {
margin-top: 1rem;
}
.groupLabel {
font-size: 0.9rem;
font-weight: bold;
margin-top: 0.5rem;
margin-bottom: 0.2rem;
}

.footer {
display: flex;
justify-content: space-between;
}
Loading

0 comments on commit be26d31

Please sign in to comment.