PI / src /App.tsx
noodcon's picture
Upload 64 files
9b9eafc verified
Raw History Blame Contribute Delete
3.92 kB
import { useCallback, useRef, useState } from 'react';
import { useAgent } from './client/useAgent';
import { ChatPanel } from './components/Chat';
import { ModelDialog } from './components/ModelDialog';
import { TopBar } from './components/TopBar';
import { WorkspacePanel } from './components/Workspace';
const embedded = typeof window !== 'undefined' && window.self !== window.top;
const noWebGpu = typeof navigator !== 'undefined' && !('gpu' in navigator);
export default function App() {
const { state, pending, shell, actions } = useAgent();
const [draft, setDraft] = useState('');
const [split, setSplit] = useState(55);
const mainRef = useRef<HTMLElement>(null);
const send = useCallback(() => {
void actions.send(draft, () => setDraft(''));
}, [actions, draft]);
const loadClick = () => {
actions.loadModel(false).catch((e: Error) => {
if (e.name !== 'AbortError') actions.setError(e.message);
});
};
const startDrag = (e: React.PointerEvent) => {
const el = mainRef.current;
if (!el) return;
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
const move = (ev: PointerEvent) => {
const r = el.getBoundingClientRect();
setSplit(Math.min(80, Math.max(25, ((ev.clientX - r.left) / r.width) * 100)));
};
const up = () => {
window.removeEventListener('pointermove', move);
window.removeEventListener('pointerup', up);
};
window.addEventListener('pointermove', move);
window.addEventListener('pointerup', up);
};
const message = state.error || state.notice;
return (
<div className="app">
<TopBar meter={state.meter} modelLoaded={state.modelLoaded} busy={state.busy} load={state.load} onLoad={loadClick} />
{(embedded || noWebGpu) && (
<div className="banner" role="note">
<span>
{noWebGpu
? 'WebGPU is not available in this browser or frame. Use a recent Chrome or Edge.'
: 'This app is running inside a frame. If model loading fails because WebGPU or browser storage is blocked, open it in its own tab.'}
</span>
<a href={window.location.href} target="_blank" rel="noopener noreferrer">Open in a new tab ↗</a>
</div>
)}
{message && (
<div className={`banner${state.error ? ' error' : ''}`} role="status">
<span>{message}</span>
<button className="quiet" onClick={actions.dismissNotice}>Dismiss</button>
</div>
)}
<main ref={mainRef} style={{ gridTemplateColumns: `${split}fr 10px ${100 - split}fr` }}>
<ChatPanel
messages={state.messages}
streaming={state.streaming}
busy={state.busy}
draft={draft}
contextTokens={state.context.input + state.context.output}
onDraft={setDraft}
onSend={send}
onStop={actions.stop}
onNewChat={actions.newChat}
/>
<div
className="resizer"
role="separator"
aria-orientation="vertical"
aria-label="Resize chat and workspace"
tabIndex={0}
onPointerDown={startDrag}
onDoubleClick={() => setSplit(55)}
onKeyDown={(e) => {
if (e.key === 'ArrowLeft') setSplit((s) => Math.max(25, s - 3));
if (e.key === 'ArrowRight') setSplit((s) => Math.min(80, s + 3));
}}
/>
<WorkspacePanel
files={state.files}
shell={shell}
busy={state.busy}
onSave={actions.writeFile}
onRun={actions.runShell}
onClearShell={actions.clearShell}
onError={actions.setError}
/>
</main>
<ModelDialog
pending={pending}
load={state.load}
loading={state.busy === 'load'}
onConfirm={actions.confirmPending}
onCancel={actions.cancelPending}
/>
</div>
);
}