mirror of
https://github.com/aljazceru/enclava.git
synced 2025-12-18 07:54:29 +01:00
fixing prompt template view
This commit is contained in:
@@ -429,3 +429,97 @@ Please improve this prompt to make it more effective for a {request.chatbot_type
|
||||
except Exception as e:
|
||||
log_api_request("improve_prompt_with_ai_error", {"error": str(e), "user_id": user_id})
|
||||
raise HTTPException(status_code=500, detail=f"Failed to improve prompt: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/seed-defaults")
|
||||
async def seed_default_templates(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Seed default prompt templates for all chatbot types"""
|
||||
user_id = current_user.get("id") if isinstance(current_user, dict) else current_user.id
|
||||
log_api_request("seed_default_templates", {"user_id": user_id})
|
||||
|
||||
# Define default prompts (same as in reset)
|
||||
default_prompts = {
|
||||
"assistant": {
|
||||
"name": "General Assistant",
|
||||
"description": "A helpful, accurate, and friendly AI assistant",
|
||||
"prompt": "You are a helpful AI assistant. Provide accurate, concise, and friendly responses. Always aim to be helpful while being honest about your limitations. When you don't know something, say so clearly. Be professional but approachable in your communication style."
|
||||
},
|
||||
"customer_support": {
|
||||
"name": "Customer Support Agent",
|
||||
"description": "Professional customer service representative focused on solving problems",
|
||||
"prompt": "You are a professional customer support representative. Be empathetic, professional, and solution-focused in all interactions. Always try to understand the customer's issue fully before providing solutions. Use the knowledge base to provide accurate information. When you cannot resolve an issue, explain clearly how the customer can escalate or get further help. Maintain a helpful and patient tone even in difficult situations."
|
||||
},
|
||||
"teacher": {
|
||||
"name": "Educational Tutor",
|
||||
"description": "Patient and encouraging educational facilitator",
|
||||
"prompt": "You are an experienced educational tutor and learning facilitator. Break down complex concepts into understandable, digestible parts. Use analogies, examples, and step-by-step explanations to help students learn. Encourage critical thinking through thoughtful questions. Be patient, supportive, and encouraging. Adapt your teaching style to different learning preferences. When a student makes mistakes, guide them to the correct answer rather than just providing it."
|
||||
},
|
||||
"researcher": {
|
||||
"name": "Research Assistant",
|
||||
"description": "Thorough researcher focused on evidence-based information",
|
||||
"prompt": "You are a thorough research assistant with a focus on accuracy and evidence-based information. Provide well-researched, factual information with sources when possible. Be thorough in your analysis and present multiple perspectives when relevant topics have different viewpoints. Always distinguish between established facts, current research, and opinions. When information is uncertain or contested, clearly communicate the level of confidence and supporting evidence."
|
||||
},
|
||||
"creative_writer": {
|
||||
"name": "Creative Writing Mentor",
|
||||
"description": "Imaginative storytelling expert and writing coach",
|
||||
"prompt": "You are an experienced creative writing mentor and storytelling expert. Help with brainstorming ideas, character development, plot structure, dialogue, and creative expression. Be imaginative and inspiring while providing constructive, actionable feedback. Encourage experimentation with different writing styles and techniques. When reviewing work, balance praise for strengths with specific suggestions for improvement. Help writers find their unique voice while mastering fundamental storytelling principles."
|
||||
},
|
||||
"custom": {
|
||||
"name": "Custom Chatbot",
|
||||
"description": "Customizable AI assistant with user-defined behavior",
|
||||
"prompt": "You are a helpful AI assistant. Your personality, expertise, and behavior will be defined by the user through custom instructions. Follow the user's guidance on how to respond, what tone to use, and what role to play. Be adaptable and responsive to the specific needs and preferences outlined in your configuration."
|
||||
}
|
||||
}
|
||||
|
||||
created_templates = []
|
||||
updated_templates = []
|
||||
|
||||
try:
|
||||
for type_key, template_data in default_prompts.items():
|
||||
# Check if template already exists
|
||||
existing = await db.execute(
|
||||
select(PromptTemplate).where(PromptTemplate.type_key == type_key)
|
||||
)
|
||||
existing_template = existing.scalar_one_or_none()
|
||||
|
||||
if existing_template:
|
||||
# Only update if it's still the default (version 1)
|
||||
if existing_template.version == 1 and existing_template.is_default:
|
||||
existing_template.name = template_data["name"]
|
||||
existing_template.description = template_data["description"]
|
||||
existing_template.system_prompt = template_data["prompt"]
|
||||
existing_template.updated_at = datetime.utcnow()
|
||||
updated_templates.append(type_key)
|
||||
else:
|
||||
# Create new template
|
||||
new_template = PromptTemplate(
|
||||
id=str(uuid.uuid4()),
|
||||
name=template_data["name"],
|
||||
type_key=type_key,
|
||||
description=template_data["description"],
|
||||
system_prompt=template_data["prompt"],
|
||||
is_default=True,
|
||||
is_active=True,
|
||||
version=1,
|
||||
created_at=datetime.utcnow(),
|
||||
updated_at=datetime.utcnow()
|
||||
)
|
||||
db.add(new_template)
|
||||
created_templates.append(type_key)
|
||||
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"message": "Default templates seeded successfully",
|
||||
"created": created_templates,
|
||||
"updated": updated_templates,
|
||||
"total": len(created_templates) + len(updated_templates)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
log_api_request("seed_default_templates_error", {"error": str(e), "user_id": user_id})
|
||||
raise HTTPException(status_code=500, detail=f"Failed to seed default templates: {str(e)}")
|
||||
@@ -1,5 +1,3 @@
|
||||
name: enclava
|
||||
|
||||
services:
|
||||
# Nginx reverse proxy - Main application entry point
|
||||
enclava-nginx:
|
||||
|
||||
@@ -31,6 +31,7 @@ import { Edit3, RotateCcw, Loader2, Save, AlertTriangle, Plus, Sparkles } from '
|
||||
import toast from 'react-hot-toast'
|
||||
import { apiClient } from '@/lib/api-client'
|
||||
import { config } from '@/lib/config'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
|
||||
interface PromptTemplate {
|
||||
id: string
|
||||
@@ -54,6 +55,7 @@ interface PromptVariable {
|
||||
}
|
||||
|
||||
export default function PromptTemplatesPage() {
|
||||
const { user, isAuthenticated } = useAuth()
|
||||
const [templates, setTemplates] = useState<PromptTemplate[]>([])
|
||||
const [variables, setVariables] = useState<PromptVariable[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -62,6 +64,7 @@ export default function PromptTemplatesPage() {
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [improvingWithAI, setImprovingWithAI] = useState(false)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
// Form state for editing
|
||||
const [editForm, setEditForm] = useState({
|
||||
@@ -93,34 +96,58 @@ export default function PromptTemplatesPage() {
|
||||
{ value: "custom", label: "Custom Chatbot" },
|
||||
]
|
||||
|
||||
// Fix hydration mismatch
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (mounted && isAuthenticated) {
|
||||
loadData()
|
||||
} else if (mounted) {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [mounted, isAuthenticated])
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
// Get auth token
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
throw new Error('No authentication token found')
|
||||
}
|
||||
|
||||
// Load templates and variables in parallel
|
||||
// Load templates and variables in parallel using apiClient which handles auth automatically
|
||||
const [templatesResult, variablesResult] = await Promise.allSettled([
|
||||
apiClient.get('/api-internal/v1/prompt-templates/templates'),
|
||||
apiClient.get('/api-internal/v1/prompt-templates/variables')
|
||||
])
|
||||
|
||||
if (templatesResult.status === 'rejected' || variablesResult.status === 'rejected') {
|
||||
throw new Error('Failed to load data')
|
||||
if (templatesResult.status === 'rejected') {
|
||||
console.error('Failed to load templates:', templatesResult.reason)
|
||||
throw new Error('Failed to load templates')
|
||||
}
|
||||
|
||||
setTemplates(templatesResult.value)
|
||||
if (variablesResult.status === 'rejected') {
|
||||
console.error('Failed to load variables:', variablesResult.reason)
|
||||
throw new Error('Failed to load variables')
|
||||
}
|
||||
|
||||
const loadedTemplates = templatesResult.value
|
||||
setTemplates(loadedTemplates)
|
||||
setVariables(variablesResult.value)
|
||||
|
||||
// If no templates exist, seed the defaults
|
||||
if (loadedTemplates.length === 0) {
|
||||
try {
|
||||
await apiClient.post('/api-internal/v1/prompt-templates/seed-defaults', {})
|
||||
// Reload templates after seeding
|
||||
const newTemplates = await apiClient.get('/api-internal/v1/prompt-templates/templates')
|
||||
setTemplates(newTemplates)
|
||||
toast.success('Default prompt templates created successfully')
|
||||
} catch (error) {
|
||||
toast.error('Failed to load prompt templates')
|
||||
console.error('Failed to seed default templates:', error)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading prompt templates:', error)
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to load prompt templates')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -193,13 +220,7 @@ export default function PromptTemplatesPage() {
|
||||
try {
|
||||
setSaving(true)
|
||||
|
||||
// Get auth token
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
throw new Error('No authentication token found')
|
||||
}
|
||||
|
||||
const newTemplate = await apiClient.post('/api-internal/v1/prompt-templates/create', {
|
||||
const newTemplate = await apiClient.post('/api-internal/v1/prompt-templates/templates/create', {
|
||||
name: finalForm.name,
|
||||
type_key: finalForm.type_key,
|
||||
description: finalForm.description,
|
||||
@@ -235,12 +256,6 @@ export default function PromptTemplatesPage() {
|
||||
try {
|
||||
setImprovingWithAI(true)
|
||||
|
||||
// Get auth token
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
throw new Error('No authentication token found')
|
||||
}
|
||||
|
||||
const result = await apiClient.post('/api-internal/v1/prompt-templates/improve', {
|
||||
current_prompt: currentPrompt,
|
||||
chatbot_type: chatbotType,
|
||||
@@ -275,6 +290,32 @@ export default function PromptTemplatesPage() {
|
||||
return displayNames[typeKey] || typeKey
|
||||
}
|
||||
|
||||
// Prevent hydration mismatch by not rendering dynamic content until mounted
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<div className="flex items-center justify-center min-h-64">
|
||||
<div className="h-8 w-8" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Check authentication after mounting
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<Card>
|
||||
<CardContent className="py-8 text-center">
|
||||
<p className="text-muted-foreground">
|
||||
Please <a href="/login" className="underline">log in</a> to access prompt templates.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useRef, useEffect, useCallback, useMemo } from "react"
|
||||
import "./ChatInterface.css"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
@@ -135,8 +136,8 @@ export function ChatInterface({ chatbotId, chatbotName, onClose }: ChatInterface
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Card className="h-[600px] flex flex-col">
|
||||
<CardHeader className="pb-3">
|
||||
<Card className="h-[600px] flex flex-col bg-background border-border">
|
||||
<CardHeader className="pb-3 border-b border-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<MessageCircle className="h-5 w-5" />
|
||||
@@ -161,10 +162,10 @@ export function ChatInterface({ chatbotId, chatbotName, onClose }: ChatInterface
|
||||
>
|
||||
<div className="space-y-4 py-4">
|
||||
{messages.length === 0 && (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
<Bot className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||
<p>Start a conversation with your chatbot!</p>
|
||||
<p className="text-sm">Type a message below to begin.</p>
|
||||
<div className="text-center py-8">
|
||||
<Bot className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
|
||||
<p className="text-foreground/70">Start a conversation with your chatbot!</p>
|
||||
<p className="text-sm text-muted-foreground">Type a message below to begin.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -172,24 +173,25 @@ export function ChatInterface({ chatbotId, chatbotName, onClose }: ChatInterface
|
||||
<div key={message.id} className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||||
<div className={`max-w-[75%] min-w-0 space-y-2`}>
|
||||
<div className={`flex items-start space-x-2 ${message.role === 'user' ? 'flex-row-reverse space-x-reverse' : ''}`}>
|
||||
<div className={`p-2 rounded-full ${message.role === 'user' ? 'bg-primary' : 'bg-muted'}`}>
|
||||
<div className={`p-2 rounded-full ${message.role === 'user' ? 'bg-primary' : 'bg-secondary/50 dark:bg-slate-700'}`}>
|
||||
{message.role === 'user' ? (
|
||||
<User className="h-4 w-4 text-primary-foreground" />
|
||||
) : (
|
||||
<Bot className="h-4 w-4 text-muted-foreground" />
|
||||
<Bot className="h-4 w-4 text-muted-foreground dark:text-slate-300" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 space-y-2 min-w-0">
|
||||
<div className={`rounded-lg p-4 ${
|
||||
message.role === 'user'
|
||||
? 'bg-primary text-primary-foreground ml-auto max-w-fit'
|
||||
: 'bg-muted'
|
||||
? 'bg-primary text-primary-foreground ml-auto max-w-fit chat-message-user'
|
||||
: 'bg-muted text-foreground dark:bg-slate-700 dark:text-slate-200 chat-message-assistant'
|
||||
}`}>
|
||||
<div className="text-sm prose prose-sm max-w-full break-words overflow-hidden markdown-content">
|
||||
<div className="text-sm prose prose-sm dark:prose-invert max-w-full break-words overflow-hidden markdown-content dark:text-slate-200">
|
||||
{message.role === 'user' ? (
|
||||
<div className="whitespace-pre-wrap break-words overflow-x-auto">{message.content}</div>
|
||||
) : (
|
||||
<ReactMarkdown
|
||||
className="dark:text-slate-100"
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeHighlight]}
|
||||
components={{
|
||||
@@ -203,17 +205,17 @@ export function ChatInterface({ chatbotId, chatbotName, onClose }: ChatInterface
|
||||
code: ({ children, className }) => {
|
||||
const isInline = !className;
|
||||
return isInline ? (
|
||||
<code className="bg-muted px-1.5 py-0.5 rounded text-xs font-mono border break-all">
|
||||
<code className="bg-muted/50 text-foreground px-1.5 py-0.5 rounded text-xs font-mono border break-all">
|
||||
{children}
|
||||
</code>
|
||||
) : (
|
||||
<code className={`block bg-muted p-3 rounded text-sm font-mono overflow-x-auto border max-w-full ${className || ''}`}>
|
||||
<code className={`block bg-muted/50 text-foreground p-3 rounded text-sm font-mono overflow-x-auto border max-w-full ${className || ''}`}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
},
|
||||
pre: ({ children }) => (
|
||||
<pre className="bg-muted p-3 rounded overflow-x-auto text-sm font-mono mb-2 border max-w-full">
|
||||
<pre className="bg-muted/50 text-foreground p-3 rounded overflow-x-auto text-sm font-mono mb-2 border max-w-full">
|
||||
{children}
|
||||
</pre>
|
||||
),
|
||||
@@ -235,7 +237,7 @@ export function ChatInterface({ chatbotId, chatbotName, onClose }: ChatInterface
|
||||
{/* Sources for assistant messages */}
|
||||
{message.role === 'assistant' && message.sources && message.sources.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-muted-foreground">Sources:</p>
|
||||
<p className="text-xs text-foreground/60">Sources:</p>
|
||||
<div className="space-y-1">
|
||||
{message.sources.map((source, index) => (
|
||||
<Badge key={index} variant="outline" className="text-xs">
|
||||
@@ -246,7 +248,7 @@ export function ChatInterface({ chatbotId, chatbotName, onClose }: ChatInterface
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<div className="flex items-center justify-between text-xs text-foreground/50 dark:text-slate-400 chat-timestamp">
|
||||
<span>{formatTime(message.timestamp)}</span>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Button
|
||||
@@ -290,13 +292,13 @@ export function ChatInterface({ chatbotId, chatbotName, onClose }: ChatInterface
|
||||
<div className="flex justify-start">
|
||||
<div className="max-w-[80%]">
|
||||
<div className="flex items-start space-x-2">
|
||||
<div className="p-2 rounded-full bg-muted">
|
||||
<div className="p-2 rounded-full bg-secondary/50 dark:bg-slate-700">
|
||||
<Bot className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="bg-muted rounded-lg p-3">
|
||||
<div className="bg-muted dark:bg-slate-700 rounded-lg p-3 chat-thinking">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span className="text-sm text-muted-foreground">Thinking...</span>
|
||||
<Loader2 className="h-4 w-4 animate-spin text-foreground dark:text-slate-200" />
|
||||
<span className="text-sm text-foreground/70 dark:text-slate-200">Thinking...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -314,7 +316,7 @@ export function ChatInterface({ chatbotId, chatbotName, onClose }: ChatInterface
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Type your message..."
|
||||
disabled={isLoading}
|
||||
className="flex-1"
|
||||
className="flex-1 bg-background text-foreground placeholder:text-muted-foreground dark:bg-slate-800 dark:text-slate-200 dark:placeholder:text-slate-400 chat-input"
|
||||
aria-label="Chat message input"
|
||||
aria-describedby="chat-input-help"
|
||||
maxLength={4000}
|
||||
@@ -333,7 +335,7 @@ export function ChatInterface({ chatbotId, chatbotName, onClose }: ChatInterface
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p id="chat-input-help" className="text-xs text-muted-foreground mt-2">
|
||||
<p id="chat-input-help" className="text-xs text-foreground/60 mt-2">
|
||||
Press Enter to send, Shift+Enter for new line. Maximum 4000 characters.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -237,8 +237,26 @@ export function ChatbotManager() {
|
||||
const loadPromptTemplates = async () => {
|
||||
try {
|
||||
const templates = await apiClient.get('/api-internal/v1/prompt-templates/templates')
|
||||
setPromptTemplates(templates)
|
||||
|
||||
// If no templates exist, seed the defaults
|
||||
if (templates.length === 0) {
|
||||
try {
|
||||
await apiClient.post('/api-internal/v1/prompt-templates/seed-defaults', {})
|
||||
// Reload templates after seeding
|
||||
const newTemplates = await apiClient.get('/api-internal/v1/prompt-templates/templates')
|
||||
setPromptTemplates(newTemplates)
|
||||
toast({
|
||||
title: "Templates Initialized",
|
||||
description: "Default prompt templates have been created"
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to seed default templates:', error)
|
||||
}
|
||||
} else {
|
||||
setPromptTemplates(templates)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load prompt templates:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ const AvailablePluginCard: React.FC<AvailablePluginCardProps> = ({ plugin, onIns
|
||||
};
|
||||
|
||||
export const PluginManager: React.FC = () => {
|
||||
const { user, token } = useAuth();
|
||||
const { user, isAuthenticated } = useAuth();
|
||||
const {
|
||||
installedPlugins,
|
||||
availablePlugins,
|
||||
@@ -246,17 +246,17 @@ export const PluginManager: React.FC = () => {
|
||||
|
||||
// Load initial data only when authenticated
|
||||
useEffect(() => {
|
||||
if (user && token) {
|
||||
if (isAuthenticated && user) {
|
||||
refreshInstalledPlugins();
|
||||
}
|
||||
}, [user, token, refreshInstalledPlugins]);
|
||||
}, [isAuthenticated, user, refreshInstalledPlugins]);
|
||||
|
||||
// Load available plugins when switching to discover tab and authenticated
|
||||
useEffect(() => {
|
||||
if (activeTab === 'discover' && user && token) {
|
||||
if (activeTab === 'discover' && isAuthenticated && user) {
|
||||
searchAvailablePlugins();
|
||||
}
|
||||
}, [activeTab, user, token, searchAvailablePlugins]);
|
||||
}, [activeTab, isAuthenticated, user, searchAvailablePlugins]);
|
||||
|
||||
const handlePluginAction = async (action: string, plugin: PluginInfo) => {
|
||||
try {
|
||||
@@ -306,7 +306,7 @@ export const PluginManager: React.FC = () => {
|
||||
const categories = Array.from(new Set(availablePlugins.map(p => p.category)));
|
||||
|
||||
// Show authentication required message if not authenticated (client-side only)
|
||||
if (isClient && (!user || !token)) {
|
||||
if (isClient && !isAuthenticated) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Alert>
|
||||
@@ -319,13 +319,12 @@ export const PluginManager: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Show loading state during hydration
|
||||
// Prevent hydration mismatch by showing minimal UI during SSR
|
||||
if (!isClient) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<RotateCw className="h-6 w-6 animate-spin mr-2" />
|
||||
Loading...
|
||||
<div className="h-6 w-6 mr-2" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user