Unverified Commit 635951b5 authored by Timothy Jaeryang Baek's avatar Timothy Jaeryang Baek Committed by GitHub
Browse files

Merge branch 'dev' into feat/backend-web-search

parents 8b3e370a bf604bc0
...@@ -20,7 +20,16 @@ jobs: ...@@ -20,7 +20,16 @@ jobs:
- name: Build and run Compose Stack - name: Build and run Compose Stack
run: | run: |
docker compose up --detach --build docker compose --file docker-compose.yaml --file docker-compose.api.yaml up --detach --build
- name: Wait for Ollama to be up
timeout-minutes: 5
run: |
until curl --output /dev/null --silent --fail http://localhost:11434; do
printf '.'
sleep 1
done
echo "Service is up!"
- name: Preload Ollama model - name: Preload Ollama model
run: | run: |
......
...@@ -80,6 +80,7 @@ from config import ( ...@@ -80,6 +80,7 @@ from config import (
RAG_EMBEDDING_MODEL_AUTO_UPDATE, RAG_EMBEDDING_MODEL_AUTO_UPDATE,
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE, RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
ENABLE_RAG_HYBRID_SEARCH, ENABLE_RAG_HYBRID_SEARCH,
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
RAG_RERANKING_MODEL, RAG_RERANKING_MODEL,
PDF_EXTRACT_IMAGES, PDF_EXTRACT_IMAGES,
RAG_RERANKING_MODEL_AUTO_UPDATE, RAG_RERANKING_MODEL_AUTO_UPDATE,
...@@ -91,7 +92,7 @@ from config import ( ...@@ -91,7 +92,7 @@ from config import (
CHUNK_SIZE, CHUNK_SIZE,
CHUNK_OVERLAP, CHUNK_OVERLAP,
RAG_TEMPLATE, RAG_TEMPLATE,
ENABLE_LOCAL_WEB_FETCH, ENABLE_RAG_LOCAL_WEB_FETCH,
) )
from constants import ERROR_MESSAGES from constants import ERROR_MESSAGES
...@@ -105,6 +106,9 @@ app.state.TOP_K = RAG_TOP_K ...@@ -105,6 +106,9 @@ app.state.TOP_K = RAG_TOP_K
app.state.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD app.state.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
app.state.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH app.state.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
app.state.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
)
app.state.CHUNK_SIZE = CHUNK_SIZE app.state.CHUNK_SIZE = CHUNK_SIZE
app.state.CHUNK_OVERLAP = CHUNK_OVERLAP app.state.CHUNK_OVERLAP = CHUNK_OVERLAP
...@@ -114,6 +118,7 @@ app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL ...@@ -114,6 +118,7 @@ app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
app.state.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL app.state.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
app.state.RAG_TEMPLATE = RAG_TEMPLATE app.state.RAG_TEMPLATE = RAG_TEMPLATE
app.state.OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL app.state.OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
app.state.OPENAI_API_KEY = RAG_OPENAI_API_KEY app.state.OPENAI_API_KEY = RAG_OPENAI_API_KEY
...@@ -313,6 +318,7 @@ async def get_rag_config(user=Depends(get_admin_user)): ...@@ -313,6 +318,7 @@ async def get_rag_config(user=Depends(get_admin_user)):
"chunk_size": app.state.CHUNK_SIZE, "chunk_size": app.state.CHUNK_SIZE,
"chunk_overlap": app.state.CHUNK_OVERLAP, "chunk_overlap": app.state.CHUNK_OVERLAP,
}, },
"web_loader_ssl_verification": app.state.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
} }
...@@ -322,15 +328,34 @@ class ChunkParamUpdateForm(BaseModel): ...@@ -322,15 +328,34 @@ class ChunkParamUpdateForm(BaseModel):
class ConfigUpdateForm(BaseModel): class ConfigUpdateForm(BaseModel):
pdf_extract_images: bool pdf_extract_images: Optional[bool] = None
chunk: ChunkParamUpdateForm chunk: Optional[ChunkParamUpdateForm] = None
web_loader_ssl_verification: Optional[bool] = None
@app.post("/config/update") @app.post("/config/update")
async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)): async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)):
app.state.PDF_EXTRACT_IMAGES = form_data.pdf_extract_images app.state.PDF_EXTRACT_IMAGES = (
app.state.CHUNK_SIZE = form_data.chunk.chunk_size form_data.pdf_extract_images
app.state.CHUNK_OVERLAP = form_data.chunk.chunk_overlap if form_data.pdf_extract_images != None
else app.state.PDF_EXTRACT_IMAGES
)
app.state.CHUNK_SIZE = (
form_data.chunk.chunk_size if form_data.chunk != None else app.state.CHUNK_SIZE
)
app.state.CHUNK_OVERLAP = (
form_data.chunk.chunk_overlap
if form_data.chunk != None
else app.state.CHUNK_OVERLAP
)
app.state.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
form_data.web_loader_ssl_verification
if form_data.web_loader_ssl_verification != None
else app.state.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
)
return { return {
"status": True, "status": True,
...@@ -339,6 +364,7 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_ ...@@ -339,6 +364,7 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
"chunk_size": app.state.CHUNK_SIZE, "chunk_size": app.state.CHUNK_SIZE,
"chunk_overlap": app.state.CHUNK_OVERLAP, "chunk_overlap": app.state.CHUNK_OVERLAP,
}, },
"web_loader_ssl_verification": app.state.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
} }
...@@ -490,7 +516,9 @@ def store_youtube_video(form_data: UrlForm, user=Depends(get_current_user)): ...@@ -490,7 +516,9 @@ def store_youtube_video(form_data: UrlForm, user=Depends(get_current_user)):
def store_web(form_data: UrlForm, user=Depends(get_current_user)): def store_web(form_data: UrlForm, user=Depends(get_current_user)):
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm" # "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
try: try:
loader = get_web_loader(form_data.url) loader = get_web_loader(
form_data.url, verify_ssl=app.state.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
)
data = loader.load() data = loader.load()
collection_name = form_data.collection_name collection_name = form_data.collection_name
...@@ -510,12 +538,11 @@ def store_web(form_data: UrlForm, user=Depends(get_current_user)): ...@@ -510,12 +538,11 @@ def store_web(form_data: UrlForm, user=Depends(get_current_user)):
detail=ERROR_MESSAGES.DEFAULT(e), detail=ERROR_MESSAGES.DEFAULT(e),
) )
def get_web_loader(url: Union[str, Sequence[str]], verify_ssl: bool = True):
def get_web_loader(url: Union[str, Sequence[str]]):
# Check if the URL is valid # Check if the URL is valid
if not validate_url(url): if not validate_url(url):
raise ValueError(ERROR_MESSAGES.INVALID_URL) raise ValueError(ERROR_MESSAGES.INVALID_URL)
return WebBaseLoader(url) return WebBaseLoader(url, verify_ssl=verify_ssl)
def validate_url(url: Union[str, Sequence[str]]): def validate_url(url: Union[str, Sequence[str]]):
......
...@@ -287,14 +287,14 @@ def rag_messages( ...@@ -287,14 +287,14 @@ def rag_messages(
for doc in docs: for doc in docs:
context = None context = None
collection = doc.get("collection_name") collection_names = (
if collection: doc["collection_names"]
collection = [collection] if doc["type"] == "collection"
else: else [doc["collection_name"]]
collection = doc.get("collection_names", []) )
collection = set(collection).difference(extracted_collections) collection_names = set(collection_names).difference(extracted_collections)
if not collection: if not collection_names:
log.debug(f"skipping {doc} as it has already been extracted") log.debug(f"skipping {doc} as it has already been extracted")
continue continue
...@@ -304,11 +304,7 @@ def rag_messages( ...@@ -304,11 +304,7 @@ def rag_messages(
else: else:
if hybrid_search: if hybrid_search:
context = query_collection_with_hybrid_search( context = query_collection_with_hybrid_search(
collection_names=( collection_names=collection_names,
doc["collection_names"]
if doc["type"] == "collection"
else [doc["collection_name"]]
),
query=query, query=query,
embedding_function=embedding_function, embedding_function=embedding_function,
k=k, k=k,
...@@ -317,11 +313,7 @@ def rag_messages( ...@@ -317,11 +313,7 @@ def rag_messages(
) )
else: else:
context = query_collection( context = query_collection(
collection_names=( collection_names=collection_names,
doc["collection_names"]
if doc["type"] == "collection"
else [doc["collection_name"]]
),
query=query, query=query,
embedding_function=embedding_function, embedding_function=embedding_function,
k=k, k=k,
...@@ -331,18 +323,31 @@ def rag_messages( ...@@ -331,18 +323,31 @@ def rag_messages(
context = None context = None
if context: if context:
relevant_contexts.append(context) relevant_contexts.append({**context, "source": doc})
extracted_collections.extend(collection) extracted_collections.extend(collection_names)
context_string = "" context_string = ""
citations = []
for context in relevant_contexts: for context in relevant_contexts:
try: try:
if "documents" in context: if "documents" in context:
items = [item for item in context["documents"][0] if item is not None] context_string += "\n\n".join(
context_string += "\n\n".join(items) [text for text in context["documents"][0] if text is not None]
)
if "metadatas" in context:
citations.append(
{
"source": context["source"],
"document": context["documents"][0],
"metadata": context["metadatas"][0],
}
)
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)
context_string = context_string.strip() context_string = context_string.strip()
ra_content = rag_template( ra_content = rag_template(
...@@ -371,7 +376,7 @@ def rag_messages( ...@@ -371,7 +376,7 @@ def rag_messages(
messages[last_user_message_idx] = new_user_message messages[last_user_message_idx] = new_user_message
return messages return messages, citations
def get_model_path(model: str, update_model: bool = False): def get_model_path(model: str, update_model: bool = False):
......
...@@ -18,6 +18,18 @@ from secrets import token_bytes ...@@ -18,6 +18,18 @@ from secrets import token_bytes
from constants import ERROR_MESSAGES from constants import ERROR_MESSAGES
####################################
# Load .env file
####################################
try:
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv("../.env"))
except ImportError:
print("dotenv not installed, skipping...")
#################################### ####################################
# LOGGING # LOGGING
#################################### ####################################
...@@ -59,16 +71,6 @@ for source in log_sources: ...@@ -59,16 +71,6 @@ for source in log_sources:
log.setLevel(SRC_LOG_LEVELS["CONFIG"]) log.setLevel(SRC_LOG_LEVELS["CONFIG"])
####################################
# Load .env file
####################################
try:
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv("../.env"))
except ImportError:
log.warning("dotenv not installed, skipping...")
WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI") WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
if WEBUI_NAME != "Open WebUI": if WEBUI_NAME != "Open WebUI":
...@@ -454,6 +456,11 @@ ENABLE_RAG_HYBRID_SEARCH = ( ...@@ -454,6 +456,11 @@ ENABLE_RAG_HYBRID_SEARCH = (
os.environ.get("ENABLE_RAG_HYBRID_SEARCH", "").lower() == "true" os.environ.get("ENABLE_RAG_HYBRID_SEARCH", "").lower() == "true"
) )
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
os.environ.get("ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION", "True").lower() == "true"
)
RAG_EMBEDDING_ENGINE = os.environ.get("RAG_EMBEDDING_ENGINE", "") RAG_EMBEDDING_ENGINE = os.environ.get("RAG_EMBEDDING_ENGINE", "")
PDF_EXTRACT_IMAGES = os.environ.get("PDF_EXTRACT_IMAGES", "False").lower() == "true" PDF_EXTRACT_IMAGES = os.environ.get("PDF_EXTRACT_IMAGES", "False").lower() == "true"
...@@ -531,7 +538,9 @@ RAG_TEMPLATE = os.environ.get("RAG_TEMPLATE", DEFAULT_RAG_TEMPLATE) ...@@ -531,7 +538,9 @@ RAG_TEMPLATE = os.environ.get("RAG_TEMPLATE", DEFAULT_RAG_TEMPLATE)
RAG_OPENAI_API_BASE_URL = os.getenv("RAG_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL) RAG_OPENAI_API_BASE_URL = os.getenv("RAG_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL)
RAG_OPENAI_API_KEY = os.getenv("RAG_OPENAI_API_KEY", OPENAI_API_KEY) RAG_OPENAI_API_KEY = os.getenv("RAG_OPENAI_API_KEY", OPENAI_API_KEY)
ENABLE_LOCAL_WEB_FETCH = os.getenv("ENABLE_LOCAL_WEB_FETCH", "False").lower() == "true" ENABLE_RAG_LOCAL_WEB_FETCH = (
os.getenv("ENABLE_RAG_LOCAL_WEB_FETCH", "False").lower() == "true"
)
SEARXNG_QUERY_URL = os.getenv("SEARXNG_QUERY_URL", "") SEARXNG_QUERY_URL = os.getenv("SEARXNG_QUERY_URL", "")
GOOGLE_PSE_API_KEY = os.getenv("GOOGLE_PSE_API_KEY", "") GOOGLE_PSE_API_KEY = os.getenv("GOOGLE_PSE_API_KEY", "")
......
...@@ -15,7 +15,7 @@ from fastapi.middleware.wsgi import WSGIMiddleware ...@@ -15,7 +15,7 @@ from fastapi.middleware.wsgi import WSGIMiddleware
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import StreamingResponse
from apps.ollama.main import app as ollama_app from apps.ollama.main import app as ollama_app
from apps.openai.main import app as openai_app from apps.openai.main import app as openai_app
...@@ -102,6 +102,8 @@ origins = ["*"] ...@@ -102,6 +102,8 @@ origins = ["*"]
class RAGMiddleware(BaseHTTPMiddleware): class RAGMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next): async def dispatch(self, request: Request, call_next):
return_citations = False
if request.method == "POST" and ( if request.method == "POST" and (
"/api/chat" in request.url.path or "/chat/completions" in request.url.path "/api/chat" in request.url.path or "/chat/completions" in request.url.path
): ):
...@@ -114,11 +116,15 @@ class RAGMiddleware(BaseHTTPMiddleware): ...@@ -114,11 +116,15 @@ class RAGMiddleware(BaseHTTPMiddleware):
# Parse string to JSON # Parse string to JSON
data = json.loads(body_str) if body_str else {} data = json.loads(body_str) if body_str else {}
return_citations = data.get("citations", False)
if "citations" in data:
del data["citations"]
# Example: Add a new key-value pair or modify existing ones # Example: Add a new key-value pair or modify existing ones
# data["modified"] = True # Example modification # data["modified"] = True # Example modification
if "docs" in data: if "docs" in data:
data = {**data} data = {**data}
data["messages"] = rag_messages( data["messages"], citations = rag_messages(
docs=data["docs"], docs=data["docs"],
messages=data["messages"], messages=data["messages"],
template=rag_app.state.RAG_TEMPLATE, template=rag_app.state.RAG_TEMPLATE,
...@@ -130,7 +136,9 @@ class RAGMiddleware(BaseHTTPMiddleware): ...@@ -130,7 +136,9 @@ class RAGMiddleware(BaseHTTPMiddleware):
) )
del data["docs"] del data["docs"]
log.debug(f"data['messages']: {data['messages']}") log.debug(
f"data['messages']: {data['messages']}, citations: {citations}"
)
modified_body_bytes = json.dumps(data).encode("utf-8") modified_body_bytes = json.dumps(data).encode("utf-8")
...@@ -148,11 +156,36 @@ class RAGMiddleware(BaseHTTPMiddleware): ...@@ -148,11 +156,36 @@ class RAGMiddleware(BaseHTTPMiddleware):
] ]
response = await call_next(request) response = await call_next(request)
if return_citations:
# Inject the citations into the response
if isinstance(response, StreamingResponse):
# If it's a streaming response, inject it as SSE event or NDJSON line
content_type = response.headers.get("Content-Type")
if "text/event-stream" in content_type:
return StreamingResponse(
self.openai_stream_wrapper(response.body_iterator, citations),
)
if "application/x-ndjson" in content_type:
return StreamingResponse(
self.ollama_stream_wrapper(response.body_iterator, citations),
)
return response return response
async def _receive(self, body: bytes): async def _receive(self, body: bytes):
return {"type": "http.request", "body": body, "more_body": False} return {"type": "http.request", "body": body, "more_body": False}
async def openai_stream_wrapper(self, original_generator, citations):
yield f"data: {json.dumps({'citations': citations})}\n\n"
async for data in original_generator:
yield data
async def ollama_stream_wrapper(self, original_generator, citations):
yield f"{json.dumps({'citations': citations})}\n"
async for data in original_generator:
yield data
app.add_middleware(RAGMiddleware) app.add_middleware(RAGMiddleware)
......
...@@ -82,3 +82,12 @@ select { ...@@ -82,3 +82,12 @@ select {
.katex-mathml { .katex-mathml {
display: none; display: none;
} }
.scrollbar-none:active::-webkit-scrollbar-thumb,
.scrollbar-none:focus::-webkit-scrollbar-thumb,
.scrollbar-none:hover::-webkit-scrollbar-thumb {
visibility: visible;
}
.scrollbar-none::-webkit-scrollbar-thumb {
visibility: hidden;
}
...@@ -159,7 +159,11 @@ export const generateTitle = async ( ...@@ -159,7 +159,11 @@ export const generateTitle = async (
body: JSON.stringify({ body: JSON.stringify({
model: model, model: model,
prompt: template, prompt: template,
stream: false stream: false,
options: {
// Restrict the number of tokens generated to 50
num_predict: 50
}
}) })
}) })
.then(async (res) => { .then(async (res) => {
......
...@@ -295,7 +295,9 @@ export const generateTitle = async ( ...@@ -295,7 +295,9 @@ export const generateTitle = async (
content: template content: template
} }
], ],
stream: false stream: false,
// Restricting the max tokens to 50 to avoid long titles
max_tokens: 50
}) })
}) })
.then(async (res) => { .then(async (res) => {
......
...@@ -33,8 +33,9 @@ type ChunkConfigForm = { ...@@ -33,8 +33,9 @@ type ChunkConfigForm = {
}; };
type RAGConfigForm = { type RAGConfigForm = {
pdf_extract_images: boolean; pdf_extract_images?: boolean;
chunk: ChunkConfigForm; chunk?: ChunkConfigForm;
web_loader_ssl_verification?: boolean;
}; };
export const updateRAGConfig = async (token: string, payload: RAGConfigForm) => { export const updateRAGConfig = async (token: string, payload: RAGConfigForm) => {
......
...@@ -4,6 +4,8 @@ import type { ParsedEvent } from 'eventsource-parser'; ...@@ -4,6 +4,8 @@ import type { ParsedEvent } from 'eventsource-parser';
type TextStreamUpdate = { type TextStreamUpdate = {
done: boolean; done: boolean;
value: string; value: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
citations?: any;
}; };
// createOpenAITextStream takes a responseBody with a SSE response, // createOpenAITextStream takes a responseBody with a SSE response,
...@@ -45,6 +47,11 @@ async function* openAIStreamToIterator( ...@@ -45,6 +47,11 @@ async function* openAIStreamToIterator(
const parsedData = JSON.parse(data); const parsedData = JSON.parse(data);
console.log(parsedData); console.log(parsedData);
if (parsedData.citations) {
yield { done: false, value: '', citations: parsedData.citations };
continue;
}
yield { done: false, value: parsedData.choices?.[0]?.delta?.content ?? '' }; yield { done: false, value: parsedData.choices?.[0]?.delta?.content ?? '' };
} catch (e) { } catch (e) {
console.error('Error extracting delta from SSE event:', e); console.error('Error extracting delta from SSE event:', e);
...@@ -62,6 +69,10 @@ async function* streamLargeDeltasAsRandomChunks( ...@@ -62,6 +69,10 @@ async function* streamLargeDeltasAsRandomChunks(
yield textStreamUpdate; yield textStreamUpdate;
return; return;
} }
if (textStreamUpdate.citations) {
yield textStreamUpdate;
continue;
}
let content = textStreamUpdate.value; let content = textStreamUpdate.value;
if (content.length < 5) { if (content.length < 5) {
yield { done: false, value: content }; yield { done: false, value: content };
......
<script lang="ts">
import { getContext, onMount, tick } from 'svelte';
import Modal from '$lib/components/common/Modal.svelte';
const i18n = getContext('i18n');
export let show = false;
export let citation;
let mergedDocuments = [];
$: if (citation) {
mergedDocuments = citation.document?.map((c, i) => {
return {
source: citation.source,
document: c,
metadata: citation.metadata?.[i]
};
});
}
</script>
<Modal size="lg" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
<div class=" text-lg font-medium self-center capitalize">
{$i18n.t('Citation')}
</div>
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
<div class="flex flex-col md:flex-row w-full px-6 pb-5 md:space-x-4">
<div
class="flex flex-col w-full dark:text-gray-200 overflow-y-scroll max-h-[22rem] scrollbar-none"
>
{#each mergedDocuments as document, documentIdx}
<div class="flex flex-col w-full">
<div class="text-sm font-medium dark:text-gray-300">
{$i18n.t('Source')}
</div>
<div class="text-sm dark:text-gray-400">
{document.source?.name ?? $i18n.t('No source available')}
</div>
</div>
<div class="flex flex-col w-full">
<div class=" text-sm font-medium dark:text-gray-300">
{$i18n.t('Content')}
</div>
<pre class="text-sm dark:text-gray-400 whitespace-pre-line">
{document.document}
</pre>
</div>
{#if documentIdx !== mergedDocuments.length - 1}
<hr class=" dark:border-gray-850 my-3" />
{/if}
{/each}
</div>
</div>
</div>
</Modal>
...@@ -32,6 +32,7 @@ ...@@ -32,6 +32,7 @@
import { WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_BASE_URL } from '$lib/constants';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import RateComment from './RateComment.svelte'; import RateComment from './RateComment.svelte';
import CitationsModal from '$lib/components/chat/Messages/CitationsModal.svelte';
export let modelfiles = []; export let modelfiles = [];
export let message; export let message;
...@@ -65,6 +66,9 @@ ...@@ -65,6 +66,9 @@
let showRateComment = false; let showRateComment = false;
let showCitationModal = false;
let selectedCitation = null;
$: tokens = marked.lexer(sanitizeResponseContent(message.content)); $: tokens = marked.lexer(sanitizeResponseContent(message.content));
const renderer = new marked.Renderer(); const renderer = new marked.Renderer();
...@@ -324,6 +328,8 @@ ...@@ -324,6 +328,8 @@
}); });
</script> </script>
<CitationsModal bind:show={showCitationModal} citation={selectedCitation} />
{#key message.id} {#key message.id}
<div class=" flex w-full message-{message.id}" id="message-{message.id}"> <div class=" flex w-full message-{message.id}" id="message-{message.id}">
<ProfileImage <ProfileImage
...@@ -360,7 +366,6 @@ ...@@ -360,7 +366,6 @@
{/each} {/each}
</div> </div>
{/if} {/if}
<div <div
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line" class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line"
> >
...@@ -441,6 +446,66 @@ ...@@ -441,6 +446,66 @@
{/each} {/each}
<!-- {@html marked(message.content.replaceAll('\\', '\\\\'))} --> <!-- {@html marked(message.content.replaceAll('\\', '\\\\'))} -->
{/if} {/if}
</div>
{/if}
</div>
</div>
<!-- if (message.citations) {
citations = message.citations.forEach((citation) => {
citation.document.forEach((document, index) => {
const metadata = citation.metadata?.[index];
const source = citation?.source?.name ?? metadata?.source ?? 'N/A';
citations[source] = citations[source] || {
source: citation.source,
document: [],
metadata: []
};
citations[source].document.push(document);
citations[source].metadata.push(metadata);
});
});
} -->
{#if message.citations}
<hr class=" dark:border-gray-800" />
<div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
{#each message.citations.reduce((acc, citation) => {
citation.document.forEach((document, index) => {
const metadata = citation.metadata?.[index];
const id = metadata?.source ?? 'N/A';
const existingSource = acc.find((item) => item.id === id);
if (existingSource) {
existingSource.document.push(document);
existingSource.metadata.push(metadata);
} else {
acc.push( { id: id, source: citation?.source, document: [document], metadata: metadata ? [metadata] : [] } );
}
});
return acc;
}, []) as citation, idx}
<div class="flex gap-1 text-xs font-semibold">
<div>
[{idx + 1}]
</div>
<button
class="dark:text-gray-500 underline"
on:click={() => {
showCitationModal = true;
selectedCitation = citation;
}}
>
{citation.source.name}
</button>
</div>
{/each}
</div>
{/if}
{#if message.done} {#if message.done}
<div <div
...@@ -553,8 +618,8 @@ ...@@ -553,8 +618,8 @@
<button <button
class="{isLastMessage class="{isLastMessage
? 'visible' ? 'visible'
: 'invisible group-hover:visible'} p-1 rounded {message?.annotation : 'invisible group-hover:visible'} p-1 rounded {message?.annotation?.rating ===
?.rating === 1 1
? 'bg-gray-100 dark:bg-gray-800' ? 'bg-gray-100 dark:bg-gray-800'
: ''} dark:hover:text-white hover:text-black transition" : ''} dark:hover:text-white hover:text-black transition"
on:click={() => { on:click={() => {
...@@ -562,9 +627,7 @@ ...@@ -562,9 +627,7 @@
showRateComment = true; showRateComment = true;
window.setTimeout(() => { window.setTimeout(() => {
document document.getElementById(`message-feedback-${message.id}`)?.scrollIntoView();
.getElementById(`message-feedback-${message.id}`)
?.scrollIntoView();
}, 0); }, 0);
}} }}
> >
...@@ -577,10 +640,11 @@ ...@@ -577,10 +640,11 @@
stroke-linejoin="round" stroke-linejoin="round"
class="w-4 h-4" class="w-4 h-4"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
><path
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
/></svg
> >
<path
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
/>
</svg>
</button> </button>
</Tooltip> </Tooltip>
...@@ -588,17 +652,15 @@ ...@@ -588,17 +652,15 @@
<button <button
class="{isLastMessage class="{isLastMessage
? 'visible' ? 'visible'
: 'invisible group-hover:visible'} p-1 rounded {message?.annotation : 'invisible group-hover:visible'} p-1 rounded {message?.annotation?.rating ===
?.rating === -1 -1
? 'bg-gray-100 dark:bg-gray-800' ? 'bg-gray-100 dark:bg-gray-800'
: ''} dark:hover:text-white hover:text-black transition" : ''} dark:hover:text-white hover:text-black transition"
on:click={() => { on:click={() => {
rateMessage(message.id, -1); rateMessage(message.id, -1);
showRateComment = true; showRateComment = true;
window.setTimeout(() => { window.setTimeout(() => {
document document.getElementById(`message-feedback-${message.id}`)?.scrollIntoView();
.getElementById(`message-feedback-${message.id}`)
?.scrollIntoView();
}, 0); }, 0);
}} }}
> >
...@@ -611,10 +673,11 @@ ...@@ -611,10 +673,11 @@
stroke-linejoin="round" stroke-linejoin="round"
class="w-4 h-4" class="w-4 h-4"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
><path
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
/></svg
> >
<path
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
/>
</svg>
</button> </button>
</Tooltip> </Tooltip>
{/if} {/if}
...@@ -637,35 +700,32 @@ ...@@ -637,35 +700,32 @@
fill="currentColor" fill="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
><style> >
<style>
.spinner_S1WN { .spinner_S1WN {
animation: spinner_MGfb 0.8s linear infinite; animation: spinner_MGfb 0.8s linear infinite;
animation-delay: -0.8s; animation-delay: -0.8s;
} }
.spinner_Km9P { .spinner_Km9P {
animation-delay: -0.65s; animation-delay: -0.65s;
} }
.spinner_JApP { .spinner_JApP {
animation-delay: -0.5s; animation-delay: -0.5s;
} }
@keyframes spinner_MGfb { @keyframes spinner_MGfb {
93.75%, 93.75%,
100% { 100% {
opacity: 0.2; opacity: 0.2;
} }
} }
</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle </style>
class="spinner_S1WN spinner_Km9P" <circle class="spinner_S1WN" cx="4" cy="12" r="3" />
cx="12" <circle class="spinner_S1WN spinner_Km9P" cx="12" cy="12" r="3" />
cy="12" <circle class="spinner_S1WN spinner_JApP" cx="20" cy="12" r="3" />
r="3" </svg>
/><circle
class="spinner_S1WN spinner_JApP"
cx="20"
cy="12"
r="3"
/></svg
>
{:else if speaking} {:else if speaking}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
...@@ -718,35 +778,32 @@ ...@@ -718,35 +778,32 @@
fill="currentColor" fill="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
><style> >
<style>
.spinner_S1WN { .spinner_S1WN {
animation: spinner_MGfb 0.8s linear infinite; animation: spinner_MGfb 0.8s linear infinite;
animation-delay: -0.8s; animation-delay: -0.8s;
} }
.spinner_Km9P { .spinner_Km9P {
animation-delay: -0.65s; animation-delay: -0.65s;
} }
.spinner_JApP { .spinner_JApP {
animation-delay: -0.5s; animation-delay: -0.5s;
} }
@keyframes spinner_MGfb { @keyframes spinner_MGfb {
93.75%, 93.75%,
100% { 100% {
opacity: 0.2; opacity: 0.2;
} }
} }
</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle </style>
class="spinner_S1WN spinner_Km9P" <circle class="spinner_S1WN" cx="4" cy="12" r="3" />
cx="12" <circle class="spinner_S1WN spinner_Km9P" cx="12" cy="12" r="3" />
cy="12" <circle class="spinner_S1WN spinner_JApP" cx="20" cy="12" r="3" />
r="3" </svg>
/><circle
class="spinner_S1WN spinner_JApP"
cx="20"
cy="12"
r="3"
/></svg
>
{:else} {:else}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
...@@ -867,10 +924,6 @@ ...@@ -867,10 +924,6 @@
}} }}
/> />
{/if} {/if}
</div>
{/if}
</div>
</div>
{/if} {/if}
</div> </div>
</div> </div>
......
...@@ -82,7 +82,7 @@ ...@@ -82,7 +82,7 @@
</div> </div>
{:else} {:else}
<div class=" self-center disabled:text-gray-600 disabled:hover:text-gray-600 mr-2"> <div class=" self-center disabled:text-gray-600 disabled:hover:text-gray-600 mr-2">
<Tooltip content="Remove Model"> <Tooltip content={$i18n.t('Remove Model')}>
<button <button
{disabled} {disabled}
on:click={() => { on:click={() => {
......
...@@ -305,7 +305,7 @@ ...@@ -305,7 +305,7 @@
{:else} {:else}
<div> <div>
<div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100"> <div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100">
No results found {$i18n.t('No results found')}
</div> </div>
</div> </div>
{/each} {/each}
...@@ -317,7 +317,7 @@ ...@@ -317,7 +317,7 @@
pullModelHandler(); pullModelHandler();
}} }}
> >
Pull "{searchValue}" from Ollama.com {$i18n.t(`Pull "{{searchValue}}" from Ollama.com`, { searchValue: searchValue })}
</button> </button>
{/if} {/if}
......
...@@ -447,7 +447,7 @@ ...@@ -447,7 +447,7 @@
{/if} {/if}
</button> </button>
<Tooltip content="Create new key"> <Tooltip content={$i18n.t('Create new key')}>
<button <button
class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg" class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg"
on:click={() => { on:click={() => {
...@@ -479,7 +479,7 @@ ...@@ -479,7 +479,7 @@
> >
<Plus strokeWidth="2" className=" size-3.5" /> <Plus strokeWidth="2" className=" size-3.5" />
Create new secret key</button {$i18n.t('Create new secret key')}</button
> >
{/if} {/if}
</div> </div>
......
...@@ -164,7 +164,7 @@ ...@@ -164,7 +164,7 @@
<div class="flex gap-1.5"> <div class="flex gap-1.5">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter URL (e.g. http://localhost:11434)" placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
bind:value={url} bind:value={url}
/> />
......
...@@ -97,9 +97,10 @@ ...@@ -97,9 +97,10 @@
<div class=" text-sm dark:text-gray-300 mb-1"> <div class=" text-sm dark:text-gray-300 mb-1">
{#if chat.share_id} {#if chat.share_id}
<a href="/s/{chat.share_id}" target="_blank" <a href="/s/{chat.share_id}" target="_blank"
>You have shared this chat <span class=" underline">before</span>.</a >{$i18n.t('You have shared this chat')}
<span class=" underline">{$i18n.t('before')}</span>.</a
> >
Click here to {$i18n.t('Click here to')}
<button <button
class="underline" class="underline"
on:click={async () => { on:click={async () => {
...@@ -108,11 +109,14 @@ ...@@ -108,11 +109,14 @@
if (res) { if (res) {
chat = await getChatById(localStorage.token, chatId); chat = await getChatById(localStorage.token, chatId);
} }
}}>delete this link</button }}
> and create a new shared link. >{$i18n.t('delete this link')}
</button>
{$i18n.t('and create a new shared link.')}
{:else} {:else}
Messages you send after creating your link won't be shared. Users with the URL will be {$i18n.t(
able to view the shared chat. "Messages you send after creating your link won't be shared. Users with the URL will beable to view the shared chat."
)}
{/if} {/if}
</div> </div>
......
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
<button <button
class=" p-5" class=" p-5"
on:click={() => { on:click={() => {
downloadImage(src, 'Image.png'); downloadImage(src, src.substring(src.lastIndexOf('/') + 1));
}} }}
> >
<svg <svg
......
<script lang="ts">
import { getDocs } from '$lib/apis/documents';
import {
getRAGConfig,
updateRAGConfig,
getQuerySettings,
scanDocs,
updateQuerySettings,
resetVectorDB,
getEmbeddingConfig,
updateEmbeddingConfig,
getRerankingConfig,
updateRerankingConfig
} from '$lib/apis/rag';
import { documents, models } from '$lib/stores';
import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
export let saveHandler: Function;
let scanDirLoading = false;
let updateEmbeddingModelLoading = false;
let updateRerankingModelLoading = false;
let showResetConfirm = false;
let chunkSize = 0;
let chunkOverlap = 0;
let pdfExtractImages = true;
const submitHandler = async () => {
const res = await updateRAGConfig(localStorage.token, {
pdf_extract_images: pdfExtractImages,
chunk: {
chunk_overlap: chunkOverlap,
chunk_size: chunkSize
}
});
};
onMount(async () => {
const res = await getRAGConfig(localStorage.token);
if (res) {
pdfExtractImages = res.pdf_extract_images;
chunkSize = res.chunk.chunk_size;
chunkOverlap = res.chunk.chunk_overlap;
}
});
</script>
<form
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={() => {
submitHandler();
saveHandler();
}}
>
<div class=" space-y-3 pr-1.5 overflow-y-scroll h-full max-h-[22rem]">
<div class=" ">
<div class=" text-sm font-medium">{$i18n.t('Chunk Params')}</div>
<div class=" flex">
<div class=" flex w-full justify-between">
<div class="self-center text-xs font-medium min-w-fit">{$i18n.t('Chunk Size')}</div>
<div class="self-center p-3">
<input
class=" w-full rounded-lg py-1.5 px-4 text-sm 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="flex w-full">
<div class=" self-center text-xs font-medium min-w-fit">
{$i18n.t('Chunk Overlap')}
</div>
<div class="self-center p-3">
<input
class="w-full rounded-lg py-1.5 px-4 text-sm 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="pr-2">
<div class="flex justify-between items-center text-xs">
<div class=" text-xs font-medium">{$i18n.t('PDF Extract Images (OCR)')}</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
pdfExtractImages = !pdfExtractImages;
}}>{pdfExtractImages ? $i18n.t('On') : $i18n.t('Off')}</button
>
</div>
</div>
</div>
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"
>
{$i18n.t('Save')}
</button>
</div>
</form>
<script lang="ts"> <script lang="ts">
import { getDocs } from '$lib/apis/documents'; import { getDocs } from '$lib/apis/documents';
import { import {
getRAGConfig,
updateRAGConfig,
getQuerySettings, getQuerySettings,
scanDocs, scanDocs,
updateQuerySettings, updateQuerySettings,
...@@ -17,8 +15,6 @@ ...@@ -17,8 +15,6 @@
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let saveHandler: Function; export let saveHandler: Function;
...@@ -36,10 +32,6 @@ ...@@ -36,10 +32,6 @@
let OpenAIKey = ''; let OpenAIKey = '';
let OpenAIUrl = ''; let OpenAIUrl = '';
let chunkSize = 0;
let chunkOverlap = 0;
let pdfExtractImages = true;
let querySettings = { let querySettings = {
template: '', template: '',
r: 0.0, r: 0.0,
...@@ -151,14 +143,11 @@ ...@@ -151,14 +143,11 @@
}; };
const submitHandler = async () => { const submitHandler = async () => {
const res = await updateRAGConfig(localStorage.token, { embeddingModelUpdateHandler();
pdf_extract_images: pdfExtractImages,
chunk: { if (querySettings.hybrid) {
chunk_overlap: chunkOverlap, rerankingModelUpdateHandler();
chunk_size: chunkSize
} }
});
querySettings = await updateQuerySettings(localStorage.token, querySettings);
}; };
const setEmbeddingConfig = async () => { const setEmbeddingConfig = async () => {
...@@ -183,20 +172,10 @@ ...@@ -183,20 +172,10 @@
const toggleHybridSearch = async () => { const toggleHybridSearch = async () => {
querySettings.hybrid = !querySettings.hybrid; querySettings.hybrid = !querySettings.hybrid;
querySettings = await updateQuerySettings(localStorage.token, querySettings); querySettings = await updateQuerySettings(localStorage.token, querySettings);
}; };
onMount(async () => { onMount(async () => {
const res = await getRAGConfig(localStorage.token);
if (res) {
pdfExtractImages = res.pdf_extract_images;
chunkSize = res.chunk.chunk_size;
chunkOverlap = res.chunk.chunk_overlap;
}
await setEmbeddingConfig(); await setEmbeddingConfig();
await setRerankingConfig(); await setRerankingConfig();
...@@ -211,24 +190,54 @@ ...@@ -211,24 +190,54 @@
saveHandler(); saveHandler();
}} }}
> >
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[22rem]"> <div class=" space-y-2.5 pr-1.5 overflow-y-scroll max-h-[22rem]">
<div> <div class="flex flex-col gap-0.5">
<div class=" mb-2 text-sm font-medium">{$i18n.t('General Settings')}</div> <div class=" mb-0.5 text-sm font-medium">{$i18n.t('General Settings')}</div>
<div class=" flex w-full justify-between"> <div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Hybrid Search')}</div> <div class=" self-center text-xs font-medium">
{$i18n.t('Scan for documents from {{path}}', { path: '/data/docs' })}
</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class=" self-center text-xs p-1 px-3 bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded-lg flex flex-row space-x-1 items-center {scanDirLoading
? ' cursor-not-allowed'
: ''}"
on:click={() => { on:click={() => {
toggleHybridSearch(); scanHandler();
console.log('check');
}} }}
type="button" type="button"
disabled={scanDirLoading}
> >
{#if querySettings.hybrid === true} <div class="self-center font-medium">{$i18n.t('Scan')}</div>
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else} {#if scanDirLoading}
<span class="ml-2 self-center">{$i18n.t('Off')}</span> <div class="ml-3 self-center">
<svg
class=" w-3 h-3"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{/if} {/if}
</button> </button>
</div> </div>
...@@ -256,7 +265,7 @@ ...@@ -256,7 +265,7 @@
</div> </div>
{#if embeddingEngine === 'openai'} {#if embeddingEngine === 'openai'}
<div class="mt-1 flex gap-2"> <div class="my-0.5 flex gap-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
...@@ -272,11 +281,31 @@ ...@@ -272,11 +281,31 @@
/> />
</div> </div>
{/if} {/if}
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Hybrid Search')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
toggleHybridSearch();
}}
type="button"
>
{#if querySettings.hybrid === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div> </div>
</div>
<hr class=" dark:border-gray-700 my-1" />
<div class="space-y-2"> <div class="space-y-2" />
<div> <div>
<div class=" mb-2 text-sm font-medium">{$i18n.t('Update Embedding Model')}</div> <div class=" mb-2 text-sm font-medium">{$i18n.t('Embedding Model')}</div>
{#if embeddingEngine === 'ollama'} {#if embeddingEngine === 'ollama'}
<div class="flex w-full"> <div class="flex w-full">
...@@ -297,66 +326,20 @@ ...@@ -297,66 +326,20 @@
{/each} {/each}
</select> </select>
</div> </div>
<button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => {
embeddingModelUpdateHandler();
}}
disabled={updateEmbeddingModelLoading}
>
{#if updateEmbeddingModelLoading}
<div class="self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{:else}
<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="M12.416 3.376a.75.75 0 0 1 .208 1.04l-5 7.5a.75.75 0 0 1-1.154.114l-3-3a.75.75 0 0 1 1.06-1.06l2.353 2.353 4.493-6.74a.75.75 0 0 1 1.04-.207Z"
clip-rule="evenodd"
/>
</svg>
{/if}
</button>
</div> </div>
{:else} {:else}
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Update embedding model (e.g. {{model}})', { placeholder={$i18n.t('Set embedding model (e.g. {{model}})', {
model: embeddingModel.slice(-40) model: embeddingModel.slice(-40)
})} })}
bind:value={embeddingModel} bind:value={embeddingModel}
/> />
</div> </div>
{#if embeddingEngine === ''}
<button <button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => { on:click={() => {
...@@ -406,6 +389,7 @@ ...@@ -406,6 +389,7 @@
</svg> </svg>
{/if} {/if}
</button> </button>
{/if}
</div> </div>
{/if} {/if}
...@@ -415,18 +399,16 @@ ...@@ -415,18 +399,16 @@
)} )}
</div> </div>
<hr class=" dark:border-gray-700 my-3" />
{#if querySettings.hybrid === true} {#if querySettings.hybrid === true}
<div class=" "> <div class=" ">
<div class=" mb-2 text-sm font-medium">{$i18n.t('Update Reranking Model')}</div> <div class=" mb-2 text-sm font-medium">{$i18n.t('Reranking Model')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Update reranking model (e.g. {{model}})', { placeholder={$i18n.t('Set reranking model (e.g. {{model}})', {
model: rerankingModel.slice(-40) model: 'BAAI/bge-reranker-v2-m3'
})} })}
bind:value={rerankingModel} bind:value={rerankingModel}
/> />
...@@ -482,174 +464,10 @@ ...@@ -482,174 +464,10 @@
</button> </button>
</div> </div>
</div> </div>
<hr class=" dark:border-gray-700 my-3" />
{/if}
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Scan for documents from {{path}}', { path: '/data/docs' })}
</div>
<button
class=" self-center text-xs p-1 px-3 bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded-lg flex flex-row space-x-1 items-center {scanDirLoading
? ' cursor-not-allowed'
: ''}"
on:click={() => {
scanHandler();
console.log('check');
}}
type="button"
disabled={scanDirLoading}
>
<div class="self-center font-medium">{$i18n.t('Scan')}</div>
{#if scanDirLoading}
<div class="ml-3 self-center">
<svg
class=" w-3 h-3"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{/if} {/if}
</button>
</div> </div>
<hr class=" dark:border-gray-700 my-3" /> <hr class=" dark:border-gray-700" />
<div class=" ">
<div class=" text-sm font-medium">{$i18n.t('Chunk Params')}</div>
<div class=" flex">
<div class=" flex w-full justify-between">
<div class="self-center text-xs font-medium min-w-fit">{$i18n.t('Chunk Size')}</div>
<div class="self-center p-3">
<input
class=" w-full rounded-lg py-1.5 px-4 text-sm 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="flex w-full">
<div class=" self-center text-xs font-medium min-w-fit">
{$i18n.t('Chunk Overlap')}
</div>
<div class="self-center p-3">
<input
class="w-full rounded-lg py-1.5 px-4 text-sm 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="pr-2">
<div class="flex justify-between items-center text-xs">
<div class=" text-xs font-medium">{$i18n.t('PDF Extract Images (OCR)')}</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
pdfExtractImages = !pdfExtractImages;
}}>{pdfExtractImages ? $i18n.t('On') : $i18n.t('Off')}</button
>
</div>
</div>
</div>
<hr class=" dark:border-gray-700 my-3" />
<div class=" ">
<div class=" text-sm font-medium">{$i18n.t('Query Params')}</div>
<div class=" flex">
<div class=" flex w-full justify-between">
<div class="self-center text-xs font-medium min-w-fit">{$i18n.t('Top K')}</div>
<div class="self-center p-3">
<input
class=" w-full rounded-lg py-1.5 px-4 text-sm 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>
</div>
{#if querySettings.hybrid === true}
<div class="flex w-full">
<div class=" self-center text-xs font-medium min-w-fit">
{$i18n.t('Minimum Score')}
</div>
<div class="self-center p-3">
<input
class=" w-full rounded-lg py-1.5 px-4 text-sm 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 mb-1 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>
<hr class=" dark:border-gray-700 my-3" />
{/if}
<div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('RAG Template')}</div>
<textarea
bind:value={querySettings.template}
class="w-full rounded-lg px-4 py-3 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
rows="4"
/>
</div>
</div>
{#if showResetConfirm} {#if showResetConfirm}
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition"> <div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
...@@ -743,8 +561,6 @@ ...@@ -743,8 +561,6 @@
</button> </button>
{/if} {/if}
</div> </div>
</div>
</div>
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end pt-3 text-sm font-medium">
<button <button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment