feat(memory): P1 长期记忆升级 —— 异步攒批 Consolidate + 软删 + importance/last_seen #1
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@@ -1,6 +1,7 @@
|
|||||||
import { useMemo, type CSSProperties } from "react";
|
import { useMemo, type CSSProperties } from "react";
|
||||||
import { deriveTeam, teamNow, type RunPhase, type TeamSeat } from "../lib/run";
|
import { deriveTeam, teamNow, type RunPhase, type TeamSeat } from "../lib/run";
|
||||||
import type { ExecEvent } from "../lib/api";
|
import type { ExecEvent } from "../lib/api";
|
||||||
|
import roomSprite from "../assets/office/room.webp";
|
||||||
import leadSprite from "../assets/office/lead.webp";
|
import leadSprite from "../assets/office/lead.webp";
|
||||||
import deskSprite from "../assets/office/desk.webp";
|
import deskSprite from "../assets/office/desk.webp";
|
||||||
import a0 from "../assets/office/a0.webp";
|
import a0 from "../assets/office/a0.webp";
|
||||||
@@ -22,17 +23,15 @@ const ACTORS = [a0, a1, a2, a3, a4];
|
|||||||
// 复盘时按事件时间戳算出各自的出场/交付时刻,压进 PLAY_MS 内当一出戏放。
|
// 复盘时按事件时间戳算出各自的出场/交付时刻,压进 PLAY_MS 内当一出戏放。
|
||||||
// 动效=状态编码,不是装饰;并守 prefers-reduced-motion。
|
// 动效=状态编码,不是装饰;并守 prefers-reduced-motion。
|
||||||
|
|
||||||
const VB_W = 720;
|
// 场景尺寸 = 房间贴图的比例(2760×1504 → 1.835),让背景零拉伸:
|
||||||
const VB_H = 510;
|
// 拉伸 30% 的话墙上那个挂钟会变成椭圆,一眼假。宁可改场景去迁就图。
|
||||||
const FLOOR_Y = 200;
|
const VB_W = 940;
|
||||||
|
const VB_H = 512;
|
||||||
|
const FLOOR_Y = 242; // 贴图里量的地平线在 47.3%
|
||||||
|
|
||||||
const OUTLINE = "#2f2a26";
|
const OUTLINE = "#2f2a26";
|
||||||
const SKIN = "#f7d9bd";
|
const SKIN = "#f7d9bd";
|
||||||
const PANTS = "#4a5568";
|
const PANTS = "#4a5568";
|
||||||
const WALL = "#f3ede4";
|
|
||||||
const FLOOR = "#ddd0be";
|
|
||||||
const DESK_TOP = "#ffffff";
|
|
||||||
const DESK_FRONT = "#f1ebe2";
|
|
||||||
|
|
||||||
const RUN = "#7c5cf6";
|
const RUN = "#7c5cf6";
|
||||||
const OK = "#34d399";
|
const OK = "#34d399";
|
||||||
@@ -43,16 +42,16 @@ const SHIRTS = ["#7c5cf6", "#38bdf8", "#34d399", "#fbbf24", "#fb7185", "#a78bfa"
|
|||||||
const HAIRS = ["#3f3229", "#6b4a2f", "#c8913f", "#2e3f52", "#8c4a4a", "#4a3a6b", "#5c5148", "#7a4a2a"];
|
const HAIRS = ["#3f3229", "#6b4a2f", "#c8913f", "#2e3f52", "#8c4a4a", "#4a3a6b", "#5c5148", "#7a4a2a"];
|
||||||
|
|
||||||
// 门口:出场点。角色从这里走向自己的工位。
|
// 门口:出场点。角色从这里走向自己的工位。
|
||||||
const DOOR_X = 84;
|
const DOOR_X = 279; // 贴图里那扇门的中心(29.7%)——角色从画上的真门里走出来
|
||||||
const DOOR_Y = 198;
|
const DOOR_Y = 250;
|
||||||
const DOOR_SCALE = 0.5;
|
const DOOR_SCALE = 0.5;
|
||||||
|
|
||||||
// 协调者机位;递简报时专家走到它桌子两侧。
|
// 协调者机位;递简报时专家走到它桌子两侧。
|
||||||
const LEAD_X = 360;
|
const LEAD_X = 470;
|
||||||
const LEAD_Y = 232;
|
const LEAD_Y = 268; // 下限 242(地平线,再高就飘到墙上)、上限 276(再低撞到工位气泡)
|
||||||
const LEAD_SCALE = 0.62;
|
const LEAD_SCALE = 0.62;
|
||||||
const HANDOFF_DX = 86;
|
const HANDOFF_DX = 86;
|
||||||
const HANDOFF_Y = 250;
|
const HANDOFF_Y = 288;
|
||||||
const HANDOFF_SCALE = 0.66;
|
const HANDOFF_SCALE = 0.66;
|
||||||
|
|
||||||
const ENTER_MS = 950;
|
const ENTER_MS = 950;
|
||||||
@@ -271,10 +270,10 @@ function Deskette({ name, sub, tone, glow, w = 200 }: { name: string; sub: strin
|
|||||||
掉到挡板外会骑在桌子下边线上、被切成半截。 */}
|
掉到挡板外会骑在桌子下边线上、被切成半截。 */}
|
||||||
<rect x={-hw + 18} y={-24} width={w - 36} height={23} rx={4} fill="#fff" stroke={OUTLINE} strokeWidth={2.2} />
|
<rect x={-hw + 18} y={-24} width={w - 36} height={23} rx={4} fill="#fff" stroke={OUTLINE} strokeWidth={2.2} />
|
||||||
<circle cx={-hw + 31} cy={-12.5} r={4.2} fill={tone} />
|
<circle cx={-hw + 31} cy={-12.5} r={4.2} fill={tone} />
|
||||||
<text x={-hw + 42} y={-7} fontSize={16} fontWeight={600} fill="#2f2a26" style={{ fontFamily: "system-ui, sans-serif" }}>
|
<text x={-hw + 42} y={-7} fontSize={20} fontWeight={600} fill="#2f2a26" style={{ fontFamily: "system-ui, sans-serif" }}>
|
||||||
{fitText(name, (w - 62) / 16)}
|
{fitText(name, (w - 62) / 20)}
|
||||||
</text>
|
</text>
|
||||||
<text x={0} y={16} fontSize={13} textAnchor="middle" fill="#6b6156" style={{ fontFamily: "system-ui, sans-serif" }}>
|
<text x={0} y={16} fontSize={16} textAnchor="middle" fill="#6b6156" style={{ fontFamily: "system-ui, sans-serif" }}>
|
||||||
{sub}
|
{sub}
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
@@ -376,40 +375,10 @@ function Actor({ cue, reduced, hasLead }: { cue: Cue; reduced: boolean; hasLead:
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 场景道具
|
// 场景道具:整间房是一张贴图(AI 生成),门/窗/挂钟/绿植都在图里。
|
||||||
|
// 原来这里是手绘 SVG 的墙地门窗,被立绘一衬托就露怯,整段换掉。
|
||||||
function Room() {
|
function Room() {
|
||||||
return (
|
return <image href={roomSprite} x={0} y={0} width={VB_W} height={VB_H} preserveAspectRatio="xMidYMid slice" />;
|
||||||
<g>
|
|
||||||
<rect x={0} y={0} width={VB_W} height={FLOOR_Y} fill={WALL} />
|
|
||||||
<rect x={0} y={FLOOR_Y} width={VB_W} height={VB_H - FLOOR_Y} fill={FLOOR} />
|
|
||||||
<rect x={0} y={FLOOR_Y - 5} width={VB_W} height={5} fill="#cabfae" />
|
|
||||||
{/* 地毯 */}
|
|
||||||
<ellipse cx={360} cy={402} rx={318} ry={48} fill="#e9dfd0" stroke="#cfc2ae" strokeWidth={2} />
|
|
||||||
{/* 门:专家从这里进场 */}
|
|
||||||
<g>
|
|
||||||
<rect x={44} y={86} width={80} height={114} rx={4} fill="#e4d9c6" stroke={OUTLINE} strokeWidth={2.8} />
|
|
||||||
<rect x={52} y={94} width={64} height={98} rx={2} fill="#efe6d6" stroke={OUTLINE} strokeWidth={2} />
|
|
||||||
<circle cx={110} cy={146} r={3.6} fill={OUTLINE} />
|
|
||||||
</g>
|
|
||||||
{/* 挂钟 */}
|
|
||||||
<g transform="translate(534, 58)">
|
|
||||||
<circle r={22} fill="#fff" stroke={OUTLINE} strokeWidth={2.6} />
|
|
||||||
<path d="M 0 0 L 0 -13 M 0 0 L 9 5" stroke={OUTLINE} strokeWidth={2.4} strokeLinecap="round" />
|
|
||||||
</g>
|
|
||||||
{/* 窗 */}
|
|
||||||
<g>
|
|
||||||
<rect x={578} y={34} width={116} height={92} rx={5} fill="#cfe9f5" stroke={OUTLINE} strokeWidth={2.6} />
|
|
||||||
<path d="M 636 34 L 636 126 M 578 80 L 694 80" stroke={OUTLINE} strokeWidth={2.2} />
|
|
||||||
<path d="M 588 120 L 626 42 L 642 42 L 604 120 Z" fill="#fff" opacity={0.35} />
|
|
||||||
</g>
|
|
||||||
{/* 绿植 */}
|
|
||||||
<g transform="translate(636, 186)">
|
|
||||||
<path d="M 0 -6 C -22 -14 -26 -44 -4 -52 C -12 -30 -4 -18 0 -6 Z" fill="#4a9c6d" stroke={OUTLINE} strokeWidth={2.2} strokeLinejoin="round" />
|
|
||||||
<path d="M 0 -6 C 22 -18 24 -50 2 -58 C 12 -32 4 -18 0 -6 Z" fill="#5cb07d" stroke={OUTLINE} strokeWidth={2.2} strokeLinejoin="round" />
|
|
||||||
<path d="M -13 -6 L 13 -6 L 9 16 L -9 16 Z" fill="#e6e0d6" stroke={OUTLINE} strokeWidth={2.4} strokeLinejoin="round" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OfficeView({ events, output, phase }: { events: ExecEvent[]; output?: string; phase?: RunPhase }) {
|
export function OfficeView({ events, output, phase }: { events: ExecEvent[]; output?: string; phase?: RunPhase }) {
|
||||||
@@ -426,7 +395,7 @@ export function OfficeView({ events, output, phase }: { events: ExecEvent[]; out
|
|||||||
// 排位:≤5 人一排(前排),否则拆两排,后排小、前排大 —— 用缩放造纵深。
|
// 排位:≤5 人一排(前排),否则拆两排,后排小、前排大 —— 用缩放造纵深。
|
||||||
// 两排时后排不出头顶气泡:气泡很高,会顶到协调者的桌子;状态仍由显示器辉光和铭牌圆点表达。
|
// 两排时后排不出头顶气泡:气泡很高,会顶到协调者的桌子;状态仍由显示器辉光和铭牌圆点表达。
|
||||||
const rows: TeamSeat[][] = seats.length <= 5 ? [seats] : [seats.slice(0, Math.ceil(seats.length / 2)), seats.slice(Math.ceil(seats.length / 2))];
|
const rows: TeamSeat[][] = seats.length <= 5 ? [seats] : [seats.slice(0, Math.ceil(seats.length / 2)), seats.slice(Math.ceil(seats.length / 2))];
|
||||||
const rowCfg = rows.length === 1 ? [{ y: 452, s: 1 }] : [{ y: 330, s: 0.58 }, { y: 460, s: 0.8 }];
|
const rowCfg = rows.length === 1 ? [{ y: 470, s: 1 }] : [{ y: 358, s: 0.62 }, { y: 478, s: 0.85 }];
|
||||||
|
|
||||||
// 档期:直播时事件到哪演到哪(延迟一律 0);复盘时按时间戳排,整段压进 PLAY_MS。
|
// 档期:直播时事件到哪演到哪(延迟一律 0);复盘时按时间戳排,整段压进 PLAY_MS。
|
||||||
const rate = streaming ? 0 : Math.min(1, PLAY_MS / span);
|
const rate = streaming ? 0 : Math.min(1, PLAY_MS / span);
|
||||||
@@ -524,11 +493,11 @@ export function OfficeView({ events, output, phase }: { events: ExecEvent[]; out
|
|||||||
|
|
||||||
{/* 白板:协调者的思考/综合过程 */}
|
{/* 白板:协调者的思考/综合过程 */}
|
||||||
<g>
|
<g>
|
||||||
<rect x={176} y={18} width={320} height={94} rx={6} fill="#fff" stroke={OUTLINE} strokeWidth={2.8} />
|
<rect x={347} y={22} width={266} height={104} rx={6} fill="#fff" stroke={OUTLINE} strokeWidth={2.8} />
|
||||||
<rect x={176} y={104} width={320} height={8} rx={3} fill="#e2dbcf" stroke={OUTLINE} strokeWidth={2.2} />
|
<rect x={347} y={118} width={266} height={8} rx={3} fill="#e2dbcf" stroke={OUTLINE} strokeWidth={2.2} />
|
||||||
<foreignObject x={188} y={26} width={296} height={74}>
|
<foreignObject x={357} y={30} width={246} height={86}>
|
||||||
<div style={{ fontFamily: "system-ui, sans-serif", color: "#4a4238", lineHeight: 1.35 }}>
|
<div style={{ fontFamily: "system-ui, sans-serif", color: "#4a4238", lineHeight: 1.35 }}>
|
||||||
<div style={{ fontSize: 12, color: leadTone, fontWeight: 600 }}>
|
<div style={{ fontSize: 15, color: leadTone, fontWeight: 600 }}>
|
||||||
{lead
|
{lead
|
||||||
? streaming
|
? streaming
|
||||||
? "综合中…"
|
? "综合中…"
|
||||||
@@ -543,7 +512,7 @@ export function OfficeView({ events, output, phase }: { events: ExecEvent[]; out
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: 13,
|
fontSize: 16,
|
||||||
marginTop: 3,
|
marginTop: 3,
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
display: "-webkit-box",
|
display: "-webkit-box",
|
||||||
|
|||||||
Reference in New Issue
Block a user