feat: working serach now
This commit is contained in:
@@ -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()}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 }
|
||||||
]
|
]
|
||||||
|
|||||||
257
frontend/src/pages/Search.jsx
Normal file
257
frontend/src/pages/Search.jsx
Normal 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>
|
||||||
|
)}
|
||||||
Reference in New Issue
Block a user