diff --git a/apps/desktop/src/lib/utils.ts b/apps/desktop/src/lib/utils.ts index a5ef193..7d293cb 100644 --- a/apps/desktop/src/lib/utils.ts +++ b/apps/desktop/src/lib/utils.ts @@ -4,3 +4,19 @@ import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +export function formatDate(date: Date): string { + const now = new Date(); + const diffInDays = Math.floor( + (now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24), + ); + + if (diffInDays === 0) return "Today"; + if (diffInDays === 1) return "Yesterday"; + + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: date.getFullYear() !== now.getFullYear() ? "numeric" : undefined, + }); +} diff --git a/apps/desktop/src/main/preload.ts b/apps/desktop/src/main/preload.ts index 31987e0..bdc021d 100644 --- a/apps/desktop/src/main/preload.ts +++ b/apps/desktop/src/main/preload.ts @@ -11,6 +11,9 @@ interface ShortcutData { } const api: ElectronAPI = { + // Platform information + platform: process.platform, + sendAudioChunk: ( chunk: Float32Array, isFinalChunk: boolean = false, diff --git a/apps/desktop/src/renderer/main/components/command-search-button.tsx b/apps/desktop/src/renderer/main/components/command-search-button.tsx new file mode 100644 index 0000000..16d1f56 --- /dev/null +++ b/apps/desktop/src/renderer/main/components/command-search-button.tsx @@ -0,0 +1,169 @@ +"use client"; + +import * as React from "react"; +import { IconSearch } from "@tabler/icons-react"; +import { useNavigate } from "@tanstack/react-router"; +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { Button } from "@/components/ui/button"; +import { api } from "@/trpc/react"; +import { FileTextIcon } from "lucide-react"; +import { formatDate } from "@/lib/utils"; +import { SETTINGS_NAV_ITEMS } from "../lib/settings-navigation"; + +// Detect platform for keyboard shortcuts +const isMac = window.electronAPI.platform === "darwin"; + +export function CommandSearchButton() { + const [open, setOpen] = React.useState(false); + const [search, setSearch] = React.useState(""); + const navigate = useNavigate(); + + // Client-side filtering for settings + const settingsResults = React.useMemo(() => { + const query = search.toLowerCase().trim(); + if (!query) { + return SETTINGS_NAV_ITEMS; + } + return SETTINGS_NAV_ITEMS.filter((page) => { + const searchText = [page.title, page.description].join(" ").toLowerCase(); + return searchText.includes(query); + }); + }, [search]); + + const { data: noteResults = [] } = api.notes.searchNotes.useQuery( + { query: search }, + { + enabled: open, + staleTime: 1000 * 60 * 5, + }, + ); + + const searchResults = React.useMemo(() => { + return [ + ...settingsResults, + ...noteResults.map((n) => ({ + ...n, + url: `/settings/notes/${n.id}`, + description: formatDate(new Date(n.createdAt)), + type: "note" as const, + icon: n.icon || "file-text", + })), + ]; + }, [settingsResults, noteResults]); + + React.useEffect(() => { + const down = (e: KeyboardEvent) => { + if (e.key === "k" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setOpen((open) => !open); + } + }; + + document.addEventListener("keydown", down); + return () => document.removeEventListener("keydown", down); + }, []); + + const shortcutDisplay = isMac ? "⌘ K" : "Ctrl+K"; + + const handleSelect = (url: string) => { + setOpen(false); + setSearch(""); + navigate({ to: url }); + }; + + return ( + <> + + + + + + No results found. + {(() => { + // Separate results by type + const noteResults = searchResults.filter( + (item) => item.type === "note", + ); + const settingsResults = searchResults.filter( + (item) => item.type === "settings", + ); + + return ( + <> + {settingsResults.length > 0 && ( + + {settingsResults.map((page) => ( + handleSelect(page.url)} + className="cursor-pointer" + > + {typeof page.icon === "string" ? ( + {page.icon} + ) : ( + + )} +
+ {page.title} + + {page.description} + +
+
+ ))} +
+ )} + {noteResults.length > 0 && ( + + {noteResults.map((note) => ( + handleSelect(note.url)} + className="cursor-pointer" + > + {note.icon ? ( + + ) : ( + {note.icon} + )} +
+ {note.title} + + {note.description} + +
+
+ ))} +
+ )} + + ); + })()} +
+
+ + ); +} diff --git a/apps/desktop/src/renderer/main/components/settings-sidebar.tsx b/apps/desktop/src/renderer/main/components/settings-sidebar.tsx index 141b152..bbe91a5 100644 --- a/apps/desktop/src/renderer/main/components/settings-sidebar.tsx +++ b/apps/desktop/src/renderer/main/components/settings-sidebar.tsx @@ -1,16 +1,5 @@ import * as React from "react"; -import { - IconSettings, - IconMicrophone, - IconBook, - IconBrain, - IconHistory, - IconInfoCircle, - IconBookFilled, - IconKeyboard, - IconAdjustments, - IconNotes, -} from "@tabler/icons-react"; +import { IconBookFilled } from "@tabler/icons-react"; import { NavMain } from "@/components/nav-main"; import { NavSecondary } from "@/components/nav-secondary"; @@ -23,6 +12,8 @@ import { SidebarMenuButton, SidebarMenuItem, } from "@/components/ui/sidebar"; +import { CommandSearchButton } from "./command-search-button"; +import { SETTINGS_NAV_ITEMS } from "../lib/settings-navigation"; // Custom Discord icon component const DiscordIcon = ({ className }: { className?: string }) => ( @@ -34,53 +25,11 @@ const DiscordIcon = ({ className }: { className?: string }) => ( ); const data = { - navMain: [ - { - title: "Preferences", - url: "/settings/preferences", - icon: IconSettings, - }, - { - title: "Dictation", - url: "/settings/dictation", - icon: IconMicrophone, - }, - { - title: "Shortcuts", - url: "/settings/shortcuts", - icon: IconKeyboard, - }, - { - title: "Notes", - url: "/settings/notes", - icon: IconNotes, - }, - { - title: "Vocabulary", - url: "/settings/vocabulary", - icon: IconBook, - }, - { - title: "AI Models", - url: "/settings/ai-models", - icon: IconBrain, - }, - { - title: "History", - url: "/settings/history", - icon: IconHistory, - }, - { - title: "Advanced", - url: "/settings/advanced", - icon: IconAdjustments, - }, - { - title: "About", - url: "/settings/about", - icon: IconInfoCircle, - }, - ], + navMain: SETTINGS_NAV_ITEMS.map(({ title, url, icon }) => ({ + title, + url, + icon: typeof icon === "string" ? undefined : icon, + })), navSecondary: [ { title: "Docs", @@ -120,6 +69,9 @@ export function SettingsSidebar({ + + + diff --git a/apps/desktop/src/renderer/main/lib/settings-navigation.ts b/apps/desktop/src/renderer/main/lib/settings-navigation.ts new file mode 100644 index 0000000..e36f418 --- /dev/null +++ b/apps/desktop/src/renderer/main/lib/settings-navigation.ts @@ -0,0 +1,86 @@ +import { + IconSettings, + IconMicrophone, + IconBook, + IconBrain, + IconHistory, + IconInfoCircle, + IconKeyboard, + IconAdjustments, + IconNotes, + type Icon, +} from "@tabler/icons-react"; + +export interface SettingsNavItem { + title: string; + url: string; + description: string; + icon: Icon | string; + type: "settings"; +} + +export const SETTINGS_NAV_ITEMS: SettingsNavItem[] = [ + { + title: "Preferences", + url: "/settings/preferences", + description: "Configure general application preferences and behavior", + icon: IconSettings, + type: "settings", + }, + { + title: "Dictation", + url: "/settings/dictation", + description: "Configure speech recognition and dictation settings", + icon: IconMicrophone, + type: "settings", + }, + { + title: "Shortcuts", + url: "/settings/shortcuts", + description: "Customize keyboard shortcuts and hotkeys", + icon: IconKeyboard, + type: "settings", + }, + { + title: "Notes", + url: "/settings/notes", + description: "Manage your notes", + icon: IconNotes, + type: "settings", + }, + { + title: "Vocabulary", + url: "/settings/vocabulary", + description: "Manage custom vocabulary and word recognition", + icon: IconBook, + type: "settings", + }, + { + title: "AI Models", + url: "/settings/ai-models", + description: "Configure AI models and providers", + icon: IconBrain, + type: "settings", + }, + { + title: "History", + url: "/settings/history", + description: "View and manage transcription history", + icon: IconHistory, + type: "settings", + }, + { + title: "Advanced", + url: "/settings/advanced", + description: "Advanced configuration options", + icon: IconAdjustments, + type: "settings", + }, + { + title: "About", + url: "/settings/about", + description: "About Amical and version information", + icon: IconInfoCircle, + type: "settings", + }, +]; diff --git a/apps/desktop/src/renderer/main/pages/notes/components/note-card.tsx b/apps/desktop/src/renderer/main/pages/notes/components/note-card.tsx index 9367dc9..c698f3d 100644 --- a/apps/desktop/src/renderer/main/pages/notes/components/note-card.tsx +++ b/apps/desktop/src/renderer/main/pages/notes/components/note-card.tsx @@ -1,7 +1,7 @@ "use client"; import { FileText, Calendar } from "lucide-react"; -import { cn } from "@/lib/utils"; +import { cn, formatDate } from "@/lib/utils"; import { Note } from "../types"; interface RecentNoteCardProps { @@ -9,22 +9,6 @@ interface RecentNoteCardProps { onNoteClick: (noteId: number) => void; } -function formatDate(date: Date): string { - const now = new Date(); - const diffInDays = Math.floor( - (now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24), - ); - - if (diffInDays === 0) return "Today"; - if (diffInDays === 1) return "Yesterday"; - - return date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: date.getFullYear() !== now.getFullYear() ? "numeric" : undefined, - }); -} - export function NoteCard({ note, onNoteClick }: RecentNoteCardProps) { return (
{ + const notes = await notesService.listNotes({ + search: input.query || "", + limit: input.limit, + }); + return notes.map((note) => ({ + id: note.id, + title: note.title, + createdAt: note.createdAt, + icon: note.icon || "file-text", + })); + }), }); diff --git a/apps/desktop/src/types/electron-api.ts b/apps/desktop/src/types/electron-api.ts index 2a1a127..ece6dd8 100644 --- a/apps/desktop/src/types/electron-api.ts +++ b/apps/desktop/src/types/electron-api.ts @@ -5,6 +5,9 @@ declare global { } export interface ElectronAPI { + // Platform information + platform: NodeJS.Platform; + // Listeners remain the same (two-way to renderer) onGlobalShortcut: ( callback: (data: { shortcut: string }) => void,