mirror of
https://github.com/open-webui/open-webui
synced 2025-06-26 18:26:48 +00:00
fix merge conflicts
This commit is contained in:
@@ -34,7 +34,8 @@
|
||||
mobile,
|
||||
showOverview,
|
||||
chatTitle,
|
||||
showArtifacts
|
||||
showArtifacts,
|
||||
tools
|
||||
} from '$lib/stores';
|
||||
import {
|
||||
convertMessagesToHistory,
|
||||
@@ -65,7 +66,7 @@
|
||||
import {
|
||||
chatCompleted,
|
||||
generateTitle,
|
||||
generateSearchQuery,
|
||||
generateQueries,
|
||||
chatAction,
|
||||
generateMoACompletion,
|
||||
generateTags
|
||||
@@ -78,6 +79,7 @@
|
||||
import ChatControls from './ChatControls.svelte';
|
||||
import EventConfirmDialog from '../common/ConfirmDialog.svelte';
|
||||
import Placeholder from './Placeholder.svelte';
|
||||
import { getTools } from '$lib/apis/tools';
|
||||
|
||||
export let chatIdProp = '';
|
||||
|
||||
@@ -141,6 +143,38 @@
|
||||
})();
|
||||
}
|
||||
|
||||
$: if (selectedModels && chatIdProp !== '') {
|
||||
saveSessionSelectedModels();
|
||||
}
|
||||
|
||||
const saveSessionSelectedModels = () => {
|
||||
if (selectedModels.length === 0 || (selectedModels.length === 1 && selectedModels[0] === '')) {
|
||||
return;
|
||||
}
|
||||
sessionStorage.selectedModels = JSON.stringify(selectedModels);
|
||||
console.log('saveSessionSelectedModels', selectedModels, sessionStorage.selectedModels);
|
||||
};
|
||||
|
||||
$: if (selectedModels) {
|
||||
setToolIds();
|
||||
}
|
||||
|
||||
const setToolIds = async () => {
|
||||
if (!$tools) {
|
||||
tools.set(await getTools(localStorage.token));
|
||||
}
|
||||
|
||||
if (selectedModels.length !== 1) {
|
||||
return;
|
||||
}
|
||||
const model = $models.find((m) => m.id === selectedModels[0]);
|
||||
if (model) {
|
||||
selectedToolIds = (model?.info?.meta?.toolIds ?? []).filter((id) =>
|
||||
$tools.find((t) => t.id === id)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const showMessage = async (message) => {
|
||||
const _chatId = JSON.parse(JSON.stringify($chatId));
|
||||
let _messageId = JSON.parse(JSON.stringify(message.id));
|
||||
@@ -182,7 +216,7 @@
|
||||
} else {
|
||||
message.statusHistory = [data];
|
||||
}
|
||||
} else if (type === 'citation') {
|
||||
} else if (type === 'source' || type === 'citation') {
|
||||
if (data?.type === 'code_execution') {
|
||||
// Code execution; update existing code execution by ID, or add new one.
|
||||
if (!message?.code_executions) {
|
||||
@@ -201,11 +235,11 @@
|
||||
|
||||
message.code_executions = message.code_executions;
|
||||
} else {
|
||||
// Regular citation.
|
||||
if (message?.citations) {
|
||||
message.citations.push(data);
|
||||
// Regular source.
|
||||
if (message?.sources) {
|
||||
message.sources.push(data);
|
||||
} else {
|
||||
message.citations = [data];
|
||||
message.sources = [data];
|
||||
}
|
||||
}
|
||||
} else if (type === 'message') {
|
||||
@@ -300,6 +334,7 @@
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
console.log('mounted');
|
||||
window.addEventListener('message', onMessageHandler);
|
||||
$socket?.on('chat-events', chatEventHandler);
|
||||
|
||||
@@ -545,28 +580,6 @@
|
||||
//////////////////////////
|
||||
|
||||
const initNewChat = async () => {
|
||||
await showControls.set(false);
|
||||
await showCallOverlay.set(false);
|
||||
await showOverview.set(false);
|
||||
await showArtifacts.set(false);
|
||||
|
||||
if ($page.url.pathname.includes('/c/')) {
|
||||
window.history.replaceState(history.state, '', `/`);
|
||||
}
|
||||
|
||||
autoScroll = true;
|
||||
|
||||
await chatId.set('');
|
||||
await chatTitle.set('');
|
||||
|
||||
history = {
|
||||
messages: {},
|
||||
currentId: null
|
||||
};
|
||||
|
||||
chatFiles = [];
|
||||
params = {};
|
||||
|
||||
if ($page.url.searchParams.get('models')) {
|
||||
selectedModels = $page.url.searchParams.get('models')?.split(',');
|
||||
} else if ($page.url.searchParams.get('model')) {
|
||||
@@ -593,15 +606,21 @@
|
||||
} else {
|
||||
selectedModels = urlModels;
|
||||
}
|
||||
} else if ($settings?.models) {
|
||||
selectedModels = $settings?.models;
|
||||
} else if ($config?.default_models) {
|
||||
console.log($config?.default_models.split(',') ?? '');
|
||||
selectedModels = $config?.default_models.split(',');
|
||||
} else {
|
||||
if (sessionStorage.selectedModels) {
|
||||
selectedModels = JSON.parse(sessionStorage.selectedModels);
|
||||
sessionStorage.removeItem('selectedModels');
|
||||
} else {
|
||||
if ($settings?.models) {
|
||||
selectedModels = $settings?.models;
|
||||
} else if ($config?.default_models) {
|
||||
console.log($config?.default_models.split(',') ?? '');
|
||||
selectedModels = $config?.default_models.split(',');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectedModels = selectedModels.filter((modelId) => $models.map((m) => m.id).includes(modelId));
|
||||
|
||||
if (selectedModels.length === 0 || (selectedModels.length === 1 && selectedModels[0] === '')) {
|
||||
if ($models.length > 0) {
|
||||
selectedModels = [$models[0].id];
|
||||
@@ -610,7 +629,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
console.log(selectedModels);
|
||||
await showControls.set(false);
|
||||
await showCallOverlay.set(false);
|
||||
await showOverview.set(false);
|
||||
await showArtifacts.set(false);
|
||||
|
||||
if ($page.url.pathname.includes('/c/')) {
|
||||
window.history.replaceState(history.state, '', `/`);
|
||||
}
|
||||
|
||||
autoScroll = true;
|
||||
|
||||
await chatId.set('');
|
||||
await chatTitle.set('');
|
||||
|
||||
history = {
|
||||
messages: {},
|
||||
currentId: null
|
||||
};
|
||||
|
||||
chatFiles = [];
|
||||
params = {};
|
||||
|
||||
if ($page.url.searchParams.get('youtube')) {
|
||||
uploadYoutubeTranscription(
|
||||
@@ -751,7 +790,8 @@
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
info: m.info ? m.info : undefined,
|
||||
timestamp: m.timestamp
|
||||
timestamp: m.timestamp,
|
||||
...(m.sources ? { sources: m.sources } : {})
|
||||
})),
|
||||
chat_id: chatId,
|
||||
session_id: $socket?.id,
|
||||
@@ -804,7 +844,8 @@
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
info: m.info ? m.info : undefined,
|
||||
timestamp: m.timestamp
|
||||
timestamp: m.timestamp,
|
||||
...(m.sources ? { sources: m.sources } : {})
|
||||
})),
|
||||
...(event ? { event: event } : {}),
|
||||
chat_id: chatId,
|
||||
@@ -923,9 +964,12 @@
|
||||
console.log('submitPrompt', userPrompt, $chatId);
|
||||
|
||||
const messages = createMessagesList(history.currentId);
|
||||
selectedModels = selectedModels.map((modelId) =>
|
||||
const _selectedModels = selectedModels.map((modelId) =>
|
||||
$models.map((m) => m.id).includes(modelId) ? modelId : ''
|
||||
);
|
||||
if (JSON.stringify(selectedModels) !== JSON.stringify(_selectedModels)) {
|
||||
selectedModels = _selectedModels;
|
||||
}
|
||||
|
||||
if (userPrompt === '') {
|
||||
toast.error($i18n.t('Please enter a prompt'));
|
||||
@@ -971,11 +1015,10 @@
|
||||
await tick();
|
||||
|
||||
// Reset chat input textarea
|
||||
const chatInputContainer = document.getElementById('chat-input-container');
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
|
||||
if (chatInputContainer) {
|
||||
chatInputContainer.value = '';
|
||||
chatInputContainer.style.height = '';
|
||||
if (chatInputElement) {
|
||||
chatInputElement.style.height = '';
|
||||
}
|
||||
|
||||
const _files = JSON.parse(JSON.stringify(files));
|
||||
@@ -1018,6 +1061,7 @@
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
chatInput?.focus();
|
||||
|
||||
saveSessionSelectedModels();
|
||||
_responses = await sendPrompt(userPrompt, userMessageId, { newChat: true });
|
||||
|
||||
return _responses;
|
||||
@@ -1139,11 +1183,14 @@
|
||||
}
|
||||
|
||||
let _response = null;
|
||||
if (model?.owned_by === 'ollama') {
|
||||
_response = await sendPromptOllama(model, prompt, responseMessageId, _chatId);
|
||||
} else if (model) {
|
||||
_response = await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
|
||||
}
|
||||
|
||||
// if (model?.owned_by === 'ollama') {
|
||||
// _response = await sendPromptOllama(model, prompt, responseMessageId, _chatId);
|
||||
// } else if (model) {
|
||||
// }
|
||||
|
||||
_response = await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
|
||||
|
||||
_responses.push(_response);
|
||||
|
||||
if (chatEventEmitter) clearInterval(chatEventEmitter);
|
||||
@@ -1290,24 +1337,14 @@
|
||||
$settings?.params?.stream_response ??
|
||||
params?.stream_response ??
|
||||
true;
|
||||
|
||||
const [res, controller] = await generateChatCompletion(localStorage.token, {
|
||||
stream: stream,
|
||||
model: model.id,
|
||||
messages: messagesBody,
|
||||
options: {
|
||||
...{ ...($settings?.params ?? {}), ...params },
|
||||
stop:
|
||||
(params?.stop ?? $settings?.params?.stop ?? undefined)
|
||||
? (params?.stop.split(',').map((token) => token.trim()) ?? $settings.params.stop).map(
|
||||
(str) => decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
|
||||
)
|
||||
: undefined,
|
||||
num_predict: params?.max_tokens ?? $settings?.params?.max_tokens ?? undefined,
|
||||
repeat_penalty:
|
||||
params?.frequency_penalty ?? $settings?.params?.frequency_penalty ?? undefined
|
||||
},
|
||||
format: $settings.requestFormat ?? undefined,
|
||||
keep_alive: $settings.keepAlive ?? undefined,
|
||||
|
||||
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
|
||||
files: files.length > 0 ? files : undefined,
|
||||
session_id: $socket?.id,
|
||||
@@ -1360,8 +1397,8 @@
|
||||
console.log(line);
|
||||
let data = JSON.parse(line);
|
||||
|
||||
if ('citations' in data) {
|
||||
responseMessage.citations = data.citations;
|
||||
if ('sources' in data) {
|
||||
responseMessage.sources = data.sources;
|
||||
// Only remove status if it was initially set
|
||||
if (model?.info?.meta?.knowledge ?? false) {
|
||||
responseMessage.statusHistory = responseMessage.statusHistory.filter(
|
||||
@@ -1625,13 +1662,6 @@
|
||||
{
|
||||
stream: stream,
|
||||
model: model.id,
|
||||
...(stream && (model.info?.meta?.capabilities?.usage ?? false)
|
||||
? {
|
||||
stream_options: {
|
||||
include_usage: true
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
messages: [
|
||||
params?.system || $settings.system || (responseMessage?.userContext ?? null)
|
||||
? {
|
||||
@@ -1676,23 +1706,36 @@
|
||||
content: message?.merged?.content ?? message.content
|
||||
})
|
||||
})),
|
||||
seed: params?.seed ?? $settings?.params?.seed ?? undefined,
|
||||
stop:
|
||||
(params?.stop ?? $settings?.params?.stop ?? undefined)
|
||||
? (params?.stop.split(',').map((token) => token.trim()) ?? $settings.params.stop).map(
|
||||
(str) => decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
|
||||
)
|
||||
: undefined,
|
||||
temperature: params?.temperature ?? $settings?.params?.temperature ?? undefined,
|
||||
top_p: params?.top_p ?? $settings?.params?.top_p ?? undefined,
|
||||
frequency_penalty:
|
||||
params?.frequency_penalty ?? $settings?.params?.frequency_penalty ?? undefined,
|
||||
max_tokens: params?.max_tokens ?? $settings?.params?.max_tokens ?? undefined,
|
||||
|
||||
// params: {
|
||||
// ...$settings?.params,
|
||||
// ...params,
|
||||
|
||||
// format: $settings.requestFormat ?? undefined,
|
||||
// keep_alive: $settings.keepAlive ?? undefined,
|
||||
// stop:
|
||||
// (params?.stop ?? $settings?.params?.stop ?? undefined)
|
||||
// ? (
|
||||
// params?.stop.split(',').map((token) => token.trim()) ?? $settings.params.stop
|
||||
// ).map((str) =>
|
||||
// decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
|
||||
// )
|
||||
// : undefined
|
||||
// },
|
||||
|
||||
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
|
||||
files: files.length > 0 ? files : undefined,
|
||||
session_id: $socket?.id,
|
||||
chat_id: $chatId,
|
||||
id: responseMessageId
|
||||
id: responseMessageId,
|
||||
|
||||
...(stream && (model.info?.meta?.capabilities?.usage ?? false)
|
||||
? {
|
||||
stream_options: {
|
||||
include_usage: true
|
||||
}
|
||||
}
|
||||
: {})
|
||||
},
|
||||
`${WEBUI_BASE_URL}/api`
|
||||
);
|
||||
@@ -1714,11 +1757,12 @@
|
||||
const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks);
|
||||
|
||||
for await (const update of textStream) {
|
||||
const { value, done, citations, selectedModelId, error, usage } = update;
|
||||
const { value, done, sources, selectedModelId, error, usage } = update;
|
||||
if (error) {
|
||||
await handleOpenAIError(error, null, model, responseMessage);
|
||||
break;
|
||||
}
|
||||
|
||||
if (done || stopResponseFlag || _chatId !== $chatId) {
|
||||
responseMessage.done = true;
|
||||
history.messages[responseMessageId] = responseMessage;
|
||||
@@ -1731,7 +1775,7 @@
|
||||
}
|
||||
|
||||
if (usage) {
|
||||
responseMessage.info = { ...usage, openai: true, usage };
|
||||
responseMessage.usage = usage;
|
||||
}
|
||||
|
||||
if (selectedModelId) {
|
||||
@@ -1740,8 +1784,8 @@
|
||||
continue;
|
||||
}
|
||||
|
||||
if (citations) {
|
||||
responseMessage.citations = citations;
|
||||
if (sources) {
|
||||
responseMessage.sources = sources;
|
||||
// Only remove status if it was initially set
|
||||
if (model?.info?.meta?.knowledge ?? false) {
|
||||
responseMessage.statusHistory = responseMessage.statusHistory.filter(
|
||||
@@ -1919,6 +1963,33 @@
|
||||
console.log('stopResponse');
|
||||
};
|
||||
|
||||
const submitMessage = async (parentId, prompt) => {
|
||||
let userPrompt = prompt;
|
||||
let userMessageId = uuidv4();
|
||||
|
||||
let userMessage = {
|
||||
id: userMessageId,
|
||||
parentId: parentId,
|
||||
childrenIds: [],
|
||||
role: 'user',
|
||||
content: userPrompt,
|
||||
models: selectedModels
|
||||
};
|
||||
|
||||
if (parentId !== null) {
|
||||
history.messages[parentId].childrenIds = [
|
||||
...history.messages[parentId].childrenIds,
|
||||
userMessageId
|
||||
];
|
||||
}
|
||||
|
||||
history.messages[userMessageId] = userMessage;
|
||||
history.currentId = userMessageId;
|
||||
|
||||
await tick();
|
||||
await sendPrompt(userPrompt, userMessageId);
|
||||
};
|
||||
|
||||
const regenerateResponse = async (message) => {
|
||||
console.log('regenerateResponse');
|
||||
|
||||
@@ -1949,7 +2020,9 @@
|
||||
responseMessage.done = false;
|
||||
await tick();
|
||||
|
||||
const model = $models.filter((m) => m.id === responseMessage.model).at(0);
|
||||
const model = $models
|
||||
.filter((m) => m.id === (responseMessage?.selectedModelId ?? responseMessage.model))
|
||||
.at(0);
|
||||
|
||||
if (model) {
|
||||
if (model?.owned_by === 'openai') {
|
||||
@@ -1967,8 +2040,6 @@
|
||||
_chatId
|
||||
);
|
||||
}
|
||||
} else {
|
||||
toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1993,7 +2064,7 @@
|
||||
if (res && res.ok && res.body) {
|
||||
const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks);
|
||||
for await (const update of textStream) {
|
||||
const { value, done, citations, error, usage } = update;
|
||||
const { value, done, sources, error, usage } = update;
|
||||
if (error || done) {
|
||||
break;
|
||||
}
|
||||
@@ -2020,20 +2091,21 @@
|
||||
};
|
||||
|
||||
const generateChatTitle = async (messages) => {
|
||||
const lastUserMessage = messages.filter((message) => message.role === 'user').at(-1);
|
||||
|
||||
if ($settings?.title?.auto ?? true) {
|
||||
const lastMessage = messages.at(-1);
|
||||
const modelId = selectedModels[0];
|
||||
|
||||
const title = await generateTitle(localStorage.token, modelId, messages, $chatId).catch(
|
||||
(error) => {
|
||||
console.error(error);
|
||||
return 'New Chat';
|
||||
return lastUserMessage?.content ?? 'New Chat';
|
||||
}
|
||||
);
|
||||
|
||||
return title;
|
||||
return title ? title : (lastUserMessage?.content ?? 'New Chat');
|
||||
} else {
|
||||
return 'New Chat';
|
||||
return lastUserMessage?.content ?? 'New Chat';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2089,6 +2161,7 @@
|
||||
parentId: string,
|
||||
responseMessageId: string
|
||||
) => {
|
||||
// TODO: move this to the backend
|
||||
const responseMessage = history.messages[responseMessageId];
|
||||
const userMessage = history.messages[parentId];
|
||||
const messages = createMessagesList(history.currentId);
|
||||
@@ -2103,17 +2176,17 @@
|
||||
history.messages[responseMessageId] = responseMessage;
|
||||
|
||||
const prompt = userMessage.content;
|
||||
let searchQuery = await generateSearchQuery(
|
||||
let queries = await generateQueries(
|
||||
localStorage.token,
|
||||
model,
|
||||
messages.filter((message) => message?.content?.trim()),
|
||||
prompt
|
||||
).catch((error) => {
|
||||
console.log(error);
|
||||
return prompt;
|
||||
return [prompt];
|
||||
});
|
||||
|
||||
if (!searchQuery || searchQuery == '') {
|
||||
if (queries.length === 0) {
|
||||
responseMessage.statusHistory.push({
|
||||
done: true,
|
||||
error: true,
|
||||
@@ -2124,6 +2197,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const searchQuery = queries[0];
|
||||
|
||||
responseMessage.statusHistory.push({
|
||||
done: false,
|
||||
action: 'web_search',
|
||||
@@ -2326,47 +2401,17 @@
|
||||
{selectedModels}
|
||||
{sendPrompt}
|
||||
{showMessage}
|
||||
{submitMessage}
|
||||
{continueResponse}
|
||||
{regenerateResponse}
|
||||
{mergeResponses}
|
||||
{chatActionHandler}
|
||||
bottomPadding={files.length > 0}
|
||||
on:submit={async (e) => {
|
||||
if (e.detail) {
|
||||
// New user message
|
||||
let userPrompt = e.detail.prompt;
|
||||
let userMessageId = uuidv4();
|
||||
|
||||
let userMessage = {
|
||||
id: userMessageId,
|
||||
parentId: e.detail.parentId,
|
||||
childrenIds: [],
|
||||
role: 'user',
|
||||
content: userPrompt,
|
||||
models: selectedModels
|
||||
};
|
||||
|
||||
let messageParentId = e.detail.parentId;
|
||||
|
||||
if (messageParentId !== null) {
|
||||
history.messages[messageParentId].childrenIds = [
|
||||
...history.messages[messageParentId].childrenIds,
|
||||
userMessageId
|
||||
];
|
||||
}
|
||||
|
||||
history.messages[userMessageId] = userMessage;
|
||||
history.currentId = userMessageId;
|
||||
|
||||
await tick();
|
||||
await sendPrompt(userPrompt, userMessageId);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" pb-[1.6rem]">
|
||||
<div class=" pb-[1rem]">
|
||||
<MessageInput
|
||||
{history}
|
||||
{selectedModels}
|
||||
@@ -2376,13 +2421,6 @@
|
||||
bind:selectedToolIds
|
||||
bind:webSearchEnabled
|
||||
bind:atSelectedModel
|
||||
availableToolIds={selectedModelIds.reduce((a, e, i, arr) => {
|
||||
const model = $models.find((m) => m.id === e);
|
||||
if (model?.info?.meta?.toolIds ?? false) {
|
||||
return [...new Set([...a, ...model.info.meta.toolIds])];
|
||||
}
|
||||
return a;
|
||||
}, [])}
|
||||
transparentBackground={$settings?.backgroundImageUrl ?? false}
|
||||
{stopResponse}
|
||||
{createMessagePair}
|
||||
@@ -2400,15 +2438,19 @@
|
||||
on:submit={async (e) => {
|
||||
if (e.detail) {
|
||||
await tick();
|
||||
submitPrompt(e.detail.replaceAll('\n\n', '\n'));
|
||||
submitPrompt(
|
||||
($settings?.richTextInput ?? true)
|
||||
? e.detail.replaceAll('\n\n', '\n')
|
||||
: e.detail
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
class="absolute bottom-1.5 text-xs text-gray-500 text-center line-clamp-1 right-0 left-0"
|
||||
class="absolute bottom-1 text-xs text-gray-500 text-center line-clamp-1 right-0 left-0"
|
||||
>
|
||||
{$i18n.t('LLMs can make mistakes. Verify important information.')}
|
||||
<!-- {$i18n.t('LLMs can make mistakes. Verify important information.')} -->
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -2422,13 +2464,6 @@
|
||||
bind:selectedToolIds
|
||||
bind:webSearchEnabled
|
||||
bind:atSelectedModel
|
||||
availableToolIds={selectedModelIds.reduce((a, e, i, arr) => {
|
||||
const model = $models.find((m) => m.id === e);
|
||||
if (model?.info?.meta?.toolIds ?? false) {
|
||||
return [...new Set([...a, ...model.info.meta.toolIds])];
|
||||
}
|
||||
return a;
|
||||
}, [])}
|
||||
transparentBackground={$settings?.backgroundImageUrl ?? false}
|
||||
{stopResponse}
|
||||
{createMessagePair}
|
||||
@@ -2444,7 +2479,11 @@
|
||||
on:submit={async (e) => {
|
||||
if (e.detail) {
|
||||
await tick();
|
||||
submitPrompt(e.detail.replaceAll('\n\n', '\n'));
|
||||
submitPrompt(
|
||||
($settings?.richTextInput ?? true)
|
||||
? e.detail.replaceAll('\n\n', '\n')
|
||||
: e.detail
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
export let models = [];
|
||||
export let chatFiles = [];
|
||||
export let params = {};
|
||||
|
||||
let showValves = false;
|
||||
</script>
|
||||
|
||||
<div class=" dark:text-white">
|
||||
@@ -59,9 +61,9 @@
|
||||
<hr class="my-2 border-gray-50 dark:border-gray-700/10" />
|
||||
{/if}
|
||||
|
||||
<Collapsible title={$i18n.t('Valves')} buttonClassName="w-full">
|
||||
<Collapsible bind:open={showValves} title={$i18n.t('Valves')} buttonClassName="w-full">
|
||||
<div class="text-sm" slot="content">
|
||||
<Valves />
|
||||
<Valves show={showValves} />
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
import {
|
||||
getUserValvesSpecById as getToolUserValvesSpecById,
|
||||
getUserValvesById as getToolUserValvesById,
|
||||
updateUserValvesById as updateToolUserValvesById
|
||||
updateUserValvesById as updateToolUserValvesById,
|
||||
getTools
|
||||
} from '$lib/apis/tools';
|
||||
import {
|
||||
getUserValvesSpecById as getFunctionUserValvesSpecById,
|
||||
getUserValvesById as getFunctionUserValvesById,
|
||||
updateUserValvesById as updateFunctionUserValvesById
|
||||
updateUserValvesById as updateFunctionUserValvesById,
|
||||
getFunctions
|
||||
} from '$lib/apis/functions';
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
@@ -23,6 +25,8 @@
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
|
||||
let tab = 'tools';
|
||||
let selectedId = '';
|
||||
|
||||
@@ -112,77 +116,98 @@
|
||||
$: if (selectedId) {
|
||||
getUserValves();
|
||||
}
|
||||
|
||||
$: if (show) {
|
||||
init();
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
loading = true;
|
||||
|
||||
if ($functions === null) {
|
||||
functions.set(await getFunctions(localStorage.token));
|
||||
}
|
||||
if ($tools === null) {
|
||||
tools.set(await getTools(localStorage.token));
|
||||
}
|
||||
|
||||
loading = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={() => {
|
||||
submitHandler();
|
||||
dispatch('save');
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div class="space-y-1">
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-1">
|
||||
<select
|
||||
class=" w-full rounded text-xs py-2 px-1 bg-transparent outline-none"
|
||||
bind:value={tab}
|
||||
placeholder="Select"
|
||||
>
|
||||
<option value="tools" class="bg-gray-100 dark:bg-gray-800">{$i18n.t('Tools')}</option>
|
||||
<option value="functions" class="bg-gray-100 dark:bg-gray-800"
|
||||
>{$i18n.t('Functions')}</option
|
||||
{#if show && !loading}
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={() => {
|
||||
submitHandler();
|
||||
dispatch('save');
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div class="space-y-1">
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-1">
|
||||
<select
|
||||
class=" w-full rounded text-xs py-2 px-1 bg-transparent outline-none"
|
||||
bind:value={tab}
|
||||
placeholder="Select"
|
||||
>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<select
|
||||
class="w-full rounded py-2 px-1 text-xs bg-transparent outline-none"
|
||||
bind:value={selectedId}
|
||||
on:change={async () => {
|
||||
await tick();
|
||||
}}
|
||||
>
|
||||
{#if tab === 'tools'}
|
||||
<option value="" selected disabled class="bg-gray-100 dark:bg-gray-800"
|
||||
>{$i18n.t('Select a tool')}</option
|
||||
<option value="tools" class="bg-gray-100 dark:bg-gray-800">{$i18n.t('Tools')}</option>
|
||||
<option value="functions" class="bg-gray-100 dark:bg-gray-800"
|
||||
>{$i18n.t('Functions')}</option
|
||||
>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{#each $tools as tool, toolIdx}
|
||||
<option value={tool.id} class="bg-gray-100 dark:bg-gray-800">{tool.name}</option>
|
||||
{/each}
|
||||
{:else if tab === 'functions'}
|
||||
<option value="" selected disabled class="bg-gray-100 dark:bg-gray-800"
|
||||
>{$i18n.t('Select a function')}</option
|
||||
>
|
||||
<div class="flex-1">
|
||||
<select
|
||||
class="w-full rounded py-2 px-1 text-xs bg-transparent outline-none"
|
||||
bind:value={selectedId}
|
||||
on:change={async () => {
|
||||
await tick();
|
||||
}}
|
||||
>
|
||||
{#if tab === 'tools'}
|
||||
<option value="" selected disabled class="bg-gray-100 dark:bg-gray-800"
|
||||
>{$i18n.t('Select a tool')}</option
|
||||
>
|
||||
|
||||
{#each $functions as func, funcIdx}
|
||||
<option value={func.id} class="bg-gray-100 dark:bg-gray-800">{func.name}</option>
|
||||
{/each}
|
||||
{/if}
|
||||
</select>
|
||||
{#each $tools as tool, toolIdx}
|
||||
<option value={tool.id} class="bg-gray-100 dark:bg-gray-800">{tool.name}</option>
|
||||
{/each}
|
||||
{:else if tab === 'functions'}
|
||||
<option value="" selected disabled class="bg-gray-100 dark:bg-gray-800"
|
||||
>{$i18n.t('Select a function')}</option
|
||||
>
|
||||
|
||||
{#each $functions as func, funcIdx}
|
||||
<option value={func.id} class="bg-gray-100 dark:bg-gray-800">{func.name}</option>
|
||||
{/each}
|
||||
{/if}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if selectedId}
|
||||
<hr class="dark:border-gray-800 my-1 w-full" />
|
||||
|
||||
<div class="my-2 text-xs">
|
||||
{#if !loading}
|
||||
<Valves
|
||||
{valvesSpec}
|
||||
bind:valves
|
||||
on:change={() => {
|
||||
debounceSubmitHandler();
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<Spinner className="size-5" />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if selectedId}
|
||||
<hr class="dark:border-gray-800 my-1 w-full" />
|
||||
|
||||
<div class="my-2 text-xs">
|
||||
{#if !loading}
|
||||
<Valves
|
||||
{valvesSpec}
|
||||
bind:valves
|
||||
on:change={() => {
|
||||
debounceSubmitHandler();
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<Spinner className="size-5" />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
{:else}
|
||||
<Spinner className="size-4" />
|
||||
{/if}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,24 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { knowledge, prompts } from '$lib/stores';
|
||||
|
||||
import { removeLastWordFromString } from '$lib/utils';
|
||||
import { getPrompts } from '$lib/apis/prompts';
|
||||
import { getKnowledgeBases } from '$lib/apis/knowledge';
|
||||
|
||||
import Prompts from './Commands/Prompts.svelte';
|
||||
import Knowledge from './Commands/Knowledge.svelte';
|
||||
import Models from './Commands/Models.svelte';
|
||||
|
||||
import { removeLastWordFromString } from '$lib/utils';
|
||||
import { processWeb, processYoutubeVideo } from '$lib/apis/retrieval';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
|
||||
export let prompt = '';
|
||||
export let files = [];
|
||||
|
||||
let loading = false;
|
||||
let commandElement = null;
|
||||
|
||||
export const selectUp = () => {
|
||||
@@ -26,55 +31,90 @@
|
||||
|
||||
let command = '';
|
||||
$: command = prompt?.split('\n').pop()?.split(' ')?.pop() ?? '';
|
||||
|
||||
let show = false;
|
||||
$: show = ['/', '#', '@'].includes(command?.charAt(0)) || '\\#' === command.slice(0, 2);
|
||||
|
||||
$: if (show) {
|
||||
init();
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
loading = true;
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
prompts.set(await getPrompts(localStorage.token));
|
||||
})(),
|
||||
(async () => {
|
||||
knowledge.set(await getKnowledgeBases(localStorage.token));
|
||||
})()
|
||||
]);
|
||||
loading = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if ['/', '#', '@'].includes(command?.charAt(0)) || '\\#' === command.slice(0, 2)}
|
||||
{#if command?.charAt(0) === '/'}
|
||||
<Prompts bind:this={commandElement} bind:prompt bind:files {command} />
|
||||
{:else if (command?.charAt(0) === '#' && command.startsWith('#') && !command.includes('# ')) || ('\\#' === command.slice(0, 2) && command.startsWith('#') && !command.includes('# '))}
|
||||
<Knowledge
|
||||
bind:this={commandElement}
|
||||
bind:prompt
|
||||
command={command.includes('\\#') ? command.slice(2) : command}
|
||||
on:youtube={(e) => {
|
||||
console.log(e);
|
||||
dispatch('upload', {
|
||||
type: 'youtube',
|
||||
data: e.detail
|
||||
});
|
||||
}}
|
||||
on:url={(e) => {
|
||||
console.log(e);
|
||||
dispatch('upload', {
|
||||
type: 'web',
|
||||
data: e.detail
|
||||
});
|
||||
}}
|
||||
on:select={(e) => {
|
||||
console.log(e);
|
||||
files = [
|
||||
...files,
|
||||
{
|
||||
...e.detail,
|
||||
status: 'processed'
|
||||
}
|
||||
];
|
||||
{#if show}
|
||||
{#if !loading}
|
||||
{#if command?.charAt(0) === '/'}
|
||||
<Prompts bind:this={commandElement} bind:prompt bind:files {command} />
|
||||
{:else if (command?.charAt(0) === '#' && command.startsWith('#') && !command.includes('# ')) || ('\\#' === command.slice(0, 2) && command.startsWith('#') && !command.includes('# '))}
|
||||
<Knowledge
|
||||
bind:this={commandElement}
|
||||
bind:prompt
|
||||
command={command.includes('\\#') ? command.slice(2) : command}
|
||||
on:youtube={(e) => {
|
||||
console.log(e);
|
||||
dispatch('upload', {
|
||||
type: 'youtube',
|
||||
data: e.detail
|
||||
});
|
||||
}}
|
||||
on:url={(e) => {
|
||||
console.log(e);
|
||||
dispatch('upload', {
|
||||
type: 'web',
|
||||
data: e.detail
|
||||
});
|
||||
}}
|
||||
on:select={(e) => {
|
||||
console.log(e);
|
||||
files = [
|
||||
...files,
|
||||
{
|
||||
...e.detail,
|
||||
status: 'processed'
|
||||
}
|
||||
];
|
||||
|
||||
dispatch('select');
|
||||
}}
|
||||
/>
|
||||
{:else if command?.charAt(0) === '@'}
|
||||
<Models
|
||||
bind:this={commandElement}
|
||||
{command}
|
||||
on:select={(e) => {
|
||||
prompt = removeLastWordFromString(prompt, command);
|
||||
dispatch('select');
|
||||
}}
|
||||
/>
|
||||
{:else if command?.charAt(0) === '@'}
|
||||
<Models
|
||||
bind:this={commandElement}
|
||||
{command}
|
||||
on:select={(e) => {
|
||||
prompt = removeLastWordFromString(prompt, command);
|
||||
|
||||
dispatch('select', {
|
||||
type: 'model',
|
||||
data: e.detail
|
||||
});
|
||||
}}
|
||||
/>
|
||||
dispatch('select', {
|
||||
type: 'model',
|
||||
data: e.detail
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<div
|
||||
id="commands-container"
|
||||
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
|
||||
>
|
||||
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
|
||||
<div
|
||||
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
||||
<div
|
||||
id="commands-container"
|
||||
class="pl-3 pr-14 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10"
|
||||
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
|
||||
>
|
||||
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
|
||||
<div
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
{#if filteredItems.length > 0}
|
||||
<div
|
||||
id="commands-container"
|
||||
class="pl-3 pr-14 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10"
|
||||
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
|
||||
>
|
||||
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
|
||||
<div
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
{#if filteredPrompts.length > 0}
|
||||
<div
|
||||
id="commands-container"
|
||||
class="pl-3 pr-14 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10"
|
||||
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
|
||||
>
|
||||
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
|
||||
<div
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu } from 'bits-ui';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
import { getContext } from 'svelte';
|
||||
import { createPicker } from '$lib/utils/google-drive-picker';
|
||||
import { getContext, onMount, tick } from 'svelte';
|
||||
|
||||
import { config, user, tools as _tools } from '$lib/stores';
|
||||
import { getTools } from '$lib/apis/tools';
|
||||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import DocumentArrowUpSolid from '$lib/components/icons/DocumentArrowUpSolid.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import GlobeAltSolid from '$lib/components/icons/GlobeAltSolid.svelte';
|
||||
import { config } from '$lib/stores';
|
||||
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
@@ -18,22 +19,31 @@
|
||||
export let uploadGoogleDriveHandler: Function;
|
||||
|
||||
export let selectedToolIds: string[] = [];
|
||||
export let webSearchEnabled: boolean;
|
||||
|
||||
export let tools = {};
|
||||
export let webSearchEnabled: boolean;
|
||||
export let onClose: Function;
|
||||
|
||||
$: tools = Object.fromEntries(
|
||||
Object.keys(tools).map((toolId) => [
|
||||
toolId,
|
||||
{
|
||||
...tools[toolId],
|
||||
enabled: selectedToolIds.includes(toolId)
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
let tools = {};
|
||||
let show = false;
|
||||
|
||||
$: if (show) {
|
||||
init();
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
if ($_tools === null) {
|
||||
await _tools.set(await getTools(localStorage.token));
|
||||
}
|
||||
|
||||
tools = $_tools.reduce((a, tool, i, arr) => {
|
||||
a[tool.id] = {
|
||||
name: tool.name,
|
||||
description: tool.meta.description,
|
||||
enabled: selectedToolIds.includes(tool.id)
|
||||
};
|
||||
return a;
|
||||
}, {});
|
||||
};
|
||||
</script>
|
||||
|
||||
<Dropdown
|
||||
@@ -60,49 +70,63 @@
|
||||
{#if Object.keys(tools).length > 0}
|
||||
<div class=" max-h-28 overflow-y-auto scrollbar-hidden">
|
||||
{#each Object.keys(tools) as toolId}
|
||||
<div
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
|
||||
<button
|
||||
class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
|
||||
on:click={() => {
|
||||
tools[toolId].enabled = !tools[toolId].enabled;
|
||||
}}
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="flex-1 truncate">
|
||||
<Tooltip
|
||||
content={tools[toolId]?.description ?? ''}
|
||||
placement="top-start"
|
||||
className="flex flex-1 gap-2 items-center"
|
||||
className="flex flex-1 gap-2 items-center"
|
||||
>
|
||||
<WrenchSolid />
|
||||
<div class="flex-shrink-0">
|
||||
<WrenchSolid />
|
||||
</div>
|
||||
|
||||
<div class=" line-clamp-1">{tools[toolId].name}</div>
|
||||
<div class=" truncate">{tools[toolId].name}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
bind:state={tools[toolId].enabled}
|
||||
on:change={(e) => {
|
||||
selectedToolIds = e.detail
|
||||
? [...selectedToolIds, toolId]
|
||||
: selectedToolIds.filter((id) => id !== toolId);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class=" flex-shrink-0">
|
||||
<Switch
|
||||
state={tools[toolId].enabled}
|
||||
on:change={async (e) => {
|
||||
const state = e.detail;
|
||||
await tick();
|
||||
if (state) {
|
||||
selectedToolIds = [...selectedToolIds, toolId];
|
||||
} else {
|
||||
selectedToolIds = selectedToolIds.filter((id) => id !== toolId);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<hr class="border-gray-100 dark:border-gray-800 my-1" />
|
||||
<hr class="border-black/5 dark:border-white/5 my-1" />
|
||||
{/if}
|
||||
|
||||
{#if $config?.features?.enable_web_search}
|
||||
<div
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
|
||||
<button
|
||||
class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
|
||||
on:click={() => {
|
||||
webSearchEnabled = !webSearchEnabled;
|
||||
}}
|
||||
>
|
||||
<div class="flex-1 flex items-center gap-2">
|
||||
<GlobeAltSolid />
|
||||
<div class=" line-clamp-1">{$i18n.t('Web Search')}</div>
|
||||
</div>
|
||||
|
||||
<Switch bind:state={webSearchEnabled} />
|
||||
</div>
|
||||
<Switch state={webSearchEnabled} />
|
||||
</button>
|
||||
|
||||
<hr class="border-gray-100 dark:border-gray-800 my-1" />
|
||||
<hr class="border-black/5 dark:border-white/5 my-1" />
|
||||
{/if}
|
||||
|
||||
<DropdownMenu.Item
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
mediaRecorder.ondataavailable = (event) => audioChunks.push(event.data);
|
||||
mediaRecorder.onstop = async () => {
|
||||
console.log('Recording stopped');
|
||||
if (($settings?.audio?.stt?.engine ?? '') === 'web') {
|
||||
if ($config.audio.stt.engine === 'web' || ($settings?.audio?.stt?.engine ?? '') === 'web') {
|
||||
audioChunks = [];
|
||||
} else {
|
||||
if (confirmed) {
|
||||
@@ -229,8 +229,7 @@
|
||||
console.log('recognition ended');
|
||||
|
||||
confirmRecording();
|
||||
dispatch('confirm', transcription);
|
||||
|
||||
dispatch('confirm', { text: transcription });
|
||||
confirmed = false;
|
||||
loading = false;
|
||||
};
|
||||
@@ -251,6 +250,11 @@
|
||||
if (recording && mediaRecorder) {
|
||||
await mediaRecorder.stop();
|
||||
}
|
||||
|
||||
if (speechRecognition) {
|
||||
speechRecognition.stop();
|
||||
}
|
||||
|
||||
stopDurationCounter();
|
||||
audioChunks = [];
|
||||
|
||||
@@ -325,8 +329,8 @@
|
||||
|
||||
rounded-full"
|
||||
on:click={async () => {
|
||||
dispatch('cancel');
|
||||
stopRecording();
|
||||
dispatch('cancel');
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
|
||||
@@ -29,8 +29,10 @@
|
||||
export let continueResponse: Function;
|
||||
export let regenerateResponse: Function;
|
||||
export let mergeResponses: Function;
|
||||
|
||||
export let chatActionHandler: Function;
|
||||
export let showMessage: Function = () => {};
|
||||
export let submitMessage: Function = () => {};
|
||||
|
||||
export let readOnly = false;
|
||||
|
||||
@@ -79,9 +81,9 @@
|
||||
element.scrollTop = element.scrollHeight;
|
||||
};
|
||||
|
||||
const updateChatHistory = async () => {
|
||||
await tick();
|
||||
const updateChat = async () => {
|
||||
history = history;
|
||||
await tick();
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
history: history,
|
||||
messages: messages
|
||||
@@ -195,7 +197,7 @@
|
||||
rating: rating
|
||||
};
|
||||
|
||||
await updateChatHistory();
|
||||
await updateChat();
|
||||
};
|
||||
|
||||
const editMessage = async (messageId, content, submit = true) => {
|
||||
@@ -232,7 +234,7 @@
|
||||
} else {
|
||||
// Edit user message
|
||||
history.messages[messageId].content = content;
|
||||
await updateChatHistory();
|
||||
await updateChat();
|
||||
}
|
||||
} else {
|
||||
if (submit) {
|
||||
@@ -246,6 +248,7 @@
|
||||
id: responseMessageId,
|
||||
parentId: parentId,
|
||||
childrenIds: [],
|
||||
files: undefined,
|
||||
content: content,
|
||||
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
|
||||
};
|
||||
@@ -261,16 +264,25 @@
|
||||
];
|
||||
}
|
||||
|
||||
await updateChatHistory();
|
||||
await updateChat();
|
||||
} else {
|
||||
// Edit response message
|
||||
history.messages[messageId].originalContent = history.messages[messageId].content;
|
||||
history.messages[messageId].content = content;
|
||||
await updateChatHistory();
|
||||
await updateChat();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const actionMessage = async (actionId, message, event = null) => {
|
||||
await chatActionHandler(chatId, actionId, message.model, message.id, event);
|
||||
};
|
||||
|
||||
const saveMessage = async (messageId, message) => {
|
||||
history.messages[messageId] = message;
|
||||
await updateChat();
|
||||
};
|
||||
|
||||
const deleteMessage = async (messageId) => {
|
||||
const messageToDelete = history.messages[messageId];
|
||||
const parentMessageId = messageToDelete.parentId;
|
||||
@@ -306,7 +318,17 @@
|
||||
showMessage({ id: parentMessageId });
|
||||
|
||||
// Update the chat
|
||||
await updateChatHistory();
|
||||
await updateChat();
|
||||
};
|
||||
|
||||
const triggerScroll = () => {
|
||||
if (autoScroll) {
|
||||
const element = document.getElementById('messages-container');
|
||||
autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -372,37 +394,18 @@
|
||||
{user}
|
||||
{showPreviousMessage}
|
||||
{showNextMessage}
|
||||
{updateChat}
|
||||
{editMessage}
|
||||
{deleteMessage}
|
||||
{rateMessage}
|
||||
{actionMessage}
|
||||
{saveMessage}
|
||||
{submitMessage}
|
||||
{regenerateResponse}
|
||||
{continueResponse}
|
||||
{mergeResponses}
|
||||
{triggerScroll}
|
||||
{readOnly}
|
||||
on:submit={async (e) => {
|
||||
dispatch('submit', e.detail);
|
||||
}}
|
||||
on:action={async (e) => {
|
||||
if (typeof e.detail === 'string') {
|
||||
await chatActionHandler(chatId, e.detail, message.model, message.id);
|
||||
} else {
|
||||
const { id, event } = e.detail;
|
||||
await chatActionHandler(chatId, id, message.model, message.id, event);
|
||||
}
|
||||
}}
|
||||
on:update={() => {
|
||||
updateChatHistory();
|
||||
}}
|
||||
on:scroll={() => {
|
||||
if (autoScroll) {
|
||||
const element = document.getElementById('messages-container');
|
||||
autoScroll =
|
||||
element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let citations = [];
|
||||
export let sources = [];
|
||||
|
||||
let _citations = [];
|
||||
let citations = [];
|
||||
let showPercentage = false;
|
||||
let showRelevance = true;
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
let selectedCitation: any = null;
|
||||
let isCollapsibleOpen = false;
|
||||
|
||||
function calculateShowRelevance(citations: any[]) {
|
||||
const distances = citations.flatMap((citation) => citation.distances ?? []);
|
||||
function calculateShowRelevance(sources: any[]) {
|
||||
const distances = sources.flatMap((citation) => citation.distances ?? []);
|
||||
const inRange = distances.filter((d) => d !== undefined && d >= -1 && d <= 1).length;
|
||||
const outOfRange = distances.filter((d) => d !== undefined && (d < -1 || d > 1)).length;
|
||||
|
||||
@@ -36,25 +36,31 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function shouldShowPercentage(citations: any[]) {
|
||||
const distances = citations.flatMap((citation) => citation.distances ?? []);
|
||||
function shouldShowPercentage(sources: any[]) {
|
||||
const distances = sources.flatMap((citation) => citation.distances ?? []);
|
||||
return distances.every((d) => d !== undefined && d >= -1 && d <= 1);
|
||||
}
|
||||
|
||||
$: {
|
||||
_citations = citations.reduce((acc, citation) => {
|
||||
citation.document.forEach((document, index) => {
|
||||
const metadata = citation.metadata?.[index];
|
||||
const distance = citation.distances?.[index];
|
||||
citations = sources.reduce((acc, source) => {
|
||||
if (Object.keys(source).length === 0) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
source.document.forEach((document, index) => {
|
||||
const metadata = source.metadata?.[index];
|
||||
const distance = source.distances?.[index];
|
||||
|
||||
// Within the same citation there could be multiple documents
|
||||
const id = metadata?.source ?? 'N/A';
|
||||
let source = citation?.source;
|
||||
let _source = source?.source;
|
||||
|
||||
if (metadata?.name) {
|
||||
source = { ...source, name: metadata.name };
|
||||
_source = { ..._source, name: metadata.name };
|
||||
}
|
||||
|
||||
if (id.startsWith('http://') || id.startsWith('https://')) {
|
||||
source = { name: id, ...source, url: id };
|
||||
_source = { ..._source, name: id, url: id };
|
||||
}
|
||||
|
||||
const existingSource = acc.find((item) => item.id === id);
|
||||
@@ -66,7 +72,7 @@
|
||||
} else {
|
||||
acc.push({
|
||||
id: id,
|
||||
source: source,
|
||||
source: _source,
|
||||
document: [document],
|
||||
metadata: metadata ? [metadata] : [],
|
||||
distances: distance !== undefined ? [distance] : undefined
|
||||
@@ -76,8 +82,8 @@
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
showRelevance = calculateShowRelevance(_citations);
|
||||
showPercentage = shouldShowPercentage(_citations);
|
||||
showRelevance = calculateShowRelevance(citations);
|
||||
showPercentage = shouldShowPercentage(citations);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -88,96 +94,67 @@
|
||||
{showRelevance}
|
||||
/>
|
||||
|
||||
{#if _citations.length > 0}
|
||||
<div class="mt-1 mb-2 w-full flex gap-1 items-center flex-wrap">
|
||||
{#if _citations.length <= 3}
|
||||
{#each _citations as citation, idx}
|
||||
<div class="flex gap-1 text-xs font-semibold">
|
||||
{#if citations.length > 0}
|
||||
<div class=" py-0.5 -mx-0.5 w-full flex gap-1 items-center flex-wrap">
|
||||
{#if citations.length <= 3}
|
||||
<div class="flex text-xs font-medium">
|
||||
{#each citations as citation, idx}
|
||||
<button
|
||||
class="no-toggle flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl max-w-96"
|
||||
id={`source-${citation.source.name}`}
|
||||
class="no-toggle outline-none flex dark:text-gray-300 p-1 bg-white dark:bg-gray-900 rounded-xl max-w-96"
|
||||
on:click={() => {
|
||||
showCitationModal = true;
|
||||
selectedCitation = citation;
|
||||
}}
|
||||
>
|
||||
{#if _citations.every((c) => c.distances !== undefined)}
|
||||
<div class="bg-white dark:bg-gray-700 rounded-full size-4">
|
||||
{#if citations.every((c) => c.distances !== undefined)}
|
||||
<div class="bg-gray-50 dark:bg-gray-800 rounded-full size-4">
|
||||
{idx + 1}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-1 mx-2 line-clamp-1 truncate">
|
||||
<div
|
||||
class="flex-1 mx-1 line-clamp-1 text-black/60 hover:text-black dark:text-white/60 dark:hover:text-white transition"
|
||||
>
|
||||
{citation.source.name}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Collapsible bind:open={isCollapsibleOpen} className="w-full">
|
||||
<div
|
||||
class="flex items-center gap-1 text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 transition cursor-pointer"
|
||||
class="flex items-center gap-2 text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 transition cursor-pointer"
|
||||
>
|
||||
<div class="flex-grow flex items-center gap-1 overflow-hidden">
|
||||
<span class="whitespace-nowrap hidden sm:inline">{$i18n.t('References from')}</span>
|
||||
<div class="flex items-center">
|
||||
{#if _citations.length > 1 && _citations
|
||||
.slice(0, 2)
|
||||
.reduce((acc, citation) => acc + citation.source.name.length, 0) <= 50}
|
||||
{#each _citations.slice(0, 2) as citation, idx}
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
class="no-toggle flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl max-w-96 text-xs font-semibold"
|
||||
on:click={() => {
|
||||
showCitationModal = true;
|
||||
selectedCitation = citation;
|
||||
}}
|
||||
>
|
||||
{#if _citations.every((c) => c.distances !== undefined)}
|
||||
<div class="bg-white dark:bg-gray-700 rounded-full size-4">
|
||||
{idx + 1}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-1 mx-2 line-clamp-1">
|
||||
{citation.source.name}
|
||||
<div class="flex text-xs font-medium items-center">
|
||||
{#each citations.slice(0, 2) as citation, idx}
|
||||
<button
|
||||
class="no-toggle outline-none flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
|
||||
on:click={() => {
|
||||
showCitationModal = true;
|
||||
selectedCitation = citation;
|
||||
}}
|
||||
on:pointerup={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{#if citations.every((c) => c.distances !== undefined)}
|
||||
<div class="bg-gray-50 dark:bg-gray-800 rounded-full size-4">
|
||||
{idx + 1}
|
||||
</div>
|
||||
</button>
|
||||
{#if idx === 0}<span class="mr-1">,</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-1 mx-1 line-clamp-1 truncate">
|
||||
{citation.source.name}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each _citations.slice(0, 1) as citation, idx}
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
class="no-toggle flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl max-w-96 text-xs font-semibold"
|
||||
on:click={() => {
|
||||
showCitationModal = true;
|
||||
selectedCitation = citation;
|
||||
}}
|
||||
>
|
||||
{#if _citations.every((c) => c.distances !== undefined)}
|
||||
<div class="bg-white dark:bg-gray-700 rounded-full size-4">
|
||||
{idx + 1}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-1 mx-2 line-clamp-1">
|
||||
{citation.source.name}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 whitespace-nowrap">
|
||||
<span class="hidden sm:inline">{$i18n.t('and')}</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">
|
||||
{_citations.length -
|
||||
(_citations.length > 1 &&
|
||||
_citations
|
||||
.slice(0, 2)
|
||||
.reduce((acc, citation) => acc + citation.source.name.length, 0) <= 50
|
||||
? 2
|
||||
: 1)}
|
||||
</span>
|
||||
{citations.length - 2}
|
||||
<span>{$i18n.t('more')}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,27 +166,25 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div slot="content" class="mt-2">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each _citations as citation, idx}
|
||||
<div class="flex gap-1 text-xs font-semibold">
|
||||
<button
|
||||
class="no-toggle flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl max-w-96"
|
||||
on:click={() => {
|
||||
showCitationModal = true;
|
||||
selectedCitation = citation;
|
||||
}}
|
||||
>
|
||||
{#if _citations.every((c) => c.distances !== undefined)}
|
||||
<div class="bg-white dark:bg-gray-700 rounded-full size-4">
|
||||
{idx + 1}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-1 mx-2 line-clamp-1">
|
||||
{citation.source.name}
|
||||
<div slot="content">
|
||||
<div class="flex text-xs font-medium">
|
||||
{#each citations as citation, idx}
|
||||
<button
|
||||
class="no-toggle outline-none flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
|
||||
on:click={() => {
|
||||
showCitationModal = true;
|
||||
selectedCitation = citation;
|
||||
}}
|
||||
>
|
||||
{#if citations.every((c) => c.distances !== undefined)}
|
||||
<div class="bg-gray-50 dark:bg-gray-800 rounded-full size-4">
|
||||
{idx + 1}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-1 mx-1 line-clamp-1 truncate">
|
||||
{citation.source.name}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { getContext, onMount, tick } from 'svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -13,9 +14,9 @@
|
||||
let mergedDocuments = [];
|
||||
|
||||
function calculatePercentage(distance: number) {
|
||||
if (distance < 0) return 100;
|
||||
if (distance > 1) return 0;
|
||||
return Math.round((1 - distance) * 10000) / 100;
|
||||
if (distance < 0) return 0;
|
||||
if (distance > 1) return 100;
|
||||
return Math.round(distance * 10000) / 100;
|
||||
}
|
||||
|
||||
function getRelevanceColor(percentage: number) {
|
||||
@@ -38,7 +39,9 @@
|
||||
};
|
||||
});
|
||||
if (mergedDocuments.every((doc) => doc.distance !== undefined)) {
|
||||
mergedDocuments.sort((a, b) => (a.distance ?? Infinity) - (b.distance ?? Infinity));
|
||||
mergedDocuments = mergedDocuments.sort(
|
||||
(a, b) => (b.distance ?? Infinity) - (a.distance ?? Infinity)
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -89,7 +92,7 @@
|
||||
<a
|
||||
class="hover:text-gray-500 hover:dark:text-gray-100 underline flex-grow"
|
||||
href={document?.metadata?.file_id
|
||||
? `/api/v1/files/${document?.metadata?.file_id}/content${document?.metadata?.page !== undefined ? `#page=${document.metadata.page + 1}` : ''}`
|
||||
? `${WEBUI_API_BASE_URL}/files/${document?.metadata?.file_id}/content${document?.metadata?.page !== undefined ? `#page=${document.metadata.page + 1}` : ''}`
|
||||
: document.source?.url?.includes('http')
|
||||
? document.source.url
|
||||
: `#`}
|
||||
@@ -148,9 +151,18 @@
|
||||
<div class=" text-sm font-medium dark:text-gray-300 mt-2">
|
||||
{$i18n.t('Content')}
|
||||
</div>
|
||||
<pre class="text-sm dark:text-gray-400 whitespace-pre-line">
|
||||
{document.document}
|
||||
</pre>
|
||||
{#if document.metadata?.html}
|
||||
<iframe
|
||||
class="w-full border-0 h-auto rounded-none"
|
||||
sandbox="allow-scripts allow-forms allow-same-origin"
|
||||
srcdoc={document.document}
|
||||
title={$i18n.t('Content')}
|
||||
></iframe>
|
||||
{:else}
|
||||
<pre class="text-sm dark:text-gray-400 whitespace-pre-line">
|
||||
{document.document}
|
||||
</pre>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if documentIdx !== mergedDocuments.length - 1}
|
||||
|
||||
@@ -7,13 +7,16 @@
|
||||
import LightBlub from '$lib/components/icons/LightBlub.svelte';
|
||||
import { chatId, mobile, showArtifacts, showControls, showOverview } from '$lib/stores';
|
||||
import ChatBubble from '$lib/components/icons/ChatBubble.svelte';
|
||||
import { stringify } from 'postcss';
|
||||
|
||||
export let id;
|
||||
export let content;
|
||||
export let model = null;
|
||||
export let sources = null;
|
||||
|
||||
export let save = false;
|
||||
export let floatingButtons = true;
|
||||
export let onSourceClick = () => {};
|
||||
|
||||
let contentContainerElement;
|
||||
let buttonsContainerElement;
|
||||
@@ -129,6 +132,32 @@
|
||||
{content}
|
||||
{model}
|
||||
{save}
|
||||
sourceIds={(sources ?? []).reduce((acc, s) => {
|
||||
let ids = [];
|
||||
s.document.forEach((document, index) => {
|
||||
const metadata = s.metadata?.[index];
|
||||
const id = metadata?.source ?? 'N/A';
|
||||
|
||||
if (metadata?.name) {
|
||||
ids.push(metadata.name);
|
||||
return ids;
|
||||
}
|
||||
|
||||
if (id.startsWith('http://') || id.startsWith('https://')) {
|
||||
ids.push(id);
|
||||
} else {
|
||||
ids.push(s?.source?.name ?? id);
|
||||
}
|
||||
|
||||
return ids;
|
||||
});
|
||||
|
||||
acc = [...acc, ...ids];
|
||||
|
||||
// remove duplicates
|
||||
return acc.filter((item, index) => acc.indexOf(item) === index);
|
||||
}, [])}
|
||||
{onSourceClick}
|
||||
on:update={(e) => {
|
||||
dispatch('update', e.detail);
|
||||
}}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
export let content = '';
|
||||
</script>
|
||||
|
||||
<div class="flex my-2 gap-2.5 border px-4 py-3 border-red-800 bg-red-800/30 rounded-lg">
|
||||
<div class="flex my-2 gap-2.5 border px-4 py-3 border-red-600/10 bg-red-600/10 rounded-lg">
|
||||
<div class=" self-start mt-0.5">
|
||||
<Info className="size-5" />
|
||||
<Info className="size-5 text-red-700 dark:text-red-400" />
|
||||
</div>
|
||||
|
||||
<div class=" self-center text-sm">
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
export let model = null;
|
||||
export let save = false;
|
||||
|
||||
export let sourceIds = [];
|
||||
export let onSourceClick = () => {};
|
||||
|
||||
let tokens = [];
|
||||
|
||||
const options = {
|
||||
@@ -28,7 +31,7 @@
|
||||
$: (async () => {
|
||||
if (content) {
|
||||
tokens = marked.lexer(
|
||||
replaceTokens(processResponseContent(content), model?.name, $user?.name)
|
||||
replaceTokens(processResponseContent(content), sourceIds, model?.name, $user?.name)
|
||||
);
|
||||
}
|
||||
})();
|
||||
@@ -39,6 +42,7 @@
|
||||
{tokens}
|
||||
{id}
|
||||
{save}
|
||||
{onSourceClick}
|
||||
on:update={(e) => {
|
||||
dispatch('update', e.detail);
|
||||
}}
|
||||
|
||||
@@ -12,9 +12,11 @@
|
||||
|
||||
import Image from '$lib/components/common/Image.svelte';
|
||||
import KatexRenderer from './KatexRenderer.svelte';
|
||||
import Source from './Source.svelte';
|
||||
|
||||
export let id: string;
|
||||
export let tokens: Token[];
|
||||
export let onSourceClick: Function = () => {};
|
||||
</script>
|
||||
|
||||
{#each tokens as token}
|
||||
@@ -26,13 +28,15 @@
|
||||
{@html html}
|
||||
{:else if token.text.includes(`<iframe src="${WEBUI_BASE_URL}/api/v1/files/`)}
|
||||
{@html `${token.text}`}
|
||||
{:else if token.text.includes(`<source_id`)}
|
||||
<Source {token} onClick={onSourceClick} />
|
||||
{:else}
|
||||
{token.text}
|
||||
{/if}
|
||||
{:else if token.type === 'link'}
|
||||
{#if token.tokens}
|
||||
<a href={token.href} target="_blank" rel="nofollow" title={token.title}>
|
||||
<svelte:self id={`${id}-a`} tokens={token.tokens} />
|
||||
<svelte:self id={`${id}-a`} tokens={token.tokens} {onSourceClick} />
|
||||
</a>
|
||||
{:else}
|
||||
<a href={token.href} target="_blank" rel="nofollow" title={token.title}>{token.text}</a>
|
||||
@@ -41,11 +45,11 @@
|
||||
<Image src={token.href} alt={token.text} />
|
||||
{:else if token.type === 'strong'}
|
||||
<strong>
|
||||
<svelte:self id={`${id}-strong`} tokens={token.tokens} />
|
||||
<svelte:self id={`${id}-strong`} tokens={token.tokens} {onSourceClick} />
|
||||
</strong>
|
||||
{:else if token.type === 'em'}
|
||||
<em>
|
||||
<svelte:self id={`${id}-em`} tokens={token.tokens} />
|
||||
<svelte:self id={`${id}-em`} tokens={token.tokens} {onSourceClick} />
|
||||
</em>
|
||||
{:else if token.type === 'codespan'}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
@@ -61,7 +65,7 @@
|
||||
<br />
|
||||
{:else if token.type === 'del'}
|
||||
<del>
|
||||
<svelte:self id={`${id}-del`} tokens={token.tokens} />
|
||||
<svelte:self id={`${id}-del`} tokens={token.tokens} {onSourceClick} />
|
||||
</del>
|
||||
{:else if token.type === 'inlineKatex'}
|
||||
{#if token.text}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<script lang="ts">
|
||||
import DOMPurify from 'dompurify';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import { marked, type Token } from 'marked';
|
||||
import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils';
|
||||
|
||||
@@ -10,6 +15,8 @@
|
||||
import MarkdownInlineTokens from '$lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte';
|
||||
import KatexRenderer from './KatexRenderer.svelte';
|
||||
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -18,19 +25,45 @@
|
||||
export let top = true;
|
||||
|
||||
export let save = false;
|
||||
export let onSourceClick: Function = () => {};
|
||||
|
||||
const headerComponent = (depth: number) => {
|
||||
return 'h' + depth;
|
||||
};
|
||||
|
||||
const exportTableToCSVHandler = (token, tokenIdx = 0) => {
|
||||
console.log('Exporting table to CSV');
|
||||
|
||||
// Create an array for rows that will hold the mapped cell text.
|
||||
const rows = token.rows.map((row) =>
|
||||
row.map((cell) => cell.tokens.map((token) => token.text).join(''))
|
||||
);
|
||||
|
||||
// Join the rows using commas (,) as the separator and rows using newline (\n).
|
||||
const csvContent = rows.map((row) => row.join(',')).join('\n');
|
||||
|
||||
// Log rows and CSV content to ensure everything is correct.
|
||||
console.log(rows);
|
||||
console.log(csvContent);
|
||||
|
||||
// To handle Unicode characters, you need to prefix the data with a BOM:
|
||||
const bom = '\uFEFF'; // BOM for UTF-8
|
||||
|
||||
// Create a new Blob prefixed with the BOM to ensure proper Unicode encoding.
|
||||
const blob = new Blob([bom + csvContent], { type: 'text/csv;charset=UTF-8' });
|
||||
|
||||
// Use FileSaver.js's saveAs function to save the generated CSV file.
|
||||
saveAs(blob, `table-${id}-${tokenIdx}.csv`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- {JSON.stringify(tokens)} -->
|
||||
{#each tokens as token, tokenIdx (tokenIdx)}
|
||||
{#if token.type === 'hr'}
|
||||
<hr />
|
||||
<hr class=" border-gray-50 dark:border-gray-850" />
|
||||
{:else if token.type === 'heading'}
|
||||
<svelte:element this={headerComponent(token.depth)}>
|
||||
<MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} />
|
||||
<MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} {onSourceClick} />
|
||||
</svelte:element>
|
||||
{:else if token.type === 'code'}
|
||||
{#if token.raw.includes('```')}
|
||||
@@ -55,35 +88,70 @@
|
||||
{token.text}
|
||||
{/if}
|
||||
{:else if token.type === 'table'}
|
||||
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
{#each token.header as header, headerIdx}
|
||||
<th style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}>
|
||||
<MarkdownInlineTokens
|
||||
id={`${id}-${tokenIdx}-header-${headerIdx}`}
|
||||
tokens={header.tokens}
|
||||
/>
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each token.rows as row, rowIdx}
|
||||
<tr>
|
||||
{#each row ?? [] as cell, cellIdx}
|
||||
<td style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}>
|
||||
<MarkdownInlineTokens
|
||||
id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
|
||||
tokens={cell.tokens}
|
||||
/>
|
||||
</td>
|
||||
<div class="relative w-full group">
|
||||
<div class="scrollbar-hidden relative overflow-x-auto max-w-full rounded-lg">
|
||||
<table
|
||||
class=" w-full text-sm text-left text-gray-500 dark:text-gray-400 max-w-full rounded-xl"
|
||||
>
|
||||
<thead
|
||||
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 border-none"
|
||||
>
|
||||
<tr class="">
|
||||
{#each token.header as header, headerIdx}
|
||||
<th
|
||||
scope="col"
|
||||
class="!px-3 !py-1.5 cursor-pointer select-none border border-gray-50 dark:border-gray-850"
|
||||
style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}
|
||||
>
|
||||
<div class="flex flex-col gap-1.5 text-left">
|
||||
<div class="flex-shrink-0 break-normal">
|
||||
<MarkdownInlineTokens
|
||||
id={`${id}-${tokenIdx}-header-${headerIdx}`}
|
||||
tokens={header.tokens}
|
||||
{onSourceClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each token.rows as row, rowIdx}
|
||||
<tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
|
||||
{#each row ?? [] as cell, cellIdx}
|
||||
<td
|
||||
class="!px-3 !py-1.5 text-gray-900 dark:text-white w-max border border-gray-50 dark:border-gray-850"
|
||||
style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}
|
||||
>
|
||||
<div class="flex flex-col break-normal">
|
||||
<MarkdownInlineTokens
|
||||
id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
|
||||
tokens={cell.tokens}
|
||||
{onSourceClick}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class=" absolute top-1 right-1.5 z-20 invisible group-hover:visible">
|
||||
<Tooltip content={$i18n.t('Export to CSV')}>
|
||||
<button
|
||||
class="p-1 rounded-lg bg-transparent transition"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
exportTableToCSVHandler(token, tokenIdx);
|
||||
}}
|
||||
>
|
||||
<ArrowDownTray className=" size-3.5" strokeWidth="1.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{:else if token.type === 'blockquote'}
|
||||
<blockquote>
|
||||
@@ -140,19 +208,27 @@
|
||||
></iframe>
|
||||
{:else if token.type === 'paragraph'}
|
||||
<p>
|
||||
<MarkdownInlineTokens id={`${id}-${tokenIdx}-p`} tokens={token.tokens ?? []} />
|
||||
<MarkdownInlineTokens
|
||||
id={`${id}-${tokenIdx}-p`}
|
||||
tokens={token.tokens ?? []}
|
||||
{onSourceClick}
|
||||
/>
|
||||
</p>
|
||||
{:else if token.type === 'text'}
|
||||
{#if top}
|
||||
<p>
|
||||
{#if token.tokens}
|
||||
<MarkdownInlineTokens id={`${id}-${tokenIdx}-t`} tokens={token.tokens} />
|
||||
<MarkdownInlineTokens id={`${id}-${tokenIdx}-t`} tokens={token.tokens} {onSourceClick} />
|
||||
{:else}
|
||||
{unescapeHtml(token.text)}
|
||||
{/if}
|
||||
</p>
|
||||
{:else if token.tokens}
|
||||
<MarkdownInlineTokens id={`${id}-${tokenIdx}-p`} tokens={token.tokens ?? []} />
|
||||
<MarkdownInlineTokens
|
||||
id={`${id}-${tokenIdx}-p`}
|
||||
tokens={token.tokens ?? []}
|
||||
{onSourceClick}
|
||||
/>
|
||||
{:else}
|
||||
{unescapeHtml(token.text)}
|
||||
{/if}
|
||||
|
||||
25
src/lib/components/chat/Messages/Markdown/Source.svelte
Normal file
25
src/lib/components/chat/Messages/Markdown/Source.svelte
Normal file
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
export let token;
|
||||
export let onClick: Function = () => {};
|
||||
|
||||
let id = '';
|
||||
function extractDataAttribute(input) {
|
||||
// Use a regular expression to extract the value of the `data` attribute
|
||||
const match = input.match(/data="([^"]*)"/);
|
||||
// Check if a match was found and return the first captured group
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
$: id = extractDataAttribute(token.text);
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="text-xs font-medium w-fit translate-y-[2px] px-2 py-0.5 dark:bg-white/5 dark:text-white/60 dark:hover:text-white bg-gray-50 text-black/60 hover:text-black transition rounded-lg"
|
||||
on:click={() => {
|
||||
onClick(id);
|
||||
}}
|
||||
>
|
||||
<span class="line-clamp-1">
|
||||
{id}
|
||||
</span>
|
||||
</button>
|
||||
@@ -22,23 +22,21 @@
|
||||
|
||||
export let showPreviousMessage;
|
||||
export let showNextMessage;
|
||||
export let updateChat;
|
||||
|
||||
export let editMessage;
|
||||
export let saveMessage;
|
||||
export let deleteMessage;
|
||||
export let rateMessage;
|
||||
export let actionMessage;
|
||||
export let submitMessage;
|
||||
|
||||
export let regenerateResponse;
|
||||
export let continueResponse;
|
||||
|
||||
// MultiResponseMessages
|
||||
export let mergeResponses;
|
||||
|
||||
export let autoScroll = false;
|
||||
export let triggerScroll;
|
||||
export let readOnly = false;
|
||||
|
||||
onMount(() => {
|
||||
// console.log('message', idx);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -61,7 +59,7 @@
|
||||
{showPreviousMessage}
|
||||
{showNextMessage}
|
||||
{editMessage}
|
||||
on:delete={() => deleteMessage(messageId)}
|
||||
{deleteMessage}
|
||||
{readOnly}
|
||||
/>
|
||||
{:else if (history.messages[history.messages[messageId].parentId]?.models?.length ?? 1) === 1}
|
||||
@@ -73,30 +71,14 @@
|
||||
siblings={history.messages[history.messages[messageId].parentId]?.childrenIds ?? []}
|
||||
{showPreviousMessage}
|
||||
{showNextMessage}
|
||||
{updateChat}
|
||||
{editMessage}
|
||||
{saveMessage}
|
||||
{rateMessage}
|
||||
{actionMessage}
|
||||
{submitMessage}
|
||||
{continueResponse}
|
||||
{regenerateResponse}
|
||||
on:submit={async (e) => {
|
||||
dispatch('submit', e.detail);
|
||||
}}
|
||||
on:action={async (e) => {
|
||||
dispatch('action', e.detail);
|
||||
}}
|
||||
on:update={async (e) => {
|
||||
dispatch('update');
|
||||
}}
|
||||
on:save={async (e) => {
|
||||
console.log('save', e);
|
||||
|
||||
const message = e.detail;
|
||||
if (message) {
|
||||
history.messages[message.id] = message;
|
||||
dispatch('update');
|
||||
} else {
|
||||
dispatch('update');
|
||||
}
|
||||
}}
|
||||
{readOnly}
|
||||
/>
|
||||
{:else}
|
||||
@@ -105,35 +87,16 @@
|
||||
{chatId}
|
||||
{messageId}
|
||||
isLastMessage={messageId === history?.currentId}
|
||||
{rateMessage}
|
||||
{updateChat}
|
||||
{editMessage}
|
||||
{saveMessage}
|
||||
{rateMessage}
|
||||
{actionMessage}
|
||||
{submitMessage}
|
||||
{continueResponse}
|
||||
{regenerateResponse}
|
||||
{mergeResponses}
|
||||
on:submit={async (e) => {
|
||||
dispatch('submit', e.detail);
|
||||
}}
|
||||
on:action={async (e) => {
|
||||
dispatch('action', e.detail);
|
||||
}}
|
||||
on:update={async (e) => {
|
||||
dispatch('update');
|
||||
}}
|
||||
on:save={async (e) => {
|
||||
console.log('save', e);
|
||||
const message = e.detail;
|
||||
if (message) {
|
||||
history.messages[message.id] = message;
|
||||
dispatch('update');
|
||||
} else {
|
||||
dispatch('update');
|
||||
}
|
||||
}}
|
||||
on:change={async () => {
|
||||
await tick();
|
||||
dispatch('update');
|
||||
dispatch('scroll');
|
||||
}}
|
||||
{triggerScroll}
|
||||
{readOnly}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -25,13 +25,19 @@
|
||||
export let isLastMessage;
|
||||
export let readOnly = false;
|
||||
|
||||
export let updateChat: Function;
|
||||
export let editMessage: Function;
|
||||
export let saveMessage: Function;
|
||||
export let rateMessage: Function;
|
||||
export let actionMessage: Function;
|
||||
|
||||
export let submitMessage: Function;
|
||||
export let continueResponse: Function;
|
||||
export let regenerateResponse: Function;
|
||||
export let mergeResponses: Function;
|
||||
|
||||
export let triggerScroll: Function;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let currentMessageId;
|
||||
@@ -46,7 +52,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
const showPreviousMessage = (modelIdx) => {
|
||||
const showPreviousMessage = async (modelIdx) => {
|
||||
groupedMessageIdsIdx[modelIdx] = Math.max(0, groupedMessageIdsIdx[modelIdx] - 1);
|
||||
|
||||
let messageId = groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]];
|
||||
@@ -60,10 +66,13 @@
|
||||
}
|
||||
|
||||
history.currentId = messageId;
|
||||
dispatch('change');
|
||||
|
||||
await tick();
|
||||
await updateChat();
|
||||
triggerScroll();
|
||||
};
|
||||
|
||||
const showNextMessage = (modelIdx) => {
|
||||
const showNextMessage = async (modelIdx) => {
|
||||
groupedMessageIdsIdx[modelIdx] = Math.min(
|
||||
groupedMessageIds[modelIdx].messageIds.length - 1,
|
||||
groupedMessageIdsIdx[modelIdx] + 1
|
||||
@@ -80,7 +89,10 @@
|
||||
}
|
||||
|
||||
history.currentId = messageId;
|
||||
dispatch('change');
|
||||
|
||||
await tick();
|
||||
await updateChat();
|
||||
triggerScroll();
|
||||
};
|
||||
|
||||
const initHandler = async () => {
|
||||
@@ -182,7 +194,7 @@
|
||||
: `border-gray-50 dark:border-gray-850 border-dashed ${
|
||||
$mobile ? 'min-w-full' : 'min-w-80'
|
||||
}`} transition-all p-5 rounded-2xl"
|
||||
on:click={() => {
|
||||
on:click={async () => {
|
||||
if (messageId != _messageId) {
|
||||
let currentMessageId = _messageId;
|
||||
let messageChildrenIds = history.messages[currentMessageId].childrenIds;
|
||||
@@ -191,7 +203,10 @@
|
||||
messageChildrenIds = history.messages[currentMessageId].childrenIds;
|
||||
}
|
||||
history.currentId = currentMessageId;
|
||||
dispatch('change');
|
||||
|
||||
await tick();
|
||||
await updateChat();
|
||||
triggerScroll();
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -205,8 +220,12 @@
|
||||
siblings={groupedMessageIds[modelIdx].messageIds}
|
||||
showPreviousMessage={() => showPreviousMessage(modelIdx)}
|
||||
showNextMessage={() => showNextMessage(modelIdx)}
|
||||
{rateMessage}
|
||||
{updateChat}
|
||||
{editMessage}
|
||||
{saveMessage}
|
||||
{rateMessage}
|
||||
{actionMessage}
|
||||
{submitMessage}
|
||||
{continueResponse}
|
||||
regenerateResponse={async (message) => {
|
||||
regenerateResponse(message);
|
||||
@@ -214,18 +233,6 @@
|
||||
groupedMessageIdsIdx[modelIdx] =
|
||||
groupedMessageIds[modelIdx].messageIds.length - 1;
|
||||
}}
|
||||
on:submit={async (e) => {
|
||||
dispatch('submit', e.detail);
|
||||
}}
|
||||
on:action={async (e) => {
|
||||
dispatch('action', e.detail);
|
||||
}}
|
||||
on:update={async (e) => {
|
||||
dispatch('update', e.detail);
|
||||
}}
|
||||
on:save={async (e) => {
|
||||
dispatch('save', e.detail);
|
||||
}}
|
||||
{readOnly}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { settings } from '$lib/stores';
|
||||
import ProfileImageBase from './ProfileImageBase.svelte';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
export let className = 'size-8';
|
||||
export let src = '';
|
||||
export let src = `${WEBUI_BASE_URL}/static/favicon.png`;
|
||||
</script>
|
||||
|
||||
<div class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}`}>
|
||||
<ProfileImageBase {src} {className} />
|
||||
</div>
|
||||
<img
|
||||
crossorigin="anonymous"
|
||||
src={src === ''
|
||||
? `${WEBUI_BASE_URL}/static/favicon.png`
|
||||
: src.startsWith(WEBUI_BASE_URL) ||
|
||||
src.startsWith('https://www.gravatar.com/avatar/') ||
|
||||
src.startsWith('data:') ||
|
||||
src.startsWith('/')
|
||||
? src
|
||||
: `/user.png`}
|
||||
class=" {className} object-cover rounded-full -translate-y-[1px]"
|
||||
alt="profile"
|
||||
draggable="false"
|
||||
/>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
export let className = 'size-8';
|
||||
export let src = `${WEBUI_BASE_URL}/static/favicon.png`;
|
||||
</script>
|
||||
|
||||
<img
|
||||
crossorigin="anonymous"
|
||||
src={src === ''
|
||||
? `${WEBUI_BASE_URL}/static/favicon.png`
|
||||
: src.startsWith(WEBUI_BASE_URL) ||
|
||||
src.startsWith('https://www.gravatar.com/avatar/') ||
|
||||
src.startsWith('data:') ||
|
||||
src.startsWith('/')
|
||||
? src
|
||||
: `/user.png`}
|
||||
class=" {className} object-cover rounded-full -translate-y-[1px]"
|
||||
alt="profile"
|
||||
draggable="false"
|
||||
/>
|
||||
@@ -38,6 +38,7 @@
|
||||
let selectedReason = null;
|
||||
let comment = '';
|
||||
|
||||
let detailedRating = null;
|
||||
let selectedModel = null;
|
||||
|
||||
$: if (message?.annotation?.rating === 1) {
|
||||
@@ -56,6 +57,7 @@
|
||||
tags = (message?.annotation?.tags ?? []).map((tag) => ({
|
||||
name: tag
|
||||
}));
|
||||
detailedRating = message?.annotation?.details?.rating ?? null;
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
@@ -79,7 +81,10 @@
|
||||
dispatch('save', {
|
||||
reason: selectedReason,
|
||||
comment: comment,
|
||||
tags: tags.map((tag) => tag.name)
|
||||
tags: tags.map((tag) => tag.name),
|
||||
details: {
|
||||
rating: detailedRating
|
||||
}
|
||||
});
|
||||
|
||||
toast.success($i18n.t('Thanks for your feedback!'));
|
||||
@@ -100,7 +105,9 @@
|
||||
id="message-feedback-{message.id}"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class=" text-sm">{$i18n.t('Tell us more:')}</div>
|
||||
<div class="text-sm font-medium">{$i18n.t('How would you rate this response?')}</div>
|
||||
|
||||
<!-- <div class=" text-sm">{$i18n.t('Tell us more:')}</div> -->
|
||||
|
||||
<button
|
||||
on:click={() => {
|
||||
@@ -120,53 +127,89 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if reasons.length > 0}
|
||||
<div class="flex flex-wrap gap-1.5 text-sm mt-2.5">
|
||||
{#each reasons as reason}
|
||||
<button
|
||||
class="px-3 py-0.5 border border-gray-50 dark:border-gray-850 hover:bg-gray-100 dark:hover:bg-gray-850 {selectedReason ===
|
||||
reason
|
||||
? 'bg-gray-200 dark:bg-gray-800'
|
||||
: ''} transition rounded-lg"
|
||||
on:click={() => {
|
||||
selectedReason = reason;
|
||||
}}
|
||||
>
|
||||
{#if reason === 'accurate_information'}
|
||||
{$i18n.t('Accurate information')}
|
||||
{:else if reason === 'followed_instructions_perfectly'}
|
||||
{$i18n.t('Followed instructions perfectly')}
|
||||
{:else if reason === 'showcased_creativity'}
|
||||
{$i18n.t('Showcased creativity')}
|
||||
{:else if reason === 'positive_attitude'}
|
||||
{$i18n.t('Positive attitude')}
|
||||
{:else if reason === 'attention_to_detail'}
|
||||
{$i18n.t('Attention to detail')}
|
||||
{:else if reason === 'thorough_explanation'}
|
||||
{$i18n.t('Thorough explanation')}
|
||||
{:else if reason === 'dont_like_the_style'}
|
||||
{$i18n.t("Don't like the style")}
|
||||
{:else if reason === 'too_verbose'}
|
||||
{$i18n.t('Too verbose')}
|
||||
{:else if reason === 'not_helpful'}
|
||||
{$i18n.t('Not helpful')}
|
||||
{:else if reason === 'not_factually_correct'}
|
||||
{$i18n.t('Not factually correct')}
|
||||
{:else if reason === 'didnt_fully_follow_instructions'}
|
||||
{$i18n.t("Didn't fully follow instructions")}
|
||||
{:else if reason === 'refused_when_it_shouldnt_have'}
|
||||
{$i18n.t("Refused when it shouldn't have")}
|
||||
{:else if reason === 'being_lazy'}
|
||||
{$i18n.t('Being lazy')}
|
||||
{:else if reason === 'other'}
|
||||
{$i18n.t('Other')}
|
||||
{:else}
|
||||
{reason}
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
<div class="w-full flex justify-center">
|
||||
<div class=" relative w-fit">
|
||||
<div class="mt-1.5 w-fit flex gap-1 pb-5">
|
||||
<!-- 1-10 scale -->
|
||||
{#each Array.from({ length: 10 }).map((_, i) => i + 1) as rating}
|
||||
<button
|
||||
class="size-7 text-sm border border-gray-50 dark:border-gray-850 hover:bg-gray-50 dark:hover:bg-gray-850 {detailedRating ===
|
||||
rating
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ''} transition rounded-full disabled:cursor-not-allowed disabled:text-gray-500 disabled:bg-white disabled:dark:bg-gray-900"
|
||||
on:click={() => {
|
||||
detailedRating = rating;
|
||||
}}
|
||||
disabled={message?.annotation?.rating === -1 ? rating > 5 : rating < 6}
|
||||
>
|
||||
{rating}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-0 left-0 right-0 flex justify-between text-xs">
|
||||
<div>
|
||||
1 - {$i18n.t('Awful')}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
10 - {$i18n.t('Amazing')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{#if reasons.length > 0}
|
||||
<div class="text-sm mt-1.5 font-medium">{$i18n.t('Why?')}</div>
|
||||
|
||||
<div class="flex flex-wrap gap-1.5 text-sm mt-1.5">
|
||||
{#each reasons as reason}
|
||||
<button
|
||||
class="px-3 py-0.5 border border-gray-50 dark:border-gray-850 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedReason ===
|
||||
reason
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ''} transition rounded-xl"
|
||||
on:click={() => {
|
||||
selectedReason = reason;
|
||||
}}
|
||||
>
|
||||
{#if reason === 'accurate_information'}
|
||||
{$i18n.t('Accurate information')}
|
||||
{:else if reason === 'followed_instructions_perfectly'}
|
||||
{$i18n.t('Followed instructions perfectly')}
|
||||
{:else if reason === 'showcased_creativity'}
|
||||
{$i18n.t('Showcased creativity')}
|
||||
{:else if reason === 'positive_attitude'}
|
||||
{$i18n.t('Positive attitude')}
|
||||
{:else if reason === 'attention_to_detail'}
|
||||
{$i18n.t('Attention to detail')}
|
||||
{:else if reason === 'thorough_explanation'}
|
||||
{$i18n.t('Thorough explanation')}
|
||||
{:else if reason === 'dont_like_the_style'}
|
||||
{$i18n.t("Don't like the style")}
|
||||
{:else if reason === 'too_verbose'}
|
||||
{$i18n.t('Too verbose')}
|
||||
{:else if reason === 'not_helpful'}
|
||||
{$i18n.t('Not helpful')}
|
||||
{:else if reason === 'not_factually_correct'}
|
||||
{$i18n.t('Not factually correct')}
|
||||
{:else if reason === 'didnt_fully_follow_instructions'}
|
||||
{$i18n.t("Didn't fully follow instructions")}
|
||||
{:else if reason === 'refused_when_it_shouldnt_have'}
|
||||
{$i18n.t("Refused when it shouldn't have")}
|
||||
{:else if reason === 'being_lazy'}
|
||||
{$i18n.t('Being lazy')}
|
||||
{:else if reason === 'other'}
|
||||
{$i18n.t('Other')}
|
||||
{:else}
|
||||
{reason}
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<textarea
|
||||
@@ -195,7 +238,7 @@
|
||||
</div>
|
||||
|
||||
<button
|
||||
class=" bg-emerald-700 hover:bg-emerald-800 transition text-white text-sm font-medium rounded-xl px-3.5 py-1.5"
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||
on:click={() => {
|
||||
saveHandler();
|
||||
}}
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
import {
|
||||
copyToClipboard as _copyToClipboard,
|
||||
approximateToHumanReadable,
|
||||
extractParagraphsForAudio,
|
||||
extractSentencesForAudio,
|
||||
cleanText,
|
||||
getMessageContentParts,
|
||||
sanitizeResponseContent,
|
||||
createMessagesList
|
||||
@@ -33,7 +30,6 @@
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import WebSearchResults from './ResponseMessage/WebSearchResults.svelte';
|
||||
import Sparkles from '$lib/components/icons/Sparkles.svelte';
|
||||
import Markdown from './Markdown.svelte';
|
||||
import Error from './Error.svelte';
|
||||
import Citations from './Citations.svelte';
|
||||
import CodeExecutions from './CodeExecutions.svelte';
|
||||
@@ -68,7 +64,7 @@
|
||||
};
|
||||
done: boolean;
|
||||
error?: boolean | { content: string };
|
||||
citations?: string[];
|
||||
sources?: string[];
|
||||
code_executions?: {
|
||||
uuid: string;
|
||||
name: string;
|
||||
@@ -112,9 +108,13 @@
|
||||
export let showPreviousMessage: Function;
|
||||
export let showNextMessage: Function;
|
||||
|
||||
export let updateChat: Function;
|
||||
export let editMessage: Function;
|
||||
export let saveMessage: Function;
|
||||
export let rateMessage: Function;
|
||||
export let actionMessage: Function;
|
||||
|
||||
export let submitMessage: Function;
|
||||
export let continueResponse: Function;
|
||||
export let regenerateResponse: Function;
|
||||
|
||||
@@ -329,7 +329,10 @@
|
||||
url: `${image.url}`
|
||||
}));
|
||||
|
||||
dispatch('save', { ...message, files: files });
|
||||
saveMessage(message.id, {
|
||||
...message,
|
||||
files: files
|
||||
});
|
||||
}
|
||||
|
||||
generatingImage = false;
|
||||
@@ -337,19 +340,16 @@
|
||||
|
||||
let feedbackLoading = false;
|
||||
|
||||
const feedbackHandler = async (
|
||||
rating: number | null = null,
|
||||
annotation: object | null = null
|
||||
) => {
|
||||
const feedbackHandler = async (rating: number | null = null, details: object | null = null) => {
|
||||
feedbackLoading = true;
|
||||
console.log('Feedback', rating, annotation);
|
||||
console.log('Feedback', rating, details);
|
||||
|
||||
const updatedMessage = {
|
||||
...message,
|
||||
annotation: {
|
||||
...(message?.annotation ?? {}),
|
||||
...(rating !== null ? { rating: rating } : {}),
|
||||
...(annotation ? annotation : {})
|
||||
...(details ? details : {})
|
||||
}
|
||||
};
|
||||
|
||||
@@ -422,11 +422,11 @@
|
||||
}
|
||||
|
||||
console.log(updatedMessage);
|
||||
dispatch('save', updatedMessage);
|
||||
saveMessage(message.id, updatedMessage);
|
||||
|
||||
await tick();
|
||||
|
||||
if (!annotation) {
|
||||
if (!details) {
|
||||
showRateComment = true;
|
||||
|
||||
if (!updatedMessage.annotation?.tags) {
|
||||
@@ -443,7 +443,7 @@
|
||||
updatedMessage.annotation.tags = tags;
|
||||
feedbackItem.data.tags = tags;
|
||||
|
||||
dispatch('save', updatedMessage);
|
||||
saveMessage(message.id, updatedMessage);
|
||||
await updateFeedbackById(
|
||||
localStorage.token,
|
||||
updatedMessage.feedbackId,
|
||||
@@ -465,7 +465,7 @@
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
console.log('ResponseMessage mounted');
|
||||
// console.log('ResponseMessage mounted');
|
||||
|
||||
await tick();
|
||||
});
|
||||
@@ -477,10 +477,13 @@
|
||||
id="message-{message.id}"
|
||||
dir={$settings.chatDirection}
|
||||
>
|
||||
<ProfileImage
|
||||
src={model?.info?.meta?.profile_image_url ??
|
||||
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||
/>
|
||||
<div class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}`}>
|
||||
<ProfileImage
|
||||
src={model?.info?.meta?.profile_image_url ??
|
||||
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||
className={'size-8'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-auto w-0 pl-1">
|
||||
<Name>
|
||||
@@ -514,7 +517,7 @@
|
||||
{@const status = (
|
||||
message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]
|
||||
).at(-1)}
|
||||
<div class="status-description flex items-center gap-2 pt-0.5 pb-1">
|
||||
<div class="status-description flex items-center gap-2 py-0.5">
|
||||
{#if status?.done === false}
|
||||
<div class="">
|
||||
<Spinner className="size-4" />
|
||||
@@ -618,9 +621,18 @@
|
||||
<ContentRenderer
|
||||
id={message.id}
|
||||
content={message.content}
|
||||
sources={message.sources}
|
||||
floatingButtons={message?.done}
|
||||
save={!readOnly}
|
||||
{model}
|
||||
onSourceClick={(e) => {
|
||||
console.log(e);
|
||||
const sourceButton = document.getElementById(`source-${e}`);
|
||||
|
||||
if (sourceButton) {
|
||||
sourceButton.click();
|
||||
}
|
||||
}}
|
||||
on:update={(e) => {
|
||||
const { raw, oldContent, newContent } = e.detail;
|
||||
|
||||
@@ -628,33 +640,30 @@
|
||||
message.id
|
||||
].content.replace(raw, raw.replace(oldContent, newContent));
|
||||
|
||||
dispatch('update');
|
||||
updateChat();
|
||||
}}
|
||||
on:select={(e) => {
|
||||
const { type, content } = e.detail;
|
||||
|
||||
if (type === 'explain') {
|
||||
dispatch('submit', {
|
||||
parentId: message.id,
|
||||
prompt: `Explain this section to me in more detail\n\n\`\`\`\n${content}\n\`\`\``
|
||||
});
|
||||
submitMessage(
|
||||
message.id,
|
||||
`Explain this section to me in more detail\n\n\`\`\`\n${content}\n\`\`\``
|
||||
);
|
||||
} else if (type === 'ask') {
|
||||
const input = e.detail?.input ?? '';
|
||||
dispatch('submit', {
|
||||
parentId: message.id,
|
||||
prompt: `\`\`\`\n${content}\n\`\`\`\n${input}`
|
||||
});
|
||||
submitMessage(message.id, `\`\`\`\n${content}\n\`\`\`\n${input}`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if message.error}
|
||||
{#if message?.error}
|
||||
<Error content={message?.error?.content ?? message.content} />
|
||||
{/if}
|
||||
|
||||
{#if message.citations}
|
||||
<Citations citations={message.citations} />
|
||||
{#if (message?.sources || message?.citations) && (model?.info?.meta?.capabilities?.citations ?? true)}
|
||||
<Citations sources={message?.sources ?? message?.citations} />
|
||||
{/if}
|
||||
|
||||
{#if message.code_executions}
|
||||
@@ -726,7 +735,7 @@
|
||||
|
||||
{#if message.done}
|
||||
{#if !readOnly}
|
||||
{#if $user.role === 'user' ? ($config?.permissions?.chat?.editing ?? true) : true}
|
||||
{#if $user.role === 'user' ? ($user?.permissions?.chat?.edit ?? true) : true}
|
||||
<Tooltip content={$i18n.t('Edit')} placement="bottom">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
@@ -923,82 +932,45 @@
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if message.info}
|
||||
{#if message.usage}
|
||||
<Tooltip
|
||||
content={message.info.openai
|
||||
? message.info.usage
|
||||
? `<pre>${sanitizeResponseContent(
|
||||
JSON.stringify(message.info.usage, null, 2)
|
||||
.replace(/"([^(")"]+)":/g, '$1:')
|
||||
.slice(1, -1)
|
||||
.split('\n')
|
||||
.map((line) => line.slice(2))
|
||||
.map((line) => (line.endsWith(',') ? line.slice(0, -1) : line))
|
||||
.join('\n')
|
||||
)}</pre>`
|
||||
: `prompt_tokens: ${message.info.prompt_tokens ?? 'N/A'}<br/>
|
||||
completion_tokens: ${message.info.completion_tokens ?? 'N/A'}<br/>
|
||||
total_tokens: ${message.info.total_tokens ?? 'N/A'}`
|
||||
: `response_token/s: ${
|
||||
`${
|
||||
Math.round(
|
||||
((message.info.eval_count ?? 0) /
|
||||
((message.info.eval_duration ?? 0) / 1000000000)) *
|
||||
100
|
||||
) / 100
|
||||
} tokens` ?? 'N/A'
|
||||
}<br/>
|
||||
prompt_token/s: ${
|
||||
Math.round(
|
||||
((message.info.prompt_eval_count ?? 0) /
|
||||
((message.info.prompt_eval_duration ?? 0) / 1000000000)) *
|
||||
100
|
||||
) / 100 ?? 'N/A'
|
||||
} tokens<br/>
|
||||
total_duration: ${
|
||||
Math.round(((message.info.total_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A'
|
||||
}ms<br/>
|
||||
load_duration: ${
|
||||
Math.round(((message.info.load_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A'
|
||||
}ms<br/>
|
||||
prompt_eval_count: ${message.info.prompt_eval_count ?? 'N/A'}<br/>
|
||||
prompt_eval_duration: ${
|
||||
Math.round(((message.info.prompt_eval_duration ?? 0) / 1000000) * 100) / 100 ??
|
||||
'N/A'
|
||||
}ms<br/>
|
||||
eval_count: ${message.info.eval_count ?? 'N/A'}<br/>
|
||||
eval_duration: ${
|
||||
Math.round(((message.info.eval_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A'
|
||||
}ms<br/>
|
||||
approximate_total: ${approximateToHumanReadable(message.info.total_duration ?? 0)}`}
|
||||
placement="top"
|
||||
content={message.usage
|
||||
? `<pre>${sanitizeResponseContent(
|
||||
JSON.stringify(message.usage, null, 2)
|
||||
.replace(/"([^(")"]+)":/g, '$1:')
|
||||
.slice(1, -1)
|
||||
.split('\n')
|
||||
.map((line) => line.slice(2))
|
||||
.map((line) => (line.endsWith(',') ? line.slice(0, -1) : line))
|
||||
.join('\n')
|
||||
)}</pre>`
|
||||
: ''}
|
||||
placement="bottom"
|
||||
>
|
||||
<Tooltip content={$i18n.t('Generation Info')} placement="bottom">
|
||||
<button
|
||||
class=" {isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
|
||||
on:click={() => {
|
||||
console.log(message);
|
||||
}}
|
||||
id="info-{message.id}"
|
||||
<button
|
||||
class=" {isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
|
||||
on:click={() => {
|
||||
console.log(message);
|
||||
}}
|
||||
id="info-{message.id}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2.3"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2.3"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
@@ -1016,21 +988,6 @@
|
||||
disabled={feedbackLoading}
|
||||
on:click={async () => {
|
||||
await feedbackHandler(1);
|
||||
|
||||
(model?.actions ?? [])
|
||||
.filter((action) => action?.__webui__ ?? false)
|
||||
.forEach((action) => {
|
||||
dispatch('action', {
|
||||
id: action.id,
|
||||
event: {
|
||||
id: 'good-response',
|
||||
data: {
|
||||
messageId: message.id
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
window.setTimeout(() => {
|
||||
document
|
||||
.getElementById(`message-feedback-${message.id}`)
|
||||
@@ -1067,21 +1024,6 @@
|
||||
disabled={feedbackLoading}
|
||||
on:click={async () => {
|
||||
await feedbackHandler(-1);
|
||||
|
||||
(model?.actions ?? [])
|
||||
.filter((action) => action?.__webui__ ?? false)
|
||||
.forEach((action) => {
|
||||
dispatch('action', {
|
||||
id: action.id,
|
||||
event: {
|
||||
id: 'bad-response',
|
||||
data: {
|
||||
messageId: message.id
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
window.setTimeout(() => {
|
||||
document
|
||||
.getElementById(`message-feedback-${message.id}`)
|
||||
@@ -1117,20 +1059,6 @@
|
||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||
on:click={() => {
|
||||
continueResponse();
|
||||
|
||||
(model?.actions ?? [])
|
||||
.filter((action) => action?.__webui__ ?? false)
|
||||
.forEach((action) => {
|
||||
dispatch('action', {
|
||||
id: action.id,
|
||||
event: {
|
||||
id: 'continue-response',
|
||||
data: {
|
||||
messageId: message.id
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
@@ -1154,50 +1082,50 @@
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Tooltip content={$i18n.t('Regenerate')} placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||
on:click={() => {
|
||||
showRateComment = false;
|
||||
regenerateResponse(message);
|
||||
<Tooltip content={$i18n.t('Regenerate')} placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||
on:click={() => {
|
||||
showRateComment = false;
|
||||
regenerateResponse(message);
|
||||
|
||||
(model?.actions ?? [])
|
||||
.filter((action) => action?.__webui__ ?? false)
|
||||
.forEach((action) => {
|
||||
dispatch('action', {
|
||||
id: action.id,
|
||||
event: {
|
||||
id: 'regenerate-response',
|
||||
data: {
|
||||
messageId: message.id
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}}
|
||||
(model?.actions ?? []).forEach((action) => {
|
||||
dispatch('action', {
|
||||
id: action.id,
|
||||
event: {
|
||||
id: 'regenerate-response',
|
||||
data: {
|
||||
messageId: message.id
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2.3"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2.3"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{#each (model?.actions ?? []).filter((action) => !(action?.__webui__ ?? false)) as action}
|
||||
{#if isLastMessage}
|
||||
{#each model?.actions ?? [] as action}
|
||||
<Tooltip content={action.name} placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
@@ -1205,7 +1133,7 @@
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||
on:click={() => {
|
||||
dispatch('action', action.id);
|
||||
actionMessage(action.id, message);
|
||||
}}
|
||||
>
|
||||
{#if action.icon_url}
|
||||
@@ -1235,26 +1163,8 @@
|
||||
bind:show={showRateComment}
|
||||
on:save={async (e) => {
|
||||
await feedbackHandler(null, {
|
||||
tags: e.detail.tags,
|
||||
comment: e.detail.comment,
|
||||
reason: e.detail.reason
|
||||
...e.detail
|
||||
});
|
||||
|
||||
(model?.actions ?? [])
|
||||
.filter((action) => action?.__webui__ ?? false)
|
||||
.forEach((action) => {
|
||||
dispatch('action', {
|
||||
id: action.id,
|
||||
event: {
|
||||
id: 'rate-comment',
|
||||
data: {
|
||||
messageId: message.id,
|
||||
comment: e.detail.comment,
|
||||
reason: e.detail.reason
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
<script lang="ts">
|
||||
import dayjs from 'dayjs';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { tick, createEventDispatcher, getContext, onMount } from 'svelte';
|
||||
import { tick, getContext, onMount } from 'svelte';
|
||||
|
||||
import { models, settings } from '$lib/stores';
|
||||
import { user as _user } from '$lib/stores';
|
||||
import {
|
||||
copyToClipboard as _copyToClipboard,
|
||||
processResponseContent,
|
||||
replaceTokens
|
||||
} from '$lib/utils';
|
||||
import { copyToClipboard as _copyToClipboard } from '$lib/utils';
|
||||
|
||||
import Name from './Name.svelte';
|
||||
import ProfileImage from './ProfileImage.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import FileItem from '$lib/components/common/FileItem.svelte';
|
||||
import Markdown from './Markdown.svelte';
|
||||
import Image from '$lib/components/common/Image.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
export let user;
|
||||
|
||||
export let history;
|
||||
@@ -31,6 +27,7 @@
|
||||
export let showNextMessage: Function;
|
||||
|
||||
export let editMessage: Function;
|
||||
export let deleteMessage: Function;
|
||||
|
||||
export let isFirstMessage: boolean;
|
||||
export let readOnly: boolean;
|
||||
@@ -78,21 +75,25 @@
|
||||
};
|
||||
|
||||
const deleteMessageHandler = async () => {
|
||||
dispatch('delete', message.id);
|
||||
deleteMessage(message.id);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
console.log('UserMessage mounted');
|
||||
// console.log('UserMessage mounted');
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class=" flex w-full user-message" dir={$settings.chatDirection} id="message-{message.id}">
|
||||
{#if !($settings?.chatBubble ?? true)}
|
||||
<ProfileImage
|
||||
src={message.user
|
||||
? ($models.find((m) => m.id === message.user)?.info?.meta?.profile_image_url ?? '/user.png')
|
||||
: (user?.profile_image_url ?? '/user.png')}
|
||||
/>
|
||||
<div class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}`}>
|
||||
<ProfileImage
|
||||
src={message.user
|
||||
? ($models.find((m) => m.id === message.user)?.info?.meta?.profile_image_url ??
|
||||
'/user.png')
|
||||
: (user?.profile_image_url ?? '/user.png')}
|
||||
className={'size-8'}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-auto w-0 max-w-full pl-1">
|
||||
{#if !($settings?.chatBubble ?? true)}
|
||||
@@ -124,7 +125,7 @@
|
||||
{#each message.files as file}
|
||||
<div class={($settings?.chatBubble ?? true) ? 'self-end' : ''}>
|
||||
{#if file.type === 'image'}
|
||||
<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
|
||||
<Image src={file.url} imageClassName=" max-h-96 rounded-lg" />
|
||||
{:else}
|
||||
<FileItem
|
||||
item={file}
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
import Selector from './ModelSelector/Selector.svelte';
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
|
||||
import { setDefaultModels } from '$lib/apis/configs';
|
||||
import { updateUserSettings } from '$lib/apis/users';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let selectedModels = [''];
|
||||
@@ -48,7 +46,7 @@
|
||||
model: model
|
||||
}))}
|
||||
showTemporaryChatControl={$user.role === 'user'
|
||||
? ($config?.permissions?.chat?.temporary ?? true)
|
||||
? ($user?.permissions?.chat?.temporary ?? true)
|
||||
: true}
|
||||
bind:value={selectedModel}
|
||||
/>
|
||||
|
||||
@@ -55,20 +55,18 @@
|
||||
let selectedModelIdx = 0;
|
||||
|
||||
const fuse = new Fuse(
|
||||
items
|
||||
.filter((item) => !item.model?.info?.meta?.hidden)
|
||||
.map((item) => {
|
||||
const _item = {
|
||||
...item,
|
||||
modelName: item.model?.name,
|
||||
tags: item.model?.info?.meta?.tags?.map((tag) => tag.name).join(' '),
|
||||
desc: item.model?.info?.meta?.description
|
||||
};
|
||||
return _item;
|
||||
}),
|
||||
items.map((item) => {
|
||||
const _item = {
|
||||
...item,
|
||||
modelName: item.model?.name,
|
||||
tags: item.model?.info?.meta?.tags?.map((tag) => tag.name).join(' '),
|
||||
desc: item.model?.info?.meta?.description
|
||||
};
|
||||
return _item;
|
||||
}),
|
||||
{
|
||||
keys: ['value', 'tags', 'modelName'],
|
||||
threshold: 0.3
|
||||
threshold: 0.4
|
||||
}
|
||||
);
|
||||
|
||||
@@ -76,7 +74,7 @@
|
||||
? fuse.search(searchValue).map((e) => {
|
||||
return e.item;
|
||||
})
|
||||
: items.filter((item) => !item.model?.info?.meta?.hidden);
|
||||
: items;
|
||||
|
||||
const pullModelHandler = async () => {
|
||||
const sanitizedModelTag = searchValue.trim().replace(/^ollama\s+(run|pull)\s+/, '');
|
||||
@@ -322,12 +320,17 @@
|
||||
<div class="flex items-center min-w-fit">
|
||||
<div class="line-clamp-1">
|
||||
<div class="flex items-center min-w-fit">
|
||||
<img
|
||||
src={item.model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
|
||||
alt="Model"
|
||||
class="rounded-full size-5 flex items-center mr-2"
|
||||
/>
|
||||
{item.label}
|
||||
<Tooltip
|
||||
content={$user?.role === 'admin' ? (item?.value ?? '') : ''}
|
||||
placement="top-start"
|
||||
>
|
||||
<img
|
||||
src={item.model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
|
||||
alt="Model"
|
||||
class="rounded-full size-5 flex items-center mr-2"
|
||||
/>
|
||||
{item.label}
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{#if item.model.owned_by === 'ollama' && (item.model.ollama?.details?.parameter_size ?? '') !== ''}
|
||||
@@ -438,14 +441,23 @@
|
||||
{/each}
|
||||
|
||||
{#if !(searchValue.trim() in $MODEL_DOWNLOAD_POOL) && searchValue && ollamaVersion && $user.role === 'admin'}
|
||||
<button
|
||||
class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
|
||||
on:click={() => {
|
||||
pullModelHandler();
|
||||
}}
|
||||
<Tooltip
|
||||
content={$i18n.t(`Pull "{{searchValue}}" from Ollama.com`, {
|
||||
searchValue: searchValue
|
||||
})}
|
||||
placement="top-start"
|
||||
>
|
||||
{$i18n.t(`Pull "{{searchValue}}" from Ollama.com`, { searchValue: searchValue })}
|
||||
</button>
|
||||
<button
|
||||
class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
|
||||
on:click={() => {
|
||||
pullModelHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" truncate">
|
||||
{$i18n.t(`Pull "{{searchValue}}" from Ollama.com`, { searchValue: searchValue })}
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#each Object.keys($MODEL_DOWNLOAD_POOL) as model}
|
||||
@@ -574,14 +586,3 @@
|
||||
</slot>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<style>
|
||||
.scrollbar-hidden:active::-webkit-scrollbar-thumb,
|
||||
.scrollbar-hidden:focus::-webkit-scrollbar-thumb,
|
||||
.scrollbar-hidden:hover::-webkit-scrollbar-thumb {
|
||||
visibility: visible;
|
||||
}
|
||||
.scrollbar-hidden::-webkit-scrollbar-thumb {
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
|
||||
|
||||
import ProfileImageBase from '../Messages/ProfileImageBase.svelte';
|
||||
import ProfileImage from '../Messages/ProfileImage.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Heart from '$lib/components/icons/Heart.svelte';
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
>
|
||||
{#if data.message.role === 'user'}
|
||||
<div class="flex w-full">
|
||||
<ProfileImageBase
|
||||
<ProfileImage
|
||||
src={data.user?.profile_image_url ?? '/user.png'}
|
||||
className={'size-5 -translate-y-[1px]'}
|
||||
/>
|
||||
@@ -40,7 +40,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex w-full">
|
||||
<ProfileImageBase
|
||||
<ProfileImage
|
||||
src={data?.model?.info?.meta?.profile_image_url ?? ''}
|
||||
className={'size-5 -translate-y-[1px]'}
|
||||
/>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
export let prompt = '';
|
||||
export let files = [];
|
||||
export let availableToolIds = [];
|
||||
|
||||
export let selectedToolIds = [];
|
||||
export let webSearchEnabled = false;
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
await tick();
|
||||
};
|
||||
|
||||
let mounted = false;
|
||||
let selectedModelIdx = 0;
|
||||
|
||||
$: if (selectedModels.length > 0) {
|
||||
@@ -84,148 +83,143 @@
|
||||
|
||||
$: models = selectedModels.map((id) => $_models.find((m) => m.id === id));
|
||||
|
||||
onMount(() => {
|
||||
mounted = true;
|
||||
});
|
||||
onMount(() => {});
|
||||
</script>
|
||||
|
||||
{#key mounted}
|
||||
<div class="m-auto w-full max-w-6xl px-2 xl:px-20 translate-y-6 py-24 text-center">
|
||||
{#if $temporaryChatEnabled}
|
||||
<Tooltip
|
||||
content="This chat won't appear in history and your messages will not be saved."
|
||||
className="w-full flex justify-center mb-0.5"
|
||||
placement="top"
|
||||
>
|
||||
<div class="flex items-center gap-2 text-gray-500 font-medium text-lg my-2 w-fit">
|
||||
<EyeSlash strokeWidth="2.5" className="size-5" /> Temporary Chat
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="w-full text-3xl text-gray-800 dark:text-gray-100 font-medium text-center flex items-center gap-4 font-primary"
|
||||
<div class="m-auto w-full max-w-6xl px-2 xl:px-20 translate-y-6 py-24 text-center">
|
||||
{#if $temporaryChatEnabled}
|
||||
<Tooltip
|
||||
content="This chat won't appear in history and your messages will not be saved."
|
||||
className="w-full flex justify-center mb-0.5"
|
||||
placement="top"
|
||||
>
|
||||
<div class="w-full flex flex-col justify-center items-center">
|
||||
<div class="flex flex-row justify-center gap-3 sm:gap-3.5 w-fit px-5">
|
||||
<div class="flex flex-shrink-0 justify-center">
|
||||
<div class="flex -space-x-4 mb-0.5" in:fade={{ duration: 100 }}>
|
||||
{#each models as model, modelIdx}
|
||||
<Tooltip
|
||||
content={(models[modelIdx]?.info?.meta?.tags ?? [])
|
||||
.map((tag) => tag.name.toUpperCase())
|
||||
.join(', ')}
|
||||
placement="top"
|
||||
>
|
||||
<button
|
||||
on:click={() => {
|
||||
selectedModelIdx = modelIdx;
|
||||
}}
|
||||
>
|
||||
<img
|
||||
crossorigin="anonymous"
|
||||
src={model?.info?.meta?.profile_image_url ??
|
||||
($i18n.language === 'dg-DG'
|
||||
? `/doge.png`
|
||||
: `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||
class=" size-9 sm:size-10 rounded-full border-[1px] border-gray-200 dark:border-none"
|
||||
alt="logo"
|
||||
draggable="false"
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-gray-500 font-medium text-lg my-2 w-fit">
|
||||
<EyeSlash strokeWidth="2.5" className="size-5" /> Temporary Chat
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<div class=" capitalize text-3xl sm:text-4xl line-clamp-1" in:fade={{ duration: 100 }}>
|
||||
{#if models[selectedModelIdx]?.name}
|
||||
{models[selectedModelIdx]?.name}
|
||||
{:else}
|
||||
{$i18n.t('Hello, {{name}}', { name: $user.name })}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-1 mb-2">
|
||||
<div in:fade={{ duration: 100, delay: 50 }}>
|
||||
{#if models[selectedModelIdx]?.info?.meta?.description ?? null}
|
||||
<div
|
||||
class="w-full text-3xl text-gray-800 dark:text-gray-100 font-medium text-center flex items-center gap-4 font-primary"
|
||||
>
|
||||
<div class="w-full flex flex-col justify-center items-center">
|
||||
<div class="flex flex-row justify-center gap-3 sm:gap-3.5 w-fit px-5">
|
||||
<div class="flex flex-shrink-0 justify-center">
|
||||
<div class="flex -space-x-4 mb-0.5" in:fade={{ duration: 100 }}>
|
||||
{#each models as model, modelIdx}
|
||||
<Tooltip
|
||||
className=" w-fit"
|
||||
content={marked.parse(
|
||||
sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description ?? '')
|
||||
)}
|
||||
content={(models[modelIdx]?.info?.meta?.tags ?? [])
|
||||
.map((tag) => tag.name.toUpperCase())
|
||||
.join(', ')}
|
||||
placement="top"
|
||||
>
|
||||
<div
|
||||
class="mt-0.5 px-2 text-sm font-normal text-gray-500 dark:text-gray-400 line-clamp-2 max-w-xl markdown"
|
||||
<button
|
||||
on:click={() => {
|
||||
selectedModelIdx = modelIdx;
|
||||
}}
|
||||
>
|
||||
{@html marked.parse(
|
||||
sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description)
|
||||
)}
|
||||
</div>
|
||||
<img
|
||||
crossorigin="anonymous"
|
||||
src={model?.info?.meta?.profile_image_url ??
|
||||
($i18n.language === 'dg-DG'
|
||||
? `/doge.png`
|
||||
: `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||
class=" size-9 sm:size-10 rounded-full border-[1px] border-gray-200 dark:border-none"
|
||||
alt="logo"
|
||||
draggable="false"
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{#if models[selectedModelIdx]?.info?.meta?.user}
|
||||
<div class="mt-0.5 text-sm font-normal text-gray-400 dark:text-gray-500">
|
||||
By
|
||||
{#if models[selectedModelIdx]?.info?.meta?.user.community}
|
||||
<a
|
||||
href="https://openwebui.com/m/{models[selectedModelIdx]?.info?.meta?.user
|
||||
.username}"
|
||||
>{models[selectedModelIdx]?.info?.meta?.user.name
|
||||
? models[selectedModelIdx]?.info?.meta?.user.name
|
||||
: `@${models[selectedModelIdx]?.info?.meta?.user.username}`}</a
|
||||
>
|
||||
{:else}
|
||||
{models[selectedModelIdx]?.info?.meta?.user.name}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-base font-normal xl:translate-x-6 md:max-w-3xl w-full py-3 {atSelectedModel
|
||||
? 'mt-2'
|
||||
: ''}"
|
||||
>
|
||||
<MessageInput
|
||||
{history}
|
||||
{selectedModels}
|
||||
bind:files
|
||||
bind:prompt
|
||||
bind:autoScroll
|
||||
bind:selectedToolIds
|
||||
bind:webSearchEnabled
|
||||
bind:atSelectedModel
|
||||
{availableToolIds}
|
||||
{transparentBackground}
|
||||
{stopResponse}
|
||||
{createMessagePair}
|
||||
placeholder={$i18n.t('How can I help you today?')}
|
||||
on:upload={(e) => {
|
||||
dispatch('upload', e.detail);
|
||||
}}
|
||||
on:submit={(e) => {
|
||||
dispatch('submit', e.detail);
|
||||
}}
|
||||
/>
|
||||
<div class=" text-3xl sm:text-4xl line-clamp-1" in:fade={{ duration: 100 }}>
|
||||
{#if models[selectedModelIdx]?.name}
|
||||
{models[selectedModelIdx]?.name}
|
||||
{:else}
|
||||
{$i18n.t('Hello, {{name}}', { name: $user.name })}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-auto max-w-2xl font-primary" in:fade={{ duration: 200, delay: 200 }}>
|
||||
<div class="mx-5">
|
||||
<Suggestions
|
||||
suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
|
||||
$config?.default_prompt_suggestions ??
|
||||
[]}
|
||||
on:select={(e) => {
|
||||
selectSuggestionPrompt(e.detail);
|
||||
|
||||
<div class="flex mt-1 mb-2">
|
||||
<div in:fade={{ duration: 100, delay: 50 }}>
|
||||
{#if models[selectedModelIdx]?.info?.meta?.description ?? null}
|
||||
<Tooltip
|
||||
className=" w-fit"
|
||||
content={marked.parse(
|
||||
sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description ?? '')
|
||||
)}
|
||||
placement="top"
|
||||
>
|
||||
<div
|
||||
class="mt-0.5 px-2 text-sm font-normal text-gray-500 dark:text-gray-400 line-clamp-2 max-w-xl markdown"
|
||||
>
|
||||
{@html marked.parse(
|
||||
sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description)
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{#if models[selectedModelIdx]?.info?.meta?.user}
|
||||
<div class="mt-0.5 text-sm font-normal text-gray-400 dark:text-gray-500">
|
||||
By
|
||||
{#if models[selectedModelIdx]?.info?.meta?.user.community}
|
||||
<a
|
||||
href="https://openwebui.com/m/{models[selectedModelIdx]?.info?.meta?.user
|
||||
.username}"
|
||||
>{models[selectedModelIdx]?.info?.meta?.user.name
|
||||
? models[selectedModelIdx]?.info?.meta?.user.name
|
||||
: `@${models[selectedModelIdx]?.info?.meta?.user.username}`}</a
|
||||
>
|
||||
{:else}
|
||||
{models[selectedModelIdx]?.info?.meta?.user.name}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-base font-normal xl:translate-x-6 md:max-w-3xl w-full py-3 {atSelectedModel
|
||||
? 'mt-2'
|
||||
: ''}"
|
||||
>
|
||||
<MessageInput
|
||||
{history}
|
||||
{selectedModels}
|
||||
bind:files
|
||||
bind:prompt
|
||||
bind:autoScroll
|
||||
bind:selectedToolIds
|
||||
bind:webSearchEnabled
|
||||
bind:atSelectedModel
|
||||
{transparentBackground}
|
||||
{stopResponse}
|
||||
{createMessagePair}
|
||||
placeholder={$i18n.t('How can I help you today?')}
|
||||
on:upload={(e) => {
|
||||
dispatch('upload', e.detail);
|
||||
}}
|
||||
on:submit={(e) => {
|
||||
dispatch('submit', e.detail);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
<div class="mx-auto max-w-2xl font-primary" in:fade={{ duration: 200, delay: 200 }}>
|
||||
<div class="mx-5">
|
||||
<Suggestions
|
||||
suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
|
||||
$config?.default_prompt_suggestions ??
|
||||
[]}
|
||||
on:select={(e) => {
|
||||
selectSuggestionPrompt(e.detail);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full justify-between space-y-3 text-sm mb-6">
|
||||
<div class=" space-y-3">
|
||||
<div class=" space-y-3 overflow-y-scroll max-h-[28rem] lg:max-h-full">
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium flex space-x-2 items-center">
|
||||
<div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
|
||||
import { user } from '$lib/stores';
|
||||
import { user, config } from '$lib/stores';
|
||||
import { updateUserProfile, createAPIKey, getAPIKey } from '$lib/apis/auths';
|
||||
|
||||
import UpdatePassword from './Account/UpdatePassword.svelte';
|
||||
@@ -26,7 +26,6 @@
|
||||
|
||||
let APIKey = '';
|
||||
let APIKeyCopied = false;
|
||||
|
||||
let profileImageInputElement: HTMLInputElement;
|
||||
|
||||
const submitHandler = async () => {
|
||||
@@ -70,7 +69,7 @@
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full justify-between text-sm">
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem]">
|
||||
<div class=" space-y-3 overflow-y-scroll max-h-[28rem] lg:max-h-full">
|
||||
<input
|
||||
id="profile-image-input"
|
||||
bind:this={profileImageInputElement}
|
||||
@@ -301,96 +300,97 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="justify-between w-full">
|
||||
<div class="flex justify-between w-full">
|
||||
<div class="self-center text-xs font-medium">{$i18n.t('API Key')}</div>
|
||||
</div>
|
||||
{#if $config?.features?.enable_api_key ?? true}
|
||||
<div class="justify-between w-full">
|
||||
<div class="flex justify-between w-full">
|
||||
<div class="self-center text-xs font-medium">{$i18n.t('API Key')}</div>
|
||||
</div>
|
||||
<div class="flex mt-2">
|
||||
{#if APIKey}
|
||||
<SensitiveInput value={APIKey} readOnly={true} />
|
||||
|
||||
<div class="flex mt-2">
|
||||
{#if APIKey}
|
||||
<SensitiveInput value={APIKey} readOnly={true} />
|
||||
|
||||
<button
|
||||
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg"
|
||||
on:click={() => {
|
||||
copyToClipboard(APIKey);
|
||||
APIKeyCopied = true;
|
||||
setTimeout(() => {
|
||||
APIKeyCopied = false;
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
{#if APIKeyCopied}
|
||||
<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="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<Tooltip content={$i18n.t('Create new key')}>
|
||||
<button
|
||||
class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg"
|
||||
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg"
|
||||
on:click={() => {
|
||||
copyToClipboard(APIKey);
|
||||
APIKeyCopied = true;
|
||||
setTimeout(() => {
|
||||
APIKeyCopied = false;
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
{#if APIKeyCopied}
|
||||
<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="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<Tooltip content={$i18n.t('Create new key')}>
|
||||
<button
|
||||
class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg"
|
||||
on:click={() => {
|
||||
createAPIKeyHandler();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<button
|
||||
class="flex gap-1.5 items-center font-medium px-3.5 py-1.5 rounded-lg bg-gray-100/70 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-850 transition"
|
||||
on:click={() => {
|
||||
createAPIKeyHandler();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<button
|
||||
class="flex gap-1.5 items-center font-medium px-3.5 py-1.5 rounded-lg bg-gray-100/70 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-850 transition"
|
||||
on:click={() => {
|
||||
createAPIKeyHandler();
|
||||
}}
|
||||
>
|
||||
<Plus strokeWidth="2" className=" size-3.5" />
|
||||
<Plus strokeWidth="2" className=" size-3.5" />
|
||||
|
||||
{$i18n.t('Create new secret key')}</button
|
||||
>
|
||||
{/if}
|
||||
{$i18n.t('Create new secret key')}</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -98,7 +98,7 @@
|
||||
dispatch('save');
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem]">
|
||||
<div class=" space-y-3 overflow-y-scroll max-h-[28rem] lg:max-h-full">
|
||||
<div>
|
||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
|
||||
|
||||
|
||||
@@ -97,8 +97,8 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full justify-between space-y-3 text-sm max-h-[22rem]">
|
||||
<div class=" space-y-2">
|
||||
<div class="flex flex-col h-full justify-between space-y-3 text-sm">
|
||||
<div class=" space-y-2 overflow-y-scroll max-h-[28rem] lg:max-h-full">
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="chat-import-input"
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
mirostat_tau: null,
|
||||
top_k: null,
|
||||
top_p: null,
|
||||
min_p: null,
|
||||
stop: null,
|
||||
tfs_z: null,
|
||||
num_ctx: null,
|
||||
@@ -156,7 +157,7 @@
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full justify-between text-sm">
|
||||
<div class=" pr-1.5 overflow-y-scroll max-h-[25rem]">
|
||||
<div class=" overflow-y-scroll max-h-[28rem] lg:max-h-full">
|
||||
<div class="">
|
||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Settings')}</div>
|
||||
|
||||
@@ -340,6 +341,7 @@
|
||||
mirostat_tau: params.mirostat_tau !== null ? params.mirostat_tau : undefined,
|
||||
top_k: params.top_k !== null ? params.top_k : undefined,
|
||||
top_p: params.top_p !== null ? params.top_p : undefined,
|
||||
min_p: params.min_p !== null ? params.min_p : undefined,
|
||||
tfs_z: params.tfs_z !== null ? params.tfs_z : undefined,
|
||||
num_ctx: params.num_ctx !== null ? params.num_ctx : undefined,
|
||||
num_batch: params.num_batch !== null ? params.num_batch : undefined,
|
||||
|
||||
@@ -31,11 +31,15 @@
|
||||
let defaultModelId = '';
|
||||
let showUsername = false;
|
||||
let richTextInput = true;
|
||||
let largeTextAsFile = false;
|
||||
|
||||
let landingPageMode = '';
|
||||
let chatBubble = true;
|
||||
let chatDirection: 'LTR' | 'RTL' = 'LTR';
|
||||
|
||||
// Admin - Show Update Available Toast
|
||||
let showUpdateToast = true;
|
||||
let showChangelog = true;
|
||||
|
||||
let showEmojiInCall = false;
|
||||
let voiceInterruption = false;
|
||||
@@ -71,6 +75,11 @@
|
||||
saveSettings({ showUpdateToast: showUpdateToast });
|
||||
};
|
||||
|
||||
const toggleShowChangelog = async () => {
|
||||
showChangelog = !showChangelog;
|
||||
saveSettings({ showChangelog: showChangelog });
|
||||
};
|
||||
|
||||
const toggleShowUsername = async () => {
|
||||
showUsername = !showUsername;
|
||||
saveSettings({ showUsername: showUsername });
|
||||
@@ -131,6 +140,11 @@
|
||||
saveSettings({ richTextInput });
|
||||
};
|
||||
|
||||
const toggleLargeTextAsFile = async () => {
|
||||
largeTextAsFile = !largeTextAsFile;
|
||||
saveSettings({ largeTextAsFile });
|
||||
};
|
||||
|
||||
const toggleResponseAutoCopy = async () => {
|
||||
const permission = await navigator.clipboard
|
||||
.readText()
|
||||
@@ -174,11 +188,14 @@
|
||||
|
||||
showUsername = $settings.showUsername ?? false;
|
||||
showUpdateToast = $settings.showUpdateToast ?? true;
|
||||
showChangelog = $settings.showChangelog ?? true;
|
||||
|
||||
showEmojiInCall = $settings.showEmojiInCall ?? false;
|
||||
voiceInterruption = $settings.voiceInterruption ?? false;
|
||||
|
||||
richTextInput = $settings.richTextInput ?? true;
|
||||
largeTextAsFile = $settings.largeTextAsFile ?? false;
|
||||
|
||||
landingPageMode = $settings.landingPageMode ?? '';
|
||||
chatBubble = $settings.chatBubble ?? true;
|
||||
widescreenMode = $settings.widescreenMode ?? false;
|
||||
@@ -233,29 +250,7 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem] scrollbar-hidden">
|
||||
<div class=" space-y-1 mb-3">
|
||||
<div class="mb-2">
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<div class=" text-sm font-medium">{$i18n.t('Default Model')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 mr-2">
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={defaultModelId}
|
||||
placeholder="Select a model"
|
||||
>
|
||||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
||||
{#each $models.filter((model) => model.id) as model}
|
||||
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div class=" space-y-3 overflow-y-scroll max-h-[28rem] lg:max-h-full">
|
||||
<div>
|
||||
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('UI')}</div>
|
||||
|
||||
@@ -383,101 +378,30 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
{$i18n.t(`Show "What's New" modal on login`)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
toggleShowChangelog();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if showChangelog === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
{$i18n.t('Fluidly stream large external response chunks')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
toggleSplitLargeChunks();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if splitLargeChunks === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
{$i18n.t('Scroll to bottom when switching between branches')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
togglesScrollOnBranchChange();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if scrollOnBranchChange === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
{$i18n.t('Rich Text Input for Chat')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
toggleRichTextInput();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if richTextInput === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
{$i18n.t('Chat Background Image')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
if (backgroundImageUrl !== null) {
|
||||
backgroundImageUrl = null;
|
||||
saveSettings({ backgroundImageUrl });
|
||||
} else {
|
||||
filesInputElement.click();
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if backgroundImageUrl !== null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Reset')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Upload')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" my-1.5 text-sm font-medium">{$i18n.t('Chat')}</div>
|
||||
|
||||
<div>
|
||||
@@ -523,7 +447,7 @@
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
{$i18n.t('Response AutoCopy to Clipboard')}
|
||||
{$i18n.t('Auto-Copy Response to Clipboard')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -542,6 +466,77 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
{$i18n.t('Rich Text Input for Chat')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
toggleRichTextInput();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if richTextInput === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
{$i18n.t('Paste Large Text as File')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
toggleLargeTextAsFile();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if largeTextAsFile === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
{$i18n.t('Chat Background Image')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
if (backgroundImageUrl !== null) {
|
||||
backgroundImageUrl = null;
|
||||
saveSettings({ backgroundImageUrl });
|
||||
} else {
|
||||
filesInputElement.click();
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if backgroundImageUrl !== null}
|
||||
<span class="ml-2 self-center">{$i18n.t('Reset')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Upload')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">{$i18n.t('Allow User Location')}</div>
|
||||
@@ -582,6 +577,50 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
{$i18n.t('Fluidly stream large external response chunks')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
toggleSplitLargeChunks();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if splitLargeChunks === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
{$i18n.t('Scroll to bottom when switching between branches')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
togglesScrollOnBranchChange();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if scrollOnBranchChange === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" my-1.5 text-sm font-medium">{$i18n.t('Voice')}</div>
|
||||
|
||||
<div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,7 @@
|
||||
dispatch('save');
|
||||
}}
|
||||
>
|
||||
<div class=" pr-1.5 py-1 overflow-y-scroll max-h-[25rem]">
|
||||
<div class="py-1 overflow-y-scroll max-h-[28rem] lg:max-h-full">
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<Tooltip
|
||||
|
||||
@@ -15,11 +15,298 @@
|
||||
import Chats from './Settings/Chats.svelte';
|
||||
import User from '../icons/User.svelte';
|
||||
import Personalization from './Settings/Personalization.svelte';
|
||||
import SearchInput from '../layout/Sidebar/SearchInput.svelte';
|
||||
import Search from '../icons/Search.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
|
||||
interface SettingsTab {
|
||||
id: string;
|
||||
title: string;
|
||||
keywords: string[];
|
||||
}
|
||||
|
||||
const searchData: SettingsTab[] = [
|
||||
{
|
||||
id: 'general',
|
||||
title: 'General',
|
||||
keywords: [
|
||||
'general',
|
||||
'theme',
|
||||
'language',
|
||||
'notifications',
|
||||
'system',
|
||||
'systemprompt',
|
||||
'prompt',
|
||||
'advanced',
|
||||
'settings',
|
||||
'defaultsettings',
|
||||
'configuration',
|
||||
'systemsettings',
|
||||
'notificationsettings',
|
||||
'systempromptconfig',
|
||||
'languageoptions',
|
||||
'defaultparameters',
|
||||
'systemparameters'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'interface',
|
||||
title: 'Interface',
|
||||
keywords: [
|
||||
'defaultmodel',
|
||||
'selectmodel',
|
||||
'ui',
|
||||
'userinterface',
|
||||
'display',
|
||||
'layout',
|
||||
'design',
|
||||
'landingpage',
|
||||
'landingpagemode',
|
||||
'default',
|
||||
'chat',
|
||||
'chatbubble',
|
||||
'chatui',
|
||||
'username',
|
||||
'showusername',
|
||||
'displayusername',
|
||||
'widescreen',
|
||||
'widescreenmode',
|
||||
'fullscreen',
|
||||
'expandmode',
|
||||
'chatdirection',
|
||||
'lefttoright',
|
||||
'ltr',
|
||||
'righttoleft',
|
||||
'rtl',
|
||||
'notifications',
|
||||
'toast',
|
||||
'toastnotifications',
|
||||
'largechunks',
|
||||
'streamlargechunks',
|
||||
'scroll',
|
||||
'scrollonbranchchange',
|
||||
'scrollbehavior',
|
||||
'richtext',
|
||||
'richtextinput',
|
||||
'background',
|
||||
'chatbackground',
|
||||
'chatbackgroundimage',
|
||||
'backgroundimage',
|
||||
'uploadbackground',
|
||||
'resetbackground',
|
||||
'titleautogen',
|
||||
'titleautogeneration',
|
||||
'autotitle',
|
||||
'chattags',
|
||||
'autochattags',
|
||||
'responseautocopy',
|
||||
'clipboard',
|
||||
'location',
|
||||
'userlocation',
|
||||
'userlocationaccess',
|
||||
'haptic',
|
||||
'hapticfeedback',
|
||||
'vibration',
|
||||
'voice',
|
||||
'voicecontrol',
|
||||
'voiceinterruption',
|
||||
'call',
|
||||
'emojis',
|
||||
'displayemoji',
|
||||
'save',
|
||||
'interfaceoptions',
|
||||
'interfacecustomization'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'personalization',
|
||||
title: 'Personalization',
|
||||
keywords: [
|
||||
'personalization',
|
||||
'memory',
|
||||
'personalize',
|
||||
'preferences',
|
||||
'profile',
|
||||
'personalsettings',
|
||||
'customsettings',
|
||||
'userpreferences',
|
||||
'accountpreferences'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'audio',
|
||||
title: 'Audio',
|
||||
keywords: [
|
||||
'audio',
|
||||
'sound',
|
||||
'soundsettings',
|
||||
'audiocontrol',
|
||||
'volume',
|
||||
'speech',
|
||||
'speechrecognition',
|
||||
'stt',
|
||||
'speechtotext',
|
||||
'tts',
|
||||
'texttospeech',
|
||||
'playback',
|
||||
'playbackspeed',
|
||||
'voiceplayback',
|
||||
'speechplayback',
|
||||
'audiooutput',
|
||||
'speechengine',
|
||||
'voicecontrol',
|
||||
'audioplayback',
|
||||
'transcription',
|
||||
'autotranscribe',
|
||||
'autosend',
|
||||
'speechsettings',
|
||||
'audiovoice',
|
||||
'voiceoptions',
|
||||
'setvoice',
|
||||
'nonlocalvoices',
|
||||
'savesettings',
|
||||
'audioconfig',
|
||||
'speechconfig',
|
||||
'voicerecognition',
|
||||
'speechsynthesis',
|
||||
'speechmode',
|
||||
'voicespeed',
|
||||
'speechrate',
|
||||
'speechspeed',
|
||||
'audioinput',
|
||||
'audiofeatures',
|
||||
'voicemodes'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'chats',
|
||||
title: 'Chats',
|
||||
keywords: [
|
||||
'chat',
|
||||
'messages',
|
||||
'conversations',
|
||||
'chatsettings',
|
||||
'history',
|
||||
'chathistory',
|
||||
'messagehistory',
|
||||
'messagearchive',
|
||||
'convo',
|
||||
'chats',
|
||||
'conversationhistory',
|
||||
'exportmessages',
|
||||
'chatactivity'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'account',
|
||||
title: 'Account',
|
||||
keywords: [
|
||||
'account',
|
||||
'profile',
|
||||
'security',
|
||||
'privacy',
|
||||
'settings',
|
||||
'login',
|
||||
'useraccount',
|
||||
'userdata',
|
||||
'api',
|
||||
'apikey',
|
||||
'userprofile',
|
||||
'profiledetails',
|
||||
'accountsettings',
|
||||
'accountpreferences',
|
||||
'securitysettings',
|
||||
'privacysettings'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'admin',
|
||||
title: 'Admin',
|
||||
keywords: [
|
||||
'admin',
|
||||
'administrator',
|
||||
'adminsettings',
|
||||
'adminpanel',
|
||||
'systemadmin',
|
||||
'administratoraccess',
|
||||
'systemcontrol',
|
||||
'manage',
|
||||
'management',
|
||||
'admincontrols',
|
||||
'adminfeatures',
|
||||
'usercontrol',
|
||||
'arenamodel',
|
||||
'evaluations',
|
||||
'websearch',
|
||||
'database',
|
||||
'pipelines',
|
||||
'images',
|
||||
'audio',
|
||||
'documents',
|
||||
'rag',
|
||||
'models',
|
||||
'ollama',
|
||||
'openai',
|
||||
'users'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'about',
|
||||
title: 'About',
|
||||
keywords: [
|
||||
'about',
|
||||
'info',
|
||||
'information',
|
||||
'version',
|
||||
'documentation',
|
||||
'help',
|
||||
'support',
|
||||
'details',
|
||||
'aboutus',
|
||||
'softwareinfo',
|
||||
'timothyjaeryangbaek',
|
||||
'openwebui',
|
||||
'release',
|
||||
'updates',
|
||||
'updateinfo',
|
||||
'versioninfo',
|
||||
'aboutapp',
|
||||
'terms',
|
||||
'termsandconditions',
|
||||
'contact',
|
||||
'aboutpage'
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
let search = '';
|
||||
let visibleTabs = searchData.map((tab) => tab.id);
|
||||
let searchDebounceTimeout;
|
||||
|
||||
const searchSettings = (query: string): string[] => {
|
||||
const lowerCaseQuery = query.toLowerCase().trim();
|
||||
return searchData
|
||||
.filter(
|
||||
(tab) =>
|
||||
tab.title.toLowerCase().includes(lowerCaseQuery) ||
|
||||
tab.keywords.some((keyword) => keyword.includes(lowerCaseQuery))
|
||||
)
|
||||
.map((tab) => tab.id);
|
||||
};
|
||||
|
||||
const searchDebounceHandler = () => {
|
||||
clearTimeout(searchDebounceTimeout);
|
||||
searchDebounceTimeout = setTimeout(() => {
|
||||
visibleTabs = searchSettings(search);
|
||||
if (visibleTabs.length > 0 && !visibleTabs.includes(selectedTab)) {
|
||||
selectedTab = visibleTabs[0];
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const saveSettings = async (updated) => {
|
||||
console.log(updated);
|
||||
await settings.set({ ...$settings, ...updated });
|
||||
@@ -65,7 +352,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:show>
|
||||
<Modal size="xl" bind:show>
|
||||
<div class="text-gray-700 dark:text-gray-100">
|
||||
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-1">
|
||||
<div class=" text-lg font-medium self-center">{$i18n.t('Settings')}</div>
|
||||
@@ -88,213 +375,235 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row w-full px-4 pt-2 pb-4 md:space-x-4">
|
||||
<div class="flex flex-col md:flex-row w-full px-4 pt-1 pb-4 md:space-x-4">
|
||||
<div
|
||||
id="settings-tabs-container"
|
||||
class="tabs flex flex-row overflow-x-auto space-x-1 md:space-x-0 md:space-y-1 md:flex-col flex-1 md:flex-none md:w-40 dark:text-gray-200 text-xs text-left mb-3 md:mb-0"
|
||||
class="tabs flex flex-row overflow-x-auto gap-2.5 md:gap-1 md:flex-col flex-1 md:flex-none md:w-40 dark:text-gray-200 text-sm font-medium text-left mb-1 md:mb-0 -translate-y-1"
|
||||
>
|
||||
<button
|
||||
class="px-2.5 py-2 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
|
||||
'general'
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'general';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<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="M8.34 1.804A1 1 0 019.32 1h1.36a1 1 0 01.98.804l.295 1.473c.497.144.971.342 1.416.587l1.25-.834a1 1 0 011.262.125l.962.962a1 1 0 01.125 1.262l-.834 1.25c.245.445.443.919.587 1.416l1.473.294a1 1 0 01.804.98v1.361a1 1 0 01-.804.98l-1.473.295a6.95 6.95 0 01-.587 1.416l.834 1.25a1 1 0 01-.125 1.262l-.962.962a1 1 0 01-1.262.125l-1.25-.834a6.953 6.953 0 01-1.416.587l-.294 1.473a1 1 0 01-.98.804H9.32a1 1 0 01-.98-.804l-.295-1.473a6.957 6.957 0 01-1.416-.587l-1.25.834a1 1 0 01-1.262-.125l-.962-.962a1 1 0 01-.125-1.262l.834-1.25a6.957 6.957 0 01-.587-1.416l-1.473-.294A1 1 0 011 10.68V9.32a1 1 0 01.804-.98l1.473-.295c.144-.497.342-.971.587-1.416l-.834-1.25a1 1 0 01.125-1.262l.962-.962A1 1 0 015.38 3.03l1.25.834a6.957 6.957 0 011.416-.587l.294-1.473zM13 10a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<div class="hidden md:flex w-full rounded-xl -mb-1 px-0.5 gap-2" id="settings-search">
|
||||
<div class="self-center rounded-l-xl bg-transparent">
|
||||
<Search className="size-3.5" />
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('General')}</div>
|
||||
</button>
|
||||
<input
|
||||
class="w-full py-1.5 text-sm bg-transparent dark:text-gray-300 outline-none"
|
||||
bind:value={search}
|
||||
on:input={searchDebounceHandler}
|
||||
placeholder={$i18n.t('Search')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
|
||||
'interface'
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'interface';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2 4.25A2.25 2.25 0 0 1 4.25 2h7.5A2.25 2.25 0 0 1 14 4.25v5.5A2.25 2.25 0 0 1 11.75 12h-1.312c.1.128.21.248.328.36a.75.75 0 0 1 .234.545v.345a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1-.75-.75v-.345a.75.75 0 0 1 .234-.545c.118-.111.228-.232.328-.36H4.25A2.25 2.25 0 0 1 2 9.75v-5.5Zm2.25-.75a.75.75 0 0 0-.75.75v4.5c0 .414.336.75.75.75h7.5a.75.75 0 0 0 .75-.75v-4.5a.75.75 0 0 0-.75-.75h-7.5Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Interface')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
|
||||
'personalization'
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'personalization';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<User />
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Personalization')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
|
||||
'audio'
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'audio';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M7.557 2.066A.75.75 0 0 1 8 2.75v10.5a.75.75 0 0 1-1.248.56L3.59 11H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1.59l3.162-2.81a.75.75 0 0 1 .805-.124ZM12.95 3.05a.75.75 0 1 0-1.06 1.06 5.5 5.5 0 0 1 0 7.78.75.75 0 1 0 1.06 1.06 7 7 0 0 0 0-9.9Z"
|
||||
/>
|
||||
<path
|
||||
d="M10.828 5.172a.75.75 0 1 0-1.06 1.06 2.5 2.5 0 0 1 0 3.536.75.75 0 1 0 1.06 1.06 4 4 0 0 0 0-5.656Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Audio')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
|
||||
'chats'
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'chats';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8 2C4.262 2 1 4.57 1 8c0 1.86.98 3.486 2.455 4.566a3.472 3.472 0 0 1-.469 1.26.75.75 0 0 0 .713 1.14 6.961 6.961 0 0 0 3.06-1.06c.403.062.818.094 1.241.094 3.738 0 7-2.57 7-6s-3.262-6-7-6ZM5 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm7-1a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM8 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Chats')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
|
||||
'account'
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'account';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M15 8A7 7 0 1 1 1 8a7 7 0 0 1 14 0Zm-5-2a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM8 9c-1.825 0-3.422.977-4.295 2.437A5.49 5.49 0 0 0 8 13.5a5.49 5.49 0 0 0 4.294-2.063A4.997 4.997 0 0 0 8 9Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Account')}</div>
|
||||
</button>
|
||||
|
||||
{#if $user.role === 'admin'}
|
||||
<button
|
||||
class="px-2.5 py-2 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
|
||||
'admin'
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
|
||||
on:click={async () => {
|
||||
await goto('/admin/settings');
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
{#if visibleTabs.length > 0}
|
||||
{#each visibleTabs as tabId (tabId)}
|
||||
{#if tabId === 'general'}
|
||||
<button
|
||||
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
|
||||
'general'
|
||||
? ''
|
||||
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'general';
|
||||
}}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4.5 3.75a3 3 0 0 0-3 3v10.5a3 3 0 0 0 3 3h15a3 3 0 0 0 3-3V6.75a3 3 0 0 0-3-3h-15Zm4.125 3a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Zm-3.873 8.703a4.126 4.126 0 0 1 7.746 0 .75.75 0 0 1-.351.92 7.47 7.47 0 0 1-3.522.877 7.47 7.47 0 0 1-3.522-.877.75.75 0 0 1-.351-.92ZM15 8.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15ZM14.25 12a.75.75 0 0 1 .75-.75h3.75a.75.75 0 0 1 0 1.5H15a.75.75 0 0 1-.75-.75Zm.75 2.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Admin Settings')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
|
||||
'about'
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'about';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<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="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<div class=" self-center mr-2">
|
||||
<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="M8.34 1.804A1 1 0 019.32 1h1.36a1 1 0 01.98.804l.295 1.473c.497.144.971.342 1.416.587l1.25-.834a1 1 0 011.262.125l.962.962a1 1 0 01.125 1.262l-.834 1.25c.245.445.443.919.587 1.416l1.473.294a1 1 0 01.804.98v1.361a1 1 0 01-.804.98l-1.473.295a6.95 6.95 0 01-.587 1.416l.834 1.25a1 1 0 01-.125 1.262l-.962.962a1 1 0 01-1.262.125l-1.25-.834a6.953 6.953 0 01-1.416.587l-.294 1.473a1 1 0 01-.98.804H9.32a1 1 0 01-.98-.804l-.295-1.473a6.957 6.957 0 01-1.416-.587l-1.25.834a1 1 0 01-1.262-.125l-.962-.962a1 1 0 01-.125-1.262l.834-1.25a6.957 6.957 0 01-.587-1.416l-1.473-.294A1 1 0 011 10.68V9.32a1 1 0 01.804-.98l1.473-.295c.144-.497.342-.971.587-1.416l-.834-1.25a1 1 0 01.125-1.262l.962-.962A1 1 0 015.38 3.03l1.25.834a6.957 6.957 0 011.416-.587l.294-1.473zM13 10a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('General')}</div>
|
||||
</button>
|
||||
{:else if tabId === 'interface'}
|
||||
<button
|
||||
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
|
||||
'interface'
|
||||
? ''
|
||||
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'interface';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2 4.25A2.25 2.25 0 0 1 4.25 2h7.5A2.25 2.25 0 0 1 14 4.25v5.5A2.25 2.25 0 0 1 11.75 12h-1.312c.1.128.21.248.328.36a.75.75 0 0 1 .234.545v.345a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1-.75-.75v-.345a.75.75 0 0 1 .234-.545c.118-.111.228-.232.328-.36H4.25A2.25 2.25 0 0 1 2 9.75v-5.5Zm2.25-.75a.75.75 0 0 0-.75.75v4.5c0 .414.336.75.75.75h7.5a.75.75 0 0 0 .75-.75v-4.5a.75.75 0 0 0-.75-.75h-7.5Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Interface')}</div>
|
||||
</button>
|
||||
{:else if tabId === 'personalization'}
|
||||
<button
|
||||
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
|
||||
'personalization'
|
||||
? ''
|
||||
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'personalization';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<User />
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Personalization')}</div>
|
||||
</button>
|
||||
{:else if tabId === 'audio'}
|
||||
<button
|
||||
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
|
||||
'audio'
|
||||
? ''
|
||||
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'audio';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M7.557 2.066A.75.75 0 0 1 8 2.75v10.5a.75.75 0 0 1-1.248.56L3.59 11H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1.59l3.162-2.81a.75.75 0 0 1 .805-.124ZM12.95 3.05a.75.75 0 1 0-1.06 1.06 5.5 5.5 0 0 1 0 7.78.75.75 0 1 0 1.06 1.06 7 7 0 0 0 0-9.9Z"
|
||||
/>
|
||||
<path
|
||||
d="M10.828 5.172a.75.75 0 1 0-1.06 1.06 2.5 2.5 0 0 1 0 3.536.75.75 0 1 0 1.06 1.06 4 4 0 0 0 0-5.656Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Audio')}</div>
|
||||
</button>
|
||||
{:else if tabId === 'chats'}
|
||||
<button
|
||||
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
|
||||
'chats'
|
||||
? ''
|
||||
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'chats';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8 2C4.262 2 1 4.57 1 8c0 1.86.98 3.486 2.455 4.566a3.472 3.472 0 0 1-.469 1.26.75.75 0 0 0 .713 1.14 6.961 6.961 0 0 0 3.06-1.06c.403.062.818.094 1.241.094 3.738 0 7-2.57 7-6s-3.262-6-7-6ZM5 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm7-1a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM8 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Chats')}</div>
|
||||
</button>
|
||||
{:else if tabId === 'account'}
|
||||
<button
|
||||
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
|
||||
'account'
|
||||
? ''
|
||||
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'account';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M15 8A7 7 0 1 1 1 8a7 7 0 0 1 14 0Zm-5-2a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM8 9c-1.825 0-3.422.977-4.295 2.437A5.49 5.49 0 0 0 8 13.5a5.49 5.49 0 0 0 4.294-2.063A4.997 4.997 0 0 0 8 9Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Account')}</div>
|
||||
</button>
|
||||
{:else if tabId === 'about'}
|
||||
<button
|
||||
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
|
||||
'about'
|
||||
? ''
|
||||
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'about';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<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="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('About')}</div>
|
||||
</button>
|
||||
{:else if tabId === 'admin'}
|
||||
{#if $user.role === 'admin'}
|
||||
<button
|
||||
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
|
||||
'admin'
|
||||
? ''
|
||||
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||
on:click={async () => {
|
||||
await goto('/admin/settings');
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4.5 3.75a3 3 0 0 0-3 3v10.5a3 3 0 0 0 3 3h15a3 3 0 0 0 3-3V6.75a3 3 0 0 0-3-3h-15Zm4.125 3a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Zm-3.873 8.703a4.126 4.126 0 0 1 7.746 0 .75.75 0 0 1-.351.92 7.47 7.47 0 0 1-3.522.877 7.47 7.47 0 0 1-3.522-.877.75.75 0 0 1-.351-.92ZM15 8.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15ZM14.25 12a.75.75 0 0 1 .75-.75h3.75a.75.75 0 0 1 0 1.5H15a.75.75 0 0 1-.75-.75Zm.75 2.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Admin Settings')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="text-center text-gray-500 mt-4">
|
||||
{$i18n.t('No results found')}
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('About')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-1 md:min-h-[28rem]">
|
||||
<div class="flex-1 md:min-h-[32rem] max-h-[32rem]">
|
||||
{#if selectedTab === 'general'}
|
||||
<General
|
||||
{getModels}
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:show size="sm">
|
||||
<Modal bind:show size="md">
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-0.5">
|
||||
<div class=" text-lg font-medium self-center">{$i18n.t('Share Chat')}</div>
|
||||
|
||||
Reference in New Issue
Block a user