diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..68dc84378 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,162 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/engine/reference/builder/#dockerignore-file + +**/.DS_Store +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/*.Dockerfile +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.log + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# VS Code +.vscode/ + +# Ignore other unnecessary files +*.bak +*.swp +.DS_Store +*.pdb +*.sqlite3 diff --git a/code/backend/pages/04_Configuration.py b/code/backend/pages/04_Configuration.py index 74294fd63..2ed84daa5 100644 --- a/code/backend/pages/04_Configuration.py +++ b/code/backend/pages/04_Configuration.py @@ -491,37 +491,43 @@ def validate_documents(): "Configuration saved successfully! Please restart the chat service for these changes to take effect." ) - with st.popover(":red[Reset configuration to defaults]"): - - # Close button with a custom class - if st.button("X", key="close_popup", help="Close popup"): - st.session_state["popup_open"] = False - st.rerun() - - st.write( - "**Resetting the configuration cannot be reversed, proceed with caution!**" - ) + @st.dialog("Reset Configuration", width="small") + def reset_config_dialog(): + st.write("**Resetting the configuration cannot be reversed. Proceed with caution!**") st.text_input('Enter "reset" to proceed', key="reset_configuration") if st.button( - ":red[Reset]", disabled=st.session_state["reset_configuration"] != "reset" + ":red[Reset]", + disabled=st.session_state.get("reset_configuration", "") != "reset", + key="confirm_reset" ): - try: - ConfigHelper.delete_config() - except ResourceNotFoundError: - pass - - for key in st.session_state: - del st.session_state[key] - + with st.spinner("Resetting Configuration to Default values..."): + try: + ConfigHelper.delete_config() + except ResourceNotFoundError: + pass + + ConfigHelper.clear_config() + st.session_state.clear() st.session_state["reset"] = True st.session_state["reset_configuration"] = "" + st.session_state["show_reset_dialog"] = False st.rerun() - if st.session_state.get("reset") is True: - st.success("Configuration reset successfully!") - del st.session_state["reset"] - del st.session_state["reset_configuration"] + # Reset configuration button + if st.button(":red[Reset configuration to defaults]"): + st.session_state["show_reset_dialog"] = True + + # Open the dialog if needed + if st.session_state.get("show_reset_dialog"): + reset_config_dialog() + st.session_state["show_reset_dialog"] = False + + # After reset success + if st.session_state.get("reset"): + st.success("Configuration reset successfully!") + del st.session_state["reset"] + del st.session_state["reset_configuration"] except Exception as e: logger.error(f"Error occurred: {e}") diff --git a/code/frontend/src/components/ChatHistoryListItemCell/ChatHistoryListItemCell.test.tsx b/code/frontend/src/components/ChatHistoryListItemCell/ChatHistoryListItemCell.test.tsx index 94ae37505..19d730d06 100644 --- a/code/frontend/src/components/ChatHistoryListItemCell/ChatHistoryListItemCell.test.tsx +++ b/code/frontend/src/components/ChatHistoryListItemCell/ChatHistoryListItemCell.test.tsx @@ -11,6 +11,17 @@ jest.mock('../../api/api', () => ({ historyDelete: jest.fn() })) +// Mocking TooltipHost from @fluentui/react +jest.mock('@fluentui/react', () => { + const actual = jest.requireActual('@fluentui/react'); + return { + ...actual, + TooltipHost: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + }; +}); + const conversation: Conversation = { id: '1', title: 'Test Chat', @@ -47,7 +58,7 @@ describe('ChatHistoryListItemCell', () => { }) test('renders the chat history item', () => { -render(); + render(); const titleElement = screen.getByText(/Test Chat/i) expect(titleElement).toBeInTheDocument() }) @@ -71,10 +82,10 @@ render(); // Check if the truncated title is in the document const truncatedTitle = screen.getByText(/A very long title that shoul .../i) expect(truncatedTitle).toBeInTheDocument() - }) + }) test('calls onSelect when clicked', () => { - render(); + render(); const item = screen.getByLabelText('chat history item') fireEvent.click(item) expect(mockOnSelect).toHaveBeenCalledWith(conversation) @@ -93,7 +104,7 @@ render(); // Expect that no content related to the title is rendered const titleElement = screen.queryByText(/Test Chat/i); expect(titleElement).not.toBeInTheDocument(); -}) + }) test('displays delete and edit buttons on hover', async () => { const mockAppStateUpdated = { @@ -132,7 +143,7 @@ render(); }) test('shows confirmation dialog and deletes item', async () => { - ;(historyDelete as jest.Mock).mockResolvedValueOnce({ + ; (historyDelete as jest.Mock).mockResolvedValueOnce({ ok: true, json: async () => ({}) }) @@ -156,7 +167,7 @@ render(); }) test('when delete API fails or return false', async () => { - ;(historyDelete as jest.Mock).mockResolvedValueOnce({ + ; (historyDelete as jest.Mock).mockResolvedValueOnce({ ok: false, json: async () => ({}) }) @@ -204,10 +215,10 @@ render(); const appStateWithRequestInitiated = { ...componentProps, isGenerating: true, - selectedConvId:'1' + selectedConvId: '1' } - render(); + render(); const item = screen.getByLabelText('chat history item') fireEvent.mouseEnter(item) const deleteButton = screen.getByTitle(/Delete/i) @@ -218,10 +229,10 @@ render(); }) test('does not disable buttons when request is not initiated', () => { - render(); - const item = screen.getByLabelText('chat history item') - fireEvent.mouseEnter(item) - const deleteButton = screen.getByTitle(/Delete/i) + render(); + const item = screen.getByLabelText('chat history item') + fireEvent.mouseEnter(item) + const deleteButton = screen.getByTitle(/Delete/i) const editButton = screen.getByTitle(/Edit/i) expect(deleteButton).not.toBeDisabled() @@ -245,12 +256,12 @@ render(); }) test('handles input onChange and onKeyDown ENTER events correctly', async () => { - ;(historyRename as jest.Mock).mockResolvedValueOnce({ + ; (historyRename as jest.Mock).mockResolvedValueOnce({ ok: true, json: async () => ({}) }) -render(); + render(); // Simulate hover to reveal Edit button const item = screen.getByLabelText('chat history item') fireEvent.mouseEnter(item) @@ -316,7 +327,7 @@ render(); test('Should hide the rename from when cancel it.', async () => { userEvent.setup() - render() + render() const item = screen.getByLabelText('chat history item') fireEvent.mouseEnter(item) // Wait for the Edit button to appear and click it @@ -336,10 +347,10 @@ render(); test.skip('handles rename save API failed', async () => { userEvent.setup() - ;(historyRename as jest.Mock).mockRejectedValue({ - ok: false, - json: async () => ({}) - }) + ; (historyRename as jest.Mock).mockRejectedValue({ + ok: false, + json: async () => ({}) + }) render(); // Simulate hover to reveal Edit button @@ -380,12 +391,12 @@ render(); date: new Date().toISOString() } - ;(historyRename as jest.Mock).mockResolvedValueOnce({ - ok: false, - json: async () => ({ message: 'Title already exists' }) - }) + ; (historyRename as jest.Mock).mockResolvedValueOnce({ + ok: false, + json: async () => ({ message: 'Title already exists' }) + }) -render(); + render(); const item = screen.getByLabelText('chat history item') fireEvent.mouseEnter(item) @@ -406,7 +417,7 @@ render(); test('triggers edit functionality when Enter key is pressed', async () => { - ;(historyRename as jest.Mock).mockResolvedValueOnce({ + ; (historyRename as jest.Mock).mockResolvedValueOnce({ ok: true, json: async () => ({ message: 'Title changed' }) }) @@ -438,12 +449,12 @@ render(); }) test('successfully saves edited title', async () => { - ;(historyRename as jest.Mock).mockResolvedValueOnce({ + ; (historyRename as jest.Mock).mockResolvedValueOnce({ ok: true, json: async () => ({}) }) -render(); + render(); const item = screen.getByLabelText('chat history item') fireEvent.mouseEnter(item) @@ -500,7 +511,7 @@ render(); /////// test('opens delete confirmation dialog when Enter key is pressed on the Delete button', async () => { -render(); + render(); const item = screen.getByLabelText('chat history item') fireEvent.mouseEnter(item) @@ -511,7 +522,7 @@ render(); }) test('opens delete confirmation dialog when Space key is pressed on the Delete button', async () => { -render(); + render(); const item = screen.getByLabelText('chat history item') fireEvent.mouseEnter(item) @@ -522,7 +533,7 @@ render(); }) test('opens edit input when Space key is pressed on the Edit button', async () => { - render(); + render(); const item = screen.getByLabelText('chat history item') fireEvent.mouseEnter(item) @@ -534,7 +545,7 @@ render(); }) test('opens edit input when Enter key is pressed on the Edit button', async () => { - render(); + render(); const item = screen.getByLabelText('chat history item') fireEvent.mouseEnter(item) @@ -547,11 +558,11 @@ render(); test('handles rename save when the updated text is equal to initial text', async () => { userEvent.setup() - ;(historyRename as jest.Mock).mockRejectedValue({ - ok: false, - json: async () => ({ message: 'Title not changed' }) - }) - render() + ; (historyRename as jest.Mock).mockRejectedValue({ + ok: false, + json: async () => ({ message: 'Title not changed' }) + }) + render() // Simulate hover to reveal Edit button const item = screen.getByLabelText('chat history item') @@ -573,26 +584,26 @@ render(); //fireEvent.change(inputItem, { target: { value: 'Test Chat' } }); }) expect(historyRename).not.toHaveBeenCalled() -}) -test('Should hide the rename from on Enter or space .', async () => { - userEvent.setup() - - render() - const item = screen.getByLabelText('chat history item') - fireEvent.mouseEnter(item) - // Wait for the Edit button to appear and click it - await waitFor(() => { - const editButton = screen.getByTitle(/Edit/i) - fireEvent.click(editButton) }) + test('Should hide the rename from on Enter or space .', async () => { + userEvent.setup() - const editButton =screen.getByRole('button', { name: 'cancel edit title' }) - fireEvent.keyDown(editButton, { key: 'Enter', code: 'Enter', charCode: 13 }) + render() + const item = screen.getByLabelText('chat history item') + fireEvent.mouseEnter(item) + // Wait for the Edit button to appear and click it + await waitFor(() => { + const editButton = screen.getByTitle(/Edit/i) + fireEvent.click(editButton) + }) - // Wait for the error to be hidden after 5 seconds - await waitFor(() => { - const input = screen.queryByLabelText('confirm new title') - expect(input).not.toBeInTheDocument() + const editButton = screen.getByRole('button', { name: 'cancel edit title' }) + fireEvent.keyDown(editButton, { key: 'Enter', code: 'Enter', charCode: 13 }) + + // Wait for the error to be hidden after 5 seconds + await waitFor(() => { + const input = screen.queryByLabelText('confirm new title') + expect(input).not.toBeInTheDocument() + }) }) }) -}) diff --git a/code/frontend/src/components/ChatHistoryListItemCell/ChatHistoryListItemCell.tsx b/code/frontend/src/components/ChatHistoryListItemCell/ChatHistoryListItemCell.tsx index ab8541e67..fc47ef2c1 100644 --- a/code/frontend/src/components/ChatHistoryListItemCell/ChatHistoryListItemCell.tsx +++ b/code/frontend/src/components/ChatHistoryListItemCell/ChatHistoryListItemCell.tsx @@ -7,10 +7,12 @@ import { DialogType, IconButton, ITextField, + ITooltipHostStyles, PrimaryButton, Stack, Text, TextField, + TooltipHost, } from "@fluentui/react"; import { useBoolean } from "@fluentui/react-hooks"; @@ -30,6 +32,11 @@ interface ChatHistoryListItemCellProps { toggleToggleSpinner: (toggler: boolean) => void; } + +const calloutProps = { gapSpace: 0 }; +const hostStyles: Partial = { root: { display: 'inline-block' } }; + + export const ChatHistoryListItemCell: React.FC< ChatHistoryListItemCellProps > = ({ @@ -51,6 +58,7 @@ export const ChatHistoryListItemCell: React.FC< const [textFieldFocused, setTextFieldFocused] = useState(false); const textFieldRef = useRef(null); const isSelected = item?.id === selectedConvId; + const tooltipId = 'tooltip'+ item?.id; const dialogContentProps = { type: DialogType.close, title: "Are you sure you want to delete this item?", @@ -257,7 +265,14 @@ export const ChatHistoryListItemCell: React.FC< ) : ( <> -
{truncatedTitle}
+
+ {truncatedTitle}
{(isSelected || isHovered) && (