diff --git a/.github/workflows/scripts/build_push.sh b/.github/workflows/scripts/build_push.sh index 8502ea6a2..58e92a208 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" == "CodeGen" ];then + docker_build ${IMAGE_NAME}-react-ui docker/Dockerfile.react + fi ;; "VisualQnA") echo "Not supported yet" diff --git a/ChatQnA/docker/ui/docker/Dockerfile.react b/ChatQnA/docker/ui/docker/Dockerfile.react index 277546a9b..8ec70c657 100644 --- a/ChatQnA/docker/ui/docker/Dockerfile.react +++ b/ChatQnA/docker/ui/docker/Dockerfile.react @@ -1,4 +1,8 @@ -FROM node as vite-app +# 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 diff --git a/CodeGen/assets/img/codegen_react.png b/CodeGen/assets/img/codegen_react.png new file mode 100644 index 000000000..f2417966b Binary files /dev/null and b/CodeGen/assets/img/codegen_react.png differ diff --git a/CodeGen/docker/gaudi/README.md b/CodeGen/docker/gaudi/README.md index 05f0d2056..614e73751 100644 --- a/CodeGen/docker/gaudi/README.md +++ b/CodeGen/docker/gaudi/README.md @@ -38,11 +38,21 @@ cd GenAIExamples/CodeGen/docker/ui/ docker build -t opea/codegen-ui:latest --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy -f ./docker/Dockerfile . ``` +### 5. Build the React UI Docker Image + +Construct the React frontend Docker image via the command below: + +```bash +cd GenAIExamples/CodeGen/docker/ui/ +docker build -t opea/codegen-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 3 Docker images: - `opea/llm-tgi:latest` - `opea/codegen:latest` - `opea/codegen-ui:latest` +- `opea/codegen-react-ui:latest` ## 🚀 Start MicroServices and MegaService @@ -143,12 +153,12 @@ export LANGCHAIN_TRACING_V2=true export LANGCHAIN_API_KEY=ls_... ``` -## 🚀 Launch the UI +## 🚀 Launch the Svelte Based UI To access the frontend, open the following URL in your browser: `http://{host_ip}:5173`. By default, the UI runs on port 5173 internally. If you prefer to use a different host port to access the frontend, you can modify the port mapping in the `docker_compose.yaml` file as shown below: ```yaml - codegen-xeon-ui-server: + codegen-gaudi-ui-server: image: opea/codegen-ui:latest ... ports: @@ -157,6 +167,20 @@ To access the frontend, open the following URL in your browser: `http://{host_ip ![project-screenshot](../../assets/img/codeGen_ui_init.jpg) +## 🚀 Launch the React Based UI + +To access the frontend, open the following URL in your browser: `http://{host_ip}:5174`. By default, the UI runs on port 5174 internally. If you prefer to use a different host port to access the frontend, you can modify the port mapping in the `docker_compose.yaml` file as shown below: + +```yaml + codegen-gaudi-react-ui-server: + image: opea/codegen-react-ui:latest + ... + ports: + - "80:5174" +``` + +![project-screenshot](../../assets/img/codegen_react.png) + ## Install Copilot VSCode extension from Plugin Marketplace as the frontend In addition to the Svelte UI, users can also install the Copilot VSCode extension from the Plugin Marketplace as the frontend. diff --git a/CodeGen/docker/gaudi/docker_compose.yaml b/CodeGen/docker/gaudi/docker_compose.yaml index fe194822e..965bbea97 100644 --- a/CodeGen/docker/gaudi/docker_compose.yaml +++ b/CodeGen/docker/gaudi/docker_compose.yaml @@ -72,6 +72,19 @@ services: ipc: host restart: always + codegen-gaudi-react-ui-server: + image: opea/codegen-react-ui:latest + container_name: codegen-gaudi-react-ui-server + depends_on: + - codegen-gaudi-backend-server + ports: + - "5174:80" + build: + args: + - BACKEND_SERVICE_ENDPOINT=${BACKEND_SERVICE_ENDPOINT} + ipc: host + restart: always + networks: default: driver: bridge diff --git a/CodeGen/docker/ui/docker/Dockerfile.react b/CodeGen/docker/ui/docker/Dockerfile.react new file mode 100644 index 000000000..04bb59fa1 --- /dev/null +++ b/CodeGen/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_CODE_GEN_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/CodeGen/docker/ui/react/.env b/CodeGen/docker/ui/react/.env new file mode 100644 index 000000000..c5a7e3cad --- /dev/null +++ b/CodeGen/docker/ui/react/.env @@ -0,0 +1 @@ +VITE_CODE_GEN_URL=http://ip_address:7778/v1/codegen \ No newline at end of file diff --git a/CodeGen/docker/ui/react/.eslintrc.cjs b/CodeGen/docker/ui/react/.eslintrc.cjs new file mode 100644 index 000000000..78174f683 --- /dev/null +++ b/CodeGen/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/CodeGen/docker/ui/react/.gitignore b/CodeGen/docker/ui/react/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/CodeGen/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/CodeGen/docker/ui/react/README.md b/CodeGen/docker/ui/react/README.md new file mode 100644 index 000000000..5a9b07287 --- /dev/null +++ b/CodeGen/docker/ui/react/README.md @@ -0,0 +1,25 @@ +

Code Gen

+ +### 📸 Project Screenshots + +![project-screenshot](../../../assets/img/codegen_ui_react.png) + +

🧐 Features

+ +Here're some of the project's features: + +- Generate code: generate the corresponding code based on the current user's input. + +

🛠️ Get it Running:

+ +1. Clone the repo. + +2. cd command to the current folder. + +3. Modify the required .env variables. + ``` + VITE_CODE_GEN_URL = '' + ``` +4. Execute `npm install` to install the corresponding dependencies. + +5. Execute `npm run dev` in both environments diff --git a/CodeGen/docker/ui/react/index.html b/CodeGen/docker/ui/react/index.html new file mode 100644 index 000000000..fbe87e0fd --- /dev/null +++ b/CodeGen/docker/ui/react/index.html @@ -0,0 +1,18 @@ + + + + + + + + + Conversations UI + + +
+ + + diff --git a/CodeGen/docker/ui/react/nginx.conf b/CodeGen/docker/ui/react/nginx.conf new file mode 100644 index 000000000..00433fcda --- /dev/null +++ b/CodeGen/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/CodeGen/docker/ui/react/package.json b/CodeGen/docker/ui/react/package.json new file mode 100644 index 000000000..4b2097ece --- /dev/null +++ b/CodeGen/docker/ui/react/package.json @@ -0,0 +1,51 @@ +{ + "name": "ui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview", + "test": "vitest" + }, + "dependencies": { + "@mantine/core": "^7.10.0", + "@mantine/hooks": "^7.10.0", + "@mantine/notifications": "^7.10.2", + "@microsoft/fetch-event-source": "^2.0.1", + "@reduxjs/toolkit": "^2.2.5", + "@tabler/icons-react": "^3.5.0", + "axios": "^1.7.2", + "luxon": "^3.4.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-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/CodeGen/docker/ui/react/postcss.config.cjs b/CodeGen/docker/ui/react/postcss.config.cjs new file mode 100644 index 000000000..e817f567b --- /dev/null +++ b/CodeGen/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/CodeGen/docker/ui/react/src/App.scss b/CodeGen/docker/ui/react/src/App.scss new file mode 100644 index 000000000..187764a17 --- /dev/null +++ b/CodeGen/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/CodeGen/docker/ui/react/src/App.tsx b/CodeGen/docker/ui/react/src/App.tsx new file mode 100644 index 000000000..90f3cfb8a --- /dev/null +++ b/CodeGen/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 { IconMessages } from "@tabler/icons-react" +import { Notifications } from '@mantine/notifications'; +import CodeGen from "./components/CodeGen/CodeGen"; + +const title = "Code Gen" +const navList: SidebarNavList = [ + { icon: IconMessages, label: title } +] + +function App() { + + return ( + + +
+ +
+ +
+
+
+ ) +} + +export default App diff --git a/CodeGen/docker/ui/react/src/__tests__/util.test.ts b/CodeGen/docker/ui/react/src/__tests__/util.test.ts new file mode 100644 index 000000000..e67ba2c86 --- /dev/null +++ b/CodeGen/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/CodeGen/docker/ui/react/src/assets/opea-icon-black.svg b/CodeGen/docker/ui/react/src/assets/opea-icon-black.svg new file mode 100644 index 000000000..5c96dc762 --- /dev/null +++ b/CodeGen/docker/ui/react/src/assets/opea-icon-black.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CodeGen/docker/ui/react/src/assets/opea-icon-color.svg b/CodeGen/docker/ui/react/src/assets/opea-icon-color.svg new file mode 100644 index 000000000..790151171 --- /dev/null +++ b/CodeGen/docker/ui/react/src/assets/opea-icon-color.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CodeGen/docker/ui/react/src/common/client.ts b/CodeGen/docker/ui/react/src/common/client.ts new file mode 100644 index 000000000..7512f73e3 --- /dev/null +++ b/CodeGen/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/CodeGen/docker/ui/react/src/common/util.ts b/CodeGen/docker/ui/react/src/common/util.ts new file mode 100644 index 000000000..df65b2d8e --- /dev/null +++ b/CodeGen/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/CodeGen/docker/ui/react/src/components/CodeGen/CodeGen.tsx b/CodeGen/docker/ui/react/src/components/CodeGen/CodeGen.tsx new file mode 100644 index 000000000..8d35d89ab --- /dev/null +++ b/CodeGen/docker/ui/react/src/components/CodeGen/CodeGen.tsx @@ -0,0 +1,135 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { KeyboardEventHandler, SyntheticEvent, useEffect, useRef, useState } from 'react' +import styleClasses from "./codeGen.module.scss" +import { ActionIcon, Textarea, Title, rem } from '@mantine/core' +import { IconArrowRight } from '@tabler/icons-react' +import { ConversationMessage } from '../Message/conversationMessage' +import { fetchEventSource } from '@microsoft/fetch-event-source' +import { CODE_GEN_URL } from '../../config' + + + +const CodeGen = () => { + const [prompt, setPrompt] = useState("") + const [submittedPrompt, setSubmittedPrompt] = useState("") + const [response,setResponse] = useState(""); + const promptInputRef = useRef(null) + const scrollViewport = useRef(null) + + const toSend = "Enter" + + const handleSubmit = async () => { + setResponse("") + setSubmittedPrompt(prompt) + const body = { + messages:prompt + } + fetchEventSource(CODE_GEN_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 match = msg.data.match(/b'([^']*)'/); + if (match && match[1] != "") { + const extractedText = match[1].replace(/\\n/g, "\n"); + setResponse(prev=>prev+extractedText); + } + } catch (e) { + console.log("something wrong in msg", e); + throw e; + } + } + }, + onerror(err) { + console.log("error", err); + setResponse("") + throw err; + }, + onclose() { + setPrompt("") + }, + }); + + } + + const scrollToBottom = () => { + scrollViewport.current!.scrollTo({ top: scrollViewport.current!.scrollHeight }) + } + + useEffect(() => { + scrollToBottom() + }, [response]) + + const handleKeyDown: KeyboardEventHandler = (event) => { + if (!event.shiftKey && event.key === toSend) { + handleSubmit() + setTimeout(() => { + setPrompt("") + }, 1) + } + } + + const handleChange = (event: SyntheticEvent) => { + event.preventDefault() + setPrompt((event.target as HTMLTextAreaElement).value) + } + return ( +
+
+
+
+ CodeGen +
+ +
+ {submittedPrompt && ( + + )} + {response && ( + + )} +
+ +
+