gestion des providers

This commit is contained in:
fabien committed 2026-08-31 21:23:35 +02:00
1 parent 357bf9b41c
commit 4d72952b2c
46 files changed
+995 -150

No files matched your search

@@ -0,0 +1,13 @@
import { api } from '../client';
import type { Provider, Item } from "$lib/types/api"
import { space } from "$lib/state/space.svelte"
export const providerApi = {
providerTypes: () => api.get<Item[]>('/provider/providerTypes'),
getProviders: () => api.get<Provider[]>(`/provider/${space.id}`),
getProvider: (id: string) => api.get<Provider>(`/provider/${space.id}/${id}`),
createProvider: (data: Partial<Provider>) => api.post<Provider>(`/provider/${space.id}`, data),
updateProvider: (data: Partial<Provider>) => api.put<Provider>(`/provider/${space.id}`, data),
deleteProvider: (id: string) => api.delete(`/provider/${space.id}/${id}`),
getAvailableModels: (data: Partial<Provider>) => api.post<string[]>(`/provider/${space.id}/avalable-models`, data),
};
+2 -2
View File
@@ -1,8 +1,8 @@
import { api } from '../client';
import type { User, Address } from "$lib/types/api"
import type { User, Address, UserWithTeams } from "$lib/types/api"
export const userApi = {
me: () => api.get<User>('/user/me'),
me: () => api.get<UserWithTeams>('/user/me'),
update: (id: string, data: Partial<User>) => api.put<User>(`/user/${id}`, data),
listAddresses: (id: string) => api.get<Address[]>(`/user/${id}/address`),
@@ -8,6 +8,4 @@
{#if errors.has(key)}
<span class="text-red-700 dark:text-red-400 text-xs">{errors.get(key)}</span>
{:else}
<span class="text-xs">&nbsp;</span>
{/if}
+28 -13
View File
@@ -3,6 +3,7 @@
import Menu from "$lib/components/Menu.svelte"
import { logout } from "$lib/state/auth.svelte"
import logo from "$lib/assets/logo-txt.png"
import { space } from '$lib/state/space.svelte'
let {handleMenuDesktop, handleMenuMobile} = $props();
@@ -12,7 +13,7 @@
</script>
<div class="flex flex-row italic mb-4">
<div class="flex flex-row italic mb-2">
{#if uiState.menuContentVisible}
<div class="flex-1 p-3 font openonly">
<img src={logo} class="h-5" alt="logo"/>
@@ -25,24 +26,38 @@
<i class="icon-menu pointer block p-2"></i>
</button>
</div>
{#if uiState.menuContentVisible}
<div class="m-2">
<div class="formelement">
<select class="textinput">
{#each space.spaces as s (s.id)}
<option value={s.id}>{s.label}</option>
{/each}
</select>
</div>
</div>
{/if}
<div id="menu" class="flex-1 overflow-y-scroll">
<Menu label="Conversation" icon="icon-chat" link="/"/>
<Menu label="Tâches" icon="icon-tasks" link="/tasks"/>
<Menu label="Nouveau chat" icon="icon-plus" link="/"/>
<Menu label="Conversations" icon="icon-chat" link="/history"/>
<Menu label="Planification" icon="icon-tasks" link="/tasks"/>
<Menu label="Connaissances" icon="icon-graduation-cap" link="/kb"/>
<Menu label="Historique" icon="icon-history" link="/history"/>
<Menu label="Paramétrage" icon="icon-cog" link="/settings"/>
<Menu label="Réglages" icon="icon-cog" link="/settings"/>
</div>
<div id="profile" class="w-full flex flex-row items-center border-t border-stone-200 dark:border-stone-800">
<div id="profile" class="w-full items-center border-t border-stone-200 dark:border-stone-800">
{#if uiState.menuContentVisible}
<div class="p-4 flex-1 profile">
<div>Fabien Masson</div>
<div class="flex">
<a href="/profile" class="text-xs flex-1">Modifier</a>
<div class="flex flex-row gap-2 p-2 ">
<div class="py-1 flex-1 flex flex-row text-xs">
<div class="w-6 h-6 rounded-full bg-cyan-600 text-white flex items-center justify-center font-medium">
FM
</div>
<div class="flex-1 ml-2 inline-flex items-center">Fabien Masson</div>
</div>
</div>
<div class="p-2">
<button class="w-full bt" onclick={handleLogout} title="Déconecter">
<a href="/profile" class="bt small inline-flex items-center justify-center" title="Edition profil">
<i class="icon-edit"></i>
</a>
<button class="bt small" onclick={handleLogout} title="Déconnecter">
<i class="icon-logout"></i>
</button>
</div>
+3 -5
View File
@@ -1,15 +1,13 @@
import { type User } from "$lib/types/api"
import type { User } from "$lib/types/api"
type AuthState = {
user: User | null;
status: 'loading' | 'authenticated' | 'unauthenticated';
//accessToken: string | null
user: User | null
status: 'loading' | 'authenticated' | 'unauthenticated'
}
export const auth: AuthState = $state({
user: null,
status: 'loading',
//accessToken: null
})
export const logout = () => {
@@ -0,0 +1,6 @@
import type { Team } from "$lib/types/api"
export const space = $state({
spaces: [] as Team[],
id: "",
});
+50
View File
@@ -1,3 +1,8 @@
export type Item = {
id: string
text: string
}
export interface User {
id: string
firstname: string
@@ -9,9 +14,54 @@ export interface User {
_version: string
}
export interface UserTeam {
id: string
userId: string
teamId: string
administrator: boolean
_version: string
}
export interface Team {
id: string
label: string
defaultModelId: string
_version: string
}
export interface UserWithTeams {
user: User
userTeams: UserTeam[]
teams: Team[]
}
export interface Address {
id: string
type: string
address: string
_version: string
}
export interface Provider {
id: string
teamId: string
type: string
name: string
apiKey: string
baseUrl: string
models: string
_date_created: string
_date_updated: string
_version: string
}
export interface Model {
id: string
teamId: string
providerId: string
modelName: string
configuration: object
_date_created: string
_date_updated: string
_version: string
}
@@ -1,5 +1,10 @@
const emailRegex: RegExp = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
const urlRegex: RegExp = /^https?:\/\/([a-zA-Z0-9.-]+|\[[0-9a-fA-F:.]+\])(:\d+)?(\/[^\s]*)?$/;
export function validateEmail(input: string): boolean {
return emailRegex.test(input);
}
export function validateUrl(input: string): boolean {
return urlRegex.test(input);
}
@@ -1,7 +1,8 @@
<div class="container">
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
</div>
@@ -3,6 +3,7 @@
import { page } from '$app/state';
import {uiState} from "$lib/state/ui.svelte"
import { auth } from '$lib/state/auth.svelte'
import { space } from '$lib/state/space.svelte'
import Sidebar from "$lib/components/Sidebar.svelte"
import { goto } from '$app/navigation'
import { onMount } from "svelte";
@@ -39,7 +40,9 @@
onMount(async () => {
handleResize()
if (data.me) {
auth.user = data.me
auth.user = data.me.user
space.spaces = data.me.teams
space.id = data.me.teams[0].id
themeToLocalStorage(auth.user.theme)
applyTheme()
auth.status = 'authenticated'
@@ -64,7 +67,7 @@
</div>
<div id="main">
<div class="flex flex-col h-screen ">
<div class={["header", page.data.theme]}>
<div class={["header", "container" , page.data.theme]}>
<h1 class="flex">
<span class="flex-1">{page.data.title}</span>
{#if page.data.back}
@@ -2,7 +2,7 @@
import { SvelteMap } from 'svelte/reactivity';
import type { PageProps } from './$types';
import type { User, Address } from '$lib/types/api'
import type { User, Address, Item } from '$lib/types/api'
import { userApi } from '$lib/api/endpoints/user'
import { miscApi } from '$lib/api/endpoints/misc'
import FormError from '$lib/components/FormError.svelte';
@@ -13,15 +13,15 @@
let { data }: PageProps = $props();
let form : User = $state(data.me)
let form : User = $state(data.me.user)
let addresses : Address[] = $state([] as Address[])
let newAddress : Address = $state({} as Address)
let timezones = $state([] as string[])
let timezones = $state([] as Item[])
let errors = new SvelteMap<string,string>()
onMount(async ()=> {
addresses = await userApi.listAddresses(form.id)
timezones = await miscApi.listTimezones()
timezones = (await miscApi.listTimezones()).map((it)=>{return {id:it, text:it}})
})
$effect(()=>{
@@ -64,24 +64,26 @@
</script>
<div class="mb-4 formgroup">
<div class="mb-4 formgroup container">
<h2>Mon compte</h2>
<form onsubmit={submitUser} onchange={()=>{errors.clear()}}>
<label for="email" class="block my-1 flex-1">Email</label>
<input
id="email"
type="text"
bind:value={form.email}
class="textinput"
autofocus
/>
<FormError key="email" errors={errors} />
<div class="formelement">
<label for="email" class="my-1 flex-1">Email</label>
<input
id="email"
type="text"
bind:value={form.email}
class="textinput"
autofocus
/>
<FormError key="email" errors={errors} />
</div>
<div class="flex">
<div class="flex-1 mr-2">
<label for="firstname" class="block my-1 flex-1">Prénom</label>
<div class="grid grid-cols-1 gap-x-4 md:grid-cols-2">
<div class="formelement">
<label for="firstname" class=" my-1 flex-1">Prénom</label>
<input
id="firstname"
type="text"
@@ -91,8 +93,8 @@
<FormError key="firstname" errors={errors} />
</div>
<div class="flex-1 ml-2">
<label for="lastname" class="block my-1 flex-1">Nom</label>
<div class="formelement">
<label for="lastname" class="my-1 flex-1">Nom</label>
<input
id="lastname"
type="text"
@@ -103,9 +105,9 @@
</div>
</div>
<div class="flex">
<div class="grid grid-cols-1 gap-x-4 md:grid-cols-2">
<div class="flex-1 mr-2">
<div class="formelement">
<label for="lang" class="block my-1 flex-1">Langue</label>
<select bind:value={form.lang} class="textinput">
<option value="en">English</option>
@@ -114,14 +116,14 @@
<FormError key="lang" errors={errors} />
</div>
<div class="flex-1 ml-2">
<label for="lang" class="block my-1 flex-1">Timezone</label>
<div class="formelement">
<label for="timezone" class="block my-1 flex-1">Timezone</label>
<Select items={timezones} bind:value={form.timezone}/>
<FormError key="lang" errors={errors} />
<FormError key="timezone" errors={errors} />
</div>
</div>
<div class="flex-1 mr-2">
<div class="formelement">
<label for="theme" class="block my-1 flex-1">Thème</label>
<select bind:value={form.theme} class="textinput">
<option value="auto">Automatique</option>
@@ -137,14 +139,14 @@
</div>
<div class="mb-4 formgroup">
<div class="mb-4 formgroup container">
<h2>Canaux de communication</h2>
{#each addresses as address (address.id) }
<div class="m-2">
<div class="dark:bg-stone-700 bg-stone-300 rounded flex inline px-2 py-1">
<div class="dark:bg-stone-700 bg-stone-300 rounded inline px-2 py-1 text-sm">
<span class="inline-block w-20">{address.type} : </span>
<span class="font-bold">{address.address}</span>
<span class="">{address.address}</span>
<button class="ml-4 pointer" onclick={()=>{handleDeleteAddress(address.id)}}>
<i class="icon-cancel"></i>
</button>
@@ -1,4 +1,4 @@
<div class="grid grid-cols-[repeat(auto-fit,minmax(250px,1fr))] gap-4">
<div class="container grid grid-cols-[repeat(auto-fit,minmax(250px,1fr))] gap-4">
<a class="bt p-5 text-center"
title="Modèles"
@@ -1 +1,157 @@
<div>Modèles</div>
<script lang="ts">
import { SvelteMap } from 'svelte/reactivity';
import type { Provider} from '$lib/types/api'
import type { PageProps } from './$types';
import { providerApi } from "$lib/api/endpoints/provider"
import { onMount } from 'svelte';
import { validateUrl } from '$lib/utilities/validators'
import ProviderForm from './ProviderForm.svelte'
let { data }: PageProps = $props();
let providers = $state([] as Provider[])
let addingProvider = $state(false)
let editingProvider = $state(null as string|null)
let providerForm = $state({} as Provider)
let errors = new SvelteMap<string,string>()
onMount(async ()=>{
providers = await providerApi.getProviders()
})
const enterAddProviderHandler = () => {
if (editingProvider) return
errors.clear()
providerForm = {} as Provider
providerForm.models = ""
addingProvider= true
}
const cancelAddProviderHandler = () => {
addingProvider= false
}
const submitAddProvider = (p: Provider) => {
if (!p.name) {
errors.set("name","Le nom est obligatoire !")
}
if (!p.type) {
errors.set("type","Le type est obligatoire !")
}
if (p.baseUrl && !validateUrl(p.baseUrl)) {
errors.set("baseUrl","Mauvaise URL !")
}
if (errors.size === 0) {
providerApi.createProvider(p)
.then((p)=>{
providers.push(p)
addingProvider= false
providerForm = {} as Provider
})
.catch((err)=>{
console.log(err)
})
}
}
const cancelEditProviderHandler = () => {
editingProvider = null
providerForm = {} as Provider
}
const submitEditProvider = (p: Provider) => {
if (!p.name) {
errors.set("name","Le nom est obligatoire !")
}
if (!p.type) {
errors.set("type","Le type est obligatoire !")
}
if (p.baseUrl && !validateUrl(p.baseUrl)) {
errors.set("baseUrl","Mauvaise URL !")
}
if (errors.size === 0) {
providerApi.updateProvider(p)
.then((p)=>{
providers = providers.map((it)=>{return p.id===it.id ? p : it})
editingProvider = null
providerForm = {} as Provider
})
.catch((err)=>{
console.log(err)
})
}
}
const handleEditProvider = async (id: string) => {
errors.clear()
if (addingProvider) return
providerApi.getProvider(id)
.then((data)=>{
providers = providers.map((it)=>it.id===id ? data : it)
editingProvider = id
})
}
const handleDeleteProvider = (id: string) => {
providerApi.deleteProvider(id)
.then(() => {
providers = providers.filter((it)=>it.id!==id)
})
.catch((err) => {console.log(err)})
}
</script>
<div class="container">
{#if addingProvider}
{#key "new"}
<ProviderForm
item={providerForm}
errors={errors}
providerTypes={data.providerTypes}
onSave={submitAddProvider}
onCancel={cancelAddProviderHandler}
/>
{/key}
{:else}
<div class="rounded border border-dashed p-2 border-stone-500 cursor-pointer" onclick={enterAddProviderHandler} tabindex="0">
<i class="icon-plus"></i>
Ajouter un fournisseur
</div>
{/if}
{#each providers as p (p.id) }
{#if editingProvider===p.id}
{#key p.id}
<ProviderForm
item={p}
errors={errors}
providerTypes={data.providerTypes}
onSave={submitEditProvider}
onCancel={cancelEditProviderHandler}
/>
{/key}
{:else}
<div class="rounded border p-2 my-2 border-stone-500 flex">
<div class="flex-1">
<span>{ p.name }</span>
<span class="text-stone-500">({ p.type })</span>
</div>
<button class="bt small mx-1" onclick={()=>handleEditProvider(p.id)}>
<i class="icon-edit"></i>
</button>
<button class="bt small mx-1" onclick={()=>handleDeleteProvider(p.id)}>
<i class="icon-trash-empty"></i>
</button>
</div>
{/if}
{/each}
</div>
@@ -1,7 +1,11 @@
export function load() {
import { providerApi } from "$lib/api/endpoints/provider"
import type { PageLoad } from './$types';
export const load: PageLoad = async () => {
return {
title: 'Paramétrage : Modèles',
back: "/settings",
theme: "settings"
theme: "settings",
providerTypes: await providerApi.providerTypes(),
};
}
@@ -0,0 +1,142 @@
<script lang="ts">
import { SvelteMap } from 'svelte/reactivity';
import FormError from '$lib/components/FormError.svelte';
import Select from 'svelte-select/no-styles';
import type { Provider, Item } from "$lib/types/api"
import { providerApi } from "$lib/api/endpoints/provider"
type Props = {
item: Provider
errors: Map<string,string>
providerTypes: Item[]
onSave: (item: Provider) => void
onCancel: () => void
}
let {
item,
errors,
providerTypes,
onSave,
onCancel,
}: Props = $props();
type Selectable = {
value: string
selected: boolean
}
let selectedModels = $state(item.models.split(";"))
let availableModels : Selectable[]= $state(item.models.split(";").map((it)=> {return {value:it, selected:true}}))
let modelsErrors = new SvelteMap<string,string>()
const autofocus = (node: HTMLElement) => {
node.focus();
}
const handleSave = (e: Event) => {
e.preventDefault()
item.models = selectedModels.join(";")
onSave(item)
}
const handleCancel = (e: Event) => {
e.preventDefault()
onCancel()
}
const fetchModels = (e: Event) => {
e.preventDefault()
modelsErrors.clear()
providerApi.getAvailableModels(item)
.then((data) => {
availableModels = data.map((it)=>{
return { value: it, selected: selectedModels.includes(it)}
});
})
.catch((err) => { modelsErrors.set("models",err) })
}
const handleModelChange = () => {
selectedModels = availableModels.filter((it)=>it.selected).map((it)=>it.value)
}
</script>
<div>
<div class="formgroup my-2">
<form onsubmit={handleSave} onchange={()=>{errors.clear()}}>
<div class="formelement">
<label for="name">Nom</label>
<input
id="name"
type="text"
bind:value={item.name}
class="textinput"
placeholder="nom affiché"
use:autofocus
/>
<FormError key="name" errors={errors} />
</div>
<div class="formelement">
<label for="type" class="block my-1 flex-1">Type</label>
<Select items={providerTypes}
valueMode="id" itemId="id" label="text"
bind:value={item.type}
/>
<FormError key="type" errors={errors} />
</div>
<div class="formelement">
<label for="baseUrl">URL</label>
<input
id="baseUrl"
type="text"
bind:value={item.baseUrl}
class="textinput"
placeholder="http://127.0.0.1:8080/v1"
autocomplete="off"
/>
<FormError key="baseUrl" errors={errors} />
</div>
<div class="formelement">
<label for="apiKey">Clé API</label>
<input
id="apiKey"
type="password"
bind:value={item.apiKey}
class="textinput"
placeholder="clé API (optionnel)"
autocomplete="new-password"
/>
<FormError key="apiKey" errors={errors} />
</div>
<div class="formelement">
<label for="apiKey">Models</label>
<button class="bt small" onclick={fetchModels}>
Afficher les modèles disponibles
</button>
<ol class="ml-2">
{#each availableModels as model (model.value)}
{#if model.value}
<li>
<label>
<input type="checkbox" bind:checked={model.selected} onchange={handleModelChange}/>
{model.value}
</label>
</li>
{/if}
{/each}
</ol>
<div><FormError key="models" errors={modelsErrors} /></div>
</div>
<div class="text-right">
<button type="button" class="bt" onclick={handleCancel}>Annuler</button>
<button type="submit" class="bt mx-4">Enregistrer</button>
</div>
</form>
</div>
</div>
+34 -18
View File
@@ -8,35 +8,45 @@ html {
@apply h-full;
}
body {
@apply h-full bg-stone-100 text-stone-900 dark:bg-stone-900 dark:text-stone-200;
@apply h-full bg-stone-100 text-stone-950 dark:bg-stone-950 dark:text-stone-100;
}
label {
.formelement > label {
@apply text-xs text-stone-600 uppercase dark:text-stone-400;
}
hr {
@apply border-stone-200 dark:border-stone-800;
}
h2 {
@apply mb-4 font-bold;
@apply mb-4 text-base font-bold;
}
.container {
@apply mx-auto max-w-3xl;
}
.pointer {
@apply cursor-pointer;
}
.bt {
appearance: button;
@apply cursor-pointer rounded bg-stone-300 px-4 py-1 font-bold hover:bg-stone-200 dark:bg-stone-800 dark:hover:bg-stone-700;
&.small {
@apply px-2 py-1;
}
}
.menuitem {
@apply text-stone-800 text-stone-950 dark:text-stone-300 hover:dark:text-stone-50;
@apply text-stone-950 dark:text-stone-300 hover:dark:text-stone-50;
}
.header {
@apply p-2 text-lg;
@apply py-2 text-lg font-bold;
/*
h1 {
@apply rounded bg-stone-200 px-2 font-bold text-sky-600 dark:bg-stone-800 dark:text-sky-200;
@apply rounded bg-stone-200 px-2 font-bold uppercase dark:bg-stone-800;
}
*/
&.settings {
h1 {
@@ -53,10 +63,6 @@ h2 {
@apply text-sky-500;
}
.formgroup {
@apply rounded border border-stone-300 bg-stone-100 p-2 dark:border-stone-700 dark:bg-stone-900;
}
#outer {
@apply flex h-full flex-row;
}
@@ -94,7 +100,7 @@ h2 {
@apply ml-10;
}
#sidebar {
@apply fixed left-0 h-full w-full bg-stone-100 dark:bg-stone-900;
@apply fixed left-0 h-full w-full bg-stone-100 dark:bg-stone-950;
}
.mobileMenuClosed {
#sidebar {
@@ -103,14 +109,24 @@ h2 {
}
}
/* svelte select */
/* form */
.textinput {
@apply h-8 w-full rounded border-stone-700 bg-white px-2 py-1 text-stone-900 dark:bg-stone-800 dark:text-stone-100;
.formgroup {
@apply rounded border border-stone-300 bg-stone-100 p-2 text-sm dark:border-stone-700 dark:bg-stone-900;
}
.formelement {
@apply mb-3;
}
.textinput {
@apply h-8 w-full rounded border-stone-700 bg-stone-50 px-2 py-1 text-sm text-stone-900 dark:bg-stone-950 dark:text-stone-100;
}
/* svelte select */
.svelte-select {
@apply relative flex h-8 w-full border-none text-stone-900 dark:bg-stone-800 dark:text-stone-100;
@apply relative flex h-8 w-full border-none text-stone-900 dark:bg-stone-950 dark:text-stone-100;
}
.svelte-select .value-container {
@@ -187,7 +203,7 @@ h2 {
}
.svelte-select-list .list-group-title {
@apply h-8 cursor-default px-5 text-sm leading-10 font-medium overflow-ellipsis whitespace-nowrap text-gray-400 uppercase;
@apply h-8 cursor-default px-5 text-sm leading-10 font-medium text-ellipsis whitespace-nowrap text-gray-400 uppercase;
}
.svelte-select-list .empty {
@@ -195,7 +211,7 @@ h2 {
}
.svelte-select-item {
@apply h-8 cursor-default overflow-hidden px-5 leading-10 overflow-ellipsis whitespace-nowrap text-gray-800;
@apply h-8 cursor-default overflow-hidden px-5 leading-10 text-ellipsis whitespace-nowrap text-gray-800;
}
.item.group-item {
@@ -223,7 +239,7 @@ h2 {
}
.multi-item {
@apply mt-1 mr-1 flex h-8 max-w-full cursor-default items-center overflow-hidden rounded-sm border border-gray-200 bg-gray-100 pr-1 pl-1 leading-8 overflow-ellipsis whitespace-nowrap;
@apply mt-1 mr-1 flex h-8 max-w-full cursor-default items-center overflow-hidden rounded-sm border border-gray-200 bg-gray-100 pr-1 pl-1 leading-8 text-ellipsis whitespace-nowrap;
}
.multi-item.disabled {