Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions client/src/components/ai-search/SearchBar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from "react";

const SearchBar = ({ query, setQuery, onSearch, loading, onClear }) => {
return (
<div className="w-full bg-white p-6 md:p-8 rounded-2xl shadow-xl border border-gray-100">
<div className="flex items-center gap-3 mb-5">
<input
type="text"
placeholder="Ask e.g. 'What decisions were made in the finance meeting?'"
className="flex-grow px-5 py-3 text-sm md:text-base border border-gray-200 rounded-full shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && onSearch()}
/>
{query && (
<button
onClick={onClear}
className="px-4 py-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-full transition"
title="Clear search"
>
</button>
)}
<button
onClick={onSearch}
disabled={loading}
className="px-6 py-2 md:px-8 md:py-3 bg-blue-600 text-white font-semibold rounded-full hover:bg-blue-700 transition disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{loading ? (
<span className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
Searching...
</span>
) : (
"Search"
)}
</button>
</div>
</div>
);
};

export default SearchBar;
33 changes: 33 additions & 0 deletions client/src/components/ai-search/SearchEmptyState.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from "react";

const SearchEmptyState = ({ hasSearched }) => {
if (hasSearched) {
return (
<div className="text-center py-16">
<div className="text-6xl mb-4">🔍</div>
<h3 className="text-xl font-semibold text-gray-700 mb-2">
No results found
</h3>
<p className="text-gray-500 max-w-md mx-auto">
We couldn't find any matches for your search. Try different keywords
or adjust your filters.
</p>
</div>
);
}

return (
<div className="text-center py-16">
<div className="text-6xl mb-4">💬</div>
<h3 className="text-xl font-semibold text-gray-700 mb-2">
Start searching
</h3>
<p className="text-gray-500 max-w-md mx-auto">
Type your question above to explore your meetings, policies, and AI
summaries using natural language.
</p>
</div>
);
};

export default SearchEmptyState;
68 changes: 68 additions & 0 deletions client/src/components/ai-search/SearchFilters.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from "react";

const SearchFilters = ({ filters, setFilters, resultCount }) => {
const handleFilterChange = (key, value) => {
setFilters((prev) => ({ ...prev, [key]: value }));
};

return (
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-100 mb-6">
<div className="flex flex-wrap gap-4 items-center">
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-gray-700">Type:</label>
<select
value={filters.resultType}
onChange={(e) => handleFilterChange("resultType", e.target.value)}
className="px-3 py-2 text-sm border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none"
>
<option value="all">All Types</option>
<option value="meeting">Meetings</option>
<option value="policy">Policies</option>
<option value="summary">AI Summaries</option>
</select>
</div>

<div className="flex items-center gap-2">
<label className="text-sm font-medium text-gray-700">From:</label>
<input
type="date"
value={filters.dateFrom || ""}
onChange={(e) => handleFilterChange("dateFrom", e.target.value)}
className="px-3 py-2 text-sm border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none"
/>
</div>

<div className="flex items-center gap-2">
<label className="text-sm font-medium text-gray-700">To:</label>
<input
type="date"
value={filters.dateTo || ""}
onChange={(e) => handleFilterChange("dateTo", e.target.value)}
className="px-3 py-2 text-sm border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none"
/>
</div>

<div className="flex items-center gap-2">
<label className="text-sm font-medium text-gray-700">Sort:</label>
<select
value={filters.sortBy}
onChange={(e) => handleFilterChange("sortBy", e.target.value)}
className="px-3 py-2 text-sm border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none"
>
<option value="relevance">Relevance</option>
<option value="date-desc">Newest First</option>
<option value="date-asc">Oldest First</option>
</select>
</div>

{resultCount > 0 && (
<div className="ml-auto text-sm text-gray-500">
{resultCount} result{resultCount !== 1 ? "s" : ""}
</div>
)}
</div>
</div>
);
};

export default SearchFilters;
132 changes: 132 additions & 0 deletions client/src/components/ai-search/SearchResultCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import React from "react";

const SearchResultCard = ({
result,
onViewDetails,
onOpenMeeting,
onCopySummary,
}) => {
const getResultTypeLabel = (type) => {
switch (type) {
case "meeting":
return "Meeting";
case "policy":
return "Policy";
case "summary":
return "AI Summary";
default:
return "Meeting";
}
};

const getResultTypeColor = (type) => {
switch (type) {
case "meeting":
return "bg-blue-100 text-blue-700";
case "policy":
return "bg-purple-100 text-purple-700";
case "summary":
return "bg-green-100 text-green-700";
default:
return "bg-blue-100 text-blue-700";
}
};

const formatDate = (date) => {
if (!date) return "Unknown date";
return new Date(date).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
});
};

const getSimilarityColor = (score) => {
if (score >= 0.8) return "text-green-600";
if (score >= 0.6) return "text-blue-600";
if (score >= 0.4) return "text-yellow-600";
return "text-gray-600";
};

return (
<div className="group p-6 bg-white rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-all hover:-translate-y-0.5">
<div className="flex justify-between items-start mb-3">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<span
className={`text-xs px-2 py-1 rounded-full font-medium ${getResultTypeColor(result.resultType)}`}
>
{getResultTypeLabel(result.resultType)}
</span>
{result.similarityScore && (
<span
className={`text-xs font-medium ${getSimilarityColor(result.similarityScore)}`}
>
{Math.round(result.similarityScore * 100)}% match
</span>
)}
</div>
<h3
className="text-lg font-semibold text-gray-900 group-hover:text-blue-600 transition cursor-pointer"
onClick={() => onViewDetails(result)}
>
{result.title || "Untitled Meeting"}
</h3>
</div>
</div>

<p className="text-gray-700 text-sm mt-2 leading-relaxed line-clamp-3">
{result.summary ||
result.transcript?.slice(0, 200) ||
"No summary available."}
</p>

<div className="flex flex-wrap gap-2 mt-3">
{result.tags &&
result.tags.length > 0 &&
result.tags.slice(0, 3).map((tag) => (
<span
key={tag}
className="bg-gray-100 text-gray-600 px-2 py-1 rounded text-xs"
>
#{tag}
</span>
))}
</div>

<div className="flex justify-between items-center mt-4 pt-4 border-t border-gray-100">
<div className="text-xs text-gray-500">
📅 {formatDate(result.createdAt)}
{result.organization && (
<span className="ml-3">🏢 {result.organization}</span>
)}
</div>
<div className="flex gap-2">
<button
onClick={() => onViewDetails(result)}
className="text-sm text-blue-600 font-medium hover:underline"
>
View Details
</button>
{result.resultType === "meeting" && (
<button
onClick={() => onOpenMeeting(result)}
className="text-sm text-gray-600 hover:text-gray-800"
>
Open Meeting
</button>
)}
<button
onClick={() => onCopySummary(result)}
className="text-sm text-gray-600 hover:text-gray-800"
title="Copy summary"
>
📋
</button>
</div>
</div>
</div>
);
};

export default SearchResultCard;
31 changes: 31 additions & 0 deletions client/src/components/ai-search/SearchSkeleton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from "react";

const SearchSkeleton = () => {
return (
<div className="space-y-5">
{[1, 2, 3].map((i) => (
<div
key={i}
className="p-6 bg-white rounded-xl shadow-sm border border-gray-100"
>
<div className="flex items-center gap-2 mb-3">
<div className="h-5 w-16 bg-gray-200 rounded-full animate-pulse"></div>
<div className="h-5 w-12 bg-gray-200 rounded-full animate-pulse"></div>
</div>
<div className="h-6 w-3/4 bg-gray-200 rounded animate-pulse mb-3"></div>
<div className="space-y-2">
<div className="h-4 w-full bg-gray-200 rounded animate-pulse"></div>
<div className="h-4 w-full bg-gray-200 rounded animate-pulse"></div>
<div className="h-4 w-2/3 bg-gray-200 rounded animate-pulse"></div>
</div>
<div className="flex justify-between items-center mt-4 pt-4 border-t border-gray-100">
<div className="h-4 w-24 bg-gray-200 rounded animate-pulse"></div>
<div className="h-4 w-20 bg-gray-200 rounded animate-pulse"></div>
</div>
</div>
))}
</div>
);
};

export default SearchSkeleton;
Loading
Loading