mirror of
https://github.com/sortedcord/omnia.git
synced 2026-07-21 19:42:48 +05:30
feat(gui): Minor UI Improvements to InteractView
This commit is contained in:
2
apps/gui/next-env.d.ts
vendored
2
apps/gui/next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
BIN
apps/gui/public/calendar_logo.png
Normal file
BIN
apps/gui/public/calendar_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 607 B |
BIN
apps/gui/public/clock_day_icon.png
Normal file
BIN
apps/gui/public/clock_day_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
apps/gui/public/clock_night_icon.png
Normal file
BIN
apps/gui/public/clock_night_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
apps/gui/public/map_pointer_icon.png
Normal file
BIN
apps/gui/public/map_pointer_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 463 B |
@@ -128,7 +128,13 @@ export function HandoffModal({ entry, onClose }: HandoffModalProps) {
|
||||
</h3>
|
||||
{chunks.map(
|
||||
(
|
||||
chunk: { content: string; importance: number },
|
||||
chunk: {
|
||||
content: string;
|
||||
importance: number;
|
||||
quotes?: string[];
|
||||
retainInBuffer?: boolean;
|
||||
involvedEntityIds?: string[];
|
||||
},
|
||||
index: number,
|
||||
) => (
|
||||
<div
|
||||
|
||||
188
apps/gui/src/components/play/InteractDock.tsx
Normal file
188
apps/gui/src/components/play/InteractDock.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { SimSnapshot } from "@/lib/simulation-types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { formatSimDate, formatSimTimeHM, getClockIcon } from "@/lib/utils";
|
||||
|
||||
interface InteractDockProps {
|
||||
snapshot: SimSnapshot;
|
||||
loading: boolean;
|
||||
playerInput: string;
|
||||
setPlayerInput: (value: string) => void;
|
||||
onSubmitAction: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||
onPauseRequested: () => void;
|
||||
onResumeRequested: () => void;
|
||||
onStopRequested: () => void;
|
||||
}
|
||||
|
||||
export function InteractDock({
|
||||
snapshot,
|
||||
loading,
|
||||
playerInput,
|
||||
setPlayerInput,
|
||||
onSubmitAction,
|
||||
onPauseRequested,
|
||||
onResumeRequested,
|
||||
onStopRequested,
|
||||
}: InteractDockProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<footer className="sticky bottom-0 px-8 py-4 z-20 shrink-0 bg-background/70 backdrop-blur-md border-t border-border/10">
|
||||
<div className="max-w-200 mx-auto relative">
|
||||
{snapshot.status === "running" ||
|
||||
snapshot.status === "waiting_player" ? (
|
||||
<div className="border border-border/30 bg-card/85 p-4 shadow-sm backdrop-blur-sm relative z-10">
|
||||
{snapshot.status === "waiting_player" && snapshot.waitingEntity && (
|
||||
<details className="mb-3">
|
||||
<summary className="cursor-pointer text-sm font-medium font-head text-primary select-none outline-none">
|
||||
<strong>Your context as {snapshot.waitingEntity.name}</strong>
|
||||
</summary>
|
||||
<pre className="text-xs whitespace-pre-wrap bg-input border border-border/20 p-2 max-h-37.5 overflow-y-auto mt-2 font-mono">
|
||||
{snapshot.waitingEntity.userContext}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
{snapshot.worldTime ? (
|
||||
<span className="text-base font-mono text-muted-foreground font-medium">
|
||||
<img
|
||||
src="/calendar_logo.png"
|
||||
alt="Date"
|
||||
className="w-7 h-7 inline-block align-middle mr-1 opacity-70"
|
||||
/>
|
||||
{formatSimDate(snapshot.worldTime)}
|
||||
<img
|
||||
src={getClockIcon(snapshot.worldTime)}
|
||||
alt="Time"
|
||||
className="w-7 h-7 inline-block align-middle ml-2 mr-1 opacity-70"
|
||||
/>
|
||||
{formatSimTimeHM(snapshot.worldTime)}
|
||||
{snapshot.currentLocation && (
|
||||
<>
|
||||
<img
|
||||
src="/map_pointer_icon.png"
|
||||
alt="Location"
|
||||
className="w-7 h-7 inline-block align-middle ml-2 mr-1 opacity-70"
|
||||
/>
|
||||
{snapshot.currentLocation}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
{loading ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={onPauseRequested}
|
||||
>
|
||||
Pause
|
||||
</Button>
|
||||
) : (
|
||||
snapshot.status === "running" && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={onResumeRequested}
|
||||
>
|
||||
Resume
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={onStopRequested}
|
||||
>
|
||||
Stop
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{snapshot.status === "waiting_player" &&
|
||||
snapshot.waitingEntity && (
|
||||
<form
|
||||
onSubmit={onSubmitAction}
|
||||
className="flex flex-row gap-2 items-stretch"
|
||||
>
|
||||
<Textarea
|
||||
value={playerInput}
|
||||
onChange={(e) => setPlayerInput(e.target.value)}
|
||||
placeholder="Describe what your character does, says, or thinks..."
|
||||
rows={3}
|
||||
className="flex-1 min-h-26"
|
||||
disabled={loading}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading || !playerInput.trim()}
|
||||
className="w-32 shrink-0 self-stretch"
|
||||
>
|
||||
{loading ? "Processing..." : "Submit"}
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : snapshot.status === "done" || snapshot.status === "error" ? (
|
||||
<div className="flex justify-between items-center bg-card/85 border border-border/30 p-4 shadow-sm backdrop-blur-sm">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-sm font-mono text-muted-foreground">
|
||||
{snapshot.status === "error"
|
||||
? "Simulation finished with an error."
|
||||
: "Simulation complete."}
|
||||
</span>
|
||||
{snapshot.worldTime && (
|
||||
<div className="flex items-center gap-2 text-base font-mono text-muted-foreground">
|
||||
<img
|
||||
src="/calendar_logo.png"
|
||||
alt="Date"
|
||||
className="w-7 h-7 opacity-70"
|
||||
/>
|
||||
<span>{formatSimDate(snapshot.worldTime)}</span>
|
||||
<img
|
||||
src={getClockIcon(snapshot.worldTime)}
|
||||
alt="Time"
|
||||
className="w-7 h-7 opacity-70"
|
||||
/>
|
||||
<span>{formatSimTimeHM(snapshot.worldTime)}</span>
|
||||
{snapshot.currentLocation && (
|
||||
<>
|
||||
<img
|
||||
src="/map_pointer_icon.png"
|
||||
alt="Location"
|
||||
className="w-7 h-7 opacity-70"
|
||||
/>
|
||||
<span>{snapshot.currentLocation}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
router.push("/");
|
||||
}}
|
||||
size="sm"
|
||||
>
|
||||
{snapshot.status === "error"
|
||||
? "Back to Dashboard"
|
||||
: "New Simulation"}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { SimSnapshot } from "@/lib/simulation-types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { hydrate } from "@omnia/voice";
|
||||
@@ -15,6 +13,7 @@ import {
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/components/ui/alert";
|
||||
import { InteractDock } from "./InteractDock";
|
||||
|
||||
function IntentTag({
|
||||
intent,
|
||||
@@ -117,7 +116,7 @@ function LogEntryCard({
|
||||
entry: SimSnapshot["log"][number];
|
||||
onShowPrompt: (entry: SimSnapshot["log"][number]) => void;
|
||||
isPlayerCard: boolean;
|
||||
playerAlciases: Record<string, string>;
|
||||
playerAliases: Record<string, string>;
|
||||
playerId: string;
|
||||
entities: SimSnapshot["entities"];
|
||||
}) {
|
||||
@@ -182,6 +181,9 @@ interface InteractViewProps {
|
||||
onShowPrompt: (entry: SimSnapshot["log"][number]) => void;
|
||||
onShowHandoff: (entry: SimSnapshot["log"][number]) => void;
|
||||
logEndRef: React.RefObject<HTMLDivElement | null>;
|
||||
onPauseRequested: () => void;
|
||||
onResumeRequested: () => void;
|
||||
onStopRequested: () => void;
|
||||
}
|
||||
|
||||
export function InteractView({
|
||||
@@ -194,16 +196,17 @@ export function InteractView({
|
||||
onShowPrompt,
|
||||
onShowHandoff,
|
||||
logEndRef,
|
||||
onPauseRequested,
|
||||
onResumeRequested,
|
||||
onStopRequested,
|
||||
}: InteractViewProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const playerEntity = snapshot.entities.find((e) => e.isPlayer);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Scrollable Center Viewport */}
|
||||
<main className="flex-1 overflow-y-auto px-8 py-6">
|
||||
<div className="flex flex-col gap-4 max-w-200 mx-auto pb-12">
|
||||
<div className="flex flex-col gap-4 max-w-200 mx-auto pb-44 md:pb-52">
|
||||
{snapshot.log.map((entry, i) => {
|
||||
if (entry.isHandoff) {
|
||||
return (
|
||||
@@ -257,54 +260,16 @@ export function InteractView({
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Sticky Chat / Interaction Input Footer */}
|
||||
<footer className="sticky bottom-0 bg-background/95 backdrop-blur-xs border-t border-dotted border-border/20 px-8 py-4 z-10 shrink-0">
|
||||
<div className="max-w-200 mx-auto">
|
||||
{snapshot.status === "waiting_player" && snapshot.waitingEntity ? (
|
||||
<div className="border border-border/30 bg-card p-4 shadow-sm">
|
||||
<details className="mb-3">
|
||||
<summary className="cursor-pointer text-sm font-medium font-head text-primary select-none outline-none">
|
||||
<strong>Your context as {snapshot.waitingEntity.name}</strong>
|
||||
</summary>
|
||||
<pre className="text-xs whitespace-pre-wrap bg-input border border-border/20 p-2 max-h-37.5 overflow-y-auto mt-2 font-mono">
|
||||
{snapshot.waitingEntity.userContext}
|
||||
</pre>
|
||||
</details>
|
||||
|
||||
<form onSubmit={onSubmitAction} className="flex flex-col gap-2">
|
||||
<Textarea
|
||||
value={playerInput}
|
||||
onChange={(e) => setPlayerInput(e.target.value)}
|
||||
placeholder="Describe what your character does, says, or thinks..."
|
||||
rows={3}
|
||||
disabled={loading}
|
||||
/>
|
||||
<Button type="submit" disabled={loading || !playerInput.trim()}>
|
||||
{loading ? "Processing..." : "Submit Action"}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
) : snapshot.status === "done" || snapshot.status === "error" ? (
|
||||
<div className="flex justify-between items-center bg-card border border-border/30 p-4 shadow-sm">
|
||||
<span className="text-sm font-mono text-muted-foreground">
|
||||
{snapshot.status === "error"
|
||||
? "Simulation finished with an error."
|
||||
: "Simulation complete."}
|
||||
</span>
|
||||
<Button
|
||||
onClick={() => {
|
||||
router.push("/");
|
||||
}}
|
||||
size="sm"
|
||||
>
|
||||
{snapshot.status === "error"
|
||||
? "Back to Dashboard"
|
||||
: "New Simulation"}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</footer>
|
||||
<InteractDock
|
||||
snapshot={snapshot}
|
||||
loading={loading}
|
||||
playerInput={playerInput}
|
||||
setPlayerInput={setPlayerInput}
|
||||
onSubmitAction={onSubmitAction}
|
||||
onPauseRequested={onPauseRequested}
|
||||
onResumeRequested={onResumeRequested}
|
||||
onStopRequested={onStopRequested}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -319,43 +319,6 @@ export function PlayView() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* Simulation Global Controls */}
|
||||
<div className="flex gap-2 shrink-0">
|
||||
{snapshot.status !== "done" && snapshot.status !== "error" && (
|
||||
<>
|
||||
{snapshot.status === "running" &&
|
||||
(loading ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
pauseRequestedRef.current = true;
|
||||
}}
|
||||
>
|
||||
Pause
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => runSteps(snapshot.id)}
|
||||
>
|
||||
Resume
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
pauseRequestedRef.current = true;
|
||||
router.push("/");
|
||||
}}
|
||||
>
|
||||
Stop
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs font-mono mt-1 pt-1.5 border-t border-border/10">
|
||||
<span className="text-muted-foreground">
|
||||
@@ -390,6 +353,14 @@ export function PlayView() {
|
||||
onShowPrompt={setSelectedEntryForModal}
|
||||
onShowHandoff={setSelectedHandoffForModal}
|
||||
logEndRef={logEndRef}
|
||||
onPauseRequested={() => {
|
||||
pauseRequestedRef.current = true;
|
||||
}}
|
||||
onResumeRequested={() => runSteps(snapshot.id)}
|
||||
onStopRequested={() => {
|
||||
pauseRequestedRef.current = true;
|
||||
router.push("/");
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ManageView snapshot={snapshot} onRename={setSnapshot} />
|
||||
|
||||
@@ -42,6 +42,9 @@ export interface HandoffResult {
|
||||
chunks: {
|
||||
content: string;
|
||||
importance: number;
|
||||
quotes?: string[];
|
||||
retainInBuffer?: boolean;
|
||||
involvedEntityIds?: string[];
|
||||
}[];
|
||||
}
|
||||
|
||||
@@ -103,4 +106,6 @@ export interface SimSnapshot {
|
||||
entityIndex: number;
|
||||
waitingEntity?: WaitingContext;
|
||||
error?: string;
|
||||
worldTime?: string;
|
||||
currentLocation?: string;
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ export async function runHandoffResolution(session: SimSession): Promise<void> {
|
||||
}
|
||||
: undefined,
|
||||
usage: lastCall?.usage,
|
||||
handoffResult: lastResult?.response || lastCall?.response,
|
||||
handoffResult: (lastResult?.response || lastCall?.response) as any,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,6 +483,23 @@ export class SimulationManager {
|
||||
};
|
||||
});
|
||||
|
||||
// Get current location from the waiting entity
|
||||
let currentLocation: string | undefined;
|
||||
if (
|
||||
worldState &&
|
||||
session.entityIndex >= 0 &&
|
||||
session.entityIndex < session.entities.length
|
||||
) {
|
||||
const currentEntityInfo = session.entities[session.entityIndex];
|
||||
const actualEntity = worldState.getEntity(currentEntityInfo.id);
|
||||
if (actualEntity?.locationId) {
|
||||
const location = worldState.getLocation(actualEntity.locationId);
|
||||
if (location) {
|
||||
currentLocation = location.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: session.worldInstanceId,
|
||||
status: session.status,
|
||||
@@ -495,6 +512,8 @@ export class SimulationManager {
|
||||
entityIndex: session.entityIndex,
|
||||
waitingEntity: session.waitingEntity,
|
||||
error: session.error,
|
||||
worldTime: worldState?.clock.get().toISOString(),
|
||||
currentLocation,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
IntentInfo,
|
||||
LogEntry,
|
||||
WaitingContext,
|
||||
ValidatorCall,
|
||||
} from "../simulation-types";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -88,7 +89,7 @@ async function processIntents(
|
||||
prompt: {
|
||||
systemPrompt: lastResult.systemPrompt || "",
|
||||
userContext: lastResult.userContext || "",
|
||||
components: lastResult.promptComponents,
|
||||
components: lastResult.components,
|
||||
},
|
||||
response: {
|
||||
isValid: outcome.isValid,
|
||||
|
||||
@@ -4,3 +4,41 @@ import { twMerge } from "tailwind-merge";
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function formatSimDate(isoString: string): string {
|
||||
try {
|
||||
const d = new Date(isoString);
|
||||
if (isNaN(d.getTime())) return isoString;
|
||||
const yyyy = d.getUTCFullYear();
|
||||
const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
|
||||
const dd = String(d.getUTCDate()).padStart(2, "0");
|
||||
return `${yyyy}-${mm}-${dd}`;
|
||||
} catch {
|
||||
return isoString;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatSimTimeHM(isoString: string): string {
|
||||
try {
|
||||
const d = new Date(isoString);
|
||||
if (isNaN(d.getTime())) return isoString;
|
||||
const hh = String(d.getUTCHours());
|
||||
const mm = String(d.getUTCMinutes()).padStart(2, "0");
|
||||
return `${hh}:${mm}`;
|
||||
} catch {
|
||||
return isoString;
|
||||
}
|
||||
}
|
||||
|
||||
export function getClockIcon(isoString: string): string {
|
||||
try {
|
||||
const d = new Date(isoString);
|
||||
if (isNaN(d.getTime())) return "/clock_day_icon.png";
|
||||
const hour = d.getUTCHours();
|
||||
return hour >= 6 && hour < 18
|
||||
? "/clock_day_icon.png"
|
||||
: "/clock_night_icon.png";
|
||||
} catch {
|
||||
return "/clock_day_icon.png";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user