diff --git a/.changeset/full-screen-note-editor.md b/.changeset/full-screen-note-editor.md new file mode 100644 index 0000000..4705f03 --- /dev/null +++ b/.changeset/full-screen-note-editor.md @@ -0,0 +1,9 @@ +--- +"think-app": patch +--- + +Replace add memory dialog with full-screen TipTap editor + +- Add new TipTap-based NoteEditor component with formatting toolbar +- Remove AddMemoryDialog in favor of full-screen editing experience +- Update MemoryDetailPanel to open editor for notes diff --git a/app/package.json b/app/package.json index 221549b..84cdb3f 100644 --- a/app/package.json +++ b/app/package.json @@ -59,6 +59,10 @@ }, "dependencies": { "@microsoft/fetch-event-source": "^2.0.1", + "@tiptap/extension-placeholder": "^3.13.0", + "@tiptap/pm": "^3.13.0", + "@tiptap/react": "^3.13.0", + "@tiptap/starter-kit": "^3.13.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.460.0", diff --git a/app/src/components/AddMemoryDialog.tsx b/app/src/components/AddMemoryDialog.tsx deleted file mode 100644 index 06a3533..0000000 --- a/app/src/components/AddMemoryDialog.tsx +++ /dev/null @@ -1,316 +0,0 @@ -import { useState, useEffect, useRef } from "react"; -import { createPortal } from "react-dom"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { X, Globe, FileText, Loader2 } from "lucide-react"; -import { cn } from "@/lib/utils"; -import { apiFetch } from "@/lib/api"; - -interface Tag { - id: number; - name: string; - usage_count: number; -} - -interface AddMemoryDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - onSuccess: () => void; - allTags: Tag[]; -} - -export function AddMemoryDialog({ - open, - onOpenChange, - onSuccess, - allTags, -}: AddMemoryDialogProps) { - // Form state - const [newMemory, setNewMemory] = useState({ - title: "", - content: "", - type: "note" as "web" | "note", - url: "", - }); - const [adding, setAdding] = useState(false); - - // Tags state - const [selectedTags, setSelectedTags] = useState([]); - const [tagInput, setTagInput] = useState(""); - const [tagSuggestions, setTagSuggestions] = useState([]); - const [showTagSuggestions, setShowTagSuggestions] = useState(false); - - const titleInputRef = useRef(null); - - // Auto-focus title input when dialog opens - useEffect(() => { - if (open) { - setTimeout(() => titleInputRef.current?.focus(), 100); - } - }, [open]); - - // Handle escape key - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape" && open) { - onOpenChange(false); - } - }; - - document.addEventListener("keydown", handleKeyDown); - return () => document.removeEventListener("keydown", handleKeyDown); - }, [open, onOpenChange]); - - // Prevent body scroll when dialog is open - useEffect(() => { - if (open) { - document.body.style.overflow = "hidden"; - } else { - document.body.style.overflow = ""; - } - return () => { - document.body.style.overflow = ""; - }; - }, [open]); - - // Tag input handling - const handleTagInputChange = (value: string) => { - setTagInput(value); - if (value.trim()) { - const filtered = allTags.filter( - (tag) => - tag.name.toLowerCase().includes(value.toLowerCase()) && - !selectedTags.includes(tag.name) - ); - setTagSuggestions(filtered.slice(0, 5)); - setShowTagSuggestions(filtered.length > 0); - } else { - setShowTagSuggestions(false); - } - }; - - const addTag = (tagName: string) => { - const normalized = tagName.trim().toLowerCase(); - if (normalized && !selectedTags.includes(normalized)) { - setSelectedTags((prev) => [...prev, normalized]); - } - setTagInput(""); - setShowTagSuggestions(false); - }; - - const removeSelectedTag = (tagName: string) => { - setSelectedTags((prev) => prev.filter((t) => t !== tagName)); - }; - - const handleTagKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" || e.key === ",") { - e.preventDefault(); - if (tagInput.trim()) { - addTag(tagInput); - } - } - }; - - // Submit handler - const handleSubmit = async () => { - if (!newMemory.title.trim()) return; - setAdding(true); - - try { - const payload = { - title: newMemory.title, - content: newMemory.content, - type: newMemory.type, - url: newMemory.url || null, - tags: selectedTags.length > 0 ? selectedTags : null, - }; - - const res = await apiFetch("/api/memories", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - - if (res.ok) { - // Reset form - setNewMemory({ title: "", content: "", type: "note", url: "" }); - setSelectedTags([]); - onOpenChange(false); - onSuccess(); - } - } catch (err) { - console.error("Failed to add memory:", err); - } finally { - setAdding(false); - } - }; - - if (!open) return null; - - return createPortal( -
- {/* Backdrop */} -
onOpenChange(false)} - /> - - {/* Dialog panel */} -
-
- {/* Header */} -
-

Add Memory

- -
- - {/* Form */} -
- {/* Type toggle */} -
- - -
- - {/* Title */} - - setNewMemory((prev) => ({ ...prev, title: e.target.value })) - } - /> - - {/* URL (for web type) */} - {newMemory.type === "web" && ( - - setNewMemory((prev) => ({ ...prev, url: e.target.value })) - } - /> - )} - - {/* Content */} -