mirror of
https://github.com/iamlukethedev/Claw3D.git
synced 2026-08-14 00:58:04 +00:00
fix findings - bigger chatbox
This commit is contained in:
@@ -91,17 +91,28 @@ function agentListPayload() {
|
||||
|
||||
function buildDemoReply(agent, message) {
|
||||
const normalized = message.trim();
|
||||
const compactMessage = normalized.replace(/\s+/g, " ").trim();
|
||||
const greetingOnly = /^(hi|hello|hey|yo|sup|what'?s up|how are you)[!.? ]*$/i.test(compactMessage);
|
||||
const opening =
|
||||
agent.role === "Orchestrator"
|
||||
? `${agent.name} here. Demo office is live and the team is synced.`
|
||||
: `${agent.name} reporting in from the ${agent.role.toLowerCase()} desk.`;
|
||||
: `${agent.name} checking in from the ${agent.role.toLowerCase()} desk.`;
|
||||
if (greetingOnly) {
|
||||
return agent.role === "Orchestrator"
|
||||
? `${opening} I can coordinate the room, sketch a plan, or hand work to Research and Builder.`
|
||||
: `${opening} Give me a concrete task and I will respond in-character with a focused next step.`;
|
||||
}
|
||||
const focusLine =
|
||||
compactMessage.length > 160
|
||||
? `${compactMessage.slice(0, 160).trimEnd()}...`
|
||||
: compactMessage;
|
||||
const action =
|
||||
agent.role === "Research"
|
||||
? "I would break this down into sources, constraints, and next questions."
|
||||
? "I would turn this into source checks, constraints, and follow-up questions."
|
||||
: agent.role === "Builder"
|
||||
? "I would turn that into concrete implementation steps and validation."
|
||||
: "I can coordinate the team, route work, and summarize progress.";
|
||||
return `${opening} You said: "${normalized}". ${action}`;
|
||||
? "I would translate this into implementation steps, edge cases, and validation."
|
||||
: "I would route the work, keep the team aligned, and summarize the next move.";
|
||||
return `${opening} Focus: ${focusLine}. ${action}`;
|
||||
}
|
||||
|
||||
async function handleMethod(method, params, id, sendEvent) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import type { AgentState as AgentRecord } from "@/features/agents/state/store";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { Check, ChevronRight, Clock, Mic, Pencil, Square, Trash2, X } from "lucide-react";
|
||||
import { Check, ChevronRight, Clock, Mic, Paperclip, Pencil, Square, Trash2, X } from "lucide-react";
|
||||
import type { GatewayModelChoice } from "@/lib/gateway/models";
|
||||
import type { AgentAvatarProfile } from "@/lib/avatars/profile";
|
||||
import { rewriteMediaLinesToMarkdown } from "@/lib/text/media-markdown";
|
||||
@@ -64,6 +64,54 @@ const EMPTY_CHAT_INTRO_MESSAGES = [
|
||||
"What are we working on today?",
|
||||
"I'm here and ready. What's the plan?",
|
||||
];
|
||||
const TEXT_ATTACHMENT_EXTENSIONS = new Set([
|
||||
"txt",
|
||||
"md",
|
||||
"markdown",
|
||||
"json",
|
||||
"js",
|
||||
"jsx",
|
||||
"ts",
|
||||
"tsx",
|
||||
"py",
|
||||
"rb",
|
||||
"go",
|
||||
"rs",
|
||||
"java",
|
||||
"kt",
|
||||
"sql",
|
||||
"html",
|
||||
"css",
|
||||
"xml",
|
||||
"yaml",
|
||||
"yml",
|
||||
"csv",
|
||||
"log",
|
||||
]);
|
||||
const MAX_ATTACHMENT_TEXT_CHARS = 12_000;
|
||||
|
||||
const isTextAttachmentFile = (file: File): boolean => {
|
||||
const mime = file.type.trim().toLowerCase();
|
||||
if (mime.startsWith("text/")) return true;
|
||||
if (
|
||||
mime.includes("json") ||
|
||||
mime.includes("javascript") ||
|
||||
mime.includes("typescript") ||
|
||||
mime.includes("xml") ||
|
||||
mime.includes("yaml")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const extension = file.name.split(".").pop()?.trim().toLowerCase() ?? "";
|
||||
return extension.length > 0 && TEXT_ATTACHMENT_EXTENSIONS.has(extension);
|
||||
};
|
||||
|
||||
const buildAttachmentPromptBlock = (fileName: string, content: string): string =>
|
||||
[
|
||||
`[Attached reference: ${fileName}]`,
|
||||
content,
|
||||
`[End attached reference: ${fileName}]`,
|
||||
].join("\n");
|
||||
|
||||
const stableStringHash = (value: string): number => {
|
||||
let hash = 0;
|
||||
@@ -882,6 +930,7 @@ const AgentChatComposer = memo(function AgentChatComposer({
|
||||
onChange,
|
||||
onKeyDown,
|
||||
onSend,
|
||||
onAttachmentFiles,
|
||||
onVoiceToggle,
|
||||
onStop,
|
||||
canSend,
|
||||
@@ -893,6 +942,8 @@ const AgentChatComposer = memo(function AgentChatComposer({
|
||||
voiceSupported,
|
||||
voiceState,
|
||||
voiceError,
|
||||
attachmentStatus,
|
||||
attachmentInputRef,
|
||||
queuedMessages,
|
||||
onRemoveQueuedMessage,
|
||||
inputRef,
|
||||
@@ -911,6 +962,7 @@ const AgentChatComposer = memo(function AgentChatComposer({
|
||||
onChange: (event: ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
onKeyDown: (event: KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
onSend: () => void;
|
||||
onAttachmentFiles: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
onVoiceToggle?: () => void;
|
||||
onStop: () => void;
|
||||
canSend: boolean;
|
||||
@@ -922,6 +974,8 @@ const AgentChatComposer = memo(function AgentChatComposer({
|
||||
voiceSupported: boolean;
|
||||
voiceState: VoiceRecorderState;
|
||||
voiceError?: string | null;
|
||||
attachmentStatus?: string | null;
|
||||
attachmentInputRef: MutableRefObject<HTMLInputElement | null>;
|
||||
queuedMessages: string[];
|
||||
onRemoveQueuedMessage?: (index: number) => void;
|
||||
inputRef: (el: HTMLTextAreaElement | HTMLInputElement | null) => void;
|
||||
@@ -1125,7 +1179,7 @@ const AgentChatComposer = memo(function AgentChatComposer({
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{voiceStatusText || voiceError ? (
|
||||
{voiceStatusText || voiceError || attachmentStatus ? (
|
||||
<div
|
||||
className={`mb-2 rounded-md border px-2.5 py-1.5 font-mono text-[10px] tracking-[0.02em] ${
|
||||
voiceError
|
||||
@@ -1134,10 +1188,18 @@ const AgentChatComposer = memo(function AgentChatComposer({
|
||||
}`}
|
||||
data-testid="agent-voice-status"
|
||||
>
|
||||
{voiceError ?? voiceStatusText}
|
||||
{voiceError ?? voiceStatusText ?? attachmentStatus}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex items-end gap-2">
|
||||
<input
|
||||
ref={attachmentInputRef}
|
||||
type="file"
|
||||
className="hidden"
|
||||
multiple
|
||||
accept=".txt,.md,.markdown,.json,.js,.jsx,.ts,.tsx,.py,.rb,.go,.rs,.java,.kt,.sql,.html,.css,.xml,.yaml,.yml,.csv,.log,text/*,application/json,application/xml"
|
||||
onChange={onAttachmentFiles}
|
||||
/>
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
rows={1}
|
||||
@@ -1147,6 +1209,19 @@ const AgentChatComposer = memo(function AgentChatComposer({
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="type a message"
|
||||
/>
|
||||
<button
|
||||
className="rounded-md border border-border/70 bg-surface-3 px-2.5 py-2 font-mono text-[11px] font-medium tracking-[0.02em] text-white transition hover:bg-surface-2 hover:text-white disabled:cursor-not-allowed disabled:border-border/30 disabled:bg-muted/20 disabled:text-muted-foreground"
|
||||
type="button"
|
||||
onClick={() => attachmentInputRef.current?.click()}
|
||||
disabled={!canSend}
|
||||
aria-label="Attach text files"
|
||||
title="Attach text files"
|
||||
>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Paperclip className="h-3.5 w-3.5" />
|
||||
<span>Attach</span>
|
||||
</span>
|
||||
</button>
|
||||
{voiceEnabled ? (
|
||||
<button
|
||||
className={`rounded-md border px-2.5 py-2 font-mono text-[11px] font-medium tracking-[0.02em] transition ${
|
||||
@@ -1224,7 +1299,9 @@ export const AgentChatPanel = ({
|
||||
const [renameSaving, setRenameSaving] = useState(false);
|
||||
const [renameDraft, setRenameDraft] = useState(agent.name);
|
||||
const [renameError, setRenameError] = useState<string | null>(null);
|
||||
const [attachmentStatus, setAttachmentStatus] = useState<string | null>(null);
|
||||
const draftRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const attachmentInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const renameInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const renameEditorRef = useRef<HTMLDivElement | null>(null);
|
||||
const scrollToBottomNextOutputRef = useRef(false);
|
||||
@@ -1317,6 +1394,7 @@ export const AgentChatPanel = ({
|
||||
if (!trimmed) return;
|
||||
plainDraftRef.current = "";
|
||||
setDraftValue("");
|
||||
setAttachmentStatus(null);
|
||||
onDraftChange("");
|
||||
scrollToBottomNextOutputRef.current = true;
|
||||
onSend(trimmed);
|
||||
@@ -1402,6 +1480,48 @@ export const AgentChatPanel = ({
|
||||
handleSend(draftValue);
|
||||
}, [draftValue, handleSend]);
|
||||
|
||||
const handleAttachmentFiles = useCallback(
|
||||
async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(event.target.files ?? []);
|
||||
event.target.value = "";
|
||||
if (files.length === 0) return;
|
||||
const unsupported = files.filter((file) => !isTextAttachmentFile(file));
|
||||
const supported = files.filter((file) => isTextAttachmentFile(file));
|
||||
if (supported.length === 0) {
|
||||
setAttachmentStatus("Only text/code/markdown-style files can be attached right now.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const blocks = await Promise.all(
|
||||
supported.map(async (file) => {
|
||||
const rawText = await file.text();
|
||||
const normalizedText = rawText.trim();
|
||||
const clippedText =
|
||||
normalizedText.length > MAX_ATTACHMENT_TEXT_CHARS
|
||||
? `${normalizedText.slice(0, MAX_ATTACHMENT_TEXT_CHARS).trimEnd()}\n[Truncated]`
|
||||
: normalizedText;
|
||||
return buildAttachmentPromptBlock(file.name, clippedText);
|
||||
})
|
||||
);
|
||||
const nextValue = [draftValue.trim(), ...blocks].filter(Boolean).join("\n\n");
|
||||
plainDraftRef.current = nextValue;
|
||||
setDraftValue(nextValue);
|
||||
onDraftChange(nextValue);
|
||||
const statusParts = [`Attached ${supported.length} file${supported.length === 1 ? "" : "s"}.`];
|
||||
if (unsupported.length > 0) {
|
||||
statusParts.push(`${unsupported.length} unsupported file${unsupported.length === 1 ? "" : "s"} skipped.`);
|
||||
}
|
||||
setAttachmentStatus(statusParts.join(" "));
|
||||
scrollToBottomNextOutputRef.current = true;
|
||||
} catch (error) {
|
||||
setAttachmentStatus(
|
||||
error instanceof Error ? error.message : "Failed to read one or more attachments."
|
||||
);
|
||||
}
|
||||
},
|
||||
[draftValue, onDraftChange]
|
||||
);
|
||||
|
||||
const handleVoiceToggle = useCallback(
|
||||
() => {
|
||||
if (!canSend && voiceState !== "recording") return;
|
||||
@@ -1654,6 +1774,7 @@ export const AgentChatPanel = ({
|
||||
onChange={handleComposerChange}
|
||||
onKeyDown={handleComposerKeyDown}
|
||||
onSend={handleComposerSend}
|
||||
onAttachmentFiles={handleAttachmentFiles}
|
||||
onVoiceToggle={handleVoiceToggle}
|
||||
onStop={onStopRun}
|
||||
canSend={canSend}
|
||||
@@ -1665,6 +1786,8 @@ export const AgentChatPanel = ({
|
||||
voiceSupported={voiceSupported}
|
||||
voiceState={voiceState}
|
||||
voiceError={voiceError}
|
||||
attachmentStatus={attachmentStatus}
|
||||
attachmentInputRef={attachmentInputRef}
|
||||
queuedMessages={agent.queuedMessages ?? []}
|
||||
onRemoveQueuedMessage={onRemoveQueuedMessage}
|
||||
modelOptions={modelOptionsWithFallback.map((option) => ({
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { MessageSquare, ChevronDown, Mic } from "lucide-react";
|
||||
import { MessageSquare, ChevronDown, ChevronLeft, ChevronRight, Mic } from "lucide-react";
|
||||
import { RetroOffice3D } from "@/features/retro-office/RetroOffice3D";
|
||||
import type { OfficeAgent } from "@/features/retro-office/core/types";
|
||||
import { RunningAvatarLoader } from "@/features/agents/components/RunningAvatarLoader";
|
||||
@@ -77,6 +77,7 @@ import {
|
||||
} from "@/lib/office/floorRoster";
|
||||
import {
|
||||
getOfficeFloor,
|
||||
listOfficeFloorsForProvider,
|
||||
resolveActiveOfficeFloorId,
|
||||
type FloorId,
|
||||
} from "@/lib/office/floors";
|
||||
@@ -728,16 +729,42 @@ const normalizeOfficeFeedText = (
|
||||
value: string | null | undefined,
|
||||
maxChars?: number,
|
||||
): string => {
|
||||
const normalized = (value ?? "").replace(/\s+/g, " ").trim();
|
||||
if (!normalized) return "";
|
||||
const normalized = (value ?? "")
|
||||
.replace(/([.!?])([A-Z])/g, "$1 $2")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
const deduped = (normalized.match(/[^.!?]+[.!?]?/g) ?? [])
|
||||
.map((fragment) => fragment.trim())
|
||||
.filter((fragment, index, fragments) => {
|
||||
if (!fragment) return false;
|
||||
const normalizedFragment = fragment
|
||||
.toLowerCase()
|
||||
.replace(/[—–-]/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
return (
|
||||
fragments.findIndex((entry) => {
|
||||
const normalizedEntry = entry
|
||||
.toLowerCase()
|
||||
.replace(/[—–-]/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
return normalizedEntry === normalizedFragment;
|
||||
}) === index
|
||||
);
|
||||
})
|
||||
.join(" ")
|
||||
.trim();
|
||||
const finalText = deduped || normalized;
|
||||
if (!finalText) return "";
|
||||
if (
|
||||
typeof maxChars !== "number" ||
|
||||
!Number.isFinite(maxChars) ||
|
||||
maxChars <= 0
|
||||
) {
|
||||
return normalized;
|
||||
return finalText;
|
||||
}
|
||||
if (normalized.length <= maxChars) return normalized;
|
||||
if (finalText.length <= maxChars) return finalText;
|
||||
return `${normalized.slice(0, Math.max(0, maxChars - 1)).trimEnd()}…`;
|
||||
};
|
||||
|
||||
@@ -932,6 +959,7 @@ export function OfficeScreen({
|
||||
gatewayUrl,
|
||||
token,
|
||||
selectedAdapterType,
|
||||
detectedAdapterType,
|
||||
activeAdapterType,
|
||||
localGatewayDefaults,
|
||||
error: gatewayError,
|
||||
@@ -1023,6 +1051,7 @@ export function OfficeScreen({
|
||||
const historyInFlightRef = useRef<Set<string>>(new Set());
|
||||
const lastTransportHistoryRefreshKeyRef = useRef<Record<string, string>>({});
|
||||
const [chatOpen, setChatOpen] = useState(false);
|
||||
const [chatRosterCollapsed, setChatRosterCollapsed] = useState(false);
|
||||
const [selectedChatAgentId, setSelectedChatAgentId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
@@ -1070,6 +1099,9 @@ export function OfficeScreen({
|
||||
Record<string, string>
|
||||
>({});
|
||||
const [activeFloorId, setActiveFloorId] = useState<FloorId>("lobby");
|
||||
const previousGatewayStatusRef = useRef<"disconnected" | "connecting" | "connected">(
|
||||
"disconnected",
|
||||
);
|
||||
const [floorRosterCache, setFloorRosterCache] = useState(() =>
|
||||
createFloorRosterCache(),
|
||||
);
|
||||
@@ -1130,6 +1162,38 @@ export function OfficeScreen({
|
||||
};
|
||||
}, [settingsCoordinator]);
|
||||
|
||||
useEffect(() => {
|
||||
const previousStatus = previousGatewayStatusRef.current;
|
||||
previousGatewayStatusRef.current = status;
|
||||
if (previousStatus === "connected" || status !== "connected") return;
|
||||
if (activeFloor.kind !== "lobby" || activeFloor.provider !== "demo") return;
|
||||
|
||||
const connectedProvider =
|
||||
detectedAdapterType && detectedAdapterType !== "demo"
|
||||
? detectedAdapterType
|
||||
: selectedAdapterType !== "demo"
|
||||
? selectedAdapterType
|
||||
: null;
|
||||
if (!connectedProvider) return;
|
||||
|
||||
const targetFloor =
|
||||
listOfficeFloorsForProvider(connectedProvider).find(
|
||||
(floor) => floor.enabled && floor.kind === "runtime",
|
||||
) ?? null;
|
||||
if (!targetFloor || targetFloor.id === activeFloor.id) return;
|
||||
|
||||
setActiveFloorId(targetFloor.id);
|
||||
settingsCoordinator.schedulePatch({ activeFloorId: targetFloor.id }, 0);
|
||||
}, [
|
||||
activeFloor.id,
|
||||
activeFloor.kind,
|
||||
activeFloor.provider,
|
||||
detectedAdapterType,
|
||||
selectedAdapterType,
|
||||
settingsCoordinator,
|
||||
status,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
initJukeboxStore();
|
||||
}, [initJukeboxStore]);
|
||||
@@ -2583,7 +2647,7 @@ export function OfficeScreen({
|
||||
|
||||
for (const agent of state.agents) {
|
||||
const previewText = normalizeOfficeFeedText(
|
||||
agent.lastResult ?? agent.latestPreview,
|
||||
agent.latestPreview ?? agent.lastResult,
|
||||
);
|
||||
const previewTs = agent.lastAssistantMessageAt ?? 0;
|
||||
if (!previewText || previewTs <= 0) continue;
|
||||
@@ -4605,7 +4669,10 @@ export function OfficeScreen({
|
||||
/>
|
||||
<section className="relative h-full min-h-0 min-w-0 overflow-hidden">
|
||||
<RetroOffice3D
|
||||
key={activeFloor.id}
|
||||
agents={allVisibleAgents}
|
||||
storageNamespace={activeFloor.id}
|
||||
layoutPreset={activeFloor.kind === "lobby" ? "lobby" : "office"}
|
||||
officeCenterSignal={officeCameraCenterSignal}
|
||||
animationState={officeAnimationState}
|
||||
deskAssignmentByDeskUid={deskAssignmentByDeskUid}
|
||||
@@ -5236,19 +5303,70 @@ export function OfficeScreen({
|
||||
{chatOpen && (
|
||||
<div
|
||||
className="flex overflow-hidden rounded border border-white/10 bg-[#0e0a04] shadow-2xl"
|
||||
style={{ width: 560, height: 520 }}
|
||||
style={{
|
||||
width: chatRosterCollapsed
|
||||
? "min(680px, calc(100vw - 1.5rem))"
|
||||
: "min(780px, calc(100vw - 1.5rem))",
|
||||
height: "min(560px, calc(100vh - 5.5rem))",
|
||||
}}
|
||||
>
|
||||
<div className="flex w-44 shrink-0 flex-col border-r border-white/10">
|
||||
<div
|
||||
className={`flex shrink-0 flex-col border-r border-white/10 transition-[width] ${
|
||||
chatRosterCollapsed ? "w-12" : "w-52"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-white/10 px-3 py-2">
|
||||
<span className="font-mono text-[11px] font-semibold uppercase tracking-widest text-white/60">
|
||||
Agents
|
||||
</span>
|
||||
<span className="font-mono text-[10px] text-white/40">
|
||||
{chatRosterEntries.length}
|
||||
</span>
|
||||
{!chatRosterCollapsed ? (
|
||||
<>
|
||||
<span className="font-mono text-[11px] font-semibold uppercase tracking-widest text-white/60">
|
||||
Agents
|
||||
</span>
|
||||
<span className="font-mono text-[10px] text-white/40">
|
||||
{chatRosterEntries.length}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="mx-auto font-mono text-[10px] text-white/45">
|
||||
{chatRosterEntries.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setChatRosterCollapsed((current) => !current)}
|
||||
className="mx-2 mt-2 inline-flex items-center justify-center rounded border border-white/10 bg-white/5 px-2 py-2 text-white/65 transition hover:border-white/20 hover:bg-white/10 hover:text-white"
|
||||
aria-label={chatRosterCollapsed ? "Expand agent list" : "Collapse agent list"}
|
||||
title={chatRosterCollapsed ? "Expand agent list" : "Collapse agent list"}
|
||||
>
|
||||
{chatRosterCollapsed ? (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{chatRosterEntries.length === 0 ? (
|
||||
{chatRosterCollapsed ? (
|
||||
<div className="flex flex-col items-center gap-2 px-1 py-2">
|
||||
{chatRosterEntries.map((agent) => {
|
||||
const isSelected = agent.id === selectedChatAgentId;
|
||||
return (
|
||||
<button
|
||||
key={agent.id}
|
||||
type="button"
|
||||
onClick={() => handleOpenAgentChat(agent.id)}
|
||||
className={`inline-flex h-8 w-8 items-center justify-center rounded border font-mono text-[10px] transition ${
|
||||
isSelected
|
||||
? "border-cyan-400/45 bg-cyan-950/50 text-cyan-100"
|
||||
: "border-white/10 bg-white/5 text-white/55 hover:border-white/20 hover:bg-white/10 hover:text-white/80"
|
||||
}`}
|
||||
title={agent.name}
|
||||
>
|
||||
{agent.name.slice(0, 1).toUpperCase()}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : chatRosterEntries.length === 0 ? (
|
||||
<div className="px-3 py-4 font-mono text-[11px] text-white/30">
|
||||
No agents.
|
||||
</div>
|
||||
|
||||
@@ -90,6 +90,7 @@ import {
|
||||
ensureOfficeServerRoom,
|
||||
isRetiredPingPongLamp,
|
||||
materializeDefaults,
|
||||
type OfficeLayoutPreset,
|
||||
} from "@/features/retro-office/core/furnitureDefaults";
|
||||
import {
|
||||
clampPointToZone,
|
||||
@@ -2309,12 +2310,37 @@ const getAgentInitials = (name: string | null | undefined): string => {
|
||||
.join("");
|
||||
};
|
||||
|
||||
const buildInitialFurnitureLayout = (
|
||||
storageNamespace: string,
|
||||
layoutPreset: OfficeLayoutPreset,
|
||||
): FurnitureItem[] =>
|
||||
ensureOfficeKanbanBoard(
|
||||
ensureOfficeJukebox(
|
||||
ensureOfficeQaLab(
|
||||
ensureOfficeGymRoom(
|
||||
ensureOfficeServerRoom(
|
||||
ensureOfficePhoneBooth(
|
||||
ensureOfficeSmsBooth(
|
||||
ensureOfficeAtm(
|
||||
ensureOfficePingPongTable(
|
||||
loadFurniture(storageNamespace) ?? materializeDefaults(layoutPreset),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
export function RetroOffice3D({
|
||||
agents,
|
||||
officeCenterSignal = 0,
|
||||
animationState = null,
|
||||
readOnly = false,
|
||||
storageNamespace = "default",
|
||||
layoutPreset = "office",
|
||||
deskAssignmentByDeskUid = EMPTY_STRING_RECORD,
|
||||
cleaningCues = EMPTY_CLEANING_CUES,
|
||||
deskHoldByAgentId = EMPTY_BOOLEAN_RECORD,
|
||||
@@ -2428,6 +2454,7 @@ export function RetroOffice3D({
|
||||
> | null;
|
||||
readOnly?: boolean;
|
||||
storageNamespace?: string;
|
||||
layoutPreset?: OfficeLayoutPreset;
|
||||
deskAssignmentByDeskUid?: Record<string, string>;
|
||||
cleaningCues?: OfficeCleaningCue[];
|
||||
deskHoldByAgentId?: Record<string, boolean>;
|
||||
@@ -2568,26 +2595,8 @@ export function RetroOffice3D({
|
||||
);
|
||||
|
||||
const [furniture, setFurniture] = useState<FurnitureItem[]>(() =>
|
||||
ensureOfficeKanbanBoard(
|
||||
ensureOfficeJukebox(
|
||||
ensureOfficeQaLab(
|
||||
ensureOfficeGymRoom(
|
||||
ensureOfficeServerRoom(
|
||||
ensureOfficePhoneBooth(
|
||||
ensureOfficeSmsBooth(
|
||||
ensureOfficeAtm(
|
||||
ensureOfficePingPongTable(
|
||||
(
|
||||
loadFurniture(storageNamespace) ?? materializeDefaults()
|
||||
).filter((item) => !isRetiredPingPongLamp(item)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
buildInitialFurnitureLayout(storageNamespace, layoutPreset).filter(
|
||||
(item) => !isRetiredPingPongLamp(item),
|
||||
),
|
||||
);
|
||||
const defaultRemoteLayoutFurniture = useMemo(
|
||||
@@ -2614,6 +2623,19 @@ export function RetroOffice3D({
|
||||
: defaultRemoteLayoutFurniture,
|
||||
[defaultRemoteLayoutFurniture, remoteLayoutSnapshot, remoteOfficeEnabled],
|
||||
);
|
||||
useEffect(() => {
|
||||
setFurniture(
|
||||
buildInitialFurnitureLayout(storageNamespace, layoutPreset).filter(
|
||||
(item) => !isRetiredPingPongLamp(item),
|
||||
),
|
||||
);
|
||||
setSelectedUid(null);
|
||||
setDeskActionUid(null);
|
||||
setDeskAssignPickerOpen(false);
|
||||
setDrag({ kind: "idle" });
|
||||
setGhostPos(null);
|
||||
setWallDrawStart(null);
|
||||
}, [layoutPreset, storageNamespace]);
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [selectedUid, setSelectedUid] = useState<string | null>(null);
|
||||
const [hoverUid, setHoverUid] = useState<string | null>(null);
|
||||
@@ -4984,7 +5006,7 @@ export function RetroOffice3D({
|
||||
.filter((item) => item.type === "desk_cubicle")
|
||||
.map((item) => item._uid),
|
||||
);
|
||||
setFurniture(materializeDefaults());
|
||||
setFurniture(materializeDefaults(layoutPreset));
|
||||
setSelectedUid(null);
|
||||
setDrag({ kind: "idle" });
|
||||
setGhostPos(null);
|
||||
|
||||
@@ -24,6 +24,8 @@ import type {
|
||||
FurnitureSeed,
|
||||
} from "@/features/retro-office/core/types";
|
||||
|
||||
export type OfficeLayoutPreset = "office" | "lobby";
|
||||
|
||||
const DEFAULT_PINGPONG_TABLE: FurnitureSeed = {
|
||||
type: "pingpong",
|
||||
x: 950,
|
||||
@@ -418,6 +420,39 @@ const DEFAULT_ART_ROOM_ITEMS: FurnitureSeed[] = [
|
||||
{ type: "plant", x: 280, y: 240 },
|
||||
];
|
||||
|
||||
const DEFAULT_LOBBY_FURNITURE: FurnitureSeed[] = [
|
||||
{ type: "round_table", x: 120, y: 110, r: 72 },
|
||||
{ type: "chair", x: 182, y: 110, facing: 0 },
|
||||
{ type: "chair", x: 160, y: 168, facing: 220 },
|
||||
{ type: "chair", x: 92, y: 170, facing: 140 },
|
||||
{ type: "chair", x: 58, y: 112, facing: 90 },
|
||||
{ type: "chair", x: 92, y: 52, facing: 40 },
|
||||
{ type: "bookshelf", x: 248, y: 32, w: 78, h: 118 },
|
||||
{ type: "couch", x: 332, y: 92, w: 44, h: 112, vertical: true, facing: 180 },
|
||||
{ type: "couch", x: 430, y: 92, w: 44, h: 112, vertical: true, facing: 180 },
|
||||
{ type: "table_rect", x: 382, y: 138, w: 72, h: 34 },
|
||||
{ type: "beanbag", x: 332, y: 210, color: "#1565c0", facing: 135 },
|
||||
{ type: "beanbag", x: 436, y: 216, color: "#7c3aed", facing: 225 },
|
||||
{ type: "whiteboard", x: 36, y: 214, w: 10, h: 64 },
|
||||
{ type: "clock", x: 566, y: 6 },
|
||||
{ type: "table_rect", x: 874, y: 102, w: 124, h: 34, facing: 0 },
|
||||
{ type: "chair", x: 934, y: 176, facing: 180 },
|
||||
{ type: "vending", x: 788, y: 10 },
|
||||
{ type: "trash", x: 826, y: 20 },
|
||||
{ type: "couch", x: 982, y: 382, w: 112, h: 42, facing: 90 },
|
||||
{ type: "couch", x: 392, y: 634, w: 112, h: 42 },
|
||||
{ type: "table_rect", x: 980, y: 380, w: 60, h: 30, facing: 270 },
|
||||
{ type: "plant", x: 40, y: 40 },
|
||||
{ type: "plant", x: 662, y: 32 },
|
||||
{ type: "plant", x: 340, y: 700 },
|
||||
{ type: "plant", x: 1088, y: 312 },
|
||||
{ type: "plant", x: 530, y: 700 },
|
||||
...DEFAULT_SERVER_ROOM_ITEMS,
|
||||
...DEFAULT_GYM_ITEMS,
|
||||
...DEFAULT_QA_LAB_ITEMS,
|
||||
...DEFAULT_ART_ROOM_ITEMS,
|
||||
];
|
||||
|
||||
const DEFAULT_FURNITURE: FurnitureSeed[] = [
|
||||
{ type: "round_table", x: 50, y: 50, r: 90 },
|
||||
{ type: "chair", x: 130, y: 50, facing: 0 },
|
||||
@@ -522,10 +557,12 @@ const DEFAULT_FURNITURE: FurnitureSeed[] = [
|
||||
{ type: "chair", x: 100, y: 200, facing: 180 },
|
||||
];
|
||||
|
||||
export const materializeDefaults = (): FurnitureItem[] =>
|
||||
DEFAULT_FURNITURE.map((item, index) => ({
|
||||
export const materializeDefaults = (
|
||||
preset: OfficeLayoutPreset = "office",
|
||||
): FurnitureItem[] =>
|
||||
(preset === "lobby" ? DEFAULT_LOBBY_FURNITURE : DEFAULT_FURNITURE).map((item, index) => ({
|
||||
...item,
|
||||
_uid: `default_${index}`,
|
||||
_uid: `${preset}_${index}`,
|
||||
}));
|
||||
|
||||
export const isRetiredPingPongLamp = (item: FurnitureItem) =>
|
||||
|
||||
Reference in New Issue
Block a user