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
#
FROM ubuntu:oracular AS python-build
FROM ubuntu:oracular AS python
SHELL [ "/bin/bash", "-c" ]
# Instructions Dockerfied from:
#
# 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
#
#
# Install some utilities frequently used
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y \
gpg \
wget \
nano \
rsync \
jq \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log}
# ipex only supports python 3.11, so use 3.11 instead of latest oracular (3.12)
# Install latest Python3
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y \
build-essential \
ca-certificates \
ccache \
cmake \
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}
python3 \
python3-pip \
python3-venv \
python3-dev
FROM ubuntu:oracular AS ze-monitor
# From https://github.com/jketreno/ze-monitor
@ -101,67 +55,13 @@ RUN cmake .. \
&& make \
&& 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:
# * python 3.11
# * pytorch xpu w/ ipex-llm
# * ollama-ipex-llm
# * src/server.py - model server supporting RAG and fine-tuned models
#
FROM ubuntu:oracular 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}
FROM python AS llm-base
# Install Intel graphics runtimes
RUN apt-get update \
@ -177,13 +77,10 @@ RUN apt-get update \
&& apt-get clean \
&& 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 \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y \
libncurses6 \
rsync \
jq \
&& apt-get clean \
&& 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
RUN { \
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 'source /opt/backstory/venv/bin/activate' ; \
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
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
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
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 peft datasets
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
#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
# 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 )
@ -245,18 +141,22 @@ RUN pip install "sentence_transformers<3.4.1"
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
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
# Install packages needed for stock.py
RUN pip install yfinance pyzt geopy PyHyphen nltk
# Install packages needed for utils/tools/*
RUN pip install yfinance pyzt geopy
# Install packages needed for vector operations
RUN pip install umap-learn
FROM llm-base AS backstory
COPY /src/requirements.txt /opt/backstory/src/requirements.txt
RUN pip install -r /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 'markitdown[all]' pydantic
# Prometheus
@ -269,7 +169,6 @@ RUN { \
echo 'echo "Container: backstory"'; \
echo 'set -e'; \
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 'source /opt/backstory/venv/bin/activate'; \
echo ''; \
@ -283,14 +182,6 @@ RUN { \
echo ' exec /bin/bash'; \
echo ' fi' ; \
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 ' 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"'; \
@ -320,11 +211,6 @@ ENV SYCL_CACHE_PERSISTENT=1
ENV PATH=/opt/backstory:$PATH
COPY /src/ /opt/backstory/src/
COPY /frontend/ /opt/backstory/frontend/
WORKDIR /opt/backstory/frontend
RUN npm install --force
WORKDIR /opt/backstory
ENTRYPOINT [ "/entrypoint.sh" ]
@ -381,7 +267,6 @@ RUN python3 -m venv --system-site-packages /opt/ollama/venv
# Setup the docker pip shell
RUN { \
echo '#!/bin/bash' ; \
update-alternatives --set python3 /opt/python/bin/python3.11 ; \
echo 'source /opt/ollama/venv/bin/activate' ; \
echo 'if [[ "${1}" != "" ]]; then bash -c ${*}; else bash; fi' ; \
} > /opt/ollama/shell ; \
@ -454,6 +339,14 @@ ENTRYPOINT [ "/entrypoint.sh" ]
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" ]
# BEGIN setup Jupyter
@ -463,9 +356,8 @@ RUN pip install \
&& jupyter lab build --dev-build=False --minimize=False
# END setup Jupyter
COPY /src/requirements.txt /opt/backstory/src/requirements.txt
RUN pip install -r /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 timm xformers
@ -483,7 +375,6 @@ RUN { \
echo ' echo "${HF_ACCESS_TOKEN}" > /root/.cache/hub/token' ; \
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 'source /opt/backstory/venv/bin/activate' ; \
echo 'if [[ "${1}" == "shell" ]]; then echo "Dropping to shell"; /bin/bash; exit $?; fi' ; \
@ -510,9 +401,7 @@ RUN { \
ENTRYPOINT [ "/entrypoint-jupyter.sh" ]
FROM ubuntu:oracular AS miniircd
COPY --from=python-build /opt/python /opt/python
FROM python AS miniircd
# Get a couple prerequisites
RUN apt-get update \
@ -526,15 +415,12 @@ RUN apt-get update \
WORKDIR /opt/miniircd
RUN update-alternatives --install /usr/bin/python3 python3 /opt/python/bin/python3.11 2
# Setup the ollama python virtual environment
RUN python3 -m venv --system-site-packages /opt/miniircd/venv
# Setup the docker pip shell
RUN { \
echo '#!/bin/bash' ; \
echo 'update-alternatives --set python3 /opt/python/bin/python3.11' ; \
echo 'source /opt/miniircd/venv/bin/activate' ; \
echo 'if [[ "${1}" != "" ]]; then bash -c "${@}"; else bash; fi' ; \
} > /opt/miniircd/shell ; \
@ -552,7 +438,6 @@ RUN { \
echo 'echo "Container: miniircd"'; \
echo 'set -e'; \
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 ''; \
echo 'if [[ "${1}" == "/bin/bash" ]] || [[ "${1}" =~ ^(/opt/miniircd/)?shell$ ]]; then'; \
@ -572,3 +457,65 @@ RUN { \
&& chmod +x /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 |
|:----------|:---------------------------------------------------------------|
| 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 |
| miniircd | Tiny deployment of an IRC server for testing IRC agents |
| ollama | Installation of Intel's pre-built Ollama.cpp |

View File

@ -6,7 +6,7 @@ services:
target: backstory
container_name: backstory
image: backstory
restart: "no"
restart: "always"
env_file:
- .env
environment:
@ -19,8 +19,7 @@ services:
networks:
- internal
ports:
- 8912:8911 # Flask React server
- 3000:3000 # REACT expo while developing frontend
- 8912:8911 # FastAPI React server
volumes:
- ./cache:/root/.cache # Persist all models and GPU kernel cache
- ./sessions:/opt/backstory/sessions:rw # Persist sessions
@ -28,7 +27,6 @@ services:
- ./dev-keys:/opt/backstory/keys:ro # Developer keys
- ./docs:/opt/backstory/docs:rw # Live mount of RAG content
- ./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_DAC_READ_SEARCH # Bypass all filesystem read access checks
- CAP_PERFMON # Access to perf_events (vs. overloaded CAP_SYS_ADMIN)
@ -54,18 +52,33 @@ services:
networks:
- internal
ports:
- 8911:8911 # Flask React server
- 8911:8911 # FastAPI React server
volumes:
- ./cache:/root/.cache # Persist all models and GPU kernel cache
- ./chromadb-prod:/opt/backstory/chromadb:rw # Persist ChromaDB
- ./sessions-prod:/opt/backstory/sessions:rw # Persist sessions
- ./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_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
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:
build:
context: .
@ -116,19 +129,6 @@ services:
volumes:
- ./jupyter:/opt/jupyter:rw
- ./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:
build:
@ -140,8 +140,6 @@ services:
restart: "no"
env_file:
- .env
devices:
- /dev/dri:/dev/dri
ports:
- 6667:6667 # IRC
networks:
@ -153,10 +151,6 @@ services:
image: prom/prometheus
container_name: prometheus
restart: "always"
# env_file:
# - .env
# devices:
# - /dev/dri:/dev/dri
ports:
- 9090:9090 # Prometheus
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",
"@uiw/react-json-view": "^2.0.0-alpha.31",
"jsonrepair": "^3.12.0",
"markdown-it": "^14.1.0",
"mermaid": "^11.6.0",
"mui-markdown": "^2.0.1",
"prism-react-renderer": "^2.4.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0",
"react-markdown-it": "^1.0.2",
"react-plotly.js": "^2.6.0",
"react-router-dom": "^7.6.0",
"react-scripts": "5.0.1",
"react-spinners": "^0.15.0",
"rehype-katex": "^7.0.1",
@ -42,6 +45,7 @@
},
"devDependencies": {
"@craco/craco": "^7.1.0",
"@types/markdown-it": "^14.1.2",
"@types/plotly.js": "^2.35.5"
}
},
@ -5090,6 +5094,12 @@
"integrity": "sha512-Gjm4+H9noDJgu5EdT3rUw5MhPBag46fiOy27BefvWkNL8mlZnKnCaVVVTLKj6RYXed9b62CPKnPav9govyQDzA==",
"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": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
@ -5112,6 +5122,16 @@
"@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": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
@ -5120,6 +5140,12 @@
"@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": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@ -11221,6 +11247,14 @@
"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": {
"version": "6.0.1",
"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",
"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": {
"version": "4.3.0",
"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_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": {
"version": "3.0.4",
"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",
"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": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -18316,6 +18395,14 @@
"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": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
@ -18609,6 +18696,67 @@
"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": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/react-plotly.js/-/react-plotly.js-2.6.0.tgz",
@ -18629,6 +18777,50 @@
"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": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@ -19781,6 +19973,11 @@
"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": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@ -21572,6 +21769,11 @@
"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": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",

View File

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

View File

@ -19,10 +19,16 @@
## 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...
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 :)

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:
1. **Stage 1: Isolated Analysis** (three sub-stages)
1. **Stage 1: Isolated Analysis**
- **1A: Job Analysis** - Extracts requirements from job description only
- **1B: Candidate Analysis** - Catalogs qualifications from resume/context only
- **1C: Mapping Analysis** - Identifies legitimate matches between requirements and qualifications
- **1B: Skill-Based Assessment** - For each required skill, determine a Individisual Skill Assessment, adding it to a Skill Assessments Collection.
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**
- Performs fact-checking to catch any remaining fabrications
@ -23,63 +22,62 @@ flowchart TD
A2 --> A3[Job Requirements JSON]
end
subgraph "Stage 1B: Candidate Analysis"
B1[Resume Input] --> B5[Candidate Analysis LLM]
B5 --> B4[Candidate Qualifications JSON]
B2[Candidate Info] --> B3[RAG]
B3[RAG] --> B2[Candidate Info]
A3[Job Requirements JSON] --> B3[RAG]
B3[RAG] --> B5
end
subgraph "Stage 1C: Mapping Analysis"
C1[Job Requirements JSON] --> C3[Mapping Analysis LLM]
C2[Candidate Qualifications JSON] --> C3
C3 --> C4[Skills Mapping JSON]
subgraph "Stage 1B: Skill-Based Assessment"
B1[Resume Input] --> B2[Candidate Info]
B2 --> B3[RAG System]
A3 --> B4[Skill Assessment Generator]
B3 --> B4
B4 --> B5{For Each Required Skill}
B5 --> B6[Skill-Focused LLM Query]
B6 --> B7[Individual Skill Assessment]
B7 --> B8[Skill Assessments Collection]
end
end
subgraph "Stage 2: Resume Generation"
D1[Skills Mapping JSON] --> D3[Resume Generation LLM]
D2[Original Resume Reference] --> D3
D3 --> D4[Tailored Resume Draft]
C1[Skill Assessments Collection] --> C2[Resume Generator]
C3[Original Resume Reference] --> C2
C4[Candidate Information] --> C2
C2 --> C5[Resume Generation Prompt]
C5 --> C6[Resume Generation LLM]
C6 --> C7[Tailored Resume Draft]
end
subgraph "Stage 3: Verification"
E1[Skills Mapping JSON] --> E2[Original Materials]
E2 --> E3[Tailored Resume Draft]
E3 --> E4[Verification LLM]
E4 --> E5{Verification Check}
E5 -->|PASS| E6[Approved Resume]
E5 -->|FAIL| E7[Correction Instructions]
E7 --> D3
subgraph "Stage 3: Statistics & Verification"
D1[Job Requirements JSON] --> D2[Match Statistics Calculator]
D3[Skill Assessments Collection] --> D2
D2 --> D4[Match Statistics]
D4 --> D5[Verification LLM]
C7 --> D5
D5 --> D6{Verification Check}
D6 -->|PASS| D7[Approved Resume]
D6 -->|FAIL| D8[Correction Instructions]
D8 --> C2
end
A3 --> C1
B4 --> C2
C4 --> D1
C4 --> E1
D4 --> E3
A3 --> B4
B8 --> C1
B8 --> D3
B1 --> C3
style A2 fill:#f9d77e,stroke:#333,stroke-width:2px
style B5 fill:#f9d77e,stroke:#333,stroke-width:2px
style C3 fill:#f9d77e,stroke:#333,stroke-width:2px
style D3 fill:#f9d77e,stroke:#333,stroke-width:2px
style E4 fill:#f9d77e,stroke:#333,stroke-width:2px
style E5 fill:#a3e4d7,stroke:#333,stroke-width:2px
style E6 fill:#aed6f1,stroke:#333,stroke-width:2px
style E7 fill:#f5b7b1,stroke:#333,stroke-width:2px
style B6 fill:#f9d77e,stroke:#333,stroke-width:2px
style C6 fill:#f9d77e,stroke:#333,stroke-width:2px
style D5 fill:#f9d77e,stroke:#333,stroke-width:2px
style B5 fill:#a3e4d7,stroke:#333,stroke-width:2px
style D6 fill:#a3e4d7,stroke:#333,stroke-width:2px
style D7 fill:#aed6f1,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
2. **Candidate Analysis**: Catalogs qualifications from just the resume/context
3. **Mapping Analysis**: Identifies legitimate matches between requirements and qualifications
2. **Candidate Analysis**: Catalogs qualifications for each job requirement from just the resume/context
## 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
@ -90,7 +88,7 @@ Creates a tailored resume using only verified information from the mapping
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.
* **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.

View File

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

View File

@ -1,402 +1,40 @@
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
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 React, { useRef, useCallback } from 'react';
import { BrowserRouter as Router, Routes, Route, useLocation } from "react-router-dom";
import { SessionWrapper } from "./SessionWrapper";
import { Main } from "./Main";
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';
import { ResumeBuilderPage } from './ResumeBuilderPage';
import { VectorVisualizerPage } from './VectorVisualizer';
import { AboutPage } from './AboutPage';
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);
return (
<SessionWrapper setSnack={setSnack}>
<Main setSnack={setSnack} sessionId={sessionId} />
</SessionWrapper>
);
}
const App = () => {
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
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>("");
function App() {
const snackRef = useRef<any>(null);
useEffect(() => {
if (prevIsDesktopRef.current === isDesktop)
return;
if (menuOpen) {
setMenuOpen(false);
}
prevIsDesktopRef.current = isDesktop;
}, [isDesktop, setMenuOpen, menuOpen])
const setSnack = useCallback((message: string, severity?: SeverityType) => {
snackRef.current?.setSnack(message, severity);
}, [snackRef]);
const setSnack = useCallback((message: string, severity?: SeverityType) => {
snackRef.current?.setSnack(message, severity);
}, [snackRef]);
return (
<>
<Router>
<Routes>
<Route path="*" element={<PathRouter setSnack={setSnack} />} />
</Routes>
</Router>
<Snack
ref={snackRef}
/>
</>
);
}
const handleSubmitChatQuery = (prompt: string, tunables?: QueryOptions) => {
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;
export default App;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,48 +1,32 @@
import React, { useState, useImperativeHandle, forwardRef, useEffect, useRef, useCallback } from 'react';
import Typography from '@mui/material/Typography';
import Tooltip from '@mui/material/Tooltip';
import IconButton from '@mui/material/IconButton';
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
import SendIcon from '@mui/icons-material/Send';
import CancelIcon from '@mui/icons-material/Cancel';
import { SxProps, Theme } from '@mui/material';
import PropagateLoader from "react-spinners/PropagateLoader";
import { Message, MessageList, MessageData } from './Message';
import { Message, MessageList, BackstoryMessage } from './Message';
import { ContextStatus } from './ContextStatus';
import { Scrollable } from './Scrollable';
import { DeleteConfirmation } from './DeleteConfirmation';
import { QueryOptions } from './ChatQuery';
import { Query } from './ChatQuery';
import './Conversation.css';
import { BackstoryTextField, BackstoryTextFieldRef } from './BackstoryTextField';
import { BackstoryElementProps } from './BackstoryTab';
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';
interface ConversationHandle {
submitQuery: (prompt: string, options?: QueryOptions) => void;
submitQuery: (query: Query) => 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 {
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
messages?: MessageList, //
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>(({
actionLabel,
className,
defaultPrompts,
defaultQuery,
hideDefaultPrompts,
hidePreamble,
messageFilter,
messages,
onResponse,
placeholder,
preamble,
resetAction,
resetLabel,
sessionId,
setSnack,
submitQuery,
sx,
type,
}: ConversationProps, ref) => {
const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: ConversationProps, ref) => {
const {
sessionId,
actionLabel,
className,
defaultPrompts,
defaultQuery,
hideDefaultPrompts,
hidePreamble,
messageFilter,
messages,
onResponse,
placeholder,
preamble,
resetAction,
resetLabel,
setSnack,
submitQuery,
sx,
type,
} = props;
const [contextUsedPercentage, setContextUsedPercentage] = useState<number>(0);
const [processing, setProcessing] = useState<boolean>(false);
const [countdown, setCountdown] = useState<number>(0);
const [conversation, setConversation] = useState<MessageList>([]);
const [filteredConversation, setFilteredConversation] = useState<MessageList>([]);
const [processingMessage, setProcessingMessage] = useState<MessageData | undefined>(undefined);
const [streamingMessage, setStreamingMessage] = useState<MessageData | undefined>(undefined);
const [processingMessage, setProcessingMessage] = useState<BackstoryMessage | undefined>(undefined);
const [streamingMessage, setStreamingMessage] = useState<BackstoryMessage | undefined>(undefined);
const timerRef = useRef<any>(null);
const [contextStatus, setContextStatus] = useState<ContextStatus>({ context_used: 0, max_context: 0 });
const [contextWarningShown, setContextWarningShown] = useState<boolean>(false);
@ -96,6 +81,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
const conversationRef = useRef<MessageList>([]);
const viewableElementRef = useRef<HTMLDivElement>(null);
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
const stopRef = useRef(false);
// Keep the ref updated whenever items changes
useEffect(() => {
@ -181,14 +167,25 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
const backstoryMessages: BackstoryMessage[] = messages;
setConversation(backstoryMessages.flatMap((backstoryMessage: BackstoryMessage) => [{
role: 'user',
content: backstoryMessage.prompt || "",
}, {
...backstoryMessage,
role: backstoryMessage.status === "done" ? "assistant" : backstoryMessage.status,
setConversation(backstoryMessages.flatMap((backstoryMessage: BackstoryMessage) => {
if (backstoryMessage.status === "partial") {
return [{
...backstoryMessage,
role: "assistant",
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 || "",
}] as MessageList));
}] as MessageList;
}));
setNoInteractions(false);
}
setProcessingMessage(undefined);
@ -242,12 +239,15 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
};
const handleEnter = (value: string) => {
sendQuery(value);
const query: Query = {
prompt: value
}
sendQuery(query);
};
useImperativeHandle(ref, () => ({
submitQuery: (query: string, tunables?: QueryOptions) => {
sendQuery(query, tunables);
submitQuery: (query: Query) => {
sendQuery(query);
},
fetchHistory: () => { return fetchHistory(); }
}));
@ -294,20 +294,27 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
}
};
const sendQuery = async (request: string, options?: QueryOptions) => {
request = request.trim();
const cancelQuery = () => {
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,
// and there is no prompt for the user, send the default request.
if (!request && defaultQuery && !prompt) {
request = defaultQuery.trim();
if (!query.prompt && defaultQuery && !prompt) {
query.prompt = defaultQuery.trim();
}
// Do not send an empty request.
if (!request) {
if (!query.prompt) {
return;
}
stopRef.current = false;
setNoInteractions(false);
setConversation([
@ -315,7 +322,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
{
role: 'user',
origin: type,
content: request,
content: query.prompt,
disableCopy: true
}
]);
@ -325,31 +332,16 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
try {
setProcessing(true);
// Create a unique ID for the processing message
const processingId = Date.now().toString();
// Add initial processing message
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
await new Promise(resolve => setTimeout(resolve, 0));
// Make the fetch request with proper headers
let query;
if (options) {
query = {
options: options,
prompt: request.trim()
}
} else {
query = {
prompt: request.trim()
}
}
const response = await fetch(connectionBase + `/api/chat/${sessionId}/${type}`, {
const response = await fetch(connectionBase + `/api/${type}/${sessionId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -379,17 +371,20 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
switch (update.status) {
case 'done':
console.log('Done processing:', update);
stopCountdown();
setStreamingMessage(undefined);
setProcessingMessage(undefined);
case 'partial':
if (update.status === 'done') stopCountdown();
if (update.status === 'done') setStreamingMessage(undefined);
if (update.status === 'done') setProcessingMessage(undefined);
const backstoryMessage: BackstoryMessage = update;
setConversation([
...conversationRef.current, {
...backstoryMessage,
role: 'assistant',
origin: type,
prompt: ['done', 'partial'].includes(update.status) ? update.prompt : '',
content: backstoryMessage.response || "",
expanded: update.status === "done" ? true : false,
expandable: update.status === "done" ? false : true,
}] as MessageList);
// Add a small delay to ensure React has time to update the UI
await new Promise(resolve => setTimeout(resolve, 0));
@ -424,9 +419,9 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
// Update processing message with immediate re-render
if (update.status === "streaming") {
streaming_response += update.chunk
setStreamingMessage({ role: update.status, content: streaming_response });
setStreamingMessage({ role: update.status, content: streaming_response, disableCopy: true });
} else {
setProcessingMessage({ role: update.status, content: update.response });
setProcessingMessage({ role: update.status, content: update.response, disableCopy: true });
/* Reset stream on non streaming message */
streaming_response = ""
}
@ -437,12 +432,11 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
}
}
while (true) {
while (!stopRef.current) {
const { done, value } = await reader.read();
if (done) {
break;
}
const chunk = decoder.decode(value, { stream: true });
// 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();
setProcessing(false);
stopRef.current = false;
} catch (error) {
console.error('Fetch error:', 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(() => {
setProcessingMessage(undefined);
}, 5000);
stopRef.current = false;
setProcessing(false);
stopCountdown();
// Add a small delay to ensure React has time to update the UI
await new Promise(resolve => setTimeout(resolve, 0));
return;
}
};
return (
<Scrollable
className={className || "Conversation"}
className={`${className || ""} Conversation`}
autoscroll
textFieldRef={viewableElementRef}
fallbackThreshold={0.5}
@ -559,11 +559,25 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
sx={{ m: 1, gap: 1, flexGrow: 1 }}
variant="contained"
disabled={sessionId === undefined || processingMessage !== undefined}
onClick={() => { sendQuery((backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || ""); }}>
onClick={() => { sendQuery({ prompt: (backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || "" }); }}>
{actionLabel}<SendIcon />
</Button>
</span>
</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>
{(noInteractions || !hideDefaultPrompts) && defaultPrompts !== undefined && defaultPrompts.length &&

View File

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

View File

@ -14,6 +14,10 @@ const HomePage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
if (sessionId === undefined) {
return <></>;
}
const backstoryPreamble: MessageList = [
{
role: 'content',
@ -32,10 +36,10 @@ What would you like to know about James?
const backstoryQuestions = [
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
<ChatQuery 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 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 is James Ketrenos' work history?", 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 query={{ prompt: "What are James' professional strengths?", tunables: { enable_tools: 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 sx={{ p: 1 }}>
<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;
}
.MessageContent div > p:first-child {
margin-top: 0;
}
.MenuCard.MuiCard-root {
display: flex;
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 CardActions from '@mui/material/CardActions';
import Collapse from '@mui/material/Collapse';
import Typography from '@mui/material/Typography';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { ExpandMore } from './ExpandMore';
import { SxProps, Theme } from '@mui/material';
@ -47,11 +46,18 @@ type MessageRoles =
'thinking' |
'user';
type MessageData = {
type BackstoryMessage = {
// Only two required fields
role: MessageRoles,
content: string,
status?: string, // streaming, done, error...
response?: string,
// Rest are optional
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,
user?: string,
title?: string,
@ -79,16 +85,15 @@ interface MessageMetaData {
eval_duration: number,
prompt_eval_count: number,
prompt_eval_duration: number,
sessionId?: string,
connectionBase: string,
setSnack: SetSnackType,
}
type MessageList = MessageData[];
type MessageList = BackstoryMessage[];
interface MessageProps extends BackstoryElementProps {
sx?: SxProps<Theme>,
message: MessageData,
message: BackstoryMessage,
expanded?: boolean,
onExpand?: (open: boolean) => void,
className?: string,
@ -112,6 +117,8 @@ const MessageMeta = (props: MessageMetaProps) => {
} = props.metadata || {};
const message: any = props.messageProps.message;
rag.forEach((r: any) => r.query = message.prompt);
let llm_submission: string = "<|system|>\n"
llm_submission += message.system_prompt + "\n\n"
llm_submission += message.context_prompt
@ -184,31 +191,18 @@ const MessageMeta = (props: MessageMetaProps) => {
</Accordion>
}
{
rag.map((rag: any) => (
<Accordion key={rag.name}>
rag.map((collection: any) => (
<Accordion key={collection.name}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<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>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ fontSize: "0.8rem" }}>
UMAP Vector Visualization of '{rag.name}' RAG
</Box>
<VectorVisualizer inline {...props.messageProps} {...props.metadata} rag={rag} />
{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>
)}
<VectorVisualizer inline
{...props.messageProps} {...props.metadata}
rag={collection} />
{/* { ...rag, query: message.prompt }} /> */}
</AccordionDetails>
</Accordion>
))
@ -237,9 +231,14 @@ const MessageMeta = (props: MessageMetaProps) => {
};
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 textFieldRef = useRef(null);
const backstoryProps = {
submitQuery,
sessionId,
setSnack
};
const handleMetaExpandClick = () => {
setMetaExpanded(!metaExpanded);
@ -254,14 +253,17 @@ const Message = (props: MessageProps) => {
return (<></>);
}
const formattedContent = message.content.trim() || "Waiting for LLM to spool up...";
const formattedContent = message.content.trim();
if (formattedContent === "") {
return (<></>);
}
return (
<ChatBubble
className={className || "Message"}
className={`${className || ""} Message Message-${message.role}`}
{...message}
onExpand={onExpand}
expanded={expanded}
onExpand={onExpand}
sx={{
display: "flex",
flexDirection: "column",
@ -273,34 +275,24 @@ const Message = (props: MessageProps) => {
...sx,
}}>
<CardContent ref={textFieldRef} sx={{ position: "relative", display: "flex", flexDirection: "column", overflowX: "auto", m: 0, p: 0, paddingBottom: '0px !important' }}>
{message.role !== 'user' ?
<Scrollable
className="MessageContent"
autoscroll
fallbackThreshold={0.5}
sx={{
p: 0,
m: 0,
maxHeight: (message.role === "streaming") ? "20rem" : "unset",
display: "flex",
flexGrow: 1,
overflow: "auto", /* Handles scrolling for the div */
}}
>
<StyledMarkdown streaming={message.role === "streaming"} {...{ content: formattedContent, submitQuery, sessionId, setSnack }} />
</Scrollable>
:
<Typography
className="MessageContent"
ref={textFieldRef}
variant="body2"
sx={{ display: "flex", color: 'text.secondary' }}>
{message.content}
</Typography>
}
<Scrollable
className="MessageContent"
autoscroll
fallbackThreshold={0.5}
sx={{
p: 0,
m: 0,
maxHeight: (message.role === "streaming") ? "20rem" : "unset",
display: "flex",
flexGrow: 1,
overflow: "auto", /* Handles scrolling for the div */
}}
>
<StyledMarkdown streaming={message.role === "streaming"} content={formattedContent} {...backstoryProps} />
</Scrollable>
</CardContent>
<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 && (
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Button variant="text" onClick={handleMetaExpandClick} sx={{ color: "darkgrey", p: 0 }}>
@ -309,7 +301,7 @@ const Message = (props: MessageProps) => {
<ExpandMore
expand={metaExpanded}
onClick={handleMetaExpandClick}
aria-expanded={expanded}
aria-expanded={message.expanded}
aria-label="show more"
>
<ExpandMoreIcon />
@ -331,7 +323,8 @@ const Message = (props: MessageProps) => {
export type {
MessageProps,
MessageList,
MessageData,
BackstoryMessage,
MessageMetaData,
MessageRoles,
};

View File

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

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