fixing prompt template view

This commit is contained in:
2025-09-12 06:56:38 +02:00
parent 0e7c7e2017
commit 9130c0953b
6 changed files with 212 additions and 60 deletions

View File

@@ -428,4 +428,98 @@ Please improve this prompt to make it more effective for a {request.chatbot_type
raise raise
except Exception as e: except Exception as e:
log_api_request("improve_prompt_with_ai_error", {"error": str(e), "user_id": user_id}) 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)}") 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)}")

View File

@@ -1,5 +1,3 @@
name: enclava
services: services:
# Nginx reverse proxy - Main application entry point # Nginx reverse proxy - Main application entry point
enclava-nginx: enclava-nginx:

View File

@@ -31,6 +31,7 @@ import { Edit3, RotateCcw, Loader2, Save, AlertTriangle, Plus, Sparkles } from '
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { apiClient } from '@/lib/api-client' import { apiClient } from '@/lib/api-client'
import { config } from '@/lib/config' import { config } from '@/lib/config'
import { useAuth } from '@/contexts/AuthContext'
interface PromptTemplate { interface PromptTemplate {
id: string id: string
@@ -54,6 +55,7 @@ interface PromptVariable {
} }
export default function PromptTemplatesPage() { export default function PromptTemplatesPage() {
const { user, isAuthenticated } = useAuth()
const [templates, setTemplates] = useState<PromptTemplate[]>([]) const [templates, setTemplates] = useState<PromptTemplate[]>([])
const [variables, setVariables] = useState<PromptVariable[]>([]) const [variables, setVariables] = useState<PromptVariable[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@@ -62,6 +64,7 @@ export default function PromptTemplatesPage() {
const [showCreateDialog, setShowCreateDialog] = useState(false) const [showCreateDialog, setShowCreateDialog] = useState(false)
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [improvingWithAI, setImprovingWithAI] = useState(false) const [improvingWithAI, setImprovingWithAI] = useState(false)
const [mounted, setMounted] = useState(false)
// Form state for editing // Form state for editing
const [editForm, setEditForm] = useState({ const [editForm, setEditForm] = useState({
@@ -93,34 +96,58 @@ export default function PromptTemplatesPage() {
{ value: "custom", label: "Custom Chatbot" }, { value: "custom", label: "Custom Chatbot" },
] ]
// Fix hydration mismatch
useEffect(() => { useEffect(() => {
loadData() setMounted(true)
}, []) }, [])
useEffect(() => {
if (mounted && isAuthenticated) {
loadData()
} else if (mounted) {
setLoading(false)
}
}, [mounted, isAuthenticated])
const loadData = async () => { const loadData = async () => {
try { try {
setLoading(true) setLoading(true)
// Get auth token // Load templates and variables in parallel using apiClient which handles auth automatically
const token = localStorage.getItem('token')
if (!token) {
throw new Error('No authentication token found')
}
// Load templates and variables in parallel
const [templatesResult, variablesResult] = await Promise.allSettled([ const [templatesResult, variablesResult] = await Promise.allSettled([
apiClient.get('/api-internal/v1/prompt-templates/templates'), apiClient.get('/api-internal/v1/prompt-templates/templates'),
apiClient.get('/api-internal/v1/prompt-templates/variables') apiClient.get('/api-internal/v1/prompt-templates/variables')
]) ])
if (templatesResult.status === 'rejected' || variablesResult.status === 'rejected') { if (templatesResult.status === 'rejected') {
throw new Error('Failed to load data') console.error('Failed to load templates:', templatesResult.reason)
throw new Error('Failed to load templates')
}
if (variablesResult.status === 'rejected') {
console.error('Failed to load variables:', variablesResult.reason)
throw new Error('Failed to load variables')
} }
setTemplates(templatesResult.value) const loadedTemplates = templatesResult.value
setTemplates(loadedTemplates)
setVariables(variablesResult.value) 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) {
console.error('Failed to seed default templates:', error)
}
}
} catch (error) { } catch (error) {
toast.error('Failed to load prompt templates') console.error('Error loading prompt templates:', error)
toast.error(error instanceof Error ? error.message : 'Failed to load prompt templates')
} finally { } finally {
setLoading(false) setLoading(false)
} }
@@ -193,13 +220,7 @@ export default function PromptTemplatesPage() {
try { try {
setSaving(true) setSaving(true)
// Get auth token const newTemplate = await apiClient.post('/api-internal/v1/prompt-templates/templates/create', {
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', {
name: finalForm.name, name: finalForm.name,
type_key: finalForm.type_key, type_key: finalForm.type_key,
description: finalForm.description, description: finalForm.description,
@@ -235,12 +256,6 @@ export default function PromptTemplatesPage() {
try { try {
setImprovingWithAI(true) 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', { const result = await apiClient.post('/api-internal/v1/prompt-templates/improve', {
current_prompt: currentPrompt, current_prompt: currentPrompt,
chatbot_type: chatbotType, chatbot_type: chatbotType,
@@ -275,6 +290,32 @@ export default function PromptTemplatesPage() {
return displayNames[typeKey] || typeKey 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) { if (loading) {
return ( return (
<div className="container mx-auto py-8 px-4"> <div className="container mx-auto py-8 px-4">

View File

@@ -1,6 +1,7 @@
"use client" "use client"
import { useState, useRef, useEffect, useCallback, useMemo } from "react" import { useState, useRef, useEffect, useCallback, useMemo } from "react"
import "./ChatInterface.css"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
@@ -135,8 +136,8 @@ export function ChatInterface({ chatbotId, chatbotName, onClose }: ChatInterface
}, []) }, [])
return ( return (
<Card className="h-[600px] flex flex-col"> <Card className="h-[600px] flex flex-col bg-background border-border">
<CardHeader className="pb-3"> <CardHeader className="pb-3 border-b border-border">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<MessageCircle className="h-5 w-5" /> <MessageCircle className="h-5 w-5" />
@@ -161,10 +162,10 @@ export function ChatInterface({ chatbotId, chatbotName, onClose }: ChatInterface
> >
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
{messages.length === 0 && ( {messages.length === 0 && (
<div className="text-center text-muted-foreground py-8"> <div className="text-center py-8">
<Bot className="h-12 w-12 mx-auto mb-4 opacity-50" /> <Bot className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
<p>Start a conversation with your chatbot!</p> <p className="text-foreground/70">Start a conversation with your chatbot!</p>
<p className="text-sm">Type a message below to begin.</p> <p className="text-sm text-muted-foreground">Type a message below to begin.</p>
</div> </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 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={`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={`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' ? ( {message.role === 'user' ? (
<User className="h-4 w-4 text-primary-foreground" /> <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>
<div className="flex-1 space-y-2 min-w-0"> <div className="flex-1 space-y-2 min-w-0">
<div className={`rounded-lg p-4 ${ <div className={`rounded-lg p-4 ${
message.role === 'user' message.role === 'user'
? 'bg-primary text-primary-foreground ml-auto max-w-fit' ? 'bg-primary text-primary-foreground ml-auto max-w-fit chat-message-user'
: 'bg-muted' : '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' ? ( {message.role === 'user' ? (
<div className="whitespace-pre-wrap break-words overflow-x-auto">{message.content}</div> <div className="whitespace-pre-wrap break-words overflow-x-auto">{message.content}</div>
) : ( ) : (
<ReactMarkdown <ReactMarkdown
className="dark:text-slate-100"
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeHighlight]} rehypePlugins={[rehypeHighlight]}
components={{ components={{
@@ -203,17 +205,17 @@ export function ChatInterface({ chatbotId, chatbotName, onClose }: ChatInterface
code: ({ children, className }) => { code: ({ children, className }) => {
const isInline = !className; const isInline = !className;
return isInline ? ( 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} {children}
</code> </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} {children}
</code> </code>
) )
}, },
pre: ({ children }) => ( 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} {children}
</pre> </pre>
), ),
@@ -235,7 +237,7 @@ export function ChatInterface({ chatbotId, chatbotName, onClose }: ChatInterface
{/* Sources for assistant messages */} {/* Sources for assistant messages */}
{message.role === 'assistant' && message.sources && message.sources.length > 0 && ( {message.role === 'assistant' && message.sources && message.sources.length > 0 && (
<div className="space-y-2"> <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"> <div className="space-y-1">
{message.sources.map((source, index) => ( {message.sources.map((source, index) => (
<Badge key={index} variant="outline" className="text-xs"> <Badge key={index} variant="outline" className="text-xs">
@@ -246,7 +248,7 @@ export function ChatInterface({ chatbotId, chatbotName, onClose }: ChatInterface
</div> </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> <span>{formatTime(message.timestamp)}</span>
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
<Button <Button
@@ -290,13 +292,13 @@ export function ChatInterface({ chatbotId, chatbotName, onClose }: ChatInterface
<div className="flex justify-start"> <div className="flex justify-start">
<div className="max-w-[80%]"> <div className="max-w-[80%]">
<div className="flex items-start space-x-2"> <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" /> <Bot className="h-4 w-4 text-muted-foreground" />
</div> </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"> <div className="flex items-center space-x-2">
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin text-foreground dark:text-slate-200" />
<span className="text-sm text-muted-foreground">Thinking...</span> <span className="text-sm text-foreground/70 dark:text-slate-200">Thinking...</span>
</div> </div>
</div> </div>
</div> </div>
@@ -314,7 +316,7 @@ export function ChatInterface({ chatbotId, chatbotName, onClose }: ChatInterface
onKeyPress={handleKeyPress} onKeyPress={handleKeyPress}
placeholder="Type your message..." placeholder="Type your message..."
disabled={isLoading} 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-label="Chat message input"
aria-describedby="chat-input-help" aria-describedby="chat-input-help"
maxLength={4000} maxLength={4000}
@@ -333,7 +335,7 @@ export function ChatInterface({ chatbotId, chatbotName, onClose }: ChatInterface
)} )}
</Button> </Button>
</div> </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. Press Enter to send, Shift+Enter for new line. Maximum 4000 characters.
</p> </p>
</div> </div>

View File

@@ -237,8 +237,26 @@ export function ChatbotManager() {
const loadPromptTemplates = async () => { const loadPromptTemplates = async () => {
try { try {
const templates = await apiClient.get('/api-internal/v1/prompt-templates/templates') 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) { } catch (error) {
console.error('Failed to load prompt templates:', error)
} }
} }

View File

@@ -217,7 +217,7 @@ const AvailablePluginCard: React.FC<AvailablePluginCardProps> = ({ plugin, onIns
}; };
export const PluginManager: React.FC = () => { export const PluginManager: React.FC = () => {
const { user, token } = useAuth(); const { user, isAuthenticated } = useAuth();
const { const {
installedPlugins, installedPlugins,
availablePlugins, availablePlugins,
@@ -246,17 +246,17 @@ export const PluginManager: React.FC = () => {
// Load initial data only when authenticated // Load initial data only when authenticated
useEffect(() => { useEffect(() => {
if (user && token) { if (isAuthenticated && user) {
refreshInstalledPlugins(); refreshInstalledPlugins();
} }
}, [user, token, refreshInstalledPlugins]); }, [isAuthenticated, user, refreshInstalledPlugins]);
// Load available plugins when switching to discover tab and authenticated // Load available plugins when switching to discover tab and authenticated
useEffect(() => { useEffect(() => {
if (activeTab === 'discover' && user && token) { if (activeTab === 'discover' && isAuthenticated && user) {
searchAvailablePlugins(); searchAvailablePlugins();
} }
}, [activeTab, user, token, searchAvailablePlugins]); }, [activeTab, isAuthenticated, user, searchAvailablePlugins]);
const handlePluginAction = async (action: string, plugin: PluginInfo) => { const handlePluginAction = async (action: string, plugin: PluginInfo) => {
try { try {
@@ -306,7 +306,7 @@ export const PluginManager: React.FC = () => {
const categories = Array.from(new Set(availablePlugins.map(p => p.category))); const categories = Array.from(new Set(availablePlugins.map(p => p.category)));
// Show authentication required message if not authenticated (client-side only) // Show authentication required message if not authenticated (client-side only)
if (isClient && (!user || !token)) { if (isClient && !isAuthenticated) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Alert> <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) { if (!isClient) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<RotateCw className="h-6 w-6 animate-spin mr-2" /> <div className="h-6 w-6 mr-2" />
Loading...
</div> </div>
</div> </div>
); );