From 2f36e5723653d6c582baa1919ec507e6efd15809 Mon Sep 17 00:00:00 2001 From: zehao-intel Date: Mon, 20 May 2024 22:06:24 +0800 Subject: [PATCH] Refactor Document Summarization Example with Microservice (#151) Signed-off-by: zehao-intel Co-authored-by: lvliang-intel Co-authored-by: chensuyue --- .github/workflows/DocSum_gaudi.yml | 57 ++++ .../workflows/{DocSum.yml => DocSum_xeon.yml} | 29 +- .pre-commit-config.yaml | 3 +- DocSum/README.md | 124 +------ DocSum/depracated/README.md | 134 ++++++++ .../langchain/docker/Dockerfile | 0 .../langchain/docker/build_docker.sh | 0 .../langchain/docker/requirements.txt | 0 .../langchain/docker/summarize-app/.gitignore | 0 .../langchain/docker/summarize-app/Dockerfile | 0 .../langchain/docker/summarize-app/README.md | 0 .../docker/summarize-app/app/__init__.py | 0 .../docker/summarize-app/app/server.py | 0 .../docker/summarize-app/app/utils.py | 0 .../docker/summarize-app/packages/README.md | 0 .../docker/summarize-app/pyproject.toml | 0 .../serving/tgi_gaudi/build_docker.sh | 0 .../serving/tgi_gaudi/launch_tgi_service.sh | 0 .../tests/test_langchain_inference.sh | 0 DocSum/depracated/ui/svelte/.env | 1 + DocSum/{ => depracated}/ui/svelte/.gitignore | 0 DocSum/{ => depracated}/ui/svelte/.npmrc | 0 DocSum/depracated/ui/svelte/README.md | 30 ++ DocSum/depracated/ui/svelte/package.json | 53 +++ .../depracated/ui/svelte/postcss.config.cjs | 27 ++ DocSum/depracated/ui/svelte/src/app.d.ts | 27 ++ DocSum/depracated/ui/svelte/src/app.html | 28 ++ .../{ => depracated}/ui/svelte/src/app.pcss | 0 .../ui/svelte/src/lib/assets/imgLogo.svelte | 0 .../ui/svelte/src/lib/assets/intelLogo.svelte | 0 .../src/lib/assets/loadingAnimation.svelte | 0 .../svelte/src/lib/assets/spinLoading.svelte | 0 .../svelte/src/lib/assets/summaryLogo.svelte | 0 .../ui/svelte/src/lib/doc.svelte | 0 .../ui/svelte/src/lib/dropFile.svelte | 0 .../ui/svelte/src/lib/header.svelte | 0 .../ui/svelte/src/lib/index.ts | 0 .../ui/svelte/src/lib/shared/Network.ts | 0 .../ui/svelte/src/lib/shared/Store.ts | 0 .../ui/svelte/src/lib/shared/Utils.ts | 26 ++ .../ui/svelte/src/lib/summary.svelte | 0 .../ui/svelte/src/routes/+layout.svelte | 25 ++ .../ui/svelte/src/routes/+page.svelte | 94 ++++++ .../depracated/ui/svelte/static/favicon.png | Bin 0 -> 70954 bytes DocSum/depracated/ui/svelte/svelte.config.js | 35 ++ .../depracated/ui/svelte/tailwind.config.cjs | 43 +++ DocSum/depracated/ui/svelte/tsconfig.json | 15 + DocSum/depracated/ui/svelte/vite.config.ts | 20 ++ DocSum/microservice/gaudi/README.md | 111 +++++++ DocSum/microservice/gaudi/docker/Dockerfile | 41 +++ DocSum/microservice/gaudi/docker_compose.yaml | 75 +++++ DocSum/microservice/gaudi/docsum.py | 53 +++ DocSum/microservice/xeon/README.md | 114 +++++++ DocSum/microservice/xeon/docker/Dockerfile | 41 +++ DocSum/microservice/xeon/docker_compose.yaml | 76 +++++ DocSum/microservice/xeon/docsum.py | 53 +++ DocSum/tests/test_docsum_on_gaudi.sh | 124 +++++++ DocSum/tests/test_docsum_on_xeon.sh | 123 +++++++ DocSum/ui/docker/Dockerfile | 20 ++ DocSum/ui/svelte/.editorconfig | 10 + DocSum/ui/svelte/.env | 7 +- DocSum/ui/svelte/.eslintignore | 13 + DocSum/ui/svelte/.eslintrc.cjs | 34 ++ DocSum/ui/svelte/.prettierignore | 13 + DocSum/ui/svelte/.prettierrc | 13 + DocSum/ui/svelte/README.md | 20 +- DocSum/ui/svelte/package.json | 85 ++--- DocSum/ui/svelte/playwright.config.ts | 83 +++++ DocSum/ui/svelte/postcss.config.cjs | 12 +- DocSum/ui/svelte/src/app.d.ts | 16 +- DocSum/ui/svelte/src/app.html | 18 +- DocSum/ui/svelte/src/app.postcss | 86 +++++ .../lib/assets/avatar/svelte/Delete.svelte | 30 ++ .../lib/assets/chat/svelte/Assistant.svelte | 44 +++ .../assets/chat/svelte/PaperAirplane.svelte | 68 ++++ .../assets/chat/svelte/PersonOutlined.svelte | 26 ++ .../src/lib/assets/layout/css/driver.css | 94 ++++++ .../svelte/src/lib/assets/upload/next.svelte | 31 ++ .../src/lib/assets/upload/previous.svelte | 31 ++ .../svelte/src/lib/assets/voice/svg/paste.svg | 1 + .../src/lib/assets/voice/svg/uploadFile.svg | 1 + .../src/lib/modules/chat/ChatMessage.svelte | 68 ++++ .../src/lib/modules/chat/MessageAvatar.svelte | 30 ++ .../src/lib/modules/chat/MessageTimer.svelte | 67 ++++ .../src/lib/modules/frame/Layout.svelte | 48 +++ .../ui/svelte/src/lib/network/chat/Network.ts | 33 ++ .../svelte/src/lib/network/upload/Network.ts | 58 ++++ DocSum/ui/svelte/src/lib/shared/Utils.ts | 48 ++- .../lib/shared/components/chat/gallery.svelte | 156 +++++++++ .../shared/components/loading/Loading.svelte | 48 +++ .../components/scrollbar/Scrollbar.svelte | 48 +++ .../components/upload/PasteKnowledge.svelte | 52 +++ .../components/upload/upload-knowledge.svelte | 48 +++ .../components/upload/uploadFile.svelte | 167 ++++++++++ .../src/lib/shared/constant/Interface.ts | 47 +++ .../src/lib/shared/stores/common/Store.ts | 39 +++ DocSum/ui/svelte/src/routes/+layout.svelte | 29 +- DocSum/ui/svelte/src/routes/+page.svelte | 306 ++++++++++++++---- DocSum/ui/svelte/src/routes/+page.ts | 26 ++ DocSum/ui/svelte/svelte.config.js | 29 +- DocSum/ui/svelte/tailwind.config.cjs | 44 +-- DocSum/ui/svelte/tests/chatQnA.spec.ts | 72 +++++ DocSum/ui/svelte/tests/test_file.txt | 104 ++++++ DocSum/ui/svelte/tsconfig.json | 28 +- DocSum/ui/svelte/vite.config.ts | 11 +- 105 files changed, 3608 insertions(+), 336 deletions(-) create mode 100644 .github/workflows/DocSum_gaudi.yml rename .github/workflows/{DocSum.yml => DocSum_xeon.yml} (59%) create mode 100644 DocSum/depracated/README.md rename DocSum/{ => depracated}/langchain/docker/Dockerfile (100%) rename DocSum/{ => depracated}/langchain/docker/build_docker.sh (100%) mode change 100755 => 100644 rename DocSum/{ => depracated}/langchain/docker/requirements.txt (100%) rename DocSum/{ => depracated}/langchain/docker/summarize-app/.gitignore (100%) rename DocSum/{ => depracated}/langchain/docker/summarize-app/Dockerfile (100%) rename DocSum/{ => depracated}/langchain/docker/summarize-app/README.md (100%) rename DocSum/{ => depracated}/langchain/docker/summarize-app/app/__init__.py (100%) rename DocSum/{ => depracated}/langchain/docker/summarize-app/app/server.py (100%) rename DocSum/{ => depracated}/langchain/docker/summarize-app/app/utils.py (100%) rename DocSum/{ => depracated}/langchain/docker/summarize-app/packages/README.md (100%) rename DocSum/{ => depracated}/langchain/docker/summarize-app/pyproject.toml (100%) rename DocSum/{ => depracated}/serving/tgi_gaudi/build_docker.sh (100%) rename DocSum/{ => depracated}/serving/tgi_gaudi/launch_tgi_service.sh (100%) rename DocSum/{ => depracated}/tests/test_langchain_inference.sh (100%) create mode 100644 DocSum/depracated/ui/svelte/.env rename DocSum/{ => depracated}/ui/svelte/.gitignore (100%) rename DocSum/{ => depracated}/ui/svelte/.npmrc (100%) create mode 100644 DocSum/depracated/ui/svelte/README.md create mode 100644 DocSum/depracated/ui/svelte/package.json create mode 100644 DocSum/depracated/ui/svelte/postcss.config.cjs create mode 100644 DocSum/depracated/ui/svelte/src/app.d.ts create mode 100644 DocSum/depracated/ui/svelte/src/app.html rename DocSum/{ => depracated}/ui/svelte/src/app.pcss (100%) rename DocSum/{ => depracated}/ui/svelte/src/lib/assets/imgLogo.svelte (100%) rename DocSum/{ => depracated}/ui/svelte/src/lib/assets/intelLogo.svelte (100%) rename DocSum/{ => depracated}/ui/svelte/src/lib/assets/loadingAnimation.svelte (100%) rename DocSum/{ => depracated}/ui/svelte/src/lib/assets/spinLoading.svelte (100%) rename DocSum/{ => depracated}/ui/svelte/src/lib/assets/summaryLogo.svelte (100%) rename DocSum/{ => depracated}/ui/svelte/src/lib/doc.svelte (100%) rename DocSum/{ => depracated}/ui/svelte/src/lib/dropFile.svelte (100%) rename DocSum/{ => depracated}/ui/svelte/src/lib/header.svelte (100%) rename DocSum/{ => depracated}/ui/svelte/src/lib/index.ts (100%) rename DocSum/{ => depracated}/ui/svelte/src/lib/shared/Network.ts (100%) rename DocSum/{ => depracated}/ui/svelte/src/lib/shared/Store.ts (100%) create mode 100644 DocSum/depracated/ui/svelte/src/lib/shared/Utils.ts rename DocSum/{ => depracated}/ui/svelte/src/lib/summary.svelte (100%) create mode 100644 DocSum/depracated/ui/svelte/src/routes/+layout.svelte create mode 100644 DocSum/depracated/ui/svelte/src/routes/+page.svelte create mode 100644 DocSum/depracated/ui/svelte/static/favicon.png create mode 100644 DocSum/depracated/ui/svelte/svelte.config.js create mode 100644 DocSum/depracated/ui/svelte/tailwind.config.cjs create mode 100644 DocSum/depracated/ui/svelte/tsconfig.json create mode 100644 DocSum/depracated/ui/svelte/vite.config.ts create mode 100644 DocSum/microservice/gaudi/README.md create mode 100644 DocSum/microservice/gaudi/docker/Dockerfile create mode 100644 DocSum/microservice/gaudi/docker_compose.yaml create mode 100644 DocSum/microservice/gaudi/docsum.py create mode 100644 DocSum/microservice/xeon/README.md create mode 100644 DocSum/microservice/xeon/docker/Dockerfile create mode 100644 DocSum/microservice/xeon/docker_compose.yaml create mode 100644 DocSum/microservice/xeon/docsum.py create mode 100644 DocSum/tests/test_docsum_on_gaudi.sh create mode 100644 DocSum/tests/test_docsum_on_xeon.sh create mode 100644 DocSum/ui/docker/Dockerfile create mode 100644 DocSum/ui/svelte/.editorconfig create mode 100644 DocSum/ui/svelte/.eslintignore create mode 100644 DocSum/ui/svelte/.eslintrc.cjs create mode 100644 DocSum/ui/svelte/.prettierignore create mode 100644 DocSum/ui/svelte/.prettierrc create mode 100644 DocSum/ui/svelte/playwright.config.ts create mode 100644 DocSum/ui/svelte/src/app.postcss create mode 100644 DocSum/ui/svelte/src/lib/assets/avatar/svelte/Delete.svelte create mode 100644 DocSum/ui/svelte/src/lib/assets/chat/svelte/Assistant.svelte create mode 100644 DocSum/ui/svelte/src/lib/assets/chat/svelte/PaperAirplane.svelte create mode 100644 DocSum/ui/svelte/src/lib/assets/chat/svelte/PersonOutlined.svelte create mode 100644 DocSum/ui/svelte/src/lib/assets/layout/css/driver.css create mode 100644 DocSum/ui/svelte/src/lib/assets/upload/next.svelte create mode 100644 DocSum/ui/svelte/src/lib/assets/upload/previous.svelte create mode 100644 DocSum/ui/svelte/src/lib/assets/voice/svg/paste.svg create mode 100644 DocSum/ui/svelte/src/lib/assets/voice/svg/uploadFile.svg create mode 100644 DocSum/ui/svelte/src/lib/modules/chat/ChatMessage.svelte create mode 100644 DocSum/ui/svelte/src/lib/modules/chat/MessageAvatar.svelte create mode 100644 DocSum/ui/svelte/src/lib/modules/chat/MessageTimer.svelte create mode 100644 DocSum/ui/svelte/src/lib/modules/frame/Layout.svelte create mode 100644 DocSum/ui/svelte/src/lib/network/chat/Network.ts create mode 100644 DocSum/ui/svelte/src/lib/network/upload/Network.ts create mode 100644 DocSum/ui/svelte/src/lib/shared/components/chat/gallery.svelte create mode 100644 DocSum/ui/svelte/src/lib/shared/components/loading/Loading.svelte create mode 100644 DocSum/ui/svelte/src/lib/shared/components/scrollbar/Scrollbar.svelte create mode 100644 DocSum/ui/svelte/src/lib/shared/components/upload/PasteKnowledge.svelte create mode 100644 DocSum/ui/svelte/src/lib/shared/components/upload/upload-knowledge.svelte create mode 100644 DocSum/ui/svelte/src/lib/shared/components/upload/uploadFile.svelte create mode 100644 DocSum/ui/svelte/src/lib/shared/constant/Interface.ts create mode 100644 DocSum/ui/svelte/src/lib/shared/stores/common/Store.ts create mode 100644 DocSum/ui/svelte/src/routes/+page.ts create mode 100644 DocSum/ui/svelte/tests/chatQnA.spec.ts create mode 100644 DocSum/ui/svelte/tests/test_file.txt diff --git a/.github/workflows/DocSum_gaudi.yml b/.github/workflows/DocSum_gaudi.yml new file mode 100644 index 000000000..2cfb43437 --- /dev/null +++ b/.github/workflows/DocSum_gaudi.yml @@ -0,0 +1,57 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +name: DocSum E2E test on Gaudi + +on: + pull_request: + branches: [main] + types: [opened, reopened, ready_for_review, synchronize] # added `ready_for_review` since draft is skipped + paths: + - DocSum/microservice/gaudi/** + - DocSum/tests/test_docsum_on_gaudi.sh + - "!**.md" + - "!**/ui/**" + - .github/workflows/DocSum_gaudi.yml + workflow_dispatch: + +# If there is a new commit, the previous jobs will be canceled +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + DocSum-Gaudi: + runs-on: aise-cluster + steps: + - name: Clean Up Working Directory + run: sudo rm -rf ${{github.workspace}}/* + + - name: Checkout out Repo + uses: actions/checkout@v4 + with: + ref: "refs/pull/${{ github.event.number }}/merge" + + - name: Run Test + env: + HUGGINGFACEHUB_API_TOKEN: ${{ secrets.HUGGINGFACEHUB_API_TOKEN }} + AISE_CLUSTER_01_2_IP: ${{ secrets.AISE_CLUSTER_01_2_IP }} + run: | + cd ${{ github.workspace }}/DocSum/tests + timeout 20m bash test_docsum_on_gaudi.sh + + - name: Clean Up Container + if: cancelled() || failure() + run: | + cd ${{ github.workspace }}/DocSum/microservice/gaudi + container_list=$(cat docker_compose.yaml | grep container_name | cut -d':' -f2) + for container_name in $container_list; do + cid=$(docker ps -aq --filter "name=$container_name") + if [[ ! -z "$cid" ]]; then docker stop $cid && docker rm $cid && sleep 1s; fi + done + + - name: Publish pipeline artifact + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + path: ${{ github.workspace }}/DocSum/tests/*.log diff --git a/.github/workflows/DocSum.yml b/.github/workflows/DocSum_xeon.yml similarity index 59% rename from .github/workflows/DocSum.yml rename to .github/workflows/DocSum_xeon.yml index 95aa777fa..c3788457b 100644 --- a/.github/workflows/DocSum.yml +++ b/.github/workflows/DocSum_xeon.yml @@ -1,17 +1,18 @@ # Copyright (C) 2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -name: DocSum-test +name: DocSum E2E test on Xeon on: - pull_request_target: + pull_request: branches: [main] types: [opened, reopened, ready_for_review, synchronize] # added `ready_for_review` since draft is skipped paths: - - DocSum/** + - DocSum/microservice/xeon/** + - DocSum/tests/test_docsum_on_xeon.sh - "!**.md" - "!**/ui/**" - - .github/workflows/DocSum.yml + - .github/workflows/DocSum_xeon.yml workflow_dispatch: # If there is a new commit, the previous jobs will be canceled @@ -20,12 +21,8 @@ concurrency: cancel-in-progress: true jobs: - DocSum: + DocSum-Xeon: runs-on: aise-cluster - strategy: - matrix: - job_name: ["langchain"] - fail-fast: false steps: - name: Clean Up Working Directory run: sudo rm -rf ${{github.workspace}}/* @@ -38,13 +35,23 @@ jobs: - name: Run Test env: HUGGINGFACEHUB_API_TOKEN: ${{ secrets.HUGGINGFACEHUB_API_TOKEN }} + AISE_CLUSTER_01_2_IP: ${{ secrets.AISE_CLUSTER_01_2_IP }} run: | cd ${{ github.workspace }}/DocSum/tests - bash test_${{ matrix.job_name }}_inference.sh + timeout 20m bash test_docsum_on_xeon.sh + + - name: Clean Up Container + if: cancelled() || failure() + run: | + cd ${{ github.workspace }}/DocSum/microservice/xeon + container_list=$(cat docker_compose.yaml | grep container_name | cut -d':' -f2) + for container_name in $container_list; do + cid=$(docker ps -aq --filter "name=$container_name") + if [[ ! -z "$cid" ]]; then docker stop $cid && docker rm $cid && sleep 1s; fi + done - name: Publish pipeline artifact if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: - name: ${{ matrix.job_name }} path: ${{ github.workspace }}/DocSum/tests/*.log diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2a780884e..3c8970b84 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,8 @@ repos: exclude: | (?x)^( ChatQnA/ui/svelte/tsconfig.json| - SearchQnA/ui/svelte/tsconfig.json + SearchQnA/ui/svelte/tsconfig.json| + DocSum/ui/svelte/tsconfig.json )$ - id: check-yaml - id: debug-statements diff --git a/DocSum/README.md b/DocSum/README.md index fff815a94..189d87be7 100644 --- a/DocSum/README.md +++ b/DocSum/README.md @@ -1,4 +1,4 @@ -# Document Summarization +# Document Summarization Application In a world where data, information, and legal complexities is prevalent, the volume of legal documents is growing rapidly. Law firms, legal professionals, and businesses are dealing with an ever-increasing number of legal texts, including contracts, court rulings, statutes, and regulations. These documents contain important insights, but understanding them can be overwhelming. This is where the demand for legal document summarization comes in. @@ -11,124 +11,14 @@ The document summarization architecture shows below: ![Workflow](https://i.imgur.com/m9Ac9wy.png) -# Environment Setup +# Deploy Document Summarization Service -To use [šŸ¤— text-generation-inference](https://github.com/huggingface/text-generation-inference) on Habana Gaudi/Gaudi2, please follow these steps: +The Document Summarization service can be effortlessly deployed on either Intel Gaudi2 or Intel XEON Scalable Processors. -## Build TGI Gaudi Docker Image +## Deploy Document Summarization on Gaudi -```bash -bash ./serving/tgi_gaudi/build_docker.sh -``` +Refer to the [Gaudi Guide](./microservice/gaudi/README.md) for instructions on deploying Document Summarization on Gaudi. -## Launch TGI Gaudi Service +## Deploy Document Summarization on Xeon -### Launch a local server instance on 1 Gaudi card: - -```bash -bash ./serving/tgi_gaudi/launch_tgi_service.sh -``` - -For gated models such as `LLAMA-2`, you will have to pass -e HUGGING_FACE_HUB_TOKEN=\ to the docker run command above with a valid Hugging Face Hub read token. - -Please follow this link [huggingface token](https://huggingface.co/docs/hub/security-tokens) to get the access token and export `HUGGINGFACEHUB_API_TOKEN` environment with the token. - -```bash -export HUGGINGFACEHUB_API_TOKEN= -``` - -### Launch a local server instance on 8 Gaudi cards: - -```bash -bash ./serving/tgi_gaudi/launch_tgi_service.sh 8 -``` - -### Customize TGI Gaudi Service - -The ./serving/tgi_gaudi/launch_tgi_service.sh script accepts three parameters: - -- num_cards: The number of Gaudi cards to be utilized, ranging from 1 to 8. The default is set to 1. -- port_number: The port number assigned to the TGI Gaudi endpoint, with the default being 8080. -- model_name: The model name utilized for LLM, with the default set to "Intel/neural-chat-7b-v3-3". - -You have the flexibility to customize these parameters according to your specific needs. Additionally, you can set the TGI Gaudi endpoint by exporting the environment variable `TGI_ENDPOINT`: - -```bash -export TGI_ENDPOINT="http://xxx.xxx.xxx.xxx:8080" -``` - -## Launch Document Summary Docker - -### Build Document Summary Docker Image (Optional) - -```bash -cd langchain/docker/ -bash ./build_docker.sh -cd ../../ -``` - -### Launch Document Summary Docker - -```bash -docker run -it --net=host --ipc=host -e http_proxy=${http_proxy} -e https_proxy=${https_proxy} -v /var/run/docker.sock:/var/run/docker.sock intel/gen-ai-examples:document-summarize bash -``` - -# Start Document Summary Server - -## Start the Backend Service - -Make sure TGI-Gaudi service is running. Launch the backend service: - -```bash -export HUGGINGFACEHUB_API_TOKEN= -nohup python app/server.py & -``` - -Then you can make requests like below to check the DocSum backend service status: - -```bash -curl http://127.0.0.1:8000/v1/text_summarize \ - -X POST \ - -H 'Content-Type: application/json' \ - -d '{"text":"Text Embeddings Inference (TEI) is a toolkit for deploying and serving open source text embeddings and sequence classification models. TEI enables high-performance extraction for the most popular models, including FlagEmbedding, Ember, GTE and E5."}' -``` - -## Start the Frontend Service - -Navigate to the "ui" folder and execute the following commands to start the frontend GUI: - -```bash -cd ui -sudo apt-get install npm && \ - npm install -g n && \ - n stable && \ - hash -r && \ - npm install -g npm@latest -``` - -For CentOS, please use the following commands instead: - -```bash -curl -sL https://rpm.nodesource.com/setup_20.x | sudo bash - -sudo yum install -y nodejs -``` - -Update the `BASIC_URL` environment variable in the `.env` file by replacing the IP address '127.0.0.1' with the actual IP address. - -Run the following command to install the required dependencies: - -```bash -npm install -``` - -Start the development server by executing the following command: - -```bash -nohup npm run dev & -``` - -This will initiate the frontend service and launch the application. - -# - -SCRIPT USAGE NOTICE:Ā  By downloading and using any script file included with the associated software package (such as files with .bat, .cmd, or .JS extensions, Docker files, or any other type of file that, when executed, automatically downloads and/or installs files onto your system) (the ā€œScript Fileā€), it is your obligation to review the Script File to understand what files (e.g.,Ā  other software, AI models, AI Datasets) the Script File will download to your system (ā€œDownloaded Filesā€). Furthermore, by downloading and using the Downloaded Files, even if they are installed through a silent install, you agree to any and all terms and conditions associated with such files, including but not limited to, license terms, notices, or disclaimers. +Refer to the [Xeon Guide](./microservice/xeon/README.md) for instructions on deploying Document Summarization on Xeon. diff --git a/DocSum/depracated/README.md b/DocSum/depracated/README.md new file mode 100644 index 000000000..fff815a94 --- /dev/null +++ b/DocSum/depracated/README.md @@ -0,0 +1,134 @@ +# Document Summarization + +In a world where data, information, and legal complexities is prevalent, the volume of legal documents is growing rapidly. Law firms, legal professionals, and businesses are dealing with an ever-increasing number of legal texts, including contracts, court rulings, statutes, and regulations. +These documents contain important insights, but understanding them can be overwhelming. This is where the demand for legal document summarization comes in. + +Large Language Models (LLMs) have revolutionized the way we interact with text, LLMs can be used to create summaries of news articles, research papers, technical documents, and other types of text. Suppose you have a set of documents (PDFs, Notion pages, customer questions, etc.) and you want to summarize the content. In this example use case, we use LangChain to apply some summarization strategies and run LLM inference using Text Generation Inference on Intel Xeon and Gaudi2. + +The document summarization architecture shows below: + +![Architecture](https://i.imgur.com/XT0YUhu.png) + +![Workflow](https://i.imgur.com/m9Ac9wy.png) + +# Environment Setup + +To use [šŸ¤— text-generation-inference](https://github.com/huggingface/text-generation-inference) on Habana Gaudi/Gaudi2, please follow these steps: + +## Build TGI Gaudi Docker Image + +```bash +bash ./serving/tgi_gaudi/build_docker.sh +``` + +## Launch TGI Gaudi Service + +### Launch a local server instance on 1 Gaudi card: + +```bash +bash ./serving/tgi_gaudi/launch_tgi_service.sh +``` + +For gated models such as `LLAMA-2`, you will have to pass -e HUGGING_FACE_HUB_TOKEN=\ to the docker run command above with a valid Hugging Face Hub read token. + +Please follow this link [huggingface token](https://huggingface.co/docs/hub/security-tokens) to get the access token and export `HUGGINGFACEHUB_API_TOKEN` environment with the token. + +```bash +export HUGGINGFACEHUB_API_TOKEN= +``` + +### Launch a local server instance on 8 Gaudi cards: + +```bash +bash ./serving/tgi_gaudi/launch_tgi_service.sh 8 +``` + +### Customize TGI Gaudi Service + +The ./serving/tgi_gaudi/launch_tgi_service.sh script accepts three parameters: + +- num_cards: The number of Gaudi cards to be utilized, ranging from 1 to 8. The default is set to 1. +- port_number: The port number assigned to the TGI Gaudi endpoint, with the default being 8080. +- model_name: The model name utilized for LLM, with the default set to "Intel/neural-chat-7b-v3-3". + +You have the flexibility to customize these parameters according to your specific needs. Additionally, you can set the TGI Gaudi endpoint by exporting the environment variable `TGI_ENDPOINT`: + +```bash +export TGI_ENDPOINT="http://xxx.xxx.xxx.xxx:8080" +``` + +## Launch Document Summary Docker + +### Build Document Summary Docker Image (Optional) + +```bash +cd langchain/docker/ +bash ./build_docker.sh +cd ../../ +``` + +### Launch Document Summary Docker + +```bash +docker run -it --net=host --ipc=host -e http_proxy=${http_proxy} -e https_proxy=${https_proxy} -v /var/run/docker.sock:/var/run/docker.sock intel/gen-ai-examples:document-summarize bash +``` + +# Start Document Summary Server + +## Start the Backend Service + +Make sure TGI-Gaudi service is running. Launch the backend service: + +```bash +export HUGGINGFACEHUB_API_TOKEN= +nohup python app/server.py & +``` + +Then you can make requests like below to check the DocSum backend service status: + +```bash +curl http://127.0.0.1:8000/v1/text_summarize \ + -X POST \ + -H 'Content-Type: application/json' \ + -d '{"text":"Text Embeddings Inference (TEI) is a toolkit for deploying and serving open source text embeddings and sequence classification models. TEI enables high-performance extraction for the most popular models, including FlagEmbedding, Ember, GTE and E5."}' +``` + +## Start the Frontend Service + +Navigate to the "ui" folder and execute the following commands to start the frontend GUI: + +```bash +cd ui +sudo apt-get install npm && \ + npm install -g n && \ + n stable && \ + hash -r && \ + npm install -g npm@latest +``` + +For CentOS, please use the following commands instead: + +```bash +curl -sL https://rpm.nodesource.com/setup_20.x | sudo bash - +sudo yum install -y nodejs +``` + +Update the `BASIC_URL` environment variable in the `.env` file by replacing the IP address '127.0.0.1' with the actual IP address. + +Run the following command to install the required dependencies: + +```bash +npm install +``` + +Start the development server by executing the following command: + +```bash +nohup npm run dev & +``` + +This will initiate the frontend service and launch the application. + +# + +SCRIPT USAGE NOTICE:Ā  By downloading and using any script file included with the associated software package (such as files with .bat, .cmd, or .JS extensions, Docker files, or any other type of file that, when executed, automatically downloads and/or installs files onto your system) (the ā€œScript Fileā€), it is your obligation to review the Script File to understand what files (e.g.,Ā  other software, AI models, AI Datasets) the Script File will download to your system (ā€œDownloaded Filesā€). Furthermore, by downloading and using the Downloaded Files, even if they are installed through a silent install, you agree to any and all terms and conditions associated with such files, including but not limited to, license terms, notices, or disclaimers. diff --git a/DocSum/langchain/docker/Dockerfile b/DocSum/depracated/langchain/docker/Dockerfile similarity index 100% rename from DocSum/langchain/docker/Dockerfile rename to DocSum/depracated/langchain/docker/Dockerfile diff --git a/DocSum/langchain/docker/build_docker.sh b/DocSum/depracated/langchain/docker/build_docker.sh old mode 100755 new mode 100644 similarity index 100% rename from DocSum/langchain/docker/build_docker.sh rename to DocSum/depracated/langchain/docker/build_docker.sh diff --git a/DocSum/langchain/docker/requirements.txt b/DocSum/depracated/langchain/docker/requirements.txt similarity index 100% rename from DocSum/langchain/docker/requirements.txt rename to DocSum/depracated/langchain/docker/requirements.txt diff --git a/DocSum/langchain/docker/summarize-app/.gitignore b/DocSum/depracated/langchain/docker/summarize-app/.gitignore similarity index 100% rename from DocSum/langchain/docker/summarize-app/.gitignore rename to DocSum/depracated/langchain/docker/summarize-app/.gitignore diff --git a/DocSum/langchain/docker/summarize-app/Dockerfile b/DocSum/depracated/langchain/docker/summarize-app/Dockerfile similarity index 100% rename from DocSum/langchain/docker/summarize-app/Dockerfile rename to DocSum/depracated/langchain/docker/summarize-app/Dockerfile diff --git a/DocSum/langchain/docker/summarize-app/README.md b/DocSum/depracated/langchain/docker/summarize-app/README.md similarity index 100% rename from DocSum/langchain/docker/summarize-app/README.md rename to DocSum/depracated/langchain/docker/summarize-app/README.md diff --git a/DocSum/langchain/docker/summarize-app/app/__init__.py b/DocSum/depracated/langchain/docker/summarize-app/app/__init__.py similarity index 100% rename from DocSum/langchain/docker/summarize-app/app/__init__.py rename to DocSum/depracated/langchain/docker/summarize-app/app/__init__.py diff --git a/DocSum/langchain/docker/summarize-app/app/server.py b/DocSum/depracated/langchain/docker/summarize-app/app/server.py similarity index 100% rename from DocSum/langchain/docker/summarize-app/app/server.py rename to DocSum/depracated/langchain/docker/summarize-app/app/server.py diff --git a/DocSum/langchain/docker/summarize-app/app/utils.py b/DocSum/depracated/langchain/docker/summarize-app/app/utils.py similarity index 100% rename from DocSum/langchain/docker/summarize-app/app/utils.py rename to DocSum/depracated/langchain/docker/summarize-app/app/utils.py diff --git a/DocSum/langchain/docker/summarize-app/packages/README.md b/DocSum/depracated/langchain/docker/summarize-app/packages/README.md similarity index 100% rename from DocSum/langchain/docker/summarize-app/packages/README.md rename to DocSum/depracated/langchain/docker/summarize-app/packages/README.md diff --git a/DocSum/langchain/docker/summarize-app/pyproject.toml b/DocSum/depracated/langchain/docker/summarize-app/pyproject.toml similarity index 100% rename from DocSum/langchain/docker/summarize-app/pyproject.toml rename to DocSum/depracated/langchain/docker/summarize-app/pyproject.toml diff --git a/DocSum/serving/tgi_gaudi/build_docker.sh b/DocSum/depracated/serving/tgi_gaudi/build_docker.sh similarity index 100% rename from DocSum/serving/tgi_gaudi/build_docker.sh rename to DocSum/depracated/serving/tgi_gaudi/build_docker.sh diff --git a/DocSum/serving/tgi_gaudi/launch_tgi_service.sh b/DocSum/depracated/serving/tgi_gaudi/launch_tgi_service.sh similarity index 100% rename from DocSum/serving/tgi_gaudi/launch_tgi_service.sh rename to DocSum/depracated/serving/tgi_gaudi/launch_tgi_service.sh diff --git a/DocSum/tests/test_langchain_inference.sh b/DocSum/depracated/tests/test_langchain_inference.sh similarity index 100% rename from DocSum/tests/test_langchain_inference.sh rename to DocSum/depracated/tests/test_langchain_inference.sh diff --git a/DocSum/depracated/ui/svelte/.env b/DocSum/depracated/ui/svelte/.env new file mode 100644 index 000000000..4fe9cb96a --- /dev/null +++ b/DocSum/depracated/ui/svelte/.env @@ -0,0 +1 @@ +BASIC_URL = 'http://x.x.x.x:yyyy' \ No newline at end of file diff --git a/DocSum/ui/svelte/.gitignore b/DocSum/depracated/ui/svelte/.gitignore similarity index 100% rename from DocSum/ui/svelte/.gitignore rename to DocSum/depracated/ui/svelte/.gitignore diff --git a/DocSum/ui/svelte/.npmrc b/DocSum/depracated/ui/svelte/.npmrc similarity index 100% rename from DocSum/ui/svelte/.npmrc rename to DocSum/depracated/ui/svelte/.npmrc diff --git a/DocSum/depracated/ui/svelte/README.md b/DocSum/depracated/ui/svelte/README.md new file mode 100644 index 000000000..1a48e301f --- /dev/null +++ b/DocSum/depracated/ui/svelte/README.md @@ -0,0 +1,30 @@ +

Doc Summary

+ +### šŸ“ø Project Screenshots + +![project-screenshot](https://imgur.com/oRuDrGX.png) +![project-screenshot](https://imgur.com/j6vo4gl.png) +![project-screenshot](https://imgur.com/LPBvBmM.png) +![project-screenshot](https://imgur.com/yHryOQS.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. + ``` + BASIC_URL = '' + ``` +4. Execute `npm install` to install the corresponding dependencies. + +5. Execute `npm run dev` in both environments diff --git a/DocSum/depracated/ui/svelte/package.json b/DocSum/depracated/ui/svelte/package.json new file mode 100644 index 000000000..027dad195 --- /dev/null +++ b/DocSum/depracated/ui/svelte/package.json @@ -0,0 +1,53 @@ +{ + "name": "doc-summary", + "version": "0.0.1", + "scripts": { + "dev": "vite dev", + "build": "vite build && npm run package", + "preview": "vite preview", + "package": "svelte-kit sync && svelte-package && publint", + "prepublishOnly": "npm run package", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "svelte": "./dist/index.js" + } + }, + "files": [ + "dist", + "!dist/**/*.test.*", + "!dist/**/*.spec.*" + ], + "peerDependencies": { + "svelte": "^4.0.0" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^3.0.0", + "@sveltejs/kit": "^2.0.0", + "@sveltejs/package": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "autoprefixer": "^10.4.16", + "flowbite": "^2.3.0", + "flowbite-svelte": "^0.38.5", + "flowbite-svelte-icons": "^1.4.0", + "postcss": "^8.4.32", + "postcss-load-config": "^5.0.2", + "publint": "^0.1.9", + "svelte": "^4.2.7", + "svelte-check": "^3.6.0", + "tailwindcss": "^3.3.6", + "tslib": "^2.4.1", + "typescript": "^5.0.0", + "vite": "^5.0.11" + }, + "svelte": "./dist/index.js", + "types": "./dist/index.d.ts", + "type": "module", + "dependencies": { + "sse.js": "^0.6.1", + "svelte-notifications": "^0.9.98" + } +} diff --git a/DocSum/depracated/ui/svelte/postcss.config.cjs b/DocSum/depracated/ui/svelte/postcss.config.cjs new file mode 100644 index 000000000..5f822bcb1 --- /dev/null +++ b/DocSum/depracated/ui/svelte/postcss.config.cjs @@ -0,0 +1,27 @@ +// 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. + +const tailwindcss = require("tailwindcss"); +const autoprefixer = require("autoprefixer"); + +const config = { + plugins: [ + //Some plugins, like tailwindcss/nesting, need to run before Tailwind, + tailwindcss(), + //But others, like autoprefixer, need to run after, + autoprefixer, + ], +}; + +module.exports = config; diff --git a/DocSum/depracated/ui/svelte/src/app.d.ts b/DocSum/depracated/ui/svelte/src/app.d.ts new file mode 100644 index 000000000..1b9de033b --- /dev/null +++ b/DocSum/depracated/ui/svelte/src/app.d.ts @@ -0,0 +1,27 @@ +// 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. + +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/DocSum/depracated/ui/svelte/src/app.html b/DocSum/depracated/ui/svelte/src/app.html new file mode 100644 index 000000000..cdcef542d --- /dev/null +++ b/DocSum/depracated/ui/svelte/src/app.html @@ -0,0 +1,28 @@ + + + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/DocSum/ui/svelte/src/app.pcss b/DocSum/depracated/ui/svelte/src/app.pcss similarity index 100% rename from DocSum/ui/svelte/src/app.pcss rename to DocSum/depracated/ui/svelte/src/app.pcss diff --git a/DocSum/ui/svelte/src/lib/assets/imgLogo.svelte b/DocSum/depracated/ui/svelte/src/lib/assets/imgLogo.svelte similarity index 100% rename from DocSum/ui/svelte/src/lib/assets/imgLogo.svelte rename to DocSum/depracated/ui/svelte/src/lib/assets/imgLogo.svelte diff --git a/DocSum/ui/svelte/src/lib/assets/intelLogo.svelte b/DocSum/depracated/ui/svelte/src/lib/assets/intelLogo.svelte similarity index 100% rename from DocSum/ui/svelte/src/lib/assets/intelLogo.svelte rename to DocSum/depracated/ui/svelte/src/lib/assets/intelLogo.svelte diff --git a/DocSum/ui/svelte/src/lib/assets/loadingAnimation.svelte b/DocSum/depracated/ui/svelte/src/lib/assets/loadingAnimation.svelte similarity index 100% rename from DocSum/ui/svelte/src/lib/assets/loadingAnimation.svelte rename to DocSum/depracated/ui/svelte/src/lib/assets/loadingAnimation.svelte diff --git a/DocSum/ui/svelte/src/lib/assets/spinLoading.svelte b/DocSum/depracated/ui/svelte/src/lib/assets/spinLoading.svelte similarity index 100% rename from DocSum/ui/svelte/src/lib/assets/spinLoading.svelte rename to DocSum/depracated/ui/svelte/src/lib/assets/spinLoading.svelte diff --git a/DocSum/ui/svelte/src/lib/assets/summaryLogo.svelte b/DocSum/depracated/ui/svelte/src/lib/assets/summaryLogo.svelte similarity index 100% rename from DocSum/ui/svelte/src/lib/assets/summaryLogo.svelte rename to DocSum/depracated/ui/svelte/src/lib/assets/summaryLogo.svelte diff --git a/DocSum/ui/svelte/src/lib/doc.svelte b/DocSum/depracated/ui/svelte/src/lib/doc.svelte similarity index 100% rename from DocSum/ui/svelte/src/lib/doc.svelte rename to DocSum/depracated/ui/svelte/src/lib/doc.svelte diff --git a/DocSum/ui/svelte/src/lib/dropFile.svelte b/DocSum/depracated/ui/svelte/src/lib/dropFile.svelte similarity index 100% rename from DocSum/ui/svelte/src/lib/dropFile.svelte rename to DocSum/depracated/ui/svelte/src/lib/dropFile.svelte diff --git a/DocSum/ui/svelte/src/lib/header.svelte b/DocSum/depracated/ui/svelte/src/lib/header.svelte similarity index 100% rename from DocSum/ui/svelte/src/lib/header.svelte rename to DocSum/depracated/ui/svelte/src/lib/header.svelte diff --git a/DocSum/ui/svelte/src/lib/index.ts b/DocSum/depracated/ui/svelte/src/lib/index.ts similarity index 100% rename from DocSum/ui/svelte/src/lib/index.ts rename to DocSum/depracated/ui/svelte/src/lib/index.ts diff --git a/DocSum/ui/svelte/src/lib/shared/Network.ts b/DocSum/depracated/ui/svelte/src/lib/shared/Network.ts similarity index 100% rename from DocSum/ui/svelte/src/lib/shared/Network.ts rename to DocSum/depracated/ui/svelte/src/lib/shared/Network.ts diff --git a/DocSum/ui/svelte/src/lib/shared/Store.ts b/DocSum/depracated/ui/svelte/src/lib/shared/Store.ts similarity index 100% rename from DocSum/ui/svelte/src/lib/shared/Store.ts rename to DocSum/depracated/ui/svelte/src/lib/shared/Store.ts diff --git a/DocSum/depracated/ui/svelte/src/lib/shared/Utils.ts b/DocSum/depracated/ui/svelte/src/lib/shared/Utils.ts new file mode 100644 index 000000000..bae0543f6 --- /dev/null +++ b/DocSum/depracated/ui/svelte/src/lib/shared/Utils.ts @@ -0,0 +1,26 @@ +// 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. + +export function scrollToBottom(scrollToDiv: HTMLElement) { + if (scrollToDiv) { + setTimeout( + () => + scrollToDiv.scroll({ + behavior: "auto", + top: scrollToDiv.scrollHeight, + }), + 100, + ); + } +} diff --git a/DocSum/ui/svelte/src/lib/summary.svelte b/DocSum/depracated/ui/svelte/src/lib/summary.svelte similarity index 100% rename from DocSum/ui/svelte/src/lib/summary.svelte rename to DocSum/depracated/ui/svelte/src/lib/summary.svelte diff --git a/DocSum/depracated/ui/svelte/src/routes/+layout.svelte b/DocSum/depracated/ui/svelte/src/routes/+layout.svelte new file mode 100644 index 000000000..9a09eaadf --- /dev/null +++ b/DocSum/depracated/ui/svelte/src/routes/+layout.svelte @@ -0,0 +1,25 @@ + + + + + + + diff --git a/DocSum/depracated/ui/svelte/src/routes/+page.svelte b/DocSum/depracated/ui/svelte/src/routes/+page.svelte new file mode 100644 index 000000000..0270ecd99 --- /dev/null +++ b/DocSum/depracated/ui/svelte/src/routes/+page.svelte @@ -0,0 +1,94 @@ + + + + +
+
+

Please upload file or paste content for summarization.

+
+
+ +
+
+ +
+
+
diff --git a/DocSum/depracated/ui/svelte/static/favicon.png b/DocSum/depracated/ui/svelte/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..75b997f8156c09d1c72026d98c80d0227210da38 GIT binary patch literal 70954 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 z zOY!;Qes)I@CX5n(t*<9f53bHkta0V zGR#fAGX6sv_&|?XWB<2Xm|}jwwcg&k=*|)O(f{0O9FRmIv-e+g{enQ~HdSA7%}qXW z{Tn?$ZxjPkQ_}zK8+`;wN(l-wCGw8TivKs9Y}WrzWSt8$uATpb-Yapp$fU@B7S|1< zfepeP9)61Ng_57<-*5y>|38sEmEqn!{SSH%f_}hsVEVs-`k_`c|082zW_sVr`Y8Py hj{oHU8` ${LOG_PATH}/generate.log + exit_code=$? + if [ $exit_code -ne 0 ]; then + echo "Microservice failed, please check the logs in artifacts!" + docker logs tgi-gaudi-server >> ${LOG_PATH}/generate.log + exit 1 + fi + sleep 5s + + curl http://${ip_address}:9000/v1/chat/docsum \ + -X POST \ + -d '{"query":"Text Embeddings Inference (TEI) is a toolkit for deploying and serving open source text embeddings and sequence classification models. TEI enables high-performance extraction for the most popular models, including FlagEmbedding, Ember, GTE and E5."}' \ + -H 'Content-Type: application/json' > ${LOG_PATH}/completions.log + exit_code=$? + if [ $exit_code -ne 0 ]; then + echo "Microservice failed, please check the logs in artifacts!" + docker logs docsum-gaudi-backend-server >> ${LOG_PATH}/completions.log + exit 1 + fi + sleep 5s +} + +function validate_megaservice() { + # Curl the Mega Service + curl http://${ip_address}:8888/v1/docsum -H "Content-Type: application/json" -d '{ + "model": "Intel/neural-chat-7b-v3-3", + "messages": "Text Embeddings Inference (TEI) is a toolkit for deploying and serving open source text embeddings and sequence classification models. TEI enables high-performance extraction for the most popular models, including FlagEmbedding, Ember, GTE and E5."}' > ${LOG_PATH}/curl_megaservice.log + + echo "Checking response results, make sure the output is reasonable. " + local status=false + if [[ -f $LOG_PATH/curl_megaservice.log ]] && \ + [[ $(grep -c "versatile toolkit" $LOG_PATH/curl_megaservice.log) != 0 ]] && \ + [[ $(grep -c "offering" $LOG_PATH/curl_megaservice.log) != 0 ]] && \ + [[ $(grep -c "extraction" $LOG_PATH/curl_megaservice.log) != 0 ]]; then + status=true + fi + + if [ $status == false ]; then + echo "Response check failed, please check the logs in artifacts!" + exit 1 + else + echo "Response check succeed!" + fi + + echo "Checking response format, make sure the output format is acceptable for UI." + # TODO +} + +function stop_docker() { + cd $WORKPATH/microservice/gaudi + container_list=$(cat docker_compose.yaml | grep container_name | cut -d':' -f2) + for container_name in $container_list; do + cid=$(docker ps -aq --filter "name=$container_name") + if [[ ! -z "$cid" ]]; then docker stop $cid && docker rm $cid && sleep 1s; fi + done +} + +function main() { + + stop_docker + + build_docker_images + start_services + + validate_microservices + validate_megaservice + + stop_docker + echo y | docker system prune + +} + +main diff --git a/DocSum/tests/test_docsum_on_xeon.sh b/DocSum/tests/test_docsum_on_xeon.sh new file mode 100644 index 000000000..5e73498e0 --- /dev/null +++ b/DocSum/tests/test_docsum_on_xeon.sh @@ -0,0 +1,123 @@ +#!/bin/bash +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -x + +WORKPATH=$(dirname "$PWD") +LOG_PATH="$WORKPATH/tests" +ip_name=$(echo $(hostname) | tr '[a-z]-' '[A-Z]_')_$(echo 'IP') +ip_address=$(eval echo '$'$ip_name) + +function build_docker_images() { + cd $WORKPATH + git clone https://github.com/opea-project/GenAIComps.git + cd GenAIComps + + docker build -t opea/gen-ai-comps:llm-docsum-server --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy -f comps/llms/docsum/langchain/docker/Dockerfile . + + cd $WORKPATH/microservice/xeon + docker build --no-cache -t opea/gen-ai-comps:docsum-megaservice-server --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy -f docker/Dockerfile . + + cd $WORKPATH/ui + docker build --no-cache -t opea/gen-ai-comps:docsum-ui-server --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy -f docker/Dockerfile . + + docker images +} + +function start_services() { + cd $WORKPATH/microservice/xeon + + export LLM_MODEL_ID="Intel/neural-chat-7b-v3-3" + export TGI_LLM_ENDPOINT="http://${ip_address}:8008" + export HUGGINGFACEHUB_API_TOKEN=${HUGGINGFACEHUB_API_TOKEN} + export MEGA_SERVICE_HOST_IP=${ip_address} + export BACKEND_SERVICE_ENDPOINT="http://${ip_address}:8888/v1/docsum" + + # Start Docker Containers + # TODO: Replace the container name with a test-specific name + docker compose -f docker_compose.yaml up -d + + sleep 2m # Waits 2 minutes +} + +function validate_microservices() { + # Check if the microservices are running correctly. + # TODO: Any results check required?? + curl http://${ip_address}:8008/generate \ + -X POST \ + -d '{"inputs":"What is Deep Learning?","parameters":{"max_new_tokens":17, "do_sample": true}}' \ + -H 'Content-Type: application/json' > ${LOG_PATH}/generate.log + exit_code=$? + if [ $exit_code -ne 0 ]; then + echo "Microservice failed, please check the logs in artifacts!" + docker logs tgi_service >> ${LOG_PATH}/generate.log + exit 1 + fi + sleep 5s + + curl http://${ip_address}:9000/v1/chat/docsum \ + -X POST \ + -d '{"query":"Text Embeddings Inference (TEI) is a toolkit for deploying and serving open source text embeddings and sequence classification models. TEI enables high-performance extraction for the most popular models, including FlagEmbedding, Ember, GTE and E5."}' \ + -H 'Content-Type: application/json' > ${LOG_PATH}/completions.log + exit_code=$? + if [ $exit_code -ne 0 ]; then + echo "Microservice failed, please check the logs in artifacts!" + docker logs llm-docsum-server >> ${LOG_PATH}/completions.log + exit 1 + fi + sleep 5s +} + +function validate_megaservice() { + # Curl the Mega Service + curl http://${ip_address}:8888/v1/docsum -H "Content-Type: application/json" -d '{ + "model": "Intel/neural-chat-7b-v3-3", + "messages": "Text Embeddings Inference (TEI) is a toolkit for deploying and serving open source text embeddings and sequence classification models. TEI enables high-performance extraction for the most popular models, including FlagEmbedding, Ember, GTE and E5."}' > ${LOG_PATH}/curl_megaservice.log + + echo "Checking response results, make sure the output is reasonable. " + local status=false + if [[ -f $LOG_PATH/curl_megaservice.log ]] && \ + [[ $(grep -c " flexible" $LOG_PATH/curl_megaservice.log) != 0 ]] && \ + [[ $(grep -c " tool" $LOG_PATH/curl_megaservice.log) != 0 ]] && \ + [[ $(grep -c "kit" $LOG_PATH/curl_megaservice.log) != 0 ]]; then + status=true + fi + + if [ $status == false ]; then + echo "Response check failed, please check the logs in artifacts!" + exit 1 + else + echo "Response check succeed!" + fi + + echo "Checking response format, make sure the output format is acceptable for UI." + # TODO + +} + +function stop_docker() { + cd $WORKPATH/microservice/xeon + container_list=$(cat docker_compose.yaml | grep container_name | cut -d':' -f2) + for container_name in $container_list; do + cid=$(docker ps -aq --filter "name=$container_name") + if [[ ! -z "$cid" ]]; then docker stop $cid && docker rm $cid && sleep 1s; fi + done +} + +function main() { + + stop_docker + + build_docker_images + start_services + + validate_microservices + validate_megaservice + + stop_docker + echo y | docker system prune + +} + +main diff --git a/DocSum/ui/docker/Dockerfile b/DocSum/ui/docker/Dockerfile new file mode 100644 index 000000000..c4785f69a --- /dev/null +++ b/DocSum/ui/docker/Dockerfile @@ -0,0 +1,20 @@ +# Use node 20.11.1 as the base image +FROM node:20.11.1 + +# Update package manager and install Git +RUN apt-get update -y && apt-get install -y git + +# Clone the front-end code repository +RUN git clone https://github.com/opea-project/GenAIExamples.git /home/user/GenAIExamples + +# Set the working directory +WORKDIR /home/user/GenAIExamples/DocSum/ui/svelte + +# Install front-end dependencies +RUN npm install + +# Expose the port of the front-end application +EXPOSE 5173 + +# Run the front-end application +CMD ["npm", "run", "dev", "--", "--host=0.0.0.0"] diff --git a/DocSum/ui/svelte/.editorconfig b/DocSum/ui/svelte/.editorconfig new file mode 100644 index 000000000..2b7a6637f --- /dev/null +++ b/DocSum/ui/svelte/.editorconfig @@ -0,0 +1,10 @@ +[*] +indent_style = tab + +[package.json] +indent_style = space +indent_size = 2 + +[*.md] +indent_style = space +indent_size = 2 diff --git a/DocSum/ui/svelte/.env b/DocSum/ui/svelte/.env index 4fe9cb96a..eab200d16 100644 --- a/DocSum/ui/svelte/.env +++ b/DocSum/ui/svelte/.env @@ -1 +1,6 @@ -BASIC_URL = 'http://x.x.x.x:yyyy' \ No newline at end of file +CHAT_BASE_URL = 'http://x.x.x.x' + +UPLOAD_FILE_BASE_URL = 'http://x.x.x.x' + +PASTE_LINK_BASE_URL = 'http://x.x.x.x' + diff --git a/DocSum/ui/svelte/.eslintignore b/DocSum/ui/svelte/.eslintignore new file mode 100644 index 000000000..38972655f --- /dev/null +++ b/DocSum/ui/svelte/.eslintignore @@ -0,0 +1,13 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/DocSum/ui/svelte/.eslintrc.cjs b/DocSum/ui/svelte/.eslintrc.cjs new file mode 100644 index 000000000..a6592d11f --- /dev/null +++ b/DocSum/ui/svelte/.eslintrc.cjs @@ -0,0 +1,34 @@ +// 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. + +module.exports = { + root: true, + parser: "@typescript-eslint/parser", + extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"], + plugins: ["svelte3", "@typescript-eslint", "neverthrow"], + ignorePatterns: ["*.cjs"], + overrides: [{ files: ["*.svelte"], processor: "svelte3/svelte3" }], + settings: { + "svelte3/typescript": () => require("typescript"), + }, + parserOptions: { + sourceType: "module", + ecmaVersion: 2020, + }, + env: { + browser: true, + es2017: true, + node: true, + }, +}; diff --git a/DocSum/ui/svelte/.prettierignore b/DocSum/ui/svelte/.prettierignore new file mode 100644 index 000000000..38972655f --- /dev/null +++ b/DocSum/ui/svelte/.prettierignore @@ -0,0 +1,13 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/DocSum/ui/svelte/.prettierrc b/DocSum/ui/svelte/.prettierrc new file mode 100644 index 000000000..3b2006102 --- /dev/null +++ b/DocSum/ui/svelte/.prettierrc @@ -0,0 +1,13 @@ +{ + "pluginSearchDirs": [ + "." + ], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} \ No newline at end of file diff --git a/DocSum/ui/svelte/README.md b/DocSum/ui/svelte/README.md index 1a48e301f..ee198fbee 100644 --- a/DocSum/ui/svelte/README.md +++ b/DocSum/ui/svelte/README.md @@ -1,19 +1,21 @@ -

Doc Summary

+

ChatQnA Customized UI

### šŸ“ø Project Screenshots -![project-screenshot](https://imgur.com/oRuDrGX.png) -![project-screenshot](https://imgur.com/j6vo4gl.png) -![project-screenshot](https://imgur.com/LPBvBmM.png) -![project-screenshot](https://imgur.com/yHryOQS.png) +![project-screenshot](https://i.imgur.com/26zMnEr.png) +![project-screenshot](https://i.imgur.com/fZbOiTk.png) +![project-screenshot](https://i.imgur.com/FnY3MuU.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. +- Start a Text Chat:Initiate a text chat with the ability to input written conversations, where the dialogue content can also be customized based on uploaded files. +- Upload File: The choice between uploading locally or copying a remote link. Chat according to uploaded knowledge base. +- Clear: Clear the record of the current dialog box without retaining the contents of the dialog box. +- Chat history: Historical chat records can still be retained after refreshing, making it easier for users to view the context. +- Scroll to Bottom / Top: The chat automatically slides to the bottom. Users can also click the top icon to slide to the top of the chat record. +- End to End Time: Shows the time spent on the current conversation.

šŸ› ļø Get it Running:

@@ -23,7 +25,7 @@ Here're some of the project's features: 3. Modify the required .env variables. ``` - BASIC_URL = '' + DOC_BASE_URL = '' ``` 4. Execute `npm install` to install the corresponding dependencies. diff --git a/DocSum/ui/svelte/package.json b/DocSum/ui/svelte/package.json index 027dad195..3bcef3f8c 100644 --- a/DocSum/ui/svelte/package.json +++ b/DocSum/ui/svelte/package.json @@ -1,53 +1,60 @@ { - "name": "doc-summary", + "name": "sveltekit-auth-example", "version": "0.0.1", + "private": true, "scripts": { "dev": "vite dev", - "build": "vite build && npm run package", + "build": "vite build", "preview": "vite preview", - "package": "svelte-kit sync && svelte-package && publint", - "prepublishOnly": "npm run package", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" - }, - "exports": { - ".": { - "types": "./dist/index.d.ts", - "svelte": "./dist/index.js" - } - }, - "files": [ - "dist", - "!dist/**/*.test.*", - "!dist/**/*.spec.*" - ], - "peerDependencies": { - "svelte": "^4.0.0" + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --check . && eslint .", + "format": "prettier --write ." }, "devDependencies": { - "@sveltejs/adapter-auto": "^3.0.0", - "@sveltejs/kit": "^2.0.0", - "@sveltejs/package": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0", - "autoprefixer": "^10.4.16", - "flowbite": "^2.3.0", - "flowbite-svelte": "^0.38.5", - "flowbite-svelte-icons": "^1.4.0", - "postcss": "^8.4.32", - "postcss-load-config": "^5.0.2", - "publint": "^0.1.9", - "svelte": "^4.2.7", - "svelte-check": "^3.6.0", - "tailwindcss": "^3.3.6", - "tslib": "^2.4.1", - "typescript": "^5.0.0", - "vite": "^5.0.11" + "@playwright/test": "^1.33.0", + "@fortawesome/free-solid-svg-icons": "6.2.0", + "@sveltejs/adapter-auto": "1.0.0-next.75", + "@sveltejs/kit": "^1.30.4", + "@tailwindcss/typography": "0.5.7", + "@types/debug": "4.1.7", + "@typescript-eslint/eslint-plugin": "^5.27.0", + "@typescript-eslint/parser": "^5.27.0", + "autoprefixer": "^10.4.7", + "daisyui": "3.5.1", + "date-picker-svelte": "^2.6.0", + "debug": "4.3.4", + "eslint": "^8.16.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-neverthrow": "1.1.4", + "eslint-plugin-svelte3": "^4.0.0", + "postcss": "^8.4.31", + "postcss-load-config": "^4.0.1", + "postcss-preset-env": "^8.3.2", + "prettier": "^2.8.8", + "prettier-plugin-svelte": "^2.7.0", + "prettier-plugin-tailwindcss": "^0.3.0", + "svelte": "^3.59.1", + "svelte-check": "^2.7.1", + "svelte-fa": "3.0.3", + "svelte-preprocess": "^4.10.7", + "tailwindcss": "^3.1.5", + "tslib": "^2.3.1", + "typescript": "^4.7.4", + "vite": "^4.5.2" }, - "svelte": "./dist/index.js", - "types": "./dist/index.d.ts", "type": "module", "dependencies": { + "date-fns": "^2.30.0", + "driver.js": "^1.3.0", + "flowbite-svelte": "^0.38.5", + "flowbite-svelte-icons": "^1.4.0", + "fuse.js": "^6.6.2", + "lodash": "^4.17.21", + "ramda": "^0.29.0", "sse.js": "^0.6.1", - "svelte-notifications": "^0.9.98" + "svelte-notifications": "^0.9.98", + "playwright": "^1.44.0", + "svrollbar": "^0.12.0" } } diff --git a/DocSum/ui/svelte/playwright.config.ts b/DocSum/ui/svelte/playwright.config.ts new file mode 100644 index 000000000..49d09fa8c --- /dev/null +++ b/DocSum/ui/svelte/playwright.config.ts @@ -0,0 +1,83 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./tests", + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [["html", { open: "never" }]], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: "http://localhost:5173", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { channel: 'chrome' }, + // }, + ], + + /* Folder for test artifacts such as screenshots, videos, traces, etc. */ + // outputDir: 'test-results/', + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // port: 3000, + // }, +}); diff --git a/DocSum/ui/svelte/postcss.config.cjs b/DocSum/ui/svelte/postcss.config.cjs index 5f822bcb1..b384b43eb 100644 --- a/DocSum/ui/svelte/postcss.config.cjs +++ b/DocSum/ui/svelte/postcss.config.cjs @@ -16,12 +16,12 @@ const tailwindcss = require("tailwindcss"); const autoprefixer = require("autoprefixer"); const config = { - plugins: [ - //Some plugins, like tailwindcss/nesting, need to run before Tailwind, - tailwindcss(), - //But others, like autoprefixer, need to run after, - autoprefixer, - ], + plugins: [ + //Some plugins, like tailwindcss/nesting, need to run before Tailwind, + tailwindcss(), + //But others, like autoprefixer, need to run after, + autoprefixer, + ], }; module.exports = config; diff --git a/DocSum/ui/svelte/src/app.d.ts b/DocSum/ui/svelte/src/app.d.ts index 1b9de033b..fa6a0abf7 100644 --- a/DocSum/ui/svelte/src/app.d.ts +++ b/DocSum/ui/svelte/src/app.d.ts @@ -12,16 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -// See https://kit.svelte.dev/docs/types#app -// for information about these interfaces -declare global { - namespace App { - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} - } +// See: https://kit.svelte.dev/docs/types#app +// import { Result} from "neverthrow"; +interface Window { + deviceType: string; } - -export {}; diff --git a/DocSum/ui/svelte/src/app.html b/DocSum/ui/svelte/src/app.html index cdcef542d..db69926ea 100644 --- a/DocSum/ui/svelte/src/app.html +++ b/DocSum/ui/svelte/src/app.html @@ -16,13 +16,13 @@ - - - - - %sveltekit.head% - - -
%sveltekit.body%
- + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ diff --git a/DocSum/ui/svelte/src/app.postcss b/DocSum/ui/svelte/src/app.postcss new file mode 100644 index 000000000..1bb14630c --- /dev/null +++ b/DocSum/ui/svelte/src/app.postcss @@ -0,0 +1,86 @@ +/* Write your global styles here, in PostCSS syntax */ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, body { + height: 100%; +} + +.btn { + @apply flex-nowrap; +} +a.btn { + @apply no-underline; +} +.input { + @apply text-base; +} + +.bg-dark-blue { + background-color: #004a86; +} + +.bg-light-blue { + background-color: #0068b5; +} + +.bg-turquoise { + background-color: #00a3f6; +} + +.bg-header { + background-color: #ffffff; +} + +.bg-button { + background-color: #0068b5; +} + +.bg-title { + background-color: #f7f7f7; +} + +.text-header { + color: #0068b5; +} + +.text-button { + color: #252e47; +} + +.text-title-color { + color: rgb(38,38,38); +} + +.font-intel { + font-family: "intel-clear","tahoma",Helvetica,"helvetica",Arial,sans-serif; +} + +.font-title-intel { + font-family: "intel-one","intel-clear",Helvetica,Arial,sans-serif; +} + +.bg-footer { + background-color: #e7e7e7; +} + +.bg-light-green { + background-color: #d7f3a1; +} + +.bg-purple { + background-color: #653171; +} + +.bg-dark-blue { + background-color: #224678; +} + +.border-input-color { + border-color: #605e5c; +} + +.w-12\/12 { + width: 100% +} \ No newline at end of file diff --git a/DocSum/ui/svelte/src/lib/assets/avatar/svelte/Delete.svelte b/DocSum/ui/svelte/src/lib/assets/avatar/svelte/Delete.svelte new file mode 100644 index 000000000..8847a2227 --- /dev/null +++ b/DocSum/ui/svelte/src/lib/assets/avatar/svelte/Delete.svelte @@ -0,0 +1,30 @@ + + + + + + { + dispatch('DeleteAvatar') }} +viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="20" height="20"> + + diff --git a/DocSum/ui/svelte/src/lib/assets/chat/svelte/Assistant.svelte b/DocSum/ui/svelte/src/lib/assets/chat/svelte/Assistant.svelte new file mode 100644 index 000000000..b68d2a08c --- /dev/null +++ b/DocSum/ui/svelte/src/lib/assets/chat/svelte/Assistant.svelte @@ -0,0 +1,44 @@ + + + diff --git a/DocSum/ui/svelte/src/lib/assets/chat/svelte/PaperAirplane.svelte b/DocSum/ui/svelte/src/lib/assets/chat/svelte/PaperAirplane.svelte new file mode 100644 index 000000000..d1d14077f --- /dev/null +++ b/DocSum/ui/svelte/src/lib/assets/chat/svelte/PaperAirplane.svelte @@ -0,0 +1,68 @@ + + + + + + + + diff --git a/DocSum/ui/svelte/src/lib/assets/chat/svelte/PersonOutlined.svelte b/DocSum/ui/svelte/src/lib/assets/chat/svelte/PersonOutlined.svelte new file mode 100644 index 000000000..dd2f9fdb7 --- /dev/null +++ b/DocSum/ui/svelte/src/lib/assets/chat/svelte/PersonOutlined.svelte @@ -0,0 +1,26 @@ + + + + + diff --git a/DocSum/ui/svelte/src/lib/assets/layout/css/driver.css b/DocSum/ui/svelte/src/lib/assets/layout/css/driver.css new file mode 100644 index 000000000..453db6082 --- /dev/null +++ b/DocSum/ui/svelte/src/lib/assets/layout/css/driver.css @@ -0,0 +1,94 @@ +.driverjs-theme { + background: transparent; + color: #fff; + box-shadow: none; + padding: 0; +} + +.driver-popover-arrow { + border: 10px solid transparent; + animation: blink 1s 3 steps(1); +} + +@keyframes blink { + 0% { + opacity: 1; + } + 50% { + opacity: 0.2; + } + 100% { + opacity: 1; + } +} + +.driver-popover.driverjs-theme .driver-popover-arrow-side-left.driver-popover-arrow { + border-left-color: #174ed1; +} + +.driver-popover.driverjs-theme .driver-popover-arrow-side-right.driver-popover-arrow { + border-right-color: #174ed1; +} + +.driver-popover.driverjs-theme .driver-popover-arrow-side-top.driver-popover-arrow { + border-top-color: #174ed1; +} + +.driver-popover.driverjs-theme .driver-popover-arrow-side-bottom.driver-popover-arrow { + border-bottom-color: #174ed1; +} + +.driver-popover-footer { + background: transparent; + color: #fff; +} +.driver-popover-title { + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} + +.driver-popover-title, +.driver-popover-description { + display: block; + padding: 15px 15px 7px 15px; + background: #174ed1; + border: none; +} + +.driver-popover-close-btn { + color: #fff; +} + +.driver-popover-footer button:hover, +.driver-popover-footer button:focus { + background: #174ed1; + color: #fff; +} + +.driver-popover-description { + padding: 5px 15px; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; +} + +.driver-popover-title[style*="block"] + .driver-popover-description { + margin: 0; +} +.driver-popover-progress-text { + color: #fff; +} + +.driver-popover-footer button { + background: #174ed1; + border: 2px #174ed1 dashed; + color: #fff; + border-radius: 50%; + text-shadow: none; +} +.driver-popover-close-btn:hover, +.driver-popover-close-btn:focus { + color: #fff; +} +.driver-popover-navigation-btns button + button { + margin-left: 10px; +} diff --git a/DocSum/ui/svelte/src/lib/assets/upload/next.svelte b/DocSum/ui/svelte/src/lib/assets/upload/next.svelte new file mode 100644 index 000000000..70f4fe25e --- /dev/null +++ b/DocSum/ui/svelte/src/lib/assets/upload/next.svelte @@ -0,0 +1,31 @@ + + + diff --git a/DocSum/ui/svelte/src/lib/assets/upload/previous.svelte b/DocSum/ui/svelte/src/lib/assets/upload/previous.svelte new file mode 100644 index 000000000..c47d9c49d --- /dev/null +++ b/DocSum/ui/svelte/src/lib/assets/upload/previous.svelte @@ -0,0 +1,31 @@ + + + diff --git a/DocSum/ui/svelte/src/lib/assets/voice/svg/paste.svg b/DocSum/ui/svelte/src/lib/assets/voice/svg/paste.svg new file mode 100644 index 000000000..9fe89acc1 --- /dev/null +++ b/DocSum/ui/svelte/src/lib/assets/voice/svg/paste.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/DocSum/ui/svelte/src/lib/assets/voice/svg/uploadFile.svg b/DocSum/ui/svelte/src/lib/assets/voice/svg/uploadFile.svg new file mode 100644 index 000000000..362a6994e --- /dev/null +++ b/DocSum/ui/svelte/src/lib/assets/voice/svg/uploadFile.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/DocSum/ui/svelte/src/lib/modules/chat/ChatMessage.svelte b/DocSum/ui/svelte/src/lib/modules/chat/ChatMessage.svelte new file mode 100644 index 000000000..1f69d1429 --- /dev/null +++ b/DocSum/ui/svelte/src/lib/modules/chat/ChatMessage.svelte @@ -0,0 +1,68 @@ + + + + +
+
+ +
+
+
+

+ {@html msg.content} +

+
+
+
+{#if time} +
+ { + dispatch("scrollTop"); + }} + /> +
+{/if} + + diff --git a/DocSum/ui/svelte/src/lib/modules/chat/MessageAvatar.svelte b/DocSum/ui/svelte/src/lib/modules/chat/MessageAvatar.svelte new file mode 100644 index 000000000..0f6a24b96 --- /dev/null +++ b/DocSum/ui/svelte/src/lib/modules/chat/MessageAvatar.svelte @@ -0,0 +1,30 @@ + + + + +{#if role === MessageRole.User} + +{:else} + +{/if} diff --git a/DocSum/ui/svelte/src/lib/modules/chat/MessageTimer.svelte b/DocSum/ui/svelte/src/lib/modules/chat/MessageTimer.svelte new file mode 100644 index 000000000..836758f02 --- /dev/null +++ b/DocSum/ui/svelte/src/lib/modules/chat/MessageTimer.svelte @@ -0,0 +1,67 @@ + + + + +
+
+
+ + { + dispatch("handleTop"); + }} + > +
+
+
+ +
+
+ End to End Time: +

{time}s

+
+
+
+
diff --git a/DocSum/ui/svelte/src/lib/modules/frame/Layout.svelte b/DocSum/ui/svelte/src/lib/modules/frame/Layout.svelte new file mode 100644 index 000000000..0c5b997d2 --- /dev/null +++ b/DocSum/ui/svelte/src/lib/modules/frame/Layout.svelte @@ -0,0 +1,48 @@ + + + + +
+
+
+ + + +
+
+
diff --git a/DocSum/ui/svelte/src/lib/network/chat/Network.ts b/DocSum/ui/svelte/src/lib/network/chat/Network.ts new file mode 100644 index 000000000..1abaa2ff3 --- /dev/null +++ b/DocSum/ui/svelte/src/lib/network/chat/Network.ts @@ -0,0 +1,33 @@ +// 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 { env } from "$env/dynamic/public"; +import { SSE } from "sse.js"; + +const CHAT_BASE_URL = env.CHAT_BASE_URL; + +export async function fetchTextStream(query: string, knowledge_base_id: string) { + let payload = {}; + let url = ""; + + payload = { + query: query, + }; + url = `${CHAT_BASE_URL}`; + + return new SSE(url, { + headers: { "Content-Type": "application/json" }, + payload: JSON.stringify(payload), + }); +} diff --git a/DocSum/ui/svelte/src/lib/network/upload/Network.ts b/DocSum/ui/svelte/src/lib/network/upload/Network.ts new file mode 100644 index 000000000..3b877d473 --- /dev/null +++ b/DocSum/ui/svelte/src/lib/network/upload/Network.ts @@ -0,0 +1,58 @@ +// 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 { env } from "$env/dynamic/public"; + +const UPLOAD_FILE_BASE_URL = env.UPLOAD_FILE_BASE_URL; +const PASTE_LINK_BASE_URL = env.PASTE_LINK_BASE_URL; + +export async function fetchKnowledgeBaseId(file: Blob, fileName: string) { + const url = `${UPLOAD_FILE_BASE_URL}`; + const formData = new FormData(); + formData.append("file", file, fileName); + const init: RequestInit = { + method: "POST", + body: formData, + }; + + try { + const response = await fetch(url, init); + if (!response.ok) throw response.status; + return await response.json(); + } catch (error) { + console.error("network error: ", error); + return undefined; + } +} + +export async function fetchKnowledgeBaseIdByPaste(pasteUrlList: any, urlType: string | undefined) { + const url = `${PASTE_LINK_BASE_URL}`; + const data = { + link_list: pasteUrlList, + }; + const init: RequestInit = { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }; + + try { + const response = await fetch(url, init); + if (!response.ok) throw response.status; + return await response.json(); + } catch (error) { + console.error("network error: ", error); + return undefined; + } +} diff --git a/DocSum/ui/svelte/src/lib/shared/Utils.ts b/DocSum/ui/svelte/src/lib/shared/Utils.ts index bae0543f6..fb182cef6 100644 --- a/DocSum/ui/svelte/src/lib/shared/Utils.ts +++ b/DocSum/ui/svelte/src/lib/shared/Utils.ts @@ -13,14 +13,42 @@ // limitations under the License. export function scrollToBottom(scrollToDiv: HTMLElement) { - if (scrollToDiv) { - setTimeout( - () => - scrollToDiv.scroll({ - behavior: "auto", - top: scrollToDiv.scrollHeight, - }), - 100, - ); - } + if (scrollToDiv) { + setTimeout( + () => + scrollToDiv.scroll({ + behavior: "auto", + top: scrollToDiv.scrollHeight, + }), + 100, + ); + } +} + +export function scrollToTop(scrollToDiv: HTMLElement) { + if (scrollToDiv) { + setTimeout( + () => + scrollToDiv.scroll({ + behavior: "auto", + top: 0, + }), + 100, + ); + } +} + +export function getCurrentTimeStamp() { + return Math.floor(new Date().getTime()); +} + +export function fromTimeStampToTime(timeStamp: number) { + return new Date(timeStamp * 1000).toTimeString().slice(0, 8); +} + +export function formatTime(seconds) { + const hours = String(Math.floor(seconds / 3600)).padStart(2, "0"); + const minutes = String(Math.floor((seconds % 3600) / 60)).padStart(2, "0"); + const remainingSeconds = String(seconds % 60).padStart(2, "0"); + return `${hours}:${minutes}:${remainingSeconds}`; } diff --git a/DocSum/ui/svelte/src/lib/shared/components/chat/gallery.svelte b/DocSum/ui/svelte/src/lib/shared/components/chat/gallery.svelte new file mode 100644 index 000000000..a89e857b6 --- /dev/null +++ b/DocSum/ui/svelte/src/lib/shared/components/chat/gallery.svelte @@ -0,0 +1,156 @@ + + + + + + + diff --git a/DocSum/ui/svelte/src/lib/shared/components/loading/Loading.svelte b/DocSum/ui/svelte/src/lib/shared/components/loading/Loading.svelte new file mode 100644 index 000000000..51e89cfe7 --- /dev/null +++ b/DocSum/ui/svelte/src/lib/shared/components/loading/Loading.svelte @@ -0,0 +1,48 @@ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DocSum/ui/svelte/src/lib/shared/components/scrollbar/Scrollbar.svelte b/DocSum/ui/svelte/src/lib/shared/components/scrollbar/Scrollbar.svelte new file mode 100644 index 000000000..f18e23e69 --- /dev/null +++ b/DocSum/ui/svelte/src/lib/shared/components/scrollbar/Scrollbar.svelte @@ -0,0 +1,48 @@ + + + + +
+ +
+ +
+
+
+ + diff --git a/DocSum/ui/svelte/src/lib/shared/components/upload/PasteKnowledge.svelte b/DocSum/ui/svelte/src/lib/shared/components/upload/PasteKnowledge.svelte new file mode 100644 index 000000000..d0758e770 --- /dev/null +++ b/DocSum/ui/svelte/src/lib/shared/components/upload/PasteKnowledge.svelte @@ -0,0 +1,52 @@ + + + + + diff --git a/DocSum/ui/svelte/src/lib/shared/components/upload/upload-knowledge.svelte b/DocSum/ui/svelte/src/lib/shared/components/upload/upload-knowledge.svelte new file mode 100644 index 000000000..b1be9770c --- /dev/null +++ b/DocSum/ui/svelte/src/lib/shared/components/upload/upload-knowledge.svelte @@ -0,0 +1,48 @@ + + + + +
+ +
diff --git a/DocSum/ui/svelte/src/lib/shared/components/upload/uploadFile.svelte b/DocSum/ui/svelte/src/lib/shared/components/upload/uploadFile.svelte new file mode 100644 index 000000000..ac190f5c2 --- /dev/null +++ b/DocSum/ui/svelte/src/lib/shared/components/upload/uploadFile.svelte @@ -0,0 +1,167 @@ + + + + +
+ +
+ + +
+
+ Data Source +
+ (hidden6 = true)} + class="mb-4 dark:text-white" + /> +
+

+ Please upload your local file or paste a remote file link, and Chat will + respond based on the content of the uploaded file. +

+ + + Upload File + + + + Paste Link + + + + {#if ($knowledgeName) && ($knowledgeName !== "")} +
+

{$knowledgeName}

+ handleKnowledgeDelete()} /> +
+ {/if} +
diff --git a/DocSum/ui/svelte/src/lib/shared/constant/Interface.ts b/DocSum/ui/svelte/src/lib/shared/constant/Interface.ts new file mode 100644 index 000000000..221f17a26 --- /dev/null +++ b/DocSum/ui/svelte/src/lib/shared/constant/Interface.ts @@ -0,0 +1,47 @@ +// 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. + +export enum MessageRole { + Assistant, + User, +} + +export enum MessageType { + Text, + SingleAudio, + AudioList, + SingleImage, + ImageList, + singleVideo, +} + +type Map = T extends MessageType.Text | MessageType.SingleAudio + ? string + : T extends MessageType.AudioList + ? string[] + : T extends MessageType.SingleImage + ? { imgSrc: string; imgId: string } + : { imgSrc: string; imgId: string }[]; + +export interface Message { + role: MessageRole; + type: MessageType; + content: Map; + time: number; +} + +export enum LOCAL_STORAGE_KEY { + STORAGE_CHAT_KEY = "chatMessages", + STORAGE_TIME_KEY = "initTime", +} diff --git a/DocSum/ui/svelte/src/lib/shared/stores/common/Store.ts b/DocSum/ui/svelte/src/lib/shared/stores/common/Store.ts new file mode 100644 index 000000000..76d1fb04a --- /dev/null +++ b/DocSum/ui/svelte/src/lib/shared/stores/common/Store.ts @@ -0,0 +1,39 @@ +// 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 { writable } from "svelte/store"; + +export let open = writable(true); + +export let knowledgeAccess = writable(true); + +export let showTemplate = writable(false); + +export let showSidePage = writable(false); + +export let droppedObj = writable({}); + +export let isLoading = writable(false); + +export let newUploadNum = writable(0); + +export let ifStoreMsg = writable(true); + +export const resetControl = writable(false); + +export const knowledge1 = writable<{ + id: string; +}>(); + +export const knowledgeName = writable(""); diff --git a/DocSum/ui/svelte/src/routes/+layout.svelte b/DocSum/ui/svelte/src/routes/+layout.svelte index 9a09eaadf..8141177d4 100644 --- a/DocSum/ui/svelte/src/routes/+layout.svelte +++ b/DocSum/ui/svelte/src/routes/+layout.svelte @@ -15,11 +15,34 @@ --> - + +
+ +
+ +
diff --git a/DocSum/ui/svelte/src/routes/+page.svelte b/DocSum/ui/svelte/src/routes/+page.svelte index 0270ecd99..e66871e52 100644 --- a/DocSum/ui/svelte/src/routes/+page.svelte +++ b/DocSum/ui/svelte/src/routes/+page.svelte @@ -15,80 +15,254 @@ --> -
-
-

Please upload file or paste content for summarization.

-
-
- -
-
- -
-
+ +
+
+
+

ChatQnA

+ +
+
+
+
+ { + if (event.key === "Enter" && !event.shiftKey && query) { + event.preventDefault(); + handleTextSubmit(); + } + }} + /> + +
+
+
+ + + {#if Array.isArray(chatMessages) && chatMessages.length > 0 && !loading} +
+
+ +
+
+ {/if} + + +
+ + {#each chatMessages as message, i} + handleTop()} + msg={message} + time={i === 0 || (message.time > 0 && message.time < 100) + ? message.time + : ""} + /> + {/each} + + + {#if loading} + + {/if} +
+ +
+ + diff --git a/DocSum/ui/svelte/src/routes/+page.ts b/DocSum/ui/svelte/src/routes/+page.ts new file mode 100644 index 000000000..f4de8d676 --- /dev/null +++ b/DocSum/ui/svelte/src/routes/+page.ts @@ -0,0 +1,26 @@ +// 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 { browser } from "$app/environment"; +import { LOCAL_STORAGE_KEY } from "$lib/shared/constant/Interface"; + +export const load = async () => { + if (browser) { + const chat = localStorage.getItem(LOCAL_STORAGE_KEY.STORAGE_CHAT_KEY); + + return { + chatMsg: JSON.parse(chat || "[]"), + }; + } +}; diff --git a/DocSum/ui/svelte/svelte.config.js b/DocSum/ui/svelte/svelte.config.js index 35740d5e8..0f2977ecc 100644 --- a/DocSum/ui/svelte/svelte.config.js +++ b/DocSum/ui/svelte/svelte.config.js @@ -13,23 +13,26 @@ // limitations under the License. import adapter from "@sveltejs/adapter-auto"; -import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; +import preprocess from "svelte-preprocess"; +import postcssPresetEnv from "postcss-preset-env"; /** @type {import('@sveltejs/kit').Config} */ const config = { - // Consult https://kit.svelte.dev/docs/integrations#preprocessors - // for more information about preprocessors - preprocess: [vitePreprocess({})], + // Consult https://github.com/sveltejs/svelte-preprocess + // for more information about preprocessors + preprocess: preprocess({ + sourceMap: true, + postcss: { + plugins: [postcssPresetEnv({ features: { "nesting-rules": true } })], + }, + }), - kit: { - // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. - // If your environment is not supported or you settled on a specific environment, switch out the adapter. - // See https://kit.svelte.dev/docs/adapters for more information about adapters. - adapter: adapter(), - env: { - publicPrefix: "", - }, - }, + kit: { + adapter: adapter(), + env: { + publicPrefix: "", + }, + }, }; export default config; diff --git a/DocSum/ui/svelte/tailwind.config.cjs b/DocSum/ui/svelte/tailwind.config.cjs index 469db3355..6cc3a8b95 100644 --- a/DocSum/ui/svelte/tailwind.config.cjs +++ b/DocSum/ui/svelte/tailwind.config.cjs @@ -13,31 +13,31 @@ // limitations under the License. const config = { - content: ["./src/**/*.{html,js,svelte,ts}", "./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}"], + content: ["./src/**/*.{html,js,svelte,ts}", "./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}"], - plugins: [require("flowbite/plugin")], + plugins: [require("flowbite/plugin")], - darkMode: "class", + darkMode: "class", - theme: { - extend: { - colors: { - // flowbite-svelte - primary: { - 50: "#f2f8ff", - 100: "#eef5ff", - 200: "#deecff", - 300: "#cce2ff", - 400: "#add0ff", - 500: "#5da2fe", - 600: "#2f81ef", - 700: "#2780eb", - 800: "#226fcc", - 900: "#1b5aa5", - }, - }, - }, - }, + theme: { + extend: { + colors: { + // flowbite-svelte + primary: { + 50: "#FFF5F2", + 100: "#FFF1EE", + 200: "#FFE4DE", + 300: "#FFD5CC", + 400: "#FFBCAD", + 500: "#FE795D", + 600: "#EF562F", + 700: "#EB4F27", + 800: "#CC4522", + 900: "#A5371B", + }, + }, + }, + }, }; module.exports = config; diff --git a/DocSum/ui/svelte/tests/chatQnA.spec.ts b/DocSum/ui/svelte/tests/chatQnA.spec.ts new file mode 100644 index 000000000..ee52e802e --- /dev/null +++ b/DocSum/ui/svelte/tests/chatQnA.spec.ts @@ -0,0 +1,72 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { test, expect, type Page } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("/"); +}); + +const CHAT_ITEMS = ["What is the total revenue of Nike in 2023?"]; +const UPLOAD_LINK = ["https://www.ces.tech/"]; +const FILE_PATH = "./test_file.txt"; + +// Helper function to check notification text +async function checkNotificationText(page, expectedText) { + const notificationText = await page.textContent(".notification"); + expect(notificationText).toContain(expectedText); +} + +// Helper function to enter message to chat +async function enterMessageToChat(page, message) { + const newChat = page.getByTestId("chat-input"); + await newChat.fill(message); + await newChat.press("Enter"); + await expect(page.getByTestId("msg-time")).toBeVisible(); +} + +// Helper function to upload a file +async function uploadFile(page, filePath) { + const fileUpload = page.getByTestId("file-upload"); + await fileUpload.setInputFiles(filePath); + await checkNotificationText(page, "Uploaded successfully"); +} + +// Helper function to paste link +async function pasteLink(page, link) { + const pasteLink = page.getByTestId("paste-link"); + await pasteLink.fill(link); + const pasteClick = page.getByTestId("paste-click"); + await pasteClick.click(); + await checkNotificationText(page, "Uploaded successfully"); +} + +test.describe("New Chat", () => { + // chat + test("should enter message to chat", async ({ page }) => { + await enterMessageToChat(page, CHAT_ITEMS[0]); + }); + + // clear chat + test("should clear chat", async ({ page }) => { + const clearChat = page.getByTestId("clear-chat"); + await clearChat.click(); + }); +}); + +test.describe("Upload file and create new Chat", () => { + // upload file + test("should upload a file", async ({ page }) => { + await uploadFile(page, FILE_PATH); + }); + + // paste link + test("should paste link", async ({ page }) => { + await pasteLink(page, UPLOAD_LINK[0]); + }); + + // chat with uploaded file and link + test("should test uploaded chat", async ({ page }) => { + await enterMessageToChat(page, CHAT_ITEMS[0]); + }); +}); diff --git a/DocSum/ui/svelte/tests/test_file.txt b/DocSum/ui/svelte/tests/test_file.txt new file mode 100644 index 000000000..93fc5da94 --- /dev/null +++ b/DocSum/ui/svelte/tests/test_file.txt @@ -0,0 +1,104 @@ +Follow CES +EXHIBIT +REGISTER +All together. All ON. +Registration is now open forĀ CES® 2024 — taking place Jan.Ā 9-12, in Las Vegas. +Flip the switch on global business opportunity with CES, where you can meet with partners, customers, media, investors, and policymakers from across the industry and the world all in one place. +Don't miss your chance to be a part of the most powerful tech event in the world. +KEYNOTE ANNOUNCEMENT +Qualcomm CEO to Keynote CES 2024 + Cristiano Amon + President and CEO, Qualcomm +Anticipated Numbers for CES 2024 +130K+ +attendees +1000+ +startups within Eureka Park +3500+ +exhibitors and a sold-out West Hall + Learn more + arrow-black +Featured Podcast +Li-Fi Unleashes the Future of Esports +Listen Nowarrow-black +Featured Event +Apply Today for CTA Match +Learn Morearrow-black +CES 2024 is ALL ON +Ɨ +Vehicle Technology +With features like adaptive cruise control, collision prevention and lane guidance, technology is paving the way to safer roads. Discover what’s driving the innovations behind concept cars, connected vehicles and autonomous mobility.Ā  +Learn Morearrow-black +Ā  +Featured Podcast +On the Fast Track to Autonomous Driving: Mobileye +Read Morearrow-black +Brunswick Boating Tech Smooths Rough Waters +Register for CES 2024 +See the next generation of innovation at CES 2024. + Register Now +Featured Exhibitors +See theĀ companies from across the globe that will be showcasing the latest in digital health, food tech, automotive tech, NFTs, gaming, smart home and more. +Ā  +Ā  +Ā  + View 2024 Featured Exhibitors + arrow-black +Apply for Eureka Parkā„¢ļø +Eureka ParkĀ is the buzzworthy startup arena that provides a unique opportunity to launch a new product, service or idea. If you’re looking for your big break in the tech industry, Eureka Park is the place for you. + Apply Now +Exhibit at CES +CES is where business gets done. +CASE STUDY +VW +Over a busy four days in Las Vegas, Volkswagen showed the world and media that its accomplishments transcend the legacy auto sector. It’s ID.7 sedan promises stellar performance and efficiency with a 435-mile range plus impressive features. +Read Morearrow-black +600K +interactions on social media +CES is a great opportunity, both from a business perspective in the networking sense and seeing what's going on in the tech field, and also it's a great opportunity from a media perspective because we see massive media attendance and we see a great deal of coverage. +MARK GILLIES +DIRECTOR OF PUBLIC RELATIONS AND REPUTATION, VOLKSWAGEN +CES Success Stories 2023: Volkswagon +Ɨ +Want to Exhibit at CES 2024? +Showcase your brand, launch your latest products and win business at the ultimate platform for innovation. + Contact Us +Latest Articles + CES 2024 Sector Trends: Digital Health +Read more +arrow-black +Nasdaq Keynote, CES 2024: Finance Taps Tech for Humanity +Read more +arrow-black +Walmart Keynote at CES 2024: Disruptive Retail Tech +Read more +arrow-black +Press Releases +Qualcomm CEO Cristiano Amon to Highlight How We Will Interact with Our Devices in the AI Age During CES 2024 Keynote +HD Hyundai to Keynote CES 2024 +Elevance Health’s Gail Boudreaux to Keynote CES 2024 + View all press releases + arrow-black +CES is owned and produced by the Consumer Technology Association, which provides the ultimate platform for technology leaders to connect, collaborate, and propel consumer technology forward. + Become a CTA Member +About CES +CES Events +Innovation Awards +CES Tech Talk Podcast +Promote Your Brand +Topics +Articles +CES Success Stories +Schedule +Our Partners +Information for: +Exhibitors +Media +International +Follow CES +Code of Conduct +Terms of Use +Privacy +Sitemap +Copyright Ā© 2003 - 2023. All rights reserved. +CTATECH-PROD2 \ No newline at end of file diff --git a/DocSum/ui/svelte/tsconfig.json b/DocSum/ui/svelte/tsconfig.json index 8ed3dd7f2..6ae0c8c44 100644 --- a/DocSum/ui/svelte/tsconfig.json +++ b/DocSum/ui/svelte/tsconfig.json @@ -1,15 +1,17 @@ { - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "module": "NodeNext", - "moduleResolution": "NodeNext" - } + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true + } + // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in } diff --git a/DocSum/ui/svelte/vite.config.ts b/DocSum/ui/svelte/vite.config.ts index fc201737f..1166165dc 100644 --- a/DocSum/ui/svelte/vite.config.ts +++ b/DocSum/ui/svelte/vite.config.ts @@ -13,8 +13,11 @@ // limitations under the License. import { sveltekit } from "@sveltejs/kit/vite"; -import { defineConfig } from "vite"; +import type { UserConfig } from "vite"; -export default defineConfig({ - plugins: [sveltekit()], -}); +const config: UserConfig = { + plugins: [sveltekit()], + server: {}, +}; + +export default config;