From 812a12d71b1a73f980589d2a0b85d8aa72969d63 Mon Sep 17 00:00:00 2001 From: songtianlun Date: Wed, 30 Jul 2025 15:06:51 +0800 Subject: [PATCH] fix studio page --- src/app/studio/page.tsx | 219 ++++---------------- src/components/studio/PromptDetailModal.tsx | 182 ++++++++++++++++ 2 files changed, 219 insertions(+), 182 deletions(-) create mode 100644 src/components/studio/PromptDetailModal.tsx diff --git a/src/app/studio/page.tsx b/src/app/studio/page.tsx index 5b3a94b..90b1f05 100644 --- a/src/app/studio/page.tsx +++ b/src/app/studio/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useState } from 'react' +import { useEffect, useState, useCallback } from 'react' import { useTranslations } from 'next-intl' import { useAuth } from '@/hooks/useAuth' import { useRouter } from 'next/navigation' @@ -9,13 +9,12 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { LoadingSpinner } from '@/components/ui/loading-spinner' import { EditPromptModal } from '@/components/studio/EditPromptModal' +import { PromptDetailModal } from '@/components/studio/PromptDetailModal' import { Plus, Search, MoreHorizontal, Edit, - Trash2, - Play, FileText, Calendar, ChevronDown, @@ -51,7 +50,6 @@ export default function StudioPage() { const { user, loading } = useAuth() const router = useRouter() const t = useTranslations('studio') - const tCommon = useTranslations('common') const [prompts, setPrompts] = useState([]) const [filteredPrompts, setFilteredPrompts] = useState([]) @@ -60,7 +58,6 @@ export default function StudioPage() { const [sortField, setSortField] = useState('updatedAt') const [sortOrder, setSortOrder] = useState('desc') const [viewMode, setViewMode] = useState('grid') - const [selectedPrompts, setSelectedPrompts] = useState([]) const [isLoading, setIsLoading] = useState(true) const [allTags, setAllTags] = useState([]) @@ -68,6 +65,10 @@ export default function StudioPage() { const [editingPrompt, setEditingPrompt] = useState(null) const [isEditModalOpen, setIsEditModalOpen] = useState(false) + // Detail Modal + const [detailPrompt, setDetailPrompt] = useState(null) + const [isDetailModalOpen, setIsDetailModalOpen] = useState(false) + // Pagination const [currentPage, setCurrentPage] = useState(1) const itemsPerPage = 12 @@ -79,7 +80,7 @@ export default function StudioPage() { }) // Fetch prompts from API - const fetchPrompts = async () => { + const fetchPrompts = useCallback(async () => { if (!user) return try { @@ -105,9 +106,9 @@ export default function StudioPage() { } finally { setIsLoading(false) } - } + }, [user, currentPage, itemsPerPage, searchQuery, selectedTag, sortField, sortOrder]) - // Initial load effect + // Initialize and fetch tags only once useEffect(() => { if (!loading && !user) { router.push('/signin') @@ -128,42 +129,19 @@ export default function StudioPage() { } } - fetchPrompts() fetchTags() }, [user, loading, router]) - // Refetch when filters change + // Fetch prompts with debounced search useEffect(() => { if (!user) return - const fetchPromptsWithFilters = async () => { - try { - setIsLoading(true) - const params = new URLSearchParams({ - userId: user.id, - page: currentPage.toString(), - limit: itemsPerPage.toString(), - search: searchQuery, - tag: selectedTag, - sortBy: sortField, - sortOrder: sortOrder - }) + const timeoutId = setTimeout(() => { + fetchPrompts() + }, searchQuery ? 300 : 0) // Debounce search queries - const response = await fetch(`/api/prompts?${params}`) - if (response.ok) { - const data = await response.json() - setPrompts(data.prompts) - setPagination(data.pagination) - } - } catch (error) { - console.error('Error fetching prompts:', error) - } finally { - setIsLoading(false) - } - } - - fetchPromptsWithFilters() - }, [currentPage, itemsPerPage, searchQuery, selectedTag, sortField, sortOrder, user]) + return () => clearTimeout(timeoutId) + }, [currentPage, searchQuery, selectedTag, sortField, sortOrder, user, fetchPrompts]) // Since filtering and sorting is now done on the server, // we just use the prompts directly @@ -195,90 +173,21 @@ export default function StudioPage() { setFilteredPrompts(prev => prev.map(p => p.id === updatedPrompt.id ? updatedPrompt : p)) } - const handleDeletePrompt = async (id: string) => { - if (!user || !confirm(t('confirmDelete'))) return - - try { - const response = await fetch(`/api/prompts/${id}?userId=${user.id}`, { - method: 'DELETE' - }) - - if (response.ok) { - await fetchPrompts() // Refresh the list - setSelectedPrompts(prev => prev.filter(pId => pId !== id)) - } - } catch (error) { - console.error('Error deleting prompt:', error) - } + const handleShowDetails = (prompt: Prompt) => { + setDetailPrompt(prompt) + setIsDetailModalOpen(true) } - const handleDuplicatePrompt = async (prompt: Prompt) => { - if (!user) return - - try { - const response = await fetch('/api/prompts/bulk', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - action: 'duplicate', - promptIds: [prompt.id], - userId: user.id - }) - }) - - if (response.ok) { - await fetchPrompts() // Refresh the list - } - } catch (error) { - console.error('Error duplicating prompt:', error) - } + const handleDetailModalClose = () => { + setIsDetailModalOpen(false) + setDetailPrompt(null) } - const handleBulkDelete = async () => { - if (!user || selectedPrompts.length === 0) return - - if (!confirm(`Delete ${selectedPrompts.length} selected prompts?`)) return - - try { - const response = await fetch('/api/prompts/bulk', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - action: 'delete', - promptIds: selectedPrompts, - userId: user.id - }) - }) - - if (response.ok) { - await fetchPrompts() // Refresh the list - setSelectedPrompts([]) - } - } catch (error) { - console.error('Error bulk deleting prompts:', error) - } + const handleDebugPrompt = (id: string) => { + router.push(`/studio/${id}`) } - const handleSelectPrompt = (id: string) => { - setSelectedPrompts(prev => - prev.includes(id) - ? prev.filter(pId => pId !== id) - : [...prev, id] - ) - } - const handleSelectAll = () => { - const currentPagePrompts = filteredPrompts.slice( - (currentPage - 1) * itemsPerPage, - currentPage * itemsPerPage - ) - - if (selectedPrompts.length === currentPagePrompts.length) { - setSelectedPrompts([]) - } else { - setSelectedPrompts(currentPagePrompts.map(p => p.id)) - } - } const formatDate = (dateString: string) => { return new Intl.DateTimeFormat('default', { @@ -396,25 +305,6 @@ export default function StudioPage() { - {/* Bulk Actions */} - {selectedPrompts.length > 0 && ( -
- - {selectedPrompts.length} {t('selectedItems')} - -
- -
-
- )} {/* Content */} @@ -436,9 +326,8 @@ export default function StudioPage() { {currentPrompts.map((prompt) => (
handleSelectPrompt(prompt.id)} + className="bg-card rounded-lg border border-border p-4 hover:shadow-md transition-shadow cursor-pointer" + onClick={() => handleShowDetails(prompt)} >
@@ -466,7 +355,7 @@ export default function StudioPage() { {/* Tags */}
- {prompt.tags?.slice(0, 3).map((tag: any) => { + {prompt.tags?.slice(0, 3).map((tag: string | { name: string }) => { const tagName = typeof tag === 'string' ? tag : tag?.name || ''; return ( - {/* Action Buttons */} -
- - -
))}
@@ -534,14 +396,6 @@ export default function StudioPage() {
{/* List Header */}
-
- 0} - onChange={handleSelectAll} - className="rounded" - /> -
{t('promptName')}
{t('updatedAt')}
{t('lastUsed')}
@@ -553,17 +407,9 @@ export default function StudioPage() { {currentPrompts.map((prompt) => (
handleShowDetails(prompt)} > -
- handleSelectPrompt(prompt.id)} - className="rounded" - /> -
{prompt.name}
@@ -662,6 +508,15 @@ export default function StudioPage() { userId={user?.id || ''} /> )} + + {/* Detail Modal */} +
) } \ No newline at end of file diff --git a/src/components/studio/PromptDetailModal.tsx b/src/components/studio/PromptDetailModal.tsx new file mode 100644 index 0000000..8e21795 --- /dev/null +++ b/src/components/studio/PromptDetailModal.tsx @@ -0,0 +1,182 @@ +'use client' + +import { useTranslations } from 'next-intl' +import { Button } from '@/components/ui/button' +import { X, Edit, Play, Calendar, Tag } from 'lucide-react' + +interface Prompt { + id: string + name: string + description: string | null + content: string + tags: string[] + createdAt: string + updatedAt: string + lastUsed?: string | null + currentVersion?: number + usage?: number +} + +interface PromptDetailModalProps { + prompt: Prompt | null + isOpen: boolean + onClose: () => void + onEdit: (id: string) => void + onDebug: (id: string) => void +} + +export function PromptDetailModal({ + prompt, + isOpen, + onClose, + onEdit, + onDebug +}: PromptDetailModalProps) { + const t = useTranslations('studio') + const tCommon = useTranslations('common') + + const formatDate = (dateString: string) => { + return new Intl.DateTimeFormat('default', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }).format(new Date(dateString)) + } + + if (!isOpen || !prompt) return null + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Header */} +
+
+

+ {prompt.name} +

+ {prompt.description && ( +

+ {prompt.description} +

+ )} +
+ +
+ + {/* Content */} +
+ {/* Tags */} + {prompt.tags && prompt.tags.length > 0 && ( +
+
+ + {t('tags')} +
+
+ {prompt.tags.map((tag: string | { name: string }) => { + const tagName = typeof tag === 'string' ? tag : tag?.name || ''; + return ( + + {tagName} + + ); + })} +
+
+ )} + + {/* Prompt Content */} +
+

{t('promptContent')}

+
+
+                {prompt.content}
+              
+
+
+ + {/* Metadata */} +
+
+
+ + {t('createdAt')}: + {formatDate(prompt.createdAt)} +
+
+ + {t('updatedAt')}: + {formatDate(prompt.updatedAt)} +
+
+
+ {prompt.lastUsed && ( +
+ + {t('lastUsed')}: + {formatDate(prompt.lastUsed)} +
+ )} + {prompt.currentVersion && ( +
+ {t('version')}: + v{prompt.currentVersion} +
+ )} +
+
+
+ + {/* Footer Actions */} +
+ + + +
+
+
+ ) +} \ No newline at end of file