Shot page UI redesign

jasonnovack@jasonnovackJan 8, 2026claude_codeclaude-opus-4-5-20251101ui

Preview

BeforeBefore preview
AfterAfter preview

Diff

diff --git a/packages/web/src/app/globals.css b/packages/web/src/app/globals.css
index 8b75b6a..22a4ed6 100644
--- a/packages/web/src/app/globals.css
+++ b/packages/web/src/app/globals.css
@@ -1114,251 +1114,761 @@ select {
}
/* --------------------------------------------------------------------------
- Shot Detail Page
+ Shot Detail Page - World-Class Redesign
-------------------------------------------------------------------------- */
+.shot-detail-page {
+ max-width: var(--container-lg);
+ padding-bottom: 100px; /* Space for mobile sticky bar */
+}
+
+/* Hero Section - Full-width preview images */
+.shot-hero {
+ margin: 0 calc(-1 * var(--space-5));
+ margin-bottom: var(--space-8);
+}
+
+@media (min-width: 768px) {
+ .shot-hero {
+ margin: 0 calc(-1 * var(--space-8));
+ margin-bottom: var(--space-10);
+ }
+}
+
+.shot-hero-images {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: var(--space-4);
+}
+
+@media (min-width: 768px) {
+ .shot-hero-images {
+ grid-template-columns: repeat(2, 1fr);
+ gap: var(--space-6);
+ }
+
+ .shot-hero-images:has(.shot-hero-before):has(.shot-hero-after) {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .shot-hero-images:not(:has(.shot-hero-before)) {
+ grid-template-columns: 1fr;
+ max-width: 800px;
+ margin: 0 auto;
+ }
+}
+
+.shot-hero-image {
+ position: relative;
+ border-radius: var(--radius-xl);
+ overflow: hidden;
+ background: var(--surface);
+ border: 1px solid var(--border);
+}
+
+.shot-hero-image img {
+ width: 100%;
+ display: block;
+ transition: transform var(--transition-base);
+}
+
+.shot-hero-image a:hover img {
+ transform: scale(1.02);
+}
+
+.shot-hero-badge {
+ position: absolute;
+ top: var(--space-4);
+ left: var(--space-4);
+ padding: var(--space-2) var(--space-4);
+ background: rgba(0, 0, 0, 0.75);
+ backdrop-filter: blur(8px);
+ border-radius: var(--radius-full);
+ font-size: var(--text-tiny);
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: var(--tracking-wide);
+ color: var(--text-primary);
+}
+
+.shot-hero-after .shot-hero-badge {
+ background: var(--accent);
+ color: white;
+}
+
+/* Title Bar */
+.shot-title-bar {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-4);
+ margin-bottom: var(--space-6);
+}
+
+@media (min-width: 768px) {
+ .shot-title-bar {
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: var(--space-8);
+ }
+}
+
+.shot-title-content {
+ flex: 1;
+ min-width: 0;
+}
+
+.shot-title-content h1 {
+ font-size: clamp(1.5rem, 4vw, 2.25rem);
+ font-weight: 700;
+ line-height: var(--leading-tight);
+ margin: 0 0 var(--space-3) 0;
+ letter-spacing: var(--tracking-tight);
+}
+
+.shot-byline {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ flex-wrap: wrap;
+}
+
+.shot-author {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ color: var(--text-secondary);
+ font-weight: 500;
+ transition: color var(--transition-fast);
+}
+
+.shot-author:hover {
+ color: var(--accent);
+}
+
+.shot-author-avatar {
+ width: 28px;
+ height: 28px;
+ border-radius: var(--radius-full);
+ border: 2px solid var(--border);
+}
+
+.shot-author-name {
+ font-size: var(--text-body);
+}
+
+.shot-author-anon {
+ color: var(--text-muted);
+}
+
+.shot-byline-sep {
+ color: var(--text-disabled);
+}
+
+.shot-timestamp {
+ color: var(--text-muted);
+ font-size: var(--text-small);
+}
+
+.shot-title-actions {
+ display: none; /* Hidden on mobile, shown in sticky bar */
+ gap: var(--space-3);
+ flex-shrink: 0;
+}
+
+@media (min-width: 768px) {
+ .shot-title-actions {
+ display: flex;
+ }
+}
+
+/* Tech Stack Pills */
+.shot-tech-stack {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-2);
+ margin-bottom: var(--space-6);
+}
+
+.shot-pill {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-2);
+ padding: var(--space-2) var(--space-4);
+ border-radius: var(--radius-full);
+ font-size: var(--text-small);
+ font-weight: 500;
+ transition: all var(--transition-fast);
+}
+
+.shot-pill svg {
+ opacity: 0.7;
+}
+
+.shot-pill-harness {
+ background: linear-gradient(135deg, rgba(249, 115, 22, 0.15), rgba(249, 115, 22, 0.05));
+ border: 1px solid rgba(249, 115, 22, 0.3);
+ color: var(--accent);
+}
+
+.shot-pill-model {
+ background: linear-gradient(135deg, rgba(139, 92, 246, 0.15), rgba(139, 92, 246, 0.05));
+ border: 1px solid rgba(139, 92, 246, 0.3);
+ color: #a78bfa;
+}
+
+.shot-pill-type {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ color: var(--text-secondary);
+ text-transform: capitalize;
+}
+
+.shot-pill-tag {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ color: var(--text-muted);
+}
+
+.shot-pill-tag::before {
+ content: '#';
+ opacity: 0.5;
+}
+
+/* Stats Row */
+.shot-stats-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-4);
+ padding: var(--space-5);
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-xl);
+ margin-bottom: var(--space-6);
+}
+
+@media (min-width: 768px) {
+ .shot-stats-row {
+ gap: var(--space-8);
+ padding: var(--space-6);
+ }
+}
+
+.shot-stat {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ color: var(--text-muted);
+}
+
+.shot-stat svg {
+ opacity: 0.6;
+}
+
+.shot-stat-value {
+ font-size: var(--text-h2);
+ font-weight: 700;
+ font-family: var(--font-mono);
+ color: var(--text-primary);
+}
+
+.shot-stat-label {
+ font-size: var(--text-tiny);
+ text-transform: uppercase;
+ letter-spacing: var(--tracking-wide);
+}
+
+.shot-stat-add .shot-stat-value {
+ color: var(--success);
+}
+
+.shot-stat-add svg {
+ color: var(--success);
+ opacity: 1;
+}
+
+.shot-stat-remove .shot-stat-value {
+ color: var(--error);
+}
+
+.shot-stat-remove svg {
+ color: var(--error);
+ opacity: 1;
+}
+
+/* Links Card */
+.shot-links-card {
+ margin-bottom: var(--space-8);
+}
+
+.shot-links-grid {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-3);
+}
+
+@media (min-width: 768px) {
+ .shot-links-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: var(--space-4);
+ }
+}
+
+.shot-link-item {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ padding: var(--space-4);
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ color: var(--text-secondary);
+ text-decoration: none;
+ transition: all var(--transition-fast);
+}
+
+.shot-link-item:hover {
+ border-color: var(--accent);
+ background: var(--accent-subtle);
+ color: var(--text-primary);
+ transform: translateY(-2px);
+}
+
+.shot-link-item svg {
+ flex-shrink: 0;
+ opacity: 0.7;
+}
+
+.shot-link-item:hover svg {
+ opacity: 1;
+}
+
+.shot-link-text {
+ flex: 1;
+ font-weight: 500;
+ font-size: var(--text-small);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.shot-link-arrow {
+ font-size: var(--text-small);
+ opacity: 0.5;
+ transition: all var(--transition-fast);
+}
+
+.shot-link-item:hover .shot-link-arrow {
+ opacity: 1;
+ transform: translate(2px, -2px);
+}
+
+.shot-commit-hashes {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ font-size: var(--text-tiny);
+}
+
+.shot-commit-hashes code {
+ padding: 2px 6px;
+ background: var(--surface-elevated);
+ border-radius: var(--radius-sm);
+ font-family: var(--font-mono);
+}
+
+.shot-commit-hashes span {
+ color: var(--text-disabled);
+}
+
+.shot-link-diff {
+ flex-wrap: wrap;
+}
+
+.shot-link-preview {
+ cursor: default;
+}
+
+.shot-preview-links {
+ display: flex;
+ gap: var(--space-3);
+ margin-left: auto;
+}
+
+.shot-preview-links a {
+ color: var(--success);
+ font-size: var(--text-small);
+ font-weight: 500;
+ padding: var(--space-1) var(--space-3);
+ background: var(--success-subtle);
+ border-radius: var(--radius-md);
+ transition: all var(--transition-fast);
+}
+
+.shot-preview-links a:hover {
+ background: var(--success-muted);
+}
+
+/* Mobile Sticky Actions Bar */
+.shot-mobile-actions {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--space-4);
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: var(--space-4);
+ background: rgba(10, 10, 10, 0.95);
+ backdrop-filter: blur(12px);
+ border-top: 1px solid var(--border);
+ z-index: 50;
+}
+
+@media (min-width: 768px) {
+ .shot-mobile-actions {
+ display: none;
+ }
+
+ .shot-detail-page {
+ padding-bottom: 0;
+ }
+}
+
+/* Legacy support */
.shot-detail {
max-width: var(--container-lg);
}
-.shot-detail h1 {
- font-size: var(--text-h1);
+.shot-detail h3 {
+ margin-top: var(--space-10);
+ margin-bottom: var(--space-5);
+ font-size: var(--text-h2);
+ padding-bottom: var(--space-3);
+ border-bottom: 1px solid var(--border);
+}
+
+/* --------------------------------------------------------------------------
+ Recipe Panel - The learning section
+ -------------------------------------------------------------------------- */
+
+.recipe-panel {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-xl);
+ padding: var(--space-6);
+ margin-top: var(--space-8);
+}
+
+.recipe-panel-header {
+ margin-bottom: var(--space-6);
+}
+
+.recipe-panel-header h3 {
+ margin: 0 0 var(--space-2) 0;
+ padding: 0;
+ border: none;
+ font-size: var(--text-h2);
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ color: var(--text-primary);
+}
+
+.recipe-panel-header h3 svg {
+ color: var(--accent);
+}
+
+.recipe-panel-subtitle {
+ color: var(--text-muted);
+ font-size: var(--text-small);
+ margin: 0;
+}
+
+/* Recipe Info Grid - Model & Harness Cards */
+.recipe-info-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: var(--space-4);
+ margin-bottom: var(--space-5);
+}
+
+.recipe-info-card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: var(--space-2);
+ padding: var(--space-5);
+ background: linear-gradient(135deg, var(--surface-elevated), var(--bg));
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-lg);
+ text-align: center;
+}
+
+.recipe-info-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ background: var(--accent-subtle);
+ border-radius: var(--radius-md);
+ color: var(--accent);
+}
+
+.recipe-info-label {
+ font-size: var(--text-tiny);
+ color: var(--text-muted);
+ text-transform: uppercase;
+ letter-spacing: var(--tracking-wider);
+ font-weight: 600;
+}
+
+.recipe-info-value {
+ font-size: var(--text-body);
+ color: var(--text-primary);
+ font-weight: 500;
+}
+
+.recipe-info-value code {
+ font-size: var(--text-small);
+ background: transparent;
+ padding: 0;
+ color: var(--accent);
+}
+
+/* Recipe Sections */
+.recipe-section {
+ padding: var(--space-4);
+ background: var(--bg);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-lg);
margin-bottom: var(--space-4);
- line-height: var(--leading-tight);
}
-.shot-header {
+.recipe-section:last-of-type {
+ margin-bottom: 0;
+}
+
+.recipe-section-prompt {
+ background: linear-gradient(135deg, var(--accent-subtle), transparent);
+ border-color: var(--accent-muted);
+}
+
+.recipe-section-raw {
+ background: var(--surface);
+}
+
+.recipe-header {
display: flex;
justify-content: space-between;
- align-items: flex-start;
- gap: var(--space-6);
- padding-bottom: var(--space-6);
- border-bottom: 1px solid var(--border);
+ align-items: center;
+ margin-bottom: var(--space-3);
}
-.shot-header-content {
- flex: 1;
- min-width: 0;
+.recipe-label {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ color: var(--text-muted);
+ font-size: var(--text-tiny);
+ text-transform: uppercase;
+ letter-spacing: var(--tracking-wider);
+ font-weight: 600;
+}
+
+.recipe-label svg {
+ color: var(--text-disabled);
}
-.shot-meta-detail {
+/* Token Usage */
+.recipe-tokens {
display: flex;
flex-wrap: wrap;
- align-items: center;
gap: var(--space-3);
- margin-top: var(--space-2);
}
-/* Shot Info Section */
-.shot-info {
+.recipe-token-stat {
display: flex;
flex-direction: column;
- gap: var(--space-4);
- margin-top: var(--space-6);
- padding: var(--space-5);
+ gap: var(--space-1);
+ padding: var(--space-3);
background: var(--surface);
- border: 1px solid var(--border);
- border-radius: var(--radius-xl);
+ border-radius: var(--radius-md);
+ min-width: 80px;
}
-.shot-info-row {
- display: flex;
- align-items: center;
- gap: var(--space-3);
- font-size: var(--text-small);
+.recipe-token-label {
+ font-size: var(--text-tiny);
+ color: var(--text-muted);
+ text-transform: uppercase;
+ letter-spacing: var(--tracking-wide);
}
-.shot-info-label {
- color: var(--text-muted);
- font-weight: 500;
- min-width: 80px;
+.recipe-token-value {
+ font-size: var(--text-body);
+ font-weight: 600;
+ color: var(--text-secondary);
+ font-family: var(--font-mono);
}
-/* Commit Links */
-.commit-links {
- display: flex;
- align-items: center;
- gap: var(--space-2);
- flex-wrap: wrap;
+.recipe-token-total .recipe-token-value {
+ color: var(--accent);
}
-.commit-links code {
- padding: 2px 8px;
- font-size: var(--text-tiny);
+.recipe-token-cache .recipe-token-value {
+ color: var(--success);
}
-.separator {
- color: var(--text-disabled);
- margin: 0 var(--space-1);
+/* Parameters */
+.recipe-params {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-2);
}
-.github-diff-link {
- color: var(--text-muted) !important;
- font-size: var(--text-small);
- font-weight: 500;
- transition: color var(--transition-fast);
+.recipe-param {
+ padding: var(--space-1) var(--space-2);
+ background: var(--surface);
+ border-radius: var(--radius-sm);
}
-.github-diff-link:hover {
- color: var(--accent) !important;
+.recipe-param code {
+ font-size: var(--text-tiny);
+ background: transparent;
+ padding: 0;
}
-/* Preview Links */
-.preview-links {
+/* Plugins */
+.recipe-plugins {
display: flex;
- align-items: center;
- gap: var(--space-2);
flex-wrap: wrap;
+ gap: var(--space-2);
}
-.preview-link {
+.recipe-plugin-badge {
display: inline-flex;
align-items: center;
gap: var(--space-1);
- color: var(--success);
- font-weight: 500;
padding: var(--space-1) var(--space-3);
- background: var(--success-muted);
- border-radius: var(--radius-md);
- font-size: var(--text-small);
- transition: all var(--transition-fast);
-}
-
-.preview-link:hover {
- background: var(--success-subtle);
- color: var(--success);
- transform: translateY(-1px);
-}
-
-/* Social Links */
-.social-link {
- color: var(--text-muted);
- font-size: var(--text-small);
+ background: var(--accent-subtle);
+ border: 1px solid var(--accent-muted);
+ border-radius: var(--radius-full);
+ font-size: var(--text-tiny);
font-weight: 500;
- transition: color var(--transition-fast);
+ color: var(--accent);
}
-.social-link:hover {
- color: var(--accent);
+/* MCP Servers */
+.recipe-mcp-list {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2);
}
-/* Screenshot Preview */
-.screenshot-preview {
- margin-top: var(--space-10);
+.recipe-mcp-item {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ padding: var(--space-2) var(--space-3);
+ background: var(--surface);
+ border-radius: var(--radius-md);
}
-.screenshot-preview h3 {
- margin-bottom: var(--space-5);
- font-size: var(--text-h2);
+.recipe-mcp-item code {
+ font-size: var(--text-small);
+ background: transparent;
+ padding: 0;
+ color: var(--text-primary);
}
-.screenshot-container {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
- gap: var(--space-6);
+.recipe-mcp-command {
+ font-size: var(--text-tiny);
+ color: var(--text-muted);
+ font-family: var(--font-mono);
}
-.screenshot-item {
+/* Config Files */
+.recipe-configs {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
-.screenshot-label {
- font-size: var(--text-tiny);
- text-transform: uppercase;
- color: var(--text-muted);
- letter-spacing: var(--tracking-wider);
- font-weight: 600;
+.recipe-config-item {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: var(--space-2);
}
-.screenshot-image {
- width: 100%;
- border-radius: var(--radius-xl);
+.recipe-config-toggle {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ padding: var(--space-2) var(--space-3);
+ background: var(--surface);
border: 1px solid var(--border);
- transition: all var(--transition-base);
+ border-radius: var(--radius-md);
+ color: var(--text-secondary);
+ font-size: var(--text-small);
+ cursor: pointer;
+ transition: all var(--transition-fast);
}
-.screenshot-item a:hover .screenshot-image {
+.recipe-config-toggle:hover {
border-color: var(--accent);
- transform: translateY(-4px);
- box-shadow: var(--shadow-lg);
+ color: var(--accent);
}
-/* Full Diff Display */
-.full-diff {
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: var(--radius-xl);
- padding: var(--space-6);
- font-family: var(--font-mono);
- font-size: var(--text-small);
- line-height: var(--leading-relaxed);
- overflow-x: auto;
- white-space: pre;
- margin: var(--space-6) 0;
+.recipe-config-toggle svg {
+ color: var(--text-muted);
}
-/* Section Headers in Detail Page */
-.shot-detail h3 {
- margin-top: var(--space-10);
- margin-bottom: var(--space-5);
- font-size: var(--text-h2);
- padding-bottom: var(--space-3);
- border-bottom: 1px solid var(--border);
+.recipe-config-toggle code {
+ background: transparent;
+ padding: 0;
+ font-size: var(--text-small);
}
-/* --------------------------------------------------------------------------
- Recipe Panel - The learning section
- -------------------------------------------------------------------------- */
-
-.recipe-panel {
+.recipe-config-content {
+ width: 100%;
+ margin-top: var(--space-2);
+ padding: var(--space-4);
background: var(--surface);
border: 1px solid var(--border);
- border-radius: var(--radius-xl);
- padding: var(--space-6);
- margin-top: var(--space-10);
+ border-radius: var(--radius-md);
+ max-height: 300px;
+ overflow: auto;
}
-.recipe-panel > h3 {
- margin: 0 0 var(--space-6) 0;
+.recipe-config-content pre {
+ margin: 0;
padding: 0;
+ background: transparent;
border: none;
- font-size: var(--text-h2);
- display: flex;
- align-items: center;
- gap: var(--space-2);
-}
-
-.recipe-panel > h3::before {
- content: '📋';
-}
-
-.recipe-grid {
- display: grid;
- gap: var(--space-5);
-}
-
-.recipe-section {
- padding: var(--space-4);
- background: var(--bg);
- border: 1px solid var(--border-subtle);
- border-radius: var(--radius-lg);
-}
-
-.recipe-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: var(--space-3);
+ white-space: pre-wrap;
+ font-size: var(--text-small);
}
-.recipe-label {
+/* Toggle Button */
+.recipe-toggle-btn {
+ padding: var(--space-1) var(--space-3);
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
color: var(--text-muted);
font-size: var(--text-tiny);
- text-transform: uppercase;
- letter-spacing: var(--tracking-wider);
- font-weight: 600;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all var(--transition-fast);
+}
+
+.recipe-toggle-btn:hover {
+ border-color: var(--accent);
+ color: var(--accent);
}
.recipe-value {
@@ -1448,19 +1958,41 @@ select {
overflow-y: auto;
}
+/* Prompt Box Compact */
+.prompt-box-compact {
+ max-height: 200px;
+ overflow: auto;
+}
+
/* Recipe Tip */
.recipe-tip {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--space-3);
margin-top: var(--space-6);
padding: var(--space-4) var(--space-5);
- background: var(--accent-subtle);
- border-left: 3px solid var(--accent);
- border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
+ background: linear-gradient(135deg, var(--accent-subtle), transparent);
+ border: 1px solid var(--accent-muted);
+ border-radius: var(--radius-lg);
font-size: var(--text-small);
color: var(--text-muted);
+ line-height: var(--leading-relaxed);
+}
+
+.recipe-tip svg {
+ flex-shrink: 0;
+ color: var(--accent);
+ margin-top: 2px;
}
.recipe-tip strong {
+ color: var(--accent);
+ font-weight: 600;
+}
+
+.recipe-tip em {
color: var(--text-secondary);
+ font-style: italic;
}
/* --------------------------------------------------------------------------
@@ -2315,12 +2847,54 @@ select {
/* Recipe Panel */
.recipe-panel {
padding: var(--space-4);
+ margin-top: var(--space-6);
+ }
+
+ .recipe-panel-header h3 {
+ font-size: var(--text-h3);
+ }
+
+ .recipe-info-grid {
+ grid-template-columns: 1fr;
+ gap: var(--space-3);
+ }
+
+ .recipe-info-card {
+ padding: var(--space-4);
+ flex-direction: row;
+ justify-content: flex-start;
+ text-align: left;
+ gap: var(--space-3);
+ }
+
+ .recipe-info-icon {
+ width: 36px;
+ height: 36px;
}
.recipe-section {
padding: var(--space-3);
}
+ .recipe-tokens {
+ gap: var(--space-2);
+ }
+
+ .recipe-token-stat {
+ padding: var(--space-2);
+ min-width: 70px;
+ }
+
+ .recipe-tip {
+ flex-direction: column;
+ gap: var(--space-2);
+ padding: var(--space-3) var(--space-4);
+ }
+
+ .recipe-tip svg {
+ display: none;
+ }
+
/* User Profile */
.user-header {
flex-direction: column;
diff --git a/packages/web/src/app/shots/[id]/page.tsx b/packages/web/src/app/shots/[id]/page.tsx
index 41b793c..9593677 100644
--- a/packages/web/src/app/shots/[id]/page.tsx
+++ b/packages/web/src/app/shots/[id]/page.tsx
@@ -33,12 +33,9 @@ function extractGenerationTime(sessionData: string | null): number | null {
if (!sessionData) return null
try {
const data = JSON.parse(sessionData)
- // Check for tokenUsage which might contain timing info
- // Or calculate from message timestamps if available
if (data.generationTimeMs) {
return data.generationTimeMs
}
- // Try to calculate from token usage (rough estimate: ~50 tokens/second)
if (data.tokenUsage?.totalTokens) {
return Math.round(data.tokenUsage.totalTokens / 50 * 1000)
}
@@ -56,6 +53,16 @@ function formatDuration(ms: number): string {
return `${minutes}m ${seconds}s`
}
+// Format harness name for display
+function formatHarness(harness: string): string {
+ const names: Record<string, string> = {
+ claude_code: 'Claude Code',
+ cursor: 'Cursor',
+ codex: 'Codex CLI',
+ }
+ return names[harness] || harness
+}
+
export const dynamic = 'force-dynamic'
interface Props {
@@ -80,66 +87,67 @@ export default async function ShotDetailPage({ params }: Props) {
}
const { shot, user } = result
+ const diffStats = computeDiffStats(shot.diff)
+ const generationTime = extractGenerationTime(shot.sessionData)
return (
- <article className="shot-detail">
- <header className="shot-header">
- <div className="shot-header-content">
+ <article className="shot-detail-page">
+ {/* Hero Section with Preview */}
+ {shot.afterPreviewUrl && (
+ <section className="shot-hero">
+ <div className="shot-hero-images">
+ {shot.beforePreviewUrl && (
+ <div className="shot-hero-image shot-hero-before">
+ <span className="shot-hero-badge">Before</span>
+ <a href={shot.beforePreviewUrl} target="_blank" rel="noopener noreferrer">
+ <img
+ src={`https://image.thum.io/get/width/900/${shot.beforePreviewUrl}`}
+ alt="Before preview"
+ loading="eager"
+ />
+ </a>
+ </div>
+ )}
+ <div className="shot-hero-image shot-hero-after">
+ <span className="shot-hero-badge">{shot.beforePreviewUrl ? 'After' : 'Preview'}</span>
+ <a href={shot.afterPreviewUrl} target="_blank" rel="noopener noreferrer">
+ <img
+ src={`https://image.thum.io/get/width/900/${shot.afterPreviewUrl}`}
+ alt="After preview"
+ loading="eager"
+ />
+ </a>
+ </div>
+ </div>
+ </section>
+ )}
+
+ {/* Title and Actions Bar */}
+ <header className="shot-title-bar">
+ <div className="shot-title-content">
<h1>{shot.title}</h1>
- <div className="shot-meta-detail">
+ <div className="shot-byline">
{user ? (
- <>
- <Link href={`/u/${user.username}`} className="author-link author-with-avatar">
- {user.avatarUrl && (
- <img src={user.avatarUrl} alt={user.username} className="author-avatar" />
- )}
- @{user.username}
- </Link>
- <a
- href={`https://github.com/${user.username}`}
- target="_blank"
- rel="noopener noreferrer"
- className="social-link"
- title="GitHub"
- >
- <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style={{ marginRight: '4px', verticalAlign: '-2px' }}>
- <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
- </svg>
- GitHub
- </a>
- {user.xUsername && (
- <a
- href={`https://x.com/${user.xUsername}`}
- target="_blank"
- rel="noopener noreferrer"
- className="social-link"
- title="X/Twitter"
- >
- @{user.xUsername}
- </a>
+ <Link href={`/u/${user.username}`} className="shot-author">
+ {user.avatarUrl && (
+ <img src={user.avatarUrl} alt="" className="shot-author-avatar" />
)}
- </>
+ <span className="shot-author-name">@{user.username}</span>
+ </Link>
) : (
- <span className="anonymous-author">Anonymous</span>
+ <span className="shot-author-anon">Anonymous</span>
)}
- <span className="shot-date">
+ <span className="shot-byline-sep">·</span>
+ <time className="shot-timestamp">
{new Date(shot.createdAt).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
- </span>
- </div>
- <div className="shot-meta-detail" style={{ marginTop: '12px' }}>
- <span className="badge badge-accent">{shot.harness}</span>
- <span className="badge">{shot.model}</span>
- <span className="badge">{shot.type}</span>
- {shot.tags?.map((tag) => (
- <span key={tag} className="badge">{tag}</span>
- ))}
+ </time>
</div>
</div>
- <div className="shot-actions">
+ <div className="shot-title-actions">
<UpvoteButton shotId={shot.id} initialCount={shot.starCount || 0} />
<ShareButton
shotId={shot.id}
@@ -151,148 +159,117 @@ export default async function ShotDetailPage({ params }: Props) {
</div>
</header>
- <div className="shot-info">
- {/* Live Preview Links */}
- {(shot.beforePreviewUrl || shot.afterPreviewUrl) && (
- <div className="shot-info-row">
- <span className="shot-info-label">Live Preview</span>
- <div className="preview-links">
- {shot.beforePreviewUrl && (
- <a
- href={shot.beforePreviewUrl}
- target="_blank"
- rel="noopener noreferrer"
- className="preview-link"
- >
- Before ↗
- </a>
- )}
- {shot.beforePreviewUrl && shot.afterPreviewUrl && (
- <span className="separator">→</span>
- )}
- {shot.afterPreviewUrl && (
- <a
- href={shot.afterPreviewUrl}
- target="_blank"
- rel="noopener noreferrer"
- className="preview-link"
- >
- After ↗
- </a>
- )}
- </div>
+ {/* Tech Stack Pills */}
+ <div className="shot-tech-stack">
+ <span className="shot-pill shot-pill-harness">
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+ <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
+ </svg>
+ {formatHarness(shot.harness)}
+ </span>
+ <span className="shot-pill shot-pill-model">
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+ <circle cx="12" cy="12" r="3"/>
+ <path d="M12 1v6m0 6v10M4.22 4.22l4.24 4.24m7.08 7.08l4.24 4.24M1 12h6m6 0h10M4.22 19.78l4.24-4.24m7.08-7.08l4.24-4.24"/>
+ </svg>
+ {shot.model}
+ </span>
+ <span className="shot-pill shot-pill-type">{shot.type}</span>
+ {shot.tags?.map((tag) => (
+ <span key={tag} className="shot-pill shot-pill-tag">{tag}</span>
+ ))}
+ </div>
+
+ {/* Stats Row */}
+ <div className="shot-stats-row">
+ <div className="shot-stat">
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
+ <polyline points="14 2 14 8 20 8"/>
+ </svg>
+ <span className="shot-stat-value">{diffStats.filesChanged}</span>
+ <span className="shot-stat-label">files</span>
+ </div>
+ <div className="shot-stat shot-stat-add">
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+ <line x1="12" y1="5" x2="12" y2="19"/>
+ <line x1="5" y1="12" x2="19" y2="12"/>
+ </svg>
+ <span className="shot-stat-value">{diffStats.additions}</span>
+ <span className="shot-stat-label">added</span>
+ </div>
+ <div className="shot-stat shot-stat-remove">
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+ <line x1="5" y1="12" x2="19" y2="12"/>
+ </svg>
+ <span className="shot-stat-value">{diffStats.deletions}</span>
+ <span className="shot-stat-label">removed</span>
+ </div>
+ {generationTime && (
+ <div className="shot-stat">
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+ <circle cx="12" cy="12" r="10"/>
+ <polyline points="12 6 12 12 16 14"/>
+ </svg>
+ <span className="shot-stat-value">{formatDuration(generationTime)}</span>
+ <span className="shot-stat-label">time</span>
</div>
)}
- <div className="shot-info-row">
- <span className="shot-info-label">Commits</span>
- <div className="commit-links">
- <a
- href={`${shot.repoUrl}/tree/${shot.beforeCommitHash}`}
- target="_blank"
- rel="noopener noreferrer"
- title="View code at this commit"
- >
+ </div>
+
+ {/* Quick Links Card */}
+ <div className="shot-links-card">
+ <div className="shot-links-grid">
+ <a
+ href={shot.repoUrl}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="shot-link-item"
+ >
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
+ <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
+ </svg>
+ <span className="shot-link-text">{shot.repoUrl.replace('https://github.com/', '')}</span>
+ <span className="shot-link-arrow">↗</span>
+ </a>
+ <a
+ href={`${shot.repoUrl}/compare/${shot.beforeCommitHash}...${shot.afterCommitHash}`}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="shot-link-item shot-link-diff"
+ >
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+ <path d="M16 3h5v5M8 3H3v5M3 16v5h5M21 16v5h-5M21 3l-9 9M3 21l9-9"/>
+ </svg>
+ <span className="shot-link-text">View diff</span>
+ <div className="shot-commit-hashes">
<code>{shot.beforeCommitHash.slice(0, 7)}</code>
- </a>
- <span className="separator">→</span>
- <a
- href={`${shot.repoUrl}/tree/${shot.afterCommitHash}`}
- target="_blank"
- rel="noopener noreferrer"
- title="View code at this commit"
- >
+ <span>→</span>
<code>{shot.afterCommitHash.slice(0, 7)}</code>
- </a>
- <span className="separator">|</span>
- <a
- href={`${shot.repoUrl}/compare/${shot.beforeCommitHash}...${shot.afterCommitHash}`}
- target="_blank"
- rel="noopener noreferrer"
- className="github-diff-link"
- >
- View diff on GitHub ↗
- </a>
- </div>
- </div>
- <div className="shot-info-row">
- <span className="shot-info-label">Repository</span>
- <a href={shot.repoUrl} target="_blank" rel="noopener noreferrer">
- {shot.repoUrl.replace('https://github.com/', '')}
+ </div>
+ <span className="shot-link-arrow">↗</span>
</a>
- </div>
- </div>
-
- {/* Screenshot Preview */}
- {shot.afterPreviewUrl && (
- <div className="screenshot-preview">
- <h3>Preview</h3>
- <div className="screenshot-container">
- {shot.beforePreviewUrl && (
- <div className="screenshot-item">
- <span className="screenshot-label">Before</span>
- <a href={shot.beforePreviewUrl} target="_blank" rel="noopener noreferrer">
- <img
- src={`https://image.thum.io/get/width/800/${shot.beforePreviewUrl}`}
- alt="Before preview"
- className="screenshot-image"
- loading="lazy"
- />
- </a>
+ {(shot.beforePreviewUrl || shot.afterPreviewUrl) && (
+ <div className="shot-link-item shot-link-preview">
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
+ <circle cx="12" cy="12" r="3"/>
+ </svg>
+ <span className="shot-link-text">Live preview</span>
+ <div className="shot-preview-links">
+ {shot.beforePreviewUrl && (
+ <a href={shot.beforePreviewUrl} target="_blank" rel="noopener noreferrer">Before ↗</a>
+ )}
+ {shot.afterPreviewUrl && (
+ <a href={shot.afterPreviewUrl} target="_blank" rel="noopener noreferrer">After ↗</a>
+ )}
</div>
- )}
- <div className="screenshot-item">
- <span className="screenshot-label">{shot.beforePreviewUrl ? 'After' : 'Live Preview'}</span>
- <a href={shot.afterPreviewUrl} target="_blank" rel="noopener noreferrer">
- <img
- src={`https://image.thum.io/get/width/800/${shot.afterPreviewUrl}`}
- alt="After preview"
- className="screenshot-image"
- loading="lazy"
- />
- </a>
</div>
- </div>
+ )}
</div>
- )}
-
- {/* Diff Stats */}
- {(() => {
- const diffStats = computeDiffStats(shot.diff)
- const generationTime = extractGenerationTime(shot.sessionData)
- return (
- <div className="diff-stats-panel">
- <div className="diff-stats">
- <div className="diff-stat">
- <span className="diff-stat-value">{diffStats.filesChanged}</span>
- <span className="diff-stat-label">files changed</span>
- </div>
- <div className="diff-stat diff-stat-add">
- <span className="diff-stat-value">+{diffStats.additions}</span>
- <span className="diff-stat-label">additions</span>
- </div>
- <div className="diff-stat diff-stat-remove">
- <span className="diff-stat-value">-{diffStats.deletions}</span>
- <span className="diff-stat-label">deletions</span>
- </div>
- {generationTime && (
- <div className="diff-stat">
- <span className="diff-stat-value">{formatDuration(generationTime)}</span>
- <span className="diff-stat-label">generation time</span>
- </div>
- )}
- </div>
- <a
- href={`${shot.repoUrl}/compare/${shot.beforeCommitHash}...${shot.afterCommitHash}`}
- target="_blank"
- rel="noopener noreferrer"
- className="view-diff-btn"
- >
- View full diff on GitHub ↗
- </a>
- </div>
- )
- })()}
+ </div>
+ {/* Recipe Panel */}
<RecipePanel
prompt={shot.prompt}
model={shot.model}
@@ -300,7 +277,20 @@ export default async function ShotDetailPage({ params }: Props) {
sessionData={shot.sessionData}
/>
+ {/* Comments */}
<Comments shotId={shot.id} />
+
+ {/* Mobile Sticky Actions */}
+ <div className="shot-mobile-actions">
+ <UpvoteButton shotId={shot.id} initialCount={shot.starCount || 0} />
+ <ShareButton
+ shotId={shot.id}
+ title={shot.title}
+ model={shot.model}
+ harness={shot.harness}
+ thumbnailUrl={shot.afterPreviewUrl ? `https://image.thum.io/get/width/800/${shot.afterPreviewUrl}` : undefined}
+ />
+ </div>
</article>
)
}
diff --git a/packages/web/src/components/RecipePanel.tsx b/packages/web/src/components/RecipePanel.tsx
index 0515824..83e14a0 100644
--- a/packages/web/src/components/RecipePanel.tsx
+++ b/packages/web/src/components/RecipePanel.tsx
@@ -50,6 +50,24 @@ interface RecipePanelProps {
sessionData?: string | null
}
+// Icon components for recipe sections
+function IconCopy() {
+ return (
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
+ </svg>
+ )
+}
+
+function IconCheck() {
+ return (
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+ <polyline points="20 6 9 17 4 12"/>
+ </svg>
+ )
+}
+
function CopyButton({ text, label }: { text: string; label: string }) {
const [copied, setCopied] = useState(false)
@@ -64,8 +82,9 @@ function CopyButton({ text, label }: { text: string; label: string }) {
}
return (
- <button onClick={handleCopy} className="copy-btn" title={`Copy ${label}`}>
- {copied ? '✓ Copied' : `Copy ${label}`}
+ <button onClick={handleCopy} className={`copy-btn ${copied ? 'copied' : ''}`} title={`Copy ${label}`}>
+ {copied ? <IconCheck /> : <IconCopy />}
+ <span>{copied ? 'Copied!' : 'Copy'}</span>
</button>
)
}
@@ -107,68 +126,126 @@ export function RecipePanel({ prompt, model, harness, sessionData }: RecipePanel
}
return (
- <div className="recipe-panel">
- <h3>Recipe</h3>
+ <section className="recipe-panel">
+ <header className="recipe-panel-header">
+ <h3>
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
+ <polyline points="14 2 14 8 20 8"/>
+ <line x1="16" y1="13" x2="8" y2="13"/>
+ <line x1="16" y1="17" x2="8" y2="17"/>
+ <polyline points="10 9 9 9 8 9"/>
+ </svg>
+ Recipe
+ </h3>
+ <p className="recipe-panel-subtitle">Everything you need to reproduce this transformation</p>
+ </header>
- <div className="recipe-section">
+ {/* Prompt Section - Hero of the Recipe */}
+ <div className="recipe-section recipe-section-prompt">
<div className="recipe-header">
- <span className="recipe-label">Model</span>
+ <span className="recipe-label">
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
+ </svg>
+ Prompt
+ </span>
+ <CopyButton text={prompt} label="Prompt" />
</div>
- <div className="recipe-value">
- <code>{model}</code>
+ <div className="prompt-box">
+ {prompt}
</div>
</div>
- <div className="recipe-section">
- <div className="recipe-header">
- <span className="recipe-label">Harness</span>
+ {/* Quick Info Grid */}
+ <div className="recipe-info-grid">
+ <div className="recipe-info-card">
+ <span className="recipe-info-icon">
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+ <circle cx="12" cy="12" r="3"/>
+ <path d="M12 1v6m0 6v10M4.22 4.22l4.24 4.24m7.08 7.08l4.24 4.24M1 12h6m6 0h10M4.22 19.78l4.24-4.24m7.08-7.08l4.24-4.24"/>
+ </svg>
+ </span>
+ <span className="recipe-info-label">Model</span>
+ <span className="recipe-info-value"><code>{model}</code></span>
</div>
- <div className="recipe-value">
- {harnessNames[harness] || harness}
+ <div className="recipe-info-card">
+ <span className="recipe-info-icon">
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+ <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
+ </svg>
+ </span>
+ <span className="recipe-info-label">Harness</span>
+ <span className="recipe-info-value">{harnessNames[harness] || harness}</span>
</div>
</div>
- {/* Model Parameters */}
- {parsedSession?.modelParameters && (
+ {/* Token Usage */}
+ {parsedSession?.tokenUsage && (
<div className="recipe-section">
<div className="recipe-header">
- <span className="recipe-label">Model Parameters</span>
+ <span className="recipe-label">
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+ <path d="M12 20V10"/>
+ <path d="M18 20V4"/>
+ <path d="M6 20v-4"/>
+ </svg>
+ Token Usage
+ </span>
</div>
- <div className="recipe-value" style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
- {parsedSession.modelParameters.temperature !== undefined && (
- <span><code>temperature: {parsedSession.modelParameters.temperature}</code></span>
+ <div className="recipe-tokens">
+ {parsedSession.tokenUsage.inputTokens !== undefined && (
+ <div className="recipe-token-stat">
+ <span className="recipe-token-label">Input</span>
+ <span className="recipe-token-value">{formatTokens(parsedSession.tokenUsage.inputTokens)}</span>
+ </div>
)}
- {parsedSession.modelParameters.maxTokens !== undefined && (
- <span><code>max_tokens: {parsedSession.modelParameters.maxTokens}</code></span>
+ {parsedSession.tokenUsage.outputTokens !== undefined && (
+ <div className="recipe-token-stat">
+ <span className="recipe-token-label">Output</span>
+ <span className="recipe-token-value">{formatTokens(parsedSession.tokenUsage.outputTokens)}</span>
+ </div>
)}
- {parsedSession.modelParameters.topP !== undefined && (
- <span><code>top_p: {parsedSession.modelParameters.topP}</code></span>
+ {parsedSession.tokenUsage.totalTokens !== undefined && (
+ <div className="recipe-token-stat recipe-token-total">
+ <span className="recipe-token-label">Total</span>
+ <span className="recipe-token-value">{formatTokens(parsedSession.tokenUsage.totalTokens)}</span>
+ </div>
)}
- {parsedSession.modelParameters.topK !== undefined && (
- <span><code>top_k: {parsedSession.modelParameters.topK}</code></span>
+ {parsedSession.tokenUsage.cacheReadTokens !== undefined && parsedSession.tokenUsage.cacheReadTokens > 0 && (
+ <div className="recipe-token-stat recipe-token-cache">
+ <span className="recipe-token-label">Cached</span>
+ <span className="recipe-token-value">{formatTokens(parsedSession.tokenUsage.cacheReadTokens)}</span>
+ </div>
)}
</div>
</div>
)}
- {/* Token Usage */}
- {parsedSession?.tokenUsage && (
+ {/* Model Parameters */}
+ {parsedSession?.modelParameters && (
<div className="recipe-section">
<div className="recipe-header">
- <span className="recipe-label">Token Usage</span>
+ <span className="recipe-label">
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+ <circle cx="12" cy="12" r="3"/>
+ <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
+ </svg>
+ Parameters
+ </span>
</div>
- <div className="recipe-value" style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
- {parsedSession.tokenUsage.inputTokens !== undefined && (
- <span>Input: <code>{formatTokens(parsedSession.tokenUsage.inputTokens)}</code></span>
+ <div className="recipe-params">
+ {parsedSession.modelParameters.temperature !== undefined && (
+ <span className="recipe-param"><code>temp: {parsedSession.modelParameters.temperature}</code></span>
)}
- {parsedSession.tokenUsage.outputTokens !== undefined && (
- <span>Output: <code>{formatTokens(parsedSession.tokenUsage.outputTokens)}</code></span>
+ {parsedSession.modelParameters.maxTokens !== undefined && (
+ <span className="recipe-param"><code>max: {parsedSession.modelParameters.maxTokens}</code></span>
)}
- {parsedSession.tokenUsage.totalTokens !== undefined && (
- <span>Total: <code>{formatTokens(parsedSession.tokenUsage.totalTokens)}</code></span>
+ {parsedSession.modelParameters.topP !== undefined && (
+ <span className="recipe-param"><code>top_p: {parsedSession.modelParameters.topP}</code></span>
)}
- {parsedSession.tokenUsage.cacheReadTokens !== undefined && parsedSession.tokenUsage.cacheReadTokens > 0 && (
- <span>Cache Read: <code>{formatTokens(parsedSession.tokenUsage.cacheReadTokens)}</code></span>
+ {parsedSession.modelParameters.topK !== undefined && (
+ <span className="recipe-param"><code>top_k: {parsedSession.modelParameters.topK}</code></span>
)}
</div>
</div>
@@ -178,11 +255,16 @@ export function RecipePanel({ prompt, model, harness, sessionData }: RecipePanel
{parsedSession?.plugins && parsedSession.plugins.length > 0 && (
<div className="recipe-section">
<div className="recipe-header">
- <span className="recipe-label">Plugins</span>
+ <span className="recipe-label">
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+ <path d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/>
+ </svg>
+ Plugins
+ </span>
</div>
- <div className="recipe-value" style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
+ <div className="recipe-plugins">
{parsedSession.plugins.map((plugin, i) => (
- <span key={i} className="badge" style={{ background: 'rgba(0, 112, 243, 0.2)', color: 'var(--accent)' }}>
+ <span key={i} className="recipe-plugin-badge">
{plugin.name}{plugin.version ? ` v${plugin.version}` : ''}
</span>
))}
@@ -194,15 +276,23 @@ export function RecipePanel({ prompt, model, harness, sessionData }: RecipePanel
{parsedSession?.mcpServers && parsedSession.mcpServers.length > 0 && (
<div className="recipe-section">
<div className="recipe-header">
- <span className="recipe-label">MCP Servers</span>
+ <span className="recipe-label">
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+ <rect x="2" y="2" width="20" height="8" rx="2" ry="2"/>
+ <rect x="2" y="14" width="20" height="8" rx="2" ry="2"/>
+ <line x1="6" y1="6" x2="6.01" y2="6"/>
+ <line x1="6" y1="18" x2="6.01" y2="18"/>
+ </svg>
+ MCP Servers
+ </span>
</div>
- <div className="recipe-value">
+ <div className="recipe-mcp-list">
{parsedSession.mcpServers.map((server, i) => (
- <div key={i} style={{ marginBottom: '0.5rem' }}>
+ <div key={i} className="recipe-mcp-item">
<code>{server.name}</code>
{server.command && (
- <span style={{ color: 'var(--muted)', fontSize: '0.8rem', marginLeft: '0.5rem' }}>
- ({server.command}{server.args ? ` ${server.args.join(' ')}` : ''})
+ <span className="recipe-mcp-command">
+ {server.command}{server.args ? ` ${server.args.join(' ')}` : ''}
</span>
)}
</div>
@@ -211,24 +301,20 @@ export function RecipePanel({ prompt, model, harness, sessionData }: RecipePanel
</div>
)}
- <div className="recipe-section">
- <div className="recipe-header">
- <span className="recipe-label">Prompt</span>
- <CopyButton text={prompt} label="Prompt" />
- </div>
- <div className="prompt-box">
- {prompt}
- </div>
- </div>
-
{/* System Prompt */}
{parsedSession?.systemPrompt && (
<div className="recipe-section">
<div className="recipe-header">
- <span className="recipe-label">System Prompt</span>
+ <span className="recipe-label">
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+ <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
+ <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
+ </svg>
+ System Prompt
+ </span>
<CopyButton text={parsedSession.systemPrompt} label="System Prompt" />
</div>
- <div className="prompt-box" style={{ maxHeight: '200px', overflow: 'auto' }}>
+ <div className="prompt-box prompt-box-compact">
{parsedSession.systemPrompt}
</div>
</div>
@@ -238,24 +324,38 @@ export function RecipePanel({ prompt, model, harness, sessionData }: RecipePanel
{parsedSession?.markdownConfigs && parsedSession.markdownConfigs.length > 0 && (
<div className="recipe-section">
<div className="recipe-header">
- <span className="recipe-label">Config Files</span>
+ <span className="recipe-label">
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+ <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/>
+ <polyline points="13 2 13 9 20 9"/>
+ </svg>
+ Config Files
+ </span>
</div>
- <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
+ <div className="recipe-configs">
{parsedSession.markdownConfigs.map((config, i) => (
- <div key={i}>
- <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
- <button
- onClick={() => toggleConfig(config.filename)}
- className="toggle-btn"
- style={{ padding: '0.25rem 0.5rem' }}
+ <div key={i} className="recipe-config-item">
+ <button
+ onClick={() => toggleConfig(config.filename)}
+ className="recipe-config-toggle"
+ >
+ <svg
+ width="12"
+ height="12"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ style={{ transform: expandedConfigs.has(config.filename) ? 'rotate(90deg)' : 'none', transition: 'transform 0.2s' }}
>
- {expandedConfigs.has(config.filename) ? '▼' : '▶'} {config.filename}
- </button>
- <CopyButton text={config.content} label={config.filename} />
- </div>
+ <polyline points="9 18 15 12 9 6"/>
+ </svg>
+ <code>{config.filename}</code>
+ </button>
+ <CopyButton text={config.content} label={config.filename} />
{expandedConfigs.has(config.filename) && (
- <div className="prompt-box" style={{ marginTop: '0.5rem', maxHeight: '300px', overflow: 'auto' }}>
- <pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>{config.content}</pre>
+ <div className="recipe-config-content">
+ <pre>{config.content}</pre>
</div>
)}
</div>
@@ -264,15 +364,22 @@ export function RecipePanel({ prompt, model, harness, sessionData }: RecipePanel
</div>
)}
+ {/* Raw Session Data */}
{parsedSession && !parsedSession.manual && (
- <div className="recipe-section">
+ <div className="recipe-section recipe-section-raw">
<div className="recipe-header">
- <span className="recipe-label">Raw Session Data</span>
+ <span className="recipe-label">
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+ <polyline points="4 17 10 11 4 5"/>
+ <line x1="12" y1="19" x2="20" y2="19"/>
+ </svg>
+ Raw Session Data
+ </span>
<button
onClick={() => setShowRaw(!showRaw)}
- className="toggle-btn"
+ className="recipe-toggle-btn"
>
- {showRaw ? 'Hide' : 'Show'} Raw
+ {showRaw ? 'Hide' : 'Show'}
</button>
</div>
{showRaw && (
@@ -284,10 +391,18 @@ export function RecipePanel({ prompt, model, harness, sessionData }: RecipePanel
</div>
)}
+ {/* Tip */}
<div className="recipe-tip">
- <strong>Tip:</strong> Copy the prompt and adapt it for your own project.
- The key is understanding why this prompt worked, not reproducing it exactly.
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+ <circle cx="12" cy="12" r="10"/>
+ <line x1="12" y1="16" x2="12" y2="12"/>
+ <line x1="12" y1="8" x2="12.01" y2="8"/>
+ </svg>
+ <div>
+ <strong>Pro tip:</strong> Copy the prompt and adapt it for your own project.
+ The key is understanding <em>why</em> this prompt worked, not reproducing it exactly.
+ </div>
</div>
- </div>
+ </section>
)
}

Recipe

Model
claude-opus-4-5-20251101
Harness
Claude Code
Token Usage
Input: 42.4KOutput: 391.7KTotal: 434.0KCache Read: 224.8M
Plugins
Frontend Design vclaude-plugins-officialGithub vclaude-plugins-officialFeature Dev vclaude-plugins-officialSwift Lsp vclaude-plugins-official
Prompt
My token limit has been reset. Please continue where you left off
Raw Session Data
Tip: Copy the prompt and adapt it for your own project. The key is understanding why this prompt worked, not reproducing it exactly.

Comments (0)

Loading comments...