feat: integrate AI growth analyst experience
This commit is contained in:
207
src/lib/components/GrowthInsights.svelte
Normal file
207
src/lib/components/GrowthInsights.svelte
Normal file
@@ -0,0 +1,207 @@
|
||||
<script lang="ts">
|
||||
import CalendarHeartIcon from '@lucide/svelte/icons/calendar-heart';
|
||||
import CircleAlertIcon from '@lucide/svelte/icons/circle-alert';
|
||||
import LightbulbIcon from '@lucide/svelte/icons/lightbulb';
|
||||
import SparklesIcon from '@lucide/svelte/icons/sparkles';
|
||||
import TrophyIcon from '@lucide/svelte/icons/trophy';
|
||||
import UsersRoundIcon from '@lucide/svelte/icons/users-round';
|
||||
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
import { formatMoney, formatPercent, formatValue } from '$lib/format';
|
||||
import type { components } from '$lib/api/schema';
|
||||
|
||||
type Win = components['schemas']['GrowthHistoricalWin'];
|
||||
type Opportunity = components['schemas']['GrowthOpportunity'];
|
||||
type Risk = components['schemas']['GrowthRisk'];
|
||||
type Brief = components['schemas']['GrowthCampaignBrief'];
|
||||
type Peer = components['schemas']['GrowthPeer'];
|
||||
|
||||
let {
|
||||
wins = [],
|
||||
opportunities = [],
|
||||
risks = [],
|
||||
briefs = [],
|
||||
peer,
|
||||
terminalScoped = false,
|
||||
}: {
|
||||
wins?: Win[];
|
||||
opportunities?: Opportunity[];
|
||||
risks?: Risk[];
|
||||
briefs?: Brief[];
|
||||
peer?: Peer | null;
|
||||
terminalScoped?: boolean;
|
||||
} = $props();
|
||||
|
||||
function dateLabel(value: string) {
|
||||
return new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
|
||||
dateStyle: 'medium',
|
||||
timeZone: 'Asia/Tehran',
|
||||
}).format(new Date(value));
|
||||
}
|
||||
|
||||
function bps(value: number) {
|
||||
return formatPercent(String(value / 100));
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="grid gap-4 xl:grid-cols-[1.25fr_.75fr]">
|
||||
<div class="space-y-4">
|
||||
<Card class="rounded-lg shadow-xs">
|
||||
<CardHeader
|
||||
><CardTitle class="flex items-center gap-2 text-base"
|
||||
><CalendarHeartIcon class="text-primary size-5" /> فرصتهای پیش رو</CardTitle
|
||||
></CardHeader
|
||||
>
|
||||
<CardContent class="grid gap-3 md:grid-cols-2">
|
||||
{#each opportunities.slice(0, 5) as opportunity, index}
|
||||
<article id={`evidence-${opportunity.evidenceId}`} class="bg-muted/55 rounded-md border p-4">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<span class="text-muted-foreground text-xs">فرصت {formatValue(index + 1)}</span>
|
||||
<h3 class="mt-1 font-bold">{opportunity.eventTitleFa}</h3>
|
||||
</div>
|
||||
<Badge variant="secondary" class="shrink-0 rounded-md"
|
||||
>شباهت {bps(opportunity.similarityBps)}</Badge
|
||||
>
|
||||
</div>
|
||||
<p class="text-muted-foreground mt-3 text-xs">{dateLabel(opportunity.date)}</p>
|
||||
<p class="mt-2 text-sm leading-7">
|
||||
برآورد معمول <strong>{formatValue(opportunity.expectedSessions.p50)} پرداخت</strong> به ارزش
|
||||
<strong>{formatMoney(opportunity.expectedAmountRial.p50)}</strong> است.
|
||||
</p>
|
||||
</article>
|
||||
{:else}
|
||||
<p class="text-muted-foreground md:col-span-2 text-sm">
|
||||
در افق فعلی، فرصت رویدادی معناداری پیدا نشد.
|
||||
</p>
|
||||
{/each}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{#if briefs.length}
|
||||
<section aria-labelledby="campaign-briefs-heading">
|
||||
<h2 id="campaign-briefs-heading" class="mb-3 flex items-center gap-2 text-lg font-bold">
|
||||
<SparklesIcon class="text-primary size-5" /> پیشنهادهای آمادهسازی
|
||||
</h2>
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
{#each briefs as brief}
|
||||
<Card class="rounded-lg shadow-xs">
|
||||
<CardContent class="p-5">
|
||||
<h3 class="font-bold">{brief.headline}</h3>
|
||||
<p class="text-muted-foreground mt-2 text-sm leading-7">{brief.interpretation}</p>
|
||||
<dl class="mt-4 grid gap-3 text-sm">
|
||||
<div>
|
||||
<dt class="font-semibold">پیشنهاد فروش</dt>
|
||||
<dd class="text-muted-foreground mt-1">{brief.offerSuggestion}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="font-semibold">پیام پیشنهادی</dt>
|
||||
<dd class="text-muted-foreground mt-1">{brief.messageSuggestion}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="font-semibold">قدم آمادهسازی</dt>
|
||||
<dd class="text-muted-foreground mt-1">{brief.preparationAction}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p class="text-muted-foreground mt-4 border-t pt-3 text-xs">{brief.limitation}</p>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
{#each brief.evidenceIds as evidenceId}<a
|
||||
class="text-primary text-xs font-semibold underline-offset-4 hover:underline"
|
||||
href={`#evidence-${evidenceId}`}>مشاهده شاهد</a
|
||||
>{/each}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
<p class="text-muted-foreground mt-3 text-xs">
|
||||
این پیشنهادها نقش راهنما دارند؛ تصمیم نهایی را با شرایط کسبوکارتان هماهنگ کنید.
|
||||
</p>
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<Card class="rounded-lg shadow-xs">
|
||||
<CardHeader
|
||||
><CardTitle class="flex items-center gap-2 text-base"
|
||||
><TrophyIcon class="size-5 text-[var(--warning)]" /> تجربههای موفق گذشته</CardTitle
|
||||
></CardHeader
|
||||
>
|
||||
<CardContent class="space-y-3">
|
||||
{#each wins as win}
|
||||
<article id={`evidence-${win.evidenceId}`} class="border-b pb-3 last:border-0 last:pb-0">
|
||||
<h3 class="font-semibold">{win.eventTitleFa}</h3>
|
||||
<p class="text-muted-foreground mt-1 text-xs">{dateLabel(win.startDate)}</p>
|
||||
<p class="mt-2 text-sm">
|
||||
تعداد پرداخت <strong class="text-[var(--success)]"
|
||||
>{bps(win.paidSessionUpliftBps ?? 0)} بیشتر</strong
|
||||
>
|
||||
و مبلغ
|
||||
<strong>{bps(win.amountUpliftBps ?? 0)} بیشتر</strong> از روزهای مشابه بوده است.
|
||||
</p>
|
||||
</article>
|
||||
{:else}<p class="text-muted-foreground text-sm">
|
||||
هنوز رویداد موفقی با شواهد کافی ثبت نشده است.
|
||||
</p>{/each}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card class="rounded-lg border-amber-300/60 shadow-xs dark:border-amber-800/70">
|
||||
<CardHeader
|
||||
><CardTitle class="flex items-center gap-2 text-base"
|
||||
><CircleAlertIcon class="size-5 text-[var(--warning)]" /> دورههای نیازمند آمادگی</CardTitle
|
||||
></CardHeader
|
||||
>
|
||||
<CardContent class="space-y-3">
|
||||
{#each risks.slice(0, 3) as risk}
|
||||
<article
|
||||
id={`evidence-${risk.evidenceId}`}
|
||||
class="rounded-md border bg-amber-50/50 p-4 dark:bg-amber-950/15"
|
||||
>
|
||||
<h3 class="font-semibold">{dateLabel(risk.date)}</h3>
|
||||
<p class="text-muted-foreground mt-2 text-sm leading-7">
|
||||
ممکن است تعداد پرداخت حدود {bps(risk.sessionDeclineBps)} کمتر از روند معمول باشد. برآورد میانی این
|
||||
روز {formatValue(risk.expectedSessions.p50)} پرداخت است.
|
||||
</p>
|
||||
</article>
|
||||
{:else}<p class="text-muted-foreground text-sm">افت معناداری برای هشدار پیدا نشد.</p>{/each}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card class="rounded-lg shadow-xs">
|
||||
<CardHeader
|
||||
><CardTitle class="flex items-center gap-2 text-base"
|
||||
><UsersRoundIcon class="text-primary size-5" /> مقایسه امن با همگروهها</CardTitle
|
||||
></CardHeader
|
||||
>
|
||||
<CardContent>
|
||||
{#if terminalScoped}
|
||||
<p class="text-muted-foreground text-sm leading-7">
|
||||
برای حفظ معنای مقایسه، جایگاه همگروهی فقط در سطح کل پذیرنده نمایش داده میشود و برای یک درگاه
|
||||
محاسبه نمیشود.
|
||||
</p>
|
||||
{:else if peer?.state === 'READY'}
|
||||
<p class="text-sm leading-7">
|
||||
این مقایسه با {formatValue(peer.eligiblePeerCount ?? 0)} کسبوکار واجد شرایط و بدون نمایش اطلاعات هیچ
|
||||
پذیرندهای ساخته شده است.
|
||||
</p>
|
||||
<div class="bg-muted mt-4 h-2 overflow-hidden rounded-sm" dir="ltr">
|
||||
<div
|
||||
class="bg-primary h-full rounded-sm"
|
||||
style={`width:${Math.min(100, (peer.percentileBps ?? 0) / 100)}%`}
|
||||
></div>
|
||||
</div>
|
||||
<p class="text-muted-foreground mt-2 text-xs">
|
||||
جایگاه شما: صدک {formatValue(Math.round((peer.percentileBps ?? 0) / 100))}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-muted-foreground text-sm leading-7">
|
||||
برای یک مقایسه محرمانه و قابل اتکا، تعداد همگروههای واجد شرایط هنوز کافی نیست.
|
||||
</p>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user