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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user