Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ba55af3
test(v21): freeze immersive relationship universe
laiqian0239-glitch Aug 16, 2026
a07b74b
feat(v21): add immersive relationship universe
laiqian0239-glitch Aug 16, 2026
f4b5279
merge(v21): continue relationship universe after scope amendment
laiqian0239-glitch Aug 16, 2026
e65a390
test(v21): migrate relationship universe consumer contracts
laiqian0239-glitch Aug 16, 2026
8505975
merge(v21): continue relationship universe under amendment-2 authority
laiqian0239-glitch Aug 16, 2026
97cbcc7
test(v21): cover dense universe layout and direct dependency sections
laiqian0239-glitch Aug 16, 2026
500695c
fix(v21): keep dense relationship universe nodes collision-free
laiqian0239-glitch Aug 16, 2026
f621021
test(v21): require collision-free dense relationship controls
laiqian0239-glitch Aug 16, 2026
46dd767
fix(v21): bound dense relationship constellation geometry
laiqian0239-glitch Aug 16, 2026
0d53158
test(v21): assert dense control behavior without CSS ownership coupling
laiqian0239-glitch Aug 16, 2026
c1948d8
fix(v21): compact dense relationship universe controls
laiqian0239-glitch Aug 16, 2026
c52f0f7
fix(v21): keep universe geometry executable in contract test
laiqian0239-glitch Aug 16, 2026
3b49262
test(v21): require border-box dense relationship hit targets
laiqian0239-glitch Aug 16, 2026
6c59b3f
fix(v21): make dense relationship targets collision-safe
laiqian0239-glitch Aug 16, 2026
10a574c
test(v21): bind Chinese action labels to exact kinds
laiqian0239-glitch Aug 16, 2026
7f42916
test(v21): require compact dense layout at eight relationships
laiqian0239-glitch Aug 16, 2026
4909cbd
test(v21): localize unknown translation lifecycle states
laiqian0239-glitch Aug 16, 2026
47b94ed
fix(v21): compact eight-node relationship universe
laiqian0239-glitch Aug 16, 2026
0eee06c
fix(v21): localize translation lifecycle status chrome
laiqian0239-glitch Aug 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

248 changes: 235 additions & 13 deletions integration/element-module/src/product-experience/PeopleSurface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,114 @@ import React from "react";
import { motion } from "motion/react";
import type { RelationshipProjection } from "./experienceTypes";

export type PeopleHomeView = "list" | "universe";

type PeopleSurfaceProps = {
relationships: readonly RelationshipProjection[];
selectedRelationshipId: string;
focusedRelationshipId: string;
viewMode: PeopleHomeView;
reducedMotion: boolean;
onViewModeChange: (view: PeopleHomeView) => void;
onFocus: (relationshipId: string) => void;
onSelect: (relationshipId: string) => void;
};

type UniversePosition = {
x: number;
y: number;
ring: number;
};

function initials(name: string): string {
const parts = name.trim().split(/\s+/u).filter(Boolean);
return (parts.length > 1 ? `${parts[0][0]}${parts.at(-1)?.[0] || ""}` : parts[0]?.slice(0, 2) || "Y").toUpperCase();
}

function universePosition(index: number, count: number): UniversePosition {
const boundedCount = Math.max(1, count);
if (boundedCount <= 8) {
const angle = ((index / boundedCount) * Math.PI * 2) - (Math.PI / 2);
const radius = 30;
return {
x: 50 + Math.cos(angle) * radius,
y: 50 + Math.sin(angle) * radius,
ring: 0,
};
}

const capacities = [8, 12, 16];
const radii = [18, 31, 44];
const ring = index < capacities[0] ? 0 : index < capacities[0] + capacities[1] ? 1 : 2;
const ringStart = ring === 0 ? 0 : ring === 1 ? capacities[0] : capacities[0] + capacities[1];
const ringCount = Math.min(capacities[ring], Math.max(1, boundedCount - ringStart));
const slot = Math.min(index - ringStart, ringCount - 1);
const phase = -(Math.PI / 2) + (ring % 2 === 1 ? Math.PI / ringCount : 0);
const angle = phase + ((slot / ringCount) * Math.PI * 2);
const radius = radii[ring];
return {
x: 50 + Math.cos(angle) * radius,
y: 50 + Math.sin(angle) * radius,
ring,
};
}

export function PeopleSurface({
relationships,
selectedRelationshipId,
focusedRelationshipId,
viewMode,
reducedMotion,
onViewModeChange,
onFocus,
onSelect,
}: PeopleSurfaceProps): React.JSX.Element {
const focusedRelationship = relationships.find((row) => row.id === focusedRelationshipId) || null;
const focusedIntelligence = focusedRelationship?.relationshipIntelligence;
const latestEvidence = focusedIntelligence && focusedIntelligence.events.length
? focusedIntelligence.events[focusedIntelligence.events.length - 1]
: null;
const denseUniverse = relationships.length >= 8;
const universeRelationships = relationships.slice(0, 36);

return (
<section className="yance-people" aria-label="People">
<header className="yance-section-heading">
<section className="yance-people" aria-label="我的关系">
<header className="yance-section-heading yance-people-heading">
<div>
<span className="yance-eyebrow">People</span>
<h2>Your relationships</h2>
<span className="yance-eyebrow">关系</span>
<h2>我的关系</h2>
</div>
<span className="yance-count" aria-label={`${relationships.length} relationships`}>{relationships.length}</span>
<span className="yance-count" aria-label={`${relationships.length} 段关系`}>{relationships.length}</span>
</header>

{relationships.length ? (
<div className="yance-people-list" role="list" aria-label="Relationship list">
<div className="yance-people-view-switch" aria-label="关系视图">
<button
type="button"
aria-pressed={viewMode === "list"}
onClick={() => onViewModeChange("list")}
>
列表
</button>
<button
type="button"
aria-pressed={viewMode === "universe"}
onClick={() => onViewModeChange("universe")}
>
关系宇宙
</button>
</div>

{!relationships.length ? (
<div className="yance-empty" role="status">
<strong>暂无关系</strong>
<span>已有联系人和会话会在这里形成你的关系空间。</span>
</div>
) : viewMode === "list" ? (
<div className="yance-people-list" role="list" aria-label="关系列表">
{relationships.map((relationship) => {
const selected = relationship.id === selectedRelationshipId;
const analysisStatusLabel = relationship.relationshipIntelligence?.analysisStatusLabel
|| "No confirmed relationship intelligence";
|| "暂无已确认的关系智能";
return (
<motion.button
layout={!reducedMotion}
Expand All @@ -46,7 +120,7 @@ export function PeopleSurface({
data-selected={selected || undefined}
data-intelligence-state={relationship.relationshipIntelligence?.state || "unavailable"}
aria-pressed={selected}
aria-label={`Open relationship with ${relationship.name}. ${analysisStatusLabel}`}
aria-label={`打开与 ${relationship.name} 的关系。${analysisStatusLabel}`}
onClick={() => onSelect(relationship.id)}
whileTap={reducedMotion ? undefined : { scale: 0.985 }}
transition={{ type: "spring", stiffness: 480, damping: 36 }}
Expand All @@ -65,10 +139,158 @@ export function PeopleSurface({
})}
</div>
) : (
<div className="yance-empty" role="status">
<strong>No relationships yet</strong>
<span>People will appear here from your existing Yance customer data.</span>
</div>
<section className="yance-relationship-universe" aria-labelledby="yance-relationship-universe-title">
<div className="yance-relationship-universe__canvas">
<header className="yance-relationship-universe__heading">
<div>
<span className="yance-eyebrow">沉浸视图</span>
<h3 id="yance-relationship-universe-title">关系宇宙</h3>
</div>
<p>从你出发,看见每段关系正在发生什么</p>
</header>

<div
className="yance-relationship-universe__stage"
data-dense={denseUniverse || undefined}
aria-label="关系宇宙"
>
<svg
className="yance-relationship-universe__spokes"
viewBox="0 0 100 100"
preserveAspectRatio="none"
aria-hidden="true"
>
{universeRelationships.map((relationship, index) => {
const position = universePosition(index, universeRelationships.length);
return (
<line
key={`spoke-${relationship.id}`}
className="yance-relationship-universe__spoke"
x1="50"
y1="50"
x2={position.x}
y2={position.y}
/>
);
})}
</svg>

<div className="yance-relationship-universe__center" aria-hidden="true">
<span>我</span>
</div>

{universeRelationships.map((relationship, index) => {
const position = universePosition(index, universeRelationships.length);
const focused = relationship.id === focusedRelationshipId;
const selected = relationship.id === selectedRelationshipId;
const analysisStatusLabel = relationship.relationshipIntelligence?.analysisStatusLabel
|| "暂无已确认的关系智能";
return (
<motion.button
key={relationship.id}
type="button"
className="yance-relationship-universe__node"
data-focused={focused || undefined}
data-selected={selected || undefined}
data-ring={position.ring}
data-intelligence-state={relationship.relationshipIntelligence?.state || "unavailable"}
style={{
left: `${position.x}%`,
top: `${position.y}%`,
...(denseUniverse ? {
width: "44px",
height: "44px",
minHeight: "44px",
padding: "3px",
borderRadius: "50%",
gridTemplateColumns: "1fr",
boxSizing: "border-box",
} : {}),
}}
aria-pressed={focused}
aria-label={`查看 ${relationship.name} 的关系洞察。${analysisStatusLabel}`}
onClick={() => onFocus(relationship.id)}
whileTap={reducedMotion ? undefined : { scale: 0.97 }}
transition={{ duration: reducedMotion ? 0 : 0.16 }}
>
<span className="yance-relationship-universe__node-avatar" aria-hidden="true">
{relationship.avatarUrl ? <img src={relationship.avatarUrl} alt="" /> : initials(relationship.name)}
</span>
{denseUniverse ? null : (
<span className="yance-relationship-universe__node-copy">
<strong>{relationship.name}</strong>
<span>{analysisStatusLabel}</span>
</span>
)}
</motion.button>
);
})}
</div>
{relationships.length > universeRelationships.length ? (
<p className="yance-relationship-universe__overflow-note" role="status">
关系宇宙一次显示 36 段关系;切换“列表”可查看全部 {relationships.length} 段。
</p>
) : null}
</div>

<aside className="yance-relationship-universe__insight" aria-label="可信关系洞察">
{focusedRelationship ? (
<>
<header>
<span className="yance-eyebrow">可信关系洞察</span>
<h3>{focusedRelationship.name}</h3>
<p>{focusedRelationship.subtitle}</p>
</header>
<dl className="yance-relationship-universe__facts">
<div>
<dt>关系状态</dt>
<dd>{focusedIntelligence?.analysisStatusLabel || "暂无已确认的关系智能"}</dd>
</div>
{focusedIntelligence?.stage ? (
<div>
<dt>阶段</dt>
<dd>{focusedIntelligence.stage}</dd>
</div>
) : null}
{focusedIntelligence?.summary ? (
<div>
<dt>关系摘要</dt>
<dd>{focusedIntelligence.summary}</dd>
</div>
) : null}
{focusedIntelligence?.next ? (
<div>
<dt>下一步</dt>
<dd>{focusedIntelligence.next}</dd>
</div>
) : null}
{latestEvidence ? (
<div>
<dt>最近证据</dt>
<dd>{latestEvidence.title}{latestEvidence.sourceLabel ? ` · ${latestEvidence.sourceLabel}` : ""}</dd>
</div>
) : null}
</dl>
{!focusedIntelligence ? (
<p className="yance-relationship-universe__pending">暂无已确认的关系智能;这里不会根据本地行为猜测关系含义。</p>
) : null}
<button
type="button"
className="yance-relationship-universe__enter"
onClick={() => onSelect(focusedRelationship.id)}
>
进入关系世界
</button>
</>
) : (
<div className="yance-relationship-universe__prompt" role="status">
<span className="yance-eyebrow">可信关系洞察</span>
<strong>选择一个人,查看可信关系洞察</strong>
<p>位置只用于空间编排,不代表亲密度、重要性或关系强弱。</p>
</div>
)}
</aside>
</section>
)}
</section>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ type ProductComposerAccessoryProps = {
};

const ACTIONS: readonly Readonly<{ label: string; kind: RelationshipOverlayKind; hint: string }>[] = [
{ label: "Photo", kind: "photo", hint: "Immich library and ComfyUI" },
{ label: "Voice", kind: "voice", hint: "Voice Brain" },
{ label: "Live", kind: "live", hint: "LiveKit and CyberVerse" },
{ label: "Attachment", kind: "attachment", hint: "Existing media authority" },
{ label: "照片", kind: "photo", hint: "照片库与智能编辑" },
{ label: "语音", kind: "voice", hint: "语音能力" },
{ label: "实时陪伴", kind: "live", hint: "实时空间" },
{ label: "附件", kind: "attachment", hint: "媒体与文件" },
];

export function ProductComposerAccessory({ roomId }: ProductComposerAccessoryProps): React.JSX.Element {
Expand All @@ -35,15 +35,15 @@ export function ProductComposerAccessory({ roomId }: ProductComposerAccessoryPro
};

return (
<div className="yance-action-dock" aria-label="Relationship actions" data-room-id={roomId}>
<div className="yance-action-dock" aria-label="关系操作" data-room-id={roomId}>
<Popover.Root>
<Popover.Trigger className="yance-action-trigger" aria-label="Open Photo Voice Live and Attachment actions">
<Popover.Trigger className="yance-action-trigger" aria-label="打开照片、语音、实时陪伴和附件工具">
<span aria-hidden="true">+</span>
<span>Relationship tools</span>
<span>关系工具</span>
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner sideOffset={8} className="yance-action-positioner">
<Popover.Popup className="yance-action-popover" aria-label="Relationship action dock">
<Popover.Popup className="yance-action-popover" aria-label="关系工具面板">
<div className="yance-action-grid">
{ACTIONS.map((action) => (
<Popover.Close
Expand Down
Loading
Loading