@italq/vloop
v0.0.9
Published
Embeddable voice-call widget powered by [Pipecat](https://github.com/pipecat-ai). Ships as both a Web Component (drop-in `<vloop-ai>` via a `<script>` tag) and a React component library (composable primitives + the preset all-in-one).
Readme
@italq/vloop
Embeddable voice-call widget powered by Pipecat. Ships as both a Web Component (drop-in <vloop-ai> via a <script> tag) and a React component library (composable primitives + the preset all-in-one).
- Web Component: single bundle, zero install, works in any HTML page.
- React API:
VLoop.AIfor the all-in-one case, or composeVLoop.Root+ primitives (When,Trigger,Audio,Timer,Waveform,HangUp, …) for custom UI. - Themeable via CSS custom properties or a typed
themeobject — no JSON-string styling. - Pluggable transport. Default is Pipecat; implement
VoiceTransportto swap. - Explicit state machine (
idle | connecting | in-call | ended | error) exposed via hooks and render-time<When>.
Install
Script tag (CDN):
<script src="https://unpkg.com/@italq/[email protected]/dist/bundle.min.js" async></script>npm / pnpm:
pnpm add @italq/vloop
# or: npm install @italq/vloopQuick start
Web Component
<vloop-ai
agent-id="4018131b-7068-4ef4-8663-4f850ab713e9"
agent-name="Aria"
client="vloop"
btn-text="Call"
size="230px"
position="center" />
<script src="https://unpkg.com/@italq/[email protected]/dist/bundle.min.js" async></script>React (preset)
import { VLoopAI } from '@italq/vloop';
export default function Page() {
return (
<VLoopAI
agentId="4018131b-7068-4ef4-8663-4f850ab713e9"
agentName="Aria"
client="vloop"
btnText="Call"
size="230px"
position="center"
/>
);
}React (composing primitives)
import { VLoop } from '@italq/vloop';
<VLoop.Provider call={{ client: 'vloop', cliId: 'c-001', appId: 'sales' }}>
<VLoop.Root agent={{ id: 'agt_42', name: 'Aria' }}>
<VLoop.When status="idle">
<VLoop.Trigger asChild>
<button className="my-btn">Talk to Aria</button>
</VLoop.Trigger>
</VLoop.When>
<VLoop.When status="connecting">Connecting…</VLoop.When>
<VLoop.When status="in-call">
<VLoop.Audio />
<header>
<VLoop.StatusDot />
<VLoop.AgentName as="h2" />
<VLoop.Timer />
</header>
<VLoop.Waveform />
<VLoop.HangUp asChild>
<button>End call</button>
</VLoop.HangUp>
</VLoop.When>
<VLoop.When status="error">
<VLoop.ErrorLabel />
<VLoop.Retry asChild>
<button>Retry</button>
</VLoop.Retry>
</VLoop.When>
</VLoop.Root>
</VLoop.Provider>Web Component attributes
All attributes are kebab-case. Names that map to the legacy v0 camelCase form are still accepted and emit a deprecation warning in the console.
| Attribute | Type | Default |
|------------------|-----------------------------------|-----------------------------------|
| agent-id | string (required) | — |
| agent-name | string | "AI Assistant" |
| client | string | "unknown" |
| cli-id | string | — |
| app-id | string | — |
| call-id | string | — |
| caller-num | string | — |
| callee-num | string | — |
| endpoint | URL string | "https://api.italq.io/connect" |
| btn-text | string | "Call" |
| size | CSS length | "280px" |
| position | inline | left | center | right | "center" |
| variant | orb | pill | "orb" |
| force-secure-ws (alias wss) | boolean (presence) | off — rewrites ws:// → wss:// on the backend's WebSocket URL (dev escape hatch) |
| theme-accent | CSS color | "#1d5ba5" |
| theme-radius | sm | md | lg | full | "full" |
| theme-bg | CSS color | "rgba(10,30,65,0.7)" |
| theme-font | CSS font-family | "Outfit, Inter, sans-serif" |
| theme-metal | CSS color | "#1d5ba5" |
| theme-aura | on | off | "on" |
| theme-aura-intensity | subtle | normal | vivid | 0–1.5 | "normal" |
| theme-aura-speed | number (1 = normal, 2 = 2× faster, 0.5 = half) | 1 |
| theme-aura-color | CSS color | lightened theme-metal |
| theme-agent-color | CSS color (agent-name text) | #ffffff |
| theme-btn-color | CSS color (button label text) | theme text |
| theme-btn-bg | CSS color (button fill, pill only) | theme-accent |
| theme-shadow | raw CSS box-shadow (outer shadow) | layered dark drop shadow |
| theme-pill-bg | CSS background (pill variant only) | theme-bg |
Variants
variant chooses the widget's visual form:
orb(default) — the full circular orb in every state.pill— a compact horizontal pill at rest (mini aura avatar + agent name + call button) that morphs into the full orb once a call starts. Less intrusive; anchors perposition. Its background is set independently viatheme-pill-bg(any CSS background — color, gradient, rgba).
<vloop-ai agent-id="..." variant="pill" position="right" theme-pill-bg="#ffffff" />The aura
The orb's surface is a living conic "fan" of rays that flows and breathes. It has two states, crossfaded smoothly:
| State | When | Feel |
|-------|------|------|
| Calm | no call (idle / ended) | slow drift, barely moving |
| In-call | connecting / in-call | steadier, more present |
theme-metal recolors the whole effect from a single base color; light/mid/deep ray stops are derived from it automatically. The theme-aura-* attributes tune it further and compose on top of both states (e.g. theme-aura-speed="2" speeds up both proportionally rather than replacing them):
<vloop-ai
agent-id="..."
theme-metal="#6d28d9"
theme-aura-intensity="vivid"
theme-aura-color="#a78bfa" />
<!-- Sober, static fan (no animation, lower CPU): -->
<vloop-ai agent-id="..." theme-aura="off" />Theming via inline CSS variables
theme-* attributes are a convenience for the common cases. For fine-grained control, set CSS variables directly:
<vloop-ai
agent-id="..."
style="--vloop-accent: #6366f1;
--vloop-bg: rgba(20, 20, 30, 0.85);
--vloop-radius-btn: 0.75em;" />Available variables: --vloop-accent, --vloop-bg, --vloop-bg-deep, --vloop-font, --vloop-radius-btn, --vloop-text, --vloop-text-muted, --vloop-border, --vloop-shadow, --vloop-shadow-hover, --vloop-status-color, --vloop-metal, --vloop-aura-color, --vloop-aura-intensity, --vloop-aura-speed, --vloop-agent-color, --vloop-btn-color, --vloop-shadow-user, --vloop-pill-bg, --vloop-btn-bg.
React API
Components
| Export | Purpose |
|-------------------------|--------------------------------------------------------------------------|
| VLoop.AI | All-in-one preset. Equivalent to the Web Component. |
| VLoop.Provider | Optional ancestor that supplies default call / theme / transport. |
| VLoop.Root | Owns one call session and exposes state + actions via context. |
| VLoop.When | Render its children only while state.status matches. |
| VLoop.Trigger | Starts the call. asChild clones any child (Radix-style Slot). |
| VLoop.HangUp | Ends the current call. |
| VLoop.Retry | Re-runs start() after an error. |
| VLoop.Audio | Mounts the Pipecat audio sink. Render only while in-call. |
| VLoop.AgentName | Renders agent.name. as chooses the wrapping element. |
| VLoop.StatusDot | Color-coded dot reflecting the current status. |
| VLoop.Timer | Elapsed call time. Default format MM:SS, override with format. |
| VLoop.Waveform | Animated bars; pulses while botSpeaking. |
| VLoop.ErrorLabel | Displays state.error.message while in error. |
Theme object
VLoop.AI and VLoop.Provider accept a typed theme (the React equivalent of the theme-* attributes):
<VLoopAI
agentId="..."
theme={{
accent: '#1d5ba5', // primary accent (pill button default fill)
radius: 'full', // 'sm' | 'md' | 'lg' | 'full'
bg: 'rgba(10,30,65,0.7)', // generic background (orb panel / pill default)
font: 'Outfit, sans-serif',
metal: '#6d28d9', // base color the aura derives from
auraColor: '#a78bfa',
auraIntensity: 'vivid', // 'subtle' | 'normal' | 'vivid' | number (0–1.5)
auraSpeed: 1.5, // motion multiplier
aura: true, // false to freeze to a static fan
agentColor: '#ffffff', // agent-name text color (default)
btnColor: '#ffffff', // button label text color
btnBg: '#1d5ba5', // pill button fill color
shadow: '0 20px 60px 0 rgba(109,40,217,0.5)', // raw CSS box-shadow (outer)
pillBg: '#ffffff', // pill-variant background (any CSS background)
}}
/>Hooks
| Hook | Returns |
|----------------------|--------------------------------------------------------------------|
| useVLoop() | The discriminated state plus start / hangUp / retry / reset. |
| useCallStatus() | The current status literal. |
| useBotSpeaking() | true while the bot is producing audio. |
| useCallElapsed() | Elapsed seconds while in-call, 0 otherwise. |
All hooks throw if called outside <VLoop.Root> (or <VLoop.AI>, which renders a Root internally).
State shape
type VLoopState =
| { status: 'idle' }
| { status: 'connecting'; since: number }
| { status: 'in-call'; botSpeaking: boolean; elapsedSeconds: number; startedAt: number }
| { status: 'ended'; reason: 'user' | 'remote' | 'timeout'; durationSeconds: number }
| { status: 'error'; error: Error; retryable: boolean };Custom transport
import { VLoop, type VoiceTransport, type TransportEvent } from '@italq/vloop';
class MockTransport implements VoiceTransport {
private listeners = new Set<(e: TransportEvent) => void>();
subscribe(fn: (e: TransportEvent) => void) {
this.listeners.add(fn);
return () => this.listeners.delete(fn);
}
async start() { this.emit({ type: 'connecting' }); this.emit({ type: 'ready' }); }
async stop() { this.emit({ type: 'disconnected', reason: 'user' }); }
private emit(e: TransportEvent) { this.listeners.forEach(fn => fn(e)); }
}
<VLoop.Root agent={{ id: 'agt_42' }} transport={new MockTransport()}>…</VLoop.Root>Migration from v0.x
| v0 (deprecated) | v1 |
|-----------------------------|-------------------------------------|
| agentId="..." | agent-id="..." |
| agentName="..." | agent-name="..." |
| btnText="Call" | btn-text="Call" |
| cliId="..." | cli-id="..." |
| appId="..." | app-id="..." |
| callId="..." | call-id="..." |
| callerNum="..." | caller-num="..." |
| calleeNum="..." | callee-num="..." |
| styles='{"bottom":"40px"}'| style="--vloop-accent: ..." or theme-* attributes |
| import VLoopWidget from '@italq/vloop' (default export) | import { VLoopAI } from '@italq/vloop' (named) |
The legacy camelCase attributes continue to work in v1 and emit a one-time console.warn with the canonical name. They will be removed in v2.
Development
pnpm install
pnpm dev # vite dev server with hot reload
pnpm build # tsc -b && vite build -> dist/bundle.min.js
pnpm lint # eslint srcManual smoke test of the bundled output:
pnpm build
# then open examples/agent.html and uncomment the local <script src="../dist/bundle.min.js">Publishing
pnpm build
npm publish --access public