From 546eb0db00ebc68631489138251562a806200761 Mon Sep 17 00:00:00 2001 From: Duc Nguyen Date: Thu, 30 Nov 2023 10:19:55 -0500 Subject: [PATCH] Sync parity (#31) - Updates to Dockerfile & docker-compose. Relying on 1 single Dockerfile now. - Multi-stage build in Dockerfile with different targets to mimic prod & staging env. - Add more availability to openai and cohere endpoints - front-end revamp with multi-chat and persistent chat models Signed-off-by: Duc Nguyen --- .dockerignore | 32 ++ .github/workflows/default.yml | 83 ++-- Dockerfile | 38 +- Makefile | 14 +- README.md | 7 +- SECURITY.md | 2 +- catalog-info.yaml | 2 +- docker-compose.yml | 30 +- front_end/.dockerignore | 1 - front_end/.env-cmdrc | 11 + front_end/Dockerfile | 6 - front_end/README.md | 5 - front_end/package.json | 5 +- front_end/public/index.html | 5 +- front_end/public/manifest.json | 4 +- front_end/public/styles.css | 358 +++++++++++++++--- front_end/src/app/app.tsx | 202 ++++++++-- .../src/app/components/ChatBox/ChatBox.tsx | 120 +++--- .../app/components/ChatBox/ClearButton.tsx | 67 ---- .../app/components/ChatBox/MessageHistory.tsx | 10 +- .../app/components/ChatNavBar/ChatNavBar.tsx | 77 ++++ .../app/components/ChatNavBar/ChatNavItem.tsx | 124 ++++++ .../components/ChatNavBar/ClearChatDialog.tsx | 69 ++++ .../src/app/components/ChatNavBar/index.ts | 20 + .../EmptyChatBox/EmptyChatBoxComponent.tsx} | 28 +- .../src/app/components/EmptyChatBox/index.ts | 20 + front_end/src/app/components/Icons/Arrow.tsx | 36 ++ .../src/app/components/Icons/CancelIcon.tsx | 35 ++ .../src/app/components/Icons/EditIcon.tsx | 37 ++ .../Icons/EmptyStateIllustration.tsx | 53 +++ .../src/app/components/Icons/SaveIcon.tsx | 33 ++ .../src/app/components/Icons/TrashCanIcon.tsx | 45 +++ .../ModelBanner/ClearChatConfirmDialog.tsx | 61 +++ .../components/ModelBanner/ModelBanner.tsx | 60 +++ .../src/app/components/ModelBanner/index.ts | 20 + .../SettingsDialog/settingsDialog.tsx | 144 +++++-- .../markdown-pre-block.component.tsx | 9 +- front_end/src/app/interfaces.tsx | 62 ++- front_end/src/constants.tsx | 183 +++++++-- .../{environment.ts => environment.dev.ts} | 3 - .../environment.staging.ts} | 38 +- front_end/src/services/apiService.tsx | 32 +- front_end/yarn.lock | 10 +- llm_gateway/app.py | 11 +- llm_gateway/constants.py | 64 ++++ llm_gateway/db/models.py | 3 +- llm_gateway/db/utils.py | 7 +- llm_gateway/exceptions.py | 55 ++- llm_gateway/logger.py | 21 + llm_gateway/models.py | 16 +- llm_gateway/providers/cohere.py | 118 ++++-- llm_gateway/providers/openai.py | 190 ++++++---- llm_gateway/routers/api.py | 48 +++ llm_gateway/routers/cohere_api.py | 76 +++- llm_gateway/routers/openai_api.py | 136 +++++-- llm_gateway/utils.py | 79 +++- tests/test_pii_scrubber.py | 54 ++- tests/test_utils.py | 7 +- 58 files changed, 2451 insertions(+), 635 deletions(-) create mode 100644 .dockerignore delete mode 100644 front_end/.dockerignore create mode 100644 front_end/.env-cmdrc delete mode 100644 front_end/Dockerfile delete mode 100644 front_end/src/app/components/ChatBox/ClearButton.tsx create mode 100644 front_end/src/app/components/ChatNavBar/ChatNavBar.tsx create mode 100644 front_end/src/app/components/ChatNavBar/ChatNavItem.tsx create mode 100644 front_end/src/app/components/ChatNavBar/ClearChatDialog.tsx create mode 100644 front_end/src/app/components/ChatNavBar/index.ts rename front_end/src/app/{app.spec.tsx => components/EmptyChatBox/EmptyChatBoxComponent.tsx} (64%) create mode 100644 front_end/src/app/components/EmptyChatBox/index.ts create mode 100644 front_end/src/app/components/Icons/Arrow.tsx create mode 100644 front_end/src/app/components/Icons/CancelIcon.tsx create mode 100644 front_end/src/app/components/Icons/EditIcon.tsx create mode 100644 front_end/src/app/components/Icons/EmptyStateIllustration.tsx create mode 100644 front_end/src/app/components/Icons/SaveIcon.tsx create mode 100644 front_end/src/app/components/Icons/TrashCanIcon.tsx create mode 100644 front_end/src/app/components/ModelBanner/ClearChatConfirmDialog.tsx create mode 100644 front_end/src/app/components/ModelBanner/ModelBanner.tsx create mode 100644 front_end/src/app/components/ModelBanner/index.ts rename front_end/src/environments/{environment.ts => environment.dev.ts} (85%) rename front_end/src/{app/components/ChatBox/SendButton.tsx => environments/environment.staging.ts} (54%) create mode 100644 llm_gateway/constants.py create mode 100644 llm_gateway/logger.py create mode 100644 llm_gateway/routers/api.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fdd7d68 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,32 @@ +# Python +__pycache__ +*.py[cod] +*.pyo +*.pyd +.Python +env/ +venv/ +*.log +*.csv +*.json +*.sqlite + +# Node +/node_modules +/npm-debug.log +/yarn-error.log + +# React +/build + +# Docker +Dockerfile +.dockerignore + +# Git +.git +.gitignore + +# IDEs +.idea +.vscode diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index 022b850..1d09a20 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -9,6 +9,59 @@ concurrency: cancel-in-progress: true jobs: + check_modified_files: + name: Check modified files + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - uses: dorny/paths-filter@v2 + id: check-files + with: + filters: | + frontend_modified: + - "front_end/**" + backend_modified: + - "Dockerfile" + - 'llm_service/**' + - pyproject.toml + - poetry.lock + outputs: + frontend_modified: ${{ steps.check-files.outputs.frontend_modified }} + backend_modified: ${{ steps.check-files.outputs.backend_modified }} + + + frontend_test: + name: Tests & Lints LLM Gateway Frontend + if: needs.check_modified_files.outputs.frontend_modified == 'true' + runs-on: ubuntu-latest + needs: + - check_modified_files + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: 16.13.1 + + - name: Install dependencies + run: | + npm install -g yarn + (cd front_end && yarn install) + + - name: Check compatible licenses + run: (cd front_end && yarn check-license) + + - name: Run linting + run: (cd front_end && yarn lint) + + - name: Run Tests + run: (cd front_end && yarn test) + + backend_tests: name: Tests & Lints LLM Gateway Backend runs-on: ubuntu-latest @@ -36,31 +89,5 @@ jobs: - name: Run Python Test Suite run: poetry run pytest - - frontend_tests: - name: Tests & Lints LLM Gateway Frontend - strategy: - matrix: - node-version: [16.13.1] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - - name: Install dependencies - run: | - npm install -g yarn - (cd front_end && yarn install) - - - name: Check compatible licenses - run: (cd front_end && yarn check-license) - - - name: Run linting - run: (cd front_end && yarn lint) - - - name: Run Tests - run: (cd front_end && yarn test) + env: + DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/llm_gateway" diff --git a/Dockerfile b/Dockerfile index 3ab39a3..0b7da5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ ARG BASE_LAYER=base -FROM python:3.11.3-slim-buster1 as base +FROM python:3.11.3-slim-buster as base LABEL maintainer="Data Science & Engineering @ Wealthsimple " ############################################## @@ -14,7 +14,7 @@ FROM python:3.11-bullseye as base-dev ############################################## -FROM $BASE_LAYER as builder +FROM $BASE_LAYER as backend-builder RUN pip install poetry==1.3.2 @@ -39,7 +39,7 @@ COPY --chown=nobody:nogroup ./alembic/ ./alembic/ ############################################## -FROM builder as test-suite +FROM backend-builder as backend-test-suite RUN poetry install --no-interaction --no-root @@ -52,9 +52,33 @@ CMD ["pytest"] ############################################## -FROM builder as application +FROM node:16.13.1 as frontend-pre-builder -# fastapi server port -EXPOSE 5000 5000 +WORKDIR /usr/src/app + +# Install dependencies +COPY front_end/package.json front_end/yarn.lock front_end/.eslintrc.json front_end/tsconfig.json front_end/.env-cmdrc ./ +RUN yarn install + +############################################## + +FROM frontend-pre-builder as frontend-builder-staging + +# Build ready for Staging +COPY ./front_end ./ +RUN yarn build-staging + +############################################## + +FROM frontend-pre-builder as frontend-builder-production + +# Build ready for Production +COPY ./front_end ./ +RUN yarn build-production + +############################################## -CMD ["uvicorn", "--host", "0.0.0.0","--port", "5000", "llm_gateway.app:app"] +# Copy frontend build artifacts into the backend image +FROM backend-builder as application +COPY --from=frontend-builder-production /usr/src/app/build/ /usr/src/app/front_end/build-production/ +COPY --from=frontend-builder-staging /usr/src/app/build/ /usr/src/app/front_end/build-staging/ diff --git a/Makefile b/Makefile index bf0dc12..50f8e27 100644 --- a/Makefile +++ b/Makefile @@ -1,27 +1,25 @@ docker-build: docker buildx build \ --build-arg BASE_LAYER=base-dev \ - -t llm-gateway-backend . - docker buildx build \ - -t llm-gateway-frontend ./front_end + -t llm-gateway . docker-test: docker buildx build \ --build-arg BASE_LAYER=base-dev \ - --target test-suite \ + --target backend-test-suite \ -t llm-gateway-test . docker run -ti llm-gateway-test pytest $(TEST_OPTIONS) -docker-run: - docker-compose -p llm-gateway -f docker-compose.yml up --detach - browse: open http://localhost:3000 browse-api: open http://localhost:5000/api/docs -clean: +up: docker-build + docker-compose -p llm-gateway -f docker-compose.yml up --detach + +down: docker-compose -p llm-gateway -f docker-compose.yml down docker-compose rm # force postgres to execute docker-entrypoint-initdb.d/init.sql diff --git a/README.md b/README.md index c480fd6..8630206 100644 --- a/README.md +++ b/README.md @@ -87,11 +87,8 @@ poetry run pre-commit install To run in Docker: ``` -# build docker image -make docker-build - # spin up docker-compose -make docker-run +make up # open frontend in browser make browse @@ -100,5 +97,5 @@ make browse make browse-api # delete docker-compose setup -make clean +make down ``` diff --git a/SECURITY.md b/SECURITY.md index ed64509..1a679ee 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,4 +12,4 @@ If you have a vulnerability to report about the Wealthsimple platform, please us ### General Support -For additional support, please open a [Github Discussion](https://github.com/wealthsimple/llm-gateway/discussions). +For additional support, please open a [Github Discussion](https://github.com/wealthsimple/llm-gateway/discussions). diff --git a/catalog-info.yaml b/catalog-info.yaml index f94d0f5..2ca65a8 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -5,7 +5,7 @@ metadata: description: Gateway for secure & reliable communications with OpenAI and other LLM providers tags: - machine-learning - - open-source + - open-source - python spec: type: website diff --git a/docker-compose.yml b/docker-compose.yml index 0bce055..6301a1a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,10 @@ services: retries: 5 migration: - image: llm-gateway-backend + build: + context: . + dockerfile: Dockerfile + target: backend-builder env_file: - .envrc command: alembic upgrade head @@ -26,17 +29,12 @@ services: condition: service_healthy llm_gateway_backend: - image: llm-gateway-backend + build: + context: . + dockerfile: Dockerfile + target: backend-builder container_name: llm-gateway-backend - command: [ - "uvicorn", - "--host", - "0.0.0.0", - "--port", - "5000", - "llm_gateway.app:app", - "--reload" - ] + command: uvicorn --host 0.0.0.0 --port 5000 llm_gateway.app:app --reload env_file: - .envrc ports: @@ -56,12 +54,12 @@ services: - ./llm_gateway:/usr/src/app/llm_gateway llm_gateway_frontend: - image: llm-gateway-frontend + build: + context: . + dockerfile: Dockerfile + target: frontend-pre-builder container_name: llm-gateway-frontend - command: [ - "yarn", - "start" - ] + command: yarn start ports: - "3000:3000" volumes: diff --git a/front_end/.dockerignore b/front_end/.dockerignore deleted file mode 100644 index c2658d7..0000000 --- a/front_end/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -node_modules/ diff --git a/front_end/.env-cmdrc b/front_end/.env-cmdrc new file mode 100644 index 0000000..ee7495b --- /dev/null +++ b/front_end/.env-cmdrc @@ -0,0 +1,11 @@ +{ + "development": { + "REACT_APP_ENV": "development" + }, + "staging": { + "REACT_APP_ENV": "staging" + }, + "production": { + "REACT_APP_ENV": "production" + } +} diff --git a/front_end/Dockerfile b/front_end/Dockerfile deleted file mode 100644 index c59d5a2..0000000 --- a/front_end/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM node:16.13.1 -LABEL maintainer="Data Science & Engineering @ Wealthsimple " - -WORKDIR /usr/src/app -COPY package.json yarn.lock .eslintrc.json tsconfig.json ./ -RUN yarn install diff --git a/front_end/README.md b/front_end/README.md index 80415ca..e0b078f 100644 --- a/front_end/README.md +++ b/front_end/README.md @@ -1,10 +1,5 @@ # LLM Gateway Front-end -### 🐳 Development with Docker - -The guide to development with Docker is located in the main -[README.md](../README.md). - ### 🚫🐳 Development without Docker 1. Install [fnm](https://github.com/Schniz/fnm) or diff --git a/front_end/package.json b/front_end/package.json index 4f741f1..e771116 100644 --- a/front_end/package.json +++ b/front_end/package.json @@ -13,6 +13,7 @@ "@types/react": "^18.2.13", "@types/react-dom": "^18.2.6", "axios": "^1.4.0", + "env-cmd": "^10.1.0", "highlight.js": "^11.8.0", "markdown-to-jsx": "^7.2.1", "nth-check": "^2.0.1", @@ -24,7 +25,9 @@ }, "scripts": { "start": "react-scripts start", - "build": "react-scripts build", + "build": "node_modules/.bin/env-cmd -e development react-scripts build", + "build-staging": "node_modules/.bin/env-cmd -e staging react-scripts build", + "build-production": "node_modules/.bin/env-cmd -e production react-scripts build", "lint": "eslint .", "test": "react-scripts test --watchAll=false", "prettier": "prettier --write .", diff --git a/front_end/public/index.html b/front_end/public/index.html index f22e966..84a9cc6 100644 --- a/front_end/public/index.html +++ b/front_end/public/index.html @@ -1,10 +1,9 @@ - + LLM Gateway - @@ -12,4 +11,6 @@
+ + diff --git a/front_end/public/manifest.json b/front_end/public/manifest.json index 080d6c7..5bbdceb 100644 --- a/front_end/public/manifest.json +++ b/front_end/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "React App", - "name": "Create React App Sample", + "short_name": "LLMGateway", + "name": "Framework for safe and secure LLM interactions", "icons": [ { "src": "favicon.ico", diff --git a/front_end/public/styles.css b/front_end/public/styles.css index 4ae34d1..cbf4725 100644 --- a/front_end/public/styles.css +++ b/front_end/public/styles.css @@ -1,86 +1,324 @@ -/* You can add global styles to this file, and also import other style files */ +/* Referenced from Wealthsimple's style guide */ +:root { + --slate-element-bg: #1c1c1b; + --soft-bg: #f1f0f0; + --bg-soft: #f4eff5; + --fg-strong: #804b83; + --sky-bg-soft: #ecf2f6; + --sky-fg-strong: #456274; + --positive-bg-strong: #486635; + --positive-bg-soft: #e9f5df; + --negative-fg-strong: #a43d12; + --outline: #e4e2e1; +} -#input-box { - resize: vertical; +.container { + min-width: 50%; +} + +.primary-btn { + background-color: var(--sky-fg-strong); + border: none; + border-radius: 8px; + color: white; + font-size: 16px; + font-weight: 500; + padding: 12px; +} + +.secondary-btn { + background-color: white; + color: var(--sky-fg-strong); + border: 1.5px solid var(--sky-fg-strong); + border-radius: 8px; + font-size: 16px; + font-weight: 500; + padding: 12px; +} + +/* Styling for main app container */ + +.container .title { + font-size: 32px; + font-weight: 700; + color: var(--slate-element-bg); + margin: 0; +} + +.waiting { + cursor: wait !important; } -#send-button { - width: 90%; - background-color: #486635; - color: #e9f5df; +.cursor-disable { + pointer-events: none; } -#send-buttons-div { +.container article.appDescription { + box-shadow: none; + margin: 20px 0px 32px 0px; + padding: 0; +} + +.appDescription p { + color: var(--slate-element-bg); + font-size: 18px; +} + +.appDescription a { + color: var(--fg-strong); + font-weight: 600; + text-decoration: underline; +} + +.container .chat-container { display: flex; + flex-direction: row; + justify-content: space-around; + border-style: solid; + border-color: var(--outline); + border-radius: 12px; + border-width: 1.5px; +} + +.chat-container .main-chat-container { + flex-grow: 2; + display: flex; + flex-direction: column; + width: fit-content; + min-height: 100%; +} + +/* Styling for chat nav bar */ +.multichat-nav-bar { + border-width: 1.5px; + border-right-style: solid; + border-right-color: var(--outline); + width: 24%; +} + +.multichat-nav-bar-container { + display: flex; + flex-direction: column; + padding: 0; + margin: 0; + overflow-y: auto; +} + +.multichat-nav-bar-container article.title { + margin: 0; + box-shadow: none; + padding: 12px 12px; + border-radius: 12px 12px 0 0; +} + +.multichat-nav-bar-container article.new-chat-button-div { + padding: 12px 12px; + box-shadow: none; + border-bottom: 1px solid var(--outline); + border-radius: 12px 12px 0 0; + margin: 0; +} + +.new-chat-button-div body { + white-space: nowrap; + color: white; + text-align: center; +} + +.new-chat-button-div button { + margin: 0; + padding: 8px; + max-width: 100%; +} + +.multichat-nav-bar-container .chat-selected { + background-color: var(--sky-bg-soft); +} + +.chat-wrap { + margin: 0; + height: 64px; + box-shadow: none; + border-bottom: 1px solid var(--outline); + border-radius: 0; + display: flex; + flex-direction: row; justify-content: space-between; + gap: 12px; + padding: 16px; +} + +.chat-wrap .nav-item-label { + white-space: nowrap; + overflow: hidden; + font-size: 16px; + text-overflow: ellipsis; + padding: 0; + flex-grow: 2; + display: flex; + align-items: center; +} + +.nav-item-label .editing-item-label { + height: auto; + margin: 0; + padding: 0 4px; + font-size: 16px; + border-color: var(--sky-fg-strong); + border-radius: 4px; + background-color: white; +} + +.chat-wrap .nav-item-icons { + display: flex; + flex-direction: row; + justify-content: flex-end; + gap: 4px; +} + +.chat-wrap .nav-item-icons > div { + cursor: pointer; +} + +/* Styling for Model Settings */ +.main-chat-container .model-banner { + background-color: var(--soft-bg); + color: var(--slate-element-bg); + display: flex; + flex-direction: row; + justify-content: space-around; + border-radius: 4px 4px 0px 0px; + box-shadow: none; + margin: 0 0 12px 0; + text-align: center; + font-weight: 600; + padding: 8px; +} + +.model-banner .model-name { + flex-grow: 2; + text-align: center; +} + +.model-banner .clear-chat { + float: right; + margin-right: 10px; +} + +/* Styling for Chat Box */ +.chatbox { + flex-grow: 4; + display: flex; + flex-direction: column; + max-width: 100%; +} + +.chatbox #chat-action-buttons-group { + display: flex; + flex-wrap: nowrap; + justify-content: flex-end; + align-items: flex-start; + gap: 12px; + padding: 0 40px; +} + +#chat-action-buttons-group .input-box-div { + flex-grow: 4; + margin: 0; +} + +.input-box-div #input-box { + resize: vertical; + margin: 0; + max-height: 200px; + border: 1.5px solid var(--outline); + padding: 12px; + font-size: 16px; } #error-message { - color: #a34d12; - font-weight: bold; + color: var(--negative-fg-strong); + font-weight: 400; + margin: 0; } -#message-history-border { - border: 1px solid #808080; - border-radius: 5px; - overflow: hidden; +.chatbox .helpful-tip { + color: var(--slate-element-bg); + font-weight: 400; + font-size: 14px; + margin: 0; + padding: 0 40px 16px 40px; + text-align: center; +} + +.message-history-container { + border-radius: 0px 0px 4px 4px; + overflow-y: auto; + flex-grow: 4; + margin: 0 0 12px 0; + padding: 0 40px; + max-width: 100%; } -#message-history { - overflow-y: scroll; - max-height: 60vh; - min-height: 30vh; +.message-history-container .message-history { + padding: 0px; + height: 50vh; + max-width: 100%; } -.message { - border-radius: 15px; - padding-left: 15px; - padding-right: 15px; - padding-top: 10px; - padding-bottom: 10px; +.message-history ul { + margin: 0px; + padding: 0; +} + +.message-history .message { + border-radius: 16px; + padding: 16px; max-width: 85%; width: fit-content; list-style-type: none; white-space: pre-wrap; } -.message-user { - border: 2px #ecf3d5; +.message-history .message-user { border-top-right-radius: 0; margin-left: auto; - margin-right: 16px; - background-color: #ecf3d5; + margin-bottom: 8px; + background-color: #ecf2f6; color: #32302f; } -.message-assistant { - border: 2px #f1f0f0; +.message-history .message-assistant { + border: 2px var(--soft-bg); border-top-left-radius: 0; margin-right: auto; - margin-left: 8px; - background-color: #f1f0f0; + margin-bottom: 8px; + background-color: var(--soft-bg); + max-width: 80%; } -.message p, li { - color: #1c1b1b; +.message-history .message p, +li { + color: var(--slate-element-bg); + font-size: 18px; } /* For ol (ordered lists), make sure to retain numbers. Otherwise it defaults to square bullet points: */ -.message ol > li { +.message-history .message ol > li { list-style-type: number; } /* Make browser highlight color more visible against dark mode colors: */ -.message code span::selection { +.message-history .message code span::selection { background: #4877a5; } -.message code { +.message-history .message code { padding: 0.1rem 0.5rem; } -.code-header { +.markdown-code-header { padding: 0.1rem 0.5rem; display: flex; justify-content: space-between; @@ -92,34 +330,38 @@ cursor: pointer; } -.confirmation-modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; +.code-block { + max-width: 90%; +} + +/* styling for empty state screen */ +.empty-state { + min-height: 400px; + text-align: center; + padding: 20px; display: flex; + flex-direction: column; justify-content: center; - align-items: center; - background-color: rgba(0, 0, 0, 0.5); } -.confirmation-content { - padding: 20px; - border-radius: 5px; - text-align: center; +.empty-state .illustration { + margin: 20px; } -.confirmation-buttons { - margin-top: 20px; +.empty-state h1 { + font-size: 28px; + margin-bottom: 8px; + font-weight: 600; } -.green-button { - background-color: #486635; - color: white; +.empty-state h3 { + font-size: 20px; + font-weight: 400; + margin-bottom: 12px; + color: var(--sky-fg-strong); } -.red-button { - background-color: #a43d12; - color: white; +.footer { + color: var(--slate-element-bg); + font-size: 16px; } diff --git a/front_end/src/app/app.tsx b/front_end/src/app/app.tsx index febbbba..743b443 100644 --- a/front_end/src/app/app.tsx +++ b/front_end/src/app/app.tsx @@ -17,49 +17,199 @@ // limitations under the License. // ***************************************************************************** -import React, { useState } from 'react'; -import { modelChoices, DEFAULT_MODEL_TEMP, APP_DESCRIPTION} from '../constants'; +import React, { useEffect, useState } from 'react'; +import { + modelChoices, + DEFAULT_MODEL_TEMP, + APP_DESCRIPTION, + APP_FOOTER, + DEFAULT_SETTINGS, + CONVERSATION_KEY, + DEFAULT_CONVERSATIONS_STATE, +} from '../constants'; import { ChatBoxComponent } from './components/ChatBox'; import { ModelSettingsDialog } from './components/SettingsDialog'; +import { + type Conversation, + type ConversationsState, + type Message, +} from './interfaces'; +import { ChatNavBarComponent } from './components/ChatNavBar'; +import { EmptyChatBoxComponent } from './components/EmptyChatBox'; +import { ModelBannerComponent } from './components/ModelBanner'; + +// Key actions for the gateway app. We should put it in the highest level component. +const saveConversationsStateToLocalStorage = ( + conversationsState: ConversationsState, +) => { + const conversationJson = JSON.stringify(conversationsState); + localStorage.setItem(CONVERSATION_KEY, conversationJson); +}; + +const loadConversationsStateFromLocalStorage = (): ConversationsState => { + const conversationJson = localStorage.getItem(CONVERSATION_KEY); + if (conversationJson) { + return JSON.parse(conversationJson); + } + return DEFAULT_CONVERSATIONS_STATE; +}; export function App(): JSX.Element { - const allModelsKeys = Object.keys(modelChoices); - const [model, setModel] = useState(allModelsKeys[0]); + const [conversationState, setConversationState] = + useState(loadConversationsStateFromLocalStorage()); + + const getConversationById = ( + state: ConversationsState, + id: number | null, + ) => { + return state.conversations.find((convo) => convo.id === id); + }; + + const [currentSelectedId, setCurrentSelectedId] = useState( + conversationState.selectedConversationId, + ); + const [currentMessages, setCurrentMessages] = useState( + getConversationById(conversationState, currentSelectedId)?.messages || [], + ); + const [currentModel, setCurrentModel] = useState( + getConversationById(conversationState, currentSelectedId)?.model || + DEFAULT_SETTINGS.model, + ); + const [currentChatTitle, setCurrentChatTitle] = useState( + getConversationById(conversationState, currentSelectedId)?.title || '', + ); + const [isLoadingReply, setIsLoadingReply] = useState(false); + + useEffect(() => { + const newConversationState = { ...conversationState }; + const conversationIdx = newConversationState.conversations.findIndex( + (c) => c.id === currentSelectedId, + ); + if (conversationIdx === -1) { + return; + } + newConversationState.conversations[conversationIdx].messages = + currentMessages; + newConversationState.conversations[conversationIdx].model = currentModel; + newConversationState.conversations[conversationIdx].title = + currentChatTitle; + setConversationState(newConversationState); + }, [currentMessages, currentModel, currentChatTitle]); + + useEffect(() => { + saveConversationsStateToLocalStorage(conversationState); + }, [conversationState]); + + useEffect(() => { + const newConversationState = { ...conversationState }; + newConversationState.selectedConversationId = currentSelectedId; + const currentConversation = getConversationById( + newConversationState, + currentSelectedId, + ); + setCurrentMessages(currentConversation?.messages || []); + setCurrentModel(currentConversation?.model || DEFAULT_SETTINGS.model); + setCurrentChatTitle(currentConversation?.title || ''); + setConversationState(newConversationState); + }, [currentSelectedId]); + + const updateChatTitle = (id: number, title: string) => { + const newConversationState = { ...conversationState }; + const conversationIdx = newConversationState.conversations.findIndex( + (c) => c.id === id, + ); + newConversationState.conversations[conversationIdx].title = title; + setConversationState(newConversationState); + }; + + const addConversation = (title: string, model: string) => { + const newConversationState = { ...conversationState }; + const newConversation: Conversation = { + id: Date.now(), + title: title, + model: model, + messages: modelChoices[model].initialPrompt, + }; + newConversationState.conversations.push(newConversation); + setConversationState(newConversationState); + setCurrentSelectedId(newConversation.id); + }; + + const deleteConversation = (id: number | null) => { + const newConversationState = { ...conversationState }; + newConversationState.conversations = + newConversationState.conversations.filter((convo) => convo.id !== id); + if (newConversationState.conversations.length > 0) { + setCurrentSelectedId(newConversationState.conversations[0].id); + } else { + setCurrentSelectedId(null); + } + setConversationState(newConversationState); + }; + + const clearCurrentConversation = () => { + if (currentSelectedId !== null) { + const newCurrentMessages = modelChoices[currentModel].initialPrompt; + setCurrentMessages(newCurrentMessages); + } + }; + const temperature = DEFAULT_MODEL_TEMP; const description = APP_DESCRIPTION; + const appFooter = APP_FOOTER; + const [showSettings, setShowSettings] = useState(false); return ( <> -
-
-

LLM Gateway

-
- -

{description}

+ {/* Note: To set cursor animation and disable cursor at the same time, + set cursor to waiting in the parent component first + then disable the cursor in the child component */} +
+
+ {description} +
- +
+ +
+ {currentSelectedId !== null ? ( + <> + + + + ) : ( + + )} +
+


- -
- -

- Made by Wealthsimple with{' '} - - ❤️ - -

+
{appFooter}
); diff --git a/front_end/src/app/components/ChatBox/ChatBox.tsx b/front_end/src/app/components/ChatBox/ChatBox.tsx index 3678e5d..b3c2291 100644 --- a/front_end/src/app/components/ChatBox/ChatBox.tsx +++ b/front_end/src/app/components/ChatBox/ChatBox.tsx @@ -17,18 +17,18 @@ // limitations under the License. // ***************************************************************************** -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { fetchResponseFromModel } from '../../../services/apiService'; -import { CONVERSATION_KEY, DIALOGUE_DEFAULT_MESSAGE } from '../../../constants'; -import { Message, Role } from '../../interfaces'; -import { SendButtonComponent } from './SendButton'; -import { ClearButton } from './ClearButton'; +import { type Message, Role, type IRequestBody } from '../../interfaces'; import { MessageHistoryComponent } from './MessageHistory'; interface ChatBoxProps { + messages: Message[]; + setMessages: (arg: Message[]) => void; modelName: string; modelTemperature: number; - setShowSettings: (arg: boolean) => void; + isModelLoadingReply: boolean; + setIsModelLoadingReply: (arg: boolean) => void; } const sendMessage = ( @@ -60,7 +60,12 @@ const sendMessage = ( // add user's message to history const newMessageHistory = [...messages, message]; setMessages(newMessageHistory); - fetchResponseFromModel(model, newMessageHistory, temperature) + const apiRequestBody: IRequestBody = { + messages: newMessageHistory, + model: model, + temperature: temperature, + }; + fetchResponseFromModel(apiRequestBody) .then((resContent) => { setMessages([ ...newMessageHistory, @@ -68,7 +73,7 @@ const sendMessage = ( ]); }) .catch((err) => { - console.log('error'); + console.log(err); setErrMsg(err.message); }) .finally(() => { @@ -76,35 +81,15 @@ const sendMessage = ( setIsLoadingReply(false); }); }; -const saveConversation = (conversation: Message[]) => { - const conversationJson = JSON.stringify(conversation); - localStorage.setItem(CONVERSATION_KEY, conversationJson); -}; -const loadConversation = (): Message[] => { - const conversationJson = localStorage.getItem(CONVERSATION_KEY); - if (conversationJson) { - return JSON.parse(conversationJson); - } - return DIALOGUE_DEFAULT_MESSAGE; -}; export const ChatBoxComponent: React.FC = ({ + messages, + setMessages, modelName, modelTemperature, - setShowSettings, + isModelLoadingReply, + setIsModelLoadingReply, }) => { - const [messages, setMessages] = useState(loadConversation()); - - useEffect(() => { - saveConversation(messages); - }, [messages]); - - const clearMessages = () => { - setMessages(loadConversation); - }; - - const [isModelLoadingReply, setIsModelLoadingReply] = - useState(false); const [readyToSendMessage, setReadyToSendMessage] = useState(true); const [errMsg, setErrMsg] = useState(''); const [inputVal, setInputVal] = useState(''); @@ -124,43 +109,50 @@ export const ChatBoxComponent: React.FC = ({ ); return ( -
+
{/* slice(1) to remove initial assistant message */} -