Exclusivobeta
Context Rings <mv-context-rings>
Hace visible, y editable, lo que un asistente de IA “sabe” para la tarea actual: tres anillos concéntricos (“This task” en el centro, “Always” para preferencias y reglas, “Ambient” para flujos: correo electrónico, calendario, chat) más una bandeja “Out of context”. Cada elemento es un chip (icono de la fuente, tamaño según los tokens que consume) repartido sin colisiones alrededor de su anillo; arrástralo a otro anillo con un resorte o muévelo desde el teclado. Un indicador fino alrededor del anillo exterior compara los tokens con el presupuesto: cuando se supera, los elementos ambient no fijados de menor prioridad se atenúan con una etiqueta “will be ignored”, en orden de desalojo. Los nuevos elementos ambient entran en órbita; una vista de lista toma el relevo en pantallas pequeñas.
| Categoría | Visualización de datos |
|---|---|
| Tipo | Web Component (<mv-context-rings>) |
| Estado | beta |
| Kit | IA que puedes verificar |
| Keywords | exclusive, ai, assistant, agent, context, context-window, memory, tokens, budget, gauge, drag-and-drop, transparency, user-control, rings, orbit |
When to use
- Users must see and edit which files, rules and streams an AI assistant uses for the current task
- A token budget should show which low-priority context will be ignored when over the limit
- Context items must be moved between task, always-on and ambient scopes, or excluded
Avoid when
- The goal is to capture the outcome and constraints of a request as the user types → use Intent Composer instead
- A plain token usage indicator is enough, without per-item control → use Progress instead
- The context holds hundreds of items; rings get crowded and fall back to a list anyway
Instalación
node scripts/add.mjs context-rings --out ./src/marvelousAgente de IA con el servidor MCP de Marvelous UI: install_components({ slugs: ["context-rings"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Archivos copiados (dependencias incluidas): tokens/tokens.css, core/base.css, core/dismiss.js, core/dom.js, core/element.js, core/focus.js, core/motion.js, core/observe.js, core/pointer.js, core/position.js, components/context-rings/context-rings.js, components/context-rings/context-rings.css.
Uso
Inicio rápido, el marcado mínimo que funciona:
<mv-context-rings budget="8000" task="Reply to Dana about the late delivery">
<script type="application/json">[
{ "id": "mail", "ring": "local", "label": "Latest email from Dana", "source": "email", "tokens": 1800 },
{ "id": "tone", "ring": "global", "label": "Tone: warm, concise", "source": "style", "tokens": 120, "pinned": true },
{ "id": "thread", "ring": "ambient", "label": "#support channel", "source": "slack", "tokens": 7400 }
]</script>
</mv-context-rings>Marcado de referencia: parte de él y personalízalo con atributos, data-* y variables CSS:
<div id="cr-demo" style="width:min(100%,70rem);margin-inline:auto">
<style>
#cr-demo .cr-bar { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:.75rem 1rem; margin:0 0 .75rem }
#cr-demo .cr-bar p { margin:0; color:var(--mv-fg-muted); font-size:.8125rem; max-width:40rem }
#cr-demo .cr-controls { display:flex; align-items:center; flex-wrap:wrap; gap:.5rem }
#cr-demo .cr-controls label { display:inline-flex; align-items:center; gap:.5rem; color:var(--mv-fg-muted); font-size:.8125rem }
#cr-demo .cr-controls .mv-select { width:auto; min-width:7.5rem }
#cr-demo .cr-log { display:flex; align-items:center; flex-wrap:wrap; gap:.5rem; margin:.75rem 0 0; color:var(--mv-fg-muted); font-size:.75rem; font-variant-numeric:tabular-nums }
#cr-demo .cr-log code { font:500 .7rem/1 var(--mv-font-mono); padding:.2rem .4rem; border-radius:var(--mv-radius-sm); background:var(--mv-bg-muted); color:var(--mv-fg) }
</style>
<div class="cr-bar">
<p>Drag a chip to another ring, or to “Out of context”. Keyboard: <kbd class="mv-kbd">Enter</kbd> opens the card, <kbd class="mv-kbd">1</kbd> <kbd class="mv-kbd">2</kbd> <kbd class="mv-kbd">3</kbd> move it, <kbd class="mv-kbd">P</kbd> pins it.</p>
<div class="cr-controls">
<label>Budget
<select class="mv-select" id="cr-budget">
<option value="24000">24k tokens</option>
<option value="32000" selected>32k tokens</option>
<option value="64000">64k tokens</option>
</select>
</label>
<button class="mv-button" data-variant="outline" data-size="sm" type="button" id="cr-stream">Stream a new ambient item</button>
</div>
</div>
<mv-context-rings id="cr-rings" budget="32000" task="Reply to Harper & Lane about the late delivery"></mv-context-rings>
<p class="cr-log"><code>mv-context-change</code><span id="cr-log">Context ready to send.</span></p>
<script type="module">
const rings = document.getElementById("cr-rings");
const min = (m) => Date.now() - m * 60000;
rings.items = [
// This task
{ id: "mail-last", ring: "local", label: "Latest email from Dana Harper", source: "email", tokens: 1800, updated: min(38), excerpt: "Hi, we still haven't received the 40 Arlo tables ordered on September 2. Can you give us a firm delivery date? Without an answer by Friday we'll have to cancel." },
{ id: "contract", ring: "local", label: "Contract 2025.pdf", source: "pdf", sourceLabel: "PDF · Drive", tokens: 9600, updated: min(60 * 24 * 41), excerpt: "Section 7 (Lead times): for delays over 15 business days, the customer is entitled to a 5% discount on the affected order." },
{ id: "ticket", ring: "local", label: "Ticket #4821 · late order", source: "ticket", tokens: 2400, updated: min(95), pinned: true, excerpt: "Order HL-2291 held at the Denver warehouse: pallets waiting for the carrier (pickup slot 9/24)." },
{ id: "crm", ring: "local", label: "Account history", source: "crm", tokens: 1600, updated: min(60 * 5), excerpt: "Customer since 2019 · 6 orders · 1 dispute closed in 2023 (partial delivery)." },
// Always
{ id: "tone", ring: "global", label: "Tone: warm, concise", source: "style", tokens: 120, pinned: true, priority: 2, excerpt: "Short sentences, no internal jargon, always offer a next step." },
{ id: "signature", ring: "global", label: "Signature", source: "profile", tokens: 180, priority: 2, excerpt: "Sam Okafor · Customer Care, Nomad Workshop · +1 (415) 555-0142" },
{ id: "glossary", ring: "global", label: "Product glossary", source: "glossary", tokens: 3200, updated: min(60 * 24 * 6), excerpt: "Arlo: folding oak table · Mist: stackable chair · HL-: prefix for Harper & Lane orders." },
{ id: "refunds", ring: "global", label: "Refund policy", source: "doc", tokens: 1900, updated: min(60 * 24 * 12), excerpt: "Goodwill credit up to 8% without manager approval." },
// Ambient
{ id: "thread", ring: "ambient", label: "Harper & Lane thread", source: "email", tokens: 5400, priority: 2, pinned: true, updated: min(40), excerpt: "12 messages since September 2, including 3 follow-ups from the customer." },
{ id: "agenda", ring: "ambient", label: "This week's calendar", source: "calendar", tokens: 1200, updated: min(15), excerpt: "Thursday 10:00 AM: logistics sync with the Denver warehouse." },
{ id: "support", ring: "ambient", label: "#support channel", source: "slack", sourceLabel: "Slack · #support", tokens: 4600, priority: 0, updated: min(60 * 3), excerpt: "Leah: “The carrier confirms a Tuesday pickup for HL-2291.”" },
{ id: "recent", ring: "ambient", label: "Recent files", source: "file", tokens: 2600, priority: 0, updated: min(60 * 26), excerpt: "Inventory_Sept.xlsx, Delivery_schedule.pdf, Quote_Mist.docx" },
{ id: "notes", ring: "ambient", label: "Monday meeting notes", source: "note", tokens: 1800, updated: min(60 * 30), excerpt: "Carrier delays rising; warn B2B accounts over $20,000." },
// Out of context
{ id: "random", ring: "excluded", label: "#random channel", source: "slack", sourceLabel: "Slack · #random", tokens: 4100, priority: 0, updated: min(20) },
{ id: "personal", ring: "excluded", label: "Personal calendar", source: "calendar", tokens: 900, updated: min(60 * 2) },
];
const queue = [
{ id: "reply", label: "Reply from Dana Harper", source: "email", tokens: 900, priority: 2, excerpt: "“Thanks. A Tuesday delivery works for us if you confirm it in writing.”" },
{ id: "invite", label: "Invite: Harper & Lane sync", source: "calendar", tokens: 400, priority: 1, excerpt: "Thursday 2:30 PM · video call · organizer: Leah Martin" },
{ id: "leah", label: "Message from Leah", source: "slack", sourceLabel: "Slack · #support", tokens: 700, priority: 1, excerpt: "“Pickup confirmed Tuesday 8 AM, sending you the bill of lading.”" },
{ id: "invoice", label: "Invoice INV-2291.pdf", source: "pdf", tokens: 1500, priority: 0, excerpt: "Invoice HL-2291 · 40 × Arlo table · $12,960.00" },
];
const stream = document.getElementById("cr-stream");
stream.addEventListener("click", () => {
const next = queue.shift();
if (next) rings.add({ ...next, ring: "ambient", updated: Date.now() });
if (!queue.length) { stream.disabled = true; stream.textContent = "No more new items"; }
});
document.getElementById("cr-budget").addEventListener("change", (e) => { rings.budget = Number(e.target.value); });
const log = document.getElementById("cr-log");
const k = (n) => `${(n / 1000).toLocaleString("en-US", { maximumFractionDigits: 1 })}k`;
rings.addEventListener("mv-context-change", (e) => {
const { used, budget, evicted } = e.detail;
log.textContent = `${k(used)} / ${k(budget)} tokens · ${evicted.length ? `${evicted.length} ignored (${evicted.join(", ")})` : "nothing ignored"}`;
});
</script>
</div>API
Attributes
| Name | Tipo | Default | Description |
|---|---|---|---|
budget | number (tokens) | 32000 | The assistant's token budget. When exceeded, unpinned ambient items are marked “will be ignored” (lowest priority first, then oldest, then heaviest) until the total fits again. |
task | string | Task title shown as the heading (“Reply to Harper & Lane about the late delivery”). Also names the region. | |
view | rings | list | (auto) | Forces a view. Unset: rings, unless the component is narrower than list-below or too crowded for readable chips (list). The “List view” button writes this attribute. |
list-below | number (px) | 600 | Component width under which the automatic view switches to the list (ResizeObserver). |
data-view / data-budget | rings | list · ok | near | over | Reflected by the component (effective view, budget state), usable from CSS. |
Properties
| Name | Tipo | Description |
|---|---|---|
items | { id, label, ring, tokens, source, sourceLabel?, pinned?, priority?, excerpt?, updated? }[] | The model. ring: "local" | "global" | "ambient" | "excluded". source: email, calendar, chat, slack, file, pdf, profile, style, preference, doc, glossary, ticket, note, crm, history, web, memory (icon and label), or free text. priority: 0 low, 1 normal (default), 2 high. updated: Date, timestamp or ISO string. Reading returns copies. Can be set before the element is defined; a child <script type="application/json"> works too. |
used | number | Tokens sent (rings, not the tray), before eviction. Read-only. |
evicted | string[] | Ids that will be ignored, in eviction order. Read-only. |
Methods
| Name | Description |
|---|---|
add(item, { animate? }) | Adds an item (or updates the one with the same id). An ambient item orbits in from outside the rings with a faint pulse. |
move(id, ring) | Moves an item to a ring or to "excluded". Returns false if it was already there. |
pin(id, pinned?) | Pins (or unpins, or toggles): a pinned item is never dropped automatically. |
remove(id) | Removes an item entirely. |
Events
| Name | Description |
|---|---|
mv-context-change | After every change (drag and drop, keyboard, methods, budget). detail: { items, used, budget, evicted } (evicted = ignored ids, in order). Not fired when items is assigned. |
Content structure
| Name | Description |
|---|---|
script[type="application/json"] | Optional: array of items read on mount when items was not provided. |
CSS classes
| Name | Description |
|---|---|
mv-context-rings-chip / -chip-body | Chip (button): position on the outer element, visuals and scale on the body. data-ring, data-weight (0-3, cost), data-pinned, data-evicted, data-fresh. |
mv-context-rings-band / -tab | SVG ellipses of the rings (data-ring) and the name tabs on their top edge. |
mv-context-rings-gauge-track / -fill / -over / -tick | Budget gauge: track, share within budget, overflow, budget mark. |
mv-context-rings-tray | “Out of context” tray (drop target). |
mv-context-rings-list / -section / -row | List view: four labelled sections, one row per item. |
mv-context-rings-pop | Detail card (role=dialog, top layer): source, excerpt, cost, last update, actions. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-context-rings-accent | var(--mv-accent) | Accent color: center ring, drop targets, gauge. |
--mv-context-rings-warning | var(--mv-warning) | Over-budget color (always paired with an icon and a label). |
--mv-context-rings-local / -global / -ambient | Fill of each ring. | |
--mv-context-rings-bg | var(--mv-surface) | Component background. |
Accessibility
A region named by the task title. Each ring (and the tray) is a role="group" labelled with its item count and cost; chips are real buttons with roving focus (arrows, Home/End; Tab moves between rings), in clockwise order. Full alternative to drag and drop: Enter opens a card (role=dialog, focus trapped, Escape returns focus) with “Move to…”, “Pin” (aria-pressed) and “Exclude”; direct shortcuts 1, 2, 3 (rings), Delete or 0 (exclude), P (pin), exposed through aria-keyshortcuts and an aria-describedby hint. Each chip's accessible name gives source, cost, pinned state and eviction rank. A live region announces moves and budget state changes; the budget is also a role="meter" with aria-valuetext. Ring identity never relies on color: name tabs on the rings, solid vs dashed strokes; the over-budget state pairs an icon with text. “List view” (aria-pressed) presents the same model as four labelled lists, automatically below list-below. Reduced motion (prefers-reduced-motion or data-motion="reduce"): no FLIP, orbit or pulse. No animation loop: everything is event-driven (ResizeObserver, Web Animations).