Add command palette with smart usage tracking and content search

- Implement keyboard shortcut (Cmd/Ctrl+K) to open command palette
- Add smart ranking algorithm (70% frequency + 30% recency)
- Track both route navigation and content (movies/shows) usage
- Support parameter input for dynamic routes (e.g., /movie/:id)
- Add query parameter support for search routes
- Integrate ElasticSearch fallback for content search
- Include rate limiting and error handling for API calls
- Store usage data in localStorage (commandPalette_stats)
- Auto-scroll selected items into view with keyboard navigation
This commit is contained in:
2026-02-27 18:43:38 +01:00
parent c390fcba47
commit 5bcdcd6568
4 changed files with 948 additions and 3 deletions

View File

@@ -0,0 +1,103 @@
interface CommandData {
count: number;
lastUsed: string; // ISO timestamp
routePath?: string;
type: "route" | "content";
}
interface CommandStats {
commands: {
[key: string]: CommandData;
};
version: number;
}
const STORAGE_KEY = "commandPalette_stats";
const CURRENT_VERSION = 1;
function getStats(): CommandStats {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (!stored) {
return { commands: {}, version: CURRENT_VERSION };
}
const parsed = JSON.parse(stored) as CommandStats;
return parsed;
} catch (error) {
console.error("Failed to parse command stats:", error);
return { commands: {}, version: CURRENT_VERSION };
}
}
function saveStats(stats: CommandStats): void {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(stats));
} catch (error) {
console.error("Failed to save command stats:", error);
}
}
export function trackCommand(
id: string,
type: "route" | "content",
metadata?: { routePath?: string }
): void {
const stats = getStats();
if (!stats.commands[id]) {
stats.commands[id] = {
count: 0,
lastUsed: new Date().toISOString(),
type,
routePath: metadata?.routePath
};
}
stats.commands[id].count++;
stats.commands[id].lastUsed = new Date().toISOString();
if (metadata?.routePath) {
stats.commands[id].routePath = metadata.routePath;
}
saveStats(stats);
}
export function getCommandScore(commandId: string): number {
const stats = getStats();
const command = stats.commands[commandId];
if (!command) return 0;
const now = new Date().getTime();
const lastUsed = new Date(command.lastUsed).getTime();
const daysSinceLastUse = (now - lastUsed) / (1000 * 60 * 60 * 24);
// Recency bonus: 10 points for today, decreasing to 0 after 10 days
const recencyBonus = Math.max(0, 10 - daysSinceLastUse);
// Combined score: 70% frequency, 30% recency
return command.count * 0.7 + recencyBonus * 0.3;
}
export function getTopCommands(
limit = 10
): Array<{ id: string; score: number }> {
const stats = getStats();
const scored = Object.keys(stats.commands).map(id => ({
id,
score: getCommandScore(id)
}));
return scored.sort((a, b) => b.score - a.score).slice(0, limit);
}
export function clearCommandHistory(): void {
localStorage.removeItem(STORAGE_KEY);
}
export function getCommandStats(commandId: string): CommandData | null {
const stats = getStats();
return stats.commands[commandId] || null;
}