fix findings - bigger chatbox

This commit is contained in:
gsknnft
2026-04-09 09:49:30 -04:00
parent 09067af2cd
commit 7e501136c1
5 changed files with 358 additions and 47 deletions
+16 -5
View File
@@ -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) => ({
+133 -15
View File
@@ -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>
+43 -21
View File
@@ -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) =>