Files
Investidor-V2/resources/js/pages/ai/result.tsx
T
nala 1106509116
tests / ci (push) Canceled after 0s
vrsao 2
2026-10-08 14:02:01 +02:00

325 lines
14 KiB
TypeScript

import { displayValue } from '@/lib/display-value';
import { Link, useForm } from '@inertiajs/react';
import { Management, Field, control, button } from '../management/shared';
import { capabilities, states } from './labels';
type Item = {
[key: string]: unknown;
content?: string;
title?: string;
source_url?: string;
source_date?: string;
nature?: string;
record_id?: string | number;
rule?: string;
score?: number;
score_max?: number;
favourable_factors?: string[];
missing_data?: string[];
};
type Result = {
items: Item[];
mode: string;
language: string;
computed_at: string;
status: string;
};
type Props = {
run: {
id: string;
capability: string;
mode: string;
rules_version: string;
prompt_version: string;
latency_ms: number;
version_no: number;
review_status: string;
};
result: Result | null;
unavailable: boolean;
};
const safeLink = (value: string) =>
/^https:\/\//i.test(value) || /^\/(?!\/)/.test(value);
export default function ResultPage({ run, result, unavailable }: Props) {
const en = result?.language === 'en';
const t = (pt: string, english: string) => (en ? english : pt);
const feedback = useForm({ rating: 'helpful', review_requested: true });
const review = useForm({ status: 'accepted', version_no: run.version_no });
return (
<Management
title={
capabilities[run.capability]?.[en ? 1 : 0] ??
t('Resultado assistivo', 'Assistive result')
}
>
<Link className="underline" href="/management/ai">
{t(
'Voltar às consultas e fontes',
'Back to assistance and sources',
)}
</Link>
<p>
{t(
'Resultado assistivo, sujeito a revisão. Não constitui decisão oficial ou garantia de retorno ou aprovação.',
'Assistive result requiring review. Not an official decision or a guarantee of return or approval.',
)}
</p>
<p>
{t(
'Confiança não calibrada. Não foram produzidas previsões ou afirmações por um modelo generativo.',
'Confidence is not calibrated. No forecasts or claims were generated by a generative model.',
)}
</p>
<p>
{t('Estado da revisão', 'Review status')}:{' '}
{en ? run.review_status : states[run.review_status]}
</p>
{unavailable ? (
<p role="alert">
{t(
'Conteúdo indisponível: capacidade suspensa, fonte alterada, expirada, revogada ou sem consentimento vigente.',
'Content unavailable: suspended capability, changed, expired or revoked source, or withdrawn consent.',
)}
</p>
) : (
<>
{result?.items.length === 0 && (
<p className="rounded border p-4">
{t(
'Não há evidência ou correspondências suficientes nesta consulta. Não é possível formular uma conclusão. Pode solicitar revisão humana abaixo.',
'No sufficient evidence or matches for this query. A conclusion cannot be provided. You may request human review below.',
)}
</p>
)}
{run.mode === 'extractive_preview' && (
<p>
{t(
'Pré-visualização dos primeiros excertos; não é um resumo completo do documento.',
'Preview of the first excerpts; not a complete document summary.',
)}
</p>
)}
{result?.items.map((item, index) => (
<article
className="space-y-3 rounded border p-4"
key={index}
>
<h2 className="text-lg font-semibold">
{item.title ??
item.rule?.replaceAll('_', ' ') ??
`${t('Resultado', 'Result')} ${index + 1}`}
</h2>
{item.content && (
<blockquote className="border-l-4 pl-4 whitespace-pre-wrap">
{item.content}
</blockquote>
)}
{item.source_url && safeLink(item.source_url) && (
<a
className="underline"
href={item.source_url}
rel="noreferrer"
>
{t(
'Consultar fonte / registo original',
'View source / original record',
)}
</a>
)}
{item.source_date && (
<p>
{t('Data da fonte', 'Source date')}:{' '}
{item.source_date}
</p>
)}
{item.nature && (
<p>
{t('Natureza declarada', 'Declared nature')}
: {en ? item.nature : states[item.nature]}
</p>
)}
{item.score !== undefined && (
<p>
{t(
'Filtros declarados correspondentes',
'Matching declared filters',
)}
: {item.score}/{item.score_max}.{' '}
{t(
'Esta pontuação não mede rentabilidade ou probabilidade de aprovação.',
'This score does not measure profitability or likelihood of approval.',
)}
</p>
)}
{item.favourable_factors && (
<p>
{t(
'Factores correspondentes',
'Matching factors',
)}
: {item.favourable_factors.join(', ')}
</p>
)}
{item.missing_data && (
<p>
{t(
'Dados ainda necessários',
'Missing information',
)}
: {item.missing_data.join(', ')}
</p>
)}
{!item.content && (
<dl className="grid gap-2 md:grid-cols-2">
{Object.entries(item)
.filter(
([key]) =>
![
'title',
'source_url',
'source_date',
'score',
'score_max',
'score_meaning',
'favourable_factors',
'missing_data',
'semantic_similarity',
].includes(key),
)
.map(([key, value]) => (
<div key={key}>
<dt className="font-medium">
{key.replaceAll('_', ' ')}
</dt>
<dd>
{Array.isArray(value)
? value.join(', ')
: value === null
? t(
'Não determinado',
'Undetermined',
)
: displayValue(value)}
</dd>
</div>
))}
</dl>
)}
</article>
))}
</>
)}
<p>
{t('Regras', 'Rules')}: {run.rules_version} ·{' '}
{t('Calculado em', 'Computed at')}: {result?.computed_at ?? '—'}{' '}
· {run.latency_ms} ms
</p>
<form
className="space-y-3 rounded border p-4"
onSubmit={(e) => {
e.preventDefault();
feedback.post(`/management/ai/runs/${run.id}/feedback`);
}}
>
<h2 className="text-xl font-semibold">
{t(
'Feedback e atendimento humano',
'Feedback and human review',
)}
</h2>
<Field label={t('Avaliação', 'Rating')}>
<select
className={control}
value={feedback.data.rating}
onChange={(e) =>
feedback.setData('rating', e.target.value)
}
>
<option value="helpful">{t('Útil', 'Helpful')}</option>
<option value="unhelpful">
{t('Pouco útil', 'Unhelpful')}
</option>
<option value="unsafe">
{t(
'Risco detectado — suspender capacidade',
'Risk detected — suspend capability',
)}
</option>
</select>
</Field>
<label className="flex gap-2">
<input
type="checkbox"
checked={feedback.data.review_requested}
onChange={(e) =>
feedback.setData(
'review_requested',
e.target.checked,
)
}
/>
{t(
'Solicitar revisão por uma pessoa autorizada',
'Request review by an authorised person',
)}
</label>
{Object.entries(feedback.errors).map(([k, v]) => (
<p role="alert" key={k}>
{v}
</p>
))}
<button disabled={feedback.processing} className={button}>
{t('Enviar feedback', 'Submit feedback')}
</button>
</form>
<form
className="space-y-3 rounded border p-4"
onSubmit={(e) => {
e.preventDefault();
review.transform((data) => ({
...data,
version_no: run.version_no,
}));
review.post(`/management/ai/runs/${run.id}/review`);
}}
>
<h2 className="text-xl font-semibold">
{t('Revisão independente', 'Independent review')}
</h2>
<p>
{t(
'Uma pessoa diferente deve rever o resultado. Aceitar não altera registos oficiais nem envia conteúdo ao CRM.',
'A different person must review the result. Acceptance does not change official records or send content to CRM.',
)}
</p>
<Field label={t('Parecer interno', 'Internal review')}>
<select
className={control}
value={review.data.status}
onChange={(e) =>
review.setData('status', e.target.value)
}
>
<option value="accepted">
{t(
'Aceitar para análise interna',
'Accept for internal analysis',
)}
</option>
<option value="rejected">
{t('Rejeitar', 'Reject')}
</option>
</select>
</Field>
{Object.entries(review.errors).map(([k, v]) => (
<p role="alert" key={k}>
{v}
</p>
))}
<button disabled={review.processing} className={button}>
{t('Registar revisão', 'Record review')}
</button>
</form>
</Management>
);
}