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"
-
-
+
+