feat(agent-office): notification badges + CEO desk document panel + telegram test

- Add notification state management with badge counts in useAgentManager
- Render exclamation badge on agent sprites (separate from status icons)
- Add CEO desk document icon with click-to-open activity panel
- Create DocumentPanel with unified activity feed + per-agent detail tabs
- Add telegram test button to stock agent ChatPanel
- Remove TaskHistory + bottom toolbar (replaced by DocumentPanel)
- Add getActivityFeed API helper

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 15:19:14 +09:00
parent 25715a2198
commit deb285695a
11 changed files with 459 additions and 96 deletions

View File

@@ -6,6 +6,7 @@ export class AgentSprite {
this.waypoints = waypoints;
this.state = 'idle';
this.detail = '';
this.notificationCount = 0;
const deskKey = `${agentId}_desk`;
const desk = waypoints[deskKey] || { x: 5, y: 3 };
@@ -20,6 +21,10 @@ export class AgentSprite {
this._moveSpeed = 0.05;
}
setNotification(count) {
this.notificationCount = count;
}
setState(newState, detail = '') {
this.state = newState;
this.detail = detail;

View File

@@ -1,6 +1,6 @@
import { drawTileMap } from './TileMap';
import { AgentSprite } from './AgentSprite';
import { getCharLabel } from './SpriteSheet';
import { getCharLabel, drawNotificationBadge } from './SpriteSheet';
const STATUS_ICONS = {
idle: null,
@@ -19,6 +19,8 @@ export class OfficeRenderer {
this.agents = {};
this._animId = null;
this._onClick = null;
this._onCeoClick = null;
this._ceoDocBadge = 0;
const agentIds = ['stock', 'music'];
for (const id of agentIds) {
@@ -56,6 +58,21 @@ export class OfficeRenderer {
return id;
}
}
// CEO desk click detection
const ceo = this.mapData.waypoints.ceo_desk;
if (ceo) {
const { scale, offsetX, offsetY, tileSize } = this.renderInfo;
const cx = offsetX + ceo.x * tileSize * scale;
const cy = offsetY + ceo.y * tileSize * scale;
const hitW = 5 * tileSize * scale;
const hitH = 2 * tileSize * scale;
if (canvasX >= cx - tileSize * scale && canvasY >= cy - tileSize * scale &&
canvasX <= cx + hitW && canvasY <= cy + hitH) {
if (this._onCeoClick) this._onCeoClick();
return 'ceo_desk';
}
}
return null;
}
@@ -76,6 +93,19 @@ export class OfficeRenderer {
}
}
setOnCeoClick(handler) {
this._onCeoClick = handler;
}
setCeoDocBadge(count) {
this._ceoDocBadge = count;
}
setAgentNotification(agentId, count) {
const sprite = this.agents[agentId];
if (sprite) sprite.setNotification(count);
}
_loop(timestamp) {
const { ctx, canvas, mapData } = this;
@@ -95,6 +125,9 @@ export class OfficeRenderer {
this._drawOverlay(ctx, sprite, id);
}
// CEO desk document icon
this._drawCeoDoc(ctx);
this._animId = requestAnimationFrame(this._loop);
}
@@ -111,6 +144,11 @@ export class OfficeRenderer {
ctx.fillText(icon, cx, cy - 15 * scale);
}
// Notification badge (separate from status icon)
if (sprite.notificationCount > 0) {
drawNotificationBadge(ctx, cx, cy - 15 * scale, sprite.notificationCount, scale * 1.5);
}
ctx.fillStyle = 'rgba(255,255,255,0.7)';
ctx.font = `${8 * scale}px monospace`;
ctx.textAlign = 'center';
@@ -126,4 +164,48 @@ export class OfficeRenderer {
ctx.fillText(sprite.detail, cx, bubbleY);
}
}
_drawCeoDoc(ctx) {
if (!this.renderInfo) return;
const ceo = this.mapData.waypoints.ceo_desk;
if (!ceo) return;
const { scale, offsetX, offsetY, tileSize } = this.renderInfo;
const dx = offsetX + (ceo.x - 1) * tileSize * scale;
const dy = offsetY + (ceo.y - 1) * tileSize * scale;
const docW = 12 * scale;
const docH = 16 * scale;
// Paper
ctx.fillStyle = '#e8e0d0';
ctx.fillRect(dx, dy, docW, docH);
// Lines on paper
ctx.fillStyle = '#bbb';
for (let i = 0; i < 4; i++) {
ctx.fillRect(dx + 2 * scale, dy + (3 + i * 3) * scale, 8 * scale, 1);
}
// Folded corner
ctx.fillStyle = '#d0c8b8';
ctx.beginPath();
ctx.moveTo(dx + docW - 3 * scale, dy);
ctx.lineTo(dx + docW, dy + 3 * scale);
ctx.lineTo(dx + docW - 3 * scale, dy + 3 * scale);
ctx.fill();
// Badge on document
if (this._ceoDocBadge > 0) {
const bx = dx + docW;
const by = dy;
const r = 4 * scale;
ctx.beginPath();
ctx.arc(bx, by, r, 0, Math.PI * 2);
ctx.fillStyle = '#f43f5e';
ctx.fill();
ctx.fillStyle = '#fff';
ctx.font = `bold ${5 * scale}px monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(this._ceoDocBadge > 9 ? '9+' : String(this._ceoDocBadge), bx, by);
}
}
}

View File

@@ -87,3 +87,25 @@ export function getAnimSpeed(state) {
export function getCharLabel(agentId) {
return (PIXEL_CHARS[agentId] || {}).label || agentId;
}
export function drawNotificationBadge(ctx, x, y, count, scale = 2) {
const s = scale;
const badgeX = x + 5 * s;
const badgeY = y - 8 * s;
const radius = 5 * s;
ctx.beginPath();
ctx.arc(badgeX, badgeY, radius, 0, Math.PI * 2);
ctx.fillStyle = '#f43f5e';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = `bold ${7 * s}px monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('!', badgeX, badgeY);
}