@lastbrain/ai-ui-react 1.0.8 → 1.0.10

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.
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
 
3
3
  import type { AiStatus } from "@lastbrain/ai-ui-core";
4
- import { useState } from "react";
5
- import { aiStyles } from "../styles/inline";
4
+ import { useState, useRef, useEffect } from "react";
5
+ import { aiStyles, calculateTooltipPosition } from "../styles/inline";
6
6
 
7
7
  export interface AiStatusButtonProps {
8
8
  status: AiStatus | null;
@@ -17,10 +17,40 @@ export function AiStatusButton({
17
17
  }: AiStatusButtonProps) {
18
18
  const [showTooltip, setShowTooltip] = useState(false);
19
19
  const [isHovered, setIsHovered] = useState(false);
20
+ const [tooltipPosition, setTooltipPosition] = useState<any>({});
21
+ const buttonRef = useRef<HTMLButtonElement>(null);
22
+ const tooltipRef = useRef<HTMLDivElement>(null);
23
+
24
+ useEffect(() => {
25
+ if (showTooltip && buttonRef.current) {
26
+ const buttonRect = buttonRef.current.getBoundingClientRect();
27
+ const position = calculateTooltipPosition(buttonRect);
28
+ setTooltipPosition(position);
29
+ }
30
+ }, [showTooltip]);
31
+
32
+ const handleMouseEnter = () => {
33
+ setShowTooltip(true);
34
+ setIsHovered(true);
35
+ };
36
+
37
+ const handleMouseLeave = () => {
38
+ // Keep tooltip visible if hovering over it
39
+ setTimeout(() => {
40
+ if (
41
+ !tooltipRef.current?.matches(":hover") &&
42
+ !buttonRef.current?.matches(":hover")
43
+ ) {
44
+ setShowTooltip(false);
45
+ setIsHovered(false);
46
+ }
47
+ }, 100);
48
+ };
20
49
 
21
50
  if (loading) {
22
51
  return (
23
52
  <button
53
+ ref={buttonRef}
24
54
  style={{
25
55
  ...aiStyles.statusButton,
26
56
  ...aiStyles.statusButtonDisabled,
@@ -29,9 +59,7 @@ export function AiStatusButton({
29
59
  disabled
30
60
  >
31
61
  <svg
32
- style={{
33
- animation: "ai-spin 1s linear infinite",
34
- }}
62
+ style={aiStyles.spinner}
35
63
  width="16"
36
64
  height="16"
37
65
  viewBox="0 0 24 24"
@@ -48,20 +76,15 @@ export function AiStatusButton({
48
76
  return (
49
77
  <div style={{ position: "relative", display: "inline-block" }}>
50
78
  <button
79
+ ref={buttonRef}
51
80
  style={{
52
81
  ...aiStyles.statusButton,
53
82
  color: "#ef4444",
54
83
  ...(isHovered && aiStyles.statusButtonHover),
55
84
  }}
56
85
  className={className}
57
- onMouseEnter={() => {
58
- setShowTooltip(true);
59
- setIsHovered(true);
60
- }}
61
- onMouseLeave={() => {
62
- setShowTooltip(false);
63
- setIsHovered(false);
64
- }}
86
+ onMouseEnter={handleMouseEnter}
87
+ onMouseLeave={handleMouseLeave}
65
88
  >
66
89
  <svg
67
90
  width="16"
@@ -73,7 +96,16 @@ export function AiStatusButton({
73
96
  <polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
74
97
  </svg>
75
98
  </button>
76
- {showTooltip && <div style={aiStyles.tooltip}>No status available</div>}
99
+ {showTooltip && (
100
+ <div
101
+ ref={tooltipRef}
102
+ style={{ ...aiStyles.tooltip, ...tooltipPosition }}
103
+ onMouseEnter={() => setShowTooltip(true)}
104
+ onMouseLeave={handleMouseLeave}
105
+ >
106
+ No status available
107
+ </div>
108
+ )}
77
109
  </div>
78
110
  );
79
111
  }
@@ -81,20 +113,15 @@ export function AiStatusButton({
81
113
  return (
82
114
  <div style={{ position: "relative", display: "inline-block" }}>
83
115
  <button
116
+ ref={buttonRef}
84
117
  style={{
85
118
  ...aiStyles.statusButton,
86
119
  color: "#10b981",
87
120
  ...(isHovered && aiStyles.statusButtonHover),
88
121
  }}
89
122
  className={className}
90
- onMouseEnter={() => {
91
- setShowTooltip(true);
92
- setIsHovered(true);
93
- }}
94
- onMouseLeave={() => {
95
- setShowTooltip(false);
96
- setIsHovered(false);
97
- }}
123
+ onMouseEnter={handleMouseEnter}
124
+ onMouseLeave={handleMouseLeave}
98
125
  >
99
126
  <svg
100
127
  width="16"
@@ -108,7 +135,12 @@ export function AiStatusButton({
108
135
  </button>
109
136
 
110
137
  {showTooltip && (
111
- <div style={aiStyles.tooltip}>
138
+ <div
139
+ ref={tooltipRef}
140
+ style={{ ...aiStyles.tooltip, ...tooltipPosition }}
141
+ onMouseEnter={() => setShowTooltip(true)}
142
+ onMouseLeave={handleMouseLeave}
143
+ >
112
144
  <div style={aiStyles.tooltipHeader}>API Status</div>
113
145
 
114
146
  <div
@@ -224,49 +256,43 @@ export function AiStatusButton({
224
256
 
225
257
  <div style={aiStyles.tooltipActions}>
226
258
  <a
227
- href="https://ai.lastbrain.io/fr/auth/dashboard"
259
+ href="https://prompt.lastbrain.io/fr/auth/dashboard"
228
260
  target="_blank"
229
261
  rel="noopener noreferrer"
230
262
  style={aiStyles.tooltipLink}
231
263
  onMouseEnter={(e) => {
232
- e.currentTarget.style.background = "#dbeafe";
233
- e.currentTarget.style.borderColor = "#3b82f6";
264
+ Object.assign(e.currentTarget.style, aiStyles.tooltipLinkHover);
234
265
  }}
235
266
  onMouseLeave={(e) => {
236
- e.currentTarget.style.background = "#eff6ff";
237
- e.currentTarget.style.borderColor = "#dbeafe";
267
+ Object.assign(e.currentTarget.style, aiStyles.tooltipLink);
238
268
  }}
239
269
  >
240
270
  Dashboard
241
271
  </a>
242
272
  <a
243
- href="https://ai.lastbrain.io/fr/auth/billing"
273
+ href="https://prompt.lastbrain.io/fr/auth/billing"
244
274
  target="_blank"
245
275
  rel="noopener noreferrer"
246
276
  style={aiStyles.tooltipLink}
247
277
  onMouseEnter={(e) => {
248
- e.currentTarget.style.background = "#dbeafe";
249
- e.currentTarget.style.borderColor = "#3b82f6";
278
+ Object.assign(e.currentTarget.style, aiStyles.tooltipLinkHover);
250
279
  }}
251
280
  onMouseLeave={(e) => {
252
- e.currentTarget.style.background = "#eff6ff";
253
- e.currentTarget.style.borderColor = "#dbeafe";
281
+ Object.assign(e.currentTarget.style, aiStyles.tooltipLink);
254
282
  }}
255
283
  >
256
284
  History
257
285
  </a>
258
286
  <a
259
- href="https://ai.lastbrain.io/fr/auth/billing"
287
+ href="https://prompt.lastbrain.io/fr/auth/billing"
260
288
  target="_blank"
261
289
  rel="noopener noreferrer"
262
290
  style={aiStyles.tooltipLink}
263
291
  onMouseEnter={(e) => {
264
- e.currentTarget.style.background = "#dbeafe";
265
- e.currentTarget.style.borderColor = "#3b82f6";
292
+ Object.assign(e.currentTarget.style, aiStyles.tooltipLinkHover);
266
293
  }}
267
294
  onMouseLeave={(e) => {
268
- e.currentTarget.style.background = "#eff6ff";
269
- e.currentTarget.style.borderColor = "#dbeafe";
295
+ Object.assign(e.currentTarget.style, aiStyles.tooltipLink);
270
296
  }}
271
297
  >
272
298
  Settings
@@ -5,6 +5,7 @@ import type { BaseAiProps } from "../types";
5
5
  import { useAiCallText } from "../hooks/useAiCallText";
6
6
  import { useAiModels } from "../hooks/useAiModels";
7
7
  import { AiPromptPanel } from "./AiPromptPanel";
8
+ import { aiStyles } from "../styles/inline";
8
9
 
9
10
  export interface AiTextareaProps
10
11
  extends
@@ -28,11 +29,13 @@ export function AiTextarea({
28
29
  ...textareaProps
29
30
  }: AiTextareaProps) {
30
31
  const [isOpen, setIsOpen] = useState(false);
31
- const [textValue, setTextValue] = useState(
32
+ const [textareaValue, setTextareaValue] = useState(
32
33
  textareaProps.value?.toString() ||
33
34
  textareaProps.defaultValue?.toString() ||
34
35
  ""
35
36
  );
37
+ const [isFocused, setIsFocused] = useState(false);
38
+ const [isButtonHovered, setIsButtonHovered] = useState(false);
36
39
  const textareaRef = useRef<HTMLTextAreaElement>(null);
37
40
 
38
41
  const { models } = useAiModels({ baseUrl, apiKeyId });
@@ -50,22 +53,21 @@ export function AiTextarea({
50
53
 
51
54
  const handleSubmit = async (
52
55
  selectedModel: string,
53
- selectedPrompt: string
56
+ selectedPrompt: string,
57
+ promptId?: string
54
58
  ) => {
55
59
  try {
56
60
  const result = await generateText({
57
61
  model: selectedModel,
58
62
  prompt: selectedPrompt,
59
- context: context || textValue || undefined,
63
+ context: textareaValue || context || undefined,
60
64
  actionType: "autocomplete",
61
65
  });
62
66
 
63
67
  if (result.text) {
64
- if (editMode) {
65
- setTextValue(result.text);
66
- if (textareaRef.current) {
67
- textareaRef.current.value = result.text;
68
- }
68
+ setTextareaValue(result.text);
69
+ if (textareaRef.current) {
70
+ textareaRef.current.value = result.text;
69
71
  }
70
72
  onValue?.(result.text);
71
73
  onToast?.({ type: "success", message: "AI generation successful" });
@@ -84,16 +86,14 @@ export function AiTextarea({
84
86
  const result = await generateText({
85
87
  model,
86
88
  prompt,
87
- context: context || textValue || undefined,
89
+ context: textareaValue || context || undefined,
88
90
  actionType: "autocomplete",
89
91
  });
90
92
 
91
93
  if (result.text) {
92
- if (editMode) {
93
- setTextValue(result.text);
94
- if (textareaRef.current) {
95
- textareaRef.current.value = result.text;
96
- }
94
+ setTextareaValue(result.text);
95
+ if (textareaRef.current) {
96
+ textareaRef.current.value = result.text;
97
97
  }
98
98
  onValue?.(result.text);
99
99
  onToast?.({ type: "success", message: "AI generation successful" });
@@ -105,39 +105,81 @@ export function AiTextarea({
105
105
 
106
106
  const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
107
107
  const newValue = e.target.value;
108
- setTextValue(newValue);
108
+ setTextareaValue(newValue);
109
109
  textareaProps.onChange?.(e);
110
110
  };
111
111
 
112
112
  return (
113
- <div data-ai-textarea-wrapper className={className}>
113
+ <div style={aiStyles.textareaWrapper} className={className}>
114
114
  <textarea
115
115
  ref={textareaRef}
116
116
  {...textareaProps}
117
- value={textValue}
117
+ style={{
118
+ ...aiStyles.textarea,
119
+ ...(isFocused && aiStyles.textareaFocus),
120
+ }}
121
+ value={textareaValue}
118
122
  onChange={handleTextareaChange}
123
+ onFocus={(e) => {
124
+ setIsFocused(true);
125
+ textareaProps.onFocus?.(e);
126
+ }}
127
+ onBlur={(e) => {
128
+ setIsFocused(false);
129
+ textareaProps.onBlur?.(e);
130
+ }}
119
131
  disabled={disabled || loading}
120
- data-ai-textarea
121
132
  />
122
- {hasConfiguration ? (
123
- <button
124
- onClick={handleQuickGenerate}
125
- disabled={disabled || loading}
126
- data-ai-generate-button
127
- type="button"
128
- >
129
- {loading ? "Generating..." : "AI"}
130
- </button>
131
- ) : (
132
- <button
133
- onClick={handleOpenPanel}
134
- disabled={disabled || loading}
135
- data-ai-setup-button
136
- type="button"
137
- >
138
- Setup AI
139
- </button>
140
- )}
133
+ <button
134
+ style={{
135
+ ...aiStyles.textareaAiButton,
136
+ ...(isButtonHovered && aiStyles.textareaAiButtonHover),
137
+ ...(disabled || loading
138
+ ? { opacity: 0.5, cursor: "not-allowed" }
139
+ : {}),
140
+ }}
141
+ onClick={hasConfiguration ? handleQuickGenerate : handleOpenPanel}
142
+ onMouseEnter={() => setIsButtonHovered(true)}
143
+ onMouseLeave={() => setIsButtonHovered(false)}
144
+ disabled={disabled || loading}
145
+ type="button"
146
+ title={hasConfiguration ? "Generate with AI" : "Setup AI"}
147
+ >
148
+ {loading ? (
149
+ <svg
150
+ style={aiStyles.spinner}
151
+ width="16"
152
+ height="16"
153
+ viewBox="0 0 24 24"
154
+ fill="none"
155
+ stroke="currentColor"
156
+ >
157
+ <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" />
158
+ </svg>
159
+ ) : hasConfiguration ? (
160
+ <svg
161
+ width="16"
162
+ height="16"
163
+ viewBox="0 0 24 24"
164
+ fill="none"
165
+ stroke="currentColor"
166
+ strokeWidth="2"
167
+ >
168
+ <path d="M12 5v14M5 12h14" />
169
+ </svg>
170
+ ) : (
171
+ <svg
172
+ width="16"
173
+ height="16"
174
+ viewBox="0 0 24 24"
175
+ fill="none"
176
+ stroke="currentColor"
177
+ strokeWidth="2"
178
+ >
179
+ <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" />
180
+ </svg>
181
+ )}
182
+ </button>
141
183
  {isOpen && (
142
184
  <AiPromptPanel
143
185
  isOpen={isOpen}
@@ -145,6 +187,7 @@ export function AiTextarea({
145
187
  onSubmit={handleSubmit}
146
188
  uiMode={uiMode}
147
189
  models={models || []}
190
+ sourceText={textareaValue || undefined}
148
191
  />
149
192
  )}
150
193
  </div>