mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-03 20:41:07 +02:00
tiptap improvements + removing ask AI button
This commit is contained in:
parent
73ba7fee99
commit
9c1ddf66ed
10 changed files with 316 additions and 117 deletions
|
|
@ -27,6 +27,8 @@
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tiptap/extension-link": "^3.15.3",
|
"@tiptap/extension-link": "^3.15.3",
|
||||||
"@tiptap/extension-placeholder": "^3.15.3",
|
"@tiptap/extension-placeholder": "^3.15.3",
|
||||||
|
"@tiptap/extension-task-item": "^3.15.3",
|
||||||
|
"@tiptap/extension-task-list": "^3.15.3",
|
||||||
"@tiptap/pm": "^3.15.3",
|
"@tiptap/pm": "^3.15.3",
|
||||||
"@tiptap/react": "^3.15.3",
|
"@tiptap/react": "^3.15.3",
|
||||||
"@tiptap/starter-kit": "^3.15.3",
|
"@tiptap/starter-kit": "^3.15.3",
|
||||||
|
|
|
||||||
|
|
@ -251,28 +251,6 @@ const collectDirPaths = (nodes: TreeNode[]): string[] =>
|
||||||
const collectFilePaths = (nodes: TreeNode[]): string[] =>
|
const collectFilePaths = (nodes: TreeNode[]): string[] =>
|
||||||
nodes.flatMap(n => n.kind === 'file' ? [n.path] : (n.children ? collectFilePaths(n.children) : []))
|
nodes.flatMap(n => n.kind === 'file' ? [n.path] : (n.children ? collectFilePaths(n.children) : []))
|
||||||
|
|
||||||
// Sample chat history (will be replaced with real data later)
|
|
||||||
const chatHistory = [
|
|
||||||
{
|
|
||||||
id: 'project-kickoff',
|
|
||||||
title: 'Project kickoff',
|
|
||||||
preview: 'Scope, roles, and milestones.',
|
|
||||||
time: 'Today',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'design-review',
|
|
||||||
title: 'Design review',
|
|
||||||
preview: 'UI polish and sidebar UX.',
|
|
||||||
time: 'Yesterday',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tools-audit',
|
|
||||||
title: 'Tools audit',
|
|
||||||
preview: 'MCP inventory and tool gaps.',
|
|
||||||
time: 'Mon',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
// File browser state (for Knowledge section)
|
// File browser state (for Knowledge section)
|
||||||
const [selectedPath, setSelectedPath] = useState<string | null>(null)
|
const [selectedPath, setSelectedPath] = useState<string | null>(null)
|
||||||
|
|
@ -630,9 +608,9 @@ function App() {
|
||||||
setExpandedPaths(newExpanded)
|
setExpandedPaths(newExpanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle sidebar section changes - switch to chat view for ask-ai and agents
|
// Handle sidebar section changes - switch to chat view for agents
|
||||||
const handleSectionChange = useCallback((section: ActiveSection) => {
|
const handleSectionChange = useCallback((section: ActiveSection) => {
|
||||||
if (section === 'ask-ai' || section === 'agents') {
|
if (section === 'agents') {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
}
|
}
|
||||||
|
|
@ -971,7 +949,7 @@ function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<SidebarSectionProvider defaultSection="ask-ai" onSectionChange={handleSectionChange}>
|
<SidebarSectionProvider defaultSection="knowledge" onSectionChange={handleSectionChange}>
|
||||||
<div className="flex h-svh w-full">
|
<div className="flex h-svh w-full">
|
||||||
{/* Icon sidebar - always visible, fixed position */}
|
{/* Icon sidebar - always visible, fixed position */}
|
||||||
<SidebarIcon />
|
<SidebarIcon />
|
||||||
|
|
@ -992,7 +970,6 @@ function App() {
|
||||||
expandedPaths={expandedPaths}
|
expandedPaths={expandedPaths}
|
||||||
onSelectFile={toggleExpand}
|
onSelectFile={toggleExpand}
|
||||||
knowledgeActions={knowledgeActions}
|
knowledgeActions={knowledgeActions}
|
||||||
chats={chatHistory}
|
|
||||||
/>
|
/>
|
||||||
<SidebarInset className="!overflow-hidden min-h-0">
|
<SidebarInset className="!overflow-hidden min-h-0">
|
||||||
{/* Header with sidebar trigger */}
|
{/* Header with sidebar trigger */}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
|
import { useState, useCallback } from 'react'
|
||||||
import type { Editor } from '@tiptap/react'
|
import type { Editor } from '@tiptap/react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@/components/ui/popover'
|
||||||
import {
|
import {
|
||||||
BoldIcon,
|
BoldIcon,
|
||||||
ItalicIcon,
|
ItalicIcon,
|
||||||
|
|
@ -10,42 +17,104 @@ import {
|
||||||
Heading3Icon,
|
Heading3Icon,
|
||||||
ListIcon,
|
ListIcon,
|
||||||
ListOrderedIcon,
|
ListOrderedIcon,
|
||||||
|
ListTodoIcon,
|
||||||
QuoteIcon,
|
QuoteIcon,
|
||||||
MinusIcon,
|
MinusIcon,
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
CodeSquareIcon,
|
CodeSquareIcon,
|
||||||
|
Undo2Icon,
|
||||||
|
Redo2Icon,
|
||||||
|
ExternalLinkIcon,
|
||||||
|
Trash2Icon,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
|
||||||
interface EditorToolbarProps {
|
interface EditorToolbarProps {
|
||||||
editor: Editor | null
|
editor: Editor | null
|
||||||
|
onSelectionHighlight?: (range: { from: number; to: number } | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditorToolbar({ editor }: EditorToolbarProps) {
|
export function EditorToolbar({ editor, onSelectionHighlight }: EditorToolbarProps) {
|
||||||
if (!editor) return null
|
const [linkUrl, setLinkUrl] = useState('')
|
||||||
|
const [isLinkPopoverOpen, setIsLinkPopoverOpen] = useState(false)
|
||||||
|
|
||||||
const setLink = () => {
|
const openLinkPopover = useCallback(() => {
|
||||||
const previousUrl = editor.getAttributes('link').href
|
if (!editor) return
|
||||||
const url = window.prompt('URL', previousUrl)
|
const previousUrl = editor.getAttributes('link').href || ''
|
||||||
|
setLinkUrl(previousUrl)
|
||||||
|
|
||||||
if (url === null) return
|
// Highlight the current selection while popover is open
|
||||||
|
const { from, to } = editor.state.selection
|
||||||
if (url === '') {
|
if (from !== to && onSelectionHighlight) {
|
||||||
editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
onSelectionHighlight({ from, to })
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
|
setIsLinkPopoverOpen(true)
|
||||||
}
|
}, [editor, onSelectionHighlight])
|
||||||
|
|
||||||
|
const closeLinkPopover = useCallback(() => {
|
||||||
|
setIsLinkPopoverOpen(false)
|
||||||
|
setLinkUrl('')
|
||||||
|
onSelectionHighlight?.(null)
|
||||||
|
}, [onSelectionHighlight])
|
||||||
|
|
||||||
|
const applyLink = useCallback(() => {
|
||||||
|
if (!editor) return
|
||||||
|
|
||||||
|
if (linkUrl === '') {
|
||||||
|
editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||||
|
} else {
|
||||||
|
// Ensure URL has protocol
|
||||||
|
let url = linkUrl.trim()
|
||||||
|
if (url && !url.match(/^https?:\/\//i) && !url.startsWith('mailto:')) {
|
||||||
|
url = 'https://' + url
|
||||||
|
}
|
||||||
|
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
|
||||||
|
}
|
||||||
|
closeLinkPopover()
|
||||||
|
}, [editor, linkUrl, closeLinkPopover])
|
||||||
|
|
||||||
|
const removeLink = useCallback(() => {
|
||||||
|
if (!editor) return
|
||||||
|
editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||||
|
closeLinkPopover()
|
||||||
|
}, [editor, closeLinkPopover])
|
||||||
|
|
||||||
|
if (!editor) return null
|
||||||
|
|
||||||
|
const isLinkActive = editor.isActive('link')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="editor-toolbar">
|
<div className="editor-toolbar">
|
||||||
|
{/* Undo / Redo */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={() => editor.chain().focus().undo().run()}
|
||||||
|
disabled={!editor.can().undo()}
|
||||||
|
title="Undo (Ctrl+Z)"
|
||||||
|
>
|
||||||
|
<Undo2Icon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={() => editor.chain().focus().redo().run()}
|
||||||
|
disabled={!editor.can().redo()}
|
||||||
|
title="Redo (Ctrl+Shift+Z)"
|
||||||
|
>
|
||||||
|
<Redo2Icon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="separator" />
|
||||||
|
|
||||||
|
{/* Text formatting */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||||
data-active={editor.isActive('bold') || undefined}
|
data-active={editor.isActive('bold') || undefined}
|
||||||
className="data-[active]:bg-accent"
|
className="data-[active]:bg-accent"
|
||||||
title="Bold"
|
title="Bold (Ctrl+B)"
|
||||||
>
|
>
|
||||||
<BoldIcon className="size-4" />
|
<BoldIcon className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -55,7 +124,7 @@ export function EditorToolbar({ editor }: EditorToolbarProps) {
|
||||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||||
data-active={editor.isActive('italic') || undefined}
|
data-active={editor.isActive('italic') || undefined}
|
||||||
className="data-[active]:bg-accent"
|
className="data-[active]:bg-accent"
|
||||||
title="Italic"
|
title="Italic (Ctrl+I)"
|
||||||
>
|
>
|
||||||
<ItalicIcon className="size-4" />
|
<ItalicIcon className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -82,6 +151,7 @@ export function EditorToolbar({ editor }: EditorToolbarProps) {
|
||||||
|
|
||||||
<div className="separator" />
|
<div className="separator" />
|
||||||
|
|
||||||
|
{/* Headings */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
|
|
@ -115,6 +185,7 @@ export function EditorToolbar({ editor }: EditorToolbarProps) {
|
||||||
|
|
||||||
<div className="separator" />
|
<div className="separator" />
|
||||||
|
|
||||||
|
{/* Lists */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
|
|
@ -135,9 +206,20 @@ export function EditorToolbar({ editor }: EditorToolbarProps) {
|
||||||
>
|
>
|
||||||
<ListOrderedIcon className="size-4" />
|
<ListOrderedIcon className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={() => editor.chain().focus().toggleTaskList().run()}
|
||||||
|
data-active={editor.isActive('taskList') || undefined}
|
||||||
|
className="data-[active]:bg-accent"
|
||||||
|
title="Task List"
|
||||||
|
>
|
||||||
|
<ListTodoIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
<div className="separator" />
|
<div className="separator" />
|
||||||
|
|
||||||
|
{/* Blocks */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
|
|
@ -166,16 +248,78 @@ export function EditorToolbar({ editor }: EditorToolbarProps) {
|
||||||
>
|
>
|
||||||
<MinusIcon className="size-4" />
|
<MinusIcon className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
{/* Link with popover */}
|
||||||
size="icon-sm"
|
<Popover
|
||||||
onClick={setLink}
|
open={isLinkPopoverOpen}
|
||||||
data-active={editor.isActive('link') || undefined}
|
onOpenChange={(open) => {
|
||||||
className="data-[active]:bg-accent"
|
if (!open) {
|
||||||
title="Link"
|
closeLinkPopover()
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<LinkIcon className="size-4" />
|
<PopoverTrigger asChild>
|
||||||
</Button>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={openLinkPopover}
|
||||||
|
data-active={isLinkActive || undefined}
|
||||||
|
className="data-[active]:bg-accent"
|
||||||
|
title="Link"
|
||||||
|
>
|
||||||
|
<LinkIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-80 p-3" align="start">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
{isLinkActive ? 'Edit Link' : 'Add Link'}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
placeholder="https://example.com"
|
||||||
|
value={linkUrl}
|
||||||
|
onChange={(e) => setLinkUrl(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
applyLink()
|
||||||
|
}
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
setIsLinkPopoverOpen(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button size="sm" onClick={applyLink} className="flex-1">
|
||||||
|
{isLinkActive ? 'Update' : 'Apply'}
|
||||||
|
</Button>
|
||||||
|
{isLinkActive && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
window.open(linkUrl, '_blank')
|
||||||
|
}}
|
||||||
|
title="Open link"
|
||||||
|
>
|
||||||
|
<ExternalLinkIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={removeLink}
|
||||||
|
title="Remove link"
|
||||||
|
>
|
||||||
|
<Trash2Icon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import { useEditor, EditorContent } from '@tiptap/react'
|
import { useEditor, EditorContent, Extension } from '@tiptap/react'
|
||||||
|
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||||
|
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||||
import StarterKit from '@tiptap/starter-kit'
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
import Link from '@tiptap/extension-link'
|
import Link from '@tiptap/extension-link'
|
||||||
import Placeholder from '@tiptap/extension-placeholder'
|
import Placeholder from '@tiptap/extension-placeholder'
|
||||||
|
import TaskList from '@tiptap/extension-task-list'
|
||||||
|
import TaskItem from '@tiptap/extension-task-item'
|
||||||
import { Markdown } from 'tiptap-markdown'
|
import { Markdown } from 'tiptap-markdown'
|
||||||
import { useEffect, useCallback, useMemo, useRef, useState } from 'react'
|
import { useEffect, useCallback, useMemo, useRef, useState } from 'react'
|
||||||
import { EditorToolbar } from './editor-toolbar'
|
import { EditorToolbar } from './editor-toolbar'
|
||||||
|
|
@ -30,6 +34,41 @@ type WikiLinkMatch = {
|
||||||
query: string
|
query: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SelectionHighlightRange = { from: number; to: number } | null
|
||||||
|
|
||||||
|
// Plugin key for the selection highlight
|
||||||
|
const selectionHighlightKey = new PluginKey('selectionHighlight')
|
||||||
|
|
||||||
|
// Create the selection highlight extension
|
||||||
|
const createSelectionHighlightExtension = (getRange: () => SelectionHighlightRange) => {
|
||||||
|
return Extension.create({
|
||||||
|
name: 'selectionHighlight',
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
key: selectionHighlightKey,
|
||||||
|
props: {
|
||||||
|
decorations(state) {
|
||||||
|
const range = getRange()
|
||||||
|
if (!range) return DecorationSet.empty
|
||||||
|
|
||||||
|
const { from, to } = range
|
||||||
|
if (from >= to || from < 0 || to > state.doc.content.size) {
|
||||||
|
return DecorationSet.empty
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoration = Decoration.inline(from, to, {
|
||||||
|
class: 'selection-highlight',
|
||||||
|
})
|
||||||
|
return DecorationSet.create(state.doc, [decoration])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function MarkdownEditor({
|
export function MarkdownEditor({
|
||||||
content,
|
content,
|
||||||
onChange,
|
onChange,
|
||||||
|
|
@ -40,6 +79,17 @@ export function MarkdownEditor({
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null)
|
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||||
const [activeWikiLink, setActiveWikiLink] = useState<WikiLinkMatch | null>(null)
|
const [activeWikiLink, setActiveWikiLink] = useState<WikiLinkMatch | null>(null)
|
||||||
const [anchorPosition, setAnchorPosition] = useState<{ left: number; top: number } | null>(null)
|
const [anchorPosition, setAnchorPosition] = useState<{ left: number; top: number } | null>(null)
|
||||||
|
const [selectionHighlight, setSelectionHighlight] = useState<SelectionHighlightRange>(null)
|
||||||
|
const selectionHighlightRef = useRef<SelectionHighlightRange>(null)
|
||||||
|
|
||||||
|
// Keep ref in sync with state for the plugin to access
|
||||||
|
selectionHighlightRef.current = selectionHighlight
|
||||||
|
|
||||||
|
// Memoize the selection highlight extension
|
||||||
|
const selectionHighlightExtension = useMemo(
|
||||||
|
() => createSelectionHighlightExtension(() => selectionHighlightRef.current),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: [
|
extensions: [
|
||||||
|
|
@ -62,6 +112,10 @@ export function MarkdownEditor({
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
}),
|
}),
|
||||||
|
TaskList,
|
||||||
|
TaskItem.configure({
|
||||||
|
nested: true,
|
||||||
|
}),
|
||||||
Placeholder.configure({
|
Placeholder.configure({
|
||||||
placeholder,
|
placeholder,
|
||||||
}),
|
}),
|
||||||
|
|
@ -71,6 +125,7 @@ export function MarkdownEditor({
|
||||||
transformCopiedText: true,
|
transformCopiedText: true,
|
||||||
transformPastedText: true,
|
transformPastedText: true,
|
||||||
}),
|
}),
|
||||||
|
selectionHighlightExtension,
|
||||||
],
|
],
|
||||||
content: '',
|
content: '',
|
||||||
onUpdate: ({ editor }) => {
|
onUpdate: ({ editor }) => {
|
||||||
|
|
@ -184,6 +239,14 @@ export function MarkdownEditor({
|
||||||
}
|
}
|
||||||
}, [editor, content])
|
}, [editor, content])
|
||||||
|
|
||||||
|
// Force re-render decorations when selection highlight changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (editor) {
|
||||||
|
// Trigger a transaction to force decoration re-render
|
||||||
|
editor.view.dispatch(editor.state.tr)
|
||||||
|
}
|
||||||
|
}, [editor, selectionHighlight])
|
||||||
|
|
||||||
const normalizedQuery = normalizeWikiPath(activeWikiLink?.query ?? '').toLowerCase()
|
const normalizedQuery = normalizeWikiPath(activeWikiLink?.query ?? '').toLowerCase()
|
||||||
const filteredFiles = useMemo(() => {
|
const filteredFiles = useMemo(() => {
|
||||||
if (!activeWikiLink) return []
|
if (!activeWikiLink) return []
|
||||||
|
|
@ -237,7 +300,7 @@ export function MarkdownEditor({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tiptap-editor" onKeyDown={handleKeyDown}>
|
<div className="tiptap-editor" onKeyDown={handleKeyDown}>
|
||||||
<EditorToolbar editor={editor} />
|
<EditorToolbar editor={editor} onSelectionHighlight={setSelectionHighlight} />
|
||||||
<div className="editor-content-wrapper" ref={wrapperRef} onScroll={handleScroll}>
|
<div className="editor-content-wrapper" ref={wrapperRef} onScroll={handleScroll}>
|
||||||
<EditorContent editor={editor} />
|
<EditorContent editor={editor} />
|
||||||
{wikiLinks ? (
|
{wikiLinks ? (
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,6 @@ import {
|
||||||
FolderPlus,
|
FolderPlus,
|
||||||
Loader2,
|
Loader2,
|
||||||
Mail,
|
Mail,
|
||||||
MessageSquare,
|
|
||||||
MessageSquarePlus,
|
|
||||||
Microscope,
|
Microscope,
|
||||||
Network,
|
Network,
|
||||||
Pencil,
|
Pencil,
|
||||||
|
|
@ -88,11 +86,9 @@ type SidebarContentPanelProps = {
|
||||||
expandedPaths: Set<string>
|
expandedPaths: Set<string>
|
||||||
onSelectFile: (path: string, kind: "file" | "dir") => void
|
onSelectFile: (path: string, kind: "file" | "dir") => void
|
||||||
knowledgeActions: KnowledgeActions
|
knowledgeActions: KnowledgeActions
|
||||||
chats: { id: string; title: string; preview: string; time: string }[]
|
|
||||||
} & React.ComponentProps<typeof Sidebar>
|
} & React.ComponentProps<typeof Sidebar>
|
||||||
|
|
||||||
const sectionTitles = {
|
const sectionTitles = {
|
||||||
"ask-ai": "Ask AI",
|
|
||||||
knowledge: "Knowledge",
|
knowledge: "Knowledge",
|
||||||
agents: "Agents",
|
agents: "Agents",
|
||||||
}
|
}
|
||||||
|
|
@ -166,7 +162,6 @@ export function SidebarContentPanel({
|
||||||
expandedPaths,
|
expandedPaths,
|
||||||
onSelectFile,
|
onSelectFile,
|
||||||
knowledgeActions,
|
knowledgeActions,
|
||||||
chats,
|
|
||||||
...props
|
...props
|
||||||
}: SidebarContentPanelProps) {
|
}: SidebarContentPanelProps) {
|
||||||
const { activeSection } = useSidebarSection()
|
const { activeSection } = useSidebarSection()
|
||||||
|
|
@ -179,9 +174,6 @@ export function SidebarContentPanel({
|
||||||
</div>
|
</div>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
{activeSection === "ask-ai" && (
|
|
||||||
<ChatSection chats={chats} />
|
|
||||||
)}
|
|
||||||
{activeSection === "knowledge" && (
|
{activeSection === "knowledge" && (
|
||||||
<KnowledgeSection
|
<KnowledgeSection
|
||||||
tree={tree}
|
tree={tree}
|
||||||
|
|
@ -200,43 +192,6 @@ export function SidebarContentPanel({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chat Section
|
|
||||||
function ChatSection({ chats }: { chats: { id: string; title: string; preview: string; time: string }[] }) {
|
|
||||||
return (
|
|
||||||
<SidebarGroup>
|
|
||||||
<SidebarGroupLabel className="flex items-center justify-between">
|
|
||||||
<span>Recent Chats</span>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button className="text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent rounded p-1 transition-colors">
|
|
||||||
<MessageSquarePlus className="size-4" />
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right">New Chat</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</SidebarGroupLabel>
|
|
||||||
<SidebarGroupContent>
|
|
||||||
<SidebarMenu>
|
|
||||||
{chats.map((chat) => (
|
|
||||||
<SidebarMenuItem key={chat.id}>
|
|
||||||
<SidebarMenuButton className="h-auto items-start gap-2 py-2">
|
|
||||||
<MessageSquare className="mt-0.5 size-4" />
|
|
||||||
<div className="flex flex-1 flex-col gap-1">
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<span className="truncate text-sm font-medium">{chat.title}</span>
|
|
||||||
<span className="text-xs text-muted-foreground">{chat.time}</span>
|
|
||||||
</div>
|
|
||||||
<span className="truncate text-xs text-muted-foreground">{chat.preview}</span>
|
|
||||||
</div>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
))}
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroupContent>
|
|
||||||
</SidebarGroup>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Knowledge Section
|
// Knowledge Section
|
||||||
function KnowledgeSection({
|
function KnowledgeSection({
|
||||||
tree,
|
tree,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import {
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
Settings,
|
Settings,
|
||||||
Ship,
|
Ship,
|
||||||
Sparkles,
|
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
|
||||||
|
|
@ -33,7 +32,6 @@ type SecondaryItem = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const navItems: NavItem[] = [
|
const navItems: NavItem[] = [
|
||||||
{ id: "ask-ai", title: "Ask AI", icon: Sparkles },
|
|
||||||
{ id: "knowledge", title: "Knowledge", icon: Brain },
|
{ id: "knowledge", title: "Knowledge", icon: Brain },
|
||||||
{ id: "agents", title: "Agents", icon: Bot },
|
{ id: "agents", title: "Agents", icon: Bot },
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ const SIDEBAR_WIDTH_MIN = 200
|
||||||
const SIDEBAR_WIDTH_MAX = 480
|
const SIDEBAR_WIDTH_MAX = 480
|
||||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
||||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
const SIDEBAR_WIDTH_ICON = "3rem"
|
||||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
|
||||||
const SIDEBAR_OFFSET = "0px" // Default offset for nested sidebars
|
const SIDEBAR_OFFSET = "0px" // Default offset for nested sidebars
|
||||||
|
|
||||||
type SidebarContextProps = {
|
type SidebarContextProps = {
|
||||||
|
|
@ -100,22 +99,6 @@ function SidebarProvider({
|
||||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
|
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
|
||||||
}, [isMobile, setOpen, setOpenMobile])
|
}, [isMobile, setOpen, setOpenMobile])
|
||||||
|
|
||||||
// Adds a keyboard shortcut to toggle the sidebar.
|
|
||||||
React.useEffect(() => {
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
|
||||||
if (
|
|
||||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
|
||||||
(event.metaKey || event.ctrlKey)
|
|
||||||
) {
|
|
||||||
event.preventDefault()
|
|
||||||
toggleSidebar()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown)
|
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
|
||||||
}, [toggleSidebar])
|
|
||||||
|
|
||||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||||
// This makes it easier to style the sidebar with Tailwind classes.
|
// This makes it easier to style the sidebar with Tailwind classes.
|
||||||
const state = open ? "expanded" : "collapsed"
|
const state = open ? "expanded" : "collapsed"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
export type ActiveSection = "ask-ai" | "knowledge" | "agents"
|
export type ActiveSection = "knowledge" | "agents"
|
||||||
|
|
||||||
type SidebarSectionContextProps = {
|
type SidebarSectionContextProps = {
|
||||||
activeSection: ActiveSection
|
activeSection: ActiveSection
|
||||||
|
|
@ -20,7 +20,7 @@ export function useSidebarSection() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SidebarSectionProvider({
|
export function SidebarSectionProvider({
|
||||||
defaultSection = "ask-ai",
|
defaultSection = "knowledge",
|
||||||
onSectionChange,
|
onSectionChange,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
|
|
|
||||||
|
|
@ -175,17 +175,41 @@
|
||||||
.tiptap-editor .ProseMirror ul[data-type="taskList"] {
|
.tiptap-editor .ProseMirror ul[data-type="taskList"] {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
|
margin: 0.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tiptap-editor .ProseMirror ul[data-type="taskList"] li {
|
.tiptap-editor .ProseMirror ul[data-type="taskList"] li {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
|
margin: 0.25em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tiptap-editor .ProseMirror ul[data-type="taskList"] li > label {
|
.tiptap-editor .ProseMirror ul[data-type="taskList"] li > label {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-top: 0.25em;
|
margin-top: 0.25em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap-editor .ProseMirror ul[data-type="taskList"] li > label input[type="checkbox"] {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap-editor .ProseMirror ul[data-type="taskList"] li > div {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap-editor .ProseMirror ul[data-type="taskList"] li[data-checked="true"] > div {
|
||||||
|
text-decoration: line-through;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nested task lists */
|
||||||
|
.tiptap-editor .ProseMirror ul[data-type="taskList"] ul[data-type="taskList"] {
|
||||||
|
margin-left: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Selection */
|
/* Selection */
|
||||||
|
|
@ -228,3 +252,32 @@
|
||||||
width: 0;
|
width: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Wiki Links */
|
||||||
|
.tiptap-editor .ProseMirror .wiki-link,
|
||||||
|
.tiptap-editor .ProseMirror a[data-type="wiki-link"] {
|
||||||
|
color: var(--primary);
|
||||||
|
background-color: color-mix(in srgb, var(--primary) 10%, transparent);
|
||||||
|
padding: 0.1em 0.3em;
|
||||||
|
border-radius: 0.25em;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap-editor .ProseMirror .wiki-link:hover,
|
||||||
|
.tiptap-editor .ProseMirror a[data-type="wiki-link"]:hover {
|
||||||
|
background-color: color-mix(in srgb, var(--primary) 20%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disabled button state */
|
||||||
|
.editor-toolbar button:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selection highlight for when editor loses focus (e.g., link popover open) */
|
||||||
|
.tiptap-editor .ProseMirror .selection-highlight {
|
||||||
|
background-color: color-mix(in srgb, var(--primary) 25%, transparent);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
|
||||||
24
apps/x/pnpm-lock.yaml
generated
24
apps/x/pnpm-lock.yaml
generated
|
|
@ -130,6 +130,12 @@ importers:
|
||||||
'@tiptap/extension-placeholder':
|
'@tiptap/extension-placeholder':
|
||||||
specifier: ^3.15.3
|
specifier: ^3.15.3
|
||||||
version: 3.15.3(@tiptap/extensions@3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3))
|
version: 3.15.3(@tiptap/extensions@3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3))
|
||||||
|
'@tiptap/extension-task-item':
|
||||||
|
specifier: ^3.15.3
|
||||||
|
version: 3.15.3(@tiptap/extension-list@3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3))
|
||||||
|
'@tiptap/extension-task-list':
|
||||||
|
specifier: ^3.15.3
|
||||||
|
version: 3.15.3(@tiptap/extension-list@3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3))
|
||||||
'@tiptap/pm':
|
'@tiptap/pm':
|
||||||
specifier: ^3.15.3
|
specifier: ^3.15.3
|
||||||
version: 3.15.3
|
version: 3.15.3
|
||||||
|
|
@ -1775,6 +1781,16 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^3.15.3
|
'@tiptap/core': ^3.15.3
|
||||||
|
|
||||||
|
'@tiptap/extension-task-item@3.15.3':
|
||||||
|
resolution: {integrity: sha512-bkrmouc1rE5n9ONw2G7+zCGfBRoF2HJWq8REThPMzg/6+L5GJJ5YTN4UmncaP48U9jHX8xeihjgg9Ypenjl4lw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@tiptap/extension-list': ^3.15.3
|
||||||
|
|
||||||
|
'@tiptap/extension-task-list@3.15.3':
|
||||||
|
resolution: {integrity: sha512-nh8iBk1LHVIoqxphLoqZlLAN9fF2i9ZeK+2TjGSS35lfh7sYzRoSjNW0E81Uy48YuCzM1NQYghYR5Qfc7vm4jA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@tiptap/extension-list': ^3.15.3
|
||||||
|
|
||||||
'@tiptap/extension-text@3.15.3':
|
'@tiptap/extension-text@3.15.3':
|
||||||
resolution: {integrity: sha512-MhkBz8ZvrqOKtKNp+ZWISKkLUlTrDR7tbKZc2OnNcUTttL9dz0HwT+cg91GGz19fuo7ttDcfsPV6eVmflvGToA==}
|
resolution: {integrity: sha512-MhkBz8ZvrqOKtKNp+ZWISKkLUlTrDR7tbKZc2OnNcUTttL9dz0HwT+cg91GGz19fuo7ttDcfsPV6eVmflvGToA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -5929,6 +5945,14 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 3.15.3(@tiptap/pm@3.15.3)
|
'@tiptap/core': 3.15.3(@tiptap/pm@3.15.3)
|
||||||
|
|
||||||
|
'@tiptap/extension-task-item@3.15.3(@tiptap/extension-list@3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3))':
|
||||||
|
dependencies:
|
||||||
|
'@tiptap/extension-list': 3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3)
|
||||||
|
|
||||||
|
'@tiptap/extension-task-list@3.15.3(@tiptap/extension-list@3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3))':
|
||||||
|
dependencies:
|
||||||
|
'@tiptap/extension-list': 3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3)
|
||||||
|
|
||||||
'@tiptap/extension-text@3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))':
|
'@tiptap/extension-text@3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 3.15.3(@tiptap/pm@3.15.3)
|
'@tiptap/core': 3.15.3(@tiptap/pm@3.15.3)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue