Restructure: move Vite project into app/, deploy build output to repo root

The webserver serves the repo checkout directly as static files, so
index.html at the repo root must be the production build (pointing at
hashed /assets/*.js) rather than the dev template (/src/main.tsx),
which was being served with an unrecognized application/octet-stream
MIME type and blocked by the browser.

- Move src/, public/, scripts/, server/, package.json, vite.config.ts,
  tsconfig.json, .env(.example), and the dev index.html into app/.
- Add app/scripts/deploy.mjs: copies app/dist/* into the repo root
  after every `vite build`, cleaning up stale hashed filenames.
- Wire it into `npm run build` (app/package.json).
- Update run.sh to operate from app/, and .gitignore for the new layout.
- Commit the current build output (index.html, assets/, manifest,
  sw.js, workbox-*.js, favicon.svg, icons/, LICENSE) to the repo root.
- Update README with the new project layout and deployment model.
This commit is contained in:
oliver committed 2026-11-04 14:05:46 -03:00
1 parent 5245c0a856
commit c7b653ff6a
54 files changed
+224 -34

No files matched your search

+97
View File
@@ -0,0 +1,97 @@
import { useEffect, useRef } from "react";
import { useStore } from "./state/store";
import { isConfigured } from "./lib/config";
import { useTts } from "./hooks/useTts";
import { ConfigScreen } from "./components/ConfigScreen";
import { DerezLogoLink } from "./components/DerezLogo";
import { Sidebar } from "./components/Sidebar";
import { ChatView } from "./components/ChatView";
import { SettingsPanel } from "./components/SettingsPanel";
import { Toast } from "./components/Toast";
function AutoSpeak() {
const messages = useStore((s) => s.messages);
const streaming = useStore((s) => s.streaming);
const speakReplies = useStore((s) => s.config.speakReplies);
const { speak } = useTts();
const lastSpoken = useRef<string | null>(null);
useEffect(() => {
if (!speakReplies || streaming) return;
const last = messages[messages.length - 1];
if (!last || last.role !== "assistant") return;
if (!last.id || last.id === lastSpoken.current) return;
if (!last.content.trim()) return;
lastSpoken.current = last.id;
speak(last.content);
}, [messages, streaming, speakReplies, speak]);
return null;
}
export function App() {
const ready = useStore((s) => s.ready);
const config = useStore((s) => s.config);
const connectionError = useStore((s) => s.connectionError);
const showSettings = useStore((s) => s.showSettings);
const bootstrap = useStore((s) => s.bootstrap);
const connect = useStore((s) => s.connect);
useEffect(() => {
void bootstrap();
}, [bootstrap]);
if (!ready) {
return (
<div className="screen">
<div className="spinner" />
</div>
);
}
if (!isConfigured(config)) {
return (
<>
<ConfigScreen />
{showSettings && <SettingsPanel />}
<Toast />
</>
);
}
if (connectionError) {
return (
<div className="screen">
<div className="card">
<div style={{ marginBottom: 18 }}>
<DerezLogoLink height={30} />
</div>
<h1>Can’t reach Hermes</h1>
<p className="lead">{connectionError}</p>
<div className="actions">
<button className="btn" onClick={() => useStore.getState().setShowSettings(true)}>
Settings
</button>
<button className="btn primary" onClick={() => connect()}>
Retry
</button>
</div>
</div>
{showSettings && <SettingsPanel />}
<Toast />
</div>
);
}
return (
<>
<div className="app">
<Sidebar />
<ChatView />
</div>
<AutoSpeak />
{showSettings && <SettingsPanel />}
<Toast />
</>
);
}