|
1 | 1 | <script lang="ts"> |
| 2 | + import { RotateCcw } from '@lucide/svelte'; |
2 | 3 | import { Checkbox } from '$lib/components/ui/checkbox'; |
3 | 4 | import { Input } from '$lib/components/ui/input'; |
4 | 5 | import Label from '$lib/components/ui/label/label.svelte'; |
5 | 6 | import * as Select from '$lib/components/ui/select'; |
6 | 7 | import { Textarea } from '$lib/components/ui/textarea'; |
7 | 8 | import { SETTING_CONFIG_DEFAULT, SETTING_CONFIG_INFO } from '$lib/constants/settings-config'; |
8 | 9 | import { supportsVision } from '$lib/stores/server.svelte'; |
| 10 | + import { getParameterInfo, resetParameterToServerDefault } from '$lib/stores/settings.svelte'; |
| 11 | + import { ParameterSyncService } from '$lib/services/parameter-sync'; |
| 12 | + import ParameterSourceIndicator from './ParameterSourceIndicator.svelte'; |
9 | 13 | import type { Component } from 'svelte'; |
10 | 14 |
|
11 | 15 | interface Props { |
|
16 | 20 | } |
17 | 21 |
|
18 | 22 | let { fields, localConfig, onConfigChange, onThemeChange }: Props = $props(); |
| 23 | +
|
| 24 | + // Helper function to get parameter source info for syncable parameters |
| 25 | + function getParameterSourceInfo(key: string) { |
| 26 | + if (!ParameterSyncService.canSyncParameter(key)) { |
| 27 | + return null; |
| 28 | + } |
| 29 | +
|
| 30 | + return getParameterInfo(key); |
| 31 | + } |
19 | 32 | </script> |
20 | 33 |
|
21 | 34 | {#each fields as field (field.key)} |
22 | 35 | <div class="space-y-2"> |
23 | 36 | {#if field.type === 'input'} |
24 | | - <Label for={field.key} class="block text-sm font-medium"> |
25 | | - {field.label} |
26 | | - </Label> |
| 37 | + {@const paramInfo = getParameterSourceInfo(field.key)} |
| 38 | + {@const currentValue = String(localConfig[field.key] ?? '')} |
| 39 | + {@const propsDefault = paramInfo?.serverDefault} |
| 40 | + {@const isCustomRealTime = (() => { |
| 41 | + if (!paramInfo || propsDefault === undefined) return false; |
27 | 42 |
|
28 | | - <Input |
29 | | - id={field.key} |
30 | | - value={String(localConfig[field.key] ?? '')} |
31 | | - onchange={(e) => onConfigChange(field.key, e.currentTarget.value)} |
32 | | - placeholder={`Default: ${SETTING_CONFIG_DEFAULT[field.key] ?? 'none'}`} |
33 | | - class="w-full md:max-w-md" |
34 | | - /> |
| 43 | + // Apply same rounding logic for real-time comparison |
| 44 | + const inputValue = currentValue; |
| 45 | + const numericInput = parseFloat(inputValue); |
| 46 | + const normalizedInput = !isNaN(numericInput) |
| 47 | + ? Math.round(numericInput * 1000000) / 1000000 |
| 48 | + : inputValue; |
| 49 | + const normalizedDefault = |
| 50 | + typeof propsDefault === 'number' |
| 51 | + ? Math.round(propsDefault * 1000000) / 1000000 |
| 52 | + : propsDefault; |
| 53 | + |
| 54 | + return normalizedInput !== normalizedDefault; |
| 55 | + })()} |
| 56 | + |
| 57 | + <div class="flex items-center gap-2"> |
| 58 | + <Label for={field.key} class="text-sm font-medium"> |
| 59 | + {field.label} |
| 60 | + </Label> |
| 61 | + {#if isCustomRealTime} |
| 62 | + <ParameterSourceIndicator /> |
| 63 | + {/if} |
| 64 | + </div> |
| 65 | + |
| 66 | + <div class="relative w-full md:max-w-md"> |
| 67 | + <Input |
| 68 | + id={field.key} |
| 69 | + value={currentValue} |
| 70 | + oninput={(e) => { |
| 71 | + // Update local config immediately for real-time badge feedback |
| 72 | + onConfigChange(field.key, e.currentTarget.value); |
| 73 | + }} |
| 74 | + placeholder={`Default: ${SETTING_CONFIG_DEFAULT[field.key] ?? 'none'}`} |
| 75 | + class="w-full {isCustomRealTime ? 'pr-8' : ''}" |
| 76 | + /> |
| 77 | + {#if isCustomRealTime} |
| 78 | + <button |
| 79 | + type="button" |
| 80 | + onclick={() => { |
| 81 | + resetParameterToServerDefault(field.key); |
| 82 | + // Trigger UI update by calling onConfigChange with the default value |
| 83 | + const defaultValue = propsDefault ?? SETTING_CONFIG_DEFAULT[field.key]; |
| 84 | + onConfigChange(field.key, String(defaultValue)); |
| 85 | + }} |
| 86 | + 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" |
| 87 | + aria-label="Reset to default" |
| 88 | + title="Reset to default" |
| 89 | + > |
| 90 | + <RotateCcw class="h-3 w-3" /> |
| 91 | + </button> |
| 92 | + {/if} |
| 93 | + </div> |
35 | 94 | {#if field.help || SETTING_CONFIG_INFO[field.key]} |
36 | 95 | <p class="mt-1 text-xs text-muted-foreground"> |
37 | 96 | {field.help || SETTING_CONFIG_INFO[field.key]} |
|
59 | 118 | (opt: { value: string; label: string; icon?: Component }) => |
60 | 119 | opt.value === localConfig[field.key] |
61 | 120 | )} |
| 121 | + {@const paramInfo = getParameterSourceInfo(field.key)} |
| 122 | + {@const currentValue = localConfig[field.key]} |
| 123 | + {@const propsDefault = paramInfo?.serverDefault} |
| 124 | + {@const isCustomRealTime = (() => { |
| 125 | + if (!paramInfo || propsDefault === undefined) return false; |
62 | 126 |
|
63 | | - <Label for={field.key} class="block text-sm font-medium"> |
64 | | - {field.label} |
65 | | - </Label> |
| 127 | + // For select fields, do direct comparison (no rounding needed) |
| 128 | + return currentValue !== propsDefault; |
| 129 | + })()} |
| 130 | + |
| 131 | + <div class="flex items-center gap-2"> |
| 132 | + <Label for={field.key} class="text-sm font-medium"> |
| 133 | + {field.label} |
| 134 | + </Label> |
| 135 | + {#if isCustomRealTime} |
| 136 | + <ParameterSourceIndicator /> |
| 137 | + {/if} |
| 138 | + </div> |
66 | 139 |
|
67 | 140 | <Select.Root |
68 | 141 | type="single" |
69 | | - value={localConfig[field.key]} |
| 142 | + value={currentValue} |
70 | 143 | onValueChange={(value) => { |
71 | 144 | if (field.key === 'theme' && value && onThemeChange) { |
72 | 145 | onThemeChange(value); |
|
75 | 148 | } |
76 | 149 | }} |
77 | 150 | > |
78 | | - <Select.Trigger class="w-full md:w-auto md:max-w-md"> |
79 | | - <div class="flex items-center gap-2"> |
80 | | - {#if selectedOption?.icon} |
81 | | - {@const IconComponent = selectedOption.icon} |
82 | | - <IconComponent class="h-4 w-4" /> |
83 | | - {/if} |
84 | | - |
85 | | - {selectedOption?.label || `Select ${field.label.toLowerCase()}`} |
86 | | - </div> |
87 | | - </Select.Trigger> |
| 151 | + <div class="relative w-full md:w-auto md:max-w-md"> |
| 152 | + <Select.Trigger class="w-full"> |
| 153 | + <div class="flex items-center gap-2"> |
| 154 | + {#if selectedOption?.icon} |
| 155 | + {@const IconComponent = selectedOption.icon} |
| 156 | + <IconComponent class="h-4 w-4" /> |
| 157 | + {/if} |
| 158 | + |
| 159 | + {selectedOption?.label || `Select ${field.label.toLowerCase()}`} |
| 160 | + </div> |
| 161 | + </Select.Trigger> |
| 162 | + {#if isCustomRealTime} |
| 163 | + <button |
| 164 | + type="button" |
| 165 | + onclick={() => { |
| 166 | + resetParameterToServerDefault(field.key); |
| 167 | + // Trigger UI update by calling onConfigChange with the default value |
| 168 | + const defaultValue = propsDefault ?? SETTING_CONFIG_DEFAULT[field.key]; |
| 169 | + onConfigChange(field.key, String(defaultValue)); |
| 170 | + }} |
| 171 | + 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" |
| 172 | + aria-label="Reset to default" |
| 173 | + title="Reset to default" |
| 174 | + > |
| 175 | + <RotateCcw class="h-3 w-3" /> |
| 176 | + </button> |
| 177 | + {/if} |
| 178 | + </div> |
88 | 179 | <Select.Content> |
89 | 180 | {#if field.options} |
90 | 181 | {#each field.options as option (option.value)} |
|
0 commit comments