Files
nala c6373365bf
tests / ci (push) Canceled after 0s
commit9
2026-10-30 20:02:30 +02:00

238 lines
10 KiB
TypeScript

import { useForm } from '@inertiajs/react';
import { Panel, FormErrors, controlClass } from '@/components/procurement';
import { Button } from '@/components/ui/button';
export type AiPlan = {
state: 'draft';
provider: 'none' | 'openai';
model: string;
domains: string[];
data_scopes: string[];
monthly_budget_usd: string;
monthly_requests: number;
max_input_bytes: number;
max_output_tokens: number;
};
export function AiPlanSettings({
plan,
domains,
scopes,
version,
ready,
}: {
plan: AiPlan;
domains: Record<string, string>;
scopes: Record<string, string>;
version: number;
ready: boolean;
}) {
const form = useForm({
version,
provider: plan.provider,
model: plan.model,
domains: plan.domains,
data_scopes: plan.data_scopes,
monthly_budget_usd: plan.monthly_budget_usd,
monthly_requests: plan.monthly_requests,
max_input_bytes: plan.max_input_bytes,
max_output_tokens: plan.max_output_tokens,
reason: '',
});
function choices(
field: 'domains' | 'data_scopes',
labels: Record<string, string>,
) {
return Object.entries(labels).map(([id, label]) => (
<label key={id} className="flex items-center gap-2">
<input
type="checkbox"
checked={form.data[field].includes(id)}
onChange={(event) =>
form.setData(
field,
event.target.checked
? [...form.data[field], id]
: form.data[field].filter(
(value) => value !== id,
),
)
}
/>
{label}
</label>
));
}
return (
<Panel title="Plano de IA externa — preparação">
<div className="space-y-4 text-sm">
<p>
Prepare o fornecedor, o âmbito e os limites pretendidos.
Guardar este plano permite pré-visualizar os factos
autorizados nos relatórios; não envia dados nem activa
chamadas externas.
</p>
{!ready ? (
<p>Instalação da configuração pendente.</p>
) : (
<form
className="space-y-4"
onSubmit={(event) => {
event.preventDefault();
form.post('/administracao/parametrizacao/ai_plan', {
preserveScroll: true,
});
}}
>
<FormErrors errors={form.errors} />
<div className="grid gap-4 md:grid-cols-2">
<label className="grid gap-1">
Fornecedor pretendido
<select
className={controlClass}
value={form.data.provider}
onChange={(event) =>
form.setData(
'provider',
event.target
.value as AiPlan['provider'],
)
}
>
<option value="none">
Sem fornecedor planeado
</option>
<option value="openai">OpenAI</option>
</select>
</label>
<label className="grid gap-1">
Modelo pretendido
<input
className={controlClass}
value={form.data.model}
maxLength={120}
onChange={(event) =>
form.setData(
'model',
event.target.value,
)
}
/>
<span>
Identificador do modelo; compatibilidade
ainda sujeita a validação.
</span>
</label>
</div>
<fieldset className="space-y-2">
<legend className="mb-2 font-medium">
Percursos abrangidos
</legend>
{choices('domains', domains)}
</fieldset>
<fieldset className="space-y-2">
<legend className="mb-2 font-medium">
Categorias de factos permitidas
</legend>
{choices('data_scopes', scopes)}
</fieldset>
<p>
As permissões dos documentos continuam a aplicar-se.
Esta escolha não concede acesso a candidaturas,
entrevistas ou finanças.
</p>
<div className="grid gap-4 md:grid-cols-2">
<label className="grid gap-1">
Orçamento mensal planeado (USD)
<input
className={controlClass}
type="number"
min="0"
step="0.01"
max="99999.99"
value={form.data.monthly_budget_usd}
onChange={(event) =>
form.setData(
'monthly_budget_usd',
event.target.value,
)
}
/>
</label>
<label className="grid gap-1">
Número mensal planeado de pedidos
<input
className={controlClass}
type="number"
min={0}
max={100000}
value={form.data.monthly_requests}
onChange={(event) =>
form.setData(
'monthly_requests',
Number(event.target.value),
)
}
/>
</label>
<label className="grid gap-1">
Limite de dados por pedido (bytes)
<input
className={controlClass}
type="number"
min={512}
max={64000}
value={form.data.max_input_bytes}
onChange={(event) =>
form.setData(
'max_input_bytes',
Number(event.target.value),
)
}
/>
</label>
<label className="grid gap-1">
Limite de saída pretendido (tokens)
<input
className={controlClass}
type="number"
min={64}
max={4000}
value={form.data.max_output_tokens}
onChange={(event) =>
form.setData(
'max_output_tokens',
Number(event.target.value),
)
}
/>
</label>
</div>
<p>
Limites de planeamento: ainda não existe contador
mensal, reserva de orçamento ou cobrança por este
plano.
</p>
<label className="grid gap-1">
Motivo e efeito pretendido
<textarea
className={controlClass}
required
minLength={10}
maxLength={4000}
value={form.data.reason}
onChange={(event) =>
form.setData('reason', event.target.value)
}
/>
</label>
<Button disabled={form.processing}>
Guardar plano para pré-visualização
</Button>
</form>
)}
</div>
</Panel>
);
}