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);
|