feat: Make semantic search optional and disabled by default

- Add SemanticSearchConfig interface with enabled toggle to siteConfig.ts
- Default semantic search to disabled (enabled: false) to avoid blocking forks without OPENAI_API_KEY
- Update SearchModal.tsx to conditionally show mode toggle based on config
- Update sync-posts.ts to skip embedding generation when disabled
- Add semantic search toggle to Dashboard config generator
- Update FORK_CONFIG.md with Semantic Search Configuration section
- Update fork-config.json.example with semanticSearch option
- Update docs-semantic-search.md with enable/disable instructions
- Update changelog and documentation

When disabled (default):
- Search modal shows only keyword search (no mode toggle)
- Embedding generation skipped during sync
- No OpenAI API key required

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Wayne Sutton
2026-01-05 22:22:50 -08:00
parent 5a8df46681
commit 3c9feb071b
15 changed files with 239 additions and 49 deletions

View File

@@ -11,6 +11,7 @@ import {
TextAa,
Brain,
} from "@phosphor-icons/react";
import { siteConfig } from "../config/siteConfig";
interface SearchModalProps {
isOpen: boolean;
@@ -31,6 +32,9 @@ interface SearchResult {
}
export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
// Check if semantic search is enabled in siteConfig
const semanticEnabled = siteConfig.semanticSearch?.enabled !== false;
const [searchQuery, setSearchQuery] = useState("");
const [selectedIndex, setSelectedIndex] = useState(0);
const [searchMode, setSearchMode] = useState<SearchMode>("keyword");
@@ -100,8 +104,8 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
// Handle keyboard navigation
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
// Tab toggles between search modes
if (e.key === "Tab") {
// Tab toggles between search modes (only if semantic search is enabled)
if (e.key === "Tab" && semanticEnabled) {
e.preventDefault();
setSearchMode((prev) => (prev === "keyword" ? "semantic" : "keyword"));
return;
@@ -142,7 +146,7 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
break;
}
},
[results, selectedIndex, navigate, onClose, searchMode, searchQuery]
[results, selectedIndex, navigate, onClose, searchMode, searchQuery, semanticEnabled]
);
// Handle clicking on a result
@@ -168,25 +172,27 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
return (
<div className="search-modal-backdrop" onClick={handleBackdropClick}>
<div className="search-modal">
{/* Search mode toggle */}
<div className="search-mode-toggle">
<button
className={`search-mode-btn ${searchMode === "keyword" ? "active" : ""}`}
onClick={() => setSearchMode("keyword")}
title="Keyword search - matches exact words"
>
<TextAa size={16} weight="bold" />
<span>Keyword</span>
</button>
<button
className={`search-mode-btn ${searchMode === "semantic" ? "active" : ""}`}
onClick={() => setSearchMode("semantic")}
title="Semantic search - finds similar meaning"
>
<Brain size={16} weight="bold" />
<span>Semantic</span>
</button>
</div>
{/* Search mode toggle - only shown when semantic search is enabled */}
{semanticEnabled && (
<div className="search-mode-toggle">
<button
className={`search-mode-btn ${searchMode === "keyword" ? "active" : ""}`}
onClick={() => setSearchMode("keyword")}
title="Keyword search - matches exact words"
>
<TextAa size={16} weight="bold" />
<span>Keyword</span>
</button>
<button
className={`search-mode-btn ${searchMode === "semantic" ? "active" : ""}`}
onClick={() => setSearchMode("semantic")}
title="Semantic search - finds similar meaning"
>
<Brain size={16} weight="bold" />
<span>Semantic</span>
</button>
</div>
)}
{/* Search input */}
<div className="search-modal-input-wrapper">
@@ -223,9 +229,11 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
: "Describe what you're looking for"}
</p>
<div className="search-modal-shortcuts">
<span className="search-shortcut">
<kbd>Tab</kbd> Switch mode
</span>
{semanticEnabled && (
<span className="search-shortcut">
<kbd>Tab</kbd> Switch mode
</span>
)}
<span className="search-shortcut">
<kbd></kbd>
<kbd></kbd> Navigate
@@ -292,9 +300,11 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
{/* Footer with keyboard hints */}
{results && results.length > 0 && (
<div className="search-modal-footer">
<span className="search-footer-hint">
<kbd>Tab</kbd> switch mode
</span>
{semanticEnabled && (
<span className="search-footer-hint">
<kbd>Tab</kbd> switch mode
</span>
)}
<span className="search-footer-hint">
<kbd></kbd> select
</span>

View File

@@ -236,6 +236,13 @@ export interface ImageLightboxConfig {
enabled: boolean; // Global toggle for image lightbox feature
}
// Semantic search configuration
// Enables AI-powered search using vector embeddings
// Requires OPENAI_API_KEY environment variable in Convex dashboard
export interface SemanticSearchConfig {
enabled: boolean; // Global toggle for semantic search feature
}
// Social link configuration for social footer
export interface SocialLink {
platform:
@@ -365,6 +372,9 @@ export interface SiteConfig {
// AI Dashboard configuration (optional)
aiDashboard?: AIDashboardConfig;
// Semantic search configuration (optional)
semanticSearch?: SemanticSearchConfig;
}
// Default site configuration
@@ -744,6 +754,13 @@ export const siteConfig: SiteConfig = {
},
],
},
// Semantic search configuration
// Set enabled: true to enable semantic search (requires OPENAI_API_KEY in Convex)
// When disabled, only keyword search is available (no API key needed)
semanticSearch: {
enabled: false, // Set to true to enable semantic search (requires OPENAI_API_KEY)
},
};
// Export the config as default for easy importing

View File

@@ -4597,6 +4597,8 @@ function ConfigSection({
mcpServerRequireAuth: siteConfig.mcpServer?.requireAuth || false,
// Image lightbox
imageLightboxEnabled: siteConfig.imageLightbox?.enabled !== false,
// Semantic search
semanticSearchEnabled: siteConfig.semanticSearch?.enabled || false,
});
const [copied, setCopied] = useState(false);
@@ -4762,6 +4764,12 @@ export const siteConfig: SiteConfig = {
imageLightbox: {
enabled: ${config.imageLightboxEnabled},
},
// Semantic search configuration
// Set enabled: true to enable AI-powered semantic search (requires OPENAI_API_KEY in Convex)
semanticSearch: {
enabled: ${config.semanticSearchEnabled},
},
};
export default siteConfig;
@@ -5573,6 +5581,26 @@ export default siteConfig;
</div>
</div>
{/* Semantic Search */}
<div className="dashboard-config-card">
<h3>Semantic Search</h3>
<div className="config-field checkbox">
<label>
<input
type="checkbox"
checked={config.semanticSearchEnabled}
onChange={(e) =>
handleChange("semanticSearchEnabled", e.target.checked)
}
/>
<span>Enable semantic search (requires OPENAI_API_KEY in Convex)</span>
</label>
</div>
<p className="config-hint">
When enabled, search modal shows both Keyword and Semantic modes. Requires OpenAI API key for embeddings.
</p>
</div>
{/* Links */}
<div className="dashboard-config-card">
<h3>External Links</h3>