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:
@@ -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],
|
||||
}
|
||||
Reference in New Issue
Block a user