diff --git a/Cargo.lock b/Cargo.lock index 5f9f14fa4..c909f16bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16150,8 +16150,8 @@ dependencies = [ [[package]] name = "whisper-rs" -version = "0.14.2" -source = "git+https://github.com/tazz4843/whisper-rs?rev=de30f9c#de30f9c23da52c81b06fa78bab005ab353d74637" +version = "0.14.3" +source = "git+https://github.com/tazz4843/whisper-rs?rev=e3d67d5#e3d67d513b86c6360f62e1f563bc4601c87946d6" dependencies = [ "tracing", "whisper-rs-sys", @@ -16159,8 +16159,8 @@ dependencies = [ [[package]] name = "whisper-rs-sys" -version = "0.12.1" -source = "git+https://github.com/tazz4843/whisper-rs?rev=de30f9c#de30f9c23da52c81b06fa78bab005ab353d74637" +version = "0.13.0" +source = "git+https://github.com/tazz4843/whisper-rs?rev=e3d67d5#e3d67d513b86c6360f62e1f563bc4601c87946d6" dependencies = [ "bindgen 0.71.1", "cfg-if", 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" 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 dc8111045..c23f4ae9b 100644 --- a/apps/desktop/src/components/editor-area/index.tsx +++ b/apps/desktop/src/components/editor-area/index.tsx @@ -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"; @@ -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 }); @@ -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, @@ -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); 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..b07c1f6b8 --- /dev/null +++ b/apps/desktop/src/components/right-panel/components/search-header.tsx @@ -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; + 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 d0f74c86a..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,19 +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 useDebouncedCallback from "beautiful-react-hooks/useDebouncedCallback"; -import { - AudioLinesIcon, - CheckIcon, - ChevronDownIcon, - ChevronUpIcon, - ClipboardIcon, - CopyIcon, - ReplaceIcon, - TextSearchIcon, - UploadIcon, - XIcon, -} 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"; @@ -26,11 +14,11 @@ 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 { SearchHeader } from "../components/search-header"; import { useTranscript } from "../hooks/useTranscript"; import { useTranscriptWidget } from "../hooks/useTranscriptWidget"; @@ -64,7 +52,10 @@ function useContainerWidth(ref: React.RefObject) { export function TranscriptView() { const queryClient = useQueryClient(); - // Add container ref to track the panel width + // Search state + const [isSearchActive, setIsSearchActive] = useState(false); + + // Single container ref and panel width for the entire component const containerRef = useRef(null); const panelWidth = useContainerWidth(containerRef); @@ -89,6 +80,20 @@ export function TranscriptView() { } }, [words, isLive]); + // Add Ctrl+F keyboard shortcut + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key === "f") { + const currentShowActions = hasTranscript && sessionId && ongoingSession.isInactive; + if (currentShowActions) { + setIsSearchActive(true); + } + } + }; + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [hasTranscript, sessionId, ongoingSession.isInactive]); + const audioExist = useQuery( { refetchInterval: 2500, @@ -130,36 +135,56 @@ 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 - ? + ? : ( <> ({ 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 ( -
+
; - panelWidth: number; -}) { - 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); - 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 (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(); - setTimeout(() => { - const storage = editorRef.current.editor.storage.searchAndReplace; - const results = storage.results || []; - setResultCount(results.length); - setCurrentIndex(results.length > 0 ? 1 : 0); - }, 100); - } - }; - - 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(); - } - } - }; - - 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"} - -
- - -
-
- )} - - -
- )} -
- ); -} - function CopyButton({ onCopy }: { onCopy: () => void }) { const [copied, setCopied] = useState(false); 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..4b35ee370 100644 --- a/apps/desktop/src/components/toolbar/buttons/share-button.tsx +++ b/apps/desktop/src/components/toolbar/buttons/share-button.tsx @@ -1,52 +1,288 @@ -import { Share2Icon } from "lucide-react"; +import { useParams } from "@tanstack/react-router"; +import { FileText, Share2Icon, X } from "lucide-react"; import { useState } from "react"; -import ShareAndPermissionPanel from "@/components/share-and-permission"; import { Button } from "@hypr/ui/components/ui/button"; +import { Modal, ModalBody, ModalHeader, ModalTitle } from "@hypr/ui/components/ui/modal"; import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/popover"; +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 currentUser = { - name: "John Jeong", - email: "john@fastrepl.com", - avatarUrl: "", + 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 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); + 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 ( - - - - +
+ ); + + return ( + <> + + + + + + {renderMainView()} + + + + {/* Slack Share Modal */} + - - - + +
+
+ + Share to Slack +
+ +
+
+ + +
+ {/* Workspace Selection */} +
+ + +
+ + {/* Channel Selection */} +
+ + +
+ + {/* Message */} +
+ +