Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/desktop/public/assets/transcript-edit.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/desktop/public/assets/waving.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 49 additions & 1 deletion apps/desktop/src/components/editor-area/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> {
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,
Expand Down Expand Up @@ -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 ?? "",
Expand Down Expand Up @@ -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);
}
},
});

Expand Down
213 changes: 213 additions & 0 deletions apps/desktop/src/components/tips-modal/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<div className="fixed inset-0 z-50 bg-black/25 backdrop-blur-sm" onClick={handleClose} />

<Modal
open={isOpen}
onClose={handleClose}
size="md"
showOverlay={false}
className="bg-background w-[560px] max-w-[90vw]"
>
<div className="relative">
<Button
variant="ghost"
size="icon"
onClick={handleClose}
className="absolute top-2 right-2 z-10 h-8 w-8 rounded-full hover:bg-neutral-100 text-neutral-500 hover:text-neutral-700 transition-colors"
>
<X className="h-4 w-4" />
</Button>

<ModalBody className="p-5">
<div className="mb-4 text-center">
<ModalTitle className="text-xl font-semibold text-foreground">
{currentTip.title}
</ModalTitle>
</div>

<ModalDescription className="text-neutral-600 text-sm text-center mb-4">
{currentTip.description}
</ModalDescription>

{/* Image/GIF placeholder */}
<div className="flex justify-center mb-4">
{currentSlide === 0
? (
<img
src="/assets/waving.gif"
alt="Celebration animation"
className="w-48 h-36 object-contain rounded-md"
/>
)
: currentSlide === 1
? (
<img
src="/assets/transcript-edit.gif"
alt="Transcript editing demonstration"
className="w-full max-w-lg h-64 object-cover rounded-md"
style={{ objectPosition: "center top" }}
/>
)
: currentSlide === 2
? (
<img
src="/assets/transcription-setting.gif"
alt="Transcription settings demonstration"
className="w-full max-w-lg h-64 object-cover rounded-md"
style={{ objectPosition: "center top" }}
/>
)
: (
<img
src="/assets/intelligence-setting.gif"
alt="Intelligence settings demonstration"
className="w-full max-w-lg h-64 object-cover rounded-md"
style={{ objectPosition: "center top" }}
/>
)}
</div>

{/* Slide indicator dots */}
<div className="flex justify-center mb-4">
{tips.map((_, index) => (
<div
key={index}
className={`w-2 h-2 rounded-full mx-1 transition-colors ${
index === currentSlide ? "bg-black" : "bg-neutral-300"
}`}
/>
))}
</div>

{/* Navigation buttons */}
<div className="flex justify-between items-center">
<Button
variant="outline"
onClick={handlePrevious}
disabled={isFirstSlide}
className="flex items-center gap-2"
>
<ChevronLeft className="h-4 w-4" />
Previous
</Button>

{isLastSlide
? (
<Button
onClick={handleComplete}
className="bg-black text-white hover:bg-neutral-800"
>
Got it!
</Button>
)
: (
<Button
onClick={handleNext}
className="flex items-center gap-2 bg-black text-white hover:bg-neutral-800"
>
{isFirstSlide ? "Show Tips!" : "Next"}
<ChevronRight className="h-4 w-4" />
</Button>
)}
</div>
</ModalBody>
</div>
</Modal>
</>
);
}

export type { TipSlide, TipsModalProps } from "./types";
26 changes: 26 additions & 0 deletions apps/desktop/src/components/tips-modal/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from "react";
import { createRoot } from "react-dom/client";
import { TipsModal } from "./index";

export function showTipsModal(userId?: string): Promise<void> {
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,
}),
);
});
}
10 changes: 10 additions & 0 deletions apps/desktop/src/components/tips-modal/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface TipsModalProps {
isOpen: boolean;
onClose: () => void;
userId?: string;
}

export type TipSlide = {
title: string;
description: string;
};
Loading