- Cargo workspace with 6 domain crates: core, audio, transcription, ai-formatting, storage, cloud-providers - Minimal Tauri shell (lib.rs + main.rs) with plugin registration - Svelte 5 frontend copied from Ramble v0.2 - All crates compile as empty stubs - App identifier: uk.co.corbel.kon Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
265 lines
10 KiB
Svelte
265 lines
10 KiB
Svelte
<script>
|
|
import { profiles, saveProfiles, templates, saveTemplates, page } from "$lib/stores/page.svelte.js";
|
|
import Card from "$lib/components/Card.svelte";
|
|
import SegmentedButton from "$lib/components/SegmentedButton.svelte";
|
|
|
|
let tab = $state("Profiles");
|
|
let showNewDialog = $state(false);
|
|
let newName = $state("");
|
|
|
|
// ---- Profiles ----
|
|
|
|
function createProfile() {
|
|
if (!newName.trim()) return;
|
|
profiles.push({ name: newName.trim(), words: "", saved: false });
|
|
saveProfiles();
|
|
newName = "";
|
|
showNewDialog = false;
|
|
}
|
|
|
|
function deleteProfile(index) {
|
|
if (page.activeProfile === profiles[index].name) {
|
|
page.activeProfile = "None";
|
|
}
|
|
profiles.splice(index, 1);
|
|
saveProfiles();
|
|
}
|
|
|
|
function saveProfile(index) {
|
|
saveProfiles();
|
|
profiles[index].saved = true;
|
|
setTimeout(() => { if (profiles[index]) profiles[index].saved = false; }, 2000);
|
|
}
|
|
|
|
function wordCount(words) {
|
|
return words.split("\n").filter((w) => w.trim()).length;
|
|
}
|
|
|
|
// ---- Templates ----
|
|
|
|
function createTemplate() {
|
|
if (!newName.trim()) return;
|
|
templates.push({ name: newName.trim(), sections: ["Section 1", "Section 2", "Section 3"] });
|
|
saveTemplates();
|
|
newName = "";
|
|
showNewDialog = false;
|
|
}
|
|
|
|
function deleteTemplate(index) {
|
|
templates.splice(index, 1);
|
|
saveTemplates();
|
|
}
|
|
|
|
function saveTemplate(index) {
|
|
saveTemplates();
|
|
templates[index]._saved = true;
|
|
setTimeout(() => { if (templates[index]) templates[index]._saved = false; }, 2000);
|
|
}
|
|
|
|
function handleCreate() {
|
|
if (tab === "Profiles") createProfile();
|
|
else createTemplate();
|
|
}
|
|
</script>
|
|
|
|
<div class="flex flex-col h-full overflow-y-auto animate-fade-in">
|
|
<!-- Header -->
|
|
<div class="flex items-center gap-4 px-7 pt-6 pb-2">
|
|
<h2 class="font-display text-[24px] italic text-text">
|
|
{tab === "Profiles" ? "Profiles" : "Templates"}
|
|
</h2>
|
|
<div class="flex-1"></div>
|
|
<button
|
|
class="px-4 py-2 rounded-lg text-[13px] font-medium text-white bg-accent hover:bg-accent-hover
|
|
shadow-[0_2px_8px_rgba(232,168,124,0.2)] active:scale-[0.97] transition-all duration-150"
|
|
onclick={() => showNewDialog = true}
|
|
>
|
|
+ New {tab === "Profiles" ? "Profile" : "Template"}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tab switcher -->
|
|
<div class="px-7 pb-4">
|
|
<SegmentedButton options={["Profiles", "Templates"]} bind:value={tab} />
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
<p class="text-[12px] text-text-secondary px-7 pb-5 leading-relaxed max-w-lg">
|
|
{#if tab === "Profiles"}
|
|
Add custom vocabulary to improve transcription accuracy.
|
|
Names, places, and specialist terms that Whisper might misspell.
|
|
{:else}
|
|
Create structured templates for dictation. Sections become headings
|
|
you can click and record into — fill reports, meeting notes, or any repeating format.
|
|
{/if}
|
|
</p>
|
|
|
|
<!-- New dialog -->
|
|
{#if showNewDialog}
|
|
<div class="px-7 pb-4 animate-slide-up">
|
|
<Card>
|
|
<div class="p-5">
|
|
<p class="text-[14px] font-semibold text-text mb-3">
|
|
New {tab === "Profiles" ? "Profile" : "Template"}
|
|
</p>
|
|
<input
|
|
class="w-full bg-bg-input border border-border rounded-lg px-3 py-2.5 text-[13px] text-text
|
|
placeholder:text-text-tertiary focus:outline-none focus:border-accent focus:shadow-[0_0_0_3px_rgba(232,168,124,0.1)] mb-2"
|
|
placeholder={tab === "Profiles" ? "e.g. Work, DnD Campaign, Interview..." : "e.g. Meeting Notes, Report, Daily Log..."}
|
|
bind:value={newName}
|
|
onkeydown={(e) => e.key === "Enter" && handleCreate()}
|
|
data-no-transition
|
|
/>
|
|
<p class="text-[11px] text-text-tertiary mb-4">
|
|
{tab === "Profiles"
|
|
? "You can add vocabulary after creating the profile."
|
|
: "You can edit sections after creating the template."}
|
|
</p>
|
|
<div class="flex gap-2">
|
|
<button
|
|
class="px-4 py-1.5 rounded-lg text-[12px] font-medium text-white bg-accent hover:bg-accent-hover
|
|
active:scale-[0.97] transition-all duration-150"
|
|
onclick={handleCreate}
|
|
>Create</button>
|
|
<button
|
|
class="px-4 py-1.5 rounded-lg text-[12px] text-text-secondary hover:bg-hover hover:text-text"
|
|
onclick={() => { showNewDialog = false; newName = ""; }}
|
|
>Cancel</button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Content -->
|
|
<div class="flex-1 px-7 pb-6 space-y-3">
|
|
{#if tab === "Profiles"}
|
|
<!-- Profile cards -->
|
|
{#if profiles.length === 0 && !showNewDialog}
|
|
<div class="flex flex-col items-center justify-center py-16 gap-3">
|
|
<div class="w-12 h-12 rounded-full bg-bg-elevated flex items-center justify-center">
|
|
<svg class="w-6 h-6 text-text-tertiary" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M12 12a5 5 0 1 0 0-10 5 5 0 0 0 0 10Zm0 2c-5.33 0-8 2.67-8 4v2h16v-2c0-1.33-2.67-4-8-4Z" />
|
|
</svg>
|
|
</div>
|
|
<p class="text-[13px] text-text-tertiary">No profiles yet</p>
|
|
<p class="text-[11px] text-text-tertiary">Create one to improve transcription accuracy</p>
|
|
</div>
|
|
{/if}
|
|
|
|
{#each profiles as profile, i}
|
|
<div class="animate-slide-up">
|
|
<Card>
|
|
<div class="p-5">
|
|
<div class="flex items-center gap-3 mb-1">
|
|
<h3 class="text-[15px] font-semibold text-text">{profile.name}</h3>
|
|
<span class="text-[11px] text-text-tertiary px-2 py-0.5 rounded-md bg-bg-elevated">
|
|
{wordCount(profile.words)} {wordCount(profile.words) === 1 ? 'word' : 'words'}
|
|
</span>
|
|
<div class="flex-1"></div>
|
|
<button
|
|
class="px-2.5 py-1 rounded-md text-[11px] text-text-tertiary hover:text-danger hover:bg-hover"
|
|
onclick={() => deleteProfile(i)}
|
|
>Delete</button>
|
|
</div>
|
|
|
|
<p class="text-[11px] text-text-tertiary mb-3">
|
|
One word or phrase per line. These will be prioritised during transcription.
|
|
</p>
|
|
|
|
<textarea
|
|
class="w-full h-[140px] bg-bg-elevated border border-border-subtle rounded-xl px-4 py-3
|
|
text-[13px] text-text leading-relaxed resize-none
|
|
focus:outline-none focus:border-accent focus:shadow-[0_0_0_3px_rgba(232,168,124,0.1)]"
|
|
placeholder="CORBEL Jake Sames Northampton ..."
|
|
bind:value={profile.words}
|
|
data-no-transition
|
|
></textarea>
|
|
|
|
<div class="flex items-center gap-3 mt-3">
|
|
<button
|
|
class="px-4 py-1.5 rounded-lg text-[12px] font-medium text-white bg-accent hover:bg-accent-hover
|
|
active:scale-[0.97] transition-all duration-150"
|
|
onclick={() => saveProfile(i)}
|
|
>Save</button>
|
|
{#if profile.saved}
|
|
<span class="text-[11px] text-success font-medium animate-fade-in">Saved</span>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
{/each}
|
|
|
|
{:else}
|
|
<!-- Template cards -->
|
|
{#if templates.length === 0 && !showNewDialog}
|
|
<div class="flex flex-col items-center justify-center py-16 gap-3">
|
|
<div class="w-12 h-12 rounded-full bg-bg-elevated flex items-center justify-center">
|
|
<svg class="w-6 h-6 text-text-tertiary" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6Zm4 18H6V4h7v5h5v11Z" />
|
|
</svg>
|
|
</div>
|
|
<p class="text-[13px] text-text-tertiary">No templates yet</p>
|
|
<p class="text-[11px] text-text-tertiary">Create one for structured dictation</p>
|
|
</div>
|
|
{/if}
|
|
|
|
{#each templates as template, i}
|
|
<div class="animate-slide-up">
|
|
<Card>
|
|
<div class="p-5">
|
|
<div class="flex items-center gap-3 mb-1">
|
|
<h3 class="text-[15px] font-semibold text-text">{template.name}</h3>
|
|
<span class="text-[11px] text-text-tertiary px-2 py-0.5 rounded-md bg-bg-elevated">
|
|
{template.sections.length} sections
|
|
</span>
|
|
<div class="flex-1"></div>
|
|
<button
|
|
class="px-2.5 py-1 rounded-md text-[11px] text-text-tertiary hover:text-danger hover:bg-hover"
|
|
onclick={() => deleteTemplate(i)}
|
|
>Delete</button>
|
|
</div>
|
|
|
|
<p class="text-[11px] text-text-tertiary mb-3">
|
|
One section per line. These become headings in your transcript.
|
|
</p>
|
|
|
|
<textarea
|
|
class="w-full h-[140px] bg-bg-elevated border border-border-subtle rounded-xl px-4 py-3
|
|
text-[13px] text-text leading-relaxed resize-none
|
|
focus:outline-none focus:border-accent focus:shadow-[0_0_0_3px_rgba(232,168,124,0.1)]"
|
|
placeholder="Summary Background Key Findings Recommendations"
|
|
value={template.sections.join("\n")}
|
|
oninput={(e) => { template.sections = e.target.value.split("\n").filter((s) => s.trim()); }}
|
|
data-no-transition
|
|
></textarea>
|
|
|
|
<div class="flex items-center gap-3 mt-3">
|
|
<button
|
|
class="px-4 py-1.5 rounded-lg text-[12px] font-medium text-white bg-accent hover:bg-accent-hover
|
|
active:scale-[0.97] transition-all duration-150"
|
|
onclick={() => saveTemplate(i)}
|
|
>Save</button>
|
|
{#if template._saved}
|
|
<span class="text-[11px] text-success font-medium animate-fade-in">Saved</span>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Preview -->
|
|
<div class="mt-3 pt-3 border-t border-border-subtle">
|
|
<p class="text-[10px] text-text-tertiary uppercase tracking-wider mb-2">Preview</p>
|
|
<div class="text-[12px] text-text-secondary space-y-1">
|
|
{#each template.sections as section}
|
|
<p class="font-medium text-text"># {section}</p>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
{/each}
|
|
{/if}
|
|
</div>
|
|
</div>
|