mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 04:15:25 +00:00
feat: note chat
This commit is contained in:
parent
a31a1f3c0d
commit
9bd001a14b
10 changed files with 545 additions and 165 deletions
|
|
@ -246,7 +246,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" pb-[1rem]">
|
<div class=" pb-[1rem] px-2.5">
|
||||||
<MessageInput
|
<MessageInput
|
||||||
id="root"
|
id="root"
|
||||||
{typingUsers}
|
{typingUsers}
|
||||||
|
|
|
||||||
|
|
@ -31,17 +31,22 @@
|
||||||
let content = '';
|
let content = '';
|
||||||
let files = [];
|
let files = [];
|
||||||
|
|
||||||
let chatInputElement;
|
export let chatInputElement;
|
||||||
let filesInputElement;
|
let filesInputElement;
|
||||||
let inputFiles;
|
let inputFiles;
|
||||||
|
|
||||||
export let typingUsers = [];
|
export let typingUsers = [];
|
||||||
|
export let inputLoading = false;
|
||||||
|
|
||||||
|
export let onSubmit: Function = (e) => {};
|
||||||
|
export let onChange: Function = (e) => {};
|
||||||
|
export let onStop: Function = (e) => {};
|
||||||
|
|
||||||
export let onSubmit: Function;
|
|
||||||
export let onChange: Function;
|
|
||||||
export let scrollEnd = true;
|
export let scrollEnd = true;
|
||||||
export let scrollToBottom: Function = () => {};
|
export let scrollToBottom: Function = () => {};
|
||||||
|
|
||||||
|
export let acceptFiles = true;
|
||||||
|
|
||||||
const screenCaptureHandler = async () => {
|
const screenCaptureHandler = async () => {
|
||||||
try {
|
try {
|
||||||
// Request screen media
|
// Request screen media
|
||||||
|
|
@ -260,7 +265,7 @@
|
||||||
const onDrop = async (e) => {
|
const onDrop = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (e.dataTransfer?.files) {
|
if (e.dataTransfer?.files && acceptFiles) {
|
||||||
const inputFiles = Array.from(e.dataTransfer?.files);
|
const inputFiles = Array.from(e.dataTransfer?.files);
|
||||||
if (inputFiles && inputFiles.length > 0) {
|
if (inputFiles && inputFiles.length > 0) {
|
||||||
console.log(inputFiles);
|
console.log(inputFiles);
|
||||||
|
|
@ -330,7 +335,8 @@
|
||||||
|
|
||||||
<FilesOverlay show={draggedOver} />
|
<FilesOverlay show={draggedOver} />
|
||||||
|
|
||||||
<input
|
{#if acceptFiles}
|
||||||
|
<input
|
||||||
bind:this={filesInputElement}
|
bind:this={filesInputElement}
|
||||||
bind:files={inputFiles}
|
bind:files={inputFiles}
|
||||||
type="file"
|
type="file"
|
||||||
|
|
@ -345,12 +351,14 @@
|
||||||
|
|
||||||
filesInputElement.value = '';
|
filesInputElement.value = '';
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="bg-transparent">
|
<div class="bg-transparent">
|
||||||
<div
|
<div
|
||||||
class="{($settings?.widescreenMode ?? null)
|
class="{($settings?.widescreenMode ?? null)
|
||||||
? 'max-w-full'
|
? 'max-w-full'
|
||||||
: 'max-w-6xl'} px-2.5 mx-auto inset-x-0 relative"
|
: 'max-w-6xl'} mx-auto inset-x-0 relative"
|
||||||
>
|
>
|
||||||
<div class="absolute top-0 left-0 right-0 mx-auto inset-x-0 bg-transparent flex justify-center">
|
<div class="absolute top-0 left-0 right-0 mx-auto inset-x-0 bg-transparent flex justify-center">
|
||||||
<div class="flex flex-col px-3 w-full">
|
<div class="flex flex-col px-3 w-full">
|
||||||
|
|
@ -492,7 +500,7 @@
|
||||||
|
|
||||||
<div class="px-2.5">
|
<div class="px-2.5">
|
||||||
<div
|
<div
|
||||||
class="scrollbar-hidden font-primary text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
|
class="scrollbar-hidden font-primary text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 resize-none h-fit max-h-80 overflow-auto"
|
||||||
>
|
>
|
||||||
<RichTextInput
|
<RichTextInput
|
||||||
bind:this={chatInputElement}
|
bind:this={chatInputElement}
|
||||||
|
|
@ -547,6 +555,8 @@
|
||||||
|
|
||||||
<div class=" flex justify-between mb-2.5 mt-1.5 mx-0.5">
|
<div class=" flex justify-between mb-2.5 mt-1.5 mx-0.5">
|
||||||
<div class="ml-1 self-end flex space-x-1">
|
<div class="ml-1 self-end flex space-x-1">
|
||||||
|
<slot name="menu">
|
||||||
|
{#if acceptFiles}
|
||||||
<InputMenu
|
<InputMenu
|
||||||
{screenCaptureHandler}
|
{screenCaptureHandler}
|
||||||
uploadFilesHandler={() => {
|
uploadFilesHandler={() => {
|
||||||
|
|
@ -570,6 +580,8 @@
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</InputMenu>
|
</InputMenu>
|
||||||
|
{/if}
|
||||||
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="self-end flex space-x-1 mr-1">
|
<div class="self-end flex space-x-1 mr-1">
|
||||||
|
|
@ -620,6 +632,31 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class=" flex items-center">
|
<div class=" flex items-center">
|
||||||
|
{#if inputLoading && onStop}
|
||||||
|
<div class=" flex items-center">
|
||||||
|
<Tooltip content={$i18n.t('Stop')}>
|
||||||
|
<button
|
||||||
|
class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
|
||||||
|
on:click={() => {
|
||||||
|
onStop();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm6-2.438c0-.724.588-1.312 1.313-1.312h4.874c.725 0 1.313.588 1.313 1.313v4.874c0 .725-.588 1.313-1.313 1.313H9.564a1.312 1.312 0 01-1.313-1.313V9.564z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
<div class=" flex items-center">
|
<div class=" flex items-center">
|
||||||
<Tooltip content={$i18n.t('Send message')}>
|
<Tooltip content={$i18n.t('Send message')}>
|
||||||
<button
|
<button
|
||||||
|
|
@ -645,6 +682,7 @@
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,7 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class=" pb-[1rem]">
|
<div class=" pb-[1rem] px-2.5">
|
||||||
<MessageInput id={threadId} {typingUsers} {onChange} onSubmit={submitHandler} />
|
<MessageInput id={threadId} {typingUsers} {onChange} onSubmit={submitHandler} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,8 @@
|
||||||
let displayMediaRecord = false;
|
let displayMediaRecord = false;
|
||||||
|
|
||||||
let showPanel = false;
|
let showPanel = false;
|
||||||
|
let selectedPanel = 'chat';
|
||||||
|
|
||||||
let showDeleteConfirm = false;
|
let showDeleteConfirm = false;
|
||||||
|
|
||||||
let dragged = false;
|
let dragged = false;
|
||||||
|
|
@ -677,6 +679,9 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||||
import { PaneGroup, Pane, PaneResizer } from 'paneforge';
|
import { PaneGroup, Pane, PaneResizer } from 'paneforge';
|
||||||
import XMark from '../icons/XMark.svelte';
|
import XMark from '../icons/XMark.svelte';
|
||||||
import MenuLines from '../icons/MenuLines.svelte';
|
import MenuLines from '../icons/MenuLines.svelte';
|
||||||
|
import ChatBubbleOval from '../icons/ChatBubbleOval.svelte';
|
||||||
|
import Settings from './NoteEditor/Settings.svelte';
|
||||||
|
import Chat from './NoteEditor/Chat.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FilesOverlay show={dragged} />
|
<FilesOverlay show={dragged} />
|
||||||
|
|
@ -709,7 +714,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||||
<div class="w-full flex items-center">
|
<div class="w-full flex items-center">
|
||||||
<div
|
<div
|
||||||
class="{$showSidebar
|
class="{$showSidebar
|
||||||
? 'md:hidden'
|
? 'md:hidden pl-0.5'
|
||||||
: ''} flex flex-none items-center pr-1 -translate-x-1"
|
: ''} flex flex-none items-center pr-1 -translate-x-1"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
|
@ -734,7 +739,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex items-center gap-2 translate-x-1">
|
<div class="flex items-center gap-0.5 translate-x-1">
|
||||||
{#if note.data?.versions?.length > 0}
|
{#if note.data?.versions?.length > 0}
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-0.5 self-center min-w-fit" dir="ltr">
|
<div class="flex items-center gap-0.5 self-center min-w-fit" dir="ltr">
|
||||||
|
|
@ -780,17 +785,46 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||||
showDeleteConfirm = true;
|
showDeleteConfirm = true;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div class="p-1 bg-transparent hover:bg-white/5 transition rounded-lg">
|
||||||
<EllipsisHorizontal className="size-5" />
|
<EllipsisHorizontal className="size-5" />
|
||||||
|
</div>
|
||||||
</NoteMenu>
|
</NoteMenu>
|
||||||
|
|
||||||
|
<Tooltip placement="top" content={$i18n.t('Chat')} className="cursor-pointer">
|
||||||
<button
|
<button
|
||||||
class="p-1.5 bg-transparent hover:bg-white/5 transition rounded-lg"
|
class="p-1.5 bg-transparent hover:bg-white/5 transition rounded-lg"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showPanel = !showPanel;
|
if (showPanel && selectedPanel === 'chat') {
|
||||||
|
showPanel = false;
|
||||||
|
} else {
|
||||||
|
if (!showPanel) {
|
||||||
|
showPanel = true;
|
||||||
|
}
|
||||||
|
selectedPanel = 'chat';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChatBubbleOval />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip placement="top" content={$i18n.t('Settings')} className="cursor-pointer">
|
||||||
|
<button
|
||||||
|
class="p-1.5 bg-transparent hover:bg-white/5 transition rounded-lg"
|
||||||
|
on:click={() => {
|
||||||
|
if (showPanel && selectedPanel === 'settings') {
|
||||||
|
showPanel = false;
|
||||||
|
} else {
|
||||||
|
if (!showPanel) {
|
||||||
|
showPanel = true;
|
||||||
|
}
|
||||||
|
selectedPanel = 'settings';
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Cog6 />
|
<Cog6 />
|
||||||
</button>
|
</button>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -998,36 +1032,10 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||||
</div>
|
</div>
|
||||||
</Pane>
|
</Pane>
|
||||||
<NotePanel bind:show={showPanel}>
|
<NotePanel bind:show={showPanel}>
|
||||||
<div class="flex items-center mb-2">
|
{#if selectedPanel === 'chat'}
|
||||||
<div class=" -translate-x-1.5">
|
<Chat bind:show={showPanel} bind:selectedModelId />
|
||||||
<button
|
{:else if selectedPanel === 'settings'}
|
||||||
class="p-1.5 bg-transparent transition rounded-lg"
|
<Settings bind:show={showPanel} bind:selectedModelId />
|
||||||
on:click={() => {
|
{/if}
|
||||||
showPanel = !showPanel;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<XMark className="size-5" strokeWidth="2.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" font-medium text-base">Settings</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-1">
|
|
||||||
<div>
|
|
||||||
<div class=" text-xs font-medium mb-1">Model</div>
|
|
||||||
|
|
||||||
<div class="w-full">
|
|
||||||
<select class="w-full bg-transparent text-sm outline-hidden" bind:value={selectedModelId}>
|
|
||||||
<option value="" class="bg-gray-50 dark:bg-gray-700" disabled>
|
|
||||||
{$i18n.t('Select a model')}
|
|
||||||
</option>
|
|
||||||
{#each $models.filter((model) => !(model?.info?.meta?.hidden ?? false)) as model}
|
|
||||||
<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</NotePanel>
|
</NotePanel>
|
||||||
</PaneGroup>
|
</PaneGroup>
|
||||||
|
|
|
||||||
257
src/lib/components/notes/NoteEditor/Chat.svelte
Normal file
257
src/lib/components/notes/NoteEditor/Chat.svelte
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let show = false;
|
||||||
|
export let selectedModelId = '';
|
||||||
|
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { onMount, tick, getContext } from 'svelte';
|
||||||
|
|
||||||
|
import {
|
||||||
|
OLLAMA_API_BASE_URL,
|
||||||
|
OPENAI_API_BASE_URL,
|
||||||
|
WEBUI_API_BASE_URL,
|
||||||
|
WEBUI_BASE_URL
|
||||||
|
} from '$lib/constants';
|
||||||
|
import { WEBUI_NAME, config, user, models, settings } from '$lib/stores';
|
||||||
|
|
||||||
|
import { chatCompletion, generateOpenAIChatCompletion } from '$lib/apis/openai';
|
||||||
|
|
||||||
|
import { splitStream } from '$lib/utils';
|
||||||
|
|
||||||
|
import Messages from '$lib/components/notes/NoteEditor/Chat/Messages.svelte';
|
||||||
|
import MessageInput from '$lib/components/channel/MessageInput.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
let loaded = false;
|
||||||
|
|
||||||
|
let loading = false;
|
||||||
|
let stopResponseFlag = false;
|
||||||
|
|
||||||
|
let systemTextareaElement: HTMLTextAreaElement;
|
||||||
|
let messagesContainerElement: HTMLDivElement;
|
||||||
|
|
||||||
|
let system = '';
|
||||||
|
let content = '';
|
||||||
|
|
||||||
|
let messages = [];
|
||||||
|
let chatInputElement = null;
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
const element = messagesContainerElement;
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
element.scrollTop = element?.scrollHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopHandler = () => {
|
||||||
|
stopResponseFlag = true;
|
||||||
|
console.log('stopResponse');
|
||||||
|
};
|
||||||
|
|
||||||
|
const chatCompletionHandler = async () => {
|
||||||
|
if (selectedModelId === '') {
|
||||||
|
toast.error($i18n.t('Please select a model.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const model = $models.find((model) => model.id === selectedModelId);
|
||||||
|
if (!model) {
|
||||||
|
selectedModelId = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [res, controller] = await chatCompletion(
|
||||||
|
localStorage.token,
|
||||||
|
{
|
||||||
|
model: model.id,
|
||||||
|
stream: true,
|
||||||
|
messages: [
|
||||||
|
system
|
||||||
|
? {
|
||||||
|
role: 'system',
|
||||||
|
content: system
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
...messages
|
||||||
|
].filter((message) => message)
|
||||||
|
},
|
||||||
|
`${WEBUI_BASE_URL}/api`
|
||||||
|
);
|
||||||
|
|
||||||
|
let responseMessage;
|
||||||
|
if (messages.at(-1)?.role === 'assistant') {
|
||||||
|
responseMessage = messages.at(-1);
|
||||||
|
} else {
|
||||||
|
responseMessage = {
|
||||||
|
role: 'assistant',
|
||||||
|
content: ''
|
||||||
|
};
|
||||||
|
messages.push(responseMessage);
|
||||||
|
messages = messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
const textareaElement = document.getElementById(`assistant-${messages.length - 1}-textarea`);
|
||||||
|
|
||||||
|
if (res && res.ok) {
|
||||||
|
const reader = res.body
|
||||||
|
.pipeThrough(new TextDecoderStream())
|
||||||
|
.pipeThrough(splitStream('\n'))
|
||||||
|
.getReader();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { value, done } = await reader.read();
|
||||||
|
if (done || stopResponseFlag) {
|
||||||
|
if (stopResponseFlag) {
|
||||||
|
controller.abort('User: Stop Response');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let lines = value.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line !== '') {
|
||||||
|
console.log(line);
|
||||||
|
if (line === 'data: [DONE]') {
|
||||||
|
// responseMessage.done = true;
|
||||||
|
messages = messages;
|
||||||
|
} else {
|
||||||
|
let data = JSON.parse(line.replace(/^data: /, ''));
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
if (responseMessage.content == '' && data.choices[0].delta.content == '\n') {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
textareaElement.style.height = textareaElement.scrollHeight + 'px';
|
||||||
|
|
||||||
|
responseMessage.content += data.choices[0].delta.content ?? '';
|
||||||
|
messages = messages;
|
||||||
|
|
||||||
|
textareaElement.style.height = textareaElement.scrollHeight + 'px';
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitHandler = async (e) => {
|
||||||
|
const { content, data } = e;
|
||||||
|
if (selectedModelId && content) {
|
||||||
|
messages.push({
|
||||||
|
role: 'user',
|
||||||
|
content: content
|
||||||
|
});
|
||||||
|
messages = messages;
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
scrollToBottom();
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
await chatCompletionHandler();
|
||||||
|
|
||||||
|
loading = false;
|
||||||
|
stopResponseFlag = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if ($user?.role !== 'admin') {
|
||||||
|
await goto('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($settings?.models) {
|
||||||
|
selectedModelId = $settings?.models[0];
|
||||||
|
} else if ($config?.default_models) {
|
||||||
|
selectedModelId = $config?.default_models.split(',')[0];
|
||||||
|
} else {
|
||||||
|
selectedModelId = '';
|
||||||
|
}
|
||||||
|
loaded = true;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<div class=" -translate-x-1.5">
|
||||||
|
<button
|
||||||
|
class="p-1.5 bg-transparent transition rounded-lg"
|
||||||
|
on:click={() => {
|
||||||
|
show = !show;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XMark className="size-5" strokeWidth="2.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" font-medium text-base flex items-center gap-1">
|
||||||
|
<div>
|
||||||
|
{$i18n.t('Chat')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Tooltip
|
||||||
|
content={$i18n.t('This is an experimental feature, it may not work as expected.')}
|
||||||
|
position="top"
|
||||||
|
className="inline-block"
|
||||||
|
>
|
||||||
|
<span class="text-gray-500 text-sm">({$i18n.t('Experimental')})</span>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center mb-2 flex-1">
|
||||||
|
<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
|
||||||
|
<div class="mx-auto w-full md:px-0 h-full relative">
|
||||||
|
<div class=" flex flex-col h-full">
|
||||||
|
<div
|
||||||
|
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0"
|
||||||
|
id="messages-container"
|
||||||
|
bind:this={messagesContainerElement}
|
||||||
|
>
|
||||||
|
<div class=" h-full w-full flex flex-col">
|
||||||
|
<div class="flex-1 p-1">
|
||||||
|
<Messages bind:messages />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" pb-2">
|
||||||
|
<MessageInput
|
||||||
|
bind:chatInputElement
|
||||||
|
acceptFiles={false}
|
||||||
|
inputLoading={loading}
|
||||||
|
onSubmit={submitHandler}
|
||||||
|
onStop={stopHandler}
|
||||||
|
>
|
||||||
|
<div slot="menu">
|
||||||
|
<select
|
||||||
|
class=" bg-transparent rounded-lg py-1 px-2 -mx-0.5 text-sm outline-hidden w-50"
|
||||||
|
bind:value={selectedModelId}
|
||||||
|
>
|
||||||
|
{#each $models as model}
|
||||||
|
<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</MessageInput>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
52
src/lib/components/notes/NoteEditor/Chat/Message.svelte
Normal file
52
src/lib/components/notes/NoteEditor/Chat/Message.svelte
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount, getContext } from 'svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let message;
|
||||||
|
export let idx;
|
||||||
|
|
||||||
|
export let onDelete;
|
||||||
|
|
||||||
|
let textAreaElement: HTMLTextAreaElement;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
textAreaElement.style.height = '';
|
||||||
|
textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1 group">
|
||||||
|
<div class="flex items-start pt-1">
|
||||||
|
<div
|
||||||
|
class="px-2 py-1 text-sm font-semibold uppercase min-w-[6rem] text-left rounded-lg transition"
|
||||||
|
>
|
||||||
|
{$i18n.t(message.role)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<!-- $i18n.t('a user') -->
|
||||||
|
<!-- $i18n.t('an assistant') -->
|
||||||
|
<textarea
|
||||||
|
id="{message.role}-{idx}-textarea"
|
||||||
|
bind:this={textAreaElement}
|
||||||
|
class="w-full bg-transparent outline-hidden rounded-lg px-2 text-sm resize-none overflow-hidden"
|
||||||
|
placeholder={$i18n.t(`Enter {{role}} message here`, {
|
||||||
|
role: message.role === 'user' ? $i18n.t('a user') : $i18n.t('an assistant')
|
||||||
|
})}
|
||||||
|
rows="1"
|
||||||
|
on:input={(e) => {
|
||||||
|
textAreaElement.style.height = '';
|
||||||
|
textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
|
||||||
|
}}
|
||||||
|
on:focus={(e) => {
|
||||||
|
textAreaElement.style.height = '';
|
||||||
|
textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
|
||||||
|
|
||||||
|
// e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
|
||||||
|
}}
|
||||||
|
bind:value={message.content}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
20
src/lib/components/notes/NoteEditor/Chat/Messages.svelte
Normal file
20
src/lib/components/notes/NoteEditor/Chat/Messages.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount, getContext } from 'svelte';
|
||||||
|
import Message from './Message.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let messages = [];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
{#each messages as message, idx}
|
||||||
|
<Message
|
||||||
|
{message}
|
||||||
|
{idx}
|
||||||
|
onDelete={() => {
|
||||||
|
messages = messages.filter((message, messageIdx) => messageIdx !== idx);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
42
src/lib/components/notes/NoteEditor/Settings.svelte
Normal file
42
src/lib/components/notes/NoteEditor/Settings.svelte
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
import { models } from '$lib/stores';
|
||||||
|
|
||||||
|
export let show = false;
|
||||||
|
export let selectedModelId = '';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<div class=" -translate-x-1.5">
|
||||||
|
<button
|
||||||
|
class="p-1.5 bg-transparent transition rounded-lg"
|
||||||
|
on:click={() => {
|
||||||
|
show = !show;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XMark className="size-5" strokeWidth="2.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" font-medium text-base">Settings</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-1">
|
||||||
|
<div>
|
||||||
|
<div class=" text-xs font-medium mb-1">Model</div>
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<select class="w-full bg-transparent text-sm outline-hidden" bind:value={selectedModelId}>
|
||||||
|
<option value="" class="bg-gray-50 dark:bg-gray-700" disabled>
|
||||||
|
{$i18n.t('Select a model')}
|
||||||
|
</option>
|
||||||
|
{#each $models.filter((model) => !(model?.info?.meta?.hidden ?? false)) as model}
|
||||||
|
<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -61,8 +61,8 @@
|
||||||
|
|
||||||
<Pane
|
<Pane
|
||||||
bind:pane
|
bind:pane
|
||||||
defaultSize={30}
|
defaultSize={35}
|
||||||
minSize={30}
|
minSize={35}
|
||||||
onCollapse={() => {
|
onCollapse={() => {
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
|
|
@ -72,7 +72,7 @@
|
||||||
{#if show}
|
{#if show}
|
||||||
<div class="flex max-h-full min-h-full">
|
<div class="flex max-h-full min-h-full">
|
||||||
<div
|
<div
|
||||||
class="w-full pl-1.5 pr-2.5 pt-2 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-100 dark:border-gray-850 z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
|
class="w-full pl-1.5 pr-2.5 pt-2 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-100 dark:border-gray-850 z-40 pointer-events-auto overflow-y-auto scrollbar-hidden flex flex-col"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -211,42 +211,6 @@
|
||||||
|
|
||||||
<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
|
<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
|
||||||
<div class="mx-auto w-full md:px-0 h-full relative">
|
<div class="mx-auto w-full md:px-0 h-full relative">
|
||||||
<Sidebar bind:show={showSettings} className=" bg-white dark:bg-gray-900" width="300px">
|
|
||||||
<div class="flex flex-col px-5 py-3 text-sm">
|
|
||||||
<div class="flex justify-between items-center mb-2">
|
|
||||||
<div class=" font-medium text-base">Settings</div>
|
|
||||||
|
|
||||||
<div class=" translate-x-1.5">
|
|
||||||
<button
|
|
||||||
class="p-1.5 bg-transparent hover:bg-white/5 transition rounded-lg"
|
|
||||||
on:click={() => {
|
|
||||||
showSettings = !showSettings;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ArrowRight className="size-3" strokeWidth="2.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-1">
|
|
||||||
<div>
|
|
||||||
<div class=" text-xs font-medium mb-1">Model</div>
|
|
||||||
|
|
||||||
<div class="w-full">
|
|
||||||
<select
|
|
||||||
class="w-full bg-transparent border border-gray-100 dark:border-gray-850 rounded-lg py-1 px-2 -mx-0.5 text-sm outline-hidden"
|
|
||||||
bind:value={selectedModelId}
|
|
||||||
>
|
|
||||||
{#each $models as model}
|
|
||||||
<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Sidebar>
|
|
||||||
|
|
||||||
<div class=" flex flex-col h-full px-3.5">
|
<div class=" flex flex-col h-full px-3.5">
|
||||||
<div class="flex w-full items-start gap-1.5">
|
<div class="flex w-full items-start gap-1.5">
|
||||||
<Collapsible
|
<Collapsible
|
||||||
|
|
@ -292,17 +256,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
||||||
<div class="translate-y-1">
|
|
||||||
<button
|
|
||||||
class="p-1.5 bg-transparent hover:bg-white/5 transition rounded-lg"
|
|
||||||
on:click={() => {
|
|
||||||
showSettings = !showSettings;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Cog6 />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -318,9 +271,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pb-3">
|
<div class="pb-3">
|
||||||
<div class="text-xs font-medium text-gray-500 px-2 py-1">
|
|
||||||
{selectedModelId}
|
|
||||||
</div>
|
|
||||||
<div class="border border-gray-100 dark:border-gray-850 w-full px-3 py-2.5 rounded-xl">
|
<div class="border border-gray-100 dark:border-gray-850 w-full px-3 py-2.5 rounded-xl">
|
||||||
<div class="py-0.5">
|
<div class="py-0.5">
|
||||||
<!-- $i18n.t('a user') -->
|
<!-- $i18n.t('a user') -->
|
||||||
|
|
@ -359,7 +309,20 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="">
|
||||||
|
<select
|
||||||
|
class=" bg-transparent border border-gray-100 dark:border-gray-850 rounded-lg py-1 px-2 -mx-0.5 text-sm outline-hidden w-40"
|
||||||
|
bind:value={selectedModelId}
|
||||||
|
>
|
||||||
|
{#each $models as model}
|
||||||
|
<option value={model.id} class="bg-gray-50 dark:bg-gray-700"
|
||||||
|
>{model.name}</option
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if !loading}
|
{#if !loading}
|
||||||
<button
|
<button
|
||||||
disabled={message === ''}
|
disabled={message === ''}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue