feat: AI draft pentru research brief + sinteza AI in briefing

- BriefForm: buton "Generează cu AI" cheama /v1/research-briefs/draft si
  precompleteaza titlul+rezumatul; textul spune explicit ca nimic nu s-a
  salvat inca si arata costul real al generarii
- Briefing: afiseaza aiExplanation cand exista, cu nota ca prioritizarea
  ramane deterministica si AI-ul doar explica
This commit is contained in:
valentinbvro 2026-07-28 22:34:49 +02:00
parent 60e5befa31
commit b2260fe839
2 changed files with 53 additions and 9 deletions

View file

@ -19,17 +19,37 @@ interface BriefFormProps {
onClose: () => void; onClose: () => void;
} }
interface DraftResponse {
title: string;
summary: string;
costUsd: number;
}
export function BriefForm({ tenantId, organizationId, organizationName, onClose }: BriefFormProps) { export function BriefForm({ tenantId, organizationId, organizationName, onClose }: BriefFormProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { const {
register, register,
handleSubmit, handleSubmit,
setValue,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
} = useForm<BriefForm>({ } = useForm<BriefForm>({
resolver: zodResolver(briefSchema), resolver: zodResolver(briefSchema),
defaultValues: { title: `Research brief — ${organizationName}` }, defaultValues: { title: `Research brief — ${organizationName}` },
}); });
const generateDraft = useMutation({
mutationFn: () =>
apiFetch<DraftResponse>('/v1/research-briefs/draft', {
method: 'POST',
tenantId,
body: { organizationId },
}),
onSuccess: (draft) => {
setValue('title', draft.title, { shouldValidate: true });
setValue('summary', draft.summary, { shouldValidate: true });
},
});
const createBrief = useMutation({ const createBrief = useMutation({
mutationFn: (values: BriefForm) => mutationFn: (values: BriefForm) =>
apiFetch('/v1/research-briefs', { apiFetch('/v1/research-briefs', {
@ -58,14 +78,32 @@ export function BriefForm({ tenantId, organizationId, organizationName, onClose
{errors.title && <p className="mt-1 text-xs text-signal-danger">{errors.title.message}</p>} {errors.title && <p className="mt-1 text-xs text-signal-danger">{errors.title.message}</p>}
</div> </div>
<div> <div>
<label className="label" htmlFor="brief-summary"> <div className="mb-1.5 flex items-center justify-between">
Rezumat <label className="label mb-0" htmlFor="brief-summary">
</label> Rezumat
<textarea id="brief-summary" rows={5} className="field" {...register('summary')} /> </label>
<button
type="button"
onClick={() => generateDraft.mutate()}
disabled={generateDraft.isPending}
className="text-xs font-medium text-bronze-deep hover:underline disabled:opacity-50"
>
{generateDraft.isPending ? 'Se generează…' : 'Generează cu AI'}
</button>
</div>
<textarea id="brief-summary" rows={7} className="field" {...register('summary')} />
{errors.summary && <p className="mt-1 text-xs text-signal-danger">{errors.summary.message}</p>} {errors.summary && <p className="mt-1 text-xs text-signal-danger">{errors.summary.message}</p>}
<p className="mt-1 text-xs text-ink-faint"> {generateDraft.isError && (
Scris de tine — nu există încă un AI Gateway care să genereze acest rezumat automat. <p className="mt-1 text-xs text-signal-danger">
</p> {generateDraft.error instanceof Error ? generateDraft.error.message : 'Eroare la generare'}
</p>
)}
{generateDraft.isSuccess && (
<p className="mt-1 text-xs text-ink-faint">
Draft generat de AI din datele Apollo (cost ${generateDraft.data.costUsd.toFixed(4)}).
Revizuiește-l înainte să salvezi — nimic nu s-a salvat încă.
</p>
)}
</div> </div>
{createBrief.isError && ( {createBrief.isError && (

View file

@ -47,6 +47,13 @@ export default function OverviewPage() {
</div> </div>
</div> </div>
{briefing.aiExplanation && (
<section className="card border-l-4 border-l-bronze p-5">
<p className="label mb-1.5">Sinteza zilei</p>
<p className="whitespace-pre-wrap text-sm text-ink-soft">{briefing.aiExplanation}</p>
</section>
)}
{briefing.overdueTasks.length > 0 && ( {briefing.overdueTasks.length > 0 && (
<section> <section>
<h2 className="label mb-2">Sarcini restante</h2> <h2 className="label mb-2">Sarcini restante</h2>
@ -82,8 +89,7 @@ export default function OverviewPage() {
</section> </section>
<p className="text-xs text-ink-faint"> <p className="text-xs text-ink-faint">
Prioritizare deterministă din task-uri. Explicația AI nu e încă disponibilă (AI Gateway Prioritizarea e deterministă; AI-ul doar explică rezultatul, nu decide ordinea.
neconstruit).
</p> </p>
</div> </div>
)} )}