Add features & update project
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import type { ProviderModelOption } from '@/lib/models-dev';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { loadModelsDevOptions } from '@/lib/models-dev';
|
||||
import type { ProviderModelOption } from '@/lib/provider-model-options';
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
modelOptionsFromIds,
|
||||
suggestedModelOptions,
|
||||
supportsCustomModelOptions,
|
||||
} from '@/lib/provider-model-options';
|
||||
import { useAction, useMutation, useQuery } from 'convex/react';
|
||||
import { makeFunctionReference } from 'convex/server';
|
||||
import { KeyRound, Trash2 } from 'lucide-react';
|
||||
@@ -11,6 +15,7 @@ import { toast } from 'sonner';
|
||||
import type { Id } from '@spoon/backend/convex/_generated/dataModel.js';
|
||||
import { api } from '@spoon/backend/convex/_generated/api.js';
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -50,6 +55,7 @@ const saveProfileRef = makeFunctionReference<
|
||||
secret?: string;
|
||||
baseUrl?: string;
|
||||
defaultModel: string;
|
||||
modelOptions?: string[];
|
||||
reasoningEffort: ReasoningEffort;
|
||||
enabled: boolean;
|
||||
},
|
||||
@@ -119,33 +125,24 @@ export const AiProviderProfilesPanel = () => {
|
||||
);
|
||||
const [secret, setSecret] = useState('');
|
||||
const [baseUrl, setBaseUrl] = useState('');
|
||||
const [defaultModelValue, setDefaultModelValue] = useState('');
|
||||
const [modelOptions, setModelOptions] = useState<ProviderModelOption[]>([]);
|
||||
const [defaultModelValue, setDefaultModelValue] = useState(
|
||||
suggestedModelOptions('openai')[0]?.id ?? '',
|
||||
);
|
||||
const [modelOptions, setModelOptions] = useState<ProviderModelOption[]>(
|
||||
suggestedModelOptions('openai'),
|
||||
);
|
||||
const [customModelId, setCustomModelId] = useState('');
|
||||
const [reasoningEffort, setReasoningEffort] =
|
||||
useState<ReasoningEffort>('medium');
|
||||
const [enabled, setEnabled] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
loadModelsDevOptions(provider)
|
||||
.then((options) => {
|
||||
if (cancelled) return;
|
||||
setModelOptions(options);
|
||||
setDefaultModelValue((current) =>
|
||||
current && options.some((option) => option.id === current)
|
||||
? current
|
||||
: (options[0]?.id ?? ''),
|
||||
);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.error(error);
|
||||
if (!cancelled) setModelOptions([]);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [provider]);
|
||||
const resetModelOptions = (nextProvider: Provider) => {
|
||||
const options = suggestedModelOptions(nextProvider);
|
||||
setModelOptions(options);
|
||||
setDefaultModelValue(options[0]?.id ?? '');
|
||||
setCustomModelId('');
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
setProfileId(undefined);
|
||||
@@ -153,6 +150,8 @@ export const AiProviderProfilesPanel = () => {
|
||||
setSecret('');
|
||||
setBaseUrl('');
|
||||
setDefaultModelValue('');
|
||||
setModelOptions(suggestedModelOptions('openai'));
|
||||
setCustomModelId('');
|
||||
setReasoningEffort('medium');
|
||||
setEnabled(true);
|
||||
setName('OpenAI');
|
||||
@@ -165,6 +164,14 @@ export const AiProviderProfilesPanel = () => {
|
||||
setSecret('');
|
||||
setBaseUrl(profile.baseUrl ?? '');
|
||||
setDefaultModelValue(profile.defaultModel);
|
||||
setModelOptions(
|
||||
modelOptionsFromIds(
|
||||
profile.modelOptions?.length
|
||||
? profile.modelOptions
|
||||
: [profile.defaultModel],
|
||||
),
|
||||
);
|
||||
setCustomModelId('');
|
||||
setReasoningEffort(profile.reasoningEffort as ReasoningEffort);
|
||||
setEnabled(profile.enabled);
|
||||
};
|
||||
@@ -181,6 +188,7 @@ export const AiProviderProfilesPanel = () => {
|
||||
secret: secret.trim() ? secret : undefined,
|
||||
baseUrl: baseUrl.trim() || undefined,
|
||||
defaultModel: defaultModelValue,
|
||||
modelOptions: modelOptions.map((model) => model.id),
|
||||
reasoningEffort,
|
||||
enabled,
|
||||
});
|
||||
@@ -310,6 +318,7 @@ export const AiProviderProfilesPanel = () => {
|
||||
onValueChange={(value) => {
|
||||
const nextProvider = value as Provider;
|
||||
setProvider(nextProvider);
|
||||
resetModelOptions(nextProvider);
|
||||
setName(
|
||||
providerOptions
|
||||
.find((option) => option.value === nextProvider)
|
||||
@@ -397,9 +406,47 @@ export const AiProviderProfilesPanel = () => {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Models are loaded from Models.dev, the catalog OpenCode uses
|
||||
for provider/model metadata.
|
||||
Saved model options are used by Spoons. Add custom model IDs
|
||||
for compatible provider gateways.
|
||||
</p>
|
||||
<div className='rounded-md border p-2'>
|
||||
<p className='text-muted-foreground mb-2 text-xs'>
|
||||
Available model options
|
||||
</p>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
{modelOptions.map((model) => (
|
||||
<Badge key={model.id} variant='outline'>
|
||||
{model.id}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{supportsCustomModelOptions(provider) ? (
|
||||
<div className='flex gap-2'>
|
||||
<Input
|
||||
value={customModelId}
|
||||
placeholder='provider/model-id'
|
||||
onChange={(event) => setCustomModelId(event.target.value)}
|
||||
/>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
onClick={() => {
|
||||
const id = customModelId.trim();
|
||||
if (!id) return;
|
||||
setModelOptions((current) =>
|
||||
current.some((model) => model.id === id)
|
||||
? current
|
||||
: [...current, ...modelOptionsFromIds([id])],
|
||||
);
|
||||
setDefaultModelValue((current) => current || id);
|
||||
setCustomModelId('');
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
<Label>Thinking</Label>
|
||||
|
||||
Reference in New Issue
Block a user