agent: dictation — restyle with brand identity, Lucide icons, empty states

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jake
2026-03-21 11:04:05 +00:00
parent 5f05e25b74
commit 73851bdda9

View File

@@ -9,6 +9,11 @@
import { extractTasks } from "$lib/utils/taskExtractor.js";
import { pad } from "$lib/utils/time.js";
import { MAX_PCM_SAMPLES, MIN_CHUNK_SAMPLES, CHUNK_INTERVAL_MS, FEEDBACK_TIMEOUT_MS } from "$lib/utils/constants.js";
import { Mic, Loader2, SquareCheck, AlertTriangle } from 'lucide-svelte';
import EmptyState from '$lib/components/EmptyState.svelte';
import { getPreferences } from '$lib/stores/preferences.svelte.js';
import { bionicReading } from '$lib/actions/bionicReading.js';
const prefs = getPreferences();
let transcript = $state("");
let segments = $state([]);
@@ -196,6 +201,7 @@
async function startRecording() {
error = "";
saved = false;
transcriptionFailed = false;
if (!modelReady) {
if (needsDownload) return;
await loadModel();
@@ -339,6 +345,7 @@
} catch (err) {
console.error("transcribe_pcm failed:", err);
error = typeof err === "string" ? err : err.message || "Transcription failed";
transcriptionFailed = true;
if (!page.recording) {
page.status = "Error";
page.statusColor = "#e87171";
@@ -496,6 +503,15 @@
const trimmed = transcript.trim();
return trimmed ? trimmed.split(/\s+/).length : 0;
});
let reduceMotion = $derived(
prefs.accessibility.reduceMotion === 'on'
|| (prefs.accessibility.reduceMotion === 'system'
&& typeof window !== 'undefined'
&& window.matchMedia('(prefers-reduced-motion: reduce)').matches)
);
let transcriptionFailed = $state(false);
</script>
<div class="flex flex-col h-full animate-fade-in">
@@ -510,7 +526,7 @@
<!-- Record button -->
<button
class="relative flex items-center justify-center w-[40px] h-[40px] min-w-[40px] flex-shrink-0 rounded-full text-white
active:scale-[0.93] transition-all duration-200
active:scale-[0.93] transition-all
{page.recording
? 'bg-danger animate-pulse-warm'
: modelLoading
@@ -519,27 +535,36 @@
onclick={toggleRecording}
disabled={modelLoading}
aria-label={page.recording ? "Stop recording" : "Start recording"}
style="transition-duration: var(--duration-ui)"
>
{#if page.recording}
<span class="w-[14px] h-[14px] rounded-[3px] bg-white"></span>
{:else if modelLoading}
<svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round" />
</svg>
<Loader2 size={16} class="animate-spin" aria-hidden="true" />
{:else}
<span class="w-[14px] h-[14px] rounded-full bg-white/90"></span>
{/if}
</button>
<span class="text-[11px] font-medium flex-shrink-0 {page.recording ? 'text-danger' : 'text-text-secondary'}">
{page.recording ? 'Stop' : modelLoading ? 'Loading' : 'Record'}
</span>
<!-- Waveform placeholder (Phase 2: live visualiser) -->
<div class="flex-1 min-h-[32px] rounded-md border border-border-subtle bg-bg-elevated/40 flex items-center px-3">
{#if page.recording}
<span class="flex items-end gap-[3px] h-[18px]">
{#each [0.4, 0.8, 0.5, 1, 0.6, 0.9, 0.4, 0.7, 0.5, 0.8, 0.3, 0.6] as h, i}
<span
class="w-[2px] rounded-full bg-danger/70 animate-pulse-soft"
style="height: {h * 100}%; animation-delay: {i * 60}ms"
></span>
{#if reduceMotion}
<span
class="w-[2px] rounded-full bg-danger"
style="height: 50%"
></span>
{:else}
<span
class="w-[2px] rounded-full bg-danger/70 animate-pulse-soft"
style="height: {h * 100}%; animation-delay: {i * 60}ms"
></span>
{/if}
{/each}
</span>
{:else}
@@ -586,9 +611,10 @@
onclick={() => page.taskSidebarOpen = !page.taskSidebarOpen}
aria-label="Toggle task sidebar"
>
<svg class="w-4 h-4 {page.taskSidebarOpen ? 'text-accent' : 'text-text-tertiary'}" viewBox="0 0 24 24" fill="currentColor">
<path d="M9 11l3 3L22 4M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
</svg>
<span class="flex items-center gap-1.5">
<SquareCheck size={16} class="{page.taskSidebarOpen ? 'text-accent' : 'text-text-tertiary'}" aria-hidden="true" />
<span class="text-[11px] {page.taskSidebarOpen ? 'text-accent' : 'text-text-tertiary'}">Tasks</span>
</span>
{#if taskCount > 0}
<span class="absolute -top-0.5 -right-0.5 text-[9px] px-1 py-0 rounded-full bg-accent text-white font-medium min-w-[14px] text-center leading-[14px]">
{taskCount}
@@ -679,19 +705,40 @@
{/if}
<!-- Transcript area -->
<div class="flex-1 px-5 pt-3 pb-3 min-h-0">
<div class="flex-1 px-5 pt-3 pb-3 min-h-0" style="--text-transcript: {prefs.accessibility.transcriptSize}px; font-size: var(--text-transcript)">
<Card classes="h-full flex flex-col">
<textarea
bind:this={textareaEl}
class="font-transcript flex-1 w-full bg-transparent text-text p-6
resize-none focus:outline-none placeholder:text-text-tertiary min-h-0"
placeholder={activeTemplate ? "Click a section above, then press record..." : "Your words will appear here..."}
bind:value={transcript}
onclick={() => { showExportMenu = false; showTemplateMenu = false; }}
data-no-transition
aria-label="Transcript"
aria-live="polite"
></textarea>
{#if transcriptionFailed && !transcript.trim() && !page.recording}
<div class="flex-1 flex items-center justify-center">
<EmptyState icon={AlertTriangle} message="Something went wrong with that transcription. Your audio is saved try again when you're ready." />
</div>
{:else if !transcript.trim() && !page.recording && !transcribing}
<div class="flex-1 flex items-center justify-center">
<EmptyState icon={Mic} message="Press record or Ctrl+Shift+R" />
</div>
<textarea
bind:this={textareaEl}
class="font-transcript flex-1 w-full bg-transparent text-text p-6
resize-none focus:outline-none placeholder:text-text-tertiary min-h-0 hidden"
placeholder={activeTemplate ? "Click a section above, then press record..." : "Your words will appear here..."}
bind:value={transcript}
onclick={() => { showExportMenu = false; showTemplateMenu = false; }}
data-no-transition
aria-label="Transcript"
aria-live="polite"
></textarea>
{:else}
<textarea
bind:this={textareaEl}
class="font-transcript flex-1 w-full bg-transparent text-text p-6
resize-none focus:outline-none placeholder:text-text-tertiary min-h-0"
placeholder={activeTemplate ? "Click a section above, then press record..." : "Your words will appear here..."}
bind:value={transcript}
onclick={() => { showExportMenu = false; showTemplateMenu = false; }}
data-no-transition
aria-label="Transcript"
aria-live="polite"
></textarea>
{/if}
<!-- Status footer (inside transcript card) -->
<div class="flex items-center justify-between px-6 pb-3 pt-1 flex-shrink-0">