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
162
web/src/components/PipelineAnalysis/details/claims.tsx
Normal file
162
web/src/components/PipelineAnalysis/details/claims.tsx
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
import React from 'react';
|
||||
import { XCircle, CheckCircle2, HelpCircle, Plus, ChevronDown } from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { localized } from '../../../utils/i18n-fields';
|
||||
import { enumLabel } from '../../../utils/i18n-enums';
|
||||
import { FindingCard, type FindingAccent } from '../sections/FindingCard';
|
||||
import { SectionDivider } from '../sections/SectionDivider';
|
||||
import {
|
||||
CollapseBtn, CollapseBtnLeft, CollapseBtnChevron,
|
||||
StancePill, FindingSourceLink,
|
||||
} from '../styles';
|
||||
|
||||
const DEFAULT_VISIBLE = 4;
|
||||
|
||||
function claimIcon(status: string, statusColor?: string): LucideIcon {
|
||||
if (status === 'verified_false' || statusColor === 'red') return XCircle;
|
||||
if (status === 'verified_true' || statusColor === 'green') return CheckCircle2;
|
||||
return HelpCircle;
|
||||
}
|
||||
|
||||
function claimAccent(status: string, statusColor?: string): FindingAccent {
|
||||
if (status === 'verified_false' || statusColor === 'red') return 'critical';
|
||||
if (status === 'verified_true' || statusColor === 'green') return 'success';
|
||||
return 'neutral';
|
||||
}
|
||||
|
||||
function stanceFromString(s: string): 'contradicts' | 'supports' | 'neutral' {
|
||||
const u = (s || '').toUpperCase();
|
||||
if (u === 'CONTRADICTS') return 'contradicts';
|
||||
if (u === 'SUPPORTS') return 'supports';
|
||||
return 'neutral';
|
||||
}
|
||||
|
||||
function sourceHostname(url: string): string {
|
||||
return (url || '').replace(/^https?:\/\/(www\.)?/, '').split('/')[0];
|
||||
}
|
||||
|
||||
export function renderClaimsEditorial(
|
||||
data: any,
|
||||
isRo: boolean,
|
||||
expanded: boolean,
|
||||
onToggle: () => void,
|
||||
) {
|
||||
if (!data) return null;
|
||||
const claims = data.claims_verified || data.claims || [];
|
||||
if (claims.length === 0) return null;
|
||||
|
||||
const total = data.total_claims || claims.length;
|
||||
const trueCount = data.verified_true || 0;
|
||||
const falseCount = data.verified_false || 0;
|
||||
const unverified = data.unverified || 0;
|
||||
|
||||
const summaryParts = [
|
||||
`${total} ${isRo ? 'total' : 'total'}`,
|
||||
trueCount > 0 ? `${trueCount} ${isRo ? 'adevărate' : 'true'}` : null,
|
||||
falseCount > 0 ? `${falseCount} ${isRo ? 'false' : 'false'}` : null,
|
||||
unverified > 0 ? `${unverified} ${isRo ? 'neverificate' : 'unverified'}` : null,
|
||||
].filter(Boolean).join(' · ');
|
||||
|
||||
const visible = expanded ? claims.length : Math.min(DEFAULT_VISIBLE, claims.length);
|
||||
const shown = claims.slice(0, visible);
|
||||
const remaining = claims.length - visible;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionDivider
|
||||
label={isRo ? 'Verificarea afirmațiilor' : 'Claim verification'}
|
||||
count={summaryParts}
|
||||
/>
|
||||
{shown.map((claim: any, i: number) => {
|
||||
const icon = claimIcon(claim.status, claim.status_color);
|
||||
const accent = claimAccent(claim.status, claim.status_color);
|
||||
const statusName = localized(claim, 'status_name') || enumLabel(claim.status || '');
|
||||
const typeName = claim.type_name || enumLabel(claim.type || '');
|
||||
const eyebrowParts = [
|
||||
statusName,
|
||||
typeName,
|
||||
claim.priority ? `${isRo ? 'prioritate' : 'priority'} ${claim.priority}` : null,
|
||||
].filter(Boolean);
|
||||
const sources = (claim.sources || []).slice(0, 6);
|
||||
return (
|
||||
<FindingCard
|
||||
key={claim.id || i}
|
||||
icon={icon}
|
||||
accent={accent}
|
||||
eyebrow={eyebrowParts.join(' · ')}
|
||||
headline={claim.text}
|
||||
quote={claim.reasoning}
|
||||
source={sources.length > 0 ? (
|
||||
<>
|
||||
{(() => {
|
||||
const dominantStance = sources[0]?.stance ? stanceFromString(sources[0].stance) : 'neutral';
|
||||
return (
|
||||
<StancePill stance={dominantStance}>
|
||||
{isRo
|
||||
? (dominantStance === 'contradicts' ? 'Contrazice' : dominantStance === 'supports' ? 'Susține' : 'Neutru')
|
||||
: (dominantStance === 'contradicts' ? 'Contradicts' : dominantStance === 'supports' ? 'Supports' : 'Neutral')}
|
||||
</StancePill>
|
||||
);
|
||||
})()}
|
||||
{sources.map((src: any, idx: number) => (
|
||||
<React.Fragment key={idx}>
|
||||
{idx > 0 && <span className="sep">·</span>}
|
||||
<FindingSourceLink href={src.url} target="_blank" rel="noopener noreferrer" onClick={e => e.stopPropagation()}>
|
||||
{sourceHostname(src.url)}
|
||||
</FindingSourceLink>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
) : undefined}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{remaining > 0 && (
|
||||
<CollapseBtn onClick={onToggle}>
|
||||
<CollapseBtnLeft>
|
||||
<Plus size={16} strokeWidth={2} />
|
||||
<span>
|
||||
{isRo
|
||||
? `Vezi celelalte ${remaining} ${remaining === 1 ? 'afirmație' : 'afirmații'}`
|
||||
: `Show ${remaining} more ${remaining === 1 ? 'claim' : 'claims'}`}
|
||||
</span>
|
||||
</CollapseBtnLeft>
|
||||
<CollapseBtnChevron expanded={false}>
|
||||
<ChevronDown size={14} strokeWidth={2.5} />
|
||||
</CollapseBtnChevron>
|
||||
</CollapseBtn>
|
||||
)}
|
||||
{expanded && claims.length > DEFAULT_VISIBLE && (
|
||||
<CollapseBtn onClick={onToggle}>
|
||||
<CollapseBtnLeft>
|
||||
<ChevronDown size={16} strokeWidth={2} />
|
||||
<span>{isRo ? 'Ascunde' : 'Hide'}</span>
|
||||
</CollapseBtnLeft>
|
||||
<CollapseBtnChevron expanded={true}>
|
||||
<ChevronDown size={14} strokeWidth={2.5} />
|
||||
</CollapseBtnChevron>
|
||||
</CollapseBtn>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderClaimsDetail(data: any, _t: any) {
|
||||
const isRo = (typeof document !== 'undefined' && document.documentElement.lang || '').toLowerCase().startsWith('ro');
|
||||
return <RenderClaimsStatic data={data} isRo={isRo} />;
|
||||
}
|
||||
|
||||
const RenderClaimsStatic: React.FC<{ data: any; isRo: boolean }> = ({ data, isRo }) => {
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
return <>{renderClaimsEditorial(data, isRo, expanded, () => setExpanded(o => !o))}</>;
|
||||
};
|
||||
|
||||
export function renderClaimsSummary(data: any, t: any) {
|
||||
if (!data) return null;
|
||||
return (
|
||||
<span style={{ fontSize: 12, color: 'var(--fg-secondary)' }}>
|
||||
{data.total_claims || 0} {t('pipeline.claimsLabel')}
|
||||
{data.verified_false > 0 ? ` · ${data.verified_false} ${t('pipeline.falseLabel')}` : ''}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue