Repository: obot-platform/obot
Pull request #2927: feat: enhance model providers ux
Author: login: ryu-man
Created: 2025-05-13T15:54:22Z | Merged: 2025-05-20T21:30:31Z

Addresses #2926 


Diff:
diff --git a/ui/user/package.json b/ui/user/package.json
index 936d64331b..1d288bbcd3 100644
--- a/ui/user/package.json
+++ b/ui/user/package.json
@@ -73,5 +73,8 @@
 		"typescript-eslint": "^8.25.0",
 		"vite": "^5.4.14"
 	},
-	"type": "module"
+	"type": "module",
+	"dependencies": {
+		"es-toolkit": "^1.37.2"
+	}
 }
diff --git a/ui/user/pnpm-lock.yaml b/ui/user/pnpm-lock.yaml
index 76cd048275..db2342c8bd 100644
--- a/ui/user/pnpm-lock.yaml
+++ b/ui/user/pnpm-lock.yaml
@@ -7,6 +7,10 @@ settings:
 importers:
 
   .:
+    dependencies:
+      es-toolkit:
+        specifier: ^1.37.2
+        version: 1.37.2
     devDependencies:
       '@babel/runtime':
         specifier: ^7.26.9
@@ -1467,6 +1471,9 @@ packages:
     resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
     engines: {node: '>=10.13.0'}
 
+  es-toolkit@1.37.2:
+    resolution: {integrity: sha512-ADDfk+pPFF0ofMpRAIc6on01p8heiuwuuJsYLzTP4UOjxVK9QsE2+0D1Q4J/zX2XBo6ac+27H5++YBIwmGAX/g==}
+
   esbuild@0.21.5:
     resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
     engines: {node: '>=12'}
@@ -4061,6 +4068,8 @@ snapshots:
       graceful-fs: 4.2.11
       tapable: 2.2.1
 
+  es-toolkit@1.37.2: {}
+
   esbuild@0.21.5:
     optionalDependencies:
       '@esbuild/aix-ppc64': 0.21.5
diff --git a/ui/user/src/lib/components/ModelProviderCard.svelte b/ui/user/src/lib/components/ModelProviderCard.svelte
new file mode 100644
index 0000000000..b60e92f212
--- /dev/null
+++ b/ui/user/src/lib/components/ModelProviderCard.svelte
@@ -0,0 +1,684 @@
+<script module>
+	function sortGptModels(models: string[]) {
+		console.log('gpt models....');
+		const query = 'gpt';
+
+		return [...models].sort((m1, m2) => {
+			const m1LowerCase = m1.toLowerCase();
+			const isM1StartWithQuery = m1LowerCase.startsWith(query);
+
+			// m1 does not starts with query move it out
+			if (!isM1StartWithQuery) return 1;
+
+			const m2LowerCase = m2.toLowerCase();
+			const isM2StartsWithQuery = m2LowerCase.startsWith(query);
+
+			// Both starts with query; local compare
+			if (isM1StartWithQuery && isM2StartsWithQuery) {
+				return m1LowerCase.localeCompare(m2LowerCase);
+			}
+
+			// move m1 in
+			return -1;
+		});
+	}
+</script>
+
+<script lang="ts">
+	import type { ModelProvider, Project } from '$lib/services';
+	import { CheckCircleIcon, Loader2, Search } from 'lucide-svelte';
+	import { darkMode } from '$lib/stores';
+	import {
+		updateProject,
+		listAvailableModels,
+		configureModelProvider,
+		deconfigureModelProvider,
+		getModelProviderConfig
+	} from '$lib/services/chat/operations';
+	import { twMerge } from 'tailwind-merge';
+	import { fade, slide } from 'svelte/transition';
+	import { delay, throttle } from 'es-toolkit';
+	import { untrack } from 'svelte';
+	import Confirm from './Confirm.svelte';
+
+	type Props = {
+		provider: ModelProvider;
+		project: Project;
+		configuringProvider?: string | null;
+		onError?: (error: string) => void;
+	};
+
+	let {
+		provider,
+		project = $bindable(),
+		configuringProvider = $bindable(),
+		onError
+	}: Props = $props();
+
+	let models: string[] = $state([]);
+
+	let selectedModels: string[] = $derived(project.models?.[provider.id] ?? []);
+	let error: string | null = $state(null);
+
+	let defaultModelProvider = $derived(project.defaultModelProvider || '');
+	let defaultModel = $derived(project.defaultModel || '');
+
+	let isConfigured = $derived(provider?.configured ?? false);
+	let providerId = $derived(provider.id);
+	// let selectedModels = $derived<Record<string, string[]>>(project.models || {});
+
+	let configuration: Record<string, string> = $state({});
+	let oldConfiguration: Record<string, string> = $state(structuredClone({}));
+
+	let isConfigurationLoading = $state(false);
+	let isModelsLoading = $state(false);
+	let isSaving = $state(false);
+	let isProviderConfigurationShown = $state(false);
+	let isUnconfigureProviderDialogShown = $state(false);
+
+	let modelQuery: string = $state('');
+	let filteredModels: string[] = $state([]);
+
+	// Throttle search model for better performance
+	const searchModels = throttle((query: string, models: string[] = []) => {
+		// Filter out items and keep those include the query text
+		// Sort results where items that starts with the query text goes first
+		filteredModels = [
+			...models.filter((model) => model.trim().toLowerCase().includes(query.trim().toLowerCase()))
+		].sort((m1, m2) => {
+			const m1LowerCase = m1.toLowerCase();
+			const isM1StartWithQuery = m1LowerCase.startsWith(query);
+
+			// m1 does not starts with query move it out
+			if (!isM1StartWithQuery) return 1;
+
+			const m2LowerCase = m2.toLowerCase();
+			const isM2StartsWithQuery = m2LowerCase.startsWith(query);
+
+			// Both starts with query; local compare
+			if (isM1StartWithQuery && isM2StartsWithQuery) {
+				return m1LowerCase.localeCompare(m2LowerCase);
+			}
+
+			// move m1 in
+			return -1;
+		});
+	}, 100);
+
+	$effect(() => {
+		if (providerId.startsWith('openai-model-provider') && !modelQuery) {
+			filteredModels = sortGptModels(models);
+		} else {
+			searchModels(modelQuery, models);
+		}
+	});
+
+	// When a provider is selected for configuration, load its config
+	$effect(() => {
+		if (isConfigured) {
+			untrack(async () => {
+				try {
+					await loadProviderConfig(providerId);
+
+					await delay(1000);
+
+					models = await loadAvailableModels(providerId);
+				} catch (err) {
+					console.error(err);
+				}
+			});
+		} else {
+			models = [];
+		}
+	});
+
+	// Deconfigure model provider
+	async function handleDeconfigureModelProvider(provider: ModelProvider) {
+		try {
+			// Unselect all model first
+			await unselectModels(selectedModels);
+			// Unconfigure model provider
+			await deconfigureModelProvider(project.assistantID, project.id, provider.id);
+
+			models = [];
+			provider.configured = false;
+			delete project.models?.[provider.id];
+
+			// clear current configurations
+			configuration = {};
+			// clear previous configurations
+			oldConfiguration = {};
+		} catch (err) {
+			console.error(`Failed to deconfigure ${provider.name}`, err);
+		}
+	}
+
+	type UpdateProjectParams = {
+		models: Project['models'];
+		defaultModel?: Project['defaultModel'];
+		defaultModelProvider?: Project['defaultModelProvider'];
+	};
+	// Update the project's model selection directly and trigger auto-save
+	function updateProject_(params: UpdateProjectParams) {
+		project = {
+			...project,
+			...params
+		};
+
+		saveChanges();
+	}
+
+	// Load configuration for a model provider
+	async function loadProviderConfig(providerId: string) {
+		try {
+			configuration = await getModelProviderConfig(project.assistantID, project.id, providerId);
+			oldConfiguration = structuredClone($state.snapshot(configuration));
+		} catch (err) {
+			console.error(`Failed to get configuration for provider ${providerId}`, err);
+		} finally {
+			isConfigurationLoading = false;
+		}
+	}
+
+	// Configure model provider
+	async function handleConfigureModelProvider(
+		provider: ModelProvider,
+		config: Record<string, string>
+	) {
+		try {
+			await configureModelProvider(project.assistantID, project.id, provider.id, config);
+			const newProject = setProjectModels(project, providerId, []);
+
+			await updateProject(newProject);
+
+			project = newProject;
+		} catch (err) {
+			onError?.((error = `Failed to configure ${provider.name}`));
+
+			console.error(error, err);
+		}
+	}
+
+	// Load available models for a provider
+	async function loadAvailableModels(providerId: string) {
+		isModelsLoading = true;
+
+		try {
+			const response = await listAvailableModels(project.assistantID, project.id, providerId);
+
+			return (response.data || [])
+				.filter((m) => m.metadata && m.metadata.usage === 'llm')
+				.map((m) => m.id)
+				.sort((a, b) => a.localeCompare(b));
+		} catch (err) {
+			console.error(`Failed to load models for provider ${providerId}`, err);
+			return [];
+		} finally {
+			await delay(500);
+			isModelsLoading = false;
+		}
+	}
+
+	async function saveHandler(ev: Event) {
+		ev.preventDefault();
+
+		try {
+			isModelsLoading = true;
+			await handleConfigureModelProvider(provider, configuration);
+
+			provider.configured = true;
+
+			await delay(400);
+
+			const array = await loadAvailableModels(provider.id);
+
+			isProviderConfigurationShown = false;
+
+			await delay(300);
+
+			models = array;
+		} catch (err) {
+			console.log(err);
+			provider.configured = false;
+		} finally {
+			isModelsLoading = false;
+		}
+	}
+
+	// Toggle model selection
+	function toggleModel(model: string, isChecked: boolean) {
+		// Take a snapshot of current models
+		let projectModels = { ...(project.models || {}) };
+		const currentProviderModels = new Set([...(projectModels[provider.id] || [])]);
+
+		if (isChecked) {
+			currentProviderModels.add(model);
+		} else {
+			currentProviderModels.delete(model);
+		}
+
+		let p = project;
+
+		if (!isChecked && defaultModelProvider === provider.id) {
+			p = setProjectDefaultProvider(p);
+		}
+
+		if (!isChecked && defaultModel === model) {
+			p = setProjectDefaultModel(p, model);
+		}
+
+		p = setProjectModels(p, provider.id, currentProviderModels.values().toArray());
+
+		project = p;
+	}
+
+	function selectModels(models: string[]) {
+		// Take a snapshot of current models
+		let projectModels = { ...(project.models || {}) };
+
+		projectModels = {
+			...projectModels,
+			[provider.id]: models
+		};
+
+		return updateProject_({ models: projectModels });
+	}
+
+	function unselectModels(models: string[]) {
+		// Take a snapshot of current models
+		let projectModels = { ...(project.models || {}) };
+
+		const array = new Set(projectModels[providerId] ?? []);
+
+		for (const model of models) {
+			array.delete(model);
+		}
+
+		projectModels = {
+			...projectModels,
+			[provider.id]: array.values().toArray()
+		};
+
+		return updateProject_({ models: projectModels });
+	}
+
+	function setProjectDefaultProvider(project: Project, providerId?: string) {
+		const clone = structuredClone($state.snapshot(project));
+
+		if (provider) {
+			clone.defaultModelProvider = providerId;
+		} else {
+			delete clone.defaultModelProvider;
+		}
+
+		return clone;
+	}
+
+	function setProjectDefaultModel(project: Project, modelId: string) {
+		const clone = structuredClone($state.snapshot(project));
+
+		if (provider) {
+			clone.defaultModel = modelId;
+		} else {
+			delete clone.defaultModel;
+		}
+
+		return clone;
+	}
+
+	function setProjectModels(project: Project, providerId: string, models: string[]) {
+		const clone = structuredClone($state.snapshot(project));
+
+		if (!clone.models) {
+			clone.models = {};
+		}
+
+		if (!models) {
+			delete clone.models[providerId];
+		} else {
+			clone.models[providerId] = models;
+		}
+
+		return clone;
+	}
+
+	// Save changes to the server
+	async function saveChanges() {
+		try {
+			isSaving = true;
+			// isSaving = true;
+			// Update the project prop directly
+			project = await updateProject(project);
+		} catch (err) {
+			console.error('Failed to save model configuration', err);
+			error = 'Failed to save changes. Please try again.';
+		} finally {
+			await delay(500);
+			isSaving = false;
+		}
+	}
+
+	function onConfigureProviderClickHandler() {
+		configuringProvider = provider.id;
+		isProviderConfigurationShown = true;
+	}
+</script>
+
+<div
+	class="model-provider-card border-surface2 flex max-h-[514px] min-h-fit w-full flex-col rounded-md border py-4 shadow-sm 2xl:mb-4 2xl:min-h-[514px] 2xl:last:mb-0"
+	data-provider-id={provider.id}
+>
+	<div class="flex flex-col px-4">
+		<div class="mb-2 flex items-center gap-2">
+			{#if provider.icon || provider.iconDark}
+				<img
+					src={darkMode.isDark && provider.iconDark ? provider.iconDark : provider.icon}
+					alt={provider.name}
+					class="h-6 w-6 {darkMode.isDark && !provider.iconDark ? 'dark:invert' : ''}"
+				/>
+			{/if}
+			<div class="flex items-center gap-4">
+				<h4 class="truncate text-lg font-medium">{provider.name}</h4>
+				{#if [isModelsLoading, isSaving, isConfigurationLoading].some(Boolean)}
+					<div
+						class="flex justify-center"
+						in:fade={{ duration: 1000 }}
+						out:fade={{ duration: 600 }}
+					>
+						<Loader2 class="size-5 animate-spin" />
+					</div>
+				{/if}
+			</div>
+
+			{#if provider.configured}
+				<CheckCircleIcon class="text-blue ml-auto aspect-square h-5" />
+			{/if}
+		</div>
+	</div>
+
+	<div class="flex flex-1 flex-col gap-4 px-4 pt-4 pb-4">
+		{#if isConfigured}
+			<!-- Models Selection Section -->
+			<div
+				class={twMerge(
+					'bg-surface1/0 flex flex-1 flex-col gap-2 rounded-md',
+					isProviderConfigurationShown && 'pointer-events-none opacity-50'
+				)}
+				in:fade={{ duration: 200 }}
+				out:fade={{ duration: 0 }}
+			>
+				<div
+					class="model-provider-search-input bg-surface1 dark:bg-surface2 relative flex h-10 w-full items-center gap-2 rounded-md text-sm"
+				>
+					<div class="absolute inset-x-0 left-2 aspect-square h-5 opacity-50">
+						<Search class="h-full" />
+					</div>
+					<input
+						bind:value={modelQuery}
+						class="h-full w-full bg-transparent px-3 pr-8 pl-9"
+						type="text"
+						placeholder="Search your model here..."
+					/>
+
+					{#if filteredModels.length !== models.length}
+						<div
+							class="absolute inset-y-0 right-4 flex h-full items-center justify-center text-sm font-medium opacity-50"
+							transition:fade={{ duration: 100 }}
+						>
+							<div>{filteredModels.length}</div>
+						</div>
+					{/if}
+				</div>
+
+				<div class="border-surface1 flex flex-1 flex-col gap-2 rounded-md border">
+					<div class="flex items-center gap-2 px-4 py-2 text-sm font-medium">
+						<h5 class="flex items-center">
+							<input
+								class="bg-surface3 mr-2 h-4 w-4"
+								type="checkbox"
+								id={`model-${provider.id}-toggle-all`}
+								bind:checked={
+									() => models.length > 0 && models.length === selectedModels.length,
+									(v) => {
+										const indeterminate =
+											selectedModels.length > 0 && models.length > selectedModels.length;
+
+										if (indeterminate || v) {
+											selectModels(models);
+										} else {
+											unselectModels(selectedModels);
+										}
+									}
+								}
+								indeterminate={selectedModels.length > 0 && models.length > selectedModels.length}
+							/>
+							<label class="inline" for={`model-${provider.id}-toggle-all`}>Available Models</label>
+
+							{#if models.length}
+								<span class="opacity-50">({models.length})</span>
+							{/if}
+						</h5>
+
+						{#if selectedModels.length}
+							<div class="h-full border-l"></div>
+							<button class="inline font-normal">
+								<span class="">Selected</span>
+								<span class="opacity-50">({selectedModels.length})</span>
+							</button>
+						{/if}
+					</div>
+
+					<div class="relative flex flex-1 flex-col">
+						<div
+							class="default-scrollbar-thin scrollbar-track-rounded-full absolute inset-0 flex h-full max-h-full flex-col overflow-y-auto px-2"
+						>
+							{#each filteredModels as model (model)}
+								<div class="hover:bg-surface1 bored flex items-center rounded px-2 py-2">
+									<input
+										class="bg-surface3 mr-2 h-4 w-4"
+										type="checkbox"
+										id={`model-${provider.id}-${model}`}
+										bind:checked={
+											() => (selectedModels ?? []).includes(model),
+											(checked) => toggleModel(model, checked)
+										}
+									/>
+
+									<label
+										for={`model-${provider.id}-${model}`}
+										class="flex-1 cursor-pointer truncate text-sm select-none"
+									>
+										{model}
+									</label>
+								</div>
+							{:else}
+								<div
+									class="w-full h-full flex items-center justify-center text-gray-400 text-lg font-semibold rounded-lg absolute inset-0 p-8"
+									transition:fade={{ duration: 100 }}
+								>
+									{#if isModelsLoading}
+										<p>Loading models...</p>
+									{:else}
+										<p>No model is available</p>
+									{/if}
+								</div>
+							{/each}
+						</div>
+					</div>
+				</div>
+			</div>
+		{:else}
+			<!-- TODO: this is an example placeholder, feel free to change if you have a better one -->
+			<div
+				class="bg-surface1 flex flex-1 flex-col items-center justify-center rounded-lg p-8"
+				in:fade={{ duration: 200 }}
+				out:fade={{ duration: 0 }}
+			>
+				<div class="mb-2 text-lg font-semibold text-gray-400">Provider is not yet configured</div>
+				<p class="text-center text-xs opacity-50">
+					Click on the "Configure" button below to set up this provider. We’ll then validate your
+					configuration and display available models.
+				</p>
+			</div>
+		{/if}
+
+		{#if isProviderConfigurationShown}
+			<div transition:slide={{ duration: 200 }} class="flex flex-col gap-2">
+				<div class="flex items-center gap-2">
+					<div class="flex items-center font-medium">
+						<span>Configurations</span>
+					</div>
+					<div class="border-surface2 flex-1 border-b"></div>
+				</div>
+
+				<div class="flex flex-col gap-4">
+					{#each provider.requiredConfigurationParameters || [] as param}
+						<div class="flex w-full flex-col gap-1">
+							<label class="mb-1 block text-sm font-medium" for={param.name}>
+								{param.friendlyName || param.name}
+								{#if param.description}
+									<span class="text-muted text-xs">({param.description})</span>
+								{/if}
+							</label>
+
+							<input
+								type={param.sensitive ? 'password' : 'text'}
+								id={param.name}
+								class="w-full rounded-md border p-2 text-sm"
+								bind:value={configuration[param.name]}
+								required
+							/>
+						</div>
+					{/each}
+				</div>
+			</div>
+		{/if}
+	</div>
+
+	<div class="border-surface2 flex min-h-14 w-full justify-between gap-4 border-t px-4 pt-4">
+		{#if isProviderConfigurationShown}
+			{@const isDirty =
+				JSON.stringify($state.snapshot(configuration)) !==
+				JSON.stringify($state.snapshot(oldConfiguration))}
+
+			<div class="flex gap-2">
+				{#if isDirty}
+					<button
+						in:fade={{ duration: 100 }}
+						out:fade={{ duration: 0 }}
+						type="button"
+						class="button hover:bg-surface1 rounded-full px-4 py-2 text-sm transition-colors duration-100"
+						onclick={() => {
+							configuration = $state.snapshot(oldConfiguration);
+						}}
+					>
+						Reset
+					</button>
+				{:else}
+					<button
+						in:fade={{ duration: 100 }}
+						out:fade={{ duration: 0 }}
+						type="button"
+						class={twMerge(
+							'button hover:bg-surface1 rounded-full px-4 py-2 text-sm transition-colors duration-100'
+						)}
+						onclick={() => {
+							// Close configuration UI
+							isProviderConfigurationShown = false;
+						}}
+					>
+						Cancel
+					</button>
+				{/if}
+
+				{#if isDirty}
+					<button
+						in:fade={{ duration: 100 }}
+						out:fade={{ duration: 0 }}
+						type="button"
+						class="button bg-blue/10 text-blue hover:bg-blue/15 active:bg-blue/20 rounded-full border-none px-4 py-2 text-sm transition-colors duration-100"
+						disabled={!isDirty}
+						onclick={(ev) => saveHandler(ev)}
+					>
+						Save
+					</button>
+				{/if}
+			</div>
+		{/if}
+
+		<div class="ml-auto flex gap-2">
+			{#if !isProviderConfigurationShown}
+				<button
+					class="button hover:bg-surface3/80 active:bg-surface3/100 bg-transparent text-sm font-medium"
+					onclick={onConfigureProviderClickHandler}
+				>
+					{provider.configured ? 'Reconfigure' : 'Configure'}
+				</button>
+			{/if}
+
+			{#if provider.configured}
+				<button
+					class="button bg-red-500/5 text-sm font-medium text-red-500 transition-colors duration-100 hover:bg-red-500/10 active:bg-red-500/15"
+					onclick={() => {
+						isUnconfigureProviderDialogShown = true;
+					}}
+				>
+					Unconfigure
+				</button>
+			{/if}
+		</div>
+	</div>
+</div>
+
+<Confirm
+	show={isUnconfigureProviderDialogShown}
+	msg={`Are you sure you want to unconfigure ${provider.name} models provider?`}
+	onsuccess={async () => {
+		await handleDeconfigureModelProvider(provider);
+		isUnconfigureProviderDialogShown = false;
+	}}
+	oncancel={() => (isUnconfigureProviderDialogShown = false)}
+/>
+
+<style>
+	.model-provider-card {
+		break-inside: avoid;
+	}
+	.markdown-content :global(a) {
+		text-decoration: underline;
+		font-weight: 500;
+	}
+	.markdown-content :global(a:hover) {
+		opacity: 0.8;
+	}
+	.markdown-content :global(p) {
+		margin-bottom: 0.5rem;
+	}
+	.markdown-content :global(ul),
+	.markdown-content :global(ol) {
+		margin-left: 1.5rem;
+		margin-bottom: 0.5rem;
+	}
+	.markdown-content :global(ul) {
+		list-style-type: disc;
+	}
+	.markdown-content :global(ol) {
+		list-style-type: decimal;
+	}
+	.markdown-content :global(code) {
+		font-family: monospace;
+		background-color: rgba(0, 0, 0, 0.1);
+		padding: 0.125rem 0.25rem;
+		border-radius: 0.25rem;
+	}
+	.markdown-content :global(h1),
+	.markdown-content :global(h2),
+	.markdown-content :global(h3),
+	.markdown-content :global(h4),
+	.markdown-content :global(h5),
+	.markdown-content :global(h6) {
+		font-weight: 600;
+		margin-top: 0.5rem;
+		margin-bottom: 0.5rem;
+	}
+</style>
diff --git a/ui/user/src/lib/components/ModelProviders.svelte b/ui/user/src/lib/components/ModelProviders.svelte
index 921ac482d3..7dccde8d08 100644
--- a/ui/user/src/lib/components/ModelProviders.svelte
+++ b/ui/user/src/lib/components/ModelProviders.svelte
@@ -4,23 +4,17 @@
 	import {
 		updateProject,
 		listAvailableModels,
-		listModelProviders,
-		configureModelProvider,
-		deconfigureModelProvider,
-		getModelProviderConfig
+		listModelProviders
 	} from '$lib/services/chat/operations';
-	import { CheckCircleIcon, CircleIcon, Loader2 } from 'lucide-svelte';
-	import { toHTMLFromMarkdown } from '$lib/markdown';
-	import { darkMode } from '$lib/stores';
+	import { ChevronDown, Loader2 } from 'lucide-svelte';
+	import ModelProviderCard from './ModelProviderCard.svelte';
+	import { twMerge } from 'tailwind-merge';
 
 	let { project = $bindable() }: { project: Project } = $props();
 
 	let modelProviders: ModelProvider[] = $state([]);
 	let isLoading = $state(true);
 	let error: string | null = $state(null);
-	let configuringProvider: string | null = $state(null);
-	let configFormData: Record<string, string> = $state({});
-	let configIsLoading = $state(true);
 	let isSaving = $state(false);
 
 	// Convenience getters for derived values
@@ -32,6 +26,12 @@
 	let availableModels: Record<string, string[]> = $state({});
 	let loadingModels: Record<string, boolean> = $state({});
 
+	let isDefaultModelSelectOpen = $state(false);
+
+	const hasOneModelSelected = $derived(
+		Object.values(selectedModels).some((provider) => provider.length)
+	);
+
 	// Load model providers
 	async function loadModelProviders() {
 		isLoading = true;
@@ -94,93 +94,6 @@
 		}
 	}
 
-	// Configure model provider
-	async function handleConfigureModelProvider(
-		provider: ModelProvider,
-		config: Record<string, string>
-	) {
-		try {
-			await configureModelProvider(project.assistantID, project.id, provider.id, config);
-			configuringProvider = null;
-			await loadModelProviders();
-
-			// Load models for newly configured provider
-			loadAvailableModels(provider.id);
-		} catch (err) {
-			console.error(`Failed to configure ${provider.name}`, err);
-		}
-	}
-
-	// Deconfigure model provider
-	async function handleDeconfigureModelProvider(provider: ModelProvider) {
-		try {
-			await deconfigureModelProvider(project.assistantID, project.id, provider.id);
-
-			// Update the project model list directly
-			const updatedModels = { ...(project.models || {}) };
-			delete updatedModels[provider.id];
-
-			// Handle default model updates
-			let updatedDefaultProvider = defaultModelProvider;
-			let updatedDefaultModel = defaultModel;
-
-			if (defaultModelProvider === provider.id) {
-				updatedDefaultProvider = '';
-				updatedDefaultModel = '';
-			}
-
-			// Update the project directly
-			updateProjectModels(updatedModels, updatedDefaultModel, updatedDefaultProvider);
-
-			// Refresh providers list
-			await loadModelProviders();
-		} catch (err) {
-			console.error(`Failed to deconfigure ${provider.name}`, err);
-		}
-	}
-
-	// Toggle model selection
-	function toggleModelSelection(providerId: string, modelName: string) {
-		// Take a snapshot of current models
-		const currentModels = { ...(project.models || {}) };
-		const currentProviderModels = [...(currentModels[providerId] || [])];
-
-		const isSelected = currentProviderModels.includes(modelName);
-
-		let updatedProviderModels;
-		if (isSelected) {
-			// Remove model
-			updatedProviderModels = currentProviderModels.filter((m) => m !== modelName);
-		} else {
-			// Add model
-			updatedProviderModels = [...currentProviderModels, modelName];
-		}
-
-		// Update models
-		const updatedModels = {
-			...currentModels,
-			[providerId]: updatedProviderModels
-		};
-
-		// If there are no models for this provider, remove the provider entry
-		if (updatedProviderModels.length === 0) {
-			delete updatedModels[providerId];
-		}
-
-		// Check if we're removing the default model
-		let updatedDefaultModel = defaultModel;
-		let updatedDefaultProvider = defaultModelProvider;
-
-		if (defaultModelProvider === providerId && defaultModel === modelName && isSelected) {
-			// We're removing the current default model, so clear it
-			updatedDefaultModel = '';
-			updatedDefaultProvider = '';
-		}
-
-		// Update the project directly and trigger auto-save
-		updateProjectModels(updatedModels, updatedDefaultModel, updatedDefaultProvider);
-	}
-
 	// Update the project's model selection directly and trigger auto-save
 	function updateProjectModels(
 		models: Record<string, string[]>,
@@ -212,37 +125,6 @@
 		}
 	}
 
-	// Load configuration for a model provider
-	async function loadProviderConfig(providerId: string) {
-		configIsLoading = true;
-		configFormData = {};
-
-		try {
-			const data = await getModelProviderConfig(project.assistantID, project.id, providerId);
-			configFormData = data || {};
-		} catch (err) {
-			console.error(`Failed to get configuration for provider ${providerId}`, err);
-		} finally {
-			configIsLoading = false;
-		}
-	}
-
-	function handleFormSubmit(provider: ModelProvider, event: Event) {
-		event.preventDefault();
-		handleConfigureModelProvider(provider, configFormData);
-	}
-
-	function updateFormValue(key: string, value: string) {
-		configFormData = { ...configFormData, [key]: value };
-	}
-
-	// When a provider is selected for configuration, load its config
-	$effect(() => {
-		if (configuringProvider) {
-			loadProviderConfig(configuringProvider);
-		}
-	});
-
 	onMount(() => {
 		if (project && project.id) {
 			loadModelProviders();
@@ -250,9 +132,15 @@
 	});
 </script>
 
-<div class="px-4 pt-0 pb-4">
+<div class="flex w-full flex-col pt-10 pb-10">
 	<div class="mb-4 flex items-center justify-between">
-		<h3 class="text-lg font-semibold">Model Providers</h3>
+		<div class="flex flex-col">
+			<h3 class="text-lg font-semibold">Model Providers</h3>
+			<p class="text-gray text-xs">
+				Configure model providers and select models to make them available to all threads and tasks.
+				The default model will be used in place of Obot’s built-in model.
+			</p>
+		</div>
 
 		{#if isSaving}
 			<div class="text-muted flex items-center gap-1 text-sm">
@@ -282,189 +170,59 @@
 		<div class="text-muted">No model providers available</div>
 	{:else}
 		<!-- Default Model Provider Section -->
-		<div class="mb-6">
-			<h4 class="text-md mb-2 font-medium">Default Model</h4>
-			<p class="text-muted mb-3 text-sm">
-				Select the default model for this project. This will be used when no specific model is
-				specified.
-			</p>
-
-			{#if !Object.keys(selectedModels).length}
-				<div class="mb-2 text-sm text-yellow-500">
-					Configure at least one provider and select models to set a default model.
-				</div>
-			{:else}
-				<div>
-					<select
-						class="w-full rounded-md border p-2 text-sm"
-						value={defaultModelProvider && defaultModel
-							? `${defaultModel}|||${defaultModelProvider}`
-							: ''}
-						onchange={(e) => updateDefaultModel(e.currentTarget.value)}
+		<div class={twMerge('mb-6 flex flex-col gap-2', !hasOneModelSelected && 'opacity-50')}>
+			<h4 class="text-md font-medium">Default Model</h4>
+
+			<!-- Not the best approach -->
+			<!-- TODO: Use a 3rd-party UI library or create an internal custom component in the future -->
+			<div class="relative">
+				<select
+					class="border-surface2 dark:bg-surface1 w-full appearance-none rounded-md border px-4 py-4 text-sm"
+					value={defaultModelProvider && defaultModel
+						? `${defaultModel}|||${defaultModelProvider}`
+						: ''}
+					onchange={(e) => updateDefaultModel(e.currentTarget.value)}
+					onclick={() => (isDefaultModelSelectOpen = !isDefaultModelSelectOpen)}
+				>
+					<option class="dark:bg-surface3" value="" disabled
+						>Select the default model for this agent</option
 					>
-						<option value="">Select a default model</option>
+					{#if hasOneModelSelected}
 						{#each Object.entries(selectedModels) as [providerId, models]}
 							{#each models as model}
 								{#if providerId}
 									{@const provider = modelProviders.find((p) => p.id === providerId)}
+
 									{#if provider}
-										<option value={`${model}|||${providerId}`}>
+										<option class="dark:bg-surface3" value={`${model}|||${providerId}`}>
 											{model} ({provider.name})
 										</option>
 									{/if}
 								{/if}
 							{/each}
 						{/each}
-					</select>
+					{/if}
+				</select>
+
+				<div
+					class={twMerge(
+						'absolute inset-y-0 right-0 flex aspect-square h-full items-center justify-center transition-transform duration-200',
+						isDefaultModelSelectOpen && 'rotate-180'
+					)}
+				>
+					<ChevronDown />
 				</div>
-			{/if}
+			</div>
 		</div>
 
 		<!-- Available Model Providers -->
-		<h4 class="text-md mb-2 font-medium">Available Model Providers</h4>
-		<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
-			{#each modelProviders as provider}
-				<div class="flex flex-col gap-2 rounded-md border p-4">
-					<div class="flex items-center gap-2">
-						{#if provider.icon || provider.iconDark}
-							<img
-								src={darkMode.isDark && provider.iconDark ? provider.iconDark : provider.icon}
-								alt={provider.name}
-								class="h-6 w-6 {darkMode.isDark && !provider.iconDark ? 'dark:invert' : ''}"
-							/>
-						{/if}
-						<h4 class="font-medium">{provider.name}</h4>
-					</div>
-
-					{#if provider.description}
-						<div class="markdown-content text-muted mb-2 text-sm">
-							{@html toHTMLFromMarkdown(provider.description)}
-						</div>
-					{/if}
-
-					<div class="mt-1 text-sm">
-						{#if provider.configured}
-							<span class="flex items-center gap-1 text-green-500">
-								<CheckCircleIcon class="h-4 w-4" />
-								Configured
-							</span>
-						{:else}
-							<span class="flex items-center gap-1 text-yellow-500">
-								<CircleIcon class="h-4 w-4" />
-								Not Configured
-							</span>
-						{/if}
-					</div>
-
-					{#if provider.configured && !configuringProvider}
-						<!-- Models Selection Section -->
-						<div class="mt-2">
-							<h5 class="mb-2 text-sm font-medium">Available Models</h5>
-
-							{#if loadingModels[provider.id]}
-								<div class="flex justify-center p-2">
-									<Loader2 class="size-6 animate-spin" />
-								</div>
-							{:else if availableModels[provider.id]?.length > 0}
-								<div class="max-h-48 overflow-y-auto rounded-md border p-2">
-									{#each availableModels[provider.id] as model}
-										<div class="hover:bg-surface1 flex items-center rounded px-1 py-1.5">
-											<input
-												type="checkbox"
-												id={`model-${provider.id}-${model}`}
-												checked={(selectedModels[provider.id] || []).includes(model)}
-												onchange={() => toggleModelSelection(provider.id, model)}
-												class="mr-2 h-4 w-4"
-											/>
-											<label
-												for={`model-${provider.id}-${model}`}
-												class="flex-1 cursor-pointer text-sm select-none"
-											>
-												{model}
-												{#if defaultModelProvider === provider.id && defaultModel === model}
-													<span class="text-primary ml-2 text-xs font-medium">(Default Model)</span>
-												{/if}
-											</label>
-										</div>
-									{/each}
-								</div>
-							{:else}
-								<p class="text-muted text-sm">No models available for this provider.</p>
-							{/if}
-						</div>
-					{/if}
-
-					<div class="mt-auto pt-2">
-						{#if configuringProvider === provider.id}
-							<!-- Configuration Form -->
-							{#if configIsLoading}
-								<div class="flex justify-center p-4">
-									<Loader2 class="h-4 w-4 animate-spin" />
-								</div>
-							{:else}
-								<form onsubmit={(e) => handleFormSubmit(provider, e)}>
-									{#each provider.requiredConfigurationParameters || [] as param}
-										<div class="mb-3">
-											<label class="mb-1 block text-sm font-medium" for={param.name}>
-												{param.friendlyName || param.name}
-												{#if param.description}
-													<span class="text-muted text-xs">({param.description})</span>
-												{/if}
-											</label>
-											<input
-												type={param.sensitive ? 'password' : 'text'}
-												id={param.name}
-												class="w-full rounded-md border p-2 text-sm"
-												value={configFormData[param.name] || ''}
-												oninput={(e) => updateFormValue(param.name, e.currentTarget.value)}
-												required
-											/>
-										</div>
-									{/each}
-
-									<div class="mt-4 flex gap-2">
-										<button
-											type="submit"
-											class="bg-primary text-primary-foreground flex-1 rounded-md px-4 py-2 text-sm font-medium"
-										>
-											Save
-										</button>
-										<button
-											type="button"
-											class="border-input flex-1 rounded-md border bg-transparent px-4 py-2 text-sm font-medium"
-											onclick={() => (configuringProvider = null)}
-										>
-											Cancel
-										</button>
-									</div>
-								</form>
-							{/if}
-						{:else}
-							<button
-								class="w-full px-4 py-2 {provider.configured
-									? 'bg-secondary text-secondary-foreground'
-									: 'bg-primary text-primary-foreground'} rounded-md text-sm font-medium"
-								onclick={() => (configuringProvider = provider.id)}
-							>
-								{provider.configured ? 'Modify' : 'Configure'}
-							</button>
-
-							{#if provider.configured}
-								<button
-									class="border-input mt-2 w-full rounded-md border bg-transparent px-4 py-2 text-sm font-medium"
-									onclick={() => {
-										if (confirm(`Are you sure you want to deconfigure ${provider.name}?`)) {
-											handleDeconfigureModelProvider(provider);
-										}
-									}}
-								>
-									Deconfigure
-								</button>
-							{/if}
-						{/if}
-					</div>
-				</div>
-			{/each}
+		<div class="flex flex-col gap-0">
+			<h4 class="text-md mb-2 font-medium">Available Model Providers</h4>
+			<div class="model-providers-cards flex flex-col gap-4 2xl:grid 2xl:grid-cols-2">
+				{#each modelProviders as provider (provider.id)}
+					<ModelProviderCard {provider} bind:project />
+				{/each}
+			</div>
 		</div>
 	{/if}
 </div>
diff --git a/ui/user/src/lib/components/SidebarConfig.svelte b/ui/user/src/lib/components/SidebarConfig.svelte
index 194c8eb18b..fcc4cf4024 100644
--- a/ui/user/src/lib/components/SidebarConfig.svelte
+++ b/ui/user/src/lib/components/SidebarConfig.svelte
@@ -39,7 +39,10 @@
 	});
 </script>
 
-<div class="default-scrollbar-thin relative flex w-full justify-center overflow-y-auto" in:fade>
+<div
+	class="default-scrollbar-thin relative flex w-full justify-center overflow-y-auto px-4 lg:px-32"
+	in:fade
+>
 	{#if layout.sidebarConfig === 'slack'}
 		<Slack {project} />
 	{:else if layout.sidebarConfig === 'invitations'}
diff --git a/ui/user/src/lib/components/edit/ThreadModelSelector.svelte b/ui/user/src/lib/components/edit/ThreadModelSelector.svelte
index 8efde112ba..654f534b8c 100644
--- a/ui/user/src/lib/components/edit/ThreadModelSelector.svelte
+++ b/ui/user/src/lib/components/edit/ThreadModelSelector.svelte
@@ -4,6 +4,7 @@
 	import type { Thread as ThreadType } from '$lib/services/chat/types';
 	import type { Project } from '$lib/services';
 	import { getThread, updateThread, getDefaultModelForThread } from '$lib/services/chat/operations';
+	import { twMerge } from 'tailwind-merge';
 
 	interface Props {
 		threadId: string | undefined;
@@ -20,6 +21,12 @@
 	let modelButtonRef = $state<HTMLButtonElement>();
 	let defaultModel = $state<{ model: string; modelProvider: string } | null>(null);
 
+	let modelsEntries = $derived(Object.entries(project.models || {}));
+
+	const isDefaultModelSelected = $derived(
+		threadDetails && defaultModel?.model !== '' && defaultModel?.model === threadDetails?.model
+	);
+
 	// Function to fetch thread details including model
 	async function fetchThreadDetails() {
 		if (!threadId) return;
@@ -57,6 +64,7 @@
 		}
 
 		isUpdatingModel = true;
+
 		try {
 			const updatedThread = await updateThread(project.assistantID, project.id, {
 				...threadDetails,
@@ -125,9 +133,13 @@
 	});
 </script>
 
-<div class="relative pr-2">
+<!-- TODO: Refactor this to use a dropdown component either third-party in internally crafted -->
+<div class="relative mr-2 md:mr-6 lg:mr-8">
 	<button
-		class="hover:bg-surface1 focus:ring-blue flex items-center gap-1 rounded-md border border-[--color-border] bg-transparent px-2 py-1 text-sm focus:ring-1 focus:outline-none"
+		class={twMerge(
+			'hover:bg-surface2/50 active:bg-surface2/80 flex h-10 items-center gap-3 rounded-full px-2  py-1 text-xs text-gray-600 md:px-4 lg:px-6',
+			isDefaultModelSelected && 'text-blue hover:bg-blue/10 active:bg-blue/15 bg-transparent'
+		)}
 		onclick={(e) => {
 			e.stopPropagation();
 			showModelSelector = !showModelSelector;
@@ -136,20 +148,23 @@
 		aria-haspopup="listbox"
 		aria-expanded={showModelSelector}
 		id="thread-model-button"
-		title="Select model for this thread"
+		title={isDefaultModelSelected
+			? 'Default model is selected'
+			: threadDetails?.model
+				? ''
+				: 'Select model for this thread'}
 		bind:this={modelButtonRef}
 	>
-		<span>
+		<div class="max-w-40 truncate sm:max-w-60 md:max-w-96 lg:max-w-none">
 			{#if threadDetails?.modelProvider && threadDetails?.model}
 				{threadDetails.model}
 			{:else if defaultModel?.model && defaultModel.model !== ''}
-				Default ({defaultModel.model})
-			{:else if defaultModel?.model === '' && defaultModel?.modelProvider === ''}
-				No Default Model
+				{defaultModel.model}
 			{:else}
-				Default Model
+				No Default Model
 			{/if}
-		</span>
+		</div>
+
 		<ChevronDown class="h-4 w-4" />
 	</button>
 
@@ -158,7 +173,7 @@
 			role="listbox"
 			tabindex="-1"
 			aria-labelledby="thread-model-button"
-			class="absolute bottom-full z-10 mb-1 max-h-60 w-60 overflow-auto rounded-md border border-[--color-border] bg-white p-1 shadow-lg dark:bg-black"
+			class="available-models-popover default-scrollbar-thin border-surface1 dark:bg-surface2 absolute right-0 bottom-full z-10 mb-1 max-h-60 w-max max-w-sm overflow-hidden overflow-y-auto rounded-md border bg-white p-2 shadow-lg md:max-w-md lg:max-w-lg"
 			onclick={(e) => e.stopPropagation()}
 			onkeydown={(e) => {
 				if (e.key === 'Escape') {
@@ -168,55 +183,49 @@
 			}}
 			bind:this={modelSelectorRef}
 		>
-			<button
-				role="option"
-				aria-selected={!threadDetails?.modelProvider && !threadDetails?.model}
-				class="hover:bg-surface1 focus:bg-surface1 w-full rounded px-2 py-1.5 text-left text-sm focus:outline-none"
-				onclick={() => setThreadModel('', '')}
-				tabindex="0"
-				disabled={defaultModel?.model === '' && defaultModel?.modelProvider === ''}
-				class:opacity-50={defaultModel?.model === '' && defaultModel?.modelProvider === ''}
-			>
-				Default
-				{#if defaultModel?.model && defaultModel.model !== ''}
-					<span class="text-xs text-gray-500">({defaultModel.model})</span>
-				{:else if defaultModel?.model === '' && defaultModel?.modelProvider === ''}
-					<span class="text-xs text-red-500">(No default model available)</span>
-				{/if}
-				{#if !threadDetails?.modelProvider && !threadDetails?.model}
-					<span class="ml-1 text-xs text-green-500">✓</span>
-				{/if}
-			</button>
-
-			{#each Object.entries(project.models || {}) as [providerId, models]}
-				{#if Array.isArray(models) && models.length > 0 && providerId}
-					{#each models as model}
-						<button
-							role="option"
-							aria-selected={threadDetails?.modelProvider === providerId &&
-								threadDetails?.model === model}
-							class="hover:bg-surface1 focus:bg-surface1 w-full rounded px-2 py-1.5 text-left text-sm focus:outline-none"
-							onclick={() => setThreadModel(model, providerId)}
-							tabindex="0"
-						>
-							{model}
-							{#if threadDetails?.modelProvider === providerId && threadDetails?.model === model}
-								<span class="ml-1 text-xs text-green-500">✓</span>
-							{/if}
-						</button>
-					{/each}
+			{#if modelsEntries.length}
+				{#each modelsEntries as [providerId, models] (providerId)}
+					{#if Array.isArray(models) && models.length > 0 && providerId}
+						{#each models as model (model)}
+							<button
+								role="option"
+								aria-selected={threadDetails?.modelProvider === providerId &&
+									threadDetails?.model === model}
+								class={twMerge(
+									'hover:bg-surface1 focus:bg-surface1 w-full rounded px-2 py-1.5 text-left text-sm focus:outline-none',
+									defaultModel?.model === model && 'text-blue hover:bg-blue/10 active:bg-blue/15'
+								)}
+								onclick={() => setThreadModel(model, providerId)}
+								tabindex="0"
+							>
+								{model}
+								{#if threadDetails?.modelProvider === providerId && threadDetails?.model === model}
+									<span class="ml-1 text-xs text-green-500">✓</span>
+								{/if}
+							</button>
+						{/each}
+					{/if}
+				{/each}
+
+				{#if isUpdatingModel}
+					<div class="flex justify-center p-2">
+						<div
+							class="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"
+							aria-hidden="true"
+						></div>
+						<span class="sr-only">Loading...</span>
+					</div>
 				{/if}
-			{/each}
-
-			{#if isUpdatingModel}
-				<div class="flex justify-center p-2">
-					<div
-						class="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"
-						aria-hidden="true"
-					></div>
-					<span class="sr-only">Loading...</span>
-				</div>
+			{:else}
+				<p class="truncate text-sm text-gray-400">See "Configuration" for more options</p>
 			{/if}
 		</div>
 	{/if}
 </div>
+
+<style>
+	.available-models-popover {
+		display: grid;
+		grid-template-columns: minmax(fit-content, auto);
+	}
+</style>
diff --git a/ui/user/src/lib/components/sidebar/ModelProviders.svelte b/ui/user/src/lib/components/sidebar/ModelProviders.svelte
index 47a28e127c..e6039f3b1d 100644
--- a/ui/user/src/lib/components/sidebar/ModelProviders.svelte
+++ b/ui/user/src/lib/components/sidebar/ModelProviders.svelte
@@ -1,9 +1,14 @@
 <script lang="ts">
-	import { onMount } from 'svelte';
+	import { untrack } from 'svelte';
 	import { getLayout } from '$lib/context/layout.svelte';
 	import CollapsePane from '$lib/components/edit/CollapsePane.svelte';
 	import { HELPER_TEXTS } from '$lib/context/helperMode.svelte';
-	import { listModelProviders, type Project } from '$lib/services';
+	import { listModelProviders, type ModelProvider, type Project } from '$lib/services';
+	import { Info } from 'lucide-svelte';
+	import { SvelteMap } from 'svelte/reactivity';
+	import { darkMode } from '$lib/stores';
+	import { twMerge } from 'tailwind-merge';
+	import { delay } from 'es-toolkit';
 
 	interface Props {
 		project?: Project;
@@ -12,12 +17,16 @@
 	let { project }: Props = $props();
 	const layout = getLayout();
 
-	let providerNames = $state<Record<string, string>>({});
 	let isLoading = $state(false);
 
+	let projectProviderModelIds = $derived(
+		Object.keys(project?.models ?? {}).sort((a, b) => a.localeCompare(b))
+	);
+	let providersWithMoreData: Map<string, ModelProvider> = new SvelteMap();
+
 	// Get a friendly name for a provider
 	function getProviderName(providerId: string): string {
-		return providerNames[providerId] || providerId;
+		return providersWithMoreData.get(providerId)?.name || providerId;
 	}
 
 	// Function to open model providers config
@@ -25,35 +34,18 @@
 		layout.sidebarConfig = 'model-providers';
 	}
 
-	// Sort provider IDs by their display names
-	function getSortedProviderIds(providerIds: string[]): string[] {
-		return [...providerIds].sort((a, b) => {
-			const nameA = getProviderName(a);
-			const nameB = getProviderName(b);
-			return nameA.localeCompare(nameB);
-		});
-	}
-
 	// Function to fetch model providers
-	async function fetchModelProviders() {
-		if (!project || !project.assistantID || !project.id) {
-			return;
-		}
-
+	async function loadModelProviders(project: Project) {
 		isLoading = true;
-		try {
-			const providers = await listModelProviders(project.assistantID, project.id);
-			const names: Record<string, string> = {};
 
-			if (providers && providers.items) {
-				providers.items.forEach((provider) => {
-					if (provider.id && provider.name) {
-						names[provider.id] = provider.name;
+		try {
+			listModelProviders(project.assistantID, project.id).then((res) => {
+				untrack(() => {
+					for (const provider of res.items) {
+						providersWithMoreData.set(provider.id, provider);
 					}
 				});
-			}
-
-			providerNames = names;
+			});
 		} catch (error) {
 			console.error('Failed to load model providers:', error);
 		} finally {
@@ -61,16 +53,10 @@
 		}
 	}
 
-	onMount(() => {
-		if (project) {
-			fetchModelProviders();
-		}
-	});
-
 	$effect(() => {
-		if (project) {
-			fetchModelProviders();
-		}
+		if (!project || !project.assistantID || !project.id) return;
+
+		loadModelProviders(project);
 	});
 </script>
 
@@ -80,32 +66,77 @@
 	header="Model Providers"
 	helpText={HELPER_TEXTS.modelProviders}
 >
-	<div class="flex flex-col p-2">
-		<p class="pb-2 text-xs">
-			Agents use our included LLM by default. Connect other model providers to unlock more LLMs you
-			can chat with.
-		</p>
-
-		{#if isLoading}
-			<p class="text-xs italic">Loading model providers...</p>
-		{:else if project?.models && Object.keys(project.models).length > 0}
-			<div class="pb-2">
-				<p class="mb-1 text-xs font-medium">Configured providers:</p>
-				<ul class="space-y-0.5 pl-3 text-xs">
-					{#each getSortedProviderIds(Object.keys(project.models)) as providerId}
-						<li class="flex items-center gap-1">
-							<span>{getProviderName(providerId)}</span>
-							<span class="text-muted-foreground"
-								>({project.models[providerId].length}
-								{project.models[providerId].length === 1 ? 'model' : 'models'})</span
-							>
-						</li>
-					{/each}
-				</ul>
-			</div>
-		{:else}
-			<p class="text-xs">No model providers configured</p>
-		{/if}
+	<div class="flex flex-col gap-8 p-2">
+		<div class="flex flex-col gap-4">
+			<p class="text-xs font-light text-gray-500">
+				Agents use our included LLM by default. Connect other model providers to unlock more LLMs
+				you can chat with.
+			</p>
+
+			{#if isLoading}
+				<p class="text-xs italic">Loading model providers...</p>
+			{:else if project?.models && Object.keys(project.models).length > 0}
+				<div class="pb-2">
+					<p class="mb-1 text-sm font-medium">Configured providers:</p>
+					<ul class="flex flex-col text-xs">
+						{#each projectProviderModelIds as providerId}
+							{@const provider = providersWithMoreData.get(providerId)}
+
+							<li class="model-provider w-full">
+								<button
+									class="hover:bg-surface3 flex w-full items-center gap-1 rounded-md p-2 transition-colors duration-200"
+									onclick={async () => {
+										openModelProvidersConfig();
+
+										// Wait for the UI to be ready
+										await delay(300);
+
+										requestAnimationFrame(() => {
+											const cardElement = document.querySelector(
+												`.model-provider-card[data-provider-id="${providerId}"]`
+											);
+
+											if (cardElement) {
+												cardElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
+											}
+										});
+									}}
+								>
+									<div class="size-4">
+										{#if provider?.icon || provider?.iconDark}
+											<img
+												src={darkMode.isDark && provider.iconDark
+													? provider.iconDark
+													: provider.icon}
+												alt={provider.name}
+												class={twMerge(
+													'size-full',
+													darkMode.isDark && !provider.iconDark ? 'dark:invert' : ''
+												)}
+											/>
+										{/if}
+									</div>
+
+									<span>{getProviderName(providerId)}</span>
+									<span class="text-muted-foreground"
+										>({project.models[providerId].length}
+										{project.models[providerId].length === 1 ? 'model' : 'models'})</span
+									>
+								</button>
+							</li>
+						{/each}
+					</ul>
+				</div>
+			{:else}
+				<div class="text-gray flex items-center gap-2 rounded-lg py-4 text-xs">
+					<div>
+						<Info class="h-5" />
+					</div>
+
+					<div>No model providers configured</div>
+				</div>
+			{/if}
+		</div>
 
 		<div class="flex justify-end">
 			<button class="button flex items-center gap-1" onclick={openModelProvidersConfig}>


Reviews:
user: login: ivyjeong13
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T17:42:48Z
user: login: ivyjeong13
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T17:47:16Z
user: login: ivyjeong13
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T17:48:56Z
user: login: ivyjeong13
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T17:50:59Z
user: login: ivyjeong13
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T17:58:01Z
user: login: ivyjeong13
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T17:59:22Z
user: login: ryu-man
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T18:04:35Z
user: login: ryu-man
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T18:19:29Z
user: login: ryu-man
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T18:20:33Z
user: login: ryu-man
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T18:21:52Z
user: login: ryu-man
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T18:30:16Z
user: login: cjellick
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T21:06:59Z
user: login: cjellick
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T21:30:21Z

Review comments:
diff_hunk: @@ -73,5 +73,8 @@
 		"typescript-eslint": "^8.25.0",
 		"vite": "^5.4.14"
 	},
-	"type": "module"
+	"type": "module",
+	"dependencies": {
+		"es-toolkit": "^1.37.2"
path: ui/user/package.json
user: login: ivyjeong13
body: Wondering if es-toolkit is needed or if the imported util functions are simple enough for us to make (just again making sure, we avoid unnecessary dependencies unless the use case is complex that it is needed!)
created_at: 2025-05-13T17:42:48Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,629 @@
+<script lang="ts">
+	import type { ModelProvider, Project } from '$lib/services';
+	import { CheckCircleIcon, Loader2, Search } from 'lucide-svelte';
+	import { toHTMLFromMarkdown } from '$lib/markdown';
+	import { darkMode } from '$lib/stores';
+	import {
+		updateProject,
+		listAvailableModels,
+		configureModelProvider,
+		deconfigureModelProvider,
+		getModelProviderConfig
+	} from '$lib/services/chat/operations';
+	import { twMerge } from 'tailwind-merge';
+	import { fade, slide } from 'svelte/transition';
+	import { delay, throttle } from 'es-toolkit';
+	import { untrack } from 'svelte';
+
+	type Props = {
+		provider: ModelProvider;
+		project: Project;
+		configuringProvider?: string | null;
+		onError?: (error: string) => void;
+	};
+
+	let {
+		provider,
+		project = $bindable(),
+		configuringProvider = $bindable(),
+		onError
+	}: Props = $props();
+
+	let models: string[] = $state([]);
+
+	let selectedModels: string[] = $derived(project.models?.[provider.id] ?? []);
+	let error: string | null = $state(null);
+
+	let defaultModelProvider = $derived(project.defaultModelProvider || '');
+	let defaultModel = $derived(project.defaultModel || '');
+
+	let isConfigured = $derived(provider?.configured ?? false);
+	let providerId = $derived(provider.id);
+	// let selectedModels = $derived<Record<string, string[]>>(project.models || {});
+
+	let configuration: Record<string, string> = $state({});
+	// svelte-ignore state_referenced_locally
+	let oldConfiguration: Record<string, string> = $state(
+		structuredClone($state.snapshot(configuration))
+	);
+
+	let isConfigurationLoading = $state(false);
+	let isModelsLoading = $state(false);
+	let isSaving = $state(false);
+	let isProviderConfigurationShown = $state(false);
+
+	let modelQuery: string = $state('');
+	let filteredModels: string[] = $state([]);
+
+	// Throttle search model for better performance
+	const searchModels = throttle((query: string, models: string[] = []) => {
+		filteredModels = models.filter((model) =>
+			model.trim().toLowerCase().includes(query.trim().toLowerCase())
+		);
+	}, 100);
+
+	$effect(() => {
+		searchModels(modelQuery, models);
+	});
+
+	// When a provider is selected for configuration, load its config
+	$effect(() => {
+		if (isConfigured) {
+			untrack(async () => {
+				try {
+					await loadProviderConfig(providerId);
+					await delay(1000);
+					models = await loadAvailableModels(providerId);
+				} catch (err) {
+					console.error(err);
+				}
+			});
+		} else {
+			models = [];
+		}
+	});
+
+	// Deconfigure model provider
+	async function handleDeconfigureModelProvider(provider: ModelProvider) {
+		try {
+			// Unselect all model first
+			await unselectModels(selectedModels);
+			// Unconfigure model provider
+			await deconfigureModelProvider(project.assistantID, project.id, provider.id);
+
+			models = [];
+			provider.configured = false;
+			delete project.models?.[provider.id];
+
+			// clear current configurations
+			configuration = {};
+			// clear previous configurations
+			oldConfiguration = {};
+		} catch (err) {
+			console.error(`Failed to deconfigure ${provider.name}`, err);
+		}
+	}
+
+	type UpdateProjectParams = {
+		models: Project['models'];
+		defaultModel?: Project['defaultModel'];
+		defaultModelProvider?: Project['defaultModelProvider'];
+	};
+	// Update the project's model selection directly and trigger auto-save
+	function updateProject_(params: UpdateProjectParams) {
+		project = {
+			...project,
+			...params
+		};
+
+		saveChanges();
+	}
+
+	// Load configuration for a model provider
+	async function loadProviderConfig(providerId: string) {
+		try {
+			configuration = await getModelProviderConfig(project.assistantID, project.id, providerId);
+			oldConfiguration = structuredClone($state.snapshot(configuration));
+		} catch (err) {
+			console.error(`Failed to get configuration for provider ${providerId}`, err);
+		} finally {
+			isConfigurationLoading = false;
+		}
+	}
+
+	// Configure model provider
+	async function handleConfigureModelProvider(
+		provider: ModelProvider,
+		config: Record<string, string>
+	) {
+		try {
+			await configureModelProvider(project.assistantID, project.id, provider.id, config);
+			const newProject = setProjectModels(project, providerId, []);
+
+			await updateProject(newProject);
+
+			project = newProject;
+		} catch (err) {
+			onError?.((error = `Failed to configure ${provider.name}`));
+
+			console.error(error, err);
+		}
+	}
+
+	// Load available models for a provider
+	async function loadAvailableModels(providerId: string) {
+		isModelsLoading = true;
+
+		try {
+			const response = await listAvailableModels(project.assistantID, project.id, providerId);
+
+			return (response.data || [])
+				.filter((m) => m.metadata && m.metadata.usage === 'llm')
+				.map((m) => m.id)
+				.sort((a, b) => a.localeCompare(b));
+		} catch (err) {
+			console.error(`Failed to load models for provider ${providerId}`, err);
+			return [];
+		} finally {
+			await delay(500);
+			isModelsLoading = false;
+		}
+	}
+
+	async function saveHandler(ev: Event) {
+		ev.preventDefault();
+
+		try {
+			isModelsLoading = true;
+			await handleConfigureModelProvider(provider, configuration);
+
+			provider.configured = true;
+
+			await delay(400);
+
+			const array = await loadAvailableModels(provider.id);
+
+			isProviderConfigurationShown = false;
+
+			await delay(300);
+
+			models = array;
+		} catch (err) {
+			console.log(err);
+			provider.configured = false;
+		} finally {
+			isModelsLoading = false;
+		}
+	}
+
+	// Toggle model selection
+	function toggleModel(model: string, isChecked: boolean) {
+		// Take a snapshot of current models
+		let projectModels = { ...(project.models || {}) };
+		const currentProviderModels = new Set([...(projectModels[provider.id] || [])]);
+
+		if (isChecked) {
+			currentProviderModels.add(model);
+		} else {
+			currentProviderModels.delete(model);
+		}
+
+		let p = project;
+
+		if (!isChecked && defaultModelProvider === provider.id) {
+			p = setProjectDefaultProvider(p);
+		}
+
+		if (!isChecked && defaultModel === model) {
+			p = setProjectDefaultModel(p, model);
+		}
+
+		p = setProjectModels(p, provider.id, currentProviderModels.values().toArray());
+
+		project = p;
+	}
+
+	function selectModels(models: string[]) {
+		// Take a snapshot of current models
+		let projectModels = { ...(project.models || {}) };
+
+		projectModels = {
+			...projectModels,
+			[provider.id]: models
+		};
+
+		return updateProject_({ models: projectModels });
+	}
+
+	function unselectModels(models: string[]) {
+		// Take a snapshot of current models
+		let projectModels = { ...(project.models || {}) };
+
+		const array = new Set(projectModels[providerId] ?? []);
+
+		for (const model of models) {
+			array.delete(model);
+		}
+
+		projectModels = {
+			...projectModels,
+			[provider.id]: array.values().toArray()
+		};
+
+		return updateProject_({ models: projectModels });
+	}
+
+	function setProjectDefaultProvider(project: Project, providerId?: string) {
+		const clone = structuredClone($state.snapshot(project));
+
+		if (provider) {
+			clone.defaultModelProvider = providerId;
+		} else {
+			delete clone.defaultModelProvider;
+		}
+
+		return clone;
+	}
+
+	function setProjectDefaultModel(project: Project, modelId: string) {
+		const clone = structuredClone($state.snapshot(project));
+
+		if (provider) {
+			clone.defaultModel = modelId;
+		} else {
+			delete clone.defaultModel;
+		}
+
+		return clone;
+	}
+
+	function setProjectModels(project: Project, providerId: string, models: string[]) {
+		const clone = structuredClone($state.snapshot(project));
+
+		if (!clone.models) {
+			clone.models = {};
+		}
+
+		if (!models) {
+			delete clone.models[providerId];
+		} else {
+			clone.models[providerId] = models;
+		}
+
+		return clone;
+	}
+
+	// Save changes to the server
+	async function saveChanges() {
+		try {
+			isSaving = true;
+			// isSaving = true;
+			// Update the project prop directly
+			project = await updateProject(project);
+		} catch (err) {
+			console.error('Failed to save model configuration', err);
+			error = 'Failed to save changes. Please try again.';
+		} finally {
+			await delay(500);
+			isSaving = false;
+		}
+	}
+
+	function onConfigureProviderClickHandler() {
+		configuringProvider = provider.id;
+		isProviderConfigurationShown = true;
+	}
+</script>
+
+<div
+	class="model-provider-card border-surface2 flex h-fit w-full flex-col rounded-md border py-4 shadow-sm 2xl:mb-4 2xl:h-[514px] 2xl:last:mb-0"
+>
+	<div class="flex flex-col px-4">
+		<div class="mb-2 flex items-center gap-2">
+			{#if provider.icon || provider.iconDark}
+				<img
+					src={darkMode.isDark && provider.iconDark ? provider.iconDark : provider.icon}
+					alt={provider.name}
+					class="h-6 w-6 {darkMode.isDark && !provider.iconDark ? 'dark:invert' : ''}"
+				/>
+			{/if}
+			<div class="flex items-center gap-4">
+				<h4 class="truncate text-lg font-medium">{provider.name}</h4>
+				{#if [isModelsLoading, isSaving, isConfigurationLoading].some(Boolean)}
+					<div
+						class="flex justify-center"
+						in:fade={{ duration: 1000 }}
+						out:fade={{ duration: 600 }}
+					>
+						<Loader2 class="size-5 animate-spin" />
+					</div>
+				{/if}
+			</div>
+
+			{#if provider.configured}
+				<CheckCircleIcon class="text-blue ml-auto aspect-square h-5" />
+			{/if}
+		</div>
+
+		{#if provider.description}
+			<div class="markdown-content text-gray text-sm">
+				{@html toHTMLFromMarkdown(provider.description)}
+			</div>
+		{:else}
+			<div class="bg-surface1 text-gray flex h-14 w-full items-center rounded-lg px-4 py-3 text-sm">
+				<p>Description is not available</p>
path: ui/user/src/lib/components/ModelProviderCard.svelte
user: login: ivyjeong13
body: <img width="824" alt="Screenshot 2025-05-13 at 1 36 56 PM" src="https://github.com/user-attachments/assets/fa70082e-1c99-415f-ba14-63b8f642ce36" />

Feel free to resolve if this already went through with Craig and Will! Wondering if showing that description is not available or the provider is not configured text is needed? Seems like the sentence above is enough for me to know I need to configure at least one provider. If we can axe those empty spaces, would allow the user to see more model providers in their view.
created_at: 2025-05-13T17:47:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,629 @@
+<script lang="ts">
+	import type { ModelProvider, Project } from '$lib/services';
+	import { CheckCircleIcon, Loader2, Search } from 'lucide-svelte';
+	import { toHTMLFromMarkdown } from '$lib/markdown';
+	import { darkMode } from '$lib/stores';
+	import {
+		updateProject,
+		listAvailableModels,
+		configureModelProvider,
+		deconfigureModelProvider,
+		getModelProviderConfig
+	} from '$lib/services/chat/operations';
+	import { twMerge } from 'tailwind-merge';
+	import { fade, slide } from 'svelte/transition';
+	import { delay, throttle } from 'es-toolkit';
+	import { untrack } from 'svelte';
+
+	type Props = {
+		provider: ModelProvider;
+		project: Project;
+		configuringProvider?: string | null;
+		onError?: (error: string) => void;
+	};
+
+	let {
+		provider,
+		project = $bindable(),
+		configuringProvider = $bindable(),
+		onError
+	}: Props = $props();
+
+	let models: string[] = $state([]);
+
+	let selectedModels: string[] = $derived(project.models?.[provider.id] ?? []);
+	let error: string | null = $state(null);
+
+	let defaultModelProvider = $derived(project.defaultModelProvider || '');
+	let defaultModel = $derived(project.defaultModel || '');
+
+	let isConfigured = $derived(provider?.configured ?? false);
+	let providerId = $derived(provider.id);
+	// let selectedModels = $derived<Record<string, string[]>>(project.models || {});
+
+	let configuration: Record<string, string> = $state({});
+	// svelte-ignore state_referenced_locally
+	let oldConfiguration: Record<string, string> = $state(
+		structuredClone($state.snapshot(configuration))
+	);
+
+	let isConfigurationLoading = $state(false);
+	let isModelsLoading = $state(false);
+	let isSaving = $state(false);
+	let isProviderConfigurationShown = $state(false);
+
+	let modelQuery: string = $state('');
+	let filteredModels: string[] = $state([]);
+
+	// Throttle search model for better performance
+	const searchModels = throttle((query: string, models: string[] = []) => {
+		filteredModels = models.filter((model) =>
+			model.trim().toLowerCase().includes(query.trim().toLowerCase())
+		);
+	}, 100);
+
+	$effect(() => {
+		searchModels(modelQuery, models);
+	});
+
+	// When a provider is selected for configuration, load its config
+	$effect(() => {
+		if (isConfigured) {
+			untrack(async () => {
+				try {
+					await loadProviderConfig(providerId);
+					await delay(1000);
+					models = await loadAvailableModels(providerId);
+				} catch (err) {
+					console.error(err);
+				}
+			});
+		} else {
+			models = [];
+		}
+	});
+
+	// Deconfigure model provider
+	async function handleDeconfigureModelProvider(provider: ModelProvider) {
+		try {
+			// Unselect all model first
+			await unselectModels(selectedModels);
+			// Unconfigure model provider
+			await deconfigureModelProvider(project.assistantID, project.id, provider.id);
+
+			models = [];
+			provider.configured = false;
+			delete project.models?.[provider.id];
+
+			// clear current configurations
+			configuration = {};
+			// clear previous configurations
+			oldConfiguration = {};
+		} catch (err) {
+			console.error(`Failed to deconfigure ${provider.name}`, err);
+		}
+	}
+
+	type UpdateProjectParams = {
+		models: Project['models'];
+		defaultModel?: Project['defaultModel'];
+		defaultModelProvider?: Project['defaultModelProvider'];
+	};
+	// Update the project's model selection directly and trigger auto-save
+	function updateProject_(params: UpdateProjectParams) {
+		project = {
+			...project,
+			...params
+		};
+
+		saveChanges();
+	}
+
+	// Load configuration for a model provider
+	async function loadProviderConfig(providerId: string) {
+		try {
+			configuration = await getModelProviderConfig(project.assistantID, project.id, providerId);
+			oldConfiguration = structuredClone($state.snapshot(configuration));
+		} catch (err) {
+			console.error(`Failed to get configuration for provider ${providerId}`, err);
+		} finally {
+			isConfigurationLoading = false;
+		}
+	}
+
+	// Configure model provider
+	async function handleConfigureModelProvider(
+		provider: ModelProvider,
+		config: Record<string, string>
+	) {
+		try {
+			await configureModelProvider(project.assistantID, project.id, provider.id, config);
+			const newProject = setProjectModels(project, providerId, []);
+
+			await updateProject(newProject);
+
+			project = newProject;
+		} catch (err) {
+			onError?.((error = `Failed to configure ${provider.name}`));
+
+			console.error(error, err);
+		}
+	}
+
+	// Load available models for a provider
+	async function loadAvailableModels(providerId: string) {
+		isModelsLoading = true;
+
+		try {
+			const response = await listAvailableModels(project.assistantID, project.id, providerId);
+
+			return (response.data || [])
+				.filter((m) => m.metadata && m.metadata.usage === 'llm')
+				.map((m) => m.id)
+				.sort((a, b) => a.localeCompare(b));
+		} catch (err) {
+			console.error(`Failed to load models for provider ${providerId}`, err);
+			return [];
+		} finally {
+			await delay(500);
+			isModelsLoading = false;
+		}
+	}
+
+	async function saveHandler(ev: Event) {
+		ev.preventDefault();
+
+		try {
+			isModelsLoading = true;
+			await handleConfigureModelProvider(provider, configuration);
+
+			provider.configured = true;
+
+			await delay(400);
+
+			const array = await loadAvailableModels(provider.id);
+
+			isProviderConfigurationShown = false;
+
+			await delay(300);
+
+			models = array;
+		} catch (err) {
+			console.log(err);
+			provider.configured = false;
+		} finally {
+			isModelsLoading = false;
+		}
+	}
+
+	// Toggle model selection
+	function toggleModel(model: string, isChecked: boolean) {
+		// Take a snapshot of current models
+		let projectModels = { ...(project.models || {}) };
+		const currentProviderModels = new Set([...(projectModels[provider.id] || [])]);
+
+		if (isChecked) {
+			currentProviderModels.add(model);
+		} else {
+			currentProviderModels.delete(model);
+		}
+
+		let p = project;
+
+		if (!isChecked && defaultModelProvider === provider.id) {
+			p = setProjectDefaultProvider(p);
+		}
+
+		if (!isChecked && defaultModel === model) {
+			p = setProjectDefaultModel(p, model);
+		}
+
+		p = setProjectModels(p, provider.id, currentProviderModels.values().toArray());
+
+		project = p;
+	}
+
+	function selectModels(models: string[]) {
+		// Take a snapshot of current models
+		let projectModels = { ...(project.models || {}) };
+
+		projectModels = {
+			...projectModels,
+			[provider.id]: models
+		};
+
+		return updateProject_({ models: projectModels });
+	}
+
+	function unselectModels(models: string[]) {
+		// Take a snapshot of current models
+		let projectModels = { ...(project.models || {}) };
+
+		const array = new Set(projectModels[providerId] ?? []);
+
+		for (const model of models) {
+			array.delete(model);
+		}
+
+		projectModels = {
+			...projectModels,
+			[provider.id]: array.values().toArray()
+		};
+
+		return updateProject_({ models: projectModels });
+	}
+
+	function setProjectDefaultProvider(project: Project, providerId?: string) {
+		const clone = structuredClone($state.snapshot(project));
+
+		if (provider) {
+			clone.defaultModelProvider = providerId;
+		} else {
+			delete clone.defaultModelProvider;
+		}
+
+		return clone;
+	}
+
+	function setProjectDefaultModel(project: Project, modelId: string) {
+		const clone = structuredClone($state.snapshot(project));
+
+		if (provider) {
+			clone.defaultModel = modelId;
+		} else {
+			delete clone.defaultModel;
+		}
+
+		return clone;
+	}
+
+	function setProjectModels(project: Project, providerId: string, models: string[]) {
+		const clone = structuredClone($state.snapshot(project));
+
+		if (!clone.models) {
+			clone.models = {};
+		}
+
+		if (!models) {
+			delete clone.models[providerId];
+		} else {
+			clone.models[providerId] = models;
+		}
+
+		return clone;
+	}
+
+	// Save changes to the server
+	async function saveChanges() {
+		try {
+			isSaving = true;
+			// isSaving = true;
+			// Update the project prop directly
+			project = await updateProject(project);
+		} catch (err) {
+			console.error('Failed to save model configuration', err);
+			error = 'Failed to save changes. Please try again.';
+		} finally {
+			await delay(500);
+			isSaving = false;
+		}
+	}
+
+	function onConfigureProviderClickHandler() {
+		configuringProvider = provider.id;
+		isProviderConfigurationShown = true;
+	}
+</script>
+
+<div
+	class="model-provider-card border-surface2 flex h-fit w-full flex-col rounded-md border py-4 shadow-sm 2xl:mb-4 2xl:h-[514px] 2xl:last:mb-0"
+>
+	<div class="flex flex-col px-4">
+		<div class="mb-2 flex items-center gap-2">
+			{#if provider.icon || provider.iconDark}
+				<img
+					src={darkMode.isDark && provider.iconDark ? provider.iconDark : provider.icon}
+					alt={provider.name}
+					class="h-6 w-6 {darkMode.isDark && !provider.iconDark ? 'dark:invert' : ''}"
+				/>
+			{/if}
+			<div class="flex items-center gap-4">
+				<h4 class="truncate text-lg font-medium">{provider.name}</h4>
+				{#if [isModelsLoading, isSaving, isConfigurationLoading].some(Boolean)}
+					<div
+						class="flex justify-center"
+						in:fade={{ duration: 1000 }}
+						out:fade={{ duration: 600 }}
+					>
+						<Loader2 class="size-5 animate-spin" />
+					</div>
+				{/if}
+			</div>
+
+			{#if provider.configured}
+				<CheckCircleIcon class="text-blue ml-auto aspect-square h-5" />
+			{/if}
+		</div>
+
+		{#if provider.description}
+			<div class="markdown-content text-gray text-sm">
+				{@html toHTMLFromMarkdown(provider.description)}
+			</div>
+		{:else}
+			<div class="bg-surface1 text-gray flex h-14 w-full items-center rounded-lg px-4 py-3 text-sm">
+				<p>Description is not available</p>
+			</div>
+		{/if}
+	</div>
+
+	<div class="flex flex-1 flex-col gap-4 px-4 pt-4 pb-4">
+		{#if isConfigured}
+			<!-- Models Selection Section -->
+			<div
+				class={twMerge(
+					'bg-surface1/0 flex flex-1 flex-col gap-2 rounded-md',
+					isProviderConfigurationShown && 'pointer-events-none opacity-50'
+				)}
+				transition:slide={{ duration: 100 }}
+			>
+				<div
+					class="model-provider-search-input bg-surface1 dark:bg-surface2 relative flex h-10 w-full items-center gap-2 rounded-md text-sm"
+				>
+					<div class="absolute inset-x-0 left-2 aspect-square h-5 opacity-50">
+						<Search class="h-full" />
+					</div>
+					<input
+						bind:value={modelQuery}
+						class="h-full w-full bg-transparent px-3 pr-8 pl-9"
+						type="text"
+						placeholder="Search your model here..."
+					/>
+
+					{#if filteredModels.length !== models.length}
+						<div
+							class="absolute inset-y-0 right-4 flex h-full items-center justify-center text-sm font-medium opacity-50"
+							transition:fade={{ duration: 100 }}
+						>
+							<div>{filteredModels.length}</div>
+						</div>
+					{/if}
+				</div>
+
+				<div class="flex justify-between">
+					<div class="flex items-center gap-2 px-3 text-sm font-medium">
+						<h5 class="flex items-center">
+							<input
+								class="bg-surface3 mr-2 h-4 w-4"
+								type="checkbox"
+								id={`model-${provider.id}-toggle-all`}
+								bind:checked={
+									() => models.length > 0 && models.length === selectedModels.length,
+									(v) => {
+										const indeterminate =
+											selectedModels.length > 0 && models.length > selectedModels.length;
+
+										if (indeterminate || v) {
+											selectModels(models);
+										} else {
+											unselectModels(selectedModels);
+										}
+									}
+								}
+								indeterminate={selectedModels.length > 0 && models.length > selectedModels.length}
+							/>
+							<label class="inline" for={`model-${provider.id}-toggle-all`}>Available Models</label>
+
+							{#if models.length}
+								<span class="opacity-50">({models.length})</span>
+							{/if}
+						</h5>
+
+						{#if selectedModels.length}
path: ui/user/src/lib/components/ModelProviderCard.svelte
user: login: ivyjeong13
body: <img width="1012" alt="Screenshot 2025-05-13 at 1 47 28 PM" src="https://github.com/user-attachments/assets/0c08b18e-ae74-4170-a35d-6588e29d62a8" />

Would be nice to bubble the model provider(s) I did configure to the top, maybe in two sections: Configured Model Providers & Available Model Providers, if we want consistent height because more vertical space is needed for when it's configured?
created_at: 2025-05-13T17:48:56Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,629 @@
+<script lang="ts">
+	import type { ModelProvider, Project } from '$lib/services';
+	import { CheckCircleIcon, Loader2, Search } from 'lucide-svelte';
+	import { toHTMLFromMarkdown } from '$lib/markdown';
+	import { darkMode } from '$lib/stores';
+	import {
+		updateProject,
+		listAvailableModels,
+		configureModelProvider,
+		deconfigureModelProvider,
+		getModelProviderConfig
+	} from '$lib/services/chat/operations';
+	import { twMerge } from 'tailwind-merge';
+	import { fade, slide } from 'svelte/transition';
+	import { delay, throttle } from 'es-toolkit';
+	import { untrack } from 'svelte';
+
+	type Props = {
+		provider: ModelProvider;
+		project: Project;
+		configuringProvider?: string | null;
+		onError?: (error: string) => void;
+	};
+
+	let {
+		provider,
+		project = $bindable(),
+		configuringProvider = $bindable(),
+		onError
+	}: Props = $props();
+
+	let models: string[] = $state([]);
+
+	let selectedModels: string[] = $derived(project.models?.[provider.id] ?? []);
+	let error: string | null = $state(null);
+
+	let defaultModelProvider = $derived(project.defaultModelProvider || '');
+	let defaultModel = $derived(project.defaultModel || '');
+
+	let isConfigured = $derived(provider?.configured ?? false);
+	let providerId = $derived(provider.id);
+	// let selectedModels = $derived<Record<string, string[]>>(project.models || {});
+
+	let configuration: Record<string, string> = $state({});
+	// svelte-ignore state_referenced_locally
path: ui/user/src/lib/components/ModelProviderCard.svelte
user: login: ivyjeong13
body: remove svelte-ignore, do not hide warnings/errors
created_at: 2025-05-13T17:50:59Z
author_association: COLLABORATOR
diff_hunk: @@ -80,32 +65,57 @@
 	header="Model Providers"
 	helpText={HELPER_TEXTS.modelProviders}
 >
-	<div class="flex flex-col p-2">
-		<p class="pb-2 text-xs">
-			Agents use our included LLM by default. Connect other model providers to unlock more LLMs you
-			can chat with.
-		</p>
-
-		{#if isLoading}
-			<p class="text-xs italic">Loading model providers...</p>
-		{:else if project?.models && Object.keys(project.models).length > 0}
-			<div class="pb-2">
-				<p class="mb-1 text-xs font-medium">Configured providers:</p>
-				<ul class="space-y-0.5 pl-3 text-xs">
-					{#each getSortedProviderIds(Object.keys(project.models)) as providerId}
-						<li class="flex items-center gap-1">
-							<span>{getProviderName(providerId)}</span>
-							<span class="text-muted-foreground"
-								>({project.models[providerId].length}
-								{project.models[providerId].length === 1 ? 'model' : 'models'})</span
-							>
-						</li>
-					{/each}
-				</ul>
-			</div>
-		{:else}
-			<p class="text-xs">No model providers configured</p>
-		{/if}
+	<div class="flex flex-col gap-8 p-2">
+		<div class="flex flex-col gap-4">
+			<p class="text-xs font-light text-gray-500">
+				Agents use our included LLM by default. Connect other model providers to unlock more LLMs
+				you can chat with.
+			</p>
+
+			{#if isLoading}
+				<p class="text-xs italic">Loading model providers...</p>
+			{:else if project?.models && Object.keys(project.models).length > 0}
+				<div class="pb-2">
+					<p class="mb-1 text-sm font-medium">Configured providers:</p>
+					<ul class="flex flex-col text-xs">
+						{#each projectProviderModelIds as providerId}
+							{@const provider = providersWithMoreData.get(providerId)}
+
+							<li class="model-provider flex items-center gap-1 py-2">
+								<div class="size-4">
path: ui/user/src/lib/components/sidebar/ModelProviders.svelte
user: login: ivyjeong13
body: <img width="537" alt="Screenshot 2025-05-13 at 1 49 28 PM" src="https://github.com/user-attachments/assets/7a186194-4248-4950-87ec-aa4905752879" />

If we're keeping the way it is, it would nice if I could click on the model provider and it takes me to it. A small QOL instead of having to click Manage Model Providers then scroll down to find it. 
created_at: 2025-05-13T17:58:01Z
author_association: COLLABORATOR
diff_hunk: @@ -282,194 +163,64 @@
 		<div class="text-muted">No model providers available</div>
 	{:else}
 		<!-- Default Model Provider Section -->
-		<div class="mb-6">
-			<h4 class="text-md mb-2 font-medium">Default Model</h4>
-			<p class="text-muted mb-3 text-sm">
-				Select the default model for this project. This will be used when no specific model is
-				specified.
-			</p>
-
-			{#if !Object.keys(selectedModels).length}
-				<div class="mb-2 text-sm text-yellow-500">
-					Configure at least one provider and select models to set a default model.
-				</div>
-			{:else}
-				<div>
-					<select
-						class="w-full rounded-md border p-2 text-sm"
-						value={defaultModelProvider && defaultModel
-							? `${defaultModel}|||${defaultModelProvider}`
-							: ''}
-						onchange={(e) => updateDefaultModel(e.currentTarget.value)}
+		<div class={twMerge('mb-6 flex flex-col gap-2', !hasOneModelSelected && 'opacity-50')}>
+			<h4 class="text-md font-medium">Default Model</h4>
+
+			<div>
+				<select
+					class="border-surface2 dark:bg-surface1 w-full rounded-md border px-4 py-4 text-sm"
path: ui/user/src/lib/components/ModelProviders.svelte
user: login: ivyjeong13
body: <img width="862" alt="Screenshot 2025-05-13 at 1 55 56 PM" src="https://github.com/user-attachments/assets/3f90de55-526d-4d16-a114-93cd025cb628" />

nit: chevron icon is not evenly spaced away, right up against the border 😅 
created_at: 2025-05-13T17:59:22Z
author_association: COLLABORATOR
diff_hunk: @@ -73,5 +73,8 @@
 		"typescript-eslint": "^8.25.0",
 		"vite": "^5.4.14"
 	},
-	"type": "module"
+	"type": "module",
+	"dependencies": {
+		"es-toolkit": "^1.37.2"
path: ui/user/package.json
user: login: ryu-man
body: At the moment it is not used for complex stuff. I am using the delay and throttle functions. I thought maybe using something already made will save us effort and time rather than re-implementing that. the library is tree-shakeable, so unused imports will not be included in the final bundle
created_at: 2025-05-13T18:04:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,629 @@
+<script lang="ts">
+	import type { ModelProvider, Project } from '$lib/services';
+	import { CheckCircleIcon, Loader2, Search } from 'lucide-svelte';
+	import { toHTMLFromMarkdown } from '$lib/markdown';
+	import { darkMode } from '$lib/stores';
+	import {
+		updateProject,
+		listAvailableModels,
+		configureModelProvider,
+		deconfigureModelProvider,
+		getModelProviderConfig
+	} from '$lib/services/chat/operations';
+	import { twMerge } from 'tailwind-merge';
+	import { fade, slide } from 'svelte/transition';
+	import { delay, throttle } from 'es-toolkit';
+	import { untrack } from 'svelte';
+
+	type Props = {
+		provider: ModelProvider;
+		project: Project;
+		configuringProvider?: string | null;
+		onError?: (error: string) => void;
+	};
+
+	let {
+		provider,
+		project = $bindable(),
+		configuringProvider = $bindable(),
+		onError
+	}: Props = $props();
+
+	let models: string[] = $state([]);
+
+	let selectedModels: string[] = $derived(project.models?.[provider.id] ?? []);
+	let error: string | null = $state(null);
+
+	let defaultModelProvider = $derived(project.defaultModelProvider || '');
+	let defaultModel = $derived(project.defaultModel || '');
+
+	let isConfigured = $derived(provider?.configured ?? false);
+	let providerId = $derived(provider.id);
+	// let selectedModels = $derived<Record<string, string[]>>(project.models || {});
+
+	let configuration: Record<string, string> = $state({});
+	// svelte-ignore state_referenced_locally
+	let oldConfiguration: Record<string, string> = $state(
+		structuredClone($state.snapshot(configuration))
+	);
+
+	let isConfigurationLoading = $state(false);
+	let isModelsLoading = $state(false);
+	let isSaving = $state(false);
+	let isProviderConfigurationShown = $state(false);
+
+	let modelQuery: string = $state('');
+	let filteredModels: string[] = $state([]);
+
+	// Throttle search model for better performance
+	const searchModels = throttle((query: string, models: string[] = []) => {
+		filteredModels = models.filter((model) =>
+			model.trim().toLowerCase().includes(query.trim().toLowerCase())
+		);
+	}, 100);
+
+	$effect(() => {
+		searchModels(modelQuery, models);
+	});
+
+	// When a provider is selected for configuration, load its config
+	$effect(() => {
+		if (isConfigured) {
+			untrack(async () => {
+				try {
+					await loadProviderConfig(providerId);
+					await delay(1000);
+					models = await loadAvailableModels(providerId);
+				} catch (err) {
+					console.error(err);
+				}
+			});
+		} else {
+			models = [];
+		}
+	});
+
+	// Deconfigure model provider
+	async function handleDeconfigureModelProvider(provider: ModelProvider) {
+		try {
+			// Unselect all model first
+			await unselectModels(selectedModels);
+			// Unconfigure model provider
+			await deconfigureModelProvider(project.assistantID, project.id, provider.id);
+
+			models = [];
+			provider.configured = false;
+			delete project.models?.[provider.id];
+
+			// clear current configurations
+			configuration = {};
+			// clear previous configurations
+			oldConfiguration = {};
+		} catch (err) {
+			console.error(`Failed to deconfigure ${provider.name}`, err);
+		}
+	}
+
+	type UpdateProjectParams = {
+		models: Project['models'];
+		defaultModel?: Project['defaultModel'];
+		defaultModelProvider?: Project['defaultModelProvider'];
+	};
+	// Update the project's model selection directly and trigger auto-save
+	function updateProject_(params: UpdateProjectParams) {
+		project = {
+			...project,
+			...params
+		};
+
+		saveChanges();
+	}
+
+	// Load configuration for a model provider
+	async function loadProviderConfig(providerId: string) {
+		try {
+			configuration = await getModelProviderConfig(project.assistantID, project.id, providerId);
+			oldConfiguration = structuredClone($state.snapshot(configuration));
+		} catch (err) {
+			console.error(`Failed to get configuration for provider ${providerId}`, err);
+		} finally {
+			isConfigurationLoading = false;
+		}
+	}
+
+	// Configure model provider
+	async function handleConfigureModelProvider(
+		provider: ModelProvider,
+		config: Record<string, string>
+	) {
+		try {
+			await configureModelProvider(project.assistantID, project.id, provider.id, config);
+			const newProject = setProjectModels(project, providerId, []);
+
+			await updateProject(newProject);
+
+			project = newProject;
+		} catch (err) {
+			onError?.((error = `Failed to configure ${provider.name}`));
+
+			console.error(error, err);
+		}
+	}
+
+	// Load available models for a provider
+	async function loadAvailableModels(providerId: string) {
+		isModelsLoading = true;
+
+		try {
+			const response = await listAvailableModels(project.assistantID, project.id, providerId);
+
+			return (response.data || [])
+				.filter((m) => m.metadata && m.metadata.usage === 'llm')
+				.map((m) => m.id)
+				.sort((a, b) => a.localeCompare(b));
+		} catch (err) {
+			console.error(`Failed to load models for provider ${providerId}`, err);
+			return [];
+		} finally {
+			await delay(500);
+			isModelsLoading = false;
+		}
+	}
+
+	async function saveHandler(ev: Event) {
+		ev.preventDefault();
+
+		try {
+			isModelsLoading = true;
+			await handleConfigureModelProvider(provider, configuration);
+
+			provider.configured = true;
+
+			await delay(400);
+
+			const array = await loadAvailableModels(provider.id);
+
+			isProviderConfigurationShown = false;
+
+			await delay(300);
+
+			models = array;
+		} catch (err) {
+			console.log(err);
+			provider.configured = false;
+		} finally {
+			isModelsLoading = false;
+		}
+	}
+
+	// Toggle model selection
+	function toggleModel(model: string, isChecked: boolean) {
+		// Take a snapshot of current models
+		let projectModels = { ...(project.models || {}) };
+		const currentProviderModels = new Set([...(projectModels[provider.id] || [])]);
+
+		if (isChecked) {
+			currentProviderModels.add(model);
+		} else {
+			currentProviderModels.delete(model);
+		}
+
+		let p = project;
+
+		if (!isChecked && defaultModelProvider === provider.id) {
+			p = setProjectDefaultProvider(p);
+		}
+
+		if (!isChecked && defaultModel === model) {
+			p = setProjectDefaultModel(p, model);
+		}
+
+		p = setProjectModels(p, provider.id, currentProviderModels.values().toArray());
+
+		project = p;
+	}
+
+	function selectModels(models: string[]) {
+		// Take a snapshot of current models
+		let projectModels = { ...(project.models || {}) };
+
+		projectModels = {
+			...projectModels,
+			[provider.id]: models
+		};
+
+		return updateProject_({ models: projectModels });
+	}
+
+	function unselectModels(models: string[]) {
+		// Take a snapshot of current models
+		let projectModels = { ...(project.models || {}) };
+
+		const array = new Set(projectModels[providerId] ?? []);
+
+		for (const model of models) {
+			array.delete(model);
+		}
+
+		projectModels = {
+			...projectModels,
+			[provider.id]: array.values().toArray()
+		};
+
+		return updateProject_({ models: projectModels });
+	}
+
+	function setProjectDefaultProvider(project: Project, providerId?: string) {
+		const clone = structuredClone($state.snapshot(project));
+
+		if (provider) {
+			clone.defaultModelProvider = providerId;
+		} else {
+			delete clone.defaultModelProvider;
+		}
+
+		return clone;
+	}
+
+	function setProjectDefaultModel(project: Project, modelId: string) {
+		const clone = structuredClone($state.snapshot(project));
+
+		if (provider) {
+			clone.defaultModel = modelId;
+		} else {
+			delete clone.defaultModel;
+		}
+
+		return clone;
+	}
+
+	function setProjectModels(project: Project, providerId: string, models: string[]) {
+		const clone = structuredClone($state.snapshot(project));
+
+		if (!clone.models) {
+			clone.models = {};
+		}
+
+		if (!models) {
+			delete clone.models[providerId];
+		} else {
+			clone.models[providerId] = models;
+		}
+
+		return clone;
+	}
+
+	// Save changes to the server
+	async function saveChanges() {
+		try {
+			isSaving = true;
+			// isSaving = true;
+			// Update the project prop directly
+			project = await updateProject(project);
+		} catch (err) {
+			console.error('Failed to save model configuration', err);
+			error = 'Failed to save changes. Please try again.';
+		} finally {
+			await delay(500);
+			isSaving = false;
+		}
+	}
+
+	function onConfigureProviderClickHandler() {
+		configuringProvider = provider.id;
+		isProviderConfigurationShown = true;
+	}
+</script>
+
+<div
+	class="model-provider-card border-surface2 flex h-fit w-full flex-col rounded-md border py-4 shadow-sm 2xl:mb-4 2xl:h-[514px] 2xl:last:mb-0"
+>
+	<div class="flex flex-col px-4">
+		<div class="mb-2 flex items-center gap-2">
+			{#if provider.icon || provider.iconDark}
+				<img
+					src={darkMode.isDark && provider.iconDark ? provider.iconDark : provider.icon}
+					alt={provider.name}
+					class="h-6 w-6 {darkMode.isDark && !provider.iconDark ? 'dark:invert' : ''}"
+				/>
+			{/if}
+			<div class="flex items-center gap-4">
+				<h4 class="truncate text-lg font-medium">{provider.name}</h4>
+				{#if [isModelsLoading, isSaving, isConfigurationLoading].some(Boolean)}
+					<div
+						class="flex justify-center"
+						in:fade={{ duration: 1000 }}
+						out:fade={{ duration: 600 }}
+					>
+						<Loader2 class="size-5 animate-spin" />
+					</div>
+				{/if}
+			</div>
+
+			{#if provider.configured}
+				<CheckCircleIcon class="text-blue ml-auto aspect-square h-5" />
+			{/if}
+		</div>
+
+		{#if provider.description}
+			<div class="markdown-content text-gray text-sm">
+				{@html toHTMLFromMarkdown(provider.description)}
+			</div>
+		{:else}
+			<div class="bg-surface1 text-gray flex h-14 w-full items-center rounded-lg px-4 py-3 text-sm">
+				<p>Description is not available</p>
+			</div>
+		{/if}
+	</div>
+
+	<div class="flex flex-1 flex-col gap-4 px-4 pt-4 pb-4">
+		{#if isConfigured}
+			<!-- Models Selection Section -->
+			<div
+				class={twMerge(
+					'bg-surface1/0 flex flex-1 flex-col gap-2 rounded-md',
+					isProviderConfigurationShown && 'pointer-events-none opacity-50'
+				)}
+				transition:slide={{ duration: 100 }}
+			>
+				<div
+					class="model-provider-search-input bg-surface1 dark:bg-surface2 relative flex h-10 w-full items-center gap-2 rounded-md text-sm"
+				>
+					<div class="absolute inset-x-0 left-2 aspect-square h-5 opacity-50">
+						<Search class="h-full" />
+					</div>
+					<input
+						bind:value={modelQuery}
+						class="h-full w-full bg-transparent px-3 pr-8 pl-9"
+						type="text"
+						placeholder="Search your model here..."
+					/>
+
+					{#if filteredModels.length !== models.length}
+						<div
+							class="absolute inset-y-0 right-4 flex h-full items-center justify-center text-sm font-medium opacity-50"
+							transition:fade={{ duration: 100 }}
+						>
+							<div>{filteredModels.length}</div>
+						</div>
+					{/if}
+				</div>
+
+				<div class="flex justify-between">
+					<div class="flex items-center gap-2 px-3 text-sm font-medium">
+						<h5 class="flex items-center">
+							<input
+								class="bg-surface3 mr-2 h-4 w-4"
+								type="checkbox"
+								id={`model-${provider.id}-toggle-all`}
+								bind:checked={
+									() => models.length > 0 && models.length === selectedModels.length,
+									(v) => {
+										const indeterminate =
+											selectedModels.length > 0 && models.length > selectedModels.length;
+
+										if (indeterminate || v) {
+											selectModels(models);
+										} else {
+											unselectModels(selectedModels);
+										}
+									}
+								}
+								indeterminate={selectedModels.length > 0 && models.length > selectedModels.length}
+							/>
+							<label class="inline" for={`model-${provider.id}-toggle-all`}>Available Models</label>
+
+							{#if models.length}
+								<span class="opacity-50">({models.length})</span>
+							{/if}
+						</h5>
+
+						{#if selectedModels.length}
path: ui/user/src/lib/components/ModelProviderCard.svelte
user: login: ryu-man
body: Is this about resorting the configured model providers or creating another section for configured providers?
created_at: 2025-05-13T18:19:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,629 @@
+<script lang="ts">
+	import type { ModelProvider, Project } from '$lib/services';
+	import { CheckCircleIcon, Loader2, Search } from 'lucide-svelte';
+	import { toHTMLFromMarkdown } from '$lib/markdown';
+	import { darkMode } from '$lib/stores';
+	import {
+		updateProject,
+		listAvailableModels,
+		configureModelProvider,
+		deconfigureModelProvider,
+		getModelProviderConfig
+	} from '$lib/services/chat/operations';
+	import { twMerge } from 'tailwind-merge';
+	import { fade, slide } from 'svelte/transition';
+	import { delay, throttle } from 'es-toolkit';
+	import { untrack } from 'svelte';
+
+	type Props = {
+		provider: ModelProvider;
+		project: Project;
+		configuringProvider?: string | null;
+		onError?: (error: string) => void;
+	};
+
+	let {
+		provider,
+		project = $bindable(),
+		configuringProvider = $bindable(),
+		onError
+	}: Props = $props();
+
+	let models: string[] = $state([]);
+
+	let selectedModels: string[] = $derived(project.models?.[provider.id] ?? []);
+	let error: string | null = $state(null);
+
+	let defaultModelProvider = $derived(project.defaultModelProvider || '');
+	let defaultModel = $derived(project.defaultModel || '');
+
+	let isConfigured = $derived(provider?.configured ?? false);
+	let providerId = $derived(provider.id);
+	// let selectedModels = $derived<Record<string, string[]>>(project.models || {});
+
+	let configuration: Record<string, string> = $state({});
+	// svelte-ignore state_referenced_locally
+	let oldConfiguration: Record<string, string> = $state(
+		structuredClone($state.snapshot(configuration))
+	);
+
+	let isConfigurationLoading = $state(false);
+	let isModelsLoading = $state(false);
+	let isSaving = $state(false);
+	let isProviderConfigurationShown = $state(false);
+
+	let modelQuery: string = $state('');
+	let filteredModels: string[] = $state([]);
+
+	// Throttle search model for better performance
+	const searchModels = throttle((query: string, models: string[] = []) => {
+		filteredModels = models.filter((model) =>
+			model.trim().toLowerCase().includes(query.trim().toLowerCase())
+		);
+	}, 100);
+
+	$effect(() => {
+		searchModels(modelQuery, models);
+	});
+
+	// When a provider is selected for configuration, load its config
+	$effect(() => {
+		if (isConfigured) {
+			untrack(async () => {
+				try {
+					await loadProviderConfig(providerId);
+					await delay(1000);
+					models = await loadAvailableModels(providerId);
+				} catch (err) {
+					console.error(err);
+				}
+			});
+		} else {
+			models = [];
+		}
+	});
+
+	// Deconfigure model provider
+	async function handleDeconfigureModelProvider(provider: ModelProvider) {
+		try {
+			// Unselect all model first
+			await unselectModels(selectedModels);
+			// Unconfigure model provider
+			await deconfigureModelProvider(project.assistantID, project.id, provider.id);
+
+			models = [];
+			provider.configured = false;
+			delete project.models?.[provider.id];
+
+			// clear current configurations
+			configuration = {};
+			// clear previous configurations
+			oldConfiguration = {};
+		} catch (err) {
+			console.error(`Failed to deconfigure ${provider.name}`, err);
+		}
+	}
+
+	type UpdateProjectParams = {
+		models: Project['models'];
+		defaultModel?: Project['defaultModel'];
+		defaultModelProvider?: Project['defaultModelProvider'];
+	};
+	// Update the project's model selection directly and trigger auto-save
+	function updateProject_(params: UpdateProjectParams) {
+		project = {
+			...project,
+			...params
+		};
+
+		saveChanges();
+	}
+
+	// Load configuration for a model provider
+	async function loadProviderConfig(providerId: string) {
+		try {
+			configuration = await getModelProviderConfig(project.assistantID, project.id, providerId);
+			oldConfiguration = structuredClone($state.snapshot(configuration));
+		} catch (err) {
+			console.error(`Failed to get configuration for provider ${providerId}`, err);
+		} finally {
+			isConfigurationLoading = false;
+		}
+	}
+
+	// Configure model provider
+	async function handleConfigureModelProvider(
+		provider: ModelProvider,
+		config: Record<string, string>
+	) {
+		try {
+			await configureModelProvider(project.assistantID, project.id, provider.id, config);
+			const newProject = setProjectModels(project, providerId, []);
+
+			await updateProject(newProject);
+
+			project = newProject;
+		} catch (err) {
+			onError?.((error = `Failed to configure ${provider.name}`));
+
+			console.error(error, err);
+		}
+	}
+
+	// Load available models for a provider
+	async function loadAvailableModels(providerId: string) {
+		isModelsLoading = true;
+
+		try {
+			const response = await listAvailableModels(project.assistantID, project.id, providerId);
+
+			return (response.data || [])
+				.filter((m) => m.metadata && m.metadata.usage === 'llm')
+				.map((m) => m.id)
+				.sort((a, b) => a.localeCompare(b));
+		} catch (err) {
+			console.error(`Failed to load models for provider ${providerId}`, err);
+			return [];
+		} finally {
+			await delay(500);
+			isModelsLoading = false;
+		}
+	}
+
+	async function saveHandler(ev: Event) {
+		ev.preventDefault();
+
+		try {
+			isModelsLoading = true;
+			await handleConfigureModelProvider(provider, configuration);
+
+			provider.configured = true;
+
+			await delay(400);
+
+			const array = await loadAvailableModels(provider.id);
+
+			isProviderConfigurationShown = false;
+
+			await delay(300);
+
+			models = array;
+		} catch (err) {
+			console.log(err);
+			provider.configured = false;
+		} finally {
+			isModelsLoading = false;
+		}
+	}
+
+	// Toggle model selection
+	function toggleModel(model: string, isChecked: boolean) {
+		// Take a snapshot of current models
+		let projectModels = { ...(project.models || {}) };
+		const currentProviderModels = new Set([...(projectModels[provider.id] || [])]);
+
+		if (isChecked) {
+			currentProviderModels.add(model);
+		} else {
+			currentProviderModels.delete(model);
+		}
+
+		let p = project;
+
+		if (!isChecked && defaultModelProvider === provider.id) {
+			p = setProjectDefaultProvider(p);
+		}
+
+		if (!isChecked && defaultModel === model) {
+			p = setProjectDefaultModel(p, model);
+		}
+
+		p = setProjectModels(p, provider.id, currentProviderModels.values().toArray());
+
+		project = p;
+	}
+
+	function selectModels(models: string[]) {
+		// Take a snapshot of current models
+		let projectModels = { ...(project.models || {}) };
+
+		projectModels = {
+			...projectModels,
+			[provider.id]: models
+		};
+
+		return updateProject_({ models: projectModels });
+	}
+
+	function unselectModels(models: string[]) {
+		// Take a snapshot of current models
+		let projectModels = { ...(project.models || {}) };
+
+		const array = new Set(projectModels[providerId] ?? []);
+
+		for (const model of models) {
+			array.delete(model);
+		}
+
+		projectModels = {
+			...projectModels,
+			[provider.id]: array.values().toArray()
+		};
+
+		return updateProject_({ models: projectModels });
+	}
+
+	function setProjectDefaultProvider(project: Project, providerId?: string) {
+		const clone = structuredClone($state.snapshot(project));
+
+		if (provider) {
+			clone.defaultModelProvider = providerId;
+		} else {
+			delete clone.defaultModelProvider;
+		}
+
+		return clone;
+	}
+
+	function setProjectDefaultModel(project: Project, modelId: string) {
+		const clone = structuredClone($state.snapshot(project));
+
+		if (provider) {
+			clone.defaultModel = modelId;
+		} else {
+			delete clone.defaultModel;
+		}
+
+		return clone;
+	}
+
+	function setProjectModels(project: Project, providerId: string, models: string[]) {
+		const clone = structuredClone($state.snapshot(project));
+
+		if (!clone.models) {
+			clone.models = {};
+		}
+
+		if (!models) {
+			delete clone.models[providerId];
+		} else {
+			clone.models[providerId] = models;
+		}
+
+		return clone;
+	}
+
+	// Save changes to the server
+	async function saveChanges() {
+		try {
+			isSaving = true;
+			// isSaving = true;
+			// Update the project prop directly
+			project = await updateProject(project);
+		} catch (err) {
+			console.error('Failed to save model configuration', err);
+			error = 'Failed to save changes. Please try again.';
+		} finally {
+			await delay(500);
+			isSaving = false;
+		}
+	}
+
+	function onConfigureProviderClickHandler() {
+		configuringProvider = provider.id;
+		isProviderConfigurationShown = true;
+	}
+</script>
+
+<div
+	class="model-provider-card border-surface2 flex h-fit w-full flex-col rounded-md border py-4 shadow-sm 2xl:mb-4 2xl:h-[514px] 2xl:last:mb-0"
+>
+	<div class="flex flex-col px-4">
+		<div class="mb-2 flex items-center gap-2">
+			{#if provider.icon || provider.iconDark}
+				<img
+					src={darkMode.isDark && provider.iconDark ? provider.iconDark : provider.icon}
+					alt={provider.name}
+					class="h-6 w-6 {darkMode.isDark && !provider.iconDark ? 'dark:invert' : ''}"
+				/>
+			{/if}
+			<div class="flex items-center gap-4">
+				<h4 class="truncate text-lg font-medium">{provider.name}</h4>
+				{#if [isModelsLoading, isSaving, isConfigurationLoading].some(Boolean)}
+					<div
+						class="flex justify-center"
+						in:fade={{ duration: 1000 }}
+						out:fade={{ duration: 600 }}
+					>
+						<Loader2 class="size-5 animate-spin" />
+					</div>
+				{/if}
+			</div>
+
+			{#if provider.configured}
+				<CheckCircleIcon class="text-blue ml-auto aspect-square h-5" />
+			{/if}
+		</div>
+
+		{#if provider.description}
+			<div class="markdown-content text-gray text-sm">
+				{@html toHTMLFromMarkdown(provider.description)}
+			</div>
+		{:else}
+			<div class="bg-surface1 text-gray flex h-14 w-full items-center rounded-lg px-4 py-3 text-sm">
+				<p>Description is not available</p>
path: ui/user/src/lib/components/ModelProviderCard.svelte
user: login: ryu-man
body: I made the provider's card to have a fixed height to prevent UI jumping in & out when the content grows and shrinks. Those placeholders are used to fill up the empty space and help the user expecting what to see behind them. I will see what @cjellick thinks about it
created_at: 2025-05-13T18:20:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -80,32 +65,57 @@
 	header="Model Providers"
 	helpText={HELPER_TEXTS.modelProviders}
 >
-	<div class="flex flex-col p-2">
-		<p class="pb-2 text-xs">
-			Agents use our included LLM by default. Connect other model providers to unlock more LLMs you
-			can chat with.
-		</p>
-
-		{#if isLoading}
-			<p class="text-xs italic">Loading model providers...</p>
-		{:else if project?.models && Object.keys(project.models).length > 0}
-			<div class="pb-2">
-				<p class="mb-1 text-xs font-medium">Configured providers:</p>
-				<ul class="space-y-0.5 pl-3 text-xs">
-					{#each getSortedProviderIds(Object.keys(project.models)) as providerId}
-						<li class="flex items-center gap-1">
-							<span>{getProviderName(providerId)}</span>
-							<span class="text-muted-foreground"
-								>({project.models[providerId].length}
-								{project.models[providerId].length === 1 ? 'model' : 'models'})</span
-							>
-						</li>
-					{/each}
-				</ul>
-			</div>
-		{:else}
-			<p class="text-xs">No model providers configured</p>
-		{/if}
+	<div class="flex flex-col gap-8 p-2">
+		<div class="flex flex-col gap-4">
+			<p class="text-xs font-light text-gray-500">
+				Agents use our included LLM by default. Connect other model providers to unlock more LLMs
+				you can chat with.
+			</p>
+
+			{#if isLoading}
+				<p class="text-xs italic">Loading model providers...</p>
+			{:else if project?.models && Object.keys(project.models).length > 0}
+				<div class="pb-2">
+					<p class="mb-1 text-sm font-medium">Configured providers:</p>
+					<ul class="flex flex-col text-xs">
+						{#each projectProviderModelIds as providerId}
+							{@const provider = providersWithMoreData.get(providerId)}
+
+							<li class="model-provider flex items-center gap-1 py-2">
+								<div class="size-4">
path: ui/user/src/lib/components/sidebar/ModelProviders.svelte
user: login: ryu-man
body: I will add that
created_at: 2025-05-13T18:21:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -282,194 +163,64 @@
 		<div class="text-muted">No model providers available</div>
 	{:else}
 		<!-- Default Model Provider Section -->
-		<div class="mb-6">
-			<h4 class="text-md mb-2 font-medium">Default Model</h4>
-			<p class="text-muted mb-3 text-sm">
-				Select the default model for this project. This will be used when no specific model is
-				specified.
-			</p>
-
-			{#if !Object.keys(selectedModels).length}
-				<div class="mb-2 text-sm text-yellow-500">
-					Configure at least one provider and select models to set a default model.
-				</div>
-			{:else}
-				<div>
-					<select
-						class="w-full rounded-md border p-2 text-sm"
-						value={defaultModelProvider && defaultModel
-							? `${defaultModel}|||${defaultModelProvider}`
-							: ''}
-						onchange={(e) => updateDefaultModel(e.currentTarget.value)}
+		<div class={twMerge('mb-6 flex flex-col gap-2', !hasOneModelSelected && 'opacity-50')}>
+			<h4 class="text-md font-medium">Default Model</h4>
+
+			<div>
+				<select
+					class="border-surface2 dark:bg-surface1 w-full rounded-md border px-4 py-4 text-sm"
path: ui/user/src/lib/components/ModelProviders.svelte
user: login: ryu-man
body: Nice catch. That's a select element; its chevron does not respond to padding. We probably need a UI library or to create our own at some point. I have to create a custom chevron
created_at: 2025-05-13T18:30:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -282,189 +170,59 @@
 		<div class="text-muted">No model providers available</div>
 	{:else}
 		<!-- Default Model Provider Section -->
-		<div class="mb-6">
-			<h4 class="text-md mb-2 font-medium">Default Model</h4>
-			<p class="text-muted mb-3 text-sm">
-				Select the default model for this project. This will be used when no specific model is
-				specified.
-			</p>
-
-			{#if !Object.keys(selectedModels).length}
-				<div class="mb-2 text-sm text-yellow-500">
-					Configure at least one provider and select models to set a default model.
-				</div>
-			{:else}
-				<div>
-					<select
-						class="w-full rounded-md border p-2 text-sm"
-						value={defaultModelProvider && defaultModel
-							? `${defaultModel}|||${defaultModelProvider}`
-							: ''}
-						onchange={(e) => updateDefaultModel(e.currentTarget.value)}
+		<div class={twMerge('mb-6 flex flex-col gap-2', !hasOneModelSelected && 'opacity-50')}>
+			<h4 class="text-md font-medium">Default Model</h4>
+
+			<!-- Not the best approach -->
+			<!-- TODO: Use a 3rd-party UI library or create an internal custom component in the future -->
+			<div class="relative">
+				<select
+					class="border-surface2 dark:bg-surface1 w-full appearance-none rounded-md border px-4 py-4 text-sm"
+					value={defaultModelProvider && defaultModel
+						? `${defaultModel}|||${defaultModelProvider}`
+						: ''}
+					onchange={(e) => updateDefaultModel(e.currentTarget.value)}
+					onclick={() => (isDefaultModelSelectOpen = !isDefaultModelSelectOpen)}
+				>
+					<option class="dark:bg-surface3" value="" disabled
+						>Select the default model for {project.name} project</option
path: ui/user/src/lib/components/ModelProviders.svelte
user: login: cjellick
body: we do not use the term project. can you change this text to Select the default model for this agent
created_at: 2025-05-20T21:06:59Z
author_association: CONTRIBUTOR

PR comments:
user: login: cjellick
created_at: 2025-05-15T17:03:18Z
body: Feedback:

- In the left nav, I don’t like the notification background color for "No model providers configured". It is the only thing we have like that in the left nave and it sticks out too much. lets drop the background color
- Put this description under the Model Providers title at the top:
    - Configure model providers and select models to make them available to all threads and tasks. The default model will be used in place of Obot’s built-in model.
- Drop the bit of text under the default model drop down
- Drop the description sections in each model provider. not needed.
- The model selector needs a box around it. i don't know where it ends and it makes me over scroll
- The unconfigure button opens an old school modal. do not want those in the app at all
- Change the text inside the configure square of each model provider to this:
    - Click on the "Configure" button below to set up this provider.  We’ll then validate your configuration and display available models.
- The API key's help text cuts off in the openai provider
- The model search bar does not work how i want it. i want to be able to type gpt- and it limits to just models that start exactly that way. it should match form the start of the word
- In the openai provider, bubble models that start with gpt to the top of the list
- In chat, the model selector is too skinny. It should be as wide as the names in the selector
- In chat, change this text: "No model is available. Configure you provider first!" to this:
    - You can add additional models under the Model Providers tab in the Configuration section.
    - Also, pad that out a bit so it is nicely presented. it felt really cramped.

You can do these in a follow up pr, they are lower priority:
- if i configure with a bad key i get a 500 error, id like that displayed in that model provider’s section, guiding them to update their configuration
- When i create a new thread, i would like to be able to pick the model before every sending a message. right now the drop down doesnt show up until after my first message is sent.

Here is a loom of me going over this feedback:
https://www.loom.com/share/dbc68472302f4098923573f8c2e6d854
author_association: CONTRIBUTOR
user: login: ryu-man
created_at: 2025-05-16T23:53:44Z
body: @cjellick I am not sure I understand what you mean by adding a box around the selector!
author_association: CONTRIBUTOR
user: login: cjellick
created_at: 2025-05-17T00:00:08Z
body: just want a subtle boundary around the selector box of models
<img width="976" alt="Screenshot 2025-05-16 at 4 59 00 PM" src="https://github.com/user-attachments/assets/3e94bc8f-6a5e-40c2-9c56-724e18fb3b41" />

author_association: CONTRIBUTOR
user: login: ryu-man
created_at: 2025-05-17T00:28:45Z
body: @cjellick all changes were made except errors and models selector in chat are not addressed in these changes
author_association: CONTRIBUTOR