238 lines
10 KiB
TypeScript
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>
|
|
);
|
|
}
|