Compare commits

..

5 Commits

Author SHA1 Message Date
bbd5cb3783 Refactored VectorVisualization
Added LoadingPage
2025-05-15 04:06:00 -07:00
5dc5e8415c Routing works from fresh load 2025-05-14 17:40:08 -07:00
9f7ddca90a Routing refactored 2025-05-14 16:17:27 -07:00
e044f9c639 Reformatted all content to black 2025-05-14 11:31:31 -07:00
a1798b58ac Switching to one-call per skill 2025-05-12 16:57:20 -07:00
144 changed files with 4955 additions and 4327 deletions

View File

@ -1,74 +1,28 @@
# #
# Build Pyton 3.11 for use in later stages # Build Pyton 3.11 for use in later stages
# #
FROM ubuntu:oracular AS python-build FROM ubuntu:oracular AS python
SHELL [ "/bin/bash", "-c" ] SHELL [ "/bin/bash", "-c" ]
# Instructions Dockerfied from: # Install some utilities frequently used
#
# https://github.com/pytorch/pytorch
#
# and
#
# https://pytorch.org/docs/stable/notes/get_start_xpu.html
# https://www.intel.com/content/www/us/en/developer/articles/tool/pytorch-prerequisites-for-intel-gpu/2-6.html
#
#
RUN apt-get update \ RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y \ && DEBIAN_FRONTEND=noninteractive apt-get install -y \
gpg \ gpg \
wget \ wget \
nano \
rsync \
jq \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log} && rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log}
# Install latest Python3
# ipex only supports python 3.11, so use 3.11 instead of latest oracular (3.12)
RUN apt-get update \ RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y \ && DEBIAN_FRONTEND=noninteractive apt-get install -y \
build-essential \ python3 \
ca-certificates \ python3-pip \
ccache \ python3-venv \
cmake \ python3-dev
curl \
git \
gpg-agent \
less \
libbz2-dev \
libffi-dev \
libjpeg-dev \
libpng-dev \
libreadline-dev \
libssl-dev \
libsqlite3-dev \
llvm \
nano \
wget \
zlib1g-dev \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log}
# python3 \
# python3-pip \
# python3-venv \
# python3-dev \
RUN /usr/sbin/update-ccache-symlinks
RUN mkdir /opt/ccache && ccache --set-config=cache_dir=/opt/ccache
# Build Python in /opt/..., install it locally, then remove the build environment
# collapsed to a single docker layer.
WORKDIR /opt
ENV PYTHON_VERSION=3.11.9
RUN wget -q -O - https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz | tar -xz \
&& cd Python-${PYTHON_VERSION} \
&& ./configure --prefix=/opt/python --enable-optimizations \
&& make -j$(nproc) \
&& make install \
&& cd /opt \
&& rm -rf Python-${PYTHON_VERSION}
FROM ubuntu:oracular AS ze-monitor FROM ubuntu:oracular AS ze-monitor
# From https://github.com/jketreno/ze-monitor # From https://github.com/jketreno/ze-monitor
@ -101,67 +55,13 @@ RUN cmake .. \
&& make \ && make \
&& cpack && cpack
#
# Build the ipex-llm wheel for use in later stages
#
FROM python-build AS ipex-llm-src
RUN update-alternatives --install /usr/bin/python3 python3 /opt/python/bin/python3.11 2
RUN git clone --branch main --depth 1 https://github.com/intel/ipex-llm.git /opt/ipex-llm \
&& cd /opt/ipex-llm \
&& git fetch --depth 1 origin cb3c4b26ad058c156591816aa37eec4acfcbf765 \
&& git checkout cb3c4b26ad058c156591816aa37eec4acfcbf765
WORKDIR /opt/ipex-llm
RUN python3 -m venv --system-site-packages /opt/ipex-llm/venv
RUN { \
echo '#!/bin/bash' ; \
echo 'update-alternatives --set python3 /opt/python/bin/python3.11' ; \
echo 'source /opt/ipex-llm/venv/bin/activate' ; \
echo 'if [[ "${1}" != "" ]]; then bash -c "${@}"; else bash; fi' ; \
} > /opt/ipex-llm/shell ; \
chmod +x /opt/ipex-llm/shell
SHELL [ "/opt/ipex-llm/shell" ]
RUN pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/xpu
WORKDIR /opt/ipex-llm/python/llm
RUN pip install requests wheel
RUN python setup.py clean --all bdist_wheel --linux
#
# The main backstory image: # The main backstory image:
# * python 3.11 # * python 3.11
# * pytorch xpu w/ ipex-llm # * pytorch xpu w/ ipex-llm
# * ollama-ipex-llm # * ollama-ipex-llm
# * src/server.py - model server supporting RAG and fine-tuned models # * src/server.py - model server supporting RAG and fine-tuned models
# #
FROM ubuntu:oracular AS llm-base FROM python AS llm-base
COPY --from=python-build /opt/python /opt/python
# Get a couple prerequisites
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y \
gpg \
# python3 \
# python3-pip \
# python3-venv \
wget \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log}
# The client frontend is built using React Expo to allow
# easy creation of an Android app as well as web app
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y \
nodejs \
npm \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log}
# Install Intel graphics runtimes # Install Intel graphics runtimes
RUN apt-get update \ RUN apt-get update \
@ -177,13 +77,10 @@ RUN apt-get update \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log} && rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log}
RUN update-alternatives --install /usr/bin/python3 python3 /opt/python/bin/python3.11 2 # Prerequisite for ze-monitor
RUN apt-get update \ RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y \ && DEBIAN_FRONTEND=noninteractive apt-get install -y \
libncurses6 \ libncurses6 \
rsync \
jq \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log} && rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log}
@ -199,7 +96,6 @@ RUN python3 -m venv --system-site-packages /opt/backstory/venv
# Setup the docker pip shell # Setup the docker pip shell
RUN { \ RUN { \
echo '#!/bin/bash' ; \ echo '#!/bin/bash' ; \
echo 'update-alternatives --set python3 /opt/python/bin/python3.11' ; \
echo 'if [[ -e /opt/intel/oneapi/setvars.sh ]]; then source /opt/intel/oneapi/setvars.sh; fi' ; \ echo 'if [[ -e /opt/intel/oneapi/setvars.sh ]]; then source /opt/intel/oneapi/setvars.sh; fi' ; \
echo 'source /opt/backstory/venv/bin/activate' ; \ echo 'source /opt/backstory/venv/bin/activate' ; \
echo 'if [[ "${1}" != "" ]]; then bash -c "${@}"; else bash; fi' ; \ echo 'if [[ "${1}" != "" ]]; then bash -c "${@}"; else bash; fi' ; \
@ -211,12 +107,12 @@ SHELL [ "/opt/backstory/shell" ]
# From https://pytorch-extension.intel.com/installation?platform=gpu&version=v2.6.10%2Bxpu&os=linux%2Fwsl2&package=pip # From https://pytorch-extension.intel.com/installation?platform=gpu&version=v2.6.10%2Bxpu&os=linux%2Fwsl2&package=pip
RUN pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/xpu RUN pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/xpu
RUN pip install intel-extension-for-pytorch==2.6.10+xpu oneccl_bind_pt==2.6.0+xpu --extra-index-url https://pytorch-extension.intel.com/release-whl/stable/xpu/us/ #RUN pip install intel-extension-for-pytorch==2.6.10+xpu oneccl_bind_pt==2.6.0+xpu --extra-index-url https://pytorch-extension.intel.com/release-whl/stable/xpu/us/
# From https://huggingface.co/docs/bitsandbytes/main/en/installation?backend=Intel+CPU+%2B+GPU#multi-backend # From https://huggingface.co/docs/bitsandbytes/main/en/installation?backend=Intel+CPU+%2B+GPU#multi-backend
RUN pip install "transformers>=4.45.1" RUN pip install "transformers>=4.45.1"
RUN pip install 'https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_multi-backend-refactor/bitsandbytes-0.44.1.dev0-py3-none-manylinux_2_24_x86_64.whl' #RUN pip install 'https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_multi-backend-refactor/bitsandbytes-0.44.1.dev0-py3-none-manylinux_2_24_x86_64.whl'
RUN pip install 'https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_multi-backend-refactor/bitsandbytes-0.45.3.dev272-py3-none-manylinux_2_24_x86_64.whl'
# Install ollama python module # Install ollama python module
RUN pip install ollama langchain-ollama RUN pip install ollama langchain-ollama
@ -227,8 +123,8 @@ RUN pip install tiktoken
RUN pip install flask flask_cors flask_sock RUN pip install flask flask_cors flask_sock
RUN pip install peft datasets RUN pip install peft datasets
COPY --from=ipex-llm-src /opt/ipex-llm/python/llm/dist/*.whl /opt/wheels/ #COPY --from=ipex-llm-src /opt/ipex-llm/python/llm/dist/*.whl /opt/wheels/
RUN for pkg in /opt/wheels/ipex_llm*.whl; do pip install $pkg; done #RUN for pkg in /opt/wheels/ipex_llm*.whl; do pip install $pkg; done
# mistral fails with cache_position errors with transformers>4.40 (or at least it fails with the latest) # mistral fails with cache_position errors with transformers>4.40 (or at least it fails with the latest)
# as well as MistralSpda* and QwenSpda* things missing (needed when loading models with ) # as well as MistralSpda* and QwenSpda* things missing (needed when loading models with )
@ -245,18 +141,22 @@ RUN pip install "sentence_transformers<3.4.1"
RUN pip3 install 'bigdl-core-xe-all>=2.6.0b' RUN pip3 install 'bigdl-core-xe-all>=2.6.0b'
# NOTE: IPEX includes the oneAPI components... not sure if they still need to be installed separately with a oneAPI env # NOTE: IPEX includes the oneAPI components... not sure if they still need to be installed separately with a oneAPI env
RUN pip install einops diffusers # Required for IPEX optimize(), which is required to convert from Params4bit # Required for IPEX optimize(), which is required to convert from Params4bit
RUN pip install einops diffusers
# Needed by src/utils/chroma.py # Needed by src/utils/rag.py
RUN pip install watchdog RUN pip install watchdog
# Install packages needed for stock.py # Install packages needed for utils/tools/*
RUN pip install yfinance pyzt geopy PyHyphen nltk RUN pip install yfinance pyzt geopy
# Install packages needed for vector operations
RUN pip install umap-learn
FROM llm-base AS backstory FROM llm-base AS backstory
COPY /src/requirements.txt /opt/backstory/src/requirements.txt #COPY /src/requirements.txt /opt/backstory/src/requirements.txt
RUN pip install -r /opt/backstory/src/requirements.txt #RUN pip install -r /opt/backstory/src/requirements.txt
RUN pip install 'markitdown[all]' pydantic RUN pip install 'markitdown[all]' pydantic
# Prometheus # Prometheus
@ -269,7 +169,6 @@ RUN { \
echo 'echo "Container: backstory"'; \ echo 'echo "Container: backstory"'; \
echo 'set -e'; \ echo 'set -e'; \
echo 'echo "Setting pip environment to /opt/backstory"'; \ echo 'echo "Setting pip environment to /opt/backstory"'; \
echo 'update-alternatives --set python3 /opt/python/bin/python3.11' ; \
echo 'if [[ -e /opt/intel/oneapi/setvars.sh ]]; then source /opt/intel/oneapi/setvars.sh; fi' ; \ echo 'if [[ -e /opt/intel/oneapi/setvars.sh ]]; then source /opt/intel/oneapi/setvars.sh; fi' ; \
echo 'source /opt/backstory/venv/bin/activate'; \ echo 'source /opt/backstory/venv/bin/activate'; \
echo ''; \ echo ''; \
@ -283,14 +182,6 @@ RUN { \
echo ' exec /bin/bash'; \ echo ' exec /bin/bash'; \
echo ' fi' ; \ echo ' fi' ; \
echo 'else'; \ echo 'else'; \
echo ' if [[ "${PRODUCTION}" -eq 0 ]]; then'; \
echo ' while true; do'; \
echo ' cd /opt/backstory/frontend'; \
echo ' echo "Launching Backstory React Frontend..."'; \
echo ' npm start "${@}" || echo "Backstory frontend died. Restarting in 3 seconds."'; \
echo ' sleep 3'; \
echo ' done &' ; \
echo ' fi' ; \
echo ' if [[ ! -e src/cert.pem ]]; then' ; \ echo ' if [[ ! -e src/cert.pem ]]; then' ; \
echo ' echo "Generating self-signed certificate for HTTPS"'; \ echo ' echo "Generating self-signed certificate for HTTPS"'; \
echo ' openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout src/key.pem -out src/cert.pem -subj "/C=US/ST=OR/L=Portland/O=Development/CN=localhost"'; \ echo ' openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout src/key.pem -out src/cert.pem -subj "/C=US/ST=OR/L=Portland/O=Development/CN=localhost"'; \
@ -320,11 +211,6 @@ ENV SYCL_CACHE_PERSISTENT=1
ENV PATH=/opt/backstory:$PATH ENV PATH=/opt/backstory:$PATH
COPY /src/ /opt/backstory/src/ COPY /src/ /opt/backstory/src/
COPY /frontend/ /opt/backstory/frontend/
WORKDIR /opt/backstory/frontend
RUN npm install --force
WORKDIR /opt/backstory
ENTRYPOINT [ "/entrypoint.sh" ] ENTRYPOINT [ "/entrypoint.sh" ]
@ -381,7 +267,6 @@ RUN python3 -m venv --system-site-packages /opt/ollama/venv
# Setup the docker pip shell # Setup the docker pip shell
RUN { \ RUN { \
echo '#!/bin/bash' ; \ echo '#!/bin/bash' ; \
update-alternatives --set python3 /opt/python/bin/python3.11 ; \
echo 'source /opt/ollama/venv/bin/activate' ; \ echo 'source /opt/ollama/venv/bin/activate' ; \
echo 'if [[ "${1}" != "" ]]; then bash -c ${*}; else bash; fi' ; \ echo 'if [[ "${1}" != "" ]]; then bash -c ${*}; else bash; fi' ; \
} > /opt/ollama/shell ; \ } > /opt/ollama/shell ; \
@ -454,6 +339,14 @@ ENTRYPOINT [ "/entrypoint.sh" ]
FROM llm-base AS jupyter FROM llm-base AS jupyter
# npm and Node.JS are required for jupyterlab
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y \
nodejs \
npm \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log}
SHELL [ "/opt/backstory/shell" ] SHELL [ "/opt/backstory/shell" ]
# BEGIN setup Jupyter # BEGIN setup Jupyter
@ -463,9 +356,8 @@ RUN pip install \
&& jupyter lab build --dev-build=False --minimize=False && jupyter lab build --dev-build=False --minimize=False
# END setup Jupyter # END setup Jupyter
COPY /src/requirements.txt /opt/backstory/src/requirements.txt #COPY /src/requirements.txt /opt/backstory/src/requirements.txt
#RUN pip install -r /opt/backstory/src/requirements.txt
RUN pip install -r /opt/backstory/src/requirements.txt
RUN pip install timm xformers RUN pip install timm xformers
@ -483,7 +375,6 @@ RUN { \
echo ' echo "${HF_ACCESS_TOKEN}" > /root/.cache/hub/token' ; \ echo ' echo "${HF_ACCESS_TOKEN}" > /root/.cache/hub/token' ; \
echo ' fi' ; \ echo ' fi' ; \
echo 'fi' ; \ echo 'fi' ; \
echo 'update-alternatives --set python3 /opt/python/bin/python3.11' ; \
echo 'if [[ -e /opt/intel/oneapi/setvars.sh ]]; then source /opt/intel/oneapi/setvars.sh; fi' ; \ echo 'if [[ -e /opt/intel/oneapi/setvars.sh ]]; then source /opt/intel/oneapi/setvars.sh; fi' ; \
echo 'source /opt/backstory/venv/bin/activate' ; \ echo 'source /opt/backstory/venv/bin/activate' ; \
echo 'if [[ "${1}" == "shell" ]]; then echo "Dropping to shell"; /bin/bash; exit $?; fi' ; \ echo 'if [[ "${1}" == "shell" ]]; then echo "Dropping to shell"; /bin/bash; exit $?; fi' ; \
@ -510,9 +401,7 @@ RUN { \
ENTRYPOINT [ "/entrypoint-jupyter.sh" ] ENTRYPOINT [ "/entrypoint-jupyter.sh" ]
FROM ubuntu:oracular AS miniircd FROM python AS miniircd
COPY --from=python-build /opt/python /opt/python
# Get a couple prerequisites # Get a couple prerequisites
RUN apt-get update \ RUN apt-get update \
@ -526,15 +415,12 @@ RUN apt-get update \
WORKDIR /opt/miniircd WORKDIR /opt/miniircd
RUN update-alternatives --install /usr/bin/python3 python3 /opt/python/bin/python3.11 2
# Setup the ollama python virtual environment # Setup the ollama python virtual environment
RUN python3 -m venv --system-site-packages /opt/miniircd/venv RUN python3 -m venv --system-site-packages /opt/miniircd/venv
# Setup the docker pip shell # Setup the docker pip shell
RUN { \ RUN { \
echo '#!/bin/bash' ; \ echo '#!/bin/bash' ; \
echo 'update-alternatives --set python3 /opt/python/bin/python3.11' ; \
echo 'source /opt/miniircd/venv/bin/activate' ; \ echo 'source /opt/miniircd/venv/bin/activate' ; \
echo 'if [[ "${1}" != "" ]]; then bash -c "${@}"; else bash; fi' ; \ echo 'if [[ "${1}" != "" ]]; then bash -c "${@}"; else bash; fi' ; \
} > /opt/miniircd/shell ; \ } > /opt/miniircd/shell ; \
@ -552,7 +438,6 @@ RUN { \
echo 'echo "Container: miniircd"'; \ echo 'echo "Container: miniircd"'; \
echo 'set -e'; \ echo 'set -e'; \
echo 'echo "Setting pip environment to /opt/miniircd"'; \ echo 'echo "Setting pip environment to /opt/miniircd"'; \
echo 'update-alternatives --set python3 /opt/python/bin/python3.11' ; \
echo 'source /opt/miniircd/venv/bin/activate'; \ echo 'source /opt/miniircd/venv/bin/activate'; \
echo ''; \ echo ''; \
echo 'if [[ "${1}" == "/bin/bash" ]] || [[ "${1}" =~ ^(/opt/miniircd/)?shell$ ]]; then'; \ echo 'if [[ "${1}" == "/bin/bash" ]] || [[ "${1}" =~ ^(/opt/miniircd/)?shell$ ]]; then'; \
@ -572,3 +457,65 @@ RUN { \
&& chmod +x /entrypoint.sh && chmod +x /entrypoint.sh
ENTRYPOINT [ "/entrypoint.sh" ] ENTRYPOINT [ "/entrypoint.sh" ]
FROM ubuntu:oracular AS frontend
# The client frontend is built using React Expo to allow
# easy creation of an Android app as well as web app
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y \
nodejs \
npm \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log}
SHELL [ "/bin/bash", "-c" ]
RUN { \
echo '#!/bin/bash'; \
echo 'echo "Container: frontend"'; \
echo 'set -e'; \
echo ''; \
echo 'if [[ "${1}" == "/bin/bash" ]] || [[ "${1}" =~ ^(/opt/backstory/)?shell$ ]]; then'; \
echo ' echo "Dropping to shell"'; \
echo ' shift' ; \
echo ' echo "Running: ${@}"' ; \
echo ' if [[ "${1}" != "" ]]; then' ; \
echo ' exec ${@}'; \
echo ' else' ; \
echo ' exec /bin/bash'; \
echo ' fi' ; \
echo 'fi' ; \
echo 'cd /opt/backstory/frontend'; \
echo 'if [[ "${1}" == "install" ]] || [[ ! -d node_modules ]]; then'; \
echo ' echo "Installing node modules"'; \
echo ' if [[ -d node_modules ]]; then'; \
echo ' echo "Deleting current node_modules"'; \
echo ' rm -rf node_modules'; \
echo ' fi'; \
echo ' npm install --force'; \
echo 'fi'; \
echo 'if [[ "${1}" == "build" ]]; then'; \
echo ' echo "Building production static build"'; \
echo ' ./build.sh'; \
echo 'fi'; \
echo 'while true; do'; \
echo ' echo "Launching Backstory React Frontend..."'; \
echo ' npm start "${@}" || echo "Backstory frontend died. Restarting in 3 seconds."'; \
echo ' sleep 3'; \
echo 'done' ; \
} > /entrypoint.sh \
&& chmod +x /entrypoint.sh
WORKDIR /opt/backstory/frontend
RUN { \
echo '#!/bin/bash' ; \
echo 'if [[ "${1}" != "" ]]; then bash -c "${@}"; else bash; fi' ; \
} > /opt/backstory/shell ; \
chmod +x /opt/backstory/shell
COPY /frontend/ /opt/backstory/frontend/
ENV PATH=/opt/backstory:$PATH
ENTRYPOINT [ "/entrypoint.sh" ]

View File

@ -91,7 +91,9 @@ This project provides the following containers:
| Container | Purpose | | Container | Purpose |
|:----------|:---------------------------------------------------------------| |:----------|:---------------------------------------------------------------|
| backstory | Base container with GPU packages installed and configured. Main server entry point. Also used for frontend development. | | backstory | Base container with GPU packages installed and configured. Main server entry point. Exposes an HTTPS entrypoint for use by frontend development |
| backstory-prod | Base container with GPU packages installed and configured. Main server entry point. Exposes an HTTP entrypoint for exposing via nginx or other reverse proxy server. Serves static files generated by frontend. |
| frontend | Frontend development and building static file for backstory-prod. |
| jupyter | backstory + Jupyter notebook for running Jupyter sessions | | jupyter | backstory + Jupyter notebook for running Jupyter sessions |
| miniircd | Tiny deployment of an IRC server for testing IRC agents | | miniircd | Tiny deployment of an IRC server for testing IRC agents |
| ollama | Installation of Intel's pre-built Ollama.cpp | | ollama | Installation of Intel's pre-built Ollama.cpp |

View File

@ -6,7 +6,7 @@ services:
target: backstory target: backstory
container_name: backstory container_name: backstory
image: backstory image: backstory
restart: "no" restart: "always"
env_file: env_file:
- .env - .env
environment: environment:
@ -19,8 +19,7 @@ services:
networks: networks:
- internal - internal
ports: ports:
- 8912:8911 # Flask React server - 8912:8911 # FastAPI React server
- 3000:3000 # REACT expo while developing frontend
volumes: volumes:
- ./cache:/root/.cache # Persist all models and GPU kernel cache - ./cache:/root/.cache # Persist all models and GPU kernel cache
- ./sessions:/opt/backstory/sessions:rw # Persist sessions - ./sessions:/opt/backstory/sessions:rw # Persist sessions
@ -28,7 +27,6 @@ services:
- ./dev-keys:/opt/backstory/keys:ro # Developer keys - ./dev-keys:/opt/backstory/keys:ro # Developer keys
- ./docs:/opt/backstory/docs:rw # Live mount of RAG content - ./docs:/opt/backstory/docs:rw # Live mount of RAG content
- ./src:/opt/backstory/src:rw # Live mount server src - ./src:/opt/backstory/src:rw # Live mount server src
- ./frontend:/opt/backstory/frontend:rw # Live mount frontend src
cap_add: # used for running ze-monitor within container cap_add: # used for running ze-monitor within container
- CAP_DAC_READ_SEARCH # Bypass all filesystem read access checks - CAP_DAC_READ_SEARCH # Bypass all filesystem read access checks
- CAP_PERFMON # Access to perf_events (vs. overloaded CAP_SYS_ADMIN) - CAP_PERFMON # Access to perf_events (vs. overloaded CAP_SYS_ADMIN)
@ -54,18 +52,33 @@ services:
networks: networks:
- internal - internal
ports: ports:
- 8911:8911 # Flask React server - 8911:8911 # FastAPI React server
volumes: volumes:
- ./cache:/root/.cache # Persist all models and GPU kernel cache - ./cache:/root/.cache # Persist all models and GPU kernel cache
- ./chromadb-prod:/opt/backstory/chromadb:rw # Persist ChromaDB - ./chromadb-prod:/opt/backstory/chromadb:rw # Persist ChromaDB
- ./sessions-prod:/opt/backstory/sessions:rw # Persist sessions - ./sessions-prod:/opt/backstory/sessions:rw # Persist sessions
- ./docs-prod:/opt/backstory/docs:rw # Live mount of RAG content - ./docs-prod:/opt/backstory/docs:rw # Live mount of RAG content
- ./frontend:/opt/backstory/frontend:rw # Live mount frontend src - ./frontend/deployed:/opt/backstory/deployed:ro # Live mount built frontend
cap_add: # used for running ze-monitor within container cap_add: # used for running ze-monitor within container
- CAP_DAC_READ_SEARCH # Bypass all filesystem read access checks - CAP_DAC_READ_SEARCH # Bypass all filesystem read access checks
- CAP_PERFMON # Access to perf_events (vs. overloaded CAP_SYS_ADMIN) - CAP_PERFMON # Access to perf_events (vs. overloaded CAP_SYS_ADMIN)
- CAP_SYS_PTRACE # PTRACE_MODE_READ_REALCREDS ptrace access mode check - CAP_SYS_PTRACE # PTRACE_MODE_READ_REALCREDS ptrace access mode check
frontend:
build:
context: .
dockerfile: Dockerfile
target: frontend
container_name: frontend
image: frontend
restart: "always"
env_file:
- .env
ports:
- 3000:3000 # REACT expo while developing frontend
volumes:
- ./frontend:/opt/backstory/frontend:rw # Live mount frontend src
ollama: ollama:
build: build:
context: . context: .
@ -116,19 +129,6 @@ services:
volumes: volumes:
- ./jupyter:/opt/jupyter:rw - ./jupyter:/opt/jupyter:rw
- ./cache:/root/.cache - ./cache:/root/.cache
deploy:
resources:
limits:
memory: "0" # No memory limit (Docker treats 0 as unlimited)
reservations:
memory: "0" # No reserved memory (optional)
ulimits:
memlock: -1 # Prevents memory from being locked
#oom_kill_disable: true # Prevents OOM killer from killing the container
cap_add: # used for running ze-monitor within container
- CAP_DAC_READ_SEARCH # Bypass all filesystem read access checks
- CAP_PERFMON # Access to perf_events (vs. overloaded CAP_SYS_ADMIN)
- CAP_SYS_PTRACE # PTRACE_MODE_READ_REALCREDS ptrace access mode check
miniircd: miniircd:
build: build:
@ -140,8 +140,6 @@ services:
restart: "no" restart: "no"
env_file: env_file:
- .env - .env
devices:
- /dev/dri:/dev/dri
ports: ports:
- 6667:6667 # IRC - 6667:6667 # IRC
networks: networks:
@ -153,10 +151,6 @@ services:
image: prom/prometheus image: prom/prometheus
container_name: prometheus container_name: prometheus
restart: "always" restart: "always"
# env_file:
# - .env
# devices:
# - /dev/dri:/dev/dri
ports: ports:
- 9090:9090 # Prometheus - 9090:9090 # Prometheus
networks: networks:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,2 @@
"use strict";(self.webpackChunkbackstory=self.webpackChunkbackstory||[]).push([[109],{3490:(e,t,a)=>{a.d(t,{diagram:()=>b});var i=a(7799),n=a(634),l=a(3957),r=a(3759),s=a(5502),o=a(700),c=r.UI.pie,p={sections:new Map,showData:!1,config:c},d=p.sections,g=p.showData,u=structuredClone(c),h=(0,r.K2)((()=>structuredClone(u)),"getConfig"),m=(0,r.K2)((()=>{d=new Map,g=p.showData,(0,r.IU)()}),"clear"),f=(0,r.K2)((e=>{let{label:t,value:a}=e;d.has(t)||(d.set(t,a),r.Rm.debug(`added new section: ${t}, with value: ${a}`))}),"addSection"),S=(0,r.K2)((()=>d),"getSections"),x=(0,r.K2)((e=>{g=e}),"setShowData"),w=(0,r.K2)((()=>g),"getShowData"),D={getConfig:h,clear:m,setDiagramTitle:r.ke,getDiagramTitle:r.ab,setAccTitle:r.SV,getAccTitle:r.iN,setAccDescription:r.EI,getAccDescription:r.m7,addSection:f,getSections:S,setShowData:x,getShowData:w},y=(0,r.K2)(((e,t)=>{(0,i.S)(e,t),t.setShowData(e.showData),e.sections.map(t.addSection)}),"populateDb"),T={parse:(0,r.K2)((async e=>{const t=await(0,s.qg)("pie",e);r.Rm.debug(t),y(t,D)}),"parse")},$=(0,r.K2)((e=>`\n .pieCircle{\n stroke: ${e.pieStrokeColor};\n stroke-width : ${e.pieStrokeWidth};\n opacity : ${e.pieOpacity};\n }\n .pieOuterCircle{\n stroke: ${e.pieOuterStrokeColor};\n stroke-width: ${e.pieOuterStrokeWidth};\n fill: none;\n }\n .pieTitleText {\n text-anchor: middle;\n font-size: ${e.pieTitleTextSize};\n fill: ${e.pieTitleTextColor};\n font-family: ${e.fontFamily};\n }\n .slice {\n font-family: ${e.fontFamily};\n fill: ${e.pieSectionTextColor};\n font-size:${e.pieSectionTextSize};\n // fill: white;\n }\n .legend text {\n fill: ${e.pieLegendTextColor};\n font-family: ${e.fontFamily};\n font-size: ${e.pieLegendTextSize};\n }\n`),"getStyles"),C=(0,r.K2)((e=>{const t=[...e.entries()].map((e=>({label:e[0],value:e[1]}))).sort(((e,t)=>t.value-e.value));return(0,o.rLf)().value((e=>e.value))(t)}),"createPieArcs"),b={parser:T,db:D,renderer:{draw:(0,r.K2)(((e,t,a,i)=>{r.Rm.debug("rendering pie chart\n"+e);const s=i.db,c=(0,r.D7)(),p=(0,n.$t)(s.getConfig(),c.pie),d=18,g=450,u=g,h=(0,l.D)(t),m=h.append("g");m.attr("transform","translate(225,225)");const{themeVariables:f}=c;let[S]=(0,n.I5)(f.pieOuterStrokeWidth);S??=2;const x=p.textPosition,w=Math.min(u,g)/2-40,D=(0,o.JLW)().innerRadius(0).outerRadius(w),y=(0,o.JLW)().innerRadius(w*x).outerRadius(w*x);m.append("circle").attr("cx",0).attr("cy",0).attr("r",w+S/2).attr("class","pieOuterCircle");const T=s.getSections(),$=C(T),b=[f.pie1,f.pie2,f.pie3,f.pie4,f.pie5,f.pie6,f.pie7,f.pie8,f.pie9,f.pie10,f.pie11,f.pie12],k=(0,o.UMr)(b);m.selectAll("mySlices").data($).enter().append("path").attr("d",D).attr("fill",(e=>k(e.data.label))).attr("class","pieCircle");let K=0;T.forEach((e=>{K+=e})),m.selectAll("mySlices").data($).enter().append("text").text((e=>(e.data.value/K*100).toFixed(0)+"%")).attr("transform",(e=>"translate("+y.centroid(e)+")")).style("text-anchor","middle").attr("class","slice"),m.append("text").text(s.getDiagramTitle()).attr("x",0).attr("y",-200).attr("class","pieTitleText");const v=m.selectAll(".legend").data(k.domain()).enter().append("g").attr("class","legend").attr("transform",((e,t)=>"translate(216,"+(22*t-22*k.domain().length/2)+")"));v.append("rect").attr("width",d).attr("height",d).style("fill",k).style("stroke",k),v.data($).append("text").attr("x",22).attr("y",14).text((e=>{const{label:t,value:a}=e.data;return s.getShowData()?`${t} [${a}]`:t}));const A=512+Math.max(...v.selectAll("text").nodes().map((e=>e?.getBoundingClientRect().width??0)));h.attr("viewBox",`0 0 ${A} 450`),(0,r.a$)(h,g,A,p.useMaxWidth)}),"draw")},styles:$}},7799:(e,t,a)=>{function i(e,t){e.accDescr&&t.setAccDescription?.(e.accDescr),e.accTitle&&t.setAccTitle?.(e.accTitle),e.title&&t.setDiagramTitle?.(e.title)}a.d(t,{S:()=>i}),(0,a(3759).K2)(i,"populateCommonDb")}}]);
//# sourceMappingURL=109.fea37f16.chunk.js.map

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,2 @@
"use strict";(self.webpackChunkbackstory=self.webpackChunkbackstory||[]).push([[126],{2126:(r,s,e)=>{e.d(s,{diagram:()=>c});var a=e(8035),t=(e(6327),e(53),e(1580),e(2598),e(958),e(8434),e(9711),e(2596),e(634),e(3759)),c={parser:a._$,get db(){return new a.NM},renderer:a.Lh,styles:a.tM,init:(0,t.K2)((r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute}),"init")}}}]);
//# sourceMappingURL=126.9837c9af.chunk.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"static/js/126.9837c9af.chunk.js","mappings":"0NAoBIA,EAAU,CACZC,OAAQC,EAAAA,GACR,MAAIC,GACF,OAAO,IAAIC,EAAAA,EACb,EACAC,SAAUC,EAAAA,GACVC,OAAQC,EAAAA,GACRC,MAAsBC,EAAAA,EAAAA,KAAQC,IACvBA,EAAIC,QACPD,EAAIC,MAAQ,CAAC,GAEfD,EAAIC,MAAMC,oBAAsBF,EAAIE,mBAAmB,GACtD,Q","sources":["../node_modules/mermaid/dist/chunks/mermaid.core/classDiagram-v2-COTLJTTW.mjs"],"sourcesContent":["import {\n ClassDB,\n classDiagram_default,\n classRenderer_v3_unified_default,\n styles_default\n} from \"./chunk-A2AXSNBT.mjs\";\nimport \"./chunk-RZ5BOZE2.mjs\";\nimport \"./chunk-TYCBKAJE.mjs\";\nimport \"./chunk-IIMUDSI4.mjs\";\nimport \"./chunk-VV3M67IP.mjs\";\nimport \"./chunk-HRU6DDCH.mjs\";\nimport \"./chunk-K557N5IZ.mjs\";\nimport \"./chunk-H2D2JQ3I.mjs\";\nimport \"./chunk-C3MQ5ANM.mjs\";\nimport \"./chunk-O4NI6UNU.mjs\";\nimport {\n __name\n} from \"./chunk-YTJNT7DU.mjs\";\n\n// src/diagrams/class/classDiagram-v2.ts\nvar diagram = {\n parser: classDiagram_default,\n get db() {\n return new ClassDB();\n },\n renderer: classRenderer_v3_unified_default,\n styles: styles_default,\n init: /* @__PURE__ */ __name((cnf) => {\n if (!cnf.class) {\n cnf.class = {};\n }\n cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;\n }, \"init\")\n};\nexport {\n diagram\n};\n"],"names":["diagram","parser","classDiagram_default","db","ClassDB","renderer","classRenderer_v3_unified_default","styles","styles_default","init","__name","cnf","class","arrowMarkerAbsolute"],"sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
"use strict";(self.webpackChunkbackstory=self.webpackChunkbackstory||[]).push([[318],{318:(e,s,a)=>{a.d(s,{createGitGraphServices:()=>c.b});var c=a(3670);a(1584)}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
"use strict";(self.webpackChunkbackstory=self.webpackChunkbackstory||[]).push([[372],{7372:(e,s,c)=>{c.d(s,{createInfoServices:()=>a.v});var a=c(9456);c(1584)}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,2 @@
"use strict";(self.webpackChunkbackstory=self.webpackChunkbackstory||[]).push([[453],{8834:(e,t,n)=>{n.r(t),n.d(t,{getCLS:()=>y,getFCP:()=>g,getFID:()=>F,getLCP:()=>P,getTTFB:()=>D});var i,r,a,o,u=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:"v2-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},c=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if("first-input"===e&&!("PerformanceEventTiming"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},s=function(e,t){var n=function n(i){"pagehide"!==i.type&&"hidden"!==document.visibilityState||(e(i),t&&(removeEventListener("visibilitychange",n,!0),removeEventListener("pagehide",n,!0)))};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0)},f=function(e){addEventListener("pageshow",(function(t){t.persisted&&e(t)}),!0)},m=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},v=-1,p=function(){return"hidden"===document.visibilityState?0:1/0},d=function(){s((function(e){var t=e.timeStamp;v=t}),!0)},l=function(){return v<0&&(v=p(),d(),f((function(){setTimeout((function(){v=p(),d()}),0)}))),{get firstHiddenTime(){return v}}},g=function(e,t){var n,i=l(),r=u("FCP"),a=function(e){"first-contentful-paint"===e.name&&(s&&s.disconnect(),e.startTime<i.firstHiddenTime&&(r.value=e.startTime,r.entries.push(e),n(!0)))},o=window.performance&&performance.getEntriesByName&&performance.getEntriesByName("first-contentful-paint")[0],s=o?null:c("paint",a);(o||s)&&(n=m(e,r,t),o&&a(o),f((function(i){r=u("FCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,n(!0)}))}))})))},h=!1,T=-1,y=function(e,t){h||(g((function(e){T=e.value})),h=!0);var n,i=function(t){T>-1&&e(t)},r=u("CLS",0),a=0,o=[],v=function(e){if(!e.hadRecentInput){var t=o[0],i=o[o.length-1];a&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(a+=e.value,o.push(e)):(a=e.value,o=[e]),a>r.value&&(r.value=a,r.entries=o,n())}},p=c("layout-shift",v);p&&(n=m(i,r,t),s((function(){p.takeRecords().map(v),n(!0)})),f((function(){a=0,T=-1,r=u("CLS",0),n=m(i,r,t)})))},E={passive:!0,capture:!0},w=new Date,L=function(e,t){i||(i=t,r=e,a=new Date,k(removeEventListener),S())},S=function(){if(r>=0&&r<a-w){var e={entryType:"first-input",name:i.type,target:i.target,cancelable:i.cancelable,startTime:i.timeStamp,processingStart:i.timeStamp+r};o.forEach((function(t){t(e)})),o=[]}},b=function(e){if(e.cancelable){var t=(e.timeStamp>1e12?new Date:performance.now())-e.timeStamp;"pointerdown"==e.type?function(e,t){var n=function(){L(e,t),r()},i=function(){r()},r=function(){removeEventListener("pointerup",n,E),removeEventListener("pointercancel",i,E)};addEventListener("pointerup",n,E),addEventListener("pointercancel",i,E)}(t,e):L(t,e)}},k=function(e){["mousedown","keydown","touchstart","pointerdown"].forEach((function(t){return e(t,b,E)}))},F=function(e,t){var n,a=l(),v=u("FID"),p=function(e){e.startTime<a.firstHiddenTime&&(v.value=e.processingStart-e.startTime,v.entries.push(e),n(!0))},d=c("first-input",p);n=m(e,v,t),d&&s((function(){d.takeRecords().map(p),d.disconnect()}),!0),d&&f((function(){var a;v=u("FID"),n=m(e,v,t),o=[],r=-1,i=null,k(addEventListener),a=p,o.push(a),S()}))},C={},P=function(e,t){var n,i=l(),r=u("LCP"),a=function(e){var t=e.startTime;t<i.firstHiddenTime&&(r.value=t,r.entries.push(e),n())},o=c("largest-contentful-paint",a);if(o){n=m(e,r,t);var v=function(){C[r.id]||(o.takeRecords().map(a),o.disconnect(),C[r.id]=!0,n(!0))};["keydown","click"].forEach((function(e){addEventListener(e,v,{once:!0,capture:!0})})),s(v,!0),f((function(i){r=u("LCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,C[r.id]=!0,n(!0)}))}))}))}},D=function(e){var t,n=u("TTFB");t=function(){try{var t=performance.getEntriesByType("navigation")[0]||function(){var e=performance.timing,t={entryType:"navigation",startTime:0};for(var n in e)"navigationStart"!==n&&"toJSON"!==n&&(t[n]=Math.max(e[n]-e.navigationStart,0));return t}();if(n.value=n.delta=t.responseStart,n.value<0||n.value>performance.now())return;n.entries=[t],e(n)}catch(e){}},"complete"===document.readyState?setTimeout(t,0):addEventListener("load",(function(){return setTimeout(t,0)}))}}}]);
//# sourceMappingURL=453.6fae039d.chunk.js.map

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,2 @@
"use strict";(self.webpackChunkbackstory=self.webpackChunkbackstory||[]).push([[461],{5461:(e,r,a)=>{a.d(r,{diagram:()=>c});var t=a(9945),s=a(3957),n=a(3759),i=a(5502),d={parse:(0,n.K2)((async e=>{const r=await(0,i.qg)("info",e);n.Rm.debug(r)}),"parse")},o={version:t.n.version},c={parser:d,db:{getVersion:(0,n.K2)((()=>o.version),"getVersion")},renderer:{draw:(0,n.K2)(((e,r,a)=>{n.Rm.debug("rendering info diagram\n"+e);const t=(0,s.D)(r);(0,n.a$)(t,100,400,!0);t.append("g").append("text").attr("x",100).attr("y",40).attr("class","version").attr("font-size",32).style("text-anchor","middle").text(`v${a}`)}),"draw")}}}}]);
//# sourceMappingURL=461.dcde3ae6.chunk.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"static/js/461.dcde3ae6.chunk.js","mappings":"wKAcIA,EAAS,CACXC,OAAuBC,EAAAA,EAAAA,KAAOC,UAC5B,MAAMC,QAAYH,EAAAA,EAAAA,IAAM,OAAQI,GAChCC,EAAAA,GAAIC,MAAMH,EAAI,GACb,UAIDI,EAAkB,CAAEC,QAASC,EAAAA,EAAgBD,SAiB7CE,EAAU,CACZX,SACAY,GAjBO,CACPC,YAF+BX,EAAAA,EAAAA,KAAO,IAAMM,EAAgBC,SAAS,eAmBrEK,SANa,CAAEC,MAPUb,EAAAA,EAAAA,KAAO,CAACc,EAAMC,EAAIR,KAC3CH,EAAAA,GAAIC,MAAM,2BAA6BS,GACvC,MAAME,GAAMC,EAAAA,EAAAA,GAAiBF,IAC7BG,EAAAA,EAAAA,IAAiBF,EAAK,IAAK,KAAK,GAClBA,EAAIG,OAAO,KACnBA,OAAO,QAAQC,KAAK,IAAK,KAAKA,KAAK,IAAK,IAAIA,KAAK,QAAS,WAAWA,KAAK,YAAa,IAAIC,MAAM,cAAe,UAAUP,KAAK,IAAIP,IAAU,GAClJ,S","sources":["../node_modules/mermaid/dist/chunks/mermaid.core/infoDiagram-PH2N3AL5.mjs"],"sourcesContent":["import {\n package_default\n} from \"./chunk-5NNNAHNI.mjs\";\nimport {\n selectSvgElement\n} from \"./chunk-7B677QYD.mjs\";\nimport {\n __name,\n configureSvgSize,\n log\n} from \"./chunk-YTJNT7DU.mjs\";\n\n// src/diagrams/info/infoParser.ts\nimport { parse } from \"@mermaid-js/parser\";\nvar parser = {\n parse: /* @__PURE__ */ __name(async (input) => {\n const ast = await parse(\"info\", input);\n log.debug(ast);\n }, \"parse\")\n};\n\n// src/diagrams/info/infoDb.ts\nvar DEFAULT_INFO_DB = { version: package_default.version };\nvar getVersion = /* @__PURE__ */ __name(() => DEFAULT_INFO_DB.version, \"getVersion\");\nvar db = {\n getVersion\n};\n\n// src/diagrams/info/infoRenderer.ts\nvar draw = /* @__PURE__ */ __name((text, id, version) => {\n log.debug(\"rendering info diagram\\n\" + text);\n const svg = selectSvgElement(id);\n configureSvgSize(svg, 100, 400, true);\n const group = svg.append(\"g\");\n group.append(\"text\").attr(\"x\", 100).attr(\"y\", 40).attr(\"class\", \"version\").attr(\"font-size\", 32).style(\"text-anchor\", \"middle\").text(`v${version}`);\n}, \"draw\");\nvar renderer = { draw };\n\n// src/diagrams/info/infoDiagram.ts\nvar diagram = {\n parser,\n db,\n renderer\n};\nexport {\n diagram\n};\n"],"names":["parser","parse","__name","async","ast","input","log","debug","DEFAULT_INFO_DB","version","package_default","diagram","db","getVersion","renderer","draw","text","id","svg","selectSvgElement","configureSvgSize","append","attr","style"],"sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,9 @@
/*!
Embeddable Minimum Strictly-Compliant Promises/A+ 1.1.1 Thenable
Copyright (c) 2013-2014 Ralf S. Engelschall (http://engelschall.com)
Licensed under The MIT License (http://opensource.org/licenses/MIT)
*/
/*! Bezier curve function generator. Copyright Gaetan Renaudeau. MIT License: http://en.wikipedia.org/wiki/MIT_License */
/*! Runge-Kutta spring physics function generator. Adapted from Framer.js, copyright Koen Bok. MIT License: http://en.wikipedia.org/wiki/MIT_License */

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,2 @@
"use strict";(self.webpackChunkbackstory=self.webpackChunkbackstory||[]).push([[674],{4674:(t,e,a)=>{a.d(e,{diagram:()=>x});var r=a(7799),n=a(634),o=a(3957),l=a(3759),i=a(5502),c={packet:[]},s=structuredClone(c),d=l.UI.packet,k=(0,l.K2)((()=>{const t=(0,n.$t)({...d,...(0,l.zj)().packet});return t.showBits&&(t.paddingY+=10),t}),"getConfig"),p=(0,l.K2)((()=>s.packet),"getPacket"),b={pushWord:(0,l.K2)((t=>{t.length>0&&s.packet.push(t)}),"pushWord"),getPacket:p,getConfig:k,clear:(0,l.K2)((()=>{(0,l.IU)(),s=structuredClone(c)}),"clear"),setAccTitle:l.SV,getAccTitle:l.iN,setDiagramTitle:l.ke,getDiagramTitle:l.ab,getAccDescription:l.m7,setAccDescription:l.EI},g=(0,l.K2)((t=>{(0,r.S)(t,b);let e=-1,a=[],n=1;const{bitsPerRow:o}=b.getConfig();for(let{start:r,end:i,label:c}of t.blocks){if(i&&i<r)throw new Error(`Packet block ${r} - ${i} is invalid. End must be greater than start.`);if(r!==e+1)throw new Error(`Packet block ${r} - ${i??r} is not contiguous. It should start from ${e+1}.`);for(e=i??r,l.Rm.debug(`Packet block ${r} - ${e} with label ${c}`);a.length<=o+1&&b.getPacket().length<1e4;){const[t,e]=h({start:r,end:i,label:c},n,o);if(a.push(t),t.end+1===n*o&&(b.pushWord(a),a=[],n++),!e)break;({start:r,end:i,label:c}=e)}}b.pushWord(a)}),"populate"),h=(0,l.K2)(((t,e,a)=>{if(void 0===t.end&&(t.end=t.start),t.start>t.end)throw new Error(`Block start ${t.start} is greater than block end ${t.end}.`);return t.end+1<=e*a?[t,void 0]:[{start:t.start,end:e*a-1,label:t.label},{start:e*a,end:t.end,label:t.label}]}),"getNextFittingBlock"),f={parse:(0,l.K2)((async t=>{const e=await(0,i.qg)("packet",t);l.Rm.debug(e),g(e)}),"parse")},u=(0,l.K2)(((t,e,a,r)=>{const n=r.db,i=n.getConfig(),{rowHeight:c,paddingY:s,bitWidth:d,bitsPerRow:k}=i,p=n.getPacket(),b=n.getDiagramTitle(),g=c+s,h=g*(p.length+1)-(b?0:c),f=d*k+2,u=(0,o.D)(e);u.attr("viewbox",`0 0 ${f} ${h}`),(0,l.a$)(u,h,f,i.useMaxWidth);for(const[o,l]of p.entries())$(u,l,o,i);u.append("text").text(b).attr("x",f/2).attr("y",h-g/2).attr("dominant-baseline","middle").attr("text-anchor","middle").attr("class","packetTitle")}),"draw"),$=(0,l.K2)(((t,e,a,r)=>{let{rowHeight:n,paddingX:o,paddingY:l,bitWidth:i,bitsPerRow:c,showBits:s}=r;const d=t.append("g"),k=a*(n+l)+l;for(const p of e){const t=p.start%c*i+1,e=(p.end-p.start+1)*i-o;if(d.append("rect").attr("x",t).attr("y",k).attr("width",e).attr("height",n).attr("class","packetBlock"),d.append("text").attr("x",t+e/2).attr("y",k+n/2).attr("class","packetLabel").attr("dominant-baseline","middle").attr("text-anchor","middle").text(p.label),!s)continue;const a=p.end===p.start,r=k-2;d.append("text").attr("x",t+(a?e/2:0)).attr("y",r).attr("class","packetByte start").attr("dominant-baseline","auto").attr("text-anchor",a?"middle":"start").text(p.start),a||d.append("text").attr("x",t+e).attr("y",r).attr("class","packetByte end").attr("dominant-baseline","auto").attr("text-anchor","end").text(p.end)}}),"drawWord"),w={byteFontSize:"10px",startByteColor:"black",endByteColor:"black",labelColor:"black",labelFontSize:"12px",titleColor:"black",titleFontSize:"14px",blockStrokeColor:"black",blockStrokeWidth:"1",blockFillColor:"#efefef"},x={parser:f,db:b,renderer:{draw:u},styles:(0,l.K2)((function(){let{packet:t}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};const e=(0,n.$t)(w,t);return`\n\t.packetByte {\n\t\tfont-size: ${e.byteFontSize};\n\t}\n\t.packetByte.start {\n\t\tfill: ${e.startByteColor};\n\t}\n\t.packetByte.end {\n\t\tfill: ${e.endByteColor};\n\t}\n\t.packetLabel {\n\t\tfill: ${e.labelColor};\n\t\tfont-size: ${e.labelFontSize};\n\t}\n\t.packetTitle {\n\t\tfill: ${e.titleColor};\n\t\tfont-size: ${e.titleFontSize};\n\t}\n\t.packetBlock {\n\t\tstroke: ${e.blockStrokeColor};\n\t\tstroke-width: ${e.blockStrokeWidth};\n\t\tfill: ${e.blockFillColor};\n\t}\n\t`}),"styles")}},7799:(t,e,a)=>{function r(t,e){t.accDescr&&e.setAccDescription?.(t.accDescr),t.accTitle&&e.setAccTitle?.(t.accTitle),t.title&&e.setDiagramTitle?.(t.title)}a.d(e,{S:()=>r}),(0,a(3759).K2)(r,"populateCommonDb")}}]);
//# sourceMappingURL=674.cb2cf2f8.chunk.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,2 @@
"use strict";(self.webpackChunkbackstory=self.webpackChunkbackstory||[]).push([[807],{2807:(e,r,t)=>{t.d(r,{diagram:()=>k});var a=t(3355),s=(t(6327),t(53),t(1580),t(2598),t(958),t(8434),t(9711),t(2596),t(634),t(3759)),k={parser:a.Zk,get db(){return new a.u4(2)},renderer:a.q7,styles:a.tM,init:(0,s.K2)((e=>{e.state||(e.state={}),e.state.arrowMarkerAbsolute=e.arrowMarkerAbsolute}),"init")}}}]);
//# sourceMappingURL=807.a9b3c2ae.chunk.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"static/js/807.a9b3c2ae.chunk.js","mappings":"0NAoBIA,EAAU,CACZC,OAAQC,EAAAA,GACR,MAAIC,GACF,OAAO,IAAIC,EAAAA,GAAQ,EACrB,EACAC,SAAUC,EAAAA,GACVC,OAAQC,EAAAA,GACRC,MAAsBC,EAAAA,EAAAA,KAAQC,IACvBA,EAAIC,QACPD,EAAIC,MAAQ,CAAC,GAEfD,EAAIC,MAAMC,oBAAsBF,EAAIE,mBAAmB,GACtD,Q","sources":["../node_modules/mermaid/dist/chunks/mermaid.core/stateDiagram-v2-YXO3MK2T.mjs"],"sourcesContent":["import {\n StateDB,\n stateDiagram_default,\n stateRenderer_v3_unified_default,\n styles_default\n} from \"./chunk-AEK57VVT.mjs\";\nimport \"./chunk-RZ5BOZE2.mjs\";\nimport \"./chunk-TYCBKAJE.mjs\";\nimport \"./chunk-IIMUDSI4.mjs\";\nimport \"./chunk-VV3M67IP.mjs\";\nimport \"./chunk-HRU6DDCH.mjs\";\nimport \"./chunk-K557N5IZ.mjs\";\nimport \"./chunk-H2D2JQ3I.mjs\";\nimport \"./chunk-C3MQ5ANM.mjs\";\nimport \"./chunk-O4NI6UNU.mjs\";\nimport {\n __name\n} from \"./chunk-YTJNT7DU.mjs\";\n\n// src/diagrams/state/stateDiagram-v2.ts\nvar diagram = {\n parser: stateDiagram_default,\n get db() {\n return new StateDB(2);\n },\n renderer: stateRenderer_v3_unified_default,\n styles: styles_default,\n init: /* @__PURE__ */ __name((cnf) => {\n if (!cnf.state) {\n cnf.state = {};\n }\n cnf.state.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;\n }, \"init\")\n};\nexport {\n diagram\n};\n"],"names":["diagram","parser","stateDiagram_default","db","StateDB","renderer","stateRenderer_v3_unified_default","styles","styles_default","init","__name","cnf","state","arrowMarkerAbsolute"],"sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
"use strict";(self.webpackChunkbackstory=self.webpackChunkbackstory||[]).push([[914],{914:(e,s,c)=>{c.d(s,{createPieServices:()=>a.f});var a=c(7789);c(1584)}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
"use strict";(self.webpackChunkbackstory=self.webpackChunkbackstory||[]).push([[929],{4929:(e,a,s)=>{s.d(a,{createRadarServices:()=>c.f});var c=s(2502);s(1584)}}]);

View File

@ -0,0 +1,2 @@
"use strict";(self.webpackChunkbackstory=self.webpackChunkbackstory||[]).push([[972],{5972:(r,s,e)=>{e.d(s,{diagram:()=>c});var a=e(8035),t=(e(6327),e(53),e(1580),e(2598),e(958),e(8434),e(9711),e(2596),e(634),e(3759)),c={parser:a._$,get db(){return new a.NM},renderer:a.Lh,styles:a.tM,init:(0,t.K2)((r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute}),"init")}}}]);
//# sourceMappingURL=972.19d4b287.chunk.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"static/js/972.19d4b287.chunk.js","mappings":"0NAoBIA,EAAU,CACZC,OAAQC,EAAAA,GACR,MAAIC,GACF,OAAO,IAAIC,EAAAA,EACb,EACAC,SAAUC,EAAAA,GACVC,OAAQC,EAAAA,GACRC,MAAsBC,EAAAA,EAAAA,KAAQC,IACvBA,EAAIC,QACPD,EAAIC,MAAQ,CAAC,GAEfD,EAAIC,MAAMC,oBAAsBF,EAAIE,mBAAmB,GACtD,Q","sources":["../node_modules/mermaid/dist/chunks/mermaid.core/classDiagram-GIVACNV2.mjs"],"sourcesContent":["import {\n ClassDB,\n classDiagram_default,\n classRenderer_v3_unified_default,\n styles_default\n} from \"./chunk-A2AXSNBT.mjs\";\nimport \"./chunk-RZ5BOZE2.mjs\";\nimport \"./chunk-TYCBKAJE.mjs\";\nimport \"./chunk-IIMUDSI4.mjs\";\nimport \"./chunk-VV3M67IP.mjs\";\nimport \"./chunk-HRU6DDCH.mjs\";\nimport \"./chunk-K557N5IZ.mjs\";\nimport \"./chunk-H2D2JQ3I.mjs\";\nimport \"./chunk-C3MQ5ANM.mjs\";\nimport \"./chunk-O4NI6UNU.mjs\";\nimport {\n __name\n} from \"./chunk-YTJNT7DU.mjs\";\n\n// src/diagrams/class/classDiagram.ts\nvar diagram = {\n parser: classDiagram_default,\n get db() {\n return new ClassDB();\n },\n renderer: classRenderer_v3_unified_default,\n styles: styles_default,\n init: /* @__PURE__ */ __name((cnf) => {\n if (!cnf.class) {\n cnf.class = {};\n }\n cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;\n }, \"init\")\n};\nexport {\n diagram\n};\n"],"names":["diagram","parser","classDiagram_default","db","ClassDB","renderer","classRenderer_v3_unified_default","styles","styles_default","init","__name","cnf","class","arrowMarkerAbsolute"],"sourceRoot":""}

View File

@ -0,0 +1 @@
"use strict";(self.webpackChunkbackstory=self.webpackChunkbackstory||[]).push([[974],{6974:(e,c,s)=>{s.d(c,{createPacketServices:()=>a.$});var a=s(8062);s(1584)}}]);

View File

@ -0,0 +1 @@
"use strict";(self.webpackChunkbackstory=self.webpackChunkbackstory||[]).push([[987],{987:(e,c,r)=>{r.d(c,{createArchitectureServices:()=>s.S});var s=r(1438);r(1584)}}]);

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,157 @@
/*!
* Determine if an object is a Buffer
*
* @author Feross Aboukhadijeh <https://feross.org>
* @license MIT
*/
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <https://feross.org>
* @license MIT
*/
/*!
* pad-left <https://github.com/jonschlinkert/pad-left>
*
* Copyright (c) 2014-2015, Jon Schlinkert.
* Licensed under the MIT license.
*/
/*!
* repeat-string <https://github.com/jonschlinkert/repeat-string>
*
* Copyright (c) 2014-2015, Jon Schlinkert.
* Licensed under the MIT License.
*/
/*! @license DOMPurify 3.2.5 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.2.5/LICENSE */
/*! Bundled license information:
js-yaml/dist/js-yaml.mjs:
(*! js-yaml 4.1.0 https://github.com/nodeca/js-yaml @license MIT *)
*/
/*! Bundled license information:
native-promise-only/lib/npo.src.js:
(*! Native Promise Only
v0.8.1 (c) Kyle Simpson
MIT License: http://getify.mit-license.org
*)
polybooljs/index.js:
(*
* @copyright 2016 Sean Connelly (@voidqk), http://syntheti.cc
* @license MIT
* @preserve Project Home: https://github.com/voidqk/polybooljs
*)
ieee754/index.js:
(*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> *)
buffer/index.js:
(*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <https://feross.org>
* @license MIT
*)
safe-buffer/index.js:
(*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> *)
assert/build/internal/util/comparisons.js:
(*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <feross@feross.org> <http://feross.org>
* @license MIT
*)
object-assign/index.js:
(*
object-assign
(c) Sindre Sorhus
@license MIT
*)
maplibre-gl/dist/maplibre-gl.js:
(**
* MapLibre GL JS
* @license 3-Clause BSD. Full text of license: https://github.com/maplibre/maplibre-gl-js/blob/v4.7.1/LICENSE.txt
*)
*/
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
/**
* @license React
* react-dom-client.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-dom.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-is.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-jsx-runtime.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.13.1
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

View File

@ -25,13 +25,16 @@
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"@uiw/react-json-view": "^2.0.0-alpha.31", "@uiw/react-json-view": "^2.0.0-alpha.31",
"jsonrepair": "^3.12.0", "jsonrepair": "^3.12.0",
"markdown-it": "^14.1.0",
"mermaid": "^11.6.0", "mermaid": "^11.6.0",
"mui-markdown": "^2.0.1", "mui-markdown": "^2.0.1",
"prism-react-renderer": "^2.4.1", "prism-react-renderer": "^2.4.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-markdown-it": "^1.0.2",
"react-plotly.js": "^2.6.0", "react-plotly.js": "^2.6.0",
"react-router-dom": "^7.6.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-spinners": "^0.15.0", "react-spinners": "^0.15.0",
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.1",
@ -42,6 +45,7 @@
}, },
"devDependencies": { "devDependencies": {
"@craco/craco": "^7.1.0", "@craco/craco": "^7.1.0",
"@types/markdown-it": "^14.1.2",
"@types/plotly.js": "^2.35.5" "@types/plotly.js": "^2.35.5"
} }
}, },
@ -5090,6 +5094,12 @@
"integrity": "sha512-Gjm4+H9noDJgu5EdT3rUw5MhPBag46fiOy27BefvWkNL8mlZnKnCaVVVTLKj6RYXed9b62CPKnPav9govyQDzA==", "integrity": "sha512-Gjm4+H9noDJgu5EdT3rUw5MhPBag46fiOy27BefvWkNL8mlZnKnCaVVVTLKj6RYXed9b62CPKnPav9govyQDzA==",
"peer": true "peer": true
}, },
"node_modules/@types/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
"dev": true
},
"node_modules/@types/long": { "node_modules/@types/long": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
@ -5112,6 +5122,16 @@
"@types/pbf": "*" "@types/pbf": "*"
} }
}, },
"node_modules/@types/markdown-it": {
"version": "14.1.2",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
"dev": true,
"dependencies": {
"@types/linkify-it": "^5",
"@types/mdurl": "^2"
}
},
"node_modules/@types/mdast": { "node_modules/@types/mdast": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
@ -5120,6 +5140,12 @@
"@types/unist": "*" "@types/unist": "*"
} }
}, },
"node_modules/@types/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"dev": true
},
"node_modules/@types/mime": { "node_modules/@types/mime": {
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@ -11221,6 +11247,14 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/get-stdin": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
"integrity": "sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/get-stream": { "node_modules/get-stream": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@ -14334,6 +14368,14 @@
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
}, },
"node_modules/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"dependencies": {
"uc.micro": "^2.0.0"
}
},
"node_modules/loader-runner": { "node_modules/loader-runner": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
@ -14706,6 +14748,38 @@
"node": "^16.13.0 || >=18.0.0" "node": "^16.13.0 || >=18.0.0"
} }
}, },
"node_modules/markdown-it": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
"linkify-it": "^5.0.0",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"
},
"bin": {
"markdown-it": "bin/markdown-it.mjs"
}
},
"node_modules/markdown-it/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/markdown-it/node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/markdown-table": { "node_modules/markdown-table": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
@ -15044,6 +15118,11 @@
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz",
"integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==" "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA=="
}, },
"node_modules/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="
},
"node_modules/media-typer": { "node_modules/media-typer": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -18316,6 +18395,14 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"engines": {
"node": ">=6"
}
},
"node_modules/q": { "node_modules/q": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
@ -18609,6 +18696,67 @@
"react": ">=18" "react": ">=18"
} }
}, },
"node_modules/react-markdown-it": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/react-markdown-it/-/react-markdown-it-1.0.2.tgz",
"integrity": "sha512-Bzo/9UCCxlL2D7rYiVlxEqiOU66mqmLTzjxN0JLlioEhZhp7amzSq1YNS0+Jf0YKQmpBb5rfI9nh5s3wBsKnww==",
"dependencies": {
"markdown-it": "^4.4.0",
"strip-indent": "^1.0.1"
}
},
"node_modules/react-markdown-it/node_modules/entities": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="
},
"node_modules/react-markdown-it/node_modules/linkify-it": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-1.2.4.tgz",
"integrity": "sha512-eGHwtlABkp1NOJSiKUNqBf3SYAS5jPHtvRXPAgNaQwTqmkTahjtiLH9NtxdR5IOPhNvwNMN/diswSfZKzUkhGg==",
"dependencies": {
"uc.micro": "^1.0.1"
}
},
"node_modules/react-markdown-it/node_modules/markdown-it": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-4.4.0.tgz",
"integrity": "sha512-Rl8dHHeLuAh3E72OPY0tY7CLvlxgHiLhlshIYswAAabAg4YDBLa6e/LTgNkkxBO2K61ESzoquPQFMw/iMrT1PA==",
"dependencies": {
"argparse": "~1.0.2",
"entities": "~1.1.1",
"linkify-it": "~1.2.0",
"mdurl": "~1.0.0",
"uc.micro": "^1.0.0"
},
"bin": {
"markdown-it": "bin/markdown-it.js"
}
},
"node_modules/react-markdown-it/node_modules/mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
},
"node_modules/react-markdown-it/node_modules/strip-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
"integrity": "sha512-I5iQq6aFMM62fBEAIB/hXzwJD6EEZ0xEGCX2t7oXqaKPIRgt4WruAQ285BISgdkP+HLGWyeGmNJcpIwFeRYRUA==",
"dependencies": {
"get-stdin": "^4.0.1"
},
"bin": {
"strip-indent": "cli.js"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-markdown-it/node_modules/uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
},
"node_modules/react-plotly.js": { "node_modules/react-plotly.js": {
"version": "2.6.0", "version": "2.6.0",
"resolved": "https://registry.npmjs.org/react-plotly.js/-/react-plotly.js-2.6.0.tgz", "resolved": "https://registry.npmjs.org/react-plotly.js/-/react-plotly.js-2.6.0.tgz",
@ -18629,6 +18777,50 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-router": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz",
"integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.0.tgz",
"integrity": "sha512-DYgm6RDEuKdopSyGOWZGtDfSm7Aofb8CCzgkliTjtu/eDuB0gcsv6qdFhhi8HdtmA+KHkt5MfZ5K2PdzjugYsA==",
"dependencies": {
"react-router": "7.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/react-router/node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"engines": {
"node": ">=18"
}
},
"node_modules/react-scripts": { "node_modules/react-scripts": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@ -19781,6 +19973,11 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="
},
"node_modules/set-function-length": { "node_modules/set-function-length": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@ -21572,6 +21769,11 @@
"node": ">=4.2.0" "node": ">=4.2.0"
} }
}, },
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="
},
"node_modules/ufo": { "node_modules/ufo": {
"version": "1.6.1", "version": "1.6.1",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",

View File

@ -20,13 +20,16 @@
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"@uiw/react-json-view": "^2.0.0-alpha.31", "@uiw/react-json-view": "^2.0.0-alpha.31",
"jsonrepair": "^3.12.0", "jsonrepair": "^3.12.0",
"markdown-it": "^14.1.0",
"mermaid": "^11.6.0", "mermaid": "^11.6.0",
"mui-markdown": "^2.0.1", "mui-markdown": "^2.0.1",
"prism-react-renderer": "^2.4.1", "prism-react-renderer": "^2.4.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-markdown-it": "^1.0.2",
"react-plotly.js": "^2.6.0", "react-plotly.js": "^2.6.0",
"react-router-dom": "^7.6.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-spinners": "^0.15.0", "react-spinners": "^0.15.0",
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.1",
@ -60,6 +63,7 @@
}, },
"devDependencies": { "devDependencies": {
"@craco/craco": "^7.1.0", "@craco/craco": "^7.1.0",
"@types/markdown-it": "^14.1.2",
"@types/plotly.js": "^2.35.5" "@types/plotly.js": "^2.35.5"
} }
} }

View File

@ -19,10 +19,16 @@
## Some questions I've been asked ## Some questions I've been asked
Q. <ChatQuery prompt="Why aren't you providing this as a Platform As a Service (PaaS) application?" tunables={{ "enable_tools": false }} /> Q. <ChatQuery query={
prompt: "Why aren't you providing this as a Platform As a Service (PaaS) application?",
tunables: { "enable_tools": false }
} />
A. I could; but I don't want to store your data. I also don't want to have to be on the hook for support of this service. I like it, it's fun, but it's not what I want as my day-gig, you know? If it was, I wouldn't be looking for a job... A. I could; but I don't want to store your data. I also don't want to have to be on the hook for support of this service. I like it, it's fun, but it's not what I want as my day-gig, you know? If it was, I wouldn't be looking for a job...
Q. <ChatQuery prompt="Why can't I just ask Backstory these questions?" tunables={{ "enable_tools": false }} /> Q. <ChatQuery query={
prompt: "Why can't I just ask Backstory these questions?",
tunables: { "enable_tools": false }
} />
A. Try it. See what you find out :) A. Try it. See what you find out :)

View File

@ -4,13 +4,12 @@ The system follows a carefully designed pipeline with isolated stages to prevent
The system uses a pipeline of isolated analysis and generation steps: The system uses a pipeline of isolated analysis and generation steps:
1. **Stage 1: Isolated Analysis** (three sub-stages) 1. **Stage 1: Isolated Analysis**
- **1A: Job Analysis** - Extracts requirements from job description only - **1A: Job Analysis** - Extracts requirements from job description only
- **1B: Candidate Analysis** - Catalogs qualifications from resume/context only - **1B: Skill-Based Assessment** - For each required skill, determine a Individisual Skill Assessment, adding it to a Skill Assessments Collection.
- **1C: Mapping Analysis** - Identifies legitimate matches between requirements and qualifications
2. **Stage 2: Resume Generation** 2. **Stage 2: Resume Generation**
- Uses mapping output to create a tailored resume with evidence-based content - Uses Skills Asessments Collection to generate a tailored resume.
3. **Stage 3: Verification** 3. **Stage 3: Verification**
- Performs fact-checking to catch any remaining fabrications - Performs fact-checking to catch any remaining fabrications
@ -23,63 +22,62 @@ flowchart TD
A2 --> A3[Job Requirements JSON] A2 --> A3[Job Requirements JSON]
end end
subgraph "Stage 1B: Candidate Analysis" subgraph "Stage 1B: Skill-Based Assessment"
B1[Resume Input] --> B5[Candidate Analysis LLM] B1[Resume Input] --> B2[Candidate Info]
B5 --> B4[Candidate Qualifications JSON] B2 --> B3[RAG System]
B2[Candidate Info] --> B3[RAG] A3 --> B4[Skill Assessment Generator]
B3[RAG] --> B2[Candidate Info] B3 --> B4
A3[Job Requirements JSON] --> B3[RAG] B4 --> B5{For Each Required Skill}
B3[RAG] --> B5 B5 --> B6[Skill-Focused LLM Query]
end B6 --> B7[Individual Skill Assessment]
B7 --> B8[Skill Assessments Collection]
subgraph "Stage 1C: Mapping Analysis"
C1[Job Requirements JSON] --> C3[Mapping Analysis LLM]
C2[Candidate Qualifications JSON] --> C3
C3 --> C4[Skills Mapping JSON]
end end
end end
subgraph "Stage 2: Resume Generation" subgraph "Stage 2: Resume Generation"
D1[Skills Mapping JSON] --> D3[Resume Generation LLM] C1[Skill Assessments Collection] --> C2[Resume Generator]
D2[Original Resume Reference] --> D3 C3[Original Resume Reference] --> C2
D3 --> D4[Tailored Resume Draft] C4[Candidate Information] --> C2
C2 --> C5[Resume Generation Prompt]
C5 --> C6[Resume Generation LLM]
C6 --> C7[Tailored Resume Draft]
end end
subgraph "Stage 3: Verification" subgraph "Stage 3: Statistics & Verification"
E1[Skills Mapping JSON] --> E2[Original Materials] D1[Job Requirements JSON] --> D2[Match Statistics Calculator]
E2 --> E3[Tailored Resume Draft] D3[Skill Assessments Collection] --> D2
E3 --> E4[Verification LLM] D2 --> D4[Match Statistics]
E4 --> E5{Verification Check} D4 --> D5[Verification LLM]
E5 -->|PASS| E6[Approved Resume] C7 --> D5
E5 -->|FAIL| E7[Correction Instructions] D5 --> D6{Verification Check}
E7 --> D3 D6 -->|PASS| D7[Approved Resume]
D6 -->|FAIL| D8[Correction Instructions]
D8 --> C2
end end
A3 --> C1 A3 --> B4
B4 --> C2 B8 --> C1
C4 --> D1 B8 --> D3
C4 --> E1 B1 --> C3
D4 --> E3
style A2 fill:#f9d77e,stroke:#333,stroke-width:2px style A2 fill:#f9d77e,stroke:#333,stroke-width:2px
style B5 fill:#f9d77e,stroke:#333,stroke-width:2px style B6 fill:#f9d77e,stroke:#333,stroke-width:2px
style C3 fill:#f9d77e,stroke:#333,stroke-width:2px style C6 fill:#f9d77e,stroke:#333,stroke-width:2px
style D3 fill:#f9d77e,stroke:#333,stroke-width:2px style D5 fill:#f9d77e,stroke:#333,stroke-width:2px
style E4 fill:#f9d77e,stroke:#333,stroke-width:2px style B5 fill:#a3e4d7,stroke:#333,stroke-width:2px
style E5 fill:#a3e4d7,stroke:#333,stroke-width:2px style D6 fill:#a3e4d7,stroke:#333,stroke-width:2px
style E6 fill:#aed6f1,stroke:#333,stroke-width:2px style D7 fill:#aed6f1,stroke:#333,stroke-width:2px
style E7 fill:#f5b7b1,stroke:#333,stroke-width:2px style D8 fill:#f5b7b1,stroke:#333,stroke-width:2px
``` ```
## Stage 1: Isolated Analysis (three separate sub-stages) ## Stage 1: Isolated Analysis
1. **Job Analysis**: Extracts requirements from just the job description 1. **Job Analysis**: Extracts requirements from just the job description
2. **Candidate Analysis**: Catalogs qualifications from just the resume/context 2. **Candidate Analysis**: Catalogs qualifications for each job requirement from just the resume/context
3. **Mapping Analysis**: Identifies legitimate matches between requirements and qualifications
## Stage 2: Resume Generation ## Stage 2: Resume Generation
Creates a tailored resume using only verified information from the mapping Creates a tailored resume using the skills collection and candidate information.
## Stage 3: Verification ## Stage 3: Verification
@ -90,7 +88,7 @@ Creates a tailored resume using only verified information from the mapping
The system uses several techniques to prevent fabrication: The system uses several techniques to prevent fabrication:
* **Isolation of Analysis Stages**: By analyzing the job and candidate separately, the system prevents the LLM from prematurely creating connections that might lead to fabrication. * **Isolation of Analysis Stages**: By analyzing the job and candidate separately, and having the LLM only provide evidence of a single skill per pass, the system prevents the LLM from prematurely creating connections that might lead to fabrication.
* **Evidence Requirements**: Each qualification included must have explicit evidence from the original materials. * **Evidence Requirements**: Each qualification included must have explicit evidence from the original materials.
* **Conservative Transferability**: The system is instructed to be conservative when claiming skills are transferable. * **Conservative Transferability**: The system is instructed to be conservative when claiming skills are transferable.
* **Verification Layer**: A dedicated verification step acts as a safety check to catch any remaining fabrications. * **Verification Layer**: A dedicated verification step acts as a safety check to catch any remaining fabrications.

View File

@ -6,32 +6,54 @@ import { Document } from './Document';
const AboutPage = (props: BackstoryPageProps) => { const AboutPage = (props: BackstoryPageProps) => {
const { sessionId, submitQuery, setSnack, route, setRoute } = props; const { sessionId, submitQuery, setSnack, route, setRoute } = props;
const [ page, setPage ] = useState<string>(""); const [subRoute, setSubRoute] = useState<string>("");
const [ subRoute, setSubRoute] = useState<string>(""); const [page, setPage] = useState<string>("");
useEffect(() => {
console.log(`AboutPage: ${page} - route - ${route} - subRoute: ${subRoute}`);
}, [page, route, subRoute]);
useEffect(() => { useEffect(() => {
if (route === undefined) { return; } if (route === undefined) { return; }
const parts = route.split("/"); const parts = route.split("/");
if (parts.length === 0) { return; }
setPage(parts[0]); setPage(parts[0]);
if (parts.length > 1) { parts.shift();
parts.shift(); setSubRoute(parts.join("/"));
setSubRoute(parts.join("/")); }, [route]);
useEffect(() => {
console.log(`AboutPage: ${page} - sub-route - ${subRoute}`);
}, [page, subRoute]);
useEffect(() => {
if (route) {
const parts = route.split("/");
if (parts[0] !== page) {
parts.shift();
const incomingSubRoute = parts.join("/");
if (incomingSubRoute !== subRoute) {
setSubRoute(incomingSubRoute);
}
}
} else if (subRoute) {
setRoute && setRoute(subRoute);
} }
}, [route, setPage, setSubRoute]); }, [page, route, setRoute, subRoute]);
useEffect(() => {
let newRoute = page;
if (subRoute) {
newRoute += '/' + subRoute;
}
if (route !== newRoute && setRoute) {
setRoute(newRoute);
}
}, [route, page, subRoute, setRoute]);
const onDocumentExpand = (document: string, open: boolean) => { const onDocumentExpand = (document: string, open: boolean) => {
console.log("Document expanded:", document, open); console.log("Document expanded:", document, open);
if (open) { if (open) {
setSubRoute("");
setPage(document); setPage(document);
if (setRoute) setRoute(document);
} else { } else {
setSubRoute("");
setPage(""); setPage("");
if (setRoute) setRoute("");
} }
} }

View File

@ -1,402 +1,40 @@
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import React, { useRef, useCallback } from 'react';
import useMediaQuery from '@mui/material/useMediaQuery'; import { BrowserRouter as Router, Routes, Route, useLocation } from "react-router-dom";
import Card from '@mui/material/Card'; import { SessionWrapper } from "./SessionWrapper";
import { styled } from '@mui/material/styles'; import { Main } from "./Main";
import Avatar from '@mui/material/Avatar';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import Tooltip from '@mui/material/Tooltip';
import AppBar from '@mui/material/AppBar';
import Drawer from '@mui/material/Drawer';
import Toolbar from '@mui/material/Toolbar';
import SettingsIcon from '@mui/icons-material/Settings';
import IconButton from '@mui/material/IconButton';
import Box from '@mui/material/Box';
import CssBaseline from '@mui/material/CssBaseline';
import MenuIcon from '@mui/icons-material/Menu';
import { Snack, SeverityType } from './Snack'; import { Snack, SeverityType } from './Snack';
import { ConversationHandle } from './Conversation';
import { QueryOptions } from './ChatQuery';
import { Scrollable } from './Scrollable';
import { BackstoryPage, BackstoryTabProps } from './BackstoryTab';
import { connectionBase } from './Global'; export function PathRouter({ setSnack }: { setSnack: any }) {
const location = useLocation();
const segments = location.pathname.split("/").filter(Boolean);
const sessionId = segments[segments.length - 1];
import { HomePage } from './HomePage'; return (
import { ResumeBuilderPage } from './ResumeBuilderPage'; <SessionWrapper setSnack={setSnack}>
import { VectorVisualizerPage } from './VectorVisualizer'; <Main setSnack={setSnack} sessionId={sessionId} />
import { AboutPage } from './AboutPage'; </SessionWrapper>
import { ControlsPage } from './ControlsPage'; );
import './App.css';
import './Conversation.css';
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
const isValidUUIDv4 = (str: string): boolean => {
const pattern = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89ab][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/i;
return pattern.test(str);
} }
const App = () => { function App() {
const [sessionId, setSessionId] = useState<string | undefined>(undefined); const snackRef = useRef<any>(null);
const [menuOpen, setMenuOpen] = useState(false);
const [isMenuClosing, setIsMenuClosing] = useState(false);
const [activeTab, setActiveTab] = useState<number>(0);
const isDesktop = useMediaQuery('(min-width:650px)');
const prevIsDesktopRef = useRef<boolean>(isDesktop);
const chatRef = useRef<ConversationHandle>(null);
const snackRef = useRef<any>(null);
const [subRoute, setSubRoute] = useState<string>("");
useEffect(() => { const setSnack = useCallback((message: string, severity?: SeverityType) => {
if (prevIsDesktopRef.current === isDesktop) snackRef.current?.setSnack(message, severity);
return; }, [snackRef]);
if (menuOpen) {
setMenuOpen(false);
}
prevIsDesktopRef.current = isDesktop;
}, [isDesktop, setMenuOpen, menuOpen])
const setSnack = useCallback((message: string, severity?: SeverityType) => { return (
snackRef.current?.setSnack(message, severity); <>
}, [snackRef]); <Router>
<Routes>
<Route path="*" element={<PathRouter setSnack={setSnack} />} />
</Routes>
</Router>
<Snack
ref={snackRef}
/>
</>
);
}
const handleSubmitChatQuery = (prompt: string, tunables?: QueryOptions) => { export default App;
console.log(`handleSubmitChatQuery: ${prompt} ${tunables || {}} -- `, chatRef.current ? ' sending' : 'no handler');
chatRef.current?.submitQuery(prompt, tunables);
setActiveTab(0);
};
const tabs: BackstoryTabProps[] = useMemo(() => {
const homeTab: BackstoryTabProps = {
label: "",
path: "",
tabProps: {
label: "Backstory",
sx: { flexGrow: 1, fontSize: '1rem' },
icon:
<Avatar sx={{
width: 24,
height: 24
}}
variant="rounded"
alt="Backstory logo"
src="/logo192.png" />,
iconPosition: "start"
},
children: <HomePage ref={chatRef} {...{ sessionId, setSnack, submitQuery: handleSubmitChatQuery, route: subRoute, setRoute: setSubRoute }} />
};
const resumeBuilderTab: BackstoryTabProps = {
label: "Resume Builder",
path: "resume-builder",
children: <ResumeBuilderPage {...{ sessionId, setSnack, submitQuery: handleSubmitChatQuery, route: subRoute, setRoute: setSubRoute }} />
};
const contextVisualizerTab: BackstoryTabProps = {
label: "Context Visualizer",
path: "context-visualizer",
children: <VectorVisualizerPage sx={{ p: 1 }} {...{ sessionId, setSnack, submitQuery: handleSubmitChatQuery, route: subRoute, setRoute: setSubRoute }} />
};
const aboutTab = {
label: "About",
path: "about",
children: <AboutPage {...{ sessionId, setSnack, submitQuery: handleSubmitChatQuery, route: subRoute, setRoute: setSubRoute }} />
};
const controlsTab: BackstoryTabProps = {
path: "controls",
tabProps: {
sx: { flexShrink: 1, flexGrow: 0, fontSize: '1rem' },
icon: <SettingsIcon />
},
children: (
<Scrollable
autoscroll={false}
sx={{
maxWidth: "1024px",
height: "calc(100vh - 72px)",
flexDirection: "column",
margin: "0 auto",
p: 1,
}}
>
{sessionId !== undefined &&
<ControlsPage {...{ sessionId, setSnack, submitQuery: handleSubmitChatQuery, route: subRoute, setRoute: setSubRoute }} />
}
</Scrollable>
)
};
return [
homeTab,
resumeBuilderTab,
contextVisualizerTab,
aboutTab,
controlsTab,
];
}, [sessionId, setSnack, subRoute]);
useEffect(() => {
if (sessionId === undefined || activeTab > tabs.length - 1) { return; }
console.log(`route - '${tabs[activeTab].path}', subRoute - '${subRoute}'`);
let path = tabs[activeTab].path ? `/${tabs[activeTab].path}` : '';
if (subRoute) {
path += `/${subRoute}`;
}
path += `/${sessionId}`;
console.log('pushState: ', path);
// window.history.pushState({}, '', path);
}, [activeTab, sessionId, subRoute, tabs]);
const fetchSession = useCallback((async (pathParts?: string[]) => {
try {
const response = await fetch(connectionBase + `/api/context`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw Error("Server is temporarily down.");
}
const new_session = (await response.json()).id;
console.log(`Session created: ${new_session}`);
if (pathParts === undefined) {
setSessionId(new_session);
const newPath = `/${new_session}`;
window.history.replaceState({}, '', newPath);
} else {
const currentPath = pathParts.length < 2 ? '' : pathParts[0];
let tabIndex = tabs.findIndex((tab) => tab.path === currentPath);
if (-1 === tabIndex) {
console.log(`Invalid path "${currentPath}" -- redirecting to default`);
window.history.replaceState({}, '', `/${new_session}`);
setActiveTab(0);
} else {
window.history.replaceState({}, '', `/${pathParts.join('/')}/${new_session}`);
// tabs[tabIndex].route = pathParts[2] || "";
setActiveTab(tabIndex);
}
setSessionId(new_session);
}
} catch (error: any) {
console.error(error);
setSnack("Server is temporarily down", "error");
}
}), [setSnack, tabs]);
useEffect(() => {
const url = new URL(window.location.href);
const pathParts = url.pathname.split('/').filter(Boolean); // [path, sessionId]
if (pathParts.length < 1) {
console.log("No session id or path -- creating new session");
fetchSession();
} else {
const currentPath = pathParts.length < 2 ? '' : pathParts[0];
const path_session = pathParts.length < 2 ? pathParts[0] : pathParts[1];
if (!isValidUUIDv4(path_session)) {
console.log(`Invalid session id ${path_session}-- creating new session`);
fetchSession();
} else {
let tabIndex = tabs.findIndex((tab) => tab.path === currentPath);
if (-1 === tabIndex) {
console.log(`Invalid path "${currentPath}" -- redirecting to default`);
tabIndex = 0;
}
// tabs[tabIndex].route = pathParts[2] || ""
setSessionId(path_session);
setActiveTab(tabIndex);
}
}
}, [setSessionId, setSnack, tabs, fetchSession]);
const handleMenuClose = () => {
setIsMenuClosing(true);
setMenuOpen(false);
};
const handleMenuTransitionEnd = () => {
setIsMenuClosing(false);
};
const handleMenuToggle = () => {
if (!isMenuClosing) {
setMenuOpen(!menuOpen);
}
};
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
if (newValue > tabs.length) {
return;
}
setActiveTab(newValue);
const tabPath = tabs[newValue].path;
let path = `/${sessionId}`;
if (tabPath) {
// if (openDocument) {
// path = `/${tabPath}/${openDocument}/${sessionId}`;
// } else {
path = `/${tabPath}/${sessionId}`;
// }
}
window.history.pushState({}, '', path);
handleMenuClose();
};
useEffect(() => {
const handlePopState = () => {
const url = new URL(window.location.href);
const pathParts = url.pathname.split('/').filter(Boolean);
const currentPath = pathParts.length < 2 ? '' : pathParts[0];
const session = pathParts.length < 2 ? pathParts[0] : pathParts[1];
let tabIndex = tabs.findIndex((tab) => tab.path === currentPath);
if (-1 === tabIndex) {
console.log(`Invalid path "${currentPath}" -- redirecting to default`);
tabIndex = 0;
}
setSessionId(session);
setActiveTab(tabIndex);
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, [setSessionId, tabs]);
/* toolbar height is 64px + 8px margin-top */
const Offset = styled('div')(() => ({ minHeight: '72px', height: '72px' }));
return (
<Box className="App"
sx={{ display: 'flex', flexDirection: 'column' }}>
<CssBaseline />
<AppBar
position="fixed"
sx={{
zIndex: (theme) => theme.zIndex.drawer + 1,
maxWidth: "100vw"
}}
>
<Toolbar>
<Box sx={{ display: "flex", flexGrow: 1, flexDirection: "row" }}>
{!isDesktop &&
<Box sx={{ display: "flex", flexGrow: 1, flexDirection: "row" }}>
<IconButton
sx={{ display: "flex", margin: 'auto 0px' }}
size="large"
edge="start"
color="inherit"
onClick={handleMenuToggle}
>
<Tooltip title="Navigation">
<MenuIcon />
</Tooltip>
</IconButton>
<Tooltip title="Backstory">
<Box
sx={{ m: 1, gap: 1, display: "flex", flexDirection: "row", alignItems: "center", fontWeight: "bold", fontSize: "1.0rem", cursor: "pointer" }}
onClick={() => { setActiveTab(0); setMenuOpen(false); }}
>
<Avatar sx={{
width: 24,
height: 24
}}
variant="rounded"
alt="Backstory logo"
src="/logo192.png" />
BACKSTORY
</Box>
</Tooltip>
</Box>
}
{menuOpen === false && isDesktop &&
<Tabs sx={{ display: "flex", flexGrow: 1 }}
value={activeTab}
indicatorColor="secondary"
textColor="inherit"
variant="fullWidth"
allowScrollButtonsMobile
onChange={handleTabChange}
aria-label="Backstory navigation">
{tabs.map((tab, index) => <Tab key={index} value={index} label={tab.label} {...tab.tabProps} />)}
</Tabs>
}
</Box>
</Toolbar>
</AppBar>
<Offset />
<Box
sx={{ display: "flex", flexGrow: 1, flexDirection: "column" }}
>
<Box
component="nav"
aria-label="mailbox folders"
>
<Drawer
container={window.document.body}
variant="temporary"
open={menuOpen}
onTransitionEnd={handleMenuTransitionEnd}
onClose={handleMenuClose}
sx={{
display: 'block',
'& .MuiDrawer-paper': { boxSizing: 'border-box' },
}}
slotProps={{
root: {
keepMounted: true, // Better open performance on mobile.
},
}}
>
<Toolbar />
<Card className="MenuCard">
<Tabs sx={{ display: "flex", flexGrow: 1 }}
orientation="vertical"
value={activeTab}
indicatorColor="secondary"
textColor="inherit"
variant="scrollable"
allowScrollButtonsMobile
onChange={handleTabChange}
aria-label="Backstory navigation">
{tabs.map((tab, index) => <Tab key={index} value={index} label={tab.label} {...tab.tabProps} />)}
</Tabs>
</Card>
</Drawer>
</Box>
{
tabs.map((tab: any, i: number) =>
<BackstoryPage key={i} active={i === activeTab} path={tab.path}>{tab.children}</BackstoryPage>
)
}
</Box>
<Snack
ref={snackRef}
/>
</Box >
);
};
export default App;

View File

@ -5,7 +5,7 @@ import { ChatSubmitQueryInterface } from './ChatQuery';
import { SetSnackType } from './Snack'; import { SetSnackType } from './Snack';
interface BackstoryElementProps { interface BackstoryElementProps {
sessionId: string | undefined, sessionId: string,
setSnack: SetSnackType, setSnack: SetSnackType,
submitQuery: ChatSubmitQueryInterface, submitQuery: ChatSubmitQueryInterface,
sx?: SxProps<Theme>, sx?: SxProps<Theme>,

View File

@ -30,7 +30,6 @@ const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryText
const shadowRef = useRef<HTMLTextAreaElement>(null); const shadowRef = useRef<HTMLTextAreaElement>(null);
const [editValue, setEditValue] = useState<string>(value); const [editValue, setEditValue] = useState<string>(value);
console.log({ value, placeholder, editValue });
// Sync editValue with prop value if it changes externally // Sync editValue with prop value if it changes externally
useEffect(() => { useEffect(() => {
setEditValue(value || ""); setEditValue(value || "");

View File

@ -158,10 +158,9 @@ function ChatBubble(props: ChatBubbleProps) {
}; };
// Render Accordion for expandable content // Render Accordion for expandable content
if (expandable || (role === 'content' && title)) { if (expandable || title) {
// Determine if Accordion is controlled // Determine if Accordion is controlled
const isControlled = typeof expanded === 'boolean' && typeof onExpand === 'function'; const isControlled = typeof expanded === 'boolean' && typeof onExpand === 'function';
return ( return (
<Accordion <Accordion
expanded={isControlled ? expanded : undefined} // Omit expanded prop for uncontrolled expanded={isControlled ? expanded : undefined} // Omit expanded prop for uncontrolled

View File

@ -1,30 +1,32 @@
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
type QueryOptions = { /* backstory/src/utils/message.py */
type Tunables = {
enable_rag?: boolean, enable_rag?: boolean,
enable_tools?: boolean, enable_tools?: boolean,
enable_context?: boolean, enable_context?: boolean,
}; };
type ChatSubmitQueryInterface = (prompt: string, tunables?: QueryOptions) => void; /* backstory/src/server.py */
type Query = {
prompt: string,
tunables?: Tunables,
agent_options?: {},
};
type ChatSubmitQueryInterface = (query: Query) => void;
interface ChatQueryInterface { interface ChatQueryInterface {
prompt: string, query: Query,
tunables?: QueryOptions,
submitQuery?: ChatSubmitQueryInterface submitQuery?: ChatSubmitQueryInterface
} }
const ChatQuery = (props : ChatQueryInterface) => { const ChatQuery = (props : ChatQueryInterface) => {
const { prompt, submitQuery } = props; const { query, submitQuery } = props;
let tunables = props.tunables;
if (typeof (tunables) === "string") {
tunables = JSON.parse(tunables);
}
if (submitQuery === undefined) { if (submitQuery === undefined) {
return (<Box>{prompt}</Box>); return (<Box>{query.prompt}</Box>);
} }
return ( return (
<Button variant="outlined" sx={{ <Button variant="outlined" sx={{
@ -32,15 +34,15 @@ const ChatQuery = (props : ChatQueryInterface) => {
borderColor: theme => theme.palette.custom.highlight, borderColor: theme => theme.palette.custom.highlight,
m: 1 m: 1
}} }}
size="small" onClick={(e: any) => { submitQuery(prompt, tunables); }}> size="small" onClick={(e: any) => { submitQuery(query); }}>
{prompt} {query.prompt}
</Button> </Button>
); );
} }
export type { export type {
ChatQueryInterface, ChatQueryInterface,
QueryOptions, Query,
ChatSubmitQueryInterface, ChatSubmitQueryInterface,
}; };

View File

@ -86,7 +86,8 @@ const SystemInfoComponent: React.FC<{ systemInfo: SystemInfo | undefined }> = ({
return <div className="SystemInfo">{systemElements}</div>; return <div className="SystemInfo">{systemElements}</div>;
}; };
const ControlsPage = ({ sessionId, setSnack }: BackstoryPageProps) => { const ControlsPage = (props: BackstoryPageProps) => {
const { setSnack, sessionId } = props;
const [editSystemPrompt, setEditSystemPrompt] = useState<string>(""); const [editSystemPrompt, setEditSystemPrompt] = useState<string>("");
const [systemInfo, setSystemInfo] = useState<SystemInfo | undefined>(undefined); const [systemInfo, setSystemInfo] = useState<SystemInfo | undefined>(undefined);
const [tools, setTools] = useState<Tool[]>([]); const [tools, setTools] = useState<Tool[]>([]);
@ -281,7 +282,7 @@ const ControlsPage = ({ sessionId, setSnack }: BackstoryPageProps) => {
}, },
}); });
const data = await response.json(); const data = await response.json();
console.log("Server tunables: ", data); // console.log("Server tunables: ", data);
setServerTunables(data); setServerTunables(data);
setSystemPrompt(data["system_prompt"]); setSystemPrompt(data["system_prompt"]);
setMessageHistoryLength(data["message_history_length"]); setMessageHistoryLength(data["message_history_length"]);

View File

@ -1,48 +1,32 @@
import React, { useState, useImperativeHandle, forwardRef, useEffect, useRef, useCallback } from 'react'; import React, { useState, useImperativeHandle, forwardRef, useEffect, useRef, useCallback } from 'react';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Tooltip from '@mui/material/Tooltip'; import Tooltip from '@mui/material/Tooltip';
import IconButton from '@mui/material/IconButton';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import SendIcon from '@mui/icons-material/Send'; import SendIcon from '@mui/icons-material/Send';
import CancelIcon from '@mui/icons-material/Cancel';
import { SxProps, Theme } from '@mui/material'; import { SxProps, Theme } from '@mui/material';
import PropagateLoader from "react-spinners/PropagateLoader"; import PropagateLoader from "react-spinners/PropagateLoader";
import { Message, MessageList, MessageData } from './Message'; import { Message, MessageList, BackstoryMessage } from './Message';
import { ContextStatus } from './ContextStatus'; import { ContextStatus } from './ContextStatus';
import { Scrollable } from './Scrollable'; import { Scrollable } from './Scrollable';
import { DeleteConfirmation } from './DeleteConfirmation'; import { DeleteConfirmation } from './DeleteConfirmation';
import { QueryOptions } from './ChatQuery'; import { Query } from './ChatQuery';
import './Conversation.css'; import './Conversation.css';
import { BackstoryTextField, BackstoryTextFieldRef } from './BackstoryTextField'; import { BackstoryTextField, BackstoryTextFieldRef } from './BackstoryTextField';
import { BackstoryElementProps } from './BackstoryTab'; import { BackstoryElementProps } from './BackstoryTab';
import { connectionBase } from './Global'; import { connectionBase } from './Global';
const loadingMessage: MessageData = { "role": "status", "content": "Establishing connection with server..." }; const loadingMessage: BackstoryMessage = { "role": "status", "content": "Establishing connection with server..." };
type ConversationMode = 'chat' | 'job_description' | 'resume' | 'fact_check'; type ConversationMode = 'chat' | 'job_description' | 'resume' | 'fact_check';
interface ConversationHandle { interface ConversationHandle {
submitQuery: (prompt: string, options?: QueryOptions) => void; submitQuery: (query: Query) => void;
fetchHistory: () => void; fetchHistory: () => void;
} }
interface BackstoryMessage {
prompt: string;
preamble: {};
status: string;
full_content: string;
response: string; // Set when status === 'done' or 'error'
chunk: string; // Used when status === 'streaming'
metadata: {
rag: { documents: [] };
tools: string[];
eval_count: number;
eval_duration: number;
prompt_eval_count: number;
prompt_eval_duration: number;
};
actions: string[];
timestamp: string;
};
interface ConversationProps extends BackstoryElementProps { interface ConversationProps extends BackstoryElementProps {
className?: string, // Override default className className?: string, // Override default className
@ -59,36 +43,37 @@ interface ConversationProps extends BackstoryElementProps {
messageFilter?: ((messages: MessageList) => MessageList) | undefined, // Filter callback to determine which Messages to display in Conversation messageFilter?: ((messages: MessageList) => MessageList) | undefined, // Filter callback to determine which Messages to display in Conversation
messages?: MessageList, // messages?: MessageList, //
sx?: SxProps<Theme>, sx?: SxProps<Theme>,
onResponse?: ((message: MessageData) => void) | undefined, // Event called when a query completes (provides messages) onResponse?: ((message: BackstoryMessage) => void) | undefined, // Event called when a query completes (provides messages)
}; };
const Conversation = forwardRef<ConversationHandle, ConversationProps>(({ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: ConversationProps, ref) => {
actionLabel, const {
className, sessionId,
defaultPrompts, actionLabel,
defaultQuery, className,
hideDefaultPrompts, defaultPrompts,
hidePreamble, defaultQuery,
messageFilter, hideDefaultPrompts,
messages, hidePreamble,
onResponse, messageFilter,
placeholder, messages,
preamble, onResponse,
resetAction, placeholder,
resetLabel, preamble,
sessionId, resetAction,
setSnack, resetLabel,
submitQuery, setSnack,
sx, submitQuery,
type, sx,
}: ConversationProps, ref) => { type,
} = props;
const [contextUsedPercentage, setContextUsedPercentage] = useState<number>(0); const [contextUsedPercentage, setContextUsedPercentage] = useState<number>(0);
const [processing, setProcessing] = useState<boolean>(false); const [processing, setProcessing] = useState<boolean>(false);
const [countdown, setCountdown] = useState<number>(0); const [countdown, setCountdown] = useState<number>(0);
const [conversation, setConversation] = useState<MessageList>([]); const [conversation, setConversation] = useState<MessageList>([]);
const [filteredConversation, setFilteredConversation] = useState<MessageList>([]); const [filteredConversation, setFilteredConversation] = useState<MessageList>([]);
const [processingMessage, setProcessingMessage] = useState<MessageData | undefined>(undefined); const [processingMessage, setProcessingMessage] = useState<BackstoryMessage | undefined>(undefined);
const [streamingMessage, setStreamingMessage] = useState<MessageData | undefined>(undefined); const [streamingMessage, setStreamingMessage] = useState<BackstoryMessage | undefined>(undefined);
const timerRef = useRef<any>(null); const timerRef = useRef<any>(null);
const [contextStatus, setContextStatus] = useState<ContextStatus>({ context_used: 0, max_context: 0 }); const [contextStatus, setContextStatus] = useState<ContextStatus>({ context_used: 0, max_context: 0 });
const [contextWarningShown, setContextWarningShown] = useState<boolean>(false); const [contextWarningShown, setContextWarningShown] = useState<boolean>(false);
@ -96,6 +81,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
const conversationRef = useRef<MessageList>([]); const conversationRef = useRef<MessageList>([]);
const viewableElementRef = useRef<HTMLDivElement>(null); const viewableElementRef = useRef<HTMLDivElement>(null);
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null); const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
const stopRef = useRef(false);
// Keep the ref updated whenever items changes // Keep the ref updated whenever items changes
useEffect(() => { useEffect(() => {
@ -181,14 +167,25 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
const backstoryMessages: BackstoryMessage[] = messages; const backstoryMessages: BackstoryMessage[] = messages;
setConversation(backstoryMessages.flatMap((backstoryMessage: BackstoryMessage) => [{ setConversation(backstoryMessages.flatMap((backstoryMessage: BackstoryMessage) => {
role: 'user', if (backstoryMessage.status === "partial") {
content: backstoryMessage.prompt || "", return [{
}, { ...backstoryMessage,
...backstoryMessage, role: "assistant",
role: backstoryMessage.status === "done" ? "assistant" : backstoryMessage.status, content: backstoryMessage.response || "",
expanded: false,
expandable: true,
}]
}
return [{
role: 'user',
content: backstoryMessage.prompt || "",
}, {
...backstoryMessage,
role: ['done'].includes(backstoryMessage.status || "") ? "assistant" : backstoryMessage.status,
content: backstoryMessage.response || "", content: backstoryMessage.response || "",
}] as MessageList)); }] as MessageList;
}));
setNoInteractions(false); setNoInteractions(false);
} }
setProcessingMessage(undefined); setProcessingMessage(undefined);
@ -242,12 +239,15 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
}; };
const handleEnter = (value: string) => { const handleEnter = (value: string) => {
sendQuery(value); const query: Query = {
prompt: value
}
sendQuery(query);
}; };
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
submitQuery: (query: string, tunables?: QueryOptions) => { submitQuery: (query: Query) => {
sendQuery(query, tunables); sendQuery(query);
}, },
fetchHistory: () => { return fetchHistory(); } fetchHistory: () => { return fetchHistory(); }
})); }));
@ -294,20 +294,27 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
} }
}; };
const sendQuery = async (request: string, options?: QueryOptions) => { const cancelQuery = () => {
request = request.trim(); console.log("Stop query");
stopRef.current = true;
};
const sendQuery = async (query: Query) => {
query.prompt = query.prompt.trim();
// If the request was empty, a default request was provided, // If the request was empty, a default request was provided,
// and there is no prompt for the user, send the default request. // and there is no prompt for the user, send the default request.
if (!request && defaultQuery && !prompt) { if (!query.prompt && defaultQuery && !prompt) {
request = defaultQuery.trim(); query.prompt = defaultQuery.trim();
} }
// Do not send an empty request. // Do not send an empty request.
if (!request) { if (!query.prompt) {
return; return;
} }
stopRef.current = false;
setNoInteractions(false); setNoInteractions(false);
setConversation([ setConversation([
@ -315,7 +322,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
{ {
role: 'user', role: 'user',
origin: type, origin: type,
content: request, content: query.prompt,
disableCopy: true disableCopy: true
} }
]); ]);
@ -325,31 +332,16 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
try { try {
setProcessing(true); setProcessing(true);
// Create a unique ID for the processing message
const processingId = Date.now().toString();
// Add initial processing message // Add initial processing message
setProcessingMessage( setProcessingMessage(
{ role: 'status', content: 'Submitting request...', id: processingId, isProcessing: true } { role: 'status', content: 'Submitting request...', disableCopy: true }
); );
// Add a small delay to ensure React has time to update the UI // Add a small delay to ensure React has time to update the UI
await new Promise(resolve => setTimeout(resolve, 0)); await new Promise(resolve => setTimeout(resolve, 0));
// Make the fetch request with proper headers const response = await fetch(connectionBase + `/api/${type}/${sessionId}`, {
let query;
if (options) {
query = {
options: options,
prompt: request.trim()
}
} else {
query = {
prompt: request.trim()
}
}
const response = await fetch(connectionBase + `/api/chat/${sessionId}/${type}`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -379,17 +371,20 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
switch (update.status) { switch (update.status) {
case 'done': case 'done':
console.log('Done processing:', update); case 'partial':
stopCountdown(); if (update.status === 'done') stopCountdown();
setStreamingMessage(undefined); if (update.status === 'done') setStreamingMessage(undefined);
setProcessingMessage(undefined); if (update.status === 'done') setProcessingMessage(undefined);
const backstoryMessage: BackstoryMessage = update; const backstoryMessage: BackstoryMessage = update;
setConversation([ setConversation([
...conversationRef.current, { ...conversationRef.current, {
...backstoryMessage, ...backstoryMessage,
role: 'assistant', role: 'assistant',
origin: type, origin: type,
prompt: ['done', 'partial'].includes(update.status) ? update.prompt : '',
content: backstoryMessage.response || "", content: backstoryMessage.response || "",
expanded: update.status === "done" ? true : false,
expandable: update.status === "done" ? false : true,
}] as MessageList); }] as MessageList);
// Add a small delay to ensure React has time to update the UI // Add a small delay to ensure React has time to update the UI
await new Promise(resolve => setTimeout(resolve, 0)); await new Promise(resolve => setTimeout(resolve, 0));
@ -424,9 +419,9 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
// Update processing message with immediate re-render // Update processing message with immediate re-render
if (update.status === "streaming") { if (update.status === "streaming") {
streaming_response += update.chunk streaming_response += update.chunk
setStreamingMessage({ role: update.status, content: streaming_response }); setStreamingMessage({ role: update.status, content: streaming_response, disableCopy: true });
} else { } else {
setProcessingMessage({ role: update.status, content: update.response }); setProcessingMessage({ role: update.status, content: update.response, disableCopy: true });
/* Reset stream on non streaming message */ /* Reset stream on non streaming message */
streaming_response = "" streaming_response = ""
} }
@ -437,12 +432,11 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
} }
} }
while (true) { while (!stopRef.current) {
const { done, value } = await reader.read(); const { done, value } = await reader.read();
if (done) { if (done) {
break; break;
} }
const chunk = decoder.decode(value, { stream: true }); const chunk = decoder.decode(value, { stream: true });
// Process each complete line immediately // Process each complete line immediately
@ -470,26 +464,32 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
} }
} }
if (stopRef.current) {
await reader.cancel();
setProcessingMessage(undefined);
setStreamingMessage(undefined);
setSnack("Processing cancelled", "warning");
}
stopCountdown(); stopCountdown();
setProcessing(false); setProcessing(false);
stopRef.current = false;
} catch (error) { } catch (error) {
console.error('Fetch error:', error); console.error('Fetch error:', error);
setSnack("Unable to process query", "error"); setSnack("Unable to process query", "error");
setProcessingMessage({ role: 'error', content: "Unable to process query" }); setProcessingMessage({ role: 'error', content: "Unable to process query", disableCopy: true });
setTimeout(() => { setTimeout(() => {
setProcessingMessage(undefined); setProcessingMessage(undefined);
}, 5000); }, 5000);
stopRef.current = false;
setProcessing(false); setProcessing(false);
stopCountdown(); stopCountdown();
// Add a small delay to ensure React has time to update the UI return;
await new Promise(resolve => setTimeout(resolve, 0));
} }
}; };
return ( return (
<Scrollable <Scrollable
className={className || "Conversation"} className={`${className || ""} Conversation`}
autoscroll autoscroll
textFieldRef={viewableElementRef} textFieldRef={viewableElementRef}
fallbackThreshold={0.5} fallbackThreshold={0.5}
@ -559,11 +559,25 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
sx={{ m: 1, gap: 1, flexGrow: 1 }} sx={{ m: 1, gap: 1, flexGrow: 1 }}
variant="contained" variant="contained"
disabled={sessionId === undefined || processingMessage !== undefined} disabled={sessionId === undefined || processingMessage !== undefined}
onClick={() => { sendQuery((backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || ""); }}> onClick={() => { sendQuery({ prompt: (backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || "" }); }}>
{actionLabel}<SendIcon /> {actionLabel}<SendIcon />
</Button> </Button>
</span> </span>
</Tooltip> </Tooltip>
<Tooltip title="Cancel">
<span style={{ display: "flex" }}> { /* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */}
<IconButton
aria-label="cancel"
onClick={() => { cancelQuery(); }}
sx={{ display: "flex", margin: 'auto 0px' }}
size="large"
edge="start"
disabled={stopRef.current || sessionId === undefined || processing === false}
>
<CancelIcon />
</IconButton>
</span>
</Tooltip>
</Box> </Box>
</Box> </Box>
{(noInteractions || !hideDefaultPrompts) && defaultPrompts !== undefined && defaultPrompts.length && {(noInteractions || !hideDefaultPrompts) && defaultPrompts !== undefined && defaultPrompts.length &&

View File

@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Message } from './Message'; import { Message } from './Message';
import { BackstoryElementProps } from './BackstoryTab'; import { BackstoryElementProps } from './BackstoryTab';
import { connectionBase } from './Global';
interface DocumentProps extends BackstoryElementProps { interface DocumentProps extends BackstoryElementProps {
title: string; title: string;
@ -13,7 +12,12 @@ interface DocumentProps extends BackstoryElementProps {
} }
const Document = (props: DocumentProps) => { const Document = (props: DocumentProps) => {
const { setSnack, submitQuery, filepath, content, title, expanded, disableCopy, onExpand, sessionId } = props; const { sessionId, setSnack, submitQuery, filepath, content, title, expanded, disableCopy, onExpand } = props;
const backstoryProps = {
submitQuery,
setSnack,
sessionId
}
const [document, setDocument] = useState<string>(""); const [document, setDocument] = useState<string>("");
@ -55,14 +59,11 @@ const Document = (props: DocumentProps) => {
flexGrow: 0, flexGrow: 0,
}, },
message: { role: 'content', title: title, content: document || content || "" }, message: { role: 'content', title: title, content: document || content || "" },
connectionBase,
submitQuery,
setSnack,
expanded, expanded,
disableCopy, disableCopy,
onExpand, onExpand,
sessionId, }}
}} /> {...backstoryProps} />
); );
}; };

View File

@ -14,6 +14,10 @@ const HomePage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md')); const isMobile = useMediaQuery(theme.breakpoints.down('md'));
if (sessionId === undefined) {
return <></>;
}
const backstoryPreamble: MessageList = [ const backstoryPreamble: MessageList = [
{ {
role: 'content', role: 'content',
@ -32,10 +36,10 @@ What would you like to know about James?
const backstoryQuestions = [ const backstoryQuestions = [
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}> <Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
<ChatQuery prompt="What is James Ketrenos' work history?" tunables={{ enable_tools: false }} submitQuery={submitQuery} /> <ChatQuery query={{ prompt: "What is James Ketrenos' work history?", tunables: { enable_tools: false } }} submitQuery={submitQuery} />
<ChatQuery prompt="What programming languages has James used?" tunables={{ enable_tools: false }} submitQuery={submitQuery} /> <ChatQuery query={{ prompt: "Provide an exhaustive list of programming languages James has used.", tunables: { enable_tools: false } }} submitQuery={submitQuery} />
<ChatQuery prompt="What are James' professional strengths?" tunables={{ enable_tools: false }} submitQuery={submitQuery} /> <ChatQuery query={{ prompt: "What are James' professional strengths?", tunables: { enable_tools: false } }} submitQuery={submitQuery} />
<ChatQuery prompt="What are today's headlines on CNBC.com?" tunables={{ enable_tools: true, enable_rag: false, enable_context: false }} submitQuery={submitQuery} /> <ChatQuery query={{ prompt: "What are today's headlines on CNBC.com?", tunables: { enable_tools: true, enable_rag: false, enable_context: false } }} submitQuery={submitQuery} />
</Box>, </Box>,
<Box sx={{ p: 1 }}> <Box sx={{ p: 1 }}>
<MuiMarkdown> <MuiMarkdown>

View File

@ -0,0 +1,21 @@
import Box from '@mui/material/Box';
import { BackstoryPageProps } from './BackstoryTab';
import { BackstoryMessage, Message } from './Message';
const LoadingPage = (props: BackstoryPageProps) => {
const backstoryPreamble: BackstoryMessage = {
role: 'info',
title: 'Please wait while connecting to Backstory...',
disableCopy: true,
content: '...',
expandable: false,
}
return <Box sx={{display: "flex", flexGrow: 1, maxWidth: "1024px", margin: "0 auto"}}>
<Message message={backstoryPreamble} {...props} />
</Box>
};
export {
LoadingPage
};

View File

@ -88,6 +88,10 @@ button {
flex-grow: 1; flex-grow: 1;
} }
.MessageContent div > p:first-child {
margin-top: 0;
}
.MenuCard.MuiCard-root { .MenuCard.MuiCard-root {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

328
frontend/src/Main.tsx Normal file
View File

@ -0,0 +1,328 @@
import React, { useEffect, useState, useRef, useMemo } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import useMediaQuery from '@mui/material/useMediaQuery';
import Card from '@mui/material/Card';
import { styled } from '@mui/material/styles';
import Avatar from '@mui/material/Avatar';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import Tooltip from '@mui/material/Tooltip';
import AppBar from '@mui/material/AppBar';
import Drawer from '@mui/material/Drawer';
import Toolbar from '@mui/material/Toolbar';
import SettingsIcon from '@mui/icons-material/Settings';
import IconButton from '@mui/material/IconButton';
import Box from '@mui/material/Box';
import CssBaseline from '@mui/material/CssBaseline';
import MenuIcon from '@mui/icons-material/Menu';
import { ConversationHandle } from './Conversation';
import { Query } from './ChatQuery';
import { Scrollable } from './Scrollable';
import { BackstoryPage, BackstoryTabProps } from './BackstoryTab';
import { HomePage } from './HomePage';
import { LoadingPage } from './LoadingPage';
import { ResumeBuilderPage } from './ResumeBuilderPage';
import { VectorVisualizerPage } from './VectorVisualizer';
import { AboutPage } from './AboutPage';
import { ControlsPage } from './ControlsPage';
import { SetSnackType } from './Snack';
import './Main.css';
import './Conversation.css';
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
interface MainProps {
sessionId: string,
setSnack: SetSnackType
}
const Main = (props: MainProps) => {
const { sessionId } = props;
const navigate = useNavigate();
const location = useLocation();
const [menuOpen, setMenuOpen] = useState(false);
const [isMenuClosing, setIsMenuClosing] = useState(false);
const [activeTab, setActiveTab] = useState<number>(0);
const [tab, setTab] = useState<any>(undefined);
const isDesktop = useMediaQuery('(min-width:650px)');
const prevIsDesktopRef = useRef<boolean>(isDesktop);
const chatRef = useRef<ConversationHandle>(null);
const [subRoute, setSubRoute] = useState<string>("");
const backstoryProps = useMemo(() => {
const handleSubmitChatQuery = (query: Query) => {
console.log(`handleSubmitChatQuery:`, query, chatRef.current ? ' sending' : 'no handler');
chatRef.current?.submitQuery(query);
setActiveTab(0);
};
return { ...props, route: subRoute, setRoute: setSubRoute, submitQuery: handleSubmitChatQuery };
}, [props, setActiveTab, subRoute]);
useEffect(() => {
if (prevIsDesktopRef.current === isDesktop)
return;
if (menuOpen) {
setMenuOpen(false);
}
prevIsDesktopRef.current = isDesktop;
}, [isDesktop, setMenuOpen, menuOpen])
const tabs: BackstoryTabProps[] = useMemo(() => {
const homeTab: BackstoryTabProps = {
label: "",
path: "",
tabProps: {
label: "Backstory",
sx: { flexGrow: 1, fontSize: '1rem' },
icon:
<Avatar sx={{
width: 24,
height: 24
}}
variant="rounded"
alt="Backstory logo"
src="/logo192.png" />,
iconPosition: "start"
},
children: <HomePage ref={chatRef} {...backstoryProps} />
};
const loadingTab: BackstoryTabProps = {
...homeTab,
children: <LoadingPage {...backstoryProps} />
};
const resumeBuilderTab: BackstoryTabProps = {
label: "Resume Builder",
path: "resume-builder",
children: <ResumeBuilderPage {...backstoryProps} />
};
const contextVisualizerTab: BackstoryTabProps = {
label: "Context Visualizer",
path: "context-visualizer",
children: <VectorVisualizerPage sx={{ p: 1 }} {...backstoryProps} />
};
const aboutTab = {
label: "About",
path: "about",
children: <AboutPage {...backstoryProps} />
};
const controlsTab: BackstoryTabProps = {
path: "controls",
tabProps: {
sx: { flexShrink: 1, flexGrow: 0, fontSize: '1rem' },
icon: <SettingsIcon />
},
children: (
<Scrollable
autoscroll={false}
sx={{
maxWidth: "1024px",
height: "calc(100vh - 72px)",
flexDirection: "column",
margin: "0 auto",
p: 1,
}}
>
<ControlsPage {...backstoryProps} />
</Scrollable>
)
};
if (sessionId === undefined) {
return [loadingTab];
} else {
return [
homeTab,
resumeBuilderTab,
contextVisualizerTab,
aboutTab,
controlsTab,
];
}
}, [backstoryProps, sessionId]);
const handleMenuClose = () => {
setIsMenuClosing(true);
setMenuOpen(false);
};
const handleMenuTransitionEnd = () => {
setIsMenuClosing(false);
};
const handleMenuToggle = () => {
if (!isMenuClosing) {
setMenuOpen(!menuOpen);
}
};
useEffect(() => {
if (tab === undefined || tab === tabs[activeTab]) {
return;
}
setTab(tabs[activeTab]);
setSubRoute("");
}, [tabs, activeTab, tab]);
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
if (newValue > tabs.length) {
console.log(`Invalid tab requested: ${newValue}`);
return;
}
setActiveTab(newValue);
handleMenuClose();
};
useEffect(() => {
const pathParts = window.location.pathname.split('/').filter(Boolean);
const currentPath = pathParts.length < 2 ? '' : pathParts[0];
let currentSubRoute = pathParts.length > 2 ? pathParts.slice(1, -1).join('/') : '';
let tabIndex = tabs.findIndex((tab) => tab.path === currentPath);
if (tabIndex === -1) {
console.log(`Invalid path "${currentPath}" -- redirecting to default`);
tabIndex = 0
currentSubRoute = ""
}
setActiveTab(tabIndex);
setTab(tabs[tabIndex]);
setSubRoute(currentSubRoute);
console.log(`Initial load set to tab ${tabs[tabIndex].path} subRoute: ${currentSubRoute}`);
}, [tabs]);
useEffect(() => {
if (tab === undefined || sessionId === undefined) {
return;
}
let path = tab.path ? `/${tab.path}` : '';
if (subRoute) path += `/${subRoute}`;
path += `/${sessionId}`;
if (path !== location.pathname) {
console.log(`Pusing state ${path}`)
navigate(path, { replace: true });
}
}, [tab, subRoute, sessionId, navigate, location.pathname]);
/* toolbar height is 64px + 8px margin-top */
const Offset = styled('div')(() => ({ minHeight: '72px', height: '72px' }));
return (
<Box className="App"
sx={{ display: 'flex', flexDirection: 'column' }}>
<CssBaseline />
<AppBar
position="fixed"
sx={{
zIndex: (theme) => theme.zIndex.drawer + 1,
maxWidth: "100vw"
}}
>
<Toolbar>
<Box sx={{ display: "flex", flexGrow: 1, flexDirection: "row" }}>
{!isDesktop &&
<Box sx={{ display: "flex", flexGrow: 1, flexDirection: "row" }}>
<IconButton
sx={{ display: "flex", margin: 'auto 0px' }}
size="large"
edge="start"
color="inherit"
onClick={handleMenuToggle}
>
<Tooltip title="Navigation">
<MenuIcon />
</Tooltip>
</IconButton>
<Tooltip title="Backstory">
<Box
sx={{ m: 1, gap: 1, display: "flex", flexDirection: "row", alignItems: "center", fontWeight: "bold", fontSize: "1.0rem", cursor: "pointer" }}
onClick={() => { setActiveTab(0); setMenuOpen(false); }}
>
<Avatar sx={{
width: 24,
height: 24
}}
variant="rounded"
alt="Backstory logo"
src="/logo192.png" />
BACKSTORY
</Box>
</Tooltip>
</Box>
}
{menuOpen === false && isDesktop &&
<Tabs sx={{ display: "flex", flexGrow: 1 }}
value={activeTab}
indicatorColor="secondary"
textColor="inherit"
variant="fullWidth"
allowScrollButtonsMobile
onChange={handleTabChange}
aria-label="Backstory navigation">
{tabs.map((tab, index) => <Tab key={index} value={index} label={tab.label} {...tab.tabProps} />)}
</Tabs>
}
</Box>
</Toolbar>
</AppBar>
<Offset />
<Box sx={{ display: "flex", flexGrow: 1, flexDirection: "column" }} >
<Drawer
container={window.document.body}
variant="temporary"
open={menuOpen}
onTransitionEnd={handleMenuTransitionEnd}
onClose={handleMenuClose}
sx={{
display: 'block',
'& .MuiDrawer-paper': { boxSizing: 'border-box' },
}}
slotProps={{
root: {
keepMounted: true, // Better open performance on mobile.
},
}}
>
<Toolbar />
<Card className="MenuCard">
<Tabs sx={{ display: "flex", flexGrow: 1 }}
orientation="vertical"
value={activeTab}
indicatorColor="secondary"
textColor="inherit"
variant="scrollable"
allowScrollButtonsMobile
onChange={handleTabChange}
aria-label="Backstory navigation">
{tabs.map((tab, index) => <Tab key={index} value={index} label={tab.label} {...tab.tabProps} />)}
</Tabs>
</Card>
</Drawer>
{
tabs.map((tab: any, i: number) =>
<BackstoryPage key={i} active={i === activeTab} path={tab.path}>{tab.children}</BackstoryPage>
)
}
</Box>
</Box >
);
};
export {
Main
}

View File

@ -15,7 +15,6 @@ import Button from '@mui/material/Button';
import CardContent from '@mui/material/CardContent'; import CardContent from '@mui/material/CardContent';
import CardActions from '@mui/material/CardActions'; import CardActions from '@mui/material/CardActions';
import Collapse from '@mui/material/Collapse'; import Collapse from '@mui/material/Collapse';
import Typography from '@mui/material/Typography';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { ExpandMore } from './ExpandMore'; import { ExpandMore } from './ExpandMore';
import { SxProps, Theme } from '@mui/material'; import { SxProps, Theme } from '@mui/material';
@ -47,11 +46,18 @@ type MessageRoles =
'thinking' | 'thinking' |
'user'; 'user';
type MessageData = { type BackstoryMessage = {
// Only two required fields
role: MessageRoles, role: MessageRoles,
content: string, content: string,
status?: string, // streaming, done, error... // Rest are optional
response?: string, prompt?: string;
preamble?: {};
status?: string;
full_content?: string;
response?: string; // Set when status === 'done', 'partial', or 'error'
chunk?: string; // Used when status === 'streaming'
timestamp?: string;
disableCopy?: boolean, disableCopy?: boolean,
user?: string, user?: string,
title?: string, title?: string,
@ -79,16 +85,15 @@ interface MessageMetaData {
eval_duration: number, eval_duration: number,
prompt_eval_count: number, prompt_eval_count: number,
prompt_eval_duration: number, prompt_eval_duration: number,
sessionId?: string,
connectionBase: string, connectionBase: string,
setSnack: SetSnackType, setSnack: SetSnackType,
} }
type MessageList = MessageData[]; type MessageList = BackstoryMessage[];
interface MessageProps extends BackstoryElementProps { interface MessageProps extends BackstoryElementProps {
sx?: SxProps<Theme>, sx?: SxProps<Theme>,
message: MessageData, message: BackstoryMessage,
expanded?: boolean, expanded?: boolean,
onExpand?: (open: boolean) => void, onExpand?: (open: boolean) => void,
className?: string, className?: string,
@ -112,6 +117,8 @@ const MessageMeta = (props: MessageMetaProps) => {
} = props.metadata || {}; } = props.metadata || {};
const message: any = props.messageProps.message; const message: any = props.messageProps.message;
rag.forEach((r: any) => r.query = message.prompt);
let llm_submission: string = "<|system|>\n" let llm_submission: string = "<|system|>\n"
llm_submission += message.system_prompt + "\n\n" llm_submission += message.system_prompt + "\n\n"
llm_submission += message.context_prompt llm_submission += message.context_prompt
@ -184,31 +191,18 @@ const MessageMeta = (props: MessageMetaProps) => {
</Accordion> </Accordion>
} }
{ {
rag.map((rag: any) => ( rag.map((collection: any) => (
<Accordion key={rag.name}> <Accordion key={collection.name}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box sx={{ fontSize: "0.8rem" }}> <Box sx={{ fontSize: "0.8rem" }}>
Top RAG {rag.ids.length} matches from '{rag.name}' collection against embedding vector of {rag.query_embedding.length} dimensions Top {collection.ids.length} RAG matches from {collection.size} entries using an embedding vector of {collection.query_embedding.length} dimensions
</Box> </Box>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<Box sx={{ fontSize: "0.8rem" }}> <VectorVisualizer inline
UMAP Vector Visualization of '{rag.name}' RAG {...props.messageProps} {...props.metadata}
</Box> rag={collection} />
<VectorVisualizer inline {...props.messageProps} {...props.metadata} rag={rag} /> {/* { ...rag, query: message.prompt }} /> */}
{rag.ids.map((id: number, index: number) => <Box key={index}>
<Divider />
<Box sx={{ whiteSpace: "nowrap", fontSize: "0.75rem", p: 0, m: 0, pt: 0.5 }}>Doc ID: {rag.ids[index]}</Box>
<Box sx={{ fontSize: "0.75rem", display: "flex", flexDirection: "row", mb: 0.5, mt: 0.5 }}>
<div style={{ display: "flex", flexDirection: "column", paddingRight: "1rem", minWidth: "10rem" }}>
<div style={{ whiteSpace: "nowrap" }}>Distance: {Math.round(rag.distances[index] * 100) / 100}</div>
<div style={{ whiteSpace: "nowrap" }}>Type: {rag.metadatas[index].doc_type}</div>
<div style={{ whiteSpace: "nowrap" }}>Chunk Len: {rag.documents[index].length}</div>
</div>
<div style={{ display: "flex", padding: "3px", flexGrow: 1, border: "1px solid #E0E0E0", maxHeight: "5rem", overflow: "auto" }}>{rag.documents[index]}</div>
</Box>
</Box>
)}
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
)) ))
@ -237,9 +231,14 @@ const MessageMeta = (props: MessageMetaProps) => {
}; };
const Message = (props: MessageProps) => { const Message = (props: MessageProps) => {
const { message, submitQuery, sx, className, onExpand, expanded, sessionId, setSnack } = props; const { message, submitQuery, sx, className, onExpand, setSnack, sessionId, expanded } = props;
const [metaExpanded, setMetaExpanded] = useState<boolean>(false); const [metaExpanded, setMetaExpanded] = useState<boolean>(false);
const textFieldRef = useRef(null); const textFieldRef = useRef(null);
const backstoryProps = {
submitQuery,
sessionId,
setSnack
};
const handleMetaExpandClick = () => { const handleMetaExpandClick = () => {
setMetaExpanded(!metaExpanded); setMetaExpanded(!metaExpanded);
@ -254,14 +253,17 @@ const Message = (props: MessageProps) => {
return (<></>); return (<></>);
} }
const formattedContent = message.content.trim() || "Waiting for LLM to spool up..."; const formattedContent = message.content.trim();
if (formattedContent === "") {
return (<></>);
}
return ( return (
<ChatBubble <ChatBubble
className={className || "Message"} className={`${className || ""} Message Message-${message.role}`}
{...message} {...message}
onExpand={onExpand}
expanded={expanded} expanded={expanded}
onExpand={onExpand}
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
@ -273,34 +275,24 @@ const Message = (props: MessageProps) => {
...sx, ...sx,
}}> }}>
<CardContent ref={textFieldRef} sx={{ position: "relative", display: "flex", flexDirection: "column", overflowX: "auto", m: 0, p: 0, paddingBottom: '0px !important' }}> <CardContent ref={textFieldRef} sx={{ position: "relative", display: "flex", flexDirection: "column", overflowX: "auto", m: 0, p: 0, paddingBottom: '0px !important' }}>
{message.role !== 'user' ? <Scrollable
<Scrollable className="MessageContent"
className="MessageContent" autoscroll
autoscroll fallbackThreshold={0.5}
fallbackThreshold={0.5} sx={{
sx={{ p: 0,
p: 0, m: 0,
m: 0, maxHeight: (message.role === "streaming") ? "20rem" : "unset",
maxHeight: (message.role === "streaming") ? "20rem" : "unset", display: "flex",
display: "flex", flexGrow: 1,
flexGrow: 1, overflow: "auto", /* Handles scrolling for the div */
overflow: "auto", /* Handles scrolling for the div */ }}
}} >
> <StyledMarkdown streaming={message.role === "streaming"} content={formattedContent} {...backstoryProps} />
<StyledMarkdown streaming={message.role === "streaming"} {...{ content: formattedContent, submitQuery, sessionId, setSnack }} /> </Scrollable>
</Scrollable>
:
<Typography
className="MessageContent"
ref={textFieldRef}
variant="body2"
sx={{ display: "flex", color: 'text.secondary' }}>
{message.content}
</Typography>
}
</CardContent> </CardContent>
<CardActions disableSpacing sx={{ display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: "center", width: "100%", p: 0, m: 0 }}> <CardActions disableSpacing sx={{ display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: "center", width: "100%", p: 0, m: 0 }}>
{(message.disableCopy === undefined || message.disableCopy === false) && ["assistant", "content"].includes(message.role) && <CopyBubble content={message.content} />} {(message.disableCopy === undefined || message.disableCopy === false) && <CopyBubble content={message.content} />}
{message.metadata && ( {message.metadata && (
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}> <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Button variant="text" onClick={handleMetaExpandClick} sx={{ color: "darkgrey", p: 0 }}> <Button variant="text" onClick={handleMetaExpandClick} sx={{ color: "darkgrey", p: 0 }}>
@ -309,7 +301,7 @@ const Message = (props: MessageProps) => {
<ExpandMore <ExpandMore
expand={metaExpanded} expand={metaExpanded}
onClick={handleMetaExpandClick} onClick={handleMetaExpandClick}
aria-expanded={expanded} aria-expanded={message.expanded}
aria-label="show more" aria-label="show more"
> >
<ExpandMoreIcon /> <ExpandMoreIcon />
@ -331,7 +323,8 @@ const Message = (props: MessageProps) => {
export type { export type {
MessageProps, MessageProps,
MessageList, MessageList,
MessageData, BackstoryMessage,
MessageMetaData,
MessageRoles, MessageRoles,
}; };

View File

@ -6,8 +6,8 @@ import {
} from '@mui/material'; } from '@mui/material';
import { SxProps } from '@mui/material'; import { SxProps } from '@mui/material';
import { ChatQuery } from './ChatQuery'; import { ChatQuery, Query } from './ChatQuery';
import { MessageList, MessageData } from './Message'; import { MessageList, BackstoryMessage } from './Message';
import { Conversation } from './Conversation'; import { Conversation } from './Conversation';
import { BackstoryPageProps } from './BackstoryTab'; import { BackstoryPageProps } from './BackstoryTab';
@ -19,12 +19,13 @@ import './ResumeBuilderPage.css';
* A responsive component that displays job descriptions, generated resumes and fact checks * A responsive component that displays job descriptions, generated resumes and fact checks
* with different layouts for mobile and desktop views. * with different layouts for mobile and desktop views.
*/ */
const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({ const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
sx, const {
sessionId, sx,
setSnack, sessionId,
submitQuery, setSnack,
}) => { submitQuery,
} = props
// State for editing job description // State for editing job description
const [hasJobDescription, setHasJobDescription] = useState<boolean>(false); const [hasJobDescription, setHasJobDescription] = useState<boolean>(false);
const [hasResume, setHasResume] = useState<boolean>(false); const [hasResume, setHasResume] = useState<boolean>(false);
@ -42,18 +43,18 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
setActiveTab(newValue); setActiveTab(newValue);
}; };
const handleJobQuery = (query: string) => { const handleJobQuery = (query: Query) => {
console.log(`handleJobQuery: ${query} -- `, jobConversationRef.current ? ' sending' : 'no handler'); console.log(`handleJobQuery: ${query.prompt} -- `, jobConversationRef.current ? ' sending' : 'no handler');
jobConversationRef.current?.submitQuery(query); jobConversationRef.current?.submitQuery(query);
}; };
const handleResumeQuery = (query: string) => { const handleResumeQuery = (query: Query) => {
console.log(`handleResumeQuery: ${query} -- `, resumeConversationRef.current ? ' sending' : 'no handler'); console.log(`handleResumeQuery: ${query.prompt} -- `, resumeConversationRef.current ? ' sending' : 'no handler');
resumeConversationRef.current?.submitQuery(query); resumeConversationRef.current?.submitQuery(query);
}; };
const handleFactsQuery = (query: string) => { const handleFactsQuery = (query: Query) => {
console.log(`handleFactsQuery: ${query} -- `, factsConversationRef.current ? ' sending' : 'no handler'); console.log(`handleFactsQuery: ${query.prompt} -- `, factsConversationRef.current ? ' sending' : 'no handler');
factsConversationRef.current?.submitQuery(query); factsConversationRef.current?.submitQuery(query);
}; };
@ -62,11 +63,6 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
return []; return [];
} }
if (messages.length > 2) {
setHasResume(true);
setHasFacts(true);
}
if (messages.length > 0) { if (messages.length > 0) {
messages[0].role = 'content'; messages[0].role = 'content';
messages[0].title = 'Job Description'; messages[0].title = 'Job Description';
@ -74,6 +70,19 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
messages[0].expandable = true; messages[0].expandable = true;
} }
if (-1 !== messages.findIndex(m => m.status === 'done')) {
setHasResume(true);
setHasFacts(true);
}
return messages;
if (messages.length > 1) {
setHasResume(true);
setHasFacts(true);
}
if (messages.length > 3) { if (messages.length > 3) {
// messages[2] is Show job requirements // messages[2] is Show job requirements
messages[3].role = 'job-requirements'; messages[3].role = 'job-requirements';
@ -95,6 +104,8 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
return []; return [];
} }
return messages;
if (messages.length > 1) { if (messages.length > 1) {
// messages[0] is Show Qualifications // messages[0] is Show Qualifications
messages[1].role = 'qualifications'; messages[1].role = 'qualifications';
@ -139,7 +150,7 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
return filtered; return filtered;
}, []); }, []);
const jobResponse = useCallback(async (message: MessageData) => { const jobResponse = useCallback(async (message: BackstoryMessage) => {
console.log('onJobResponse', message); console.log('onJobResponse', message);
if (message.actions && message.actions.includes("job_description")) { if (message.actions && message.actions.includes("job_description")) {
await jobConversationRef.current.fetchHistory(); await jobConversationRef.current.fetchHistory();
@ -155,12 +166,12 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
} }
}, [setHasFacts, setHasResume, setActiveTab]); }, [setHasFacts, setHasResume, setActiveTab]);
const resumeResponse = useCallback((message: MessageData): void => { const resumeResponse = useCallback((message: BackstoryMessage): void => {
console.log('onResumeResponse', message); console.log('onResumeResponse', message);
setHasFacts(true); setHasFacts(true);
}, [setHasFacts]); }, [setHasFacts]);
const factsResponse = useCallback((message: MessageData): void => { const factsResponse = useCallback((message: BackstoryMessage): void => {
console.log('onFactsResponse', message); console.log('onFactsResponse', message);
}, []); }, []);
@ -183,23 +194,27 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
console.log('renderJobDescriptionView'); console.log('renderJobDescriptionView');
const jobDescriptionQuestions = [ const jobDescriptionQuestions = [
<Box sx={{ display: "flex", flexDirection: "column" }}> <Box sx={{ display: "flex", flexDirection: "column" }}>
<ChatQuery prompt="What are the key skills necessary for this position?" tunables={{ enable_tools: false }} submitQuery={handleJobQuery} /> <ChatQuery query={{ prompt: "What are the key skills necessary for this position?", tunables: { enable_tools: false } }} submitQuery={handleJobQuery} />
<ChatQuery prompt="How much should this position pay (accounting for inflation)?" tunables={{ enable_tools: false }} submitQuery={handleJobQuery} /> <ChatQuery query={{ prompt: "How much should this position pay (accounting for inflation)?", tunables: { enable_tools: false } }} submitQuery={handleJobQuery} />
</Box>, </Box>,
]; ];
const jobDescriptionPreamble: MessageList = [{ const jobDescriptionPreamble: MessageList = [{
role: 'info', role: 'info',
content: `Once you paste a job description and press **Generate Resume**, the system will perform the following actions: content: `Once you paste a job description and press **Generate Resume**, Backstory will perform the following actions:
1. **RAG**: Collects information from the RAG database relavent to the job description 1. **Job Analysis**: LLM extracts requirements from '\`Job Description\`' to generate a list of desired '\`Skills\`'.
2. **Isolated Analysis**: Three sub-stages 2. **Candidate Analysis**: LLM determines candidate qualifications by performing skill assessments.
1. **Job Analysis**: Extracts requirements from job description only
2. **Candidate Analysis**: Catalogs qualifications from resume/context only For each '\`Skill\`' from **Job Analysis** phase:
3. **Mapping Analysis**: Identifies legitimate matches between requirements and qualifications
3. **Resume Generation**: Uses mapping output to create a tailored resume with evidence-based content 1. **RAG**: Retrieval Augmented Generation collection is queried for context related content for each '\`Skill\`'.
4. **Verification**: Performs fact-checking to catch any remaining fabrications 2. **Evidence Creation**: LLM is queried to generate supporting evidence of '\`Skill\`' from the '\`RAG\`' and '\`Candidate Resume\`'.
1. **Re-generation**: If verification does not pass, a second attempt is made to correct any issues` 3. **Resume Generation**: LLM is provided the output from the **Candidate Analysis:Evidence Creation** phase and asked to generate a professional resume.
See [About > Resume Generation Architecture](/about/resume-generation) for more details.
`,
disableCopy: true
}]; }];
@ -251,8 +266,8 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
const renderResumeView = useCallback((sx: SxProps) => { const renderResumeView = useCallback((sx: SxProps) => {
const resumeQuestions = [ const resumeQuestions = [
<Box sx={{ display: "flex", flexDirection: "column" }}> <Box sx={{ display: "flex", flexDirection: "column" }}>
<ChatQuery prompt="Is this resume a good fit for the provided job description?" tunables={{ enable_tools: false }} submitQuery={handleResumeQuery} /> <ChatQuery query={{ prompt: "Is this resume a good fit for the provided job description?", tunables: { enable_tools: false } }} submitQuery={handleResumeQuery} />
<ChatQuery prompt="Provide a more concise resume." tunables={{ enable_tools: false }} submitQuery={handleResumeQuery} /> <ChatQuery query={{ prompt: "Provide a more concise resume.", tunables: { enable_tools: false } }} submitQuery={handleResumeQuery} />
</Box>, </Box>,
]; ];
@ -300,7 +315,7 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
const renderFactCheckView = useCallback((sx: SxProps) => { const renderFactCheckView = useCallback((sx: SxProps) => {
const factsQuestions = [ const factsQuestions = [
<Box sx={{ display: "flex", flexDirection: "column" }}> <Box sx={{ display: "flex", flexDirection: "column" }}>
<ChatQuery prompt="Rewrite the resume to address any discrepancies." tunables={{ enable_tools: false }} submitQuery={handleFactsQuery} /> <ChatQuery query={{ prompt: "Rewrite the resume to address any discrepancies.", tunables: { enable_tools: false } }} submitQuery={handleFactsQuery} />
</Box>, </Box>,
]; ];

Some files were not shown because too many files have changed in this diff Show More