Rebuild client dist with Hanzo branding, update Makefile

- Rebuild client/dist with updated icons and branding assets
- Update .dockerignore and Makefile for Hanzo build
This commit is contained in:
Hanzo Dev
2026-02-27 13:13:51 -08:00
parent 73e71a24e0
commit bde4b33b1f
107 changed files with 1380 additions and 732 deletions
+5
View File
@@ -10,3 +10,8 @@ tests
*.tgz
log.txt
docker/Dockerfile.*
node_modules
**/node_modules
.env
.env.*
!.env.static
+25
View File
@@ -0,0 +1,25 @@
# Static SPA — pre-built dist served by lightweight file server
# Gateway/ingress handles load balancing, rate limiting, SSL, SPA routing.
#
# Pre-requisite: run `make build-static-local` first to build client/dist
#
# Build: docker build -f Dockerfile.static -t ghcr.io/hanzoai/chat:latest .
# Run: docker run -p 3080:3080 ghcr.io/hanzoai/chat:latest
FROM node:22-alpine
RUN npm install -g serve@14
WORKDIR /app
# Copy pre-built static files
COPY client/dist/ /app/dist/
COPY client/public/ /app/public/
# Merge public assets into dist (favicon, icons, etc.)
RUN cp -rn /app/public/* /app/dist/ 2>/dev/null || true && rm -rf /app/public
EXPOSE 3080
# serve with SPA fallback on port 3080
CMD ["serve", "-s", "dist", "-l", "3080"]
+22
View File
@@ -130,6 +130,28 @@ build-prod:
@echo "$(GREEN)Building production image...$(NC)"
@docker build -f docker/Dockerfile -t hanzoai/chat:latest .
build-static-local:
@echo "$(GREEN)Building static SPA locally...$(NC)"
@cp .env.static .env
@pnpm run build:data-provider
@pnpm run --filter @librechat/client build
@cd client && pnpm run build
@cp .env.backup .env 2>/dev/null || true
@echo "$(GREEN)Local build complete: client/dist/$(NC)"
build-static: build-static-local
@echo "$(GREEN)Building Docker image...$(NC)"
@docker build -f Dockerfile.static -t ghcr.io/hanzoai/chat:latest .
@echo "$(GREEN)Image built: ghcr.io/hanzoai/chat:latest$(NC)"
deploy-static:
@echo "$(GREEN)Pushing and deploying static SPA...$(NC)"
@docker push ghcr.io/hanzoai/chat:latest
@kubectl apply -f k8s/deployment-static.yaml
@kubectl rollout restart deployment/chat -n hanzo
@echo "$(GREEN)Deployed. Watching rollout...$(NC)"
@kubectl rollout status deployment/chat -n hanzo --timeout=120s
# ============================================================================
# PRODUCTION
# ============================================================================
File diff suppressed because one or more lines are too long
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
import{r as e,j as s,bE as a}from"./vendor.hYViCx_o.js";import{ay as t,az as r,aC as o}from"./radix-ui.zUF2t8pu.js";import{g as i,Q as n,S as c,u as l,a as m,d,T as u,c as j,B as p}from"./index.ChtrlAPT.js";import{u as x}from"./tanstack-vendor.CcfA9pap.js";import"./utilities.D3AhyVZJ.js";import"./animations.C9kiQ0P7.js";import{E as h,l as g,y as f,P as b}from"./headlessui.CpNfReFM.js";import"./react-interactions.CDNIj_P0.js";import"./markdown_highlight.Dt5gy-Cs.js";import"./validation.CgW5gGeX.js";import"./math-katex.DxUdIsOj.js";import"./locales.CneDv1Po.js";import"./i18n.B6o-Gbrg.js";import"./routing.D6ZJbNew.js";import"./http-client.B1ynDCuQ.js";import"./date-utils.CHq2fTxU.js";import"./forms.CgWlvBRM.js";import"./framer-motion.BeyjVqYH.js";import"./advanced-inputs.oQ6kIoBT.js";import"./security-ui.MEypZJRJ.js";import"./markdown-processing.TTzZ1vwl.js";import"./avatars.zbie9GOZ.js";import"./virtualization.CzFJ0wKU.js";import"./heic-converter.CBfFuDy-.js";import"./sandpack.DtKLOr2o.js";import"./codemirror-core.K3HVILnM.js";import"./codemirror-state.ClnsKiKb.js";import"./codemirror-view.yuH3vRs8.js";import"./codemirror-language.B5LqEgWw.js";const v=({tag:a,selected:o,handleSubmit:i,icon:n,...l})=>{const[m,d]=e.useState(!1);return s.jsx(h,{"aria-label":a,className:"group flex w-full gap-2 rounded-lg p-2.5 text-sm text-text-primary transition-colors duration-200 focus:outline-none data-[focus]:bg-surface-hover data-[focus-visible]:ring-2 data-[focus-visible]:ring-primary",...l,as:"button",onClick:async()=>{"New Bookmark"!==a?(d(!0),i(a),d(!1)):i()},children:s.jsx("div",{className:"flex grow items-center justify-between gap-2",children:s.jsxs("div",{className:"flex items-center gap-2",children:[null!=n?n:m?s.jsx(c,{className:"size-4"}):o?s.jsx(t,{className:"size-4"}):s.jsx(r,{className:"size-4"}),s.jsx("div",{style:{wordBreak:"break-word",overflowWrap:"anywhere"},children:a})]})})})},k=({tags:e,handleSubmit:a,header:t})=>{const{bookmarks:r}=l();return s.jsxs(s.Fragment,{children:[t,r.length>0&&s.jsx("div",{className:"my-1.5 h-px",role:"none"}),r.map((t,r)=>s.jsx(v,{tag:t.tag,selected:e.includes(t.tag),handleSubmit:a},`${t._id??t.tag}-${r}`))]})},y=({conversation:a,tags:t=[],setTags:r})=>{const[i,n]=e.useState(),{bookmarks:c}=l(),d=m();e.useEffect(()=>{i||n(a)},[a,i]);const u=()=>{r([])};return 0===c.length?s.jsxs("div",{className:"flex flex-col",children:[s.jsx(v,{tag:d("com_ui_clear_all"),"data-testid":"bookmark-item-clear",handleSubmit:u,selected:!1,icon:s.jsx(o,{className:"size-4"})}),s.jsx(v,{tag:d("com_ui_no_bookmarks"),"data-testid":"bookmark-item-no-bookmarks",handleSubmit:()=>Promise.resolve(),selected:!1,icon:"🤔"})]}):s.jsx("div",{className:"flex flex-col",children:s.jsx(k,{tags:t,handleSubmit:e=>{if(void 0===e)return;const s=(e=>t.some(s=>s===e)?t.filter(s=>s!==e):[...t,e])(e);r(s)},header:s.jsx(v,{tag:d("com_ui_clear_all"),"data-testid":"bookmark-item-clear",handleSubmit:u,selected:!1,icon:s.jsx(o,{className:"size-4"})})})})},N=({tags:o,setTags:c,isSmallScreen:l})=>{const h=m(),{data:v}=x([n.conversationTags],()=>i(),{refetchOnWindowFocus:!1,refetchOnReconnect:!1,refetchOnMount:!1,...k});var k;const N=a(d.conversationByIndex(0)),w=e.useMemo(()=>o.length>0?o.join(", "):h("com_ui_bookmarks"),[o,h]);return s.jsx(g,{as:"div",className:"group relative",children:({open:e})=>s.jsxs(s.Fragment,{children:[s.jsx(u,{description:w,render:s.jsx(f,{id:"bookmark-menu-button","aria-label":h("com_ui_bookmarks"),className:j("flex items-center justify-center","size-10 border-none text-text-primary hover:bg-accent hover:text-accent-foreground","rounded-full border-none p-2 hover:bg-surface-hover md:rounded-xl",e?"bg-surface-hover":""),"data-testid":"bookmark-menu",children:o.length>0?s.jsx(t,{className:j("text-text-primary",l?"icon-md-heavy":"icon-lg"),"aria-hidden":"true"}):s.jsx(r,{className:j("text-text-primary",l?"icon-md-heavy":"icon-lg"),"aria-hidden":"true"})})}),s.jsx(b,{anchor:"bottom",className:"absolute left-0 top-full z-[100] mt-1 w-60 translate-y-0 overflow-hidden rounded-lg bg-surface-secondary p-1.5 shadow-lg outline-none",children:v&&N&&s.jsx(p.Provider,{value:{bookmarks:v.filter(e=>e.count>0)},children:s.jsx(y,{conversation:N,tags:o,setTags:c})})})]})})};export{N as default};
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 B

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 B

After

Width:  |  Height:  |  Size: 366 B

File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
-9
View File
@@ -1,9 +0,0 @@
<svg width="67" height="67" viewBox="0 0 67 67" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.21 67V44.6369H0V67H22.21Z" fill="white"/>
<path d="M0 44.6369L22.21 46.8285V44.6369H0Z" fill="#D3D3D3"/>
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="white"/>
<path d="M22.21 0H0V22.3184H22.21V0Z" fill="white"/>
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="white"/>
<path d="M66.6753 22.3185L44.5098 20.0822V22.3185H66.6753Z" fill="#D3D3D3"/>
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 571 B

-9
View File
@@ -1,9 +0,0 @@
<svg width="67" height="67" viewBox="0 0 67 67" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.21 67V44.6369H0V67H22.21Z" fill="black"/>
<path d="M0 44.6369L22.21 46.8285V44.6369H0Z" fill="#4A4A4A"/>
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="black"/>
<path d="M22.21 0H0V22.3184H22.21V0Z" fill="black"/>
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="black"/>
<path d="M66.6753 22.3185L44.5098 20.0822V22.3185H66.6753Z" fill="#4A4A4A"/>
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 571 B

-25
View File
@@ -1,25 +0,0 @@
<svg width="67" height="67" viewBox="0 0 67 67" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
.logo-main {
fill: #000000;
}
.logo-shadow {
fill: #222222;
}
@media (prefers-color-scheme: dark) {
.logo-main {
fill: #FFFFFF;
}
.logo-shadow {
fill: #D3D3D3;
}
}
</style>
<path class="logo-main" d="M22.21 67V44.6369H0V67H22.21Z"/>
<path class="logo-shadow" d="M0 44.6369L22.21 46.8285V44.6369H0Z"/>
<path class="logo-main" d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z"/>
<path class="logo-main" d="M22.21 0H0V22.3184H22.21V0Z"/>
<path class="logo-main" d="M66.7198 0H44.5098V22.3184H66.7198V0Z"/>
<path class="logo-shadow" d="M66.6753 22.3185L44.5098 20.0822V22.3185H66.6753Z"/>
<path class="logo-main" d="M66.7198 67V44.6369H44.5098V67H66.7198Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 835 B

File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
+9 -24
View File
@@ -1,25 +1,10 @@
<svg width="67" height="67" viewBox="0 0 67 67" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
.logo-main {
fill: #000000;
}
.logo-shadow {
fill: #222222;
}
@media (prefers-color-scheme: dark) {
.logo-main {
fill: #FFFFFF;
}
.logo-shadow {
fill: #D3D3D3;
}
}
</style>
<path class="logo-main" d="M22.21 67V44.6369H0V67H22.21Z"/>
<path class="logo-shadow" d="M0 44.6369L22.21 46.8285V44.6369H0Z"/>
<path class="logo-main" d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z"/>
<path class="logo-main" d="M22.21 0H0V22.3184H22.21V0Z"/>
<path class="logo-main" d="M66.7198 0H44.5098V22.3184H66.7198V0Z"/>
<path class="logo-shadow" d="M66.6753 22.3185L44.5098 20.0822V22.3185H66.6753Z"/>
<path class="logo-main" d="M66.7198 67V44.6369H44.5098V67H66.7198Z"/>
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<rect width="64" height="64" rx="8" fill="#000000"/>
<g transform="translate(8, 8) scale(0.716)">
<path d="M22.21 67V44.6369H0V67H22.21Z" fill="#ffffff"/>
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#ffffff"/>
<path d="M22.21 0H0V22.3184H22.21V0Z" fill="#ffffff"/>
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#ffffff"/>
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#ffffff"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 836 B

After

Width:  |  Height:  |  Size: 536 B

File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 60 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
-86
View File
@@ -1,86 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="135.46666mm"
height="135.46666mm"
viewBox="0 0 135.46666 135.46666"
version="1.1"
id="svg5"
xml:space="preserve"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
sodipodi:docname="web-browser.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="1.829812"
inkscape:cx="339.10587"
inkscape:cy="281.44968"
inkscape:window-width="2560"
inkscape:window-height="1369"
inkscape:window-x="1072"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs
id="defs2"><linearGradient
id="linearGradient1021"
inkscape:collect="always"><stop
style="stop-color:#6914d1;stop-opacity:1;"
offset="0"
id="stop1017" /><stop
style="stop-color:#c82090;stop-opacity:1;"
offset="1"
id="stop1019" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1021"
id="linearGradient5957"
x1="75.754601"
y1="163.95738"
x2="146.97395"
y2="86.082359"
gradientUnits="userSpaceOnUse" /></defs><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-38.978451,-51.992085)"><g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
transform="translate(38.978451,51.992085)"
style="fill:#000000;fill-opacity:1"><rect
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:34.9999"
id="rect3228"
width="135.46666"
height="135.46666"
x="0"
y="0" /></g><g
id="g5792"
transform="matrix(0.10863854,0,0,0.10863854,51.849445,65.949185)"><path
style="display:inline;fill:url(#linearGradient5957);fill-opacity:1;stroke-width:34.9999"
id="path5949"
sodipodi:type="arc"
sodipodi:cx="106.16872"
sodipodi:cy="120.26846"
sodipodi:rx="53.232887"
sodipodi:ry="53.232887"
sodipodi:start="0.31945228"
sodipodi:end="0.31361418"
sodipodi:open="true"
sodipodi:arc-type="arc"
d="M 156.70842,136.98607 A 53.232887,53.232887 0 0 1 89.524891,170.83252 53.232887,53.232887 0 0 1 55.580426,103.69845 53.232887,53.232887 0 0 1 122.66487,69.656042 53.232887,53.232887 0 0 1 156.80516,136.69073"
transform="matrix(9.2048366,0,0,9.2048366,-477.26567,-607.05147)" /><path
d="m 500,10 c -0.1,0 -0.3,0 -0.4,0 -0.1,0 -0.1,0 -0.2,0 -0.2,0 -0.4,0 -0.6,0 C 228.7,10.6 10,229.8 10,500 c 0,270.2 218.7,489.3 488.8,490 0.2,0 0.4,0 0.6,0 0.1,0 0.1,0 0.2,0 0.2,0 0.3,0 0.4,0 C 770.6,990 990,770.6 990,500 990,229.4 770.6,10 500,10 Z m 19.6,293.2 c 51.9,-1.4 102.5,-8.3 151.2,-20.1 14.7,57.8 23.8,124.3 25.2,197.3 H 519.6 Z m 0,-39.2 V 52.3 C 572.4,66.9 626,137.4 660.1,245.4 614.8,256.3 567.9,262.7 519.6,264 Z M 480.4,51.8 V 264 c -48.7,-1.4 -96,-7.8 -141.6,-19 C 373.2,136.4 427.2,65.7 480.4,51.8 Z m 0,251.4 V 480.4 H 302.8 c 1.4,-73.1 10.6,-139.7 25.2,-197.5 49.1,12 100,18.9 152.4,20.3 z M 263.3,480.4 H 49.7 c 4.3,-100.8 41.9,-193.1 102,-266.2 43.6,24 89.9,43.7 138.4,58.4 -15.8,62.5 -25.3,133 -26.8,207.8 z m 0,39.2 c 1.4,74.8 10.9,145.2 26.8,207.8 -48.5,14.7 -94.8,34.4 -138.4,58.5 C 91.6,712.7 54,620.4 49.7,519.6 Z m 39.5,0 h 177.6 v 177 C 428,698 377.1,705 328,717 313.3,659.2 304.2,592.6 302.8,519.6 Z M 480.4,735.7 V 948.1 C 427.2,934.2 373.1,863.4 338.7,754.6 c 45.7,-11 93,-17.5 141.7,-18.9 z m 39.2,212 v -212 c 48.3,1.4 95.2,7.8 140.5,18.7 C 626,862.5 572.5,933.1 519.6,947.7 Z m 0,-251.1 v -177 H 696 C 694.6,592.5 685.5,659 670.8,716.7 622.1,704.8 571.6,698 519.6,696.6 Z m 215.8,-177 H 950.3 C 946,620.4 908.5,712.7 848.4,785.8 804.4,761.6 757.7,741.8 708.8,727 724.5,664.5 734,594.2 735.4,519.6 Z m 0,-39.2 C 734,405.7 724.5,335.3 708.7,272.8 757.6,258 804.3,238.2 848.2,214 c 60.2,73.1 97.7,165.5 102.1,266.3 z M 821.2,184.1 C 782.2,204.8 741,222 698.1,235 675.2,161.3 643,101.1 605,61.6 688.4,81.7 762.9,124.8 821.2,184.1 Z M 393.5,62 c -37.8,39.4 -69.9,99.3 -92.7,172.7 -42.6,-13 -83.4,-30 -122,-50.6 C 236.7,125.2 310.6,82.2 393.5,62 Z M 178.6,815.7 c 38.7,-20.6 79.5,-37.6 122.1,-50.6 22.8,73.4 54.9,133.4 92.7,172.9 C 310.6,917.8 236.6,874.7 178.6,815.7 Z m 426.3,122.6 c 38.1,-39.5 70.3,-99.7 93.2,-173.6 43,13.1 84.2,30.2 123.2,51 C 763,875.1 688.5,918.3 604.9,938.3 Z"
id="path5790"
style="fill:#000000;fill-opacity:1" /></g></g></svg>

Before

Width:  |  Height:  |  Size: 5.0 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 427 B

+37 -37
View File
@@ -2,16 +2,16 @@
<html lang="en-US">
<head>
<meta charset="utf-8" />
<base href="/" />
<meta name="theme-color" content="#171717" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="description" content="Chat - Unified interface to Agents, LLMs, MCPs and any AI model or OpenAPI documented API with full RAG stack" />
<meta name="description" content="Hanzo Chat - AI chat platform with support for multiple AI models" />
<title>Hanzo Chat</title>
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png" />
<link rel="apple-touch-icon" href="/assets/apple-touch-icon-180x180.png" />
<link rel="icon" type="image/png" sizes="32x32" href="assets/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="assets/favicon-16x16.png" />
<link rel="apple-touch-icon" href="assets/apple-touch-icon-180x180.png" />
<meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=resizes-content" />
<style>
html,
@@ -48,38 +48,38 @@
`;
document.head.appendChild(loadingContainerStyle);
</script>
<script type="module" crossorigin src="/assets/react-interactions.CDNIj_P0.js"></script>
<script type="module" crossorigin src="/assets/markdown_highlight.Dt5gy-Cs.js"></script>
<script type="module" crossorigin src="/assets/validation.CgW5gGeX.js"></script>
<script type="module" crossorigin src="/assets/math-katex.DxUdIsOj.js"></script>
<script type="module" crossorigin src="/assets/vendor.hYViCx_o.js"></script>
<script type="module" crossorigin src="/assets/i18n.B6o-Gbrg.js"></script>
<script type="module" crossorigin src="/assets/locales.CneDv1Po.js"></script>
<script type="module" crossorigin src="/assets/routing.D6ZJbNew.js"></script>
<script type="module" crossorigin src="/assets/radix-ui.zUF2t8pu.js"></script>
<script type="module" crossorigin src="/assets/tanstack-vendor.CcfA9pap.js"></script>
<script type="module" crossorigin src="/assets/http-client.B1ynDCuQ.js"></script>
<script type="module" crossorigin src="/assets/date-utils.CHq2fTxU.js"></script>
<script type="module" crossorigin src="/assets/utilities.D3AhyVZJ.js"></script>
<script type="module" crossorigin src="/assets/forms.CgWlvBRM.js"></script>
<script type="module" crossorigin src="/assets/framer-motion.BeyjVqYH.js"></script>
<script type="module" crossorigin src="/assets/advanced-inputs.oQ6kIoBT.js"></script>
<script type="module" crossorigin src="/assets/animations.C9kiQ0P7.js"></script>
<script type="module" crossorigin src="/assets/security-ui.MEypZJRJ.js"></script>
<script type="module" crossorigin src="/assets/markdown-processing.TTzZ1vwl.js"></script>
<script type="module" crossorigin src="/assets/avatars.zbie9GOZ.js"></script>
<script type="module" crossorigin src="/assets/headlessui.CpNfReFM.js"></script>
<script type="module" crossorigin src="/assets/virtualization.CzFJ0wKU.js"></script>
<script type="module" crossorigin src="/assets/heic-converter.CBfFuDy-.js"></script>
<script type="module" crossorigin src="/assets/codemirror-state.ClnsKiKb.js"></script>
<script type="module" crossorigin src="/assets/codemirror-view.yuH3vRs8.js"></script>
<script type="module" crossorigin src="/assets/codemirror-language.B5LqEgWw.js"></script>
<script type="module" crossorigin src="/assets/codemirror-core.K3HVILnM.js"></script>
<script type="module" crossorigin src="/assets/sandpack.DtKLOr2o.js"></script>
<script type="module" crossorigin src="/assets/index.ChtrlAPT.js"></script>
<link rel="stylesheet" crossorigin href="/assets/math-katex.DAKDMjqk.css">
<link rel="stylesheet" crossorigin href="/assets/index.BhoPS7BO.css">
<link rel="manifest" href="/manifest.webmanifest" crossorigin="use-credentials"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
<script type="module" crossorigin src="./assets/react-interactions.CfyqjqXP.js"></script>
<script type="module" crossorigin src="./assets/validation.DFZwsG5o.js"></script>
<script type="module" crossorigin src="./assets/math-katex.DLu84ADl.js"></script>
<script type="module" crossorigin src="./assets/markdown_highlight.ABKSddnD.js"></script>
<script type="module" crossorigin src="./assets/vendor.Byl_tGc2.js"></script>
<script type="module" crossorigin src="./assets/i18n.CA3mTzMG.js"></script>
<script type="module" crossorigin src="./assets/locales.D47CaVtF.js"></script>
<script type="module" crossorigin src="./assets/routing.CZbmXRkT.js"></script>
<script type="module" crossorigin src="./assets/radix-ui.CVh3Dd4P.js"></script>
<script type="module" crossorigin src="./assets/tanstack-vendor.DyoeZ73M.js"></script>
<script type="module" crossorigin src="./assets/avatars.CwJDtxyh.js"></script>
<script type="module" crossorigin src="./assets/advanced-inputs.w0JmX-rs.js"></script>
<script type="module" crossorigin src="./assets/virtualization.CbKUaacL.js"></script>
<script type="module" crossorigin src="./assets/animations.CbDvfLyt.js"></script>
<script type="module" crossorigin src="./assets/headlessui.CJGpwcHH.js"></script>
<script type="module" crossorigin src="./assets/framer-motion.C9OoZPLA.js"></script>
<script type="module" crossorigin src="./assets/http-client.BNDKsQmf.js"></script>
<script type="module" crossorigin src="./assets/utilities.Cp-QZ9vZ.js"></script>
<script type="module" crossorigin src="./assets/date-utils.CHq2fTxU.js"></script>
<script type="module" crossorigin src="./assets/forms.gP1Bt4Ju.js"></script>
<script type="module" crossorigin src="./assets/security-ui.DNmv7C0T.js"></script>
<script type="module" crossorigin src="./assets/markdown-processing.BGeF1fRm.js"></script>
<script type="module" crossorigin src="./assets/codemirror-state.C-fPAHjK.js"></script>
<script type="module" crossorigin src="./assets/codemirror-view.Dyel3EV1.js"></script>
<script type="module" crossorigin src="./assets/codemirror-language.BdbQ9O-9.js"></script>
<script type="module" crossorigin src="./assets/codemirror-core.DeBwLQXr.js"></script>
<script type="module" crossorigin src="./assets/sandpack.DnyQb158.js"></script>
<script type="module" crossorigin src="./assets/heic-converter.-u32bK9M.js"></script>
<script type="module" crossorigin src="./assets/index.BzPea_a_.js"></script>
<link rel="stylesheet" crossorigin href="./assets/math-katex.f1Jb2Qab.css">
<link rel="stylesheet" crossorigin href="./assets/index.CbU1k9ZB.css">
<link rel="manifest" href="./manifest.webmanifest" crossorigin="use-credentials"><script id="vite-plugin-pwa:register-sw" src="./registerSW.js"></script></head>
<body>
<div id="root">
<div id="loading-container"></div>
+1 -1
View File
@@ -1 +1 @@
{"name":"Hanzo","short_name":"Hanzo","description":"Hanzo Chat Frontend - Modern AI Chat Interface","start_url":"/","display":"standalone","background_color":"#000000","theme_color":"#009688","lang":"en","scope":"/","icons":[{"src":"/assets/favicon-32x32.png","sizes":"32x32","type":"image/png"},{"src":"/assets/favicon-16x16.png","sizes":"16x16","type":"image/png"},{"src":"/assets/apple-touch-icon-180x180.png","sizes":"180x180","type":"image/png"},{"src":"/assets/icon-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/assets/maskable-icon.png","sizes":"512x512","type":"image/png","purpose":"maskable"}]}
{"name":"Hanzo Chat","short_name":"Hanzo Chat","description":"","start_url":"/","display":"standalone","background_color":"#000000","theme_color":"#fd4444","lang":"en","scope":"./","icons":[{"src":"assets/favicon-32x32.png","sizes":"32x32","type":"image/png"},{"src":"assets/favicon-16x16.png","sizes":"16x16","type":"image/png"},{"src":"assets/apple-touch-icon-180x180.png","sizes":"180x180","type":"image/png"},{"src":"assets/icon-192x192.png","sizes":"192x192","type":"image/png"},{"src":"assets/maskable-icon.png","sizes":"512x512","type":"image/png","purpose":"maskable"}]}
+1 -1
View File
@@ -1 +1 @@
if('serviceWorker' in navigator) {window.addEventListener('load', () => {navigator.serviceWorker.register('/sw.js', { scope: '/' })})}
if('serviceWorker' in navigator) {window.addEventListener('load', () => {navigator.serviceWorker.register('./sw.js', { scope: './' })})}
+1 -1
View File
@@ -1 +1 @@
if(!self.define){let s,e={};const i=(i,l)=>(i=new URL(i+".js",l).href,e[i]||new Promise(e=>{if("document"in self){const s=document.createElement("script");s.src=i,s.onload=e,document.head.appendChild(s)}else s=i,importScripts(i),e()}).then(()=>{let s=e[i];if(!s)throw new Error(`Module ${i} didnt register its module`);return s}));self.define=(l,r)=>{const n=s||("document"in self?document.currentScript.src:"")||location.href;if(e[n])return;let t={};const u=s=>i(s,n),o={module:{uri:n},exports:t,require:u};e[n]=Promise.all(l.map(s=>o[s]||u(s))).then(s=>(r(...s),t))}}define(["./workbox-4c320e2c"],function(s){"use strict";self.skipWaiting(),s.clientsClaim(),s.precacheAndRoute([{url:"assets/AccountSettings.BX_UYV36.js",revision:null},{url:"assets/advanced-inputs.oQ6kIoBT.js",revision:null},{url:"assets/animations.C9kiQ0P7.js",revision:null},{url:"assets/avatars.zbie9GOZ.js",revision:null},{url:"assets/BookmarkNav.C-AY-f8K.js",revision:null},{url:"assets/codemirror-core.K3HVILnM.js",revision:null},{url:"assets/codemirror-language.B5LqEgWw.js",revision:null},{url:"assets/codemirror-state.ClnsKiKb.js",revision:null},{url:"assets/codemirror-view.yuH3vRs8.js",revision:null},{url:"assets/date-utils.CHq2fTxU.js",revision:null},{url:"assets/forms.CgWlvBRM.js",revision:null},{url:"assets/framer-motion.BeyjVqYH.js",revision:null},{url:"assets/headlessui.CpNfReFM.js",revision:null},{url:"assets/heic-converter.CBfFuDy-.js",revision:null},{url:"assets/http-client.B1ynDCuQ.js",revision:null},{url:"assets/i18n.B6o-Gbrg.js",revision:null},{url:"assets/index.BhoPS7BO.css",revision:null},{url:"assets/index.ChtrlAPT.js",revision:null},{url:"assets/locales.CneDv1Po.js",revision:null},{url:"assets/markdown_highlight.Dt5gy-Cs.js",revision:null},{url:"assets/markdown-processing.TTzZ1vwl.js",revision:null},{url:"assets/math-katex.DAKDMjqk.css",revision:null},{url:"assets/math-katex.DxUdIsOj.js",revision:null},{url:"assets/radix-ui.zUF2t8pu.js",revision:null},{url:"assets/react-interactions.CDNIj_P0.js",revision:null},{url:"assets/routing.D6ZJbNew.js",revision:null},{url:"assets/sandpack.DtKLOr2o.js",revision:null},{url:"assets/security-ui.MEypZJRJ.js",revision:null},{url:"assets/tanstack-vendor.CcfA9pap.js",revision:null},{url:"assets/utilities.D3AhyVZJ.js",revision:null},{url:"assets/validation.CgW5gGeX.js",revision:null},{url:"assets/vendor.hYViCx_o.js",revision:null},{url:"assets/virtualization.CzFJ0wKU.js",revision:null},{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"manifest.webmanifest",revision:"df5fe024fb89bdcd2bdbb079181799c0"},{url:"manifest.webmanifest",revision:"df5fe024fb89bdcd2bdbb079181799c0"}],{}),s.cleanupOutdatedCaches(),s.registerRoute(new s.NavigationRoute(s.createHandlerBoundToURL("index.html"),{denylist:[/^\/oauth/,/^\/api/]}))});
if(!self.define){let s,e={};const i=(i,l)=>(i=new URL(i+".js",l).href,e[i]||new Promise(e=>{if("document"in self){const s=document.createElement("script");s.src=i,s.onload=e,document.head.appendChild(s)}else s=i,importScripts(i),e()}).then(()=>{let s=e[i];if(!s)throw new Error(`Module ${i} didnt register its module`);return s}));self.define=(l,r)=>{const n=s||("document"in self?document.currentScript.src:"")||location.href;if(e[n])return;let t={};const u=s=>i(s,n),o={module:{uri:n},exports:t,require:u};e[n]=Promise.all(l.map(s=>o[s]||u(s))).then(s=>(r(...s),t))}}define(["./workbox-7fc22fbe"],function(s){"use strict";self.skipWaiting(),s.clientsClaim(),s.precacheAndRoute([{url:"registerSW.js",revision:"402b66900e731ca748771b6fc5e7a068"},{url:"assets/virtualization.CbKUaacL.js",revision:null},{url:"assets/vendor.Byl_tGc2.js",revision:null},{url:"assets/validation.DFZwsG5o.js",revision:null},{url:"assets/utilities.Cp-QZ9vZ.js",revision:null},{url:"assets/tanstack-vendor.DyoeZ73M.js",revision:null},{url:"assets/security-ui.DNmv7C0T.js",revision:null},{url:"assets/sandpack.DnyQb158.js",revision:null},{url:"assets/routing.CZbmXRkT.js",revision:null},{url:"assets/react-interactions.CfyqjqXP.js",revision:null},{url:"assets/radix-ui.CVh3Dd4P.js",revision:null},{url:"assets/mermaid.BzRMe_od.js",revision:null},{url:"assets/math-katex.f1Jb2Qab.css",revision:null},{url:"assets/math-katex.DLu84ADl.js",revision:null},{url:"assets/markdown_highlight.ABKSddnD.js",revision:null},{url:"assets/markdown-processing.BGeF1fRm.js",revision:null},{url:"assets/locales.D47CaVtF.js",revision:null},{url:"assets/index.CbU1k9ZB.css",revision:null},{url:"assets/index.BzPea_a_.js",revision:null},{url:"assets/i18n.CA3mTzMG.js",revision:null},{url:"assets/http-client.BNDKsQmf.js",revision:null},{url:"assets/heic-converter.-u32bK9M.js",revision:null},{url:"assets/headlessui.CJGpwcHH.js",revision:null},{url:"assets/framer-motion.C9OoZPLA.js",revision:null},{url:"assets/forms.gP1Bt4Ju.js",revision:null},{url:"assets/date-utils.CHq2fTxU.js",revision:null},{url:"assets/codemirror-view.Dyel3EV1.js",revision:null},{url:"assets/codemirror-state.C-fPAHjK.js",revision:null},{url:"assets/codemirror-language.BdbQ9O-9.js",revision:null},{url:"assets/codemirror-core.DeBwLQXr.js",revision:null},{url:"assets/avatars.CwJDtxyh.js",revision:null},{url:"assets/animations.CbDvfLyt.js",revision:null},{url:"assets/advanced-inputs.w0JmX-rs.js",revision:null},{url:"assets/BookmarkNav.B6VVre05.js",revision:null},{url:"assets/AccountSettings.BSt_pskB.js",revision:null},{url:"manifest.webmanifest",revision:"8d1943f9698bf1604968186da99f2fdf"},{url:"manifest.webmanifest",revision:"8d1943f9698bf1604968186da99f2fdf"}],{}),s.cleanupOutdatedCaches(),s.registerRoute(new s.NavigationRoute(s.createHandlerBoundToURL("index.html"),{denylist:[/^\/oauth/,/^\/api/]}))});
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -153,7 +153,7 @@
"typescript": "^5.3.3",
"vite": "^6.4.1",
"vite-plugin-compression2": "^2.2.1",
"vite-plugin-node-polyfills": "^0.23.0",
"vite-plugin-node-polyfills": "^0.25.0",
"vite-plugin-pwa": "^0.21.2"
}
}
+89 -28
View File
@@ -10,6 +10,36 @@ import { getLoginError } from '~/utils';
import { useLocalize } from '~/hooks';
import LoginForm from './LoginForm';
/**
* Build Hanzo IAM OIDC authorize URL from env vars.
* Used in static SPA mode when VITE_HANZO_IAM_URL is set.
*/
function getHanzoIamUrl(): string | null {
const iamUrl = import.meta.env.VITE_HANZO_IAM_URL;
const appId = import.meta.env.VITE_HANZO_IAM_APP;
const org = import.meta.env.VITE_HANZO_IAM_ORG;
if (!iamUrl || !appId) {
return null;
}
const redirectUri = `${window.location.origin}/auth/callback`;
const state = `hanzo-chat-${Date.now()}`;
// Store state for CSRF verification
sessionStorage.setItem('oauth_state', state);
const params = new URLSearchParams({
client_id: appId,
redirect_uri: redirectUri,
response_type: 'code',
state,
scope: 'openid profile email',
});
return `${iamUrl}/login/oauth/authorize?${params.toString()}`;
}
function Login() {
const localize = useLocalize();
const { showToast } = useToastContext();
@@ -23,6 +53,10 @@ function Login() {
// Persist the disable flag locally so that once detected, auto-redirect stays disabled.
const [isAutoRedirectDisabled, setIsAutoRedirectDisabled] = useState(disableAutoRedirect);
// Check if we're in static/IAM mode
const hanzoIamUrl = getHanzoIamUrl();
const isStaticMode = !!import.meta.env.VITE_HANZO_API_URL;
useEffect(() => {
const oauthError = searchParams?.get('error');
if (oauthError && oauthError === ErrorTypes.AUTH_FAILED) {
@@ -46,44 +80,54 @@ function Login() {
}
}, [disableAutoRedirect, searchParams, setSearchParams]);
// Determine whether we should auto-redirect to OpenID.
// Determine whether we should auto-redirect to OpenID or Hanzo IAM
const shouldAutoRedirect =
startupConfig?.openidLoginEnabled &&
startupConfig?.openidAutoRedirect &&
startupConfig?.serverDomain &&
!isAutoRedirectDisabled;
!isAutoRedirectDisabled &&
((startupConfig?.openidLoginEnabled &&
startupConfig?.openidAutoRedirect &&
startupConfig?.serverDomain) ||
(isStaticMode && hanzoIamUrl));
useEffect(() => {
if (shouldAutoRedirect) {
console.log('Auto-redirecting to OpenID provider...');
window.location.href = `${startupConfig.serverDomain}/oauth/openid`;
if (isStaticMode && hanzoIamUrl) {
console.log('Auto-redirecting to Hanzo IAM...');
window.location.href = hanzoIamUrl;
} else if (startupConfig?.serverDomain) {
console.log('Auto-redirecting to OpenID provider...');
window.location.href = `${startupConfig.serverDomain}/oauth/openid`;
}
}
}, [shouldAutoRedirect, startupConfig]);
}, [shouldAutoRedirect, startupConfig, isStaticMode, hanzoIamUrl]);
// Render fallback UI if auto-redirect is active.
if (shouldAutoRedirect) {
return (
<div className="flex min-h-screen flex-col items-center justify-center p-4">
<p className="text-lg font-semibold">
{localize('com_ui_redirecting_to_provider', { 0: startupConfig.openidLabel })}
{isStaticMode
? 'Redirecting to Hanzo...'
: localize('com_ui_redirecting_to_provider', { 0: startupConfig?.openidLabel })}
</p>
<div className="mt-4">
<SocialButton
key="openid"
enabled={startupConfig.openidLoginEnabled}
serverDomain={startupConfig.serverDomain}
oauthPath="openid"
Icon={() =>
startupConfig.openidImageUrl ? (
<img src={startupConfig.openidImageUrl} alt="OpenID Logo" className="h-5 w-5" />
) : (
<OpenIDIcon />
)
}
label={startupConfig.openidLabel}
id="openid"
/>
</div>
{!isStaticMode && startupConfig && (
<div className="mt-4">
<SocialButton
key="openid"
enabled={startupConfig.openidLoginEnabled}
serverDomain={startupConfig.serverDomain}
oauthPath="openid"
Icon={() =>
startupConfig.openidImageUrl ? (
<img src={startupConfig.openidImageUrl} alt="OpenID Logo" className="h-5 w-5" />
) : (
<OpenIDIcon />
)
}
label={startupConfig.openidLabel}
id="openid"
/>
</div>
)}
</div>
);
}
@@ -91,7 +135,24 @@ function Login() {
return (
<>
{error != null && <ErrorMessage>{localize(getLoginError(error))}</ErrorMessage>}
{startupConfig?.emailLoginEnabled === true && (
{/* Hanzo IAM button (static mode) */}
{isStaticMode && hanzoIamUrl && (
<div className="mt-4">
<a
href={hanzoIamUrl}
className="flex w-full items-center justify-center space-x-3 rounded-2xl border border-border-light bg-surface-primary px-5 py-3 text-text-primary transition-colors duration-200 hover:bg-surface-tertiary"
>
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
<p>Sign in with Hanzo</p>
</a>
</div>
)}
{/* Email/password form (non-static mode or as fallback) */}
{startupConfig?.emailLoginEnabled === true && !isStaticMode && (
<LoginForm
onSubmit={login}
startupConfig={startupConfig}
@@ -99,7 +160,7 @@ function Login() {
setError={setError}
/>
)}
{startupConfig?.registrationEnabled === true && (
{startupConfig?.registrationEnabled === true && !isStaticMode && (
<p className="my-4 text-center text-sm font-light text-gray-700 dark:text-white">
{' '}
{localize('com_auth_no_account')}{' '}
@@ -0,0 +1,83 @@
/**
* OAuth Callback Handler for Hanzo IAM (hanzo.id)
*
* Flow:
* 1. User clicks "Sign in" redirects to hanzo.id OIDC
* 2. After auth, hanzo.id redirects back with ?code=xxx&state=yyy
* 3. This component exchanges the code via cloud gateway /api/signin
* 4. Gateway sets session cookie, returns user claims + access token
* 5. We set the auth context and redirect to /c/new
*/
import { useEffect, useRef } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { setTokenHeader, dataService } from 'librechat-data-provider';
import { Spinner } from '@librechat/client';
import { useAuthContext } from '~/hooks/AuthContext';
export default function OAuthCallback() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const { login } = useAuthContext();
const exchangedRef = useRef(false);
useEffect(() => {
if (exchangedRef.current) {
return;
}
const code = searchParams.get('code');
const state = searchParams.get('state');
if (!code || !state) {
console.error('Missing code or state in OAuth callback');
navigate('/login', { replace: true });
return;
}
exchangedRef.current = true;
// Exchange code for session via cloud gateway
const exchangeCode = async () => {
try {
const response = await fetch(
`${import.meta.env.VITE_HANZO_API_URL || ''}/api/signin?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`,
{
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
},
);
if (!response.ok) {
throw new Error(`Signin failed: ${response.status}`);
}
const body = await response.json();
const claims = body.status === 'ok' ? body.data : body;
// Set the access token if present
if (claims?.accessToken || claims?.AccessToken) {
const token = claims.accessToken || claims.AccessToken;
setTokenHeader(token);
}
// Navigate to chat
navigate('/c/new', { replace: true });
} catch (err) {
console.error('OAuth code exchange failed:', err);
navigate('/login?error=auth_failed', { replace: true });
}
};
exchangeCode();
}, [searchParams, navigate, login]);
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<Spinner className="mx-auto mb-4 h-8 w-8" />
<p className="text-text-secondary">Signing in...</p>
</div>
</div>
);
}
+22 -1
View File
@@ -130,10 +130,31 @@ const thirdPartyModels = [
/* Component */
/* ------------------------------------------------------------------ */
function getLoginHref(serverDomain: string): string {
// Static mode: build Hanzo IAM OIDC URL
const iamUrl = import.meta.env.VITE_HANZO_IAM_URL;
const appId = import.meta.env.VITE_HANZO_IAM_APP;
if (iamUrl && appId) {
const redirectUri = `${window.location.origin}/auth/callback`;
const state = `hanzo-chat-${Date.now()}`;
sessionStorage.setItem('oauth_state', state);
const params = new URLSearchParams({
client_id: appId,
redirect_uri: redirectUri,
response_type: 'code',
state,
scope: 'openid profile email',
});
return `${iamUrl}/login/oauth/authorize?${params.toString()}`;
}
// Backend mode: OAuth via server
return `${serverDomain}/oauth/openid`;
}
export default function LandingPage() {
const { data: config } = useGetStartupConfig();
const serverDomain = config?.serverDomain || '';
const loginHref = `${serverDomain}/oauth/openid`;
const loginHref = getLoginHref(serverDomain);
return (
<div
+5 -1
View File
@@ -141,8 +141,12 @@ const AuthContextProvider = ({
const { user, token = '' } = data ?? {};
if (token) {
setUserContext({ token, isAuthenticated: true, user });
} else if (user) {
// Session-based auth: no JWT token, but user data means session is valid
// Use a sentinel value so isAuthenticated works
setUserContext({ token: 'session', isAuthenticated: true, user });
} else {
console.log('Token is not present. User is not authenticated.');
console.log('No valid session. User is not authenticated.');
if (authConfig?.test === true) {
return;
}
+10
View File
@@ -8,6 +8,7 @@ import {
TwoFactorScreen,
RequestPasswordReset,
} from '~/components/Auth';
import OAuthCallback from '~/components/Auth/OAuthCallback';
import { MarketplaceProvider } from '~/components/Agents/MarketplaceContext';
import AgentMarketplace from '~/components/Agents/Marketplace';
import { OAuthSuccess, OAuthError } from '~/components/OAuth';
@@ -38,6 +39,15 @@ export const router = createBrowserRouter(
element: <ShareRoute />,
errorElement: <RouteErrorBoundary />,
},
{
path: 'auth/callback',
element: (
<AuthContextProvider>
<OAuthCallback />
</AuthContextProvider>
),
errorElement: <RouteErrorBoundary />,
},
{
path: 'oauth',
errorElement: <RouteErrorBoundary />,
+17 -11
View File
@@ -8,9 +8,24 @@ import { nodePolyfills } from 'vite-plugin-node-polyfills';
import { VitePWA } from 'vite-plugin-pwa';
// https://vitejs.dev/config/
// When VITE_HANZO_API_URL is set, the frontend talks directly to the cloud gateway
// and no local backend proxy is needed (static SPA mode).
const hanzoApiUrl = process.env.VITE_HANZO_API_URL;
const backendPort = process.env.BACKEND_PORT && Number(process.env.BACKEND_PORT) || 3080;
const backendURL = process.env.HOST ? `http://${process.env.HOST}:${backendPort}` : `http://localhost:${backendPort}`;
// Only proxy to local backend if not using cloud gateway directly
const devProxy = hanzoApiUrl ? {} : {
'/api': {
target: backendURL,
changeOrigin: true,
},
'/oauth': {
target: backendURL,
changeOrigin: true,
},
};
export default defineConfig(({ command }) => ({
base: '',
server: {
@@ -18,20 +33,11 @@ export default defineConfig(({ command }) => ({
host: process.env.HOST || 'localhost',
port: process.env.PORT && Number(process.env.PORT) || 3090,
strictPort: false,
proxy: {
'/api': {
target: backendURL,
changeOrigin: true,
},
'/oauth': {
target: backendURL,
changeOrigin: true,
},
},
proxy: devProxy,
},
// Set the directory where environment variables are loaded from and restrict prefixes
envDir: '../',
envPrefix: ['VITE_', 'SCRIPT_', 'DOMAIN_', 'ALLOW_'],
envPrefix: ['VITE_', 'SCRIPT_', 'DOMAIN_', 'ALLOW_', 'HANZO_'],
plugins: [
react(),
nodePolyfills(),
+9
View File
@@ -0,0 +1,9 @@
model_list:
- model_name: "gpt-4"
llm_params:
model: openai/fake
api_key: fake-key
api_base: https://exampleopenaiendpoint-production.up.railway.app/
general_settings:
alerting: ["slack"]
+61
View File
@@ -0,0 +1,61 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: chat
namespace: hanzo
labels:
app: chat
app.kubernetes.io/managed-by: hanzo-universe
app.kubernetes.io/name: chat
app.kubernetes.io/part-of: hanzo-platform
spec:
replicas: 1
selector:
matchLabels:
app: chat
app.kubernetes.io/managed-by: hanzo-universe
app.kubernetes.io/name: chat
app.kubernetes.io/part-of: hanzo-platform
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
template:
metadata:
labels:
app: chat
app.kubernetes.io/managed-by: hanzo-universe
app.kubernetes.io/name: chat
app.kubernetes.io/part-of: hanzo-platform
spec:
imagePullSecrets:
- name: ghcr-secret
containers:
- name: chat
image: ghcr.io/hanzoai/chat:latest
imagePullPolicy: Always
ports:
- name: http
containerPort: 3080
# Static file server — simple TCP check
livenessProbe:
httpGet:
path: /
port: 3080
initialDelaySeconds: 5
periodSeconds: 30
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: 3080
periodSeconds: 10
failureThreshold: 3
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
+1
View File
@@ -51,6 +51,7 @@
"build": "npx turbo run build",
"frontend": "npm run build:data-provider && npm run build:data-schemas && npm run build:api && npm run build:client-package && cd client && npm run build",
"frontend:ci": "npm run build:data-provider && npm run build:client-package && cd client && npm run build:ci",
"build:static": "cp .env.static .env && npm run build:data-provider && npm run build:client-package && cd client && npm run build",
"frontend:dev": "cd client && npm run dev",
"e2e": "playwright test --config=e2e/playwright.config.local.ts",
"e2e:headed": "playwright test --config=e2e/playwright.config.local.ts --headed",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+97 -58
View File
@@ -1,12 +1,64 @@
/**
* Hanzo Cloud Gateway API Endpoints
*
* Single source of truth for all API URLs.
* The cloud gateway at api.hanzo.ai provides:
* - REST endpoints for CRUD (chats, messages, files, auth)
* - OpenAI/Anthropic-compatible LLM endpoints
* - ZAP WebSocket for streaming & MCP tools
*
* No backward compat, no adapter layers. One way to do everything.
*/
import type { AssistantsEndpoint } from './schemas';
import * as q from './types/queries';
import { ResourceType } from './accessPermissions';
/** Runtime override for API base URL */
export declare function setApiBaseUrl(url: string): void;
export declare const apiBaseUrl: () => string;
/** ZAP WebSocket URL (derived from API base, or explicit) */
export declare function zapUrl(): string;
export declare const health: () => string;
export declare const systemInfo: () => string;
export declare const versionInfo: () => string;
/** Sign in via hanzo.id OAuth code exchange */
export declare const signin: (code: string, state: string) => string;
/** Sign out */
export declare const signout: () => string;
/** Get current account (session-based) */
export declare const getAccount: () => string;
export declare const login: () => string;
export declare const logout: () => string;
export declare const register: () => string;
export declare const refreshToken: (retry?: boolean) => string;
export declare const user: () => string;
export declare const loginPage: () => string;
export declare const registerPage: () => string;
export declare const loginFacebook: () => string;
export declare const loginGoogle: () => string;
export declare const verifyEmail: () => string;
export declare const resendVerificationEmail: () => string;
export declare const requestPasswordReset: () => string;
export declare const resetPassword: () => string;
export declare const enableTwoFactor: () => string;
export declare const verifyTwoFactor: () => string;
export declare const confirmTwoFactor: () => string;
export declare const disableTwoFactor: () => string;
export declare const regenerateBackupCodes: () => string;
export declare const verifyTwoFactorTemp: () => string;
export declare const balance: () => string;
export declare const userPlugins: () => string;
export declare const deleteUser: () => string;
export declare const conversationsRoot: string;
export declare const conversations: (params: q.ConversationListParams) => string;
export declare const conversationById: (id: string) => string;
export declare const genTitle: (conversationId: string) => string;
export declare const updateConversation: () => string;
export declare const archiveConversation: () => string;
export declare const deleteConversation: () => string;
export declare const deleteAllConversation: () => string;
export declare const importConversation: () => string;
export declare const forkConversation: () => string;
export declare const duplicateConversation: () => string;
export declare const messages: (params: q.MessagesListParams) => string;
export declare const messagesArtifacts: (messageId: string) => string;
export declare const messagesBranch: () => string;
@@ -21,65 +73,19 @@ export declare const revokeUserKey: (name: string) => string;
export declare const revokeAllUserKeys: () => string;
export declare const apiKeys: () => string;
export declare const apiKeyById: (id: string) => string;
export declare const conversationsRoot: string;
export declare const conversations: (params: q.ConversationListParams) => string;
export declare const conversationById: (id: string) => string;
export declare const genTitle: (conversationId: string) => string;
export declare const updateConversation: () => string;
export declare const archiveConversation: () => string;
export declare const deleteConversation: () => string;
export declare const deleteAllConversation: () => string;
export declare const importConversation: () => string;
export declare const forkConversation: () => string;
export declare const duplicateConversation: () => string;
export declare const search: (q: string, cursor?: string | null) => string;
export declare const searchEnabled: () => string;
export declare const presets: () => string;
export declare const deletePreset: () => string;
export declare const plugins: () => string;
export declare const config: () => string;
export declare const aiEndpoints: () => string;
export declare const models: () => string;
export declare const tokenizer: () => string;
export declare const login: () => string;
export declare const logout: () => string;
export declare const register: () => string;
export declare const loginFacebook: () => string;
export declare const loginGoogle: () => string;
export declare const refreshToken: (retry?: boolean) => string;
export declare const requestPasswordReset: () => string;
export declare const resetPassword: () => string;
export declare const verifyEmail: () => string;
export declare const loginPage: () => string;
export declare const registerPage: () => string;
export declare const resendVerificationEmail: () => string;
export declare const plugins: () => string;
export declare const mcpReinitialize: (serverName: string) => string;
export declare const mcpConnectionStatus: () => string;
export declare const mcpServerConnectionStatus: (serverName: string) => string;
export declare const mcpAuthValues: (serverName: string) => string;
export declare const cancelMCPOAuth: (serverName: string) => string;
export declare const mcpOAuthBind: (serverName: string) => string;
export declare const actionOAuthBind: (actionId: string) => string;
export declare const config: () => string;
export declare const prompts: () => string;
export declare const addPromptToGroup: (groupId: string) => string;
export declare const assistants: ({ path, options, version, endpoint, isAvatar, }: {
path?: string;
options?: object;
endpoint?: AssistantsEndpoint;
version: number | string;
isAvatar?: boolean;
}) => string;
export declare const agents: ({ path, options }: {
path?: string;
options?: object;
}) => string;
export declare const activeJobs: () => string;
export declare const mcp: {
tools: string;
servers: string;
};
export declare const mcpServer: (serverName: string) => string;
export declare const revertAgentVersion: (agent_id: string) => string;
export declare const chatCompletions: () => string;
export declare const anthropicMessages: () => string;
export declare const providers: () => string;
export declare const provider: (id: string) => string;
export declare const files: () => string;
export declare const fileUpload: () => string;
export declare const fileDelete: () => string;
@@ -94,6 +100,34 @@ export declare const textToSpeech: () => string;
export declare const textToSpeechManual: () => string;
export declare const textToSpeechVoices: () => string;
export declare const getCustomConfigSpeech: () => string;
export declare const assistants: ({ path, options, version, endpoint, isAvatar, }: {
path?: string;
options?: object;
endpoint?: AssistantsEndpoint;
version: number | string;
isAvatar?: boolean;
}) => string;
export declare const agents: ({ path, options }: {
path?: string;
options?: object;
}) => string;
export declare const activeJobs: () => string;
export declare const revertAgentVersion: (agent_id: string) => string;
export declare const mcp: {
tools: string;
servers: string;
};
export declare const mcpServer: (serverName: string) => string;
export declare const mcpReinitialize: (serverName: string) => string;
export declare const mcpConnectionStatus: () => string;
export declare const mcpServerConnectionStatus: (serverName: string) => string;
export declare const mcpAuthValues: (serverName: string) => string;
export declare const cancelMCPOAuth: (serverName: string) => string;
export declare const mcpOAuthBind: (serverName: string) => string;
export declare const actionOAuthBind: (actionId: string) => string;
export declare const refreshMcpTools: () => string;
export declare const prompts: () => string;
export declare const addPromptToGroup: (groupId: string) => string;
export declare const getPromptGroup: (_id: string) => string;
export declare const getPromptGroupsWithFilters: (filter: object) => string;
export declare const getPromptsWithFilters: (filter: object) => string;
@@ -126,12 +160,6 @@ export declare const userTerms: () => string;
export declare const acceptUserTerms: () => string;
export declare const banner: () => string;
export declare const feedback: (conversationId: string, messageId: string) => string;
export declare const enableTwoFactor: () => string;
export declare const verifyTwoFactor: () => string;
export declare const confirmTwoFactor: () => string;
export declare const disableTwoFactor: () => string;
export declare const regenerateBackupCodes: () => string;
export declare const verifyTwoFactorTemp: () => string;
export declare const memories: () => string;
export declare const memory: (key: string) => string;
export declare const memoryPreferences: () => string;
@@ -142,3 +170,14 @@ export declare const updateResourcePermissions: (resourceType: ResourceType, res
export declare const getEffectivePermissions: (resourceType: ResourceType, resourceId: string) => string;
export declare const getAllEffectivePermissions: (resourceType: ResourceType) => string;
export declare const graphToken: (scopes: string) => string;
export declare const stores: () => string;
export declare const store: (id: string) => string;
export declare const addStore: () => string;
export declare const updateStore: () => string;
export declare const deleteStore: () => string;
export declare const refreshStoreVectors: () => string;
export declare const vectors: () => string;
export declare const addVector: () => string;
export declare const deleteVector: () => string;
export declare const workflows: () => string;
export declare const tasks: () => string;
+10
View File
@@ -1,2 +1,12 @@
export declare function setAcceptLanguageHeader(value: string): void;
export declare function setTokenHeader(token: string): void;
/**
* Set a publishable key (pk-) as the default auth header.
* Used for unauthenticated access (model listing, health checks).
*/
export declare function setPublishableKeyHeader(key: string): void;
/**
* Enable cross-origin credentials for cloud gateway requests.
* Required when frontend (hanzo.chat) talks to api.hanzo.ai.
*/
export declare function enableCrossOriginCredentials(): void;
+4 -2
View File
@@ -23,10 +23,12 @@ export * from './types/graph';
export * from './accessPermissions';
export * from './keys';
export * from './headers-helpers';
export { loginPage, registerPage, apiBaseUrl } from './api-endpoints';
export { default as request } from './request';
export { loginPage, registerPage, apiBaseUrl, setApiBaseUrl, zapUrl } from './api-endpoints';
export { default as request, setPublishableKey, getWithPk } from './request';
export { dataService };
import * as dataService from './data-service';
export { ZapClient, getZapClient, connectZap } from './zap';
export type { ZapConfig, ChatStreamChunk } from './zap';
export * from './utils';
export * from './actions';
export { default as createPayload } from './createPayload';
+16
View File
@@ -1,3 +1,12 @@
/**
* HTTP request layer for Hanzo Cloud Gateway.
*
* Handles:
* - Response unwrapping: gateway returns { status: "ok", data: X } X
* - Session-based auth (cookies) with fallback to Bearer token
* - Auto-retry on session expiry via /api/get-account
* - pk- key support for unauthenticated requests
*/
import { AxiosRequestConfig } from 'axios';
import type * as t from './types';
declare function _get<T>(url: string, options?: AxiosRequestConfig): Promise<T>;
@@ -9,6 +18,10 @@ declare function _put(url: string, data?: any): Promise<any>;
declare function _delete<T>(url: string): Promise<T>;
declare function _deleteWithOptions<T>(url: string, options?: AxiosRequestConfig): Promise<T>;
declare function _patch(url: string, data?: any): Promise<any>;
/** Set a publishable key (pk-) for unauthenticated API access */
export declare function setPublishableKey(key: string): void;
/** Make a request with pk- key (no session needed) */
export declare function getWithPk<T>(url: string): Promise<T>;
declare const _default: {
get: typeof _get;
getResponse: typeof _getResponse;
@@ -20,6 +33,9 @@ declare const _default: {
deleteWithOptions: typeof _deleteWithOptions;
patch: typeof _patch;
refreshToken: (retry?: boolean) => Promise<t.TRefreshTokenResponse | undefined>;
refreshSession: () => Promise<any>;
dispatchTokenUpdatedEvent: (token: string) => void;
setPublishableKey: typeof setPublishableKey;
getWithPk: typeof getWithPk;
};
export default _default;

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