diff --git a/apps/x/apps/renderer/src/components/ai-elements/file-path-card.tsx b/apps/x/apps/renderer/src/components/ai-elements/file-path-card.tsx index e989f278..7175b5ff 100644 --- a/apps/x/apps/renderer/src/components/ai-elements/file-path-card.tsx +++ b/apps/x/apps/renderer/src/components/ai-elements/file-path-card.tsx @@ -1,26 +1,70 @@ import { useCallback, useEffect, useRef, useState } from 'react' -import { BookOpen, ExternalLink, FileIcon, Pause, Play } from 'lucide-react' +import { BookOpen, FileIcon, FileText, Image, Music, Pause, Play, Video } from 'lucide-react' import { Button } from '@/components/ui/button' import { useFileCard } from '@/contexts/file-card-context' import { wikiLabel } from '@/lib/wiki-links' const AUDIO_EXTENSIONS = new Set(['.wav', '.mp3', '.m4a', '.ogg', '.flac', '.aac']) const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp', '.ico']) +const VIDEO_EXTENSIONS = new Set(['.mp4', '.mov', '.avi', '.mkv', '.webm']) +const DOCUMENT_EXTENSIONS = new Set(['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.rtf', '.csv']) function getExtension(filePath: string): string { const dot = filePath.lastIndexOf('.') return dot >= 0 ? filePath.slice(dot).toLowerCase() : '' } -function getFileName(filePath: string): string { - return filePath.split('/').pop() || filePath +function getFileNameWithoutExt(filePath: string): string { + const name = filePath.split('/').pop() || filePath + const dot = name.lastIndexOf('.') + return dot > 0 ? name.slice(0, dot) : name } -function truncatePath(filePath: string, maxLen = 40): string { - if (filePath.length <= maxLen) return filePath - const parts = filePath.split('/') - if (parts.length <= 2) return filePath - return `.../${parts.slice(-2).join('/')}` +function getFileCategory(ext: string): { label: string; icon: typeof FileIcon } { + if (AUDIO_EXTENSIONS.has(ext)) return { label: 'Audio', icon: Music } + if (IMAGE_EXTENSIONS.has(ext)) return { label: 'Image', icon: Image } + if (VIDEO_EXTENSIONS.has(ext)) return { label: 'Video', icon: Video } + if (DOCUMENT_EXTENSIONS.has(ext)) return { label: 'Document', icon: FileText } + if (ext === '.md') return { label: 'Markdown', icon: FileText } + return { label: 'File', icon: FileIcon } +} + +function getExtLabel(ext: string): string { + return ext ? ext.slice(1).toUpperCase() : '' +} + +// Shared card shell used by all variants +function CardShell({ + icon, + title, + subtitle, + onClick, + action, +}: { + icon: React.ReactNode + title: string + subtitle: string + onClick?: () => void + action?: React.ReactNode +}) { + return ( +
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick() } } : undefined} + className="flex items-center gap-3 rounded-xl border border-border bg-card p-3 pr-4 text-left transition-colors hover:bg-accent/50 cursor-pointer w-full my-2" + > +
+ {icon} +
+
+
{title}
+
{subtitle}
+
+ {action} +
+ ) } // --- Knowledge File Card --- @@ -28,15 +72,21 @@ function truncatePath(filePath: string, maxLen = 40): string { function KnowledgeFileCard({ filePath }: { filePath: string }) { const { onOpenKnowledgeFile } = useFileCard() const label = wikiLabel(filePath) + const ext = getExtension(filePath) + const extLabel = getExtLabel(ext) return ( - + action={ + + } + /> ) } @@ -46,8 +96,11 @@ function AudioFileCard({ filePath }: { filePath: string }) { const [isPlaying, setIsPlaying] = useState(false) const [isLoading, setIsLoading] = useState(false) const audioRef = useRef(null) + const ext = getExtension(filePath) + const extLabel = getExtLabel(ext) - const handlePlayPause = useCallback(async () => { + const handlePlayPause = useCallback(async (e: React.MouseEvent) => { + e.stopPropagation() if (isPlaying && audioRef.current) { audioRef.current.pause() setIsPlaying(false) @@ -88,30 +141,28 @@ function AudioFileCard({ filePath }: { filePath: string }) { } return ( -
- -
-
{getFileName(filePath)}
-
{truncatePath(filePath)}
-
- -
+ + {isPlaying + ? + : + } + + } + title={getFileNameWithoutExt(filePath)} + subtitle={`Audio \u00b7 ${extLabel}`} + onClick={handleOpen} + action={ + + } + /> ) } @@ -121,6 +172,8 @@ function SystemFileCard({ filePath }: { filePath: string }) { const ext = getExtension(filePath) const isImage = IMAGE_EXTENSIONS.has(ext) const [thumbnail, setThumbnail] = useState(null) + const { label: categoryLabel, icon: CategoryIcon } = getFileCategory(ext) + const extLabel = getExtLabel(ext) useEffect(() => { if (!isImage) return @@ -140,21 +193,21 @@ function SystemFileCard({ filePath }: { filePath: string }) { } return ( - + action={ + + } + /> ) } diff --git a/apps/x/apps/renderer/src/components/ai-elements/message.tsx b/apps/x/apps/renderer/src/components/ai-elements/message.tsx index 635d455c..ec3acfc1 100644 --- a/apps/x/apps/renderer/src/components/ai-elements/message.tsx +++ b/apps/x/apps/renderer/src/components/ai-elements/message.tsx @@ -50,7 +50,7 @@ export const MessageContent = ({ className={cn( "is-user:dark flex w-fit max-w-full min-w-0 flex-col gap-2 overflow-hidden text-sm", "group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:bg-secondary group-[.is-user]:px-4 group-[.is-user]:py-3 group-[.is-user]:text-foreground", - "group-[.is-assistant]:text-foreground", + "group-[.is-assistant]:w-full group-[.is-assistant]:text-foreground", className )} {...props}