@flowselections/ai-authenticatie 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/dist-lib/_core-safelist.d.ts +2 -0
  2. package/dist-lib/_core-safelist.d.ts.map +1 -0
  3. package/dist-lib/_core-safelist.js +15 -0
  4. package/dist-lib/components/AiAuthenticatieLogo.d.ts +3 -0
  5. package/dist-lib/components/AiAuthenticatieLogo.d.ts.map +1 -0
  6. package/dist-lib/components/AiAuthenticatieLogo.js +4 -0
  7. package/dist-lib/components/AiAuthenticatiePage.d.ts +4 -0
  8. package/dist-lib/components/AiAuthenticatiePage.d.ts.map +1 -0
  9. package/dist-lib/components/AiAuthenticatiePage.js +145 -0
  10. package/dist-lib/components/AiModuleDetailPage.d.ts +2 -0
  11. package/dist-lib/components/AiModuleDetailPage.d.ts.map +1 -0
  12. package/dist-lib/components/AiModuleDetailPage.js +43 -0
  13. package/dist-lib/components/AiModuleTestPanel.d.ts +4 -0
  14. package/dist-lib/components/AiModuleTestPanel.d.ts.map +1 -0
  15. package/dist-lib/components/AiModuleTestPanel.js +38 -0
  16. package/dist-lib/components/AiPeriodFilter.d.ts +7 -0
  17. package/dist-lib/components/AiPeriodFilter.d.ts.map +1 -0
  18. package/dist-lib/components/AiPeriodFilter.js +14 -0
  19. package/dist-lib/components/AiStatTile.d.ts +6 -0
  20. package/dist-lib/components/AiStatTile.d.ts.map +1 -0
  21. package/dist-lib/components/AiStatTile.js +5 -0
  22. package/dist-lib/components/ModuleAgentsDialog.d.ts +14 -0
  23. package/dist-lib/components/ModuleAgentsDialog.d.ts.map +1 -0
  24. package/dist-lib/components/ModuleAgentsDialog.js +78 -0
  25. package/dist-lib/components/ProviderLogo.d.ts +8 -0
  26. package/dist-lib/components/ProviderLogo.d.ts.map +1 -0
  27. package/dist-lib/components/ProviderLogo.js +24 -0
  28. package/dist-lib/components/ProviderManageDialog.d.ts +11 -0
  29. package/dist-lib/components/ProviderManageDialog.d.ts.map +1 -0
  30. package/dist-lib/components/ProviderManageDialog.js +173 -0
  31. package/dist-lib/components/settings/AiAuthenticatieSettingsCard.d.ts +2 -0
  32. package/dist-lib/components/settings/AiAuthenticatieSettingsCard.d.ts.map +1 -0
  33. package/dist-lib/components/settings/AiAuthenticatieSettingsCard.js +7 -0
  34. package/dist-lib/components/settings/MijnModuleSettingsCard.d.ts +2 -0
  35. package/dist-lib/components/settings/MijnModuleSettingsCard.d.ts.map +1 -0
  36. package/dist-lib/components/settings/MijnModuleSettingsCard.js +42 -0
  37. package/dist-lib/hooks/useAdminCredentials.d.ts +14 -0
  38. package/dist-lib/hooks/useAdminCredentials.d.ts.map +1 -0
  39. package/dist-lib/hooks/useAdminCredentials.js +17 -0
  40. package/dist-lib/hooks/useAiAdmin.d.ts +44 -0
  41. package/dist-lib/hooks/useAiAdmin.d.ts.map +1 -0
  42. package/dist-lib/hooks/useAiAdmin.js +32 -0
  43. package/dist-lib/hooks/useAiCostOverview.d.ts +7 -0
  44. package/dist-lib/hooks/useAiCostOverview.d.ts.map +1 -0
  45. package/dist-lib/hooks/useAiCostOverview.js +28 -0
  46. package/dist-lib/hooks/useAiForGoal.d.ts +21 -0
  47. package/dist-lib/hooks/useAiForGoal.d.ts.map +1 -0
  48. package/dist-lib/hooks/useAiForGoal.js +40 -0
  49. package/dist-lib/hooks/useAiForModule.d.ts +11 -0
  50. package/dist-lib/hooks/useAiForModule.d.ts.map +1 -0
  51. package/dist-lib/hooks/useAiForModule.js +11 -0
  52. package/dist-lib/hooks/useAiGoals.d.ts +8 -0
  53. package/dist-lib/hooks/useAiGoals.d.ts.map +1 -0
  54. package/dist-lib/hooks/useAiGoals.js +22 -0
  55. package/dist-lib/hooks/useAiModules.d.ts +15 -0
  56. package/dist-lib/hooks/useAiModules.d.ts.map +1 -0
  57. package/dist-lib/hooks/useAiModules.js +59 -0
  58. package/dist-lib/hooks/useAiPricing.d.ts +20 -0
  59. package/dist-lib/hooks/useAiPricing.d.ts.map +1 -0
  60. package/dist-lib/hooks/useAiPricing.js +97 -0
  61. package/dist-lib/hooks/useAiUsage.d.ts +12 -0
  62. package/dist-lib/hooks/useAiUsage.d.ts.map +1 -0
  63. package/dist-lib/hooks/useAiUsage.js +90 -0
  64. package/dist-lib/hooks/useAiUsageByModule.d.ts +8 -0
  65. package/dist-lib/hooks/useAiUsageByModule.d.ts.map +1 -0
  66. package/dist-lib/hooks/useAiUsageByModule.js +22 -0
  67. package/dist-lib/hooks/useIsAiAdmin.d.ts +6 -0
  68. package/dist-lib/hooks/useIsAiAdmin.d.ts.map +1 -0
  69. package/dist-lib/hooks/useIsAiAdmin.js +5 -0
  70. package/dist-lib/hooks/useMijnModuleData.d.ts +13 -0
  71. package/dist-lib/hooks/useMijnModuleData.d.ts.map +1 -0
  72. package/dist-lib/hooks/useMijnModuleData.js +49 -0
  73. package/dist-lib/hooks/useModules.d.ts +14 -0
  74. package/dist-lib/hooks/useModules.d.ts.map +1 -0
  75. package/dist-lib/hooks/useModules.js +47 -0
  76. package/dist-lib/index.d.ts +29 -0
  77. package/dist-lib/index.d.ts.map +1 -0
  78. package/dist-lib/index.js +33 -0
  79. package/dist-lib/integrations/supabase/auth-attacher.d.ts +2 -0
  80. package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -0
  81. package/dist-lib/integrations/supabase/auth-attacher.js +12 -0
  82. package/dist-lib/integrations/supabase/auth-middleware.d.ts +15971 -0
  83. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -0
  84. package/dist-lib/integrations/supabase/auth-middleware.js +49 -0
  85. package/dist-lib/integrations/supabase/client.d.ts +15967 -0
  86. package/dist-lib/integrations/supabase/client.d.ts.map +1 -0
  87. package/dist-lib/integrations/supabase/client.js +13 -0
  88. package/dist-lib/integrations/supabase/client.server.d.ts +15967 -0
  89. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -0
  90. package/dist-lib/integrations/supabase/client.server.js +30 -0
  91. package/dist-lib/integrations/supabase/types.d.ts +16477 -0
  92. package/dist-lib/integrations/supabase/types.d.ts.map +1 -0
  93. package/dist-lib/integrations/supabase/types.js +78 -0
  94. package/dist-lib/lib/aiChat.functions.d.ts +31948 -0
  95. package/dist-lib/lib/aiChat.functions.d.ts.map +1 -0
  96. package/dist-lib/lib/aiChat.functions.js +51 -0
  97. package/dist-lib/lib/aiChat.server.d.ts +8 -0
  98. package/dist-lib/lib/aiChat.server.d.ts.map +1 -0
  99. package/dist-lib/lib/aiChat.server.js +347 -0
  100. package/dist-lib/lib/aiChat.stream.d.ts +33 -0
  101. package/dist-lib/lib/aiChat.stream.d.ts.map +1 -0
  102. package/dist-lib/lib/aiChat.stream.js +77 -0
  103. package/dist-lib/lib/aiChat.stream.server.d.ts +38 -0
  104. package/dist-lib/lib/aiChat.stream.server.d.ts.map +1 -0
  105. package/dist-lib/lib/aiChat.stream.server.js +259 -0
  106. package/dist-lib/lib/aiChat.types.d.ts +63 -0
  107. package/dist-lib/lib/aiChat.types.d.ts.map +1 -0
  108. package/dist-lib/lib/aiChat.types.js +1 -0
  109. package/dist-lib/lib/aiCredentialTest.functions.d.ts +31959 -0
  110. package/dist-lib/lib/aiCredentialTest.functions.d.ts.map +1 -0
  111. package/dist-lib/lib/aiCredentialTest.functions.js +714 -0
  112. package/dist-lib/lib/aiGoals.functions.d.ts +12912 -0
  113. package/dist-lib/lib/aiGoals.functions.d.ts.map +1 -0
  114. package/dist-lib/lib/aiGoals.functions.js +14 -0
  115. package/dist-lib/lib/aiModules.functions.d.ts +95851 -0
  116. package/dist-lib/lib/aiModules.functions.d.ts.map +1 -0
  117. package/dist-lib/lib/aiModules.functions.js +41 -0
  118. package/dist-lib/lib/aiModules.server.d.ts +65 -0
  119. package/dist-lib/lib/aiModules.server.d.ts.map +1 -0
  120. package/dist-lib/lib/aiModules.server.js +119 -0
  121. package/dist-lib/lib/aiPeriod.d.ts +11 -0
  122. package/dist-lib/lib/aiPeriod.d.ts.map +1 -0
  123. package/dist-lib/lib/aiPeriod.js +53 -0
  124. package/dist-lib/lib/aiPricing.functions.d.ts +31943 -0
  125. package/dist-lib/lib/aiPricing.functions.d.ts.map +1 -0
  126. package/dist-lib/lib/aiPricing.functions.js +13 -0
  127. package/dist-lib/lib/aiPricing.server.d.ts +19 -0
  128. package/dist-lib/lib/aiPricing.server.d.ts.map +1 -0
  129. package/dist-lib/lib/aiPricing.server.js +97 -0
  130. package/dist-lib/lib/aiUsage.functions.d.ts +5 -0
  131. package/dist-lib/lib/aiUsage.functions.d.ts.map +1 -0
  132. package/dist-lib/lib/aiUsage.functions.js +12 -0
  133. package/dist-lib/lib/aiUsage.server.d.ts +3 -0
  134. package/dist-lib/lib/aiUsage.server.d.ts.map +1 -0
  135. package/dist-lib/lib/aiUsage.server.js +229 -0
  136. package/dist-lib/lib/aiUsage.types.d.ts +21 -0
  137. package/dist-lib/lib/aiUsage.types.d.ts.map +1 -0
  138. package/dist-lib/lib/aiUsage.types.js +1 -0
  139. package/dist-lib/lib/aiUsageByModule.functions.d.ts +31952 -0
  140. package/dist-lib/lib/aiUsageByModule.functions.d.ts.map +1 -0
  141. package/dist-lib/lib/aiUsageByModule.functions.js +37 -0
  142. package/dist-lib/lib/modules.functions.d.ts +47924 -0
  143. package/dist-lib/lib/modules.functions.d.ts.map +1 -0
  144. package/dist-lib/lib/modules.functions.js +21 -0
  145. package/dist-lib/lib/modules.server.d.ts +23 -0
  146. package/dist-lib/lib/modules.server.d.ts.map +1 -0
  147. package/dist-lib/lib/modules.server.js +52 -0
  148. package/dist-lib/lib/utils.d.ts +3 -0
  149. package/dist-lib/lib/utils.d.ts.map +1 -0
  150. package/dist-lib/lib/utils.js +5 -0
  151. package/dist-lib/lib/validationSchemas.d.ts +15 -0
  152. package/dist-lib/lib/validationSchemas.d.ts.map +1 -0
  153. package/dist-lib/lib/validationSchemas.js +25 -0
  154. package/dist-lib/styles.css +1 -0
  155. package/package.json +92 -0
  156. package/public/flowselections-assets/ai-authenticatie/claude.png +0 -0
  157. package/public/flowselections-assets/ai-authenticatie/gemini.png +0 -0
  158. package/public/flowselections-assets/ai-authenticatie/openai.png +0 -0
  159. package/public/logo.png +0 -0
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=_core-safelist.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_core-safelist.d.ts","sourceRoot":"","sources":["../src/_core-safelist.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,CAAC"}
@@ -0,0 +1,15 @@
1
+ // Forceert Tailwind classes die uit @flowselections/core komen maar niet
2
+ // voorkomen in de eigen broncode van deze module.
3
+ // Zonder dit bestand worden deze classes weggeoptimaliseerd op de
4
+ // Lovable productie build.
5
+ const _safelist = [
6
+ // Layout — Sidebar en shell structuur
7
+ 'h-full', 'flex-col', 'flex-1', 'min-h-screen', 'ml-64', 'pt-16',
8
+ 'items-start', 'items-center', 'justify-between', 'border-t',
9
+ 'w-64', 'h-screen', 'h-20', 'overflow-y-auto',
10
+ // Grid — InstellingenPage
11
+ 'grid', 'lg:grid-cols-2', 'gap-6', 'p-6', 'space-y-6',
12
+ // Toggle/Switch — aan/uit zichtbaarheid
13
+ 'data-[state=checked]:bg-primary', 'data-[state=unchecked]:bg-input',
14
+ ];
15
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { SVGProps } from 'react';
2
+ export declare function AiAuthenticatieLogo(props: SVGProps<SVGSVGElement>): import("react").JSX.Element;
3
+ //# sourceMappingURL=AiAuthenticatieLogo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AiAuthenticatieLogo.d.ts","sourceRoot":"","sources":["../../src/components/AiAuthenticatieLogo.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEtC,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,QAAQ,CAAC,aAAa,CAAC,+BASjE"}
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function AiAuthenticatieLogo(props) {
3
+ return (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", ...props, children: [_jsx("path", { d: "M12 2l2.4 5.2L20 9l-4 3.9.9 5.6L12 15.8 7.1 18.5 8 12.9 4 9l5.6-1.8L12 2z" }), _jsx("circle", { cx: "18", cy: "18", r: "3" }), _jsx("path", { d: "M18 16v2l1.5 1" })] }));
4
+ }
@@ -0,0 +1,4 @@
1
+ export declare function AiAuthenticatiePage({ embedded }?: {
2
+ embedded?: boolean;
3
+ }): import("react").JSX.Element;
4
+ //# sourceMappingURL=AiAuthenticatiePage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AiAuthenticatiePage.d.ts","sourceRoot":"","sources":["../../src/components/AiAuthenticatiePage.tsx"],"names":[],"mappings":"AA6CA,wBAAgB,mBAAmB,CAAC,EAAE,QAAgB,EAAE,GAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAO,+BAiQpF"}
@@ -0,0 +1,145 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useMemo, useState } from 'react';
3
+ import { Link } from '@tanstack/react-router';
4
+ import { Button, Card, Skeleton, Badge, Switch, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, toast, } from '@flowselections/core';
5
+ import { KeyRound, ChevronRight, ShieldCheck, RefreshCw, TrendingUp, AlertCircle, ExternalLink, Plus, Loader2, Sparkles, Trash2, Settings2, Boxes, Code2, } from 'lucide-react';
6
+ import { useAiAdminData } from '../hooks/useAiAdmin';
7
+ import { useModelPricing, providerSummaries, formatUsd, formatTokens } from '../hooks/useAiPricing';
8
+ import { useAllProviderUsage } from '../hooks/useAiUsage';
9
+ import { useAiModules } from '../hooks/useAiModules';
10
+ import { useModules } from '../hooks/useModules';
11
+ import { useAiCostOverview } from '../hooks/useAiCostOverview';
12
+ import { periodFromPreset } from '../lib/aiPeriod';
13
+ import { AiPeriodFilter } from './AiPeriodFilter';
14
+ import { StatTile } from './AiStatTile';
15
+ import { ProviderLogo } from './ProviderLogo';
16
+ import { ProviderManageDialog } from './ProviderManageDialog';
17
+ import { ModuleAgentsDialog } from './ModuleAgentsDialog';
18
+ async function copyIntegrationSnippet(moduleKey) {
19
+ const snippet = `import { useAiForModule } from '@flowselections/ai-authenticatie';
20
+
21
+ const ai = useAiForModule('${moduleKey}');
22
+
23
+ const res = await ai.run([
24
+ { role: 'user', content: 'Jouw vraag hier' },
25
+ ]);
26
+ if (res.ok) console.log(res.content);`;
27
+ try {
28
+ await navigator.clipboard.writeText(snippet);
29
+ toast.success('Integratiecode gekopieerd');
30
+ }
31
+ catch {
32
+ toast.error('Kopiëren is niet gelukt');
33
+ }
34
+ }
35
+ /* ───────────────────────── Page ───────────────────────── */
36
+ export function AiAuthenticatiePage({ embedded = false } = {}) {
37
+ const { providers, models, credentials, bindings, loading, reload } = useAiAdminData();
38
+ const { modules, loading: modulesLoading, save: saveModule, setEnabled, remove: removeModule } = useAiModules();
39
+ const [period, setPeriod] = useState(() => periodFromPreset('month'));
40
+ const { overview, loading: overviewLoading, reload: reloadOverview } = useAiCostOverview(period.from, period.to);
41
+ const safeProviders = Array.isArray(providers) ? providers : [];
42
+ const safeModels = Array.isArray(models) ? models : [];
43
+ const safeCreds = Array.isArray(credentials) ? credentials : [];
44
+ const safeBindings = Array.isArray(bindings) ? bindings : [];
45
+ const safeModules = Array.isArray(modules) ? modules : [];
46
+ const aiModules = safeModules.filter(m => m.ai_enabled);
47
+ const statsByModule = useMemo(() => {
48
+ const rows = Array.isArray(overview?.byModule) ? overview.byModule : [];
49
+ return new Map(rows.map(r => [r.module_key, r]));
50
+ }, [overview]);
51
+ const [keysOpen, setKeysOpen] = useState(false);
52
+ const [addModuleOpen, setAddModuleOpen] = useState(false);
53
+ const [agentsFor, setAgentsFor] = useState(null);
54
+ const [manageProvider, setManageProvider] = useState(null);
55
+ const totals = overview?.totals;
56
+ const handleToggle = async (m, value) => {
57
+ const res = await setEnabled(m.module_key, value);
58
+ res.ok ? toast.success(res.message) : toast.error(res.message);
59
+ };
60
+ const handleDelete = async (m) => {
61
+ if (!confirm(`Module "${m.module_name}" verwijderen uit de AI-instellingen?`))
62
+ return;
63
+ const res = await removeModule(m.module_key);
64
+ res.ok ? toast.success(res.message) : toast.error(res.message);
65
+ };
66
+ const Wrapper = embedded ? 'div' : 'main';
67
+ const wrapperClass = embedded ? 'space-y-6' : 'mx-auto max-w-6xl p-6 space-y-6';
68
+ const SectionBox = embedded ? 'div' : Card;
69
+ const sectionClass = embedded
70
+ ? 'rounded-xl border overflow-hidden bg-background/40'
71
+ : 'rounded-2xl overflow-hidden p-0';
72
+ return (_jsxs(Wrapper, { className: wrapperClass, children: [!embedded && (_jsxs("header", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "AI-instellingen" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Beheer per module welke AI-agents gebruikt worden en bekijk de kosten." })] })), _jsxs("div", { className: "flex items-center justify-between gap-3 flex-wrap", children: [_jsx("p", { className: "text-sm font-semibold", children: "Totaaloverzicht" }), _jsx(AiPeriodFilter, { period: period, onChange: setPeriod })] }), _jsxs("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-3", children: [_jsx(StatTile, { label: "Totale AI-kosten", value: overviewLoading ? null : formatUsd(totals?.cost_usd ?? 0), sub: "Alle modules samen" }), _jsx(StatTile, { label: "Aanvragen", value: overviewLoading ? null : String(totals?.calls ?? 0), sub: `${totals?.errors ?? 0} met fout` }), _jsx(StatTile, { label: "Tokens", value: overviewLoading ? null : formatTokens(totals?.total_tokens ?? 0), sub: "In- en uitvoer" }), _jsx(StatTile, { label: "Gem. per aanvraag", value: overviewLoading ? null : formatUsd(totals?.avg_cost_per_call ?? 0), sub: "In de gekozen periode" })] }), _jsxs(SectionBox, { className: sectionClass, children: [_jsxs("div", { className: "flex items-center justify-between px-4 md:px-6 py-4 border-b bg-muted/20", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Boxes, { className: "h-4 w-4 text-primary" }), _jsx("p", { className: "text-sm font-semibold", children: "Modules met AI" })] }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => { reload(); void reloadOverview(); }, children: _jsx(RefreshCw, { className: `h-3.5 w-3.5 ${overviewLoading ? 'animate-spin' : ''}` }) })] }), _jsxs("div", { className: "hidden md:grid grid-cols-[1.4fr_1.2fr_.7fr_.7fr_.8fr_auto] gap-4 px-6 py-3 border-b bg-muted/10 text-xs font-semibold text-muted-foreground", children: [_jsx("span", { children: "Module" }), _jsx("span", { children: "AI-agent / model" }), _jsx("span", { className: "text-right", children: "Aanvragen" }), _jsx("span", { className: "text-right", children: "Tokens" }), _jsx("span", { className: "text-right", children: "Kosten" }), _jsx("span", { className: "text-right pl-2", children: "Status & acties" })] }), loading || modulesLoading ? (_jsxs("div", { className: "p-6 space-y-3", children: [_jsx(Skeleton, { className: "h-14 w-full" }), _jsx(Skeleton, { className: "h-14 w-full" })] })) : aiModules.length === 0 ? (_jsx("div", { className: "p-6 text-sm text-muted-foreground", children: "Er zijn nog geen modules met AI ingeschakeld. Voeg een module toe of zet AI aan bij een bestaande module." })) : (_jsx("ul", { className: "divide-y", children: aiModules.map(m => {
73
+ const moduleBindings = safeBindings.filter(b => b.consumer_module_id === m.module_key);
74
+ const primary = moduleBindings.find(b => b.is_default) ?? moduleBindings[0] ?? null;
75
+ const primaryModel = primary ? safeModels.find(x => x.id === primary.model_id) : undefined;
76
+ const primaryProvider = primaryModel ? safeProviders.find(p => p.id === primaryModel.provider_id) : undefined;
77
+ const s = statsByModule.get(m.module_key);
78
+ return (_jsxs("li", { className: "grid grid-cols-1 md:grid-cols-[1.4fr_1.2fr_.7fr_.7fr_.8fr_auto] gap-3 md:gap-4 items-center px-4 md:px-6 py-4", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "font-semibold text-sm truncate", children: m.module_name }), _jsx("p", { className: "text-xs text-muted-foreground truncate", children: m.module_key })] }), _jsx("button", { type: "button", onClick: () => setAgentsFor(m), className: "text-left min-w-0 rounded-lg hover:bg-muted/40 px-2 py-1 -mx-2", children: primary ? (_jsxs("span", { className: "flex items-center gap-2 min-w-0", children: [primaryProvider && _jsx(ProviderLogo, { slug: primaryProvider.slug, name: primaryProvider.name, className: "h-4 w-4 shrink-0" }), _jsxs("span", { className: "min-w-0", children: [_jsx("span", { className: "block text-sm truncate", children: primary.label || primaryModel?.display_name || 'Onbekend model' }), _jsxs("span", { className: "block text-xs text-muted-foreground truncate", children: [primaryProvider?.name ?? '—', moduleBindings.length > 1 && ` · +${moduleBindings.length - 1} agent${moduleBindings.length > 2 ? 's' : ''}`] })] })] })) : (_jsxs("span", { className: "text-sm text-primary inline-flex items-center gap-1", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), " AI-agent koppelen"] })) }), _jsx("span", { className: "text-sm md:text-right", children: s?.calls ?? 0 }), _jsx("span", { className: "text-sm md:text-right", children: formatTokens(s?.total_tokens ?? 0) }), _jsx("span", { className: "text-sm md:text-right font-medium", children: formatUsd(s?.cost_usd ?? 0) }), _jsxs("div", { className: "flex items-center justify-end gap-2 md:pl-2", children: [_jsx(Switch, { checked: m.ai_enabled, onCheckedChange: v => handleToggle(m, v), title: "AI in-/uitschakelen" }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-9 w-9", title: "Integratiecode kopi\u00EBren", onClick: () => void copyIntegrationSnippet(m.module_key), children: _jsx(Code2, { className: "h-4 w-4" }) }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-9 w-9", title: "AI-agents beheren", onClick: () => setAgentsFor(m), children: _jsx(Settings2, { className: "h-4 w-4" }) }), _jsx(Button, { asChild: true, variant: "outline", size: "icon", className: "h-9 w-9", title: "Details bekijken", children: _jsx(Link, { to: "/ai-authenticatie/$moduleKey", params: { moduleKey: m.module_key }, children: _jsx(ChevronRight, { className: "h-4 w-4" }) }) }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-9 w-9 text-muted-foreground hover:text-destructive", title: "Module verwijderen", onClick: () => handleDelete(m), children: _jsx(Trash2, { className: "h-4 w-4" }) })] })] }, m.id));
79
+ }) })), _jsxs("div", { className: "border-t px-4 md:px-6 py-3 flex items-center justify-between gap-3 bg-muted/10", children: [_jsx("p", { className: "text-xs text-muted-foreground", children: "Modules met AI uitgeschakeld worden hier niet getoond en kunnen geen AI-agent gebruiken." }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setAddModuleOpen(true), children: [_jsx(Plus, { className: "h-4 w-4 mr-1.5" }), " Module toevoegen"] })] })] }), !modulesLoading && safeModules.some(m => !m.ai_enabled) && (_jsxs("div", { className: `${embedded ? 'rounded-xl border bg-background/40' : 'rounded-2xl border bg-card shadow-sm'} p-4 space-y-2`, children: [_jsx("p", { className: "text-sm font-semibold", children: "Modules zonder AI" }), _jsx("ul", { className: "divide-y", children: safeModules.filter(m => !m.ai_enabled).map(m => (_jsxs("li", { className: "flex items-center justify-between gap-3 py-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-sm truncate", children: m.module_name }), _jsx("p", { className: "text-xs text-muted-foreground truncate", children: m.module_key })] }), _jsx(Switch, { checked: false, onCheckedChange: () => handleToggle(m, true), title: "AI inschakelen" })] }, m.id))) })] })), _jsx(ModuleDialog, { open: addModuleOpen, onOpenChange: setAddModuleOpen, existingKeys: safeModules.map(m => m.module_key), onSubmit: (key, name, enabled) => saveModule({ module_key: key, module_name: name, ai_enabled: enabled }) }), agentsFor && (_jsx(ModuleAgentsDialog, { module: agentsFor, open: !!agentsFor, onOpenChange: o => { if (!o)
80
+ setAgentsFor(null); }, providers: safeProviders, models: safeModels, credentials: safeCreds, bindings: safeBindings.filter(b => b.consumer_module_id === agentsFor.module_key), onChange: reload, onOpenProvider: p => { setAgentsFor(null); setManageProvider(p); } })), _jsx("button", { onClick: () => setKeysOpen(true), className: "w-full text-left", children: _jsxs("div", { className: `${embedded ? 'rounded-xl border bg-background/40' : 'rounded-2xl border bg-card shadow-sm'} p-5 flex items-center gap-4 hover:border-primary/60 hover:bg-muted/30 transition-colors`, children: [_jsx("div", { className: "h-11 w-11 rounded-xl bg-primary/10 text-primary flex items-center justify-center shrink-0", children: _jsx(KeyRound, { className: "h-5 w-5" }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "font-semibold text-sm", children: "API sleutels beheren" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Beheer je API sleutels voor de verschillende AI providers." })] }), _jsx(ChevronRight, { className: "h-5 w-5 text-muted-foreground shrink-0" })] }) }), _jsx(ApiKeysDialog, { open: keysOpen, onOpenChange: setKeysOpen, providers: safeProviders, models: safeModels, credentials: safeCreds, onOpenProvider: p => { setKeysOpen(false); setManageProvider(p); } }), manageProvider && (_jsx(ProviderManageDialog, { provider: manageProvider, credentials: safeCreds.filter(c => c.provider_id === manageProvider.id), open: !!manageProvider, onOpenChange: o => { if (!o) {
81
+ setManageProvider(null);
82
+ setKeysOpen(true);
83
+ } }, onChange: reload }))] }));
84
+ }
85
+ /* ───────────────────────── Module toevoegen ───────────────────────── */
86
+ function ModuleDialog({ open, onOpenChange, onSubmit, existingKeys, }) {
87
+ const { registered, loading } = useModules();
88
+ const [selected, setSelected] = useState('');
89
+ const [enabled, setEnabled] = useState(true);
90
+ const [saving, setSaving] = useState(false);
91
+ const options = (Array.isArray(registered) ? registered : [])
92
+ .filter(m => m.is_active && !existingKeys.includes(m.module_key));
93
+ const chosen = options.find(m => m.module_key === selected) ?? null;
94
+ const close = (v) => {
95
+ if (!v) {
96
+ setSelected('');
97
+ setEnabled(true);
98
+ }
99
+ onOpenChange(v);
100
+ };
101
+ const submit = async () => {
102
+ if (!chosen) {
103
+ toast.error('Kies eerst een module.');
104
+ return;
105
+ }
106
+ setSaving(true);
107
+ try {
108
+ const res = await onSubmit(chosen.module_key, chosen.module_name, enabled);
109
+ if (res.ok) {
110
+ toast.success(res.message);
111
+ close(false);
112
+ }
113
+ else
114
+ toast.error(res.message);
115
+ }
116
+ finally {
117
+ setSaving(false);
118
+ }
119
+ };
120
+ return (_jsx(Dialog, { open: open, onOpenChange: close, children: _jsxs(DialogContent, { className: "max-w-md", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(Sparkles, { className: "h-4 w-4 text-primary" }), " Module toevoegen"] }), _jsx(DialogDescription, { children: "Kies een module die op dit systeem is aangesloten. Je koppelt daarna \u00E9\u00E9n of meerdere AI-agents." })] }), _jsxs("div", { className: "space-y-4 py-2", children: [loading ? (_jsx(Skeleton, { className: "h-24 w-full rounded-xl" })) : options.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground rounded-xl border p-3", children: "Alle aangesloten modules zijn al toegevoegd." })) : (_jsx("ul", { className: "space-y-2 max-h-64 overflow-auto", children: options.map(m => (_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => setSelected(m.module_key), className: `w-full text-left rounded-xl border p-3 transition-colors ${selected === m.module_key ? 'border-primary bg-primary/5' : 'hover:bg-muted/40'}`, children: [_jsx("p", { className: "text-sm font-medium truncate", children: m.module_name }), m.description && (_jsx("p", { className: "text-xs text-muted-foreground truncate", children: m.description }))] }) }, m.module_key))) })), _jsxs("div", { className: "flex items-center justify-between rounded-xl border p-3", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium", children: "AI inschakelen" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Alleen ingeschakelde modules kunnen AI gebruiken." })] }), _jsx(Switch, { checked: enabled, onCheckedChange: setEnabled })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => close(false), disabled: saving, children: "Annuleren" }), _jsxs(Button, { onClick: submit, disabled: saving || !chosen, children: [saving ? _jsx(Loader2, { className: "h-4 w-4 mr-1.5 animate-spin" }) : _jsx(Plus, { className: "h-4 w-4 mr-1.5" }), "Module toevoegen"] })] })] }) }));
121
+ }
122
+ /* ───────────────────────── API keys dialog ───────────────────────── */
123
+ function ApiKeysDialog({ open, onOpenChange, providers, models, credentials, onOpenProvider, }) {
124
+ const { rows: pricingRows, loading: pricingLoading, refreshing, refresh } = useModelPricing();
125
+ const providerSlugs = useMemo(() => providers.map(p => p.slug.toLowerCase()), [providers]);
126
+ const { byProvider: usage, loading: usageLoading, reload: reloadUsage } = useAllProviderUsage(providerSlugs);
127
+ const summaries = useMemo(() => providerSummaries(pricingRows), [pricingRows]);
128
+ const totalMonth = Object.values(usage).reduce((s, u) => s + (u?.total_cost_usd ?? 0), 0);
129
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-4xl max-h-[90vh] overflow-y-auto", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "API sleutels beheren" }), _jsx(DialogDescription, { children: "Beheer je API sleutels, bekijk actuele tarieven en het verbruik van deze maand." })] }), _jsxs("section", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(TrendingUp, { className: "h-4 w-4 text-primary" }), _jsx("h3", { className: "text-sm font-semibold", children: "Kosten deze maand" }), !usageLoading && (_jsxs(Badge, { variant: "secondary", className: "text-xs", children: ["Totaal: ", formatUsd(totalMonth)] }))] }), _jsxs(Button, { variant: "ghost", size: "sm", onClick: reloadUsage, disabled: usageLoading, children: [_jsx(RefreshCw, { className: `h-3.5 w-3.5 mr-1 ${usageLoading ? 'animate-spin' : ''}` }), "Vernieuwen"] })] }), _jsx("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-3", children: providers.map(p => (_jsx(UsageCard, { provider: p, usage: usage[p.slug.toLowerCase()] ?? null, loading: usageLoading, onManage: () => onOpenProvider(p) }, p.id))) })] }), _jsxs("section", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h3", { className: "text-sm font-semibold", children: "Providers" }), _jsxs(Button, { variant: "ghost", size: "sm", onClick: () => refresh(true), disabled: refreshing, children: [_jsx(RefreshCw, { className: `h-3.5 w-3.5 mr-1 ${refreshing ? 'animate-spin' : ''}` }), "Tarieven bijwerken"] })] }), _jsxs(Card, { className: "rounded-2xl overflow-hidden p-0", children: [_jsxs("div", { className: "hidden md:grid grid-cols-[1.4fr_1fr_1fr_1fr_auto] gap-4 px-5 py-3 border-b bg-muted/20 text-sm font-semibold", children: [_jsx("span", { children: "Provider" }), _jsx("span", { className: "text-center", children: "Modellen" }), _jsx("span", { className: "text-center", children: "Sleutels" }), _jsx("span", { className: "text-center", children: "Vanaf tarief" }), _jsx("span", { className: "text-right pr-1", children: "Acties" })] }), _jsx("ul", { className: "divide-y", children: providers.map(p => {
130
+ const modelCount = models.filter(m => m.provider_id === p.id && m.active).length;
131
+ const credCount = credentials.filter(c => c.provider_id === p.id && c.active).length;
132
+ const summary = summaries.get(p.slug.toLowerCase());
133
+ return (_jsxs("li", { className: "grid grid-cols-1 md:grid-cols-[1.4fr_1fr_1fr_1fr_auto] gap-3 md:gap-4 items-center px-5 py-4", children: [_jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [_jsx(ProviderLogo, { slug: p.slug, name: p.name, className: "h-8 w-8 shrink-0" }), _jsx("span", { className: "font-semibold text-sm truncate", children: p.name })] }), _jsxs("span", { className: "text-sm text-center", children: [modelCount, " ", _jsxs("span", { className: "text-muted-foreground", children: ["model", modelCount === 1 ? '' : 'len'] })] }), _jsxs("span", { className: "text-sm text-center", children: [credCount, " ", _jsxs("span", { className: "text-muted-foreground", children: ["sleutel", credCount === 1 ? '' : 's'] })] }), _jsx("span", { className: "text-sm text-center text-muted-foreground", children: pricingLoading && !summary
134
+ ? _jsx(Skeleton, { className: "h-3 w-16 mx-auto" })
135
+ : summary?.min_input != null
136
+ ? _jsxs(_Fragment, { children: ["vanaf ", _jsx("span", { className: "text-foreground font-medium", children: formatUsd(summary.min_input, 2) }), " / 1M tokens"] })
137
+ : '—' }), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { variant: "outline", size: "sm", onClick: () => onOpenProvider(p), children: "Beheren" }) })] }, p.id));
138
+ }) })] }), _jsx("p", { className: "text-xs text-muted-foreground pl-1", children: "Tarieven van models.dev, dagelijks bijgewerkt. Getoond per 1 miljoen tokens in USD." })] }), _jsxs("div", { className: "flex items-center gap-3 rounded-xl border bg-muted/30 p-4", children: [_jsx("div", { className: "h-9 w-9 rounded-lg bg-primary/10 text-primary flex items-center justify-center shrink-0", children: _jsx(ShieldCheck, { className: "h-5 w-5" }) }), _jsxs("div", { children: [_jsx("p", { className: "font-semibold text-sm", children: "Je API sleutels zijn veilig" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "We delen geen gevoelige informatie met derden. Alles blijft binnen jouw omgeving." })] })] }), _jsx(DialogFooter, { children: _jsx(Button, { onClick: () => onOpenChange(false), children: "Sluiten" }) })] }) }));
139
+ }
140
+ function UsageCard({ provider, usage, loading, onManage, }) {
141
+ const supported = usage?.supported ?? true;
142
+ const hasData = usage?.ok && usage.total_cost_usd >= 0;
143
+ const missingKey = usage && !usage.ok && supported;
144
+ return (_jsxs(Card, { className: "rounded-2xl p-4 space-y-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(ProviderLogo, { slug: provider.slug, name: provider.name, className: "h-6 w-6" }), _jsx("span", { className: "font-semibold text-sm truncate", children: provider.name })] }), loading && !usage ? (_jsxs("div", { className: "space-y-2", children: [_jsx(Skeleton, { className: "h-7 w-24" }), _jsx(Skeleton, { className: "h-3 w-32" })] })) : !supported ? (_jsxs("div", { className: "space-y-2", children: [_jsx("p", { className: "text-xs text-muted-foreground", children: "Google biedt geen kostenoverzicht via API." }), _jsxs("a", { href: "https://console.cloud.google.com/billing", target: "_blank", rel: "noreferrer", className: "text-xs text-primary inline-flex items-center gap-1 hover:underline", children: ["Bekijk in Google Cloud ", _jsx(ExternalLink, { className: "h-3 w-3" })] })] })) : hasData ? (_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-2xl font-bold tracking-tight", children: formatUsd(usage.total_cost_usd) }), _jsxs("p", { className: "text-xs text-muted-foreground", children: [formatTokens(usage.input_tokens), " in \u00B7 ", formatTokens(usage.output_tokens), " uit"] })] })) : missingKey ? (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx(AlertCircle, { className: "h-3.5 w-3.5 text-amber-600 mt-0.5 shrink-0" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Admin-sleutel ontbreekt. Voeg toe om verbruik te tonen." })] }), _jsx(Button, { size: "sm", variant: "outline", onClick: onManage, className: "h-7 text-xs", children: "Sleutel toevoegen" })] })) : (_jsx("p", { className: "text-xs text-muted-foreground", children: "Geen gegevens beschikbaar." }))] }));
145
+ }
@@ -0,0 +1,2 @@
1
+ export declare function AiModuleDetailPage(): import("react").JSX.Element;
2
+ //# sourceMappingURL=AiModuleDetailPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AiModuleDetailPage.d.ts","sourceRoot":"","sources":["../../src/components/AiModuleDetailPage.tsx"],"names":[],"mappings":"AAcA,wBAAgB,kBAAkB,gCAgGjC"}
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useState } from 'react';
3
+ import { Link, useParams } from '@tanstack/react-router';
4
+ import { useServerFn } from '@tanstack/react-start';
5
+ import { Button, Card, Skeleton, Badge } from '@flowselections/core';
6
+ import { ArrowLeft, RefreshCw } from 'lucide-react';
7
+ import { getAiModuleCalls } from '../lib/aiModules.functions';
8
+ import { useAiModules } from '../hooks/useAiModules';
9
+ import { useAiCostOverview } from '../hooks/useAiCostOverview';
10
+ import { periodFromPreset } from '../lib/aiPeriod';
11
+ import { AiPeriodFilter } from './AiPeriodFilter';
12
+ import { formatUsd, formatTokens } from '../hooks/useAiPricing';
13
+ import { StatTile } from './AiStatTile';
14
+ import { AiModuleTestPanel } from './AiModuleTestPanel';
15
+ export function AiModuleDetailPage() {
16
+ const { moduleKey } = useParams({ strict: false });
17
+ const key = moduleKey ?? '';
18
+ const [period, setPeriod] = useState(() => periodFromPreset('month'));
19
+ const { modules } = useAiModules();
20
+ const { overview, loading: overviewLoading, reload: reloadOverview } = useAiCostOverview(period.from, period.to);
21
+ const module = (Array.isArray(modules) ? modules : []).find(m => m.module_key === key) ?? null;
22
+ const stats = (Array.isArray(overview?.byModule) ? overview.byModule : []).find(s => s.module_key === key) ?? null;
23
+ const callsFn = useServerFn(getAiModuleCalls);
24
+ const [calls, setCalls] = useState([]);
25
+ const [loading, setLoading] = useState(true);
26
+ const loadCalls = useCallback(async () => {
27
+ setLoading(true);
28
+ try {
29
+ const rows = await callsFn({ data: { module_key: key, from: period.from, to: period.to, limit: 200 } });
30
+ setCalls(Array.isArray(rows) ? rows : []);
31
+ }
32
+ catch {
33
+ setCalls([]);
34
+ }
35
+ finally {
36
+ setLoading(false);
37
+ }
38
+ }, [callsFn, key, period.from, period.to]);
39
+ useEffect(() => { void loadCalls(); }, [loadCalls]);
40
+ const title = module?.module_name ?? key;
41
+ const safeCalls = useMemo(() => (Array.isArray(calls) ? calls : []), [calls]);
42
+ return (_jsxs("main", { className: "mx-auto max-w-5xl p-6 space-y-6", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { children: [_jsxs(Link, { to: "/ai-authenticatie", className: "text-xs text-muted-foreground inline-flex items-center gap-1 hover:text-foreground", children: [_jsx(ArrowLeft, { className: "h-3.5 w-3.5" }), " Terug naar AI-instellingen"] }), _jsx("h1", { className: "text-2xl font-bold tracking-tight mt-1", children: title }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Alle AI-aanroepen en kosten van deze module." })] }), _jsx(AiPeriodFilter, { period: period, onChange: setPeriod })] }), _jsxs("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-3", children: [_jsx(StatTile, { label: "Kosten", value: overviewLoading ? null : formatUsd(stats?.cost_usd ?? 0) }), _jsx(StatTile, { label: "Aanvragen", value: overviewLoading ? null : String(stats?.calls ?? 0) }), _jsx(StatTile, { label: "Tokens", value: overviewLoading ? null : formatTokens(stats?.total_tokens ?? 0) }), _jsx(StatTile, { label: "Gem. per aanvraag", value: overviewLoading ? null : formatUsd(stats && stats.calls > 0 ? stats.cost_usd / stats.calls : 0) })] }), key && _jsx(AiModuleTestPanel, { moduleKey: key }), _jsxs(Card, { className: "rounded-2xl p-0 overflow-hidden", children: [_jsxs("div", { className: "flex items-center justify-between px-5 py-3 border-b bg-muted/20", children: [_jsx("p", { className: "text-sm font-semibold", children: "AI-aanroepen" }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => { void loadCalls(); void reloadOverview(); }, children: _jsx(RefreshCw, { className: `h-3.5 w-3.5 ${loading ? 'animate-spin' : ''}` }) })] }), _jsxs("div", { className: "hidden md:grid grid-cols-[1.2fr_1fr_1fr_.7fr_.7fr_.7fr_.6fr] gap-3 px-5 py-2 border-b bg-muted/10 text-xs font-semibold text-muted-foreground", children: [_jsx("span", { children: "Datum/tijd" }), _jsx("span", { children: "Provider" }), _jsx("span", { children: "Model" }), _jsx("span", { className: "text-right", children: "In" }), _jsx("span", { className: "text-right", children: "Uit" }), _jsx("span", { className: "text-right", children: "Kosten" }), _jsx("span", { className: "text-right", children: "Duur" })] }), loading && safeCalls.length === 0 ? (_jsxs("div", { className: "p-5 space-y-2", children: [_jsx(Skeleton, { className: "h-10 w-full" }), _jsx(Skeleton, { className: "h-10 w-full" })] })) : safeCalls.length === 0 ? (_jsx("p", { className: "p-5 text-sm text-muted-foreground", children: "Geen AI-aanroepen in deze periode." })) : (_jsx("ul", { className: "divide-y", children: safeCalls.map(c => (_jsxs("li", { className: "grid grid-cols-2 md:grid-cols-[1.2fr_1fr_1fr_.7fr_.7fr_.7fr_.6fr] gap-2 md:gap-3 px-5 py-3 text-sm items-center", children: [_jsxs("span", { className: "text-xs md:text-sm", children: [new Date(c.created_at).toLocaleString('nl-NL'), c.status && c.status !== 'ok' && c.status !== 'success' && (_jsx(Badge, { variant: "destructive", className: "ml-2 text-[10px]", children: "fout" }))] }), _jsx("span", { className: "truncate", children: c.provider_slug ?? '—' }), _jsx("span", { className: "truncate", children: c.model_slug ?? '—' }), _jsx("span", { className: "text-right", children: formatTokens(c.input_tokens) }), _jsx("span", { className: "text-right", children: formatTokens(c.output_tokens) }), _jsx("span", { className: "text-right font-medium", children: formatUsd(c.cost_usd) }), _jsx("span", { className: "text-right text-muted-foreground", children: c.duration_ms != null ? `${Math.round(c.duration_ms)} ms` : '—' })] }, c.id))) }))] })] }));
43
+ }
@@ -0,0 +1,4 @@
1
+ export declare function AiModuleTestPanel({ moduleKey }: {
2
+ moduleKey: string;
3
+ }): import("react").JSX.Element;
4
+ //# sourceMappingURL=AiModuleTestPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AiModuleTestPanel.d.ts","sourceRoot":"","sources":["../../src/components/AiModuleTestPanel.tsx"],"names":[],"mappings":"AAOA,wBAAgB,iBAAiB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,+BA8ErE"}
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Button, Card, Badge, Textarea, toast } from '@flowselections/core';
4
+ import { Play, Loader2, AlertCircle } from 'lucide-react';
5
+ import { useAiForModule } from '../hooks/useAiForModule';
6
+ import { formatUsd, formatTokens } from '../hooks/useAiPricing';
7
+ export function AiModuleTestPanel({ moduleKey }) {
8
+ const ai = useAiForModule(moduleKey);
9
+ const [prompt, setPrompt] = useState('Geef een korte testreactie in het Nederlands.');
10
+ const [busy, setBusy] = useState(false);
11
+ const [result, setResult] = useState(null);
12
+ const send = async () => {
13
+ const text = prompt.trim();
14
+ if (!text) {
15
+ toast.error('Vul eerst een vraag in.');
16
+ return;
17
+ }
18
+ setBusy(true);
19
+ setResult(null);
20
+ try {
21
+ const res = await ai.run([{ role: 'user', content: text }]);
22
+ setResult(res ?? null);
23
+ if (res && !res.ok)
24
+ toast.error(res.message);
25
+ }
26
+ catch (e) {
27
+ toast.error(e instanceof Error ? e.message : 'Onbekende fout');
28
+ }
29
+ finally {
30
+ setBusy(false);
31
+ }
32
+ };
33
+ return (_jsxs(Card, { className: "rounded-2xl p-0 overflow-hidden", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 px-5 py-3 border-b bg-muted/20", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-sm font-semibold", children: "Agent testen" }), _jsx("p", { className: "text-xs text-muted-foreground truncate", children: ai.loading
34
+ ? 'Koppeling controleren…'
35
+ : ai.ready
36
+ ? `${ai.provider?.name ?? '—'} · ${ai.model?.displayName ?? ai.model?.slug ?? '—'}`
37
+ : (ai.reason ?? 'Geen AI-agent gekoppeld aan deze module.') })] }), !ai.loading && (_jsx(Badge, { variant: ai.ready ? 'secondary' : 'destructive', className: "shrink-0", children: ai.ready ? 'Klaar voor gebruik' : 'Niet gekoppeld' }))] }), _jsxs("div", { className: "p-5 space-y-3", children: [_jsx(Textarea, { value: prompt, onChange: e => setPrompt(e.target.value), rows: 3, placeholder: "Typ een testvraag\u2026", disabled: !ai.ready || busy }), _jsx("div", { className: "flex items-center justify-end", children: _jsxs(Button, { onClick: () => void send(), disabled: !ai.ready || busy, children: [busy ? _jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : _jsx(Play, { className: "h-4 w-4" }), _jsx("span", { className: "ml-2", children: "Versturen" })] }) }), result && result.ok && (_jsxs("div", { className: "rounded-xl border bg-muted/20 p-4 space-y-3", children: [_jsx("p", { className: "text-sm whitespace-pre-wrap", children: result.content || '(leeg antwoord)' }), _jsxs("div", { className: "flex flex-wrap gap-4 text-xs text-muted-foreground border-t pt-2", children: [_jsxs("span", { children: ["In: ", formatTokens(result.usage.inputTokens)] }), _jsxs("span", { children: ["Uit: ", formatTokens(result.usage.outputTokens)] }), _jsxs("span", { children: ["Totaal: ", formatTokens(result.usage.totalTokens)] }), _jsxs("span", { children: ["Kosten: ", formatUsd(result.costUsd ?? 0)] })] })] })), result && !result.ok && (_jsxs("div", { className: "rounded-xl border border-destructive/40 bg-destructive/5 p-4 flex gap-2 text-sm", children: [_jsx(AlertCircle, { className: "h-4 w-4 text-destructive shrink-0 mt-0.5" }), _jsx("span", { children: result.message })] }))] })] }));
38
+ }
@@ -0,0 +1,7 @@
1
+ import { type AiPeriod } from '../lib/aiPeriod';
2
+ export declare function AiPeriodFilter({ period, onChange, className, }: {
3
+ period: AiPeriod;
4
+ onChange: (p: AiPeriod) => void;
5
+ className?: string;
6
+ }): import("react").JSX.Element;
7
+ //# sourceMappingURL=AiPeriodFilter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AiPeriodFilter.d.ts","sourceRoot":"","sources":["../../src/components/AiPeriodFilter.tsx"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,QAAQ,EACd,MAAM,iBAAiB,CAAC;AAEzB,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EAAE,QAAQ,EAAE,SAAc,GACjC,EAAE;IACD,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,IAAI,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,+BAoDA"}
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Button, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@flowselections/core';
3
+ import { CalendarRange } from 'lucide-react';
4
+ import { PERIOD_LABELS, fromDateInput, periodFromPreset, toDateInput, } from '../lib/aiPeriod';
5
+ export function AiPeriodFilter({ period, onChange, className = '', }) {
6
+ const presets = ['today', 'week', 'month', 'year', 'custom'];
7
+ return (_jsxs("div", { className: `flex flex-wrap items-center gap-2 ${className}`, children: [_jsx(CalendarRange, { className: "h-4 w-4 text-muted-foreground" }), _jsxs(Select, { value: period.preset, onValueChange: (v) => {
8
+ const preset = v;
9
+ if (preset === 'custom')
10
+ onChange({ ...period, preset });
11
+ else
12
+ onChange(periodFromPreset(preset));
13
+ }, children: [_jsx(SelectTrigger, { className: "w-[160px] h-9", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: presets.map(p => (_jsx(SelectItem, { value: p, children: PERIOD_LABELS[p] }, p))) })] }), period.preset === 'custom' && (_jsxs(_Fragment, { children: [_jsx(Input, { type: "date", className: "h-9 w-[150px]", value: toDateInput(period.from), onChange: e => onChange({ ...period, from: fromDateInput(e.target.value) }) }), _jsx("span", { className: "text-xs text-muted-foreground", children: "t/m" }), _jsx(Input, { type: "date", className: "h-9 w-[150px]", value: toDateInput(period.to), onChange: e => onChange({ ...period, to: fromDateInput(e.target.value, true) }) })] })), _jsx(Button, { variant: "ghost", size: "sm", className: "h-9", onClick: () => onChange(periodFromPreset(period.preset === 'custom' ? 'month' : period.preset)), children: "Vernieuwen" })] }));
14
+ }
@@ -0,0 +1,6 @@
1
+ export declare function StatTile({ label, value, sub }: {
2
+ label: string;
3
+ value: string | null;
4
+ sub?: string;
5
+ }): import("react").JSX.Element;
6
+ //# sourceMappingURL=AiStatTile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AiStatTile.d.ts","sourceRoot":"","sources":["../../src/components/AiStatTile.tsx"],"names":[],"mappings":"AAEA,wBAAgB,QAAQ,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,+BAQpG"}
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Card, Skeleton } from '@flowselections/core';
3
+ export function StatTile({ label, value, sub }) {
4
+ return (_jsxs(Card, { className: "rounded-2xl p-4 space-y-1", children: [_jsx("p", { className: "text-xs text-muted-foreground", children: label }), value === null ? _jsx(Skeleton, { className: "h-7 w-20" }) : _jsx("p", { className: "text-2xl font-bold tracking-tight", children: value }), sub && _jsx("p", { className: "text-xs text-muted-foreground", children: sub })] }));
5
+ }
@@ -0,0 +1,14 @@
1
+ import type { AiProvider, AiModel, AiCredential, AiBinding } from '../hooks/useAiAdmin';
2
+ import type { AiModuleRow } from '../lib/aiModules.functions';
3
+ export declare function ModuleAgentsDialog({ module, open, onOpenChange, providers, models, credentials, bindings, onChange, onOpenProvider, }: {
4
+ module: AiModuleRow;
5
+ open: boolean;
6
+ onOpenChange: (v: boolean) => void;
7
+ providers: AiProvider[];
8
+ models: AiModel[];
9
+ credentials: AiCredential[];
10
+ bindings: AiBinding[];
11
+ onChange: () => void;
12
+ onOpenProvider: (p: AiProvider) => void;
13
+ }): import("react").JSX.Element;
14
+ //# sourceMappingURL=ModuleAgentsDialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModuleAgentsDialog.d.ts","sourceRoot":"","sources":["../../src/components/ModuleAgentsDialog.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACxF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAG9D,wBAAgB,kBAAkB,CAAC,EACjC,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,cAAc,GAC/F,EAAE;IACD,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,SAAS,EAAE,UAAU,EAAE,CAAC;IACxB,MAAM,EAAE,OAAO,EAAE,CAAC;IAClB,WAAW,EAAE,YAAY,EAAE,CAAC;IAC5B,QAAQ,EAAE,SAAS,EAAE,CAAC;IACtB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,cAAc,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;CACzC,+BA0KA"}
@@ -0,0 +1,78 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Button, Badge, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, supabase, toast, } from '@flowselections/core';
4
+ import { Plus, Trash2, Star, KeyRound } from 'lucide-react';
5
+ import { ProviderLogo } from './ProviderLogo';
6
+ export function ModuleAgentsDialog({ module, open, onOpenChange, providers, models, credentials, bindings, onChange, onOpenProvider, }) {
7
+ const safeProviders = Array.isArray(providers) ? providers : [];
8
+ const safeModels = Array.isArray(models) ? models : [];
9
+ const safeCreds = Array.isArray(credentials) ? credentials : [];
10
+ const safeBindings = Array.isArray(bindings) ? bindings : [];
11
+ const [providerId, setProviderId] = useState('');
12
+ const [modelId, setModelId] = useState('');
13
+ const [label, setLabel] = useState('');
14
+ const [saving, setSaving] = useState(false);
15
+ const providerModels = safeModels.filter(m => m.provider_id === providerId && m.active);
16
+ const add = async () => {
17
+ const model = safeModels.find(m => m.id === modelId);
18
+ if (!model) {
19
+ toast.error('Kies eerst een AI-model.');
20
+ return;
21
+ }
22
+ const cred = safeCreds.find(c => c.provider_id === model.provider_id && c.active);
23
+ if (!cred) {
24
+ toast.error('Voeg eerst een actieve API-sleutel toe voor deze provider.');
25
+ return;
26
+ }
27
+ setSaving(true);
28
+ const { error } = await supabase.from('ai_module_bindings').insert({
29
+ consumer_module_id: module.module_key,
30
+ model_id: model.id,
31
+ credential_id: cred.id,
32
+ label: label.trim() || null,
33
+ is_default: safeBindings.length === 0,
34
+ });
35
+ setSaving(false);
36
+ if (error) {
37
+ toast.error(error.message.includes('duplicate') ? 'Dit model is al gekoppeld aan deze module.' : error.message);
38
+ return;
39
+ }
40
+ setProviderId('');
41
+ setModelId('');
42
+ setLabel('');
43
+ toast.success('AI-agent gekoppeld.');
44
+ onChange();
45
+ };
46
+ const makeDefault = async (binding) => {
47
+ await supabase.from('ai_module_bindings')
48
+ .update({ is_default: false }).eq('consumer_module_id', module.module_key);
49
+ const { error } = await supabase.from('ai_module_bindings')
50
+ .update({ is_default: true }).eq('id', binding.id);
51
+ if (error) {
52
+ toast.error(error.message);
53
+ return;
54
+ }
55
+ toast.success('Standaard AI-agent bijgewerkt.');
56
+ onChange();
57
+ };
58
+ const remove = async (binding) => {
59
+ const { error } = await supabase.from('ai_module_bindings').delete().eq('id', binding.id);
60
+ if (error) {
61
+ toast.error(error.message);
62
+ return;
63
+ }
64
+ toast.success('Koppeling verwijderd.');
65
+ onChange();
66
+ };
67
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-2xl", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["AI-agents voor ", module.module_name] }), _jsx(DialogDescription, { children: "Koppel \u00E9\u00E9n of meerdere AI-agents aan deze module. De standaard agent wordt gebruikt als er geen andere wordt gekozen." })] }), _jsx("div", { className: "space-y-2", children: safeBindings.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground rounded-xl border p-4", children: "Nog geen AI-agent gekoppeld aan deze module." })) : (_jsx("ul", { className: "divide-y rounded-xl border", children: safeBindings.map(b => {
68
+ const m = safeModels.find(x => x.id === b.model_id);
69
+ const p = m ? safeProviders.find(x => x.id === m.provider_id) : undefined;
70
+ const unsupported = !m || !m.active;
71
+ return (_jsxs("li", { className: "flex items-center gap-3 px-4 py-3", children: [p && _jsx(ProviderLogo, { slug: p.slug, name: p.name, className: "h-6 w-6 shrink-0" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("p", { className: "text-sm font-medium truncate", children: [b.label || m?.display_name || 'Onbekend model', b.is_default && _jsx(Badge, { variant: "secondary", className: "ml-2 text-xs", children: "Standaard" }), unsupported && _jsx(Badge, { variant: "destructive", className: "ml-2 text-xs", children: "Niet meer ondersteund" })] }), _jsx("p", { className: "text-xs text-muted-foreground truncate", children: unsupported
72
+ ? 'Dit model wordt niet meer ondersteund — kies hieronder een ander model.'
73
+ : `${p?.name ?? 'Onbekende provider'} · ${m?.display_name ?? '—'}` })] }), !b.is_default && (_jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8", title: "Als standaard instellen", onClick: () => makeDefault(b), children: _jsx(Star, { className: "h-4 w-4" }) })), _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 text-muted-foreground hover:text-destructive", title: "Koppeling verwijderen", onClick: () => remove(b), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, b.id));
74
+ }) })) }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-3 rounded-xl border p-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { children: "Provider" }), _jsxs(Select, { value: providerId, onValueChange: v => { setProviderId(v); setModelId(''); }, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies provider" }) }), _jsx(SelectContent, { children: safeProviders.map(p => {
75
+ const linked = safeCreds.some(c => c.provider_id === p.id && c.active);
76
+ return (_jsx(SelectItem, { value: p.id, disabled: !linked, className: !linked ? 'opacity-50' : '', children: _jsxs("span", { className: "flex items-center justify-between w-full gap-3", children: [_jsxs("span", { className: "flex items-center gap-2", children: [_jsx(ProviderLogo, { slug: p.slug, name: p.name, className: "h-4 w-4" }), p.name] }), !linked && (_jsxs("button", { type: "button", onClick: e => { e.stopPropagation(); e.preventDefault(); onOpenProvider(p); }, className: "inline-flex items-center gap-1 text-xs text-primary hover:underline", children: [_jsx(KeyRound, { className: "h-3 w-3" }), " Koppel"] }))] }) }, p.id));
77
+ }) })] })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { children: "Model" }), _jsxs(Select, { value: modelId, onValueChange: setModelId, disabled: !providerId, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: providerId ? 'Kies model' : 'Kies eerst provider' }) }), _jsx(SelectContent, { children: providerModels.length === 0 ? (_jsx("div", { className: "px-3 py-2 text-xs text-muted-foreground", children: "Geen modellen beschikbaar." })) : providerModels.map(m => (_jsx(SelectItem, { value: m.id, children: m.display_name }, m.id))) })] })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { children: ["Naam ", _jsx("span", { className: "text-muted-foreground font-normal", children: "(optioneel)" })] }), _jsx(Input, { value: label, onChange: e => setLabel(e.target.value), placeholder: "Bijv. Analyse-agent" })] }), _jsx("div", { className: "md:col-span-3 flex justify-end", children: _jsxs(Button, { onClick: add, disabled: saving || !modelId, children: [_jsx(Plus, { className: "h-4 w-4 mr-1.5" }), " AI-agent koppelen"] }) })] }), _jsx(DialogFooter, { children: _jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), children: "Sluiten" }) })] }) }));
78
+ }
@@ -0,0 +1,8 @@
1
+ type Props = {
2
+ slug: string;
3
+ name?: string;
4
+ className?: string;
5
+ };
6
+ export declare function ProviderLogo({ slug, name, className }: Props): import("react").JSX.Element;
7
+ export {};
8
+ //# sourceMappingURL=ProviderLogo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProviderLogo.d.ts","sourceRoot":"","sources":["../../src/components/ProviderLogo.tsx"],"names":[],"mappings":"AAIA,KAAK,KAAK,GAAG;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAgBF,wBAAgB,YAAY,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,KAAK,+BAsB5D"}
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cn } from '@flowselections/core';
3
+ const LOGO_BASE = '/flowselections-assets/ai-authenticatie';
4
+ function resolveLogo(slug) {
5
+ const s = (slug || '').toLowerCase();
6
+ if (s === 'gemini' || s === 'google' || s.includes('google') || s.includes('gemini')) {
7
+ return `${LOGO_BASE}/gemini.png`;
8
+ }
9
+ if (s === 'openai' || s.includes('openai') || s === 'gpt' || s.includes('gpt')) {
10
+ return `${LOGO_BASE}/openai.png`;
11
+ }
12
+ if (s === 'anthropic' || s.includes('claude') || s.includes('anthropic')) {
13
+ return `${LOGO_BASE}/claude.png`;
14
+ }
15
+ return null;
16
+ }
17
+ export function ProviderLogo({ slug, name, className }) {
18
+ const logo = resolveLogo(slug);
19
+ if (logo) {
20
+ return (_jsx("div", { className: cn('flex items-center justify-center rounded-lg bg-white border overflow-hidden', className), children: _jsx("img", { src: logo, alt: name ?? slug, className: "w-[70%] h-[70%] object-contain" }) }));
21
+ }
22
+ const letter = (name || slug || '?').trim().charAt(0).toUpperCase();
23
+ return (_jsx("div", { className: cn('flex items-center justify-center rounded-lg bg-gradient-to-br from-primary/80 to-primary text-primary-foreground font-semibold', className), children: letter }));
24
+ }
@@ -0,0 +1,11 @@
1
+ import { type AiProvider, type AiCredential } from '../hooks/useAiAdmin';
2
+ type Props = {
3
+ provider: AiProvider;
4
+ credentials: AiCredential[];
5
+ open: boolean;
6
+ onOpenChange: (v: boolean) => void;
7
+ onChange: () => void;
8
+ };
9
+ export declare function ProviderManageDialog({ provider, credentials, open, onOpenChange, onChange, }: Props): import("react").JSX.Element;
10
+ export {};
11
+ //# sourceMappingURL=ProviderManageDialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProviderManageDialog.d.ts","sourceRoot":"","sources":["../../src/components/ProviderManageDialog.tsx"],"names":[],"mappings":"AASA,OAAO,EAAW,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAMlF,KAAK,KAAK,GAAG;IACX,QAAQ,EAAE,UAAU,CAAC;IACrB,WAAW,EAAE,YAAY,EAAE,CAAC;IAC5B,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,EACnC,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,GACpD,EAAE,KAAK,+BA4CP"}