fixes
All checks were successful
Recommender Build and Deploy (internal) / Build Recommender Image (push) Successful in 4m5s
Recommender Build and Deploy (internal) / Deploy Recommender (internal) (push) Successful in 11s

This commit is contained in:
2026-03-31 19:55:20 -03:00
parent bb8a5da45e
commit 148755243a
5 changed files with 75 additions and 12 deletions

View File

@@ -4,7 +4,9 @@ const BASE = '/api';
async function request<T>(path: string, options?: RequestInit): Promise<T> { async function request<T>(path: string, options?: RequestInit): Promise<T> {
const res = await fetch(`${BASE}${path}`, { const res = await fetch(`${BASE}${path}`, {
headers: { 'Content-Type': 'application/json' }, headers: {
...(options?.body ? { 'Content-Type': 'application/json' } : {}),
},
...options, ...options,
}); });
if (!res.ok) { if (!res.ok) {

View File

@@ -17,6 +17,7 @@ const CATEGORY_COLORS: Record<CuratorCategory, string> = {
export function RecommendationCard({ show, existingFeedback, onFeedback }: RecommendationCardProps) { export function RecommendationCard({ show, existingFeedback, onFeedback }: RecommendationCardProps) {
const [selectedStars, setSelectedStars] = useState(existingFeedback?.stars ?? 0); const [selectedStars, setSelectedStars] = useState(existingFeedback?.stars ?? 0);
const [hoverStar, setHoverStar] = useState(0);
const [comment, setComment] = useState(existingFeedback?.feedback ?? ''); const [comment, setComment] = useState(existingFeedback?.feedback ?? '');
const [showComment, setShowComment] = useState(false); const [showComment, setShowComment] = useState(false);
const [submitted, setSubmitted] = useState(!!existingFeedback); const [submitted, setSubmitted] = useState(!!existingFeedback);
@@ -49,16 +50,21 @@ export function RecommendationCard({ show, existingFeedback, onFeedback }: Recom
<div class="card-feedback"> <div class="card-feedback">
<div class="star-rating"> <div class="star-rating">
{[1, 2, 3].map((star) => ( {[1, 2, 3].map((star) => {
<button const effective = hoverStar || selectedStars;
key={star} return (
class={`star-btn${selectedStars >= star ? ' star-active' : ''}`} <button
onClick={() => handleStarClick(star)} key={star}
aria-label={`Rate ${star} star${star > 1 ? 's' : ''}`} class={`star-btn${effective >= star ? ' star-active' : ''}`}
> onClick={() => handleStarClick(star)}
{selectedStars >= star ? '★' : '☆'} onMouseEnter={() => setHoverStar(star)}
</button> onMouseLeave={() => setHoverStar(0)}
))} aria-label={`Rate ${star} star${star > 1 ? 's' : ''}`}
>
{effective >= star ? '★' : '☆'}
</button>
);
})}
{submitted && <span class="feedback-saved">Saved</span>} {submitted && <span class="feedback-saved">Saved</span>}
</div> </div>

View File

@@ -4,10 +4,13 @@ import type { SSEEvent } from '../types/index.js';
export function useSSE( export function useSSE(
url: string | null, url: string | null,
onEvent: (event: SSEEvent) => void, onEvent: (event: SSEEvent) => void,
onClose?: () => void,
): { close: () => void } { ): { close: () => void } {
const esRef = useRef<EventSource | null>(null); const esRef = useRef<EventSource | null>(null);
const onEventRef = useRef(onEvent); const onEventRef = useRef(onEvent);
onEventRef.current = onEvent; onEventRef.current = onEvent;
const onCloseRef = useRef(onClose);
onCloseRef.current = onClose;
useEffect(() => { useEffect(() => {
if (!url) return; if (!url) return;
@@ -32,6 +35,7 @@ export function useSSE(
es.onerror = () => { es.onerror = () => {
es.close(); es.close();
esRef.current = null; esRef.current = null;
onCloseRef.current?.();
}; };
return () => { return () => {

View File

@@ -95,6 +95,27 @@
color: var(--accent); color: var(--accent);
} }
.rec-info-delete-row {
padding-top: 4px;
}
.btn-danger {
background: none;
border: 1px solid rgba(239, 68, 68, 0.4);
color: #f87171;
border-radius: var(--radius);
padding: 5px 14px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.btn-danger:hover {
background: rgba(239, 68, 68, 0.12);
border-color: #f87171;
}
.error-state { .error-state {
color: var(--text-muted); color: var(--text-muted);
} }

View File

@@ -80,7 +80,25 @@ export function Recom({ id }: RecomProps) {
[id, updateStatus, refreshList], [id, updateStatus, refreshList],
); );
useSSE(sseUrl, handleSSEEvent); const handleSSEClose = useCallback(() => {
void getRecommendation(id).then((data) => {
if (data.status === 'done') {
setSseUrl(null);
setRec(data);
setStages({ interpreter: 'done', retrieval: 'done', ranking: 'done', curator: 'done' });
updateStatus(id, 'done');
void refreshList();
} else if (data.status === 'error') {
setSseUrl(null);
setRec(data);
updateStatus(id, 'error');
} else {
setSseUrl(`/api/recommendations/${id}/stream`);
}
});
}, [id, updateStatus, refreshList]);
useSSE(sseUrl, handleSSEEvent, handleSSEClose);
const handleRetry = async () => { const handleRetry = async () => {
await rerank(id); await rerank(id);
@@ -172,6 +190,18 @@ export function Recom({ id }: RecomProps) {
<span class="rec-info-badge">enabled</span> <span class="rec-info-badge">enabled</span>
</div> </div>
)} )}
<div class="rec-info-row rec-info-delete-row">
<button
class="btn-danger"
onClick={() => {
if (confirm('Delete this recommendation?')) {
void deleteRec(id).then(() => route('/'));
}
}}
>
Delete
</button>
</div>
</div> </div>
)} )}
</div> </div>