Files
zhila/ui/src/components/Choir.svelte
T

147 lines
4.1 KiB
Svelte

<svelte:options runes={true} />
<script lang="ts">
import type { AgentMessage, AgentProposal, MeterFrame, Pattern, State } from '../lib/types';
import { VEINS } from '../lib/types';
import { commandLabel } from '../lib/format';
let {
messages,
proposals,
busy,
project,
meter,
onask,
onapprove,
onreject
}: {
messages: AgentMessage[];
proposals: AgentProposal[];
busy: boolean;
project: State | null;
meter: MeterFrame;
onask: (text: string) => void;
onapprove: (id: number) => void;
onreject: (id: number) => void;
} = $props();
let text = $state('');
let log: HTMLDivElement | null = $state(null);
$effect(() => {
messages.length;
proposals.length;
if (log) log.scrollTop = log.scrollHeight;
});
function notes(pattern: Pattern): number {
return pattern.rows.reduce(
(total, row) => total + row.velocities.filter((velocity) => velocity > 0).length,
0
);
}
function send() {
const value = text.trim();
if (!value || busy) return;
text = '';
onask(value);
}
function listen() {
if (!busy) onask('Listen to the room. What do you hear?');
}
function handleKey(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
send();
}
}
</script>
<div class="choir">
<header class="choir-head">
<div>
<h1>choir</h1>
<p>the second performer · same verbs · proposals need a hand</p>
</div>
<div class="choir-readout">
<span>{project?.transport.playing ? 'playing' : 'still'}</span>
<span>{Math.round(project?.transport.bpm ?? 128)} bpm</span>
<span>{project?.transport.loop_bars ?? 8} bars</span>
<span>{project?.cores.length ?? 0} cores</span>
</div>
</header>
<div class="choir-log" bind:this={log}>
{#if messages.length === 0}
<p class="choir-empty">
Ask the choir to listen, to lift the bass, to open section B, or to render the eight bars.
It hears the levels; project edits arrive as proposals.
</p>
{/if}
{#each messages as message}
<div class="choir-message" class:is-user={message.role === 'user'}>
<span class="choir-role">{message.role === 'agent' ? 'choir' : message.role}</span>
<p>{message.text}</p>
</div>
{/each}
{#each proposals as proposal}
<div class="choir-proposal">
<div class="choir-proposal-head">
<span>proposal</span>
<span>{proposal.commands.length} move{proposal.commands.length === 1 ? '' : 's'}</span>
</div>
<p>{proposal.description}</p>
<div class="choir-moves">
{#each proposal.commands as command}
<span class="choir-move">{commandLabel(command)}</span>
{/each}
</div>
<div class="choir-proposal-actions">
<button onclick={() => onapprove(proposal.id)} disabled={busy}>approve</button>
<button onclick={() => onreject(proposal.id)} disabled={busy}>reject</button>
</div>
</div>
{/each}
</div>
<div class="choir-live">
<div class="choir-sections" aria-label="sections">
{#each project?.arrangement ?? [] as section, index}
<span class="choir-section" class:is-on={index === project?.active_section}>
<b>{String.fromCharCode(65 + index)}</b>
{notes(section)}
</span>
{/each}
</div>
<div class="choir-veins" aria-label="levels">
{#each VEINS as vein, index}
<span class="choir-vein">
<i style={`--level:${meter.veins[index] ?? 0}`}></i>
{vein.slice(0, 3)}
</span>
{/each}
</div>
</div>
<form
class="choir-form"
onsubmit={(event) => {
event.preventDefault();
send();
}}
>
<button type="button" class="choir-listen" onclick={listen} disabled={busy}>listen</button>
<input
bind:value={text}
onkeydown={handleKey}
placeholder={busy ? 'the choir is listening…' : 'ask the choir…'}
disabled={busy}
aria-label="ask the choir"
/>
<button type="submit" class="choir-send" disabled={busy || !text.trim()}>send</button>
</form>
</div>