diff --git a/.gitignore b/.gitignore index 0d44e42d..c841af74 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ __pycache__ huf/public/frontend huf/public/docs huf/www/huf.html +huf/www/chat.html +huf/www/huf/sw.js +huf/www/huf/manifest.webmanifest +huf/www/huf/workbox-*.js huf/www/docs.html docs/.next docs/out @@ -17,4 +21,4 @@ docs/node_modules docs/yarn.lock huf/public/_next -huf/www/huf/docs \ No newline at end of file +huf/www/huf/docs diff --git a/docs/temp/chat_only_ui_tracker.md b/docs/temp/chat_only_ui_tracker.md new file mode 100644 index 00000000..e9e4a6ab --- /dev/null +++ b/docs/temp/chat_only_ui_tracker.md @@ -0,0 +1,44 @@ +# Full Huf PWA Tracker + +## Context +- Branch: `feat_pwa` +- Base: `origin/develop` +- Goal: full Huf app PWA experience for `/huf`, served through Frappe. +- Bench validation will be handled outside this worktree. + +## Decisions +- Keep first version simple: installable PWA, static frontend asset caching only, no cached private API data. +- Use Vite build output under `/assets/huf/frontend/`. +- Serve the app shell through a Frappe `www` route. +- Full-app PWA uses manifest `start_url: /huf` and `scope: /huf/`. +- Service worker is copied from generated assets into `huf/www/huf/sw.js` and registered as `/huf/sw.js` with `/huf/` scope. +- Manifest is copied to `huf/www/huf/manifest.webmanifest` so the worker's `/huf/manifest.webmanifest` precache entry resolves through `www`. +- `feat_chat_ui` owns `/huf/ui/chat`; this branch should not duplicate chat-only route/page code. + +## TODO +- [x] Inspect current Huf frontend build, routing, auth/logout, and logo/assets. +- [x] Inspect Frappe route/build integration. +- [x] Add PWA plugin and service worker registration. +- [x] Add or adapt Frappe-served app shell. +- [x] Add icons/manifest support. +- [x] Remove duplicate chat-only page and route ownership from PWA branch. +- [x] Verify build and type checks where possible. + +## Scratchpad +- Current repo already has `frontend/`, `frontend/vite.config.ts`, root `package.json`, and `huf/www/agent_chat.html`. +- Root `package.json` already delegates build/dev/install to `frontend`. +- `frontend/package.json` already builds with `vite build --base=/assets/huf/frontend/` and copies generated HTML to `huf/www/huf.html`. +- `frontend/vite.config.ts` already outputs to `../huf/public/frontend`. +- `huf/hooks.py` catch-all maps `/huf/` to `huf`, so `/huf/ui/chat` can use the existing Frappe shell. +- Existing `ChatWindow` uses `useSidebar`, so a standalone chat route needs a `SidebarProvider` or a small adaptation. +- PWA configured via `vite-plugin-pwa` with API and stream requests forced `NetworkOnly` and static frontend assets `StaleWhileRevalidate`. +- Generated PWA icons in `frontend/public/icons/` from `huf/public/Images/huf.png`. +- `yarn install` completed after network escalation and created `frontend/yarn.lock`. +- `frontend/yarn.lock` is ignored by repo rules (`**/yarn.lock`), so dependency persistence is in `frontend/package.json`. +- `yarn build` in `frontend/` passed as a smoke check and generated `manifest.webmanifest`, `sw.js`, `workbox-*.js`, then copied the shell to `huf/www/huf.html` and PWA route files to `huf/www/huf/`. +- Added generated `huf/www/huf/sw.js`, `huf/www/huf/manifest.webmanifest`, and `huf/www/huf/workbox-*.js` to `.gitignore` because they reference generated output from ignored `huf/public/frontend`. +- `yarn typecheck` in `frontend/` passed after the final full-app PWA adjustment. +- `yarn build` in `frontend/` passed after the final full-app PWA adjustment. +- Bench validation was not run because this worktree is not a bench environment. +- PWA branch updated to full-app scope; `huf/www/chat.html` is no longer generated by the copy script. +- Copy script removes stale generated `workbox-*.js` files from the `www` worker route before copying the current build output. diff --git a/frontend/index.html b/frontend/index.html index a94d21d5..95a0c084 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,8 +2,10 @@ - - + + + + Huf AI diff --git a/frontend/package.json b/frontend/package.json index fe50a05b..4d621416 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,7 +9,7 @@ "lint": "eslint .", "preview": "vite preview", "typecheck": "tsc --noEmit -p tsconfig.app.json", - "copy-html-entry": "cp ../huf/public/frontend/index.html ../huf/www/huf.html" + "copy-html-entry": "node scripts/copy-html-entry.mjs" }, "dependencies": { "@hookform/resolvers": "^3.9.0", @@ -95,6 +95,7 @@ "tailwindcss": "^3.4.13", "typescript": "^5.5.3", "typescript-eslint": "^8.7.0", - "vite": "^5.4.8" + "vite": "^5.4.8", + "vite-plugin-pwa": "^0.21.1" } } diff --git a/frontend/public/icons/icon-192.png b/frontend/public/icons/icon-192.png new file mode 100644 index 00000000..f568f734 Binary files /dev/null and b/frontend/public/icons/icon-192.png differ diff --git a/frontend/public/icons/icon-512.png b/frontend/public/icons/icon-512.png new file mode 100644 index 00000000..12b74e2e Binary files /dev/null and b/frontend/public/icons/icon-512.png differ diff --git a/frontend/public/icons/maskable-512.png b/frontend/public/icons/maskable-512.png new file mode 100644 index 00000000..12b74e2e Binary files /dev/null and b/frontend/public/icons/maskable-512.png differ diff --git a/frontend/scripts/copy-html-entry.mjs b/frontend/scripts/copy-html-entry.mjs new file mode 100644 index 00000000..3b31cf9d --- /dev/null +++ b/frontend/scripts/copy-html-entry.mjs @@ -0,0 +1,44 @@ +import { copyFile, mkdir, readdir, unlink } from 'node:fs/promises'; +import { dirname, resolve } from 'node:path'; + +const frontendDir = resolve(import.meta.dirname, '..'); +const repoDir = resolve(frontendDir, '..'); +const source = resolve(frontendDir, '../huf/public/frontend/index.html'); +const shellTarget = resolve(frontendDir, '../huf/www/huf.html'); +const serviceWorkerSource = resolve(frontendDir, '../huf/public/frontend/sw.js'); +const serviceWorkerTarget = resolve(frontendDir, '../huf/www/huf/sw.js'); +const manifestSource = resolve(frontendDir, '../huf/public/frontend/manifest.webmanifest'); +const manifestTarget = resolve(frontendDir, '../huf/www/huf/manifest.webmanifest'); +const frontendOutDir = resolve(frontendDir, '../huf/public/frontend'); +const serviceWorkerRouteDir = resolve(frontendDir, '../huf/www/huf'); + +await mkdir(dirname(shellTarget), { recursive: true }); +await copyFile(source, shellTarget); + +await mkdir(serviceWorkerRouteDir, { recursive: true }); +await copyFile(serviceWorkerSource, serviceWorkerTarget); +await copyFile(manifestSource, manifestTarget); + +const generatedFiles = await readdir(frontendOutDir); +const workboxFiles = generatedFiles.filter((file) => /^workbox-.+\.js$/.test(file)); +const existingRouteFiles = await readdir(serviceWorkerRouteDir); + +await Promise.all( + existingRouteFiles + .filter((file) => /^workbox-.+\.js$/.test(file)) + .map((file) => unlink(resolve(serviceWorkerRouteDir, file))), +); + +await Promise.all( + workboxFiles.map((file) => + copyFile(resolve(frontendOutDir, file), resolve(serviceWorkerRouteDir, file)), + ), +); + +const copiedPaths = [ + shellTarget, + serviceWorkerTarget, + manifestTarget, + ...workboxFiles.map((file) => resolve(serviceWorkerRouteDir, file)), +]; +console.log(`Copied frontend entry files to ${copiedPaths.map((target) => target.replace(`${repoDir}/`, '')).join(', ')}`); diff --git a/frontend/src/index.css b/frontend/src/index.css index 86edcc83..935fbadb 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -104,6 +104,7 @@ padding: 0; height: 100%; width: 100%; + overscroll-behavior-y: none; } #root { diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index ea9e3630..98f3e1a6 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,6 +2,7 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import App from './App.tsx'; import './index.css'; +import './pwa/registerSW'; createRoot(document.getElementById('root')!).render( diff --git a/frontend/src/pwa/registerSW.ts b/frontend/src/pwa/registerSW.ts new file mode 100644 index 00000000..f8085aee --- /dev/null +++ b/frontend/src/pwa/registerSW.ts @@ -0,0 +1,7 @@ +if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/huf/sw.js', { scope: '/huf/' }).catch((error) => { + console.error('Failed to register Huf service worker', error); + }); + }); +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 46d77f14..f654d8ad 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -2,10 +2,73 @@ import path from 'path'; import react from '@vitejs/plugin-react'; import proxyOptions from './proxyOptions'; import { defineConfig } from 'vite'; +import { VitePWA } from 'vite-plugin-pwa'; export default defineConfig({ + base: '/assets/huf/frontend/', plugins: [ react(), + VitePWA({ + injectRegister: false, + manifest: { + name: 'Huf', + short_name: 'Huf', + description: 'Build and run smart AI agents with tools, chat, and automation.', + start_url: '/huf', + scope: '/huf/', + display: 'standalone', + background_color: '#ffffff', + theme_color: '#111827', + icons: [ + { + src: '/assets/huf/frontend/icons/icon-192.png', + sizes: '192x192', + type: 'image/png', + }, + { + src: '/assets/huf/frontend/icons/icon-512.png', + sizes: '512x512', + type: 'image/png', + }, + { + src: '/assets/huf/frontend/icons/maskable-512.png', + sizes: '512x512', + type: 'image/png', + purpose: 'maskable', + }, + ], + }, + workbox: { + navigateFallback: null, + globPatterns: ['**/*.{js,css,ico,png,svg,woff,woff2,ttf}'], + modifyURLPrefix: { + '': '/assets/huf/frontend/', + }, + runtimeCaching: [ + { + urlPattern: ({ url }) => url.pathname.startsWith('/api/'), + handler: 'NetworkOnly', + options: { + cacheName: 'huf-api-network-only', + }, + }, + { + urlPattern: ({ url }) => url.pathname.startsWith('/huf/stream/'), + handler: 'NetworkOnly', + options: { + cacheName: 'huf-stream-network-only', + }, + }, + { + urlPattern: ({ url }) => url.pathname.startsWith('/assets/huf/frontend/'), + handler: 'StaleWhileRevalidate', + options: { + cacheName: 'huf-frontend-assets', + }, + }, + ], + }, + }), ], resolve: { alias: { diff --git a/huf/hooks.py b/huf/hooks.py index e32b618c..8590a2e6 100644 --- a/huf/hooks.py +++ b/huf/hooks.py @@ -58,6 +58,9 @@ # Home Pages # ---------- website_route_rules = [ + {"from_route": "/huf/sw.js", "to_route": "huf/sw.js"}, + {"from_route": "/huf/manifest.webmanifest", "to_route": "huf/manifest.webmanifest"}, + {"from_route": "/huf/workbox-", "to_route": "huf/workbox-"}, {"from_route": "/huf/stream/ping", "to_route": "huf/stream/ping"}, {"from_route": "/huf/stream/", "to_route": "huf/stream"}, {"from_route": "/huf/stream", "to_route": "huf/stream"},