feat: polish life os card stack
This commit is contained in:
parent
22b4a2be4f
commit
980dfb9e0e
13 changed files with 692 additions and 151 deletions
|
|
@ -8,6 +8,7 @@ const EXECUTABLE_SCRIPT_TYPES = new Set([
|
|||
"application/javascript",
|
||||
"module",
|
||||
]);
|
||||
const CARD_LIVE_CONTENT_EVENT = "nanobot:card-live-content-change";
|
||||
const CARD_SELECTION_EVENT = "nanobot:card-selection-change";
|
||||
const cardLiveContentStore = new Map<string, JsonValue>();
|
||||
const cardRefreshHandlers = new Map<string, () => void>();
|
||||
|
|
@ -74,6 +75,10 @@ function resolveCardRoot(target: HTMLScriptElement | HTMLElement | null): HTMLEl
|
|||
return target.closest("[data-nanobot-card-root]");
|
||||
}
|
||||
|
||||
function dispatchCardLiveContentChange(cardId: string): void {
|
||||
window.dispatchEvent(new CustomEvent(CARD_LIVE_CONTENT_EVENT, { detail: { cardId } }));
|
||||
}
|
||||
|
||||
function setCardLiveContent(
|
||||
target: HTMLScriptElement | HTMLElement | null,
|
||||
snapshot: JsonValue | null | undefined,
|
||||
|
|
@ -84,9 +89,11 @@ function setCardLiveContent(
|
|||
const cloned = cloneJsonValue(snapshot ?? undefined);
|
||||
if (cloned === undefined) {
|
||||
cardLiveContentStore.delete(cardId);
|
||||
dispatchCardLiveContentChange(cardId);
|
||||
return;
|
||||
}
|
||||
cardLiveContentStore.set(cardId, cloned);
|
||||
dispatchCardLiveContentChange(cardId);
|
||||
}
|
||||
|
||||
function getCardLiveContent(cardId: string | null | undefined): JsonValue | undefined {
|
||||
|
|
@ -437,6 +444,22 @@ const LANE_TITLES: Record<CardLane, string> = {
|
|||
history: "History",
|
||||
};
|
||||
const LANE_ORDER: CardLane[] = ["attention", "work", "context", "history"];
|
||||
const LANE_RANK: Record<CardLane, number> = {
|
||||
attention: 0,
|
||||
work: 1,
|
||||
context: 2,
|
||||
history: 3,
|
||||
};
|
||||
|
||||
function readCardScore(card: CardItem): number {
|
||||
if (!card.serverId) return card.priority;
|
||||
const liveContent = window.__nanobotGetCardLiveContent?.(card.serverId);
|
||||
if (!liveContent || typeof liveContent !== "object" || Array.isArray(liveContent)) {
|
||||
return card.priority;
|
||||
}
|
||||
const score = (liveContent as Record<string, JsonValue>).score;
|
||||
return typeof score === "number" && Number.isFinite(score) ? score : card.priority;
|
||||
}
|
||||
|
||||
interface CardProps {
|
||||
card: CardItem;
|
||||
|
|
@ -690,7 +713,13 @@ export function CardFeed({ cards, viewActive, onDismiss, onChoice, onAskCard }:
|
|||
lane,
|
||||
title: LANE_TITLES[lane],
|
||||
cards: cards.filter((card) => card.lane === lane),
|
||||
})).filter((group) => group.cards.length > 0),
|
||||
orderScore: Math.max(...cards.filter((card) => card.lane === lane).map(readCardScore)),
|
||||
}))
|
||||
.filter((group) => group.cards.length > 0)
|
||||
.sort((left, right) => {
|
||||
if (left.orderScore !== right.orderScore) return right.orderScore - left.orderScore;
|
||||
return LANE_RANK[left.lane] - LANE_RANK[right.lane];
|
||||
}),
|
||||
[cards],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import type {
|
|||
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL ?? "";
|
||||
const WEBRTC_STUN_URL = import.meta.env.VITE_WEBRTC_STUN_URL?.trim() ?? "";
|
||||
const LOCAL_ICE_GATHER_TIMEOUT_MS = 350;
|
||||
const CARD_LIVE_CONTENT_EVENT = "nanobot:card-live-content-change";
|
||||
|
||||
let cardIdCounter = 0;
|
||||
let logIdCounter = 0;
|
||||
|
|
@ -80,11 +81,23 @@ interface RTCCallbacks {
|
|||
closePC: () => void;
|
||||
}
|
||||
|
||||
function readCardScore(card: Pick<CardItem, "priority" | "serverId">): number {
|
||||
if (!card.serverId) return card.priority;
|
||||
const liveContent = window.__nanobotGetCardLiveContent?.(card.serverId);
|
||||
if (!liveContent || typeof liveContent !== "object" || Array.isArray(liveContent)) {
|
||||
return card.priority;
|
||||
}
|
||||
const score = (liveContent as Record<string, unknown>).score;
|
||||
return typeof score === "number" && Number.isFinite(score) ? score : card.priority;
|
||||
}
|
||||
|
||||
function compareCards(a: CardItem, b: CardItem): number {
|
||||
const laneDiff = LANE_RANK[a.lane] - LANE_RANK[b.lane];
|
||||
if (laneDiff !== 0) return laneDiff;
|
||||
const stateDiff = STATE_RANK[a.state] - STATE_RANK[b.state];
|
||||
if (stateDiff !== 0) return stateDiff;
|
||||
const scoreDiff = readCardScore(b) - readCardScore(a);
|
||||
if (scoreDiff !== 0) return scoreDiff;
|
||||
const laneDiff = LANE_RANK[a.lane] - LANE_RANK[b.lane];
|
||||
if (laneDiff !== 0) return laneDiff;
|
||||
if (a.priority !== b.priority) return b.priority - a.priority;
|
||||
const updatedDiff = b.updatedAt.localeCompare(a.updatedAt);
|
||||
if (updatedDiff !== 0) return updatedDiff;
|
||||
|
|
@ -182,7 +195,7 @@ function handleTypedMessage(
|
|||
): void {
|
||||
if (msg.type === "agent_state") {
|
||||
idleFallback.clear();
|
||||
setAgentState((prev) => (prev === "listening" ? prev : msg.state));
|
||||
setAgentState(() => msg.state);
|
||||
return;
|
||||
}
|
||||
if (msg.type === "message") {
|
||||
|
|
@ -519,6 +532,26 @@ function useCardsState() {
|
|||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onCardLiveContentChange = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<{ cardId?: unknown }>;
|
||||
const cardId =
|
||||
customEvent.detail && typeof customEvent.detail.cardId === "string"
|
||||
? customEvent.detail.cardId
|
||||
: "";
|
||||
setCards((prev) => {
|
||||
if (prev.length === 0) return prev;
|
||||
if (cardId && !prev.some((card) => card.serverId === cardId)) return prev;
|
||||
return sortCards([...prev]);
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener(CARD_LIVE_CONTENT_EVENT, onCardLiveContentChange);
|
||||
return () => {
|
||||
window.removeEventListener(CARD_LIVE_CONTENT_EVENT, onCardLiveContentChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { cards, upsertCard, dismissCard, loadPersistedCards };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -638,7 +638,8 @@ body {
|
|||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 200, 140, 0.25) transparent;
|
||||
background: #ffffff;
|
||||
background: #ece8e1;
|
||||
box-shadow: inset 0 1px 0 rgba(52, 40, 31, 0.24);
|
||||
}
|
||||
#card-feed::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
|
|
@ -688,7 +689,7 @@ body {
|
|||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 0 8px;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
.card.dismissing {
|
||||
|
|
@ -878,12 +879,9 @@ body {
|
|||
color: inherit;
|
||||
}
|
||||
.card.kind-text .card-body > [data-nanobot-card-root] > :not(script) {
|
||||
border-radius: 16px;
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 6px 14px rgba(0, 0, 0, 0.1),
|
||||
0 14px 26px rgba(0, 0, 0, 0.25),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.25);
|
||||
box-shadow: none;
|
||||
}
|
||||
.card-question {
|
||||
color: rgba(255, 245, 235, 0.95);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue