@lastbrain/ai-ui-react 1.0.9 → 1.0.11
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.
- package/dist/components/AiImageButton.d.ts.map +1 -1
- package/dist/components/AiImageButton.js +5 -2
- package/dist/components/AiInput.d.ts.map +1 -1
- package/dist/components/AiInput.js +21 -8
- package/dist/components/AiPromptPanel.d.ts +4 -2
- package/dist/components/AiPromptPanel.d.ts.map +1 -1
- package/dist/components/AiPromptPanel.js +344 -27
- package/dist/components/AiSelect.d.ts.map +1 -1
- package/dist/components/AiSelect.js +4 -1
- package/dist/components/AiStatusButton.d.ts.map +1 -1
- package/dist/components/AiStatusButton.js +130 -38
- package/dist/components/AiTextarea.d.ts.map +1 -1
- package/dist/components/AiTextarea.js +36 -9
- package/dist/components/UsageToast.d.ts +14 -0
- package/dist/components/UsageToast.d.ts.map +1 -0
- package/dist/components/UsageToast.js +144 -0
- package/dist/hooks/usePrompts.d.ts +1 -0
- package/dist/hooks/usePrompts.d.ts.map +1 -1
- package/dist/hooks/usePrompts.js +0 -1
- package/dist/styles/inline.d.ts.map +1 -1
- package/dist/styles/inline.js +119 -62
- package/package.json +2 -1
- package/src/components/AiImageButton.tsx +13 -2
- package/src/components/AiInput.tsx +35 -28
- package/src/components/AiPromptPanel.tsx +639 -66
- package/src/components/AiSelect.tsx +11 -0
- package/src/components/AiStatusButton.tsx +284 -169
- package/src/components/AiTextarea.tsx +58 -29
- package/src/components/UsageToast.tsx +182 -0
- package/src/hooks/usePrompts.ts +1 -1
- package/src/styles/inline.ts +129 -63
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import React, {
|
|
3
|
+
import React, {
|
|
4
|
+
useState,
|
|
5
|
+
useRef,
|
|
6
|
+
useLayoutEffect,
|
|
7
|
+
type TextareaHTMLAttributes,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { Sparkles } from "lucide-react";
|
|
4
10
|
import type { BaseAiProps } from "../types";
|
|
5
11
|
import { useAiCallText } from "../hooks/useAiCallText";
|
|
6
12
|
import { useAiModels } from "../hooks/useAiModels";
|
|
7
13
|
import { AiPromptPanel } from "./AiPromptPanel";
|
|
14
|
+
import { UsageToast, useUsageToast } from "./UsageToast";
|
|
8
15
|
import { aiStyles } from "../styles/inline";
|
|
9
16
|
|
|
10
17
|
export interface AiTextareaProps
|
|
@@ -37,6 +44,7 @@ export function AiTextarea({
|
|
|
37
44
|
const [isFocused, setIsFocused] = useState(false);
|
|
38
45
|
const [isButtonHovered, setIsButtonHovered] = useState(false);
|
|
39
46
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
47
|
+
const { showUsageToast, toastData, toastKey, clearToast } = useUsageToast();
|
|
40
48
|
|
|
41
49
|
const { models } = useAiModels({ baseUrl, apiKeyId });
|
|
42
50
|
const { generateText, loading } = useAiCallText({ baseUrl, apiKeyId });
|
|
@@ -53,14 +61,22 @@ export function AiTextarea({
|
|
|
53
61
|
|
|
54
62
|
const handleSubmit = async (
|
|
55
63
|
selectedModel: string,
|
|
56
|
-
selectedPrompt: string
|
|
64
|
+
selectedPrompt: string,
|
|
65
|
+
promptId?: string
|
|
57
66
|
) => {
|
|
58
67
|
try {
|
|
68
|
+
const resolvedContext = textareaValue || context || undefined;
|
|
69
|
+
const hasContext = Boolean(
|
|
70
|
+
resolvedContext && String(resolvedContext).trim()
|
|
71
|
+
);
|
|
72
|
+
const promptWithContext = hasContext
|
|
73
|
+
? `${selectedPrompt}\n\nTexte:\n${String(resolvedContext)}`
|
|
74
|
+
: selectedPrompt;
|
|
59
75
|
const result = await generateText({
|
|
60
76
|
model: selectedModel,
|
|
61
|
-
prompt:
|
|
62
|
-
context:
|
|
63
|
-
actionType: "autocomplete",
|
|
77
|
+
prompt: promptWithContext,
|
|
78
|
+
context: resolvedContext,
|
|
79
|
+
actionType: hasContext ? "generate-text" : "autocomplete",
|
|
64
80
|
});
|
|
65
81
|
|
|
66
82
|
if (result.text) {
|
|
@@ -70,6 +86,7 @@ export function AiTextarea({
|
|
|
70
86
|
}
|
|
71
87
|
onValue?.(result.text);
|
|
72
88
|
onToast?.({ type: "success", message: "AI generation successful" });
|
|
89
|
+
showUsageToast(result);
|
|
73
90
|
}
|
|
74
91
|
} catch (error) {
|
|
75
92
|
onToast?.({ type: "error", message: "Failed to generate text" });
|
|
@@ -82,11 +99,18 @@ export function AiTextarea({
|
|
|
82
99
|
if (!model || !prompt) return;
|
|
83
100
|
|
|
84
101
|
try {
|
|
102
|
+
const resolvedContext = textareaValue || context || undefined;
|
|
103
|
+
const hasContext = Boolean(
|
|
104
|
+
resolvedContext && String(resolvedContext).trim()
|
|
105
|
+
);
|
|
106
|
+
const promptWithContext = hasContext
|
|
107
|
+
? `${prompt}\n\nTexte:\n${String(resolvedContext)}`
|
|
108
|
+
: prompt;
|
|
85
109
|
const result = await generateText({
|
|
86
110
|
model,
|
|
87
|
-
prompt,
|
|
88
|
-
context:
|
|
89
|
-
actionType: "autocomplete",
|
|
111
|
+
prompt: promptWithContext,
|
|
112
|
+
context: resolvedContext,
|
|
113
|
+
actionType: hasContext ? "generate-text" : "autocomplete",
|
|
90
114
|
});
|
|
91
115
|
|
|
92
116
|
if (result.text) {
|
|
@@ -96,6 +120,7 @@ export function AiTextarea({
|
|
|
96
120
|
}
|
|
97
121
|
onValue?.(result.text);
|
|
98
122
|
onToast?.({ type: "success", message: "AI generation successful" });
|
|
123
|
+
showUsageToast(result);
|
|
99
124
|
}
|
|
100
125
|
} catch (error) {
|
|
101
126
|
onToast?.({ type: "error", message: "Failed to generate text" });
|
|
@@ -108,6 +133,20 @@ export function AiTextarea({
|
|
|
108
133
|
textareaProps.onChange?.(e);
|
|
109
134
|
};
|
|
110
135
|
|
|
136
|
+
const adjustHeight = () => {
|
|
137
|
+
const element = textareaRef.current;
|
|
138
|
+
if (!element) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
element.style.height = "auto";
|
|
143
|
+
element.style.height = `${element.scrollHeight}px`;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
useLayoutEffect(() => {
|
|
147
|
+
adjustHeight();
|
|
148
|
+
}, [textareaValue]);
|
|
149
|
+
|
|
111
150
|
return (
|
|
112
151
|
<div style={aiStyles.textareaWrapper} className={className}>
|
|
113
152
|
<textarea
|
|
@@ -122,6 +161,7 @@ export function AiTextarea({
|
|
|
122
161
|
onFocus={(e) => {
|
|
123
162
|
setIsFocused(true);
|
|
124
163
|
textareaProps.onFocus?.(e);
|
|
164
|
+
adjustHeight();
|
|
125
165
|
}}
|
|
126
166
|
onBlur={(e) => {
|
|
127
167
|
setIsFocused(false);
|
|
@@ -155,28 +195,8 @@ export function AiTextarea({
|
|
|
155
195
|
>
|
|
156
196
|
<path d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83" />
|
|
157
197
|
</svg>
|
|
158
|
-
) : hasConfiguration ? (
|
|
159
|
-
<svg
|
|
160
|
-
width="16"
|
|
161
|
-
height="16"
|
|
162
|
-
viewBox="0 0 24 24"
|
|
163
|
-
fill="none"
|
|
164
|
-
stroke="currentColor"
|
|
165
|
-
strokeWidth="2"
|
|
166
|
-
>
|
|
167
|
-
<path d="M12 5v14M5 12h14" />
|
|
168
|
-
</svg>
|
|
169
198
|
) : (
|
|
170
|
-
<
|
|
171
|
-
width="16"
|
|
172
|
-
height="16"
|
|
173
|
-
viewBox="0 0 24 24"
|
|
174
|
-
fill="none"
|
|
175
|
-
stroke="currentColor"
|
|
176
|
-
strokeWidth="2"
|
|
177
|
-
>
|
|
178
|
-
<path d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
|
|
179
|
-
</svg>
|
|
199
|
+
<Sparkles size={16} />
|
|
180
200
|
)}
|
|
181
201
|
</button>
|
|
182
202
|
{isOpen && (
|
|
@@ -186,6 +206,15 @@ export function AiTextarea({
|
|
|
186
206
|
onSubmit={handleSubmit}
|
|
187
207
|
uiMode={uiMode}
|
|
188
208
|
models={models || []}
|
|
209
|
+
sourceText={textareaValue || undefined}
|
|
210
|
+
/>
|
|
211
|
+
)}
|
|
212
|
+
{Boolean(toastData) && (
|
|
213
|
+
<UsageToast
|
|
214
|
+
key={toastKey}
|
|
215
|
+
result={toastData}
|
|
216
|
+
position="bottom-right"
|
|
217
|
+
onComplete={clearToast}
|
|
189
218
|
/>
|
|
190
219
|
)}
|
|
191
220
|
</div>
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { X } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
interface UsageToastProps {
|
|
7
|
+
result: unknown;
|
|
8
|
+
position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
|
|
9
|
+
onComplete?: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function UsageToast({
|
|
13
|
+
result,
|
|
14
|
+
position = "bottom-right",
|
|
15
|
+
onComplete,
|
|
16
|
+
}: UsageToastProps) {
|
|
17
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
18
|
+
const [isClosing, setIsClosing] = useState(false);
|
|
19
|
+
const fadeTimeoutRef = useRef<number | null>(null);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (result) {
|
|
23
|
+
// Show toast immediately
|
|
24
|
+
setIsVisible(true);
|
|
25
|
+
setIsClosing(false);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return () => {
|
|
29
|
+
if (fadeTimeoutRef.current) {
|
|
30
|
+
window.clearTimeout(fadeTimeoutRef.current);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}, [result]);
|
|
34
|
+
|
|
35
|
+
const handleClose = () => {
|
|
36
|
+
if (isClosing) return;
|
|
37
|
+
setIsClosing(true);
|
|
38
|
+
fadeTimeoutRef.current = window.setTimeout(() => {
|
|
39
|
+
setIsVisible(false);
|
|
40
|
+
onComplete?.();
|
|
41
|
+
}, 200);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const extractUsageMessage = (data: unknown) => {
|
|
45
|
+
const result = data as any;
|
|
46
|
+
|
|
47
|
+
// Extract cost from various possible locations
|
|
48
|
+
const rawCost =
|
|
49
|
+
result?.cost ??
|
|
50
|
+
result?.price ??
|
|
51
|
+
result?.usage?.cost ??
|
|
52
|
+
result?.usage?.total ??
|
|
53
|
+
result?.usage?.usd ??
|
|
54
|
+
result?.usage?.credits ??
|
|
55
|
+
result?.tokens?.cost ??
|
|
56
|
+
result?.tokens?.price ??
|
|
57
|
+
result?.balance?.cost ??
|
|
58
|
+
result?.balance?.used ??
|
|
59
|
+
result?.balance?.spent ??
|
|
60
|
+
result?.credits_used ??
|
|
61
|
+
result?.usd ??
|
|
62
|
+
0;
|
|
63
|
+
|
|
64
|
+
const cost = Number.isFinite(Number(rawCost)) ? Number(rawCost) : 0;
|
|
65
|
+
|
|
66
|
+
// Smart formatting: show meaningful decimals
|
|
67
|
+
let formatted;
|
|
68
|
+
if (cost >= 1) {
|
|
69
|
+
formatted = cost.toFixed(2);
|
|
70
|
+
} else if (cost >= 0.01) {
|
|
71
|
+
formatted = cost.toFixed(4);
|
|
72
|
+
} else if (cost >= 0.0001) {
|
|
73
|
+
formatted = cost.toFixed(6);
|
|
74
|
+
} else if (cost > 0) {
|
|
75
|
+
formatted = cost.toFixed(8);
|
|
76
|
+
} else {
|
|
77
|
+
formatted = "0.0000";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Remove trailing zeros
|
|
81
|
+
formatted = parseFloat(formatted).toString();
|
|
82
|
+
|
|
83
|
+
return `${formatted}$ used`;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const message = extractUsageMessage(result);
|
|
87
|
+
if (!result) return null;
|
|
88
|
+
|
|
89
|
+
const getPositionStyles = () => {
|
|
90
|
+
const baseStyles = {
|
|
91
|
+
position: "absolute" as const,
|
|
92
|
+
zIndex: 1000,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
switch (position) {
|
|
96
|
+
case "bottom-right":
|
|
97
|
+
return { ...baseStyles, bottom: "8px", right: "8px" };
|
|
98
|
+
case "bottom-left":
|
|
99
|
+
return { ...baseStyles, bottom: "8px", left: "8px" };
|
|
100
|
+
case "top-right":
|
|
101
|
+
return { ...baseStyles, top: "8px", right: "8px" };
|
|
102
|
+
case "top-left":
|
|
103
|
+
return { ...baseStyles, top: "8px", left: "8px" };
|
|
104
|
+
default:
|
|
105
|
+
return { ...baseStyles, bottom: "8px", right: "8px" };
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div
|
|
111
|
+
style={{
|
|
112
|
+
...getPositionStyles(),
|
|
113
|
+
opacity: isVisible && !isClosing ? 1 : 0,
|
|
114
|
+
transform: `translateY(${isVisible && !isClosing ? "0" : "8px"})`,
|
|
115
|
+
transition: "opacity 200ms ease, transform 200ms ease",
|
|
116
|
+
padding: "4px 6px",
|
|
117
|
+
borderRadius: "6px",
|
|
118
|
+
marginBottom: "4px",
|
|
119
|
+
right: "6px",
|
|
120
|
+
background: "rgba(22, 163, 74, 0.12)",
|
|
121
|
+
border: "1px solid rgba(22, 163, 74, 0.3)",
|
|
122
|
+
color: "#16a34a",
|
|
123
|
+
fontSize: "9px",
|
|
124
|
+
fontWeight: 600,
|
|
125
|
+
display: "flex",
|
|
126
|
+
alignItems: "center",
|
|
127
|
+
gap: "8px",
|
|
128
|
+
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1)",
|
|
129
|
+
backdropFilter: "blur(4px)",
|
|
130
|
+
WebkitBackdropFilter: "blur(4px)",
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
<span>{message}</span>
|
|
134
|
+
<button
|
|
135
|
+
onClick={handleClose}
|
|
136
|
+
style={{
|
|
137
|
+
background: "transparent",
|
|
138
|
+
border: "none",
|
|
139
|
+
color: "#16a34a",
|
|
140
|
+
cursor: "pointer",
|
|
141
|
+
padding: "2px",
|
|
142
|
+
display: "flex",
|
|
143
|
+
alignItems: "center",
|
|
144
|
+
justifyContent: "center",
|
|
145
|
+
borderRadius: "4px",
|
|
146
|
+
transition: "background-color 150ms ease",
|
|
147
|
+
}}
|
|
148
|
+
onMouseEnter={(e) => {
|
|
149
|
+
e.currentTarget.style.backgroundColor = "rgba(22, 163, 74, 0.2)";
|
|
150
|
+
}}
|
|
151
|
+
onMouseLeave={(e) => {
|
|
152
|
+
e.currentTarget.style.backgroundColor = "transparent";
|
|
153
|
+
}}
|
|
154
|
+
title="Close"
|
|
155
|
+
>
|
|
156
|
+
<X size={12} />
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function useUsageToast() {
|
|
163
|
+
const [toastData, setToastData] = useState<unknown>(null);
|
|
164
|
+
const [toastKey, setToastKey] = useState(0);
|
|
165
|
+
|
|
166
|
+
const showUsageToast = (result: unknown) => {
|
|
167
|
+
// Replace any existing toast with new one
|
|
168
|
+
setToastKey((prev) => prev + 1);
|
|
169
|
+
setToastData(result);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const clearToast = () => {
|
|
173
|
+
setToastData(null);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
showUsageToast,
|
|
178
|
+
toastData,
|
|
179
|
+
toastKey,
|
|
180
|
+
clearToast,
|
|
181
|
+
};
|
|
182
|
+
}
|
package/src/hooks/usePrompts.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface Prompt {
|
|
|
12
12
|
is_public: boolean;
|
|
13
13
|
favorite: boolean;
|
|
14
14
|
tags: string[];
|
|
15
|
+
model?: string | null;
|
|
15
16
|
created_at: string;
|
|
16
17
|
updated_at?: string;
|
|
17
18
|
}
|
|
@@ -168,7 +169,6 @@ export function usePrompts(): UsePromptsReturn {
|
|
|
168
169
|
});
|
|
169
170
|
} catch (err) {
|
|
170
171
|
// Silent fail for stats
|
|
171
|
-
console.error("Failed to increment stat:", err);
|
|
172
172
|
}
|
|
173
173
|
},
|
|
174
174
|
[]
|