import { useState, useEffect, useCallback } from 'react';
import { AppMode, DocType } from './types';
import { usePersona } from './hooks/usePersona';
import { useStats } from './hooks/useStats';
import { useSearch } from './hooks/useSearch';
import { useBrowse } from './hooks/useBrowse';
import { useConsult } from './hooks/useConsult';
import { useReflect } from './hooks/useReflect';
import { useUrlState } from './hooks/useUrlState';
import { Header } from './components/Header';
import { SearchBox } from './components/SearchBox';
import { TypeTabs } from './components/TypeTabs';
import { ResultsList } from './components/ResultsList';
import { ResultCard } from './components/ResultCard';
import { Pagination } from './components/Pagination';
import { GuidanceBox } from './components/GuidanceBox';
import { Graph } from './components/Graph';
import { LearnModal } from './components/LearnModal';
import { FileModal } from './components/FileModal';
import { ArthurPage } from './components/Arthur';
type Page = 'oracle' | 'arthur';
function App() {
// Page routing based on hash
const [page, setPage] = useState<Page>(() => {
return window.location.hash === '#arthur' ? 'arthur' : 'oracle';
});
// Listen for hash changes
useEffect(() => {
const handleHashChange = () => {
setPage(window.location.hash === '#arthur' ? 'arthur' : 'oracle');
};
window.addEventListener('hashchange', handleHashChange);
return () => window.removeEventListener('hashchange', handleHashChange);
}, []);
// Navigation functions
const navigateToOracle = useCallback(() => {
window.location.hash = '';
setPage('oracle');
}, []);
const navigateToArthur = useCallback(() => {
window.location.hash = '#arthur';
setPage('arthur');
}, []);
// Render Arthur page if selected
if (page === 'arthur') {
return <ArthurPage onNavigateToOracle={navigateToOracle} />;
}
const { config, toggle } = usePersona();
const { stats, loading: statsLoading, refresh: refreshStats } = useStats();
const urlState = useUrlState();
const [query, setQuery] = useState(urlState.query);
const [mode, setMode] = useState<AppMode>(urlState.mode);
const [type, setType] = useState<DocType>(urlState.type);
const [offset, setOffset] = useState(urlState.offset);
const [learnModalOpen, setLearnModalOpen] = useState(false);
const [fileModalPath, setFileModalPath] = useState<string | null>(null);
// Hooks for different modes
const search = useSearch();
const browse = useBrowse();
const consult = useConsult();
const reflect = useReflect();
// Sync URL state
useEffect(() => {
urlState.updateUrl({ mode, type, offset, query });
}, [mode, type, offset, query]);
// Load initial data based on mode
useEffect(() => {
if (mode === 'browse') {
browse.browse(type, offset);
} else if (mode === 'search' && query) {
search.search(query, type, offset);
}
}, []); // Only on mount
// Handle search
const handleSearch = useCallback(() => {
if (!query.trim()) return;
setMode('search');
setOffset(0);
search.search(query, type, 0);
}, [query, type, search]);
// Handle browse
const handleBrowse = useCallback(() => {
setMode('browse');
setOffset(0);
browse.browse(type, 0);
}, [type, browse]);
// Handle consult
const handleConsult = useCallback(() => {
if (!query.trim()) return;
setMode('consult');
consult.consult(query);
}, [query, consult]);
// Handle reflect
const handleReflect = useCallback(() => {
setMode('reflect');
reflect.reflect();
}, [reflect]);
// Handle graph
const handleGraph = useCallback(() => {
setMode('graph');
}, []);
// Handle type change
const handleTypeChange = useCallback((newType: DocType) => {
setType(newType);
setOffset(0);
if (mode === 'browse') {
browse.browse(newType, 0);
} else if (mode === 'search' && query) {
search.search(query, newType, 0);
}
}, [mode, query, browse, search]);
// Handle pagination
const handlePrev = useCallback(() => {
const pageSize = 20;
const newOffset = Math.max(0, offset - pageSize);
setOffset(newOffset);
if (mode === 'browse') {
browse.browse(type, newOffset);
} else if (mode === 'search') {
search.search(query, type, newOffset);
}
window.scrollTo({ top: 0, behavior: 'smooth' });
}, [offset, mode, type, query, browse, search]);
const handleNext = useCallback(() => {
const pageSize = 20;
const newOffset = offset + pageSize;
setOffset(newOffset);
if (mode === 'browse') {
browse.browse(type, newOffset);
} else if (mode === 'search') {
search.search(query, type, newOffset);
}
window.scrollTo({ top: 0, behavior: 'smooth' });
}, [offset, mode, type, query, browse, search]);
// Handle file click
const handleFileClick = useCallback((path: string) => {
setFileModalPath(path);
}, []);
// Handle learn success
const handleLearnSuccess = useCallback(() => {
refreshStats();
}, [refreshStats]);
// Handle keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
setFileModalPath(null);
setLearnModalOpen(false);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
// Render content based on mode
const renderContent = () => {
switch (mode) {
case 'search':
return (
<>
<TypeTabs currentType={type} onTypeChange={handleTypeChange} />
<ResultsList
results={search.results}
loading={search.loading}
error={search.error}
emptyMessage={query ? 'No results found' : 'Enter a query to search'}
onCardClick={handleFileClick}
/>
{search.results.length > 0 && (
<Pagination
currentPage={search.pagination.currentPage}
totalPages={search.pagination.totalPages}
total={search.total}
hasPrev={search.pagination.hasPrev}
hasNext={search.pagination.hasNext}
onPrev={handlePrev}
onNext={handleNext}
/>
)}
</>
);
case 'browse':
return (
<>
<TypeTabs currentType={type} onTypeChange={handleTypeChange} />
<ResultsList
results={browse.results}
loading={browse.loading}
error={browse.error}
emptyMessage="No documents found"
onCardClick={handleFileClick}
/>
{browse.results.length > 0 && (
<Pagination
currentPage={browse.pagination.currentPage}
totalPages={browse.pagination.totalPages}
total={browse.total}
hasPrev={browse.pagination.hasPrev}
hasNext={browse.pagination.hasNext}
onPrev={handlePrev}
onNext={handleNext}
label="documents"
/>
)}
</>
);
case 'consult':
if (consult.loading) {
return <div className="loading">Consulting Oracle...</div>;
}
if (consult.error) {
return <div className="error">Error: {consult.error}</div>;
}
if (consult.data) {
return <GuidanceBox data={consult.data} />;
}
return <div className="loading">Enter a decision query and click Consult</div>;
case 'reflect':
if (reflect.loading) {
return <div className="loading">Reflecting...</div>;
}
if (reflect.error) {
return <div className="error">Error: {reflect.error}</div>;
}
if (reflect.data) {
return (
<div className="results">
<ResultCard
result={{ ...reflect.data, source: undefined }}
onClick={handleFileClick}
highlighted
/>
</div>
);
}
return <div className="loading">Click Reflect for random wisdom</div>;
case 'graph':
return <Graph />;
default:
return <div className="loading">Select a mode to get started</div>;
}
};
return (
<div className="container">
<Header
config={config}
stats={stats}
loading={statsLoading}
onTogglePersona={toggle}
onNavigateToArthur={navigateToArthur}
/>
<SearchBox
query={query}
mode={mode}
onQueryChange={setQuery}
onSearch={handleSearch}
onBrowse={handleBrowse}
onConsult={handleConsult}
onReflect={handleReflect}
onGraph={handleGraph}
onLearn={() => setLearnModalOpen(true)}
/>
{renderContent()}
<footer>
<span>{config.title} - "{config.tagline}"</span>
</footer>
<LearnModal
isOpen={learnModalOpen}
onClose={() => setLearnModalOpen(false)}
onSuccess={handleLearnSuccess}
/>
<FileModal path={fileModalPath} onClose={() => setFileModalPath(null)} />
</div>
);
}
export default App;