import { Button } from '@/components/ui/button'
import { Codicon } from '@/components/ui/codicon'
import { triggerHaptic } from '@/lib/haptics'
import { AudioLines, Layers3, Loader2, Square } from '@/lib/icons'
import { cn } from '@/lib/utils'
import type { ConversationStatus } from './hooks/use-voice-conversation'
import type { ChatBarState, VoiceStatus } from './types'
export const ICON_BTN = 'size-(--composer-control-size) shrink-0 rounded-md'
export const GHOST_ICON_BTN = cn(
ICON_BTN,
'text-(--ui-text-tertiary) hover:bg-(--chrome-action-hover) hover:text-foreground'
)
// Send/voice-conversation primary: solid foreground-on-background circle
// (reads as black-on-white in light mode, white-on-black in dark mode) to
// match the reference composer's high-contrast CTA. Keeps the pill itself
// neutral and lets the action visually dominate the row.
export const PRIMARY_ICON_BTN = cn(
'size-(--composer-control-primary-size,var(--composer-control-size)) shrink-0 rounded-full p-0',
'bg-foreground text-background hover:bg-foreground/90',
'disabled:bg-foreground/30 disabled:text-background disabled:opacity-100'
)
interface ConversationProps {
active: boolean
level: number
muted: boolean
status: ConversationStatus
onEnd: () => void
onStart: () => void
onStopTurn: () => void
onToggleMute: () => void
}
export function ComposerControls({
busy,
busyAction,
canSubmit,
conversation,
disabled,
hasComposerPayload,
state,
voiceStatus,
onDictate
}: {
busy: boolean
busyAction: 'queue' | 'stop'
canSubmit: boolean
conversation: ConversationProps
disabled: boolean
hasComposerPayload: boolean
state: ChatBarState
voiceStatus: VoiceStatus
onDictate: () => void
}) {
if (conversation.active) {
return
}
const showVoicePrimary = !busy && !hasComposerPayload
return (
{showVoicePrimary ? (
) : (
)}
)
}
function ConversationPill({
disabled,
level,
muted,
onEnd,
onStopTurn,
onToggleMute,
status
}: ConversationProps & { disabled: boolean }) {
const speaking = status === 'speaking'
const listening = status === 'listening' && !muted
const label =
status === 'speaking'
? 'Speaking'
: status === 'transcribing'
? 'Transcribing'
: status === 'thinking'
? 'Thinking'
: muted
? 'Muted'
: 'Listening'
return (
{listening && (
)}
{label}
)
}
function ConversationIndicator({
level,
listening,
speaking
}: {
level: number
listening: boolean
speaking: boolean
}) {
if (speaking) {
return
}
const bars = [0.55, 0.85, 1, 0.85, 0.55]
const normalized = Math.max(0, Math.min(level, 1))
return (
{bars.map((weight, index) => {
const height = listening ? 0.3 + Math.min(0.7, normalized * weight) : 0.3
return
})}
)
}
function DictationButton({
disabled,
state,
status,
onToggle
}: {
disabled: boolean
state: ChatBarState['voice']
status: VoiceStatus
onToggle: () => void
}) {
const active = state.active || status !== 'idle'
const aria =
status === 'recording' ? 'Stop dictation' : status === 'transcribing' ? 'Transcribing dictation' : 'Voice dictation'
return (
)
}