refac: frontend

This commit is contained in:
Timothy Jaeryang Baek
2024-11-15 02:05:43 -08:00
parent 2ab5b2fd71
commit d9dc04f1a1
20 changed files with 1340 additions and 1407 deletions

View File

@@ -112,7 +112,7 @@
</div>
</div>
<div class="my-3 mb-5 grid md:grid-cols-2 lg:grid-cols-3 gap-2">
<div class="my-3 mb-5 grid lg:grid-cols-2 xl:grid-cols-3 gap-2">
{#each filteredItems as item}
<button
class=" flex space-x-4 cursor-pointer text-left w-full px-4 py-3 border border-gray-50 dark:border-gray-850 dark:hover:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-xl"

View File

@@ -12,7 +12,12 @@
const i18n = getContext('i18n');
import { WEBUI_NAME, config, mobile, models as _models, settings, user } from '$lib/stores';
import { addNewModel, deleteModelById, getModelInfos, updateModelById } from '$lib/apis/models';
import {
createNewModel,
deleteModelById,
getWorkspaceModels,
updateModelById
} from '$lib/apis/models';
import { getModels } from '$lib/apis';
@@ -38,29 +43,15 @@
let showModelDeleteConfirm = false;
$: if (models) {
filteredModels = models
.filter((m) => m?.owned_by !== 'arena')
.filter(
(m) => searchValue === '' || m.name.toLowerCase().includes(searchValue.toLowerCase())
);
filteredModels = models.filter(
(m) => searchValue === '' || m.name.toLowerCase().includes(searchValue.toLowerCase())
);
}
let sortable = null;
let searchValue = '';
const deleteModelHandler = async (model) => {
console.log(model.info);
if (!model?.info) {
toast.error(
$i18n.t('{{ owner }}: You cannot delete a base model', {
owner: model.owned_by.toUpperCase()
})
);
return null;
}
const res = await deleteModelById(localStorage.token, model.id);
if (res) {
toast.success($i18n.t(`Deleted {{name}}`, { name: model.id }));
}
@@ -70,17 +61,12 @@
};
const cloneModelHandler = async (model) => {
if ((model?.info?.base_model_id ?? null) === null) {
toast.error($i18n.t('You cannot clone a base model'));
return;
} else {
sessionStorage.model = JSON.stringify({
...model,
id: `${model.id}-clone`,
name: `${model.name} (Clone)`
});
goto('/workspace/models/create');
}
sessionStorage.model = JSON.stringify({
...model,
id: `${model.id}-clone`,
name: `${model.name} (Clone)`
});
goto('/workspace/models/create');
};
const shareModelHandler = async (model) => {
@@ -104,58 +90,6 @@
window.addEventListener('message', messageHandler, false);
};
const moveToTopHandler = async (model) => {
// find models with position 0 and set them to 1
const topModels = models.filter((m) => m.info?.meta?.position === 0);
for (const m of topModels) {
let info = m.info;
if (!info) {
info = {
id: m.id,
name: m.name,
meta: {
position: 1
},
params: {}
};
}
info.meta = {
...info.meta,
position: 1
};
await updateModelById(localStorage.token, info.id, info);
}
let info = model.info;
if (!info) {
info = {
id: model.id,
name: model.name,
meta: {
position: 0
},
params: {}
};
}
info.meta = {
...info.meta,
position: 0
};
const res = await updateModelById(localStorage.token, info.id, info);
if (res) {
toast.success($i18n.t(`Model {{name}} is now at the top`, { name: info.id }));
}
await _models.set(await getModels(localStorage.token));
models = $_models;
};
const hideModelHandler = async (model) => {
let info = model.info;
@@ -206,65 +140,8 @@
saveAs(blob, `${model.id}-${Date.now()}.json`);
};
const positionChangeHandler = async () => {
// Get the new order of the models
const modelIds = Array.from(document.getElementById('model-list').children).map((child) =>
child.id.replace('model-item-', '')
);
// Update the position of the models
for (const [index, id] of modelIds.entries()) {
const model = $_models.find((m) => m.id === id);
if (model) {
let info = model.info;
if (!info) {
info = {
id: model.id,
name: model.name,
meta: {
position: index
},
params: {}
};
}
info.meta = {
...info.meta,
position: index
};
await updateModelById(localStorage.token, info.id, info);
}
}
await tick();
await _models.set(await getModels(localStorage.token));
};
onMount(async () => {
if ($user?.role === 'admin') {
models = $_models;
// Legacy code to sync localModelfiles with models
localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
if (localModelfiles) {
console.log(localModelfiles);
}
if (!$mobile) {
// SortableJS
sortable = new Sortable(document.getElementById('model-list'), {
animation: 150,
onUpdate: async (event) => {
console.log(event);
positionChangeHandler();
}
});
}
} else {
models = [];
}
models = await getWorkspaceModels(localStorage.token);
const onKeyDown = (event) => {
if (event.key === 'Shift') {
@@ -376,47 +253,35 @@
>
<div class=" self-start w-8 pt-0.5">
<div
class=" rounded-full object-cover {(model?.info?.meta?.hidden ?? false)
class=" rounded-full object-cover {(model?.meta?.hidden ?? false)
? 'brightness-90 dark:brightness-50'
: ''} "
>
<img
src={model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
src={model?.meta?.profile_image_url ?? '/static/favicon.png'}
alt="modelfile profile"
class=" rounded-full w-full h-auto object-cover"
/>
</div>
</div>
<div
class=" flex-1 self-center {(model?.info?.meta?.hidden ?? false) ? 'text-gray-500' : ''}"
>
<div class=" flex-1 self-center {(model?.meta?.hidden ?? false) ? 'text-gray-500' : ''}">
<Tooltip
content={marked.parse(
model?.ollama?.digest
? `${model?.ollama?.digest} *(${model?.ollama?.modified_at})*`
: ''
)}
content={marked.parse(model?.meta?.description ?? model.id)}
className=" w-fit"
placement="top-start"
>
<div class=" font-semibold line-clamp-1">{model.name}</div>
</Tooltip>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1 text-gray-500">
{!!model?.info?.meta?.description
? model?.info?.meta?.description
: model?.ollama?.digest
? `${model.id} (${model?.ollama?.digest})`
: model.id}
{model?.meta?.description ?? model.id}
</div>
</div>
</a>
<div class="flex flex-row gap-0.5 self-center">
{#if $user?.role === 'admin' && shiftKey}
<Tooltip
content={(model?.info?.meta?.hidden ?? false)
? $i18n.t('Show Model')
: $i18n.t('Hide Model')}
content={(model?.meta?.hidden ?? false) ? $i18n.t('Show Model') : $i18n.t('Hide Model')}
>
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
@@ -425,7 +290,7 @@
hideModelHandler(model);
}}
>
{#if model?.info?.meta?.hidden ?? false}
{#if model?.meta?.hidden ?? false}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
@@ -511,9 +376,6 @@
exportHandler={() => {
exportModelHandler(model);
}}
moveToTopHandler={() => {
moveToTopHandler(model);
}}
hideHandler={() => {
hideModelHandler(model);
}}
@@ -561,7 +423,7 @@
return null;
});
} else {
await addNewModel(localStorage.token, model.info).catch((error) => {
await createNewModel(localStorage.token, model.info).catch((error) => {
return null;
});
}

View File

@@ -17,6 +17,7 @@
import { getTools } from '$lib/apis/tools';
import { getFunctions } from '$lib/apis/functions';
import { getKnowledgeItems } from '$lib/apis/knowledge';
import AccessPermissions from '../common/AccessPermissionsModal.svelte';
const i18n = getContext('i18n');
@@ -82,7 +83,7 @@
if (baseModel) {
if (baseModel.owned_by === 'openai') {
capabilities.usage = baseModel.info?.meta?.capabilities?.usage ?? false;
capabilities.usage = basemodel?.meta?.capabilities?.usage ?? false;
} else {
delete capabilities.usage;
}
@@ -159,33 +160,31 @@
id = model.id;
if (model.info.base_model_id) {
if (model.base_model_id) {
const base_model = $models
.filter((m) => !m?.preset && m?.owned_by !== 'arena')
.find((m) =>
[model.info.base_model_id, `${model.info.base_model_id}:latest`].includes(m.id)
);
.find((m) => [model.base_model_id, `${model.base_model_id}:latest`].includes(m.id));
console.log('base_model', base_model);
if (base_model) {
model.info.base_model_id = base_model.id;
model.base_model_id = base_model.id;
} else {
model.info.base_model_id = null;
model.base_model_id = null;
}
}
params = { ...params, ...model?.info?.params };
params = { ...params, ...model?.params };
params.stop = params?.stop
? (typeof params.stop === 'string' ? params.stop.split(',') : (params?.stop ?? [])).join(
','
)
: null;
toolIds = model?.info?.meta?.toolIds ?? [];
filterIds = model?.info?.meta?.filterIds ?? [];
actionIds = model?.info?.meta?.actionIds ?? [];
knowledge = (model?.info?.meta?.knowledge ?? []).map((item) => {
toolIds = model?.meta?.toolIds ?? [];
filterIds = model?.meta?.filterIds ?? [];
actionIds = model?.meta?.actionIds ?? [];
knowledge = (model?.meta?.knowledge ?? []).map((item) => {
if (item?.collection_name) {
return {
id: item.collection_name,
@@ -203,7 +202,7 @@
return item;
}
});
capabilities = { ...capabilities, ...(model?.info?.meta?.capabilities ?? {}) };
capabilities = { ...capabilities, ...(model?.meta?.capabilities ?? {}) };
if (model?.owned_by === 'openai') {
capabilities.usage = false;
}
@@ -212,8 +211,8 @@
...info,
...JSON.parse(
JSON.stringify(
model?.info
? model?.info
model
? model
: {
id: model.id,
name: model.name
@@ -441,6 +440,26 @@
{/if}
</div>
<div class="my-1">
<div class="">
<Tags
tags={info?.meta?.tags ?? []}
on:delete={(e) => {
const tagName = e.detail;
info.meta.tags = info.meta.tags.filter((tag) => tag.name !== tagName);
}}
on:add={(e) => {
const tagName = e.detail;
if (!(info?.meta?.tags ?? null)) {
info.meta.tags = [{ name: tagName }];
} else {
info.meta.tags = [...info.meta.tags, { name: tagName }];
}
}}
/>
</div>
</div>
<hr class=" dark:border-gray-850 my-1.5" />
<div class="my-2">
@@ -620,28 +639,8 @@
<Capabilities bind:capabilities />
</div>
<div class="my-1">
<div class="flex w-full justify-between items-center">
<div class=" self-center text-sm font-semibold">{$i18n.t('Tags')}</div>
</div>
<div class="mt-2">
<Tags
tags={info?.meta?.tags ?? []}
on:delete={(e) => {
const tagName = e.detail;
info.meta.tags = info.meta.tags.filter((tag) => tag.name !== tagName);
}}
on:add={(e) => {
const tagName = e.detail;
if (!(info?.meta?.tags ?? null)) {
info.meta.tags = [{ name: tagName }];
} else {
info.meta.tags = [...info.meta.tags, { name: tagName }];
}
}}
/>
</div>
<div class="my-2">
<AccessPermissions />
</div>
<div class="my-2 text-gray-300 dark:text-gray-700">

View File

@@ -23,7 +23,6 @@
export let cloneHandler: Function;
export let exportHandler: Function;
export let moveToTopHandler: Function;
export let hideHandler: Function;
export let deleteHandler: Function;
export let onClose: Function;
@@ -83,71 +82,6 @@
<div class="flex items-center">{$i18n.t('Export')}</div>
</DropdownMenu.Item>
{#if user?.role === 'admin'}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
moveToTopHandler();
}}
>
<ArrowUpCircle />
<div class="flex items-center">{$i18n.t('Move to Top')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
hideHandler();
}}
>
{#if model?.info?.meta?.hidden ?? false}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88"
/>
</svg>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
{/if}
<div class="flex items-center">
{#if model?.info?.meta?.hidden ?? false}
{$i18n.t('Show Model')}
{:else}
{$i18n.t('Hide Model')}
{/if}
</div>
</DropdownMenu.Item>
{/if}
<hr class="border-gray-100 dark:border-gray-800 my-1" />
<DropdownMenu.Item

View File

@@ -0,0 +1,101 @@
<script lang="ts">
import { getContext } from 'svelte';
const i18n = getContext('i18n');
export let type = 'private';
let query = '';
</script>
<div>
<div>
<div class=" text-sm font-semibold mb-0.5">{$i18n.t('People with access')}</div>
<div>
<div class="flex w-full py-1">
<div class="flex flex-1">
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
clip-rule="evenodd"
/>
</svg>
</div>
<input
class=" w-full text-sm pr-4 placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none bg-transparent"
bind:value={query}
placeholder={$i18n.t('Add user or groups')}
/>
</div>
</div>
</div>
</div>
<div class="">
<div class=" text-sm font-semibold mb-2">{$i18n.t('General access')}</div>
<div class="flex gap-2.5 items-center">
<div>
<div class=" p-2 bg-gray-700 rounded-full">
{#if type === 'private'}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z"
/>
</svg>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.115 5.19l.319 1.913A6 6 0 008.11 10.36L9.75 12l-.387.775c-.217.433-.132.956.21 1.298l1.348 1.348c.21.21.329.497.329.795v1.089c0 .426.24.815.622 1.006l.153.076c.433.217.956.132 1.298-.21l.723-.723a8.7 8.7 0 002.288-4.042 1.087 1.087 0 00-.358-1.099l-1.33-1.108c-.251-.21-.582-.299-.905-.245l-1.17.195a1.125 1.125 0 01-.98-.314l-.295-.295a1.125 1.125 0 010-1.591l.13-.132a1.125 1.125 0 011.3-.21l.603.302a.809.809 0 001.086-1.086L14.25 7.5l1.256-.837a4.5 4.5 0 001.528-1.732l.146-.292M6.115 5.19A9 9 0 1017.18 4.64M6.115 5.19A8.965 8.965 0 0112 3c1.929 0 3.716.607 5.18 1.64"
/>
</svg>
{/if}
</div>
</div>
<div>
<select
id="models"
class="outline-none bg-transparent text-sm font-medium rounded-lg block w-fit pr-10 max-w-full placeholder-gray-400"
bind:value={type}
>
<option class=" text-gray-700" value="private" selected>Private</option>
<option class=" text-gray-700" value="public" selected>Public</option>
</select>
<div class=" text-xs text-gray-400 font-medium">
{#if type === 'private'}
{$i18n.t('Only users with permission can access')}
{:else if type === 'public'}
{$i18n.t('Accessible to all users')}
{/if}
</div>
</div>
</div>
</div>
</div>