mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 08:15:59 +00:00
Enhance dashboard functionality
Added an overview (landing page showing current and available config) and advanced stats windows, controlled by top-right menu Stats calculation is no longer optional, but we use a trivial token estimator by default
This commit is contained in:
1 parent
45407f7a2a
commit
ad4fec9230
10 files changed
+1234
-385
No files matched your search
@@ -12,7 +12,7 @@ from serena.constants import REPO_ROOT
|
||||
from serena.tools import FindFileTool, FindReferencingSymbolsTool, GetSymbolsOverviewTool, SearchForPatternTool
|
||||
|
||||
if __name__ == "__main__":
|
||||
agent = SerenaAgent(project=REPO_ROOT, serena_config=SerenaConfig(gui_log_window_enabled=False, web_dashboard=False))
|
||||
agent = SerenaAgent(project=REPO_ROOT, serena_config=SerenaConfig(gui_log_window_enabled=False, web_dashboard=True))
|
||||
|
||||
# apply a tool
|
||||
find_refs_tool = agent.get_tool(FindReferencingSymbolsTool)
|
||||
@@ -21,6 +21,7 @@ if __name__ == "__main__":
|
||||
overview_tool = agent.get_tool(GetSymbolsOverviewTool)
|
||||
|
||||
result = agent.execute_task(
|
||||
lambda: overview_tool.apply("src/solidlsp/ls.py"),
|
||||
lambda: overview_tool.apply_ex(relative_file_path="src/solidlsp/ls.py"),
|
||||
)
|
||||
pprint(json.loads(result))
|
||||
# input("Press Enter to continue...")
|
||||
+24
-27
@@ -127,25 +127,9 @@ class SerenaAgent:
|
||||
if self._gui_log_viewer is not None:
|
||||
self._gui_log_viewer.set_tool_names(tool_names)
|
||||
|
||||
self._tool_usage_stats: ToolUsageStats | None = None
|
||||
if self.serena_config.record_tool_usage_stats:
|
||||
token_count_estimator = RegisteredTokenCountEstimator[self.serena_config.token_count_estimator]
|
||||
log.info(f"Tool usage statistics recording is enabled with token count estimator: {token_count_estimator.name}.")
|
||||
self._tool_usage_stats = ToolUsageStats(token_count_estimator)
|
||||
|
||||
# start the dashboard (web frontend), registering its log handler
|
||||
if self.serena_config.web_dashboard:
|
||||
self._dashboard_thread, port = SerenaDashboardAPI(
|
||||
get_memory_log_handler(), tool_names, agent=self, tool_usage_stats=self._tool_usage_stats
|
||||
).run_in_thread()
|
||||
dashboard_url = f"http://127.0.0.1:{port}/dashboard/index.html"
|
||||
log.info("Serena web dashboard started at %s", dashboard_url)
|
||||
if self.serena_config.web_dashboard_open_on_launch:
|
||||
# open the dashboard URL in the default web browser (using a separate process to control
|
||||
# output redirection)
|
||||
process = multiprocessing.Process(target=self._open_dashboard, args=(dashboard_url,))
|
||||
process.start()
|
||||
process.join(timeout=1)
|
||||
token_count_estimator = RegisteredTokenCountEstimator[self.serena_config.token_count_estimator]
|
||||
log.info(f"Will record tool usage statistics with token count estimator: {token_count_estimator.name}.")
|
||||
self._tool_usage_stats = ToolUsageStats(token_count_estimator)
|
||||
|
||||
# log fundamental information
|
||||
log.info(f"Starting Serena server (version={serena_version()}, process id={os.getpid()}, parent process id={os.getppid()})")
|
||||
@@ -192,6 +176,22 @@ class SerenaAgent:
|
||||
except Exception as e:
|
||||
log.error(f"Error activating project '{project}' at startup: {e}", exc_info=e)
|
||||
|
||||
# start the dashboard (web frontend), registering its log handler
|
||||
# should be the last thing to happen in the initialization since the dashboard
|
||||
# may access various parts of the agent
|
||||
if self.serena_config.web_dashboard:
|
||||
self._dashboard_thread, port = SerenaDashboardAPI(
|
||||
get_memory_log_handler(), tool_names, agent=self, tool_usage_stats=self._tool_usage_stats
|
||||
).run_in_thread()
|
||||
dashboard_url = f"http://127.0.0.1:{port}/dashboard/index.html"
|
||||
log.info("Serena web dashboard started at %s", dashboard_url)
|
||||
if self.serena_config.web_dashboard_open_on_launch:
|
||||
# open the dashboard URL in the default web browser (using a separate process to control
|
||||
# output redirection)
|
||||
process = multiprocessing.Process(target=self._open_dashboard, args=(dashboard_url,))
|
||||
process.start()
|
||||
process.join(timeout=1)
|
||||
|
||||
def get_context(self) -> SerenaAgentContext:
|
||||
return self._context
|
||||
|
||||
@@ -234,18 +234,15 @@ class SerenaAgent:
|
||||
tool_inclusion_definitions.append(project.project_config)
|
||||
return tool_inclusion_definitions
|
||||
|
||||
def record_tool_usage_if_enabled(self, input_kwargs: dict, tool_result: str | dict, tool: Tool) -> None:
|
||||
def record_tool_usage(self, input_kwargs: dict, tool_result: str | dict, tool: Tool) -> None:
|
||||
"""
|
||||
Record the usage of a tool with the given input and output strings if tool usage statistics recording is enabled.
|
||||
"""
|
||||
tool_name = tool.get_name()
|
||||
if self._tool_usage_stats is not None:
|
||||
input_str = str(input_kwargs)
|
||||
output_str = str(tool_result)
|
||||
log.debug(f"Recording tool usage for tool '{tool_name}'")
|
||||
self._tool_usage_stats.record_tool_usage(tool_name, input_str, output_str)
|
||||
else:
|
||||
log.debug(f"Tool usage statistics recording is disabled, not recording usage of '{tool_name}'.")
|
||||
input_str = str(input_kwargs)
|
||||
output_str = str(tool_result)
|
||||
log.debug(f"Recording tool usage for tool '{tool_name}'")
|
||||
self._tool_usage_stats.record_tool_usage(tool_name, input_str, output_str)
|
||||
|
||||
@staticmethod
|
||||
def _open_dashboard(url: str) -> None:
|
||||
|
||||
+17
-1
@@ -69,12 +69,26 @@ class AnthropicTokenCount(TokenCountEstimator):
|
||||
return self._send_count_tokens_request(text).input_tokens
|
||||
|
||||
|
||||
class CharCountEstimator(TokenCountEstimator):
|
||||
"""
|
||||
A naive character count estimator that estimates tokens based on character count.
|
||||
"""
|
||||
|
||||
def __init__(self, avg_chars_per_token: int = 4):
|
||||
self._avg_chars_per_token = avg_chars_per_token
|
||||
|
||||
def estimate_token_count(self, text: str) -> int:
|
||||
# Assuming an average of 4 characters per token
|
||||
return len(text) // self._avg_chars_per_token
|
||||
|
||||
|
||||
_registered_token_estimator_instances_cache: dict[RegisteredTokenCountEstimator, TokenCountEstimator] = {}
|
||||
|
||||
|
||||
class RegisteredTokenCountEstimator(Enum):
|
||||
TIKTOKEN_GPT4O = "TIKTOKEN_GPT4O"
|
||||
ANTHROPIC_CLAUDE_SONNET_4 = "ANTHROPIC_CLAUDE_SONNET_4"
|
||||
CHAR_COUNT = "CHAR_COUNT"
|
||||
|
||||
@classmethod
|
||||
def get_valid_names(cls) -> list[str]:
|
||||
@@ -89,8 +103,10 @@ class RegisteredTokenCountEstimator(Enum):
|
||||
return TiktokenCountEstimator(model_name="gpt-4o")
|
||||
case RegisteredTokenCountEstimator.ANTHROPIC_CLAUDE_SONNET_4:
|
||||
return AnthropicTokenCount(model_name="claude-sonnet-4-20250514")
|
||||
case RegisteredTokenCountEstimator.CHAR_COUNT:
|
||||
return CharCountEstimator(avg_chars_per_token=4)
|
||||
case _:
|
||||
raise ValueError(f"Unknown token count estimator: {self.value}")
|
||||
raise ValueError(f"Unknown token count estimator: {self}")
|
||||
|
||||
def load_estimator(self) -> TokenCountEstimator:
|
||||
estimator_instance = _registered_token_estimator_instances_cache.get(self)
|
||||
|
||||
@@ -340,10 +340,8 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
|
||||
"""
|
||||
whether to apply JetBrains mode
|
||||
"""
|
||||
record_tool_usage_stats: bool = False
|
||||
"""Whether to record tool usage statistics, they will be shown in the web dashboard if recording is active.
|
||||
"""
|
||||
token_count_estimator: str = RegisteredTokenCountEstimator.TIKTOKEN_GPT4O.name
|
||||
|
||||
token_count_estimator: str = RegisteredTokenCountEstimator.CHAR_COUNT.name
|
||||
"""Only relevant if `record_tool_usage` is True; the name of the token count estimator to use for tool usage statistics.
|
||||
See the `RegisteredTokenCountEstimator` enum for available options.
|
||||
|
||||
@@ -457,7 +455,6 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
|
||||
instance.excluded_tools = loaded_commented_yaml.get("excluded_tools", [])
|
||||
instance.included_optional_tools = loaded_commented_yaml.get("included_optional_tools", [])
|
||||
instance.jetbrains = loaded_commented_yaml.get("jetbrains", False)
|
||||
instance.record_tool_usage_stats = loaded_commented_yaml.get("record_tool_usage_stats", False)
|
||||
instance.token_count_estimator = loaded_commented_yaml.get(
|
||||
"token_count_estimator", RegisteredTokenCountEstimator.TIKTOKEN_GPT4O.name
|
||||
)
|
||||
|
||||
@@ -39,6 +39,18 @@ class ResponseToolStats(BaseModel):
|
||||
stats: dict[str, dict[str, int]]
|
||||
|
||||
|
||||
class ResponseConfigOverview(BaseModel):
|
||||
active_project: dict[str, str | None]
|
||||
context: dict[str, str]
|
||||
modes: list[dict[str, str]]
|
||||
active_tools: list[str]
|
||||
tool_stats_summary: dict[str, dict[str, int]]
|
||||
registered_projects: list[dict[str, str | bool]]
|
||||
available_tools: list[dict[str, str | bool]]
|
||||
available_modes: list[dict[str, str | bool]]
|
||||
available_contexts: list[dict[str, str | bool]]
|
||||
|
||||
|
||||
class SerenaDashboardAPI:
|
||||
log = logging.getLogger(__qualname__)
|
||||
|
||||
@@ -104,6 +116,11 @@ class SerenaDashboardAPI:
|
||||
estimator_name = self._tool_usage_stats.token_estimator_name if self._tool_usage_stats else "unknown"
|
||||
return {"token_count_estimator_name": estimator_name}
|
||||
|
||||
@self._app.route("/get_config_overview", methods=["GET"])
|
||||
def get_config_overview() -> dict[str, Any]:
|
||||
result = self._get_config_overview()
|
||||
return result.model_dump()
|
||||
|
||||
@self._app.route("/shutdown", methods=["PUT"])
|
||||
def shutdown() -> dict[str, str]:
|
||||
self._shutdown()
|
||||
@@ -129,6 +146,95 @@ class SerenaDashboardAPI:
|
||||
if self._tool_usage_stats is not None:
|
||||
self._tool_usage_stats.clear()
|
||||
|
||||
def _get_config_overview(self) -> ResponseConfigOverview:
|
||||
from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode
|
||||
|
||||
# Get active project info
|
||||
project = self._agent.get_active_project()
|
||||
active_project_name = project.project_name if project else None
|
||||
project_info = {
|
||||
"name": active_project_name,
|
||||
"language": project.project_config.language.value if project else None,
|
||||
}
|
||||
|
||||
# Get context info
|
||||
context = self._agent.get_context()
|
||||
context_info = {
|
||||
"name": context.name,
|
||||
"description": context.description,
|
||||
}
|
||||
|
||||
# Get active modes
|
||||
modes = self._agent.get_active_modes()
|
||||
modes_info = [{"name": mode.name, "description": mode.description} for mode in modes]
|
||||
active_mode_names = [mode.name for mode in modes]
|
||||
|
||||
# Get active tools
|
||||
active_tools = self._agent.get_active_tool_names()
|
||||
|
||||
# Get registered projects
|
||||
registered_projects: list[dict[str, str | bool]] = []
|
||||
for proj in self._agent.serena_config.projects:
|
||||
registered_projects.append(
|
||||
{
|
||||
"name": proj.project_name,
|
||||
"path": str(proj.project_root),
|
||||
"is_active": proj.project_name == active_project_name,
|
||||
}
|
||||
)
|
||||
|
||||
# Get all available tools (excluding active ones)
|
||||
all_tool_names = sorted([tool.get_name_from_cls() for tool in self._agent._all_tools.values()])
|
||||
available_tools: list[dict[str, str | bool]] = []
|
||||
for tool_name in all_tool_names:
|
||||
if tool_name not in active_tools:
|
||||
available_tools.append(
|
||||
{
|
||||
"name": tool_name,
|
||||
"is_active": False,
|
||||
}
|
||||
)
|
||||
|
||||
# Get all available modes
|
||||
all_mode_names = SerenaAgentMode.list_registered_mode_names()
|
||||
available_modes: list[dict[str, str | bool]] = []
|
||||
for mode_name in all_mode_names:
|
||||
available_modes.append(
|
||||
{
|
||||
"name": mode_name,
|
||||
"is_active": mode_name in active_mode_names,
|
||||
}
|
||||
)
|
||||
|
||||
# Get all available contexts
|
||||
all_context_names = SerenaAgentContext.list_registered_context_names()
|
||||
available_contexts: list[dict[str, str | bool]] = []
|
||||
for context_name in all_context_names:
|
||||
available_contexts.append(
|
||||
{
|
||||
"name": context_name,
|
||||
"is_active": context_name == context.name,
|
||||
}
|
||||
)
|
||||
|
||||
# Get basic tool stats (just num_calls for overview)
|
||||
tool_stats_summary = {}
|
||||
if self._tool_usage_stats is not None:
|
||||
full_stats = self._tool_usage_stats.get_tool_stats_dict()
|
||||
tool_stats_summary = {name: {"num_calls": stats["num_times_called"]} for name, stats in full_stats.items()}
|
||||
|
||||
return ResponseConfigOverview(
|
||||
active_project=project_info,
|
||||
context=context_info,
|
||||
modes=modes_info,
|
||||
active_tools=active_tools,
|
||||
tool_stats_summary=tool_stats_summary,
|
||||
registered_projects=registered_projects,
|
||||
available_tools=available_tools,
|
||||
available_modes=available_modes,
|
||||
available_contexts=available_contexts,
|
||||
)
|
||||
|
||||
def _shutdown(self) -> None:
|
||||
log.info("Shutting down Serena")
|
||||
if self._shutdown_callback:
|
||||
|
||||
@@ -0,0 +1,620 @@
|
||||
:root {
|
||||
/* Light theme variables */
|
||||
--bg-primary: #f5f5f5;
|
||||
--bg-secondary: #ffffff;
|
||||
--text-primary: #000000;
|
||||
--text-secondary: #333333;
|
||||
--text-muted: #666666;
|
||||
--border-color: #ddd;
|
||||
--btn-primary: #eaa45d;
|
||||
--btn-hover: #dca662;
|
||||
--btn-disabled: #6c757d;
|
||||
--shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
--tool-highlight: #ffff00;
|
||||
--tool-highlight-text: #000000;
|
||||
--log-debug: #808080;
|
||||
--log-info: #000000;
|
||||
--log-warning: #FF8C00;
|
||||
--log-error: #FF0000;
|
||||
--stats-header: #f8f9fa;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
/* Dark theme variables */
|
||||
--bg-primary: #1a1a1a;
|
||||
--bg-secondary: #2d2d2d;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #e0e0e0;
|
||||
--text-muted: #b0b0b0;
|
||||
--border-color: #444;
|
||||
--btn-primary: #eaa45d;
|
||||
--btn-hover: #dca662;
|
||||
--btn-disabled: #6c757d;
|
||||
--shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
--tool-highlight: #ffd700;
|
||||
--tool-highlight-text: #000000;
|
||||
--log-debug: #808080;
|
||||
--log-info: #ffffff;
|
||||
--log-warning: #FF8C00;
|
||||
--log-error: #FF0000;
|
||||
--stats-header: #3a3a3a;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
padding-top: 130px;
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||
min-height: 70px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.logo-container img {
|
||||
max-width: 300px;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header-nav {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.menu-button {
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.menu-button:hover {
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.menu-dropdown {
|
||||
position: absolute;
|
||||
top: 45px;
|
||||
right: 0;
|
||||
background-color: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--shadow);
|
||||
min-width: 200px;
|
||||
z-index: 999;
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.menu-dropdown a {
|
||||
display: block;
|
||||
padding: 12px 20px;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.menu-dropdown a:hover {
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.menu-dropdown a.active {
|
||||
background-color: var(--btn-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menu-dropdown hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.page-view {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Overview Page Layout */
|
||||
.overview-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 400px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.overview-left {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.overview-right {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Overview Page Styles */
|
||||
.config-section,
|
||||
.basic-stats-section,
|
||||
.projects-section {
|
||||
background-color: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 5px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.config-section h2,
|
||||
.basic-stats-section h2 {
|
||||
margin-top: 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Collapsible Headers */
|
||||
.collapsible-header {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
font-size: 18px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.collapsible-header:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
transition: transform 0.3s ease;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.toggle-icon.expanded {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
|
||||
.collapsible-content {
|
||||
margin-top: 15px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 180px 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.config-label {
|
||||
font-weight: bold;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.config-value {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.config-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.config-list li {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.tools-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.tool-item {
|
||||
background-color: var(--bg-primary);
|
||||
padding: 6px 10px;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Projects List */
|
||||
.project-item {
|
||||
padding: 10px 12px;
|
||||
margin: 5px 0;
|
||||
border-radius: 4px;
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.project-item:hover {
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.project-item.active {
|
||||
background-color: var(--btn-primary);
|
||||
color: white;
|
||||
border-color: var(--btn-primary);
|
||||
}
|
||||
|
||||
.project-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.project-path {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.project-item.active .project-path {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* Generic Item Styles for Tools/Modes/Contexts */
|
||||
.info-item {
|
||||
padding: 8px 12px;
|
||||
margin: 5px 0;
|
||||
border-radius: 4px;
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
transition: background-color 0.2s ease;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.info-item:hover {
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.info-item.active {
|
||||
background-color: var(--btn-primary);
|
||||
color: white;
|
||||
border-color: var(--btn-primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Basic Stats Styles */
|
||||
.basic-stats-section {
|
||||
background-color: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 5px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.basic-stats-section h2 {
|
||||
margin-top: 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.stat-bar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 8px 0;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat-tool-name {
|
||||
min-width: 200px;
|
||||
max-width: 200px;
|
||||
font-weight: bold;
|
||||
color: var(--text-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.bar-wrapper {
|
||||
flex: 1;
|
||||
height: 24px;
|
||||
background-color: var(--border-color);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bar {
|
||||
height: 100%;
|
||||
background-color: var(--btn-primary);
|
||||
transition: width 0.5s ease;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.stat-count {
|
||||
min-width: 60px;
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.no-stats-message {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Log Container Styles */
|
||||
.log-container {
|
||||
background-color: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 5px;
|
||||
height: calc(100vh - 260px);
|
||||
max-height: 700px;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
padding: 10px;
|
||||
white-space: pre-wrap;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: sticky;
|
||||
top: 90px;
|
||||
z-index: 100;
|
||||
background-color: var(--bg-primary);
|
||||
padding: 10px 0;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: var(--btn-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: var(--btn-hover);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background-color: var(--btn-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.theme-toggle span {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.log-debug {
|
||||
color: var(--log-debug);
|
||||
}
|
||||
|
||||
.log-info {
|
||||
color: var(--log-info);
|
||||
}
|
||||
|
||||
.log-warning {
|
||||
color: var(--log-warning);
|
||||
}
|
||||
|
||||
.log-error {
|
||||
color: var(--log-error);
|
||||
}
|
||||
|
||||
.log-default {
|
||||
color: var(--log-info);
|
||||
}
|
||||
|
||||
/* Tool name highlighting */
|
||||
.tool-name {
|
||||
background-color: var(--tool-highlight);
|
||||
color: var(--tool-highlight-text);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: var(--log-error);
|
||||
text-align: center;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
/* Advanced Stats Styles */
|
||||
.charts-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
justify-content: space-between;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.chart-group {
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
max-width: 320px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chart-wide {
|
||||
flex: 0 0 100%;
|
||||
min-width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.chart-group h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.stats-summary {
|
||||
margin: 0 auto;
|
||||
border-collapse: collapse;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
transition: background-color 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.stats-summary th,
|
||||
.stats-summary td {
|
||||
padding: 10px 20px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
transition: border-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.stats-summary th {
|
||||
background-color: var(--stats-header);
|
||||
font-weight: bold;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.stats-summary tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.overview-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.overview-right {
|
||||
order: -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding-top: 140px;
|
||||
}
|
||||
|
||||
.header {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo-container img {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.header-nav {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.charts-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chart-group,
|
||||
.chart-wide {
|
||||
min-width: auto;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.config-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.tools-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stat-bar-container {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-tool-name {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class LogMessage {
|
||||
}
|
||||
|
||||
escapeHtml (convertString) {
|
||||
if (typeof convertString !== 'string') return convertString;
|
||||
if (typeof convertString !== 'string') return convertString;
|
||||
|
||||
const patterns = {
|
||||
'<' : '<',
|
||||
@@ -49,47 +49,322 @@ class Dashboard {
|
||||
constructor() {
|
||||
let self = this;
|
||||
|
||||
// Page state
|
||||
this.currentPage = 'overview';
|
||||
this.configData = null;
|
||||
|
||||
// Tool names and stats
|
||||
this.toolNames = [];
|
||||
this.currentMaxIdx = -1;
|
||||
this.pollInterval = null;
|
||||
this.configPollInterval = null;
|
||||
this.failureCount = 0;
|
||||
|
||||
// jQuery elements
|
||||
this.$logContainer = $('#log-container');
|
||||
this.$errorContainer = $('#error-container');
|
||||
this.$loadButton = $('#load-logs');
|
||||
this.$shutdownButton = $('#shutdown');
|
||||
this.$toggleStats = $('#toggle-stats');
|
||||
this.$statsSection = $('#stats-section');
|
||||
this.$refreshStats = $('#refresh-stats');
|
||||
this.$clearStats = $('#clear-stats');
|
||||
this.$menuToggle = $('#menu-toggle');
|
||||
this.$menuDropdown = $('#menu-dropdown');
|
||||
this.$menuShutdown = $('#menu-shutdown');
|
||||
this.$themeToggle = $('#theme-toggle');
|
||||
this.$themeIcon = $('#theme-icon');
|
||||
this.$themeText = $('#theme-text');
|
||||
this.$configDisplay = $('#config-display');
|
||||
this.$basicStatsDisplay = $('#basic-stats-display');
|
||||
this.$statsSection = $('#stats-section');
|
||||
this.$refreshStats = $('#refresh-stats');
|
||||
this.$clearStats = $('#clear-stats');
|
||||
this.$projectsDisplay = $('#projects-display');
|
||||
this.$projectsHeader = $('#projects-header');
|
||||
this.$availableToolsDisplay = $('#available-tools-display');
|
||||
this.$availableModesDisplay = $('#available-modes-display');
|
||||
this.$availableContextsDisplay = $('#available-contexts-display');
|
||||
|
||||
// Chart references
|
||||
this.countChart = null;
|
||||
this.tokensChart = null;
|
||||
this.inputChart = null;
|
||||
this.outputChart = null;
|
||||
|
||||
// register event handlers
|
||||
// Register event handlers
|
||||
this.$loadButton.click(this.loadLogs.bind(this));
|
||||
this.$shutdownButton.click(this.shutdown.bind(this));
|
||||
this.$toggleStats.click(this.toggleStats.bind(this));
|
||||
this.$menuShutdown.click(this.shutdown.bind(this));
|
||||
this.$menuToggle.click(this.toggleMenu.bind(this));
|
||||
this.$themeToggle.click(this.toggleTheme.bind(this));
|
||||
this.$refreshStats.click(this.loadStats.bind(this));
|
||||
this.$clearStats.click(this.clearStats.bind(this));
|
||||
this.$themeToggle.click(this.toggleTheme.bind(this));
|
||||
|
||||
// initialize theme
|
||||
// Page navigation
|
||||
$('[data-page]').click(function(e) {
|
||||
e.preventDefault();
|
||||
const page = $(this).data('page');
|
||||
self.navigateToPage(page);
|
||||
});
|
||||
|
||||
// Close menu when clicking outside
|
||||
$(document).click(function(e) {
|
||||
if (!$(e.target).closest('.header-nav').length) {
|
||||
self.$menuDropdown.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Collapsible sections
|
||||
$('.collapsible-header').click(function() {
|
||||
const $header = $(this);
|
||||
const $content = $header.next('.collapsible-content');
|
||||
const $icon = $header.find('.toggle-icon');
|
||||
|
||||
$content.slideToggle(300);
|
||||
$icon.toggleClass('expanded');
|
||||
});
|
||||
|
||||
// Initialize theme
|
||||
this.initializeTheme();
|
||||
|
||||
// initialize the application
|
||||
// Initialize the application
|
||||
this.loadToolNames().then(function() {
|
||||
// Load logs on page load after tool names are loaded
|
||||
self.loadLogs();
|
||||
// Start on overview page
|
||||
self.loadConfigOverview();
|
||||
self.startConfigPolling();
|
||||
});
|
||||
}
|
||||
|
||||
toggleMenu() {
|
||||
this.$menuDropdown.toggle();
|
||||
}
|
||||
|
||||
navigateToPage(page) {
|
||||
// Hide menu
|
||||
this.$menuDropdown.hide();
|
||||
|
||||
// Hide all pages
|
||||
$('.page-view').hide();
|
||||
|
||||
// Show selected page
|
||||
$('#page-' + page).show();
|
||||
|
||||
// Update menu active state
|
||||
$('[data-page]').removeClass('active');
|
||||
$('[data-page="' + page + '"]').addClass('active');
|
||||
|
||||
// Update current page
|
||||
this.currentPage = page;
|
||||
|
||||
// Stop all polling
|
||||
this.stopPolling();
|
||||
|
||||
// Start appropriate polling for the page
|
||||
if (page === 'overview') {
|
||||
this.loadConfigOverview();
|
||||
this.startConfigPolling();
|
||||
} else if (page === 'logs') {
|
||||
this.loadLogs();
|
||||
} else if (page === 'stats') {
|
||||
this.loadStats();
|
||||
}
|
||||
}
|
||||
|
||||
stopPolling() {
|
||||
if (this.pollInterval) {
|
||||
clearInterval(this.pollInterval);
|
||||
this.pollInterval = null;
|
||||
}
|
||||
if (this.configPollInterval) {
|
||||
clearInterval(this.configPollInterval);
|
||||
this.configPollInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Config Overview Methods =====
|
||||
|
||||
loadConfigOverview() {
|
||||
let self = this;
|
||||
$.ajax({
|
||||
url: '/get_config_overview',
|
||||
type: 'GET',
|
||||
success: function(response) {
|
||||
self.configData = response;
|
||||
self.displayConfig(response);
|
||||
self.displayBasicStats(response.tool_stats_summary);
|
||||
self.displayProjects(response.registered_projects);
|
||||
self.displayAvailableTools(response.available_tools);
|
||||
self.displayAvailableModes(response.available_modes);
|
||||
self.displayAvailableContexts(response.available_contexts);
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Error loading config overview:', error);
|
||||
self.$configDisplay.html('<div class="error-message">Error loading configuration</div>');
|
||||
self.$basicStatsDisplay.html('<div class="error-message">Error loading stats</div>');
|
||||
self.$projectsDisplay.html('<div class="error-message">Error loading projects</div>');
|
||||
self.$availableToolsDisplay.html('<div class="error-message">Error loading tools</div>');
|
||||
self.$availableModesDisplay.html('<div class="error-message">Error loading modes</div>');
|
||||
self.$availableContextsDisplay.html('<div class="error-message">Error loading contexts</div>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
startConfigPolling() {
|
||||
// Poll every 2 seconds for config updates
|
||||
this.configPollInterval = setInterval(this.loadConfigOverview.bind(this), 2000);
|
||||
}
|
||||
|
||||
displayConfig(config) {
|
||||
// Check if tools section is currently expanded
|
||||
const $existingContent = $('#tools-content');
|
||||
const wasExpanded = $existingContent.is(':visible');
|
||||
|
||||
let html = '<div class="config-grid">';
|
||||
|
||||
// Project info
|
||||
html += '<div class="config-label">Active Project:</div>';
|
||||
html += '<div class="config-value">' + (config.active_project.name || 'None') + '</div>';
|
||||
|
||||
html += '<div class="config-label">Language:</div>';
|
||||
html += '<div class="config-value">' + (config.active_project.language || 'N/A') + '</div>';
|
||||
|
||||
// Context info
|
||||
html += '<div class="config-label">Context:</div>';
|
||||
html += '<div class="config-value">' + config.context.name + '</div>';
|
||||
|
||||
// Modes info
|
||||
html += '<div class="config-label">Active Modes:</div>';
|
||||
html += '<div class="config-value">';
|
||||
if (config.modes.length > 0) {
|
||||
html += '<ul class="config-list">';
|
||||
config.modes.forEach(function(mode) {
|
||||
html += '<li>' + mode.name + '</li>';
|
||||
});
|
||||
html += '</ul>';
|
||||
} else {
|
||||
html += 'None';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
html += '</div>';
|
||||
|
||||
// Active tools - collapsible
|
||||
html += '<div style="margin-top: 20px;">';
|
||||
html += '<h3 class="collapsible-header" id="tools-header" style="font-size: 16px; margin: 0;">';
|
||||
html += '<span>Active Tools (' + config.active_tools.length + ')</span>';
|
||||
html += '<span class="toggle-icon' + (wasExpanded ? ' expanded' : '') + '">▼</span>';
|
||||
html += '</h3>';
|
||||
html += '<div class="collapsible-content tools-grid" id="tools-content" style="' + (wasExpanded ? '' : 'display:none;') + ' margin-top: 10px;">';
|
||||
config.active_tools.forEach(function(tool) {
|
||||
html += '<div class="tool-item" title="' + tool + '">' + tool + '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
this.$configDisplay.html(html);
|
||||
|
||||
// Re-attach collapsible handler for the newly created tools header
|
||||
$('#tools-header').click(function() {
|
||||
const $header = $(this);
|
||||
const $content = $('#tools-content');
|
||||
const $icon = $header.find('.toggle-icon');
|
||||
|
||||
$content.slideToggle(300);
|
||||
$icon.toggleClass('expanded');
|
||||
});
|
||||
}
|
||||
|
||||
displayBasicStats(stats) {
|
||||
if (Object.keys(stats).length === 0) {
|
||||
this.$basicStatsDisplay.html('<div class="no-stats-message">No tool usage stats collected yet.</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort tools by call count (descending)
|
||||
const sortedTools = Object.keys(stats).sort((a, b) => {
|
||||
return stats[b].num_calls - stats[a].num_calls;
|
||||
});
|
||||
|
||||
const maxCalls = Math.max(...sortedTools.map(tool => stats[tool].num_calls));
|
||||
|
||||
let html = '';
|
||||
sortedTools.forEach(function(toolName) {
|
||||
const count = stats[toolName].num_calls;
|
||||
const percentage = maxCalls > 0 ? (count / maxCalls * 100) : 0;
|
||||
|
||||
html += '<div class="stat-bar-container">';
|
||||
html += '<div class="stat-tool-name" title="' + toolName + '">' + toolName + '</div>';
|
||||
html += '<div class="bar-wrapper">';
|
||||
html += '<div class="bar" style="width: ' + percentage + '%"></div>';
|
||||
html += '</div>';
|
||||
html += '<div class="stat-count">' + count + '</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
this.$basicStatsDisplay.html(html);
|
||||
}
|
||||
|
||||
displayProjects(projects) {
|
||||
if (!projects || projects.length === 0) {
|
||||
this.$projectsDisplay.html('<div class="no-stats-message">No projects registered.</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
projects.forEach(function(project) {
|
||||
const activeClass = project.is_active ? ' active' : '';
|
||||
html += '<div class="project-item' + activeClass + '">';
|
||||
html += '<div class="project-name" title="' + project.name + '">' + project.name + '</div>';
|
||||
html += '<div class="project-path" title="' + project.path + '">' + project.path + '</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
this.$projectsDisplay.html(html);
|
||||
}
|
||||
|
||||
displayAvailableTools(tools) {
|
||||
if (!tools || tools.length === 0) {
|
||||
this.$availableToolsDisplay.html('<div class="no-stats-message">All tools are active.</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
tools.forEach(function(tool) {
|
||||
html += '<div class="info-item" title="' + tool.name + '">' + tool.name + '</div>';
|
||||
});
|
||||
|
||||
this.$availableToolsDisplay.html(html);
|
||||
}
|
||||
|
||||
displayAvailableModes(modes) {
|
||||
if (!modes || modes.length === 0) {
|
||||
this.$availableModesDisplay.html('<div class="no-stats-message">No modes available.</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
modes.forEach(function(mode) {
|
||||
const activeClass = mode.is_active ? ' active' : '';
|
||||
html += '<div class="info-item' + activeClass + '" title="' + mode.name + '">' + mode.name + '</div>';
|
||||
});
|
||||
|
||||
this.$availableModesDisplay.html(html);
|
||||
}
|
||||
|
||||
displayAvailableContexts(contexts) {
|
||||
if (!contexts || contexts.length === 0) {
|
||||
this.$availableContextsDisplay.html('<div class="no-stats-message">No contexts available.</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
contexts.forEach(function(context) {
|
||||
const activeClass = context.is_active ? ' active' : '';
|
||||
html += '<div class="info-item' + activeClass + '" title="' + context.name + '">' + context.name + '</div>';
|
||||
});
|
||||
|
||||
this.$availableContextsDisplay.html(html);
|
||||
}
|
||||
|
||||
// ===== Logs Methods =====
|
||||
|
||||
displayLogMessage(message) {
|
||||
$('#log-container').append(new LogMessage(message, this.toolNames).$elem);
|
||||
this.$logContainer.append(new LogMessage(message, this.toolNames).$elem);
|
||||
}
|
||||
|
||||
loadToolNames() {
|
||||
@@ -227,16 +502,7 @@ class Dashboard {
|
||||
this.pollInterval = setInterval(this.pollForNewLogs.bind(this), 1000);
|
||||
}
|
||||
|
||||
toggleStats() {
|
||||
if (this.$statsSection.is(':visible')) {
|
||||
this.$statsSection.hide();
|
||||
this.$toggleStats.text('Show Stats');
|
||||
} else {
|
||||
this.$statsSection.show();
|
||||
this.$toggleStats.text('Hide Stats');
|
||||
this.loadStats();
|
||||
}
|
||||
}
|
||||
// ===== Stats Methods =====
|
||||
|
||||
loadStats() {
|
||||
let self = this;
|
||||
@@ -252,7 +518,6 @@ class Dashboard {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
clearStats() {
|
||||
let self = this;
|
||||
$.ajax({
|
||||
@@ -292,12 +557,12 @@ class Dashboard {
|
||||
const inputTokens = names.map(n => stats[n].input_tokens);
|
||||
const outputTokens = names.map(n => stats[n].output_tokens);
|
||||
const totalTokens = names.map(n => stats[n].input_tokens + stats[n].output_tokens);
|
||||
|
||||
|
||||
// Calculate totals for summary table
|
||||
const totalCalls = counts.reduce((sum, count) => sum + count, 0);
|
||||
const totalInputTokens = inputTokens.reduce((sum, tokens) => sum + tokens, 0);
|
||||
const totalOutputTokens = outputTokens.reduce((sum, tokens) => sum + tokens, 0);
|
||||
|
||||
|
||||
// Generate consistent colors for tools
|
||||
const colors = this.generateColors(names.length);
|
||||
|
||||
@@ -325,16 +590,16 @@ class Dashboard {
|
||||
// Tool calls pie chart
|
||||
this.countChart = new Chart(countCtx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: names,
|
||||
datasets: [{
|
||||
data: {
|
||||
labels: names,
|
||||
datasets: [{
|
||||
data: counts,
|
||||
backgroundColor: colors
|
||||
}]
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
legend: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: {
|
||||
color: textColor
|
||||
@@ -353,16 +618,16 @@ class Dashboard {
|
||||
// Input tokens pie chart
|
||||
this.inputChart = new Chart(inputCtx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: names,
|
||||
datasets: [{
|
||||
data: {
|
||||
labels: names,
|
||||
datasets: [{
|
||||
data: inputTokens,
|
||||
backgroundColor: colors
|
||||
}]
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
legend: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: {
|
||||
color: textColor
|
||||
@@ -381,16 +646,16 @@ class Dashboard {
|
||||
// Output tokens pie chart
|
||||
this.outputChart = new Chart(outputCtx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: names,
|
||||
datasets: [{
|
||||
data: {
|
||||
labels: names,
|
||||
datasets: [{
|
||||
data: outputTokens,
|
||||
backgroundColor: colors
|
||||
}]
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
legend: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: {
|
||||
color: textColor
|
||||
@@ -409,11 +674,11 @@ class Dashboard {
|
||||
// Combined input/output tokens bar chart
|
||||
this.tokensChart = new Chart(tokensCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: names,
|
||||
data: {
|
||||
labels: names,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Input Tokens',
|
||||
{
|
||||
label: 'Input Tokens',
|
||||
data: inputTokens,
|
||||
backgroundColor: colors.map(color => color + '80'), // Semi-transparent
|
||||
borderColor: colors,
|
||||
@@ -421,8 +686,8 @@ class Dashboard {
|
||||
borderSkipped: false,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: 'Output Tokens',
|
||||
{
|
||||
label: 'Output Tokens',
|
||||
data: outputTokens,
|
||||
backgroundColor: colors,
|
||||
yAxisID: 'y1'
|
||||
@@ -452,8 +717,8 @@ class Dashboard {
|
||||
display: true,
|
||||
position: 'left',
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Input Tokens',
|
||||
color: textColor
|
||||
},
|
||||
@@ -469,15 +734,15 @@ class Dashboard {
|
||||
display: true,
|
||||
position: 'right',
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Output Tokens',
|
||||
color: textColor
|
||||
},
|
||||
ticks: {
|
||||
color: textColor
|
||||
},
|
||||
grid: {
|
||||
grid: {
|
||||
drawOnChartArea: false,
|
||||
color: gridColor
|
||||
}
|
||||
@@ -508,10 +773,12 @@ class Dashboard {
|
||||
$('#stats-summary').html(tableHtml);
|
||||
}
|
||||
|
||||
// ===== Theme Methods =====
|
||||
|
||||
initializeTheme() {
|
||||
// Check if user has manually set a theme preference
|
||||
const savedTheme = localStorage.getItem('serena-theme');
|
||||
|
||||
|
||||
if (savedTheme) {
|
||||
// User has manually set a preference, use it
|
||||
this.setTheme(savedTheme);
|
||||
@@ -519,7 +786,7 @@ class Dashboard {
|
||||
// No manual preference, detect system color scheme
|
||||
this.detectSystemTheme();
|
||||
}
|
||||
|
||||
|
||||
// Listen for system theme changes
|
||||
this.setupSystemThemeListener();
|
||||
}
|
||||
@@ -534,7 +801,7 @@ class Dashboard {
|
||||
setupSystemThemeListener() {
|
||||
// Listen for changes in system color scheme
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
|
||||
const handleSystemThemeChange = (e) => {
|
||||
// Only auto-switch if user hasn't manually set a preference
|
||||
const savedTheme = localStorage.getItem('serena-theme');
|
||||
@@ -543,7 +810,7 @@ class Dashboard {
|
||||
this.setTheme(newTheme);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Add listener for system theme changes
|
||||
if (mediaQuery.addEventListener) {
|
||||
mediaQuery.addEventListener('change', handleSystemThemeChange);
|
||||
@@ -556,7 +823,7 @@ class Dashboard {
|
||||
toggleTheme() {
|
||||
const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
|
||||
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||
|
||||
|
||||
// When user manually toggles, save their preference
|
||||
localStorage.setItem('serena-theme', newTheme);
|
||||
this.setTheme(newTheme);
|
||||
@@ -565,8 +832,8 @@ class Dashboard {
|
||||
setTheme(theme) {
|
||||
// Set the theme on the document element
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
|
||||
// Update the toggle button
|
||||
|
||||
// Update the theme toggle button
|
||||
if (theme === 'dark') {
|
||||
this.$themeIcon.text('☀️');
|
||||
this.$themeText.text('Light');
|
||||
@@ -574,13 +841,13 @@ class Dashboard {
|
||||
this.$themeIcon.text('🌙');
|
||||
this.$themeText.text('Dark');
|
||||
}
|
||||
|
||||
|
||||
// Update the logo based on theme
|
||||
this.updateLogo(theme);
|
||||
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('serena-theme', theme);
|
||||
|
||||
|
||||
// Update charts if they exist
|
||||
this.updateChartsTheme();
|
||||
}
|
||||
@@ -600,43 +867,47 @@ class Dashboard {
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
const textColor = isDark ? '#ffffff' : '#000000';
|
||||
const gridColor = isDark ? '#444' : '#ddd';
|
||||
|
||||
// Update existing charts
|
||||
if (this.countChart) {
|
||||
this.countChart.options.scales.x.ticks.color = textColor;
|
||||
this.countChart.options.scales.y.ticks.color = textColor;
|
||||
this.countChart.options.scales.x.grid.color = gridColor;
|
||||
this.countChart.options.scales.y.grid.color = gridColor;
|
||||
|
||||
// Update existing charts if they exist and have the scales property
|
||||
if (this.countChart && this.countChart.options.plugins) {
|
||||
if (this.countChart.options.plugins.legend) {
|
||||
this.countChart.options.plugins.legend.labels.color = textColor;
|
||||
}
|
||||
this.countChart.update();
|
||||
}
|
||||
|
||||
if (this.inputChart) {
|
||||
this.inputChart.options.scales.x.ticks.color = textColor;
|
||||
this.inputChart.options.scales.y.ticks.color = textColor;
|
||||
this.inputChart.options.scales.x.grid.color = gridColor;
|
||||
this.inputChart.options.scales.y.grid.color = gridColor;
|
||||
|
||||
if (this.inputChart && this.inputChart.options.plugins) {
|
||||
if (this.inputChart.options.plugins.legend) {
|
||||
this.inputChart.options.plugins.legend.labels.color = textColor;
|
||||
}
|
||||
this.inputChart.update();
|
||||
}
|
||||
|
||||
if (this.outputChart) {
|
||||
this.outputChart.options.scales.x.ticks.color = textColor;
|
||||
this.outputChart.options.scales.y.ticks.color = textColor;
|
||||
this.outputChart.options.scales.x.grid.color = gridColor;
|
||||
this.outputChart.options.scales.y.grid.color = gridColor;
|
||||
|
||||
if (this.outputChart && this.outputChart.options.plugins) {
|
||||
if (this.outputChart.options.plugins.legend) {
|
||||
this.outputChart.options.plugins.legend.labels.color = textColor;
|
||||
}
|
||||
this.outputChart.update();
|
||||
}
|
||||
|
||||
if (this.tokensChart) {
|
||||
|
||||
if (this.tokensChart && this.tokensChart.options.scales) {
|
||||
this.tokensChart.options.scales.x.ticks.color = textColor;
|
||||
this.tokensChart.options.scales.y.ticks.color = textColor;
|
||||
this.tokensChart.options.scales.y1.ticks.color = textColor;
|
||||
this.tokensChart.options.scales.x.grid.color = gridColor;
|
||||
this.tokensChart.options.scales.y.grid.color = gridColor;
|
||||
this.tokensChart.options.scales.y1.grid.color = gridColor;
|
||||
this.tokensChart.options.scales.y.title.color = textColor;
|
||||
this.tokensChart.options.scales.y1.title.color = textColor;
|
||||
if (this.tokensChart.options.plugins && this.tokensChart.options.plugins.legend) {
|
||||
this.tokensChart.options.plugins.legend.labels.color = textColor;
|
||||
}
|
||||
this.tokensChart.update();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Shutdown Method =====
|
||||
|
||||
shutdown() {
|
||||
const self = this;
|
||||
const _shutdown = function () {
|
||||
@@ -658,5 +929,8 @@ class Dashboard {
|
||||
} else {
|
||||
console.log("Shutdown cancelled");
|
||||
}
|
||||
|
||||
// Close menu
|
||||
self.$menuDropdown.hide();
|
||||
}
|
||||
}
|
||||
@@ -8,271 +8,112 @@
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="serena-icon-16.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="serena-icon-32.png">
|
||||
<link rel="icon" type="image/png" sizes="48x48" href="serena-icon-48.png">
|
||||
<link rel="stylesheet" href="dashboard.css">
|
||||
<script src="jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2"></script>
|
||||
<script src="dashboard.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
/* Light theme variables */
|
||||
--bg-primary: #f5f5f5;
|
||||
--bg-secondary: #ffffff;
|
||||
--text-primary: #000000;
|
||||
--text-secondary: #333333;
|
||||
--text-muted: #666666;
|
||||
--border-color: #ddd;
|
||||
--btn-primary: #eaa45d;
|
||||
--btn-hover: #dca662;
|
||||
--btn-disabled: #6c757d;
|
||||
--shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
--tool-highlight: #ffff00;
|
||||
--tool-highlight-text: #000000;
|
||||
--log-debug: #808080;
|
||||
--log-info: #000000;
|
||||
--log-warning: #FF8C00;
|
||||
--log-error: #FF0000;
|
||||
--stats-header: #f8f9fa;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
/* Dark theme variables */
|
||||
--bg-primary: #1a1a1a;
|
||||
--bg-secondary: #2d2d2d;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #e0e0e0;
|
||||
--text-muted: #b0b0b0;
|
||||
--border-color: #444;
|
||||
--btn-primary: #eaa45d;
|
||||
--btn-hover: #dca662;
|
||||
--btn-disabled: #6c757d;
|
||||
--shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
--tool-highlight: #ffd700;
|
||||
--tool-highlight-text: #000000;
|
||||
--log-debug: #808080;
|
||||
--log-info: #ffffff;
|
||||
--log-warning: #FF8C00;
|
||||
--log-error: #FF0000;
|
||||
--stats-header: #3a3a3a;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
background-color: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 5px;
|
||||
height: 600px;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
padding: 10px;
|
||||
white-space: pre-wrap;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: var(--btn-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: var(--btn-hover);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background-color: var(--btn-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background-color: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.theme-toggle .icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.log-debug {
|
||||
color: var(--log-debug);
|
||||
}
|
||||
|
||||
.log-info {
|
||||
color: var(--log-info);
|
||||
}
|
||||
|
||||
.log-warning {
|
||||
color: var(--log-warning);
|
||||
}
|
||||
|
||||
.log-error {
|
||||
color: var(--log-error);
|
||||
}
|
||||
|
||||
.log-default {
|
||||
color: var(--log-info);
|
||||
}
|
||||
|
||||
/* Tool name highlighting */
|
||||
.tool-name {
|
||||
background-color: var(--tool-highlight);
|
||||
color: var(--tool-highlight-text);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: var(--log-error);
|
||||
text-align: center;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.charts-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
justify-content: space-between;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.chart-group {
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
max-width: 320px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chart-wide {
|
||||
flex: 0 0 100%;
|
||||
min-width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.chart-group h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.stats-summary {
|
||||
margin: 0 auto;
|
||||
border-collapse: collapse;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
transition: background-color 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.stats-summary th,
|
||||
.stats-summary td {
|
||||
padding: 10px 20px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
transition: border-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.stats-summary th {
|
||||
background-color: var(--stats-header);
|
||||
font-weight: bold;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.stats-summary tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.charts-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chart-group,
|
||||
.chart-wide {
|
||||
min-width: auto;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="header">
|
||||
<img id="serena-logo" src="serena-logs.png" alt="Serena" style="max-width: 400px; height: auto;">
|
||||
</div>
|
||||
<header class="header">
|
||||
<div class="logo-container">
|
||||
<img id="serena-logo" src="serena-logs.png" alt="Serena">
|
||||
</div>
|
||||
<nav class="header-nav">
|
||||
<button id="theme-toggle" class="theme-toggle">
|
||||
<span id="theme-icon">🌙</span>
|
||||
<span id="theme-text">Dark</span>
|
||||
</button>
|
||||
<button id="menu-toggle" class="menu-button">
|
||||
<span>☰</span>
|
||||
<span>Menu</span>
|
||||
</button>
|
||||
<div id="menu-dropdown" class="menu-dropdown" style="display:none">
|
||||
<a href="#" data-page="overview" class="active">Overview</a>
|
||||
<a href="#" data-page="logs">Logs</a>
|
||||
<a href="#" data-page="stats">Advanced Stats</a>
|
||||
<hr>
|
||||
<a href="#" id="menu-shutdown">Shutdown Server</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="controls">
|
||||
<button id="load-logs" class="btn">Reload Log</button>
|
||||
<button id="shutdown" class="btn">Shutdown Server</button>
|
||||
<button id="toggle-stats" class="btn">Show Stats</button>
|
||||
<div id="theme-toggle" class="theme-toggle" title="Toggle theme">
|
||||
<span class="icon" id="theme-icon">🌙</span>
|
||||
<span id="theme-text">Dark</span>
|
||||
<!-- Overview Page (Landing) -->
|
||||
<div id="page-overview" class="page-view">
|
||||
<div class="overview-container">
|
||||
<div class="overview-left">
|
||||
<section class="config-section">
|
||||
<h2>Current Configuration</h2>
|
||||
<div id="config-display">
|
||||
<div class="loading">Loading configuration...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="basic-stats-section">
|
||||
<h2>Tool Usage</h2>
|
||||
<div id="basic-stats-display">
|
||||
<div class="loading">Loading stats...</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="overview-right">
|
||||
<section class="projects-section">
|
||||
<h2 class="collapsible-header" id="projects-header">
|
||||
<span>Registered Projects</span>
|
||||
<span class="toggle-icon">▼</span>
|
||||
</h2>
|
||||
<div id="projects-display" class="collapsible-content" style="display:none">
|
||||
<div class="loading">Loading projects...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="projects-section">
|
||||
<h2 class="collapsible-header" id="available-tools-header">
|
||||
<span>Available Tools (Disabled)</span>
|
||||
<span class="toggle-icon">▼</span>
|
||||
</h2>
|
||||
<div id="available-tools-display" class="collapsible-content" style="display:none">
|
||||
<div class="loading">Loading tools...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="projects-section">
|
||||
<h2 class="collapsible-header" id="available-modes-header">
|
||||
<span>Available Modes</span>
|
||||
<span class="toggle-icon">▼</span>
|
||||
</h2>
|
||||
<div id="available-modes-display" class="collapsible-content" style="display:none">
|
||||
<div class="loading">Loading modes...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="projects-section">
|
||||
<h2 class="collapsible-header" id="available-contexts-header">
|
||||
<span>Available Contexts</span>
|
||||
<span class="toggle-icon">▼</span>
|
||||
</h2>
|
||||
<div id="available-contexts-display" class="collapsible-content" style="display:none">
|
||||
<div class="loading">Loading contexts...</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="error-container"></div>
|
||||
<div id="log-container" class="log-container"></div>
|
||||
<!-- Logs Page -->
|
||||
<div id="page-logs" class="page-view" style="display:none">
|
||||
<div class="controls">
|
||||
<button id="load-logs" class="btn">Reload Log</button>
|
||||
</div>
|
||||
<div id="error-container"></div>
|
||||
<div id="log-container" class="log-container"></div>
|
||||
</div>
|
||||
|
||||
<div id="stats-section" style="display:none; margin-top:20px;">
|
||||
<div style="text-align:center; margin-bottom:20px;">
|
||||
<!-- Advanced Stats Page -->
|
||||
<div id="page-stats" class="page-view" style="display:none">
|
||||
<div class="controls">
|
||||
<button id="refresh-stats" class="btn">Refresh Stats</button>
|
||||
<button id="clear-stats" class="btn">Clear Stats</button>
|
||||
</div>
|
||||
@@ -280,10 +121,9 @@
|
||||
<div id="stats-summary" style="margin-bottom:20px; text-align:center;"></div>
|
||||
<div id="estimator-name" style="text-align:center; margin-bottom:10px;"></div>
|
||||
<div id="no-stats-message" style="text-align:center; color:var(--text-muted); font-style:italic; display:none;">
|
||||
No tool stats collected. Have you enabled tool stats collection in the configuration?
|
||||
No tool stats collected yet.
|
||||
</div>
|
||||
|
||||
|
||||
<div class="charts-container">
|
||||
<div class="chart-group">
|
||||
<h3>Tool Calls</h3>
|
||||
@@ -311,4 +151,4 @@
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -61,16 +61,14 @@ default_max_tool_answer_chars: 150000
|
||||
# Even though the value of the max_answer_chars can be changed when calling the tool, it may make sense to adjust this default
|
||||
# through the global configuration.
|
||||
|
||||
record_tool_usage_stats: False
|
||||
# whether to record tool usage statistics, they will be shown in the web dashboard if recording is active.
|
||||
|
||||
token_count_estimator: TIKTOKEN_GPT4O
|
||||
# Only relevant if `record_tool_usage` is True; the name of the token count estimator to use for tool usage statistics.
|
||||
token_count_estimator: CHAR_COUNT
|
||||
# the name of the token count estimator to use for tool usage statistics.
|
||||
# See the `RegisteredTokenCountEstimator` enum for available options.
|
||||
#
|
||||
# Note: some token estimators (like tiktoken) may require downloading data files
|
||||
# on the first run, which can take some time and require internet access. Others, like the Anthropic ones, may require an API key
|
||||
# and rate limits may apply.
|
||||
# By default, a very naive character count estimator is used, which simply counts the number of characters.
|
||||
# You can configure this to TIKTOKEN_GPT4 to use a local tiktoken-based estimator for GPT-4 (will download tiktoken
|
||||
# data files on first run), or ANTHROPIC_CLAUDE_SONNET_4 which will use the (free of cost) Anthropic API to
|
||||
# estimate the token count using the Claude Sonnet 4 tokenizer.
|
||||
|
||||
|
||||
# MANAGED BY SERENA, KEEP AT THE BOTTOM OF THE YAML AND DON'T EDIT WITHOUT NEED
|
||||
|
||||
@@ -260,7 +260,7 @@ class Tool(Component):
|
||||
raise
|
||||
|
||||
# record tool usage
|
||||
self.agent.record_tool_usage_if_enabled(kwargs, result, self)
|
||||
self.agent.record_tool_usage(kwargs, result, self)
|
||||
|
||||
except Exception as e:
|
||||
if not catch_exceptions:
|
||||
|
||||
Reference in new issue
Block a user