Home

Embedding Docs Browser in Git Chat: Architecture Options

Embedding Docs Browser in Git Chat: Architecture Options

Context: We've built a Spotify-style documentation browser in yap (vanilla HTML/CSS/JS, server-side rendering). Now we want to embed it into Git Chat (React + TypeScript + Vite) as a new page/route, similar to how Instagram has Reels or YouTube has Shorts.

Goal: Unified experience where you can browse repos (contacts), execute commands (chat), and read documentation (new docs page) - all in one app.


The Technical Challenge

yap (docs browser):

Git Chat:

The Question: How do we integrate these without significant sacrifices?


Architecture

┌─────────────────────────────────────────────┐
│  Git Chat Frontend (React)                  │
│  Port 3015                                  │
│                                             │
│  ├─ ContactsPage.tsx (repos)               │
│  ├─ ChatPage.tsx (chat interface)          │
│  └─ DocsPage.tsx (NEW - pure React)        │  ← Port the card UI
│     - Uses React components                │
│     - Fetches from API below               │
│     - Tailwind CSS styling                 │
└─────────────────┬───────────────────────────┘
                  │
                  │ HTTP requests
                  │
┌─────────────────▼───────────────────────────┐
│  Git Chat Backend (Node.js)                 │
│  Port 8000                                  │
│                                             │
│  NEW Endpoints:                             │
│  GET /api/docs/all                          │
│    → Scans workspace repos                  │
│    → Finds all .md files                    │
│    → Extracts metadata (hybrid approach)    │
│    → Returns JSON                           │
│                                             │
│  GET /api/docs/:repoId                      │
│    → Docs for specific repo                 │
│                                             │
│  GET /api/docs/:repoId/:docPath             │
│    → Full doc content + metadata            │
└─────────────────────────────────────────────┘

Implementation Steps

1. Backend (Git Chat server.js)

// New route: Get all docs from all repos
app.get('/api/docs/all', (req, res) => {
  const workspacePath = '/home/ubuntu/workplace';
  const allDocs = [];

  // Scan all repos
  const repos = fs.readdirSync(workspacePath)
    .filter(dir => {
      const gitPath = path.join(workspacePath, dir, '.git');
      return fs.existsSync(gitPath);
    });

  repos.forEach(repo => {
    const repoPath = path.join(workspacePath, repo);
    const mdFiles = findMarkdownFiles(repoPath);

    mdFiles.forEach(file => {
      const content = fs.readFileSync(file, 'utf-8');
      const metadata = extractMetadata(content, file, repo);
      allDocs.push(metadata);
    });
  });

  // Sort by date, most recent first
  allDocs.sort((a, b) => new Date(b.dateAdded) - new Date(a.dateAdded));

  res.json(allDocs);
});

// Helper: Extract metadata (hybrid approach)
function extractMetadata(content, filepath, repo) {
  const { data, content: markdown } = matter(content);

  return {
    id: generateId(filepath),
    title: data.title || extractFirstH1(markdown) || filenameToTitle(filepath),
    category: data.category || inferCategoryFromPath(filepath),
    repo: data.repo || repo,
    dateAdded: data.dateAdded || getGitCommitDate(filepath) || new Date().toISOString(),
    tags: data.tags || [],
    path: filepath,
    preview: markdown.substring(0, 200) // First 200 chars
  };
}

// Helper: Infer category from folder structure
function inferCategoryFromPath(filepath) {
  const parts = filepath.split('/');
  const parentFolder = parts[parts.length - 2];

  const folderMap = {
    'design': 'Design',
    'docs': 'Documentation',
    'stories': 'Stories',
    'personal': 'Personal',
    // ...
  };

  return folderMap[parentFolder] ||
         guessFromFilename(filepath) ||
         'Uncategorized';
}

2. Frontend (Git Chat React)

// src/pages/DocsPage.tsx
import { useState, useEffect } from 'react';
import { DocCard } from '@/components/DocCard';

interface Doc {
  id: string;
  title: string;
  category: string;
  repo: string;
  dateAdded: string;
  path: string;
}

export default function DocsPage() {
  const [docs, setDocs] = useState<Doc[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/api/docs/all')
      .then(res => res.json())
      .then(data => {
        setDocs(data);
        setLoading(false);
      });
  }, []);

  const recentDocs = docs.slice(0, 10);
  const groupedDocs = groupByCategory(docs);

  if (loading) return <LoadingSpinner />;

  return (
    <div className="docs-page">
      <header className="px-8 py-6">
        <h1 className="text-3xl font-bold">Docs</h1>
      </header>

      {/* Recently Added Section */}
      <section className="mb-12">
        <h2 className="px-8 text-2xl font-semibold mb-4">Recently Added</h2>
        <HorizontalScroll>
          {recentDocs.map(doc => (
            <DocCard key={doc.id} doc={doc} />
          ))}
        </HorizontalScroll>
      </section>

      {/* Category Sections */}
      {Object.entries(groupedDocs).map(([category, docs]) => (
        <section key={category} className="mb-12">
          <h2 className="px-8 text-2xl font-semibold mb-4">{category}</h2>
          <HorizontalScroll>
            {docs.map(doc => (
              <DocCard key={doc.id} doc={doc} />
            ))}
          </HorizontalScroll>
        </section>
      ))}
    </div>
  );
}
// src/components/DocCard.tsx
import { Link } from 'react-router-dom';

interface DocCardProps {
  doc: {
    id: string;
    title: string;
    category: string;
    path: string;
  };
}

// Hash function for deterministic colors
function simpleHash(str: string): number {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    hash = ((hash << 5) - hash) + str.charCodeAt(i);
    hash = hash & hash;
  }
  return Math.abs(hash);
}

export function DocCard({ doc }: DocCardProps) {
  const hash = simpleHash(doc.title);
  const hue1 = hash % 360;
  const hue2 = (hash * 137) % 360;

  const gradient = `linear-gradient(135deg,
    hsl(${hue1}, 30%, 40%),
    hsl(${hue2}, 35%, 45%))`;

  return (
    <Link
      to={`/docs/${doc.id}`}
      className="doc-card flex-shrink-0 w-[180px] h-[240px] rounded-lg relative overflow-hidden cursor-pointer transition-transform hover:scale-105"
      style={{ background: gradient }}
    >
      {/* Texture overlay */}
      <div className="absolute inset-0 bg-gradient-radial from-white/10 via-transparent to-black/10" />

      <div className="absolute bottom-10 left-4 right-4">
        <h3 className="text-white font-semibold text-sm line-clamp-3 drop-shadow-lg">
          {doc.title}
        </h3>
      </div>

      <div className="absolute bottom-4 left-4">
        <span className="text-white/90 text-xs uppercase tracking-wide font-medium drop-shadow">
          {doc.category}
        </span>
      </div>
    </Link>
  );
}
// src/components/HorizontalScroll.tsx
export function HorizontalScroll({ children }: { children: React.ReactNode }) {
  return (
    <div className="horizontal-scroll flex gap-4 overflow-x-auto snap-x snap-mandatory px-8 pb-4 scrollbar-hide">
      {children}
    </div>
  );
}

3. Routing (Git Chat App.tsx)

// Add new route
<Route path="/docs" element={<DocsPage />} />
<Route path="/docs/:docId" element={<DocViewPage />} />

Pros ✅

Cons ❌

When to Use

✅ Best choice if you want:


Option 2: iframe Embedding (Quick & Dirty)

Architecture

// Git Chat DocsPage.tsx
export default function DocsPage() {
  return (
    <div className="h-screen">
      <iframe
        src="http://localhost:3040"
        className="w-full h-full border-0"
        title="Documentation Browser"
      />
    </div>
  );
}

Keep yap server running on port 3040, embed entire page as iframe.

Pros ✅

Cons ❌

When to Use

✅ Only for quick prototyping or temporary solutions ❌ Not recommended for production


Option 3: Hybrid API + Vanilla JS Component

Architecture

Build the card UI as a vanilla JS web component that can be dropped into React.

// yap/public/docs-browser.js (vanilla JS module)
class DocsBrowser extends HTMLElement {
  connectedCallback() {
    this.render();
    this.fetchDocs();
  }

  async fetchDocs() {
    const docs = await fetch('/api/docs/all').then(r => r.json());
    this.renderCards(docs);
  }

  renderCards(docs) {
    // Same gradient logic as before
    // Pure vanilla JS, no React
  }
}

customElements.define('docs-browser', DocsBrowser);
// Git Chat React component
export default function DocsPage() {
  useEffect(() => {
    // Load vanilla JS component
    const script = document.createElement('script');
    script.src = 'http://localhost:3040/docs-browser.js';
    document.body.appendChild(script);
  }, []);

  return <docs-browser />;
}

Pros ✅

Cons ❌

When to Use

✅ If you want to embed in multiple different apps ❌ Overkill for just Git Chat


Option 4: Build-Time Integration

Architecture

Build yap UI as static HTML/CSS/JS, import into React as raw assets.

# yap repo: Export as static build
npm run build-standalone
# Outputs: dist/docs-browser.html, docs-browser.css, docs-browser.js

# Git Chat: Copy assets
cp ../yap/dist/* public/docs/
// Git Chat DocsPage.tsx
export default function DocsPage() {
  return (
    <div
      dangerouslySetInnerHTML={{ __html: staticHTML }}
      className="docs-container"
    />
  );
}

Pros ✅

Cons ❌

When to Use

❌ Not recommended - worst of both worlds


Comparison Matrix

Criterion Option 1: API + React Option 2: iframe Option 3: Web Component Option 4: Static Build
UX Quality ⭐⭐⭐⭐⭐ Native ⭐⭐ Embedded site ⭐⭐⭐ Good ⭐⭐⭐ Good
Dev Speed ⭐⭐⭐ 2-3 hours ⭐⭐⭐⭐⭐ 5 mins ⭐⭐⭐ 4-6 hours ⭐⭐ Complex
Maintenance ⭐⭐⭐ Moderate ⭐⭐⭐⭐⭐ Easy ⭐⭐⭐⭐ Easy ⭐⭐ Hard
Performance ⭐⭐⭐⭐⭐ Fast ⭐⭐⭐ Okay ⭐⭐⭐⭐ Fast ⭐⭐⭐⭐⭐ Fastest
Flexibility ⭐⭐⭐⭐⭐ Full ⭐ Limited ⭐⭐⭐⭐ Good ⭐⭐ Limited
Type Safety ⭐⭐⭐⭐⭐ Yes ❌ No ⭐⭐⭐ Partial ❌ No
Repo Independence ⭐⭐⭐⭐ Good ⭐⭐⭐⭐⭐ Perfect ⭐⭐⭐⭐ Good ⭐⭐ Coupled

Recommendation: Option 1 (Backend API + React Port)

Why:

  1. Best UX - Feels native, no jarring iframe boundaries
  2. Long-term scalable - Can add features like search, filters, favorites
  3. Consistent styling - Uses Tailwind like rest of Git Chat
  4. Type-safe - Full TypeScript benefits
  5. Repo independence preserved - Backend does aggregation, markdown files stay clean

Tradeoffs:

Mitigation:

Keep yap as standalone reference implementation + testing ground. When UI is stable, port to Git Chat. Future changes happen in Git Chat only.


Implementation Roadmap

Phase 1: Backend API (Git Chat)

Time: 1-2 hours

  1. Add /api/docs/all endpoint
  2. Implement hybrid metadata extraction
  3. Test with curl/Postman

Phase 2: React Components (Git Chat)

Time: 2-3 hours

  1. Create DocCard.tsx component
  2. Create DocsPage.tsx with horizontal scrolling
  3. Add routing: /docs and /docs/:docId
  4. Port hash-to-gradient logic
  5. Style with Tailwind

Phase 3: Doc Viewer (Git Chat)

Time: 1 hour

  1. Create DocViewPage.tsx
  2. Fetch doc content from backend
  3. Render markdown (use react-markdown)
  4. Add back navigation

Phase 4: Polish

Time: 1-2 hours

  1. Loading states
  2. Error handling
  3. Empty states ("No docs found")
  4. Mobile responsive tweaks

Total: ~6-8 hours for full integration


Future Enhancements

Once basic integration is done:

  1. Search - Fuzzy search across all docs
  2. Filters - By repo, category, date, tags
  3. Favorites - Save docs for quick access
  4. Reading Progress - Track which docs you've read
  5. Recent History - Show recently viewed docs
  6. Cross-linking - Link between chat commands and relevant docs
  7. AI Integration - "Ask AI about this doc"

Conclusion

Start with Option 1 (Backend API + React Port) because:

Keep yap repo for:

The two repos remain independent but Git Chat becomes the primary UX surface for browsing docs across all your repos.

READ i