ChatSettingsFields.svelte 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. <script lang="ts">
  2. import { Checkbox } from '$lib/components/ui/checkbox';
  3. import { Input } from '$lib/components/ui/input';
  4. import Label from '$lib/components/ui/label/label.svelte';
  5. import * as Select from '$lib/components/ui/select';
  6. import { Textarea } from '$lib/components/ui/textarea';
  7. import { SETTING_CONFIG_DEFAULT, SETTING_CONFIG_INFO } from '$lib/constants/settings-config';
  8. import { IsMobile } from '$lib/hooks/is-mobile.svelte';
  9. import { supportsVision } from '$lib/stores/server.svelte';
  10. import type { Component } from 'svelte';
  11. interface Props {
  12. fields: SettingsFieldConfig[];
  13. localConfig: SettingsConfigType;
  14. onConfigChange: (key: string, value: string | boolean) => void;
  15. onThemeChange?: (theme: string) => void;
  16. }
  17. let { fields, localConfig, onConfigChange, onThemeChange }: Props = $props();
  18. let isMobile = $state(new IsMobile());
  19. </script>
  20. {#each fields as field (field.key)}
  21. <div class="space-y-2">
  22. {#if field.type === 'input'}
  23. <Label for={field.key} class="block text-sm font-medium">
  24. {field.label}
  25. </Label>
  26. <Input
  27. id={field.key}
  28. value={String(localConfig[field.key] || '')}
  29. onchange={(e) => onConfigChange(field.key, e.currentTarget.value)}
  30. placeholder={`Default: ${SETTING_CONFIG_DEFAULT[field.key] || 'none'}`}
  31. class={isMobile ? 'w-full' : 'max-w-md'}
  32. />
  33. {#if field.help || SETTING_CONFIG_INFO[field.key]}
  34. <p class="mt-1 text-xs text-muted-foreground">
  35. {field.help || SETTING_CONFIG_INFO[field.key]}
  36. </p>
  37. {/if}
  38. {:else if field.type === 'textarea'}
  39. <Label for={field.key} class="block text-sm font-medium">
  40. {field.label}
  41. </Label>
  42. <Textarea
  43. id={field.key}
  44. value={String(localConfig[field.key] || '')}
  45. onchange={(e) => onConfigChange(field.key, e.currentTarget.value)}
  46. placeholder={`Default: ${SETTING_CONFIG_DEFAULT[field.key] || 'none'}`}
  47. class={isMobile ? 'min-h-[100px] w-full' : 'min-h-[100px] max-w-2xl'}
  48. />
  49. {#if field.help || SETTING_CONFIG_INFO[field.key]}
  50. <p class="mt-1 text-xs text-muted-foreground">
  51. {field.help || SETTING_CONFIG_INFO[field.key]}
  52. </p>
  53. {/if}
  54. {:else if field.type === 'select'}
  55. {@const selectedOption = field.options?.find(
  56. (opt: { value: string; label: string; icon?: Component }) =>
  57. opt.value === localConfig[field.key]
  58. )}
  59. <Label for={field.key} class="block text-sm font-medium">
  60. {field.label}
  61. </Label>
  62. <Select.Root
  63. type="single"
  64. value={localConfig[field.key]}
  65. onValueChange={(value) => {
  66. if (field.key === 'theme' && value && onThemeChange) {
  67. onThemeChange(value);
  68. } else {
  69. onConfigChange(field.key, value);
  70. }
  71. }}
  72. >
  73. <Select.Trigger class={isMobile ? 'w-full' : 'max-w-md'}>
  74. <div class="flex items-center gap-2">
  75. {#if selectedOption?.icon}
  76. {@const IconComponent = selectedOption.icon}
  77. <IconComponent class="h-4 w-4" />
  78. {/if}
  79. {selectedOption?.label || `Select ${field.label.toLowerCase()}`}
  80. </div>
  81. </Select.Trigger>
  82. <Select.Content>
  83. {#if field.options}
  84. {#each field.options as option (option.value)}
  85. <Select.Item value={option.value} label={option.label}>
  86. <div class="flex items-center gap-2">
  87. {#if option.icon}
  88. {@const IconComponent = option.icon}
  89. <IconComponent class="h-4 w-4" />
  90. {/if}
  91. {option.label}
  92. </div>
  93. </Select.Item>
  94. {/each}
  95. {/if}
  96. </Select.Content>
  97. </Select.Root>
  98. {#if field.help || SETTING_CONFIG_INFO[field.key]}
  99. <p class="mt-1 text-xs text-muted-foreground">
  100. {field.help || SETTING_CONFIG_INFO[field.key]}
  101. </p>
  102. {/if}
  103. {:else if field.type === 'checkbox'}
  104. {@const isDisabled = field.key === 'pdfAsImage' && !supportsVision()}
  105. <div class="flex items-start space-x-3">
  106. <Checkbox
  107. id={field.key}
  108. checked={Boolean(localConfig[field.key])}
  109. disabled={isDisabled}
  110. onCheckedChange={(checked) => onConfigChange(field.key, checked)}
  111. class="mt-1"
  112. />
  113. <div class="space-y-1">
  114. <label
  115. for={field.key}
  116. class="cursor-pointer text-sm leading-none font-medium {isDisabled
  117. ? 'text-muted-foreground'
  118. : ''}"
  119. >
  120. {field.label}
  121. </label>
  122. {#if field.help || SETTING_CONFIG_INFO[field.key]}
  123. <p class="text-xs text-muted-foreground">
  124. {field.help || SETTING_CONFIG_INFO[field.key]}
  125. </p>
  126. {:else if field.key === 'pdfAsImage' && !supportsVision()}
  127. <p class="text-xs text-muted-foreground">
  128. PDF-to-image processing requires a vision-capable model. PDFs will be processed as
  129. text.
  130. </p>
  131. {/if}
  132. </div>
  133. </div>
  134. {/if}
  135. </div>
  136. {/each}