fixes
All checks were successful
Recommender Build and Deploy (internal) / Build Recommender Image (push) Successful in 5m38s
Recommender Build and Deploy (internal) / Deploy Recommender (internal) (push) Successful in 40s

This commit is contained in:
2026-04-02 13:46:14 -03:00
parent 39edec4a7c
commit 91870f4046
4 changed files with 63 additions and 20 deletions

View File

@@ -187,7 +187,7 @@ export async function runPipeline(
.set({ recommendations: curatorOutput, status: 'done', title: aiTitle }) .set({ recommendations: curatorOutput, status: 'done', title: aiTitle })
.where(eq(recommendations.id, rec.id)); .where(eq(recommendations.id, rec.id));
sseWrite({ stage: 'complete', status: 'done' }); sseWrite({ stage: 'complete', status: 'done', data: { title: aiTitle } });
log(rec.id, `Pipeline complete (total: ${Date.now() - startTime}ms)`); log(rec.id, `Pipeline complete (total: ${Date.now() - startTime}ms)`);
return curatorOutput; return curatorOutput;

View File

@@ -72,6 +72,10 @@ export function useRecommendations() {
[], [],
); );
const updateTitle = useCallback((id: string, title: string) => {
setList((prev) => prev.map((r) => (r.id === id ? { ...r, title } : r)));
}, []);
const deleteRec = useCallback(async (id: string) => { const deleteRec = useCallback(async (id: string) => {
await deleteRecommendation(id); await deleteRecommendation(id);
setList((prev) => prev.filter((r) => r.id !== id)); setList((prev) => prev.filter((r) => r.id !== id));
@@ -86,6 +90,7 @@ export function useRecommendations() {
rerank, rerank,
submitFeedback: handleSubmitFeedback, submitFeedback: handleSubmitFeedback,
updateStatus, updateStatus,
updateTitle,
refreshList, refreshList,
deleteRec, deleteRec,
}; };

View File

@@ -97,25 +97,44 @@
.rec-info-delete-row { .rec-info-delete-row {
padding-top: 4px; padding-top: 4px;
align-items: center;
} }
.btn-danger { /* Shared sizing for the two action buttons in the info panel */
background: none; .btn-danger,
border: 1px solid rgba(239, 68, 68, 0.4); .rec-info-delete-row .btn-rerun {
color: #f87171;
border-radius: var(--radius);
padding: 5px 14px; padding: 5px 14px;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
border-radius: var(--radius);
border: 1px solid;
background: none;
cursor: pointer; cursor: pointer;
transition: background 0.15s, border-color 0.15s; transition: background 0.15s, border-color 0.15s;
} }
.btn-danger {
border-color: rgba(239, 68, 68, 0.4);
color: #f87171;
}
.btn-danger:hover { .btn-danger:hover {
background: rgba(239, 68, 68, 0.12); background: rgba(239, 68, 68, 0.12);
border-color: #f87171; border-color: #f87171;
} }
/* Re-run button — same shape as Delete, neutral tone */
.rec-info-delete-row .btn-rerun {
border-color: var(--border);
color: var(--text-muted);
}
.rec-info-delete-row .btn-rerun:hover {
background: var(--bg-surface-2);
border-color: var(--text-dim);
color: var(--text);
}
.error-state { .error-state {
color: var(--text-muted); color: var(--text-muted);
} }

View File

@@ -25,24 +25,31 @@ const DEFAULT_STAGES: StageMap = {
const STAGE_ORDER: (keyof StageMap)[] = ['interpreter', 'retrieval', 'ranking', 'curator']; const STAGE_ORDER: (keyof StageMap)[] = ['interpreter', 'retrieval', 'ranking', 'curator'];
export function Recom({ id }: RecomProps) { export function Recom({ id }: RecomProps) {
const { list, feedback, submitFeedback, rerank, updateStatus, refreshList, createNew, deleteRec } = useRecommendationsContext(); const { list, feedback, submitFeedback, rerank, updateStatus, updateTitle, refreshList, createNew, deleteRec } = useRecommendationsContext();
const [rec, setRec] = useState<Recommendation | null>(null); const [rec, setRec] = useState<Recommendation | null>(null);
const [stages, setStages] = useState<StageMap>(DEFAULT_STAGES); const [stages, setStages] = useState<StageMap>(DEFAULT_STAGES);
const [sseUrl, setSseUrl] = useState<string | null>(null); // sseKey drives the SSE connection. null = inactive; a number = active.
// Using a timestamp nonce ensures the URL is always unique on (re)connect,
// so useSSE's useEffect always re-runs even if the base path hasn't changed.
const [sseKey, setSseKey] = useState<number | null>(null);
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [infoExpanded, setInfoExpanded] = useState(true); const [infoExpanded, setInfoExpanded] = useState(true);
// Derive the actual URL from the key; query param is ignored by the server
// but makes the string unique so React/Preact state always treats it as changed.
const sseUrl = sseKey !== null ? `/api/recommendations/${id}/stream?_k=${sseKey}` : null;
useEffect(() => { useEffect(() => {
setRec(null); setRec(null);
setStages(DEFAULT_STAGES); setStages(DEFAULT_STAGES);
setSseUrl(null); setSseKey(null);
getRecommendation(id) getRecommendation(id)
.then((data) => { .then((data) => {
setRec(data); setRec(data);
if (data.status === 'running' || data.status === 'pending') { if (data.status === 'running' || data.status === 'pending') {
setStages(DEFAULT_STAGES); setStages(DEFAULT_STAGES);
setSseUrl(`/api/recommendations/${id}/stream`); setSseKey(Date.now());
} }
}) })
.catch(() => route('/')); .catch(() => route('/'));
@@ -61,14 +68,24 @@ export function Recom({ id }: RecomProps) {
} }
if (event.stage === 'complete' && event.status === 'done') { if (event.stage === 'complete' && event.status === 'done') {
setSseUrl(null); const incoming = event.data as { title?: string } | undefined;
if (incoming?.title) {
updateTitle(id, incoming.title);
setRec((prev) => (prev ? { ...prev, title: incoming.title! } : prev));
}
setSseKey(null);
updateStatus(id, 'done'); updateStatus(id, 'done');
void getRecommendation(id).then(setRec); void getRecommendation(id)
.then(setRec)
.catch(() => {
// Fetch failed after completion — poll once more via handleSSEClose logic
updateStatus(id, 'done');
});
void refreshList(); void refreshList();
} }
if (event.status === 'error') { if (event.status === 'error') {
setSseUrl(null); setSseKey(null);
updateStatus(id, 'error'); updateStatus(id, 'error');
setRec((prev) => (prev ? { ...prev, status: 'error' as const } : null)); setRec((prev) => (prev ? { ...prev, status: 'error' as const } : null));
const stageKey = event.stage as PipelineStage; const stageKey = event.stage as PipelineStage;
@@ -77,23 +94,25 @@ export function Recom({ id }: RecomProps) {
} }
} }
}, },
[id, updateStatus, refreshList], [id, updateStatus, updateTitle, refreshList],
); );
const handleSSEClose = useCallback(() => { const handleSSEClose = useCallback(() => {
void getRecommendation(id).then((data) => { void getRecommendation(id).then((data) => {
if (data.status === 'done') { if (data.status === 'done') {
setSseUrl(null); setSseKey(null);
setRec(data); setRec(data);
setStages({ interpreter: 'done', retrieval: 'done', ranking: 'done', curator: 'done' }); setStages({ interpreter: 'done', retrieval: 'done', ranking: 'done', curator: 'done' });
updateStatus(id, 'done'); updateStatus(id, 'done');
void refreshList(); void refreshList();
} else if (data.status === 'error') { } else if (data.status === 'error') {
setSseUrl(null); setSseKey(null);
setRec(data); setRec(data);
updateStatus(id, 'error'); updateStatus(id, 'error');
} else { } else {
setSseUrl(`/api/recommendations/${id}/stream`); // Pipeline still running — reconnect with a new unique key so that
// useSSE's useEffect always re-fires even if the base URL is the same.
setSseKey(Date.now());
} }
}); });
}, [id, updateStatus, refreshList]); }, [id, updateStatus, refreshList]);
@@ -103,14 +122,14 @@ export function Recom({ id }: RecomProps) {
const handleRetry = async () => { const handleRetry = async () => {
await rerank(id); await rerank(id);
setStages(DEFAULT_STAGES); setStages(DEFAULT_STAGES);
setSseUrl(`/api/recommendations/${id}/stream`); setSseKey(Date.now());
setRec((prev) => (prev ? { ...prev, status: 'pending' as const } : null)); setRec((prev) => (prev ? { ...prev, status: 'pending' as const } : null));
}; };
const handleRerank = async () => { const handleRerank = async () => {
await rerank(id); await rerank(id);
setStages(DEFAULT_STAGES); setStages(DEFAULT_STAGES);
setSseUrl(`/api/recommendations/${id}/stream`); setSseKey(Date.now());
setRec((prev) => (prev ? { ...prev, status: 'pending' as const } : null)); setRec((prev) => (prev ? { ...prev, status: 'pending' as const } : null));
}; };
@@ -127,7 +146,7 @@ export function Recom({ id }: RecomProps) {
route(`/recom/${newId}`); route(`/recom/${newId}`);
}; };
const isRunning = rec?.status === 'running' || rec?.status === 'pending' || !!sseUrl; const isRunning = rec?.status === 'running' || rec?.status === 'pending' || sseKey !== null;
const feedbackMap = new Map(feedback.map((f) => [f.item_name, f])); const feedbackMap = new Map(feedback.map((f) => [f.item_name, f]));
return ( return (