@lastbrain/ai-ui-react 1.0.7 → 1.0.9

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 });
@@ -54,16 +57,14 @@ export function AiInput({
54
57
  const result = await generateText({
55
58
  model: selectedModel,
56
59
  prompt: selectedPrompt,
57
- context: context || inputValue || undefined,
60
+ context: inputValue || context || undefined,
58
61
  actionType: "autocomplete",
59
62
  });
60
63
 
61
64
  if (result.text) {
62
- if (editMode) {
63
- setInputValue(result.text);
64
- if (inputRef.current) {
65
- inputRef.current.value = result.text;
66
- }
65
+ setInputValue(result.text);
66
+ if (inputRef.current) {
67
+ inputRef.current.value = result.text;
67
68
  }
68
69
  onValue?.(result.text);
69
70
  onToast?.({ type: "success", message: "AI generation successful" });
@@ -82,16 +83,14 @@ export function AiInput({
82
83
  const result = await generateText({
83
84
  model,
84
85
  prompt,
85
- context: context || inputValue || undefined,
86
+ context: inputValue || context || undefined,
86
87
  actionType: "autocomplete",
87
88
  });
88
89
 
89
90
  if (result.text) {
90
- if (editMode) {
91
- setInputValue(result.text);
92
- if (inputRef.current) {
93
- inputRef.current.value = result.text;
94
- }
91
+ setInputValue(result.text);
92
+ if (inputRef.current) {
93
+ inputRef.current.value = result.text;
95
94
  }
96
95
  onValue?.(result.text);
97
96
  onToast?.({ type: "success", message: "AI generation successful" });
@@ -108,34 +107,76 @@ export function AiInput({
108
107
  };
109
108
 
110
109
  return (
111
- <div data-ai-input-wrapper className={className}>
110
+ <div style={aiStyles.inputWrapper} className={className}>
112
111
  <input
113
112
  ref={inputRef}
114
113
  {...inputProps}
114
+ style={{
115
+ ...aiStyles.input,
116
+ ...(isFocused && aiStyles.inputFocus),
117
+ }}
115
118
  value={inputValue}
116
119
  onChange={handleInputChange}
120
+ onFocus={(e) => {
121
+ setIsFocused(true);
122
+ inputProps.onFocus?.(e);
123
+ }}
124
+ onBlur={(e) => {
125
+ setIsFocused(false);
126
+ inputProps.onBlur?.(e);
127
+ }}
117
128
  disabled={disabled || loading}
118
- data-ai-input
119
129
  />
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
- )}
130
+ <button
131
+ style={{
132
+ ...aiStyles.inputAiButton,
133
+ ...(isButtonHovered && aiStyles.inputAiButtonHover),
134
+ ...(disabled || loading
135
+ ? { opacity: 0.5, cursor: "not-allowed" }
136
+ : {}),
137
+ }}
138
+ onClick={hasConfiguration ? handleQuickGenerate : handleOpenPanel}
139
+ onMouseEnter={() => setIsButtonHovered(true)}
140
+ onMouseLeave={() => setIsButtonHovered(false)}
141
+ disabled={disabled || loading}
142
+ type="button"
143
+ title={hasConfiguration ? "Generate with AI" : "Setup AI"}
144
+ >
145
+ {loading ? (
146
+ <svg
147
+ style={aiStyles.spinner}
148
+ width="16"
149
+ height="16"
150
+ viewBox="0 0 24 24"
151
+ fill="none"
152
+ stroke="currentColor"
153
+ >
154
+ <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" />
155
+ </svg>
156
+ ) : hasConfiguration ? (
157
+ <svg
158
+ width="16"
159
+ height="16"
160
+ viewBox="0 0 24 24"
161
+ fill="none"
162
+ stroke="currentColor"
163
+ strokeWidth="2"
164
+ >
165
+ <path d="M12 5v14M5 12h14" />
166
+ </svg>
167
+ ) : (
168
+ <svg
169
+ width="16"
170
+ height="16"
171
+ viewBox="0 0 24 24"
172
+ fill="none"
173
+ stroke="currentColor"
174
+ strokeWidth="2"
175
+ >
176
+ <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" />
177
+ </svg>
178
+ )}
179
+ </button>
139
180
  {isOpen && (
140
181
  <AiPromptPanel
141
182
  isOpen={isOpen}
@@ -1,8 +1,9 @@
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";
6
7
 
7
8
  export interface AiPromptPanelProps {
8
9
  isOpen: boolean;
@@ -33,10 +34,22 @@ export function AiPromptPanel({
33
34
  }: AiPromptPanelProps) {
34
35
  const [selectedModel, setSelectedModel] = useState(models[0]?.id || "");
35
36
  const [prompt, setPrompt] = useState("");
37
+ const [isCloseHovered, setIsCloseHovered] = useState(false);
38
+ const [isCancelHovered, setIsCancelHovered] = useState(false);
39
+ const [isSubmitHovered, setIsSubmitHovered] = useState(false);
40
+ const [promptFocused, setPromptFocused] = useState(false);
41
+ const [modelFocused, setModelFocused] = useState(false);
42
+
43
+ useEffect(() => {
44
+ if (models.length > 0 && !selectedModel) {
45
+ setSelectedModel(models[0].id);
46
+ }
47
+ }, [models, selectedModel]);
36
48
 
37
49
  if (!isOpen) return null;
38
50
 
39
51
  const handleSubmit = () => {
52
+ if (!selectedModel || !prompt.trim()) return;
40
53
  onSubmit(selectedModel, prompt);
41
54
  setPrompt("");
42
55
  };
@@ -46,6 +59,15 @@ export function AiPromptPanel({
46
59
  setPrompt("");
47
60
  };
48
61
 
62
+ const handleKeyDown = (e: React.KeyboardEvent) => {
63
+ if (e.key === "Escape") {
64
+ handleClose();
65
+ }
66
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
67
+ handleSubmit();
68
+ }
69
+ };
70
+
49
71
  const renderProps: AiPromptPanelRenderProps = {
50
72
  models,
51
73
  selectedModel,
@@ -58,31 +80,51 @@ export function AiPromptPanel({
58
80
 
59
81
  if (children) {
60
82
  return (
61
- <div data-ai-prompt-panel data-type={uiMode}>
83
+ <div style={aiStyles.modal} onKeyDown={handleKeyDown}>
62
84
  {children(renderProps)}
63
85
  </div>
64
86
  );
65
87
  }
66
88
 
67
89
  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>
90
+ <div style={aiStyles.modal} onKeyDown={handleKeyDown}>
91
+ <div style={aiStyles.modalOverlay} onClick={handleClose} />
92
+ <div style={aiStyles.modalContent}>
93
+ <div style={aiStyles.modalHeader}>
94
+ <h2 style={aiStyles.modalTitle}>AI Prompt Configuration</h2>
95
+ <button
96
+ style={{
97
+ ...aiStyles.modalCloseButton,
98
+ ...(isCloseHovered && aiStyles.modalCloseButtonHover),
99
+ }}
100
+ onClick={handleClose}
101
+ onMouseEnter={() => setIsCloseHovered(true)}
102
+ onMouseLeave={() => setIsCloseHovered(false)}
103
+ aria-label="Close"
104
+ >
74
105
  ×
75
106
  </button>
76
107
  </div>
77
- <div data-ai-prompt-panel-body>
78
- <div data-ai-model-select-wrapper>
79
- <label htmlFor="model-select">Model</label>
108
+
109
+ <div style={aiStyles.modalBody}>
110
+ <div style={aiStyles.modalInputGroup}>
111
+ <label htmlFor="model-select" style={aiStyles.modalLabel}>
112
+ AI Model
113
+ </label>
80
114
  <select
81
115
  id="model-select"
82
116
  value={selectedModel}
83
117
  onChange={(e) => setSelectedModel(e.target.value)}
84
- data-ai-model-select
118
+ onFocus={() => setModelFocused(true)}
119
+ onBlur={() => setModelFocused(false)}
120
+ style={{
121
+ ...aiStyles.select,
122
+ ...(modelFocused && aiStyles.selectFocus),
123
+ }}
85
124
  >
125
+ {models.length === 0 && (
126
+ <option value="">No models available</option>
127
+ )}
86
128
  {models.map((model) => (
87
129
  <option key={model.id} value={model.id}>
88
130
  {model.name}
@@ -90,28 +132,56 @@ export function AiPromptPanel({
90
132
  ))}
91
133
  </select>
92
134
  </div>
93
- <div data-ai-prompt-wrapper>
94
- <label htmlFor="prompt-input">Prompt</label>
135
+
136
+ <div style={aiStyles.modalInputGroup}>
137
+ <label htmlFor="prompt-input" style={aiStyles.modalLabel}>
138
+ Prompt
139
+ <span style={{ color: aiStyles.textareaFocus.borderColor, marginLeft: "4px" }}>
140
+ (Cmd/Ctrl + Enter to submit)
141
+ </span>
142
+ </label>
95
143
  <textarea
96
144
  id="prompt-input"
97
145
  value={prompt}
98
146
  onChange={(e) => setPrompt(e.target.value)}
99
- placeholder="Enter your prompt..."
100
- rows={5}
101
- data-ai-prompt-input
147
+ onFocus={() => setPromptFocused(true)}
148
+ onBlur={() => setPromptFocused(false)}
149
+ placeholder="Enter your AI prompt... e.g., 'Correct spelling and grammar'"
150
+ rows={6}
151
+ style={{
152
+ ...aiStyles.textarea,
153
+ padding: "12px 16px",
154
+ ...(promptFocused && aiStyles.textareaFocus),
155
+ }}
102
156
  />
103
157
  </div>
104
158
  </div>
105
- <div data-ai-prompt-panel-footer>
106
- <button onClick={handleClose} data-ai-cancel-button>
159
+
160
+ <div style={aiStyles.modalFooter}>
161
+ <button
162
+ onClick={handleClose}
163
+ onMouseEnter={() => setIsCancelHovered(true)}
164
+ onMouseLeave={() => setIsCancelHovered(false)}
165
+ style={{
166
+ ...aiStyles.button,
167
+ ...aiStyles.buttonSecondary,
168
+ ...(isCancelHovered && aiStyles.buttonSecondaryHover),
169
+ }}
170
+ >
107
171
  Cancel
108
172
  </button>
109
173
  <button
110
174
  onClick={handleSubmit}
111
- disabled={!selectedModel || !prompt}
112
- data-ai-submit-button
175
+ disabled={!selectedModel || !prompt.trim()}
176
+ onMouseEnter={() => setIsSubmitHovered(true)}
177
+ onMouseLeave={() => setIsSubmitHovered(false)}
178
+ style={{
179
+ ...aiStyles.button,
180
+ ...(isSubmitHovered && !(!selectedModel || !prompt.trim()) && aiStyles.buttonHover),
181
+ ...(!selectedModel || !prompt.trim() ? aiStyles.buttonDisabled : {}),
182
+ }}
113
183
  >
114
- Generate
184
+ Generate with AI
115
185
  </button>
116
186
  </div>
117
187
  </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}