- Surse complete web (React/Vite) + mobil (React Native/Expo) + extensie (MV3) - Documentație de livrare: ghid utilizare, matrice trasabilitate cerințe, raport testare furnizor - Artefacte binare: imagine Docker didi-frontend:lot3-1.0, APK, extensie v3.2.6 + SHA256SUMS - Configurare adresă platformă externalizată (build args / .env / config.js) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
309 lines
9.1 KiB
TypeScript
309 lines
9.1 KiB
TypeScript
import React from 'react';
|
|
import { View, Text, TouchableOpacity, StyleSheet, ActivityIndicator } from 'react-native';
|
|
import {
|
|
AlertCircle, Cpu, ShieldCheck, ShieldAlert, AlertOctagon, AlertTriangle,
|
|
CheckSquare, HelpCircle, FileText, Image as ImageIcon, Music, Video, Globe, Type,
|
|
} from 'lucide-react-native';
|
|
import type { LucideIcon } from 'lucide-react-native';
|
|
import {
|
|
COLORS,
|
|
SPACING,
|
|
RADIUS,
|
|
FONT_SIZES,
|
|
FONT_WEIGHTS,
|
|
ACCENT,
|
|
getBadgeBgColor,
|
|
} from '../theme/colors';
|
|
import type { AnalysisSession } from '../types/analysis';
|
|
import { useTranslation, getTranslation, enumLabel } from '../i18n';
|
|
|
|
interface RunningItemProgress {
|
|
progress: number;
|
|
completedComponents: string[];
|
|
}
|
|
|
|
interface Props {
|
|
item: AnalysisSession;
|
|
onPress: () => void;
|
|
onLongPress: () => void;
|
|
progress?: RunningItemProgress;
|
|
}
|
|
|
|
function formatRelativeTime(dateStr: string): string {
|
|
const { t } = getTranslation();
|
|
const date = new Date(dateStr);
|
|
const now = new Date();
|
|
const diffMs = now.getTime() - date.getTime();
|
|
const diffMin = Math.floor(diffMs / 60000);
|
|
if (diffMin < 1) return t('justNow');
|
|
if (diffMin < 60) return t('minutesAgo', { min: diffMin });
|
|
const diffHr = Math.floor(diffMin / 60);
|
|
if (diffHr < 24) return t('hoursAgo', { hr: diffHr });
|
|
const diffDay = Math.floor(diffHr / 24);
|
|
if (diffDay < 7) return t('daysAgo', { day: diffDay });
|
|
return date.toLocaleDateString();
|
|
}
|
|
|
|
const TYPE_ICON: Record<string, LucideIcon> = {
|
|
text: Type,
|
|
image: ImageIcon,
|
|
audio: Music,
|
|
video: Video,
|
|
url: Globe,
|
|
};
|
|
|
|
interface CardSummary {
|
|
badgeScore: number;
|
|
label: string;
|
|
badgeColor: string;
|
|
badgeIcon: LucideIcon;
|
|
}
|
|
|
|
/** Decide what to show on the right side of the card based on session type. */
|
|
function summarizeSession(item: AnalysisSession, t: (k: any) => string, isRo: boolean): CardSummary {
|
|
const componentsRun = item.components_run || [];
|
|
const isSingle = componentsRun.length === 1;
|
|
const single = isSingle ? componentsRun[0] : null;
|
|
|
|
if (single === 'domain' || single === 'source_assessment') {
|
|
const trust = (item as any).source_trust_score ?? 0;
|
|
return {
|
|
badgeScore: trust,
|
|
label: (item as any).source_verdict
|
|
? enumLabel((item as any).source_verdict)
|
|
: t('sourceAssessment'),
|
|
badgeColor: trust >= 70 ? '#22c55e' : trust >= 40 ? '#eab308' : trust >= 20 ? '#f97316' : '#ef4444',
|
|
badgeIcon: trust >= 70 ? ShieldCheck : trust >= 40 ? ShieldAlert : AlertOctagon,
|
|
};
|
|
}
|
|
if (single === 'ai_tampered' || single === 'ai-tampered') {
|
|
const score = item.risk_score ?? 0;
|
|
return {
|
|
badgeScore: score,
|
|
label: t('aiDetection'),
|
|
badgeColor: score >= 80 ? '#ef4444' : score >= 60 ? '#f97316' : score >= 40 ? '#eab308' : '#22c55e',
|
|
badgeIcon: score >= 60 ? AlertTriangle : Cpu,
|
|
};
|
|
}
|
|
if (single === 'techniques') {
|
|
const score = item.risk_score ?? 0;
|
|
return {
|
|
badgeScore: score,
|
|
label: t('manipulationTechniques'),
|
|
badgeColor: score >= 70 ? '#ef4444' : score >= 40 ? '#f97316' : score >= 20 ? '#eab308' : '#22c55e',
|
|
badgeIcon: score >= 70 ? AlertOctagon : score >= 40 ? AlertTriangle : ShieldCheck,
|
|
};
|
|
}
|
|
if (single === 'claims') {
|
|
const score = item.risk_score ?? 0;
|
|
return {
|
|
badgeScore: score,
|
|
label: t('claimsVerification'),
|
|
badgeColor: ACCENT.violet,
|
|
badgeIcon: CheckSquare,
|
|
};
|
|
}
|
|
// Pipeline (multi-component or unknown) — use risk_score + risk_category
|
|
const score = item.risk_score ?? 0;
|
|
return {
|
|
badgeScore: score,
|
|
label: item.risk_category
|
|
? enumLabel(item.risk_category)
|
|
: (item.risk_level ? enumLabel(item.risk_level) : t('analysis')),
|
|
badgeColor: score >= 70 ? '#ef4444'
|
|
: score >= 50 ? '#f97316'
|
|
: score >= 30 ? '#eab308'
|
|
: score > 0 ? '#22c55e'
|
|
: ACCENT.violet,
|
|
badgeIcon: score >= 70 ? AlertOctagon : score >= 30 ? AlertTriangle : score > 0 ? ShieldCheck : HelpCircle,
|
|
};
|
|
}
|
|
|
|
export default function HistoryCard({ item, onPress, onLongPress, progress }: Props) {
|
|
const { t, language } = useTranslation();
|
|
const isRo = language === 'ro';
|
|
const TypeIcon = TYPE_ICON[item.input_type || 'text'] || AlertCircle;
|
|
const statusDone = item.status === 'completed';
|
|
const isRunning = item.status === 'running' || item.status === 'pending' || item.status === 'processing';
|
|
|
|
const summary = summarizeSession(item, t, isRo);
|
|
|
|
const a11yLabel = [
|
|
item.input_text || item.input_url || t('mediaAnalysis'),
|
|
item.created_at ? formatRelativeTime(item.created_at) : null,
|
|
statusDone ? `${summary.label}, ${Math.round(summary.badgeScore)}` : item.status,
|
|
]
|
|
.filter(Boolean)
|
|
.join('. ');
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
style={styles.card}
|
|
onPress={onPress}
|
|
onLongPress={onLongPress}
|
|
activeOpacity={0.75}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={a11yLabel}
|
|
>
|
|
{/* Type icon */}
|
|
<View style={styles.typeIconBox}>
|
|
<TypeIcon size={18} color={COLORS.brand.primaryLight} strokeWidth={2} />
|
|
</View>
|
|
|
|
{/* Content */}
|
|
<View style={styles.content}>
|
|
<Text style={styles.inputText} numberOfLines={2}>
|
|
{item.input_text || item.input_url || t('mediaAnalysis')}
|
|
</Text>
|
|
<View style={styles.metaRow}>
|
|
{item.created_at && (
|
|
<Text style={styles.time}>{formatRelativeTime(item.created_at)}</Text>
|
|
)}
|
|
{isRunning && (
|
|
<View style={styles.statusBadge}>
|
|
<Text style={styles.statusText}>
|
|
{progress ? `${Math.round(progress.progress)}%` : item.status}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
{!statusDone && !isRunning && item.status && (
|
|
<View style={styles.statusBadge}>
|
|
<Text style={styles.statusText}>{item.status}</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
{isRunning && progress && progress.progress > 0 && (
|
|
<View style={styles.progressBarBg}>
|
|
<View style={[styles.progressBarFill, { width: `${Math.min(progress.progress, 100)}%` }]} />
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{/* Score / state */}
|
|
<View style={styles.scoreBox}>
|
|
{statusDone ? (
|
|
<>
|
|
<View style={[styles.scoreCircle, { borderColor: summary.badgeColor }]}>
|
|
<Text
|
|
style={[styles.scoreNum, { color: summary.badgeColor }]}
|
|
maxFontSizeMultiplier={1.5}
|
|
>
|
|
{Math.round(summary.badgeScore)}
|
|
</Text>
|
|
</View>
|
|
<View style={[styles.catBadge, { backgroundColor: getBadgeBgColor(summary.badgeColor, 0.16) }]}>
|
|
<summary.badgeIcon size={11} color={summary.badgeColor} strokeWidth={2.4} />
|
|
<Text
|
|
style={[styles.catText, { color: summary.badgeColor }]}
|
|
numberOfLines={1}
|
|
maxFontSizeMultiplier={1.5}
|
|
>
|
|
{summary.label}
|
|
</Text>
|
|
</View>
|
|
</>
|
|
) : (
|
|
<ActivityIndicator size="small" color={COLORS.brand.primaryLight} />
|
|
)}
|
|
</View>
|
|
</TouchableOpacity>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
card: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: 'rgba(255,255,255,0.025)',
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(255,255,255,0.06)',
|
|
borderRadius: RADIUS.xl,
|
|
padding: 14,
|
|
marginBottom: SPACING.sm + 2,
|
|
},
|
|
typeIconBox: {
|
|
width: 38,
|
|
height: 38,
|
|
borderRadius: RADIUS.lg,
|
|
backgroundColor: 'rgba(167,139,250,0.10)',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
marginRight: 12,
|
|
},
|
|
content: {
|
|
flex: 1,
|
|
marginRight: SPACING.sm,
|
|
},
|
|
inputText: {
|
|
color: COLORS.text.primary,
|
|
fontSize: FONT_SIZES.sm,
|
|
lineHeight: 19,
|
|
marginBottom: 4,
|
|
},
|
|
metaRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
},
|
|
time: {
|
|
color: COLORS.text.muted,
|
|
fontSize: FONT_SIZES.xs,
|
|
},
|
|
statusBadge: {
|
|
marginLeft: SPACING.sm,
|
|
paddingHorizontal: SPACING.sm,
|
|
paddingVertical: 1,
|
|
borderRadius: RADIUS.full,
|
|
backgroundColor: 'rgba(255,255,255,0.06)',
|
|
},
|
|
statusText: {
|
|
color: COLORS.status.yellow,
|
|
fontSize: FONT_SIZES.xs - 1,
|
|
textTransform: 'capitalize',
|
|
},
|
|
progressBarBg: {
|
|
width: '100%',
|
|
height: 3,
|
|
borderRadius: 1.5,
|
|
backgroundColor: 'rgba(255,255,255,0.08)',
|
|
overflow: 'hidden',
|
|
marginTop: SPACING.xs,
|
|
},
|
|
progressBarFill: {
|
|
height: '100%',
|
|
borderRadius: 1.5,
|
|
backgroundColor: COLORS.brand.primary,
|
|
},
|
|
scoreBox: {
|
|
alignItems: 'center',
|
|
minWidth: 64,
|
|
gap: 4,
|
|
},
|
|
scoreCircle: {
|
|
width: 44,
|
|
height: 44,
|
|
borderRadius: 22,
|
|
borderWidth: 2,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
scoreNum: {
|
|
fontSize: 16,
|
|
fontWeight: FONT_WEIGHTS.bold,
|
|
letterSpacing: -0.4,
|
|
},
|
|
catBadge: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 3,
|
|
paddingHorizontal: 6,
|
|
paddingVertical: 2,
|
|
borderRadius: 6,
|
|
maxWidth: 90,
|
|
},
|
|
catText: {
|
|
fontSize: 9.5,
|
|
fontWeight: FONT_WEIGHTS.semibold,
|
|
letterSpacing: 0.4,
|
|
textTransform: 'uppercase',
|
|
},
|
|
});
|