fixes
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user