import { useState, useEffect } from 'react' import { api } from './api' import { StatCard } from './components/StatCard' import { SettingsPanel } from './components/SettingsPanel' import type { LinkGraphResult, LinkSuggestion, SemanticSuggestion, Cluster, DuplicatePair, WPPost } from './types' type Tab = 'graph' | 'suggestions' | 'clusters' | 'duplicates' | 'settings' export function App() { const [tab, setTab] = useState('graph') const [posts, setPosts] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState('') const [graphResult, setGraphResult] = useState(null) const [graphLoading, setGraphLoading] = useState(false) const [selectedSlug, setSelectedSlug] = useState('') const [selectedPostId, setSelectedPostId] = useState(null) const [suggestions, setSuggestions] = useState([]) const [semanticSuggestions, setSemanticSuggestions] = useState([]) const [suggestLoading, setSuggestLoading] = useState(false) const [useSemanticEngine, setUseSemanticEngine] = useState(false) const [clusters, setClusters] = useState([]) const [clusterLoading, setClusterLoading] = useState(false) const [duplicates, setDuplicates] = useState([]) const [dupThreshold, setDupThreshold] = useState(0.7) const [dupLoading, setDupLoading] = useState(false) useEffect(() => { api.getPosts().catch(() => []).then((postsData) => { setPosts(postsData) setLoading(false) }) }, []) async function handleBuildGraph() { setGraphLoading(true) setError('') try { const result = await api.buildLinkGraph() setGraphResult(result) } catch (e) { setError(e instanceof Error ? e.message : 'Failed to build link graph') } finally { setGraphLoading(false) } } async function handleSuggest() { if (!selectedSlug) return setSuggestLoading(true) setError('') setSuggestions([]) setSemanticSuggestions([]) try { if (useSemanticEngine) { if (!selectedPostId) { setError('Post ID unavailable — try reloading the page') return } const result = await api.semanticSuggestLinks(selectedPostId) setSemanticSuggestions(result.suggestions || []) } else { const result = await api.suggestLinks(selectedSlug) setSuggestions(result.suggestions || []) } } catch (e) { setError(e instanceof Error ? e.message : 'Failed to get suggestions') } finally { setSuggestLoading(false) } } async function handleClusters() { setClusterLoading(true) setError('') try { const result = await api.detectClusters() setClusters(result.clusters || []) } catch (e) { setError(e instanceof Error ? e.message : 'Failed to detect clusters') } finally { setClusterLoading(false) } } async function handleDuplicates() { setDupLoading(true) setError('') try { const result = await api.findDuplicates(dupThreshold) setDuplicates((result.duplicates || []).sort((a, b) => b.similarity - a.similarity)) } catch (e) { setError(e instanceof Error ? e.message : 'Failed to find duplicates') } finally { setDupLoading(false) } } if (loading) { return (
) } return (
Ganda Tech Services

Supan Content

by Ganda Tech Services

{error && (
{error}
)} {/* Tabs */}
{(['graph', 'suggestions', 'clusters', 'duplicates', 'settings'] as Tab[]).map((t) => ( ))}
{/* Link Graph Tab */} {tab === 'graph' && (

Internal Link Graph

Analyze the link structure across {posts.length} posts

{graphResult && ( <>
{Object.entries(graphResult.graph).map(([slug, entry]) => ( ))}
Slug Outgoing Incoming Status
{slug} {entry.outgoing.length} {entry.incoming.length} {graphResult.orphans.includes(slug) ? ( Orphan ) : ( Linked )}
)}
)} {/* Suggestions Tab */} {tab === 'suggestions' && (

Get Link Suggestions

{suggestions.length > 0 && (
{suggestions.map((s, i) => (

{s.target_title}

{s.reason}

= 0.8 ? 'bg-green-100 text-green-700' : s.relevance_score >= 0.5 ? 'bg-yellow-100 text-yellow-700' : 'bg-gray-100 text-gray-600' }`}> {Math.round(s.relevance_score * 100)}%
))}
)} {semanticSuggestions.length > 0 && (

AI-powered semantic suggestions

{semanticSuggestions.map((s, i) => (

{s.title}

{s.reason}

= 0.8 ? 'bg-green-100 text-green-700' : s.relevance >= 0.5 ? 'bg-yellow-100 text-yellow-700' : 'bg-gray-100 text-gray-600' }`}> {Math.round(s.relevance * 100)}%
))}
)}
)} {/* Clusters Tab */} {tab === 'clusters' && (

Topic Clusters

Detect content groups by topic

{clusters.length > 0 && (
{clusters.map((c, i) => (
{c.tag} {c.size} posts
{c.posts.map((slug) => ( {slug} ))}
))}
)}
)} {/* Duplicates Tab */} {tab === 'duplicates' && (

Duplicate Content

Find near-duplicate posts by similarity

setDupThreshold(Number(e.target.value) / 100)} className="w-full mt-1" />
{duplicates.length > 0 && (
{duplicates.map((d, i) => (
= 0.9 ? 'bg-red-50 border-red-200' : d.similarity >= 0.7 ? 'bg-yellow-50 border-yellow-200' : 'bg-purple-50 border-purple-200' }`}>
{d.slug_a} {d.slug_b}
{Math.round(d.similarity * 100)}%
))}
)}
)} {/* Settings Tab */} {tab === 'settings' && ( )}
) }