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
cc3f84f9
Commit
cc3f84f9
authored
Jan 07, 2024
by
Timothy J. Baek
Browse files
feat: # to import doc
parent
2603ac30
Changes
11
Show whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
894 additions
and
32 deletions
+894
-32
backend/apps/web/main.py
backend/apps/web/main.py
+12
-4
backend/apps/web/models/documents.py
backend/apps/web/models/documents.py
+123
-0
backend/apps/web/routers/documents.py
backend/apps/web/routers/documents.py
+119
-0
src/lib/apis/documents/index.ts
src/lib/apis/documents/index.ts
+177
-0
src/lib/components/AddFilesPlaceholder.svelte
src/lib/components/AddFilesPlaceholder.svelte
+6
-0
src/lib/components/chat/MessageInput.svelte
src/lib/components/chat/MessageInput.svelte
+33
-28
src/lib/components/chat/MessageInput/Documents.svelte
src/lib/components/chat/MessageInput/Documents.svelte
+78
-0
src/lib/constants.ts
src/lib/constants.ts
+7
-0
src/lib/stores/index.ts
src/lib/stores/index.ts
+15
-0
src/lib/utils/index.ts
src/lib/utils/index.ts
+18
-0
src/routes/(app)/documents/+page.svelte
src/routes/(app)/documents/+page.svelte
+306
-0
No files found.
backend/apps/web/main.py
View file @
cc3f84f9
from
fastapi
import
FastAPI
,
Depends
from
fastapi
import
FastAPI
,
Depends
from
fastapi.routing
import
APIRoute
from
fastapi.routing
import
APIRoute
from
fastapi.middleware.cors
import
CORSMiddleware
from
fastapi.middleware.cors
import
CORSMiddleware
from
apps.web.routers
import
auths
,
users
,
chats
,
modelfiles
,
prompts
,
configs
,
utils
from
apps.web.routers
import
(
auths
,
users
,
chats
,
documents
,
modelfiles
,
prompts
,
configs
,
utils
,
)
from
config
import
WEBUI_VERSION
,
WEBUI_AUTH
from
config
import
WEBUI_VERSION
,
WEBUI_AUTH
app
=
FastAPI
()
app
=
FastAPI
()
...
@@ -22,9 +31,8 @@ app.add_middleware(
...
@@ -22,9 +31,8 @@ app.add_middleware(
app
.
include_router
(
auths
.
router
,
prefix
=
"/auths"
,
tags
=
[
"auths"
])
app
.
include_router
(
auths
.
router
,
prefix
=
"/auths"
,
tags
=
[
"auths"
])
app
.
include_router
(
users
.
router
,
prefix
=
"/users"
,
tags
=
[
"users"
])
app
.
include_router
(
users
.
router
,
prefix
=
"/users"
,
tags
=
[
"users"
])
app
.
include_router
(
chats
.
router
,
prefix
=
"/chats"
,
tags
=
[
"chats"
])
app
.
include_router
(
chats
.
router
,
prefix
=
"/chats"
,
tags
=
[
"chats"
])
app
.
include_router
(
modelfiles
.
router
,
app
.
include_router
(
documents
.
router
,
prefix
=
"/documents"
,
tags
=
[
"documents"
])
prefix
=
"/modelfiles"
,
app
.
include_router
(
modelfiles
.
router
,
prefix
=
"/modelfiles"
,
tags
=
[
"modelfiles"
])
tags
=
[
"modelfiles"
])
app
.
include_router
(
prompts
.
router
,
prefix
=
"/prompts"
,
tags
=
[
"prompts"
])
app
.
include_router
(
prompts
.
router
,
prefix
=
"/prompts"
,
tags
=
[
"prompts"
])
app
.
include_router
(
configs
.
router
,
prefix
=
"/configs"
,
tags
=
[
"configs"
])
app
.
include_router
(
configs
.
router
,
prefix
=
"/configs"
,
tags
=
[
"configs"
])
...
...
backend/apps/web/models/documents.py
0 → 100644
View file @
cc3f84f9
from
pydantic
import
BaseModel
from
peewee
import
*
from
playhouse.shortcuts
import
model_to_dict
from
typing
import
List
,
Union
,
Optional
import
time
from
utils.utils
import
decode_token
from
utils.misc
import
get_gravatar_url
from
apps.web.internal.db
import
DB
import
json
####################
# Documents DB Schema
####################
class
Document
(
Model
):
collection_name
=
CharField
(
unique
=
True
)
name
=
CharField
(
unique
=
True
)
title
=
CharField
()
filename
=
CharField
()
content
=
TextField
(
null
=
True
)
user_id
=
CharField
()
timestamp
=
DateField
()
class
Meta
:
database
=
DB
class
DocumentModel
(
BaseModel
):
collection_name
:
str
name
:
str
title
:
str
filename
:
str
content
:
Optional
[
str
]
=
None
user_id
:
str
timestamp
:
int
# timestamp in epoch
####################
# Forms
####################
class
DocumentUpdateForm
(
BaseModel
):
name
:
str
title
:
str
class
DocumentForm
(
DocumentUpdateForm
):
collection_name
:
str
filename
:
str
content
:
Optional
[
str
]
=
None
class
DocumentsTable
:
def
__init__
(
self
,
db
):
self
.
db
=
db
self
.
db
.
create_tables
([
Document
])
def
insert_new_doc
(
self
,
user_id
:
str
,
form_data
:
DocumentForm
)
->
Optional
[
DocumentModel
]:
document
=
DocumentModel
(
**
{
**
form_data
.
model_dump
(),
"user_id"
:
user_id
,
"timestamp"
:
int
(
time
.
time
()),
}
)
try
:
result
=
Document
.
create
(
**
document
.
model_dump
())
if
result
:
return
document
else
:
return
None
except
:
return
None
def
get_doc_by_name
(
self
,
name
:
str
)
->
Optional
[
DocumentModel
]:
try
:
document
=
Document
.
get
(
Document
.
name
==
name
)
return
DocumentModel
(
**
model_to_dict
(
document
))
except
:
return
None
def
get_docs
(
self
)
->
List
[
DocumentModel
]:
return
[
DocumentModel
(
**
model_to_dict
(
doc
))
for
doc
in
Document
.
select
()
# .limit(limit).offset(skip)
]
def
update_doc_by_name
(
self
,
name
:
str
,
form_data
:
DocumentUpdateForm
)
->
Optional
[
DocumentModel
]:
try
:
query
=
Document
.
update
(
title
=
form_data
.
title
,
name
=
form_data
.
name
,
timestamp
=
int
(
time
.
time
()),
).
where
(
Document
.
name
==
name
)
query
.
execute
()
doc
=
Document
.
get
(
Document
.
name
==
name
)
return
DocumentModel
(
**
model_to_dict
(
doc
))
except
:
return
None
def
delete_doc_by_name
(
self
,
name
:
str
)
->
bool
:
try
:
query
=
Document
.
delete
().
where
((
Document
.
name
==
name
))
query
.
execute
()
# Remove the rows, return number of rows removed.
return
True
except
:
return
False
Documents
=
DocumentsTable
(
DB
)
backend/apps/web/routers/documents.py
0 → 100644
View file @
cc3f84f9
from
fastapi
import
Depends
,
FastAPI
,
HTTPException
,
status
from
datetime
import
datetime
,
timedelta
from
typing
import
List
,
Union
,
Optional
from
fastapi
import
APIRouter
from
pydantic
import
BaseModel
import
json
from
apps.web.models.documents
import
(
Documents
,
DocumentForm
,
DocumentUpdateForm
,
DocumentModel
,
)
from
utils.utils
import
get_current_user
from
constants
import
ERROR_MESSAGES
router
=
APIRouter
()
############################
# GetDocuments
############################
@
router
.
get
(
"/"
,
response_model
=
List
[
DocumentModel
])
async
def
get_documents
(
user
=
Depends
(
get_current_user
)):
return
Documents
.
get_docs
()
############################
# CreateNewDoc
############################
@
router
.
post
(
"/create"
,
response_model
=
Optional
[
DocumentModel
])
async
def
create_new_doc
(
form_data
:
DocumentForm
,
user
=
Depends
(
get_current_user
)):
if
user
.
role
!=
"admin"
:
raise
HTTPException
(
status_code
=
status
.
HTTP_401_UNAUTHORIZED
,
detail
=
ERROR_MESSAGES
.
ACCESS_PROHIBITED
,
)
doc
=
Documents
.
get_doc_by_name
(
form_data
.
name
)
if
doc
==
None
:
doc
=
Documents
.
insert_new_doc
(
user
.
id
,
form_data
)
if
doc
:
return
doc
else
:
raise
HTTPException
(
status_code
=
status
.
HTTP_401_UNAUTHORIZED
,
detail
=
ERROR_MESSAGES
.
DEFAULT
(),
)
else
:
raise
HTTPException
(
status_code
=
status
.
HTTP_400_BAD_REQUEST
,
detail
=
ERROR_MESSAGES
.
COMMAND_TAKEN
,
)
############################
# GetDocByName
############################
@
router
.
get
(
"/name/{name}"
,
response_model
=
Optional
[
DocumentModel
])
async
def
get_doc_by_name
(
name
:
str
,
user
=
Depends
(
get_current_user
)):
doc
=
Documents
.
get_doc_by_name
(
name
)
if
doc
:
return
doc
else
:
raise
HTTPException
(
status_code
=
status
.
HTTP_401_UNAUTHORIZED
,
detail
=
ERROR_MESSAGES
.
NOT_FOUND
,
)
############################
# UpdateDocByName
############################
@
router
.
post
(
"/name/{name}/update"
,
response_model
=
Optional
[
DocumentModel
])
async
def
update_doc_by_name
(
name
:
str
,
form_data
:
DocumentUpdateForm
,
user
=
Depends
(
get_current_user
)
):
if
user
.
role
!=
"admin"
:
raise
HTTPException
(
status_code
=
status
.
HTTP_401_UNAUTHORIZED
,
detail
=
ERROR_MESSAGES
.
ACCESS_PROHIBITED
,
)
doc
=
Documents
.
update_doc_by_name
(
name
,
form_data
)
if
doc
:
return
doc
else
:
raise
HTTPException
(
status_code
=
status
.
HTTP_401_UNAUTHORIZED
,
detail
=
ERROR_MESSAGES
.
ACCESS_PROHIBITED
,
)
############################
# DeleteDocByName
############################
@
router
.
delete
(
"/name/{name}/delete"
,
response_model
=
bool
)
async
def
delete_doc_by_name
(
name
:
str
,
user
=
Depends
(
get_current_user
)):
if
user
.
role
!=
"admin"
:
raise
HTTPException
(
status_code
=
status
.
HTTP_401_UNAUTHORIZED
,
detail
=
ERROR_MESSAGES
.
ACCESS_PROHIBITED
,
)
result
=
Documents
.
delete_doc_by_name
(
name
)
return
result
src/lib/apis/documents/index.ts
0 → 100644
View file @
cc3f84f9
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
;
};
src/lib/components/AddFilesPlaceholder.svelte
0 → 100644
View file @
cc3f84f9
<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>
src/lib/components/chat/MessageInput.svelte
View file @
cc3f84f9
...
@@ -7,6 +7,9 @@
...
@@ -7,6 +7,9 @@
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';
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 submitPrompt: Function;
export let stopResponse: Function;
export let stopResponse: Function;
...
@@ -16,6 +19,7 @@
...
@@ -16,6 +19,7 @@
let filesInputElement;
let filesInputElement;
let promptsElement;
let promptsElement;
let documentsElement;
let inputFiles;
let inputFiles;
let dragged = false;
let dragged = false;
...
@@ -143,14 +147,7 @@
...
@@ -143,14 +147,7 @@
const file = inputFiles[0];
const file = inputFiles[0];
if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) {
if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) {
reader.readAsDataURL(file);
reader.readAsDataURL(file);
} else if (
} else if (SUPPORTED_FILE_TYPE.includes(file['type'])) {
[
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain',
'text/csv'
].includes(file['type'])
) {
uploadDoc(file);
uploadDoc(file);
} else {
} else {
toast.error(`Unsupported File Type '${file['type']}'.`);
toast.error(`Unsupported File Type '${file['type']}'.`);
...
@@ -179,12 +176,7 @@
...
@@ -179,12 +176,7 @@
<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>
<AddFilesPlaceholder />
<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>
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -224,6 +216,22 @@
...
@@ -224,6 +216,22 @@
<div class="w-full">
<div class="w-full">
{#if prompt.charAt(0) === '/'}
{#if prompt.charAt(0) === '/'}
<Prompts bind:this={promptsElement} bind:prompt />
<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}
{:else if messages.length == 0 && suggestionPrompts.length !== 0}
<Suggestions {suggestionPrompts} {submitPrompt} />
<Suggestions {suggestionPrompts} {submitPrompt} />
{/if}
{/if}
...
@@ -256,14 +264,7 @@
...
@@ -256,14 +264,7 @@
const file = inputFiles[0];
const file = inputFiles[0];
if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) {
if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) {
reader.readAsDataURL(file);
reader.readAsDataURL(file);
} else if (
} else if (SUPPORTED_FILE_TYPE.includes(file['type'])) {
[
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain',
'text/csv'
].includes(file['type'])
) {
uploadDoc(file);
uploadDoc(file);
filesInputElement.value = '';
filesInputElement.value = '';
} else {
} else {
...
@@ -448,8 +449,10 @@
...
@@ -448,8 +449,10 @@
editButton?.click();
editButton?.click();
}
}
if (prompt.charAt(0) === '/' && e.key === 'ArrowUp') {
if (['/', '#'].includes(prompt.charAt(0)) && e.key === 'ArrowUp') {
promptsElement.selectUp();
e.preventDefault();
(promptsElement || documentsElement).selectUp();
const commandOptionButton = [
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
...document.getElementsByClassName('selected-command-option-button')
...
@@ -457,8 +460,10 @@
...
@@ -457,8 +460,10 @@
commandOptionButton.scrollIntoView({ block: 'center' });
commandOptionButton.scrollIntoView({ block: 'center' });
}
}
if (prompt.charAt(0) === '/' && e.key === 'ArrowDown') {
if (['/', '#'].includes(prompt.charAt(0)) && e.key === 'ArrowDown') {
promptsElement.selectDown();
e.preventDefault();
(promptsElement || documentsElement).selectDown();
const commandOptionButton = [
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
...document.getElementsByClassName('selected-command-option-button')
...
@@ -466,7 +471,7 @@
...
@@ -466,7 +471,7 @@
commandOptionButton.scrollIntoView({ block: 'center' });
commandOptionButton.scrollIntoView({ block: 'center' });
}
}
if (prompt.charAt(0)
=== '/'
&& e.key === 'Enter') {
if
(['/', '#'].includes
(prompt.charAt(0)
)
&& e.key === 'Enter') {
e.preventDefault();
e.preventDefault();
const commandOptionButton = [
const commandOptionButton = [
...
@@ -476,7 +481,7 @@
...
@@ -476,7 +481,7 @@
commandOptionButton?.click();
commandOptionButton?.click();
}
}
if (prompt.charAt(0)
=== '/'
&& e.key === 'Tab') {
if
(['/', '#'].includes
(prompt.charAt(0)
)
&& e.key === 'Tab') {
e.preventDefault();
e.preventDefault();
const commandOptionButton = [
const commandOptionButton = [
...
...
src/lib/components/chat/MessageInput/Documents.svelte
0 → 100644
View file @
cc3f84f9
<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}
src/lib/constants.ts
View file @
cc3f84f9
...
@@ -11,6 +11,13 @@ export const WEB_UI_VERSION = 'v1.0.0-alpha-static';
...
@@ -11,6 +11,13 @@ export const WEB_UI_VERSION = 'v1.0.0-alpha-static';
export
const
REQUIRED_OLLAMA_VERSION
=
'
0.1.16
'
;
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
// Source: https://kit.svelte.dev/docs/modules#$env-static-public
// This feature, akin to $env/static/private, exclusively incorporates environment variables
// This feature, akin to $env/static/private, exclusively incorporates environment variables
// that are prefixed with config.kit.env.publicPrefix (usually set to PUBLIC_).
// that are prefixed with config.kit.env.publicPrefix (usually set to PUBLIC_).
...
...
src/lib/stores/index.ts
View file @
cc3f84f9
...
@@ -11,8 +11,23 @@ export const chatId = writable('');
...
@@ -11,8 +11,23 @@ export const chatId = writable('');
export
const
chats
=
writable
([]);
export
const
chats
=
writable
([]);
export
const
models
=
writable
([]);
export
const
models
=
writable
([]);
export
const
modelfiles
=
writable
([]);
export
const
modelfiles
=
writable
([]);
export
const
prompts
=
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
settings
=
writable
({});
export
const
showSettings
=
writable
(
false
);
export
const
showSettings
=
writable
(
false
);
src/lib/utils/index.ts
View file @
cc3f84f9
...
@@ -128,6 +128,24 @@ export const findWordIndices = (text) => {
...
@@ -128,6 +128,24 @@ export const findWordIndices = (text) => {
return
matches
;
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
)
=>
{
export
const
calculateSHA256
=
async
(
file
)
=>
{
// Create a FileReader to read the file asynchronously
// Create a FileReader to read the file asynchronously
const
reader
=
new
FileReader
();
const
reader
=
new
FileReader
();
...
...
src/routes/(app)/documents/+page.svelte
View file @
cc3f84f9
<script lang="ts">
import toast from 'svelte-french-toast';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { onMount } from 'svelte';
import { documents } from '$lib/stores';
import { createNewDoc, deleteDocByName, getDocs } from '$lib/apis/documents';
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
import { SUPPORTED_FILE_TYPE } from '$lib/constants';
let importFiles = '';
let query = '';
let dragged = false;
const deleteDoc = async (name) => {
await deleteDocByName(localStorage.token, name);
await documents.set(await getDocs(localStorage.token));
};
onMount(() => {
// const dropZone = document.querySelector('body');
const dropZone = document.getElementById('dropzone');
dropZone?.addEventListener('dragover', (e) => {
e.preventDefault();
dragged = true;
});
dropZone?.addEventListener('drop', async (e) => {
e.preventDefault();
console.log(e);
if (e.dataTransfer?.files) {
const inputFiles = e.dataTransfer?.files;
if (inputFiles && inputFiles.length > 0) {
const file = inputFiles[0];
if (SUPPORTED_FILE_TYPE.includes(file['type'])) {
console.log(file);
// uploadDoc(file);
} else {
toast.error(`Unsupported File Type '${file['type']}'.`);
}
} else {
toast.error(`File not found.`);
}
}
});
dropZone?.addEventListener('dragleave', () => {
dragged = false;
});
});
</script>
<div class="min-h-screen w-full flex justify-center dark:text-white">
<div class=" py-2.5 flex flex-col justify-between w-full">
<div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10">
<div class="mb-6 flex justify-between items-center">
<div class=" text-2xl font-semibold self-center">My Documents</div>
</div>
<div class=" flex w-full space-x-2">
<div class="flex flex-1">
<div class=" self-center ml-1 mr-3">
<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="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
clip-rule="evenodd"
/>
</svg>
</div>
<input
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
bind:value={query}
placeholder="Search Document"
/>
</div>
<div>
<a
class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1"
href="/prompts/create"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
/>
</svg>
</a>
</div>
</div>
<div
class="z-50 touch-none pointer-events-none"
id="dropzone"
role="region"
aria-label="Drag and Drop Container"
>
{#if $documents.length === 0 || dragged}
<div class="my-3 py-16 rounded-lg border-2 border-dashed dark:border-gray-600">
<AddFilesPlaceholder />
</div>
{:else}
{#each $documents.filter((p) => query === '' || p.name.includes(query)) as doc}
<hr class=" dark:border-gray-700 my-2.5" />
<div class=" flex space-x-4 cursor-pointer w-full mb-3">
<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
<a href={`/prompts/edit?command=${encodeURIComponent(doc.name)}`}>
<div class=" flex-1 self-center pl-5">
<div class=" font-bold line-clamp-1">#{doc.name} ({doc.filename})</div>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
{doc.title}
</div>
</div>
</a>
</div>
<div class="flex flex-row space-x-1 self-center">
<a
class="self-center w-fit text-sm px-2 py-2 border dark:border-gray-600 rounded-xl"
type="button"
href={`/prompts/edit?command=${encodeURIComponent(doc.name)}`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
/>
</svg>
</a>
<!-- <button
class="self-center w-fit text-sm px-2 py-2 border dark:border-gray-600 rounded-xl"
type="button"
on:click={() => {
sharePrompt(prompt);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z"
/>
</svg>
</button> -->
<button
class="self-center w-fit text-sm px-2 py-2 border dark:border-gray-600 rounded-xl"
type="button"
on:click={() => {
deleteDoc(doc.name);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
</button>
</div>
</div>
{/each}
{/if}
</div>
{#if $documents.length != 0}
<hr class=" dark:border-gray-700 my-2.5" />
<div class=" flex justify-between w-full mb-3">
<div class="flex space-x-2">
<input
id="documents-import-input"
bind:files={importFiles}
type="file"
accept=".json"
hidden
on:change={() => {
console.log(importFiles);
const reader = new FileReader();
reader.onload = async (event) => {
const savedDocs = JSON.parse(event.target.result);
console.log(savedDocs);
for (const doc of savedDocs) {
await createNewDoc(
localStorage.token,
doc.collection_name,
doc.filename,
doc.name,
doc.title
).catch((error) => {
toast.error(error);
return null;
});
}
await documents.set(await getDocs(localStorage.token));
};
reader.readAsText(importFiles[0]);
}}
/>
<button
class="self-center w-fit text-sm px-3 py-1 border dark:border-gray-600 rounded-xl flex"
on:click={async () => {
document.getElementById('documents-import-input')?.click();
}}
>
<div class=" self-center mr-2 font-medium">Import Documents Mapping</div>
<div class=" self-center">
<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="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
<button
class="self-center w-fit text-sm px-3 py-1 border dark:border-gray-600 rounded-xl flex"
on:click={async () => {
let blob = new Blob([JSON.stringify($documents)], {
type: 'application/json'
});
saveAs(blob, `documents-mapping-export-${Date.now()}.json`);
}}
>
<div class=" self-center mr-2 font-medium">Export Documents Mapping</div>
<div class=" self-center">
<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="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
<!-- <button
on:click={() => {
loadDefaultPrompts();
}}
>
dd
</button> -->
</div>
</div>
{/if}
</div>
</div>
</div>
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