diff --git a/apps/desktop/src/components/right-panel/views/transcript-view.tsx b/apps/desktop/src/components/right-panel/views/transcript-view.tsx index 3e56cdc591..32f42efa02 100644 --- a/apps/desktop/src/components/right-panel/views/transcript-view.tsx +++ b/apps/desktop/src/components/right-panel/views/transcript-view.tsx @@ -2,7 +2,18 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useMatch } from "@tanstack/react-router"; import { writeText as writeTextToClipboard } from "@tauri-apps/plugin-clipboard-manager"; import useDebouncedCallback from "beautiful-react-hooks/useDebouncedCallback"; -import { AudioLinesIcon, CheckIcon, ClipboardIcon, CopyIcon, TextSearchIcon, UploadIcon } from "lucide-react"; +import { + AudioLinesIcon, + CheckIcon, + ChevronDownIcon, + ChevronUpIcon, + ClipboardIcon, + CopyIcon, + ReplaceIcon, + TextSearchIcon, + UploadIcon, + XIcon, +} from "lucide-react"; import { memo, useCallback, useEffect, useRef, useState } from "react"; import { ParticipantsChipInner } from "@/components/editor-area/note-header/chips/participants-chip"; @@ -101,6 +112,7 @@ export function TranscriptView() { )}
+ {showActions && } {(audioExist.data && showActions) && ( )} - {showActions && } {showActions && }
@@ -328,22 +339,31 @@ function SpeakerRangeSelector({ value, onChange }: SpeakerRangeSelectorProps) { ); } -function SearchAndReplace({ editorRef }: { editorRef: React.RefObject }) { - const [expanded, setExpanded] = useState(false); +export function SearchAndReplace({ editorRef }: { editorRef: React.RefObject }) { + const [isActive, setIsActive] = useState(false); const [searchTerm, setSearchTerm] = useState(""); const [replaceTerm, setReplaceTerm] = useState(""); + const [resultCount, setResultCount] = useState(0); + const [currentIndex, setCurrentIndex] = useState(0); + // Add ref for the search container + const searchContainerRef = useRef(null); + + // Debounced search term update const debouncedSetSearchTerm = useDebouncedCallback( (value: string) => { if (editorRef.current) { editorRef.current.editor.commands.setSearchTerm(value); - - if (value.substring(0, value.length - 1) === replaceTerm) { - setReplaceTerm(value); - } + editorRef.current.editor.commands.resetIndex(); + setTimeout(() => { + const storage = editorRef.current.editor.storage.searchAndReplace; + const results = storage.results || []; + setResultCount(results.length); + setCurrentIndex((storage.resultIndex ?? 0) + 1); + }, 100); } }, - [editorRef, replaceTerm], + [editorRef], 300, ); @@ -357,55 +377,212 @@ function SearchAndReplace({ editorRef }: { editorRef: React.RefObject }) { } }, [replaceTerm]); + // Click outside handler + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (searchContainerRef.current && !searchContainerRef.current.contains(event.target as Node)) { + if (isActive) { + setIsActive(false); + setSearchTerm(""); + setReplaceTerm(""); + setResultCount(0); + setCurrentIndex(0); + if (editorRef.current) { + editorRef.current.editor.commands.setSearchTerm(""); + } + } + } + }; + + if (isActive) { + document.addEventListener("mousedown", handleClickOutside); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [isActive, editorRef]); + + // Keyboard shortcut handler - only when transcript editor is focused + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key === "f") { + const isTranscriptFocused = editorRef.current?.editor?.isFocused; + if (isTranscriptFocused) { + e.preventDefault(); + setIsActive(true); + } + } + }; + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [editorRef]); + + // Use extension's navigation commands + const handleNext = () => { + if (editorRef.current?.editor) { + editorRef.current.editor.commands.nextSearchResult(); + setTimeout(() => { + const storage = editorRef.current.editor.storage.searchAndReplace; + setCurrentIndex((storage.resultIndex ?? 0) + 1); + scrollCurrentResultIntoView(editorRef); + }, 100); + } + }; + + const handlePrevious = () => { + if (editorRef.current?.editor) { + editorRef.current.editor.commands.previousSearchResult(); + setTimeout(() => { + const storage = editorRef.current.editor.storage.searchAndReplace; + setCurrentIndex((storage.resultIndex ?? 0) + 1); + scrollCurrentResultIntoView(editorRef); + }, 100); + } + }; + + function scrollCurrentResultIntoView(editorRef: React.RefObject) { + if (!editorRef.current) { + return; + } + const editorElement = editorRef.current.editor.view.dom; + const current = editorElement.querySelector(".search-result-current") as HTMLElement | null; + if (current) { + current.scrollIntoView({ + behavior: "smooth", + block: "center", + inline: "nearest", + }); + } + } + const handleReplaceAll = () => { if (editorRef.current && searchTerm) { - editorRef.current.editor.commands.replaceAll(replaceTerm); - setExpanded(false); + editorRef.current.editor.commands.replaceAll(); + setTimeout(() => { + const storage = editorRef.current.editor.storage.searchAndReplace; + const results = storage.results || []; + setResultCount(results.length); + setCurrentIndex(results.length > 0 ? 1 : 0); + }, 100); } }; - useEffect(() => { - if (!expanded) { + const handleToggle = () => { + setIsActive(!isActive); + if (isActive && editorRef.current) { setSearchTerm(""); setReplaceTerm(""); + setResultCount(0); + setCurrentIndex(0); + editorRef.current.editor.commands.setSearchTerm(""); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Escape") { + handleToggle(); + } else if (e.key === "Enter") { + e.preventDefault(); + if (e.shiftKey) { + handlePrevious(); + } else { + handleNext(); + } + } else if (e.key === "F3") { + e.preventDefault(); + if (e.shiftKey) { + handlePrevious(); + } else { + handleNext(); + } } - }, [expanded]); + }; return ( - - - - - -
- setSearchTerm(e.target.value)} - placeholder="Search" - /> - setReplaceTerm(e.target.value)} - placeholder="Replace" - /> +
+ {!isActive + ? ( -
- - + ) + : ( +
+
+ setSearchTerm(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Search..." + autoFocus + /> +
+ setReplaceTerm(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Replace..." + /> +
+ {searchTerm && ( +
+ + {resultCount > 0 ? `${currentIndex}/${resultCount}` : "0/0"} + +
+ + +
+
+ )} + + +
+ )} +
); } diff --git a/crates/llama/Cargo.toml b/crates/llama/Cargo.toml index b8a4eabfc8..c8c0bd2777 100644 --- a/crates/llama/Cargo.toml +++ b/crates/llama/Cargo.toml @@ -21,7 +21,8 @@ thiserror = { workspace = true } llama-cpp-2 = { git = "https://github.com/utilityai/llama-cpp-rs", default-features = false, features = ["openmp"], branch = "update-llama-cpp-2025-06-04" } [target.'cfg(all(target_os = "macos", target_arch = "aarch64"))'.dependencies] -llama-cpp-2 = { git = "https://github.com/utilityai/llama-cpp-rs", features = ["openmp", "metal"], branch = "update-llama-cpp-2025-06-04" } + +llama-cpp-2 = { git = "https://github.com/utilityai/llama-cpp-rs", features = ["openmp", "metal"], branch = "update-llama-cpp-2025-05-28" } [target.'cfg(all(target_os = "macos", target_arch = "x86_64"))'.dependencies] llama-cpp-2 = { git = "https://github.com/utilityai/llama-cpp-rs", features = ["native"], branch = "update-llama-cpp-2025-06-04" } diff --git a/crates/whisper/Cargo.toml b/crates/whisper/Cargo.toml index 439239ea94..fd71e8e523 100644 --- a/crates/whisper/Cargo.toml +++ b/crates/whisper/Cargo.toml @@ -39,7 +39,8 @@ regex = { workspace = true, optional = true } whisper-rs = { git = "https://github.com/tazz4843/whisper-rs", rev = "e3d67d5", features = ["raw-api", "tracing_backend"], optional = true } [target.'cfg(all(target_os = "macos", target_arch = "aarch64"))'.dependencies] -whisper-rs = { git = "https://github.com/tazz4843/whisper-rs", rev = "e3d67d5", features = ["raw-api", "tracing_backend", "metal"], optional = true } + +whisper-rs = { git = "https://github.com/tazz4843/whisper-rs", rev = "e3d67d5", features = ["raw-api", "tracing_backend"], optional = true } [target.'cfg(all(target_os = "macos", target_arch = "x86_64"))'.dependencies] whisper-rs = { git = "https://github.com/tazz4843/whisper-rs", rev = "e3d67d5", features = ["raw-api", "tracing_backend"], optional = true } diff --git a/packages/tiptap/src/styles/transcript.css b/packages/tiptap/src/styles/transcript.css index 557cc0799c..7afcfb1b51 100644 --- a/packages/tiptap/src/styles/transcript.css +++ b/packages/tiptap/src/styles/transcript.css @@ -4,6 +4,12 @@ padding: 1px 0; } +.search-result-current { + background-color: #31e054 !important; + border-radius: 2px; + padding: 1px 0; +} + .transcript-speaker { margin-bottom: 14px; line-height: 1.6;