ultisuite-client/components/admin/settings/sections/ultimeet-section.tsx
R3D347HR4Y ad1370ea7e
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat: enhance configuration and add new demo layouts
- Introduced turbopack alias for canvas in next.config.mjs.
- Updated package.json scripts for development and branding tasks.
- Added new dependencies for Tiptap extensions.
- Implemented new demo layouts for agenda, contacts, drive, and mail applications.
- Enhanced globals.css for improved theming and splash screen animations.
- Added OAuth callback handling for drive mounts.
- Updated layout components to integrate new demo shells and improve structure.
2026-06-12 19:10:24 +02:00

348 lines
13 KiB
TypeScript

"use client"
import { useEffect, useState } from "react"
import { OrgSettingsSection } from "@/components/admin/settings/org-settings-form"
import { useOrgSettingsStore } from "@/lib/admin-settings/org-settings-store"
import {
MEET_EMAIL_RECIPIENTS_LABELS,
MEET_EXTERNAL_API_PROVIDER_LABELS,
MEET_TRANSCRIPTION_ENGINE_LABELS,
MEET_TRANSCRIPTION_MODE_LABELS,
type MeetOrgPolicySettings,
} from "@/lib/meet/meet-settings-types"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
export function UltimeetSection() {
const meet = useOrgSettingsStore((s) => s.meet)
const llmProviders = useOrgSettingsStore((s) => s.llm.providers)
const setMeet = useOrgSettingsStore((s) => s.setMeet)
const effective = useOrgSettingsStore((s) => s.meta?.effective.jitsi)
const [draft, setDraft] = useState(meet)
useEffect(() => {
setDraft(meet)
}, [meet])
const patch = (next: Partial<MeetOrgPolicySettings>) =>
setDraft((prev) => ({ ...prev, ...next }))
const patchPost = (next: Partial<MeetOrgPolicySettings["post_actions"]>) =>
setDraft((prev) => ({
...prev,
post_actions: { ...prev.post_actions, ...next },
}))
return (
<OrgSettingsSection
title="UltiMeet"
description="Visioconférence Jitsi, transcription et traitements post-réunion."
policySection="meet"
beforeSave={() => setMeet(draft)}
>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Infrastructure</CardTitle>
<CardDescription>
Jitsi {effective?.enabled ? "actif" : "inactif"}
{effective?.public_url ? `${effective.public_url}` : ""}
</CardDescription>
</CardHeader>
</Card>
<div className="space-y-6 rounded-lg border p-4">
<label className="flex items-center justify-between gap-4">
<div>
<p className="text-sm font-medium">Transcription activée</p>
<p className="text-xs text-muted-foreground">
Active les sous-titres Jitsi (live) ou le pipeline différé selon le mode.
</p>
</div>
<Switch
checked={draft.transcription_enabled}
onCheckedChange={(v) => patch({ transcription_enabled: v })}
/>
</label>
{draft.transcription_enabled ? (
<>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label>Mode</Label>
<Select
value={draft.transcription_mode}
onValueChange={(v) =>
patch({ transcription_mode: v as MeetOrgPolicySettings["transcription_mode"] })
}
>
<SelectTrigger className="h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(MEET_TRANSCRIPTION_MODE_LABELS).map(([id, label]) => (
<SelectItem key={id} value={id}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Moteur</Label>
<Select
value={draft.transcription_engine}
onValueChange={(v) =>
patch({
transcription_engine: v as MeetOrgPolicySettings["transcription_engine"],
})
}
>
<SelectTrigger className="h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(MEET_TRANSCRIPTION_ENGINE_LABELS).map(([id, label]) => (
<SelectItem key={id} value={id}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<label className="flex items-center justify-between gap-4">
<div>
<p className="text-sm font-medium">Démarrage automatique</p>
<p className="text-xs text-muted-foreground">
Lance la transcription dès l&apos;ouverture de la salle (sinon via bouton
Sous-titres pour les modérateurs).
</p>
</div>
<Switch
checked={draft.auto_start_transcription}
onCheckedChange={(v) => patch({ auto_start_transcription: v })}
/>
</label>
{draft.transcription_engine === "faster_whisper_local" ? (
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label>URL Skynet (interne)</Label>
<Input
value={draft.skynet_url}
onChange={(e) => patch({ skynet_url: e.target.value })}
placeholder="http://skynet:8000"
/>
</div>
<div className="space-y-2">
<Label>Modèle Whisper</Label>
<Input
value={draft.whisper_model}
onChange={(e) => patch({ whisper_model: e.target.value })}
placeholder="tiny, base, small…"
/>
</div>
</div>
) : (
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2 sm:col-span-2">
<Label>Fournisseur API</Label>
<Select
value={draft.external_api_provider}
onValueChange={(v) =>
patch({
external_api_provider:
v as MeetOrgPolicySettings["external_api_provider"],
})
}
>
<SelectTrigger className="h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(MEET_EXTERNAL_API_PROVIDER_LABELS).map(([id, label]) => (
<SelectItem key={id} value={id}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2 sm:col-span-2">
<Label>URL API</Label>
<Input
value={draft.external_api_url}
onChange={(e) => patch({ external_api_url: e.target.value })}
placeholder="https://api.openai.com/v1/audio/transcriptions"
/>
</div>
<div className="space-y-2 sm:col-span-2">
<Label>Clé API</Label>
<Input
type="password"
value={draft.external_api_key}
onChange={(e) => patch({ external_api_key: e.target.value })}
placeholder="Laisser vide pour conserver la clé enregistrée"
autoComplete="off"
/>
</div>
</div>
)}
</>
) : null}
</div>
{draft.transcription_enabled ? (
<div className="space-y-4 rounded-lg border p-4">
<div>
<p className="text-sm font-medium">Après la réunion</p>
<p className="text-xs text-muted-foreground">
Actions exécutées quand le transcript est reçu par le backend.
</p>
</div>
<label className="flex items-center justify-between gap-4">
<div>
<p className="text-sm font-medium">Enregistrer dans UltiDrive</p>
</div>
<Switch
checked={draft.post_actions.drive_enabled}
onCheckedChange={(v) => patchPost({ drive_enabled: v })}
/>
</label>
{draft.post_actions.drive_enabled ? (
<div className="space-y-2">
<Label>Dossier Drive</Label>
<Input
value={draft.post_actions.drive_folder_path}
onChange={(e) => patchPost({ drive_folder_path: e.target.value })}
placeholder="/UltiMeet/Transcripts"
/>
</div>
) : null}
<label className="flex items-center justify-between gap-4">
<div>
<p className="text-sm font-medium">Envoyer par mail</p>
<p className="text-xs text-muted-foreground">
Utilise le SMTP organisationnel (réglages Mailing).
</p>
</div>
<Switch
checked={draft.post_actions.email_enabled}
onCheckedChange={(v) => patchPost({ email_enabled: v })}
/>
</label>
{draft.post_actions.email_enabled ? (
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label>Destinataires</Label>
<Select
value={draft.post_actions.email_recipients}
onValueChange={(v) =>
patchPost({
email_recipients: v as MeetOrgPolicySettings["post_actions"]["email_recipients"],
})
}
>
<SelectTrigger className="h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(MEET_EMAIL_RECIPIENTS_LABELS).map(([id, label]) => (
<SelectItem key={id} value={id}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{draft.post_actions.email_recipients === "custom" ? (
<div className="space-y-2 sm:col-span-2">
<Label>Adresses (séparées par des virgules)</Label>
<Input
value={draft.post_actions.email_custom_addresses}
onChange={(e) => patchPost({ email_custom_addresses: e.target.value })}
/>
</div>
) : null}
</div>
) : null}
<label className="flex items-center justify-between gap-4">
<div>
<p className="text-sm font-medium">Traitement LLM</p>
<p className="text-xs text-muted-foreground">
Résume ou transforme le transcript via un fournisseur LLM organisationnel.
</p>
</div>
<Switch
checked={draft.post_actions.llm_enabled}
onCheckedChange={(v) => patchPost({ llm_enabled: v })}
/>
</label>
{draft.post_actions.llm_enabled ? (
<div className="space-y-4">
<div className="space-y-2">
<Label>Fournisseur LLM</Label>
<Select
value={draft.post_actions.llm_provider_id || "__default__"}
onValueChange={(v) =>
patchPost({ llm_provider_id: v === "__default__" ? "" : v })
}
>
<SelectTrigger className="h-9">
<SelectValue placeholder="Par défaut (organisation)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__default__">Par défaut (organisation)</SelectItem>
{llmProviders.map((p) => (
<SelectItem key={p.id} value={p.id}>
{p.name || p.id}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Prompt</Label>
<Textarea
value={draft.post_actions.llm_prompt}
onChange={(e) => patchPost({ llm_prompt: e.target.value })}
rows={4}
/>
</div>
<label className="flex items-center justify-between gap-4">
<span className="text-sm">Envoyer le résultat LLM par mail</span>
<Switch
checked={draft.post_actions.llm_then_email}
onCheckedChange={(v) => patchPost({ llm_then_email: v })}
/>
</label>
<label className="flex items-center justify-between gap-4">
<span className="text-sm">Enregistrer le résultat LLM dans Drive</span>
<Switch
checked={draft.post_actions.llm_then_drive}
onCheckedChange={(v) => patchPost({ llm_then_drive: v })}
/>
</label>
</div>
) : null}
</div>
) : null}
</OrgSettingsSection>
)
}