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