mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
Merge: branch main from upstream
This commit is contained in:
@@ -24,6 +24,7 @@ conversations_collection = db["conversations"]
|
||||
sources_collection = db["sources"]
|
||||
prompts_collection = db["prompts"]
|
||||
api_key_collection = db["api_keys"]
|
||||
user_logs_collection = db["user_logs"]
|
||||
answer = Blueprint("answer", __name__)
|
||||
|
||||
gpt_model = ""
|
||||
@@ -37,7 +38,9 @@ if settings.MODEL_NAME: # in case there is particular model name configured
|
||||
gpt_model = settings.MODEL_NAME
|
||||
|
||||
# load the prompts
|
||||
current_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
current_dir = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
)
|
||||
with open(os.path.join(current_dir, "prompts", "chat_combine_default.txt"), "r") as f:
|
||||
chat_combine_template = f.read()
|
||||
|
||||
@@ -98,9 +101,12 @@ def get_retriever(source_id: str):
|
||||
return retriever_name
|
||||
|
||||
|
||||
|
||||
def is_azure_configured():
|
||||
return settings.OPENAI_API_BASE and settings.OPENAI_API_VERSION and settings.AZURE_DEPLOYMENT_NAME
|
||||
return (
|
||||
settings.OPENAI_API_BASE
|
||||
and settings.OPENAI_API_VERSION
|
||||
and settings.AZURE_DEPLOYMENT_NAME
|
||||
)
|
||||
|
||||
|
||||
def save_conversation(conversation_id, question, response, source_log_docs, llm):
|
||||
@@ -201,6 +207,21 @@ def complete_stream(
|
||||
data = json.dumps({"type": "id", "id": str(conversation_id)})
|
||||
yield f"data: {data}\n\n"
|
||||
|
||||
retriever_params = retriever.get_params()
|
||||
user_logs_collection.insert_one(
|
||||
{
|
||||
"action": "stream_answer",
|
||||
"level": "info",
|
||||
"user": "local",
|
||||
"api_key": user_api_key,
|
||||
"question": question,
|
||||
"response": response_full,
|
||||
"sources": source_log_docs,
|
||||
"retriever_params": retriever_params,
|
||||
"timestamp": datetime.datetime.now(datetime.timezone.utc),
|
||||
}
|
||||
)
|
||||
|
||||
data = json.dumps({"type": "end"})
|
||||
yield f"data: {data}\n\n"
|
||||
except Exception as e:
|
||||
@@ -258,7 +279,7 @@ def stream():
|
||||
user_api_key = data["api_key"]
|
||||
|
||||
elif "active_docs" in data:
|
||||
source = {"active_docs" : data["active_docs"]}
|
||||
source = {"active_docs": data["active_docs"]}
|
||||
retriever_name = get_retriever(data["active_docs"]) or retriever_name
|
||||
user_api_key = None
|
||||
|
||||
@@ -266,12 +287,13 @@ def stream():
|
||||
source = {}
|
||||
user_api_key = None
|
||||
|
||||
current_app.logger.info(f"/stream - request_data: {data}, source: {source}",
|
||||
extra={"data": json.dumps({"request_data": data, "source": source})}
|
||||
current_app.logger.info(
|
||||
f"/stream - request_data: {data}, source: {source}",
|
||||
extra={"data": json.dumps({"request_data": data, "source": source})},
|
||||
)
|
||||
|
||||
prompt = get_prompt(prompt_id)
|
||||
|
||||
|
||||
retriever = RetrieverCreator.create_retriever(
|
||||
retriever_name,
|
||||
question=question,
|
||||
@@ -304,8 +326,9 @@ def stream():
|
||||
mimetype="text/event-stream",
|
||||
)
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"/stream - error: {str(e)} - traceback: {traceback.format_exc()}",
|
||||
extra={"error": str(e), "traceback": traceback.format_exc()}
|
||||
current_app.logger.error(
|
||||
f"/stream - error: {str(e)} - traceback: {traceback.format_exc()}",
|
||||
extra={"error": str(e), "traceback": traceback.format_exc()},
|
||||
)
|
||||
message = e.args[0]
|
||||
status_code = 400
|
||||
@@ -364,7 +387,7 @@ def api_answer():
|
||||
retriever_name = data_key["retriever"] or retriever_name
|
||||
user_api_key = data["api_key"]
|
||||
elif "active_docs" in data:
|
||||
source = {"active_docs":data["active_docs"]}
|
||||
source = {"active_docs": data["active_docs"]}
|
||||
retriever_name = get_retriever(data["active_docs"]) or retriever_name
|
||||
user_api_key = None
|
||||
else:
|
||||
@@ -373,8 +396,9 @@ def api_answer():
|
||||
|
||||
prompt = get_prompt(prompt_id)
|
||||
|
||||
current_app.logger.info(f"/api/answer - request_data: {data}, source: {source}",
|
||||
extra={"data": json.dumps({"request_data": data, "source": source})}
|
||||
current_app.logger.info(
|
||||
f"/api/answer - request_data: {data}, source: {source}",
|
||||
extra={"data": json.dumps({"request_data": data, "source": source})},
|
||||
)
|
||||
|
||||
retriever = RetrieverCreator.create_retriever(
|
||||
@@ -406,13 +430,30 @@ def api_answer():
|
||||
|
||||
result = {"answer": response_full, "sources": source_log_docs}
|
||||
result["conversation_id"] = str(
|
||||
save_conversation(conversation_id, question, response_full, source_log_docs, llm)
|
||||
save_conversation(
|
||||
conversation_id, question, response_full, source_log_docs, llm
|
||||
)
|
||||
)
|
||||
retriever_params = retriever.get_params()
|
||||
user_logs_collection.insert_one(
|
||||
{
|
||||
"action": "api_answer",
|
||||
"level": "info",
|
||||
"user": "local",
|
||||
"api_key": user_api_key,
|
||||
"question": question,
|
||||
"response": response_full,
|
||||
"sources": source_log_docs,
|
||||
"retriever_params": retriever_params,
|
||||
"timestamp": datetime.datetime.now(datetime.timezone.utc),
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"/api/answer - error: {str(e)} - traceback: {traceback.format_exc()}",
|
||||
extra={"error": str(e), "traceback": traceback.format_exc()}
|
||||
current_app.logger.error(
|
||||
f"/api/answer - error: {str(e)} - traceback: {traceback.format_exc()}",
|
||||
extra={"error": str(e), "traceback": traceback.format_exc()},
|
||||
)
|
||||
return bad_request(500, str(e))
|
||||
|
||||
@@ -431,7 +472,7 @@ def api_search():
|
||||
source = {"active_docs":data_key["source"]}
|
||||
user_api_key = data["api_key"]
|
||||
elif "active_docs" in data:
|
||||
source = {"active_docs":data["active_docs"]}
|
||||
source = {"active_docs": data["active_docs"]}
|
||||
user_api_key = None
|
||||
else:
|
||||
source = {}
|
||||
@@ -445,9 +486,10 @@ def api_search():
|
||||
token_limit = data["token_limit"]
|
||||
else:
|
||||
token_limit = settings.DEFAULT_MAX_HISTORY
|
||||
|
||||
current_app.logger.info(f"/api/answer - request_data: {data}, source: {source}",
|
||||
extra={"data": json.dumps({"request_data": data, "source": source})}
|
||||
|
||||
current_app.logger.info(
|
||||
f"/api/answer - request_data: {data}, source: {source}",
|
||||
extra={"data": json.dumps({"request_data": data, "source": source})},
|
||||
)
|
||||
|
||||
retriever = RetrieverCreator.create_retriever(
|
||||
@@ -463,6 +505,20 @@ def api_search():
|
||||
)
|
||||
docs = retriever.search()
|
||||
|
||||
retriever_params = retriever.get_params()
|
||||
user_logs_collection.insert_one(
|
||||
{
|
||||
"action": "api_search",
|
||||
"level": "info",
|
||||
"user": "local",
|
||||
"api_key": user_api_key,
|
||||
"question": question,
|
||||
"sources": docs,
|
||||
"retriever_params": retriever_params,
|
||||
"timestamp": datetime.datetime.now(datetime.timezone.utc),
|
||||
}
|
||||
)
|
||||
|
||||
if data.get("isNoneDoc"):
|
||||
for doc in docs:
|
||||
doc["source"] = "None"
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import datetime
|
||||
import os
|
||||
import uuid
|
||||
import shutil
|
||||
from flask import Blueprint, request, jsonify
|
||||
from pymongo import MongoClient
|
||||
from bson.objectid import ObjectId
|
||||
import uuid
|
||||
|
||||
from bson.binary import Binary, UuidRepresentation
|
||||
from werkzeug.utils import secure_filename
|
||||
from bson.dbref import DBRef
|
||||
from bson.objectid import ObjectId
|
||||
from flask import Blueprint, jsonify, request
|
||||
from pymongo import MongoClient
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from application.api.user.tasks import ingest, ingest_remote
|
||||
|
||||
from application.core.settings import settings
|
||||
@@ -19,11 +22,36 @@ sources_collection = db["sources"]
|
||||
prompts_collection = db["prompts"]
|
||||
feedback_collection = db["feedback"]
|
||||
api_key_collection = db["api_keys"]
|
||||
token_usage_collection = db["token_usage"]
|
||||
shared_conversations_collections = db["shared_conversations"]
|
||||
user_logs_collection = db["user_logs"]
|
||||
|
||||
user = Blueprint("user", __name__)
|
||||
|
||||
current_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
current_dir = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
)
|
||||
|
||||
|
||||
def generate_minute_range(start_date, end_date):
|
||||
return {
|
||||
(start_date + datetime.timedelta(minutes=i)).strftime("%Y-%m-%d %H:%M:00"): 0
|
||||
for i in range(int((end_date - start_date).total_seconds() // 60) + 1)
|
||||
}
|
||||
|
||||
|
||||
def generate_hourly_range(start_date, end_date):
|
||||
return {
|
||||
(start_date + datetime.timedelta(hours=i)).strftime("%Y-%m-%d %H:00"): 0
|
||||
for i in range(int((end_date - start_date).total_seconds() // 3600) + 1)
|
||||
}
|
||||
|
||||
|
||||
def generate_date_range(start_date, end_date):
|
||||
return {
|
||||
(start_date + datetime.timedelta(days=i)).strftime("%Y-%m-%d"): 0
|
||||
for i in range((end_date - start_date).days + 1)
|
||||
}
|
||||
|
||||
|
||||
@user.route("/api/delete_conversation", methods=["POST"])
|
||||
@@ -53,7 +81,9 @@ def get_conversations():
|
||||
conversations = conversations_collection.find().sort("date", -1).limit(30)
|
||||
list_conversations = []
|
||||
for conversation in conversations:
|
||||
list_conversations.append({"id": str(conversation["_id"]), "name": conversation["name"]})
|
||||
list_conversations.append(
|
||||
{"id": str(conversation["_id"]), "name": conversation["name"]}
|
||||
)
|
||||
|
||||
# list_conversations = [{"id": "default", "name": "default"}, {"id": "jeff", "name": "jeff"}]
|
||||
|
||||
@@ -84,7 +114,12 @@ def api_feedback():
|
||||
question = data["question"]
|
||||
answer = data["answer"]
|
||||
feedback = data["feedback"]
|
||||
new_doc = {"question": question, "answer": answer, "feedback": feedback}
|
||||
new_doc = {
|
||||
"question": question,
|
||||
"answer": answer,
|
||||
"feedback": feedback,
|
||||
"timestamp": datetime.datetime.now(datetime.timezone.utc),
|
||||
}
|
||||
if "api_key" in data:
|
||||
new_doc["api_key"] = data["api_key"]
|
||||
feedback_collection.insert_one(new_doc)
|
||||
@@ -110,24 +145,31 @@ def delete_by_ids():
|
||||
def delete_old():
|
||||
"""Delete old indexes."""
|
||||
import shutil
|
||||
|
||||
source_id = request.args.get("source_id")
|
||||
doc = sources_collection.find_one({
|
||||
"_id": ObjectId(source_id),
|
||||
"user": "local",
|
||||
})
|
||||
if(doc is None):
|
||||
return {"status":"not found"},404
|
||||
doc = sources_collection.find_one(
|
||||
{
|
||||
"_id": ObjectId(source_id),
|
||||
"user": "local",
|
||||
}
|
||||
)
|
||||
if doc is None:
|
||||
return {"status": "not found"}, 404
|
||||
if settings.VECTOR_STORE == "faiss":
|
||||
try:
|
||||
shutil.rmtree(os.path.join(current_dir, str(doc["_id"])))
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
else:
|
||||
vetorstore = VectorCreator.create_vectorstore(settings.VECTOR_STORE, source_id=str(doc["_id"]))
|
||||
vetorstore = VectorCreator.create_vectorstore(
|
||||
settings.VECTOR_STORE, source_id=str(doc["_id"])
|
||||
)
|
||||
vetorstore.delete_index()
|
||||
sources_collection.delete_one({
|
||||
"_id": ObjectId(source_id),
|
||||
})
|
||||
sources_collection.delete_one(
|
||||
{
|
||||
"_id": ObjectId(source_id),
|
||||
}
|
||||
)
|
||||
|
||||
return {"status": "ok"}
|
||||
|
||||
@@ -161,7 +203,9 @@ def upload_file():
|
||||
file.save(os.path.join(temp_dir, filename))
|
||||
|
||||
# Use shutil.make_archive to zip the temp directory
|
||||
zip_path = shutil.make_archive(base_name=os.path.join(save_dir, job_name), format="zip", root_dir=temp_dir)
|
||||
zip_path = shutil.make_archive(
|
||||
base_name=os.path.join(save_dir, job_name), format="zip", root_dir=temp_dir
|
||||
)
|
||||
final_filename = os.path.basename(zip_path)
|
||||
|
||||
# Clean up the temporary directory after zipping
|
||||
@@ -203,7 +247,9 @@ def upload_remote():
|
||||
source_data = request.form["data"]
|
||||
|
||||
if source_data:
|
||||
task = ingest_remote.delay(source_data=source_data, job_name=job_name, user=user, loader=source)
|
||||
task = ingest_remote.delay(
|
||||
source_data=source_data, job_name=job_name, user=user, loader=source
|
||||
)
|
||||
task_id = task.id
|
||||
return {"status": "ok", "task_id": task_id}
|
||||
else:
|
||||
@@ -248,7 +294,9 @@ def combined_json():
|
||||
"model": settings.EMBEDDINGS_NAME,
|
||||
"location": "local",
|
||||
"tokens": index["tokens"] if ("tokens" in index.keys()) else "",
|
||||
"retriever": index["retriever"] if ("retriever" in index.keys()) else "classic",
|
||||
"retriever": (
|
||||
index["retriever"] if ("retriever" in index.keys()) else "classic"
|
||||
),
|
||||
}
|
||||
)
|
||||
if "duckduck_search" in settings.RETRIEVERS_ENABLED:
|
||||
@@ -317,7 +365,9 @@ def get_prompts():
|
||||
list_prompts.append({"id": "creative", "name": "creative", "type": "public"})
|
||||
list_prompts.append({"id": "strict", "name": "strict", "type": "public"})
|
||||
for prompt in prompts:
|
||||
list_prompts.append({"id": str(prompt["_id"]), "name": prompt["name"], "type": "private"})
|
||||
list_prompts.append(
|
||||
{"id": str(prompt["_id"]), "name": prompt["name"], "type": "private"}
|
||||
)
|
||||
|
||||
return jsonify(list_prompts)
|
||||
|
||||
@@ -326,15 +376,21 @@ def get_prompts():
|
||||
def get_single_prompt():
|
||||
prompt_id = request.args.get("id")
|
||||
if prompt_id == "default":
|
||||
with open(os.path.join(current_dir, "prompts", "chat_combine_default.txt"), "r") as f:
|
||||
with open(
|
||||
os.path.join(current_dir, "prompts", "chat_combine_default.txt"), "r"
|
||||
) as f:
|
||||
chat_combine_template = f.read()
|
||||
return jsonify({"content": chat_combine_template})
|
||||
elif prompt_id == "creative":
|
||||
with open(os.path.join(current_dir, "prompts", "chat_combine_creative.txt"), "r") as f:
|
||||
with open(
|
||||
os.path.join(current_dir, "prompts", "chat_combine_creative.txt"), "r"
|
||||
) as f:
|
||||
chat_reduce_creative = f.read()
|
||||
return jsonify({"content": chat_reduce_creative})
|
||||
elif prompt_id == "strict":
|
||||
with open(os.path.join(current_dir, "prompts", "chat_combine_strict.txt"), "r") as f:
|
||||
with open(
|
||||
os.path.join(current_dir, "prompts", "chat_combine_strict.txt"), "r"
|
||||
) as f:
|
||||
chat_reduce_strict = f.read()
|
||||
return jsonify({"content": chat_reduce_strict})
|
||||
|
||||
@@ -363,7 +419,9 @@ def update_prompt_name():
|
||||
# check if name is null
|
||||
if name == "":
|
||||
return {"status": "error"}
|
||||
prompts_collection.update_one({"_id": ObjectId(id)}, {"$set": {"name": name, "content": content}})
|
||||
prompts_collection.update_one(
|
||||
{"_id": ObjectId(id)}, {"$set": {"name": name, "content": content}}
|
||||
)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@@ -373,7 +431,7 @@ def get_api_keys():
|
||||
keys = api_key_collection.find({"user": user})
|
||||
list_keys = []
|
||||
for key in keys:
|
||||
if "source" in key and isinstance(key["source"],DBRef):
|
||||
if "source" in key and isinstance(key["source"], DBRef):
|
||||
source = db.dereference(key["source"])
|
||||
if source is None:
|
||||
continue
|
||||
@@ -383,7 +441,7 @@ def get_api_keys():
|
||||
source_name = key["retriever"]
|
||||
else:
|
||||
continue
|
||||
|
||||
|
||||
list_keys.append(
|
||||
{
|
||||
"id": str(key["_id"]),
|
||||
@@ -443,8 +501,10 @@ def share_conversation():
|
||||
conversation_id = data["conversation_id"]
|
||||
isPromptable = request.args.get("isPromptable").lower() == "true"
|
||||
|
||||
conversation = conversations_collection.find_one({"_id": ObjectId(conversation_id)})
|
||||
if(conversation is None):
|
||||
conversation = conversations_collection.find_one(
|
||||
{"_id": ObjectId(conversation_id)}
|
||||
)
|
||||
if conversation is None:
|
||||
raise Exception("Conversation does not exist")
|
||||
current_n_queries = len(conversation["queries"])
|
||||
|
||||
@@ -456,24 +516,24 @@ def share_conversation():
|
||||
chunks = "2" if "chunks" not in data else data["chunks"]
|
||||
|
||||
name = conversation["name"] + "(shared)"
|
||||
new_api_key_data = {
|
||||
"prompt_id": prompt_id,
|
||||
"chunks": chunks,
|
||||
"user": user,
|
||||
}
|
||||
new_api_key_data = {
|
||||
"prompt_id": prompt_id,
|
||||
"chunks": chunks,
|
||||
"user": user,
|
||||
}
|
||||
if "source" in data and ObjectId.is_valid(data["source"]):
|
||||
new_api_key_data["source"] = DBRef("sources",ObjectId(data["source"]))
|
||||
new_api_key_data["source"] = DBRef("sources", ObjectId(data["source"]))
|
||||
elif "retriever" in data:
|
||||
new_api_key_data["retriever"] = data["retriever"]
|
||||
|
||||
pre_existing_api_document = api_key_collection.find_one(
|
||||
new_api_key_data
|
||||
)
|
||||
|
||||
pre_existing_api_document = api_key_collection.find_one(new_api_key_data)
|
||||
if pre_existing_api_document:
|
||||
api_uuid = pre_existing_api_document["key"]
|
||||
pre_existing = shared_conversations_collections.find_one(
|
||||
{
|
||||
"conversation_id": DBRef("conversations", ObjectId(conversation_id)),
|
||||
"conversation_id": DBRef(
|
||||
"conversations", ObjectId(conversation_id)
|
||||
),
|
||||
"isPromptable": isPromptable,
|
||||
"first_n_queries": current_n_queries,
|
||||
"user": user,
|
||||
@@ -504,33 +564,39 @@ def share_conversation():
|
||||
"api_key": api_uuid,
|
||||
}
|
||||
)
|
||||
return jsonify({"success": True, "identifier": str(explicit_binary.as_uuid())})
|
||||
return jsonify(
|
||||
{"success": True, "identifier": str(explicit_binary.as_uuid())}
|
||||
)
|
||||
else:
|
||||
|
||||
|
||||
api_uuid = str(uuid.uuid4())
|
||||
new_api_key_data["key"] = api_uuid
|
||||
new_api_key_data["name"] = name
|
||||
if "source" in data and ObjectId.is_valid(data["source"]):
|
||||
new_api_key_data["source"] = DBRef("sources", ObjectId(data["source"]))
|
||||
new_api_key_data["source"] = DBRef(
|
||||
"sources", ObjectId(data["source"])
|
||||
)
|
||||
if "retriever" in data:
|
||||
new_api_key_data["retriever"] = data["retriever"]
|
||||
api_key_collection.insert_one(new_api_key_data)
|
||||
shared_conversations_collections.insert_one(
|
||||
{
|
||||
"uuid": explicit_binary,
|
||||
"conversation_id": {
|
||||
"$ref": "conversations",
|
||||
"$id": ObjectId(conversation_id),
|
||||
},
|
||||
"isPromptable": isPromptable,
|
||||
"first_n_queries": current_n_queries,
|
||||
"user": user,
|
||||
"api_key": api_uuid,
|
||||
}
|
||||
)
|
||||
{
|
||||
"uuid": explicit_binary,
|
||||
"conversation_id": {
|
||||
"$ref": "conversations",
|
||||
"$id": ObjectId(conversation_id),
|
||||
},
|
||||
"isPromptable": isPromptable,
|
||||
"first_n_queries": current_n_queries,
|
||||
"user": user,
|
||||
"api_key": api_uuid,
|
||||
}
|
||||
)
|
||||
## Identifier as route parameter in frontend
|
||||
return (
|
||||
jsonify({"success": True, "identifier": str(explicit_binary.as_uuid())}),
|
||||
jsonify(
|
||||
{"success": True, "identifier": str(explicit_binary.as_uuid())}
|
||||
),
|
||||
201,
|
||||
)
|
||||
|
||||
@@ -545,7 +611,9 @@ def share_conversation():
|
||||
)
|
||||
if pre_existing is not None:
|
||||
return (
|
||||
jsonify({"success": True, "identifier": str(pre_existing["uuid"].as_uuid())}),
|
||||
jsonify(
|
||||
{"success": True, "identifier": str(pre_existing["uuid"].as_uuid())}
|
||||
),
|
||||
200,
|
||||
)
|
||||
else:
|
||||
@@ -563,7 +631,9 @@ def share_conversation():
|
||||
)
|
||||
## Identifier as route parameter in frontend
|
||||
return (
|
||||
jsonify({"success": True, "identifier": str(explicit_binary.as_uuid())}),
|
||||
jsonify(
|
||||
{"success": True, "identifier": str(explicit_binary.as_uuid())}
|
||||
),
|
||||
201,
|
||||
)
|
||||
except Exception as err:
|
||||
@@ -575,10 +645,16 @@ def share_conversation():
|
||||
@user.route("/api/shared_conversation/<string:identifier>", methods=["GET"])
|
||||
def get_publicly_shared_conversations(identifier: str):
|
||||
try:
|
||||
query_uuid = Binary.from_uuid(uuid.UUID(identifier), UuidRepresentation.STANDARD)
|
||||
query_uuid = Binary.from_uuid(
|
||||
uuid.UUID(identifier), UuidRepresentation.STANDARD
|
||||
)
|
||||
shared = shared_conversations_collections.find_one({"uuid": query_uuid})
|
||||
conversation_queries = []
|
||||
if shared and "conversation_id" in shared and isinstance(shared["conversation_id"], DBRef):
|
||||
if (
|
||||
shared
|
||||
and "conversation_id" in shared
|
||||
and isinstance(shared["conversation_id"], DBRef)
|
||||
):
|
||||
# Resolve the DBRef
|
||||
conversation_ref = shared["conversation_id"]
|
||||
conversation = db.dereference(conversation_ref)
|
||||
@@ -592,7 +668,9 @@ def get_publicly_shared_conversations(identifier: str):
|
||||
),
|
||||
404,
|
||||
)
|
||||
conversation_queries = conversation["queries"][: (shared["first_n_queries"])]
|
||||
conversation_queries = conversation["queries"][
|
||||
: (shared["first_n_queries"])
|
||||
]
|
||||
else:
|
||||
return (
|
||||
jsonify(
|
||||
@@ -616,3 +694,466 @@ def get_publicly_shared_conversations(identifier: str):
|
||||
except Exception as err:
|
||||
print(err)
|
||||
return jsonify({"success": False, "error": str(err)}), 400
|
||||
|
||||
|
||||
@user.route("/api/get_message_analytics", methods=["POST"])
|
||||
def get_message_analytics():
|
||||
data = request.get_json()
|
||||
api_key_id = data.get("api_key_id")
|
||||
filter_option = data.get("filter_option", "last_30_days")
|
||||
|
||||
try:
|
||||
api_key = (
|
||||
api_key_collection.find_one({"_id": ObjectId(api_key_id)})["key"]
|
||||
if api_key_id
|
||||
else None
|
||||
)
|
||||
except Exception as err:
|
||||
print(err)
|
||||
return jsonify({"success": False, "error": str(err)}), 400
|
||||
end_date = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
if filter_option == "last_hour":
|
||||
start_date = end_date - datetime.timedelta(hours=1)
|
||||
group_format = "%Y-%m-%d %H:%M:00"
|
||||
group_stage = {
|
||||
"$group": {
|
||||
"_id": {
|
||||
"minute": {
|
||||
"$dateToString": {"format": group_format, "date": "$date"}
|
||||
}
|
||||
},
|
||||
"total_messages": {"$sum": 1},
|
||||
}
|
||||
}
|
||||
|
||||
elif filter_option == "last_24_hour":
|
||||
start_date = end_date - datetime.timedelta(hours=24)
|
||||
group_format = "%Y-%m-%d %H:00"
|
||||
group_stage = {
|
||||
"$group": {
|
||||
"_id": {
|
||||
"hour": {"$dateToString": {"format": group_format, "date": "$date"}}
|
||||
},
|
||||
"total_messages": {"$sum": 1},
|
||||
}
|
||||
}
|
||||
|
||||
else:
|
||||
if filter_option in ["last_7_days", "last_15_days", "last_30_days"]:
|
||||
filter_days = (
|
||||
6
|
||||
if filter_option == "last_7_days"
|
||||
else (14 if filter_option == "last_15_days" else 29)
|
||||
)
|
||||
else:
|
||||
return jsonify({"success": False, "error": "Invalid option"}), 400
|
||||
start_date = end_date - datetime.timedelta(days=filter_days)
|
||||
start_date = start_date.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end_date = end_date.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||
group_format = "%Y-%m-%d"
|
||||
group_stage = {
|
||||
"$group": {
|
||||
"_id": {
|
||||
"day": {"$dateToString": {"format": group_format, "date": "$date"}}
|
||||
},
|
||||
"total_messages": {"$sum": 1},
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
match_stage = {
|
||||
"$match": {
|
||||
"date": {"$gte": start_date, "$lte": end_date},
|
||||
}
|
||||
}
|
||||
if api_key:
|
||||
match_stage["$match"]["api_key"] = api_key
|
||||
message_data = conversations_collection.aggregate(
|
||||
[
|
||||
match_stage,
|
||||
group_stage,
|
||||
{"$sort": {"_id": 1}},
|
||||
]
|
||||
)
|
||||
|
||||
if filter_option == "last_hour":
|
||||
intervals = generate_minute_range(start_date, end_date)
|
||||
elif filter_option == "last_24_hour":
|
||||
intervals = generate_hourly_range(start_date, end_date)
|
||||
else:
|
||||
intervals = generate_date_range(start_date, end_date)
|
||||
|
||||
daily_messages = {interval: 0 for interval in intervals}
|
||||
|
||||
for entry in message_data:
|
||||
if filter_option == "last_hour":
|
||||
daily_messages[entry["_id"]["minute"]] = entry["total_messages"]
|
||||
elif filter_option == "last_24_hour":
|
||||
daily_messages[entry["_id"]["hour"]] = entry["total_messages"]
|
||||
else:
|
||||
daily_messages[entry["_id"]["day"]] = entry["total_messages"]
|
||||
|
||||
except Exception as err:
|
||||
print(err)
|
||||
return jsonify({"success": False, "error": str(err)}), 400
|
||||
|
||||
return jsonify({"success": True, "messages": daily_messages}), 200
|
||||
|
||||
|
||||
@user.route("/api/get_token_analytics", methods=["POST"])
|
||||
def get_token_analytics():
|
||||
data = request.get_json()
|
||||
api_key_id = data.get("api_key_id")
|
||||
filter_option = data.get("filter_option", "last_30_days")
|
||||
|
||||
try:
|
||||
api_key = (
|
||||
api_key_collection.find_one({"_id": ObjectId(api_key_id)})["key"]
|
||||
if api_key_id
|
||||
else None
|
||||
)
|
||||
except Exception as err:
|
||||
print(err)
|
||||
return jsonify({"success": False, "error": str(err)}), 400
|
||||
end_date = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
if filter_option == "last_hour":
|
||||
start_date = end_date - datetime.timedelta(hours=1)
|
||||
group_format = "%Y-%m-%d %H:%M:00"
|
||||
group_stage = {
|
||||
"$group": {
|
||||
"_id": {
|
||||
"minute": {
|
||||
"$dateToString": {"format": group_format, "date": "$timestamp"}
|
||||
}
|
||||
},
|
||||
"total_tokens": {
|
||||
"$sum": {"$add": ["$prompt_tokens", "$generated_tokens"]}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
elif filter_option == "last_24_hour":
|
||||
start_date = end_date - datetime.timedelta(hours=24)
|
||||
group_format = "%Y-%m-%d %H:00"
|
||||
group_stage = {
|
||||
"$group": {
|
||||
"_id": {
|
||||
"hour": {
|
||||
"$dateToString": {"format": group_format, "date": "$timestamp"}
|
||||
}
|
||||
},
|
||||
"total_tokens": {
|
||||
"$sum": {"$add": ["$prompt_tokens", "$generated_tokens"]}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
else:
|
||||
if filter_option in ["last_7_days", "last_15_days", "last_30_days"]:
|
||||
filter_days = (
|
||||
6
|
||||
if filter_option == "last_7_days"
|
||||
else (14 if filter_option == "last_15_days" else 29)
|
||||
)
|
||||
else:
|
||||
return jsonify({"success": False, "error": "Invalid option"}), 400
|
||||
start_date = end_date - datetime.timedelta(days=filter_days)
|
||||
start_date = start_date.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end_date = end_date.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||
group_format = "%Y-%m-%d"
|
||||
group_stage = {
|
||||
"$group": {
|
||||
"_id": {
|
||||
"day": {
|
||||
"$dateToString": {"format": group_format, "date": "$timestamp"}
|
||||
}
|
||||
},
|
||||
"total_tokens": {
|
||||
"$sum": {"$add": ["$prompt_tokens", "$generated_tokens"]}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
match_stage = {
|
||||
"$match": {
|
||||
"timestamp": {"$gte": start_date, "$lte": end_date},
|
||||
}
|
||||
}
|
||||
if api_key:
|
||||
match_stage["$match"]["api_key"] = api_key
|
||||
|
||||
token_usage_data = token_usage_collection.aggregate(
|
||||
[
|
||||
match_stage,
|
||||
group_stage,
|
||||
{"$sort": {"_id": 1}},
|
||||
]
|
||||
)
|
||||
|
||||
if filter_option == "last_hour":
|
||||
intervals = generate_minute_range(start_date, end_date)
|
||||
elif filter_option == "last_24_hour":
|
||||
intervals = generate_hourly_range(start_date, end_date)
|
||||
else:
|
||||
intervals = generate_date_range(start_date, end_date)
|
||||
|
||||
daily_token_usage = {interval: 0 for interval in intervals}
|
||||
|
||||
for entry in token_usage_data:
|
||||
if filter_option == "last_hour":
|
||||
daily_token_usage[entry["_id"]["minute"]] = entry["total_tokens"]
|
||||
elif filter_option == "last_24_hour":
|
||||
daily_token_usage[entry["_id"]["hour"]] = entry["total_tokens"]
|
||||
else:
|
||||
daily_token_usage[entry["_id"]["day"]] = entry["total_tokens"]
|
||||
|
||||
except Exception as err:
|
||||
print(err)
|
||||
return jsonify({"success": False, "error": str(err)}), 400
|
||||
|
||||
return jsonify({"success": True, "token_usage": daily_token_usage}), 200
|
||||
|
||||
|
||||
@user.route("/api/get_feedback_analytics", methods=["POST"])
|
||||
def get_feedback_analytics():
|
||||
data = request.get_json()
|
||||
api_key_id = data.get("api_key_id")
|
||||
filter_option = data.get("filter_option", "last_30_days")
|
||||
|
||||
try:
|
||||
api_key = (
|
||||
api_key_collection.find_one({"_id": ObjectId(api_key_id)})["key"]
|
||||
if api_key_id
|
||||
else None
|
||||
)
|
||||
except Exception as err:
|
||||
print(err)
|
||||
return jsonify({"success": False, "error": str(err)}), 400
|
||||
end_date = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
if filter_option == "last_hour":
|
||||
start_date = end_date - datetime.timedelta(hours=1)
|
||||
group_format = "%Y-%m-%d %H:%M:00"
|
||||
group_stage_1 = {
|
||||
"$group": {
|
||||
"_id": {
|
||||
"minute": {
|
||||
"$dateToString": {"format": group_format, "date": "$timestamp"}
|
||||
},
|
||||
"feedback": "$feedback",
|
||||
},
|
||||
"count": {"$sum": 1},
|
||||
}
|
||||
}
|
||||
group_stage_2 = {
|
||||
"$group": {
|
||||
"_id": "$_id.minute",
|
||||
"likes": {
|
||||
"$sum": {
|
||||
"$cond": [
|
||||
{"$eq": ["$_id.feedback", "LIKE"]},
|
||||
"$count",
|
||||
0,
|
||||
]
|
||||
}
|
||||
},
|
||||
"dislikes": {
|
||||
"$sum": {
|
||||
"$cond": [
|
||||
{"$eq": ["$_id.feedback", "DISLIKE"]},
|
||||
"$count",
|
||||
0,
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
elif filter_option == "last_24_hour":
|
||||
start_date = end_date - datetime.timedelta(hours=24)
|
||||
group_format = "%Y-%m-%d %H:00"
|
||||
group_stage_1 = {
|
||||
"$group": {
|
||||
"_id": {
|
||||
"hour": {
|
||||
"$dateToString": {"format": group_format, "date": "$timestamp"}
|
||||
},
|
||||
"feedback": "$feedback",
|
||||
},
|
||||
"count": {"$sum": 1},
|
||||
}
|
||||
}
|
||||
group_stage_2 = {
|
||||
"$group": {
|
||||
"_id": "$_id.hour",
|
||||
"likes": {
|
||||
"$sum": {
|
||||
"$cond": [
|
||||
{"$eq": ["$_id.feedback", "LIKE"]},
|
||||
"$count",
|
||||
0,
|
||||
]
|
||||
}
|
||||
},
|
||||
"dislikes": {
|
||||
"$sum": {
|
||||
"$cond": [
|
||||
{"$eq": ["$_id.feedback", "DISLIKE"]},
|
||||
"$count",
|
||||
0,
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
else:
|
||||
if filter_option in ["last_7_days", "last_15_days", "last_30_days"]:
|
||||
filter_days = (
|
||||
6
|
||||
if filter_option == "last_7_days"
|
||||
else (14 if filter_option == "last_15_days" else 29)
|
||||
)
|
||||
else:
|
||||
return jsonify({"success": False, "error": "Invalid option"}), 400
|
||||
start_date = end_date - datetime.timedelta(days=filter_days)
|
||||
start_date = start_date.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end_date = end_date.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||
group_format = "%Y-%m-%d"
|
||||
group_stage_1 = {
|
||||
"$group": {
|
||||
"_id": {
|
||||
"day": {
|
||||
"$dateToString": {"format": group_format, "date": "$timestamp"}
|
||||
},
|
||||
"feedback": "$feedback",
|
||||
},
|
||||
"count": {"$sum": 1},
|
||||
}
|
||||
}
|
||||
group_stage_2 = {
|
||||
"$group": {
|
||||
"_id": "$_id.day",
|
||||
"likes": {
|
||||
"$sum": {
|
||||
"$cond": [
|
||||
{"$eq": ["$_id.feedback", "LIKE"]},
|
||||
"$count",
|
||||
0,
|
||||
]
|
||||
}
|
||||
},
|
||||
"dislikes": {
|
||||
"$sum": {
|
||||
"$cond": [
|
||||
{"$eq": ["$_id.feedback", "DISLIKE"]},
|
||||
"$count",
|
||||
0,
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
match_stage = {
|
||||
"$match": {
|
||||
"timestamp": {"$gte": start_date, "$lte": end_date},
|
||||
}
|
||||
}
|
||||
if api_key:
|
||||
match_stage["$match"]["api_key"] = api_key
|
||||
|
||||
feedback_data = feedback_collection.aggregate(
|
||||
[
|
||||
match_stage,
|
||||
group_stage_1,
|
||||
group_stage_2,
|
||||
{"$sort": {"_id": 1}},
|
||||
]
|
||||
)
|
||||
|
||||
if filter_option == "last_hour":
|
||||
intervals = generate_minute_range(start_date, end_date)
|
||||
elif filter_option == "last_24_hour":
|
||||
intervals = generate_hourly_range(start_date, end_date)
|
||||
else:
|
||||
intervals = generate_date_range(start_date, end_date)
|
||||
|
||||
daily_feedback = {
|
||||
interval: {"positive": 0, "negative": 0} for interval in intervals
|
||||
}
|
||||
|
||||
for entry in feedback_data:
|
||||
daily_feedback[entry["_id"]] = {
|
||||
"positive": entry["likes"],
|
||||
"negative": entry["dislikes"],
|
||||
}
|
||||
|
||||
except Exception as err:
|
||||
print(err)
|
||||
return jsonify({"success": False, "error": str(err)}), 400
|
||||
|
||||
return jsonify({"success": True, "feedback": daily_feedback}), 200
|
||||
|
||||
|
||||
@user.route("/api/get_user_logs", methods=["POST"])
|
||||
def get_user_logs():
|
||||
data = request.get_json()
|
||||
page = int(data.get("page", 1))
|
||||
api_key_id = data.get("api_key_id")
|
||||
page_size = int(data.get("page_size", 10))
|
||||
skip = (page - 1) * page_size
|
||||
|
||||
try:
|
||||
api_key = (
|
||||
api_key_collection.find_one({"_id": ObjectId(api_key_id)})["key"]
|
||||
if api_key_id
|
||||
else None
|
||||
)
|
||||
except Exception as err:
|
||||
print(err)
|
||||
return jsonify({"success": False, "error": str(err)}), 400
|
||||
|
||||
query = {}
|
||||
if api_key:
|
||||
query = {"api_key": api_key}
|
||||
items_cursor = (
|
||||
user_logs_collection.find(query)
|
||||
.sort("timestamp", -1)
|
||||
.skip(skip)
|
||||
.limit(page_size + 1)
|
||||
)
|
||||
items = list(items_cursor)
|
||||
|
||||
results = []
|
||||
for item in items[:page_size]:
|
||||
results.append(
|
||||
{
|
||||
"id": str(item.get("_id")),
|
||||
"action": item.get("action"),
|
||||
"level": item.get("level"),
|
||||
"user": item.get("user"),
|
||||
"question": item.get("question"),
|
||||
"sources": item.get("sources"),
|
||||
"retriever_params": item.get("retriever_params"),
|
||||
"timestamp": item.get("timestamp"),
|
||||
}
|
||||
)
|
||||
has_more = len(items) > page_size
|
||||
|
||||
return (
|
||||
jsonify(
|
||||
{
|
||||
"success": True,
|
||||
"logs": results,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"has_more": has_more,
|
||||
}
|
||||
),
|
||||
200,
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ EbookLib==0.18
|
||||
elasticsearch==8.14.0
|
||||
escodegen==1.0.11
|
||||
esprima==4.0.1
|
||||
Flask==3.0.1
|
||||
Flask==3.0.3
|
||||
faiss-cpu==1.8.0.post1
|
||||
gunicorn==23.0.0
|
||||
html2text==2020.1.16
|
||||
@@ -31,6 +31,6 @@ retry==0.9.2
|
||||
sentence-transformers==3.0.1
|
||||
tiktoken==0.7.0
|
||||
torch
|
||||
tqdm==4.66.3
|
||||
transformers==4.44.0
|
||||
Werkzeug==3.0.3
|
||||
tqdm==4.66.5
|
||||
transformers==4.44.2
|
||||
Werkzeug==3.0.4
|
||||
|
||||
@@ -12,3 +12,7 @@ class BaseRetriever(ABC):
|
||||
@abstractmethod
|
||||
def search(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_params(self):
|
||||
pass
|
||||
|
||||
@@ -101,3 +101,15 @@ class BraveRetSearch(BaseRetriever):
|
||||
|
||||
def search(self):
|
||||
return self._get_data()
|
||||
|
||||
def get_params(self):
|
||||
return {
|
||||
"question": self.question,
|
||||
"source": self.source,
|
||||
"chat_history": self.chat_history,
|
||||
"prompt": self.prompt,
|
||||
"chunks": self.chunks,
|
||||
"token_limit": self.token_limit,
|
||||
"gpt_model": self.gpt_model,
|
||||
"user_api_key": self.user_api_key
|
||||
}
|
||||
|
||||
@@ -45,13 +45,12 @@ class ClassicRAG(BaseRetriever):
|
||||
settings.VECTOR_STORE, self.vectorstore, settings.EMBEDDINGS_KEY
|
||||
)
|
||||
docs_temp = docsearch.search(self.question, k=self.chunks)
|
||||
print(docs_temp)
|
||||
docs = [
|
||||
{
|
||||
"title": (
|
||||
i.metadata["title"].split("/")[-1]
|
||||
if i.metadata
|
||||
else i.page_content
|
||||
),
|
||||
"title": i.metadata.get(
|
||||
"title", i.metadata.get("post_title", i.page_content)
|
||||
).split("/")[-1],
|
||||
"text": i.page_content,
|
||||
"source": (
|
||||
i.metadata.get("source")
|
||||
@@ -105,3 +104,15 @@ class ClassicRAG(BaseRetriever):
|
||||
|
||||
def search(self):
|
||||
return self._get_data()
|
||||
|
||||
def get_params(self):
|
||||
return {
|
||||
"question": self.question,
|
||||
"source": self.vectorstore,
|
||||
"chat_history": self.chat_history,
|
||||
"prompt": self.prompt,
|
||||
"chunks": self.chunks,
|
||||
"token_limit": self.token_limit,
|
||||
"gpt_model": self.gpt_model,
|
||||
"user_api_key": self.user_api_key
|
||||
}
|
||||
|
||||
@@ -118,3 +118,15 @@ class DuckDuckSearch(BaseRetriever):
|
||||
|
||||
def search(self):
|
||||
return self._get_data()
|
||||
|
||||
def get_params(self):
|
||||
return {
|
||||
"question": self.question,
|
||||
"source": self.source,
|
||||
"chat_history": self.chat_history,
|
||||
"prompt": self.prompt,
|
||||
"chunks": self.chunks,
|
||||
"token_limit": self.token_limit,
|
||||
"gpt_model": self.gpt_model,
|
||||
"user_api_key": self.user_api_key
|
||||
}
|
||||
|
||||
28
frontend/package-lock.json
generated
28
frontend/package-lock.json
generated
@@ -14,6 +14,7 @@
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dropzone": "^14.2.3",
|
||||
@@ -858,6 +859,12 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@kurkle/color": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
|
||||
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -2503,6 +2510,18 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "4.4.4",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.4.tgz",
|
||||
"integrity": "sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"pnpm": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
@@ -7253,6 +7272,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-chartjs-2": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz",
|
||||
"integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==",
|
||||
"peerDependencies": {
|
||||
"chart.js": "^4.1.1",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-copy-to-clipboard": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dropzone": "^14.2.3",
|
||||
|
||||
BIN
frontend/public/fonts/IBMPlexMono-Medium.ttf
Normal file
BIN
frontend/public/fonts/IBMPlexMono-Medium.ttf
Normal file
Binary file not shown.
@@ -12,6 +12,10 @@ const endpoints = {
|
||||
SINGLE_PROMPT: (id: string) => `/api/get_single_prompt?id=${id}`,
|
||||
DELETE_PATH: (docPath: string) => `/api/delete_old?source_id=${docPath}`,
|
||||
TASK_STATUS: (task_id: string) => `/api/task_status?task_id=${task_id}`,
|
||||
MESSAGE_ANALYTICS: '/api/get_message_analytics',
|
||||
TOKEN_ANALYTICS: '/api/get_token_analytics',
|
||||
FEEDBACK_ANALYTICS: '/api/get_feedback_analytics',
|
||||
LOGS: `/api/get_user_logs`,
|
||||
},
|
||||
CONVERSATION: {
|
||||
ANSWER: '/api/answer',
|
||||
|
||||
@@ -23,6 +23,14 @@ const userService = {
|
||||
apiClient.get(endpoints.USER.DELETE_PATH(docPath)),
|
||||
getTaskStatus: (task_id: string): Promise<any> =>
|
||||
apiClient.get(endpoints.USER.TASK_STATUS(task_id)),
|
||||
getMessageAnalytics: (data: any): Promise<any> =>
|
||||
apiClient.post(endpoints.USER.MESSAGE_ANALYTICS, data),
|
||||
getTokenAnalytics: (data: any): Promise<any> =>
|
||||
apiClient.post(endpoints.USER.TOKEN_ANALYTICS, data),
|
||||
getFeedbackAnalytics: (data: any): Promise<any> =>
|
||||
apiClient.post(endpoints.USER.FEEDBACK_ANALYTICS, data),
|
||||
getLogs: (data: any): Promise<any> =>
|
||||
apiClient.post(endpoints.USER.LOGS, data),
|
||||
};
|
||||
|
||||
export default userService;
|
||||
|
||||
3
frontend/src/assets/chevron-right.svg
Normal file
3
frontend/src/assets/chevron-right.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.29154 4.88202L1.70154 0.29202C1.60896 0.199438 1.49905 0.125998 1.37808 0.0758932C1.25712 0.0257882 1.12747 -2.37536e-07 0.99654 -2.44235e-07C0.86561 -2.50934e-07 0.735961 0.0257882 0.614997 0.0758931C0.494033 0.125998 0.384122 0.199438 0.29154 0.29202C0.198958 0.384602 0.125519 0.494513 0.0754137 0.615477C0.0253086 0.736441 -0.00048069 0.86609 -0.000480695 0.99702C-0.000480701 1.12795 0.0253086 1.2576 0.0754136 1.37856C0.125519 1.49953 0.198958 1.60944 0.29154 1.70202L4.17154 5.59202L0.29154 9.47202C0.198958 9.5646 0.125518 9.67451 0.0754133 9.79548C0.0253082 9.91644 -0.000481091 10.0461 -0.000481097 10.177C-0.000481102 10.3079 0.0253082 10.4376 0.0754132 10.5586C0.125518 10.6795 0.198958 10.7894 0.29154 10.882C0.384121 10.9746 0.494032 11.048 0.614996 11.0981C0.73596 11.1483 0.865609 11.174 0.99654 11.174C1.12747 11.174 1.25712 11.1483 1.37808 11.0981C1.49905 11.048 1.60896 10.9746 1.70154 10.882L6.29154 6.29202C6.38424 6.19951 6.45779 6.08962 6.50797 5.96864C6.55815 5.84767 6.58398 5.71799 6.58398 5.58702C6.58398 5.45605 6.55815 5.32637 6.50797 5.2054C6.45779 5.08442 6.38424 4.97453 6.29154 4.88202Z" fill="#666666"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,16 +1,24 @@
|
||||
import { useState } from 'react';
|
||||
import Copy from './../assets/copy.svg?react';
|
||||
import CheckMark from './../assets/checkmark.svg?react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function CoppyButton({ text }: { text: string }) {
|
||||
import CheckMark from '../assets/checkmark.svg?react';
|
||||
import Copy from '../assets/copy.svg?react';
|
||||
|
||||
export default function CoppyButton({
|
||||
text,
|
||||
colorLight,
|
||||
colorDark,
|
||||
}: {
|
||||
text: string;
|
||||
colorLight?: string;
|
||||
colorDark?: string;
|
||||
}) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [isCopyHovered, setIsCopyHovered] = useState(false);
|
||||
|
||||
const handleCopyClick = (text: string) => {
|
||||
copy(text);
|
||||
setCopied(true);
|
||||
// Reset copied to false after a few seconds
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 3000);
|
||||
@@ -20,8 +28,8 @@ export default function CoppyButton({ text }: { text: string }) {
|
||||
<div
|
||||
className={`flex items-center justify-center rounded-full p-2 ${
|
||||
isCopyHovered
|
||||
? 'bg-[#EEEEEE] dark:bg-purple-taupe'
|
||||
: 'bg-[#ffffff] dark:bg-transparent'
|
||||
? `bg-[#EEEEEE] dark:bg-purple-taupe`
|
||||
: `bg-[${colorLight ? colorLight : '#FFFFFF'}] dark:bg-[${colorDark ? colorDark : 'transparent'}]`
|
||||
}`}
|
||||
>
|
||||
{copied ? (
|
||||
|
||||
@@ -16,6 +16,7 @@ function Dropdown({
|
||||
showDelete,
|
||||
onDelete,
|
||||
placeholder,
|
||||
contentSize = 'text-base',
|
||||
}: {
|
||||
options:
|
||||
| string[]
|
||||
@@ -42,6 +43,7 @@ function Dropdown({
|
||||
showDelete?: boolean;
|
||||
onDelete?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
contentSize?: string;
|
||||
}) {
|
||||
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
@@ -85,9 +87,9 @@ function Dropdown({
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className={`overflow-hidden text-ellipsis dark:text-bright-gray ${
|
||||
className={`truncate overflow-hidden dark:text-bright-gray ${
|
||||
!selectedValue && 'text-silver dark:text-gray-400'
|
||||
}`}
|
||||
} ${contentSize}`}
|
||||
>
|
||||
{selectedValue && 'label' in selectedValue
|
||||
? selectedValue.label
|
||||
@@ -124,7 +126,7 @@ function Dropdown({
|
||||
onSelect(option);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className="ml-5 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap py-3 dark:text-light-gray"
|
||||
className={`ml-5 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap py-3 dark:text-light-gray ${contentSize}`}
|
||||
>
|
||||
{typeof option === 'string'
|
||||
? option
|
||||
|
||||
@@ -10,6 +10,7 @@ const Input = ({
|
||||
maxLength,
|
||||
className,
|
||||
colorVariant = 'silver',
|
||||
borderVariant = 'thick',
|
||||
children,
|
||||
onChange,
|
||||
onPaste,
|
||||
@@ -20,10 +21,13 @@ const Input = ({
|
||||
jet: 'border-jet',
|
||||
gray: 'border-gray-5000 dark:text-silver',
|
||||
};
|
||||
|
||||
const borderStyles = {
|
||||
thin: 'border',
|
||||
thick: 'border-2',
|
||||
};
|
||||
return (
|
||||
<input
|
||||
className={`h-[42px] w-full rounded-full border-2 px-3 outline-none dark:bg-transparent dark:text-white ${className} ${colorStyles[colorVariant]}`}
|
||||
className={`h-[42px] w-full rounded-full px-3 py-1 outline-none dark:bg-transparent dark:text-white ${className} ${colorStyles[colorVariant]} ${borderStyles[borderVariant]}`}
|
||||
type={type}
|
||||
id={id}
|
||||
name={name}
|
||||
|
||||
@@ -2,6 +2,7 @@ export type InputProps = {
|
||||
type: 'text' | 'number';
|
||||
value: string | string[] | number;
|
||||
colorVariant?: 'silver' | 'jet' | 'gray';
|
||||
borderVariant?: 'thin' | 'thick';
|
||||
isAutoFocused?: boolean;
|
||||
id?: string;
|
||||
maxLength?: number;
|
||||
|
||||
@@ -180,7 +180,7 @@ const ConversationBubble = forwardRef<
|
||||
))}
|
||||
{(sources?.length ?? 0) > 3 && (
|
||||
<div
|
||||
className="flex h-24 cursor-pointer flex-col-reverse rounded-[20px] bg-gray-1000 p-4 text-purple-30 hover:bg-[#F1F1F1] hover:text-[#6D3ECC] dark:bg-gun-metal dark:hover:bg-[#2C2E3C] dark:hover:text-[#8C67D7]"
|
||||
className="flex h-28 cursor-pointer flex-col-reverse rounded-[20px] bg-gray-1000 p-4 text-purple-30 hover:bg-[#F1F1F1] hover:text-[#6D3ECC] dark:bg-gun-metal dark:hover:bg-[#2C2E3C] dark:hover:text-[#8C67D7]"
|
||||
onClick={() => setIsSidebarOpen(true)}
|
||||
>
|
||||
<p className="ellipsis-text h-22 text-xs">{`View ${
|
||||
|
||||
@@ -420,10 +420,36 @@ template {
|
||||
src: url('/fonts/Inter-Variable.ttf');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'IBMPlexMono-Medium';
|
||||
font-weight: 500;
|
||||
src: url('/fonts/IBMPlexMono-Medium.ttf');
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
input:-webkit-autofill {
|
||||
-webkit-box-shadow: 0 0 0 50px white inset;
|
||||
}
|
||||
|
||||
input:-webkit-autofill:focus {
|
||||
-webkit-box-shadow: 0 0 0 50px white inset;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
input:-webkit-autofill {
|
||||
-webkit-box-shadow: 0 0 0 50px rgb(68, 70, 84) inset;
|
||||
-webkit-text-fill-color: white;
|
||||
}
|
||||
|
||||
input:-webkit-autofill:focus {
|
||||
-webkit-box-shadow: 0 0 0 50px rgb(68, 70, 84) inset;
|
||||
-webkit-text-fill-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.inputbox-style {
|
||||
resize: none;
|
||||
padding-left: 36px;
|
||||
@@ -441,3 +467,7 @@ template {
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.logs-table {
|
||||
font-family: 'IBMPlexMono-Medium', system-ui;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,12 @@
|
||||
"key": "API Key",
|
||||
"sourceDoc": "Source Document",
|
||||
"createNew": "Create New"
|
||||
},
|
||||
"analytics": {
|
||||
"label": "Analytics"
|
||||
},
|
||||
"logs": {
|
||||
"label": "Logs"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
|
||||
@@ -62,6 +62,12 @@
|
||||
"key": "Clave de API",
|
||||
"sourceDoc": "Documento Fuente",
|
||||
"createNew": "Crear Nuevo"
|
||||
},
|
||||
"analytics": {
|
||||
"label": "Analítica"
|
||||
},
|
||||
"logs": {
|
||||
"label": "Registros"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
|
||||
@@ -62,6 +62,12 @@
|
||||
"key": "APIキー",
|
||||
"sourceDoc": "ソースドキュメント",
|
||||
"createNew": "新規作成"
|
||||
},
|
||||
"analytics": {
|
||||
"label": "分析"
|
||||
},
|
||||
"logs": {
|
||||
"label": "ログ"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
|
||||
@@ -62,6 +62,12 @@
|
||||
"key": "API 密钥",
|
||||
"sourceDoc": "源文档",
|
||||
"createNew": "创建新的"
|
||||
},
|
||||
"analytics": {
|
||||
"label": "分析"
|
||||
},
|
||||
"logs": {
|
||||
"label": "日志"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
|
||||
@@ -5,15 +5,14 @@ import userService from '../api/services/userService';
|
||||
import Trash from '../assets/trash.svg';
|
||||
import CreateAPIKeyModal from '../modals/CreateAPIKeyModal';
|
||||
import SaveAPIKeyModal from '../modals/SaveAPIKeyModal';
|
||||
import { APIKeyData } from './types';
|
||||
|
||||
export default function APIKeys() {
|
||||
const { t } = useTranslation();
|
||||
const [isCreateModalOpen, setCreateModal] = React.useState(false);
|
||||
const [isSaveKeyModalOpen, setSaveKeyModal] = React.useState(false);
|
||||
const [newKey, setNewKey] = React.useState('');
|
||||
const [apiKeys, setApiKeys] = React.useState<
|
||||
{ name: string; key: string; source: string; id: string }[]
|
||||
>([]);
|
||||
const [apiKeys, setApiKeys] = React.useState<APIKeyData[]>([]);
|
||||
|
||||
const handleFetchKeys = async () => {
|
||||
try {
|
||||
|
||||
390
frontend/src/settings/Analytics.tsx
Normal file
390
frontend/src/settings/Analytics.tsx
Normal file
@@ -0,0 +1,390 @@
|
||||
import {
|
||||
BarElement,
|
||||
CategoryScale,
|
||||
Chart as ChartJS,
|
||||
Legend,
|
||||
LinearScale,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from 'chart.js';
|
||||
import React from 'react';
|
||||
import { Bar } from 'react-chartjs-2';
|
||||
|
||||
import userService from '../api/services/userService';
|
||||
import Dropdown from '../components/Dropdown';
|
||||
import { htmlLegendPlugin } from '../utils/chartUtils';
|
||||
import { formatDate } from '../utils/dateTimeUtils';
|
||||
import { APIKeyData } from './types';
|
||||
|
||||
import type { ChartData } from 'chart.js';
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
BarElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
);
|
||||
|
||||
const filterOptions = [
|
||||
{ label: 'Hour', value: 'last_hour' },
|
||||
{ label: '24 Hours', value: 'last_24_hour' },
|
||||
{ label: '7 Days', value: 'last_7_days' },
|
||||
{ label: '15 Days', value: 'last_15_days' },
|
||||
{ label: '30 Days', value: 'last_30_days' },
|
||||
];
|
||||
|
||||
export default function Analytics() {
|
||||
const [messagesData, setMessagesData] = React.useState<Record<
|
||||
string,
|
||||
number
|
||||
> | null>(null);
|
||||
const [tokenUsageData, setTokenUsageData] = React.useState<Record<
|
||||
string,
|
||||
number
|
||||
> | null>(null);
|
||||
const [feedbackData, setFeedbackData] = React.useState<Record<
|
||||
string,
|
||||
{
|
||||
positive: number;
|
||||
negative: number;
|
||||
}
|
||||
> | null>(null);
|
||||
const [chatbots, setChatbots] = React.useState<APIKeyData[]>([]);
|
||||
const [selectedChatbot, setSelectedChatbot] =
|
||||
React.useState<APIKeyData | null>();
|
||||
const [messagesFilter, setMessagesFilter] = React.useState<{
|
||||
label: string;
|
||||
value: string;
|
||||
}>({ label: '30 Days', value: 'last_30_days' });
|
||||
const [tokenUsageFilter, setTokenUsageFilter] = React.useState<{
|
||||
label: string;
|
||||
value: string;
|
||||
}>({ label: '30 Days', value: 'last_30_days' });
|
||||
const [feedbackFilter, setFeedbackFilter] = React.useState<{
|
||||
label: string;
|
||||
value: string;
|
||||
}>({ label: '30 Days', value: 'last_30_days' });
|
||||
|
||||
const fetchChatbots = async () => {
|
||||
try {
|
||||
const response = await userService.getAPIKeys();
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch Chatbots');
|
||||
}
|
||||
const chatbots = await response.json();
|
||||
setChatbots(chatbots);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMessagesData = async (chatbot_id?: string, filter?: string) => {
|
||||
try {
|
||||
const response = await userService.getMessageAnalytics({
|
||||
api_key_id: chatbot_id,
|
||||
filter_option: filter,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch analytics data');
|
||||
}
|
||||
const data = await response.json();
|
||||
setMessagesData(data.messages);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTokenData = async (chatbot_id?: string, filter?: string) => {
|
||||
try {
|
||||
const response = await userService.getTokenAnalytics({
|
||||
api_key_id: chatbot_id,
|
||||
filter_option: filter,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch analytics data');
|
||||
}
|
||||
const data = await response.json();
|
||||
setTokenUsageData(data.token_usage);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchFeedbackData = async (chatbot_id?: string, filter?: string) => {
|
||||
try {
|
||||
const response = await userService.getFeedbackAnalytics({
|
||||
api_key_id: chatbot_id,
|
||||
filter_option: filter,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch analytics data');
|
||||
}
|
||||
const data = await response.json();
|
||||
setFeedbackData(data.feedback);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchChatbots();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
const id = selectedChatbot?.id;
|
||||
const filter = messagesFilter;
|
||||
fetchMessagesData(id, filter?.value);
|
||||
}, [selectedChatbot, messagesFilter]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const id = selectedChatbot?.id;
|
||||
const filter = tokenUsageFilter;
|
||||
fetchTokenData(id, filter?.value);
|
||||
}, [selectedChatbot, tokenUsageFilter]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const id = selectedChatbot?.id;
|
||||
const filter = feedbackFilter;
|
||||
fetchFeedbackData(id, filter?.value);
|
||||
}, [selectedChatbot, feedbackFilter]);
|
||||
return (
|
||||
<div className="mt-12">
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="font-bold text-jet dark:text-bright-gray">
|
||||
Filter by chatbot
|
||||
</p>
|
||||
<Dropdown
|
||||
size="w-[55vw] sm:w-[360px]"
|
||||
options={[
|
||||
...chatbots.map((chatbot) => ({
|
||||
label: chatbot.name,
|
||||
value: chatbot.id,
|
||||
})),
|
||||
{ label: 'None', value: '' },
|
||||
]}
|
||||
placeholder="Select chatbot"
|
||||
onSelect={(chatbot: { label: string; value: string }) => {
|
||||
setSelectedChatbot(
|
||||
chatbots.find((item) => item.id === chatbot.value),
|
||||
);
|
||||
}}
|
||||
selectedValue={
|
||||
(selectedChatbot && {
|
||||
label: selectedChatbot.name,
|
||||
value: selectedChatbot.id,
|
||||
}) ||
|
||||
null
|
||||
}
|
||||
rounded="3xl"
|
||||
border="border"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-8 w-full flex flex-col sm:flex-row gap-3">
|
||||
<div className="h-[345px] sm:w-1/2 w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40">
|
||||
<div className="flex flex-row items-center justify-start gap-3">
|
||||
<p className="font-bold text-jet dark:text-bright-gray">
|
||||
Messages
|
||||
</p>
|
||||
<Dropdown
|
||||
size="w-[125px]"
|
||||
options={filterOptions}
|
||||
placeholder="Filter"
|
||||
onSelect={(selectedOption: {
|
||||
label: string;
|
||||
value: string;
|
||||
}) => {
|
||||
setMessagesFilter(selectedOption);
|
||||
}}
|
||||
selectedValue={messagesFilter ?? null}
|
||||
rounded="3xl"
|
||||
border="border"
|
||||
contentSize="text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-px relative h-[245px] w-full">
|
||||
<div
|
||||
id="legend-container-1"
|
||||
className="flex flex-row items-center justify-end"
|
||||
></div>
|
||||
<AnalyticsChart
|
||||
data={{
|
||||
labels: Object.keys(messagesData || {}).map((item) =>
|
||||
formatDate(item),
|
||||
),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Messages',
|
||||
data: Object.values(messagesData || {}),
|
||||
backgroundColor: '#7D54D1',
|
||||
},
|
||||
],
|
||||
}}
|
||||
legendID="legend-container-1"
|
||||
maxTicksLimitInX={8}
|
||||
isStacked={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-[345px] sm:w-1/2 w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40">
|
||||
<div className="flex flex-row items-center justify-start gap-3">
|
||||
<p className="font-bold text-jet dark:text-bright-gray">
|
||||
Token Usage
|
||||
</p>
|
||||
<Dropdown
|
||||
size="w-[125px]"
|
||||
options={filterOptions}
|
||||
placeholder="Filter"
|
||||
onSelect={(selectedOption: {
|
||||
label: string;
|
||||
value: string;
|
||||
}) => {
|
||||
setTokenUsageFilter(selectedOption);
|
||||
}}
|
||||
selectedValue={tokenUsageFilter ?? null}
|
||||
rounded="3xl"
|
||||
border="border"
|
||||
contentSize="text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-px relative h-[245px] w-full">
|
||||
<div
|
||||
id="legend-container-2"
|
||||
className="flex flex-row items-center justify-end"
|
||||
></div>
|
||||
<AnalyticsChart
|
||||
data={{
|
||||
labels: Object.keys(tokenUsageData || {}).map((item) =>
|
||||
formatDate(item),
|
||||
),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Tokens',
|
||||
data: Object.values(tokenUsageData || {}),
|
||||
backgroundColor: '#7D54D1',
|
||||
},
|
||||
],
|
||||
}}
|
||||
legendID="legend-container-2"
|
||||
maxTicksLimitInX={8}
|
||||
isStacked={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 w-full">
|
||||
<div className="h-[345px] w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40">
|
||||
<div className="flex flex-row items-center justify-start gap-3">
|
||||
<p className="font-bold text-jet dark:text-bright-gray">
|
||||
User Feedback
|
||||
</p>
|
||||
<Dropdown
|
||||
size="w-[125px]"
|
||||
options={filterOptions}
|
||||
placeholder="Filter"
|
||||
onSelect={(selectedOption: {
|
||||
label: string;
|
||||
value: string;
|
||||
}) => {
|
||||
setFeedbackFilter(selectedOption);
|
||||
}}
|
||||
selectedValue={feedbackFilter ?? null}
|
||||
rounded="3xl"
|
||||
border="border"
|
||||
contentSize="text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-px relative h-[245px] w-full">
|
||||
<div
|
||||
id="legend-container-3"
|
||||
className="flex flex-row items-center justify-end"
|
||||
></div>
|
||||
<AnalyticsChart
|
||||
data={{
|
||||
labels: Object.keys(feedbackData || {}).map((item) =>
|
||||
formatDate(item),
|
||||
),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Positive',
|
||||
data: Object.values(feedbackData || {}).map(
|
||||
(item) => item.positive,
|
||||
),
|
||||
backgroundColor: '#8BD154',
|
||||
},
|
||||
{
|
||||
label: 'Negative',
|
||||
data: Object.values(feedbackData || {}).map(
|
||||
(item) => item.negative,
|
||||
),
|
||||
backgroundColor: '#D15454',
|
||||
},
|
||||
],
|
||||
}}
|
||||
legendID="legend-container-3"
|
||||
maxTicksLimitInX={10}
|
||||
isStacked={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type AnalyticsChartProps = {
|
||||
data: ChartData<'bar'>;
|
||||
legendID: string;
|
||||
maxTicksLimitInX: number;
|
||||
isStacked: boolean;
|
||||
};
|
||||
|
||||
function AnalyticsChart({
|
||||
data,
|
||||
legendID,
|
||||
maxTicksLimitInX,
|
||||
isStacked,
|
||||
}: AnalyticsChartProps) {
|
||||
const options = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
htmlLegend: {
|
||||
containerID: legendID,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
lineWidth: 0.2,
|
||||
color: '#C4C4C4',
|
||||
},
|
||||
border: {
|
||||
width: 0.2,
|
||||
color: '#C4C4C4',
|
||||
},
|
||||
ticks: {
|
||||
maxTicksLimit: maxTicksLimitInX,
|
||||
},
|
||||
stacked: isStacked,
|
||||
},
|
||||
y: {
|
||||
grid: {
|
||||
lineWidth: 0.2,
|
||||
color: '#C4C4C4',
|
||||
},
|
||||
border: {
|
||||
width: 0.2,
|
||||
color: '#C4C4C4',
|
||||
},
|
||||
stacked: isStacked,
|
||||
},
|
||||
},
|
||||
};
|
||||
return <Bar options={options} plugins={[htmlLegendPlugin]} data={data} />;
|
||||
}
|
||||
@@ -89,7 +89,7 @@ export default function General() {
|
||||
changeLanguage(selectedLanguage?.value);
|
||||
}, [selectedLanguage, changeLanguage]);
|
||||
return (
|
||||
<div className="mt-[59px]">
|
||||
<div className="mt-12">
|
||||
<div className="mb-5">
|
||||
<p className="font-bold text-jet dark:text-bright-gray">
|
||||
{t('settings.general.selectTheme')}
|
||||
|
||||
175
frontend/src/settings/Logs.tsx
Normal file
175
frontend/src/settings/Logs.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import React from 'react';
|
||||
|
||||
import userService from '../api/services/userService';
|
||||
import ChevronRight from '../assets/chevron-right.svg';
|
||||
import Dropdown from '../components/Dropdown';
|
||||
import { APIKeyData, LogData } from './types';
|
||||
import CoppyButton from '../components/CopyButton';
|
||||
|
||||
export default function Logs() {
|
||||
const [chatbots, setChatbots] = React.useState<APIKeyData[]>([]);
|
||||
const [selectedChatbot, setSelectedChatbot] =
|
||||
React.useState<APIKeyData | null>();
|
||||
const [logs, setLogs] = React.useState<LogData[]>([]);
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [hasMore, setHasMore] = React.useState(true);
|
||||
|
||||
const fetchChatbots = async () => {
|
||||
try {
|
||||
const response = await userService.getAPIKeys();
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch Chatbots');
|
||||
}
|
||||
const chatbots = await response.json();
|
||||
setChatbots(chatbots);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLogs = async () => {
|
||||
try {
|
||||
const response = await userService.getLogs({
|
||||
page: page,
|
||||
api_key_id: selectedChatbot?.id,
|
||||
page_size: 10,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch logs');
|
||||
}
|
||||
const olderLogs = await response.json();
|
||||
setLogs([...logs, ...olderLogs.logs]);
|
||||
setHasMore(olderLogs.has_more);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchChatbots();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasMore) fetchLogs();
|
||||
}, [page, selectedChatbot]);
|
||||
return (
|
||||
<div className="mt-12">
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="font-bold text-jet dark:text-bright-gray">
|
||||
Filter by chatbot
|
||||
</p>
|
||||
<Dropdown
|
||||
size="w-[55vw] sm:w-[360px]"
|
||||
options={[
|
||||
...chatbots.map((chatbot) => ({
|
||||
label: chatbot.name,
|
||||
value: chatbot.id,
|
||||
})),
|
||||
{ label: 'None', value: '' },
|
||||
]}
|
||||
placeholder="Select chatbot"
|
||||
onSelect={(chatbot: { label: string; value: string }) => {
|
||||
setSelectedChatbot(
|
||||
chatbots.find((item) => item.id === chatbot.value),
|
||||
);
|
||||
setLogs([]);
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
}}
|
||||
selectedValue={
|
||||
(selectedChatbot && {
|
||||
label: selectedChatbot.name,
|
||||
value: selectedChatbot.id,
|
||||
}) ||
|
||||
null
|
||||
}
|
||||
rounded="3xl"
|
||||
border="border"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<LogsTable logs={logs} setPage={setPage} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type LogsTableProps = {
|
||||
logs: LogData[];
|
||||
setPage: React.Dispatch<React.SetStateAction<number>>;
|
||||
};
|
||||
|
||||
function LogsTable({ logs, setPage }: LogsTableProps) {
|
||||
const observerRef = React.useRef<any>();
|
||||
const firstObserver = React.useCallback((node: HTMLDivElement) => {
|
||||
if (observerRef.current) {
|
||||
observerRef.current = new IntersectionObserver((enteries) => {
|
||||
if (enteries[0].isIntersecting) setPage((prev) => prev + 1);
|
||||
});
|
||||
}
|
||||
if (node && observerRef.current) observerRef.current.observe(node);
|
||||
}, []);
|
||||
return (
|
||||
<div className="logs-table border rounded-2xl h-[55vh] w-full overflow-hidden border-silver dark:border-silver/40">
|
||||
<div className="h-8 bg-black/10 dark:bg-chinese-black flex flex-col items-start justify-center">
|
||||
<p className="px-3 text-xs dark:text-gray-6000">
|
||||
API generated / chatbot conversations
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
ref={observerRef}
|
||||
className="flex flex-col items-start h-[51vh] overflow-y-auto bg-transparent flex-grow gap-px"
|
||||
>
|
||||
{logs.map((log, index) => {
|
||||
if (index === logs.length - 1) {
|
||||
return (
|
||||
<div ref={firstObserver} key={index}>
|
||||
<Log log={log} />
|
||||
</div>
|
||||
);
|
||||
} else return <Log key={index} log={log} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Log({ log }: { log: LogData }) {
|
||||
const logLevelColor = {
|
||||
info: 'text-green-500',
|
||||
error: 'text-red-500',
|
||||
warning: 'text-yellow-500',
|
||||
};
|
||||
const { id, action, timestamp, ...filteredLog } = log;
|
||||
return (
|
||||
<details className="group bg-transparent [&_summary::-webkit-details-marker]:hidden w-full hover:bg-[#F9F9F9] hover:dark:bg-dark-charcoal">
|
||||
<summary className="flex flex-row items-center gap-2 text-gray-900 cursor-pointer p-2 group-open:bg-[#F9F9F9] dark:group-open:bg-dark-charcoal">
|
||||
<img
|
||||
src={ChevronRight}
|
||||
alt="chevron-right"
|
||||
className="w-3 h-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>
|
||||
<h2 className="text-xs text-[#913400] dark:text-[#DF5200]">{`[${log.action}]`}</h2>
|
||||
<h2
|
||||
className={`text-xs ${logLevelColor[log.level]}`}
|
||||
>{`${log.question}`}</h2>
|
||||
</span>
|
||||
</summary>
|
||||
<div className="px-4 group-open:bg-[#F9F9F9] dark:group-open:bg-dark-charcoal">
|
||||
<p className="px-2 leading-relaxed text-gray-700 dark:text-gray-400 text-xs">
|
||||
{JSON.stringify(filteredLog, null, 2)}
|
||||
</p>
|
||||
<div className="my-px w-8">
|
||||
<CoppyButton
|
||||
text={JSON.stringify(filteredLog)}
|
||||
colorLight="transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
@@ -11,9 +11,11 @@ import {
|
||||
selectSourceDocs,
|
||||
setSourceDocs,
|
||||
} from '../preferences/preferenceSlice';
|
||||
import Analytics from './Analytics';
|
||||
import APIKeys from './APIKeys';
|
||||
import Documents from './Documents';
|
||||
import General from './General';
|
||||
import Logs from './Logs';
|
||||
import Widgets from './Widgets';
|
||||
|
||||
export default function Settings() {
|
||||
@@ -23,6 +25,8 @@ export default function Settings() {
|
||||
t('settings.general.label'),
|
||||
t('settings.documents.label'),
|
||||
t('settings.apiKeys.label'),
|
||||
t('settings.analytics.label'),
|
||||
t('settings.logs.label'),
|
||||
];
|
||||
const [activeTab, setActiveTab] = React.useState(t('settings.general.label'));
|
||||
const [widgetScreenshot, setWidgetScreenshot] = React.useState<File | null>(
|
||||
@@ -128,6 +132,10 @@ export default function Settings() {
|
||||
);
|
||||
case t('settings.apiKeys.label'):
|
||||
return <APIKeys />;
|
||||
case t('settings.analytics.label'):
|
||||
return <Analytics />;
|
||||
case t('settings.logs.label'):
|
||||
return <Logs />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
20
frontend/src/settings/types/index.ts
Normal file
20
frontend/src/settings/types/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export type APIKeyData = {
|
||||
id: string;
|
||||
name: string;
|
||||
key: string;
|
||||
source: string;
|
||||
prompt_id: string;
|
||||
chunks: string;
|
||||
};
|
||||
|
||||
export type LogData = {
|
||||
id: string;
|
||||
action: string;
|
||||
level: 'info' | 'error' | 'warning';
|
||||
user: string;
|
||||
question: string;
|
||||
response: string;
|
||||
sources: Record<string, any>[];
|
||||
retriever_params: Record<string, any>;
|
||||
timestamp: string;
|
||||
};
|
||||
@@ -275,7 +275,7 @@ function Upload({
|
||||
} else
|
||||
setRedditData({
|
||||
...redditData,
|
||||
[name]: value,
|
||||
[name]: name === 'number_posts' ? parseInt(value) : value,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -321,6 +321,7 @@ function Upload({
|
||||
colorVariant="gray"
|
||||
value={docName}
|
||||
onChange={(e) => setDocName(e.target.value)}
|
||||
borderVariant="thin"
|
||||
></Input>
|
||||
<div className="relative bottom-12 left-2 mt-[-20px]">
|
||||
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
|
||||
@@ -356,6 +357,7 @@ function Upload({
|
||||
{activeTab === 'remote' && (
|
||||
<>
|
||||
<Dropdown
|
||||
border="border"
|
||||
options={urlOptions}
|
||||
selectedValue={urlType}
|
||||
onSelect={(value: { label: string; value: string }) =>
|
||||
@@ -371,6 +373,7 @@ function Upload({
|
||||
type="text"
|
||||
value={urlName}
|
||||
onChange={(e) => setUrlName(e.target.value)}
|
||||
borderVariant="thin"
|
||||
></Input>
|
||||
<div className="relative bottom-12 left-2 mt-[-20px]">
|
||||
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
|
||||
@@ -382,6 +385,7 @@ function Upload({
|
||||
type="text"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
borderVariant="thin"
|
||||
></Input>
|
||||
<div className="relative bottom-12 left-2 mt-[-20px]">
|
||||
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
|
||||
@@ -390,68 +394,83 @@ function Upload({
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Input
|
||||
placeholder="Enter client ID"
|
||||
type="text"
|
||||
name="client_id"
|
||||
value={redditData.client_id}
|
||||
onChange={handleChange}
|
||||
></Input>
|
||||
<div className="relative bottom-12 left-2 mt-[-20px]">
|
||||
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
|
||||
{t('modals.uploadDoc.reddit.id')}
|
||||
</span>
|
||||
<div className="flex flex-col gap-1 mt-2">
|
||||
<div>
|
||||
<Input
|
||||
placeholder="Enter client ID"
|
||||
type="text"
|
||||
name="client_id"
|
||||
value={redditData.client_id}
|
||||
onChange={handleChange}
|
||||
borderVariant="thin"
|
||||
></Input>
|
||||
<div className="relative bottom-[52px] left-2">
|
||||
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
|
||||
{t('modals.uploadDoc.reddit.id')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Enter client secret"
|
||||
type="text"
|
||||
name="client_secret"
|
||||
value={redditData.client_secret}
|
||||
onChange={handleChange}
|
||||
></Input>
|
||||
<div className="relative bottom-12 left-2 mt-[-20px]">
|
||||
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
|
||||
{t('modals.uploadDoc.reddit.secret')}
|
||||
</span>
|
||||
<div>
|
||||
<Input
|
||||
placeholder="Enter client secret"
|
||||
type="text"
|
||||
name="client_secret"
|
||||
value={redditData.client_secret}
|
||||
onChange={handleChange}
|
||||
borderVariant="thin"
|
||||
></Input>
|
||||
<div className="relative bottom-[52px] left-2">
|
||||
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
|
||||
{t('modals.uploadDoc.reddit.secret')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Enter user agent"
|
||||
type="text"
|
||||
name="user_agent"
|
||||
value={redditData.user_agent}
|
||||
onChange={handleChange}
|
||||
></Input>
|
||||
<div className="relative bottom-12 left-2 mt-[-20px]">
|
||||
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
|
||||
{t('modals.uploadDoc.reddit.agent')}
|
||||
</span>
|
||||
<div>
|
||||
<Input
|
||||
placeholder="Enter user agent"
|
||||
type="text"
|
||||
name="user_agent"
|
||||
value={redditData.user_agent}
|
||||
onChange={handleChange}
|
||||
borderVariant="thin"
|
||||
></Input>
|
||||
<div className="relative bottom-[52px] left-2">
|
||||
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
|
||||
{t('modals.uploadDoc.reddit.agent')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Enter search queries"
|
||||
type="text"
|
||||
name="search_queries"
|
||||
value={redditData.search_queries}
|
||||
onChange={handleChange}
|
||||
></Input>
|
||||
<div className="relative bottom-12 left-2 mt-[-20px]">
|
||||
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
|
||||
{t('modals.uploadDoc.reddit.searchQueries')}
|
||||
</span>
|
||||
<div>
|
||||
<Input
|
||||
placeholder="Enter search queries"
|
||||
type="text"
|
||||
name="search_queries"
|
||||
value={redditData.search_queries}
|
||||
onChange={handleChange}
|
||||
borderVariant="thin"
|
||||
></Input>
|
||||
<div className="relative bottom-[52px] left-2">
|
||||
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
|
||||
{t('modals.uploadDoc.reddit.searchQueries')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Enter number of posts"
|
||||
type="number"
|
||||
name="number_posts"
|
||||
value={redditData.number_posts}
|
||||
onChange={handleChange}
|
||||
></Input>
|
||||
<div className="relative bottom-12 left-2 mt-[-20px]">
|
||||
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
|
||||
{t('modals.uploadDoc.reddit.numberOfPosts')}
|
||||
</span>
|
||||
<div>
|
||||
<Input
|
||||
placeholder="Enter number of posts"
|
||||
type="number"
|
||||
name="number_posts"
|
||||
value={redditData.number_posts}
|
||||
onChange={handleChange}
|
||||
borderVariant="thin"
|
||||
></Input>
|
||||
<div className="relative bottom-[52px] left-2">
|
||||
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
|
||||
{t('modals.uploadDoc.reddit.numberOfPosts')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
77
frontend/src/utils/chartUtils.ts
Normal file
77
frontend/src/utils/chartUtils.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Chart as ChartJS } from 'chart.js';
|
||||
|
||||
const getOrCreateLegendList = (
|
||||
chart: ChartJS,
|
||||
id: string,
|
||||
): HTMLUListElement => {
|
||||
const legendContainer = document.getElementById(id);
|
||||
let listContainer = legendContainer?.querySelector('ul') as HTMLUListElement;
|
||||
|
||||
if (!listContainer) {
|
||||
listContainer = document.createElement('ul');
|
||||
listContainer.style.display = 'flex';
|
||||
listContainer.style.flexDirection = 'row';
|
||||
listContainer.style.margin = '0';
|
||||
listContainer.style.padding = '0';
|
||||
|
||||
legendContainer?.appendChild(listContainer);
|
||||
}
|
||||
|
||||
return listContainer;
|
||||
};
|
||||
|
||||
export const htmlLegendPlugin = {
|
||||
id: 'htmlLegend',
|
||||
afterUpdate(chart: ChartJS, args: any, options: { containerID: string }) {
|
||||
const ul = getOrCreateLegendList(chart, options.containerID);
|
||||
|
||||
while (ul.firstChild) {
|
||||
ul.firstChild.remove();
|
||||
}
|
||||
|
||||
const items =
|
||||
chart.options.plugins?.legend?.labels?.generateLabels?.(chart) || [];
|
||||
|
||||
items.forEach((item: any) => {
|
||||
const li = document.createElement('li');
|
||||
li.style.alignItems = 'center';
|
||||
li.style.cursor = 'pointer';
|
||||
li.style.display = 'flex';
|
||||
li.style.flexDirection = 'row';
|
||||
li.style.marginLeft = '10px';
|
||||
|
||||
li.onclick = () => {
|
||||
chart.setDatasetVisibility(
|
||||
item.datasetIndex,
|
||||
!chart.isDatasetVisible(item.datasetIndex),
|
||||
);
|
||||
chart.update();
|
||||
};
|
||||
|
||||
const boxSpan = document.createElement('span');
|
||||
boxSpan.style.background = item.fillStyle;
|
||||
boxSpan.style.borderColor = item.strokeStyle;
|
||||
boxSpan.style.borderWidth = item.lineWidth + 'px';
|
||||
boxSpan.style.display = 'inline-block';
|
||||
boxSpan.style.flexShrink = '0';
|
||||
boxSpan.style.height = '10px';
|
||||
boxSpan.style.marginRight = '10px';
|
||||
boxSpan.style.width = '10px';
|
||||
boxSpan.style.borderRadius = '10px';
|
||||
|
||||
const textContainer = document.createElement('p');
|
||||
textContainer.style.fontSize = '12px';
|
||||
textContainer.style.color = item.fontColor;
|
||||
textContainer.style.margin = '0';
|
||||
textContainer.style.padding = '0';
|
||||
textContainer.style.textDecoration = item.hidden ? 'line-through' : '';
|
||||
|
||||
const text = document.createTextNode(item.text);
|
||||
textContainer.appendChild(text);
|
||||
|
||||
li.appendChild(boxSpan);
|
||||
li.appendChild(textContainer);
|
||||
ul.appendChild(li);
|
||||
});
|
||||
},
|
||||
};
|
||||
20
frontend/src/utils/dateTimeUtils.ts
Normal file
20
frontend/src/utils/dateTimeUtils.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export function formatDate(dateString: string): string {
|
||||
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(dateString)) {
|
||||
const dateTime = new Date(dateString);
|
||||
return dateTime.toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
} else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(dateString)) {
|
||||
const dateTime = new Date(dateString);
|
||||
return dateTime.toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
} else if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||
} else {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user