Skip to content
Closed
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
5 changes: 3 additions & 2 deletions apps/desktop/src/components/editor-area/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,12 @@ async function generateTitleDirect(

for (const tagName of suggestedTags.slice(0, 2)) {
try {
const existingTag = existingTagsMap.get(tagName.toLowerCase());
const cleanedTagName = tagName.startsWith("@") ? tagName.slice(1) : tagName;
const existingTag = existingTagsMap.get(cleanedTagName.toLowerCase());

const tag = await dbCommands.upsertTag({
id: existingTag?.id || crypto.randomUUID(),
name: tagName,
name: cleanedTagName,
});
Comment on lines +84 to 90
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Normalize suggested tag names more robustly and avoid empty tags

  • Strip multiple leading “@” and trim whitespace.
  • Skip creating a tag when the cleaned name becomes empty (e.g., tag "@").
  • De-dup against legacy “@tag” entries to prevent duplicates after normalization.

Apply this diff:

-            const cleanedTagName = tagName.startsWith("@") ? tagName.slice(1) : tagName;
-            const existingTag = existingTagsMap.get(cleanedTagName.toLowerCase());
+            const cleanedTagName = tagName.replace(/^@+/, "").trim();
+            if (!cleanedTagName) {
+              continue;
+            }
+            const existingTag =
+              existingTagsMap.get(cleanedTagName.toLowerCase())
+              // fallback: match legacy '@tag' entries to avoid duplicates
+              || existingTagsMap.get(("@"+cleanedTagName).toLowerCase());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const cleanedTagName = tagName.startsWith("@") ? tagName.slice(1) : tagName;
const existingTag = existingTagsMap.get(cleanedTagName.toLowerCase());
const tag = await dbCommands.upsertTag({
id: existingTag?.id || crypto.randomUUID(),
name: tagName,
name: cleanedTagName,
});
const cleanedTagName = tagName.replace(/^@+/, "").trim();
if (!cleanedTagName) {
continue;
}
const existingTag =
existingTagsMap.get(cleanedTagName.toLowerCase())
// fallback: match legacy '@tag' entries to avoid duplicates
|| existingTagsMap.get(("@"+cleanedTagName).toLowerCase());
const tag = await dbCommands.upsertTag({
id: existingTag?.id || crypto.randomUUID(),
name: cleanedTagName,
});
🤖 Prompt for AI Agents
In apps/desktop/src/components/editor-area/index.tsx around lines 84–90,
normalize the incoming tagName by removing all leading '@' characters and
trimming whitespace (e.g., tagName.replace(/^@+/, '').trim()), then if the
cleaned name is empty skip creating/upserting the tag; for deduplication also
check existingTagsMap for both cleanedName.toLowerCase() and ('@' +
cleanedName).toLowerCase() to find legacy entries and reuse their id when
calling dbCommands.upsertTag (generate a new id only when no existing tag is
found).


await dbCommands.assignTagToSession(tag.id, targetSessionId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ interface ShareChipProps {

export function ShareChip({ isVeryNarrow = false }: ShareChipProps) {
const [open, setOpen] = useState(false);
const { hasEnhancedNote, handleOpenStateChange } = useShareLogic();
const { hasShareableNote, shareTitle, handleOpenStateChange } = useShareLogic();

const handleOpenChange = (newOpen: boolean) => {
setOpen(newOpen);
if (hasEnhancedNote) {
if (hasShareableNote) {
handleOpenStateChange(newOpen);
}
};
Expand All @@ -35,7 +35,7 @@ export function ShareChip({ isVeryNarrow = false }: ShareChipProps) {
align="start"
sideOffset={7}
>
{hasEnhancedNote ? <SharePopoverContent /> : <SharePlaceholderContent />}
{hasShareableNote ? <SharePopoverContent shareTitle={shareTitle} /> : <SharePlaceholderContent />}
</PopoverContent>
</Popover>
);
Expand All @@ -44,16 +44,17 @@ export function ShareChip({ isVeryNarrow = false }: ShareChipProps) {
function SharePlaceholderContent() {
return (
<div className="flex flex-col gap-3">
<div className="text-sm font-medium text-neutral-700">Share Enhanced Note</div>
<div className="text-sm font-medium text-neutral-700">Share Note</div>
<div className="flex flex-col items-center justify-center py-8 px-4 text-center">
<div className="w-12 h-12 rounded-full bg-neutral-100 flex items-center justify-center mb-4">
<TextSelect size={24} className="text-neutral-400" />
</div>
<h3 className="text-sm font-medium text-neutral-900 mb-2">
Enhanced Note Required
No Content Available
</h3>
<p className="text-xs text-neutral-500 leading-relaxed">
Complete your meeting to generate an enhanced note, then share it via PDF, email, Obsidian, and more.
Start taking notes or record a meeting to have content available for sharing via PDF, email, Obsidian, and
more.
</p>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,13 @@ interface ObsidianFolder {
}

const exportHandlers = {
copy: async (session: Session): Promise<ExportResult> => {
copy: async (session: Session, isViewingRaw: boolean = false): Promise<ExportResult> => {
try {
let textToCopy = "";

if (session.enhanced_memo_html) {
if (isViewingRaw && session.raw_memo_html) {
textToCopy = html2md(session.raw_memo_html);
} else if (!isViewingRaw && session.enhanced_memo_html) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enhanced note is skipped when isViewingRaw is true but raw content is missing, leading to empty copy operation.

Prompt for AI agents
Address the following comment on apps/desktop/src/components/editor-area/note-header/share-button-header.tsx at line 60:

<comment>Enhanced note is skipped when isViewingRaw is true but raw content is missing, leading to empty copy operation.</comment>

<file context>
@@ -51,11 +51,13 @@ interface ObsidianFolder {
 }
 
 const exportHandlers = {
-  copy: async (session: Session): Promise&lt;ExportResult&gt; =&gt; {
+  copy: async (session: Session, isViewingRaw: boolean = false): Promise&lt;ExportResult&gt; =&gt; {
     try {
       let textToCopy = &quot;&quot;;
 
-      if (session.enhanced_memo_html) {
</file context>
Suggested change
} else if (!isViewingRaw && session.enhanced_memo_html) {
} else if (session.enhanced_memo_html) {

textToCopy = html2md(session.enhanced_memo_html);
} else if (session.raw_memo_html) {
textToCopy = html2md(session.raw_memo_html);
Expand All @@ -71,8 +73,8 @@ const exportHandlers = {
}
},

pdf: async (session: Session, theme: ThemeName = "default"): Promise<ExportResult> => {
const path = await exportToPDF(session, theme);
pdf: async (session: Session, theme: ThemeName = "default", isViewingRaw: boolean = false): Promise<ExportResult> => {
const path = await exportToPDF(session, theme, isViewingRaw);
if (path) {
await message(`Meeting summary saved to your 'Downloads' folder ("${path}")`);
}
Expand All @@ -82,10 +84,13 @@ const exportHandlers = {
email: async (
session: Session,
sessionParticipants?: Array<{ full_name: string | null; email: string | null }>,
isViewingRaw: boolean = false,
): Promise<ExportResult> => {
let bodyContent = "Here is the meeting summary: \n\n";

if (session.enhanced_memo_html) {
if (isViewingRaw && session.raw_memo_html) {
bodyContent += html2md(session.raw_memo_html);
} else if (!isViewingRaw && session.enhanced_memo_html) {
bodyContent += html2md(session.enhanced_memo_html);
} else if (session.raw_memo_html) {
bodyContent += html2md(session.raw_memo_html);
Expand Down Expand Up @@ -126,6 +131,7 @@ const exportHandlers = {
sessionTags: Tag[] | undefined,
sessionParticipants: Array<{ full_name: string | null }> | undefined,
includeTranscript: boolean = false,
isViewingRaw: boolean = false,
): Promise<ExportResult> => {
const [baseFolder, apiKey, baseUrl] = await Promise.all([
obsidianCommands.getBaseFolder(),
Expand All @@ -148,7 +154,14 @@ const exportHandlers = {
finalPath = await join(selectedFolder, filename);
}

let convertedMarkdown = session.enhanced_memo_html ? html2md(session.enhanced_memo_html) : "";
let convertedMarkdown = "";
if (isViewingRaw && session.raw_memo_html) {
convertedMarkdown = html2md(session.raw_memo_html);
} else if (!isViewingRaw && session.enhanced_memo_html) {
convertedMarkdown = html2md(session.enhanced_memo_html);
} else if (session.raw_memo_html) {
convertedMarkdown = html2md(session.raw_memo_html);
}

// Add transcript if requested
if (includeTranscript && session.words && session.words.length > 0) {
Expand Down Expand Up @@ -328,13 +341,20 @@ export function useShareLogic() {
const { userId } = useHypr();
const param = useParams({ from: "/app/note/$id", shouldThrow: true });
const session = useSession(param.id, (s) => s.session);
const showRaw = useSession(param.id, (s) => s.showRaw);

const [expandedId, setExpandedId] = useState<string | null>(null);
const [selectedObsidianFolder, setSelectedObsidianFolder] = useState<string>("default");
const [selectedPdfTheme, setSelectedPdfTheme] = useState<ThemeName>("default");
const [includeTranscript, setIncludeTranscript] = useState(false);
const [copySuccess, setCopySuccess] = useState(false);

// Determine what content is available and what to share
const hasEnhancedNote = !!session?.enhanced_memo_html;
const hasRawNote = !!session?.raw_memo_html;
const hasShareableNote = hasEnhancedNote || hasRawNote;
const isViewingRaw = showRaw || !hasEnhancedNote;
const shareTitle = isViewingRaw ? "Share Raw Note" : "Share Enhanced Note";

const isObsidianConfigured = useQuery({
queryKey: ["integration", "obsidian", "enabled"],
Expand Down Expand Up @@ -431,17 +451,17 @@ export function useShareLogic() {
let result: ExportResult | null = null;

if (optionId === "copy") {
result = await exportHandlers.copy(session);
result = await exportHandlers.copy(session, isViewingRaw);
} else if (optionId === "pdf") {
result = await exportHandlers.pdf(session, selectedPdfTheme);
result = await exportHandlers.pdf(session, selectedPdfTheme, isViewingRaw);
} else if (optionId === "email") {
try {
// fetch participants directly, bypassing cache
const freshParticipants = await dbCommands.sessionListParticipants(param.id);
result = await exportHandlers.email(session, freshParticipants);
result = await exportHandlers.email(session, freshParticipants, isViewingRaw);
} catch (participantError) {
console.warn("Failed to fetch participants, sending email without them:", participantError);
result = await exportHandlers.email(session, undefined);
result = await exportHandlers.email(session, undefined, isViewingRaw);
}
} else if (optionId === "obsidian") {
sessionTags.refetch();
Expand All @@ -466,6 +486,7 @@ export function useShareLogic() {
sessionTagsData,
sessionParticipantsData,
includeTranscript,
isViewingRaw,
);
}

Expand Down Expand Up @@ -531,6 +552,9 @@ export function useShareLogic() {
return {
session,
hasEnhancedNote,
hasShareableNote,
shareTitle,
isViewingRaw,
expandedId,
selectedObsidianFolder,
setSelectedObsidianFolder,
Expand All @@ -552,8 +576,9 @@ export function useShareLogic() {
}

// Reusable Share Content Component
export function SharePopoverContent() {
export function SharePopoverContent({ shareTitle: propShareTitle }: { shareTitle?: string }) {
const {
shareTitle,
expandedId,
selectedObsidianFolder,
setSelectedObsidianFolder,
Expand All @@ -572,7 +597,7 @@ export function SharePopoverContent() {

return (
<div className="flex flex-col gap-3">
<div className="text-sm font-medium text-neutral-700">Share Enhanced Note</div>
<div className="text-sm font-medium text-neutral-700">{propShareTitle || shareTitle}</div>
<div className="flex flex-col gap-2">
{/* Direct action buttons */}
{directActions.map((action) => {
Expand Down
11 changes: 11 additions & 0 deletions apps/desktop/src/components/editor-area/text-selection-popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ export function TextSelectionPopover(
}

const range = sel.getRangeAt(0);
const commonAncestor = range.commonAncestorContainer;
const container = commonAncestor.nodeType === Node.TEXT_NODE
? commonAncestor.parentElement
: commonAncestor as Element;

// block popover in transcript area
if (container?.closest(".tiptap-transcript")) {
setSelection(null);
return;
}

const rect = range.getBoundingClientRect();

const selectedText = sel.toString().trim();
Expand Down
Loading
Loading