About
A voice orb drawn in monospace characters. Glyph density follows the light and the voice, with eight conversation states and live audio levels.
Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds ASCII Orb to your project and fits it to your theme.
Code
How ASCII Orb is used: the demo's code. The component's own source comes with a free account.
Usage.tsx
"use client"
import * as React from "react"
import { AsciiOrb, VoiceOrbState, createChatcnSpeechLevel, voiceOrbStateLabels } from "@/components/ui/ascii-orb"
/** A turn of conversation that plays until you pick a state yourself. */
const SCRIPT: [VoiceOrbState, number][] = [
["listening", 3400],
["thinking", 1800],
["speaking", 4200],
["idle", 1400],
]
const STATES: VoiceOrbState[] = [
"idle",
"connecting",
"listening",
"thinking",
"speaking",
"interrupted",
"muted",
"error",
]
export default function AsciiOrbDemo() {
const [state, setState] = React.useState<VoiceOrbState>("speaking")
const [auto, setAuto] = React.useState(true)
const user = React.useMemo(() => createChatcnSpeechLevel({ seed: 3 }), [])
const agent = React.useMemo(() => createChatcnSpeechLevel({ seed: 7, loudness: 0.85 }), [])
React.useEffect(() => {
user.setSpeaking(state === "listening")
agent.setSpeaking(state === "speaking")
}, [state, user, agent])
React.useEffect(() => {
if (!auto) return
let step = 2
let timer = 0
const next = () => {
const [s, ms] = SCRIPT[step % SCRIPT.length]
setState(s)
step++
timer = window.setTimeout(next, ms)
}
next()
return () => window.clearTimeout(timer)
}, [auto])
return (
<div className="flex min-h-[520px] w-full flex-col items-center justify-center gap-8 p-6">
<div className="flex flex-col items-center gap-4">
<AsciiOrb size="lg" state={state} inputLevel={user.getLevel} outputLevel={agent.getLevel} />
<p aria-live="polite" className="text-[0.875rem]/[1.25rem] tracking-[-0.003em] text-(color:--chatcn-fg-muted) h-5 font-medium">
{voiceOrbStateLabels[state]}
</p>
</div>
<fieldset className="flex max-w-md flex-wrap justify-center gap-1">
<legend className="sr-only">Orb state</legend>
{STATES.map((s) => (
<button
key={s}
type="button"
aria-pressed={s === state}
onClick={() => {
setAuto(false)
setState(s)
}}
className="text-[0.8125rem]/[1.25rem] text-(color:--chatcn-fg-subtle) hover:text-(color:--chatcn-fg) hover:bg-(color:--chatcn-fill) aria-pressed:bg-(color:--chatcn-fill-active) aria-pressed:text-(color:--chatcn-fg) rounded-(--chatcn-radius-md) h-7 px-2.5 font-medium transition-colors"
>
{voiceOrbStateLabels[s]}
</button>
))}
</fieldset>
<p className="text-[0.75rem]/[1rem] text-(color:--chatcn-fg-faint)">
Dither, halftone, pixel and particle versions live on{" "}
<a
href="https://chatcn.dev/docs/components/ascii-orb"
target="_blank"
rel="noopener"
className="text-(color:--chatcn-fg-subtle) hover:text-(color:--chatcn-fg) underline underline-offset-2"
>
chatcn.dev
</a>
</p>
</div>
)
}
















