@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.
@@ -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 AiInputProps
10
11
  extends
@@ -31,6 +32,8 @@ export function AiInput({
31
32
  const [inputValue, setInputValue] = useState(
32
33
  inputProps.value?.toString() || inputProps.defaultValue?.toString() || ""
33
34
  );
35
+ const [isFocused, setIsFocused] = useState(false);
36
+ const [isButtonHovered, setIsButtonHovered] = useState(false);
34
37
  const inputRef = useRef<HTMLInputElement>(null);
35
38
 
36
39
  const { models } = useAiModels({ baseUrl, apiKeyId });
@@ -48,22 +51,21 @@ export function AiInput({
48
51
 
49
52
  const handleSubmit = async (
50
53
  selectedModel: string,
51
- selectedPrompt: string
54
+ selectedPrompt: string,
55
+ promptId?: string
52
56
  ) => {
53
57
  try {
54
58
  const result = await generateText({
55
59
  model: selectedModel,
56
60
  prompt: selectedPrompt,
57
- context: context || inputValue || undefined,
61
+ context: inputValue || context || undefined,
58
62
  actionType: "autocomplete",
59
63
  });
60
64
 
61
65
  if (result.text) {
62
- if (editMode) {
63
- setInputValue(result.text);
64
- if (inputRef.current) {
65
- inputRef.current.value = result.text;
66
- }
66
+ setInputValue(result.text);
67
+ if (inputRef.current) {
68
+ inputRef.current.value = result.text;
67
69
  }
68
70
  onValue?.(result.text);
69
71
  onToast?.({ type: "success", message: "AI generation successful" });
@@ -82,16 +84,14 @@ export function AiInput({
82
84
  const result = await generateText({
83
85
  model,
84
86
  prompt,
85
- context: context || inputValue || undefined,
87
+ context: inputValue || context || undefined,
86
88
  actionType: "autocomplete",
87
89
  });
88
90
 
89
91
  if (result.text) {
90
- if (editMode) {
91
- setInputValue(result.text);
92
- if (inputRef.current) {
93
- inputRef.current.value = result.text;
94
- }
92
+ setInputValue(result.text);
93
+ if (inputRef.current) {
94
+ inputRef.current.value = result.text;
95
95
  }
96
96
  onValue?.(result.text);
97
97
  onToast?.({ type: "success", message: "AI generation successful" });
@@ -108,34 +108,76 @@ export function AiInput({
108
108
  };
109
109
 
110
110
  return (
111
- <div data-ai-input-wrapper className={className}>
111
+ <div style={aiStyles.inputWrapper} className={className}>
112
112
  <input
113
113
  ref={inputRef}
114
114
  {...inputProps}
115
+ style={{
116
+ ...aiStyles.input,
117
+ ...(isFocused && aiStyles.inputFocus),
118
+ }}
115
119
  value={inputValue}
116
120
  onChange={handleInputChange}
121
+ onFocus={(e) => {
122
+ setIsFocused(true);
123
+ inputProps.onFocus?.(e);
124
+ }}
125
+ onBlur={(e) => {
126
+ setIsFocused(false);
127
+ inputProps.onBlur?.(e);
128
+ }}
117
129
  disabled={disabled || loading}
118
- data-ai-input
119
130
  />
120
- {hasConfiguration ? (
121
- <button
122
- onClick={handleQuickGenerate}
123
- disabled={disabled || loading}
124
- data-ai-generate-button
125
- type="button"
126
- >
127
- {loading ? "Generating..." : "AI"}
128
- </button>
129
- ) : (
130
- <button
131
- onClick={handleOpenPanel}
132
- disabled={disabled || loading}
133
- data-ai-setup-button
134
- type="button"
135
- >
136
- Setup AI
137
- </button>
138
- )}
131
+ <button
132
+ style={{
133
+ ...aiStyles.inputAiButton,
134
+ ...(isButtonHovered && aiStyles.inputAiButtonHover),
135
+ ...(disabled || loading
136
+ ? { opacity: 0.5, cursor: "not-allowed" }
137
+ : {}),
138
+ }}
139
+ onClick={hasConfiguration ? handleQuickGenerate : handleOpenPanel}
140
+ onMouseEnter={() => setIsButtonHovered(true)}
141
+ onMouseLeave={() => setIsButtonHovered(false)}
142
+ disabled={disabled || loading}
143
+ type="button"
144
+ title={hasConfiguration ? "Generate with AI" : "Setup AI"}
145
+ >
146
+ {loading ? (
147
+ <svg
148
+ style={aiStyles.spinner}
149
+ width="16"
150
+ height="16"
151
+ viewBox="0 0 24 24"
152
+ fill="none"
153
+ stroke="currentColor"
154
+ >
155
+ <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" />
156
+ </svg>
157
+ ) : hasConfiguration ? (
158
+ <svg
159
+ width="16"
160
+ height="16"
161
+ viewBox="0 0 24 24"
162
+ fill="none"
163
+ stroke="currentColor"
164
+ strokeWidth="2"
165
+ >
166
+ <path d="M12 5v14M5 12h14" />
167
+ </svg>
168
+ ) : (
169
+ <svg
170
+ width="16"
171
+ height="16"
172
+ viewBox="0 0 24 24"
173
+ fill="none"
174
+ stroke="currentColor"
175
+ strokeWidth="2"
176
+ >
177
+ <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" />
178
+ </svg>
179
+ )}
180
+ </button>
139
181
  {isOpen && (
140
182
  <AiPromptPanel
141
183
  isOpen={isOpen}
@@ -143,6 +185,7 @@ export function AiInput({
143
185
  onSubmit={handleSubmit}
144
186
  uiMode={uiMode}
145
187
  models={models || []}
188
+ sourceText={inputValue || undefined}
146
189
  />
147
190
  )}
148
191
  </div>
@@ -1,15 +1,18 @@
1
1
  "use client";
2
2
 
3
- import { useState, type ReactNode } from "react";
3
+ import { useState, useEffect, type ReactNode } from "react";
4
4
  import type { ModelRef } from "@lastbrain/ai-ui-core";
5
5
  import type { UiMode } from "../types";
6
+ import { aiStyles } from "../styles/inline";
7
+ import { usePrompts, type Prompt, type PublicPrompt } from "../hooks/usePrompts";
6
8
 
7
9
  export interface AiPromptPanelProps {
8
10
  isOpen: boolean;
9
11
  onClose: () => void;
10
- onSubmit: (model: string, prompt: string) => void;
12
+ onSubmit: (model: string, prompt: string, promptId?: string) => void;
11
13
  uiMode?: UiMode;
12
14
  models?: ModelRef[];
15
+ sourceText?: string; // Current text from input/textarea
13
16
  children?: (props: AiPromptPanelRenderProps) => ReactNode;
14
17
  }
15
18
 
@@ -19,6 +22,7 @@ export interface AiPromptPanelRenderProps {
19
22
  setSelectedModel: (model: string) => void;
20
23
  prompt: string;
21
24
  setPrompt: (prompt: string) => void;
25
+ sourceText?: string;
22
26
  handleSubmit: () => void;
23
27
  handleClose: () => void;
24
28
  }
@@ -29,89 +33,349 @@ export function AiPromptPanel({
29
33
  onSubmit,
30
34
  uiMode = "modal",
31
35
  models = [],
36
+ sourceText,
32
37
  children,
33
38
  }: AiPromptPanelProps) {
34
- const [selectedModel, setSelectedModel] = useState(models[0]?.id || "");
39
+ const [selectedModel, setSelectedModel] = useState("");
35
40
  const [prompt, setPrompt] = useState("");
41
+ const [promptId, setPromptId] = useState<string | undefined>(undefined);
42
+ const [isCloseHovered, setIsCloseHovered] = useState(false);
43
+ const [isCancelHovered, setIsCancelHovered] = useState(false);
44
+ const [isSubmitHovered, setIsSubmitHovered] = useState(false);
45
+ const [promptFocused, setPromptFocused] = useState(false);
46
+ const [modelFocused, setModelFocused] = useState(false);
47
+ const [showPromptLibrary, setShowPromptLibrary] = useState(false);
48
+
49
+ const { prompts, loading: promptsLoading, fetchPrompts, incrementStat } = usePrompts();
50
+
51
+ // Set initial model when models change
52
+ useEffect(() => {
53
+ if (models.length > 0 && !selectedModel) {
54
+ setSelectedModel(models[0].id);
55
+ }
56
+ }, [models, selectedModel]);
57
+
58
+ // Fetch prompts when modal opens
59
+ useEffect(() => {
60
+ if (isOpen && models.length > 0) {
61
+ const modelType = models.find((m) => m.id === selectedModel)?.type;
62
+ fetchPrompts({
63
+ type: modelType === "image" ? "image" : "text",
64
+ });
65
+ }
66
+ }, [isOpen, selectedModel, models, fetchPrompts]);
36
67
 
37
68
  if (!isOpen) return null;
38
69
 
39
70
  const handleSubmit = () => {
40
- onSubmit(selectedModel, prompt);
71
+ if (!selectedModel || !prompt.trim()) return;
72
+ onSubmit(selectedModel, prompt, promptId);
41
73
  setPrompt("");
74
+ setPromptId(undefined);
42
75
  };
43
76
 
44
77
  const handleClose = () => {
45
78
  onClose();
46
79
  setPrompt("");
80
+ setPromptId(undefined);
81
+ setShowPromptLibrary(false);
82
+ };
83
+
84
+ const handleKeyDown = (e: React.KeyboardEvent) => {
85
+ if (e.key === "Escape") {
86
+ handleClose();
87
+ }
88
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
89
+ handleSubmit();
90
+ }
91
+ };
92
+
93
+ const handleSelectPrompt = (promptData: Prompt | PublicPrompt) => {
94
+ setPrompt(promptData.content);
95
+ setPromptId(promptData.id);
96
+ incrementStat(promptData.id, "picked");
97
+ setShowPromptLibrary(false);
47
98
  };
48
99
 
100
+ const currentModelType = models.find((m) => m.id === selectedModel)?.type;
101
+ const filteredPrompts = prompts.filter((p: Prompt | PublicPrompt) => {
102
+ const matchesType =
103
+ currentModelType === "image" ? p.type === "image" : p.type !== "image";
104
+ return matchesType;
105
+ });
106
+
49
107
  const renderProps: AiPromptPanelRenderProps = {
50
108
  models,
51
109
  selectedModel,
52
110
  setSelectedModel,
53
111
  prompt,
54
112
  setPrompt,
113
+ sourceText,
55
114
  handleSubmit,
56
115
  handleClose,
57
116
  };
58
117
 
59
118
  if (children) {
60
119
  return (
61
- <div data-ai-prompt-panel data-type={uiMode}>
120
+ <div style={aiStyles.modal} onKeyDown={handleKeyDown}>
62
121
  {children(renderProps)}
63
122
  </div>
64
123
  );
65
124
  }
66
125
 
67
126
  return (
68
- <div data-ai-prompt-panel data-type={uiMode}>
69
- <div data-ai-prompt-panel-overlay onClick={handleClose} />
70
- <div data-ai-prompt-panel-content>
71
- <div data-ai-prompt-panel-header>
72
- <h2>AI Prompt</h2>
73
- <button onClick={handleClose} data-ai-close-button>
127
+ <div style={aiStyles.modal} onKeyDown={handleKeyDown}>
128
+ <div style={aiStyles.modalOverlay} onClick={handleClose} />
129
+ <div style={aiStyles.modalContent}>
130
+ <div style={aiStyles.modalHeader}>
131
+ <h2 style={aiStyles.modalTitle}>
132
+ {showPromptLibrary ? "Select a Prompt" : "AI Prompt Configuration"}
133
+ </h2>
134
+ <button
135
+ style={{
136
+ ...aiStyles.modalCloseButton,
137
+ ...(isCloseHovered && aiStyles.modalCloseButtonHover),
138
+ }}
139
+ onClick={handleClose}
140
+ onMouseEnter={() => setIsCloseHovered(true)}
141
+ onMouseLeave={() => setIsCloseHovered(false)}
142
+ aria-label="Close"
143
+ >
74
144
  ×
75
145
  </button>
76
146
  </div>
77
- <div data-ai-prompt-panel-body>
78
- <div data-ai-model-select-wrapper>
79
- <label htmlFor="model-select">Model</label>
80
- <select
81
- id="model-select"
82
- value={selectedModel}
83
- onChange={(e) => setSelectedModel(e.target.value)}
84
- data-ai-model-select
85
- >
86
- {models.map((model) => (
87
- <option key={model.id} value={model.id}>
88
- {model.name}
89
- </option>
90
- ))}
91
- </select>
92
- </div>
93
- <div data-ai-prompt-wrapper>
94
- <label htmlFor="prompt-input">Prompt</label>
95
- <textarea
96
- id="prompt-input"
97
- value={prompt}
98
- onChange={(e) => setPrompt(e.target.value)}
99
- placeholder="Enter your prompt..."
100
- rows={5}
101
- data-ai-prompt-input
102
- />
103
- </div>
147
+
148
+ <div style={aiStyles.modalBody}>
149
+ {!showPromptLibrary ? (
150
+ <>
151
+ {sourceText && (
152
+ <div style={{
153
+ ...aiStyles.modalInputGroup,
154
+ marginBottom: "16px",
155
+ }}>
156
+ <label style={aiStyles.modalLabel}>
157
+ Source Text
158
+ </label>
159
+ <div style={{
160
+ padding: "12px",
161
+ background: aiStyles.textarea.background,
162
+ border: `1px solid ${aiStyles.input.border}`,
163
+ borderRadius: "8px",
164
+ fontSize: "13px",
165
+ color: aiStyles.textarea.color,
166
+ maxHeight: "120px",
167
+ overflow: "auto",
168
+ whiteSpace: "pre-wrap",
169
+ wordBreak: "break-word",
170
+ }}>
171
+ {sourceText}
172
+ </div>
173
+ </div>
174
+ )}
175
+
176
+ <div style={aiStyles.modalInputGroup}>
177
+ <label htmlFor="model-select" style={aiStyles.modalLabel}>
178
+ AI Model
179
+ </label>
180
+ <select
181
+ id="model-select"
182
+ value={selectedModel}
183
+ onChange={(e) => setSelectedModel(e.target.value)}
184
+ onFocus={() => setModelFocused(true)}
185
+ onBlur={() => setModelFocused(false)}
186
+ style={{
187
+ ...aiStyles.select,
188
+ ...(modelFocused && aiStyles.selectFocus),
189
+ }}
190
+ >
191
+ {models.length === 0 && (
192
+ <option value="">Loading models...</option>
193
+ )}
194
+ {models.map((model) => (
195
+ <option key={model.id} value={model.id}>
196
+ {model.name}
197
+ </option>
198
+ ))}
199
+ </select>
200
+ </div>
201
+
202
+ <div style={aiStyles.modalInputGroup}>
203
+ <div style={{
204
+ display: "flex",
205
+ justifyContent: "space-between",
206
+ alignItems: "center",
207
+ marginBottom: "8px",
208
+ }}>
209
+ <label htmlFor="prompt-input" style={aiStyles.modalLabel}>
210
+ Prompt
211
+ <span style={{
212
+ color: "#6b7280",
213
+ marginLeft: "4px",
214
+ fontSize: "12px",
215
+ fontWeight: 400,
216
+ }}>
217
+ (Cmd/Ctrl + Enter to submit)
218
+ </span>
219
+ </label>
220
+ {filteredPrompts.length > 0 && (
221
+ <button
222
+ onClick={() => setShowPromptLibrary(true)}
223
+ style={{
224
+ padding: "4px 12px",
225
+ fontSize: "12px",
226
+ color: "#ffffff",
227
+ background: "#8b5cf620",
228
+ border: "none",
229
+ borderRadius: "6px",
230
+ cursor: "pointer",
231
+ transition: "all 0.2s",
232
+ }}
233
+ onMouseEnter={(e) => {
234
+ e.currentTarget.style.background = "#8b5cf630";
235
+ }}
236
+ onMouseLeave={(e) => {
237
+ e.currentTarget.style.background = "#8b5cf620";
238
+ }}
239
+ >
240
+ 📚 Browse Prompts ({filteredPrompts.length})
241
+ </button>
242
+ )}
243
+ </div>
244
+ <textarea
245
+ id="prompt-input"
246
+ value={prompt}
247
+ onChange={(e) => setPrompt(e.target.value)}
248
+ onFocus={() => setPromptFocused(true)}
249
+ onBlur={() => setPromptFocused(false)}
250
+ placeholder={sourceText
251
+ ? "Enter your AI prompt... e.g., 'Correct spelling and grammar', 'Make it more professional', 'Translate to English'"
252
+ : "Enter your AI prompt... e.g., 'Write a blog post about AI', 'Generate product description'"
253
+ }
254
+ rows={6}
255
+ style={{
256
+ ...aiStyles.textarea,
257
+ padding: "12px 16px",
258
+ ...(promptFocused && aiStyles.textareaFocus),
259
+ }}
260
+ />
261
+ </div>
262
+ </>
263
+ ) : (
264
+ <div>
265
+ <button
266
+ onClick={() => setShowPromptLibrary(false)}
267
+ style={{
268
+ padding: "8px 0",
269
+ fontSize: "14px",
270
+ color: "#8b5cf6",
271
+ background: "transparent",
272
+ border: "none",
273
+ cursor: "pointer",
274
+ marginBottom: "16px",
275
+ display: "flex",
276
+ alignItems: "center",
277
+ gap: "4px",
278
+ }}
279
+ >
280
+ ← Back to form
281
+ </button>
282
+
283
+ {promptsLoading ? (
284
+ <div style={{ textAlign: "center", padding: "40px 0" }}>
285
+ Loading prompts...
286
+ </div>
287
+ ) : filteredPrompts.length === 0 ? (
288
+ <div style={{ textAlign: "center", padding: "40px 0", color: "#6b7280" }}>
289
+ No prompts available for this model type
290
+ </div>
291
+ ) : (
292
+ <div style={{
293
+ display: "flex",
294
+ flexDirection: "column" as const,
295
+ gap: "12px",
296
+ maxHeight: "400px",
297
+ overflow: "auto",
298
+ }}>
299
+ {filteredPrompts.map((promptData: Prompt | PublicPrompt) => (
300
+ <div
301
+ key={promptData.id}
302
+ onClick={() => handleSelectPrompt(promptData)}
303
+ style={{
304
+ padding: "16px",
305
+ border: `1px solid #e5e7eb`,
306
+ borderRadius: "8px",
307
+ cursor: "pointer",
308
+ transition: "all 0.2s",
309
+ }}
310
+ onMouseEnter={(e) => {
311
+ e.currentTarget.style.background = "#8b5cf610";
312
+ e.currentTarget.style.borderColor = "#8b5cf6";
313
+ }}
314
+ onMouseLeave={(e) => {
315
+ e.currentTarget.style.background = "transparent";
316
+ e.currentTarget.style.borderColor = "#e5e7eb";
317
+ }}
318
+ >
319
+ <div style={{
320
+ fontWeight: 600,
321
+ marginBottom: "4px",
322
+ color: "#111827",
323
+ }}>
324
+ {promptData.title}
325
+ </div>
326
+ <div style={{
327
+ fontSize: "13px",
328
+ color: "#6b7280",
329
+ overflow: "hidden",
330
+ textOverflow: "ellipsis",
331
+ display: "-webkit-box",
332
+ WebkitLineClamp: 2,
333
+ WebkitBoxOrient: "vertical" as any,
334
+ }}>
335
+ {promptData.content}
336
+ </div>
337
+ {("category" in promptData && promptData.category) ? (
338
+ <div style={{
339
+ marginTop: "8px",
340
+ fontSize: "11px",
341
+ color: "#8b5cf6",
342
+ }}>
343
+ {String(promptData.category)}
344
+ </div>
345
+ ) : null}
346
+ </div>
347
+ ))}
348
+ </div>
349
+ )}
350
+ </div>
351
+ )}
104
352
  </div>
105
- <div data-ai-prompt-panel-footer>
106
- <button onClick={handleClose} data-ai-cancel-button>
353
+
354
+ <div style={aiStyles.modalFooter}>
355
+ <button
356
+ onClick={handleClose}
357
+ onMouseEnter={() => setIsCancelHovered(true)}
358
+ onMouseLeave={() => setIsCancelHovered(false)}
359
+ style={{
360
+ ...aiStyles.button,
361
+ ...aiStyles.buttonSecondary,
362
+ ...(isCancelHovered && aiStyles.buttonSecondaryHover),
363
+ }}
364
+ >
107
365
  Cancel
108
366
  </button>
109
367
  <button
110
368
  onClick={handleSubmit}
111
- disabled={!selectedModel || !prompt}
112
- data-ai-submit-button
369
+ disabled={!selectedModel || !prompt.trim()}
370
+ onMouseEnter={() => setIsSubmitHovered(true)}
371
+ onMouseLeave={() => setIsSubmitHovered(false)}
372
+ style={{
373
+ ...aiStyles.button,
374
+ ...(isSubmitHovered && !(!selectedModel || !prompt.trim()) && aiStyles.buttonHover),
375
+ ...(!selectedModel || !prompt.trim() ? aiStyles.buttonDisabled : {}),
376
+ }}
113
377
  >
114
- Generate
378
+ {sourceText ? "Transform with AI" : "Generate with AI"}
115
379
  </button>
116
380
  </div>
117
381
  </div>
@@ -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 AiSelectProps
10
11
  extends
@@ -29,6 +30,7 @@ export function AiSelect({
29
30
  ...selectProps
30
31
  }: AiSelectProps) {
31
32
  const [isOpen, setIsOpen] = useState(false);
33
+ const [isFocused, setIsFocused] = useState(false);
32
34
 
33
35
  const { models } = useAiModels({ baseUrl, apiKeyId });
34
36
  const { generateText, loading } = useAiCallText({ baseUrl, apiKeyId });
@@ -65,18 +67,25 @@ export function AiSelect({
65
67
  };
66
68
 
67
69
  return (
68
- <div data-ai-select-wrapper className={className}>
69
- <select {...selectProps} disabled={disabled || loading} data-ai-select>
70
- {children}
71
- </select>
72
- <button
73
- onClick={handleOpenPanel}
70
+ <div style={{ width: "100%" }} className={className}>
71
+ <select
72
+ {...selectProps}
73
+ style={{
74
+ ...aiStyles.select,
75
+ ...(isFocused && aiStyles.selectFocus),
76
+ }}
77
+ onFocus={(e) => {
78
+ setIsFocused(true);
79
+ selectProps.onFocus?.(e);
80
+ }}
81
+ onBlur={(e) => {
82
+ setIsFocused(false);
83
+ selectProps.onBlur?.(e);
84
+ }}
74
85
  disabled={disabled || loading}
75
- data-ai-assist-button
76
- type="button"
77
86
  >
78
- AI Assist
79
- </button>
87
+ {children}
88
+ </select>
80
89
  {isOpen && (
81
90
  <AiPromptPanel
82
91
  isOpen={isOpen}