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>
This commit is contained in:
commit
cec967f953
321 changed files with 80506 additions and 0 deletions
309
android/didi-app-source/src/components/HistoryCard.tsx
Normal file
309
android/didi-app-source/src/components/HistoryCard.tsx
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
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',
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue