diff --git a/application/agents/llm_handler.py b/application/agents/llm_handler.py index 9267dc53..a70357f8 100644 --- a/application/agents/llm_handler.py +++ b/application/agents/llm_handler.py @@ -72,9 +72,9 @@ class OpenAILLMHandler(LLMHandler): while True: tool_calls = {} for chunk in resp: - if isinstance(chunk, str): + if isinstance(chunk, str) and len(chunk) > 0: return - else: + elif hasattr(chunk, "delta"): chunk_delta = chunk.delta if ( @@ -113,6 +113,8 @@ class OpenAILLMHandler(LLMHandler): tool_response, call_id = agent._execute_tool_action( tools_dict, call ) + if isinstance(call["function"]["arguments"], str): + call["function"]["arguments"] = json.loads(call["function"]["arguments"]) function_call_dict = { "function_call": { @@ -156,6 +158,8 @@ class OpenAILLMHandler(LLMHandler): and chunk.finish_reason == "stop" ): return + elif isinstance(chunk, str) and len(chunk) == 0: + continue resp = agent.llm.gen_stream( model=agent.gpt_model, messages=messages, tools=agent.tools diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py index ce06fda5..16d2c481 100644 --- a/application/api/answer/routes.py +++ b/application/api/answer/routes.py @@ -42,6 +42,8 @@ elif settings.LLM_NAME == "anthropic": gpt_model = "claude-2" elif settings.LLM_NAME == "groq": gpt_model = "llama3-8b-8192" +elif settings.LLM_NAME == "novita": + gpt_model = "deepseek/deepseek-r1" if settings.MODEL_NAME: # in case there is particular model name configured gpt_model = settings.MODEL_NAME @@ -733,7 +735,6 @@ class Search(Resource): retriever = RetrieverCreator.create_retriever( retriever_name, - question=question, source=source, chat_history=[], prompt="default", @@ -743,7 +744,7 @@ class Search(Resource): user_api_key=user_api_key, ) - docs = retriever.search() + docs = retriever.search(question) retriever_params = retriever.get_params() user_logs_collection.insert_one( diff --git a/application/cache.py b/application/cache.py index 80dee4f4..117b444a 100644 --- a/application/cache.py +++ b/application/cache.py @@ -11,21 +11,25 @@ from application.utils import get_hash logger = logging.getLogger(__name__) _redis_instance = None +_redis_creation_failed = False _instance_lock = Lock() - def get_redis_instance(): - global _redis_instance - if _redis_instance is None: + global _redis_instance, _redis_creation_failed + if _redis_instance is None and not _redis_creation_failed: with _instance_lock: - if _redis_instance is None: + if _redis_instance is None and not _redis_creation_failed: try: _redis_instance = redis.Redis.from_url( settings.CACHE_REDIS_URL, socket_connect_timeout=2 ) + except ValueError as e: + logger.error(f"Invalid Redis URL: {e}") + _redis_creation_failed = True # Stop future attempts + _redis_instance = None except redis.ConnectionError as e: logger.error(f"Redis connection error: {e}") - _redis_instance = None + _redis_instance = None # Keep trying for connection errors return _redis_instance @@ -41,36 +45,48 @@ def gen_cache_key(messages, model="docgpt", tools=None): def gen_cache(func): def wrapper(self, model, messages, stream, tools=None, *args, **kwargs): + if tools is not None: + return func(self, model, messages, stream, tools, *args, **kwargs) + try: cache_key = gen_cache_key(messages, model, tools) - redis_client = get_redis_instance() - if redis_client: - try: - cached_response = redis_client.get(cache_key) - if cached_response: - return cached_response.decode("utf-8") - except redis.ConnectionError as e: - logger.error(f"Redis connection error: {e}") - - result = func(self, model, messages, stream, tools, *args, **kwargs) - if redis_client and isinstance(result, str): - try: - redis_client.set(cache_key, result, ex=1800) - except redis.ConnectionError as e: - logger.error(f"Redis connection error: {e}") - - return result except ValueError as e: - logger.error(e) - return "Error: No user message found in the conversation to generate a cache key." + logger.error(f"Cache key generation failed: {e}") + return func(self, model, messages, stream, tools, *args, **kwargs) + + redis_client = get_redis_instance() + if redis_client: + try: + cached_response = redis_client.get(cache_key) + if cached_response: + return cached_response.decode("utf-8") + except Exception as e: + logger.error(f"Error getting cached response: {e}") + + result = func(self, model, messages, stream, tools, *args, **kwargs) + if redis_client and isinstance(result, str): + try: + redis_client.set(cache_key, result, ex=1800) + except Exception as e: + logger.error(f"Error setting cache: {e}") + + return result return wrapper def stream_cache(func): def wrapper(self, model, messages, stream, tools=None, *args, **kwargs): - cache_key = gen_cache_key(messages, model, tools) - logger.info(f"Stream cache key: {cache_key}") + if tools is not None: + yield from func(self, model, messages, stream, tools, *args, **kwargs) + return + + try: + cache_key = gen_cache_key(messages, model, tools) + except ValueError as e: + logger.error(f"Cache key generation failed: {e}") + yield from func(self, model, messages, stream, tools, *args, **kwargs) + return redis_client = get_redis_instance() if redis_client: @@ -81,23 +97,21 @@ def stream_cache(func): cached_response = json.loads(cached_response.decode("utf-8")) for chunk in cached_response: yield chunk - time.sleep(0.03) + time.sleep(0.03) # Simulate streaming delay return - except redis.ConnectionError as e: - logger.error(f"Redis connection error: {e}") + except Exception as e: + logger.error(f"Error getting cached stream: {e}") - result = func(self, model, messages, stream, tools=tools, *args, **kwargs) stream_cache_data = [] - - for chunk in result: - stream_cache_data.append(chunk) + for chunk in func(self, model, messages, stream, tools, *args, **kwargs): yield chunk + stream_cache_data.append(str(chunk)) if redis_client: try: redis_client.set(cache_key, json.dumps(stream_cache_data), ex=1800) logger.info(f"Stream cache saved for key: {cache_key}") - except redis.ConnectionError as e: - logger.error(f"Redis connection error: {e}") + except Exception as e: + logger.error(f"Error setting stream cache: {e}") return wrapper diff --git a/application/llm/docsgpt_provider.py b/application/llm/docsgpt_provider.py index bb23d824..001035c4 100644 --- a/application/llm/docsgpt_provider.py +++ b/application/llm/docsgpt_provider.py @@ -1,34 +1,131 @@ -from application.llm.base import BaseLLM import json -import requests + +from application.core.settings import settings +from application.llm.base import BaseLLM class DocsGPTAPILLM(BaseLLM): def __init__(self, api_key=None, user_api_key=None, *args, **kwargs): + from openai import OpenAI + super().__init__(*args, **kwargs) - self.api_key = api_key + self.client = OpenAI(api_key="sk-docsgpt-public", base_url="https://oai.arc53.com") self.user_api_key = user_api_key - self.endpoint = "https://llm.arc53.com" + self.api_key = api_key - def _raw_gen(self, baseself, model, messages, stream=False, *args, **kwargs): - response = requests.post( - f"{self.endpoint}/answer", json={"messages": messages, "max_new_tokens": 30} - ) - response_clean = response.json()["a"].replace("###", "") + def _clean_messages_openai(self, messages): + cleaned_messages = [] + for message in messages: + role = message.get("role") + content = message.get("content") - return response_clean + if role == "model": + role = "assistant" - def _raw_gen_stream(self, baseself, model, messages, stream=True, *args, **kwargs): - response = requests.post( - f"{self.endpoint}/stream", - json={"messages": messages, "max_new_tokens": 256}, - stream=True, - ) + if role and content is not None: + if isinstance(content, str): + cleaned_messages.append({"role": role, "content": content}) + elif isinstance(content, list): + for item in content: + if "text" in item: + cleaned_messages.append( + {"role": role, "content": item["text"]} + ) + elif "function_call" in item: + tool_call = { + "id": item["function_call"]["call_id"], + "type": "function", + "function": { + "name": item["function_call"]["name"], + "arguments": json.dumps( + item["function_call"]["args"] + ), + }, + } + cleaned_messages.append( + { + "role": "assistant", + "content": None, + "tool_calls": [tool_call], + } + ) + elif "function_response" in item: + cleaned_messages.append( + { + "role": "tool", + "tool_call_id": item["function_response"][ + "call_id" + ], + "content": json.dumps( + item["function_response"]["response"]["result"] + ), + } + ) + else: + raise ValueError( + f"Unexpected content dictionary format: {item}" + ) + else: + raise ValueError(f"Unexpected content type: {type(content)}") - for line in response.iter_lines(): - if line: - data_str = line.decode("utf-8") - if data_str.startswith("data: "): - data = json.loads(data_str[6:]) - yield data["a"] + return cleaned_messages + + def _raw_gen( + self, + baseself, + model, + messages, + stream=False, + tools=None, + engine=settings.AZURE_DEPLOYMENT_NAME, + **kwargs, + ): + messages = self._clean_messages_openai(messages) + if tools: + response = self.client.chat.completions.create( + model="docsgpt", + messages=messages, + stream=stream, + tools=tools, + **kwargs, + ) + return response.choices[0] + else: + response = self.client.chat.completions.create( + model="docsgpt", messages=messages, stream=stream, **kwargs + ) + return response.choices[0].message.content + + def _raw_gen_stream( + self, + baseself, + model, + messages, + stream=True, + tools=None, + engine=settings.AZURE_DEPLOYMENT_NAME, + **kwargs, + ): + messages = self._clean_messages_openai(messages) + if tools: + response = self.client.chat.completions.create( + model="docsgpt", + messages=messages, + stream=stream, + tools=tools, + **kwargs, + ) + else: + response = self.client.chat.completions.create( + model="docsgpt", messages=messages, stream=stream, **kwargs + ) + + for line in response: + if len(line.choices) > 0 and line.choices[0].delta.content is not None and len(line.choices[0].delta.content) > 0: + yield line.choices[0].delta.content + elif len(line.choices) > 0: + yield line.choices[0] + + def _supports_tools(self): + return True \ No newline at end of file diff --git a/application/llm/llm_creator.py b/application/llm/llm_creator.py index f32089de..9f1305ba 100644 --- a/application/llm/llm_creator.py +++ b/application/llm/llm_creator.py @@ -7,7 +7,7 @@ from application.llm.anthropic import AnthropicLLM from application.llm.docsgpt_provider import DocsGPTAPILLM from application.llm.premai import PremAILLM from application.llm.google_ai import GoogleLLM - +from application.llm.novita import NovitaLLM class LLMCreator: llms = { @@ -20,7 +20,8 @@ class LLMCreator: "docsgpt": DocsGPTAPILLM, "premai": PremAILLM, "groq": GroqLLM, - "google": GoogleLLM + "google": GoogleLLM, + "novita": NovitaLLM } @classmethod diff --git a/application/llm/novita.py b/application/llm/novita.py new file mode 100644 index 00000000..8d6ac042 --- /dev/null +++ b/application/llm/novita.py @@ -0,0 +1,32 @@ +from application.llm.base import BaseLLM +from openai import OpenAI + + +class NovitaLLM(BaseLLM): + def __init__(self, api_key=None, user_api_key=None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.client = OpenAI(api_key=api_key, base_url="https://api.novita.ai/v3/openai") + self.api_key = api_key + self.user_api_key = user_api_key + + def _raw_gen(self, baseself, model, messages, stream=False, tools=None, **kwargs): + if tools: + response = self.client.chat.completions.create( + model=model, messages=messages, stream=stream, tools=tools, **kwargs + ) + return response.choices[0] + else: + response = self.client.chat.completions.create( + model=model, messages=messages, stream=stream, **kwargs + ) + return response.choices[0].message.content + + def _raw_gen_stream( + self, baseself, model, messages, stream=True, tools=None, **kwargs + ): + response = self.client.chat.completions.create( + model=model, messages=messages, stream=stream, **kwargs + ) + for line in response: + if line.choices[0].delta.content is not None: + yield line.choices[0].delta.content diff --git a/application/llm/openai.py b/application/llm/openai.py index 938de523..5e11a072 100644 --- a/application/llm/openai.py +++ b/application/llm/openai.py @@ -125,9 +125,9 @@ class OpenAILLM(BaseLLM): ) for line in response: - if line.choices[0].delta.content is not None: + if len(line.choices) > 0 and line.choices[0].delta.content is not None and len(line.choices[0].delta.content) > 0: yield line.choices[0].delta.content - else: + elif len(line.choices) > 0: yield line.choices[0] def _supports_tools(self): @@ -137,17 +137,17 @@ class OpenAILLM(BaseLLM): class AzureOpenAILLM(OpenAILLM): def __init__( - self, openai_api_key, openai_api_base, openai_api_version, deployment_name + self, api_key, user_api_key, *args, **kwargs ): - super().__init__(openai_api_key) + + super().__init__(api_key) self.api_base = (settings.OPENAI_API_BASE,) self.api_version = (settings.OPENAI_API_VERSION,) self.deployment_name = (settings.AZURE_DEPLOYMENT_NAME,) from openai import AzureOpenAI self.client = AzureOpenAI( - api_key=openai_api_key, + api_key=api_key, api_version=settings.OPENAI_API_VERSION, - api_base=settings.OPENAI_API_BASE, - deployment_name=settings.AZURE_DEPLOYMENT_NAME, + azure_endpoint=settings.OPENAI_API_BASE ) diff --git a/application/requirements.txt b/application/requirements.txt index c00c24f7..5323fe85 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -4,7 +4,7 @@ beautifulsoup4==4.12.3 celery==5.4.0 dataclasses-json==0.6.7 docx2txt==0.8 -duckduckgo-search==7.4.2 +duckduckgo-search==7.5.2 ebooklib==0.18 elastic-transport==8.17.0 elasticsearch==8.17.1 @@ -30,12 +30,12 @@ jsonschema==4.23.0 jsonschema-spec==0.2.4 jsonschema-specifications==2023.7.1 kombu==5.4.2 -langchain==0.3.14 -langchain-community==0.3.14 -langchain-core==0.3.40 -langchain-openai==0.3.0 -langchain-text-splitters==0.3.5 -langsmith==0.2.10 +langchain==0.3.20 +langchain-community==0.3.19 +langchain-core==0.3.45 +langchain-openai==0.3.8 +langchain-text-splitters==0.3.6 +langsmith==0.3.15 lazy-object-proxy==1.10.0 lxml==5.3.1 markupsafe==3.0.2 @@ -45,7 +45,7 @@ multidict==6.1.0 mypy-extensions==1.0.0 networkx==3.4.2 numpy==2.2.1 -openai==1.59.5 +openai==1.66.3 openapi-schema-validator==0.6.3 openapi-spec-validator==0.6.0 openapi3-parser==1.1.19 diff --git a/frontend/src/Hero.tsx b/frontend/src/Hero.tsx index a000ab2e..0161eac2 100644 --- a/frontend/src/Hero.tsx +++ b/frontend/src/Hero.tsx @@ -1,6 +1,6 @@ -import { Fragment } from 'react'; import DocsGPT3 from './assets/cute_docsgpt3.svg'; import { useTranslation } from 'react-i18next'; + export default function Hero({ handleQuestion, }: { @@ -17,38 +17,41 @@ export default function Hero({ header: string; query: string; }>; - return ( -
-
-
- DocsGPT - docsgpt -
-
+ return ( +
+ {/* Header Section */} +
+
+ DocsGPT + docsgpt +
-
- {demos?.map( - (demo: { header: string; query: string }, key: number) => - demo.header && - demo.query && ( - + + {/* Demo Buttons Section */} +
+
+ {demos?.map( + (demo: { header: string; query: string }, key: number) => + demo.header && + demo.query && ( - - ), - )} + ), + )} +
); diff --git a/frontend/src/Navigation.tsx b/frontend/src/Navigation.tsx index c0fb8231..15d11b3e 100644 --- a/frontend/src/Navigation.tsx +++ b/frontend/src/Navigation.tsx @@ -247,7 +247,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { ref={navRef} className={`${ !navOpen && '-ml-96 md:-ml-[18rem]' - } duration-20 fixed top-0 z-20 flex h-full w-72 flex-col border-r-[1px] border-b-0 bg-white transition-all dark:border-r-purple-taupe dark:bg-chinese-black dark:text-white`} + } duration-20 fixed top-0 z-20 flex h-full w-72 flex-col border-r-[1px] border-b-0 bg-lotion dark:bg-chinese-black transition-all dark:border-r-purple-taupe dark:text-white`} >
`${ - isActive ? 'bg-gray-3000 dark:bg-transparent' : '' - } group sticky mx-4 mt-4 flex cursor-pointer gap-2.5 rounded-3xl border border-silver p-3 hover:border-rainy-gray hover:bg-gray-3000 dark:border-purple-taupe dark:text-white dark:hover:bg-transparent` + isActive ? 'bg-transparent' : '' + } group sticky mx-4 mt-4 flex cursor-pointer gap-2.5 rounded-3xl border border-silver p-3 hover:border-rainy-gray dark:border-purple-taupe dark:text-white hover:bg-transparent` } >
-
+
Upload document { setUploadModalState('ACTIVE'); @@ -392,7 +394,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { Settings

{t('settings.label')} diff --git a/frontend/src/assets/eye-view.svg b/frontend/src/assets/eye-view.svg new file mode 100644 index 00000000..1e569cfd --- /dev/null +++ b/frontend/src/assets/eye-view.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/src/assets/sync.svg b/frontend/src/assets/sync.svg index 003dec43..4733bbdb 100644 --- a/frontend/src/assets/sync.svg +++ b/frontend/src/assets/sync.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/src/assets/user.png b/frontend/src/assets/user.png deleted file mode 100644 index 3cc5d2f1..00000000 Binary files a/frontend/src/assets/user.png and /dev/null differ diff --git a/frontend/src/assets/user.svg b/frontend/src/assets/user.svg new file mode 100644 index 00000000..970bffb5 --- /dev/null +++ b/frontend/src/assets/user.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + diff --git a/frontend/src/components/ContextMenu.tsx b/frontend/src/components/ContextMenu.tsx new file mode 100644 index 00000000..96497482 --- /dev/null +++ b/frontend/src/components/ContextMenu.tsx @@ -0,0 +1,132 @@ +import { SyntheticEvent, useRef, useEffect, CSSProperties } from 'react'; + +export interface MenuOption { + icon?: string; + label: string; + onClick: (event: SyntheticEvent) => void; + variant?: 'primary' | 'danger'; + iconClassName?: string; + iconWidth?: number; + iconHeight?: number; +} + +interface ContextMenuProps { + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; + options: MenuOption[]; + anchorRef: React.RefObject; + className?: string; + position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'; + offset?: { x: number; y: number }; +} + +export default function ContextMenu({ + isOpen, + setIsOpen, + options, + anchorRef, + className = '', + position = 'bottom-right', + offset = { x: 0, y: 8 }, +}: ContextMenuProps) { + const menuRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + menuRef.current && + !menuRef.current.contains(event.target as Node) && + !anchorRef.current?.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + return () => + document.removeEventListener('mousedown', handleClickOutside); + } + }, [isOpen, setIsOpen]); + + if (!isOpen) return null; + + const getMenuPosition = (): CSSProperties => { + if (!anchorRef.current) return {}; + + const rect = anchorRef.current.getBoundingClientRect(); + const scrollY = window.scrollY || document.documentElement.scrollTop; + const scrollX = window.scrollX || document.documentElement.scrollLeft; + + let top = rect.bottom + scrollY + offset.y; + let left = rect.right + scrollX + offset.x; + + switch (position) { + case 'bottom-left': + left = rect.left + scrollX - offset.x; + break; + case 'top-right': + top = rect.top + scrollY - offset.y; + break; + case 'top-left': + top = rect.top + scrollY - offset.y; + left = rect.left + scrollX - offset.x; + break; + // bottom-right is default + } + + return { + position: 'fixed', + top: `${top}px`, + left: `${left}px`, + }; + }; + + return ( +

e.stopPropagation()} + > +
+ {options.map((option, index) => ( + + ))} +
+
+ ); +} diff --git a/frontend/src/components/CopyButton.tsx b/frontend/src/components/CopyButton.tsx index f0559f52..c430603f 100644 --- a/frontend/src/components/CopyButton.tsx +++ b/frontend/src/components/CopyButton.tsx @@ -1,18 +1,22 @@ import copy from 'copy-to-clipboard'; import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import CheckMark from '../assets/checkmark.svg?react'; import Copy from '../assets/copy.svg?react'; -export default function CoppyButton({ +export default function CopyButton({ text, colorLight, colorDark, + showText = false, }: { text: string; colorLight?: string; colorDark?: string; + showText?: boolean; }) { + const { t } = useTranslation(); const [copied, setCopied] = useState(false); const [isCopyHovered, setIsCopyHovered] = useState(false); @@ -25,29 +29,30 @@ export default function CoppyButton({ }; return ( -
handleCopyClick(text)} + onMouseEnter={() => setIsCopyHovered(true)} + onMouseLeave={() => setIsCopyHovered(false)} + className="flex items-center gap-2" > - {copied ? ( - setIsCopyHovered(true)} - onMouseLeave={() => setIsCopyHovered(false)} - /> - ) : ( - { - handleCopyClick(text); - }} - onMouseEnter={() => setIsCopyHovered(true)} - onMouseLeave={() => setIsCopyHovered(false)} - /> +
+ {copied ? ( + + ) : ( + + )} +
+ {showText && ( + + {copied ? t('conversation.copied') : t('conversation.copy')} + )} -
+ ); } diff --git a/frontend/src/components/Dropdown.tsx b/frontend/src/components/Dropdown.tsx index 5932933e..f8783aad 100644 --- a/frontend/src/components/Dropdown.tsx +++ b/frontend/src/components/Dropdown.tsx @@ -11,6 +11,7 @@ function Dropdown({ rounded = 'xl', border = 'border-2', borderColor = 'silver', + darkBorderColor = 'dim-gray', showEdit, onEdit, showDelete, @@ -38,6 +39,7 @@ function Dropdown({ rounded?: 'xl' | '3xl'; border?: 'border' | 'border-2'; borderColor?: string; + darkBorderColor?: string; showEdit?: boolean; onEdit?: (value: { name: string; id: string; type: string }) => void; showDelete?: boolean; @@ -77,7 +79,7 @@ function Dropdown({ > {isOpen && (
{options.map((option: any, index) => (
void; defaultValue?: string; icon?: string; + isOpen?: boolean; + onOpenChange?: (isOpen: boolean) => void; + anchorRef?: React.RefObject; + className?: string; + position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'; + offset?: { x: number; y: number }; }; export default function DropdownMenu({ @@ -14,24 +21,33 @@ export default function DropdownMenu({ onSelect, defaultValue = 'none', icon, + isOpen: controlledIsOpen, + onOpenChange, + anchorRef, + className = '', + position = 'bottom-right', + offset = { x: 0, y: 8 }, }: DropdownMenuProps) { const dropdownRef = React.useRef(null); - const [isOpen, setIsOpen] = React.useState(false); + const [internalIsOpen, setInternalIsOpen] = React.useState(false); const [selectedOption, setSelectedOption] = React.useState( options.find((option) => option.value === defaultValue) || options[0], ); - const handleToggle = () => { - setIsOpen(!isOpen); - }; + const isOpen = + controlledIsOpen !== undefined ? controlledIsOpen : internalIsOpen; + const setIsOpen = onOpenChange || setInternalIsOpen; + const handleClickOutside = (event: MouseEvent) => { if ( dropdownRef.current && - !dropdownRef.current.contains(event.target as Node) + !dropdownRef.current.contains(event.target as Node) && + !anchorRef?.current?.contains(event.target as Node) ) { setIsOpen(false); } }; + const handleClickOption = (optionId: number) => { setIsOpen(false); setSelectedOption(options[optionId]); @@ -39,26 +55,40 @@ export default function DropdownMenu({ }; React.useEffect(() => { - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + return () => + document.removeEventListener('mousedown', handleClickOutside); + } + }, [isOpen]); + + if (!isOpen) return null; + + const getMenuPosition = (): React.CSSProperties => { + if (!anchorRef?.current) return {}; + + const rect = anchorRef.current.getBoundingClientRect(); + + const top = rect.bottom + offset.y; + const left = rect.right + offset.x; + + return { + position: 'fixed', + top: `${top}px`, + left: `${left}px`, + zIndex: 9999, }; - }, []); - return ( -
- + }; + + // Use a portal to render the dropdown outside the table flow + return ReactDOM.createPortal( +
e.stopPropagation()} + >
-
+
, + document.body, ); } diff --git a/frontend/src/components/Input.tsx b/frontend/src/components/Input.tsx index e703ff14..3fd84bdc 100644 --- a/frontend/src/components/Input.tsx +++ b/frontend/src/components/Input.tsx @@ -1,4 +1,5 @@ import { InputProps } from './types'; +import { useRef } from 'react'; const Input = ({ id, @@ -7,13 +8,13 @@ const Input = ({ value, isAutoFocused = false, placeholder, - label, required = false, maxLength, - className, + className = '', colorVariant = 'silver', borderVariant = 'thick', children, + labelBgClassName = 'bg-white dark:bg-raisin-black', onChange, onPaste, onKeyDown, @@ -27,15 +28,27 @@ const Input = ({ thin: 'border', thick: 'border-2', }; + + const inputRef = useRef(null); + return ( -
+
{children} - {label && ( -
- - {label} - {required && ( - - * - - )} - -
+ {placeholder && ( + )}
); diff --git a/frontend/src/components/MessageInput.tsx b/frontend/src/components/MessageInput.tsx new file mode 100644 index 00000000..ef422ca3 --- /dev/null +++ b/frontend/src/components/MessageInput.tsx @@ -0,0 +1,94 @@ +import { useEffect, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDarkTheme } from '../hooks'; +import Send from '../assets/send.svg'; +import SendDark from '../assets/send_dark.svg'; +import SpinnerDark from '../assets/spinner-dark.svg'; +import Spinner from '../assets/spinner.svg'; + +interface MessageInputProps { + value: string; + onChange: (e: React.ChangeEvent) => void; + onSubmit: () => void; + loading: boolean; +} + +export default function MessageInput({ + value, + onChange, + onSubmit, + loading, +}: MessageInputProps) { + const { t } = useTranslation(); + const [isDarkTheme] = useDarkTheme(); + const inputRef = useRef(null); + + const handleInput = () => { + if (inputRef.current) { + if (window.innerWidth < 350) inputRef.current.style.height = 'auto'; + else inputRef.current.style.height = '64px'; + inputRef.current.style.height = `${Math.min( + inputRef.current.scrollHeight, + 96, + )}px`; + } + }; + + // Focus the textarea and set initial height on mount. + useEffect(() => { + inputRef.current?.focus(); + handleInput(); + }, []); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + onSubmit(); + if (inputRef.current) { + inputRef.current.value = ''; + handleInput(); + } + } + }; + + return ( +
+ + - {status === 'loading' ? ( - {t('loading')} - ) : ( -
- -
- )} + setInput(e.target.value)} + onSubmit={handleQuestionSubmission} + loading={status === 'loading'} + />
-

+

{t('tagline')}

diff --git a/frontend/src/conversation/ConversationBubble.tsx b/frontend/src/conversation/ConversationBubble.tsx index 3ebf46b4..8638718f 100644 --- a/frontend/src/conversation/ConversationBubble.tsx +++ b/frontend/src/conversation/ConversationBubble.tsx @@ -5,10 +5,14 @@ import { useTranslation } from 'react-i18next'; import ReactMarkdown from 'react-markdown'; import { useSelector } from 'react-redux'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism'; +import { + vscDarkPlus, + oneLight, +} from 'react-syntax-highlighter/dist/cjs/styles/prism'; import rehypeKatex from 'rehype-katex'; import remarkGfm from 'remark-gfm'; import remarkMath from 'remark-math'; +import { useDarkTheme } from '../hooks'; import DocsGPT3 from '../assets/cute_docsgpt3.svg'; import ChevronDown from '../assets/chevron-down.svg'; @@ -18,7 +22,7 @@ import Edit from '../assets/edit.svg'; import Like from '../assets/like.svg?react'; import Link from '../assets/link.svg'; import Sources from '../assets/sources.svg'; -import UserIcon from '../assets/user.png'; +import UserIcon from '../assets/user.svg'; import Accordion from '../components/Accordion'; import Avatar from '../components/Avatar'; import CopyButton from '../components/CopyButton'; @@ -69,6 +73,7 @@ const ConversationBubble = forwardRef< ref, ) { const { t } = useTranslation(); + const [isDarkTheme] = useDarkTheme(); // const bubbleRef = useRef(null); const chunks = useSelector(selectChunks); const selectedDocs = useSelector(selectSelectedDocs); @@ -113,7 +118,7 @@ const ConversationBubble = forwardRef< style={{ wordBreak: 'break-word', }} - className="text-sm sm:text-base ml-2 mr-2 flex items-center rounded-[28px] bg-purple-30 py-[14px] px-[19px] text-white max-w-full whitespace-pre-wrap leading-normal" + className="text-sm sm:text-base ml-2 mr-2 flex items-center rounded-[28px] bg-gradient-to-b from-medium-purple to-slate-blue py-[14px] px-[19px] text-white max-w-full whitespace-pre-wrap leading-normal" > {message}
@@ -122,7 +127,7 @@ const ConversationBubble = forwardRef< setIsEditClicked(true); setEditInputBox(message); }} - className={`flex-shrink-0 h-fit mt-3 p-2 cursor-pointer rounded-full hover:bg-[#35363B] flex items-center ${isQuestionHovered || isEditClicked ? 'visible' : 'invisible'}`} + className={`flex-shrink-0 h-fit mt-3 p-2 cursor-pointer rounded-full hover:bg-light-silver dark:hover:bg-[#35363B] flex items-center ${isQuestionHovered || isEditClicked ? 'visible' : 'invisible'}`} > Edit @@ -138,29 +143,29 @@ const ConversationBubble = forwardRef< onChange={(e) => { setEditInputBox(e.target.value); }} - onKeyDown={(e) => { - if(e.key === 'Enter' && !e.shiftKey){ - e.preventDefault(); + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); handleEditClick(); } }} rows={5} value={editInputBox} - className="w-full resize-none border-2 border-black dark:border-white rounded-3xl px-4 py-3 text-base leading-relaxed text-black dark:bg-raisin-black dark:text-white focus:outline-none focus:ring-2 focus:ring-[#CDB5FF]" + className="w-full resize-none border border-silver dark:border-philippine-grey rounded-3xl px-4 py-3 text-base leading-relaxed text-carbon dark:text-chinese-white dark:bg-raisin-black focus:outline-none" />
- +
)} @@ -341,7 +346,7 @@ const ConversationBubble = forwardRef<

- - {String(children).replace(/\n$/, '')} - -
+
+
+ + {language} +
+ + {String(children).replace(/\n$/, '')} +
) : ( diff --git a/frontend/src/conversation/ConversationMessages.tsx b/frontend/src/conversation/ConversationMessages.tsx new file mode 100644 index 00000000..c83ee50c --- /dev/null +++ b/frontend/src/conversation/ConversationMessages.tsx @@ -0,0 +1,180 @@ +import { Fragment, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import ConversationBubble from './ConversationBubble'; +import Hero from '../Hero'; +import { FEEDBACK, Query, Status } from './conversationModels'; +import ArrowDown from '../assets/arrow-down.svg'; +import RetryIcon from '../components/RetryIcon'; +import { useDarkTheme } from '../hooks'; + +interface ConversationMessagesProps { + handleQuestion: (params: { + question: string; + isRetry?: boolean; + updated?: boolean | null; + indx?: number; + }) => void; + handleQuestionSubmission: ( + updatedQuestion?: string, + updated?: boolean, + indx?: number, + ) => void; + handleFeedback?: (query: Query, feedback: FEEDBACK, index: number) => void; + queries: Query[]; + status: Status; +} + +export default function ConversationMessages({ + handleQuestion, + handleQuestionSubmission, + queries, + status, + handleFeedback, +}: ConversationMessagesProps) { + const [isDarkTheme] = useDarkTheme(); + const { t } = useTranslation(); + + const conversationRef = useRef(null); + const [hasScrolledToLast, setHasScrolledToLast] = useState(true); + const [eventInterrupt, setEventInterrupt] = useState(false); + + const handleUserInterruption = () => { + if (!eventInterrupt && status === 'loading') { + setEventInterrupt(true); + } + }; + + const scrollIntoView = () => { + if (!conversationRef?.current || eventInterrupt) return; + + if (status === 'idle' || !queries[queries.length - 1]?.response) { + conversationRef.current.scrollTo({ + behavior: 'smooth', + top: conversationRef.current.scrollHeight, + }); + } else { + conversationRef.current.scrollTop = conversationRef.current.scrollHeight; + } + }; + + const checkScroll = () => { + const el = conversationRef.current; + if (!el) return; + const isBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 10; + setHasScrolledToLast(isBottom); + }; + + useEffect(() => { + !eventInterrupt && scrollIntoView(); + }, [queries.length, queries[queries.length - 1]]); + + useEffect(() => { + if (status === 'idle') { + setEventInterrupt(false); + } + }, [status]); + + useEffect(() => { + conversationRef.current?.addEventListener('scroll', checkScroll); + return () => { + conversationRef.current?.removeEventListener('scroll', checkScroll); + }; + }, []); + + const prepResponseView = (query: Query, index: number) => { + let responseView; + if (query.response) { + responseView = ( + handleFeedback(query, feedback, index) + : undefined + } + /> + ); + } else if (query.error) { + const retryBtn = ( + + ); + responseView = ( + + ); + } + return responseView; + }; + + return ( +
+ {queries.length > 0 && !hasScrolledToLast && ( + + )} + +
+ {queries.length > 0 ? ( + queries.map((query, index) => ( + + + {prepResponseView(query, index)} + + )) + ) : ( + + )} +
+
+ ); +} diff --git a/frontend/src/conversation/ConversationTile.tsx b/frontend/src/conversation/ConversationTile.tsx index c29b9d71..c9efcd02 100644 --- a/frontend/src/conversation/ConversationTile.tsx +++ b/frontend/src/conversation/ConversationTile.tsx @@ -18,6 +18,9 @@ import { selectConversationId } from '../preferences/preferenceSlice'; import { ActiveState } from '../models/misc'; import { ShareConversationModal } from '../modals/ShareConversationModal'; import { useTranslation } from 'react-i18next'; +import ContextMenu from '../components/ContextMenu'; +import { MenuOption } from '../components/ContextMenu'; +import { useOutsideAlerter } from '../hooks'; interface ConversationProps { name: string; @@ -128,6 +131,50 @@ export default function ConversationTile({ } }; + const menuOptions: MenuOption[] = [ + { + icon: Share, + label: t('convTile.share'), + onClick: (event: SyntheticEvent) => { + event.stopPropagation(); + setShareModalState(true); + setOpen(false); + }, + variant: 'primary', + iconWidth: 14, + iconHeight: 14, + }, + { + icon: Edit, + label: t('convTile.rename'), + onClick: handleEditConversation, + variant: 'primary', + }, + { + icon: Trash, + label: t('convTile.delete'), + onClick: (event: SyntheticEvent) => { + event.stopPropagation(); + setDeleteModalState('ACTIVE'); + setOpen(false); + }, + iconWidth: 18, + iconHeight: 18, + variant: 'danger', + }, + ]; + + useOutsideAlerter( + tileRef, + () => { + if (isEdit) { + onClear(); + } + }, + [isEdit], + true, + ); + return ( <>
{ - setIsHovered(false); + if (!isEdit) { + setIsHovered(false); + } }} onClick={() => { onCoversationClick(); conversationId !== conversation.id && selectConversation(conversation.id); }} - className={`my-auto mx-4 mt-4 flex h-9 cursor-pointer items-center justify-between pl-4 gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${ - conversationId === conversation.id || isOpen || isHovered - ? 'bg-gray-100 dark:bg-[#28292E]' + className={`my-auto mx-4 mt-4 flex h-9 cursor-pointer items-center justify-between pl-4 gap-4 rounded-3xl hover:bg-bright-gray dark:hover:bg-dark-charcoal ${ + conversationId === conversation.id || isOpen || isHovered || isEdit + ? 'bg-bright-gray dark:bg-dark-charcoal' : '' }`} > @@ -160,13 +209,13 @@ export default function ConversationTile({ onKeyDown={handleRenameKeyDown} /> ) : ( -

+

{conversationName}

)}
{(conversationId === conversation.id || isHovered || isOpen) && ( -
+
{isEdit ? (
)} - {isOpen && ( -
- - - -
- )} +
)}
diff --git a/frontend/src/conversation/SharedConversation.tsx b/frontend/src/conversation/SharedConversation.tsx index c44d6598..28ce9773 100644 --- a/frontend/src/conversation/SharedConversation.tsx +++ b/frontend/src/conversation/SharedConversation.tsx @@ -1,16 +1,20 @@ import { Fragment, useEffect, useRef, useState } from 'react'; import { Helmet } from 'react-helmet'; + import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate, useParams } from 'react-router-dom'; - +import ConversationMessages from './ConversationMessages'; +import MessageInput from '../components/MessageInput'; import conversationService from '../api/services/conversationService'; + import Send from '../assets/send.svg'; import Spinner from '../assets/spinner.svg'; import { selectToken } from '../preferences/preferenceSlice'; import { AppDispatch } from '../store'; import ConversationBubble from './ConversationBubble'; import { Query } from './conversationModels'; + import { addQuery, fetchSharedAnswer, @@ -24,6 +28,7 @@ import { setIdentifier, updateQuery, } from './sharedConversationSlice'; +import { formatDate } from '../utils/dateTimeUtils'; export const SharedConversation = () => { const navigate = useNavigate(); @@ -36,27 +41,14 @@ export const SharedConversation = () => { const apiKey = useSelector(selectClientAPIKey); const status = useSelector(selectStatus); - const inputRef = useRef(null); - const sharedConversationRef = useRef(null); + const [input, setInput] = useState(''); const { t } = useTranslation(); const dispatch = useDispatch(); const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false); - const [eventInterrupt, setEventInterrupt] = useState(false); - const endMessageRef = useRef(null); - const handleUserInterruption = () => { - if (!eventInterrupt && status === 'loading') setEventInterrupt(true); - }; - useEffect(() => { - !eventInterrupt && scrollIntoView(); - }, [queries.length, queries[queries.length - 1]]); useEffect(() => { identifier && dispatch(setIdentifier(identifier)); - const element = document.getElementById('inputbox') as HTMLInputElement; - if (element) { - element.focus(); - } }, []); useEffect(() => { @@ -66,20 +58,6 @@ export const SharedConversation = () => { } }, [queries[queries.length - 1]]); - const scrollIntoView = () => { - if (!sharedConversationRef?.current || eventInterrupt) return; - - if (status === 'idle' || !queries[queries.length - 1].response) { - sharedConversationRef.current.scrollTo({ - behavior: 'smooth', - top: sharedConversationRef.current.scrollHeight, - }); - } else { - sharedConversationRef.current.scrollTop = - sharedConversationRef.current.scrollHeight; - } - }; - const fetchQueries = () => { identifier && conversationService @@ -95,7 +73,7 @@ export const SharedConversation = () => { setFetchedData({ queries: data.queries, title: data.title, - date: data.date, + date: formatDate(data.timestamp), identifier, }), ); @@ -103,47 +81,16 @@ export const SharedConversation = () => { } }); }; - const handlePaste = (e: React.ClipboardEvent) => { - e.preventDefault(); - const text = e.clipboardData.getData('text/plain'); - inputRef.current && (inputRef.current.innerText = text); - }; - const prepResponseView = (query: Query, index: number) => { - let responseView; - if (query.response) { - responseView = ( - - ); - } else if (query.error) { - responseView = ( - - ); - } - return responseView; - }; + const handleQuestionSubmission = () => { - if (inputRef.current?.textContent && status !== 'loading') { + if (input && status !== 'loading') { if (lastQueryReturnedErr) { // update last failed query with new prompt dispatch( updateQuery({ index: queries.length - 1, query: { - prompt: inputRef.current.textContent, + prompt: input, }, }), ); @@ -152,9 +99,9 @@ export const SharedConversation = () => { isRetry: true, }); } else { - handleQuestion({ question: inputRef.current.textContent }); + handleQuestion({ question: input }); } - inputRef.current.textContent = ''; + setInput(''); } }; @@ -167,7 +114,6 @@ export const SharedConversation = () => { }) => { question = question.trim(); if (question === '') return; - setEventInterrupt(false); !isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries dispatch(fetchSharedAnswer({ question })); }; @@ -192,93 +138,47 @@ export const SharedConversation = () => { content="Shared conversations with DocsGPT" /> -
-
-
-
-

- {title} -

-

- {t('sharedConv.subtitle')}{' '} - - DocsGPT - -

-

- {date} -

-
-
- {queries?.map((query, index) => { - return ( - - - - {prepResponseView(query, index)} - - ); - })} -
-
+
+
+

+ {title} +

+

+ {t('sharedConv.subtitle')}{' '} + + DocsGPT + +

+

+ {date} +

- -
+ +
{apiKey ? ( -
-
{ - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleQuestionSubmission(); - } - }} - >
- {status === 'loading' ? ( - - ) : ( -
- -
- )} -
+ setInput(e.target.value)} + onSubmit={() => handleQuestionSubmission()} + loading={status === 'loading'} + /> ) : ( )} - + +

{t('sharedConv.meta')} - +

diff --git a/frontend/src/conversation/conversationSlice.ts b/frontend/src/conversation/conversationSlice.ts index 651ead04..7cd14d5e 100644 --- a/frontend/src/conversation/conversationSlice.ts +++ b/frontend/src/conversation/conversationSlice.ts @@ -267,8 +267,7 @@ export const conversationSlice = createSlice({ return state; } state.status = 'failed'; - state.queries[state.queries.length - 1].error = - 'Something went wrong. Please check your internet connection.'; + state.queries[state.queries.length - 1].error = 'Something went wrong'; }); }, }); diff --git a/frontend/src/conversation/sharedConversationSlice.ts b/frontend/src/conversation/sharedConversationSlice.ts index bef306dd..7ae4862b 100644 --- a/frontend/src/conversation/sharedConversationSlice.ts +++ b/frontend/src/conversation/sharedConversationSlice.ts @@ -231,8 +231,7 @@ export const sharedConversationSlice = createSlice({ return state; } state.status = 'failed'; - state.queries[state.queries.length - 1].error = - 'Something went wrong. Please check your internet connection.'; + state.queries[state.queries.length - 1].error = 'Something went wrong'; }); }, }); diff --git a/frontend/src/index.css b/frontend/src/index.css index 85d6fced..07760385 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -485,25 +485,31 @@ template { ::-webkit-scrollbar { width: 10; } - -input:-webkit-autofill { - -webkit-box-shadow: 0 0 0 50px white inset; +/* Light mode specific autofill styles */ +input:-webkit-autofill, +input:-webkit-autofill:hover, +input:-webkit-autofill:focus, +input:-webkit-autofill:active { + -webkit-text-fill-color: #343541 !important; + -webkit-box-shadow: 0 0 0 30px transparent inset !important; + transition: background-color 5000s ease-in-out 0s; + caret-color: #343541; } -input:-webkit-autofill:focus { - -webkit-box-shadow: 0 0 0 50px white inset; +/* Dark mode specific autofill styles */ +.dark input:-webkit-autofill, +.dark input:-webkit-autofill:hover, +.dark input:-webkit-autofill:focus, +.dark input:-webkit-autofill:active { + -webkit-text-fill-color: #e5e7eb !important; + -webkit-box-shadow: 0 0 0 30px transparent inset !important; + background-color: transparent !important; + caret-color: #e5e7eb; } -@media (prefers-color-scheme: dark) { - input:-webkit-autofill { - -webkit-box-shadow: 0 0 0 50px rgb(68, 70, 84) inset; - -webkit-text-fill-color: white; - } - - input:-webkit-autofill:focus { - -webkit-box-shadow: 0 0 0 50px rgb(68, 70, 84) inset; - -webkit-text-fill-color: white; - } +/* Additional autocomplete dropdown styles for dark mode */ +.dark input:-webkit-autofill::first-line { + color: #e5e7eb; } .inputbox-style { diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 1081ddf0..54f6f441 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -50,7 +50,8 @@ "medium": "Medium", "high": "High", "unlimited": "Unlimited", - "default": "Default" + "default": "Default", + "add": "Add" }, "documents": { "title": "This table contains all the documents that are available to you and those you have uploaded", @@ -72,6 +73,7 @@ "monthly": "Monthly" }, "actions": "Actions", + "view": "View", "deleteWarning": "Are you sure you want to delete \"{{name}}\"?" }, "apiKeys": { @@ -97,7 +99,7 @@ }, "messages": "Messages", "tokenUsage": "Token Usage", - "feedback": "Feedback", + "userFeedback": "User Feedback", "filterPlaceholder": "Filter", "none": "None", "positiveFeedback": "Positive Feedback", diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index e699a3c6..fc5532bb 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -43,7 +43,7 @@ "prompt": "Prompt Activo", "deleteAllLabel": "Eliminar todas las conversaciones", "deleteAllBtn": "Eliminar todo", - "addNew": "Agregar Nuevo", + "add": "Añadir", "convHistory": "Historial de conversaciones", "none": "Ninguno", "low": "Bajo", @@ -72,6 +72,7 @@ "monthly": "Mensual" }, "actions": "Acciones", + "view": "Ver", "deleteWarning": "¿Estás seguro de que deseas eliminar \"{{name}}\"?" }, "apiKeys": { @@ -97,7 +98,7 @@ }, "messages": "Mensajes", "tokenUsage": "Uso de Tokens", - "feedback": "Retroalimentación", + "userFeedback": "Retroalimentación del Usuario", "filterPlaceholder": "Filtrar", "none": "Ninguno", "positiveFeedback": "Retroalimentación Positiva", diff --git a/frontend/src/locale/jp.json b/frontend/src/locale/jp.json index 7c716135..10ee9989 100644 --- a/frontend/src/locale/jp.json +++ b/frontend/src/locale/jp.json @@ -50,7 +50,8 @@ "medium": "中", "high": "高", "unlimited": "無制限", - "default": "デフォルト" + "default": "デフォルト", + "add": "追加" }, "documents": { "label": "ドキュメント", @@ -71,6 +72,7 @@ "monthly": "毎月" }, "actions": "アクション", + "view": "表示", "deleteWarning": "\"{{name}}\"を削除してもよろしいですか?" }, "apiKeys": { @@ -100,7 +102,8 @@ "filterPlaceholder": "フィルター", "none": "なし", "positiveFeedback": "肯定的なフィードバック", - "negativeFeedback": "否定的なフィードバック" + "negativeFeedback": "否定的なフィードバック", + "userFeedback": "ユーザーフィードバック" }, "logs": { "label": "ログ", diff --git a/frontend/src/locale/ru.json b/frontend/src/locale/ru.json index 3ec75bfb..f265ea08 100644 --- a/frontend/src/locale/ru.json +++ b/frontend/src/locale/ru.json @@ -50,7 +50,8 @@ "medium": "Средний", "high": "Высокий", "unlimited": "Без ограничений", - "default": "По умолчанию" + "default": "По умолчанию", + "add": "Добавить" }, "documents": { "title": "Эта таблица содержит все документы, которые доступны вам и те, которые вы загрузили", @@ -72,6 +73,7 @@ "monthly": "Ежемесячно" }, "actions": "Действия", + "view": "Просмотр", "deleteWarning": "Вы уверены, что хотите удалить \"{{name}}\"?" }, "apiKeys": { @@ -97,7 +99,7 @@ }, "messages": "Сообщения", "tokenUsage": "Использование токена", - "feedback": "Обратная связь", + "userFeedback": "Обратная Связь Пользователя", "filterPlaceholder": "Фильтр", "none": "Нет", "positiveFeedback": "Положительная обратная связь", diff --git a/frontend/src/locale/zh-TW.json b/frontend/src/locale/zh-TW.json index eb0240a0..97478172 100644 --- a/frontend/src/locale/zh-TW.json +++ b/frontend/src/locale/zh-TW.json @@ -50,7 +50,8 @@ "medium": "中", "high": "高", "unlimited": "無限制", - "default": "預設" + "default": "預設", + "add": "添加" }, "documents": { "title": "此表格包含所有可供您使用的文件以及您上傳的文件", @@ -72,6 +73,7 @@ "monthly": "每月" }, "actions": "操作", + "view": "查看", "deleteWarning": "您確定要刪除 \"{{name}}\" 嗎?" }, "apiKeys": { @@ -97,7 +99,7 @@ }, "messages": "訊息", "tokenUsage": "Token 使用量", - "feedback": "回饋", + "userFeedback": "使用者反饋", "filterPlaceholder": "篩選", "none": "無", "positiveFeedback": "正向回饋", diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json index 245333c3..20a216d2 100644 --- a/frontend/src/locale/zh.json +++ b/frontend/src/locale/zh.json @@ -50,7 +50,8 @@ "medium": "中", "high": "高", "unlimited": "无限", - "default": "默认" + "default": "默认", + "add": "添加" }, "documents": { "title": "此表格包含所有可供您使用的文档以及您上传的文档", @@ -72,6 +73,7 @@ "monthly": "每月" }, "actions": "操作", + "view": "查看", "deleteWarning": "您确定要删除 \"{{name}}\" 吗?" }, "apiKeys": { @@ -97,7 +99,7 @@ }, "messages": "消息", "tokenUsage": "令牌使用", - "feedback": "反馈", + "userFeedback": "用户反馈", "filterPlaceholder": "筛选", "none": "无", "positiveFeedback": "正向反馈", diff --git a/frontend/src/modals/AddActionModal.tsx b/frontend/src/modals/AddActionModal.tsx index c7c813c3..5db46558 100644 --- a/frontend/src/modals/AddActionModal.tsx +++ b/frontend/src/modals/AddActionModal.tsx @@ -59,6 +59,7 @@ export default function AddActionModal({ setFunctionNameError(!isValidFunctionName(value)); }} borderVariant="thin" + labelBgClassName="bg-white dark:bg-charleston-green-2" placeholder={'Enter name'} />

diff --git a/frontend/src/modals/AddToolModal.tsx b/frontend/src/modals/AddToolModal.tsx index 94aebe1e..9885edab 100644 --- a/frontend/src/modals/AddToolModal.tsx +++ b/frontend/src/modals/AddToolModal.tsx @@ -100,57 +100,60 @@ export default function AddToolModal({ {modalState === 'ACTIVE' && ( setModalState('INACTIVE')} - className="h-[85vh] w-[90vw] md:w-[75vw]" + className="max-w-[950px] w-[90vw] md:w-[85vw] lg:w-[75vw] h-[85vh]" > -

+

{t('settings.tools.selectToolSetup')}

-
+
{loading ? ( -
+
) : ( - availableTools.map((tool, index) => ( -
{ - setSelectedTool(tool); - handleAddTool(tool); - }} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { +
+ {availableTools.map((tool, index) => ( +
{ setSelectedTool(tool); handleAddTool(tool); - } - }} - > -
-
- -
-
-

- {tool.displayName} -

-

- {tool.description} -

+ }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + setSelectedTool(tool); + handleAddTool(tool); + } + }} + > +
+
+ {`${tool.name} +
+
+

+ {tool.displayName} +

+

+ {tool.description} +

+
-
- )) + ))} +
)}
diff --git a/frontend/src/modals/ChunkModal.tsx b/frontend/src/modals/ChunkModal.tsx index 83a100ee..4ef169d7 100644 --- a/frontend/src/modals/ChunkModal.tsx +++ b/frontend/src/modals/ChunkModal.tsx @@ -61,6 +61,7 @@ export default function ChunkModal({ onChange={(e) => setTitle(e.target.value)} borderVariant="thin" placeholder={'Enter title'} + labelBgClassName="bg-white dark:bg-charleston-green-2" >
@@ -83,7 +84,7 @@ export default function ChunkModal({ handleSubmit(title, chunkText); setModalState('INACTIVE'); }} - className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-[#6F3FD1]" + className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-violets-are-blue" > Add @@ -123,15 +124,13 @@ export default function ChunkModal({ Edit Chunk
- - Title - setTitle(e.target.value)} borderVariant="thin" placeholder={'Enter title'} + labelBgClassName="bg-white dark:bg-charleston-green-2" >
@@ -163,7 +162,7 @@ export default function ChunkModal({ handleSubmit(title, chunkText); setModalState('INACTIVE'); }} - className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-[#6F3FD1]" + className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-violets-are-blue" > Update diff --git a/frontend/src/modals/ConfigToolModal.tsx b/frontend/src/modals/ConfigToolModal.tsx index a8a80aaf..e631419a 100644 --- a/frontend/src/modals/ConfigToolModal.tsx +++ b/frontend/src/modals/ConfigToolModal.tsx @@ -65,7 +65,7 @@ export default function ConfigToolModal({ onChange={(e) => setAuthKey(e.target.value)} borderVariant="thin" placeholder={t('modals.configTool.apiKeyPlaceholder')} - label={t('modals.configTool.apiKeyLabel')} + labelBgClassName="bg-white dark:bg-charleston-green-2" />
@@ -73,7 +73,7 @@ export default function ConfigToolModal({ onClick={() => { tool && handleAddTool(tool); }} - className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-[#6F3FD1]" + className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-violets-are-blue" > {t('modals.configTool.addButton')} diff --git a/frontend/src/modals/ConfirmationModal.tsx b/frontend/src/modals/ConfirmationModal.tsx index 28b9c582..25f8c2da 100644 --- a/frontend/src/modals/ConfirmationModal.tsx +++ b/frontend/src/modals/ConfirmationModal.tsx @@ -11,6 +11,7 @@ export default function ConfirmationModal({ handleSubmit, cancelLabel, handleCancel, + variant = 'default', }: { message: string; modalState: ActiveState; @@ -19,8 +20,15 @@ export default function ConfirmationModal({ handleSubmit: () => void; cancelLabel?: string; handleCancel?: () => void; + variant?: 'default' | 'danger'; }) { const { t } = useTranslation(); + + const submitButtonClasses = + variant === 'danger' + ? 'rounded-3xl bg-rosso-corsa px-5 py-2 text-sm text-lotion transition-all hover:bg-red-2000 hover:font-bold tracking-[0.019em] hover:tracking-normal' + : 'rounded-3xl bg-purple-30 px-5 py-2 text-sm text-lotion transition-all hover:bg-violets-are-blue'; + return ( <> {modalState === 'ACTIVE' && ( @@ -39,7 +47,7 @@ export default function ConfirmationModal({
diff --git a/frontend/src/modals/CreateAPIKeyModal.tsx b/frontend/src/modals/CreateAPIKeyModal.tsx index 3d18f512..79e2120d 100644 --- a/frontend/src/modals/CreateAPIKeyModal.tsx +++ b/frontend/src/modals/CreateAPIKeyModal.tsx @@ -85,9 +85,10 @@ export default function CreateAPIKeyModal({ type="text" className="rounded-md" value={APIKeyName} - label={t('modals.createAPIKey.apiKeyName')} + placeholder={t('modals.createAPIKey.apiKeyName')} onChange={(e) => setAPIKeyName(e.target.value)} borderVariant="thin" + labelBgClassName="bg-white dark:bg-charleston-green-2" >
@@ -145,7 +146,7 @@ export default function CreateAPIKeyModal({ createAPIKey(payload); } }} - className="float-right mt-4 rounded-full bg-purple-30 px-5 py-2 text-sm text-white hover:bg-[#6F3FD1] disabled:opacity-50" + className="float-right mt-4 rounded-full bg-purple-30 px-5 py-2 text-sm text-white hover:bg-violets-are-blue disabled:opacity-50" > {t('modals.createAPIKey.create')} diff --git a/frontend/src/modals/DeleteConvModal.tsx b/frontend/src/modals/DeleteConvModal.tsx index 43ec2a65..630300ad 100644 --- a/frontend/src/modals/DeleteConvModal.tsx +++ b/frontend/src/modals/DeleteConvModal.tsx @@ -42,6 +42,7 @@ export default function DeleteConvModal({ submitLabel={t('modals.deleteConv.delete')} handleSubmit={handleSubmit} handleCancel={handleCancel} + variant="danger" /> ); } diff --git a/frontend/src/modals/SaveAPIKeyModal.tsx b/frontend/src/modals/SaveAPIKeyModal.tsx index abe862cb..1e589a18 100644 --- a/frontend/src/modals/SaveAPIKeyModal.tsx +++ b/frontend/src/modals/SaveAPIKeyModal.tsx @@ -34,7 +34,7 @@ export default function SaveAPIKeyModal({
) : (