Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,50 @@ The following operations are blocked to ensure subagents remain focused on their
:::info
Subagents can browse extensions for suggestions but cannot enable them to avoid modifying the parent session.
:::

## Additional Resources

import ContentCardCarousel from '@site/src/components/ContentCardCarousel';
import subagentsVsSubrecipes from '@site/blog/2025-09-26-subagents-vs-subrecipes/subrecipes-vs-subagents.png';
import agentCoordination from '@site/blog/2025-08-14-agent-coordination-patterns/agent-coordination.png';

<ContentCardCarousel
items={[
{
type: 'video',
title: 'How I Built an App with 6 Subagents',
description: 'Deep dive into goose subagents. Walk through building an app using 6 specialized AI agents for advanced workflow automation and development.',
thumbnailUrl: 'https://img.youtube.com/vi/yIBrD5AxtTc/maxresdefault.jpg',
linkUrl: 'https://www.youtube.com/watch?v=yIBrD5AxtTc',
date: '2025-10-01',
duration: '6:53'
},
{
type: 'video',
title: 'Flight School - Choosing the Right Tools for AI Work',
description: 'Discover the differences between subagents and subrecipes for efficient task execution in goose. Learn which approach is best for your workflow.',
thumbnailUrl: 'https://img.youtube.com/vi/joePzlkARjs/maxresdefault.jpg',
linkUrl: 'https://www.youtube.com/watch?v=joePzlkARjs',
date: '2025-09-29',
duration: '6:13'
},
{
type: 'blog',
title: 'How to Choose Between Subagents and Subrecipes in goose',
description: 'Detailed guide to subagents and subrecipes in goose. Compare reusability, setup complexity, and get practical advice for choosing the right approach.',
thumbnailUrl: subagentsVsSubrecipes,
linkUrl: '/goose/blog/2025/09/26/subagents-vs-subrecipes',
date: '2025-09-26',
duration: '6 min read'
},
{
type: 'blog',
title: 'Agents, Subagents, and Multi Agents: What They Are and When to Use Them',
description: 'Compare agents, subagents, and multi agents in AI workflows. Learn how they work together and practical scenarios for each approach.',
thumbnailUrl: agentCoordination,
linkUrl: '/goose/blog/2025/08/14/agent-coordination-patterns',
date: '2025-08-14',
duration: '4 min read'
}
]}
/>
232 changes: 232 additions & 0 deletions documentation/src/components/ContentCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import React from 'react';

type ContentType = 'video' | 'blog';

interface ContentCardProps {
type: ContentType;
title: string;
description: string;
thumbnailUrl?: string; // meta url or ES6 import for blogs
linkUrl: string;
date?: string;
duration?: string; // e.g. '6:04' for videos and '5 min read' for blogs
size?: 'large' | 'compact';
}

const styles = {
cardContainer: {
display: 'flex',
flexDirection: 'row' as const,
width: '100%',
border: '1px solid var(--ifm-color-emphasis-200)',
borderRadius: '12px',
textDecoration: 'none',
color: 'inherit',
overflow: 'hidden',
background: 'var(--ifm-background-color)',
transition: 'box-shadow 0.2s ease, transform 0.2s ease',
marginBottom: '1rem',
},
cardContainerLarge: {
width: '100%',
maxWidth: '500px',
aspectRatio: '16/9',
},
cardContainerCompact: {
width: '100%',
maxWidth: '350px',
aspectRatio: '16/9',
},
cardHover: {
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
transform: 'translateY(-2px)',
},

mainArea: {
flex: 1,
display: 'flex',
flexDirection: 'column' as const,
},
thumbnailWrapper: {
position: 'relative' as const,
width: '100%',
height: '100%',
paddingBottom: 0,
overflow: 'hidden' as const,
background: 'var(--ifm-color-emphasis-100)',
},
thumbnail: {
position: 'absolute' as const,
top: 0,
left: 0,
width: '100%',
height: '100%',
objectFit: 'cover' as const,
},
placeholderLogo: {
position: 'absolute' as const,
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '64px',
height: '64px',
opacity: 0.6,
},

hoverOverlay: {
position: 'absolute' as const,
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.9)',
color: 'white',
padding: '1.25rem',
display: 'flex',
flexDirection: 'column' as const,
justifyContent: 'center',
opacity: 0,
transition: 'opacity 0.3s ease',
zIndex: 10,
borderRadius: '12px',
},
hoverOverlayVisible: {
opacity: 1,
},
hoverTitle: {
fontSize: '1.1rem',
fontWeight: '600' as const,
marginBottom: '0.5rem',
color: 'white',
},
hoverDescription: {
fontSize: '0.875rem',
lineHeight: '1.4',
marginBottom: '0.75rem',
color: 'rgba(255, 255, 255, 0.9)',
},
hoverMetadata: {
fontSize: '0.75rem',
color: 'rgba(255, 255, 255, 0.8)',
fontWeight: '600' as const,
display: 'flex',
justifyContent: 'space-between',
marginTop: 'auto',
},
};

export default function ContentCard({
type,
title,
description,
thumbnailUrl,
linkUrl,
date,
duration,
size = 'compact',
}: ContentCardProps) {
const [isHovering, setIsHovering] = React.useState(false);
const [isTouchDevice, setIsTouchDevice] = React.useState(false);
const isCompact = size === 'compact';
const showHoverOverlay = true;

// Detect touch device on mount
React.useEffect(() => {
setIsTouchDevice('ontouchstart' in window || navigator.maxTouchPoints > 0);
}, []);

const containerStyle = {
...styles.cardContainer,
...(isCompact ? styles.cardContainerCompact : styles.cardContainerLarge),
...(isHovering ? styles.cardHover : {}),
position: 'relative' as const,
};

const thumbnailWrapperStyle = styles.thumbnailWrapper;

const hoverOverlayStyle = {
...styles.hoverOverlay,
...(isHovering && showHoverOverlay && !isTouchDevice ? styles.hoverOverlayVisible : {}),
...(size === 'large' ? {
padding: '2.00rem',
} : {}),
};

const hoverTitleStyle = {
...styles.hoverTitle,
...(size === 'large' ? {
fontSize: '1.4rem',
} : {}),
};

const hoverDescriptionStyle = {
...styles.hoverDescription,
...(size === 'large' ? {
fontSize: '1.1rem',
} : {}),
};

const hoverMetadataStyle = {
...styles.hoverMetadata,
...(size === 'large' ? {
fontSize: '0.9rem',
} : {}),
};

const formatDate = (dateString: string) => {
const [year, month, day] = dateString.split('-').map(Number);
const date = new Date(year, month - 1, day);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
};

return (
<a
href={linkUrl}
style={containerStyle}
onMouseEnter={() => !isTouchDevice && setIsHovering(true)}
onMouseLeave={() => !isTouchDevice && setIsHovering(false)}
>

<div style={styles.mainArea}>
<div style={thumbnailWrapperStyle}>
{thumbnailUrl ? (
<img
style={styles.thumbnail}
src={thumbnailUrl}
alt={`Thumbnail for ${title}`}
/>
) : (
<img
style={styles.placeholderLogo}
src="/goose/img/goose.svg"
alt="Goose logo placeholder"
/>
)}
</div>
</div>

{showHoverOverlay && !isTouchDevice && (
<div style={hoverOverlayStyle}>
<h3 style={hoverTitleStyle}>{title}</h3>
<p style={hoverDescriptionStyle}>{description}</p>
<div style={hoverMetadataStyle}>
<div>
<span>{type.toUpperCase()}</span>
</div>
<div>
{date && <span>{formatDate(date)}</span>}
</div>
<div>
{duration && <span>{duration}</span>}
{type === 'blog' && !duration && <span>5 min read</span>}
</div>
</div>
</div>
)}
</a>
);
}
77 changes: 77 additions & 0 deletions documentation/src/components/ContentCardCarousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Navigation, Pagination, FreeMode } from 'swiper/modules';
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
import 'swiper/css/free-mode';
import ContentCard from './ContentCard';

type ContentType = 'video' | 'blog' ;

interface ContentItem {
type: ContentType;
title: string;
description: string;
thumbnailUrl?: string;
linkUrl: string;
date?: string;
duration?: string;
}

interface ContentCardCarouselProps {
items: ContentItem[];
size?: 'large' | 'compact';
showNavigation?: boolean;
showPagination?: boolean;
}

const carouselStyles = {
container: {
margin: '2rem 0',
},
swiperContainer: {
paddingBottom: '2rem', // Space for pagination dots
},
};

export default function ContentCardCarousel({
items,
size,
showNavigation = true,
showPagination = true,
}: ContentCardCarouselProps) {
return (
<div style={carouselStyles.container}>
<Swiper
slidesPerView="auto"
spaceBetween={16}
freeMode={false}
navigation={showNavigation}
pagination={showPagination ? {
clickable: true
} : false}
modules={[Navigation, Pagination, FreeMode]}
style={carouselStyles.swiperContainer}
>
{items.map((item, index) => (
<SwiperSlide key={index} style={{
width: size === 'large' ? 'min(500px, 90vw)' : 'min(350px, 85vw)',
minWidth: size === 'large' ? '300px' : '250px'
}}>
<ContentCard
type={item.type}
title={item.title}
description={item.description}
thumbnailUrl={item.thumbnailUrl}
linkUrl={item.linkUrl}
date={item.date}
duration={item.duration}
size={size}
/>
</SwiperSlide>
))}
</Swiper>
</div>
);
}