feat(frontend): rework chat composer (sticky input, inline new session, status footer)
- Center ThothII logo + tagline; break "Human In The Loop" onto a second line - Rename "New question" → "New session"; the button now clears the screen and focuses the sticky bottom composer instead of opening a dialog - Composer is always rendered and pinned at the bottom; typing the first message there creates the session (no modal) - Add inline stop control (square + inner circle) that interrupts and saves the session via /close - Always-present send affordance (CornerDownLeft ↵) on the right of the box - Status footer under the box: mic (inert), model name, thinking level, and a context-usage gauge (placeholder) - Brand-tinted spinner beside the logo while the harness/backend holds the ball; hidden once a widget needs human input Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
import { useSessionStream } from "../stream/useSessionStream";
|
||||
import { useSessionStore } from "../store/sessionStore";
|
||||
import { WidgetHost } from "./WidgetHost";
|
||||
import { Transcript } from "./Transcript";
|
||||
import { NavSessions } from "./NavSessions";
|
||||
import { NewSessionDialog } from "./NewSessionDialog";
|
||||
import { SettingsDialog } from "./SettingsDialog";
|
||||
import { SteerInput } from "./SteerInput";
|
||||
import { SteerInput, ComposerFooter } from "./SteerInput";
|
||||
import { WorkflowBar } from "./WorkflowBar";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Toaster } from "../components/ui/sonner";
|
||||
import { useState } from "react";
|
||||
import { closeSession } from "../api/sessions";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
/**
|
||||
* The page is designed to live INSIDE the Omics Portal chrome (its left sidebar
|
||||
@@ -17,9 +19,32 @@ import { useState } from "react";
|
||||
*/
|
||||
export function AppShell() {
|
||||
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
||||
const composerRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// The harness "holds the ball" whenever a session is live and no widget is
|
||||
// waiting on the human; once a widget appears, input is back in the user's court.
|
||||
const pendingWidget = useSessionStore((s) => s.pendingWidget);
|
||||
const resetSession = useSessionStore((s) => s.resetSession);
|
||||
const working = Boolean(activeSessionId) && !pendingWidget;
|
||||
|
||||
useSessionStream(activeSessionId);
|
||||
|
||||
function startNewSession() {
|
||||
resetSession();
|
||||
setActiveSessionId(null);
|
||||
requestAnimationFrame(() => composerRef.current?.focus());
|
||||
}
|
||||
|
||||
async function stopSession() {
|
||||
if (!activeSessionId) return;
|
||||
try {
|
||||
await closeSession(activeSessionId);
|
||||
} finally {
|
||||
resetSession();
|
||||
setActiveSessionId(null);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-background text-foreground">
|
||||
{/* Conversation column */}
|
||||
@@ -45,28 +70,49 @@ export function AppShell() {
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{activeSessionId && (
|
||||
<div className="shrink-0 px-6 pb-6 pt-2">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<SteerInput sessionId={activeSessionId} />
|
||||
{/* Sticky composer: the box stays pinned while the conversation scrolls above. */}
|
||||
<div className="shrink-0 px-6 pb-6 pt-2">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="rounded-2xl border border-input bg-card shadow-sm transition-colors focus-within:border-primary/40 focus-within:ring-3 focus-within:ring-ring/15">
|
||||
<div className="px-2.5 py-2">
|
||||
<SteerInput
|
||||
sessionId={activeSessionId}
|
||||
onSessionCreated={setActiveSessionId}
|
||||
onStop={stopSession}
|
||||
inputRef={composerRef}
|
||||
/>
|
||||
</div>
|
||||
<ComposerFooter />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right session rail — symmetric to the portal's left sidebar */}
|
||||
<aside className="flex w-[280px] shrink-0 flex-col border-l border-border bg-sidebar">
|
||||
<div className="px-4 pb-3 pt-5">
|
||||
<div className="relative px-4 pb-3 pt-5 text-center">
|
||||
{working && (
|
||||
<WorkingSpinner className="absolute left-4 top-[1.35rem]" />
|
||||
)}
|
||||
<h1 className="font-heading text-xl font-semibold leading-none tracking-tight text-foreground">
|
||||
Thoth<span className="text-primary">II</span>
|
||||
</h1>
|
||||
<p className="mt-1.5 text-[0.7rem] uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Datamart Builder with Human In The Loop
|
||||
Datamart Builder with
|
||||
<br />
|
||||
Human In The Loop
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 px-4 pb-3">
|
||||
<NewSessionDialog onCreated={setActiveSessionId} />
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={startNewSession}
|
||||
>
|
||||
New session
|
||||
</Button>
|
||||
<SettingsDialog />
|
||||
</div>
|
||||
|
||||
@@ -88,6 +134,22 @@ export function AppShell() {
|
||||
);
|
||||
}
|
||||
|
||||
/** Brand-tinted spinner shown while the assistant/harness is working. */
|
||||
function WorkingSpinner({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
role="status"
|
||||
aria-label="Assistant is working"
|
||||
className={["size-4 animate-spin text-primary", className].filter(Boolean).join(" ")}
|
||||
>
|
||||
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="3" opacity="0.2" />
|
||||
<path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" strokeWidth="3" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyState() {
|
||||
return (
|
||||
<div className="flex min-h-[60vh] flex-col items-center justify-center text-center">
|
||||
@@ -96,8 +158,8 @@ function EmptyState() {
|
||||
</span>
|
||||
<p className="mt-4 max-w-md text-balance text-[0.95rem] leading-relaxed text-muted-foreground">
|
||||
Build datamarts from your data warehouse through a guided,
|
||||
human-in-the-loop workflow. Start a new question from the panel on the
|
||||
right to begin a session.
|
||||
human-in-the-loop workflow. Type your question in the box below to begin
|
||||
a session.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user