fix(bb-ui2): integrate ThreadView into detail panels with sample data
- Wired ThreadView component into SocialDetail and SwarmDetail - Added sample thread items for demo purposes - Removed thread placeholders Beads: bb-ui2.13 closed
This commit is contained in:
parent
8dd2d01686
commit
f6c5398f0c
21 changed files with 2630 additions and 58 deletions
|
|
@ -7,3 +7,4 @@ export { statusGradient, statusBorder, statusDotColor, sessionStateGlow } from '
|
|||
export { EpicChipStrip } from './epic-chip-strip';
|
||||
export { WorkspaceHero } from './workspace-hero';
|
||||
export { ProjectScopeControls } from './project-scope-controls';
|
||||
export { ThreadView, type ThreadItem } from './thread-view';
|
||||
|
|
|
|||
159
src/components/shared/thread-view.tsx
Normal file
159
src/components/shared/thread-view.tsx
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
'use client';
|
||||
|
||||
import { ArrowRight, Ban, CheckCircle2, MessageSquare, UserMinus } from 'lucide-react';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type ThreadItemType = 'comment' | 'status_change' | 'protocol_event';
|
||||
|
||||
export interface ThreadItem {
|
||||
id: string;
|
||||
type: ThreadItemType;
|
||||
author?: string;
|
||||
content?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
event?: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
interface ThreadViewProps {
|
||||
items: ThreadItem[];
|
||||
onAddComment?: (text: string) => void;
|
||||
}
|
||||
|
||||
function getInitials(name: string): string {
|
||||
return name
|
||||
.split(' ')
|
||||
.map((part) => part[0])
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
}
|
||||
|
||||
function formatRelativeTime(date: Date): string {
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffSecs = Math.floor(diffMs / 1000);
|
||||
const diffMins = Math.floor(diffSecs / 60);
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffSecs < 60) return 'just now';
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
if (diffDays < 7) return `${diffDays}d ago`;
|
||||
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
||||
}
|
||||
|
||||
function getProtocolIcon(event?: string) {
|
||||
switch (event?.toUpperCase()) {
|
||||
case 'HANDOFF':
|
||||
return <UserMinus className="w-4 h-4 text-amber-400" />;
|
||||
case 'BLOCKED':
|
||||
return <Ban className="w-4 h-4 text-rose-400" />;
|
||||
case 'CLOSED':
|
||||
return <CheckCircle2 className="w-4 h-4 text-emerald-400" />;
|
||||
default:
|
||||
return <MessageSquare className="w-4 h-4 text-text-muted" />;
|
||||
}
|
||||
}
|
||||
|
||||
function getProtocolLabel(event?: string): string {
|
||||
switch (event?.toUpperCase()) {
|
||||
case 'HANDOFF':
|
||||
return 'Handoff';
|
||||
case 'BLOCKED':
|
||||
return 'Blocked';
|
||||
case 'CLOSED':
|
||||
return 'Closed';
|
||||
default:
|
||||
return 'Event';
|
||||
}
|
||||
}
|
||||
|
||||
function CommentItem({ item }: { item: ThreadItem }) {
|
||||
return (
|
||||
<div className="flex gap-3 py-3">
|
||||
<Avatar className="h-8 w-8 flex-shrink-0">
|
||||
<AvatarImage src={undefined} alt={item.author} />
|
||||
<AvatarFallback className="bg-surface-muted text-text-body text-xs font-semibold">
|
||||
{item.author ? getInitials(item.author) : '??'}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-text-primary text-sm font-medium">
|
||||
{item.author || 'Unknown'}
|
||||
</span>
|
||||
<span className="text-text-muted text-xs">
|
||||
{formatRelativeTime(item.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-text-secondary text-sm whitespace-pre-wrap break-words">
|
||||
{item.content}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusChangeItem({ item }: { item: ThreadItem }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 py-2 text-sm">
|
||||
<ArrowRight className="w-4 h-4 text-text-muted flex-shrink-0" />
|
||||
<span className="text-text-muted">
|
||||
Status: <span className="text-text-primary font-medium">{item.from || 'unknown'}</span>
|
||||
<ArrowRight className="w-3 h-3 inline mx-1 text-text-muted" />
|
||||
<span className="text-text-primary font-medium">{item.to || 'unknown'}</span>
|
||||
</span>
|
||||
<span className="text-text-muted text-xs ml-auto">
|
||||
{formatRelativeTime(item.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProtocolEventItem({ item }: { item: ThreadItem }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 py-2">
|
||||
<div className="flex-shrink-0">{getProtocolIcon(item.event)}</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-text-primary text-sm font-medium">
|
||||
{getProtocolLabel(item.event)}
|
||||
</span>
|
||||
{item.content && (
|
||||
<span className="text-text-secondary text-sm ml-2">{item.content}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-text-muted text-xs">
|
||||
{formatRelativeTime(item.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ThreadView({ items, onAddComment }: ThreadViewProps) {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{items.length === 0 ? (
|
||||
<p className="text-text-muted text-sm italic py-4">No activity yet</p>
|
||||
) : (
|
||||
<div className="divide-y divide-white/5">
|
||||
{items.map((item) => {
|
||||
switch (item.type) {
|
||||
case 'comment':
|
||||
return <CommentItem key={item.id} item={item} />;
|
||||
case 'status_change':
|
||||
return <StatusChangeItem key={item.id} item={item} />;
|
||||
case 'protocol_event':
|
||||
return <ProtocolEventItem key={item.id} item={item} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,8 +3,34 @@
|
|||
import type { SocialCard as SocialCardData, AgentStatus } from '../../lib/social-cards';
|
||||
import { StatusBadge } from '../shared/status-badge';
|
||||
import { AgentAvatar } from '../shared/agent-avatar';
|
||||
import { ThreadView, type ThreadItem } from '../shared/thread-view';
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
// Sample data for demo - remove when real data connected
|
||||
const SAMPLE_THREAD_ITEMS: ThreadItem[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'status_change',
|
||||
from: 'backlog',
|
||||
to: 'in_progress',
|
||||
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'comment',
|
||||
author: 'zenchantlive',
|
||||
content: 'Started working on this task. Will need input from the API team.',
|
||||
timestamp: new Date(Date.now() - 1 * 60 * 60 * 1000),
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'protocol_event',
|
||||
event: 'HANDOFF',
|
||||
content: 'Handed off to bb-agent-1 for implementation',
|
||||
timestamp: new Date(Date.now() - 30 * 60 * 1000),
|
||||
},
|
||||
];
|
||||
|
||||
interface SocialDetailProps {
|
||||
data: SocialCardData;
|
||||
}
|
||||
|
|
@ -37,13 +63,11 @@ export function SocialDetail({ data }: SocialDetailProps) {
|
|||
<StatusBadge status={data.status} size="sm" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-text-muted text-xs font-semibold uppercase tracking-wider">
|
||||
Thread
|
||||
</h3>
|
||||
<p className="text-text-muted text-sm italic">
|
||||
Thread placeholder (bb-ui2.13)
|
||||
</p>
|
||||
<ThreadView items={SAMPLE_THREAD_ITEMS} />
|
||||
</div>
|
||||
|
||||
{data.blocks.length > 0 && (
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@
|
|||
import type { SwarmCard as SwarmCardType } from '../../lib/swarm-cards';
|
||||
import { Badge } from '../../../components/ui/badge';
|
||||
import { AgentAvatar } from '../shared/agent-avatar';
|
||||
import { ThreadView, type ThreadItem } from '../shared/thread-view';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { AlertTriangle, Clock, MessageSquare, Users } from 'lucide-react';
|
||||
import { AlertTriangle, Clock, Users } from 'lucide-react';
|
||||
|
||||
interface SwarmDetailProps {
|
||||
card: SwarmCardType;
|
||||
|
|
@ -148,21 +149,38 @@ function LastActivitySection({ date }: { date: Date }) {
|
|||
);
|
||||
}
|
||||
|
||||
function ThreadPlaceholder() {
|
||||
// Sample data for demo - remove when real data connected
|
||||
const SAMPLE_SWARM_THREAD: ThreadItem[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'status_change',
|
||||
from: 'planning',
|
||||
to: 'in_progress',
|
||||
timestamp: new Date(Date.now() - 4 * 60 * 60 * 1000),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'comment',
|
||||
author: 'bb-agent-1',
|
||||
content: 'Starting work on the first batch of tasks.',
|
||||
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000),
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'protocol_event',
|
||||
event: 'CLOSED',
|
||||
content: 'Task bb-buff.1 completed',
|
||||
timestamp: new Date(Date.now() - 30 * 60 * 1000),
|
||||
},
|
||||
];
|
||||
|
||||
function ThreadSection() {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<MessageSquare className="h-3.5 w-3.5" style={{ color: 'var(--color-text-muted)' }} />
|
||||
<span className="text-xs font-semibold uppercase tracking-wider" style={{ color: 'var(--color-text-muted)' }}>
|
||||
Thread
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="p-3 rounded-md text-center text-xs"
|
||||
style={{ backgroundColor: 'var(--color-bg-elevated)', color: 'var(--color-text-muted)' }}
|
||||
>
|
||||
Thread coming soon
|
||||
</div>
|
||||
<span className="text-xs font-semibold uppercase tracking-wider" style={{ color: 'var(--color-text-muted)' }}>
|
||||
Thread
|
||||
</span>
|
||||
<ThreadView items={SAMPLE_SWARM_THREAD} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -200,8 +218,8 @@ export function SwarmDetail({ card }: SwarmDetailProps) {
|
|||
{/* Last Activity */}
|
||||
<LastActivitySection date={card.lastActivity} />
|
||||
|
||||
{/* Thread Placeholder */}
|
||||
<ThreadPlaceholder />
|
||||
{/* Thread */}
|
||||
<ThreadSection />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
197
src/video/Main.tsx
Normal file
197
src/video/Main.tsx
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
import { interpolate, useCurrentFrame, useVideoConfig, AbsoluteFill, Sequence, Img, staticFile, spring } from 'remotion';
|
||||
import React from 'react';
|
||||
import { loadFont } from "@remotion/google-fonts/Inter";
|
||||
import { Background } from './components/Background';
|
||||
import { TerminalScene } from './components/TerminalScene';
|
||||
import { TimelineScene } from './components/TimelineScene';
|
||||
|
||||
loadFont();
|
||||
|
||||
const COLORS = {
|
||||
textPrimary: '#FFFFFF',
|
||||
textSecondary: '#B8B8B8',
|
||||
accentGreen: '#7CB97A',
|
||||
accentTeal: '#5BA8A0',
|
||||
accentAmber: '#D4A574',
|
||||
};
|
||||
|
||||
const Logo: React.FC<{ scale?: number }> = ({ scale = 1 }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const dots = [
|
||||
{ color: COLORS.accentGreen, delay: 0 },
|
||||
{ color: COLORS.accentTeal, delay: 5 },
|
||||
{ color: COLORS.accentAmber, delay: 10 },
|
||||
{ color: COLORS.accentTeal, delay: 15 },
|
||||
{ color: COLORS.accentGreen, delay: 20 },
|
||||
{ color: COLORS.accentAmber, delay: 25 },
|
||||
{ color: COLORS.accentGreen, delay: 30 },
|
||||
{ color: COLORS.accentTeal, delay: 35 },
|
||||
{ color: COLORS.accentAmber, delay: 40 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-3 p-4" style={{ transform: `scale(${scale})` }}>
|
||||
{dots.map((dot, i) => {
|
||||
const spr = spring({
|
||||
frame: frame - dot.delay,
|
||||
fps,
|
||||
config: { damping: 10 }
|
||||
});
|
||||
const s = interpolate(spr, [0, 1], [0, 1]);
|
||||
const opacity = interpolate(spr, [0, 1], [0, 1]);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="w-4 h-4 rounded-full shadow-[0_0_15px_rgba(255,255,255,0.3)]"
|
||||
style={{
|
||||
backgroundColor: dot.color,
|
||||
opacity,
|
||||
transform: `scale(${s})`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const GlassCard: React.FC<{ children: React.ReactNode; className?: string }> = ({ children, className }) => {
|
||||
return (
|
||||
<div
|
||||
className={`relative overflow-hidden rounded-xl border border-[rgba(255,255,255,0.1)] bg-[#363636]/40 shadow-2xl ${className}`}
|
||||
style={{
|
||||
backdropFilter: 'blur(16px)',
|
||||
boxShadow: '0 24px 56px rgba(0, 0, 0, 0.5), inset 0 1px 1px rgba(255, 255, 255, 0.15)',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TitleScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const opacity = interpolate(frame, [0, 20], [0, 1]);
|
||||
const y = interpolate(frame, [0, 20], [30, 0]);
|
||||
const blur = interpolate(frame, [0, 20], [10, 0]);
|
||||
|
||||
return (
|
||||
<AbsoluteFill className="items-center justify-center flex-col z-10">
|
||||
<div style={{ opacity, transform: `translateY(${y}px)`, filter: `blur(${blur}px)` }} className="flex flex-col items-center gap-8">
|
||||
<Logo scale={3} />
|
||||
<div className="text-center mt-10">
|
||||
<h1 className="text-8xl font-bold tracking-tight mb-6 drop-shadow-2xl" style={{ color: COLORS.textPrimary, fontFamily: 'Inter' }}>
|
||||
Beadboard
|
||||
</h1>
|
||||
<p className="text-3xl font-medium tracking-wide drop-shadow-md" style={{ color: COLORS.textSecondary, fontFamily: 'Inter' }}>
|
||||
Agent-Driven Project Orchestration
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
const ShowcaseScene: React.FC<{ src: string; title: string; subtitle: string }> = ({ src, title, subtitle }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const spr = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 14, mass: 0.8 },
|
||||
});
|
||||
|
||||
const opacity = interpolate(frame, [0, 15], [0, 1]);
|
||||
const scale = interpolate(spr, [0, 1], [0.92, 1]);
|
||||
const y = interpolate(spr, [0, 1], [60, 0]);
|
||||
|
||||
// Continuous floating animation
|
||||
const floatY = Math.sin(frame / 40) * 8;
|
||||
|
||||
return (
|
||||
<AbsoluteFill className="items-center justify-center p-20 z-10">
|
||||
<div className="w-full max-w-6xl flex flex-col items-center gap-10" style={{ opacity, transform: `translateY(${y}px)` }}>
|
||||
<div className="text-center drop-shadow-lg">
|
||||
<h2 className="text-6xl font-bold mb-4" style={{ color: COLORS.textPrimary, fontFamily: 'Inter' }}>{title}</h2>
|
||||
<p className="text-2xl font-medium" style={{ color: COLORS.textSecondary, fontFamily: 'Inter' }}>{subtitle}</p>
|
||||
</div>
|
||||
|
||||
<div style={{ transform: `translateY(${floatY}px)` }} className="w-full">
|
||||
<GlassCard className="w-full aspect-video flex items-center justify-center group">
|
||||
<div style={{ transform: `scale(${scale})`, width: '100%', height: '100%' }}>
|
||||
<Img src={staticFile(src)} className="w-full h-full object-cover opacity-90" />
|
||||
{/* Shine effect */}
|
||||
<div
|
||||
className="absolute inset-0 bg-gradient-to-tr from-white/0 via-white/5 to-white/0"
|
||||
style={{ transform: `translateX(${Math.sin(frame / 60) * 10}%)` }}
|
||||
/>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
const OutroScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const opacity = interpolate(frame, [0, 20], [0, 1]);
|
||||
const scale = interpolate(frame, [0, 100], [1, 1.1]);
|
||||
|
||||
return (
|
||||
<AbsoluteFill className="items-center justify-center z-10">
|
||||
<div style={{ opacity, transform: `scale(${scale})` }} className="text-center">
|
||||
<h2 className="text-8xl font-bold mb-8 drop-shadow-2xl" style={{ color: COLORS.textPrimary, fontFamily: 'Inter' }}>
|
||||
Build Faster.
|
||||
</h2>
|
||||
<p className="text-4xl font-semibold drop-shadow-lg" style={{ color: COLORS.accentTeal, fontFamily: 'Inter' }}>
|
||||
Deploy with Confidence.
|
||||
</p>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
}
|
||||
|
||||
export const Main: React.FC = () => {
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<Background />
|
||||
|
||||
<Sequence durationInFrames={90}>
|
||||
<TitleScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={90} durationInFrames={100}>
|
||||
<ShowcaseScene
|
||||
src="graph-hero.png"
|
||||
title="Visual Workflow"
|
||||
subtitle="Orchestrate complex agent behaviors with intuitive graphs."
|
||||
/>
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={190} durationInFrames={100}>
|
||||
<ShowcaseScene
|
||||
src="kanban-hero.png"
|
||||
title="Agent Kanban"
|
||||
subtitle="Track autonomous tasks and parallel execution."
|
||||
/>
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={290} durationInFrames={300}>
|
||||
<TerminalScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={590} durationInFrames={140}>
|
||||
<TimelineScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={730} durationInFrames={80}>
|
||||
<OutroScene />
|
||||
</Sequence>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
25
src/video/Root.tsx
Normal file
25
src/video/Root.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { Composition, Still } from 'remotion';
|
||||
import { Main } from './Main';
|
||||
import './style.css';
|
||||
|
||||
export const RemotionRoot: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Composition
|
||||
id="Main"
|
||||
component={Main}
|
||||
durationInFrames={810}
|
||||
fps={30}
|
||||
width={1920}
|
||||
height={1080}
|
||||
/>
|
||||
<Still
|
||||
id="Thumbnail"
|
||||
component={Main}
|
||||
width={1920}
|
||||
height={1080}
|
||||
defaultProps={{}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
100
src/video/components/Background.tsx
Normal file
100
src/video/components/Background.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { interpolate, useCurrentFrame, useVideoConfig, AbsoluteFill } from 'remotion';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
const COLORS = {
|
||||
bgBase: '#2D2D2D',
|
||||
accentGreen: '#7CB97A',
|
||||
accentAmber: '#D4A574',
|
||||
accentTeal: '#5BA8A0',
|
||||
};
|
||||
|
||||
const AnimatedGradient: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { durationInFrames } = useVideoConfig();
|
||||
|
||||
// Create smooth looping motion for the blobs
|
||||
const offset1 = Math.sin(frame / 60) * 10;
|
||||
const offset2 = Math.cos(frame / 50) * 10;
|
||||
const scale1 = interpolate(Math.sin(frame / 80), [-1, 1], [0.8, 1.2]);
|
||||
const scale2 = interpolate(Math.cos(frame / 70), [-1, 1], [0.8, 1.2]);
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: COLORS.bgBase, overflow: 'hidden' }}>
|
||||
<div
|
||||
className="absolute top-[-20%] left-[-10%] w-[60%] h-[60%] rounded-full opacity-20 blur-[140px]"
|
||||
style={{
|
||||
background: COLORS.accentGreen,
|
||||
transform: `translate(${offset1}%, ${offset2}%) scale(${scale1})`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute bottom-[-20%] right-[-10%] w-[60%] h-[60%] rounded-full opacity-20 blur-[140px]"
|
||||
style={{
|
||||
background: COLORS.accentAmber,
|
||||
transform: `translate(${-offset2}%, ${-offset1}%) scale(${scale2})`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute top-[30%] left-[30%] w-[40%] h-[40%] rounded-full opacity-10 blur-[120px]"
|
||||
style={{
|
||||
background: COLORS.accentTeal,
|
||||
transform: `translate(${offset2 * 0.5}%, ${offset1 * 0.5}%) scale(${scale1})`,
|
||||
}}
|
||||
/>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
const DotGrid: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { width, height } = useVideoConfig();
|
||||
|
||||
// Generate a static grid of dots
|
||||
// Only calculate once
|
||||
const dots = useMemo(() => {
|
||||
const d = [];
|
||||
const spacing = 80;
|
||||
const cols = Math.ceil(width / spacing);
|
||||
const rows = Math.ceil(height / spacing);
|
||||
|
||||
for (let i = 0; i < cols; i++) {
|
||||
for (let j = 0; j < rows; j++) {
|
||||
d.push({ x: i * spacing, y: j * spacing, delay: (i + j) * 2 });
|
||||
}
|
||||
}
|
||||
return d;
|
||||
}, [width, height]);
|
||||
|
||||
return (
|
||||
<AbsoluteFill className="items-center justify-center">
|
||||
<svg width="100%" height="100%">
|
||||
{dots.map((dot, i) => {
|
||||
// Subtle fade in/out ripple effect based on position
|
||||
const wave = Math.sin((frame - dot.delay) / 20);
|
||||
const opacity = interpolate(wave, [-1, 1], [0.03, 0.15]);
|
||||
const scale = interpolate(wave, [-1, 1], [0.5, 1.2]);
|
||||
|
||||
return (
|
||||
<circle
|
||||
key={i}
|
||||
cx={dot.x + 40}
|
||||
cy={dot.y + 40}
|
||||
r={2 * scale}
|
||||
fill="white"
|
||||
opacity={opacity}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
export const Background: React.FC = () => {
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<AnimatedGradient />
|
||||
<DotGrid />
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
120
src/video/components/TerminalScene.tsx
Normal file
120
src/video/components/TerminalScene.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import { interpolate, useCurrentFrame, useVideoConfig, AbsoluteFill, Sequence, spring } from 'remotion';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
const TerminalLine: React.FC<{ text: string; delay: number; color?: string }> = ({ text, delay, color = '#d1d5db' }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const chars = text.split('');
|
||||
|
||||
return (
|
||||
<div className="font-mono text-xl mb-2 flex">
|
||||
{chars.map((char, i) => {
|
||||
const show = frame > delay + i * 1.5;
|
||||
return (
|
||||
<span key={i} style={{ opacity: show ? 1 : 0, color }}>
|
||||
{char}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const JSONLine: React.FC<{ data: object; delay: number }> = ({ data, delay }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const str = JSON.stringify(data, null, 2);
|
||||
const lines = str.split('\n');
|
||||
|
||||
const show = frame > delay;
|
||||
const opacity = interpolate(frame, [delay, delay + 10], [0, 1]);
|
||||
const y = interpolate(frame, [delay, delay + 10], [10, 0]);
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
<div style={{ opacity, transform: `translateY(${y}px)` }} className="font-mono text-sm text-green-400/90 bg-black/20 p-4 rounded-md border border-green-500/20 my-2">
|
||||
{lines.map((line, i) => (
|
||||
<div key={i}>{line}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export const TerminalScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const opacity = interpolate(frame, [0, 15], [0, 1]);
|
||||
const scale = interpolate(frame, [0, 15], [0.95, 1]);
|
||||
|
||||
// Header animation
|
||||
const headerY = interpolate(frame, [0, 20], [20, 0]);
|
||||
const headerOpacity = interpolate(frame, [0, 20], [0, 1]);
|
||||
|
||||
return (
|
||||
<AbsoluteFill className="items-center justify-center bg-transparent p-20 z-10">
|
||||
|
||||
{/* Header */}
|
||||
<div style={{ transform: `translateY(${headerY}px)`, opacity: headerOpacity }} className="absolute top-20 text-center w-full">
|
||||
<h2 className="text-6xl font-bold text-white mb-2 font-['Inter'] drop-shadow-lg">Protocol v1</h2>
|
||||
<p className="text-xl text-teal-400 font-mono tracking-widest uppercase">Safe Coordination Contract</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="w-full max-w-5xl bg-[#1e1e1e] rounded-xl overflow-hidden shadow-2xl border border-gray-700/50"
|
||||
style={{ opacity, transform: `scale(${scale})` }}
|
||||
>
|
||||
{/* Terminal Header */}
|
||||
<div className="bg-[#2d2d2d] px-4 py-3 flex items-center gap-2 border-b border-gray-700">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500" />
|
||||
<div className="w-3 h-3 rounded-full bg-yellow-500" />
|
||||
<div className="w-3 h-3 rounded-full bg-green-500" />
|
||||
<div className="ml-4 text-xs text-gray-400 font-mono">beadboard-agent — -zsh — 80x24</div>
|
||||
</div>
|
||||
|
||||
{/* Terminal Body */}
|
||||
<div className="p-6 h-[600px] font-mono text-gray-300 overflow-hidden relative">
|
||||
<Sequence from={20}>
|
||||
<TerminalLine text="> bb agent heartbeat --agent amber-otter --json" delay={0} color="#a5f3fc" />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={60}>
|
||||
<JSONLine
|
||||
delay={0}
|
||||
data={{
|
||||
status: "ok",
|
||||
agent_id: "amber-otter",
|
||||
last_seen: "2026-02-16T10:42:15Z",
|
||||
liveness: "active"
|
||||
}}
|
||||
/>
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={110}>
|
||||
<TerminalLine text="> bb protocol emit HANDOFF --to cobalt-harbor" delay={0} color="#a5f3fc" />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={150}>
|
||||
<div className="mt-4 p-4 border-l-4 border-blue-500 bg-blue-500/10">
|
||||
<TerminalLine text="[EVENT] HANDOFF DETECTED" delay={0} color="#60a5fa" />
|
||||
<TerminalLine text="Scope: src/components/sessions/*" delay={10} />
|
||||
<TerminalLine text="From: amber-otter -> To: cobalt-harbor" delay={20} />
|
||||
<TerminalLine text="Reason: Implementation complete, ready for review." delay={30} />
|
||||
</div>
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={250}>
|
||||
<div className="mt-4 p-4 border-l-4 border-yellow-500 bg-yellow-500/10">
|
||||
<TerminalLine text="[WARN] INCURSION PREVENTED" delay={0} color="#fbbf24" />
|
||||
<TerminalLine text="Target: src/lib/parser.ts (Locked by: obsidian-fox)" delay={10} />
|
||||
<TerminalLine text="Action: Write blocked. Queueing request." delay={20} />
|
||||
</div>
|
||||
</Sequence>
|
||||
|
||||
{/* Scanlines / CRT Effect Overlay */}
|
||||
<div className="absolute inset-0 pointer-events-none opacity-5 bg-[linear-gradient(rgba(18,16,16,0)_50%,rgba(0,0,0,0.25)_50%),linear-gradient(90deg,rgba(255,0,0,0.06),rgba(0,255,0,0.02),rgba(0,0,255,0.06))]" style={{ backgroundSize: "100% 2px, 3px 100%" }} />
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
112
src/video/components/TimelineScene.tsx
Normal file
112
src/video/components/TimelineScene.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { interpolate, useCurrentFrame, useVideoConfig, AbsoluteFill, Sequence, spring } from 'remotion';
|
||||
import React from 'react';
|
||||
|
||||
const COLORS = {
|
||||
bgBase: '#2D2D2D',
|
||||
cardBg: '#363636',
|
||||
accentGreen: '#7CB97A',
|
||||
accentAmber: '#D4A574',
|
||||
accentTeal: '#5BA8A0',
|
||||
textPrimary: '#FFFFFF',
|
||||
textSecondary: '#B8B8B8',
|
||||
border: 'rgba(255, 255, 255, 0.08)',
|
||||
};
|
||||
|
||||
const TimelineCard: React.FC<{
|
||||
title: string;
|
||||
subtitle: string;
|
||||
time: string;
|
||||
type: 'commit' | 'issue' | 'alert';
|
||||
index: number;
|
||||
}> = ({ title, subtitle, time, type, index }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const delay = index * 5;
|
||||
const spr = spring({
|
||||
frame: frame - delay,
|
||||
fps,
|
||||
config: { damping: 14, mass: 0.8 },
|
||||
});
|
||||
|
||||
const y = interpolate(spr, [0, 1], [50, 0]);
|
||||
const opacity = interpolate(spr, [0, 1], [0, 1]);
|
||||
|
||||
let iconColor = COLORS.textSecondary;
|
||||
if (type === 'commit') iconColor = COLORS.accentTeal;
|
||||
if (type === 'issue') iconColor = COLORS.accentGreen;
|
||||
if (type === 'alert') iconColor = COLORS.accentAmber;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ opacity, transform: `translateY(${y}px)` }}
|
||||
className="flex items-start gap-4 p-4 rounded-lg border bg-[#363636] shadow-lg mb-4 w-full max-w-2xl"
|
||||
// className="flex items-start gap-4 p-4 rounded-lg border border-[rgba(255,255,255,0.08)] bg-[#363636] shadow-lg mb-4 w-full max-w-2xl"
|
||||
>
|
||||
<div className="mt-1 w-3 h-3 rounded-full" style={{ backgroundColor: iconColor }} />
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between items-baseline">
|
||||
<h3 className="text-lg font-semibold text-white">{title}</h3>
|
||||
<span className="text-xs text-gray-500 font-mono">{time}</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 mt-1">{subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const TimelineScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const titleOpacity = interpolate(frame, [0, 20], [0, 1]);
|
||||
const titleY = interpolate(frame, [0, 20], [20, 0]);
|
||||
|
||||
return (
|
||||
<AbsoluteFill className="items-center justify-center p-10 z-10 flex-col">
|
||||
<div style={{ opacity: titleOpacity, transform: `translateY(${titleY}px)` }} className="mb-12 text-center">
|
||||
<h2 className="text-6xl font-bold text-white mb-2 font-['Inter'] drop-shadow-lg">Live Activity Feed</h2>
|
||||
<p className="text-xl text-teal-400 font-mono tracking-widest uppercase">Real-time Project Pulse</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-full max-w-2xl">
|
||||
<div className="text-xs font-mono text-gray-500 mb-4 uppercase tracking-wider ml-2">Today</div>
|
||||
<Sequence from={10}>
|
||||
<TimelineCard
|
||||
index={0} type="commit" title="feat: Implement Session Protocol v1"
|
||||
subtitle="amber-otter pushed to main" time="10:42 AM"
|
||||
/>
|
||||
</Sequence>
|
||||
<Sequence from={25}>
|
||||
<TimelineCard
|
||||
index={1} type="issue" title="Docs: Update RFC-001"
|
||||
subtitle="cobalt-harbor commented on #23" time="10:45 AM"
|
||||
/>
|
||||
</Sequence>
|
||||
<Sequence from={40}>
|
||||
<TimelineCard
|
||||
index={2} type="alert" title="Incursion Alert"
|
||||
subtitle="obsidian-fox attempted write to locked scope" time="11:02 AM"
|
||||
/>
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={60}>
|
||||
<div className="text-xs font-mono text-gray-500 mt-6 mb-4 uppercase tracking-wider ml-2">Yesterday</div>
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={70}>
|
||||
<TimelineCard
|
||||
index={3} type="issue" title="Refactor: Agent Registry"
|
||||
subtitle="emerald-wolf closed issue #19" time="4:20 PM"
|
||||
/>
|
||||
</Sequence>
|
||||
<Sequence from={85}>
|
||||
<TimelineCard
|
||||
index={4} type="commit" title="fix: Graph layout rendering"
|
||||
subtitle="amber-otter pushed to feature/graph-v2" time="3:15 PM"
|
||||
/>
|
||||
</Sequence>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
4
src/video/index.ts
Normal file
4
src/video/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { registerRoot } from 'remotion';
|
||||
import { RemotionRoot } from './Root';
|
||||
|
||||
registerRoot(RemotionRoot);
|
||||
3
src/video/style.css
Normal file
3
src/video/style.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
Loading…
Add table
Add a link
Reference in a new issue