1<script lang="ts">
  2	import {
  3		Settings,
  4		Funnel,
  5		AlertTriangle,
  6		Code,
  7		Monitor,
  8		Sun,
  9		Moon,
 10		ChevronLeft,
 11		ChevronRight,
 12		Database
 13	} from '@lucide/svelte';
 14	import {
 15		ChatSettingsFooter,
 16		ChatSettingsImportExportTab,
 17		ChatSettingsFields
 18	} from '$lib/components/app';
 19	import { ScrollArea } from '$lib/components/ui/scroll-area';
 20	import { config, settingsStore } from '$lib/stores/settings.svelte';
 21	import { setMode } from 'mode-watcher';
 22	import type { Component } from 'svelte';
 23
 24	interface Props {
 25		onSave?: () => void;
 26	}
 27
 28	let { onSave }: Props = $props();
 29
 30	const settingSections: Array<{
 31		fields: SettingsFieldConfig[];
 32		icon: Component;
 33		title: string;
 34	}> = [
 35		{
 36			title: 'General',
 37			icon: Settings,
 38			fields: [
 39				{
 40					key: 'theme',
 41					label: 'Theme',
 42					type: 'select',
 43					options: [
 44						{ value: 'system', label: 'System', icon: Monitor },
 45						{ value: 'light', label: 'Light', icon: Sun },
 46						{ value: 'dark', label: 'Dark', icon: Moon }
 47					]
 48				},
 49				{ key: 'apiKey', label: 'API Key', type: 'input' },
 50				{
 51					key: 'systemMessage',
 52					label: 'System Message',
 53					type: 'textarea'
 54				},
 55				{
 56					key: 'pasteLongTextToFileLen',
 57					label: 'Paste long text to file length',
 58					type: 'input'
 59				},
 60				{
 61					key: 'copyTextAttachmentsAsPlainText',
 62					label: 'Copy text attachments as plain text',
 63					type: 'checkbox'
 64				},
 65				{
 66					key: 'enableContinueGeneration',
 67					label: 'Enable "Continue" button',
 68					type: 'checkbox',
 69					isExperimental: true
 70				},
 71				{
 72					key: 'pdfAsImage',
 73					label: 'Parse PDF as image',
 74					type: 'checkbox'
 75				},
 76				{
 77					key: 'askForTitleConfirmation',
 78					label: 'Ask for confirmation before changing conversation title',
 79					type: 'checkbox'
 80				}
 81			]
 82		},
 83		{
 84			title: 'Display',
 85			icon: Monitor,
 86			fields: [
 87				{
 88					key: 'showMessageStats',
 89					label: 'Show message generation statistics',
 90					type: 'checkbox'
 91				},
 92				{
 93					key: 'showThoughtInProgress',
 94					label: 'Show thought in progress',
 95					type: 'checkbox'
 96				},
 97				{
 98					key: 'keepStatsVisible',
 99					label: 'Keep stats visible after generation',
100					type: 'checkbox'
101				},
102				{
103					key: 'autoMicOnEmpty',
104					label: 'Show microphone on empty input',
105					type: 'checkbox',
106					isExperimental: true
107				},
108				{
109					key: 'renderUserContentAsMarkdown',
110					label: 'Render user content as Markdown',
111					type: 'checkbox'
112				},
113				{
114					key: 'disableAutoScroll',
115					label: 'Disable automatic scroll',
116					type: 'checkbox'
117				},
118				{
119					key: 'alwaysShowSidebarOnDesktop',
120					label: 'Always show sidebar on desktop',
121					type: 'checkbox'
122				},
123				{
124					key: 'autoShowSidebarOnNewChat',
125					label: 'Auto-show sidebar on new chat',
126					type: 'checkbox'
127				}
128			]
129		},
130		{
131			title: 'Sampling',
132			icon: Funnel,
133			fields: [
134				{
135					key: 'temperature',
136					label: 'Temperature',
137					type: 'input'
138				},
139				{
140					key: 'dynatemp_range',
141					label: 'Dynamic temperature range',
142					type: 'input'
143				},
144				{
145					key: 'dynatemp_exponent',
146					label: 'Dynamic temperature exponent',
147					type: 'input'
148				},
149				{
150					key: 'top_k',
151					label: 'Top K',
152					type: 'input'
153				},
154				{
155					key: 'top_p',
156					label: 'Top P',
157					type: 'input'
158				},
159				{
160					key: 'min_p',
161					label: 'Min P',
162					type: 'input'
163				},
164				{
165					key: 'xtc_probability',
166					label: 'XTC probability',
167					type: 'input'
168				},
169				{
170					key: 'xtc_threshold',
171					label: 'XTC threshold',
172					type: 'input'
173				},
174				{
175					key: 'typ_p',
176					label: 'Typical P',
177					type: 'input'
178				},
179				{
180					key: 'max_tokens',
181					label: 'Max tokens',
182					type: 'input'
183				},
184				{
185					key: 'samplers',
186					label: 'Samplers',
187					type: 'input'
188				},
189				{
190					key: 'backend_sampling',
191					label: 'Backend sampling',
192					type: 'checkbox'
193				}
194			]
195		},
196		{
197			title: 'Penalties',
198			icon: AlertTriangle,
199			fields: [
200				{
201					key: 'repeat_last_n',
202					label: 'Repeat last N',
203					type: 'input'
204				},
205				{
206					key: 'repeat_penalty',
207					label: 'Repeat penalty',
208					type: 'input'
209				},
210				{
211					key: 'presence_penalty',
212					label: 'Presence penalty',
213					type: 'input'
214				},
215				{
216					key: 'frequency_penalty',
217					label: 'Frequency penalty',
218					type: 'input'
219				},
220				{
221					key: 'dry_multiplier',
222					label: 'DRY multiplier',
223					type: 'input'
224				},
225				{
226					key: 'dry_base',
227					label: 'DRY base',
228					type: 'input'
229				},
230				{
231					key: 'dry_allowed_length',
232					label: 'DRY allowed length',
233					type: 'input'
234				},
235				{
236					key: 'dry_penalty_last_n',
237					label: 'DRY penalty last N',
238					type: 'input'
239				}
240			]
241		},
242		{
243			title: 'Import/Export',
244			icon: Database,
245			fields: []
246		},
247		{
248			title: 'Developer',
249			icon: Code,
250			fields: [
251				{
252					key: 'showToolCalls',
253					label: 'Show tool call labels',
254					type: 'checkbox'
255				},
256				{
257					key: 'disableReasoningFormat',
258					label: 'Show raw LLM output',
259					type: 'checkbox'
260				},
261				{
262					key: 'custom',
263					label: 'Custom JSON',
264					type: 'textarea'
265				}
266			]
267		}
268		// TODO: Experimental features section will be implemented after initial release
269		// This includes Python interpreter (Pyodide integration) and other experimental features
270		// {
271		// 	title: 'Experimental',
272		// 	icon: Beaker,
273		// 	fields: [
274		// 		{
275		// 			key: 'pyInterpreterEnabled',
276		// 			label: 'Enable Python interpreter',
277		// 			type: 'checkbox'
278		// 		}
279		// 	]
280		// }
281	];
282
283	let activeSection = $state('General');
284	let currentSection = $derived(
285		settingSections.find((section) => section.title === activeSection) || settingSections[0]
286	);
287	let localConfig: SettingsConfigType = $state({ ...config() });
288
289	let canScrollLeft = $state(false);
290	let canScrollRight = $state(false);
291	let scrollContainer: HTMLDivElement | undefined = $state();
292
293	function handleThemeChange(newTheme: string) {
294		localConfig.theme = newTheme;
295
296		setMode(newTheme as 'light' | 'dark' | 'system');
297	}
298
299	function handleConfigChange(key: string, value: string | boolean) {
300		localConfig[key] = value;
301	}
302
303	function handleReset() {
304		localConfig = { ...config() };
305
306		setMode(localConfig.theme as 'light' | 'dark' | 'system');
307	}
308
309	function handleSave() {
310		if (localConfig.custom && typeof localConfig.custom === 'string' && localConfig.custom.trim()) {
311			try {
312				JSON.parse(localConfig.custom);
313			} catch (error) {
314				alert('Invalid JSON in custom parameters. Please check the format and try again.');
315				console.error(error);
316				return;
317			}
318		}
319
320		// Convert numeric strings to numbers for numeric fields
321		const processedConfig = { ...localConfig };
322		const numericFields = [
323			'temperature',
324			'top_k',
325			'top_p',
326			'min_p',
327			'max_tokens',
328			'pasteLongTextToFileLen',
329			'dynatemp_range',
330			'dynatemp_exponent',
331			'typ_p',
332			'xtc_probability',
333			'xtc_threshold',
334			'repeat_last_n',
335			'repeat_penalty',
336			'presence_penalty',
337			'frequency_penalty',
338			'dry_multiplier',
339			'dry_base',
340			'dry_allowed_length',
341			'dry_penalty_last_n'
342		];
343
344		for (const field of numericFields) {
345			if (processedConfig[field] !== undefined && processedConfig[field] !== '') {
346				const numValue = Number(processedConfig[field]);
347				if (!isNaN(numValue)) {
348					processedConfig[field] = numValue;
349				} else {
350					alert(`Invalid numeric value for ${field}. Please enter a valid number.`);
351					return;
352				}
353			}
354		}
355
356		settingsStore.updateMultipleConfig(processedConfig);
357		onSave?.();
358	}
359
360	function scrollToCenter(element: HTMLElement) {
361		if (!scrollContainer) return;
362
363		const containerRect = scrollContainer.getBoundingClientRect();
364		const elementRect = element.getBoundingClientRect();
365
366		const elementCenter = elementRect.left + elementRect.width / 2;
367		const containerCenter = containerRect.left + containerRect.width / 2;
368		const scrollOffset = elementCenter - containerCenter;
369
370		scrollContainer.scrollBy({ left: scrollOffset, behavior: 'smooth' });
371	}
372
373	function scrollLeft() {
374		if (!scrollContainer) return;
375
376		scrollContainer.scrollBy({ left: -250, behavior: 'smooth' });
377	}
378
379	function scrollRight() {
380		if (!scrollContainer) return;
381
382		scrollContainer.scrollBy({ left: 250, behavior: 'smooth' });
383	}
384
385	function updateScrollButtons() {
386		if (!scrollContainer) return;
387
388		const { scrollLeft, scrollWidth, clientWidth } = scrollContainer;
389		canScrollLeft = scrollLeft > 0;
390		canScrollRight = scrollLeft < scrollWidth - clientWidth - 1; // -1 for rounding
391	}
392
393	export function reset() {
394		localConfig = { ...config() };
395
396		setTimeout(updateScrollButtons, 100);
397	}
398
399	$effect(() => {
400		if (scrollContainer) {
401			updateScrollButtons();
402		}
403	});
404</script>
405
406<div class="flex h-full flex-col overflow-hidden md:flex-row">
407	<!-- Desktop Sidebar -->
408	<div class="hidden w-64 border-r border-border/30 p-6 md:block">
409		<nav class="space-y-1 py-2">
410			{#each settingSections as section (section.title)}
411				<button
412					class="flex w-full cursor-pointer items-center gap-3 rounded-lg px-3 py-2 text-left text-sm transition-colors hover:bg-accent {activeSection ===
413					section.title
414						? 'bg-accent text-accent-foreground'
415						: 'text-muted-foreground'}"
416					onclick={() => (activeSection = section.title)}
417				>
418					<section.icon class="h-4 w-4" />
419
420					<span class="ml-2">{section.title}</span>
421				</button>
422			{/each}
423		</nav>
424	</div>
425
426	<!-- Mobile Header with Horizontal Scrollable Menu -->
427	<div class="flex flex-col pt-6 md:hidden">
428		<div class="border-b border-border/30 py-4">
429			<!-- Horizontal Scrollable Category Menu with Navigation -->
430			<div class="relative flex items-center" style="scroll-padding: 1rem;">
431				<button
432					class="absolute left-2 z-10 flex h-6 w-6 items-center justify-center rounded-full bg-muted shadow-md backdrop-blur-sm transition-opacity hover:bg-accent {canScrollLeft
433						? 'opacity-100'
434						: 'pointer-events-none opacity-0'}"
435					onclick={scrollLeft}
436					aria-label="Scroll left"
437				>
438					<ChevronLeft class="h-4 w-4" />
439				</button>
440
441				<div
442					class="scrollbar-hide overflow-x-auto py-2"
443					bind:this={scrollContainer}
444					onscroll={updateScrollButtons}
445				>
446					<div class="flex min-w-max gap-2">
447						{#each settingSections as section (section.title)}
448							<button
449								class="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm whitespace-nowrap transition-colors first:ml-4 last:mr-4 hover:bg-accent {activeSection ===
450								section.title
451									? 'bg-accent text-accent-foreground'
452									: 'text-muted-foreground'}"
453								onclick={(e: MouseEvent) => {
454									activeSection = section.title;
455									scrollToCenter(e.currentTarget as HTMLElement);
456								}}
457							>
458								<section.icon class="h-4 w-4 flex-shrink-0" />
459								<span>{section.title}</span>
460							</button>
461						{/each}
462					</div>
463				</div>
464
465				<button
466					class="absolute right-2 z-10 flex h-6 w-6 items-center justify-center rounded-full bg-muted shadow-md backdrop-blur-sm transition-opacity hover:bg-accent {canScrollRight
467						? 'opacity-100'
468						: 'pointer-events-none opacity-0'}"
469					onclick={scrollRight}
470					aria-label="Scroll right"
471				>
472					<ChevronRight class="h-4 w-4" />
473				</button>
474			</div>
475		</div>
476	</div>
477
478	<ScrollArea class="max-h-[calc(100dvh-13.5rem)] flex-1 md:max-h-[calc(100vh-13.5rem)]">
479		<div class="space-y-6 p-4 md:p-6">
480			<div class="grid">
481				<div class="mb-6 flex hidden items-center gap-2 border-b border-border/30 pb-6 md:flex">
482					<currentSection.icon class="h-5 w-5" />
483
484					<h3 class="text-lg font-semibold">{currentSection.title}</h3>
485				</div>
486
487				{#if currentSection.title === 'Import/Export'}
488					<ChatSettingsImportExportTab />
489				{:else}
490					<div class="space-y-6">
491						<ChatSettingsFields
492							fields={currentSection.fields}
493							{localConfig}
494							onConfigChange={handleConfigChange}
495							onThemeChange={handleThemeChange}
496						/>
497					</div>
498				{/if}
499			</div>
500
501			<div class="mt-8 border-t pt-6">
502				<p class="text-xs text-muted-foreground">Settings are saved in browser's localStorage</p>
503			</div>
504		</div>
505	</ScrollArea>
506</div>
507
508<ChatSettingsFooter onReset={handleReset} onSave={handleSave} />