mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-12-14 11:51:30 +00:00
feat: analytics dashboard with respective endpoints
This commit is contained in:
@@ -1,14 +1,17 @@
|
|||||||
|
import datetime
|
||||||
import os
|
import os
|
||||||
import uuid
|
|
||||||
import shutil
|
import shutil
|
||||||
from flask import Blueprint, request, jsonify
|
import uuid
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from pymongo import MongoClient
|
|
||||||
from bson.objectid import ObjectId
|
|
||||||
from bson.binary import Binary, UuidRepresentation
|
from bson.binary import Binary, UuidRepresentation
|
||||||
from werkzeug.utils import secure_filename
|
|
||||||
from bson.dbref import DBRef
|
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.api.user.tasks import ingest, ingest_remote
|
||||||
|
|
||||||
from application.core.settings import settings
|
from application.core.settings import settings
|
||||||
@@ -21,6 +24,7 @@ vectors_collection = db["vectors"]
|
|||||||
prompts_collection = db["prompts"]
|
prompts_collection = db["prompts"]
|
||||||
feedback_collection = db["feedback"]
|
feedback_collection = db["feedback"]
|
||||||
api_key_collection = db["api_keys"]
|
api_key_collection = db["api_keys"]
|
||||||
|
token_usage_collection = db["token_usage"]
|
||||||
shared_conversations_collections = db["shared_conversations"]
|
shared_conversations_collections = db["shared_conversations"]
|
||||||
|
|
||||||
user = Blueprint("user", __name__)
|
user = Blueprint("user", __name__)
|
||||||
@@ -30,6 +34,27 @@ current_dir = os.path.dirname(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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"])
|
@user.route("/api/delete_conversation", methods=["POST"])
|
||||||
def delete_conversation():
|
def delete_conversation():
|
||||||
# deletes a conversation from the database
|
# deletes a conversation from the database
|
||||||
@@ -96,6 +121,7 @@ def api_feedback():
|
|||||||
"question": question,
|
"question": question,
|
||||||
"answer": answer,
|
"answer": answer,
|
||||||
"feedback": feedback,
|
"feedback": feedback,
|
||||||
|
"timestamp": datetime.datetime.now(datetime.timezone.utc),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
@@ -697,3 +723,407 @@ def get_publicly_shared_conversations(identifier: str):
|
|||||||
except Exception as err:
|
except Exception as err:
|
||||||
print(err)
|
print(err)
|
||||||
return jsonify({"success": False, "error": str(err)}), 400
|
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
|
||||||
|
|||||||
28
frontend/package-lock.json
generated
28
frontend/package-lock.json
generated
@@ -14,6 +14,7 @@
|
|||||||
"i18next-browser-languagedetector": "^8.0.0",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-chartjs-2": "^5.2.0",
|
||||||
"react-copy-to-clipboard": "^5.1.0",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
@@ -858,6 +859,12 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@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": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@@ -2503,6 +2510,18 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"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": {
|
"node_modules/chokidar": {
|
||||||
"version": "3.5.3",
|
"version": "3.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||||
@@ -7253,6 +7272,15 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/react-copy-to-clipboard": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
|
"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",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-chartjs-2": "^5.2.0",
|
||||||
"react-copy-to-clipboard": "^5.1.0",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ const endpoints = {
|
|||||||
SINGLE_PROMPT: (id: string) => `/api/get_single_prompt?id=${id}`,
|
SINGLE_PROMPT: (id: string) => `/api/get_single_prompt?id=${id}`,
|
||||||
DELETE_PATH: (docPath: string) => `/api/delete_old?path=${docPath}`,
|
DELETE_PATH: (docPath: string) => `/api/delete_old?path=${docPath}`,
|
||||||
TASK_STATUS: (task_id: string) => `/api/task_status?task_id=${task_id}`,
|
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',
|
||||||
},
|
},
|
||||||
CONVERSATION: {
|
CONVERSATION: {
|
||||||
ANSWER: '/api/answer',
|
ANSWER: '/api/answer',
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ const userService = {
|
|||||||
apiClient.get(endpoints.USER.DELETE_PATH(docPath)),
|
apiClient.get(endpoints.USER.DELETE_PATH(docPath)),
|
||||||
getTaskStatus: (task_id: string): Promise<any> =>
|
getTaskStatus: (task_id: string): Promise<any> =>
|
||||||
apiClient.get(endpoints.USER.TASK_STATUS(task_id)),
|
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),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default userService;
|
export default userService;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ function Dropdown({
|
|||||||
showDelete,
|
showDelete,
|
||||||
onDelete,
|
onDelete,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
contentSize = 'text-base',
|
||||||
}: {
|
}: {
|
||||||
options:
|
options:
|
||||||
| string[]
|
| string[]
|
||||||
@@ -41,6 +42,7 @@ function Dropdown({
|
|||||||
showDelete?: boolean;
|
showDelete?: boolean;
|
||||||
onDelete?: (value: string) => void;
|
onDelete?: (value: string) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
contentSize?: string;
|
||||||
}) {
|
}) {
|
||||||
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
||||||
const [isOpen, setIsOpen] = React.useState(false);
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
@@ -84,9 +86,9 @@ function Dropdown({
|
|||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<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'
|
!selectedValue && 'text-silver dark:text-gray-400'
|
||||||
}`}
|
} ${contentSize}`}
|
||||||
>
|
>
|
||||||
{selectedValue && 'label' in selectedValue
|
{selectedValue && 'label' in selectedValue
|
||||||
? selectedValue.label
|
? selectedValue.label
|
||||||
@@ -123,7 +125,7 @@ function Dropdown({
|
|||||||
onSelect(option);
|
onSelect(option);
|
||||||
setIsOpen(false);
|
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'
|
{typeof option === 'string'
|
||||||
? option
|
? option
|
||||||
|
|||||||
@@ -62,6 +62,9 @@
|
|||||||
"key": "API Key",
|
"key": "API Key",
|
||||||
"sourceDoc": "Source Document",
|
"sourceDoc": "Source Document",
|
||||||
"createNew": "Create New"
|
"createNew": "Create New"
|
||||||
|
},
|
||||||
|
"analytics": {
|
||||||
|
"label": "Analytics"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
|
|||||||
@@ -62,6 +62,9 @@
|
|||||||
"key": "Clave de API",
|
"key": "Clave de API",
|
||||||
"sourceDoc": "Documento Fuente",
|
"sourceDoc": "Documento Fuente",
|
||||||
"createNew": "Crear Nuevo"
|
"createNew": "Crear Nuevo"
|
||||||
|
},
|
||||||
|
"analytics": {
|
||||||
|
"label": "Analítica"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
|
|||||||
@@ -62,6 +62,9 @@
|
|||||||
"key": "APIキー",
|
"key": "APIキー",
|
||||||
"sourceDoc": "ソースドキュメント",
|
"sourceDoc": "ソースドキュメント",
|
||||||
"createNew": "新規作成"
|
"createNew": "新規作成"
|
||||||
|
},
|
||||||
|
"analytics": {
|
||||||
|
"label": "分析"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
|
|||||||
@@ -62,6 +62,9 @@
|
|||||||
"key": "API 密钥",
|
"key": "API 密钥",
|
||||||
"sourceDoc": "源文档",
|
"sourceDoc": "源文档",
|
||||||
"createNew": "创建新的"
|
"createNew": "创建新的"
|
||||||
|
},
|
||||||
|
"analytics": {
|
||||||
|
"label": "分析"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
|
|||||||
@@ -5,15 +5,14 @@ import userService from '../api/services/userService';
|
|||||||
import Trash from '../assets/trash.svg';
|
import Trash from '../assets/trash.svg';
|
||||||
import CreateAPIKeyModal from '../modals/CreateAPIKeyModal';
|
import CreateAPIKeyModal from '../modals/CreateAPIKeyModal';
|
||||||
import SaveAPIKeyModal from '../modals/SaveAPIKeyModal';
|
import SaveAPIKeyModal from '../modals/SaveAPIKeyModal';
|
||||||
|
import { APIKeyData } from './types';
|
||||||
|
|
||||||
export default function APIKeys() {
|
export default function APIKeys() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isCreateModalOpen, setCreateModal] = React.useState(false);
|
const [isCreateModalOpen, setCreateModal] = React.useState(false);
|
||||||
const [isSaveKeyModalOpen, setSaveKeyModal] = React.useState(false);
|
const [isSaveKeyModalOpen, setSaveKeyModal] = React.useState(false);
|
||||||
const [newKey, setNewKey] = React.useState('');
|
const [newKey, setNewKey] = React.useState('');
|
||||||
const [apiKeys, setApiKeys] = React.useState<
|
const [apiKeys, setApiKeys] = React.useState<APIKeyData[]>([]);
|
||||||
{ name: string; key: string; source: string; id: string }[]
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
const handleFetchKeys = async () => {
|
const handleFetchKeys = async () => {
|
||||||
try {
|
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);
|
changeLanguage(selectedLanguage?.value);
|
||||||
}, [selectedLanguage, changeLanguage]);
|
}, [selectedLanguage, changeLanguage]);
|
||||||
return (
|
return (
|
||||||
<div className="mt-[59px]">
|
<div className="mt-12">
|
||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
<p className="font-bold text-jet dark:text-bright-gray">
|
<p className="font-bold text-jet dark:text-bright-gray">
|
||||||
{t('settings.general.selectTheme')}
|
{t('settings.general.selectTheme')}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
selectSourceDocs,
|
selectSourceDocs,
|
||||||
setSourceDocs,
|
setSourceDocs,
|
||||||
} from '../preferences/preferenceSlice';
|
} from '../preferences/preferenceSlice';
|
||||||
|
import Analytics from './Analytics';
|
||||||
import APIKeys from './APIKeys';
|
import APIKeys from './APIKeys';
|
||||||
import Documents from './Documents';
|
import Documents from './Documents';
|
||||||
import General from './General';
|
import General from './General';
|
||||||
@@ -23,6 +24,7 @@ export default function Settings() {
|
|||||||
t('settings.general.label'),
|
t('settings.general.label'),
|
||||||
t('settings.documents.label'),
|
t('settings.documents.label'),
|
||||||
t('settings.apiKeys.label'),
|
t('settings.apiKeys.label'),
|
||||||
|
t('settings.analytics.label'),
|
||||||
];
|
];
|
||||||
const [activeTab, setActiveTab] = React.useState(t('settings.general.label'));
|
const [activeTab, setActiveTab] = React.useState(t('settings.general.label'));
|
||||||
const [widgetScreenshot, setWidgetScreenshot] = React.useState<File | null>(
|
const [widgetScreenshot, setWidgetScreenshot] = React.useState<File | null>(
|
||||||
@@ -129,6 +131,8 @@ export default function Settings() {
|
|||||||
);
|
);
|
||||||
case t('settings.apiKeys.label'):
|
case t('settings.apiKeys.label'):
|
||||||
return <APIKeys />;
|
return <APIKeys />;
|
||||||
|
case t('settings.analytics.label'):
|
||||||
|
return <Analytics />;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
8
frontend/src/settings/types/index.ts
Normal file
8
frontend/src/settings/types/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export type APIKeyData = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
key: string;
|
||||||
|
source: string;
|
||||||
|
prompt_id: string;
|
||||||
|
chunks: string;
|
||||||
|
};
|
||||||
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