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 { extractTasks } from "$lib/utils/taskExtractor.js";
import { pad } from "$lib/utils/time.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 { 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 transcript = $state("");
let segments = $state([]); let segments = $state([]);
@@ -196,6 +201,7 @@
async function startRecording() { async function startRecording() {
error = ""; error = "";
saved = false; saved = false;
transcriptionFailed = false;
if (!modelReady) { if (!modelReady) {
if (needsDownload) return; if (needsDownload) return;
await loadModel(); await loadModel();
@@ -339,6 +345,7 @@
} catch (err) { } catch (err) {
console.error("transcribe_pcm failed:", err); console.error("transcribe_pcm failed:", err);
error = typeof err === "string" ? err : err.message || "Transcription failed"; error = typeof err === "string" ? err : err.message || "Transcription failed";
transcriptionFailed = true;
if (!page.recording) { if (!page.recording) {
page.status = "Error"; page.status = "Error";
page.statusColor = "#e87171"; page.statusColor = "#e87171";
@@ -496,6 +503,15 @@
const trimmed = transcript.trim(); const trimmed = transcript.trim();
return trimmed ? trimmed.split(/\s+/).length : 0; 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> </script>
<div class="flex flex-col h-full animate-fade-in"> <div class="flex flex-col h-full animate-fade-in">
@@ -510,7 +526,7 @@
<!-- Record button --> <!-- Record button -->
<button <button
class="relative flex items-center justify-center w-[40px] h-[40px] min-w-[40px] flex-shrink-0 rounded-full text-white 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 {page.recording
? 'bg-danger animate-pulse-warm' ? 'bg-danger animate-pulse-warm'
: modelLoading : modelLoading
@@ -519,27 +535,36 @@
onclick={toggleRecording} onclick={toggleRecording}
disabled={modelLoading} disabled={modelLoading}
aria-label={page.recording ? "Stop recording" : "Start recording"} aria-label={page.recording ? "Stop recording" : "Start recording"}
style="transition-duration: var(--duration-ui)"
> >
{#if page.recording} {#if page.recording}
<span class="w-[14px] h-[14px] rounded-[3px] bg-white"></span> <span class="w-[14px] h-[14px] rounded-[3px] bg-white"></span>
{:else if modelLoading} {:else if modelLoading}
<svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> <Loader2 size={16} class="animate-spin" aria-hidden="true" />
<path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round" />
</svg>
{:else} {:else}
<span class="w-[14px] h-[14px] rounded-full bg-white/90"></span> <span class="w-[14px] h-[14px] rounded-full bg-white/90"></span>
{/if} {/if}
</button> </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) --> <!-- 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"> <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} {#if page.recording}
<span class="flex items-end gap-[3px] h-[18px]"> <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} {#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 {#if reduceMotion}
class="w-[2px] rounded-full bg-danger/70 animate-pulse-soft" <span
style="height: {h * 100}%; animation-delay: {i * 60}ms" class="w-[2px] rounded-full bg-danger"
></span> 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} {/each}
</span> </span>
{:else} {:else}
@@ -586,9 +611,10 @@
onclick={() => page.taskSidebarOpen = !page.taskSidebarOpen} onclick={() => page.taskSidebarOpen = !page.taskSidebarOpen}
aria-label="Toggle task sidebar" 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"> <span class="flex items-center gap-1.5">
<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" /> <SquareCheck size={16} class="{page.taskSidebarOpen ? 'text-accent' : 'text-text-tertiary'}" aria-hidden="true" />
</svg> <span class="text-[11px] {page.taskSidebarOpen ? 'text-accent' : 'text-text-tertiary'}">Tasks</span>
</span>
{#if taskCount > 0} {#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]"> <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} {taskCount}
@@ -679,19 +705,40 @@
{/if} {/if}
<!-- Transcript area --> <!-- 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"> <Card classes="h-full flex flex-col">
<textarea {#if transcriptionFailed && !transcript.trim() && !page.recording}
bind:this={textareaEl} <div class="flex-1 flex items-center justify-center">
class="font-transcript flex-1 w-full bg-transparent text-text p-6 <EmptyState icon={AlertTriangle} message="Something went wrong with that transcription. Your audio is saved try again when you're ready." />
resize-none focus:outline-none placeholder:text-text-tertiary min-h-0" </div>
placeholder={activeTemplate ? "Click a section above, then press record..." : "Your words will appear here..."} {:else if !transcript.trim() && !page.recording && !transcribing}
bind:value={transcript} <div class="flex-1 flex items-center justify-center">
onclick={() => { showExportMenu = false; showTemplateMenu = false; }} <EmptyState icon={Mic} message="Press record or Ctrl+Shift+R" />
data-no-transition </div>
aria-label="Transcript" <textarea
aria-live="polite" bind:this={textareaEl}
></textarea> 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) --> <!-- Status footer (inside transcript card) -->
<div class="flex items-center justify-between px-6 pb-3 pt-1 flex-shrink-0"> <div class="flex items-center justify-between px-6 pb-3 pt-1 flex-shrink-0">