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:
MaheshtheDev 2026-05-12 00:26:52 +00:00
parent 5051a2ad5b
commit 8860cb1622
3 changed files with 79 additions and 8 deletions

View file

@ -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(

View file

@ -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>

View file

@ -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)