Skip to content

Commit 0f9dd22

Browse files
committed
Add loading spinner
1 parent 3f6871c commit 0f9dd22

2 files changed

Lines changed: 66 additions & 39 deletions

File tree

webapp/_webapp/src/views/chat/footer/toolbar/selection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export type SelectionItem<T> = {
1313
disabled?: boolean;
1414
disabledReason?: string;
1515
id?: string;
16-
isCustom: boolean;
16+
isCustom?: boolean;
1717
};
1818

1919
type SelectionProps<T> = {

webapp/_webapp/src/views/settings/sections/api-key-settings.tsx

Lines changed: 65 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ type CustomModelSectionProps = NewCustomModelSectionProps | ExistingCustomModelS
105105
const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModelSectionProps) => {
106106
const id = customModel?.id || "";
107107
const [isEditing, setIsEditing] = useState(isNew);
108+
const [isSaving, setIsSaving] = useState(false);
108109
const [baseUrl, setBaseUrl] = useState(customModel?.baseUrl || "");
109110
const [slug, setSlug] = useState(customModel?.slug ?? "");
110111
const [apiKey, setApiKey] = useState(customModel?.apiKey || "");
@@ -126,6 +127,8 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel
126127
const errorInputClassName = "!border-red-500 focus:!border-red-500";
127128

128129
const handleOnChange = async (isDelete: boolean) => {
130+
if (isSaving) return;
131+
129132
if (
130133
modelName.trim().length < 1 ||
131134
slug.trim().length < 1 ||
@@ -139,32 +142,39 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel
139142
return;
140143
}
141144

142-
await onChange(
143-
{
144-
id: id,
145-
name: modelName.trim(),
146-
baseUrl: baseUrl.trim(),
147-
slug: slug.trim(),
148-
apiKey: apiKey.trim(),
149-
contextWindow: contextWindow,
150-
maxOutput: maxOutput,
151-
inputPrice: inputPrice,
152-
outputPrice: outputPrice,
153-
},
154-
isDelete,
155-
);
145+
const isSaveAction = !isDelete;
146+
if (isSaveAction) setIsSaving(true);
156147

157-
if (isNew) {
158-
setModelName("");
159-
setBaseUrl("");
160-
setSlug("");
161-
setApiKey("");
162-
setContextWindow(0);
163-
setMaxOutput(0);
164-
setInputPrice(0);
165-
setOutputPrice(0);
166-
} else {
167-
setIsEditing(false);
148+
try {
149+
await onChange(
150+
{
151+
id: id,
152+
name: modelName.trim(),
153+
baseUrl: baseUrl.trim(),
154+
slug: slug.trim(),
155+
apiKey: apiKey.trim(),
156+
contextWindow: contextWindow,
157+
maxOutput: maxOutput,
158+
inputPrice: inputPrice,
159+
outputPrice: outputPrice,
160+
},
161+
isDelete,
162+
);
163+
164+
if (isNew) {
165+
setModelName("");
166+
setBaseUrl("");
167+
setSlug("");
168+
setApiKey("");
169+
setContextWindow(0);
170+
setMaxOutput(0);
171+
setInputPrice(0);
172+
setOutputPrice(0);
173+
} else if (isSaveAction) {
174+
setIsEditing(false);
175+
}
176+
} finally {
177+
if (isSaveAction) setIsSaving(false);
168178
}
169179
};
170180

@@ -176,7 +186,7 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel
176186
value={modelName}
177187
placeholder="My Model"
178188
type="text"
179-
disabled={!isEditing}
189+
disabled={!isEditing || isSaving}
180190
onChange={(e) => {
181191
setIsModelNameValid(true);
182192
setModelName(e.target.value);
@@ -185,8 +195,16 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel
185195

186196
{isNew ? (
187197
<Tooltip content="Add" placement="bottom" className="noselect" delay={500}>
188-
<button onClick={() => handleOnChange(false)} className="p-1 hover:bg-default-100 rounded">
189-
<Icon icon="tabler:device-floppy" width="16" />
198+
<button
199+
onClick={() => handleOnChange(false)}
200+
disabled={isSaving}
201+
className="p-1 hover:bg-default-100 rounded disabled:opacity-60"
202+
>
203+
<Icon
204+
icon={isSaving ? "tabler:loader-2" : "tabler:device-floppy"}
205+
width="16"
206+
className={isSaving ? "animate-spin" : ""}
207+
/>
190208
</button>
191209
</Tooltip>
192210
) : (
@@ -200,13 +218,22 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel
200218
setIsEditing(true);
201219
}
202220
}}
203-
className="p-1 hover:bg-default-100 rounded"
221+
disabled={isSaving}
222+
className="p-1 hover:bg-default-100 rounded disabled:opacity-60"
204223
>
205-
<Icon icon={isEditing ? "tabler:device-floppy" : "tabler:pencil"} width="16" />
224+
<Icon
225+
icon={isEditing ? (isSaving ? "tabler:loader-2" : "tabler:device-floppy") : "tabler:pencil"}
226+
width="16"
227+
className={isEditing && isSaving ? "animate-spin" : ""}
228+
/>
206229
</button>
207230
</Tooltip>
208231
<Tooltip content="Delete" placement="bottom" className="noselect" delay={500}>
209-
<button onClick={() => handleOnChange(true)} className="p-1 hover:bg-default-100 rounded">
232+
<button
233+
onClick={() => handleOnChange(true)}
234+
disabled={isSaving}
235+
className="p-1 hover:bg-default-100 rounded disabled:opacity-60"
236+
>
210237
<Icon icon="tabler:trash" width="16" />
211238
</button>
212239
</Tooltip>
@@ -221,7 +248,7 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel
221248
value={slug}
222249
placeholder="e.g., gemini-2.5-flash"
223250
type="text"
224-
disabled={!isEditing}
251+
disabled={!isEditing || isSaving}
225252
onChange={(e) => {
226253
setIsSlugValid(true);
227254
setSlug(e.target.value);
@@ -236,7 +263,7 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel
236263
value={baseUrl}
237264
placeholder="An OpenAI-compatible endpoint"
238265
type="text"
239-
disabled={!isEditing}
266+
disabled={!isEditing || isSaving}
240267
onChange={(e) => {
241268
setIsBaseUrlValid(true);
242269
setBaseUrl(e.target.value);
@@ -251,7 +278,7 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel
251278
value={apiKey}
252279
placeholder="Your API Key"
253280
type={!isEditing && !isNew ? "password" : "text"}
254-
disabled={!isEditing}
281+
disabled={!isEditing || isSaving}
255282
onChange={(e) => {
256283
setIsApiKeyValid(true);
257284
setApiKey(e.target.value);
@@ -278,7 +305,7 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel
278305
type="number"
279306
min={0}
280307
step="1"
281-
disabled={!isEditing}
308+
disabled={!isEditing || isSaving}
282309
onChange={(e) => setContextWindow(e.target.value === "" ? 0 : Math.trunc(Number(e.target.value)))}
283310
/>
284311
</div>
@@ -291,7 +318,7 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel
291318
type="number"
292319
min={0}
293320
step="1"
294-
disabled={!isEditing}
321+
disabled={!isEditing || isSaving}
295322
onChange={(e) => setMaxOutput(e.target.value === "" ? 0 : Math.trunc(Number(e.target.value)))}
296323
/>
297324
</div>
@@ -304,7 +331,7 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel
304331
type="number"
305332
min={0}
306333
step="1"
307-
disabled={!isEditing}
334+
disabled={!isEditing || isSaving}
308335
onChange={(e) => setInputPrice(e.target.value === "" ? 0 : Math.trunc(Number(e.target.value)))}
309336
/>
310337
</div>
@@ -318,7 +345,7 @@ const CustomModelSection = ({ isNew, onChange, model: customModel }: CustomModel
318345
min={0}
319346
step="1"
320347
pattern="[0-9]*"
321-
disabled={!isEditing}
348+
disabled={!isEditing || isSaving}
322349
onChange={(e) => setOutputPrice(e.target.value === "" ? 0 : Math.trunc(Number(e.target.value)))}
323350
/>
324351
</div>

0 commit comments

Comments
 (0)