Add tool rating & review from My Tools page

- Server: add GET /api/v1/tools/<owner>/<name>/my-review endpoint
- Client: add get_tool_rating, get_my_review, submit/update/delete review methods
- GUI: new ReviewDialog with star selector, title, content, submit/update/delete
- Tools page: rating display in details panel, Rate/Edit Rating button with
  context-sensitive enable/disable, background workers, rating cache
- Fix qualified name usage throughout tools page for owner-prefixed tools

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
rob
2026-01-30 02:15:23 -04:00
co-authored by Claude Opus 4.5
parent b00115a52e
commit 14b3f3d856
4 changed files with 756 additions and 8 deletions
+206
View File
@@ -0,0 +1,206 @@
"""Review dialog for rating registry tools."""
from typing import Optional, Dict
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QLineEdit, QPlainTextEdit, QMessageBox
)
from PySide6.QtCore import Qt
class ReviewDialog(QDialog):
"""Dialog for submitting or editing a tool review."""
# Set when user clicks Delete - caller checks this after reject()
delete_requested = False
def __init__(
self,
parent,
owner: str,
name: str,
existing_review: Optional[Dict] = None
):
super().__init__(parent)
self._owner = owner
self._name = name
self._existing = existing_review
self._rating = existing_review["rating"] if existing_review else 0
self.delete_requested = False
self._setup_ui()
def _setup_ui(self):
editing = self._existing is not None
self.setWindowTitle(f"{'Edit Review' if editing else 'Rate Tool'} - {self._owner}/{self._name}")
self.setMinimumSize(420, 380)
layout = QVBoxLayout(self)
layout.setSpacing(14)
# Title
title = QLabel(f"{'Edit your review' if editing else 'Rate'} {self._owner}/{self._name}")
title.setStyleSheet("font-size: 15px; font-weight: 600;")
layout.addWidget(title)
# Star selector
star_label = QLabel("Rating:")
star_label.setStyleSheet("font-weight: 500;")
layout.addWidget(star_label)
star_row = QHBoxLayout()
star_row.setSpacing(4)
self._star_buttons = []
for i in range(1, 6):
btn = QPushButton("☆")
btn.setFixedSize(36, 36)
btn.setStyleSheet("""
QPushButton {
font-size: 20px;
border: 1px solid #e2e8f0;
border-radius: 4px;
background: #f7fafc;
}
QPushButton:hover {
background: #edf2f7;
}
""")
btn.setCursor(Qt.PointingHandCursor)
btn.clicked.connect(lambda checked, n=i: self._set_rating(n))
self._star_buttons.append(btn)
star_row.addWidget(btn)
star_row.addStretch()
layout.addLayout(star_row)
# Pre-fill rating
if self._rating:
self._set_rating(self._rating)
# Title input
title_label = QLabel("Title (optional):")
title_label.setStyleSheet("font-weight: 500;")
layout.addWidget(title_label)
self.title_input = QLineEdit()
self.title_input.setMaxLength(100)
self.title_input.setPlaceholderText("Brief summary of your review")
self.title_input.setMinimumHeight(28)
if self._existing and self._existing.get("title"):
self.title_input.setText(self._existing["title"])
layout.addWidget(self.title_input)
# Content input
content_label = QLabel("Review (optional, min 10 chars if provided):")
content_label.setStyleSheet("font-weight: 500;")
layout.addWidget(content_label)
self.content_input = QPlainTextEdit()
self.content_input.setPlaceholderText("Share your experience with this tool...")
self.content_input.setMinimumHeight(100)
self.content_input.setMaximumHeight(180)
if self._existing and self._existing.get("content"):
self.content_input.setPlainText(self._existing["content"])
layout.addWidget(self.content_input)
# Character count
self._char_count = QLabel("0 / 2000")
self._char_count.setStyleSheet("color: #718096; font-size: 11px;")
self._char_count.setAlignment(Qt.AlignRight)
self.content_input.textChanged.connect(self._update_char_count)
self._update_char_count()
layout.addWidget(self._char_count)
layout.addStretch()
# Buttons
btn_layout = QHBoxLayout()
if editing:
self.btn_delete = QPushButton("Delete Review")
self.btn_delete.setObjectName("danger")
self.btn_delete.clicked.connect(self._on_delete)
btn_layout.addWidget(self.btn_delete)
btn_layout.addStretch()
btn_cancel = QPushButton("Cancel")
btn_cancel.setObjectName("secondary")
btn_cancel.clicked.connect(self.reject)
btn_layout.addWidget(btn_cancel)
submit_text = "Update" if editing else "Submit"
self.btn_submit = QPushButton(submit_text)
self.btn_submit.clicked.connect(self._on_submit)
btn_layout.addWidget(self.btn_submit)
layout.addLayout(btn_layout)
def _set_rating(self, n: int):
self._rating = n
for i, btn in enumerate(self._star_buttons):
if i < n:
btn.setText("★")
btn.setStyleSheet("""
QPushButton {
font-size: 20px;
border: 1px solid #d69e2e;
border-radius: 4px;
background: #fefcbf;
color: #d69e2e;
}
QPushButton:hover { background: #fef3c7; }
""")
else:
btn.setText("☆")
btn.setStyleSheet("""
QPushButton {
font-size: 20px;
border: 1px solid #e2e8f0;
border-radius: 4px;
background: #f7fafc;
}
QPushButton:hover { background: #edf2f7; }
""")
def _update_char_count(self):
count = len(self.content_input.toPlainText())
self._char_count.setText(f"{count} / 2000")
if count > 2000:
self._char_count.setStyleSheet("color: #e53e3e; font-size: 11px;")
else:
self._char_count.setStyleSheet("color: #718096; font-size: 11px;")
def _on_submit(self):
if self._rating < 1:
QMessageBox.warning(self, "Validation", "Please select a rating (1-5 stars).")
return
content = self.content_input.toPlainText().strip()
if content and len(content) < 10:
QMessageBox.warning(self, "Validation", "Review content must be at least 10 characters.")
return
if len(content) > 2000:
QMessageBox.warning(self, "Validation", "Review content must be 2000 characters or less.")
return
self.accept()
def _on_delete(self):
reply = QMessageBox.question(
self, "Delete Review",
"Are you sure you want to delete your review?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
self.delete_requested = True
self.reject()
def get_review_data(self) -> Dict:
"""Get the review data entered by the user."""
return {
"rating": self._rating,
"title": self.title_input.text().strip(),
"content": self.content_input.toPlainText().strip()[:2000],
}