From 2a0958b70ddee26fd2066c5cf36e5f3428ae3e79 Mon Sep 17 00:00:00 2001 From: R3D347HR4Y Date: Tue, 16 Jun 2026 10:46:31 +0200 Subject: [PATCH] feat: update agenda references to use ULTICAL_APP_NAME and enhance AI usage sections - Replaced hardcoded "Agenda" labels with dynamic ULTICAL_APP_NAME in various components for consistency. - Introduced new AiUsageSection and CompteAiUsageSection components to track AI usage and costs. - Updated settings and metadata to reflect changes in AI cost policies and usage limits. - Enhanced user interface elements for better accessibility and user experience across admin settings. --- app/demo/agenda/layout.tsx | 6 +- app/onboard/migration/page.tsx | 4 +- .../settings/admin-settings-section-view.tsx | 5 + .../settings/sections/agenda-section.tsx | 4 +- .../settings/sections/ai-usage-section.tsx | 351 ++++++++++++++++++ .../sections/migration-projects-panel.tsx | 4 +- .../settings/sections/plugins-section.tsx | 5 +- .../settings/sections/quotas-section.tsx | 33 +- .../admin/settings/sections/users-section.tsx | 46 +++ components/agenda/agenda-header.tsx | 3 +- components/ai/ai-chat-panel.tsx | 7 +- components/ai/ai-spend-bar.tsx | 84 +++++ components/ai/docs-ai-panel.tsx | 7 +- .../compte/compte-settings-section-view.tsx | 2 + .../sections/compte-ai-usage-section.tsx | 101 +++++ components/demo/demo-agenda-data.ts | 2 +- .../automation/rule-simulator-panel.tsx | 6 +- .../sections/agenda-settings-section.tsx | 4 +- .../sections/automation-settings-section.tsx | 2 +- components/landing/landing-data.ts | 7 +- components/landing/landing-demo.tsx | 3 +- lib/admin-settings/default-plugins.ts | 2 +- lib/admin-settings/map-api-org-settings.ts | 12 +- lib/admin-settings/org-settings-store.ts | 15 +- lib/admin-settings/org-settings-types.ts | 4 + lib/admin-settings/settings-nav.ts | 13 +- lib/ai/chat-context.ts | 2 +- lib/ai/ultiai-tool-groups.ts | 4 +- lib/api/admin-org-types.ts | 3 + lib/api/hooks/use-admin-ai-usage.ts | 105 ++++++ lib/api/hooks/use-ai-queries.ts | 45 ++- lib/compte-settings/settings-nav.ts | 11 +- lib/demo/demo-drive-preview.ts | 2 +- lib/mail-automation/api-token-permissions.ts | 4 +- lib/mail-automation/domains.ts | 3 +- lib/mail-settings/settings-nav.ts | 3 +- lib/suite/favorite-apps.ts | 3 +- lib/suite/page-metadata.ts | 7 +- lib/suite/suite-app-splash.ts | 7 +- tsconfig.tsbuildinfo | 2 +- 40 files changed, 863 insertions(+), 70 deletions(-) create mode 100644 components/admin/settings/sections/ai-usage-section.tsx create mode 100644 components/ai/ai-spend-bar.tsx create mode 100644 components/compte/sections/compte-ai-usage-section.tsx create mode 100644 lib/api/hooks/use-admin-ai-usage.ts diff --git a/app/demo/agenda/layout.tsx b/app/demo/agenda/layout.tsx index 6b366f4..c460ec3 100644 --- a/app/demo/agenda/layout.tsx +++ b/app/demo/agenda/layout.tsx @@ -1,14 +1,14 @@ import { DemoAgendaShell } from "@/components/demo/demo-agenda-shell" import type { Metadata } from "next" -import { suitePageMetadata } from "@/lib/suite/page-metadata" +import { ULTICAL_APP_NAME, suitePageMetadata } from "@/lib/suite/page-metadata" export const metadata: Metadata = { ...suitePageMetadata({ app: "agenda", - title: "Démo UltiAgenda", + title: `Démo ${ULTICAL_APP_NAME}`, absoluteTitle: true, description: - "Essayez l'agenda UltiAgenda sans compte — démo interactive, zéro rétention.", + `Essayez ${ULTICAL_APP_NAME} sans compte — démo interactive, zéro rétention.`, }), robots: { index: false }, } diff --git a/app/onboard/migration/page.tsx b/app/onboard/migration/page.tsx index 5a0ebfb..dd28642 100644 --- a/app/onboard/migration/page.tsx +++ b/app/onboard/migration/page.tsx @@ -18,12 +18,12 @@ import { useStartMigrationOAuth, } from "@/lib/api/hooks/use-hosted-mail" import { useAuthReady } from "@/lib/api/use-auth-ready" -import { buildOidcLoginUrl } from "@/lib/auth/login-url" +import { ULTICAL_APP_NAME } from "@/lib/suite/page-metadata" const SERVICE_LABELS: Record = { mail: "Mail", contacts: "Contacts", - calendar: "Agenda", + calendar: ULTICAL_APP_NAME, drive: "Drive", } diff --git a/components/admin/settings/admin-settings-section-view.tsx b/components/admin/settings/admin-settings-section-view.tsx index 315e77b..eeac4d4 100644 --- a/components/admin/settings/admin-settings-section-view.tsx +++ b/components/admin/settings/admin-settings-section-view.tsx @@ -85,6 +85,11 @@ const SECTIONS: Record = { default: m.AiAssistantSection, })) ), + "ai-usage": loadSection(() => + import("@/components/admin/settings/sections/ai-usage-section").then((m) => ({ + default: m.AiUsageSection, + })) + ), audit: loadSection(() => import("@/components/admin/settings/sections/audit-section").then((m) => ({ default: m.AuditSection, diff --git a/components/admin/settings/sections/agenda-section.tsx b/components/admin/settings/sections/agenda-section.tsx index 217e2d2..535ef86 100644 --- a/components/admin/settings/sections/agenda-section.tsx +++ b/components/admin/settings/sections/agenda-section.tsx @@ -23,7 +23,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" -import type { MailThemeMode } from "@/lib/mail-settings/types" +import { ULTICAL_APP_NAME } from "@/lib/suite/page-metadata" const THEME_OPTIONS: { id: MailThemeMode; label: string }[] = [ { id: "light", label: "Clair" }, @@ -52,7 +52,7 @@ export function AgendaSection() { return ( setAgenda(draft)} diff --git a/components/admin/settings/sections/ai-usage-section.tsx b/components/admin/settings/sections/ai-usage-section.tsx new file mode 100644 index 0000000..759165d --- /dev/null +++ b/components/admin/settings/sections/ai-usage-section.tsx @@ -0,0 +1,351 @@ +"use client" + +import Link from "next/link" +import { useMemo } from "react" +import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts" +import type { ComponentType } from "react" +import { CalendarDays, Coins, Gauge, TrendingUp } from "lucide-react" +import { SettingsSectionHeader } from "@/components/gmail/settings/settings-section-header" +import { SettingsSyncBanner } from "@/components/gmail/settings/settings-sync-banner" +import { SettingsCard } from "@/components/settings/settings-kit" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { + ChartContainer, + ChartTooltip, + ChartTooltipContent, + type ChartConfig, +} from "@/components/ui/chart" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { + useAdminAIUsage, + useAdminAIPricing, +} from "@/lib/api/hooks/use-admin-ai-usage" +import { formatAiCostEUR } from "@/lib/api/hooks/use-ai-queries" +import { cn } from "@/lib/utils" + +const chartConfig = { + cost: { + label: "Coût org", + color: "hsl(var(--primary))", + }, +} satisfies ChartConfig + +function formatPricePerMTok(eur: number): string { + return new Intl.NumberFormat("fr-FR", { + style: "currency", + currency: "EUR", + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(eur) +} + +function formatShortDate(isoDate: string): string { + const date = new Date(`${isoDate}T12:00:00`) + return date.toLocaleDateString("fr-FR", { day: "numeric", month: "short" }) +} + +function StatCard({ + label, + value, + hint, + icon: Icon, +}: { + label: string + value: string + hint?: string + icon: ComponentType<{ className?: string }> +}) { + return ( +
+
+
+

{label}

+

{value}

+ {hint ?

{hint}

: null} +
+
+ +
+
+
+ ) +} + +function RankedRow({ + rank, + label, + sublabel, + value, + sharePct, +}: { + rank: number + label: string + sublabel?: string + value: string + sharePct: number +}) { + return ( +
+ + {rank} + +
+
+
+

{label}

+ {sublabel ? ( +

{sublabel}

+ ) : null} +
+ {value} +
+
+
+
+
+
+ ) +} + +function EmptyPanel({ message }: { message: string }) { + return ( +

{message}

+ ) +} + +export function AiUsageSection() { + const { data, isFetching, isError, refetch } = useAdminAIUsage("org") + const { data: pricing } = useAdminAIPricing() + + const dailySeries = data?.daily_series ?? [] + const topUsers = data?.top_users ?? [] + const topModels = data?.top_models ?? [] + const pricingRows = pricing ?? [] + + const chartData = useMemo( + () => + dailySeries.map((day) => ({ + date: day.date, + label: formatShortDate(day.date), + cost: day.cost_org_micro_eur / 1_000_000, + requests: day.requests, + })), + [dailySeries] + ) + + const maxUserCost = useMemo( + () => Math.max(...topUsers.map((u) => u.cost_org_micro_eur), 1), + [topUsers] + ) + const maxModelCost = useMemo( + () => Math.max(...topModels.map((m) => m.cost_micro_eur), 1), + [topModels] + ) + + return ( + <> + + refetch()} + /> + +
+ +
+ + {data ? ( +
+
+ + + +
+ + {dailySeries.length > 0 ? ( + + + + + + + new Intl.NumberFormat("fr-FR", { + style: "currency", + currency: "EUR", + notation: "compact", + maximumFractionDigits: 1, + }).format(v) + } + /> + { + const row = payload?.[0]?.payload as { date?: string } | undefined + return row?.date + ? new Date(`${row.date}T12:00:00`).toLocaleDateString("fr-FR", { + weekday: "short", + day: "numeric", + month: "long", + }) + : "" + }} + formatter={(value, _name, item) => { + const requests = (item.payload as { requests?: number }).requests ?? 0 + return ( +
+ {chartConfig.cost.label} + + {formatAiCostEUR(Number(value) * 1_000_000)} + {requests > 0 ? ` · ${requests} req.` : ""} + +
+ ) + }} + /> + } + /> + +
+
+
+ ) : null} + +
+ + {topUsers.length === 0 ? ( + + ) : ( +
+ {topUsers.map((u, index) => ( + + ))} +
+ )} +
+ + + {topModels.length === 0 ? ( + + ) : ( +
+ {topModels.map((m, index) => ( + 1 ? "s" : ""}`} + value={formatAiCostEUR(m.cost_micro_eur)} + sharePct={Math.round((m.cost_micro_eur / maxModelCost) * 100)} + /> + ))} +
+ )} +
+
+ + {pricingRows.length > 0 ? ( + +
+ + + + Modèle + + Input / 1M + + + Output / 1M + + + + + {pricingRows.map((p) => ( + + +
+ {p.model_id} + {p.provider_type ? ( + + {p.provider_type} + + ) : null} +
+
+ + {formatPricePerMTok(p.input_eur_per_mtok)} + + + {formatPricePerMTok(p.output_eur_per_mtok)} + +
+ ))} +
+
+
+
+ ) : null} +
+ ) : isFetching ? ( +

Chargement de la consommation…

+ ) : null} + + ) +} diff --git a/components/admin/settings/sections/migration-projects-panel.tsx b/components/admin/settings/sections/migration-projects-panel.tsx index 3ed7983..e577132 100644 --- a/components/admin/settings/sections/migration-projects-panel.tsx +++ b/components/admin/settings/sections/migration-projects-panel.tsx @@ -46,12 +46,12 @@ import { useRetryMigrationFailedJobs, useRetryMigrationJob, } from "@/lib/api/hooks/use-hosted-mail" -import { ApiRequestError } from "@/lib/api/client" +import { ULTICAL_APP_NAME } from "@/lib/suite/page-metadata" const SERVICE_LABELS: Record = { mail: "Mail", contacts: "Contacts", - calendar: "Agenda", + calendar: ULTICAL_APP_NAME, drive: "Drive", } diff --git a/components/admin/settings/sections/plugins-section.tsx b/components/admin/settings/sections/plugins-section.tsx index 01a0210..65f79a6 100644 --- a/components/admin/settings/sections/plugins-section.tsx +++ b/components/admin/settings/sections/plugins-section.tsx @@ -31,6 +31,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" +import { ULTICAL_APP_NAME } from "@/lib/suite/page-metadata" import { cn } from "@/lib/utils" const SIMPLE_PLUGIN_IDS = new Set(["mail-automation", "contact-discovery", "public-share"]) @@ -201,7 +202,7 @@ function NextcloudPluginCard() { return ( setNextcloud({ drive_enabled })} /> setNextcloud({ calendar_enabled })} /> diff --git a/components/admin/settings/sections/quotas-section.tsx b/components/admin/settings/sections/quotas-section.tsx index 008490f..12e1d8b 100644 --- a/components/admin/settings/sections/quotas-section.tsx +++ b/components/admin/settings/sections/quotas-section.tsx @@ -71,22 +71,37 @@ export function QuotasSection() { setUsageQuotas({ llm_requests_per_day: v })} + label="Plafond journalier" + unit="€" + step={0.5} + value={usageQuotas.llm_daily_cost_limit_eur} + onChange={(v) => setUsageQuotas({ llm_daily_cost_limit_eur: v })} /> setUsageQuotas({ llm_tokens_per_month: v })} + label="Plafond mensuel" + unit="€" + step={1} + value={usageQuotas.llm_monthly_cost_limit_eur} + onChange={(v) => setUsageQuotas({ llm_monthly_cost_limit_eur: v })} /> + setUsageQuotas({ llm_cost_warn_threshold_pct: v })} + /> + ("user") const [mailGib, setMailGib] = useState("5") const [driveGib, setDriveGib] = useState("5") const [photosGib, setPhotosGib] = useState("5") + const [aiDailyEur, setAiDailyEur] = useState("") + const [aiMonthlyEur, setAiMonthlyEur] = useState("") const open = Boolean(userId) @@ -554,6 +558,14 @@ function UserDetailSheet({ }) } + async function saveAiPolicy() { + if (!userId) return + await setAiPolicy.mutateAsync({ + daily_limit_eur: aiDailyEur.trim() === "" ? null : Number(aiDailyEur), + monthly_limit_eur: aiMonthlyEur.trim() === "" ? null : Number(aiMonthlyEur), + }) + } + return ( !v && onClose()}> @@ -646,6 +658,40 @@ function UserDetailSheet({
) : null} +
+

Plafond IA (clé org)

+

+ Override utilisateur. Laissez vide pour hériter des plafonds org/groupe. +

+
+
+ + setAiDailyEur(e.target.value)} + /> +
+
+ + setAiMonthlyEur(e.target.value)} + /> +
+
+ +
+

ID : {user.id}

)} diff --git a/components/agenda/agenda-header.tsx b/components/agenda/agenda-header.tsx index e27228c..c204e04 100644 --- a/components/agenda/agenda-header.tsx +++ b/components/agenda/agenda-header.tsx @@ -22,6 +22,7 @@ import { } from "@/lib/agenda/agenda-url" import { useAgendaSettingsStore, useAgendaUIStore } from "@/lib/agenda/agenda-store" import { useIsMobile } from "@/hooks/use-mobile" +import { ULTICAL_APP_NAME } from "@/lib/suite/page-metadata" import { SUITE_APP_LOGO_LOCKUP_CLASS, SUITE_APP_LOGO_MARK_CLASS, @@ -74,7 +75,7 @@ export function AgendaHeader({ className={cn("mr-1 hidden sm:flex", SUITE_APP_LOGO_LOCKUP_CLASS)} > - Agenda + {ULTICAL_APP_NAME}