mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
Merge branch 'main' into feat/jwt-auth
This commit is contained in:
@@ -72,9 +72,9 @@ class OpenAILLMHandler(LLMHandler):
|
|||||||
while True:
|
while True:
|
||||||
tool_calls = {}
|
tool_calls = {}
|
||||||
for chunk in resp:
|
for chunk in resp:
|
||||||
if isinstance(chunk, str):
|
if isinstance(chunk, str) and len(chunk) > 0:
|
||||||
return
|
return
|
||||||
else:
|
elif hasattr(chunk, "delta"):
|
||||||
chunk_delta = chunk.delta
|
chunk_delta = chunk.delta
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -113,6 +113,8 @@ class OpenAILLMHandler(LLMHandler):
|
|||||||
tool_response, call_id = agent._execute_tool_action(
|
tool_response, call_id = agent._execute_tool_action(
|
||||||
tools_dict, call
|
tools_dict, call
|
||||||
)
|
)
|
||||||
|
if isinstance(call["function"]["arguments"], str):
|
||||||
|
call["function"]["arguments"] = json.loads(call["function"]["arguments"])
|
||||||
|
|
||||||
function_call_dict = {
|
function_call_dict = {
|
||||||
"function_call": {
|
"function_call": {
|
||||||
@@ -156,6 +158,8 @@ class OpenAILLMHandler(LLMHandler):
|
|||||||
and chunk.finish_reason == "stop"
|
and chunk.finish_reason == "stop"
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
elif isinstance(chunk, str) and len(chunk) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
resp = agent.llm.gen_stream(
|
resp = agent.llm.gen_stream(
|
||||||
model=agent.gpt_model, messages=messages, tools=agent.tools
|
model=agent.gpt_model, messages=messages, tools=agent.tools
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ elif settings.LLM_NAME == "anthropic":
|
|||||||
gpt_model = "claude-2"
|
gpt_model = "claude-2"
|
||||||
elif settings.LLM_NAME == "groq":
|
elif settings.LLM_NAME == "groq":
|
||||||
gpt_model = "llama3-8b-8192"
|
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
|
if settings.MODEL_NAME: # in case there is particular model name configured
|
||||||
gpt_model = settings.MODEL_NAME
|
gpt_model = settings.MODEL_NAME
|
||||||
@@ -733,7 +735,6 @@ class Search(Resource):
|
|||||||
|
|
||||||
retriever = RetrieverCreator.create_retriever(
|
retriever = RetrieverCreator.create_retriever(
|
||||||
retriever_name,
|
retriever_name,
|
||||||
question=question,
|
|
||||||
source=source,
|
source=source,
|
||||||
chat_history=[],
|
chat_history=[],
|
||||||
prompt="default",
|
prompt="default",
|
||||||
@@ -743,7 +744,7 @@ class Search(Resource):
|
|||||||
user_api_key=user_api_key,
|
user_api_key=user_api_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
docs = retriever.search()
|
docs = retriever.search(question)
|
||||||
retriever_params = retriever.get_params()
|
retriever_params = retriever.get_params()
|
||||||
|
|
||||||
user_logs_collection.insert_one(
|
user_logs_collection.insert_one(
|
||||||
|
|||||||
@@ -11,21 +11,25 @@ from application.utils import get_hash
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_redis_instance = None
|
_redis_instance = None
|
||||||
|
_redis_creation_failed = False
|
||||||
_instance_lock = Lock()
|
_instance_lock = Lock()
|
||||||
|
|
||||||
|
|
||||||
def get_redis_instance():
|
def get_redis_instance():
|
||||||
global _redis_instance
|
global _redis_instance, _redis_creation_failed
|
||||||
if _redis_instance is None:
|
if _redis_instance is None and not _redis_creation_failed:
|
||||||
with _instance_lock:
|
with _instance_lock:
|
||||||
if _redis_instance is None:
|
if _redis_instance is None and not _redis_creation_failed:
|
||||||
try:
|
try:
|
||||||
_redis_instance = redis.Redis.from_url(
|
_redis_instance = redis.Redis.from_url(
|
||||||
settings.CACHE_REDIS_URL, socket_connect_timeout=2
|
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:
|
except redis.ConnectionError as e:
|
||||||
logger.error(f"Redis connection error: {e}")
|
logger.error(f"Redis connection error: {e}")
|
||||||
_redis_instance = None
|
_redis_instance = None # Keep trying for connection errors
|
||||||
return _redis_instance
|
return _redis_instance
|
||||||
|
|
||||||
|
|
||||||
@@ -41,36 +45,48 @@ def gen_cache_key(messages, model="docgpt", tools=None):
|
|||||||
|
|
||||||
def gen_cache(func):
|
def gen_cache(func):
|
||||||
def wrapper(self, model, messages, stream, tools=None, *args, **kwargs):
|
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:
|
try:
|
||||||
cache_key = gen_cache_key(messages, model, tools)
|
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:
|
except ValueError as e:
|
||||||
logger.error(e)
|
logger.error(f"Cache key generation failed: {e}")
|
||||||
return "Error: No user message found in the conversation to generate a cache key."
|
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
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def stream_cache(func):
|
def stream_cache(func):
|
||||||
def wrapper(self, model, messages, stream, tools=None, *args, **kwargs):
|
def wrapper(self, model, messages, stream, tools=None, *args, **kwargs):
|
||||||
cache_key = gen_cache_key(messages, model, tools)
|
if tools is not None:
|
||||||
logger.info(f"Stream cache key: {cache_key}")
|
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()
|
redis_client = get_redis_instance()
|
||||||
if redis_client:
|
if redis_client:
|
||||||
@@ -81,23 +97,21 @@ def stream_cache(func):
|
|||||||
cached_response = json.loads(cached_response.decode("utf-8"))
|
cached_response = json.loads(cached_response.decode("utf-8"))
|
||||||
for chunk in cached_response:
|
for chunk in cached_response:
|
||||||
yield chunk
|
yield chunk
|
||||||
time.sleep(0.03)
|
time.sleep(0.03) # Simulate streaming delay
|
||||||
return
|
return
|
||||||
except redis.ConnectionError as e:
|
except Exception as e:
|
||||||
logger.error(f"Redis connection error: {e}")
|
logger.error(f"Error getting cached stream: {e}")
|
||||||
|
|
||||||
result = func(self, model, messages, stream, tools=tools, *args, **kwargs)
|
|
||||||
stream_cache_data = []
|
stream_cache_data = []
|
||||||
|
for chunk in func(self, model, messages, stream, tools, *args, **kwargs):
|
||||||
for chunk in result:
|
|
||||||
stream_cache_data.append(chunk)
|
|
||||||
yield chunk
|
yield chunk
|
||||||
|
stream_cache_data.append(str(chunk))
|
||||||
|
|
||||||
if redis_client:
|
if redis_client:
|
||||||
try:
|
try:
|
||||||
redis_client.set(cache_key, json.dumps(stream_cache_data), ex=1800)
|
redis_client.set(cache_key, json.dumps(stream_cache_data), ex=1800)
|
||||||
logger.info(f"Stream cache saved for key: {cache_key}")
|
logger.info(f"Stream cache saved for key: {cache_key}")
|
||||||
except redis.ConnectionError as e:
|
except Exception as e:
|
||||||
logger.error(f"Redis connection error: {e}")
|
logger.error(f"Error setting stream cache: {e}")
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|||||||
@@ -1,34 +1,131 @@
|
|||||||
from application.llm.base import BaseLLM
|
|
||||||
import json
|
import json
|
||||||
import requests
|
|
||||||
|
from application.core.settings import settings
|
||||||
|
from application.llm.base import BaseLLM
|
||||||
|
|
||||||
|
|
||||||
class DocsGPTAPILLM(BaseLLM):
|
class DocsGPTAPILLM(BaseLLM):
|
||||||
|
|
||||||
def __init__(self, api_key=None, user_api_key=None, *args, **kwargs):
|
def __init__(self, api_key=None, user_api_key=None, *args, **kwargs):
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
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.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):
|
def _clean_messages_openai(self, messages):
|
||||||
response = requests.post(
|
cleaned_messages = []
|
||||||
f"{self.endpoint}/answer", json={"messages": messages, "max_new_tokens": 30}
|
for message in messages:
|
||||||
)
|
role = message.get("role")
|
||||||
response_clean = response.json()["a"].replace("###", "")
|
content = message.get("content")
|
||||||
|
|
||||||
return response_clean
|
if role == "model":
|
||||||
|
role = "assistant"
|
||||||
|
|
||||||
def _raw_gen_stream(self, baseself, model, messages, stream=True, *args, **kwargs):
|
if role and content is not None:
|
||||||
response = requests.post(
|
if isinstance(content, str):
|
||||||
f"{self.endpoint}/stream",
|
cleaned_messages.append({"role": role, "content": content})
|
||||||
json={"messages": messages, "max_new_tokens": 256},
|
elif isinstance(content, list):
|
||||||
stream=True,
|
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():
|
return cleaned_messages
|
||||||
if line:
|
|
||||||
data_str = line.decode("utf-8")
|
def _raw_gen(
|
||||||
if data_str.startswith("data: "):
|
self,
|
||||||
data = json.loads(data_str[6:])
|
baseself,
|
||||||
yield data["a"]
|
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
|
||||||
@@ -7,7 +7,7 @@ from application.llm.anthropic import AnthropicLLM
|
|||||||
from application.llm.docsgpt_provider import DocsGPTAPILLM
|
from application.llm.docsgpt_provider import DocsGPTAPILLM
|
||||||
from application.llm.premai import PremAILLM
|
from application.llm.premai import PremAILLM
|
||||||
from application.llm.google_ai import GoogleLLM
|
from application.llm.google_ai import GoogleLLM
|
||||||
|
from application.llm.novita import NovitaLLM
|
||||||
|
|
||||||
class LLMCreator:
|
class LLMCreator:
|
||||||
llms = {
|
llms = {
|
||||||
@@ -20,7 +20,8 @@ class LLMCreator:
|
|||||||
"docsgpt": DocsGPTAPILLM,
|
"docsgpt": DocsGPTAPILLM,
|
||||||
"premai": PremAILLM,
|
"premai": PremAILLM,
|
||||||
"groq": GroqLLM,
|
"groq": GroqLLM,
|
||||||
"google": GoogleLLM
|
"google": GoogleLLM,
|
||||||
|
"novita": NovitaLLM
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
32
application/llm/novita.py
Normal file
32
application/llm/novita.py
Normal file
@@ -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
|
||||||
@@ -125,9 +125,9 @@ class OpenAILLM(BaseLLM):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for line in response:
|
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
|
yield line.choices[0].delta.content
|
||||||
else:
|
elif len(line.choices) > 0:
|
||||||
yield line.choices[0]
|
yield line.choices[0]
|
||||||
|
|
||||||
def _supports_tools(self):
|
def _supports_tools(self):
|
||||||
@@ -137,17 +137,17 @@ class OpenAILLM(BaseLLM):
|
|||||||
class AzureOpenAILLM(OpenAILLM):
|
class AzureOpenAILLM(OpenAILLM):
|
||||||
|
|
||||||
def __init__(
|
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_base = (settings.OPENAI_API_BASE,)
|
||||||
self.api_version = (settings.OPENAI_API_VERSION,)
|
self.api_version = (settings.OPENAI_API_VERSION,)
|
||||||
self.deployment_name = (settings.AZURE_DEPLOYMENT_NAME,)
|
self.deployment_name = (settings.AZURE_DEPLOYMENT_NAME,)
|
||||||
from openai import AzureOpenAI
|
from openai import AzureOpenAI
|
||||||
|
|
||||||
self.client = AzureOpenAI(
|
self.client = AzureOpenAI(
|
||||||
api_key=openai_api_key,
|
api_key=api_key,
|
||||||
api_version=settings.OPENAI_API_VERSION,
|
api_version=settings.OPENAI_API_VERSION,
|
||||||
api_base=settings.OPENAI_API_BASE,
|
azure_endpoint=settings.OPENAI_API_BASE
|
||||||
deployment_name=settings.AZURE_DEPLOYMENT_NAME,
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ beautifulsoup4==4.12.3
|
|||||||
celery==5.4.0
|
celery==5.4.0
|
||||||
dataclasses-json==0.6.7
|
dataclasses-json==0.6.7
|
||||||
docx2txt==0.8
|
docx2txt==0.8
|
||||||
duckduckgo-search==7.4.2
|
duckduckgo-search==7.5.2
|
||||||
ebooklib==0.18
|
ebooklib==0.18
|
||||||
elastic-transport==8.17.0
|
elastic-transport==8.17.0
|
||||||
elasticsearch==8.17.1
|
elasticsearch==8.17.1
|
||||||
@@ -30,12 +30,12 @@ jsonschema==4.23.0
|
|||||||
jsonschema-spec==0.2.4
|
jsonschema-spec==0.2.4
|
||||||
jsonschema-specifications==2023.7.1
|
jsonschema-specifications==2023.7.1
|
||||||
kombu==5.4.2
|
kombu==5.4.2
|
||||||
langchain==0.3.14
|
langchain==0.3.20
|
||||||
langchain-community==0.3.14
|
langchain-community==0.3.19
|
||||||
langchain-core==0.3.40
|
langchain-core==0.3.45
|
||||||
langchain-openai==0.3.0
|
langchain-openai==0.3.8
|
||||||
langchain-text-splitters==0.3.5
|
langchain-text-splitters==0.3.6
|
||||||
langsmith==0.2.10
|
langsmith==0.3.15
|
||||||
lazy-object-proxy==1.10.0
|
lazy-object-proxy==1.10.0
|
||||||
lxml==5.3.1
|
lxml==5.3.1
|
||||||
markupsafe==3.0.2
|
markupsafe==3.0.2
|
||||||
@@ -45,7 +45,7 @@ multidict==6.1.0
|
|||||||
mypy-extensions==1.0.0
|
mypy-extensions==1.0.0
|
||||||
networkx==3.4.2
|
networkx==3.4.2
|
||||||
numpy==2.2.1
|
numpy==2.2.1
|
||||||
openai==1.59.5
|
openai==1.66.3
|
||||||
openapi-schema-validator==0.6.3
|
openapi-schema-validator==0.6.3
|
||||||
openapi-spec-validator==0.6.0
|
openapi-spec-validator==0.6.0
|
||||||
openapi3-parser==1.1.19
|
openapi3-parser==1.1.19
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Fragment } from 'react';
|
|
||||||
import DocsGPT3 from './assets/cute_docsgpt3.svg';
|
import DocsGPT3 from './assets/cute_docsgpt3.svg';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export default function Hero({
|
export default function Hero({
|
||||||
handleQuestion,
|
handleQuestion,
|
||||||
}: {
|
}: {
|
||||||
@@ -17,38 +17,41 @@ export default function Hero({
|
|||||||
header: string;
|
header: string;
|
||||||
query: string;
|
query: string;
|
||||||
}>;
|
}>;
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`pt-20 sm:pt-0 pb-6 sm:pb-12 flex h-full w-full flex-col text-black-1000 dark:text-bright-gray sm:w-full px-2 sm:px-0`}
|
|
||||||
>
|
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="p-0 text-4xl font-semibold">DocsGPT</span>
|
|
||||||
<img className="mb-1 inline w-14 p-0" src={DocsGPT3} alt="docsgpt" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-4 flex flex-col items-center justify-center dark:text-white"></div>
|
return (
|
||||||
|
<div className="flex h-full w-full flex-col text-black-1000 dark:text-bright-gray items-center justify-between">
|
||||||
|
{/* Header Section */}
|
||||||
|
<div className="flex flex-col items-center justify-center flex-grow pt-8 md:pt-0">
|
||||||
|
<div className="flex items-center mb-4">
|
||||||
|
<span className="text-4xl font-semibold">DocsGPT</span>
|
||||||
|
<img className="mb-1 inline w-14" src={DocsGPT3} alt="docsgpt" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-16 grid w-full grid-cols-1 items-center gap-4 self-center text-xs sm:w-auto sm:gap-6 md:mb-0 md:text-sm lg:grid-cols-2">
|
|
||||||
{demos?.map(
|
{/* Demo Buttons Section */}
|
||||||
(demo: { header: string; query: string }, key: number) =>
|
<div className="w-full max-w-full mb-8 md:mb-16">
|
||||||
demo.header &&
|
<div className="grid grid-cols-1 md:grid-cols-1 lg:grid-cols-2 gap-3 md:gap-4 text-xs">
|
||||||
demo.query && (
|
{demos?.map(
|
||||||
<Fragment key={key}>
|
(demo: { header: string; query: string }, key: number) =>
|
||||||
|
demo.header &&
|
||||||
|
demo.query && (
|
||||||
<button
|
<button
|
||||||
|
key={key}
|
||||||
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] bg-white dark:bg-raisin-black focus:outline-none"
|
className="w-full rounded-[66px] border bg-transparent px-6 py-[14px] text-left transition-colors
|
||||||
|
border-dark-gray text-just-black hover:bg-cultured
|
||||||
|
dark:border-dim-gray dark:text-chinese-white dark:hover:bg-charleston-green"
|
||||||
>
|
>
|
||||||
<p className="mb-1 font-semibold text-black-1000 dark:text-bright-gray">
|
<p className="mb-2 font-semibold text-black-1000 dark:text-bright-gray">
|
||||||
{demo.header}
|
{demo.header}
|
||||||
</p>
|
</p>
|
||||||
<span className="text-gray-700 dark:text-gray-300">
|
<span className="text-gray-700 dark:text-gray-300 opacity-60 line-clamp-2">
|
||||||
{demo.query}
|
{demo.query}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</Fragment>
|
),
|
||||||
),
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
ref={navRef}
|
ref={navRef}
|
||||||
className={`${
|
className={`${
|
||||||
!navOpen && '-ml-96 md:-ml-[18rem]'
|
!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`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={'visible mt-2 flex h-[6vh] w-full justify-between md:h-12'}
|
className={'visible mt-2 flex h-[6vh] w-full justify-between md:h-12'}
|
||||||
@@ -290,8 +290,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
}}
|
}}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
`${
|
`${
|
||||||
isActive ? 'bg-gray-3000 dark: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 hover:bg-gray-3000 dark:border-purple-taupe dark:text-white dark:hover: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`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -346,7 +346,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex h-auto flex-col justify-end text-eerie-black dark:text-white">
|
<div className="flex h-auto flex-col justify-end text-eerie-black dark:text-white">
|
||||||
<div className="flex flex-col-reverse border-b-[1px] dark:border-b-purple-taupe">
|
<div className="flex flex-col-reverse border-b-[1px] dark:border-b-purple-taupe">
|
||||||
<div className="relative my-4 mx-4 flex gap-2">
|
<div className="relative my-4 mx-4 flex gap-4 items-center">
|
||||||
<SourceDropdown
|
<SourceDropdown
|
||||||
options={docs}
|
options={docs}
|
||||||
selectedDocs={selectedDocs}
|
selectedDocs={selectedDocs}
|
||||||
@@ -361,8 +361,10 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
className="mt-2 h-9 w-9 hover:cursor-pointer"
|
className="hover:cursor-pointer"
|
||||||
src={UploadIcon}
|
src={UploadIcon}
|
||||||
|
width={28}
|
||||||
|
height={25}
|
||||||
alt="Upload document"
|
alt="Upload document"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setUploadModalState('ACTIVE');
|
setUploadModalState('ACTIVE');
|
||||||
@@ -392,7 +394,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
<img
|
<img
|
||||||
src={SettingGear}
|
src={SettingGear}
|
||||||
alt="Settings"
|
alt="Settings"
|
||||||
className="ml-2 w-5 filter dark:invert"
|
className="ml-2 w- 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">
|
||||||
{t('settings.label')}
|
{t('settings.label')}
|
||||||
|
|||||||
11
frontend/src/assets/eye-view.svg
Normal file
11
frontend/src/assets/eye-view.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_5819_12451)">
|
||||||
|
<path d="M8.40039 14.3C5.06706 14.3 2.53372 12.4334 0.533724 8.63338L0.400391 8.30005L0.533724 7.96672C2.53372 4.16672 5.06706 2.30005 8.40039 2.30005C11.7337 2.30005 14.2671 4.16672 16.2671 7.96672L16.4004 8.30005L16.2671 8.63338C14.2671 12.4334 11.7337 14.3 8.40039 14.3ZM1.93372 8.30005C3.60039 11.4334 5.73372 12.9667 8.40039 12.9667C11.0671 12.9667 13.2004 11.4334 14.8671 8.30005C13.2004 5.16672 11.0671 3.63338 8.40039 3.63338C5.73372 3.63338 3.60039 5.16672 1.93372 8.30005Z" fill="#747474"/>
|
||||||
|
<path d="M8.40072 11.6333C6.53405 11.6333 5.06738 10.1667 5.06738 8.30001C5.06738 6.43334 6.53405 4.96667 8.40072 4.96667C10.2674 4.96667 11.734 6.43334 11.734 8.30001C11.734 10.1667 10.2674 11.6333 8.40072 11.6333ZM8.40072 6.30001C7.26738 6.30001 6.40072 7.16667 6.40072 8.30001C6.40072 9.43334 7.26738 10.3 8.40072 10.3C9.53405 10.3 10.4007 9.43334 10.4007 8.30001C10.4007 7.16667 9.53405 6.30001 8.40072 6.30001Z" fill="#747474"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_5819_12451">
|
||||||
|
<rect width="16" height="16" fill="white" transform="translate(0.400391 0.300049)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#ffffff"><path d="M.01 0h24v24h-24V0z" fill="none"/><path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#747474"><path d="M.01 0h24v24h-24V0z" fill="none"/><path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/></svg>
|
||||||
|
Before Width: | Height: | Size: 386 B After Width: | Height: | Size: 386 B |
Binary file not shown.
|
Before Width: | Height: | Size: 735 B |
27
frontend/src/assets/user.svg
Normal file
27
frontend/src/assets/user.svg
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 855.6 867.3" style="enable-background:new 0 0 855.6 867.3;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:url(#SVGID_1_);}
|
||||||
|
</style>
|
||||||
|
<g id="Layer_2_00000062898455470481303740000016245162613792141746_">
|
||||||
|
<g id="avatar">
|
||||||
|
|
||||||
|
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="281.0639" y1="214.2932" x2="737.1139" y2="670.3433" gradientTransform="matrix(1 0 0 -1 0 874.2527)">
|
||||||
|
<stop offset="0" style="stop-color:#526676"/>
|
||||||
|
<stop offset="1" style="stop-color:#6B9DBF"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path class="st0" d="M725.1,663.3c-0.8,11.7-8,19.2-17.7,25c-25,14.9-51.5,26.3-79.2,35.1c-70.2,22.4-142.4,30.9-215.8,29.8
|
||||||
|
c-62.5-0.9-123.7-9.8-183.4-28.1c-32-9.8-62.8-22.3-91.7-39.3c-8.9-5.3-13.8-12.1-15-22.5c-2.5-21.2-2.4-42.6,0.3-63.8
|
||||||
|
c3.1-24,18-40.7,35.5-55.3c19.7-16.5,43.2-26,67.6-33c28.6-8.3,57.4-15.8,86-23.8c9.8,25.7,22.8,47,39,63.5
|
||||||
|
c22.4,22.8,50.2,35.3,78.4,35.3c41.9,0,80.6-26.8,103.4-71.6c4.2-8.3,8-16.8,11.3-25.5c7.4,2.5,14.9,4.6,22.5,6.5
|
||||||
|
c16.2,4.1,32.5,7.3,48.6,11.6c26.3,7.1,51.6,16.5,73.8,32.9c24.1,17.8,37.6,41.5,38.6,71.3C727.8,628.7,726.2,646,725.1,663.3z
|
||||||
|
M317.7,351c2.7,2.5,4.7,5.7,5.8,9.3c3,10.3,4.8,21.1,8.2,31.3c8.2,24.8,20.8,47.4,41,64.4c18.7,15.8,40.5,23.3,61.9,23.3
|
||||||
|
c31.7,0,62.5-16.5,81.5-47.2c13.6-22.1,21.8-45.9,26.2-71.3c0.6-3.4,1.7-8,4.1-9.4c21.5-13,25.1-34,25.7-56.2
|
||||||
|
c0.2-8.3-1.7-9.6-8-10.3c0-17,0.6-33.8-0.2-50.6c-0.4-10.4-2.1-20.6-5-30.6c-8.3-27.4-25.6-46.7-53.6-55.1
|
||||||
|
c-4.3-1.3-10.5,0.2-12.9-3.9c-11-19.4-31.6-25.6-54.4-25.6c-19.3,0-40.2,4.4-58.6,9.2c-21.1,5.4-40.2,14.6-54.8,31.5
|
||||||
|
c-20.4,23.8-26.2,52.1-24.7,82.4c0.7,14.6,2.9,29.1,4.4,44.1c-10.9,0.9-11.2,4.8-11,13.6C293.9,321.5,301.4,336.5,317.7,351z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
132
frontend/src/components/ContextMenu.tsx
Normal file
132
frontend/src/components/ContextMenu.tsx
Normal file
@@ -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<HTMLElement>;
|
||||||
|
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<HTMLDivElement>(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 (
|
||||||
|
<div
|
||||||
|
ref={menuRef}
|
||||||
|
className={`fixed z-50 ${className}`}
|
||||||
|
style={{ ...getMenuPosition() }}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex w-32 flex-col rounded-xl text-sm shadow-xl md:w-36 dark:bg-charleston-green-2 bg-lotion"
|
||||||
|
style={{ minWidth: '144px' }}
|
||||||
|
>
|
||||||
|
{options.map((option, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
option.onClick(event);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
className={`
|
||||||
|
flex justify-start items-center gap-4 p-3
|
||||||
|
transition-colors duration-200 ease-in-out
|
||||||
|
${index === 0 ? 'rounded-t-xl' : ''}
|
||||||
|
${index === options.length - 1 ? 'rounded-b-xl' : ''}
|
||||||
|
${
|
||||||
|
option.variant === 'danger'
|
||||||
|
? 'dark:text-red-2000 dark:hover:bg-charcoal-grey text-rosso-corsa hover:bg-bright-gray'
|
||||||
|
: 'dark:text-bright-gray dark:hover:bg-charcoal-grey text-eerie-black hover:bg-bright-gray'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{option.icon && (
|
||||||
|
<img
|
||||||
|
width={option.iconWidth || 16}
|
||||||
|
height={option.iconHeight || 16}
|
||||||
|
src={option.icon}
|
||||||
|
alt={option.label}
|
||||||
|
className={`cursor-pointer hover:opacity-75 ${option.iconClassName || ''}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span>{option.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
import copy from 'copy-to-clipboard';
|
import copy from 'copy-to-clipboard';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import CheckMark from '../assets/checkmark.svg?react';
|
import CheckMark from '../assets/checkmark.svg?react';
|
||||||
import Copy from '../assets/copy.svg?react';
|
import Copy from '../assets/copy.svg?react';
|
||||||
|
|
||||||
export default function CoppyButton({
|
export default function CopyButton({
|
||||||
text,
|
text,
|
||||||
colorLight,
|
colorLight,
|
||||||
colorDark,
|
colorDark,
|
||||||
|
showText = false,
|
||||||
}: {
|
}: {
|
||||||
text: string;
|
text: string;
|
||||||
colorLight?: string;
|
colorLight?: string;
|
||||||
colorDark?: string;
|
colorDark?: string;
|
||||||
|
showText?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [isCopyHovered, setIsCopyHovered] = useState(false);
|
const [isCopyHovered, setIsCopyHovered] = useState(false);
|
||||||
|
|
||||||
@@ -25,29 +29,30 @@ export default function CoppyButton({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<button
|
||||||
className={`flex items-center justify-center rounded-full p-2 ${
|
onClick={() => handleCopyClick(text)}
|
||||||
isCopyHovered
|
onMouseEnter={() => setIsCopyHovered(true)}
|
||||||
? `bg-[#EEEEEE] dark:bg-purple-taupe`
|
onMouseLeave={() => setIsCopyHovered(false)}
|
||||||
: `bg-[${colorLight ? colorLight : '#FFFFFF'}] dark:bg-[${colorDark ? colorDark : 'transparent'}]`
|
className="flex items-center gap-2"
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{copied ? (
|
<div
|
||||||
<CheckMark
|
className={`flex items-center justify-center rounded-full p-2 ${
|
||||||
className="cursor-pointer stroke-green-2000"
|
isCopyHovered
|
||||||
onMouseEnter={() => setIsCopyHovered(true)}
|
? `bg-[#EEEEEE] dark:bg-purple-taupe`
|
||||||
onMouseLeave={() => setIsCopyHovered(false)}
|
: `bg-[${colorLight ? colorLight : '#FFFFFF'}] dark:bg-[${colorDark ? colorDark : 'transparent'}]`
|
||||||
/>
|
}`}
|
||||||
) : (
|
>
|
||||||
<Copy
|
{copied ? (
|
||||||
className="w-4 cursor-pointer fill-none"
|
<CheckMark className="cursor-pointer stroke-green-2000" />
|
||||||
onClick={() => {
|
) : (
|
||||||
handleCopyClick(text);
|
<Copy className="w-4 cursor-pointer fill-none" />
|
||||||
}}
|
)}
|
||||||
onMouseEnter={() => setIsCopyHovered(true)}
|
</div>
|
||||||
onMouseLeave={() => setIsCopyHovered(false)}
|
{showText && (
|
||||||
/>
|
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
{copied ? t('conversation.copied') : t('conversation.copy')}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ function Dropdown({
|
|||||||
rounded = 'xl',
|
rounded = 'xl',
|
||||||
border = 'border-2',
|
border = 'border-2',
|
||||||
borderColor = 'silver',
|
borderColor = 'silver',
|
||||||
|
darkBorderColor = 'dim-gray',
|
||||||
showEdit,
|
showEdit,
|
||||||
onEdit,
|
onEdit,
|
||||||
showDelete,
|
showDelete,
|
||||||
@@ -38,6 +39,7 @@ function Dropdown({
|
|||||||
rounded?: 'xl' | '3xl';
|
rounded?: 'xl' | '3xl';
|
||||||
border?: 'border' | 'border-2';
|
border?: 'border' | 'border-2';
|
||||||
borderColor?: string;
|
borderColor?: string;
|
||||||
|
darkBorderColor?: string;
|
||||||
showEdit?: boolean;
|
showEdit?: boolean;
|
||||||
onEdit?: (value: { name: string; id: string; type: string }) => void;
|
onEdit?: (value: { name: string; id: string; type: string }) => void;
|
||||||
showDelete?: boolean;
|
showDelete?: boolean;
|
||||||
@@ -77,7 +79,7 @@ function Dropdown({
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
className={`flex w-full cursor-pointer items-center justify-between ${border} border-${borderColor} bg-white px-5 py-3 dark:border-${borderColor}/40 dark:bg-transparent ${
|
className={`flex w-full cursor-pointer items-center justify-between ${border} border-${borderColor} bg-white px-5 py-3 dark:border-${darkBorderColor} dark:bg-transparent ${
|
||||||
isOpen ? `${borderTopRadius}` : `${borderRadius}`
|
isOpen ? `${borderTopRadius}` : `${borderRadius}`
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -114,7 +116,7 @@ function Dropdown({
|
|||||||
</button>
|
</button>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div
|
<div
|
||||||
className={`absolute left-0 right-0 z-20 -mt-1 max-h-40 overflow-y-auto rounded-b-xl ${border} border-${borderColor} bg-white shadow-lg dark:border-${borderColor}/40 dark:bg-dark-charcoal`}
|
className={`absolute left-0 right-0 z-20 -mt-1 max-h-40 overflow-y-auto rounded-b-xl ${border} border-${borderColor} bg-white shadow-lg dark:border-${darkBorderColor} dark:bg-dark-charcoal`}
|
||||||
>
|
>
|
||||||
{options.map((option: any, index) => (
|
{options.map((option: any, index) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
type DropdownMenuProps = {
|
type DropdownMenuProps = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -6,6 +7,12 @@ type DropdownMenuProps = {
|
|||||||
onSelect: (value: string) => void;
|
onSelect: (value: string) => void;
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
isOpen?: boolean;
|
||||||
|
onOpenChange?: (isOpen: boolean) => void;
|
||||||
|
anchorRef?: React.RefObject<HTMLElement>;
|
||||||
|
className?: string;
|
||||||
|
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
||||||
|
offset?: { x: number; y: number };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function DropdownMenu({
|
export default function DropdownMenu({
|
||||||
@@ -14,24 +21,33 @@ export default function DropdownMenu({
|
|||||||
onSelect,
|
onSelect,
|
||||||
defaultValue = 'none',
|
defaultValue = 'none',
|
||||||
icon,
|
icon,
|
||||||
|
isOpen: controlledIsOpen,
|
||||||
|
onOpenChange,
|
||||||
|
anchorRef,
|
||||||
|
className = '',
|
||||||
|
position = 'bottom-right',
|
||||||
|
offset = { x: 0, y: 8 },
|
||||||
}: DropdownMenuProps) {
|
}: DropdownMenuProps) {
|
||||||
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
||||||
const [isOpen, setIsOpen] = React.useState(false);
|
const [internalIsOpen, setInternalIsOpen] = React.useState(false);
|
||||||
const [selectedOption, setSelectedOption] = React.useState(
|
const [selectedOption, setSelectedOption] = React.useState(
|
||||||
options.find((option) => option.value === defaultValue) || options[0],
|
options.find((option) => option.value === defaultValue) || options[0],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleToggle = () => {
|
const isOpen =
|
||||||
setIsOpen(!isOpen);
|
controlledIsOpen !== undefined ? controlledIsOpen : internalIsOpen;
|
||||||
};
|
const setIsOpen = onOpenChange || setInternalIsOpen;
|
||||||
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (
|
if (
|
||||||
dropdownRef.current &&
|
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);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickOption = (optionId: number) => {
|
const handleClickOption = (optionId: number) => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setSelectedOption(options[optionId]);
|
setSelectedOption(options[optionId]);
|
||||||
@@ -39,26 +55,40 @@ export default function DropdownMenu({
|
|||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
if (isOpen) {
|
||||||
return () => {
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
document.removeEventListener('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 (
|
|
||||||
<div className="static inline-block text-left" ref={dropdownRef}>
|
// Use a portal to render the dropdown outside the table flow
|
||||||
<button
|
return ReactDOM.createPortal(
|
||||||
onClick={handleToggle}
|
<div
|
||||||
className="flex w-20 cursor-pointer flex-row gap-1 rounded-3xl border-purple-30/25 bg-purple-30 p-2 text-xs text-white hover:bg-[#6F3FD1] focus:outline-none"
|
ref={dropdownRef}
|
||||||
>
|
style={{ ...getMenuPosition() }}
|
||||||
{icon && <img src={icon} alt="OptionIcon" className="h-4 w-4" />}
|
onClick={(e) => e.stopPropagation()}
|
||||||
{selectedOption.value !== 'never' ? selectedOption.label : name}
|
>
|
||||||
</button>
|
|
||||||
<div
|
<div
|
||||||
className={`absolute z-50 right-0 mt-1 w-28 transform rounded-md bg-transparent shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-200 ease-in-out ${
|
className={`w-28 transform rounded-md bg-white dark:bg-dark-charcoal shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-200 ease-in-out ${className}`}
|
||||||
isOpen
|
|
||||||
? 'scale-100 opacity-100'
|
|
||||||
: 'pointer-events-none scale-95 opacity-0'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="menu"
|
role="menu"
|
||||||
@@ -83,6 +113,7 @@ export default function DropdownMenu({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>,
|
||||||
|
document.body,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { InputProps } from './types';
|
import { InputProps } from './types';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
const Input = ({
|
const Input = ({
|
||||||
id,
|
id,
|
||||||
@@ -7,13 +8,13 @@ const Input = ({
|
|||||||
value,
|
value,
|
||||||
isAutoFocused = false,
|
isAutoFocused = false,
|
||||||
placeholder,
|
placeholder,
|
||||||
label,
|
|
||||||
required = false,
|
required = false,
|
||||||
maxLength,
|
maxLength,
|
||||||
className,
|
className = '',
|
||||||
colorVariant = 'silver',
|
colorVariant = 'silver',
|
||||||
borderVariant = 'thick',
|
borderVariant = 'thick',
|
||||||
children,
|
children,
|
||||||
|
labelBgClassName = 'bg-white dark:bg-raisin-black',
|
||||||
onChange,
|
onChange,
|
||||||
onPaste,
|
onPaste,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
@@ -27,15 +28,27 @@ const Input = ({
|
|||||||
thin: 'border',
|
thin: 'border',
|
||||||
thick: 'border-2',
|
thick: 'border-2',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className={`relative ${className}`}>
|
||||||
<input
|
<input
|
||||||
className={`h-[42px] w-full rounded-full px-3 py-1 outline-none dark:bg-transparent dark:text-white ${className} ${colorStyles[colorVariant]} ${borderStyles[borderVariant]}`}
|
ref={inputRef}
|
||||||
|
className={`peer h-[42px] w-full rounded-full px-3 py-1
|
||||||
|
bg-transparent outline-none
|
||||||
|
text-jet dark:text-bright-gray
|
||||||
|
placeholder-transparent
|
||||||
|
${colorStyles[colorVariant]}
|
||||||
|
${borderStyles[borderVariant]}
|
||||||
|
[&:-webkit-autofill]:bg-transparent
|
||||||
|
[&:-webkit-autofill]:appearance-none
|
||||||
|
[&:-webkit-autofill_selected]:bg-transparent`}
|
||||||
type={type}
|
type={type}
|
||||||
id={id}
|
id={id}
|
||||||
name={name}
|
name={name}
|
||||||
autoFocus={isAutoFocused}
|
autoFocus={isAutoFocused}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder || ''}
|
||||||
maxLength={maxLength}
|
maxLength={maxLength}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@@ -45,17 +58,20 @@ const Input = ({
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</input>
|
</input>
|
||||||
{label && (
|
{placeholder && (
|
||||||
<div className="absolute -top-2 left-2">
|
<label
|
||||||
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-[#26272E] dark:text-silver flex items-center">
|
htmlFor={id}
|
||||||
{label}
|
className={`absolute left-3 -top-2.5 px-2 text-xs transition-all
|
||||||
{required && (
|
peer-placeholder-shown:top-2.5 peer-placeholder-shown:left-3 peer-placeholder-shown:text-base
|
||||||
<span className="text-[#D30000] dark:text-[#D42626] ml-0.5">
|
peer-placeholder-shown:text-gray-4000 peer-focus:-top-2.5 peer-focus:left-3 peer-focus:text-xs
|
||||||
*
|
peer-focus:text-gray-4000 dark:text-silver dark:peer-placeholder-shown:text-gray-400
|
||||||
</span>
|
cursor-none pointer-events-none ${labelBgClassName}`}
|
||||||
)}
|
>
|
||||||
</span>
|
{placeholder}
|
||||||
</div>
|
{required && (
|
||||||
|
<span className="text-[#D30000] dark:text-[#D42626] ml-0.5">*</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
94
frontend/src/components/MessageInput.tsx
Normal file
94
frontend/src/components/MessageInput.tsx
Normal file
@@ -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<HTMLTextAreaElement>) => void;
|
||||||
|
onSubmit: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MessageInput({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onSubmit,
|
||||||
|
loading,
|
||||||
|
}: MessageInputProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isDarkTheme] = useDarkTheme();
|
||||||
|
const inputRef = useRef<HTMLTextAreaElement>(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<HTMLTextAreaElement>) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
onSubmit();
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.value = '';
|
||||||
|
handleInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full mx-2 items-center rounded-[40px] border dark:border-grey border-dark-gray bg-lotion dark:bg-charleston-green-3">
|
||||||
|
<label htmlFor="message-input" className="sr-only">
|
||||||
|
{t('inputPlaceholder')}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="message-input"
|
||||||
|
ref={inputRef}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
tabIndex={1}
|
||||||
|
placeholder={t('inputPlaceholder')}
|
||||||
|
className="inputbox-style w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-full bg-lotion dark:bg-charleston-green-3 py-5 text-base leading-tight opacity-100 focus:outline-none dark:text-bright-gray dark:placeholder-bright-gray dark:placeholder-opacity-50 px-6"
|
||||||
|
onInput={handleInput}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
aria-label={t('inputPlaceholder')}
|
||||||
|
/>
|
||||||
|
{loading ? (
|
||||||
|
<img
|
||||||
|
src={isDarkTheme ? SpinnerDark : Spinner}
|
||||||
|
className="relative right-[38px] bottom-[24px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent"
|
||||||
|
alt={t('loading')}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="mx-1 cursor-pointer rounded-full p-3 text-center hover:bg-gray-3000 dark:hover:bg-dark-charcoal">
|
||||||
|
<button
|
||||||
|
onClick={onSubmit}
|
||||||
|
aria-label={t('send')}
|
||||||
|
className="flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="ml-[4px] h-6 w-6 text-white filter dark:invert-[0.45] invert-[0.35]"
|
||||||
|
src={isDarkTheme ? SendDark : Send}
|
||||||
|
alt={t('send')}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -77,8 +77,8 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
|
|||||||
onClick={() => setActiveTab(tab)}
|
onClick={() => setActiveTab(tab)}
|
||||||
className={`snap-start h-9 rounded-3xl px-4 font-bold transition-colors ${
|
className={`snap-start h-9 rounded-3xl px-4 font-bold transition-colors ${
|
||||||
activeTab === tab
|
activeTab === tab
|
||||||
? 'bg-neutral-200 text-neutral-900 dark:bg-dark-charcoal dark:text-white'
|
? 'bg-[#F4F4F5] text-neutral-900 dark:bg-dark-charcoal dark:text-white'
|
||||||
: 'text-neutral-700 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-white'
|
: 'text-neutral-700 hover:text-neutral-900 dark:text-neutral-300 dark:hover:text-white'
|
||||||
}`}
|
}`}
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-selected={activeTab === tab}
|
aria-selected={activeTab === tab}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import Trash from '../assets/trash.svg';
|
import Trash from '../assets/trash.svg';
|
||||||
import Arrow2 from '../assets/dropdown-arrow.svg';
|
import Arrow2 from '../assets/dropdown-arrow.svg';
|
||||||
import { Doc } from '../models/misc';
|
import { Doc, ActiveState } from '../models/misc';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
options: Doc[] | null;
|
options: Doc[] | null;
|
||||||
selectedDocs: Doc | null;
|
selectedDocs: Doc | null;
|
||||||
@@ -30,6 +32,10 @@ function SourceDropdown({
|
|||||||
import.meta.env.VITE_EMBEDDINGS_NAME ||
|
import.meta.env.VITE_EMBEDDINGS_NAME ||
|
||||||
'huggingface_sentence-transformers/all-mpnet-base-v2';
|
'huggingface_sentence-transformers/all-mpnet-base-v2';
|
||||||
|
|
||||||
|
const [deleteModalState, setDeleteModalState] =
|
||||||
|
useState<ActiveState>('INACTIVE');
|
||||||
|
const [documentToDelete, setDocumentToDelete] = useState<Doc | null>(null);
|
||||||
|
|
||||||
const handleEmptyDocumentSelect = () => {
|
const handleEmptyDocumentSelect = () => {
|
||||||
dispatch(setSelectedDocs(null));
|
dispatch(setSelectedDocs(null));
|
||||||
setIsDocsListOpen(false);
|
setIsDocsListOpen(false);
|
||||||
@@ -50,11 +56,30 @@ function SourceDropdown({
|
|||||||
document.removeEventListener('mousedown', handleClickOutside);
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const confirmDelete = (option: Doc) => {
|
||||||
|
setDocumentToDelete(option);
|
||||||
|
setDeleteModalState('ACTIVE');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmedDelete = () => {
|
||||||
|
if (documentToDelete) {
|
||||||
|
handleDeleteClick(documentToDelete);
|
||||||
|
setDeleteModalState('INACTIVE');
|
||||||
|
setDocumentToDelete(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelDelete = () => {
|
||||||
|
setDeleteModalState('INACTIVE');
|
||||||
|
setDocumentToDelete(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-5/6 rounded-3xl" ref={dropdownRef}>
|
<div className="relative w-5/6 rounded-3xl" ref={dropdownRef}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsDocsListOpen(!isDocsListOpen)}
|
onClick={() => setIsDocsListOpen(!isDocsListOpen)}
|
||||||
className={`flex w-full cursor-pointer items-center border border-silver bg-white p-[14px] dark:bg-transparent ${
|
className={`flex w-full cursor-pointer items-center border border-silver bg-white p-[11px] dark:bg-transparent ${
|
||||||
isDocsListOpen
|
isDocsListOpen
|
||||||
? 'rounded-t-3xl dark:border-silver/40'
|
? 'rounded-t-3xl dark:border-silver/40'
|
||||||
: 'rounded-3xl dark:border-purple-taupe'
|
: 'rounded-3xl dark:border-purple-taupe'
|
||||||
@@ -76,7 +101,7 @@ function SourceDropdown({
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
{isDocsListOpen && (
|
{isDocsListOpen && (
|
||||||
<div className="absolute left-0 right-0 z-50 -mt-1 max-h-28 overflow-y-auto rounded-b-xl border border-silver bg-white shadow-lg dark:border-silver/40 dark:bg-dark-charcoal">
|
<div className="absolute left-0 right-0 z-20 -mt-1 max-h-28 overflow-y-auto rounded-b-xl border border-silver bg-white shadow-lg dark:border-silver/40 dark:bg-dark-charcoal">
|
||||||
{options ? (
|
{options ? (
|
||||||
options.map((option: any, index: number) => {
|
options.map((option: any, index: number) => {
|
||||||
if (option.model === embeddingsName) {
|
if (option.model === embeddingsName) {
|
||||||
@@ -106,7 +131,7 @@ function SourceDropdown({
|
|||||||
id={`img-${index}`}
|
id={`img-${index}`}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
handleDeleteClick(option);
|
confirmDelete(option);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -132,6 +157,17 @@ function SourceDropdown({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<ConfirmationModal
|
||||||
|
message={t('settings.documents.deleteWarning', {
|
||||||
|
name: documentToDelete?.name,
|
||||||
|
})}
|
||||||
|
modalState={deleteModalState}
|
||||||
|
setModalState={setDeleteModalState}
|
||||||
|
handleSubmit={handleConfirmedDelete}
|
||||||
|
handleCancel={handleCancelDelete}
|
||||||
|
submitLabel={t('convTile.delete')}
|
||||||
|
variant="danger"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ type ToggleSwitchProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
activeColor?: string;
|
size?: 'small' | 'medium' | 'large';
|
||||||
inactiveColor?: string;
|
labelPosition?: 'left' | 'right';
|
||||||
id?: string;
|
id?: string;
|
||||||
|
ariaLabel?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ToggleSwitch: React.FC<ToggleSwitchProps> = ({
|
const ToggleSwitch: React.FC<ToggleSwitchProps> = ({
|
||||||
@@ -17,37 +18,65 @@ const ToggleSwitch: React.FC<ToggleSwitchProps> = ({
|
|||||||
className = '',
|
className = '',
|
||||||
label,
|
label,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
activeColor = 'bg-purple-30',
|
size = 'medium',
|
||||||
inactiveColor = 'bg-transparent',
|
labelPosition = 'left',
|
||||||
id,
|
id,
|
||||||
|
ariaLabel,
|
||||||
}) => {
|
}) => {
|
||||||
|
// Size configurations
|
||||||
|
const sizeConfig = {
|
||||||
|
small: {
|
||||||
|
box: 'h-5 w-9',
|
||||||
|
toggle: 'h-4 w-4 left-0.5 top-0.5',
|
||||||
|
translate: 'translate-x-full',
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
box: 'h-8 w-14',
|
||||||
|
toggle: 'h-6 w-6 left-1 top-1',
|
||||||
|
translate: 'translate-x-full',
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
box: 'h-10 w-16',
|
||||||
|
toggle: 'h-8 w-8 left-1 top-1',
|
||||||
|
translate: 'translate-x-full',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { box, toggle, translate } = sizeConfig[size];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={`cursor-pointer select-none justify-between flex flex-row items-center ${disabled ? 'opacity-50 cursor-not-allowed' : ''} ${className}`}
|
className={`cursor-pointer select-none flex flex-row items-center ${
|
||||||
htmlFor={id}
|
labelPosition === 'right' ? 'flex-row-reverse' : ''
|
||||||
|
} ${disabled ? 'opacity-50 cursor-not-allowed' : ''} ${className}`}
|
||||||
>
|
>
|
||||||
{label && (
|
{label && (
|
||||||
<span className="mr-2 text-eerie-black dark:text-white">{label}</span>
|
<span
|
||||||
|
className={`text-eerie-black dark:text-white ${
|
||||||
|
labelPosition === 'left' ? 'mr-1' : 'ml-1'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
id={id}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onChange={(e) => onChange(e.target.checked)}
|
onChange={(e) => onChange(e.target.checked)}
|
||||||
className="sr-only"
|
className="sr-only"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
id={id}
|
aria-label={ariaLabel}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`box block h-8 w-14 rounded-full border border-purple-30 ${
|
className={`block ${box} rounded-full ${
|
||||||
checked
|
checked ? 'bg-north-texas-green' : 'bg-silver dark:bg-charcoal-grey'
|
||||||
? `${activeColor} dark:${activeColor}`
|
|
||||||
: `${inactiveColor} dark:${inactiveColor}`
|
|
||||||
}`}
|
}`}
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
className={`absolute left-1 top-1 flex h-6 w-6 items-center justify-center rounded-full transition ${
|
className={`absolute ${toggle} flex items-center justify-center rounded-full transition bg-white opacity-80 ${
|
||||||
checked ? 'translate-x-full bg-silver' : 'bg-purple-30'
|
checked ? `${translate} bg-silver` : ''
|
||||||
}`}
|
}`}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ export type InputProps = {
|
|||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
label?: string;
|
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
children?: React.ReactElement;
|
children?: React.ReactElement;
|
||||||
|
labelBgClassName?: string;
|
||||||
onChange: (
|
onChange: (
|
||||||
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>,
|
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>,
|
||||||
) => void;
|
) => void;
|
||||||
|
|||||||
@@ -1,26 +1,17 @@
|
|||||||
import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import Hero from '../Hero';
|
|
||||||
import { useDropzone } from 'react-dropzone';
|
import { useDropzone } from 'react-dropzone';
|
||||||
import DragFileUpload from '../assets/DragFileUpload.svg';
|
import DragFileUpload from '../assets/DragFileUpload.svg';
|
||||||
import ArrowDown from '../assets/arrow-down.svg';
|
|
||||||
import newChatIcon from '../assets/openNewChat.svg';
|
import newChatIcon from '../assets/openNewChat.svg';
|
||||||
import Send from '../assets/send.svg';
|
|
||||||
import SendDark from '../assets/send_dark.svg';
|
|
||||||
import ShareIcon from '../assets/share.svg';
|
import ShareIcon from '../assets/share.svg';
|
||||||
import SpinnerDark from '../assets/spinner-dark.svg';
|
import { useMediaQuery } from '../hooks';
|
||||||
import Spinner from '../assets/spinner.svg';
|
|
||||||
import RetryIcon from '../components/RetryIcon';
|
|
||||||
import { useDarkTheme, useMediaQuery } from '../hooks';
|
|
||||||
import { ShareConversationModal } from '../modals/ShareConversationModal';
|
import { ShareConversationModal } from '../modals/ShareConversationModal';
|
||||||
import {
|
import {
|
||||||
selectConversationId,
|
selectConversationId,
|
||||||
selectToken,
|
selectToken,
|
||||||
} from '../preferences/preferenceSlice';
|
} from '../preferences/preferenceSlice';
|
||||||
import { AppDispatch } from '../store';
|
import { AppDispatch } from '../store';
|
||||||
import ConversationBubble from './ConversationBubble';
|
|
||||||
import { handleSendFeedback } from './conversationHandlers';
|
import { handleSendFeedback } from './conversationHandlers';
|
||||||
import { FEEDBACK, Query } from './conversationModels';
|
import { FEEDBACK, Query } from './conversationModels';
|
||||||
import {
|
import {
|
||||||
@@ -35,20 +26,17 @@ import {
|
|||||||
} from './conversationSlice';
|
} from './conversationSlice';
|
||||||
import Upload from '../upload/Upload';
|
import Upload from '../upload/Upload';
|
||||||
import { ActiveState } from '../models/misc';
|
import { ActiveState } from '../models/misc';
|
||||||
|
import ConversationMessages from './ConversationMessages';
|
||||||
|
import MessageInput from '../components/MessageInput';
|
||||||
|
|
||||||
export default function Conversation() {
|
export default function Conversation() {
|
||||||
const token = useSelector(selectToken);
|
const token = useSelector(selectToken);
|
||||||
const queries = useSelector(selectQueries);
|
const queries = useSelector(selectQueries);
|
||||||
const navigate = useNavigate();
|
|
||||||
const status = useSelector(selectStatus);
|
const status = useSelector(selectStatus);
|
||||||
const conversationId = useSelector(selectConversationId);
|
const conversationId = useSelector(selectConversationId);
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
const conversationRef = useRef<HTMLDivElement>(null);
|
const [input, setInput] = useState('');
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
const [isDarkTheme] = useDarkTheme();
|
|
||||||
const [hasScrolledToLast, setHasScrolledToLast] = useState(true);
|
|
||||||
const fetchStream = useRef<any>(null);
|
const fetchStream = useRef<any>(null);
|
||||||
const [eventInterrupt, setEventInterrupt] = useState(false);
|
|
||||||
const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false);
|
const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false);
|
||||||
const [isShareModalOpen, setShareModalState] = useState<boolean>(false);
|
const [isShareModalOpen, setShareModalState] = useState<boolean>(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -95,23 +83,6 @@ export default function Conversation() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleUserInterruption = () => {
|
|
||||||
if (!eventInterrupt && status === 'loading') setEventInterrupt(true);
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
!eventInterrupt && scrollIntoView();
|
|
||||||
if (queries.length == 0) {
|
|
||||||
resetConversation();
|
|
||||||
}
|
|
||||||
}, [queries.length, queries[queries.length - 1]]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const element = document.getElementById('inputbox') as HTMLTextAreaElement;
|
|
||||||
if (element) {
|
|
||||||
element.focus();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (queries.length) {
|
if (queries.length) {
|
||||||
queries[queries.length - 1].error && setLastQueryReturnedErr(true);
|
queries[queries.length - 1].error && setLastQueryReturnedErr(true);
|
||||||
@@ -119,19 +90,6 @@ export default function Conversation() {
|
|||||||
}
|
}
|
||||||
}, [queries[queries.length - 1]]);
|
}, [queries[queries.length - 1]]);
|
||||||
|
|
||||||
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 handleQuestion = ({
|
const handleQuestion = ({
|
||||||
question,
|
question,
|
||||||
isRetry = false,
|
isRetry = false,
|
||||||
@@ -150,7 +108,6 @@ export default function Conversation() {
|
|||||||
} else {
|
} else {
|
||||||
question = question.trim();
|
question = question.trim();
|
||||||
if (question === '') return;
|
if (question === '') return;
|
||||||
setEventInterrupt(false);
|
|
||||||
!isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries
|
!isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries
|
||||||
fetchStream.current = dispatch(fetchAnswer({ question }));
|
fetchStream.current = dispatch(fetchAnswer({ question }));
|
||||||
}
|
}
|
||||||
@@ -187,14 +144,14 @@ export default function Conversation() {
|
|||||||
) => {
|
) => {
|
||||||
if (updated === true) {
|
if (updated === true) {
|
||||||
handleQuestion({ question: updatedQuestion as string, updated, indx });
|
handleQuestion({ question: updatedQuestion as string, updated, indx });
|
||||||
} else if (inputRef.current?.value && status !== 'loading') {
|
} else if (input && status !== 'loading') {
|
||||||
if (lastQueryReturnedErr) {
|
if (lastQueryReturnedErr) {
|
||||||
// update last failed query with new prompt
|
// update last failed query with new prompt
|
||||||
dispatch(
|
dispatch(
|
||||||
updateQuery({
|
updateQuery({
|
||||||
index: queries.length - 1,
|
index: queries.length - 1,
|
||||||
query: {
|
query: {
|
||||||
prompt: inputRef.current.value,
|
prompt: input,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -203,10 +160,9 @@ export default function Conversation() {
|
|||||||
isRetry: true,
|
isRetry: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
handleQuestion({ question: inputRef.current.value });
|
handleQuestion({ question: input });
|
||||||
}
|
}
|
||||||
inputRef.current.value = '';
|
setInput('');
|
||||||
handleInput();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const resetConversation = () => {
|
const resetConversation = () => {
|
||||||
@@ -221,98 +177,21 @@ export default function Conversation() {
|
|||||||
if (queries && queries.length > 0) resetConversation();
|
if (queries && queries.length > 0) resetConversation();
|
||||||
};
|
};
|
||||||
|
|
||||||
const prepResponseView = (query: Query, index: number) => {
|
|
||||||
let responseView;
|
|
||||||
if (query.response) {
|
|
||||||
responseView = (
|
|
||||||
<ConversationBubble
|
|
||||||
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'}`}
|
|
||||||
key={`${index}ANSWER`}
|
|
||||||
message={query.response}
|
|
||||||
type={'ANSWER'}
|
|
||||||
sources={query.sources}
|
|
||||||
toolCalls={query.tool_calls}
|
|
||||||
feedback={query.feedback}
|
|
||||||
handleFeedback={(feedback: FEEDBACK) =>
|
|
||||||
handleFeedback(query, feedback, index)
|
|
||||||
}
|
|
||||||
></ConversationBubble>
|
|
||||||
);
|
|
||||||
} else if (query.error) {
|
|
||||||
const retryBtn = (
|
|
||||||
<button
|
|
||||||
className="flex items-center justify-center gap-3 self-center rounded-full py-3 px-5 text-lg text-gray-500 transition-colors delay-100 hover:border-gray-500 disabled:cursor-not-allowed dark:text-bright-gray"
|
|
||||||
disabled={status === 'loading'}
|
|
||||||
onClick={() => {
|
|
||||||
handleQuestion({
|
|
||||||
question: queries[queries.length - 1].prompt,
|
|
||||||
isRetry: true,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RetryIcon
|
|
||||||
width={isMobile ? 12 : 12} // change the width and height according to device size if necessary
|
|
||||||
height={isMobile ? 12 : 12}
|
|
||||||
fill={isDarkTheme ? 'rgb(236 236 241)' : 'rgb(107 114 120)'}
|
|
||||||
stroke={isDarkTheme ? 'rgb(236 236 241)' : 'rgb(107 114 120)'}
|
|
||||||
strokeWidth={10}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
responseView = (
|
|
||||||
<ConversationBubble
|
|
||||||
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'} `}
|
|
||||||
key={`${index}ERROR`}
|
|
||||||
message={query.error}
|
|
||||||
type="ERROR"
|
|
||||||
retryBtn={retryBtn}
|
|
||||||
></ConversationBubble>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return responseView;
|
|
||||||
};
|
|
||||||
|
|
||||||
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`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const checkScroll = () => {
|
|
||||||
const el = conversationRef.current;
|
|
||||||
if (!el) return;
|
|
||||||
const isBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 10;
|
|
||||||
setHasScrolledToLast(isBottom);
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
handleInput();
|
|
||||||
window.addEventListener('resize', handleInput);
|
|
||||||
conversationRef.current?.addEventListener('scroll', checkScroll);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', handleInput);
|
|
||||||
conversationRef.current?.removeEventListener('scroll', checkScroll);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1 h-full justify-end ">
|
<div className="flex flex-col gap-1 h-full justify-end">
|
||||||
{conversationId && queries.length > 0 && (
|
{conversationId && queries.length > 0 && (
|
||||||
<div className="absolute top-4 right-20 z-10 ">
|
<div className="absolute top-4 right-20">
|
||||||
{' '}
|
<div className="flex mt-2 items-center gap-4">
|
||||||
<div className="flex mt-2 items-center gap-4 ">
|
|
||||||
{isMobile && queries.length > 0 && (
|
{isMobile && queries.length > 0 && (
|
||||||
<button
|
<button
|
||||||
title="Open New Chat"
|
title="Open New Chat"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
newChat();
|
newChat();
|
||||||
}}
|
}}
|
||||||
className="hover:bg-bright-gray dark:hover:bg-[#28292E]"
|
className="hover:bg-bright-gray dark:hover:bg-[#28292E] rounded-full p-2"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className=" h-5 w-5 filter dark:invert "
|
className="h-5 w-5 filter dark:invert"
|
||||||
alt="NewChat"
|
alt="NewChat"
|
||||||
src={newChatIcon}
|
src={newChatIcon}
|
||||||
/>
|
/>
|
||||||
@@ -324,10 +203,10 @@ export default function Conversation() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShareModalState(true);
|
setShareModalState(true);
|
||||||
}}
|
}}
|
||||||
className=" hover:bg-bright-gray dark:hover:bg-[#28292E]"
|
className="hover:bg-bright-gray dark:hover:bg-[#28292E] rounded-full p-2"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className=" h-5 w-5 filter dark:invert"
|
className="h-5 w-5 filter dark:invert"
|
||||||
alt="share"
|
alt="share"
|
||||||
src={ShareIcon}
|
src={ShareIcon}
|
||||||
/>
|
/>
|
||||||
@@ -343,102 +222,33 @@ export default function Conversation() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
|
||||||
ref={conversationRef}
|
|
||||||
onWheel={handleUserInterruption}
|
|
||||||
onTouchMove={handleUserInterruption}
|
|
||||||
className="flex justify-center w-full overflow-y-auto h-screen sm:mt-12"
|
|
||||||
>
|
|
||||||
{queries.length > 0 && !hasScrolledToLast && (
|
|
||||||
<button
|
|
||||||
onClick={scrollIntoView}
|
|
||||||
aria-label="scroll to bottom"
|
|
||||||
className="fixed bottom-40 right-14 z-10 flex h-7 w-7 items-center justify-center rounded-full border-[0.5px] border-gray-alpha bg-gray-100 bg-opacity-50 dark:bg-purple-taupe md:h-9 md:w-9 md:bg-opacity-100 "
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={ArrowDown}
|
|
||||||
alt="arrow down"
|
|
||||||
className="h-4 w-4 opacity-50 md:h-5 md:w-5"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{queries.length > 0 ? (
|
<ConversationMessages
|
||||||
<div className="w-full md:w-8/12">
|
handleQuestion={handleQuestion}
|
||||||
{queries.map((query, index) => {
|
handleQuestionSubmission={handleQuestionSubmission}
|
||||||
return (
|
handleFeedback={handleFeedback}
|
||||||
<Fragment key={index}>
|
queries={queries}
|
||||||
<ConversationBubble
|
status={status}
|
||||||
className={'first:mt-5'}
|
/>
|
||||||
key={`${index}QUESTION`}
|
|
||||||
message={query.prompt}
|
|
||||||
type="QUESTION"
|
|
||||||
handleUpdatedQuestionSubmission={handleQuestionSubmission}
|
|
||||||
questionNumber={index}
|
|
||||||
sources={query.sources}
|
|
||||||
></ConversationBubble>
|
|
||||||
|
|
||||||
{prepResponseView(query, index)}
|
<div className="flex flex-col items-end self-center rounded-2xl bg-opacity-0 z-3 w-full md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12 max-w-[1300px] h-auto py-1">
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Hero handleQuestion={handleQuestion} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex w-11/12 flex-col items-end self-center rounded-2xl bg-opacity-0 z-3 sm:w-[62%] h-auto py-1">
|
|
||||||
<div
|
<div
|
||||||
{...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]"
|
||||||
>
|
>
|
||||||
<label htmlFor="file-upload" className="sr-only">
|
<label htmlFor="file-upload" className="sr-only">
|
||||||
{t('modals.uploadDoc.label')}
|
{t('modals.uploadDoc.label')}
|
||||||
</label>
|
</label>
|
||||||
<input {...getInputProps()} id="file-upload" />
|
<input {...getInputProps()} id="file-upload" />
|
||||||
<label htmlFor="message-input" className="sr-only">
|
<MessageInput
|
||||||
{t('inputPlaceholder')}
|
value={input}
|
||||||
</label>
|
onChange={(e) => setInput(e.target.value)}
|
||||||
<textarea
|
onSubmit={handleQuestionSubmission}
|
||||||
id="message-input"
|
loading={status === 'loading'}
|
||||||
ref={inputRef}
|
/>
|
||||||
tabIndex={1}
|
|
||||||
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 dark:placeholder-bright-gray dark:placeholder-opacity-50`}
|
|
||||||
onInput={handleInput}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleQuestionSubmission();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
aria-label={t('inputPlaceholder')}
|
|
||||||
></textarea>
|
|
||||||
{status === 'loading' ? (
|
|
||||||
<img
|
|
||||||
src={isDarkTheme ? SpinnerDark : Spinner}
|
|
||||||
className="relative right-[38px] bottom-[24px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent"
|
|
||||||
alt={t('loading')}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="mx-1 cursor-pointer rounded-full p-3 text-center hover:bg-gray-3000 dark:hover:bg-dark-charcoal">
|
|
||||||
<button
|
|
||||||
onClick={() => handleQuestionSubmission()}
|
|
||||||
aria-label={t('send')}
|
|
||||||
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>
|
||||||
|
|
||||||
<p className="text-gray-595959 hidden w-[100vw] self-center bg-transparent py-2 text-center text-xs dark:text-bright-gray md:inline md:w-full">
|
<p className="text-gray-4000 hidden w-[100vw] self-center bg-transparent py-2 text-center text-xs dark:text-sonic-silver md:inline md:w-full">
|
||||||
{t('tagline')}
|
{t('tagline')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
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 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 { useDarkTheme } from '../hooks';
|
||||||
|
|
||||||
import DocsGPT3 from '../assets/cute_docsgpt3.svg';
|
import DocsGPT3 from '../assets/cute_docsgpt3.svg';
|
||||||
import ChevronDown from '../assets/chevron-down.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 Like from '../assets/like.svg?react';
|
||||||
import Link from '../assets/link.svg';
|
import Link from '../assets/link.svg';
|
||||||
import Sources from '../assets/sources.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 Accordion from '../components/Accordion';
|
||||||
import Avatar from '../components/Avatar';
|
import Avatar from '../components/Avatar';
|
||||||
import CopyButton from '../components/CopyButton';
|
import CopyButton from '../components/CopyButton';
|
||||||
@@ -69,6 +73,7 @@ const ConversationBubble = forwardRef<
|
|||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [isDarkTheme] = useDarkTheme();
|
||||||
// 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,7 +118,7 @@ const ConversationBubble = forwardRef<
|
|||||||
style={{
|
style={{
|
||||||
wordBreak: 'break-word',
|
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}
|
{message}
|
||||||
</div>
|
</div>
|
||||||
@@ -122,7 +127,7 @@ const ConversationBubble = forwardRef<
|
|||||||
setIsEditClicked(true);
|
setIsEditClicked(true);
|
||||||
setEditInputBox(message);
|
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'}`}
|
||||||
>
|
>
|
||||||
<img src={Edit} alt="Edit" className="cursor-pointer" />
|
<img src={Edit} alt="Edit" className="cursor-pointer" />
|
||||||
</button>
|
</button>
|
||||||
@@ -138,29 +143,29 @@ const ConversationBubble = forwardRef<
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setEditInputBox(e.target.value);
|
setEditInputBox(e.target.value);
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if(e.key === 'Enter' && !e.shiftKey){
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleEditClick();
|
handleEditClick();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
rows={5}
|
rows={5}
|
||||||
value={editInputBox}
|
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"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
className="rounded-full bg-[#CDB5FF] hover:bg-[#E1D3FF] px-4 py-2 text-purple-30 text-sm font-medium"
|
className="px-4 py-2 text-purple-30 text-sm font-semibold hover:text-chinese-black-2 dark:hover:text-[#B9BCBE] hover:bg-gainsboro dark:hover:bg-onyx-2 transition-colors rounded-full"
|
||||||
onClick={handleEditClick}
|
|
||||||
>
|
|
||||||
{t('conversation.edit.update')}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="px-4 py-2 text-purple-30 text-sm hover:underline"
|
|
||||||
onClick={() => setIsEditClicked(false)}
|
onClick={() => setIsEditClicked(false)}
|
||||||
>
|
>
|
||||||
{t('conversation.edit.cancel')}
|
{t('conversation.edit.cancel')}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className="rounded-full bg-purple-30 hover:bg-violets-are-blue dark:hover:bg-royal-purple px-4 py-2 text-white text-sm font-medium transition-colors"
|
||||||
|
onClick={handleEditClick}
|
||||||
|
>
|
||||||
|
{t('conversation.edit.update')}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -341,7 +346,7 @@ const ConversationBubble = forwardRef<
|
|||||||
</p>
|
</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-[18px] px-7 dark:bg-gun-metal md:max-w-[70vw] lg:max-w-[50vw] ${
|
||||||
type === 'ERROR'
|
type === 'ERROR'
|
||||||
? 'relative flex-row items-center rounded-full border border-transparent bg-[#FFE7E7] p-2 py-5 text-sm font-normal text-red-3000 dark:border-red-2000 dark:text-white'
|
? 'relative flex-row items-center rounded-full border border-transparent bg-[#FFE7E7] p-2 py-5 text-sm font-normal text-red-3000 dark:border-red-2000 dark:text-white'
|
||||||
: 'flex-col rounded-3xl'
|
: 'flex-col rounded-3xl'
|
||||||
@@ -355,25 +360,32 @@ const ConversationBubble = forwardRef<
|
|||||||
code(props) {
|
code(props) {
|
||||||
const { children, className, node, ref, ...rest } = props;
|
const { children, className, node, ref, ...rest } = props;
|
||||||
const match = /language-(\w+)/.exec(className || '');
|
const match = /language-(\w+)/.exec(className || '');
|
||||||
|
const language = match ? match[1] : '';
|
||||||
|
|
||||||
return match ? (
|
return match ? (
|
||||||
<div className="group relative">
|
<div className="group relative rounded-[14px] overflow-hidden border border-light-silver dark:border-raisin-black">
|
||||||
<SyntaxHighlighter
|
<div className="flex justify-between items-center px-2 py-1 bg-platinum dark:bg-eerie-black-2">
|
||||||
{...rest}
|
<span className="text-xs font-medium text-just-black dark:text-chinese-white">
|
||||||
PreTag="div"
|
{language}
|
||||||
language={match[1]}
|
</span>
|
||||||
style={vscDarkPlus}
|
|
||||||
>
|
|
||||||
{String(children).replace(/\n$/, '')}
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
<div
|
|
||||||
className={`absolute right-3 top-3 lg:invisible
|
|
||||||
${type !== 'ERROR' ? 'group-hover:lg:visible' : ''} `}
|
|
||||||
>
|
|
||||||
<CopyButton
|
<CopyButton
|
||||||
text={String(children).replace(/\n$/, '')}
|
text={String(children).replace(/\n$/, '')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
{...rest}
|
||||||
|
PreTag="div"
|
||||||
|
language={language}
|
||||||
|
style={isDarkTheme ? vscDarkPlus : oneLight}
|
||||||
|
className="!mt-0"
|
||||||
|
customStyle={{
|
||||||
|
margin: 0,
|
||||||
|
borderRadius: 0,
|
||||||
|
scrollbarWidth: 'thin',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{String(children).replace(/\n$/, '')}
|
||||||
|
</SyntaxHighlighter>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<code className="whitespace-pre-line rounded-[6px] bg-gray-200 px-[8px] py-[4px] text-xs font-normal dark:bg-independence dark:text-bright-gray">
|
<code className="whitespace-pre-line rounded-[6px] bg-gray-200 px-[8px] py-[4px] text-xs font-normal dark:bg-independence dark:text-bright-gray">
|
||||||
|
|||||||
180
frontend/src/conversation/ConversationMessages.tsx
Normal file
180
frontend/src/conversation/ConversationMessages.tsx
Normal file
@@ -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<HTMLDivElement>(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 = (
|
||||||
|
<ConversationBubble
|
||||||
|
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'}`}
|
||||||
|
key={`${index}ANSWER`}
|
||||||
|
message={query.response}
|
||||||
|
type={'ANSWER'}
|
||||||
|
sources={query.sources}
|
||||||
|
toolCalls={query.tool_calls}
|
||||||
|
feedback={query.feedback}
|
||||||
|
handleFeedback={
|
||||||
|
handleFeedback
|
||||||
|
? (feedback) => handleFeedback(query, feedback, index)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (query.error) {
|
||||||
|
const retryBtn = (
|
||||||
|
<button
|
||||||
|
className="flex items-center justify-center gap-3 self-center rounded-full py-3 px-5 text-lg text-gray-500 transition-colors delay-100 hover:border-gray-500 disabled:cursor-not-allowed dark:text-bright-gray"
|
||||||
|
disabled={status === 'loading'}
|
||||||
|
onClick={() => {
|
||||||
|
handleQuestion({
|
||||||
|
question: queries[queries.length - 1].prompt,
|
||||||
|
isRetry: true,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RetryIcon
|
||||||
|
width={12}
|
||||||
|
height={12}
|
||||||
|
fill={isDarkTheme ? 'rgb(236 236 241)' : 'rgb(107 114 120)'}
|
||||||
|
stroke={isDarkTheme ? 'rgb(236 236 241)' : 'rgb(107 114 120)'}
|
||||||
|
strokeWidth={10}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
responseView = (
|
||||||
|
<ConversationBubble
|
||||||
|
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'} `}
|
||||||
|
key={`${index}ERROR`}
|
||||||
|
message={query.error}
|
||||||
|
type="ERROR"
|
||||||
|
retryBtn={retryBtn}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return responseView;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={conversationRef}
|
||||||
|
onWheel={handleUserInterruption}
|
||||||
|
onTouchMove={handleUserInterruption}
|
||||||
|
className="flex justify-center w-full overflow-y-auto h-screen sm:pt-12"
|
||||||
|
>
|
||||||
|
{queries.length > 0 && !hasScrolledToLast && (
|
||||||
|
<button
|
||||||
|
onClick={scrollIntoView}
|
||||||
|
aria-label="scroll to bottom"
|
||||||
|
className="fixed bottom-40 right-14 z-10 flex h-7 w-7 items-center justify-center rounded-full border-[0.5px] border-gray-alpha bg-gray-100 bg-opacity-50 dark:bg-gunmetal md:h-9 md:w-9 md:bg-opacity-100"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={ArrowDown}
|
||||||
|
alt="arrow down"
|
||||||
|
className="h-4 w-4 opacity-50 md:h-5 md:w-5 filter dark:invert"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="w-full md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12 max-w-[1300px] px-2">
|
||||||
|
{queries.length > 0 ? (
|
||||||
|
queries.map((query, index) => (
|
||||||
|
<Fragment key={index}>
|
||||||
|
<ConversationBubble
|
||||||
|
className={'first:mt-5'}
|
||||||
|
key={`${index}QUESTION`}
|
||||||
|
message={query.prompt}
|
||||||
|
type="QUESTION"
|
||||||
|
handleUpdatedQuestionSubmission={handleQuestionSubmission}
|
||||||
|
questionNumber={index}
|
||||||
|
sources={query.sources}
|
||||||
|
/>
|
||||||
|
{prepResponseView(query, index)}
|
||||||
|
</Fragment>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Hero handleQuestion={handleQuestion} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,6 +18,9 @@ import { selectConversationId } from '../preferences/preferenceSlice';
|
|||||||
import { ActiveState } from '../models/misc';
|
import { ActiveState } from '../models/misc';
|
||||||
import { ShareConversationModal } from '../modals/ShareConversationModal';
|
import { ShareConversationModal } from '../modals/ShareConversationModal';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import ContextMenu from '../components/ContextMenu';
|
||||||
|
import { MenuOption } from '../components/ContextMenu';
|
||||||
|
import { useOutsideAlerter } from '../hooks';
|
||||||
|
|
||||||
interface ConversationProps {
|
interface ConversationProps {
|
||||||
name: string;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@@ -136,16 +183,18 @@ export default function ConversationTile({
|
|||||||
setIsHovered(true);
|
setIsHovered(true);
|
||||||
}}
|
}}
|
||||||
onMouseLeave={() => {
|
onMouseLeave={() => {
|
||||||
setIsHovered(false);
|
if (!isEdit) {
|
||||||
|
setIsHovered(false);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onCoversationClick();
|
onCoversationClick();
|
||||||
conversationId !== conversation.id &&
|
conversationId !== conversation.id &&
|
||||||
selectConversation(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] ${
|
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
|
conversationId === conversation.id || isOpen || isHovered || isEdit
|
||||||
? 'bg-gray-100 dark:bg-[#28292E]'
|
? 'bg-bright-gray dark:bg-dark-charcoal'
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -160,13 +209,13 @@ export default function ConversationTile({
|
|||||||
onKeyDown={handleRenameKeyDown}
|
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-bright-gray">
|
||||||
{conversationName}
|
{conversationName}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{(conversationId === conversation.id || isHovered || isOpen) && (
|
{(conversationId === conversation.id || isHovered || isOpen) && (
|
||||||
<div className="flex text-white dark:text-[#949494]" ref={menuRef}>
|
<div className="flex text-white dark:text-sonic-silver" ref={menuRef}>
|
||||||
{isEdit ? (
|
{isEdit ? (
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<img
|
<img
|
||||||
@@ -204,64 +253,14 @@ export default function ConversationTile({
|
|||||||
<img src={threeDots} width={8} />
|
<img src={threeDots} width={8} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{isOpen && (
|
<ContextMenu
|
||||||
<div
|
isOpen={isOpen}
|
||||||
className="flex-start absolute z-30 flex w-32 translate-x-1 translate-y-5 flex-col rounded-xl bg-stone-100 text-sm text-black shadow-xl dark:bg-chinese-black dark:text-chinese-silver md:w-36"
|
setIsOpen={setOpen}
|
||||||
style={{
|
options={menuOptions}
|
||||||
top: `${(tileRef.current?.getBoundingClientRect().top ?? 0) + window.scrollY + 8}px`,
|
anchorRef={tileRef}
|
||||||
}}
|
position="bottom-right"
|
||||||
>
|
offset={{ x: 1, y: 8 }}
|
||||||
<button
|
/>
|
||||||
onClick={(event: SyntheticEvent) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
setShareModalState(true);
|
|
||||||
setOpen(false);
|
|
||||||
}}
|
|
||||||
className="flex-start flex items-center gap-4 rounded-t-xl p-3 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={Share}
|
|
||||||
alt="Share"
|
|
||||||
width={14}
|
|
||||||
height={14}
|
|
||||||
className="cursor-pointer hover:opacity-50"
|
|
||||||
id={`img-${conversation.id}`}
|
|
||||||
/>
|
|
||||||
<span>{t('convTile.share')}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleEditConversation}
|
|
||||||
className="flex-start flex items-center gap-4 p-3 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={Edit}
|
|
||||||
alt="Edit"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
className="cursor-pointer hover:opacity-50"
|
|
||||||
id={`img-${conversation.id}`}
|
|
||||||
/>
|
|
||||||
<span>{t('convTile.rename')}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={(event: SyntheticEvent) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
setDeleteModalState('ACTIVE');
|
|
||||||
setOpen(false);
|
|
||||||
}}
|
|
||||||
className="flex-start flex items-center gap-3 rounded-b-xl p-2 text-red-700 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={Trash}
|
|
||||||
alt="Delete"
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
className="cursor-pointer hover:opacity-50"
|
|
||||||
/>
|
|
||||||
<span>{t('convTile.delete')}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import { Fragment, useEffect, useRef, useState } from 'react';
|
import { Fragment, useEffect, useRef, useState } from 'react';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import ConversationMessages from './ConversationMessages';
|
||||||
|
import MessageInput from '../components/MessageInput';
|
||||||
import conversationService from '../api/services/conversationService';
|
import conversationService from '../api/services/conversationService';
|
||||||
|
|
||||||
import Send from '../assets/send.svg';
|
import Send from '../assets/send.svg';
|
||||||
import Spinner from '../assets/spinner.svg';
|
import Spinner from '../assets/spinner.svg';
|
||||||
import { selectToken } from '../preferences/preferenceSlice';
|
import { selectToken } from '../preferences/preferenceSlice';
|
||||||
import { AppDispatch } from '../store';
|
import { AppDispatch } from '../store';
|
||||||
import ConversationBubble from './ConversationBubble';
|
import ConversationBubble from './ConversationBubble';
|
||||||
import { Query } from './conversationModels';
|
import { Query } from './conversationModels';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addQuery,
|
addQuery,
|
||||||
fetchSharedAnswer,
|
fetchSharedAnswer,
|
||||||
@@ -24,6 +28,7 @@ import {
|
|||||||
setIdentifier,
|
setIdentifier,
|
||||||
updateQuery,
|
updateQuery,
|
||||||
} from './sharedConversationSlice';
|
} from './sharedConversationSlice';
|
||||||
|
import { formatDate } from '../utils/dateTimeUtils';
|
||||||
|
|
||||||
export const SharedConversation = () => {
|
export const SharedConversation = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -36,27 +41,14 @@ export const SharedConversation = () => {
|
|||||||
const apiKey = useSelector(selectClientAPIKey);
|
const apiKey = useSelector(selectClientAPIKey);
|
||||||
const status = useSelector(selectStatus);
|
const status = useSelector(selectStatus);
|
||||||
|
|
||||||
const inputRef = useRef<HTMLDivElement>(null);
|
const [input, setInput] = useState('');
|
||||||
const sharedConversationRef = useRef<HTMLDivElement>(null);
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
|
||||||
const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false);
|
const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false);
|
||||||
const [eventInterrupt, setEventInterrupt] = useState(false);
|
|
||||||
const endMessageRef = useRef<HTMLDivElement>(null);
|
|
||||||
const handleUserInterruption = () => {
|
|
||||||
if (!eventInterrupt && status === 'loading') setEventInterrupt(true);
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
!eventInterrupt && scrollIntoView();
|
|
||||||
}, [queries.length, queries[queries.length - 1]]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
identifier && dispatch(setIdentifier(identifier));
|
identifier && dispatch(setIdentifier(identifier));
|
||||||
const element = document.getElementById('inputbox') as HTMLInputElement;
|
|
||||||
if (element) {
|
|
||||||
element.focus();
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -66,20 +58,6 @@ export const SharedConversation = () => {
|
|||||||
}
|
}
|
||||||
}, [queries[queries.length - 1]]);
|
}, [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 = () => {
|
const fetchQueries = () => {
|
||||||
identifier &&
|
identifier &&
|
||||||
conversationService
|
conversationService
|
||||||
@@ -95,7 +73,7 @@ export const SharedConversation = () => {
|
|||||||
setFetchedData({
|
setFetchedData({
|
||||||
queries: data.queries,
|
queries: data.queries,
|
||||||
title: data.title,
|
title: data.title,
|
||||||
date: data.date,
|
date: formatDate(data.timestamp),
|
||||||
identifier,
|
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 = (
|
|
||||||
<ConversationBubble
|
|
||||||
ref={endMessageRef}
|
|
||||||
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'}`}
|
|
||||||
key={`${index}ANSWER`}
|
|
||||||
message={query.response}
|
|
||||||
type={'ANSWER'}
|
|
||||||
sources={query.sources ?? []}
|
|
||||||
toolCalls={query.tool_calls}
|
|
||||||
></ConversationBubble>
|
|
||||||
);
|
|
||||||
} else if (query.error) {
|
|
||||||
responseView = (
|
|
||||||
<ConversationBubble
|
|
||||||
ref={endMessageRef}
|
|
||||||
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'} `}
|
|
||||||
key={`${index}ERROR`}
|
|
||||||
message={query.error}
|
|
||||||
type="ERROR"
|
|
||||||
></ConversationBubble>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return responseView;
|
|
||||||
};
|
|
||||||
const handleQuestionSubmission = () => {
|
const handleQuestionSubmission = () => {
|
||||||
if (inputRef.current?.textContent && status !== 'loading') {
|
if (input && status !== 'loading') {
|
||||||
if (lastQueryReturnedErr) {
|
if (lastQueryReturnedErr) {
|
||||||
// update last failed query with new prompt
|
// update last failed query with new prompt
|
||||||
dispatch(
|
dispatch(
|
||||||
updateQuery({
|
updateQuery({
|
||||||
index: queries.length - 1,
|
index: queries.length - 1,
|
||||||
query: {
|
query: {
|
||||||
prompt: inputRef.current.textContent,
|
prompt: input,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -152,9 +99,9 @@ export const SharedConversation = () => {
|
|||||||
isRetry: true,
|
isRetry: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
handleQuestion({ question: inputRef.current.textContent });
|
handleQuestion({ question: input });
|
||||||
}
|
}
|
||||||
inputRef.current.textContent = '';
|
setInput('');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -167,7 +114,6 @@ export const SharedConversation = () => {
|
|||||||
}) => {
|
}) => {
|
||||||
question = question.trim();
|
question = question.trim();
|
||||||
if (question === '') return;
|
if (question === '') return;
|
||||||
setEventInterrupt(false);
|
|
||||||
!isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries
|
!isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries
|
||||||
dispatch(fetchSharedAnswer({ question }));
|
dispatch(fetchSharedAnswer({ question }));
|
||||||
};
|
};
|
||||||
@@ -192,93 +138,47 @@ export const SharedConversation = () => {
|
|||||||
content="Shared conversations with DocsGPT"
|
content="Shared conversations with DocsGPT"
|
||||||
/>
|
/>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="flex h-full flex-col items-center justify-between gap-2 overflow-y-hidden dark:bg-raisin-black">
|
<div className="flex h-full flex-col items-center justify-between gap-2 overflow-y-hidden dark:bg-raisin-black ">
|
||||||
<div
|
<div className="border-b p-2 dark:border-b-silver w-full md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12 max-w-[1200px]">
|
||||||
ref={sharedConversationRef}
|
<h1 className="font-semi-bold text-4xl text-chinese-black dark:text-chinese-silver">
|
||||||
onWheel={handleUserInterruption}
|
{title}
|
||||||
onTouchMove={handleUserInterruption}
|
</h1>
|
||||||
className="flex w-full justify-center overflow-auto"
|
<h2 className="font-semi-bold text-base text-chinese-black dark:text-chinese-silver">
|
||||||
>
|
{t('sharedConv.subtitle')}{' '}
|
||||||
<div className="mt-0 w-11/12 md:w-10/12 lg:w-6/12">
|
<a href="/" className="text-[#007DFF]">
|
||||||
<div className="mb-2 w-full border-b pb-2 dark:border-b-silver">
|
DocsGPT
|
||||||
<h1 className="font-semi-bold text-4xl text-chinese-black dark:text-chinese-silver">
|
</a>
|
||||||
{title}
|
</h2>
|
||||||
</h1>
|
<h2 className="font-semi-bold text-base text-chinese-black dark:text-chinese-silver">
|
||||||
<h2 className="font-semi-bold text-base text-chinese-black dark:text-chinese-silver">
|
{date}
|
||||||
{t('sharedConv.subtitle')}{' '}
|
</h2>
|
||||||
<a href="/" className="text-[#007DFF]">
|
|
||||||
DocsGPT
|
|
||||||
</a>
|
|
||||||
</h2>
|
|
||||||
<h2 className="font-semi-bold text-base text-chinese-black dark:text-chinese-silver">
|
|
||||||
{date}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div className="">
|
|
||||||
{queries?.map((query, index) => {
|
|
||||||
return (
|
|
||||||
<Fragment key={index}>
|
|
||||||
<ConversationBubble
|
|
||||||
ref={endMessageRef}
|
|
||||||
className={'mb-1 last:mb-28 md:mb-7'}
|
|
||||||
key={`${index}QUESTION`}
|
|
||||||
message={query.prompt}
|
|
||||||
type="QUESTION"
|
|
||||||
sources={query.sources}
|
|
||||||
></ConversationBubble>
|
|
||||||
|
|
||||||
{prepResponseView(query, index)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<ConversationMessages
|
||||||
<div className="flex w-11/12 flex-col items-center gap-4 pb-2 md:w-10/12 lg:w-6/12">
|
handleQuestion={handleQuestion}
|
||||||
|
handleQuestionSubmission={handleQuestionSubmission}
|
||||||
|
queries={queries}
|
||||||
|
status={status}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col items-center gap-4 pb-2 w-full md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12 max-w-[1200px]">
|
||||||
{apiKey ? (
|
{apiKey ? (
|
||||||
<div className="flex h-full w-full items-center rounded-[40px] border border-silver bg-white py-1 dark:bg-raisin-black">
|
<MessageInput
|
||||||
<div
|
value={input}
|
||||||
id="inputbox"
|
onChange={(e) => setInput(e.target.value)}
|
||||||
ref={inputRef}
|
onSubmit={() => handleQuestionSubmission()}
|
||||||
tabIndex={1}
|
loading={status === 'loading'}
|
||||||
onPaste={handlePaste}
|
/>
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
contentEditable
|
|
||||||
className={`inputbox-style max-h-24 w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-full bg-white pt-5 pb-[22px] text-base leading-tight opacity-100 focus:outline-none dark:bg-raisin-black dark:text-bright-gray`}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleQuestionSubmission();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
{status === 'loading' ? (
|
|
||||||
<img
|
|
||||||
src={Spinner}
|
|
||||||
className="relative right-[38px] bottom-[24px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent filter dark:invert"
|
|
||||||
></img>
|
|
||||||
) : (
|
|
||||||
<div className="mx-1 cursor-pointer rounded-full p-3 text-center hover:bg-gray-3000 dark:hover:bg-dark-charcoal">
|
|
||||||
<img
|
|
||||||
onClick={handleQuestionSubmission}
|
|
||||||
className="ml-[4px] h-6 w-6 text-white filter dark:invert"
|
|
||||||
src={Send}
|
|
||||||
></img>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<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 mb-14 sm:mb-0"
|
className="w-fit rounded-full bg-purple-30 py-3 px-5 text-white shadow-xl transition-colors duration-200 hover:bg-violets-are-blue mb-14 sm:mb-0"
|
||||||
>
|
>
|
||||||
{t('sharedConv.button')}
|
{t('sharedConv.button')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<span className="mb-2 hidden text-xs text-dark-charcoal dark:text-silver sm:inline">
|
|
||||||
|
<p className="text-gray-4000 hidden w-[100vw] self-center bg-transparent py-2 text-center text-xs dark:text-sonic-silver md:inline md:w-full">
|
||||||
{t('sharedConv.meta')}
|
{t('sharedConv.meta')}
|
||||||
</span>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -267,8 +267,7 @@ export const conversationSlice = createSlice({
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
state.status = 'failed';
|
state.status = 'failed';
|
||||||
state.queries[state.queries.length - 1].error =
|
state.queries[state.queries.length - 1].error = 'Something went wrong';
|
||||||
'Something went wrong. Please check your internet connection.';
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -231,8 +231,7 @@ export const sharedConversationSlice = createSlice({
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
state.status = 'failed';
|
state.status = 'failed';
|
||||||
state.queries[state.queries.length - 1].error =
|
state.queries[state.queries.length - 1].error = 'Something went wrong';
|
||||||
'Something went wrong. Please check your internet connection.';
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -485,25 +485,31 @@ template {
|
|||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 10;
|
width: 10;
|
||||||
}
|
}
|
||||||
|
/* Light mode specific autofill styles */
|
||||||
input:-webkit-autofill {
|
input:-webkit-autofill,
|
||||||
-webkit-box-shadow: 0 0 0 50px white inset;
|
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 {
|
/* Dark mode specific autofill styles */
|
||||||
-webkit-box-shadow: 0 0 0 50px white inset;
|
.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) {
|
/* Additional autocomplete dropdown styles for dark mode */
|
||||||
input:-webkit-autofill {
|
.dark input:-webkit-autofill::first-line {
|
||||||
-webkit-box-shadow: 0 0 0 50px rgb(68, 70, 84) inset;
|
color: #e5e7eb;
|
||||||
-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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputbox-style {
|
.inputbox-style {
|
||||||
|
|||||||
@@ -50,7 +50,8 @@
|
|||||||
"medium": "Medium",
|
"medium": "Medium",
|
||||||
"high": "High",
|
"high": "High",
|
||||||
"unlimited": "Unlimited",
|
"unlimited": "Unlimited",
|
||||||
"default": "Default"
|
"default": "Default",
|
||||||
|
"add": "Add"
|
||||||
},
|
},
|
||||||
"documents": {
|
"documents": {
|
||||||
"title": "This table contains all the documents that are available to you and those you have uploaded",
|
"title": "This table contains all the documents that are available to you and those you have uploaded",
|
||||||
@@ -72,6 +73,7 @@
|
|||||||
"monthly": "Monthly"
|
"monthly": "Monthly"
|
||||||
},
|
},
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
|
"view": "View",
|
||||||
"deleteWarning": "Are you sure you want to delete \"{{name}}\"?"
|
"deleteWarning": "Are you sure you want to delete \"{{name}}\"?"
|
||||||
},
|
},
|
||||||
"apiKeys": {
|
"apiKeys": {
|
||||||
@@ -97,7 +99,7 @@
|
|||||||
},
|
},
|
||||||
"messages": "Messages",
|
"messages": "Messages",
|
||||||
"tokenUsage": "Token Usage",
|
"tokenUsage": "Token Usage",
|
||||||
"feedback": "Feedback",
|
"userFeedback": "User Feedback",
|
||||||
"filterPlaceholder": "Filter",
|
"filterPlaceholder": "Filter",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"positiveFeedback": "Positive Feedback",
|
"positiveFeedback": "Positive Feedback",
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
"prompt": "Prompt Activo",
|
"prompt": "Prompt Activo",
|
||||||
"deleteAllLabel": "Eliminar todas las conversaciones",
|
"deleteAllLabel": "Eliminar todas las conversaciones",
|
||||||
"deleteAllBtn": "Eliminar todo",
|
"deleteAllBtn": "Eliminar todo",
|
||||||
"addNew": "Agregar Nuevo",
|
"add": "Añadir",
|
||||||
"convHistory": "Historial de conversaciones",
|
"convHistory": "Historial de conversaciones",
|
||||||
"none": "Ninguno",
|
"none": "Ninguno",
|
||||||
"low": "Bajo",
|
"low": "Bajo",
|
||||||
@@ -72,6 +72,7 @@
|
|||||||
"monthly": "Mensual"
|
"monthly": "Mensual"
|
||||||
},
|
},
|
||||||
"actions": "Acciones",
|
"actions": "Acciones",
|
||||||
|
"view": "Ver",
|
||||||
"deleteWarning": "¿Estás seguro de que deseas eliminar \"{{name}}\"?"
|
"deleteWarning": "¿Estás seguro de que deseas eliminar \"{{name}}\"?"
|
||||||
},
|
},
|
||||||
"apiKeys": {
|
"apiKeys": {
|
||||||
@@ -97,7 +98,7 @@
|
|||||||
},
|
},
|
||||||
"messages": "Mensajes",
|
"messages": "Mensajes",
|
||||||
"tokenUsage": "Uso de Tokens",
|
"tokenUsage": "Uso de Tokens",
|
||||||
"feedback": "Retroalimentación",
|
"userFeedback": "Retroalimentación del Usuario",
|
||||||
"filterPlaceholder": "Filtrar",
|
"filterPlaceholder": "Filtrar",
|
||||||
"none": "Ninguno",
|
"none": "Ninguno",
|
||||||
"positiveFeedback": "Retroalimentación Positiva",
|
"positiveFeedback": "Retroalimentación Positiva",
|
||||||
|
|||||||
@@ -50,7 +50,8 @@
|
|||||||
"medium": "中",
|
"medium": "中",
|
||||||
"high": "高",
|
"high": "高",
|
||||||
"unlimited": "無制限",
|
"unlimited": "無制限",
|
||||||
"default": "デフォルト"
|
"default": "デフォルト",
|
||||||
|
"add": "追加"
|
||||||
},
|
},
|
||||||
"documents": {
|
"documents": {
|
||||||
"label": "ドキュメント",
|
"label": "ドキュメント",
|
||||||
@@ -71,6 +72,7 @@
|
|||||||
"monthly": "毎月"
|
"monthly": "毎月"
|
||||||
},
|
},
|
||||||
"actions": "アクション",
|
"actions": "アクション",
|
||||||
|
"view": "表示",
|
||||||
"deleteWarning": "\"{{name}}\"を削除してもよろしいですか?"
|
"deleteWarning": "\"{{name}}\"を削除してもよろしいですか?"
|
||||||
},
|
},
|
||||||
"apiKeys": {
|
"apiKeys": {
|
||||||
@@ -100,7 +102,8 @@
|
|||||||
"filterPlaceholder": "フィルター",
|
"filterPlaceholder": "フィルター",
|
||||||
"none": "なし",
|
"none": "なし",
|
||||||
"positiveFeedback": "肯定的なフィードバック",
|
"positiveFeedback": "肯定的なフィードバック",
|
||||||
"negativeFeedback": "否定的なフィードバック"
|
"negativeFeedback": "否定的なフィードバック",
|
||||||
|
"userFeedback": "ユーザーフィードバック"
|
||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"label": "ログ",
|
"label": "ログ",
|
||||||
|
|||||||
@@ -50,7 +50,8 @@
|
|||||||
"medium": "Средний",
|
"medium": "Средний",
|
||||||
"high": "Высокий",
|
"high": "Высокий",
|
||||||
"unlimited": "Без ограничений",
|
"unlimited": "Без ограничений",
|
||||||
"default": "По умолчанию"
|
"default": "По умолчанию",
|
||||||
|
"add": "Добавить"
|
||||||
},
|
},
|
||||||
"documents": {
|
"documents": {
|
||||||
"title": "Эта таблица содержит все документы, которые доступны вам и те, которые вы загрузили",
|
"title": "Эта таблица содержит все документы, которые доступны вам и те, которые вы загрузили",
|
||||||
@@ -72,6 +73,7 @@
|
|||||||
"monthly": "Ежемесячно"
|
"monthly": "Ежемесячно"
|
||||||
},
|
},
|
||||||
"actions": "Действия",
|
"actions": "Действия",
|
||||||
|
"view": "Просмотр",
|
||||||
"deleteWarning": "Вы уверены, что хотите удалить \"{{name}}\"?"
|
"deleteWarning": "Вы уверены, что хотите удалить \"{{name}}\"?"
|
||||||
},
|
},
|
||||||
"apiKeys": {
|
"apiKeys": {
|
||||||
@@ -97,7 +99,7 @@
|
|||||||
},
|
},
|
||||||
"messages": "Сообщения",
|
"messages": "Сообщения",
|
||||||
"tokenUsage": "Использование токена",
|
"tokenUsage": "Использование токена",
|
||||||
"feedback": "Обратная связь",
|
"userFeedback": "Обратная Связь Пользователя",
|
||||||
"filterPlaceholder": "Фильтр",
|
"filterPlaceholder": "Фильтр",
|
||||||
"none": "Нет",
|
"none": "Нет",
|
||||||
"positiveFeedback": "Положительная обратная связь",
|
"positiveFeedback": "Положительная обратная связь",
|
||||||
|
|||||||
@@ -50,7 +50,8 @@
|
|||||||
"medium": "中",
|
"medium": "中",
|
||||||
"high": "高",
|
"high": "高",
|
||||||
"unlimited": "無限制",
|
"unlimited": "無限制",
|
||||||
"default": "預設"
|
"default": "預設",
|
||||||
|
"add": "添加"
|
||||||
},
|
},
|
||||||
"documents": {
|
"documents": {
|
||||||
"title": "此表格包含所有可供您使用的文件以及您上傳的文件",
|
"title": "此表格包含所有可供您使用的文件以及您上傳的文件",
|
||||||
@@ -72,6 +73,7 @@
|
|||||||
"monthly": "每月"
|
"monthly": "每月"
|
||||||
},
|
},
|
||||||
"actions": "操作",
|
"actions": "操作",
|
||||||
|
"view": "查看",
|
||||||
"deleteWarning": "您確定要刪除 \"{{name}}\" 嗎?"
|
"deleteWarning": "您確定要刪除 \"{{name}}\" 嗎?"
|
||||||
},
|
},
|
||||||
"apiKeys": {
|
"apiKeys": {
|
||||||
@@ -97,7 +99,7 @@
|
|||||||
},
|
},
|
||||||
"messages": "訊息",
|
"messages": "訊息",
|
||||||
"tokenUsage": "Token 使用量",
|
"tokenUsage": "Token 使用量",
|
||||||
"feedback": "回饋",
|
"userFeedback": "使用者反饋",
|
||||||
"filterPlaceholder": "篩選",
|
"filterPlaceholder": "篩選",
|
||||||
"none": "無",
|
"none": "無",
|
||||||
"positiveFeedback": "正向回饋",
|
"positiveFeedback": "正向回饋",
|
||||||
|
|||||||
@@ -50,7 +50,8 @@
|
|||||||
"medium": "中",
|
"medium": "中",
|
||||||
"high": "高",
|
"high": "高",
|
||||||
"unlimited": "无限",
|
"unlimited": "无限",
|
||||||
"default": "默认"
|
"default": "默认",
|
||||||
|
"add": "添加"
|
||||||
},
|
},
|
||||||
"documents": {
|
"documents": {
|
||||||
"title": "此表格包含所有可供您使用的文档以及您上传的文档",
|
"title": "此表格包含所有可供您使用的文档以及您上传的文档",
|
||||||
@@ -72,6 +73,7 @@
|
|||||||
"monthly": "每月"
|
"monthly": "每月"
|
||||||
},
|
},
|
||||||
"actions": "操作",
|
"actions": "操作",
|
||||||
|
"view": "查看",
|
||||||
"deleteWarning": "您确定要删除 \"{{name}}\" 吗?"
|
"deleteWarning": "您确定要删除 \"{{name}}\" 吗?"
|
||||||
},
|
},
|
||||||
"apiKeys": {
|
"apiKeys": {
|
||||||
@@ -97,7 +99,7 @@
|
|||||||
},
|
},
|
||||||
"messages": "消息",
|
"messages": "消息",
|
||||||
"tokenUsage": "令牌使用",
|
"tokenUsage": "令牌使用",
|
||||||
"feedback": "反馈",
|
"userFeedback": "用户反馈",
|
||||||
"filterPlaceholder": "筛选",
|
"filterPlaceholder": "筛选",
|
||||||
"none": "无",
|
"none": "无",
|
||||||
"positiveFeedback": "正向反馈",
|
"positiveFeedback": "正向反馈",
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export default function AddActionModal({
|
|||||||
setFunctionNameError(!isValidFunctionName(value));
|
setFunctionNameError(!isValidFunctionName(value));
|
||||||
}}
|
}}
|
||||||
borderVariant="thin"
|
borderVariant="thin"
|
||||||
|
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||||
placeholder={'Enter name'}
|
placeholder={'Enter name'}
|
||||||
/>
|
/>
|
||||||
<p
|
<p
|
||||||
@@ -74,7 +75,7 @@ export default function AddActionModal({
|
|||||||
<div className="mt-3 flex flex-row-reverse gap-1 px-3">
|
<div className="mt-3 flex flex-row-reverse gap-1 px-3">
|
||||||
<button
|
<button
|
||||||
onClick={handleAddAction}
|
onClick={handleAddAction}
|
||||||
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
|
Add
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -100,57 +100,60 @@ export default function AddToolModal({
|
|||||||
{modalState === 'ACTIVE' && (
|
{modalState === 'ACTIVE' && (
|
||||||
<WrapperComponent
|
<WrapperComponent
|
||||||
close={() => setModalState('INACTIVE')}
|
close={() => setModalState('INACTIVE')}
|
||||||
className="h-[85vh] w-[90vw] md:w-[75vw]"
|
className="max-w-[950px] w-[90vw] md:w-[85vw] lg:w-[75vw] h-[85vh]"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-4 h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div>
|
<div>
|
||||||
<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">
|
||||||
{t('settings.tools.selectToolSetup')}
|
{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 h-[73vh] overflow-auto px-3 py-px">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="h-full col-span-3 flex items-center justify-center">
|
<div className="h-full flex items-center justify-center">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
availableTools.map((tool, index) => (
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 auto-rows-fr pb-2">
|
||||||
<div
|
{availableTools.map((tool, index) => (
|
||||||
role="button"
|
<div
|
||||||
tabIndex={0}
|
role="button"
|
||||||
key={index}
|
tabIndex={0}
|
||||||
className="h-52 w-full p-6 border rounded-2xl border-silver dark:border-[#4D4E58] flex flex-col justify-between dark:bg-[#32333B] cursor-pointer hover:border-[#9d9d9d] hover:dark:border-[#717179]"
|
key={index}
|
||||||
onClick={() => {
|
className="h-52 w-full p-6 border rounded-2xl border-light-gainsboro dark:border-arsenic bg-white-3000 dark:bg-gunmetal flex flex-col justify-between cursor-pointer hover:border-[#9d9d9d] hover:dark:border-[#717179]"
|
||||||
setSelectedTool(tool);
|
onClick={() => {
|
||||||
handleAddTool(tool);
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
setSelectedTool(tool);
|
setSelectedTool(tool);
|
||||||
handleAddTool(tool);
|
handleAddTool(tool);
|
||||||
}
|
}}
|
||||||
}}
|
onKeyDown={(e) => {
|
||||||
>
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
<div className="w-full">
|
setSelectedTool(tool);
|
||||||
<div className="px-1 w-full flex items-center justify-between">
|
handleAddTool(tool);
|
||||||
<img
|
}
|
||||||
src={`/toolIcons/tool_${tool.name}.svg`}
|
}}
|
||||||
className="h-8 w-8"
|
>
|
||||||
/>
|
<div className="w-full">
|
||||||
</div>
|
<div className="px-1 w-full flex items-center justify-between">
|
||||||
<div className="mt-[9px]">
|
<img
|
||||||
<p
|
src={`/toolIcons/tool_${tool.name}.svg`}
|
||||||
title={tool.displayName}
|
className="h-6 w-6"
|
||||||
className="px-1 text-sm font-semibold text-eerie-black dark:text-white leading-relaxed capitalize truncate"
|
alt={`${tool.name} icon`}
|
||||||
>
|
/>
|
||||||
{tool.displayName}
|
</div>
|
||||||
</p>
|
<div className="mt-[9px]">
|
||||||
<p className="mt-1 px-1 h-24 overflow-auto text-sm text-gray-600 dark:text-[#8a8a8c] leading-relaxed">
|
<p
|
||||||
{tool.description}
|
title={tool.displayName}
|
||||||
</p>
|
className="px-1 text-[13px] font-semibold text-raisin-black-light dark:text-bright-gray leading-relaxed capitalize truncate"
|
||||||
|
>
|
||||||
|
{tool.displayName}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 px-1 h-24 overflow-auto text-[12px] text-old-silver dark:text-sonic-silver-light leading-relaxed">
|
||||||
|
{tool.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export default function ChunkModal({
|
|||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
borderVariant="thin"
|
borderVariant="thin"
|
||||||
placeholder={'Enter title'}
|
placeholder={'Enter title'}
|
||||||
|
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||||
></Input>
|
></Input>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 relative px-3">
|
<div className="mt-6 relative px-3">
|
||||||
@@ -83,7 +84,7 @@ export default function ChunkModal({
|
|||||||
handleSubmit(title, chunkText);
|
handleSubmit(title, chunkText);
|
||||||
setModalState('INACTIVE');
|
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
|
Add
|
||||||
</button>
|
</button>
|
||||||
@@ -123,15 +124,13 @@ export default function ChunkModal({
|
|||||||
Edit Chunk
|
Edit Chunk
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mt-6 relative px-3">
|
<div className="mt-6 relative px-3">
|
||||||
<span className="z-10 absolute left-5 -top-2 bg-white px-2 text-xs text-gray-4000 dark:bg-[#26272E] dark:text-silver">
|
|
||||||
Title
|
|
||||||
</span>
|
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
borderVariant="thin"
|
borderVariant="thin"
|
||||||
placeholder={'Enter title'}
|
placeholder={'Enter title'}
|
||||||
|
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||||
></Input>
|
></Input>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 relative px-3">
|
<div className="mt-6 relative px-3">
|
||||||
@@ -163,7 +162,7 @@ export default function ChunkModal({
|
|||||||
handleSubmit(title, chunkText);
|
handleSubmit(title, chunkText);
|
||||||
setModalState('INACTIVE');
|
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
|
Update
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default function ConfigToolModal({
|
|||||||
onChange={(e) => setAuthKey(e.target.value)}
|
onChange={(e) => setAuthKey(e.target.value)}
|
||||||
borderVariant="thin"
|
borderVariant="thin"
|
||||||
placeholder={t('modals.configTool.apiKeyPlaceholder')}
|
placeholder={t('modals.configTool.apiKeyPlaceholder')}
|
||||||
label={t('modals.configTool.apiKeyLabel')}
|
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 flex flex-row-reverse gap-1 px-3">
|
<div className="mt-8 flex flex-row-reverse gap-1 px-3">
|
||||||
@@ -73,7 +73,7 @@ export default function ConfigToolModal({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
tool && handleAddTool(tool);
|
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')}
|
{t('modals.configTool.addButton')}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default function ConfirmationModal({
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
cancelLabel,
|
cancelLabel,
|
||||||
handleCancel,
|
handleCancel,
|
||||||
|
variant = 'default',
|
||||||
}: {
|
}: {
|
||||||
message: string;
|
message: string;
|
||||||
modalState: ActiveState;
|
modalState: ActiveState;
|
||||||
@@ -19,8 +20,15 @@ export default function ConfirmationModal({
|
|||||||
handleSubmit: () => void;
|
handleSubmit: () => void;
|
||||||
cancelLabel?: string;
|
cancelLabel?: string;
|
||||||
handleCancel?: () => void;
|
handleCancel?: () => void;
|
||||||
|
variant?: 'default' | 'danger';
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{modalState === 'ACTIVE' && (
|
{modalState === 'ACTIVE' && (
|
||||||
@@ -39,7 +47,7 @@ export default function ConfirmationModal({
|
|||||||
<div className="mt-6 flex flex-row-reverse gap-1">
|
<div className="mt-6 flex flex-row-reverse gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-[#6F3FD1]"
|
className={submitButtonClasses}
|
||||||
>
|
>
|
||||||
{submitLabel}
|
{submitLabel}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -85,9 +85,10 @@ export default function CreateAPIKeyModal({
|
|||||||
type="text"
|
type="text"
|
||||||
className="rounded-md"
|
className="rounded-md"
|
||||||
value={APIKeyName}
|
value={APIKeyName}
|
||||||
label={t('modals.createAPIKey.apiKeyName')}
|
placeholder={t('modals.createAPIKey.apiKeyName')}
|
||||||
onChange={(e) => setAPIKeyName(e.target.value)}
|
onChange={(e) => setAPIKeyName(e.target.value)}
|
||||||
borderVariant="thin"
|
borderVariant="thin"
|
||||||
|
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||||
></Input>
|
></Input>
|
||||||
</div>
|
</div>
|
||||||
<div className="my-4">
|
<div className="my-4">
|
||||||
@@ -145,7 +146,7 @@ export default function CreateAPIKeyModal({
|
|||||||
createAPIKey(payload);
|
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')}
|
{t('modals.createAPIKey.create')}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export default function DeleteConvModal({
|
|||||||
submitLabel={t('modals.deleteConv.delete')}
|
submitLabel={t('modals.deleteConv.delete')}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
handleCancel={handleCancel}
|
handleCancel={handleCancel}
|
||||||
|
variant="danger"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export default function SaveAPIKeyModal({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="my-1 h-10 w-20 rounded-full border border-solid border-purple-30 p-2 text-sm text-purple-30 hover:bg-purple-30 hover:text-white"
|
className="my-1 h-10 w-20 rounded-full border border-solid border-violets-are-blue p-2 text-sm text-violets-are-blue hover:bg-violets-are-blue hover:text-white"
|
||||||
onClick={handleCopyKey}
|
onClick={handleCopyKey}
|
||||||
>
|
>
|
||||||
{isCopied ? t('modals.saveKey.copied') : t('modals.saveKey.copy')}
|
{isCopied ? t('modals.saveKey.copied') : t('modals.saveKey.copy')}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useSelector } from 'react-redux';
|
|||||||
import conversationService from '../api/services/conversationService';
|
import conversationService from '../api/services/conversationService';
|
||||||
import Spinner from '../assets/spinner.svg';
|
import Spinner from '../assets/spinner.svg';
|
||||||
import Dropdown from '../components/Dropdown';
|
import Dropdown from '../components/Dropdown';
|
||||||
|
import ToggleSwitch from '../components/ToggleSwitch';
|
||||||
import { Doc } from '../models/misc';
|
import { Doc } from '../models/misc';
|
||||||
import {
|
import {
|
||||||
selectChunks,
|
selectChunks,
|
||||||
@@ -104,38 +105,21 @@ export const ShareConversationModal = ({
|
|||||||
return (
|
return (
|
||||||
<WrapperModal close={close}>
|
<WrapperModal close={close}>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h2 className="text-xl font-medium text-eerie-black dark:text-white">
|
<h2 className="text-xl font-medium text-eerie-black dark:text-chinese-white">
|
||||||
{t('modals.shareConv.label')}
|
{t('modals.shareConv.label')}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-eerie-black dark:text-white">
|
<p className="text-sm text-eerie-black dark:text-silver/60">
|
||||||
{t('modals.shareConv.note')}
|
{t('modals.shareConv.note')}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-lg text-eerie-black dark:text-white">
|
<span className="text-lg text-eerie-black dark:text-white">
|
||||||
{t('modals.shareConv.option')}
|
{t('modals.shareConv.option')}
|
||||||
</span>
|
</span>
|
||||||
<label className="cursor-pointer select-none items-center">
|
<ToggleSwitch
|
||||||
<div className="relative">
|
checked={allowPrompt}
|
||||||
<input
|
onChange={togglePromptPermission}
|
||||||
type="checkbox"
|
size="medium"
|
||||||
checked={allowPrompt}
|
/>
|
||||||
onChange={togglePromptPermission}
|
|
||||||
className="sr-only"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={`box block h-8 w-14 rounded-full border border-purple-30 ${
|
|
||||||
allowPrompt
|
|
||||||
? 'bg-purple-30 dark:bg-purple-30'
|
|
||||||
: 'dark:bg-transparent'
|
|
||||||
}`}
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
className={`absolute left-1 top-1 flex h-6 w-6 items-center justify-center rounded-full transition ${
|
|
||||||
allowPrompt ? 'translate-x-full bg-silver' : 'bg-purple-30'
|
|
||||||
}`}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
{allowPrompt && (
|
{allowPrompt && (
|
||||||
<div className="my-4">
|
<div className="my-4">
|
||||||
@@ -152,19 +136,19 @@ export const ShareConversationModal = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-baseline justify-between gap-2">
|
<div className="flex items-baseline justify-between gap-2">
|
||||||
<span className="no-scrollbar w-full overflow-x-auto whitespace-nowrap rounded-full border-2 py-3 px-4 text-eerie-black dark:text-white">
|
<span className="no-scrollbar w-full overflow-x-auto whitespace-nowrap rounded-full border-2 border-silver dark:border-silver/40 py-3 px-4 text-eerie-black dark:text-white">
|
||||||
{`${domain}/share/${identifier ?? '....'}`}
|
{`${domain}/share/${identifier ?? '....'}`}
|
||||||
</span>
|
</span>
|
||||||
{status === 'fetched' ? (
|
{status === 'fetched' ? (
|
||||||
<button
|
<button
|
||||||
className="my-1 h-10 w-28 rounded-full border border-solid bg-purple-30 p-2 text-sm text-white hover:bg-[#6F3FD1]"
|
className="my-1 h-10 w-28 rounded-full bg-purple-30 p-2 text-sm text-white hover:bg-violets-are-blue"
|
||||||
onClick={() => handleCopyKey(`${domain}/share/${identifier}`)}
|
onClick={() => handleCopyKey(`${domain}/share/${identifier}`)}
|
||||||
>
|
>
|
||||||
{isCopied ? t('modals.saveKey.copied') : t('modals.saveKey.copy')}
|
{isCopied ? t('modals.saveKey.copied') : t('modals.saveKey.copy')}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
className="my-1 flex h-10 w-28 items-center justify-evenly rounded-full border border-solid bg-purple-30 p-2 text-center text-sm font-normal text-white hover:bg-[#6F3FD1]"
|
className="my-1 flex h-10 w-28 items-center justify-evenly rounded-full bg-purple-30 p-2 text-center text-sm font-normal text-white hover:bg-violets-are-blue"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
shareCoversationPublicly(allowPrompt);
|
shareCoversationPublicly(allowPrompt);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -32,16 +32,14 @@ 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"
|
||||||
label={t('modals.prompts.promptName')}
|
className="mb-4"
|
||||||
className="h-10 rounded-lg"
|
|
||||||
value={newPromptName}
|
value={newPromptName}
|
||||||
onChange={(e) => setNewPromptName(e.target.value)}
|
onChange={(e) => setNewPromptName(e.target.value)}
|
||||||
|
labelBgClassName="bg-white dark:bg-[#26272E]"
|
||||||
|
borderVariant="thin"
|
||||||
/>
|
/>
|
||||||
<div className="relative top-[7px] left-3">
|
<div className="relative top-[7px] left-3">
|
||||||
<span className="bg-white px-1 text-xs text-silver dark:bg-[#26272E] dark:text-silver">
|
<span className="bg-white px-1 text-xs text-silver dark:bg-[#26272E] dark:text-silver">
|
||||||
@@ -49,7 +47,7 @@ function AddPrompt({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<label htmlFor="new-prompt-content" className="sr-only">
|
<label htmlFor="new-prompt-content" className="sr-only">
|
||||||
Prompt Text
|
{t('modals.prompts.promptText')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="new-prompt-content"
|
id="new-prompt-content"
|
||||||
@@ -62,7 +60,7 @@ function AddPrompt({
|
|||||||
<div className="mt-6 flex flex-row-reverse">
|
<div className="mt-6 flex flex-row-reverse">
|
||||||
<button
|
<button
|
||||||
onClick={handleAddPrompt}
|
onClick={handleAddPrompt}
|
||||||
className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:opacity-90"
|
className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-violets-are-blue disabled:hover:bg-purple-30"
|
||||||
disabled={disableSave}
|
disabled={disableSave}
|
||||||
title={
|
title={
|
||||||
disableSave && newPromptName ? t('modals.prompts.nameExists') : ''
|
disableSave && newPromptName ? t('modals.prompts.nameExists') : ''
|
||||||
@@ -98,7 +96,7 @@ function EditPrompt({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="p-8">
|
<div className="">
|
||||||
<p className="mb-1 text-xl text-jet dark:text-bright-gray">
|
<p className="mb-1 text-xl text-jet dark:text-bright-gray">
|
||||||
{t('modals.prompts.editPrompt')}
|
{t('modals.prompts.editPrompt')}
|
||||||
</p>
|
</p>
|
||||||
@@ -106,28 +104,22 @@ 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="mb-4"
|
||||||
value={editPromptName}
|
value={editPromptName}
|
||||||
onChange={(e) => setEditPromptName(e.target.value)}
|
onChange={(e) => setEditPromptName(e.target.value)}
|
||||||
|
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||||
|
borderVariant="thin"
|
||||||
/>
|
/>
|
||||||
<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">
|
|
||||||
{t('modals.prompts.promptName')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="relative top-[7px] left-3">
|
<div className="relative top-[7px] left-3">
|
||||||
<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-charleston-green-2 dark:text-silver">
|
||||||
{t('modals.prompts.promptText')}
|
{t('modals.prompts.promptText')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<label htmlFor="edit-prompt-content" className="sr-only">
|
<label htmlFor="edit-prompt-content" className="sr-only">
|
||||||
Prompt Text
|
{t('modals.prompts.promptText')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="edit-prompt-content"
|
id="edit-prompt-content"
|
||||||
@@ -139,10 +131,10 @@ function EditPrompt({
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-6 flex flex-row-reverse gap-4">
|
<div className="mt-6 flex flex-row-reverse gap-4">
|
||||||
<button
|
<button
|
||||||
className={`rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all ${
|
className={`rounded-3xl bg-purple-30 disabled:hover:bg-purple-30 hover:bg-violets-are-blue px-5 py-2 text-sm text-white transition-all ${
|
||||||
currentPromptEdit.type === 'public'
|
currentPromptEdit.type === 'public'
|
||||||
? 'cursor-not-allowed opacity-50'
|
? 'cursor-not-allowed opacity-50'
|
||||||
: 'hover:opacity-90'
|
: ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleEditPrompt &&
|
handleEditPrompt &&
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export default function APIKeys() {
|
|||||||
<div className="mb-6 flex flex-col sm:flex-row justify-end items-start sm:items-center gap-3">
|
<div className="mb-6 flex flex-col sm:flex-row justify-end items-start sm:items-center gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => setCreateModal(true)}
|
onClick={() => setCreateModal(true)}
|
||||||
className="rounded-full w-full sm:w-40 bg-purple-30 px-4 py-3 text-white hover:bg-[#6F3FD1]"
|
className="rounded-full text-sm w-[108px] h-[30px] bg-purple-30 text-white hover:bg-violets-are-blue flex items-center justify-center"
|
||||||
title={t('settings.apiKeys.createNew')}
|
title={t('settings.apiKeys.createNew')}
|
||||||
>
|
>
|
||||||
{t('settings.apiKeys.createNew')}
|
{t('settings.apiKeys.createNew')}
|
||||||
@@ -125,13 +125,13 @@ export default function APIKeys() {
|
|||||||
<table className="w-full table-auto">
|
<table className="w-full table-auto">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-gray-300 dark:border-silver/40">
|
<tr className="border-b border-gray-300 dark:border-silver/40">
|
||||||
<th className="py-3 px-4 text-left text-xs font-medium text-sonic-silver uppercase w-[35%]">
|
<th className="py-3 px-4 text-left text-xs font-medium text-sonic-silver w-[35%]">
|
||||||
{t('settings.apiKeys.name')}
|
{t('settings.apiKeys.name')}
|
||||||
</th>
|
</th>
|
||||||
<th className="py-3 px-4 text-left text-xs font-medium text-sonic-silver uppercase w-[35%]">
|
<th className="py-3 px-4 text-left text-xs font-medium text-sonic-silver w-[35%]">
|
||||||
{t('settings.apiKeys.sourceDoc')}
|
{t('settings.apiKeys.sourceDoc')}
|
||||||
</th>
|
</th>
|
||||||
<th className="py-3 px-4 text-left text-xs font-medium text-sonic-silver uppercase w-[25%]">
|
<th className="py-3 px-4 text-left text-xs font-medium text-sonic-silver w-[25%]">
|
||||||
<span className="hidden sm:inline">
|
<span className="hidden sm:inline">
|
||||||
{t('settings.apiKeys.key')}
|
{t('settings.apiKeys.key')}
|
||||||
</span>
|
</span>
|
||||||
@@ -139,7 +139,7 @@ export default function APIKeys() {
|
|||||||
{t('settings.apiKeys.key')}
|
{t('settings.apiKeys.key')}
|
||||||
</span>
|
</span>
|
||||||
</th>
|
</th>
|
||||||
<th className="py-3 px-4 text-right text-xs font-medium text-gray-700 dark:text-[#E0E0E0] uppercase w-[5%]">
|
<th className="py-3 px-4 text-right text-xs font-medium text-gray-700 dark:text-[#E0E0E0] w-[5%]">
|
||||||
<span className="sr-only">Actions</span>
|
<span className="sr-only">Actions</span>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -163,7 +163,7 @@ export default function APIKeys() {
|
|||||||
key={element.id}
|
key={element.id}
|
||||||
className="group transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
className="group transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||||
>
|
>
|
||||||
<td className="py-4 px-4 text-sm text-gray-700 dark:text-[#E0E0E0] w-[35%] min-w-48 max-w-0">
|
<td className="py-4 px-4 text-sm font-semibold text-gray-700 dark:text-[#E0E0E0] w-[35%] min-w-48 max-w-0">
|
||||||
<div className="truncate" title={element.name}>
|
<div className="truncate" title={element.name}>
|
||||||
{element.name}
|
{element.name}
|
||||||
</div>
|
</div>
|
||||||
@@ -225,6 +225,7 @@ export default function APIKeys() {
|
|||||||
submitLabel={t('modals.deleteConv.delete')}
|
submitLabel={t('modals.deleteConv.delete')}
|
||||||
handleSubmit={() => handleDeleteKey(keyToDelete.id)}
|
handleSubmit={() => handleDeleteKey(keyToDelete.id)}
|
||||||
handleCancel={() => setKeyToDelete(null)}
|
handleCancel={() => setKeyToDelete(null)}
|
||||||
|
variant="danger"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ export default function Analytics() {
|
|||||||
}
|
}
|
||||||
rounded="3xl"
|
rounded="3xl"
|
||||||
border="border"
|
border="border"
|
||||||
borderColor="gray-700"
|
darkBorderColor="dim-gray"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -348,7 +348,7 @@ export default function Analytics() {
|
|||||||
<div className="h-[345px] w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40 overflow-hidden">
|
<div className="h-[345px] w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40 overflow-hidden">
|
||||||
<div className="flex flex-row items-center justify-start gap-3">
|
<div className="flex flex-row items-center justify-start gap-3">
|
||||||
<p className="font-bold text-jet dark:text-bright-gray">
|
<p className="font-bold text-jet dark:text-bright-gray">
|
||||||
{t('settings.analytics.feedback')}
|
{t('settings.analytics.userFeedback')}
|
||||||
</p>
|
</p>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
size="w-[125px]"
|
size="w-[125px]"
|
||||||
@@ -461,5 +461,17 @@ function AnalyticsChart({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return <Bar options={options} plugins={[htmlLegendPlugin]} data={data} />;
|
return (
|
||||||
|
<Bar
|
||||||
|
options={options}
|
||||||
|
plugins={[htmlLegendPlugin]}
|
||||||
|
data={{
|
||||||
|
...data,
|
||||||
|
datasets: data.datasets.map((dataset) => ({
|
||||||
|
...dataset,
|
||||||
|
hoverBackgroundColor: `${dataset.backgroundColor}CC`, // 80% opacity
|
||||||
|
})),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
@@ -6,10 +6,11 @@ import userService from '../api/services/userService';
|
|||||||
import ArrowLeft from '../assets/arrow-left.svg';
|
import ArrowLeft from '../assets/arrow-left.svg';
|
||||||
import caretSort from '../assets/caret-sort.svg';
|
import caretSort from '../assets/caret-sort.svg';
|
||||||
import Edit from '../assets/edit.svg';
|
import Edit from '../assets/edit.svg';
|
||||||
|
import EyeView from '../assets/eye-view.svg';
|
||||||
import NoFilesDarkIcon from '../assets/no-files-dark.svg';
|
import NoFilesDarkIcon from '../assets/no-files-dark.svg';
|
||||||
import NoFilesIcon from '../assets/no-files.svg';
|
import NoFilesIcon from '../assets/no-files.svg';
|
||||||
import SyncIcon from '../assets/sync.svg';
|
import SyncIcon from '../assets/sync.svg';
|
||||||
import Trash from '../assets/trash.svg';
|
import Trash from '../assets/red-trash.svg';
|
||||||
import Pagination from '../components/DocumentPagination';
|
import Pagination from '../components/DocumentPagination';
|
||||||
import DropdownMenu from '../components/DropdownMenu';
|
import DropdownMenu from '../components/DropdownMenu';
|
||||||
import Input from '../components/Input';
|
import Input from '../components/Input';
|
||||||
@@ -28,6 +29,8 @@ import {
|
|||||||
import Upload from '../upload/Upload';
|
import Upload from '../upload/Upload';
|
||||||
import { formatDate } from '../utils/dateTimeUtils';
|
import { formatDate } from '../utils/dateTimeUtils';
|
||||||
import { ChunkType } from './types';
|
import { ChunkType } from './types';
|
||||||
|
import ContextMenu, { MenuOption } from '../components/ContextMenu';
|
||||||
|
import ThreeDots from '../assets/three-dots.svg';
|
||||||
|
|
||||||
const formatTokens = (tokens: number): string => {
|
const formatTokens = (tokens: number): string => {
|
||||||
const roundToTwoDecimals = (num: number): string => {
|
const roundToTwoDecimals = (num: number): string => {
|
||||||
@@ -63,6 +66,53 @@ export default function Documents({
|
|||||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||||
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
|
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
|
||||||
const [totalPages, setTotalPages] = useState<number>(1);
|
const [totalPages, setTotalPages] = useState<number>(1);
|
||||||
|
|
||||||
|
const [activeMenuId, setActiveMenuId] = useState<string | null>(null);
|
||||||
|
const menuRefs = useRef<{ [key: string]: React.RefObject<HTMLDivElement> }>(
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create or get a ref for each document wrapper div (not the td)
|
||||||
|
const getMenuRef = (docId: string) => {
|
||||||
|
if (!menuRefs.current[docId]) {
|
||||||
|
menuRefs.current[docId] = React.createRef<HTMLDivElement>();
|
||||||
|
}
|
||||||
|
return menuRefs.current[docId];
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuClick = (e: React.MouseEvent, docId: string) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const isAnyMenuOpen =
|
||||||
|
(syncMenuState.isOpen && syncMenuState.docId === docId) ||
|
||||||
|
activeMenuId === docId;
|
||||||
|
|
||||||
|
if (isAnyMenuOpen) {
|
||||||
|
setSyncMenuState((prev) => ({ ...prev, isOpen: false, docId: null }));
|
||||||
|
setActiveMenuId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setActiveMenuId(docId);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (activeMenuId) {
|
||||||
|
const activeRef = menuRefs.current[activeMenuId];
|
||||||
|
if (
|
||||||
|
activeRef?.current &&
|
||||||
|
!activeRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setActiveMenuId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}, [activeMenuId]);
|
||||||
|
|
||||||
const currentDocuments = paginatedDocuments ?? [];
|
const currentDocuments = paginatedDocuments ?? [];
|
||||||
const syncOptions = [
|
const syncOptions = [
|
||||||
{ label: t('settings.documents.syncFrequency.never'), value: 'never' },
|
{ label: t('settings.documents.syncFrequency.never'), value: 'never' },
|
||||||
@@ -71,6 +121,16 @@ export default function Documents({
|
|||||||
{ label: t('settings.documents.syncFrequency.monthly'), value: 'monthly' },
|
{ label: t('settings.documents.syncFrequency.monthly'), value: 'monthly' },
|
||||||
];
|
];
|
||||||
const [showDocumentChunks, setShowDocumentChunks] = useState<Doc>();
|
const [showDocumentChunks, setShowDocumentChunks] = useState<Doc>();
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
const [syncMenuState, setSyncMenuState] = useState<{
|
||||||
|
isOpen: boolean;
|
||||||
|
docId: string | null;
|
||||||
|
document: Doc | null;
|
||||||
|
}>({
|
||||||
|
isOpen: false,
|
||||||
|
docId: null,
|
||||||
|
document: null,
|
||||||
|
});
|
||||||
|
|
||||||
const refreshDocs = useCallback(
|
const refreshDocs = useCallback(
|
||||||
(
|
(
|
||||||
@@ -169,6 +229,50 @@ export default function Documents({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getActionOptions = (index: number, document: Doc): MenuOption[] => {
|
||||||
|
const actions: MenuOption[] = [
|
||||||
|
{
|
||||||
|
icon: EyeView,
|
||||||
|
label: t('settings.documents.view'),
|
||||||
|
onClick: () => {
|
||||||
|
setShowDocumentChunks(document);
|
||||||
|
},
|
||||||
|
iconWidth: 18,
|
||||||
|
iconHeight: 18,
|
||||||
|
variant: 'primary',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (document.syncFrequency) {
|
||||||
|
actions.push({
|
||||||
|
icon: SyncIcon,
|
||||||
|
label: t('settings.documents.sync'),
|
||||||
|
onClick: () => {
|
||||||
|
setSyncMenuState({
|
||||||
|
isOpen: true,
|
||||||
|
docId: document.id ?? null,
|
||||||
|
document: document,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
iconWidth: 14,
|
||||||
|
iconHeight: 14,
|
||||||
|
variant: 'primary',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.push({
|
||||||
|
icon: Trash,
|
||||||
|
label: t('convTile.delete'),
|
||||||
|
onClick: () => {
|
||||||
|
handleDeleteConfirmation(index, document);
|
||||||
|
},
|
||||||
|
iconWidth: 18,
|
||||||
|
iconHeight: 18,
|
||||||
|
variant: 'danger',
|
||||||
|
});
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshDocs(undefined, 1, rowsPerPage);
|
refreshDocs(undefined, 1, rowsPerPage);
|
||||||
}, [searchTerm]);
|
}, [searchTerm]);
|
||||||
@@ -208,7 +312,7 @@ export default function Documents({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="rounded-full w-full sm:w-40 bg-purple-30 px-4 py-3 text-white hover:bg-[#6F3FD1]"
|
className="rounded-full w-[108px] h-[32px] text-sm bg-purple-30 text-white hover:bg-violets-are-blue flex items-center justify-center"
|
||||||
title={t('settings.documents.addNew')}
|
title={t('settings.documents.addNew')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsOnboarding(false);
|
setIsOnboarding(false);
|
||||||
@@ -224,11 +328,11 @@ export default function Documents({
|
|||||||
<table className="w-full table-auto">
|
<table className="w-full table-auto">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-gray-300 dark:border-silver/40">
|
<tr className="border-b border-gray-300 dark:border-silver/40">
|
||||||
<th className="py-3 px-4 text-left text-xs font-medium text-sonic-silver uppercase w-[45%]">
|
<th className="py-3 px-4 text-left text-xs font-medium text-sonic-silver w-[45%]">
|
||||||
{t('settings.documents.name')}
|
{t('settings.documents.name')}
|
||||||
</th>
|
</th>
|
||||||
<th className="py-3 px-4 text-center text-xs font-medium text-sonic-silver uppercase w-[20%]">
|
<th className="py-3 px-4 text-left text-xs font-medium text-sonic-silver w-[30%]">
|
||||||
<div className="flex justify-center items-center">
|
<div className="flex justify-start items-center">
|
||||||
{t('settings.documents.date')}
|
{t('settings.documents.date')}
|
||||||
<img
|
<img
|
||||||
className="cursor-pointer ml-2"
|
className="cursor-pointer ml-2"
|
||||||
@@ -238,8 +342,8 @@ export default function Documents({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th className="py-3 px-4 text-center text-xs font-medium text-sonic-silver uppercase w-[25%]">
|
<th className="py-3 px-4 text-left text-xs font-medium text-sonic-silver w-[15%]">
|
||||||
<div className="flex justify-center items-center">
|
<div className="flex justify-start items-center">
|
||||||
<span className="hidden sm:inline">
|
<span className="hidden sm:inline">
|
||||||
{t('settings.documents.tokenUsage')}
|
{t('settings.documents.tokenUsage')}
|
||||||
</span>
|
</span>
|
||||||
@@ -254,10 +358,8 @@ export default function Documents({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th className="py-3 px-4 text-right text-xs font-medium text-gray-700 dark:text-[#E0E0E0] uppercase w-[10%]">
|
<th className="py-3 px-4 sr-only w-[10%]">
|
||||||
<span className="sr-only">
|
{t('settings.documents.actions')}
|
||||||
{t('settings.documents.actions')}
|
|
||||||
</span>
|
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -274,61 +376,88 @@ export default function Documents({
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
currentDocuments.map((document, index) => (
|
currentDocuments.map((document, index) => {
|
||||||
<tr
|
const docId = document.id ? document.id.toString() : '';
|
||||||
key={index}
|
|
||||||
className="group transition-colors cursor-pointer"
|
return (
|
||||||
onClick={() => setShowDocumentChunks(document)}
|
<tr key={docId} className="group transition-colors">
|
||||||
>
|
<td
|
||||||
<td
|
className="py-4 px-4 text-sm font-semibold text-gray-700 dark:text-[#E0E0E0] min-w-48 max-w-0 truncate group-hover:bg-gray-50 dark:group-hover:bg-gray-800/50"
|
||||||
className="py-4 px-4 text-sm text-gray-700 dark:text-[#E0E0E0] w-[45%] min-w-48 max-w-0 truncate group-hover:bg-gray-50 dark:group-hover:bg-gray-800/50"
|
title={document.name}
|
||||||
title={document.name}
|
>
|
||||||
>
|
{document.name}
|
||||||
{document.name}
|
</td>
|
||||||
</td>
|
<td className="py-4 px-4 text-sm text-gray-700 dark:text-[#E0E0E0] whitespace-nowrap group-hover:bg-gray-50 dark:group-hover:bg-gray-800/50">
|
||||||
<td className="py-4 px-4 text-center text-sm text-gray-700 dark:text-[#E0E0E0] whitespace-nowrap w-[20%] group-hover:bg-gray-50 dark:group-hover:bg-gray-800/50">
|
{document.date ? formatDate(document.date) : ''}
|
||||||
{document.date ? formatDate(document.date) : ''}
|
</td>
|
||||||
</td>
|
<td className="py-4 px-4 text-sm text-gray-700 dark:text-[#E0E0E0] whitespace-nowrap group-hover:bg-gray-50 dark:group-hover:bg-gray-800/50">
|
||||||
<td className="py-4 px-4 text-center text-sm text-gray-700 dark:text-[#E0E0E0] whitespace-nowrap w-[25%] group-hover:bg-gray-50 dark:group-hover:bg-gray-800/50">
|
{document.tokens
|
||||||
{document.tokens
|
? formatTokens(+document.tokens)
|
||||||
? formatTokens(+document.tokens)
|
: ''}
|
||||||
: ''}
|
</td>
|
||||||
</td>
|
<td
|
||||||
<td
|
className="py-4 px-4 text-right group-hover:bg-gray-50 dark:group-hover:bg-gray-800/50"
|
||||||
className="py-4 px-4 text-right w-[10%] group-hover:bg-gray-50 dark:group-hover:bg-gray-800/50"
|
onClick={(e) => e.stopPropagation()}
|
||||||
onClick={(e) => e.stopPropagation()} // Stop event propagation for the entire actions cell
|
>
|
||||||
>
|
<div
|
||||||
<div className="flex items-center justify-end gap-3">
|
ref={getMenuRef(docId)}
|
||||||
{!document.syncFrequency && (
|
className="flex items-center justify-end gap-3 relative"
|
||||||
<div className="w-8"></div>
|
|
||||||
)}
|
|
||||||
{document.syncFrequency && (
|
|
||||||
<DropdownMenu
|
|
||||||
name={t('settings.documents.sync')}
|
|
||||||
options={syncOptions}
|
|
||||||
onSelect={(value: string) => {
|
|
||||||
handleManageSync(document, value);
|
|
||||||
}}
|
|
||||||
defaultValue={document.syncFrequency}
|
|
||||||
icon={SyncIcon}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
handleDeleteConfirmation(index, document);
|
|
||||||
}}
|
|
||||||
className="inline-flex items-center justify-center w-8 h-8 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex-shrink-0"
|
|
||||||
>
|
>
|
||||||
<img
|
{document.syncFrequency && (
|
||||||
src={Trash}
|
<DropdownMenu
|
||||||
alt={t('convTile.delete')}
|
name={t('settings.documents.sync')}
|
||||||
className="h-4 w-4 opacity-60 hover:opacity-100"
|
options={syncOptions}
|
||||||
|
onSelect={(value: string) => {
|
||||||
|
handleManageSync(document, value);
|
||||||
|
}}
|
||||||
|
defaultValue={document.syncFrequency}
|
||||||
|
icon={SyncIcon}
|
||||||
|
isOpen={
|
||||||
|
syncMenuState.docId === docId &&
|
||||||
|
syncMenuState.isOpen
|
||||||
|
}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
setSyncMenuState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
isOpen,
|
||||||
|
docId: isOpen ? docId : null,
|
||||||
|
document: isOpen ? document : null,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
anchorRef={getMenuRef(docId)}
|
||||||
|
position="bottom-left"
|
||||||
|
offset={{ x: 24, y: -24 }}
|
||||||
|
className="min-w-[120px]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={(e) => handleMenuClick(e, docId)}
|
||||||
|
className="inline-flex items-center justify-center w-8 h-8 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex-shrink-0"
|
||||||
|
aria-label="Open menu"
|
||||||
|
data-testid={`menu-button-${docId}`}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={ThreeDots}
|
||||||
|
alt={t('convTile.menu')}
|
||||||
|
className="h-4 w-4 opacity-60 hover:opacity-100"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<ContextMenu
|
||||||
|
isOpen={activeMenuId === docId}
|
||||||
|
setIsOpen={(isOpen) => {
|
||||||
|
setActiveMenuId(isOpen ? docId : null);
|
||||||
|
}}
|
||||||
|
options={getActionOptions(index, document)}
|
||||||
|
anchorRef={getMenuRef(docId)}
|
||||||
|
position="bottom-left"
|
||||||
|
offset={{ x: 48, y: -24 }}
|
||||||
|
className="z-50"
|
||||||
/>
|
/>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
);
|
||||||
))
|
})
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -380,6 +509,7 @@ export default function Documents({
|
|||||||
setDocumentToDelete(null);
|
setDocumentToDelete(null);
|
||||||
}}
|
}}
|
||||||
submitLabel={t('convTile.delete')}
|
submitLabel={t('convTile.delete')}
|
||||||
|
variant="danger"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -533,7 +663,7 @@ function DocumentChunks({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="rounded-full w-full sm:w-40 bg-purple-30 px-4 py-3 text-white hover:bg-[#6F3FD1]"
|
className="rounded-full w-[108px] h-[32px] text-sm bg-purple-30 text-white hover:bg-violets-are-blue flex items-center justify-center"
|
||||||
title={t('settings.documents.addNew')}
|
title={t('settings.documents.addNew')}
|
||||||
onClick={() => setAddModal('ACTIVE')}
|
onClick={() => setAddModal('ACTIVE')}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -84,9 +84,11 @@ export default function General() {
|
|||||||
changeLanguage(selectedLanguage?.value);
|
changeLanguage(selectedLanguage?.value);
|
||||||
}, [selectedLanguage, changeLanguage]);
|
}, [selectedLanguage, changeLanguage]);
|
||||||
return (
|
return (
|
||||||
<div className="mt-12">
|
<div className="mt-12 flex flex-col gap-4">
|
||||||
<div className="mb-5">
|
{' '}
|
||||||
<label className="block mb-2 font-bold text-jet dark:text-bright-gray">
|
<div className="flex flex-col gap-4">
|
||||||
|
{' '}
|
||||||
|
<label className="font-medium text-base text-jet dark:text-bright-gray">
|
||||||
{t('settings.general.selectTheme')}
|
{t('settings.general.selectTheme')}
|
||||||
</label>
|
</label>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -103,8 +105,8 @@ export default function General() {
|
|||||||
border="border"
|
border="border"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-5">
|
<div className="flex flex-col gap-4">
|
||||||
<label className="block mb-2 font-bold text-jet dark:text-bright-gray">
|
<label className="font-medium text-base text-jet dark:text-bright-gray">
|
||||||
{t('settings.general.selectLanguage')}
|
{t('settings.general.selectLanguage')}
|
||||||
</label>
|
</label>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -121,8 +123,8 @@ export default function General() {
|
|||||||
border="border"
|
border="border"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-5">
|
<div className="flex flex-col gap-4">
|
||||||
<label className="block font-bold text-jet dark:text-bright-gray">
|
<label className="font-medium text-base text-jet dark:text-bright-gray">
|
||||||
{t('settings.general.chunks')}
|
{t('settings.general.chunks')}
|
||||||
</label>
|
</label>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -134,8 +136,8 @@ export default function General() {
|
|||||||
border="border"
|
border="border"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-5">
|
<div className="flex flex-col gap-4">
|
||||||
<label className="mb-2 block font-bold text-jet dark:text-bright-gray">
|
<label className="font-medium text-base text-jet dark:text-bright-gray">
|
||||||
{t('settings.general.convHistory')}
|
{t('settings.general.convHistory')}
|
||||||
</label>
|
</label>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -159,7 +161,7 @@ export default function General() {
|
|||||||
border="border"
|
border="border"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-5">
|
<div className="flex flex-col gap-4">
|
||||||
<Prompts
|
<Prompts
|
||||||
prompts={prompts}
|
prompts={prompts}
|
||||||
selectedPrompt={selectedPrompt}
|
selectedPrompt={selectedPrompt}
|
||||||
@@ -169,12 +171,11 @@ export default function General() {
|
|||||||
setPrompts={setPrompts}
|
setPrompts={setPrompts}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-56">
|
<hr className="border-t w-[calc(min(665px,100%))] my-4 border-silver dark:border-silver/40" />
|
||||||
<label className="block font-bold text-jet dark:text-bright-gray">
|
<div className="flex flex-col gap-2">
|
||||||
{t('settings.general.deleteAllLabel')}
|
|
||||||
</label>
|
|
||||||
<button
|
<button
|
||||||
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"
|
title={t('settings.general.deleteAllLabel')}
|
||||||
|
className="flex font-medium text-sm w-fit cursor-pointer items-center justify-between rounded-3xl border border-solid border-rosso-corsa bg-transparent px-5 py-3 text-rosso-corsa transition-colors hover:bg-rosso-corsa hover:text-white hover:font-bold tracking-[0.015em] hover:tracking-normal"
|
||||||
onClick={() => dispatch(setModalStateDeleteConv('ACTIVE'))}
|
onClick={() => dispatch(setModalStateDeleteConv('ACTIVE'))}
|
||||||
>
|
>
|
||||||
{t('settings.general.deleteAllBtn')}
|
{t('settings.general.deleteAllBtn')}
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ export default function Logs() {
|
|||||||
}
|
}
|
||||||
rounded="3xl"
|
rounded="3xl"
|
||||||
border="border"
|
border="border"
|
||||||
|
darkBorderColor="dim-gray"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -127,46 +128,78 @@ type LogsTableProps = {
|
|||||||
setPage: React.Dispatch<React.SetStateAction<number>>;
|
setPage: React.Dispatch<React.SetStateAction<number>>;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function LogsTable({ logs, setPage, loading }: LogsTableProps) {
|
function LogsTable({ logs, setPage, loading }: LogsTableProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const observerRef = useRef<any>();
|
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||||
const firstObserver = useCallback((node: HTMLDivElement) => {
|
const [openLogId, setOpenLogId] = useState<string | null>(null);
|
||||||
if (observerRef.current) {
|
|
||||||
observerRef.current = new IntersectionObserver((entries) => {
|
const handleLogToggle = (logId: string) => {
|
||||||
if (entries[0].isIntersecting) setPage((prev) => prev + 1);
|
if (openLogId && openLogId !== logId) {
|
||||||
});
|
// If a different log is being opened, close the current one
|
||||||
|
const currentOpenLog = document.getElementById(
|
||||||
|
openLogId,
|
||||||
|
) as HTMLDetailsElement;
|
||||||
|
if (currentOpenLog) {
|
||||||
|
currentOpenLog.open = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (node && observerRef.current) observerRef.current.observe(node);
|
setOpenLogId(logId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const firstObserver = useCallback((node: HTMLDivElement | null) => {
|
||||||
|
if (observerRef.current) {
|
||||||
|
observerRef.current.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
observerRef.current = new IntersectionObserver((entries) => {
|
||||||
|
if (entries[0].isIntersecting) {
|
||||||
|
setPage((prev) => prev + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observerRef.current.observe(node);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (observerRef.current) {
|
||||||
|
observerRef.current.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="logs-table border rounded-2xl h-[55vh] w-full overflow-hidden border-silver dark:border-silver/40">
|
<div className="logs-table rounded-xl h-[55vh] w-full overflow-hidden bg-white dark:bg-black border border-light-silver dark:border-transparent">
|
||||||
<div className="h-8 bg-black/10 dark:bg-chinese-black flex flex-col items-start justify-center">
|
<div className="h-8 bg-black/10 dark:bg-[#191919] flex flex-col items-start justify-center">
|
||||||
<p className="px-3 text-xs dark:text-gray-6000">
|
<p className="px-3 text-xs dark:text-gray-6000">
|
||||||
{t('settings.logs.tableHeader')}
|
{t('settings.logs.tableHeader')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="flex flex-col items-start h-[51vh] overflow-y-auto bg-transparent flex-grow gap-2 p-4">
|
||||||
ref={observerRef}
|
|
||||||
className="flex flex-col items-start h-[51vh] overflow-y-auto bg-transparent flex-grow gap-px"
|
|
||||||
>
|
|
||||||
{logs?.map((log, index) => {
|
{logs?.map((log, index) => {
|
||||||
if (index === logs.length - 1) {
|
if (index === logs.length - 1) {
|
||||||
return (
|
return (
|
||||||
<div ref={firstObserver} key={index} className="w-full">
|
<div ref={firstObserver} key={index} className="w-full">
|
||||||
<Log log={log} />
|
<Log log={log} onToggle={handleLogToggle} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else return <Log key={index} log={log} />;
|
} else
|
||||||
|
return <Log key={index} log={log} onToggle={handleLogToggle} />;
|
||||||
})}
|
})}
|
||||||
{loading && <SkeletonLoader component="logs" />}
|
{loading && <SkeletonLoader component="logs" />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
function Log({
|
||||||
function Log({ log }: { log: LogData }) {
|
log,
|
||||||
|
onToggle,
|
||||||
|
}: {
|
||||||
|
log: LogData;
|
||||||
|
onToggle: (id: string) => void;
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const logLevelColor = {
|
const logLevelColor = {
|
||||||
info: 'text-green-500',
|
info: 'text-green-500',
|
||||||
@@ -174,9 +207,18 @@ function Log({ log }: { log: LogData }) {
|
|||||||
warning: 'text-yellow-500',
|
warning: 'text-yellow-500',
|
||||||
};
|
};
|
||||||
const { id, action, timestamp, ...filteredLog } = log;
|
const { id, action, timestamp, ...filteredLog } = log;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<details className="group bg-transparent [&_summary::-webkit-details-marker]:hidden w-full hover:bg-[#F9F9F9] hover:dark:bg-dark-charcoal">
|
<details
|
||||||
<summary className="flex flex-row items-start gap-2 text-gray-900 cursor-pointer p-2 group-open:bg-[#F9F9F9] dark:group-open:bg-dark-charcoal">
|
id={log.id}
|
||||||
|
className="group bg-transparent [&_summary::-webkit-details-marker]:hidden w-full hover:bg-[#F9F9F9] hover:dark:bg-dark-charcoal rounded-xl group-open:opacity-80 [&[open]]:border [&[open]]:border-[#d9d9d9]"
|
||||||
|
onToggle={(e) => {
|
||||||
|
if ((e.target as HTMLDetailsElement).open) {
|
||||||
|
onToggle(log.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<summary className="flex flex-row items-start gap-2 text-gray-900 cursor-pointer px-4 py-3 group-open:bg-[#F1F1F1] dark:group-open:bg-[#1B1B1B] group-open:rounded-t-xl p-2">
|
||||||
<img
|
<img
|
||||||
src={ChevronRight}
|
src={ChevronRight}
|
||||||
alt="Expand log entry"
|
alt="Expand log entry"
|
||||||
@@ -194,14 +236,15 @@ function Log({ log }: { log: LogData }) {
|
|||||||
</h2>
|
</h2>
|
||||||
</span>
|
</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div className="px-4 group-open:bg-[#F9F9F9] dark:group-open:bg-dark-charcoal">
|
<div className="px-4 py-3 group-open:bg-[#F1F1F1] dark:group-open:bg-[#1B1B1B] group-open:rounded-b-xl">
|
||||||
<p className="px-2 leading-relaxed text-gray-700 dark:text-gray-400 text-xs break-words">
|
<p className="px-2 leading-relaxed text-gray-700 dark:text-gray-400 text-xs break-words">
|
||||||
{JSON.stringify(filteredLog, null, 2)}
|
{JSON.stringify(filteredLog, null, 2)}
|
||||||
</p>
|
</p>
|
||||||
<div className="my-px w-8">
|
<div className="my-px w-fit">
|
||||||
<CopyButton
|
<CopyButton
|
||||||
text={JSON.stringify(filteredLog)}
|
text={JSON.stringify(filteredLog)}
|
||||||
colorLight="transparent"
|
colorLight="transparent"
|
||||||
|
showText={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,10 +38,7 @@ export default function Prompts({
|
|||||||
});
|
});
|
||||||
const [modalType, setModalType] = React.useState<'ADD' | 'EDIT'>('ADD');
|
const [modalType, setModalType] = React.useState<'ADD' | 'EDIT'>('ADD');
|
||||||
const [modalState, setModalState] = React.useState<ActiveState>('INACTIVE');
|
const [modalState, setModalState] = React.useState<ActiveState>('INACTIVE');
|
||||||
const {
|
const { t } = useTranslation();
|
||||||
t,
|
|
||||||
i18n: { changeLanguage, language },
|
|
||||||
} = useTranslation();
|
|
||||||
|
|
||||||
const handleAddPrompt = async () => {
|
const handleAddPrompt = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -144,11 +141,11 @@ export default function Prompts({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-row items-center gap-8">
|
<div className="flex flex-col gap-4">
|
||||||
<div>
|
<p className="font-medium dark:text-bright-gray">
|
||||||
<p className="font-semibold dark:text-bright-gray">
|
{t('settings.general.prompt')}
|
||||||
{t('settings.general.prompt')}
|
</p>
|
||||||
</p>
|
<div className="flex flex-row justify-start items-baseline gap-6">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
options={prompts}
|
options={prompts}
|
||||||
selectedValue={selectedPrompt.name}
|
selectedValue={selectedPrompt.name}
|
||||||
@@ -175,16 +172,17 @@ export default function Prompts({
|
|||||||
}}
|
}}
|
||||||
onDelete={handleDeletePrompt}
|
onDelete={handleDeletePrompt}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="rounded-3xl w-20 h-10 text-sm border border-solid border-violets-are-blue text-violets-are-blue transition-colors hover:text-white hover:bg-violets-are-blue"
|
||||||
|
onClick={() => {
|
||||||
|
setModalType('ADD');
|
||||||
|
setModalState('ACTIVE');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('settings.general.add')}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
className="mt-[24px] rounded-3xl border border-solid border-purple-30 px-5 py-3 text-purple-30 transition-colors hover:text-white hover:bg-[#6F3FD1] dark:border-purple-30 dark:text-purple-30 dark:hover:bg-purple-30 dark:hover:text-white"
|
|
||||||
onClick={() => {
|
|
||||||
setModalType('ADD');
|
|
||||||
setModalState('ACTIVE');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('settings.general.addNew')}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PromptsModal
|
<PromptsModal
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ import CircleX from '../assets/circle-x.svg';
|
|||||||
import Trash from '../assets/trash.svg';
|
import Trash from '../assets/trash.svg';
|
||||||
import Dropdown from '../components/Dropdown';
|
import Dropdown from '../components/Dropdown';
|
||||||
import Input from '../components/Input';
|
import Input from '../components/Input';
|
||||||
|
import ToggleSwitch from '../components/ToggleSwitch';
|
||||||
import AddActionModal from '../modals/AddActionModal';
|
import AddActionModal from '../modals/AddActionModal';
|
||||||
import { ActiveState } from '../models/misc';
|
import { ActiveState } from '../models/misc';
|
||||||
import { selectToken } from '../preferences/preferenceSlice';
|
import { selectToken } from '../preferences/preferenceSlice';
|
||||||
import { APIActionType, APIToolType, UserToolType } from './types';
|
import { APIActionType, APIToolType, UserToolType } from './types';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export default function ToolConfig({
|
export default function ToolConfig({
|
||||||
tool,
|
tool,
|
||||||
@@ -28,7 +30,7 @@ export default function ToolConfig({
|
|||||||
);
|
);
|
||||||
const [actionModalState, setActionModalState] =
|
const [actionModalState, setActionModalState] =
|
||||||
React.useState<ActiveState>('INACTIVE');
|
React.useState<ActiveState>('INACTIVE');
|
||||||
|
const { t } = useTranslation();
|
||||||
const handleCheckboxChange = (actionIndex: number, property: string) => {
|
const handleCheckboxChange = (actionIndex: number, property: string) => {
|
||||||
setTool({
|
setTool({
|
||||||
...tool,
|
...tool,
|
||||||
@@ -140,21 +142,18 @@ export default function ToolConfig({
|
|||||||
{Object.keys(tool?.config).length !== 0 &&
|
{Object.keys(tool?.config).length !== 0 &&
|
||||||
tool.name !== 'api_tool' && (
|
tool.name !== 'api_tool' && (
|
||||||
<div className="relative w-96">
|
<div className="relative w-96">
|
||||||
<span className="z-10 absolute left-5 -top-2 bg-white px-2 text-xs text-gray-4000 dark:bg-[#26272E] dark:text-silver">
|
|
||||||
API Key / Oauth
|
|
||||||
</span>
|
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={authKey}
|
value={authKey}
|
||||||
onChange={(e) => setAuthKey(e.target.value)}
|
onChange={(e) => setAuthKey(e.target.value)}
|
||||||
borderVariant="thin"
|
borderVariant="thin"
|
||||||
placeholder="Enter API Key / Oauth"
|
placeholder={t('modals.configTool.apiKeyPlaceholder')}
|
||||||
></Input>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
className="rounded-full px-5 py-[10px] bg-purple-30 text-white hover:bg-[#6F3FD1] text-nowrap text-sm"
|
className="rounded-full px-5 py-[10px] bg-purple-30 text-white hover:bg-violets-are-blue text-nowrap text-sm"
|
||||||
onClick={handleSaveChanges}
|
onClick={handleSaveChanges}
|
||||||
>
|
>
|
||||||
Save changes
|
Save changes
|
||||||
@@ -178,7 +177,7 @@ export default function ToolConfig({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActionModalState('ACTIVE');
|
setActionModalState('ACTIVE');
|
||||||
}}
|
}}
|
||||||
className="border border-solid border-purple-30 text-purple-30 dark:border-purple-30 dark:text-purple-30 transition-colors hover:bg-[#6F3FD1] hover:text-white dark:hover:bg-purple-30 dark:hover:text-white rounded-full text-sm px-5 py-1"
|
className="border border-solid border-violets-are-blue text-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white rounded-full text-sm px-5 py-1"
|
||||||
>
|
>
|
||||||
Add action
|
Add action
|
||||||
</button>
|
</button>
|
||||||
@@ -198,33 +197,27 @@ export default function ToolConfig({
|
|||||||
<p className="font-semibold text-eerie-black dark:text-bright-gray">
|
<p className="font-semibold text-eerie-black dark:text-bright-gray">
|
||||||
{action.name}
|
{action.name}
|
||||||
</p>
|
</p>
|
||||||
<label
|
<ToggleSwitch
|
||||||
htmlFor={`actionToggle-${actionIndex}`}
|
checked={action.active}
|
||||||
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]"
|
onChange={(checked) => {
|
||||||
>
|
setTool({
|
||||||
<input
|
...tool,
|
||||||
type="checkbox"
|
actions: tool.actions.map((act, index) => {
|
||||||
id={`actionToggle-${actionIndex}`}
|
if (index === actionIndex) {
|
||||||
className="peer sr-only"
|
return { ...act, active: checked };
|
||||||
checked={action.active}
|
}
|
||||||
onChange={() => {
|
return act;
|
||||||
setTool({
|
}),
|
||||||
...tool,
|
});
|
||||||
actions: tool.actions.map((act, index) => {
|
}}
|
||||||
if (index === actionIndex) {
|
size="small"
|
||||||
return { ...act, active: !act.active };
|
id={`actionToggle-${actionIndex}`}
|
||||||
}
|
/>
|
||||||
return act;
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="absolute inset-y-0 start-0 m-[3px] size-[18px] rounded-full bg-white transition-all peer-checked:start-4"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 relative px-5 w-full sm:w-96">
|
<div className="mt-5 relative">
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
|
className="w-[97%] ml-5"
|
||||||
placeholder="Enter description"
|
placeholder="Enter description"
|
||||||
value={action.description}
|
value={action.description}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -242,7 +235,7 @@ export default function ToolConfig({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
borderVariant="thin"
|
borderVariant="thin"
|
||||||
></Input>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
<table className="table-default">
|
<table className="table-default">
|
||||||
@@ -437,19 +430,12 @@ function APIToolConfig({
|
|||||||
<p className="font-semibold text-eerie-black dark:text-bright-gray">
|
<p className="font-semibold text-eerie-black dark:text-bright-gray">
|
||||||
{action.name}
|
{action.name}
|
||||||
</p>
|
</p>
|
||||||
<label
|
<ToggleSwitch
|
||||||
htmlFor={`actionToggle-${actionIndex}`}
|
checked={action.active}
|
||||||
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]"
|
onChange={() => handleActionToggle(actionName)}
|
||||||
>
|
size="small"
|
||||||
<input
|
id={`actionToggle-${actionIndex}`}
|
||||||
type="checkbox"
|
/>
|
||||||
id={`actionToggle-${actionIndex}`}
|
|
||||||
className="peer sr-only"
|
|
||||||
checked={action.active}
|
|
||||||
onChange={() => handleActionToggle(actionName)}
|
|
||||||
/>
|
|
||||||
<span className="absolute inset-y-0 start-0 m-[3px] size-[18px] rounded-full bg-white transition-all peer-checked:start-4"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 px-5">
|
<div className="mt-8 px-5">
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
@@ -844,7 +830,7 @@ function APIActionTable({
|
|||||||
<td colSpan={4} className="text-right">
|
<td colSpan={4} className="text-right">
|
||||||
<button
|
<button
|
||||||
onClick={handleAddProperty}
|
onClick={handleAddProperty}
|
||||||
className="bg-purple-30 text-white hover:bg-[#6F3FD1] rounded-full px-5 py-[4px] mr-1 text-sm"
|
className="bg-purple-30 text-white hover:bg-violets-are-blue rounded-full px-5 py-[4px] mr-1 text-sm"
|
||||||
>
|
>
|
||||||
{' '}
|
{' '}
|
||||||
Add{' '}
|
Add{' '}
|
||||||
@@ -871,7 +857,7 @@ function APIActionTable({
|
|||||||
<td colSpan={5}>
|
<td colSpan={5}>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleAddPropertyStart(section)}
|
onClick={() => handleAddPropertyStart(section)}
|
||||||
className="flex items-start rounded-full px-5 py-[4px] border border-solid border-purple-30 text-purple-30 dark:border-purple-30 dark:text-purple-30 transition-colors hover:bg-[#6F3FD1] hover:text-white dark:hover:bg-purple-30 dark:hover:text-white text-nowrap text-sm"
|
className="flex items-start rounded-full px-5 py-[4px] border border-solid text-violets-are-blue border-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white text-nowrap text-sm"
|
||||||
>
|
>
|
||||||
Add New Field
|
Add New Field
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -4,16 +4,14 @@ import { useSelector } from 'react-redux';
|
|||||||
|
|
||||||
import userService from '../api/services/userService';
|
import userService from '../api/services/userService';
|
||||||
import CogwheelIcon from '../assets/cogwheel.svg';
|
import CogwheelIcon from '../assets/cogwheel.svg';
|
||||||
import NoFilesDarkIcon from '../assets/no-files-dark.svg';
|
|
||||||
import NoFilesIcon from '../assets/no-files.svg';
|
|
||||||
import Input from '../components/Input';
|
import Input from '../components/Input';
|
||||||
import Spinner from '../components/Spinner';
|
import Spinner from '../components/Spinner';
|
||||||
import { useDarkTheme } from '../hooks';
|
|
||||||
import AddToolModal from '../modals/AddToolModal';
|
import AddToolModal from '../modals/AddToolModal';
|
||||||
import { ActiveState } from '../models/misc';
|
import { ActiveState } from '../models/misc';
|
||||||
import { selectToken } from '../preferences/preferenceSlice';
|
import { selectToken } from '../preferences/preferenceSlice';
|
||||||
import ToolConfig from './ToolConfig';
|
import ToolConfig from './ToolConfig';
|
||||||
import { APIToolType, UserToolType } from './types';
|
import { APIToolType, UserToolType } from './types';
|
||||||
|
import ToggleSwitch from '../components/ToggleSwitch';
|
||||||
|
|
||||||
export default function Tools() {
|
export default function Tools() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -117,7 +115,7 @@ export default function Tools() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="rounded-full min-w-[160px] bg-purple-30 px-6 py-3 text-white hover:bg-[#6F3FD1] text-nowrap"
|
className="rounded-full w-[108px] h-[30px] text-sm bg-purple-30 text-white hover:bg-violets-are-blue flex items-center justify-center"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAddToolModalState('ACTIVE');
|
setAddToolModalState('ACTIVE');
|
||||||
}}
|
}}
|
||||||
@@ -125,6 +123,7 @@ export default function Tools() {
|
|||||||
{t('settings.tools.addTool')}
|
{t('settings.tools.addTool')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="border-b border-light-silver dark:border-dim-gray mb-8 mt-5" />
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<div className="mt-24 h-32 col-span-2 lg:col-span-3 flex items-center justify-center">
|
<div className="mt-24 h-32 col-span-2 lg:col-span-3 flex items-center justify-center">
|
||||||
@@ -132,90 +131,66 @@ export default function Tools() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="flex flex-wrap gap-4 justify-center sm:justify-start">
|
||||||
{userTools.filter((tool) =>
|
{userTools
|
||||||
tool.displayName
|
.filter((tool) =>
|
||||||
.toLowerCase()
|
tool.displayName
|
||||||
.includes(searchTerm.toLowerCase()),
|
.toLowerCase()
|
||||||
).length === 0 ? (
|
.includes(searchTerm.toLowerCase()),
|
||||||
<div className="mt-24 col-span-2 lg:col-span-3 text-center text-gray-500 dark:text-gray-400">
|
)
|
||||||
<img
|
.map((tool, index) => (
|
||||||
src={isDarkTheme ? NoFilesDarkIcon : NoFilesIcon}
|
<div
|
||||||
alt="No tools found"
|
key={index}
|
||||||
className="h-24 w-24 mx-auto mb-2"
|
className="h-52 w-[300px] p-6 border rounded-2xl border-light-gainsboro dark:border-arsenic bg-white-3000 dark:bg-transparent flex flex-col justify-between relative"
|
||||||
/>
|
>
|
||||||
{t('settings.tools.noToolsFound')}
|
<button
|
||||||
</div>
|
onClick={() => handleSettingsClick(tool)}
|
||||||
) : (
|
aria-label={t('settings.tools.configureToolAria', {
|
||||||
userTools
|
toolName: tool.displayName,
|
||||||
.filter((tool) =>
|
})}
|
||||||
tool.displayName
|
className="absolute top-4 right-4"
|
||||||
.toLowerCase()
|
|
||||||
.includes(searchTerm.toLowerCase()),
|
|
||||||
)
|
|
||||||
.map((tool, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="relative h-56 w-full p-6 border rounded-2xl border-silver dark:border-silver/40 flex flex-col justify-between"
|
|
||||||
>
|
>
|
||||||
<div className="w-full">
|
<img
|
||||||
<div className="w-full flex items-center justify-between">
|
src={CogwheelIcon}
|
||||||
<img
|
alt={t('settings.tools.settingsIconAlt')}
|
||||||
src={`/toolIcons/tool_${tool.name}.svg`}
|
className="h-[19px] w-[19px]"
|
||||||
alt={`${tool.displayName} icon`}
|
/>
|
||||||
className="h-8 w-8"
|
</button>
|
||||||
/>
|
<div className="w-full">
|
||||||
<button
|
<div className="px-1 w-full flex items-center">
|
||||||
className="absolute top-3 right-3 cursor-pointer"
|
<img
|
||||||
onClick={() => handleSettingsClick(tool)}
|
src={`/toolIcons/tool_${tool.name}.svg`}
|
||||||
aria-label={t(
|
alt={`${tool.displayName} icon`}
|
||||||
'settings.tools.configureToolAria',
|
className="h-6 w-6"
|
||||||
{
|
/>
|
||||||
toolName: tool.displayName,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={CogwheelIcon}
|
|
||||||
alt={t('settings.tools.settingsIconAlt')}
|
|
||||||
className="h-[19px] w-[19px]"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="mt-[9px]">
|
|
||||||
<p className="text-sm font-semibold text-eerie-black dark:text-[#EEEEEE] leading-relaxed">
|
|
||||||
{tool.displayName}
|
|
||||||
</p>
|
|
||||||
<p className="mt-1 h-16 overflow-auto text-[13px] text-gray-600 dark:text-gray-400 leading-relaxed pr-1">
|
|
||||||
{tool.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute bottom-3 right-3">
|
<div className="mt-[9px]">
|
||||||
<label
|
<p
|
||||||
htmlFor={`toolToggle-${index}`}
|
title={tool.displayName}
|
||||||
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="px-1 text-[13px] font-semibold text-raisin-black-light dark:text-bright-gray leading-relaxed capitalize truncate"
|
||||||
>
|
>
|
||||||
<span className="sr-only">
|
{tool.displayName}
|
||||||
{t('settings.tools.toggleToolAria', {
|
</p>
|
||||||
toolName: tool.displayName,
|
<p className="mt-1 px-1 h-24 overflow-auto text-[12px] text-old-silver dark:text-sonic-silver-light leading-relaxed">
|
||||||
})}
|
{tool.description}
|
||||||
</span>
|
</p>
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id={`toolToggle-${index}`}
|
|
||||||
className="peer sr-only"
|
|
||||||
checked={tool.status}
|
|
||||||
onChange={() =>
|
|
||||||
updateToolStatus(tool.id, !tool.status)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<span className="absolute inset-y-0 start-0 m-[3px] size-[18px] rounded-full bg-white transition-all peer-checked:start-4"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
<div className="absolute bottom-4 right-4">
|
||||||
)}
|
<ToggleSwitch
|
||||||
|
checked={tool.status}
|
||||||
|
onChange={(checked) =>
|
||||||
|
updateToolStatus(tool.id, checked)
|
||||||
|
}
|
||||||
|
size="small"
|
||||||
|
id={`toolToggle-${index}`}
|
||||||
|
ariaLabel={t('settings.tools.toggleToolAria', {
|
||||||
|
toolName: tool.displayName,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -101,9 +101,9 @@ function Upload({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
borderVariant="thin"
|
borderVariant="thin"
|
||||||
label={field.label}
|
|
||||||
required={isRequired}
|
required={isRequired}
|
||||||
colorVariant="silver"
|
colorVariant="silver"
|
||||||
|
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'number':
|
case 'number':
|
||||||
@@ -123,9 +123,9 @@ function Upload({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
borderVariant="thin"
|
borderVariant="thin"
|
||||||
label={field.label}
|
|
||||||
required={isRequired}
|
required={isRequired}
|
||||||
colorVariant="silver"
|
colorVariant="silver"
|
||||||
|
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'enum':
|
case 'enum':
|
||||||
@@ -612,7 +612,7 @@ function Upload({
|
|||||||
onChange={(e) => setDocName(e.target.value)}
|
onChange={(e) => setDocName(e.target.value)}
|
||||||
borderVariant="thin"
|
borderVariant="thin"
|
||||||
placeholder={t('modals.uploadDoc.name')}
|
placeholder={t('modals.uploadDoc.name')}
|
||||||
label={t('modals.uploadDoc.name')}
|
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||||
required={true}
|
required={true}
|
||||||
/>
|
/>
|
||||||
<div className="my-2" {...getRootProps()}>
|
<div className="my-2" {...getRootProps()}>
|
||||||
@@ -659,6 +659,7 @@ function Upload({
|
|||||||
handleIngestorTypeChange(selected.value as IngestorType)
|
handleIngestorTypeChange(selected.value as IngestorType)
|
||||||
}
|
}
|
||||||
size="w-full"
|
size="w-full"
|
||||||
|
darkBorderColor="dim-gray"
|
||||||
rounded="3xl"
|
rounded="3xl"
|
||||||
/>
|
/>
|
||||||
{/* Dynamically render form fields based on schema */}
|
{/* Dynamically render form fields based on schema */}
|
||||||
@@ -670,8 +671,8 @@ function Upload({
|
|||||||
onChange={(e) => setRemoteName(e.target.value)}
|
onChange={(e) => setRemoteName(e.target.value)}
|
||||||
borderVariant="thin"
|
borderVariant="thin"
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
label="Name"
|
|
||||||
required={true}
|
required={true}
|
||||||
|
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||||
/>
|
/>
|
||||||
{renderFormFields()}
|
{renderFormFields()}
|
||||||
{IngestorFormSchemas[ingestor.type].some(
|
{IngestorFormSchemas[ingestor.type].some(
|
||||||
@@ -710,7 +711,7 @@ function Upload({
|
|||||||
className={`rounded-3xl px-4 py-2 font-medium text-[14px] ${
|
className={`rounded-3xl px-4 py-2 font-medium text-[14px] ${
|
||||||
isUploadDisabled()
|
isUploadDisabled()
|
||||||
? 'cursor-not-allowed bg-gray-300 text-gray-500'
|
? 'cursor-not-allowed bg-gray-300 text-gray-500'
|
||||||
: 'cursor-pointer bg-purple-30 text-white hover:bg-purple-40'
|
: 'cursor-pointer bg-purple-30 text-white hover:bg-violets-are-blue'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{t('modals.uploadDoc.train')}
|
{t('modals.uploadDoc.train')}
|
||||||
|
|||||||
@@ -1,19 +1,39 @@
|
|||||||
export function formatDate(dateString: string): string {
|
export function formatDate(dateString: string): string {
|
||||||
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(dateString)) {
|
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/.test(dateString)) {
|
||||||
|
// ISO 8601 format
|
||||||
const dateTime = new Date(dateString);
|
const dateTime = new Date(dateString);
|
||||||
return dateTime.toLocaleTimeString([], {
|
return dateTime.toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
} else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(dateString)) {
|
||||||
|
const dateTime = new Date(dateString);
|
||||||
|
return dateTime.toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
});
|
});
|
||||||
} else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(dateString)) {
|
} else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(dateString)) {
|
||||||
const dateTime = new Date(dateString);
|
const dateTime = new Date(dateString);
|
||||||
return dateTime.toLocaleTimeString([], {
|
return dateTime.toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
});
|
});
|
||||||
} else if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
|
} else if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
return date.toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
return dateString;
|
return dateString;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ module.exports = {
|
|||||||
colors: {
|
colors: {
|
||||||
'eerie-black': '#212121',
|
'eerie-black': '#212121',
|
||||||
'black-1000': '#343541',
|
'black-1000': '#343541',
|
||||||
jet: '#343541',
|
'jet': '#343541',
|
||||||
'gray-alpha': 'rgba(0,0,0, .64)',
|
'gray-alpha': 'rgba(0,0,0, .64)',
|
||||||
'gray-1000': '#F6F6F6',
|
'gray-1000': '#F6F6F6',
|
||||||
'gray-2000': 'rgba(0, 0, 0, 0.5)',
|
'gray-2000': 'rgba(0, 0, 0, 0.5)',
|
||||||
@@ -32,23 +32,54 @@ module.exports = {
|
|||||||
'green-2000': '#0FFF50',
|
'green-2000': '#0FFF50',
|
||||||
'light-gray': '#edeef0',
|
'light-gray': '#edeef0',
|
||||||
'white-3000': '#ffffff',
|
'white-3000': '#ffffff',
|
||||||
'just-black':"#00000",
|
'just-black': '#00000',
|
||||||
'purple-taupe':'#464152',
|
'purple-taupe': '#464152',
|
||||||
'dove-gray': '#6c6c6c',
|
'dove-gray': '#6c6c6c',
|
||||||
'silver': '#c4c4c4',
|
'silver': '#c4c4c4',
|
||||||
'rainy-gray': '#a4a4a4',
|
'rainy-gray': '#a4a4a4',
|
||||||
'raisin-black':'#222327',
|
'raisin-black': '#222327',
|
||||||
'chinese-black':'#161616',
|
'chinese-black': '#161616',
|
||||||
'chinese-silver':'#CDCDCD',
|
'chinese-silver': '#CDCDCD',
|
||||||
'dark-charcoal':'#2F3036',
|
'dark-charcoal': '#2F3036',
|
||||||
'bright-gray':'#ECECF1',
|
'bright-gray': '#ECECF1',
|
||||||
'outer-space':'#444654',
|
'outer-space': '#444654',
|
||||||
'gun-metal':'#2E303E',
|
'gun-metal': '#2E303E',
|
||||||
'sonic-silver':'#747474',
|
'sonic-silver': '#747474',
|
||||||
'soap':'#D8CCF1',
|
'soap': '#D8CCF1',
|
||||||
'independence':'#54546D',
|
'independence': '#54546D',
|
||||||
'philippine-yellow':'#FFC700',
|
'philippine-yellow': '#FFC700',
|
||||||
'bright-gray':'#EBEBEB'
|
'bright-gray': '#EBEBEB',
|
||||||
|
'chinese-white': '#e0e0e0',
|
||||||
|
'dark-gray': '#aaaaaa',
|
||||||
|
'dim-gray': '#6A6A6A',
|
||||||
|
'cultured': '#f4f4f4',
|
||||||
|
'charleston-green': '#2b2c31',
|
||||||
|
'charleston-green-2' : '#26272e',
|
||||||
|
'charleston-green-3':'#26272A',
|
||||||
|
'grey': '#7e7e7e',
|
||||||
|
'lotion': '#FBFBFB',
|
||||||
|
'platinum': '#e6e6e6',
|
||||||
|
'eerie-black-2': '#191919',
|
||||||
|
'light-silver': '#D9D9D9',
|
||||||
|
'carbon': '#2E2E2E',
|
||||||
|
'onyx':'#35363B',
|
||||||
|
'royal-purple': '#6C4AB0',
|
||||||
|
'chinese-black-2': '#0F1419',
|
||||||
|
'gainsboro': '#D9DCDE',
|
||||||
|
'onyx-2': '#35383C',
|
||||||
|
'philippine-grey': '#929292',
|
||||||
|
'charcoal-grey':'#53545D',
|
||||||
|
'rosso-corsa': '#D30000',
|
||||||
|
'north-texas-green': '#0C9D35',
|
||||||
|
'medium-purple': '#8d66dd',
|
||||||
|
'slate-blue': '#6f5fca',
|
||||||
|
'old-silver': '#848484',
|
||||||
|
'arsenic': '#4D4E58',
|
||||||
|
'light-gainsboro': '#d7D7D7',
|
||||||
|
'raisin-black-light': '#18181B',
|
||||||
|
'gunmetal': '#32333B',
|
||||||
|
'sonic-silver-light': '#7f7f82',
|
||||||
|
'violets-are-blue':'#976AF3'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
17
setup.sh
17
setup.sh
@@ -148,6 +148,7 @@ prompt_cloud_api_provider_options() {
|
|||||||
echo -e "${YELLOW}4) Groq${NC}"
|
echo -e "${YELLOW}4) Groq${NC}"
|
||||||
echo -e "${YELLOW}5) HuggingFace Inference API${NC}"
|
echo -e "${YELLOW}5) HuggingFace Inference API${NC}"
|
||||||
echo -e "${YELLOW}6) Azure OpenAI${NC}"
|
echo -e "${YELLOW}6) Azure OpenAI${NC}"
|
||||||
|
echo -e "${YELLOW}7) Novita${NC}"
|
||||||
echo -e "${YELLOW}b) Back to Main Menu${NC}"
|
echo -e "${YELLOW}b) Back to Main Menu${NC}"
|
||||||
echo
|
echo
|
||||||
read -p "$(echo -e "${DEFAULT_FG}Choose option (1-6, or b): ${NC}")" provider_choice
|
read -p "$(echo -e "${DEFAULT_FG}Choose option (1-6, or b): ${NC}")" provider_choice
|
||||||
@@ -428,6 +429,12 @@ connect_cloud_api_provider() {
|
|||||||
model_name="gpt-4o"
|
model_name="gpt-4o"
|
||||||
get_api_key
|
get_api_key
|
||||||
break ;;
|
break ;;
|
||||||
|
7) # Novita
|
||||||
|
provider_name="Novita"
|
||||||
|
llm_name="novita"
|
||||||
|
model_name="deepseek/deepseek-r1"
|
||||||
|
get_api_key
|
||||||
|
break ;;
|
||||||
b|B) clear; return ;; # Clear screen and Back to Main Menu
|
b|B) clear; return ;; # Clear screen and Back to Main Menu
|
||||||
*) echo -e "\n${RED}Invalid choice. Please choose 1-6, or b.${NC}" ; sleep 1 ;;
|
*) echo -e "\n${RED}Invalid choice. Please choose 1-6, or b.${NC}" ; sleep 1 ;;
|
||||||
esac
|
esac
|
||||||
@@ -443,7 +450,7 @@ connect_cloud_api_provider() {
|
|||||||
check_and_start_docker
|
check_and_start_docker
|
||||||
|
|
||||||
echo -e "\n${NC}Starting Docker Compose...${NC}"
|
echo -e "\n${NC}Starting Docker Compose...${NC}"
|
||||||
docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" build && docker compose -f "${COMPOSE_FILE}" up -d
|
docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" up -d --build
|
||||||
docker_compose_status=$?
|
docker_compose_status=$?
|
||||||
|
|
||||||
echo "Docker Compose Exit Status: $docker_compose_status" # Debug output
|
echo "Docker Compose Exit Status: $docker_compose_status" # Debug output
|
||||||
@@ -469,16 +476,16 @@ while true; do # Main menu loop
|
|||||||
case $main_choice in
|
case $main_choice in
|
||||||
1) # Use DocsGPT Public API Endpoint
|
1) # Use DocsGPT Public API Endpoint
|
||||||
use_docs_public_api_endpoint
|
use_docs_public_api_endpoint
|
||||||
;;
|
break ;;
|
||||||
2) # Serve Local (with Ollama)
|
2) # Serve Local (with Ollama)
|
||||||
serve_local_ollama
|
serve_local_ollama
|
||||||
;;
|
break ;;
|
||||||
3) # Connect Local Inference Engine
|
3) # Connect Local Inference Engine
|
||||||
connect_local_inference_engine
|
connect_local_inference_engine
|
||||||
;;
|
break ;;
|
||||||
4) # Connect Cloud API Provider
|
4) # Connect Cloud API Provider
|
||||||
connect_cloud_api_provider
|
connect_cloud_api_provider
|
||||||
;;
|
break ;;
|
||||||
*)
|
*)
|
||||||
echo -e "\n${RED}Invalid choice. Please choose 1-4.${NC}" ; sleep 1 ;;
|
echo -e "\n${RED}Invalid choice. Please choose 1-4.${NC}" ; sleep 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -64,7 +64,5 @@ class TestAnthropicLLM(unittest.TestCase):
|
|||||||
max_tokens_to_sample=300,
|
max_tokens_to_sample=300,
|
||||||
stream=True
|
stream=True
|
||||||
)
|
)
|
||||||
mock_redis_instance.set.assert_called_once()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user