feat(kon): scaffold hybrid modular workspace
- 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>
This commit is contained in:
264
src/lib/pages/ProfilesPage.svelte
Normal file
264
src/lib/pages/ProfilesPage.svelte
Normal file
@@ -0,0 +1,264 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user