@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.
- package/README.md +16 -2
- package/dist/components/AiChipLabel.d.ts +7 -6
- package/dist/components/AiChipLabel.d.ts.map +1 -1
- package/dist/components/AiChipLabel.js +25 -36
- package/dist/components/AiInput.d.ts.map +1 -1
- package/dist/components/AiInput.js +27 -13
- package/dist/components/AiPromptPanel.d.ts.map +1 -1
- package/dist/components/AiPromptPanel.js +42 -3
- package/dist/components/AiSelect.d.ts.map +1 -1
- package/dist/components/AiSelect.js +12 -1
- package/dist/components/AiStatusButton.d.ts.map +1 -1
- package/dist/components/AiStatusButton.js +70 -4
- package/dist/components/AiTextarea.d.ts.map +1 -1
- package/dist/components/AiTextarea.js +29 -15
- package/dist/styles/inline.d.ts +67 -0
- package/dist/styles/inline.d.ts.map +1 -0
- package/dist/styles/inline.js +514 -0
- package/package.json +1 -1
- package/src/components/AiChipLabel.tsx +38 -76
- package/src/components/AiInput.tsx +74 -33
- package/src/components/AiPromptPanel.tsx +92 -22
- package/src/components/AiSelect.tsx +19 -10
- package/src/components/AiStatusButton.tsx +178 -71
- package/src/components/AiTextarea.tsx +77 -36
- package/src/styles/inline.ts +587 -0
|
@@ -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:
|
|
60
|
+
context: inputValue || context || undefined,
|
|
58
61
|
actionType: "autocomplete",
|
|
59
62
|
});
|
|
60
63
|
|
|
61
64
|
if (result.text) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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:
|
|
86
|
+
context: inputValue || context || undefined,
|
|
86
87
|
actionType: "autocomplete",
|
|
87
88
|
});
|
|
88
89
|
|
|
89
90
|
if (result.text) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
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
|
|
69
|
-
<div
|
|
70
|
-
<div
|
|
71
|
-
<div
|
|
72
|
-
<h2>AI Prompt</h2>
|
|
73
|
-
<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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
|
69
|
-
<select
|
|
70
|
-
{
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
79
|
-
</
|
|
87
|
+
{children}
|
|
88
|
+
</select>
|
|
80
89
|
{isOpen && (
|
|
81
90
|
<AiPromptPanel
|
|
82
91
|
isOpen={isOpen}
|