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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { openUrl } from "@tauri-apps/plugin-opener";
import { format, isSameDay, subDays } from "date-fns";
import { CalendarIcon, SearchIcon, SpeechIcon, VideoIcon, XIcon } from "lucide-react";
import { useState } from "react";
import { useEffect, useState } from "react";

import { useHypr } from "@/contexts";
import { commands as appleCalendarCommands } from "@hypr/plugin-apple-calendar";
Expand Down Expand Up @@ -37,6 +37,12 @@ export function EventChip({ sessionId }: EventChipProps) {
const [activeTab, setActiveTab] = useState<"event" | "date">("event");
const queryClient = useQueryClient();

useEffect(() => {
if (isPopoverOpen) {
setActiveTab("event");
}
}, [isPopoverOpen]);

const {
sessionCreatedAt,
updateTitle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const openaiModels = [
];

const geminiModels = [
"gemini-2.5-pro",
"gemini-1.5-pro",
"gemini-1.5-flash",
];
Expand Down
24 changes: 3 additions & 21 deletions apps/desktop/src/components/toolbar/buttons/chat-panel-button.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Trans } from "@lingui/react/macro";
import { memo, useEffect, useState } from "react";
import { MessageCircleMore } from "lucide-react";
import { memo, useEffect } from "react";

import { useRightPanel } from "@/contexts";
import { Button } from "@hypr/ui/components/ui/button";
Expand All @@ -9,22 +10,9 @@ import Shortcut from "../../shortcut";

function ChatPanelButtonBase() {
const { isExpanded, currentView, togglePanel } = useRightPanel();
const [isAnimating, setIsAnimating] = useState(false);

const isActive = isExpanded && currentView === "chat";

useEffect(() => {
const animationInterval = setInterval(() => {
setIsAnimating(true);
const timeout = setTimeout(() => {
setIsAnimating(false);
}, 1625);
return () => clearTimeout(timeout);
}, 4625);

return () => clearInterval(animationInterval);
}, []);

useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "j" && (event.metaKey || event.ctrlKey)) {
Expand Down Expand Up @@ -53,13 +41,7 @@ function ChatPanelButtonBase() {
onClick={handleClick}
className={cn("hover:bg-neutral-200 text-xs size-7 p-0", isActive && "bg-neutral-200")}
>
<div className="relative w-6 aspect-square flex items-center justify-center">
<img
src={isAnimating ? "/assets/dynamic.gif" : "/assets/static.png"}
alt="Chat Assistant"
className="w-full h-full"
/>
</div>
<MessageCircleMore className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
Expand Down
91 changes: 85 additions & 6 deletions apps/desktop/src/components/toolbar/buttons/share-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { join } from "@tauri-apps/api/path";
import { message } from "@tauri-apps/plugin-dialog";
import { fetch as tauriFetch } from "@tauri-apps/plugin-http";
import { openPath, openUrl } from "@tauri-apps/plugin-opener";
import { BookText, ChevronDown, ChevronUp, FileText, HelpCircle, Mail, Share2Icon } from "lucide-react";
import { BookText, Check, ChevronDown, ChevronUp, Copy, FileText, HelpCircle, Mail, Share2Icon } from "lucide-react";
import { useState } from "react";

import { useHypr } from "@/contexts";
Expand Down Expand Up @@ -38,6 +38,7 @@ function ShareButtonInNote() {
const [open, setOpen] = useState(false);
const [expandedId, setExpandedId] = useState<string | null>(null);
const [selectedObsidianFolder, setSelectedObsidianFolder] = useState<string>("default");
const [copySuccess, setCopySuccess] = useState(false);
const hasEnhancedNote = !!session?.enhanced_memo_html;

const isObsidianConfigured = useQuery({
Expand Down Expand Up @@ -72,6 +73,15 @@ function ShareButtonInNote() {
staleTime: 5 * 60 * 1000,
});

const directActions: DirectAction[] = [
{
id: "copy",
title: "Copy Note",
icon: <Copy size={20} />,
description: "",
},
];

const exportOptions: ExportCard[] = [
{
id: "pdf",
Expand Down Expand Up @@ -124,6 +134,10 @@ function ShareButtonInNote() {
setOpen(newOpen);
setExpandedId(null);

if (!newOpen) {
setCopySuccess(false);
}

if (newOpen) {
isObsidianConfigured.refetch().then((configResult) => {
if (configResult.data) {
Expand All @@ -143,7 +157,9 @@ function ShareButtonInNote() {
const start = performance.now();
let result: ExportResult | null = null;

if (optionId === "pdf") {
if (optionId === "copy") {
result = await exportHandlers.copy(session);
} else if (optionId === "pdf") {
result = await exportHandlers.pdf(session);
} else if (optionId === "email") {
result = await exportHandlers.email(session);
Expand Down Expand Up @@ -187,16 +203,22 @@ function ShareButtonInNote() {
});
},
onSuccess: (result) => {
if (result?.type === "pdf" && result.path) {
if (result?.type === "copy" && result.success) {
setCopySuccess(true);
// Reset after 2 seconds
setTimeout(() => setCopySuccess(false), 2000);
} else if (result?.type === "pdf" && result.path) {
openPath(result.path);
} else if (result?.type === "email" && result.url) {
openUrl(result.url);
} else if (result?.type === "obsidian" && result.url) {
openUrl(result.url);
}
},
onSettled: () => {
setOpen(false);
onSettled: (result) => {
if (result?.type !== "copy") {
setOpen(false);
}
},
onError: (error) => {
console.error(error);
Expand Down Expand Up @@ -238,6 +260,35 @@ function ShareButtonInNote() {
</p>
</div>
<div className="space-y-2">
{/* Direct action buttons */}
{directActions.map((action) => {
const isLoading = exportMutation.isPending && exportMutation.variables?.optionId === action.id;
const isSuccess = action.id === "copy" && copySuccess;

return (
<div key={action.id} className="border rounded-lg overflow-hidden">
<button
onClick={() => handleExport(action.id)}
disabled={exportMutation.isPending}
className="w-full flex items-center justify-between p-3 hover:bg-gray-50 transition-colors disabled:opacity-50"
>
<div className="flex items-center space-x-3">
<div className={`text-gray-700 transition-colors ${isSuccess ? "text-green-600" : ""}`}>
{isSuccess ? <Check size={20} /> : action.icon}
</div>
<div className="text-left">
<span className="font-medium text-sm block">{action.title}</span>
<span className="text-xs text-gray-600">{action.description}</span>
</div>
</div>
{isLoading && <span className="text-xs text-gray-500">Copying...</span>}
{isSuccess && <span className="text-xs text-green-600">Copied!</span>}
</button>
</div>
);
})}

{/* Expandable export options */}
{exportOptions.map((option) => {
const expanded = expandedId === option.id;

Expand Down Expand Up @@ -315,6 +366,13 @@ function ShareButtonInNote() {
);
}

interface DirectAction {
id: "copy";
title: string;
icon: React.ReactNode;
description: string;
}

interface ExportCard {
id: "pdf" | "email" | "obsidian";
title: string;
Expand All @@ -324,9 +382,10 @@ interface ExportCard {
}

interface ExportResult {
type: "pdf" | "email" | "obsidian";
type: "copy" | "pdf" | "email" | "obsidian";
path?: string;
url?: string;
success?: boolean;
}

interface ObsidianFolder {
Expand All @@ -335,6 +394,26 @@ interface ObsidianFolder {
}

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

if (session.enhanced_memo_html) {
textToCopy = html2md(session.enhanced_memo_html);
} else if (session.raw_memo_html) {
textToCopy = html2md(session.raw_memo_html);
} else {
textToCopy = session.title || "No content available";
}

await navigator.clipboard.writeText(textToCopy);
return { type: "copy", success: true };
} catch (error) {
console.error("Failed to copy to clipboard:", error);
throw new Error("Failed to copy note to clipboard");
}
},

pdf: async (session: Session): Promise<ExportResult> => {
const path = await exportToPDF(session);
return { type: "pdf", path };
Expand Down
Loading
Loading