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

604 lines
25 KiB
TypeScript

import { Link, router, useForm } from '@inertiajs/react';
import { useState } from 'react';
import { Management, Field, control, button } from '../management/shared';
import { capabilities, purposes, states } from './labels';
import { ProvinceSelect } from '@/components/investment-territory';
type Source = {
id: string;
title: string;
language: string;
status: string;
nature: string;
valid_until: string;
};
type Props = {
controls: { capability: string; suspended: boolean }[];
sources: Source[];
documents: { id: string; file_name: string }[];
runs: {
id: string;
capability: string;
review_status: string;
review_requested: boolean;
created_at: string;
}[];
today: string;
};
export default function Index({
controls,
sources,
documents,
runs,
today,
}: Props) {
const [language, setLanguage] = useState<'pt' | 'en'>('pt');
const t = (pt: string, en: string) => (language === 'pt' ? pt : en);
const query = useForm({
capability: 'assistant',
language: 'pt',
purpose: 'investment_information',
query: '',
source_id: '',
interest_id: '',
province: '',
maturity_level: '',
});
const upload = useForm<{ file: File | null; authorised: boolean }>({
file: null,
authorised: false,
});
const source = useForm({
document_id: '',
title: '',
language: 'pt',
nature: 'pending',
source_url: '',
source_owner: '',
evidence_reference: '',
source_date: today,
valid_until: '',
authorised: false,
});
const [actionError, setActionError] = useState('');
const post = (url: string, data: Record<string, string | boolean>) => {
setActionError('');
router.post(url, data, {
onError: (errors) =>
setActionError(Object.values(errors).join(' ')),
});
};
return (
<Management
title={t(
'Assistência, fontes e revisão',
'Assistance, sources and review',
)}
>
<Field label="Idioma / Language">
<select
className={control}
value={language}
onChange={(e) => setLanguage(e.target.value as 'pt' | 'en')}
>
<option value="pt">Português</option>
<option value="en">English</option>
</select>
</Field>
<p className="rounded border p-4">
{t(
'Ferramentas assistivas com fontes e regras. Não há modelo generativo activo. Os resultados não são decisões oficiais nem garantias de retorno ou aprovação.',
'Assistive tools using sources and rules. No generative model is active. Results are not official decisions or guarantees of return or approval.',
)}
</p>
<form
className="grid gap-4 md:grid-cols-2"
onSubmit={(e) => {
e.preventDefault();
query.transform((data) => ({
...data,
language,
purpose:
purposes[data.capability] ??
'investment_information',
}));
query.post('/management/ai/runs');
}}
>
<Field label={t('Capacidade', 'Capability')}>
<select
className={control}
value={query.data.capability}
onChange={(e) =>
query.setData('capability', e.target.value)
}
>
{controls.map((c) => (
<option
key={c.capability}
value={c.capability}
disabled={c.suspended}
>
{
capabilities[c.capability]?.[
language === 'pt' ? 0 : 1
]
}
{c.suspended
? t(' (suspensa)', ' (suspended)')
: ''}
</option>
))}
</select>
</Field>
{['assistant', 'search'].includes(query.data.capability) && (
<Field
label={t(
'Pergunta ou termos de pesquisa',
'Question or search terms',
)}
>
<textarea
required
maxLength={1000}
minLength={3}
className={control}
value={query.data.query}
onChange={(e) =>
query.setData('query', e.target.value)
}
/>
</Field>
)}
{['assistant', 'search', 'summary'].includes(
query.data.capability,
) && (
<Field label={t('Fonte autorizada', 'Authorised source')}>
<select
required={query.data.capability === 'summary'}
className={control}
value={query.data.source_id}
onChange={(e) =>
query.setData('source_id', e.target.value)
}
>
<option value="">
{t(
'Todas as fontes elegíveis',
'All eligible sources',
)}
</option>
{sources
.filter((s) => s.status === 'approved')
.map((s) => (
<option key={s.id} value={s.id}>
{s.title}
</option>
))}
</select>
</Field>
)}
{query.data.capability === 'classify' && (
<Field
label={t(
'Número interno do pedido (requer consentimento específico)',
'Internal request number (specific consent required)',
)}
>
<input
required
type="number"
min={1}
className={control}
value={query.data.interest_id}
onChange={(e) =>
query.setData('interest_id', e.target.value)
}
/>
</Field>
)}
{query.data.capability === 'recommend' && (
<>
<Field
label={t(
'Província declarada',
'Declared province',
)}
>
<ProvinceSelect
value={query.data.province}
onChange={(province) =>
query.setData('province', province)
}
className={control}
placeholder={t(
'Seleccione uma província',
'Select a province',
)}
label={t(
'Província declarada',
'Declared province',
)}
/>
</Field>
<Field
label={t(
'Maturidade pretendida',
'Desired maturity',
)}
>
<select
className={control}
value={query.data.maturity_level}
onChange={(e) =>
query.setData(
'maturity_level',
e.target.value,
)
}
>
<option value="">
{t('Não indicada', 'Not specified')}
</option>
{[1, 2, 3, 4, 5, 6].map((n) => (
<option key={n}>{n}</option>
))}
</select>
</Field>
</>
)}
<div>
{Object.entries(query.errors).map(([k, v]) => (
<p role="alert" key={k}>
{v}
</p>
))}
<button
disabled={
query.processing ||
controls.find(
(c) => c.capability === query.data.capability,
)?.suspended
}
className={button}
>
{t('Consultar', 'Run assistance')}
</button>
</div>
</form>
<section className="space-y-3">
<h2 className="text-xl font-semibold">
{t('Suspensão imediata', 'Immediate suspension')}
</h2>
{actionError && <p role="alert">{actionError}</p>}
<div className="flex flex-wrap gap-3">
{controls.map((c) => (
<button
className={button}
key={c.capability}
onClick={() =>
post(
`/management/ai/controls/${c.capability}`,
{
suspended: !c.suspended,
},
)
}
>
{t(
c.suspended ? 'Retomar' : 'Suspender',
c.suspended ? 'Resume' : 'Suspend',
)}{' '}
·{' '}
{
capabilities[c.capability]?.[
language === 'pt' ? 0 : 1
]
}
</button>
))}
</div>
</section>
<section className="space-y-3">
<h2 className="text-xl font-semibold">
{t(
'Resultados recentes e revisão humana',
'Recent results and human review',
)}
</h2>
{runs.length === 0 && (
<p>{t('Ainda sem consultas.', 'No runs yet.')}</p>
)}
{runs.map((r) => (
<p key={r.id}>
<Link
className="underline"
href={`/management/ai/runs/${r.id}`}
>
{
capabilities[r.capability]?.[
language === 'pt' ? 0 : 1
]
}{' '}
· {r.created_at}
</Link>{' '}
—{' '}
{language === 'pt'
? states[r.review_status]
: r.review_status}
{r.review_requested &&
t(' · revisão solicitada', ' · review requested')}
</p>
))}
</section>
<section className="space-y-3">
<h2 className="text-xl font-semibold">
{t('Fontes documentais', 'Document sources')}
</h2>
<p>
{t(
'Apenas ficheiros de texto UTF-8 do cadastro documental privado, activos e não confidenciais. A autorização de uso deve ser confirmada por outra pessoa.',
'Only active, non-confidential UTF-8 text files from the private document registry. Another person must confirm permission to use the source.',
)}
</p>
{sources.map((s) => (
<article
className="space-y-2 rounded border p-3"
key={s.id}
>
<p>
{s.title} · {s.language.toUpperCase()} ·{' '}
{language === 'pt' ? states[s.status] : s.status} ·{' '}
{s.valid_until}
</p>
<p>{language === 'pt' ? states[s.nature] : s.nature}</p>
<div className="flex gap-3">
<button
className={button}
onClick={() =>
post(
`/management/ai/sources/${s.id}/review`,
{
status: 'approved',
expected_status: s.status,
},
)
}
>
{t(
'Confirmar autorização',
'Confirm authorisation',
)}
</button>
<button
className={button}
onClick={() =>
post(
`/management/ai/sources/${s.id}/review`,
{
status: 'revoked',
expected_status: s.status,
},
)
}
>
{t('Revogar', 'Revoke')}
</button>
</div>
</article>
))}
</section>
<details className="rounded border p-4">
<summary>
{t(
'Adicionar documento de texto autorizado',
'Add an authorised text document',
)}
</summary>
<form
className="mt-4 space-y-3"
onSubmit={(e) => {
e.preventDefault();
upload.post('/management/ai/documents', {
forceFormData: true,
onSuccess: () => upload.reset(),
});
}}
>
<Field
label={t(
'Ficheiro .txt UTF-8 (até 195 KiB)',
'UTF-8 .txt file (up to 195 KiB)',
)}
>
<input
required
type="file"
accept=".txt,text/plain"
className={control}
onChange={(e) =>
upload.setData(
'file',
e.target.files?.[0] ?? null,
)
}
/>
</Field>
<label className="flex gap-2">
<input
required
type="checkbox"
checked={upload.data.authorised}
onChange={(e) =>
upload.setData('authorised', e.target.checked)
}
/>
{t(
'Tenho autorização e confirmei a ausência de dados confidenciais ou pessoais desnecessários.',
'I am authorised and have confirmed there is no confidential or unnecessary personal data.',
)}
</label>
{Object.entries(upload.errors).map(([key, value]) => (
<p role="alert" key={key}>
{value}
</p>
))}
<button className={button} disabled={upload.processing}>
{t('Guardar documento', 'Save document')}
</button>
</form>
</details>
<details className="rounded border p-4">
<summary>
{t(
'Registar fonte existente para revisão',
'Register an existing source for review',
)}
</summary>
<form
className="mt-4 grid gap-4 md:grid-cols-2"
onSubmit={(e) => {
e.preventDefault();
source.post('/management/ai/sources', {
onSuccess: () => source.reset(),
});
}}
>
<Field label={t('Documento', 'Document')}>
<select
required
className={control}
value={source.data.document_id}
onChange={(e) =>
source.setData('document_id', e.target.value)
}
>
<option value="">
{t('Seleccionar', 'Select')}
</option>
{documents.map((d) => (
<option key={d.id} value={d.id}>
{d.file_name}
</option>
))}
</select>
</Field>
{(
[
'title',
'source_url',
'source_owner',
'evidence_reference',
'source_date',
'valid_until',
] as const
).map((key) => (
<Field
key={key}
label={
{
title: t('Título', 'Title'),
source_url: t(
'Fonte HTTPS',
'HTTPS source',
),
source_owner: t(
'Entidade proprietária',
'Source owner',
),
evidence_reference: t(
'Referência da evidência e autorização',
'Evidence and authorisation reference',
),
source_date: t(
'Data da fonte',
'Source date',
),
valid_until: t('Válida até', 'Valid until'),
}[key]
}
>
<input
required
type={
['source_date', 'valid_until'].includes(key)
? 'date'
: key === 'source_url'
? 'url'
: 'text'
}
className={control}
value={source.data[key]}
onChange={(e) =>
source.setData(key, e.target.value)
}
/>
</Field>
))}
<Field label={t('Idioma da fonte', 'Source language')}>
<select
className={control}
value={source.data.language}
onChange={(e) =>
source.setData('language', e.target.value)
}
>
<option value="pt">Português</option>
<option value="en">English</option>
</select>
</Field>
<Field label={t('Natureza declarada', 'Declared nature')}>
<select
className={control}
value={source.data.nature}
onChange={(e) =>
source.setData('nature', e.target.value)
}
>
{[
'pending',
'official',
'historical',
'illustrative',
].map((n) => (
<option key={n} value={n}>
{language === 'pt' ? states[n] : n}
</option>
))}
</select>
</Field>
<label className="flex items-start gap-2">
<input
required
type="checkbox"
checked={source.data.authorised}
onChange={(e) =>
source.setData('authorised', e.target.checked)
}
/>
{t(
'Confirmo autorização para consulta interna e ausência de dados confidenciais ou pessoais desnecessários.',
'I confirm authorisation for internal use and the absence of confidential or unnecessary personal data.',
)}
</label>
<div>
{Object.entries(source.errors).map(([k, v]) => (
<p role="alert" key={k}>
{v}
</p>
))}
<button
disabled={
source.processing || documents.length === 0
}
className={button}
>
{t('Submeter fonte', 'Submit source')}
</button>
</div>
</form>
</details>
</Management>
);
}