Skip to content

Commit 95890e8

Browse files
gblanc-1aclaude
andcommitted
feat(ui): replace Rate & Feedback button with interactive stars
Replace the "Rate & Feedback" button in the bundle detail panel with interactive hover/click stars and inline comment form. Feedback is submitted directly to EngagementService without VS Code dialogs. Both the marketplace card and bundle detail views now use the same inline star rating pattern. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 970bd1b commit 95890e8

File tree

4 files changed

+301
-28
lines changed

4 files changed

+301
-28
lines changed

src/ui/MarketplaceViewProvider.ts

Lines changed: 80 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,81 @@ export class MarketplaceViewProvider implements vscode.WebviewViewProvider {
741741
}
742742
}
743743

744+
/**
745+
* Handle feedback from the bundle detail panel's interactive stars.
746+
* Submits directly to EngagementService, sends result back to the detail panel.
747+
*/
748+
private async handleBundleDetailFeedback(
749+
panel: vscode.WebviewPanel,
750+
bundle: Bundle,
751+
bundleId: string,
752+
rating: number,
753+
comment?: string
754+
): Promise<void> {
755+
try {
756+
const sources = await this.registryManager.listSources();
757+
const source = sources.find(s => s.id === bundle.sourceId);
758+
const ratingScore = rating as RatingScore;
759+
const feedbackComment = comment || `Rated ${rating} stars`;
760+
761+
const engagementService = EngagementService.getInstance();
762+
let synced = false;
763+
764+
if (engagementService.initialized) {
765+
try {
766+
await engagementService.submitFeedback(
767+
'bundle', bundleId, feedbackComment,
768+
{ version: bundle.version, rating: ratingScore, hubId: source?.hubId || undefined }
769+
);
770+
synced = true;
771+
} catch (err) {
772+
this.logger.warn(`Failed to submit feedback to remote: ${err}`);
773+
}
774+
}
775+
776+
try {
777+
const storage = engagementService.getStorage?.();
778+
if (storage) {
779+
await storage.savePendingFeedback({
780+
id: crypto.randomUUID(),
781+
bundleId,
782+
sourceId: bundle.sourceId || bundleId,
783+
hubId: source?.hubId || '',
784+
resourceType: 'bundle',
785+
rating: ratingScore,
786+
comment: comment || undefined,
787+
timestamp: new Date().toISOString(),
788+
synced,
789+
});
790+
}
791+
} catch {
792+
this.logger.error('Failed to save pending feedback locally');
793+
}
794+
795+
try {
796+
const ratingCache = RatingCache.getInstance();
797+
ratingCache.applyOptimisticRating(bundle.sourceId, bundleId, ratingScore);
798+
} catch {
799+
// non-critical
800+
}
801+
802+
panel.webview.postMessage({
803+
type: 'feedbackSubmitted',
804+
bundleId,
805+
rating,
806+
synced,
807+
success: true,
808+
});
809+
} catch (error) {
810+
this.logger.error(`Failed to process detail panel feedback: ${error}`);
811+
panel.webview.postMessage({
812+
type: 'feedbackSubmitted',
813+
bundleId,
814+
success: false,
815+
});
816+
}
817+
}
818+
744819
/**
745820
* Open a prompt file in the editor
746821
*/
@@ -1071,21 +1146,11 @@ export class MarketplaceViewProvider implements vscode.WebviewViewProvider {
10711146
const newStatus = await this.registryManager.autoUpdateService?.isAutoUpdateEnabled(installed.bundleId) || false;
10721147
panel.webview.postMessage({ type: 'autoUpdateStatusChanged', enabled: newStatus });
10731148
}
1074-
} else if (message.type === 'feedback' || message.type === 'submitFeedback' || message.type === 'quickFeedback') {
1075-
// Execute the unified feedback command with bundle info
1076-
// Get source info for issue redirect and hub routing
1077-
const sources = await this.registryManager.listSources();
1078-
const source = sources.find(s => s.id === bundle.sourceId);
1079-
1080-
await vscode.commands.executeCommand('promptRegistry.feedback', {
1081-
resourceId: message.bundleId,
1082-
resourceType: 'bundle',
1083-
name: bundle.name,
1084-
version: bundle.version,
1085-
sourceUrl: source?.url || '',
1086-
sourceType: source?.type || '',
1087-
hubId: source?.hubId || ''
1088-
});
1149+
} else if (message.type === 'submitFeedback' && message.rating) {
1150+
// Handle inline feedback submission directly (no VS Code dialogs)
1151+
await this.handleBundleDetailFeedback(
1152+
panel, bundle, message.bundleId, message.rating, message.comment
1153+
);
10891154
}
10901155
},
10911156
undefined,

src/ui/webview/bundleDetails/bundleDetails.css

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,3 +298,104 @@ code {
298298
color: var(--vscode-descriptionForeground);
299299
font-style: italic;
300300
}
301+
302+
/* Interactive star rating */
303+
.interactive-stars {
304+
display: inline-flex;
305+
align-items: center;
306+
gap: 4px;
307+
}
308+
309+
.interactive-stars .star {
310+
font-size: 24px;
311+
color: var(--vscode-descriptionForeground);
312+
transition: color 0.1s ease;
313+
cursor: pointer;
314+
user-select: none;
315+
}
316+
317+
.interactive-stars .star.filled {
318+
color: #ffa500;
319+
}
320+
321+
.interactive-stars .star.hovered {
322+
color: #ffc966;
323+
}
324+
325+
.interactive-stars .star-label {
326+
font-size: 13px;
327+
color: var(--vscode-descriptionForeground);
328+
margin-left: 8px;
329+
}
330+
331+
/* Inline feedback form */
332+
.inline-feedback-form {
333+
display: none;
334+
margin-top: 12px;
335+
padding: 12px;
336+
background: var(--vscode-editor-background);
337+
border: 1px solid var(--vscode-input-border);
338+
border-radius: 4px;
339+
}
340+
341+
.inline-feedback-form.visible {
342+
display: block;
343+
}
344+
345+
.inline-feedback-form textarea {
346+
width: 100%;
347+
min-height: 60px;
348+
padding: 8px;
349+
font-family: inherit;
350+
font-size: 13px;
351+
color: var(--vscode-input-foreground);
352+
background: var(--vscode-input-background);
353+
border: 1px solid var(--vscode-input-border);
354+
border-radius: 3px;
355+
resize: vertical;
356+
box-sizing: border-box;
357+
}
358+
359+
.inline-feedback-form textarea:focus {
360+
outline: none;
361+
border-color: var(--vscode-focusBorder);
362+
}
363+
364+
.inline-feedback-form .form-actions {
365+
display: flex;
366+
justify-content: flex-end;
367+
gap: 8px;
368+
margin-top: 8px;
369+
}
370+
371+
.btn {
372+
padding: 6px 14px;
373+
border-radius: 4px;
374+
cursor: pointer;
375+
font-size: 13px;
376+
border: none;
377+
}
378+
379+
.btn-primary {
380+
background: var(--vscode-button-background);
381+
color: var(--vscode-button-foreground);
382+
}
383+
384+
.btn-primary:hover {
385+
background: var(--vscode-button-hoverBackground);
386+
}
387+
388+
.btn-secondary {
389+
background: var(--vscode-button-secondaryBackground);
390+
color: var(--vscode-button-secondaryForeground);
391+
}
392+
393+
.btn-secondary:hover {
394+
background: var(--vscode-button-secondaryHoverBackground);
395+
}
396+
397+
.feedback-success {
398+
color: var(--vscode-testing-iconPassed);
399+
font-size: 13px;
400+
margin-top: 8px;
401+
}

src/ui/webview/bundleDetails/bundleDetails.html

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,21 @@ <h1>
2626
{{ratingDisplay}}
2727
</div>
2828
<div class="feedback-actions">
29-
<button class="btn-feedback" data-action="feedback">⭐ Rate & Feedback</button>
29+
<div class="interactive-stars" id="detailStars">
30+
<span class="star" data-star="1" title="1 star"></span>
31+
<span class="star" data-star="2" title="2 stars"></span>
32+
<span class="star" data-star="3" title="3 stars"></span>
33+
<span class="star" data-star="4" title="4 stars"></span>
34+
<span class="star" data-star="5" title="5 stars"></span>
35+
<span class="star-label">Rate this bundle</span>
36+
</div>
37+
</div>
38+
</div>
39+
<div class="inline-feedback-form" id="detailFeedbackForm">
40+
<textarea placeholder="Optional: share your experience..." maxlength="1000"></textarea>
41+
<div class="form-actions">
42+
<button class="btn btn-secondary btn-small" id="cancelFeedbackBtn">Cancel</button>
43+
<button class="btn btn-primary btn-small" id="submitFeedbackBtn">Submit</button>
3044
</div>
3145
</div>
3246
</div>

src/ui/webview/bundleDetails/bundleDetails.js

Lines changed: 105 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,26 +48,122 @@
4848
}
4949
}
5050

51-
/**
52-
* Open unified feedback dialog (star rating + binary feedback + optional issue redirect)
53-
*/
54-
function feedback() {
55-
vscode.postMessage({
56-
type: 'feedback',
57-
bundleId: bundleId
51+
// ========================================================================
52+
// Interactive Star Rating
53+
// ========================================================================
54+
55+
let selectedRating = 0;
56+
const starsContainer = document.getElementById('detailStars');
57+
const feedbackForm = document.getElementById('detailFeedbackForm');
58+
const submitBtn = document.getElementById('submitFeedbackBtn');
59+
const cancelBtn = document.getElementById('cancelFeedbackBtn');
60+
61+
if (starsContainer) {
62+
const stars = starsContainer.querySelectorAll('.star');
63+
64+
// Hover preview
65+
stars.forEach(star => {
66+
star.addEventListener('mouseover', () => {
67+
const val = parseInt(star.dataset.star);
68+
stars.forEach(s => {
69+
const sv = parseInt(s.dataset.star);
70+
s.classList.toggle('hovered', sv <= val);
71+
});
72+
});
73+
74+
star.addEventListener('mouseout', () => {
75+
stars.forEach(s => {
76+
s.classList.remove('hovered');
77+
const sv = parseInt(s.dataset.star);
78+
s.classList.toggle('filled', sv <= selectedRating);
79+
});
80+
});
81+
82+
// Click to select rating
83+
star.addEventListener('click', () => {
84+
selectedRating = parseInt(star.dataset.star);
85+
stars.forEach(s => {
86+
const sv = parseInt(s.dataset.star);
87+
s.classList.toggle('filled', sv <= selectedRating);
88+
s.classList.remove('hovered');
89+
});
90+
91+
// Show inline feedback form
92+
if (feedbackForm) {
93+
feedbackForm.classList.add('visible');
94+
feedbackForm.querySelector('textarea')?.focus();
95+
}
96+
97+
// Update label
98+
const label = starsContainer.querySelector('.star-label');
99+
if (label) {
100+
label.textContent = selectedRating + ' star' + (selectedRating > 1 ? 's' : '') + ' selected';
101+
}
102+
});
103+
});
104+
}
105+
106+
// Submit feedback
107+
if (submitBtn) {
108+
submitBtn.addEventListener('click', () => {
109+
if (selectedRating === 0) return;
110+
const textarea = feedbackForm.querySelector('textarea');
111+
const comment = textarea ? textarea.value.trim() : '';
112+
113+
vscode.postMessage({
114+
type: 'submitFeedback',
115+
bundleId: bundleId,
116+
rating: selectedRating,
117+
comment: comment || undefined,
118+
});
119+
120+
// Disable submit while pending
121+
submitBtn.disabled = true;
122+
submitBtn.textContent = 'Submitting...';
123+
});
124+
}
125+
126+
// Cancel feedback
127+
if (cancelBtn) {
128+
cancelBtn.addEventListener('click', () => {
129+
if (feedbackForm) {
130+
feedbackForm.classList.remove('visible');
131+
}
58132
});
59133
}
60134

61-
// Listen for status updates from extension
135+
// Listen for messages from extension
62136
window.addEventListener('message', function(event) {
63137
const message = event.data;
64138
if (message.type === 'autoUpdateStatusChanged') {
65139
autoUpdateEnabled = message.enabled;
66140
updateToggleUI();
141+
} else if (message.type === 'feedbackSubmitted') {
142+
if (feedbackForm) {
143+
feedbackForm.classList.remove('visible');
144+
const textarea = feedbackForm.querySelector('textarea');
145+
if (textarea) textarea.value = '';
146+
}
147+
if (submitBtn) {
148+
submitBtn.disabled = false;
149+
submitBtn.textContent = 'Submit';
150+
}
151+
152+
// Show success message
153+
const section = document.querySelector('.rating-section');
154+
if (section && message.success) {
155+
const existing = section.querySelector('.feedback-success');
156+
if (existing) existing.remove();
157+
const msg = document.createElement('div');
158+
msg.className = 'feedback-success';
159+
msg.textContent = message.synced ? 'Thank you for your feedback!' : 'Feedback saved locally.';
160+
section.appendChild(msg);
161+
setTimeout(() => msg.remove(), 4000);
162+
}
67163
}
68164
});
69165

70-
// Event delegation for all click handlers (CSP compliant)
166+
// Event delegation for remaining click handlers (CSP compliant)
71167
document.addEventListener('click', function(e) {
72168
const target = e.target;
73169
const actionElement = target.closest('[data-action]');
@@ -84,9 +180,6 @@
84180
case 'toggleAutoUpdate':
85181
toggleAutoUpdate();
86182
break;
87-
case 'feedback':
88-
feedback();
89-
break;
90183
}
91184
}
92185
});

0 commit comments

Comments
 (0)