forked from open-webui/open-webui
feat: # to import doc
This commit is contained in:
parent
2603ac30bc
commit
cc3f84f916
11 changed files with 894 additions and 32 deletions
177
src/lib/apis/documents/index.ts
Normal file
177
src/lib/apis/documents/index.ts
Normal file
|
@ -0,0 +1,177 @@
|
|||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
export const createNewDoc = async (
|
||||
token: string,
|
||||
collection_name: string,
|
||||
filename: string,
|
||||
name: string,
|
||||
title: string
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/documents/create`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
collection_name: collection_name,
|
||||
filename: filename,
|
||||
name: name,
|
||||
title: title
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getDocs = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/documents/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getDocByName = async (token: string, name: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/documents/name/${name}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
type DocUpdateForm = {
|
||||
name: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export const updateDocByName = async (token: string, name: string, form: DocUpdateForm) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/name/${name}/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: form.name,
|
||||
title: form.title
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const deleteDocByName = async (token: string, name: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/documents/name/${name}/delete`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
6
src/lib/components/AddFilesPlaceholder.svelte
Normal file
6
src/lib/components/AddFilesPlaceholder.svelte
Normal file
|
@ -0,0 +1,6 @@
|
|||
<div class=" text-center text-6xl mb-3">📄</div>
|
||||
<div class="text-center dark:text-white text-2xl font-semibold z-50">Add Files</div>
|
||||
|
||||
<div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
|
||||
Drop any files here to add to the conversation
|
||||
</div>
|
|
@ -7,6 +7,9 @@
|
|||
import Prompts from './MessageInput/PromptCommands.svelte';
|
||||
import Suggestions from './MessageInput/Suggestions.svelte';
|
||||
import { uploadDocToVectorDB } from '$lib/apis/rag';
|
||||
import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte';
|
||||
import { SUPPORTED_FILE_TYPE } from '$lib/constants';
|
||||
import Documents from './MessageInput/Documents.svelte';
|
||||
|
||||
export let submitPrompt: Function;
|
||||
export let stopResponse: Function;
|
||||
|
@ -16,6 +19,7 @@
|
|||
|
||||
let filesInputElement;
|
||||
let promptsElement;
|
||||
let documentsElement;
|
||||
|
||||
let inputFiles;
|
||||
let dragged = false;
|
||||
|
@ -143,14 +147,7 @@
|
|||
const file = inputFiles[0];
|
||||
if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) {
|
||||
reader.readAsDataURL(file);
|
||||
} else if (
|
||||
[
|
||||
'application/pdf',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'text/plain',
|
||||
'text/csv'
|
||||
].includes(file['type'])
|
||||
) {
|
||||
} else if (SUPPORTED_FILE_TYPE.includes(file['type'])) {
|
||||
uploadDoc(file);
|
||||
} else {
|
||||
toast.error(`Unsupported File Type '${file['type']}'.`);
|
||||
|
@ -179,12 +176,7 @@
|
|||
<div class="absolute rounded-xl w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
|
||||
<div class="m-auto pt-64 flex flex-col justify-center">
|
||||
<div class="max-w-md">
|
||||
<div class=" text-center text-6xl mb-3">🗂️</div>
|
||||
<div class="text-center dark:text-white text-2xl font-semibold z-50">Add Files</div>
|
||||
|
||||
<div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
|
||||
Drop any files/images here to add to the conversation
|
||||
</div>
|
||||
<AddFilesPlaceholder />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -224,6 +216,22 @@
|
|||
<div class="w-full">
|
||||
{#if prompt.charAt(0) === '/'}
|
||||
<Prompts bind:this={promptsElement} bind:prompt />
|
||||
{:else if prompt.charAt(0) === '#'}
|
||||
<Documents
|
||||
bind:this={documentsElement}
|
||||
bind:prompt
|
||||
on:select={(e) => {
|
||||
console.log(e);
|
||||
files = [
|
||||
...files,
|
||||
{
|
||||
type: 'doc',
|
||||
...e.detail,
|
||||
upload_status: true
|
||||
}
|
||||
];
|
||||
}}
|
||||
/>
|
||||
{:else if messages.length == 0 && suggestionPrompts.length !== 0}
|
||||
<Suggestions {suggestionPrompts} {submitPrompt} />
|
||||
{/if}
|
||||
|
@ -256,14 +264,7 @@
|
|||
const file = inputFiles[0];
|
||||
if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) {
|
||||
reader.readAsDataURL(file);
|
||||
} else if (
|
||||
[
|
||||
'application/pdf',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'text/plain',
|
||||
'text/csv'
|
||||
].includes(file['type'])
|
||||
) {
|
||||
} else if (SUPPORTED_FILE_TYPE.includes(file['type'])) {
|
||||
uploadDoc(file);
|
||||
filesInputElement.value = '';
|
||||
} else {
|
||||
|
@ -448,8 +449,10 @@
|
|||
editButton?.click();
|
||||
}
|
||||
|
||||
if (prompt.charAt(0) === '/' && e.key === 'ArrowUp') {
|
||||
promptsElement.selectUp();
|
||||
if (['/', '#'].includes(prompt.charAt(0)) && e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
|
||||
(promptsElement || documentsElement).selectUp();
|
||||
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
|
@ -457,8 +460,10 @@
|
|||
commandOptionButton.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
|
||||
if (prompt.charAt(0) === '/' && e.key === 'ArrowDown') {
|
||||
promptsElement.selectDown();
|
||||
if (['/', '#'].includes(prompt.charAt(0)) && e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
|
||||
(promptsElement || documentsElement).selectDown();
|
||||
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
|
@ -466,7 +471,7 @@
|
|||
commandOptionButton.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
|
||||
if (prompt.charAt(0) === '/' && e.key === 'Enter') {
|
||||
if (['/', '#'].includes(prompt.charAt(0)) && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
|
||||
const commandOptionButton = [
|
||||
|
@ -476,7 +481,7 @@
|
|||
commandOptionButton?.click();
|
||||
}
|
||||
|
||||
if (prompt.charAt(0) === '/' && e.key === 'Tab') {
|
||||
if (['/', '#'].includes(prompt.charAt(0)) && e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
|
||||
const commandOptionButton = [
|
||||
|
|
78
src/lib/components/chat/MessageInput/Documents.svelte
Normal file
78
src/lib/components/chat/MessageInput/Documents.svelte
Normal file
|
@ -0,0 +1,78 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import { documents } from '$lib/stores';
|
||||
import { removeFirstHashWord } from '$lib/utils';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
export let prompt = '';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let selectedIdx = 0;
|
||||
let filteredDocs = [];
|
||||
|
||||
$: filteredDocs = $documents
|
||||
.filter((p) => p.name.includes(prompt.split(' ')?.at(0)?.substring(1) ?? ''))
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
$: if (prompt) {
|
||||
selectedIdx = 0;
|
||||
}
|
||||
|
||||
export const selectUp = () => {
|
||||
selectedIdx = Math.max(0, selectedIdx - 1);
|
||||
};
|
||||
|
||||
export const selectDown = () => {
|
||||
selectedIdx = Math.min(selectedIdx + 1, filteredDocs.length - 1);
|
||||
};
|
||||
|
||||
const confirmSelect = async (doc) => {
|
||||
dispatch('select', doc);
|
||||
|
||||
prompt = removeFirstHashWord(prompt);
|
||||
const chatInputElement = document.getElementById('chat-textarea');
|
||||
|
||||
await tick();
|
||||
chatInputElement?.focus();
|
||||
await tick();
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if filteredDocs.length > 0}
|
||||
<div class="md:px-2 mb-3 text-left w-full">
|
||||
<div class="flex w-full rounded-lg border border-gray-100 dark:border-gray-700">
|
||||
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-lg text-center">
|
||||
<div class=" text-lg font-semibold mt-2">#</div>
|
||||
</div>
|
||||
|
||||
<div class="max-h-60 flex flex-col w-full rounded-r-lg">
|
||||
<div class=" overflow-y-auto bg-white p-2 rounded-tr-lg space-y-0.5">
|
||||
{#each filteredDocs as doc, docIdx}
|
||||
<button
|
||||
class=" px-3 py-1.5 rounded-lg w-full text-left {docIdx === selectedIdx
|
||||
? ' bg-gray-100 selected-command-option-button'
|
||||
: ''}"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
confirmSelect(doc);
|
||||
}}
|
||||
on:mousemove={() => {
|
||||
selectedIdx = docIdx;
|
||||
}}
|
||||
on:focus={() => {}}
|
||||
>
|
||||
<div class=" font-medium text-black line-clamp-1">
|
||||
#{doc.name} ({doc.filename})
|
||||
</div>
|
||||
|
||||
<div class=" text-xs text-gray-600 line-clamp-1">
|
||||
{doc.title}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
|
@ -11,6 +11,13 @@ export const WEB_UI_VERSION = 'v1.0.0-alpha-static';
|
|||
|
||||
export const REQUIRED_OLLAMA_VERSION = '0.1.16';
|
||||
|
||||
export const SUPPORTED_FILE_TYPE = [
|
||||
'application/pdf',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'text/plain',
|
||||
'text/csv'
|
||||
];
|
||||
|
||||
// Source: https://kit.svelte.dev/docs/modules#$env-static-public
|
||||
// This feature, akin to $env/static/private, exclusively incorporates environment variables
|
||||
// that are prefixed with config.kit.env.publicPrefix (usually set to PUBLIC_).
|
||||
|
|
|
@ -11,8 +11,23 @@ export const chatId = writable('');
|
|||
|
||||
export const chats = writable([]);
|
||||
export const models = writable([]);
|
||||
|
||||
export const modelfiles = writable([]);
|
||||
export const prompts = writable([]);
|
||||
export const documents = writable([
|
||||
{
|
||||
collection_name: 'collection_name',
|
||||
filename: 'filename',
|
||||
name: 'name',
|
||||
title: 'title'
|
||||
},
|
||||
{
|
||||
collection_name: 'collection_name1',
|
||||
filename: 'filename1',
|
||||
name: 'name1',
|
||||
title: 'title1'
|
||||
}
|
||||
]);
|
||||
|
||||
export const settings = writable({});
|
||||
export const showSettings = writable(false);
|
||||
|
|
|
@ -128,6 +128,24 @@ export const findWordIndices = (text) => {
|
|||
return matches;
|
||||
};
|
||||
|
||||
export const removeFirstHashWord = (inputString) => {
|
||||
// Split the string into an array of words
|
||||
const words = inputString.split(' ');
|
||||
|
||||
// Find the index of the first word that starts with #
|
||||
const index = words.findIndex((word) => word.startsWith('#'));
|
||||
|
||||
// Remove the first word with #
|
||||
if (index !== -1) {
|
||||
words.splice(index, 1);
|
||||
}
|
||||
|
||||
// Join the remaining words back into a string
|
||||
const resultString = words.join(' ');
|
||||
|
||||
return resultString;
|
||||
};
|
||||
|
||||
export const calculateSHA256 = async (file) => {
|
||||
// Create a FileReader to read the file asynchronously
|
||||
const reader = new FileReader();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue