From 2ccf6b043c9e63d94275ae0dfb47e72e7d2f3f8c Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Sat, 14 Jun 2025 07:40:49 +0900 Subject: [PATCH 01/17] fix --- apps/desktop/.cargo/config.toml | 18 ++++++++++++++++++ apps/desktop/build.rs | 19 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 apps/desktop/.cargo/config.toml create mode 100644 apps/desktop/build.rs diff --git a/apps/desktop/.cargo/config.toml b/apps/desktop/.cargo/config.toml new file mode 100644 index 000000000..22d651c4d --- /dev/null +++ b/apps/desktop/.cargo/config.toml @@ -0,0 +1,18 @@ +[env] +GGML_NATIVE = "OFF" +GGML_CPU_NATIVE = "0" +CMAKE_C_FLAGS = "-mcpu=apple-m1" +CMAKE_CXX_FLAGS = "-mcpu=apple-m1" +CC = "clang" +CXX = "clang++" +CFLAGS = "-mcpu=apple-m1" +CXXFLAGS = "-mcpu=apple-m1" +CMAKE_OSX_ARCHITECTURES = "arm64" +CMAKE_SYSTEM_PROCESSOR = "aarch64" +CMAKE_HOST_SYSTEM_PROCESSOR = "aarch64" + +[target.aarch64-apple-darwin] +rustflags = ["-C", "target-cpu=apple-m1"] + +[build] +target = "aarch64-apple-darwin" diff --git a/apps/desktop/build.rs b/apps/desktop/build.rs new file mode 100644 index 000000000..84ed04916 --- /dev/null +++ b/apps/desktop/build.rs @@ -0,0 +1,19 @@ +fn main() { + if cfg!(target_arch = "aarch64") && cfg!(target_os = "macos") { + println!("cargo:rustc-env=GGML_NATIVE=OFF"); + println!("cargo:rustc-env=CMAKE_C_FLAGS=-mcpu=apple-m1"); + println!("cargo:rustc-env=CMAKE_CXX_FLAGS=-mcpu=apple-m1"); + println!("cargo:rustc-env=CC=clang"); + println!("cargo:rustc-env=CXX=clang++"); + println!("cargo:rustc-env=CFLAGS=-mcpu=apple-m1"); + println!("cargo:rustc-env=CXXFLAGS=-mcpu=apple-m1"); + + std::env::set_var("GGML_NATIVE", "OFF"); + std::env::set_var("CMAKE_C_FLAGS", "-mcpu=apple-m1"); + std::env::set_var("CMAKE_CXX_FLAGS", "-mcpu=apple-m1"); + std::env::set_var("CC", "clang"); + std::env::set_var("CXX", "clang++"); + std::env::set_var("CFLAGS", "-mcpu=apple-m1"); + std::env::set_var("CXXFLAGS", "-mcpu=apple-m1"); + } +} From ebaa1da71ff83958e5bb8131885c1164862cba91 Mon Sep 17 00:00:00 2001 From: "Deokhaeng Lee (Duck)" Date: Sat, 14 Jun 2025 02:08:42 -0400 Subject: [PATCH 02/17] Transcription search bar (#937) --- .../right-panel/views/transcript-view.tsx | 267 +++++++++++++++--- crates/llama/Cargo.toml | 3 +- crates/whisper/Cargo.toml | 3 +- packages/tiptap/src/styles/transcript.css | 6 + 4 files changed, 232 insertions(+), 47 deletions(-) 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 3e56cdc59..32f42efa0 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 + ? ( -
- - + ) + : ( +
+
+ setSearchTerm(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Search..." + autoFocus + /> +
+ setReplaceTerm(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Replace..." + /> +
+ {searchTerm && ( +
+ + {resultCount > 0 ? `${currentIndex}/${resultCount}` : "0/0"} + +
+ + +
+
+ )} + + +
+ )} +
); } diff --git a/crates/llama/Cargo.toml b/crates/llama/Cargo.toml index b8a4eabfc..c8c0bd277 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 439239ea9..fd71e8e52 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 557cc0799..7afcfb1b5 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; From f649de80381c4208bfc9ec14f9bfbfd549c24052 Mon Sep 17 00:00:00 2001 From: "Deokhaeng Lee (Duck)" Date: Sun, 15 Jun 2025 08:17:30 -0400 Subject: [PATCH 03/17] Right panel responsiveness fix (#940) --- Cargo.lock | 4 +- .../right-panel/views/transcript-view.tsx | 130 +++++++++++++++--- crates/llama/Cargo.toml | 5 +- crates/whisper/Cargo.toml | 1 - 4 files changed, 112 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4563a9442..afee6d9ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7840,7 +7840,7 @@ dependencies = [ [[package]] name = "llama-cpp-2" version = "0.1.108" -source = "git+https://github.com/utilityai/llama-cpp-rs?branch=update-llama-cpp-2025-06-04#73707a44ce892578ad94d630b356c0ec417c7476" +source = "git+https://github.com/utilityai/llama-cpp-rs?branch=update-llama-cpp-2025-05-28#c007d3654a093a37ee1580cf66f54ce75e8efaef" dependencies = [ "enumflags2", "llama-cpp-sys-2", @@ -7852,7 +7852,7 @@ dependencies = [ [[package]] name = "llama-cpp-sys-2" version = "0.1.108" -source = "git+https://github.com/utilityai/llama-cpp-rs?branch=update-llama-cpp-2025-06-04#73707a44ce892578ad94d630b356c0ec417c7476" +source = "git+https://github.com/utilityai/llama-cpp-rs?branch=update-llama-cpp-2025-05-28#c007d3654a093a37ee1580cf66f54ce75e8efaef" dependencies = [ "bindgen 0.69.5", "cc", 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 32f42efa0..d0f74c86a 100644 --- a/apps/desktop/src/components/right-panel/views/transcript-view.tsx +++ b/apps/desktop/src/components/right-panel/views/transcript-view.tsx @@ -34,9 +34,40 @@ import { ListeningIndicator } from "../components/listening-indicator"; import { useTranscript } from "../hooks/useTranscript"; import { useTranscriptWidget } from "../hooks/useTranscriptWidget"; +function useContainerWidth(ref: React.RefObject) { + const [width, setWidth] = useState(0); + + useEffect(() => { + const element = ref.current; + if (!element) { + return; + } + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + setWidth(entry.contentRect.width); + } + }); + + resizeObserver.observe(element); + // Set initial width + setWidth(element.getBoundingClientRect().width); + + return () => { + resizeObserver.disconnect(); + }; + }, [ref]); + + return width; +} + export function TranscriptView() { const queryClient = useQueryClient(); + // Add container ref to track the panel width + const containerRef = useRef(null); + const panelWidth = useContainerWidth(containerRef); + const noteMatch = useMatch({ from: "/app/note/$id", shouldThrow: true }); const sessionId = noteMatch.params.id; @@ -98,7 +129,7 @@ export function TranscriptView() { const showActions = hasTranscript && sessionId && ongoingSession.isInactive; return ( -
+
{!showEmptyMessage && (
@@ -112,7 +143,7 @@ export function TranscriptView() {
)}
- {showActions && } + {showActions && panelWidth >= 530 && } {(audioExist.data && showActions) && ( - to see live transcript + {showFullText && to see live transcript}
-
+
or
- - + {isUltraCompact + ? ( + <> + + + + ) + : ( + <> + + + + )}
@@ -339,7 +423,10 @@ function SpeakerRangeSelector({ value, onChange }: SpeakerRangeSelectorProps) { ); } -export function SearchAndReplace({ editorRef }: { editorRef: React.RefObject }) { +export function SearchAndReplace({ editorRef, panelWidth }: { + editorRef: React.RefObject; + panelWidth: number; +}) { const [isActive, setIsActive] = useState(false); const [searchTerm, setSearchTerm] = useState(""); const [replaceTerm, setReplaceTerm] = useState(""); @@ -500,7 +587,7 @@ export function SearchAndReplace({ editorRef }: { editorRef: React.RefObject +
{!isActive ? ( diff --git a/crates/llama/Cargo.toml b/crates/llama/Cargo.toml index c8c0bd277..f0772b65d 100644 --- a/crates/llama/Cargo.toml +++ b/crates/llama/Cargo.toml @@ -18,14 +18,13 @@ serde = { workspace = true } thiserror = { workspace = true } [target.'cfg(not(target_os = "macos"))'.dependencies] -llama-cpp-2 = { git = "https://github.com/utilityai/llama-cpp-rs", default-features = false, features = ["openmp"], branch = "update-llama-cpp-2025-06-04" } +llama-cpp-2 = { git = "https://github.com/utilityai/llama-cpp-rs", default-features = false, features = ["openmp"], branch = "update-llama-cpp-2025-05-28" } [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-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" } +llama-cpp-2 = { git = "https://github.com/utilityai/llama-cpp-rs", features = ["native"], branch = "update-llama-cpp-2025-05-28" } [dev-dependencies] hypr-buffer = { workspace = true } diff --git a/crates/whisper/Cargo.toml b/crates/whisper/Cargo.toml index fd71e8e52..74c71ecf2 100644 --- a/crates/whisper/Cargo.toml +++ b/crates/whisper/Cargo.toml @@ -39,7 +39,6 @@ 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"], optional = true } [target.'cfg(all(target_os = "macos", target_arch = "x86_64"))'.dependencies] From f2242af8884a3a15530fbf5ad387e699c13127b6 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Mon, 16 Jun 2025 17:29:32 +0900 Subject: [PATCH 04/17] patch --- apps/desktop/.cargo/config.toml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 apps/desktop/.cargo/config.toml diff --git a/apps/desktop/.cargo/config.toml b/apps/desktop/.cargo/config.toml new file mode 100644 index 000000000..d144cfb4c --- /dev/null +++ b/apps/desktop/.cargo/config.toml @@ -0,0 +1,5 @@ +[target.aarch64-apple-darwin] +rustflags = ["-C", "target-cpu=apple-m1"] + +[build] +target = "aarch64-apple-darwin" From 102868128d2be9218986ab6ad34547dd0598c0d5 Mon Sep 17 00:00:00 2001 From: Deokhaeng Lee Date: Mon, 16 Jun 2025 23:23:37 -0400 Subject: [PATCH 05/17] changed searchbar ui --- .../right-panel/views/transcript-view.tsx | 306 +++++++++--------- 1 file changed, 158 insertions(+), 148 deletions(-) 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 d0f74c86a..510440b1e 100644 --- a/apps/desktop/src/components/right-panel/views/transcript-view.tsx +++ b/apps/desktop/src/components/right-panel/views/transcript-view.tsx @@ -63,6 +63,9 @@ function useContainerWidth(ref: React.RefObject) { export function TranscriptView() { const queryClient = useQueryClient(); + + // Add search state to main component + const [isSearchActive, setIsSearchActive] = useState(false); // Add container ref to track the panel width const containerRef = useRef(null); @@ -89,6 +92,22 @@ export function TranscriptView() { } }, [words, isLive]); + // Add Ctrl+F keyboard shortcut + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key === "f") { + const isTranscriptFocused = editorRef.current?.editor?.isFocused; + const currentShowActions = hasTranscript && sessionId && ongoingSession.isInactive; + if (isTranscriptFocused && currentShowActions) { + e.preventDefault(); + setIsSearchActive(true); + } + } + }; + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [hasTranscript, sessionId, ongoingSession.isInactive]); + const audioExist = useQuery( { refetchInterval: 2500, @@ -130,32 +149,50 @@ export function TranscriptView() { return (
-
- {!showEmptyMessage && ( -
-

Transcript

- {isLive && ( -
-
-
-
+ {/* Conditional Header Rendering */} + {isSearchActive ? ( + setIsSearchActive(false)} + /> + ) : ( +
+ {!showEmptyMessage && ( +
+

Transcript

+ {isLive && ( +
+
+
+
+ )} +
+ )} +
+ {/* Search Icon Button */} + {showActions && ( + )} + {(audioExist.data && showActions) && ( + + )} + {showActions && }
- )} -
- {showActions && panelWidth >= 530 && } - {(audioExist.data && showActions) && ( - - )} - {showActions && } -
-
+
+ )}
{showEmptyMessage @@ -423,18 +460,17 @@ function SpeakerRangeSelector({ value, onChange }: SpeakerRangeSelectorProps) { ); } -export function SearchAndReplace({ editorRef, panelWidth }: { +function SearchHeader({ editorRef, onClose }: { editorRef: React.RefObject; - panelWidth: number; + onClose: () => void; }) { - 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); + // Add ref for the search header container + const searchHeaderRef = useRef(null); // Debounced search term update const debouncedSetSearchTerm = useDebouncedCallback( @@ -467,45 +503,26 @@ export function SearchAndReplace({ editorRef, panelWidth }: { // 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 (searchHeaderRef.current && !searchHeaderRef.current.contains(event.target as Node)) { + handleClose(); } }; - if (isActive) { - document.addEventListener("mousedown", handleClickOutside); - } - - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, [isActive, editorRef]); + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); - // Keyboard shortcut handler - only when transcript editor is focused + // Keyboard shortcuts 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); - } + if (e.key === "Escape") { + handleClose(); } }; 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(); @@ -555,21 +572,15 @@ export function SearchAndReplace({ editorRef, panelWidth }: { } }; - const handleToggle = () => { - setIsActive(!isActive); - if (isActive && editorRef.current) { - setSearchTerm(""); - setReplaceTerm(""); - setResultCount(0); - setCurrentIndex(0); + const handleClose = () => { + if (editorRef.current) { editorRef.current.editor.commands.setSearchTerm(""); } + onClose(); }; const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Escape") { - handleToggle(); - } else if (e.key === "Enter") { + if (e.key === "Enter") { e.preventDefault(); if (e.shiftKey) { handlePrevious(); @@ -587,88 +598,87 @@ export function SearchAndReplace({ editorRef, panelWidth }: { }; return ( -
- {!isActive - ? ( - - ) - : ( -
-
- setSearchTerm(e.target.value)} - onKeyDown={handleKeyDown} - placeholder="Search..." - autoFocus - /> -
- setReplaceTerm(e.target.value)} - onKeyDown={handleKeyDown} - placeholder="Replace..." - /> -
- {searchTerm && ( -
- - {resultCount > 0 ? `${currentIndex}/${resultCount}` : "0/0"} - -
- - -
-
- )} - - -
+
+
+ {/* Search Input */} +
+ setSearchTerm(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Search..." + autoFocus + /> +
+ + {/* Replace Input */} +
+ setReplaceTerm(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Replace..." + /> +
+ + {/* Results Counter */} + {searchTerm && ( + + {resultCount > 0 ? `${currentIndex}/${resultCount}` : "0/0"} + )} -
+
+ + {/* Action Buttons */} +
+ + + + +
+ ); } From 12305915456f3d0cfe788b444f418def06935778 Mon Sep 17 00:00:00 2001 From: Deokhaeng Lee Date: Mon, 16 Jun 2025 23:57:16 -0400 Subject: [PATCH 06/17] search/replace bar finalized --- .../right-panel/views/transcript-view.tsx | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) 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 510440b1e..37726971e 100644 --- a/apps/desktop/src/components/right-panel/views/transcript-view.tsx +++ b/apps/desktop/src/components/right-panel/views/transcript-view.tsx @@ -96,12 +96,11 @@ export function TranscriptView() { useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === "f") { - const isTranscriptFocused = editorRef.current?.editor?.isFocused; const currentShowActions = hasTranscript && sessionId && ongoingSession.isInactive; - if (isTranscriptFocused && currentShowActions) { - e.preventDefault(); - setIsSearchActive(true); + if (currentShowActions) { + setIsSearchActive(true); } + } }; document.addEventListener("keydown", handleKeyDown); @@ -604,10 +603,9 @@ function SearchHeader({ editorRef, onClose }: { >
{/* Search Input */} -
+
setSearchTerm(e.target.value)} onKeyDown={handleKeyDown} @@ -617,10 +615,9 @@ function SearchHeader({ editorRef, onClose }: {
{/* Replace Input */} -
+
setReplaceTerm(e.target.value)} onKeyDown={handleKeyDown} From 699d44b274f4eba229a3f69b74c85798bc85db70 Mon Sep 17 00:00:00 2001 From: Deokhaeng Lee Date: Wed, 18 Jun 2025 02:28:40 -0400 Subject: [PATCH 07/17] fixed search/replace bar, added pre meeting notes --- .../src/components/editor-area/index.tsx | 7 +- .../right-panel/components/search-header.tsx | 231 ++++++++++++++++ .../right-panel/views/transcript-view.tsx | 252 +----------------- crates/template/assets/enhance.system.jinja | 7 + crates/template/assets/enhance.user.jinja | 4 + packages/utils/src/stores/ongoing-session.ts | 9 + packages/utils/src/stores/session.ts | 10 + 7 files changed, 278 insertions(+), 242 deletions(-) create mode 100644 apps/desktop/src/components/right-panel/components/search-header.tsx diff --git a/apps/desktop/src/components/editor-area/index.tsx b/apps/desktop/src/components/editor-area/index.tsx index 6476039a4..cf0662d97 100644 --- a/apps/desktop/src/components/editor-area/index.tsx +++ b/apps/desktop/src/components/editor-area/index.tsx @@ -53,8 +53,10 @@ 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) => { console.log("useEnhanceMutation onSuccess", content); @@ -169,15 +171,17 @@ 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 setEnhanceController = useOngoingSession((s) => s.setEnhanceController); const { persistSession, setEnhancedContent } = useSession(sessionId, (s) => ({ persistSession: s.persistSession, @@ -219,6 +223,7 @@ export function useEnhanceMutation({ "enhance.user", { type, + preEditor:preMeetingNote, editor: rawContent, words: JSON.stringify(words), participants, diff --git a/apps/desktop/src/components/right-panel/components/search-header.tsx b/apps/desktop/src/components/right-panel/components/search-header.tsx new file mode 100644 index 000000000..646bed803 --- /dev/null +++ b/apps/desktop/src/components/right-panel/components/search-header.tsx @@ -0,0 +1,231 @@ +import useDebouncedCallback from "beautiful-react-hooks/useDebouncedCallback"; +import { + ChevronDownIcon, + ChevronUpIcon, + ReplaceIcon, + XIcon, +} from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { Button } from "@hypr/ui/components/ui/button"; +import { Input } from "@hypr/ui/components/ui/input"; +interface SearchHeaderProps { + editorRef: React.RefObject; + 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(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) { + 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 ( +
+
+ {/* Search Input */} +
+ setSearchTerm(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Search..." + autoFocus + /> +
+ + {/* Replace Input */} +
+ setReplaceTerm(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Replace..." + /> +
+ + {/* Results Counter */} + {searchTerm && ( + + {resultCount > 0 ? `${currentIndex}/${resultCount}` : "0/0"} + + )} +
+ + {/* Action Buttons */} +
+ + + + +
+
+ ); +} 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 e9f96bd5d..985eedc2c 100644 --- a/apps/desktop/src/components/right-panel/views/transcript-view.tsx +++ b/apps/desktop/src/components/right-panel/views/transcript-view.tsx @@ -1,18 +1,13 @@ 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, - ChevronDownIcon, - ChevronUpIcon, ClipboardIcon, CopyIcon, - ReplaceIcon, TextSearchIcon, - UploadIcon, - XIcon, + UploadIcon } from "lucide-react"; import { memo, useCallback, useEffect, useRef, useState } from "react"; @@ -26,13 +21,13 @@ import TranscriptEditor, { type TranscriptEditorRef, } from "@hypr/tiptap/transcript"; import { Button } from "@hypr/ui/components/ui/button"; -import { Input } from "@hypr/ui/components/ui/input"; import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/popover"; import { Spinner } from "@hypr/ui/components/ui/spinner"; import { useOngoingSession } from "@hypr/utils/contexts"; import { ListeningIndicator } from "../components/listening-indicator"; import { useTranscript } from "../hooks/useTranscript"; import { useTranscriptWidget } from "../hooks/useTranscriptWidget"; +import { SearchHeader } from "../components/search-header"; function useContainerWidth(ref: React.RefObject) { const [width, setWidth] = useState(0); @@ -64,14 +59,10 @@ function useContainerWidth(ref: React.RefObject) { export function TranscriptView() { const queryClient = useQueryClient(); - // Add search state to main component + // Search state const [isSearchActive, setIsSearchActive] = useState(false); - // Add container ref to track the panel width - const containerRef = useRef(null); - const panelWidth = useContainerWidth(containerRef); - - // Add container ref to track the panel width + // Single container ref and panel width for the entire component const containerRef = useRef(null); const panelWidth = useContainerWidth(containerRef); @@ -199,7 +190,7 @@ export function TranscriptView() {
{showEmptyMessage - ? + ? : ( <> ({ start: s.start, status: s.status, loading: s.loading, })); - // Add container ref to track the panel width - const containerRef = useRef(null); - const panelWidth = useContainerWidth(containerRef); - const handleStartRecording = () => { if (ongoingSession.status === "inactive") { ongoingSession.start(sessionId); } }; - // Determine layout based on actual panel width (empty screen) + // Determine layout based on panel width passed from parent const isUltraCompact = panelWidth < 150; // Just icons const isVeryNarrow = panelWidth < 200; // Short text const isNarrow = panelWidth < 400; // No helper text const showFullText = panelWidth >= 400; // Full text return ( -
+
; - onClose: () => void; -}) { - 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(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) { - 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 ( -
-
- {/* Search Input */} -
- setSearchTerm(e.target.value)} - onKeyDown={handleKeyDown} - placeholder="Search..." - autoFocus - /> -
- - {/* Replace Input */} -
- setReplaceTerm(e.target.value)} - onKeyDown={handleKeyDown} - placeholder="Replace..." - /> -
- - {/* Results Counter */} - {searchTerm && ( - - {resultCount > 0 ? `${currentIndex}/${resultCount}` : "0/0"} - - )} -
- - {/* Action Buttons */} -
- - - - -
-
- ); -} - function CopyButton({ onCopy }: { onCopy: () => void }) { const [copied, setCopied] = useState(false); diff --git a/crates/template/assets/enhance.system.jinja b/crates/template/assets/enhance.system.jinja index de3ee54c5..617b8a1b1 100644 --- a/crates/template/assets/enhance.system.jinja +++ b/crates/template/assets/enhance.system.jinja @@ -12,11 +12,17 @@ You will be given multiple inputs from the user. Below are useful information th # Inputs Provided by the user - Meeting Information (txt) +- Pre Meeting Note (markdown) - Raw Note (markdown) - Meeting Transcript (txt) +# About Pre Meeting Notes +- Pre Meeting Notes is what user wrote before the meeting started. Therefore, it is liekly to contain information that user wants to learn from the meeting (interview questions, decisiont to be made, etc.) +- May sometimes be empty. + # About Raw Notes +- Raw Notes is what user wrote during the meeting. - The beginning of a raw note may include agenda items, discussion topics, and preliminary questions. - Primarily consist of key phrases or sentences the user wants to remember, though they may also contain random or extraneous words. - May sometimes be empty. @@ -40,6 +46,7 @@ You will be given multiple inputs from the user. Below are useful information th - Do not include meeting note title, attendee lists nor explanatory notes about the output structure. Just print a markdown document. - It is super important to acknowledge what the user found to be important, and raw notes show a glimpse of the important information as well as moments during the meeting. Naturally integrate raw note entries into relevant sections instead of forcefully converting them into headers. - Preserve essential details; avoid excessive abstraction. Ensure content remains concrete and specific. +- When creating the final report, compare pre meeting note and raw note to focus on what user mainly cares about (especially when the content of two notes are drastically different). However, the main focus should alwyas be the raw note as that was written during the actual meeting. - Pay close attention to emphasized text in raw notes. Users highlight information using four styles: bold(**text**), italic(_text_), underline(text), strikethrough(~~text~~). - Recognize H3 headers (### Header) in raw notes—these indicate highly important topics that the user wants to retain no matter what. {% if config.general.display_language is not english %} diff --git a/crates/template/assets/enhance.user.jinja b/crates/template/assets/enhance.user.jinja index d6d3efca7..6c6c25077 100644 --- a/crates/template/assets/enhance.user.jinja +++ b/crates/template/assets/enhance.user.jinja @@ -4,6 +4,10 @@ {% endfor %} + +{{ preEditor }} + + {{ editor }} diff --git a/packages/utils/src/stores/ongoing-session.ts b/packages/utils/src/stores/ongoing-session.ts index 34e1b624f..12c2fee0b 100644 --- a/packages/utils/src/stores/ongoing-session.ts +++ b/packages/utils/src/stores/ongoing-session.ts @@ -55,6 +55,7 @@ export const createOngoingSessionStore = (sessionsStore: ReturnType { + console.log("start", sessionId); set((state) => mutate(state, (draft) => { draft.sessionId = sessionId; @@ -63,7 +64,15 @@ export const createOngoingSessionStore = (sessionsStore: ReturnType { if (payload.type === "audioAmplitude") { diff --git a/packages/utils/src/stores/session.ts b/packages/utils/src/stores/session.ts index e05098d21..d089bce57 100644 --- a/packages/utils/src/stores/session.ts +++ b/packages/utils/src/stores/session.ts @@ -14,6 +14,7 @@ type Actions = { refresh: () => Promise; setShowRaw: (showRaw: boolean) => void; updateTitle: (title: string) => void; + updatePreMeetingNote: (note: string) => void; updateRawNote: (note: string) => void; updateEnhancedNote: (note: string) => void; persistSession: (session?: Session, force?: boolean) => Promise; @@ -49,6 +50,15 @@ export const createSessionStore = (session: Session) => { return next; }); }, + updatePreMeetingNote: (note: string) => { + set((state) => { + const next = mutate(state, (draft) => { + draft.session.pre_meeting_memo_html = note; + }); + get().persistSession(next.session); + return next; + }); + }, updateRawNote: (note: string) => { set((state) => { const next = mutate(state, (draft) => { From cdb5d85c2e5eb09a43cd46fb5286f54d237b6c1e Mon Sep 17 00:00:00 2001 From: Deokhaeng Lee Date: Wed, 18 Jun 2025 02:32:00 -0400 Subject: [PATCH 08/17] removed debug prints --- packages/utils/src/stores/ongoing-session.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/utils/src/stores/ongoing-session.ts b/packages/utils/src/stores/ongoing-session.ts index 12c2fee0b..3522dc015 100644 --- a/packages/utils/src/stores/ongoing-session.ts +++ b/packages/utils/src/stores/ongoing-session.ts @@ -68,7 +68,6 @@ export const createOngoingSessionStore = (sessionsStore: ReturnType Date: Wed, 18 Jun 2025 02:33:11 -0400 Subject: [PATCH 09/17] done dprint check --- .../src/components/editor-area/index.tsx | 6 +- .../right-panel/components/search-header.tsx | 13 +-- .../right-panel/views/transcript-view.tsx | 106 +++++++++--------- crates/template/assets/enhance.system.jinja | 9 +- packages/utils/src/stores/ongoing-session.ts | 2 +- 5 files changed, 63 insertions(+), 73 deletions(-) diff --git a/apps/desktop/src/components/editor-area/index.tsx b/apps/desktop/src/components/editor-area/index.tsx index cf0662d97..eeb7b9c20 100644 --- a/apps/desktop/src/components/editor-area/index.tsx +++ b/apps/desktop/src/components/editor-area/index.tsx @@ -53,7 +53,7 @@ export default function EditorArea({ ); const generateTitle = useGenerateTitleMutation({ sessionId }); - const preMeetingNote = useSession(sessionId, (s) => s.session.pre_meeting_memo_html) ?? "" + const preMeetingNote = useSession(sessionId, (s) => s.session.pre_meeting_memo_html) ?? ""; const enhance = useEnhanceMutation({ sessionId, preMeetingNote, @@ -181,7 +181,7 @@ export function useEnhanceMutation({ onSuccess: (enhancedContent: string) => void; }) { const { userId, onboardingSessionId } = useHypr(); - + const setEnhanceController = useOngoingSession((s) => s.setEnhanceController); const { persistSession, setEnhancedContent } = useSession(sessionId, (s) => ({ persistSession: s.persistSession, @@ -223,7 +223,7 @@ export function useEnhanceMutation({ "enhance.user", { type, - preEditor:preMeetingNote, + preEditor: preMeetingNote, editor: rawContent, words: JSON.stringify(words), participants, diff --git a/apps/desktop/src/components/right-panel/components/search-header.tsx b/apps/desktop/src/components/right-panel/components/search-header.tsx index 646bed803..b07c1f6b8 100644 --- a/apps/desktop/src/components/right-panel/components/search-header.tsx +++ b/apps/desktop/src/components/right-panel/components/search-header.tsx @@ -1,13 +1,8 @@ -import useDebouncedCallback from "beautiful-react-hooks/useDebouncedCallback"; -import { - ChevronDownIcon, - ChevronUpIcon, - ReplaceIcon, - XIcon, -} from "lucide-react"; -import { useEffect, useRef, useState } from "react"; 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; onClose: () => void; @@ -148,7 +143,7 @@ export function SearchHeader({ editorRef, onClose }: SearchHeaderProps) { }; return ( -
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 985eedc2c..a65b8969d 100644 --- a/apps/desktop/src/components/right-panel/views/transcript-view.tsx +++ b/apps/desktop/src/components/right-panel/views/transcript-view.tsx @@ -1,14 +1,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useMatch } from "@tanstack/react-router"; import { writeText as writeTextToClipboard } from "@tauri-apps/plugin-clipboard-manager"; -import { - AudioLinesIcon, - CheckIcon, - ClipboardIcon, - CopyIcon, - TextSearchIcon, - UploadIcon -} from "lucide-react"; +import { AudioLinesIcon, CheckIcon, ClipboardIcon, CopyIcon, TextSearchIcon, UploadIcon } from "lucide-react"; import { memo, useCallback, useEffect, useRef, useState } from "react"; import { ParticipantsChipInner } from "@/components/editor-area/note-header/chips/participants-chip"; @@ -25,9 +18,9 @@ import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/ import { Spinner } from "@hypr/ui/components/ui/spinner"; import { useOngoingSession } from "@hypr/utils/contexts"; import { ListeningIndicator } from "../components/listening-indicator"; +import { SearchHeader } from "../components/search-header"; import { useTranscript } from "../hooks/useTranscript"; import { useTranscriptWidget } from "../hooks/useTranscriptWidget"; -import { SearchHeader } from "../components/search-header"; function useContainerWidth(ref: React.RefObject) { const [width, setWidth] = useState(0); @@ -58,7 +51,7 @@ function useContainerWidth(ref: React.RefObject) { export function TranscriptView() { const queryClient = useQueryClient(); - + // Search state const [isSearchActive, setIsSearchActive] = useState(false); @@ -93,9 +86,8 @@ export function TranscriptView() { if ((e.ctrlKey || e.metaKey) && e.key === "f") { const currentShowActions = hasTranscript && sessionId && ongoingSession.isInactive; if (currentShowActions) { - setIsSearchActive(true); + setIsSearchActive(true); } - } }; document.addEventListener("keydown", handleKeyDown); @@ -144,49 +136,51 @@ export function TranscriptView() { return (
{/* Conditional Header Rendering */} - {isSearchActive ? ( - setIsSearchActive(false)} - /> - ) : ( -
- {!showEmptyMessage && ( -
-

Transcript

- {isLive && ( -
-
-
-
+ {isSearchActive + ? ( + setIsSearchActive(false)} + /> + ) + : ( +
+ {!showEmptyMessage && ( +
+

Transcript

+ {isLive && ( +
+
+
+
+ )} +
+ )} +
+ {/* Search Icon Button */} + {showActions && ( + )} + {(audioExist.data && showActions) && ( + + )} + {showActions && }
- )} -
- {/* Search Icon Button */} - {showActions && ( - - )} - {(audioExist.data && showActions) && ( - - )} - {showActions && } -
-
- )} +
+ )}
{showEmptyMessage @@ -208,9 +202,9 @@ export function TranscriptView() { ); } -function RenderEmpty({ sessionId, panelWidth }: { - sessionId: string; - panelWidth: number; +function RenderEmpty({ sessionId, panelWidth }: { + sessionId: string; + panelWidth: number; }) { const ongoingSession = useOngoingSession((s) => ({ start: s.start, diff --git a/crates/template/assets/enhance.system.jinja b/crates/template/assets/enhance.system.jinja index 617b8a1b1..2aa5ab8fb 100644 --- a/crates/template/assets/enhance.system.jinja +++ b/crates/template/assets/enhance.system.jinja @@ -16,13 +16,14 @@ You will be given multiple inputs from the user. Below are useful information th - Raw Note (markdown) - Meeting Transcript (txt) -# About Pre Meeting Notes +# About Pre Meeting Notes + - Pre Meeting Notes is what user wrote before the meeting started. Therefore, it is liekly to contain information that user wants to learn from the meeting (interview questions, decisiont to be made, etc.) -- May sometimes be empty. +- May sometimes be empty. # About Raw Notes -- Raw Notes is what user wrote during the meeting. +- Raw Notes is what user wrote during the meeting. - The beginning of a raw note may include agenda items, discussion topics, and preliminary questions. - Primarily consist of key phrases or sentences the user wants to remember, though they may also contain random or extraneous words. - May sometimes be empty. @@ -46,7 +47,7 @@ You will be given multiple inputs from the user. Below are useful information th - Do not include meeting note title, attendee lists nor explanatory notes about the output structure. Just print a markdown document. - It is super important to acknowledge what the user found to be important, and raw notes show a glimpse of the important information as well as moments during the meeting. Naturally integrate raw note entries into relevant sections instead of forcefully converting them into headers. - Preserve essential details; avoid excessive abstraction. Ensure content remains concrete and specific. -- When creating the final report, compare pre meeting note and raw note to focus on what user mainly cares about (especially when the content of two notes are drastically different). However, the main focus should alwyas be the raw note as that was written during the actual meeting. +- When creating the final report, compare pre meeting note and raw note to focus on what user mainly cares about (especially when the content of two notes are drastically different). However, the main focus should alwyas be the raw note as that was written during the actual meeting. - Pay close attention to emphasized text in raw notes. Users highlight information using four styles: bold(**text**), italic(_text_), underline(text), strikethrough(~~text~~). - Recognize H3 headers (### Header) in raw notes—these indicate highly important topics that the user wants to retain no matter what. {% if config.general.display_language is not english %} diff --git a/packages/utils/src/stores/ongoing-session.ts b/packages/utils/src/stores/ongoing-session.ts index 3522dc015..e382146af 100644 --- a/packages/utils/src/stores/ongoing-session.ts +++ b/packages/utils/src/stores/ongoing-session.ts @@ -67,7 +67,7 @@ export const createOngoingSessionStore = (sessionsStore: ReturnType Date: Wed, 18 Jun 2025 02:41:25 -0400 Subject: [PATCH 10/17] deleted build.rs file --- apps/desktop/build.rs | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 apps/desktop/build.rs diff --git a/apps/desktop/build.rs b/apps/desktop/build.rs deleted file mode 100644 index 84ed04916..000000000 --- a/apps/desktop/build.rs +++ /dev/null @@ -1,19 +0,0 @@ -fn main() { - if cfg!(target_arch = "aarch64") && cfg!(target_os = "macos") { - println!("cargo:rustc-env=GGML_NATIVE=OFF"); - println!("cargo:rustc-env=CMAKE_C_FLAGS=-mcpu=apple-m1"); - println!("cargo:rustc-env=CMAKE_CXX_FLAGS=-mcpu=apple-m1"); - println!("cargo:rustc-env=CC=clang"); - println!("cargo:rustc-env=CXX=clang++"); - println!("cargo:rustc-env=CFLAGS=-mcpu=apple-m1"); - println!("cargo:rustc-env=CXXFLAGS=-mcpu=apple-m1"); - - std::env::set_var("GGML_NATIVE", "OFF"); - std::env::set_var("CMAKE_C_FLAGS", "-mcpu=apple-m1"); - std::env::set_var("CMAKE_CXX_FLAGS", "-mcpu=apple-m1"); - std::env::set_var("CC", "clang"); - std::env::set_var("CXX", "clang++"); - std::env::set_var("CFLAGS", "-mcpu=apple-m1"); - std::env::set_var("CXXFLAGS", "-mcpu=apple-m1"); - } -} From 8084f2bc0538e83bff2973890b61cd24662997ba Mon Sep 17 00:00:00 2001 From: Deokhaeng Lee Date: Wed, 18 Jun 2025 22:56:36 -0400 Subject: [PATCH 11/17] deleting pre meeting wip --- apps/desktop/src/components/editor-area/index.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/desktop/src/components/editor-area/index.tsx b/apps/desktop/src/components/editor-area/index.tsx index eeb7b9c20..8f03ad15b 100644 --- a/apps/desktop/src/components/editor-area/index.tsx +++ b/apps/desktop/src/components/editor-area/index.tsx @@ -182,6 +182,9 @@ export function useEnhanceMutation({ }) { const { userId, onboardingSessionId } = useHypr(); + console.log("this is the preMeetingNote", preMeetingNote); + console.log("this is the rawContent", rawContent); + const setEnhanceController = useOngoingSession((s) => s.setEnhanceController); const { persistSession, setEnhancedContent } = useSession(sessionId, (s) => ({ persistSession: s.persistSession, @@ -277,6 +280,8 @@ export function useEnhanceMutation({ return text.then(miscCommands.opinionatedMdToHtml); }, onSuccess: (enhancedContent) => { + console.log("enhancing done", enhancedContent); + onSuccess(enhancedContent ?? ""); analyticsCommands.event({ From 3917df58a8ba845776cd342abf52ee596b105c67 Mon Sep 17 00:00:00 2001 From: Deokhaeng Lee Date: Fri, 20 Jun 2025 01:26:18 -0400 Subject: [PATCH 12/17] unstable commit - pdf + pre meeting --- apps/desktop/package.json | 9 + .../src-tauri/capabilities/default.json | 10 +- .../src/components/editor-area/index.tsx | 30 +- .../components/toolbar/bars/main-toolbar.tsx | 3 +- .../toolbar/buttons/share-button.tsx | 248 ++++- .../components/toolbar/utils/pdf-export.ts | 269 ++++++ crates/template/assets/enhance.system.jinja | 11 +- crates/template/assets/enhance.user.jinja | 4 - pnpm-lock.yaml | 894 +++++++++++++++++- 9 files changed, 1414 insertions(+), 64 deletions(-) create mode 100644 apps/desktop/src/components/toolbar/utils/pdf-export.ts diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 49468a855..a8718e0e0 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -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", @@ -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", @@ -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", diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json index 6c840c687..843b48487 100644 --- a/apps/desktop/src-tauri/capabilities/default.json +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -57,6 +57,13 @@ "identifier": "fs:allow-exists", "allow": [{ "path": "/Applications/*" }] }, + { + "identifier": "fs:allow-write-file", + "allow": [ + { "path": "$DOWNLOAD/*" }, + { "path": "$DOWNLOAD/**" } + ] + }, { "identifier": "http:default", "allow": [ @@ -64,6 +71,7 @@ { "url": "http://127.0.0.1:*" }, { "url": "https://**" } ] - } + }, + "dialog:allow-save" ] } diff --git a/apps/desktop/src/components/editor-area/index.tsx b/apps/desktop/src/components/editor-area/index.tsx index 8f03ad15b..7adf75a75 100644 --- a/apps/desktop/src/components/editor-area/index.tsx +++ b/apps/desktop/src/components/editor-area/index.tsx @@ -4,6 +4,7 @@ import usePreviousValue from "beautiful-react-hooks/usePreviousValue"; import { motion } from "motion/react"; import { AnimatePresence } from "motion/react"; import { useCallback, useEffect, useMemo, useRef } from "react"; +import { diffLines, diffWords, diffChars } from 'diff'; import { useHypr } from "@/contexts"; import { commands as analyticsCommands } from "@hypr/plugin-analytics"; @@ -20,6 +21,7 @@ import { useOngoingSession, useSession } from "@hypr/utils/contexts"; import { enhanceFailedToast } from "../toast/shared"; import { FloatingButton } from "./floating-button"; import { NoteHeader } from "./note-header"; +import { extractTextFromHtml } from "@/utils/parse"; export default function EditorArea({ editable, @@ -54,6 +56,8 @@ export default function EditorArea({ const generateTitle = useGenerateTitleMutation({ sessionId }); const preMeetingNote = useSession(sessionId, (s) => s.session.pre_meeting_memo_html) ?? ""; + + const enhance = useEnhanceMutation({ sessionId, preMeetingNote, @@ -182,8 +186,21 @@ export function useEnhanceMutation({ }) { const { userId, onboardingSessionId } = useHypr(); - console.log("this is the preMeetingNote", preMeetingNote); - console.log("this is the rawContent", rawContent); + 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) => ({ @@ -217,6 +234,7 @@ export function useEnhanceMutation({ const config = await dbCommands.getConfig(); const participants = await dbCommands.sessionListParticipants(sessionId); + console.log("type", type); const systemMessage = await templateCommands.render( "enhance.system", { config, type }, @@ -226,13 +244,16 @@ export function useEnhanceMutation({ "enhance.user", { type, - preEditor: preMeetingNote, - 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); @@ -280,7 +301,6 @@ export function useEnhanceMutation({ return text.then(miscCommands.opinionatedMdToHtml); }, onSuccess: (enhancedContent) => { - console.log("enhancing done", enhancedContent); onSuccess(enhancedContent ?? ""); diff --git a/apps/desktop/src/components/toolbar/bars/main-toolbar.tsx b/apps/desktop/src/components/toolbar/bars/main-toolbar.tsx index 03f3b7b93..da75ad629 100644 --- a/apps/desktop/src/components/toolbar/bars/main-toolbar.tsx +++ b/apps/desktop/src/components/toolbar/bars/main-toolbar.tsx @@ -4,6 +4,7 @@ import { useMatch } from "@tanstack/react-router"; import { DeleteNoteButton } from "@/components/toolbar/buttons/delete-note-button"; import { NewNoteButton } from "@/components/toolbar/buttons/new-note-button"; import { NewWindowButton } from "@/components/toolbar/buttons/new-window-button"; +import { ShareButton } from "@/components/toolbar/buttons/share-button"; import { useLeftSidebar } from "@/contexts"; import { commands as flagsCommands } from "@hypr/plugin-flags"; import { getCurrentWebviewWindowLabel } from "@hypr/plugin-windows"; @@ -50,6 +51,7 @@ export function MainToolbar() { + )}
@@ -62,7 +64,6 @@ export function MainToolbar() { > {isMain && ( <> - {/* {isNote && } */} {(organizationMatch || humanMatch) && } {noteChatQuery.data && } diff --git a/apps/desktop/src/components/toolbar/buttons/share-button.tsx b/apps/desktop/src/components/toolbar/buttons/share-button.tsx index 514fc0f01..e1997baf9 100644 --- a/apps/desktop/src/components/toolbar/buttons/share-button.tsx +++ b/apps/desktop/src/components/toolbar/buttons/share-button.tsx @@ -1,33 +1,153 @@ -import { Share2Icon } from "lucide-react"; +import { Share2Icon, FileText, X } from "lucide-react"; import { useState } from "react"; +import { useParams } from "@tanstack/react-router"; -import ShareAndPermissionPanel from "@/components/share-and-permission"; import { Button } from "@hypr/ui/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/popover"; +import { Modal, ModalBody, ModalHeader, ModalTitle } from "@hypr/ui/components/ui/modal"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@hypr/ui/components/ui/select"; +import { useSession } from "@hypr/utils/contexts"; +import { exportToPDF } from "../utils/pdf-export"; + +// Slack Logo Component with official colors +const SlackIcon = ({ className }: { className?: string }) => ( + + + + + + + + + + +); export function ShareButton() { - const [email, setEmail] = useState(""); + const param = useParams({ from: "/app/note/$id", shouldThrow: false }); + return param ? : null; +} + +function ShareButtonInNote() { + const param = useParams({ from: "/app/note/$id", shouldThrow: true }); const [open, setOpen] = useState(false); + const [slackModalOpen, setSlackModalOpen] = useState(false); + const [isExportingPDF, setIsExportingPDF] = useState(false); + + // Slack form state + const [selectedWorkspace, setSelectedWorkspace] = useState('Atlassian'); + const [selectedChannel, setSelectedChannel] = useState(''); + const [message, setMessage] = useState(''); + const [isSharing, setIsSharing] = useState(false); + + const session = useSession(param.id, (s) => s.session); + const hasEnhancedNote = !!session?.enhanced_memo_html; - const currentUser = { - name: "John Jeong", - email: "john@fastrepl.com", - avatarUrl: "", + const handleShareToSlack = () => { + setOpen(false); // Close the popover + setSlackModalOpen(true); // Open the modal }; - const participants = [ - currentUser, - { - name: "Alice Smith", - email: "alice@fastrepl.com", - avatarUrl: "", - }, - ]; + const handleCloseSlackModal = () => { + setSlackModalOpen(false); + // Reset form state + setSelectedChannel(''); + setMessage(''); + setIsSharing(false); + }; + + const handleSlackShare = async () => { + if (!selectedChannel) { + alert('Please select a channel'); + return; + } + + setIsSharing(true); + + try { + // TODO: Implement actual Slack API call + console.log('Sharing to Slack:', { + workspace: selectedWorkspace, + channel: selectedChannel, + message, + content: session?.enhanced_memo_html + }); + + // Simulate API call + await new Promise(resolve => setTimeout(resolve, 1500)); + + alert('Successfully shared to Slack!'); + handleCloseSlackModal(); + + } catch (error) { + console.error('Failed to share to Slack:', error); + alert('Failed to share to Slack. Please try again.'); + } finally { + setIsSharing(false); + } + }; + + const handleExportToPDF = async () => { + if (!session) return; + + setIsExportingPDF(true); + setOpen(false); // Close popover + + try { + const filename = await exportToPDF(session as any); + alert(`PDF saved to Downloads folder: ${filename}`); + } catch (error) { + console.error('Failed to export PDF:', error); + alert('Failed to export PDF. Please try again.'); + } finally { + setIsExportingPDF(false); + } + }; + + const renderMainView = () => ( +
+ {/* Header */} +
+

Share Enhanced Note

+

Share your AI-enhanced meeting notes

+
+ + {/* Share Actions */} +
+ {/* + + */} + + +
+
+ ); return ( + <> +
+ + + +
+ {/* Workspace Selection */} +
+ + +
+ + {/* Channel Selection */} +
+ + +
+ + {/* Message */} +
+ +