adding stuff
This commit is contained in:
@@ -30,6 +30,17 @@ spec:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: recommender-secrets
|
name: recommender-secrets
|
||||||
key: DATABASE_URL
|
key: DATABASE_URL
|
||||||
|
- name: BEARER_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: recommender-secrets
|
||||||
|
key: BEARER_TOKEN
|
||||||
|
- name: PROVIDER_URL
|
||||||
|
value: "https://openrouter.ai/api/v1"
|
||||||
|
- name: MODEL_NAME
|
||||||
|
value: "openai/gpt-5.4"
|
||||||
|
- name: AI_PROVIDER
|
||||||
|
value: "GENERIC"
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
memory: "256Mi"
|
memory: "256Mi"
|
||||||
|
|||||||
@@ -116,6 +116,21 @@ export default async function recommendationsRoute(fastify: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// DELETE /recommendations/:id — delete a recommendation
|
||||||
|
fastify.delete('/recommendations/:id', async (request, reply) => {
|
||||||
|
const { id } = request.params as { id: string };
|
||||||
|
const [rec] = await db
|
||||||
|
.select({ id: recommendations.id })
|
||||||
|
.from(recommendations)
|
||||||
|
.where(eq(recommendations.id, id));
|
||||||
|
|
||||||
|
if (!rec) return reply.code(404).send({ error: 'Not found' });
|
||||||
|
|
||||||
|
await db.delete(recommendations).where(eq(recommendations.id, id));
|
||||||
|
|
||||||
|
return reply.send({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
// POST /recommendations/:id/rerank — reset status so client can re-open SSE stream
|
// POST /recommendations/:id/rerank — reset status so client can re-open SSE stream
|
||||||
fastify.post('/recommendations/:id/rerank', async (request, reply) => {
|
fastify.post('/recommendations/:id/rerank', async (request, reply) => {
|
||||||
const { id } = request.params as { id: string };
|
const { id } = request.params as { id: string };
|
||||||
|
|||||||
@@ -55,3 +55,7 @@ export function submitFeedback(body: {
|
|||||||
export function getFeedback(): Promise<FeedbackEntry[]> {
|
export function getFeedback(): Promise<FeedbackEntry[]> {
|
||||||
return request('/feedback');
|
return request('/feedback');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function deleteRecommendation(id: string): Promise<{ ok: boolean }> {
|
||||||
|
return request(`/recommendations/${id}`, { method: 'DELETE' });
|
||||||
|
}
|
||||||
|
|||||||
@@ -125,6 +125,8 @@
|
|||||||
padding: 16px 0 32px;
|
padding: 16px 0 32px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-rerank {
|
.btn-rerank {
|
||||||
|
|||||||
@@ -84,3 +84,7 @@
|
|||||||
.pipeline-step-label {
|
.pipeline-step-label {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pipeline-retry {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const STAGES: { key: keyof StageMap; label: string }[] = [
|
|||||||
|
|
||||||
interface PipelineProgressProps {
|
interface PipelineProgressProps {
|
||||||
stages: StageMap;
|
stages: StageMap;
|
||||||
|
onRetry?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function StageIcon({ status }: { status: StageStatus }) {
|
function StageIcon({ status }: { status: StageStatus }) {
|
||||||
@@ -25,10 +26,12 @@ function StageIcon({ status }: { status: StageStatus }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PipelineProgress({ stages }: PipelineProgressProps) {
|
export function PipelineProgress({ stages, onRetry }: PipelineProgressProps) {
|
||||||
|
const hasError = Object.values(stages).some((s) => s === 'error');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="pipeline-progress">
|
<div class="pipeline-progress">
|
||||||
<h3 class="pipeline-title">Generating Recommendations…</h3>
|
<h3 class="pipeline-title">{hasError ? 'Pipeline Failed' : 'Generating Recommendations…'}</h3>
|
||||||
<ul class="pipeline-steps">
|
<ul class="pipeline-steps">
|
||||||
{STAGES.map(({ key, label }) => (
|
{STAGES.map(({ key, label }) => (
|
||||||
<li key={key} class={`pipeline-step pipeline-step--${stages[key]}`}>
|
<li key={key} class={`pipeline-step pipeline-step--${stages[key]}`}>
|
||||||
@@ -37,6 +40,11 @@ export function PipelineProgress({ stages }: PipelineProgressProps) {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
{hasError && onRetry && (
|
||||||
|
<div class="pipeline-retry">
|
||||||
|
<button class="btn-primary" onClick={onRetry}>Re-run Pipeline</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,4 +142,25 @@
|
|||||||
.sidebar-type-movie {
|
.sidebar-type-movie {
|
||||||
background: rgba(245, 158, 11, 0.15);
|
background: rgba(245, 158, 11, 0.15);
|
||||||
color: #fbbf24;
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item-delete {
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-dim);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0 2px;
|
||||||
|
line-height: 1;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item:hover .sidebar-item-delete {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item-delete:hover {
|
||||||
|
color: var(--red);
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ interface SidebarProps {
|
|||||||
selectedId: string | null;
|
selectedId: string | null;
|
||||||
onSelect: (id: string) => void;
|
onSelect: (id: string) => void;
|
||||||
onNewClick: () => void;
|
onNewClick: () => void;
|
||||||
|
onDelete?: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusIcon(status: RecommendationSummary['status']): string {
|
function statusIcon(status: RecommendationSummary['status']): string {
|
||||||
@@ -26,7 +27,7 @@ function statusClass(status: RecommendationSummary['status']): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({ list, selectedId, onSelect, onNewClick }: SidebarProps) {
|
export function Sidebar({ list, selectedId, onSelect, onNewClick, onDelete }: SidebarProps) {
|
||||||
return (
|
return (
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
@@ -56,6 +57,13 @@ export function Sidebar({ list, selectedId, onSelect, onNewClick }: SidebarProps
|
|||||||
<span class={`sidebar-type-badge sidebar-type-${item.media_type}`}>
|
<span class={`sidebar-type-badge sidebar-type-${item.media_type}`}>
|
||||||
{item.media_type === 'movie' ? 'Film' : 'TV'}
|
{item.media_type === 'movie' ? 'Film' : 'TV'}
|
||||||
</span>
|
</span>
|
||||||
|
{onDelete && (
|
||||||
|
<button
|
||||||
|
class="sidebar-item-delete"
|
||||||
|
onClick={(e) => { e.stopPropagation(); onDelete(item.id); }}
|
||||||
|
title="Delete"
|
||||||
|
>×</button>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
rerankRecommendation,
|
rerankRecommendation,
|
||||||
submitFeedback,
|
submitFeedback,
|
||||||
getFeedback,
|
getFeedback,
|
||||||
|
deleteRecommendation,
|
||||||
} from '../api/client.js';
|
} from '../api/client.js';
|
||||||
|
|
||||||
export function useRecommendations() {
|
export function useRecommendations() {
|
||||||
@@ -71,6 +72,11 @@ export function useRecommendations() {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const deleteRec = useCallback(async (id: string) => {
|
||||||
|
await deleteRecommendation(id);
|
||||||
|
setList((prev) => prev.filter((r) => r.id !== id));
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
list,
|
list,
|
||||||
selectedId,
|
selectedId,
|
||||||
@@ -81,5 +87,6 @@ export function useRecommendations() {
|
|||||||
submitFeedback: handleSubmitFeedback,
|
submitFeedback: handleSubmitFeedback,
|
||||||
updateStatus,
|
updateStatus,
|
||||||
refreshList,
|
refreshList,
|
||||||
|
deleteRec,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ 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 } = useRecommendationsContext();
|
const { list, feedback, submitFeedback, rerank, updateStatus, 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);
|
||||||
@@ -70,6 +70,7 @@ export function Recom({ id }: RecomProps) {
|
|||||||
if (event.status === 'error') {
|
if (event.status === 'error') {
|
||||||
setSseUrl(null);
|
setSseUrl(null);
|
||||||
updateStatus(id, 'error');
|
updateStatus(id, 'error');
|
||||||
|
setRec((prev) => (prev ? { ...prev, status: 'error' as const } : null));
|
||||||
const stageKey = event.stage as PipelineStage;
|
const stageKey = event.stage as PipelineStage;
|
||||||
if (stageKey !== 'complete') {
|
if (stageKey !== 'complete') {
|
||||||
setStages((prev) => ({ ...prev, [stageKey as keyof StageMap]: 'error' }));
|
setStages((prev) => ({ ...prev, [stageKey as keyof StageMap]: 'error' }));
|
||||||
@@ -81,11 +82,23 @@ export function Recom({ id }: RecomProps) {
|
|||||||
|
|
||||||
useSSE(sseUrl, handleSSEEvent);
|
useSSE(sseUrl, handleSSEEvent);
|
||||||
|
|
||||||
|
const handleRetry = async () => {
|
||||||
|
await rerank(id);
|
||||||
|
setStages(DEFAULT_STAGES);
|
||||||
|
setSseUrl(`/api/recommendations/${id}/stream`);
|
||||||
|
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`);
|
setSseUrl(`/api/recommendations/${id}/stream`);
|
||||||
setRec((prev) => (prev ? { ...prev, status: 'pending' } : null));
|
setRec((prev) => (prev ? { ...prev, status: 'pending' as const } : null));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (sid: string) => {
|
||||||
|
await deleteRec(sid);
|
||||||
|
if (sid === id) route('/');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateNew = async (body: {
|
const handleCreateNew = async (body: {
|
||||||
@@ -111,6 +124,7 @@ export function Recom({ id }: RecomProps) {
|
|||||||
selectedId={id}
|
selectedId={id}
|
||||||
onSelect={(sid) => route(`/recom/${sid}`)}
|
onSelect={(sid) => route(`/recom/${sid}`)}
|
||||||
onNewClick={() => setShowModal(true)}
|
onNewClick={() => setShowModal(true)}
|
||||||
|
onDelete={handleDelete}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
@@ -166,7 +180,7 @@ export function Recom({ id }: RecomProps) {
|
|||||||
|
|
||||||
{isRunning && (
|
{isRunning && (
|
||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<PipelineProgress stages={stages} />
|
<PipelineProgress stages={stages} onRetry={handleRetry} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -185,6 +199,9 @@ export function Recom({ id }: RecomProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div class="rerank-section">
|
<div class="rerank-section">
|
||||||
|
<button class="btn-rerank btn-rerun" onClick={handleRetry}>
|
||||||
|
Re-run Pipeline
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn-rerank"
|
class="btn-rerank"
|
||||||
onClick={handleRerank}
|
onClick={handleRerank}
|
||||||
@@ -200,9 +217,9 @@ export function Recom({ id }: RecomProps) {
|
|||||||
{!isRunning && rec?.status === 'error' && (
|
{!isRunning && rec?.status === 'error' && (
|
||||||
<div class="content-area error-state">
|
<div class="content-area error-state">
|
||||||
<h2>Something went wrong</h2>
|
<h2>Something went wrong</h2>
|
||||||
<p>The pipeline encountered an error. You can try again by clicking Re-rank.</p>
|
<p>The pipeline encountered an error. You can try again by clicking the button below.</p>
|
||||||
<button class="btn-primary" onClick={handleRerank}>
|
<button class="btn-primary" onClick={handleRetry}>
|
||||||
Try Again
|
Re-run Pipeline
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user