Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
2037848b4e build(deps): bump packaging from 24.2 to 25.0 in /application
Bumps [packaging](https://github.com/pypa/packaging) from 24.2 to 25.0.
- [Release notes](https://github.com/pypa/packaging/releases)
- [Changelog](https://github.com/pypa/packaging/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pypa/packaging/compare/24.2...25.0)

---
updated-dependencies:
- dependency-name: packaging
  dependency-version: '25.0'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 20:54:56 +00:00
89 changed files with 3821 additions and 7795 deletions

View File

@@ -1,95 +1,33 @@
import os
from typing import Dict, Generator, List, Any
import logging
from typing import Dict, Generator, List
from application.agents.base import BaseAgent
from application.logging import build_stack_data, LogContext
from application.retriever.base import BaseRetriever
logger = logging.getLogger(__name__)
current_dir = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
with open(
os.path.join(current_dir, "application/prompts", "react_planning_prompt.txt"), "r"
) as f:
planning_prompt_template = f.read()
planning_prompt = f.read()
with open(
os.path.join(current_dir, "application/prompts", "react_final_prompt.txt"),
"r",
) as f:
final_prompt_template = f.read()
MAX_ITERATIONS_REASONING = 10
final_prompt = f.read()
class ReActAgent(BaseAgent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.plan: str = ""
self.plan = ""
self.observations: List[str] = []
def _extract_content_from_llm_response(self, resp: Any) -> str:
"""
Helper to extract string content from various LLM response types.
Handles strings, message objects (OpenAI-like), and streams.
Adapt stream handling for your specific LLM client if not OpenAI.
"""
collected_content = []
if isinstance(resp, str):
collected_content.append(resp)
elif ( # OpenAI non-streaming or Anthropic non-streaming (older SDK style)
hasattr(resp, "message")
and hasattr(resp.message, "content")
and resp.message.content is not None
):
collected_content.append(resp.message.content)
elif ( # OpenAI non-streaming (Pydantic model), Anthropic new SDK non-streaming
hasattr(resp, "choices") and resp.choices and
hasattr(resp.choices[0], "message") and
hasattr(resp.choices[0].message, "content") and
resp.choices[0].message.content is not None
):
collected_content.append(resp.choices[0].message.content) # OpenAI
elif ( # Anthropic new SDK non-streaming content block
hasattr(resp, "content") and isinstance(resp.content, list) and resp.content and
hasattr(resp.content[0], "text")
):
collected_content.append(resp.content[0].text) # Anthropic
else:
# Assume resp is a stream if not a recognized object
try:
for chunk in resp: # This will fail if resp is not iterable (e.g. a non-streaming response object)
content_piece = ""
# OpenAI-like stream
if hasattr(chunk, 'choices') and len(chunk.choices) > 0 and \
hasattr(chunk.choices[0], 'delta') and \
hasattr(chunk.choices[0].delta, 'content') and \
chunk.choices[0].delta.content is not None:
content_piece = chunk.choices[0].delta.content
# Anthropic-like stream (ContentBlockDelta)
elif hasattr(chunk, 'type') and chunk.type == 'content_block_delta' and \
hasattr(chunk, 'delta') and hasattr(chunk.delta, 'text'):
content_piece = chunk.delta.text
elif isinstance(chunk, str): # Simplest case: stream of strings
content_piece = chunk
if content_piece:
collected_content.append(content_piece)
except TypeError: # If resp is not iterable (e.g. a final response object that wasn't caught above)
logger.debug(f"Response type {type(resp)} could not be iterated as a stream. It might be a non-streaming object not handled by specific checks.")
except Exception as e:
logger.error(f"Error processing potential stream chunk: {e}, chunk was: {getattr(chunk, '__dict__', chunk)}")
return "".join(collected_content)
def _gen_inner(
self, query: str, retriever: BaseRetriever, log_context: LogContext
) -> Generator[Dict, None, None]:
# Reset state for this generation call
self.plan = ""
self.observations = []
retrieved_data = self._retriever_search(retriever, query, log_context)
if self.user_api_key:
@@ -99,131 +37,96 @@ class ReActAgent(BaseAgent):
self._prepare_tools(tools_dict)
docs_together = "\n".join([doc["text"] for doc in retrieved_data])
iterating_reasoning = 0
while iterating_reasoning < MAX_ITERATIONS_REASONING:
iterating_reasoning += 1
# 1. Create Plan
logger.info("ReActAgent: Creating plan...")
plan_stream = self._create_plan(query, docs_together, log_context)
current_plan_parts = []
yield {"thought": f"Reasoning... (iteration {iterating_reasoning})\n\n"}
for line_chunk in plan_stream:
current_plan_parts.append(line_chunk)
yield {"thought": line_chunk}
self.plan = "".join(current_plan_parts)
if self.plan:
self.observations.append(f"Plan: {self.plan} Iteration: {iterating_reasoning}")
plan = self._create_plan(query, docs_together, log_context)
for line in plan:
if isinstance(line, str):
self.plan += line
yield {"thought": line}
prompt = self.prompt + f"\nFollow this plan: {self.plan}"
messages = self._build_messages(prompt, query, retrieved_data)
max_obs_len = 20000
obs_str = "\n".join(self.observations)
if len(obs_str) > max_obs_len:
obs_str = obs_str[:max_obs_len] + "\n...[observations truncated]"
execution_prompt_str = (
(self.prompt or "")
+ f"\n\nFollow this plan:\n{self.plan}"
+ f"\n\nObservations:\n{obs_str}"
+ f"\n\nIf there is enough data to complete user query '{query}', Respond with 'SATISFIED' only. Otherwise, continue. Dont Menstion 'SATISFIED' in your response if you are not ready. "
resp = self._llm_gen(messages, log_context)
if isinstance(resp, str):
self.observations.append(resp)
if (
hasattr(resp, "message")
and hasattr(resp.message, "content")
and resp.message.content is not None
):
self.observations.append(resp.message.content)
resp = self._llm_handler(resp, tools_dict, messages, log_context)
for tool_call in self.tool_calls:
observation = (
f"Action '{tool_call['action_name']}' of tool '{tool_call['tool_name']}' "
f"with arguments '{tool_call['arguments']}' returned: '{tool_call['result']}'"
)
messages = self._build_messages(execution_prompt_str, query, retrieved_data)
self.observations.append(observation)
resp_from_llm_gen = self._llm_gen(messages, log_context)
if isinstance(resp, str):
self.observations.append(resp)
elif (
hasattr(resp, "message")
and hasattr(resp.message, "content")
and resp.message.content is not None
):
self.observations.append(resp.message.content)
else:
completion = self.llm.gen_stream(
model=self.gpt_model, messages=messages, tools=self.tools
)
for line in completion:
if isinstance(line, str):
self.observations.append(line)
initial_llm_thought_content = self._extract_content_from_llm_response(resp_from_llm_gen)
if initial_llm_thought_content:
self.observations.append(f"Initial thought/response: {initial_llm_thought_content}")
else:
logger.info("ReActAgent: Initial LLM response (before handler) had no textual content (might be only tool calls).")
resp_after_handler = self._llm_handler(resp_from_llm_gen, tools_dict, messages, log_context)
for tool_call_info in self.tool_calls: # Iterate over self.tool_calls populated by _llm_handler
observation_string = (
f"Executed Action: Tool '{tool_call_info.get('tool_name', 'N/A')}' "
f"with arguments '{tool_call_info.get('arguments', '{}')}'. Result: '{str(tool_call_info.get('result', ''))[:200]}...'"
)
self.observations.append(observation_string)
log_context.stacks.append(
{"component": "agent", "data": {"tool_calls": self.tool_calls.copy()}}
)
content_after_handler = self._extract_content_from_llm_response(resp_after_handler)
if content_after_handler:
self.observations.append(f"Response after tool execution: {content_after_handler}")
else:
logger.info("ReActAgent: LLM response after handler had no textual content.")
yield {"sources": retrieved_data}
# clean tool_call_data only send first 50 characters of tool_call['result']
for tool_call in self.tool_calls:
if len(str(tool_call["result"])) > 50:
tool_call["result"] = str(tool_call["result"])[:50] + "..."
yield {"tool_calls": self.tool_calls.copy()}
if log_context:
log_context.stacks.append(
{"component": "agent_tool_calls", "data": {"tool_calls": self.tool_calls.copy()}}
)
yield {"sources": retrieved_data}
display_tool_calls = []
for tc in self.tool_calls:
cleaned_tc = tc.copy()
if len(str(cleaned_tc.get("result", ""))) > 50:
cleaned_tc["result"] = str(cleaned_tc["result"])[:50] + "..."
display_tool_calls.append(cleaned_tc)
if display_tool_calls:
yield {"tool_calls": display_tool_calls}
if "SATISFIED" in content_after_handler:
logger.info("ReActAgent: LLM satisfied with the plan and data. Stopping reasoning.")
break
# 3. Create Final Answer based on all observations
final_answer_stream = self._create_final_answer(query, self.observations, log_context)
for answer_chunk in final_answer_stream:
yield {"answer": answer_chunk}
logger.info("ReActAgent: Finished generating final answer.")
final_answer = self._create_final_answer(query, self.observations, log_context)
for line in final_answer:
if isinstance(line, str):
yield {"answer": line}
def _create_plan(
self, query: str, docs_data: str, log_context: LogContext = None
) -> Generator[str, None, None]:
plan_prompt_filled = planning_prompt_template.replace("{query}", query)
if "{summaries}" in plan_prompt_filled:
summaries = docs_data if docs_data else "No documents retrieved."
plan_prompt_filled = plan_prompt_filled.replace("{summaries}", summaries)
plan_prompt_filled = plan_prompt_filled.replace("{prompt}", self.prompt or "")
plan_prompt_filled = plan_prompt_filled.replace("{observations}", "\n".join(self.observations))
plan_prompt = planning_prompt.replace("{query}", query)
if "{summaries}" in planning_prompt:
summaries = docs_data
plan_prompt = plan_prompt.replace("{summaries}", summaries)
messages = [{"role": "user", "content": plan_prompt_filled}]
plan_stream_from_llm = self.llm.gen_stream(
model=self.gpt_model, messages=messages, tools=getattr(self, 'tools', None) # Use self.tools
messages = [{"role": "user", "content": plan_prompt}]
print(self.tools)
plan = self.llm.gen_stream(
model=self.gpt_model, messages=messages, tools=self.tools
)
if log_context:
data = build_stack_data(self.llm)
log_context.stacks.append({"component": "planning_llm", "data": data})
for chunk in plan_stream_from_llm:
content_piece = self._extract_content_from_llm_response(chunk)
if content_piece:
yield content_piece
return plan
def _create_final_answer(
self, query: str, observations: List[str], log_context: LogContext = None
) -> Generator[str, None, None]:
) -> str:
observation_string = "\n".join(observations)
max_obs_len = 10000
if len(observation_string) > max_obs_len:
observation_string = observation_string[:max_obs_len] + "\n...[observations truncated]"
logger.warning("ReActAgent: Truncated observations for final answer prompt due to length.")
final_answer_prompt_filled = final_prompt_template.format(
final_answer_prompt = final_prompt.format(
query=query, observations=observation_string
)
messages = [{"role": "user", "content": final_answer_prompt_filled}]
# Final answer should synthesize, not call tools.
final_answer_stream_from_llm = self.llm.gen_stream(
model=self.gpt_model, messages=messages, tools=None
)
messages = [{"role": "user", "content": final_answer_prompt}]
final_answer = self.llm.gen_stream(model=self.gpt_model, messages=messages)
if log_context:
data = build_stack_data(self.llm)
log_context.stacks.append({"component": "final_answer_llm", "data": data})
for chunk in final_answer_stream_from_llm:
content_piece = self._extract_content_from_llm_response(chunk)
if content_piece:
yield content_piece
return final_answer

View File

@@ -439,23 +439,20 @@ class Stream(Resource):
try:
question = data["question"]
history = limit_chat_history(
json.loads(data.get("history", "[]")), gpt_model=gpt_model
json.loads(data.get("history", [])), gpt_model=gpt_model
)
conversation_id = data.get("conversation_id")
prompt_id = data.get("prompt_id", "default")
attachment_ids = data.get("attachments", [])
index = data.get("index", None)
chunks_from_request = data.get("chunks", 2)
chunks = chunks_from_request if str(chunks_from_request) == 'Auto' else int(chunks_from_request)
chunks = int(data.get("chunks", 2))
token_limit = data.get("token_limit", settings.DEFAULT_MAX_HISTORY)
retriever_name = data.get("retriever", "classic")
agent_id = data.get("agent_id", None)
agent_type = settings.AGENT_NAME
decoded_token = getattr(request, "decoded_token", None)
user_sub = decoded_token.get("sub") if decoded_token else None
agent_key, is_shared_usage, shared_token = get_agent_key(
agent_id, user_sub
agent_id, request.decoded_token.get("sub")
)
if agent_key:
@@ -528,7 +525,8 @@ class Stream(Resource):
user_api_key=user_api_key,
decoded_token=decoded_token,
)
is_shared_usage_val = data.get("is_shared_usage", False)
is_shared_token_val = data.get("shared_token", None)
return Response(
complete_stream(
question=question,
@@ -541,8 +539,8 @@ class Stream(Resource):
index=index,
should_save_conversation=save_conv,
agent_id=agent_id,
is_shared_usage=is_shared_usage,
shared_token=shared_token,
is_shared_usage=is_shared_usage_val,
shared_token=is_shared_token_val,
),
mimetype="text/event-stream",
)
@@ -621,8 +619,7 @@ class Answer(Resource):
)
conversation_id = data.get("conversation_id")
prompt_id = data.get("prompt_id", "default")
chunks_from_request = data.get("chunks", 2)
chunks = chunks_from_request if str(chunks_from_request) == 'Auto' else int(chunks_from_request)
chunks = int(data.get("chunks", 2))
token_limit = data.get("token_limit", settings.DEFAULT_MAX_HISTORY)
retriever_name = data.get("retriever", "classic")
agent_type = settings.AGENT_NAME
@@ -816,8 +813,7 @@ class Search(Resource):
try:
question = data["question"]
chunks_from_request = data.get("chunks", 2)
chunks = chunks_from_request if str(chunks_from_request) == 'Auto' else int(chunks_from_request)
chunks = int(data.get("chunks", 2))
token_limit = data.get("token_limit", settings.DEFAULT_MAX_HISTORY)
retriever_name = data.get("retriever", "classic")

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,10 @@
You are an AI assistant and talk like you're thinking out loud. Given the following query, outline a concise thought process that includes key steps and considerations necessary for effective analysis and response. Avoid pointwise formatting. The goal is to break down the query into manageable components without excessive detail, focusing on clarity and logical progression.
Include the following elements in your thought and execution process:
Include the following elements in your thought process:
1. Identify the main objective of the query.
2. Determine any relevant context or background information needed to understand the query.
3. List potential approaches or methods to address the query.
4. Highlight any critical factors or constraints that may influence the outcome.
5. Plan with available tools to help you with the analysis but dont execute them. Tools will be executed by another AI.
Query: {query}
Summaries: {summaries}
Prompt: {prompt}
Observations(potentially previous tool calls): {observations}
Summaries: {summaries}

View File

@@ -41,12 +41,12 @@ numpy==2.2.1
openai==1.78.1
openapi3-parser==1.1.21
orjson==3.10.14
packaging==24.2
packaging==25.0
pandas==2.2.3
openpyxl==3.1.5
pathable==0.4.4
pillow==11.1.0
portalocker>=2.7.0,<3.0.0
portalocker==3.1.1
prance==23.6.21.0
prompt-toolkit==3.0.51
protobuf==5.29.3
@@ -62,7 +62,7 @@ python-dotenv==1.0.1
python-jose==3.4.0
python-pptx==1.0.2
redis==5.2.1
referencing>=0.28.0,<0.31.0
referencing==0.36.2
regex==2024.11.6
requests==2.32.3
retry==0.9.2

View File

@@ -2,16 +2,11 @@ import logging
from application.core.settings import settings
from application.llm.llm_creator import LLMCreator
from application.retriever.base import BaseRetriever
from application.vectorstore.vector_creator import VectorCreator
logger = logging.getLogger(__name__)
class ClassicRAG(BaseRetriever):
# Settings for Auto-Chunking
AUTO_CHUNK_MIN: int = 0
AUTO_CHUNK_MAX: int = 10
SIMILARITY_SCORE_THRESHOLD: float = 0.5
def __init__(
self,
source,
@@ -52,7 +47,6 @@ class ClassicRAG(BaseRetriever):
self.question = self._rephrase_query()
self.vectorstore = source["active_docs"] if "active_docs" in source else None
self.decoded_token = decoded_token
self.actual_chunks_retrieved = 0
def _rephrase_query(self):
if (
@@ -83,66 +77,8 @@ class ClassicRAG(BaseRetriever):
return self.original_question
def _get_data(self):
if self.chunks == 'Auto':
return self._get_data_auto()
else:
return self._get_data_classic()
def _get_data_auto(self):
if not self.vectorstore:
self.actual_chunks_retrieved = 0
return []
docsearch = VectorCreator.create_vectorstore(
settings.VECTOR_STORE, self.vectorstore, settings.EMBEDDINGS_KEY
)
try:
docs_with_scores = docsearch.search_with_scores(self.question, k=self.AUTO_CHUNK_MAX)
except Exception as e:
logger.error(f"Error during search_with_scores: {e}", exc_info=True)
self.actual_chunks_retrieved = 0
return []
if not docs_with_scores:
self.actual_chunks_retrieved = 0
return []
candidate_docs = []
for doc, score in docs_with_scores:
if score >= self.SIMILARITY_SCORE_THRESHOLD:
candidate_docs.append(doc)
if len(candidate_docs) < self.AUTO_CHUNK_MIN and self.AUTO_CHUNK_MIN > 0:
final_docs_to_format = [doc for doc, score in docs_with_scores[:self.AUTO_CHUNK_MIN]]
else:
final_docs_to_format = candidate_docs
self.actual_chunks_retrieved = len(final_docs_to_format)
if not final_docs_to_format:
return []
formatted_docs = [
{
"title": i.metadata.get(
"title", i.metadata.get("post_title", i.page_content)
).split("/")[-1],
"text": i.page_content,
"source": (
i.metadata.get("source")
if i.metadata.get("source")
else "local"
),
}
for i in final_docs_to_format
]
logger.info(f"AutoRAG: Retrieved {self.actual_chunks_retrieved} chunks for query '{self.original_question}'.")
return formatted_docs
def _get_data_classic(self):
if self.chunks == 0:
return []
docs = []
else:
docsearch = VectorCreator.create_vectorstore(
settings.VECTOR_STORE, self.vectorstore, settings.EMBEDDINGS_KEY
@@ -162,7 +98,8 @@ class ClassicRAG(BaseRetriever):
}
for i in docs_temp
]
return docs
return docs
def gen():
pass
@@ -174,24 +111,12 @@ class ClassicRAG(BaseRetriever):
return self._get_data()
def get_params(self):
params = {
return {
"question": self.original_question,
"rephrased_question": self.question,
"source": self.vectorstore,
"chunks": self.chunks,
"token_limit": self.token_limit,
"gpt_model": self.gpt_model,
"user_api_key": self.user_api_key,
}
if self.chunks == 'Auto':
params.update({
"chunks_mode": "Auto",
"chunks_retrieved_auto": self.actual_chunks_retrieved,
"auto_chunk_min_setting": self.AUTO_CHUNK_MIN,
"auto_chunk_max_setting": self.AUTO_CHUNK_MAX,
"similarity_threshold_setting": self.SIMILARITY_SCORE_THRESHOLD,
})
else:
params["chunks_mode"] = "Classic"
params["chunks"] = self.chunks
return params

View File

@@ -2,18 +2,19 @@ from application.retriever.classic_rag import ClassicRAG
from application.retriever.duckduck_search import DuckDuckSearch
from application.retriever.brave_search import BraveRetSearch
class RetrieverCreator:
retrievers = {
"classic": ClassicRAG,
"duckduck_search": DuckDuckSearch,
"brave_search": BraveRetSearch,
"default": ClassicRAG,
'classic': ClassicRAG,
'duckduck_search': DuckDuckSearch,
'brave_search': BraveRetSearch,
'default': ClassicRAG
}
@classmethod
def create_retriever(cls, type, *args, **kwargs):
retriever_type = (type or "default").lower()
retiever_class = cls.retrievers.get(retriever_type)
retiever_class = cls.retrievers.get(type.lower())
if not retiever_class:
raise ValueError(f"No retievers class found for type {type}")
return retiever_class(*args, **kwargs)
return retiever_class(*args, **kwargs)

View File

@@ -58,10 +58,6 @@ class BaseVectorStore(ABC):
def search(self, *args, **kwargs):
pass
@abstractmethod
def search_with_scores(self, query: str, k: int, *args, **kwargs):
pass
def is_azure_configured(self):
return settings.OPENAI_API_BASE and settings.OPENAI_API_VERSION and settings.AZURE_DEPLOYMENT_NAME

View File

@@ -108,46 +108,6 @@ class ElasticsearchStore(BaseVectorStore):
doc_list.append(Document(page_content = hit['_source']['text'], metadata = hit['_source']['metadata']))
return doc_list
def search_with_scores(self, query: str, k: int, *args, **kwargs):
embeddings = self._get_embeddings(settings.EMBEDDINGS_NAME, self.embeddings_key)
vector = embeddings.embed_query(query)
knn = {
"filter": [{"match": {"metadata.source_id.keyword": self.source_id}}],
"field": "vector",
"k": k,
"num_candidates": 100,
"query_vector": vector,
}
full_query = {
"knn": knn,
"query": {
"bool": {
"must": [
{
"match": {
"text": {
"query": question,
}
}
}
],
"filter": [{"match": {"metadata.source_id.keyword": self.source_id}}],
}
},
"rank": {"rrf": {}},
}
resp = self.docsearch.search(index=self.index_name, query=full_query['query'], size=k, knn=full_query['knn'])
docs_with_scores = []
for hit in resp['hits']['hits']:
score = hit['_score']
# Normalize the score. Elasticsearch returns a score of 1.0 + cosine similarity.
similarity = max(0, score - 1.0)
doc = Document(page_content=hit['_source']['text'], metadata=hit['_source']['metadata'])
docs_with_scores.append((doc, similarity))
return docs_with_scores
def _create_index_if_not_exists(
self, index_name, dims_length

View File

@@ -32,26 +32,22 @@ class FaissStore(BaseVectorStore):
with tempfile.TemporaryDirectory() as temp_dir:
faiss_path = f"{self.path}/index.faiss"
pkl_path = f"{self.path}/index.pkl"
if not self.storage.file_exists(
faiss_path
) or not self.storage.file_exists(pkl_path):
raise FileNotFoundError(
f"Index files not found in storage at {self.path}"
)
if not self.storage.file_exists(faiss_path) or not self.storage.file_exists(pkl_path):
raise FileNotFoundError(f"Index files not found in storage at {self.path}")
faiss_file = self.storage.get_file(faiss_path)
pkl_file = self.storage.get_file(pkl_path)
local_faiss_path = os.path.join(temp_dir, "index.faiss")
local_pkl_path = os.path.join(temp_dir, "index.pkl")
with open(local_faiss_path, "wb") as f:
with open(local_faiss_path, 'wb') as f:
f.write(faiss_file.read())
with open(local_pkl_path, "wb") as f:
with open(local_pkl_path, 'wb') as f:
f.write(pkl_file.read())
self.docsearch = FAISS.load_local(
temp_dir, self.embeddings, allow_dangerous_deserialization=True
)
@@ -62,18 +58,6 @@ class FaissStore(BaseVectorStore):
def search(self, *args, **kwargs):
return self.docsearch.similarity_search(*args, **kwargs)
def search_with_scores(self, query: str, k: int, *args, **kwargs):
docs_and_distances = self.docsearch.similarity_search_with_score(query, k, *args, **kwargs)
# Convert L2 distance to a normalized similarity score (0-1, higher is better)
docs_and_similarities = []
for doc, distance in docs_and_distances:
if distance < 0: distance = 0
similarity = 1 / (1 + distance)
docs_and_similarities.append((doc, similarity))
return docs_and_similarities
def add_texts(self, *args, **kwargs):
return self.docsearch.add_texts(*args, **kwargs)

View File

@@ -2,8 +2,6 @@ from typing import List, Optional
import importlib
from application.vectorstore.base import BaseVectorStore
from application.core.settings import settings
from application.vectorstore.document_class import Document
class LanceDBVectorStore(BaseVectorStore):
"""Class for LanceDB Vector Store integration."""
@@ -89,23 +87,6 @@ class LanceDBVectorStore(BaseVectorStore):
results = self.docsearch.search(query_embedding).limit(k).to_list()
return [(result["_distance"], result["text"], result["metadata"]) for result in results]
def search_with_scores(self, query: str, k: int, *args, **kwargs):
"""Perform a similarity search with scores."""
self.ensure_table_exists()
query_embedding = self._get_embeddings(settings.EMBEDDINGS_NAME, self.embeddings_key).embed_query(query)
results = self.docsearch.search(query_embedding).limit(k).to_list()
docs_with_scores = []
for result in results:
distance = result.get('_distance', float('inf'))
if distance < 0: distance = 0
# Convert L2 distance to a normalized similarity score
similarity = 1 / (1 + distance)
doc = Document(page_content=result['text'], metadata=result["metadata"])
docs_with_scores.append((doc, similarity))
return docs_with_scores
def delete_index(self):
"""Delete the entire LanceDB index (table)."""
if self.table:

View File

@@ -25,16 +25,6 @@ class MilvusStore(BaseVectorStore):
def search(self, question, k=2, *args, **kwargs):
expr = f"source_id == '{self._source_id}'"
return self._docsearch.similarity_search(query=question, k=k, expr=expr, *args, **kwargs)
def search_with_scores(self, query: str, k: int, *args, **kwargs):
expr = f"source_id == '{self._source_id}'"
docs_and_distances = self._docsearch.similarity_search_with_score(query, k, expr=expr, *args, **kwargs)
docs_with_scores = []
for doc, distance in docs_and_distances:
similarity = 1.0 - distance
docs_with_scores.append((doc, max(0, similarity)))
return docs_with_scores
def add_texts(self, texts: List[str], metadatas: Optional[List[dict]], *args, **kwargs):
ids = [str(uuid4()) for _ in range(len(texts))]

View File

@@ -62,40 +62,6 @@ class MongoDBVectorStore(BaseVectorStore):
metadata = doc
results.append(Document(text, metadata))
return results
def search_with_scores(self, query: str, k: int, *args, **kwargs):
query_vector = self._embedding.embed_query(query)
pipeline = [
{
"$vectorSearch": {
"queryVector": query_vector,
"path": self._embedding_key,
"limit": k,
"numCandidates": k * 10,
"index": self._index_name,
"filter": {"source_id": {"$eq": self._source_id}},
}
},
{
"$addFields": {
"score": {"$meta": "vectorSearchScore"}
}
}
]
cursor = self._collection.aggregate(pipeline)
results = []
for doc in cursor:
score = doc.pop("score", 0.0)
text = doc.pop(self._text_key)
doc.pop("_id")
doc.pop(self._embedding_key, None)
metadata = doc
doc = Document(page_content=text, metadata=metadata)
results.append((doc, score))
return results
def _insert_texts(self, texts, metadatas):
if not texts:

View File

@@ -35,9 +35,6 @@ class QdrantStore(BaseVectorStore):
def search(self, *args, **kwargs):
return self._docsearch.similarity_search(filter=self._filter, *args, **kwargs)
def search_with_scores(self, query: str, k: int, *args, **kwargs):
return self._docsearch.similarity_search_with_score(query=query, k=k, filter=self._filter, *args, **kwargs)
def add_texts(self, *args, **kwargs):
return self._docsearch.add_texts(*args, **kwargs)

View File

@@ -1,117 +0,0 @@
import Image from 'next/image';
const iconMap = {
'API Tool': '/toolIcons/tool_api_tool.svg',
'Brave Search Tool': '/toolIcons/tool_brave.svg',
'Cryptoprice Tool': '/toolIcons/tool_cryptoprice.svg',
'Ntfy Tool': '/toolIcons/tool_ntfy.svg',
'PostgreSQL Tool': '/toolIcons/tool_postgres.svg',
'Read Webpage Tool': '/toolIcons/tool_read_webpage.svg',
'Telegram Tool': '/toolIcons/tool_telegram.svg'
};
export function ToolCards({ items }) {
return (
<>
<div className="tool-cards">
{items.map(({ title, link, description }) => {
const isExternal = link.startsWith('https://');
const iconSrc = iconMap[title] || '/default-icon.png'; // Default icon if not found
return (
<div
key={title}
className={`card${isExternal ? ' external' : ''}`}
>
<a href={link} target={isExternal ? '_blank' : undefined} rel="noopener noreferrer" className="card-link-wrapper">
<div className="card-icon-container">
{iconSrc && <div className="card-icon"><Image src={iconSrc} alt={title} width={32} height={32} /></div>} {/* Reduced icon size */}
</div>
<h3 className="card-title">{title}</h3>
{description && <p className="card-description">{description}</p>}
{/* Card URL element removed from here */}
</a>
</div>
);
})}
</div>
<style jsx>{`
.tool-cards {
margin-top: 24px;
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
@media (min-width: 768px) {
.tool-cards {
grid-template-columns: 1fr 1fr; /* Keeps two columns on wider screens */
}
}
.card {
background-color: #222222;
border-radius: 8px;
padding: 16px; /* Existing padding */
transition: background-color 0.3s;
position: relative;
color: #ffffff;
display: flex; /* Using flex to help with alignment */
flex-direction: column;
/* align-items: center; // Alignment for items inside card-link-wrapper is better */
/* justify-content: center; // We want content to flow from top */
height: 100%; /* Fill the height of the grid cell, ensures cards in a row are same height */
}
.card:hover {
background-color: #333333;
}
.card.external::after {
content: "↗";
position: absolute;
top: 12px;
right: 12px;
color: #ffffff;
font-size: 0.7em;
opacity: 0.8;
}
.card-link-wrapper {
display: flex;
flex-direction: column;
align-items:center; /* Centers icon, title, description horizontally */
text-align: center; /* Ensures text within p and h3 is centered */
color: inherit;
text-decoration: none;
width:100%;
height: 100%; /* Make the link wrapper take full card height */
justify-content: flex-start; /* Align content to the top */
}
.card-icon-container{
display:flex;
justify-content:center;
width: 100%;
margin-top: 8px; /* Added some margin at the top if needed */
margin-bottom: 12px; /* Increased space between icon and title */
}
.card-icon {
display: block;
/* margin: 0 auto; // Center handled by card-icon-container */
}
.card-title {
font-weight: 600;
margin-bottom: 8px; /* Increased space below title */
font-size: 16px; /* Consider increasing slightly if descriptions are longer e.g. 17px or 18px */
color: #f0f0f0;
}
.card-description {
/* margin-bottom: 0; // Original value */
font-size: 14px; /* Slightly increased font size for better readability */
color: #aaaaaa;
line-height: 1.5; /* Slightly increased line height */
flex-grow: 1; /* Allows description to take available space */
overflow-y: auto; /* Adds scroll if description is too long, though ideally content fits */
padding-bottom: 8px; /* Add some padding at the bottom of the description area */
}
`}</style>
</>
);
}

1661
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,8 @@
"license": "MIT",
"dependencies": {
"@vercel/analytics": "^1.1.1",
"docsgpt-react": "^0.5.1",
"next": "^15.3.3",
"docsgpt-react": "^0.5.0",
"next": "^14.2.26",
"nextra": "^2.13.2",
"nextra-theme-docs": "^2.13.2",
"react": "^18.2.0",

View File

@@ -1,6 +0,0 @@
{
"basics": {
"title": "🤖 Agent Basics",
"href": "/Agents/basics"
}
}

View File

@@ -1,109 +0,0 @@
---
title: Understanding DocsGPT Agents
description: Learn about DocsGPT Agents, their types, how to create and manage them, and how they can enhance your interaction with documents and tools.
---
import { Callout } from 'nextra/components';
import Image from 'next/image'; // Assuming you might want to embed images later, like the ones you uploaded.
# Understanding DocsGPT Agents 🤖
DocsGPT Agents are advanced, configurable AI entities designed to go beyond simple question-answering. They act as specialized assistants or workers that combine instructions (prompts), knowledge (document sources), and capabilities (tools) to perform a wide range of tasks, automate workflows, and provide tailored interactions.
Think of an Agent as a pre-configured version of DocsGPT, fine-tuned for a specific purpose, such as classifying documents, responding to new form submissions, or validating emails.
## Why Use Agents?
* **Personalization:** Create AI assistants that behave and respond according to specific roles or personas.
* **Task Specialization:** Design agents focused on particular tasks, like customer support, data extraction, or content generation.
* **Knowledge Integration:** Equip agents with specific document sources, making them experts in particular domains.
* **Tool Utilization:** Grant agents access to various tools, allowing them to interact with external services, fetch live data, or perform actions.
* **Automation:** Automate repetitive tasks by defining an agent's behavior and integrating it via webhooks or other means.
* **Shareability:** Share your custom-configured agents with others or use agents shared with you.
Agents provide a more structured and powerful way to leverage LLMs compared to a standard chat interface, as they come with a pre-defined context, instruction set, and set of capabilities.
## Core Components of an Agent
When you create or configure an agent, you'll work with these key components:
**Meta:**
* **Agent Name:** A user-friendly name to identify the agent (e.g., "Support Ticket Classifier," "Product Spec Expert").
* **Describe your agent:** A brief description for you or users to understand the agent's purpose.
**Source:**
* **Select source:** The knowledge base for the agent. You can select from previously uploaded documents or data sources. This is what the agent will "know."
* **Chunks per query:** A numerical value determining how many relevant text chunks from the selected source are sent to the LLM with each query. This helps manage context length and relevance.
**Prompt:**
The main set of instructions or system [prompt](/Guides/Customising-prompts) that defines the agent's persona, objectives, constraints, and how it should behave or respond.
**Tools:** A selection of available [DocsGPT Tools](/Tools/basics) that the agent can use to perform actions or access external information.
**Agent type:** The underlying operational logic or architecture the agent uses. DocsGPT supports different types of agents, each suited for different kinds of tasks.
## Understanding Agent Types
DocsGPT allows for different "types" of agents, each with a distinct way of processing information and generating responses. The code for these agent types can be found in the `application/agents/` directory.
### 1. Classic Agent (`classic_agent.py`)
**How it works:** The Classic Agent follows a traditional Retrieval Augmented Generation (RAG) approach.
1. **Retrieve:** When a query is made, it first searches the selected Source documents for relevant information.
2. **Augment:** This retrieved data is then added to the context, along with the main Prompt and the user's query.
3. **Generate:** The LLM generates a response based on this augmented context. It can also utilize any configured tools if the LLM decides they are necessary.
**Best for:**
* Direct question-answering over a specific set of documents.
* Tasks where the primary goal is to extract and synthesize information from the provided sources.
* Simpler tool integrations where the decision to use a tool is straightforward.
### 2. ReAct Agent (`react_agent.py`)
**How it works:** The ReAct Agent employs a more sophisticated "Reason and Act" framework. This involves a multi-step process:
1. **Plan (Thought):** Based on the query, its prompt, and available tools/sources, the LLM first generates a plan or a sequence of thoughts on how to approach the problem. You might see this output as a "thought" process during generation.
2. **Act:** The agent then executes actions based on this plan. This might involve querying its sources, using a tool, or performing internal reasoning.
3. **Observe:** It gathers observations from the results of its actions (e.g., data from a tool, snippets from documents).
4. **Repeat (if necessary):** Steps 2 and 3 can be repeated as the agent refines its approach or gathers more information.
5. **Conclude:** Finally, it generates the final answer based on the initial query and all accumulated observations.
**Best for:**
* More complex tasks that require multi-step reasoning or problem-solving.
* Scenarios where the agent needs to dynamically decide which tools to use and in what order, based on intermediate results.
* Interactive tasks where the agent needs to "think" through a problem.
<Callout type="info">
Developers looking to introduce new agent architectures can explore the `application/agents/` directory. `classic_agent.py` and `react_agent.py` serve as excellent starting points, demonstrating how to inherit from `BaseAgent` and structure agent logic.
</Callout>
## Navigating and Managing Agents in DocsGPT
You can easily access and manage your agents through the DocsGPT user interface. Recently used agents appear at the top of the left sidebar for quick access. Below these, the "Manage Agents" button will take you to the main Agents page.
### Creating a New Agent
1. Navigate to the "Agents" page.
2. Click the **"New Agent"** button.
3. You will be presented with the "New Agent" configuration screen:
<Image
src="/new-agent.png"
alt="API Tool configuration example for phone validation"
width={800}
height={450}
style={{ margin: '1em auto', display: 'block', borderRadius: '8px' }}
/>
4. Fill in the fields as described in the "Core Components of an Agent" section.
5. Once configured, you can **"Save Draft"** to continue editing later or **"Publish"** to make the agent active.
## Interacting with and Editing Agents
Once an agent is created, you can:
* **Chat with it:** Select the agent to start an interaction.
* **View Logs:** Access usage statistics, monitor token consumption per interaction, and review user message feedbacks. This is crucial for understanding how your agent is being used and performing.
* **Edit an Agent:**
* Modify any of its configuration settings (name, description, source, prompt, tools, type).
* **Generate a Public Link:** From the edit screen, you can create a shareable public link that allows others to import and use your agent.
* **Get a Webhook URL:** You can also obtain a Webhook URL for the agent. This allows external applications or services to trigger the agent and receive responses programmatically, enabling powerful integrations and automations.

View File

@@ -95,49 +95,6 @@ EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2 # You can al
In this case, even though you are using Ollama locally, `LLM_NAME` is set to `openai` because Ollama (and many other local inference engines) are designed to be API-compatible with OpenAI. `OPENAI_BASE_URL` points DocsGPT to the local Ollama server.
## Authentication Settings
DocsGPT includes a JWT (JSON Web Token) based authentication feature for managing sessions or securing local deployments while allowing access.
- **`AUTH_TYPE`**: This setting in your `.env` file or `settings.py` determines the authentication method.
- **Possible values:**
- `None` (or not set): No authentication is used.
- `simple_jwt`: A single, long-lived JWT token is generated and used for all authenticated requests. This is useful for securing a local deployment with a shared secret.
- `session_jwt`: Unique JWT tokens are generated for sessions, typically for individual users or temporary access.
- If `AUTH_TYPE` is set to `simple_jwt` or `session_jwt`, then a `JWT_SECRET_KEY` is required.
- **`JWT_SECRET_KEY`**: This is a crucial secret key used to sign and verify JWTs.
- It can be set directly in your `.env` file or `settings.py`.
- **Automatic Key Generation**: If `AUTH_TYPE` is `simple_jwt` or `session_jwt` and `JWT_SECRET_KEY` is _not_ set in your environment variables or `settings.py`, DocsGPT will attempt to:
1. Read the key from a file named `.jwt_secret_key` in the project's root directory.
2. If the file doesn't exist, it will generate a new 32-byte random key, save it to `.jwt_secret_key`, and use it for the session. This ensures that the key persists across application restarts.
- **Security Note**: It's vital to keep this key secure. If you set it manually, choose a strong, random string.
**How it works:**
- When `AUTH_TYPE` is set to `simple_jwt`, a token is generated at startup (if not already present or configured) and printed to the console. This token should be included in the `Authorization` header of your API requests as a Bearer token (e.g., `Authorization: Bearer YOUR_SIMPLE_JWT_TOKEN`).
- When `AUTH_TYPE` is set to `session_jwt`:
- Clients can request a new token from the `/api/generate_token` endpoint.
- This token should then be included in the `Authorization` header for subsequent requests.
- The backend verifies the JWT token provided in the `Authorization` header for protected routes.
- The `/api/config` endpoint can be used to check the current `auth_type` and whether authentication is required.
**Frontend Token Input for `simple_jwt`:**
<img
src="/jwt-input.png"
alt="Frontend prompt for JWT Token"
style={{
width: '500px',
maxWidth: '100%',
display: 'block',
margin: '1em auto'
}}
/>
If you have configured `AUTH_TYPE=simple_jwt`, the DocsGPT frontend will prompt you to enter the JWT token if it's not already set or is invalid. You'll need to paste the `SIMPLE_JWT_TOKEN` (which is printed to your console when the backend starts) into this field to access the application.
## Exploring More Settings
These are just the basic settings to get you started. The `settings.py` file contains many more advanced options that you can explore to further customize DocsGPT, such as:

View File

@@ -1,14 +0,0 @@
{
"basics": {
"title": "🔧 Tools Basics",
"href": "/Tools/basics"
},
"api-tool": {
"title": "🗝️ API Tool",
"href": "/Tools/api-tool"
},
"creating-a-tool": {
"title": "🛠️ Creating a Custom Tool",
"href": "/Tools/creating-a-tool"
}
}

View File

@@ -1,153 +0,0 @@
---
title: 🗝️ Generic API Tool
description: Learn how to configure and use the API Tool in DocsGPT to connect with any RESTful API without writing custom code.
---
import { Callout } from 'nextra/components';
import Image from 'next/image';
# Using the Generic API Tool
The API Tool provides a no-code/low-code solution to make DocsGPT interact with third-party or internal RESTful APIs. It acts as a bridge, allowing the Large Language Model (LLM) to leverage external services based on your chat interactions.
This guide will walk you through its capabilities, configuration, and best practices.
## Introduction to the Generic API Tool
**When to Use It:**
* Ideal for quickly integrating existing APIs where the interaction involves standard HTTP requests (GET, POST, PUT, DELETE).
* Suitable for fetching data to enrich answers (e.g., current weather, stock prices, product details).
* Useful for triggering simple actions in other systems (e.g., sending a notification, creating a basic task).
**Contrast with Custom Python Tools:**
* **API Tool:** Best for straightforward API calls. Configuration is done through the DocsGPT UI.
* **Custom Python Tools:** Preferable when you need complex logic before or after the API call, handle non-standard authentication (like complex OAuth flows), manage multi-step API interactions, or require intricate data processing not easily managed by the LLM alone. See [Creating a Custom Tool](/Tools/creating-a-tool) for more.
## Capabilities of the API Tool
**Supported HTTP Methods:** You can configure actions using standard HTTP methods such as:
* `GET`: To retrieve data.
* `POST`: To submit data to create a new resource.
* `PUT`: To update an existing resource.
* `DELETE`: To remove a resource.
**Request Configuration:**
* **Headers:** Define static or dynamic HTTP headers for authentication (e.g., API keys), content type specification, etc.
* **Query Parameters:** Specify URL query parameters, which can be static or dynamically filled by the LLM based on user input.
* **Request Body:** Define the structure of the request body (e.g., JSON), with fields that can be static or dynamically populated by the LLM.
**Response Handling:**
* The API Tool executes the request and receives the raw response from the API (typically JSON or plain text).
* This raw response is then passed back to the LLM.
* The LLM uses this response, along with the context of your query and the description of the API tool action, to formulate an answer or decide on follow-up actions. The API tool itself doesn't deeply parse or transform the response beyond basic content type detection (e.g., loading JSON into a parsable object).
## Configuring an API as a Tool
You can configure the API Tool through the DocsGPT user interface, found in **Settings -> Tools**. When you add or modify an API Tool, you'll define specific actions that DocsGPT can perform.
<Callout type="info">
The configuration involves defining how DocsGPT should call an API endpoint. Each configured API call essentially becomes a distinct "action" the LLM can choose to use.
</Callout>
Below is an example of how you might configure an API action, inspired by setting up a phone number validation service:
<Image
src="/toolIcons/api-tool-example.png"
alt="API Tool configuration example for phone validation"
width={800}
height={450}
style={{ margin: '1em auto', display: 'block', borderRadius: '8px' }}
/>
_Figure 1: Example configuration for an API Tool action to validate phone numbers._
**Defining an API Endpoint/Action:**
When you configure a new API action, you'll fill in the following fields:
- **`Name`:** A user-friendly name for this specific API action (e.g., "Phone-check" as in the image, or more specific like "ValidateUSPhoneNumber"). This helps in managing your tools.
- **`Description`:** This is a **critical field**. Provide a clear and concise description of what the API action does, what kind of input it expects (implicitly), and what kind of output it provides. The LLM uses this description to understand when and how to use this action.
- **`URL`:** The full endpoint URL for the API request.
- **`HTTP Method`:** Select the appropriate HTTP method (e.g., GET, POST) from a dropdown.
- **`Headers`:** You can add custom HTTP headers as key-value pairs (Name, Value). Indicate if the value should be `Filled by LLM` or is static. If filled by LLM, provide a `Description` for the LLM.
- **`Query Parameters`:** For `GET` requests or when parameters are sent in the URL.
* **`Name`:** The name of the query parameter (e.g., `api_key`, `phone`).
* **`Type`:** The data type of the parameter (e.g., `string`).
* **`Filled by LLM` (Checkbox):**
- **Unchecked (Static):** The `Value` you provide will be used for every call (e.g., for an `api_key` that doesn't change).
- **Checked (Dynamic):** The LLM will extract the appropriate value from the user's chat query based on the `Description` you provide for this parameter. The `Value` field is typically left empty or contains a placeholder if `Filled by LLM` is checked.
* `Description`: Context for the LLM if the parameter is to be filled dynamically, or for your own reference if static.
* `Value`: The static value if not filled by LLM.
- **`Request Body`:** Used to send data (commonly JSON) to the API. Similar to Query Parameters, you define fields with `Name`, `Type`, whether it's `Filled by LLM`, a `Description` for dynamic fields, and a static `Value` if applicable.
**Response Handling Guidance for the LLM:**
While the API Tool configuration UI doesn't have explicit fields for defining response parsing rules (like JSONPath extractors), you significantly influence how the LLM handles the response through:
* **Tool Action `Description`:** Clearly state what kind of information the API returns (e.g., "This API returns a JSON object with 'status' and 'location' fields for the phone number."). This helps the LLM know what to look for in the API's output.
* **Prompt Engineering:** For more complex scenarios, you might need to adjust your global or agent-specific prompts to guide DocsGPT on how to interpret and present information from API tool responses. See [Customising Prompts](/Guides/Customising-prompts).
## Using the Configured API Tool in Chat
Once an API action is configured and enabled, DocsGPT's LLM can decide to use it based on your natural language queries.
**Example (based on the phone validation tool in Figure 1):**
1. **User Query:** "Hey DocsGPT, can you check if +14155555555 is a valid phone number?"
2. **DocsGPT (LLM Orchestration):**
* The LLM analyzes the query.
* It matches the intent ("check if ... is a valid phone number") with the description of the "Phone-check" API action.
* It identifies `+14155555555` as the value for the `phone` parameter (which was marked as `Filled by LLM` with the description "Phone number to check").
* DocsGPT constructs the GET API request.
3. **API Tool Execution:**
* The API Tool makes the HTTP GET request.
* The external API (AbstractAPI) processes the request and returns a JSON response, e.g.:
```json
{
"phone": "+14155555555",
"valid": true,
"format": {
"international": "+1 415-555-5555",
"national": "(415) 555-5555"
},
"country": {
"code": "US",
"name": "United States",
"prefix": "+1"
},
"location": "California",
"type": "Landline"
}
```
4. **DocsGPT Response Formulation:**
* The API Tool passes this JSON response back to the LLM.
* The LLM, guided by the tool's description and the user's original query, extracts relevant information and formulates a user-friendly answer.
* **DocsGPT Chat Response:** "Yes, +14155555555 appears to be a valid landline phone number in California, United States."
## Advanced Tips and Best Practices
**Clear Description is the Key:** The LLM relies heavily on the `Description` field of the API action and its parameters. Make them unambiguous and action-oriented. Clearly state what the tool does and what kind of input it expects (even if implicitly through parameter descriptions).
**Iterative Testing:** After configuring an API tool, test it with various phrasings of user queries to ensure the LLM triggers it correctly and interprets the response as expected.
**Error Handling:**
* If an API call fails, the API Tool will return an error message and status code from the `requests` library or the API itself. The LLM may relay this error or try to explain it.
* Check DocsGPT's backend logs for more detailed error information if you encounter issues.
**Security Considerations:**
* **API Keys:** Be mindful of API keys and other sensitive credentials. The example image shows an API key directly in the configuration. For production or shared environments avoid exposing configurations with sensitive keys.
* **Rate Limits:** Be aware of the rate limits of the APIs you are integrating. Frequent calls from DocsGPT could exceed these limits.
* **Data Privacy:** Consider the data privacy implications of sending user query data to third-party APIs.
- **Idempotency:** For tools that modify data (POST, PUT, DELETE), be aware of whether the API operations are idempotent to avoid unintended consequences from repeated calls if the LLM retries an action.
## Limitations
While powerful, the Generic API Tool has some limitations:
- **Complex Authentication:** Advanced authentication flows like OAuth 2.0 (especially 3-legged OAuth requiring user redirection) or custom signature-based authentication often require custom Python tools.
- **Multi-Step API Interactions:** If a task requires multiple API calls that depend on each other (e.g., fetch a list, then for each item, fetch details), this kind of complex chaining and logic is better handled by a custom Python tool.
- **Complex Data Transformations:** If the API response needs significant transformation or processing before being useful to the LLM, a custom Python tool offers more flexibility.
- **Real-time Streaming (SSE, WebSockets):** The tool is designed for request-response interactions, not for maintaining persistent streaming connections.
For scenarios that exceed these limitations, developing a [Custom Python Tool](/Tools/creating-a-tool) is the recommended approach.

View File

@@ -1,92 +0,0 @@
---
title: Tools Basics - Enhancing DocsGPT Capabilities
description: Understand what DocsGPT Tools are, how they work, and explore the built-in tools available to extend DocsGPT's functionality.
---
import { Callout } from 'nextra/components';
import Image from 'next/image';
import { ToolCards } from '../../components/ToolCards';
# Understanding DocsGPT Tools
DocsGPT Tools are powerful extensions that significantly enhance the capabilities of your DocsGPT application.
They allow DocsGPT to move beyond its core function of retrieving information from your documents and enable it to perform actions,
interact with external data sources, and integrate with other services. You can find and configure available tools within
the "Tools" section of the DocsGPT application settings in the user interface.
## What are Tools?
- **Purpose:** The primary purpose of Tools is to bridge the gap between understanding a user's request (natural language processing by the LLM) and executing a tangible action. This could involve fetching live data from the web, sending notifications, running code snippets, querying databases, or interacting with third-party APIs.
- **LLM as an Orchestrator:** The Large Language Model (LLM) at the heart of DocsGPT is designed to act as an intelligent orchestrator. Based on your query and the declared capabilities of the available tools (defined in their metadata), the LLM decides if a tool is needed, which tool to use, and what parameters to pass to it.
- **Action-Oriented Interactions:** Tools enable more dynamic and action-oriented interactions. For example:
* *"What's the latest news on renewable energy?"* - This might trigger a web search tool to fetch current articles.
* *"Fetch the order status for customer ID 12345 from our database."* - This could use a database tool.
* *"Summarize the content of this webpage and send the summary to the #general channel on Telegram."* - This might involve a web scraping tool followed by a Telegram notification tool.
## Overview of Built-in Tools
DocsGPT includes a suite of pre-built tools designed to expand its capabilities out-of-the-box. Below is an overview of the currently available tools.
<ToolCards
items={[
{
title: 'API Tool',
link: '/Tools/api-tool',
description: 'A highly flexible tool that allows DocsGPT to interact with virtually any API without needing to write custom Python code.'
},
{
title: 'Brave Search Tool',
link: 'https://github.com/arc53/DocsGPT/blob/main/application/agents/tools/brave.py',
description: 'Enables DocsGPT to perform real-time web and image searches using the Brave Search API for up-to-date information.'
},
{
title: 'Cryptoprice Tool',
link: 'https://github.com/arc53/DocsGPT/blob/main/application/agents/tools/cryptoprice.py',
description: 'Fetches the current price of specified cryptocurrencies.'
},
{
title: 'Ntfy Tool',
link: 'https://github.com/arc53/DocsGPT/blob/main/application/agents/tools/ntfy.py',
description: 'Allows DocsGPT to send push notifications to Ntfy.sh channels, ideal for alerts and updates.'
},
{
title: 'PostgreSQL Tool',
link: 'https://github.com/arc53/DocsGPT/blob/main/application/agents/tools/postgres.py',
description: 'Provides capabilities to connect to a PostgreSQL database, execute SQL queries, and retrieve schema information.'
},
{
title: 'Read Webpage Tool', // Renamed from Scraper Tool
link: 'https://github.com/arc53/DocsGPT/blob/main/application/agents/tools/read_webpage.py',
description: 'Enables DocsGPT to fetch and extract (scrape) textual content from specified web page URLs.'
},
{
title: 'Telegram Tool',
link: 'https://github.com/arc53/DocsGPT/blob/main/application/agents/tools/telegram.py',
description: 'Allows DocsGPT to send messages or images to Telegram chats via a Telegram Bot.'
}
]}
/>
## Using Tools in DocsGPT (User Perspective)
Interacting with tools in DocsGPT is designed to be intuitive:
1. **Natural Language Interaction:** As a user, you typically interact with DocsGPT using natural language queries or commands. The LLM within DocsGPT analyzes your input to determine if a specific task can or should be handled by one of the available and configured tools.
2. **Configuration in UI:**
* Tools are generally managed and configured within the DocsGPT application's settings, found under a "Tools" section in the GUI.
* For tools that interact with external services (like Brave Search, Telegram, or any service via the API Tool), you might need to provide authentication credentials (e.g., API keys, tokens) or specific endpoint information during the tool's setup in the UI.
3. **Prompt Engineering for Tools:** While the LLM aims to intelligently use tools, for more complex or reliable agent-like behaviors, you might need to customize the system prompts. Modifying the prompt can guide the LLM on when and how to prioritize or chain tools to achieve specific outcomes, especially if you're building an agent designed to perform a certain sequence of actions every time. For more on this, see [Customising Prompts](/Guides/Customising-prompts).
## Advancing with Tools
Understanding the basics of DocsGPT Tools opens up many possibilities:
* **Leverage the API Tool:** For quick integrations with numerous external services, explore the [API Tool Detailed Guide](/Tools/api-tool).
* **Develop Custom Tools:** If you have specific needs not covered by built-in tools or the generic API tool, you can develop your own. See our guide on `[Developing Custom Tools](/Tools/creating-a-tool)` (placeholder for now).
* **Build AI Agents:** Tools are the fundamental building blocks for creating sophisticated AI agents within DocsGPT. Explore how these can be combined by looking into the `[Agents section/tab concept - link to be added once available]`.
By harnessing the power of Tools, you can transform DocsGPT into a more versatile and proactive assistant tailored to your unique workflows.

View File

@@ -1,186 +0,0 @@
---
title: 🛠️ Creating a Custom Tool
description: Learn how to create custom Python tools to extend DocsGPT's functionality and integrate with various services or perform specific actions.
---
import { Callout } from 'nextra/components';
import { Steps } from 'nextra/components';
# 🛠️ Creating a Custom Python Tool
This guide provides developers with a comprehensive, step-by-step approach to creating their own custom tools for DocsGPT. By developing custom tools, you can significantly extend DocsGPT's capabilities, enabling it to interact with new data sources, services, and perform specialized actions tailored to your unique needs.
## Introduction to Custom Tool Development
### Why Create Custom Tools?
While DocsGPT offers a range of built-in tools and a versatile API Tool, there are many scenarios where a custom Python tool is the best solution:
* **Integrating with Proprietary Systems:** Connect to internal APIs, databases, or services that are not publicly accessible or require complex authentication.
* **Adding Domain-Specific Functionalities:** Implement logic specific to your industry or use case that isn't covered by general-purpose tools.
* **Automating Unique Workflows:** Create tools that orchestrate multiple steps or interact with systems in a way unique to your operational needs.
* **Connecting to Any System with an Accessible Interface:** If you can interact with a system programmatically using Python (e.g., through libraries, SDKs, or direct HTTP requests), you can likely build a DocsGPT tool for it.
* **Complex Logic or Data Transformation:** When API interactions require intricate logic before sending a request or after receiving a response, or when data needs significant transformation that is difficult for an LLM to handle directly.
### Prerequisites
Before you begin, ensure you have:
* A solid understanding of Python programming.
* Familiarity with the DocsGPT project structure, particularly the `application/agents/tools/` directory where custom tools reside.
* Basic knowledge of how APIs work, as many tools involve interacting with external or internal APIs.
* Your DocsGPT development environment set up. If not, please refer to the [Setting Up a Development Environment](/Deploying/Development-Environment) guide.
## The Anatomy of a DocsGPT Tool
Custom tools in DocsGPT are Python classes that inherit from a base `Tool` class and implement specific methods to define their behavior, capabilities, and configuration needs.
The **foundation** for all custom tools is the abstract base class, located in `application/agents/tools/base.py`. Your custom tool class **must** inherit from this class.
### Essential Methods to Implement
Your custom tool class needs to implement the following methods:
1. **`__init__(self, config: dict)`**
- **Purpose:** The constructor for your tool. It's called when DocsGPT initializes the tool.
- **Usage:** This method is typically used to receive and store tool-specific configurations passed via the `config` dictionary. This dictionary is populated based on the tool's settings, often configured through the DocsGPT UI or environment variables. For example, you would store API keys, base URLs, or database connection strings here.
- **Example** (`brave.py`)**:**
``` python
class BraveSearchTool(Tool):
def __init__(self, config):
self.config = config
self.token = config.get("token", "") # API Key for Brave Search
self.base_url = "https://api.search.brave.com/res/v1"
```
2. **`execute_action(self, action_name: str, **kwargs) -> dict`**
- **Purpose:** This is the workhorse of your tool. The LLM, acting as an agent, calls this method when it decides to use one of the actions your tool provides.
- **Parameters:**
- `action_name` (str): A string specifying which of the tool's actions to run (e.g., "brave_web_search").
- `**kwargs` (dict): A dictionary containing the parameters for that specific action. These parameters are defined in the tool's metadata (`get_actions_metadata()`) and are extracted or inferred by the LLM from the user's query.
- **Return Value:** A dictionary containing the result of the action. It's good practice to include keys like:
- `status_code` (int): An HTTP-like status code (e.g., 200 for success, 500 for error).
- `message` (str): A human-readable message describing the outcome.
- `data` (any): The actual data payload returned by the action (if applicable).
- `error` (str): An error message if the action failed.
- **Example (`read_webpage.py`):**
``` python
def execute_action(self, action_name: str, **kwargs) -> str:
if action_name != "read_webpage":
return f"Error: Unknown action '{action_name}'. This tool only supports 'read_webpage'."
url = kwargs.get("url")
if not url:
return "Error: URL parameter is missing."
# ... (logic to fetch and parse webpage) ...
try:
# ...
return markdown_content
except Exception as e:
return f"Error processing URL {url}: {e}"
```
A more structured return:
``` python
# ... inside execute_action
try:
# ... logic ...
return {"status_code": 200, "message": "Webpage read successfully", "data": markdown_content}
except Exception as e:
return {"status_code": 500, "message": f"Error processing URL {url}", "error": str(e)}
```
3. **`get_actions_metadata(self) -> list`**
- **Purpose:** This method is **critical** for the LLM to understand what your tool can do, when to use it, and what parameters it needs. It effectively advertises your tool's capabilities.
- **Return Value:** A list of dictionaries. Each dictionary describes one distinct action the tool can perform and must follow a specific JSON schema structure.
- `name` (str): A unique and descriptive name for the action (e.g., `mytool_get_user_details`). It's a common convention to prefix with the tool name to avoid collisions.
- `description` (str): A clear, concise, and unambiguous description of what the action does. **Write this for the LLM.** The LLM uses this description to decide if this action is appropriate for a given user query.
- `parameters` (dict): A JSON Schema object defining the parameters that the action expects. This schema tells the LLM what arguments are needed, their types, and which are required.
- `type`: Should always be `"object"`.
- `properties`: A dictionary where each key is a parameter name, and the value is an object defining its `type` (e.g., "string", "integer", "boolean") and `description`.
- `required`: A list of strings, where each string is the name of a parameter that is mandatory for the action.
- **Example (`postgres.py` - partial):**
``` python
def get_actions_metadata(self):
return [
{
"name": "postgres_execute_sql",
"description": "Execute an SQL query against the PostgreSQL database...",
"parameters": {
"type": "object",
"properties": {
"sql_query": {
"type": "string",
"description": "The SQL query to execute.",
},
},
"required": ["sql_query"],
"additionalProperties": False, # Good practice to prevent unexpected params
},
},
# ... other actions like postgres_get_schema
]
```
4. **`get_config_requirements(self) -> dict`**
- **Purpose:** Defines the configuration parameters that your tool needs to function (e.g., API keys, specific base URLs, connection strings, default settings). This information can be used by the DocsGPT UI to dynamically render configuration fields for your tool or for validation.
- **Return Value:** A dictionary where keys are the configuration item names (which will be keys in the `config` dict passed to `__init__`) and values are dictionaries describing each requirement:
- `type` (str): The expected data type of the config value (e.g., "string", "boolean", "integer").
- `description` (str): A human-readable description of what this configuration item is for.
- `secret` (bool, optional): Set to `True` if the value is sensitive (e.g., an API key) and should be masked or handled specially in UIs. Defaults to `False`.
- **Example (`brave.py`):**
``` python
def get_config_requirements(self):
return {
"token": { # This 'token' will be a key in the config dict for __init__
"type": "string",
"description": "Brave Search API key for authentication",
"secret": True
},
}
```
## Tool Registration and Discovery
DocsGPT's ToolManager (located in application/agents/tools/tool_manager.py) automatically discovers and loads tools.
As long as your custom tool:
1. Is placed in a Python file within the `application/agents/tools/` directory (and the filename is not `base.py` or starts with `__`).
2. Correctly inherits from the `Tool` base class.
3. Implements all the abstract methods (`execute_action`, `get_actions_metadata`, `get_config_requirements`).
The `ToolManager` should be able to load it when DocsGPT starts.
## Configuration & Secrets Management
- **Configuration Source:** The `config` dictionary passed to your tool's `__init__` method is typically populated from settings defined in the DocsGPT UI (if available for the tool) or from environment variables/configuration files that DocsGPT loads (see [⚙️ App Configuration](/Deploying/DocsGPT-Settings)). The keys in this dictionary should match the names you define in `get_config_requirements()`.
- **Secrets:** Never hardcode secrets (like API keys or passwords) directly into your tool's Python code. Instead, define them as configuration requirements (using `secret: True` in `get_config_requirements()`) and let DocsGPT's configuration system inject them via the `config` dictionary at runtime. This ensures that secrets are managed securely and are not exposed in your codebase.
## Best Practices for Tool Development
- **Atomicity:** Design tool actions to be as atomic (single, well-defined purpose) as possible. This makes them easier for the LLM to understand and combine.
- **Clarity in Metadata:** Ensure action names and descriptions in `get_actions_metadata()` are extremely clear, specific, and unambiguous. This is the primary way the LLM understands your tool.
- **Robust Error Handling:** Implement comprehensive error handling within your `execute_action` logic (and the private methods it calls). Return informative error messages in the result dictionary so the LLM or user can understand what went wrong.
- **Security:**
- Be mindful of the security implications of your tool, especially if it interacts with sensitive systems or can execute arbitrary code/queries.
- Validate and sanitize any inputs, especially if they are used to construct database queries or shell commands, to prevent injection attacks.
- **Performance:** Consider the performance implications of your tool's actions. If an action is slow, it will impact the user experience. Optimize where possible.
## (Optional) Contributing Your Tool
If you develop a custom tool that you believe could be valuable to the broader DocsGPT community and is general-purpose:
1. Ensure it's well-documented (both in code and with clear metadata).
2. Make sure it adheres to the best practices outlined above.
3. Consider opening a Pull Request to the [DocsGPT GitHub repository](https://github.com/arc53/DocsGPT) with your new tool, including any necessary documentation updates.
By following this guide, you can create powerful custom tools that extend DocsGPT's capabilities to your specific operational environment.

View File

@@ -4,8 +4,6 @@
"quickstart": "Quickstart",
"Deploying": "Deploying",
"Models": "Models",
"Tools": "Tools",
"Agents": "Agents",
"Extensions": "Extensions",
"https://gptcloud.arc53.com/": {
"title": "API",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="1 6 38 28" xmlns="http://www.w3.org/2000/svg">
<path d="M3,33.5c-0.827,0-1.5-0.673-1.5-1.5V8c0-0.827,0.673-1.5,1.5-1.5h34c0.827,0,1.5,0.673,1.5,1.5v24 c0,0.827-0.673,1.5-1.5,1.5H3z" style="fill: rgb(7, 106, 255);"/>
<path d="M37,7c0.551,0,1,0.449,1,1v24c0,0.551-0.449,1-1,1H3c-0.551,0-1-0.449-1-1V8c0-0.551,0.449-1,1-1 H37 M37,6H3C1.895,6,1,6.895,1,8v24c0,1.105,0.895,2,2,2h34c1.105,0,2-0.895,2-2V8C39,6.895,38.105,6,37,6L37,6z" style="fill: rgb(7, 106, 255);"/>
<path d="M 19.296 13.226 C 20.066 13.06 21.108 12.955 22.147 12.955 C 23.772 12.955 25.153 13.185 26.047 14.038 C 26.88 14.766 27.255 15.931 27.255 17.118 C 27.255 18.638 26.798 19.718 26.07 20.489 C 25.196 21.426 23.801 21.842 22.656 21.842 C 22.47 21.842 22.302 21.842 22.115 21.821 L 22.115 27.045 L 19.297 27.045 L 19.297 13.226 L 19.296 13.226 Z M 22.114 19.616 C 22.259 19.637 22.405 19.637 22.571 19.637 C 23.945 19.637 24.55 18.657 24.55 17.347 C 24.55 16.119 24.049 15.162 22.78 15.162 C 22.532 15.162 22.281 15.203 22.114 15.266 L 22.114 19.616 Z M 29.158 12.955 L 31.976 12.955 L 31.976 27.045 L 29.158 27.045 L 29.158 12.955 Z M 15.001 27.045 L 17.887 27.045 L 14.91 12.955 L 11.342 12.955 L 8.024 27.045 L 10.91 27.045 L 11.524 24.227 L 14.408 24.227 L 15.001 27.045 Z M 13 15.547 L 13.068 15.547 C 13.205 16.467 13.409 17.888 13.568 18.745 L 14.021 21.409 L 11.942 21.409 L 12.457 18.746 C 12.614 17.93 12.841 16.488 13 15.547 Z" style="fill: rgb(255, 255, 255);"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 194.18 227.53"><defs><style>.cls-1{fill-rule:evenodd;fill:url(#linear-gradient);}.cls-2{fill:#fff;}</style><linearGradient id="linear-gradient" y1="116.23" x2="194.18" y2="116.23" gradientTransform="matrix(1, 0, 0, -1, 0, 230)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff5601"/><stop offset="0.5" stop-color="#ff4000"/><stop offset="1" stop-color="#ff1f01"/></linearGradient></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M187.39,54.58l5.34-13.1s-6.8-7.27-15-15.52S152,22.56,152,22.56L132,0H62.14L42.23,22.56S24.76,17.71,16.51,26s-15,15.52-15,15.52L6.8,54.58,0,74s20,75.65,22.33,84.89c4.61,18.19,7.77,25.22,20.88,34.44S80.1,218.55,84,221s8.74,6.56,13.11,6.56,9.22-4.13,13.11-6.56,27.67-18.43,40.78-27.65,16.26-16.25,20.87-34.44C174.19,149.64,194.18,74,194.18,74Z"/><path class="cls-2" d="M121.85,41c2.91,0,24.51-4.12,24.51-4.12S172,67.8,172,74.41c0,5.47-2.21,7.6-4.8,10.12-.54.53-1.1,1.08-1.66,1.67l-19.2,20.37-.63.64c-1.91,1.92-4.73,4.76-2.74,9.47l.41,1c2.18,5.1,4.87,11.39,1.44,17.78-3.64,6.78-9.89,11.31-13.9,10.56s-13.41-5.66-16.87-7.9S99.6,126.8,99.6,123.35c0-2.89,7.88-7.68,11.71-10,.77-.47,1.37-.83,1.71-1.07l1.88-1.18c3.49-2.17,9.8-6.09,10-7.83.2-2.14.12-2.77-2.69-8.06-.6-1.13-1.3-2.33-2-3.58-2.68-4.61-5.69-9.78-5-13.48.75-4.18,7.3-6.57,12.85-8.6l2-.75,5.78-2.17c5.54-2.07,11.69-4.37,12.71-4.84,1.4-.65,1-1.27-3.22-1.67l-2.06-.21c-5.27-.56-15-1.59-19.71-.28l-3.06.84c-5.31,1.43-11.81,3.19-12.44,4.21-.11.18-.22.33-.32.47-.6.85-1,1.41-.32,5,.19,1.08.6,3.19,1.1,5.81,1.46,7.65,3.75,19.58,4,22.26,0,.38.08.74.13,1.09.36,3,.61,5-2.87,5.77l-.91.21c-3.92.9-9.67,2.22-11.75,2.22s-7.83-1.32-11.76-2.22l-.9-.21c-3.48-.79-3.23-2.78-2.87-5.77,0-.35.09-.71.13-1.09.29-2.68,2.58-14.65,4-22.3.5-2.59.9-4.7,1.1-5.77.66-3.6.27-4.16-.33-5-.1-.14-.21-.29-.32-.47-.62-1-7.13-2.78-12.43-4.21l-3.07-.84C66,58.31,56.25,59.34,51,59.9l-2.06.21c-4.26.4-4.62,1-3.22,1.67,1,.47,7.17,2.77,12.71,4.84l5.78,2.17,2,.75c5.55,2,12.1,4.42,12.85,8.6.67,3.7-2.34,8.87-5,13.48-.72,1.25-1.43,2.45-2,3.58-2.82,5.29-2.9,5.92-2.7,8.06.16,1.74,6.47,5.66,10,7.83.82.5,1.48.92,1.88,1.18s.94.6,1.71,1.06c3.83,2.33,11.71,7.13,11.71,10,0,3.45-11,12.49-14.42,14.73S67.3,145.24,63.29,146,53,142.2,49.39,135.42c-3.43-6.38-.74-12.68,1.44-17.78l.41-1c2-4.71-.83-7.55-2.74-9.47l-.63-.64L28.67,86.2c-.56-.59-1.12-1.14-1.66-1.67-2.59-2.52-4.79-4.65-4.79-10.12,0-6.61,25.6-37.53,25.6-37.53S69.42,41,72.33,41c2.33,0,6.82-1.55,11.49-3.16l3.56-1.21a34.33,34.33,0,0,1,9.71-2,34.33,34.33,0,0,1,9.71,2c1.18.39,2.37.81,3.56,1.21C115,39.45,119.52,41,121.85,41Z"/><path class="cls-2" d="M118.14,150.39c4.57,2.35,7.81,4,9,4.78,1.59,1,.62,2.86-.82,3.88s-20.85,16-22.73,17.69l-.76.68c-1.82,1.64-4.13,3.72-5.77,3.72s-4-2.08-5.77-3.72l-.76-.68c-1.88-1.66-21.28-16.67-22.73-17.69s-2.41-2.89-.82-3.88c1.23-.77,4.47-2.44,9-4.79l4.34-2.24c6.84-3.54,15.37-6.54,16.7-6.54s9.86,3,16.7,6.54Z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.88 122.88"><path d="M17.89 0h88.9c8.85 0 16.1 7.24 16.1 16.1v90.68c0 8.85-7.24 16.1-16.1 16.1H16.1c-8.85 0-16.1-7.24-16.1-16.1v-88.9C0 8.05 8.05 0 17.89 0zm57.04 66.96l16.46 4.96c-1.1 4.61-2.84 8.47-5.23 11.56-2.38 3.1-5.32 5.43-8.85 7-3.52 1.57-8.01 2.36-13.45 2.36-6.62 0-12.01-.96-16.21-2.87-4.19-1.92-7.79-5.3-10.83-10.13-3.04-4.82-4.57-11.02-4.57-18.54 0-10.04 2.67-17.76 8.02-23.17 5.36-5.39 12.93-8.09 22.71-8.09 7.65 0 13.68 1.54 18.06 4.64 4.37 3.1 7.64 7.85 9.76 14.27l-16.55 3.66c-.58-1.84-1.19-3.18-1.82-4.03-1.06-1.43-2.35-2.53-3.86-3.3-1.53-.78-3.22-1.16-5.11-1.16-4.27 0-7.54 1.71-9.8 5.12-1.71 2.53-2.57 6.52-2.57 11.94 0 6.73 1.02 11.33 3.07 13.83 2.05 2.49 4.92 3.73 8.63 3.73 3.59 0 6.31-1 8.15-3.03 1.83-1.99 3.16-4.92 3.99-8.75z" fill-rule="evenodd" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 855 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="-8.78 0 70 70" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg">
<metadata>
<rdf:RDF>
<cc:Work>
<dc:subject>
Data
</dc:subject>
<dc:identifier>
sql-database-generic
</dc:identifier>
<dc:title>
SQL Database (Generic)
</dc:title>
<dc:format>
image/svg+xml
</dc:format>
<dc:publisher>
Amido Limited
</dc:publisher>
<dc:creator>
Richard Slater
</dc:creator>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
</cc:Work>
</rdf:RDF>
</metadata>
<path d="m 852.97077,1013.9363 c -6.55238,-0.4723 -13.02857,-2.1216 -17.00034,-4.3296 -2.26232,-1.2576 -3.98589,-2.8032 -4.66223,-4.1807 l -0.4024,-0.8196 0,-25.70807 0,-25.7081 0.31843,-0.6465 c 1.42297,-2.889 5.96432,-5.4935 12.30378,-7.0562 2.15195,-0.5305 5.2586,-1.0588 7.79304,-1.3252 2.58797,-0.2721 9.44765,-0.2307 12.02919,0.073 6.86123,0.8061 12.69967,2.6108 16.29768,5.0377 1.38756,0.9359 2.81137,2.4334 3.29371,3.4642 l 0.41358,0.8838 -0.0354,25.6303 -0.0354,25.63047 -0.33195,0.6744 c -0.18257,0.3709 -0.73406,1.1007 -1.22553,1.6216 -2.99181,3.1715 -9.40919,5.5176 -17.8267,6.5172 -1.71567,0.2038 -9.16916,0.3686 -10.92937,0.2417 z m 12.07501,-22.02839 c -0.0252,-0.0657 -1.00472,-0.93831 -2.17671,-1.93922 -1.17199,-1.00091 -2.18138,-1.86687 -2.24309,-1.92436 -0.0617,-0.0575 0.15481,-0.26106 0.48117,-0.45237 0.32635,-0.19131 0.95163,-0.7235 1.3895,-1.18265 1.2805,-1.34272 1.88466,-3.00131 1.88466,-5.17388 0,-2.1388 -0.65162,-3.8645 -1.95671,-5.1818 -1.31533,-1.3278 -2.82554,-1.8983 -5.02486,-1.8983 -3.39007,0 -5.99368,1.9781 -6.82468,5.1851 -0.28586,1.1031 -0.28432,3.33211 0.003,4.31023 0.74941,2.55136 2.79044,4.40434 5.33062,4.83946 0.8596,0.14724 0.97605,0.21071 1.5621,0.85144 0.34829,0.38078 1.06301,1.14085 1.58827,1.68904 l 0.95501,0.9967 2.53878,0 c 1.39633,0 2.51816,-0.0537 2.49296,-0.11939 z m -8.70653,-7.10848 c -0.61119,-0.31868 -0.84225,-0.56599 -1.19079,-1.27453 -0.26919,-0.54724 -0.31522,-0.85851 -0.31824,-2.15197 -0.003,-1.3143 0.0388,-1.5983 0.31987,-2.169 0.45985,-0.9339 1.09355,-1.376 2.07384,-1.4469 1.36454,-0.099 2.15217,0.5707 2.56498,2.1801 0.50612,1.97321 -0.0504,4.07107 -1.26471,4.76729 -0.63707,0.36527 -1.58737,0.40659 -2.18495,0.095 z m -11.25315,3.66269 c 2.66179,-0.5048 4.1728,-2.0528 4.1728,-4.27495 0,-1.97137 -0.97548,-3.12004 -3.6716,-4.32364 -1.54338,-0.689 -2.10241,-1.1215 -2.10241,-1.6268 0,-0.4188 0.53052,-0.8777 1.14813,-0.993 0.60302,-0.1126 2.20237,0.1652 3.14683,0.5467 l 0.79167,0.3198 0,-1.7524 0,-1.7525 -0.85923,-0.1906 c -0.53103,-0.1178 -1.64689,-0.1885 -2.92137,-0.1849 -1.80528,0 -2.15881,0.044 -2.83818,0.3138 -1.98445,0.7878 -2.92613,2.1298 -2.91107,4.1485 0.0141,1.8898 1.01108,3.06864 3.49227,4.12912 1.46399,0.62572 2.05076,1.10218 2.05076,1.66522 0,1.1965 -1.99362,1.34375 -4.10437,0.30315 -0.57805,-0.28498 -1.09739,-0.54137 -1.1541,-0.56976 -0.0567,-0.0284 -0.10311,0.79023 -0.10311,1.81917 0,1.86239 0.002,1.87137 0.33919,1.99974 1.26979,0.48278 4.07626,0.69787 5.52379,0.42335 z m 30.4308,-1.72766 0,-1.58098 -2.40584,0 -2.40583,0 0,-5.43035 0,-5.4303 -2.13089,0 -2.13088,0 0,7.0113 0,7.01131 4.53672,0 4.53672,0 0,-1.58098 z m -14.84745,-27.70503 c 4.23447,-0.2937 7.4086,-0.8482 10.20178,-1.7821 2.78264,-0.9304 4.42643,-2.0562 4.79413,-3.2834 0.14166,-0.4729 0.13146,-0.6523 -0.0665,-1.1708 -0.88775,-2.3245 -5.84694,-4.1104 -13.42493,-4.8345 -3.24154,-0.3098 -9.13671,-0.2094 -12.22745,0.2081 -4.71604,0.6372 -8.54333,1.8208 -10.2451,3.1683 -3.44251,2.726 0.19793,5.7242 8.66397,7.1354 3.67084,0.6119 8.42674,0.828 12.30414,0.559 z" fill="#00bcf2" transform="translate(-830.906 -943.981)"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-155.5t86-127Q252-817 325-848.5T480-880q83 0 155.5 31.5t127 86q54.5 54.5 86 127T880-480q0 82-31.5 155t-86 127.5q-54.5 54.5-127 86T480-80Zm0-82q26-36 45-75t31-83H404q12 44 31 83t45 75Zm-104-16q-18-33-31.5-68.5T322-320H204q29 50 72.5 87t99.5 55Zm208 0q56-18 99.5-55t72.5-87H638q-9 38-22.5 73.5T584-178ZM170-400h136q-3-20-4.5-39.5T300-480q0-21 1.5-40.5T306-560H170q-5 20-7.5 39.5T160-480q0 21 2.5 40.5T170-400Zm216 0h188q3-20 4.5-39.5T580-480q0-21-1.5-40.5T574-560H386q-3 20-4.5 39.5T380-480q0 21 1.5 40.5T386-400Zm268 0h136q5-20 7.5-39.5T800-480q0-21-2.5-40.5T790-560H654q3 20 4.5 39.5T660-480q0 21-1.5 40.5T654-400Zm-16-240h118q-29-50-72.5-87T584-782q18 33 31.5 68.5T638-640Zm-234 0h152q-12-44-31-83t-45-75q-26 36-45 75t-31 83Zm-200 0h118q9-38 22.5-73.5T376-782q-56 18-99.5 55T204-640Z"/></svg>

Before

Width:  |  Height:  |  Size: 976 B

View File

@@ -1,10 +0,0 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 0.5C8.81812 0.5 5.76375 1.76506 3.51562 4.01469C1.2652 6.26522 0.000643966 9.31734 0 12.5C0 15.6813 1.26562 18.7357 3.51562 20.9853C5.76375 23.2349 8.81812 24.5 12 24.5C15.1819 24.5 18.2362 23.2349 20.4844 20.9853C22.7344 18.7357 24 15.6813 24 12.5C24 9.31869 22.7344 6.26431 20.4844 4.01469C18.2362 1.76506 15.1819 0.5 12 0.5Z" fill="url(#paint0_linear_5586_9958)"/>
<path d="M5.43282 12.373C8.93157 10.849 11.2641 9.8443 12.4303 9.3588C15.7641 7.97261 16.4559 7.73186 16.9078 7.7237C17.0072 7.72211 17.2284 7.74667 17.3728 7.86339C17.4928 7.96183 17.5266 8.09495 17.5434 8.18842C17.5584 8.2818 17.5791 8.49461 17.5622 8.66074C17.3822 10.5582 16.6003 15.1629 16.2028 17.2882C16.0359 18.1874 15.7041 18.4889 15.3834 18.5184C14.6859 18.5825 14.1572 18.0579 13.4822 17.6155C12.4266 16.9231 11.8303 16.4922 10.8047 15.8167C9.6197 15.0359 10.3884 14.6067 11.0634 13.9055C11.2397 13.7219 14.3109 10.9291 14.3691 10.6758C14.3766 10.6441 14.3841 10.526 14.3128 10.4637C14.2434 10.4013 14.1403 10.4227 14.0653 10.4395C13.9584 10.4635 12.2728 11.5788 9.00282 13.7851C8.52469 14.114 8.09157 14.2743 7.70157 14.2659C7.27407 14.2567 6.44907 14.0236 5.83595 13.8245C5.08595 13.5802 4.48782 13.451 4.54032 13.036C4.56657 12.82 4.8647 12.599 5.43282 12.373Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_5586_9958" x1="1200" y1="0.5" x2="1200" y2="2400.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#2AABEE"/>
<stop offset="1" stop-color="#229ED9"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "docsgpt",
"version": "0.5.1",
"version": "0.5.0",
"private": false,
"description": "DocsGPT 🦖 is an innovative open-source tool designed to simplify the retrieval of information from project documentation using advanced GPT models 🤖.",
"source": "./src/index.html",

View File

@@ -4,19 +4,19 @@ import { WidgetCore } from './DocsGPTWidget';
import { SearchBarProps } from '@/types';
import { getSearchResults } from '../requests/searchAPI';
import { Result } from '@/types';
import MarkdownIt from 'markdown-it';
import { getOS, processMarkdownString } from '../utils/helper';
import DOMPurify from 'dompurify';
import {
CodeIcon,
import {
CodeIcon,
TextAlignLeftIcon,
HeadingIcon,
ReaderIcon,
ListBulletIcon,
QuoteIcon
ReaderIcon,
ListBulletIcon,
QuoteIcon
} from '@radix-ui/react-icons';
const themes = {
dark: {
name: 'dark',
bg: '#202124',
text: '#EDEDED',
primary: {
@@ -29,7 +29,6 @@ const themes = {
}
},
light: {
name: 'light',
bg: '#EAEAEA',
text: '#171717',
primary: {
@@ -45,16 +44,15 @@ const themes = {
const GlobalStyle = createGlobalStyle`
.highlight {
color: ${props => props.theme.name === 'dark' ? '#4B9EFF' : '#0066CC'};
font-weight: 500;
color:#007EE6;
}
`;
const loadGeistFont = () => {
const link = document.createElement('link');
link.href = 'https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap';
link.rel = 'stylesheet';
document.head.appendChild(link);
const link = document.createElement('link');
link.href = 'https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap';
link.rel = 'stylesheet';
document.head.appendChild(link);
};
const Main = styled.div`
@@ -83,27 +81,12 @@ const Container = styled.div`
position: relative;
display: inline-block;
`
const SearchOverlay = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #0000001A;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 99;
`;
const SearchResults = styled.div`
position: fixed;
display: flex;
flex-direction: column;
background-color: ${props => props.theme.name === 'dark' ?
'rgba(0, 0, 0, 0.15)' :
'rgba(255, 255, 255, 0.4)'};
border: 1px solid rgba(255, 255, 255, 0.18);
background-color: ${props => props.theme.primary.bg};
border: 1px solid ${props => props.theme.bg};
border-radius: 15px;
padding: 8px 0px 8px 0px;
width: 792px;
@@ -114,12 +97,8 @@ const SearchResults = styled.div`
top: 50%;
transform: translate(-50%, -50%);
color: ${props => props.theme.primary.text};
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
backdrop-filter: blur(82px);
-webkit-backdrop-filter: blur(82px);
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(16px);
box-sizing: border-box;
@media only screen and (max-width: 768px) {
@@ -163,33 +142,6 @@ const ContentWrapper = styled.div`
flex-direction: column;
gap: 12px;
`;
const ResultWrapper = styled.div`
display: flex;
align-items: flex-start;
width: 100%;
box-sizing: border-box;
padding: 8px 16px;
cursor: pointer;
background-color: transparent;
font-family: 'Geist', sans-serif;
border-radius: 8px;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-word;
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
`;
const Content = styled.div`
display: flex;
margin-left: 8px;
@@ -199,10 +151,9 @@ const Content = styled.div`
font-size: 15px;
color: ${props => props.theme.primary.text};
line-height: 1.6;
border-left: 2px solid ${props => props.theme.primary.text}CC;
border-left: 2px solid #585858;
overflow: hidden;
`;
`
const ContentSegment = styled.div`
display: flex;
align-items: flex-start;
@@ -214,6 +165,80 @@ const ContentSegment = styled.div`
text-overflow: ellipsis;
`
const ResultWrapper = styled.div`
display: flex;
align-items: flex-start;
width: 100%;
box-sizing: border-box;
padding: 8px 16px;
cursor: pointer;
background-color: ${props => props.theme.primary.bg};
font-family: 'Geist', sans-serif;
transition: background-color 0.2s;
border-radius: 8px;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-word;
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
background-color: ${props => props.theme.bg};
}
`
const Markdown = styled.div`
line-height:18px;
font-size: 11px;
white-space: pre-wrap;
pre {
padding: 8px;
width: 90%;
font-size: 11px;
border-radius: 6px;
overflow-x: auto;
background-color: #1B1C1F;
color: #fff ;
}
h1,h2 {
font-size: 14px;
font-weight: 600;
color: ${(props) => props.theme.text};
opacity: 0.8;
}
h3 {
font-size: 12px;
}
p {
margin: 0px;
line-height: 1.35rem;
font-size: 11px;
}
code:not(pre code) {
border-radius: 6px;
padding: 2px 2px;
margin: 2px;
font-size: 9px;
display: inline;
background-color: #646464;
color: #fff ;
}
img{
max-width: 50%;
}
code {
overflow-x: auto;
}
a{
color: #007ee6;
}
`
const Toolkit = styled.kbd`
position: absolute;
right: 4px;
@@ -234,8 +259,8 @@ const Toolkit = styled.kbd`
`
const Loader = styled.div`
margin: 2rem auto;
border: 4px solid ${props => props.theme.name === 'dark' ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)'};
border-top: 4px solid ${props => props.theme.name === 'dark' ? '#FFFFFF' : props.theme.primary.bg};
border: 4px solid ${props => props.theme.secondary.text};
border-top: 4px solid ${props => props.theme.primary.bg};
border-radius: 50%;
width: 12px;
height: 12px;
@@ -255,8 +280,7 @@ const NoResults = styled.div`
margin-top: 2rem;
text-align: center;
font-size: 14px;
color: ${props => props.theme.name === 'dark' ? '#E0E0E0' : '#505050'};
font-weight: 500;
color: #888;
`;
const AskAIButton = styled.button`
display: flex;
@@ -269,35 +293,25 @@ const AskAIButton = styled.button`
height: 50px;
padding: 8px 24px;
border: none;
border-radius: 8px;
border-radius: 6px;
background-color: ${props => props.theme.bg};
color: ${props => props.theme.text};
cursor: pointer;
transition: background-color 0.2s, box-shadow 0.2s;
font-size: 16px;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
background-color: ${props => props.theme.name === 'dark' ?
'rgba(255, 255, 255, 0.05)' :
'rgba(0, 0, 0, 0.03)'};
&:hover {
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
background-color: ${props => props.theme.name === 'dark' ?
'rgba(255, 255, 255, 0.1)' :
'rgba(0, 0, 0, 0.06)'};
opacity: 0.8;
}
`;
`
const SearchHeader = styled.div`
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid ${props => props.theme.name === 'dark' ? '#FFFFFF24' : 'rgba(0, 0, 0, 0.14)'};
`;
border-bottom: 1px solid ${props => props.theme.bg};
`
const TextField = styled.input`
width: calc(100% - 32px);
@@ -313,16 +327,8 @@ const TextField = styled.input`
&:focus {
border-color: none;
}
&::placeholder {
color: ${props => props.theme.name === 'dark' ? 'rgba(255, 255, 255, 0.6)' : 'rgba(0, 0, 0, 0.5)'} !important;
opacity: 100%; /* Force opacity to ensure placeholder is visible */
font-weight: 500;
}
`
const EscapeInstruction = styled.kbd`
display: flex;
align-items: center;
@@ -331,21 +337,17 @@ const EscapeInstruction = styled.kbd`
padding: 4px 8px;
border-radius: 4px;
background-color: transparent;
border: 1px solid ${props => props.theme.name === 'dark' ?
'rgba(237, 237, 237, 0.6)' :
'rgba(23, 23, 23, 0.6)'};
color: ${props => props.theme.name === 'dark' ? '#EDEDED' : '#171717'};
border: 1px solid ${props => props.theme.secondary.text};
color: ${props => props.theme.text};
font-size: 12px;
font-family: 'Geist', sans-serif;
white-space: nowrap;
cursor: pointer;
width: fit-content;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
`;
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
`
export const SearchBar = ({
apiKey = "74039c6d-bff7-44ce-ae55-2973cbf13837",
apiHost = "https://gptcloud.arc53.com",
@@ -365,7 +367,7 @@ export const SearchBar = ({
const abortControllerRef = React.useRef<AbortController | null>(null);
const browserOS = getOS();
const isTouch = 'ontouchstart' in window;
const getKeyboardInstruction = () => {
if (isResultVisible) return "Enter";
return browserOS === 'mac' ? '⌘ + K' : 'Ctrl + K';
@@ -392,7 +394,7 @@ export const SearchBar = ({
}
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleKeyDown);
return () => {
@@ -402,34 +404,33 @@ export const SearchBar = ({
}, []);
React.useEffect(() => {
if (!input) {
setResults([]);
setLoading(false);
return;
}
setLoading(true);
if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current);
}
if (!input) {
setResults([]);
return;
}
setLoading(true);
if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current);
}
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const abortController = new AbortController();
abortControllerRef.current = abortController;
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const abortController = new AbortController();
abortControllerRef.current = abortController;
debounceTimeout.current = setTimeout(() => {
getSearchResults(input, apiKey, apiHost, abortController.signal)
.then((data) => setResults(data))
.catch((err) => !abortController.signal.aborted && console.log(err))
.finally(() => setLoading(false));
}, 500);
debounceTimeout.current = setTimeout(() => {
getSearchResults(input, apiKey, apiHost, abortController.signal)
.then((data) => setResults(data))
.catch((err) => !abortController.signal.aborted && console.log(err))
.finally(() => setLoading(false));
}, 500);
return () => {
abortController.abort();
clearTimeout(debounceTimeout.current ?? undefined);
};
}, [input])
return () => {
abortController.abort();
clearTimeout(debounceTimeout.current ?? undefined);
};
}, [input])
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
@@ -461,8 +462,6 @@ export const SearchBar = ({
</SearchButton>
{
isResultVisible && (
<>
<SearchOverlay onClick={() => setIsResultVisible(false)} />
<SearchResults>
<SearchHeader>
<TextField
@@ -478,8 +477,8 @@ export const SearchBar = ({
</EscapeInstruction>
</SearchHeader>
<AskAIButton onClick={openWidget}>
<img
src="https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png"
<img
src="https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png"
alt="DocsGPT"
width={24}
height={24}
@@ -540,7 +539,6 @@ export const SearchBar = ({
)}
</SearchResultsScroll>
</SearchResults>
</>
)
}
{

File diff suppressed because it is too large Load Diff

View File

@@ -34,9 +34,9 @@
"react-helmet": "^6.1.0",
"react-i18next": "^15.4.0",
"react-markdown": "^9.0.1",
"react-redux": "^9.2.0",
"react-router-dom": "^7.6.1",
"react-syntax-highlighter": "^15.6.1",
"react-redux": "^8.0.5",
"react-router-dom": "^7.1.1",
"react-syntax-highlighter": "^15.5.0",
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0"
@@ -52,13 +52,13 @@
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.13",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.5",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard-with-typescript": "^34.0.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-n": "^15.7.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-promise": "^6.6.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react": "^7.37.3",
"eslint-plugin-unused-imports": "^4.1.4",
"husky": "^8.0.0",
"lint-staged": "^15.3.0",
@@ -66,8 +66,8 @@
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vite-plugin-svgr": "^4.3.0"
"typescript": "^5.7.2",
"vite": "^5.4.14",
"vite-plugin-svgr": "^4.2.0"
}
}

99
frontend/src/About.tsx Normal file
View File

@@ -0,0 +1,99 @@
//TODO - Add hyperlinks to text
//TODO - Styling
import DocsGPT3 from './assets/cute_docsgpt3.svg';
export default function About() {
return (
<div className="mx-5 grid min-h-screen md:mx-36">
<article className="place-items-left mx-auto my-auto flex w-full max-w-6xl flex-col gap-4 rounded-3xl bg-gray-100 p-6 text-jet dark:bg-gun-metal dark:text-bright-gray lg:p-6 xl:p-10">
<div className="flex items-center">
<p className="mr-2 text-3xl">About DocsGPT</p>
<img className="h14 mb-2" src={DocsGPT3} alt="DocsGPT" />
</div>
<p className="mt-4">
Find the information in your documentation through AI-powered
<a
className="text-blue-500"
href="https://github.com/arc53/DocsGPT"
target="_blank"
rel="noreferrer"
>
{' '}
open-source{' '}
</a>
chatbot. Powered by GPT-3, Faiss and LangChain.
</p>
<div>
<p>
If you want to add your own documentation, please follow the
instruction below:
</p>
<p className="ml-2 mt-4">
1. Navigate to{' '}
<span className="bg-gray-200 italic dark:bg-outer-space">
{' '}
/application
</span>{' '}
folder
</p>
<p className="ml-2 mt-4">
2. Install dependencies from{' '}
<span className="bg-gray-200 italic dark:bg-outer-space">
pip install -r requirements.txt
</span>
</p>
<p className="ml-2 mt-4">
3. Prepare a{' '}
<span className="bg-gray-200 italic dark:bg-outer-space">.env</span>{' '}
file. Copy{' '}
<span className="bg-gray-200 italic dark:bg-outer-space">
.env_sample
</span>{' '}
and create{' '}
<span className="bg-gray-200 italic dark:bg-outer-space">.env</span>{' '}
with your OpenAI API token
</p>
<p className="ml-2 mt-4">
4. Run the app with{' '}
<span className="bg-gray-200 italic dark:bg-outer-space">
python app.py
</span>
</p>
</div>
<p>
Currently It uses{' '}
<span className="font-medium text-blue-950">DocsGPT</span>{' '}
documentation, so it will respond to information relevant to{' '}
<span className="font-medium text-blue-950">DocsGPT</span>. If you
want to train it on different documentation - please follow
<a
className="text-blue-500"
href="https://github.com/arc53/DocsGPT/wiki/How-to-train-on-other-documentation"
target="_blank"
rel="noreferrer"
>
{' '}
this guide
</a>
.
</p>
<p className="mt-4 text-left">
If you want to launch it on your own server - follow
<a
className="text-blue-500"
href="https://github.com/arc53/DocsGPT/wiki/Hosting-the-app"
target="_blank"
rel="noreferrer"
>
{' '}
this guide
</a>
.
</p>
</article>
</div>
);
}

View File

@@ -3,9 +3,7 @@ import './locale/i18n';
import { useState } from 'react';
import { Outlet, Route, Routes } from 'react-router-dom';
import Agents from './agents';
import SharedAgentGate from './agents/SharedAgentGate';
import ActionButtons from './components/ActionButtons';
import About from './About';
import Spinner from './components/Spinner';
import Conversation from './conversation/Conversation';
import { SharedConversation } from './conversation/SharedConversation';
@@ -14,6 +12,7 @@ import useTokenAuth from './hooks/useTokenAuth';
import Navigation from './Navigation';
import PageNotFound from './PageNotFound';
import Setting from './settings';
import Agents from './agents';
function AuthWrapper({ children }: { children: React.ReactNode }) {
const { isAuthLoading } = useTokenAuth();
@@ -29,18 +28,17 @@ function AuthWrapper({ children }: { children: React.ReactNode }) {
}
function MainLayout() {
const { isMobile, isTablet } = useMediaQuery();
const [navOpen, setNavOpen] = useState(!(isMobile || isTablet));
const { isMobile } = useMediaQuery();
const [navOpen, setNavOpen] = useState(!isMobile);
return (
<div className="relative h-screen overflow-hidden dark:bg-raisin-black">
<div className="relative h-screen overflow-auto dark:bg-raisin-black">
<Navigation navOpen={navOpen} setNavOpen={setNavOpen} />
<ActionButtons showNewChat={true} showShare={true} />
<div
className={`h-[calc(100dvh-64px)] overflow-auto lg:h-screen ${
!(isMobile || isTablet)
? `ml-0 ${!navOpen ? 'lg:mx-auto' : 'lg:ml-72'}`
: 'ml-0 lg:ml-16'
className={`h-[calc(100dvh-64px)] md:h-screen ${
!isMobile
? `ml-0 ${!navOpen ? 'md:mx-auto lg:mx-auto' : 'md:ml-72'}`
: 'ml-0 md:ml-16'
}`}
>
<Outlet />
@@ -48,13 +46,14 @@ function MainLayout() {
</div>
);
}
export default function App() {
const [, , componentMounted] = useDarkTheme();
if (!componentMounted) {
return <div />;
}
return (
<div className="relative h-full overflow-hidden">
<div className="relative h-full overflow-auto">
<Routes>
<Route
element={
@@ -64,11 +63,11 @@ export default function App() {
}
>
<Route index element={<Conversation />} />
<Route path="/about" element={<About />} />
<Route path="/settings/*" element={<Setting />} />
<Route path="/agents/*" element={<Agents />} />
</Route>
<Route path="/share/:identifier" element={<SharedConversation />} />
<Route path="/shared/agent/:agentId" element={<SharedAgentGate />} />
<Route path="/*" element={<PageNotFound />} />
</Routes>
</div>

View File

@@ -42,7 +42,6 @@ import {
selectConversations,
selectModalStateDeleteConv,
selectSelectedAgent,
selectSharedAgents,
selectToken,
setAgents,
setConversations,
@@ -68,10 +67,9 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
const conversationId = useSelector(selectConversationId);
const modalStateDeleteConv = useSelector(selectModalStateDeleteConv);
const agents = useSelector(selectAgents);
const sharedAgents = useSelector(selectSharedAgents);
const selectedAgent = useSelector(selectSelectedAgent);
const { isMobile, isTablet } = useMediaQuery();
const { isMobile } = useMediaQuery();
const [isDarkTheme] = useDarkTheme();
const { showTokenModal, handleTokenSubmit } = useTokenAuth();
@@ -131,7 +129,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
useEffect(() => {
fetchRecentAgents();
}, [agents, sharedAgents, token, dispatch]);
}, [agents, token, dispatch]);
useEffect(() => {
if (!conversations?.data) fetchConversations();
@@ -162,7 +160,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
const handleAgentClick = (agent: Agent) => {
resetConversation();
dispatch(setSelectedAgent(agent));
if (isMobile || isTablet) setNavOpen(!navOpen);
if (isMobile) setNavOpen(!navOpen);
navigate('/');
};
@@ -181,54 +179,33 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
dispatch(setSelectedAgent(null));
conversationService
.getConversation(index, token)
.then((response) => {
if (!response.ok) {
navigate('/');
dispatch(setSelectedAgent(null));
return null;
}
return response.json();
})
.then((response) => response.json())
.then((data) => {
if (!data) return;
dispatch(setConversation(data.queries));
dispatch(
updateConversationId({
query: { conversationId: index },
}),
);
if (isMobile || isTablet) {
setNavOpen(false);
}
if (data.agent_id) {
if (data.is_shared_usage) {
userService
.getSharedAgent(data.shared_token, token)
.then((response) => {
if (!response.ok) {
navigate('/');
dispatch(setSelectedAgent(null));
return;
if (response.ok) {
response.json().then((agent: Agent) => {
navigate(`/agents/shared/${agent.shared_token}`);
});
}
response.json().then((agent: Agent) => {
navigate(`/agents/shared/${agent.shared_token}`);
});
});
} else {
userService.getAgent(data.agent_id, token).then((response) => {
if (!response.ok) {
navigate('/');
dispatch(setSelectedAgent(null));
return;
}
response.json().then((agent: Agent) => {
if (agent.shared_token)
navigate(`/agents/shared/${agent.shared_token}`);
else {
dispatch(setSelectedAgent(agent));
if (response.ok) {
response.json().then((agent: Agent) => {
navigate('/');
}
});
dispatch(setSelectedAgent(agent));
});
}
});
}
} else {
@@ -274,14 +251,14 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
}
useEffect(() => {
setNavOpen(!(isMobile || isTablet));
}, [isMobile, isTablet]);
setNavOpen(!isMobile);
}, [isMobile]);
useDefaultDocument();
return (
<>
{!navOpen && (
<div className="duration-25 absolute left-3 top-3 z-20 hidden transition-all lg:block">
<div className="duration-25 absolute left-3 top-3 z-20 hidden transition-all md:block">
<div className="flex items-center gap-3">
<button
onClick={() => {
@@ -355,7 +332,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
<NavLink
to={'/'}
onClick={() => {
if (isMobile || isTablet) {
if (isMobile) {
setNavOpen(!navOpen);
}
resetConversation();
@@ -418,7 +395,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
</p>
</div>
<div
className={`${isMobile || isTablet ? 'flex' : 'invisible flex group-hover:visible'} items-center px-3`}
className={`${isMobile ? 'flex' : 'invisible flex group-hover:visible'} items-center px-3`}
>
<button
className="rounded-full hover:opacity-75"
@@ -440,9 +417,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
className="mx-4 my-auto mt-2 flex h-9 cursor-pointer items-center gap-2 rounded-3xl pl-4 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
onClick={() => {
dispatch(setSelectedAgent(null));
if (isMobile || isTablet) {
setNavOpen(false);
}
navigate('/agents');
}}
>
@@ -454,7 +428,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
/>
</div>
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm leading-6 text-eerie-black dark:text-bright-gray">
{t('manageAgents')}
Manage Agents
</p>
</div>
</div>
@@ -462,13 +436,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
) : (
<div
className="mx-4 my-auto mt-2 flex h-9 cursor-pointer items-center gap-2 rounded-3xl pl-4 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
onClick={() => {
if (isMobile || isTablet) {
setNavOpen(false);
}
dispatch(setSelectedAgent(null));
navigate('/agents');
}}
onClick={() => navigate('/agents')}
>
<div className="flex w-6 justify-center">
<img
@@ -478,7 +446,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
/>
</div>
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm leading-6 text-eerie-black dark:text-bright-gray">
{t('manageAgents')}
Manage Agents
</p>
</div>
)}
@@ -514,8 +482,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
<div className="flex flex-col gap-2 border-b-[1px] py-2 dark:border-b-purple-taupe">
<NavLink
onClick={() => {
if (isMobile || isTablet) {
setNavOpen(false);
if (isMobile) {
setNavOpen(!navOpen);
}
resetConversation();
}}
@@ -585,10 +553,10 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
</div>
</div>
</div>
<div className="sticky z-10 h-16 w-full border-b-2 bg-gray-50 dark:border-b-purple-taupe dark:bg-chinese-black lg:hidden">
<div className="sticky z-10 h-16 w-full border-b-2 bg-gray-50 dark:border-b-purple-taupe dark:bg-chinese-black md:hidden">
<div className="ml-6 flex h-full items-center gap-6">
<button
className="h-6 w-6 lg:hidden"
className="h-6 w-6 md:hidden"
onClick={() => setNavOpen(true)}
>
<img

View File

@@ -65,18 +65,22 @@ export default function AgentPreview() {
);
const handleQuestionSubmission = (
question?: string,
updatedQuestion?: string,
updated?: boolean,
indx?: number,
) => {
if (updated === true && question !== undefined && indx !== undefined) {
if (
updated === true &&
updatedQuestion !== undefined &&
indx !== undefined
) {
handleQuestion({
question,
question: updatedQuestion,
index: indx,
isRetry: false,
});
} else if (question && status !== 'loading') {
const currentInput = question.trim();
} else if (input.trim() && status !== 'loading') {
const currentInput = input.trim();
if (lastQueryReturnedErr && queries.length > 0) {
const lastQueryIndex = queries.length - 1;
handleQuestion({
@@ -91,6 +95,14 @@ export default function AgentPreview() {
index: undefined,
});
}
setInput('');
}
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
handleQuestionSubmission();
}
};
@@ -123,7 +135,9 @@ export default function AgentPreview() {
</div>
<div className="flex w-[95%] max-w-[1500px] flex-col items-center gap-4 pb-2 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
<MessageInput
onSubmit={(text) => handleQuestionSubmission(text)}
value={input}
onChange={(e) => setInput(e.target.value)}
onSubmit={() => handleQuestionSubmission()}
loading={status === 'loading'}
showSourceButton={selectedAgent ? false : true}
showToolButton={selectedAgent ? false : true}

View File

@@ -21,7 +21,6 @@ import {
import { useDarkTheme } from '../hooks';
import { selectToken, setSelectedAgent } from '../preferences/preferenceSlice';
import { AppDispatch } from '../store';
import SharedAgentCard from './SharedAgentCard';
import { Agent } from './types';
export default function SharedAgent() {
@@ -92,18 +91,22 @@ export default function SharedAgent() {
);
const handleQuestionSubmission = (
question?: string,
updatedQuestion?: string,
updated?: boolean,
indx?: number,
) => {
if (updated === true && question !== undefined && indx !== undefined) {
if (
updated === true &&
updatedQuestion !== undefined &&
indx !== undefined
) {
handleQuestion({
question,
question: updatedQuestion,
index: indx,
isRetry: false,
});
} else if (question && status !== 'loading') {
const currentInput = question.trim();
} else if (input.trim() && status !== 'loading') {
const currentInput = input.trim();
if (lastQueryReturnedErr && queries.length > 0) {
const lastQueryIndex = queries.length - 1;
handleQuestion({
@@ -178,9 +181,11 @@ export default function SharedAgent() {
}
/>
</div>
<div className="flex w-[95%] max-w-[1500px] flex-col items-center pb-2 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
<div className="flex w-[95%] max-w-[1500px] flex-col items-center gap-4 pb-2 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
<MessageInput
onSubmit={(text) => handleQuestionSubmission(text)}
value={input}
onChange={(e) => setInput(e.target.value)}
onSubmit={() => handleQuestionSubmission()}
loading={status === 'loading'}
showSourceButton={sharedAgent ? false : true}
showToolButton={sharedAgent ? false : true}
@@ -194,3 +199,65 @@ export default function SharedAgent() {
</div>
);
}
function SharedAgentCard({ agent }: { agent: Agent }) {
return (
<div className="flex w-full max-w-[720px] flex-col rounded-3xl border border-dark-gray p-6 shadow-sm dark:border-grey sm:w-fit sm:min-w-[480px]">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center overflow-hidden rounded-full p-1">
<img src={Robot} className="h-full w-full object-contain" />
</div>
<div className="flex max-h-[92px] w-[80%] flex-col gap-px">
<h2 className="text-base font-semibold text-[#212121] dark:text-[#E0E0E0] sm:text-lg">
{agent.name}
</h2>
<p className="overflow-y-auto text-wrap break-all text-xs text-[#71717A] dark:text-[#949494] sm:text-sm">
{agent.description}
</p>
</div>
</div>
<div className="mt-4 flex items-center gap-8">
{agent.shared_metadata?.shared_by && (
<p className="text-xs font-light text-[#212121] dark:text-[#E0E0E0] sm:text-sm">
by {agent.shared_metadata.shared_by}
</p>
)}
{agent.shared_metadata?.shared_at && (
<p className="text-xs font-light text-[#71717A] dark:text-[#949494] sm:text-sm">
Shared on{' '}
{new Date(agent.shared_metadata.shared_at).toLocaleString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true,
})}
</p>
)}
</div>
{agent.tools.length > 0 && (
<div className="mt-8">
<p className="text-sm font-semibold text-[#212121] dark:text-[#E0E0E0] sm:text-base">
Connected Tools
</p>
<div className="mt-2 flex flex-wrap gap-2">
{agent.tools.map((tool, index) => (
<span
key={index}
className="flex items-center gap-1 rounded-full bg-bright-gray px-3 py-1 text-xs font-light text-[#212121] dark:bg-dark-charcoal dark:text-[#E0E0E0]"
>
<img
src={`/toolIcons/tool_${tool}.svg`}
alt={`${tool} icon`}
className="h-3 w-3"
/>{' '}
{tool}
</span>
))}
</div>
</div>
)}
</div>
);
}

View File

@@ -1,69 +0,0 @@
import Robot from '../assets/robot.svg';
import { Agent } from './types';
export default function SharedAgentCard({ agent }: { agent: Agent }) {
return (
<div className="flex w-full max-w-[720px] flex-col rounded-3xl border border-dark-gray p-6 shadow-sm dark:border-grey sm:w-fit sm:min-w-[480px]">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center overflow-hidden rounded-full p-1">
<img src={Robot} className="h-full w-full object-contain" />
</div>
<div className="flex max-h-[92px] w-[80%] flex-col gap-px">
<h2 className="text-base font-semibold text-[#212121] dark:text-[#E0E0E0] sm:text-lg">
{agent.name}
</h2>
<p className="overflow-y-auto text-wrap break-all text-xs text-[#71717A] dark:text-[#949494] sm:text-sm">
{agent.description}
</p>
</div>
</div>
{agent.shared_metadata && (
<div className="mt-4 flex items-center gap-8">
{agent.shared_metadata?.shared_by && (
<p className="text-xs font-light text-[#212121] dark:text-[#E0E0E0] sm:text-sm">
by {agent.shared_metadata.shared_by}
</p>
)}
{agent.shared_metadata?.shared_at && (
<p className="text-xs font-light text-[#71717A] dark:text-[#949494] sm:text-sm">
Shared on{' '}
{new Date(agent.shared_metadata.shared_at).toLocaleString(
'en-US',
{
month: 'long',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true,
},
)}
</p>
)}
</div>
)}
{agent.tool_details && agent.tool_details.length > 0 && (
<div className="mt-8">
<p className="text-sm font-semibold text-[#212121] dark:text-[#E0E0E0] sm:text-base">
Connected Tools
</p>
<div className="mt-2 flex flex-wrap gap-2">
{agent.tool_details.map((tool, index) => (
<span
key={index}
className="flex items-center gap-1 rounded-full bg-bright-gray px-3 py-1 text-xs font-light text-[#212121] dark:bg-dark-charcoal dark:text-[#E0E0E0]"
>
<img
src={`/toolIcons/tool_${tool.name}.svg`}
alt={`${tool.name} icon`}
className="h-3 w-3"
/>{' '}
{tool.name}
</span>
))}
</div>
</div>
)}
</div>
);
}

View File

@@ -1,7 +0,0 @@
import { Navigate, useParams } from 'react-router-dom';
export default function SharedAgentGate() {
const { agentId } = useParams();
return <Navigate to={`/agents/shared/${agentId}`} replace />;
}

View File

@@ -4,11 +4,11 @@ import { Route, Routes, useNavigate } from 'react-router-dom';
import userService from '../api/services/userService';
import Edit from '../assets/edit.svg';
import Link from '../assets/link-gray.svg';
import Monitoring from '../assets/monitoring.svg';
import Pin from '../assets/pin.svg';
import Trash from '../assets/red-trash.svg';
import Robot from '../assets/robot.svg';
import Link from '../assets/link-gray.svg';
import ThreeDots from '../assets/three-dots.svg';
import UnPin from '../assets/unpin.svg';
import ContextMenu, { MenuOption } from '../components/ContextMenu';
@@ -22,11 +22,9 @@ import { ActiveState } from '../models/misc';
import {
selectAgents,
selectSelectedAgent,
selectSharedAgents,
selectToken,
setAgents,
setSelectedAgent,
setSharedAgents,
} from '../preferences/preferenceSlice';
import AgentLogs from './AgentLogs';
import NewAgent from './NewAgent';
@@ -61,12 +59,13 @@ const sectionConfig = {
};
function AgentsList() {
const navigate = useNavigate();
const dispatch = useDispatch();
const token = useSelector(selectToken);
const agents = useSelector(selectAgents);
const sharedAgents = useSelector(selectSharedAgents);
const selectedAgent = useSelector(selectSelectedAgent);
const [sharedAgents, setSharedAgents] = useState<Agent[]>([]);
const [loadingUserAgents, setLoadingUserAgents] = useState<boolean>(true);
const [loadingSharedAgents, setLoadingSharedAgents] = useState<boolean>(true);
@@ -90,7 +89,7 @@ function AgentsList() {
const response = await userService.getSharedAgents(token);
if (!response.ok) throw new Error('Failed to fetch shared agents');
const data = await response.json();
dispatch(setSharedAgents(data));
setSharedAgents(data);
setLoadingSharedAgents(false);
} catch (error) {
console.error('Error:', error);
@@ -163,17 +162,11 @@ function AgentsList() {
</div> */}
<AgentSection
agents={agents ?? []}
updateAgents={(updatedAgents) => {
dispatch(setAgents(updatedAgents));
}}
loading={loadingUserAgents}
section="user"
/>
<AgentSection
agents={sharedAgents ?? []}
updateAgents={(updatedAgents) => {
dispatch(setSharedAgents(updatedAgents));
}}
loading={loadingSharedAgents}
section="shared"
/>
@@ -183,12 +176,10 @@ function AgentsList() {
function AgentSection({
agents,
updateAgents,
loading,
section,
}: {
agents: Agent[];
updateAgents?: (agents: Agent[]) => void;
loading: boolean;
section: keyof typeof sectionConfig;
}) {
@@ -213,23 +204,20 @@ function AgentSection({
</button>
)}
</div>
<div>
<div className="grid w-full grid-cols-2 gap-2 md:flex md:flex-wrap md:gap-4">
{loading ? (
<div className="flex h-72 w-full items-center justify-center">
<Spinner />
</div>
) : agents && agents.length > 0 ? (
<div className="grid grid-cols-1 gap-4 sm:flex sm:flex-wrap">
{agents.map((agent, idx) => (
<AgentCard
key={agent.id}
agent={agent}
agents={agents}
updateAgents={updateAgents}
section={section}
/>
))}
</div>
agents.map((agent) => (
<AgentCard
key={agent.id}
agent={agent}
agents={agents}
section={section}
/>
))
) : (
<div className="flex h-72 w-full flex-col items-center justify-center gap-3 text-base text-[#18181B] dark:text-[#E0E0E0]">
<p>{sectionConfig[section].emptyStateDescription}</p>
@@ -251,12 +239,10 @@ function AgentSection({
function AgentCard({
agent,
agents,
updateAgents,
section,
}: {
agent: Agent;
agents: Agent[];
updateAgents?: (agents: Agent[]) => void;
section: keyof typeof sectionConfig;
}) {
const navigate = useNavigate();
@@ -278,23 +264,7 @@ function AgentCard({
return { ...prevAgent, pinned: !prevAgent.pinned };
return prevAgent;
});
updateAgents?.(updatedAgents);
} catch (error) {
console.error('Error:', error);
}
};
const handleHideSharedAgent = async () => {
try {
const response = await userService.removeSharedAgent(
agent.id ?? '',
token,
);
if (!response.ok) throw new Error('Failed to hide shared agent');
const updatedAgents = agents.filter(
(prevAgent) => prevAgent.id !== agent.id,
);
updateAgents?.(updatedAgents);
dispatch(setAgents(updatedAgents));
} catch (error) {
console.error('Error:', error);
}
@@ -356,30 +326,8 @@ function AgentCard({
navigate(`/agents/shared/${agent.shared_token}`);
},
variant: 'primary',
iconWidth: 12,
iconHeight: 12,
},
{
icon: agent.pinned ? UnPin : Pin,
label: agent.pinned ? 'Unpin' : 'Pin agent',
onClick: (e: SyntheticEvent) => {
e.stopPropagation();
togglePin();
},
variant: 'primary',
iconWidth: 18,
iconHeight: 18,
},
{
icon: Trash,
label: 'Remove',
onClick: (e: SyntheticEvent) => {
e.stopPropagation();
handleHideSharedAgent();
},
variant: 'danger',
iconWidth: 13,
iconHeight: 13,
iconWidth: 14,
iconHeight: 14,
},
],
};

View File

@@ -1,9 +1,3 @@
export type ToolSummary = {
id: string;
name: string;
display_name: string;
};
export type Agent = {
id?: string;
name: string;
@@ -14,7 +8,6 @@ export type Agent = {
retriever: string;
prompt_id: string;
tools: string[];
tool_details?: ToolSummary[];
agent_type: string;
status: string;
key?: string;

View File

@@ -18,7 +18,6 @@ const endpoints = {
SHARED_AGENT: (id: string) => `/api/shared_agent?token=${id}`,
SHARED_AGENTS: '/api/shared_agents',
SHARE_AGENT: `/api/share_agent`,
REMOVE_SHARED_AGENT: (id: string) => `/api/remove_shared_agent?id=${id}`,
AGENT_WEBHOOK: (id: string) => `/api/agent_webhook?id=${id}`,
PROMPTS: '/api/get_prompts',
CREATE_PROMPT: '/api/create_prompt',

View File

@@ -41,8 +41,6 @@ const userService = {
apiClient.get(endpoints.USER.SHARED_AGENTS, token),
shareAgent: (data: any, token: string | null): Promise<any> =>
apiClient.put(endpoints.USER.SHARE_AGENT, data, token),
removeSharedAgent: (id: string, token: string | null): Promise<any> =>
apiClient.delete(endpoints.USER.REMOVE_SHARED_AGENT(id), token),
getAgentWebhook: (id: string, token: string | null): Promise<any> =>
apiClient.get(endpoints.USER.AGENT_WEBHOOK(id), token),
getPrompts: (token: string | null): Promise<any> =>

View File

@@ -1,3 +1,3 @@
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 1.5H3C2.46957 1.5 1.96086 1.71071 1.58579 2.08579C1.21071 2.46086 1 2.96957 1 3.5V15.5C1 16.0304 1.21071 16.5391 1.58579 16.9142C1.96086 17.2893 2.46957 17.5 3 17.5H15C15.5304 17.5 16.0391 17.2893 16.4142 16.9142C16.7893 16.5391 17 16.0304 17 15.5V11.5M9 9.5L17 1.5M17 1.5V6.5M17 1.5H12" stroke="#747474" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 1.5H3C2.46957 1.5 1.96086 1.71071 1.58579 2.08579C1.21071 2.46086 1 2.96957 1 3.5V15.5C1 16.0304 1.21071 16.5391 1.58579 16.9142C1.96086 17.2893 2.46957 17.5 3 17.5H15C15.5304 17.5 16.0391 17.2893 16.4142 16.9142C16.7893 16.5391 17 16.0304 17 15.5V11.5M9 9.5L17 1.5M17 1.5V6.5M17 1.5H12" stroke="#949494" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 486 B

After

Width:  |  Height:  |  Size: 486 B

View File

@@ -1,89 +0,0 @@
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import newChatIcon from '../assets/openNewChat.svg';
import ShareIcon from '../assets/share.svg';
import { ShareConversationModal } from '../modals/ShareConversationModal';
import { useState } from 'react';
import { selectConversationId } from '../preferences/preferenceSlice';
import { useDispatch } from 'react-redux';
import { AppDispatch } from '../store';
import {
setConversation,
updateConversationId,
} from '../conversation/conversationSlice';
interface ActionButtonsProps {
className?: string;
showNewChat?: boolean;
showShare?: boolean;
}
import { useNavigate } from 'react-router-dom';
export default function ActionButtons({
className = '',
showNewChat = true,
showShare = true,
}: ActionButtonsProps) {
const { t } = useTranslation();
const dispatch = useDispatch<AppDispatch>();
const conversationId = useSelector(selectConversationId);
const [isShareModalOpen, setShareModalState] = useState<boolean>(false);
const navigate = useNavigate();
const newChat = () => {
dispatch(setConversation([]));
dispatch(
updateConversationId({
query: { conversationId: null },
}),
);
navigate('/');
};
return (
<div className="fixed right-4 top-0 z-10 flex h-16 flex-col justify-center">
<div className={`flex items-center gap-2 sm:gap-4 ${className}`}>
{showNewChat && (
<button
title="Open New Chat"
onClick={newChat}
className="flex items-center gap-1 rounded-full p-2 hover:bg-bright-gray dark:hover:bg-[#28292E] lg:hidden"
>
<img
className="filter dark:invert"
alt="NewChat"
width={21}
height={21}
src={newChatIcon}
/>
</button>
)}
{showShare && conversationId && (
<>
<button
title="Share"
onClick={() => setShareModalState(true)}
className="rounded-full p-2 hover:bg-bright-gray dark:hover:bg-[#28292E]"
>
<img
className="filter dark:invert"
alt="share"
width={16}
height={16}
src={ShareIcon}
/>
</button>
{isShareModalOpen && (
<ShareConversationModal
close={() => setShareModalState(false)}
conversationId={conversationId}
/>
)}
</>
)}
<div>{/* <UserButton /> */}</div>
</div>
</div>
);
}

View File

@@ -30,17 +30,7 @@ export default function ContextMenu({
offset = { x: 0, y: 8 },
}: ContextMenuProps) {
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isOpen && menuRef.current) {
const positionStyle = getMenuPosition();
if (menuRef.current) {
Object.assign(menuRef.current.style, {
top: positionStyle.top,
left: positionStyle.left,
});
}
}
}, [isOpen]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
@@ -71,45 +61,20 @@ export default function ContextMenu({
let top = rect.bottom + scrollY + offset.y;
let left = rect.right + scrollX + offset.x;
// Get menu dimensions (need ref to be available)
const menuWidth = menuRef.current?.offsetWidth || 144; // Default min-width
const menuHeight = menuRef.current?.offsetHeight || 0;
// Get viewport dimensions
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Adjust position based on specified position
switch (position) {
case 'bottom-left':
left = rect.left + scrollX - offset.x;
break;
case 'top-right':
top = rect.top + scrollY - offset.y - menuHeight;
top = rect.top + scrollY - offset.y;
break;
case 'top-left':
top = rect.top + scrollY - offset.y - menuHeight;
top = rect.top + scrollY - offset.y;
left = rect.left + scrollX - offset.x;
break;
// bottom-right is default
}
if (left + menuWidth > viewportWidth) {
left = Math.max(5, viewportWidth - menuWidth - 5);
}
if (left < 5) {
left = 5;
}
if (top + menuHeight > viewportHeight + scrollY) {
top = rect.top + scrollY - menuHeight - offset.y;
}
if (top < scrollY + 5) {
top = rect.bottom + scrollY + offset.y;
}
return {
position: 'fixed',
top: `${top}px`,
@@ -125,7 +90,7 @@ export default function ContextMenu({
onClick={(e) => e.stopPropagation()}
>
<div
className="flex flex-col rounded-xl bg-lotion text-sm shadow-xl dark:bg-charleston-green-2"
className="flex w-32 flex-col rounded-xl bg-lotion text-sm shadow-xl dark:bg-charleston-green-2 md:w-36"
style={{ minWidth: '144px' }}
>
{options.map((option, index) => (
@@ -144,7 +109,7 @@ export default function ContextMenu({
} `}
>
{option.icon && (
<div className="flex w-4 min-w-4 flex-shrink-0 justify-center">
<div className="flex w-4 justify-center">
<img
width={option.iconWidth || 16}
height={option.iconHeight || 16}
@@ -154,7 +119,7 @@ export default function ContextMenu({
/>
</div>
)}
<span className="hyphens-auto break-words">{option.label}</span>
<span>{option.label}</span>
</button>
))}
</div>

View File

@@ -36,8 +36,6 @@ const Input = ({
const inputRef = useRef<HTMLInputElement>(null);
const hasValue = value !== undefined && value !== null && value !== '';
return (
<div className={`relative ${className}`}>
<input
@@ -60,11 +58,7 @@ const Input = ({
{placeholder && (
<label
htmlFor={id}
className={`absolute select-none ${
hasValue ? '-top-2.5 left-3 text-xs' : ''
} px-2 transition-all peer-placeholder-shown:left-3 peer-placeholder-shown:top-2.5 peer-placeholder-shown:${
textSizeStyles[textSize]
} pointer-events-none cursor-none text-gray-4000 peer-focus:-top-2.5 peer-focus:left-3 peer-focus:text-xs dark:text-gray-400 ${labelBgClassName} max-w-[calc(100%-24px)] overflow-hidden text-ellipsis whitespace-nowrap`}
className={`absolute -top-2.5 left-3 px-2 ${textSizeStyles[textSize]} transition-all peer-placeholder-shown:left-3 peer-placeholder-shown:top-2.5 peer-placeholder-shown:${textSizeStyles[textSize]} pointer-events-none cursor-none 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 ${labelBgClassName}`}
>
{placeholder}
{required && (

View File

@@ -30,7 +30,9 @@ import SourcesPopup from './SourcesPopup';
import ToolsPopup from './ToolsPopup';
type MessageInputProps = {
onSubmit: (text: string) => void;
value: string;
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
onSubmit: () => void;
loading: boolean;
showSourceButton?: boolean;
showToolButton?: boolean;
@@ -38,6 +40,8 @@ type MessageInputProps = {
};
export default function MessageInput({
value,
onChange,
onSubmit,
loading,
showSourceButton = true,
@@ -46,7 +50,6 @@ export default function MessageInput({
}: MessageInputProps) {
const { t } = useTranslation();
const [isDarkTheme] = useDarkTheme();
const [value, setValue] = useState('');
const inputRef = useRef<HTMLTextAreaElement>(null);
const sourceButtonRef = useRef<HTMLButtonElement>(null);
const toolButtonRef = useRef<HTMLButtonElement>(null);
@@ -229,11 +232,6 @@ export default function MessageInput({
handleInput();
}, []);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setValue(e.target.value);
handleInput();
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
@@ -250,10 +248,7 @@ export default function MessageInput({
};
const handleSubmit = () => {
if (value.trim() && !loading) {
onSubmit(value);
setValue('');
}
onSubmit();
};
return (
<div className="mx-2 flex w-full flex-col">
@@ -279,11 +274,11 @@ export default function MessageInput({
dispatch(removeAttachment(attachment.id));
}
}}
aria-label={t('conversation.attachments.remove')}
aria-label="Remove attachment"
>
<img
src={ExitIcon}
alt={t('conversation.attachments.remove')}
alt="Remove"
className="h-2.5 w-2.5 filter dark:invert"
/>
</button>
@@ -339,7 +334,7 @@ export default function MessageInput({
id="message-input"
ref={inputRef}
value={value}
onChange={handleChange}
onChange={onChange}
tabIndex={1}
placeholder={t('inputPlaceholder')}
className="inputbox-style no-scrollbar w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-t-[23px] bg-lotion px-4 py-3 text-base leading-tight opacity-100 focus:outline-none dark:bg-transparent dark:text-bright-gray dark:placeholder-bright-gray dark:placeholder-opacity-50 sm:px-6 sm:py-5"
@@ -403,7 +398,7 @@ export default function MessageInput({
className="mr-1 h-3.5 w-3.5 sm:mr-1.5 sm:h-4 sm:w-4"
/>
<span className="xs:text-[12px] text-[10px] font-medium text-[#5D5D5D] dark:text-bright-gray sm:text-[14px]">
{t('conversation.attachments.attach')}
Attach
</span>
<input
type="file"
@@ -411,6 +406,7 @@ export default function MessageInput({
onChange={handleFileAttachment}
/>
</label>
{/* Additional badges can be added here in the future */}
</div>

View File

@@ -81,6 +81,12 @@ export default function SourcesPopup({
return () => window.removeEventListener('resize', updatePosition);
}, [isOpen, anchorRef]);
const handleEmptyDocumentSelect = () => {
dispatch(setSelectedDocs(null));
handlePostDocumentSelect(null);
onClose();
};
const handleClickOutside = (event: MouseEvent) => {
if (
popupRef.current &&
@@ -147,24 +153,14 @@ export default function SourcesPopup({
<>
{filteredOptions?.map((option: any, index: number) => {
if (option.model === embeddingsName) {
const isSelected =
selectedDocs &&
(option.id
? selectedDocs.id === option.id
: selectedDocs.date === option.date);
return (
<div
key={index}
className="flex cursor-pointer items-center border-b border-[#D9D9D9] border-opacity-80 p-3 transition-colors hover:bg-gray-100 dark:border-dim-gray dark:text-[14px] dark:hover:bg-[#2C2E3C]"
onClick={() => {
if (isSelected) {
dispatch(setSelectedDocs(null));
handlePostDocumentSelect(null);
} else {
dispatch(setSelectedDocs(option));
handlePostDocumentSelect(option);
}
dispatch(setSelectedDocs(option));
handlePostDocumentSelect(option);
onClose();
}}
>
<img
@@ -180,19 +176,44 @@ export default function SourcesPopup({
<div
className={`flex h-4 w-4 flex-shrink-0 items-center justify-center border border-[#C6C6C6] p-[0.5px] dark:border-[#757783]`}
>
{isSelected && (
<img
src={CheckIcon}
alt="Selected"
className="h-3 w-3"
/>
)}
{selectedDocs &&
(option.id
? selectedDocs.id === option.id // For documents with MongoDB IDs
: selectedDocs.date === option.date) && ( // For preloaded sources
<img
src={CheckIcon}
alt="Selected"
className="h-3 w-3"
/>
)}
</div>
</div>
);
}
return null;
})}
<div
className="flex cursor-pointer items-center border-b border-[#D9D9D9] border-opacity-80 p-3 transition-colors hover:bg-gray-100 dark:border-dim-gray dark:text-[14px] dark:hover:bg-[#2C2E3C]"
onClick={handleEmptyDocumentSelect}
>
<img
width={14}
height={14}
src={SourceIcon}
alt="Source"
className="mr-3 flex-shrink-0"
/>
<span className="mr-3 flex-grow font-medium text-[#5D5D5D] dark:text-bright-gray">
{t('none')}
</span>
<div
className={`flex h-4 w-4 flex-shrink-0 items-center justify-center border border-[#C6C6C6] p-[0.5px] dark:border-[#757783]`}
>
{selectedDocs === null && (
<img src={CheckIcon} alt="Selected" className="h-3 w-3" />
)}
</div>
</div>
</>
) : (
<div className="p-4 text-center text-gray-500 dark:text-[14px] dark:text-bright-gray">
@@ -207,7 +228,7 @@ export default function SourcesPopup({
className="inline-flex items-center gap-2 text-base font-medium text-violets-are-blue"
onClick={onClose}
>
{t('settings.documents.goToDocuments')}
Go to Documents
<img src={RedirectIcon} alt="Redirect" className="h-3 w-3" />
</a>
</div>
@@ -217,7 +238,7 @@ export default function SourcesPopup({
onClick={handleUploadClick}
className="w-auto rounded-full border border-violets-are-blue px-4 py-2 text-[14px] font-medium text-violets-are-blue transition-colors duration-200 hover:bg-violets-are-blue hover:text-white"
>
{t('settings.documents.uploadNew')}
Upload new
</button>
</div>
</div>

View File

@@ -201,7 +201,7 @@ export default function ToolsPopup({
/>
<div className="overflow-hidden">
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-xs font-medium text-gray-900 dark:text-white">
{tool.customName || tool.displayName}
{tool.displayName}
</p>
</div>
</div>

View File

@@ -3,10 +3,12 @@ import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import SharedAgentCard from '../agents/SharedAgentCard';
import DragFileUpload from '../assets/DragFileUpload.svg';
import newChatIcon from '../assets/openNewChat.svg';
import ShareIcon from '../assets/share.svg';
import MessageInput from '../components/MessageInput';
import { useMediaQuery } from '../hooks';
import { ShareConversationModal } from '../modals/ShareConversationModal';
import { ActiveState } from '../models/misc';
import {
selectConversationId,
@@ -40,6 +42,7 @@ export default function Conversation() {
const conversationId = useSelector(selectConversationId);
const selectedAgent = useSelector(selectSelectedAgent);
const [input, setInput] = useState<string>('');
const [uploadModalState, setUploadModalState] =
useState<ActiveState>('INACTIVE');
const [files, setFiles] = useState<File[]>([]);
@@ -143,19 +146,19 @@ export default function Conversation() {
};
const handleQuestionSubmission = (
question?: string,
updatedQuestion?: string,
updated?: boolean,
indx?: number,
) => {
if (updated === true) {
handleQuestion({ question: question as string, index: indx });
} else if (question && status !== 'loading') {
handleQuestion({ question: updatedQuestion as string, index: indx });
} else if (input && status !== 'loading') {
if (lastQueryReturnedErr) {
dispatch(
updateQuery({
index: queries.length - 1,
query: {
prompt: question,
prompt: input,
},
}),
);
@@ -165,9 +168,10 @@ export default function Conversation() {
});
} else {
handleQuestion({
question,
question: input,
});
}
setInput('');
}
};
@@ -180,6 +184,10 @@ export default function Conversation() {
);
};
const newChat = () => {
if (queries && queries.length > 0) resetConversation();
};
useEffect(() => {
if (queries.length) {
queries[queries.length - 1].error && setLastQueryReturnedErr(true);
@@ -188,20 +196,56 @@ export default function Conversation() {
}, [queries[queries.length - 1]]);
return (
<div className="flex h-full flex-col justify-end gap-1">
{conversationId && queries.length > 0 && (
<div className="absolute right-20 top-4">
<div className="mt-2 flex items-center gap-4">
{isMobile && queries.length > 0 && (
<button
title="Open New Chat"
onClick={() => {
newChat();
}}
className="rounded-full p-2 hover:bg-bright-gray dark:hover:bg-[#28292E]"
>
<img
className="h-5 w-5 filter dark:invert"
alt="NewChat"
src={newChatIcon}
/>
</button>
)}
<button
title="Share"
onClick={() => {
setShareModalState(true);
}}
className="rounded-full p-2 hover:bg-bright-gray dark:hover:bg-[#28292E]"
>
<img
className="h-5 w-5 filter dark:invert"
alt="share"
src={ShareIcon}
/>
</button>
</div>
{isShareModalOpen && (
<ShareConversationModal
close={() => {
setShareModalState(false);
}}
conversationId={conversationId}
/>
)}
</div>
)}
<ConversationMessages
handleQuestion={handleQuestion}
handleQuestionSubmission={handleQuestionSubmission}
handleFeedback={handleFeedback}
queries={queries}
status={status}
showHeroOnEmpty={selectedAgent ? false : true}
headerContent={
selectedAgent ? (
<div className="flex w-full items-center justify-center py-4">
<SharedAgentCard agent={selectedAgent} />
</div>
) : undefined
}
/>
<div className="z-3 flex h-auto w-full max-w-[1300px] flex-col items-end self-center rounded-2xl bg-opacity-0 py-1 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
@@ -214,9 +258,9 @@ export default function Conversation() {
</label>
<input {...getInputProps()} id="file-upload" />
<MessageInput
onSubmit={(text) => {
handleQuestionSubmission(text);
}}
value={input}
onChange={(e) => setInput(e.target.value)}
onSubmit={handleQuestionSubmission}
loading={status === 'loading'}
showSourceButton={selectedAgent ? false : true}
showToolButton={selectedAgent ? false : true}

View File

@@ -49,7 +49,7 @@ const ConversationBubble = forwardRef<
feedback?: FEEDBACK;
handleFeedback?: (feedback: FEEDBACK) => void;
thought?: string;
sources?: { title: string; text: string; link: string }[];
sources?: { title: string; text: string; source: string }[];
toolCalls?: ToolCallsType[];
retryBtn?: React.ReactElement;
questionNumber?: number;
@@ -233,7 +233,7 @@ const ConversationBubble = forwardRef<
{DisableSourceFE ||
type === 'ERROR' ||
sources?.length === 0 ||
sources?.some((source) => source.link === 'None') ? null : !sources &&
sources?.some((source) => source.source === 'None') ? null : !sources &&
chunks !== '0' &&
selectedDocs ? (
<div className="mb-4 flex flex-col flex-wrap items-start self-start lg:flex-nowrap">
@@ -300,14 +300,14 @@ const ConversationBubble = forwardRef<
</p>
<div
className={`mt-[14px] flex flex-row items-center gap-[6px] underline-offset-2 ${
source.link && source.link !== 'local'
source.source && source.source !== 'local'
? 'hover:text-[#007DFF] hover:underline dark:hover:text-[#48A0FF]'
: ''
}`}
onClick={() =>
source.link && source.link !== 'local'
source.source && source.source !== 'local'
? window.open(
source.link,
source.source,
'_blank',
'noopener, noreferrer',
)
@@ -322,13 +322,13 @@ const ConversationBubble = forwardRef<
<p
className="mt-[2px] truncate text-xs"
title={
source.link && source.link !== 'local'
? source.link
source.source && source.source !== 'local'
? source.source
: source.title
}
>
{source.link && source.link !== 'local'
? source.link
{source.source && source.source !== 'local'
? source.source
: source.title}
</p>
</div>
@@ -339,7 +339,7 @@ const ConversationBubble = forwardRef<
onMouseOver={() => setActiveTooltip(index)}
onMouseOut={() => setActiveTooltip(null)}
>
<p className="line-clamp-6 max-h-[164px] overflow-hidden text-ellipsis break-words rounded-md text-sm">
<p className="max-h-[164px] overflow-y-auto break-words rounded-md text-sm">
{source.text}
</p>
</div>
@@ -649,68 +649,50 @@ const ConversationBubble = forwardRef<
});
type AllSourcesProps = {
sources: { title: string; text: string; link?: string }[];
sources: { title: string; text: string; source: string }[];
};
function AllSources(sources: AllSourcesProps) {
const { t } = useTranslation();
const handleCardClick = (link: string) => {
if (link && link !== 'local') {
window.open(link, '_blank', 'noopener,noreferrer');
}
};
return (
<div className="h-full w-full">
<div className="w-full">
<p className="text-left text-xl">{`${sources.sources.length} ${t('conversation.sources.title')}`}</p>
<p className="text-left text-xl">{`${sources.sources.length} Sources`}</p>
<div className="mx-1 mt-2 h-[0.8px] w-full rounded-full bg-[#C4C4C4]/40 lg:w-[95%]"></div>
</div>
<div className="mt-6 flex h-[90%] w-60 flex-col items-center gap-4 overflow-y-auto sm:w-80">
{sources.sources.map((source, index) => {
const isExternalSource = source.link && source.link !== 'local';
return (
<div
key={index}
className={`group/card relative w-full rounded-[20px] bg-gray-1000 p-4 transition-colors hover:bg-[#F1F1F1] dark:bg-[#28292E] dark:hover:bg-[#2C2E3C] ${
isExternalSource ? 'cursor-pointer' : ''
}`}
onClick={() =>
isExternalSource && source.link && handleCardClick(source.link)
}
>
{sources.sources.map((source, index) => (
<div
key={index}
className="min-h-32 w-full rounded-[20px] bg-gray-1000 p-4 dark:bg-[#28292E]"
>
<span className="flex flex-row">
<p
title={source.title}
className={`ellipsis-text break-words text-left text-sm font-semibold ${
isExternalSource
? 'group-hover/card:text-purple-30 dark:group-hover/card:text-[#8C67D7]'
: ''
}`}
className="ellipsis-text break-words text-left text-sm font-semibold"
>
{`${index + 1}. ${source.title}`}
{isExternalSource && (
<img
src={Link}
alt="External Link"
className={`ml-1 inline h-3 w-3 object-fill dark:invert ${
isExternalSource
? 'group-hover/card:contrast-[50%] group-hover/card:hue-rotate-[235deg] group-hover/card:invert-[31%] group-hover/card:saturate-[752%] group-hover/card:sepia-[80%] group-hover/card:filter'
: ''
}`}
/>
)}
</p>
<p className="mt-3 line-clamp-4 break-words rounded-md text-left text-xs text-black dark:text-chinese-silver">
{source.text}
</p>
</div>
);
})}
{source.source && source.source !== 'local' ? (
<img
src={Link}
alt={'Link'}
className="h-3 w-3 cursor-pointer object-fill"
onClick={() =>
window.open(source.source, '_blank', 'noopener, noreferrer')
}
></img>
) : null}
</span>
<p className="mt-3 max-h-16 overflow-y-auto break-words rounded-md text-left text-xs text-black dark:text-chinese-silver">
{source.text}
</p>
</div>
))}
</div>
</div>
);
}
export default ConversationBubble;
function ToolCalls({ toolCalls }: { toolCalls: ToolCallsType[] }) {

View File

@@ -190,7 +190,7 @@ export default function ConversationMessages({
ref={conversationRef}
onWheel={handleUserScrollInterruption}
onTouchMove={handleUserScrollInterruption}
className="flex h-full w-full justify-center overflow-y-auto will-change-scroll sm:pt-6 lg:pt-12"
className="flex h-full w-full justify-center overflow-y-auto sm:pt-12"
>
{queries.length > 0 && !hasScrolledToLast && (
<button

View File

@@ -35,6 +35,7 @@ export const SharedConversation = () => {
const apiKey = useSelector(selectClientAPIKey);
const status = useSelector(selectStatus);
const [input, setInput] = useState('');
const { t } = useTranslation();
const dispatch = useDispatch<AppDispatch>();
@@ -75,15 +76,15 @@ export const SharedConversation = () => {
});
};
const handleQuestionSubmission = (question?: string) => {
if (question && status !== 'loading') {
const handleQuestionSubmission = () => {
if (input && status !== 'loading') {
if (lastQueryReturnedErr) {
// update last failed query with new prompt
dispatch(
updateQuery({
index: queries.length - 1,
query: {
prompt: question,
prompt: input,
},
}),
);
@@ -92,8 +93,9 @@ export const SharedConversation = () => {
isRetry: true,
});
} else {
handleQuestion({ question });
handleQuestion({ question: input });
}
setInput('');
}
};
@@ -154,12 +156,10 @@ export const SharedConversation = () => {
<div className="flex w-full max-w-[1200px] flex-col items-center gap-4 pb-2 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
{apiKey ? (
<MessageInput
onSubmit={(text) => {
handleQuestionSubmission(text);
}}
value={input}
onChange={(e) => setInput(e.target.value)}
onSubmit={() => handleQuestionSubmission()}
loading={status === 'loading'}
showSourceButton={false}
showToolButton={false}
/>
) : (
<button

View File

@@ -150,7 +150,10 @@ export function handleFetchAnswerSteaming(
done,
value,
}: ReadableStreamReadResult<Uint8Array>) => {
if (done) return;
if (done) {
console.log(counterrr);
return;
}
counterrr += 1;
@@ -160,7 +163,7 @@ export function handleFetchAnswerSteaming(
const events = buffer.split('\n\n');
buffer = events.pop() ?? '';
for (const event of events) {
for (let event of events) {
if (event.trim().startsWith('data:')) {
const dataLine: string = event
.split('\n')

View File

@@ -43,7 +43,7 @@ export interface Query {
conversationId?: string | null;
title?: string | null;
thought?: string;
sources?: { title: string; text: string; link: string }[];
sources?: { title: string; text: string; source: string }[];
tool_calls?: ToolCallsType[];
error?: string;
attachments?: { fileName: string; id: string }[];

View File

@@ -72,7 +72,7 @@ export const fetchSharedAnswer = createAsyncThunk<Answer, { question: string }>(
dispatch(sharedConversationSlice.actions.setStatus('failed'));
dispatch(
sharedConversationSlice.actions.raiseError({
index: state.sharedConversation.queries.length - 1,
index: state.conversation.queries.length - 1,
message: data.error,
}),
);

View File

@@ -35,21 +35,21 @@ export function useOutsideAlerter<T extends HTMLElement>(
export function useMediaQuery() {
const mobileQuery = '(max-width: 768px)';
const tabletQuery = '(max-width: 1023px)';
const desktopQuery = '(min-width: 1024px)';
const darkModeQuery = '(prefers-color-scheme: dark)'; // Detect dark mode
const desktopQuery = '(min-width: 960px)';
const [isMobile, setIsMobile] = useState(false);
const [isTablet, setIsTablet] = useState(false);
const [isDesktop, setIsDesktop] = useState(false);
const [isDarkMode, setIsDarkMode] = useState(false);
useEffect(() => {
const mobileMedia = window.matchMedia(mobileQuery);
const tabletMedia = window.matchMedia(tabletQuery);
const desktopMedia = window.matchMedia(desktopQuery);
const darkModeMedia = window.matchMedia(darkModeQuery);
const updateMediaQueries = () => {
setIsMobile(mobileMedia.matches);
setIsTablet(tabletMedia.matches && !mobileMedia.matches); // Tablet but not mobile
setIsDesktop(desktopMedia.matches);
setIsDarkMode(darkModeMedia.matches);
};
updateMediaQueries();
@@ -60,9 +60,9 @@ export function useMediaQuery() {
return () => {
window.removeEventListener('resize', listener);
};
}, [mobileQuery, tabletQuery, desktopQuery]);
}, [mobileQuery, desktopQuery, darkModeQuery]);
return { isMobile, isTablet, isDesktop };
return { isMobile, isDesktop, isDarkMode };
}
export function useDarkTheme() {

View File

@@ -46,31 +46,6 @@ body.dark {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
/* Thin scrollbar utility */
.scrollbar-thin::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background: rgba(156, 163, 175, 0.5);
border-radius: 3px;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: rgba(156, 163, 175, 0.7);
}
/* For Firefox */
.scrollbar-thin {
scrollbar-width: thin;
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
}
}
@layer components {

View File

@@ -11,7 +11,6 @@
"help": "Help",
"emailUs": "Email us",
"documentation": "Documentation",
"manageAgents": "Manage Agents",
"demo": [
{
"header": "Learn about DocsGPT",
@@ -73,13 +72,7 @@
},
"actions": "Actions",
"view": "View",
"deleteWarning": "Are you sure you want to delete \"{{name}}\"?",
"backToAll": "Back to all documents",
"chunks": "Chunks",
"noChunks": "No chunks found",
"noChunksAlt": "No chunks found",
"goToDocuments": "Go to Documents",
"uploadNew": "Upload new"
"deleteWarning": "Are you sure you want to delete \"{{name}}\"?"
},
"apiKeys": {
"label": "Chatbots",
@@ -124,44 +117,9 @@
"noToolsFound": "No tools found",
"selectToolSetup": "Select a tool to set up",
"settingsIconAlt": "Settings icon",
"configureToolAria": "Configure {{toolName}}",
"toggleToolAria": "Toggle {{toolName}}",
"manageTools": "Go to Tools",
"edit": "Edit",
"delete": "Delete",
"deleteWarning": "Are you sure you want to delete the tool \"{{toolName}}\" ?",
"unsavedChanges": "You have unsaved changes that will be lost if you leave without saving.",
"leaveWithoutSaving": "Leave without Saving",
"saveAndLeave": "Save and Leave",
"customName": "Custom Name",
"customNamePlaceholder": "Enter a custom name (optional)",
"authentication": "Authentication",
"actions": "Actions",
"addAction": "Add action",
"noActionsFound": "No actions found",
"url": "URL",
"urlPlaceholder": "Enter URL",
"method": "Method",
"description": "Description",
"descriptionPlaceholder": "Enter description",
"headers": "Headers",
"queryParameters": "Query Parameters",
"body": "Body",
"deleteActionWarning": "Are you sure you want to delete the action \"{{name}}\"?",
"backToAllTools": "Back to all tools",
"save": "Save",
"fieldName": "Field Name",
"fieldType": "Field Type",
"filledByLLM": "Filled by LLM",
"fieldDescription": "Field description",
"value": "Value",
"addProperty": "Add property",
"propertyName": "New property key",
"add": "Add",
"cancel": "Cancel",
"addNew": "Add New",
"name": "Name",
"type": "Type"
"configureToolAria": "Configure {toolName}",
"toggleToolAria": "Toggle {toolName}",
"manageTools": "Go to Tools"
}
},
"modals": {
@@ -246,18 +204,6 @@
"promptText": "Prompt Text",
"save": "Save",
"nameExists": "Name already exists"
},
"chunk": {
"add": "Add Chunk",
"edit": "Edit Chunk",
"title": "Title",
"enterTitle": "Enter title",
"bodyText": "Body text",
"promptText": "Prompt Text",
"update": "Update",
"close": "Close",
"delete": "Delete",
"deleteConfirmation": "Are you sure you want to delete this chunk?"
}
},
"sharedConv": {
@@ -295,11 +241,6 @@
"link": "Source link",
"view_more": "{{count}} more sources"
},
"attachments": {
"attach": "Attach",
"remove": "Remove attachment",
"uploadFailed": "Upload failed"
},
"retry": "Retry"
}
}

View File

@@ -11,7 +11,6 @@
"help": "Asistencia",
"emailUs": "Envíanos un correo",
"documentation": "Documentación",
"manageAgents": "Administrar Agentes",
"demo": [
{
"header": "Aprende sobre DocsGPT",
@@ -49,8 +48,7 @@
"medium": "Medio",
"high": "Alto",
"unlimited": "Ilimitado",
"default": "Predeterminado",
"addNew": "Añadir Nuevo"
"default": "Predeterminado"
},
"documents": {
"title": "Esta tabla contiene todos los documentos que están disponibles para ti y los que has subido",
@@ -73,13 +71,7 @@
},
"actions": "Acciones",
"view": "Ver",
"deleteWarning": "¿Estás seguro de que deseas eliminar \"{{name}}\"?",
"backToAll": "Volver a todos los documentos",
"chunks": "Fragmentos",
"noChunks": "No se encontraron fragmentos",
"noChunksAlt": "No se encontraron fragmentos",
"goToDocuments": "Ir a Documentos",
"uploadNew": "Subir nuevo"
"deleteWarning": "¿Estás seguro de que deseas eliminar \"{{name}}\"?"
},
"apiKeys": {
"label": "Chatbots",
@@ -124,44 +116,8 @@
"noToolsFound": "No se encontraron herramientas",
"selectToolSetup": "Seleccione una herramienta para configurar",
"settingsIconAlt": "Icono de configuración",
"configureToolAria": "Configurar {{toolName}}",
"toggleToolAria": "Alternar {{toolName}}",
"manageTools": "Ir a Herramientas",
"edit": "Editar",
"delete": "Eliminar",
"deleteWarning": "¿Estás seguro de que deseas eliminar la herramienta \"{{toolName}}\"?",
"unsavedChanges": "Tienes cambios sin guardar que se perderán si sales sin guardar.",
"leaveWithoutSaving": "Salir sin Guardar",
"saveAndLeave": "Guardar y Salir",
"customName": "Nombre Personalizado",
"customNamePlaceholder": "Ingresa un nombre personalizado (opcional)",
"authentication": "Autenticación",
"actions": "Acciones",
"addAction": "Agregar acción",
"noActionsFound": "No se encontraron acciones",
"url": "URL",
"urlPlaceholder": "Ingresa url",
"method": "Método",
"description": "Descripción",
"descriptionPlaceholder": "Ingresa descripción",
"headers": "Encabezados",
"queryParameters": "Parámetros de Consulta",
"body": "Cuerpo",
"deleteActionWarning": "¿Estás seguro de que deseas eliminar la acción \"{{name}}\"?",
"save": "Guardar",
"name": "Nombre",
"type": "Tipo",
"filledByLLM": "Completado por LLM",
"value": "Valor",
"addProperty": "Agregar propiedad",
"propertyName": "Nueva clave de propiedad",
"backToAllTools": "Volver a todas las herramientas",
"fieldName": "Nombre del campo",
"fieldType": "Tipo de campo",
"fieldDescription": "Descripción del campo",
"add": "Añadir",
"cancel": "Cancelar",
"addNew": "Añadir Nuevo"
"configureToolAria": "Configurar {toolName}",
"toggleToolAria": "Alternar {toolName}"
}
},
"modals": {
@@ -246,18 +202,6 @@
"promptText": "Texto del Prompt",
"save": "Guardar",
"nameExists": "El nombre ya existe"
},
"chunk": {
"add": "Agregar Fragmento",
"edit": "Editar Fragmento",
"title": "Título",
"enterTitle": "Ingresar título",
"bodyText": "Texto del cuerpo",
"promptText": "Texto del prompt",
"update": "Actualizar",
"close": "Cerrar",
"delete": "Eliminar",
"deleteConfirmation": "¿Estás seguro de que deseas eliminar este fragmento?"
}
},
"sharedConv": {
@@ -291,14 +235,9 @@
},
"sources": {
"title": "Fuentes",
"text": "Texto fuente",
"link": "Enlace fuente",
"view_more": "Ver {{count}} más fuentes",
"text": "Elegir tus fuentes"
},
"attachments": {
"attach": "Adjuntar",
"remove": "Eliminar adjunto",
"uploadFailed": "Error al subir"
"view_more": "Ver {{count}} más fuentes"
},
"retry": "Reintentar"
}

View File

@@ -11,7 +11,6 @@
"help": "ヘルプ",
"emailUs": "メールを送る",
"documentation": "ドキュメント",
"manageAgents": "エージェント管理",
"demo": [
{
"header": "DocsGPTについて学ぶ",
@@ -53,7 +52,6 @@
"add": "追加"
},
"documents": {
"title": "この表には、利用可能なすべてのドキュメントとアップロードしたドキュメントが含まれています",
"label": "ドキュメント",
"name": "ドキュメント名",
"date": "ベクトル日付",
@@ -73,13 +71,7 @@
},
"actions": "アクション",
"view": "表示",
"deleteWarning": "\"{{name}}\"を削除してもよろしいですか?",
"backToAll": "すべてのドキュメントに戻る",
"chunks": "チャンク",
"noChunks": "チャンクが見つかりません",
"noChunksAlt": "チャンクが見つかりません",
"goToDocuments": "ドキュメントへ移動",
"uploadNew": "新規アップロード"
"deleteWarning": "\"{{name}}\"を削除してもよろしいですか?"
},
"apiKeys": {
"label": "チャットボット",
@@ -104,6 +96,7 @@
},
"messages": "メッセージ",
"tokenUsage": "トークン使用量",
"feedback": "フィードバック",
"filterPlaceholder": "フィルター",
"none": "なし",
"positiveFeedback": "肯定的なフィードバック",
@@ -124,44 +117,8 @@
"noToolsFound": "ツールが見つかりません",
"selectToolSetup": "設定するツールを選択してください",
"settingsIconAlt": "設定アイコン",
"configureToolAria": "{{toolName}}を設定",
"toggleToolAria": "{{toolName}}を切り替え",
"manageTools": "ツールへ移動",
"edit": "編集",
"delete": "削除",
"deleteWarning": "ツール \"{{toolName}}\" を削除してもよろしいですか?",
"unsavedChanges": "保存されていない変更があります。保存せずに離れると失われます。",
"leaveWithoutSaving": "保存せずに離れる",
"saveAndLeave": "保存して離れる",
"customName": "カスタム名",
"customNamePlaceholder": "カスタム名を入力(任意)",
"authentication": "認証",
"actions": "アクション",
"addAction": "アクションを追加",
"noActionsFound": "アクションが見つかりません",
"url": "URL",
"urlPlaceholder": "URLを入力",
"method": "メソッド",
"description": "説明",
"descriptionPlaceholder": "説明を入力",
"headers": "ヘッダー",
"queryParameters": "クエリパラメータ",
"body": "ボディ",
"deleteActionWarning": "アクション \"{{name}}\" を削除してもよろしいですか?",
"backToAllTools": "すべてのツールに戻る",
"save": "保存",
"fieldName": "フィールド名",
"fieldType": "フィールドタイプ",
"filledByLLM": "LLMによる入力",
"fieldDescription": "フィールドの説明",
"value": "値",
"addProperty": "プロパティを追加",
"propertyName": "新しいプロパティキー",
"add": "追加",
"cancel": "キャンセル",
"addNew": "新規追加",
"name": "名前",
"type": "タイプ"
"configureToolAria": "{toolName} を設定",
"toggleToolAria": "{toolName} を切り替え"
}
},
"modals": {
@@ -246,18 +203,6 @@
"promptText": "プロンプトテキスト",
"save": "保存",
"nameExists": "名前が既に存在します"
},
"chunk": {
"add": "チャンクを追加",
"edit": "チャンクを編集",
"title": "タイトル",
"enterTitle": "タイトルを入力",
"bodyText": "本文",
"promptText": "プロンプトテキスト",
"update": "更新",
"close": "閉じる",
"delete": "削除",
"deleteConfirmation": "このチャンクを削除してもよろしいですか?"
}
},
"sharedConv": {
@@ -293,12 +238,7 @@
"title": "ソース",
"text": "ソーステキスト",
"link": "ソースリンク",
"view_more": "さらに{{count}}個のソース"
},
"attachments": {
"attach": "添付",
"remove": "添付ファイルを削除",
"uploadFailed": "アップロード失敗"
"view_more": "さらに{{count}}個のソースを表示"
},
"retry": "再試行"
}

View File

@@ -11,7 +11,6 @@
"help": "Помощь",
"emailUs": "Напишите нам",
"documentation": "Документация",
"manageAgents": "Управление агентами",
"demo": [
{
"header": "Узнайте о DocsGPT",
@@ -73,13 +72,7 @@
},
"actions": "Действия",
"view": "Просмотр",
"deleteWarning": "Вы уверены, что хотите удалить \"{{name}}\"?",
"backToAll": "Вернуться ко всем документам",
"chunks": "Фрагменты",
"noChunks": "Фрагменты не найдены",
"noChunksAlt": "Фрагменты не найдены",
"goToDocuments": "Перейти к документам",
"uploadNew": "Загрузить новый"
"deleteWarning": "Вы уверены, что хотите удалить \"{{name}}\"?"
},
"apiKeys": {
"label": "API ключи",
@@ -123,45 +116,9 @@
"addTool": "Добавить инструмент",
"noToolsFound": "Инструменты не найдены",
"selectToolSetup": "Выберите инструмент для настройки",
"settingsIconAlt": "Значок настроек",
"configureToolAria": "Настроить {{toolName}}",
"toggleToolAria": "Переключить {{toolName}}",
"manageTools": "Перейти к инструментам",
"edit": "Редактировать",
"delete": "Удалить",
"deleteWarning": "Вы уверены, что хотите удалить инструмент \"{{toolName}}\"?",
"unsavedChanges": "У вас есть несохраненные изменения, которые будут потеряны, если вы уйдете без сохранения.",
"leaveWithoutSaving": "Уйти без сохранения",
"saveAndLeave": "Сохранить и уйти",
"customName": "Пользовательское имя",
"customNamePlaceholder": "Введите пользовательское имя (необязательно)",
"authentication": "Аутентификация",
"actions": "Действия",
"addAction": "Добавить действие",
"noActionsFound": "Действия не найдены",
"url": "URL",
"urlPlaceholder": "Введите URL",
"method": "Метод",
"description": "Описание",
"descriptionPlaceholder": "Введите описание",
"headers": "Заголовки",
"queryParameters": "Параметры запроса",
"body": "Тело запроса",
"deleteActionWarning": "Вы уверены, что хотите удалить действие \"{{name}}\"?",
"backToAllTools": "Вернуться ко всем инструментам",
"save": "Сохранить",
"fieldName": "Имя поля",
"fieldType": "Тип поля",
"filledByLLM": "Заполняется LLM",
"fieldDescription": "Описание поля",
"value": "Значение",
"addProperty": "Добавить свойство",
"propertyName": "Новый ключ свойства",
"add": "Добавить",
"cancel": "Отмена",
"addNew": "Добавить новое",
"name": "Имя",
"type": "Тип"
"settingsIconAlt": "Иконка настроек",
"configureToolAria": "Настроить {toolName}",
"toggleToolAria": "Переключить {toolName}"
}
},
"modals": {
@@ -246,18 +203,6 @@
"promptText": "Текст подсказки",
"save": "Сохранить",
"nameExists": "Название уже существует"
},
"chunk": {
"add": "Добавить фрагмент",
"edit": "Редактировать фрагмент",
"title": "Заголовок",
"enterTitle": "Введите заголовок",
"bodyText": "Текст",
"promptText": "Текст подсказки",
"update": "Обновить",
"close": "Закрыть",
"delete": "Удалить",
"deleteConfirmation": "Вы уверены, что хотите удалить этот фрагмент?"
}
},
"sharedConv": {
@@ -291,14 +236,9 @@
},
"sources": {
"title": "Источники",
"text": "Выберите ваши источники",
"text": "Текст источника",
"link": "Ссылка на источник",
"view_more": "ещё {{count}} источников"
},
"attachments": {
"attach": "Прикрепить",
"remove": "Удалить вложение",
"uploadFailed": "Ошибка загрузки"
"view_more": "Показать еще {{count}} источников"
},
"retry": "Повторить"
}

View File

@@ -11,7 +11,6 @@
"help": "幫助",
"emailUs": "給我們發電郵",
"documentation": "文件",
"manageAgents": "管理代理",
"demo": [
{
"header": "了解 DocsGPT",
@@ -73,13 +72,7 @@
},
"actions": "操作",
"view": "查看",
"deleteWarning": "您確定要刪除 \"{{name}}\" 嗎?",
"backToAll": "返回所有文件",
"chunks": "文本塊",
"noChunks": "未找到文本塊",
"noChunksAlt": "未找到文本塊",
"goToDocuments": "前往文件",
"uploadNew": "上傳新文件"
"deleteWarning": "您確定要刪除 \"{{name}}\" 嗎?"
},
"apiKeys": {
"label": "聊天機器人",
@@ -119,49 +112,13 @@
},
"tools": {
"label": "工具",
"searchPlaceholder": "搜尋工具...",
"searchPlaceholder": "搜尋...",
"addTool": "新增工具",
"noToolsFound": "找不到工具",
"selectToolSetup": "選擇要設定的工具",
"settingsIconAlt": "設定圖",
"configureToolAria": "設定 {{toolName}}",
"toggleToolAria": "切換 {{toolName}}",
"manageTools": "前往工具",
"edit": "編輯",
"delete": "刪除",
"deleteWarning": "您確定要刪除工具 \"{{toolName}}\" 嗎?",
"unsavedChanges": "您有未儲存的變更,如果不儲存就離開將會遺失。",
"leaveWithoutSaving": "不儲存離開",
"saveAndLeave": "儲存並離開",
"customName": "自訂名稱",
"customNamePlaceholder": "輸入自訂名稱(選填)",
"authentication": "認證",
"actions": "操作",
"addAction": "新增操作",
"noActionsFound": "找不到操作",
"url": "URL",
"urlPlaceholder": "輸入url",
"method": "方法",
"description": "描述",
"descriptionPlaceholder": "輸入描述",
"headers": "標頭",
"queryParameters": "查詢參數",
"body": "主體",
"deleteActionWarning": "您確定要刪除操作 \"{{name}}\" 嗎?",
"backToAllTools": "返回所有工具",
"save": "儲存",
"fieldName": "欄位名稱",
"fieldType": "欄位類型",
"filledByLLM": "由LLM填入",
"fieldDescription": "欄位描述",
"value": "值",
"addProperty": "新增屬性",
"propertyName": "新屬性鍵",
"add": "新增",
"cancel": "取消",
"addNew": "新增",
"name": "名稱",
"type": "類型"
"settingsIconAlt": "設定圖",
"configureToolAria": "配置 {toolName}",
"toggleToolAria": "切換 {toolName}"
}
},
"modals": {
@@ -246,18 +203,6 @@
"promptText": "提示文字",
"save": "儲存",
"nameExists": "名稱已存在"
},
"chunk": {
"add": "新增區塊",
"edit": "編輯區塊",
"title": "標題",
"enterTitle": "輸入標題",
"bodyText": "內文",
"promptText": "提示文字",
"update": "更新",
"close": "關閉",
"delete": "刪除",
"deleteConfirmation": "您確定要刪除此區塊嗎?"
}
},
"sharedConv": {
@@ -295,11 +240,6 @@
"link": "來源連結",
"view_more": "查看更多 {{count}} 個來源"
},
"attachments": {
"attach": "附件",
"remove": "刪除附件",
"uploadFailed": "上傳失敗"
},
"retry": "重試"
}
}

View File

@@ -11,7 +11,6 @@
"help": "帮助",
"emailUs": "给我们发邮件",
"documentation": "文档",
"manageAgents": "管理代理",
"demo": [
{
"header": "了解 DocsGPT",
@@ -73,13 +72,7 @@
},
"actions": "操作",
"view": "查看",
"deleteWarning": "您确定要删除 \"{{name}}\" 吗?",
"backToAll": "返回所有文档",
"chunks": "文本块",
"noChunks": "未找到文本块",
"noChunksAlt": "未找到文本块",
"goToDocuments": "前往文档",
"uploadNew": "上传新文档"
"deleteWarning": "您确定要删除 \"{{name}}\" 吗?"
},
"apiKeys": {
"label": "聊天机器人",
@@ -119,49 +112,13 @@
},
"tools": {
"label": "工具",
"searchPlaceholder": "搜索工具...",
"searchPlaceholder": "搜索...",
"addTool": "添加工具",
"noToolsFound": "未找到工具",
"selectToolSetup": "选择要设置的工具",
"settingsIconAlt": "设置图标",
"configureToolAria": "配置 {{toolName}}",
"toggleToolAria": "切换 {{toolName}}",
"manageTools": "前往工具",
"edit": "编辑",
"delete": "删除",
"deleteWarning": "您确定要删除工具 \"{{toolName}}\" 吗?",
"unsavedChanges": "您有未保存的更改,如果不保存就离开将会丢失。",
"leaveWithoutSaving": "不保存离开",
"saveAndLeave": "保存并离开",
"customName": "自定义名称",
"customNamePlaceholder": "输入自定义名称(可选)",
"authentication": "认证",
"actions": "操作",
"addAction": "添加操作",
"noActionsFound": "未找到操作",
"url": "URL",
"urlPlaceholder": "输入url",
"method": "方法",
"description": "描述",
"descriptionPlaceholder": "输入描述",
"headers": "请求头",
"queryParameters": "查询参数",
"body": "请求体",
"deleteActionWarning": "您确定要删除操作 \"{{name}}\" 吗?",
"backToAllTools": "返回所有工具",
"save": "保存",
"fieldName": "字段名称",
"fieldType": "字段类型",
"filledByLLM": "由LLM填充",
"fieldDescription": "字段描述",
"value": "值",
"addProperty": "添加属性",
"propertyName": "新属性键",
"add": "添加",
"cancel": "取消",
"addNew": "添加新的",
"name": "名称",
"type": "类型"
"configureToolAria": "配置 {toolName}",
"toggleToolAria": "切换 {toolName}"
}
},
"modals": {
@@ -246,18 +203,6 @@
"promptText": "提示文本",
"save": "保存",
"nameExists": "名称已存在"
},
"chunk": {
"add": "添加块",
"edit": "编辑块",
"title": "标题",
"enterTitle": "输入标题",
"bodyText": "正文",
"promptText": "提示文本",
"update": "更新",
"close": "关闭",
"delete": "删除",
"deleteConfirmation": "您确定要删除此块吗?"
}
},
"sharedConv": {
@@ -293,12 +238,7 @@
"title": "来源",
"text": "来源文本",
"link": "来源链接",
"view_more": "还有{{count}}个来源"
},
"attachments": {
"attach": "附件",
"remove": "删除附件",
"uploadFailed": "上传失败"
"view_more": "更多{{count}}个来源"
},
"retry": "重试"
}

View File

@@ -47,6 +47,9 @@ export default function AddActionModal({
New Action
</h2>
<div className="relative mt-6 px-3">
<span className="absolute -top-2 left-5 z-10 bg-white px-2 text-xs text-gray-4000 dark:bg-[#26272E] dark:text-silver">
Action Name
</span>
<Input
type="text"
value={actionName}
@@ -57,8 +60,7 @@ export default function AddActionModal({
}}
borderVariant="thin"
labelBgClassName="bg-white dark:bg-charleston-green-2"
placeholder="Action Name"
required={true}
placeholder={'Enter name'}
/>
<p
className={`ml-1 mt-2 text-xs italic ${

View File

@@ -97,7 +97,7 @@ export default function AgentDetailsModal({
{sharedToken && (
<div className="mb-1">
<CopyButton
textToCopy={`${baseURL}/shared/agent/${sharedToken}`}
textToCopy={`${baseURL}/agents/shared/${sharedToken}`}
padding="p-1"
/>
</div>
@@ -106,7 +106,7 @@ export default function AgentDetailsModal({
{sharedToken ? (
<div className="flex flex-col flex-wrap items-start gap-2">
<p className="f break-all font-mono text-sm text-gray-700 dark:text-[#ECECF1]">
{`${baseURL}/shared/agent/${sharedToken}`}
{`${baseURL}/agents/shared/${sharedToken}`}
</p>
</div>
) : (

View File

@@ -1,10 +1,9 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import Exit from '../assets/exit.svg';
import Input from '../components/Input';
import { ActiveState } from '../models/misc';
import ConfirmationModal from './ConfirmationModal';
import WrapperModal from './WrapperModal';
export default function ChunkModal({
type,
@@ -23,7 +22,6 @@ export default function ChunkModal({
originalText?: string;
handleDelete?: () => void;
}) {
const { t } = useTranslation();
const [title, setTitle] = React.useState('');
const [chunkText, setChunkText] = React.useState('');
const [deleteModal, setDeleteModal] = React.useState<ActiveState>('INACTIVE');
@@ -32,105 +30,157 @@ export default function ChunkModal({
setTitle(originalTitle || '');
setChunkText(originalText || '');
}, [originalTitle, originalText]);
if (modalState !== 'ACTIVE') return null;
const content = (
<div>
<h2 className="px-3 text-xl font-semibold text-jet dark:text-bright-gray">
{t(`modals.chunk.${type === 'ADD' ? 'add' : 'edit'}`)}
</h2>
<div className="relative mt-6 px-3">
<Input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
borderVariant="thin"
placeholder={t('modals.chunk.title')}
labelBgClassName="bg-white dark:bg-charleston-green-2"
/>
</div>
<div className="relative mt-6 px-3">
<div className="rounded-lg border border-silver pb-1 pt-3 dark:border-silver/40">
<span className="absolute -top-2 left-5 rounded-lg bg-white px-2 text-xs text-gray-4000 dark:bg-[#26272E] dark:text-silver">
{t('modals.chunk.bodyText')}
</span>
<textarea
id="chunk-body-text"
className="h-60 max-h-60 w-full resize-none px-3 outline-none dark:bg-transparent dark:text-white"
value={chunkText}
onChange={(e) => setChunkText(e.target.value)}
aria-label={t('modals.chunk.promptText')}
></textarea>
</div>
</div>
{type === 'ADD' ? (
<div className="mt-8 flex flex-row-reverse gap-1 px-3">
<button
onClick={() => {
handleSubmit(title, chunkText);
setModalState('INACTIVE');
}}
className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-violets-are-blue"
>
{t('modals.chunk.add')}
</button>
<button
onClick={() => {
setModalState('INACTIVE');
}}
className="cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium hover:bg-gray-100 dark:bg-transparent dark:text-light-gray dark:hover:bg-[#767183]/50"
>
{t('modals.chunk.close')}
</button>
</div>
) : (
<div className="mt-8 flex w-full items-center justify-between px-3">
<button
className="text-nowrap rounded-full border border-solid border-red-500 px-5 py-2 text-sm text-red-500 hover:bg-red-500 hover:text-white"
onClick={() => {
setDeleteModal('ACTIVE');
}}
>
{t('modals.chunk.delete')}
</button>
<div className="flex flex-row-reverse gap-1">
<button
onClick={() => {
handleSubmit(title, chunkText);
setModalState('INACTIVE');
}}
className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-violets-are-blue"
>
{t('modals.chunk.update')}
</button>
<button
onClick={() => {
setModalState('INACTIVE');
}}
className="cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium hover:bg-gray-100 dark:bg-transparent dark:text-light-gray dark:hover:bg-[#767183]/50"
>
{t('modals.chunk.close')}
</button>
</div>
</div>
)}
</div>
);
return (
<>
<WrapperModal
close={() => setModalState('INACTIVE')}
className="sm:w-[620px]"
if (type === 'ADD') {
return (
<div
className={`${
modalState === 'ACTIVE' ? 'visible' : 'hidden'
} fixed left-0 top-0 z-30 flex h-screen w-screen items-center justify-center bg-gray-alpha`}
>
{content}
</WrapperModal>
{type === 'EDIT' && (
<article className="flex w-11/12 flex-col gap-4 rounded-2xl bg-white shadow-lg dark:bg-[#26272E] sm:w-[620px]">
<div className="relative">
<button
className="absolute right-4 top-3 m-2 w-3"
onClick={() => {
setModalState('INACTIVE');
}}
>
<img className="filter dark:invert" src={Exit} />
</button>
<div className="p-6">
<h2 className="px-3 text-xl font-semibold text-jet dark:text-bright-gray">
Add Chunk
</h2>
<div className="relative mt-6 px-3">
<span className="absolute -top-2 left-5 z-10 bg-white px-2 text-xs text-gray-4000 dark:bg-[#26272E] dark:text-silver">
Title
</span>
<Input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
borderVariant="thin"
placeholder={'Enter title'}
labelBgClassName="bg-white dark:bg-charleston-green-2"
></Input>
</div>
<div className="relative mt-6 px-3">
<div className="rounded-lg border border-silver pb-1 pt-3 dark:border-silver/40">
<span className="absolute -top-2 left-5 rounded-lg bg-white px-2 text-xs text-gray-4000 dark:bg-[#26272E] dark:text-silver">
Body text
</span>
<textarea
id="chunk-body-text"
className="h-60 w-full px-3 outline-none dark:bg-transparent dark:text-white"
value={chunkText}
onChange={(e) => setChunkText(e.target.value)}
aria-label="Prompt Text"
></textarea>
</div>
</div>
<div className="mt-8 flex flex-row-reverse gap-1 px-3">
<button
onClick={() => {
handleSubmit(title, chunkText);
setModalState('INACTIVE');
}}
className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-violets-are-blue"
>
Add
</button>
<button
onClick={() => {
setModalState('INACTIVE');
}}
className="cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium hover:bg-gray-100 dark:bg-transparent dark:text-light-gray dark:hover:bg-[#767183]/50"
>
Close
</button>
</div>
</div>
</div>
</article>
</div>
);
} else {
return (
<div
className={`${
modalState === 'ACTIVE' ? 'visible' : 'hidden'
} fixed left-0 top-0 z-30 flex h-screen w-screen items-center justify-center bg-gray-alpha`}
>
<article className="flex w-11/12 flex-col gap-4 rounded-2xl bg-white shadow-lg dark:bg-[#26272E] sm:w-[620px]">
<div className="relative">
<button
className="absolute right-4 top-3 m-2 w-3"
onClick={() => {
setModalState('INACTIVE');
}}
>
<img className="filter dark:invert" src={Exit} />
</button>
<div className="p-6">
<h2 className="px-3 text-xl font-semibold text-jet dark:text-bright-gray">
Edit Chunk
</h2>
<div className="relative mt-6 px-3">
<Input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
borderVariant="thin"
placeholder={'Enter title'}
labelBgClassName="bg-white dark:bg-charleston-green-2"
></Input>
</div>
<div className="relative mt-6 px-3">
<div className="rounded-lg border border-silver pb-1 pt-3 dark:border-silver/40">
<span className="absolute -top-2 left-5 rounded-lg bg-white px-2 text-xs text-gray-4000 dark:bg-[#26272E] dark:text-silver">
Body text
</span>
<textarea
id="chunk-body-text"
className="h-60 w-full px-3 outline-none dark:bg-transparent dark:text-white"
value={chunkText}
onChange={(e) => setChunkText(e.target.value)}
aria-label="Prompt Text"
></textarea>
</div>
</div>
<div className="mt-8 flex w-full items-center justify-between px-3">
<button
className="text-nowrap rounded-full border border-solid border-red-500 px-5 py-2 text-sm text-red-500 hover:bg-red-500 hover:text-white"
onClick={() => {
setDeleteModal('ACTIVE');
}}
>
Delete
</button>
<div className="flex flex-row-reverse gap-1">
<button
onClick={() => {
handleSubmit(title, chunkText);
setModalState('INACTIVE');
}}
className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-violets-are-blue"
>
Update
</button>
<button
onClick={() => {
setModalState('INACTIVE');
}}
className="cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium hover:bg-gray-100 dark:bg-transparent dark:text-light-gray dark:hover:bg-[#767183]/50"
>
Close
</button>
</div>
</div>
</div>
</div>
</article>
<ConfirmationModal
message={t('modals.chunk.deleteConfirmation')}
message="Are you sure you want to delete this chunk?"
modalState={deleteModal}
setModalState={setDeleteModal}
handleSubmit={
@@ -140,9 +190,9 @@ export default function ChunkModal({
/* no-op */
}
}
submitLabel={t('modals.chunk.delete')}
submitLabel="Delete"
/>
)}
</>
);
</div>
);
}
}

View File

@@ -25,7 +25,6 @@ export default function ConfigToolModal({
const { t } = useTranslation();
const token = useSelector(selectToken);
const [authKey, setAuthKey] = React.useState<string>('');
const [customName, setCustomName] = React.useState<string>('');
const handleAddTool = (tool: AvailableToolType) => {
userService
@@ -35,7 +34,6 @@ export default function ConfigToolModal({
displayName: tool.displayName,
description: tool.description,
config: { token: authKey },
customName: customName,
actions: tool.actions,
status: true,
},
@@ -60,16 +58,6 @@ export default function ConfigToolModal({
{t('modals.configTool.type')}:{' '}
<span className="font-semibold">{tool?.name}</span>
</p>
<div className="mt-6 px-3">
<Input
type="text"
value={customName}
onChange={(e) => setCustomName(e.target.value)}
borderVariant="thin"
placeholder="Enter custom name (optional)"
labelBgClassName="bg-white dark:bg-charleston-green-2"
/>
</div>
<div className="mt-6 px-3">
<Input
type="text"

View File

@@ -32,7 +32,12 @@ export default function ConfirmationModal({
return (
<>
{modalState === 'ACTIVE' && (
<WrapperModal close={() => setModalState('INACTIVE')}>
<WrapperModal
close={() => {
setModalState('INACTIVE');
handleCancel && handleCancel();
}}
>
<div className="relative">
<div>
<p className="font-base mb-1 w-[90%] break-words text-lg text-jet dark:text-bright-gray">

View File

@@ -1,5 +1,4 @@
import React, { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import Exit from '../assets/exit.svg';
@@ -46,7 +45,7 @@ export default function WrapperModal({
};
}, [close]);
const modalContent = (
return (
<div className="fixed left-0 top-0 z-30 flex h-screen w-screen items-center justify-center bg-gray-alpha bg-opacity-50">
<div
ref={modalRef}
@@ -64,6 +63,4 @@ export default function WrapperModal({
</div>
</div>
);
return createPortal(modalContent, document.body);
}

View File

@@ -51,7 +51,7 @@ function AddPrompt({
</label>
<textarea
id="new-prompt-content"
className="h-56 w-full resize-none rounded-lg border-2 border-silver px-3 py-2 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
className="h-56 w-full rounded-lg border-2 border-silver px-3 py-2 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
value={newPromptContent}
onChange={(e) => setNewPromptContent(e.target.value)}
aria-label="Prompt Text"
@@ -123,7 +123,7 @@ function EditPrompt({
</label>
<textarea
id="edit-prompt-content"
className="h-56 w-full resize-none rounded-lg border-2 border-silver px-3 py-2 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
className="h-56 w-full rounded-lg border-2 border-silver px-3 py-2 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
value={editPromptContent}
onChange={(e) => setEditPromptContent(e.target.value)}
aria-label="Prompt Text"

View File

@@ -25,7 +25,6 @@ export interface Preference {
modalState: ActiveState;
paginatedDocuments: Doc[] | null;
agents: Agent[] | null;
sharedAgents: Agent[] | null;
selectedAgent: Agent | null;
}
@@ -52,7 +51,6 @@ const initialState: Preference = {
modalState: 'INACTIVE',
paginatedDocuments: null,
agents: null,
sharedAgents: null,
selectedAgent: null,
};
@@ -93,9 +91,6 @@ export const prefSlice = createSlice({
setAgents: (state, action) => {
state.agents = action.payload;
},
setSharedAgents: (state, action) => {
state.sharedAgents = action.payload;
},
setSelectedAgent: (state, action) => {
state.selectedAgent = action.payload;
},
@@ -114,7 +109,6 @@ export const {
setModalStateDeleteConv,
setPaginatedDocuments,
setAgents,
setSharedAgents,
setSelectedAgent,
} = prefSlice.actions;
export default prefSlice.reducer;
@@ -191,7 +185,5 @@ export const selectTokenLimit = (state: RootState) =>
export const selectPaginatedDocuments = (state: RootState) =>
state.preference.paginatedDocuments;
export const selectAgents = (state: RootState) => state.preference.agents;
export const selectSharedAgents = (state: RootState) =>
state.preference.sharedAgents;
export const selectSelectedAgent = (state: RootState) =>
state.preference.selectedAgent;

View File

@@ -312,7 +312,7 @@ export default function Documents({
/>
</div>
<button
className="flex h-[32px] min-w-[108px] items-center justify-center whitespace-normal rounded-full bg-purple-30 px-4 text-sm text-white hover:bg-violets-are-blue"
className="flex h-[32px] w-[108px] items-center justify-center rounded-full bg-purple-30 text-sm text-white hover:bg-violets-are-blue"
title={t('settings.documents.addNew')}
onClick={() => {
setIsOnboarding(false);
@@ -450,7 +450,7 @@ export default function Documents({
options={getActionOptions(index, document)}
anchorRef={getMenuRef(docId)}
position="bottom-left"
offset={{ x: 48, y: 0 }}
offset={{ x: 48, y: -24 }}
className="z-50"
/>
</div>
@@ -641,11 +641,11 @@ function DocumentChunks({
>
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
</button>
<p className="mt-px">{t('settings.documents.backToAll')}</p>
<p className="mt-px">Back to all documents</p>
</div>
<div className="my-3 flex items-center justify-between gap-1">
<div className="flex w-full items-center gap-2 text-eerie-black dark:text-bright-gray sm:w-auto">
<p className="hidden text-2xl font-semibold sm:flex">{`${totalChunks} ${t('settings.documents.chunks')}`}</p>
<p className="hidden text-2xl font-semibold sm:flex">{`${totalChunks} Chunks`}</p>
<label htmlFor="chunk-search-input" className="sr-only">
{t('settings.documents.searchPlaceholder')}
</label>
@@ -663,7 +663,7 @@ function DocumentChunks({
/>
</div>
<button
className="flex h-[32px] min-w-[108px] items-center justify-center whitespace-normal rounded-full bg-purple-30 px-4 text-sm text-white hover:bg-violets-are-blue"
className="flex h-[32px] w-[108px] items-center justify-center rounded-full bg-purple-30 text-sm text-white hover:bg-violets-are-blue"
title={t('settings.documents.addNew')}
onClick={() => setAddModal('ACTIVE')}
>
@@ -687,10 +687,10 @@ function DocumentChunks({
<div className="col-span-2 mt-24 text-center text-gray-500 dark:text-gray-400 lg:col-span-3">
<img
src={isDarkTheme ? NoFilesDarkIcon : NoFilesIcon}
alt={t('settings.documents.noChunksAlt')}
alt="No tools found"
className="mx-auto mb-2 h-24 w-24"
/>
{t('settings.documents.noChunks')}
No chunks found
</div>
) : (
paginatedChunks

View File

@@ -36,7 +36,7 @@ export default function General() {
{ label: '繁體中文(臺灣)', value: 'zhTW' },
{ label: 'Русский', value: 'ru' },
];
const chunks = ['Auto', '0', '2', '4', '6', '8', '10'];
const chunks = ['0', '2', '4', '6', '8', '10'];
const token_limits = new Map([
[0, t('settings.general.none')],
[100, t('settings.general.low')],

View File

@@ -17,16 +17,12 @@ type LogsProps = {
export default function Logs({ agentId, tableHeader }: LogsProps) {
const token = useSelector(selectToken);
const [logsByPage, setLogsByPage] = useState<Record<number, LogData[]>>({});
const [logs, setLogs] = useState<LogData[]>([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [loadingLogs, setLoadingLogs] = useLoaderState(true);
const logs = Object.values(logsByPage).flat();
const fetchLogs = async () => {
if (logsByPage[page] && logsByPage[page].length > 0) return;
setLoadingLogs(true);
try {
const response = await userService.getLogs(
@@ -38,13 +34,9 @@ export default function Logs({ agentId, tableHeader }: LogsProps) {
token,
);
if (!response.ok) throw new Error('Failed to fetch logs');
const data = await response.json();
setLogsByPage((prev) => ({
...prev,
[page]: data.logs,
}));
setHasMore(data.has_more);
const olderLogs = await response.json();
setLogs((prevLogs) => [...prevLogs, ...olderLogs.logs]);
setHasMore(olderLogs.has_more);
} catch (error) {
console.error(error);
} finally {
@@ -81,11 +73,16 @@ function LogsTable({ logs, setPage, loading, tableHeader }: LogsTableProps) {
const [openLogId, setOpenLogId] = useState<string | null>(null);
const handleLogToggle = (logId: string) => {
if (openLogId === logId) {
setOpenLogId(null);
} else {
setOpenLogId(logId);
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;
}
}
setOpenLogId(logId);
};
const firstObserver = useCallback((node: HTMLDivElement | null) => {
@@ -119,27 +116,16 @@ function LogsTable({ logs, setPage, loading, tableHeader }: LogsTableProps) {
{tableHeader ? tableHeader : t('settings.logs.tableHeader')}
</p>
</div>
<div className="relative flex h-[51vh] flex-grow flex-col items-start gap-2 overflow-y-auto overscroll-contain bg-transparent p-4">
<div className="flex h-[51vh] flex-grow flex-col items-start gap-2 overflow-y-auto bg-transparent p-4">
{logs?.map((log, index) => {
if (index === logs.length - 1) {
return (
<div ref={firstObserver} key={index} className="w-full">
<Log
log={log}
isOpen={openLogId === log.id}
onToggle={handleLogToggle}
/>
<Log log={log} onToggle={handleLogToggle} />
</div>
);
} else
return (
<Log
key={index}
log={log}
isOpen={openLogId === log.id}
onToggle={handleLogToggle}
/>
);
return <Log key={index} log={log} onToggle={handleLogToggle} />;
})}
{loading && <SkeletonLoader component="logs" />}
</div>
@@ -148,11 +134,9 @@ function LogsTable({ logs, setPage, loading, tableHeader }: LogsTableProps) {
}
function Log({
log,
isOpen,
onToggle,
}: {
log: LogData;
isOpen: boolean;
onToggle: (id: string) => void;
}) {
const { t } = useTranslation();
@@ -164,17 +148,20 @@ function Log({
const { id, action, timestamp, ...filteredLog } = log;
return (
<div className="group w-full rounded-xl bg-transparent hover:bg-[#F9F9F9] hover:dark:bg-dark-charcoal">
<div
onClick={() => onToggle(log.id)}
className={`flex cursor-pointer flex-row items-start gap-2 p-2 px-4 py-3 text-gray-900 ${
isOpen ? 'rounded-t-xl bg-[#F1F1F1] dark:bg-[#1B1B1B]' : ''
}`}
>
<details
id={log.id}
className="group w-full rounded-xl bg-transparent hover:bg-[#F9F9F9] group-open:opacity-80 hover:dark:bg-dark-charcoal [&[open]]:border [&[open]]:border-[#d9d9d9] [&_summary::-webkit-details-marker]:hidden"
onToggle={(e) => {
if ((e.target as HTMLDetailsElement).open) {
onToggle(log.id);
}
}}
>
<summary className="flex cursor-pointer flex-row items-start gap-2 p-2 px-4 py-3 text-gray-900 group-open:rounded-t-xl group-open:bg-[#F1F1F1] dark:group-open:bg-[#1B1B1B]">
<img
src={ChevronRight}
alt="Expand log entry"
className={`mt-[3px] h-3 w-3 transition duration-300 ${isOpen ? 'rotate-90' : ''}`}
className="mt-[3px] h-3 w-3 transition duration-300 group-open:rotate-90"
/>
<span className="flex flex-row gap-2">
<h2 className="text-xs text-black/60 dark:text-bright-gray">{`${log.timestamp}`}</h2>
@@ -187,22 +174,18 @@ function Log({
: log.question}
</h2>
</span>
</div>
{isOpen && (
<div className="rounded-b-xl bg-[#F1F1F1] px-4 py-3 dark:bg-[#1B1B1B]">
<div className="scrollbar-thin overflow-y-auto">
<pre className="whitespace-pre-wrap break-words px-2 font-mono text-xs leading-relaxed text-gray-700 dark:text-gray-400">
{JSON.stringify(filteredLog, null, 2)}
</pre>
</div>
<div className="my-px w-fit">
<CopyButton
textToCopy={JSON.stringify(filteredLog)}
showText={true}
/>
</div>
</summary>
<div className="px-4 py-3 group-open:rounded-b-xl group-open:bg-[#F1F1F1] dark:group-open:bg-[#1B1B1B]">
<p className="break-words px-2 text-xs leading-relaxed text-gray-700 dark:text-gray-400">
{JSON.stringify(filteredLog, null, 2)}
</p>
<div className="my-px w-fit">
<CopyButton
textToCopy={JSON.stringify(filteredLog)}
showText={true}
/>
</div>
)}
</div>
</div>
</details>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import userService from '../api/services/userService';
import ThreeDotsIcon from '../assets/three-dots.svg';
import CogwheelIcon from '../assets/cogwheel.svg';
import NoFilesIcon from '../assets/no-files.svg';
import NoFilesDarkIcon from '../assets/no-files-dark.svg';
import Input from '../components/Input';
@@ -15,10 +15,6 @@ import { ActiveState } from '../models/misc';
import { selectToken } from '../preferences/preferenceSlice';
import ToolConfig from './ToolConfig';
import { APIToolType, UserToolType } from './types';
import ContextMenu, { MenuOption } from '../components/ContextMenu';
import Edit from '../assets/edit.svg';
import Trash from '../assets/red-trash.svg';
import ConfirmationModal from '../modals/ConfirmationModal';
export default function Tools() {
const { t } = useTranslation();
@@ -33,57 +29,6 @@ export default function Tools() {
UserToolType | APIToolType | null
>(null);
const [loading, setLoading] = React.useState(false);
const [activeMenuId, setActiveMenuId] = React.useState<string | null>(null);
const menuRefs = React.useRef<{
[key: string]: React.RefObject<HTMLDivElement>;
}>({});
const [deleteModalState, setDeleteModalState] =
React.useState<ActiveState>('INACTIVE');
const [toolToDelete, setToolToDelete] = React.useState<UserToolType | null>(
null,
);
React.useEffect(() => {
userTools.forEach((tool) => {
if (!menuRefs.current[tool.id]) {
menuRefs.current[tool.id] = React.createRef();
}
});
}, [userTools]);
const handleDeleteTool = (tool: UserToolType) => {
setToolToDelete(tool);
setDeleteModalState('ACTIVE');
};
const confirmDeleteTool = () => {
if (toolToDelete) {
userService.deleteTool({ id: toolToDelete.id }, token).then(() => {
getUserTools();
setDeleteModalState('INACTIVE');
setToolToDelete(null);
});
}
};
const getMenuOptions = (tool: UserToolType): MenuOption[] => [
{
icon: Edit,
label: t('settings.tools.edit'),
onClick: () => handleSettingsClick(tool),
variant: 'primary',
iconWidth: 14,
iconHeight: 14,
},
{
icon: Trash,
label: t('settings.tools.delete'),
onClick: () => handleDeleteTool(tool),
variant: 'danger',
iconWidth: 12,
iconHeight: 12,
},
];
const getUserTools = () => {
setLoading(true);
@@ -157,8 +102,11 @@ export default function Tools() {
) : (
<div className="mt-8">
<div className="relative flex flex-col">
<div className="my-3 flex flex-col items-start gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="w-full sm:w-auto">
<div className="my-3 flex items-center justify-between gap-1">
<div className="p-1">
<label htmlFor="tool-search-input" className="sr-only">
{t('settings.tools.searchPlaceholder')}
</label>
<Input
maxLength={256}
placeholder={t('settings.tools.searchPlaceholder')}
@@ -171,7 +119,7 @@ export default function Tools() {
/>
</div>
<button
className="flex h-[32px] min-w-[108px] items-center justify-center whitespace-normal rounded-full bg-purple-30 px-4 text-sm text-white hover:bg-violets-are-blue"
className="flex h-[30px] w-[108px] items-center justify-center rounded-full bg-purple-30 text-sm text-white hover:bg-violets-are-blue"
onClick={() => {
setAddToolModalState('ACTIVE');
}}
@@ -202,41 +150,28 @@ export default function Tools() {
) : (
userTools
.filter((tool) =>
(tool.customName || tool.displayName)
tool.displayName
.toLowerCase()
.includes(searchTerm.toLowerCase()),
)
.map((tool, index) => (
<div
key={index}
className="relative flex h-52 w-[300px] flex-col justify-between rounded-2xl bg-[#F5F5F5] p-6 hover:bg-[#ECECEC] dark:bg-[#383838] dark:hover:bg-[#303030]"
className="relative flex h-52 w-[300px] flex-col justify-between rounded-2xl border border-light-gainsboro bg-white-3000 p-6 dark:border-arsenic dark:bg-transparent"
>
<div
ref={menuRefs.current[tool.id]}
onClick={(e) => {
e.stopPropagation();
setActiveMenuId(
activeMenuId === tool.id ? null : tool.id,
);
}}
className="absolute right-4 top-4 z-10 cursor-pointer"
<button
onClick={() => handleSettingsClick(tool)}
aria-label={t('settings.tools.configureToolAria', {
toolName: tool.displayName,
})}
className="absolute right-4 top-4"
>
<img
src={ThreeDotsIcon}
src={CogwheelIcon}
alt={t('settings.tools.settingsIconAlt')}
className="h-[19px] w-[19px]"
/>
<ContextMenu
isOpen={activeMenuId === tool.id}
setIsOpen={(isOpen) => {
setActiveMenuId(isOpen ? tool.id : null);
}}
options={getMenuOptions(tool)}
anchorRef={menuRefs.current[tool.id]}
position="bottom-right"
offset={{ x: 0, y: 0 }}
/>
</div>
</button>
<div className="w-full">
<div className="flex w-full items-center px-1">
<img
@@ -247,10 +182,10 @@ export default function Tools() {
</div>
<div className="mt-[9px]">
<p
title={tool.customName || tool.displayName}
title={tool.displayName}
className="truncate px-1 text-[13px] font-semibold capitalize leading-relaxed text-raisin-black-light dark:text-bright-gray"
>
{tool.customName || tool.displayName}
{tool.displayName}
</p>
<p className="mt-1 h-24 overflow-auto px-1 text-[12px] leading-relaxed text-old-silver dark:text-sonic-silver-light">
{tool.description}
@@ -266,7 +201,7 @@ export default function Tools() {
size="small"
id={`toolToggle-${index}`}
ariaLabel={t('settings.tools.toggleToolAria', {
toolName: tool.customName || tool.displayName,
toolName: tool.displayName,
})}
/>
</div>
@@ -283,17 +218,6 @@ export default function Tools() {
getUserTools={getUserTools}
onToolAdded={handleToolAdded}
/>
<ConfirmationModal
message={t('settings.tools.deleteWarning', {
toolName:
toolToDelete?.customName || toolToDelete?.displayName || '',
})}
modalState={deleteModalState}
setModalState={setDeleteModalState}
handleSubmit={confirmDeleteTool}
submitLabel={t('settings.tools.delete')}
variant="danger"
/>
</div>
)}
</div>

View File

@@ -41,7 +41,6 @@ export type UserToolType = {
id: string;
name: string;
displayName: string;
customName?: string;
description: string;
status: boolean;
config: {
@@ -82,7 +81,6 @@ export type APIToolType = {
id: string;
name: string;
displayName: string;
customName?: string;
description: string;
status: boolean;
config: { actions: { [key: string]: APIActionType } };

View File

@@ -42,7 +42,6 @@ const preloadedState: { preference: Preference } = {
modalState: 'INACTIVE',
paginatedDocuments: null,
agents: null,
sharedAgents: null,
selectedAgent: null,
},
};

View File

@@ -1,29 +0,0 @@
/**
* Deeply compares two objects for equality
* @param obj1 First object to compare
* @param obj2 Second object to compare
* @returns boolean indicating if objects are equal
*/
export function areObjectsEqual(obj1: any, obj2: any): boolean {
if (obj1 === obj2) return true;
if (obj1 == null || obj2 == null) return false;
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return false;
if (Array.isArray(obj1) && Array.isArray(obj2)) {
if (obj1.length !== obj2.length) return false;
return obj1.every((val, idx) => areObjectsEqual(val, obj2[idx]));
}
if (obj1 instanceof Date && obj2 instanceof Date) {
return obj1.getTime() === obj2.getTime();
}
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
return keys1.every((key) => {
return keys2.includes(key) && areObjectsEqual(obj1[key], obj2[key]);
});
}