Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Frontend multi-language support #1690 #1790

Merged
merged 31 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
785a2f6
WIP: Frontend multi-language support #1690
bnodir Jul 7, 2024
e786334
Merge remote-tracking branch 'origin/main' into multiLang
bnodir Jul 13, 2024
ed8c1e9
WIP(2): Merge branch 'main' into multiLang
bnodir Jul 13, 2024
f764afd
WIP(4): Implement language switching for File Upload Feature
bnodir Jul 15, 2024
028bf67
Playwright fail fix
bnodir Jul 17, 2024
300aa16
Merge with main
bnodir Jul 18, 2024
b98976c
Merge branch 'multiLang2' into multiLang
bnodir Jul 18, 2024
1c22ceb
Merge fix
bnodir Jul 18, 2024
89d9d70
Merge branch 'main' into multiLang
bnodir Jul 19, 2024
79a2f46
Use fluent dropdown and add label to language picker
pamelafox Jul 19, 2024
34b78a6
Use string instead of String
pamelafox Jul 19, 2024
869ec95
Pin mypy
pamelafox Jul 19, 2024
9663e71
Reflecting changes based on review comments
bnodir Jul 24, 2024
2d72aa9
Reflecting changes based on review comments-2
bnodir Jul 24, 2024
7b91210
Merge branch 'main' into multiLang
bnodir Jul 24, 2024
e78fae9
Make the language picker optional
bnodir Jul 24, 2024
c7d8fe3
Fixes in docs/deploy_features.md
bnodir Jul 25, 2024
40b367a
Revert changes made to package-lock.json
bnodir Jul 25, 2024
fa45ead
minor fix
bnodir Jul 26, 2024
a76dd94
Merge branch 'Azure-Samples:main' into multiLang
bnodir Aug 2, 2024
fa1bdd9
Merge branch 'Azure-Samples:main' into multiLang
bnodir Aug 7, 2024
de68265
minor fix
bnodir Aug 7, 2024
07b818e
Merge branch 'multiLang' of https://github.com/bnodir/azure-search-op…
bnodir Aug 7, 2024
679e4bb
Merge branch 'Azure-Samples:main' into multiLang
bnodir Aug 13, 2024
98bd2b1
Merge branch 'main' into multiLang
pamelafox Aug 13, 2024
235c9d9
Playwright fail fix
bnodir Aug 16, 2024
06b15f5
Merge branch 'multiLang' of https://github.com/bnodir/azure-search-op…
bnodir Aug 16, 2024
f7f0de3
Use the language value in the API payload
bnodir Aug 19, 2024
443e414
merge
bnodir Aug 27, 2024
bf091f1
merge
bnodir Aug 27, 2024
6620ee9
Merge branch 'main' into multiLang
pamelafox Aug 30, 2024
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
284 changes: 281 additions & 3 deletions app/frontend/package-lock.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions app/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@
"@react-spring/web": "^9.7.3",
"marked": "^13.0.0",
"dompurify": "^3.0.6",
"i18next": "^23.11.5",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.5.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.23.1",
"react-helmet": "^6.1.0",
bnodir marked this conversation as resolved.
Show resolved Hide resolved
"react-i18next": "^14.1.2",
"ndjson-readablestream": "^1.2.0",
"react-syntax-highlighter": "^15.5.0",
"scheduler": "^0.20.2"
Expand All @@ -32,6 +37,7 @@
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"@types/react-helmet": "^6.1.11",
"prettier": "^3.0.3",
"typescript": "^5.5.3",
"@types/react-syntax-highlighter": "^15.5.13",
Expand Down
9 changes: 5 additions & 4 deletions app/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Stack, Pivot, PivotItem } from "@fluentui/react";

import { useTranslation } from "react-i18next";
import styles from "./AnalysisPanel.module.css";

import { SupportingContent } from "../SupportingContent";
Expand Down Expand Up @@ -30,6 +30,7 @@ export const AnalysisPanel = ({ answer, activeTab, activeCitation, citationHeigh
const [citation, setCitation] = useState("");

const client = useLogin ? useMsal().instance : undefined;
const { t } = useTranslation();

const fetchCitation = async () => {
const token = client ? await getToken(client) : undefined;
Expand Down Expand Up @@ -78,21 +79,21 @@ export const AnalysisPanel = ({ answer, activeTab, activeCitation, citationHeigh
>
<PivotItem
itemKey={AnalysisPanelTabs.ThoughtProcessTab}
headerText="Thought process"
headerText={t("headerTexts.thoughtProcess")}
headerButtonProps={isDisabledThoughtProcessTab ? pivotItemDisabledStyle : undefined}
>
<ThoughtProcess thoughts={answer.context.thoughts || []} />
</PivotItem>
<PivotItem
itemKey={AnalysisPanelTabs.SupportingContentTab}
headerText="Supporting content"
headerText={t("headerTexts.supportingContent")}
headerButtonProps={isDisabledSupportingContentTab ? pivotItemDisabledStyle : undefined}
>
<SupportingContent supportingContent={answer.context.data_points} />
</PivotItem>
<PivotItem
itemKey={AnalysisPanelTabs.CitationTab}
headerText="Citation"
headerText={t("headerTexts.citation")}
headerButtonProps={isDisabledCitationTab ? pivotItemDisabledStyle : undefined}
>
{renderFileViewer()}
Expand Down
9 changes: 5 additions & 4 deletions app/frontend/src/components/Answer/Answer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useMemo } from "react";
import { Stack, IconButton } from "@fluentui/react";
import { useTranslation } from "react-i18next";
import DOMPurify from "dompurify";

import styles from "./Answer.module.css";
Expand Down Expand Up @@ -39,7 +40,7 @@ export const Answer = ({
const followupQuestions = answer.context?.followup_questions;
const messageContent = answer.message.content;
const parsedAnswer = useMemo(() => parseAnswerToHtml(messageContent, isStreaming, onCitationClicked), [answer]);

const { t } = useTranslation();
const sanitizedAnswerHtml = DOMPurify.sanitize(parsedAnswer.answerHtml);

return (
Expand All @@ -51,15 +52,15 @@ export const Answer = ({
<IconButton
style={{ color: "black" }}
iconProps={{ iconName: "Lightbulb" }}
title="Show thought process"
title={t("tooltips.showThoughtProcess")}
ariaLabel="Show thought process"
onClick={() => onThoughtProcessClicked()}
disabled={!answer.context.thoughts?.length}
/>
<IconButton
style={{ color: "black" }}
iconProps={{ iconName: "ClipboardList" }}
title="Show supporting content"
title={t("tooltips.showSupportingContent")}
ariaLabel="Show supporting content"
onClick={() => onSupportingContentClicked()}
disabled={!answer.context.data_points}
Expand All @@ -77,7 +78,7 @@ export const Answer = ({
{!!parsedAnswer.citations.length && (
<Stack.Item>
<Stack horizontal wrap tokens={{ childrenGap: 5 }}>
<span className={styles.citationLearnMore}>Citations:</span>
<span className={styles.citationLearnMore}>{t("headerTexts.citation")}:</span>
{parsedAnswer.citations.map((x, i) => {
const path = getCitationFilePath(x);
return (
Expand Down
4 changes: 3 additions & 1 deletion app/frontend/src/components/Answer/AnswerLoading.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Stack } from "@fluentui/react";
import { animated, useSpring } from "@react-spring/web";
import { useTranslation } from "react-i18next";

import styles from "./Answer.module.css";
import { AnswerIcon } from "./AnswerIcon";

export const AnswerLoading = () => {
const { t, i18n } = useTranslation();
const animatedStyles = useSpring({
from: { opacity: 0 },
to: { opacity: 1 }
Expand All @@ -16,7 +18,7 @@ export const AnswerLoading = () => {
<AnswerIcon />
<Stack.Item grow>
<p className={styles.answerText}>
Generating answer
{t("generatingAnswer")}
<span className={styles.loadingdots} />
</p>
</Stack.Item>
Expand Down
5 changes: 3 additions & 2 deletions app/frontend/src/components/Answer/SpeechOutputAzure.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from "react";

import { useTranslation } from "react-i18next";
import { IconButton } from "@fluentui/react";

interface Props {
Expand All @@ -10,6 +10,7 @@ let audio = new Audio();

export const SpeechOutputAzure = ({ url }: Props) => {
const [isPlaying, setIsPlaying] = useState(false);
const { t } = useTranslation();

const startOrStopAudio = async () => {
if (isPlaying) {
Expand All @@ -35,7 +36,7 @@ export const SpeechOutputAzure = ({ url }: Props) => {
<IconButton
style={{ color: color }}
iconProps={{ iconName: "Volume3" }}
title="Speak answer"
title={t("tooltips.speakAnswer")}
ariaLabel="Speak answer"
onClick={() => startOrStopAudio()}
disabled={!url}
Expand Down
18 changes: 13 additions & 5 deletions app/frontend/src/components/Answer/SpeechOutputBrowser.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useState } from "react";
import { IconButton } from "@fluentui/react";
import { useTranslation } from "react-i18next";
import { supportedLngs } from "../../i18n/config";

interface Props {
answer: string;
Expand All @@ -15,19 +17,25 @@ try {
console.error("SpeechSynthesis is not supported");
}

const getUtterance = function (text: string) {
const getUtterance = function (text: string, lngCode: string) {
if (synth) {
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = "en-US";
utterance.lang = lngCode;
utterance.volume = 1;
utterance.rate = 1;
utterance.pitch = 1;
utterance.voice = synth.getVoices().filter((voice: SpeechSynthesisVoice) => voice.lang === "en-US")[0];
utterance.voice = synth.getVoices().filter((voice: SpeechSynthesisVoice) => voice.lang === lngCode)[0];
bnodir marked this conversation as resolved.
Show resolved Hide resolved
return utterance;
}
};

export const SpeechOutputBrowser = ({ answer }: Props) => {
const { t, i18n } = useTranslation();
const currentLng = i18n.language;
let lngCode = supportedLngs[currentLng]?.locale;
if (!lngCode) {
lngCode = "en-US";
}
const [isPlaying, setIsPlaying] = useState<boolean>(false);

const startOrStopSpeech = (answer: string) => {
Expand All @@ -37,7 +45,7 @@ export const SpeechOutputBrowser = ({ answer }: Props) => {
setIsPlaying(false);
return;
}
const utterance: SpeechSynthesisUtterance | undefined = getUtterance(answer);
const utterance: SpeechSynthesisUtterance | undefined = getUtterance(answer, lngCode);

if (!utterance) {
return;
Expand All @@ -62,7 +70,7 @@ export const SpeechOutputBrowser = ({ answer }: Props) => {
<IconButton
style={{ color: color }}
iconProps={{ iconName: "Volume3" }}
title="Speak answer"
title={t("tooltips.speakAnswer")}
ariaLabel="Speak answer"
onClick={() => startOrStopSpeech(answer)}
disabled={!synth}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Delete24Regular } from "@fluentui/react-icons";
import { Button } from "@fluentui/react-components";
import { useTranslation } from "react-i18next";

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

Expand All @@ -10,10 +11,11 @@ interface Props {
}

export const ClearChatButton = ({ className, disabled, onClick }: Props) => {
const { t, i18n } = useTranslation();
return (
<div className={`${styles.container} ${className ?? ""}`}>
<Button icon={<Delete24Regular />} disabled={disabled} onClick={onClick}>
{"Clear chat"}
{t("clearChat")}
</Button>
</div>
);
Expand Down
18 changes: 6 additions & 12 deletions app/frontend/src/components/Example/ExampleList.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
import { Example } from "./Example";
import { useTranslation } from "react-i18next";

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

const DEFAULT_EXAMPLES: string[] = [
"What is included in my Northwind Health Plus plan that is not in standard?",
"What happens in a performance review?",
"What does a Product Manager do?"
];

const GPT4V_EXAMPLES: string[] = [
"Compare the impact of interest rates and GDP in financial markets.",
"What is the expected trend for the S&P 500 index over the next five years? Compare it to the past S&P 500 performance",
"Can you identify any correlation between oil prices and stock market trends?"
];

interface Props {
onExampleClicked: (value: string) => void;
useGPT4V?: boolean;
}

export const ExampleList = ({ onExampleClicked, useGPT4V }: Props) => {
const { t } = useTranslation();

const DEFAULT_EXAMPLES: string[] = [t("defaultExamples.1"), t("defaultExamples.2"), t("defaultExamples.3")];
const GPT4V_EXAMPLES: string[] = [t("gpt4vExamples.1"), t("gpt4vExamples.2"), t("gpt4vExamples.3")];

return (
<ul className={styles.examplesNavList}>
{(useGPT4V ? GPT4V_EXAMPLES : DEFAULT_EXAMPLES).map((question, i) => (
Expand Down
17 changes: 9 additions & 8 deletions app/frontend/src/components/GPT4VSettings/GPT4VSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useEffect, useState } from "react";
import { Stack, Checkbox, ICheckboxProps, IDropdownOption, IDropdownProps, Dropdown } from "@fluentui/react";
import { useId } from "@fluentui/react-hooks";
import { useTranslation } from "react-i18next";

import styles from "./GPT4VSettings.module.css";
import { GPT4VInput } from "../../api";
import { HelpCallout } from "../../components/HelpCallout";
import { toolTipText } from "../../i18n/tooltips.js";

interface Props {
gpt4vInputs: GPT4VInput;
Expand Down Expand Up @@ -39,37 +39,38 @@ export const GPT4VSettings = ({ updateGPT4VInputs, updateUseGPT4V, isUseGPT4V, g
const useGPT4VFieldId = useId("useGPT4VField");
const gpt4VInputId = useId("gpt4VInput");
const gpt4VInputFieldId = useId("gpt4VInputField");
const { t } = useTranslation();

return (
<Stack className={styles.container} tokens={{ childrenGap: 10 }}>
<Checkbox
id={useGPT4VFieldId}
checked={useGPT4V}
label="Use GPT vision model"
label={t("labels.useGPT4V")}
onChange={onuseGPT4V}
aria-labelledby={useGPT4VId}
onRenderLabel={(props: ICheckboxProps | undefined) => (
<HelpCallout labelId={useGPT4VId} fieldId={useGPT4VFieldId} helpText={toolTipText.useGPT4Vision} label={props?.label} />
<HelpCallout labelId={useGPT4VId} fieldId={useGPT4VFieldId} helpText={t("helpTexts.useGPT4Vision")} label={props?.label} />
)}
/>
{useGPT4V && (
<Dropdown
id={gpt4VInputFieldId}
selectedKey={vectorFieldOption}
label="GPT vision model inputs"
label={t("labels.gpt4VInput.label")}
options={[
{
key: GPT4VInput.TextAndImages,
text: "Images and text"
text: t("labels.gpt4VInput.options.textAndImages")
},
{ text: "Images", key: GPT4VInput.Images },
{ text: "Text", key: GPT4VInput.Texts }
{ text: t("labels.gpt4VInput.options.images"), key: GPT4VInput.Images },
{ text: t("labels.gpt4VInput.options.texts"), key: GPT4VInput.Texts }
]}
required
onChange={onSetGPT4VInput}
aria-labelledby={gpt4VInputId}
onRenderLabel={(props: IDropdownProps | undefined) => (
<HelpCallout labelId={gpt4VInputId} fieldId={gpt4VInputFieldId} helpText={toolTipText.gpt4VisionInputs} label={props?.label} />
<HelpCallout labelId={gpt4VInputId} fieldId={gpt4VInputFieldId} helpText={t("helpTexts.gpt4VisionInputs")} label={props?.label} />
)}
/>
)}
Expand Down
13 changes: 11 additions & 2 deletions app/frontend/src/components/HelpCallout/HelpCallout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ITextFieldProps, DefaultButton, IconButton, IButtonStyles, Callout, IStackTokens, Stack, IStackStyles, initializeIcons } from "@fluentui/react";
import { useBoolean, useId } from "@fluentui/react-hooks";
import { useTranslation } from "react-i18next";

const stackTokens: IStackTokens = {
childrenGap: 4,
Expand All @@ -21,20 +22,28 @@ export const HelpCallout = (props: IHelpCalloutProps): JSX.Element => {
const [isCalloutVisible, { toggle: toggleIsCalloutVisible }] = useBoolean(false);
const descriptionId: string = useId("description");
const iconButtonId: string = useId("iconButton");
const { t } = useTranslation();

return (
<>
<Stack horizontal verticalAlign="center" tokens={stackTokens}>
<label id={props.labelId} htmlFor={props.fieldId}>
{props.label}
</label>
<IconButton id={iconButtonId} iconProps={iconProps} title="Info" ariaLabel="Info" onClick={toggleIsCalloutVisible} styles={iconButtonStyles} />
<IconButton
id={iconButtonId}
iconProps={iconProps}
title={t("tooltips.info")}
ariaLabel="Info"
onClick={toggleIsCalloutVisible}
styles={iconButtonStyles}
/>
</Stack>
{isCalloutVisible && (
<Callout target={"#" + iconButtonId} setInitialFocus onDismiss={toggleIsCalloutVisible} ariaDescribedBy={descriptionId} role="alertdialog">
<Stack tokens={stackTokens} horizontalAlign="start" styles={labelCalloutStackStyles}>
<span id={descriptionId}>{props.helpText}</span>
<DefaultButton onClick={toggleIsCalloutVisible}>Close</DefaultButton>
<DefaultButton onClick={toggleIsCalloutVisible}>{t("labels.closeButton")}</DefaultButton>
</Stack>
</Callout>
)}
Expand Down
4 changes: 3 additions & 1 deletion app/frontend/src/components/LoginButton/LoginButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DefaultButton } from "@fluentui/react";
import { useMsal } from "@azure/msal-react";
import { useTranslation } from "react-i18next";

import styles from "./LoginButton.module.css";
import { getRedirectUri, loginRequest, appServicesLogout, getUsername, checkLoggedIn } from "../../authConfig";
Expand All @@ -11,6 +12,7 @@ export const LoginButton = () => {
const { loggedIn, setLoggedIn } = useContext(LoginContext);
const activeAccount = instance.getActiveAccount();
const [username, setUsername] = useState("");
const { t } = useTranslation();

useEffect(() => {
const fetchUsername = async () => {
Expand Down Expand Up @@ -55,7 +57,7 @@ export const LoginButton = () => {
};
return (
<DefaultButton
text={loggedIn ? `Logout\n${username}` : "Login"}
text={loggedIn ? `${t("logout")}\n${username}` : `${t("login")}`}
className={styles.loginButton}
onClick={loggedIn ? handleLogoutPopup : handleLoginPopup}
></DefaultButton>
Expand Down
Loading