mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-15 13:55:19 +00:00
前端模型选择器加载/展示用户私有模型,新增“添加API”入口,和删除功能
..
This commit is contained in:
parent
e03e1ed981
commit
dabbf320db
10 changed files with 565 additions and 121 deletions
|
|
@ -91,6 +91,7 @@ from open_webui.routers import (
|
||||||
evaluations,
|
evaluations,
|
||||||
tools,
|
tools,
|
||||||
users,
|
users,
|
||||||
|
user_models,
|
||||||
utils,
|
utils,
|
||||||
scim,
|
scim,
|
||||||
)
|
)
|
||||||
|
|
@ -1297,7 +1298,9 @@ app.include_router(configs.router, prefix="/api/v1/configs", tags=["configs"])
|
||||||
app.include_router(auths.router, prefix="/api/v1/auths", tags=["auths"])
|
app.include_router(auths.router, prefix="/api/v1/auths", tags=["auths"])
|
||||||
app.include_router(users.router, prefix="/api/v1/users", tags=["users"])
|
app.include_router(users.router, prefix="/api/v1/users", tags=["users"])
|
||||||
|
|
||||||
|
app.include_router(
|
||||||
|
user_models.router, prefix="/api/v1/user/models", tags=["user_models"]
|
||||||
|
)
|
||||||
app.include_router(channels.router, prefix="/api/v1/channels", tags=["channels"])
|
app.include_router(channels.router, prefix="/api/v1/channels", tags=["channels"])
|
||||||
app.include_router(chats.router, prefix="/api/v1/chats", tags=["chats"])
|
app.include_router(chats.router, prefix="/api/v1/chats", tags=["chats"])
|
||||||
app.include_router(notes.router, prefix="/api/v1/notes", tags=["notes"])
|
app.include_router(notes.router, prefix="/api/v1/notes", tags=["notes"])
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ from open_webui.routers.pipelines import (
|
||||||
|
|
||||||
from open_webui.models.functions import Functions
|
from open_webui.models.functions import Functions
|
||||||
from open_webui.models.models import Models
|
from open_webui.models.models import Models
|
||||||
|
from open_webui.models.user_model_credentials import UserModelCredentials
|
||||||
|
|
||||||
|
|
||||||
from open_webui.utils.plugin import (
|
from open_webui.utils.plugin import (
|
||||||
|
|
@ -225,6 +226,23 @@ async def generate_chat_completion(
|
||||||
|
|
||||||
# === 4. 验证模型存在性 ===
|
# === 4. 验证模型存在性 ===
|
||||||
model_id = form_data["model"]
|
model_id = form_data["model"]
|
||||||
|
|
||||||
|
# 私有模型直连:如果是用户私有模型,使用 credential_id 注入 direct 配置
|
||||||
|
if form_data.get("is_user_model") and form_data.get("model_item", {}).get("credential_id"):
|
||||||
|
cred = UserModelCredentials.get_credential_by_id_and_user_id(
|
||||||
|
form_data["model_item"]["credential_id"], user.id
|
||||||
|
)
|
||||||
|
if not cred:
|
||||||
|
raise Exception("User model credential not found")
|
||||||
|
request.state.direct = True
|
||||||
|
request.state.model = {
|
||||||
|
"id": cred.model_id,
|
||||||
|
"name": cred.name or cred.model_id,
|
||||||
|
"base_url": cred.base_url,
|
||||||
|
"api_key": cred.api_key,
|
||||||
|
}
|
||||||
|
models = {request.state.model["id"]: request.state.model}
|
||||||
|
model_id = cred.model_id
|
||||||
if model_id not in models:
|
if model_id not in models:
|
||||||
raise Exception("Model not found")
|
raise Exception("Model not found")
|
||||||
|
|
||||||
|
|
|
||||||
130
src/lib/apis/userModels.ts
Normal file
130
src/lib/apis/userModels.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
|
export type UserModelCredentialForm = {
|
||||||
|
name?: string;
|
||||||
|
model_id: string;
|
||||||
|
base_url?: string;
|
||||||
|
api_key: string;
|
||||||
|
config?: object;
|
||||||
|
};
|
||||||
|
|
||||||
|
// WEBUI_API_BASE_URL 已包含 /api 或 /api/v1 前缀,这里仅追加 user/models
|
||||||
|
const BASE = `${WEBUI_API_BASE_URL}/user/models`;
|
||||||
|
|
||||||
|
export const listUserModels = async (token: string = '') => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(BASE, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err;
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createUserModel = async (token: string, body: UserModelCredentialForm) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(BASE, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err;
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateUserModel = async (
|
||||||
|
token: string,
|
||||||
|
id: string,
|
||||||
|
body: UserModelCredentialForm
|
||||||
|
) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${BASE}/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err;
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteUserModel = async (token: string, id: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${BASE}/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err;
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
@ -13,34 +13,35 @@
|
||||||
import type { i18n as i18nType } from 'i18next';
|
import type { i18n as i18nType } from 'i18next';
|
||||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
chatId,
|
chatId,
|
||||||
chats,
|
chats,
|
||||||
config,
|
config,
|
||||||
type Model,
|
type Model,
|
||||||
models,
|
models,
|
||||||
tags as allTags,
|
userModels,
|
||||||
settings,
|
tags as allTags,
|
||||||
showSidebar,
|
settings,
|
||||||
WEBUI_NAME,
|
showSidebar,
|
||||||
banners,
|
WEBUI_NAME,
|
||||||
user,
|
banners,
|
||||||
socket,
|
user,
|
||||||
showControls,
|
socket,
|
||||||
showCallOverlay,
|
showControls,
|
||||||
currentChatPage,
|
showCallOverlay,
|
||||||
temporaryChatEnabled,
|
currentChatPage,
|
||||||
mobile,
|
temporaryChatEnabled,
|
||||||
showOverview,
|
mobile,
|
||||||
chatTitle,
|
showOverview,
|
||||||
showArtifacts,
|
chatTitle,
|
||||||
tools,
|
showArtifacts,
|
||||||
toolServers,
|
tools,
|
||||||
functions,
|
toolServers,
|
||||||
selectedFolder,
|
functions,
|
||||||
pinnedChats,
|
selectedFolder,
|
||||||
showEmbeds
|
pinnedChats,
|
||||||
} from '$lib/stores';
|
showEmbeds
|
||||||
|
} from '$lib/stores';
|
||||||
import {
|
import {
|
||||||
convertMessagesToHistory,
|
convertMessagesToHistory,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
|
|
@ -1550,11 +1551,12 @@ let memoryLocked = false;
|
||||||
const submitPrompt = async (userPrompt, { _raw = false } = {}) => {
|
const submitPrompt = async (userPrompt, { _raw = false } = {}) => {
|
||||||
console.log('submitPrompt', userPrompt, $chatId);
|
console.log('submitPrompt', userPrompt, $chatId);
|
||||||
|
|
||||||
// === 1. 模型验证:确保选中的模型仍然存在 ===
|
// === 1. 模型验证:确保选中的模型仍然存在 ===
|
||||||
// 过滤掉已被删除或不可用的模型,避免发送请求时出错
|
// 过滤掉已被删除或不可用的模型,避免发送请求时出错
|
||||||
const _selectedModels = selectedModels.map((modelId) =>
|
const _selectedModels = selectedModels.map((modelId) => {
|
||||||
$models.map((m) => m.id).includes(modelId) ? modelId : ''
|
const allIds = [...$models.map((m) => m.id), ...$userModels.map((m) => m.id)];
|
||||||
);
|
return allIds.includes(modelId) ? modelId : '';
|
||||||
|
});
|
||||||
|
|
||||||
// 如果模型列表发生变化,同步更新
|
// 如果模型列表发生变化,同步更新
|
||||||
if (JSON.stringify(selectedModels) !== JSON.stringify(_selectedModels)) {
|
if (JSON.stringify(selectedModels) !== JSON.stringify(_selectedModels)) {
|
||||||
|
|
@ -1773,9 +1775,9 @@ let memoryLocked = false;
|
||||||
// === 4. 为每个选中的模型创建响应消息占位符 ===
|
// === 4. 为每个选中的模型创建响应消息占位符 ===
|
||||||
// 这样 UI 可以立即显示"正在输入..."状态
|
// 这样 UI 可以立即显示"正在输入..."状态
|
||||||
for (const [_modelIdx, modelId] of selectedModelIds.entries()) {
|
for (const [_modelIdx, modelId] of selectedModelIds.entries()) {
|
||||||
const model = $models.filter((m) => m.id === modelId).at(0);
|
const combined = getCombinedModelById(modelId);
|
||||||
|
if (combined) {
|
||||||
if (model) {
|
const model = combined.model ?? combined.credential;
|
||||||
// 4.1 生成响应消息 ID 和空消息对象
|
// 4.1 生成响应消息 ID 和空消息对象
|
||||||
let responseMessageId = uuidv4();
|
let responseMessageId = uuidv4();
|
||||||
let responseMessage = {
|
let responseMessage = {
|
||||||
|
|
@ -1784,8 +1786,14 @@ let memoryLocked = false;
|
||||||
childrenIds: [],
|
childrenIds: [],
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: '', // 初始为空,后续通过 WebSocket 流式填充
|
content: '', // 初始为空,后续通过 WebSocket 流式填充
|
||||||
model: model.id,
|
model:
|
||||||
modelName: model.name ?? model.id,
|
combined.source === 'user' && combined.credential
|
||||||
|
? combined.credential.model_id
|
||||||
|
: model.id,
|
||||||
|
modelName:
|
||||||
|
combined.source === 'user' && combined.credential
|
||||||
|
? combined.credential.name ?? combined.credential.model_id
|
||||||
|
: model.name ?? model.id,
|
||||||
modelIdx: modelIdx ? modelIdx : _modelIdx, // 多模型对话时,区分不同模型的响应
|
modelIdx: modelIdx ? modelIdx : _modelIdx, // 多模型对话时,区分不同模型的响应
|
||||||
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
|
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
|
||||||
};
|
};
|
||||||
|
|
@ -1829,22 +1837,27 @@ let memoryLocked = false;
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
selectedModelIds.map(async (modelId, _modelIdx) => {
|
selectedModelIds.map(async (modelId, _modelIdx) => {
|
||||||
console.log('modelId', modelId);
|
console.log('modelId', modelId);
|
||||||
const model = $models.filter((m) => m.id === modelId).at(0);
|
const combined = getCombinedModelById(modelId);
|
||||||
|
const model = combined?.model ?? combined?.credential;
|
||||||
|
|
||||||
if (model) {
|
if (combined && model) {
|
||||||
// 7.1 检查模型视觉能力(如果消息包含图片)
|
// 7.1 检查模型视觉能力(如果消息包含图片)
|
||||||
const hasImages = createMessagesList(_history, parentId).some((message) =>
|
const hasImages = createMessagesList(_history, parentId).some((message) =>
|
||||||
message.files?.some((file) => file.type === 'image')
|
message.files?.some((file) => file.type === 'image')
|
||||||
|
);
|
||||||
|
|
||||||
|
// 如果消息包含图片,但模型不支持视觉,提示错误(私有模型默认视为支持)
|
||||||
|
if (
|
||||||
|
combined.source !== 'user' &&
|
||||||
|
hasImages &&
|
||||||
|
!(model.info?.meta?.capabilities?.vision ?? true)
|
||||||
|
) {
|
||||||
|
toast.error(
|
||||||
|
$i18n.t('Model {{modelName}} is not vision capable', {
|
||||||
|
modelName: model.name ?? model.id
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
}
|
||||||
// 如果消息包含图片,但模型不支持视觉,提示错误
|
|
||||||
if (hasImages && !(model.info?.meta?.capabilities?.vision ?? true)) {
|
|
||||||
toast.error(
|
|
||||||
$i18n.t('Model {{modelName}} is not vision capable', {
|
|
||||||
modelName: model.name ?? model.id
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7.2 获取响应消息 ID
|
// 7.2 获取响应消息 ID
|
||||||
let responseMessageId =
|
let responseMessageId =
|
||||||
|
|
@ -1860,12 +1873,12 @@ let memoryLocked = false;
|
||||||
// - 构造请求 payload(messages、files、tools、features 等)
|
// - 构造请求 payload(messages、files、tools、features 等)
|
||||||
// - 调用 generateOpenAIChatCompletion API
|
// - 调用 generateOpenAIChatCompletion API
|
||||||
// - 处理流式响应(通过 WebSocket 实时更新消息内容)
|
// - 处理流式响应(通过 WebSocket 实时更新消息内容)
|
||||||
await sendMessageSocket(
|
await sendMessageSocket(
|
||||||
model,
|
combined,
|
||||||
messages && messages.length > 0
|
messages && messages.length > 0
|
||||||
? messages // 使用自定义消息列表(例如重新生成时追加 follow-up)
|
? messages // 使用自定义消息列表(例如重新生成时追加 follow-up)
|
||||||
: createMessagesList(_history, responseMessageId), // 使用完整历史记录
|
: createMessagesList(_history, responseMessageId), // 使用完整历史记录
|
||||||
_history,
|
_history,
|
||||||
responseMessageId,
|
responseMessageId,
|
||||||
_chatId
|
_chatId
|
||||||
);
|
);
|
||||||
|
|
@ -1883,8 +1896,8 @@ let memoryLocked = false;
|
||||||
chats.set(await getChatList(localStorage.token, $currentChatPage));
|
chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFeatures = () => {
|
const getFeatures = () => {
|
||||||
let features = {};
|
let features = {};
|
||||||
|
|
||||||
if ($config?.features)
|
if ($config?.features)
|
||||||
features = {
|
features = {
|
||||||
|
|
@ -1921,17 +1934,27 @@ let memoryLocked = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果用户手动切换了记忆开关,覆盖全局设置
|
// 如果用户手动切换了记忆开关,覆盖全局设置
|
||||||
if (memoryEnabled !== undefined && memoryEnabled !== ($settings?.memory ?? false)) {
|
if (memoryEnabled !== undefined && memoryEnabled !== ($settings?.memory ?? false)) {
|
||||||
features = { ...features, memory: memoryEnabled };
|
features = { ...features, memory: memoryEnabled };
|
||||||
}
|
}
|
||||||
|
|
||||||
return features;
|
return features;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendMessageSocket = async (model, _messages, _history, responseMessageId, _chatId) => {
|
const getCombinedModelById = (modelId) => {
|
||||||
|
const platform = $models.find((m) => m.id === modelId);
|
||||||
|
if (platform) return { source: 'platform', model: platform };
|
||||||
|
const priv = $userModels.find((m) => m.id === modelId);
|
||||||
|
if (priv) return { source: 'user', credential: priv };
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendMessageSocket = async (combinedModel, _messages, _history, responseMessageId, _chatId) => {
|
||||||
const responseMessage = _history.messages[responseMessageId];
|
const responseMessage = _history.messages[responseMessageId];
|
||||||
const userMessage = _history.messages[responseMessage.parentId];
|
const userMessage = _history.messages[responseMessage.parentId];
|
||||||
|
|
||||||
|
const model = combinedModel?.model ?? combinedModel?.credential ?? combinedModel;
|
||||||
|
|
||||||
const chatMessageFiles = _messages
|
const chatMessageFiles = _messages
|
||||||
.filter((message) => message.files)
|
.filter((message) => message.files)
|
||||||
.flatMap((message) => message.files);
|
.flatMap((message) => message.files);
|
||||||
|
|
@ -1972,6 +1995,9 @@ let memoryLocked = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isUserModel = combinedModel?.source === 'user';
|
||||||
|
const credential = combinedModel?.credential;
|
||||||
|
|
||||||
const stream =
|
const stream =
|
||||||
model?.info?.params?.stream_response ??
|
model?.info?.params?.stream_response ??
|
||||||
$settings?.params?.stream_response ??
|
$settings?.params?.stream_response ??
|
||||||
|
|
@ -2039,7 +2065,7 @@ let memoryLocked = false;
|
||||||
localStorage.token,
|
localStorage.token,
|
||||||
{
|
{
|
||||||
stream: stream,
|
stream: stream,
|
||||||
model: model.id,
|
model: isUserModel ? credential.model_id : model.id,
|
||||||
messages: messages,
|
messages: messages,
|
||||||
params: {
|
params: {
|
||||||
...$settings?.params,
|
...$settings?.params,
|
||||||
|
|
@ -2063,7 +2089,8 @@ let memoryLocked = false;
|
||||||
variables: {
|
variables: {
|
||||||
...getPromptVariables($user?.name, $settings?.userLocation ? userLocation : undefined)
|
...getPromptVariables($user?.name, $settings?.userLocation ? userLocation : undefined)
|
||||||
},
|
},
|
||||||
model_item: $models.find((m) => m.id === model.id),
|
model_item: isUserModel ? { credential_id: credential.id } : $models.find((m) => m.id === model.id),
|
||||||
|
is_user_model: isUserModel,
|
||||||
|
|
||||||
session_id: $socket?.id,
|
session_id: $socket?.id,
|
||||||
chat_id: $chatId,
|
chat_id: $chatId,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,15 @@
|
||||||
import { getChatById } from '$lib/apis/chats';
|
import { getChatById } from '$lib/apis/chats';
|
||||||
import { generateTags } from '$lib/apis';
|
import { generateTags } from '$lib/apis';
|
||||||
|
|
||||||
import { config, models, settings, temporaryChatEnabled, TTSWorker, user } from '$lib/stores';
|
import {
|
||||||
|
config,
|
||||||
|
models,
|
||||||
|
settings,
|
||||||
|
temporaryChatEnabled,
|
||||||
|
TTSWorker,
|
||||||
|
user,
|
||||||
|
userModels
|
||||||
|
} from '$lib/stores';
|
||||||
import { synthesizeOpenAISpeech } from '$lib/apis/audio';
|
import { synthesizeOpenAISpeech } from '$lib/apis/audio';
|
||||||
import { imageGenerations } from '$lib/apis/images';
|
import { imageGenerations } from '$lib/apis/images';
|
||||||
import {
|
import {
|
||||||
|
|
@ -147,8 +155,30 @@
|
||||||
let buttonsContainerElement: HTMLDivElement;
|
let buttonsContainerElement: HTMLDivElement;
|
||||||
let showDeleteConfirm = false;
|
let showDeleteConfirm = false;
|
||||||
|
|
||||||
let model = null;
|
let model = null;
|
||||||
$: model = $models.find((m) => m.id === message.model);
|
let userModel = null;
|
||||||
|
let modelName = '';
|
||||||
|
|
||||||
|
$: {
|
||||||
|
const platformModel = $models.find((m) => m.id === message.model);
|
||||||
|
const credentialModel = (() => {
|
||||||
|
// 优先使用随消息携带的 credential_id 精确匹配
|
||||||
|
if (message?.model_item?.credential_id) {
|
||||||
|
return $userModels.find((m) => m.id === message.model_item.credential_id);
|
||||||
|
}
|
||||||
|
// 兼容历史消息:通过 model_id 反查
|
||||||
|
return $userModels.find((m) => m.model_id === message.model);
|
||||||
|
})();
|
||||||
|
|
||||||
|
userModel = credentialModel ? { ...credentialModel, id: credentialModel.model_id } : null;
|
||||||
|
model = platformModel ?? userModel;
|
||||||
|
|
||||||
|
modelName =
|
||||||
|
message.modelName ??
|
||||||
|
model?.name ??
|
||||||
|
(userModel?.name ?? userModel?.model_id) ??
|
||||||
|
message.model;
|
||||||
|
}
|
||||||
|
|
||||||
let edit = false;
|
let edit = false;
|
||||||
let editedContent = '';
|
let editedContent = '';
|
||||||
|
|
@ -619,10 +649,10 @@
|
||||||
|
|
||||||
<div class="flex-auto w-0 pl-1 relative">
|
<div class="flex-auto w-0 pl-1 relative">
|
||||||
<Name>
|
<Name>
|
||||||
<Tooltip content={model?.name ?? message.model} placement="top-start">
|
<Tooltip content={modelName} placement="top-start">
|
||||||
<span class="line-clamp-1 text-black dark:text-white">
|
<span class="line-clamp-1 text-black dark:text-white">
|
||||||
{model?.name ?? message.model}
|
{modelName}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{#if message.timestamp}
|
{#if message.timestamp}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,34 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { models, showSettings, settings, user, mobile, config } from '$lib/stores';
|
import {
|
||||||
|
models,
|
||||||
|
userModels,
|
||||||
|
showSettings,
|
||||||
|
settings,
|
||||||
|
user,
|
||||||
|
mobile,
|
||||||
|
config
|
||||||
|
} from '$lib/stores';
|
||||||
import { onMount, tick, getContext } from 'svelte';
|
import { onMount, tick, getContext } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import Selector from './ModelSelector/Selector.svelte';
|
import Selector from './ModelSelector/Selector.svelte';
|
||||||
import Tooltip from '../common/Tooltip.svelte';
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
|
|
||||||
import { updateUserSettings } from '$lib/apis/users';
|
import { updateUserSettings } from '$lib/apis/users';
|
||||||
|
import {
|
||||||
|
listUserModels,
|
||||||
|
createUserModel,
|
||||||
|
updateUserModel,
|
||||||
|
deleteUserModel
|
||||||
|
} from '$lib/apis/userModels';
|
||||||
|
|
||||||
|
let showUserModelModal = false;
|
||||||
|
let editingCredential = null;
|
||||||
|
let form = {
|
||||||
|
name: '',
|
||||||
|
model_id: '',
|
||||||
|
base_url: '',
|
||||||
|
api_key: ''
|
||||||
|
};
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let selectedModels = [''];
|
export let selectedModels = [''];
|
||||||
|
|
@ -38,17 +61,92 @@
|
||||||
await updateUserSettings(localStorage.token, { ui: $settings });
|
await updateUserSettings(localStorage.token, { ui: $settings });
|
||||||
};
|
};
|
||||||
|
|
||||||
$: if (selectedModels.length > 0 && $models.length > 0) {
|
$: if (selectedModels.length > 0 && ($models.length > 0 || $userModels.length > 0)) {
|
||||||
const _selectedModels = selectedModels.map((model) =>
|
const allIds = [
|
||||||
$models.map((m) => m.id).includes(model) ? model : ''
|
...$models.map((m) => m.id),
|
||||||
);
|
...$userModels.map((m) => m.id)
|
||||||
|
];
|
||||||
|
|
||||||
|
const _selectedModels = selectedModels.map((model) => (allIds.includes(model) ? model : ''));
|
||||||
|
|
||||||
if (JSON.stringify(_selectedModels) !== JSON.stringify(selectedModels)) {
|
if (JSON.stringify(_selectedModels) !== JSON.stringify(selectedModels)) {
|
||||||
selectedModels = _selectedModels;
|
selectedModels = _selectedModels;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadUserModels = async () => {
|
||||||
|
const res = await listUserModels(localStorage.token).catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
userModels.set(
|
||||||
|
(res ?? []).map((item) => ({
|
||||||
|
...item,
|
||||||
|
id: `user:${item.id}`, // 前端区分私有模型 ID
|
||||||
|
source: 'user'
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await loadUserModels();
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
form = {
|
||||||
|
name: '',
|
||||||
|
model_id: '',
|
||||||
|
base_url: '',
|
||||||
|
api_key: ''
|
||||||
|
};
|
||||||
|
editingCredential = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitUserModel = async () => {
|
||||||
|
if (!form.model_id || !form.api_key) {
|
||||||
|
toast.error($i18n.t('Model ID and API Key are required'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editingCredential) {
|
||||||
|
const res = await updateUserModel(localStorage.token, editingCredential, form).catch((err) => {
|
||||||
|
toast.error(`${err?.detail ?? err}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (res) {
|
||||||
|
toast.success($i18n.t('Updated'));
|
||||||
|
await loadUserModels();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const res = await createUserModel(localStorage.token, form).catch((err) => {
|
||||||
|
toast.error(`${err?.detail ?? err}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (res) {
|
||||||
|
toast.success($i18n.t('Added'));
|
||||||
|
await loadUserModels();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetForm();
|
||||||
|
showUserModelModal = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeUserModel = async (cred) => {
|
||||||
|
console.log("removeUserModel");
|
||||||
|
console.log(cred);
|
||||||
|
const res = await deleteUserModel(localStorage.token, cred.id.replace('user:', '')).catch((err) => {
|
||||||
|
toast.error(`${err?.detail ?? err}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (res) {
|
||||||
|
toast.success($i18n.t('Deleted'));
|
||||||
|
await loadUserModels();
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<div class="flex flex-col w-full items-start">
|
<div class="flex flex-col w-full items-start">
|
||||||
{#each selectedModels as selectedModel, selectedModelIdx}
|
{#each selectedModels as selectedModel, selectedModelIdx}
|
||||||
<div class="flex w-full max-w-fit">
|
<div class="flex w-full max-w-fit">
|
||||||
|
|
@ -57,12 +155,35 @@
|
||||||
<Selector
|
<Selector
|
||||||
id={`${selectedModelIdx}`}
|
id={`${selectedModelIdx}`}
|
||||||
placeholder={$i18n.t('Select a model')}
|
placeholder={$i18n.t('Select a model')}
|
||||||
items={$models.map((model) => ({
|
items={[
|
||||||
value: model.id,
|
...$models.map((model) => ({
|
||||||
label: model.name,
|
value: model.id,
|
||||||
model: model
|
label: model.name,
|
||||||
}))}
|
model: model,
|
||||||
|
source: 'platform'
|
||||||
|
})),
|
||||||
|
...$userModels.map((model) => ({
|
||||||
|
value: model.id,
|
||||||
|
label: model.name || model.model_id,
|
||||||
|
model: {
|
||||||
|
id: model.id,
|
||||||
|
name: model.name || model.model_id,
|
||||||
|
info: { meta: { description: model.base_url ?? '' } },
|
||||||
|
owned_by: 'openai',
|
||||||
|
source: 'user'
|
||||||
|
},
|
||||||
|
source: 'user',
|
||||||
|
_credential: model
|
||||||
|
}))
|
||||||
|
]}
|
||||||
{pinModelHandler}
|
{pinModelHandler}
|
||||||
|
addUserModel={() => {
|
||||||
|
resetForm();
|
||||||
|
showUserModelModal = true;
|
||||||
|
}}
|
||||||
|
on:deleteUserModel={(e) => {
|
||||||
|
removeUserModel(e.detail);
|
||||||
|
}}
|
||||||
bind:value={selectedModel}
|
bind:value={selectedModel}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -70,39 +191,26 @@
|
||||||
|
|
||||||
{#if $user?.role === 'admin' || ($user?.permissions?.chat?.multiple_models ?? true)}
|
{#if $user?.role === 'admin' || ($user?.permissions?.chat?.multiple_models ?? true)}
|
||||||
{#if selectedModelIdx === 0}
|
{#if selectedModelIdx === 0}
|
||||||
|
<div class="self-center mx-1 disabled:text-gray-600 disabled:hover:text-gray-600 -translate-y-[0.5px]">
|
||||||
<!-- ai-friend 屏蔽添加模型按钮 -->
|
|
||||||
<div
|
|
||||||
class=" self-center mx-1 disabled:text-gray-600 disabled:hover:text-gray-600 -translate-y-[0.5px]"
|
|
||||||
>
|
|
||||||
<Tooltip content={$i18n.t('Add Model')}>
|
<Tooltip content={$i18n.t('Add Model')}>
|
||||||
{#if false}
|
{#if false}
|
||||||
<button
|
<button
|
||||||
class=" "
|
class=" "
|
||||||
{disabled}
|
{disabled}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedModels = [...selectedModels, ''];
|
selectedModels = [...selectedModels, ''];
|
||||||
}}
|
}}
|
||||||
aria-label="Add Model"
|
aria-label="Add Model"
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="size-3.5"
|
|
||||||
>
|
>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-3.5">
|
||||||
</svg>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
|
||||||
</button>
|
</svg>
|
||||||
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div class="self-center mx-1 disabled:text-gray-600 disabled:hover:text-gray-600 -translate-y-[0.5px]">
|
||||||
class=" self-center mx-1 disabled:text-gray-600 disabled:hover:text-gray-600 -translate-y-[0.5px]"
|
|
||||||
>
|
|
||||||
<Tooltip content={$i18n.t('Remove Model')}>
|
<Tooltip content={$i18n.t('Remove Model')}>
|
||||||
<button
|
<button
|
||||||
{disabled}
|
{disabled}
|
||||||
|
|
@ -112,14 +220,7 @@
|
||||||
}}
|
}}
|
||||||
aria-label="Remove Model"
|
aria-label="Remove Model"
|
||||||
>
|
>
|
||||||
<svg
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-3">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="size-3"
|
|
||||||
>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -129,6 +230,70 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
{#if showUserModelModal}
|
||||||
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||||
|
<div class="bg-white dark:bg-gray-900 rounded-2xl shadow-xl w-full max-w-md p-4 space-y-3">
|
||||||
|
<div class="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{editingCredential ? $i18n.t('Edit My API') : $i18n.t('Add My API')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2 text-sm">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label class="text-gray-600 dark:text-gray-400">{$i18n.t('Display Name')}</label>
|
||||||
|
<input
|
||||||
|
class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-3 py-2 outline-none"
|
||||||
|
bind:value={form.name}
|
||||||
|
placeholder={$i18n.t('Optional')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label class="text-gray-600 dark:text-gray-400">{$i18n.t('Model ID')}</label>
|
||||||
|
<input
|
||||||
|
class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-3 py-2 outline-none"
|
||||||
|
bind:value={form.model_id}
|
||||||
|
placeholder="gpt-4o / claude-3..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label class="text-gray-600 dark:text-gray-400">{$i18n.t('Base URL')}</label>
|
||||||
|
<input
|
||||||
|
class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-3 py-2 outline-none"
|
||||||
|
bind:value={form.base_url}
|
||||||
|
placeholder="https://api.openai.com/v1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label class="text-gray-600 dark:text-gray-400">API Key</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-3 py-2 outline-none"
|
||||||
|
bind:value={form.api_key}
|
||||||
|
placeholder="sk-..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
class="px-3 py-2 rounded-lg text-sm bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700"
|
||||||
|
on:click={() => {
|
||||||
|
resetForm();
|
||||||
|
showUserModelModal = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{$i18n.t('Cancel')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-3 py-2 rounded-lg text-sm bg-black text-white dark:bg-white dark:text-black"
|
||||||
|
on:click={submitUserModel}
|
||||||
|
>
|
||||||
|
{$i18n.t('Save')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if showSetDefault}
|
{#if showSetDefault}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import Tag from '$lib/components/icons/Tag.svelte';
|
import Tag from '$lib/components/icons/Tag.svelte';
|
||||||
import Label from '$lib/components/icons/Label.svelte';
|
import Label from '$lib/components/icons/Label.svelte';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
|
@ -29,6 +30,8 @@
|
||||||
|
|
||||||
export let onClick: () => void = () => {};
|
export let onClick: () => void = () => {};
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
const copyLinkHandler = async (model) => {
|
const copyLinkHandler = async (model) => {
|
||||||
const baseUrl = window.location.origin;
|
const baseUrl = window.location.origin;
|
||||||
const res = await copyToClipboard(`${baseUrl}/?model=${encodeURIComponent(model.id)}`);
|
const res = await copyToClipboard(`${baseUrl}/?model=${encodeURIComponent(model.id)}`);
|
||||||
|
|
@ -86,11 +89,19 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<Tooltip content={`${item.label} (${item.value})`} placement="top-start">
|
<Tooltip content={`${item.label}`} placement="top-start">
|
||||||
<div class="line-clamp-1">
|
<div class="line-clamp-1">
|
||||||
{item.label}
|
{item.label}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
{#if item?.source === 'user'}
|
||||||
|
<div
|
||||||
|
class="ml-2 inline-flex items-center rounded-full bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-100 px-1.5 py-0.5 text-[11px]"
|
||||||
|
>
|
||||||
|
{$i18n.t('My API')}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" shrink-0 flex items-center gap-2">
|
<div class=" shrink-0 flex items-center gap-2">
|
||||||
|
|
@ -249,10 +260,15 @@
|
||||||
<ModelItemMenu
|
<ModelItemMenu
|
||||||
bind:show={showMenu}
|
bind:show={showMenu}
|
||||||
model={item.model}
|
model={item.model}
|
||||||
|
{item}
|
||||||
{pinModelHandler}
|
{pinModelHandler}
|
||||||
copyLinkHandler={() => {
|
copyLinkHandler={() => {
|
||||||
copyLinkHandler(item.model);
|
copyLinkHandler(item.model);
|
||||||
}}
|
}}
|
||||||
|
onDelete={(cred) => {
|
||||||
|
const target = cred?._credential ?? cred;
|
||||||
|
dispatch('deleteUserModel', target);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label={`${$i18n.t('More Options')}`}
|
aria-label={`${$i18n.t('More Options')}`}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
export let model;
|
export let model;
|
||||||
|
export let item: any = {};
|
||||||
|
|
||||||
|
export let onDelete: Function = () => {}; // required: pass handler from parent
|
||||||
|
|
||||||
export let pinModelHandler: (modelId: string) => void = () => {};
|
export let pinModelHandler: (modelId: string) => void = () => {};
|
||||||
export let copyLinkHandler: Function = () => {};
|
export let copyLinkHandler: Function = () => {};
|
||||||
|
|
@ -92,5 +95,20 @@
|
||||||
|
|
||||||
<div class="flex items-center">{$i18n.t('Copy Link')}</div>
|
<div class="flex items-center">{$i18n.t('Copy Link')}</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
|
{#if item?.source === 'user'}
|
||||||
|
<DropdownMenu.Item
|
||||||
|
type="button"
|
||||||
|
class="flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition items-center gap-2 text-red-500"
|
||||||
|
on:click={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
onDelete(item);
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex items-center">{$i18n.t('Delete')}</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{/if}
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,14 @@
|
||||||
export let placeholder = $i18n.t('Select a model');
|
export let placeholder = $i18n.t('Select a model');
|
||||||
export let searchEnabled = true;
|
export let searchEnabled = true;
|
||||||
export let searchPlaceholder = $i18n.t('Search a model');
|
export let searchPlaceholder = $i18n.t('Search a model');
|
||||||
|
export let addUserModel: Function = () => {};
|
||||||
|
|
||||||
export let items: {
|
export let items: {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
model: Model;
|
model: Model;
|
||||||
|
source?: string;
|
||||||
|
_credential?: any;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
@ -541,6 +544,33 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-2.5 max-h-64 overflow-y-auto group relative">
|
<div class="px-2.5 max-h-64 overflow-y-auto group relative">
|
||||||
|
<!-- Add private model button -->
|
||||||
|
<button
|
||||||
|
class="w-full text-left px-2 py-1.5 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800/60 flex items-center gap-2 transition mb-1"
|
||||||
|
on:click={() => {
|
||||||
|
addUserModel();
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="size-7 rounded-full bg-gray-900 text-white dark:bg-gray-100 dark:text-gray-900 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{$i18n.t('Add My API')}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
{#each filteredItems as item, index}
|
{#each filteredItems as item, index}
|
||||||
<ModelItem
|
<ModelItem
|
||||||
{selectedModelIdx}
|
{selectedModelIdx}
|
||||||
|
|
@ -555,6 +585,12 @@
|
||||||
|
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
|
on:deleteUserModel={(e) => {
|
||||||
|
dispatch('deleteUserModel', e.detail);
|
||||||
|
}}
|
||||||
|
on:editUserModel={(e) => {
|
||||||
|
dispatch('editUserModel', e.detail);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="">
|
<div class="">
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ export const folders = writable([]);
|
||||||
export const selectedFolder = writable(null);
|
export const selectedFolder = writable(null);
|
||||||
|
|
||||||
export const models: Writable<Model[]> = writable([]);
|
export const models: Writable<Model[]> = writable([]);
|
||||||
|
export const userModels: Writable<any[]> = writable([]);
|
||||||
|
|
||||||
export const prompts: Writable<null | Prompt[]> = writable(null);
|
export const prompts: Writable<null | Prompt[]> = writable(null);
|
||||||
export const knowledge: Writable<null | Document[]> = writable(null);
|
export const knowledge: Writable<null | Document[]> = writable(null);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue