Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 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
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;
}
Comment on lines +9 to +13
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Type compile error: React.RefObject used without importing React types

Using React.RefObject in a type position requires importing the React type (or switching to RefObject). Add a type‑only import to avoid “Cannot find namespace 'React'”.

Apply this diff:

-import { Button } from "@hypr/ui/components/ui/button";
+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";
 import { type TranscriptEditorRef } from "@hypr/tiptap/transcript";
 import { type TiptapEditor } from "@hypr/tiptap/editor";
+import type React from "react";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface FloatingSearchBoxProps {
editorRef: React.RefObject<TranscriptEditorRef | null> | React.RefObject<{ editor: TiptapEditor | null }>;
onClose: () => void;
isVisible: boolean;
}
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";
import { type TranscriptEditorRef } from "@hypr/tiptap/transcript";
import { type TiptapEditor } from "@hypr/tiptap/editor";
import type React from "react";
interface FloatingSearchBoxProps {
editorRef: React.RefObject<TranscriptEditorRef | null> | React.RefObject<{ editor: TiptapEditor | null }>;
onClose: () => void;
isVisible: boolean;
}
🤖 Prompt for AI Agents
In apps/desktop/src/components/editor-area/floating-search-box.tsx around lines
9 to 13, the prop types use React.RefObject in a type position but React types
are not imported, causing a “Cannot find namespace 'React'” compile error; add a
type-only import and switch to RefObject to fix it (for example import type {
RefObject } from 'react' and update the prop type to use RefObject<...>), or
alternatively add import type React from 'react' so React.RefObject is
recognized.


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