|
| 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 | +} |
0 commit comments