(sync:locales) with static content

This commit is contained in:
ManishMadan2882
2025-01-18 03:46:05 +05:30
33 changed files with 798 additions and 305 deletions

View File

@@ -551,7 +551,7 @@ class CombinedJson(Resource):
user = "local" user = "local"
data = [ data = [
{ {
"name": "default", "name": "Default",
"date": "default", "date": "default",
"model": settings.EMBEDDINGS_NAME, "model": settings.EMBEDDINGS_NAME,
"location": "remote", "location": "remote",
@@ -2105,4 +2105,4 @@ class DeleteTool(Resource):
except Exception as err: except Exception as err:
return {"success": False, "error": str(err)}, 400 return {"success": False, "error": str(err)}, 400
return {"success": True}, 200 return {"success": True}, 200

View File

@@ -2,16 +2,16 @@ import requests
from urllib.parse import urlparse, urljoin from urllib.parse import urlparse, urljoin
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from application.parser.remote.base import BaseRemote from application.parser.remote.base import BaseRemote
from application.parser.schema.base import Document
from langchain_community.document_loaders import WebBaseLoader
class CrawlerLoader(BaseRemote): class CrawlerLoader(BaseRemote):
def __init__(self, limit=10): def __init__(self, limit=10):
from langchain_community.document_loaders import WebBaseLoader
self.loader = WebBaseLoader # Initialize the document loader self.loader = WebBaseLoader # Initialize the document loader
self.limit = limit # Set the limit for the number of pages to scrape self.limit = limit # Set the limit for the number of pages to scrape
def load_data(self, inputs): def load_data(self, inputs):
url = inputs url = inputs
# Check if the input is a list and if it is, use the first element
if isinstance(url, list) and url: if isinstance(url, list) and url:
url = url[0] url = url[0]
@@ -19,24 +19,29 @@ class CrawlerLoader(BaseRemote):
if not urlparse(url).scheme: if not urlparse(url).scheme:
url = "http://" + url url = "http://" + url
visited_urls = set() # Keep track of URLs that have been visited visited_urls = set()
base_url = urlparse(url).scheme + "://" + urlparse(url).hostname # Extract the base URL base_url = urlparse(url).scheme + "://" + urlparse(url).hostname
urls_to_visit = [url] # List of URLs to be visited, starting with the initial URL urls_to_visit = [url]
loaded_content = [] # Store the loaded content from each URL loaded_content = []
# Continue crawling until there are no more URLs to visit
while urls_to_visit: while urls_to_visit:
current_url = urls_to_visit.pop(0) # Get the next URL to visit current_url = urls_to_visit.pop(0)
visited_urls.add(current_url) # Mark the URL as visited visited_urls.add(current_url)
# Try to load and process the content from the current URL
try: try:
response = requests.get(current_url) # Fetch the content of the current URL response = requests.get(current_url)
response.raise_for_status() # Raise an exception for HTTP errors response.raise_for_status()
loader = self.loader([current_url]) # Initialize the document loader for the current URL loader = self.loader([current_url])
loaded_content.extend(loader.load()) # Load the content and add it to the loaded_content list docs = loader.load()
# Convert the loaded documents to your Document schema
for doc in docs:
loaded_content.append(
Document(
doc.page_content,
extra_info=doc.metadata
)
)
except Exception as e: except Exception as e:
# Print an error message if loading or processing fails and continue with the next URL
print(f"Error processing URL {current_url}: {e}") print(f"Error processing URL {current_url}: {e}")
continue continue
@@ -45,15 +50,15 @@ class CrawlerLoader(BaseRemote):
all_links = [ all_links = [
urljoin(current_url, a['href']) urljoin(current_url, a['href'])
for a in soup.find_all('a', href=True) for a in soup.find_all('a', href=True)
if base_url in urljoin(current_url, a['href']) # Ensure links are from the same domain if base_url in urljoin(current_url, a['href'])
] ]
# Add new links to the list of URLs to visit if they haven't been visited yet # Add new links to the list of URLs to visit if they haven't been visited yet
urls_to_visit.extend([link for link in all_links if link not in visited_urls]) urls_to_visit.extend([link for link in all_links if link not in visited_urls])
urls_to_visit = list(set(urls_to_visit)) # Remove duplicate URLs urls_to_visit = list(set(urls_to_visit))
# Stop crawling if the limit of pages to scrape is reached # Stop crawling if the limit of pages to scrape is reached
if self.limit is not None and len(visited_urls) >= self.limit: if self.limit is not None and len(visited_urls) >= self.limit:
break break
return loaded_content # Return the loaded content from all visited URLs return loaded_content

View File

@@ -0,0 +1,139 @@
import requests
from urllib.parse import urlparse, urljoin
from bs4 import BeautifulSoup
from application.parser.remote.base import BaseRemote
import re
from markdownify import markdownify
from application.parser.schema.base import Document
import tldextract
class CrawlerLoader(BaseRemote):
def __init__(self, limit=10, allow_subdomains=False):
"""
Given a URL crawl web pages up to `self.limit`,
convert HTML content to Markdown, and returning a list of Document objects.
:param limit: The maximum number of pages to crawl.
:param allow_subdomains: If True, crawl pages on subdomains of the base domain.
"""
self.limit = limit
self.allow_subdomains = allow_subdomains
self.session = requests.Session()
def load_data(self, inputs):
url = inputs
if isinstance(url, list) and url:
url = url[0]
# Ensure the URL has a scheme (if not, default to http)
if not urlparse(url).scheme:
url = "http://" + url
# Keep track of visited URLs to avoid revisiting the same page
visited_urls = set()
# Determine the base domain for link filtering using tldextract
base_domain = self._get_base_domain(url)
urls_to_visit = {url}
documents = []
while urls_to_visit:
current_url = urls_to_visit.pop()
# Skip if already visited
if current_url in visited_urls:
continue
visited_urls.add(current_url)
# Fetch the page content
html_content = self._fetch_page(current_url)
if html_content is None:
continue
# Convert the HTML to Markdown for cleaner text formatting
title, language, processed_markdown = self._process_html_to_markdown(html_content, current_url)
if processed_markdown:
# Create a Document for each visited page
documents.append(
Document(
processed_markdown, # content
None, # doc_id
None, # embedding
{"source": current_url, "title": title, "language": language} # extra_info
)
)
# Extract links and filter them according to domain rules
new_links = self._extract_links(html_content, current_url)
filtered_links = self._filter_links(new_links, base_domain)
# Add any new, not-yet-visited links to the queue
urls_to_visit.update(link for link in filtered_links if link not in visited_urls)
# If we've reached the limit, stop crawling
if self.limit is not None and len(visited_urls) >= self.limit:
break
return documents
def _fetch_page(self, url):
try:
response = self.session.get(url, timeout=10)
response.raise_for_status()
return response.text
except requests.exceptions.RequestException as e:
print(f"Error fetching URL {url}: {e}")
return None
def _process_html_to_markdown(self, html_content, current_url):
soup = BeautifulSoup(html_content, 'html.parser')
title_tag = soup.find('title')
title = title_tag.text.strip() if title_tag else "No Title"
# Extract language
language_tag = soup.find('html')
language = language_tag.get('lang', 'en') if language_tag else "en"
markdownified = markdownify(html_content, heading_style="ATX", newline_style="BACKSLASH")
# Reduce sequences of more than two newlines to exactly three
markdownified = re.sub(r'\n{3,}', '\n\n\n', markdownified)
return title, language, markdownified
def _extract_links(self, html_content, current_url):
soup = BeautifulSoup(html_content, 'html.parser')
links = []
for a in soup.find_all('a', href=True):
full_url = urljoin(current_url, a['href'])
links.append((full_url, a.text.strip()))
return links
def _get_base_domain(self, url):
extracted = tldextract.extract(url)
# Reconstruct the domain as domain.suffix
base_domain = f"{extracted.domain}.{extracted.suffix}"
return base_domain
def _filter_links(self, links, base_domain):
"""
Filter the extracted links to only include those that match the crawling criteria:
- If allow_subdomains is True, allow any link whose domain ends with the base_domain.
- If allow_subdomains is False, only allow exact matches of the base_domain.
"""
filtered = []
for link, _ in links:
parsed_link = urlparse(link)
if not parsed_link.netloc:
continue
extracted = tldextract.extract(parsed_link.netloc)
link_base = f"{extracted.domain}.{extracted.suffix}"
if self.allow_subdomains:
# For subdomains: sub.example.com ends with example.com
if link_base == base_domain or link_base.endswith("." + base_domain):
filtered.append(link)
else:
# Exact domain match
if link_base == base_domain:
filtered.append(link)
return filtered

View File

@@ -6,12 +6,12 @@ dataclasses-json==0.6.7
docx2txt==0.8 docx2txt==0.8
duckduckgo-search==6.3.0 duckduckgo-search==6.3.0
ebooklib==0.18 ebooklib==0.18
elastic-transport==8.15.1 elastic-transport==8.17.0
elasticsearch==8.17.0 elasticsearch==8.17.0
escodegen==1.0.11 escodegen==1.0.11
esprima==4.0.1 esprima==4.0.1
esutils==1.0.1 esutils==1.0.1
Flask==3.0.3 Flask==3.1.0
faiss-cpu==1.9.0.post1 faiss-cpu==1.9.0.post1
flask-restx==1.3.0 flask-restx==1.3.0
gTTS==2.5.4 gTTS==2.5.4
@@ -33,7 +33,7 @@ langchain-community==0.3.14
langchain-core==0.3.29 langchain-core==0.3.29
langchain-openai==0.3.0 langchain-openai==0.3.0
langchain-text-splitters==0.3.5 langchain-text-splitters==0.3.5
langsmith==0.2.6 langsmith==0.2.10
lazy-object-proxy==1.10.0 lazy-object-proxy==1.10.0
lxml==5.3.0 lxml==5.3.0
markupsafe==3.0.2 markupsafe==3.0.2
@@ -46,13 +46,13 @@ numpy==2.2.1
openai==1.59.5 openai==1.59.5
openapi-schema-validator==0.6.2 openapi-schema-validator==0.6.2
openapi-spec-validator==0.6.0 openapi-spec-validator==0.6.0
openapi3-parser==1.1.18 openapi3-parser==1.1.19
orjson==3.10.14 orjson==3.10.14
packaging==24.1 packaging==24.1
pandas==2.2.3 pandas==2.2.3
openpyxl==3.1.5 openpyxl==3.1.5
pathable==0.4.4 pathable==0.4.4
pillow==10.4.0 pillow==11.1.0
portalocker==2.10.1 portalocker==2.10.1
prance==23.6.21.0 prance==23.6.21.0
primp==0.9.3 primp==0.9.3
@@ -77,8 +77,8 @@ sentence-transformers==3.3.1
tiktoken==0.8.0 tiktoken==0.8.0
tokenizers==0.21.0 tokenizers==0.21.0
torch==2.5.1 torch==2.5.1
tqdm==4.66.5 tqdm==4.67.1
transformers==4.47.1 transformers==4.48.0
typing-extensions==4.12.2 typing-extensions==4.12.2
typing-inspect==0.9.0 typing-inspect==0.9.0
tzdata==2024.2 tzdata==2024.2
@@ -86,4 +86,6 @@ urllib3==2.3.0
vine==5.1.0 vine==5.1.0
wcwidth==0.2.13 wcwidth==0.2.13
werkzeug==3.1.3 werkzeug==3.1.3
yarl==1.18.3 yarl==1.18.3
markdownify==0.14.1
tldextract==5.1.3

View File

@@ -203,53 +203,61 @@ def remote_worker(
sync_frequency="never", sync_frequency="never",
operation_mode="upload", operation_mode="upload",
doc_id=None, doc_id=None,
): ):
full_path = os.path.join(directory, user, name_job) full_path = os.path.join(directory, user, name_job)
if not os.path.exists(full_path): if not os.path.exists(full_path):
os.makedirs(full_path) os.makedirs(full_path)
self.update_state(state="PROGRESS", meta={"current": 1}) self.update_state(state="PROGRESS", meta={"current": 1})
logging.info( try:
f"Remote job: {full_path}", logging.info("Initializing remote loader with type: %s", loader)
extra={"user": user, "job": name_job, "source_data": source_data}, remote_loader = RemoteCreator.create_loader(loader)
) raw_docs = remote_loader.load_data(source_data)
remote_loader = RemoteCreator.create_loader(loader) chunker = Chunker(
raw_docs = remote_loader.load_data(source_data) chunking_strategy="classic_chunk",
max_tokens=MAX_TOKENS,
min_tokens=MIN_TOKENS,
duplicate_headers=False
)
docs = chunker.chunk(documents=raw_docs)
docs = [Document.to_langchain_format(raw_doc) for raw_doc in raw_docs]
tokens = count_tokens_docs(docs)
logging.info("Total tokens calculated: %d", tokens)
chunker = Chunker( if operation_mode == "upload":
chunking_strategy="classic_chunk", id = ObjectId()
max_tokens=MAX_TOKENS, embed_and_store_documents(docs, full_path, id, self)
min_tokens=MIN_TOKENS, elif operation_mode == "sync":
duplicate_headers=False if not doc_id or not ObjectId.is_valid(doc_id):
) logging.error("Invalid doc_id provided for sync operation: %s", doc_id)
docs = chunker.chunk(documents=raw_docs) raise ValueError("doc_id must be provided for sync operation.")
id = ObjectId(doc_id)
embed_and_store_documents(docs, full_path, id, self)
tokens = count_tokens_docs(docs) self.update_state(state="PROGRESS", meta={"current": 100})
if operation_mode == "upload":
id = ObjectId()
embed_and_store_documents(docs, full_path, id, self)
elif operation_mode == "sync":
if not doc_id or not ObjectId.is_valid(doc_id):
raise ValueError("doc_id must be provided for sync operation.")
id = ObjectId(doc_id)
embed_and_store_documents(docs, full_path, id, self)
self.update_state(state="PROGRESS", meta={"current": 100})
file_data = { file_data = {
"name": name_job, "name": name_job,
"user": user, "user": user,
"tokens": tokens, "tokens": tokens,
"retriever": retriever, "retriever": retriever,
"id": str(id), "id": str(id),
"type": loader, "type": loader,
"remote_data": source_data, "remote_data": source_data,
"sync_frequency": sync_frequency, "sync_frequency": sync_frequency,
} }
upload_index(full_path, file_data) upload_index(full_path, file_data)
shutil.rmtree(full_path) except Exception as e:
logging.error("Error in remote_worker task: %s", str(e), exc_info=True)
raise
finally:
if os.path.exists(full_path):
shutil.rmtree(full_path)
logging.info("remote_worker task completed successfully")
return {"urls": source_data, "name_job": name_job, "user": user, "limited": False} return {"urls": source_data, "name_job": name_job, "user": user, "limited": False}
def sync( def sync(

View File

@@ -1,43 +1,85 @@
#!/bin/bash #!/bin/bash
## chmod +x publish.sh - to upgrade ownership
set -e set -e
cat package.json >> package_copy.json
cat package-lock.json >> package-lock_copy.json # Create backup of original files
cp package.json package_original.json
cp package-lock.json package-lock_original.json
# Store the latest version after publishing
LATEST_VERSION=""
publish_package() { publish_package() {
PACKAGE_NAME=$1 PACKAGE_NAME=$1
BUILD_COMMAND=$2 BUILD_COMMAND=$2
# Update package name in package.json IS_REACT=$3
jq --arg name "$PACKAGE_NAME" '.name=$name' package.json > temp.json && mv temp.json package.json
# Remove 'target' key if the package name is 'docsgpt-react' echo "Preparing to publish ${PACKAGE_NAME}..."
if [ "$PACKAGE_NAME" = "docsgpt-react" ]; then
jq 'del(.targets)' package.json > temp.json && mv temp.json package.json # Restore original package.json state before each publish
fi cp package_original.json package.json
cp package-lock_original.json package-lock.json
if [ -d "dist" ]; then # Update package name in package.json
echo "Deleting existing dist directory..." jq --arg name "$PACKAGE_NAME" '.name=$name' package.json > temp.json && mv temp.json package.json
rm -rf dist
fi
npm version patch # Handle targets based on package type
if [ "$IS_REACT" = "true" ]; then
echo "Removing targets for React library build..."
jq 'del(.targets)' package.json > temp.json && mv temp.json package.json
fi
npm run "$BUILD_COMMAND" # Clean dist directory
if [ -d "dist" ]; then
echo "Cleaning dist directory..."
rm -rf dist
fi
# Publish to npm # update version and store it
npm publish LATEST_VERSION=$(npm version patch)
# Clean up echo "New version: ${LATEST_VERSION}"
mv package_copy.json package.json
mv package-lock_copy.json package-lock.json # Build package
echo "Published ${PACKAGE_NAME}" npm run "$BUILD_COMMAND"
# Replace npm publish with npm pack for testing
npm publish
echo "Successfully packaged ${PACKAGE_NAME}"
# Log the bundle size
TARBALL="${PACKAGE_NAME}-${LATEST_VERSION#v}.tgz"
if [ -f "$TARBALL" ]; then
BUNDLE_SIZE=$(du -h "$TARBALL" | cut -f1)
echo "Bundle size for ${PACKAGE_NAME}: ${BUNDLE_SIZE}"
else
echo "Error: ${TARBALL} not found."
exit 1
fi
} }
# Publish docsgpt package # First publish docsgpt (HTML bundle)
publish_package "docsgpt" "build" publish_package "docsgpt" "build" "false"
# Publish docsgpt-react package # Then publish docsgpt-react (React library)
publish_package "docsgpt-react" "build:react" publish_package "docsgpt-react" "build:react" "true"
# Restore original state but keep the updated version
cp package_original.json package.json
cp package-lock_original.json package-lock.json
rm -rf package_copy.json # Update the version in the final package.json
rm -rf package-lock_copy.json jq --arg version "${LATEST_VERSION#v}" '.version=$version' package.json > temp.json && mv temp.json package.json
echo "---Process completed---"
# Run npm install to update package-lock.json with the new version
npm install --package-lock-only
# Cleanup backup files
rm -f package_original.json
rm -f package-lock_original.json
rm -f temp.json
echo "---Process completed---"
echo "Final version in package.json: $(jq -r '.version' package.json)"
echo "Final version in package-lock.json: $(jq -r '.version' package-lock.json)"
echo "Generated test packages:"
ls *.tgz

View File

@@ -242,14 +242,20 @@ white-space: pre-wrap;
const Toolkit = styled.kbd` const Toolkit = styled.kbd`
position: absolute; position: absolute;
right: 4px; right: 4px;
top: 4px; top: 50%;
transform: translateY(-50%);
background-color: ${(props) => props.theme.primary.bg}; background-color: ${(props) => props.theme.primary.bg};
color: ${(props) => props.theme.secondary.text}; color: ${(props) => props.theme.secondary.text};
font-weight: 600; font-weight: 600;
font-size: 10px; font-size: 10px;
padding: 3px; padding: 3px 6px;
border: 1px solid ${(props) => props.theme.secondary.text}; border: 1px solid ${(props) => props.theme.secondary.text};
border-radius: 4px; border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
pointer-events: none;
` `
const Loader = styled.div` const Loader = styled.div`
margin: 2rem auto; margin: 2rem auto;

View File

@@ -37,12 +37,14 @@ export default function Hero({
<Fragment key={key}> <Fragment key={key}>
<button <button
onClick={() => handleQuestion({ question: demo.query })} onClick={() => handleQuestion({ question: demo.query })}
className="w-full rounded-full border border-silver px-6 py-4 text-left hover:border-gray-4000 dark:hover:border-gray-3000 xl:min-w-[24vw]" className="w-full rounded-full border border-silver px-6 py-4 text-left hover:border-gray-4000 dark:hover:border-gray-3000 xl:min-w-[24vw] bg-white dark:bg-raisin-black focus:outline-none focus:ring-2 focus:ring-purple-taupe"
> >
<p className="mb-1 font-semibold text-black dark:text-silver"> <p className="mb-1 font-semibold text-black-1000 dark:text-bright-gray">
{demo.header} {demo.header}
</p> </p>
<span className="text-gray-400">{demo.query}</span> <span className="text-gray-700 dark:text-gray-300">
{demo.query}
</span>
</button> </button>
</Fragment> </Fragment>
), ),

View File

@@ -21,11 +21,10 @@ import {
handleAbort, handleAbort,
} from './conversation/conversationSlice'; } from './conversation/conversationSlice';
import ConversationTile from './conversation/ConversationTile'; import ConversationTile from './conversation/ConversationTile';
import { useDarkTheme, useMediaQuery, useOutsideAlerter } from './hooks'; import { useDarkTheme, useMediaQuery } from './hooks';
import useDefaultDocument from './hooks/useDefaultDocument'; import useDefaultDocument from './hooks/useDefaultDocument';
import DeleteConvModal from './modals/DeleteConvModal'; import DeleteConvModal from './modals/DeleteConvModal';
import { ActiveState, Doc } from './models/misc'; import { ActiveState, Doc } from './models/misc';
import APIKeyModal from './preferences/APIKeyModal';
import { getConversations, getDocs } from './preferences/preferenceApi'; import { getConversations, getDocs } from './preferences/preferenceApi';
import { import {
selectApiKeyStatus, selectApiKeyStatus,
@@ -68,8 +67,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
const [isDocsListOpen, setIsDocsListOpen] = useState(false); const [isDocsListOpen, setIsDocsListOpen] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const isApiKeySet = useSelector(selectApiKeyStatus); const isApiKeySet = useSelector(selectApiKeyStatus);
const [apiKeyModalState, setApiKeyModalState] =
useState<ActiveState>('INACTIVE');
const [uploadModalState, setUploadModalState] = const [uploadModalState, setUploadModalState] =
useState<ActiveState>('INACTIVE'); useState<ActiveState>('INACTIVE');
@@ -192,12 +189,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
console.error(err); console.error(err);
}); });
} }
useOutsideAlerter(navRef, () => {
if (isMobile && navOpen && apiKeyModalState === 'INACTIVE') {
setNavOpen(false);
setIsDocsListOpen(false);
}
}, [navOpen, isDocsListOpen, apiKeyModalState]);
/* /*
Needed to fix bug where if mobile nav was closed and then window was resized to desktop, nav would still be closed but the button to open would be gone, as per #1 on issue #146 Needed to fix bug where if mobile nav was closed and then window was resized to desktop, nav would still be closed but the button to open would be gone, as per #1 on issue #146
@@ -220,7 +211,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
> >
<img <img
src={Expand} src={Expand}
alt="menu toggle" alt="Toggle navigation menu"
className={`${ className={`${
!navOpen ? 'rotate-180' : 'rotate-0' !navOpen ? 'rotate-180' : 'rotate-0'
} m-auto transition-all duration-200`} } m-auto transition-all duration-200`}
@@ -234,7 +225,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
> >
<img <img
src={openNewChat} src={openNewChat}
alt="open new chat icon" alt="Start new chat"
className="cursor-pointer" className="cursor-pointer"
/> />
</button> </button>
@@ -263,7 +254,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
}} }}
> >
<a href="/" className="flex gap-1.5"> <a href="/" className="flex gap-1.5">
<img className="mb-2 h-10" src={DocsGPT3} alt="" /> <img className="mb-2 h-10" src={DocsGPT3} alt="DocsGPT Logo" />
<p className="my-auto text-2xl font-semibold">DocsGPT</p> <p className="my-auto text-2xl font-semibold">DocsGPT</p>
</a> </a>
</div> </div>
@@ -275,7 +266,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
> >
<img <img
src={Expand} src={Expand}
alt="menu toggle" alt="Toggle navigation menu"
className={`${ className={`${
!navOpen ? 'rotate-180' : 'rotate-0' !navOpen ? 'rotate-180' : 'rotate-0'
} m-auto transition-all duration-200`} } m-auto transition-all duration-200`}
@@ -298,7 +289,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
> >
<img <img
src={Add} src={Add}
alt="new" alt="Create new chat"
className="opacity-80 group-hover:opacity-100" className="opacity-80 group-hover:opacity-100"
/> />
<p className=" text-sm text-dove-gray group-hover:text-neutral-600 dark:text-chinese-silver dark:group-hover:text-bright-gray"> <p className=" text-sm text-dove-gray group-hover:text-neutral-600 dark:text-chinese-silver dark:group-hover:text-bright-gray">
@@ -314,7 +305,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
<img <img
src={isDarkTheme ? SpinnerDark : Spinner} src={isDarkTheme ? SpinnerDark : Spinner}
className="animate-spin cursor-pointer bg-transparent" className="animate-spin cursor-pointer bg-transparent"
alt="Loading..." alt="Loading conversations"
/> />
</div> </div>
)} )}
@@ -365,6 +356,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
<img <img
className="mt-2 h-9 w-9 hover:cursor-pointer" className="mt-2 h-9 w-9 hover:cursor-pointer"
src={UploadIcon} src={UploadIcon}
alt="Upload document"
onClick={() => { onClick={() => {
setUploadModalState('ACTIVE'); setUploadModalState('ACTIVE');
if (isMobile) { if (isMobile) {
@@ -392,7 +384,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
> >
<img <img
src={SettingGear} src={SettingGear}
alt="icon" alt="Settings"
className="ml-2 w-5 filter dark:invert" className="ml-2 w-5 filter dark:invert"
/> />
<p className="my-auto text-sm text-eerie-black dark:text-white"> <p className="my-auto text-sm text-eerie-black dark:text-white">
@@ -414,7 +406,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
> >
<img <img
src={Discord} src={Discord}
alt="discord" alt="Join Discord community"
className="m-2 w-6 self-center filter dark:invert" className="m-2 w-6 self-center filter dark:invert"
/> />
</NavLink> </NavLink>
@@ -427,7 +419,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
> >
<img <img
src={Twitter} src={Twitter}
alt="x" alt="Follow us on Twitter"
className="m-2 w-5 self-center filter dark:invert" className="m-2 w-5 self-center filter dark:invert"
/> />
</NavLink> </NavLink>
@@ -440,7 +432,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
> >
<img <img
src={Github} src={Github}
alt="github" alt="View on GitHub"
className="m-2 w-6 self-center filter dark:invert" className="m-2 w-6 self-center filter dark:invert"
/> />
</NavLink> </NavLink>
@@ -457,18 +449,13 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
> >
<img <img
src={Hamburger} src={Hamburger}
alt="menu toggle" alt="Toggle mobile menu"
className="w-7 filter dark:invert" className="w-7 filter dark:invert"
/> />
</button> </button>
<div className="text-[#949494] font-medium text-[20px]">DocsGPT</div> <div className="text-[#949494] font-medium text-[20px]">DocsGPT</div>
</div> </div>
</div> </div>
<APIKeyModal
modalState={apiKeyModalState}
setModalState={setApiKeyModalState}
isCancellable={isApiKeySet}
/>
<DeleteConvModal <DeleteConvModal
modalState={modalStateDeleteConv} modalState={modalStateDeleteConv}
setModalState={setModalStateDeleteConv} setModalState={setModalStateDeleteConv}

View File

@@ -59,7 +59,8 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
<div className="md:hidden z-10"> <div className="md:hidden z-10">
<button <button
onClick={() => scrollTabs(-1)} onClick={() => scrollTabs(-1)}
className="flex h-6 w-6 items-center rounded-full justify-center transition-all hover:bg-gray-100" className="flex h-6 w-6 items-center rounded-full justify-center transition-all hover:bg-gray-200 dark:hover:bg-gray-700"
aria-label="Scroll tabs left"
> >
<img src={ArrowLeft} alt="left-arrow" className="h-3" /> <img src={ArrowLeft} alt="left-arrow" className="h-3" />
</button> </button>
@@ -67,16 +68,22 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
<div <div
ref={containerRef} ref={containerRef}
className="flex flex-nowrap overflow-x-auto no-scrollbar md:space-x-4 scroll-smooth snap-x" className="flex flex-nowrap overflow-x-auto no-scrollbar md:space-x-4 scroll-smooth snap-x"
role="tablist"
aria-label="Settings tabs"
> >
{tabs.map((tab, index) => ( {tabs.map((tab, index) => (
<button <button
key={index} key={index}
onClick={() => setActiveTab(tab)} onClick={() => setActiveTab(tab)}
className={`snap-start h-9 rounded-3xl px-4 font-bold hover:text-neutral-600 dark:hover:text-white/60 ${ className={`snap-start h-9 rounded-3xl px-4 font-bold transition-colors ${
activeTab === tab activeTab === tab
? 'bg-neutral-100 text-neutral-600 dark:bg-dark-charcoal dark:text-white/60' ? 'bg-neutral-200 text-neutral-900 dark:bg-dark-charcoal dark:text-white'
: 'text-gray-6000' : 'text-neutral-700 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-white'
}`} }`}
role="tab"
aria-selected={activeTab === tab}
aria-controls={`${tab.toLowerCase()}-panel`}
id={`${tab.toLowerCase()}-tab`}
> >
{tab} {tab}
</button> </button>
@@ -85,7 +92,8 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
<div className="md:hidden z-10"> <div className="md:hidden z-10">
<button <button
onClick={() => scrollTabs(1)} onClick={() => scrollTabs(1)}
className="flex h-6 w-6 rounded-full items-center justify-center hover:bg-gray-100" className="flex h-6 w-6 rounded-full items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700"
aria-label="Scroll tabs right"
> >
<img src={ArrowRight} alt="right-arrow" className="h-3" /> <img src={ArrowRight} alt="right-arrow" className="h-3" />
</button> </button>

View File

@@ -386,13 +386,19 @@ export default function Conversation() {
{...getRootProps()} {...getRootProps()}
className="flex w-full items-center rounded-[40px] border border-silver bg-white dark:bg-raisin-black" className="flex w-full items-center rounded-[40px] border border-silver bg-white dark:bg-raisin-black"
> >
<input {...getInputProps()}></input> <label htmlFor="file-upload" className="sr-only">
{t('modals.uploadDoc.label')}
</label>
<input {...getInputProps()} id="file-upload" />
<label htmlFor="message-input" className="sr-only">
{t('inputPlaceholder')}
</label>
<textarea <textarea
id="inputbox" id="message-input"
ref={inputRef} ref={inputRef}
tabIndex={1} tabIndex={1}
placeholder={t('inputPlaceholder')} placeholder={t('inputPlaceholder')}
className={`inputbox-style w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-full bg-transparent py-5 text-base leading-tight opacity-100 focus:outline-none dark:bg-transparent dark:text-bright-gray`} className={`inputbox-style w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-full bg-transparent py-5 text-base leading-tight opacity-100 focus:outline-none dark:bg-transparent dark:text-bright-gray dark:placeholder-bright-gray dark:placeholder-opacity-50`}
onInput={handleInput} onInput={handleInput}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
@@ -400,19 +406,27 @@ export default function Conversation() {
handleQuestionSubmission(); handleQuestionSubmission();
} }
}} }}
aria-label={t('inputPlaceholder')}
></textarea> ></textarea>
{status === 'loading' ? ( {status === 'loading' ? (
<img <img
src={isDarkTheme ? SpinnerDark : Spinner} src={isDarkTheme ? SpinnerDark : Spinner}
className="relative right-[38px] bottom-[24px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent" className="relative right-[38px] bottom-[24px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent"
></img> alt={t('loading')}
/>
) : ( ) : (
<div className="mx-1 cursor-pointer rounded-full p-3 text-center hover:bg-gray-3000 dark:hover:bg-dark-charcoal"> <div className="mx-1 cursor-pointer rounded-full p-3 text-center hover:bg-gray-3000 dark:hover:bg-dark-charcoal">
<img <button
className="ml-[4px] h-6 w-6 text-white "
onClick={() => handleQuestionSubmission()} onClick={() => handleQuestionSubmission()}
src={isDarkTheme ? SendDark : Send} aria-label={t('send')}
></img> className="flex items-center justify-center"
>
<img
className="ml-[4px] h-6 w-6 text-white"
src={isDarkTheme ? SendDark : Send}
alt={t('send')}
/>
</button>
</div> </div>
)} )}
</div> </div>

View File

@@ -8,6 +8,7 @@ import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import rehypeKatex from 'rehype-katex'; import rehypeKatex from 'rehype-katex';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math'; import remarkMath from 'remark-math';
import { useTranslation } from 'react-i18next';
import DocsGPT3 from '../assets/cute_docsgpt3.svg'; import DocsGPT3 from '../assets/cute_docsgpt3.svg';
import Dislike from '../assets/dislike.svg?react'; import Dislike from '../assets/dislike.svg?react';
@@ -62,6 +63,7 @@ const ConversationBubble = forwardRef<
}, },
ref, ref,
) { ) {
const { t } = useTranslation();
// const bubbleRef = useRef<HTMLDivElement | null>(null); // const bubbleRef = useRef<HTMLDivElement | null>(null);
const chunks = useSelector(selectChunks); const chunks = useSelector(selectChunks);
const selectedDocs = useSelector(selectSelectedDocs); const selectedDocs = useSelector(selectSelectedDocs);
@@ -113,13 +115,13 @@ const ConversationBubble = forwardRef<
{isEditClicked && ( {isEditClicked && (
<div ref={editableQueryRef} className="w-[75%] flex flex-col"> <div ref={editableQueryRef} className="w-[75%] flex flex-col">
<textarea <textarea
placeholder="Type the updated query..." placeholder={t('conversation.edit.placeholder')}
onChange={(e) => { onChange={(e) => {
setEditInputBox(e.target.value); setEditInputBox(e.target.value);
}} }}
rows={1} rows={1}
value={editInputBox} value={editInputBox}
className="ml-2 mr-12 text-[15px] resize-y h-12 min-h-max rounded-3xl p-3 no-scrollbar leading-relaxed dark:border-[0.5px] dark:border-white dark:bg-raisin-black dark:text-white px-[18px] border-[1.5px] border-black" className="ml-2 mr-12 text-[15px] resize-y h-12 min-h-max rounded-3xl p-3 no-scrollbar leading-relaxed dark:border-[0.5px] dark:border-white dark:bg-raisin-black dark:text-white px-[18px] border-[1.5px] border-black"
/> />
<div <div
className={`flex flex-row-reverse justify-end gap-1 mt-3 text-sm font-medium`} className={`flex flex-row-reverse justify-end gap-1 mt-3 text-sm font-medium`}
@@ -128,13 +130,13 @@ const ConversationBubble = forwardRef<
className="rounded-full bg-[#CDB5FF] hover:bg-[#E1D3FF] py-[10px] px-[15px] text-purple-30 max-w-full whitespace-pre-wrap leading-none" className="rounded-full bg-[#CDB5FF] hover:bg-[#E1D3FF] py-[10px] px-[15px] text-purple-30 max-w-full whitespace-pre-wrap leading-none"
onClick={() => handleEditClick()} onClick={() => handleEditClick()}
> >
Update {t('conversation.edit.update')}
</button> </button>
<button <button
className="py-[10px] px-[15px] no-underline hover:underline text-purple-30 max-w-full whitespace-pre-wrap leading-normal" className="py-[10px] px-[15px] no-underline hover:underline text-purple-30 max-w-full whitespace-pre-wrap leading-normal"
onClick={() => setIsEditClicked(false)} onClick={() => setIsEditClicked(false)}
> >
Cancel {t('conversation.edit.cancel')}
</button> </button>
</div> </div>
</div> </div>
@@ -185,12 +187,14 @@ const ConversationBubble = forwardRef<
avatar={ avatar={
<img <img
src={Sources} src={Sources}
alt="Sources" alt={t('conversation.sources.title')}
className="h-full w-full object-fill" className="h-full w-full object-fill"
/> />
} }
/> />
<p className="text-base font-semibold">Sources</p> <p className="text-base font-semibold">
{t('conversation.sources.title')}
</p>
</div> </div>
<div className="grid grid-cols-2 gap-2 lg:grid-cols-4"> <div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, index) => ( {Array.from({ length: 4 }).map((_, index) => (
@@ -217,12 +221,14 @@ const ConversationBubble = forwardRef<
avatar={ avatar={
<img <img
src={Sources} src={Sources}
alt="Sources" alt={t('conversation.sources.title')}
className="h-full w-full object-fill" className="h-full w-full object-fill"
/> />
} }
/> />
<p className="text-base font-semibold">Sources</p> <p className="text-base font-semibold">
{t('conversation.sources.title')}
</p>
</div> </div>
<div className="fade-in ml-3 mr-5 max-w-[90vw] md:max-w-[70vw] lg:max-w-[50vw]"> <div className="fade-in ml-3 mr-5 max-w-[90vw] md:max-w-[70vw] lg:max-w-[50vw]">
<div className="grid grid-cols-2 gap-2 lg:grid-cols-4"> <div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
@@ -289,9 +295,11 @@ const ConversationBubble = forwardRef<
className="flex h-28 cursor-pointer flex-col-reverse rounded-[20px] bg-gray-1000 p-4 text-purple-30 hover:bg-[#F1F1F1] hover:text-[#6D3ECC] dark:bg-gun-metal dark:hover:bg-[#2C2E3C] dark:hover:text-[#8C67D7]" className="flex h-28 cursor-pointer flex-col-reverse rounded-[20px] bg-gray-1000 p-4 text-purple-30 hover:bg-[#F1F1F1] hover:text-[#6D3ECC] dark:bg-gun-metal dark:hover:bg-[#2C2E3C] dark:hover:text-[#8C67D7]"
onClick={() => setIsSidebarOpen(true)} onClick={() => setIsSidebarOpen(true)}
> >
<p className="ellipsis-text h-22 text-xs">{`View ${ <p className="ellipsis-text h-22 text-xs">
sources?.length ? sources.length - 3 : 0 {t('conversation.sources.view_more', {
} more`}</p> count: sources?.length ? sources.length - 3 : 0,
})}
</p>
</div> </div>
)} )}
</div> </div>
@@ -306,12 +314,14 @@ const ConversationBubble = forwardRef<
avatar={ avatar={
<img <img
src={DocsGPT3} src={DocsGPT3}
alt="DocsGPT" alt={t('conversation.answer')}
className="h-full w-full object-cover" className="h-full w-full object-cover"
/> />
} }
/> />
<p className="text-base font-semibold">Answer</p> <p className="text-base font-semibold">
{t('conversation.answer')}
</p>
</div> </div>
<div <div
className={`fade-in-bubble ml-2 mr-5 flex max-w-[90vw] rounded-[28px] bg-gray-1000 py-[14px] px-7 dark:bg-gun-metal md:max-w-[70vw] lg:max-w-[50vw] ${ className={`fade-in-bubble ml-2 mr-5 flex max-w-[90vw] rounded-[28px] bg-gray-1000 py-[14px] px-7 dark:bg-gun-metal md:max-w-[70vw] lg:max-w-[50vw] ${
@@ -419,7 +429,7 @@ const ConversationBubble = forwardRef<
${type !== 'ERROR' ? 'group-hover:lg:visible' : 'hidden'}`} ${type !== 'ERROR' ? 'group-hover:lg:visible' : 'hidden'}`}
> >
<div> <div>
<SpeakButton text={message} /> {/* Add SpeakButton here */} <SpeakButton text={message} />
</div> </div>
</div> </div>
{type === 'ERROR' && ( {type === 'ERROR' && (
@@ -557,7 +567,7 @@ function AllSources(sources: AllSourcesProps) {
{source.source && source.source !== 'local' ? ( {source.source && source.source !== 'local' ? (
<img <img
src={Link} src={Link}
alt="Link" alt={'Link'}
className="h-3 w-3 cursor-pointer object-fill" className="h-3 w-3 cursor-pointer object-fill"
onClick={() => onClick={() =>
window.open(source.source, '_blank', 'noopener, noreferrer') window.open(source.source, '_blank', 'noopener, noreferrer')

View File

@@ -115,6 +115,19 @@ export default function ConversationTile({
setConversationsName(conversation.name); setConversationsName(conversation.name);
setIsEdit(false); setIsEdit(false);
} }
const handleRenameKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
e.stopPropagation();
if (e.key === 'Enter') {
handleSaveConversation({
id: conversation.id,
name: conversationName,
});
} else if (e.key === 'Escape') {
onClear();
}
};
return ( return (
<> <>
<div <div
@@ -144,6 +157,7 @@ export default function ConversationTile({
className="h-6 w-full bg-transparent px-1 text-sm font-normal leading-6 focus:outline-[#0075FF]" className="h-6 w-full bg-transparent px-1 text-sm font-normal leading-6 focus:outline-[#0075FF]"
value={conversationName} value={conversationName}
onChange={(e) => setConversationsName(e.target.value)} onChange={(e) => setConversationsName(e.target.value)}
onKeyDown={handleRenameKeyDown}
/> />
) : ( ) : (
<p className="my-auto overflow-hidden overflow-ellipsis whitespace-nowrap text-sm font-normal leading-6 text-eerie-black dark:text-white"> <p className="my-auto overflow-hidden overflow-ellipsis whitespace-nowrap text-sm font-normal leading-6 text-eerie-black dark:text-white">
@@ -239,7 +253,7 @@ export default function ConversationTile({
> >
<img <img
src={Trash} src={Trash}
alt="Edit" alt="Delete"
width={24} width={24}
height={24} height={24}
className="cursor-pointer hover:opacity-50" className="cursor-pointer hover:opacity-50"

View File

@@ -236,7 +236,7 @@ export const SharedConversation = () => {
</div> </div>
</div> </div>
<div className=" flex w-11/12 flex-col items-center gap-4 pb-2 md:w-10/12 lg:w-6/12"> <div className="flex w-11/12 flex-col items-center gap-4 pb-2 md:w-10/12 lg:w-6/12">
{apiKey ? ( {apiKey ? (
<div className="flex h-full w-full items-center rounded-[40px] border border-silver bg-white py-1 dark:bg-raisin-black"> <div className="flex h-full w-full items-center rounded-[40px] border border-silver bg-white py-1 dark:bg-raisin-black">
<div <div
@@ -272,7 +272,7 @@ export const SharedConversation = () => {
) : ( ) : (
<button <button
onClick={() => navigate('/')} onClick={() => navigate('/')}
className="w-fit rounded-full bg-purple-30 p-4 text-white shadow-xl transition-colors duration-200 hover:bg-purple-taupe" className="w-fit rounded-full bg-purple-30 p-4 text-white shadow-xl transition-colors duration-200 hover:bg-purple-taupe mb-14 sm:mb-0"
> >
{t('sharedConv.button')} {t('sharedConv.button')}
</button> </button>

View File

@@ -41,16 +41,16 @@
"selectLanguage": "Select Language", "selectLanguage": "Select Language",
"chunks": "Chunks processed per query", "chunks": "Chunks processed per query",
"prompt": "Active Prompt", "prompt": "Active Prompt",
"deleteAllLabel": "Delete all Conversation", "deleteAllLabel": "Delete All Conversations",
"deleteAllBtn": "Delete all", "deleteAllBtn": "Delete All",
"addNew": "Add New", "addNew": "Add New",
"convHistory": "Conversational history", "convHistory": "Conversation History",
"none": "None", "none": "None",
"low": "Low", "low": "Low",
"medium": "Medium", "medium": "Medium",
"high": "High", "high": "High",
"unlimited": "Unlimited", "unlimited": "Unlimited",
"default": "default" "default": "Default"
}, },
"documents": { "documents": {
"label": "Documents", "label": "Documents",
@@ -58,7 +58,19 @@
"date": "Vector Date", "date": "Vector Date",
"type": "Type", "type": "Type",
"tokenUsage": "Token Usage", "tokenUsage": "Token Usage",
"noData": "No existing Documents" "noData": "No existing Documents",
"searchPlaceholder": "Search...",
"addNew": "Add New",
"preLoaded": "Pre-loaded",
"private": "Private",
"sync": "Sync",
"syncFrequency": {
"never": "Never",
"daily": "Daily",
"weekly": "Weekly",
"monthly": "Monthly"
},
"actions": "Actions"
}, },
"apiKeys": { "apiKeys": {
"label": "Chatbots", "label": "Chatbots",
@@ -126,11 +138,19 @@
"secret": "Client Secret", "secret": "Client Secret",
"agent": "User agent", "agent": "User agent",
"searchQueries": "Search queries", "searchQueries": "Search queries",
"numberOfPosts": "Number of posts" "numberOfPosts": "Number of posts",
"addQuery": "Add Query"
}, },
"drag": { "drag": {
"title": "Upload a source file", "title": "Upload a source file",
"description": "Drop your file here to add it as a source" "description": "Drop your file here to add it as a source"
},
"progress": {
"upload": "Upload is in progress",
"training": "Training is in progress",
"completed": "Training completed",
"wait": "This may take several minutes",
"tokenLimit": "Over the token limit, please consider uploading smaller document"
} }
}, },
"createAPIKey": { "createAPIKey": {
@@ -195,5 +215,12 @@
"previousPage": "Previous page", "previousPage": "Previous page",
"nextPage": "Next page", "nextPage": "Next page",
"lastPage": "Last page" "lastPage": "Last page"
},
"conversation": {
"edit": {
"update": "Update",
"cancel": "Cancel",
"placeholder": "Enter updated query..."
}
} }
} }

View File

@@ -1,5 +1,5 @@
{ {
"language": "Spanish", "language": "Español",
"chat": "Chat", "chat": "Chat",
"chats": "Chats", "chats": "Chats",
"newChat": "Nuevo Chat", "newChat": "Nuevo Chat",
@@ -8,7 +8,7 @@
"inputPlaceholder": "Escribe tu mensaje aquí...", "inputPlaceholder": "Escribe tu mensaje aquí...",
"tagline": "DocsGPT utiliza GenAI, por favor revisa información crítica utilizando fuentes.", "tagline": "DocsGPT utiliza GenAI, por favor revisa información crítica utilizando fuentes.",
"sourceDocs": "Fuente", "sourceDocs": "Fuente",
"none": "Nada", "none": "Ninguno",
"cancel": "Cancelar", "cancel": "Cancelar",
"help": "Asistencia", "help": "Asistencia",
"emailUs": "Envíanos un correo", "emailUs": "Envíanos un correo",
@@ -36,29 +36,41 @@
"general": { "general": {
"label": "General", "label": "General",
"selectTheme": "Seleccionar Tema", "selectTheme": "Seleccionar Tema",
"light": "de luz", "light": "Claro",
"dark": "oscura", "dark": "Oscuro",
"selectLanguage": "Seleccionar Idioma", "selectLanguage": "Seleccionar Idioma",
"chunks": "Trozos procesados por consulta", "chunks": "Fragmentos procesados por consulta",
"prompt": "Prompt Activo", "prompt": "Prompt Activo",
"deleteAllLabel": "Eliminar toda la Conversación", "deleteAllLabel": "Eliminar todas las conversaciones",
"deleteAllBtn": "Eliminar todo", "deleteAllBtn": "Eliminar todo",
"addNew": "Agregar Nuevo", "addNew": "Agregar Nuevo",
"convHistory": "Historia conversacional", "convHistory": "Historial de conversaciones",
"none": "ninguno", "none": "Ninguno",
"low": "Bajo", "low": "Bajo",
"medium": "Medio", "medium": "Medio",
"high": "Alto", "high": "Alto",
"unlimited": "Ilimitado", "unlimited": "Ilimitado",
"default": "predeterminada" "default": "Predeterminado"
}, },
"documents": { "documents": {
"label": "Documentos", "label": "Documentos",
"name": "Nombre del Documento", "name": "Nombre del Documento",
"date": "Fecha Vector", "date": "Fecha de Vector",
"type": "Tipo", "type": "Tipo",
"tokenUsage": "Uso de Tokens", "tokenUsage": "Uso de Tokens",
"noData": "No hay documentos existentes" "noData": "No hay documentos existentes",
"searchPlaceholder": "Buscar...",
"addNew": "Agregar Nuevo",
"preLoaded": "Precargado",
"private": "Privado",
"sync": "Sincronizar",
"syncFrequency": {
"never": "Nunca",
"daily": "Diario",
"weekly": "Semanal",
"monthly": "Mensual"
},
"actions": "Acciones"
}, },
"apiKeys": { "apiKeys": {
"label": "Chatbots", "label": "Chatbots",
@@ -95,21 +107,26 @@
"tableHeader": "Conversaciones generadas por API / chatbot" "tableHeader": "Conversaciones generadas por API / chatbot"
}, },
"tools": { "tools": {
"label": "Herramientas" "label": "Herramientas",
"searchPlaceholder": "Buscar...",
"addTool": "Agregar Herramienta",
"noToolsAlt": "No se encontraron herramientas",
"noToolsFound": "No se encontraron herramientas",
"selectToolSetup": "Seleccione una herramienta para configurar"
} }
}, },
"modals": { "modals": {
"uploadDoc": { "uploadDoc": {
"label": "Subir nuevo documento", "label": "Subir nuevo documento",
"select": "Elija cómo cargar su documento en DocsGPT", "select": "Elige cómo cargar tu documento en DocsGPT",
"file": "Subir desde el dispositivo", "file": "Subir desde el dispositivo",
"back": "Atrás", "back": "Atrás",
"wait": "Espere por favor ...", "wait": "Por favor espera ...",
"remote": "Recoger desde un sitio web", "remote": "Recoger desde un sitio web",
"start": "Empezar a chatear", "start": "Comenzar a chatear",
"name": "Nombre", "name": "Nombre",
"choose": "Seleccionar Archivos", "choose": "Seleccionar Archivos",
"info": "Por favor, suba archivos .pdf, .txt, .rst, .csv, .xlsx, .docx, .md, .html, .epub, .json, .pptx, .zip limitados a 25 MB", "info": "Por favor, sube archivos .pdf, .txt, .rst, .csv, .xlsx, .docx, .md, .html, .epub, .json, .pptx, .zip limitados a 25MB",
"uploadedFiles": "Archivos Subidos", "uploadedFiles": "Archivos Subidos",
"cancel": "Cancelar", "cancel": "Cancelar",
"train": "Entrenar", "train": "Entrenar",
@@ -117,39 +134,47 @@
"urlLink": "Enlace URL", "urlLink": "Enlace URL",
"repoUrl": "URL del Repositorio", "repoUrl": "URL del Repositorio",
"reddit": { "reddit": {
"id": "ID de Cliente", "id": "ID del Cliente",
"secret": "Secreto de Cliente", "secret": "Secreto del Cliente",
"agent": "Agente de Usuario", "agent": "Agente de usuario",
"searchQueries": "Consultas de Búsqueda", "searchQueries": "Consultas de búsqueda",
"numberOfPosts": "Número de publicaciones" "numberOfPosts": "Número de publicaciones",
"addQuery": "Agregar Consulta"
}, },
"drag": { "drag": {
"title": "Cargar un archivo fuente", "title": "Subir archivo fuente",
"description": "Suelta tu archivo aquí para agregarlo como fuente." "description": "Arrastra tu archivo aquí para agregarlo como fuente"
},
"progress": {
"upload": "Subida en progreso",
"training": "Entrenamiento en progreso",
"completed": "Entrenamiento completado",
"wait": "Esto puede tardar varios minutos",
"tokenLimit": "Excede el límite de tokens, considere cargar un documento más pequeño"
} }
}, },
"createAPIKey": { "createAPIKey": {
"label": "Crear Nueva Clave de API", "label": "Crear Nueva Clave de API",
"apiKeyName": "Nombre de la Clave de API", "apiKeyName": "Nombre de la Clave de API",
"chunks": "Fragmentos procesados por consulta", "chunks": "Fragmentos procesados por consulta",
"prompt": "Seleccione el prompt activo", "prompt": "Selecciona el prompt activo",
"sourceDoc": "Documento Fuente", "sourceDoc": "Documento Fuente",
"create": "Crear" "create": "Crear"
}, },
"saveKey": { "saveKey": {
"note": "Por favor, guarde su Clave", "note": "Por favor, guarda tu Clave",
"disclaimer": "Esta es la única vez que se mostrará su clave.", "disclaimer": "Esta es la única vez que se mostrará tu clave.",
"copy": "Copiar", "copy": "Copiar",
"copied": "Copiado", "copied": "Copiado",
"confirm": "He guardado la Clave" "confirm": "He guardado la Clave"
}, },
"deleteConv": { "deleteConv": {
"confirm": "¿Está seguro de que desea eliminar todas las conversaciones?", "confirm": "¿Estás seguro de que deseas eliminar todas las conversaciones?",
"delete": "Eliminar" "delete": "Eliminar"
}, },
"shareConv": { "shareConv": {
"label": "Crear una página pública para compartir", "label": "Crear una página pública para compartir",
"note": "El documento original, la información personal y las conversaciones posteriores permanecerán privadas", "note": "El documento fuente, información personal y conversaciones posteriores permanecerán privadas",
"create": "Crear", "create": "Crear",
"option": "Permitir a los usuarios realizar más consultas" "option": "Permitir a los usuarios realizar más consultas"
}, },
@@ -165,7 +190,7 @@
"sharedConv": { "sharedConv": {
"subtitle": "Creado con", "subtitle": "Creado con",
"button": "Comienza con DocsGPT", "button": "Comienza con DocsGPT",
"meta": "DocsGPT utiliza GenAI, por favor revise la información crítica utilizando fuentes." "meta": "DocsGPT utiliza GenAI, por favor revisa la información crítica utilizando fuentes."
}, },
"convTile": { "convTile": {
"share": "Compartir", "share": "Compartir",
@@ -180,5 +205,23 @@
"previousPage": "Página anterior", "previousPage": "Página anterior",
"nextPage": "Página siguiente", "nextPage": "Página siguiente",
"lastPage": "Última página" "lastPage": "Última página"
},
"conversation": {
"copy": "Copiar",
"copied": "Copiado",
"speak": "Hablar",
"answer": "Respuesta",
"edit": {
"update": "Actualizar",
"cancel": "Cancelar",
"placeholder": "Ingrese la consulta actualizada..."
},
"sources": {
"title": "Fuentes",
"text": "Texto fuente",
"link": "Enlace fuente",
"view_more": "Ver {{count}} más fuentes"
},
"retry": "Reintentar"
} }
} }

View File

@@ -28,7 +28,7 @@
}, },
{ {
"header": "学習支援", "header": "学習支援",
"query": "コンテキストに対する潜在的な質問を書いてください" "query": "このコンテンツに対する可能な質問を書いてください"
} }
], ],
"settings": { "settings": {
@@ -58,7 +58,19 @@
"date": "ベクトル日付", "date": "ベクトル日付",
"type": "タイプ", "type": "タイプ",
"tokenUsage": "トークン使用量", "tokenUsage": "トークン使用量",
"noData": "既存のドキュメントがありません" "noData": "既存のドキュメントがありません",
"searchPlaceholder": "検索...",
"addNew": "新規追加",
"preLoaded": "プリロード済み",
"private": "プライベート",
"sync": "同期",
"syncFrequency": {
"never": "なし",
"daily": "毎日",
"weekly": "毎週",
"monthly": "毎月"
},
"actions": "アクション"
}, },
"apiKeys": { "apiKeys": {
"label": "APIキー", "label": "APIキー",
@@ -114,7 +126,7 @@
"start": "チャットを開始する", "start": "チャットを開始する",
"name": "名前", "name": "名前",
"choose": "ファイルを選択", "choose": "ファイルを選択",
"info": "25MBまでの.pdf, .txt, .rst, .csv, .xlsx, .docx, .md, .html, .epub, .json, .pptx, .zipファイルをアップロードしてください", "info": "25MBまでの.pdf.txt.rst.csv.xlsx.docx.md.html.epub.json.pptx.zipファイルをアップロードしてください",
"uploadedFiles": "アップロードされたファイル", "uploadedFiles": "アップロードされたファイル",
"cancel": "キャンセル", "cancel": "キャンセル",
"train": "トレーニング", "train": "トレーニング",
@@ -126,11 +138,19 @@
"secret": "クライアントシークレット", "secret": "クライアントシークレット",
"agent": "ユーザーエージェント", "agent": "ユーザーエージェント",
"searchQueries": "検索クエリ", "searchQueries": "検索クエリ",
"numberOfPosts": "投稿数" "numberOfPosts": "投稿数",
"addQuery": "クエリを追加"
}, },
"drag": { "drag": {
"title": "ソースファイルをアップロードする", "title": "ソースファイルをアップロード",
"description": "ファイルをここにドロップしてソースとして追加します" "description": "ファイルをここにドロップしてソースとして追加してください"
},
"progress": {
"upload": "アップロード中",
"training": "トレーニング中",
"completed": "トレーニング完了",
"wait": "数分かかる場合があります",
"tokenLimit": "トークン制限を超えています。より小さいドキュメントをアップロードしてください"
} }
}, },
"createAPIKey": { "createAPIKey": {
@@ -195,5 +215,23 @@
"previousPage": "前のページ", "previousPage": "前のページ",
"nextPage": "次のページ", "nextPage": "次のページ",
"lastPage": "最後のページ" "lastPage": "最後のページ"
},
"conversation": {
"copy": "コピー",
"copied": "コピー済み",
"speak": "読み上げ",
"answer": "回答",
"edit": {
"update": "更新",
"cancel": "キャンセル",
"placeholder": "更新されたクエリを入力..."
},
"sources": {
"title": "ソース",
"text": "ソーステキスト",
"link": "ソースリンク",
"view_more": "さらに{{count}}個のソースを表示"
},
"retry": "再試行"
} }
} }

View File

@@ -28,7 +28,7 @@
}, },
{ {
"header": "Помощь в обучении", "header": "Помощь в обучении",
"query": "Написать потенциальные вопросы для контекста" "query": "Написать возможные вопросы для этого контента"
} }
], ],
"settings": { "settings": {
@@ -58,7 +58,19 @@
"date": "Дата вектора", "date": "Дата вектора",
"type": "Тип", "type": "Тип",
"tokenUsage": "Использование токена", "tokenUsage": "Использование токена",
"noData": "Нет существующих документов" "noData": "Нет существующих документов",
"searchPlaceholder": "Поиск...",
"addNew": "добавить новый",
"preLoaded": "Предзагруженный",
"private": "Частный",
"sync": "Синхронизация",
"syncFrequency": {
"never": "Никогда",
"daily": "Ежедневно",
"weekly": "Еженедельно",
"monthly": "Ежемесячно"
},
"actions": "Действия"
}, },
"apiKeys": { "apiKeys": {
"label": "API ключи", "label": "API ключи",
@@ -117,9 +129,9 @@
"info": "Пожалуйста, загрузите файлы .pdf, .txt, .rst, .csv, .xlsx, .docx, .md, .html, .epub, .json, .pptx, .zip размером до 25 МБ", "info": "Пожалуйста, загрузите файлы .pdf, .txt, .rst, .csv, .xlsx, .docx, .md, .html, .epub, .json, .pptx, .zip размером до 25 МБ",
"uploadedFiles": "Загруженные файлы", "uploadedFiles": "Загруженные файлы",
"cancel": "Отмена", "cancel": "Отмена",
"train": "Обучение", "train": "Тренировка",
"link": "Ссылка", "link": "Ссылка",
"urlLink": "URL-ссылка", "urlLink": "URL ссылка",
"repoUrl": "URL репозитория", "repoUrl": "URL репозитория",
"reddit": { "reddit": {
"id": "ID клиента", "id": "ID клиента",
@@ -131,6 +143,13 @@
"drag": { "drag": {
"title": "Загрузить исходный файл", "title": "Загрузить исходный файл",
"description": "Перетащите файл сюда, чтобы добавить его как источник" "description": "Перетащите файл сюда, чтобы добавить его как источник"
},
"progress": {
"upload": "Идет загрузка",
"training": "Идет обучение",
"completed": "Обучение завершено",
"wait": "Это может занять несколько минут",
"tokenLimit": "Превышен лимит токенов, рассмотрите возможность загрузки документа меньшего размера"
} }
}, },
"createAPIKey": { "createAPIKey": {
@@ -195,5 +214,23 @@
"previousPage": "Предыдущая страница", "previousPage": "Предыдущая страница",
"nextPage": "Следующая страница", "nextPage": "Следующая страница",
"lastPage": "Последняя страница" "lastPage": "Последняя страница"
},
"conversation": {
"copy": "Копировать",
"copied": "Скопировано",
"speak": "Озвучить",
"answer": "Ответ",
"edit": {
"update": "Обновить",
"cancel": "Отмена",
"placeholder": "Введите обновленный запрос..."
},
"sources": {
"title": "Источники",
"text": "Текст источника",
"link": "Ссылка на источник",
"view_more": "Показать еще {{count}} источников"
},
"retry": "Повторить"
} }
} }

View File

@@ -58,7 +58,19 @@
"date": "向量日期", "date": "向量日期",
"type": "類型", "type": "類型",
"tokenUsage": "Token 使用量", "tokenUsage": "Token 使用量",
"noData": "沒有現有的文件" "noData": "沒有現有的文件",
"searchPlaceholder": "搜尋...",
"addNew": "新增文件",
"preLoaded": "預載入",
"private": "私人",
"sync": "同步",
"syncFrequency": {
"never": "從不",
"daily": "每天",
"weekly": "每週",
"monthly": "每月"
},
"actions": "操作"
}, },
"apiKeys": { "apiKeys": {
"label": "聊天機器人", "label": "聊天機器人",
@@ -114,23 +126,31 @@
"start": "開始對話", "start": "開始對話",
"name": "名稱", "name": "名稱",
"choose": "選擇檔案", "choose": "選擇檔案",
"info": "請上傳 .pdf, .txt, .rst, .csv, .xlsx, .docx, .md, .html, .epub, .json, .pptx, .zip 檔案,大小限制為 25MB", "info": "請上傳限制為25MB的.pdf.txt.rst.csv.xlsx.docx.md.html.epub.json.pptx.zip檔案",
"uploadedFiles": "已上傳檔案", "uploadedFiles": "已上傳檔案",
"cancel": "取消", "cancel": "取消",
"train": "訓練", "train": "訓練",
"link": "連結", "link": "連結",
"urlLink": "URL 連結", "urlLink": "URL 連結",
"repoUrl": "儲存庫 URL", "repoUrl": "儲存庫 URL",
"reddit": { "reddit": {
"id": "戶端 ID", "id": "戶端ID",
"secret": "戶端鑰", "secret": "戶端鑰",
"agent": "使用者代理(User-Agent)", "agent": "使用者代理",
"searchQueries": "搜尋查詢", "searchQueries": "搜尋查詢",
"numberOfPosts": "貼文數量" "numberOfPosts": "貼文數量",
"addQuery": "新增查詢"
}, },
"drag": { "drag": {
"title": "上傳原始檔", "title": "上傳來源檔案",
"description": "將您的文件拖放到此處以將其添加為來源" "description": "將檔案拖放到此處以新增為來源"
},
"progress": {
"upload": "正在上傳",
"training": "正在訓練",
"completed": "訓練完成",
"wait": "這可能需要幾分鐘",
"tokenLimit": "超出令牌限制,請考慮上傳較小的文檔"
} }
}, },
"createAPIKey": { "createAPIKey": {

View File

@@ -34,7 +34,7 @@
"settings": { "settings": {
"label": "设置", "label": "设置",
"general": { "general": {
"label": "般", "label": "般",
"selectTheme": "选择主题", "selectTheme": "选择主题",
"light": "浅色", "light": "浅色",
"dark": "暗色", "dark": "暗色",
@@ -58,7 +58,18 @@
"date": "向量日期", "date": "向量日期",
"type": "类型", "type": "类型",
"tokenUsage": "令牌使用", "tokenUsage": "令牌使用",
"noData": "没有现有的文档" "noData": "没有现有的文档",
"searchPlaceholder": "搜索...",
"addNew": "添加新文档",
"preLoaded": "预加载",
"private": "私有",
"sync": "同步",
"syncFrequency": {
"never": "从不",
"daily": "每天",
"weekly": "每周",
"monthly": "每月"
}
}, },
"apiKeys": { "apiKeys": {
"label": "聊天机器人", "label": "聊天机器人",
@@ -114,7 +125,7 @@
"start": "开始聊天", "start": "开始聊天",
"name": "名称", "name": "名称",
"choose": "选择文件", "choose": "选择文件",
"info": "请上传 .pdf, .txt, .rst, .csv, .xlsx, .docx, .md, .html, .epub, .json, .pptx, .zip 文件,限 25MB", "info": "请上传限制为25MB的.pdf.txt.rst.csv.xlsx.docx.md.html.epub.json.pptx.zip文件",
"uploadedFiles": "已上传文件", "uploadedFiles": "已上传文件",
"cancel": "取消", "cancel": "取消",
"train": "训练", "train": "训练",
@@ -122,15 +133,23 @@
"urlLink": "URL 链接", "urlLink": "URL 链接",
"repoUrl": "存储库 URL", "repoUrl": "存储库 URL",
"reddit": { "reddit": {
"id": "客户端 ID", "id": "客户端ID",
"secret": "客户端密钥", "secret": "客户端密钥",
"agent": "用户代理", "agent": "用户代理",
"searchQueries": "搜索查询", "searchQueries": "搜索查询",
"numberOfPosts": "帖子数量" "numberOfPosts": "帖子数量",
"addQuery": "添加查询"
}, },
"drag": { "drag": {
"title": "上传源文件", "title": "上传源文件",
"description": "将您的文件拖放到此处以将其添加为源" "description": "将文件拖放到此处以添加为源"
},
"progress": {
"upload": "正在上传",
"training": "正在训练",
"completed": "训练完成",
"wait": "这可能需要几分钟",
"tokenLimit": "超出令牌限制,请考虑上传较小的文档"
} }
}, },
"createAPIKey": { "createAPIKey": {
@@ -195,5 +214,23 @@
"previousPage": "上一页", "previousPage": "上一页",
"nextPage": "下一页", "nextPage": "下一页",
"lastPage": "最后一页" "lastPage": "最后一页"
},
"conversation": {
"copy": "复制",
"copied": "已复制",
"speak": "朗读",
"answer": "回答",
"edit": {
"update": "更新",
"cancel": "取消",
"placeholder": "输入更新的查询..."
},
"sources": {
"title": "来源",
"text": "来源文本",
"link": "来源链接",
"view_more": "更多{{count}}个来源"
},
"retry": "重试"
} }
} }

View File

@@ -0,0 +1,11 @@
{
"modals": {
"uploadDoc": {
"progress": {
"completed": "訓練完成",
"wait": "這可能需要幾分鐘",
"tokenLimit": "超出令牌限制,請考慮上傳較小的文檔"
}
}
}
}

View File

@@ -1,10 +1,11 @@
import React from 'react'; import React, { useRef } from 'react';
import userService from '../api/services/userService'; import userService from '../api/services/userService';
import Exit from '../assets/exit.svg'; import Exit from '../assets/exit.svg';
import { ActiveState } from '../models/misc'; import { ActiveState } from '../models/misc';
import { AvailableTool } from './types'; import { AvailableTool } from './types';
import ConfigToolModal from './ConfigToolModal'; import ConfigToolModal from './ConfigToolModal';
import { useOutsideAlerter } from '../hooks';
import { useTranslation } from 'react-i18next';
export default function AddToolModal({ export default function AddToolModal({
message, message,
@@ -25,6 +26,14 @@ export default function AddToolModal({
); );
const [configModalState, setConfigModalState] = const [configModalState, setConfigModalState] =
React.useState<ActiveState>('INACTIVE'); React.useState<ActiveState>('INACTIVE');
const modalRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
useOutsideAlerter(modalRef, () => {
if (modalState === 'ACTIVE') {
setModalState('INACTIVE');
}
}, [modalState]);
const getAvailableTools = () => { const getAvailableTools = () => {
userService userService
@@ -63,14 +72,18 @@ export default function AddToolModal({
React.useEffect(() => { React.useEffect(() => {
if (modalState === 'ACTIVE') getAvailableTools(); if (modalState === 'ACTIVE') getAvailableTools();
}, [modalState]); }, [modalState]);
return ( return (
<> <>
<div <div
className={`${ className={`${
modalState === 'ACTIVE' ? 'visible' : 'hidden' modalState === 'ACTIVE' ? 'visible' : 'hidden'
} fixed top-0 left-0 z-30 h-screen w-screen bg-gray-alpha flex items-center justify-center`} } fixed top-0 left-0 z-30 h-screen w-screen bg-gray-alpha flex items-center justify-center`}
> >
<article className="flex h-[85vh] w-[90vw] md:w-[75vw] flex-col gap-4 rounded-2xl bg-[#FBFBFB] shadow-lg dark:bg-[#26272E]"> <article
ref={modalRef}
className="flex h-[85vh] w-[90vw] md:w-[75vw] flex-col gap-4 rounded-2xl bg-[#FBFBFB] shadow-lg dark:bg-[#26272E]"
>
<div className="relative"> <div className="relative">
<button <button
className="absolute top-3 right-4 m-2 w-3" className="absolute top-3 right-4 m-2 w-3"
@@ -78,11 +91,15 @@ export default function AddToolModal({
setModalState('INACTIVE'); setModalState('INACTIVE');
}} }}
> >
<img className="filter dark:invert" src={Exit} /> <img
className="filter dark:invert"
src={Exit}
alt={t('cancel')}
/>
</button> </button>
<div className="p-6"> <div className="p-6">
<h2 className="font-semibold text-xl text-jet dark:text-bright-gray px-3"> <h2 className="font-semibold text-xl text-jet dark:text-bright-gray px-3">
Select a tool to set up {t('settings.tools.selectToolSetup')}
</h2> </h2>
<div className="mt-5 flex flex-col sm:grid sm:grid-cols-3 gap-4 h-[73vh] overflow-auto px-3 py-px"> <div className="mt-5 flex flex-col sm:grid sm:grid-cols-3 gap-4 h-[73vh] overflow-auto px-3 py-px">
{availableTools.map((tool, index) => ( {availableTools.map((tool, index) => (

View File

@@ -14,4 +14,5 @@ export type WrapperModalProps = {
children?: React.ReactNode; children?: React.ReactNode;
isPerformingTask?: boolean; isPerformingTask?: boolean;
close: () => void; close: () => void;
className?: string;
}; };

View File

@@ -32,8 +32,9 @@ function AddPrompt({
setNewPromptName(''); setNewPromptName('');
setNewPromptContent(''); setNewPromptContent('');
}} }}
aria-label="Close add prompt modal"
> >
<img className="filter dark:invert" src={Exit} /> <img className="filter dark:invert" src={Exit} alt="Close modal" />
</button> </button>
<div className="p-8"> <div className="p-8">
<p className="mb-1 text-xl text-jet dark:text-bright-gray"> <p className="mb-1 text-xl text-jet dark:text-bright-gray">
@@ -43,6 +44,9 @@ function AddPrompt({
{t('modals.prompts.addDescription')} {t('modals.prompts.addDescription')}
</p> </p>
<div> <div>
<label htmlFor="new-prompt-name" className="sr-only">
Prompt Name
</label>
<Input <Input
placeholder={t('modals.prompts.promptName')} placeholder={t('modals.prompts.promptName')}
type="text" type="text"
@@ -60,10 +64,15 @@ function AddPrompt({
{t('modals.prompts.promptText')} {t('modals.prompts.promptText')}
</span> </span>
</div> </div>
<label htmlFor="new-prompt-content" className="sr-only">
Prompt Text
</label>
<textarea <textarea
id="new-prompt-content"
className="h-56 w-full rounded-lg border-2 border-silver px-3 py-2 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white" className="h-56 w-full rounded-lg border-2 border-silver px-3 py-2 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
value={newPromptContent} value={newPromptContent}
onChange={(e) => setNewPromptContent(e.target.value)} onChange={(e) => setNewPromptContent(e.target.value)}
aria-label="Prompt Text"
></textarea> ></textarea>
</div> </div>
<div className="mt-6 flex flex-row-reverse"> <div className="mt-6 flex flex-row-reverse">
@@ -111,8 +120,9 @@ function EditPrompt({
onClick={() => { onClick={() => {
setModalState('INACTIVE'); setModalState('INACTIVE');
}} }}
aria-label="Close edit prompt modal"
> >
<img className="filter dark:invert" src={Exit} /> <img className="filter dark:invert" src={Exit} alt="Close modal" />
</button> </button>
<div className="p-8"> <div className="p-8">
<p className="mb-1 text-xl text-jet dark:text-bright-gray"> <p className="mb-1 text-xl text-jet dark:text-bright-gray">
@@ -122,13 +132,16 @@ function EditPrompt({
{t('modals.prompts.editDescription')} {t('modals.prompts.editDescription')}
</p> </p>
<div> <div>
<label htmlFor="edit-prompt-name" className="sr-only">
Prompt Name
</label>
<Input <Input
placeholder={t('modals.prompts.promptName')} placeholder={t('modals.prompts.promptName')}
type="text" type="text"
className="h-10 rounded-lg" className="h-10 rounded-lg"
value={editPromptName} value={editPromptName}
onChange={(e) => setEditPromptName(e.target.value)} onChange={(e) => setEditPromptName(e.target.value)}
></Input> />
<div className="relative bottom-12 left-3 mt-[-3.00px]"> <div className="relative bottom-12 left-3 mt-[-3.00px]">
<span className="bg-white px-1 text-xs text-silver dark:bg-outer-space dark:text-silver"> <span className="bg-white px-1 text-xs text-silver dark:bg-outer-space dark:text-silver">
{t('modals.prompts.promptName')} {t('modals.prompts.promptName')}
@@ -139,10 +152,15 @@ function EditPrompt({
{t('modals.prompts.promptText')} {t('modals.prompts.promptText')}
</span> </span>
</div> </div>
<label htmlFor="edit-prompt-content" className="sr-only">
Prompt Text
</label>
<textarea <textarea
id="edit-prompt-content"
className="h-56 w-full rounded-lg border-2 border-silver px-3 py-2 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white" className="h-56 w-full rounded-lg border-2 border-silver px-3 py-2 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
value={editPromptContent} value={editPromptContent}
onChange={(e) => setEditPromptContent(e.target.value)} onChange={(e) => setEditPromptContent(e.target.value)}
aria-label="Prompt Text"
></textarea> ></textarea>
</div> </div>
<div className="mt-6 flex flex-row-reverse gap-4"> <div className="mt-6 flex flex-row-reverse gap-4">

View File

@@ -115,12 +115,20 @@ export default function APIKeys() {
<table className="min-w-full divide-y divide-silver dark:divide-silver/40 "> <table className="min-w-full divide-y divide-silver dark:divide-silver/40 ">
<thead> <thead>
<tr className="text-start text-sm font-medium text-gray-700 dark:text-gray-50 uppercase"> <tr className="text-start text-sm font-medium text-gray-700 dark:text-gray-50 uppercase">
<th className="p-2">{t('settings.apiKeys.name')}</th> <th scope="col" className="p-2">
<th className="p-2"> {t('settings.apiKeys.name')}
</th>
<th scope="col" className="p-2">
{t('settings.apiKeys.sourceDoc')} {t('settings.apiKeys.sourceDoc')}
</th> </th>
<th className="p-2">{t('settings.apiKeys.key')}</th> <th scope="col" className="p-2">
<th></th> {t('settings.apiKeys.key')}
</th>
<th
scope="col"
className="p-2"
aria-label="Actions"
></th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-200 dark:divide-neutral-700"> <tbody className="divide-y divide-gray-200 dark:divide-neutral-700">
@@ -146,7 +154,7 @@ export default function APIKeys() {
<td> <td>
<img <img
src={Trash} src={Trash}
alt="Delete" alt={`Delete ${element.name}`}
className="h-4 w-4 cursor-pointer hover:opacity-50" className="h-4 w-4 cursor-pointer hover:opacity-50"
id={`img-${index}`} id={`img-${index}`}
onClick={() => handleDeleteKey(element.id)} onClick={() => handleDeleteKey(element.id)}

View File

@@ -215,6 +215,7 @@ export default function Analytics() {
} }
rounded="3xl" rounded="3xl"
border="border" border="border"
borderColor="gray-700"
/> />
</div> </div>

View File

@@ -54,10 +54,10 @@ const Documents: React.FC<DocumentsProps> = ({
const [totalPages, setTotalPages] = useState<number>(1); const [totalPages, setTotalPages] = useState<number>(1);
const currentDocuments = paginatedDocuments ?? []; const currentDocuments = paginatedDocuments ?? [];
const syncOptions = [ const syncOptions = [
{ label: 'Never', value: 'never' }, { label: t('settings.documents.syncFrequency.never'), value: 'never' },
{ label: 'Daily', value: 'daily' }, { label: t('settings.documents.syncFrequency.daily'), value: 'daily' },
{ label: 'Weekly', value: 'weekly' }, { label: t('settings.documents.syncFrequency.weekly'), value: 'weekly' },
{ label: 'Monthly', value: 'monthly' }, { label: t('settings.documents.syncFrequency.monthly'), value: 'monthly' },
]; ];
const refreshDocs = useCallback( const refreshDocs = useCallback(
@@ -151,9 +151,12 @@ const Documents: React.FC<DocumentsProps> = ({
<div className="z-10 w-full overflow-x-auto"> <div className="z-10 w-full overflow-x-auto">
<div className="my-3 flex justify-between items-center"> <div className="my-3 flex justify-between items-center">
<div className="p-1"> <div className="p-1">
<label htmlFor="document-search-input" className="sr-only">
{t('settings.documents.searchPlaceholder')}
</label>
<Input <Input
maxLength={256} maxLength={256}
placeholder="Search..." placeholder={t('settings.documents.searchPlaceholder')}
name="Document-search-input" name="Document-search-input"
type="text" type="text"
id="document-search-input" id="document-search-input"
@@ -161,21 +164,18 @@ const Documents: React.FC<DocumentsProps> = ({
onChange={(e) => { onChange={(e) => {
setSearchTerm(e.target.value); setSearchTerm(e.target.value);
setCurrentPage(1); setCurrentPage(1);
// refreshDocs(sortField, 1, rowsPerPage); }}
// do not call refreshDocs here the state is async
// so it will not have the updated value
}} // Handle search input change
/> />
</div> </div>
<button <button
className="rounded-full w-40 bg-purple-30 px-4 py-3 text-white hover:bg-[#6F3FD1]" className="rounded-full w-40 bg-purple-30 px-4 py-3 text-white hover:bg-[#6F3FD1]"
title="Add New Document" title={t('settings.documents.addNew')}
onClick={() => { onClick={() => {
setIsOnboarding(false); // Set onboarding flag if needed setIsOnboarding(false);
setModalState('ACTIVE'); // Open the upload modal setModalState('ACTIVE');
}} }}
> >
Add New {t('settings.documents.addNew')}
</button> </button>
</div> </div>
{loading ? ( {loading ? (
@@ -224,9 +224,10 @@ const Documents: React.FC<DocumentsProps> = ({
*/} */}
<th <th
scope="col" scope="col"
className="px-6 py-2 text-start font-medium text-gray-700 dark:text-gray-50 uppercase" className="px-6 py-2 text-start font-medium text-gray-700 dark:text-gray-50 uppercase sr-only"
aria-label={t('settings.documents.actions')}
> >
{' '} {t('settings.documents.actions')}
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -270,7 +271,7 @@ const Documents: React.FC<DocumentsProps> = ({
{document.type !== 'remote' && ( {document.type !== 'remote' && (
<img <img
src={Trash} src={Trash}
alt="Delete" alt={t('convTile.delete')}
className="h-4 w-4 cursor-pointer opacity-60 hover:opacity-100" className="h-4 w-4 cursor-pointer opacity-60 hover:opacity-100"
id={`img-${index}`} id={`img-${index}`}
onClick={(event) => { onClick={(event) => {
@@ -282,7 +283,7 @@ const Documents: React.FC<DocumentsProps> = ({
{document.syncFrequency && ( {document.syncFrequency && (
<div className="ml-2"> <div className="ml-2">
<DropdownMenu <DropdownMenu
name="Sync" name={t('settings.documents.sync')}
options={syncOptions} options={syncOptions}
onSelect={(value: string) => { onSelect={(value: string) => {
handleManageSync(document, value); handleManageSync(document, value);

View File

@@ -27,30 +27,12 @@ export default function General() {
]; ];
const languageOptions = [ const languageOptions = [
{ { label: 'English', value: 'en' },
label: 'English', { label: 'Español', value: 'es' },
value: 'en', { label: '日本語', value: 'jp' },
}, { label: '普通话', value: 'zh' },
{ { label: '繁體中文(臺灣)', value: 'zhTW' },
label: 'Spanish', { label: 'Русский', value: 'ru' },
value: 'es',
},
{
label: 'Japanese',
value: 'jp',
},
{
label: 'Mandarin',
value: 'zh',
},
{
label: 'Traditional Chinese',
value: 'zhTW',
},
{
label: 'Russian',
value: 'ru',
},
]; ];
const chunks = ['0', '2', '4', '6', '8', '10']; const chunks = ['0', '2', '4', '6', '8', '10'];
const token_limits = new Map([ const token_limits = new Map([
@@ -102,9 +84,9 @@ export default function General() {
return ( return (
<div className="mt-12"> <div className="mt-12">
<div className="mb-5"> <div className="mb-5">
<p className="font-bold text-jet dark:text-bright-gray"> <label className="block font-bold text-jet dark:text-bright-gray">
{t('settings.general.selectTheme')} {t('settings.general.selectTheme')}
</p> </label>
<Dropdown <Dropdown
options={themes} options={themes}
selectedValue={ selectedValue={
@@ -120,9 +102,9 @@ export default function General() {
/> />
</div> </div>
<div className="mb-5"> <div className="mb-5">
<p className="mb-2 font-bold text-jet dark:text-bright-gray"> <label className="block mb-2 font-bold text-jet dark:text-bright-gray">
{t('settings.general.selectLanguage')} {t('settings.general.selectLanguage')}
</p> </label>
<Dropdown <Dropdown
options={languageOptions.filter( options={languageOptions.filter(
(languageOption) => (languageOption) =>
@@ -138,9 +120,9 @@ export default function General() {
/> />
</div> </div>
<div className="mb-5"> <div className="mb-5">
<p className="font-bold text-jet dark:text-bright-gray"> <label className="block font-bold text-jet dark:text-bright-gray">
{t('settings.general.chunks')} {t('settings.general.chunks')}
</p> </label>
<Dropdown <Dropdown
options={chunks} options={chunks}
selectedValue={selectedChunks} selectedValue={selectedChunks}
@@ -151,9 +133,9 @@ export default function General() {
/> />
</div> </div>
<div className="mb-5"> <div className="mb-5">
<p className="mb-2 font-bold text-jet dark:text-bright-gray"> <label className="mb-2 block font-bold text-jet dark:text-bright-gray">
{t('settings.general.convHistory')} {t('settings.general.convHistory')}
</p> </label>
<Dropdown <Dropdown
options={Array.from(token_limits, ([value, desc]) => ({ options={Array.from(token_limits, ([value, desc]) => ({
value: value, value: value,
@@ -186,16 +168,14 @@ export default function General() {
/> />
</div> </div>
<div className="w-56"> <div className="w-56">
<p className="font-bold text-jet dark:text-bright-gray"> <label className="block font-bold text-jet dark:text-bright-gray">
{t('settings.general.deleteAllLabel')} {t('settings.general.deleteAllLabel')}
</p> </label>
<button <button
className="mt-2 flex w-full cursor-pointer items-center justify-between rounded-3xl border border-solid border-red-500 px-5 py-3 text-red-500 hover:bg-red-500 hover:text-white" className="mt-2 flex w-full cursor-pointer items-center justify-between rounded-3xl border border-solid border-red-700 px-5 py-3 text-red-700 transition-colors hover:bg-red-700 hover:text-white dark:border-red-600 dark:text-red-600 dark:hover:bg-red-600 dark:hover:text-white"
onClick={() => dispatch(setModalStateDeleteConv('ACTIVE'))} onClick={() => dispatch(setModalStateDeleteConv('ACTIVE'))}
> >
<span className="overflow-hidden text-ellipsis "> {t('settings.general.deleteAllBtn')}
{t('settings.general.deleteAllBtn')}
</span>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -67,9 +67,12 @@ export default function Logs() {
<div className="mt-12"> <div className="mt-12">
<div className="flex flex-col items-start"> <div className="flex flex-col items-start">
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<p className="font-bold text-jet dark:text-bright-gray"> <label
id="chatbot-filter-label"
className="font-bold text-jet dark:text-bright-gray"
>
{t('settings.logs.filterByChatbot')} {t('settings.logs.filterByChatbot')}
</p> </label>
{loadingChatbots ? ( {loadingChatbots ? (
<SkeletonLoader /> <SkeletonLoader />
) : ( ) : (

View File

@@ -168,7 +168,7 @@ export default function Prompts({
/> />
</div> </div>
<button <button
className="mt-[24px] rounded-3xl border border-solid border-purple-30 px-5 py-3 text-purple-30 hover:bg-purple-30 hover:text-white" className="mt-[24px] rounded-3xl border border-solid border-purple-700 px-5 py-3 text-purple-700 transition-colors hover:bg-purple-700 hover:text-white dark:border-purple-400 dark:text-purple-400 dark:hover:bg-purple-400 dark:hover:text-white"
onClick={() => { onClick={() => {
setModalType('ADD'); setModalType('ADD');
setModalState('ACTIVE'); setModalState('ACTIVE');

View File

@@ -72,18 +72,21 @@ export default function Tools() {
<div className="flex flex-col relative"> <div className="flex flex-col relative">
<div className="my-3 flex justify-between items-center gap-1"> <div className="my-3 flex justify-between items-center gap-1">
<div className="p-1"> <div className="p-1">
<label htmlFor="tool-search-input" className="sr-only">
{t('settings.tools.searchPlaceholder')}
</label>
<Input <Input
maxLength={256} maxLength={256}
placeholder={t('settings.tools.searchPlaceholder')} placeholder={t('settings.tools.searchPlaceholder')}
name="Document-search-input" name="Document-search-input"
type="text" type="text"
id="document-search-input" id="tool-search-input"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
/> />
</div> </div>
<button <button
className="rounded-full w-40 bg-purple-30 px-4 py-3 text-white hover:bg-[#6F3FD1] text-nowrap" className="rounded-full min-w-[160px] bg-purple-30 px-6 py-3 text-white hover:bg-[#6F3FD1] text-nowrap"
onClick={() => { onClick={() => {
setAddToolModalState('ACTIVE'); setAddToolModalState('ACTIVE');
}} }}
@@ -100,7 +103,7 @@ export default function Tools() {
<div className="mt-24 col-span-2 lg:col-span-3 text-center text-gray-500 dark:text-gray-400"> <div className="mt-24 col-span-2 lg:col-span-3 text-center text-gray-500 dark:text-gray-400">
<img <img
src={isDarkTheme ? NoFilesDarkIcon : NoFilesIcon} src={isDarkTheme ? NoFilesDarkIcon : NoFilesIcon}
alt={t('settings.tools.noToolsAlt')} alt="No tools found"
className="h-24 w-24 mx-auto mb-2" className="h-24 w-24 mx-auto mb-2"
/> />
{t('settings.tools.noToolsFound')} {t('settings.tools.noToolsFound')}
@@ -121,15 +124,19 @@ export default function Tools() {
<div className="w-full flex items-center justify-between"> <div className="w-full flex items-center justify-between">
<img <img
src={`/toolIcons/tool_${tool.name}.svg`} src={`/toolIcons/tool_${tool.name}.svg`}
alt={`${tool.displayName} icon`}
className="h-8 w-8" className="h-8 w-8"
/> />
<button <button
className="absolute top-3 right-3 cursor-pointer" className="absolute top-3 right-3 cursor-pointer"
onClick={() => handleSettingsClick(tool)} onClick={() => handleSettingsClick(tool)}
aria-label={t('settings.tools.configureToolAria', {
toolName: tool.displayName,
})}
> >
<img <img
src={CogwheelIcon} src={CogwheelIcon}
alt="settings" alt={t('settings.tools.settingsIconAlt')}
className="h-[19px] w-[19px]" className="h-[19px] w-[19px]"
/> />
</button> </button>
@@ -148,6 +155,11 @@ export default function Tools() {
htmlFor={`toolToggle-${index}`} htmlFor={`toolToggle-${index}`}
className="relative inline-block h-6 w-10 cursor-pointer rounded-full bg-gray-300 dark:bg-[#D2D5DA33]/20 transition [-webkit-tap-highlight-color:_transparent] has-[:checked]:bg-[#0C9D35CC] has-[:checked]:dark:bg-[#0C9D35CC]" className="relative inline-block h-6 w-10 cursor-pointer rounded-full bg-gray-300 dark:bg-[#D2D5DA33]/20 transition [-webkit-tap-highlight-color:_transparent] has-[:checked]:bg-[#0C9D35CC] has-[:checked]:dark:bg-[#0C9D35CC]"
> >
<span className="sr-only">
{t('settings.tools.toggleToolAria', {
toolName: tool.displayName,
})}
</span>
<input <input
type="checkbox" type="checkbox"
id={`toolToggle-${index}`} id={`toolToggle-${index}`}

View File

@@ -91,8 +91,8 @@ export default function Settings() {
case 'Widgets': case 'Widgets':
return ( return (
<Widgets <Widgets
widgetScreenshot={widgetScreenshot} // Add this line widgetScreenshot={widgetScreenshot}
onWidgetScreenshotChange={updateWidgetScreenshot} // Add this line onWidgetScreenshotChange={updateWidgetScreenshot}
/> />
); );
case t('settings.apiKeys.label'): case t('settings.apiKeys.label'):

View File

@@ -54,11 +54,11 @@ function Upload({
const setTimeoutRef = useRef<number | null>(); const setTimeoutRef = useRef<number | null>();
const urlOptions: { label: string; value: string }[] = [ const urlOptions: { label: string; value: string }[] = [
{ label: 'Crawler', value: 'crawler' }, { label: `Crawler`, value: 'crawler' },
// { label: 'Sitemap', value: 'sitemap' }, // { label: t('modals.uploadDoc.sitemap'), value: 'sitemap' },
{ label: 'Link', value: 'url' }, { label: `Link`, value: 'url' },
{ label: 'Reddit', value: 'reddit' }, { label: `GitHub`, value: 'github' },
{ label: 'GitHub', value: 'github' }, // P3f93 { label: `Reddit`, value: 'reddit' },
]; ];
const [urlType, setUrlType] = useState<{ label: string; value: string }>({ const [urlType, setUrlType] = useState<{ label: string; value: string }>({
@@ -113,12 +113,14 @@ function Upload({
<div className="mt-5 flex flex-col items-center gap-2 text-gray-2000 dark:text-bright-gray"> <div className="mt-5 flex flex-col items-center gap-2 text-gray-2000 dark:text-bright-gray">
<p className="text-gra text-xl tracking-[0.15px]"> <p className="text-gra text-xl tracking-[0.15px]">
{isTraining && {isTraining &&
(progress?.percentage === 100 ? 'Training completed' : title)} (progress?.percentage === 100
? t('modals.uploadDoc.progress.completed')
: title)}
{!isTraining && title} {!isTraining && title}
</p> </p>
<p className="text-sm">This may take several minutes</p> <p className="text-sm">{t('modals.uploadDoc.progress.wait')}</p>
<p className={`ml-5 text-xl text-red-400 ${isFailed ? '' : 'hidden'}`}> <p className={`ml-5 text-xl text-red-400 ${isFailed ? '' : 'hidden'}`}>
Over the token limit, please consider uploading smaller document {t('modals.uploadDoc.progress.tokenLimit')}
</p> </p>
{/* <p className="mt-10 text-2xl">{progress?.percentage || 0}%</p> */} {/* <p className="mt-10 text-2xl">{progress?.percentage || 0}%</p> */}
<ProgressBar progressPercent={progress?.percentage || 0} /> <ProgressBar progressPercent={progress?.percentage || 0} />
@@ -148,7 +150,7 @@ function Upload({
} }
function UploadProgress() { function UploadProgress() {
return <Progress title="Upload is in progress"></Progress>; return <Progress title={t('modals.uploadDoc.progress.upload')}></Progress>;
} }
function TrainingProgress() { function TrainingProgress() {
@@ -239,7 +241,7 @@ function Upload({
}, [progress, dispatch]); }, [progress, dispatch]);
return ( return (
<Progress <Progress
title="Training is in progress" title={t('modals.uploadDoc.progress.training')}
isCancellable={progress?.percentage === 100} isCancellable={progress?.percentage === 100}
isFailed={progress?.failed === true} isFailed={progress?.failed === true}
isTraining={true} isTraining={true}