@lastbrain/module-ai 0.1.0

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 (56) hide show
  1. package/dist/ai.build.config.d.ts +4 -0
  2. package/dist/ai.build.config.d.ts.map +1 -0
  3. package/dist/ai.build.config.js +86 -0
  4. package/dist/api/admin/user-token/[id].d.ts +29 -0
  5. package/dist/api/admin/user-token/[id].d.ts.map +1 -0
  6. package/dist/api/admin/user-token/[id].js +57 -0
  7. package/dist/api/admin/user-token.d.ts +20 -0
  8. package/dist/api/admin/user-token.d.ts.map +1 -0
  9. package/dist/api/admin/user-token.js +92 -0
  10. package/dist/api/admin/user_prompts.d.ts +17 -0
  11. package/dist/api/admin/user_prompts.d.ts.map +1 -0
  12. package/dist/api/admin/user_prompts.js +94 -0
  13. package/dist/api/admin/user_token_ledger.d.ts +17 -0
  14. package/dist/api/admin/user_token_ledger.d.ts.map +1 -0
  15. package/dist/api/admin/user_token_ledger.js +94 -0
  16. package/dist/api/auth/generate-image.d.ts +11 -0
  17. package/dist/api/auth/generate-image.d.ts.map +1 -0
  18. package/dist/api/auth/generate-image.js +104 -0
  19. package/dist/api/auth/generate-text.d.ts +11 -0
  20. package/dist/api/auth/generate-text.d.ts.map +1 -0
  21. package/dist/api/auth/generate-text.js +96 -0
  22. package/dist/api/auth/user_prompts.d.ts +17 -0
  23. package/dist/api/auth/user_prompts.d.ts.map +1 -0
  24. package/dist/api/auth/user_prompts.js +94 -0
  25. package/dist/api/auth/user_token_ledger.d.ts +17 -0
  26. package/dist/api/auth/user_token_ledger.d.ts.map +1 -0
  27. package/dist/api/auth/user_token_ledger.js +94 -0
  28. package/dist/components/Doc.d.ts +2 -0
  29. package/dist/components/Doc.d.ts.map +1 -0
  30. package/dist/components/Doc.js +5 -0
  31. package/dist/index.d.ts +10 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +12 -0
  34. package/dist/server.d.ts +61 -0
  35. package/dist/server.d.ts.map +1 -0
  36. package/dist/server.js +186 -0
  37. package/dist/web/admin/UserTokenIdPage.d.ts +6 -0
  38. package/dist/web/admin/UserTokenIdPage.d.ts.map +1 -0
  39. package/dist/web/admin/UserTokenIdPage.js +8 -0
  40. package/dist/web/admin/UserTokenPage.d.ts +2 -0
  41. package/dist/web/admin/UserTokenPage.d.ts.map +1 -0
  42. package/dist/web/admin/UserTokenPage.js +6 -0
  43. package/dist/web/auth/TokenPage.d.ts +2 -0
  44. package/dist/web/auth/TokenPage.d.ts.map +1 -0
  45. package/dist/web/auth/TokenPage.js +6 -0
  46. package/dist/web/components/ImageGenerative.d.ts +22 -0
  47. package/dist/web/components/ImageGenerative.d.ts.map +1 -0
  48. package/dist/web/components/ImageGenerative.js +87 -0
  49. package/dist/web/components/TextareaGenerative.d.ts +23 -0
  50. package/dist/web/components/TextareaGenerative.d.ts.map +1 -0
  51. package/dist/web/components/TextareaGenerative.js +60 -0
  52. package/package.json +72 -0
  53. package/supabase/migrations/20251121000000_ai_tokens.sql +189 -0
  54. package/supabase/migrations/20251121093113_module-ai_init.sql +122 -0
  55. package/supabase/migrations-down/20251121000000_ai_tokens.sql +23 -0
  56. package/supabase/migrations-down/20251121093113_module-ai_init.sql +11 -0
@@ -0,0 +1,8 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Card, CardBody, CardHeader } from "@lastbrain/ui";
4
+ import { use } from "react";
5
+ export function UserTokenIdPage({ params, }) {
6
+ const { id } = use(params);
7
+ return (_jsx("div", { className: "container mx-auto p-6", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h1", { className: "text-2xl font-bold", children: "D\u00E9tails Token Utilisateur" }) }), _jsx(CardBody, { children: _jsxs("p", { className: "text-default-600", children: ["D\u00E9tails pour l'utilisateur: ", _jsx("strong", { children: id })] }) })] }) }));
8
+ }
@@ -0,0 +1,2 @@
1
+ export declare function UserTokenPage(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=UserTokenPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UserTokenPage.d.ts","sourceRoot":"","sources":["../../../src/web/admin/UserTokenPage.tsx"],"names":[],"mappings":"AAIA,wBAAgB,aAAa,4CAe5B"}
@@ -0,0 +1,6 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Card, CardBody, CardHeader } from "@lastbrain/ui";
4
+ export function UserTokenPage() {
5
+ return (_jsx("div", { className: "container mx-auto p-6", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h1", { className: "text-2xl font-bold", children: "UserToken" }) }), _jsx(CardBody, { children: _jsx("p", { className: "text-default-600", children: "Contenu de la page UserToken (section: admin)" }) })] }) }));
6
+ }
@@ -0,0 +1,2 @@
1
+ export declare function TokenPage(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=TokenPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TokenPage.d.ts","sourceRoot":"","sources":["../../../src/web/auth/TokenPage.tsx"],"names":[],"mappings":"AAIA,wBAAgB,SAAS,4CAexB"}
@@ -0,0 +1,6 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Card, CardBody, CardHeader } from "@lastbrain/ui";
4
+ export function TokenPage() {
5
+ return (_jsx("div", { className: "container mx-auto p-6", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h1", { className: "text-2xl font-bold", children: "Token" }) }), _jsx(CardBody, { children: _jsx("p", { className: "text-default-600", children: "Contenu de la page token (section: auth)" }) })] }) }));
6
+ }
@@ -0,0 +1,22 @@
1
+ export interface GenerativeImageResponse {
2
+ imageUrl: string;
3
+ tokensUsed: number;
4
+ tokensRemaining: number;
5
+ model: string;
6
+ cost?: number;
7
+ prompt: string;
8
+ }
9
+ export interface ImageGenerativeProps {
10
+ prompt: string;
11
+ model?: string;
12
+ size?: "256x256" | "512x512" | "1024x1024" | "1792x1024" | "1024x1792";
13
+ quality?: "standard" | "hd";
14
+ onChange?: (response: GenerativeImageResponse) => void;
15
+ onError?: (error: Error) => void;
16
+ className?: string;
17
+ disabled?: boolean;
18
+ apiEndpoint?: string;
19
+ showTokenBalance?: boolean;
20
+ }
21
+ export declare function ImageGenerative({ prompt, model, size, quality, onChange, onError, className, disabled, apiEndpoint, showTokenBalance, }: ImageGenerativeProps): import("react/jsx-runtime").JSX.Element;
22
+ //# sourceMappingURL=ImageGenerative.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImageGenerative.d.ts","sourceRoot":"","sources":["../../../src/web/components/ImageGenerative.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;IACvE,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,uBAAuB,KAAK,IAAI,CAAC;IACvD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,wBAAgB,eAAe,CAAC,EAC9B,MAAM,EACN,KAAkB,EAClB,IAAkB,EAClB,OAAoB,EACpB,QAAQ,EACR,OAAO,EACP,SAAS,EACT,QAAgB,EAChB,WAAwC,EACxC,gBAAuB,GACxB,EAAE,oBAAoB,2CA6LtB"}
@@ -0,0 +1,87 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState, useCallback } from "react";
4
+ import { Button, Chip, Progress, Card, CardBody } from "@lastbrain/ui";
5
+ import { Sparkles, Loader2, AlertCircle, Download } from "lucide-react";
6
+ import Image from "next/image";
7
+ export function ImageGenerative({ prompt, model = "dall-e-3", size = "1024x1024", quality = "standard", onChange, onError, className, disabled = false, apiEndpoint = "/api/auth/generate-image", showTokenBalance = true, }) {
8
+ const [isGenerating, setIsGenerating] = useState(false);
9
+ const [generatedImage, setGeneratedImage] = useState(null);
10
+ const [error, setError] = useState(null);
11
+ const [tokenBalance, setTokenBalance] = useState(null);
12
+ const handleGenerate = useCallback(async () => {
13
+ if (!prompt || isGenerating)
14
+ return;
15
+ setIsGenerating(true);
16
+ setError(null);
17
+ try {
18
+ const response = await fetch(apiEndpoint, {
19
+ method: "POST",
20
+ headers: { "Content-Type": "application/json" },
21
+ body: JSON.stringify({
22
+ prompt,
23
+ model,
24
+ size,
25
+ quality,
26
+ }),
27
+ });
28
+ if (!response.ok) {
29
+ const errorData = await response.json();
30
+ throw new Error(errorData.error || "Erreur lors de la génération");
31
+ }
32
+ const data = await response.json();
33
+ const imageResponse = {
34
+ imageUrl: data.imageUrl,
35
+ tokensUsed: data.tokensUsed || 0,
36
+ tokensRemaining: data.tokensRemaining || 0,
37
+ model: data.model || model,
38
+ cost: data.cost,
39
+ prompt: prompt,
40
+ };
41
+ setGeneratedImage(imageResponse);
42
+ setTokenBalance(data.tokensRemaining);
43
+ if (onChange) {
44
+ onChange(imageResponse);
45
+ }
46
+ }
47
+ catch (err) {
48
+ const errorMessage = err instanceof Error ? err.message : "Erreur inconnue";
49
+ setError(errorMessage);
50
+ if (onError) {
51
+ onError(err);
52
+ }
53
+ }
54
+ finally {
55
+ setIsGenerating(false);
56
+ }
57
+ }, [
58
+ prompt,
59
+ model,
60
+ size,
61
+ quality,
62
+ apiEndpoint,
63
+ onChange,
64
+ onError,
65
+ isGenerating,
66
+ ]);
67
+ const handleDownload = useCallback(async () => {
68
+ if (!generatedImage?.imageUrl)
69
+ return;
70
+ try {
71
+ const response = await fetch(generatedImage.imageUrl);
72
+ const blob = await response.blob();
73
+ const url = window.URL.createObjectURL(blob);
74
+ const link = document.createElement("a");
75
+ link.href = url;
76
+ link.download = `generated-image-${Date.now()}.png`;
77
+ document.body.appendChild(link);
78
+ link.click();
79
+ document.body.removeChild(link);
80
+ window.URL.revokeObjectURL(url);
81
+ }
82
+ catch (error) {
83
+ console.error("Erreur lors du téléchargement:", error);
84
+ }
85
+ }, [generatedImage]);
86
+ return (_jsx("div", { className: className, children: _jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-2 flex-wrap", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { color: "primary", size: "sm", onClick: handleGenerate, disabled: disabled || isGenerating || !prompt, startContent: isGenerating ? (_jsx(Loader2, { className: "animate-spin", size: 16 })) : (_jsx(Sparkles, { size: 16 })), children: isGenerating ? "Génération..." : "Générer l'image" }), 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 })] })), 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] })] })] }) }))] }) }));
87
+ }
@@ -0,0 +1,23 @@
1
+ export interface GenerativeResponse {
2
+ text: string;
3
+ tokensUsed: number;
4
+ tokensRemaining: number;
5
+ model: string;
6
+ cost?: number;
7
+ }
8
+ export interface TextareaGenerativeProps {
9
+ prompt: string;
10
+ model?: string;
11
+ placeholder?: string;
12
+ defaultValue?: string;
13
+ onChange?: (response: GenerativeResponse) => void;
14
+ onError?: (error: Error) => void;
15
+ className?: string;
16
+ disabled?: boolean;
17
+ minRows?: number;
18
+ maxRows?: number;
19
+ apiEndpoint?: string;
20
+ showTokenBalance?: boolean;
21
+ }
22
+ export declare function TextareaGenerative({ prompt, model, placeholder, defaultValue, onChange, onError, className, disabled, minRows, maxRows, apiEndpoint, showTokenBalance, }: TextareaGenerativeProps): import("react/jsx-runtime").JSX.Element;
23
+ //# sourceMappingURL=TextareaGenerative.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TextareaGenerative.d.ts","sourceRoot":"","sources":["../../../src/web/components/TextareaGenerative.tsx"],"names":[],"mappings":"AAMA,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAClD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,wBAAgB,kBAAkB,CAAC,EACjC,MAAM,EACN,KAAqB,EACrB,WAAmC,EACnC,YAAiB,EACjB,QAAQ,EACR,OAAO,EACP,SAAS,EACT,QAAgB,EAChB,OAAW,EACX,OAAY,EACZ,WAAuC,EACvC,gBAAuB,GACxB,EAAE,uBAAuB,2CAwIzB"}
@@ -0,0 +1,60 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState, useCallback } from "react";
4
+ import { Textarea, Button, Chip, Progress } from "@lastbrain/ui";
5
+ import { Sparkles, Loader2, AlertCircle } from "lucide-react";
6
+ export function TextareaGenerative({ prompt, model = "gpt-4o-mini", placeholder = "Générer du texte...", defaultValue = "", onChange, onError, className, disabled = false, minRows = 4, maxRows = 20, apiEndpoint = "/api/auth/generate-text", showTokenBalance = true, }) {
7
+ const [value, setValue] = useState(defaultValue);
8
+ const [isGenerating, setIsGenerating] = useState(false);
9
+ const [lastResponse, setLastResponse] = useState(null);
10
+ const [error, setError] = useState(null);
11
+ const [tokenBalance, setTokenBalance] = useState(null);
12
+ const handleGenerate = useCallback(async () => {
13
+ if (!prompt || isGenerating)
14
+ return;
15
+ setIsGenerating(true);
16
+ setError(null);
17
+ try {
18
+ const response = await fetch(apiEndpoint, {
19
+ method: "POST",
20
+ headers: { "Content-Type": "application/json" },
21
+ body: JSON.stringify({
22
+ prompt,
23
+ model,
24
+ context: value,
25
+ }),
26
+ });
27
+ if (!response.ok) {
28
+ const errorData = await response.json();
29
+ throw new Error(errorData.error || "Erreur lors de la génération");
30
+ }
31
+ const data = await response.json();
32
+ const generativeResponse = {
33
+ text: data.text,
34
+ tokensUsed: data.tokensUsed || 0,
35
+ tokensRemaining: data.tokensRemaining || 0,
36
+ model: data.model || model,
37
+ cost: data.cost,
38
+ };
39
+ setValue(data.text);
40
+ setLastResponse(generativeResponse);
41
+ setTokenBalance(data.tokensRemaining);
42
+ if (onChange) {
43
+ onChange(generativeResponse);
44
+ }
45
+ }
46
+ catch (err) {
47
+ const errorMessage = err instanceof Error ? err.message : "Erreur inconnue";
48
+ setError(errorMessage);
49
+ if (onError) {
50
+ onError(err);
51
+ }
52
+ }
53
+ finally {
54
+ setIsGenerating(false);
55
+ }
56
+ }, [prompt, model, value, apiEndpoint, onChange, onError, isGenerating]);
57
+ return (_jsx("div", { className: className, children: _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Textarea, { value: value, onChange: (e) => setValue(e.target.value), placeholder: placeholder, disabled: disabled || isGenerating, minRows: minRows, maxRows: maxRows, classNames: {
58
+ input: "resize-y",
59
+ } }), _jsxs("div", { className: "flex items-center justify-between gap-2 flex-wrap", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { color: "primary", size: "sm", onClick: handleGenerate, disabled: disabled || isGenerating || !prompt, startContent: isGenerating ? (_jsx(Loader2, { className: "animate-spin", size: 16 })) : (_jsx(Sparkles, { size: 16 })), children: isGenerating ? "Génération..." : "Générer" }), showTokenBalance && tokenBalance !== null && (_jsxs(Chip, { size: "sm", variant: "flat", color: "success", children: [tokenBalance.toLocaleString(), " tokens restants"] }))] }), lastResponse && (_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsxs(Chip, { size: "sm", variant: "flat", children: [lastResponse.tokensUsed, " tokens utilis\u00E9s"] }), _jsx(Chip, { size: "sm", variant: "flat", color: "primary", children: lastResponse.model }), lastResponse.cost && (_jsxs(Chip, { size: "sm", variant: "flat", color: "warning", children: ["$", lastResponse.cost.toFixed(4)] }))] }))] }), isGenerating && (_jsx(Progress, { size: "sm", isIndeterminate: true, "aria-label": "G\u00E9n\u00E9ration en cours...", className: "max-w-md" })), error && (_jsxs("div", { className: "flex items-center gap-2 text-danger text-sm p-2 bg-danger-50 rounded-md", children: [_jsx(AlertCircle, { size: 16 }), _jsx("span", { children: error })] }))] }) }));
60
+ }
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@lastbrain/module-ai",
3
+ "version": "0.1.0",
4
+ "description": "Module de génération IA (texte et images) avec gestion de tokens pour LastBrain",
5
+ "private": false,
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "keywords": [
10
+ "lastbrain",
11
+ "module",
12
+ "ai",
13
+ "openai",
14
+ "generation",
15
+ "dalle",
16
+ "gpt",
17
+ "tokens"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/lastpublication/starter.git",
22
+ "directory": "packages/module-ai"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "supabase"
30
+ ],
31
+ "dependencies": {
32
+ "@heroui/react": "^2.4.23",
33
+ "@heroui/system": "^2.4.23",
34
+ "@heroui/theme": "^2.4.23",
35
+ "@lastbrain/core": "0.1.0",
36
+ "@lastbrain/ui": "0.1.0",
37
+ "lucide-react": "^0.554.0",
38
+ "next": "^15.5.6",
39
+ "openai": "^4.76.0",
40
+ "react": "^19.0.0",
41
+ "react-dom": "^19.0.0"
42
+ },
43
+ "peerDependencies": {
44
+ "next": ">=15.0.0"
45
+ },
46
+ "devDependencies": {
47
+ "typescript": "^5.4.0"
48
+ },
49
+ "exports": {
50
+ ".": {
51
+ "types": "./dist/index.d.ts",
52
+ "default": "./dist/index.js"
53
+ },
54
+ "./server": {
55
+ "types": "./dist/server.d.ts",
56
+ "default": "./dist/server.js"
57
+ },
58
+ "./ai.build.config": {
59
+ "types": "./dist/ai.build.config.d.ts",
60
+ "default": "./dist/ai.build.config.js"
61
+ },
62
+ "./api/*": {
63
+ "types": "./dist/api/*.d.ts",
64
+ "default": "./dist/api/*.js"
65
+ }
66
+ },
67
+ "sideEffects": false,
68
+ "scripts": {
69
+ "build": "tsc -p tsconfig.json",
70
+ "dev": "tsc -p tsconfig.json --watch"
71
+ }
72
+ }
@@ -0,0 +1,189 @@
1
+ -- AI Token System Migration
2
+ -- Gestion des tokens pour l'utilisation de l'IA
3
+
4
+ -- =====================================================
5
+ -- Table: user_token_ledger
6
+ -- =====================================================
7
+ -- Un seul ledger pour tous les mouvements de tokens
8
+ -- Crédit (amount > 0): purchase, gift, adjust+
9
+ -- Débit (amount < 0): use, adjust-
10
+
11
+ CREATE TABLE IF NOT EXISTS public.user_token_ledger (
12
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
13
+ owner_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
14
+ ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
15
+ type TEXT NOT NULL CHECK (type IN ('purchase', 'gift', 'use', 'adjust')),
16
+ amount BIGINT NOT NULL,
17
+ model TEXT NULL,
18
+ prompt TEXT NULL,
19
+ meta JSONB NOT NULL DEFAULT '{}'::JSONB,
20
+ created_by UUID NULL REFERENCES auth.users(id) ON DELETE SET NULL,
21
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
22
+ );
23
+
24
+ -- =====================================================
25
+ -- Indexes
26
+ -- =====================================================
27
+ CREATE INDEX IF NOT EXISTS idx_user_token_ledger_owner_ts ON public.user_token_ledger(owner_id, ts DESC);
28
+ CREATE INDEX IF NOT EXISTS idx_user_token_ledger_owner ON public.user_token_ledger(owner_id);
29
+ CREATE INDEX IF NOT EXISTS idx_user_token_ledger_type_ts ON public.user_token_ledger(type, ts DESC);
30
+ CREATE INDEX IF NOT EXISTS idx_user_token_ledger_usages ON public.user_token_ledger(owner_id, ts DESC) WHERE amount < 0;
31
+
32
+ -- =====================================================
33
+ -- Vue: user_token_balance_v
34
+ -- =====================================================
35
+ -- Solde courant par utilisateur
36
+
37
+ DROP VIEW IF EXISTS public.user_token_balance_v;
38
+ CREATE VIEW public.user_token_balance_v AS
39
+ SELECT
40
+ owner_id,
41
+ COALESCE(SUM(amount), 0)::BIGINT AS balance
42
+ FROM public.user_token_ledger
43
+ GROUP BY owner_id;
44
+
45
+ -- =====================================================
46
+ -- Trigger: Anti-solde négatif
47
+ -- =====================================================
48
+ -- Empêche une utilisation (amount < 0) si le solde devient négatif
49
+
50
+ CREATE OR REPLACE FUNCTION public.check_token_balance()
51
+ RETURNS TRIGGER AS $$
52
+ DECLARE
53
+ current_balance BIGINT;
54
+ BEGIN
55
+ -- Ne vérifier que pour les débits (use, adjust négatif)
56
+ IF NEW.amount >= 0 THEN
57
+ RETURN NEW;
58
+ END IF;
59
+
60
+ -- Calculer le solde actuel
61
+ SELECT COALESCE(SUM(amount), 0)
62
+ INTO current_balance
63
+ FROM public.user_token_ledger
64
+ WHERE owner_id = NEW.owner_id;
65
+
66
+ -- Vérifier si le nouveau solde serait négatif
67
+ IF (current_balance + NEW.amount) < 0 THEN
68
+ RAISE EXCEPTION 'INSUFFICIENT_TOKEN_BALANCE: current=%, requested=%',
69
+ current_balance, ABS(NEW.amount);
70
+ END IF;
71
+
72
+ RETURN NEW;
73
+ END;
74
+ $$ LANGUAGE plpgsql;
75
+
76
+ DROP TRIGGER IF EXISTS trigger_check_token_balance ON public.user_token_ledger;
77
+ CREATE TRIGGER trigger_check_token_balance
78
+ BEFORE INSERT ON public.user_token_ledger
79
+ FOR EACH ROW
80
+ EXECUTE FUNCTION public.check_token_balance();
81
+
82
+ -- =====================================================
83
+ -- RLS (Row Level Security)
84
+ -- =====================================================
85
+
86
+ ALTER TABLE public.user_token_ledger ENABLE ROW LEVEL SECURITY;
87
+
88
+ -- Policy: Les utilisateurs voient uniquement leurs propres tokens
89
+ DROP POLICY IF EXISTS user_token_ledger_select_own ON public.user_token_ledger;
90
+ CREATE POLICY user_token_ledger_select_own
91
+ ON public.user_token_ledger
92
+ FOR SELECT
93
+ USING (
94
+ owner_id = auth.uid()
95
+ OR is_superadmin(auth.uid())
96
+ );
97
+
98
+ -- Policy: Les utilisateurs peuvent créer leurs propres entrées de type 'use'
99
+ DROP POLICY IF EXISTS user_token_ledger_insert_own ON public.user_token_ledger;
100
+ CREATE POLICY user_token_ledger_insert_own
101
+ ON public.user_token_ledger
102
+ FOR INSERT
103
+ WITH CHECK (
104
+ (owner_id = auth.uid() AND type = 'use')
105
+ OR is_superadmin(auth.uid())
106
+ );
107
+
108
+ -- Policy: Seuls les superadmins peuvent update
109
+ DROP POLICY IF EXISTS user_token_ledger_update_admin ON public.user_token_ledger;
110
+ CREATE POLICY user_token_ledger_update_admin
111
+ ON public.user_token_ledger
112
+ FOR UPDATE
113
+ USING (is_superadmin(auth.uid()));
114
+
115
+ -- Policy: Seuls les superadmins peuvent delete
116
+ DROP POLICY IF EXISTS user_token_ledger_delete_admin ON public.user_token_ledger;
117
+ CREATE POLICY user_token_ledger_delete_admin
118
+ ON public.user_token_ledger
119
+ FOR DELETE
120
+ USING (is_superadmin(auth.uid()));
121
+
122
+ -- =====================================================
123
+ -- Table: user_prompts
124
+ -- =====================================================
125
+ -- Prompts personnalisés des utilisateurs
126
+
127
+ CREATE TABLE IF NOT EXISTS public.user_prompts (
128
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
129
+ owner_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
130
+ type TEXT NOT NULL CHECK (type IN ('text', 'image')),
131
+ prompt TEXT NOT NULL,
132
+ label TEXT,
133
+ category TEXT,
134
+ is_favorite BOOLEAN DEFAULT FALSE,
135
+ usage_count INTEGER DEFAULT 0,
136
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
137
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
138
+ );
139
+
140
+ -- Index pour requêtes rapides
141
+ CREATE INDEX IF NOT EXISTS idx_user_prompts_owner_id ON public.user_prompts(owner_id);
142
+ CREATE INDEX IF NOT EXISTS idx_user_prompts_type ON public.user_prompts(type);
143
+ CREATE INDEX IF NOT EXISTS idx_user_prompts_is_favorite ON public.user_prompts(is_favorite);
144
+
145
+ -- RLS (Row Level Security)
146
+ ALTER TABLE public.user_prompts ENABLE ROW LEVEL SECURITY;
147
+
148
+ -- Politique: Les utilisateurs peuvent voir leurs propres prompts
149
+ DROP POLICY IF EXISTS "Users can view own prompts" ON public.user_prompts;
150
+ CREATE POLICY "Users can view own prompts"
151
+ ON public.user_prompts
152
+ FOR SELECT
153
+ USING (auth.uid() = owner_id);
154
+
155
+ -- Politique: Les utilisateurs peuvent insérer leurs propres prompts
156
+ DROP POLICY IF EXISTS "Users can insert own prompts" ON public.user_prompts;
157
+ CREATE POLICY "Users can insert own prompts"
158
+ ON public.user_prompts
159
+ FOR INSERT
160
+ WITH CHECK (auth.uid() = owner_id);
161
+
162
+ -- Politique: Les utilisateurs peuvent modifier leurs propres prompts
163
+ DROP POLICY IF EXISTS "Users can update own prompts" ON public.user_prompts;
164
+ CREATE POLICY "Users can update own prompts"
165
+ ON public.user_prompts
166
+ FOR UPDATE
167
+ USING (auth.uid() = owner_id);
168
+
169
+ -- Politique: Les utilisateurs peuvent supprimer leurs propres prompts
170
+ DROP POLICY IF EXISTS "Users can delete own prompts" ON public.user_prompts;
171
+ CREATE POLICY "Users can delete own prompts"
172
+ ON public.user_prompts
173
+ FOR DELETE
174
+ USING (auth.uid() = owner_id);
175
+
176
+ -- Trigger pour updated_at
177
+ CREATE OR REPLACE FUNCTION public.update_user_prompts_updated_at()
178
+ RETURNS TRIGGER AS $$
179
+ BEGIN
180
+ NEW.updated_at = NOW();
181
+ RETURN NEW;
182
+ END;
183
+ $$ LANGUAGE plpgsql;
184
+
185
+ DROP TRIGGER IF EXISTS update_user_prompts_updated_at ON public.user_prompts;
186
+ CREATE TRIGGER update_user_prompts_updated_at
187
+ BEFORE UPDATE ON public.user_prompts
188
+ FOR EACH ROW
189
+ EXECUTE FUNCTION public.update_user_prompts_updated_at();
@@ -0,0 +1,122 @@
1
+ -- module-ai module initial migration
2
+ -- Auto-generated by module-create.ts
3
+ -- NOTE: uses helper function set_updated_at() from base migration
4
+
5
+ -- ===========================================================================
6
+ -- Helper: set_updated_at trigger function (if not already present)
7
+ -- ===========================================================================
8
+ CREATE OR REPLACE FUNCTION public.set_updated_at()
9
+ RETURNS trigger
10
+ LANGUAGE plpgsql
11
+ AS $$
12
+ BEGIN
13
+ NEW.updated_at := now();
14
+ RETURN NEW;
15
+ END;
16
+ $$;
17
+
18
+ -- ===========================================================================
19
+ -- Table: public.user_token_ledger
20
+ -- ===========================================================================
21
+ CREATE TABLE IF NOT EXISTS public.user_token_ledger (
22
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
23
+ owner_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
24
+ title text NOT NULL,
25
+ description text,
26
+ created_at timestamptz NOT NULL DEFAULT now(),
27
+ updated_at timestamptz NOT NULL DEFAULT now()
28
+ );
29
+
30
+ -- RLS
31
+ ALTER TABLE public.user_token_ledger ENABLE ROW LEVEL SECURITY;
32
+
33
+ -- Politique: Les utilisateurs peuvent voir leurs propres enregistrements
34
+ DROP POLICY IF EXISTS user_token_ledger_owner_select ON public.user_token_ledger;
35
+ CREATE POLICY user_token_ledger_owner_select ON public.user_token_ledger
36
+ FOR SELECT TO authenticated
37
+ USING (owner_id = auth.uid());
38
+
39
+ -- Politique: Les utilisateurs peuvent créer leurs propres enregistrements
40
+ DROP POLICY IF EXISTS user_token_ledger_owner_insert ON public.user_token_ledger;
41
+ CREATE POLICY user_token_ledger_owner_insert ON public.user_token_ledger
42
+ FOR INSERT TO authenticated
43
+ WITH CHECK (owner_id = auth.uid());
44
+
45
+ -- Politique: Les utilisateurs peuvent modifier leurs propres enregistrements
46
+ DROP POLICY IF EXISTS user_token_ledger_owner_update ON public.user_token_ledger;
47
+ CREATE POLICY user_token_ledger_owner_update ON public.user_token_ledger
48
+ FOR UPDATE TO authenticated
49
+ USING (owner_id = auth.uid())
50
+ WITH CHECK (owner_id = auth.uid());
51
+
52
+ -- Politique: Les utilisateurs peuvent supprimer leurs propres enregistrements
53
+ DROP POLICY IF EXISTS user_token_ledger_owner_delete ON public.user_token_ledger;
54
+ CREATE POLICY user_token_ledger_owner_delete ON public.user_token_ledger
55
+ FOR DELETE TO authenticated
56
+ USING (owner_id = auth.uid());
57
+
58
+ -- Trigger updated_at
59
+ DROP TRIGGER IF EXISTS set_user_token_ledger_updated_at ON public.user_token_ledger;
60
+ CREATE TRIGGER set_user_token_ledger_updated_at
61
+ BEFORE UPDATE ON public.user_token_ledger
62
+ FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
63
+
64
+ -- Index
65
+ CREATE INDEX IF NOT EXISTS idx_user_token_ledger_owner_id ON public.user_token_ledger(owner_id);
66
+
67
+ -- Grants
68
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.user_token_ledger TO service_role;
69
+
70
+
71
+ -- ===========================================================================
72
+ -- Table: public.user_prompts
73
+ -- ===========================================================================
74
+ CREATE TABLE IF NOT EXISTS public.user_prompts (
75
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
76
+ owner_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
77
+ title text NOT NULL,
78
+ description text,
79
+ created_at timestamptz NOT NULL DEFAULT now(),
80
+ updated_at timestamptz NOT NULL DEFAULT now()
81
+ );
82
+
83
+ -- RLS
84
+ ALTER TABLE public.user_prompts ENABLE ROW LEVEL SECURITY;
85
+
86
+ -- Politique: Les utilisateurs peuvent voir leurs propres enregistrements
87
+ DROP POLICY IF EXISTS user_prompts_owner_select ON public.user_prompts;
88
+ CREATE POLICY user_prompts_owner_select ON public.user_prompts
89
+ FOR SELECT TO authenticated
90
+ USING (owner_id = auth.uid());
91
+
92
+ -- Politique: Les utilisateurs peuvent créer leurs propres enregistrements
93
+ DROP POLICY IF EXISTS user_prompts_owner_insert ON public.user_prompts;
94
+ CREATE POLICY user_prompts_owner_insert ON public.user_prompts
95
+ FOR INSERT TO authenticated
96
+ WITH CHECK (owner_id = auth.uid());
97
+
98
+ -- Politique: Les utilisateurs peuvent modifier leurs propres enregistrements
99
+ DROP POLICY IF EXISTS user_prompts_owner_update ON public.user_prompts;
100
+ CREATE POLICY user_prompts_owner_update ON public.user_prompts
101
+ FOR UPDATE TO authenticated
102
+ USING (owner_id = auth.uid())
103
+ WITH CHECK (owner_id = auth.uid());
104
+
105
+ -- Politique: Les utilisateurs peuvent supprimer leurs propres enregistrements
106
+ DROP POLICY IF EXISTS user_prompts_owner_delete ON public.user_prompts;
107
+ CREATE POLICY user_prompts_owner_delete ON public.user_prompts
108
+ FOR DELETE TO authenticated
109
+ USING (owner_id = auth.uid());
110
+
111
+ -- Trigger updated_at
112
+ DROP TRIGGER IF EXISTS set_user_prompts_updated_at ON public.user_prompts;
113
+ CREATE TRIGGER set_user_prompts_updated_at
114
+ BEFORE UPDATE ON public.user_prompts
115
+ FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
116
+
117
+ -- Index
118
+ CREATE INDEX IF NOT EXISTS idx_user_prompts_owner_id ON public.user_prompts(owner_id);
119
+
120
+ -- Grants
121
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.user_prompts TO service_role;
122
+
@@ -0,0 +1,23 @@
1
+ -- Migration DOWN pour ai_tokens
2
+ -- Supprimer tous les éléments créés dans la migration up
3
+
4
+ -- Supprimer le trigger
5
+ DROP TRIGGER IF EXISTS check_token_balance_trigger ON public.user_token_ledger;
6
+
7
+ -- Supprimer la fonction trigger
8
+ DROP FUNCTION IF EXISTS public.check_token_balance() CASCADE;
9
+
10
+ -- Supprimer les policies RLS
11
+ DROP POLICY IF EXISTS "Users can view their own token balance" ON public.user_token_balance_v;
12
+ DROP POLICY IF EXISTS "Users can view their own token ledger" ON public.user_token_ledger;
13
+
14
+ -- Supprimer la vue
15
+ DROP VIEW IF EXISTS public.user_token_balance_v CASCADE;
16
+
17
+ -- Supprimer les indexes
18
+ DROP INDEX IF EXISTS idx_user_token_ledger_owner_id;
19
+ DROP INDEX IF EXISTS idx_user_token_ledger_ts;
20
+ DROP INDEX IF EXISTS idx_user_token_ledger_type;
21
+
22
+ -- Supprimer la table user_token_ledger
23
+ DROP TABLE IF EXISTS public.user_token_ledger CASCADE;