diff --git a/.github/workflows/scripts/build_push.sh b/.github/workflows/scripts/build_push.sh index 58e92a208..cb17027ad 100755 --- a/.github/workflows/scripts/build_push.sh +++ b/.github/workflows/scripts/build_push.sh @@ -55,6 +55,9 @@ for MEGA_SVC in $1; do if [ "$MEGA_SVC" == "ChatQnA" ];then docker_build ${IMAGE_NAME}-conversation-ui docker/Dockerfile.react fi + if [ "$MEGA_SVC" == "DocSum" ];then + docker_build ${IMAGE_NAME}-react-ui docker/Dockerfile.react + fi if [ "$MEGA_SVC" == "CodeGen" ];then docker_build ${IMAGE_NAME}-react-ui docker/Dockerfile.react fi diff --git a/DocSum/assets/img/docsum-ui-react-error.png b/DocSum/assets/img/docsum-ui-react-error.png new file mode 100644 index 000000000..de3cfedaf Binary files /dev/null and b/DocSum/assets/img/docsum-ui-react-error.png differ diff --git a/DocSum/assets/img/docsum-ui-react-file.png b/DocSum/assets/img/docsum-ui-react-file.png new file mode 100644 index 000000000..d4c18ebd4 Binary files /dev/null and b/DocSum/assets/img/docsum-ui-react-file.png differ diff --git a/DocSum/assets/img/docsum-ui-react.png b/DocSum/assets/img/docsum-ui-react.png new file mode 100644 index 000000000..9034bc84f Binary files /dev/null and b/DocSum/assets/img/docsum-ui-react.png differ diff --git a/DocSum/docker/gaudi/README.md b/DocSum/docker/gaudi/README.md index ace3c5c0d..290972d8b 100644 --- a/DocSum/docker/gaudi/README.md +++ b/DocSum/docker/gaudi/README.md @@ -44,12 +44,23 @@ cd GenAIExamples/DocSum/docker/ui/ docker build -t opea/docsum-ui:latest --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy -f ./docker/Dockerfile . ``` +### 5. Build React UI Docker Image + +Build the frontend Docker image via below command: + +```bash +cd GenAIExamples/DocSum/docker/ui/ +export BACKEND_SERVICE_ENDPOINT="http://${host_ip}:8888/v1/docsum" +docker build -t opea/docsum-react-ui:latest --build-arg BACKEND_SERVICE_ENDPOINT=$BACKEND_SERVICE_ENDPOINT -f ./docker/Dockerfile.react . +``` + Then run the command `docker images`, you will have the following Docker Images: 1. `ghcr.io/huggingface/tgi-gaudi:2.0.1` 2. `opea/llm-docsum-tgi:latest` 3. `opea/docsum:latest` 4. `opea/docsum-ui:latest` +5. `opea/docsum-react-ui:latest` ## 🚀 Start Microservices and MegaService @@ -125,7 +136,7 @@ export LANGCHAIN_TRACING_V2=true export LANGCHAIN_API_KEY=ls_... ``` -## 🚀 Launch the UI +## 🚀 Launch the Svelte UI Open this URL `http://{host_ip}:5173` in your browser to access the frontend. @@ -134,3 +145,9 @@ Open this URL `http://{host_ip}:5173` in your browser to access the frontend. Here is an example for summarizing a article. ![image](https://github.com/intel-ai-tce/GenAIExamples/assets/21761437/67ecb2ec-408d-4e81-b124-6ded6b833f55) + +## 🚀 Launch the React UI + +Open this URL `http://{host_ip}:5175` in your browser to access the frontend. + +![project-screenshot](../../assets/img/docsum-ui-react.png) diff --git a/DocSum/docker/gaudi/docker_compose.yaml b/DocSum/docker/gaudi/docker_compose.yaml index d2984a125..a381a9f42 100644 --- a/DocSum/docker/gaudi/docker_compose.yaml +++ b/DocSum/docker/gaudi/docker_compose.yaml @@ -71,6 +71,18 @@ services: - DOC_BASE_URL=${BACKEND_SERVICE_ENDPOINT} ipc: host restart: always + docsum-xeon-react-ui-server: + image: opea/docsum-react-ui:latest + container_name: docsum-gaudi-react-ui-server + depends_on: + - docsum-gaudi-backend-server + build: + args: + - BACKEND_SERVICE_ENDPOINT=${BACKEND_SERVICE_ENDPOINT} + ports: + - "5174:80" + environment: + - DOC_BASE_URL=${BACKEND_SERVICE_ENDPOINT} networks: default: diff --git a/DocSum/docker/ui/docker/Dockerfile.react b/DocSum/docker/ui/docker/Dockerfile.react new file mode 100644 index 000000000..aa8f3fe78 --- /dev/null +++ b/DocSum/docker/ui/docker/Dockerfile.react @@ -0,0 +1,24 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# Use node 20.11.1 as the base image +FROM node:20.11.1 as vite-app + +COPY . /usr/app +WORKDIR /usr/app/react + +ARG BACKEND_SERVICE_ENDPOINT +ENV VITE_DOC_SUM_URL=$BACKEND_SERVICE_ENDPOINT + +RUN ["npm", "install"] +RUN ["npm", "run", "build"] + + +FROM nginx:alpine +EXPOSE 80 + + +COPY --from=vite-app /usr/app/react/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=vite-app /usr/app/react/dist /usr/share/nginx/html + +ENTRYPOINT ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/DocSum/docker/ui/react/.env b/DocSum/docker/ui/react/.env new file mode 100644 index 000000000..88e4996a2 --- /dev/null +++ b/DocSum/docker/ui/react/.env @@ -0,0 +1 @@ +VITE_DOC_SUM_URL=http://backend_address:8888/v1/docsum \ No newline at end of file diff --git a/DocSum/docker/ui/react/.eslintrc.cjs b/DocSum/docker/ui/react/.eslintrc.cjs new file mode 100644 index 000000000..78174f683 --- /dev/null +++ b/DocSum/docker/ui/react/.eslintrc.cjs @@ -0,0 +1,11 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended"], + ignorePatterns: ["dist", ".eslintrc.cjs"], + parser: "@typescript-eslint/parser", + plugins: ["react-refresh"], + rules: { + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], + }, +}; diff --git a/DocSum/docker/ui/react/.gitignore b/DocSum/docker/ui/react/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/DocSum/docker/ui/react/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/DocSum/docker/ui/react/README.md b/DocSum/docker/ui/react/README.md new file mode 100644 index 000000000..819469e73 --- /dev/null +++ b/DocSum/docker/ui/react/README.md @@ -0,0 +1,29 @@ +

Doc Summary React

+ +### 📸 Project Screenshots + +![project-screenshot](../../../assets/img/docsum-ui-react.png) +![project-screenshot](../../../assets/img/docsum-ui-react-file.png) +![project-screenshot](../../../assets/img/docsum-ui-react-error.png) + +

🧐 Features

+ +Here're some of the project's features: + +- Summarizing Uploaded Files: Upload files from their local device, then click 'Generate Summary' to summarize the content of the uploaded file. The summary will be displayed on the 'Summary' box. +- Summarizing Text via Pasting: Paste the text to be summarized into the text box, then click 'Generate Summary' to produce a condensed summary of the content, which will be displayed in the 'Summary' box on the right. +- Scroll to Bottom: The summarized content will automatically scroll to the bottom. + +

🛠️ Get it Running:

+ +1. Clone the repo. + +2. cd command to the current folder. + +3. Modify the required .env variables. + ``` + VITE_DOC_SUM_URL = '' + ``` +4. Execute `npm install` to install the corresponding dependencies. + +5. Execute `npm run dev` in both environments diff --git a/DocSum/docker/ui/react/index.html b/DocSum/docker/ui/react/index.html new file mode 100644 index 000000000..bedf57052 --- /dev/null +++ b/DocSum/docker/ui/react/index.html @@ -0,0 +1,18 @@ + + + + + + + + + Opea Doc Sum + + +
+ + + diff --git a/DocSum/docker/ui/react/nginx.conf b/DocSum/docker/ui/react/nginx.conf new file mode 100644 index 000000000..00433fcda --- /dev/null +++ b/DocSum/docker/ui/react/nginx.conf @@ -0,0 +1,20 @@ +server { + listen 80; + + gzip on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types font/woff2 text/css application/javascript application/json application/font-woff application/font-tff image/gif image/png image/svg+xml application/octet-stream; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html =404; + + location ~* \.(gif|jpe?g|png|webp|ico|svg|css|js|mp4|woff2)$ { + expires 1d; + } + } +} \ No newline at end of file diff --git a/DocSum/docker/ui/react/package.json b/DocSum/docker/ui/react/package.json new file mode 100644 index 000000000..1d88b1c6b --- /dev/null +++ b/DocSum/docker/ui/react/package.json @@ -0,0 +1,52 @@ +{ + "name": "ui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview", + "test": "vitest" + }, + "dependencies": { + "@mantine/core": "^7.11.1", + "@mantine/dropzone": "^7.11.1", + "@mantine/hooks": "^7.11.1", + "@mantine/notifications": "^7.11.1", + "@microsoft/fetch-event-source": "^2.0.1", + "@reduxjs/toolkit": "^2.2.5", + "@tabler/icons-react": "^3.9.0", + "axios": "^1.7.2", + "luxon": "^3.4.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-markdown": "^9.0.1", + "react-syntax-highlighter": "^15.5.0", + "remark-frontmatter": "^5.0.0", + "remark-gfm": "^4.0.0" + }, + "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", + "@types/react-syntax-highlighter": "^15.5.13", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "jsdom": "^24.1.0", + "postcss": "^8.4.38", + "postcss-preset-mantine": "^1.15.0", + "postcss-simple-vars": "^7.0.1", + "sass": "1.64.2", + "typescript": "^5.2.2", + "vite": "^5.2.13", + "vitest": "^1.6.0" + } +} diff --git a/DocSum/docker/ui/react/postcss.config.cjs b/DocSum/docker/ui/react/postcss.config.cjs new file mode 100644 index 000000000..e817f567b --- /dev/null +++ b/DocSum/docker/ui/react/postcss.config.cjs @@ -0,0 +1,14 @@ +module.exports = { + plugins: { + "postcss-preset-mantine": {}, + "postcss-simple-vars": { + variables: { + "mantine-breakpoint-xs": "36em", + "mantine-breakpoint-sm": "48em", + "mantine-breakpoint-md": "62em", + "mantine-breakpoint-lg": "75em", + "mantine-breakpoint-xl": "88em", + }, + }, + }, +}; diff --git a/DocSum/docker/ui/react/src/App.scss b/DocSum/docker/ui/react/src/App.scss new file mode 100644 index 000000000..187764a17 --- /dev/null +++ b/DocSum/docker/ui/react/src/App.scss @@ -0,0 +1,42 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +@import "./styles/styles"; + +.root { + @include flex(row, nowrap, flex-start, flex-start); +} + +.layout-wrapper { + @include absolutes; + + display: grid; + + width: 100%; + height: 100%; + + grid-template-columns: 80px auto; + grid-template-rows: 1fr; +} + +/* ===== Scrollbar CSS ===== */ +/* Firefox */ +* { + scrollbar-width: thin; + scrollbar-color: #d6d6d6 #ffffff; +} + +/* Chrome, Edge, and Safari */ +*::-webkit-scrollbar { + width: 8px; +} + +*::-webkit-scrollbar-track { + background: #ffffff; +} + +*::-webkit-scrollbar-thumb { + background-color: #d6d6d6; + border-radius: 16px; + border: 4px double #dedede; +} diff --git a/DocSum/docker/ui/react/src/App.tsx b/DocSum/docker/ui/react/src/App.tsx new file mode 100644 index 000000000..f2fd996bf --- /dev/null +++ b/DocSum/docker/ui/react/src/App.tsx @@ -0,0 +1,32 @@ +// 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 { IconFileText } from "@tabler/icons-react" +import { Notifications } from '@mantine/notifications'; +import DocSum from "./components/DocSum/DocSum"; + +const title = "Doc Summary" +const navList: SidebarNavList = [ + { icon: IconFileText, label: title } +] + +function App() { + + return ( + + +
+ +
+ +
+
+
+ ) +} + +export default App diff --git a/DocSum/docker/ui/react/src/__tests__/util.test.ts b/DocSum/docker/ui/react/src/__tests__/util.test.ts new file mode 100644 index 000000000..e67ba2c86 --- /dev/null +++ b/DocSum/docker/ui/react/src/__tests__/util.test.ts @@ -0,0 +1,14 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, test } from "vitest"; +import { getCurrentTimeStamp, uuidv4 } from "../common/util"; + +describe("unit tests", () => { + test("check UUID is of length 36", () => { + expect(uuidv4()).toHaveLength(36); + }); + test("check TimeStamp generated is of unix", () => { + expect(getCurrentTimeStamp()).toBe(Math.floor(Date.now() / 1000)); + }); +}); diff --git a/DocSum/docker/ui/react/src/assets/opea-icon-black.svg b/DocSum/docker/ui/react/src/assets/opea-icon-black.svg new file mode 100644 index 000000000..5c96dc762 --- /dev/null +++ b/DocSum/docker/ui/react/src/assets/opea-icon-black.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DocSum/docker/ui/react/src/assets/opea-icon-color.svg b/DocSum/docker/ui/react/src/assets/opea-icon-color.svg new file mode 100644 index 000000000..790151171 --- /dev/null +++ b/DocSum/docker/ui/react/src/assets/opea-icon-color.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DocSum/docker/ui/react/src/common/client.ts b/DocSum/docker/ui/react/src/common/client.ts new file mode 100644 index 000000000..7512f73e3 --- /dev/null +++ b/DocSum/docker/ui/react/src/common/client.ts @@ -0,0 +1,8 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import axios from "axios"; + +//add iterceptors to add any request headers + +export default axios; diff --git a/DocSum/docker/ui/react/src/common/util.ts b/DocSum/docker/ui/react/src/common/util.ts new file mode 100644 index 000000000..df65b2d8e --- /dev/null +++ b/DocSum/docker/ui/react/src/common/util.ts @@ -0,0 +1,12 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +export const getCurrentTimeStamp = () => { + return Math.floor(Date.now() / 1000); +}; + +export const uuidv4 = () => { + return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) => + (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16), + ); +}; diff --git a/DocSum/docker/ui/react/src/components/DocSum/DocSum.tsx b/DocSum/docker/ui/react/src/components/DocSum/DocSum.tsx new file mode 100644 index 000000000..9e7472c65 --- /dev/null +++ b/DocSum/docker/ui/react/src/components/DocSum/DocSum.tsx @@ -0,0 +1,153 @@ +import styleClasses from './docSum.module.scss' +import { Button, Text, Textarea, Title } from '@mantine/core' +import { FileUpload } from './FileUpload' +import { useEffect, useState } from 'react' +import Markdown from '../Shared/Markdown/Markdown' +import { fetchEventSource } from '@microsoft/fetch-event-source' +import { notifications } from '@mantine/notifications' +import { DOC_SUM_URL } from '../../config' +import { FileWithPath } from '@mantine/dropzone' + + +const DocSum = () => { + const [isFile, setIsFile] = useState(false); + const [files, setFiles] = useState([]) + const [isGenerating, setIsGenerating] = useState(false); + const [value, setValue] = useState(''); + const [fileContent, setFileContent] = useState(''); + const [response, setResponse] = useState(''); + + useEffect(() => { + if(isFile){ + setValue('') + } + },[isFile]) + + useEffect(()=>{ + if (files.length) { + const reader = new FileReader() + reader.onload = async () => { + const text = reader.result?.toString() + setFileContent(text || '') + }; + reader.readAsText(files[0]) + } + },[files]) + + + const handleSubmit = async () => { + setResponse("") + if(!isFile && !value){ + notifications.show({ + color: "red", + id: "input", + message: "Please Upload Content", + }) + return + } + + setIsGenerating(true) + const body = { + messages: isFile ? fileContent : value + } + fetchEventSource(DOC_SUM_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Accept": "*/*" + }, + body: JSON.stringify(body), + openWhenHidden: true, + async onopen(response) { + if (response.ok) { + return; + } else if (response.status >= 400 && response.status < 500 && response.status !== 429) { + const e = await response.json(); + console.log(e); + throw Error(e.error.message); + } else { + console.log("error", response); + } + }, + onmessage(msg) { + if (msg?.data != "[DONE]") { + try { + const res = JSON.parse(msg.data) + 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 + ) { + setResponse(prev=>prev+log.value); + } + } + }); + } catch (e) { + console.log("something wrong in msg", e); + throw e; + } + } + }, + onerror(err) { + console.log("error", err); + setIsGenerating(false) + throw err; + }, + onclose() { + setIsGenerating(false) + }, + }); +} + + + return ( +
+
+
+
+ Doc Summary +
+
+ Please upload file or paste content for summarization. +
+
+ + + + +
+
+ {isFile ? ( +
+ { setFiles(files) }} /> +
+ ) : ( +
+