didi-lot3-frontend/web/src/components/PipelineAnalysis/sections/AiResults.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

357 lines
13 KiB
TypeScript

import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import styled from '@emotion/styled';
import {
Cpu, ShieldCheck, ShieldAlert, AlertOctagon, AlertTriangle, AlertCircle,
FileText, Image as ImageIcon, Sparkles, ChevronDown, Plus,
} from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import { typography } from '../../../theme';
import { localized } from '../../../utils/i18n-fields';
import { enumLabel } from '../../../utils/i18n-enums';
import type { AiTamperedResult, AiIndicatorDetected } from '../../../types/analysis-session';
import { HeadlineCard } from './HeadlineCard';
import { FindingCard, type FindingAccent } from './FindingCard';
import { SectionDivider } from './SectionDivider';
import {
StatBlock, ChipsRow, Chip,
CollapseBtn, CollapseBtnLeft, CollapseBtnChevron,
} from '../styles';
const probAccent = (p: number): string =>
p >= 80 ? '#ef4444' : p >= 60 ? '#f97316' : p >= 40 ? '#eab308' : '#22c55e';
const probIcon = (p: number): LucideIcon => {
if (p >= 80) return AlertOctagon;
if (p >= 60) return AlertTriangle;
if (p >= 40) return Cpu;
return ShieldCheck;
};
const indicatorAccent = (confidence: number): FindingAccent => {
if (confidence >= 80) return 'critical';
if (confidence >= 60) return 'warning';
if (confidence >= 40) return 'info';
return 'neutral';
};
const verdictLabelKey = (verdict: string): string => {
switch (verdict) {
case 'LIKELY_AI': return 'aiTamper.verdicts.likelyAi';
case 'POSSIBLY_AI': return 'aiTamper.verdicts.possiblyAi';
case 'UNLIKELY_AI': return 'aiTamper.verdicts.unlikelyAi';
case 'LIKELY_HUMAN': return 'aiTamper.verdicts.likelyHuman';
case 'HUMAN': return 'aiTamper.verdicts.humanWritten';
default: return '';
}
};
const CATEGORY_LABEL_KEYS: Record<string, string> = {
T1: 'aiTamper.categories.writingStyle',
T2: 'aiTamper.categories.contentPatterns',
T3: 'aiTamper.categories.structuralAnalysis',
T4: 'aiTamper.categories.statisticalMarkers',
T5: 'aiTamper.categories.explicitSignals',
};
const DEFAULT_VISIBLE_INDICATORS = 5;
/** Display-friendly shape that flattens AiTamperedResult variations across modes. */
export interface AiDisplayResult {
verdict: string;
ai_probability: number;
disclosure_detected?: boolean;
disclosure_explicit?: boolean;
disclosure_text?: string | null;
/** Quick mode text indicators */
indicators_found?: string[];
/** Deep mode text + audio/video indicators */
indicators_detected?: AiIndicatorDetected[];
categories_affected?: string[];
coupling_context?: AiTamperedResult['coupling_context'];
image_indicators?: string[];
image_evidence?: string;
model_used?: string;
transcript?: string;
}
/** Build AiDisplayResult from a raw AiTamperedResult + optional input meta. */
export function toAiDisplayResult(
r: AiTamperedResult,
ctx: { inputType?: string; inputText?: string | null } = {},
): AiDisplayResult {
const out: AiDisplayResult = {
verdict: r.verdict,
ai_probability: r.ai_probability,
disclosure_detected: r.disclosure_detected,
disclosure_explicit: r.disclosure_explicit,
disclosure_text: r.disclosure_text,
};
if (r.indicators_detected && r.indicators_detected.length > 0) {
out.indicators_detected = r.indicators_detected;
out.categories_affected = r.categories_affected;
out.coupling_context = r.coupling_context;
}
if (r.image_analysis?.indicators && r.image_analysis.indicators.length > 0) {
out.image_indicators = r.image_analysis.indicators;
out.image_evidence = r.image_analysis.evidence;
out.model_used = r.image_analysis.model_used;
}
if (ctx.inputText && (ctx.inputType === 'audio' || ctx.inputType === 'video')) {
out.transcript = ctx.inputText;
}
return out;
}
interface Props {
result: AiDisplayResult;
isRo: boolean;
/** When set, a "try deep analysis" CTA is shown inside the clean-content card. Used by the live standalone where the user can re-run in deep mode. History view should omit this. */
onTryDeepScan?: () => void;
}
export const AiResults: React.FC<Props> = ({ result, isRo, onTryDeepScan }) => {
const { t } = useTranslation();
const [indicatorsExpand, setIndicatorsExpand] = useState(false);
const probability = result.ai_probability || 0;
const verdict = result.verdict || '';
const accent = probAccent(probability);
const Icon = probIcon(probability);
const verdictText = verdictLabelKey(verdict)
? t(verdictLabelKey(verdict))
: (verdict || '').replace(/_/g, ' ');
const tldr = (() => {
const indCount = result.indicators_detected?.length
|| result.indicators_found?.length
|| result.image_indicators?.length
|| 0;
const disclosureNote = result.disclosure_detected === true
? (isRo ? ' cu declarație AI' : ' with AI disclosure')
: result.disclosure_detected === false
? (isRo ? ' fără declarație AI' : ' without AI disclosure')
: '';
if (isRo) {
return `Probabilitate ${probability}% de generare AI${disclosureNote}${indCount > 0 ? ` · ${indCount} ${indCount === 1 ? 'indicator detectat' : 'indicatori detectați'}` : ''}.`;
}
return `${probability}% likelihood of AI generation${disclosureNote}${indCount > 0 ? ` · ${indCount} ${indCount === 1 ? 'indicator detected' : 'indicators detected'}` : ''}.`;
})();
const totalIndicators = result.indicators_detected?.length ?? 0;
const visibleCount = indicatorsExpand ? totalIndicators : Math.min(DEFAULT_VISIBLE_INDICATORS, totalIndicators);
const visibleIndicators = result.indicators_detected?.slice(0, visibleCount) ?? [];
const remainingIndicators = totalIndicators - visibleCount;
return (
<>
<HeadlineCard
accent={accent}
score={probability}
scoreLabel="% AI"
eyebrow={isRo ? 'Detectare AI' : 'AI detection'}
icon={Icon}
category={verdictText}
descriptor={
<>
{result.coupling_context?.for_verdict?.confidence_level && (
<>
<span>{isRo ? 'încredere' : 'confidence'} {enumLabel(result.coupling_context.for_verdict.confidence_level).toLowerCase()}</span>
{result.disclosure_detected !== undefined && <span className="sep">·</span>}
</>
)}
{result.disclosure_detected === true && (
<span>{isRo ? 'cu declarație AI' : 'AI disclosure'}</span>
)}
{result.disclosure_detected === false && (
<span>{isRo ? 'fără declarație AI' : 'no AI disclosure'}</span>
)}
</>
}
tldr={tldr}
/>
{(result.coupling_context?.for_verdict || (result.categories_affected && result.categories_affected.length > 0) || result.model_used) && (
<StatBlock>
<ChipsRow>
{result.coupling_context?.for_verdict?.undisclosed_ai && (
<Chip variant="warning">{t('aiTamper.undisclosedAi')}</Chip>
)}
{result.coupling_context?.for_verdict?.needs_manual_review && (
<Chip variant="warning">{t('aiTamper.needsManualReview')}</Chip>
)}
{result.categories_affected?.map(cat => (
<Chip key={cat} variant="violet">
{cat} · {CATEGORY_LABEL_KEYS[cat] ? t(CATEGORY_LABEL_KEYS[cat]) : cat}
</Chip>
))}
{result.model_used && (
<Chip variant="neutral">{result.model_used}</Chip>
)}
</ChipsRow>
</StatBlock>
)}
{result.disclosure_detected !== undefined && (
<FindingCard
icon={result.disclosure_detected ? ShieldCheck : ShieldAlert}
accent={result.disclosure_detected ? 'success' : 'neutral'}
eyebrow={isRo ? 'Declarație AI' : 'AI disclosure'}
headline={
result.disclosure_detected
? (result.disclosure_explicit ? t('aiTamper.explicitDisclosure') : t('aiTamper.implicitDisclosure'))
: t('aiTamper.noDisclosureDetected')
}
quote={result.disclosure_text || undefined}
/>
)}
{result.image_evidence && (
<FindingCard
icon={ImageIcon}
accent="info"
eyebrow={isRo ? 'Analiză imagine' : 'Image analysis'}
headline={t('aiTamper.analysisSummary')}
quote={result.image_evidence}
/>
)}
{result.transcript && (
<FindingCard
icon={FileText}
accent="info"
eyebrow={t('aiTamper.transcript')}
headline={isRo ? 'Conținut transcris' : 'Transcribed content'}
quote={result.transcript.length > 320
? result.transcript.slice(0, 320) + '…'
: result.transcript}
/>
)}
{result.indicators_found && result.indicators_found.length > 0 && (
<>
<SectionDivider
label={isRo ? 'Semnale detectate' : 'Signals detected'}
count={result.indicators_found.length}
/>
{result.indicators_found.map((ind, i) => (
<FindingCard
key={i}
icon={AlertCircle}
accent="info"
headline={ind}
/>
))}
</>
)}
{result.image_indicators && result.image_indicators.length > 0 && (
<>
<SectionDivider
label={t('aiTamper.imageIndicators')}
count={result.image_indicators.length}
/>
{result.image_indicators.map((ind, i) => (
<FindingCard
key={i}
icon={Sparkles}
accent="warning"
headline={ind}
/>
))}
</>
)}
{totalIndicators > 0 && (
<>
<SectionDivider
label={isRo ? 'Indicatori detectați' : 'Indicators detected'}
count={`${totalIndicators} ${isRo ? 'detectați' : 'detected'}${result.categories_affected?.length ? ` · ${result.categories_affected.join(' · ')}` : ''}`}
/>
{visibleIndicators.map(ind => {
const catLabel = CATEGORY_LABEL_KEYS[ind.category]
? t(CATEGORY_LABEL_KEYS[ind.category])
: ind.category;
const eyebrowParts = [
ind.category,
catLabel,
`${isRo ? 'încredere' : 'confidence'} ${ind.confidence}%`,
];
return (
<FindingCard
key={ind.id}
icon={AlertCircle}
accent={indicatorAccent(ind.confidence)}
eyebrow={eyebrowParts.join(' · ')}
headline={localized(ind, 'indicator_name', 'name')}
quote={ind.evidence}
/>
);
})}
{remainingIndicators > 0 && (
<CollapseBtn onClick={() => setIndicatorsExpand(true)}>
<CollapseBtnLeft>
<Plus size={16} strokeWidth={2} />
<span>
{isRo
? `Vezi celelalți ${remainingIndicators} ${remainingIndicators === 1 ? 'indicator' : 'indicatori'}`
: `Show ${remainingIndicators} more ${remainingIndicators === 1 ? 'indicator' : 'indicators'}`}
</span>
</CollapseBtnLeft>
<CollapseBtnChevron expanded={false}>
<ChevronDown size={14} strokeWidth={2.5} />
</CollapseBtnChevron>
</CollapseBtn>
)}
{indicatorsExpand && totalIndicators > DEFAULT_VISIBLE_INDICATORS && (
<CollapseBtn onClick={() => setIndicatorsExpand(false)}>
<CollapseBtnLeft>
<ChevronDown size={16} strokeWidth={2} />
<span>{isRo ? 'Ascunde' : 'Hide'}</span>
</CollapseBtnLeft>
<CollapseBtnChevron expanded={true}>
<ChevronDown size={14} strokeWidth={2.5} />
</CollapseBtnChevron>
</CollapseBtn>
)}
</>
)}
{(result.indicators_found?.length === 0
&& !result.indicators_detected
&& !result.image_indicators?.length) && (
<FindingCard
icon={ShieldCheck}
accent="success"
eyebrow={isRo ? 'Conținut curat' : 'Clean content'}
headline={t('aiTamper.noSignalsQuick')}
source={
onTryDeepScan ? (
<DeepScanHint onClick={onTryDeepScan}>
{t('aiTamper.tryDeepAnalysis')}
</DeepScanHint>
) : undefined
}
/>
)}
</>
);
};
const DeepScanHint = styled.button`
padding: 0;
border: none;
background: none;
font-family: ${typography.fontFamily.primary};
font-size: 12px;
font-weight: ${typography.fontWeight.semibold};
color: var(--accent-text);
cursor: pointer;
text-decoration: underline;
text-underline-offset: 2px;
&:hover {
color: var(--accent-hover);
}
`;