diff --git a/web/public/ata-cropped.png b/web/public/ata-cropped.png new file mode 100644 index 0000000..c2fe551 Binary files /dev/null and b/web/public/ata-cropped.png differ diff --git a/web/src/App.jsx b/web/src/App.jsx index 1a0fef9..afe8174 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -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} />
diff --git a/web/src/components/TeamBoard.jsx b/web/src/components/TeamBoard.jsx index 6e84601..f89a469 100644 --- a/web/src/components/TeamBoard.jsx +++ b/web/src/components/TeamBoard.jsx @@ -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 ( }> - +
) : ( diff --git a/web/src/office/OfficeAgent.jsx b/web/src/office/OfficeAgent.jsx index d1b58c1..0e98e9d 100644 --- a/web/src/office/OfficeAgent.jsx +++ b/web/src/office/OfficeAgent.jsx @@ -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 ( - + { + event.stopPropagation(); + onSelect?.(agent.id); + }} + > + {selected ? ( + + + + + ) : null} + - + - + - + - + - + - + - + - + {appearance.tieColor ? ( - + ) : null} - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + {isSkirt ? ( - + ) : ( - + )} - - - - - - - - - - - - - - - - - {isShorts ? ( + {isSeated ? ( <> - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - ) : null} - - - - - - - - - + ) : ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )} +
{agent.name}
+
+ {speechText ? ( + + + + + + ) : null}
); } diff --git a/web/src/office/OfficeCamera.jsx b/web/src/office/OfficeCamera.jsx index 45da287..0d91eea 100644 --- a/web/src/office/OfficeCamera.jsx +++ b/web/src/office/OfficeCamera.jsx @@ -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]} /> ); diff --git a/web/src/office/OfficeCanvas.jsx b/web/src/office/OfficeCanvas.jsx index c7bb34d..f23f0cd 100644 --- a/web/src/office/OfficeCanvas.jsx +++ b/web/src/office/OfficeCanvas.jsx @@ -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 (
- +
); diff --git a/web/src/office/OfficeFloor.jsx b/web/src/office/OfficeFloor.jsx index 24250cb..023fafe 100644 --- a/web/src/office/OfficeFloor.jsx +++ b/web/src/office/OfficeFloor.jsx @@ -1,6 +1,14 @@ -export default function OfficeFloor() { +export default function OfficeFloor({ onSelect }) { return ( - + { + event.stopPropagation(); + onSelect?.(event.point.toArray()); + }} + > diff --git a/web/src/office/OfficeLayout.jsx b/web/src/office/OfficeLayout.jsx index 1da0726..383a245 100644 --- a/web/src/office/OfficeLayout.jsx +++ b/web/src/office/OfficeLayout.jsx @@ -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} /> - {[panels[2]].map((panel, index) => ( - - - - - ))} + + + +
); } @@ -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 ( - - - - - + onZoneSelect?.("teamLeadDesk")} /> + onZoneSelect?.("frontendDesk")} /> + onZoneSelect?.("backendDesk")} /> + onZoneSelect?.("uiuxDesk")} /> + onZoneSelect?.("iosDesk")} /> - + onOfficeObjectSelect?.("object", "coffeeMachine")} + /> ); } diff --git a/web/src/office/OfficeLighting.jsx b/web/src/office/OfficeLighting.jsx index 2d8cbb5..0c3062a 100644 --- a/web/src/office/OfficeLighting.jsx +++ b/web/src/office/OfficeLighting.jsx @@ -1,11 +1,11 @@ export default function OfficeLighting() { return ( <> - - + + - - + + - + - + {Object.values(agents).map((agent) => ( - + ))} {debug ? : null} diff --git a/web/src/office/officeAgents.js b/web/src/office/officeAgents.js index f1b7975..e346537 100644 --- a/web/src/office/officeAgents.js +++ b/web/src/office/officeAgents.js @@ -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({ diff --git a/web/src/office/officeCommands.js b/web/src/office/officeCommands.js index 091a55a..ca0b700 100644 --- a/web/src/office/officeCommands.js +++ b/web/src/office/officeCommands.js @@ -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; diff --git a/web/src/office/officeZones.js b/web/src/office/officeZones.js index e70c170..a47941c 100644 --- a/web/src/office/officeZones.js +++ b/web/src/office/officeZones.js @@ -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", diff --git a/web/src/office/primitives/CoffeeMachine.jsx b/web/src/office/primitives/CoffeeMachine.jsx index fce5256..211e23d 100644 --- a/web/src/office/primitives/CoffeeMachine.jsx +++ b/web/src/office/primitives/CoffeeMachine.jsx @@ -1,3 +1,5 @@ +import { Html } from "@react-three/drei"; + function Cup({ position = [0, 0, 0], scale = 1, sleeve = false }) { return ( @@ -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 ( + { + event.stopPropagation(); + onSelect?.(); + }} + > + + + + {selected ? ( + <> + + + + + +
Kahve Makinesi
+ + + ) : null} diff --git a/web/src/office/primitives/Desk.jsx b/web/src/office/primitives/Desk.jsx index 0798158..38ecc38 100644 --- a/web/src/office/primitives/Desk.jsx +++ b/web/src/office/primitives/Desk.jsx @@ -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 ( - + { + event.stopPropagation(); + onSelect?.(); + }} + > + + + + @@ -34,8 +44,8 @@ export default function Desk({ position = [0, 0, 0], nameplateColor = "#52b6ff" - - + +