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
10 changes: 10 additions & 0 deletions website/docs/user-stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
title: User Stories & Use Cases
description: Real stories from the Hermes Agent community — what people are actually building, scraped from X, GitHub, Reddit, Hacker News, YouTube, blogs, and podcasts.
hide_title: true
hide_table_of_contents: true
---

import UserStoriesCollage from '@site/src/components/UserStoriesCollage';

<UserStoriesCollage />
1 change: 1 addition & 0 deletions website/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';

const sidebars: SidebarsConfig = {
docs: [
'user-stories',
{
type: 'category',
label: 'Getting Started',
Expand Down
310 changes: 310 additions & 0 deletions website/src/components/UserStoriesCollage/index.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
Loading
Loading