Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions apps/desktop/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[target.aarch64-apple-darwin]
rustflags = ["-C", "target-cpu=apple-m1"]

[build]
target = "aarch64-apple-darwin"
9 changes: 9 additions & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@react-pdf/renderer": "^4.3.0",
"@remixicon/react": "^4.6.0",
"@sentry/react": "^8.55.0",
"@tanstack/react-query": "^5.79.0",
Expand All @@ -69,6 +70,11 @@
"beautiful-react-hooks": "^5.0.3",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"diff": "^8.0.2",
"html-pdf": "^3.0.1",
"html2canvas": "^1.4.1",
"html2pdf.js": "^0.10.3",
"jspdf": "^3.0.1",
"lodash-es": "^4.17.21",
"lucide-react": "^0.511.0",
"motion": "^11.18.2",
Expand Down Expand Up @@ -96,6 +102,9 @@
"@mux/mux-player": "^3.4.0",
"@tanstack/router-plugin": "^1.120.13",
"@tauri-apps/cli": "^2.5.0",
"@types/diff": "^8.0.0",
"@types/html-pdf": "^3.0.3",
"@types/jspdf": "^2.0.0",
"@types/node": "^22.15.29",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
Expand Down
10 changes: 9 additions & 1 deletion apps/desktop/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,21 @@
"identifier": "fs:allow-exists",
"allow": [{ "path": "/Applications/*" }]
},
{
"identifier": "fs:allow-write-file",
"allow": [
{ "path": "$DOWNLOAD/*" },
{ "path": "$DOWNLOAD/**" }
]
},
{
"identifier": "http:default",
"allow": [
{ "url": "http://localhost:*" },
{ "url": "http://127.0.0.1:*" },
{ "url": "https://**" }
]
}
},
"dialog:allow-save"
]
}
26 changes: 25 additions & 1 deletion apps/desktop/src/components/editor-area/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { toast } from "@hypr/ui/components/ui/toast";
import { useMutation } from "@tanstack/react-query";
import usePreviousValue from "beautiful-react-hooks/usePreviousValue";
import { diffWords } from "diff";
import { motion } from "motion/react";
import { AnimatePresence } from "motion/react";
import { useCallback, useEffect, useMemo, useRef } from "react";

import { useHypr } from "@/contexts";
import { extractTextFromHtml } from "@/utils/parse";
import { commands as analyticsCommands } from "@hypr/plugin-analytics";
import { commands as connectorCommands } from "@hypr/plugin-connector";
import { commands as dbCommands } from "@hypr/plugin-db";
Expand Down Expand Up @@ -60,8 +62,11 @@ export default function EditorArea({
);

const generateTitle = useGenerateTitleMutation({ sessionId });
const preMeetingNote = useSession(sessionId, (s) => s.session.pre_meeting_memo_html) ?? "";

const enhance = useEnhanceMutation({
sessionId,
preMeetingNote,
rawContent,
onSuccess: (content) => {
generateTitle.mutate({ enhancedContent: content });
Expand Down Expand Up @@ -175,15 +180,31 @@ export default function EditorArea({

export function useEnhanceMutation({
sessionId,
preMeetingNote,
rawContent,
onSuccess,
}: {
sessionId: string;
preMeetingNote: string;
rawContent: string;
onSuccess: (enhancedContent: string) => void;
}) {
const { userId, onboardingSessionId } = useHypr();

const preMeetingText = extractTextFromHtml(preMeetingNote);
const rawText = extractTextFromHtml(rawContent);

// finalInput is the text that will be used to enhance the note
var finalInput = "";
const wordDiff = diffWords(preMeetingText, rawText);
if (wordDiff && wordDiff.length > 0) {
for (const diff of wordDiff) {
if (diff.added && diff.removed == false) {
finalInput += " " + diff.value;
}
}
}

const setEnhanceController = useOngoingSession((s) => s.setEnhanceController);
const { persistSession, setEnhancedContent } = useSession(sessionId, (s) => ({
persistSession: s.persistSession,
Expand Down Expand Up @@ -225,12 +246,15 @@ export function useEnhanceMutation({
"enhance.user",
{
type,
editor: rawContent,
editor: finalInput,
words: JSON.stringify(words),
participants,
},
);

// console.log("systemMessage", systemMessage);
// console.log("userMessage", userMessage);

const abortController = new AbortController();
const abortSignal = AbortSignal.any([abortController.signal, AbortSignal.timeout(60 * 1000)]);
setEnhanceController(abortController);
Expand Down
226 changes: 226 additions & 0 deletions apps/desktop/src/components/right-panel/components/search-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
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, ReplaceIcon, XIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
interface SearchHeaderProps {
editorRef: React.RefObject<any>;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Type the editorRef prop properly instead of using any.

Using any defeats TypeScript's type safety. Define or import the proper type for the editor reference.

-  editorRef: React.RefObject<any>;
+  editorRef: React.RefObject<TranscriptEditorRef>;
📝 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
editorRef: React.RefObject<any>;
editorRef: React.RefObject<TranscriptEditorRef>;
🤖 Prompt for AI Agents
In apps/desktop/src/components/right-panel/components/search-header.tsx at line
7, the editorRef prop is currently typed as React.RefObject<any>, which bypasses
TypeScript's type safety. Replace the any type with the specific type of the
editor instance that editorRef is expected to reference, either by importing the
editor's type or defining it explicitly, to ensure proper type checking and
safety.

onClose: () => void;
}

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

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

// Debounced search term update
const debouncedSetSearchTerm = useDebouncedCallback(
(value: string) => {
if (editorRef.current) {
editorRef.current.editor.commands.setSearchTerm(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],
300,
);

useEffect(() => {
debouncedSetSearchTerm(searchTerm);
}, [searchTerm]);
Comment on lines +38 to +40
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add missing dependency to useEffect.

Include debouncedSetSearchTerm in the dependency array to avoid potential stale closure issues.

  useEffect(() => {
    debouncedSetSearchTerm(searchTerm);
-  }, [searchTerm]);
+  }, [searchTerm, debouncedSetSearchTerm]);
📝 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
useEffect(() => {
debouncedSetSearchTerm(searchTerm);
}, [searchTerm]);
useEffect(() => {
debouncedSetSearchTerm(searchTerm);
}, [searchTerm, debouncedSetSearchTerm]);
🤖 Prompt for AI Agents
In apps/desktop/src/components/right-panel/components/search-header.tsx around
lines 38 to 40, the useEffect hook is missing the debouncedSetSearchTerm
function in its dependency array. Add debouncedSetSearchTerm to the dependency
array to ensure the effect updates correctly when this function changes and to
prevent stale closure issues.


useEffect(() => {
if (editorRef.current) {
editorRef.current.editor.commands.setReplaceTerm(replaceTerm);
}
}, [replaceTerm]);

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

document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
Comment on lines +48 to +58
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add missing dependencies to click outside handler.

The effect uses handleClose which is defined later and should be included in dependencies. Consider using useCallback to memoize handleClose.

Add onClose to the dependency array:

    document.addEventListener("mousedown", handleClickOutside);
    return () => document.removeEventListener("mousedown", handleClickOutside);
-  }, []);
+  }, [onClose]);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/desktop/src/components/right-panel/components/search-header.tsx around
lines 48 to 58, the useEffect hook for the click outside handler is missing
dependencies, specifically the handleClose function. To fix this, memoize
handleClose using useCallback and include it in the dependency array of the
useEffect hook to ensure the effect updates correctly when handleClose changes.


// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
handleClose();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, []);

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);
}
};
Comment on lines +71 to +91
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid using setTimeout for state synchronization.

Multiple setTimeout calls with hardcoded delays can lead to race conditions. Consider using promises or callbacks from the editor API if available, or at least extract the delay to a named constant.

+const EDITOR_UPDATE_DELAY = 100; // ms

  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);
+      }, EDITOR_UPDATE_DELAY);
    }
  };

  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);
+      }, EDITOR_UPDATE_DELAY);
    }
  };
📝 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
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);
}
};
// extract magic number for consistency and easy adjustment
const EDITOR_UPDATE_DELAY = 100; // ms
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);
}, EDITOR_UPDATE_DELAY);
}
};
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);
}, EDITOR_UPDATE_DELAY);
}
};
🤖 Prompt for AI Agents
In apps/desktop/src/components/right-panel/components/search-header.tsx around
lines 71 to 91, avoid using setTimeout with hardcoded delays for state
synchronization as it can cause race conditions. Instead, refactor handleNext
and handlePrevious to use promises or callbacks provided by the editor API to
update the current index and scroll after the search result changes. If the
editor API does not support this, at minimum extract the delay duration into a
named constant for clarity and easier adjustment.


function scrollCurrentResultIntoView(editorRef: React.RefObject<any>) {
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();
setTimeout(() => {
const storage = editorRef.current.editor.storage.searchAndReplace;
const results = storage.results || [];
setResultCount(results.length);
setCurrentIndex(results.length > 0 ? 1 : 0);
}, 100);
}
};

const handleClose = () => {
if (editorRef.current) {
editorRef.current.editor.commands.setSearchTerm("");
}
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();
}
}
};

return (
<header
ref={searchHeaderRef}
className="flex items-center w-full px-4 py-1 my-1 border-b border-neutral-100 bg-neutral-50"
>
<div className="flex items-center gap-2 flex-1">
{/* Search Input */}
<div className="flex items-center gap-1 bg-transparent border border-neutral-200 rounded px-2 py-0.5 mb-1.5 flex-1 max-w-xs">
<Input
className="h-5 border-0 focus-visible:ring-0 focus-visible:ring-offset-0 px-1 bg-transparent flex-1 text-xs"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Search..."
autoFocus
/>
</div>

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

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

{/* Action Buttons */}
<div className="flex items-center gap-1 ml-2">
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={handlePrevious}
disabled={resultCount === 0}
title="Previous (Shift+Enter)"
>
<ChevronUpIcon size={14} />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={handleNext}
disabled={resultCount === 0}
title="Next (Enter)"
>
<ChevronDownIcon size={14} />
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleReplaceAll}
disabled={!searchTerm || resultCount === 0}
className="h-7 px-2"
title="Replace All"
>
<ReplaceIcon size={12} />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={handleClose}
title="Close (Esc)"
>
<XIcon size={14} />
</Button>
</div>
</header>
);
}
Loading
Loading