mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-07 02:51:50 +00:00
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:
parent
dfe512c58d
commit
a2a32688ca
5 changed files with 1664 additions and 0 deletions
310
website/src/components/UserStoriesCollage/index.tsx
Normal file
310
website/src/components/UserStoriesCollage/index.tsx
Normal 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 & 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 — 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}>“{s.quote}”</p>
|
||||
<span className={styles.author}>
|
||||
{s.author}
|
||||
{s.date ? <> · {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'll pick it up.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue