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
4 changes: 2 additions & 2 deletions apps/desktop/src/components/editor-area/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ async function generateTitleDirect(enhancedContent: string, targetSessionId: str
]);

const model = provider.languageModel("defaultModel");
const abortSignal = AbortSignal.timeout(30_000);
const abortSignal = AbortSignal.timeout(60_000);

const { text } = await generateText({
abortSignal,
Expand Down Expand Up @@ -365,7 +365,7 @@ export function useEnhanceMutation({
);

const abortController = new AbortController();
const abortSignal = AbortSignal.any([abortController.signal, AbortSignal.timeout(60 * 1000)]);
const abortSignal = AbortSignal.any([abortController.signal, AbortSignal.timeout(120 * 1000)]);
setEnhanceController(abortController);

const provider = await modelProvider();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,21 @@ interface ChatInputProps {
entityId?: string;
entityType?: BadgeType;
onNoteBadgeClick?: () => void;
isGenerating?: boolean;
}

export function ChatInput(
{ inputValue, onChange, onSubmit, onKeyDown, autoFocus = false, entityId, entityType = "note", onNoteBadgeClick }:
ChatInputProps,
{
inputValue,
onChange,
onSubmit,
onKeyDown,
autoFocus = false,
entityId,
entityType = "note",
onNoteBadgeClick,
isGenerating = false,
}: ChatInputProps,
) {
const { chatInputRef } = useRightPanel();

Expand Down Expand Up @@ -107,6 +117,7 @@ export function ChatInput(
placeholder="Type a message..."
className="w-full resize-none overflow-hidden px-3 py-2 pr-10 text-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 min-h-[40px] max-h-[120px]"
rows={1}
disabled={isGenerating}
/>
<div className="flex items-center justify-between pb-2 px-3">
{entityId
Expand All @@ -123,7 +134,7 @@ export function ChatInput(
<Button
size="icon"
onClick={onSubmit}
disabled={!inputValue.trim()}
disabled={!inputValue.trim() || isGenerating}
>
<ArrowUpIcon className="h-4 w-4" />
</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { cn } from "@hypr/ui/lib/utils";
import { Trans } from "@lingui/react/macro";
import { MessageContent } from "./message-content";
import { Message } from "./types";

interface ChatMessageProps {
message: Message;
sessionTitle?: string;
hasEnhancedNote?: boolean;
onApplyMarkdown?: (markdownContent: string) => void;
}

export function ChatMessage({ message }: ChatMessageProps) {
export function ChatMessage({ message, sessionTitle, hasEnhancedNote, onApplyMarkdown }: ChatMessageProps) {
return (
<div className="w-full mb-4">
<div
Expand All @@ -17,7 +21,12 @@ export function ChatMessage({ message }: ChatMessageProps) {
>
{message.isUser ? <Trans>User:</Trans> : <Trans>Assistant:</Trans>}
</div>
<div className="text-sm whitespace-pre-wrap break-words overflow-wrap-anywhere max-w-full">{message.content}</div>
<MessageContent
message={message}
sessionTitle={sessionTitle}
hasEnhancedNote={hasEnhancedNote}
onApplyMarkdown={onApplyMarkdown}
/>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import { Message } from "./types";

interface ChatMessagesViewProps {
messages: Message[];
sessionTitle?: string;
hasEnhancedNote?: boolean;
onApplyMarkdown?: (markdownContent: string) => void;
}

export function ChatMessagesView({ messages }: ChatMessagesViewProps) {
export function ChatMessagesView({ messages, sessionTitle, hasEnhancedNote, onApplyMarkdown }: ChatMessagesViewProps) {
const messagesEndRef = useRef<HTMLDivElement>(null);

useEffect(() => {
Expand All @@ -15,7 +18,15 @@ export function ChatMessagesView({ messages }: ChatMessagesViewProps) {

return (
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((message) => <ChatMessage key={message.id} message={message} />)}
{messages.map((message) => (
<ChatMessage
key={message.id}
message={message}
sessionTitle={sessionTitle}
hasEnhancedNote={hasEnhancedNote}
onApplyMarkdown={onApplyMarkdown}
/>
))}
<div ref={messagesEndRef} />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { commands as windowsCommands } from "@hypr/plugin-windows";
import { Badge } from "@hypr/ui/components/ui/badge";
import { Trans } from "@lingui/react/macro";
import { memo, useCallback } from "react";

Expand All @@ -15,14 +17,42 @@ export const EmptyChatState = memo(({ onQuickAction, onFocusInput }: EmptyChatSt
onQuickAction(prompt);
}, [onQuickAction]);

const handleCustomEndpointsClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
windowsCommands.windowShow({ type: "settings" }).then(() => {
windowsCommands.windowNavigate({ type: "settings" }, "/app/settings?tab=ai");
});
}, []);

return (
<div
className="flex-1 flex flex-col items-center justify-center h-full p-4 text-center"
onClick={handleContainerClick}
>
<h3 className="text-lg font-medium mb-4">
<Trans>How can I help you today?</Trans>
</h3>
<div className="flex items-center gap-2 mb-4">
<h3 className="text-lg font-medium">
<Trans>Chat with meeting notes</Trans>
</h3>
<Badge variant="secondary" className="text-[10px] px-1.5 py-0.5 bg-blue-100 text-blue-800 border-blue-200">
Beta
</Badge>
</div>

<div className="mb-6 p-3 rounded-lg bg-neutral-50 border border-neutral-200 max-w-[240px] text-left">
<p className="text-xs text-neutral-600">
<Trans>
Chat feature is in beta. For best results, we recommend you to use{" "}
<span
onClick={handleCustomEndpointsClick}
className="text-blue-600 hover:text-blue-800 cursor-pointer underline"
>
custom endpoints
</span>
.
</Trans>
</p>
</div>

<div className="flex flex-wrap gap-2 justify-center mb-4 max-w-[280px]">
<button
onClick={handleButtonClick("Summarize this meeting")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ export * from "./chat-message";
export * from "./chat-messages-view";
export * from "./empty-chat-state";
export * from "./floating-action-buttons";
export { MarkdownCard } from "./markdown-card";
export { MessageContent } from "./message-content";
export * from "./types";
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { commands as miscCommands } from "@hypr/plugin-misc";
import Renderer from "@hypr/tiptap/renderer";
import { Button } from "@hypr/ui/components/ui/button";
import { CopyIcon, FileTextIcon, PlayIcon } from "lucide-react";
import { useEffect, useState } from "react";

interface MarkdownCardProps {
content: string;
isComplete: boolean;
sessionTitle?: string;
onApplyMarkdown?: (markdownContent: string) => void;
hasEnhancedNote?: boolean;
}

export function MarkdownCard(
{ content, isComplete, sessionTitle, onApplyMarkdown, hasEnhancedNote = false }: MarkdownCardProps,
) {
const [htmlContent, setHtmlContent] = useState<string>("");
const [isCopied, setIsCopied] = useState(false);

const handleApplyClick = () => {
if (onApplyMarkdown) {
onApplyMarkdown(content);
}
};

const handleCopyClick = async () => {
try {
await navigator.clipboard.writeText(content);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000); // Reset after 2 seconds
} catch (error) {
console.error("Failed to copy to clipboard:", error);
}
};

useEffect(() => {
const convertMarkdown = async () => {
try {
let html = await miscCommands.opinionatedMdToHtml(content);

// Clean up spacing
html = html
.replace(/<p>\s*<\/p>/g, "")
.replace(/<p>\u00A0<\/p>/g, "")
.replace(/<p>&nbsp;<\/p>/g, "")
.replace(/<p>\s+<\/p>/g, "")
.replace(/<p> <\/p>/g, "")
.trim();

setHtmlContent(html);
} catch (error) {
console.error("Failed to convert markdown:", error);
setHtmlContent(content);
}
};

if (content.trim()) {
convertMarkdown();
}
}, [content]);

return (
<>
<style>
{`
/* Override tiptap spacing for compact cards */
.markdown-card-container .tiptap-normal {
font-size: 0.875rem !important;
line-height: 2 !important;
padding: 0 !important;
/* Enable text selection */
user-select: text !important;
-webkit-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
}

.markdown-card-container .tiptap-normal * {
/* Ensure all children are selectable */
user-select: text !important;
-webkit-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
}

.markdown-card-container .tiptap-normal h1 {
margin: 8px 0 8px 0 !important;
font-size: 1rem !important;
font-weight: 600 !important;
}

.markdown-card-container .tiptap-normal h1:first-child {
margin-top: 0 !important;
}

.markdown-card-container .tiptap-normal p {
margin: 0 0 20px 0 !important;
}

.markdown-card-container .tiptap-normal ul {
margin: 0 0 8px 0 !important;
padding-left: 1.2rem !important;
}

.markdown-card-container .tiptap-normal li {
margin-bottom: 3px !important;
}

/* Make selection highlight visible */
.markdown-card-container .tiptap-normal ::selection {
background-color: #3b82f6 !important;
color: white !important;
}

.markdown-card-container .tiptap-normal ::-moz-selection {
background-color: #3b82f6 !important;
color: white !important;
}
`}
</style>

{/* Flat card with no shadow */}
<div className="mt-4 mb-4 border border-neutral-200 rounded-lg bg-white overflow-hidden">
{/* Grey header section - Made thinner with py-1 */}
<div className="bg-neutral-50 px-4 py-1 border-b border-neutral-200 flex items-center justify-between">
<div className="text-sm text-neutral-600 flex items-center gap-2">
<FileTextIcon className="h-4 w-4" />
{sessionTitle || "Hyprnote Suggestion"}
</div>

{/* Conditional button based on hasEnhancedNote */}
{hasEnhancedNote
? (
<Button
variant="ghost"
className="hover:bg-neutral-200 h-6 px-2 text-xs text-neutral-600 flex items-center gap-1"
onClick={handleApplyClick}
>
<PlayIcon className="size-3" />
Apply
</Button>
)
: (
<Button
variant="ghost"
className="hover:bg-neutral-200 h-6 px-2 text-xs text-neutral-600 flex items-center gap-1"
onClick={handleCopyClick}
>
<CopyIcon className="size-3" />
{isCopied ? "Copied" : "Copy"}
</Button>
)}
</div>

{/* Content section - Add selectable class */}
<div className="p-4">
<div className="markdown-card-container select-text">
<Renderer initialContent={htmlContent} />
</div>
</div>
</div>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { MarkdownCard } from "./markdown-card";
import { Message } from "./types";

interface MessageContentProps {
message: Message;
sessionTitle?: string;
hasEnhancedNote?: boolean;
onApplyMarkdown?: (markdownContent: string) => void;
}

export function MessageContent({ message, sessionTitle, hasEnhancedNote, onApplyMarkdown }: MessageContentProps) {
// If no parts are parsed, show regular content
if (!message.parts || message.parts.length === 0) {
return (
<div className="whitespace-pre-wrap text-sm text-neutral-800">
{message.content}
</div>
);
}

return (
<div className="space-y-1">
{message.parts.map((part, index) => (
<div key={index}>
{part.type === "text"
? (
<div className="whitespace-pre-wrap text-sm text-neutral-800">
{part.content}
</div>
)
: (
<MarkdownCard
content={part.content}
isComplete={part.isComplete || false}
sessionTitle={sessionTitle}
hasEnhancedNote={hasEnhancedNote}
onApplyMarkdown={onApplyMarkdown}
/>
)}
</div>
))}
</div>
);
}
Loading
Loading