141 lines
5.9 KiB
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>
|