feat(ui): HostedAiBanner one-time hosted-AI notice component (T10.3/M3.3)

New component mirroring ConsentNotice.svelte's card/overlay pattern:
shown before first use of a hosted (non-local) provider, explains that
the transcript leaves the device to a third party, and offers
accept/cancel. Uses the existing design tokens (--accent, --warning,
--bg-elevated, --radius-lg/sm, --shadow-lg) so it matches the
recording-consent notice visually. Not yet wired into any view — next
commits add it to Settings and SummaryPanel.
This commit is contained in:
iamdoubz
2026-07-07 07:52:25 -05:00
parent 3cdb0abb2d
commit 754f0f0b6a
+94
View File
@@ -0,0 +1,94 @@
<script lang="ts">
// One-time third-party "data leaves your device" notice (ADR-0011, T10.3/
// M3.3), shown before the first use of any hosted (non-local) AI provider —
// Anthropic today, a hosted OpenAI-compatible gateway once wired the same
// way. Same shared-copy/shared-acknowledgment pattern as ConsentNotice.svelte
// (recording consent, ADR-0009): both gate a single Settings toggle AND a
// second use-time trigger point on the same one-time flag.
import { Globe } from "@lucide/svelte";
let {
providerLabel,
onAccept,
onCancel,
}: { providerLabel: string; onAccept: () => void; onCancel: () => void } = $props();
</script>
<div class="banner" role="alertdialog" aria-labelledby="hosted-ai-heading">
<div class="heading">
<Globe size={18} aria-hidden="true" />
<strong id="hosted-ai-heading">Before using {providerLabel}</strong>
</div>
<p>
Generating with <strong>{providerLabel}</strong> sends this meeting's transcript to their
servers — it leaves this device and is subject to their privacy policy. WhispAssist has no
control over data handling once it leaves your device. This is off by default; you're choosing
it now.
</p>
<div class="actions">
<button class="primary" onclick={onAccept}>I understand continue</button>
<button class="ghost" onclick={onCancel}>Cancel</button>
</div>
</div>
<style>
.banner {
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
padding: 1rem 1.1rem;
background: var(--bg-elevated);
color: var(--fg);
max-width: 420px;
}
.heading {
display: flex;
align-items: center;
gap: 0.45rem;
color: var(--warning);
margin-bottom: 0.4rem;
}
.heading strong {
color: var(--fg);
}
p {
margin: 0;
font-size: 0.9rem;
line-height: 1.5;
color: var(--muted);
}
p strong {
color: var(--fg);
}
.actions {
display: flex;
align-items: center;
gap: 0.6rem;
flex-wrap: wrap;
margin-top: 0.8rem;
}
button {
font: inherit;
cursor: pointer;
}
button.primary {
background: var(--accent);
color: var(--accent-fg);
border: 1px solid transparent;
padding: 0.4rem 0.8rem;
border-radius: var(--radius-sm);
font-weight: 600;
}
button.primary:hover {
background: var(--accent-hover);
}
button.ghost {
background: none;
border: 1px solid var(--border);
padding: 0.4rem 0.8rem;
border-radius: var(--radius-sm);
color: var(--fg);
}
button.ghost:hover {
background: var(--bg-hover);
}
</style>