Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- 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.
348 lines
13 KiB
TypeScript
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'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>
|
|
)
|
|
}
|