From 608f20c8cebdeb03403c8f9bf239510514bcf8a0 Mon Sep 17 00:00:00 2001 From: John Jeong Date: Tue, 21 Oct 2025 14:56:35 +0900 Subject: [PATCH 01/10] polished settings layout --- .../src/routes/app/settings/_layout.tsx | 72 +++++-------------- 1 file changed, 18 insertions(+), 54 deletions(-) diff --git a/apps/desktop/src/routes/app/settings/_layout.tsx b/apps/desktop/src/routes/app/settings/_layout.tsx index 33fad00870..c6a7fbcf7a 100644 --- a/apps/desktop/src/routes/app/settings/_layout.tsx +++ b/apps/desktop/src/routes/app/settings/_layout.tsx @@ -11,6 +11,7 @@ import { } from "lucide-react"; import { z } from "zod"; +import { Button } from "@hypr/ui/components/ui/button"; import { cn } from "@hypr/utils"; const TABS = [ @@ -42,56 +43,24 @@ function Component() { const group3Tabs = TABS.filter((tab) => info(tab).group === 3); return ( -
-
-
- +
+ -
- - -
-
-
- -
+
-
-

+

{info(search.tab).label}

-
-
+
@@ -117,7 +86,6 @@ function Group(
@@ -127,22 +95,18 @@ function Group( const Icon = tabInfo.icon; return ( - + + {tabInfo.label} + ); })}
From ff897c4e2649d0ea842ae063fd6f3213a1ba4fb1 Mon Sep 17 00:00:00 2001 From: John Jeong Date: Tue, 21 Oct 2025 16:36:57 +0900 Subject: [PATCH 02/10] general --- .../src/components/settings/general.tsx | 388 +++++++++++++----- 1 file changed, 293 insertions(+), 95 deletions(-) diff --git a/apps/desktop/src/components/settings/general.tsx b/apps/desktop/src/components/settings/general.tsx index 5c33da68b2..677c2356f7 100644 --- a/apps/desktop/src/components/settings/general.tsx +++ b/apps/desktop/src/components/settings/general.tsx @@ -1,14 +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 { cn } from "@hypr/utils"; + import { useUpdateGeneral } from "./shared"; type ISO_639_1_CODE = keyof typeof LANGUAGES_ISO_639_1; @@ -60,13 +60,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,100 +259,188 @@ 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

-