@lastbrain/ai-ui-react 1.0.3
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 +84 -0
- package/dist/components/AiChipLabel.d.ts +8 -0
- package/dist/components/AiChipLabel.d.ts.map +1 -0
- package/dist/components/AiChipLabel.js +39 -0
- package/dist/components/AiImageButton.d.ts +8 -0
- package/dist/components/AiImageButton.d.ts.map +1 -0
- package/dist/components/AiImageButton.js +36 -0
- package/dist/components/AiInput.d.ts +7 -0
- package/dist/components/AiInput.d.ts.map +1 -0
- package/dist/components/AiInput.js +77 -0
- package/dist/components/AiModelSelect.d.ts +10 -0
- package/dist/components/AiModelSelect.d.ts.map +1 -0
- package/dist/components/AiModelSelect.js +5 -0
- package/dist/components/AiPromptPanel.d.ts +22 -0
- package/dist/components/AiPromptPanel.d.ts.map +1 -0
- package/dist/components/AiPromptPanel.js +30 -0
- package/dist/components/AiSelect.d.ts +8 -0
- package/dist/components/AiSelect.d.ts.map +1 -0
- package/dist/components/AiSelect.js +38 -0
- package/dist/components/AiSettingsButton.d.ts +11 -0
- package/dist/components/AiSettingsButton.d.ts.map +1 -0
- package/dist/components/AiSettingsButton.js +16 -0
- package/dist/components/AiTextarea.d.ts +7 -0
- package/dist/components/AiTextarea.d.ts.map +1 -0
- package/dist/components/AiTextarea.js +79 -0
- package/dist/context/AiProvider.d.ts +18 -0
- package/dist/context/AiProvider.d.ts.map +1 -0
- package/dist/context/AiProvider.js +20 -0
- package/dist/hooks/useAiCallImage.d.ts +12 -0
- package/dist/hooks/useAiCallImage.d.ts.map +1 -0
- package/dist/hooks/useAiCallImage.js +29 -0
- package/dist/hooks/useAiCallText.d.ts +12 -0
- package/dist/hooks/useAiCallText.d.ts.map +1 -0
- package/dist/hooks/useAiCallText.js +29 -0
- package/dist/hooks/useAiClient.d.ts +12 -0
- package/dist/hooks/useAiClient.d.ts.map +1 -0
- package/dist/hooks/useAiClient.js +13 -0
- package/dist/hooks/useAiModels.d.ts +13 -0
- package/dist/hooks/useAiModels.d.ts.map +1 -0
- package/dist/hooks/useAiModels.js +32 -0
- package/dist/hooks/useAiStatus.d.ts +13 -0
- package/dist/hooks/useAiStatus.d.ts.map +1 -0
- package/dist/hooks/useAiStatus.js +32 -0
- package/dist/hooks/usePrompts.d.ts +35 -0
- package/dist/hooks/usePrompts.d.ts.map +1 -0
- package/dist/hooks/usePrompts.js +125 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/types.d.ts +20 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +60 -0
- package/src/components/AiChipLabel.tsx +88 -0
- package/src/components/AiImageButton.tsx +87 -0
- package/src/components/AiInput.tsx +150 -0
- package/src/components/AiModelSelect.tsx +37 -0
- package/src/components/AiPromptPanel.tsx +120 -0
- package/src/components/AiSelect.tsx +91 -0
- package/src/components/AiSettingsButton.tsx +111 -0
- package/src/components/AiTextarea.tsx +152 -0
- package/src/context/AiProvider.tsx +44 -0
- package/src/hooks/useAiCallImage.ts +49 -0
- package/src/hooks/useAiCallText.ts +49 -0
- package/src/hooks/useAiClient.ts +23 -0
- package/src/hooks/useAiModels.ts +50 -0
- package/src/hooks/useAiStatus.ts +50 -0
- package/src/hooks/usePrompts.ts +187 -0
- package/src/index.ts +23 -0
- package/src/types.ts +20 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, useRef, type InputHTMLAttributes } from "react";
|
|
4
|
+
import type { BaseAiProps } from "../types";
|
|
5
|
+
import { useAiCallText } from "../hooks/useAiCallText";
|
|
6
|
+
import { useAiModels } from "../hooks/useAiModels";
|
|
7
|
+
import { AiPromptPanel } from "./AiPromptPanel";
|
|
8
|
+
|
|
9
|
+
export interface AiInputProps
|
|
10
|
+
extends
|
|
11
|
+
Omit<BaseAiProps, "type">,
|
|
12
|
+
Omit<InputHTMLAttributes<HTMLInputElement>, "onValue"> {
|
|
13
|
+
uiMode?: "modal" | "drawer";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function AiInput({
|
|
17
|
+
baseUrl,
|
|
18
|
+
apiKeyId,
|
|
19
|
+
uiMode = "modal",
|
|
20
|
+
context,
|
|
21
|
+
model,
|
|
22
|
+
prompt,
|
|
23
|
+
editMode = false,
|
|
24
|
+
onValue,
|
|
25
|
+
onToast,
|
|
26
|
+
disabled,
|
|
27
|
+
className,
|
|
28
|
+
...inputProps
|
|
29
|
+
}: AiInputProps) {
|
|
30
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
31
|
+
const [inputValue, setInputValue] = useState(
|
|
32
|
+
inputProps.value?.toString() || inputProps.defaultValue?.toString() || ""
|
|
33
|
+
);
|
|
34
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
35
|
+
|
|
36
|
+
const { models } = useAiModels({ baseUrl, apiKeyId });
|
|
37
|
+
const { generateText, loading } = useAiCallText({ baseUrl, apiKeyId });
|
|
38
|
+
|
|
39
|
+
const hasConfiguration = Boolean(model && prompt);
|
|
40
|
+
|
|
41
|
+
const handleOpenPanel = () => {
|
|
42
|
+
setIsOpen(true);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleClosePanel = () => {
|
|
46
|
+
setIsOpen(false);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const handleSubmit = async (
|
|
50
|
+
selectedModel: string,
|
|
51
|
+
selectedPrompt: string
|
|
52
|
+
) => {
|
|
53
|
+
try {
|
|
54
|
+
const result = await generateText({
|
|
55
|
+
model: selectedModel,
|
|
56
|
+
prompt: selectedPrompt,
|
|
57
|
+
context: context || inputValue || undefined,
|
|
58
|
+
actionType: "autocomplete",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (result.text) {
|
|
62
|
+
if (editMode) {
|
|
63
|
+
setInputValue(result.text);
|
|
64
|
+
if (inputRef.current) {
|
|
65
|
+
inputRef.current.value = result.text;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
onValue?.(result.text);
|
|
69
|
+
onToast?.({ type: "success", message: "AI generation successful" });
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
onToast?.({ type: "error", message: "Failed to generate text" });
|
|
73
|
+
} finally {
|
|
74
|
+
setIsOpen(false);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const handleQuickGenerate = async () => {
|
|
79
|
+
if (!model || !prompt) return;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const result = await generateText({
|
|
83
|
+
model,
|
|
84
|
+
prompt,
|
|
85
|
+
context: context || inputValue || undefined,
|
|
86
|
+
actionType: "autocomplete",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (result.text) {
|
|
90
|
+
if (editMode) {
|
|
91
|
+
setInputValue(result.text);
|
|
92
|
+
if (inputRef.current) {
|
|
93
|
+
inputRef.current.value = result.text;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
onValue?.(result.text);
|
|
97
|
+
onToast?.({ type: "success", message: "AI generation successful" });
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
onToast?.({ type: "error", message: "Failed to generate text" });
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
105
|
+
const newValue = e.target.value;
|
|
106
|
+
setInputValue(newValue);
|
|
107
|
+
inputProps.onChange?.(e);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div data-ai-input-wrapper className={className}>
|
|
112
|
+
<input
|
|
113
|
+
ref={inputRef}
|
|
114
|
+
{...inputProps}
|
|
115
|
+
value={inputValue}
|
|
116
|
+
onChange={handleInputChange}
|
|
117
|
+
disabled={disabled || loading}
|
|
118
|
+
data-ai-input
|
|
119
|
+
/>
|
|
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
|
+
)}
|
|
139
|
+
{isOpen && (
|
|
140
|
+
<AiPromptPanel
|
|
141
|
+
isOpen={isOpen}
|
|
142
|
+
onClose={handleClosePanel}
|
|
143
|
+
onSubmit={handleSubmit}
|
|
144
|
+
uiMode={uiMode}
|
|
145
|
+
models={models || []}
|
|
146
|
+
/>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import type { ModelRef } from "@lastbrain/ai-ui-core";
|
|
5
|
+
|
|
6
|
+
export interface AiModelSelectProps {
|
|
7
|
+
models: ModelRef[];
|
|
8
|
+
value: string;
|
|
9
|
+
onChange: (value: string) => void;
|
|
10
|
+
className?: string;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function AiModelSelect({
|
|
15
|
+
models,
|
|
16
|
+
value,
|
|
17
|
+
onChange,
|
|
18
|
+
className,
|
|
19
|
+
disabled,
|
|
20
|
+
}: AiModelSelectProps) {
|
|
21
|
+
return (
|
|
22
|
+
<select
|
|
23
|
+
value={value}
|
|
24
|
+
onChange={(e) => onChange(e.target.value)}
|
|
25
|
+
className={className}
|
|
26
|
+
disabled={disabled}
|
|
27
|
+
data-ai-model-select
|
|
28
|
+
>
|
|
29
|
+
<option value="">Select a model</option>
|
|
30
|
+
{models.map((model) => (
|
|
31
|
+
<option key={model.id} value={model.id}>
|
|
32
|
+
{model.name}
|
|
33
|
+
</option>
|
|
34
|
+
))}
|
|
35
|
+
</select>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, type ReactNode } from "react";
|
|
4
|
+
import type { ModelRef } from "@lastbrain/ai-ui-core";
|
|
5
|
+
import type { UiMode } from "../types";
|
|
6
|
+
|
|
7
|
+
export interface AiPromptPanelProps {
|
|
8
|
+
isOpen: boolean;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
onSubmit: (model: string, prompt: string) => void;
|
|
11
|
+
uiMode?: UiMode;
|
|
12
|
+
models?: ModelRef[];
|
|
13
|
+
children?: (props: AiPromptPanelRenderProps) => ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AiPromptPanelRenderProps {
|
|
17
|
+
models?: ModelRef[];
|
|
18
|
+
selectedModel: string;
|
|
19
|
+
setSelectedModel: (model: string) => void;
|
|
20
|
+
prompt: string;
|
|
21
|
+
setPrompt: (prompt: string) => void;
|
|
22
|
+
handleSubmit: () => void;
|
|
23
|
+
handleClose: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function AiPromptPanel({
|
|
27
|
+
isOpen,
|
|
28
|
+
onClose,
|
|
29
|
+
onSubmit,
|
|
30
|
+
uiMode = "modal",
|
|
31
|
+
models = [],
|
|
32
|
+
children,
|
|
33
|
+
}: AiPromptPanelProps) {
|
|
34
|
+
const [selectedModel, setSelectedModel] = useState(models[0]?.id || "");
|
|
35
|
+
const [prompt, setPrompt] = useState("");
|
|
36
|
+
|
|
37
|
+
if (!isOpen) return null;
|
|
38
|
+
|
|
39
|
+
const handleSubmit = () => {
|
|
40
|
+
onSubmit(selectedModel, prompt);
|
|
41
|
+
setPrompt("");
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleClose = () => {
|
|
45
|
+
onClose();
|
|
46
|
+
setPrompt("");
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const renderProps: AiPromptPanelRenderProps = {
|
|
50
|
+
models,
|
|
51
|
+
selectedModel,
|
|
52
|
+
setSelectedModel,
|
|
53
|
+
prompt,
|
|
54
|
+
setPrompt,
|
|
55
|
+
handleSubmit,
|
|
56
|
+
handleClose,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (children) {
|
|
60
|
+
return (
|
|
61
|
+
<div data-ai-prompt-panel data-type={uiMode}>
|
|
62
|
+
{children(renderProps)}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
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>
|
|
74
|
+
×
|
|
75
|
+
</button>
|
|
76
|
+
</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>
|
|
104
|
+
</div>
|
|
105
|
+
<div data-ai-prompt-panel-footer>
|
|
106
|
+
<button onClick={handleClose} data-ai-cancel-button>
|
|
107
|
+
Cancel
|
|
108
|
+
</button>
|
|
109
|
+
<button
|
|
110
|
+
onClick={handleSubmit}
|
|
111
|
+
disabled={!selectedModel || !prompt}
|
|
112
|
+
data-ai-submit-button
|
|
113
|
+
>
|
|
114
|
+
Generate
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, type SelectHTMLAttributes } from "react";
|
|
4
|
+
import type { BaseAiProps } from "../types";
|
|
5
|
+
import { useAiCallText } from "../hooks/useAiCallText";
|
|
6
|
+
import { useAiModels } from "../hooks/useAiModels";
|
|
7
|
+
import { AiPromptPanel } from "./AiPromptPanel";
|
|
8
|
+
|
|
9
|
+
export interface AiSelectProps
|
|
10
|
+
extends
|
|
11
|
+
Omit<BaseAiProps, "type">,
|
|
12
|
+
Omit<SelectHTMLAttributes<HTMLSelectElement>, "onValue"> {
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
uiMode?: "modal" | "drawer";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function AiSelect({
|
|
18
|
+
baseUrl,
|
|
19
|
+
apiKeyId,
|
|
20
|
+
uiMode = "modal",
|
|
21
|
+
context,
|
|
22
|
+
model,
|
|
23
|
+
prompt,
|
|
24
|
+
onValue,
|
|
25
|
+
onToast,
|
|
26
|
+
disabled,
|
|
27
|
+
className,
|
|
28
|
+
children,
|
|
29
|
+
...selectProps
|
|
30
|
+
}: AiSelectProps) {
|
|
31
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
32
|
+
|
|
33
|
+
const { models } = useAiModels({ baseUrl, apiKeyId });
|
|
34
|
+
const { generateText, loading } = useAiCallText({ baseUrl, apiKeyId });
|
|
35
|
+
|
|
36
|
+
const handleOpenPanel = () => {
|
|
37
|
+
setIsOpen(true);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const handleClosePanel = () => {
|
|
41
|
+
setIsOpen(false);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleSubmit = async (
|
|
45
|
+
selectedModel: string,
|
|
46
|
+
selectedPrompt: string
|
|
47
|
+
) => {
|
|
48
|
+
try {
|
|
49
|
+
const result = await generateText({
|
|
50
|
+
model: selectedModel,
|
|
51
|
+
prompt: selectedPrompt,
|
|
52
|
+
context: context || undefined,
|
|
53
|
+
actionType: "autocomplete",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (result.text) {
|
|
57
|
+
onValue?.(result.text);
|
|
58
|
+
onToast?.({ type: "success", message: "AI suggestion ready" });
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
onToast?.({ type: "error", message: "Failed to generate suggestion" });
|
|
62
|
+
} finally {
|
|
63
|
+
setIsOpen(false);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
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}
|
|
74
|
+
disabled={disabled || loading}
|
|
75
|
+
data-ai-assist-button
|
|
76
|
+
type="button"
|
|
77
|
+
>
|
|
78
|
+
AI Assist
|
|
79
|
+
</button>
|
|
80
|
+
{isOpen && (
|
|
81
|
+
<AiPromptPanel
|
|
82
|
+
isOpen={isOpen}
|
|
83
|
+
onClose={handleClosePanel}
|
|
84
|
+
onSubmit={handleSubmit}
|
|
85
|
+
uiMode={uiMode}
|
|
86
|
+
models={models || []}
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, type ButtonHTMLAttributes } from "react";
|
|
4
|
+
import { useAiStatus } from "../hooks/useAiStatus";
|
|
5
|
+
|
|
6
|
+
export interface AiSettingsButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
apiKeyId?: string;
|
|
9
|
+
onAddTokens?: () => void;
|
|
10
|
+
onAddStorage?: () => void;
|
|
11
|
+
onDashboard?: () => void;
|
|
12
|
+
onDocumentation?: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function AiSettingsButton({
|
|
16
|
+
baseUrl,
|
|
17
|
+
apiKeyId,
|
|
18
|
+
onAddTokens,
|
|
19
|
+
onAddStorage,
|
|
20
|
+
onDashboard,
|
|
21
|
+
onDocumentation,
|
|
22
|
+
className,
|
|
23
|
+
children,
|
|
24
|
+
...buttonProps
|
|
25
|
+
}: AiSettingsButtonProps) {
|
|
26
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
27
|
+
const { status, loading } = useAiStatus({ baseUrl, apiKeyId });
|
|
28
|
+
|
|
29
|
+
const handleToggle = () => {
|
|
30
|
+
setIsOpen(!isOpen);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const handleClose = () => {
|
|
34
|
+
setIsOpen(false);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const hasApiKey = Boolean(apiKeyId);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div data-ai-settings-wrapper>
|
|
41
|
+
<button
|
|
42
|
+
{...buttonProps}
|
|
43
|
+
onClick={handleToggle}
|
|
44
|
+
className={className}
|
|
45
|
+
data-ai-settings-button
|
|
46
|
+
disabled={loading}
|
|
47
|
+
>
|
|
48
|
+
{children || "AI Settings"}
|
|
49
|
+
</button>
|
|
50
|
+
{isOpen && (
|
|
51
|
+
<div data-ai-settings-panel>
|
|
52
|
+
<div data-ai-settings-overlay onClick={handleClose} />
|
|
53
|
+
<div data-ai-settings-content>
|
|
54
|
+
<div data-ai-settings-header>
|
|
55
|
+
<h3>AI Settings</h3>
|
|
56
|
+
<button onClick={handleClose} data-ai-close-button>
|
|
57
|
+
×
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
<div data-ai-settings-body>
|
|
61
|
+
{hasApiKey ? (
|
|
62
|
+
<>
|
|
63
|
+
{status && (
|
|
64
|
+
<div data-ai-status-section>
|
|
65
|
+
<div data-ai-status-item>
|
|
66
|
+
<span>Tokens:</span>
|
|
67
|
+
<span>{status.balance?.total || 0}</span>
|
|
68
|
+
</div>
|
|
69
|
+
{status.storage?.total_mb !== undefined && (
|
|
70
|
+
<div data-ai-status-item>
|
|
71
|
+
<span>Storage:</span>
|
|
72
|
+
<span>{status.storage.total_mb.toFixed(2)} MB</span>
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
<div data-ai-actions-section>
|
|
78
|
+
{onDashboard && (
|
|
79
|
+
<button onClick={onDashboard} data-ai-action-button>
|
|
80
|
+
Dashboard
|
|
81
|
+
</button>
|
|
82
|
+
)}
|
|
83
|
+
{onAddTokens && (
|
|
84
|
+
<button onClick={onAddTokens} data-ai-action-button>
|
|
85
|
+
Add Tokens
|
|
86
|
+
</button>
|
|
87
|
+
)}
|
|
88
|
+
{onAddStorage && (
|
|
89
|
+
<button onClick={onAddStorage} data-ai-action-button>
|
|
90
|
+
Add Storage
|
|
91
|
+
</button>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
</>
|
|
95
|
+
) : (
|
|
96
|
+
<div data-ai-no-key-section>
|
|
97
|
+
<p>No API key configured</p>
|
|
98
|
+
{onDocumentation && (
|
|
99
|
+
<button onClick={onDocumentation} data-ai-action-button>
|
|
100
|
+
View Documentation
|
|
101
|
+
</button>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, useRef, type TextareaHTMLAttributes } from "react";
|
|
4
|
+
import type { BaseAiProps } from "../types";
|
|
5
|
+
import { useAiCallText } from "../hooks/useAiCallText";
|
|
6
|
+
import { useAiModels } from "../hooks/useAiModels";
|
|
7
|
+
import { AiPromptPanel } from "./AiPromptPanel";
|
|
8
|
+
|
|
9
|
+
export interface AiTextareaProps
|
|
10
|
+
extends
|
|
11
|
+
Omit<BaseAiProps, "type">,
|
|
12
|
+
Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "onValue"> {
|
|
13
|
+
uiMode?: "modal" | "drawer";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function AiTextarea({
|
|
17
|
+
baseUrl,
|
|
18
|
+
apiKeyId,
|
|
19
|
+
uiMode = "modal",
|
|
20
|
+
context,
|
|
21
|
+
model,
|
|
22
|
+
prompt,
|
|
23
|
+
editMode = false,
|
|
24
|
+
onValue,
|
|
25
|
+
onToast,
|
|
26
|
+
disabled,
|
|
27
|
+
className,
|
|
28
|
+
...textareaProps
|
|
29
|
+
}: AiTextareaProps) {
|
|
30
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
31
|
+
const [textValue, setTextValue] = useState(
|
|
32
|
+
textareaProps.value?.toString() ||
|
|
33
|
+
textareaProps.defaultValue?.toString() ||
|
|
34
|
+
""
|
|
35
|
+
);
|
|
36
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
37
|
+
|
|
38
|
+
const { models } = useAiModels({ baseUrl, apiKeyId });
|
|
39
|
+
const { generateText, loading } = useAiCallText({ baseUrl, apiKeyId });
|
|
40
|
+
|
|
41
|
+
const hasConfiguration = Boolean(model && prompt);
|
|
42
|
+
|
|
43
|
+
const handleOpenPanel = () => {
|
|
44
|
+
setIsOpen(true);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const handleClosePanel = () => {
|
|
48
|
+
setIsOpen(false);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const handleSubmit = async (
|
|
52
|
+
selectedModel: string,
|
|
53
|
+
selectedPrompt: string
|
|
54
|
+
) => {
|
|
55
|
+
try {
|
|
56
|
+
const result = await generateText({
|
|
57
|
+
model: selectedModel,
|
|
58
|
+
prompt: selectedPrompt,
|
|
59
|
+
context: context || textValue || undefined,
|
|
60
|
+
actionType: "autocomplete",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (result.text) {
|
|
64
|
+
if (editMode) {
|
|
65
|
+
setTextValue(result.text);
|
|
66
|
+
if (textareaRef.current) {
|
|
67
|
+
textareaRef.current.value = result.text;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
onValue?.(result.text);
|
|
71
|
+
onToast?.({ type: "success", message: "AI generation successful" });
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
onToast?.({ type: "error", message: "Failed to generate text" });
|
|
75
|
+
} finally {
|
|
76
|
+
setIsOpen(false);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleQuickGenerate = async () => {
|
|
81
|
+
if (!model || !prompt) return;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const result = await generateText({
|
|
85
|
+
model,
|
|
86
|
+
prompt,
|
|
87
|
+
context: context || textValue || undefined,
|
|
88
|
+
actionType: "autocomplete",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (result.text) {
|
|
92
|
+
if (editMode) {
|
|
93
|
+
setTextValue(result.text);
|
|
94
|
+
if (textareaRef.current) {
|
|
95
|
+
textareaRef.current.value = result.text;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
onValue?.(result.text);
|
|
99
|
+
onToast?.({ type: "success", message: "AI generation successful" });
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
onToast?.({ type: "error", message: "Failed to generate text" });
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
107
|
+
const newValue = e.target.value;
|
|
108
|
+
setTextValue(newValue);
|
|
109
|
+
textareaProps.onChange?.(e);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div data-ai-textarea-wrapper className={className}>
|
|
114
|
+
<textarea
|
|
115
|
+
ref={textareaRef}
|
|
116
|
+
{...textareaProps}
|
|
117
|
+
value={textValue}
|
|
118
|
+
onChange={handleTextareaChange}
|
|
119
|
+
disabled={disabled || loading}
|
|
120
|
+
data-ai-textarea
|
|
121
|
+
/>
|
|
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
|
+
)}
|
|
141
|
+
{isOpen && (
|
|
142
|
+
<AiPromptPanel
|
|
143
|
+
isOpen={isOpen}
|
|
144
|
+
onClose={handleClosePanel}
|
|
145
|
+
onSubmit={handleSubmit}
|
|
146
|
+
uiMode={uiMode}
|
|
147
|
+
models={models || []}
|
|
148
|
+
/>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
4
|
+
import type { UiMode } from "../types";
|
|
5
|
+
|
|
6
|
+
export interface AiContextValue {
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
apiKeyId: string;
|
|
9
|
+
uiMode: UiMode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const AiContext = createContext<AiContextValue | undefined>(undefined);
|
|
13
|
+
|
|
14
|
+
export interface AiProviderProps {
|
|
15
|
+
baseUrl: string;
|
|
16
|
+
apiKeyId: string;
|
|
17
|
+
uiMode?: UiMode;
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function AiProvider({
|
|
22
|
+
baseUrl,
|
|
23
|
+
apiKeyId,
|
|
24
|
+
uiMode = "modal",
|
|
25
|
+
children,
|
|
26
|
+
}: AiProviderProps) {
|
|
27
|
+
const value: AiContextValue = {
|
|
28
|
+
baseUrl,
|
|
29
|
+
apiKeyId,
|
|
30
|
+
uiMode,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return <AiContext.Provider value={value}>{children}</AiContext.Provider>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useAiContext(): AiContextValue {
|
|
37
|
+
const context = useContext(AiContext);
|
|
38
|
+
if (!context) {
|
|
39
|
+
throw new Error("useAiContext must be used within AiProvider");
|
|
40
|
+
}
|
|
41
|
+
return context;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { AiContext };
|