Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
chenpangpang
open-webui
Commits
fef4725d
Commit
fef4725d
authored
Jan 07, 2024
by
Timothy J. Baek
Browse files
feat: frontend file upload support
parent
3229ec11
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
223 additions
and
92 deletions
+223
-92
backend/apps/rag/main.py
backend/apps/rag/main.py
+2
-2
src/lib/apis/rag/index.ts
src/lib/apis/rag/index.ts
+0
-3
src/lib/components/chat/MessageInput.svelte
src/lib/components/chat/MessageInput.svelte
+63
-14
src/lib/utils/index.ts
src/lib/utils/index.ts
+35
-0
src/lib/utils/rag/index.ts
src/lib/utils/rag/index.ts
+20
-0
src/routes/(app)/+page.svelte
src/routes/(app)/+page.svelte
+103
-73
No files found.
backend/apps/rag/main.py
View file @
fef4725d
...
@@ -91,7 +91,7 @@ def store_web(form_data: StoreWebForm):
...
@@ -91,7 +91,7 @@ def store_web(form_data: StoreWebForm):
loader
=
WebBaseLoader
(
form_data
.
url
)
loader
=
WebBaseLoader
(
form_data
.
url
)
data
=
loader
.
load
()
data
=
loader
.
load
()
store_data_in_vector_db
(
data
,
form_data
.
collection_name
)
store_data_in_vector_db
(
data
,
form_data
.
collection_name
)
return
{
"status"
:
True
}
return
{
"status"
:
True
,
"collection_name"
:
form_data
.
collection_name
}
except
Exception
as
e
:
except
Exception
as
e
:
print
(
e
)
print
(
e
)
raise
HTTPException
(
raise
HTTPException
(
...
@@ -129,7 +129,7 @@ def store_doc(collection_name: str = Form(...), file: UploadFile = File(...)):
...
@@ -129,7 +129,7 @@ def store_doc(collection_name: str = Form(...), file: UploadFile = File(...)):
data
=
loader
.
load
()
data
=
loader
.
load
()
store_data_in_vector_db
(
data
,
collection_name
)
store_data_in_vector_db
(
data
,
collection_name
)
return
{
"status"
:
True
}
return
{
"status"
:
True
,
"collection_name"
:
collection_name
}
except
Exception
as
e
:
except
Exception
as
e
:
print
(
e
)
print
(
e
)
raise
HTTPException
(
raise
HTTPException
(
...
...
src/lib/apis/rag/index.ts
View file @
fef4725d
...
@@ -11,7 +11,6 @@ export const uploadDocToVectorDB = async (token: string, collection_name: string
...
@@ -11,7 +11,6 @@ export const uploadDocToVectorDB = async (token: string, collection_name: string
method
:
'
POST
'
,
method
:
'
POST
'
,
headers
:
{
headers
:
{
Accept
:
'
application/json
'
,
Accept
:
'
application/json
'
,
'
Content-Type
'
:
'
application/json
'
,
authorization
:
`Bearer
${
token
}
`
authorization
:
`Bearer
${
token
}
`
},
},
body
:
data
body
:
data
...
@@ -85,7 +84,6 @@ export const queryVectorDB = async (
...
@@ -85,7 +84,6 @@ export const queryVectorDB = async (
method
:
'
GET
'
,
method
:
'
GET
'
,
headers
:
{
headers
:
{
Accept
:
'
application/json
'
,
Accept
:
'
application/json
'
,
'
Content-Type
'
:
'
application/json
'
,
authorization
:
`Bearer
${
token
}
`
authorization
:
`Bearer
${
token
}
`
}
}
}
}
...
@@ -96,7 +94,6 @@ export const queryVectorDB = async (
...
@@ -96,7 +94,6 @@ export const queryVectorDB = async (
})
})
.
catch
((
err
)
=>
{
.
catch
((
err
)
=>
{
error
=
err
.
detail
;
error
=
err
.
detail
;
console
.
log
(
err
);
return
null
;
return
null
;
});
});
...
...
src/lib/components/chat/MessageInput.svelte
View file @
fef4725d
...
@@ -2,10 +2,11 @@
...
@@ -2,10 +2,11 @@
import toast from 'svelte-french-toast';
import toast from 'svelte-french-toast';
import { onMount, tick } from 'svelte';
import { onMount, tick } from 'svelte';
import { settings } from '$lib/stores';
import { settings } from '$lib/stores';
import { findWordIndices } from '$lib/utils';
import {
calculateSHA256,
findWordIndices } from '$lib/utils';
import Prompts from './MessageInput/PromptCommands.svelte';
import Prompts from './MessageInput/PromptCommands.svelte';
import Suggestions from './MessageInput/Suggestions.svelte';
import Suggestions from './MessageInput/Suggestions.svelte';
import { uploadDocToVectorDB } from '$lib/apis/rag';
export let submitPrompt: Function;
export let submitPrompt: Function;
export let stopResponse: Function;
export let stopResponse: Function;
...
@@ -98,7 +99,7 @@
...
@@ -98,7 +99,7 @@
dragged = true;
dragged = true;
});
});
dropZone.addEventListener('drop', (e) => {
dropZone.addEventListener('drop',
async
(e) => {
e.preventDefault();
e.preventDefault();
console.log(e);
console.log(e);
...
@@ -115,14 +116,30 @@
...
@@ -115,14 +116,30 @@
];
];
};
};
if (
if (e.dataTransfer?.files && e.dataTransfer?.files.length > 0) {
e.dataTransfer?.files &&
const file = e.dataTransfer?.files[0];
e.dataTransfer?.files.length > 0 &&
if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) {
['image/gif', 'image/jpeg', 'image/png'].includes(e.dataTransfer?.files[0]['type'])
reader.readAsDataURL(file);
) {
} else if (['application/pdf', 'text/plain'].includes(file['type'])) {
reader.readAsDataURL(e.dataTransfer?.files[0]);
console.log(file);
const hash = await calculateSHA256(file);
// const res = uploadDocToVectorDB(localStorage.token, hash,file);
if (true) {
files = [
...files,
{
type: 'doc',
name: file.name,
collection_name: hash
}
];
}
} else {
toast.error(`Unsupported File Type '${file['type']}'.`);
}
} else {
} else {
toast.error(`
Unsupported File Type '${e.dataTransfer?.files[0]['type']}'
.`);
toast.error(`
File not found
.`);
}
}
}
}
...
@@ -145,11 +162,11 @@
...
@@ -145,11 +162,11 @@
<div class="absolute rounded-xl w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
<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="m-auto pt-64 flex flex-col justify-center">
<div class="max-w-md">
<div class="max-w-md">
<div class=" text-center text-6xl mb-3">
🏞
️</div>
<div class=" text-center text-6xl mb-3">
🗂
️</div>
<div class="text-center dark:text-white text-2xl font-semibold z-50">Add
Imag
es</div>
<div class="text-center dark:text-white text-2xl font-semibold z-50">Add
Fil
es</div>
<div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
<div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
Drop any images here to add to the conversation
Drop any
files/
images here to add to the conversation
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -237,10 +254,42 @@
...
@@ -237,10 +254,42 @@
}}
}}
>
>
{#if files.length > 0}
{#if files.length > 0}
<div class="m
l
-2 mt-2 mb-1 flex
space-x
-2">
<div class="m
x
-2 mt-2 mb-1 flex
flex-wrap gap
-2">
{#each files as file, fileIdx}
{#each files as file, fileIdx}
<div class=" relative group">
<div class=" relative group">
<img src={file.url} alt="input" class=" h-16 w-16 rounded-xl object-cover" />
{#if file.type === 'image'}
<img src={file.url} alt="input" class=" h-16 w-16 rounded-xl object-cover" />
{:else if file.type === 'doc'}
<div
class="h-16 w-[15rem] flex items-center space-x-3 px-2 bg-gray-600 rounded-xl"
>
<div class="p-2.5 bg-red-400 rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-6 h-6"
>
<path
fill-rule="evenodd"
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
clip-rule="evenodd"
/>
<path
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
/>
</svg>
</div>
<div class="flex flex-col justify-center -space-y-0.5">
<div class=" text-gray-100 text-sm line-clamp-1">
{file.name}
</div>
<div class=" text-gray-500 text-sm">Document</div>
</div>
</div>
{/if}
<div class=" absolute -top-1 -right-1">
<div class=" absolute -top-1 -right-1">
<button
<button
...
...
src/lib/utils/index.ts
View file @
fef4725d
...
@@ -127,3 +127,38 @@ export const findWordIndices = (text) => {
...
@@ -127,3 +127,38 @@ export const findWordIndices = (text) => {
return
matches
;
return
matches
;
};
};
export
const
calculateSHA256
=
async
(
file
)
=>
{
console
.
log
(
file
);
// Create a FileReader to read the file asynchronously
const
reader
=
new
FileReader
();
// Define a promise to handle the file reading
const
readFile
=
new
Promise
((
resolve
,
reject
)
=>
{
reader
.
onload
=
()
=>
resolve
(
reader
.
result
);
reader
.
onerror
=
reject
;
});
// Read the file as an ArrayBuffer
reader
.
readAsArrayBuffer
(
file
);
try
{
// Wait for the FileReader to finish reading the file
const
buffer
=
await
readFile
;
// Convert the ArrayBuffer to a Uint8Array
const
uint8Array
=
new
Uint8Array
(
buffer
);
// Calculate the SHA-256 hash using Web Crypto API
const
hashBuffer
=
await
crypto
.
subtle
.
digest
(
'
SHA-256
'
,
uint8Array
);
// Convert the hash to a hexadecimal string
const
hashArray
=
Array
.
from
(
new
Uint8Array
(
hashBuffer
));
const
hashHex
=
hashArray
.
map
((
byte
)
=>
byte
.
toString
(
16
).
padStart
(
2
,
'
0
'
)).
join
(
''
);
return
`sha256:
${
hashHex
}
`
;
}
catch
(
error
)
{
console
.
error
(
'
Error calculating SHA-256 hash:
'
,
error
);
throw
error
;
}
};
src/lib/utils/rag/index.ts
0 → 100644
View file @
fef4725d
export
const
RAGTemplate
=
(
context
:
string
,
query
:
string
)
=>
{
let
template
=
`Use the following context as your learned knowledge, inside <context></context> XML tags.
<context>
[context]
</context>
When answer to user:
- If you don't know, just say that you don't know.
- If you don't know when you are not sure, ask for clarification.
Avoid mentioning that you obtained the information from the context.
And answer according to the language of the user's question.
Given the context information, answer the query.
Query: [query]`
;
template
=
template
.
replace
(
/
\[
context
\]
/g
,
context
);
template
=
template
.
replace
(
/
\[
query
\]
/g
,
query
);
return
template
;
};
src/routes/(app)/+page.svelte
View file @
fef4725d
...
@@ -7,16 +7,18 @@
...
@@ -7,16 +7,18 @@
import { page } from '$app/stores';
import { page } from '$app/stores';
import { models, modelfiles, user, settings, chats, chatId, config } from '$lib/stores';
import { models, modelfiles, user, settings, chats, chatId, config } from '$lib/stores';
import { copyToClipboard, splitStream } from '$lib/utils';
import { generateChatCompletion, generateTitle } from '$lib/apis/ollama';
import { generateChatCompletion, generateTitle } from '$lib/apis/ollama';
import { copyToClipboard, splitStream } from '$lib/utils';
import { createNewChat, getChatList, updateChatById } from '$lib/apis/chats';
import { queryVectorDB } from '$lib/apis/rag';
import { generateOpenAIChatCompletion } from '$lib/apis/openai';
import MessageInput from '$lib/components/chat/MessageInput.svelte';
import MessageInput from '$lib/components/chat/MessageInput.svelte';
import Messages from '$lib/components/chat/Messages.svelte';
import Messages from '$lib/components/chat/Messages.svelte';
import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
import Navbar from '$lib/components/layout/Navbar.svelte';
import Navbar from '$lib/components/layout/Navbar.svelte';
import { createNewChat, getChatList, updateChatById } from '$lib/apis/chats';
import { RAGTemplate } from '$lib/utils/rag';
import { generateOpenAIChatCompletion } from '$lib/apis/openai';
let stopResponseFlag = false;
let stopResponseFlag = false;
let autoScroll = true;
let autoScroll = true;
...
@@ -113,8 +115,103 @@
...
@@ -113,8 +115,103 @@
// Ollama functions
// Ollama functions
//////////////////////////
//////////////////////////
const submitPrompt = async (userPrompt) => {
console.log('submitPrompt', $chatId);
if (selectedModels.includes('')) {
toast.error('Model not selected');
} else if (messages.length != 0 && messages.at(-1).done != true) {
// Response not done
console.log('wait');
} else {
// Reset chat message textarea height
document.getElementById('chat-textarea').style.height = '';
// Create user message
let userMessageId = uuidv4();
let userMessage = {
id: userMessageId,
parentId: messages.length !== 0 ? messages.at(-1).id : null,
childrenIds: [],
role: 'user',
content: userPrompt,
files: files.length > 0 ? files : undefined
};
// Add message to history and Set currentId to messageId
history.messages[userMessageId] = userMessage;
history.currentId = userMessageId;
// Append messageId to childrenIds of parent message
if (messages.length !== 0) {
history.messages[messages.at(-1).id].childrenIds.push(userMessageId);
}
// Wait until history/message have been updated
await tick();
// Create new chat if only one message in messages
if (messages.length == 1) {
if ($settings.saveChatHistory ?? true) {
chat = await createNewChat(localStorage.token, {
id: $chatId,
title: 'New Chat',
models: selectedModels,
system: $settings.system ?? undefined,
options: {
...($settings.options ?? {})
},
messages: messages,
history: history,
timestamp: Date.now()
});
await chats.set(await getChatList(localStorage.token));
await chatId.set(chat.id);
} else {
await chatId.set('local');
}
await tick();
}
// Reset chat input textarea
prompt = '';
files = [];
// Send prompt
await sendPrompt(userPrompt, userMessageId);
}
};
const sendPrompt = async (prompt, parentId) => {
const sendPrompt = async (prompt, parentId) => {
const _chatId = JSON.parse(JSON.stringify($chatId));
const _chatId = JSON.parse(JSON.stringify($chatId));
// TODO: update below to include all ancestral files
const docs = history.messages[parentId].files.filter((item) => item.type === 'file');
if (docs.length > 0) {
const query = history.messages[parentId].content;
let relevantContexts = await Promise.all(
docs.map(async (doc) => {
return await queryVectorDB(localStorage.token, doc.collection_name, query, 4).catch(
(error) => {
console.log(error);
return null;
}
);
})
);
relevantContexts = relevantContexts.filter((context) => context);
const contextString = relevantContexts.reduce((a, context, i, arr) => {
return `${a}${context.documents.join(' ')}\n`;
}, '');
history.messages[parentId].raContent = RAGTemplate(contextString, query);
history.messages[parentId].contexts = relevantContexts;
await tick();
}
await Promise.all(
await Promise.all(
selectedModels.map(async (model) => {
selectedModels.map(async (model) => {
console.log(model);
console.log(model);
...
@@ -177,7 +274,7 @@
...
@@ -177,7 +274,7 @@
.filter((message) => message)
.filter((message) => message)
.map((message) => ({
.map((message) => ({
role: message.role,
role: message.role,
content: message.content,
content:
message?.raContent ??
message.content,
...(message.files && {
...(message.files && {
images: message.files
images: message.files
.filter((file) => file.type === 'image')
.filter((file) => file.type === 'image')
...
@@ -366,7 +463,7 @@
...
@@ -366,7 +463,7 @@
content: [
content: [
{
{
type: 'text',
type: 'text',
text: message.content
text:
message?.raContent ??
message.content
},
},
...message.files
...message.files
.filter((file) => file.type === 'image')
.filter((file) => file.type === 'image')
...
@@ -378,7 +475,7 @@
...
@@ -378,7 +475,7 @@
}))
}))
]
]
}
}
: { content: message.content })
: { content:
message?.raContent ??
message.content })
})),
})),
seed: $settings?.options?.seed ?? undefined,
seed: $settings?.options?.seed ?? undefined,
stop: $settings?.options?.stop ?? undefined,
stop: $settings?.options?.stop ?? undefined,
...
@@ -494,73 +591,6 @@
...
@@ -494,73 +591,6 @@
}
}
};
};
const submitPrompt = async (userPrompt) => {
console.log('submitPrompt', $chatId);
if (selectedModels.includes('')) {
toast.error('Model not selected');
} else if (messages.length != 0 && messages.at(-1).done != true) {
// Response not done
console.log('wait');
} else {
// Reset chat message textarea height
document.getElementById('chat-textarea').style.height = '';
// Create user message
let userMessageId = uuidv4();
let userMessage = {
id: userMessageId,
parentId: messages.length !== 0 ? messages.at(-1).id : null,
childrenIds: [],
role: 'user',
content: userPrompt,
files: files.length > 0 ? files : undefined
};
// Add message to history and Set currentId to messageId
history.messages[userMessageId] = userMessage;
history.currentId = userMessageId;
// Append messageId to childrenIds of parent message
if (messages.length !== 0) {
history.messages[messages.at(-1).id].childrenIds.push(userMessageId);
}
// Wait until history/message have been updated
await tick();
// Create new chat if only one message in messages
if (messages.length == 1) {
if ($settings.saveChatHistory ?? true) {
chat = await createNewChat(localStorage.token, {
id: $chatId,
title: 'New Chat',
models: selectedModels,
system: $settings.system ?? undefined,
options: {
...($settings.options ?? {})
},
messages: messages,
history: history,
timestamp: Date.now()
});
await chats.set(await getChatList(localStorage.token));
await chatId.set(chat.id);
} else {
await chatId.set('local');
}
await tick();
}
// Reset chat input textarea
prompt = '';
files = [];
// Send prompt
await sendPrompt(userPrompt, userMessageId);
}
};
const stopResponse = () => {
const stopResponse = () => {
stopResponseFlag = true;
stopResponseFlag = true;
console.log('stopResponse');
console.log('stopResponse');
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment