Spaces:
Sleeping
Sleeping
'use client'; | |
import { useState, useEffect, useRef, useMemo } from 'react'; | |
import type { RoleRes, UpdateRoleReq } from '@/service/role'; | |
import { Modal, message } from 'antd'; | |
import { MoreOutlined } from '@ant-design/icons'; | |
import { useLoadInfoStore } from '@/store/useLoadInfoStore'; | |
import { copyToClipboard } from '@/utils/copy'; | |
interface RoleCardProps { | |
role: RoleRes; | |
onClick: () => void; | |
onEdit: (role_id: number, data: UpdateRoleReq) => Promise<void>; | |
onDelete: (role_id: number) => Promise<void>; | |
} | |
export default function RoleCard({ role, onClick, onEdit, onDelete }: RoleCardProps) { | |
const [showMenu, setShowMenu] = useState(false); | |
const [showEditModal, setShowEditModal] = useState(false); | |
const [showDeleteModal, setShowDeleteModal] = useState(false); | |
const [showShareModal, setShowShareModal] = useState(false); | |
const loadInfo = useLoadInfoStore((state) => state.loadInfo); | |
const isRegistered = useMemo(() => { | |
return loadInfo?.status === 'online'; | |
}, [loadInfo]); | |
const menuRef = useRef<HTMLDivElement>(null); | |
const menuButtonRef = useRef<HTMLDivElement>(null); | |
const [messageApi, contextHolder] = message.useMessage(); | |
useEffect(() => { | |
const handleClickOutside = (event: MouseEvent) => { | |
if ( | |
menuRef.current && | |
!menuRef.current.contains(event.target as Node) && | |
menuButtonRef.current && | |
!menuButtonRef.current.contains(event.target as Node) | |
) { | |
setShowMenu(false); | |
} | |
}; | |
document.addEventListener('mousedown', handleClickOutside); | |
return () => { | |
document.removeEventListener('mousedown', handleClickOutside); | |
}; | |
}, []); | |
// Initialize edit form data | |
const initEditForm = () => ({ | |
name: role.name, | |
description: role.description, | |
system_prompt: role.system_prompt, | |
icon: role.icon, | |
is_active: role.is_active, | |
enable_l0_retrieval: role.enable_l0_retrieval | |
}); | |
const [editForm, setEditForm] = useState<UpdateRoleReq>(initEditForm()); | |
const handleCardClick = (e: React.MouseEvent) => { | |
e.stopPropagation(); | |
onClick(); | |
}; | |
const handleMenuClick = (e: React.MouseEvent) => { | |
e.stopPropagation(); | |
setShowMenu(!showMenu); | |
}; | |
const handleEdit = (e: React.MouseEvent) => { | |
e.stopPropagation(); | |
setShowMenu(false); | |
// Reset form data when opening edit modal | |
setEditForm(initEditForm()); | |
setShowEditModal(true); | |
}; | |
const handleDelete = (e: React.MouseEvent) => { | |
e.stopPropagation(); | |
setShowMenu(false); | |
setShowDeleteModal(true); | |
}; | |
const handleEditSubmit = async () => { | |
await onEdit(role.id, editForm); | |
setShowEditModal(false); | |
}; | |
const handleDeleteConfirm = async () => { | |
await onDelete(role.id); | |
setShowDeleteModal(false); | |
}; | |
return ( | |
<div className="relative border rounded-lg p-4 hover:shadow-md transition-shadow bg-white"> | |
{contextHolder} | |
<div className="w-full text-left cursor-pointer" onClick={handleCardClick}> | |
<div className="flex justify-between items-start mb-3"> | |
<div className="flex items-center gap-2"> | |
{role.icon && <img alt={role.name} className="w-6 h-6 rounded" src={role.icon} />} | |
<h3 className="text-lg font-medium">{role.name}</h3> | |
</div> | |
<div className="flex items-center gap-2"> | |
<div | |
ref={menuButtonRef} | |
className="p-1 hover:bg-gray-100 rounded-full cursor-pointer" | |
onClick={handleMenuClick} | |
> | |
<MoreOutlined className="text-lg text-gray-500" /> | |
</div> | |
</div> | |
</div> | |
<p className="text-gray-600 text-sm line-clamp-2 mb-3">{role.description}</p> | |
<div className="text-sm text-gray-500 flex justify-between"> | |
<span>Created {new Date(role.create_time).toLocaleDateString()}</span> | |
<span>Updated {new Date(role.update_time).toLocaleDateString()}</span> | |
</div> | |
</div> | |
{/* Dropdown menu */} | |
{showMenu && ( | |
<div | |
ref={menuRef} | |
className="absolute right-4 top-12 w-32 bg-white border rounded-lg shadow-lg py-1 z-10" | |
> | |
<div | |
className="w-full px-4 py-2 text-left text-sm hover:bg-gray-100 cursor-pointer flex items-center gap-2" | |
onClick={(e) => { | |
e.stopPropagation(); | |
setShowMenu(false); | |
setShowShareModal(true); | |
}} | |
> | |
<svg | |
className="w-[14px] h-[14px]" | |
fill="none" | |
viewBox="0 0 24 24" | |
xmlns="http://www.w3.org/2000/svg" | |
> | |
<circle cx="18" cy="5" r="2.5" stroke="currentColor" strokeWidth="1.5" /> | |
<circle cx="6" cy="12" r="2.5" stroke="currentColor" strokeWidth="1.5" /> | |
<circle cx="18" cy="19" r="2.5" stroke="currentColor" strokeWidth="1.5" /> | |
<path d="M15.0355 6.41421L8.96447 10.5858" stroke="currentColor" strokeWidth="1.5" /> | |
<path d="M15.0355 17.5858L8.96447 13.4142" stroke="currentColor" strokeWidth="1.5" /> | |
</svg> | |
share | |
</div> | |
<div | |
className="w-full px-4 py-2 text-left text-sm hover:bg-gray-100 cursor-pointer flex items-center gap-2" | |
onClick={handleEdit} | |
> | |
<svg | |
className="w-[14px] h-[14px]" | |
fill="none" | |
viewBox="0 0 24 24" | |
xmlns="http://www.w3.org/2000/svg" | |
> | |
<path | |
d="M16.5 3.5L20.5 7.5L7 21H3V17L16.5 3.5Z" | |
stroke="currentColor" | |
strokeLinejoin="round" | |
strokeWidth="1.5" | |
/> | |
<path | |
d="M14 6L18 10" | |
stroke="currentColor" | |
strokeLinejoin="round" | |
strokeWidth="1.5" | |
/> | |
</svg> | |
Edit | |
</div> | |
<div | |
className="w-full px-4 py-2 text-left text-sm hover:bg-gray-100 text-red-600 cursor-pointer flex items-center gap-2" | |
onClick={handleDelete} | |
> | |
<svg | |
className="w-[14px] h-[14px]" | |
fill="none" | |
viewBox="0 0 24 24" | |
xmlns="http://www.w3.org/2000/svg" | |
> | |
<path d="M4 7H20" stroke="currentColor" strokeLinecap="round" strokeWidth="1.5" /> | |
<path d="M10 11V17" stroke="currentColor" strokeLinecap="round" strokeWidth="1.5" /> | |
<path d="M14 11V17" stroke="currentColor" strokeLinecap="round" strokeWidth="1.5" /> | |
<path | |
d="M5 7L6 19C6 20.1046 6.89543 21 8 21H16C17.1046 21 18 20.1046 18 19L19 7" | |
stroke="currentColor" | |
strokeLinecap="round" | |
strokeLinejoin="round" | |
strokeWidth="1.5" | |
/> | |
<path | |
d="M9 7V4C9 3.44772 9.44772 3 10 3H14C14.5523 3 15 3.44772 15 4V7" | |
stroke="currentColor" | |
strokeLinecap="round" | |
strokeLinejoin="round" | |
strokeWidth="1.5" | |
/> | |
</svg> | |
Delete | |
</div> | |
</div> | |
)} | |
{/* Edit modal */} | |
<Modal | |
centered | |
okText="Save" | |
onCancel={() => { | |
setShowEditModal(false); | |
// Reset form data when closing modal | |
setEditForm(initEditForm()); | |
}} | |
onOk={handleEditSubmit} | |
open={showEditModal} | |
title="Edit Role" | |
> | |
<div className="space-y-4"> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label> | |
<input | |
className="w-full px-3 py-2 border rounded-lg" | |
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })} | |
type="text" | |
value={editForm.name} | |
/> | |
</div> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-1">Description</label> | |
<textarea | |
className="w-full px-3 py-2 border rounded-lg" | |
onChange={(e) => setEditForm({ ...editForm, description: e.target.value })} | |
rows={3} | |
value={editForm.description} | |
/> | |
</div> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-1">System Prompt</label> | |
<textarea | |
className="w-full px-3 py-2 border rounded-lg" | |
onChange={(e) => setEditForm({ ...editForm, system_prompt: e.target.value })} | |
rows={3} | |
value={editForm.system_prompt} | |
/> | |
</div> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-1">Icon URL</label> | |
<input | |
className="w-full px-3 py-2 border rounded-lg" | |
onChange={(e) => setEditForm({ ...editForm, icon: e.target.value })} | |
type="text" | |
value={editForm.icon} | |
/> | |
</div> | |
</div> | |
</Modal> | |
{/* Delete confirmation modal */} | |
<Modal | |
centered | |
okButtonProps={{ danger: true }} | |
okText="Delete" | |
onCancel={() => setShowDeleteModal(false)} | |
onOk={handleDeleteConfirm} | |
open={showDeleteModal} | |
title="Delete Role" | |
> | |
<p>Are you sure you want to delete this role? This action cannot be undone.</p> | |
</Modal> | |
{/* Share modal */} | |
<Modal | |
footer={null} | |
onCancel={() => setShowShareModal(false)} | |
open={showShareModal} | |
title="Share" | |
> | |
{isRegistered ? ( | |
<div className="space-y-4"> | |
<input | |
className="w-full px-3 py-2 border rounded-lg bg-gray-50" | |
readOnly | |
value="https://secondme.ai/share/123456" | |
/> | |
<button | |
className="w-full px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors" | |
onClick={async () => { | |
copyToClipboard('https://secondme.ai/share/123456') | |
.then(() => { | |
messageApi.success({ | |
content: 'Link copied.' | |
}); | |
}) | |
.catch(() => { | |
messageApi.error({ | |
content: 'Failed to copy, please copy manually' | |
}); | |
}); | |
}} | |
> | |
Copy Link | |
</button> | |
</div> | |
) : ( | |
<div className="text-center text-red-500">Please register this Upload before sharing</div> | |
)} | |
</Modal> | |
</div> | |
); | |
} | |