mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-05 08:06:19 +00:00
feat: add URL detection to note tab with dynamic CTA
- Note tab auto-detects when content is a URL and changes CTA to 'Save link' - URL detection uses plain text (not markdown) to avoid Tiptap serialization issues - Requires dot in hostname for protocol-less URLs to prevent false positives - URL normalization happens in parent handler for consistent Cmd+Enter and CTA behavior - TextEditor now exposes onPlainTextChange callback
This commit is contained in:
parent
5051a2ad5b
commit
8860cb1622
3 changed files with 79 additions and 8 deletions
|
|
@ -105,6 +105,9 @@ export function AddDocument({
|
|||
|
||||
// Form data state for button click handling
|
||||
const [noteContent, setNoteContent] = useState("")
|
||||
const [noteContentType, setNoteContentType] = useState<"note" | "link">(
|
||||
"note",
|
||||
)
|
||||
const [linkData, setLinkData] = useState<LinkData>({
|
||||
url: "",
|
||||
title: "",
|
||||
|
|
@ -129,19 +132,28 @@ export function AddDocument({
|
|||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setFileData({ items: [], title: "", description: "" })
|
||||
setNoteContentType("note")
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
// Submit handlers
|
||||
const handleNoteSubmit = useCallback(
|
||||
(content: string) => {
|
||||
(content: string, contentType: "note" | "link") => {
|
||||
if (!content.trim()) {
|
||||
toast.error("Please enter some content")
|
||||
return
|
||||
}
|
||||
noteMutation.mutate({ content, project: localSelectedProject })
|
||||
if (contentType === "link") {
|
||||
const normalizedUrl = normalizeUrl(content.trim())
|
||||
linkMutation.mutate({
|
||||
url: normalizedUrl,
|
||||
project: localSelectedProject,
|
||||
})
|
||||
} else {
|
||||
noteMutation.mutate({ content, project: localSelectedProject })
|
||||
}
|
||||
},
|
||||
[noteMutation, localSelectedProject],
|
||||
[noteMutation, linkMutation, localSelectedProject],
|
||||
)
|
||||
|
||||
const handleLinkSubmit = useCallback(
|
||||
|
|
@ -215,6 +227,12 @@ export function AddDocument({
|
|||
setNoteContent(content)
|
||||
}, [])
|
||||
|
||||
const handleNoteRequestSubmit = useCallback(() => {
|
||||
// This will be called by Cmd+Enter from the editor
|
||||
// For now it just does the note submit; Task 3 will expand this for files
|
||||
handleNoteSubmit(noteContent, noteContentType)
|
||||
}, [handleNoteSubmit, noteContent, noteContentType])
|
||||
|
||||
const handleLinkDataChange = useCallback((data: LinkData) => {
|
||||
setLinkData(data)
|
||||
}, [])
|
||||
|
|
@ -226,7 +244,7 @@ export function AddDocument({
|
|||
const handleButtonClick = () => {
|
||||
switch (activeTab) {
|
||||
case "note":
|
||||
handleNoteSubmit(noteContent)
|
||||
handleNoteSubmit(noteContent, noteContentType)
|
||||
break
|
||||
case "link":
|
||||
handleLinkSubmit(linkData)
|
||||
|
|
@ -311,6 +329,8 @@ export function AddDocument({
|
|||
<NoteContent
|
||||
onSubmit={handleNoteSubmit}
|
||||
onContentChange={handleNoteContentChange}
|
||||
onContentTypeChange={setNoteContentType}
|
||||
onRequestSubmit={handleNoteRequestSubmit}
|
||||
isSubmitting={noteMutation.isPending}
|
||||
isOpen={isOpen}
|
||||
/>
|
||||
|
|
@ -389,7 +409,13 @@ export function AddDocument({
|
|||
</>
|
||||
) : (
|
||||
<>
|
||||
+ Add {activeTab}{" "}
|
||||
{activeTab === "note"
|
||||
? noteContentType === "link"
|
||||
? "Save link"
|
||||
: "Save note"
|
||||
: activeTab === "link"
|
||||
? "Save link"
|
||||
: `+ Add ${activeTab}`}{" "}
|
||||
{!isMobile && (
|
||||
<span
|
||||
className={cn(
|
||||
|
|
|
|||
|
|
@ -2,10 +2,35 @@
|
|||
|
||||
import { useState, useEffect } from "react"
|
||||
import { TextEditor } from "../text-editor"
|
||||
import { isValidUrl } from "@/lib/url-helpers"
|
||||
|
||||
function detectContentType(plainText: string): "note" | "link" {
|
||||
const trimmed = plainText.trim()
|
||||
if (!trimmed) return "note"
|
||||
// Must be a single token (no whitespace)
|
||||
if (/\s/.test(trimmed)) return "note"
|
||||
// Try as-is first (has protocol)
|
||||
if (
|
||||
trimmed.startsWith("http://") ||
|
||||
trimmed.startsWith("https://") ||
|
||||
trimmed.startsWith("HTTP://") ||
|
||||
trimmed.startsWith("HTTPS://")
|
||||
) {
|
||||
if (isValidUrl(trimmed)) return "link"
|
||||
return "note"
|
||||
}
|
||||
// Protocol-less: require a dot to avoid classifying single words as links
|
||||
if (!trimmed.includes(".")) return "note"
|
||||
const withProtocol = `https://${trimmed}`
|
||||
if (isValidUrl(withProtocol)) return "link"
|
||||
return "note"
|
||||
}
|
||||
|
||||
interface NoteContentProps {
|
||||
onSubmit?: (content: string) => void
|
||||
onSubmit?: (content: string, contentType: "note" | "link") => void
|
||||
onContentChange?: (content: string) => void
|
||||
onContentTypeChange?: (type: "note" | "link") => void
|
||||
onRequestSubmit?: () => void
|
||||
isSubmitting?: boolean
|
||||
isOpen?: boolean
|
||||
}
|
||||
|
|
@ -13,16 +38,24 @@ interface NoteContentProps {
|
|||
export function NoteContent({
|
||||
onSubmit,
|
||||
onContentChange,
|
||||
onContentTypeChange,
|
||||
onRequestSubmit,
|
||||
isSubmitting,
|
||||
isOpen,
|
||||
}: NoteContentProps) {
|
||||
const [content, setContent] = useState("")
|
||||
const [plainText, setPlainText] = useState("")
|
||||
|
||||
const canSubmit = content.trim().length > 0 && !isSubmitting
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (canSubmit && onSubmit) {
|
||||
onSubmit(content)
|
||||
const type = detectContentType(plainText)
|
||||
if (type === "link") {
|
||||
onSubmit(plainText.trim(), "link")
|
||||
} else {
|
||||
onSubmit(content, "note")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -35,6 +68,7 @@ export function NoteContent({
|
|||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setContent("")
|
||||
setPlainText("")
|
||||
onContentChange?.("")
|
||||
}
|
||||
}, [isOpen, onContentChange])
|
||||
|
|
@ -44,7 +78,12 @@ export function NoteContent({
|
|||
<TextEditor
|
||||
content={undefined}
|
||||
onContentChange={handleContentChange}
|
||||
onSubmit={handleSubmit}
|
||||
onPlainTextChange={(text) => {
|
||||
setPlainText(text)
|
||||
const type = detectContentType(text)
|
||||
onContentTypeChange?.(type)
|
||||
}}
|
||||
onSubmit={onRequestSubmit ?? handleSubmit}
|
||||
debounceMs={0}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,11 +16,13 @@ const extensions = [...defaultExtensions, slashCommand, Markdown]
|
|||
export function TextEditor({
|
||||
content: initialContent,
|
||||
onContentChange,
|
||||
onPlainTextChange,
|
||||
onSubmit,
|
||||
debounceMs = 500,
|
||||
}: {
|
||||
content: string | undefined
|
||||
onContentChange: (content: string) => void
|
||||
onPlainTextChange?: (text: string) => void
|
||||
onSubmit: () => void
|
||||
debounceMs?: number
|
||||
}) {
|
||||
|
|
@ -38,6 +40,8 @@ export function TextEditor({
|
|||
const json = editor.getJSON()
|
||||
const markdown = editor.storage.markdown?.manager?.serialize(json) ?? ""
|
||||
onContentChange?.(markdown)
|
||||
const plainText = editor.getText()
|
||||
onPlainTextChange?.(plainText)
|
||||
}, debounceMs)
|
||||
|
||||
const editor = useEditor({
|
||||
|
|
@ -55,6 +59,8 @@ export function TextEditor({
|
|||
const json = editor.getJSON()
|
||||
const markdown = editor.storage.markdown?.manager?.serialize(json) ?? ""
|
||||
onContentChange?.(markdown)
|
||||
const plainText = editor.getText()
|
||||
onPlainTextChange?.(plainText)
|
||||
return
|
||||
}
|
||||
debouncedUpdates(editor)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue