feat: 3d ofis deneyimini ve etkilesimleri gelistir

This commit is contained in:
2026-03-20 01:36:20 +03:00
parent db477c7d5f
commit bda7500922
16 changed files with 583 additions and 130 deletions

BIN
web/public/ata-cropped.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

View File

@@ -17,6 +17,8 @@ export default function App() {
const [busy, setBusy] = useState(false);
const [teamView, setTeamView] = useState("board");
const [officeAgents, setOfficeAgents] = useState(() => createInitialOfficeAgents());
const [selectedOfficeObject, setSelectedOfficeObject] = useState(null);
const [dismissedSpeech, setDismissedSpeech] = useState({});
const autoStartedRef = useRef(false);
async function runAction(action) {
@@ -59,7 +61,7 @@ export default function App() {
...current,
[command.agentId]: {
...current[command.agentId],
zoneId: command.zoneId,
targetZoneId: command.zoneId,
targetPosition: zone.approachPosition
}
}));
@@ -68,12 +70,88 @@ export default function App() {
}
function handleAgentArrive(agentId, position) {
setOfficeAgents((current) => ({
...current,
[agentId]: {
...current[agentId],
currentPosition: position
let arrivedZoneId = null;
setOfficeAgents((current) => {
arrivedZoneId = current[agentId]?.targetZoneId ?? null;
return {
...current,
[agentId]: {
...current[agentId],
currentPosition: position,
currentZoneId: arrivedZoneId
}
};
});
if (String(arrivedZoneId ?? "").endsWith("Desk")) {
setSelectedOfficeObject((current) =>
current?.type === "agent" && current.id === agentId ? null : current
);
}
}
function handleOfficeAgentSelect(agentId) {
setSelectedOfficeObject(() => ({ type: "agent", id: agentId }));
}
function handleOfficeObjectSelect(type, id) {
setSelectedOfficeObject(() => ({ type, id }));
}
function handleOfficeFloorSelect(position) {
if (!selectedOfficeObject || selectedOfficeObject.type !== "agent") {
return;
}
const agentId = selectedOfficeObject.id;
setOfficeAgents((current) => {
if (!current[agentId]) {
return current;
}
return {
...current,
[agentId]: {
...current[agentId],
targetZoneId: null,
targetPosition: [position[0], 0, position[2]]
}
};
});
}
function handleOfficeZoneSelect(zoneId) {
if (!selectedOfficeObject || selectedOfficeObject.type !== "agent") {
return;
}
const zone = getZoneById(zoneId);
if (!zone) {
return;
}
const agentId = selectedOfficeObject.id;
setOfficeAgents((current) => {
if (!current[agentId]) {
return current;
}
return {
...current,
[agentId]: {
...current[agentId],
targetZoneId: zoneId,
targetPosition: zone.approachPosition
}
};
});
}
function handleDismissSpeech(agentId, speechKey) {
setDismissedSpeech((current) => ({
...current,
[agentId]: speechKey
}));
}
@@ -110,6 +188,13 @@ export default function App() {
onViewChange={setTeamView}
officeAgents={officeAgents}
onAgentArrive={handleAgentArrive}
selectedOfficeObject={selectedOfficeObject}
onAgentSelect={handleOfficeAgentSelect}
onOfficeObjectSelect={handleOfficeObjectSelect}
onFloorSelect={handleOfficeFloorSelect}
onZoneSelect={handleOfficeZoneSelect}
dismissedSpeech={dismissedSpeech}
onDismissSpeech={handleDismissSpeech}
/>
</div>
<div className="console-grid__main">

View File

@@ -37,9 +37,33 @@ function TeamCard({ member }) {
);
}
export default function TeamBoard({ chat, view = "board", onViewChange, officeAgents, onAgentArrive }) {
export default function TeamBoard({
chat,
view = "board",
onViewChange,
officeAgents,
onAgentArrive,
selectedOfficeObject,
onAgentSelect,
onOfficeObjectSelect,
onFloorSelect,
onZoneSelect,
dismissedSpeech,
onDismissSpeech
}) {
const members = parseTeamFeed(chat);
const isOfficeView = view === "office";
const speechByAgent = {
teamLead: members.find((member) => member.name === "Mazlum")?.messages.at(-1)?.text ?? "",
frontend: members.find((member) => member.name === "Berkecan")?.messages.at(-1)?.text ?? "",
backend: members.find((member) => member.name === "Simsar")?.messages.at(-1)?.text ?? "",
uiux: members.find((member) => member.name === "Aybuke")?.messages.at(-1)?.text ?? "",
ios: members.find((member) => member.name === "Ive")?.messages.at(-1)?.text ?? "",
trainee: members.find((member) => member.name === "Irgatov")?.messages.at(-1)?.text ?? ""
};
const speechEntries = Object.fromEntries(
Object.entries(speechByAgent).map(([agentId, text]) => [agentId, { text, key: `${agentId}:${text}` }])
);
return (
<PanelFrame
@@ -73,7 +97,19 @@ export default function TeamBoard({ chat, view = "board", onViewChange, officeAg
{isOfficeView ? (
<div className="team-board__office" aria-label="Office view">
<Suspense fallback={<div className="team-board__office-fallback" />}>
<OfficeCanvas debug={false} agents={officeAgents} onAgentArrive={onAgentArrive} />
<OfficeCanvas
debug={false}
agents={officeAgents}
onAgentArrive={onAgentArrive}
speechByAgent={speechEntries}
selectedOfficeObject={selectedOfficeObject}
onAgentSelect={onAgentSelect}
onOfficeObjectSelect={onOfficeObjectSelect}
onFloorSelect={onFloorSelect}
onZoneSelect={onZoneSelect}
dismissedSpeech={dismissedSpeech}
onDismissSpeech={onDismissSpeech}
/>
</Suspense>
</div>
) : (

View File

@@ -1,5 +1,5 @@
import { useEffect, useRef } from "react";
import { Html, RoundedBox } from "@react-three/drei";
import { Billboard, Html, RoundedBox } from "@react-three/drei";
import { useFrame } from "@react-three/fiber";
import { Vector3 } from "three";
@@ -78,10 +78,24 @@ function HairStyle({ style, color }) {
);
}
export default function OfficeAgent({ agent, onArrive }) {
export default function OfficeAgent({
agent,
onArrive,
selected = false,
speechText = "",
speechKey = "",
onSelect,
onDismissSpeech
}) {
const groupRef = useRef(null);
const bodyRef = useRef(null);
const lastArrivalKeyRef = useRef("");
const targetRef = useRef(new Vector3(...agent.targetPosition));
const leftArmRef = useRef(null);
const rightArmRef = useRef(null);
const leftLegRef = useRef(null);
const rightLegRef = useRef(null);
const walkCycleRef = useRef(0);
const appearance = {
skinColor: "#f0c6a9",
hairColor: "#171717",
@@ -95,7 +109,8 @@ export default function OfficeAgent({ agent, onArrive }) {
};
const isSkirt = appearance.lowerType === "skirt";
const isShorts = appearance.lowerType === "shorts";
const isMoving = !positionsMatch(agent.currentPosition, agent.targetPosition);
const isSeated = !isMoving && String(agent.currentZoneId ?? "").endsWith("Desk");
useEffect(() => {
if (!groupRef.current) {
@@ -114,9 +129,63 @@ export default function OfficeAgent({ agent, onArrive }) {
return;
}
const speed = Math.min(1, delta * 2.4);
const speed = Math.min(1, delta * 1.44);
groupRef.current.position.lerp(targetRef.current, speed);
const directionX = targetRef.current.x - groupRef.current.position.x;
const directionZ = targetRef.current.z - groupRef.current.position.z;
if (bodyRef.current) {
if (isMoving && (Math.abs(directionX) > 0.01 || Math.abs(directionZ) > 0.01)) {
const targetYaw = Math.atan2(directionX, directionZ);
bodyRef.current.rotation.y += (targetYaw - bodyRef.current.rotation.y) * Math.min(1, delta * 8);
} else if (isSeated) {
bodyRef.current.rotation.y += (0 - bodyRef.current.rotation.y) * Math.min(1, delta * 8);
}
}
if (isMoving) {
walkCycleRef.current += delta * 6;
const swing = Math.sin(walkCycleRef.current) * 0.45;
if (leftArmRef.current) {
leftArmRef.current.rotation.x = swing;
}
if (rightArmRef.current) {
rightArmRef.current.rotation.x = -swing;
}
if (leftLegRef.current) {
leftLegRef.current.rotation.x = -swing;
}
if (rightLegRef.current) {
rightLegRef.current.rotation.x = swing;
}
} else if (isSeated) {
if (leftArmRef.current) {
leftArmRef.current.rotation.x = -0.18;
}
if (rightArmRef.current) {
rightArmRef.current.rotation.x = -0.18;
}
if (leftLegRef.current) {
leftLegRef.current.rotation.x = Math.PI / 2.7;
}
if (rightLegRef.current) {
rightLegRef.current.rotation.x = Math.PI / 2.7;
}
} else {
if (leftArmRef.current) {
leftArmRef.current.rotation.x = 0;
}
if (rightArmRef.current) {
rightArmRef.current.rotation.x = 0;
}
if (leftLegRef.current) {
leftLegRef.current.rotation.x = 0;
}
if (rightLegRef.current) {
rightLegRef.current.rotation.x = 0;
}
}
const current = [
groupRef.current.position.x,
groupRef.current.position.y,
@@ -131,111 +200,177 @@ export default function OfficeAgent({ agent, onArrive }) {
});
return (
<group ref={groupRef}>
<group
ref={groupRef}
onPointerDown={(event) => {
event.stopPropagation();
onSelect?.(agent.id);
}}
>
{selected ? (
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.01, 0]}>
<ringGeometry args={[0.36, 0.48, 32]} />
<meshBasicMaterial color="#7ff7ff" transparent opacity={0.8} />
</mesh>
) : null}
<group ref={bodyRef}>
<HairStyle style={appearance.hairStyle} color={appearance.hairColor} />
<mesh castShadow position={[0, 1.55, 0]}>
<mesh castShadow position={[0, isSeated ? 1.47 : 1.55, 0]}>
<boxGeometry args={[0.42, 0.42, 0.42]} />
<meshStandardMaterial color={appearance.skinColor} roughness={0.95} />
</mesh>
<mesh castShadow position={[-0.08, 1.58, 0.222]}>
<mesh castShadow position={[-0.08, isSeated ? 1.5 : 1.58, 0.222]}>
<boxGeometry args={[0.08, 0.012, 0.016]} />
<meshStandardMaterial color="#101010" roughness={0.4} />
</mesh>
<mesh castShadow position={[0.08, 1.58, 0.222]}>
<mesh castShadow position={[0.08, isSeated ? 1.5 : 1.58, 0.222]}>
<boxGeometry args={[0.08, 0.012, 0.016]} />
<meshStandardMaterial color="#101010" roughness={0.4} />
</mesh>
<mesh castShadow position={[-0.08, 1.65, 0.214]}>
<mesh castShadow position={[-0.08, isSeated ? 1.57 : 1.65, 0.214]}>
<boxGeometry args={[0.1, 0.018, 0.02]} />
<meshStandardMaterial color="#222222" roughness={0.7} />
</mesh>
<mesh castShadow position={[0.08, 1.65, 0.214]}>
<mesh castShadow position={[0.08, isSeated ? 1.57 : 1.65, 0.214]}>
<boxGeometry args={[0.1, 0.018, 0.02]} />
<meshStandardMaterial color="#222222" roughness={0.7} />
</mesh>
<mesh castShadow position={[0, 1.5, 0.22]} rotation={[Math.PI / 2.4, 0, 0]}>
<mesh castShadow position={[0, isSeated ? 1.42 : 1.5, 0.22]} rotation={[Math.PI / 2.4, 0, 0]}>
<coneGeometry args={[0.035, 0.13, 4]} />
<meshStandardMaterial color="#deaf90" roughness={0.88} />
</mesh>
<mesh castShadow position={[0, 1.39, 0.222]}>
<mesh castShadow position={[0, isSeated ? 1.31 : 1.39, 0.222]}>
<boxGeometry args={[0.12, 0.012, 0.016]} />
<meshStandardMaterial color="#101010" roughness={0.5} />
</mesh>
<RoundedBox castShadow receiveShadow position={[0, 1.13, 0]} args={[0.62, 0.58, 0.32]} radius={0.04} smoothness={2}>
<RoundedBox castShadow receiveShadow position={[0, isSeated ? 1.02 : 1.13, 0]} args={[0.62, 0.58, 0.32]} radius={0.04} smoothness={2}>
<meshStandardMaterial color={appearance.shirtColor} roughness={0.82} />
</RoundedBox>
{appearance.tieColor ? (
<mesh castShadow position={[0, 1.12, 0.17]}>
<mesh castShadow position={[0, isSeated ? 1.01 : 1.12, 0.17]}>
<boxGeometry args={[0.11, 0.48, 0.03]} />
<meshStandardMaterial color={appearance.tieColor} roughness={0.64} />
</mesh>
) : null}
<mesh castShadow position={[-0.44, 1.15, 0]}>
<boxGeometry args={[0.22, 0.16, 0.18]} />
<meshStandardMaterial color={appearance.shirtColor} roughness={0.82} />
</mesh>
<mesh castShadow position={[0.44, 1.15, 0]}>
<boxGeometry args={[0.22, 0.16, 0.18]} />
<meshStandardMaterial color={appearance.shirtColor} roughness={0.82} />
</mesh>
<mesh castShadow position={[-0.44, 0.93, 0]}>
<boxGeometry args={[0.14, 0.28, 0.14]} />
<meshStandardMaterial color={appearance.skinColor} roughness={0.92} />
</mesh>
<mesh castShadow position={[0.44, 0.93, 0]}>
<boxGeometry args={[0.14, 0.28, 0.14]} />
<meshStandardMaterial color={appearance.skinColor} roughness={0.92} />
</mesh>
<group ref={leftArmRef} position={[-0.44, isSeated ? 1.12 : 1.24, 0]}>
<mesh castShadow position={[0, -0.09, 0]}>
<boxGeometry args={[0.22, 0.16, 0.18]} />
<meshStandardMaterial color={appearance.shirtColor} roughness={0.82} />
</mesh>
<mesh castShadow position={[0, -0.31, 0]}>
<boxGeometry args={[0.14, 0.28, 0.14]} />
<meshStandardMaterial color={appearance.skinColor} roughness={0.92} />
</mesh>
</group>
<group ref={rightArmRef} position={[0.44, isSeated ? 1.12 : 1.24, 0]}>
<mesh castShadow position={[0, -0.09, 0]}>
<boxGeometry args={[0.22, 0.16, 0.18]} />
<meshStandardMaterial color={appearance.shirtColor} roughness={0.82} />
</mesh>
<mesh castShadow position={[0, -0.31, 0]}>
<boxGeometry args={[0.14, 0.28, 0.14]} />
<meshStandardMaterial color={appearance.skinColor} roughness={0.92} />
</mesh>
</group>
{isSkirt ? (
<mesh castShadow position={[0, 0.76, 0]}>
<mesh castShadow position={[0, isSeated ? 0.7 : 0.76, 0]}>
<boxGeometry args={[0.54, 0.22, 0.34]} />
<meshStandardMaterial color={appearance.lowerColor} roughness={0.86} />
</mesh>
) : (
<mesh castShadow position={[0, 0.76, 0]}>
<mesh castShadow position={[0, isSeated ? 0.72 : 0.76, 0]}>
<boxGeometry args={[0.5, 0.22, 0.3]} />
<meshStandardMaterial color={appearance.lowerColor} roughness={0.86} />
</mesh>
)}
<mesh castShadow position={[-0.14, 0.41, 0]}>
<boxGeometry args={[0.14, 0.48, 0.14]} />
<meshStandardMaterial color={appearance.skinColor} roughness={0.9} />
</mesh>
<mesh castShadow position={[0.14, 0.41, 0]}>
<boxGeometry args={[0.14, 0.48, 0.14]} />
<meshStandardMaterial color={appearance.skinColor} roughness={0.9} />
</mesh>
<mesh castShadow position={[-0.14, 0.62, 0]}>
<boxGeometry args={[0.16, 0.08, 0.16]} />
<meshStandardMaterial color={appearance.skinColor} roughness={0.9} />
</mesh>
<mesh castShadow position={[0.14, 0.62, 0]}>
<boxGeometry args={[0.16, 0.08, 0.16]} />
<meshStandardMaterial color={appearance.skinColor} roughness={0.9} />
</mesh>
{isShorts ? (
{isSeated ? (
<>
<mesh castShadow position={[-0.14, 0.58, 0]}>
<boxGeometry args={[0.16, 0.08, 0.16]} />
<meshStandardMaterial color={appearance.skinColor} roughness={0.9} />
</mesh>
<mesh castShadow position={[0.14, 0.58, 0]}>
<boxGeometry args={[0.16, 0.08, 0.16]} />
<meshStandardMaterial color={appearance.skinColor} roughness={0.9} />
</mesh>
<group ref={leftLegRef} position={[-0.14, 0.73, 0.03]}>
<mesh castShadow position={[0, -0.07, 0.11]}>
<boxGeometry args={[0.14, 0.14, 0.34]} />
<meshStandardMaterial color={appearance.lowerColor} roughness={0.88} />
</mesh>
<mesh castShadow position={[0, -0.39, 0.24]}>
<boxGeometry args={[0.14, 0.42, 0.14]} />
<meshStandardMaterial color={isSkirt ? appearance.skinColor : appearance.lowerColor} roughness={0.9} />
</mesh>
<mesh castShadow position={[0, -0.65, 0.32]}>
<boxGeometry args={[0.16, 0.12, 0.24]} />
<meshStandardMaterial color={appearance.shoeColor} roughness={0.9} />
</mesh>
</group>
<group ref={rightLegRef} position={[0.14, 0.73, 0.03]}>
<mesh castShadow position={[0, -0.07, 0.11]}>
<boxGeometry args={[0.14, 0.14, 0.34]} />
<meshStandardMaterial color={appearance.lowerColor} roughness={0.88} />
</mesh>
<mesh castShadow position={[0, -0.39, 0.24]}>
<boxGeometry args={[0.14, 0.42, 0.14]} />
<meshStandardMaterial color={isSkirt ? appearance.skinColor : appearance.lowerColor} roughness={0.9} />
</mesh>
<mesh castShadow position={[0, -0.65, 0.32]}>
<boxGeometry args={[0.16, 0.12, 0.24]} />
<meshStandardMaterial color={appearance.shoeColor} roughness={0.9} />
</mesh>
</group>
</>
) : null}
<mesh castShadow position={[-0.14, 0.11, 0.02]}>
<boxGeometry args={[0.16, 0.12, 0.24]} />
<meshStandardMaterial color={appearance.shoeColor} roughness={0.9} />
</mesh>
<mesh castShadow position={[0.14, 0.11, 0.02]}>
<boxGeometry args={[0.16, 0.12, 0.24]} />
<meshStandardMaterial color={appearance.shoeColor} roughness={0.9} />
</mesh>
<Html position={[0, 2.08, 0]} center distanceFactor={10}>
) : (
<>
<group ref={leftLegRef} position={[-0.14, 0.66, 0]}>
<mesh castShadow position={[0, -0.25, 0]}>
<boxGeometry args={[0.14, 0.48, 0.14]} />
<meshStandardMaterial color={isSkirt ? appearance.skinColor : appearance.lowerColor} roughness={0.9} />
</mesh>
<mesh castShadow position={[0, -0.04, 0]}>
<boxGeometry args={[0.16, 0.08, 0.16]} />
<meshStandardMaterial color={isSkirt ? appearance.skinColor : appearance.lowerColor} roughness={0.9} />
</mesh>
<mesh castShadow position={[0, -0.55, 0.02]}>
<boxGeometry args={[0.16, 0.12, 0.24]} />
<meshStandardMaterial color={appearance.shoeColor} roughness={0.9} />
</mesh>
</group>
<group ref={rightLegRef} position={[0.14, 0.66, 0]}>
<mesh castShadow position={[0, -0.25, 0]}>
<boxGeometry args={[0.14, 0.48, 0.14]} />
<meshStandardMaterial color={isSkirt ? appearance.skinColor : appearance.lowerColor} roughness={0.9} />
</mesh>
<mesh castShadow position={[0, -0.04, 0]}>
<boxGeometry args={[0.16, 0.08, 0.16]} />
<meshStandardMaterial color={isSkirt ? appearance.skinColor : appearance.lowerColor} roughness={0.9} />
</mesh>
<mesh castShadow position={[0, -0.55, 0.02]}>
<boxGeometry args={[0.16, 0.12, 0.24]} />
<meshStandardMaterial color={appearance.shoeColor} roughness={0.9} />
</mesh>
</group>
</>
)}
<Html position={[0, 2.02, 0]} center distanceFactor={10}>
<div className="office-agent__label">{agent.name}</div>
</Html>
</group>
{speechText ? (
<Billboard position={[0, 3.03, 0]} follow lockX={false} lockY={false} lockZ={false}>
<Html center distanceFactor={10}>
<button
type="button"
className="office-agent__bubble"
onWheel={(event) => {
event.preventDefault();
event.stopPropagation();
event.currentTarget.scrollTop += event.deltaY;
}}
onClick={(event) => {
event.stopPropagation();
onDismissSpeech?.(agent.id, speechKey);
}}
>
{speechText}
</button>
</Html>
</Billboard>
) : null}
</group>
);
}

View File

@@ -7,11 +7,9 @@ export default function OfficeCamera() {
enableDamping
dampingFactor={0.08}
minDistance={12}
maxDistance={20}
maxDistance={30}
minPolarAngle={0.7}
maxPolarAngle={1.02}
minAzimuthAngle={-0.85}
maxAzimuthAngle={0.85}
target={[0.5, 0.8, 0.2]}
/>
);

View File

@@ -1,11 +1,35 @@
import { Canvas } from "@react-three/fiber";
import OfficeScene from "./OfficeScene.jsx";
export default function OfficeCanvas({ debug = false, agents = {}, onAgentArrive }) {
export default function OfficeCanvas({
debug = false,
agents = {},
onAgentArrive,
speechByAgent = {},
selectedOfficeObject,
onAgentSelect,
onOfficeObjectSelect,
onFloorSelect,
onZoneSelect,
dismissedSpeech,
onDismissSpeech
}) {
return (
<div className="office-canvas">
<Canvas shadows dpr={[1, 1.5]} camera={{ position: [12, 12, 12], fov: 38 }}>
<OfficeScene debug={debug} agents={agents} onAgentArrive={onAgentArrive} />
<OfficeScene
debug={debug}
agents={agents}
onAgentArrive={onAgentArrive}
speechByAgent={speechByAgent}
selectedOfficeObject={selectedOfficeObject}
onAgentSelect={onAgentSelect}
onOfficeObjectSelect={onOfficeObjectSelect}
onFloorSelect={onFloorSelect}
onZoneSelect={onZoneSelect}
dismissedSpeech={dismissedSpeech}
onDismissSpeech={onDismissSpeech}
/>
</Canvas>
</div>
);

View File

@@ -1,6 +1,14 @@
export default function OfficeFloor() {
export default function OfficeFloor({ onSelect }) {
return (
<mesh receiveShadow rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.001, 0]}>
<mesh
receiveShadow
rotation={[-Math.PI / 2, 0, 0]}
position={[0, -0.001, 0]}
onPointerDown={(event) => {
event.stopPropagation();
onSelect?.(event.point.toArray());
}}
>
<planeGeometry args={[24, 18]} />
<meshStandardMaterial color="#efe0c5" roughness={0.95} metalness={0.02} />
</mesh>

View File

@@ -8,6 +8,7 @@ import { OFFICE_ZONES } from "./officeZones.js";
function AccentWallPanels() {
const steveTexture = useLoader(TextureLoader, "/steve.png");
const monaTexture = useLoader(TextureLoader, "/mona.png");
const ataTexture = useLoader(TextureLoader, "/ata-cropped.png");
const panels = [
{ position: [-8.2, 1.8, -8.78], color: "#495b8a" },
{ position: [0, 2.15, -8.76], color: "#6b4c78" },
@@ -34,12 +35,15 @@ function AccentWallPanels() {
toneMapped={false}
/>
</mesh>
{[panels[2]].map((panel, index) => (
<mesh key={index} position={panel.position}>
<boxGeometry args={[1, 0.55, 0.05]} />
<meshStandardMaterial color={panel.color} roughness={0.55} />
</mesh>
))}
<mesh position={[6.2, 1.47, -8.73]}>
<planeGeometry args={[1.62, 2.184]} />
<meshBasicMaterial
map={ataTexture}
transparent
alphaTest={0.05}
toneMapped={false}
/>
</mesh>
</group>
);
}
@@ -59,18 +63,24 @@ function FloorDecals() {
);
}
export default function OfficeLayout() {
export default function OfficeLayout({ selectedOfficeObject, onOfficeObjectSelect, onZoneSelect }) {
const coffeeMachineSelected = selectedOfficeObject?.type === "object" && selectedOfficeObject?.id === "coffeeMachine";
return (
<group>
<FloorDecals />
<AccentWallPanels />
<Desk position={OFFICE_ZONES[0].position} nameplateColor="#d6c15d" />
<Desk position={OFFICE_ZONES[1].position} nameplateColor="#49c2f1" />
<Desk position={OFFICE_ZONES[2].position} nameplateColor="#7bd87a" />
<Desk position={OFFICE_ZONES[3].position} nameplateColor="#f48cc7" />
<Desk position={OFFICE_ZONES[4].position} nameplateColor="#8aa4ff" />
<Desk position={OFFICE_ZONES[0].position} nameplateColor="#d6c15d" onSelect={() => onZoneSelect?.("teamLeadDesk")} />
<Desk position={OFFICE_ZONES[1].position} nameplateColor="#49c2f1" onSelect={() => onZoneSelect?.("frontendDesk")} />
<Desk position={OFFICE_ZONES[2].position} nameplateColor="#7bd87a" onSelect={() => onZoneSelect?.("backendDesk")} />
<Desk position={OFFICE_ZONES[3].position} nameplateColor="#f48cc7" onSelect={() => onZoneSelect?.("uiuxDesk")} />
<Desk position={OFFICE_ZONES[4].position} nameplateColor="#8aa4ff" onSelect={() => onZoneSelect?.("iosDesk")} />
<MeetingTable position={OFFICE_ZONES[5].position} />
<CoffeeMachine position={OFFICE_ZONES[6].position} />
<CoffeeMachine
position={OFFICE_ZONES[6].position}
selected={coffeeMachineSelected}
onSelect={() => onOfficeObjectSelect?.("object", "coffeeMachine")}
/>
</group>
);
}

View File

@@ -1,11 +1,11 @@
export default function OfficeLighting() {
return (
<>
<ambientLight intensity={1.6} />
<hemisphereLight args={["#fff4de", "#17110d", 1.15]} />
<ambientLight intensity={2.1} />
<hemisphereLight args={["#fff8ea", "#2a2018", 1.45]} />
<directionalLight
castShadow
intensity={1.9}
intensity={2.45}
position={[7, 12, 5]}
shadow-mapSize-width={1024}
shadow-mapSize-height={1024}

View File

@@ -7,17 +7,42 @@ import OfficeDebugLabels from "./OfficeDebugLabels.jsx";
import OfficeAgent from "./OfficeAgent.jsx";
import { OFFICE_ZONES } from "./officeZones.js";
export default function OfficeScene({ debug = false, agents = {}, onAgentArrive }) {
export default function OfficeScene({
debug = false,
agents = {},
onAgentArrive,
speechByAgent = {},
selectedOfficeObject,
onAgentSelect,
onOfficeObjectSelect,
onFloorSelect,
onZoneSelect,
dismissedSpeech = {},
onDismissSpeech
}) {
return (
<>
<color attach="background" args={["#120d0a"]} />
<fog attach="fog" args={["#120d0a", 18, 34]} />
<color attach="background" args={["#1a130f"]} />
<fog attach="fog" args={["#1a130f", 42, 72]} />
<OfficeLighting />
<OfficeFloor />
<OfficeFloor onSelect={onFloorSelect} />
<OfficeWalls />
<OfficeLayout />
<OfficeLayout
selectedOfficeObject={selectedOfficeObject}
onOfficeObjectSelect={onOfficeObjectSelect}
onZoneSelect={onZoneSelect}
/>
{Object.values(agents).map((agent) => (
<OfficeAgent key={agent.id} agent={agent} onArrive={onAgentArrive} />
<OfficeAgent
key={agent.id}
agent={agent}
onArrive={onAgentArrive}
selected={selectedOfficeObject?.type === "agent" && selectedOfficeObject?.id === agent.id}
speechText={dismissedSpeech[agent.id] === speechByAgent[agent.id]?.key ? "" : (speechByAgent[agent.id]?.text ?? "")}
speechKey={speechByAgent[agent.id]?.key ?? ""}
onSelect={onAgentSelect}
onDismissSpeech={onDismissSpeech}
/>
))}
<OfficeCamera />
{debug ? <OfficeDebugLabels zones={OFFICE_ZONES} /> : null}

View File

@@ -15,7 +15,8 @@ function createAgent({
name,
role,
color,
zoneId,
currentZoneId: zoneId,
targetZoneId: zoneId,
currentPosition: zone?.approachPosition ?? [0, 0, 0],
targetPosition: zone?.approachPosition ?? [0, 0, 0],
appearance
@@ -38,7 +39,7 @@ export function createInitialOfficeAgents() {
tieColor: "#ba1d2f",
lowerColor: "#75777d",
shoeColor: "#111111",
lowerType: "shorts"
lowerType: "pants"
}
}),
frontend: createAgent({

View File

@@ -28,15 +28,27 @@ function findMatch(normalizedText, map) {
return Object.entries(map).find(([, aliases]) => aliases.some((alias) => normalizedText.includes(alias)))?.[0] ?? null;
}
function inferZoneFromIntent(normalizedText) {
if (/\b(kahve al|kahve alsin|kahve alsın|kahve getir|kahve makinesi|kahve makinasi|kahve alanina|kahve alanına)\b/.test(normalizedText)) {
return "coffeeMachine";
}
if (/\b(toplantiya git|toplanti masasi|toplantiya|toplanti alanina|toplanti alanina|masaya gec|masaya geç)\b/.test(normalizedText)) {
return "meetingTable";
}
return null;
}
export function parseOfficeCommand(prompt) {
const normalized = normalizeText(prompt);
if (!/\b(git|gidebilir|gitsin|gidin|toplan|yuru|yurusun|gitmesi)\b/.test(normalized)) {
if (!/\b(git|gidebilir|gitsin|gidin|toplan|yuru|yurusun|gitmesi|al|alsin|alsın|getir)\b/.test(normalized)) {
return null;
}
const agentId = findMatch(normalized, AGENT_ALIASES);
const zoneId = findMatch(normalized, ZONE_ALIASES);
const zoneId = findMatch(normalized, ZONE_ALIASES) ?? inferZoneFromIntent(normalized);
if (!agentId || !zoneId) {
return null;

View File

@@ -5,7 +5,7 @@ export const OFFICE_ZONES = [
role: "Team Lead",
type: "desk",
position: [0, 0, -4.2],
approachPosition: [0, 0, -2.9]
approachPosition: [0, 0, -5.25]
},
{
id: "frontendDesk",
@@ -13,7 +13,7 @@ export const OFFICE_ZONES = [
role: "Frontend Dev",
type: "desk",
position: [-3.8, 0, -0.9],
approachPosition: [-2.7, 0, 0.3]
approachPosition: [-3.8, 0, -1.95]
},
{
id: "backendDesk",
@@ -21,7 +21,7 @@ export const OFFICE_ZONES = [
role: "Backend Dev",
type: "desk",
position: [3.8, 0, -0.9],
approachPosition: [2.7, 0, 0.3]
approachPosition: [3.8, 0, -1.95]
},
{
id: "uiuxDesk",
@@ -29,7 +29,7 @@ export const OFFICE_ZONES = [
role: "UI/UX Designer",
type: "desk",
position: [-3.8, 0, 2.9],
approachPosition: [-2.7, 0, 1.8]
approachPosition: [-3.8, 0, 1.85]
},
{
id: "iosDesk",
@@ -37,7 +37,7 @@ export const OFFICE_ZONES = [
role: "iOS Dev",
type: "desk",
position: [3.8, 0, 2.9],
approachPosition: [2.7, 0, 1.8]
approachPosition: [3.8, 0, 1.85]
},
{
id: "meetingTable",

View File

@@ -1,3 +1,5 @@
import { Html } from "@react-three/drei";
function Cup({ position = [0, 0, 0], scale = 1, sleeve = false }) {
return (
<group position={position} scale={scale}>
@@ -57,9 +59,30 @@ function FrontCups() {
);
}
export default function CoffeeMachine({ position = [0, 0, 0] }) {
export default function CoffeeMachine({ position = [0, 0, 0], selected = false, onSelect }) {
return (
<group position={position} scale={0.82}>
<mesh
position={[0, 0.7, 0.12]}
onPointerDown={(event) => {
event.stopPropagation();
onSelect?.();
}}
>
<boxGeometry args={[2.3, 1.7, 1.8]} />
<meshBasicMaterial transparent opacity={0} />
</mesh>
{selected ? (
<>
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.015, 0.12]}>
<ringGeometry args={[1.15, 1.35, 40]} />
<meshBasicMaterial color="#8fe7ff" transparent opacity={0.75} />
</mesh>
<Html position={[0, 2.15, 0.12]} center sprite>
<div className="office-object__label">Kahve Makinesi</div>
</Html>
</>
) : null}
<mesh receiveShadow position={[0, 0.22, 0.12]}>
<boxGeometry args={[2.25, 0.36, 1.65]} />
<meshStandardMaterial color="#6e4024" roughness={0.98} />

View File

@@ -3,11 +3,21 @@ import { TextureLoader } from "three";
import { DoubleSide } from "three";
import Chair from "./Chair.jsx";
export default function Desk({ position = [0, 0, 0], nameplateColor = "#52b6ff" }) {
export default function Desk({ position = [0, 0, 0], nameplateColor = "#52b6ff", onSelect }) {
const appleLogoTexture = useLoader(TextureLoader, "/apple-logo.png");
return (
<group position={position}>
<group
position={position}
onPointerDown={(event) => {
event.stopPropagation();
onSelect?.();
}}
>
<mesh position={[0, 0.72, -0.08]}>
<boxGeometry args={[2.1, 1.5, 2.1]} />
<meshBasicMaterial transparent opacity={0} />
</mesh>
<mesh castShadow receiveShadow position={[0, 0.78, 0]}>
<boxGeometry args={[1.9, 0.16, 1.15]} />
<meshStandardMaterial color="#b78256" roughness={0.86} />
@@ -34,8 +44,8 @@ export default function Desk({ position = [0, 0, 0], nameplateColor = "#52b6ff"
<boxGeometry args={[0.62, 0.03, 0.4]} />
<meshStandardMaterial color="#c8ced6" roughness={0.28} metalness={0.58} />
</mesh>
<mesh position={[0, 0.016, 0.2]} rotation={[-Math.PI / 2, 0, Math.PI]}>
<planeGeometry args={[0.114, 0.114]} />
<mesh position={[0, 0.022, 0.2]} rotation={[-Math.PI / 2, 0, Math.PI]}>
<planeGeometry args={[0.18, 0.18]} />
<meshBasicMaterial
map={appleLogoTexture}
transparent

View File

@@ -237,7 +237,7 @@
.console-grid {
display: grid;
grid-template-columns: minmax(0, 1.55fr) minmax(320px, 0.9fr);
grid-template-columns: minmax(0, 2.15fr) minmax(220px, 0.45fr);
gap: 16px;
align-items: start;
}
@@ -279,6 +279,36 @@
flex-direction: column;
}
.chat-panel .panel-frame__header {
padding: 12px 12px 0;
gap: 10px;
}
.chat-panel .panel-frame__eyebrow {
margin-bottom: 6px;
font-size: 0.48rem;
letter-spacing: 0.14em;
}
.chat-panel .panel-frame__title {
font-size: 0.96rem;
line-height: 1.15;
}
.chat-panel .session-toolbar {
gap: 6px;
}
.chat-panel .pixel-button {
min-width: 94px;
}
.chat-panel .pixel-button span {
padding: 9px 8px;
font-size: 0.44rem;
letter-spacing: 0.09em;
}
.chat-panel .panel-frame__body {
height: 62vh;
min-height: 62vh;
@@ -289,7 +319,7 @@
min-height: 420px;
max-height: 62vh;
overflow: auto;
padding: 16px;
padding: 12px;
border: 2px solid var(--border-mid);
background:
linear-gradient(180deg, rgba(4, 8, 5, 0.88) 0%, rgba(8, 12, 9, 0.92) 100%);
@@ -299,6 +329,13 @@
height: 100%;
min-height: 100%;
max-height: none;
scrollbar-width: none;
-ms-overflow-style: none;
}
.chat-panel .chat-stream::-webkit-scrollbar {
width: 0;
height: 0;
}
.chat-stream pre,
@@ -311,7 +348,8 @@
.chat-stream pre {
color: var(--accent-green);
line-height: 1.55;
font-size: 0.84rem;
line-height: 1.42;
}
.empty-state {
@@ -322,27 +360,29 @@
text-align: center;
color: var(--text-dim);
font-family: var(--font-display);
font-size: 0.62rem;
line-height: 1.9;
font-size: 0.52rem;
line-height: 1.6;
}
.empty-state--small {
font-size: 0.56rem;
font-size: 0.5rem;
}
.prompt-composer {
padding: 14px;
padding: 12px;
}
.prompt-composer textarea {
width: 100%;
min-height: 110px;
min-height: 96px;
resize: vertical;
border: 3px solid var(--border-dark);
box-shadow: inset 0 0 0 2px var(--border-mid);
background: rgba(5, 10, 6, 0.95);
color: var(--text-main);
padding: 14px;
padding: 10px;
font-size: 0.9rem;
line-height: 1.35;
outline: none;
}
@@ -357,7 +397,7 @@
align-items: center;
margin-top: 12px;
color: var(--text-muted);
font-size: 0.78rem;
font-size: 0.68rem;
}
.team-board {
@@ -444,17 +484,63 @@
}
.office-agent__label {
padding: 6px 10px;
padding: 6.61px 13.23px;
border: 1px solid rgba(99, 245, 255, 0.45);
background: rgba(6, 11, 8, 0.92);
color: var(--text-main);
font-family: var(--font-display);
font-size: 0.58rem;
font-size: 0.8745rem;
letter-spacing: 0.1em;
text-transform: uppercase;
white-space: nowrap;
}
.office-agent__bubble,
.office-object__label {
max-width: 504px;
padding: 6px 8px;
border: 1px solid rgba(165, 172, 176, 0.72);
background: rgba(8, 8, 8, 0.94);
color: #f3f6f3;
font-family: var(--font-body);
font-size: 1.314144rem;
line-height: 1.28;
white-space: pre-wrap;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.22);
pointer-events: auto;
cursor: pointer;
}
.office-agent__bubble {
appearance: none;
text-align: left;
width: 504px;
min-height: 155px;
max-height: 155px;
padding: 2px 8px 6px;
display: block;
overflow-y: auto;
overflow-x: hidden;
white-space: pre-wrap;
align-content: start;
scrollbar-width: none;
-ms-overflow-style: none;
}
.office-agent__bubble::-webkit-scrollbar {
width: 0;
height: 0;
}
.office-object__label {
max-width: none;
font-family: var(--font-display);
font-size: 0.58rem;
letter-spacing: 0.08em;
text-transform: uppercase;
white-space: nowrap;
}
.office-scene__svg {
display: block;
width: 100%;