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
357
web/src/components/PipelineAnalysis/sections/AiResults.tsx
Normal file
357
web/src/components/PipelineAnalysis/sections/AiResults.tsx
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
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);
|
||||
}
|
||||
`;
|
||||
Loading…
Add table
Add a link
Reference in a new issue