feature: new changes!
All checks were successful
Recommender Build and Deploy (internal) / Build Recommender Image (push) Successful in 3m59s
Recommender Build and Deploy (internal) / Deploy Recommender (internal) (push) Successful in 10s

This commit is contained in:
2026-03-25 20:09:32 -03:00
parent 26f61077b8
commit f9f3d95406
25 changed files with 964 additions and 696 deletions

View File

@@ -0,0 +1,151 @@
/* ── Cards ─────────────────────────────────────────────── */
.cards-grid {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 32px;
}
.card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px;
transition: border-color 0.15s;
}
.card:hover {
border-color: var(--bg-surface-3);
}
.card-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
flex-wrap: wrap;
}
.badge {
font-size: 11px;
font-weight: 600;
padding: 2px 8px;
border-radius: 100px;
text-transform: uppercase;
letter-spacing: 0.04em;
flex-shrink: 0;
}
.badge-green {
background: rgba(34, 197, 94, 0.15);
color: #4ade80;
}
.badge-blue {
background: rgba(59, 130, 246, 0.15);
color: #60a5fa;
}
.badge-yellow {
background: rgba(234, 179, 8, 0.15);
color: #facc15;
}
.badge-red {
background: rgba(239, 68, 68, 0.15);
color: #f87171;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: var(--text);
}
.card-explanation {
font-size: 13px;
color: var(--text-muted);
line-height: 1.5;
margin-bottom: 12px;
}
.card-feedback {
border-top: 1px solid var(--border);
padding-top: 10px;
}
.star-rating {
display: flex;
align-items: center;
gap: 2px;
}
.star-btn {
background: none;
border: none;
cursor: pointer;
font-size: 18px;
color: var(--text-dim);
padding: 0 2px;
transition: color 0.1s, transform 0.1s;
}
.star-btn:hover,
.star-btn.star-active {
color: var(--yellow);
}
.star-btn:hover {
transform: scale(1.1);
}
.feedback-saved {
margin-left: 8px;
font-size: 12px;
color: var(--green);
font-weight: 500;
}
.comment-area {
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 6px;
}
.comment-input {
font-size: 13px;
resize: vertical;
}
/* ── Rerank Button ──────────────────────────────────────── */
.rerank-section {
padding: 16px 0 32px;
display: flex;
justify-content: center;
}
.btn-rerank {
padding: 12px 28px;
background: var(--bg-surface-2);
color: var(--text);
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.btn-rerank:hover:not(:disabled) {
background: var(--bg-surface-3);
border-color: var(--accent);
color: var(--accent);
}
.btn-rerank:disabled {
opacity: 0.4;
cursor: not-allowed;
}

View File

@@ -0,0 +1,105 @@
/* ── Modal ──────────────────────────────────────────────── */
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
backdrop-filter: blur(2px);
}
.modal {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 12px;
width: 650px;
max-width: 96vw;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 20px 0;
}
.modal-header h2 {
font-size: 18px;
font-weight: 700;
}
.modal-close {
background: none;
border: none;
font-size: 22px;
color: var(--text-muted);
cursor: pointer;
line-height: 1;
padding: 0;
}
.modal-close:hover {
color: var(--text);
}
.modal-form {
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
flex: 1;
}
.form-group label {
font-size: 13px;
font-weight: 500;
color: var(--text-muted);
}
.form-input,
.form-textarea {
background: var(--bg-surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text);
font-size: 14px;
padding: 10px 12px;
outline: none;
transition: border-color 0.15s;
font-family: inherit;
width: 100%;
}
.form-input[type="range"] {
padding: 0;
cursor: pointer;
}
.form-input:focus,
.form-textarea:focus {
border-color: var(--accent);
}
.form-textarea {
resize: vertical;
min-height: 80px;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
padding-top: 4px;
}

View File

@@ -1,4 +1,5 @@
import { useState } from 'preact/hooks';
import './Modal.css';
interface NewRecommendationModalProps {
onClose: () => void;
@@ -7,6 +8,7 @@ interface NewRecommendationModalProps {
liked_shows: string;
disliked_shows: string;
themes: string;
brainstorm_count?: number;
}) => Promise<void>;
}
@@ -15,6 +17,7 @@ export function NewRecommendationModal({ onClose, onSubmit }: NewRecommendationM
const [likedShows, setLikedShows] = useState('');
const [dislikedShows, setDislikedShows] = useState('');
const [themes, setThemes] = useState('');
const [brainstormCount, setBrainstormCount] = useState(100);
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: Event) => {
@@ -27,6 +30,7 @@ export function NewRecommendationModal({ onClose, onSubmit }: NewRecommendationM
liked_shows: likedShows.trim(),
disliked_shows: dislikedShows.trim(),
themes: themes.trim(),
brainstorm_count: brainstormCount,
});
onClose();
} finally {
@@ -62,30 +66,28 @@ export function NewRecommendationModal({ onClose, onSubmit }: NewRecommendationM
/>
</div>
<div class="form-row">
<div class="form-group">
<label for="liked-shows">Shows you liked</label>
<input
id="liked-shows"
type="text"
class="form-input"
placeholder="e.g. Breaking Bad, The Wire"
value={likedShows}
onInput={(e) => setLikedShows((e.target as HTMLInputElement).value)}
/>
</div>
<div class="form-group">
<label for="liked-shows">Shows you liked</label>
<input
id="liked-shows"
type="text"
class="form-input"
placeholder="e.g. Breaking Bad, The Wire"
value={likedShows}
onInput={(e) => setLikedShows((e.target as HTMLInputElement).value)}
/>
</div>
<div class="form-group">
<label for="disliked-shows">Shows you disliked</label>
<input
id="disliked-shows"
type="text"
class="form-input"
placeholder="e.g. Game of Thrones"
value={dislikedShows}
onInput={(e) => setDislikedShows((e.target as HTMLInputElement).value)}
/>
</div>
<div class="form-group">
<label for="disliked-shows">Shows you disliked</label>
<input
id="disliked-shows"
type="text"
class="form-input"
placeholder="e.g. Game of Thrones"
value={dislikedShows}
onInput={(e) => setDislikedShows((e.target as HTMLInputElement).value)}
/>
</div>
<div class="form-group">
@@ -100,6 +102,20 @@ export function NewRecommendationModal({ onClose, onSubmit }: NewRecommendationM
/>
</div>
<div class="form-group">
<label for="brainstorm-count">Shows to brainstorm ({brainstormCount})</label>
<input
id="brainstorm-count"
type="range"
class="form-input"
min={50}
max={200}
step={10}
value={brainstormCount}
onInput={(e) => setBrainstormCount(Number((e.target as HTMLInputElement).value))}
/>
</div>
<div class="modal-actions">
<button type="button" class="btn-secondary" onClick={onClose} disabled={loading}>
Cancel

View File

@@ -0,0 +1,86 @@
/* ── Pipeline Progress ──────────────────────────────────── */
.pipeline-progress {
padding: 40px 0;
}
.pipeline-title {
font-size: 18px;
font-weight: 600;
color: var(--text);
margin-bottom: 24px;
}
.pipeline-steps {
list-style: none;
display: flex;
flex-direction: column;
gap: 12px;
}
.pipeline-step {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: 14px;
}
.pipeline-step--running {
border-color: var(--accent);
background: var(--accent-dim);
}
.pipeline-step--done {
border-color: rgba(34, 197, 94, 0.3);
background: rgba(34, 197, 94, 0.05);
}
.pipeline-step--error {
border-color: rgba(239, 68, 68, 0.3);
background: rgba(239, 68, 68, 0.05);
}
.stage-icon {
font-size: 16px;
width: 20px;
text-align: center;
}
.stage-done {
color: var(--green);
}
.stage-error {
color: var(--red);
}
.stage-running {
color: var(--accent);
}
.stage-pending {
color: var(--text-dim);
}
.spinner {
display: inline-block;
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.pipeline-step-label {
font-weight: 500;
}

View File

@@ -1,3 +1,4 @@
import './PipelineProgress.css';
import type { StageMap, StageStatus } from '../types/index.js';
const STAGES: { key: keyof StageMap; label: string }[] = [

View File

@@ -1,4 +1,5 @@
import { useState } from 'preact/hooks';
import './Cards.css';
import type { CuratorOutput, CuratorCategory } from '../types/index.js';
interface RecommendationCardProps {

View File

@@ -0,0 +1,124 @@
/* ── Sidebar ────────────────────────────────────────────── */
.sidebar {
width: var(--sidebar-width);
background: var(--bg-surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
flex-shrink: 0;
}
.sidebar-header {
padding: 20px 16px 12px;
border-bottom: 1px solid var(--border);
text-align: center;
}
.sidebar-title {
font-size: 24px;
font-weight: 700;
color: var(--text);
letter-spacing: -0.3px;
text-decoration: none;
display: inline-block;
transition: color 0.15s ease;
padding-bottom: 10px;
}
.sidebar-title:hover {
color: var(--accent);
}
.btn-new {
margin: 12px;
padding: 10px 14px;
background: var(--accent);
color: #fff;
border: none;
border-radius: var(--radius);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
text-align: left;
}
.btn-new:hover {
background: var(--accent-hover);
}
.sidebar-section-label {
padding: 8px 16px 4px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-dim);
}
.sidebar-list {
list-style: none;
overflow-y: auto;
flex: 1;
padding: 4px 8px 8px;
}
.sidebar-empty {
padding: 8px;
color: var(--text-dim);
font-size: 13px;
}
.sidebar-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border-radius: var(--radius-sm);
cursor: pointer;
color: var(--text-muted);
font-size: 13px;
transition: background 0.1s, color 0.1s;
user-select: none;
}
.sidebar-item:hover {
background: var(--bg-surface-2);
color: var(--text);
}
.sidebar-item.selected {
background: var(--accent-dim);
color: var(--text);
}
.sidebar-icon {
font-size: 12px;
width: 16px;
text-align: center;
flex-shrink: 0;
}
.status-done {
color: var(--green);
}
.status-error {
color: var(--red);
}
.status-running {
color: var(--accent);
}
.status-pending {
color: var(--text-dim);
}
.sidebar-item-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@@ -1,3 +1,4 @@
import './Sidebar.css';
import type { RecommendationSummary } from '../types/index.js';
interface SidebarProps {
@@ -29,7 +30,7 @@ export function Sidebar({ list, selectedId, onSelect, onNewClick }: SidebarProps
return (
<aside class="sidebar">
<div class="sidebar-header">
<span class="sidebar-title">Recommender</span>
<a href="/" class="sidebar-title">Recommender</a>
</div>
<button class="btn-new" onClick={onNewClick}>