@lastbrain/module-ai 0.1.18 → 0.1.20

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 (39) hide show
  1. package/dist/ai.build.config.d.ts.map +1 -1
  2. package/dist/ai.build.config.js +65 -1
  3. package/dist/api/admin/token-packs/[id].d.ts +28 -0
  4. package/dist/api/admin/token-packs/[id].d.ts.map +1 -0
  5. package/dist/api/admin/token-packs/[id].js +44 -0
  6. package/dist/api/admin/token-packs.d.ts +20 -0
  7. package/dist/api/admin/token-packs.d.ts.map +1 -0
  8. package/dist/api/admin/token-packs.js +57 -0
  9. package/dist/api/admin/user-token.js +2 -1
  10. package/dist/api/auth/create-checkout.d.ts +31 -0
  11. package/dist/api/auth/create-checkout.d.ts.map +1 -0
  12. package/dist/api/auth/create-checkout.js +93 -0
  13. package/dist/api/auth/generate-image.d.ts +3 -3
  14. package/dist/api/auth/generate-image.d.ts.map +1 -1
  15. package/dist/api/auth/generate-image.js +79 -31
  16. package/dist/api/auth/generate-text.d.ts.map +1 -1
  17. package/dist/api/auth/generate-text.js +11 -4
  18. package/dist/api/auth/token-checkout.d.ts +12 -0
  19. package/dist/api/auth/token-checkout.d.ts.map +1 -0
  20. package/dist/api/auth/token-checkout.js +79 -0
  21. package/dist/api/auth/token-packs.d.ts +11 -0
  22. package/dist/api/auth/token-packs.d.ts.map +1 -0
  23. package/dist/api/auth/token-packs.js +29 -0
  24. package/dist/api/public/webhook.d.ts +2 -0
  25. package/dist/api/public/webhook.d.ts.map +1 -0
  26. package/dist/api/public/webhook.js +171 -0
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +1 -0
  30. package/dist/web/admin/AdminTokenPacksPage.d.ts +2 -0
  31. package/dist/web/admin/AdminTokenPacksPage.d.ts.map +1 -0
  32. package/dist/web/admin/AdminTokenPacksPage.js +127 -0
  33. package/dist/web/auth/TokenPage.d.ts.map +1 -1
  34. package/dist/web/auth/TokenPage.js +51 -3
  35. package/dist/web/components/ImageGenerative.d.ts +5 -2
  36. package/dist/web/components/ImageGenerative.d.ts.map +1 -1
  37. package/dist/web/components/ImageGenerative.js +51 -7
  38. package/package.json +5 -4
  39. package/supabase/migrations/20251201000000_token_packs.sql +73 -0
@@ -4,6 +4,18 @@ import { useState, useCallback } from "react";
4
4
  import { Button, Chip, Progress, Card, CardBody, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Textarea, Select, SelectItem, addToast, } from "@lastbrain/ui";
5
5
  import { Image as ImageIcon, Loader2, AlertCircle, Download, Wand2, } from "lucide-react";
6
6
  import Image from "next/image";
7
+ // Coût en tokens par combinaison modèle/taille(/qualité)
8
+ const TOKENS_PER_IMAGE_KEY = {
9
+ "dall-e-2-256x256": 3000,
10
+ "dall-e-2-512x512": 4000,
11
+ "dall-e-2-1024x1024": 5000,
12
+ "dall-e-3-1024x1024-standard": 6000,
13
+ "dall-e-3-1024x1024-hd": 8000,
14
+ "dall-e-3-1024x1792-standard": 8000,
15
+ "dall-e-3-1024x1792-hd": 10000,
16
+ "dall-e-3-1792x1024-standard": 8000,
17
+ "dall-e-3-1792x1024-hd": 10000,
18
+ };
7
19
  const imageStyles = [
8
20
  {
9
21
  key: "realistic",
@@ -34,7 +46,7 @@ const imageStyles = [
34
46
  },
35
47
  { key: "pop-art", label: "Pop Art", description: "Style pop art vibrant" },
36
48
  ];
37
- export function ImageGenerative({ defaultPrompt = "", model = "dall-e-3", size = "1024x1024", quality = "standard", onChange, onError, className, disabled = false, apiEndpoint = "/api/ai/generate-image", showTokenBalance = true, label, description, defaultStyle = "realistic", hideStyleEditor = false, uploadPath, }) {
49
+ export function ImageGenerative({ defaultPrompt = "", model = "dall-e-3", size = "1024x1024", quality = "standard", onChange, onError, className, disabled = false, apiEndpoint = "/api/ai/generate-image", showTokenBalance = true, label, description, defaultStyle = "realistic", hideStyleEditor = false, uploadPath, preview = true, inline = false, }) {
38
50
  const [isGenerating, setIsGenerating] = useState(false);
39
51
  const [generatedImage, setGeneratedImage] = useState(null);
40
52
  const [error, setError] = useState(null);
@@ -44,6 +56,19 @@ export function ImageGenerative({ defaultPrompt = "", model = "dall-e-3", size =
44
56
  const [userPrompt, setUserPrompt] = useState(defaultPrompt);
45
57
  const [selectedStyle, setSelectedStyle] = useState(defaultStyle);
46
58
  const [selectedModel, setSelectedModel] = useState(model);
59
+ const [selectedSize, setSelectedSize] = useState(size);
60
+ const [selectedQuality, setSelectedQuality] = useState(quality);
61
+ // Calculer le coût estimé
62
+ const estimatedCost = () => {
63
+ let key = "";
64
+ if (selectedModel === "dall-e-2") {
65
+ key = `${selectedModel}-${selectedSize}`;
66
+ }
67
+ else {
68
+ key = `${selectedModel}-${selectedSize}-${selectedQuality}`;
69
+ }
70
+ return TOKENS_PER_IMAGE_KEY[key] || 0;
71
+ };
47
72
  const handleGenerate = useCallback(async () => {
48
73
  if (!userPrompt || isGenerating)
49
74
  return;
@@ -59,8 +84,8 @@ export function ImageGenerative({ defaultPrompt = "", model = "dall-e-3", size =
59
84
  body: JSON.stringify({
60
85
  prompt: enhancedPrompt,
61
86
  model: selectedModel,
62
- size,
63
- quality,
87
+ size: selectedSize,
88
+ quality: selectedQuality,
64
89
  uploadPath,
65
90
  }),
66
91
  });
@@ -70,11 +95,11 @@ export function ImageGenerative({ defaultPrompt = "", model = "dall-e-3", size =
70
95
  }
71
96
  const data = await response.json();
72
97
  const imageResponse = {
98
+ supabaseImageUrl: data.supabaseImageUrl,
73
99
  imageUrl: data.imageUrl,
74
100
  tokensUsed: data.tokensUsed || 0,
75
101
  tokensRemaining: data.tokensRemaining || 0,
76
102
  model: data.model || selectedModel,
77
- cost: data.cost,
78
103
  prompt: userPrompt,
79
104
  };
80
105
  setGeneratedImage(imageResponse);
@@ -109,8 +134,8 @@ export function ImageGenerative({ defaultPrompt = "", model = "dall-e-3", size =
109
134
  userPrompt,
110
135
  selectedStyle,
111
136
  selectedModel,
112
- size,
113
- quality,
137
+ selectedSize,
138
+ selectedQuality,
114
139
  apiEndpoint,
115
140
  uploadPath,
116
141
  onChange,
@@ -136,5 +161,24 @@ export function ImageGenerative({ defaultPrompt = "", model = "dall-e-3", size =
136
161
  console.error("Erreur lors du téléchargement:", error);
137
162
  }
138
163
  }, [generatedImage]);
139
- return (_jsxs("div", { className: className, children: [_jsxs("div", { className: "flex flex-col gap-4", children: [label && _jsx("label", { className: "text-sm font-medium", children: label }), description && _jsx("p", { className: "text-sm text-gray-500", children: description }), generatedImage && !isGenerating && (_jsx(Card, { children: _jsxs(CardBody, { children: [_jsx("div", { className: "relative w-full aspect-square", children: _jsx(Image, { src: generatedImage.imageUrl, alt: generatedImage.prompt, fill: true, className: "object-contain rounded-lg", priority: true }) }), _jsxs("div", { className: "mt-4", children: [_jsxs("p", { className: "text-sm text-default-600", children: [_jsx("strong", { children: "Prompt:" }), " ", generatedImage.prompt] }), _jsxs("p", { className: "text-xs text-default-400 mt-1", children: [size, " \u2022 ", quality] })] })] }) })), _jsx(Button, { color: "primary", onClick: () => setIsModalOpen(true), disabled: disabled || isGenerating, startContent: _jsx(Wand2, { size: 18 }), className: "w-full", children: isGenerating ? "Génération..." : "Générer une image" }), _jsxs("div", { className: "flex items-center justify-between gap-2 flex-wrap", children: [_jsx("div", { className: "flex items-center gap-2", children: showTokenBalance && tokenBalance !== null && (_jsxs(Chip, { size: "sm", variant: "flat", color: "success", children: [tokenBalance.toLocaleString(), " tokens restants"] })) }), generatedImage && (_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsxs(Chip, { size: "sm", variant: "flat", children: [generatedImage.tokensUsed, " tokens utilis\u00E9s"] }), _jsx(Chip, { size: "sm", variant: "flat", color: "primary", children: generatedImage.model }), generatedImage.cost && (_jsxs(Chip, { size: "sm", variant: "flat", color: "warning", children: ["$", generatedImage.cost.toFixed(4)] })), _jsx(Button, { size: "sm", variant: "flat", onClick: handleDownload, startContent: _jsx(Download, { size: 16 }), children: "T\u00E9l\u00E9charger" })] }))] }), isGenerating && (_jsxs("div", { className: "space-y-2", children: [_jsx(Progress, { size: "sm", isIndeterminate: true, "aria-label": "G\u00E9n\u00E9ration en cours...", className: "max-w-md" }), _jsx("p", { className: "text-sm text-default-500", children: "G\u00E9n\u00E9ration de l'image en cours... Cela peut prendre quelques instants." })] })), error && (_jsxs("div", { className: "flex items-center gap-2 text-danger text-sm p-3 bg-danger-50 rounded-md", children: [_jsx(AlertCircle, { size: 16 }), _jsx("span", { children: error })] }))] }), _jsx(Modal, { isOpen: isModalOpen, onClose: () => setIsModalOpen(false), size: "2xl", children: _jsxs(ModalContent, { children: [_jsx(ModalHeader, { children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(ImageIcon, { size: 20 }), _jsx("span", { children: "G\u00E9n\u00E9rer une image avec l'IA" })] }) }), _jsx(ModalBody, { children: _jsxs("div", { className: "space-y-4", children: [_jsx(Textarea, { label: "Description de l'image", placeholder: "D\u00E9crivez l'image que vous souhaitez g\u00E9n\u00E9rer...", value: userPrompt, onChange: (e) => setUserPrompt(e.target.value), minRows: 3, description: "Soyez pr\u00E9cis et d\u00E9taill\u00E9 pour de meilleurs r\u00E9sultats" }), !hideStyleEditor && (_jsxs(_Fragment, { children: [_jsx(Select, { label: "Style artistique", selectedKeys: [selectedStyle], onSelectionChange: (keys) => setSelectedStyle(Array.from(keys)[0]), description: "Le style influence l'apparence finale de l'image", children: imageStyles.map((style) => (_jsx(SelectItem, { textValue: style.label, children: _jsxs("div", { className: "flex flex-col", children: [_jsx("span", { className: "font-medium", children: style.label }), _jsx("span", { className: "text-xs text-gray-500", children: style.description })] }) }, style.key))) }), _jsxs(Select, { label: "Mod\u00E8le IA", selectedKeys: [selectedModel], onSelectionChange: (keys) => setSelectedModel(Array.from(keys)[0]), description: "DALL-E 3 offre la meilleure qualit\u00E9", children: [_jsx(SelectItem, { children: "DALL-E 3 (Haute qualit\u00E9)" }, "dall-e-3"), _jsx(SelectItem, { children: "DALL-E 2 (Standard)" }, "dall-e-2")] })] })), generatedImage && (_jsxs("div", { className: "p-3 bg-gray-50 dark:bg-gray-800 rounded-lg", children: [_jsx("p", { className: "text-sm font-medium mb-2", children: "Aper\u00E7u de l'image actuelle:" }), _jsx("div", { className: "relative w-full h-32", children: _jsx(Image, { src: generatedImage.imageUrl, alt: "Current", fill: true, className: "object-cover rounded" }) })] }))] }) }), _jsxs(ModalFooter, { children: [_jsx(Button, { variant: "light", onClick: () => setIsModalOpen(false), children: "Annuler" }), _jsx(Button, { color: "primary", onClick: handleGenerate, disabled: !userPrompt.trim() || isGenerating, startContent: isGenerating ? (_jsx(Loader2, { className: "animate-spin", size: 16 })) : (_jsx(ImageIcon, { size: 16 })), children: isGenerating ? "Génération..." : "Générer" })] })] }) })] }));
164
+ return (_jsxs("div", { className: className, children: [_jsxs("div", { className: "flex flex-col gap-4", children: [label && _jsx("label", { className: "text-sm font-medium", children: label }), description && _jsx("p", { className: "text-sm text-gray-500", children: description }), generatedImage && !isGenerating && preview && (_jsx(Card, { children: _jsxs(CardBody, { children: [_jsx("div", { className: "relative w-full aspect-square", children: _jsx(Image, { src: generatedImage.imageUrl, alt: generatedImage.prompt, fill: true, className: "object-contain rounded-lg", priority: true }) }), _jsxs("div", { className: "mt-4", children: [_jsxs("p", { className: "text-sm text-default-600", children: [_jsx("strong", { children: "Prompt:" }), " ", generatedImage.prompt] }), _jsxs("p", { className: "text-xs text-default-400 mt-1", children: [selectedSize, " \u2022 ", selectedQuality] })] })] }) })), inline ? (_jsxs("div", { className: "space-y-4", children: [_jsx(Textarea, { label: "Description de l'image", placeholder: "D\u00E9crivez l'image que vous souhaitez g\u00E9n\u00E9rer...", value: userPrompt, onChange: (e) => setUserPrompt(e.target.value), minRows: 3, description: "Soyez pr\u00E9cis et d\u00E9taill\u00E9 pour de meilleurs r\u00E9sultats" }), !hideStyleEditor && (_jsxs(_Fragment, { children: [_jsx(Select, { label: "Style artistique", selectedKeys: [selectedStyle], onSelectionChange: (keys) => setSelectedStyle(Array.from(keys)[0]), description: "Le style influence l'apparence finale de l'image", children: imageStyles.map((style) => (_jsx(SelectItem, { textValue: style.label, children: _jsxs("div", { className: "flex flex-col", children: [_jsx("span", { className: "font-medium", children: style.label }), _jsx("span", { className: "text-xs text-gray-500", children: style.description })] }) }, style.key))) }), _jsxs(Select, { label: "Mod\u00E8le IA", selectedKeys: [selectedModel], onSelectionChange: (keys) => {
165
+ const newModel = Array.from(keys)[0];
166
+ setSelectedModel(newModel);
167
+ // Ajuster la taille/qualité selon le modèle
168
+ if (newModel === "dall-e-2") {
169
+ setSelectedQuality("standard");
170
+ if (!["256x256", "512x512", "1024x1024"].includes(selectedSize)) {
171
+ setSelectedSize("1024x1024");
172
+ }
173
+ }
174
+ }, description: "DALL-E 3 offre la meilleure qualit\u00E9", children: [_jsx(SelectItem, { children: "DALL-E 3 (Haute qualit\u00E9)" }, "dall-e-3"), _jsx(SelectItem, { children: "DALL-E 2 (Standard)" }, "dall-e-2")] }), _jsx(Select, { label: "Taille de l'image", selectedKeys: [selectedSize], onSelectionChange: (keys) => setSelectedSize(Array.from(keys)[0]), description: "Tailles disponibles selon le mod\u00E8le", children: selectedModel === "dall-e-2" ? (_jsxs(_Fragment, { children: [_jsx(SelectItem, { children: "256\u00D7256 (3000 tokens)" }, "256x256"), _jsx(SelectItem, { children: "512\u00D7512 (4000 tokens)" }, "512x512"), _jsx(SelectItem, { children: "1024\u00D71024 (5000 tokens)" }, "1024x1024")] })) : (_jsxs(_Fragment, { children: [_jsx(SelectItem, { children: "1024\u00D71024 (6000-8000 tokens)" }, "1024x1024"), _jsx(SelectItem, { children: "1024\u00D71792 Portrait (8000-10000 tokens)" }, "1024x1792"), _jsx(SelectItem, { children: "1792\u00D71024 Paysage (8000-10000 tokens)" }, "1792x1024")] })) }), selectedModel === "dall-e-3" && (_jsxs(Select, { label: "Qualit\u00E9", selectedKeys: [selectedQuality], onSelectionChange: (keys) => setSelectedQuality(Array.from(keys)[0]), description: "HD offre plus de d\u00E9tails mais co\u00FBte plus de tokens", children: [_jsx(SelectItem, { children: "Standard" }, "standard"), _jsx(SelectItem, { children: "HD (Haute D\u00E9finition)" }, "hd")] })), _jsx("div", { className: "p-3 bg-primary-50 dark:bg-primary-900/20 rounded-lg", children: _jsxs("p", { className: "text-sm font-medium text-primary-700 dark:text-primary-300", children: ["Co\u00FBt estim\u00E9: ", estimatedCost().toLocaleString(), " tokens"] }) })] })), _jsx(Button, { color: "primary", onClick: handleGenerate, disabled: !userPrompt.trim() || disabled || isGenerating, startContent: isGenerating ? (_jsx(Loader2, { className: "animate-spin", size: 16 })) : (_jsx(Wand2, { size: 18 })), className: "w-full", children: isGenerating ? "Génération..." : "Générer" })] })) : (_jsx(Button, { color: "primary", onClick: () => setIsModalOpen(true), disabled: disabled || isGenerating, startContent: _jsx(Wand2, { size: 18 }), className: "w-full", children: isGenerating ? "Génération..." : "Générer une image" })), _jsxs("div", { className: "flex items-center justify-between gap-2 flex-wrap", children: [_jsx("div", { className: "flex items-center gap-2", children: showTokenBalance && tokenBalance !== null && (_jsxs(Chip, { size: "sm", variant: "flat", color: "success", children: [tokenBalance.toLocaleString(), " tokens restants"] })) }), generatedImage && (_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsxs(Chip, { size: "sm", variant: "flat", children: [generatedImage.tokensUsed, " tokens utilis\u00E9s"] }), _jsx(Chip, { size: "sm", variant: "flat", color: "primary", children: generatedImage.model }), preview && (_jsx(Button, { size: "sm", variant: "flat", onClick: handleDownload, startContent: _jsx(Download, { size: 16 }), children: "T\u00E9l\u00E9charger" }))] }))] }), isGenerating && (_jsxs("div", { className: "space-y-2", children: [_jsx(Progress, { size: "sm", isIndeterminate: true, "aria-label": "G\u00E9n\u00E9ration en cours...", className: "max-w-md" }), _jsx("p", { className: "text-sm text-default-500", children: "G\u00E9n\u00E9ration de l'image en cours... Cela peut prendre quelques instants." })] })), error && (_jsxs("div", { className: "flex items-center gap-2 text-danger text-sm p-3 bg-danger-50 rounded-md", children: [_jsx(AlertCircle, { size: 16 }), _jsx("span", { children: error })] }))] }), _jsx(Modal, { backdrop: "blur", isOpen: isModalOpen, onClose: () => setIsModalOpen(false), size: "2xl", children: _jsxs(ModalContent, { children: [_jsx(ModalHeader, { children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(ImageIcon, { size: 20 }), _jsx("span", { children: "G\u00E9n\u00E9rer une image avec l'IA" })] }) }), _jsx(ModalBody, { children: _jsxs("div", { className: "space-y-4", children: [_jsx(Textarea, { label: "Description de l'image", placeholder: "D\u00E9crivez l'image que vous souhaitez g\u00E9n\u00E9rer...", value: userPrompt, onChange: (e) => setUserPrompt(e.target.value), minRows: 3, description: "Soyez pr\u00E9cis et d\u00E9taill\u00E9 pour de meilleurs r\u00E9sultats" }), !hideStyleEditor && (_jsxs(_Fragment, { children: [_jsx(Select, { label: "Style artistique", selectedKeys: [selectedStyle], onSelectionChange: (keys) => setSelectedStyle(Array.from(keys)[0]), description: "Le style influence l'apparence finale de l'image", children: imageStyles.map((style) => (_jsx(SelectItem, { textValue: style.label, children: _jsxs("div", { className: "flex flex-col", children: [_jsx("span", { className: "font-medium", children: style.label }), _jsx("span", { className: "text-xs text-gray-500", children: style.description })] }) }, style.key))) }), _jsxs(Select, { label: "Mod\u00E8le IA", selectedKeys: [selectedModel], onSelectionChange: (keys) => {
175
+ const newModel = Array.from(keys)[0];
176
+ setSelectedModel(newModel);
177
+ if (newModel === "dall-e-2") {
178
+ setSelectedQuality("standard");
179
+ if (!["256x256", "512x512", "1024x1024"].includes(selectedSize)) {
180
+ setSelectedSize("1024x1024");
181
+ }
182
+ }
183
+ }, description: "DALL-E 3 offre la meilleure qualit\u00E9", children: [_jsx(SelectItem, { children: "DALL-E 3 (Haute qualit\u00E9)" }, "dall-e-3"), _jsx(SelectItem, { children: "DALL-E 2 (Standard)" }, "dall-e-2")] }), _jsx(Select, { label: "Taille de l'image", selectedKeys: [selectedSize], onSelectionChange: (keys) => setSelectedSize(Array.from(keys)[0]), description: "Tailles disponibles selon le mod\u00E8le", children: selectedModel === "dall-e-2" ? (_jsxs(_Fragment, { children: [_jsx(SelectItem, { children: "256\u00D7256 (3000 tokens)" }, "256x256"), _jsx(SelectItem, { children: "512\u00D7512 (4000 tokens)" }, "512x512"), _jsx(SelectItem, { children: "1024\u00D71024 (5000 tokens)" }, "1024x1024")] })) : (_jsxs(_Fragment, { children: [_jsx(SelectItem, { children: "1024\u00D71024 (6000-8000 tokens)" }, "1024x1024"), _jsx(SelectItem, { children: "1024\u00D71792 Portrait (8000-10000 tokens)" }, "1024x1792"), _jsx(SelectItem, { children: "1792\u00D71024 Paysage (8000-10000 tokens)" }, "1792x1024")] })) }), selectedModel === "dall-e-3" && (_jsxs(Select, { label: "Qualit\u00E9", selectedKeys: [selectedQuality], onSelectionChange: (keys) => setSelectedQuality(Array.from(keys)[0]), description: "HD offre plus de d\u00E9tails mais co\u00FBte plus de tokens", children: [_jsx(SelectItem, { children: "Standard" }, "standard"), _jsx(SelectItem, { children: "HD (Haute D\u00E9finition)" }, "hd")] })), _jsx("div", { className: "p-3 bg-primary-50 dark:bg-primary-900/20 rounded-lg", children: _jsxs("p", { className: "text-sm font-medium text-primary-700 dark:text-primary-300", children: ["Co\u00FBt estim\u00E9: ", estimatedCost().toLocaleString(), " tokens"] }) })] })), generatedImage && (_jsxs("div", { className: "p-3 bg-gray-50 dark:bg-gray-800 rounded-lg", children: [_jsx("p", { className: "text-sm font-medium mb-2", children: "Aper\u00E7u de l'image actuelle:" }), _jsx("div", { className: "relative w-full", children: _jsx(Image, { src: generatedImage.imageUrl, alt: "Current", fill: true, className: "object-cover rounded" }) })] }))] }) }), _jsxs(ModalFooter, { children: [_jsx(Button, { variant: "light", onClick: () => setIsModalOpen(false), children: "Annuler" }), _jsx(Button, { color: "primary", onClick: handleGenerate, disabled: !userPrompt.trim() || isGenerating, startContent: isGenerating ? (_jsx(Loader2, { className: "animate-spin", size: 16 })) : (_jsx(ImageIcon, { size: 16 })), children: isGenerating ? "Génération..." : "Générer" })] })] }) })] }));
140
184
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lastbrain/module-ai",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "Module de génération IA (texte et images) avec gestion de tokens pour LastBrain",
5
5
  "private": false,
6
6
  "release": {
@@ -35,13 +35,14 @@
35
35
  "@heroui/react": "^2.4.23",
36
36
  "@heroui/system": "^2.4.23",
37
37
  "@heroui/theme": "^2.4.23",
38
+ "@lastbrain-labs/module-core-payment-pro": "^0.1.0",
38
39
  "@lastbrain/core": "^0.1.0",
39
40
  "@lastbrain/ui": "^0.1.0",
40
41
  "lucide-react": "^0.554.0",
41
- "next": "^16.0.4",
42
+ "next": "^16.0.7",
42
43
  "openai": "^6.9.1",
43
- "react": "^19.0.0",
44
- "react-dom": "^19.0.0",
44
+ "react": "^19.2.1",
45
+ "react-dom": "^19.2.1",
45
46
  "zod": "^3.23.8"
46
47
  },
47
48
  "peerDependencies": {
@@ -0,0 +1,73 @@
1
+ -- ===========================================================================
2
+ -- Module: @lastbrain/module-ai
3
+ -- Migration: 20251201000000_token_packs.sql
4
+ -- Description: Token packs for purchasing tokens via Stripe
5
+ -- ===========================================================================
6
+
7
+ -- ===========================================================================
8
+ -- Table: public.token_packs
9
+ -- Defines available token packs that users can purchase
10
+ -- ===========================================================================
11
+ CREATE TABLE IF NOT EXISTS public.token_packs (
12
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
13
+ name text NOT NULL, -- Display name (e.g., "100 Tokens")
14
+ description text NULL, -- Optional description
15
+ tokens integer NOT NULL, -- Number of tokens in pack
16
+ price_cents integer NOT NULL, -- Price in cents
17
+ currency text NOT NULL DEFAULT 'EUR', -- ISO currency code
18
+ stripe_price_id text NULL, -- Optional Stripe price ID for recurring
19
+ is_active boolean NOT NULL DEFAULT true, -- Whether pack is available for purchase
20
+ sort_order integer NOT NULL DEFAULT 0, -- Display order
21
+ created_at timestamptz NOT NULL DEFAULT now(),
22
+ updated_at timestamptz NOT NULL DEFAULT now()
23
+ );
24
+
25
+ -- ===========================================================================
26
+ -- Indexes
27
+ -- ===========================================================================
28
+ CREATE INDEX IF NOT EXISTS idx_token_packs_active_order
29
+ ON public.token_packs(is_active, sort_order);
30
+
31
+ -- ===========================================================================
32
+ -- Insert default token packs
33
+ -- ===========================================================================
34
+ INSERT INTO public.token_packs (name, description, tokens, price_cents, currency, sort_order)
35
+ VALUES
36
+ ('Starter', 'Starter pack', 100000, 500, 'EUR', 1),
37
+ ('Standard', 'Standard pack', 250000, 1000, 'EUR', 2),
38
+ ('Premium', 'Premium pack', 600000, 2000, 'EUR', 3),
39
+ ('Creator', 'Creator pack', 2000000, 5000, 'EUR', 4)
40
+
41
+ ON CONFLICT DO NOTHING;
42
+
43
+ -- ===========================================================================
44
+ -- RLS (Row Level Security)
45
+ -- ===========================================================================
46
+ ALTER TABLE public.token_packs ENABLE ROW LEVEL SECURITY;
47
+
48
+ -- Policy: Everyone can view active packs
49
+ DROP POLICY IF EXISTS token_packs_select_public ON public.token_packs;
50
+ CREATE POLICY token_packs_select_public ON public.token_packs
51
+ FOR SELECT
52
+ USING (is_active = true OR is_superadmin(auth.uid()));
53
+
54
+ -- Policy: Only superadmins can insert/update/delete
55
+ DROP POLICY IF EXISTS token_packs_admin_all ON public.token_packs;
56
+ CREATE POLICY token_packs_admin_all ON public.token_packs
57
+ FOR ALL
58
+ USING (is_superadmin(auth.uid()));
59
+
60
+ -- ===========================================================================
61
+ -- Trigger for updated_at
62
+ -- ===========================================================================
63
+ DROP TRIGGER IF EXISTS set_token_packs_updated_at ON public.token_packs;
64
+ CREATE TRIGGER set_token_packs_updated_at
65
+ BEFORE UPDATE ON public.token_packs
66
+ FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
67
+
68
+ -- ===========================================================================
69
+ -- Grants
70
+ -- ===========================================================================
71
+ GRANT SELECT ON public.token_packs TO anon;
72
+ GRANT SELECT ON public.token_packs TO authenticated;
73
+ GRANT ALL ON public.token_packs TO service_role;