mirror of
https://github.com/open-webui/open-webui
synced 2025-03-05 11:59:08 +00:00
Merge pull request #7900 from taylorwilsdon/add_google_drive_integration
feat: Add Google Drive integration for Open-Webui
This commit is contained in:
commit
5871df02ac
@ -307,6 +307,18 @@ GOOGLE_CLIENT_SECRET = PersistentConfig(
|
||||
os.environ.get("GOOGLE_CLIENT_SECRET", ""),
|
||||
)
|
||||
|
||||
GOOGLE_DRIVE_CLIENT_ID = PersistentConfig(
|
||||
"GOOGLE_DRIVE_CLIENT_ID",
|
||||
"google_drive.client_id",
|
||||
os.environ.get("GOOGLE_DRIVE_CLIENT_ID", ""),
|
||||
)
|
||||
|
||||
GOOGLE_DRIVE_API_KEY = PersistentConfig(
|
||||
"GOOGLE_DRIVE_API_KEY",
|
||||
"google_drive.api_key",
|
||||
os.environ.get("GOOGLE_DRIVE_API_KEY", ""),
|
||||
)
|
||||
|
||||
GOOGLE_OAUTH_SCOPE = PersistentConfig(
|
||||
"GOOGLE_OAUTH_SCOPE",
|
||||
"oauth.google.scope",
|
||||
@ -1426,6 +1438,13 @@ RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig(
|
||||
],
|
||||
)
|
||||
|
||||
# If configured, Google Drive will be available as an upload option.
|
||||
ENABLE_GOOGLE_DRIVE = PersistentConfig(
|
||||
"ENABLE_GOOGLE_DRIVE",
|
||||
"rag.drive.enable",
|
||||
os.getenv("ENABLE_GOOGLE_DRIVE", "False").lower() == "true",
|
||||
)
|
||||
|
||||
SEARXNG_QUERY_URL = PersistentConfig(
|
||||
"SEARXNG_QUERY_URL",
|
||||
"rag.web.search.searxng_query_url",
|
||||
|
@ -177,10 +177,13 @@ from open_webui.config import (
|
||||
MOJEEK_SEARCH_API_KEY,
|
||||
GOOGLE_PSE_API_KEY,
|
||||
GOOGLE_PSE_ENGINE_ID,
|
||||
GOOGLE_DRIVE_CLIENT_ID,
|
||||
GOOGLE_DRIVE_API_KEY,
|
||||
ENABLE_RAG_HYBRID_SEARCH,
|
||||
ENABLE_RAG_LOCAL_WEB_FETCH,
|
||||
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
||||
ENABLE_RAG_WEB_SEARCH,
|
||||
ENABLE_GOOGLE_DRIVE,
|
||||
UPLOAD_DIR,
|
||||
# WebUI
|
||||
WEBUI_AUTH,
|
||||
@ -483,6 +486,7 @@ app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH
|
||||
app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE
|
||||
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST
|
||||
|
||||
app.state.config.ENABLE_GOOGLE_DRIVE = ENABLE_GOOGLE_DRIVE
|
||||
app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
|
||||
app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
|
||||
app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID
|
||||
@ -935,6 +939,7 @@ async def get_app_config(request: Request):
|
||||
**(
|
||||
{
|
||||
"enable_web_search": app.state.config.ENABLE_RAG_WEB_SEARCH,
|
||||
"enable_google_drive": app.state.config.ENABLE_GOOGLE_DRIVE,
|
||||
"enable_image_generation": app.state.config.ENABLE_IMAGE_GENERATION,
|
||||
"enable_community_sharing": app.state.config.ENABLE_COMMUNITY_SHARING,
|
||||
"enable_message_rating": app.state.config.ENABLE_MESSAGE_RATING,
|
||||
@ -945,6 +950,10 @@ async def get_app_config(request: Request):
|
||||
else {}
|
||||
),
|
||||
},
|
||||
"google_drive": {
|
||||
"client_id": GOOGLE_DRIVE_CLIENT_ID.value,
|
||||
"api_key": GOOGLE_DRIVE_API_KEY.value,
|
||||
},
|
||||
**(
|
||||
{
|
||||
"default_models": app.state.config.DEFAULT_MODELS,
|
||||
|
@ -11,7 +11,12 @@ from open_webui.models.knowledge import (
|
||||
)
|
||||
from open_webui.models.files import Files, FileModel
|
||||
from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT
|
||||
from open_webui.routers.retrieval import process_file, ProcessFileForm, process_files_batch, BatchProcessFilesForm
|
||||
from open_webui.routers.retrieval import (
|
||||
process_file,
|
||||
ProcessFileForm,
|
||||
process_files_batch,
|
||||
BatchProcessFilesForm,
|
||||
)
|
||||
|
||||
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
@ -519,6 +524,7 @@ async def reset_knowledge_by_id(id: str, user=Depends(get_verified_user)):
|
||||
# AddFilesToKnowledge
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/{id}/files/batch/add", response_model=Optional[KnowledgeFilesResponse])
|
||||
def add_files_to_knowledge_batch(
|
||||
id: str,
|
||||
@ -555,27 +561,25 @@ def add_files_to_knowledge_batch(
|
||||
|
||||
# Process files
|
||||
try:
|
||||
result = process_files_batch(BatchProcessFilesForm(
|
||||
files=files,
|
||||
collection_name=id
|
||||
))
|
||||
except Exception as e:
|
||||
log.error(f"add_files_to_knowledge_batch: Exception occurred: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
result = process_files_batch(
|
||||
BatchProcessFilesForm(files=files, collection_name=id)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
log.error(
|
||||
f"add_files_to_knowledge_batch: Exception occurred: {e}", exc_info=True
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
# Add successful files to knowledge base
|
||||
data = knowledge.data or {}
|
||||
existing_file_ids = data.get("file_ids", [])
|
||||
|
||||
|
||||
# Only add files that were successfully processed
|
||||
successful_file_ids = [r.file_id for r in result.results if r.status == "completed"]
|
||||
for file_id in successful_file_ids:
|
||||
if file_id not in existing_file_ids:
|
||||
existing_file_ids.append(file_id)
|
||||
|
||||
|
||||
data["file_ids"] = existing_file_ids
|
||||
knowledge = Knowledges.update_knowledge_data_by_id(id=id, data=data)
|
||||
|
||||
@ -587,11 +591,10 @@ def add_files_to_knowledge_batch(
|
||||
files=Files.get_files_by_ids(existing_file_ids),
|
||||
warnings={
|
||||
"message": "Some files failed to process",
|
||||
"errors": error_details
|
||||
}
|
||||
"errors": error_details,
|
||||
},
|
||||
)
|
||||
|
||||
return KnowledgeFilesResponse(
|
||||
**knowledge.model_dump(),
|
||||
files=Files.get_files_by_ids(existing_file_ids)
|
||||
**knowledge.model_dump(), files=Files.get_files_by_ids(existing_file_ids)
|
||||
)
|
||||
|
@ -347,6 +347,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
|
||||
return {
|
||||
"status": True,
|
||||
"pdf_extract_images": request.app.state.config.PDF_EXTRACT_IMAGES,
|
||||
"enable_google_drive": request.app.state.config.ENABLE_GOOGLE_DRIVE,
|
||||
"content_extraction": {
|
||||
"engine": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
|
||||
"tika_server_url": request.app.state.config.TIKA_SERVER_URL,
|
||||
@ -369,6 +370,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
|
||||
"web_loader_ssl_verification": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
||||
"search": {
|
||||
"enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH,
|
||||
"drive": request.app.state.config.ENABLE_GOOGLE_DRIVE,
|
||||
"engine": request.app.state.config.RAG_WEB_SEARCH_ENGINE,
|
||||
"searxng_query_url": request.app.state.config.SEARXNG_QUERY_URL,
|
||||
"google_pse_api_key": request.app.state.config.GOOGLE_PSE_API_KEY,
|
||||
@ -445,6 +447,7 @@ class WebConfig(BaseModel):
|
||||
|
||||
class ConfigUpdateForm(BaseModel):
|
||||
pdf_extract_images: Optional[bool] = None
|
||||
enable_google_drive: Optional[bool] = None
|
||||
file: Optional[FileConfig] = None
|
||||
content_extraction: Optional[ContentExtractionConfig] = None
|
||||
chunk: Optional[ChunkParamUpdateForm] = None
|
||||
@ -462,6 +465,12 @@ async def update_rag_config(
|
||||
else request.app.state.config.PDF_EXTRACT_IMAGES
|
||||
)
|
||||
|
||||
request.app.state.config.ENABLE_GOOGLE_DRIVE = (
|
||||
form_data.enable_google_drive
|
||||
if form_data.enable_google_drive is not None
|
||||
else request.app.state.config.ENABLE_GOOGLE_DRIVE
|
||||
)
|
||||
|
||||
if form_data.file is not None:
|
||||
request.app.state.config.FILE_MAX_SIZE = form_data.file.max_size
|
||||
request.app.state.config.FILE_MAX_COUNT = form_data.file.max_count
|
||||
|
@ -37007,16 +37007,14 @@
|
||||
Pe.createElement('span', { className: 'brace-close' }, '}')
|
||||
),
|
||||
pe.size
|
||||
? pe
|
||||
.entrySeq()
|
||||
.map(([s, o]) =>
|
||||
Pe.createElement(xe, {
|
||||
key: `${s}-${o}`,
|
||||
propKey: s,
|
||||
propVal: o,
|
||||
propClass: 'property'
|
||||
})
|
||||
)
|
||||
? pe.entrySeq().map(([s, o]) =>
|
||||
Pe.createElement(xe, {
|
||||
key: `${s}-${o}`,
|
||||
propKey: s,
|
||||
propVal: o,
|
||||
propClass: 'property'
|
||||
})
|
||||
)
|
||||
: null
|
||||
);
|
||||
}
|
||||
@ -37167,16 +37165,14 @@
|
||||
)
|
||||
: null,
|
||||
C && z.size
|
||||
? z
|
||||
.entrySeq()
|
||||
.map(([s, o]) =>
|
||||
Pe.createElement(le, {
|
||||
key: `${s}-${o}`,
|
||||
propKey: s,
|
||||
propVal: o,
|
||||
propClass: rs
|
||||
})
|
||||
)
|
||||
? z.entrySeq().map(([s, o]) =>
|
||||
Pe.createElement(le, {
|
||||
key: `${s}-${o}`,
|
||||
propKey: s,
|
||||
propVal: o,
|
||||
propClass: rs
|
||||
})
|
||||
)
|
||||
: null,
|
||||
U ? Pe.createElement(ie, { source: U }) : null,
|
||||
Z &&
|
||||
@ -57290,20 +57286,18 @@
|
||||
Pe.createElement(
|
||||
'div',
|
||||
{ className: 'modal-ux-content' },
|
||||
x
|
||||
.valueSeq()
|
||||
.map((x, j) =>
|
||||
Pe.createElement(C, {
|
||||
key: j,
|
||||
AST: w,
|
||||
definitions: x,
|
||||
getComponent: i,
|
||||
errSelectors: u,
|
||||
authSelectors: s,
|
||||
authActions: o,
|
||||
specSelectors: _
|
||||
})
|
||||
)
|
||||
x.valueSeq().map((x, j) =>
|
||||
Pe.createElement(C, {
|
||||
key: j,
|
||||
AST: w,
|
||||
definitions: x,
|
||||
getComponent: i,
|
||||
errSelectors: u,
|
||||
authSelectors: s,
|
||||
authActions: o,
|
||||
specSelectors: _
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -90,6 +90,11 @@ extract_msg
|
||||
pydub
|
||||
duckduckgo-search~=6.3.5
|
||||
|
||||
## Google Drive
|
||||
google-api-python-client
|
||||
google-auth-httplib2
|
||||
google-auth-oauthlib
|
||||
|
||||
## Tests
|
||||
docker~=7.1.0
|
||||
pytest~=8.3.2
|
||||
|
20
package-lock.json
generated
20
package-lock.json
generated
@ -2260,9 +2260,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.9.0.tgz",
|
||||
"integrity": "sha512-W3E7ed3ChB6kPqRs2H7tcHp+Z7oiTFC6m+lLyAQQuyXeqw6LdNuuwEUla+5VM0OGgqQD+cYD6+7Xq80vVm17Vg==",
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.12.1.tgz",
|
||||
"integrity": "sha512-M3rPijGImeOkI0DBJSwjqz+YFX2DyOf6NzWgHVk3mqpT06dlYCpcv5xh1q4rYEqB58yQlk4QA1Y35PUqnUiFKw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -8267,15 +8267,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.6.tgz",
|
||||
"integrity": "sha512-rRq0eMHoGZxlvaFOUdK1Ev83Bd1IgzzR+WJ3IbDJ7QOSdAxYjlurSPqFs9s4lJg29RT6nPwizFtJhQS6V5xgiA==",
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
||||
"integrity": "sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
@ -8976,15 +8977,16 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/postcss/node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
|
@ -45,6 +45,7 @@ type YoutubeConfigForm = {
|
||||
|
||||
type RAGConfigForm = {
|
||||
pdf_extract_images?: boolean;
|
||||
enable_google_drive?: boolean;
|
||||
chunk?: ChunkConfigForm;
|
||||
content_extraction?: ContentExtractConfigForm;
|
||||
web_loader_ssl_verification?: boolean;
|
||||
|
@ -56,6 +56,8 @@
|
||||
let chunkOverlap = 0;
|
||||
let pdfExtractImages = true;
|
||||
|
||||
let enableGoogleDrive = false;
|
||||
|
||||
let OpenAIUrl = '';
|
||||
let OpenAIKey = '';
|
||||
|
||||
@ -175,6 +177,7 @@
|
||||
}
|
||||
const res = await updateRAGConfig(localStorage.token, {
|
||||
pdf_extract_images: pdfExtractImages,
|
||||
enable_google_drive: enableGoogleDrive,
|
||||
file: {
|
||||
max_size: fileMaxSize === '' ? null : fileMaxSize,
|
||||
max_count: fileMaxCount === '' ? null : fileMaxCount
|
||||
@ -245,6 +248,8 @@
|
||||
|
||||
fileMaxSize = res?.file.max_size ?? '';
|
||||
fileMaxCount = res?.file.max_count ?? '';
|
||||
|
||||
enableGoogleDrive = res.enable_google_drive;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -571,6 +576,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
{#if showTikaServerUrl}
|
||||
<div class="flex w-full mt-1">
|
||||
<div class="flex-1 mr-2">
|
||||
@ -584,251 +591,268 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850" />
|
||||
<div class="">
|
||||
<div class="text-sm font-medium mb-1">{$i18n.t('Google Drive')}</div>
|
||||
|
||||
<div class=" ">
|
||||
<div class=" text-sm font-medium mb-1">{$i18n.t('Query Params')}</div>
|
||||
|
||||
<div class=" flex gap-1.5">
|
||||
<div class="flex flex-col w-full gap-1">
|
||||
<div class=" text-xs font-medium w-full">{$i18n.t('Top K')}</div>
|
||||
|
||||
<div class="w-full">
|
||||
<input
|
||||
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="number"
|
||||
placeholder={$i18n.t('Enter Top K')}
|
||||
bind:value={querySettings.k}
|
||||
autocomplete="off"
|
||||
min="0"
|
||||
/>
|
||||
<div class="my-2">
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<div class="text-xs font-medium">{$i18n.t('Enable Google Drive')}</div>
|
||||
<div>
|
||||
<Switch bind:state={enableGoogleDrive} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if querySettings.hybrid === true}
|
||||
<div class=" flex flex-col w-full gap-1">
|
||||
<div class="text-xs font-medium w-full">
|
||||
{$i18n.t('Minimum Score')}
|
||||
</div>
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div class=" ">
|
||||
<div class=" text-sm font-medium mb-1">{$i18n.t('Query Params')}</div>
|
||||
|
||||
<div class=" flex gap-1.5">
|
||||
<div class="flex flex-col w-full gap-1">
|
||||
<div class=" text-xs font-medium w-full">{$i18n.t('Top K')}</div>
|
||||
|
||||
<div class="w-full">
|
||||
<input
|
||||
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder={$i18n.t('Enter Score')}
|
||||
bind:value={querySettings.r}
|
||||
placeholder={$i18n.t('Enter Top K')}
|
||||
bind:value={querySettings.k}
|
||||
autocomplete="off"
|
||||
min="0.0"
|
||||
title={$i18n.t('The score should be a value between 0.0 (0%) and 1.0 (100%).')}
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if querySettings.hybrid === true}
|
||||
<div class=" flex flex-col w-full gap-1">
|
||||
<div class="text-xs font-medium w-full">
|
||||
{$i18n.t('Minimum Score')}
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<input
|
||||
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder={$i18n.t('Enter Score')}
|
||||
bind:value={querySettings.r}
|
||||
autocomplete="off"
|
||||
min="0.0"
|
||||
title={$i18n.t('The score should be a value between 0.0 (0%) and 1.0 (100%).')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if querySettings.hybrid === true}
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t(
|
||||
'Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.'
|
||||
)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if querySettings.hybrid === true}
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t(
|
||||
'Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.'
|
||||
)}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-2">
|
||||
<div class=" mb-1 text-xs font-medium">{$i18n.t('RAG Template')}</div>
|
||||
<Tooltip
|
||||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
placement="top-start"
|
||||
>
|
||||
<Textarea
|
||||
bind:value={querySettings.template}
|
||||
placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div class=" ">
|
||||
<div class="mb-1 text-sm font-medium">{$i18n.t('Chunk Params')}</div>
|
||||
|
||||
<div class="flex w-full justify-between mb-1.5">
|
||||
<div class="self-center text-xs font-medium">{$i18n.t('Text Splitter')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={textSplitter}
|
||||
<div class="mt-2">
|
||||
<div class=" mb-1 text-xs font-medium">{$i18n.t('RAG Template')}</div>
|
||||
<Tooltip
|
||||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
placement="top-start"
|
||||
>
|
||||
<option value="">{$i18n.t('Default')} ({$i18n.t('Character')})</option>
|
||||
<option value="token">{$i18n.t('Token')} ({$i18n.t('Tiktoken')})</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" flex gap-1.5">
|
||||
<div class=" w-full justify-between">
|
||||
<div class="self-center text-xs font-medium min-w-fit mb-1">{$i18n.t('Chunk Size')}</div>
|
||||
<div class="self-center">
|
||||
<input
|
||||
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="number"
|
||||
placeholder={$i18n.t('Enter Chunk Size')}
|
||||
bind:value={chunkSize}
|
||||
autocomplete="off"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Chunk Overlap')}
|
||||
</div>
|
||||
|
||||
<div class="self-center">
|
||||
<input
|
||||
class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="number"
|
||||
placeholder={$i18n.t('Enter Chunk Overlap')}
|
||||
bind:value={chunkOverlap}
|
||||
autocomplete="off"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-2">
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<div class=" text-xs font-medium">{$i18n.t('PDF Extract Images (OCR)')}</div>
|
||||
|
||||
<div>
|
||||
<Switch bind:state={pdfExtractImages} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div class="">
|
||||
<div class="text-sm font-medium mb-1">{$i18n.t('Files')}</div>
|
||||
|
||||
<div class=" flex gap-1.5">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Max Upload Size')}
|
||||
</div>
|
||||
|
||||
<div class="self-center">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'The maximum file size in MB. If the file size exceeds this limit, the file will not be uploaded.'
|
||||
<Textarea
|
||||
bind:value={querySettings.template}
|
||||
placeholder={$i18n.t(
|
||||
'Leave empty to use the default prompt, or enter a custom prompt'
|
||||
)}
|
||||
placement="top-start"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div class=" ">
|
||||
<div class="mb-1 text-sm font-medium">{$i18n.t('Chunk Params')}</div>
|
||||
|
||||
<div class="flex w-full justify-between mb-1.5">
|
||||
<div class="self-center text-xs font-medium">{$i18n.t('Text Splitter')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={textSplitter}
|
||||
>
|
||||
<input
|
||||
class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="number"
|
||||
placeholder={$i18n.t('Leave empty for unlimited')}
|
||||
bind:value={fileMaxSize}
|
||||
autocomplete="off"
|
||||
min="0"
|
||||
/>
|
||||
</Tooltip>
|
||||
<option value="">{$i18n.t('Default')} ({$i18n.t('Character')})</option>
|
||||
<option value="token">{$i18n.t('Token')} ({$i18n.t('Tiktoken')})</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" w-full">
|
||||
<div class="self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Max Upload Count')}
|
||||
</div>
|
||||
<div class="self-center">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'The maximum number of files that can be used at once in chat. If the number of files exceeds this limit, the files will not be uploaded.'
|
||||
)}
|
||||
placement="top-start"
|
||||
>
|
||||
<div class=" flex gap-1.5">
|
||||
<div class=" w-full justify-between">
|
||||
<div class="self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Chunk Size')}
|
||||
</div>
|
||||
<div class="self-center">
|
||||
<input
|
||||
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="number"
|
||||
placeholder={$i18n.t('Leave empty for unlimited')}
|
||||
bind:value={fileMaxCount}
|
||||
placeholder={$i18n.t('Enter Chunk Size')}
|
||||
bind:value={chunkSize}
|
||||
autocomplete="off"
|
||||
min="0"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Chunk Overlap')}
|
||||
</div>
|
||||
|
||||
<div class="self-center">
|
||||
<input
|
||||
class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="number"
|
||||
placeholder={$i18n.t('Enter Chunk Overlap')}
|
||||
bind:value={chunkOverlap}
|
||||
autocomplete="off"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-2">
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<div class=" text-xs font-medium">{$i18n.t('PDF Extract Images (OCR)')}</div>
|
||||
|
||||
<div>
|
||||
<Switch bind:state={pdfExtractImages} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div class="">
|
||||
<div class="text-sm font-medium mb-1">{$i18n.t('Files')}</div>
|
||||
|
||||
<div class=" flex gap-1.5">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Max Upload Size')}
|
||||
</div>
|
||||
|
||||
<div class="self-center">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'The maximum file size in MB. If the file size exceeds this limit, the file will not be uploaded.'
|
||||
)}
|
||||
placement="top-start"
|
||||
>
|
||||
<input
|
||||
class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="number"
|
||||
placeholder={$i18n.t('Leave empty for unlimited')}
|
||||
bind:value={fileMaxSize}
|
||||
autocomplete="off"
|
||||
min="0"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" w-full">
|
||||
<div class="self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Max Upload Count')}
|
||||
</div>
|
||||
<div class="self-center">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'The maximum number of files that can be used at once in chat. If the number of files exceeds this limit, the files will not be uploaded.'
|
||||
)}
|
||||
placement="top-start"
|
||||
>
|
||||
<input
|
||||
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="number"
|
||||
placeholder={$i18n.t('Leave empty for unlimited')}
|
||||
bind:value={fileMaxCount}
|
||||
autocomplete="off"
|
||||
min="0"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div>
|
||||
<button
|
||||
class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||
on:click={() => {
|
||||
showResetUploadDirConfirm = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875ZM9.75 14.25a.75.75 0 0 0 0 1.5H15a.75.75 0 0 0 0-1.5H9.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M14.25 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center text-sm font-medium">{$i18n.t('Reset Upload Directory')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||
on:click={() => {
|
||||
showResetConfirm = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
<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="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center text-sm font-medium">
|
||||
{$i18n.t('Reset Vector Storage/Knowledge')}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div>
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||
on:click={() => {
|
||||
showResetUploadDirConfirm = true;
|
||||
}}
|
||||
type="button"
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||
type="submit"
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875ZM9.75 14.25a.75.75 0 0 0 0 1.5H15a.75.75 0 0 0 0-1.5H9.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M14.25 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center text-sm font-medium">{$i18n.t('Reset Upload Directory')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||
on:click={() => {
|
||||
showResetConfirm = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
<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="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center text-sm font-medium">
|
||||
{$i18n.t('Reset Vector Storage/Knowledge')}
|
||||
</div>
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||
type="submit"
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -384,6 +384,131 @@
|
||||
|
||||
// File upload functions
|
||||
|
||||
const uploadGoogleDriveFile = async (fileData) => {
|
||||
console.log('Starting uploadGoogleDriveFile with:', {
|
||||
id: fileData.id,
|
||||
name: fileData.name,
|
||||
url: fileData.url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
// Validate input
|
||||
if (!fileData?.id || !fileData?.name || !fileData?.url || !fileData?.headers?.Authorization) {
|
||||
throw new Error('Invalid file data provided');
|
||||
}
|
||||
|
||||
const tempItemId = uuidv4();
|
||||
const fileItem = {
|
||||
type: 'file',
|
||||
file: '',
|
||||
id: null,
|
||||
url: fileData.url,
|
||||
name: fileData.name,
|
||||
collection_name: '',
|
||||
status: 'uploading',
|
||||
error: '',
|
||||
itemId: tempItemId,
|
||||
size: 0
|
||||
};
|
||||
|
||||
try {
|
||||
files = [...files, fileItem];
|
||||
console.log('Processing web file with URL:', fileData.url);
|
||||
|
||||
// Configure fetch options with proper headers
|
||||
const fetchOptions = {
|
||||
headers: {
|
||||
Authorization: fileData.headers.Authorization,
|
||||
Accept: '*/*'
|
||||
},
|
||||
method: 'GET'
|
||||
};
|
||||
|
||||
// Attempt to fetch the file
|
||||
console.log('Fetching file content from Google Drive...');
|
||||
const fileResponse = await fetch(fileData.url, fetchOptions);
|
||||
|
||||
if (!fileResponse.ok) {
|
||||
const errorText = await fileResponse.text();
|
||||
throw new Error(`Failed to fetch file (${fileResponse.status}): ${errorText}`);
|
||||
}
|
||||
|
||||
// Get content type from response
|
||||
const contentType = fileResponse.headers.get('content-type') || 'application/octet-stream';
|
||||
console.log('Response received with content-type:', contentType);
|
||||
|
||||
// Convert response to blob
|
||||
console.log('Converting response to blob...');
|
||||
const fileBlob = await fileResponse.blob();
|
||||
|
||||
if (fileBlob.size === 0) {
|
||||
throw new Error('Retrieved file is empty');
|
||||
}
|
||||
|
||||
console.log('Blob created:', {
|
||||
size: fileBlob.size,
|
||||
type: fileBlob.type || contentType
|
||||
});
|
||||
|
||||
// Create File object with proper MIME type
|
||||
const file = new File([fileBlob], fileData.name, {
|
||||
type: fileBlob.type || contentType
|
||||
});
|
||||
|
||||
console.log('File object created:', {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type
|
||||
});
|
||||
|
||||
if (file.size === 0) {
|
||||
throw new Error('Created file is empty');
|
||||
}
|
||||
|
||||
// Upload file to server
|
||||
console.log('Uploading file to server...');
|
||||
const uploadedFile = await uploadFile(localStorage.token, file);
|
||||
|
||||
if (!uploadedFile) {
|
||||
throw new Error('Server returned null response for file upload');
|
||||
}
|
||||
|
||||
console.log('File uploaded successfully:', uploadedFile);
|
||||
|
||||
// Update file item with upload results
|
||||
fileItem.status = 'uploaded';
|
||||
fileItem.file = uploadedFile;
|
||||
fileItem.id = uploadedFile.id;
|
||||
fileItem.size = file.size;
|
||||
fileItem.collection_name = uploadedFile?.meta?.collection_name;
|
||||
fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`;
|
||||
|
||||
files = files;
|
||||
toast.success($i18n.t('File uploaded successfully'));
|
||||
} catch (e) {
|
||||
console.error('Error uploading file:', e);
|
||||
files = files.filter((f) => f.itemId !== tempItemId);
|
||||
toast.error(
|
||||
$i18n.t('Error uploading file: {{error}}', {
|
||||
error: e.message || 'Unknown error'
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoogleDrivePicker = async () => {
|
||||
try {
|
||||
const fileData = await createPicker();
|
||||
if (fileData) {
|
||||
await uploadGoogleDriveFile(fileData);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Error accessing Google Drive: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const uploadWeb = async (url) => {
|
||||
console.log(url);
|
||||
|
||||
@ -1901,6 +2026,8 @@
|
||||
await uploadWeb(data);
|
||||
} else if (type === 'youtube') {
|
||||
await uploadYoutubeTranscription(data);
|
||||
} else if (type === 'google-drive') {
|
||||
await uploadGoogleDriveFile(data);
|
||||
}
|
||||
}}
|
||||
on:submit={async (e) => {
|
||||
|
@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { createPicker, getAuthToken } from '$lib/utils/google-drive-picker';
|
||||
|
||||
import { onMount, tick, getContext, createEventDispatcher, onDestroy } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
@ -132,8 +133,6 @@
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(file);
|
||||
|
||||
const tempItemId = uuidv4();
|
||||
const fileItem = {
|
||||
type: 'file',
|
||||
@ -177,14 +176,22 @@
|
||||
const uploadedFile = await uploadFile(localStorage.token, file);
|
||||
|
||||
if (uploadedFile) {
|
||||
console.log('File upload completed:', {
|
||||
id: uploadedFile.id,
|
||||
name: fileItem.name,
|
||||
collection: uploadedFile?.meta?.collection_name
|
||||
});
|
||||
|
||||
if (uploadedFile.error) {
|
||||
console.warn('File upload warning:', uploadedFile.error);
|
||||
toast.warning(uploadedFile.error);
|
||||
}
|
||||
|
||||
fileItem.status = 'uploaded';
|
||||
fileItem.file = uploadedFile;
|
||||
fileItem.id = uploadedFile.id;
|
||||
fileItem.collection_name = uploadedFile?.meta?.collection_name;
|
||||
fileItem.collection_name =
|
||||
uploadedFile?.meta?.collection_name || uploadedFile?.collection_name;
|
||||
fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`;
|
||||
|
||||
files = files;
|
||||
@ -198,13 +205,23 @@
|
||||
};
|
||||
|
||||
const inputFilesHandler = async (inputFiles) => {
|
||||
console.log('Input files handler called with:', inputFiles);
|
||||
inputFiles.forEach((file) => {
|
||||
console.log(file, file.name.split('.').at(-1));
|
||||
console.log('Processing file:', {
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
extension: file.name.split('.').at(-1)
|
||||
});
|
||||
|
||||
if (
|
||||
($config?.file?.max_size ?? null) !== null &&
|
||||
file.size > ($config?.file?.max_size ?? 0) * 1024 * 1024
|
||||
) {
|
||||
console.log('File exceeds max size limit:', {
|
||||
fileSize: file.size,
|
||||
maxSize: ($config?.file?.max_size ?? 0) * 1024 * 1024
|
||||
});
|
||||
toast.error(
|
||||
$i18n.t(`File size should not exceed {{maxSize}} MB.`, {
|
||||
maxSize: $config?.file?.max_size
|
||||
@ -593,6 +610,26 @@
|
||||
uploadFilesHandler={() => {
|
||||
filesInputElement.click();
|
||||
}}
|
||||
uploadGoogleDriveHandler={async () => {
|
||||
try {
|
||||
const fileData = await createPicker();
|
||||
if (fileData) {
|
||||
const file = new File([fileData.blob], fileData.name, {
|
||||
type: fileData.blob.type
|
||||
});
|
||||
await uploadFileHandler(file);
|
||||
} else {
|
||||
console.log('No file was selected from Google Drive');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Google Drive Error:', error);
|
||||
toast.error(
|
||||
$i18n.t('Error accessing Google Drive: {{error}}', {
|
||||
error: error.message
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
onClose={async () => {
|
||||
await tick();
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
import { getContext, onMount, tick } from 'svelte';
|
||||
|
||||
import { config, user, tools as _tools, mobile } from '$lib/stores';
|
||||
import { createPicker } from '$lib/utils/google-drive-picker';
|
||||
import { getTools } from '$lib/apis/tools';
|
||||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
@ -18,6 +19,8 @@
|
||||
|
||||
export let screenCaptureHandler: Function;
|
||||
export let uploadFilesHandler: Function;
|
||||
export let uploadGoogleDriveHandler: Function;
|
||||
|
||||
export let selectedToolIds: string[] = [];
|
||||
|
||||
export let webSearchEnabled: boolean;
|
||||
@ -142,14 +145,51 @@
|
||||
{/if}
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
||||
on:click={() => {
|
||||
uploadFilesHandler();
|
||||
}}
|
||||
>
|
||||
<DocumentArrowUpSolid />
|
||||
<div class=" line-clamp-1">{$i18n.t('Upload Files')}</div>
|
||||
<div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
{#if $config?.features?.enable_google_drive}
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
||||
on:click={() => {
|
||||
uploadGoogleDriveHandler();
|
||||
}}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.3 78" class="w-5 h-5">
|
||||
<path
|
||||
d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z"
|
||||
fill="#0066da"
|
||||
/>
|
||||
<path
|
||||
d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z"
|
||||
fill="#00ac47"
|
||||
/>
|
||||
<path
|
||||
d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z"
|
||||
fill="#ea4335"
|
||||
/>
|
||||
<path
|
||||
d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z"
|
||||
fill="#00832d"
|
||||
/>
|
||||
<path
|
||||
d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z"
|
||||
fill="#2684fc"
|
||||
/>
|
||||
<path
|
||||
d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z"
|
||||
fill="#ffba00"
|
||||
/>
|
||||
</svg>
|
||||
<div class="line-clamp-1">{$i18n.t('Google Drive')}</div>
|
||||
</DropdownMenu.Item>
|
||||
{/if}
|
||||
</DropdownMenu.Content>
|
||||
</div>
|
||||
</Dropdown>
|
||||
|
@ -176,6 +176,7 @@ type Config = {
|
||||
enable_signup: boolean;
|
||||
enable_login_form: boolean;
|
||||
enable_web_search?: boolean;
|
||||
enable_google_drive: boolean;
|
||||
enable_image_generation: boolean;
|
||||
enable_admin_export: boolean;
|
||||
enable_admin_chat_access: boolean;
|
||||
|
212
src/lib/utils/google-drive-picker.ts
Normal file
212
src/lib/utils/google-drive-picker.ts
Normal file
@ -0,0 +1,212 @@
|
||||
// Google Drive Picker API configuration
|
||||
let API_KEY = '';
|
||||
let CLIENT_ID = '';
|
||||
|
||||
// Function to fetch credentials from backend config
|
||||
async function getCredentials() {
|
||||
const response = await fetch('/api/config');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch Google Drive credentials');
|
||||
}
|
||||
const config = await response.json();
|
||||
API_KEY = config.google_drive?.api_key;
|
||||
CLIENT_ID = config.google_drive?.client_id;
|
||||
|
||||
if (!API_KEY || !CLIENT_ID) {
|
||||
throw new Error('Google Drive API credentials not configured');
|
||||
}
|
||||
}
|
||||
const SCOPE = [
|
||||
'https://www.googleapis.com/auth/drive.readonly',
|
||||
'https://www.googleapis.com/auth/drive.file'
|
||||
];
|
||||
|
||||
// Validate required credentials
|
||||
const validateCredentials = () => {
|
||||
if (!API_KEY || !CLIENT_ID) {
|
||||
throw new Error('Google Drive API credentials not configured');
|
||||
}
|
||||
if (API_KEY === '' || CLIENT_ID === '') {
|
||||
throw new Error('Please configure valid Google Drive API credentials');
|
||||
}
|
||||
};
|
||||
|
||||
let pickerApiLoaded = false;
|
||||
let oauthToken: string | null = null;
|
||||
let initialized = false;
|
||||
|
||||
export const loadGoogleDriveApi = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof gapi === 'undefined') {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://apis.google.com/js/api.js';
|
||||
script.onload = () => {
|
||||
gapi.load('picker', () => {
|
||||
pickerApiLoaded = true;
|
||||
resolve(true);
|
||||
});
|
||||
};
|
||||
script.onerror = reject;
|
||||
document.body.appendChild(script);
|
||||
} else {
|
||||
gapi.load('picker', () => {
|
||||
pickerApiLoaded = true;
|
||||
resolve(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const loadGoogleAuthApi = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof google === 'undefined') {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://accounts.google.com/gsi/client';
|
||||
script.onload = resolve;
|
||||
script.onerror = reject;
|
||||
document.body.appendChild(script);
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getAuthToken = async () => {
|
||||
if (!oauthToken) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tokenClient = google.accounts.oauth2.initTokenClient({
|
||||
client_id: CLIENT_ID,
|
||||
scope: SCOPE.join(' '),
|
||||
callback: (response: any) => {
|
||||
if (response.access_token) {
|
||||
oauthToken = response.access_token;
|
||||
resolve(oauthToken);
|
||||
} else {
|
||||
reject(new Error('Failed to get access token'));
|
||||
}
|
||||
},
|
||||
error_callback: (error: any) => {
|
||||
reject(new Error(error.message || 'OAuth error occurred'));
|
||||
}
|
||||
});
|
||||
tokenClient.requestAccessToken();
|
||||
});
|
||||
}
|
||||
return oauthToken;
|
||||
};
|
||||
|
||||
const initialize = async () => {
|
||||
if (!initialized) {
|
||||
await getCredentials();
|
||||
validateCredentials();
|
||||
await Promise.all([loadGoogleDriveApi(), loadGoogleAuthApi()]);
|
||||
initialized = true;
|
||||
}
|
||||
};
|
||||
|
||||
export const createPicker = () => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
console.log('Initializing Google Drive Picker...');
|
||||
await initialize();
|
||||
console.log('Getting auth token...');
|
||||
const token = await getAuthToken();
|
||||
if (!token) {
|
||||
console.error('Failed to get OAuth token');
|
||||
throw new Error('Unable to get OAuth token');
|
||||
}
|
||||
console.log('Auth token obtained successfully');
|
||||
|
||||
const picker = new google.picker.PickerBuilder()
|
||||
.enableFeature(google.picker.Feature.NAV_HIDDEN)
|
||||
.enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
|
||||
.addView(
|
||||
new google.picker.DocsView()
|
||||
.setIncludeFolders(false)
|
||||
.setSelectFolderEnabled(false)
|
||||
.setMimeTypes(
|
||||
'application/pdf,text/plain,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.google-apps.document,application/vnd.google-apps.spreadsheet,application/vnd.google-apps.presentation'
|
||||
)
|
||||
)
|
||||
.setOAuthToken(token)
|
||||
.setDeveloperKey(API_KEY)
|
||||
// Remove app ID setting as it's not needed and can cause 404 errors
|
||||
.setCallback(async (data: any) => {
|
||||
if (data[google.picker.Response.ACTION] === google.picker.Action.PICKED) {
|
||||
try {
|
||||
const doc = data[google.picker.Response.DOCUMENTS][0];
|
||||
const fileId = doc[google.picker.Document.ID];
|
||||
const fileName = doc[google.picker.Document.NAME];
|
||||
const fileUrl = doc[google.picker.Document.URL];
|
||||
|
||||
if (!fileId || !fileName) {
|
||||
throw new Error('Required file details missing');
|
||||
}
|
||||
|
||||
// Construct download URL based on MIME type
|
||||
const mimeType = doc[google.picker.Document.MIME_TYPE];
|
||||
|
||||
let downloadUrl;
|
||||
let exportFormat;
|
||||
|
||||
if (mimeType.includes('google-apps')) {
|
||||
// Handle Google Workspace files
|
||||
if (mimeType.includes('document')) {
|
||||
exportFormat = 'text/plain';
|
||||
} else if (mimeType.includes('spreadsheet')) {
|
||||
exportFormat = 'text/csv';
|
||||
} else if (mimeType.includes('presentation')) {
|
||||
exportFormat = 'text/plain';
|
||||
} else {
|
||||
exportFormat = 'application/pdf';
|
||||
}
|
||||
downloadUrl = `https://www.googleapis.com/drive/v3/files/${fileId}/export?mimeType=${encodeURIComponent(exportFormat)}`;
|
||||
} else {
|
||||
// Regular files use direct download URL
|
||||
downloadUrl = `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`;
|
||||
}
|
||||
// Create a Blob from the file download
|
||||
const response = await fetch(downloadUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: '*/*'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Download failed:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText
|
||||
});
|
||||
throw new Error(`Failed to download file (${response.status}): ${errorText}`);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const result = {
|
||||
id: fileId,
|
||||
name: fileName,
|
||||
url: downloadUrl,
|
||||
blob: blob,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: '*/*'
|
||||
}
|
||||
};
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
} else if (data[google.picker.Response.ACTION] === google.picker.Action.CANCEL) {
|
||||
resolve(null);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
picker.setVisible(true);
|
||||
} catch (error) {
|
||||
console.error('Google Drive Picker error:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
@ -552,31 +552,33 @@ export const removeEmojis = (str: string) => {
|
||||
};
|
||||
|
||||
export const removeFormattings = (str: string) => {
|
||||
return str
|
||||
// Block elements (remove completely)
|
||||
.replace(/(```[\s\S]*?```)/g, '') // Code blocks
|
||||
.replace(/^\|.*\|$/gm, '') // Tables
|
||||
// Inline elements (preserve content)
|
||||
.replace(/(?:\*\*|__)(.*?)(?:\*\*|__)/g, '$1') // Bold
|
||||
.replace(/(?:[*_])(.*?)(?:[*_])/g, '$1') // Italic
|
||||
.replace(/~~(.*?)~~/g, '$1') // Strikethrough
|
||||
.replace(/`([^`]+)`/g, '$1') // Inline code
|
||||
|
||||
// Links and images
|
||||
.replace(/!?\[([^\]]*)\](?:\([^)]+\)|\[[^\]]*\])/g, '$1') // Links & images
|
||||
.replace(/^\[[^\]]+\]:\s*.*$/gm, '') // Reference definitions
|
||||
|
||||
// Block formatting
|
||||
.replace(/^#{1,6}\s+/gm, '') // Headers
|
||||
.replace(/^\s*[-*+]\s+/gm, '') // Lists
|
||||
.replace(/^\s*(?:\d+\.)\s+/gm, '') // Numbered lists
|
||||
.replace(/^\s*>[> ]*/gm, '') // Blockquotes
|
||||
.replace(/^\s*:\s+/gm, '') // Definition lists
|
||||
|
||||
// Cleanup
|
||||
.replace(/\[\^[^\]]*\]/g, '') // Footnotes
|
||||
.replace(/[-*_~]/g, '') // Remaining markers
|
||||
.replace(/\n{2,}/g, '\n') // Multiple newlines
|
||||
return (
|
||||
str
|
||||
// Block elements (remove completely)
|
||||
.replace(/(```[\s\S]*?```)/g, '') // Code blocks
|
||||
.replace(/^\|.*\|$/gm, '') // Tables
|
||||
// Inline elements (preserve content)
|
||||
.replace(/(?:\*\*|__)(.*?)(?:\*\*|__)/g, '$1') // Bold
|
||||
.replace(/(?:[*_])(.*?)(?:[*_])/g, '$1') // Italic
|
||||
.replace(/~~(.*?)~~/g, '$1') // Strikethrough
|
||||
.replace(/`([^`]+)`/g, '$1') // Inline code
|
||||
|
||||
// Links and images
|
||||
.replace(/!?\[([^\]]*)\](?:\([^)]+\)|\[[^\]]*\])/g, '$1') // Links & images
|
||||
.replace(/^\[[^\]]+\]:\s*.*$/gm, '') // Reference definitions
|
||||
|
||||
// Block formatting
|
||||
.replace(/^#{1,6}\s+/gm, '') // Headers
|
||||
.replace(/^\s*[-*+]\s+/gm, '') // Lists
|
||||
.replace(/^\s*(?:\d+\.)\s+/gm, '') // Numbered lists
|
||||
.replace(/^\s*>[> ]*/gm, '') // Blockquotes
|
||||
.replace(/^\s*:\s+/gm, '') // Definition lists
|
||||
|
||||
// Cleanup
|
||||
.replace(/\[\^[^\]]*\]/g, '') // Footnotes
|
||||
.replace(/[-*_~]/g, '') // Remaining markers
|
||||
.replace(/\n{2,}/g, '\n')
|
||||
); // Multiple newlines
|
||||
};
|
||||
|
||||
export const cleanText = (content: string) => {
|
||||
|
Loading…
Reference in New Issue
Block a user