gestion des providers
This commit is contained in:
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),
|
||||
};
|
||||
@@ -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"> </span>
|
||||
{/if}
|
||||
@@ -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>
|
||||
|
||||
@@ -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: "",
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user