Files
zarinpal-analytics-frontend/src/lib/components/AIInsightPanel.svelte

141 lines
5.9 KiB
Svelte

<script lang="ts">
import RefreshCcwIcon from '@lucide/svelte/icons/refresh-ccw';
import SparklesIcon from '@lucide/svelte/icons/sparkles';
import { ApiError, api, type AIInsight, type AIInsightModule } from '$lib/api/client';
import { Badge } from '$lib/components/ui/badge';
import { Button } from '$lib/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
import { Skeleton } from '$lib/components/ui/skeleton';
import { useDashboardContext } from '$lib/dashboard-context.svelte';
let {
module,
terminalId,
periodId,
periodLabel,
}: { module: AIInsightModule; terminalId?: string; periodId?: string; periodLabel?: string } = $props();
const dashboard = useDashboardContext();
let insight = $state<AIInsight | null>(null);
let loading = $state(true);
let refreshing = $state(false);
let error = $state('');
let sequence = 0;
let activeScope = '';
const pageTitles: Record<AIInsightModule, string> = {
overview: 'پیشخوان',
payments: 'عملکرد پرداخت',
fees: 'کارمزدها',
peers: 'هم‌گروه‌ها',
transactions: 'تراکنش‌ها',
growth: 'مربی رشد',
};
async function load(
requestedModule: AIInsightModule,
requestedTerminalId?: string,
requestedPeriodId?: string,
) {
const current = ++sequence;
error = '';
try {
const next = await api.latestAIInsight(requestedModule, requestedTerminalId, requestedPeriodId);
if (current === sequence) insight = next;
} catch (cause) {
if (current !== sequence) return;
error =
cause instanceof ApiError && cause.status === 404
? 'هنوز تحلیلی برای این بازه آماده نشده است.'
: 'بخش تحلیل هوشمند در دسترس نیست؛ سایر اطلاعات صفحه همچنان قابل استفاده‌اند.';
} finally {
if (current === sequence) loading = false;
}
}
async function refresh() {
if (!dashboard.session || refreshing) return;
refreshing = true;
error = '';
try {
const job = await api.refreshAIInsight(module, dashboard.session.csrfToken, terminalId, periodId);
for (let attempt = 0; attempt < 45; attempt += 1) {
await new Promise((resolve) => setTimeout(resolve, 2000));
const current = await api.aiInsightJob(job.jobId);
if (current.state === 'COMPLETED') {
await load(module, terminalId, periodId);
return;
}
if (current.state === 'FAILED') throw new Error('job failed');
}
error = 'ساخت نسخه تازه ادامه دارد؛ خلاصه قبلی همچنان نمایش داده می‌شود.';
} catch (cause) {
error =
cause instanceof ApiError && cause.status === 429
? 'سهم به‌روزرسانی امروز استفاده شده است؛ خلاصه قبلی همچنان نمایش داده می‌شود.'
: 'خلاصه تازه آماده نشد؛ اطلاعات دیگر صفحه بدون تغییر باقی ماند.';
} finally {
refreshing = false;
}
}
$effect(() => {
const requestedModule = module;
const requestedTerminalId = terminalId;
const requestedPeriodId = periodId;
const scope = `${requestedModule}\u0000${requestedTerminalId ?? ''}\u0000${requestedPeriodId ?? ''}`;
if (scope === activeScope) return;
activeScope = scope;
insight = null;
loading = true;
void load(requestedModule, requestedTerminalId, requestedPeriodId);
});
</script>
<section class="my-4" aria-label="تحلیل هوشمند" aria-busy={loading || refreshing}>
{#if loading && !insight}
<Card class="rounded-lg">
<CardContent class="space-y-3 p-5" role="status" aria-label="در حال دریافت تحلیل هوشمند">
<Skeleton class="h-6 w-44" />
<Skeleton class="h-16 w-full" />
</CardContent>
</Card>
{:else if !insight}
<Card class="rounded-lg border-dashed">
<CardContent class="flex flex-col gap-4 p-5 sm:flex-row sm:items-center sm:justify-between">
<div>
<strong>خلاصه هوشمند</strong>
<div class="text-muted-foreground mt-1 text-sm" role="status">{error}</div>
</div>
<Button variant="outline" onclick={refresh} disabled={refreshing}>
<RefreshCcwIcon class={refreshing ? 'animate-spin' : ''} /> ساخت خلاصه
</Button>
</CardContent>
</Card>
{:else}
<Card class="border-primary/20 overflow-hidden rounded-lg shadow-xs">
<CardHeader class="bg-primary/[.035] -mt-4 border-b pt-4">
<div class="flex min-h-6 flex-wrap items-center justify-between gap-3">
<CardTitle class="flex h-6 items-center gap-2 text-base leading-none">
<SparklesIcon class="text-primary size-5" /> خلاصه‌ای از {pageTitles[module]}
</CardTitle>
{#if periodLabel}<Badge class="h-6 leading-none" variant="secondary">{periodLabel}</Badge>{/if}
</div>
</CardHeader>
<CardContent class="space-y-4 p-5">
<p class="text-sm leading-8">{insight.copy.latestObserved.body}</p>
<p class="text-muted-foreground text-sm leading-8">{insight.copy.past.body}</p>
<div class="flex flex-col gap-2 border-t pt-4 sm:flex-row sm:items-center sm:justify-between">
<span class="text-muted-foreground text-xs" aria-live="polite">
{error ||
(refreshing ? 'خلاصه تازه در حال آماده‌شدن است.' : 'این خلاصه برای کمک به تصمیم‌گیری است.')}
</span>
<Button variant="outline" size="sm" onclick={refresh} disabled={refreshing}>
<RefreshCcwIcon class={refreshing ? 'animate-spin' : ''} /> به‌روزرسانی خلاصه
</Button>
</div>
</CardContent>
</Card>
{/if}
</section>