익스클루시브

Context Rings <mv-context-rings>

현재 작업에서 AI 어시스턴트가 “알고 있는” 내용을 보이게 하고 편집할 수 있게 합니다. 세 개의 동심원(가운데 “This task”, 선호와 규칙을 담는 “Always”, 이메일, 캘린더, 채팅 같은 스트림을 담는 “Ambient”)과 “Out of context” 트레이로 구성됩니다.

작동 방식

AI 어시스턴트가 현재 작업에 대해 “알고 있는” 것을 보이게 하고 편집할 수 있게 합니다: 세 개의 동심원 링(중심의 “This task”, 선호와 규칙을 위한 “Always”, 이메일, 캘린더, 채팅 같은 스트림을 위한 “Ambient”)과 “Out of context” 트레이로 구성됩니다. 각 항목은 칩(출처 아이콘, 토큰 비용에 따른 크기)으로 표시되어 링 주위에 겹치지 않게 배치됩니다. 스프링 효과와 함께 다른 링으로 드래그하거나 키보드로 옮길 수 있습니다. 바깥 링을 감싸는 가는 게이지가 토큰 수를 예산과 비교하며, 예산을 넘으면 고정되지 않은 우선순위가 가장 낮은 ambient 항목부터 제외 순서대로 “will be ignored” 레이블과 함께 흐려집니다. 새 ambient 항목은 궤도를 그리며 들어오고, 작은 화면에서는 목록 보기로 전환됩니다.

카테고리데이터 표시
유형Web Component (<mv-context-rings>)
상태안정
키트검증할 수 있는 AI
Keywordsexclusive, 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

설치

node scripts/add.mjs context-rings --out ./src/marvelous

Marvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["context-rings"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

복사되는 파일(의존성 포함): 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.

사용법

빠른 시작, 동작하는 가장 작은 마크업:

<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": "chat", "tokens": 7400 }
  ]</script>
</mv-context-rings>

기본 마크업입니다. 여기서 시작해 속성, data-*, 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 &amp; 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 · Shared 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, Fernpeak 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: "chat", sourceLabel: "Team chat · #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: "chat", sourceLabel: "Team chat · #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: "chat", sourceLabel: "Team chat · #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유형DefaultDescription
budgetnumber (tokens)32000The 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.
taskstringTask title shown as the heading (“Reply to Harper & Lane about the late delivery”). Also names the region.
viewrings | 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-belownumber (px)600Component width under which the automatic view switches to the list (ResizeObserver).
data-view / data-budgetrings | list · ok | near | overReflected by the component (effective view, budget state), usable from CSS.

Properties

Name유형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.
usednumberTokens sent (rings, not the tray), before eviction. Read-only.
evictedstring[]Ids that will be ignored, in eviction order. Read-only.

Methods

NameDescription
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.
removeItem(id)Removes an item entirely.

Events

NameDescription
mv-context-changeAfter 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

NameDescription
script[type="application/json"]Optional: array of items read on mount when items was not provided.

CSS classes

NameDescription
mv-context-rings-chip / -chip-bodyChip (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 / -tabSVG ellipses of the rings (data-ring) and the name tabs on their top edge.
mv-context-rings-gauge-track / -fill / -over / -tickBudget gauge: track, share within budget, overflow, budget mark.
mv-context-rings-tray“Out of context” tray (drop target).
mv-context-rings-list / -section / -rowList view: four labelled sections, one row per item.
mv-context-rings-popDetail card (role=dialog, top layer): source, excerpt, cost, last update, actions.

CSS variables

NameDefaultDescription
--mv-context-rings-accentvar(--mv-accent)Accent color: center ring, drop targets, gauge.
--mv-context-rings-warningvar(--mv-warning)Over-budget color (always paired with an icon and a label).
--mv-context-rings-local / -global / -ambientFill of each ring.
--mv-context-rings-bgvar(--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). Known limits: an item is kept or ignored as a whole (no partial truncation of a long document); dragging a chip on touch starts right away and blocks page scrolling while the finger is on the chip (no long press; the card and list view stay available); the rings are ellipses rather than circles so wide chips fit, which is a deliberate visual choice.

이 페이지는 AI로 번역되었습니다. 번역 문제 신고