init
9
.dev.vars
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
OPENAI_API_KEY=
|
||||||
|
EVENTSOURCE_HOST=
|
||||||
|
GROQ_API_KEY=
|
||||||
|
ANTHROPIC_API_KEY=
|
||||||
|
FIREWORKS_API_KEY=
|
||||||
|
XAI_API_KEY=
|
||||||
|
CEREBRAS_API_KEY=
|
||||||
|
CLOUDFLARE_API_KEY=
|
||||||
|
CLOUDFLARE_ACCOUNT_ID=
|
34
.github/workflows/update-vpn-blocklist.yaml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: "Update VPN Blocklist"
|
||||||
|
|
||||||
|
on:
|
||||||
|
# uncomment to deploy on next push
|
||||||
|
# push:
|
||||||
|
# branches:
|
||||||
|
# - main
|
||||||
|
workflow_dispatch: # Manual trigger
|
||||||
|
schedule:
|
||||||
|
- cron: "57 8 * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install
|
||||||
|
|
||||||
|
# Step 5: Update block-list-ipv4.txt
|
||||||
|
- name: Update block-list-ipv4.txt
|
||||||
|
run: -|
|
||||||
|
curl https://raw.githubusercontent.com/X4BNet/lists_vpn/refs/heads/main/output/vpn/ipv4.txt > workers/session-proxy/block-list-ipv4.txt
|
||||||
|
|
||||||
|
# Step 6: Deploy application
|
||||||
|
- name: Deploy application
|
||||||
|
env:
|
||||||
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
run: bun deploy:session-proxy:production && bun deploy:session-proxy:staging && bun deploy:session-proxy:dev
|
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
**/node_modules/
|
||||||
|
/dist/
|
||||||
|
**/.wrangler/
|
||||||
|
/.idea/
|
||||||
|
public/sitemap.xml
|
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Geoff Seemueller
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
99
README.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
## open-geoff-seemueller-io
|
||||||
|
|
||||||
|
I am making this available for others to learn from. It is a downstream fork of the source code powering my personal website. Search and attachments are not implemented. I have several more mature variants of this repository which have extended capabilities.
|
||||||
|
|
||||||
|
|
||||||
|
### Stack:
|
||||||
|
- vike
|
||||||
|
- react
|
||||||
|
- cloudflare workers
|
||||||
|
- openai sdk
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
1. `bun i`
|
||||||
|
2. `bun run build`
|
||||||
|
3. Configure .dev.vars
|
||||||
|
4. In isolated shells, run `bun run worker:dev` and `bun run vite:dev`
|
||||||
|
|
||||||
|
|
||||||
|
### Further Documentation
|
||||||
|
Upstream versions contain further documentation, tests, and features. Any of the latter can be made available upon request.
|
||||||
|
|
||||||
|
History
|
||||||
|
---
|
||||||
|
|
||||||
|
### **May 2025**
|
||||||
|
|
||||||
|
| Hash | Change |
|
||||||
|
| ------- | --------------------------------------------------------------------- |
|
||||||
|
| 049bf97 | **Add** *seemueller.ai* sidebar link and constrain Hero heading width |
|
||||||
|
| 6be5f68 | **Consolidate** configuration files (CI, bundler, environment) |
|
||||||
|
| a047f19 | **Expand** Markdown usage guide for end‑users |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **April 2025**
|
||||||
|
|
||||||
|
| Hash | Change |
|
||||||
|
| ----------------- | --------------------------------------------------------------------------- |
|
||||||
|
| ce3457a | **Introduce** custom error page and purge dead code |
|
||||||
|
| 806c933 | **Fix** duplicate`robots.txt` entries (SEO) |
|
||||||
|
| 4bbe8ea · e909e0b | **Restore** bundle‑size safeguards and **switch** toBun as package manager |
|
||||||
|
| 7f1520b·aa71f86 | **Automate** VPN block‑list deployment; retire legacy pull script |
|
||||||
|
| b332c93 | **Repair** CI job for block‑list updates |
|
||||||
|
| d506e7d | **Deprecate** experimental **Mixtral** model |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **March 2025**
|
||||||
|
|
||||||
|
| Hash | Change |
|
||||||
|
| ----------------- | ------------------------------------------------------------------------ |
|
||||||
|
| 8b9e9eb | **Add** per‑model `max_tokens` limits |
|
||||||
|
| cb0d912 | **Expose** Cloudflare AI models for staging |
|
||||||
|
| 85de6ed·cec4f70 | **Shrink** production bundles: re‑enable minifier and drop unused assets |
|
||||||
|
| 4805c7e · 9709f61 | **Refresh** landing‑page copy (“Welcomehome”) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **February 2025**
|
||||||
|
|
||||||
|
| Hash | Change |
|
||||||
|
| ----------------- | --------------------------------------------------------------------------- |
|
||||||
|
| 8d70eef·886d45a | **Ship** runtime theme switching with dynamic navigation colors |
|
||||||
|
| 4efaa93/194b168 | **Polish** resume & selector styling (padding, borders) |
|
||||||
|
| 7f925d1·0b9088a | **Refine** responsive chat: correct breakpoints, input scaling, MobX typing |
|
||||||
|
| 0865897 | **Remove** deprecated DocumentAPI |
|
||||||
|
| e355540 | **Fix** background rendering issues |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **January 2025**
|
||||||
|
|
||||||
|
| Hash | Change |
|
||||||
|
| ----------------- | --------------------------------------------------------------------------- |
|
||||||
|
| d8b47c9 ·361a523 | **Enable** full LaTeX/KaTeX math rendering |
|
||||||
|
| 64a0513·6ecc4f5 | **Set** default model to *llama‑v3p1‑70b‑instruct* and **limit** model list |
|
||||||
|
| 0ad9dc4 | **Add** rate‑limit middleware |
|
||||||
|
| 42f371b·1f526ce | **Launch** VPN blocker with live CIDR validation and CI workflow |
|
||||||
|
| f7464a1 | **Remove** user‑uploaded attachments to cut storage costs |
|
||||||
|
| e9c3a12 | **Rotate** Fireworks API credentials |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Late 2024 Highlights**
|
||||||
|
|
||||||
|
| Area | Notable Work |
|
||||||
|
| ----------------- | ---------------------------------------------------------------------- |
|
||||||
|
| **Generative UX** | Image‑generation pipeline; model‑selection UI; seasonal prompt packs |
|
||||||
|
| **Analytics** | Worker‑based metrics engine, event capture, tail helpers |
|
||||||
|
| **Model Support** | GROQ & Anthropic streaming integrations with attachment handling |
|
||||||
|
| **Feedback Loop** | Modal‑driven user‑feedback feature with dedicated store |
|
||||||
|
| **Payments** | On‑chain ETH/DOGE processor with dynamic deposit addresses |
|
||||||
|
| **Performance** | Tokenizer limits, LightningCSS minifier, esbuild migration |
|
||||||
|
| **Mobile & A11y** | Dynamic textarea sizing, cookie‑consent banner, iMessage‑style bubbles |
|
||||||
|
|
||||||
|
|
||||||
|
### August 2024 - December 2024
|
||||||
|
History is available by request.
|
1
gitleaks-report.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[]
|
106
package.json
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"clean": "rm -rf node_modules && rm -rf .wrangler && rm -rf dist",
|
||||||
|
"build": "pnpm client:build && pnpm worker:build",
|
||||||
|
"vite:dev": "pnpm vite dev --host 0.0.0.0",
|
||||||
|
"worker:dev": "pnpm run build && pnpm wrangler dev",
|
||||||
|
"client": "pnpm vite:dev",
|
||||||
|
"client:build": "vite build",
|
||||||
|
"worker:build": "WRANGLER_LOG=info wrangler build",
|
||||||
|
"agents:dev": "(cd ../web-agent-rs; cargo run)",
|
||||||
|
"agents:docker": "(cd ../web-agent-rs; docker compose up --build)",
|
||||||
|
"dev:session-proxy": "wrangler dev -c workers/session-proxy/wrangler-session-proxy.toml",
|
||||||
|
"dev:image-generation-service": "wrangler dev -c workers/image-generation-service/wrangler-image-generation-service.toml",
|
||||||
|
"dev:email-service": "wrangler dev -c workers/email/wrangler-email.toml",
|
||||||
|
"dev:analytics-service": "wrangler dev -c workers/analytics/wrangler-analytics.toml",
|
||||||
|
"deploy:dev": "CI=true vite build && wrangler deploy --keep-vars=true --minify=true --env dev && pnpm deploy:session-proxy:dev",
|
||||||
|
"deploy:staging": "CI=true vite build && wrangler deploy --minify --env staging && pnpm deploy:session-proxy:staging",
|
||||||
|
"deploy:production": "CI=true vite build && wrangler deploy --minify --env production",
|
||||||
|
"deploy:production:full": "CI=true vite build && wrangler deploy --minify --env production && pnpm deploy:session-proxy:production && ./scripts/update_vpn_blocklist.sh && watch gh run list --workflow=update-vpn-blocklist.yaml",
|
||||||
|
"deploy:session-proxy:dev": "CI=true wrangler deploy --minify -c workers/session-proxy/wrangler-session-proxy.toml --env dev",
|
||||||
|
"deploy:session-proxy:staging": "CI=true wrangler deploy --minify -c workers/session-proxy/wrangler-session-proxy.toml --env staging",
|
||||||
|
"deploy:session-proxy:production": "CI=true wrangler deploy --minify -c workers/session-proxy/wrangler-session-proxy.toml --env production",
|
||||||
|
"deploy:rate-limiter": "CI=true wrangler deploy --minify -c workers/rate-limiter/wrangler-rate-limiter.toml",
|
||||||
|
"deploy:image-generation-service": "wrangler deploy -c workers/image-generation-service/wrangler-image-generation-service.toml",
|
||||||
|
"deploy:email-service": "wrangler deploy -c workers/email/wrangler-email.toml",
|
||||||
|
"deploy:analytics-service": "wrangler deploy -c workers/analytics/wrangler-analytics.toml",
|
||||||
|
"deploy:next": "pnpm clean && pnpm install --frozen-lockfile && pnpm deploy:staging && pnpm deploy:production",
|
||||||
|
"deploy:all": "pnpm deploy:dev && pnpm deploy:staging && pnpm deploy:production",
|
||||||
|
"tail:dev": "wrangler tail",
|
||||||
|
"tail:staging": "wrangler tail --env staging",
|
||||||
|
"tail:production": "wrangler tail --env production",
|
||||||
|
"tail:email-service": "wrangler tail -c workers/email/wrangler-email.toml",
|
||||||
|
"tail:analytics-service": "wrangler tail -c workers/analytics/wrangler-analytics.toml",
|
||||||
|
"tail:image-generation-service": "wrangler tail -c workers/image-generation-service/wrangler-image-generation-service.toml",
|
||||||
|
"tail:session-proxy": "wrangler tail -c workers/session-proxy/wrangler-session-proxy.toml --env production"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/runtime-corejs3": "^7.26.0",
|
||||||
|
"babel-plugin-inferno": "^6.7.2",
|
||||||
|
"compression": "^1.7.5",
|
||||||
|
"express": "^4.21.2",
|
||||||
|
"kill-port": "^2.0.1",
|
||||||
|
"llama3-tokenizer-js": "^1.2.0",
|
||||||
|
"mimetext": "^3.0.24",
|
||||||
|
"replicate": "^1.0.1",
|
||||||
|
"scheduler": "^0.23.2",
|
||||||
|
"suspend-react": "^0.1.3",
|
||||||
|
"together-ai": "^0.7.0",
|
||||||
|
"@anthropic-ai/sdk": "^0.32.1",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||||
|
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||||
|
"@babel/plugin-transform-react-jsx": "^7.25.9",
|
||||||
|
"@babel/plugin-transform-runtime": "^7.25.9",
|
||||||
|
"@babel/preset-env": "^7.26.0",
|
||||||
|
"@babel/preset-react": "^7.26.3",
|
||||||
|
"@babel/preset-typescript": "^7.26.0",
|
||||||
|
"@babel/runtime": "^7.26.9",
|
||||||
|
"@chakra-ui/react": "^2.10.6",
|
||||||
|
"@cloudflare/workers-types": "^4.20241205.0",
|
||||||
|
"@emotion/react": "^11.13.5",
|
||||||
|
"@emotion/styled": "^11.13.5",
|
||||||
|
"@mdxeditor/editor": "^3.20.0",
|
||||||
|
"@types/marked": "^6.0.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"chokidar": "^4.0.1",
|
||||||
|
"framer-motion": "^11.13.1",
|
||||||
|
"gpt-tokenizer": "^2.7.0",
|
||||||
|
"hastscript": "^9.0.0",
|
||||||
|
"isomorphic-dompurify": "^2.19.0",
|
||||||
|
"itty-router": "^5.0.18",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
|
"katex": "^0.16.20",
|
||||||
|
"lucide-react": "^0.436.0",
|
||||||
|
"manifold-workflow-engine": "^2.0.2",
|
||||||
|
"marked": "^15.0.4",
|
||||||
|
"marked-extended-latex": "^1.1.0",
|
||||||
|
"marked-footnote": "^1.2.4",
|
||||||
|
"marked-katex-extension": "^5.1.4",
|
||||||
|
"mobx": "^6.13.5",
|
||||||
|
"mobx-react-lite": "^4.0.7",
|
||||||
|
"mobx-state-tree": "^6.0.1",
|
||||||
|
"moo": "^0.5.2",
|
||||||
|
"openai": "^4.76.0",
|
||||||
|
"qrcode.react": "^4.1.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-icons": "^5.4.0",
|
||||||
|
"react-markdown": "^9.0.1",
|
||||||
|
"react-streaming": "^0.3.44",
|
||||||
|
"react-textarea-autosize": "^8.5.5",
|
||||||
|
"rehype-katex": "^7.0.1",
|
||||||
|
"rehype-react": "^8.0.0",
|
||||||
|
"remark-gfm": "^4.0.0",
|
||||||
|
"remark-math": "^6.0.0",
|
||||||
|
"remark-parse": "^11.0.0",
|
||||||
|
"remark-rehype": "^11.1.1",
|
||||||
|
"shiki": "^1.24.0",
|
||||||
|
"terser": "^5.39.0",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vike": "0.4.193",
|
||||||
|
"vite": "^5.4.11",
|
||||||
|
"wrangler": "^4.14.4",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
public/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 8.8 KiB |
44
public/cfga.min.js
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
!(function (t, e, n) {
|
||||||
|
var a = t.screen,
|
||||||
|
r = encodeURIComponent,
|
||||||
|
o = Math.max,
|
||||||
|
i = t.performance,
|
||||||
|
d = i && i.timing,
|
||||||
|
c = function (t) {
|
||||||
|
return isNaN(t) || t == 1 / 0 || t < 0 ? void 0 : t;
|
||||||
|
},
|
||||||
|
g = function (t) {
|
||||||
|
return Math.random().toString(36).slice(-t);
|
||||||
|
},
|
||||||
|
m = function (t) {
|
||||||
|
return Math.ceil(Math.random() * (t - 1)) + 1;
|
||||||
|
};
|
||||||
|
function s() {
|
||||||
|
var i = [
|
||||||
|
g(m(4)) + "=" + g(m(6)),
|
||||||
|
"ga=" + t.ga_tid,
|
||||||
|
"dt=" + r(e.title),
|
||||||
|
"de=" + r(e.characterSet || e.charset),
|
||||||
|
"dr=" + r(e.referrer),
|
||||||
|
"ul=" + (n.language || n.browserLanguage || n.userLanguage),
|
||||||
|
"sd=" + a.colorDepth + "-bit",
|
||||||
|
"sr=" + a.width + "x" + a.height,
|
||||||
|
"vp=" +
|
||||||
|
o(e.documentElement.clientWidth, t.innerWidth || 0) +
|
||||||
|
"x" +
|
||||||
|
o(e.documentElement.clientHeight, t.innerHeight || 0),
|
||||||
|
"plt=" + c(d.loadEventStart - d.navigationStart || 0),
|
||||||
|
"dns=" + c(d.domainLookupEnd - d.domainLookupStart || 0),
|
||||||
|
"pdt=" + c(d.responseEnd - d.responseStart || 0),
|
||||||
|
"rrt=" + c(d.redirectEnd - d.redirectStart || 0),
|
||||||
|
"tcp=" + c(d.connectEnd - d.connectStart || 0),
|
||||||
|
"srt=" + c(d.responseStart - d.requestStart || 0),
|
||||||
|
"dit=" + c(d.domInteractive - d.domLoading || 0),
|
||||||
|
"clt=" + c(d.domContentLoadedEventStart - d.navigationStart || 0),
|
||||||
|
"z=" + Date.now(),
|
||||||
|
];
|
||||||
|
(t.__ga_img = new Image()), (t.__ga_img.src = t.ga_api + "?" + i.join("&"));
|
||||||
|
}
|
||||||
|
(t.cfga = s),
|
||||||
|
"complete" === e.readyState ? s() : t.addEventListener("load", s);
|
||||||
|
})(window, document, navigator);
|
BIN
public/code-tokenizer-md.jpg
Normal file
After Width: | Height: | Size: 638 KiB |
BIN
public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 563 B |
BIN
public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
public/general-problem-solver.png
Normal file
After Width: | Height: | Size: 534 KiB |
BIN
public/me.png
Normal file
After Width: | Height: | Size: 373 KiB |
BIN
public/reactive-state-machine-4.png
Normal file
After Width: | Height: | Size: 1.4 MiB |
BIN
public/reactive_state_machine_5.png
Normal file
After Width: | Height: | Size: 1.4 MiB |
BIN
public/rehoboam.png
Normal file
After Width: | Height: | Size: 165 KiB |
7
public/robots.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
Allow: /connect
|
||||||
|
Disallow: /api
|
||||||
|
Disallow: /assets
|
||||||
|
|
||||||
|
Sitemap: https://geoff.seemueller.io/sitemap.xml
|
19
public/site.webmanifest
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"short_name": "",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#fffff0",
|
||||||
|
"background_color": "#000000",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
BIN
public/static/fonts/KaTeX_AMS-Regular.ttf
Normal file
BIN
public/static/fonts/KaTeX_AMS-Regular.woff
Normal file
BIN
public/static/fonts/KaTeX_AMS-Regular.woff2
Normal file
BIN
public/static/fonts/KaTeX_Caligraphic-Bold.ttf
Normal file
BIN
public/static/fonts/KaTeX_Caligraphic-Bold.woff
Normal file
BIN
public/static/fonts/KaTeX_Caligraphic-Bold.woff2
Normal file
BIN
public/static/fonts/KaTeX_Caligraphic-Regular.ttf
Normal file
BIN
public/static/fonts/KaTeX_Caligraphic-Regular.woff
Normal file
BIN
public/static/fonts/KaTeX_Caligraphic-Regular.woff2
Normal file
BIN
public/static/fonts/KaTeX_Fraktur-Bold.ttf
Normal file
BIN
public/static/fonts/KaTeX_Fraktur-Bold.woff
Normal file
BIN
public/static/fonts/KaTeX_Fraktur-Bold.woff2
Normal file
BIN
public/static/fonts/KaTeX_Fraktur-Regular.ttf
Normal file
BIN
public/static/fonts/KaTeX_Fraktur-Regular.woff
Normal file
BIN
public/static/fonts/KaTeX_Fraktur-Regular.woff2
Normal file
BIN
public/static/fonts/KaTeX_Main-Bold.ttf
Normal file
BIN
public/static/fonts/KaTeX_Main-Bold.woff
Normal file
BIN
public/static/fonts/KaTeX_Main-Bold.woff2
Normal file
BIN
public/static/fonts/KaTeX_Main-BoldItalic.ttf
Normal file
BIN
public/static/fonts/KaTeX_Main-BoldItalic.woff
Normal file
BIN
public/static/fonts/KaTeX_Main-BoldItalic.woff2
Normal file
BIN
public/static/fonts/KaTeX_Main-Italic.ttf
Normal file
BIN
public/static/fonts/KaTeX_Main-Italic.woff
Normal file
BIN
public/static/fonts/KaTeX_Main-Italic.woff2
Normal file
BIN
public/static/fonts/KaTeX_Main-Regular.ttf
Normal file
BIN
public/static/fonts/KaTeX_Main-Regular.woff
Normal file
BIN
public/static/fonts/KaTeX_Main-Regular.woff2
Normal file
BIN
public/static/fonts/KaTeX_Math-BoldItalic.ttf
Normal file
BIN
public/static/fonts/KaTeX_Math-BoldItalic.woff
Normal file
BIN
public/static/fonts/KaTeX_Math-BoldItalic.woff2
Normal file
BIN
public/static/fonts/KaTeX_Math-Italic.ttf
Normal file
BIN
public/static/fonts/KaTeX_Math-Italic.woff
Normal file
BIN
public/static/fonts/KaTeX_Math-Italic.woff2
Normal file
BIN
public/static/fonts/KaTeX_SansSerif-Bold.ttf
Normal file
BIN
public/static/fonts/KaTeX_SansSerif-Bold.woff
Normal file
BIN
public/static/fonts/KaTeX_SansSerif-Bold.woff2
Normal file
BIN
public/static/fonts/KaTeX_SansSerif-Italic.ttf
Normal file
BIN
public/static/fonts/KaTeX_SansSerif-Italic.woff
Normal file
BIN
public/static/fonts/KaTeX_SansSerif-Italic.woff2
Normal file
BIN
public/static/fonts/KaTeX_SansSerif-Regular.ttf
Normal file
BIN
public/static/fonts/KaTeX_SansSerif-Regular.woff
Normal file
BIN
public/static/fonts/KaTeX_SansSerif-Regular.woff2
Normal file
BIN
public/static/fonts/KaTeX_Script-Regular.ttf
Normal file
BIN
public/static/fonts/KaTeX_Script-Regular.woff
Normal file
BIN
public/static/fonts/KaTeX_Script-Regular.woff2
Normal file
BIN
public/static/fonts/KaTeX_Size1-Regular.ttf
Normal file
BIN
public/static/fonts/KaTeX_Size1-Regular.woff
Normal file
BIN
public/static/fonts/KaTeX_Size1-Regular.woff2
Normal file
BIN
public/static/fonts/KaTeX_Size2-Regular.ttf
Normal file
BIN
public/static/fonts/KaTeX_Size2-Regular.woff
Normal file
BIN
public/static/fonts/KaTeX_Size2-Regular.woff2
Normal file
BIN
public/static/fonts/KaTeX_Size3-Regular.ttf
Normal file
BIN
public/static/fonts/KaTeX_Size3-Regular.woff
Normal file
BIN
public/static/fonts/KaTeX_Size3-Regular.woff2
Normal file
BIN
public/static/fonts/KaTeX_Size4-Regular.ttf
Normal file
BIN
public/static/fonts/KaTeX_Size4-Regular.woff
Normal file
BIN
public/static/fonts/KaTeX_Size4-Regular.woff2
Normal file
BIN
public/static/fonts/KaTeX_Typewriter-Regular.ttf
Normal file
BIN
public/static/fonts/KaTeX_Typewriter-Regular.woff
Normal file
BIN
public/static/fonts/KaTeX_Typewriter-Regular.woff2
Normal file
31
scripts/check-analytics.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
const TOKEN = "";
|
||||||
|
const ACCOUNT_ID = "";
|
||||||
|
|
||||||
|
async function showTables() {
|
||||||
|
const url = `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/analytics_engine/sql`;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${TOKEN}`,
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: "SHOW TABLES",
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("Sending request to Cloudflare Analytics Engine...");
|
||||||
|
const response = await fetch(url, options);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log("Response received:", JSON.stringify(data, null, 2));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error occurred:", error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showTables();
|
30
scripts/gen_sitemap.js
Executable file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
const currentDate = new Date().toISOString().split("T")[0];
|
||||||
|
|
||||||
|
const sitemapTemplate = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 ">
|
||||||
|
<url>
|
||||||
|
<loc>https://geoff.seemueller.io/</loc>
|
||||||
|
<lastmod>${currentDate}</lastmod>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://geoff.seemueller.io/connect</loc>
|
||||||
|
<lastmod>${currentDate}</lastmod>
|
||||||
|
<priority>0.7</priority>
|
||||||
|
</url>
|
||||||
|
</urlset>`;
|
||||||
|
|
||||||
|
const sitemapPath = "./public/sitemap.xml";
|
||||||
|
|
||||||
|
fs.writeFile(sitemapPath, sitemapTemplate, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Error writing sitemap file:", err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log("Sitemap updated successfully with current date:", currentDate);
|
||||||
|
});
|
22
scripts/get_groq_models.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
(async () => {
|
||||||
|
// Run the script with bun so it automatically picks up the env
|
||||||
|
const apiKey = process.env.GROQ_API_KEY;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("https://api.groq.com/openai/v1/models", {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(JSON.stringify(data));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching data:", error);
|
||||||
|
}
|
||||||
|
})();
|
36
scripts/killport.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import * as child_process from "node:child_process";
|
||||||
|
|
||||||
|
export const killProcessOnPort = (port) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
child_process.exec(`lsof -t -i :${port}`.trim(), (err, stdout) => {
|
||||||
|
if (err) {
|
||||||
|
if (err.code !== 1) {
|
||||||
|
console.error(`Error finding process on port ${port}:`, err);
|
||||||
|
return reject(err);
|
||||||
|
} else {
|
||||||
|
console.log(`No process found on port ${port}`);
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pid = stdout.trim();
|
||||||
|
if (!pid) {
|
||||||
|
console.log(`No process is currently running on port ${port}`);
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
child_process.exec(`kill -9 ${pid}`.trim(), (killErr) => {
|
||||||
|
if (killErr) {
|
||||||
|
console.error(
|
||||||
|
`Failed to kill process ${pid} on port ${port}`,
|
||||||
|
killErr,
|
||||||
|
);
|
||||||
|
return reject(killErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Successfully killed process ${pid} on port ${port}`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
3
scripts/update_vpn_blocklist.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
gh workflow run "Update VPN Blocklist"
|
12
secrets.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"OPENAI_API_KEY": "",
|
||||||
|
"OPENAI_API_ENDPOINT": "",
|
||||||
|
"PERIGON_API_KEY": "",
|
||||||
|
"EVENTSOURCE_HOST": "",
|
||||||
|
"GROQ_API_KEY": "",
|
||||||
|
"ANTHROPIC_API_KEY": "",
|
||||||
|
"FIREWORKS_API_KEY": "",
|
||||||
|
"GEMINI_API_KEY": "",
|
||||||
|
"XAI_API_KEY": "",
|
||||||
|
"CEREBRAS_API_KEY": ""
|
||||||
|
}
|
26
src/components/BuiltWithButton.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { IconButton } from "@chakra-ui/react";
|
||||||
|
import { LucideHammer } from "lucide-react";
|
||||||
|
import { toolbarButtonZIndex } from "./toolbar/Toolbar";
|
||||||
|
|
||||||
|
export default function BuiltWithButton() {
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
aria-label="Build Info"
|
||||||
|
icon={<LucideHammer />}
|
||||||
|
size="md"
|
||||||
|
bg="transparent"
|
||||||
|
stroke="text.accent"
|
||||||
|
color="text.accent"
|
||||||
|
onClick={() => alert("Built by Geoff Seemueller")}
|
||||||
|
_hover={{
|
||||||
|
bg: "transparent",
|
||||||
|
svg: {
|
||||||
|
stroke: "accent.secondary",
|
||||||
|
transition: "stroke 0.3s ease-in-out",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
zIndex={toolbarButtonZIndex}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
53
src/components/ThemeSelection.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { getColorThemes } from "../layout/theme/color-themes";
|
||||||
|
import { Center, IconButton, VStack } from "@chakra-ui/react";
|
||||||
|
import userOptionsStore from "../stores/UserOptionsStore";
|
||||||
|
import { Circle } from "lucide-react";
|
||||||
|
import { toolbarButtonZIndex } from "./toolbar/Toolbar";
|
||||||
|
import React from "react";
|
||||||
|
import { useIsMobile } from "./contexts/MobileContext";
|
||||||
|
|
||||||
|
export function ThemeSelectionOptions() {
|
||||||
|
const children = [];
|
||||||
|
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
for (const theme of getColorThemes()) {
|
||||||
|
children.push(
|
||||||
|
<IconButton
|
||||||
|
as="div"
|
||||||
|
key={theme.name}
|
||||||
|
onClick={() => userOptionsStore.selectTheme(theme.name)}
|
||||||
|
size="xs"
|
||||||
|
icon={
|
||||||
|
<Circle
|
||||||
|
size={!isMobile ? 16 : 20}
|
||||||
|
stroke="transparent"
|
||||||
|
style={{
|
||||||
|
background: `conic-gradient(${theme.colors.background.primary.startsWith("#") ? theme.colors.background.primary : theme.colors.background.secondary} 0 50%, ${theme.colors.text.secondary} 50% 100%)`,
|
||||||
|
borderRadius: "50%",
|
||||||
|
boxShadow: "0 0 0.5px 0.25px #fff",
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: "background 0.2s",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
bg="transparent"
|
||||||
|
borderRadius="50%" // Ensures the button has a circular shape
|
||||||
|
stroke="transparent"
|
||||||
|
color="transparent"
|
||||||
|
_hover={{
|
||||||
|
svg: {
|
||||||
|
transition: "stroke 0.3s ease-in-out", // Smooth transition effect
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
zIndex={toolbarButtonZIndex}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack align={!isMobile ? "end" : "start"} p={1.2}>
|
||||||
|
<Center>{children}</Center>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
84
src/components/WelcomeHomeMessage.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { Box, Center, VStack } from "@chakra-ui/react";
|
||||||
|
import {
|
||||||
|
welcome_home_text,
|
||||||
|
welcome_home_tip,
|
||||||
|
} from "../static-data/welcome_home_text";
|
||||||
|
import CustomMarkdownRenderer, {
|
||||||
|
WelcomeHomeMarkdownRenderer,
|
||||||
|
} from "./chat/CustomMarkdownRenderer";
|
||||||
|
|
||||||
|
function WelcomeHomeMessage({ visible }) {
|
||||||
|
const containerVariants = {
|
||||||
|
visible: {
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hidden: {
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.05,
|
||||||
|
staggerDirection: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const textVariants = {
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.5,
|
||||||
|
ease: [0.165, 0.84, 0.44, 1],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hidden: {
|
||||||
|
opacity: 0,
|
||||||
|
y: 20,
|
||||||
|
transition: {
|
||||||
|
duration: 0.3,
|
||||||
|
ease: [0.165, 0.84, 0.44, 1],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Center>
|
||||||
|
<VStack spacing={8} align="center" maxW="400px">
|
||||||
|
{/* Welcome Message */}
|
||||||
|
<Box
|
||||||
|
fontSize="sm"
|
||||||
|
fontStyle="italic"
|
||||||
|
textAlign="center"
|
||||||
|
color="text.secondary"
|
||||||
|
mt={4}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate={visible ? "visible" : "hidden"}
|
||||||
|
>
|
||||||
|
<Box userSelect={"none"}>
|
||||||
|
<motion.div variants={textVariants}>
|
||||||
|
<WelcomeHomeMarkdownRenderer markdown={welcome_home_text} />
|
||||||
|
</motion.div>
|
||||||
|
</Box>
|
||||||
|
</motion.div>
|
||||||
|
</Box>
|
||||||
|
<motion.div variants={textVariants}>
|
||||||
|
<Box
|
||||||
|
fontSize="sm"
|
||||||
|
fontStyle="italic"
|
||||||
|
textAlign="center"
|
||||||
|
color="text.secondary"
|
||||||
|
mt={1}
|
||||||
|
>
|
||||||
|
<CustomMarkdownRenderer markdown={welcome_home_tip} />
|
||||||
|
</Box>
|
||||||
|
</motion.div>
|
||||||
|
</VStack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WelcomeHomeMessage;
|
44
src/components/about/AboutComponent.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Grid, GridItem, Image, Text } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
const fontSize = "md";
|
||||||
|
|
||||||
|
function AboutComponent() {
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
templateColumns="1fr"
|
||||||
|
gap={4}
|
||||||
|
maxW={["100%", "100%", "100%"]}
|
||||||
|
mx="auto"
|
||||||
|
className="about-container"
|
||||||
|
>
|
||||||
|
<GridItem colSpan={1} justifySelf="center" mb={[6, 6, 8]}>
|
||||||
|
<Image
|
||||||
|
src="/me.png"
|
||||||
|
alt="Geoff Seemueller"
|
||||||
|
borderRadius="full"
|
||||||
|
boxSize={["120px", "150px"]}
|
||||||
|
objectFit="cover"
|
||||||
|
/>
|
||||||
|
</GridItem>
|
||||||
|
<GridItem
|
||||||
|
colSpan={1}
|
||||||
|
maxW={["100%", "100%", "container.md"]}
|
||||||
|
justifySelf="center"
|
||||||
|
minH={"100%"}
|
||||||
|
>
|
||||||
|
<Grid templateColumns="1fr" gap={4} overflowY={"auto"}>
|
||||||
|
<GridItem>
|
||||||
|
<Text fontSize={fontSize}>
|
||||||
|
If you're interested in collaborating on innovative projects that
|
||||||
|
push technological boundaries and create real value, I'd be keen
|
||||||
|
to connect and explore potential opportunities.
|
||||||
|
</Text>
|
||||||
|
</GridItem>
|
||||||
|
</Grid>
|
||||||
|
</GridItem>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AboutComponent;
|
40
src/components/chat/Attachments.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { IconButton, Tag, TagCloseButton, TagLabel } from "@chakra-ui/react";
|
||||||
|
import { PaperclipIcon } from "lucide-react";
|
||||||
|
|
||||||
|
// Add a new component for UploadedItem
|
||||||
|
export const UploadedItem: React.FC<{
|
||||||
|
url: string;
|
||||||
|
onRemove: () => void;
|
||||||
|
name: string;
|
||||||
|
}> = ({ url, onRemove, name }) => (
|
||||||
|
<Tag size="md" borderRadius="full" variant="solid" colorScheme="teal">
|
||||||
|
<TagLabel>{name || url.split("/").pop()}</TagLabel>
|
||||||
|
<TagCloseButton onClick={onRemove} />
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AttachmentButton: React.FC<{
|
||||||
|
onClick: () => void;
|
||||||
|
disabled: boolean;
|
||||||
|
}> = ({ onClick, disabled }) => (
|
||||||
|
<IconButton
|
||||||
|
aria-label="Attach"
|
||||||
|
title="Attach"
|
||||||
|
bg="transparent"
|
||||||
|
color="text.tertiary"
|
||||||
|
icon={<PaperclipIcon size={"1.3337rem"} />}
|
||||||
|
onClick={onClick}
|
||||||
|
_hover={{
|
||||||
|
bg: "transparent",
|
||||||
|
svg: {
|
||||||
|
stroke: "accent.secondary",
|
||||||
|
transition: "stroke 0.3s ease-in-out",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
isDisabled={disabled}
|
||||||
|
_focus={{ boxShadow: "none" }}
|
||||||
|
/>
|
||||||
|
);
|
75
src/components/chat/Chat.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Box, Grid, GridItem } from "@chakra-ui/react";
|
||||||
|
import ChatMessages from "./ChatMessages";
|
||||||
|
import ChatInput from "./ChatInput";
|
||||||
|
import chatStore from "../../stores/ClientChatStore";
|
||||||
|
import menuState from "../../stores/AppMenuStore";
|
||||||
|
import WelcomeHomeMessage from "../WelcomeHomeMessage";
|
||||||
|
|
||||||
|
const Chat = observer(({ height, width }) => {
|
||||||
|
const scrollRef = useRef();
|
||||||
|
const [isAndroid, setIsAndroid] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
setIsAndroid(/android/i.test(window.navigator.userAgent));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
templateRows="1fr auto"
|
||||||
|
templateColumns="1fr"
|
||||||
|
height={height}
|
||||||
|
width={width}
|
||||||
|
gap={0}
|
||||||
|
>
|
||||||
|
<GridItem alignSelf="center" hidden={!(chatStore.messages.length < 1)}>
|
||||||
|
<WelcomeHomeMessage visible={chatStore.messages.length < 1} />
|
||||||
|
</GridItem>
|
||||||
|
|
||||||
|
<GridItem
|
||||||
|
overflow="auto"
|
||||||
|
width="100%"
|
||||||
|
maxH="100%"
|
||||||
|
ref={scrollRef}
|
||||||
|
// If there are attachments, use "100px". Otherwise, use "128px" on Android, "73px" elsewhere.
|
||||||
|
pb={
|
||||||
|
chatStore.attachments.length > 0
|
||||||
|
? "100px"
|
||||||
|
: isAndroid
|
||||||
|
? "128px"
|
||||||
|
: "73px"
|
||||||
|
}
|
||||||
|
alignSelf="flex-end"
|
||||||
|
>
|
||||||
|
<ChatMessages scrollRef={scrollRef} />
|
||||||
|
</GridItem>
|
||||||
|
|
||||||
|
<GridItem
|
||||||
|
position="relative"
|
||||||
|
bg="background.primary"
|
||||||
|
zIndex={1000}
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
w="100%"
|
||||||
|
display="flex"
|
||||||
|
justifyContent="center"
|
||||||
|
mx="auto"
|
||||||
|
hidden={menuState.isOpen}
|
||||||
|
>
|
||||||
|
<ChatInput
|
||||||
|
input={chatStore.input}
|
||||||
|
setInput={(value) => chatStore.setInput(value)}
|
||||||
|
handleSendMessage={chatStore.sendMessage}
|
||||||
|
isLoading={chatStore.isLoading}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</GridItem>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Chat;
|
157
src/components/chat/ChatInput.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Grid,
|
||||||
|
GridItem,
|
||||||
|
useBreakpointValue,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import chatStore from "../../stores/ClientChatStore";
|
||||||
|
import InputMenu from "./flyoutmenu/InputMenu";
|
||||||
|
import InputTextarea from "./ChatInputTextArea";
|
||||||
|
import SendButton from "./ChatInputSendButton";
|
||||||
|
import { useMaxWidth } from "../../layout/useMaxWidth";
|
||||||
|
import userOptionsStore from "../../stores/UserOptionsStore";
|
||||||
|
|
||||||
|
const ChatInput = observer(() => {
|
||||||
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const maxWidth = useMaxWidth();
|
||||||
|
const [inputValue, setInputValue] = useState<string>("");
|
||||||
|
|
||||||
|
const [containerHeight, setContainerHeight] = useState(56);
|
||||||
|
const [containerBorderRadius, setContainerBorderRadius] = useState(9999);
|
||||||
|
|
||||||
|
const [shouldFollow, setShouldFollow] = useState<boolean>(
|
||||||
|
userOptionsStore.followModeEnabled,
|
||||||
|
);
|
||||||
|
const [couldFollow, setCouldFollow] = useState<boolean>(chatStore.isLoading);
|
||||||
|
|
||||||
|
const [inputWidth, setInputWidth] = useState<string>("50%");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShouldFollow(chatStore.isLoading && userOptionsStore.followModeEnabled);
|
||||||
|
setCouldFollow(chatStore.isLoading);
|
||||||
|
}, [chatStore.isLoading, userOptionsStore.followModeEnabled]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
setInputValue(chatStore.input);
|
||||||
|
}, [chatStore.input]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
const observer = new ResizeObserver((entries) => {
|
||||||
|
for (let entry of entries) {
|
||||||
|
const newHeight = entry.target.clientHeight;
|
||||||
|
setContainerHeight(newHeight);
|
||||||
|
|
||||||
|
const newBorderRadius = Math.max(28 - (newHeight - 56) * 0.2, 16);
|
||||||
|
setContainerBorderRadius(newBorderRadius);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(containerRef.current);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
chatStore.sendMessage();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
chatStore.sendMessage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputMaxWidth = useBreakpointValue(
|
||||||
|
{ base: "50rem", lg: "50rem", md: "80%", sm: "100vw" },
|
||||||
|
{ ssr: true },
|
||||||
|
);
|
||||||
|
const inputMinWidth = useBreakpointValue({ lg: "40rem" }, { ssr: true });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInputWidth("100%");
|
||||||
|
}, [inputMaxWidth, inputMinWidth]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
width={inputWidth}
|
||||||
|
maxW={inputMaxWidth}
|
||||||
|
minWidth={inputMinWidth}
|
||||||
|
mx="auto"
|
||||||
|
p={2}
|
||||||
|
pl={2}
|
||||||
|
pb={`calc(env(safe-area-inset-bottom) + 16px)`}
|
||||||
|
bottom={0}
|
||||||
|
position="fixed"
|
||||||
|
zIndex={1000}
|
||||||
|
>
|
||||||
|
{couldFollow && (
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top={-8}
|
||||||
|
right={0}
|
||||||
|
zIndex={1001}
|
||||||
|
display="flex"
|
||||||
|
justifyContent="flex-end"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
colorScheme="blue"
|
||||||
|
onClick={(_) => {
|
||||||
|
userOptionsStore.toggleFollowMode();
|
||||||
|
}}
|
||||||
|
isDisabled={!chatStore.isLoading}
|
||||||
|
>
|
||||||
|
{shouldFollow ? "Disable Follow Mode" : "Enable Follow Mode"}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Grid
|
||||||
|
ref={containerRef}
|
||||||
|
p={2}
|
||||||
|
bg="background.secondary"
|
||||||
|
borderRadius={`${containerBorderRadius}px`}
|
||||||
|
templateColumns="auto 1fr auto"
|
||||||
|
gap={2}
|
||||||
|
alignItems="center"
|
||||||
|
style={{
|
||||||
|
transition: "border-radius 0.2s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GridItem>
|
||||||
|
<InputMenu
|
||||||
|
selectedModel={chatStore.model}
|
||||||
|
onSelectModel={chatStore.setModel}
|
||||||
|
isDisabled={chatStore.isLoading}
|
||||||
|
/>
|
||||||
|
</GridItem>
|
||||||
|
<GridItem>
|
||||||
|
<InputTextarea
|
||||||
|
inputRef={inputRef}
|
||||||
|
value={chatStore.input}
|
||||||
|
onChange={chatStore.setInput}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
isLoading={chatStore.isLoading}
|
||||||
|
/>
|
||||||
|
</GridItem>
|
||||||
|
<GridItem>
|
||||||
|
<SendButton
|
||||||
|
isLoading={chatStore.isLoading}
|
||||||
|
isDisabled={chatStore.isLoading || !chatStore.input.trim()}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
/>
|
||||||
|
</GridItem>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ChatInput;
|
55
src/components/chat/ChatInputSendButton.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Button } from "@chakra-ui/react";
|
||||||
|
import clientChatStore from "../../stores/ClientChatStore";
|
||||||
|
import { CirclePause, Send } from "lucide-react";
|
||||||
|
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
interface SendButtonProps {
|
||||||
|
isLoading: boolean;
|
||||||
|
isDisabled: boolean;
|
||||||
|
onClick: (e: React.FormEvent) => void;
|
||||||
|
onStop?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SendButton: React.FC<SendButtonProps> = ({ onClick }) => {
|
||||||
|
const isDisabled =
|
||||||
|
clientChatStore.input.trim().length === 0 && !clientChatStore.isLoading;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={(e) =>
|
||||||
|
clientChatStore.isLoading
|
||||||
|
? clientChatStore.stopIncomingMessage()
|
||||||
|
: onClick(e)
|
||||||
|
}
|
||||||
|
bg="transparent"
|
||||||
|
color={
|
||||||
|
clientChatStore.input.trim().length <= 1 ? "brand.700" : "text.primary"
|
||||||
|
}
|
||||||
|
borderRadius="full"
|
||||||
|
p={2}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
_hover={{ bg: !isDisabled ? "rgba(255, 255, 255, 0.2)" : "inherit" }}
|
||||||
|
_active={{ bg: !isDisabled ? "rgba(255, 255, 255, 0.3)" : "inherit" }}
|
||||||
|
_focus={{ boxShadow: "none" }}
|
||||||
|
>
|
||||||
|
{clientChatStore.isLoading ? <MySpinner /> : <Send size={20} />}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MySpinner = ({ onClick }) => (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.9 }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.4,
|
||||||
|
ease: "easeInOut",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CirclePause color={"#F0F0F0"} size={24} onClick={onClick} />
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default SendButton;
|
149
src/components/chat/ChatInputTextArea.tsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
AlertIcon,
|
||||||
|
Box,
|
||||||
|
chakra,
|
||||||
|
HStack,
|
||||||
|
InputGroup,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import fileUploadStore from "../../stores/FileUploadStore";
|
||||||
|
import { UploadedItem } from "./Attachments";
|
||||||
|
import AutoResize from "react-textarea-autosize";
|
||||||
|
|
||||||
|
const AutoResizeTextArea = chakra(AutoResize);
|
||||||
|
|
||||||
|
interface InputTextAreaProps {
|
||||||
|
inputRef: React.RefObject<HTMLTextAreaElement>;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InputTextArea: React.FC<InputTextAreaProps> = observer(
|
||||||
|
({ inputRef, value, onChange, onKeyDown, isLoading }) => {
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleAttachmentClick = () => {
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
fileUploadStore.uploadFile(file, "/api/documents");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveUploadedItem = (url: string) => {
|
||||||
|
fileUploadStore.removeUploadedFile(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [heightConstraint, setHeightConstraint] = useState<
|
||||||
|
number | undefined
|
||||||
|
>(10);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value.length > 10) {
|
||||||
|
setHeightConstraint();
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
position="relative"
|
||||||
|
width="100%"
|
||||||
|
height={heightConstraint}
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
>
|
||||||
|
{/* Attachments Section */}
|
||||||
|
{fileUploadStore.uploadResults.length > 0 && (
|
||||||
|
<HStack
|
||||||
|
spacing={2}
|
||||||
|
mb={2}
|
||||||
|
overflowX="auto"
|
||||||
|
css={{ "&::-webkit-scrollbar": { display: "none" } }}
|
||||||
|
// Ensure attachments wrap if needed
|
||||||
|
flexWrap="wrap"
|
||||||
|
>
|
||||||
|
{fileUploadStore.uploadResults.map((result) => (
|
||||||
|
<UploadedItem
|
||||||
|
key={result.url}
|
||||||
|
url={result.url}
|
||||||
|
name={result.name}
|
||||||
|
onRemove={() => handleRemoveUploadedItem(result.url)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Input Area */}
|
||||||
|
<InputGroup position="relative">
|
||||||
|
<AutoResizeTextArea
|
||||||
|
fontFamily="Arial, sans-serif"
|
||||||
|
ref={inputRef}
|
||||||
|
value={value}
|
||||||
|
height={heightConstraint}
|
||||||
|
autoFocus
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
p={2}
|
||||||
|
pr="8px"
|
||||||
|
pl="17px"
|
||||||
|
bg="rgba(255, 255, 255, 0.15)"
|
||||||
|
color="text.primary"
|
||||||
|
borderRadius="20px" // Set a consistent border radius
|
||||||
|
border="none"
|
||||||
|
placeholder="Free my mind..."
|
||||||
|
_placeholder={{ color: "gray.400" }}
|
||||||
|
_focus={{
|
||||||
|
outline: "none",
|
||||||
|
}}
|
||||||
|
disabled={isLoading}
|
||||||
|
minRows={1}
|
||||||
|
maxRows={12}
|
||||||
|
style={{
|
||||||
|
touchAction: "none",
|
||||||
|
resize: "none",
|
||||||
|
overflowY: "auto",
|
||||||
|
width: "100%",
|
||||||
|
transition: "height 0.2s ease-in-out",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/*<InputRightElement*/}
|
||||||
|
{/* position="absolute"*/}
|
||||||
|
{/* right={0}*/}
|
||||||
|
{/* top={0}*/}
|
||||||
|
{/* bottom={0}*/}
|
||||||
|
{/* width="40px"*/}
|
||||||
|
{/* height="100%"*/}
|
||||||
|
{/* display="flex"*/}
|
||||||
|
{/* alignItems="center"*/}
|
||||||
|
{/* justifyContent="center"*/}
|
||||||
|
{/*>*/}
|
||||||
|
{/*<EnableSearchButton />*/}
|
||||||
|
{/*</InputRightElement>*/}
|
||||||
|
</InputGroup>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
style={{ display: "none" }}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
{fileUploadStore.uploadError && (
|
||||||
|
<Alert status="error" mt={2}>
|
||||||
|
<AlertIcon />
|
||||||
|
{fileUploadStore.uploadError}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default InputTextArea;
|
9
src/components/chat/ChatMessageContent.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import CustomMarkdownRenderer from "./CustomMarkdownRenderer";
|
||||||
|
|
||||||
|
const ChatMessageContent = ({ content }) => {
|
||||||
|
return <CustomMarkdownRenderer markdown={content} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(ChatMessageContent);
|