feat: working serach now

This commit is contained in:
Mann Patel
2026-02-15 06:27:50 -07:00
parent 2ec7c53302
commit 409f45c05f
5 changed files with 301 additions and 25 deletions

View File

@@ -5,6 +5,7 @@ import Feed from './pages/Feed'
import CreatePost from './pages/CreatePost' import CreatePost from './pages/CreatePost'
import History from './pages/History' import History from './pages/History'
import Settings from './pages/Settings' import Settings from './pages/Settings'
import Search from './pages/Search'
import { api } from './api' import { api } from './api'
export default function App() { export default function App() {
@@ -16,6 +17,7 @@ export default function App() {
const [isRegistering, setIsRegistering] = useState(false) const [isRegistering, setIsRegistering] = useState(false)
const [loginError, setLoginError] = useState(null) const [loginError, setLoginError] = useState(null)
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [headerSearchQuery, setHeaderSearchQuery] = useState('')
// Check for saved user on mount // Check for saved user on mount
useEffect(() => { useEffect(() => {
@@ -73,6 +75,11 @@ export default function App() {
} }
} }
const handleNavigateToSearch = (query) => {
setActiveTab('search')
setHeaderSearchQuery(query)
}
const handlePostCreated = () => { const handlePostCreated = () => {
// Switch to feed after creating a post // Switch to feed after creating a post
setActiveTab('feed') setActiveTab('feed')
@@ -90,6 +97,8 @@ export default function App() {
switch (activeTab) { switch (activeTab) {
case 'create': case 'create':
return <CreatePost user={user} onPostCreated={handlePostCreated} /> return <CreatePost user={user} onPostCreated={handlePostCreated} />
case 'search':
return <Search user={user} initialQuery={headerSearchQuery} />
case 'history': case 'history':
return <History user={user} /> return <History user={user} />
case 'settings': case 'settings':
@@ -178,10 +187,10 @@ export default function App() {
// Main App // Main App
return ( return (
<div className="h-screen bg-gray-50 text-gray-800 flex flex-col overflow-hidden"> <div className="h-screen bg-gray-50 text-gray-800 flex flex-col overflow-hidden">
<Header user={user} onSearch={handleSearch} onLogout={handleLogout} /> <Header onSearch={handleSearch} onLogout={handleLogout} onNavigateToSearch={handleNavigateToSearch} />
<div className="flex-1 flex overflow-hidden max-w-[1400px] mx-auto w-full"> <div className="flex-1 flex overflow-hidden max-w-[1400px] mx-auto w-full">
<Sidebar activeTab={activeTab} onTabChange={setActiveTab} /> <Sidebar user={user} activeTab={activeTab} onTabChange={setActiveTab} />
<main className="flex-1 overflow-y-auto p-6"> <main className="flex-1 overflow-y-auto p-6">
{renderPage()} {renderPage()}

View File

@@ -178,6 +178,12 @@ export default function AudioPostCard({ post }) {
}`}> }`}>
{post.status} {post.status}
</span> </span>
<span></span>
{post.language && (
<span className="text-xs px-2 py-0.5 bg-blue-100 text-blue-700 rounded">
{post.language.toUpperCase()}
</span>
)}
</div> </div>
</div> </div>
{post.status === 'ready' && ( {post.status === 'ready' && (
@@ -205,6 +211,8 @@ export default function AudioPostCard({ post }) {
</button> </button>
</div> </div>
{/* Description */} {/* Description */}
{post.description && ( {post.description && (
<p className="text-sm text-gray-700 mb-4 line-clamp-2"> <p className="text-sm text-gray-700 mb-4 line-clamp-2">
@@ -212,6 +220,7 @@ export default function AudioPostCard({ post }) {
</p> </p>
)} )}
{/* Audio Player - Only show if ready */} {/* Audio Player - Only show if ready */}
{post.status === 'ready' && ( {post.status === 'ready' && (
<> <>
@@ -325,15 +334,6 @@ export default function AudioPostCard({ post }) {
<p className="text-sm text-red-800">Failed to process this recording. Please try uploading again.</p> <p className="text-sm text-red-800">Failed to process this recording. Please try uploading again.</p>
</div> </div>
)} )}
{/* Language Tag */}
{post.language && (
<div className="mt-3">
<span className="text-xs px-2 py-1 bg-blue-100 text-blue-700 rounded">
{post.language.toUpperCase()}
</span>
</div>
)}
</div> </div>
</article> </article>
) )

View File

@@ -1,13 +1,21 @@
import { Search, LogOut } from 'lucide-react' import { Search, LogOut } from 'lucide-react'
import { useState } from 'react' import { useState } from 'react'
export default function Header({ onSearch, onLogout }) { export default function Header({ onSearch, onLogout, onNavigateToSearch }) {
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const handleSearch = (e) => { const handleSearch = (e) => {
const query = e.target.value e.preventDefault()
setSearchQuery(query) if (searchQuery.trim()) {
onSearch?.(query) onNavigateToSearch?.(searchQuery.trim())
onSearch?.(searchQuery.trim())
}
}
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
handleSearch(e)
}
} }
return ( return (
@@ -24,18 +32,19 @@ export default function Header({ onSearch, onLogout }) {
</div> </div>
{/* Center: Search Bar */} {/* Center: Search Bar */}
<div className="flex-1 max-w-2xl"> <form onSubmit={handleSearch} className="flex-1 max-w-2xl">
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" size={18} /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" size={18} />
<input <input
type="text" type="text"
placeholder="Search your archives..." placeholder="Search your archives..."
value={searchQuery} value={searchQuery}
onChange={handleSearch} onChange={(e) => setSearchQuery(e.target.value)}
onKeyPress={handleKeyPress}
className="w-full bg-gray-50 border border-gray-300 rounded-lg pl-10 pr-4 py-2 text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#f4b840] focus:border-transparent" className="w-full bg-gray-50 border border-gray-300 rounded-lg pl-10 pr-4 py-2 text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#f4b840] focus:border-transparent"
/> />
</div> </div>
</div> </form>
{/* Right: Logout */} {/* Right: Logout */}
<div className="flex items-center gap-3 flex-shrink-0"> <div className="flex items-center gap-3 flex-shrink-0">

View File

@@ -1,9 +1,10 @@
import { Plus, Home, History, Settings } from 'lucide-react' import { Plus, Home, History, Settings, Search } from 'lucide-react'
export default function Sidebar({ user, activeTab, onTabChange }) { export default function Sidebar({ user, activeTab, onTabChange }) {
const navItems = [ const navItems = [
{ id: 'create', label: 'Make an Archive Post', icon: Plus }, { id: 'create', label: 'Make an Archive Post', icon: Plus },
{ id: 'feed', label: 'My Feed', icon: Home }, { id: 'feed', label: 'My Feed', icon: Home },
{ id: 'search', label: 'Search Archives', icon: Search },
{ id: 'history', label: 'History', icon: History }, { id: 'history', label: 'History', icon: History },
{ id: 'settings', label: 'Settings', icon: Settings } { id: 'settings', label: 'Settings', icon: Settings }
] ]

View File

@@ -0,0 +1,257 @@
import { useState, useEffect } from 'react'
import { Search as SearchIcon, Sparkles, Clock, ExternalLink } from 'lucide-react'
import { api } from '../api'
export default function Search({ user, initialQuery = '' }) {
const [query, setQuery] = useState(initialQuery)
const [results, setResults] = useState([])
const [loading, setLoading] = useState(false)
const [searched, setSearched] = useState(false)
const [error, setError] = useState(null)
// Auto-search if initialQuery is provided
useEffect(() => {
if (initialQuery && initialQuery.trim()) {
setQuery(initialQuery)
performSearch(initialQuery)
}
}, [initialQuery])
const performSearch = async (searchQuery) => {
if (!searchQuery.trim()) {
setError('Please enter a search query')
return
}
if (!user?.user_id) {
setError('You must be logged in to search')
return
}
setLoading(true)
setError(null)
setSearched(true)
try {
const response = await api.searchRAG(searchQuery.trim(), user.user_id, 1, 50)
setResults(response.results || [])
} catch (err) {
setError(err.message || 'Search failed')
setResults([])
} finally {
setLoading(false)
}
}
const handleSearch = async (e) => {
e.preventDefault()
performSearch(query)
}
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins}:${secs.toString().padStart(2, '0')}`
}
const highlightMatch = (text, searchQuery) => {
if (!searchQuery) return text
const regex = new RegExp(`(${searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi')
const parts = text.split(regex)
return parts.map((part, i) =>
regex.test(part) ? (
<mark key={i} className="bg-[#f4b840] text-[#1a1a1a] px-1 rounded">
{part}
</mark>
) : part
)
}
return (
<div className="max-w-4xl mx-auto space-y-6">
{/* Search Header */}
<div className="text-center mb-8">
<div className="inline-flex items-center gap-2 mb-4">
<Sparkles size={32} className="text-[#f4b840]" />
<h1 className="text-3xl font-bold text-gray-900">AI Search</h1>
</div>
<p className="text-gray-600">
Search through all your archived transcripts using natural language
</p>
</div>
{/* Search Bar */}
<form onSubmit={handleSearch} className="relative">
<div className="relative">
<SearchIcon className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400" size={20} />
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search your archives... (e.g., 'WWII stories', 'family traditions')"
className="w-full pl-12 pr-32 py-4 border-2 border-gray-300 rounded-lg text-lg focus:outline-none focus:ring-2 focus:ring-[#f4b840] focus:border-transparent"
disabled={loading}
/>
<button
type="submit"
disabled={loading || !query.trim()}
className="absolute right-2 top-1/2 -translate-y-1/2 px-6 py-2 bg-[#f4b840] hover:bg-[#e5a930] text-[#1a1a1a] rounded font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Searching...' : 'Search'}
</button>
</div>
</form>
{/* Error Message */}
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-sm text-red-800">{error}</p>
</div>
)}
{/* Loading State */}
{loading && (
<div className="flex flex-col items-center justify-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#f4b840] mb-4"></div>
<p className="text-gray-600">Searching through your archives...</p>
</div>
)}
{/* Results */}
{!loading && searched && (
<>
{/* Results Header */}
<div className="flex items-center justify-between py-4 border-b border-gray-200">
<p className="text-sm text-gray-600">
{results.length > 0 ? (
<>
Found <span className="font-semibold text-gray-900">{results.length}</span> result
{results.length !== 1 ? 's' : ''} for "{query}"
</>
) : (
<>No results found for "{query}"</>
)}
</p>
</div>
{/* Results List */}
{results.length > 0 ? (
<div className="space-y-4">
{results.map((result, index) => (
<div
key={`${result.chunk_id}-${index}`}
className="bg-white rounded-lg border border-gray-200 p-6 hover:shadow-md transition-shadow"
>
{/* Post Title */}
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900 mb-1">
{result.audio_posts?.title || 'Untitled Post'}
</h3>
<div className="flex items-center gap-3 text-sm text-gray-500">
<span className="flex items-center gap-1">
<Clock size={14} />
{formatTime(result.start_sec)} - {formatTime(result.end_sec)}
</span>
{result.audio_posts?.created_at && (
<>
<span></span>
<span>
{new Date(result.audio_posts.created_at).toLocaleDateString()}
</span>
</>
)}
{result.confidence && (
<>
<span></span>
<span className="text-green-600">
{Math.round(result.confidence * 100)}% confidence
</span>
</>
)}
</div>
</div>
<a
href={`#post-${result.post_id}`}
className="flex items-center gap-1 text-sm text-[#f4b840] hover:text-[#e5a930]"
>
<span>View Post</span>
<ExternalLink size={14} />
</a>
</div>
{/* Transcript Text with Highlighting */}
<div className="p-4 bg-gray-50 rounded border border-gray-200">
<p className="text-sm text-gray-800 leading-relaxed">
{highlightMatch(result.text, query)}
</p>
</div>
{/* Timestamp Badge */}
<div className="mt-3 flex items-center gap-2">
<span className="text-xs px-2 py-1 bg-blue-100 text-blue-700 rounded">
Segment {index + 1}
</span>
<span className="text-xs text-gray-500">
Duration: {Math.round(result.end_sec - result.start_sec)}s
</span>
</div>
</div>
))}
</div>
) : (
searched && !loading && (
<div className="text-center py-12 bg-white rounded-lg border border-gray-200">
<SearchIcon size={48} className="text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">
No Results Found
</h3>
<p className="text-gray-600 mb-4">
Try different keywords or check your spelling
</p>
<div className="text-sm text-gray-500">
<p className="font-medium mb-2">Search Tips:</p>
<ul className="space-y-1">
<li> Use specific keywords from your archives</li>
<li> Try shorter search phrases</li>
<li> Search for topics, names, or places</li>
</ul>
</div>
</div>
)
)}
</>
)}
{/* Empty State */}
{!searched && !loading && (
<div className="text-center py-12 bg-white rounded-lg border border-gray-200">
<Sparkles size={48} className="text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Start Searching Your Archives
</h3>
<p className="text-gray-600 mb-4">
Enter a query above to search through all your transcribed audio
</p>
<div className="text-sm text-gray-500 space-y-1">
<p className="font-medium mb-2">Example searches:</p>
<div className="inline-flex flex-wrap gap-2 justify-center">
{['WWII', 'family history', 'grandmother', 'traditions', 'childhood'].map((example) => (
<button
key={example}
onClick={() => {
setQuery(example)
}}
className="px-3 py-1 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded text-sm transition-colors"
>
{example}
</button>
))}
</div>
</div>
</div>
)}
</div>
)}