From 78116cf51a435856fa35564f46057f326c242327 Mon Sep 17 00:00:00 2001 From: Deokhaeng Lee Date: Thu, 31 Jul 2025 12:39:12 -0700 Subject: [PATCH 01/11] added chat chip --- .../editor-area/note-header/chips/index.tsx | 21 +++++++++++++++++++ .../right-panel/hooks/useChatLogic.ts | 4 ++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/components/editor-area/note-header/chips/index.tsx b/apps/desktop/src/components/editor-area/note-header/chips/index.tsx index 28d3d6141e..6a76cd2985 100644 --- a/apps/desktop/src/components/editor-area/note-header/chips/index.tsx +++ b/apps/desktop/src/components/editor-area/note-header/chips/index.tsx @@ -1,8 +1,28 @@ +import { MessageCircleMore } from "lucide-react"; +import { useRightPanel } from "@/contexts"; import { EventChip } from "./event-chip"; import { ParticipantsChip } from "./participants-chip"; import { PastNotesChip } from "./past-notes-chip"; import { TagChip } from "./tag-chip"; +function StartChatButton() { + const { togglePanel } = useRightPanel(); + + const handleChatClick = () => { + togglePanel("chat"); + }; + + return ( + + ); +} + export default function NoteHeaderChips({ sessionId, hashtags = [] }: { sessionId: string; hashtags?: string[]; @@ -11,6 +31,7 @@ export default function NoteHeaderChips({ sessionId, hashtags = [] }: {
+
diff --git a/apps/desktop/src/components/right-panel/hooks/useChatLogic.ts b/apps/desktop/src/components/right-panel/hooks/useChatLogic.ts index 8afc185871..d14cac13d7 100644 --- a/apps/desktop/src/components/right-panel/hooks/useChatLogic.ts +++ b/apps/desktop/src/components/right-panel/hooks/useChatLogic.ts @@ -138,14 +138,14 @@ export function useChatLogic({ return; } - if (messages.length >= 4 && !getLicense.data?.valid) { + if (messages.length >= 6 && !getLicense.data?.valid) { if (userId) { await analyticsCommands.event({ event: "pro_license_required_chat", distinct_id: userId, }); } - await message("2 messages are allowed per conversation for free users.", { + await message("3 messages are allowed per conversation for free users.", { title: "Pro License Required", kind: "info", }); From 35bb0a93e4b18879fe3cefe17f2acba1b7ba15b0 Mon Sep 17 00:00:00 2001 From: Deokhaeng Lee Date: Thu, 31 Jul 2025 15:27:01 -0700 Subject: [PATCH 02/11] mention notes working --- .../editor-area/note-header/chips/index.tsx | 2 +- .../components/chat/chat-input.tsx | 278 ++++++++++++++++-- .../right-panel/hooks/useChatLogic.ts | 56 +++- .../right-panel/views/chat-view.tsx | 2 +- 4 files changed, 299 insertions(+), 39 deletions(-) diff --git a/apps/desktop/src/components/editor-area/note-header/chips/index.tsx b/apps/desktop/src/components/editor-area/note-header/chips/index.tsx index 6a76cd2985..9d0257cf6d 100644 --- a/apps/desktop/src/components/editor-area/note-header/chips/index.tsx +++ b/apps/desktop/src/components/editor-area/note-header/chips/index.tsx @@ -31,8 +31,8 @@ export default function NoteHeaderChips({ sessionId, hashtags = [] }: {
- +
); diff --git a/apps/desktop/src/components/right-panel/components/chat/chat-input.tsx b/apps/desktop/src/components/right-panel/components/chat/chat-input.tsx index 3549270b3a..1859f16e93 100644 --- a/apps/desktop/src/components/right-panel/components/chat/chat-input.tsx +++ b/apps/desktop/src/components/right-panel/components/chat/chat-input.tsx @@ -1,17 +1,21 @@ import { useQuery } from "@tanstack/react-query"; import { ArrowUpIcon, BuildingIcon, FileTextIcon, UserIcon } from "lucide-react"; -import { useEffect } from "react"; +import { useEffect, useRef, useCallback } from "react"; -import { useRightPanel } from "@/contexts"; +import { useHypr, useRightPanel } from "@/contexts"; +import { commands as analyticsCommands } from "@hypr/plugin-analytics"; import { commands as dbCommands } from "@hypr/plugin-db"; import { Badge } from "@hypr/ui/components/ui/badge"; import { Button } from "@hypr/ui/components/ui/button"; import { BadgeType } from "../../types/chat-types"; +// Only use what's actually available +import Editor, { type TiptapEditor } from "@hypr/tiptap/editor"; + interface ChatInputProps { inputValue: string; onChange: (e: React.ChangeEvent) => void; - onSubmit: () => void; + onSubmit: (mentionedNotes?: Array<{ id: string; type: string; label: string }>) => void; onKeyDown: (e: React.KeyboardEvent) => void; autoFocus?: boolean; entityId?: string; @@ -33,8 +37,11 @@ export function ChatInput( isGenerating = false, }: ChatInputProps, ) { + const { userId } = useHypr(); const { chatInputRef } = useRightPanel(); + const lastBacklinkSearchTime = useRef(0); + const { data: noteData } = useQuery({ queryKey: ["session", entityId], queryFn: async () => entityId ? dbCommands.getSession({ id: entityId }) : null, @@ -70,28 +77,185 @@ export function ChatInput( } }; - useEffect(() => { - const textarea = chatInputRef.current; - if (!textarea) { - return; + // Mention search function (same as main editor) + const handleMentionSearch = useCallback(async (query: string) => { + const now = Date.now(); + const timeSinceLastEvent = now - lastBacklinkSearchTime.current; + + if (timeSinceLastEvent >= 5000) { + analyticsCommands.event({ + event: "searched_backlink", + distinct_id: userId, + }); + lastBacklinkSearchTime.current = now; } - textarea.style.height = "auto"; + const sessions = await dbCommands.listSessions({ + type: "search", + query, + user_id: userId, + limit: 5 + }); - const baseHeight = 40; - const newHeight = Math.max(textarea.scrollHeight, baseHeight); - textarea.style.height = `${newHeight}px`; - }, [inputValue, chatInputRef]); + return sessions.map((s) => ({ + id: s.id, + type: "note" as const, + label: s.title || "Untitled Note", + })); + }, [userId]); - useEffect(() => { - const textarea = chatInputRef.current; - if (textarea) { - textarea.style.height = "40px"; - if (autoFocus) { - textarea.focus(); + // Helper function to extract plain text from HTML + const extractPlainText = useCallback((html: string) => { + const div = document.createElement('div'); + div.innerHTML = html; + return div.textContent || div.innerText || ''; + }, []); + + // Handle content changes from the Editor + const handleContentChange = useCallback((html: string) => { + const plainText = extractPlainText(html); + + // Create synthetic event to maintain compatibility + const syntheticEvent = { + target: { value: plainText }, + currentTarget: { value: plainText }, + } as React.ChangeEvent; + + onChange(syntheticEvent); + }, [onChange, extractPlainText]); + + const editorRef = useRef<{ editor: TiptapEditor | null }>(null); + + // Extract mentioned notes from editor content + const extractMentionedNotes = useCallback(() => { + if (!editorRef.current?.editor) return []; + + const doc = editorRef.current.editor.getJSON(); + const mentions: Array<{ id: string; type: string; label: string }> = []; + + // Recursive function to traverse the document tree + const traverseNode = (node: any) => { + // Check for mention nodes + if (node.type === 'mention' || node.type === 'mention-@') { + if (node.attrs) { + mentions.push({ + id: node.attrs.id || node.attrs['data-id'], + type: node.attrs.type || node.attrs['data-type'] || 'note', + label: node.attrs.label || node.attrs['data-label'] || 'Unknown', + }); + } + } + + // Check for mention marks + if (node.marks && Array.isArray(node.marks)) { + node.marks.forEach((mark: any) => { + if (mark.type === 'mention' || mark.type === 'mention-@') { + if (mark.attrs) { + mentions.push({ + id: mark.attrs.id || mark.attrs['data-id'], + type: mark.attrs.type || mark.attrs['data-type'] || 'note', + label: mark.attrs.label || mark.attrs['data-label'] || 'Unknown', + }); + } + } + }); } + + if (node.content && Array.isArray(node.content)) { + node.content.forEach(traverseNode); + } + }; + + if (doc.content) { + doc.content.forEach(traverseNode); } - }, [autoFocus, chatInputRef]); + + return mentions; + }, []); + + // Handle submit and clear editor + const handleSubmit = useCallback(() => { + const mentionedNotes = extractMentionedNotes(); + + /* + if (mentionedNotes.length > 0) { + console.log("🔗 Mentioned notes in chat message:"); + mentionedNotes.forEach((mention, index) => { + console.log(` ${index + 1}. "${mention.label}" (ID: ${mention.id}, Type: ${mention.type})`); + }); + console.log(`Total mentions: ${mentionedNotes.length}`); + } else { + console.log("No notes mentioned in this message"); + } + */ + + // Call the original onSubmit with mentioned notes + onSubmit(mentionedNotes); + + // Clear the editor content + if (editorRef.current?.editor) { + editorRef.current.editor.commands.setContent("

"); + + // Trigger onChange with empty value + const syntheticEvent = { + target: { value: "" }, + currentTarget: { value: "" }, + } as React.ChangeEvent; + + onChange(syntheticEvent); + } + }, [onSubmit, onChange, extractMentionedNotes]); + + // Expose editor reference for compatibility + useEffect(() => { + if (chatInputRef && typeof chatInputRef === "object" && editorRef.current?.editor) { + (chatInputRef as any).current = editorRef.current.editor.view.dom; + } + }, [chatInputRef]); + + // Disable rich text formatting and mention clicks + useEffect(() => { + const editor = editorRef.current?.editor; + if (editor) { + const handleKeyDown = (event: KeyboardEvent) => { + // Disable common rich text shortcuts + if (event.metaKey || event.ctrlKey) { + if (['b', 'i', 'u', 'k'].includes(event.key.toLowerCase())) { + event.preventDefault(); + return; + } + } + + // Handle Enter for submission + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + + // Only submit if there's content + if (inputValue.trim()) { + handleSubmit(); + } + } + }; + + const handleClick = (event: MouseEvent) => { + const target = event.target as HTMLElement; + // Prevent clicks on mentions + if (target && (target.classList.contains('mention') || target.closest('.mention'))) { + event.preventDefault(); + event.stopPropagation(); + return false; + } + }; + + editor.view.dom.addEventListener('keydown', handleKeyDown); + editor.view.dom.addEventListener('click', handleClick); + + return () => { + editor.view.dom.removeEventListener('keydown', handleKeyDown); + editor.view.dom.removeEventListener('click', handleClick); + }; + } + }, [editorRef.current?.editor, onKeyDown, handleSubmit, inputValue]); const getBadgeIcon = () => { switch (entityType) { @@ -109,16 +273,68 @@ export function ChatInput( return (
-