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 })
.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)`);
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) => {
await deleteRecommendation(id);
setList((prev) => prev.filter((r) => r.id !== id));
@@ -86,6 +90,7 @@ export function useRecommendations() {
rerank,
submitFeedback: handleSubmitFeedback,
updateStatus,
updateTitle,
refreshList,
deleteRec,
};

View File

@@ -97,25 +97,44 @@
.rec-info-delete-row {
padding-top: 4px;
align-items: center;
}
.btn-danger {
background: none;
border: 1px solid rgba(239, 68, 68, 0.4);
color: #f87171;
border-radius: var(--radius);
/* Shared sizing for the two action buttons in the info panel */
.btn-danger,
.rec-info-delete-row .btn-rerun {
padding: 5px 14px;
font-size: 12px;
font-weight: 600;
border-radius: var(--radius);
border: 1px solid;
background: none;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.btn-danger {
border-color: rgba(239, 68, 68, 0.4);
color: #f87171;
}
.btn-danger:hover {
background: rgba(239, 68, 68, 0.12);
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 {
color: var(--text-muted);
}

View File

@@ -25,24 +25,31 @@ const DEFAULT_STAGES: StageMap = {
const STAGE_ORDER: (keyof StageMap)[] = ['interpreter', 'retrieval', 'ranking', 'curator'];
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 [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 [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(() => {
setRec(null);
setStages(DEFAULT_STAGES);
setSseUrl(null);
setSseKey(null);
getRecommendation(id)
.then((data) => {
setRec(data);
if (data.status === 'running' || data.status === 'pending') {
setStages(DEFAULT_STAGES);
setSseUrl(`/api/recommendations/${id}/stream`);
setSseKey(Date.now());
}
})
.catch(() => route('/'));
@@ -61,14 +68,24 @@ export function Recom({ id }: RecomProps) {
}
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');
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();
}
if (event.status === 'error') {
setSseUrl(null);
setSseKey(null);
updateStatus(id, 'error');
setRec((prev) => (prev ? { ...prev, status: 'error' as const } : null));
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(() => {
void getRecommendation(id).then((data) => {
if (data.status === 'done') {
setSseUrl(null);
setSseKey(null);
setRec(data);
setStages({ interpreter: 'done', retrieval: 'done', ranking: 'done', curator: 'done' });
updateStatus(id, 'done');
void refreshList();
} else if (data.status === 'error') {
setSseUrl(null);
setSseKey(null);
setRec(data);
updateStatus(id, 'error');
} 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]);
@@ -103,14 +122,14 @@ export function Recom({ id }: RecomProps) {
const handleRetry = async () => {
await rerank(id);
setStages(DEFAULT_STAGES);
setSseUrl(`/api/recommendations/${id}/stream`);
setSseKey(Date.now());
setRec((prev) => (prev ? { ...prev, status: 'pending' as const } : null));
};
const handleRerank = async () => {
await rerank(id);
setStages(DEFAULT_STAGES);
setSseUrl(`/api/recommendations/${id}/stream`);
setSseKey(Date.now());
setRec((prev) => (prev ? { ...prev, status: 'pending' as const } : null));
};
@@ -127,7 +146,7 @@ export function Recom({ id }: RecomProps) {
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]));
return (