diff --git a/apps/desktop/public/assets/intelligence-setting.gif b/apps/desktop/public/assets/intelligence-setting.gif new file mode 100644 index 0000000000..f4f5a31bf1 Binary files /dev/null and b/apps/desktop/public/assets/intelligence-setting.gif differ diff --git a/apps/desktop/public/assets/transcript-edit.gif b/apps/desktop/public/assets/transcript-edit.gif new file mode 100644 index 0000000000..3f47cdf26a Binary files /dev/null and b/apps/desktop/public/assets/transcript-edit.gif differ diff --git a/apps/desktop/public/assets/transcription-setting.gif b/apps/desktop/public/assets/transcription-setting.gif new file mode 100644 index 0000000000..dbc22b0780 Binary files /dev/null and b/apps/desktop/public/assets/transcription-setting.gif differ diff --git a/apps/desktop/public/assets/waving.gif b/apps/desktop/public/assets/waving.gif new file mode 100644 index 0000000000..f9b0547b07 Binary files /dev/null and b/apps/desktop/public/assets/waving.gif differ diff --git a/apps/desktop/src/components/editor-area/index.tsx b/apps/desktop/src/components/editor-area/index.tsx index a52e688f91..acd237d0d7 100644 --- a/apps/desktop/src/components/editor-area/index.tsx +++ b/apps/desktop/src/components/editor-area/index.tsx @@ -30,6 +30,40 @@ import { NoteHeader } from "./note-header"; import { TextSelectionPopover } from "./text-selection-popover"; import { prepareContextText } from "./utils/summary-prepare"; +const TIPS_MODAL_SHOWN_KEY = "hypr-tips-modal-shown-v1"; + +async function shouldShowTipsModal( + userId: string, + onboardingSessionId: string, + thankYouSessionId: string, +): Promise { + try { + const hasSeenTips = localStorage.getItem(TIPS_MODAL_SHOWN_KEY) === "true"; + if (hasSeenTips) { + return false; + } + + const allSessions = await dbCommands.listSessions({ + type: "recentlyVisited", + user_id: userId, + limit: 255, + }); + + const enhancedSessionsCount = allSessions.filter(session => + session.id !== onboardingSessionId + && session.id !== thankYouSessionId + && session.enhanced_memo_html + && session.enhanced_memo_html.trim() !== "" + ).length; + + return enhancedSessionsCount === 1; + } catch (error) { + console.error("Failed to check if tips modal should be shown:", error); + return false; + } +} +import { showTipsModal } from "../tips-modal/service"; + async function generateTitleDirect( enhancedContent: string, targetSessionId: string, @@ -90,7 +124,7 @@ export default function EditorArea({ sessionId: string; }) { const showRaw = useSession(sessionId, (s) => s.showRaw); - const { userId, onboardingSessionId } = useHypr(); + const { userId, onboardingSessionId, thankYouSessionId } = useHypr(); const [rawContent, setRawContent] = useSession(sessionId, (s) => [ s.session?.raw_memo_html ?? "", @@ -152,6 +186,20 @@ export default function EditorArea({ if (hasTranscriptWords) { generateTitleDirect(content, sessionId, sessionsStore, queryClient).catch(console.error); } + + if (sessionId !== onboardingSessionId) { + setTimeout(async () => { + try { + const shouldShow = await shouldShowTipsModal(userId, onboardingSessionId, thankYouSessionId); + if (shouldShow) { + localStorage.setItem(TIPS_MODAL_SHOWN_KEY, "true"); + showTipsModal(userId); + } + } catch (error) { + console.error("Failed to show tips modal:", error); + } + }, 1200); + } }, }); diff --git a/apps/desktop/src/components/tips-modal/index.tsx b/apps/desktop/src/components/tips-modal/index.tsx new file mode 100644 index 0000000000..e524a17e40 --- /dev/null +++ b/apps/desktop/src/components/tips-modal/index.tsx @@ -0,0 +1,213 @@ +import { ChevronLeft, ChevronRight, X } from "lucide-react"; +import { useEffect, useState } from "react"; + +import { commands as analyticsCommands } from "@hypr/plugin-analytics"; +import { Button } from "@hypr/ui/components/ui/button"; +import { Modal, ModalBody, ModalDescription, ModalTitle } from "@hypr/ui/components/ui/modal"; +import type { TipSlide, TipsModalProps } from "./types"; + +const tips: TipSlide[] = [ + { + title: "Hooray for your first meeting summarization!", + description: "We prepared some pro tips for you! Interested?", + }, + { + title: "Edit Transcript", + description: + "If you are not satisfied with the transcript quality, you can freely edit it and replace identified speakers to improve accuracy.", + }, + { + title: "Transcript Settings", + description: "You can choose which AI model to use for meeting transcriptions in Settings → Transcription tab.", + }, + { + title: "Intelligence Settings", + description: + "You can choose which AI model to use for meeting summarization and chat in Settings → Intelligence tab.", + }, +]; + +export function TipsModal({ isOpen, onClose, userId }: TipsModalProps) { + const [currentSlide, setCurrentSlide] = useState(0); + + const handleNext = () => { + if (currentSlide < tips.length - 1) { + setCurrentSlide(currentSlide + 1); + } + }; + + const handlePrevious = () => { + if (currentSlide > 0) { + setCurrentSlide(currentSlide - 1); + } + }; + + const handleClose = () => { + if (userId) { + analyticsCommands.event({ + event: "tips_modal_dismiss", + distinct_id: userId, + }); + } + setCurrentSlide(0); + onClose(); + }; + + const handleComplete = () => { + if (userId) { + analyticsCommands.event({ + event: "tips_modal_complete", + distinct_id: userId, + }); + } + setCurrentSlide(0); + onClose(); + }; + + // Track slide views + useEffect(() => { + if (userId) { + const events = [ + "tips_modal_intro_show", + "tips_modal_transcript_show", + "tips_modal_transcription_show", + "tips_modal_intelligence_show", + ]; + + const event = events[currentSlide]; + if (event) { + analyticsCommands.event({ + event, + distinct_id: userId, + }); + } + } else { + console.error("no userId available for analytics"); + } + }, [currentSlide, userId]); + + const currentTip = tips[currentSlide]; + const isFirstSlide = currentSlide === 0; + const isLastSlide = currentSlide === tips.length - 1; + + return ( + <> +
+ + +
+ + + +
+ + {currentTip.title} + +
+ + + {currentTip.description} + + + {/* Image/GIF placeholder */} +
+ {currentSlide === 0 + ? ( + Celebration animation + ) + : currentSlide === 1 + ? ( + Transcript editing demonstration + ) + : currentSlide === 2 + ? ( + Transcription settings demonstration + ) + : ( + Intelligence settings demonstration + )} +
+ + {/* Slide indicator dots */} +
+ {tips.map((_, index) => ( +
+ ))} +
+ + {/* Navigation buttons */} +
+ + + {isLastSlide + ? ( + + ) + : ( + + )} +
+ +
+ + + ); +} + +export type { TipSlide, TipsModalProps } from "./types"; diff --git a/apps/desktop/src/components/tips-modal/service.ts b/apps/desktop/src/components/tips-modal/service.ts new file mode 100644 index 0000000000..3d1f41bf97 --- /dev/null +++ b/apps/desktop/src/components/tips-modal/service.ts @@ -0,0 +1,26 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import { TipsModal } from "./index"; + +export function showTipsModal(userId?: string): Promise { + return new Promise((resolve) => { + const modalDiv = document.createElement("div"); + document.body.appendChild(modalDiv); + + const root = createRoot(modalDiv); + + const handleClose = () => { + root.unmount(); + document.body.removeChild(modalDiv); + resolve(); + }; + + root.render( + React.createElement(TipsModal, { + isOpen: true, + onClose: handleClose, + userId: userId, + }), + ); + }); +} diff --git a/apps/desktop/src/components/tips-modal/types.ts b/apps/desktop/src/components/tips-modal/types.ts new file mode 100644 index 0000000000..c2c2e1fa27 --- /dev/null +++ b/apps/desktop/src/components/tips-modal/types.ts @@ -0,0 +1,10 @@ +export interface TipsModalProps { + isOpen: boolean; + onClose: () => void; + userId?: string; +} + +export type TipSlide = { + title: string; + description: string; +};