docs: add content carousel (#5086)
This commit is contained in:
@@ -278,3 +278,50 @@ The following operations are blocked to ensure subagents remain focused on their
|
|||||||
:::info
|
:::info
|
||||||
Subagents can browse extensions for suggestions but cannot enable them to avoid modifying the parent session.
|
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'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user