Compare commits

..

No commits in common. "538caba9f4d9f24d96f6c4995f6807460cbfff7d" and "0216515492318a2f1953a223f73ea1c0e329345c" have entirely different histories.

31 changed files with 697 additions and 727 deletions

View File

@ -359,8 +359,8 @@ WORKDIR /opt/ollama
#ENV OLLAMA_VERSION=https://github.com/intel/ipex-llm/releases/download/v2.3.0-nightly/ollama-ipex-llm-2.3.0b20250415-ubuntu.tgz #ENV OLLAMA_VERSION=https://github.com/intel/ipex-llm/releases/download/v2.3.0-nightly/ollama-ipex-llm-2.3.0b20250415-ubuntu.tgz
# NOTE: NO longer at github.com/intel -- now at ipex-llm # NOTE: NO longer at github.com/intel -- now at ipex-llm
ENV OLLAMA_VERSION=https://github.com/ipex-llm/ipex-llm/releases/download/v2.2.0/ollama-ipex-llm-2.2.0-ubuntu.tgz #ENV OLLAMA_VERSION=https://github.com/ipex-llm/ipex-llm/releases/download/v2.2.0/ollama-ipex-llm-2.2.0-ubuntu.tgz
#ENV OLLAMA_VERSION=https://github.com/ipex-llm/ipex-llm/releases/download/v2.3.0-nightly/ollama-ipex-llm-2.3.0b20250429-ubuntu.tgz ENV OLLAMA_VERSION=https://github.com/ipex-llm/ipex-llm/releases/download/v2.3.0-nightly/ollama-ipex-llm-2.3.0b20250429-ubuntu.tgz
RUN wget -qO - ${OLLAMA_VERSION} | \ RUN wget -qO - ${OLLAMA_VERSION} | \
tar --strip-components=1 -C . -xzv tar --strip-components=1 -C . -xzv

View File

@ -180,22 +180,22 @@ services:
- ./cache/grafana:/var/lib/grafana - ./cache/grafana:/var/lib/grafana
# loki: loki:
# image: grafana/loki image: grafana/loki
# container_name: loki container_name: loki
# restart: "always" restart: "always"
# # env_file: # env_file:
# # - .env.grafana # - .env.grafana
# ports: ports:
# - 3211:3100 # Grafana - 3211:3100 # Grafana
# networks: networks:
# - internal - internal
# command: command:
# - -config.file=/loki-config.yaml - -config.file=/loki-config.yaml
# volumes: volumes:
# # - ./prometheus.yml:/etc/prometheus/prometheus.yml # - ./prometheus.yml:/etc/prometheus/prometheus.yml
# - ./loki-config.yaml:/loki-config.yaml - ./loki-config.yaml:/loki-config.yaml
# - ./cache/loki:/loki - ./cache/loki:/loki
networks: networks:

View File

@ -1,46 +1,8 @@
{ {
"files": { "files": {
"main.css": "/static/css/main.3c3ddc39.css", "main.css": "/static/css/main.8e56f513.css",
"main.js": "/static/js/main.b1c48cdf.js", "main.js": "/static/js/main.6f07f748.js",
"static/js/732.b7e64c48.chunk.js": "/static/js/732.b7e64c48.chunk.js", "static/js/453.ec6f47ad.chunk.js": "/static/js/453.ec6f47ad.chunk.js",
"static/js/467.ed8fe19b.chunk.js": "/static/js/467.ed8fe19b.chunk.js",
"static/js/761.64498d2a.chunk.js": "/static/js/761.64498d2a.chunk.js",
"static/js/358.7641aa7d.chunk.js": "/static/js/358.7641aa7d.chunk.js",
"static/js/448.0ebff170.chunk.js": "/static/js/448.0ebff170.chunk.js",
"static/js/461.dcde3ae6.chunk.js": "/static/js/461.dcde3ae6.chunk.js",
"static/js/109.fea37f16.chunk.js": "/static/js/109.fea37f16.chunk.js",
"static/js/203.e56567f8.chunk.js": "/static/js/203.e56567f8.chunk.js",
"static/js/621.a72de2cb.chunk.js": "/static/js/621.a72de2cb.chunk.js",
"static/js/320.f30ff8c0.chunk.js": "/static/js/320.f30ff8c0.chunk.js",
"static/js/314.5b42713c.chunk.js": "/static/js/314.5b42713c.chunk.js",
"static/js/972.19d4b287.chunk.js": "/static/js/972.19d4b287.chunk.js",
"static/js/126.9837c9af.chunk.js": "/static/js/126.9837c9af.chunk.js",
"static/js/147.660f25b1.chunk.js": "/static/js/147.660f25b1.chunk.js",
"static/js/807.a9b3c2ae.chunk.js": "/static/js/807.a9b3c2ae.chunk.js",
"static/js/859.88148fa8.chunk.js": "/static/js/859.88148fa8.chunk.js",
"static/js/282.876bf6b3.chunk.js": "/static/js/282.876bf6b3.chunk.js",
"static/js/255.28a7c83e.chunk.js": "/static/js/255.28a7c83e.chunk.js",
"static/js/922.10b19c61.chunk.js": "/static/js/922.10b19c61.chunk.js",
"static/js/370.721ed12b.chunk.js": "/static/js/370.721ed12b.chunk.js",
"static/js/674.cb2cf2f8.chunk.js": "/static/js/674.cb2cf2f8.chunk.js",
"static/js/622.95b51007.chunk.js": "/static/js/622.95b51007.chunk.js",
"static/js/824.71eb7c8f.chunk.js": "/static/js/824.71eb7c8f.chunk.js",
"static/js/415.d031fb39.chunk.js": "/static/js/415.d031fb39.chunk.js",
"static/js/349.dfb2510e.chunk.js": "/static/js/349.dfb2510e.chunk.js",
"static/js/626.6df36496.chunk.js": "/static/js/626.6df36496.chunk.js",
"static/js/453.6fae039d.chunk.js": "/static/js/453.6fae039d.chunk.js",
"static/js/372.1ba03aa4.chunk.js": "/static/js/372.1ba03aa4.chunk.js",
"static/js/974.879246cb.chunk.js": "/static/js/974.879246cb.chunk.js",
"static/js/914.0546aa7a.chunk.js": "/static/js/914.0546aa7a.chunk.js",
"static/js/987.378e3c51.chunk.js": "/static/js/987.378e3c51.chunk.js",
"static/js/318.239ef60c.chunk.js": "/static/js/318.239ef60c.chunk.js",
"static/js/929.7d5d6402.chunk.js": "/static/js/929.7d5d6402.chunk.js",
"static/js/854.2162dcf9.chunk.js": "/static/js/854.2162dcf9.chunk.js",
"static/js/502.89ac9055.chunk.js": "/static/js/502.89ac9055.chunk.js",
"static/js/646.3df37337.chunk.js": "/static/js/646.3df37337.chunk.js",
"static/js/62.785a92ce.chunk.js": "/static/js/62.785a92ce.chunk.js",
"static/js/35.b34bde96.chunk.js": "/static/js/35.b34bde96.chunk.js",
"static/js/355.143eaed8.chunk.js": "/static/js/355.143eaed8.chunk.js",
"static/media/roboto-latin-700-normal.woff2": "/static/media/roboto-latin-700-normal.c4d6cab43bec89049809.woff2", "static/media/roboto-latin-700-normal.woff2": "/static/media/roboto-latin-700-normal.c4d6cab43bec89049809.woff2",
"static/media/roboto-latin-500-normal.woff2": "/static/media/roboto-latin-500-normal.599f66a60bdf974e578e.woff2", "static/media/roboto-latin-500-normal.woff2": "/static/media/roboto-latin-500-normal.599f66a60bdf974e578e.woff2",
"static/media/roboto-latin-300-normal.woff2": "/static/media/roboto-latin-300-normal.db56943a88e4852343ae.woff2", "static/media/roboto-latin-300-normal.woff2": "/static/media/roboto-latin-300-normal.db56943a88e4852343ae.woff2",
@ -114,44 +76,12 @@
"static/media/roboto-greek-ext-500-normal.woff": "/static/media/roboto-greek-ext-500-normal.1964239c2800b6bd7e39.woff", "static/media/roboto-greek-ext-500-normal.woff": "/static/media/roboto-greek-ext-500-normal.1964239c2800b6bd7e39.woff",
"static/media/roboto-greek-ext-300-normal.woff": "/static/media/roboto-greek-ext-300-normal.60729cafbded24073dfb.woff", "static/media/roboto-greek-ext-300-normal.woff": "/static/media/roboto-greek-ext-300-normal.60729cafbded24073dfb.woff",
"index.html": "/index.html", "index.html": "/index.html",
"main.3c3ddc39.css.map": "/static/css/main.3c3ddc39.css.map", "main.8e56f513.css.map": "/static/css/main.8e56f513.css.map",
"main.b1c48cdf.js.map": "/static/js/main.b1c48cdf.js.map", "main.6f07f748.js.map": "/static/js/main.6f07f748.js.map",
"732.b7e64c48.chunk.js.map": "/static/js/732.b7e64c48.chunk.js.map", "453.ec6f47ad.chunk.js.map": "/static/js/453.ec6f47ad.chunk.js.map"
"467.ed8fe19b.chunk.js.map": "/static/js/467.ed8fe19b.chunk.js.map",
"761.64498d2a.chunk.js.map": "/static/js/761.64498d2a.chunk.js.map",
"358.7641aa7d.chunk.js.map": "/static/js/358.7641aa7d.chunk.js.map",
"448.0ebff170.chunk.js.map": "/static/js/448.0ebff170.chunk.js.map",
"461.dcde3ae6.chunk.js.map": "/static/js/461.dcde3ae6.chunk.js.map",
"109.fea37f16.chunk.js.map": "/static/js/109.fea37f16.chunk.js.map",
"203.e56567f8.chunk.js.map": "/static/js/203.e56567f8.chunk.js.map",
"621.a72de2cb.chunk.js.map": "/static/js/621.a72de2cb.chunk.js.map",
"320.f30ff8c0.chunk.js.map": "/static/js/320.f30ff8c0.chunk.js.map",
"314.5b42713c.chunk.js.map": "/static/js/314.5b42713c.chunk.js.map",
"972.19d4b287.chunk.js.map": "/static/js/972.19d4b287.chunk.js.map",
"126.9837c9af.chunk.js.map": "/static/js/126.9837c9af.chunk.js.map",
"147.660f25b1.chunk.js.map": "/static/js/147.660f25b1.chunk.js.map",
"807.a9b3c2ae.chunk.js.map": "/static/js/807.a9b3c2ae.chunk.js.map",
"859.88148fa8.chunk.js.map": "/static/js/859.88148fa8.chunk.js.map",
"282.876bf6b3.chunk.js.map": "/static/js/282.876bf6b3.chunk.js.map",
"255.28a7c83e.chunk.js.map": "/static/js/255.28a7c83e.chunk.js.map",
"922.10b19c61.chunk.js.map": "/static/js/922.10b19c61.chunk.js.map",
"370.721ed12b.chunk.js.map": "/static/js/370.721ed12b.chunk.js.map",
"674.cb2cf2f8.chunk.js.map": "/static/js/674.cb2cf2f8.chunk.js.map",
"622.95b51007.chunk.js.map": "/static/js/622.95b51007.chunk.js.map",
"824.71eb7c8f.chunk.js.map": "/static/js/824.71eb7c8f.chunk.js.map",
"415.d031fb39.chunk.js.map": "/static/js/415.d031fb39.chunk.js.map",
"349.dfb2510e.chunk.js.map": "/static/js/349.dfb2510e.chunk.js.map",
"626.6df36496.chunk.js.map": "/static/js/626.6df36496.chunk.js.map",
"453.6fae039d.chunk.js.map": "/static/js/453.6fae039d.chunk.js.map",
"854.2162dcf9.chunk.js.map": "/static/js/854.2162dcf9.chunk.js.map",
"502.89ac9055.chunk.js.map": "/static/js/502.89ac9055.chunk.js.map",
"646.3df37337.chunk.js.map": "/static/js/646.3df37337.chunk.js.map",
"62.785a92ce.chunk.js.map": "/static/js/62.785a92ce.chunk.js.map",
"35.b34bde96.chunk.js.map": "/static/js/35.b34bde96.chunk.js.map",
"355.143eaed8.chunk.js.map": "/static/js/355.143eaed8.chunk.js.map"
}, },
"entrypoints": [ "entrypoints": [
"static/css/main.3c3ddc39.css", "static/css/main.8e56f513.css",
"static/js/main.b1c48cdf.js" "static/js/main.6f07f748.js"
] ]
} }

View File

@ -1,21 +0,0 @@
Backstory is developed using:
## Frontend
* React
* MUI
* Plotly.js
* MuiMarkdown
* Mermaid
## Backend
* Python
* FastAPI
* HuggingFace Transformers
* Ollama
* Backstory Agent Framework
* Prometheus
* Grafana
* ze-monitor
* Jupyter Notebook

View File

@ -1,11 +1,15 @@
## Backstory is three things The backstory about Backstory...
1. **An interactive Q&A** -- let potential employers ask questions about an individual's work history (aka "Backstory".) Based on the content the job seeker has provided to the RAG system, that can provide insights into that individual's resume and curriculum vitae that are often left out when people are trying to fit everything onto one page. ## Backstory is two things
1. An interactive Q&A -- let potential employers ask questions about an individual's work history (aka "Backstory".) Based on the content the job seeker has provided to the RAG system, that can provide insights into that individual's resume and curriculum vitae that are often left out when people are trying to fit everything onto one page.
2. **A resume builder** -- if you have a job position, and you think this person might be a candidate, paste your job description and have a resume produced based on their data. If it looks interesting, reach out to them. If not, hopefully you've gained some insight into what drives them. 2. A resume builder -- if you have a job position, and you think this person might be a candidate, paste your job description and have a resume produced based on their data. If it looks interesting, reach out to them. If not, hopefully you've gained some insight into what drives them.
3. **A curated expert about you** -- as a potential job seeker, you can self host this environment and generate resumes for yourself. -or-
2. A curated expert about you -- as a potential job seeker, you can self host this environment and generate resumes for yourself.
While this project was generally built for self-hosting with open source models, you can use any of the frontier models. The API adapters in this project can be configured to use infrastructure hosted from Anthropic, Google, Grok, and OpenAI (alphabetical.) For information, see [https://github.com/jketreno/backstory/README.md](https://github.com/jketreno/backstory/README.md#Frontier_Models). While this project was generally built for self-hosting with open source models, you can use any of the frontier models. The API adapters in this project can be configured to use infrastructure hosted from Anthropic, Google, Grok, and OpenAI (alphabetical.) For information, see [https://github.com/jketreno/backstory/README.md](https://github.com/jketreno/backstory/README.md#Frontier_Models).

View File

@ -1,100 +0,0 @@
The system follows a carefully designed pipeline with isolated stages to prevent fabrication:
## System Architecture Overview
The system uses a pipeline of isolated analysis and generation steps:
1. **Stage 1: Isolated Analysis** (three sub-stages)
- **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
2. **Stage 2: Resume Generation**
- Uses mapping output to create a tailored resume with evidence-based content
3. **Stage 3: Verification**
- Performs fact-checking to catch any remaining fabrications
```mermaid
flowchart TD
subgraph "Stage 1: Isolated Analysis"
subgraph "Stage 1A: Job Analysis"
A1[Job Description Input] --> A2[Job Analysis LLM]
A2 --> A3[Job Requirements JSON]
end
subgraph "Stage 1B: Candidate Analysis"
B1[Resume & Context Input] --> B2[Candidate Analysis LLM]
B2 --> B3[Candidate Qualifications JSON]
end
subgraph "Stage 1C: Mapping Analysis"
C1[Job Requirements JSON] --> C2[Candidate Qualifications JSON]
C2 --> C3[Mapping Analysis LLM]
C3 --> C4[Skills Mapping JSON]
end
end
subgraph "Stage 2: Resume Generation"
D1[Skills Mapping JSON] --> D2[Original Resume Reference]
D2 --> D3[Resume Generation LLM]
D3 --> D4[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
end
A3 --> C1
B3 --> C2
C4 --> D1
D4 --> E3
style A2 fill:#f9d77e,stroke:#333,stroke-width:2px
style B2 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
```
## Stage 1: Isolated Analysis (three separate sub-stages)
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
## Stage 2: Resume Generation
Creates a tailored resume using only verified information from the mapping
## Stage 3: Verification
1. Performs fact-checking to catch any remaining fabrications
2. Corrects issues if needed and re-verifies
### Key Anti-Fabrication Mechanisms
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.
* **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.
* **Strict JSON Structures**: Using structured JSON formats ensures information flows properly between stages.
## Implementation Details
* **Prompt Engineering**: Each stage has carefully designed prompts with clear instructions and output formats.
* **Error Handling**: Comprehensive validation and error handling throughout the pipeline.
* **Correction Loop**: If verification fails, the system attempts to correct issues and re-verify.
* **Traceability**: Information in the final resume can be traced back to specific evidence in the original materials.

View File

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Interactive chat with an enhanced LLM."/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>Backstory</title><script defer="defer" src="/static/js/main.b1c48cdf.js"></script><link href="/static/css/main.3c3ddc39.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html> <!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Interactive chat with an enhanced LLM."/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>Backstory</title><script defer="defer" src="/static/js/main.6f07f748.js"></script><link href="/static/css/main.8e56f513.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

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],{453:(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.ec6f47ad.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

View File

@ -0,0 +1,149 @@
/*!
* 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.
*/
/*! 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

@ -1,80 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Scrollable } from './Scrollable';
import { BackstoryPageProps } from './BackstoryTab';
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]);
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("/"));
}
}, [route, setPage, setSubRoute]);
const onDocumentExpand = (document: string, open: boolean) => {
console.log("Document expanded:", document, open);
if (open) {
setPage(document);
if (setRoute) setRoute(document);
} else {
setPage("");
if (setRoute) setRoute("");
}
}
return <Scrollable
autoscroll={false}
sx={{
maxWidth: "1024px",
height: "calc(100vh - 72px)",
flexDirection: "column",
margin: "0 auto",
p: 1,
}}
>
<Document {...{
title: "About",
filepath: "/docs/about.md",
onExpand: (open: boolean) => { onDocumentExpand('about', open); },
expanded: page === 'about',
sessionId,
submitQuery: submitQuery,
setSnack,
}} />
<Document {...{
title: "Resume Generation Architecture",
filepath: "/docs/resume-generation.md",
onExpand: (open: boolean) => { onDocumentExpand('resume-generation', open); },
expanded: page === 'resume-generation',
sessionId,
submitQuery: submitQuery,
setSnack,
}} />
<Document {...{
title: "Application Architecture",
filepath: "/docs/about-app.md",
onExpand: (open: boolean) => { onDocumentExpand('about-app', open); },
expanded: page === 'about-app',
sessionId,
submitQuery: submitQuery,
setSnack,
}} />
</Scrollable>;
};
export {
AboutPage
};

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import React, { ReactElement, JSXElementConstructor, useState, useEffect, useRef, useCallback, useMemo } from 'react';
import useMediaQuery from '@mui/material/useMediaQuery'; import useMediaQuery from '@mui/material/useMediaQuery';
import Card from '@mui/material/Card'; import Card from '@mui/material/Card';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
@ -14,22 +14,20 @@ import IconButton from '@mui/material/IconButton';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import CssBaseline from '@mui/material/CssBaseline'; import CssBaseline from '@mui/material/CssBaseline';
import MenuIcon from '@mui/icons-material/Menu'; import MenuIcon from '@mui/icons-material/Menu';
import { useTheme } from '@mui/material/styles';
import { SxProps } from '@mui/material';
import { ResumeBuilder } from './ResumeBuilder';
import { MessageList } from './Message';
import { Snack, SeverityType } from './Snack'; import { Snack, SeverityType } from './Snack';
import { ConversationHandle } from './Conversation'; import { VectorVisualizer } from './VectorVisualizer';
import { QueryOptions } from './ChatQuery'; import { Controls } from './Controls';
import { Conversation, ConversationHandle } from './Conversation';
import { ChatQuery, QueryOptions } from './ChatQuery';
import { Scrollable } from './Scrollable'; import { Scrollable } from './Scrollable';
import { BackstoryPage, BackstoryTabProps } from './BackstoryTab'; import { BackstoryTab } from './BackstoryTab';
import { Document } from './Document';
import { connectionBase } from './Global';
import { HomePage } from './HomePage';
import { ResumeBuilderPage } from './ResumeBuilderPage';
import { VectorVisualizerPage } from './VectorVisualizer';
import { AboutPage } from './AboutPage';
import { ControlsPage } from './ControlsPage';
import './App.css'; import './App.css';
import './Conversation.css'; import './Conversation.css';
@ -39,7 +37,31 @@ import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css'; import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css'; import '@fontsource/roboto/700.css';
import MuiMarkdown from 'mui-markdown';
const getConnectionBase = (loc: any): string => {
console.log(`getConnectionBase(${loc})`)
if (!loc.host.match(/.*battle-linux.*/)) {
return loc.protocol + "//" + loc.host;
} else {
return loc.protocol + "//battle-linux.ketrenos.com:8912";
}
}
const connectionBase = getConnectionBase(window.location);
interface TabProps {
label?: string,
path: string,
route?: string,
children?: any,
tabProps?: {
label?: string,
sx?: SxProps,
icon?: string | ReactElement<unknown, string | JSXElementConstructor<any>> | undefined,
iconPosition?: "bottom" | "top" | "start" | "end" | undefined
}
};
const isValidUUIDv4 = (str: string): boolean => { 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; 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;
@ -54,8 +76,9 @@ const App = () => {
const isDesktop = useMediaQuery('(min-width:650px)'); const isDesktop = useMediaQuery('(min-width:650px)');
const prevIsDesktopRef = useRef<boolean>(isDesktop); const prevIsDesktopRef = useRef<boolean>(isDesktop);
const chatRef = useRef<ConversationHandle>(null); const chatRef = useRef<ConversationHandle>(null);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const snackRef = useRef<any>(null); const snackRef = useRef<any>(null);
const [subRoute, setSubRoute] = useState<string>("");
useEffect(() => { useEffect(() => {
if (prevIsDesktopRef.current === isDesktop) if (prevIsDesktopRef.current === isDesktop)
@ -78,13 +101,51 @@ const App = () => {
setActiveTab(0); setActiveTab(0);
}; };
const tabs: BackstoryTabProps[] = useMemo(() => { const onDocumentExpand = (document: string) => {
const homeTab: BackstoryTabProps = { console.log("Document expanded:", document);
}
const tabs: TabProps[] = useMemo(() => {
console.log(document);
const backstoryPreamble: MessageList = [
{
role: 'content',
title: 'Welcome to Backstory',
disableCopy: true,
content: `
Backstory is a RAG enabled expert system with access to real-time data running self-hosted
(no cloud) versions of industry leading Large and Small Language Models (LLM/SLMs).
It was written by James Ketrenos in order to provide answers to
questions potential employers may have about his work history.
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={handleSubmitChatQuery} />
<ChatQuery prompt="What programming languages has James used?" tunables={{ enable_tools: false }} submitQuery={handleSubmitChatQuery} />
<ChatQuery prompt="What are James' professional strengths?" tunables={{ enable_tools: false }} submitQuery={handleSubmitChatQuery} />
<ChatQuery prompt="What are today's headlines on CNBC.com?" tunables={{ enable_tools: true, enable_rag: false, enable_context: false }} submitQuery={handleSubmitChatQuery} />
</Box>,
<Box sx={{ p: 1 }}>
<MuiMarkdown>
As with all LLM interactions, the results may not be 100% accurate. If you have questions about my career,
I'd love to hear from you. You can send me an email at **james_backstory@ketrenos.com**.
</MuiMarkdown>
</Box>
];
const tabSx = { flexGrow: 1, fontSize: '1rem' };
return [{
label: "", label: "",
path: "", path: "",
tabProps: { tabProps: {
label: "Backstory", label: "Backstory",
sx: { flexGrow: 1, fontSize: '1rem' }, sx: tabSx,
icon: icon:
<Avatar sx={{ <Avatar sx={{
width: 24, width: 24,
@ -95,28 +156,98 @@ const App = () => {
src="/logo192.png" />, src="/logo192.png" />,
iconPosition: "start" iconPosition: "start"
}, },
children: <HomePage ref={chatRef} {...{ sessionId, setSnack, submitQuery: handleSubmitChatQuery, route: subRoute, setRoute: setSubRoute }} /> children: (
}; <Conversation
sx={{
const resumeBuilderTab: BackstoryTabProps = { maxWidth: "1024px",
height: "calc(100vh - 72px)",
}}
ref={chatRef}
{...{
type: "chat",
prompt: "What would you like to know about James?",
resetLabel: "chat",
sessionId,
connectionBase,
setSnack,
preamble: backstoryPreamble,
defaultPrompts: backstoryQuestions
}}
/>
)
}, {
label: "Resume Builder", label: "Resume Builder",
path: "resume-builder", path: "resume-builder",
children: <ResumeBuilderPage {...{ sessionId, setSnack, submitQuery: handleSubmitChatQuery, route: subRoute, setRoute: setSubRoute }} /> children: (
}; <ResumeBuilder sx={{
margin: "0 auto",
const contextVisualizerTab: BackstoryTabProps = { height: "calc(100vh - 72px)",
overflow: "auto",
backgroundColor: "#F5F5F5",
display: "flex",
flexGrow: 1
}} {...{ setSnack, connectionBase, sessionId }}
/>
)
}, {
label: "Context Visualizer", label: "Context Visualizer",
path: "context-visualizer", path: "context-visualizer",
children: <VectorVisualizerPage sx={{ p: 1 }} {...{ sessionId, setSnack, submitQuery: handleSubmitChatQuery, route: subRoute, setRoute: setSubRoute }} /> children:
}; <Scrollable
sx={{
const aboutTab = { maxWidth: "1024px",
height: "calc(100vh - 72px)",
}}
>
<VectorVisualizer sx={{ p: 1 }} {...{ connectionBase, sessionId, setSnack }} />
</Scrollable>
}, {
label: "About", label: "About",
path: "about", path: "about",
children: <AboutPage {...{ sessionId, setSnack, submitQuery: handleSubmitChatQuery, route: subRoute, setRoute: setSubRoute }} /> children: (
}; <Scrollable
autoscroll={false}
const settingsTab: BackstoryTabProps = { sx={{
maxWidth: "1024px",
height: "calc(100vh - 72px)",
flexDirection: "column",
margin: "0 auto",
p: 1,
}}
>
<Document {...{
title: "About",
filepath: "/docs/about.md",
onExpand: () => { onDocumentExpand('about'); },
expanded: false,//openDocument === 'about',
sessionId,
connectionBase,
submitQuery: handleSubmitChatQuery,
setSnack,
}} />
<Document {...{
title: "Resume Generation Architecture",
filepath: "/docs/resume-generation.md",
onExpand: () => { onDocumentExpand('resume-generation'); },
expanded: false, //openDocument === 'about',
sessionId,
connectionBase,
submitQuery: handleSubmitChatQuery,
setSnack,
}} />
<Document {...{
title: "Application Architecture",
filepath: "/docs/about-app.md",
onExpand: () => { onDocumentExpand('resume-generation'); },
expanded: false, //openDocument === 'about-app',
sessionId,
connectionBase,
submitQuery: handleSubmitChatQuery,
setSnack,
}} />
</Scrollable>
)
}, {
path: "settings", path: "settings",
tabProps: { tabProps: {
sx: { flexShrink: 1, flexGrow: 0, fontSize: '1rem' }, sx: { flexShrink: 1, flexGrow: 0, fontSize: '1rem' },
@ -134,34 +265,12 @@ const App = () => {
}} }}
> >
{sessionId !== undefined && {sessionId !== undefined &&
<ControlsPage {...{ sessionId, setSnack, submitQuery: handleSubmitChatQuery, route: subRoute, setRoute: setSubRoute }} /> <Controls {...{ sessionId, setSnack, connectionBase }} />
} }
</Scrollable> </Scrollable>
) )
}; }];
}, [sessionId, setSnack, isMobile]);
return [
homeTab,
resumeBuilderTab,
contextVisualizerTab,
aboutTab,
settingsTab,
];
}, [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[]) => { const fetchSession = useCallback((async (pathParts?: string[]) => {
try { try {
@ -191,7 +300,7 @@ const App = () => {
setActiveTab(0); setActiveTab(0);
} else { } else {
window.history.replaceState({}, '', `/${pathParts.join('/')}/${new_session}`); window.history.replaceState({}, '', `/${pathParts.join('/')}/${new_session}`);
// tabs[tabIndex].route = pathParts[2] || ""; tabs[tabIndex].route = pathParts[2] || "";
setActiveTab(tabIndex); setActiveTab(tabIndex);
} }
setSessionId(new_session); setSessionId(new_session);
@ -221,7 +330,7 @@ const App = () => {
console.log(`Invalid path "${currentPath}" -- redirecting to default`); console.log(`Invalid path "${currentPath}" -- redirecting to default`);
tabIndex = 0; tabIndex = 0;
} }
// tabs[tabIndex].route = pathParts[2] || "" tabs[tabIndex].route = pathParts[2] || ""
setSessionId(path_session); setSessionId(path_session);
setActiveTab(tabIndex); setActiveTab(tabIndex);
} }
@ -388,7 +497,7 @@ const App = () => {
</Box> </Box>
{ {
tabs.map((tab: any, i: number) => tabs.map((tab: any, i: number) =>
<BackstoryPage key={i} active={i === activeTab} path={tab.path}>{tab.children}</BackstoryPage> <BackstoryTab key={i} active={i === activeTab}>{tab.children}</BackstoryTab>
) )
} }
</Box> </Box>

View File

@ -1,54 +1,28 @@
import React, { ReactElement, JSXElementConstructor } from 'react'; import React from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { SxProps, Theme } from '@mui/material'; import { SxProps, Theme } from '@mui/material';
import { ChatSubmitQueryInterface } from './ChatQuery';
import { SetSnackType } from './Snack';
interface BackstoryElementProps {
sessionId: string | undefined,
setSnack: SetSnackType,
submitQuery: ChatSubmitQueryInterface,
sx?: SxProps<Theme>,
}
interface BackstoryPageProps extends BackstoryElementProps {
route?: string,
setRoute?: (route: string) => void,
};
interface BackstoryTabProps { interface BackstoryTabProps {
label?: string, children?: React.ReactNode;
path: string, sx?: SxProps<Theme>,
children?: ReactElement<BackstoryPageProps>,
active?: boolean,
className?: string, className?: string,
tabProps?: { active: boolean,
label?: string,
sx?: SxProps,
icon?: string | ReactElement<unknown, string | JSXElementConstructor<any>> | undefined,
iconPosition?: "bottom" | "top" | "start" | "end" | undefined
} }
};
function BackstoryPage(props: BackstoryTabProps) { function BackstoryTab(props: BackstoryTabProps) {
const { className, active, children } = props; const { className, active, children, sx, ...other } = props;
return ( return (
<Box <Box
className={ className || "BackstoryTab"} className={ className || "BackstoryTab"}
sx={{ "display": active ? "flex" : "none", p: 0, m: 0, borders: "none" }} sx={{ "display": active ? "flex": "none", ...sx }}
{...other}
> >
{children} {children}
</Box> </Box>
); );
} }
export type {
BackstoryPageProps,
BackstoryTabProps,
BackstoryElementProps,
};
export { export {
BackstoryPage BackstoryTab
} }

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useState } from 'react';
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import { SxProps, Theme } from '@mui/material'; import { SxProps, Theme } from '@mui/material';
@ -20,11 +20,12 @@ interface ChatBubbleProps {
title?: string; title?: string;
expanded?: boolean; expanded?: boolean;
expandable?: boolean; expandable?: boolean;
onExpand?: (open: boolean) => void; onExpand?: () => void;
} }
function ChatBubble(props: ChatBubbleProps) { function ChatBubble(props: ChatBubbleProps) {
const { role, children, sx, className, title, onExpand, expandable, expanded } = props; const { role, children, sx, className, title, onExpand, expandable }: ChatBubbleProps = props;
const [expanded, setExpanded] = useState<boolean>((props.expanded === undefined) ? true : props.expanded);
const theme = useTheme(); const theme = useTheme();
@ -32,12 +33,12 @@ function ChatBubble(props: ChatBubbleProps) {
const defaultStyle = { const defaultStyle = {
padding: theme.spacing(1, 2), padding: theme.spacing(1, 2),
fontSize: '0.875rem', fontSize: '0.875rem',
alignSelf: 'flex-start', alignSelf: 'flex-start', // Left-aligned is used by default
maxWidth: '100%', maxWidth: '100%',
minWidth: '100%', minWidth: '100%',
height: 'fit-content', height: 'fit-content',
'& > *': { '& > *': {
color: 'inherit', color: 'inherit', // Children inherit 'color' from parent
overflow: 'hidden', overflow: 'hidden',
m: 0, m: 0,
}, },
@ -45,151 +46,130 @@ function ChatBubble(props: ChatBubbleProps) {
mb: 0, mb: 0,
m: 0, m: 0,
p: 0, p: 0,
}, }
}; }
const styles: any = { const styles: any = {
assistant: { 'assistant': {
...defaultStyle, ...defaultStyle,
backgroundColor: theme.palette.primary.main, backgroundColor: theme.palette.primary.main, // Midnight Blue (#1A2536)
border: `1px solid ${theme.palette.secondary.main}`, border: `1px solid ${theme.palette.secondary.main}`, // Dusty Teal (#4A7A7D)
borderRadius: `${defaultRadius} ${defaultRadius} ${defaultRadius} 0`, borderRadius: `${defaultRadius} ${defaultRadius} ${defaultRadius} 0`, // Rounded, flat bottom-left for assistant
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText, // Warm Gray (#D3CDBF) for text
}, },
content: { 'content': {
...defaultStyle, ...defaultStyle,
backgroundColor: '#F5F2EA', backgroundColor: '#F5F2EA', // Light cream background for easy reading
border: `1px solid ${theme.palette.custom.highlight}`, border: `1px solid ${theme.palette.custom.highlight}`, // Golden Ochre border
borderRadius: 0, borderRadius: 0,
alignSelf: 'center', alignSelf: 'center', // Centered in the chat
color: theme.palette.text.primary, color: theme.palette.text.primary, // Charcoal Black for maximum readability
padding: '8px 8px', padding: '8px 8px', // More generous padding for better text framing
marginBottom: '0px', marginBottom: '0px', // Space between content and conversation
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.05)', boxShadow: '0 2px 4px rgba(0, 0, 0, 0.05)', // Subtle elevation
fontSize: '0.9rem', fontSize: '0.9rem', // Slightly smaller than default
lineHeight: '1.3', lineHeight: '1.3', // More compact line height
fontFamily: theme.typography.fontFamily, fontFamily: theme.typography.fontFamily, // Consistent font with your theme
}, },
error: { 'error': {
...defaultStyle, ...defaultStyle,
backgroundColor: '#F8E7E7', backgroundColor: '#F8E7E7', // Soft light red background
border: `1px solid #D83A3A`, border: `1px solid #D83A3A`, // Prominent red border
borderRadius: defaultRadius, borderRadius: defaultRadius,
maxWidth: '90%', maxWidth: '90%',
minWidth: '90%', minWidth: '90%',
alignSelf: 'center', alignSelf: 'center',
color: '#8B2525', color: '#8B2525', // Deep red text for good contrast
padding: '10px 16px', padding: '10px 16px',
boxShadow: '0 1px 3px rgba(216, 58, 58, 0.15)', boxShadow: '0 1px 3px rgba(216, 58, 58, 0.15)', // Subtle shadow with red tint
}, },
'fact-check': 'qualifications', 'fact-check': 'qualifications',
'job-description': 'content', 'job-description': 'content',
'job-requirements': 'qualifications', 'job-requirements': 'qualifications',
info: { 'info': {
...defaultStyle, ...defaultStyle,
backgroundColor: '#BFD8D8', backgroundColor: '#BFD8D8', // Softened Dusty Teal
border: `1px solid ${theme.palette.secondary.main}`, border: `1px solid ${theme.palette.secondary.main}`, // Dusty Teal
borderRadius: defaultRadius, borderRadius: defaultRadius,
color: theme.palette.text.primary, color: theme.palette.text.primary, // Charcoal Black (#2E2E2E) — much better contrast
opacity: 0.95, opacity: 0.95,
}, },
processing: 'status', 'processing': "status",
qualifications: { 'qualifications': {
...defaultStyle, ...defaultStyle,
backgroundColor: theme.palette.primary.light, backgroundColor: theme.palette.primary.light, // Lighter shade, e.g., Soft Blue (#2A3B56)
border: `1px solid ${theme.palette.secondary.main}`, border: `1px solid ${theme.palette.secondary.main}`, // Keep Dusty Teal (#4A7A7D) for contrast
borderRadius: `${defaultRadius} ${defaultRadius} ${defaultRadius} 0`, borderRadius: `${defaultRadius} ${defaultRadius} ${defaultRadius} 0`, // Unchanged
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText, // Warm Gray (#D3CDBF) for readable text
}, },
resume: 'content', 'resume': 'content',
searching: 'status', 'searching': 'status',
status: { 'status': {
...defaultStyle, ...defaultStyle,
backgroundColor: 'rgba(74, 122, 125, 0.15)', backgroundColor: 'rgba(74, 122, 125, 0.15)', // Translucent dusty teal
border: `1px solid ${theme.palette.secondary.light}`, border: `1px solid ${theme.palette.secondary.light}`, // Lighter dusty teal
borderRadius: '4px', borderRadius: '4px',
maxWidth: '75%', maxWidth: '75%',
minWidth: '75%', minWidth: '75%',
alignSelf: 'center', alignSelf: 'center',
color: theme.palette.secondary.dark, color: theme.palette.secondary.dark, // Darker dusty teal for text
fontWeight: 500, fontWeight: 500, // Slightly bolder than normal
fontSize: '0.95rem', fontSize: '0.95rem', // Slightly smaller
padding: '8px 12px', padding: '8px 12px',
opacity: 0.9, opacity: 0.9,
transition: 'opacity 0.3s ease-in-out', transition: 'opacity 0.3s ease-in-out', // Smooth fade effect for appearing/disappearing
}, },
streaming: 'assistant', 'streaming': "assistant",
system: { 'system': {
...defaultStyle, ...defaultStyle,
backgroundColor: '#EDEAE0', backgroundColor: '#EDEAE0', // Soft warm gray that plays nice with #D3CDBF
border: `1px dashed ${theme.palette.custom.highlight}`, border: `1px dashed ${theme.palette.custom.highlight}`, // Golden Ochre
borderRadius: defaultRadius, borderRadius: defaultRadius,
maxWidth: '90%', maxWidth: '90%',
minWidth: '90%', minWidth: '90%',
alignSelf: 'center', alignSelf: 'center',
color: theme.palette.text.primary, color: theme.palette.text.primary, // Charcoal Black
fontStyle: 'italic', fontStyle: 'italic',
}, },
thinking: 'status', 'thinking': "status",
user: { 'user': {
...defaultStyle, ...defaultStyle,
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default, // Warm Gray (#D3CDBF)
border: `1px solid ${theme.palette.custom.highlight}`, border: `1px solid ${theme.palette.custom.highlight}`, // Golden Ochre (#D4A017)
borderRadius: `${defaultRadius} ${defaultRadius} 0 ${defaultRadius}`, borderRadius: `${defaultRadius} ${defaultRadius} 0 ${defaultRadius}`, // Rounded, flat bottom-right for user
alignSelf: 'flex-end', alignSelf: 'flex-end', // Right-aligned for user
color: theme.palette.primary.main, color: theme.palette.primary.main, // Midnight Blue (#1A2536) for text
}, },
}; };
// Resolve string references in styles
for (const [key, value] of Object.entries(styles)) { for (const [key, value] of Object.entries(styles)) {
if (typeof value === 'string') { if (typeof (value) === "string") {
styles[key] = styles[value]; (styles as any)[key] = styles[value];
} }
} }
const icons: any = { const icons: any = {
error: <ErrorOutline color="error" />, "error": <ErrorOutline color='error' />,
info: <InfoOutline color="info" />, "info": <InfoOutline color='info' />,
processing: <LocationSearchingIcon />, "processing": <LocationSearchingIcon />,
searching: <Memory />, // "streaming": <Stream />,
thinking: <Psychology />, "searching": <Memory />,
tooling: <LocationSearchingIcon />, "thinking": <Psychology />,
"tooling": <LocationSearchingIcon />,
}; };
// Render Accordion for expandable content
if (expandable || (role === 'content' && title)) { if (expandable || (role === 'content' && title)) {
// Determine if Accordion is controlled
const isControlled = typeof expanded === 'boolean' && typeof onExpand === 'function';
return ( return (
<Accordion <Accordion
expanded={isControlled ? expanded : undefined} // Omit expanded prop for uncontrolled expanded={expanded}
defaultExpanded={expanded} // Default to collapsed for uncontrolled Accordion
className={className} className={className}
onChange={(_event, newExpanded) => { onChange={() => { onExpand && onExpand(); setExpanded(!expanded); }}
if (isControlled && onExpand) {
onExpand(newExpanded); // Call onExpand with new state
}
}}
sx={{ ...styles[role], ...sx }} sx={{ ...styles[role], ...sx }}
> >
<AccordionSummary <AccordionSummary
expandIcon={<ExpandMoreIcon />} expandIcon={<ExpandMoreIcon />}
slotProps={{ slotProps={{ content: { sx: { fontWeight: 'bold', fontSize: '1.1rem', m: 0, p: 0, display: 'flex', justifyItems: 'center' } } }}
content: {
sx: {
fontWeight: 'bold',
fontSize: '1.1rem',
m: 0,
p: 0,
display: 'flex',
justifyItems: 'center',
},
},
}}
> >
{title || ''} {title || ""}
</AccordionSummary> </AccordionSummary>
<AccordionDetails sx={{ mt: 0, mb: 0, p: 0, pl: 2, pr: 2 }}> <AccordionDetails sx={{ mt: 0, mb: 0, p: 0, pl: 2, pr: 2 }}>
{children} {children}
@ -198,20 +178,10 @@ function ChatBubble(props: ChatBubbleProps) {
); );
} }
// Render non-expandable content
return ( return (
<Box <Box className={className} sx={{ ...(role in styles ? styles[role] : styles["status"]), gap: 1, display: "flex", ...sx, flexDirection: "row" }}>
className={className}
sx={{
...(role in styles ? styles[role] : styles['status']),
gap: 1,
display: 'flex',
...sx,
flexDirection: 'row',
}}
>
{icons[role] !== undefined && icons[role]} {icons[role] !== undefined && icons[role]}
<Box sx={{ p: 0, m: 0, gap: 0, display: 'flex', flexGrow: 1, flexDirection: 'column' }}> <Box sx={{ p: 0, m: 0, gap: 0, display: "flex", flexGrow: 1, flexDirection: "column" }}>
{children} {children}
</Box> </Box>
</Box> </Box>

View File

@ -7,12 +7,10 @@ type QueryOptions = {
enable_context?: boolean, enable_context?: boolean,
}; };
type ChatSubmitQueryInterface = (prompt: string, tunables?: QueryOptions) => void;
interface ChatQueryInterface { interface ChatQueryInterface {
prompt: string, prompt: string,
tunables?: QueryOptions, tunables?: QueryOptions,
submitQuery?: ChatSubmitQueryInterface submitQuery?: (prompt: string, tunables?: QueryOptions) => void
} }
const ChatQuery = (props : ChatQueryInterface) => { const ChatQuery = (props : ChatQueryInterface) => {
@ -41,7 +39,6 @@ const ChatQuery = (props : ChatQueryInterface) => {
export type { export type {
ChatQueryInterface, ChatQueryInterface,
QueryOptions, QueryOptions,
ChatSubmitQueryInterface,
}; };
export { export {

View File

@ -14,9 +14,7 @@ import Typography from '@mui/material/Typography';
// import ResetIcon from '@mui/icons-material/History'; // import ResetIcon from '@mui/icons-material/History';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { connectionBase } from './Global'; import { SetSnackType } from './Snack';
import { BackstoryPageProps } from './BackstoryTab';
import { restyle } from 'plotly.js';
interface ServerTunables { interface ServerTunables {
system_prompt: string, system_prompt: string,
@ -34,6 +32,12 @@ type Tool = {
returns?: any returns?: any
}; };
interface ControlsParams {
connectionBase: string,
sessionId: string | undefined,
setSnack: SetSnackType,
};
type GPUInfo = { type GPUInfo = {
name: string, name: string,
memory: number, memory: number,
@ -86,7 +90,7 @@ const SystemInfoComponent: React.FC<{ systemInfo: SystemInfo | undefined }> = ({
return <div className="SystemInfo">{systemElements}</div>; return <div className="SystemInfo">{systemElements}</div>;
}; };
const ControlsPage = ({ sessionId, setSnack }: BackstoryPageProps) => { const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
const [editSystemPrompt, setEditSystemPrompt] = useState<string>(""); const [editSystemPrompt, setEditSystemPrompt] = useState<string>("");
const [systemInfo, setSystemInfo] = useState<SystemInfo | undefined>(undefined); const [systemInfo, setSystemInfo] = useState<SystemInfo | undefined>(undefined);
const [tools, setTools] = useState<Tool[]>([]); const [tools, setTools] = useState<Tool[]>([]);
@ -122,7 +126,7 @@ const ControlsPage = ({ sessionId, setSnack }: BackstoryPageProps) => {
sendSystemPrompt(systemPrompt); sendSystemPrompt(systemPrompt);
}, [systemPrompt, sessionId, setSnack, serverTunables]); }, [systemPrompt, connectionBase, sessionId, setSnack, serverTunables]);
useEffect(() => { useEffect(() => {
if (serverTunables === undefined || messageHistoryLength === serverTunables.message_history_length || !messageHistoryLength || sessionId === undefined) { if (serverTunables === undefined || messageHistoryLength === serverTunables.message_history_length || !messageHistoryLength || sessionId === undefined) {
@ -153,7 +157,7 @@ const ControlsPage = ({ sessionId, setSnack }: BackstoryPageProps) => {
sendMessageHistoryLength(messageHistoryLength); sendMessageHistoryLength(messageHistoryLength);
}, [messageHistoryLength, setMessageHistoryLength, sessionId, setSnack, serverTunables]); }, [messageHistoryLength, setMessageHistoryLength, connectionBase, sessionId, setSnack, serverTunables]);
const reset = async (types: ("rags" | "tools" | "history" | "system_prompt" | "message_history_length")[], message: string = "Update successful.") => { const reset = async (types: ("rags" | "tools" | "history" | "system_prompt" | "message_history_length")[], message: string = "Update successful.") => {
try { try {
@ -216,7 +220,7 @@ const ControlsPage = ({ sessionId, setSnack }: BackstoryPageProps) => {
console.error('Error obtaining system information:', error); console.error('Error obtaining system information:', error);
setSnack("Unable to obtain system information.", "error"); setSnack("Unable to obtain system information.", "error");
}); });
}, [systemInfo, setSystemInfo, setSnack, sessionId]) }, [systemInfo, setSystemInfo, connectionBase, setSnack, sessionId])
useEffect(() => { useEffect(() => {
setEditSystemPrompt(systemPrompt.trim()); setEditSystemPrompt(systemPrompt.trim());
@ -290,7 +294,7 @@ const ControlsPage = ({ sessionId, setSnack }: BackstoryPageProps) => {
} }
fetchTunables(); fetchTunables();
}, [sessionId, setServerTunables, setSystemPrompt, setMessageHistoryLength, serverTunables, setTools, setRags]); }, [sessionId, connectionBase, setServerTunables, setSystemPrompt, setMessageHistoryLength, serverTunables, setTools, setRags]);
const toggle = async (type: string, index: number) => { const toggle = async (type: string, index: number) => {
switch (type) { switch (type) {
@ -429,6 +433,11 @@ const ControlsPage = ({ sessionId, setSnack }: BackstoryPageProps) => {
</div>); </div>);
} }
export {
ControlsPage export type {
ControlsParams
};
export {
Controls
}; };

View File

@ -8,14 +8,13 @@ import { SxProps, Theme } from '@mui/material';
import PropagateLoader from "react-spinners/PropagateLoader"; import PropagateLoader from "react-spinners/PropagateLoader";
import { Message, MessageList, MessageData } from './Message'; import { Message, MessageList, MessageData } from './Message';
import { SetSnackType } from './Snack';
import { ContextStatus } from './ContextStatus'; import { ContextStatus } from './ContextStatus';
import { Scrollable } from './Scrollable'; import { Scrollable } from './Scrollable';
import { DeleteConfirmation } from './DeleteConfirmation'; import { DeleteConfirmation } from './DeleteConfirmation';
import { QueryOptions } from './ChatQuery'; import { QueryOptions } from './ChatQuery';
import './Conversation.css'; import './Conversation.css';
import { BackstoryTextField } from './BackstoryTextField'; import { BackstoryTextField } from './BackstoryTextField';
import { BackstoryElementProps } from './BackstoryTab';
import { connectionBase } from './Global';
const loadingMessage: MessageData = { "role": "status", "content": "Establishing connection with server..." }; const loadingMessage: MessageData = { "role": "status", "content": "Establishing connection with server..." };
@ -45,7 +44,7 @@ interface BackstoryMessage {
timestamp: string; timestamp: string;
}; };
interface ConversationProps extends BackstoryElementProps { interface ConversationProps {
className?: string, // Override default className className?: string, // Override default className
type: ConversationMode, // Type of Conversation chat type: ConversationMode, // Type of Conversation chat
prompt?: string, // Prompt to display in TextField input prompt?: string, // Prompt to display in TextField input
@ -53,6 +52,9 @@ interface ConversationProps extends BackstoryElementProps {
resetAction?: () => void, // Callback when Reset is pressed resetAction?: () => void, // Callback when Reset is pressed
multiline?: boolean, // Render TextField as multiline or not multiline?: boolean, // Render TextField as multiline or not
resetLabel?: string, // Label to put on Reset button resetLabel?: string, // Label to put on Reset button
connectionBase: string, // Base URL for fetch() calls
sessionId?: string, // Session ID for fetch() calls
setSnack: SetSnackType, // Callback to display snack popups
defaultPrompts?: React.ReactElement[], // Set of Elements to display after the TextField defaultPrompts?: React.ReactElement[], // Set of Elements to display after the TextField
defaultQuery?: string, // Default text to populate the TextField input defaultQuery?: string, // Default text to populate the TextField input
emptyPrompt?: string, // If input is not shown and an action is taken, send this prompt emptyPrompt?: string, // If input is not shown and an action is taken, send this prompt
@ -66,26 +68,26 @@ interface ConversationProps extends BackstoryElementProps {
}; };
const Conversation = forwardRef<ConversationHandle, ConversationProps>(({ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
actionLabel,
className, className,
defaultPrompts, type,
defaultQuery, prompt,
emptyPrompt, emptyPrompt,
actionLabel,
resetAction,
multiline,
resetLabel,
connectionBase,
sessionId,
setSnack,
defaultPrompts,
hideDefaultPrompts, hideDefaultPrompts,
defaultQuery,
preamble,
hidePreamble, hidePreamble,
messageFilter, messageFilter,
messages, messages,
multiline,
onResponse,
prompt,
preamble,
resetAction,
resetLabel,
sessionId,
setSnack,
submitQuery,
sx, sx,
type, onResponse
}: ConversationProps, ref) => { }: ConversationProps, ref) => {
const [query, setQuery] = useState<string>(""); const [query, setQuery] = useState<string>("");
const [contextUsedPercentage, setContextUsedPercentage] = useState<number>(0); const [contextUsedPercentage, setContextUsedPercentage] = useState<number>(0);
@ -131,7 +133,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
} }
}; };
fetchContextStatus(); fetchContextStatus();
}, [setContextStatus, setSnack, sessionId, type]); }, [setContextStatus, connectionBase, setSnack, sessionId, type]);
/* Transform the 'Conversation' by filtering via callback, then adding /* Transform the 'Conversation' by filtering via callback, then adding
* preamble and messages based on whether the conversation * preamble and messages based on whether the conversation
@ -211,7 +213,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
setSnack("Unable to obtain chat history.", "error"); setSnack("Unable to obtain chat history.", "error");
} }
}; };
}, [setConversation, updateContextStatus, setSnack, type, sessionId]); }, [setConversation, updateContextStatus, connectionBase, setSnack, type, sessionId]);
// Set the initial chat history to "loading" or the welcome message if loaded. // Set the initial chat history to "loading" or the welcome message if loaded.
useEffect(() => { useEffect(() => {
@ -511,16 +513,16 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
> >
{ {
filteredConversation.map((message, index) => filteredConversation.map((message, index) =>
<Message key={index} expanded={message.expanded === undefined ? true : message.expanded} {...{ sendQuery, message, connectionBase, sessionId, setSnack, submitQuery }} /> <Message key={index} {...{ sendQuery, message, connectionBase, sessionId, setSnack }} />
) )
} }
{ {
processingMessage !== undefined && processingMessage !== undefined &&
<Message {...{ sendQuery, connectionBase, sessionId, setSnack, message: processingMessage, submitQuery }} /> <Message {...{ sendQuery, connectionBase, sessionId, setSnack, message: processingMessage }} />
} }
{ {
streamingMessage !== undefined && streamingMessage !== undefined &&
<Message {...{ sendQuery, connectionBase, sessionId, setSnack, message: streamingMessage, submitQuery }} /> <Message {...{ sendQuery, connectionBase, sessionId, setSnack, message: streamingMessage }} />
} }
<Box sx={{ <Box sx={{
display: "flex", display: "flex",

View File

@ -1,19 +1,21 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Message } from './Message'; import { Message, MessageSubmitQuery } from './Message';
import { BackstoryElementProps } from './BackstoryTab'; import { SetSnackType } from './Snack';
import { connectionBase } from './Global';
interface DocumentProps extends BackstoryElementProps { interface DocumentProps {
title: string; title: string;
expanded?: boolean; expanded?: boolean;
filepath: string; filepath: string;
content?: string; content?: string;
setSnack: SetSnackType;
submitQuery?: MessageSubmitQuery;
connectionBase: string;
disableCopy?: boolean; disableCopy?: boolean;
onExpand?: (open: boolean) => void; onExpand?: () => void;
} }
const Document = (props: DocumentProps) => { const Document = (props: DocumentProps) => {
const { setSnack, submitQuery, filepath, content, title, expanded, disableCopy, onExpand, sessionId } = props; const { setSnack, submitQuery, connectionBase, filepath, content, title, expanded, disableCopy, onExpand } = props;
const [document, setDocument] = useState<string>(""); const [document, setDocument] = useState<string>("");
@ -61,7 +63,6 @@ const Document = (props: DocumentProps) => {
expanded, expanded,
disableCopy, disableCopy,
onExpand, onExpand,
sessionId,
}} /> }} />
); );
}; };

View File

@ -1,13 +0,0 @@
const getConnectionBase = (loc: any): string => {
if (!loc.host.match(/.*battle-linux.*/)) {
return loc.protocol + "//" + loc.host;
} else {
return loc.protocol + "//battle-linux.ketrenos.com:8912";
}
}
const connectionBase = getConnectionBase(window.location);
export {
connectionBase
};

View File

@ -1,69 +0,0 @@
import React, { forwardRef } from 'react';
import useMediaQuery from '@mui/material/useMediaQuery';
import Box from '@mui/material/Box';
import { useTheme } from '@mui/material/styles';
import MuiMarkdown from 'mui-markdown';
import { BackstoryPageProps } from './BackstoryTab';
import { Conversation, ConversationHandle } from './Conversation';
import { ChatQuery } from './ChatQuery';
import { MessageList } from './Message';
const HomePage = forwardRef<ConversationHandle, BackstoryPageProps>((props: BackstoryPageProps, ref) => {
const { sessionId, setSnack, submitQuery } = props;
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const backstoryPreamble: MessageList = [
{
role: 'content',
title: 'Welcome to Backstory',
disableCopy: true,
content: `
Backstory is a RAG enabled expert system with access to real-time data running self-hosted
(no cloud) versions of industry leading Large and Small Language Models (LLM/SLMs).
It was written by James Ketrenos in order to provide answers to
questions potential employers may have about his work history.
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} />
</Box>,
<Box sx={{ p: 1 }}>
<MuiMarkdown>
As with all LLM interactions, the results may not be 100% accurate. If you have questions about my career,
I'd love to hear from you. You can send me an email at **james_backstory@ketrenos.com**.
</MuiMarkdown>
</Box>
];
return <Conversation
sx={{
maxWidth: "1024px",
height: "calc(100vh - 72px)",
}}
ref={ref}
{...{
type: "chat",
prompt: "What would you like to know about James?",
resetLabel: "chat",
sessionId,
setSnack,
preamble: backstoryPreamble,
defaultPrompts: backstoryQuestions,
submitQuery,
}}
/>;
});
export {
HomePage
};

View File

@ -28,7 +28,6 @@ import { VectorVisualizer } from './VectorVisualizer';
import { SetSnackType } from './Snack'; import { SetSnackType } from './Snack';
import { CopyBubble } from './CopyBubble'; import { CopyBubble } from './CopyBubble';
import { Scrollable } from './Scrollable'; import { Scrollable } from './Scrollable';
import { BackstoryElementProps } from './BackstoryTab';
type MessageRoles = type MessageRoles =
'assistant' | 'assistant' |
@ -84,13 +83,19 @@ interface MessageMetaData {
setSnack: SetSnackType, setSnack: SetSnackType,
} }
type MessageSubmitQuery = (text: string) => void;
type MessageList = MessageData[]; type MessageList = MessageData[];
interface MessageProps extends BackstoryElementProps { interface MessageProps {
sx?: SxProps<Theme>, sx?: SxProps<Theme>,
message: MessageData, message: MessageData,
expanded?: boolean, // expanded?: boolean, // Provided as part of MessageData
onExpand?: (open: boolean) => void, onExpand?: () => void,
submitQuery?: MessageSubmitQuery,
sessionId?: string,
connectionBase: string,
setSnack: SetSnackType,
className?: string, className?: string,
}; };
@ -237,12 +242,12 @@ const MessageMeta = (props: MessageMetaProps) => {
}; };
const Message = (props: MessageProps) => { const Message = (props: MessageProps) => {
const { message, submitQuery, sx, className, onExpand, expanded, sessionId, setSnack } = props; const { message, submitQuery, sx, className, onExpand } = props;
const [metaExpanded, setMetaExpanded] = useState<boolean>(false); const [expanded, setExpanded] = useState<boolean>(false);
const textFieldRef = useRef(null); const textFieldRef = useRef(null);
const handleMetaExpandClick = () => { const handleExpandClick = () => {
setMetaExpanded(!metaExpanded); setExpanded(!expanded);
}; };
if (message === undefined) { if (message === undefined) {
@ -261,7 +266,6 @@ const Message = (props: MessageProps) => {
className={className || "Message"} className={className || "Message"}
{...message} {...message}
onExpand={onExpand} onExpand={onExpand}
expanded={expanded}
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
@ -287,7 +291,7 @@ const Message = (props: MessageProps) => {
overflow: "auto", /* Handles scrolling for the div */ overflow: "auto", /* Handles scrolling for the div */
}} }}
> >
<StyledMarkdown {...{ content: formattedContent, submitQuery, sessionId, setSnack }} /> <StyledMarkdown {...{ content: formattedContent, submitQuery }} />
</Scrollable> </Scrollable>
: :
<Typography <Typography
@ -303,12 +307,12 @@ const Message = (props: MessageProps) => {
{(message.disableCopy === undefined || message.disableCopy === false) && ["assistant", "content"].includes(message.role) && <CopyBubble content={message.content} />} {(message.disableCopy === undefined || message.disableCopy === false) && ["assistant", "content"].includes(message.role) && <CopyBubble content={message.content} />}
{message.metadata && ( {message.metadata && (
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}> <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Button variant="text" onClick={handleMetaExpandClick} sx={{ color: "darkgrey", p: 0 }}> <Button variant="text" onClick={handleExpandClick} sx={{ color: "darkgrey", p: 0 }}>
LLM information for this query LLM information for this query
</Button> </Button>
<ExpandMore <ExpandMore
expand={metaExpanded} expand={expanded}
onClick={handleMetaExpandClick} onClick={handleExpandClick}
aria-expanded={expanded} aria-expanded={expanded}
aria-label="show more" aria-label="show more"
> >
@ -318,7 +322,7 @@ const Message = (props: MessageProps) => {
)} )}
</CardActions> </CardActions>
{message.metadata && <> {message.metadata && <>
<Collapse in={metaExpanded} timeout="auto" unmountOnExit> <Collapse in={expanded} timeout="auto" unmountOnExit>
<CardContent> <CardContent>
<MessageMeta messageProps={props} metadata={message.metadata} /> <MessageMeta messageProps={props} metadata={message.metadata} />
</CardContent> </CardContent>
@ -333,6 +337,7 @@ export type {
MessageList, MessageList,
MessageData, MessageData,
MessageRoles, MessageRoles,
MessageSubmitQuery
}; };
export { export {

View File

@ -4,14 +4,21 @@ import {
Tab, Tab,
Box, Box,
} from '@mui/material'; } from '@mui/material';
import { SxProps } from '@mui/material'; import { SxProps, Theme } from '@mui/material';
import { ChatQuery } from './ChatQuery'; import { ChatQuery } from './ChatQuery';
import { MessageList, MessageData } from './Message'; import { MessageList, MessageData } from './Message';
import { SetSnackType } from './Snack';
import { Conversation } from './Conversation'; import { Conversation } from './Conversation';
import { BackstoryPageProps } from './BackstoryTab';
import './ResumeBuilderPage.css'; import './ResumeBuilder.css';
interface ResumeBuilderProps {
connectionBase: string,
sessionId: string | undefined,
setSnack: SetSnackType,
sx?: SxProps<Theme>;
};
/** /**
* ResumeBuilder component * ResumeBuilder component
@ -19,11 +26,12 @@ import './ResumeBuilderPage.css';
* A responsive component that displays job descriptions, generated resumes and fact checks * A responsive component that displays job descriptions, generated resumes and fact checks
* with different layouts for mobile and desktop views. * with different layouts for mobile and desktop views.
*/ */
const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
sx, sx,
connectionBase,
sessionId, sessionId,
setSnack, setSnack
submitQuery,
}) => { }) => {
// State for editing job description // State for editing job description
const [hasJobDescription, setHasJobDescription] = useState<boolean>(false); const [hasJobDescription, setHasJobDescription] = useState<boolean>(false);
@ -79,7 +87,7 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
messages[3].role = 'job-requirements'; messages[3].role = 'job-requirements';
messages[3].title = 'Job Requirements'; messages[3].title = 'Job Requirements';
messages[3].disableCopy = false; messages[3].disableCopy = false;
messages[3].expanded = false; messages[3].expanded = true;
messages[3].expandable = true; messages[3].expandable = true;
} }
@ -128,7 +136,7 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
messages[1].role = 'fact-check'; messages[1].role = 'fact-check';
messages[1].title = 'Fact Check'; messages[1].title = 'Fact Check';
messages[1].disableCopy = false; messages[1].disableCopy = false;
messages[1].expanded = true; messages[1].expanded = false;
messages[1].expandable = true; messages[1].expandable = true;
} }
@ -217,8 +225,8 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
resetAction: resetJobDescription, resetAction: resetJobDescription,
onResponse: jobResponse, onResponse: jobResponse,
sessionId, sessionId,
connectionBase,
setSnack, setSnack,
submitQuery,
sx, sx,
}} }}
/> />
@ -236,13 +244,13 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
resetAction: resetJobDescription, resetAction: resetJobDescription,
onResponse: jobResponse, onResponse: jobResponse,
sessionId, sessionId,
connectionBase,
setSnack, setSnack,
submitQuery,
sx, sx,
}} }}
/> />
} }
}, [filterJobDescriptionMessages, hasJobDescription, sessionId, setSnack, jobResponse, resetJobDescription, hasFacts, hasResume, submitQuery]); }, [connectionBase, filterJobDescriptionMessages, hasJobDescription, sessionId, setSnack, jobResponse, resetJobDescription, hasFacts, hasResume]);
/** /**
* Renders the resume view with loading indicator * Renders the resume view with loading indicator
@ -267,8 +275,8 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
onResponse: resumeResponse, onResponse: resumeResponse,
resetAction: resetResume, resetAction: resetResume,
sessionId, sessionId,
connectionBase,
setSnack, setSnack,
submitQuery,
sx, sx,
}} }}
/> />
@ -284,14 +292,14 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
onResponse: resumeResponse, onResponse: resumeResponse,
resetAction: resetResume, resetAction: resetResume,
sessionId, sessionId,
connectionBase,
setSnack, setSnack,
defaultPrompts: resumeQuestions, defaultPrompts: resumeQuestions,
submitQuery,
sx, sx,
}} }}
/> />
} }
}, [filterResumeMessages, hasFacts, sessionId, setSnack, resumeResponse, resetResume, hasResume, submitQuery]); }, [connectionBase, filterResumeMessages, hasFacts, sessionId, setSnack, resumeResponse, resetResume, hasResume]);
/** /**
* Renders the fact check view * Renders the fact check view
@ -315,12 +323,12 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
resetAction: resetFacts, resetAction: resetFacts,
onResponse: factsResponse, onResponse: factsResponse,
sessionId, sessionId,
submitQuery, connectionBase,
setSnack, setSnack,
sx, sx,
}} }}
/> />
}, [ sessionId, setSnack, factsResponse, filterFactsMessages, resetFacts, hasResume, hasFacts, submitQuery]); }, [connectionBase, sessionId, setSnack, factsResponse, filterFactsMessages, resetFacts, hasResume, hasFacts]);
return ( return (
<Box className="ResumeBuilder" <Box className="ResumeBuilder"
@ -361,7 +369,11 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
); );
}; };
export { export type {
ResumeBuilderPage ResumeBuilderProps
};
export {
ResumeBuilder
}; };

View File

@ -1,6 +0,0 @@
.ResumeBuilder .JsonViewScrollable {
min-height: unset !important;
max-height: 30rem !important;
border: 1px solid orange;
overflow-x: auto !important;
}

View File

@ -2,7 +2,7 @@ import React from 'react';
import { MuiMarkdown } from 'mui-markdown'; import { MuiMarkdown } from 'mui-markdown';
import { SxProps, useTheme } from '@mui/material/styles'; import { SxProps, useTheme } from '@mui/material/styles';
import { Link } from '@mui/material'; import { Link } from '@mui/material';
import { ChatQuery } from './ChatQuery'; import { ChatQuery, QueryOptions } from './ChatQuery';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import JsonView from '@uiw/react-json-view'; import JsonView from '@uiw/react-json-view';
import { vscodeTheme } from '@uiw/react-json-view/vscode'; import { vscodeTheme } from '@uiw/react-json-view/vscode';
@ -11,12 +11,12 @@ import { Scrollable } from './Scrollable';
import { jsonrepair } from 'jsonrepair'; import { jsonrepair } from 'jsonrepair';
import './StyledMarkdown.css'; import './StyledMarkdown.css';
import { BackstoryElementProps } from './BackstoryTab';
interface StyledMarkdownProps extends BackstoryElementProps { interface StyledMarkdownProps {
className?: string, className?: string,
content: string, content: string,
sx?: SxProps, sx?: SxProps,
submitQuery?: (prompt: string, tunables?: QueryOptions) => void,
}; };
const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProps) => { const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProps) => {
@ -31,13 +31,10 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
if (className === "lang-mermaid") { if (className === "lang-mermaid") {
return <Mermaid className="Mermaid" chart={content} />; return <Mermaid className="Mermaid" chart={content} />;
} }
if (className === "lang-markdown") {
return <MuiMarkdown children={content} />;
}
if (className === "lang-json") { if (className === "lang-json") {
try { try {
const fixed = jsonrepair(content); const fixed = jsonrepair(content);
return <Scrollable autoscroll className="JsonViewScrollable"> return <Scrollable className="JsonViewScrollable">
<JsonView <JsonView
className="JsonView" className="JsonView"
style={{ style={{
@ -53,7 +50,6 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
displayDataTypes={false} displayDataTypes={false}
objectSortKeys={false} objectSortKeys={false}
collapsed={false} collapsed={false}
shortenTextAfterLength={100}
value={JSON.parse(fixed)}> value={JSON.parse(fixed)}>
<JsonView.String <JsonView.String
render={({ children, ...reset }) => { render={({ children, ...reset }) => {
@ -85,13 +81,17 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
} }
} }
}, },
ChatQuery: { chatQuery: undefined
};
if (submitQuery) {
overrides.ChatQuery = {
component: ChatQuery, component: ChatQuery,
props: { props: {
submitQuery, submitQuery,
}, },
}
}; };
}
return <Box return <Box
className={`MuiMarkdown ${className || ""}`} className={`MuiMarkdown ${className || ""}`}

View File

@ -8,21 +8,16 @@ import Button from '@mui/material/Button';
import SendIcon from '@mui/icons-material/Send'; import SendIcon from '@mui/icons-material/Send';
import FormControlLabel from '@mui/material/FormControlLabel'; import FormControlLabel from '@mui/material/FormControlLabel';
import Switch from '@mui/material/Switch'; import Switch from '@mui/material/Switch';
import { SxProps, Theme } from '@mui/material';
import useMediaQuery from '@mui/material/useMediaQuery'; import useMediaQuery from '@mui/material/useMediaQuery';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import JsonView from '@uiw/react-json-view'; import JsonView from '@uiw/react-json-view';
import { SetSnackType } from './Snack';
import { Scrollable } from './Scrollable'; import { Scrollable } from './Scrollable';
import { StyledMarkdown } from './StyledMarkdown'; import { StyledMarkdown } from './StyledMarkdown';
import { connectionBase } from './Global';
import './VectorVisualizer.css'; import './VectorVisualizer.css';
import { BackstoryPageProps } from './BackstoryTab';
interface VectorVisualizerProps extends BackstoryPageProps {
inline?: boolean;
rag?: any;
};
interface Metadata { interface Metadata {
doc_type?: string; doc_type?: string;
@ -51,8 +46,14 @@ interface PlotData {
layout: Partial<Plotly.Layout>; layout: Partial<Plotly.Layout>;
} }
interface VectorVisualizerProps {
connectionBase: string;
sessionId?: string;
setSnack: SetSnackType;
inline?: boolean;
rag?: any;
sx?: SxProps<Theme>;
}
interface ChromaResult { interface ChromaResult {
distances: number[]; distances: number[];
@ -108,7 +109,7 @@ const symbolMap: Record<string, string> = {
}; };
const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => { const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => {
const { setSnack, rag, inline, sessionId, sx, submitQuery } = props; const { setSnack, rag, inline, connectionBase, sessionId, sx } = props;
const [plotData, setPlotData] = useState<PlotData | null>(null); const [plotData, setPlotData] = useState<PlotData | null>(null);
const [newQuery, setNewQuery] = useState<string>(''); const [newQuery, setNewQuery] = useState<string>('');
const [newQueryEmbedding, setNewQueryEmbedding] = useState<ChromaResult | undefined>(undefined); const [newQueryEmbedding, setNewQueryEmbedding] = useState<ChromaResult | undefined>(undefined);
@ -150,7 +151,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
}; };
fetchCollection(); fetchCollection();
}, [result, setResult, setSnack, sessionId, view2D]) }, [result, setResult, connectionBase, setSnack, sessionId, view2D])
useEffect(() => { useEffect(() => {
if (!result || !result.embeddings) return; if (!result || !result.embeddings) return;
@ -436,7 +437,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
wordBreak: 'break-all', wordBreak: 'break-all',
}} }}
> >
<StyledMarkdown sx={{ p: 1, pt: 0 }} content={tooltip?.content || "Select a node in the visualization."} {...{ sessionId, setSnack, submitQuery }} /> <StyledMarkdown sx={{ p: 1, pt: 0 }} content={tooltip?.content || "Select a node in the visualization."} />
</Scrollable> </Scrollable>
} }
</Card> </Card>
@ -468,21 +469,8 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
); );
}; };
const VectorVisualizerPage: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => {
return <Scrollable
sx={{
maxWidth: "1024px",
height: "calc(100vh - 72px)",
}}
>
<VectorVisualizer {...props} />
</Scrollable>;
};
export type { VectorVisualizerProps, ResultData }; export type { VectorVisualizerProps, ResultData };
export { export {
VectorVisualizer, VectorVisualizer
VectorVisualizerPage
}; };

View File

@ -11,9 +11,9 @@ scrape_configs:
insecure_skip_verify: true insecure_skip_verify: true
- job_name: 'backstory-prod' - job_name: 'backstory-prod'
scrape_interval: 5s scrape_interval: 30s
metrics_path: /metrics metrics_path: /metrics
scheme: http scheme: https
static_configs: static_configs:
- targets: ['backstory-prod:8911'] - targets: ['backstory-prod:8911']
tls_config: tls_config:

View File

@ -197,7 +197,6 @@ class JobDescription(Agent):
job_requirements = message.metadata["generate_factual_tailored_resume"]["job_requirements"]["results"] job_requirements = message.metadata["generate_factual_tailored_resume"]["job_requirements"]["results"]
new_message.response = f"```json\n\n{json.dumps(job_requirements, indent=2)}\n```\n" new_message.response = f"```json\n\n{json.dumps(job_requirements, indent=2)}\n```\n"
new_message.status = "done" new_message.status = "done"
new_message.actions = [ "job_description" ]
self.conversation.add(new_message) self.conversation.add(new_message)
self.system_prompt = system_user_qualifications self.system_prompt = system_user_qualifications