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 +
- + 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 && ( + + + + + {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 }) => ( +
+ {children}
+
+ ), + 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 && ( - - )} -
- -
- - - - Inference Settings - Token Limit: {tokenLimit} - - Temperature: {temperature.toFixed(2)} - -