diff --git a/.github/workflows/scripts/build_push.sh b/.github/workflows/scripts/build_push.sh
index 89246006f..ac9b07369 100755
--- a/.github/workflows/scripts/build_push.sh
+++ b/.github/workflows/scripts/build_push.sh
@@ -52,6 +52,9 @@ for MEGA_SVC in $1; do
docker_build ${IMAGE_NAME}
cd ui
docker_build ${IMAGE_NAME}-ui docker/Dockerfile
+ if [ "$MEGA_SVC" == "ChatQnA" ];then
+ docker_build ${IMAGE_NAME}-conversation-ui docker/Dockerfile.react
+ fi
;;
"AudioQnA"|"SearchQnA"|"VisualQnA")
echo "Not supported yet"
diff --git a/ChatQnA/assets/img/conversation_ui_init.png b/ChatQnA/assets/img/conversation_ui_init.png
new file mode 100644
index 000000000..6bd5f2447
Binary files /dev/null and b/ChatQnA/assets/img/conversation_ui_init.png differ
diff --git a/ChatQnA/assets/img/conversation_ui_response.png b/ChatQnA/assets/img/conversation_ui_response.png
new file mode 100644
index 000000000..be9ed6045
Binary files /dev/null and b/ChatQnA/assets/img/conversation_ui_response.png differ
diff --git a/ChatQnA/assets/img/conversation_ui_upload.png b/ChatQnA/assets/img/conversation_ui_upload.png
new file mode 100644
index 000000000..b05c33819
Binary files /dev/null and b/ChatQnA/assets/img/conversation_ui_upload.png differ
diff --git a/ChatQnA/docker/ui/docker/Dockerfile.react b/ChatQnA/docker/ui/docker/Dockerfile.react
new file mode 100644
index 000000000..277546a9b
--- /dev/null
+++ b/ChatQnA/docker/ui/docker/Dockerfile.react
@@ -0,0 +1,22 @@
+FROM node as vite-app
+
+COPY . /usr/app
+WORKDIR /usr/app/react
+
+ARG BACKEND_SERVICE_ENDPOINT
+ARG DATAPREP_SERVICE_ENDPOINT
+ENV VITE_BACKEND_SERVICE_ENDPOINT=$BACKEND_SERVICE_ENDPOINT
+ENV VITE_DATA_PREP_SERVICE_URL=$DATAPREP_SERVICE_ENDPOINT
+
+RUN ["npm", "install"]
+RUN ["npm", "run", "build"]
+
+
+FROM nginx:alpine
+EXPOSE 80
+
+
+COPY --from=vite-app /usr/app/react/nginx.conf /etc/nginx/conf.d/default.conf
+COPY --from=vite-app /usr/app/react/dist /usr/share/nginx/html
+
+ENTRYPOINT ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/ChatQnA/docker/ui/react/.env b/ChatQnA/docker/ui/react/.env
new file mode 100644
index 000000000..e5d52f421
--- /dev/null
+++ b/ChatQnA/docker/ui/react/.env
@@ -0,0 +1,2 @@
+VITE_BACKEND_SERVICE_ENDPOINT=http://backend_address:8888/v1/chatqna
+VITE_DATA_PREP_SERVICE_URL=http://backend_address:6007/v1/dataprep
\ No newline at end of file
diff --git a/ChatQnA/docker/ui/react/.eslintrc.cjs b/ChatQnA/docker/ui/react/.eslintrc.cjs
new file mode 100644
index 000000000..78174f683
--- /dev/null
+++ b/ChatQnA/docker/ui/react/.eslintrc.cjs
@@ -0,0 +1,11 @@
+module.exports = {
+ root: true,
+ env: { browser: true, es2020: true },
+ extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended"],
+ ignorePatterns: ["dist", ".eslintrc.cjs"],
+ parser: "@typescript-eslint/parser",
+ plugins: ["react-refresh"],
+ rules: {
+ "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
+ },
+};
diff --git a/ChatQnA/docker/ui/react/.gitignore b/ChatQnA/docker/ui/react/.gitignore
new file mode 100644
index 000000000..a547bf36d
--- /dev/null
+++ b/ChatQnA/docker/ui/react/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/ChatQnA/docker/ui/react/README.md b/ChatQnA/docker/ui/react/README.md
new file mode 100644
index 000000000..fd5a2d342
--- /dev/null
+++ b/ChatQnA/docker/ui/react/README.md
@@ -0,0 +1,32 @@
+
ChatQnA Conversational UI
+
+### 📸 Project Screenshots
+
+![project-screenshot](../../../assets/img/conversation_ui_init.png)
+![project-screenshot](../../../assets/img/conversation_ui_response.png)
+![project-screenshot](../../../assets/img/conversation_ui_upload.png)
+
+🧐 Features
+
+Here're some of the project's features:
+
+- Start a Text Chat:Initiate a text chat with the ability to input written conversations, where the dialogue content can also be customized based on uploaded files.
+- Context Awareness: The AI assistant maintains the context of the conversation, understanding references to previous statements or questions. This allows for more natural and coherent exchanges.
+- Upload File: The choice between uploading locally or copying a remote link. Chat according to uploaded knowledge base.
+- Clear: Clear the record of the current dialog box without retaining the contents of the dialog box.
+- Chat history: Historical chat records can still be retained after refreshing, making it easier for users to view the context.
+- Conversational Chat : The application maintains a history of the conversation, allowing users to review previous messages and the AI to refer back to earlier points in the dialogue when necessary.
+
+🛠️ Get it Running:
+
+1. Clone the repo.
+
+2. cd command to the current folder.
+
+3. Modify the required .env variables.
+ ```
+ DOC_BASE_URL = ''
+ ```
+4. Execute `npm install` to install the corresponding dependencies.
+
+5. Execute `npm run dev` in both environments
diff --git a/ChatQnA/docker/ui/react/index.html b/ChatQnA/docker/ui/react/index.html
new file mode 100644
index 000000000..fbe87e0fd
--- /dev/null
+++ b/ChatQnA/docker/ui/react/index.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+ Conversations UI
+
+
+
+
+
+
diff --git a/ChatQnA/docker/ui/react/nginx.conf b/ChatQnA/docker/ui/react/nginx.conf
new file mode 100644
index 000000000..00433fcda
--- /dev/null
+++ b/ChatQnA/docker/ui/react/nginx.conf
@@ -0,0 +1,20 @@
+server {
+ listen 80;
+
+ gzip on;
+ gzip_proxied any;
+ gzip_comp_level 6;
+ gzip_buffers 16 8k;
+ gzip_http_version 1.1;
+ gzip_types font/woff2 text/css application/javascript application/json application/font-woff application/font-tff image/gif image/png image/svg+xml application/octet-stream;
+
+ location / {
+ root /usr/share/nginx/html;
+ index index.html index.htm;
+ try_files $uri $uri/ /index.html =404;
+
+ location ~* \.(gif|jpe?g|png|webp|ico|svg|css|js|mp4|woff2)$ {
+ expires 1d;
+ }
+ }
+}
\ No newline at end of file
diff --git a/ChatQnA/docker/ui/react/package.json b/ChatQnA/docker/ui/react/package.json
new file mode 100644
index 000000000..3760ed909
--- /dev/null
+++ b/ChatQnA/docker/ui/react/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "ui",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "preview": "vite preview",
+ "test": "vitest"
+ },
+ "dependencies": {
+ "@mantine/core": "^7.10.0",
+ "@mantine/hooks": "^7.10.0",
+ "@mantine/notifications": "^7.10.2",
+ "@microsoft/fetch-event-source": "^2.0.1",
+ "@reduxjs/toolkit": "^2.2.5",
+ "@tabler/icons-react": "^3.5.0",
+ "axios": "^1.7.2",
+ "luxon": "^3.4.4",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-redux": "^9.1.2"
+ },
+ "devDependencies": {
+ "@testing-library/react": "^16.0.0",
+ "@types/luxon": "^3.4.2",
+ "@types/node": "^20.12.12",
+ "@types/react": "^18.2.66",
+ "@types/react-dom": "^18.2.22",
+ "@typescript-eslint/eslint-plugin": "^7.2.0",
+ "@typescript-eslint/parser": "^7.2.0",
+ "@vitejs/plugin-react": "^4.2.1",
+ "eslint": "^8.57.0",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-refresh": "^0.4.6",
+ "jsdom": "^24.1.0",
+ "postcss": "^8.4.38",
+ "postcss-preset-mantine": "^1.15.0",
+ "postcss-simple-vars": "^7.0.1",
+ "sass": "1.64.2",
+ "typescript": "^5.2.2",
+ "vite": "^5.2.13",
+ "vitest": "^1.6.0"
+ }
+}
diff --git a/ChatQnA/docker/ui/react/postcss.config.cjs b/ChatQnA/docker/ui/react/postcss.config.cjs
new file mode 100644
index 000000000..e817f567b
--- /dev/null
+++ b/ChatQnA/docker/ui/react/postcss.config.cjs
@@ -0,0 +1,14 @@
+module.exports = {
+ plugins: {
+ "postcss-preset-mantine": {},
+ "postcss-simple-vars": {
+ variables: {
+ "mantine-breakpoint-xs": "36em",
+ "mantine-breakpoint-sm": "48em",
+ "mantine-breakpoint-md": "62em",
+ "mantine-breakpoint-lg": "75em",
+ "mantine-breakpoint-xl": "88em",
+ },
+ },
+ },
+};
diff --git a/ChatQnA/docker/ui/react/public/vite.svg b/ChatQnA/docker/ui/react/public/vite.svg
new file mode 100644
index 000000000..e7b8dfb1b
--- /dev/null
+++ b/ChatQnA/docker/ui/react/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ChatQnA/docker/ui/react/src/App.scss b/ChatQnA/docker/ui/react/src/App.scss
new file mode 100644
index 000000000..187764a17
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/App.scss
@@ -0,0 +1,42 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+@import "./styles/styles";
+
+.root {
+ @include flex(row, nowrap, flex-start, flex-start);
+}
+
+.layout-wrapper {
+ @include absolutes;
+
+ display: grid;
+
+ width: 100%;
+ height: 100%;
+
+ grid-template-columns: 80px auto;
+ grid-template-rows: 1fr;
+}
+
+/* ===== Scrollbar CSS ===== */
+/* Firefox */
+* {
+ scrollbar-width: thin;
+ scrollbar-color: #d6d6d6 #ffffff;
+}
+
+/* Chrome, Edge, and Safari */
+*::-webkit-scrollbar {
+ width: 8px;
+}
+
+*::-webkit-scrollbar-track {
+ background: #ffffff;
+}
+
+*::-webkit-scrollbar-thumb {
+ background-color: #d6d6d6;
+ border-radius: 16px;
+ border: 4px double #dedede;
+}
diff --git a/ChatQnA/docker/ui/react/src/App.tsx b/ChatQnA/docker/ui/react/src/App.tsx
new file mode 100644
index 000000000..4be4fa5bb
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/App.tsx
@@ -0,0 +1,34 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import "./App.scss"
+import { MantineProvider } from "@mantine/core"
+import '@mantine/notifications/styles.css';
+import { SideNavbar, SidebarNavList } from "./components/sidebar/sidebar"
+import { IconMessages } from "@tabler/icons-react"
+import UserInfoModal from "./components/UserInfoModal/UserInfoModal"
+import Conversation from "./components/Conversation/Conversation"
+import { Notifications } from '@mantine/notifications';
+
+const title = "Chat QnA"
+const navList: SidebarNavList = [
+ { icon: IconMessages, label: title }
+]
+
+function App() {
+
+ return (
+
+
+
+
+
+ )
+}
+
+export default App
diff --git a/ChatQnA/docker/ui/react/src/__tests__/util.test.ts b/ChatQnA/docker/ui/react/src/__tests__/util.test.ts
new file mode 100644
index 000000000..e67ba2c86
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/__tests__/util.test.ts
@@ -0,0 +1,14 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import { describe, expect, test } from "vitest";
+import { getCurrentTimeStamp, uuidv4 } from "../common/util";
+
+describe("unit tests", () => {
+ test("check UUID is of length 36", () => {
+ expect(uuidv4()).toHaveLength(36);
+ });
+ test("check TimeStamp generated is of unix", () => {
+ expect(getCurrentTimeStamp()).toBe(Math.floor(Date.now() / 1000));
+ });
+});
diff --git a/ChatQnA/docker/ui/react/src/assets/opea-icon-black.svg b/ChatQnA/docker/ui/react/src/assets/opea-icon-black.svg
new file mode 100644
index 000000000..d4e5cfda9
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/assets/opea-icon-black.svg
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ChatQnA/docker/ui/react/src/assets/opea-icon-color.svg b/ChatQnA/docker/ui/react/src/assets/opea-icon-color.svg
new file mode 100644
index 000000000..790151171
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/assets/opea-icon-color.svg
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ChatQnA/docker/ui/react/src/assets/react.svg b/ChatQnA/docker/ui/react/src/assets/react.svg
new file mode 100644
index 000000000..6c87de9bb
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ChatQnA/docker/ui/react/src/common/client.ts b/ChatQnA/docker/ui/react/src/common/client.ts
new file mode 100644
index 000000000..7512f73e3
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/common/client.ts
@@ -0,0 +1,8 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import axios from "axios";
+
+//add iterceptors to add any request headers
+
+export default axios;
diff --git a/ChatQnA/docker/ui/react/src/common/util.ts b/ChatQnA/docker/ui/react/src/common/util.ts
new file mode 100644
index 000000000..df65b2d8e
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/common/util.ts
@@ -0,0 +1,12 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+export const getCurrentTimeStamp = () => {
+ return Math.floor(Date.now() / 1000);
+};
+
+export const uuidv4 = () => {
+ return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) =>
+ (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16),
+ );
+};
diff --git a/ChatQnA/docker/ui/react/src/components/Conversation/Conversation.tsx b/ChatQnA/docker/ui/react/src/components/Conversation/Conversation.tsx
new file mode 100644
index 000000000..02736d8bd
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/components/Conversation/Conversation.tsx
@@ -0,0 +1,156 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import { KeyboardEventHandler, SyntheticEvent, useEffect, useRef, useState } from 'react'
+import styleClasses from "./conversation.module.scss"
+import { ActionIcon, Group, Textarea, Title, rem } from '@mantine/core'
+import { IconArrowRight, IconFilePlus, IconMessagePlus } from '@tabler/icons-react'
+import { conversationSelector, doConversation, newConversation } from '../../redux/Conversation/ConversationSlice'
+import { ConversationMessage } from '../Message/conversationMessage'
+import { useAppDispatch, useAppSelector } from '../../redux/store'
+import { Message, MessageRole } from '../../redux/Conversation/Conversation'
+import { getCurrentTimeStamp } from '../../common/util'
+import { useDisclosure } from '@mantine/hooks'
+import DataSource from './DataSource'
+import { ConversationSideBar } from './ConversationSideBar'
+
+type ConversationProps = {
+ title:string
+}
+
+const Conversation = ({ title }: ConversationProps) => {
+
+ const [prompt, setPrompt] = useState("")
+ const promptInputRef = useRef(null)
+ const [fileUploadOpened, { open: openFileUpload, close: closeFileUpload }] = useDisclosure(false);
+
+ const { conversations, onGoingResult, selectedConversationId } = useAppSelector(conversationSelector)
+ const dispatch = useAppDispatch();
+ const selectedConversation = conversations.find(x=>x.conversationId===selectedConversationId)
+
+ const scrollViewport = useRef(null)
+
+ const toSend = "Enter"
+
+ const systemPrompt: Partial = {
+ role: MessageRole.System,
+ content: "You are helpful assistant",
+ };
+
+
+ const handleSubmit = () => {
+
+ const userPrompt: Message = {
+ role: MessageRole.User,
+ content: prompt,
+ time: getCurrentTimeStamp()
+ };
+ let messages: Partial[] = [];
+ if(selectedConversation){
+ messages = selectedConversation.Messages.map(message => {
+ return {role:message.role, content:message.content}
+ })
+ }
+
+ messages = [systemPrompt, ...messages]
+
+ doConversation({
+ conversationId: selectedConversationId,
+ userPrompt,
+ messages,
+ model: "Intel/neural-chat-7b-v3-3",
+ })
+ setPrompt("")
+ }
+
+ const scrollToBottom = () => {
+ scrollViewport.current!.scrollTo({ top: scrollViewport.current!.scrollHeight })
+ }
+
+ useEffect(() => {
+ scrollToBottom()
+ }, [onGoingResult, selectedConversation?.Messages])
+
+ const handleKeyDown: KeyboardEventHandler = (event) => {
+ if (!event.shiftKey && event.key === toSend) {
+ handleSubmit()
+ setTimeout(() => {
+ setPrompt("")
+ }, 1)
+ }
+ }
+
+
+
+ const handleNewConversation = () => {
+ dispatch(newConversation())
+ }
+
+ const handleChange = (event: SyntheticEvent) => {
+ event.preventDefault()
+ setPrompt((event.target as HTMLTextAreaElement).value)
+ }
+ return (
+
+
+
+
+
+
{selectedConversation?.title || ""}
+
+
+ {selectedConversation && selectedConversation?.Messages.length > 0 && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ {!selectedConversation && (
+ <>
+
Start by asking a question
+
You can also upload your Document by clicking on Document icon on top right corner
+ >
+ )}
+
+ {selectedConversation?.Messages.map((message) => {
+ return (
)
+ })
+ }
+
+ {onGoingResult && (
+
+ )}
+
+
+
+
+
+
+
+
+ )
+}
+export default Conversation;
diff --git a/ChatQnA/docker/ui/react/src/components/Conversation/ConversationSideBar.tsx b/ChatQnA/docker/ui/react/src/components/Conversation/ConversationSideBar.tsx
new file mode 100644
index 000000000..12591ad76
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/components/Conversation/ConversationSideBar.tsx
@@ -0,0 +1,45 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import { ScrollAreaAutosize, Title } from "@mantine/core"
+
+import contextStyles from "../../styles/components/context.module.scss"
+import { useAppDispatch, useAppSelector } from "../../redux/store"
+import { conversationSelector, setSelectedConversationId } from "../../redux/Conversation/ConversationSlice"
+// import { userSelector } from "../../redux/User/userSlice"
+
+export interface ConversationContextProps {
+ title: string
+}
+
+export function ConversationSideBar({ title }: ConversationContextProps) {
+ const { conversations, selectedConversationId } = useAppSelector(conversationSelector)
+ // const user = useAppSelector(userSelector)
+ const dispatch = useAppDispatch()
+
+ const conversationList = conversations?.map((curr) => (
+ {
+ event.preventDefault()
+ dispatch(setSelectedConversationId(curr.conversationId))
+ // dispatch(getConversationById({ user, conversationId: curr.conversationId }))
+ }}
+ key={curr.conversationId}
+ >
+
{curr.title}
+
+ ))
+
+ return (
+
+
+ {title}
+
+
+ {conversationList}
+
+
+ )
+}
diff --git a/ChatQnA/docker/ui/react/src/components/Conversation/DataSource.tsx b/ChatQnA/docker/ui/react/src/components/Conversation/DataSource.tsx
new file mode 100644
index 000000000..cb7b326c9
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/components/Conversation/DataSource.tsx
@@ -0,0 +1,71 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import { Button, Container, Drawer, FileInput, Text, TextInput } from '@mantine/core'
+import { SyntheticEvent, useState } from 'react'
+import { useAppDispatch } from '../../redux/store'
+import { submitDataSourceURL, uploadFile } from '../../redux/Conversation/ConversationSlice'
+
+type Props = {
+ opened: boolean
+ onClose: () => void
+}
+
+export default function DataSource({ opened, onClose }: Props) {
+ const title = "Data Source"
+ const [file, setFile] = useState();
+ const [isFile, setIsFile] = useState(true);
+ const [url, setURL] = useState("");
+ const dispatch = useAppDispatch()
+ const handleFileUpload = () => {
+ if (file)
+ dispatch(uploadFile({ file }))
+ }
+
+ const handleChange = (event: SyntheticEvent) => {
+ event.preventDefault()
+ setURL((event.target as HTMLTextAreaElement).value)
+ }
+
+ const handleSubmit = () => {
+ dispatch(submitDataSourceURL({ link_list: url.split(";") }))
+ }
+
+ return (
+
+
+ Please upload your local file or paste a remote file link, and Chat will respond based on the content of the uploaded file.
+
+
+
+
+
+ setIsFile(true)}>Upload FIle
+ setIsFile(false)}>Use Link
+
+
+
+
+
+ {isFile ? (
+ <>
+
+ Upload
+ >
+ ) : (
+ <>
+
+ Upload
+ >
+ )}
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/ChatQnA/docker/ui/react/src/components/Conversation/conversation.module.scss b/ChatQnA/docker/ui/react/src/components/Conversation/conversation.module.scss
new file mode 100644
index 000000000..c637d5e11
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/components/Conversation/conversation.module.scss
@@ -0,0 +1,65 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+@import "../../styles/styles";
+
+.spacer {
+ flex: 1 1 auto;
+}
+
+.conversationWrapper {
+ @include flex(row, nowrap, flex-start, flex-start);
+ flex: 1 1 auto;
+ height: 100%;
+ & > * {
+ height: 100%;
+ }
+ .conversationContent {
+ flex: 1 1 auto;
+ position: relative;
+ .conversationContentMessages {
+ @include absolutes;
+ // @include flex(column, nowrap, flex-start, flex-start);
+
+ display: grid;
+ grid-template-areas:
+ "header"
+ "messages"
+ "inputs";
+
+ grid-template-columns: auto;
+ grid-template-rows: 60px auto 100px;
+
+ .conversationTitle {
+ grid-area: header;
+ @include flex(row, nowrap, center, flex-start);
+ height: 60px;
+ padding: 8px 24px;
+ border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
+ }
+
+ .historyContainer {
+ grid-area: messages;
+ overflow: auto;
+ width: 100%;
+ padding: 16px 32px;
+ & > * {
+ width: 100%;
+ }
+ }
+
+ .conversationActions {
+ // padding: --var()
+ grid-area: inputs;
+ padding: 18px;
+ border-top: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
+ }
+ }
+
+ .conversationSplash {
+ @include absolutes;
+ @include flex(column, nowrap, center, center);
+ font-size: 32px;
+ }
+ }
+}
diff --git a/ChatQnA/docker/ui/react/src/components/Message/conversationMessage.module.scss b/ChatQnA/docker/ui/react/src/components/Message/conversationMessage.module.scss
new file mode 100644
index 000000000..b00649553
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/components/Message/conversationMessage.module.scss
@@ -0,0 +1,15 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+@import "../../styles/styles";
+
+.conversationMessage {
+ @include flex(column, nowrap, flex-start, flex-start);
+ margin-top: 16px;
+ padding: 0 32px;
+ width: 100%;
+
+ & > * {
+ width: 100%;
+ }
+}
diff --git a/ChatQnA/docker/ui/react/src/components/Message/conversationMessage.tsx b/ChatQnA/docker/ui/react/src/components/Message/conversationMessage.tsx
new file mode 100644
index 000000000..10e785853
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/components/Message/conversationMessage.tsx
@@ -0,0 +1,55 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import { IconAi, IconUser } from "@tabler/icons-react"
+import style from "./conversationMessage.module.scss"
+import { Group, Text } from "@mantine/core"
+import { DateTime } from "luxon"
+
+export interface ConversationMessageProps {
+ message: string
+ human: boolean
+ date: number
+}
+
+export function ConversationMessage({ human, message, date }: ConversationMessageProps) {
+ const dateFormat = () => {
+ // console.log(date)
+ // console.log(new Date(date))
+ return DateTime.fromJSDate(new Date(date)).toLocaleString(DateTime.DATETIME_MED)
+ }
+
+ return (
+
+
+ {/* */}
+
+ {human && }
+ {!human && }
+
+
+
+ {human && "You"} {!human && "Assistant"}
+
+
+ {dateFormat()}
+
+
+
+
+ {message}
+
+
+ {/*
+ {human && }
+ {!human && }
+
+
+
{message}
*/}
+
+ )
+}
diff --git a/ChatQnA/docker/ui/react/src/components/UserInfoModal/UserInfoModal.tsx b/ChatQnA/docker/ui/react/src/components/UserInfoModal/UserInfoModal.tsx
new file mode 100644
index 000000000..4d54180a4
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/components/UserInfoModal/UserInfoModal.tsx
@@ -0,0 +1,48 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import { SyntheticEvent, useEffect, useState } from 'react'
+import { useDisclosure } from '@mantine/hooks';
+import { TextInput, Button, Modal } from '@mantine/core';
+import { useDispatch, useSelector } from 'react-redux';
+import { userSelector, setUser } from '../../redux/User/userSlice';
+
+
+const UserInfoModal = () => {
+ const [opened, { open, close }] = useDisclosure(false);
+ const { name } = useSelector(userSelector);
+ const [username, setUsername] = useState(name || "");
+ const dispatch = useDispatch();
+ const handleSubmit = (event: SyntheticEvent) => {
+ event.preventDefault()
+ if(username){
+ close();
+ dispatch(setUser(username));
+ setUsername("")
+ }
+
+ }
+ useEffect(() => {
+ if (!name) {
+ open();
+ }
+ }, [name])
+ return (
+ <>
+ handleSubmit} title="Tell us who you are ?" centered>
+ <>
+
+
+ >
+
+ >
+
+ )
+}
+
+export default UserInfoModal
\ No newline at end of file
diff --git a/ChatQnA/docker/ui/react/src/components/sidebar/sidebar.module.scss b/ChatQnA/docker/ui/react/src/components/sidebar/sidebar.module.scss
new file mode 100644
index 000000000..b58a253c2
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/components/sidebar/sidebar.module.scss
@@ -0,0 +1,84 @@
+/**
+ Copyright (c) 2024 Intel Corporation
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ **/
+
+@import "../../styles/styles";
+
+.navbar {
+ width: 100%;
+ @include flex(column, nowrap, center, flex-start);
+ padding: var(--mantine-spacing-md);
+ background-color: var(--mantine-color-blue-filled);
+ // background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6));
+ // border-right: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
+}
+
+.navbarMain {
+ flex: 1;
+}
+
+.navbarLogo {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ padding-top: var(--mantine-spacing-md);
+ margin-bottom: var(--mantine-spacing-xl);
+}
+
+.link {
+ width: 44px;
+ height: 44px;
+ border-radius: var(--mantine-radius-md);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--mantine-color-white);
+
+ &:hover {
+ background-color: var(--mantine-color-blue-7);
+ }
+
+ &[data-active] {
+ &,
+ &:hover {
+ box-shadow: var(--mantine-shadow-sm);
+ background-color: var(--mantine-color-white);
+ color: var(--mantine-color-blue-6);
+ }
+ }
+}
+
+.aside {
+ flex: 0 0 60px;
+ background-color: var(--mantine-color-body);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ border-right: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-7));
+}
+
+.logo {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ height: 60px;
+ padding-top: var(--mantine-spacing-s);
+ border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-7));
+ margin-bottom: var(--mantine-spacing-xl);
+}
+.logoImg {
+ width: 30px;
+}
diff --git a/ChatQnA/docker/ui/react/src/components/sidebar/sidebar.tsx b/ChatQnA/docker/ui/react/src/components/sidebar/sidebar.tsx
new file mode 100644
index 000000000..e5e9349e4
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/components/sidebar/sidebar.tsx
@@ -0,0 +1,70 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import { useState } from "react"
+import { Tooltip, UnstyledButton, Stack, rem } from "@mantine/core"
+import { IconHome2, IconLogout } from "@tabler/icons-react"
+import classes from "./sidebar.module.scss"
+import OpeaLogo from "../../assets/opea-icon-black.svg"
+import { useAppDispatch } from "../../redux/store"
+import { removeUser } from "../../redux/User/userSlice"
+import { logout } from "../../redux/Conversation/ConversationSlice"
+
+interface NavbarLinkProps {
+ icon: typeof IconHome2
+ label: string
+ active?: boolean
+ onClick?(): void
+}
+
+function NavbarLink({ icon: Icon, label, active, onClick }: NavbarLinkProps) {
+ return (
+
+
+
+
+
+ )
+}
+
+export interface SidebarNavItem {
+ icon: typeof IconHome2
+ label: string
+}
+
+export type SidebarNavList = SidebarNavItem[]
+
+export interface SideNavbarProps {
+ navList: SidebarNavList
+}
+
+export function SideNavbar({ navList }: SideNavbarProps) {
+ const dispatch =useAppDispatch()
+ const [active, setActive] = useState(0)
+
+ const handleLogout = () => {
+ dispatch(logout())
+ dispatch(removeUser())
+ }
+
+ const links = navList.map((link, index) => (
+ setActive(index)} />
+ ))
+
+ return (
+
+
+
+
+
+
+
+ {links}
+
+
+
+
+
+
+ )
+}
diff --git a/ChatQnA/docker/ui/react/src/config.ts b/ChatQnA/docker/ui/react/src/config.ts
new file mode 100644
index 000000000..f004165a2
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/config.ts
@@ -0,0 +1,5 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+export const DATA_PREP_URL = import.meta.env.VITE_DATA_PREP_SERVICE_URL;
+export const CHAT_QNA_URL = import.meta.env.VITE_BACKEND_SERVICE_ENDPOINT;
diff --git a/ChatQnA/docker/ui/react/src/index.scss b/ChatQnA/docker/ui/react/src/index.scss
new file mode 100644
index 000000000..53e71621e
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/index.scss
@@ -0,0 +1,20 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+@import "@mantine/core/styles.css";
+
+:root {
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+}
+
+html,
+body {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+}
diff --git a/ChatQnA/docker/ui/react/src/main.tsx b/ChatQnA/docker/ui/react/src/main.tsx
new file mode 100644
index 000000000..3d9c91578
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/main.tsx
@@ -0,0 +1,17 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import React from "react"
+import ReactDOM from "react-dom/client"
+import App from "./App.tsx"
+import "./index.scss"
+import { Provider } from 'react-redux'
+import { store } from "./redux/store.ts"
+
+ReactDOM.createRoot(document.getElementById("root")!).render(
+
+
+
+
+
+)
diff --git a/ChatQnA/docker/ui/react/src/redux/Conversation/Conversation.ts b/ChatQnA/docker/ui/react/src/redux/Conversation/Conversation.ts
new file mode 100644
index 000000000..47ea988a6
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/redux/Conversation/Conversation.ts
@@ -0,0 +1,32 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+export type ConversationRequest = {
+ conversationId: string;
+ userPrompt: Message;
+ messages: Partial[];
+ model: string;
+};
+export enum MessageRole {
+ Assistant = "assistant",
+ User = "user",
+ System = "system",
+}
+
+export interface Message {
+ role: MessageRole;
+ content: string;
+ time: number;
+}
+
+export interface Conversation {
+ conversationId: string;
+ title?: string;
+ Messages: Message[];
+}
+
+export interface ConversationReducer {
+ selectedConversationId: string;
+ conversations: Conversation[];
+ onGoingResult: string;
+}
diff --git a/ChatQnA/docker/ui/react/src/redux/Conversation/ConversationSlice.ts b/ChatQnA/docker/ui/react/src/redux/Conversation/ConversationSlice.ts
new file mode 100644
index 000000000..ad8fa33be
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/redux/Conversation/ConversationSlice.ts
@@ -0,0 +1,197 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import { PayloadAction, createSlice } from "@reduxjs/toolkit";
+import { RootState, store } from "../store";
+import { fetchEventSource } from "@microsoft/fetch-event-source";
+import { Message, MessageRole, ConversationReducer, ConversationRequest } from "./Conversation";
+import { getCurrentTimeStamp, uuidv4 } from "../../common/util";
+import { createAsyncThunkWrapper } from "../thunkUtil";
+import client from "../../common/client";
+import { notifications } from "@mantine/notifications";
+import { CHAT_QNA_URL, DATA_PREP_URL } from "../../config";
+
+const initialState: ConversationReducer = {
+ conversations: [],
+ selectedConversationId: "",
+ onGoingResult: "",
+};
+
+export const ConversationSlice = createSlice({
+ name: "Conversation",
+ initialState,
+ reducers: {
+ logout: (state) => {
+ state.conversations = [];
+ state.selectedConversationId = "";
+ state.onGoingResult = "";
+ },
+ setOnGoingResult: (state, action: PayloadAction) => {
+ state.onGoingResult = action.payload;
+ },
+ addMessageToMessages: (state, action: PayloadAction) => {
+ const selectedConversation = state.conversations.find((x) => x.conversationId === state.selectedConversationId);
+ selectedConversation?.Messages?.push(action.payload);
+ },
+ newConversation: (state) => {
+ (state.selectedConversationId = ""), (state.onGoingResult = "");
+ },
+ createNewConversation: (state, action: PayloadAction<{ title: string; id: string; message: Message }>) => {
+ state.conversations.push({
+ title: action.payload.title,
+ conversationId: action.payload.id,
+ Messages: [action.payload.message],
+ });
+ },
+ setSelectedConversationId: (state, action: PayloadAction) => {
+ state.selectedConversationId = action.payload;
+ },
+ },
+ extraReducers(builder) {
+ builder.addCase(uploadFile.fulfilled, () => {
+ notifications.update({
+ id: "upload-file",
+ message: "File Uploaded Successfully",
+ loading: false,
+ autoClose: 3000,
+ });
+ }),
+ builder.addCase(uploadFile.rejected, () => {
+ notifications.update({
+ color: "red",
+ id: "upload-file",
+ message: "Failed to Upload file",
+ loading: false,
+ });
+ });
+ builder.addCase(submitDataSourceURL.fulfilled, () => {
+ notifications.show({
+ message: "Submitted Successfully",
+ });
+ });
+ builder.addCase(submitDataSourceURL.rejected, () => {
+ notifications.show({
+ color: "red",
+ message: "Submit Failed",
+ });
+ });
+ },
+});
+
+export const submitDataSourceURL = createAsyncThunkWrapper(
+ "conversation/submitDataSourceURL",
+ async ({ link_list }: { link_list: string[] }, {}) => {
+ const body = new FormData();
+ body.append("link_list", JSON.stringify(link_list));
+ const response = await client.post(DATA_PREP_URL, body);
+ return response.data;
+ },
+);
+export const uploadFile = createAsyncThunkWrapper("conversation/uploadFile", async ({ file }: { file: File }, {}) => {
+ const body = new FormData();
+ body.append("files", file);
+
+ notifications.show({
+ id: "upload-file",
+ message: "uploading File",
+ loading: true,
+ });
+ const response = await client.post(DATA_PREP_URL, body);
+ return response.data;
+});
+export const {
+ logout,
+ setOnGoingResult,
+ newConversation,
+ addMessageToMessages,
+ setSelectedConversationId,
+ createNewConversation,
+} = ConversationSlice.actions;
+export const conversationSelector = (state: RootState) => state.conversationReducer;
+export default ConversationSlice.reducer;
+
+export const doConversation = (conversationRequest: ConversationRequest) => {
+ const { conversationId, userPrompt, messages, model } = conversationRequest;
+ if (!conversationId) {
+ //newConversation
+ const id = uuidv4();
+ store.dispatch(
+ createNewConversation({
+ title: userPrompt.content,
+ id,
+ message: userPrompt,
+ }),
+ );
+ store.dispatch(setSelectedConversationId(id));
+ } else {
+ store.dispatch(addMessageToMessages(userPrompt));
+ }
+ const userPromptWithoutTime = {
+ role: userPrompt.role,
+ content: userPrompt.content,
+ };
+ const body = {
+ messages: [...messages, userPromptWithoutTime],
+ model,
+ };
+
+ // let conversation: Conversation;
+ let result = "";
+ try {
+ fetchEventSource(CHAT_QNA_URL, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(body),
+ openWhenHidden: true,
+ async onopen(response) {
+ if (response.ok) {
+ return;
+ } else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
+ const e = await response.json();
+ console.log(e);
+ throw Error(e.error.message);
+ } else {
+ console.log("error", response);
+ }
+ },
+ onmessage(msg) {
+ if (msg?.data != "[DONE]") {
+ try {
+ const match = msg.data.match(/b'([^']*)'/);
+ if (match && match[1] != "") {
+ const extractedText = match[1];
+ result += extractedText;
+ store.dispatch(setOnGoingResult(result));
+ }
+ } catch (e) {
+ console.log("something wrong in msg", e);
+ throw e;
+ }
+ }
+ },
+ onerror(err) {
+ console.log("error", err);
+ store.dispatch(setOnGoingResult(""));
+ //notify here
+ throw err;
+ //handle error
+ },
+ onclose() {
+ //handle close
+ store.dispatch(setOnGoingResult(""));
+
+ store.dispatch(
+ addMessageToMessages({
+ role: MessageRole.Assistant,
+ content: result,
+ time: getCurrentTimeStamp(),
+ }),
+ );
+ },
+ });
+ } catch (err) {
+ console.log(err);
+ }
+};
diff --git a/ChatQnA/docker/ui/react/src/redux/User/user.d.ts b/ChatQnA/docker/ui/react/src/redux/User/user.d.ts
new file mode 100644
index 000000000..69c4db4ab
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/redux/User/user.d.ts
@@ -0,0 +1,6 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+export interface User {
+ name: string | null;
+}
diff --git a/ChatQnA/docker/ui/react/src/redux/User/userSlice.ts b/ChatQnA/docker/ui/react/src/redux/User/userSlice.ts
new file mode 100644
index 000000000..48d22fe2e
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/redux/User/userSlice.ts
@@ -0,0 +1,26 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+import { RootState } from "../store";
+import { User } from "./user";
+
+const initialState: User = {
+ name: localStorage.getItem("user"),
+};
+
+export const userSlice = createSlice({
+ name: "user",
+ initialState,
+ reducers: {
+ setUser: (state, action: PayloadAction) => {
+ state.name = action.payload;
+ },
+ removeUser: (state) => {
+ state.name = null;
+ },
+ },
+});
+export const { setUser, removeUser } = userSlice.actions;
+export const userSelector = (state: RootState) => state.userReducer;
+export default userSlice.reducer;
diff --git a/ChatQnA/docker/ui/react/src/redux/store.ts b/ChatQnA/docker/ui/react/src/redux/store.ts
new file mode 100644
index 000000000..260390926
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/redux/store.ts
@@ -0,0 +1,49 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import { combineReducers, configureStore } from "@reduxjs/toolkit";
+import userReducer from "./User/userSlice";
+import conversationReducer from "./Conversation/ConversationSlice";
+import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
+
+export const store = configureStore({
+ reducer: combineReducers({
+ userReducer,
+ conversationReducer,
+ }),
+ devTools: import.meta.env.PROD || true,
+ preloadedState: loadFromLocalStorage(),
+ middleware: (getDefaultMiddleware) =>
+ getDefaultMiddleware({
+ serializableCheck: false,
+ }),
+});
+
+function saveToLocalStorage(state: ReturnType) {
+ try {
+ const serialState = JSON.stringify(state);
+ localStorage.setItem("reduxStore", serialState);
+ } catch (e) {
+ console.warn(e);
+ }
+}
+
+function loadFromLocalStorage() {
+ try {
+ const serialisedState = localStorage.getItem("reduxStore");
+ if (serialisedState === null) return undefined;
+ return JSON.parse(serialisedState);
+ } catch (e) {
+ console.warn(e);
+ return undefined;
+ }
+}
+
+store.subscribe(() => saveToLocalStorage(store.getState()));
+console.log(store);
+export default store;
+export type AppDispatch = typeof store.dispatch;
+export type RootState = ReturnType;
+
+export const useAppDispatch: () => AppDispatch = useDispatch;
+export const useAppSelector: TypedUseSelectorHook = useSelector;
diff --git a/ChatQnA/docker/ui/react/src/redux/thunkUtil.ts b/ChatQnA/docker/ui/react/src/redux/thunkUtil.ts
new file mode 100644
index 000000000..5df362fd3
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/redux/thunkUtil.ts
@@ -0,0 +1,25 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import { createAsyncThunk, AsyncThunkPayloadCreator, AsyncThunk } from "@reduxjs/toolkit";
+
+interface ThunkAPIConfig {}
+
+export const createAsyncThunkWrapper = (
+ type: string,
+ thunk: AsyncThunkPayloadCreator, // <-- very unsure of this - have tried many things here
+): AsyncThunk => {
+ return createAsyncThunk(
+ type,
+ // @ts-ignore
+ async (arg, thunkAPI) => {
+ try {
+ // do some stuff here that happens on every action
+ return await thunk(arg, thunkAPI);
+ } catch (err) {
+ // do some stuff here that happens on every error
+ return thunkAPI.rejectWithValue(err);
+ }
+ },
+ );
+};
diff --git a/ChatQnA/docker/ui/react/src/styles/components/_context.scss b/ChatQnA/docker/ui/react/src/styles/components/_context.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/ChatQnA/docker/ui/react/src/styles/components/_sidebar.scss b/ChatQnA/docker/ui/react/src/styles/components/_sidebar.scss
new file mode 100644
index 000000000..23018ee1f
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/styles/components/_sidebar.scss
@@ -0,0 +1,8 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+@import "../layout/flex";
+
+@mixin sidebar {
+ @include flex(column, nowrap, flex-start, flex-start);
+}
diff --git a/ChatQnA/docker/ui/react/src/styles/components/content.scss b/ChatQnA/docker/ui/react/src/styles/components/content.scss
new file mode 100644
index 000000000..9a230f249
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/styles/components/content.scss
@@ -0,0 +1,5 @@
+@mixin textWrapEllipsis {
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+}
diff --git a/ChatQnA/docker/ui/react/src/styles/components/context.module.scss b/ChatQnA/docker/ui/react/src/styles/components/context.module.scss
new file mode 100644
index 000000000..17f37ba90
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/styles/components/context.module.scss
@@ -0,0 +1,67 @@
+@import "../layout/flex";
+@import "../components/content.scss";
+
+.contextWrapper {
+ background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
+ border-right: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
+ width: 180px;
+ overflow-y: hidden;
+ overflow-x: hidden;
+ // overflow-y: auto;
+
+ .contextTitle {
+ position: sticky;
+ top: 0;
+ font-family:
+ Greycliff CF,
+ var(--mantine-font-family);
+ margin-bottom: var(--mantine-spacing-xl);
+ background-color: var(--mantine-color-body);
+ padding: var(--mantine-spacing-md);
+ padding-top: 18px;
+ width: 100%;
+ height: 60px;
+ border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-7));
+ }
+
+ .contextList {
+ height: 90vh;
+ // display: flex();
+
+ .contextListItem {
+ display: block;
+ text-decoration: none;
+ border-top-right-radius: var(--mantine-radius-md);
+ border-bottom-right-radius: var(--mantine-radius-md);
+ color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
+ padding: 0 var(--mantine-spacing-md);
+ font-size: var(--mantine-font-size-sm);
+ margin-right: var(--mantine-spacing-md);
+ font-weight: 500;
+ height: 44px;
+ width: 100%;
+ line-height: 44px;
+ cursor: pointer;
+
+ .contextItemName {
+ flex: 1 1 auto;
+ width: 130px;
+ @include textWrapEllipsis;
+ }
+
+ &:hover {
+ background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
+ color: light-dark(var(--mantine-color-dark), var(--mantine-color-light));
+ }
+
+ &[data-active] {
+ &,
+ &:hover {
+ border-left-color: var(--mantine-color-blue-filled);
+ background-color: var(--mantine-color-blue-filled);
+ color: var(--mantine-color-white);
+ }
+ }
+ }
+ }
+}
diff --git a/ChatQnA/docker/ui/react/src/styles/layout/_basics.scss b/ChatQnA/docker/ui/react/src/styles/layout/_basics.scss
new file mode 100644
index 000000000..d11b1ef21
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/styles/layout/_basics.scss
@@ -0,0 +1,7 @@
+@mixin absolutes {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+}
diff --git a/ChatQnA/docker/ui/react/src/styles/layout/_flex.scss b/ChatQnA/docker/ui/react/src/styles/layout/_flex.scss
new file mode 100644
index 000000000..18d2ce8ec
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/styles/layout/_flex.scss
@@ -0,0 +1,6 @@
+@mixin flex($direction: row, $wrap: nowrap, $alignItems: center, $justifyContent: center) {
+ display: flex;
+ flex-flow: $direction $wrap;
+ align-items: $alignItems;
+ justify-content: $justifyContent;
+}
diff --git a/ChatQnA/docker/ui/react/src/styles/styles.scss b/ChatQnA/docker/ui/react/src/styles/styles.scss
new file mode 100644
index 000000000..8028d8ad6
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/styles/styles.scss
@@ -0,0 +1,5 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+@import "layout/flex";
+@import "layout/basics";
diff --git a/ChatQnA/docker/ui/react/src/vite-env.d.ts b/ChatQnA/docker/ui/react/src/vite-env.d.ts
new file mode 100644
index 000000000..4260915f7
--- /dev/null
+++ b/ChatQnA/docker/ui/react/src/vite-env.d.ts
@@ -0,0 +1,4 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+///
diff --git a/ChatQnA/docker/ui/react/tsconfig.json b/ChatQnA/docker/ui/react/tsconfig.json
new file mode 100644
index 000000000..f50b75c5f
--- /dev/null
+++ b/ChatQnA/docker/ui/react/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/ChatQnA/docker/ui/react/tsconfig.node.json b/ChatQnA/docker/ui/react/tsconfig.node.json
new file mode 100644
index 000000000..97ede7ee6
--- /dev/null
+++ b/ChatQnA/docker/ui/react/tsconfig.node.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "strict": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/ChatQnA/docker/ui/react/vite.config.ts b/ChatQnA/docker/ui/react/vite.config.ts
new file mode 100644
index 000000000..b7c7150c4
--- /dev/null
+++ b/ChatQnA/docker/ui/react/vite.config.ts
@@ -0,0 +1,27 @@
+// Copyright (C) 2024 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import { defineConfig } from "vitest/config";
+import react from "@vitejs/plugin-react";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ css: {
+ preprocessorOptions: {
+ scss: {
+ additionalData: `@import "./src/styles/styles.scss";`,
+ },
+ },
+ },
+ plugins: [react()],
+ server: {
+ port: 80,
+ },
+ test: {
+ globals: true,
+ environment: "jsdom",
+ },
+ define: {
+ "import.meta.env": process.env,
+ },
+});
diff --git a/ChatQnA/docker/xeon/README.md b/ChatQnA/docker/xeon/README.md
index 970590647..16c543911 100644
--- a/ChatQnA/docker/xeon/README.md
+++ b/ChatQnA/docker/xeon/README.md
@@ -118,6 +118,20 @@ docker build --no-cache -t opea/chatqna-ui:latest --build-arg https_proxy=$https
cd ../../../..
```
+### 8. Build Conversational React UI Docker Image (Optional)
+
+Build frontend Docker image that enables Conversational experience with ChatQnA megaservice via below command:
+
+**Export the value of the public IP address of your Xeon server to the `host_ip` environment variable**
+
+```bash
+cd GenAIExamples/ChatQnA/docker/ui/
+export BACKEND_SERVICE_ENDPOINT="http://${host_ip}:8888/v1/chatqna"
+export DATAPREP_SERVICE_ENDPOINT="http://${host_ip}:6007/v1/dataprep"
+docker build --no-cache -t opea/chatqna-conversation-ui:latest --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy --build-arg BACKEND_SERVICE_ENDPOINT=$BACKEND_SERVICE_ENDPOINT --build-arg DATAPREP_SERVICE_ENDPOINT=$DATAPREP_SERVICE_ENDPOINT -f ./docker/Dockerfile.react .
+cd ../../../..
+```
+
Then run the command `docker images`, you will have the following 7 Docker Images:
1. `opea/dataprep-redis:latest`
@@ -326,8 +340,24 @@ To access the frontend, open the following URL in your browser: http://{host_ip}
- "80:5173"
```
+## 🚀 Launch the Conversational UI (react)
+
+To access the Conversational UI frontend, open the following URL in your browser: http://{host_ip}:5174. By default, the UI runs on port 80 internally. If you prefer to use a different host port to access the frontend, you can modify the port mapping in the `docker_compose.yaml` file as shown below:
+
+```yaml
+ chaqna-xeon-conversation-ui-server:
+ image: opea/chatqna-conversation-ui:latest
+ ...
+ ports:
+ - "80:80"
+```
+
![project-screenshot](../../assets/img/chat_ui_init.png)
Here is an example of running ChatQnA:
![project-screenshot](../../assets/img/chat_ui_response.png)
+
+Here is an example of running ChatQnA with Conversational UI (React):
+
+![project-screenshot](../../assets/img/conversation_ui_response.png)
diff --git a/ChatQnA/docker/xeon/docker_compose.yaml b/ChatQnA/docker/xeon/docker_compose.yaml
index 225735fbc..e3be9ed52 100644
--- a/ChatQnA/docker/xeon/docker_compose.yaml
+++ b/ChatQnA/docker/xeon/docker_compose.yaml
@@ -185,6 +185,19 @@ services:
- UPLOAD_FILE_BASE_URL=${DATAPREP_SERVICE_ENDPOINT}
ipc: host
restart: always
+ chaqna-xeon-conversation-ui-server:
+ image: opea/chatqna-conversation-ui:latest
+ container_name: chatqna-xeon-conversation-ui-server
+ environment:
+ - no_proxy=${no_proxy}
+ - https_proxy=${https_proxy}
+ - http_proxy=${http_proxy}
+ ports:
+ - 5174:80
+ depends_on:
+ - chaqna-xeon-backend-server
+ ipc: host
+ restart: always
networks:
default:
diff --git a/ChatQnA/tests/test_chatqna_on_xeon.sh b/ChatQnA/tests/test_chatqna_on_xeon.sh
index b9eba8449..29946fabb 100644
--- a/ChatQnA/tests/test_chatqna_on_xeon.sh
+++ b/ChatQnA/tests/test_chatqna_on_xeon.sh
@@ -56,6 +56,7 @@ function start_services() {
echo "using image repository $IMAGE_REPO and image tag $IMAGE_TAG"
sed -i "s#image: opea/chatqna:latest#image: opea/chatqna:${IMAGE_TAG}#g" docker_compose.yaml
sed -i "s#image: opea/chatqna-ui:latest#image: opea/chatqna-ui:${IMAGE_TAG}#g" docker_compose.yaml
+ sed -i "s#image: opea/chatqna-conversation-ui:latest#image: opea/chatqna-conversation-ui:${IMAGE_TAG}#g" docker_compose.yaml
sed -i "s#image: opea/*#image: ${IMAGE_REPO}opea/#g" docker_compose.yaml
fi