mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-04-09 20:52:19 +02:00
116 modules | 100 vuln types | 18 API routes | 18 frontend pages Major features: - VulnEngine: 100 vuln types, 526+ payloads, 12 testers, anti-hallucination prompts - Autonomous Agent: 3-stream auto pentest, multi-session (5 concurrent), pause/resume/stop - CLI Agent: Claude Code / Gemini CLI / Codex CLI inside Kali containers - Validation Pipeline: negative controls, proof of execution, confidence scoring, judge - AI Reasoning: ReACT engine, token budget, endpoint classifier, CVE hunter, deep recon - Multi-Agent: 5 specialists + orchestrator + researcher AI + vuln type agents - RAG System: BM25/TF-IDF/ChromaDB vectorstore, few-shot, reasoning templates - Smart Router: 20 providers (8 CLI OAuth + 12 API), tier failover, token refresh - Kali Sandbox: container-per-scan, 56 tools, VPN support, on-demand install - Full IA Testing: methodology-driven comprehensive pentest sessions - Notifications: Discord, Telegram, WhatsApp/Twilio multi-channel alerts - Frontend: React/TypeScript with 18 pages, real-time WebSocket updates
717 lines
26 KiB
TypeScript
Executable File
717 lines
26 KiB
TypeScript
Executable File
import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import {
|
|
Upload, Link as LinkIcon, FileText, Play, AlertTriangle,
|
|
Bot, Search, Target, Brain, BookOpen, ChevronDown, Key, Settings, X
|
|
} from 'lucide-react'
|
|
import Card from '../components/common/Card'
|
|
import Button from '../components/common/Button'
|
|
import Input from '../components/common/Input'
|
|
import Textarea from '../components/common/Textarea'
|
|
import { agentApi, targetsApi } from '../services/api'
|
|
import type { AgentTask, AgentMode, AgentRequest } from '../types'
|
|
|
|
type TargetInputMode = 'single' | 'multiple' | 'file'
|
|
|
|
type AuthTypeOption = 'none' | 'cookie' | 'bearer' | 'basic' | 'header'
|
|
|
|
interface OperationModeInfo {
|
|
id: AgentMode
|
|
name: string
|
|
icon: React.ReactNode
|
|
description: string
|
|
warning?: string
|
|
color: string
|
|
}
|
|
|
|
interface Toast {
|
|
id: number
|
|
message: string
|
|
severity: 'info' | 'success' | 'warning' | 'error'
|
|
}
|
|
|
|
let _toastId = 0
|
|
|
|
function ToastContainer({ toasts, onDismiss }: { toasts: Toast[]; onDismiss: (id: number) => void }) {
|
|
if (toasts.length === 0) return null
|
|
const border: Record<string, string> = {
|
|
info: 'border-blue-500',
|
|
success: 'border-green-500',
|
|
warning: 'border-yellow-500',
|
|
error: 'border-red-500'
|
|
}
|
|
return (
|
|
<div className="fixed top-4 right-4 z-50 flex flex-col gap-2 max-w-sm">
|
|
{toasts.map(t => (
|
|
<div
|
|
key={t.id}
|
|
className={`bg-dark-800 border-l-4 ${border[t.severity]} rounded-lg px-4 py-3 shadow-xl flex items-start gap-3`}
|
|
style={{ animation: 'fadeSlideIn 0.3s ease-out' }}
|
|
>
|
|
<span className="text-sm text-dark-200 flex-1">{t.message}</span>
|
|
<button onClick={() => onDismiss(t.id)} className="text-dark-500 hover:text-white">
|
|
<X className="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const OPERATION_MODES: OperationModeInfo[] = [
|
|
{
|
|
id: 'full_auto',
|
|
name: 'Full Auto',
|
|
icon: <Bot className="w-5 h-5" />,
|
|
description: 'Complete workflow: Recon -> Analyze -> Test -> Report',
|
|
color: 'primary'
|
|
},
|
|
{
|
|
id: 'recon_only',
|
|
name: 'Recon Only',
|
|
icon: <Search className="w-5 h-5" />,
|
|
description: 'Reconnaissance and enumeration only, no vulnerability testing',
|
|
color: 'blue'
|
|
},
|
|
{
|
|
id: 'prompt_only',
|
|
name: 'AI Prompt Mode',
|
|
icon: <Brain className="w-5 h-5" />,
|
|
description: 'AI decides everything based on your prompt - full autonomy',
|
|
warning: 'HIGH TOKEN USAGE - The AI will use more API calls to decide what to do',
|
|
color: 'purple'
|
|
},
|
|
{
|
|
id: 'analyze_only',
|
|
name: 'Analyze Only',
|
|
icon: <Target className="w-5 h-5" />,
|
|
description: 'Analyze provided data without active testing',
|
|
color: 'green'
|
|
}
|
|
]
|
|
|
|
const TASK_CATEGORIES = [
|
|
{ id: 'all', name: 'All Tasks' },
|
|
{ id: 'full_auto', name: 'Full Auto' },
|
|
{ id: 'recon', name: 'Reconnaissance' },
|
|
{ id: 'vulnerability', name: 'Vulnerability' },
|
|
{ id: 'custom', name: 'Custom' },
|
|
{ id: 'reporting', name: 'Reporting' }
|
|
]
|
|
|
|
const AUTH_TYPE_OPTIONS: { id: AuthTypeOption; label: string }[] = [
|
|
{ id: 'none', label: 'None' },
|
|
{ id: 'cookie', label: 'Cookie' },
|
|
{ id: 'bearer', label: 'Bearer Token' },
|
|
{ id: 'basic', label: 'Basic Auth' },
|
|
{ id: 'header', label: 'Custom Header' }
|
|
]
|
|
|
|
export default function NewScanPage() {
|
|
const navigate = useNavigate()
|
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
|
|
// Target state
|
|
const [targetMode, setTargetMode] = useState<TargetInputMode>('single')
|
|
const [singleUrl, setSingleUrl] = useState('')
|
|
const [multipleUrls, setMultipleUrls] = useState('')
|
|
const [uploadedUrls, setUploadedUrls] = useState<string[]>([])
|
|
const [urlError, setUrlError] = useState('')
|
|
|
|
// Operation mode
|
|
const [operationMode, setOperationMode] = useState<AgentMode>('full_auto')
|
|
|
|
// Task library
|
|
const [tasks, setTasks] = useState<AgentTask[]>([])
|
|
const [selectedTask, setSelectedTask] = useState<AgentTask | null>(null)
|
|
const [taskCategory, setTaskCategory] = useState('all')
|
|
const [showTaskLibrary, setShowTaskLibrary] = useState(false)
|
|
const [loadingTasks, setLoadingTasks] = useState(false)
|
|
|
|
// Custom prompt
|
|
const [useCustomPrompt, setUseCustomPrompt] = useState(false)
|
|
const [customPrompt, setCustomPrompt] = useState('')
|
|
|
|
// Auth options
|
|
const [showAuthOptions, setShowAuthOptions] = useState(false)
|
|
const [authType, setAuthType] = useState<AuthTypeOption>('none')
|
|
const [authValue, setAuthValue] = useState('')
|
|
|
|
// Advanced options
|
|
const [maxDepth, setMaxDepth] = useState(5)
|
|
|
|
// UI state
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
|
|
// Toast state
|
|
const [toasts, setToasts] = useState<Toast[]>([])
|
|
|
|
const addToast = useCallback((message: string, severity: Toast['severity'] = 'info') => {
|
|
const id = ++_toastId
|
|
setToasts(prev => [...prev, { id, message, severity }])
|
|
setTimeout(() => {
|
|
setToasts(prev => prev.filter(t => t.id !== id))
|
|
}, 4000)
|
|
}, [])
|
|
|
|
const dismissToast = useCallback((id: number) => {
|
|
setToasts(prev => prev.filter(t => t.id !== id))
|
|
}, [])
|
|
|
|
// Load tasks on mount
|
|
useEffect(() => {
|
|
loadTasks()
|
|
}, [])
|
|
|
|
const loadTasks = async (category?: string) => {
|
|
setLoadingTasks(true)
|
|
try {
|
|
const taskList = await agentApi.tasks.list(category === 'all' ? undefined : category)
|
|
setTasks(taskList)
|
|
} catch (error) {
|
|
console.error('Failed to load tasks:', error)
|
|
} finally {
|
|
setLoadingTasks(false)
|
|
}
|
|
}
|
|
|
|
const handleCategoryChange = useCallback((category: string) => {
|
|
setTaskCategory(category)
|
|
loadTasks(category)
|
|
}, [])
|
|
|
|
const handleFileUpload = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0]
|
|
if (!file) return
|
|
|
|
try {
|
|
const result = await targetsApi.upload(file)
|
|
const validUrls = result.filter((r: { valid: boolean; normalized_url: string }) => r.valid).map((r: { valid: boolean; normalized_url: string }) => r.normalized_url)
|
|
setUploadedUrls(validUrls)
|
|
setUrlError('')
|
|
} catch (error) {
|
|
setUrlError('Failed to parse file')
|
|
}
|
|
}, [])
|
|
|
|
const getTargetUrl = useCallback((): string => {
|
|
switch (targetMode) {
|
|
case 'single':
|
|
return singleUrl.trim()
|
|
case 'multiple':
|
|
return multipleUrls.split(/[,\n]/)[0]?.trim() || ''
|
|
case 'file':
|
|
return uploadedUrls[0] || ''
|
|
default:
|
|
return ''
|
|
}
|
|
}, [targetMode, singleUrl, multipleUrls, uploadedUrls])
|
|
|
|
const handleStartAgent = useCallback(async () => {
|
|
const target = getTargetUrl()
|
|
if (!target) {
|
|
setUrlError('Please enter a target URL')
|
|
addToast('Please enter a target URL before deploying', 'warning')
|
|
return
|
|
}
|
|
|
|
setIsLoading(true)
|
|
try {
|
|
// Validate URL
|
|
const validation = await targetsApi.validateBulk([target])
|
|
if (!validation[0]?.valid) {
|
|
setUrlError('Invalid URL format')
|
|
addToast('Invalid URL format - please check and try again', 'error')
|
|
setIsLoading(false)
|
|
return
|
|
}
|
|
|
|
// Build request
|
|
const request: AgentRequest = {
|
|
target: validation[0].normalized_url,
|
|
mode: operationMode,
|
|
max_depth: maxDepth
|
|
}
|
|
|
|
// Add task or custom prompt
|
|
if (selectedTask && !useCustomPrompt) {
|
|
request.task_id = selectedTask.id
|
|
} else if (useCustomPrompt && customPrompt.trim()) {
|
|
request.prompt = customPrompt
|
|
}
|
|
|
|
// Add auth if specified
|
|
if (authType !== 'none' && authValue.trim()) {
|
|
request.auth_type = authType as AgentRequest['auth_type']
|
|
request.auth_value = authValue
|
|
}
|
|
|
|
// Start agent
|
|
const response = await agentApi.run(request)
|
|
|
|
addToast('Agent deployed successfully - redirecting...', 'success')
|
|
|
|
// Navigate to agent status page
|
|
setTimeout(() => {
|
|
navigate(`/agent/${response.agent_id}`)
|
|
}, 300)
|
|
} catch (error) {
|
|
console.error('Failed to start agent:', error)
|
|
setUrlError('Failed to start agent. Please try again.')
|
|
addToast('Failed to start agent - please try again', 'error')
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}, [getTargetUrl, operationMode, maxDepth, selectedTask, useCustomPrompt, customPrompt, authType, authValue, addToast, navigate])
|
|
|
|
const handleSelectTask = useCallback((task: AgentTask) => {
|
|
setSelectedTask(task)
|
|
addToast(`Task selected: ${task.name}`, 'info')
|
|
}, [addToast])
|
|
|
|
const handleClearTask = useCallback(() => {
|
|
setSelectedTask(null)
|
|
}, [])
|
|
|
|
const handleSetAuthType = useCallback((id: AuthTypeOption) => {
|
|
setAuthType(id)
|
|
}, [])
|
|
|
|
const handleToggleTaskLibrary = useCallback(() => {
|
|
setShowTaskLibrary(prev => !prev)
|
|
}, [])
|
|
|
|
const handleToggleAuthOptions = useCallback(() => {
|
|
setShowAuthOptions(prev => !prev)
|
|
}, [])
|
|
|
|
const handleToggleCustomPrompt = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
setUseCustomPrompt(e.target.checked)
|
|
}, [])
|
|
|
|
const handleSingleUrlChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
setSingleUrl(e.target.value)
|
|
setUrlError('')
|
|
}, [])
|
|
|
|
const handleMultipleUrlsChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
setMultipleUrls(e.target.value)
|
|
setUrlError('')
|
|
}, [])
|
|
|
|
const handleCustomPromptChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
setCustomPrompt(e.target.value)
|
|
}, [])
|
|
|
|
const handleMaxDepthChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
setMaxDepth(parseInt(e.target.value))
|
|
}, [])
|
|
|
|
const handleAuthValueChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
setAuthValue(e.target.value)
|
|
}, [])
|
|
|
|
const handleNavigateHome = useCallback(() => {
|
|
navigate('/')
|
|
}, [navigate])
|
|
|
|
// Memoized filtered task list
|
|
const filteredTasks = useMemo(() => {
|
|
if (showTaskLibrary) return tasks
|
|
return tasks.slice(0, 4)
|
|
}, [tasks, showTaskLibrary])
|
|
|
|
const currentModeInfo = OPERATION_MODES.find(m => m.id === operationMode)!
|
|
|
|
return (
|
|
<div
|
|
className="max-w-5xl mx-auto space-y-6"
|
|
style={{ animation: 'fadeSlideIn 0.4s ease-out' }}
|
|
>
|
|
<style>{`
|
|
@keyframes fadeSlideIn {
|
|
from { opacity: 0; transform: translateY(-8px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
`}</style>
|
|
|
|
<ToastContainer toasts={toasts} onDismiss={dismissToast} />
|
|
|
|
{/* Header */}
|
|
<div
|
|
className="flex items-center justify-between"
|
|
style={{ animation: 'fadeSlideIn 0.3s ease-out 0s both' }}
|
|
>
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-white flex items-center gap-3">
|
|
<Bot className="w-8 h-8 text-primary-500" />
|
|
AI Security Agent
|
|
</h1>
|
|
<p className="text-dark-400 mt-1">Autonomous penetration testing powered by AI</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Operation Mode Selector */}
|
|
<div style={{ animation: 'fadeSlideIn 0.3s ease-out 0.05s both' }}>
|
|
<Card title="Operation Mode" subtitle="Select how the AI agent should operate">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{OPERATION_MODES.map((mode, idx) => (
|
|
<div
|
|
key={mode.id}
|
|
onClick={() => setOperationMode(mode.id)}
|
|
className={`p-4 rounded-xl border-2 cursor-pointer transition-all ${
|
|
operationMode === mode.id
|
|
? `border-${mode.color}-500 bg-${mode.color}-500/10`
|
|
: 'border-dark-700 hover:border-dark-500 bg-dark-900/50'
|
|
}`}
|
|
style={{ animation: `fadeSlideIn 0.3s ease-out ${idx * 0.05}s both` }}
|
|
>
|
|
<div className={`flex items-center gap-2 mb-2 ${
|
|
operationMode === mode.id ? `text-${mode.color}-400` : 'text-dark-300'
|
|
}`}>
|
|
{mode.icon}
|
|
<span className="font-semibold">{mode.name}</span>
|
|
</div>
|
|
<p className="text-sm text-dark-400">{mode.description}</p>
|
|
{mode.warning && operationMode === mode.id && (
|
|
<div className="mt-2 flex items-start gap-2 text-yellow-400 text-xs">
|
|
<AlertTriangle className="w-4 h-4 flex-shrink-0" />
|
|
<span>{mode.warning}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Target Input */}
|
|
<div style={{ animation: 'fadeSlideIn 0.3s ease-out 0.1s both' }}>
|
|
<Card title="Target" subtitle="Enter the URL to test">
|
|
<div className="space-y-4">
|
|
{/* Mode Selector */}
|
|
<div className="flex gap-2 flex-wrap">
|
|
<Button
|
|
variant={targetMode === 'single' ? 'primary' : 'secondary'}
|
|
onClick={() => setTargetMode('single')}
|
|
>
|
|
<LinkIcon className="w-4 h-4 mr-2" />
|
|
Single URL
|
|
</Button>
|
|
<Button
|
|
variant={targetMode === 'multiple' ? 'primary' : 'secondary'}
|
|
onClick={() => setTargetMode('multiple')}
|
|
>
|
|
<FileText className="w-4 h-4 mr-2" />
|
|
Multiple URLs
|
|
</Button>
|
|
<Button
|
|
variant={targetMode === 'file' ? 'primary' : 'secondary'}
|
|
onClick={() => setTargetMode('file')}
|
|
>
|
|
<Upload className="w-4 h-4 mr-2" />
|
|
Upload File
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Input Fields */}
|
|
{targetMode === 'single' && (
|
|
<Input
|
|
placeholder="https://example.com"
|
|
value={singleUrl}
|
|
onChange={handleSingleUrlChange}
|
|
error={urlError}
|
|
/>
|
|
)}
|
|
|
|
{targetMode === 'multiple' && (
|
|
<div>
|
|
<Textarea
|
|
placeholder="Enter URLs separated by commas or new lines: https://example1.com https://example2.com"
|
|
rows={5}
|
|
value={multipleUrls}
|
|
onChange={handleMultipleUrlsChange}
|
|
/>
|
|
<p className="text-xs text-dark-500 mt-1">Note: Agent will test the first URL. Multiple URL support coming soon.</p>
|
|
{urlError && <p className="mt-1 text-sm text-red-400">{urlError}</p>}
|
|
</div>
|
|
)}
|
|
|
|
{targetMode === 'file' && (
|
|
<div>
|
|
<input
|
|
type="file"
|
|
ref={fileInputRef}
|
|
onChange={handleFileUpload}
|
|
accept=".txt,.csv,.lst"
|
|
className="hidden"
|
|
/>
|
|
<div
|
|
onClick={() => fileInputRef.current?.click()}
|
|
className="border-2 border-dashed border-dark-700 rounded-lg p-8 text-center cursor-pointer hover:border-primary-500 transition-colors"
|
|
>
|
|
<Upload className="w-10 h-10 mx-auto text-dark-400 mb-3" />
|
|
<p className="text-dark-300">Click to upload a file with URLs</p>
|
|
<p className="text-sm text-dark-500 mt-1">Supports .txt, .csv, .lst files</p>
|
|
</div>
|
|
{uploadedUrls.length > 0 && (
|
|
<p className="mt-2 text-sm text-green-400">
|
|
{uploadedUrls.length} valid URLs loaded - using first URL
|
|
</p>
|
|
)}
|
|
{urlError && <p className="mt-2 text-sm text-red-400">{urlError}</p>}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Task Library */}
|
|
<div style={{ animation: 'fadeSlideIn 0.3s ease-out 0.15s both' }}>
|
|
<Card
|
|
title={
|
|
<div className="flex items-center justify-between w-full">
|
|
<div className="flex items-center gap-2">
|
|
<BookOpen className="w-5 h-5 text-primary-500" />
|
|
<span>Task Library</span>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleToggleTaskLibrary}
|
|
>
|
|
<ChevronDown className={`w-4 h-4 transition-transform ${showTaskLibrary ? 'rotate-180' : ''}`} />
|
|
</Button>
|
|
</div>
|
|
}
|
|
subtitle="Select a preset task or create a custom prompt"
|
|
>
|
|
{/* Custom Prompt Toggle */}
|
|
<div className="flex items-center justify-between mb-4 pb-4 border-b border-dark-700">
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
id="customPrompt"
|
|
checked={useCustomPrompt}
|
|
onChange={handleToggleCustomPrompt}
|
|
className="w-4 h-4 rounded border-dark-600 bg-dark-800 text-primary-500 focus:ring-primary-500"
|
|
/>
|
|
<label htmlFor="customPrompt" className="text-white">Use custom prompt instead of task</label>
|
|
</div>
|
|
</div>
|
|
|
|
{useCustomPrompt ? (
|
|
<Textarea
|
|
placeholder="Enter your custom prompt for the AI agent... Example: Test for SQL injection on all form inputs, check for authentication bypass on the login endpoint, and look for IDOR vulnerabilities in user profile APIs."
|
|
rows={6}
|
|
value={customPrompt}
|
|
onChange={handleCustomPromptChange}
|
|
/>
|
|
) : (
|
|
<>
|
|
{showTaskLibrary && (
|
|
<>
|
|
{/* Category Filter */}
|
|
<div className="flex gap-2 mb-4 flex-wrap">
|
|
{TASK_CATEGORIES.map((cat) => (
|
|
<Button
|
|
key={cat.id}
|
|
variant={taskCategory === cat.id ? 'primary' : 'secondary'}
|
|
size="sm"
|
|
onClick={() => handleCategoryChange(cat.id)}
|
|
>
|
|
{cat.name}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Tasks Grid */}
|
|
<div className={`grid grid-cols-1 sm:grid-cols-2 gap-3 ${showTaskLibrary ? 'max-h-80 overflow-auto' : ''}`}>
|
|
{loadingTasks ? (
|
|
<p className="text-dark-400 col-span-2 text-center py-4">Loading tasks...</p>
|
|
) : (
|
|
filteredTasks.map((task, idx) => (
|
|
<div
|
|
key={task.id}
|
|
onClick={() => handleSelectTask(task)}
|
|
className={`p-4 rounded-lg border cursor-pointer transition-all ${
|
|
selectedTask?.id === task.id
|
|
? 'border-primary-500 bg-primary-500/10'
|
|
: 'border-dark-700 hover:border-dark-500 bg-dark-900/50'
|
|
}`}
|
|
style={{ animation: `fadeSlideIn 0.3s ease-out ${idx * 0.05}s both` }}
|
|
>
|
|
<div className="flex items-center justify-between mb-1">
|
|
<span className="font-medium text-white">{task.name}</span>
|
|
{task.is_preset && (
|
|
<span className="text-xs bg-primary-500/20 text-primary-400 px-2 py-0.5 rounded">Preset</span>
|
|
)}
|
|
</div>
|
|
<p className="text-sm text-dark-400 line-clamp-2">{task.description}</p>
|
|
<div className="flex items-center gap-2 mt-2">
|
|
<span className="text-xs text-dark-500">{task.category}</span>
|
|
{task.estimated_tokens > 0 && (
|
|
<span className="text-xs text-dark-500">~{task.estimated_tokens} tokens</span>
|
|
)}
|
|
</div>
|
|
{task.tags?.length > 0 && (
|
|
<div className="flex gap-1 mt-2 flex-wrap">
|
|
{task.tags.slice(0, 3).map((tag) => (
|
|
<span key={tag} className="text-xs bg-dark-700 text-dark-300 px-2 py-0.5 rounded">
|
|
{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
{!showTaskLibrary && tasks.length > 4 && (
|
|
<Button
|
|
variant="ghost"
|
|
className="w-full mt-3"
|
|
onClick={() => setShowTaskLibrary(true)}
|
|
>
|
|
Show all {tasks.length} tasks
|
|
</Button>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Selected Task Preview */}
|
|
{selectedTask && !useCustomPrompt && (
|
|
<div className="mt-4 p-4 bg-dark-800 rounded-lg border border-dark-700">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="font-medium text-white">Selected: {selectedTask.name}</span>
|
|
<Button variant="ghost" size="sm" onClick={handleClearTask}>
|
|
Clear
|
|
</Button>
|
|
</div>
|
|
<p className="text-sm text-dark-400 whitespace-pre-wrap line-clamp-4">
|
|
{selectedTask.prompt}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Authentication Options */}
|
|
<div style={{ animation: 'fadeSlideIn 0.3s ease-out 0.2s both' }}>
|
|
<Card
|
|
title={
|
|
<div className="flex items-center gap-2">
|
|
<Key className="w-5 h-5 text-primary-500" />
|
|
<span>Authentication</span>
|
|
<span className="text-xs text-dark-500">(Optional)</span>
|
|
</div>
|
|
}
|
|
>
|
|
<div className="space-y-4">
|
|
<div className="flex gap-2 flex-wrap">
|
|
{AUTH_TYPE_OPTIONS.map((type) => (
|
|
<Button
|
|
key={type.id}
|
|
variant={authType === type.id ? 'primary' : 'secondary'}
|
|
size="sm"
|
|
onClick={() => handleSetAuthType(type.id)}
|
|
>
|
|
{type.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
|
|
{authType !== 'none' && (
|
|
<Input
|
|
placeholder={
|
|
authType === 'cookie' ? 'session=abc123; token=xyz789' :
|
|
authType === 'bearer' ? 'eyJhbGciOiJIUzI1NiIs...' :
|
|
authType === 'basic' ? 'username:password' :
|
|
'X-API-Key: your-api-key'
|
|
}
|
|
value={authValue}
|
|
onChange={handleAuthValueChange}
|
|
label={
|
|
authType === 'cookie' ? 'Cookie String' :
|
|
authType === 'bearer' ? 'Bearer Token' :
|
|
authType === 'basic' ? 'Username:Password' :
|
|
'Header:Value'
|
|
}
|
|
/>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Advanced Options */}
|
|
<div style={{ animation: 'fadeSlideIn 0.3s ease-out 0.25s both' }}>
|
|
<Card
|
|
title={
|
|
<div className="flex items-center gap-2 cursor-pointer" onClick={handleToggleAuthOptions}>
|
|
<Settings className="w-5 h-5 text-primary-500" />
|
|
<span>Advanced Options</span>
|
|
<ChevronDown className={`w-4 h-4 transition-transform ${showAuthOptions ? 'rotate-180' : ''}`} />
|
|
</div>
|
|
}
|
|
>
|
|
{showAuthOptions && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="text-sm text-dark-300 mb-1 block">Max Crawl Depth</label>
|
|
<div className="flex items-center gap-4">
|
|
<input
|
|
type="range"
|
|
min="1"
|
|
max="10"
|
|
value={maxDepth}
|
|
onChange={handleMaxDepthChange}
|
|
className="flex-1"
|
|
/>
|
|
<span className="text-white font-medium w-8">{maxDepth}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{!showAuthOptions && (
|
|
<p className="text-dark-500 text-sm">Click to expand advanced options</p>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Warning for Prompt Only Mode */}
|
|
{operationMode === 'prompt_only' && (
|
|
<div
|
|
className="bg-yellow-500/10 border border-yellow-500/30 rounded-lg p-4 flex items-start gap-3"
|
|
style={{ animation: 'fadeSlideIn 0.3s ease-out' }}
|
|
>
|
|
<AlertTriangle className="w-6 h-6 text-yellow-500 flex-shrink-0" />
|
|
<div>
|
|
<p className="font-medium text-yellow-400">High Token Usage Warning</p>
|
|
<p className="text-sm text-yellow-300/80 mt-1">
|
|
In AI Prompt Mode, the agent has full autonomy to decide what tools to use and what tests to run.
|
|
This results in significantly higher API token consumption. Consider using Full Auto mode for most use cases.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Start Button */}
|
|
<div
|
|
className="flex justify-end gap-3 sticky bottom-4 bg-dark-950/90 backdrop-blur p-4 -mx-4 rounded-lg"
|
|
style={{ animation: 'fadeSlideIn 0.3s ease-out 0.3s both' }}
|
|
>
|
|
<Button variant="secondary" onClick={handleNavigateHome}>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={handleStartAgent} isLoading={isLoading} size="lg">
|
|
<Play className="w-5 h-5 mr-2" />
|
|
Deploy Agent ({currentModeInfo.name})
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|