Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- 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.
103 lines
2.8 KiB
TypeScript
103 lines
2.8 KiB
TypeScript
"use client"
|
|
|
|
import { useMutation, useQuery } from "@tanstack/react-query"
|
|
import { apiClient } from "@/lib/api/client"
|
|
import type { AiChatContext } from "@/lib/ai/chat-context"
|
|
|
|
export type AiConfig = {
|
|
enabled: boolean
|
|
public_path: string
|
|
mcp_url?: string
|
|
embed_default_temporary: boolean
|
|
default_model: string
|
|
enabled_tools: string[]
|
|
chat_sync_enabled: boolean
|
|
models?: { model_id: string; label: string; enabled: boolean }[]
|
|
restrict_models?: boolean
|
|
}
|
|
|
|
export type AiQuota = {
|
|
cost_used_today_micro_eur: number
|
|
cost_limit_today_micro_eur?: number | null
|
|
cost_used_month_micro_eur: number
|
|
cost_limit_month_micro_eur?: number | null
|
|
cost_remaining_today_micro_eur?: number | null
|
|
cost_remaining_month_micro_eur?: number | null
|
|
warn_threshold_pct: number
|
|
currency: string
|
|
billing_scope_org: boolean
|
|
by_provider_keys?: {
|
|
fingerprint: string
|
|
label: string
|
|
cost_month_micro_eur: number
|
|
cost_month_eur: number
|
|
billing_scope: string
|
|
}[]
|
|
/** @deprecated */
|
|
requests_used_today?: number
|
|
requests_limit?: number
|
|
tokens_used_month?: number
|
|
tokens_limit?: number
|
|
requests_remaining?: number
|
|
tokens_remaining?: number
|
|
}
|
|
|
|
export function formatAiCostEUR(microEur: number | null | undefined): string {
|
|
const value = typeof microEur === "number" && Number.isFinite(microEur) ? microEur : 0
|
|
return new Intl.NumberFormat("fr-FR", {
|
|
style: "currency",
|
|
currency: "EUR",
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 4,
|
|
}).format(value / 1_000_000)
|
|
}
|
|
|
|
export function aiQuotaUsedMonthPct(quota: AiQuota): number | null {
|
|
const limit = quota.cost_limit_month_micro_eur
|
|
if (!limit || limit <= 0) return null
|
|
return Math.min(100, Math.round((quota.cost_used_month_micro_eur / limit) * 100))
|
|
}
|
|
|
|
export type AiSessionResponse = {
|
|
session_id: string
|
|
embed_url: string
|
|
token_secret?: string
|
|
temporary: boolean
|
|
mcp_url?: string
|
|
enabled_tools?: string[]
|
|
}
|
|
|
|
export function useAiConfig() {
|
|
return useQuery({
|
|
queryKey: ["ai", "config"],
|
|
queryFn: () => apiClient.get<AiConfig>("/ai/config"),
|
|
staleTime: 60_000,
|
|
})
|
|
}
|
|
|
|
export function useAiQuota(enabled = true) {
|
|
return useQuery({
|
|
queryKey: ["ai", "quota"],
|
|
queryFn: () => apiClient.get<AiQuota>("/ai/quota"),
|
|
enabled,
|
|
staleTime: 30_000,
|
|
})
|
|
}
|
|
|
|
export function useCreateAiSession() {
|
|
return useMutation({
|
|
mutationFn: (context: AiChatContext) =>
|
|
apiClient.post<AiSessionResponse>("/ai/sessions", {
|
|
app: context.app,
|
|
temporary: context.temporary ?? true,
|
|
message_id: context.messageId,
|
|
account_id: context.accountId,
|
|
drive_path: context.drivePath,
|
|
file_id: context.fileId,
|
|
contact_id: context.contactId,
|
|
subject: context.subject,
|
|
snippet: context.snippet,
|
|
}),
|
|
})
|
|
}
|