diff --git a/README.md b/README.md index 344e4c8..15e53e9 100644 --- a/README.md +++ b/README.md @@ -106,4 +106,4 @@ See [CHANGELOG.md](CHANGELOG.md) for a list of changes. ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. \ No newline at end of file +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/src/app/api/search/route.ts b/src/app/api/search/route.ts index 67ee405..f64eb25 100644 --- a/src/app/api/search/route.ts +++ b/src/app/api/search/route.ts @@ -1,11 +1,138 @@ import { NextResponse } from 'next/server' import path from 'path' import fs from 'fs/promises' -import { existsSync } from 'fs' +import { existsSync, readFileSync } from 'fs' import Database from 'better-sqlite3' import { resolveWorkspacePath } from '@/utils/workspace-path' +interface SearchResult { + workspaceId: string + workspaceFolder: string | undefined + chatId: string + chatTitle: string + timestamp: string | number + matchingText: string + type: 'chat' | 'composer' +} + +function extractTextFromBubble(bubble: any): string { + let text = '' + + if (bubble.text && bubble.text.trim()) { + text = bubble.text + } + + if (!text && bubble.richText) { + try { + const richTextData = JSON.parse(bubble.richText) + if (richTextData.root && richTextData.root.children) { + text = extractTextFromRichText(richTextData.root.children) + } + } catch (error) { + // ignore parse errors + } + } + + return text +} + +function extractTextFromRichText(children: any[]): string { + let text = '' + + for (const child of children) { + if (child.type === 'text' && child.text) { + text += child.text + } else if (child.children && Array.isArray(child.children)) { + text += extractTextFromRichText(child.children) + } + } + + return text +} + +function getProjectFromFilePath(filePath: string, workspaceEntries: Array<{name: string, workspaceJsonPath: string, folder: string}>): string | null { + const normalizedPath = filePath.replace('file://', '') + + for (const entry of workspaceEntries) { + if (entry.folder) { + const workspacePath = entry.folder.replace('file://', '') + if (normalizedPath.startsWith(workspacePath)) { + return entry.name + } + } + } + return null +} + +function determineProjectForConversation( + composerData: any, + composerId: string, + projectLayoutsMap: Record, + projectNameToWorkspaceId: Record, + workspaceEntries: Array<{name: string, workspaceJsonPath: string, folder: string}>, + bubbleMap: Record +): string | null { + // First, try to get project from projectLayouts (most accurate) + const projectLayouts = projectLayoutsMap[composerId] || [] + for (const projectName of projectLayouts) { + const workspaceId = projectNameToWorkspaceId[projectName] + if (workspaceId) { + return workspaceId + } + } + + // Check newlyCreatedFiles + if (composerData.newlyCreatedFiles && composerData.newlyCreatedFiles.length > 0) { + for (const file of composerData.newlyCreatedFiles) { + if (file.uri && file.uri.path) { + const projectId = getProjectFromFilePath(file.uri.path, workspaceEntries) + if (projectId) return projectId + } + } + } + + // Check codeBlockData + if (composerData.codeBlockData) { + for (const filePath of Object.keys(composerData.codeBlockData)) { + const projectId = getProjectFromFilePath(filePath, workspaceEntries) + if (projectId) return projectId + } + } + + // Check file references in bubbles + const conversationHeaders = composerData.fullConversationHeadersOnly || [] + for (const header of conversationHeaders) { + const bubbleId = header.bubbleId + const bubble = bubbleMap[bubbleId] + + if (bubble) { + if (bubble.relevantFiles && Array.isArray(bubble.relevantFiles)) { + for (const filePath of bubble.relevantFiles) { + if (filePath) { + const projectId = getProjectFromFilePath(filePath, workspaceEntries) + if (projectId) return projectId + } + } + } + + if (bubble.context && bubble.context.fileSelections && Array.isArray(bubble.context.fileSelections)) { + for (const fileSelection of bubble.context.fileSelections) { + if (fileSelection && fileSelection.uri && fileSelection.uri.path) { + const projectId = getProjectFromFilePath(fileSelection.uri.path, workspaceEntries) + if (projectId) return projectId + } + } + } + } + } + + return null +} + export async function GET(request: Request) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let globalDb: any = null + try { const { searchParams } = new URL(request.url) const query = searchParams.get('q') @@ -15,180 +142,381 @@ export async function GET(request: Request) { return NextResponse.json({ error: 'No search query provided' }, { status: 400 }) } + const queryLower = query.toLowerCase() const workspacePath = resolveWorkspacePath() - const results = [] + const results: SearchResult[] = [] + + // Get all workspace entries + const entries = await fs.readdir(workspacePath, { withFileTypes: true }) + const workspaceEntries: Array<{name: string, workspaceJsonPath: string, folder: string}> = [] + + for (const entry of entries) { + if (entry.isDirectory()) { + const workspaceJsonPath = path.join(workspacePath, entry.name, 'workspace.json') + if (existsSync(workspaceJsonPath)) { + try { + const workspaceData = JSON.parse(readFileSync(workspaceJsonPath, 'utf-8')) + workspaceEntries.push({ + name: entry.name, + workspaceJsonPath, + folder: workspaceData.folder || '' + }) + } catch (error) { + workspaceEntries.push({ name: entry.name, workspaceJsonPath, folder: '' }) + } + } + } + } - // Search global storage for chat data first + // Create project name to workspace ID mapping + const projectNameToWorkspaceId: Record = {} + for (const entry of workspaceEntries) { + if (entry.folder) { + const folderName = entry.folder.split('/').pop() || entry.folder.split('\\').pop() + if (folderName) { + projectNameToWorkspaceId[folderName] = entry.name + } + } + } + + // Search in global storage (new format) const globalDbPath = path.join(workspacePath, '..', 'globalStorage', 'state.vscdb') - if (existsSync(globalDbPath) && (type === 'all' || type === 'chat')) { + + if (existsSync(globalDbPath)) { try { - const globalDb = new Database(globalDbPath, { readonly: true }) - const globalChatResult = globalDb.prepare(` - SELECT value FROM ItemTable - WHERE [key] = 'workbench.panel.aichat.view.aichat.chatdata' - `).get() - - if (globalChatResult && (globalChatResult as any).value) { - const chatData = JSON.parse((globalChatResult as any).value) - for (const tab of chatData.tabs) { - let hasMatch = false - let matchingText = '' - - // Search in chat title - if (tab.chatTitle?.toLowerCase().includes(query.toLowerCase())) { - hasMatch = true - matchingText = tab.chatTitle + globalDb = new Database(globalDbPath, { readonly: true }) + + // Get all bubbles for content searching + const bubbleMap: Record = {} + const bubbleRows = globalDb.prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'bubbleId:%'").all() + + for (const rowUntyped of bubbleRows) { + const row = rowUntyped as { key: string, value: string } + const parts = row.key.split(':') + if (parts.length >= 3) { + const bubbleId = parts[2] + try { + const bubble = JSON.parse(row.value) + if (bubble && typeof bubble === 'object') { + bubbleMap[bubbleId] = bubble + } + } catch (parseError) { + // ignore parse errors } + } + } - // Search in bubbles - for (const bubble of tab.bubbles) { - if (bubble.text?.toLowerCase().includes(query.toLowerCase())) { + // Get project layouts for mapping conversations to workspaces + const projectLayoutsMap: Record = {} + const messageContextRows = globalDb.prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'messageRequestContext:%'").all() + + for (const rowUntyped of messageContextRows) { + const row = rowUntyped as { key: string, value: string } + const parts = row.key.split(':') + if (parts.length >= 2) { + const composerId = parts[1] + try { + const context = JSON.parse(row.value) + if (context.projectLayouts && Array.isArray(context.projectLayouts)) { + if (!projectLayoutsMap[composerId]) { + projectLayoutsMap[composerId] = [] + } + for (const layout of context.projectLayouts) { + if (typeof layout === 'string') { + try { + const layoutObj = JSON.parse(layout) + if (layoutObj.rootPath) { + projectLayoutsMap[composerId].push(layoutObj.rootPath) + } + } catch (parseError) { + // Skip invalid JSON + } + } + } + } + } catch (parseError) { + // ignore + } + } + } + + // Search composer/agent data (new format) + if (type === 'all' || type === 'composer') { + const composerRows = globalDb.prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%' AND LENGTH(value) > 10").all() + + for (const rowUntyped of composerRows) { + const row = rowUntyped as { key: string, value: string } + const composerId = row.key.split(':')[1] + + try { + const composerData = JSON.parse(row.value) + + // Determine which project this belongs to + const projectId = determineProjectForConversation( + composerData, + composerId, + projectLayoutsMap, + projectNameToWorkspaceId, + workspaceEntries, + bubbleMap + ) + + // Get workspace folder info + const workspaceEntry = workspaceEntries.find(e => e.name === projectId) + const workspaceFolder = workspaceEntry?.folder + + // Get conversation title + let title = composerData.name || `Conversation ${composerId.slice(0, 8)}` + + // Search in title + let hasMatch = false + let matchingText = '' + + if (title.toLowerCase().includes(queryLower)) { hasMatch = true - matchingText = bubble.text - break + matchingText = title + } + + // Search in conversation bubbles + if (!hasMatch) { + const conversationHeaders = composerData.fullConversationHeadersOnly || [] + for (const header of conversationHeaders) { + const bubble = bubbleMap[header.bubbleId] + if (bubble) { + const text = extractTextFromBubble(bubble) + if (text.toLowerCase().includes(queryLower)) { + hasMatch = true + // Get a snippet around the match + const matchIndex = text.toLowerCase().indexOf(queryLower) + const start = Math.max(0, matchIndex - 50) + const end = Math.min(text.length, matchIndex + query.length + 100) + matchingText = (start > 0 ? '...' : '') + text.slice(start, end) + (end < text.length ? '...' : '') + break + } + } + } } + + if (hasMatch && projectId) { + results.push({ + workspaceId: projectId, + workspaceFolder, + chatId: composerId, + chatTitle: title, + timestamp: composerData.lastUpdatedAt || composerData.createdAt || new Date().toISOString(), + matchingText, + type: 'composer' + }) + } + } catch (parseError) { + console.error(`Error parsing composer data for ${composerId}:`, parseError) } + } + } + + // Also search in the old format chat data (for backwards compatibility) + if (type === 'all' || type === 'chat') { + const globalChatResult = globalDb.prepare(` + SELECT value FROM ItemTable + WHERE [key] = 'workbench.panel.aichat.view.aichat.chatdata' + `).get() + + if (globalChatResult && (globalChatResult as any).value) { + try { + const chatData = JSON.parse((globalChatResult as any).value) + if (chatData.tabs && Array.isArray(chatData.tabs)) { + for (const tab of chatData.tabs) { + let hasMatch = false + let matchingText = '' - if (hasMatch) { - results.push({ - workspaceId: 'global', - workspaceFolder: undefined, - chatId: tab.tabId, - chatTitle: tab.chatTitle || `Chat ${tab.tabId?.substring(0, 8) || 'Untitled'}`, - timestamp: tab.lastSendTime || new Date().toISOString(), - matchingText, - type: 'chat' - }) + if (tab.chatTitle?.toLowerCase().includes(queryLower)) { + hasMatch = true + matchingText = tab.chatTitle + } + + if (!hasMatch && tab.bubbles && Array.isArray(tab.bubbles)) { + for (const bubble of tab.bubbles) { + if (bubble.text?.toLowerCase().includes(queryLower)) { + hasMatch = true + const text = bubble.text + const matchIndex = text.toLowerCase().indexOf(queryLower) + const start = Math.max(0, matchIndex - 50) + const end = Math.min(text.length, matchIndex + query.length + 100) + matchingText = (start > 0 ? '...' : '') + text.slice(start, end) + (end < text.length ? '...' : '') + break + } + } + } + + if (hasMatch) { + results.push({ + workspaceId: 'global', + workspaceFolder: undefined, + chatId: tab.tabId, + chatTitle: tab.chatTitle || `Chat ${tab.tabId?.substring(0, 8) || 'Untitled'}`, + timestamp: tab.lastSendTime || new Date().toISOString(), + matchingText, + type: 'chat' + }) + } + } + } + } catch (parseError) { + console.error('Error parsing global chat data:', parseError) } } } + globalDb.close() + globalDb = null } catch (error) { console.error('Error searching global storage:', error) + if (globalDb) { + globalDb.close() + globalDb = null + } } } - const entries = await fs.readdir(workspacePath, { withFileTypes: true }) + // Also search in workspace-specific state.vscdb files (old format) + for (const entry of workspaceEntries) { + const dbPath = path.join(workspacePath, entry.name, 'state.vscdb') + + if (!existsSync(dbPath)) continue - for (const entry of entries) { - if (entry.isDirectory()) { - const dbPath = path.join(workspacePath, entry.name, 'state.vscdb') - const workspaceJsonPath = path.join(workspacePath, entry.name, 'workspace.json') - - if (!existsSync(dbPath)) continue - - let workspaceFolder = undefined - try { - const workspaceData = JSON.parse(await fs.readFile(workspaceJsonPath, 'utf-8')) - workspaceFolder = workspaceData.folder - } catch (error) { - console.log(`No workspace.json found for ${entry.name}`) - } - - try { - const db = new Database(dbPath, { readonly: true }) + try { + const db = new Database(dbPath, { readonly: true }) - // Search chat logs if type is 'all' or 'chat' - if (type === 'all' || type === 'chat') { - const chatResult = db.prepare(` - SELECT value FROM ItemTable - WHERE [key] = 'workbench.panel.aichat.view.aichat.chatdata' - `).get() + // Search chat logs (old format) + if (type === 'all' || type === 'chat') { + const chatResult = db.prepare(` + SELECT value FROM ItemTable + WHERE [key] = 'workbench.panel.aichat.view.aichat.chatdata' + `).get() - if (chatResult && (chatResult as any).value) { + if (chatResult && (chatResult as any).value) { + try { const chatData = JSON.parse((chatResult as any).value) - for (const tab of chatData.tabs) { - let hasMatch = false - let matchingText = '' - - // Search in chat title - if (tab.chatTitle?.toLowerCase().includes(query.toLowerCase())) { - hasMatch = true - matchingText = tab.chatTitle - } + if (chatData.tabs && Array.isArray(chatData.tabs)) { + for (const tab of chatData.tabs) { + let hasMatch = false + let matchingText = '' - // Search in bubbles - for (const bubble of tab.bubbles) { - if (bubble.text?.toLowerCase().includes(query.toLowerCase())) { + if (tab.chatTitle?.toLowerCase().includes(queryLower)) { hasMatch = true - matchingText = bubble.text - break + matchingText = tab.chatTitle + } + + if (!hasMatch && tab.bubbles && Array.isArray(tab.bubbles)) { + for (const bubble of tab.bubbles) { + if (bubble.text?.toLowerCase().includes(queryLower)) { + hasMatch = true + const text = bubble.text + const matchIndex = text.toLowerCase().indexOf(queryLower) + const start = Math.max(0, matchIndex - 50) + const end = Math.min(text.length, matchIndex + query.length + 100) + matchingText = (start > 0 ? '...' : '') + text.slice(start, end) + (end < text.length ? '...' : '') + break + } + } } - } - if (hasMatch) { - results.push({ - workspaceId: entry.name, - workspaceFolder, - chatId: tab.tabId, - chatTitle: tab.chatTitle || `Chat ${tab.tabId?.substring(0, 8) || 'Untitled'}`, - timestamp: tab.lastSendTime || new Date().toISOString(), - matchingText, - type: 'chat' - }) + if (hasMatch) { + results.push({ + workspaceId: entry.name, + workspaceFolder: entry.folder, + chatId: tab.tabId, + chatTitle: tab.chatTitle || `Chat ${tab.tabId?.substring(0, 8) || 'Untitled'}`, + timestamp: tab.lastSendTime || new Date().toISOString(), + matchingText, + type: 'chat' + }) + } } } + } catch (parseError) { + // ignore } } + } - // Search composer logs if type is 'all' or 'composer' - if (type === 'all' || type === 'composer') { - const composerResult = db.prepare(` - SELECT value FROM ItemTable - WHERE [key] = 'composer.composerData' - `).get() + // Search composer logs (old format) + if (type === 'all' || type === 'composer') { + const composerResult = db.prepare(` + SELECT value FROM ItemTable + WHERE [key] = 'composer.composerData' + `).get() - if (composerResult && (composerResult as any).value) { + if (composerResult && (composerResult as any).value) { + try { const composerData = JSON.parse((composerResult as any).value) - for (const composer of composerData.allComposers) { - let hasMatch = false - let matchingText = '' - - // Search in composer text/title - if (composer.text?.toLowerCase().includes(query.toLowerCase())) { - hasMatch = true - matchingText = composer.text - } + if (composerData.allComposers && Array.isArray(composerData.allComposers)) { + for (const composer of composerData.allComposers) { + let hasMatch = false + let matchingText = '' - // Search in conversation - if (Array.isArray(composer.conversation)) { - for (const message of composer.conversation) { - if (message.text?.toLowerCase().includes(query.toLowerCase())) { - hasMatch = true - matchingText = message.text - break + if (composer.text?.toLowerCase().includes(queryLower)) { + hasMatch = true + matchingText = composer.text + } + + if (!hasMatch && Array.isArray(composer.conversation)) { + for (const message of composer.conversation) { + if (message.text?.toLowerCase().includes(queryLower)) { + hasMatch = true + const text = message.text + const matchIndex = text.toLowerCase().indexOf(queryLower) + const start = Math.max(0, matchIndex - 50) + const end = Math.min(text.length, matchIndex + query.length + 100) + matchingText = (start > 0 ? '...' : '') + text.slice(start, end) + (end < text.length ? '...' : '') + break + } } } - } - if (hasMatch) { - results.push({ - workspaceId: entry.name, - workspaceFolder, - chatId: composer.composerId, - chatTitle: composer.text || `Composer ${composer.composerId.substring(0, 8)}`, - timestamp: composer.lastUpdatedAt || composer.createdAt || new Date().toISOString(), - matchingText, - type: 'composer' - }) + if (hasMatch) { + results.push({ + workspaceId: entry.name, + workspaceFolder: entry.folder, + chatId: composer.composerId, + chatTitle: composer.text || `Composer ${composer.composerId.substring(0, 8)}`, + timestamp: composer.lastUpdatedAt || composer.createdAt || new Date().toISOString(), + matchingText, + type: 'composer' + }) + } } } + } catch (parseError) { + // ignore } } - - db.close() - } catch (error) { - console.error(`Error processing workspace ${entry.name}:`, error) } + + db.close() + } catch (error) { + console.error(`Error processing workspace ${entry.name}:`, error) } } + // Remove duplicates based on chatId + const uniqueResults = results.filter((result, index, self) => + index === self.findIndex((r) => r.chatId === result.chatId) + ) + // Sort results by timestamp, newest first - results.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) + uniqueResults.sort((a, b) => { + const timeA = typeof a.timestamp === 'number' ? a.timestamp : new Date(a.timestamp).getTime() + const timeB = typeof b.timestamp === 'number' ? b.timestamp : new Date(b.timestamp).getTime() + return timeB - timeA + }) - return NextResponse.json({ results }) + return NextResponse.json({ results: uniqueResults }) } catch (error) { console.error('Search failed:', error) + if (globalDb) { + globalDb.close() + } return NextResponse.json({ error: 'Search failed', results: [] }, { status: 500 }) } -} \ No newline at end of file +} diff --git a/src/components/global-search.tsx b/src/components/global-search.tsx new file mode 100644 index 0000000..496c8d1 --- /dev/null +++ b/src/components/global-search.tsx @@ -0,0 +1,267 @@ +"use client" + +import { useState, useEffect, useCallback, useRef } from "react" +import { useRouter } from "next/navigation" +import { Search, MessageSquare, Sparkles, Clock, ArrowRight } from "lucide-react" +import { format } from "date-fns" +import { + Dialog, + DialogContent, + DialogTitle, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Badge } from "@/components/ui/badge" +import { cn } from "@/lib/utils" + +interface SearchResult { + workspaceId: string + workspaceFolder: string + chatId: string + chatTitle: string + timestamp: string | number + matchingText: string + type: 'chat' | 'composer' +} + +export function GlobalSearch() { + const [open, setOpen] = useState(false) + const [query, setQuery] = useState("") + const [results, setResults] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [selectedIndex, setSelectedIndex] = useState(0) + const [filter, setFilter] = useState<'all' | 'chat' | 'composer'>('all') + const inputRef = useRef(null) + const router = useRouter() + + // Keyboard shortcut to open search + useEffect(() => { + const down = (e: KeyboardEvent) => { + if (e.key === "k" && (e.metaKey || e.ctrlKey)) { + e.preventDefault() + setOpen((open) => !open) + } + } + + document.addEventListener("keydown", down) + return () => document.removeEventListener("keydown", down) + }, []) + + // Focus input when dialog opens + useEffect(() => { + if (open) { + setTimeout(() => inputRef.current?.focus(), 0) + } else { + setQuery("") + setResults([]) + setSelectedIndex(0) + } + }, [open]) + + // Debounced search + useEffect(() => { + if (!query.trim()) { + setResults([]) + return + } + + const timer = setTimeout(async () => { + setIsLoading(true) + try { + const response = await fetch(`/api/search?q=${encodeURIComponent(query)}&type=${filter}`) + const data = await response.json() + setResults(data.results || []) + setSelectedIndex(0) + } catch (error) { + console.error('Search failed:', error) + } finally { + setIsLoading(false) + } + }, 300) + + return () => clearTimeout(timer) + }, [query, filter]) + + // Keyboard navigation + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === "ArrowDown") { + e.preventDefault() + setSelectedIndex(i => Math.min(i + 1, results.length - 1)) + } else if (e.key === "ArrowUp") { + e.preventDefault() + setSelectedIndex(i => Math.max(i - 1, 0)) + } else if (e.key === "Enter" && results[selectedIndex]) { + e.preventDefault() + navigateToResult(results[selectedIndex]) + } + }, [results, selectedIndex]) + + const navigateToResult = (result: SearchResult) => { + setOpen(false) + router.push(`/workspace/${result.workspaceId}?tab=${result.chatId}&type=${result.type}`) + } + + const truncateText = (text: string, maxLength: number = 100) => { + if (text.length <= maxLength) return text + return text.slice(0, maxLength) + "..." + } + + return ( + <> + {/* Search trigger button */} + + + + + Search across all projects + + {/* Search input header */} +
+ + setQuery(e.target.value)} + onKeyDown={handleKeyDown} + className="flex-1 border-0 bg-transparent px-3 py-4 text-base focus-visible:ring-0 focus-visible:ring-offset-0" + /> + {isLoading && ( +
+ )} +
+ + {/* Filter tabs */} +
+ {(['all', 'chat', 'composer'] as const).map((type) => ( + + ))} +
+ + {/* Results */} +
+ {query && results.length === 0 && !isLoading && ( +
+ +

No results found for “{query}”

+

Try a different search term

+
+ )} + + {!query && ( +
+ +

Start typing to search

+

+ Search across all your projects and conversations +

+
+ )} + + {results.length > 0 && ( +
+
+ {results.length} result{results.length !== 1 ? 's' : ''} found +
+ {results.map((result, index) => ( + + ))} +
+ )} +
+ + {/* Footer */} +
+
+ + + + Navigate + + + + Open + + + esc + Close + +
+
+ +
+ + ) +} diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx index a872188..938ad00 100644 --- a/src/components/navbar.tsx +++ b/src/components/navbar.tsx @@ -2,6 +2,7 @@ import Link from "next/link" import { ThemeToggle } from "./theme-toggle" +import { GlobalSearch } from "./global-search" export function Navbar() { return ( @@ -11,9 +12,10 @@ export function Navbar() { Cursor Chat Browser
+
) -} \ No newline at end of file +} diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..f38593b --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}