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