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
+ ? (
-
-
-
+ )
+ : (
+
+
+ {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;