1<script lang="ts">
2 import { RotateCcw, FlaskConical } from '@lucide/svelte';
3 import { Checkbox } from '$lib/components/ui/checkbox';
4 import { Input } from '$lib/components/ui/input';
5 import Label from '$lib/components/ui/label/label.svelte';
6 import * as Select from '$lib/components/ui/select';
7 import { Textarea } from '$lib/components/ui/textarea';
8 import { SETTING_CONFIG_DEFAULT, SETTING_CONFIG_INFO } from '$lib/constants/settings-config';
9 import { settingsStore } from '$lib/stores/settings.svelte';
10 import { ChatSettingsParameterSourceIndicator } from '$lib/components/app';
11 import type { Component } from 'svelte';
12
13 interface Props {
14 fields: SettingsFieldConfig[];
15 localConfig: SettingsConfigType;
16 onConfigChange: (key: string, value: string | boolean) => void;
17 onThemeChange?: (theme: string) => void;
18 }
19
20 let { fields, localConfig, onConfigChange, onThemeChange }: Props = $props();
21
22 // Helper function to get parameter source info for syncable parameters
23 function getParameterSourceInfo(key: string) {
24 if (!settingsStore.canSyncParameter(key)) {
25 return null;
26 }
27
28 return settingsStore.getParameterInfo(key);
29 }
30</script>
31
32{#each fields as field (field.key)}
33 <div class="space-y-2">
34 {#if field.type === 'input'}
35 {@const paramInfo = getParameterSourceInfo(field.key)}
36 {@const currentValue = String(localConfig[field.key] ?? '')}
37 {@const propsDefault = paramInfo?.serverDefault}
38 {@const isCustomRealTime = (() => {
39 if (!paramInfo || propsDefault === undefined) return false;
40
41 // Apply same rounding logic for real-time comparison
42 const inputValue = currentValue;
43 const numericInput = parseFloat(inputValue);
44 const normalizedInput = !isNaN(numericInput)
45 ? Math.round(numericInput * 1000000) / 1000000
46 : inputValue;
47 const normalizedDefault =
48 typeof propsDefault === 'number'
49 ? Math.round(propsDefault * 1000000) / 1000000
50 : propsDefault;
51
52 return normalizedInput !== normalizedDefault;
53 })()}
54
55 <div class="flex items-center gap-2">
56 <Label for={field.key} class="flex items-center gap-1.5 text-sm font-medium">
57 {field.label}
58
59 {#if field.isExperimental}
60 <FlaskConical class="h-3.5 w-3.5 text-muted-foreground" />
61 {/if}
62 </Label>
63 {#if isCustomRealTime}
64 <ChatSettingsParameterSourceIndicator />
65 {/if}
66 </div>
67
68 <div class="relative w-full md:max-w-md">
69 <Input
70 id={field.key}
71 value={currentValue}
72 oninput={(e) => {
73 // Update local config immediately for real-time badge feedback
74 onConfigChange(field.key, e.currentTarget.value);
75 }}
76 placeholder={`Default: ${SETTING_CONFIG_DEFAULT[field.key] ?? 'none'}`}
77 class="w-full {isCustomRealTime ? 'pr-8' : ''}"
78 />
79 {#if isCustomRealTime}
80 <button
81 type="button"
82 onclick={() => {
83 settingsStore.resetParameterToServerDefault(field.key);
84 // Trigger UI update by calling onConfigChange with the default value
85 const defaultValue = propsDefault ?? SETTING_CONFIG_DEFAULT[field.key];
86 onConfigChange(field.key, String(defaultValue));
87 }}
88 class="absolute top-1/2 right-2 inline-flex h-5 w-5 -translate-y-1/2 items-center justify-center rounded transition-colors hover:bg-muted"
89 aria-label="Reset to default"
90 title="Reset to default"
91 >
92 <RotateCcw class="h-3 w-3" />
93 </button>
94 {/if}
95 </div>
96 {#if field.help || SETTING_CONFIG_INFO[field.key]}
97 <p class="mt-1 text-xs text-muted-foreground">
98 {@html field.help || SETTING_CONFIG_INFO[field.key]}
99 </p>
100 {/if}
101 {:else if field.type === 'textarea'}
102 <Label for={field.key} class="block flex items-center gap-1.5 text-sm font-medium">
103 {field.label}
104
105 {#if field.isExperimental}
106 <FlaskConical class="h-3.5 w-3.5 text-muted-foreground" />
107 {/if}
108 </Label>
109
110 <Textarea
111 id={field.key}
112 value={String(localConfig[field.key] ?? '')}
113 onchange={(e) => onConfigChange(field.key, e.currentTarget.value)}
114 placeholder={`Default: ${SETTING_CONFIG_DEFAULT[field.key] ?? 'none'}`}
115 class="min-h-[10rem] w-full md:max-w-2xl"
116 />
117
118 {#if field.help || SETTING_CONFIG_INFO[field.key]}
119 <p class="mt-1 text-xs text-muted-foreground">
120 {field.help || SETTING_CONFIG_INFO[field.key]}
121 </p>
122 {/if}
123
124 {#if field.key === 'systemMessage'}
125 <div class="mt-3 flex items-center gap-2">
126 <Checkbox
127 id="showSystemMessage"
128 checked={Boolean(localConfig.showSystemMessage ?? true)}
129 onCheckedChange={(checked) => onConfigChange('showSystemMessage', Boolean(checked))}
130 />
131
132 <Label for="showSystemMessage" class="cursor-pointer text-sm font-normal">
133 Show system message in conversations
134 </Label>
135 </div>
136 {/if}
137 {:else if field.type === 'select'}
138 {@const selectedOption = field.options?.find(
139 (opt: { value: string; label: string; icon?: Component }) =>
140 opt.value === localConfig[field.key]
141 )}
142 {@const paramInfo = getParameterSourceInfo(field.key)}
143 {@const currentValue = localConfig[field.key]}
144 {@const propsDefault = paramInfo?.serverDefault}
145 {@const isCustomRealTime = (() => {
146 if (!paramInfo || propsDefault === undefined) return false;
147
148 // For select fields, do direct comparison (no rounding needed)
149 return currentValue !== propsDefault;
150 })()}
151
152 <div class="flex items-center gap-2">
153 <Label for={field.key} class="flex items-center gap-1.5 text-sm font-medium">
154 {field.label}
155
156 {#if field.isExperimental}
157 <FlaskConical class="h-3.5 w-3.5 text-muted-foreground" />
158 {/if}
159 </Label>
160 {#if isCustomRealTime}
161 <ChatSettingsParameterSourceIndicator />
162 {/if}
163 </div>
164
165 <Select.Root
166 type="single"
167 value={currentValue}
168 onValueChange={(value) => {
169 if (field.key === 'theme' && value && onThemeChange) {
170 onThemeChange(value);
171 } else {
172 onConfigChange(field.key, value);
173 }
174 }}
175 >
176 <div class="relative w-full md:w-auto md:max-w-md">
177 <Select.Trigger class="w-full">
178 <div class="flex items-center gap-2">
179 {#if selectedOption?.icon}
180 {@const IconComponent = selectedOption.icon}
181 <IconComponent class="h-4 w-4" />
182 {/if}
183
184 {selectedOption?.label || `Select ${field.label.toLowerCase()}`}
185 </div>
186 </Select.Trigger>
187 {#if isCustomRealTime}
188 <button
189 type="button"
190 onclick={() => {
191 settingsStore.resetParameterToServerDefault(field.key);
192 // Trigger UI update by calling onConfigChange with the default value
193 const defaultValue = propsDefault ?? SETTING_CONFIG_DEFAULT[field.key];
194 onConfigChange(field.key, String(defaultValue));
195 }}
196 class="absolute top-1/2 right-8 inline-flex h-5 w-5 -translate-y-1/2 items-center justify-center rounded transition-colors hover:bg-muted"
197 aria-label="Reset to default"
198 title="Reset to default"
199 >
200 <RotateCcw class="h-3 w-3" />
201 </button>
202 {/if}
203 </div>
204 <Select.Content>
205 {#if field.options}
206 {#each field.options as option (option.value)}
207 <Select.Item value={option.value} label={option.label}>
208 <div class="flex items-center gap-2">
209 {#if option.icon}
210 {@const IconComponent = option.icon}
211 <IconComponent class="h-4 w-4" />
212 {/if}
213 {option.label}
214 </div>
215 </Select.Item>
216 {/each}
217 {/if}
218 </Select.Content>
219 </Select.Root>
220 {#if field.help || SETTING_CONFIG_INFO[field.key]}
221 <p class="mt-1 text-xs text-muted-foreground">
222 {field.help || SETTING_CONFIG_INFO[field.key]}
223 </p>
224 {/if}
225 {:else if field.type === 'checkbox'}
226 <div class="flex items-start space-x-3">
227 <Checkbox
228 id={field.key}
229 checked={Boolean(localConfig[field.key])}
230 onCheckedChange={(checked) => onConfigChange(field.key, checked)}
231 class="mt-1"
232 />
233
234 <div class="space-y-1">
235 <label
236 for={field.key}
237 class="flex cursor-pointer items-center gap-1.5 pt-1 pb-0.5 text-sm leading-none font-medium"
238 >
239 {field.label}
240
241 {#if field.isExperimental}
242 <FlaskConical class="h-3.5 w-3.5 text-muted-foreground" />
243 {/if}
244 </label>
245
246 {#if field.help || SETTING_CONFIG_INFO[field.key]}
247 <p class="text-xs text-muted-foreground">
248 {field.help || SETTING_CONFIG_INFO[field.key]}
249 </p>
250 {/if}
251 </div>
252 </div>
253 {/if}
254 </div>
255{/each}