mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 18:16:10 +00:00
Adding news support in dashboard, WIP
This commit is contained in:
1 parent
eb54e834b6
commit
8f89bc32b1
6 files changed
+251
-2
No files matched your search
@@ -80,6 +80,10 @@ class SerenaPaths:
|
||||
If a name of a mode matches a name of a mode in SERENAS_OWN_MODES_YAML_DIR,
|
||||
the user mode will override the default mode definition.
|
||||
"""
|
||||
self.news_snippet_id_file: str = os.path.join(self.serena_user_home_dir, "last_read_news_snippet_id.txt")
|
||||
"""
|
||||
file containing the ID of the last read news snippet
|
||||
"""
|
||||
|
||||
def get_next_log_file_path(self, prefix: str) -> str:
|
||||
"""
|
||||
|
||||
+33
-2
@@ -2,6 +2,7 @@ import os
|
||||
import socket
|
||||
import threading
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Self
|
||||
|
||||
from flask import Flask, Response, request, send_from_directory
|
||||
@@ -9,8 +10,8 @@ from pydantic import BaseModel
|
||||
from sensai.util import logging
|
||||
|
||||
from serena.analytics import ToolUsageStats
|
||||
from serena.config.serena_config import LanguageBackend
|
||||
from serena.constants import SERENA_DASHBOARD_DIR
|
||||
from serena.config.serena_config import LanguageBackend, SerenaPaths
|
||||
from serena.constants import REPO_ROOT, SERENA_DASHBOARD_DIR
|
||||
from serena.task_executor import TaskExecutor
|
||||
from serena.util.logging import MemoryLogHandler
|
||||
|
||||
@@ -317,6 +318,36 @@ class SerenaDashboardAPI:
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
@self._app.route("/news_snippet_ids", methods=["GET"])
|
||||
def get_news_snippet_ids() -> dict[str, str | list[int]]:
|
||||
def _get_unread_news_ids() -> list[int]:
|
||||
all_news_files = (Path(REPO_ROOT) / "src" / "serena" / "resources" / "dashboard" / "news").glob("*.html")
|
||||
all_news_ids = sorted([int(f.stem) for f in all_news_files])
|
||||
news_snippet_id_file = SerenaPaths().news_snippet_id_file
|
||||
if not os.path.exists(news_snippet_id_file):
|
||||
return all_news_ids
|
||||
with open(news_snippet_id_file, encoding="utf-8") as f:
|
||||
last_read_news_id = int(f.read().strip())
|
||||
return [news_id for news_id in all_news_ids if news_id > last_read_news_id]
|
||||
|
||||
try:
|
||||
unread_news_ids = _get_unread_news_ids()
|
||||
return {"news_snippet_ids": unread_news_ids, "status": "success"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
@self._app.route("/mark_news_snippet_as_read", methods=["POST"])
|
||||
def mark_news_snippet_as_read() -> dict[str, str]:
|
||||
try:
|
||||
request_data = request.get_json()
|
||||
news_snippet_id = int(request_data.get("news_snippet_id"))
|
||||
news_snippet_id_file = SerenaPaths().news_snippet_id_file
|
||||
with open(news_snippet_id_file, "w", encoding="utf-8") as f:
|
||||
f.write(str(news_snippet_id))
|
||||
return {"status": "success", "message": f"Marked news snippet {news_snippet_id} as read"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
def _get_log_messages(self, request_log: RequestLog) -> ResponseLog:
|
||||
all_messages = self._memory_log_handler.get_log_messages()
|
||||
requested_messages = all_messages[request_log.start_idx :] if request_log.start_idx <= len(all_messages) else []
|
||||
|
||||
@@ -49,6 +49,96 @@ html {
|
||||
--stats-header: #3a3a3a;
|
||||
}
|
||||
|
||||
.news-section {
|
||||
background: var(--bg-secondary);
|
||||
padding: 20px;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow);
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.news-section h2 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 18px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.news-item {
|
||||
padding: 20px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 15px;
|
||||
background: var(--bg-primary);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.news-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.news-item h3 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 16px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.news-item .date {
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
margin: 0 0 15px 0;
|
||||
}
|
||||
|
||||
.news-item p {
|
||||
margin: 10px 0;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.news-item ul {
|
||||
margin: 10px 0;
|
||||
padding-left: 25px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.news-item ul li {
|
||||
margin: 8px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.news-item strong {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.news-mark-read {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.news-mark-read:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.news-mark-read input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.news-no-items {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
margin: 0;
|
||||
|
||||
@@ -268,6 +268,8 @@ class Dashboard {
|
||||
this.$editSerenaConfigSaveBtn = $('#edit-serena-config-save-btn');
|
||||
this.$editSerenaConfigCancelBtn = $('#edit-serena-config-cancel-btn');
|
||||
this.$modalCloseEditSerenaConfig = $('.modal-close-edit-serena-config');
|
||||
this.$newsSection = $('#news-section');
|
||||
this.$newsDisplay = $('#news-display');
|
||||
|
||||
// Chart references
|
||||
this.countChart = null;
|
||||
@@ -401,6 +403,7 @@ class Dashboard {
|
||||
// Initialize the application
|
||||
this.loadToolNames().then(function () {
|
||||
// Start on overview page
|
||||
self.loadNews();
|
||||
self.loadConfigOverview();
|
||||
self.startConfigPolling();
|
||||
self.startExecutionsPolling();
|
||||
@@ -454,6 +457,7 @@ class Dashboard {
|
||||
|
||||
// Start appropriate polling for the page
|
||||
if (page === 'overview') {
|
||||
this.loadNews();
|
||||
this.loadConfigOverview();
|
||||
this.startConfigPolling();
|
||||
this.startExecutionsPolling();
|
||||
@@ -1877,6 +1881,105 @@ class Dashboard {
|
||||
});
|
||||
}
|
||||
|
||||
// ===== News Methods =====
|
||||
|
||||
loadNews() {
|
||||
let self = this;
|
||||
console.log('Loading news...');
|
||||
$.ajax({
|
||||
url: '/news_snippet_ids',
|
||||
type: 'GET',
|
||||
success: function(response) {
|
||||
console.log('News snippet IDs response:', response);
|
||||
if (response.status === 'success' && response.news_snippet_ids && response.news_snippet_ids.length > 0) {
|
||||
console.log('Displaying news with IDs:', response.news_snippet_ids);
|
||||
self.displayNews(response.news_snippet_ids);
|
||||
} else {
|
||||
console.log('No unread news, hiding section');
|
||||
self.$newsSection.hide();
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Error loading news snippet IDs:', error);
|
||||
self.$newsSection.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
displayNews(newsIds) {
|
||||
let self = this;
|
||||
console.log('displayNews called with:', newsIds);
|
||||
// Sort newest first (descending order)
|
||||
newsIds.sort((a, b) => b - a);
|
||||
|
||||
if (newsIds.length === 0) {
|
||||
console.log('No news items to display.');
|
||||
self.$newsSection.hide();
|
||||
return;
|
||||
}
|
||||
self.$newsSection.show();
|
||||
self.$newsDisplay.empty();
|
||||
console.log('Displaying ' + newsIds.length + ' news items.');
|
||||
// Load each news snippet HTML
|
||||
let loadedCount = 0;
|
||||
newsIds.forEach(function(newsId) {
|
||||
$.ajax({
|
||||
url: '/dashboard/news/' + newsId + '.html',
|
||||
type: 'GET',
|
||||
success: function(html) {
|
||||
// Wrap the HTML in a container with a checkbox
|
||||
let $newsContainer = $('<div class="news-container">').attr('data-news-id', newsId);
|
||||
let $newsContent = $(html);
|
||||
|
||||
// Add checkbox for marking as read
|
||||
let $markRead = $('<div class="news-mark-read">');
|
||||
let $checkbox = $('<input type="checkbox">').attr('id', 'news-read-' + newsId);
|
||||
let $label = $('<label>').attr('for', 'news-read-' + newsId).text('Mark as read');
|
||||
|
||||
$markRead.append($checkbox).append($label);
|
||||
$newsContent.append($markRead);
|
||||
|
||||
$newsContainer.append($newsContent);
|
||||
self.$newsDisplay.append($newsContainer);
|
||||
|
||||
// Bind checkbox event
|
||||
$checkbox.on('change', function() {
|
||||
if ($(this).is(':checked')) {
|
||||
self.markNewsAsRead(newsId);
|
||||
}
|
||||
});
|
||||
|
||||
loadedCount++;
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Error loading news snippet ' + newsId + ':', error);
|
||||
loadedCount++;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
markNewsAsRead(newsId) {
|
||||
let self = this;
|
||||
$.ajax({
|
||||
url: '/mark_news_snippet_as_read',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ news_snippet_id: newsId }),
|
||||
success: function(response) {
|
||||
if (response.status === 'success') {
|
||||
// Reload news to show updated list
|
||||
self.loadNews();
|
||||
} else {
|
||||
console.error('Error marking news as read:', response.message);
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Error marking news as read:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Serena Config Editing Methods =====
|
||||
|
||||
openEditSerenaConfigModal() {
|
||||
|
||||
@@ -55,6 +55,13 @@
|
||||
<div class="overview-container">
|
||||
<div class="overview-left">
|
||||
|
||||
<section id="news-section" class="news-section" style="display:none">
|
||||
<h2>What's New</h2>
|
||||
<div id="news-display">
|
||||
<div class="loading">Loading news...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="config-section">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
|
||||
<h2 style="margin: 0;">Current Configuration</h2>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="news-item">
|
||||
<h3>New Type Hierarchy and Documentation Tools</h3>
|
||||
<p class="date">January 9, 2026</p>
|
||||
<p>
|
||||
We're excited to announce the introduction of powerful new tools for code navigation and understanding:
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Type Hierarchy Tool:</strong> Navigate and explore type relationships in your codebase with ease. View superclasses, subclasses, and implementation hierarchies.</li>
|
||||
<li><strong>Documentation Tools:</strong> Access comprehensive documentation directly from your code, including API references, examples, and usage patterns.</li>
|
||||
</ul>
|
||||
<p>
|
||||
These tools enhance Serena's code intelligence capabilities, making it easier to understand complex codebases and navigate large projects efficiently.
|
||||
</p>
|
||||
</div>
|
||||
Reference in new issue
Block a user