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.
98 lines
2.6 KiB
TypeScript
98 lines
2.6 KiB
TypeScript
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 />
|
||
</>
|
||
);
|
||
}
|