Skip to main content
Glama
joelmnz

Article Manager MCP Server

by joelmnz
Home.tsx14.1 kB
import React, { useState, useEffect } from 'react'; import { ArticleList } from '../components/ArticleList'; import { FolderManagementModal } from '../components/FolderManagementModal'; import { apiClient } from '../utils/apiClient'; interface Article { filename: string; title: string; folder?: string; created: string; } interface SearchResult { chunk: { filename: string; title: string; headingPath: string[]; text: string; }; score: number; snippet: string; } interface HomeProps { token: string; onNavigate: (path: string) => void; } export function Home({ token, onNavigate }: HomeProps) { const [articles, setArticles] = useState<Article[]>([]); const [searchResults, setSearchResults] = useState<SearchResult[]>([]); const [searchQuery, setSearchQuery] = useState(''); const [searchMode, setSearchMode] = useState<'title' | 'semantic'>(() => { try { const saved = localStorage.getItem('search_mode'); return saved === 'semantic' ? 'semantic' : 'title'; } catch { return 'title'; } }); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(20); const [folders, setFolders] = useState<string[]>([]); const [selectedFolder, setSelectedFolder] = useState<string>(() => { const params = new URLSearchParams(window.location.search); const urlFolder = params.get('folder'); if (urlFolder) return urlFolder; try { return localStorage.getItem('selected_folder') || ''; } catch { return ''; } }); const [showFolderModal, setShowFolderModal] = useState(false); useEffect(() => { const url = new URL(window.location.href); if (selectedFolder) { url.searchParams.set('folder', selectedFolder); try { localStorage.setItem('selected_folder', selectedFolder); } catch {} } else { url.searchParams.delete('folder'); try { localStorage.removeItem('selected_folder'); } catch {} } window.history.replaceState({}, '', url.toString()); }, [selectedFolder]); const loadArticles = async () => { try { setLoading(true); setSearchResults([]); const queryParams = new URLSearchParams(); if (selectedFolder) { queryParams.append('folder', selectedFolder); } const [articlesResponse, foldersResponse] = await Promise.all([ apiClient.get(`/api/articles?${queryParams.toString()}`, token), apiClient.get('/api/folders', token) ]); if (articlesResponse.ok && foldersResponse.ok) { const articlesData = await articlesResponse.json(); const foldersData = await foldersResponse.json(); setArticles(articlesData); setFolders(foldersData); setCurrentPage(1); // Reset to first page when loading all articles } else { setError('Failed to load articles'); } } catch (err) { setError('Failed to load articles'); } finally { setLoading(false); } }; const handleSearch = async (e: React.FormEvent) => { e.preventDefault(); if (!searchQuery.trim()) { loadArticles(); return; } try { setLoading(true); if (searchMode === 'semantic') { // Hybrid search (semantic + title boost) let searchUrl = `/api/search?query=${encodeURIComponent(searchQuery)}&k=10&mode=hybrid`; if (selectedFolder) { searchUrl += `&folder=${encodeURIComponent(selectedFolder)}`; } const response = await apiClient.get(searchUrl, token); if (response.ok) { const data = await response.json(); setSearchResults(data); setArticles([]); setCurrentPage(1); // Reset to first page on search } else { const errorData = await response.json(); setError(errorData.error || 'Semantic search failed'); } } else { // Title search const queryParams = new URLSearchParams(); queryParams.append('q', searchQuery); if (selectedFolder) { queryParams.append('folder', selectedFolder); } const response = await apiClient.get(`/api/articles?${queryParams.toString()}`, token); if (response.ok) { const data = await response.json(); setArticles(data); setSearchResults([]); setCurrentPage(1); // Reset to first page on search } else { setError('Search failed'); } } } catch (err) { setError('Search failed'); } finally { setLoading(false); } }; const handleArticleClick = (filename: string) => { onNavigate(`/article/${filename.replace('.md', '')}`); }; const handleEditClick = (filename: string) => { onNavigate(`/edit/${filename.replace('.md', '')}`); }; // Calculate pagination for articles const totalPages = Math.ceil(articles.length / pageSize); const startIndex = (currentPage - 1) * pageSize; const endIndex = startIndex + pageSize; const paginatedArticles = articles.slice(startIndex, endIndex); const handlePageChange = (page: number) => { setCurrentPage(page); window.scrollTo({ top: 0, behavior: 'smooth' }); }; const handlePageSizeChange = (newSize: number) => { setPageSize(newSize); setCurrentPage(1); // Reset to first page when changing page size }; const handleSearchModeChange = (mode: 'title' | 'semantic') => { setSearchMode(mode); try { localStorage.setItem('search_mode', mode); } catch { // Ignore localStorage errors } }; const handleFolderSelect = (folder: string) => { setSelectedFolder(folder); // Reload articles will be triggered by useEffect logic if we separate effects, // but here we call it manually to keep it simple or add it to dependencies }; const handleFolderUpdate = () => { // Reload articles and folders after folder rename/delete loadArticles(); // Reset selected folder if it no longer exists setSelectedFolder(''); }; const handleLinkClick = (e: React.MouseEvent, callback: () => void) => { if (e.ctrlKey || e.metaKey || e.button === 1) { // Allow default behavior (open in new tab) return; } e.preventDefault(); callback(); }; // Reload when folder changes useEffect(() => { loadArticles(); }, [selectedFolder]); return ( <div className="page"> <div className="page-header"> <h1>Articles</h1> <div className="page-header-actions"> <button className="button button-primary" onClick={() => { const url = selectedFolder ? `/new?folder=${encodeURIComponent(selectedFolder)}` : '/new'; onNavigate(url); }} > + New Article </button> </div> </div> <div className="filter-controls-container"> {/* Folder Filter */} {folders.length > 0 && ( <div className="folder-filter-section"> <label className="filter-label">Folder:</label> <select value={selectedFolder || ''} onChange={(e) => handleFolderSelect(e.target.value)} className="folder-select" > <option value="">All Folders</option> {folders.map((folder) => ( <option key={folder} value={folder}> 📁 {folder} </option> ))} </select> <button className="folder-manage-button" onClick={() => setShowFolderModal(true)} title="Manage folders" > 📂 </button> </div> )} {/* Search Form */} <form onSubmit={handleSearch} className="search-form"> <div className="search-mode-toggle"> <label> <input type="radio" value="title" checked={searchMode === 'title'} onChange={() => handleSearchModeChange('title')} /> Title </label> <label> <input type="radio" value="semantic" checked={searchMode === 'semantic'} onChange={() => handleSearchModeChange('semantic')} /> Semantic </label> </div> <div className="search-input-row"> <input type="text" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} placeholder={searchMode === 'semantic' ? 'Search by meaning...' : 'Search articles...'} className="search-input" /> <button type="submit" className="button">Search</button> {searchQuery && ( <button type="button" className="button button-secondary" onClick={() => { setSearchQuery(''); loadArticles(); }} > Clear </button> )} </div> </form> </div> {error && <div className="error-message">{error}</div>} {loading ? ( <div className="loading">Loading...</div> ) : searchResults.length > 0 ? ( <div className="search-results"> {searchResults.map((result, index) => ( <div key={index} className="article-item"> <a href={`/article/${result.chunk.filename.replace('.md', '')}`} className="article-item-link" onClick={(e) => handleLinkClick(e, () => handleArticleClick(result.chunk.filename))} > <div className="search-result-header"> <h3>{result.chunk.title}</h3> <span className="search-result-score">{(result.score * 100).toFixed(1)}%</span> </div> {result.chunk.headingPath.length > 0 && ( <div className="search-result-path"> {result.chunk.headingPath.join(' > ')} </div> )} <p className="search-result-snippet">{result.snippet}</p> </a> <a href={`/edit/${result.chunk.filename.replace('.md', '')}`} className="article-item-action" onClick={(e) => handleLinkClick(e, () => handleEditClick(result.chunk.filename))} title="Edit article" > ✏️ </a> </div> ))} </div> ) : ( <> <ArticleList articles={paginatedArticles} onArticleClick={handleArticleClick} onEditClick={handleEditClick} /> {articles.length > 0 && ( <div className="pagination-controls"> <div className="pagination-info"> Showing {startIndex + 1}-{Math.min(endIndex, articles.length)} of {articles.length} articles </div> <div className="pagination-size-selector"> <label>Items per page:</label> <select value={pageSize} onChange={(e) => handlePageSizeChange(Number(e.target.value))} className="page-size-select" > <option value={20}>20</option> <option value={50}>50</option> <option value={100}>100</option> </select> </div> {totalPages > 1 && ( <div className="pagination-buttons"> <button className="button button-secondary" onClick={() => handlePageChange(currentPage - 1)} disabled={currentPage === 1} > Previous </button> <div className="page-numbers"> {Array.from({ length: totalPages }, (_, i) => i + 1).map(page => { // Show first page, last page, current page, and pages around current const showPage = page === 1 || page === totalPages || (page >= currentPage - 1 && page <= currentPage + 1); const showEllipsis = (page === currentPage - 2 && currentPage > 3) || (page === currentPage + 2 && currentPage < totalPages - 2); if (showEllipsis) { return <span key={page} className="page-ellipsis">...</span>; } if (!showPage) { return null; } return ( <button key={page} className={`button page-number ${page === currentPage ? 'active' : ''}`} onClick={() => handlePageChange(page)} > {page} </button> ); })} </div> <button className="button button-secondary" onClick={() => handlePageChange(currentPage + 1)} disabled={currentPage === totalPages} > Next </button> </div> )} </div> )} </> )} <FolderManagementModal isOpen={showFolderModal} onClose={() => setShowFolderModal(false)} folders={folders} selectedFolder={selectedFolder} token={token} onFolderUpdate={handleFolderUpdate} /> </div> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/joelmnz/mcp-markdown-manager'

If you have feedback or need assistance with the MCP directory API, please join our Discord server