diff --git a/.gitignore b/.gitignore
index ef79b3a..3546d91 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
test-results/
**.log
**/report.html
-docker-compose
\ No newline at end of file
+docker-compose
+**/node_modules/
diff --git a/app-backend/megaservice.py b/app-backend/megaservice.py
index db1d215..6ea4e61 100644
--- a/app-backend/megaservice.py
+++ b/app-backend/megaservice.py
@@ -7,7 +7,8 @@
import re
# library import
-from fastapi import Request
+from typing import List
+from fastapi import Request, UploadFile, File
from fastapi.responses import StreamingResponse
from dotenv import load_dotenv
@@ -65,6 +66,7 @@ def __init__(self, host="0.0.0.0", port=8000):
self.host = host
self.port = port
self.endpoint = "/v1/app-backend"
+ self.is_docsum = False
with open('config/workflow-info.json', 'r') as f:
self.workflow_info = json.load(f)
@@ -117,6 +119,8 @@ def add_remote_service(self):
if node['inMegaservice']:
print('adding Node', node_id)
microservice_name = node['name'].split('@')[1]
+ if "docsum" in microservice_name:
+ self.is_docsum = True
service_node_ip = node_id.split('@')[1].replace('_','-') if USE_NODE_ID_AS_IP else HOST_IP
microservice = templates[microservice_name].get_service(host_ip=service_node_ip, node_id_as_ip=USE_NODE_ID_AS_IP, port=os.getenv(f"{node_id.split('@')[1]}_port", None))
microservice.name = node_id
@@ -356,7 +360,7 @@ async def handle_request(self, request: Request, megaservice):
for node, response in result_dict.items():
if isinstance(response, StreamingResponse):
return response
- last_node = runtime_graph.all_leaves()[-1]
+ last_node = runtime_graph.all_leaves()[-1] # YX to fix it to the source node of chat completion
print('result_dict:', result_dict)
print('last_node:',last_node)
last_node_info = self.workflow_info['nodes'][last_node]
@@ -377,9 +381,77 @@ async def handle_request(self, request: Request, megaservice):
# handle the non-llm response
return result_dict[last_node]
+ async def handle_request_docsum(self, request: Request, files: List[UploadFile] = File(default=None)):
+ """Accept pure text, or files .txt/.pdf.docx, audio/video base64 string."""
+ if "application/json" in request.headers.get("content-type"):
+ data = await request.json()
+ stream_opt = data.get("stream", True)
+ summary_type = data.get("summary_type", "auto")
+ chunk_size = data.get("chunk_size", -1)
+ chunk_overlap = data.get("chunk_overlap", -1)
+ chat_request = ChatCompletionRequest.model_validate(data)
+ prompt = handle_message(chat_request.messages)
+
+ initial_inputs_data = {data["type"]: prompt}
+
+ elif "multipart/form-data" in request.headers.get("content-type"):
+ data = await request.form()
+ stream_opt = data.get("stream", True)
+ summary_type = data.get("summary_type", "auto")
+ chunk_size = data.get("chunk_size", -1)
+ chunk_overlap = data.get("chunk_overlap", -1)
+ chat_request = ChatCompletionRequest.model_validate(data)
+
+ data_type = data.get("type")
+
+ file_summaries = []
+ if files:
+ for file in files:
+ # Fix concurrency issue with the same file name
+ # https://github.com/opea-project/GenAIExamples/issues/1279
+ uid = str(uuid.uuid4())
+ file_path = f"/tmp/{uid}"
+
+ import aiofiles
+
+ async with aiofiles.open(file_path, "wb") as f:
+ await f.write(await file.read())
+
+ if data_type == "text":
+ docs = read_text_from_file(file, file_path)
+ elif data_type in ["audio", "video"]:
+ docs = encode_file_to_base64(file_path)
+ else:
+ raise ValueError(f"Data type not recognized: {data_type}")
+
+ os.remove(file_path)
+
+ if isinstance(docs, list):
+ file_summaries.extend(docs)
+ else:
+ file_summaries.append(docs)
+
+ if file_summaries:
+ prompt = handle_message(chat_request.messages) + "\n".join(file_summaries)
+ else:
+ prompt = handle_message(chat_request.messages)
+
+ data_type = data.get("type")
+ if data_type is not None:
+ initial_inputs_data = {}
+ initial_inputs_data[data_type] = prompt
+ else:
+ initial_inputs_data = {"messages": prompt}
+
+ else:
+ raise ValueError(f"Unknown request type: {request.headers.get('content-type')}")
+
def create_handle_request(self, megaservice):
async def handle_request_wrapper(request: Request):
- return await self.handle_request(request, megaservice)
+ if self.is_docsum:
+ return await self.handle_request_docsum(request)
+ else:
+ return await self.handle_request(request, megaservice)
return handle_request_wrapper
def start(self):
diff --git a/app-frontend/Dockerfile b/app-frontend/Dockerfile
index 10255e9..4c4d727 100644
--- a/app-frontend/Dockerfile
+++ b/app-frontend/Dockerfile
@@ -2,21 +2,20 @@
# SPDX-License-Identifier: Apache-2.0
# Use node 20.11.1 as the base image
-FROM node:latest AS vite-app
-
-COPY react /usr/app/react
+FROM node:20.11.1 as vite-app
+
+COPY ./react /usr/app/react
WORKDIR /usr/app/react
-RUN npm install --legacy-peer-deps && npm run build
-FROM nginx:1.27.4-alpine-slim
+RUN ["npm", "install"]
+RUN ["npm", "run", "build"]
+
-# Install uuidgen in the nginx:alpine image
-RUN apk add --no-cache util-linux \
- && apk upgrade --no-cache
+FROM nginx:alpine
COPY --from=vite-app /usr/app/react/dist /usr/share/nginx/html
COPY ./react/env.sh /docker-entrypoint.d/env.sh
COPY ./react/nginx.conf /etc/nginx/conf.d/default.conf
-RUN chmod +x /docker-entrypoint.d/env.sh
\ No newline at end of file
+RUN chmod +x /docker-entrypoint.d/env.sh
diff --git a/app-frontend/compose.yaml b/app-frontend/compose.yaml
new file mode 100644
index 0000000..31b3080
--- /dev/null
+++ b/app-frontend/compose.yaml
@@ -0,0 +1,52 @@
+# Copyright (C) 2024 Intel Corporation
+# SPDX-License-Identifier: Apache-2.0
+
+services:
+ app-frontend:
+ image: app-frontend:ch
+ container_name: app-frontend
+ depends_on:
+ - chathistory-mongo
+ ports:
+ - 5175:80
+ environment:
+ - no_proxy=${no_proxy}
+ - https_proxy=${https_proxy}
+ - http_proxy=${http_proxy}
+ - APP_BACKEND_SERVICE_URL=http://localhost:8888/v1/app-backend
+ - APP_DATAPREP_SERVICE_URL=http://localhost:6007/v1/dataprep
+ - APP_CHAT_HISTORY_SERVICE_URL=http://localhost:6012/v1/chathistory
+ - APP_UI_SELECTION=chat,summary,code
+ ipc: host
+ restart: always
+
+ mongo:
+ image: mongo:7.0.11
+ container_name: mongodb
+ ports:
+ - 27017:27017
+ environment:
+ http_proxy: ${http_proxy}
+ https_proxy: ${https_proxy}
+ no_proxy: ${no_proxy}
+ command: mongod --quiet --logpath /dev/null
+
+ chathistory-mongo:
+ image: ${REGISTRY:-opea}/chathistory-mongo:${TAG:-latest}
+ container_name: chathistory-mongo-server
+ ports:
+ - "6012:6012"
+ ipc: host
+ environment:
+ http_proxy: ${http_proxy}
+ no_proxy: ${no_proxy}
+ https_proxy: ${https_proxy}
+ MONGO_HOST: ${MONGO_HOST:-mongo}
+ MONGO_PORT: ${MONGO_PORT:-27017}
+ COLLECTION_NAME: ${COLLECTION_NAME:-Conversations}
+ LOGFLAG: ${LOGFLAG}
+ restart: unless-stopped
+
+networks:
+ default:
+ driver: bridge
diff --git a/app-frontend/react/.env b/app-frontend/react/.env
index ad8c238..71b04ce 100644
--- a/app-frontend/react/.env
+++ b/app-frontend/react/.env
@@ -1,2 +1,8 @@
-VITE_CHAT_SERVICE_URL=http://backend_address:8899/v1/chatqna
-VITE_DATA_PREP_SERVICE_URL=http://backend_address:6007/v1/dataprep
\ No newline at end of file
+VITE_BACKEND_SERVICE_URL=
+VITE_DATAPREP_SERVICE_URL=
+VITE_CHAT_HISTORY_SERVICE_URL=
+VITE_UI_SELECTION=
+
+VITE_PROMPT_SERVICE_GET_ENDPOINT=
+VITE_PROMPT_SERVICE_CREATE_ENDPOINT=
+VITE_PROMPT_SERVICE_DELETE_ENDPOINT=
\ No newline at end of file
diff --git a/app-frontend/react/.env.production b/app-frontend/react/.env.production
index 16b02d1..26f274d 100644
--- a/app-frontend/react/.env.production
+++ b/app-frontend/react/.env.production
@@ -1 +1,8 @@
-VITE_APP_UUID=APP_UUID
\ No newline at end of file
+VITE_BACKEND_SERVICE_URL=APP_BACKEND_SERVICE_URL
+VITE_DATAPREP_SERVICE_URL=APP_DATAPREP_SERVICE_URL
+VITE_CHAT_HISTORY_SERVICE_URL=APP_CHAT_HISTORY_SERVICE_URL
+VITE_UI_SELECTION=APP_UI_SELECTION
+
+VITE_PROMPT_SERVICE_GET_ENDPOINT=APP_PROMPT_SERVICE_GET_ENDPOINT
+VITE_PROMPT_SERVICE_CREATE_ENDPOINT=APP_PROMPT_SERVICE_CREATE_ENDPOINT
+VITE_PROMPT_SERVICE_DELETE_ENDPOINT=APP_PROMPT_SERVICE_DELETE_ENDPOINT
diff --git a/app-frontend/react/.gitignore b/app-frontend/react/.gitignore
index 418b703..a547bf3 100644
--- a/app-frontend/react/.gitignore
+++ b/app-frontend/react/.gitignore
@@ -7,8 +7,6 @@ yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
-# dependencies
-package-lock.json
node_modules
dist
dist-ssr
diff --git a/app-frontend/react/env.sh b/app-frontend/react/env.sh
index c87c502..ce1372e 100644
--- a/app-frontend/react/env.sh
+++ b/app-frontend/react/env.sh
@@ -2,12 +2,6 @@
# Copyright (C) 2024 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
-# Generate a random UUID for the application
-export APP_UUID=$(uuidgen)
-
-# Print the generated UUID for verification
-echo "Generated UUID: $APP_UUID"
-
for i in $(env | grep APP_) #// Make sure to use the prefix MY_APP_ if you have any other prefix in env.production file variable name replace it with MY_APP_
do
key=$(echo $i | cut -d '=' -f 1)
@@ -16,6 +10,6 @@ do
# sed All files
# find /usr/share/nginx/html -type f -exec sed -i "s|${key}|${value}|g" '{}' +
- # sed JS, CSS, and HTML files
- find /usr/share/nginx/html -type f \( -name '*.js' -o -name '*.css' -o -name '*.html' \) -exec sed -i "s|${key}|${value}|g" '{}' +
+ # sed JS and CSS only
+ find /usr/share/nginx/html -type f \( -name '*.js' -o -name '*.css' \) -exec sed -i "s|${key}|${value}|g" '{}' +
done
diff --git a/app-frontend/react/index.html b/app-frontend/react/index.html
index d7e8864..0548818 100644
--- a/app-frontend/react/index.html
+++ b/app-frontend/react/index.html
@@ -1,18 +1,29 @@
-
-
-
- Conversations UI
+
+
+
+
+
+
+
+
+
+
+ OPEA Studio APP
+ You need to enable JavaScript to run this app.
-
+
diff --git a/app-frontend/react/nginx.conf b/app-frontend/react/nginx.conf
index 77fd5da..01aef12 100644
--- a/app-frontend/react/nginx.conf
+++ b/app-frontend/react/nginx.conf
@@ -12,10 +12,9 @@ server {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html =404;
- add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
location ~* \.(gif|jpe?g|png|webp|ico|svg|css|js|mp4|woff2)$ {
expires 1d;
}
}
-}
\ No newline at end of file
+}
diff --git a/app-frontend/react/package.json b/app-frontend/react/package.json
index 4dbfab3..1180caf 100644
--- a/app-frontend/react/package.json
+++ b/app-frontend/react/package.json
@@ -1,49 +1,85 @@
{
- "name": "ui",
+ "name": "ProductivitySuite",
+ "version": "0.0.1",
+ "description": "ProductivitySuite UI - OPEA",
+ "homepage": ".",
"private": true,
- "version": "0.0.0",
"type": "module",
+ "engines": {
+ "node": "20.x"
+ },
"scripts": {
- "dev": "vite",
- "build": "tsc && vite build",
- "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
- "preview": "vite preview",
- "test": "vitest"
+ "dev": "vite --port 5173",
+ "build": "vite build",
+ "preview": "vite preview --port 5173",
+ "prettier:write": "prettier --write .",
+ "test": "vitest run"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
},
"dependencies": {
- "@mantine/charts": "7.17.2",
- "@mantine/core": "^7.17.2",
- "@mantine/hooks": "^7.17.2",
- "@mantine/notifications": "^7.17.2",
"@microsoft/fetch-event-source": "^2.0.1",
- "@reduxjs/toolkit": "^2.2.5",
- "@tabler/icons-react": "3.7.0",
- "axios": "^1.7.2",
- "luxon": "^3.4.4",
+ "@mui/icons-material": "^6.4.1",
+ "@mui/material": "^6.4.1",
+ "@mui/styled-engine-sc": "^6.4.0",
+ "@reduxjs/toolkit": "^2.5.0",
+ "axios": "^1.7.9",
+ "notistack": "^3.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "react-redux": "^9.1.2",
- "uuid": "^10.0.0"
+ "react-markdown": "^8.0.7",
+ "react-redux": "^9.2.0",
+ "react-router-dom": "^7.1.1",
+ "react-syntax-highlighter": "^15.6.1",
+ "remark-breaks": "^4.0.0",
+ "remark-frontmatter": "^5.0.0",
+ "remark-gfm": "^3.0.1",
+ "styled-components": "^6.1.14"
},
"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"
+ "@rollup/plugin-terser": "^0.4.4",
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^14.4.3",
+ "@types/jest": "^29.4.0",
+ "@types/node": "^18.13.0",
+ "@types/react": "^19.0.2",
+ "@types/react-dom": "^19.0.2",
+ "@types/react-syntax-highlighter": "^15.5.13",
+ "@vitejs/plugin-react": "^4.3.4",
+ "concurrently": "^7.6.0",
+ "cors": "^2.8.5",
+ "cross-env": "^7.0.3",
+ "dotenv": "^16.4.7",
+ "express": "^4.21.2",
+ "nodemon": "^3.1.9",
+ "prettier": "^3.5.3",
+ "rollup-plugin-visualizer": "^5.14.0",
+ "sass": "^1.83.1",
+ "typescript": "^5.7.3",
+ "vite": "^5.3.1",
+ "vite-plugin-compression": "^0.5.1",
+ "vite-plugin-mkcert": "^1.17.6",
+ "vite-plugin-sass-dts": "^1.3.30",
+ "vite-plugin-svgr": "^4.3.0",
+ "vitest": "^3.1.2",
+ "wait-on": "^7.0.1",
+ "webpack-bundle-analyzer": "^4.10.2"
}
}
diff --git a/app-frontend/react/postcss.config.cjs b/app-frontend/react/postcss.config.cjs
deleted file mode 100644
index e817f56..0000000
--- a/app-frontend/react/postcss.config.cjs
+++ /dev/null
@@ -1,14 +0,0 @@
-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/app-frontend/react/src/assets/opea-icon-color.svg b/app-frontend/react/public/favicon.ico
similarity index 100%
rename from app-frontend/react/src/assets/opea-icon-color.svg
rename to app-frontend/react/public/favicon.ico
diff --git a/app-frontend/react/public/logo192.png b/app-frontend/react/public/logo192.png
new file mode 100644
index 0000000..fa313ab
Binary files /dev/null and b/app-frontend/react/public/logo192.png differ
diff --git a/app-frontend/react/public/logo512.png b/app-frontend/react/public/logo512.png
new file mode 100644
index 0000000..bd5d4b5
Binary files /dev/null and b/app-frontend/react/public/logo512.png differ
diff --git a/app-frontend/react/public/manifest.json b/app-frontend/react/public/manifest.json
new file mode 100644
index 0000000..14363bb
--- /dev/null
+++ b/app-frontend/react/public/manifest.json
@@ -0,0 +1,15 @@
+{
+ "short_name": "OPEA Studio App",
+ "name": "OPEA Studio APP UI",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/app-frontend/react/public/model_configs.json b/app-frontend/react/public/model_configs.json
new file mode 100644
index 0000000..cea98dc
--- /dev/null
+++ b/app-frontend/react/public/model_configs.json
@@ -0,0 +1,9 @@
+[
+ {
+ "model_name": "Intel/neural-chat-7b-v3-3",
+ "displayName": "Intel Neural Chat",
+ "minToken": 100,
+ "maxToken": 2000,
+ "types": ["chat", "summary", "code"]
+ }
+]
diff --git a/app-frontend/react/public/robots.txt b/app-frontend/react/public/robots.txt
new file mode 100644
index 0000000..01b0f9a
--- /dev/null
+++ b/app-frontend/react/public/robots.txt
@@ -0,0 +1,2 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
diff --git a/app-frontend/react/public/vite.svg b/app-frontend/react/public/vite.svg
deleted file mode 100644
index e7b8dfb..0000000
--- a/app-frontend/react/public/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/app-frontend/react/src/App.scss b/app-frontend/react/src/App.scss
index 187764a..1317587 100644
--- a/app-frontend/react/src/App.scss
+++ b/app-frontend/react/src/App.scss
@@ -1,42 +1 @@
-// 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;
-}
+// Post javascript styles
diff --git a/app-frontend/react/src/App.tsx b/app-frontend/react/src/App.tsx
index 17ba06b..fd8a379 100644
--- a/app-frontend/react/src/App.tsx
+++ b/app-frontend/react/src/App.tsx
@@ -1,39 +1,182 @@
-// 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';
-// import { UiFeatures } from "./common/Sandbox";
-import { UI_FEATURES } from "./config";
-
-// const dispatch = useAppDispatch();
-
-const title = "OPEA Studio"
-const navList: SidebarNavList = [
- { icon: IconMessages, label: title }
-]
-
-function App() {
- const enabledUiFeatures = UI_FEATURES;
-
- return (
-
-
-
-
-
- )
-}
-
-export default App
+import "./App.scss";
+
+import React, { Suspense, useEffect } from "react";
+import { BrowserRouter, Routes, Route } from "react-router-dom";
+import ProtectedRoute from "@layouts/ProtectedRoute/ProtectedRoute";
+
+import { setUser, userSelector } from "@redux/User/userSlice";
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import {
+ conversationSelector,
+ getAllConversations,
+ getSupportedModels,
+ getSupportedUseCases,
+} from "@redux/Conversation/ConversationSlice";
+import { getPrompts } from "@redux/Prompt/PromptSlice";
+
+import MainLayout from "@layouts/Main/MainLayout";
+import MinimalLayout from "@layouts/Minimal/MinimalLayout";
+import Notification from "@components/Notification/Notification";
+import { Box, styled, Typography } from "@mui/material";
+// import { AtomIcon } from "@icons/Atom";
+
+import Home from "@pages/Home/Home";
+import ChatView from "@pages/Chat/ChatView";
+
+// const HistoryView = React.lazy(() => import("@pages/History/HistoryView"));
+// const DataSourceManagement = React.lazy(
+// () => import("@pages/DataSource/DataSourceManagement")
+// );
+
+import HistoryView from "@pages/History/HistoryView";
+import DataSourceManagement from "@pages/DataSource/DataSourceManagement";
+
+const LoadingBox = styled(Box)({
+ display: "flex",
+ flexDirection: "column",
+ justifyContent: "center",
+ alignItems: "center",
+ height: "100vh",
+ width: "100vw",
+});
+
+const App = () => {
+ const dispatch = useAppDispatch();
+ const { name, isAuthenticated } = useAppSelector(userSelector);
+ const { useCase } = useAppSelector(conversationSelector);
+
+ useEffect(() => {
+ // Set static admin user
+ dispatch(
+ setUser({
+ name: "admin",
+ isAuthenticated: true,
+ role: "Admin",
+ })
+ );
+ }, [dispatch]);
+
+ const initSettings = () => {
+ if (isAuthenticated) {
+ dispatch(getSupportedUseCases());
+ dispatch(getSupportedModels());
+ dispatch(getPrompts());
+ }
+ };
+
+ useEffect(() => {
+ if (isAuthenticated) initSettings();
+ }, [isAuthenticated]);
+
+ useEffect(() => {
+ // if (isAuthenticated && useCase) {
+ // dispatch(getAllConversations({ user: name, useCase: useCase }));
+ // }
+ dispatch(getAllConversations({ user: name}));
+
+ console.log ("on reload")
+ }, [useCase, name, isAuthenticated]);
+
+ return (
+
+
+ {/* Routes wrapped in MainLayout */}
+ }>
+
+ }
+ />
+
+
+ }>
+
+ }
+ />
+
+
+ }>
+ (
+
+ )}
+ />
+ }
+ />
+ (
+
+ )}
+ />
+ }
+ />
+
+
+ }>
+
+ }
+ />
+
+ }
+ />
+
+ }
+ />
+
+ }
+ />
+
+
+ {/* Routes not wrapped in MainLayout */}
+ }>
+ {/* } /> */}
+
+
+
+
+ );
+};
+
+export default App;
\ No newline at end of file
diff --git a/app-frontend/react/src/assets/icons/moon.svg b/app-frontend/react/src/assets/icons/moon.svg
new file mode 100644
index 0000000..a9f36a8
--- /dev/null
+++ b/app-frontend/react/src/assets/icons/moon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app-frontend/react/src/assets/opea-icon-black.svg b/app-frontend/react/src/assets/icons/opea-icon-black.svg
similarity index 100%
rename from app-frontend/react/src/assets/opea-icon-black.svg
rename to app-frontend/react/src/assets/icons/opea-icon-black.svg
diff --git a/studio-frontend/packages/server/src/nodes/opea-icon-color.svg b/app-frontend/react/src/assets/icons/opea-icon-color.svg
similarity index 100%
rename from studio-frontend/packages/server/src/nodes/opea-icon-color.svg
rename to app-frontend/react/src/assets/icons/opea-icon-color.svg
diff --git a/app-frontend/react/src/assets/icons/sun.svg b/app-frontend/react/src/assets/icons/sun.svg
new file mode 100644
index 0000000..510dad6
--- /dev/null
+++ b/app-frontend/react/src/assets/icons/sun.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app-frontend/react/src/assets/react.svg b/app-frontend/react/src/assets/react.svg
deleted file mode 100644
index 6c87de9..0000000
--- a/app-frontend/react/src/assets/react.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/app-frontend/react/src/common/Sandbox.ts b/app-frontend/react/src/common/Sandbox.ts
deleted file mode 100644
index eee8539..0000000
--- a/app-frontend/react/src/common/Sandbox.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export type UiFeatures = {
- dataprep: boolean;
- chat: boolean;
- };
\ No newline at end of file
diff --git a/app-frontend/react/src/common/client.ts b/app-frontend/react/src/common/client.ts
deleted file mode 100644
index 7512f73..0000000
--- a/app-frontend/react/src/common/client.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-// 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/app-frontend/react/src/components/Chat_Assistant/ChatAssistant.module.scss b/app-frontend/react/src/components/Chat_Assistant/ChatAssistant.module.scss
new file mode 100644
index 0000000..ac8428e
--- /dev/null
+++ b/app-frontend/react/src/components/Chat_Assistant/ChatAssistant.module.scss
@@ -0,0 +1,68 @@
+.chatReply {
+ display: flex;
+ flex-direction: row;
+
+ .icon {
+ padding-right: 1rem;
+
+ svg {
+ width: 24px;
+ height: 24px;
+ }
+ }
+}
+
+.ellipsis {
+ position: relative;
+
+ span {
+ position: relative;
+ animation: dance 1.5s infinite ease-in-out;
+ }
+
+ span:nth-child(1) {
+ margin-left: 2px;
+ animation-delay: 0s;
+ }
+
+ span:nth-child(2) {
+ animation-delay: 0.3s;
+ }
+
+ span:nth-child(3) {
+ animation-delay: 0.6s;
+ }
+}
+
+@keyframes dance {
+ 0%,
+ 100% {
+ bottom: 0;
+ opacity: 1;
+ }
+ 20% {
+ bottom: 5px;
+ opacity: 0.7;
+ }
+ 40% {
+ bottom: 0;
+ opacity: 1;
+ }
+}
+
+.textedit {
+ width: 100%;
+ min-height: 50px;
+ padding: 1rem;
+}
+
+.chatPrompt {
+ width: 100%;
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ word-break: break-word;
+
+ p:first-of-type {
+ margin-top: 0;
+ }
+}
diff --git a/app-frontend/react/src/components/Chat_Assistant/ChatAssistant.tsx b/app-frontend/react/src/components/Chat_Assistant/ChatAssistant.tsx
new file mode 100644
index 0000000..9f00433
--- /dev/null
+++ b/app-frontend/react/src/components/Chat_Assistant/ChatAssistant.tsx
@@ -0,0 +1,227 @@
+import React, { useEffect, useRef, useState } from "react";
+
+import styles from "./ChatAssistant.module.scss";
+import {
+ Button,
+ Typography,
+ IconButton,
+ Box,
+ styled,
+ Tooltip,
+} from "@mui/material";
+import { AtomIcon } from "@icons/Atom";
+import ThumbUpIcon from "@mui/icons-material/ThumbUp";
+import ThumbUpOutlinedIcon from "@mui/icons-material/ThumbUpOutlined";
+import ThumbDownIcon from "@mui/icons-material/ThumbDown";
+import ThumbDownOutlinedIcon from "@mui/icons-material/ThumbDownOutlined";
+import ContentCopyIcon from "@mui/icons-material/ContentCopy";
+import EditNoteIcon from "@mui/icons-material/EditNote";
+// import ChatSettingsModal from "@components/Chat_SettingsModal/ChatSettingsModal";
+
+import {
+ NotificationSeverity,
+ notify,
+} from "@components/Notification/Notification";
+import { ChatMessageProps, Message } from "@redux/Conversation/Conversation";
+import ChatMarkdown from "@components/Chat_Markdown/ChatMarkdown";
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import {
+ conversationSelector,
+ // saveConversationtoDatabase,
+ setSelectedConversationHistory,
+} from "@redux/Conversation/ConversationSlice";
+import WaitingIcon from "@icons/Waiting";
+
+const CancelStyle = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.actionButtons.delete,
+}));
+
+const SaveStyle = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.actionButtons.solid,
+}));
+
+const ChatAssistant: React.FC = ({
+ message,
+ pending = false,
+}) => {
+ const dispatch = useAppDispatch();
+ const {
+ onGoingResult,
+ selectedConversationHistory,
+ selectedConversationId,
+ type,
+ } = useAppSelector(conversationSelector);
+
+ const [currentMessage, setCurrentMessage] = useState(message);
+ const [editResponse, setEditResponse] = useState(false);
+ const responseRef = useRef(currentMessage.content);
+ const [disabledSave, setDisabledSave] = useState(false);
+ const [inputHeight, setInputHeight] = useState(0);
+ const heightCheck = useRef(null);
+ const isClipboardAvailable = navigator.clipboard && window.isSecureContext;
+
+ useEffect(() => {
+ setCurrentMessage(message);
+ }, [message]);
+
+ const assistantMessage = currentMessage.content ?? "";
+
+ // const [feedback, setFeedback] = useState(
+ // currentMessage.feedback?.is_thumbs_up === true ? true : currentMessage.feedback?.is_thumbs_up === false ? false : null
+ // );
+
+ // const submitFeedback = (thumbsUp: boolean) => {
+ // setFeedback(thumbsUp);
+ // notify('Feedback Submitted', NotificationSeverity.SUCCESS);
+ // // MessageService.submitFeedback({ id: currentMessage.message_id, feedback: {is_thumbs_up: thumbsUp}, useCase: selectedUseCase.use_case });
+ // };
+
+ const copyText = (text: string) => {
+ navigator.clipboard.writeText(text);
+ notify("Copied to clipboard", NotificationSeverity.SUCCESS);
+ };
+
+ const modifyResponse = () => {
+ if (heightCheck.current) {
+ let updateHeight = heightCheck.current.offsetHeight;
+ setInputHeight(updateHeight);
+ setEditResponse(true);
+ }
+ };
+
+ const updateResponse = (response: string) => {
+ responseRef.current = response;
+ setDisabledSave(response === "");
+ };
+
+ const saveResponse = () => {
+ const convoClone: Message[] = selectedConversationHistory.map(
+ (messageItem) => {
+ if (messageItem.time === currentMessage.time) {
+ return {
+ ...messageItem,
+ content: responseRef.current,
+ };
+ }
+ return messageItem;
+ },
+ );
+
+ dispatch(setSelectedConversationHistory(convoClone));
+ // dispatch(
+ // saveConversationtoDatabase({
+ // conversation: { id: selectedConversationId },
+ // }),
+ // );
+
+ setInputHeight(0);
+ setEditResponse(false);
+ setDisabledSave(false);
+ };
+
+ const cancelResponse = () => {
+ setEditResponse(false);
+ };
+
+ const displayCurrentMessage = () => {
+ if (currentMessage.content) {
+ if (editResponse) {
+ return (
+
+
+
+
+ Save
+
+ Cancel
+
+ );
+ } else {
+ return (
+
+
+
+ );
+ }
+ } else {
+ return (
+
+ Generating response
+
+ .
+ .
+ .
+
+
+ );
+ }
+ };
+
+ const displayMessageActions = () => {
+ if (onGoingResult) return;
+
+ return (
+
+ {/*TODO: feedback support */}
+ {/* submitFeedback(true)}>
+ {feedback === null || feedback === false ? (
+
+ ) : (
+
+ )}
+
+
+ submitFeedback(false)}>
+ {feedback === null || feedback === true ? (
+
+ ) : (
+
+ )}
+ */}
+
+ {/* */}
+
+ {isClipboardAvailable && (
+
+ copyText(assistantMessage)}>
+
+
+
+ )}
+
+ {type === "chat" && (
+
+
+
+
+
+ )}
+
+ );
+ };
+
+ return (
+
+
+
+
+ {displayCurrentMessage()}
+
+ {!pending && displayMessageActions()}
+
+
+ );
+};
+
+export default ChatAssistant;
diff --git a/app-frontend/react/src/components/Chat_Markdown/ChatMarkdown.tsx b/app-frontend/react/src/components/Chat_Markdown/ChatMarkdown.tsx
new file mode 100644
index 0000000..464320e
--- /dev/null
+++ b/app-frontend/react/src/components/Chat_Markdown/ChatMarkdown.tsx
@@ -0,0 +1,128 @@
+import React, { lazy, Suspense, useEffect, useState } from "react";
+import markdownStyles from "./markdown.module.scss";
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+import remarkFrontmatter from "remark-frontmatter";
+import remarkBreaks from "remark-breaks";
+import ThinkCard from "./ThinkRender/ThinkCard";
+import { Button, Collapse, Box } from "@mui/material";
+import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
+import ExpandLessIcon from "@mui/icons-material/ExpandLess";
+
+// const CodeRender = lazy(() => import("./CodeRender/CodeRender"));
+import CodeRender from "./CodeRender/CodeRender";
+
+type MarkdownProps = {
+ content: string;
+};
+
+const extractThinkBlocks = (markdown: string): { cleaned: string; thinks: string[] } => {
+ const thinkRegex = /([\s\S]*?)<\/think>/g;
+ const thinks: string[] = [];
+ let cleaned = markdown;
+ let match;
+
+ while ((match = thinkRegex.exec(markdown)) !== null) {
+ thinks.push(match[1].trim());
+ }
+
+ cleaned = markdown.replace(thinkRegex, "").trim();
+
+ return { cleaned, thinks };
+};
+
+const ChatMarkdown = ({ content }: MarkdownProps) => {
+ useEffect(() => {
+ import("./CodeRender/CodeRender");
+ }, []);
+
+ const { cleaned, thinks } = extractThinkBlocks(
+ content.replace(/\\\\n/g, "\n").replace(/\\n/g, "\n")
+ );
+
+ const [showThinks, setShowThinks] = useState(false);
+
+ return (
+
+ {thinks.length > 0 && (
+
+ setShowThinks((prev) => !prev)}
+ startIcon={showThinks ? : }
+ sx={{
+ borderColor: "#333",
+ color: "#333",
+ "&:hover": {
+ borderColor: "#000",
+ backgroundColor: "#f3f3f3",
+ },
+ }}
+ >
+ {showThinks ? "Hide thought process" : "Show thought process"}
+
+
+
+ {thinks.map((block, idx) => (
+
+ ))}
+
+
+
+ )}
+
+
{
+ const hasBlockElement = React.Children.toArray(children).some(
+ (child) =>
+ React.isValidElement(child) &&
+ typeof child.type === "string" &&
+ ["div", "h1", "h2", "h3", "ul", "ol", "table"].includes(child.type)
+ );
+ return hasBlockElement ? (
+ <>{children}>
+ ) : (
+
+ {children}
+
+ );
+ },
+ a: ({ children, ...props }) => (
+ //@ts-ignore
+
+ {children}
+
+ ),
+ table: ({ children, ...props }) => (
+
+ ),
+ code({ inline, className, children }) {
+ const lang = /language-(\w+)/.exec(className || "");
+ return (
+ Loading Code Block...}>
+ {/*@ts-ignore*/}
+
+
+ );
+ },
+ }}
+ />
+
+ );
+};
+
+export default ChatMarkdown;
diff --git a/app-frontend/react/src/components/Chat_Markdown/CodeRender/CodeRender.tsx b/app-frontend/react/src/components/Chat_Markdown/CodeRender/CodeRender.tsx
new file mode 100644
index 0000000..3fb833c
--- /dev/null
+++ b/app-frontend/react/src/components/Chat_Markdown/CodeRender/CodeRender.tsx
@@ -0,0 +1,78 @@
+import styles from "./codeRender.module.scss";
+import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
+import {
+ atomOneDark,
+ atomOneLight,
+} from "react-syntax-highlighter/dist/esm/styles/hljs";
+import ContentCopyIcon from "@mui/icons-material/ContentCopy";
+import { IconButton, styled, Tooltip, useTheme } from "@mui/material";
+import {
+ NotificationSeverity,
+ notify,
+} from "@components/Notification/Notification";
+
+const TitleBox = styled("div")(({ theme }) => ({
+ background: theme.customStyles.code?.primary,
+ color: theme.customStyles.code?.title,
+}));
+
+const StyledCode = styled(SyntaxHighlighter)(({ theme }) => ({
+ background: theme.customStyles.code?.secondary + " !important",
+}));
+
+type CodeRenderProps = {
+ cleanCode: React.ReactNode;
+ language: string;
+ inline: boolean;
+};
+const CodeRender = ({ cleanCode, language, inline }: CodeRenderProps) => {
+ const theme = useTheme();
+
+ const isClipboardAvailable = navigator.clipboard && window.isSecureContext;
+
+ cleanCode = String(cleanCode)
+ .replace(/\n$/, "")
+ .replace(/^\s*[\r\n]/gm, ""); //right trim and remove empty lines from the input
+
+ const copyText = (text: string) => {
+ navigator.clipboard.writeText(text);
+ notify("Copied to clipboard", NotificationSeverity.SUCCESS);
+ };
+
+ try {
+ return inline ? (
+
+ {cleanCode}
+
+ ) : (
+
+
+
+ {language || "language not detected"}
+
+
+ {isClipboardAvailable && (
+
+ copyText(cleanCode.toString())}>
+
+
+
+ )}
+
+
+
+
+ );
+ } catch (err) {
+ return {cleanCode} ;
+ }
+};
+
+export default CodeRender;
diff --git a/app-frontend/react/src/components/Chat_Markdown/CodeRender/codeRender.module.scss b/app-frontend/react/src/components/Chat_Markdown/CodeRender/codeRender.module.scss
new file mode 100644
index 0000000..5960048
--- /dev/null
+++ b/app-frontend/react/src/components/Chat_Markdown/CodeRender/codeRender.module.scss
@@ -0,0 +1,36 @@
+.code {
+ margin: 7px 0px;
+
+ .codeHead {
+ padding: 0px 10px !important;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ align-items: center;
+ justify-content: space-between;
+
+ .codeTitle {
+ }
+
+ .codeActionGroup {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ align-items: center;
+ justify-content: flex-start;
+ }
+ }
+
+ .codeHighlighterDiv {
+ margin: 0px !important;
+ white-space: pre-wrap !important;
+
+ code {
+ white-space: pre-wrap !important;
+ }
+ }
+}
+
+.inlineCode {
+ background: #fff;
+}
diff --git a/app-frontend/react/src/components/Chat_Markdown/ThinkRender/ThinkCard.tsx b/app-frontend/react/src/components/Chat_Markdown/ThinkRender/ThinkCard.tsx
new file mode 100644
index 0000000..74db261
--- /dev/null
+++ b/app-frontend/react/src/components/Chat_Markdown/ThinkRender/ThinkCard.tsx
@@ -0,0 +1,29 @@
+// components/ThinkCard.tsx
+import { Card, CardContent, Typography } from "@mui/material";
+
+type ThinkCardProps = {
+ content: string;
+};
+
+const ThinkCard = ({ content }: ThinkCardProps) => {
+ return (
+
+
+
+ {content}
+
+
+
+ );
+};
+
+export default ThinkCard;
diff --git a/app-frontend/react/src/components/Chat_Markdown/markdown.module.scss b/app-frontend/react/src/components/Chat_Markdown/markdown.module.scss
new file mode 100644
index 0000000..e86902e
--- /dev/null
+++ b/app-frontend/react/src/components/Chat_Markdown/markdown.module.scss
@@ -0,0 +1,29 @@
+.tableDiv {
+ &:first-of-type {
+ padding-top: 0px !important;
+ }
+
+ table,
+ th,
+ td {
+ border: 1px solid black;
+ border-collapse: collapse;
+ padding: 5px;
+ }
+}
+
+.md {
+ li {
+ margin-left: 35px; /* Adjust the value based on your preference */
+ }
+}
+
+.markdownWrapper {
+ > p:first-of-type {
+ margin-top: 0.25rem;
+ }
+
+ > p:last-of-type {
+ margin-bottom: 0.25rem;
+ }
+}
diff --git a/app-frontend/react/src/components/Chat_SettingsModal/ChatSettingsModal.tsx b/app-frontend/react/src/components/Chat_SettingsModal/ChatSettingsModal.tsx
new file mode 100644
index 0000000..732e5a2
--- /dev/null
+++ b/app-frontend/react/src/components/Chat_SettingsModal/ChatSettingsModal.tsx
@@ -0,0 +1,43 @@
+import * as React from "react";
+import {
+ Box,
+ Typography,
+ Modal,
+ IconButton,
+ styled,
+ Tooltip,
+} from "@mui/material";
+import SettingsApplicationsOutlinedIcon from "@mui/icons-material/SettingsApplicationsOutlined";
+import PromptSettings from "@components/PromptSettings/PromptSettings";
+import { Close } from "@mui/icons-material";
+import ModalBox from "@root/shared/ModalBox/ModalBox";
+
+const ChatSettingsModal = () => {
+ const [open, setOpen] = React.useState(false);
+ const handleOpen = () => setOpen(true);
+ const handleClose = () => setOpen(false);
+
+ return (
+
+
+
+
+
+
+
+
+
+ Response Settings
+ setOpen(false)}>
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ChatSettingsModal;
diff --git a/app-frontend/react/src/components/Chat_Sources/ChatSources.module.scss b/app-frontend/react/src/components/Chat_Sources/ChatSources.module.scss
new file mode 100644
index 0000000..1a6a0d7
--- /dev/null
+++ b/app-frontend/react/src/components/Chat_Sources/ChatSources.module.scss
@@ -0,0 +1,47 @@
+.sourceWrapper {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ flex-wrap: wrap;
+ width: var(--content-width);
+ margin: 0 auto var(--vertical-spacer);
+ max-width: 100%;
+}
+
+.iconWrap {
+ border: none;
+ border-radius: 6px;
+ margin-right: 0.5rem;
+ width: 30px;
+ height: 30px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.sourceBox {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ margin-left: 1rem;
+ padding: 5px;
+ border-radius: 6px;
+ margin-bottom: 1rem;
+}
+
+.title {
+ margin: 0 0.5rem 0 0;
+ white-space: nowrap;
+ display: inline-block;
+ max-width: 150px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-weight: 400;
+}
+
+.chip {
+ border-radius: 8px;
+ padding: 3px;
+ font-size: 12px;
+}
diff --git a/app-frontend/react/src/components/Chat_Sources/ChatSources.tsx b/app-frontend/react/src/components/Chat_Sources/ChatSources.tsx
new file mode 100644
index 0000000..2bf0858
--- /dev/null
+++ b/app-frontend/react/src/components/Chat_Sources/ChatSources.tsx
@@ -0,0 +1,28 @@
+import { Box } from "@mui/material";
+import { conversationSelector } from "@redux/Conversation/ConversationSlice";
+import { useAppSelector } from "@redux/store";
+import styles from "./ChatSources.module.scss";
+import FileDispaly from "@components/File_Display/FileDisplay";
+
+const ChatSources: React.FC = () => {
+ const { sourceLinks, sourceFiles, sourceType } =
+ useAppSelector(conversationSelector);
+ const isWeb = sourceType === "web";
+ const sourceElements = isWeb ? sourceLinks : sourceFiles;
+
+ if (sourceLinks.length === 0 && sourceFiles.length === 0) return;
+
+ const renderElements = () => {
+ return sourceElements.map((element: any, elementIndex) => {
+ return (
+
+
+
+ );
+ });
+ };
+
+ return {renderElements()} ;
+};
+
+export default ChatSources;
diff --git a/app-frontend/react/src/components/Chat_User/ChatUser.module.scss b/app-frontend/react/src/components/Chat_User/ChatUser.module.scss
new file mode 100644
index 0000000..3a5b507
--- /dev/null
+++ b/app-frontend/react/src/components/Chat_User/ChatUser.module.scss
@@ -0,0 +1,27 @@
+.userWrapper {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 2rem;
+ position: relative;
+
+ .userPrompt {
+ max-width: 80%;
+ border-radius: var(--input-radius);
+ padding: 0.75rem 2rem 0.75rem 1rem;
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ word-break: break-word;
+ }
+
+ .addIcon {
+ position: absolute;
+ right: -16px;
+ top: 3px;
+ opacity: 0;
+ transition: opacity 0.3s;
+ }
+
+ &:hover .addIcon {
+ opacity: 1;
+ }
+}
diff --git a/app-frontend/react/src/components/Chat_User/ChatUser.tsx b/app-frontend/react/src/components/Chat_User/ChatUser.tsx
new file mode 100644
index 0000000..8f08436
--- /dev/null
+++ b/app-frontend/react/src/components/Chat_User/ChatUser.tsx
@@ -0,0 +1,44 @@
+import { IconButton, styled, Tooltip } from "@mui/material";
+import React from "react";
+import styles from "./ChatUser.module.scss";
+import AddCircle from "@mui/icons-material/AddCircle";
+import { useAppDispatch } from "@redux/store";
+// import { addPrompt } from "@redux/Prompt/PromptSlice";
+import ChatMarkdown from "@components/Chat_Markdown/ChatMarkdown";
+
+interface ChatUserProps {
+ content: string;
+}
+
+const UserInput = styled("div")(({ theme }) => ({
+ background: theme.customStyles.user?.main,
+}));
+
+const AddIcon = styled(AddCircle)(({ theme }) => ({
+ path: {
+ fill: theme.customStyles.icon?.main,
+ },
+}));
+
+const ChatUser: React.FC = ({ content }) => {
+ const dispatch = useAppDispatch();
+
+ // const sharePrompt = () => {
+ // dispatch(addPrompt({ promptText: content }));
+ // };
+
+ return (
+
+
+
+
+ {/*
+
+
+
+ */}
+
+ );
+};
+
+export default ChatUser;
diff --git a/app-frontend/react/src/components/Conversation/Conversation.tsx b/app-frontend/react/src/components/Conversation/Conversation.tsx
deleted file mode 100644
index 3f423c6..0000000
--- a/app-frontend/react/src/components/Conversation/Conversation.tsx
+++ /dev/null
@@ -1,293 +0,0 @@
-// 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, Button, Collapse, Group, rem, Slider, Stack, Text, Textarea, Title, Tooltip } from '@mantine/core';
-import { IconArrowRight, IconChevronDown, IconChevronUp, IconFilePlus, IconMessagePlus } from '@tabler/icons-react';
-
-import { conversationSelector, doConversation, newConversation, isAgentSelector, getCurrentAgentSteps } from '../../redux/Conversation/ConversationSlice';
-import { ConversationMessage } from '../Message/conversationMessage';
-import { useAppDispatch, useAppSelector } from '../../redux/store';
-import { Message, MessageRole } from '../../redux/Conversation/Conversation';
-import { UiFeatures } from '../../common/Sandbox';
-import { getCurrentTimeStamp } from '../../common/util';
-import { useDisclosure } from '@mantine/hooks';
-import DataSource from './DataSource';
-import { ConversationSideBar } from './ConversationSideBar';
-
-type ConversationProps = {
- title: string;
- enabledUiFeatures: UiFeatures;
-};
-
-const Conversation = ({ title, enabledUiFeatures }: ConversationProps) => {
- const [prompt, setPrompt] = useState("");
- const [systemPrompt, setSystemPrompt] = useState("You are a helpful assistant.");
- const promptInputRef = useRef(null);
- const [fileUploadOpened, { open: openFileUpload, close: closeFileUpload }] = useDisclosure(false);
-
- const { conversations, onGoingResult, selectedConversationId } = useAppSelector(conversationSelector);
- const isAgent = useAppSelector(isAgentSelector);
- const dispatch = useAppDispatch();
- const selectedConversation = conversations.find(x => x.conversationId === selectedConversationId);
- const scrollViewport = useRef(null);
-
- const [tokenLimit, setTokenLimit] = useState(200);
- const [temperature, setTemperature] = useState(0.30);
-
- const [messageTokenData, setMessageTokenData] = useState<{ [key: string]: { tokens: number; rate: number; time: number } }>({});
- const [currentMessageIndex, setCurrentMessageIndex] = useState(-1);
- const [startTime, setStartTime] = useState(null);
- const [isAssistantTyping, setIsAssistantTyping] = useState(false);
- const [showInferenceParams, setShowInferenceParams] = useState(true);
- // const [isInThinkMode, setIsInThinkMode] = useState(false);
-
- const toSend = "Enter";
-
- 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 = [{ role: MessageRole.System, content: systemPrompt }, ...messages];
-
- setMessageTokenData((prev) => ({
- ...prev,
- [`${selectedConversationId}-${selectedConversation?.Messages.length}`]: { tokens: 0, rate: 0, time: 0 },
- }));
-
- setCurrentMessageIndex(selectedConversation?.Messages.length || 0);
-
- doConversation({
- conversationId: selectedConversationId,
- userPrompt,
- messages,
- maxTokens: tokenLimit,
- temperature: temperature,
- model: "",
- // setIsInThinkMode
- });
- setPrompt("");
- setStartTime(Date.now());
- setIsAssistantTyping(true);
- };
-
- const scrollToBottom = () => {
- scrollViewport.current!.scrollTo({ top: scrollViewport.current!.scrollHeight });
- };
-
- useEffect(() => {
- if (onGoingResult && startTime && currentMessageIndex !== -1) {
- let tokenLength: number;
- if (isAgent) {
- const currentSteps = getCurrentAgentSteps();
- const stepsContent = currentSteps.flatMap(step => step.content).join(" ");
- const stepsSource = currentSteps.flatMap(step => step.source).join(" ");
- const allContent = [stepsContent, stepsSource, onGoingResult].filter(str => str.trim()).join(" ");
- let prevTokenLen = messageTokenData[`${selectedConversationId}-${currentMessageIndex}`]?.tokens || 0;
- tokenLength = allContent.split(/\s+/).filter(token => token.length > 0).length + prevTokenLen;
-
- console.log("Agent Token Calc:", {
- stepsContent,
- stepsSource,
- onGoingResult,
- tokenLength
- });
- } else {
- tokenLength = onGoingResult.split(/\s+/).filter(token => token.length > 0).length;
- }
-
- const currentTimestamp = Date.now();
- const elapsedTime = (currentTimestamp - startTime) / 1000;
- const tokenRate = elapsedTime > 0 ? tokenLength / elapsedTime : 0;
-
- setMessageTokenData((prev) => {
- const updatedData = {
- ...prev,
- [`${selectedConversationId}-${currentMessageIndex}`]: { tokens: tokenLength, rate: tokenRate, time: elapsedTime },
- };
- console.log("Updated token data:", updatedData);
- return updatedData;
- });
-
- setIsAssistantTyping(false);
- }
-
- scrollToBottom();
- }, [onGoingResult, startTime, selectedConversation?.Messages, currentMessageIndex, isAgent]);
-
- 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 the Document icon in the top right corner
-
- >
- )}
-
- {selectedConversation?.Messages.map((message, index) => {
- const messageKey = `${selectedConversationId}-${index - 1}`;
- const tokenData = messageTokenData[messageKey];
- const elapsedTime = tokenData?.time ?? 0;
- const tokens = tokenData?.tokens ?? 0;
- const rate = tokenData?.rate ?? 0;
-
- return (
-
- );
- })}
-
- {selectedConversation && isAssistantTyping && (
-
- )}
-
- {onGoingResult && (
-
- )}
-
-
-
- setShowInferenceParams(!showInferenceParams)}
- rightSection={showInferenceParams ? : }
- mb="xs"
- >
- {showInferenceParams ? "Hide Inference Settings" : "Show Inference Settings"}
-
-
-
- Inference Settings
- Token Limit: {tokenLimit}
-
- Temperature: {temperature.toFixed(2)}
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default Conversation;
\ No newline at end of file
diff --git a/app-frontend/react/src/components/Conversation/ConversationSideBar.tsx b/app-frontend/react/src/components/Conversation/ConversationSideBar.tsx
deleted file mode 100644
index 12591ad..0000000
--- a/app-frontend/react/src/components/Conversation/ConversationSideBar.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-// 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/app-frontend/react/src/components/Conversation/DataSource.tsx b/app-frontend/react/src/components/Conversation/DataSource.tsx
deleted file mode 100644
index 22e87df..0000000
--- a/app-frontend/react/src/components/Conversation/DataSource.tsx
+++ /dev/null
@@ -1,296 +0,0 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-import { ActionIcon, Button, Container, Drawer, FileInput, Loader, rem, Table, Text, TextInput } from '@mantine/core'
-import { IconCheck, IconExclamationCircle, IconFileXFilled } from '@tabler/icons-react';
-import { SyntheticEvent, useState, useEffect } from 'react'
-import { useAppDispatch, useAppSelector } from '../../redux/store'
-import { submitDataSourceURL, addFileDataSource, updateFileDataSourceStatus, uploadFile, fileDataSourcesSelector, FileDataSource, clearFileDataSources } from '../../redux/Conversation/ConversationSlice';
-import { getCurrentTimeStamp, uuidv4 } from "../../common/util";
-import client from "../../common/client";
-import { DATA_PREP_URL } from "../../config";
-
-type Props = {
- opened: boolean
- onClose: () => void
-}
-interface getFileListApiResponse {
- name: string;
- id: string;
- type: string;
- parent: string;
-}
-
-export default function DataSource({ opened, onClose }: Props) {
- const title = "Data Source"
- const [file, setFile] = useState();
- const [fileList, setFileList] = useState([]);
- const [isFile, setIsFile] = useState(true);
- const [deleteSpinner, setDeleteSpinner] = useState(false);
- const [url, setURL] = useState("");
- const dispatch = useAppDispatch();
- const fileDataSources = useAppSelector(fileDataSourcesSelector);
-
- const getFileList = async () => {
-
- try {
- setTimeout(async () => {
- const response = await client.post(
- `${DATA_PREP_URL}/get`,
- {}, // Request body (if needed, replace the empty object with actual data)
- {
- headers: {
- 'Content-Type': 'application/json',
- },
- });
-
- setFileList(response.data);
- }, 1500);
- }
- catch (error) {
- console.error("Error fetching file data:", error);
- }
- };
-
- const deleteFile = async (id: string) => {
- try {
- await client.post(
- `${DATA_PREP_URL}/delete`,
- { file_path: id }, // Request body (if needed, replace the empty object with actual data)
- {
- headers: {
- 'Content-Type': 'application/json',
- },
- });
-
- getFileList();
- }
- catch (error) {
- console.error("Error fetching file data:", error);
- }
- setDeleteSpinner(false);
- }
-
- const handleFileUpload = () => {
- if (file){
- const id = uuidv4();
- dispatch(addFileDataSource({ id, source: [file.name], type: 'Files', startTime: getCurrentTimeStamp() }));
- dispatch(updateFileDataSourceStatus({ id, status: 'uploading' }));
- dispatch(uploadFile({ file }))
- .then((response) => {
- // Handle successful upload
- if (response.payload && response.payload.status === 200) {
- console.log("Upload successful:", response);
- getFileList();
- dispatch(updateFileDataSourceStatus({ id, status: 'uploaded' }));
- }
- else {
- console.error("Upload failed:", response);
- getFileList();
- dispatch(updateFileDataSourceStatus({ id, status: 'failed' }));
- }
- })
- .catch((error) => {
- // Handle failed upload
- console.error("Upload failed:", error);
- getFileList();
- dispatch(updateFileDataSourceStatus({ id, status: 'failed' }));
- });
- };
- getFileList();
- }
-
- const handleChange = (event: SyntheticEvent) => {
- event.preventDefault()
- setURL((event.target as HTMLTextAreaElement).value)
- }
-
- const handleSubmit = () => {
- const id = uuidv4();
- dispatch(addFileDataSource({ id, source: url.split(";"), type: 'URLs', startTime: getCurrentTimeStamp() }));
- dispatch(updateFileDataSourceStatus({ id, status: 'uploading' }));
- dispatch(submitDataSourceURL({ link_list: url.split(";") }))
- .then((response) => {
- // Handle successful upload
- if (response.payload && response.payload.status === 200) {
- console.log("Upload successful:", response);
- getFileList();
-
- dispatch(updateFileDataSourceStatus({ id, status: 'uploaded' }));
- }
- else {
- console.error("Upload failed:", response);
- getFileList();
-
- dispatch(updateFileDataSourceStatus({ id, status: 'failed' }));
- }
- })
- .catch((error) => {
- // Handle failed upload
- console.error("Upload failed:", error);
- getFileList();
- dispatch(updateFileDataSourceStatus({ id, status: 'failed' }));
- });
- }
-
- useEffect(() => {
- let isFetching = false; // Flag to track if the function is in progress
- getFileList();
- const interval = setInterval(async () => {
- if (!isFetching) {
- isFetching = true;
- await getFileList(); // Wait for the function to complete
- isFetching = false;
- }
- }, 20000); // 2000 ms = 2 seconds
-
- // Clear the interval when the component unmounts
- return () => clearInterval(interval);
- }, []);
-
-
- return (
-
-
- {title}
-
-
- 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
- >
- )}
-
-
-
-
- Upload Job Queue
-
-
-
-
- ID
- Type
- Start Time
- Status
-
-
-
- {fileDataSources.map((item: FileDataSource, index:number) => (
-
- {index+1}
- {item.type}
-
- {new Date(item.startTime*1000).toLocaleString('en-GB', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit',
- hour12: false,
- })}
-
- {
- item.status === 'pending' ?
- ( ) : item.status === 'uploading' ?
- ( ) : item.status === 'uploaded' ?
- (
-
-
-
- ) : (
-
- )
-
- }
-
- ))}
-
-
- dispatch(clearFileDataSources())}
- color="red"
- disabled={fileDataSources.length === 0 || fileDataSources.some((item: FileDataSource) => item.status === 'uploading')}
- style={{ marginBottom: '10px' } }>
- Clear Job Queue
-
-
-
-
- Uploaded Data Sources
-
-
-
-
- ID
- Source Name
- Action
-
-
-
- {fileList.map((item: getFileListApiResponse, index:number) => (
-
- {index+1}
-
- {item.id.length > 40 ? item.id.slice(0, 36) + '...' : item.id}
-
-
- {
- deleteFile(item.id)
- setDeleteSpinner(true)
- }}
- disabled={deleteSpinner}
- >
- {deleteSpinner? ( ) : }
-
-
-
- ))}
-
-
-
-
- )
-}
\ No newline at end of file
diff --git a/app-frontend/react/src/components/Conversation/conversation.module.scss b/app-frontend/react/src/components/Conversation/conversation.module.scss
deleted file mode 100644
index 3d0c1a0..0000000
--- a/app-frontend/react/src/components/Conversation/conversation.module.scss
+++ /dev/null
@@ -1,69 +0,0 @@
-// 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;
- display: grid;
- grid-template-areas:
- "header"
- "messages"
- "sliders"
- "inputs";
- grid-template-columns: auto;
- grid-template-rows: 60px auto min-content 125px; /* Adjusted for flexibility */
-
- .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%;
- }
- }
-
- .conversatioSliders {
- grid-area: sliders;
- padding: 18px;
- border-top: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
- min-height: 50px; /* Ensure the area doesn't collapse */
- }
-
- .conversationActions {
- 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/app-frontend/react/src/components/Data_Web/DataWebInput.tsx b/app-frontend/react/src/components/Data_Web/DataWebInput.tsx
new file mode 100644
index 0000000..ae54cfc
--- /dev/null
+++ b/app-frontend/react/src/components/Data_Web/DataWebInput.tsx
@@ -0,0 +1,71 @@
+import ProgressIcon from "@components/ProgressIcon/ProgressIcon";
+import {
+ CustomTextInput,
+ AddIcon,
+} from "@components/Summary_WebInput/WebInput";
+import styles from "@components/Summary_WebInput/WebInput.module.scss";
+import { Box, InputAdornment } from "@mui/material";
+import {
+ conversationSelector,
+ submitDataSourceURL,
+} from "@redux/Conversation/ConversationSlice";
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import { useEffect, useState } from "react";
+
+const DataWebInput = () => {
+ const { dataSourceUrlStatus } = useAppSelector(conversationSelector);
+ const [inputValue, setInputValue] = useState("");
+ const [uploading, setUploading] = useState(false);
+ const dispatch = useAppDispatch();
+
+ const handleKeyPress = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && inputValue) {
+ handleAdd(inputValue);
+ }
+ };
+
+ const handleAdd = (newSource: string) => {
+ dispatch(submitDataSourceURL({ link_list: [newSource] }));
+ setInputValue("");
+ };
+
+ const handleIconClick = () => {
+ if (inputValue) {
+ handleAdd(inputValue);
+ }
+ };
+
+ useEffect(() => {
+ setUploading(dataSourceUrlStatus === "pending");
+ }, [dataSourceUrlStatus]);
+
+ return (
+
+ ) =>
+ setInputValue(e.target.value)
+ }
+ InputProps={{
+ endAdornment: !uploading ? (
+
+
+
+ ) : (
+
+
+
+ ),
+ }}
+ fullWidth
+ />
+
+ );
+};
+
+export default DataWebInput;
diff --git a/app-frontend/react/src/components/DropDown/DropDown.module.scss b/app-frontend/react/src/components/DropDown/DropDown.module.scss
new file mode 100644
index 0000000..a8f0561
--- /dev/null
+++ b/app-frontend/react/src/components/DropDown/DropDown.module.scss
@@ -0,0 +1,63 @@
+.dropDown {
+ .noWrap {
+ white-space: nowrap;
+ display: flex;
+
+ &.ellipsis span {
+ white-space: nowrap;
+ display: inline-block;
+ width: 150px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin: 0;
+ }
+ }
+
+ .unsetMin {
+ min-width: unset;
+ }
+
+ .chevron {
+ transform: rotate(0deg);
+ transition: transform 0.5s;
+
+ &.open {
+ transform: rotate(180deg);
+ }
+ }
+
+ &.border {
+ border-radius: 8px;
+ margin-left: 0.5rem;
+
+ :global {
+ .MuiList-padding {
+ margin-left: 0 !important;
+ }
+
+ .MuiListItemIcon-root {
+ min-width: unset;
+ }
+ }
+
+ :global {
+ .MuiListItemText-root {
+ margin-top: 3px;
+ margin-bottom: 3px;
+ }
+
+ .MuiList-root {
+ padding: 0;
+ margin-left: 0.5rem;
+
+ .MuiButtonBase-root {
+ padding: 0 0.5rem;
+ }
+ }
+ }
+ }
+}
+
+.leftGap {
+ margin-left: 0.5rem !important;
+}
diff --git a/app-frontend/react/src/components/DropDown/DropDown.tsx b/app-frontend/react/src/components/DropDown/DropDown.tsx
new file mode 100644
index 0000000..dc839f9
--- /dev/null
+++ b/app-frontend/react/src/components/DropDown/DropDown.tsx
@@ -0,0 +1,118 @@
+import React, { useState } from "react";
+import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
+import {
+ List,
+ ListItemButton,
+ ListItemText,
+ MenuItem,
+ Menu,
+ Typography,
+ ListItemIcon,
+ styled,
+ Box,
+} from "@mui/material";
+import styles from "./DropDown.module.scss";
+
+interface DropDownProps {
+ options: { name: string; value: string }[];
+ value?: string;
+ handleChange: (value: string) => void;
+ readOnly?: boolean;
+ border?: boolean;
+ ellipsis?: true;
+}
+
+const CustomMenuItem = styled(MenuItem)(({ theme }) => ({
+ ...theme.customStyles.dropDown,
+}));
+
+const DropDownWrapper = styled(Box)(({ theme }) => ({
+ ...theme.customStyles.dropDown.wrapper,
+}));
+
+const DropDown: React.FC = ({
+ options,
+ value,
+ handleChange,
+ readOnly,
+ border,
+ ellipsis,
+}) => {
+ const [anchorEl, setAnchorEl] = useState(null);
+
+ const foundIndex = options.findIndex((option) => option.value === value);
+
+ const [selectedIndex, setSelectedIndex] = useState(
+ foundIndex !== -1 ? foundIndex : 0,
+ );
+
+ const open = Boolean(anchorEl);
+ const handleClickListItem = (event: React.MouseEvent) => {
+ setAnchorEl(event.currentTarget);
+ };
+
+ const handleMenuItemClick = (index: number) => {
+ setSelectedIndex(index);
+ setAnchorEl(null);
+ handleChange(options[index].value);
+ };
+
+ const handleClose = () => {
+ setAnchorEl(null);
+ };
+
+ if (readOnly) {
+ let name = foundIndex === -1 ? "Unknown" : options[selectedIndex].name;
+ return {name} ;
+ }
+
+ const Wrapper = border ? DropDownWrapper : Box;
+
+ return options.length === 0 ? (
+ <>>
+ ) : (
+
+
+
+
+
+
+
+
+
+
+
+ {options.map((option, index) => (
+ handleMenuItemClick(index)}
+ >
+ {option.name}
+
+ ))}
+
+
+ );
+};
+
+export default DropDown;
diff --git a/app-frontend/react/src/components/File_Display/FileDisplay.module.scss b/app-frontend/react/src/components/File_Display/FileDisplay.module.scss
new file mode 100644
index 0000000..46cb667
--- /dev/null
+++ b/app-frontend/react/src/components/File_Display/FileDisplay.module.scss
@@ -0,0 +1,44 @@
+.file {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: flex-start;
+ padding: 5px 10px;
+ border-radius: 5px;
+ margin-right: 0.5rem;
+ margin-bottom: 0.5rem;
+
+ button {
+ margin-left: 0.5rem;
+ }
+
+ .iconWrap {
+ border: none;
+ border-radius: 6px;
+ margin-right: 0.5rem;
+ width: 30px;
+ height: 30px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .fileName {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 1; // Limits to 2 lines
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-align: left;
+ max-width: 200px;
+ width: 100%;
+ font-size: 12px;
+ font-weight: 500;
+ }
+
+ .fileExt {
+ font-size: 11px;
+ text-align: left;
+ margin-top: -2px;
+ }
+}
diff --git a/app-frontend/react/src/components/File_Display/FileDisplay.tsx b/app-frontend/react/src/components/File_Display/FileDisplay.tsx
new file mode 100644
index 0000000..7aaed02
--- /dev/null
+++ b/app-frontend/react/src/components/File_Display/FileDisplay.tsx
@@ -0,0 +1,51 @@
+import { IconButton } from "@mui/material";
+import { Close, TaskOutlined, Language } from "@mui/icons-material";
+import styled from "styled-components";
+import styles from "./FileDisplay.module.scss";
+
+const FileWrap = styled("div")(({ theme }) => ({
+ ...theme.customStyles.fileInput.file,
+ ...theme.customStyles.gradientShadow,
+}));
+
+const IconWrap = styled("div")(({ theme }) => ({
+ ...theme.customStyles.sources.iconWrap,
+}));
+
+interface FileProps {
+ file: File;
+ index: number;
+ remove?: (value: number) => void;
+ isWeb?: boolean;
+}
+
+const FileDispaly: React.FC = ({ file, index, remove, isWeb }) => {
+ if (!file) return;
+
+ let fileExtension = file.name.split(".").pop()?.toLowerCase();
+ let fileName = isWeb ? file.name : file.name.split(".").shift();
+
+ return (
+
+
+
+
+
+
+
+ {fileName}
+
+ {!isWeb &&
.{fileExtension}
}
+
+
+ {remove && (
+ remove(index)}>
+
+
+ )}
+ {isWeb && }
+
+ );
+};
+
+export default FileDispaly;
diff --git a/app-frontend/react/src/components/File_Input/FileInput.module.scss b/app-frontend/react/src/components/File_Input/FileInput.module.scss
new file mode 100644
index 0000000..273afe7
--- /dev/null
+++ b/app-frontend/react/src/components/File_Input/FileInput.module.scss
@@ -0,0 +1,69 @@
+.fileInputWrapper {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .upload {
+ margin-left: 0.5rem;
+ }
+
+ .inputWrapper {
+ padding: 1rem;
+ text-align: center;
+ box-shadow: none;
+ border-radius: 8px;
+ width: 100%;
+ position: relative;
+ }
+
+ .expand {
+ width: 25px;
+ height: 25px;
+ border-radius: 25px;
+ min-width: unset;
+ border-width: 1px;
+ border-style: solid;
+ transition: transform 0.5s;
+ transform: rotate(0deg);
+ transform-origin: center;
+ margin-left: -12.5px;
+ margin-top: -20px;
+ position: absolute;
+ bottom: -12.5px;
+ z-index: 8;
+
+ &.open {
+ transform: rotate(180deg);
+ }
+ }
+}
+
+.previewFiles {
+ margin-bottom: 0.5rem;
+
+ .fileList {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+
+ label {
+ margin-top: 0.5rem;
+ }
+}
+
+.details {
+ max-height: 0px;
+ transition: max-height 0.4s;
+ overflow: hidden;
+
+ &.detailsOpen {
+ max-height: 400px;
+ }
+}
+
+.detailGap {
+ margin-top: 10px;
+}
diff --git a/app-frontend/react/src/components/File_Input/FileInput.tsx b/app-frontend/react/src/components/File_Input/FileInput.tsx
new file mode 100644
index 0000000..a6213f0
--- /dev/null
+++ b/app-frontend/react/src/components/File_Input/FileInput.tsx
@@ -0,0 +1,393 @@
+import React, { useEffect, useReducer, useRef, useState } from "react";
+import {
+ Box,
+ Button,
+ Typography,
+ Paper,
+ IconButton,
+ styled,
+} from "@mui/material";
+import {
+ UploadFile,
+ Close,
+ ExpandMore,
+ FileUploadOutlined,
+} from "@mui/icons-material";
+import styles from "./FileInput.module.scss";
+import {
+ NotificationSeverity,
+ notify,
+} from "@components/Notification/Notification";
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import {
+ conversationSelector,
+ setSourceFiles,
+ setUploadInProgress,
+ uploadFile,
+} from "@redux/Conversation/ConversationSlice";
+import ModalBox from "@shared/ModalBox/ModalBox";
+import { OutlineButton, SolidButton } from "@shared/ActionButtons";
+import { Link } from "react-router-dom";
+import FileDispaly from "@components/File_Display/FileDisplay";
+import ProgressIcon from "@components/ProgressIcon/ProgressIcon";
+import { s } from "vite/dist/node/types.d-aGj9QkWt";
+
+const ExpandButton = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.promptExpandButton,
+}));
+
+interface FileWithPreview {
+ file: File;
+ preview: string;
+}
+
+interface FileInputProps {
+ imageInput?: boolean;
+ summaryInput?: boolean;
+ maxFileCount?: number;
+ confirmationModal?: boolean;
+ dataManagement?: boolean;
+}
+
+const summaryFileExtensions = [
+ "txt",
+ "pdf",
+ "docx",
+ "mp3",
+ "wav",
+ "ogg",
+ "mp4",
+ "avi",
+ "mov"
+]
+
+const imageExtensions = ["jpg", "jpeg", "png", "gif"];
+const docExtensions = ["txt"];
+const dataExtensions = [
+ "txt",
+ "pdf",
+ "csv",
+ "xls",
+ "xlsx",
+ "json" /*"doc", "docx", "md", "ppt", "pptx", "html", "xml", "xsl", "xslt", "rtf", "v", "sv"*/,
+];
+const maxImageSize = 3 * 1024 * 1024; // 3MB
+const maxDocSize = 80 * 1024 * 1024; // 200MB
+const maxSummarySize = 80 * 1024 * 1024; // 200MB
+
+const FileInputWrapper = styled(Paper)(({ theme }) => ({
+ ...theme.customStyles.fileInput.wrapper,
+}));
+
+const FileInput: React.FC = ({
+ maxFileCount = 5,
+ imageInput,
+ summaryInput,
+ dataManagement,
+}) => {
+ const { model, models, useCase, filesInDataSource, uploadInProgress, type } =
+ useAppSelector(conversationSelector);
+ // const { filesInDataManagement, uploadInProgress } = useAppSelector(dataManagementSelector);
+
+ const dispatch = useAppDispatch();
+ const [confirmUpload, setConfirmUpload] = useState(false);
+ const [filesToUpload, setFilesToUpload] = useState<
+ (FileWithPreview | File)[]
+ >([]);
+ const [details, showDetails] = useState(filesToUpload.length === 0);
+
+ const inputRef = useRef(null);
+
+ const extensions = summaryInput?
+ summaryFileExtensions :
+ imageInput
+ ? imageExtensions
+ : dataManagement
+ ? dataExtensions
+ : docExtensions;
+ const maxSize = summaryInput? maxSummarySize:
+ imageInput ? maxImageSize : maxDocSize;
+
+ const [insightToken, setInsightToken] = useState(0);
+
+ useEffect(() => {
+ showDetails(filesToUpload.length === 0);
+
+ // summary / faq
+ if (!dataManagement && filesToUpload.length > 0) {
+ dispatch(setSourceFiles(filesToUpload));
+ }
+ }, [filesToUpload]);
+
+ useEffect(() => {
+ // model sets insight token in summary/faq
+ if (!dataManagement) {
+ let selectedModel = models.find(
+ (thisModel) => thisModel.model_name === model,
+ );
+ if (selectedModel) setInsightToken(selectedModel.maxToken);
+ }
+ }, [model, models]);
+
+ useEffect(() => {
+ setFilesToUpload([]);
+ dispatch(setSourceFiles([]));
+ }, [type]);
+
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault();
+ const droppedFiles = Array.from(e.dataTransfer.files);
+ validateFiles(droppedFiles);
+ };
+
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ if (e.target.files) {
+ const selectedFiles = Array.from(e.target.files);
+ const validated = validateFiles(selectedFiles);
+ if (validated) e.target.value = ""; // Clear input
+ }
+ };
+
+ const validateFiles = (newFiles: File[]) => {
+ if (newFiles.length + filesToUpload.length > maxFileCount) {
+ notify(
+ `You can only upload a maximum of ${maxFileCount} file${maxFileCount > 1 ? "s" : ""}.`,
+ NotificationSeverity.ERROR,
+ );
+ return;
+ }
+
+ const validFiles = newFiles.filter((file) => {
+ const fileExtension = file.name.split(".").pop()?.toLowerCase();
+ const isSupportedExtension = extensions.includes(fileExtension || "");
+ const isWithinSizeLimit = file.size <= maxSize;
+
+ const compareTo = dataManagement ? filesInDataSource : filesToUpload;
+
+ let duplicate = compareTo.some((f: any) => {
+ return f.name === file.name;
+ });
+
+ // duplicate file check, currently data management only (summary/faq single file)
+ if (duplicate) {
+ notify(
+ `File "${file.name}" is already added.`,
+ NotificationSeverity.ERROR,
+ );
+ return false;
+ }
+
+ if (!isSupportedExtension) {
+ notify(
+ `File "${file.name}" has an unsupported file type.`,
+ NotificationSeverity.ERROR,
+ );
+ return false;
+ }
+
+ if (!isWithinSizeLimit) {
+ notify(
+ `File "${file.name}" exceeds the maximum size limit of ${imageInput ? "3MB" : "200MB"}.`,
+ NotificationSeverity.ERROR,
+ );
+ return false;
+ }
+
+ return isSupportedExtension && isWithinSizeLimit;
+ });
+
+ if (validFiles.length > 0) {
+ addToQueue(validFiles);
+ }
+
+ return true;
+ };
+
+ const addToQueue = async (newFiles: File[]) => {
+ const filteredFiles = newFiles.filter((file: File | FileWithPreview) => {
+ let activeFile = "file" in file ? file.file : file;
+ return !filesToUpload.some((f: File | FileWithPreview) => {
+ let comparedFile = "file" in f ? f.file : f;
+ return comparedFile.name === activeFile.name;
+ });
+ });
+
+ const filesWithPreview = filteredFiles.map((file) => ({
+ file,
+ preview: URL.createObjectURL(file),
+ }));
+
+ setFilesToUpload([...filesToUpload, ...filesWithPreview]);
+ };
+
+ const removeFile = (index: number) => {
+ let updatedFiles = filesToUpload.filter(
+ (file, fileIndex) => index !== fileIndex,
+ );
+ setFilesToUpload(updatedFiles);
+ };
+
+ const uploadFiles = async () => {
+ dispatch(setUploadInProgress(true));
+
+ const responses = await Promise.all(
+ filesToUpload.map((file: any) => {
+ dispatch(uploadFile({ file: file.file }));
+ }),
+ );
+
+ dispatch(setUploadInProgress(false));
+
+ setConfirmUpload(false);
+ setFilesToUpload([]);
+ };
+
+ const showConfirmUpload = () => {
+ setConfirmUpload(true);
+ };
+
+ const filePreview = () => {
+ if (filesToUpload.length > 0) {
+ return (
+
+
+ {filesToUpload.map((file, fileIndex) => {
+ let activeFile = "file" in file ? file.file : file;
+ return (
+
+
+
+ );
+ })}
+
+
+ );
+ } else {
+ return (
+
+ Upload or Drop Files Here
+
+ );
+ }
+ };
+
+ const renderConfirmUpload = () => {
+ if (confirmUpload) {
+ return (
+
+
+ Uploading files
+ setConfirmUpload(false)}>
+
+
+
+
+
+ I hereby certify that the content uploaded is free from any
+ personally identifiable information or other private data that
+ would violate applicable privacy laws and regulations.
+
+
+ uploadFiles()}>
+ Agree and Continue
+
+ setConfirmUpload(false)}>
+ Cancel
+
+
+
+
+ );
+ }
+ };
+
+ if (uploadInProgress) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+ e.preventDefault()}
+ className={styles.inputWrapper}
+ >
+ {filePreview()}
+
+
+ {filesToUpload.length !== maxFileCount && (
+ inputRef.current?.click()}>
+ Browse Files
+
+
+ )}
+
+ {dataManagement && (
+
+ Upload
+
+ )}
+
+
+ {filesToUpload.length > 0 && (
+ showDetails(!details)}
+ >
+
+
+ )}
+
+
+
+ Limit {imageInput ? "3MB" : "80MB"} per file.
+
+
+
+ Valid file formats are {extensions.join(", ").toUpperCase()}.
+
+
+
+ You can select maximum of {maxFileCount} valid file
+ {maxFileCount > 1 ? "s" : ""}.
+
+
+ {!dataManagement && (
+
+ Max supported input tokens for {imageInput && "images"} data
+ insight is{" "}
+ {insightToken >= 1000 ? insightToken / 1000 + "K" : insightToken}
+
+ )}
+
+
+
+ {renderConfirmUpload()}
+
+ );
+};
+
+export default FileInput;
diff --git a/app-frontend/react/src/components/Header/Header.module.scss b/app-frontend/react/src/components/Header/Header.module.scss
new file mode 100644
index 0000000..4287826
--- /dev/null
+++ b/app-frontend/react/src/components/Header/Header.module.scss
@@ -0,0 +1,160 @@
+.header {
+ height: var(--header-height);
+ backdrop-filter: blur(5px);
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ width: 100%;
+ padding: var(--header-gutter);
+ position: relative;
+ z-index: 999;
+}
+
+.logoContainer {
+ display: flex;
+ align-items: center; /* Vertically centers the company name with the logo */
+ gap: 10px; /* Adjusts space between logo and company name */
+}
+
+.logoImg {
+ /* Ensure the logo has a defined size if needed */
+ height: 40px; /* Example height, adjust as needed */
+ width: auto; /* Maintain aspect ratio */
+}
+
+.companyName {
+ font-size: 1.2rem; /* Adjust font size as needed */
+ /* Add any other styling for the company name */
+}
+
+.viewContext {
+ display: inline-flex;
+ max-width: 200px;
+
+ &.titleWrap {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ :global {
+ svg {
+ min-width: 30px;
+ }
+ }
+ }
+
+ &.capitalize {
+ text-transform: capitalize;
+ }
+
+ @media screen and (max-width: 900px) {
+ display: none;
+
+ &.titleWrap {
+ display: none;
+ }
+ }
+}
+
+.sideWrapper {
+ position: relative;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ z-index: 999;
+ width: 50px;
+ margin-right: calc(var(--header-gutter) * 2);
+ min-width: 0px;
+ max-width: var(--sidebar-width);
+ transition:
+ width 0.3s,
+ min-width 0.3s;
+
+ .chatCopy {
+ opacity: 0;
+ max-width: 0;
+ transition:
+ opacity 0.3s,
+ max-width 0.3s;
+ font-size: 0.75rem;
+ margin-right: 0.5rem;
+ white-space: nowrap;
+ }
+
+ .chatWrapper {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ }
+
+ &.sideWrapperOpen {
+ width: calc(var(--sidebar-width) - (var(--header-gutter) * 2));
+ min-width: calc(var(--sidebar-width) - (var(--header-gutter) * 2));
+
+ .chatCopy {
+ max-width: 100px; // enough to show the text
+ opacity: 1;
+ }
+ }
+}
+
+.rightSide {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+}
+
+.rightActions {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.companyName {
+ font-weight: 600;
+ @media screen and (max-width: 899px) {
+ position: absolute;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+}
+
+.desktopUser {
+ display: none;
+ @media screen and (min-width: 900px) {
+ display: inline-block;
+ }
+}
+
+.newChat {
+ display: none;
+ @media screen and (min-width: 900px) {
+ display: inline-block;
+ }
+}
+
+.accessDropDown {
+ :global {
+ .MuiList-root {
+ padding: 0;
+
+ .MuiButtonBase-root {
+ padding: 0;
+ margin-left: -10px;
+ padding: 0 10px;
+
+ .MuiListItemText-root {
+ margin: 0px;
+ }
+
+ .MuiTypography-root {
+ font-size: 12px !important;
+ font-style: italic;
+ }
+ }
+ }
+ }
+}
diff --git a/app-frontend/react/src/components/Header/Header.tsx b/app-frontend/react/src/components/Header/Header.tsx
new file mode 100644
index 0000000..b369fa8
--- /dev/null
+++ b/app-frontend/react/src/components/Header/Header.tsx
@@ -0,0 +1,232 @@
+import { useEffect, useRef, useState } from "react";
+import { styled } from "@mui/material/styles";
+import { Link, useNavigate } from "react-router-dom";
+import config from "@root/config";
+import opeaLogo from "@assets/icons/opea-icon-color.svg"
+
+import styles from "./Header.module.scss";
+import { Box, IconButton, Tooltip, Typography } from "@mui/material";
+import { SideBar } from "@components/SideBar/SideBar";
+// import DropDown from "@components/DropDown/DropDown";
+// import ThemeToggle from "@components/Header_ThemeToggle/ThemeToggle";
+import ViewSidebarOutlinedIcon from "@mui/icons-material/ViewSidebarOutlined";
+// import Create from "@mui/icons-material/Create";
+import AddCommentIcon from '@mui/icons-material/AddComment';
+// import ShareOutlinedIcon from "@mui/icons-material/ShareOutlined";
+// import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
+import ChatBubbleIcon from "@icons/ChatBubble";
+
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import { userSelector } from "@redux/User/userSlice";
+import {
+ Message,
+ MessageRole,
+ // UseCase,
+} from "@redux/Conversation/Conversation";
+import {
+ conversationSelector,
+ // setUseCase,
+} from "@redux/Conversation/ConversationSlice";
+import DownloadChat from "@components/Header_DownloadChat/DownloadChat";
+import { useNavigateWithQuery } from "@utils/navigationAndAxiosWithQuery";
+
+interface HeaderProps {
+ asideOpen: boolean;
+ setAsideOpen: (open: boolean) => void;
+ chatView?: boolean;
+ historyView?: boolean;
+ dataView?: boolean;
+}
+
+// interface AvailableUseCase {
+// name: string;
+// value: string;
+// }
+
+const HeaderWrapper = styled(Box)(({ theme }) => ({
+ ...theme.customStyles.header,
+}));
+
+const Header: React.FC = ({
+ asideOpen,
+ setAsideOpen,
+ chatView,
+ historyView,
+ dataView,
+}) => {
+ const { companyName } = config;
+
+ const sideBarRef = useRef(null);
+ const toggleRef = useRef(null);
+
+ const navigate = useNavigate();
+ const navigateWithQuery = useNavigateWithQuery();
+
+ // const dispatch = useAppDispatch();
+ const { role, name } = useAppSelector(userSelector);
+ const { selectedConversationHistory, type } =
+ useAppSelector(conversationSelector);
+
+ const [currentTopic, setCurrentTopic] = useState("");
+
+ useEffect(() => {
+ if (
+ !selectedConversationHistory ||
+ selectedConversationHistory.length === 0
+ ) {
+ setCurrentTopic("");
+ return;
+ }
+ const firstUserPrompt = selectedConversationHistory.find(
+ (message: Message) => message.role === MessageRole.User,
+ );
+ if (firstUserPrompt) setCurrentTopic(firstUserPrompt.content);
+ }, [selectedConversationHistory]);
+
+ // const handleChange = (value: string) => {
+ // dispatch(setUseCase(value));
+ // };
+
+ const newChat = () => {
+ navigateWithQuery("/");
+ setAsideOpen(false);
+ };
+
+ const handleClickOutside = (event: MouseEvent) => {
+ if (
+ sideBarRef.current &&
+ toggleRef.current &&
+ !sideBarRef.current.contains(event.target as Node) &&
+ !toggleRef.current.contains(event.target as Node)
+ ) {
+ setAsideOpen(false);
+ }
+ };
+
+ useEffect(() => {
+ if (asideOpen) {
+ document.addEventListener("mousedown", handleClickOutside);
+ return () =>
+ document.removeEventListener("mousedown", handleClickOutside);
+ }
+ }, [asideOpen]);
+
+ const userDetails = () => {
+ return (
+
+ {name}
+
+ );
+ };
+
+ const getTitle = () => {
+ if (historyView)
+ return (
+
+
+ Your Chat History
+
+ );
+
+ if (dataView)
+ return (
+
+ Data Source Management
+
+ );
+
+ if (chatView) {
+ if (type !== "chat" && !currentTopic) {
+ return (
+
+ {type}
+
+ );
+ } else {
+ return (
+
+
+
+ {currentTopic}
+
+
+ );
+ }
+ }
+ };
+
+ return (
+
+
+
+ setAsideOpen(!asideOpen)}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {companyName}
+
+
+
+
+ {getTitle()}
+
+
+
+ {/* New Chat */}
+
+
+
+
+
+
+ {chatView && (
+ <>
+ {/*
+
+ */}
+
+
+ >
+ )}
+
+ {/* {chatView && { }}> } */}
+
+ {/*
+
+ */}
+
+ {/* {userDetails()} */}
+
+
+
+ );
+};
+
+export default Header;
diff --git a/app-frontend/react/src/components/Header_DownloadChat/DownloadChat.tsx b/app-frontend/react/src/components/Header_DownloadChat/DownloadChat.tsx
new file mode 100644
index 0000000..0ed8a8c
--- /dev/null
+++ b/app-frontend/react/src/components/Header_DownloadChat/DownloadChat.tsx
@@ -0,0 +1,74 @@
+import { FileDownloadOutlined } from "@mui/icons-material";
+import { IconButton, Tooltip } from "@mui/material";
+import { conversationSelector } from "@redux/Conversation/ConversationSlice";
+import { useAppSelector } from "@redux/store";
+import { useEffect, useState } from "react";
+import { Link } from "react-router-dom";
+
+const DownloadChat = () => {
+ const { selectedConversationHistory, type, model, token, temperature } =
+ useAppSelector(conversationSelector);
+ const [url, setUrl] = useState(undefined);
+ const [fileName, setFileName] = useState("");
+
+ const safeBtoa = (str: string) => {
+ const encoder = new TextEncoder();
+ const uint8Array = encoder.encode(str);
+ let binaryString = "";
+ for (let i = 0; i < uint8Array.length; i++) {
+ binaryString += String.fromCharCode(uint8Array[i]);
+ }
+ return btoa(binaryString);
+ };
+
+ useEffect(() => {
+ if (selectedConversationHistory.length === 0) return;
+
+ //TODO: if we end up with a systemPrompt for code change this
+ const userPromptIndex = type === "code" ? 0 : 1;
+
+ const conversationObject = {
+ model,
+ token,
+ temperature,
+ messages: [...selectedConversationHistory],
+ type,
+ };
+
+ const newUrl = `data:application/json;charset=utf-8;base64,${safeBtoa(JSON.stringify(conversationObject))}`;
+
+ if (
+ selectedConversationHistory &&
+ selectedConversationHistory.length > 0 &&
+ selectedConversationHistory[userPromptIndex]
+ ) {
+ const firstPrompt = selectedConversationHistory[userPromptIndex].content; // Assuming content is a string
+ if (firstPrompt) {
+ const newFileName = firstPrompt.split(" ").slice(0, 4).join("_");
+ setUrl(newUrl);
+ setFileName(newFileName.toLowerCase());
+ }
+ }
+ }, [selectedConversationHistory]);
+
+ //TODO: only support download for chat for now
+ return (
+ url &&
+ type === "chat" && (
+
+
+
+
+
+
+
+ )
+ );
+};
+
+export default DownloadChat;
diff --git a/app-frontend/react/src/components/Header_ThemeToggle/ThemeToggle.module.scss b/app-frontend/react/src/components/Header_ThemeToggle/ThemeToggle.module.scss
new file mode 100644
index 0000000..1d69292
--- /dev/null
+++ b/app-frontend/react/src/components/Header_ThemeToggle/ThemeToggle.module.scss
@@ -0,0 +1,65 @@
+.toggleWrapper {
+ position: relative;
+ margin-right: 10px;
+ display: flex;
+ align-items: center;
+
+ .toggle {
+ width: 100px;
+ height: 34px;
+ padding: 7px;
+ }
+
+ .copy {
+ position: absolute;
+ z-index: 99;
+ margin: 0 26px;
+ font-size: 14px;
+ }
+
+ :global {
+ .MuiSwitch-switchBase {
+ margin: 1px;
+ padding: 0;
+ transform: translateX(6px);
+ transition: transform 0.3s;
+
+ &.Mui-checked {
+ color: #fff;
+ transform: translateX(62px);
+
+ .MuiSwitch-track {
+ opacity: 1;
+ }
+ }
+ }
+
+ .MuiSwitch-track {
+ opacity: 1;
+ height: 30px;
+ border-radius: 30px;
+ margin-top: -5px;
+ background-color: transparent !important;
+ }
+
+ .MuiSwitch-thumb {
+ // background-color: transparent !important;
+ width: 26px;
+ height: 26px;
+ position: relative;
+ margin-top: 3px;
+ margin-left: 2px;
+ box-shadow: none;
+ &::before {
+ content: "";
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ left: 0;
+ top: 0;
+ background-repeat: no-repeat;
+ background-position: center;
+ }
+ }
+ }
+}
diff --git a/app-frontend/react/src/components/Header_ThemeToggle/ThemeToggle.tsx b/app-frontend/react/src/components/Header_ThemeToggle/ThemeToggle.tsx
new file mode 100644
index 0000000..6998770
--- /dev/null
+++ b/app-frontend/react/src/components/Header_ThemeToggle/ThemeToggle.tsx
@@ -0,0 +1,48 @@
+import React, { useContext } from "react";
+import { styled } from "@mui/material/styles";
+import { Switch, Typography, Box } from "@mui/material";
+import { ThemeContext } from "@contexts/ThemeContext";
+import styles from "./ThemeToggle.module.scss";
+
+const MaterialUISwitch = styled(Switch)(({ theme }) => ({
+ ...theme.customStyles.themeToggle,
+}));
+
+const ThemeToggle: React.FC = () => {
+ const { darkMode, toggleTheme } = useContext(ThemeContext);
+ const [checked, setChecked] = React.useState(darkMode);
+
+ const handleChange = (event: React.ChangeEvent) => {
+ setChecked(event.target.checked);
+ toggleTheme();
+ };
+
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ if (event.key === "Enter" || event.key === " ") {
+ handleChange({
+ target: { checked: !checked },
+ } as React.ChangeEvent);
+ }
+ };
+
+ return (
+
+
+ {checked ? "Dark" : "Light"}
+
+
+
+ );
+};
+
+export default ThemeToggle;
diff --git a/app-frontend/react/src/components/Message/conversationMessage.module.scss b/app-frontend/react/src/components/Message/conversationMessage.module.scss
deleted file mode 100644
index b006495..0000000
--- a/app-frontend/react/src/components/Message/conversationMessage.module.scss
+++ /dev/null
@@ -1,15 +0,0 @@
-// 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/app-frontend/react/src/components/Message/conversationMessage.tsx b/app-frontend/react/src/components/Message/conversationMessage.tsx
deleted file mode 100644
index 66df29d..0000000
--- a/app-frontend/react/src/components/Message/conversationMessage.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-// 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 { Badge, Card, Loader, Text, Tooltip, Button, Collapse, Flex } from "@mantine/core";
-import { DateTime } from "luxon";
-import { useState } from 'react';
-import { IconChevronDown, IconChevronUp } from '@tabler/icons-react';
-import { AgentStep, isAgentSelector } from '../../redux/Conversation/ConversationSlice';
-import { useAppSelector } from '../../redux/store';
-
-
-export interface ConversationMessageProps {
- message: string;
- human: boolean;
- date: number;
- tokenCount?: number;
- tokenRate?: number;
- elapsedTime?: number;
- agentSteps: AgentStep[];
- // isInThink: boolean;
-}
-
-export function ConversationMessage({ human, message, date, elapsedTime, tokenCount, tokenRate, agentSteps }: ConversationMessageProps) {
- const dateFormat = () => {
- return DateTime.fromJSDate(new Date(date)).toLocaleString(DateTime.DATETIME_MED);
- };
-
- const [showThoughts, setShowThoughts] = useState(true);
- const isAgent = useAppSelector(isAgentSelector);
-
- return (
-
-
- {human ? : }
-
-
-
- {human ? "You" : "Assistant"}
-
-
- {dateFormat()}
-
-
-
-
- {!human && isAgent && (
-
- setShowThoughts(!showThoughts)}
- rightSection={showThoughts ? : }
- mb="xs"
- >
- {showThoughts ? "Hide Thinking" : "Show Thinking"}
-
-
- {agentSteps.length > 0 ? (
- agentSteps.map((step, index) => (
-
-
-
- Step {index + 1}
-
- {step.tool && (
-
- Tool: {step.tool}
-
- )}
-
- {step.content.length > 0 && (
-
- {step.content.join(", ")}
-
- )}
- {step.source.length > 0 && (
-
- {step.source.join(", ")}
-
- )}
-
- ))
- ) : (
-
-
- Thinking...
-
-
- )}
-
-
- )}
-
-
- {human ? message : message === "..." ? : message}
-
-
- {!human && elapsedTime !== undefined && tokenCount !== undefined && tokenRate !== undefined && (
-
-
- Time: {elapsedTime.toFixed(2)}s • Tokens: {tokenCount} • {tokenRate.toFixed(2)} tokens/s
-
-
- )}
-
- );
-}
\ No newline at end of file
diff --git a/app-frontend/react/src/components/Notification/Notification.tsx b/app-frontend/react/src/components/Notification/Notification.tsx
new file mode 100644
index 0000000..f3948c5
--- /dev/null
+++ b/app-frontend/react/src/components/Notification/Notification.tsx
@@ -0,0 +1,144 @@
+import { AlertColor, IconButton, styled } from "@mui/material";
+import {
+ SnackbarProvider,
+ useSnackbar,
+ MaterialDesignContent,
+ closeSnackbar,
+} from "notistack";
+import { useEffect } from "react";
+import { Subject } from "rxjs";
+import {
+ TaskAlt,
+ WarningAmberOutlined,
+ ErrorOutlineOutlined,
+ InfoOutlined,
+ Close,
+} from "@mui/icons-material";
+
+interface NotificationDataProps {
+ message: string;
+ variant: AlertColor;
+}
+
+type NotificationSeverity = "error" | "info" | "success" | "warning";
+
+export const NotificationSeverity = {
+ SUCCESS: "success" as NotificationSeverity,
+ ERROR: "error" as NotificationSeverity,
+ WARNING: "warning" as NotificationSeverity,
+ INFO: "info" as NotificationSeverity,
+};
+
+const severityColor = (variant: string) => {
+ switch (variant) {
+ case "success":
+ return "#388e3c";
+ case "error":
+ return "#d32f2f";
+ case "warning":
+ return "#f57c00";
+ case "info":
+ return "#0288d1";
+ default:
+ return "rgba(0, 0, 0, 0.87)";
+ }
+};
+
+const StyledMaterialDesignContent = styled(MaterialDesignContent)<{
+ severity: AlertColor;
+}>(({ variant }) => ({
+ backgroundColor: (() => {
+ switch (variant) {
+ case "success":
+ return "rgb(225,238,226)";
+ case "error":
+ return "rgb(248,224,224)";
+ case "warning":
+ return "rgb(254,235,217)";
+ case "info":
+ return "rgb(217,237,248)";
+ default:
+ return "rgb(225,238,226)";
+ }
+ })(),
+ border: `1px solid ${severityColor(variant)}`,
+ color: severityColor(variant),
+ ".MuiAlert-action": {
+ paddingTop: 0,
+ scale: 0.8,
+ borderLeft: `1px solid ${severityColor(variant)}`,
+ marginLeft: "1rem",
+ },
+ svg: {
+ marginRight: "1rem",
+ },
+ "button svg": {
+ marginRight: "0",
+ path: {
+ fill: severityColor(variant),
+ },
+ },
+}));
+
+const CloseIcon = styled(IconButton)(() => ({
+ minWidth: "unset",
+}));
+
+const Notify = new Subject();
+
+export const notify = (message: string, variant: AlertColor) => {
+ if (!variant) variant = NotificationSeverity.SUCCESS;
+ Notify.next({ message, variant });
+};
+
+const NotificationComponent = () => {
+ const { enqueueSnackbar } = useSnackbar();
+
+ useEffect(() => {
+ const subscription = Notify.subscribe({
+ next: (notification) => {
+ enqueueSnackbar(notification.message, {
+ variant: notification.variant,
+ action: (key) => (
+ closeSnackbar(key)}
+ variant={notification.variant}
+ >
+
+
+ ),
+ });
+ },
+ });
+
+ return () => subscription.unsubscribe();
+ }, []);
+
+ return <>>;
+};
+
+const Notification = () => {
+ return (
+ ,
+ warning: ,
+ error: ,
+ info: ,
+ }}
+ Components={{
+ success: StyledMaterialDesignContent,
+ warning: StyledMaterialDesignContent,
+ error: StyledMaterialDesignContent,
+ info: StyledMaterialDesignContent,
+ }}
+ >
+
+
+ );
+};
+
+export default Notification;
diff --git a/app-frontend/react/src/components/PrimaryInput/PrimaryInput.module.scss b/app-frontend/react/src/components/PrimaryInput/PrimaryInput.module.scss
new file mode 100644
index 0000000..184c4d7
--- /dev/null
+++ b/app-frontend/react/src/components/PrimaryInput/PrimaryInput.module.scss
@@ -0,0 +1,44 @@
+.inputWrapper {
+ position: relative;
+}
+
+.primaryInput {
+ border-radius: var(--input-radius);
+ overflow: hidden;
+ position: relative;
+ display: flex;
+
+ .inputActions {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ position: absolute;
+ right: 10px;
+ bottom: 10px;
+ }
+
+ .circleButton {
+ border-radius: 40px;
+ width: 40px;
+ height: 40px;
+ min-width: 40px;
+ margin-left: 10px;
+ }
+
+ .textAreaAuto {
+ font-family: "Inter", serif;
+ padding: var(--header-gutter) 100px var(--header-gutter) var(--header-gutter);
+ border: 0;
+ width: 100%;
+ resize: none;
+ background-color: transparent;
+
+ &:focus {
+ outline: none;
+ }
+
+ &.summaryInput {
+ padding: var(--header-gutter) 70px var(--header-gutter) var(--header-gutter);
+ }
+ }
+}
diff --git a/app-frontend/react/src/components/PrimaryInput/PrimaryInput.tsx b/app-frontend/react/src/components/PrimaryInput/PrimaryInput.tsx
new file mode 100644
index 0000000..4f7e5ef
--- /dev/null
+++ b/app-frontend/react/src/components/PrimaryInput/PrimaryInput.tsx
@@ -0,0 +1,200 @@
+import { useEffect, useRef, useState } from "react";
+import { Box, Button, styled, TextareaAutosize } from "@mui/material";
+import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
+import styles from "./PrimaryInput.module.scss";
+import { Stop } from "@mui/icons-material";
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import {
+ abortStream,
+ conversationSelector,
+ // saveConversationtoDatabase,
+ setSourceFiles,
+ setSourceLinks,
+} from "@redux/Conversation/ConversationSlice";
+import AudioInput from "@components/PrimaryInput_AudioInput/AudioInput";
+import PromptSelector from "@components/PrimparyInput_PromptSelector/PromptSelector";
+import {
+ NotificationSeverity,
+ notify,
+} from "@components/Notification/Notification";
+
+const InputWrapper = styled(Box)(({ theme }) => ({
+ ...theme.customStyles.primaryInput.inputWrapper,
+}));
+
+const TextInput = styled(TextareaAutosize)(({ theme }) => ({
+ ...theme.customStyles.primaryInput.textInput,
+}));
+
+const CircleButton = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.primaryInput.circleButton,
+}));
+
+interface PrimaryInputProps {
+ onSend: (messageContent: string) => Promise;
+ type?: string;
+ home?: boolean;
+}
+
+const PrimaryInput: React.FC = ({
+ onSend,
+ home = false,
+}) => {
+ const {
+ onGoingResult,
+ type,
+ selectedConversationId,
+ sourceLinks,
+ sourceFiles,
+ } = useAppSelector(conversationSelector);
+ const dispatch = useAppDispatch();
+
+ const [promptText, setPromptText] = useState("");
+ const clearText = useRef(true);
+
+ const isSummary = type === "summary";
+ const isFaq = type === "faq";
+
+ useEffect(() => {
+ if (clearText.current) setPromptText("");
+ clearText.current = true;
+ }, [type, sourceFiles, sourceLinks]);
+
+ const handleSubmit = () => {
+ if (
+ (isSummary || isFaq) &&
+ sourceLinks &&
+ sourceLinks.length === 0 &&
+ sourceFiles &&
+ sourceFiles.length === 0 &&
+ promptText === ""
+ ) {
+ notify("Please provide content process", NotificationSeverity.ERROR);
+ return;
+ } else if (!(isSummary || isFaq) && promptText === "") {
+ notify("Please provide a message", NotificationSeverity.ERROR);
+ return;
+ }
+
+ let textToSend = promptText;
+ onSend(textToSend);
+ setPromptText("");
+ };
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (!event.shiftKey && event.key === "Enter") {
+ handleSubmit();
+ }
+ };
+
+ const updatePromptText = (value: string) => {
+ setPromptText(value);
+ if (sourceFiles.length > 0) {
+ clearText.current = false;
+ dispatch(setSourceFiles([]));
+ }
+ if (sourceLinks.length > 0) {
+ clearText.current = false;
+ dispatch(setSourceLinks([]));
+ }
+ };
+
+ // const cancelStream = () => {
+ // dispatch(abortStream());
+ // if (type === "chat") {
+ // dispatch(
+ // saveConversationtoDatabase({
+ // conversation: { id: selectedConversationId },
+ // }),
+ // );
+ // }
+ // };
+
+ const isActive = () => {
+ if ((isSummary || isFaq) && sourceFiles.length > 0) {
+ return true;
+ } else if (promptText !== "") return true;
+ return false;
+ };
+
+ const submitButton = () => {
+ if (!onGoingResult) {
+ return (
+
+
+
+ );
+ }
+ return;
+ };
+
+ const placeHolderCopy = () => {
+ if (home && (isSummary || isFaq)) return "Enter text here or sources below";
+ else return "Enter your message";
+ };
+
+ const renderInput = () => {
+ if (!home && onGoingResult && (isSummary || isFaq)) {
+ return (
+
+
+
+
+
+ );
+ } else if ((!home && !isSummary && !isFaq) || home) {
+ return (
+
+
+ ) =>
+ updatePromptText(e.target.value)
+ }
+ onKeyDown={handleKeyDown}
+ sx={{
+ resize: "none",
+ backgroundColor: "transparent",
+ }}
+ />
+
+
+ {/* */}
+
+ {onGoingResult && (
+
+
+
+ )}
+
+ {submitButton()}
+
+
+
+ {/* {home && !isSummary && !isFaq && (
+
+ )} */}
+
+ );
+ }
+ };
+
+ return renderInput();
+};
+
+export default PrimaryInput;
diff --git a/app-frontend/react/src/components/PrimaryInput_AudioInput/AudioInput.tsx b/app-frontend/react/src/components/PrimaryInput_AudioInput/AudioInput.tsx
new file mode 100644
index 0000000..d304ebd
--- /dev/null
+++ b/app-frontend/react/src/components/PrimaryInput_AudioInput/AudioInput.tsx
@@ -0,0 +1,85 @@
+import { Mic } from "@mui/icons-material";
+import { Button, styled, Tooltip } from "@mui/material";
+import { useState } from "react";
+import styles from "@components/PrimaryInput/PrimaryInput.module.scss";
+import {
+ NotificationSeverity,
+ notify,
+} from "@components/Notification/Notification";
+import { useAppSelector } from "@redux/store";
+import { conversationSelector } from "@redux/Conversation/ConversationSlice";
+import ProgressIcon from "@components/ProgressIcon/ProgressIcon";
+
+interface AudioInputProps {
+ setSearchText: (value: string) => void;
+}
+
+const AudioButton = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.audioEditButton,
+}));
+
+const AudioInput: React.FC = ({ setSearchText }) => {
+ const isSpeechRecognitionSupported =
+ ("webkitSpeechRecognition" in window || "SpeechRecognition" in window) &&
+ window.isSecureContext;
+
+ const { type } = useAppSelector(conversationSelector);
+ const [isListening, setIsListening] = useState(false);
+
+ const handleMicClick = () => {
+ const SpeechRecognition =
+ (window as any).SpeechRecognition ||
+ (window as any).webkitSpeechRecognition;
+ const recognition = new SpeechRecognition();
+ recognition.lang = "en-US"; // Set language for recognition
+ recognition.interimResults = false; // Only process final results
+
+ if (!isListening) {
+ setIsListening(true);
+ recognition.start();
+
+ recognition.onresult = (event: SpeechRecognitionEvent) => {
+ const transcript = event.results[0][0].transcript;
+ setSearchText(transcript); // Update search text with recognized speech
+ setIsListening(false);
+ };
+
+ recognition.onerror = (event: SpeechRecognitionErrorEvent) => {
+ notify(
+ `Speech recognition error:${event.error}`,
+ NotificationSeverity.ERROR,
+ );
+ console.error("Speech recognition error:", event);
+ setIsListening(false);
+ };
+
+ recognition.onend = () => {
+ setIsListening(false);
+ };
+ } else {
+ recognition.stop();
+ setIsListening(false);
+ }
+ };
+
+ const renderMic = () => {
+ if (type === "summary" || type === "faq" || !isSpeechRecognitionSupported)
+ return <>>;
+
+ if (isListening) {
+ return ;
+ } else {
+ return (
+
+
+
+
+
+ );
+ }
+ };
+
+ return renderMic();
+};
+
+export default AudioInput;
diff --git a/app-frontend/react/src/components/PrimparyInput_PromptSelector/PromptSelector.module.scss b/app-frontend/react/src/components/PrimparyInput_PromptSelector/PromptSelector.module.scss
new file mode 100644
index 0000000..e90edb4
--- /dev/null
+++ b/app-frontend/react/src/components/PrimparyInput_PromptSelector/PromptSelector.module.scss
@@ -0,0 +1,87 @@
+.promptsWrapper {
+ position: absolute;
+
+ z-index: 99;
+ width: 100%;
+ padding: 0 40px;
+
+ .expand {
+ width: 25px;
+ height: 25px;
+ border-radius: 25px;
+ min-width: unset;
+ border-width: 1px;
+ border-style: solid;
+ transform: rotate(180deg);
+ transition: transform 0.5s;
+ margin-top: -20px;
+ position: relative;
+ z-index: 9999;
+
+ &.open {
+ transform: rotate(0deg);
+ }
+ }
+}
+
+.promptText {
+ color: var(--copy-color) !important;
+}
+
+.promptsListWrapper {
+ margin-top: -23px;
+ max-height: 0px;
+ transition: max-height 0.5s;
+ overflow: hidden;
+ // background: #fff;
+ width: 100%;
+ z-index: 999;
+
+ &.open {
+ max-height: 250px;
+ overflow-y: auto;
+ }
+
+ ul {
+ padding: 0;
+ }
+
+ li {
+ border-bottom: 1px solid;
+ padding: 0;
+ justify-content: space-between;
+
+ button {
+ padding: 1rem;
+ width: 100%;
+ justify-content: flex-start;
+ text-align: left;
+ border-radius: 0px;
+ box-shadow: none;
+ }
+
+ &:first-of-type button {
+ padding-top: 1.2rem;
+
+ span {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 3; // Limits to 2 lines
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+
+ .delete {
+ width: 40px;
+ height: 40px;
+ border-radius: 40px;
+ margin: 0 0.5rem;
+ justify-content: center;
+
+ path {
+ fill: #cc0000;
+ }
+ }
+ }
+}
diff --git a/app-frontend/react/src/components/PrimparyInput_PromptSelector/PromptSelector.tsx b/app-frontend/react/src/components/PrimparyInput_PromptSelector/PromptSelector.tsx
new file mode 100644
index 0000000..00d58c5
--- /dev/null
+++ b/app-frontend/react/src/components/PrimparyInput_PromptSelector/PromptSelector.tsx
@@ -0,0 +1,113 @@
+import {
+ Box,
+ Button,
+ IconButton,
+ List,
+ ListItem,
+ styled,
+ Tooltip,
+} from "@mui/material";
+import { deletePrompt, promptSelector } from "@redux/Prompt/PromptSlice";
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import { useEffect, useRef, useState } from "react";
+import { Delete, ExpandMore } from "@mui/icons-material";
+import styles from "./PromptSelector.module.scss";
+
+const ExpandButton = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.promptExpandButton,
+}));
+
+const PromptButton = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.promptButton,
+}));
+
+const PromptListWrapper = styled(Box)(({ theme }) => ({
+ ...theme.customStyles.promptListWrapper,
+}));
+
+interface PromptSelectorProps {
+ setSearchText: (value: string) => void;
+}
+
+const PromptSelector: React.FC = ({ setSearchText }) => {
+ const dispatch = useAppDispatch();
+ const { prompts } = useAppSelector(promptSelector);
+ const [showPrompts, setShowPrompts] = useState(false);
+ const wrapperRef = useRef(null);
+
+ useEffect(() => {
+ if (!showPrompts) return;
+
+ const handleClickOutside = (event: MouseEvent) => {
+ if (
+ wrapperRef.current &&
+ !wrapperRef.current.contains(event.target as Node)
+ ) {
+ setShowPrompts(false);
+ }
+ };
+
+ document.addEventListener("mousedown", handleClickOutside);
+
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ };
+ }, [showPrompts]);
+
+ const handleDelete = (id: string, text: string) => {
+ dispatch(deletePrompt({ promptId: id, promptText: text }));
+ };
+
+ const handleSelect = (promptText: string) => {
+ setSearchText(promptText);
+ setShowPrompts(false);
+ };
+
+ return (
+ prompts &&
+ prompts.length > 0 && (
+
+
+ setShowPrompts(!showPrompts)}
+ >
+
+
+
+
+
+
+ {prompts?.map((prompt, promptIndex) => {
+ return (
+
+ handleSelect(prompt.prompt_text)}
+ >
+ {prompt.prompt_text}
+
+
+
+
+ handleDelete(prompt.id, prompt.prompt_text)
+ }
+ >
+
+
+
+
+ );
+ })}
+
+
+
+ )
+ );
+};
+
+export default PromptSelector;
diff --git a/app-frontend/react/src/components/ProgressIcon/ProgressIcon.tsx b/app-frontend/react/src/components/ProgressIcon/ProgressIcon.tsx
new file mode 100644
index 0000000..aa8d3ec
--- /dev/null
+++ b/app-frontend/react/src/components/ProgressIcon/ProgressIcon.tsx
@@ -0,0 +1,13 @@
+import { CircularProgress, styled } from "@mui/material";
+
+const ProgressIconStyle = styled(CircularProgress)(({ theme }) => ({
+ "svg circle": {
+ stroke: theme.customStyles.audioProgress?.stroke,
+ },
+}));
+
+const ProgressIcon = () => {
+ return ;
+};
+
+export default ProgressIcon;
diff --git a/app-frontend/react/src/components/PromptSettings/PromptSettings.module.scss b/app-frontend/react/src/components/PromptSettings/PromptSettings.module.scss
new file mode 100644
index 0000000..6b64e28
--- /dev/null
+++ b/app-frontend/react/src/components/PromptSettings/PromptSettings.module.scss
@@ -0,0 +1,89 @@
+.promptSettingsWrapper {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ max-width: 775px;
+
+ .summarySource {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ :global {
+ .MuiFormGroup-root {
+ margin-bottom: 0.75rem;
+
+ label {
+ margin-left: 0;
+ }
+
+ &:not(:last-of-type) {
+ margin-right: 0.75rem;
+ }
+ }
+ }
+}
+
+.promptSettings {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ margin-top: calc(var(--vertical-spacer) / 2);
+ flex-wrap: wrap;
+ width: 100%;
+
+ @media screen and (max-width: 900px) {
+ padding: 0 var(--content-gutter);
+ }
+
+ :global {
+ .MuiFormControlLabel-label,
+ .MuiTypography-root {
+ font-size: 13px;
+ font-weight: 400;
+ }
+ }
+
+ &.readOnly {
+ flex-direction: column;
+ align-items: flex-start;
+ padding: 0;
+ margin-top: 0;
+
+ :global {
+ .MuiFormGroup-root {
+ margin-bottom: 0;
+ margin-right: 0;
+
+ label {
+ width: 100%;
+ align-items: flex-start;
+ margin: 0;
+ }
+ }
+
+ @media screen and (max-width: 900px) {
+ .MuiFormGroup-root:not(:last-of-type) {
+ margin-bottom: 0;
+ }
+ }
+ }
+ }
+}
+
+.systemPromptTextarea {
+ padding: 8px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ font-size: 13px; /* Matches .MuiFormControlLabel-label font-size */
+ font-family: inherit;
+ background-color: #fff;
+ &:disabled {
+ background-color: #f5f5f5;
+ cursor: not-allowed;
+ }
+}
diff --git a/app-frontend/react/src/components/PromptSettings/PromptSettings.tsx b/app-frontend/react/src/components/PromptSettings/PromptSettings.tsx
new file mode 100644
index 0000000..50d4a2d
--- /dev/null
+++ b/app-frontend/react/src/components/PromptSettings/PromptSettings.tsx
@@ -0,0 +1,268 @@
+import { useEffect, useState } from "react";
+
+// import DropDown from "@components/DropDown/DropDown";
+import CustomSlider from "@components/PromptSettings_Slider/Slider";
+import { Box, Collapse, FormGroup, FormControlLabel, FormLabel, IconButton, TextareaAutosize } from "@mui/material";
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import ExpandLessIcon from '@mui/icons-material/ExpandLess';
+import styles from "./PromptSettings.module.scss";
+import TokensInput from "@components/PromptSettings_Tokens/TokensInput";
+import FileInput from "@components/File_Input/FileInput";
+// import WebInput from "@components/Summary_WebInput/WebInput";
+
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import { Model } from "@redux/Conversation/Conversation";
+import {
+ conversationSelector,
+ setModel,
+ setSourceType,
+ setTemperature,
+ setToken,
+ setSystemPrompt,
+} from "@redux/Conversation/ConversationSlice";
+
+interface AvailableModel {
+ name: string;
+ value: string;
+}
+
+interface PromptSettingsProps {
+ readOnly?: boolean;
+}
+
+const PromptSettings: React.FC = ({
+ readOnly = false,
+}) => {
+ const dispatch = useAppDispatch();
+
+ const { models, type, sourceType, model, token, maxToken, temperature, systemPrompt } =
+ useAppSelector(conversationSelector);
+
+ const [tokenError, setTokenError] = useState(false);
+ const [isSystemPromptOpen, setIsSystemPromptOpen] = useState(false);
+
+ const filterAvailableModels = (): AvailableModel[] => {
+ if (!models || !type) return [];
+
+ let typeModels: AvailableModel[] = [];
+
+ models.map((model: Model) => {
+ if (model.types.includes(type)) {
+ typeModels.push({
+ name: model.displayName,
+ value: model.model_name,
+ });
+ }
+ });
+
+ return typeModels;
+ };
+
+ const [formattedModels, setFormattedModels] = useState(
+ filterAvailableModels(),
+ );
+
+ useEffect(() => {
+ setFormattedModels(filterAvailableModels());
+ }, [type, models]);
+
+ useEffect(() => {
+ if (!model) return;
+ setTokenError(token > maxToken);
+ }, [model, token]);
+
+ useEffect(() => {
+ // console.log("System Prompt Opened: ", isSystemPromptOpen);
+ }, [isSystemPromptOpen]);
+
+ const updateTemperature = (value: number) => {
+ dispatch(setTemperature(Number(value)));
+ };
+
+ const updateTokens = (value: number) => {
+ dispatch(setToken(Number(value)));
+ };
+
+ const updateSystemPrompt = (value: string) => {
+ dispatch(setSystemPrompt(value));
+ };
+
+ // const updateModel = (value: string) => {
+ // const selectedModel = models.find(
+ // (model: Model) => model.model_name === value,
+ // );
+ // if (selectedModel) {
+ // dispatch(setModel(selectedModel));
+ // }
+ // };
+
+ const updateSource = (value: string) => {
+ dispatch(setSourceType(value));
+ };
+
+ const cursorDisable = () => {
+ return readOnly ? { pointerEvents: "none" } : {};
+ };
+
+ const displaySummarySource = () => {
+ if ((type !== "summary" && type !== "faq") || readOnly) return;
+
+ let input = null;
+ // if (sourceType === "documents") input = ;
+ // if (sourceType === "web") input = ;
+ // if (sourceType === "images" && type === "summary")
+ // input = ;
+ input = ;
+
+ return
+ {input}
+
;
+ };
+
+ // in the off chance specific types do not use these,
+ // they have been pulled into their own function
+ const tokenAndTemp = () => {
+ return (
+ <>
+
+ setIsSystemPromptOpen(!isSystemPromptOpen)}
+ sx={{ padding: '0.5rem' }}
+ disabled={readOnly}
+ >
+ {isSystemPromptOpen ? : }
+
+
+ Inference Settings
+
+
+
+
+
+
+
+
+ }
+ label={`Tokens${readOnly ? ": " : ""}`}
+ labelPlacement="start"
+ />
+
+
+
+
+ }
+ label={`Temperature${readOnly ? ": " : ""}`}
+ labelPlacement="start"
+ />
+
+
+ {
+ type === "chat" &&
+
+ updateSystemPrompt(e.target.value)}
+ disabled={readOnly}
+ className={styles.systemPromptTextarea}
+ placeholder="Enter system prompt here..."
+ style={{ width: '100%', resize: 'vertical' }}
+ />
+ }
+ label="System Prompt"
+ labelPlacement="start"
+ />
+
+ }
+
+ >
+ );
+ };
+
+ // const displaySettings = () => {
+ // if (type === "summary" || type === "faq") {
+ // //TODO: Supporting only documents to start
+ // return (
+ // <>
+ //
+ //
+ // }
+ // label="Summary Source"
+ // labelPlacement="start"
+ // />
+ //
+ // >
+ // );
+ // } else {
+ // return <>>; // tokenAndTemp() // see line 113, conditional option
+ // }
+ // };
+
+ return (
+
+
+ {/* {formattedModels && formattedModels.length > 0 && (
+
+
+ }
+ label={`Model${readOnly ? ": " : ""}`}
+ labelPlacement="start"
+ />
+
+ )} */}
+
+ {tokenAndTemp()}
+
+
+ {/* TODO: Expand source options and show label with dropdown after expansion */}
+ {/* {displaySettings()} */}
+
+ {displaySummarySource()}
+
+ );
+};
+
+export default PromptSettings;
diff --git a/app-frontend/react/src/components/PromptSettings_Slider/Slider.module.scss b/app-frontend/react/src/components/PromptSettings_Slider/Slider.module.scss
new file mode 100644
index 0000000..bdb1245
--- /dev/null
+++ b/app-frontend/react/src/components/PromptSettings_Slider/Slider.module.scss
@@ -0,0 +1,88 @@
+.sliderWrapper {
+ flex-direction: row;
+ align-items: center;
+ width: 100%;
+ max-width: 320px;
+
+ flex-wrap: nowrap !important;
+
+ font-size: 13px;
+ font-weight: 400;
+
+ .start {
+ margin-left: 0.5rem;
+ }
+
+ .trackWrapper {
+ margin: 0 0.5rem;
+ width: 100px;
+ display: flex;
+ }
+
+ .styledSlider {
+ height: 2px;
+ width: 100%;
+ padding: 16px 0;
+ display: inline-flex;
+ align-items: center;
+ position: relative;
+ cursor: pointer;
+ touch-action: none;
+ -webkit-tap-highlight-color: transparent;
+ flex-wrap: nowrap;
+
+ &.disabled {
+ pointer-events: none;
+ cursor: default;
+ opacity: 0.4;
+ }
+
+ :global {
+ .MuiSlider-rail {
+ display: block;
+ position: absolute;
+ width: 100%;
+ height: 2px;
+ border-radius: 2px;
+ opacity: 0.3;
+ }
+
+ .MuiSlider-track {
+ display: block;
+ position: absolute;
+ height: 0px;
+ }
+
+ .MuiSlider-thumb {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ margin-left: -2px;
+ width: 10px;
+ height: 10px;
+ box-sizing: border-box;
+ border-radius: 50%;
+ outline: 0;
+ transition-property: box-shadow, transform;
+ transition-timing-function: ease;
+ transition-duration: 120ms;
+ transform-origin: center;
+
+ &:hover {
+ }
+
+ &.focusVisible {
+ outline: none;
+ }
+
+ &.active {
+ outline: none;
+ }
+
+ &.disabled {
+ }
+ }
+ }
+ }
+}
diff --git a/app-frontend/react/src/components/PromptSettings_Slider/Slider.tsx b/app-frontend/react/src/components/PromptSettings_Slider/Slider.tsx
new file mode 100644
index 0000000..93bbd24
--- /dev/null
+++ b/app-frontend/react/src/components/PromptSettings_Slider/Slider.tsx
@@ -0,0 +1,49 @@
+import * as React from "react";
+import { styled } from "@mui/material/styles";
+import { Slider, Grid2, Typography } from "@mui/material";
+import styles from "./Slider.module.scss";
+
+const StyledSlider = styled(Slider)(({ theme }) => ({
+ ...theme.customStyles.styledSlider,
+}));
+
+interface CustomSliderProps {
+ value: number;
+ handleChange: (value: number) => void;
+ readOnly?: boolean;
+}
+
+const CustomSlider: React.FC = ({
+ value,
+ handleChange,
+ readOnly,
+}) => {
+ if (readOnly) {
+ return {value} ;
+ }
+
+ const handleSlideUpdate = (event: Event, value: number) => {
+ handleChange(value);
+ };
+
+ return (
+
+ 0
+
+
+
+ 1
+
+ );
+};
+
+export default CustomSlider;
diff --git a/app-frontend/react/src/components/PromptSettings_Tokens/TokensInput.module.scss b/app-frontend/react/src/components/PromptSettings_Tokens/TokensInput.module.scss
new file mode 100644
index 0000000..4d0f13d
--- /dev/null
+++ b/app-frontend/react/src/components/PromptSettings_Tokens/TokensInput.module.scss
@@ -0,0 +1,49 @@
+.numberInput {
+ font-weight: 400;
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: center;
+ align-items: center;
+
+ input {
+ font-size: 13px;
+ font-family: inherit;
+ font-weight: 400;
+ line-height: 1.375;
+ border-radius: 8px;
+ margin: 0 8px 0 0;
+ padding: 3px 5px;
+ outline: 0;
+ min-width: 0;
+ width: 3.5rem;
+ text-align: center;
+ background: transparent;
+
+ &:hover {
+ }
+
+ &:focus {
+ }
+
+ &:focus-visible {
+ outline: 0;
+ }
+
+ /* Chrome, Safari, Edge, Opera */
+ &::-webkit-outer-spin-button,
+ &::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+
+ /* Firefox */
+ &[type="number"] {
+ -moz-appearance: textfield;
+ }
+ }
+
+ .error,
+ .error:focus {
+ border: 1px solid #cc0000;
+ }
+}
diff --git a/app-frontend/react/src/components/PromptSettings_Tokens/TokensInput.tsx b/app-frontend/react/src/components/PromptSettings_Tokens/TokensInput.tsx
new file mode 100644
index 0000000..6b4bd21
--- /dev/null
+++ b/app-frontend/react/src/components/PromptSettings_Tokens/TokensInput.tsx
@@ -0,0 +1,46 @@
+import * as React from "react";
+import { styled } from "@mui/material/styles";
+
+import { Typography } from "@mui/material";
+import styles from "./TokensInput.module.scss";
+
+interface NumberInputProps {
+ value?: number;
+ handleChange: (value: number) => void;
+ error: boolean;
+ readOnly?: boolean;
+}
+
+const StyledInput = styled("input")(({ theme }) => ({
+ ...theme.customStyles.tokensInput,
+}));
+
+const TokensInput: React.FC = ({
+ value = 1,
+ handleChange,
+ error,
+ readOnly,
+}) => {
+ if (readOnly) {
+ return {value} ;
+ }
+
+ return (
+
+ ) =>
+ handleChange(parseInt(e.target.value, 10))
+ }
+ aria-label="Quantity Input"
+ />
+
+ );
+};
+
+export default TokensInput;
diff --git a/app-frontend/react/src/components/SearchInput/SearchInput.module.scss b/app-frontend/react/src/components/SearchInput/SearchInput.module.scss
new file mode 100644
index 0000000..33207e8
--- /dev/null
+++ b/app-frontend/react/src/components/SearchInput/SearchInput.module.scss
@@ -0,0 +1,17 @@
+.searchInput {
+ width: 100%;
+ margin-bottom: 1rem;
+ border-radius: var(--input-radius);
+ border: 0;
+ margin-bottom: 2rem;
+
+ &:focus {
+ outline: none;
+ }
+
+ :global {
+ .MuiInputBase-root {
+ border-radius: var(--input-radius);
+ }
+ }
+}
diff --git a/app-frontend/react/src/components/SearchInput/SearchInput.tsx b/app-frontend/react/src/components/SearchInput/SearchInput.tsx
new file mode 100644
index 0000000..49a11f6
--- /dev/null
+++ b/app-frontend/react/src/components/SearchInput/SearchInput.tsx
@@ -0,0 +1,63 @@
+import { InputAdornment, styled, TextField } from "@mui/material";
+import styles from "./SearchInput.module.scss";
+import { Close, Search } from "@mui/icons-material";
+import { useRef, useState } from "react";
+
+const StyledSearchInput = styled(TextField)(({ theme }) => ({
+ ...theme.customStyles.webInput,
+}));
+
+interface SearchInputProps {
+ handleSearch: (value: string) => void;
+}
+
+const SearchInput: React.FC = ({ handleSearch }) => {
+ const [hasValue, setHasValue] = useState(false);
+
+ const inputRef = useRef(null);
+
+ const clearSearch = () => {
+ if (inputRef.current) {
+ const input = inputRef.current.querySelector("input");
+ if (input) input.value = "";
+ }
+ handleSearch("");
+ setHasValue(false);
+ };
+
+ const search = (value: string) => {
+ handleSearch(value);
+ setHasValue(value !== "");
+ };
+
+ return (
+ ) =>
+ search(e.target.value)
+ }
+ InputProps={{
+ startAdornment: (
+
+
+
+ ),
+ endAdornment: hasValue && (
+
+
+
+ ),
+ }}
+ fullWidth
+ />
+ );
+};
+
+export default SearchInput;
diff --git a/app-frontend/react/src/components/SideBar/SideBar.module.scss b/app-frontend/react/src/components/SideBar/SideBar.module.scss
new file mode 100644
index 0000000..67b98b7
--- /dev/null
+++ b/app-frontend/react/src/components/SideBar/SideBar.module.scss
@@ -0,0 +1,117 @@
+.aside {
+ max-width: var(--sidebar-width);
+ position: fixed;
+ width: 0;
+ overflow: hidden;
+ transition: width 0.3s;
+ height: 100vh;
+ top: 0;
+ left: 0;
+ z-index: 998;
+ padding-top: var(--header-height);
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+
+ &.open {
+ max-width: var(--sidebar-width);
+ width: var(--sidebar-width);
+ }
+
+ .asideContent {
+ width: var(--sidebar-width);
+ min-width: var(--sidebar-width);
+ display: flex;
+ flex-direction: column;
+ list-style: none;
+ overflow: auto;
+ max-height: 100%;
+ margin-bottom: auto;
+
+ .emptySvg {
+ width: 24px;
+ height: 24px;
+ min-width: 24px;
+ }
+
+ li,
+ a {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ }
+
+ li {
+ padding-left: var(--header-gutter);
+ padding-right: var(--header-gutter);
+ }
+
+ a {
+ width: 100%;
+ max-width: 100%;
+ text-decoration: none;
+ }
+
+ :global {
+ .MuiListItemText-root {
+ margin-left: 10px;
+ }
+
+ .MuiTypography-root {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+
+ .viewAll span {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: flex-start;
+
+ svg {
+ margin-left: 0.5rem;
+ transform: rotate(180deg);
+ }
+ }
+
+ .divider {
+ height: 0;
+ margin: 10px var(--header-gutter);
+ }
+ }
+}
+
+.asideSpacer {
+ width: 0;
+ transition: width 0.3s;
+
+ &.asideSpacerOpen {
+ width: var(--sidebar-width);
+ }
+}
+
+@media screen and (max-width: 1200px) {
+ .asideSpacer {
+ display: none;
+ }
+}
+
+.mobileUser {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ width: var(--sidebar-width);
+ min-width: var(--sidebar-width);
+ padding: var(--header-gutter);
+
+ :global {
+ .themeToggle {
+ margin-left: -15px;
+ margin-bottom: 1rem;
+ }
+ }
+ @media screen and (min-width: 900px) {
+ display: none;
+ }
+}
diff --git a/app-frontend/react/src/components/SideBar/SideBar.tsx b/app-frontend/react/src/components/SideBar/SideBar.tsx
new file mode 100644
index 0000000..b1405cd
--- /dev/null
+++ b/app-frontend/react/src/components/SideBar/SideBar.tsx
@@ -0,0 +1,229 @@
+import React from "react";
+import { Link } from "react-router-dom";
+import { useTheme } from "@mui/material/styles";
+import { SvgIconProps } from "@mui/material/SvgIcon";
+import styles from "./SideBar.module.scss";
+// import LogoutIcon from "@mui/icons-material/Logout";
+// import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
+// import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings";
+import DatabaseIcon from "@icons/Database";
+import RecentIcon from "@icons/Recent";
+// import PeopleOutlineOutlinedIcon from "@mui/icons-material/PeopleOutlineOutlined";
+// import FileUploadOutlinedIcon from "@mui/icons-material/FileUploadOutlined";
+import { Box, ListItemText, MenuItem, MenuList } from "@mui/material";
+
+import { JSX, MouseEventHandler } from "react";
+import ThemeToggle from "@components/Header_ThemeToggle/ThemeToggle";
+
+import { useAppSelector } from "@redux/store";
+import { userSelector } from "@redux/User/userSlice";
+import {
+ conversationSelector,
+ newConversation,
+} from "@redux/Conversation/ConversationSlice";
+import { Conversation } from "@redux/Conversation/Conversation";
+// import { useKeycloak } from "@react-keycloak/web";
+// import UploadChat from "@components/SideBar_UploadChat/UploadChat";
+import { KeyboardBackspace } from "@mui/icons-material";
+import { useDispatch } from "react-redux";
+import { useNavigateWithQuery, useToWithQuery } from "@utils/navigationAndAxiosWithQuery";
+
+interface SideBarProps {
+ asideOpen: boolean;
+ setAsideOpen?: (open: boolean) => void;
+ userDetails?: () => JSX.Element;
+}
+
+interface NavIconProps {
+ component: React.ComponentType;
+}
+
+export const NavIcon: React.FC = ({
+ component: ListItemIcon,
+}) => {
+ const theme = useTheme();
+ return ;
+};
+
+const EmptySvg: React.FC = () => {
+ return (
+
+ );
+};
+
+interface LinkedMenuItemProps {
+ to: string;
+ children: React.ReactNode;
+ onClick?: MouseEventHandler;
+ sx?: any;
+ open?: boolean;
+}
+
+export const LinkedMenuItem: React.FC = ({
+ to,
+ children,
+ onClick,
+ sx,
+ open,
+}) => {
+ const toWithQuery = useToWithQuery();
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+const SideBar: React.FC = ({
+ asideOpen,
+ setAsideOpen = () => {},
+ userDetails,
+}) => {
+ const dispatch = useDispatch();
+ const theme = useTheme();
+
+ // const { keycloak } = useKeycloak();
+ const { role } = useAppSelector(userSelector);
+ const { conversations } = useAppSelector(conversationSelector);
+
+ const asideBackgroundColor = {
+ backgroundColor: theme.customStyles.aside?.main,
+ };
+
+ const dividerColor = {
+ borderBottom: `1px solid ${theme.customStyles.customDivider?.main}`,
+ };
+
+ const handleLinkedMenuItemClick = (
+ event: React.MouseEvent,
+ ) => {
+ event.currentTarget.blur(); // so we can apply the aria hidden attribute while menu closed
+ dispatch(newConversation(true));
+ setAsideOpen(false);
+ };
+
+ const history = (type: Conversation[]) => {
+ if (type && type.length > 0) {
+ return type.map((conversation: Conversation, index: number) => {
+ if (index > 2) return null;
+ return (
+
+
+ {conversation.first_query}
+
+ );
+ });
+ }
+ };
+
+ // const handleLogout = () => {
+ // // keycloak.logout();
+ // setAsideOpen(false);
+ // };
+
+ const viewAll = (path: string) => {
+ if (conversations.length > 0) {
+ return (
+
+
+
+ View All
+
+
+ );
+ } else {
+ return (
+
+
+ No recent conversations
+
+ );
+ }
+ };
+
+ return (
+
+
+ {/* */}
+
+
+
+ Recents
+
+
+ {history(conversations)}
+
+ {viewAll("/history")}
+
+ {/*
+
+ Shared
+ */}
+
+ {/* {history(allSharedConversations)} */}
+
+ {/* {viewAll('/shared')} */}
+
+
+
+ {role === "Admin" && (
+ <>
+
+
+ Data Management
+
+ >
+ )}
+
+ {/*
+
+ Log Out
+ */}
+
+
+
+
+ {userDetails && userDetails()}
+
+
+ );
+};
+
+const SideBarSpacer: React.FC = ({ asideOpen }) => {
+ return (
+
+ );
+};
+
+export { SideBar, SideBarSpacer };
diff --git a/app-frontend/react/src/components/SideBar_UploadChat/UploadChat.tsx b/app-frontend/react/src/components/SideBar_UploadChat/UploadChat.tsx
new file mode 100644
index 0000000..092f69f
--- /dev/null
+++ b/app-frontend/react/src/components/SideBar_UploadChat/UploadChat.tsx
@@ -0,0 +1,141 @@
+import {
+ NotificationSeverity,
+ notify,
+} from "@components/Notification/Notification";
+import { LinkedMenuItem, NavIcon } from "@components/SideBar/SideBar";
+import { FileUploadOutlined } from "@mui/icons-material";
+import { ListItemText } from "@mui/material";
+import {
+ conversationSelector,
+ getAllConversations,
+ saveConversationtoDatabase,
+ uploadChat,
+} from "@redux/Conversation/ConversationSlice";
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import { userSelector } from "@redux/User/userSlice";
+import { useEffect, useRef } from "react";
+import { useNavigateWithQuery } from "@utils/navigationAndAxiosWithQuery";
+
+interface UploadChatProps {
+ asideOpen: boolean;
+ setAsideOpen: (open: boolean) => void;
+}
+
+const UploadChat: React.FC = ({ asideOpen, setAsideOpen }) => {
+ const dispatch = useAppDispatch();
+ const { selectedConversationHistory } = useAppSelector(conversationSelector);
+
+ const navigateWithQuery = useNavigateWithQuery();
+
+ const fileInputRef = useRef(null);
+ const newUpload = useRef(false);
+
+ useEffect(() => {
+ if (newUpload.current && selectedConversationHistory) {
+ saveConversation();
+ }
+ }, [selectedConversationHistory]);
+
+ const handleUploadClick = (event: React.MouseEvent) => {
+ event.preventDefault();
+ event.currentTarget.blur(); // so we can apply the aria hidden attribute while menu closed
+ if (fileInputRef.current) {
+ fileInputRef.current.click();
+ }
+ };
+
+ const saveConversation = async () => {
+ try {
+ const resultAction = await dispatch(
+ saveConversationtoDatabase({ conversation: { id: "" } }),
+ );
+
+ if (saveConversationtoDatabase.fulfilled.match(resultAction)) {
+ const responseData = resultAction.payload;
+ setAsideOpen(false);
+ newUpload.current = false;
+ notify(
+ "Conversation successfully uploaded",
+ NotificationSeverity.SUCCESS,
+ );
+ navigateWithQuery(`/chat/${responseData}`);
+ } else {
+ newUpload.current = false;
+ notify("Error saving conversation", NotificationSeverity.ERROR);
+ console.error("Error saving conversation:", resultAction.error);
+ }
+ } catch (error) {
+ newUpload.current = false;
+ notify("Error saving conversation", NotificationSeverity.ERROR);
+ console.error("An unexpected error occurred:", error);
+ }
+ };
+
+ const handleFileChange = (event: React.ChangeEvent) => {
+ const file = event.target.files?.[0];
+
+ if (file) {
+ newUpload.current = true;
+ const reader = new FileReader();
+
+ reader.onload = () => {
+ try {
+ const fileContent = JSON.parse(reader.result as string);
+
+ if (
+ !fileContent.messages ||
+ !fileContent.model ||
+ !fileContent.token ||
+ !fileContent.temperature ||
+ fileContent.type
+ ) {
+ throw "Incorrect Format";
+ }
+
+ dispatch(
+ uploadChat({
+ messages: fileContent.messages,
+ model: fileContent.model,
+ token: fileContent.token,
+ temperature: fileContent.temperature,
+ type: fileContent.type,
+ }),
+ );
+ } catch (error) {
+ notify(
+ `Error parsing JSON file: ${error}`,
+ NotificationSeverity.ERROR,
+ );
+ console.error("Error parsing JSON file:", error);
+ }
+ };
+
+ reader.readAsText(file);
+ }
+ };
+
+ return (
+ <>
+
+
+ Upload Chat
+
+
+ {/* Hidden file input */}
+
+ >
+ );
+};
+
+export default UploadChat;
diff --git a/app-frontend/react/src/components/Summary_WebInput/WebInput.module.scss b/app-frontend/react/src/components/Summary_WebInput/WebInput.module.scss
new file mode 100644
index 0000000..069a27a
--- /dev/null
+++ b/app-frontend/react/src/components/Summary_WebInput/WebInput.module.scss
@@ -0,0 +1,19 @@
+.inputWrapper {
+ width: 100%;
+ max-width: 700px;
+ margin-top: 1rem;
+}
+
+.dataList {
+ width: 100%;
+ margin-top: 2rem;
+ max-height: 300;
+ overflow: auto;
+ border: 1px solid;
+ border-radius: 8px;
+ padding: 1rem;
+
+ li:not(:last-of-type) {
+ margin-bottom: 1rem;
+ }
+}
diff --git a/app-frontend/react/src/components/Summary_WebInput/WebInput.tsx b/app-frontend/react/src/components/Summary_WebInput/WebInput.tsx
new file mode 100644
index 0000000..693e824
--- /dev/null
+++ b/app-frontend/react/src/components/Summary_WebInput/WebInput.tsx
@@ -0,0 +1,120 @@
+import { AddCircle, Delete } from "@mui/icons-material";
+import {
+ IconButton,
+ InputAdornment,
+ List,
+ ListItem,
+ ListItemText,
+ styled,
+ TextField,
+ useTheme,
+} from "@mui/material";
+import { useState } from "react";
+import styles from "./WebInput.module.scss";
+import { Language } from "@mui/icons-material";
+
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import {
+ conversationSelector,
+ setSourceLinks,
+} from "@redux/Conversation/ConversationSlice";
+
+export const CustomTextInput = styled(TextField)(({ theme }) => ({
+ ...theme.customStyles.webInput,
+}));
+
+export const AddIcon = styled(AddCircle)(({ theme }) => ({
+ path: {
+ fill: theme.customStyles.icon?.main,
+ },
+}));
+
+const WebInput = () => {
+ const [inputValue, setInputValue] = useState("");
+
+ const theme = useTheme();
+
+ const { sourceLinks } = useAppSelector(conversationSelector);
+ const dispatch = useAppDispatch();
+
+ const handleAdd = (newSource: string) => {
+ if (!newSource) return;
+ const prevSource = sourceLinks ?? [];
+ dispatch(setSourceLinks([...prevSource, newSource]));
+ setInputValue("");
+ };
+
+ const handleDelete = (index: number) => {
+ const newSource = sourceLinks.filter((s: any, i: number) => i !== index);
+ dispatch(setSourceLinks([...newSource]));
+ };
+
+ const handleKeyPress = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && inputValue) {
+ handleAdd(inputValue);
+ }
+ };
+
+ const handleIconClick = () => {
+ if (inputValue) {
+ handleAdd(inputValue);
+ }
+ };
+
+ const sourcesDisplay = () => {
+ if (!sourceLinks || sourceLinks.length === 0) return;
+
+ return (
+
+ {sourceLinks.map((sourceItem: string, index: number) => (
+ handleDelete(index)}>
+
+
+ }
+ >
+ 30
+ ? `${sourceItem.substring(0, 27)}...`
+ : sourceItem
+ }
+ />
+
+
+ ))}
+
+ );
+ };
+
+ return (
+
+
) =>
+ setInputValue(e.target.value)
+ }
+ InputProps={{
+ endAdornment: (
+
+
+
+ ),
+ }}
+ fullWidth
+ />
+
+ {sourcesDisplay()}
+
+ );
+};
+
+export default WebInput;
diff --git a/app-frontend/react/src/components/UserInfoModal/UserInfoModal.tsx b/app-frontend/react/src/components/UserInfoModal/UserInfoModal.tsx
deleted file mode 100644
index d958708..0000000
--- a/app-frontend/react/src/components/UserInfoModal/UserInfoModal.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-// 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';
-
-import { useDispatch } from 'react-redux';
-import { setUser } from '../../redux/User/userSlice';
-
-
-const UserInfoModal = () => {
- // const [opened, { open, close }] = useDisclosure(false);
- // const { name } = useSelector(userSelector);
- const username = "OPEA Studio User";
- // const [username, setUsername] = useState(name || "");
-
- const dispatch = useDispatch();
- dispatch(setUser(username));
-
- // 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/app-frontend/react/src/components/sidebar/sidebar.module.scss b/app-frontend/react/src/components/sidebar/sidebar.module.scss
deleted file mode 100644
index b58a253..0000000
--- a/app-frontend/react/src/components/sidebar/sidebar.module.scss
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- 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/app-frontend/react/src/components/sidebar/sidebar.tsx b/app-frontend/react/src/components/sidebar/sidebar.tsx
deleted file mode 100644
index e5e9349..0000000
--- a/app-frontend/react/src/components/sidebar/sidebar.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-// 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/app-frontend/react/src/config.ts b/app-frontend/react/src/config.ts
index 281cab7..ffae0bb 100644
--- a/app-frontend/react/src/config.ts
+++ b/app-frontend/react/src/config.ts
@@ -1,21 +1,55 @@
-// Copyright (C) 2024 Intel Corporation
+// Copyright (C) 2025 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
-// console.log("Environment variables:", import.meta.env);
+const config = {
+ companyName: "OPEA Studio",
+ logo: "/logo512.png",
+ tagline: "what can I help you today?",
+ disclaimer:
+ "Generative AI provides significant benefits for enhancing the productivity of quality engineers, production support teams, software developers, and DevOps professionals. With a secure and scalable toolbox, it offers a flexible architecture capable of connecting to various data sources and models, enabling it to address a wide range of Generative AI use cases.
This platform saves your user ID to retain chat history, which you can choose to delete from the app at any time.
",
+ defaultChatPrompt: `You are a helpful assistant`,
+};
-export const APP_UUID = import.meta.env.VITE_APP_UUID;
-export const CHAT_QNA_URL = "VITE_APP_BACKEND_SERVICE_URL";
-export const DATA_PREP_URL = "VITE_APP_DATA_PREP_SERVICE_URL";
+export default config;
-type UiFeatures = {
- dataprep: boolean;
- chat: boolean;
-};
-export const UI_FEATURES: UiFeatures = {
- chat: CHAT_QNA_URL.startsWith('VITE_') ? false : true,
- dataprep: DATA_PREP_URL.startsWith('VITE_') ? false : true
-};
-console.log("chat qna", CHAT_QNA_URL, UI_FEATURES.chat);
-console.log("data prep", DATA_PREP_URL, UI_FEATURES.dataprep);
\ No newline at end of file
+// export const CHAT_QNA_URL = import.meta.env.VITE_BACKEND_SERVICE_ENDPOINT_CHATQNA;
+export const CHAT_QNA_URL = import.meta.env.VITE_BACKEND_SERVICE_URL
+// export const CODE_GEN_URL = import.meta.env.VITE_BACKEND_SERVICE_ENDPOINT_CODEGEN;
+export const CODE_GEN_URL = import.meta.env.VITE_BACKEND_SERVICE_URL
+// export const DOC_SUM_URL = import.meta.env.VITE_BACKEND_SERVICE_ENDPOINT_DOCSUM;
+export const DOC_SUM_URL = import.meta.env.VITE_BACKEND_SERVICE_URL
+export const UI_SELECTION = import.meta.env.VITE_UI_SELECTION;
+
+console.log ("BACKEND_SERVICE_URL", import.meta.env.VITE_BACKEND_SERVICE_URL);
+console.log ("DATA_PREP_SERVICE_URL", import.meta.env.VITE_DATAPREP_SERVICE_URL);
+console.log ("CHAT_HISTORY_SERVICE_URL", import.meta.env.VITE_CHAT_HISTORY_SERVICE_URL);
+console.log ("UI_SELECTION", import.meta.env.VITE_UI_SELECTION);
+
+// export const FAQ_GEN_URL = import.meta.env.VITE_BACKEND_SERVICE_ENDPOINT_FAQGEN;
+export const DATA_PREP_URL = import.meta.env.VITE_DATAPREP_SERVICE_URL;
+// export const DATA_PREP_URL = "http://localhost:6007/v1/dataprep/";
+export const DATA_PREP_INGEST_URL = DATA_PREP_URL + "/ingest";
+export const DATA_PREP_GET_URL = DATA_PREP_URL + "/get";
+export const DATA_PREP_DELETE_URL = DATA_PREP_URL + "/delete";
+
+console.log ("DATA_PREP_INGEST_URL", DATA_PREP_INGEST_URL);
+console.log ("DATA_PREP_GET_URL", DATA_PREP_GET_URL);
+console.log ("DATA_PREP_DELETE_URL", DATA_PREP_DELETE_URL);
+
+export const CHAT_HISTORY_URL = import.meta.env.VITE_CHAT_HISTORY_SERVICE_URL;
+// export const CHAT_HISTORY_URL = "http://localhost:6012/v1/chathistory/";
+export const CHAT_HISTORY_CREATE = CHAT_HISTORY_URL + "/create";
+export const CHAT_HISTORY_GET = CHAT_HISTORY_URL + "/get";
+export const CHAT_HISTORY_DELETE = CHAT_HISTORY_URL + "/delete";
+
+console.log ("CHAT_HISTORY_CREATE", CHAT_HISTORY_CREATE);
+console.log ("CHAT_HISTORY_GET", CHAT_HISTORY_GET);
+console.log ("CHAT_HISTORY_DELETE", CHAT_HISTORY_DELETE);
+
+export const PROMPT_MANAGER_GET = import.meta.env.VITE_PROMPT_SERVICE_GET_ENDPOINT;
+export const PROMPT_MANAGER_CREATE = import.meta.env.VITE_PROMPT_SERVICE_CREATE_ENDPOINT;
+export const PROMPT_MANAGER_DELETE = import.meta.env.VITE_PROMPT_SERVICE_DELETE_ENDPOINT;
+
+
diff --git a/app-frontend/react/src/contexts/ThemeContext.tsx b/app-frontend/react/src/contexts/ThemeContext.tsx
new file mode 100644
index 0000000..94ec15b
--- /dev/null
+++ b/app-frontend/react/src/contexts/ThemeContext.tsx
@@ -0,0 +1,39 @@
+import React, { createContext, useState, useEffect, useCallback } from "react";
+import { ThemeProvider as MuiThemeProvider, CssBaseline } from "@mui/material";
+import { themeCreator } from "../theme/theme";
+
+interface ThemeContextType {
+ darkMode: boolean;
+ toggleTheme: () => void;
+}
+
+export const ThemeContext = createContext({
+ darkMode: false,
+ toggleTheme: () => {},
+});
+
+export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({
+ children,
+}) => {
+ const savedTheme = localStorage.getItem("theme") === "dark";
+ const [darkMode, setDarkMode] = useState(savedTheme);
+
+ const toggleTheme = useCallback(() => {
+ setDarkMode((prevMode) => !prevMode);
+ }, []);
+
+ useEffect(() => {
+ localStorage.setItem("theme", darkMode ? "dark" : "light");
+ }, [darkMode]);
+
+ const theme = themeCreator(darkMode ? "dark" : "light");
+
+ return (
+
+
+
+ {children}
+
+
+ );
+};
diff --git a/app-frontend/react/src/icons/Atom.tsx b/app-frontend/react/src/icons/Atom.tsx
new file mode 100644
index 0000000..039b640
--- /dev/null
+++ b/app-frontend/react/src/icons/Atom.tsx
@@ -0,0 +1,134 @@
+import { useTheme } from "@mui/material";
+
+interface AtomIconProps {
+ className?: string;
+}
+
+const AtomIcon: React.FC = ({ className }) => {
+ const theme = useTheme();
+
+ const iconColor = theme.customStyles.icon?.main;
+
+ return (
+
+
+
+
+ );
+};
+
+const AtomAnimation: React.FC = ({ className }) => {
+ const theme = useTheme();
+
+ const iconColor = theme.customStyles.icon?.main;
+
+ return (
+
+
+ {/* Grouping each ellipse with a circle */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export { AtomAnimation, AtomIcon };
diff --git a/app-frontend/react/src/icons/ChatBubble.tsx b/app-frontend/react/src/icons/ChatBubble.tsx
new file mode 100644
index 0000000..9ba2c4a
--- /dev/null
+++ b/app-frontend/react/src/icons/ChatBubble.tsx
@@ -0,0 +1,38 @@
+import { useTheme } from "@mui/material";
+
+interface ChatBubbleIconProps {
+ className?: string;
+}
+
+const ChatBubbleIcon: React.FC = ({ className }) => {
+ const theme = useTheme();
+
+ const iconColor = theme.customStyles.icon?.main;
+
+ return (
+
+
+
+
+ );
+};
+
+export default ChatBubbleIcon;
diff --git a/app-frontend/react/src/icons/Database.tsx b/app-frontend/react/src/icons/Database.tsx
new file mode 100644
index 0000000..a74130d
--- /dev/null
+++ b/app-frontend/react/src/icons/Database.tsx
@@ -0,0 +1,29 @@
+import { SvgIcon, useTheme } from "@mui/material";
+
+interface DatabaseIconProps {
+ className?: string;
+}
+
+const DatabaseIcon: React.FC = ({ className }) => {
+ const theme = useTheme();
+ const iconColor = theme.customStyles.icon?.main;
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default DatabaseIcon;
diff --git a/app-frontend/react/src/icons/Recent.tsx b/app-frontend/react/src/icons/Recent.tsx
new file mode 100644
index 0000000..6018916
--- /dev/null
+++ b/app-frontend/react/src/icons/Recent.tsx
@@ -0,0 +1,29 @@
+import { SvgIcon, useTheme } from "@mui/material";
+
+interface RecentIconProps {
+ className?: string;
+}
+
+const RecentIcon: React.FC = ({ className }) => {
+ const theme = useTheme();
+ const iconColor = theme.customStyles.icon?.main;
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default RecentIcon;
diff --git a/app-frontend/react/src/icons/Waiting.tsx b/app-frontend/react/src/icons/Waiting.tsx
new file mode 100644
index 0000000..f2767d6
--- /dev/null
+++ b/app-frontend/react/src/icons/Waiting.tsx
@@ -0,0 +1,45 @@
+import { useTheme } from "styled-components";
+
+const WaitingIcon = () => {
+ const theme = useTheme();
+ const iconColor = theme.customStyles.icon?.main;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default WaitingIcon;
diff --git a/app-frontend/react/src/index.scss b/app-frontend/react/src/index.scss
index 53e7162..bf8ec54 100644
--- a/app-frontend/react/src/index.scss
+++ b/app-frontend/react/src/index.scss
@@ -1,20 +1,56 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
+// Before javascript styles
-@import "@mantine/core/styles.css";
+html {
+ font-size: 16px;
+}
-:root {
- font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+body {
+ margin: 0;
+ font-family: "Inter", serif;
+ font-optical-sizing: auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ height: 100vh;
line-height: 1.5;
- font-weight: 400;
}
-html,
-body {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- overflow: hidden;
+#root {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
+}
+
+:root {
+ --header-height: 72px;
+ --header-gutter: 1.5rem;
+ --sidebar-width: 260px;
+ --vertical-spacer: 2rem;
+ --content-width: 800px;
+ --content-gutter: 3rem;
+ --input-radius: 30px;
+ --copy-color: #3d447f;
+}
+
+::-webkit-scrollbar {
+ background: transparent;
+ width: 10px;
+}
+
+/* Style the thumb (the draggable part of the scrollbar) */
+::-webkit-scrollbar-thumb {
+ height: 20px;
+ background-color: rgba(0, 0, 0, 0.3);
+ /* Thumb color */
+ border-radius: 5px;
+ /* Optional, for rounded corners */
+}
+
+/* Optionally, you can add hover effects for the thumb */
+::-webkit-scrollbar-thumb:hover {
+ background-color: rgba(0, 0, 0, 0.5);
+ /* Darker thumb when hovered */
}
diff --git a/app-frontend/react/src/index.tsx b/app-frontend/react/src/index.tsx
new file mode 100644
index 0000000..910a386
--- /dev/null
+++ b/app-frontend/react/src/index.tsx
@@ -0,0 +1,24 @@
+// import React from "react";
+import { createRoot } from "react-dom/client";
+import "./index.scss";
+import App from "./App";
+import { Provider } from "react-redux";
+import { store } from "@redux/store";
+import { ThemeProvider } from "@contexts/ThemeContext";
+// import keycloak from "@root/keycloak";
+// import { ReactKeycloakProvider } from "@react-keycloak/web";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+ //@ts-ignore
+ //
+
+
+
+
+
+ // ,
+);
diff --git a/app-frontend/react/src/layouts/Main/MainLayout.module.scss b/app-frontend/react/src/layouts/Main/MainLayout.module.scss
new file mode 100644
index 0000000..0736eaa
--- /dev/null
+++ b/app-frontend/react/src/layouts/Main/MainLayout.module.scss
@@ -0,0 +1,21 @@
+.mainLayout {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ max-height: 100%;
+ overflow: hidden;
+}
+
+.mainWrapper {
+ display: flex;
+ flex-direction: row;
+ flex-grow: 1;
+ max-height: 100%;
+ overflow: auto;
+ overflow-x: hidden;
+}
+
+.contentWrapper {
+ max-height: 100%;
+ width: 100%;
+}
diff --git a/app-frontend/react/src/layouts/Main/MainLayout.tsx b/app-frontend/react/src/layouts/Main/MainLayout.tsx
new file mode 100644
index 0000000..2965e70
--- /dev/null
+++ b/app-frontend/react/src/layouts/Main/MainLayout.tsx
@@ -0,0 +1,39 @@
+import Header from "@components/Header/Header";
+import { SideBarSpacer } from "@components/SideBar/SideBar";
+import { useState } from "react";
+import { Outlet } from "react-router-dom";
+import styles from "./MainLayout.module.scss";
+
+interface MainLayoutProps {
+ chatView?: boolean;
+ historyView?: boolean;
+ dataView?: boolean;
+}
+
+const MainLayout: React.FC = ({
+ chatView = false,
+ historyView = false,
+ dataView = false,
+}) => {
+ const [asideOpen, setAsideOpen] = useState(false);
+
+ return (
+
+ );
+};
+
+export default MainLayout;
diff --git a/app-frontend/react/src/layouts/Minimal/MinimalLayout.module.scss b/app-frontend/react/src/layouts/Minimal/MinimalLayout.module.scss
new file mode 100644
index 0000000..2a2ff37
--- /dev/null
+++ b/app-frontend/react/src/layouts/Minimal/MinimalLayout.module.scss
@@ -0,0 +1,10 @@
+.minimalLayout {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ width: 100%;
+ padding: 2rem;
+ text-align: center;
+}
diff --git a/app-frontend/react/src/layouts/Minimal/MinimalLayout.tsx b/app-frontend/react/src/layouts/Minimal/MinimalLayout.tsx
new file mode 100644
index 0000000..0ccc5ae
--- /dev/null
+++ b/app-frontend/react/src/layouts/Minimal/MinimalLayout.tsx
@@ -0,0 +1,13 @@
+// About pages or privacy policy are likely minimal layouts
+import { Outlet } from "react-router-dom";
+import styles from "./MinimalLayout.module.scss";
+
+const MinimalLayout = () => {
+ return (
+
+
+
+ );
+};
+
+export default MinimalLayout;
diff --git a/app-frontend/react/src/layouts/ProtectedRoute/ProtectedRoute.tsx b/app-frontend/react/src/layouts/ProtectedRoute/ProtectedRoute.tsx
new file mode 100644
index 0000000..521e766
--- /dev/null
+++ b/app-frontend/react/src/layouts/ProtectedRoute/ProtectedRoute.tsx
@@ -0,0 +1,29 @@
+import { useAppSelector } from "@redux/store";
+import { userSelector } from "@redux/User/userSlice";
+import React, { useEffect } from "react";
+
+interface ProtectedRouteProps {
+ component: React.ComponentType;
+ requiredRoles: string[];
+}
+
+const ProtectedRoute: React.FC = ({
+ component: Component,
+ requiredRoles,
+}) => {
+ const { isAuthenticated, role } = useAppSelector(userSelector);
+
+ const isAllowed = React.useMemo(() => {
+ return isAuthenticated && requiredRoles.includes(role);
+ }, [isAuthenticated, role, requiredRoles.join(",")]);
+
+ if (!isAllowed) {
+ return (
+ Access Denied: You do not have permission to view this page.
+ );
+ }
+
+ return ;
+};
+
+export default ProtectedRoute;
diff --git a/app-frontend/react/src/logo.svg b/app-frontend/react/src/logo.svg
new file mode 100644
index 0000000..7901511
--- /dev/null
+++ b/app-frontend/react/src/logo.svg
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app-frontend/react/src/main.tsx b/app-frontend/react/src/main.tsx
deleted file mode 100644
index 3d9c915..0000000
--- a/app-frontend/react/src/main.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-// 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/app-frontend/react/src/pages/Chat/ChatView.module.scss b/app-frontend/react/src/pages/Chat/ChatView.module.scss
new file mode 100644
index 0000000..d2c5579
--- /dev/null
+++ b/app-frontend/react/src/pages/Chat/ChatView.module.scss
@@ -0,0 +1,47 @@
+.chatView {
+ display: flex;
+ flex-direction: column;
+ max-height: 100%;
+ height: 100%;
+
+ .messagesWrapper {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ padding: calc(var(--header-gutter) * 2) calc(var(--header-gutter));
+ max-height: 100%;
+ overflow-x: auto;
+
+ @media screen and (min-width: 1200px) {
+ padding: calc(var(--header-gutter) * 2);
+ }
+
+ .messageContent {
+ width: 100%;
+ max-width: var(--content-width);
+ margin: 0px auto;
+
+ pre {
+ margin: 0;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ }
+ }
+ }
+
+ .inputWrapper {
+ display: block;
+ margin: 0px auto;
+ padding: calc(var(--header-gutter) * 0.2) calc(var(--header-gutter) * 0.2);
+ max-width: calc((var(--header-gutter) * 2) + 800px);
+ width: 100%;
+ }
+
+ .promptSettings {
+ display: block;
+ margin: 0px auto;
+ padding: calc(var(--header-gutter) * 0.2) calc(var(--header-gutter) * 0.2);
+ max-width: calc((var(--header-gutter) * 2) + 800px);
+ width: 100%;
+ }
+}
diff --git a/app-frontend/react/src/pages/Chat/ChatView.tsx b/app-frontend/react/src/pages/Chat/ChatView.tsx
new file mode 100644
index 0000000..505841b
--- /dev/null
+++ b/app-frontend/react/src/pages/Chat/ChatView.tsx
@@ -0,0 +1,355 @@
+import { useEffect, useRef, JSX } from "react";
+import styles from "./ChatView.module.scss";
+import { useLocation, useNavigate, useParams } from "react-router-dom";
+
+import { Box } from "@mui/material";
+import PrimaryInput from "@components/PrimaryInput/PrimaryInput";
+
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import {
+ abortStream,
+ conversationSelector,
+ doCodeGen,
+ doConversation,
+ doSummaryFaq,
+ getConversationHistory,
+ newConversation,
+ setSelectedConversationId,
+} from "@redux/Conversation/ConversationSlice";
+import { userSelector } from "@redux/User/userSlice";
+import ChatUser from "@components/Chat_User/ChatUser";
+import ChatAssistant from "@components/Chat_Assistant/ChatAssistant";
+import PromptSettings from "@components/PromptSettings/PromptSettings";
+import { Message, MessageRole } from "@redux/Conversation/Conversation";
+import { getCurrentTimeStamp, readFilesAndSummarize } from "@utils/utils";
+import ChatSources from "@components/Chat_Sources/ChatSources";
+import { useNavigateWithQuery } from "@utils/navigationAndAxiosWithQuery";
+
+const ChatView = () => {
+ const { name } = useAppSelector(userSelector);
+ const {
+ selectedConversationHistory,
+ type,
+ sourceLinks,
+ sourceFiles,
+ temperature,
+ token,
+ model,
+ systemPrompt,
+ selectedConversationId,
+ onGoingResult,
+ isPending,
+ } = useAppSelector(conversationSelector);
+
+ const systemPromptObject: Message = {
+ role: MessageRole.System,
+ content: systemPrompt,
+ };
+
+ const dispatch = useAppDispatch();
+ const navigate = useNavigate();
+ const navigateWithQuery = useNavigateWithQuery();
+
+ // existing chat
+ const { conversation_id } = useParams();
+
+ // new chat
+ const { state } = useLocation();
+ const initialMessage = state?.initialMessage || null;
+ const isSummary = type === "summary" || false;
+ const isCodeGen = type === "code" || false;
+ const isChat = type === "chat" || false;
+ const isFaq = type === "faq" || false;
+
+ const fromHome = useRef(false);
+ const newMessage = useRef(false);
+
+ const scrollContainer = useRef(null);
+ const autoScroll = useRef(true);
+ const scrollTimeout = useRef(null);
+
+ const messagesBeginRef = useRef(null);
+ const messagesEndRef = useRef(null);
+
+ // Scroll to top of fetched message
+ const scrollToTop = () => {
+ messagesBeginRef.current?.scrollIntoView({ behavior: "smooth" });
+ };
+
+ // Scroll to the latest message
+ const scrollToBottom = () => {
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
+ };
+
+ const handleUserScroll = () => {
+ if (scrollContainer.current) {
+ const { scrollTop, scrollHeight, clientHeight } = scrollContainer.current;
+
+ // Disable autoscroll if the user scrolls up significantly
+ if (scrollTop + clientHeight < scrollHeight - 50) {
+ autoScroll.current = false;
+ } else {
+ // Use a timeout to delay re-enabling autoscroll, preventing rapid toggling
+ if (scrollTimeout.current) clearTimeout(scrollTimeout.current);
+ scrollTimeout.current = setTimeout(() => {
+ autoScroll.current = true;
+ }, 500); // Delay auto-scroll reactivation
+ }
+ }
+ };
+
+ useEffect(() => {
+ const container = scrollContainer.current;
+ if (!container) return;
+
+ container.addEventListener("scroll", handleUserScroll);
+
+ return () => {
+ container.removeEventListener("scroll", handleUserScroll);
+ if (scrollTimeout.current) clearTimeout(scrollTimeout.current);
+ if (onGoingResult) dispatch(abortStream());
+ console.log("Reset Convo, preserve settings");
+ dispatch(newConversation(false));
+ };
+ }, []);
+
+ useEffect(() => {
+ if (onGoingResult && autoScroll.current) {
+ scrollToBottom();
+ }
+ }, [onGoingResult]);
+
+ useEffect(() => {
+ if (!name) return;
+
+ // reset view (not full reset)
+ // dispatch(newConversation(false)) // moved to useEffect unmount
+
+ // convo starting, new conversation id inboud
+ if (!conversation_id) fromHome.current = true;
+
+ // existing convo, load and scroll up
+ if (conversation_id && conversation_id !== "new") {
+ dispatch(setSelectedConversationId(conversation_id));
+ dispatch(
+ getConversationHistory({ user: name, conversationId: conversation_id }),
+ );
+ scrollToTop();
+ return;
+ } else if (conversation_id === "new") {
+ // new convo
+ fromHome.current = true;
+
+ if (
+ (isSummary || isFaq) &&
+ ((sourceLinks && sourceLinks.length > 0) ||
+ (sourceFiles && sourceFiles.length > 0) ||
+ initialMessage)
+ ) {
+ // console.log('SUMMARY/FAQ')
+ newSummaryOrFaq();
+ return;
+ }
+
+ if (isCodeGen && initialMessage) {
+ // console.log('CODE')
+ newCodeGen();
+ return;
+ }
+
+ if (isChat && initialMessage) {
+ // console.log('NEW CHAT')
+ newChat();
+ return;
+ }
+
+ // no match for view, go home
+ console.log("Go Home");
+ navigateWithQuery("/");
+ }
+ }, [conversation_id, name]);
+
+ const newSummaryOrFaq = async () => {
+ const userPrompt: Message = {
+ role: MessageRole.User,
+ content: initialMessage,
+ time: getCurrentTimeStamp().toString(),
+ };
+
+ let prompt = {
+ conversationId: selectedConversationId,
+ userPrompt,
+ messages: initialMessage,
+ model,
+ files: sourceFiles,
+ temperature,
+ token,
+ type, // TODO: cannot past type
+ };
+
+ doSummaryFaq(prompt);
+ };
+
+ const newChat = () => {
+ const userPrompt: Message = {
+ role: MessageRole.User,
+ content: initialMessage,
+ time: getCurrentTimeStamp().toString(),
+ };
+
+ let messages: Message[] = [];
+ messages = [systemPromptObject, ...selectedConversationHistory];
+
+ let prompt = {
+ conversationId: selectedConversationId,
+ userPrompt,
+ messages,
+ model,
+ temperature,
+ token,
+ time: getCurrentTimeStamp().toString(), // TODO: cannot past time
+ type, // TODO: cannot past type
+ };
+
+ doConversation(prompt);
+ };
+
+ const newCodeGen = () => {
+ const userPrompt: Message = {
+ role: MessageRole.User,
+ content: initialMessage,
+ time: getCurrentTimeStamp().toString(),
+ };
+
+ let prompt = {
+ conversationId: selectedConversationId,
+ userPrompt: userPrompt,
+ messages: [],
+ model,
+ temperature,
+ token,
+ time: getCurrentTimeStamp().toString(), // TODO: cannot past time
+ type, // TODO: cannot past type
+ };
+
+ doCodeGen(prompt);
+ };
+
+ // ADD to existing conversation
+ const addMessage = (query: string) => {
+ const userPrompt: Message = {
+ role: MessageRole.User,
+ content: query,
+ time: getCurrentTimeStamp().toString(),
+ };
+
+ let messages: Message[] = [];
+
+ messages = [...selectedConversationHistory];
+
+ let prompt = {
+ conversationId: selectedConversationId,
+ userPrompt,
+ messages,
+ model,
+ temperature,
+ token,
+ type,
+ };
+
+ doConversation(prompt);
+ };
+
+ const handleSendMessage = async (messageContent: string) => {
+ newMessage.current = true;
+ addMessage(messageContent);
+ };
+
+ const displayChatUser = (message: Message) => {
+ // file post will not have message, will display file.extension instead
+ if ((isSummary || isFaq) && !message.content) return;
+
+ // normal message
+ if (message.role === MessageRole.User) {
+ return ;
+ }
+ };
+
+ const displayMessage = () => {
+ let messagesDisplay: JSX.Element[] = [];
+
+ selectedConversationHistory.map((message, messageIndex) => {
+ const timestamp = message.time || Math.random();
+ if (message.role !== MessageRole.System) {
+ messagesDisplay.push(
+
+ {displayChatUser(message)}
+ {message.role === MessageRole.Assistant && (
+
+ )}
+ ,
+ );
+ }
+ });
+
+ if (onGoingResult) {
+ const continueMessage: Message = {
+ role: MessageRole.Assistant,
+ content: onGoingResult,
+ time: Date.now().toString(),
+ };
+
+ messagesDisplay.push(
+
+
+ ,
+ );
+ } else if (isPending) {
+ const continueMessage: Message = {
+ role: MessageRole.Assistant,
+ content: "",
+ time: Date.now().toString(),
+ };
+
+ messagesDisplay.push(
+
+
+ ,
+ );
+ }
+
+ return messagesDisplay;
+ };
+
+ return !selectedConversationHistory ? (
+ <>>
+ ) : (
+
+
+
+
+
+
+ {displayMessage()}
+
+
+
+
+
+
+
+ );
+};
+
+export default ChatView;
diff --git a/app-frontend/react/src/pages/DataSource/DataSourceManagement.module.scss b/app-frontend/react/src/pages/DataSource/DataSourceManagement.module.scss
new file mode 100644
index 0000000..28e6863
--- /dev/null
+++ b/app-frontend/react/src/pages/DataSource/DataSourceManagement.module.scss
@@ -0,0 +1,71 @@
+.dataView {
+ height: 100%;
+ width: 100%;
+ max-width: var(--content-width);
+ width: 100%;
+ margin: 0px auto;
+ padding: calc(var(--header-gutter) * 2);
+}
+
+.dataItem {
+ margin-bottom: 1rem;
+ position: relative;
+ padding: 0;
+
+ :global {
+ .MuiCheckbox-root {
+ position: absolute;
+ right: 100%;
+ margin-right: 0.25rem;
+ top: 50%;
+ transform: translateY(-50%);
+ @media screen and (min-width: 901px) {
+ margin-right: 1rem;
+ }
+ }
+ }
+}
+
+.dataName {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: flex-start;
+ width: 100%;
+ padding: 1rem;
+ margin: 0;
+ line-height: 1.5;
+}
+
+.searchInput {
+ width: 100%;
+ margin-bottom: 1rem;
+ background: none;
+
+ // padding: var(--header-gutter);
+ border: 0;
+ margin-bottom: 2rem;
+ // margin-right: 45px;
+ &:focus {
+ outline: none;
+ }
+
+ :global {
+ .MuiInputBase-root {
+ border-radius: var(--input-radius);
+ }
+ }
+}
+
+.dataInputWrapper {
+ width: 100%;
+ margin-top: 2rem;
+ margin-bottom: 2rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.actions button {
+ margin-left: 0.5rem;
+}
diff --git a/app-frontend/react/src/pages/DataSource/DataSourceManagement.tsx b/app-frontend/react/src/pages/DataSource/DataSourceManagement.tsx
new file mode 100644
index 0000000..cb0fe9a
--- /dev/null
+++ b/app-frontend/react/src/pages/DataSource/DataSourceManagement.tsx
@@ -0,0 +1,242 @@
+import {
+ Box,
+ Checkbox,
+ FormControlLabel,
+ List,
+ ListItem,
+ Typography,
+ FormGroup,
+} from "@mui/material";
+import { useTheme } from "@mui/material/styles";
+import { useEffect, useState } from "react";
+import styles from "./DataSourceManagement.module.scss";
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import {
+ conversationSelector,
+ deleteInDataSource,
+ getAllFilesInDataSource,
+ deleteMultipleInDataSource,
+} from "@redux/Conversation/ConversationSlice";
+import { file } from "@redux/Conversation/Conversation";
+import DropDown from "@components/DropDown/DropDown";
+import DataWebInput from "@components/Data_Web/DataWebInput";
+import FileInput from "@components/File_Input/FileInput";
+import SearchInput from "@components/SearchInput/SearchInput";
+import {
+ DeleteButton,
+ SolidButton,
+ TextButton,
+} from "@root/shared/ActionButtons";
+
+const DataSourceManagement = () => {
+ const dispatch = useAppDispatch();
+
+ const theme = useTheme();
+
+ const { filesInDataSource } = useAppSelector(conversationSelector);
+
+ const [dataList, setDataList] = useState([]);
+ const [activeSourceType, setActiveSourceType] = useState("documents");
+ const [selectActive, setSelectActive] = useState(false);
+ const [selectAll, setSelectAll] = useState(false);
+ const [checkedItems, setCheckedItems] = useState>({});
+
+ useEffect(() => {
+ dispatch(getAllFilesInDataSource({ knowledgeBaseId: "default" }));
+ }, []);
+
+ const sortFiles = () => {
+ if (activeSourceType === "web") {
+ let webFiles = filesInDataSource.filter((file) =>
+ file.name.startsWith("http"),
+ );
+ return webFiles;
+ } else {
+ let otherFiles = filesInDataSource.filter(
+ (file) => !file.name.startsWith("http"),
+ );
+ return otherFiles;
+ }
+ };
+
+ useEffect(() => {
+ setDataList(sortFiles());
+ }, [filesInDataSource, activeSourceType]);
+
+ const handleCheckboxChange = (conversationId: string) => {
+ setCheckedItems((prev) => ({
+ ...prev,
+ [conversationId]: !prev[conversationId],
+ }));
+ };
+
+ const displayFiles = () => {
+ return dataList.map((file: file) => {
+ const isChecked = !!checkedItems[file.id];
+
+ const fileText = (
+ <>
+ {file.name}
+ {/* TODO: timestamp for all conversations? */}
+ {/* Last message {convertTime(conversation.updated_at)} */}
+ >
+ );
+
+ const controlCheckBox = (
+ handleCheckboxChange(file.id)}
+ checked={isChecked}
+ />
+ );
+
+ return (
+
+ {selectActive ? (
+
+ ) : (
+
+ {fileText}
+
+ )}
+
+ );
+ });
+ };
+
+ const cancelSelect = () => {
+ setSelectActive(false);
+ setSelectAll(false);
+ setCheckedItems({});
+ };
+
+ const deleteSelected = () => {
+ setSelectActive(false);
+
+ let files = [];
+ for (const [key, value] of Object.entries(checkedItems)) {
+ if (value === true) {
+ files.push(key);
+ }
+ }
+
+ if (files.length > 0) {
+ //update current state
+ setDataList((prev) => prev.filter((item) => !checkedItems[item.id]));
+ dispatch(deleteMultipleInDataSource({ files: files }));
+ }
+ };
+
+ const handleSelectAll = () => {
+ const newSelectAll = !selectAll;
+ setSelectAll(newSelectAll);
+
+ // Add all items' checked state
+ const updatedCheckedItems: Record = {};
+ dataList.forEach((file) => {
+ updatedCheckedItems[file.id] = newSelectAll;
+ });
+
+ setCheckedItems(updatedCheckedItems);
+ };
+
+ const handleSearch = (value: string) => {
+ const filteredList = dataList;
+ const searchResults = filteredList.filter((file: file) =>
+ file.name?.toLowerCase().includes(value.toLowerCase()),
+ );
+ setDataList(value ? searchResults : sortFiles());
+ };
+
+ const updateSource = (value: string) => {
+ setActiveSourceType(value);
+ };
+
+ const displayInput = () => {
+ let input = null;
+ if (activeSourceType === "documents")
+ input = ;
+ if (activeSourceType === "web") input = ;
+ if (activeSourceType === "images")
+ input = ;
+
+ return {input} ;
+ };
+
+ return (
+
+
+
+
+ }
+ />
+
+
+
+ {displayInput()}
+
+
+
+
+
+ You have {dataList.length} file{dataList.length !== 1 && "s"}
+
+
+ {dataList.length > 0 && (
+
+ {selectActive ? (
+ handleSelectAll()}>
+ Select All
+
+ ) : (
+ setSelectActive(true)}>
+ Select
+
+ )}
+
+ {selectActive && (
+ <>
+ cancelSelect()}>Cancel
+ deleteSelected()}>
+ Delete Selected
+
+ >
+ )}
+
+ )}
+
+
+ {displayFiles()}
+
+ );
+};
+
+export default DataSourceManagement;
diff --git a/app-frontend/react/src/pages/History/HistoryView.module.scss b/app-frontend/react/src/pages/History/HistoryView.module.scss
new file mode 100644
index 0000000..6c4c6d5
--- /dev/null
+++ b/app-frontend/react/src/pages/History/HistoryView.module.scss
@@ -0,0 +1,82 @@
+.historyView {
+ height: 100%;
+ width: 100%;
+ max-width: var(--content-width);
+ width: 100%;
+ margin: 0px auto;
+ padding: calc(var(--header-gutter) * 2);
+
+ .historyListWrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ @media screen and (min-width: 901px) {
+ flex-direction: row;
+ justify-content: space-between;
+ }
+ }
+
+ .actions button {
+ margin-left: 0.5rem;
+ margin-top: 0.5rem;
+ margin-bottom: 0.5rem;
+ &:first-of-type {
+ margin-left: 0;
+ }
+ @media screen and (min-width: 901px) {
+ margin-left: 0.5rem;
+ margin-bottom: 0;
+ margin-top: 0;
+ &:first-of-type {
+ margin-left: 0.5rem;
+ }
+ }
+ }
+}
+
+.historyItem {
+ margin-bottom: 1rem;
+ position: relative;
+ padding: 0;
+
+ :global {
+ .MuiCheckbox-root {
+ position: absolute;
+ right: 100%;
+ margin-right: 0.25rem;
+ top: 50%;
+ transform: translateY(-50%);
+
+ @media screen and (min-width: 901px) {
+ margin-right: 1rem;
+ }
+ }
+ }
+
+ a {
+ text-decoration: none;
+ }
+
+ .title {
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ word-break: break-word;
+ max-width: 100%;
+ }
+}
+
+.historyLink {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: flex-start;
+ width: 100%;
+ padding: 1rem;
+ margin: 0;
+ line-height: 1.5;
+}
diff --git a/app-frontend/react/src/pages/History/HistoryView.tsx b/app-frontend/react/src/pages/History/HistoryView.tsx
new file mode 100644
index 0000000..3a59e5e
--- /dev/null
+++ b/app-frontend/react/src/pages/History/HistoryView.tsx
@@ -0,0 +1,214 @@
+import {
+ Box,
+ Checkbox,
+ FormControlLabel,
+ List,
+ ListItem,
+ Typography,
+ Link,
+} from "@mui/material";
+import { useTheme } from "@mui/material/styles";
+import { useState } from "react";
+import styles from "./HistoryView.module.scss";
+
+import { Link as RouterLink } from "react-router-dom";
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import {
+ conversationSelector,
+ deleteConversation,
+ deleteConversations,
+} from "@redux/Conversation/ConversationSlice";
+import { Conversation } from "@redux/Conversation/Conversation";
+import { userSelector } from "@redux/User/userSlice";
+import SearchInput from "@components/SearchInput/SearchInput";
+import {
+ DeleteButton,
+ SolidButton,
+ TextButton,
+} from "@root/shared/ActionButtons";
+
+interface HistoryViewProps {
+ shared: boolean;
+}
+
+const HistoryView: React.FC = ({ shared }) => {
+ const dispatch = useAppDispatch();
+ const { name } = useAppSelector(userSelector);
+
+ const theme = useTheme();
+
+ const { conversations, sharedConversations } =
+ useAppSelector(conversationSelector);
+
+ const [historyList, setHistoryList] = useState(
+ shared ? sharedConversations : conversations,
+ );
+ const [selectActive, setSelectActive] = useState(false);
+ const [selectAll, setSelectAll] = useState(false);
+ const [checkedItems, setCheckedItems] = useState>({});
+
+ const convertTime = (timestamp: number) => {
+ const now = Math.floor(Date.now() / 1000);
+ const diffInSeconds = now - timestamp;
+
+ const diffInMinutes = Math.floor(diffInSeconds / 60);
+ const diffInHours = Math.floor(diffInSeconds / 3600);
+ const diffInDays = Math.floor(diffInSeconds / 86400);
+
+ if (diffInDays > 0) {
+ return `${diffInDays} day${diffInDays > 1 ? "s" : ""} ago`;
+ } else if (diffInHours > 0) {
+ return `${diffInHours} hour${diffInHours > 1 ? "s" : ""} ago`;
+ } else {
+ return `${diffInMinutes} minute${diffInMinutes > 1 ? "s" : ""} ago`;
+ }
+ };
+
+ const handleCheckboxChange = (conversationId: string) => {
+ setCheckedItems((prev) => ({
+ ...prev,
+ [conversationId]: !prev[conversationId],
+ }));
+ };
+
+ const displayHistory = () => {
+ return historyList.map((conversation: Conversation) => {
+ const isChecked = !!checkedItems[conversation.id];
+
+ const itemText = (
+ <>
+
+ {conversation.first_query}
+
+ {/* TODO: timestamp for all conversations? */}
+ {/* Last message {convertTime(conversation.updated_at)} */}
+ >
+ );
+
+ const controlCheckBox = (
+ handleCheckboxChange(conversation.id)}
+ checked={isChecked}
+ />
+ );
+
+ return (
+
+ {selectActive ? (
+
+ ) : (
+
+ {/* body1 Typography is automatically applied in label above, added here to match for spacing */}
+ {itemText}
+
+ )}
+
+ );
+ });
+ };
+
+ const cancelSelect = () => {
+ setSelectActive(false);
+ setSelectAll(false);
+ setCheckedItems({});
+ };
+
+ const deleteSelected = () => {
+ setSelectActive(false);
+
+ let ids = [];
+ for (const [key, value] of Object.entries(checkedItems)) {
+ if (value === true) {
+ ids.push(key);
+ }
+ }
+
+ if (ids.length > 0) {
+ //update current state
+ setHistoryList((prev) =>
+ prev.filter((conversation) => !checkedItems[conversation.id]),
+ );
+ dispatch(
+ deleteConversations({ user: name, conversationIds: ids, useCase: "" }),
+ );
+ }
+ };
+
+ const handleSelectAll = () => {
+ const newSelectAll = !selectAll;
+ setSelectAll(newSelectAll);
+
+ // Add all items' checked state
+ const updatedCheckedItems: Record = {};
+ historyList.forEach((conversation) => {
+ updatedCheckedItems[conversation.id] = newSelectAll;
+ });
+
+ setCheckedItems(updatedCheckedItems);
+ };
+
+ const handleSearch = (value: string) => {
+ const filteredList = shared ? sharedConversations : conversations;
+ const searchResults = filteredList.filter((conversation: Conversation) =>
+ conversation.first_query?.toLowerCase().includes(value.toLowerCase()),
+ );
+ setHistoryList(
+ value ? searchResults : shared ? sharedConversations : conversations,
+ );
+ };
+
+ return (
+
+
+
+
+
+ You have {historyList.length} previous chat
+ {historyList.length > 1 && "s"}
+
+
+ {historyList.length > 0 && (
+
+ {selectActive ? (
+ handleSelectAll()}>
+ Select All
+
+ ) : (
+ setSelectActive(true)}>
+ Select
+
+ )}
+
+ {selectActive && (
+ <>
+ cancelSelect()}>Cancel
+ deleteSelected()}>
+ Delete Selected
+
+ >
+ )}
+
+ )}
+
+
+
{displayHistory()}
+
+ );
+};
+
+export default HistoryView;
diff --git a/app-frontend/react/src/pages/Home/Home.module.scss b/app-frontend/react/src/pages/Home/Home.module.scss
new file mode 100644
index 0000000..b4fd3df
--- /dev/null
+++ b/app-frontend/react/src/pages/Home/Home.module.scss
@@ -0,0 +1,39 @@
+.homeView {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 100%;
+ padding: calc(var(--header-gutter) * 2);
+
+ .title {
+ text-align: center;
+
+ background-clip: text;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ }
+
+ .buttonRow {
+ margin-top: var(--vertical-spacer);
+ justify-content: center;
+ }
+
+ .promptWrapper {
+ width: 100%;
+ max-width: 775px;
+ }
+
+ .inputContainer {
+ width: 100%;
+ max-width: 800px;
+ margin-top: var(--vertical-spacer);
+ }
+
+ .disclaimer {
+ width: 100%;
+ max-width: 600px;
+ margin-top: var(--vertical-spacer);
+ font-size: 14px;
+ }
+}
diff --git a/app-frontend/react/src/pages/Home/Home.tsx b/app-frontend/react/src/pages/Home/Home.tsx
new file mode 100644
index 0000000..b02b8b6
--- /dev/null
+++ b/app-frontend/react/src/pages/Home/Home.tsx
@@ -0,0 +1,110 @@
+import { Button, Typography, Grid2, styled } from "@mui/material";
+// import { AtomIcon, AtomAnimation } from "@icons/Atom";
+import PrimaryInput from "@components/PrimaryInput/PrimaryInput";
+import config from "@root/config";
+import PromptSettings from "@components/PromptSettings/PromptSettings";
+import { UI_SELECTION } from "@root/config";
+import styles from "./Home.module.scss";
+
+import { useNavigateWithQuery } from "@utils/navigationAndAxiosWithQuery";
+import { useAppDispatch, useAppSelector } from "@redux/store";
+// import { userSelector } from "@redux/User/userSlice";
+import {
+ conversationSelector,
+ setType,
+ newConversation,
+} from "@redux/Conversation/ConversationSlice";
+import { useEffect } from "react";
+
+interface InitialStateProps {
+ initialMessage: string;
+}
+
+const HomeButton = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.homeButtons,
+}));
+
+const HomeTitle = styled(Typography)(({ theme }) => ({
+ ...theme.customStyles.homeTitle,
+}));
+
+const Home = () => {
+ // const { disclaimer } = config;
+ const enabledUI = UI_SELECTION
+ ? UI_SELECTION.split(",").map((item) => item.trim())
+ : ["chat", "summary", "code"];
+
+ console.log("Enabled UI:", enabledUI);
+
+ const { type, types, token, model, temperature } =
+ useAppSelector(conversationSelector);
+ const dispatch = useAppDispatch();
+
+ // const { name } = useAppSelector(userSelector);
+
+ const navigateWithQuery = useNavigateWithQuery();
+
+ const handleSendMessage = async (messageContent: string) => {
+ const initialState: InitialStateProps = {
+ initialMessage: messageContent,
+ };
+ navigateWithQuery(`/${type}/new`, { state: initialState });
+ };
+
+ const handleTypeChange = (updateType: string) => {
+ dispatch(setType(updateType));
+ };
+
+ useEffect(() => {
+ // clean up and reset. Can happen on going home from history/upload convo
+ // if convo is missing one of these
+ if (!model || !token || !temperature) {
+ dispatch(newConversation(true));
+ }
+ }, []);
+
+ return (
+
+ {/*
*/}
+ {/*
*/}
+
+
+ Hi, {config.tagline}
+
+
+
+ {types.map((interactionType, index) => (
+ enabledUI.includes(interactionType.key) &&
+ (
+ handleTypeChange(interactionType.key)}
+ aria-selected={type === interactionType.key}
+ startIcon={
+
+ }
+ variant="contained"
+ >
+ {interactionType.name}
+
+ )
+ ))}
+
+
+
+
+
+
+ {/*
*/}
+
+ );
+};
+
+export default Home;
diff --git a/app-frontend/react/src/redux/Conversation/Conversation.ts b/app-frontend/react/src/redux/Conversation/Conversation.ts
index 96ef58e..0714533 100644
--- a/app-frontend/react/src/redux/Conversation/Conversation.ts
+++ b/app-frontend/react/src/redux/Conversation/Conversation.ts
@@ -1,14 +1,52 @@
-// Copyright (C) 2024 Intel Corporation
+// Copyright (C) 2025 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
+export interface UseCase {
+ use_case: string;
+ display_name: string;
+ access_level: string;
+}
+
+export interface Model {
+ displayName: string;
+ endpoint?: string;
+ maxToken: number;
+ minToken: number;
+ model_name: string;
+ types: string[];
+}
+
export type ConversationRequest = {
conversationId: string;
userPrompt: Message;
- messages: Partial[];
+ messages: Message[];
+ model: string;
+ temperature: number;
+ token: number;
+ files?: any[];
+ time?: string;
+ type: string;
+};
+
+export type CodeRequest = {
+ conversationId: string;
+ userPrompt: Message;
+ messages: any[];
+ model: string;
+ type: string;
+ token?: number;
+ temperature?: number;
+};
+
+export type SummaryFaqRequest = {
+ conversationId: string;
+ userPrompt: Message;
+ messages: Message[] | string;
+ files?: any[];
model: string;
- maxTokens: number;
temperature: number;
- // setIsInThinkMode: (isInThinkMode: boolean) => void;
+ token: number;
+ type: string;
};
export enum MessageRole {
@@ -18,28 +56,57 @@ export enum MessageRole {
}
export interface Message {
+ message_id?: string;
role: MessageRole;
content: string;
- time: number;
- agentSteps?: AgentStep[]; // Optional, only for assistant messages
+ time?: string;
}
-export interface Conversation {
- conversationId: string;
- title?: string;
- Messages: Message[];
+export interface ChatMessageProps {
+ message: Message;
+ pending?: boolean;
}
-export interface AgentStep {
- tool: string;
- content: any[];
- source: string[];
+export interface Conversation {
+ id: string;
+ first_query?: string;
}
+export type file = {
+ name: string;
+ id: string;
+ type: string;
+ parent: string;
+};
+
export interface ConversationReducer {
selectedConversationId: string;
conversations: Conversation[];
+ sharedConversations: Conversation[];
+ selectedConversationHistory: Message[];
onGoingResult: string;
- fileDataSources: any;
- isAgent: boolean;
-}
\ No newline at end of file
+ isPending: boolean;
+ filesInDataSource: file[];
+ dataSourceUrlStatus: string;
+
+ useCase: string;
+ useCases: UseCase[];
+ model: string;
+ models: Model[];
+ type: string;
+ types: any[];
+ systemPrompt: string;
+ minToken: number;
+ maxToken: number;
+ token: number;
+ minTemperature: number;
+ maxTemperature: number;
+ temperature: number;
+ sourceType: string;
+ sourceLinks: string[];
+ sourceFiles: any[];
+
+ abortController: AbortController | null;
+
+ uploadInProgress: boolean;
+}
diff --git a/app-frontend/react/src/redux/Conversation/ConversationSlice.ts b/app-frontend/react/src/redux/Conversation/ConversationSlice.ts
index f695045..b0b93fa 100644
--- a/app-frontend/react/src/redux/Conversation/ConversationSlice.ts
+++ b/app-frontend/react/src/redux/Conversation/ConversationSlice.ts
@@ -1,37 +1,106 @@
-// Copyright (C) 2024 Intel Corporation
+// Copyright (C) 2025 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
-import { RootState, store } from "../store";
+import { RootState, store } from "@redux/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";
-// import { useState } from 'react';
-
-export interface FileDataSource {
- id: string;
- sources: string[];
- type: 'Files' | 'URLs';
- status: 'pending' | 'uploading' | 'uploaded' | 'failed';
- startTime: number;
-}
-
-export interface AgentStep {
- tool: string;
- content: any[];
- source: string[];
-}
+import {
+ Message,
+ MessageRole,
+ ConversationReducer,
+ ConversationRequest,
+ Conversation,
+ Model,
+ UseCase,
+ CodeRequest,
+ SummaryFaqRequest,
+} from "./Conversation";
+import { getCurrentTimeStamp } from "@utils/utils";
+import { createAsyncThunkWrapper } from "@redux/thunkUtil";
+import { axiosClient } from "../../utils/navigationAndAxiosWithQuery";
+
+import config, {
+ CHAT_QNA_URL,
+ DATA_PREP_URL,
+ DATA_PREP_INGEST_URL,
+ DATA_PREP_GET_URL,
+ DATA_PREP_DELETE_URL,
+ CHAT_HISTORY_CREATE,
+ CHAT_HISTORY_GET,
+ CHAT_HISTORY_DELETE,
+ CODE_GEN_URL,
+ DOC_SUM_URL,
+ // FAQ_GEN_URL,
+} from "@root/config";
+import { NotificationSeverity, notify } from "@components/Notification/Notification";
+import { ChatBubbleOutline, CodeOutlined, Description, QuizOutlined } from "@mui/icons-material";
+// import { data } from "react-router-dom";
+
+const urlMap: any = {
+ summary: DOC_SUM_URL,
+ // faq: FAQ_GEN_URL,
+ chat: CHAT_QNA_URL,
+ code: CODE_GEN_URL,
+};
+
+const interactionTypes = [
+ {
+ key: "chat",
+ name: "Chat Q&A",
+ icon: ChatBubbleOutline,
+ color: "#0ACA00",
+ },
+ {
+ key: "summary",
+ name: "Summarize Content",
+ icon: Description,
+ color: "#FF4FFC",
+ },
+ {
+ key: "code",
+ name: "Generate Code",
+ icon: CodeOutlined,
+ color: "#489BEA",
+ },
+ // TODO: Enable file upload support for faqgen endpoint similar to summary
+ // {
+ // key: 'faq',
+ // name: 'Generate FAQ',
+ // icon: QuizOutlined,
+ // color: '#9D00FF'
+ // },
+];
const initialState: ConversationReducer = {
conversations: [],
+ sharedConversations: [],
selectedConversationId: "",
+ selectedConversationHistory: [],
onGoingResult: "",
- fileDataSources: [] as FileDataSource[],
- isAgent: false,
+ isPending: false,
+ filesInDataSource: [],
+ dataSourceUrlStatus: "",
+
+ useCase: "",
+ useCases: [],
+ model: "",
+ models: [],
+ type: "chat",
+ types: interactionTypes,
+ systemPrompt: config.defaultChatPrompt,
+ minToken: 100,
+ maxToken: 1000,
+ token: 100,
+ minTemperature: 0,
+ maxTemperature: 1,
+ temperature: 0.4,
+ sourceType: "documents",
+ sourceLinks: [],
+ sourceFiles: [],
+
+ abortController: null,
+
+ uploadInProgress: false,
};
export const ConversationSlice = createSlice({
@@ -42,192 +111,549 @@ export const ConversationSlice = createSlice({
state.conversations = [];
state.selectedConversationId = "";
state.onGoingResult = "";
- state.isAgent = false;
+ state.selectedConversationHistory = [];
+ state.filesInDataSource = [];
+ },
+ setIsPending: (state, action: PayloadAction) => {
+ state.isPending = action.payload;
},
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);
+ state.selectedConversationHistory.push(action.payload);
},
- newConversation: (state) => {
+ newConversation: (state, action: PayloadAction) => {
state.selectedConversationId = "";
state.onGoingResult = "";
- state.isAgent = false;
+ state.selectedConversationHistory = [];
+
+ // full reset if true
+ if (action.payload) {
+ (state.sourceLinks = []), (state.sourceFiles = []);
+
+ // in case of upload / history conversation that clears model name, we want to reset to defaults
+ const currentType = state.type;
+ if (currentType) {
+ const approvedModel = state.models.find((item: Model) => item.types.includes(currentType));
+ if (approvedModel) {
+ state.model = approvedModel.model_name;
+ state.token = approvedModel.minToken;
+ state.temperature = 0.4;
+ }
+ }
+ }
},
- 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],
- });
+ updatePromptSettings: (state, action: PayloadAction) => {
+ state.model = action.payload.model;
+ state.token = action.payload.token;
+ state.temperature = action.payload.temperature;
+ state.type = action.payload.type;
},
setSelectedConversationId: (state, action: PayloadAction) => {
state.selectedConversationId = action.payload;
},
- addFileDataSource: (state, action: PayloadAction<{ id: string; source: string[]; type: 'Files' | 'URLs'; startTime: number }>) => {
- state.fileDataSources.push({
- id: action.payload.id,
- source: action.payload.source,
- type: action.payload.type,
- startTime: action.payload.startTime,
- status: 'pending',
- });
+ setSelectedConversationHistory: (state, action: PayloadAction) => {
+ state.selectedConversationHistory = action.payload;
+ },
+ setTemperature: (state, action: PayloadAction) => {
+ state.temperature = action.payload;
+ },
+ setToken: (state, action: PayloadAction) => {
+ state.token = action.payload;
+ },
+ setModel: (state, action: PayloadAction) => {
+ state.model = action.payload.model_name;
+ state.maxToken = action.payload.maxToken;
+ state.minToken = action.payload.minToken;
},
- clearFileDataSources: (state) => {
- state.fileDataSources = [];
+ setModelName: (state, action: PayloadAction) => {
+ state.model = action.payload;
},
- updateFileDataSourceStatus: (state, action: PayloadAction<{ id: string; status: 'pending' | 'uploading' | 'uploaded' | 'failed' }>) => {
- const fileDataSource = state.fileDataSources.find((item: FileDataSource) => item.id === action.payload.id);
- if (fileDataSource) {
- fileDataSource.status = action.payload.status;
+ setModels: (state, action: PayloadAction<[]>) => {
+ state.models = action.payload;
+ },
+ setUseCase: (state, action: PayloadAction) => {
+ state.useCase = action.payload;
+ },
+ setUseCases: (state, action: PayloadAction<[]>) => {
+ state.useCases = action.payload;
+ },
+ setType: (state, action: PayloadAction) => {
+ state.type = action.payload;
+
+ switch (action.payload) {
+ case "summary":
+ case "faq":
+ state.systemPrompt = "";
+ state.sourceType = "documents";
+ break;
+ case "chat":
+ case "code":
+ state.systemPrompt = config.defaultChatPrompt;
+ state.sourceFiles = [];
+ state.sourceLinks = [];
+ break;
}
+
+ let firstModel = state.models.find((model: Model) => model.types.includes(action.payload));
+ state.model = firstModel?.model_name || state.models[0].model_name;
+ },
+ setUploadInProgress: (state, action: PayloadAction) => {
+ state.uploadInProgress = action.payload;
+ },
+ setSourceLinks: (state, action: PayloadAction) => {
+ state.sourceLinks = action.payload;
+ },
+ setSourceFiles: (state, action: PayloadAction) => {
+ state.sourceFiles = action.payload;
+ },
+ setSourceType: (state, action: PayloadAction) => {
+ state.sourceType = action.payload;
+ },
+ setSystemPrompt: (state, action: PayloadAction) => {
+ state.systemPrompt = action.payload;
+ },
+ setAbortController: (state, action: PayloadAction) => {
+ state.abortController = action.payload;
+ },
+ abortStream: (state) => {
+ if (state.abortController) state.abortController.abort();
+
+ const m: Message = {
+ role: MessageRole.Assistant,
+ content: state.onGoingResult,
+ time: getCurrentTimeStamp().toString(),
+ };
+
+ // add last message before ending
+ state.selectedConversationHistory.push(m);
+ state.onGoingResult = "";
+ state.abortController = null;
},
- setIsAgent: (state, action: PayloadAction) => {
- state.isAgent = action.payload;
+ setDataSourceUrlStatus: (state, action: PayloadAction) => {
+ state.dataSourceUrlStatus = action.payload;
+ },
+ uploadChat: (state, action: PayloadAction) => {
+ state.selectedConversationHistory = action.payload.messages;
+ state.model = action.payload.model;
+ state.token = action.payload.token;
+ state.temperature = action.payload.temperature;
+ state.type = action.payload.type;
+ state.sourceFiles = []; // only chat can be uploaded, empty if set
+ state.sourceLinks = []; // only chat can be uploaded, empty if set
},
},
extraReducers(builder) {
builder.addCase(uploadFile.fulfilled, () => {
- notifications.update({
- id: "upload-file",
- message: "File Uploaded Successfully",
- loading: false,
- autoClose: 3000,
- });
+ notify("File Uploaded Successfully", NotificationSeverity.SUCCESS);
});
builder.addCase(uploadFile.rejected, () => {
- notifications.update({
- color: "red",
- id: "upload-file",
- message: "Failed to Upload file",
- loading: false,
- });
+ notify("Failed to Upload file", NotificationSeverity.ERROR);
});
- builder.addCase(submitDataSourceURL.fulfilled, () => {
- notifications.show({
- message: "Submitted Successfully",
- });
+ builder.addCase(submitDataSourceURL.fulfilled, (state) => {
+ notify("Submitted Successfully", NotificationSeverity.SUCCESS);
+ state.dataSourceUrlStatus = ""; // watching for pending only on front
});
- builder.addCase(submitDataSourceURL.rejected, () => {
- notifications.show({
- color: "red",
- message: "Submit Failed",
- });
+ builder.addCase(submitDataSourceURL.rejected, (state) => {
+ notify("Submit Failed", NotificationSeverity.ERROR);
+ state.dataSourceUrlStatus = ""; // watching for pending only on front
+ });
+ builder.addCase(deleteConversation.rejected, () => {
+ notify("Failed to Delete Conversation", NotificationSeverity.ERROR);
+ });
+ builder.addCase(getAllConversations.fulfilled, (state, action) => {
+ state.conversations = action.payload;
+ });
+ builder.addCase(getConversationHistory.fulfilled, (state, action) => {
+ state.selectedConversationHistory = action.payload;
+ });
+ builder.addCase(saveConversationtoDatabase.fulfilled, (state, action) => {
+ if (state.selectedConversationId == "") {
+ state.selectedConversationId = action.payload;
+ state.conversations.push({
+ id: action.payload,
+ first_query: state.selectedConversationHistory[1].content,
+ });
+ // Retain current query string in the URL
+ const currentQuery = window.location.search;
+ window.history.pushState({}, "", `/chat/${action.payload}${currentQuery}`);
+ }
+ });
+ builder.addCase(getAllFilesInDataSource.fulfilled, (state, action) => {
+ state.filesInDataSource = action.payload;
});
},
});
+export const getSupportedUseCases = createAsyncThunkWrapper(
+ "public/usecase_configs.json",
+ async (_: void, { getState }) => {
+ const response = await axiosClient.get("/usecase_configs.json");
+ store.dispatch(setUseCases(response.data));
+
+ // @ts-ignore
+ const state: RootState = getState();
+ const userAccess = state.userReducer.role;
+ const currentUseCase = state.conversationReducer.useCase;
+
+ // setDefault use case if not stored / already set by localStorage
+ if (!currentUseCase) {
+ const approvedAccess = response.data.find((item: UseCase) => item.access_level === userAccess);
+ if (approvedAccess) store.dispatch(setUseCase(approvedAccess));
+ }
+
+ return response.data;
+ },
+);
+
+export const getSupportedModels = createAsyncThunkWrapper(
+ "public/model_configs.json",
+ async (_: void, { getState }) => {
+ const response = await axiosClient.get("/model_configs.json");
+ store.dispatch(setModels(response.data));
+
+ // @ts-ignore
+ const state: RootState = getState();
+ const currentModel = state.conversationReducer.model;
+ const currentType = state.conversationReducer.type;
+
+ // setDefault use case if not stored / already set by localStorage
+ // TODO: revisit if type also gets stored and not defaulted on state
+ if (!currentModel && currentType) {
+ const approvedModel = response.data.find((item: Model) => item.types.includes(currentType));
+ if (approvedModel) store.dispatch(setModel(approvedModel));
+ }
+
+ return response.data;
+ },
+);
+
+export const getAllConversations = createAsyncThunkWrapper(
+ "conversation/getAllConversations",
+ async ({ user }: { user: string; }, {}) => {
+
+ //TODO: Add useCase
+ const response = await axiosClient.post(CHAT_HISTORY_GET, {
+ user,
+ });
+
+ console.log("getAllConversations response", response.data);
+
+ return response.data.reverse();
+ },
+);
+
+export const getConversationHistory = createAsyncThunkWrapper(
+ "conversation/getConversationHistory",
+ async ({ user, conversationId }: { user: string; conversationId: string }, {}) => {
+ const response = await axiosClient.post(CHAT_HISTORY_GET, {
+ user,
+ id: conversationId,
+ });
+ console.log("getAllConversations response", response.data);
+
+
+ // update settings for response settings modal
+ store.dispatch(
+ updatePromptSettings({
+ model: response.data.model,
+ token: response.data.max_tokens,
+ temperature: response.data.temperature,
+ type: response.data.request_type,
+ }),
+ );
+
+ return response.data.messages;
+ },
+);
+
export const submitDataSourceURL = createAsyncThunkWrapper(
"conversation/submitDataSourceURL",
async ({ link_list }: { link_list: string[] }, { dispatch }) => {
- const id = uuidv4();
- dispatch(updateFileDataSourceStatus({ id, status: 'uploading' }));
-
- try {
- const body = new FormData();
- body.append("link_list", JSON.stringify(link_list));
- const response = await client.post(`${DATA_PREP_URL}/ingest`, body);
- return response.data;
- } catch (error) {
- console.log("error", error);
- throw error;
- }
+ dispatch(setDataSourceUrlStatus("pending"));
+ const body = new FormData();
+ body.append("link_list", JSON.stringify(link_list));
+ const response = await axiosClient.post(DATA_PREP_INGEST_URL, body);
+ dispatch(getAllFilesInDataSource({ knowledgeBaseId: "default" }));
+ return response.data;
},
);
-export const uploadFile = createAsyncThunkWrapper("conversation/uploadFile", async ({ file }: { file: File }) => {
- try {
+export const getAllFilesInDataSource = createAsyncThunkWrapper(
+ "conversation/getAllFilesInDataSource",
+ async ({ knowledgeBaseId }: { knowledgeBaseId: string }, {}) => {
+ const body = {
+ };
+ const response = await axiosClient.post(DATA_PREP_GET_URL, body);
+ return response.data;
+ },
+);
+
+export const uploadFile = createAsyncThunkWrapper(
+ "conversation/uploadFile",
+ async ({ file }: { file: File }, { dispatch }) => {
const body = new FormData();
body.append("files", file);
+ const response = await axiosClient.post(DATA_PREP_INGEST_URL, body);
+ dispatch(getAllFilesInDataSource({ knowledgeBaseId: "default" }));
+ return response.data;
+ },
+);
- notifications.show({
- id: "upload-file",
- message: "uploading File",
- loading: true,
+export const deleteMultipleInDataSource = createAsyncThunkWrapper(
+ "conversation/deleteConversations",
+ async ({ files }: { files: string[] }, { dispatch }) => {
+ const promises = files.map((file) =>
+ axiosClient
+ .post(DATA_PREP_DELETE_URL, {
+ // file_path: file.split("_")[1],
+ file_path: file, // assuming file is the full path
+ })
+ .then((response) => {
+ return response.data;
+ })
+ .catch((err) => {
+ notify("Error deleting file", NotificationSeverity.ERROR);
+ console.error(`Error deleting file`, file, err);
+ }),
+ );
+
+ await Promise.all(promises)
+ .then(() => {
+ notify("Files deleted successfully", NotificationSeverity.SUCCESS);
+ })
+ .catch((err) => {
+ notify("Error deleting on or more of your files", NotificationSeverity.ERROR);
+ console.error("Error deleting on or more of your files", err);
+ })
+ .finally(() => {
+ dispatch(getAllFilesInDataSource({ knowledgeBaseId: "default" }));
+ });
+ },
+);
+
+export const deleteInDataSource = createAsyncThunkWrapper(
+ "conversation/deleteInDataSource",
+ async ({ file }: { file: any }, { dispatch }) => {
+ const response = await axiosClient.post(DATA_PREP_DELETE_URL, {
+ file_path: file,
});
- const response = await client.post(`${DATA_PREP_URL}/ingest`, body);
+ dispatch(getAllFilesInDataSource({ knowledgeBaseId: "default" }));
return response.data;
- } catch (error) {
- throw error;
- }
-});
+ },
+);
-export const {
- logout,
- setOnGoingResult,
- newConversation,
- addMessageToMessages,
- setSelectedConversationId,
- createNewConversation,
- addFileDataSource,
- updateFileDataSourceStatus,
- clearFileDataSources,
- setIsAgent,
-} = ConversationSlice.actions;
+export const saveConversationtoDatabase = createAsyncThunkWrapper(
+ "conversation/saveConversationtoDatabase",
+ async ({ conversation }: { conversation: Conversation }, { dispatch, getState }) => {
+ // @ts-ignore
+ const state: RootState = getState();
+ const selectedConversationHistory = state.conversationReducer.selectedConversationHistory;
+
+ //TODO: if we end up with a systemPrompt for code change this
+ const firstMessageIndex = state.conversationReducer.type === "code" ? 0 : 1;
+
+ const response = await axiosClient.post(CHAT_HISTORY_CREATE, {
+ data: {
+ user: state.userReducer.name,
+ messages: selectedConversationHistory,
+ time: getCurrentTimeStamp().toString(),
+ model: state.conversationReducer.model,
+ temperature: state.conversationReducer.temperature,
+ max_tokens: state.conversationReducer.token,
+ request_type: state.conversationReducer.type,
+ },
+ id: conversation.id == "" ? null : conversation.id,
+ first_query: selectedConversationHistory[firstMessageIndex].content,
+ });
-export const conversationSelector = (state: RootState) => state.conversationReducer;
-export const fileDataSourcesSelector = (state: RootState) => state.conversationReducer.fileDataSources;
-export const isAgentSelector = (state: RootState) => state.conversationReducer.isAgent;
+ dispatch(
+ getAllConversations({
+ user: state.userReducer.name,
+ // useCase: state.conversationReducer.useCase,
+ }),
+ );
+ return response.data;
+ },
+);
-export default ConversationSlice.reducer;
+export const deleteConversations = createAsyncThunkWrapper(
+ "conversation/deleteConversations",
+ async (
+ { user, conversationIds, useCase }: { user: string; conversationIds: string[]; useCase: string },
+ { dispatch },
+ ) => {
+ const promises = conversationIds.map((id) =>
+ axiosClient
+ .post(CHAT_HISTORY_DELETE, {
+ user,
+ id: id,
+ })
+ .then((response) => {
+ return response.data;
+ })
+ .catch((err) => {
+ notify("Error deleting conversation", NotificationSeverity.ERROR);
+ console.error(`Error deleting conversation ${id}`, err);
+ }),
+ );
+
+ await Promise.all(promises)
+ .then(() => {
+ notify("Conversations deleted successfully", NotificationSeverity.SUCCESS);
+ })
+ .catch((err) => {
+ notify("Error deleting on or more of your conversations", NotificationSeverity.ERROR);
+ console.error("Error deleting on or more of your conversations", err);
+ })
+ .finally(() => {
+ // dispatch(getAllConversations({ user, useCase }));
+ dispatch(getAllConversations({ user}));
-// let source: string[] = [];
-// let content: any[] = [];
-// let currentTool: string = "";
-let isAgent: boolean = false;
-let currentAgentSteps: AgentStep[] = []; // Temporary storage for steps during streaming
+ });
+ },
+);
+
+export const deleteConversation = createAsyncThunkWrapper(
+ "conversation/delete",
+ async (
+ { user, conversationId, useCase }: { user: string; conversationId: string; useCase: string },
+ { dispatch },
+ ) => {
+ const response = await axiosClient.post(CHAT_HISTORY_DELETE, {
+ user,
+ id: conversationId,
+ });
+
+ dispatch(newConversation(false));
+ // dispatch(getAllConversations({ user, useCase }));
+ dispatch(getAllConversations({ user }));
+
+ return response.data;
+ },
+);
export const doConversation = (conversationRequest: ConversationRequest) => {
- const { conversationId, userPrompt, messages, model, maxTokens, temperature } = conversationRequest;
- // const [isInThink, setIsInThink] = useState(false);
- if (!conversationId) {
- const id = uuidv4();
- store.dispatch(
- createNewConversation({
- title: userPrompt.content,
- id,
- message: userPrompt,
- }),
- );
- store.dispatch(setSelectedConversationId(id));
- } else {
- store.dispatch(addMessageToMessages(userPrompt));
- }
+ store.dispatch(setIsPending(true));
+
+ const { conversationId, userPrompt, messages, model, token, temperature, type } = conversationRequest;
+
+ // TODO: MAYBE... check first message if 'system' already exists... on dev during page edits the
+ // hot module reloads and instantly adds more system messages to the total messages
+ if (messages.length === 1) store.dispatch(addMessageToMessages(messages[0])); // do not re-add system prompt
+ store.dispatch(addMessageToMessages(userPrompt));
- const userPromptWithoutTime = {
+ const userPromptWithTime = {
role: userPrompt.role,
content: userPrompt.content,
+ time: getCurrentTimeStamp().toString(),
};
+
const body = {
- messages: [...messages, userPromptWithoutTime],
+ messages: [...messages, userPromptWithTime],
model: model,
- max_tokens: maxTokens,
+ max_tokens: token,
temperature: temperature,
stream: true,
+ // thread_id: "123344", // if conversationId is empty, it will be created
+ };
+
+ eventStream(type, body, conversationId);
+};
+
+
+export const doSummaryFaq = (summaryFaqRequest: SummaryFaqRequest) => {
+ store.dispatch(setIsPending(true));
+
+ const { conversationId, model, token, temperature, type, messages, files, userPrompt } = summaryFaqRequest;
+
+ const postWithFiles = files && files.length > 0;
+ console.log ("files", files)
+ const allowedFileTypes = {
+ audio: ["audio/mpeg", "audio/wav", "audio/ogg"],
+ video: ["video/mp4", "video/webm", "video/avi"],
+ documents: ["application/pdf", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
+ txt: ["text/plain"],
};
- let result = ""; // Accumulates the final answer
- let thinkBuffer = ""; // Accumulates data for think blocks
- let postThinkBuffer = ""; // Accumulates plain text after last
- let isInThink = false; // Tracks if we're inside a block
- // setIsInThinkMode(false); // Reset the think mode state
- currentAgentSteps = []; // Reset steps for this message
- isAgent = false; // Tracks if this is an agent message (set once, never reset)
- let isMessageDispatched = false; // Tracks if the final message has been dispatched
+
+
+ const body: any = {};
+ const formData = new FormData();
+
+ store.dispatch(addMessageToMessages(userPrompt));
+
+ if (postWithFiles) {
+ formData.append("messages", "");
+ formData.append("model", model);
+ formData.append("max_tokens", token.toString());
+ formData.append("temperature", temperature.toString());
+
+ files.forEach((file) => {
+ console.log("file", file);
+ console.log("file type", file.file.type);
+ console.log ("is audio", allowedFileTypes.audio.includes(file.file.type));
+ const fileType = file.file.type;
+ allowedFileTypes.audio.includes(fileType)? formData.append("type", "audio"):
+ allowedFileTypes.video.includes(fileType) ? formData.append("type", "video") :
+ formData.append("type", "text")
+ });
+ files.forEach((file) => {
+ formData.append("files", file.file);
+ });
+ console.log("FormData contents:");
+ Array.from(formData.entries()).forEach(([key, value]) => {
+ console.log(`${key}: ${value instanceof File ? value.name : value}`);
+ });
+ console.log ("urlMap[type]", urlMap[type]);
+ formDataEventStream(urlMap[type], formData);
+ } else {
+ body.messages = messages;
+ body.model = model;
+ (body.max_tokens = token), (body.temperature = temperature);
+ body.type = "text";
+
+ eventStream(type, body, conversationId);
+ }
+};
+
+export const doCodeGen = (codeRequest: CodeRequest) => {
+ store.dispatch(setIsPending(true));
+
+ const { conversationId, userPrompt, model, token, temperature, type } = codeRequest;
+
+ store.dispatch(addMessageToMessages(userPrompt));
+
+ const body = {
+ messages: userPrompt.content,
+ model: model, //'meta-llama/Llama-3.3-70B-Instruct',
+ max_tokens: token,
+ temperature: temperature,
+ };
+
+ eventStream(type, body, conversationId);
+};
+
+const eventStream = (type: string, body: any, conversationId: string = "") => {
+ const abortController = new AbortController();
+ store.dispatch(setAbortController(abortController));
+ const signal = abortController.signal;
+
+ let result = "";
try {
- console.log("CHAT_QNA_URL", CHAT_QNA_URL);
- fetchEventSource(CHAT_QNA_URL, {
+ fetchEventSource(urlMap[type], {
method: "POST",
+ body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
},
- body: JSON.stringify(body),
+ signal,
openWhenHidden: true,
async onopen(response) {
if (response.ok) {
+ store.dispatch(setIsPending(false));
return;
} else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
const e = await response.json();
@@ -235,246 +661,227 @@ export const doConversation = (conversationRequest: ConversationRequest) => {
throw Error(e.error.message);
} else {
console.log("error", response);
+ notify("Error in opening stream", NotificationSeverity.ERROR);
}
},
onmessage(msg) {
- if (msg?.data === "[DONE]") {
- // Stream is done, finalize the message
- if (isAgent && thinkBuffer) {
- processThinkContent(thinkBuffer);
- }
- if (!isMessageDispatched) {
- // Use postThinkBuffer as the final answer if present
- if (postThinkBuffer.trim()) {
- result = postThinkBuffer.trim();
+ if (msg?.data != "[DONE]") {
+ // console.log("msg", msg.data);
+ try {
+ if (type === "code") {
+ const parsedData = JSON.parse(msg.data);
+ result += parsedData.choices[0].delta.content;
+ store.dispatch(setOnGoingResult(result));
}
- store.dispatch(setOnGoingResult(result));
- store.dispatch(
- addMessageToMessages({
- role: MessageRole.Assistant,
- content: result,
- time: getCurrentTimeStamp(),
- agentSteps: isAgent ? [...currentAgentSteps] : [],
- }),
- );
- isMessageDispatched = true;
- }
- currentAgentSteps = []; // Clear steps for next message
- postThinkBuffer = "";
- return;
- }
+ if (type === "chat") {
+ let parsed = false;
- const data = msg?.data || "";
+ try {
+ const res = JSON.parse(msg.data);
+ const data = res.choices[0].delta.content;
- // Handle think blocks and non-think content
- if (data.includes("")) {
- if (!isAgent) {
- isAgent = true;
- store.dispatch(setIsAgent(true));
- }
- // Split on to handle content before it
- const parts = data.split("");
- for (let i = 0; i < parts.length; i++) {
- const part = parts[i];
- if (i === 0 && !isInThink && part) {
- // Content before (non-think)
- postThinkBuffer += part;
- if (isAgent) {
- store.dispatch(setOnGoingResult(postThinkBuffer));
- } else {
- result += part;
+ result += data;
store.dispatch(setOnGoingResult(result));
+ parsed = true;
+ } catch (e) {
+ // If JSON parsing fails, we will try to extract the text from the message
+ // This is a fallback for cases where the API returns a non-JSON response
+ // Example: msg.data = "data: b'Hello, world!'"
+ // We will extract the text between b' and ''
+ // Note: This is a workaround and should be used with caution, as it assumes a specific format
+ // and may not work for all cases.
+ const match = msg.data.match(/data:\s*b'([^']*)'/);
+ if (match && match[1] !== "") {
+ const extractedText = match[1];
+ result += extractedText;
+ store.dispatch(setOnGoingResult(result));
+ } else {
+ result += msg.data; // Fallback to adding the raw data
+ store.dispatch(setOnGoingResult(result));
+ }
+ }
+
+ // Fallback if JSON wasn't parsed
+ if (!parsed) {
+ const match = msg.data.match(/b'([^']*)'/);
+ if (match && match[1] !== "") {
+ const extractedText = match[1];
+ result += extractedText;
+ store.dispatch(setOnGoingResult(result));
+ }
}
} else {
- // Start or continue think block
- isInThink = true;
- // setIsInThinkMode(true); // Set think mode state
- thinkBuffer += part;
- // Check if part contains
- if (part.includes(" ")) {
- const [thinkContent, afterThink] = part.split(" ", 2);
- thinkBuffer = thinkBuffer.substring(0, thinkBuffer.indexOf(part)) + thinkContent;
- processThinkContent(thinkBuffer);
- thinkBuffer = "";
- isInThink = false;
- // setIsInThinkMode(false); // Reset think mode state
- if (afterThink) {
- // Handle content after as non-think
- if (!afterThink.includes("")) {
- postThinkBuffer += afterThink;
- store.dispatch(setOnGoingResult(postThinkBuffer));
- } else {
- thinkBuffer = afterThink;
- isInThink = true;
- // setIsInThinkMode(true); // Set think mode state
+ //text summary/faq for data: "ops string"
+ const res = JSON.parse(msg.data); // Parse valid JSON
+ const logs = res.ops;
+ logs.forEach((log: { op: string; path: string; value: string }) => {
+ if (log.op === "add") {
+ if (
+ log.value !== "" &&
+ log.path.endsWith("/streamed_output/-") &&
+ log.path.length > "/streamed_output/-".length
+ ) {
+ result += log.value;
+ if (log.value) store.dispatch(setOnGoingResult(result));
}
}
- }
- }
- }
- } else if (isInThink) {
- // Accumulate within think block
- thinkBuffer += data;
- if (data.includes(" ")) {
- const [thinkContent, afterThink] = data.split(" ", 2);
- thinkBuffer = thinkBuffer.substring(0, thinkBuffer.lastIndexOf(data)) + thinkContent;
- processThinkContent(thinkBuffer);
- thinkBuffer = "";
- isInThink = false;
- // setIsInThinkMode(false); // Reset think mode state
- if (afterThink) {
- // Handle content after
- if (!afterThink.includes("")) {
- postThinkBuffer += afterThink;
- store.dispatch(setOnGoingResult(postThinkBuffer));
- } else {
- thinkBuffer = afterThink;
- isInThink = true;
- // setIsInThinkMode(true); // Set think mode state
- }
+ });
}
- }
- } else {
- // Non-agent or post-think plain text
- if (isAgent) {
- postThinkBuffer += data;
- store.dispatch(setOnGoingResult(postThinkBuffer));
- } else {
- result += data;
- store.dispatch(setOnGoingResult(result));
+ } catch (e) {
+ console.log("something wrong in msg", e);
+ notify("Error in message response", NotificationSeverity.ERROR);
+ throw e;
}
}
},
onerror(err) {
console.log("error", err);
store.dispatch(setOnGoingResult(""));
+ notify("Error streaming response", NotificationSeverity.ERROR);
throw err;
},
onclose() {
- if (!isMessageDispatched && (result || postThinkBuffer || (isAgent && currentAgentSteps.length > 0))) {
- // Use postThinkBuffer as the final answer if present
- if (postThinkBuffer.trim()) {
- result = postThinkBuffer.trim();
- }
- store.dispatch(setOnGoingResult(result));
+ const m: Message = {
+ role: MessageRole.Assistant,
+ content: result,
+ time: getCurrentTimeStamp().toString(),
+ };
+
+ store.dispatch(setOnGoingResult(""));
+ store.dispatch(setAbortController(null));
+ store.dispatch(addMessageToMessages(m));
+
+ if (type === "chat") {
store.dispatch(
- addMessageToMessages({
- role: MessageRole.Assistant,
- content: result,
- time: getCurrentTimeStamp(),
- agentSteps: isAgent ? [...currentAgentSteps] : [],
+ saveConversationtoDatabase({
+ conversation: {
+ id: conversationId,
+ },
}),
);
- isMessageDispatched = true;
}
- store.dispatch(setOnGoingResult(""));
- currentAgentSteps = [];
- postThinkBuffer = "";
},
});
} catch (err) {
console.log(err);
}
+};
- // Helper function to process content within tags
- function processThinkContent(content: string) {
- content = content.trim();
- if (!content) return;
-
- const toolCallRegex = /TOOL CALL: (\{.*?\})/g;
- const finalAnswerRegex = /FINAL ANSWER: (\{.*?\})/;
- let stepContent: string[] = []; // Collect all reasoning for this think block
- let tool: string = "reasoning"; // Default tool
- let source: string[] = []; // Tool output
-
- // Split content by final answer (if present)
- let remainingContent = content;
- const finalAnswerMatch = content.match(finalAnswerRegex);
- if (finalAnswerMatch) {
- try {
- const finalAnswer = JSON.parse(finalAnswerMatch[1].replace("FINAL ANSWER: ", ""));
- if (finalAnswer.answer) {
- result = finalAnswer.answer;
- }
- remainingContent = content.split(finalAnswerMatch[0])[0].trim(); // Content before FINAL ANSWER
- tool = "final_answer";
- } catch (e) {
- console.error("Error parsing final answer:", finalAnswerMatch[1], e);
- }
+const formDataEventStream = async (url: string, formData: any) => {
+ const abortController = new AbortController();
+ store.dispatch(setAbortController(abortController));
+ const signal = abortController.signal;
+
+ let result = "";
+
+ try {
+ const response = await fetch(url, {
+ method: "POST",
+ body: formData,
+ signal,
+ });
+
+ if (!response.ok) {
+ throw new Error("Network response was not ok");
}
- // Process tool calls within the remaining content
- const toolMatches = remainingContent.match(toolCallRegex) || [];
- let currentContent = remainingContent;
+ if (response && response.body) {
+ store.dispatch(setIsPending(false));
- if (toolMatches.length > 0) {
- // Handle content before and after tool calls
- toolMatches.forEach((toolCallStr) => {
- const [beforeTool, afterTool] = currentContent.split(toolCallStr, 2);
- if (beforeTool.trim()) {
- stepContent.push(beforeTool.trim());
- }
+ const reader = response.body.getReader();
- try {
- // Attempt to parse the tool call JSON
- let toolCall;
- try {
- toolCall = JSON.parse(toolCallStr.replace("TOOL CALL: ", ""));
- } catch (e) {
- console.error("Error parsing tool call JSON, attempting recovery:", toolCallStr, e);
- // Attempt to extract tool and content manually
- const toolMatch = toolCallStr.match(/"tool":\s*"([^"]+)"/);
- const contentMatch = toolCallStr.match(/"tool_content":\s*\["([^"]+)"\]/);
- toolCall = {
- tool: toolMatch ? toolMatch[1] : "unknown",
- args: {
- tool_content: contentMatch ? [contentMatch[1]] : [],
- },
- };
- }
+ // Read the stream in chunks
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) {
+ break;
+ }
- tool = toolCall.tool || tool;
- source = toolCall.args?.tool_content || source;
+ // Process the chunk of data (e.g., convert to text)
+ const textChunk = new TextDecoder().decode(value).trim();
+
+ // sometimes double lines return
+ const lines = textChunk.split("\n");
+
+ for (let line of lines) {
+ if (line.startsWith("data:")) {
+ const jsonStr = line.replace(/^data:\s*/, ""); // Remove "data: "
+
+ if (jsonStr !== "[DONE]") {
+ try {
+ // API Response for final output regularly returns incomplete JSON,
+ // due to final response containing source summary content and exceeding
+ // token limit in the response. We don't use it anyway so don't parse it.
+ if (!jsonStr.includes('"path":"/streamed_output/-"')) {
+ const res = JSON.parse(jsonStr); // Parse valid JSON
+
+ const logs = res.ops;
+ logs.forEach((log: { op: string; path: string; value: string }) => {
+ if (log.op === "add") {
+ if (
+ log.value !== "" &&
+ log.path.endsWith("/streamed_output/-") &&
+ log.path.length > "/streamed_output/-".length
+ ) {
+ result += log.value;
+ if (log.value) store.dispatch(setOnGoingResult(result));
+ }
+ }
+ });
+ }
+ } catch (error) {
+ console.warn("Error parsing JSON:", error, "Raw Data:", jsonStr);
+ }
+ } else {
+ const m: Message = {
+ role: MessageRole.Assistant,
+ content: result,
+ time: getCurrentTimeStamp().toString(),
+ };
- // Clean up afterTool to remove invalid JSON fragments
- if (afterTool.trim()) {
- // Remove any trailing malformed JSON (e.g., "Chinook?"}})
- const cleanAfterTool = afterTool.replace(/[\s\S]*?(\}\s*)$/, "").trim();
- if (cleanAfterTool) {
- stepContent.push(cleanAfterTool);
+ store.dispatch(setOnGoingResult(""));
+ store.dispatch(addMessageToMessages(m));
+ store.dispatch(setAbortController(null));
}
}
-
- } catch (e) {
- console.error("Failed to process tool call:", toolCallStr, e);
- stepContent.push(`[Error parsing tool call: ${toolCallStr}]`);
}
-
- currentContent = afterTool;
- });
- } else {
- // No tool calls, treat as reasoning
- if (remainingContent.trim()) {
- stepContent.push(remainingContent.trim());
}
}
-
- // Add the step for this think block
- if (stepContent.length > 0 || source.length > 0) {
- currentAgentSteps.push({
- tool,
- content: stepContent,
- source,
- });
- }
-
- // Update onGoingResult to trigger UI update with latest steps
- if (isAgent) {
- const latestContent = currentAgentSteps.flatMap(step => step.content).join(" ");
- const latestSource = source.length > 0 ? source.join(" ") : "";
- store.dispatch(setOnGoingResult(latestContent + (latestSource ? " " + latestSource : "") + (postThinkBuffer ? " " + postThinkBuffer : "")));
+ } catch (error: any) {
+ if (error.name === "AbortError") {
+ console.log("Fetch aborted successfully.");
+ } else {
+ console.error("Fetch error:", error);
}
}
};
-export const getCurrentAgentSteps = () => currentAgentSteps; // Export for use in Conversation.tsx
\ No newline at end of file
+export const {
+ logout,
+ setOnGoingResult,
+ setIsPending,
+ newConversation,
+ updatePromptSettings,
+ addMessageToMessages,
+ setSelectedConversationId,
+ setSelectedConversationHistory,
+ setTemperature,
+ setToken,
+ setModel,
+ setModelName,
+ setModels,
+ setType,
+ setUploadInProgress,
+ setSourceLinks,
+ setSourceFiles,
+ setSourceType,
+ setUseCase,
+ setUseCases,
+ setSystemPrompt,
+ setAbortController,
+ abortStream,
+ setDataSourceUrlStatus,
+ uploadChat,
+} = ConversationSlice.actions;
+export const conversationSelector = (state: RootState) => state.conversationReducer;
+export default ConversationSlice.reducer;
diff --git a/app-frontend/react/src/redux/Prompt/PromptSlice.ts b/app-frontend/react/src/redux/Prompt/PromptSlice.ts
new file mode 100644
index 0000000..43f6732
--- /dev/null
+++ b/app-frontend/react/src/redux/Prompt/PromptSlice.ts
@@ -0,0 +1,96 @@
+// Copyright (C) 2025 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+import { createAsyncThunkWrapper } from "@redux/thunkUtil";
+import { RootState } from "@redux/store";
+import { PROMPT_MANAGER_CREATE, PROMPT_MANAGER_GET, PROMPT_MANAGER_DELETE } from "@root/config";
+import { NotificationSeverity, notify } from "@components/Notification/Notification";
+import { axiosClient } from "../../utils/navigationAndAxiosWithQuery";
+
+type promptReducer = {
+ prompts: Prompt[];
+};
+
+export type Prompt = {
+ id: string;
+ prompt_text: string;
+ user: string;
+ type: string;
+};
+
+const initialState: promptReducer = {
+ prompts: [],
+};
+
+export const PromptSlice = createSlice({
+ name: "Prompts",
+ initialState,
+ reducers: {
+ clearPrompts: (state) => {
+ state.prompts = [];
+ },
+ },
+ extraReducers(builder) {
+ builder.addCase(getPrompts.fulfilled, (state, action: PayloadAction) => {
+ state.prompts = action.payload;
+ });
+ builder.addCase(addPrompt.fulfilled, () => {
+ notify("Prompt added Successfully", NotificationSeverity.SUCCESS);
+ });
+ builder.addCase(deletePrompt.fulfilled, () => {
+ notify("Prompt deleted Successfully", NotificationSeverity.SUCCESS);
+ });
+ },
+});
+
+export const { clearPrompts } = PromptSlice.actions;
+export const promptSelector = (state: RootState) => state.promptReducer;
+export default PromptSlice.reducer;
+
+export const getPrompts = createAsyncThunkWrapper("prompts/getPrompts", async (_: void, { getState }) => {
+ // @ts-ignore
+ const state: RootState = getState();
+ const response = await axiosClient.post(PROMPT_MANAGER_GET, {
+ user: state.userReducer.name,
+ });
+ return response.data;
+});
+
+export const addPrompt = createAsyncThunkWrapper(
+ "prompts/addPrompt",
+ async ({ promptText }: { promptText: string }, { dispatch, getState }) => {
+ // @ts-ignore
+ const state: RootState = getState();
+ const response = await axiosClient.post(PROMPT_MANAGER_CREATE, {
+ prompt_text: promptText,
+ user: state.userReducer.name,
+ //TODO: Would be nice to support type to set prompts for each
+ // type: state.conversationReducer.type // TODO: this might be crashing chatqna endpoint?
+ });
+
+ dispatch(getPrompts());
+
+ return response.data;
+ },
+);
+
+//TODO delete prompt doesn't actually work, but responds 200
+export const deletePrompt = createAsyncThunkWrapper(
+ "prompts/deletePrompt",
+ async ({ promptId, promptText }: { promptId: string; promptText: string }, { dispatch, getState }) => {
+ // @ts-ignore
+ const state: RootState = getState();
+ const user = state.userReducer.name;
+
+ const response = await axiosClient.post(PROMPT_MANAGER_DELETE, {
+ user: user,
+ prompt_id: promptId,
+ prompt_text: promptText,
+ });
+
+ dispatch(getPrompts());
+
+ return response.data;
+ },
+);
diff --git a/app-frontend/react/src/redux/User/user.d.ts b/app-frontend/react/src/redux/User/user.d.ts
index 69c4db4..25b2e6b 100644
--- a/app-frontend/react/src/redux/User/user.d.ts
+++ b/app-frontend/react/src/redux/User/user.d.ts
@@ -1,6 +1,8 @@
-// Copyright (C) 2024 Intel Corporation
+// Copyright (C) 2025 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
export interface User {
- name: string | null;
+ name: string;
+ isAuthenticated: boolean;
+ role: "Admin" | "User";
}
diff --git a/app-frontend/react/src/redux/User/userSlice.ts b/app-frontend/react/src/redux/User/userSlice.ts
index 48d22fe..8dd7d23 100644
--- a/app-frontend/react/src/redux/User/userSlice.ts
+++ b/app-frontend/react/src/redux/User/userSlice.ts
@@ -1,23 +1,27 @@
-// Copyright (C) 2024 Intel Corporation
+// Copyright (C) 2025 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
-import { RootState } from "../store";
+import { RootState } from "@redux/store";
import { User } from "./user";
const initialState: User = {
- name: localStorage.getItem("user"),
+ name: "",
+ isAuthenticated: false,
+ role: "User",
};
export const userSlice = createSlice({
- name: "user",
+ name: "init user",
initialState,
reducers: {
- setUser: (state, action: PayloadAction) => {
- state.name = action.payload;
+ setUser: (state, action: PayloadAction) => {
+ state.name = action.payload.name;
+ state.isAuthenticated = action.payload.isAuthenticated;
+ state.role = action.payload.role;
},
removeUser: (state) => {
- state.name = null;
+ state.name = "";
},
},
});
diff --git a/app-frontend/react/src/redux/store.ts b/app-frontend/react/src/redux/store.ts
index 3a4e142..5de6ac7 100644
--- a/app-frontend/react/src/redux/store.ts
+++ b/app-frontend/react/src/redux/store.ts
@@ -1,64 +1,47 @@
+// Copyright (C) 2025 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 sandboxReducer from "./Sandbox/SandboxSlice";
+import userReducer from "@redux/User/userSlice";
+import conversationReducer from "@redux/Conversation/ConversationSlice";
+import promptReducer from "@redux/Prompt/PromptSlice";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
-import { APP_UUID } from "../config";
-
-function getBucketKey() {
- const url = new URL(window.location.href);
- const query = url.search;
- return `${query}_${APP_UUID}`;
-}
-
-function saveToLocalStorage(state: ReturnType) {
- try {
- const bucketKey = getBucketKey();
- const serialState = JSON.stringify(state);
- localStorage.setItem(`reduxStore_${bucketKey}`, serialState);
- } catch (e) {
- console.warn("Could not save state to localStorage:", e);
- }
-}
-
-function loadFromLocalStorage() {
- try {
- const bucketKey = getBucketKey();
- const serialisedState = localStorage.getItem(`reduxStore_${bucketKey}`);
- if (serialisedState === null) return undefined;
- return JSON.parse(serialisedState);
- } catch (e) {
- console.warn("Could not load state from localStorage:", e);
- return undefined;
- }
-}
export const store = configureStore({
reducer: combineReducers({
userReducer,
conversationReducer,
- // sandboxReducer,
+ promptReducer,
}),
devTools: import.meta.env.PROD || true,
- preloadedState: loadFromLocalStorage(),
+ // preloadedState: loadFromLocalStorage(),
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}),
});
-// Remove Redux state for the specific bucket key
-export function clearLocalStorageBucket() {
- try {
- const bucketKey = getBucketKey();
- localStorage.removeItem(`reduxStore_${bucketKey}`);
- } catch (e) {
- console.warn("Could not clear localStorage bucket:", e);
- }
-}
-
-store.subscribe(() => saveToLocalStorage(store.getState()));
-
+// 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()));
export default store;
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType;
diff --git a/app-frontend/react/src/redux/thunkUtil.ts b/app-frontend/react/src/redux/thunkUtil.ts
index 5df362f..8db3b30 100644
--- a/app-frontend/react/src/redux/thunkUtil.ts
+++ b/app-frontend/react/src/redux/thunkUtil.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Intel Corporation
+// Copyright (C) 2025 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
import { createAsyncThunk, AsyncThunkPayloadCreator, AsyncThunk } from "@reduxjs/toolkit";
diff --git a/app-frontend/react/src/shared/ActionButtons.tsx b/app-frontend/react/src/shared/ActionButtons.tsx
new file mode 100644
index 0000000..55abeed
--- /dev/null
+++ b/app-frontend/react/src/shared/ActionButtons.tsx
@@ -0,0 +1,94 @@
+import { Button, styled } from "@mui/material";
+
+const TextOnlyStyle = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.actionButtons.text,
+}));
+
+const DeleteStyle = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.actionButtons.delete,
+}));
+
+const SolidStyle = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.actionButtons.solid,
+}));
+
+const OutlineStyle = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.actionButtons.outline,
+}));
+
+type ButtonProps = {
+ onClick: (value: boolean) => void;
+ children: React.ReactNode | React.ReactNode[];
+ disabled?: boolean;
+ className?: string;
+};
+
+const TextButton: React.FC = ({
+ onClick,
+ children,
+ disabled = false,
+ className,
+}) => {
+ return (
+ onClick(true)}
+ className={className}
+ >
+ {children}
+
+ );
+};
+
+const DeleteButton: React.FC = ({
+ onClick,
+ children,
+ disabled = false,
+ className,
+}) => {
+ return (
+ onClick(true)}
+ className={className}
+ >
+ {children}
+
+ );
+};
+
+const SolidButton: React.FC = ({
+ onClick,
+ children,
+ disabled = false,
+ className,
+}) => {
+ return (
+ onClick(true)}
+ className={className}
+ >
+ {children}
+
+ );
+};
+
+const OutlineButton: React.FC = ({
+ onClick,
+ children,
+ disabled = false,
+ className,
+}) => {
+ return (
+ onClick(true)}
+ className={className}
+ >
+ {children}
+
+ );
+};
+
+export { TextButton, DeleteButton, SolidButton, OutlineButton };
diff --git a/app-frontend/react/src/shared/ModalBox/Modal.module.scss b/app-frontend/react/src/shared/ModalBox/Modal.module.scss
new file mode 100644
index 0000000..ae9b7d0
--- /dev/null
+++ b/app-frontend/react/src/shared/ModalBox/Modal.module.scss
@@ -0,0 +1,50 @@
+.modal {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ max-width: 400px;
+ width: 100%;
+ padding: 0;
+ min-width: 300px;
+ z-index: 9999;
+
+ :global {
+ #modal-modal-title {
+ padding: 0.75rem 1rem;
+ font-weight: 600;
+ font-size: 0.8rem;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ #modal-modal-description {
+ padding: 1.5rem 1rem 1rem;
+ margin-top: -1rem;
+
+ .MuiFormControlLabel-label,
+ .MuiTypography-root {
+ font-weight: 300;
+ font-size: 0.8rem;
+ margin-top: 0.5rem;
+ }
+
+ .MuiBox-root {
+ align-items: flex-start;
+ }
+
+ .MuiButton-root {
+ padding: 5px 10px;
+
+ + .MuiButton-root {
+ margin-left: 0.5rem;
+ }
+ }
+ }
+ button {
+ padding: 0;
+ }
+ }
+}
diff --git a/app-frontend/react/src/shared/ModalBox/ModalBox.tsx b/app-frontend/react/src/shared/ModalBox/ModalBox.tsx
new file mode 100644
index 0000000..0f3c9b9
--- /dev/null
+++ b/app-frontend/react/src/shared/ModalBox/ModalBox.tsx
@@ -0,0 +1,29 @@
+import { Modal, styled } from "@mui/material";
+
+import styles from "./Modal.module.scss";
+
+const StyledModalBox = styled("div")(({ theme }) => ({
+ ...theme.customStyles.settingsModal,
+}));
+
+const ModalBox: React.FC<{
+ children: React.ReactNode;
+ open?: boolean;
+ onClose?: () => void;
+}> = ({ children, open = true, onClose }) => {
+ let props: any = {};
+ if (onClose) props.onClose = onClose;
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default ModalBox;
diff --git a/app-frontend/react/src/styles/components/_sidebar.scss b/app-frontend/react/src/styles/components/_sidebar.scss
deleted file mode 100644
index 23018ee..0000000
--- a/app-frontend/react/src/styles/components/_sidebar.scss
+++ /dev/null
@@ -1,8 +0,0 @@
-// 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/app-frontend/react/src/styles/components/content.scss b/app-frontend/react/src/styles/components/content.scss
deleted file mode 100644
index 9a230f2..0000000
--- a/app-frontend/react/src/styles/components/content.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-@mixin textWrapEllipsis {
- text-overflow: ellipsis;
- white-space: nowrap;
- overflow: hidden;
-}
diff --git a/app-frontend/react/src/styles/components/context.module.scss b/app-frontend/react/src/styles/components/context.module.scss
deleted file mode 100644
index 17f37ba..0000000
--- a/app-frontend/react/src/styles/components/context.module.scss
+++ /dev/null
@@ -1,67 +0,0 @@
-@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/app-frontend/react/src/styles/layout/_basics.scss b/app-frontend/react/src/styles/layout/_basics.scss
deleted file mode 100644
index d11b1ef..0000000
--- a/app-frontend/react/src/styles/layout/_basics.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-@mixin absolutes {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
-}
diff --git a/app-frontend/react/src/styles/layout/_flex.scss b/app-frontend/react/src/styles/layout/_flex.scss
deleted file mode 100644
index 18d2ce8..0000000
--- a/app-frontend/react/src/styles/layout/_flex.scss
+++ /dev/null
@@ -1,6 +0,0 @@
-@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/app-frontend/react/src/styles/styles.scss b/app-frontend/react/src/styles/styles.scss
deleted file mode 100644
index 8028d8a..0000000
--- a/app-frontend/react/src/styles/styles.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-@import "layout/flex";
-@import "layout/basics";
diff --git a/app-frontend/react/src/theme/theme.tsx b/app-frontend/react/src/theme/theme.tsx
new file mode 100644
index 0000000..e79c64a
--- /dev/null
+++ b/app-frontend/react/src/theme/theme.tsx
@@ -0,0 +1,456 @@
+import { createTheme } from "@mui/material/styles";
+import moonIcon from "@assets/icons/moon.svg";
+import sunIcon from "@assets/icons/sun.svg";
+
+const lightBg = "#F2F3FF";
+
+const lightGrey = "#1f2133";
+
+const lightPurple = "#e3e5fd";
+const deepPurple = "#3D447F";
+const darkPurple = "#222647";
+const brightPurple = "#6b77db";
+const white60 = "#ffffff60";
+
+export const themeCreator = (mode: "light" | "dark") => {
+ return createTheme({
+ palette: {
+ mode: mode, // Default mode
+ primary: {
+ main: mode === "dark" ? "#ffffff" : "#ffffff",
+ contrastText: "#000000",
+ },
+ secondary: {
+ main: deepPurple,
+ contrastText: "#ffffff",
+ },
+ background: {
+ default: mode === "dark" ? "#090B1C" : lightBg,
+ paper: mode === "dark" ? "#161b22" : "#ffffff",
+ },
+ text: {
+ primary: mode === "dark" ? "#c9d1d9" : "#000000",
+ secondary: mode === "dark" ? "#ffffff" : deepPurple,
+ },
+ },
+ typography: {
+ fontFamily: "Roboto, Arial, sans-serif",
+ h1: {
+ fontWeight: 700,
+ fontSize: "2rem",
+ lineHeight: 1.5,
+ color: mode === "dark" ? "#ffffff" : deepPurple,
+ },
+ h2: {
+ fontWeight: 500,
+ fontSize: "1rem",
+ lineHeight: 1.4,
+ color: mode === "dark" ? "#ffffff" : deepPurple,
+ },
+ body1: {
+ fontSize: "1rem",
+ fontWeight: 300,
+ lineHeight: 1.5,
+ color: mode === "dark" ? "#ffffff" : deepPurple,
+ },
+ button: {
+ textTransform: "none",
+ fontWeight: 600,
+ },
+ },
+ components: {
+ MuiIconButton: {
+ styleOverrides: {
+ root: ({ theme }) => ({
+ svg: {
+ fill: theme.customStyles.icon?.main,
+ },
+ }),
+ },
+ },
+
+ MuiCheckbox: {
+ styleOverrides: {
+ root: ({ theme }) => ({
+ color: theme.customStyles.icon?.main,
+ "&.Mui-checked": {
+ color: theme.customStyles.icon?.main,
+ },
+ }),
+ },
+ },
+ MuiTooltip: {
+ styleOverrides: {
+ tooltip: {
+ backgroundColor: mode === "dark" ? lightGrey : darkPurple,
+ },
+ arrow: {
+ color: mode === "dark" ? lightGrey : darkPurple,
+ },
+ },
+ },
+ },
+ customStyles: {
+ header: {
+ backgroundColor: mode === "dark" ? "#090B1C" : "#228BE6",
+ boxShadow: mode === "dark" ? "none" : "0px 1px 24.1px 0px #4953D526",
+ borderBottom: mode === "dark" ? `1px solid ${deepPurple}7A` : "none",
+ },
+ aside: {
+ main: mode === "dark" ? lightGrey : "#E5E7FE",
+ },
+ customDivider: {
+ main: mode === "dark" ? white60 : deepPurple,
+ },
+ user: {
+ main: mode === "dark" ? "#161b22" : "#E3E5FD",
+ },
+ icon: {
+ main: mode === "dark" ? "#E5E7FE" : deepPurple,
+ },
+ input: {
+ main: mode === "dark" ? "#ffffff" : "#ffffff", // background color
+ primary: mode === "dark" ? "#c9d1d9" : "#000000",
+ secondary: mode === "dark" ? "#ffffff" : "#6b7280",
+ },
+ code: {
+ // title: mode === 'dark' ? '#2b2b2b' : '#2b2b2b',
+ primary: mode === "dark" ? "#5B5D74" : "#B6B9D4",
+ // text: mode === 'dark' ? '#ffffff' : '#ffffff',
+ // secondary: mode === 'dark' ? '#141415' : '#141415',
+ },
+ gradientShadow: {
+ border: `1px solid ${mode === "dark" ? "#ffffff20" : deepPurple + "10"}`,
+ boxShadow:
+ mode === "dark"
+ ? "0px 0px 10px rgba(0, 0, 0, 0.7)"
+ : "0px 0px 10px rgba(0, 0, 0, 0.1)",
+ },
+ gradientBlock: {
+ background:
+ mode === "dark"
+ ? `linear-gradient(180deg, ${lightGrey} 0%, rgba(61, 68, 127, 0.15)100%)`
+ : "linear-gradient(180deg, rgba(230, 232, 253, 0.50) 0%, rgba(61, 68, 127, 0.15) 100%)",
+ "&:hover": {
+ background:
+ mode === "dark"
+ ? `linear-gradient(180deg, rgba(61, 68, 127, 0.15) 0%, ${lightGrey} 100%)`
+ : "linear-gradient(180deg, rgba(61, 68, 127, 0.15) 0%, rgba(230, 232, 253, 0.50) 100%)",
+ },
+
+ ".MuiChip-root": {
+ backgroundColor: "#fff",
+ },
+ },
+ sources: {
+ iconWrap: {
+ background: "linear-gradient(90deg, #C398FA -56.85%, #7E6DBB 21.46%)",
+ svg: {
+ fill: "#ffffff !important",
+ color: "#ffffff",
+ },
+ },
+ sourceWrap: {
+ background: mode === "dark" ? "#1a1b27" : "#ffffff70",
+ border: `1px solid ${mode === "dark" ? "rgba(230, 232, 253, 0.30)" : lightPurple}`,
+ color: mode === "dark" ? "#fff" : deepPurple,
+ },
+ sourceChip: {
+ background: mode === "dark" ? "#1a1b27" : "#ffffff",
+ border: `1px solid ${mode === "dark" ? "#c398fa" : "rgba(73, 83, 213, 0.40)"}`,
+ color: mode === "dark" ? "#fff" : "#444",
+ },
+ },
+ audioProgress: {
+ stroke: mode === "dark" ? "#c9d1d9" : "#6b7280",
+ },
+ audioEditButton: {
+ boxShadow: "none",
+ border: "none",
+ backgroundColor: "transparent",
+ color: mode === "dark" ? "#fff" : deepPurple,
+ "&:hover": {
+ backgroundColor: mode === "dark" ? deepPurple : deepPurple + "40",
+ },
+ },
+ homeTitle: {
+ background:
+ mode === "dark"
+ ? "#fff"
+ : `linear-gradient(271deg, #C398FA -56.85%, #7E6DBB 21.46%, ${deepPurple} 99.77%)`,
+ },
+ homeButtons: {
+ borderRadius: "25px",
+ border: `1px solid ${mode === "dark" ? white60 : deepPurple + "60"}`, // take purple down some it down some
+ backgroundColor: mode === "dark" ? "#161b22" : lightBg,
+ color: mode === "dark" ? "#fff" : deepPurple,
+
+ boxShadow:
+ mode === "dark"
+ ? "0px 4px 10px rgba(0, 0, 0, 0.7)"
+ : "0px 4px 10px rgba(0, 0, 0, 0.1)",
+ "&:hover": {
+ backgroundColor: mode === "dark" ? darkPurple : lightPurple,
+ },
+ fontWeight: 300,
+ '&[aria-selected="true"]': {
+ fontWeight: 600,
+ backgroundColor: mode === "dark" ? darkPurple : lightPurple,
+ },
+ },
+ promptExpandButton: {
+ borderRadius: "25px",
+ border: `1px solid ${mode === "dark" ? white60 : deepPurple + "60"}`, // take purple down some it down some
+ backgroundColor: mode === "dark" ? "#161b22" : lightBg,
+ color: mode === "dark" ? "#fff" : deepPurple,
+
+ boxShadow:
+ mode === "dark"
+ ? "0px 4px 10px rgba(0, 0, 0, 0.7)"
+ : "0px 4px 10px rgba(0, 0, 0, 0.1)",
+ "&:hover": {
+ backgroundColor: mode === "dark" ? deepPurple : lightPurple,
+ },
+ },
+ promptButton: {
+ backgroundColor: mode === "dark" ? lightGrey : lightBg,
+ color: `${mode === "dark" ? "#fff" : deepPurple} !important`,
+ "&:hover": {
+ backgroundColor: mode === "dark" ? darkPurple : lightPurple,
+ color: mode === "dark" ? "#ffffff" : deepPurple,
+ },
+ },
+ promptListWrapper: {
+ backgroundColor: mode === "dark" ? lightGrey : lightBg,
+ boxShadow:
+ mode === "dark"
+ ? "0px 4px 10px rgba(0, 0, 0, 0.7)"
+ : "0px 4px 10px rgba(0, 0, 0, 0.1)",
+ },
+ primaryInput: {
+ inputWrapper: {
+ backgroundColor: mode === "dark" ? lightGrey : lightPurple,
+ border: `1px solid ${mode === "dark" ? "#ffffff20" : deepPurple + "10"}`,
+ boxShadow:
+ mode === "dark"
+ ? "0px 0px 10px rgba(0, 0, 0, 0.3)"
+ : "0px 0px 10px rgba(0, 0, 0, 0.1)",
+ "&:hover, &.active, &:focus": {
+ border: `1px solid ${mode === "dark" ? "#ffffff20" : deepPurple + "60"}`,
+ },
+ },
+ textInput: {
+ color: mode === "dark" ? "#fff" : "#3D447F",
+ "&::placeholder": {
+ color: mode === "dark" ? "#ffffff90" : "#6b7280",
+ },
+ },
+ circleButton: {
+ backgroundColor: mode === "dark" ? "transparent" : deepPurple + "80",
+ border: `1px solid ${mode === "dark" ? white60 : "transparent"}`,
+ "svg path": {
+ fill: mode === "dark" ? "#c9d1d9" : "#D9D9D9",
+ },
+ "&.active": {
+ backgroundColor: mode === "dark" ? deepPurple : lightGrey,
+ "svg path": {
+ fill: mode === "dark" ? "#c9d1d9" : "#D9D9D9",
+ },
+ },
+ "&:hover": {
+ backgroundColor: mode === "dark" ? "#646999" : "#003E71",
+ "svg path": {
+ fill: mode === "dark" ? "#c9d1d9" : "#D9D9D9",
+ },
+ },
+ },
+ },
+ tokensInput: {
+ color: mode === "dark" ? "#fff" : deepPurple,
+ backgroundColor: "transparent",
+ border: `1px solid ${mode === "dark" ? white60 : deepPurple + "70"}`,
+ boxShadow: "none",
+
+ "&:hover": {
+ borderColor: deepPurple,
+ },
+
+ "&:focus": {
+ borderColor: deepPurple,
+ },
+
+ "&[aria-invalid]": {
+ borderColor: "#cc0000 !important",
+ color: "#cc0000",
+ },
+ },
+ webInput: {
+ backgroundColor: mode === "dark" ? lightGrey : lightPurple,
+ ".Mui-focused": {
+ color: mode === "dark" ? "#ffffff" : deepPurple,
+ ".MuiOutlinedInput-notchedOutline": {
+ border: `1px solid ${mode === "dark" ? white60 : `${deepPurple}22`}`,
+ },
+ },
+ },
+ fileInputWrapper: {
+ backgroundColor: `${deepPurple}10`,
+ border: `1px dashed ${mode === "dark" ? white60 : `${deepPurple}22`}`,
+ },
+ fileInput: {
+ wrapper: {
+ backgroundColor: `${deepPurple}10`,
+ border: `1px dashed ${mode === "dark" ? white60 : `${deepPurple}22`}`,
+ },
+ file: {
+ backgroundColor:
+ mode === "dark" ? "rgba(255,255,255,0.1)" : "rgba(255,255,255,0.7)",
+ },
+ },
+ actionButtons: {
+ text: {
+ boxShadow: "none",
+ background: "none",
+ fontWeight: "400",
+ color: mode === "dark" ? "#ffffff" : "#007ce1",
+ "&:disabled": {
+ opacity: 0.5,
+ color: mode === "dark" ? "#ffffff" : "#007ce1",
+ },
+ "&:hover": {
+ background: mode === "dark" ? "#007ce1" : "#ffffff",
+ color: mode === "dark" ? "#ffffff" : "#007ce1",
+ },
+ },
+ delete: {
+ boxShadow: "none",
+ background: "#f15346",
+ fontWeight: "400",
+ color: "#fff",
+ "&:hover": {
+ background: "#cc0000",
+ },
+ "&:disabled": {
+ opacity: 0.5,
+ color: "#fff",
+ },
+ },
+ solid: {
+ boxShadow: "none",
+ background: deepPurple,
+ fontWeight: "400",
+ color: "#fff",
+ "&:hover": {
+ background: deepPurple,
+ },
+ "&:disabled": {
+ opacity: 0.5,
+ color: "#fff",
+ },
+ },
+ outline: {
+ boxShadow: "none",
+ background: "transparent",
+ fontWeight: "400",
+ color: mode === "dark" ? "#ffffff" : "#007ce1",
+ border: `1px solid ${mode === "dark" ? "#ffffff" : "#007ce1"}`,
+ "&:hover": {
+ background: mode === "dark" ? "#007ce1" : "#ffffff",
+ color: mode === "dark" ? "#ffffff" : "#007ce1",
+ },
+ "&.active": {
+ background: mode === "dark" ? "#ffffff" : "#007ce1",
+ color: mode === "dark" ? "#007ce1" : "#ffffff",
+ },
+ },
+ },
+ themeToggle: {
+ ".MuiSwitch-switchBase.Mui-checked": {
+ ".MuiSwitch-thumb:before": {
+ backgroundImage: `url(${moonIcon})`,
+ },
+ },
+ "& .MuiSwitch-thumb": {
+ backgroundColor: mode === "dark" ? "#fff" : "transparent",
+ border: `1px solid ${mode === "dark" ? "#090B1C" : deepPurple}`,
+ "svg path": {
+ fill: mode === "dark" ? "#E5E7FE" : deepPurple,
+ },
+ "&::before": {
+ backgroundImage: `url(${sunIcon})`,
+ },
+ },
+ "& .MuiSwitch-track": {
+ border: `1px solid ${mode === "dark" ? "#fff" : deepPurple}`,
+ backgroundColor: mode === "dark" ? "#8796A5" : "transparent",
+ },
+ },
+ dropDown: {
+ "&:hover, &:focus": {
+ backgroundColor:
+ mode === "dark" ? "rgba(0,0,0, 0.5)" : "rgba(230, 232, 253, 0.50)",
+ },
+ "&.Mui-selected": {
+ backgroundColor:
+ mode === "dark" ? "rgba(0,0,0, 1)" : "rgba(230, 232, 253, 0.75)",
+ },
+ "&.Mui-selected:hover, &.Mui-selected:focus": {
+ backgroundColor:
+ mode === "dark" ? "rgba(0,0,0, 1)" : "rgba(230, 232, 253, 0.75)",
+ },
+ wrapper: {
+ border: `1px solid ${mode === "dark" ? white60 : deepPurple + "70"}`,
+ },
+ },
+ settingsModal: {
+ boxShadow: " 0px 0px 20px rgba(0,0,0,0.5)",
+ border: "1px solid #000",
+ background: mode === "dark" ? lightGrey : lightBg,
+ "#modal-modal-title": {
+ backgroundColor: "#e5e7fe",
+ color: deepPurple,
+
+ svg: {
+ fill: deepPurple,
+ },
+ },
+ },
+ styledSlider: {
+ color: mode === "dark" ? brightPurple : deepPurple,
+
+ "&.disabled": {
+ color: mode === "dark" ? brightPurple : deepPurple,
+ },
+
+ ".MuiSlider-rail": {
+ backgroundColor: mode === "dark" ? brightPurple : deepPurple,
+ },
+
+ ".MuiSlider-track": {
+ backgroundColor: mode === "dark" ? brightPurple : deepPurple,
+ },
+
+ ".MuiSlider-thumb": {
+ backgroundColor: mode === "dark" ? brightPurple : deepPurple,
+
+ "&:hover": {
+ boxShadow: `0 0 0 6px rgba(61,68,127,0.3)`,
+ },
+
+ "&.focusVisible": {
+ boxShadow: `0 0 0 8px rgba(61,68,127,0.5)`,
+ },
+
+ "&.active": {
+ boxShadow: `0 0 0 8px rgba(61,68,127,0.5)`,
+ },
+
+ "&.disabled": {
+ backgroundColor: mode === "dark" ? brightPurple : deepPurple,
+ },
+ },
+ },
+ },
+ });
+};
+deepPurple;
diff --git a/app-frontend/react/src/types/common.ts b/app-frontend/react/src/types/common.ts
new file mode 100644
index 0000000..eb65a08
--- /dev/null
+++ b/app-frontend/react/src/types/common.ts
@@ -0,0 +1,13 @@
+// Copyright (C) 2025 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+export interface ErrorResponse {
+ response?: {
+ data?: {
+ error?: {
+ message?: string;
+ };
+ };
+ };
+ message: string;
+}
diff --git a/app-frontend/react/src/types/conversation.ts b/app-frontend/react/src/types/conversation.ts
new file mode 100644
index 0000000..439d998
--- /dev/null
+++ b/app-frontend/react/src/types/conversation.ts
@@ -0,0 +1,57 @@
+// Copyright (C) 2025 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+// export interface Model {
+// model_type: string;
+// token_limit: number;
+// temperature: number;
+// display_name: string;
+// version: number;
+// vendor: string;
+// platform: string;
+// min_temperature: number;
+// max_temperature: number;
+// min_token_limit: number;
+// max_token_limit: number;
+// data_insights_input_token: number;
+// data_insights_output_token: number;
+// }
+
+export interface InferenceSettings {
+ model: string;
+ temperature: number;
+ token_limit: number;
+ input_token?: number;
+ output_token?: number;
+ tags?: null;
+ maxTokenLimit?: number;
+ minTokenLimit?: number;
+ maxTemperatureLimit?: number;
+ minTemperatureLimit?: number;
+}
+
+export interface Feedback {
+ comment: string;
+ rating: number;
+ is_thumbs_up: boolean;
+}
+
+export interface SuccessResponse {
+ message: string;
+}
+
+export interface PromptsResponse {
+ prompt_text: string;
+ tags: [];
+ tag_category: string;
+ author: string;
+}
+
+export interface StreamChatProps {
+ user_id: string;
+ conversation_id: string;
+ use_case: string;
+ query: string;
+ tags: string[];
+ settings: InferenceSettings;
+}
diff --git a/app-frontend/react/src/types/global.d.ts b/app-frontend/react/src/types/global.d.ts
new file mode 100644
index 0000000..221d7c0
--- /dev/null
+++ b/app-frontend/react/src/types/global.d.ts
@@ -0,0 +1,7 @@
+// Copyright (C) 2025 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+declare module "*.svg" {
+ const content: string;
+ export default content;
+}
diff --git a/app-frontend/react/src/types/speech.d.ts b/app-frontend/react/src/types/speech.d.ts
new file mode 100644
index 0000000..1d5eb60
--- /dev/null
+++ b/app-frontend/react/src/types/speech.d.ts
@@ -0,0 +1,27 @@
+// Copyright (C) 2025 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+type SpeechRecognitionErrorEvent = Event & {
+ error:
+ | "no-speech"
+ | "audio-capture"
+ | "not-allowed"
+ | "network"
+ | "aborted"
+ | "service-not-allowed"
+ | "bad-grammar"
+ | "language-not-supported";
+ message?: string; // Some browsers may provide an additional error message
+};
+
+type SpeechRecognitionEvent = Event & {
+ results: {
+ [index: number]: {
+ [index: number]: {
+ transcript: string;
+ confidence: number;
+ };
+ isFinal: boolean;
+ };
+ };
+};
diff --git a/app-frontend/react/src/types/styles.d.ts b/app-frontend/react/src/types/styles.d.ts
new file mode 100644
index 0000000..7d3279f
--- /dev/null
+++ b/app-frontend/react/src/types/styles.d.ts
@@ -0,0 +1,7 @@
+// Copyright (C) 2025 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+declare module "*.module.scss" {
+ const classes: { [key: string]: string };
+ export default classes;
+}
diff --git a/app-frontend/react/src/types/theme.d.ts b/app-frontend/react/src/types/theme.d.ts
new file mode 100644
index 0000000..a46a8af
--- /dev/null
+++ b/app-frontend/react/src/types/theme.d.ts
@@ -0,0 +1,47 @@
+// Copyright (C) 2025 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import "@mui/material/styles";
+import { PaletteChip, PaletteColor } from "@mui/material/styles";
+
+declare module "@mui/material/styles" {
+ interface Theme {
+ customStyles: Record>;
+ }
+
+ interface ThemeOptions {
+ customStyles?: Record>;
+ }
+
+ interface Palette {
+ header?: PaletteColor;
+ aside?: PaletteColor;
+ customDivider?: PaletteColor;
+ input?: PaletteColor;
+ icon?: PaletteColor;
+ user?: PaletteColor;
+ code?: PaletteColor;
+ gradientBlock?: PaletteColor;
+ audioProgress?: PaletteColor;
+ primaryInput?: PaletteColor;
+ actionButtons?: PaletteColor;
+ themeToggle?: PaletteColor;
+ dropDown?: PaletteColor;
+ }
+
+ interface PaletteOptions {
+ header?: PaletteColorOptions;
+ aside?: PaletteColorOptions;
+ customDivider?: PaletteColorOptions;
+ input?: PaletteColorOptions;
+ icon?: PaletteColorOptions;
+ user?: PaletteColorOptions;
+ code?: PaletteColorOptions;
+ gradientBlock?: PaletteColorOptions;
+ audioProgress?: PaletteColorOptions;
+ primaryInput?: PaletteColorOptions;
+ actionButtons?: PaletteColorOptions;
+ themeToggle?: PaletteColorOptions;
+ dropDown?: PaletteColorOptions;
+ }
+}
diff --git a/app-frontend/react/src/utils/navigationAndAxiosWithQuery.ts b/app-frontend/react/src/utils/navigationAndAxiosWithQuery.ts
new file mode 100644
index 0000000..efb0082
--- /dev/null
+++ b/app-frontend/react/src/utils/navigationAndAxiosWithQuery.ts
@@ -0,0 +1,53 @@
+// navigationAndAxiosWithQuery.ts
+// Consolidated navigation and axios helpers for query string retention
+import axios from "axios";
+import { useLocation, useNavigate } from "react-router-dom";
+
+// --- Axios Client with Query String Propagation ---
+const axiosClient = axios.create();
+
+axiosClient.interceptors.request.use((config) => {
+ if (typeof window !== "undefined") {
+ config.headers = config.headers || {};
+ // Note: Setting Referer is blocked by browsers, but you can set a custom header if needed
+ // config.headers["Referer"] = window.location.href;
+ // Optionally, propagate query string as a custom header
+ config.headers["X-Current-Query"] = window.location.search;
+ }
+ return config;
+});
+
+export { axiosClient };
+
+// --- React Router: Navigation with Query String ---
+/**
+ * Custom hook to navigate while retaining current query parameters.
+ * Usage: const navigateWithQuery = useNavigateWithQuery();
+ * navigateWithQuery('/chat/123');
+ */
+export function useNavigateWithQuery() {
+ const navigate = useNavigate();
+ const location = useLocation();
+ return (to: string, options?: Parameters[1]) => {
+ const hasQuery = to.includes("?");
+ const query = location.search;
+ if (hasQuery || !query) {
+ navigate(to, options);
+ } else {
+ navigate(`${to}${query}`, options);
+ }
+ };
+}
+
+/**
+ * Appends the current location's search (query string) to a given path.
+ * Usage: const toWithQuery = useToWithQuery();
+ *
+ */
+export function useToWithQuery() {
+ const location = useLocation();
+ return (to: string) => {
+ if (to.includes("?")) return to;
+ return `${to}${location.search}`;
+ };
+}
diff --git a/app-frontend/react/src/utils/utils.js b/app-frontend/react/src/utils/utils.js
new file mode 100644
index 0000000..59f40b5
--- /dev/null
+++ b/app-frontend/react/src/utils/utils.js
@@ -0,0 +1,96 @@
+// Copyright (C) 2025 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import React from "react";
+
+export const smartTrim = (string, maxLength) => {
+ if (!string) {
+ return string;
+ }
+ if (maxLength < 1) {
+ return string;
+ }
+ if (string.length <= maxLength) {
+ return string;
+ }
+ if (maxLength === 1) {
+ return string.substring(0, 1) + "...";
+ }
+ var midpoint = Math.ceil(string.length / 2);
+ var toremove = string.length - maxLength;
+ var lstrip = Math.ceil(toremove / 2);
+ var rstrip = toremove - lstrip;
+ return string.substring(0, midpoint - lstrip) + "..." + string.substring(midpoint + rstrip);
+};
+
+export const QueryStringFromArr = (paramsArr = []) => {
+ const queryString = [];
+
+ for (const param of paramsArr) {
+ queryString.push(`${param.name}=${param.value}`);
+ }
+
+ return queryString.join("&");
+};
+
+export const isAuthorized = (
+ allowedRoles = [],
+ userRole,
+ isPreviewOnlyFeature = false,
+ isPreviewUser = false,
+ isNotAllowed = false,
+) => {
+ return (
+ (allowedRoles.length === 0 || allowedRoles.includes(userRole)) &&
+ (!isPreviewOnlyFeature || isPreviewUser) &&
+ !isNotAllowed
+ );
+};
+
+function addPropsToReactElement(element, props, i) {
+ if (React.isValidElement(element)) {
+ return React.cloneElement(element, { key: i, ...props });
+ }
+ return element;
+}
+
+export function addPropsToChildren(children, props) {
+ if (!Array.isArray(children)) {
+ return addPropsToReactElement(children, props);
+ }
+ return children.map((childElement, i) => addPropsToReactElement(childElement, props, i));
+}
+
+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),
+ );
+};
+
+export const readFilesAndSummarize = async (sourceFiles) => {
+ let summaryMessage = "";
+
+ if (sourceFiles.length) {
+ const readFilePromises = sourceFiles.map((fileWrapper) => {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ const text = reader.result?.toString() || "";
+ resolve(text);
+ };
+ reader.onerror = () => reject(new Error("Error reading file"));
+ reader.readAsText(fileWrapper.file);
+ });
+ });
+
+ const fileContents = await Promise.all(readFilePromises);
+
+ summaryMessage = fileContents.join("\n");
+ }
+
+ return summaryMessage;
+};
diff --git a/app-frontend/react/src/vite-env.d.ts b/app-frontend/react/src/vite-env.d.ts
index 4260915..0128e66 100644
--- a/app-frontend/react/src/vite-env.d.ts
+++ b/app-frontend/react/src/vite-env.d.ts
@@ -1,4 +1,5 @@
-// Copyright (C) 2024 Intel Corporation
+// Copyright (C) 2025 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
///
+///
diff --git a/app-frontend/react/tsconfig.json b/app-frontend/react/tsconfig.json
index f50b75c..d7149ff 100644
--- a/app-frontend/react/tsconfig.json
+++ b/app-frontend/react/tsconfig.json
@@ -1,23 +1,34 @@
{
"compilerOptions": {
- "target": "ES2020",
- "useDefineForClassFields": true,
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
- "module": "ESNext",
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
"skipLibCheck": true,
-
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "esnext",
+ "moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
-
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true
+ "baseUrl": "src",
+ "paths": {
+ "@components/*": ["components/*"],
+ "@shared/*": ["shared/*"],
+ "@contexts/*": ["contexts/*"],
+ "@redux/*": ["redux/*"],
+ "@services/*": ["services/*"],
+ "@pages/*": ["pages/*"],
+ "@layouts/*": ["layouts/*"],
+ "@assets/*": ["assets/*"],
+ "@icons/*": ["icons/*"],
+ "@utils/*": ["utils/*"],
+ "@root/*": ["*"]
+ }
},
- "include": ["src"],
- "references": [{ "path": "./tsconfig.node.json" }]
+ "include": ["src", "src/theme/theme.tsx", "src/**/*.d.ts"]
}
diff --git a/app-frontend/react/vite.config.js b/app-frontend/react/vite.config.js
new file mode 100644
index 0000000..bf36019
--- /dev/null
+++ b/app-frontend/react/vite.config.js
@@ -0,0 +1,120 @@
+// Copyright (C) 2025 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import react from "@vitejs/plugin-react";
+import path from "path";
+
+import { defineConfig } from "vite";
+import { visualizer } from "rollup-plugin-visualizer";
+import compression from "vite-plugin-compression";
+import terser from "@rollup/plugin-terser";
+import sassDts from "vite-plugin-sass-dts";
+import svgr from "vite-plugin-svgr";
+
+export default defineConfig({
+ base: "/",
+ optimizeDeps: {
+ include: ["**/*.scss"], // Include all .scss files
+ },
+ modulePreload: {
+ polyfill: true, // Ensures compatibility
+ },
+ css: {
+ modules: {
+ // Enable CSS Modules for all .scss files
+ localsConvention: "camelCaseOnly",
+ },
+ },
+ commonjsOptions: {
+ esmExternals: true,
+ },
+ server: {
+ // https: true,
+ host: "0.0.0.0",
+ port: 5173,
+ },
+ build: {
+ sourcemap: false,
+ rollupOptions: {
+ // output: {
+ // manualChunks(id) {
+ // if (id.includes('node_modules')) {
+
+ // if (id.match(/react-dom|react-router|react-redux/)) {
+ // return 'react-vendor';
+ // }
+
+ // // // Code render files
+ // // if (id.match(/react-syntax-highlighter|react-markdown|gfm|remark|refractor|micromark|highlight|mdast/)) {
+ // // return 'code-vendor';
+ // // }
+
+ // if (id.match(/emotion|mui|styled-components/)) {
+ // return 'style-vendor';
+ // }
+
+ // if (id.match(/keycloak-js|axios|notistack|reduxjs|fetch-event-source|azure/)) {
+ // return 'utils-vendor';
+ // }
+
+ // const packages = id.toString().split('node_modules/')[1].split('/')[0];
+ // return `vendor-${packages}`;
+ // }
+ // }
+ // },
+ plugins: [
+ terser({
+ format: { comments: false },
+ compress: {
+ drop_console: false,
+ drop_debugger: false,
+ },
+ }),
+ ],
+ },
+ chunkSizeWarningLimit: 500,
+ assetsInlineLimit: 0,
+ },
+ plugins: [
+ svgr(),
+ react(),
+ // sassDts({
+ // enabledMode: []//['production'], // Generate type declarations on build
+ // }),
+ compression({
+ algorithm: "gzip",
+ ext: ".gz",
+ deleteOriginFile: false,
+ threshold: 10240,
+ }),
+ visualizer({
+ filename: "./dist/stats.html", // Output stats file
+ open: true, // Automatically open in the browser
+ gzipSize: true, // Show gzipped sizes
+ brotliSize: true, // Show Brotli sizes
+ }),
+ ],
+ resolve: {
+ alias: {
+ "@mui/styled-engine": "@mui/styled-engine-sc",
+ "@components": path.resolve(__dirname, "src/components/"),
+ "@shared": path.resolve(__dirname, "src/shared/"),
+ "@contexts": path.resolve(__dirname, "src/contexts/"),
+ "@redux": path.resolve(__dirname, "src/redux/"),
+ "@services": path.resolve(__dirname, "src/services/"),
+ "@pages": path.resolve(__dirname, "src/pages/"),
+ "@layouts": path.resolve(__dirname, "src/layouts/"),
+ "@assets": path.resolve(__dirname, "src/assets/"),
+ "@utils": path.resolve(__dirname, "src/utils/"),
+ "@icons": path.resolve(__dirname, "src/icons/"),
+ "@root": path.resolve(__dirname, "src/"),
+ },
+ },
+ define: {
+ "import.meta.env": process.env,
+ },
+ assetsInclude: ["**/*.svg"], // Ensure Vite processes .svg files
+ // define: {
+ // "import.meta.env": process.env,
+ // },
+});
diff --git a/app-frontend/react/vite.config.ts b/app-frontend/react/vite.config.ts
deleted file mode 100644
index 0c94d87..0000000
--- a/app-frontend/react/vite.config.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-// 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,
- },
- build: {
- target: "es2022"
- },
- esbuild: {
- target: "es2022"
- },
- optimizeDeps:{
- esbuildOptions: {
- target: "es2022",
- }
- }
-});
diff --git a/setup-scripts/setup-genai-studio/manifests/studio-manifest.yaml b/setup-scripts/setup-genai-studio/manifests/studio-manifest.yaml
index d3e19b0..3eca201 100644
--- a/setup-scripts/setup-genai-studio/manifests/studio-manifest.yaml
+++ b/setup-scripts/setup-genai-studio/manifests/studio-manifest.yaml
@@ -163,6 +163,25 @@ data:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
+
+ # Location block for chathistory service
+ location /v1/chathistory {
+ # Initialize the variable for namespace
+ if ($http_referer ~* "([&?]ns=([^&]+))") {
+ set $namespace $2; # Capture the value of 'ns'
+ }
+
+ # Rewrite the request to include the namespace
+ rewrite ^/(.*)$ /$1?ns=$namespace break;
+
+ # Proxy to the desired service using the namespace
+ proxy_pass http://${APP_CHATHISTORY_DNS};
+
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
}
---
apiVersion: v1
diff --git a/setup-scripts/setup-genai-studio/studio-config.yaml b/setup-scripts/setup-genai-studio/studio-config.yaml
index 85b540f..8574900 100644
--- a/setup-scripts/setup-genai-studio/studio-config.yaml
+++ b/setup-scripts/setup-genai-studio/studio-config.yaml
@@ -12,5 +12,6 @@ data:
STUDIO_FRONTEND_DNS: "studio-frontend.studio.svc.cluster.local:3000"
APP_FRONTEND_DNS: "app-frontend.$namespace.svc.cluster.local:5275"
APP_BACKEND_DNS: "app-backend.$namespace.svc.cluster.local:8899"
+ APP_CHATHISTORY_DNS: "chathistory-mongo.$namespace.svc.cluster.local:6012"
PREPARE_DOC_REDIS_PREP_DNS: "prepare-doc-redis-prep-0.$namespace.svc.cluster.local:6007"
STUDIO_BACKEND_DNS: "studio-backend.studio.svc.cluster.local:5000"
\ No newline at end of file
diff --git a/studio-backend/app/services/exporter_service.py b/studio-backend/app/services/exporter_service.py
index 8ff3075..ad2cc69 100644
--- a/studio-backend/app/services/exporter_service.py
+++ b/studio-backend/app/services/exporter_service.py
@@ -1,6 +1,7 @@
import os
import yaml
from collections import OrderedDict
+import traceback
from app.utils.exporter_utils import TEMPLATES_DIR, manifest_map, compose_map, process_opea_services
from app.utils.placeholders_utils import ordered_load_all, replace_manifest_placeholders, replace_dynamic_manifest_placeholder, replace_compose_placeholders, replace_dynamic_compose_placeholder
@@ -8,8 +9,11 @@
def convert_proj_info_to_manifest(proj_info_json, output_file=None):
print("Converting workflow info json to manifest.")
-
- opea_services = process_opea_services(proj_info_json)
+ try:
+ opea_services = process_opea_services(proj_info_json)
+ except Exception as e:
+ traceback.print_exc()
+ print(f"Error processing OPEA services: {e}")
# print(json.dumps(opea_services, indent=4))
output_manifest = []
diff --git a/studio-backend/app/services/namespace_service.py b/studio-backend/app/services/namespace_service.py
index 8651721..eb31ec2 100644
--- a/studio-backend/app/services/namespace_service.py
+++ b/studio-backend/app/services/namespace_service.py
@@ -2,6 +2,7 @@
from kubernetes.client.rest import ApiException
import time
import yaml
+import traceback
from app.services.exporter_service import convert_proj_info_to_manifest
from app.services.dashboard_service import import_grafana_dashboards, delete_dashboard
@@ -14,7 +15,11 @@ def deploy_manifest_in_namespace(core_v1_api, apps_v1_api, proj_info):
namespace_name = f"sandbox-{proj_info['id']}"
# Convert the mega_yaml to a manifest string
- manifest_string = convert_proj_info_to_manifest(proj_info)
+ try:
+ manifest_string = convert_proj_info_to_manifest(proj_info)
+ except Exception as e:
+ traceback.print_exc()
+ print(f"Error converting project info to manifest: {e}")
# Split the manifest string into individual YAML documents
yaml_docs_deploy = yaml.safe_load_all(manifest_string)
diff --git a/studio-backend/app/services/workflow_info_service.py b/studio-backend/app/services/workflow_info_service.py
index 37116e3..f41621d 100644
--- a/studio-backend/app/services/workflow_info_service.py
+++ b/studio-backend/app/services/workflow_info_service.py
@@ -64,6 +64,9 @@ def generate_dag(self):
node_data['connected_from'].append(connected_from_id)
dag_nodes[connected_from_id]['connected_to'].append(id)
continue
+ #skip ui_choice inputs
+ if input_key == 'ui_choice':
+ continue
if input_key == 'huggingFaceToken' and not input_value:
# If huggingFaceToken is empty, set it to 'NA'
@@ -85,6 +88,7 @@ def generate_dag(self):
node_data['dependent_services'] = {}
continue
else:
+ # print("node_data", node_data)
for service_type, service_params in list(node_data['dependent_services'].items()):
if llm_engine:
if llm_engine == service_type:
@@ -100,7 +104,10 @@ def generate_dag(self):
node_data['dependent_services'][service_type][input_key] = input_value
node_data['params'].pop(input_key, None)
continue
-
+ # Handle imageRepository specific logic
+ print(f"imageRepository: {node_data.get('imageRepository')}")
+ if node_data.get('imageRepository'):
+ node_data['params']['IMAGE_REPOSITORY'] = node_data['imageRepository']
del node_data['inputParams']
del node_data['inputs']
del node_data['outputs']
diff --git a/studio-backend/app/templates/app/app.compose.yaml b/studio-backend/app/templates/app/app.compose.yaml
index 1d4927d..b62d21b 100644
--- a/studio-backend/app/templates/app/app.compose.yaml
+++ b/studio-backend/app/templates/app/app.compose.yaml
@@ -26,6 +26,7 @@ app-frontend:
- no_proxy=${no_proxy}
- https_proxy=${https_proxy}
- http_proxy=${http_proxy}
+ - VITE_UI_SELECTION=${ui_selection}
- VITE_APP_BACKEND_SERVICE_URL=/v1/app-backend
__UI_CONFIG_INFO_ENV_PLACEHOLDER__
ipc: host
diff --git a/studio-backend/app/templates/app/app.manifest.yaml b/studio-backend/app/templates/app/app.manifest.yaml
index f2b0389..73a9ee3 100644
--- a/studio-backend/app/templates/app/app.manifest.yaml
+++ b/studio-backend/app/templates/app/app.manifest.yaml
@@ -124,8 +124,16 @@ spec:
containers:
- name: app-frontend
env:
- - name: VITE_APP_BACKEND_SERVICE_URL
- value: /v1/app-backend
+ # - name: VITE_APP_BACKEND_SERVICE_URL
+ # value: "/v1/app-backend"
+ - name: APP_BACKEND_SERVICE_URL
+ value: "/v1/app-backend"
+ - name: APP_DATAPREP_SERVICE_URL
+ value: "/v1/dataprep"
+ - name: APP_CHAT_HISTORY_SERVICE_URL
+ value: "/v1/chathistory"
+ - name: APP_UI_SELECTION
+ value: "chat,summary,code"
__UI_CONFIG_INFO_ENV_PLACEHOLDER__
securityContext: {}
image: __APP_FRONTEND_IMAGE__
@@ -143,6 +151,90 @@ spec:
emptyDir: {}
---
apiVersion: v1
+kind: Service
+metadata:
+ name: mongo
+spec:
+ type: ClusterIP
+ ports:
+ - port: 27017
+ targetPort: 27017
+ protocol: TCP
+ name: mongo
+ selector:
+ app: mongo
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: mongo
+ labels:
+ app: mongo
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: mongo
+ template:
+ metadata:
+ labels:
+ app: mongo
+ spec:
+ containers:
+ - name: mongo
+ image: mongo:7.0.11
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 27017
+ command: ["mongod", "--quiet", "--logpath", "/dev/null", "--bind_ip_all"]
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: chathistory-mongo
+spec:
+ type: ClusterIP
+ ports:
+ - port: 6012
+ targetPort: 6012
+ protocol: TCP
+ name: chathistory-mongo
+ selector:
+ app: chathistory-mongo
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: chathistory-mongo
+ labels:
+ app: chathistory-mongo
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: chathistory-mongo
+ template:
+ metadata:
+ labels:
+ app: chathistory-mongo
+ spec:
+ containers:
+ - name: chathistory-mongo
+ image: opea/chathistory-mongo:latest
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 6012
+ env:
+ - name: MONGO_HOST
+ value: "mongo"
+ - name: MONGO_PORT
+ value: "27017"
+ - name: COLLECTION_NAME
+ value: "Conversations"
+ - name: LOGFLAG
+ value: "True"
+---
+apiVersion: v1
kind: ConfigMap
metadata:
name: app-nginx-config
@@ -189,6 +281,21 @@ data:
chunked_transfer_encoding off;
}
+ location /v1/chathistory {
+ proxy_pass http://chathistory-mongo:6012;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # Disable buffering for SSE
+ proxy_buffering off;
+ proxy_cache off;
+ proxy_http_version 1.1;
+ proxy_set_header Connection '';
+ chunked_transfer_encoding off;
+ }
+
__UI_CONFIG_INFO_NGINX_PLACEHOLDER__
}
---
diff --git a/studio-backend/app/templates/microsvc-manifests/asr-usvc.yaml b/studio-backend/app/templates/microsvc-manifests/asr-usvc.yaml
new file mode 100644
index 0000000..cff1ece
--- /dev/null
+++ b/studio-backend/app/templates/microsvc-manifests/asr-usvc.yaml
@@ -0,0 +1,114 @@
+---
+# Source: asr/templates/configmap.yaml
+# Copyright (C) 2024 Intel Corporation
+# SPDX-License-Identifier: Apache-2.0
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: config-{endpoint}
+data:
+ HEALTHCHECK_ENDPOINT: "{whisper_endpoint}:{whisper_port}"
+ ASR_ENDPOINT: "http://{whisper_endpoint}:{whisper_port}"
+ http_proxy: ""
+ https_proxy: ""
+ no_proxy: ""
+ LOGFLAG: "True"
+
+---
+# Source: asr/templates/service.yaml
+# Copyright (C) 2024 Intel Corporation
+# SPDX-License-Identifier: Apache-2.0
+
+apiVersion: v1
+kind: Service
+metadata:
+ name: "{endpoint}"
+spec:
+ type: ClusterIP
+ ports:
+ - port: "{port}"
+ targetPort: 9099
+ protocol: TCP
+ name: "{endpoint}"
+ selector:
+ app: "{endpoint}"
+---
+# Source: asr/templates/deployment.yaml
+# Copyright (C) 2024 Intel Corporation
+# SPDX-License-Identifier: Apache-2.0
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: "{endpoint}"
+ labels:
+ app: "{endpoint}"
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: "{endpoint}"
+ template:
+ metadata:
+ labels:
+ app: "{endpoint}"
+ spec:
+ securityContext:
+ {}
+ initContainers:
+ - name: wait-for-remote-service
+ image: busybox
+ command: ['sh', '-c', 'until nc -z -v -w30 $HEALTHCHECK_ENDPOINT 80; do echo "Waiting for remote service..."; sleep 5; done']
+ envFrom:
+ - configMapRef:
+ name: config-{endpoint}
+ containers:
+ - name: asr-usvc
+ envFrom:
+ - configMapRef:
+ name: config-{endpoint}
+ securityContext:
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop:
+ - ALL
+ readOnlyRootFilesystem: true
+ runAsNonRoot: true
+ runAsUser: 1000
+ seccompProfile:
+ type: RuntimeDefault
+ image: "${REGISTRY}/asr:${TAG}"
+ "imagePullPolicy": Always
+ ports:
+ - name: asr-usvc
+ containerPort: 9099
+ protocol: TCP
+ volumeMounts:
+ - mountPath: /tmp
+ name: tmp
+ livenessProbe:
+ failureThreshold: 24
+ httpGet:
+ path: v1/health_check
+ port: asr-usvc
+ initialDelaySeconds: 5
+ periodSeconds: 5
+ readinessProbe:
+ httpGet:
+ path: v1/health_check
+ port: asr-usvc
+ initialDelaySeconds: 5
+ periodSeconds: 5
+ startupProbe:
+ failureThreshold: 120
+ httpGet:
+ path: v1/health_check
+ port: asr-usvc
+ initialDelaySeconds: 5
+ periodSeconds: 5
+ resources:
+ {}
+ volumes:
+ - name: tmp
+ emptyDir: {}
diff --git a/studio-backend/app/templates/microsvc-manifests/llm-uservice.yaml b/studio-backend/app/templates/microsvc-manifests/llm-uservice.yaml
index 238d104..0c831e4 100644
--- a/studio-backend/app/templates/microsvc-manifests/llm-uservice.yaml
+++ b/studio-backend/app/templates/microsvc-manifests/llm-uservice.yaml
@@ -80,7 +80,7 @@ spec:
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
- image: "${REGISTRY}/llm-textgen:${TAG}"
+ image: "${REGISTRY}/{IMAGE_REPOSITORY}:${TAG}"
imagePullPolicy: Always
ports:
- name: llm-uservice
diff --git a/studio-backend/app/templates/microsvc-manifests/whisper.yaml b/studio-backend/app/templates/microsvc-manifests/whisper.yaml
new file mode 100644
index 0000000..e54e0fc
--- /dev/null
+++ b/studio-backend/app/templates/microsvc-manifests/whisper.yaml
@@ -0,0 +1,145 @@
+---
+# Source: whisper/templates/configmap.yaml
+# Copyright (C) 2024 Intel Corporation
+# SPDX-License-Identifier: Apache-2.0
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: config-{endpoint}
+data:
+ EASYOCR_MODULE_PATH: "/tmp/.EasyOCR"
+ ASR_MODEL_PATH: "openai/whisper-small"
+ http_proxy: "${HTTP_PROXY}"
+ https_proxy: "${HTTP_PROXY}"
+ no_proxy: "${NO_PROXY}"
+ HF_HOME: "/tmp/.cache/huggingface"
+ HUGGINGFACE_HUB_CACHE: "/data"
+ HF_TOKEN: "{huggingFaceToken}"
+ LOGFLAG: "True"
+---
+# Source: whisper/templates/service.yaml
+# Copyright (C) 2024 Intel Corporation
+# SPDX-License-Identifier: Apache-2.0
+
+apiVersion: v1
+kind: Service
+metadata:
+ name: "{endpoint}"
+spec:
+ type: "ClusterIP"
+ ports:
+ - port: "{port}"
+ targetPort: "{port}"
+ protocol: TCP
+ name: "{endpoint}"
+ selector:
+ app: "{endpoint}"
+---
+# Source: whisper/templates/deployment.yaml
+# Copyright (C) 2024 Intel Corporation
+# SPDX-License-Identifier: Apache-2.0
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: "{endpoint}"
+ labels:
+ app: "{endpoint}"
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: "{endpoint}"
+ template:
+ metadata:
+ labels:
+ app: "{endpoint}"
+ spec:
+ securityContext:
+ {}
+ # initContainers:
+ # - name: model-downloader
+ # envFrom:
+ # - configMapRef:
+ # name: config-{endpoint}
+ # securityContext:
+ # readOnlyRootFilesystem: true
+ # allowPrivilegeEscalation: false
+ # capabilities:
+ # drop:
+ # - ALL
+ # add:
+ # - DAC_OVERRIDE
+ # # To be able to make data model directory group writable for
+ # # previously downloaded model by old versions of helm chart
+ # - FOWNER
+ # seccompProfile:
+ # type: RuntimeDefault
+ # image: huggingface/downloader:0.17.3
+ # command: ['sh', '-ec']
+ # args:
+ # - |
+ # echo "Huggingface log in ...";
+ # huggingface-cli login --token $(HF_TOKEN);
+ # echo "Download model openai/whisper-small ... ";
+ # chmod -R g+w /data
+ # huggingface-cli download --cache-dir /data openai/whisper-small;
+ # echo "Change model files mode ...";
+ # chmod -R g+w /data/models--openai--whisper-small;
+ # # NOTE: Buggy logout command;
+ # # huggingface-cli logout;
+ # volumeMounts:
+ # - mountPath: /data
+ # name: model-volume
+ # - mountPath: /tmp
+ # name: tmp
+ containers:
+ - name: whisper
+ envFrom:
+ - configMapRef:
+ name: config-{endpoint}
+ securityContext:
+ {}
+ image: "opea/whisper"
+ imagePullPolicy: IfNotPresent
+ ports:
+ - name: whisper
+ containerPort: "{port}"
+ protocol: TCP
+ volumeMounts:
+ - mountPath: /data
+ name: model-volume
+ - mountPath: /tmp
+ name: tmp
+ livenessProbe:
+ failureThreshold: 24
+ initialDelaySeconds: 8
+ periodSeconds: 8
+ timeoutSeconds: 4
+ tcpSocket:
+ port: http
+ readinessProbe:
+ initialDelaySeconds: 16
+ periodSeconds: 8
+ timeoutSeconds: 4
+ tcpSocket:
+ port: http
+ startupProbe:
+ failureThreshold: 180
+ initialDelaySeconds: 10
+ periodSeconds: 5
+ timeoutSeconds: 2
+ tcpSocket:
+ port: http
+ volumes:
+ - name: model-volume
+ persistentVolumeClaim:
+ claimName: model-pvc
+ - name: shm
+ emptyDir:
+ medium: Memory
+ sizeLimit: 1Gi
+ - name: tmp
+ emptyDir: {}
+ terminationGracePeriodSeconds: 120
\ No newline at end of file
diff --git a/studio-backend/app/utils/exporter_utils.py b/studio-backend/app/utils/exporter_utils.py
index 256ef70..5fc1c40 100644
--- a/studio-backend/app/utils/exporter_utils.py
+++ b/studio-backend/app/utils/exporter_utils.py
@@ -16,6 +16,9 @@
"opea_service@supervisor_agent" : "microsvc-manifests/supervisor-agent.yaml",
"opea_service@rag_agent" : "microsvc-manifests/rag-agent.yaml",
"opea_service@sql_agent" : "microsvc-manifests/sql-agent.yaml",
+ "opea_service@llm_docsum" : "microsvc-manifests/llm-uservice.yaml",
+ "opea_service@asr" : "microsvc-manifests/asr-usvc.yaml",
+ "whisper" : "microsvc-manifests/whisper.yaml",
}
compose_map = {
@@ -31,6 +34,9 @@
"opea_service@supervisor_agent" : "microsvc-composes/supervisor-agent.yaml",
"opea_service@rag_agent" : "microsvc-composes/rag-agent.yaml",
"opea_service@sql_agent" : "microsvc-composes/sql-agent.yaml",
+ "opea_service@llm_docsum" : "microsvc-composes/llm-uservice.yaml",
+ "opea_service@asr" : "microsvc-composes/asr-usvc.yaml",
+ "whisper" : "microsvc-composes/whisper.yaml",
}
# Define a dictionary mapping opea service types to their ports
@@ -38,8 +44,10 @@
"opea_service@prepare_doc_redis_prep" : "/v1/dataprep",
"opea_service@embedding_tei_langchain" : "/v1/embeddings",
"opea_service@retriever_redis" : "/v1/retrieval",
- "opea_service@reranking_tei" : "/v1/reranking",
+ "opea_service@reranking_tei" : "/v1/reranking",
"opea_service@llm_tgi" : "/v1/chat/completions",
+ "opea_service@llm_docsum" : "/v1/docsum",
+ "opea_service@asr" : "/v1/audio/transcriptions",
}
# Define a dictionary mapping opea service types to their ports
@@ -49,6 +57,8 @@
"opea_service@retriever_redis" : "APP_RETRIEVAL_SERVICE_URL",
"opea_service@reranking_tei" : "APP_RERANKING_SERVICE_URL",
"opea_service@llm_tgi" : "APP_CHAT_COMPLETEION_SERVICE_URL",
+ "opea_service@llm_docsum" : "APP_DOCSUM_SERVICE_URL",
+ "opea_service@asr" : "APP_ASR_SERVICE_URL",
}
additional_files_map = {
@@ -57,11 +67,22 @@
"opea_service@sql_agent" : [{"tools/": "agent-tools/"}],
}
+additional_params_map = {
+ "opea_service@llm_tgi": {
+ "IMAGE_REPOSITORY": "llm-textgen",
+ },
+ "opea_service@llm_docsum": {
+ "IMAGE_REPOSITORY": "llm-docsum",
+ },
+}
+
def process_opea_services(proj_info_json):
print("exporter_utils.py: process_opea_services")
base_port = 9000
# Create a deep copy of the proj_info_json to avoid modifying the original data
proj_info_copy = copy.deepcopy(proj_info_json)
+
+ # check for
# Filter nodes to include only those with keys containing 'opea_service@'
# and extract their 'dependent_services', 'connected_from', and 'connected_to'
@@ -121,13 +142,14 @@ def process_opea_services(proj_info_json):
# Handle other dependent services
for node_name, node_info in opea_data['nodes'].items():
+ print("process_opea_services: node_name", node_name, "node_info", node_info)
for service_type, service_info in node_info.get('dependent_services', {}).items():
# Skip redis_vector_store as it's handled separately
if service_type == 'redis_vector_store':
continue
# Create a unique key for the service based on its modelName and huggingFaceToken
- service_key = (service_info['modelName'], service_info['huggingFaceToken'])
+ service_key = (service_info.get('modelName', 'default'), service_info['huggingFaceToken'])
# Check if the service has already been added
if service_key not in service_keys:
@@ -193,7 +215,7 @@ def process_opea_services(proj_info_json):
prefix = service_type
service_info_dict[f"{prefix}_endpoint"] = services[service_id]['endpoint']
service_info_dict[f"{prefix}_port"] = services[service_id]['port']
- service_info_dict[f"modelName"] = services[service_id]['modelName']
+ service_info_dict[f"modelName"] = services[service_id].get('modelName', 'NA')
service_info_dict[f"huggingFaceToken"] = services[service_id]['huggingFaceToken']
# Iterate through the connected_to and connected_to to map to the service info
@@ -260,6 +282,14 @@ def process_opea_services(proj_info_json):
value for key, value in node_info.items() if key.endswith('_endpoint')
]
updated_nodes[node_name]['dependent_endpoints'] = dependent_endpoints
+
+ # Update additional params for services
+ for node_name, node_info in updated_nodes.items():
+ # Check if the service type has additional params defined
+ if node_info['service_type'] in additional_params_map:
+ # Update the node info with additional params
+ for param_key, param_value in additional_params_map[node_info['service_type']].items():
+ updated_nodes[node_name][param_key] = param_value
# Merge the updated_nodes with the services dictionary
services.update(updated_nodes)
diff --git a/studio-frontend/packages/server/src/nodes/asr.js b/studio-frontend/packages/server/src/nodes/asr.js
new file mode 100644
index 0000000..938d054
--- /dev/null
+++ b/studio-frontend/packages/server/src/nodes/asr.js
@@ -0,0 +1,39 @@
+'use strict'
+Object.defineProperty(exports, '__esModule', { value: true })
+// const modelLoader_1 = require("../../../src/modelLoader");
+// const utils_1 = require("../../../src/utils");
+// const llamaindex_1 = require('llamaindex')
+class OPEAEmbeddings {
+ constructor() {
+ this.label = 'Audio and Speech Recognition Whisper'
+ this.name = 'opea_service@asr'
+ this.version = 1.0
+ this.type = 'AudioTranscriptionResponse'
+ this.icon = 'assets/embeddings.png'
+ this.category = 'Audio and Speech Recognition'
+ this.description = 'Transcribe audio and video files using OpenAI Whisper model. Supports various audio formats including mp3, wav, and mp4.'
+ this.baseClasses = [this.type, 'DocSumChatCompletionRequest']
+ this.tags = ['OPEA']
+ this.inMegaservice = true
+ this.dependent_services = {
+ 'whisper': {
+ 'huggingFaceToken': ''
+ }
+ }
+ this.inputs = [
+ {
+ label: 'Audio or Video File',
+ name: 'file',
+ type: 'UploadFile',
+ },
+ {
+ label: 'HuggingFace Token',
+ name: 'huggingFaceToken',
+ type: 'password',
+ optional: true,
+ },
+ ]
+ }
+}
+module.exports = { nodeClass: OPEAEmbeddings }
+//# sourceMappingURL=OpenAIEmbedding_LlamaIndex.js.map
diff --git a/app-frontend/react/src/styles/components/_context.scss b/studio-frontend/packages/server/src/nodes/assets/.gitkeep
similarity index 100%
rename from app-frontend/react/src/styles/components/_context.scss
rename to studio-frontend/packages/server/src/nodes/assets/.gitkeep
diff --git a/studio-frontend/packages/server/src/nodes/OPEA-favicon-32x32.png b/studio-frontend/packages/server/src/nodes/assets/OPEA-favicon-32x32.png
similarity index 100%
rename from studio-frontend/packages/server/src/nodes/OPEA-favicon-32x32.png
rename to studio-frontend/packages/server/src/nodes/assets/OPEA-favicon-32x32.png
diff --git a/studio-frontend/packages/server/src/nodes/controls.png b/studio-frontend/packages/server/src/nodes/assets/controls.png
similarity index 100%
rename from studio-frontend/packages/server/src/nodes/controls.png
rename to studio-frontend/packages/server/src/nodes/assets/controls.png
diff --git a/studio-frontend/packages/server/src/nodes/data.png b/studio-frontend/packages/server/src/nodes/assets/data.png
similarity index 100%
rename from studio-frontend/packages/server/src/nodes/data.png
rename to studio-frontend/packages/server/src/nodes/assets/data.png
diff --git a/studio-frontend/packages/server/src/nodes/embeddings.png b/studio-frontend/packages/server/src/nodes/assets/embeddings.png
similarity index 100%
rename from studio-frontend/packages/server/src/nodes/embeddings.png
rename to studio-frontend/packages/server/src/nodes/assets/embeddings.png
diff --git a/studio-frontend/packages/server/src/nodes/llm.png b/studio-frontend/packages/server/src/nodes/assets/llm.png
similarity index 100%
rename from studio-frontend/packages/server/src/nodes/llm.png
rename to studio-frontend/packages/server/src/nodes/assets/llm.png
diff --git a/studio-frontend/packages/server/src/nodes/opea-horizontal-color.svg b/studio-frontend/packages/server/src/nodes/assets/opea-horizontal-color.svg
similarity index 100%
rename from studio-frontend/packages/server/src/nodes/opea-horizontal-color.svg
rename to studio-frontend/packages/server/src/nodes/assets/opea-horizontal-color.svg
diff --git a/studio-frontend/packages/server/src/nodes/assets/opea-icon-color.svg b/studio-frontend/packages/server/src/nodes/assets/opea-icon-color.svg
new file mode 100644
index 0000000..7901511
--- /dev/null
+++ b/studio-frontend/packages/server/src/nodes/assets/opea-icon-color.svg
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/studio-frontend/packages/server/src/nodes/openai.svg b/studio-frontend/packages/server/src/nodes/assets/openai.svg
similarity index 100%
rename from studio-frontend/packages/server/src/nodes/openai.svg
rename to studio-frontend/packages/server/src/nodes/assets/openai.svg
diff --git a/studio-frontend/packages/server/src/nodes/reranking.png b/studio-frontend/packages/server/src/nodes/assets/reranking.png
similarity index 100%
rename from studio-frontend/packages/server/src/nodes/reranking.png
rename to studio-frontend/packages/server/src/nodes/assets/reranking.png
diff --git a/studio-frontend/packages/server/src/nodes/retreiver.png b/studio-frontend/packages/server/src/nodes/assets/retreiver.png
similarity index 100%
rename from studio-frontend/packages/server/src/nodes/retreiver.png
rename to studio-frontend/packages/server/src/nodes/assets/retreiver.png
diff --git a/studio-frontend/packages/server/src/nodes/vector stores.png b/studio-frontend/packages/server/src/nodes/assets/vector stores.png
similarity index 100%
rename from studio-frontend/packages/server/src/nodes/vector stores.png
rename to studio-frontend/packages/server/src/nodes/assets/vector stores.png
diff --git a/studio-frontend/packages/server/src/nodes/chat_completion.js b/studio-frontend/packages/server/src/nodes/chat_completion.js
index 68a9d16..e95ae5d 100644
--- a/studio-frontend/packages/server/src/nodes/chat_completion.js
+++ b/studio-frontend/packages/server/src/nodes/chat_completion.js
@@ -6,7 +6,7 @@ class OPEAChatCompletion {
this.name = 'chat_completion'
this.version = 1.0
this.type = 'ChatCompletion'
- this.icon = 'controls.png'
+ this.icon = 'assets/controls.png'
this.category = 'Controls'
this.description = 'Send Chat Response to UI'
this.baseClasses = []
@@ -16,7 +16,23 @@ class OPEAChatCompletion {
{
label: 'LLM Response',
name: 'llm_response',
- type: 'ChatCompletion'
+ type: 'StreamingResponse|ChatCompletion'
+ },
+ {
+ label: 'UI Choice',
+ name: 'ui_choice',
+ type: 'options',
+ default: 'chat',
+ options: [
+ {
+ name: 'chat',
+ label: 'Chat'
+ },
+ {
+ name: 'columns',
+ label: 'Two Columns (For Document Sumarization or Translation)'
+ }
+ ]
}
],
this.hideOutput = true
diff --git a/studio-frontend/packages/server/src/nodes/chat_input.js b/studio-frontend/packages/server/src/nodes/chat_input.js
index 4fd22e9..d77e07c 100644
--- a/studio-frontend/packages/server/src/nodes/chat_input.js
+++ b/studio-frontend/packages/server/src/nodes/chat_input.js
@@ -6,7 +6,7 @@ class OPEAChatInput {
this.name = 'chat_input'
this.version = 1.0
this.type = 'ChatCompletionRequest'
- this.icon = 'controls.png'
+ this.icon = 'assets/controls.png'
this.category = 'Controls'
this.description = 'User Input from Chat Window'
this.baseClasses = [this.type]
diff --git a/studio-frontend/packages/server/src/nodes/data_prep_redis.js b/studio-frontend/packages/server/src/nodes/data_prep_redis.js
index c6e83f7..98928c9 100644
--- a/studio-frontend/packages/server/src/nodes/data_prep_redis.js
+++ b/studio-frontend/packages/server/src/nodes/data_prep_redis.js
@@ -15,7 +15,7 @@ class OPEADataPrep {
this.name = 'opea_service@prepare_doc_redis_prep'
this.version = 1.0
this.type = 'EmbedDoc'
- this.icon = 'data.png'
+ this.icon = 'assets/data.png'
this.category = 'Data Preparation'
this.description = 'Data Preparation with redis using Langchain'
this.baseClasses = [this.type]
diff --git a/studio-frontend/packages/server/src/nodes/doc_input.js b/studio-frontend/packages/server/src/nodes/doc_input.js
index 6e58893..859e1f4 100644
--- a/studio-frontend/packages/server/src/nodes/doc_input.js
+++ b/studio-frontend/packages/server/src/nodes/doc_input.js
@@ -6,7 +6,7 @@ class OPEADocInput {
this.name = 'doc_input'
this.version = 1.0
this.type = 'UploadFile'
- this.icon = 'controls.png'
+ this.icon = 'assets/controls.png'
this.category = 'Controls'
this.description = 'User Input from Document Upload'
this.baseClasses = [this.type]
diff --git a/studio-frontend/packages/server/src/nodes/file_input.js b/studio-frontend/packages/server/src/nodes/file_input.js
new file mode 100644
index 0000000..71d6954
--- /dev/null
+++ b/studio-frontend/packages/server/src/nodes/file_input.js
@@ -0,0 +1,17 @@
+'use strict'
+Object.defineProperty(exports, '__esModule', { value: true })
+class OPEADocInput {
+ constructor() {
+ this.label = 'File Input'
+ this.name = 'file_input'
+ this.version = 1.0
+ this.type = 'UploadFile'
+ this.icon = 'assets/controls.png'
+ this.category = 'Controls'
+ this.description = 'User Input from File Upload'
+ this.baseClasses = [this.type]
+ this.tags = ['OPEA']
+ this.inMegaservice = false
+ }
+}
+module.exports = { nodeClass: OPEADocInput }
diff --git a/studio-frontend/packages/server/src/nodes/llm_codegen.js b/studio-frontend/packages/server/src/nodes/llm_codegen.js
new file mode 100644
index 0000000..c058053
--- /dev/null
+++ b/studio-frontend/packages/server/src/nodes/llm_codegen.js
@@ -0,0 +1,144 @@
+'use strict'
+Object.defineProperty(exports, '__esModule', { value: true })
+class OPEA_LLM_CODEGEN {
+ constructor() {
+ //@ts-ignore
+ // this.loadMethods = {
+ // async listModels() {
+ // return await (0, modelLoader_1.getModels)(modelLoader_1.MODEL_TYPE.EMBEDDING, 'openAIEmbedding_LlamaIndex');
+ // }
+ // };
+ this.label = 'LLM Code Generation'
+ this.name = 'opea_service@codegen'
+ this.version = 1.0
+ this.type = 'GeneratedDoc'
+ this.icon = 'assets/llm.png'
+ this.category = 'LLM'
+ this.description = 'LLM Code Generation Inference'
+ this.baseClasses = [this.type, 'StreamingResponse']
+ this.tags = ['OPEA']
+ this.inMegaservice = true
+ this.dependent_services = {
+ 'tgi': {
+ 'modelName': '',
+ 'huggingFaceToken': ''
+ }
+ }
+ this.inputs = [
+ {
+ label: 'LLM Params Document',
+ name: 'text',
+ type: 'LLMParamsDoc|DocSumChatCompletionRequest'
+ },
+ {
+ label: 'Model Name',
+ name: 'modelName',
+ type: 'string',
+ default: 'Intel/neural-chat-7b-v3-3'
+ },
+ {
+ label: 'HuggingFace Token',
+ name: 'huggingFaceToken',
+ type: 'password',
+ optional: true,
+ },
+ {
+ label: 'Maximum Tokens',
+ name: 'max_tokens',
+ type: 'number',
+ default: 17,
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Top K',
+ name: 'top_k',
+ type: 'number',
+ default: 10,
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Top P',
+ name: 'top_p',
+ type: 'number',
+ default: 0.95,
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Typical P',
+ name: 'typical_p',
+ type: 'number',
+ default: 0.95,
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Temperature',
+ name: 'temperature',
+ type: 'number',
+ default: 0.01,
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Presence Penalty',
+ name: 'presence_penalty',
+ type: 'number',
+ default: 1.03,
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Frequency Penalty',
+ name: 'frequency_penalty',
+ type: 'number',
+ default: 0.0,
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Streaming',
+ name: 'streaming',
+ type: 'boolean',
+ default: true,
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Chat Template',
+ name: 'chat_template',
+ type: 'string',
+ rows: true,
+ default:
+ '### You are a helpful, respectful and honest assistant to help the user with questions.\n### Context: {context}\n### Question: {question}\n### Answer:',
+ optional: true,
+ additionalParams: true
+ }
+ ]
+ }
+ async init(nodeData, _, options) {
+ return null
+ // const timeout = nodeData.inputs?.timeout;
+ // const modelName = nodeData.inputs?.modelName;
+ // const basePath = nodeData.inputs?.basepath;
+ // const credentialData = await (0, utils_1.getCredentialData)(nodeData.credential ?? '', options);
+ // const openAIApiKey = (0, utils_1.getCredentialParam)('openAIApiKey', credentialData, nodeData);
+ // const obj = {
+ // apiKey: openAIApiKey,
+ // model: modelName
+ // };
+ // if (timeout)
+ // obj.timeout = parseInt(timeout, 10);
+ // if (basePath) {
+ // obj.additionalSessionOptions = {
+ // baseURL: basePath
+ // };
+ // }
+ // const model = new llamaindex_1.OpenAIEmbedding(obj);
+ // return model;
+ }
+}
+module.exports = { nodeClass: OPEA_LLM_CODEGEN }
+//# sourceMappingURL=OpenAIEmbedding_LlamaIndex.js.map
diff --git a/studio-frontend/packages/server/src/nodes/llm_docsum.js b/studio-frontend/packages/server/src/nodes/llm_docsum.js
new file mode 100644
index 0000000..417c717
--- /dev/null
+++ b/studio-frontend/packages/server/src/nodes/llm_docsum.js
@@ -0,0 +1,144 @@
+'use strict'
+Object.defineProperty(exports, '__esModule', { value: true })
+class OPEA_LLM_DOCSUM {
+ constructor() {
+ //@ts-ignore
+ // this.loadMethods = {
+ // async listModels() {
+ // return await (0, modelLoader_1.getModels)(modelLoader_1.MODEL_TYPE.EMBEDDING, 'openAIEmbedding_LlamaIndex');
+ // }
+ // };
+ this.label = 'LLM Document Summarization'
+ this.name = 'opea_service@llm_docsum'
+ this.version = 1.0
+ this.type = 'GeneratedDoc'
+ this.icon = 'assets/llm.png'
+ this.category = 'LLM'
+ this.description = 'LLM Document Summarization Inference'
+ this.baseClasses = [this.type, 'StreamingResponse']
+ this.tags = ['OPEA']
+ this.inMegaservice = true
+ this.dependent_services = {
+ 'tgi': {
+ 'modelName': '',
+ 'huggingFaceToken': ''
+ }
+ }
+ this.inputs = [
+ {
+ label: 'LLM Params Document',
+ name: 'text',
+ type: 'LLMParamsDoc|DocSumChatCompletionRequest'
+ },
+ {
+ label: 'Model Name',
+ name: 'modelName',
+ type: 'string',
+ default: 'Intel/neural-chat-7b-v3-3'
+ },
+ {
+ label: 'HuggingFace Token',
+ name: 'huggingFaceToken',
+ type: 'password',
+ optional: true,
+ },
+ {
+ label: 'Maximum Tokens',
+ name: 'max_tokens',
+ type: 'number',
+ default: 17,
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Top K',
+ name: 'top_k',
+ type: 'number',
+ default: 10,
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Top P',
+ name: 'top_p',
+ type: 'number',
+ default: 0.95,
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Typical P',
+ name: 'typical_p',
+ type: 'number',
+ default: 0.95,
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Temperature',
+ name: 'temperature',
+ type: 'number',
+ default: 0.01,
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Presence Penalty',
+ name: 'presence_penalty',
+ type: 'number',
+ default: 1.03,
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Frequency Penalty',
+ name: 'frequency_penalty',
+ type: 'number',
+ default: 0.0,
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Streaming',
+ name: 'streaming',
+ type: 'boolean',
+ default: true,
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Chat Template',
+ name: 'chat_template',
+ type: 'string',
+ rows: true,
+ default:
+ '### You are a helpful, respectful and honest assistant to help the user with questions.\n### Context: {context}\n### Question: {question}\n### Answer:',
+ optional: true,
+ additionalParams: true
+ }
+ ]
+ }
+ async init(nodeData, _, options) {
+ return null
+ // const timeout = nodeData.inputs?.timeout;
+ // const modelName = nodeData.inputs?.modelName;
+ // const basePath = nodeData.inputs?.basepath;
+ // const credentialData = await (0, utils_1.getCredentialData)(nodeData.credential ?? '', options);
+ // const openAIApiKey = (0, utils_1.getCredentialParam)('openAIApiKey', credentialData, nodeData);
+ // const obj = {
+ // apiKey: openAIApiKey,
+ // model: modelName
+ // };
+ // if (timeout)
+ // obj.timeout = parseInt(timeout, 10);
+ // if (basePath) {
+ // obj.additionalSessionOptions = {
+ // baseURL: basePath
+ // };
+ // }
+ // const model = new llamaindex_1.OpenAIEmbedding(obj);
+ // return model;
+ }
+}
+module.exports = { nodeClass: OPEA_LLM_DOCSUM }
+//# sourceMappingURL=OpenAIEmbedding_LlamaIndex.js.map
diff --git a/studio-frontend/packages/server/src/nodes/llm.js b/studio-frontend/packages/server/src/nodes/llm_tgi.js
similarity index 99%
rename from studio-frontend/packages/server/src/nodes/llm.js
rename to studio-frontend/packages/server/src/nodes/llm_tgi.js
index cd3614c..f8b31be 100644
--- a/studio-frontend/packages/server/src/nodes/llm.js
+++ b/studio-frontend/packages/server/src/nodes/llm_tgi.js
@@ -12,7 +12,7 @@ class OPEA_LLM_TGi {
this.name = 'opea_service@llm_tgi'
this.version = 1.0
this.type = 'GeneratedDoc'
- this.icon = 'llm.png'
+ this.icon = 'assets/llm.png'
this.category = 'LLM'
this.description = 'LLM Text Generation Inference'
this.baseClasses = [this.type, 'StreamingResponse', 'ChatCompletion']
diff --git a/studio-frontend/packages/server/src/nodes/rag_agent.js b/studio-frontend/packages/server/src/nodes/rag_agent.js
index ef367ad..0e0748f 100644
--- a/studio-frontend/packages/server/src/nodes/rag_agent.js
+++ b/studio-frontend/packages/server/src/nodes/rag_agent.js
@@ -6,7 +6,7 @@ class OPEARedisRetreiver {
this.name = 'opea_service@rag_agent'
this.version = 1.0
this.type = 'AgentTask'
- this.icon = 'opea-icon-color.svg'
+ this.icon = 'assets/opea-icon-color.svg'
this.category = 'Agent'
this.description = 'RAG Agent built on Langchain/Langgraph framework'
this.baseClasses = [this.type, 'ChatCompletionRequest']
diff --git a/studio-frontend/packages/server/src/nodes/redis_vector_store.js b/studio-frontend/packages/server/src/nodes/redis_vector_store.js
index 44c6adb..5af4ad8 100644
--- a/studio-frontend/packages/server/src/nodes/redis_vector_store.js
+++ b/studio-frontend/packages/server/src/nodes/redis_vector_store.js
@@ -6,7 +6,7 @@ class OPEARedisVectorStore {
this.name = 'redis_vector_store'
this.version = 1.0
this.type = 'EmbedDoc'
- this.icon = 'vector stores.png'
+ this.icon = 'assets/vector stores.png'
this.category = 'VectorStores'
this.description = 'Redis Vector Store'
this.baseClasses = [this.type]
diff --git a/studio-frontend/packages/server/src/nodes/retreiver_redis.js b/studio-frontend/packages/server/src/nodes/retreiver_redis.js
index 13f5ec4..e69de29 100644
--- a/studio-frontend/packages/server/src/nodes/retreiver_redis.js
+++ b/studio-frontend/packages/server/src/nodes/retreiver_redis.js
@@ -1,75 +0,0 @@
-'use strict'
-Object.defineProperty(exports, '__esModule', { value: true })
-class OPEARedisRetreiver {
- constructor() {
- this.label = 'Redis Retreiver'
- this.name = 'opea_service@retriever_redis'
- this.version = 1.0
- this.type = 'SearchedDoc'
- this.icon = 'retreiver.png'
- this.category = 'Retreiver'
- this.description = 'Redis Retreiver with Langchain'
- this.baseClasses = [this.type, 'RetrievalResponse', 'ChatCompletionRequest']
- this.tags = ['OPEA']
- this.inMegaservice = true
- this.dependent_services = {
- 'tei': {
- 'modelName': '',
- 'huggingFaceToken': ''
- }
- }
- this.inputs = [
- {
- label: 'Search Query',
- name: 'text',
- type: 'EmbedDoc|RetrievalRequest|ChatCompletionRequest'
- },
- {
- label: 'Redis Vector Store',
- name: 'vector_db',
- type: 'EmbedDoc'
- },
- {
- label: 'Model Name',
- name: 'modelName',
- type: 'string',
- default: 'BAAI/bge-base-en-v1.5'
- },
- {
- label: 'HuggingFace Token',
- name: 'huggingFaceToken',
- type: 'password',
- optional: true,
- },
- {
- label: 'Search Type',
- name: 'search_type',
- type: 'options',
- default: 'similarity',
- options: [
- {
- name: 'similarity',
- label: 'similarity'
- },
- {
- name: 'similarity_distance_threshold',
- label: 'similarity_distance_threshold'
- },
- {
- name: 'similarity_score_threshold',
- label: 'similarity_score_threshold'
- },
- {
- name: 'mmr',
- label: 'mmr'
- }
- ],
- optional: true,
- additionalParams: true,
- inferenceParams: true
- }
- ]
- }
-}
-
-module.exports = { nodeClass: OPEARedisRetreiver }
diff --git a/studio-frontend/packages/server/src/nodes/retriever_redis.js b/studio-frontend/packages/server/src/nodes/retriever_redis.js
index 39f47d2..d0251f4 100644
--- a/studio-frontend/packages/server/src/nodes/retriever_redis.js
+++ b/studio-frontend/packages/server/src/nodes/retriever_redis.js
@@ -6,7 +6,7 @@ class OPEARedisRetreiver {
this.name = 'opea_service@retriever_redis'
this.version = 1.0
this.type = 'SearchedDoc'
- this.icon = 'opea-icon-color.svg'
+ this.icon = 'assets/opea-icon-color.svg'
this.category = 'Retriever'
this.description = 'Redis Retreiver with Langchain'
this.baseClasses = [this.type, 'RetrievalResponse', 'ChatCompletionRequest']
diff --git a/studio-frontend/packages/server/src/nodes/sql_agent.js b/studio-frontend/packages/server/src/nodes/sql_agent.js
index e9f7816..777ae9f 100644
--- a/studio-frontend/packages/server/src/nodes/sql_agent.js
+++ b/studio-frontend/packages/server/src/nodes/sql_agent.js
@@ -6,7 +6,7 @@ class OPEARedisRetreiver {
this.name = 'opea_service@sql_agent'
this.version = 1.0
this.type = 'AgentTask'
- this.icon = 'opea-icon-color.svg'
+ this.icon = 'assets/opea-icon-color.svg'
this.category = 'Agent'
this.description = 'Agent specifically designed and optimized for answering questions aabout data in SQL databases.'
this.baseClasses = [this.type, 'ChatCompletionRequest']
diff --git a/studio-frontend/packages/server/src/nodes/supervisor_agent.js b/studio-frontend/packages/server/src/nodes/supervisor_agent.js
index 4835eb4..f36e054 100644
--- a/studio-frontend/packages/server/src/nodes/supervisor_agent.js
+++ b/studio-frontend/packages/server/src/nodes/supervisor_agent.js
@@ -6,7 +6,7 @@ class OPEARedisRetreiver {
this.name = 'opea_service@supervisor_agent'
this.version = 1.0
this.type = 'Agent'
- this.icon = 'opea-icon-color.svg'
+ this.icon = 'assets/opea-icon-color.svg'
this.category = 'Agent'
this.description = 'ReAct Supervisor Agent built on Langchain/Langgraph framework'
this.baseClasses = [this.type, 'ChatCompletionRequest']
diff --git a/studio-frontend/packages/server/src/nodes/tei_embedding.js b/studio-frontend/packages/server/src/nodes/tei_embedding.js
index 6dfe09c..def54a7 100644
--- a/studio-frontend/packages/server/src/nodes/tei_embedding.js
+++ b/studio-frontend/packages/server/src/nodes/tei_embedding.js
@@ -2,14 +2,14 @@
Object.defineProperty(exports, '__esModule', { value: true })
// const modelLoader_1 = require("../../../src/modelLoader");
// const utils_1 = require("../../../src/utils");
-const llamaindex_1 = require('llamaindex')
+// const llamaindex_1 = require('llamaindex')
class OPEAEmbeddings {
constructor() {
this.label = 'TEI Embedding Langchain'
this.name = 'opea_service@embedding_tei_langchain'
this.version = 1.0
this.type = 'EmbedDoc'
- this.icon = 'embeddings.png'
+ this.icon = 'assets/embeddings.png'
this.category = 'Embeddings'
this.description = 'Text Embedding Inference using Langchain'
this.baseClasses = [this.type, 'EmbeddingResponse', 'ChatCompletionRequest']
diff --git a/studio-frontend/packages/server/src/nodes/tei_reranking.js b/studio-frontend/packages/server/src/nodes/tei_reranking.js
index 4bdd8b4..4e29395 100644
--- a/studio-frontend/packages/server/src/nodes/tei_reranking.js
+++ b/studio-frontend/packages/server/src/nodes/tei_reranking.js
@@ -6,7 +6,7 @@ class OPEAReranking {
this.name = 'opea_service@reranking_tei'
this.version = 1.0
this.type = 'LLMParamsDoc'
- this.icon = 'reranking.png'
+ this.icon = 'assets/reranking.png'
this.category = 'Reranking'
this.description = 'TEI Reranking'
this.baseClasses = [this.type, 'RerankingResponse', 'ChatCompletionRequest']
diff --git a/studio-frontend/packages/ui/src/ui-component/table/FlowListTable.jsx b/studio-frontend/packages/ui/src/ui-component/table/FlowListTable.jsx
index 7b3dcc7..296adbc 100644
--- a/studio-frontend/packages/ui/src/ui-component/table/FlowListTable.jsx
+++ b/studio-frontend/packages/ui/src/ui-component/table/FlowListTable.jsx
@@ -317,22 +317,22 @@ export const FlowListTable = ({ data, images, isLoading, filterFunction, updateF
Workflow Name
-
+
- Sandbox Control
+ Sandbox Status
-
+
- Sandbox Status
+ Sandbox Control
@@ -341,7 +341,7 @@ export const FlowListTable = ({ data, images, isLoading, filterFunction, updateF
spacing={1}
justifyContent='center'
>
- Launch App
+ Open Sandbox
@@ -483,6 +483,20 @@ export const FlowListTable = ({ data, images, isLoading, filterFunction, updateF
+
+
+ {row.sandboxStatus === "Getting Ready" || row.sandboxStatus === "Stopping" ? (
+
+ ) : null
+ }
+ {row.sandboxStatus}
+
+
-
-
- {row.sandboxStatus === "Getting Ready" || row.sandboxStatus === "Stopping" ? (
-
- ) : null
- }
- {row.sandboxStatus}
-
-
setObservabilityAnchorEl(null)}
>
{
handleOpenUrl(row.sandboxGrafanaUrl);
setObservabilityAnchorEl(null);
@@ -597,6 +598,7 @@ export const FlowListTable = ({ data, images, isLoading, filterFunction, updateF
Monitoring Dashboard
{
handleOpenUrl(row.sandboxTracerUrl);
setObservabilityAnchorEl(null);
@@ -606,6 +608,7 @@ export const FlowListTable = ({ data, images, isLoading, filterFunction, updateF
LLM Call Traces
{
handleOpenUrl(row.sandboxDebugLogsUrl);
setObservabilityAnchorEl(null);
diff --git a/tests/playwright/studio-e2e/001_test_sandbox_deployment.spec.ts b/tests/playwright/studio-e2e/001_test_sandbox_deployment.spec.ts
index b2aa007..e87a6a8 100644
--- a/tests/playwright/studio-e2e/001_test_sandbox_deployment.spec.ts
+++ b/tests/playwright/studio-e2e/001_test_sandbox_deployment.spec.ts
@@ -45,33 +45,35 @@ test('001_test_sandbox_deployment', async ({ browser, baseURL }) => {
const page2Promise = page.waitForEvent('popup');
await page.getByLabel('Click to open Application UI').getByRole('button').nth(0).click();
const page2 = await page2Promise;
- await expect(page2.getByRole('heading', { name: 'OPEA Studio' })).toBeVisible();
+ await expect(page2.getByRole('button', { name: 'opea logo OPEA Studio' })).toBeVisible();
await page.bringToFront();
// Open Dashboard - update the locator for V1.4
- await page.getByRole('cell', { name: 'Observability Options' }).getByRole('button').click();
const page3Promise = page.waitForEvent('popup');
+ await page.getByRole('row', { name: 'test_001 Ready Stop Sandbox' }).getByLabel('Observability Options').getByRole('button').click();
await page.getByRole('menuitem', { name: 'Monitoring Dashboard' }).click();
const page3 = await page3Promise;
await expect(page3.getByRole('link', { name: 'Grafana' })).toBeVisible({ timeout: 60000 });
await page.bringToFront();
// Open Trace - new for V1.4
- await page.getByRole('cell', { name: 'Observability Options' }).getByRole('button').click();
const page4Promise = page.waitForEvent('popup');
+ await page.getByRole('row', { name: 'test_001 Ready Stop Sandbox' }).getByLabel('Observability Options').getByRole('button').click();
await page.getByRole('menuitem', { name: 'LLM Call Traces' }).click();
const page4 = await page4Promise;
await expect(page4.getByText('No traces found')).toHaveText(/No traces found/, { timeout: 60000 });
await page.bringToFront();
// Open Debug Logs - new for V1.4
- await page.getByRole('cell', { name: 'Observability Options' }).getByRole('button').click();
const page5Promise = page.waitForEvent('popup');
+ await page.getByRole('row', { name: 'test_001 Ready Stop Sandbox' }).getByLabel('Observability Options').getByRole('button').click();
await page.getByRole('menuitem', { name: 'Debug Logs' }).click();
const page5 = await page5Promise;
await expect(page5.getByRole('heading', { name: 'Workflow name: test_001' })).toHaveText(/Workflow name: test_001/, { timeout: 60000 });
await page.bringToFront();
+
+
// Generate Deployment Package - to be deleted
//await page.getByLabel('Generate Deployment Package').getByRole('button').nth(0).click();
//const downloadPromise = page.waitForEvent('download');
@@ -83,10 +85,10 @@ test('001_test_sandbox_deployment', async ({ browser, baseURL }) => {
//expect(fs.existsSync(downloadPath)).toBe(true);
// Stop & Delete Sandbox
- await page.getByRole('button', { name: 'Stop Sandbox' }).click();
+ await page.getByRole('row', { name: 'test_001 Ready Stop Sandbox' }).getByLabel('Stop Sandbox').click();
// await expect(page.locator('td.MuiTableCell-root div.MuiStack-root p.MuiTypography-root').first()).toHaveText('Not Running', { timeout: statusChangeTimeout });
await waitForStatusText(page, 'td.MuiTableCell-root div.MuiStack-root p.MuiTypography-root', 'Not Running', 5, 60000);
- await page.locator('#demo-customized-button').click();
+ await page.getByRole('row', { name: 'test_001 Not Running Run' }).locator('#demo-customized-button').click();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'Delete' }).click();
diff --git a/tests/playwright/studio-e2e/002_test_sandbox_chatqna.spec.ts b/tests/playwright/studio-e2e/002_test_sandbox_chatqna.spec.ts
index e89db36..c0809b6 100644
--- a/tests/playwright/studio-e2e/002_test_sandbox_chatqna.spec.ts
+++ b/tests/playwright/studio-e2e/002_test_sandbox_chatqna.spec.ts
@@ -67,7 +67,7 @@ test('002_test_sandbox_chatqna', async ({ browser, baseURL }) => {
await page.goto(IDC_URL);
await expect(page.locator('td.MuiTableCell-root div.MuiStack-root p.MuiTypography-root').first()).toHaveText('Not Running', { timeout: 60000 });
await page.getByLabel('a dense table').locator('button').first().click();
- await waitForStatusText(page, 'td.MuiTableCell-root div.MuiStack-root p.MuiTypography-root', 'Ready', 20, 60000);
+ await waitForStatusText(page, 'td.MuiTableCell-root div.MuiStack-root p.MuiTypography-root', 'Getting Ready', 20, 60000);
await page.waitForTimeout(10000);
// Open APP-UI
@@ -93,8 +93,8 @@ test('002_test_sandbox_chatqna', async ({ browser, baseURL }) => {
// }
// }
await page2.waitForTimeout(2000);
- await page2.getByPlaceholder('Ask a question').fill(question);
- await page2.getByRole('button').nth(4).click();
+ await page2.getByRole('textbox', { name: 'Enter your message' }).fill(question);
+ await page2.getByRole('button').filter({ hasText: /^$/ }).nth(2).click();
await page2.waitForTimeout(20000);
let responseContainsKeyword = apiResponse && containsAnyKeyword(apiResponse.value, keywords);
console.log ('response:', apiResponse.value);
@@ -105,61 +105,77 @@ test('002_test_sandbox_chatqna', async ({ browser, baseURL }) => {
}
apiResponse.value = "";
- await page2.getByRole('button').nth(2).click();
- await page2.getByRole('button').nth(2).click(); // Double click
+ await page2.getByRole('button').nth(2).dblclick();
+ //await page2.getByRole('button').nth(2).click(); // Double click
// Document Upload 1
+ await page2.getByRole('button', { name: 'Open Sidebar' }).click();
+ await page2.getByRole('link', { name: 'Data Management' }).click();
fileChooserPromise = page2.waitForEvent('filechooser');
- await page2.getByRole('button', { name: 'Choose File' }).click()
+ await page2.getByRole('button', { name: 'Browse Files' }).click();
fileChooser = await fileChooserPromise;
await fileChooser.setFiles(uploadPDF1);
- await page2.getByRole('button', { name: 'Upload', exact: true }).click();
- await page2.waitForSelector('tr:nth-of-type(1) button[data-variant="light"] .tabler-icon-check', { state: 'visible', timeout: 300000 });
+ await page2.getByRole('button', { name: 'Upload' }).click();
+ await page2.getByRole('button', { name: 'Agree and Continue' }).click();
+ //await page2.waitForSelector('tr:nth-of-type(1) button[data-variant="light"] .tabler-icon-check', { state: 'visible', timeout: 50000 });
+ await page2.getByText('DocumentsData SourceUpload or').isVisible();
+ await page2.waitForTimeout(20000);
+
// Refresh page and verify upload with retry
let isVisible1 = false;
for (let i = 0; i < 2; i++) {
- await page2.reload();
- await page2.waitForTimeout(1500);
- await page2.getByRole('button').nth(2).click();
+ //await page2.reload();
+ //await page2.waitForTimeout(1500);
+ //await page2.getByRole('button').nth(2).click();
try {
- await expect(page2.getByRole('cell', { name: 'tennis_tutorial.pdf' })).toBeVisible({ timeout: 60000 });
+ await expect(page2.locator('div').filter({ hasText: /^tennis_tutorial\.pdf$/ })).toBeVisible({ timeout: 30000 });
isVisible1 = true;
break;
} catch (error) {
console.log(`Attempt ${i + 1} failed: ${error}`);
}
}
- await page2.waitForTimeout(1000);
+ await page2.waitForTimeout(10000);
// Document Upload 2
fileChooserPromise = page2.waitForEvent('filechooser');
- await page2.getByRole('button', { name: 'Choose File' }).click()
+ await page2.getByRole('button', { name: 'Browse Files' }).click();
fileChooser = await fileChooserPromise;
await fileChooser.setFiles(uploadPDF2);
- await page2.getByRole('button', { name: 'Upload', exact: true }).click();
- await page2.waitForSelector('tr:nth-of-type(2) button[data-variant="light"] .tabler-icon-check', { state: 'visible', timeout: 300000 });
+ await page2.getByRole('button', { name: 'Upload' }).click();
+ await page2.getByRole('button', { name: 'Agree and Continue' }).click();
+ //await page2.waitForSelector('tr:nth-of-type(1) button[data-variant="light"] .tabler-icon-check', { state: 'visible', timeout: 50000 });
+ await page2.getByText('DocumentsData SourceUpload or').isVisible();
+ await page2.waitForTimeout(20000);
+
// Refresh page and verify upload with retry
let isVisible2 = false;
for (let i = 0; i < 2; i++) {
- await page2.reload();
- await page2.waitForTimeout(1500);
- await page2.getByRole('button').nth(2).click();
+ //await page2.reload();
+ //await page2.waitForTimeout(1500);
+ //await page2.getByRole('button').nth(2).click();
try {
- await expect(page2.getByRole('cell', { name: 'Q3 24_EarningsRelease' })).toBeVisible({ timeout: 60000 });
+ await expect(page2.locator('div').filter({ hasText: /^Q3 24_EarningsRelease\.pdf$/ })).toBeVisible({ timeout: 30000 });
isVisible2 = true;
break;
} catch (error) {
console.log(`Attempt ${i + 1} failed: ${error}`);
}
}
- await page2.waitForTimeout(1000);
+ await page2.waitForTimeout(10000);
// Link Upload
- await page2.getByRole('button', { name: 'Use Link' }).click();
- await page2.getByPlaceholder('URL').click();
- await page2.getByPlaceholder('URL').fill('https://pnatraj.medium.com/kubectl-exec-plugin-invalid-apiversion-client-authentication-k8s-io-v1alpha1-870aace51998');
- await page2.getByRole('button', { name: 'Upload', exact: true }).click();
- await page2.waitForSelector('tr:nth-of-type(3) button[data-variant="light"] .tabler-icon-check', { state: 'visible', timeout: 300000 });
+ await page2.getByRole('button', { name: 'Documents' }).click();
+ await page2.getByRole('menuitem', { name: 'Web' }).click();
+ await page2.getByRole('textbox', { name: 'Enter a Web URL' }).click();
+ await page2.getByRole('textbox', { name: 'Enter a Web URL' }).fill('https://pnatraj.medium.com/kubectl-exec-plugin-invalid-apiversion-client-authentication-k8s-io-v1alpha1-870aace51998');
+ await page2.getByTestId('AddCircleIcon').click();
+ await page2.waitForTimeout(10000);
+ await page2.getByText('DocumentsData SourceUpload or').isVisible();
+ // await page2.getByRole('button', { name: 'Use Link' }).click();
+ //await page2.getByPlaceholder('URL').click();
+ //await page2.getByPlaceholder('URL').fill('https://pnatraj.medium.com/kubectl-exec-plugin-invalid-apiversion-client-authentication-k8s-io-v1alpha1-870aace51998');
+ //await page2.getByRole('button', { name: 'Upload', exact: true }).click();
// Refresh page and verify upload with retry
let isVisible3 = false;
for (let i = 0; i < 2; i++) {
@@ -167,7 +183,7 @@ test('002_test_sandbox_chatqna', async ({ browser, baseURL }) => {
await page2.waitForTimeout(1500);
await page2.getByRole('button').nth(2).click();
try {
- await expect(page2.getByRole('cell', { name: 'https://pnatraj.medium.com/' })).toBeVisible({ timeout: 60000 });
+ await expect(page2.getByRole('listitem').filter({ hasText: 'https://pnatraj.medium.com/' }).locator('div')).toBeVisible;
await page2.screenshot({ path: 'screenshot_upload_document.png' });
isVisible3 = true;
@@ -178,8 +194,8 @@ test('002_test_sandbox_chatqna', async ({ browser, baseURL }) => {
}
await page2.waitForTimeout(1000);
- await page2.getByRole('banner').getByRole('button').click();
- await page2.waitForTimeout(10000);
+ //await page2.getByRole('banner').getByRole('button').click();
+ //await page2.waitForTimeout(10000);
// Chat Attempt 2
// const buttons = page2.getByRole('button');
@@ -196,8 +212,9 @@ test('002_test_sandbox_chatqna', async ({ browser, baseURL }) => {
// }
// }
console.log ('Chat Attempt 2-------------------');
- await page2.getByPlaceholder('Ask a question').fill(question);
- await page2.getByRole('button').nth(4).click();
+ await page2.getByRole('button', { name: 'New Chat' }).click();
+ await page2.getByRole('textbox', { name: 'Enter your message' }).fill(question);
+ await page2.getByRole('button').filter({ hasText: /^$/ }).nth(2).click();
await page2.waitForTimeout(30000);
responseContainsKeyword = apiResponse && containsAnyKeyword(apiResponse.value, keywords);
console.log ('response:', apiResponse.value);
@@ -209,8 +226,8 @@ test('002_test_sandbox_chatqna', async ({ browser, baseURL }) => {
// Ask another question
const followUpQuestion = "How is Intel performing in Q3 2024?";
- await page2.getByPlaceholder('Ask a question').fill(followUpQuestion);
- await page2.getByRole('button').nth(4).click();
+ await page2.getByRole('textbox', { name: 'Enter your message' }).fill(followUpQuestion);
+ await page2.getByRole('button').filter({ hasText: /^$/ }).nth(2).click();
await page2.waitForTimeout(30000);
responseContainsKeyword = apiResponse && containsAnyKeyword(apiResponse.value, keywords);
@@ -240,9 +257,12 @@ test('002_test_sandbox_chatqna', async ({ browser, baseURL }) => {
}
console.log ('Delete 1 document + Check data sources successfully deduct 1 or not-------------------');
- await page2.getByRole('button').nth(3).click();
- await page2.getByRole('row', { name: 'tennis_tutorial.pdf' }).getByRole('button').click();
- await expect(page2.getByRole('cell', { name: 'tennis_tutorial.pdf' })).toBeHidden( { timeout: 60000 } );
+ await page2.getByRole('button', { name: 'Open Sidebar' }).click();
+ await page2.getByRole('link', { name: 'Data Management' }).click();
+ await page2.getByRole('button', { name: 'Select' }).click();
+ await page2.getByRole('checkbox', { name: 'tennis_tutorial.pdf' }).check();
+ await page2.getByRole('button', { name: 'Delete Selected' }).click();
+ await expect(page2.locator('div').filter({ hasText: /^tennis_tutorial\.pdf$/ })).toBeHidden( { timeout: 60000 } );
await page2.screenshot({ path: 'screenshot_delete_file.png' });
console.log ("Document tennis_tutorial.pdf deleted");