Skip to content

Commit f53e231

Browse files
authored
Add openai provider (#30)
1 parent 3f72ed4 commit f53e231

6 files changed

Lines changed: 195 additions & 5 deletions

File tree

public/providerIcons/OpenAI.svg

Lines changed: 15 additions & 0 deletions
Loading

src/app/settings/tabs/model/create-model-dialog.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ProviderKey, getProviderDisplayName, getProviderIcon, AVAILABLE_PROVIDE
55
import Image from 'next/image';
66
import { Modal } from '@/components/ui/modal';
77
import { AzureOpenAIForm } from './azure-openai-form';
8+
import { OpenAIForm } from './openai-form';
89
import { TUIClientSingleton } from '@/lib/tui-client-singleton';
910

1011
export interface CreateModelDialogProps {
@@ -78,6 +79,11 @@ export const CreateModelDialog = ({ onComplete }: CreateModelDialogProps) => {
7879
onSubmit={createModelAsync}
7980
/>
8081
)}
82+
{selectedProvider === 'OpenAI' && (!saving) && (
83+
<OpenAIForm
84+
onSubmit={createModelAsync}
85+
/>
86+
)}
8187
{saving && (
8288
<div className="flex w-full items-center justify-center py-8 gap-3 select-none" aria-live="polite">
8389
<div className="relative h-8 w-8" role="status" aria-label="正在保存">

src/app/settings/tabs/model/modify-model-dialog.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Modal } from '@/components/ui/modal';
66
import { AzureOpenAIForm } from './azure-openai-form';
77
import { TUIClientSingleton } from '@/lib/tui-client-singleton';
88
import { objectsAreEqual } from '@/lib/obj-helper';
9+
import { OpenAIForm } from './openai-form';
910

1011
export interface ModifyModelDialogProps {
1112
modelId: string;
@@ -89,6 +90,13 @@ export const ModifyModelDialog = ({ modelId, onComplete }: ModifyModelDialogProp
8990
onSubmit={modifyModelAsync}
9091
/>
9192
)}
93+
{providerName === 'OpenAI' && (!saving) && loaded && (
94+
<OpenAIForm
95+
initialName={modelName}
96+
initialSettings={providerParams}
97+
onSubmit={modifyModelAsync}
98+
/>
99+
)}
92100
{saving || (!loaded) && (
93101
<div className="flex w-full items-center justify-center py-8 gap-3 select-none" aria-live="polite">
94102
<div className="relative h-8 w-8" role="status" aria-label={saving ? "正在保存" : "正在加载"}>
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
"use client";
2+
3+
import { useState } from 'react';
4+
import { Input } from '@/components/ui/input';
5+
import { Button } from '@/components/ui/button';
6+
import { ProviderFormProps } from './provider-form-props';
7+
import { tryGetProperty } from '@/lib/obj-helper';
8+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
9+
10+
const validReasoningEfforts = new Set(['none', 'low', 'medium', 'high']);
11+
12+
export function OpenAIForm({ initialName, initialSettings, onSubmit }: ProviderFormProps) {
13+
const [name, setName] = useState<string>(initialName ?? '');
14+
const [url, setUrl] = useState<string>(tryGetProperty(initialSettings, 'url', 'string') ?? '');
15+
const [apiKey, setApiKey] = useState<string>(tryGetProperty(initialSettings, 'apiKey', 'string') ?? '');
16+
const [model, setModel] = useState<string>(tryGetProperty(initialSettings, 'model', 'string') ?? '');
17+
const [temperature, setTemperature] = useState<number | undefined>(tryGetProperty(initialSettings, 'temperature', 'number'));
18+
19+
const initialReasoningEffort = tryGetProperty(initialSettings, 'reasoningEffort', 'string');
20+
const [reasoningEffort, setReasoningEffort] = useState<string>(
21+
validReasoningEfforts.has(initialReasoningEffort ?? '') ? (initialReasoningEffort ?? '') : ''
22+
);
23+
24+
const canSubmit = name.trim() && apiKey.trim() && model.trim();
25+
26+
return (
27+
<div className="flex flex-col gap-4">
28+
<div className="flex flex-col gap-2">
29+
<label className="text-sm font-medium" htmlFor="openai-model-name">模型名称</label>
30+
<Input
31+
id="openai-model-name"
32+
placeholder="输入模型名称"
33+
value={name}
34+
onChange={(e) => setName(e.target.value)}
35+
required
36+
/>
37+
</div>
38+
<div className="flex flex-col gap-2">
39+
<label className="text-sm font-medium" htmlFor="openai-model-url">Base URL</label>
40+
<Input
41+
id="openai-model-url"
42+
placeholder="[可选] https://api.openai.com/v1"
43+
value={url}
44+
onChange={(e) => setUrl(e.target.value)}
45+
/>
46+
</div>
47+
<div className="flex flex-col gap-2">
48+
<label className="text-sm font-medium" htmlFor="openai-model-key">API Key</label>
49+
<Input
50+
id="openai-model-key"
51+
placeholder="API Key"
52+
type="password"
53+
value={apiKey}
54+
onChange={(e) => setApiKey(e.target.value)}
55+
required
56+
autoComplete="off"
57+
/>
58+
</div>
59+
<div className="flex flex-col gap-2">
60+
<label className="text-sm font-medium" htmlFor="openai-model-id">Model id</label>
61+
<Input
62+
id="openai-model-id"
63+
placeholder="例如 gpt-4o"
64+
value={model}
65+
onChange={(e) => setModel(e.target.value)}
66+
required
67+
/>
68+
</div>
69+
<div className="flex flex-col gap-2">
70+
<label className="text-sm font-medium" htmlFor="openai-model-temperature">Temperature</label>
71+
<Input
72+
id="openai-model-temperature"
73+
placeholder="[可选] temperature"
74+
type="number"
75+
value={temperature ?? ''}
76+
onChange={(e) => {
77+
let value: number | undefined = parseFloat(e.target.value);
78+
if (Number.isNaN(value)) {
79+
value = undefined;
80+
}
81+
setTemperature(value);
82+
}}
83+
/>
84+
</div>
85+
<div className="flex flex-col gap-2">
86+
<label className="text-sm font-medium" htmlFor="openai-model-reasoning">Reasoning Effort</label>
87+
<Select
88+
value={reasoningEffort}
89+
onValueChange={(value) => {
90+
setReasoningEffort(value);
91+
}}
92+
>
93+
<SelectTrigger
94+
id="openai-model-reasoning"
95+
className="w-full"
96+
hasValue={reasoningEffort !== ''}
97+
onClear={()=>{setReasoningEffort('')}}
98+
>
99+
<SelectValue placeholder="[可选] reasoning effort" />
100+
</SelectTrigger>
101+
<SelectContent>
102+
<SelectItem value="none">none</SelectItem>
103+
<SelectItem value="low">low</SelectItem>
104+
<SelectItem value="medium">medium</SelectItem>
105+
<SelectItem value="high">high</SelectItem>
106+
</SelectContent>
107+
</Select>
108+
</div>
109+
<div className="flex items-center justify-between gap-2 pt-2">
110+
<div className="flex items-center gap-2">
111+
<Button
112+
type="button"
113+
disabled={!canSubmit}
114+
onClick={() => {
115+
const trimmedName = name.trim();
116+
const trimmedUrl = url.trim();
117+
const settings: Record<string, unknown> = {
118+
apiKey: apiKey.trim(),
119+
model: model.trim(),
120+
};
121+
if (temperature !== undefined) {
122+
settings.temperature = temperature;
123+
}
124+
if (trimmedUrl !== '') {
125+
settings.url = trimmedUrl;
126+
}
127+
if (reasoningEffort !== '') {
128+
settings.reasoningEffort = reasoningEffort;
129+
}
130+
onSubmit(trimmedName, settings);
131+
}}
132+
>确认</Button>
133+
</div>
134+
</div>
135+
</div>
136+
);
137+
}

src/app/settings/tabs/model/provider-registry.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
export const AVAILABLE_PROVIDERS = [
2-
'AzureOpenAI'
2+
'AzureOpenAI',
3+
'OpenAI',
34
] as const;
45

56
export type ProviderKey = typeof AVAILABLE_PROVIDERS[number];
67

78
const providerIconMap: Record<string, string> = {
89
default: '/providerIcons/default.png',
9-
AzureOpenAI: '/providerIcons/Azure-OpenAI.svg'
10+
AzureOpenAI: '/providerIcons/Azure-OpenAI.svg',
11+
OpenAI: '/providerIcons/OpenAI.svg',
1012
};
1113

1214
const providerDisplayNameMap: Record<string, string> = {
13-
AzureOpenAI: 'Azure OpenAI'
15+
AzureOpenAI: 'Azure OpenAI',
16+
OpenAI: 'OpenAI',
1417
};
1518

1619
export function getProviderIcon(providerName: string): string {

src/components/ui/select.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import * as React from "react"
44
import * as SelectPrimitive from "@radix-ui/react-select"
5-
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
5+
import { CheckIcon, ChevronDownIcon, ChevronUpIcon, XIcon } from "lucide-react"
66

77
import { cn } from "@/lib/utils"
88

@@ -28,9 +28,13 @@ function SelectTrigger({
2828
className,
2929
size = "default",
3030
children,
31+
hasValue = false,
32+
onClear,
3133
...props
3234
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
3335
size?: "sm" | "default"
36+
hasValue?: boolean
37+
onClear?: () => void
3438
}) {
3539
return (
3640
<SelectPrimitive.Trigger
@@ -42,7 +46,24 @@ function SelectTrigger({
4246
)}
4347
{...props}
4448
>
45-
{children}
49+
<span className="flex min-w-0 flex-1 items-center gap-2">{children}</span>
50+
{hasValue && onClear && (
51+
<span
52+
role="presentation"
53+
className="text-muted-foreground hover:text-foreground"
54+
onPointerDown={(event) => {
55+
event.preventDefault();
56+
event.stopPropagation();
57+
}}
58+
onClick={(event) => {
59+
event.preventDefault();
60+
event.stopPropagation();
61+
onClear();
62+
}}
63+
>
64+
<XIcon className="size-4" />
65+
</span>
66+
)}
4667
<SelectPrimitive.Icon asChild>
4768
<ChevronDownIcon className="size-4 opacity-50" />
4869
</SelectPrimitive.Icon>

0 commit comments

Comments
 (0)