Thinking Orb <mv-thinking-orb>

Canvas AI 助手光球:一个光泽团块,随状态(空闲、聆听、思考时有卫星环绕、说话、成功、错误)扭曲、旋转并改变材质,状态之间平滑过渡。

工作原理

Canvas 绘制的 AI 助手光球:一团有光泽的流体会随状态扭曲、旋转并改变材质(空闲、聆听、带环绕卫星的思考、说话、成功、错误),状态之间平滑过渡。会对声音作出反应:手动设置音量级别、MediaStream,或由用户启用的麦克风。

分类反馈
类型Web Component(<mv-thinking-orb>)
状态稳定版
Keywordsai, assistant, orb, voice, microphone, audio, loader, agent, canvas, status

When to use

  • A voice or AI assistant needs a visual presence that reflects idle, listening, thinking and speaking states
  • The assistant should react to microphone input or an audio stream in real time
  • A single orb doubles as a mic toggle button for a voice interface

Avoid when

  • A lightweight inline thinking indicator is enough, such as in a chat message → use Pixel Loader instead
  • Several instances appear on one page or the target is low-end mobile; each orb runs its own canvas
  • A generic loading state with no assistant persona is needed → use Spinner instead

安装

node scripts/add.mjs thinking-orb --out ./src/marvelous

使用 Marvelous UI MCP 服务器的 AI 智能体:install_components({ slugs: ["thinking-orb"], target_dir: "<absolute path>/src/marvelous", framework: "react" })。

复制的文件(含依赖):tokens/tokens.css、core/base.css、core/canvas.js、core/dom.js、core/element.js、core/motion.js、components/thinking-orb/thinking-orb.js、components/thinking-orb/thinking-orb.css。

用法

标准标记,可在此基础上通过属性、data-* 和 CSS 变量进行定制:

<div style="display:grid;justify-items:center;gap:1.1rem;width:100%;max-width:560px">
  <mv-thinking-orb id="mv-thinking-orb-demo" state="idle" label="Assistant" mic style="--mv-thinking-orb-size:12rem"></mv-thinking-orb>
  <p id="mv-thinking-orb-demo-caption" style="margin:0;min-height:1.4em;font-size:.9rem;color:var(--mv-fg-muted)">Tap the orb to talk, or pick a state.</p>
  <div role="group" aria-label="Assistant state" style="display:flex;flex-wrap:wrap;justify-content:center;gap:.4rem">
    <button class="mv-button" data-size="sm" data-variant="outline" data-state="idle" aria-pressed="true">Idle</button>
    <button class="mv-button" data-size="sm" data-variant="outline" data-state="listening" aria-pressed="false">Listening</button>
    <button class="mv-button" data-size="sm" data-variant="outline" data-state="thinking" aria-pressed="false">Thinking</button>
    <button class="mv-button" data-size="sm" data-variant="outline" data-state="speaking" aria-pressed="false">Speaking</button>
    <button class="mv-button" data-size="sm" data-variant="outline" data-state="success" aria-pressed="false">Success</button>
    <button class="mv-button" data-size="sm" data-variant="outline" data-state="error" aria-pressed="false">Error</button>
  </div>
  <div style="display:flex;gap:1.5rem;align-items:center;margin-top:.4rem">
    <mv-thinking-orb state="thinking" label="Search" style="--mv-thinking-orb-size:3.5rem"></mv-thinking-orb>
    <mv-thinking-orb state="speaking" label="Reading" colors="oklch(0.78 0.16 160), oklch(0.72 0.14 200), oklch(0.85 0.12 110)" style="--mv-thinking-orb-size:3.5rem"></mv-thinking-orb>
    <mv-thinking-orb state="listening" label="Dictation" colors="oklch(0.72 0.19 30), oklch(0.8 0.15 70), oklch(0.66 0.2 350)" style="--mv-thinking-orb-size:3.5rem"></mv-thinking-orb>
  </div>
</div>
<style>
  [aria-label="Assistant state"] .mv-button[aria-pressed="true"] { background: var(--mv-accent-subtle); border-color: var(--mv-accent); color: var(--mv-accent-fg); }
  @media (forced-colors: active) { [aria-label="Assistant state"] .mv-button[aria-pressed="true"] { forced-color-adjust: none; background: Highlight; border-color: Highlight; color: HighlightText; } }
</style>
<script type="module">
  const orb = document.getElementById("mv-thinking-orb-demo");
  const caption = document.getElementById("mv-thinking-orb-demo-caption");
  const texts = {
    idle: "Tap the orb to talk, or pick a state.",
    listening: "I’m listening…",
    thinking: "Looking up flights to Lisbon in October…",
    speaking: "Three nonstop flights on Oct 12, from $89.",
    success: "Booking confirmed.",
    error: "Couldn’t reach the server. Try again.",
  };
  const buttons = [...document.querySelectorAll("[data-state]")].filter((b) => b.closest("[role=group]"));
  const sync = () => {
    for (const b of buttons) b.setAttribute("aria-pressed", String(b.dataset.state === orb.state));
    caption.textContent = texts[orb.state] ?? "";
  };
  for (const b of buttons) b.addEventListener("click", () => { orb.state = b.dataset.state; sync(); });
  orb.addEventListener("mv-mic", (e) => {
    if (e.detail.error) caption.textContent = "Microphone unavailable (" + e.detail.error + ").";
    else sync();
  });
</script>

API

Attributes

Name类型DefaultDescription
stateidle | listening | thinking | speaking | success | erroridleDisplayed state; parameters (size, warp, swirl, glow, satellites, hue) are smoothly interpolated.
labelstringAssistantName announced with the state (“Assistant: thinking”).
colorslist of CSS colors--mv-thinking-orb-1/2/3Three material colors (tokens accepted, follow the theme).
levelnumber 0-1Manual voice level (listening / speaking). With no level or mic, “speaking” simulates a voice.
micbooleanThe orb becomes a toggle button that opens the microphone on click (getUserMedia) and switches to listening.
speednumber1Overall animation speed.

Properties

Name类型Description
levelnumber | nullVoice level 0-1 (update every frame if needed, without an attribute).
streamMediaStream | nullExternal audio stream to visualize (not stopped by the orb).
listeningMicbooleanTrue while the orb’s microphone is open.

Methods

NameDescription
listenMic()Opens the microphone (call it from a user gesture); returns a Promise<MediaStream>.
stopMic()Stops the orb’s microphone.

Events

NameDescription
mv-micdetail: { active, stream?, error? }; microphone opened, closed or denied.

CSS variables

NameDefaultDescription
--mv-thinking-orb-size10remSize (the glow and satellites fit inside the box).
--mv-thinking-orb-1 / -2 / -3accent, sky blue, pinkDefault material colors.

Accessibility

Decorative canvas (aria-hidden); the state is exposed as visually hidden text in a polite live region (“Assistant: listening”). With mic: toggle button with a constant name and aria-pressed, visible focus. The microphone is never opened without a user gesture. Reduced motion: a representative still frame for each state. Pauses when off-screen.

本页面由 AI 翻译。报告翻译问题