147 lines
4.1 KiB
Svelte
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>
|