didi-lot3-frontend/android/didi-app-source/src/components/HistoryCard.tsx
Top Clossers cec967f953 Livrare Lot 3 (Frontend): aplicație web, aplicație mobilă Android, extensie browser
- 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>
2026-07-17 12:40:40 +03:00

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',
},
});