Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
4a5034f
added header chips
duckduckhero Aug 29, 2025
d239d69
before adding transcript tools
duckduckhero Aug 29, 2025
0ecd73e
huge wip...
duckduckhero Aug 30, 2025
107f7ee
relieved version
duckduckhero Aug 30, 2025
dd979e5
fixed transcript editor issue
duckduckhero Aug 30, 2025
ef6a091
ideation in progress
duckduckhero Aug 30, 2025
1b2057d
before moving chips
duckduckhero Aug 30, 2025
965a4c4
going towards the right direction?
duckduckhero Aug 30, 2025
0171911
tab switching fixed
duckduckhero Aug 31, 2025
9ff7eea
added a red recording dot
duckduckhero Aug 31, 2025
93c3c7d
search box initial version - partially working
duckduckhero Aug 31, 2025
8da5f4f
it works now at least
duckduckhero Aug 31, 2025
6a99619
minor changes
duckduckhero Aug 31, 2025
ed0cc7f
Merge branch 'main' of https://github.com/fastrepl/hyprnote into over…
duckduckhero Aug 31, 2025
be092c9
regenerate button floating
duckduckhero Aug 31, 2025
198e845
added top search bar
duckduckhero Aug 31, 2025
a387a18
wip
duckduckhero Sep 1, 2025
d2f81f4
thinigs trying to work?
duckduckhero Sep 1, 2025
080a3fc
something weird
duckduckhero Sep 1, 2025
e5d96b7
chat buttom position fixed
duckduckhero Sep 1, 2025
08bb7f6
Merge branch 'main' of https://github.com/fastrepl/hyprnote into over…
duckduckhero Sep 2, 2025
45595ce
expandable feeling metadata modal
duckduckhero Sep 2, 2025
b7929e0
popover metadata
duckduckhero Sep 2, 2025
1cbda4e
success?
duckduckhero Sep 24, 2025
0e848b7
wip
duckduckhero Sep 24, 2025
482ed9a
merged current main
duckduckhero Sep 25, 2025
1bbd1f6
lots of advancements
duckduckhero Sep 25, 2025
c236284
fixed floating buttons
duckduckhero Sep 25, 2025
d8e2e36
fixed test errors
duckduckhero Sep 25, 2025
c3770c2
attempt to fix tab switching issues
duckduckhero Sep 25, 2025
4592198
right panel auto expand
duckduckhero Sep 25, 2025
b768182
minor fixes
duckduckhero Sep 26, 2025
c0fe42f
Merge branch 'main' of https://github.com/fastrepl/hyprnote into over…
duckduckhero Sep 26, 2025
8a73aa2
chat wip
duckduckhero Sep 26, 2025
6d19bd6
dynamic quick actions initial draft
duckduckhero Sep 26, 2025
1f80bb5
empty chat state responsiveness
duckduckhero Sep 26, 2025
9585485
Merge branch 'main' of https://github.com/fastrepl/hyprnote into over…
duckduckhero Sep 26, 2025
74d0e2a
added brain
duckduckhero Sep 26, 2025
e8f7d41
updated chat model info modal
duckduckhero Sep 26, 2025
2bc3c5d
test
duckduckhero Sep 26, 2025
a8caf3e
ran tests
duckduckhero Sep 26, 2025
3b7bd77
Merge branch 'main' of https://github.com/fastrepl/hyprnote into over…
duckduckhero Sep 28, 2025
d677d37
Merge branch 'main' of https://github.com/fastrepl/hyprnote into over…
duckduckhero Sep 28, 2025
77b7fd6
Merge branch 'main' of https://github.com/fastrepl/hyprnote into over…
duckduckhero Sep 28, 2025
1ea8f4c
Merge branch 'main' of https://github.com/fastrepl/hyprnote into over…
duckduckhero Sep 28, 2025
2c9d363
ran tests
duckduckhero Sep 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
304 changes: 304 additions & 0 deletions apps/desktop/src/components/editor-area/floating-search-box.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
import { type TiptapEditor } from "@hypr/tiptap/editor";
import { type TranscriptEditorRef } from "@hypr/tiptap/transcript";
import { Button } from "@hypr/ui/components/ui/button";
import { Input } from "@hypr/ui/components/ui/input";
import useDebouncedCallback from "beautiful-react-hooks/useDebouncedCallback";
import { ChevronDownIcon, ChevronUpIcon, XIcon } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";

interface FloatingSearchBoxProps {
editorRef: React.RefObject<TranscriptEditorRef | null> | React.RefObject<{ editor: TiptapEditor | null }>;
onClose: () => void;
isVisible: boolean;
}

export function FloatingSearchBox({ editorRef, onClose, isVisible }: FloatingSearchBoxProps) {
const [searchTerm, setSearchTerm] = useState("");
const [replaceTerm, setReplaceTerm] = useState("");
const [resultCount, setResultCount] = useState(0);
const [currentIndex, setCurrentIndex] = useState(0);

// Get the editor - NO useCallback, we want fresh ref every time
const getEditor = () => {
const ref = editorRef.current;
if (!ref) {
return null;
}

// For both normal editor and transcript editor, just access the editor property
if ("editor" in ref && ref.editor) {
return ref.editor;
}

return null;
};

// Add ref for the search box container
const searchBoxRef = useRef<HTMLDivElement>(null);

// Debounced search term update - NO getEditor in deps
const debouncedSetSearchTerm = useDebouncedCallback(
(value: string) => {
const editor = getEditor();
if (editor && editor.commands) {
try {
editor.commands.setSearchTerm(value);
editor.commands.resetIndex();
setTimeout(() => {
const storage = editor.storage?.searchAndReplace;
const results = storage?.results || [];
setResultCount(results.length);
setCurrentIndex((storage?.resultIndex ?? 0) + 1);
}, 100);
} catch (e) {
// Editor might not be ready yet, ignore
console.warn("Editor not ready for search:", e);
}
}
},
[], // Empty deps to prevent infinite re-creation
300,
);

useEffect(() => {
debouncedSetSearchTerm(searchTerm);
}, [searchTerm, debouncedSetSearchTerm]);

useEffect(() => {
const editor = getEditor();
if (editor && editor.commands) {
try {
editor.commands.setReplaceTerm(replaceTerm);
} catch (e) {
// Editor might not be ready yet, ignore
}
}
}, [replaceTerm]); // Removed getEditor from deps

// Click outside handler
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (searchBoxRef.current && !searchBoxRef.current.contains(event.target as Node)) {
handleClose();
}
};

if (isVisible) {
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}
}, [isVisible]);

// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
handleClose();
}
};

if (isVisible) {
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}
}, [isVisible]);

const scrollCurrentResultIntoView = useCallback(() => {
const editor = getEditor();
if (!editor) {
return;
}

try {
const editorElement = editor.view.dom;
const current = editorElement.querySelector(".search-result-current") as HTMLElement | null;
if (current) {
current.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "nearest",
});
}
} catch (e) {
// Editor view not ready yet, ignore
}
}, []);

const handleNext = useCallback(() => {
const editor = getEditor();
if (editor) {
editor.commands.nextSearchResult();
setTimeout(() => {
const storage = editor.storage.searchAndReplace;
setCurrentIndex((storage?.resultIndex ?? 0) + 1);
scrollCurrentResultIntoView();
}, 100);
}
}, [scrollCurrentResultIntoView]);

const handlePrevious = useCallback(() => {
const editor = getEditor();
if (editor) {
editor.commands.previousSearchResult();
setTimeout(() => {
const storage = editor.storage.searchAndReplace;
setCurrentIndex((storage?.resultIndex ?? 0) + 1);
scrollCurrentResultIntoView();
}, 100);
}
}, [scrollCurrentResultIntoView]);

const handleReplace = useCallback(() => {
const editor = getEditor();
if (editor) {
editor.commands.replace();
setTimeout(() => {
const storage = editor.storage.searchAndReplace;
const results = storage?.results || [];
setResultCount(results.length);
setCurrentIndex((storage?.resultIndex ?? 0) + 1);
}, 100);
}
}, []);

const handleReplaceAll = useCallback(() => {
const editor = getEditor();
if (editor) {
editor.commands.replaceAll();
setTimeout(() => {
const storage = editor.storage.searchAndReplace;
const results = storage?.results || [];
setResultCount(results.length);
setCurrentIndex(0);
}, 100);
}
}, []);

const handleClose = useCallback(() => {
const editor = getEditor();
if (editor) {
editor.commands.setSearchTerm("");
}
setSearchTerm("");
setReplaceTerm("");
setResultCount(0);
setCurrentIndex(0);
onClose();
}, [onClose]);

const handleKeyDown = (e: React.KeyboardEvent) => {
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();
}
}
};

if (!isVisible) {
return null;
}

return (
<div className="absolute top-6 right-6 z-50">
<div
ref={searchBoxRef}
className="bg-white border border-neutral-200 rounded-lg shadow-lg p-3 min-w-96"
>
<div className="flex items-center gap-2 mb-2">
{/* Search Input */}
<div className="flex items-center gap-1 bg-transparent border border-neutral-200 rounded px-2 py-1 flex-1">
<Input
className="h-6 border-0 focus-visible:ring-0 focus-visible:ring-offset-0 px-1 bg-transparent flex-1 text-sm"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Search..."
autoFocus
/>
</div>

{/* Results Counter */}
{searchTerm && (
<span className="text-xs text-neutral-500 whitespace-nowrap min-w-12 text-center">
{resultCount > 0 ? `${currentIndex}/${resultCount}` : "0/0"}
</span>
)}

{/* Navigation Buttons */}
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
onClick={handlePrevious}
disabled={resultCount === 0}
>
<ChevronUpIcon size={12} />
</Button>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
onClick={handleNext}
disabled={resultCount === 0}
>
<ChevronDownIcon size={12} />
</Button>

{/* Close Button */}
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
onClick={handleClose}
>
<XIcon size={12} />
</Button>
</div>

{/* Replace Row */}
<div className="flex items-center gap-2">
{/* Replace Input */}
<div className="flex items-center gap-1 bg-transparent border border-neutral-200 rounded px-2 py-1 flex-1">
<Input
className="h-6 border-0 focus-visible:ring-0 focus-visible:ring-offset-0 px-1 bg-transparent flex-1 text-sm"
value={replaceTerm}
onChange={(e) => setReplaceTerm(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Replace..."
/>
</div>

{/* Replace Buttons */}
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs"
onClick={handleReplace}
disabled={resultCount === 0 || !replaceTerm}
>
Replace
</Button>
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs"
onClick={handleReplaceAll}
disabled={resultCount === 0 || !replaceTerm}
>
All
</Button>
</div>
</div>
</div>
);
}
Loading
Loading