docs(website): add User Stories and Use Cases collage page (#18282)

Adds a new top-of-sidebar docs page at /docs/user-stories that is a
masonry-style collage of 99 real user stories sourced from X/Twitter,
GitHub issues/PRs, Reddit, Hacker News, YouTube, blogs (Medium, Substack,
dev.to), podcasts, LinkedIn, GitHub Gists, and Product Hunt.

Every tile links to the original post/issue/video/gist where someone
described a specific use case: personal assistants, dev workflows,
trading bots, research briefs, family WhatsApp agents, Kubernetes
deployments, legal-domain self-hosted setups, and more.

- docs/user-stories.mdx: MDX entry mounting the collage component
- src/components/UserStoriesCollage: React component with category +
  source filters, CSS-columns masonry layout, per-category accent colors
- src/data/userStories.json: source-of-truth dataset (force-added; the
  root .gitignore's unanchored 'data/' rule would otherwise swallow it,
  same reason skills.json is explicitly listed in website/.gitignore)
- sidebars.ts: link added at the top of the docs sidebar
This commit is contained in:
Teknium 2026-04-30 23:56:59 -07:00 committed by GitHub
parent dfe512c58d
commit a2a32688ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 1664 additions and 0 deletions

View file

@ -0,0 +1,310 @@
import React, { useMemo, useState } from 'react';
import stories from '@site/src/data/userStories.json';
import styles from './styles.module.css';
interface Story {
id: string;
source: string;
author: string;
url: string;
date: string;
category: string;
headline: string;
quote: string;
size: 'sm' | 'md' | 'lg';
}
const allStories = stories as Story[];
// Category → pretty label + accent colors (solid + soft fill + gradient top-strip)
const CATEGORIES: Record<
string,
{ label: string; solid: string; soft: string; strip: string }
> = {
'dev-workflow': {
label: 'Dev Workflow',
solid: '#60a5fa',
soft: 'rgba(96, 165, 250, 0.14)',
strip: 'linear-gradient(90deg, #3b82f6, #60a5fa, #a78bfa)',
},
'personal-assistant': {
label: 'Personal Assistant',
solid: '#34d399',
soft: 'rgba(52, 211, 153, 0.14)',
strip: 'linear-gradient(90deg, #10b981, #34d399, #a7f3d0)',
},
'content-creation': {
label: 'Content Creation',
solid: '#f472b6',
soft: 'rgba(244, 114, 182, 0.14)',
strip: 'linear-gradient(90deg, #ec4899, #f472b6, #fda4af)',
},
'business-ops': {
label: 'Business Ops',
solid: '#fb923c',
soft: 'rgba(251, 146, 60, 0.14)',
strip: 'linear-gradient(90deg, #f97316, #fb923c, #fcd34d)',
},
trading: {
label: 'Trading & Markets',
solid: '#facc15',
soft: 'rgba(250, 204, 21, 0.16)',
strip: 'linear-gradient(90deg, #eab308, #facc15, #fde047)',
},
research: {
label: 'Research',
solid: '#a78bfa',
soft: 'rgba(167, 139, 250, 0.14)',
strip: 'linear-gradient(90deg, #8b5cf6, #a78bfa, #c4b5fd)',
},
creative: {
label: 'Creative',
solid: '#f87171',
soft: 'rgba(248, 113, 113, 0.14)',
strip: 'linear-gradient(90deg, #ef4444, #f87171, #fca5a5)',
},
marketing: {
label: 'Marketing',
solid: '#e879f9',
soft: 'rgba(232, 121, 249, 0.14)',
strip: 'linear-gradient(90deg, #d946ef, #e879f9, #f0abfc)',
},
integrations: {
label: 'Integrations',
solid: '#38bdf8',
soft: 'rgba(56, 189, 248, 0.14)',
strip: 'linear-gradient(90deg, #0ea5e9, #38bdf8, #7dd3fc)',
},
enterprise: {
label: 'Enterprise',
solid: '#94a3b8',
soft: 'rgba(148, 163, 184, 0.16)',
strip: 'linear-gradient(90deg, #64748b, #94a3b8, #cbd5e1)',
},
messaging: {
label: 'Messaging',
solid: '#22d3ee',
soft: 'rgba(34, 211, 238, 0.14)',
strip: 'linear-gradient(90deg, #06b6d4, #22d3ee, #67e8f9)',
},
privacy: {
label: 'Privacy & Self-Hosted',
solid: '#4ade80',
soft: 'rgba(74, 222, 128, 0.14)',
strip: 'linear-gradient(90deg, #16a34a, #4ade80, #86efac)',
},
'cost-optimization': {
label: 'Cost Optimization',
solid: '#fbbf24',
soft: 'rgba(251, 191, 36, 0.16)',
strip: 'linear-gradient(90deg, #f59e0b, #fbbf24, #fde68a)',
},
meta: {
label: 'Meta & Ecosystem',
solid: '#c084fc',
soft: 'rgba(192, 132, 252, 0.14)',
strip: 'linear-gradient(90deg, #a855f7, #c084fc, #d8b4fe)',
},
general: {
label: 'General',
solid: '#9ca3af',
soft: 'rgba(156, 163, 175, 0.16)',
strip: 'linear-gradient(90deg, #6b7280, #9ca3af, #d1d5db)',
},
};
// Source → compact label shown in the badge row
const SOURCE_LABELS: Record<string, string> = {
x: 'X · Twitter',
hn: 'Hacker News',
reddit: 'Reddit',
github: 'GitHub',
youtube: 'YouTube',
blog: 'Blog',
podcast: 'Podcast',
linkedin: 'LinkedIn',
gist: 'GitHub Gist',
producthunt: 'Product Hunt',
};
function sourceColor(source: string): string {
switch (source) {
case 'x': return '#1d9bf0';
case 'hn': return '#ff6600';
case 'reddit': return '#ff4500';
case 'github': return '#8b949e';
case 'youtube': return '#ff0033';
case 'blog': return '#a78bfa';
case 'podcast': return '#8b5cf6';
case 'linkedin': return '#0a66c2';
case 'gist': return '#8b949e';
case 'producthunt': return '#da552f';
default: return '#64748b';
}
}
export default function UserStoriesCollage(): JSX.Element {
const [activeCategory, setActiveCategory] = useState<string>('all');
const [activeSource, setActiveSource] = useState<string>('all');
const categoryCounts = useMemo(() => {
const counts: Record<string, number> = {};
for (const s of allStories) counts[s.category] = (counts[s.category] ?? 0) + 1;
return counts;
}, []);
const sourceCounts = useMemo(() => {
const counts: Record<string, number> = {};
for (const s of allStories) counts[s.source] = (counts[s.source] ?? 0) + 1;
return counts;
}, []);
const visible = useMemo(() => {
return allStories.filter((s) => {
if (activeCategory !== 'all' && s.category !== activeCategory) return false;
if (activeSource !== 'all' && s.source !== activeSource) return false;
return true;
});
}, [activeCategory, activeSource]);
return (
<div className={styles.wrap}>
<div className={styles.hero}>
<h1>User Stories &amp; Use Cases</h1>
<p>
What the Hermes Agent community is actually building. Every tile
below links to a real post, issue, video, or gist where someone
describes how they use Hermes &mdash; scraped from X, GitHub, Reddit,
Hacker News, YouTube, blogs, and podcasts.
</p>
<div className={styles.meta}>
<span><strong>{allStories.length}</strong> stories</span>
<span><strong>{Object.keys(categoryCounts).length}</strong> categories</span>
<span><strong>{Object.keys(sourceCounts).length}</strong> sources</span>
</div>
</div>
{/* Category filters */}
<div className={styles.filters}>
<button
type="button"
className={`${styles.filterBtn} ${activeCategory === 'all' ? styles.filterActive : ''}`}
onClick={() => setActiveCategory('all')}
>
All<span className={styles.filterCount}>{allStories.length}</span>
</button>
{Object.entries(CATEGORIES)
.filter(([key]) => categoryCounts[key])
.sort((a, b) => (categoryCounts[b[0]] ?? 0) - (categoryCounts[a[0]] ?? 0))
.map(([key, meta]) => (
<button
key={key}
type="button"
className={`${styles.filterBtn} ${activeCategory === key ? styles.filterActive : ''}`}
onClick={() => setActiveCategory(key)}
style={
activeCategory === key
? { background: meta.solid, borderColor: meta.solid, color: '#0f172a' }
: undefined
}
>
{meta.label}
<span className={styles.filterCount}>{categoryCounts[key]}</span>
</button>
))}
</div>
{/* Source filters — smaller, secondary row */}
<div className={styles.filters} style={{ marginTop: '-0.75rem' }}>
<button
type="button"
className={`${styles.filterBtn} ${activeSource === 'all' ? styles.filterActive : ''}`}
onClick={() => setActiveSource('all')}
style={{ fontSize: '0.72rem' }}
>
All sources
</button>
{Object.entries(SOURCE_LABELS)
.filter(([key]) => sourceCounts[key])
.map(([key, label]) => (
<button
key={key}
type="button"
className={`${styles.filterBtn} ${activeSource === key ? styles.filterActive : ''}`}
onClick={() => setActiveSource(key)}
style={{
fontSize: '0.72rem',
...(activeSource === key
? { background: sourceColor(key), borderColor: sourceColor(key), color: '#fff' }
: {}),
}}
>
{label}
<span className={styles.filterCount}>{sourceCounts[key]}</span>
</button>
))}
</div>
{/* Collage grid */}
{visible.length === 0 ? (
<div className={styles.empty}>No stories match that filter.</div>
) : (
<div className={styles.grid}>
{visible.map((s) => {
const cat = CATEGORIES[s.category] ?? CATEGORIES.general;
const sizeClass =
s.size === 'lg' ? styles.tileLg : s.size === 'sm' ? styles.tileSm : styles.tileMd;
const srcColor = sourceColor(s.source);
return (
<a
key={s.id}
className={`${styles.tile} ${sizeClass}`}
href={s.url}
target="_blank"
rel="noopener noreferrer"
style={
{
'--tile-accent': cat.strip,
'--tile-accent-solid': cat.solid,
'--tile-accent-soft': cat.soft,
} as React.CSSProperties
}
>
<div className={styles.badgeRow}>
<span className={styles.sourceBadge}>
<span className={styles.sourceIcon} style={{ background: srcColor }} />
{SOURCE_LABELS[s.source] ?? s.source}
</span>
<span className={styles.catTag}>{cat.label}</span>
</div>
<h3 className={styles.headline}>{s.headline}</h3>
<p className={styles.quote}>&ldquo;{s.quote}&rdquo;</p>
<span className={styles.author}>
{s.author}
{s.date ? <> &middot; {s.date}</> : null}
</span>
<span className={styles.external} aria-hidden="true"></span>
</a>
);
})}
</div>
)}
<div className={styles.footer}>
Built something with Hermes?{' '}
<a
href="https://github.com/NousResearch/hermes-agent/edit/main/website/src/data/userStories.json"
target="_blank"
rel="noopener noreferrer"
>
Add your story to this page
</a>{' '}
by editing <code>userStories.json</code>, or post it in the{' '}
<a href="https://discord.gg/NousResearch" target="_blank" rel="noopener noreferrer">
Nous Research Discord
</a>{' '}
and we&apos;ll pick it up.
</div>
</div>
);
}

View file

@ -0,0 +1,252 @@
/* User Stories collage — masonry grid with category-driven accents. */
.wrap {
max-width: 1280px;
margin: 0 auto;
padding: 0 0 4rem;
}
.hero {
padding: 2.5rem 0 2rem;
text-align: center;
}
.hero h1 {
font-size: clamp(2rem, 4vw, 3.25rem);
margin-bottom: 0.75rem;
background: linear-gradient(120deg, #a78bfa 0%, #60a5fa 50%, #34d399 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.hero p {
max-width: 680px;
margin: 0 auto;
color: var(--ifm-color-emphasis-700);
font-size: 1.05rem;
line-height: 1.6;
}
.meta {
display: flex;
gap: 1.5rem;
justify-content: center;
margin-top: 1.25rem;
flex-wrap: wrap;
font-size: 0.85rem;
color: var(--ifm-color-emphasis-600);
}
.meta strong {
color: var(--ifm-color-emphasis-900);
font-weight: 600;
}
/* Filter bar */
.filters {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
justify-content: center;
margin: 1.75rem 0 2rem;
padding: 0 1rem;
}
.filterBtn {
padding: 0.35rem 0.85rem;
border-radius: 999px;
border: 1px solid var(--ifm-color-emphasis-300);
background: transparent;
color: var(--ifm-color-emphasis-800);
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
transition: all 0.18s ease;
white-space: nowrap;
}
.filterBtn:hover {
border-color: var(--ifm-color-emphasis-500);
color: var(--ifm-color-emphasis-1000);
transform: translateY(-1px);
}
.filterActive {
background: var(--ifm-color-emphasis-900);
color: var(--ifm-background-color);
border-color: var(--ifm-color-emphasis-900);
}
[data-theme='dark'] .filterActive {
background: #e2e8f0;
color: #0f172a;
border-color: #e2e8f0;
}
.filterCount {
margin-left: 0.35rem;
opacity: 0.5;
font-variant-numeric: tabular-nums;
}
/* Masonry — use CSS columns for a true collage feel */
.grid {
column-count: 4;
column-gap: 1rem;
padding: 0 1rem;
}
@media (max-width: 1200px) { .grid { column-count: 3; } }
@media (max-width: 850px) { .grid { column-count: 2; } }
@media (max-width: 560px) { .grid { column-count: 1; } }
/* Tile */
.tile {
break-inside: avoid;
margin-bottom: 1rem;
position: relative;
display: block;
padding: 1.1rem 1.2rem 1.15rem;
border-radius: 14px;
border: 1px solid var(--ifm-color-emphasis-200);
background: var(--ifm-card-background-color, var(--ifm-background-surface-color));
color: inherit !important;
text-decoration: none !important;
overflow: hidden;
transition: transform 0.22s ease, box-shadow 0.22s ease, border-color 0.22s ease;
}
.tile::before {
/* Color accent strip */
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 3px;
background: var(--tile-accent, linear-gradient(90deg, #a78bfa, #60a5fa));
opacity: 0.9;
}
.tile::after {
/* Subtle hover glow */
content: '';
position: absolute;
inset: -1px;
border-radius: 14px;
box-shadow: 0 0 0 0 transparent;
pointer-events: none;
transition: box-shadow 0.22s ease;
}
.tile:hover {
transform: translateY(-3px);
border-color: var(--tile-accent-solid, var(--ifm-color-primary));
box-shadow: 0 8px 24px -8px rgba(0, 0, 0, 0.25);
}
[data-theme='dark'] .tile:hover {
box-shadow: 0 10px 30px -12px rgba(120, 120, 200, 0.45);
}
/* Size variants — big tiles get more visual weight */
.tileSm { min-height: 130px; }
.tileMd { min-height: 180px; }
.tileLg {
min-height: 240px;
padding: 1.35rem 1.45rem 1.45rem;
}
.tileLg .headline {
font-size: 1.3rem;
}
/* Tile body */
.badgeRow {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
font-size: 0.7rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--ifm-color-emphasis-600);
}
.sourceBadge {
display: inline-flex;
align-items: center;
gap: 0.35rem;
font-weight: 600;
}
.sourceIcon {
display: inline-block;
width: 14px;
height: 14px;
border-radius: 3px;
background: var(--tile-accent-solid, #a78bfa);
flex-shrink: 0;
}
.catTag {
display: inline-block;
padding: 0.15rem 0.55rem;
border-radius: 999px;
background: var(--tile-accent-soft, rgba(167, 139, 250, 0.12));
color: var(--tile-accent-solid, #a78bfa);
font-weight: 600;
letter-spacing: 0.04em;
}
.headline {
font-size: 1.02rem;
font-weight: 700;
line-height: 1.3;
margin: 0 0 0.5rem;
color: var(--ifm-color-emphasis-1000);
}
.quote {
font-size: 0.875rem;
line-height: 1.55;
color: var(--ifm-color-emphasis-800);
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 6;
-webkit-box-orient: vertical;
overflow: hidden;
}
.tileLg .quote { -webkit-line-clamp: 8; }
.tileSm .quote { -webkit-line-clamp: 4; }
.author {
display: block;
margin-top: 0.7rem;
font-size: 0.78rem;
color: var(--ifm-color-emphasis-600);
font-weight: 500;
}
.external {
position: absolute;
top: 0.9rem;
right: 0.9rem;
opacity: 0;
font-size: 0.85rem;
color: var(--tile-accent-solid, var(--ifm-color-primary));
transition: opacity 0.2s ease, transform 0.2s ease;
}
.tile:hover .external {
opacity: 1;
transform: translate(2px, -2px);
}
/* Footer */
.footer {
margin: 3rem auto 0;
padding: 1.5rem;
text-align: center;
max-width: 720px;
border-radius: 14px;
background: var(--ifm-color-emphasis-100);
font-size: 0.95rem;
color: var(--ifm-color-emphasis-800);
line-height: 1.6;
}
.footer a {
color: var(--ifm-color-primary);
text-decoration: none;
font-weight: 600;
}
.footer a:hover { text-decoration: underline; }
.empty {
padding: 3rem 1rem;
text-align: center;
color: var(--ifm-color-emphasis-600);
font-size: 0.95rem;
}