Skip to content
40 changes: 23 additions & 17 deletions frontend/src/components/ChatArea.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Send, Paperclip, X, Square } from 'lucide-react'
import Message from './Message'
import WelcomeScreen from './WelcomeScreen'
import EnabledToolsIndicator from './EnabledToolsIndicator'
import PromptSelector from './PromptSelector'

const ChatArea = () => {
const [inputValue, setInputValue] = useState('')
Expand Down Expand Up @@ -597,26 +598,27 @@ const ChatArea = () => {
</div>
)}

<form onSubmit={handleSubmit} className="flex gap-3">
<button
type="button"
onClick={triggerFileUpload}
className="px-3 py-3 bg-gray-700 hover:bg-gray-600 text-gray-300 rounded-lg flex items-center justify-center transition-colors flex-shrink-0"
title="Upload files"
>
<Paperclip className="w-5 h-5" />
</button>
{agentModeEnabled && (isThinking || agentPendingQuestion) && (
<form onSubmit={handleSubmit} className="flex flex-col gap-2">
<div className="flex gap-3">
<button
type="button"
onClick={stopAgent}
className="px-3 py-3 bg-red-700 hover:bg-red-600 text-white rounded-lg flex items-center justify-center transition-colors flex-shrink-0"
title="Stop agent"
onClick={triggerFileUpload}
className="px-3 py-3 bg-gray-700 hover:bg-gray-600 text-gray-300 rounded-lg flex items-center justify-center transition-colors flex-shrink-0"
title="Upload files"
>
<Square className="w-5 h-5" />
<Paperclip className="w-5 h-5" />
</button>
)}
<div className="flex-1 relative">
{agentModeEnabled && (isThinking || agentPendingQuestion) && (
<button
type="button"
onClick={stopAgent}
className="px-3 py-3 bg-red-700 hover:bg-red-600 text-white rounded-lg flex items-center justify-center transition-colors flex-shrink-0"
title="Stop agent"
>
<Square className="w-5 h-5" />
</button>
)}
<div className="flex-1 relative">
<textarea
ref={textareaRef}
value={inputValue}
Expand Down Expand Up @@ -701,6 +703,7 @@ const ChatArea = () => {
>
<Send className="w-5 h-5" />
</button>
</div>
</form>

{/* Hidden file input */}
Expand All @@ -714,7 +717,10 @@ const ChatArea = () => {
/>

<div className="flex items-center justify-between mt-2 text-xs text-gray-400">
<span>Press Shift + Enter for new line</span>
<div className="flex items-center gap-3">
<PromptSelector />
<span>Press Shift + Enter for new line</span>
</div>
{Object.keys(uploadedFiles).length > 0 && (
<span>{Object.keys(uploadedFiles).length} file(s) uploaded</span>
)}
Expand Down
25 changes: 7 additions & 18 deletions frontend/src/components/EnabledToolsIndicator.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,28 @@ import { useChat } from '../contexts/ChatContext'
import { X } from 'lucide-react'

const EnabledToolsIndicator = () => {
const { selectedTools, selectedPrompts, toggleTool, togglePrompt } = useChat()
const { selectedTools, toggleTool } = useChat()

const allTools = Array.from(selectedTools).map(key => {
const parts = key.split('_')
return { name: parts.slice(1).join('_'), key, type: 'tool' }
})

const allPrompts = Array.from(selectedPrompts).map(key => {
const parts = key.split('_')
return { name: parts.slice(1).join('_'), key, type: 'prompt' }
})

const items = [...allTools, ...allPrompts]
if (items.length === 0) return null
// Only show tools (prompts are now in the PromptSelector)
if (allTools.length === 0) return null

return (
<div className="flex items-start gap-2 text-xs text-gray-400 mb-2">
<span className="mt-1">Active:</span>
<span className="mt-1">Active Tools:</span>
<div className="flex-1 flex flex-wrap gap-1">
{items.map((item, idx) => (
{allTools.map((item, idx) => (
<div
key={idx}
className={`px-2 py-1 rounded flex items-center gap-1 ${item.type === 'prompt' ? 'bg-purple-800 text-purple-200' : 'bg-gray-700 text-gray-300'}`}
className="px-2 py-1 rounded flex items-center gap-1 bg-gray-700 text-gray-300"
>
<span>{item.name}</span>
<button
onClick={() => {
if (item.type === 'tool') {
toggleTool(item.key)
} else {
togglePrompt(item.key)
}
}}
onClick={() => toggleTool(item.key)}
className="hover:bg-red-600 hover:bg-opacity-50 rounded p-0.5 transition-colors"
title={`Remove ${item.name}`}
>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ const Header = ({ onToggleRag, onToggleTools, onToggleFiles, onToggleCanvas, onC
<button
onClick={onToggleTools}
className="p-2 rounded-lg bg-yellow-500 border border-red-500 block"
title="Toggle Tools"
title="Toggle Tools, Integrations, and Prompts"
>
<Wrench className="w-5 h-5" />
</button>
Expand Down
172 changes: 172 additions & 0 deletions frontend/src/components/PromptSelector.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { useChat } from '../contexts/ChatContext'
import { ChevronDown, Sparkles } from 'lucide-react'
import { useState, useRef, useEffect } from 'react'

const PromptSelector = () => {
const { prompts, selectedPrompts, togglePrompt, makePromptActive, selectedTools } = useChat()
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef(null)

// Get all selected prompt keys as an array
const selectedPromptKeys = selectedPrompts && selectedPrompts.size > 0
? Array.from(selectedPrompts)
: []

// Get only the prompts that are actually selected (enabled)
const allPrompts = []
prompts.forEach(server => {
if (server.prompts && server.prompts.length > 0) {
server.prompts.forEach(prompt => {
const promptKey = `${server.server}_${prompt.name}`
// Only include prompts that are actually selected
if (selectedPromptKeys.includes(promptKey)) {
allPrompts.push({
key: promptKey,
server: server.server,
name: prompt.name,
description: prompt.description || '',
compliance_level: server.compliance_level
})
}
})
}
})

// The first selected prompt is the active one (used by backend)
const activePromptKey = selectedPromptKeys.length > 0 ? selectedPromptKeys[0] : null

// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false)
}
}

if (isOpen) {
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}
}, [isOpen])

const handlePromptSelect = (promptKey) => {
// Just make this prompt active without reordering
if (makePromptActive) {
makePromptActive(promptKey)
}
}

// Get display text for the button - show the active (first) prompt name
const getButtonText = () => {
if (selectedPromptKeys.length === 0) return 'Default Prompt'
// Always show the active (first) prompt name
const key = selectedPromptKeys[0]
const idx = key.indexOf('_')
return idx === -1 ? key : key.slice(idx + 1)
}

return (
<div ref={dropdownRef} className="relative">
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-1 text-xs text-gray-400 hover:text-purple-400 transition-colors"
title="Select custom prompts"
>
<Sparkles className="w-3 h-3" />
<span className="underline decoration-dotted">
{getButtonText()}
</span>
<ChevronDown className={`w-3 h-3 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
</button>

{isOpen && (
<div className="absolute bottom-full left-0 mb-1 w-80 bg-gray-800 border border-gray-600 rounded-lg shadow-lg max-h-96 overflow-y-auto z-50">
<div className="p-2 border-b border-gray-700 bg-gray-750">
<div className="text-xs font-semibold text-gray-300 flex items-center gap-2">
<Sparkles className="w-3 h-3 text-purple-400" />
Custom Prompts
</div>
<div className="text-xs text-gray-400 mt-1">
Select prompts to customize AI behavior
</div>
</div>

{/* Default Prompt option - always available */}
<button
onClick={() => {
// Clear all selected prompts to use default
selectedPromptKeys.forEach(key => togglePrompt(key))
setIsOpen(false)
}}
className={`w-full px-3 py-2 text-left hover:bg-gray-700 transition-colors border-b border-gray-700 ${
selectedPromptKeys.length === 0 ? 'bg-blue-900/30' : ''
}`}
>
<div className="flex items-center justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="font-medium text-gray-200 flex items-center gap-2">
{selectedPromptKeys.length === 0 && <span className="text-blue-400">✓</span>}
<span className="truncate">Default Prompt</span>
{selectedPromptKeys.length === 0 && <span className="text-xs text-blue-400">(active)</span>}
</div>
<div className="text-xs text-gray-400 mt-1">
Use the standard system prompt without customization
</div>
</div>
</div>
</button>

{/* Clear all selection option - only show if prompts are selected */}
{selectedPromptKeys.length > 1 && (
<button
onClick={() => {
selectedPromptKeys.forEach(key => togglePrompt(key))
setIsOpen(false)
}}
className="w-full px-3 py-2 text-left hover:bg-gray-700 transition-colors border-b border-gray-700 text-sm"
>
<div className="font-medium text-gray-400 italic">
Clear All ({selectedPromptKeys.length})
</div>
</button>
)}

{/* Prompt list */}
{allPrompts.map((prompt) => {
const isActive = prompt.key === activePromptKey
return (
<button
key={prompt.key}
onClick={() => handlePromptSelect(prompt.key)}
className={`w-full px-3 py-2 text-left hover:bg-gray-700 transition-colors border-b border-gray-700 last:border-b-0 ${
isActive ? 'bg-blue-900/30' : ''
}`}
>
<div className="flex items-center justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="font-medium text-gray-200 flex items-center gap-2">
{isActive && <span className="text-blue-400">✓</span>}
<span className="truncate">{prompt.name}</span>
{isActive && <span className="text-xs text-blue-400">(active)</span>}
</div>
{prompt.description && (
<div className="text-xs text-gray-400 mt-1 line-clamp-2">
{prompt.description}
</div>
)}
<div className="text-xs text-gray-500 mt-1">
from {prompt.server}
</div>
</div>
</div>
</button>
)
})}
</div>
)}
</div>
)
}

export default PromptSelector
Loading
Loading