Skip to content
Merged
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
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
17 changes: 16 additions & 1 deletion apps/desktop/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,32 @@
"identifier": "opener:allow-open-url",
"allow": [{ "url": "https://**" }]
},
{
"identifier": "opener:allow-open-path",
"allow": [
{ "path": "$APPDATA/*" },
{ "path": "$APPDATA/**" }
]
},
{
"identifier": "fs:allow-exists",
"allow": [{ "path": "/Applications/*" }]
},
{
"identifier": "fs:allow-write-file",
"allow": [
{ "path": "$APPDATA/*" },
{ "path": "$APPDATA/**" }
]
},
{
"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>;
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]);

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);
}, []);

// 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);
}
};

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