diff --git a/apps/desktop/src/components/main/body/index.tsx b/apps/desktop/src/components/main/body/index.tsx index e94f283de4..52cfd2b3c7 100644 --- a/apps/desktop/src/components/main/body/index.tsx +++ b/apps/desktop/src/components/main/body/index.tsx @@ -1,6 +1,3 @@ -import { Button } from "@hypr/ui/components/ui/button"; -import { cn } from "@hypr/utils"; - import { useRouteContext } from "@tanstack/react-router"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; import { ArrowLeftIcon, ArrowRightIcon, PanelLeftOpenIcon, PlusIcon } from "lucide-react"; @@ -8,6 +5,8 @@ import { Reorder } from "motion/react"; import { useCallback, useEffect, useRef } from "react"; import { useHotkeys } from "react-hotkeys-hook"; +import { Button } from "@hypr/ui/components/ui/button"; +import { cn } from "@hypr/utils"; import { useShell } from "../../../contexts/shell"; import { type Tab, uniqueIdfromTab, useTabs } from "../../../store/zustand/tabs"; import { id } from "../../../utils"; @@ -23,10 +22,6 @@ import { TabContentNote, TabItemNote } from "./sessions"; export function Body() { const { tabs, currentTab } = useTabs(); - useTabCloseHotkey(); - useTabSelectHotkeys(); - useNewTabHotkeys(); - if (!currentTab) { return null; } @@ -42,25 +37,13 @@ export function Body() { } function Header({ tabs }: { tabs: Tab[] }) { - const { persistedStore, internalStore } = useRouteContext({ from: "__root__" }); - const { leftsidebar } = useShell(); - const { select, close, reorder, openNew, goBack, goNext, canGoBack, canGoNext, closeOthers, closeAll } = useTabs(); + const { select, close, reorder, goBack, goNext, canGoBack, canGoNext, closeOthers, closeAll } = useTabs(); const tabsScrollContainerRef = useRef(null); - const setTabRef = useScrollActiveTabIntoView(tabs); + const handleNewNote = useNewTab(); - const handleNewNote = useCallback(() => { - const sessionId = id(); - const user_id = internalStore?.getValue("user_id"); - - persistedStore?.setRow("sessions", sessionId, { user_id, created_at: new Date().toISOString(), title: "" }); - openNew({ - type: "sessions", - id: sessionId, - active: true, - state: { editor: "raw" }, - }); - }, [persistedStore, internalStore, openNew]); + const setTabRef = useScrollActiveTabIntoView(tabs); + useTabsShortcuts(); return (
{!leftsidebar.expanded && ( )} @@ -288,81 +269,6 @@ export function StandardTabWrapper( ); } -const useTabCloseHotkey = () => { - const { tabs, currentTab, close } = useTabs(); - - useHotkeys( - "mod+w", - async (e) => { - e.preventDefault(); - - if (currentTab && tabs.length > 1) { - close(currentTab); - } else { - const appWindow = getCurrentWebviewWindow(); - await appWindow.close(); - } - }, - { enableOnFormTags: true, enableOnContentEditable: true }, - [tabs, currentTab, close], - ); -}; - -const useTabSelectHotkeys = () => { - const { tabs, select } = useTabs(); - - useHotkeys( - ["mod+1", "mod+2", "mod+3", "mod+4", "mod+5", "mod+6", "mod+7", "mod+8", "mod+9"], - (event) => { - const key = event.key; - - const targetIndex = key === "9" - ? tabs.length - 1 - : Number.parseInt(key, 10) - 1; - - const target = tabs[targetIndex]; - if (!target) { - return; - } - - event.preventDefault(); - select(target); - }, - { enableOnFormTags: true, enableOnContentEditable: true }, - [tabs, select], - ); -}; - -const useNewTabHotkeys = () => { - const { persistedStore, internalStore } = useRouteContext({ from: "__root__" }); - const { currentTab, close, openNew } = useTabs(); - - useHotkeys( - ["mod+n", "mod+t"], - (e) => { - e.preventDefault(); - - const sessionId = id(); - const user_id = internalStore?.getValue("user_id"); - - persistedStore?.setRow("sessions", sessionId, { user_id, created_at: new Date().toISOString() }); - - if (e.key === "n" && currentTab) { - close(currentTab); - } - - openNew({ - type: "sessions", - id: sessionId, - active: true, - state: { editor: "raw" }, - }); - }, - { enableOnFormTags: true, enableOnContentEditable: true }, - [persistedStore, internalStore, currentTab, close, openNew], - ); -}; - function useScrollActiveTabIntoView(tabs: Tab[]) { const tabRefsMap = useRef>(new Map()); @@ -391,3 +297,82 @@ function useScrollActiveTabIntoView(tabs: Tab[]) { return setTabRef; } + +function useTabsShortcuts() { + const { tabs, currentTab, close, select } = useTabs(); + const newTab = useNewTab(); + + useHotkeys( + "mod+n", + () => { + if (currentTab) { + close(currentTab); + } + newTab(); + }, + { preventDefault: true }, + [currentTab, close, newTab], + ); + + useHotkeys( + "mod+t", + () => newTab(), + { preventDefault: true }, + [newTab], + ); + + useHotkeys( + "mod+w", + async () => { + if (currentTab && tabs.length > 1) { + close(currentTab); + } else { + const appWindow = getCurrentWebviewWindow(); + await appWindow.close(); + } + }, + { preventDefault: true }, + [tabs, currentTab, close], + ); + + useHotkeys( + "mod+1, mod+2, mod+3, mod+4, mod+5, mod+6, mod+7, mod+8, mod+9", + (event) => { + const key = event.key; + const targetIndex = key === "9" ? tabs.length - 1 : Number.parseInt(key, 10) - 1; + const target = tabs[targetIndex]; + if (target) { + select(target); + } + }, + { preventDefault: true }, + [tabs, select], + ); + + return {}; +} + +function useNewTab() { + const { persistedStore, internalStore } = useRouteContext({ from: "__root__" }); + const { openNew } = useTabs(); + + const handler = useCallback(() => { + const user_id = internalStore?.getValue("user_id"); + const sessionId = id(); + + persistedStore?.setRow("sessions", sessionId, { + user_id, + created_at: new Date().toISOString(), + title: "", + }); + + openNew({ + type: "sessions", + id: sessionId, + active: true, + state: { editor: "raw" }, + }); + }, [persistedStore, internalStore, openNew]); + + return handler; +} diff --git a/apps/desktop/src/components/main/body/search.tsx b/apps/desktop/src/components/main/body/search.tsx index 7d4e0438b0..664ab2352c 100644 --- a/apps/desktop/src/components/main/body/search.tsx +++ b/apps/desktop/src/components/main/body/search.tsx @@ -1,65 +1,15 @@ import { Loader2Icon, SearchIcon, XIcon } from "lucide-react"; -import { useRef, useState } from "react"; -import { useHotkeys } from "react-hotkeys-hook"; +import { useState } from "react"; import { cn } from "@hypr/utils"; import { useSearch } from "../../../contexts/search/ui"; export function Search() { - const { query, setQuery, isSearching, isIndexing, onFocus, onBlur } = useSearch(); - const inputRef = useRef(null); + const { query, setQuery, isSearching, isIndexing, inputRef } = useSearch(); const [isFocused, setIsFocused] = useState(false); const showLoading = isSearching || isIndexing; - useHotkeys("mod+k", (e) => { - e.preventDefault(); - inputRef.current?.focus(); - }); - - useHotkeys( - "down", - (event) => { - if (document.activeElement === inputRef.current) { - event.preventDefault(); - console.log("down"); - } - }, - { enableOnFormTags: true }, - ); - - useHotkeys( - "up", - (event) => { - if (document.activeElement === inputRef.current) { - event.preventDefault(); - console.log("up"); - } - }, - { enableOnFormTags: true }, - ); - - useHotkeys( - "enter", - (event) => { - if (document.activeElement === inputRef.current) { - event.preventDefault(); - console.log("enter"); - } - }, - { enableOnFormTags: true }, - ); - - const handleFocus = () => { - setIsFocused(true); - onFocus(); - }; - - const handleBlur = () => { - setIsFocused(false); - onBlur(); - }; - return (
setQuery(e.target.value)} - onFocus={handleFocus} - onBlur={handleBlur} + onKeyDown={(e) => { + if (e.key === "Escape") { + e.currentTarget.blur(); + } + }} + onFocus={() => setIsFocused(true)} + onBlur={() => setIsFocused(false)} className={cn([ "text-sm", "w-full pl-9 h-full", diff --git a/apps/desktop/src/components/main/sidebar/profile/index.tsx b/apps/desktop/src/components/main/sidebar/profile/index.tsx index 26e2996c8a..1b1118bb74 100644 --- a/apps/desktop/src/components/main/sidebar/profile/index.tsx +++ b/apps/desktop/src/components/main/sidebar/profile/index.tsx @@ -1,4 +1,5 @@ import { commands as windowsCommands } from "@hypr/plugin-windows"; +import { Kbd, KbdGroup } from "@hypr/ui/components/ui/kbd"; import { clsx } from "clsx"; import { Calendar, ChevronUpIcon, FolderOpen, Settings, Users } from "lucide-react"; @@ -116,7 +117,17 @@ export function ProfileSection() { { icon: FolderOpen, label: "Folders", onClick: handleClickFolders }, { icon: Users, label: "Contacts", onClick: handleClickContacts }, { icon: Calendar, label: "Calendar", onClick: handleClickCalendar }, - { icon: Settings, label: "Settings", onClick: handleClickSettings }, + { + icon: Settings, + label: "Settings", + onClick: handleClickSettings, + badge: ( + + + , + + ), + }, ]; return ( diff --git a/apps/desktop/src/components/main/sidebar/profile/ota/index.tsx b/apps/desktop/src/components/main/sidebar/profile/ota/index.tsx index 2ee622bf5a..e636fdc00c 100644 --- a/apps/desktop/src/components/main/sidebar/profile/ota/index.tsx +++ b/apps/desktop/src/components/main/sidebar/profile/ota/index.tsx @@ -79,11 +79,36 @@ export function UpdateChecker() { if (state === "downloading") { return ( -
- - - Downloading... - +
+
+ + + + + + Downloading... ({Math.round(downloadProgress.percentage)}%) + +
-
-
-
); } diff --git a/apps/desktop/src/components/settings/general.tsx b/apps/desktop/src/components/settings/general.tsx index 5c33da68b2..572ebf5b09 100644 --- a/apps/desktop/src/components/settings/general.tsx +++ b/apps/desktop/src/components/settings/general.tsx @@ -1,15 +1,14 @@ import { LANGUAGES_ISO_639_1 } from "@huggingface/languages"; -import { AlertTriangle, Check, Link2, Plus, X } from "lucide-react"; -import { useState } from "react"; +import { AlertTriangle, Check, Search, X } from "lucide-react"; +import { useMemo, useState } from "react"; import { Badge } from "@hypr/ui/components/ui/badge"; import { Button } from "@hypr/ui/components/ui/button"; -import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@hypr/ui/components/ui/command"; -import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/popover"; + import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@hypr/ui/components/ui/select"; -import { Switch } from "@hypr/ui/components/ui/switch"; -import { Textarea } from "@hypr/ui/components/ui/textarea"; -import { useUpdateGeneral } from "./shared"; +import { cn } from "@hypr/utils"; + +import { SettingRow, useUpdateGeneral } from "./shared"; type ISO_639_1_CODE = keyof typeof LANGUAGES_ISO_639_1; const SUPPORTED_LANGUAGES: ISO_639_1_CODE[] = [ @@ -60,13 +59,117 @@ const SUPPORTED_LANGUAGES: ISO_639_1_CODE[] = [ export function SettingsGeneral() { const { value, handle } = useUpdateGeneral(); - const [languagePopoverOpen, setLanguagePopoverOpen] = useState(false); + const [languageSearchQuery, setLanguageSearchQuery] = useState(""); + const [languageInputFocused, setLanguageInputFocused] = useState(false); + const [languageSelectedIndex, setLanguageSelectedIndex] = useState(-1); + const [vocabSearchQuery, setVocabSearchQuery] = useState(""); + const [vocabInputFocused, setVocabInputFocused] = useState(false); + // Mock permission states - set to true/false to test different states + const [hasMicrophoneAccess, setHasMicrophoneAccess] = useState(true); + const [hasSystemAudioAccess, setHasSystemAudioAccess] = useState(false); + + const handleGrantMicrophoneAccess = () => { + // Mock: Toggle the permission state + setHasMicrophoneAccess(!hasMicrophoneAccess); + }; + + const handleGrantSystemAudioAccess = () => { + // Mock: Toggle the permission state + setHasSystemAudioAccess(!hasSystemAudioAccess); + }; + + const filteredLanguages = useMemo(() => { + if (!languageSearchQuery.trim()) { + return []; + } + const query = languageSearchQuery.toLowerCase(); + return SUPPORTED_LANGUAGES + .filter((langCode) => { + const langName = LANGUAGES_ISO_639_1[langCode].name; + return !((value.spoken_languages ?? []).includes(langName)) + && langName.toLowerCase().includes(query); + }) + .map((langCode) => LANGUAGES_ISO_639_1[langCode].name); + }, [languageSearchQuery, value.spoken_languages]); + + const handleLanguageKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Backspace" && !languageSearchQuery && (value.spoken_languages ?? []).length > 0) { + e.preventDefault(); + const languages = value.spoken_languages ?? []; + handle.setField("spoken_languages", languages.slice(0, -1)); + return; + } + + if (!languageSearchQuery.trim() || filteredLanguages.length === 0) { + return; + } + + if (e.key === "ArrowDown") { + e.preventDefault(); + setLanguageSelectedIndex((prev) => (prev < filteredLanguages.length - 1 ? prev + 1 : prev)); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setLanguageSelectedIndex((prev) => (prev > 0 ? prev - 1 : 0)); + } else if (e.key === "Enter") { + e.preventDefault(); + if (languageSelectedIndex >= 0 && languageSelectedIndex < filteredLanguages.length) { + handle.setField("spoken_languages", [ + ...(value.spoken_languages ?? []), + filteredLanguages[languageSelectedIndex], + ]); + setLanguageSearchQuery(""); + setLanguageSelectedIndex(-1); + } + } else if (e.key === "Escape") { + e.preventDefault(); + setLanguageInputFocused(false); + setLanguageSearchQuery(""); + } + }; + + const handleVocabChange = (e: React.ChangeEvent) => { + const inputValue = e.target.value; + setVocabSearchQuery(inputValue); + + if (inputValue.includes(",")) { + const terms = inputValue.split(",").map(s => s.trim()).filter(Boolean); + if (terms.length > 0) { + const existingJargons = new Set(value.jargons ?? []); + const newTerms = terms.filter(term => !existingJargons.has(term)); + if (newTerms.length > 0) { + handle.setField("jargons", [...(value.jargons ?? []), ...newTerms]); + } + setVocabSearchQuery(""); + } + } + }; + + const handleVocabKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Backspace" && !vocabSearchQuery && (value.jargons ?? []).length > 0) { + e.preventDefault(); + const jargons = value.jargons ?? []; + handle.setField("jargons", jargons.slice(0, -1)); + return; + } + + if (e.key === "Enter" && vocabSearchQuery.trim()) { + e.preventDefault(); + const newVocab = vocabSearchQuery.trim(); + if (!((value.jargons ?? []).includes(newVocab))) { + handle.setField("jargons", [...(value.jargons ?? []), newVocab]); + } + setVocabSearchQuery(""); + } else if (e.key === "Escape") { + e.preventDefault(); + setVocabInputFocused(false); + setVocabSearchQuery(""); + } + }; return ( -
- {/* App Section */} +
-

App

+

App

- {/* Language & Vocabulary Section */}
-

Language & Vocabulary

+

Language & Vocabulary

- {/* Main Language */}
-

Main language

-

Language for summaries, chats, and AI-generated responses

+

Main language

+

Language for summaries, chats, and AI-generated responses

- {/* Spoken Languages */}
-

Spoken languages

-

Add other languages you use other than the main language

-
-
+

Spoken languages

+

Add other languages you use other than the main language

+
+
document.getElementById("language-search-input")?.focus()} + > {(value.spoken_languages ?? []).map((lang) => ( { + onClick={(e) => { + e.stopPropagation(); const updated = (value.spoken_languages ?? []).filter(l => l !== lang); handle.setField("spoken_languages", updated); }} @@ -149,118 +258,184 @@ export function SettingsGeneral() { ))} + {(value.spoken_languages ?? []).length === 0 && ( + + )} + { + setLanguageSearchQuery(e.target.value); + setLanguageSelectedIndex(-1); + }} + onKeyDown={handleLanguageKeyDown} + onFocus={() => setLanguageInputFocused(true)} + onBlur={() => setLanguageInputFocused(false)} + role="combobox" + aria-haspopup="listbox" + aria-expanded={languageInputFocused && !!languageSearchQuery.trim()} + aria-controls="language-options" + aria-activedescendant={languageSelectedIndex >= 0 + ? `language-option-${languageSelectedIndex}` + : undefined} + aria-label="Add spoken language" + placeholder={(value.spoken_languages ?? []).length === 0 ? "Add language" : ""} + className="flex-1 min-w-[120px] bg-transparent text-sm focus:outline-none placeholder:text-neutral-500" + />
- - - - - - - - No language found. - - {SUPPORTED_LANGUAGES.filter( - (langCode) => !(value.spoken_languages ?? []).includes(LANGUAGES_ISO_639_1[langCode].name), - ).map((langCode) => ( - { - const langName = LANGUAGES_ISO_639_1[langCode].name; + + {languageInputFocused && languageSearchQuery.trim() && ( +
+ {filteredLanguages.length > 0 + ? ( + filteredLanguages.map((langName, index) => ( + + )) + ) + : ( +
+ No matching languages found +
+ )} +
+ )}
- {/* Custom Vocabulary */}
-

Custom vocabulary

-

+

Custom vocabulary

+

Add jargons or industry/company-specific terms to improve transcription accuracy

-