Thinking Orb <mv-thinking-orb>
Canvas AI 助手光球:一个光泽团块,随状态(空闲、聆听、思考时有卫星环绕、说话、成功、错误)扭曲、旋转并改变材质,状态之间平滑过渡。
工作原理
Canvas 绘制的 AI 助手光球:一团有光泽的流体会随状态扭曲、旋转并改变材质(空闲、聆听、带环绕卫星的思考、说话、成功、错误),状态之间平滑过渡。会对声音作出反应:手动设置音量级别、MediaStream,或由用户启用的麦克风。
| 分类 | 反馈 |
|---|---|
| 类型 | Web Component(<mv-thinking-orb>) |
| 状态 | 稳定版 |
| Keywords | ai, 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 | 类型 | Default | Description |
|---|---|---|---|
state | idle | listening | thinking | speaking | success | error | idle | Displayed state; parameters (size, warp, swirl, glow, satellites, hue) are smoothly interpolated. |
label | string | Assistant | Name announced with the state (“Assistant: thinking”). |
colors | list of CSS colors | --mv-thinking-orb-1/2/3 | Three material colors (tokens accepted, follow the theme). |
level | number 0-1 | Manual voice level (listening / speaking). With no level or mic, “speaking” simulates a voice. | |
mic | boolean | The orb becomes a toggle button that opens the microphone on click (getUserMedia) and switches to listening. | |
speed | number | 1 | Overall animation speed. |
Properties
| Name | 类型 | Description |
|---|---|---|
level | number | null | Voice level 0-1 (update every frame if needed, without an attribute). |
stream | MediaStream | null | External audio stream to visualize (not stopped by the orb). |
listeningMic | boolean | True while the orb’s microphone is open. |
Methods
| Name | Description |
|---|---|
listenMic() | Opens the microphone (call it from a user gesture); returns a Promise<MediaStream>. |
stopMic() | Stops the orb’s microphone. |
Events
| Name | Description |
|---|---|
mv-mic | detail: { active, stream?, error? }; microphone opened, closed or denied. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-thinking-orb-size | 10rem | Size (the glow and satellites fit inside the box). |
--mv-thinking-orb-1 / -2 / -3 | accent, sky blue, pink | Default 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.