Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
7 changes: 7 additions & 0 deletions app/backend/core/sessionhelper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import uuid


def create_session_id(config_chat_history_browser_enabled: bool) -> 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",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a reasonable addition according to packagephobia and snyk:

https://packagephobia.com/result?p=idb
https://snyk.io/advisor/npm-package/idb

"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
Loading