Add backend modules and frontend components for StrikePackageGPT expansion

Co-authored-by: mblanke <9078342+mblanke@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-12-03 12:50:53 +00:00
parent 7b75477450
commit f49b63e7af
18 changed files with 4506 additions and 0 deletions

View File

@@ -0,0 +1,345 @@
/**
* ExplainButton Component
* Reusable inline "Explain" button for configs, logs, and errors
* Shows modal/popover with LLM-powered explanation
*/
import React, { useState } from 'react';
const ExplainButton = ({
type = 'config', // config, log, error, scan_result
content,
context = {},
size = 'small',
style = {}
}) => {
const [isLoading, setIsLoading] = useState(false);
const [showModal, setShowModal] = useState(false);
const [explanation, setExplanation] = useState(null);
const [error, setError] = useState(null);
const handleExplain = async () => {
setIsLoading(true);
setError(null);
setShowModal(true);
try {
const response = await fetch('/api/explain', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type,
content,
context
})
});
if (!response.ok) {
throw new Error('Failed to get explanation');
}
const data = await response.json();
setExplanation(data);
} catch (err) {
console.error('Error getting explanation:', err);
setError('Failed to load explanation. Please try again.');
} finally {
setIsLoading(false);
}
};
const closeModal = () => {
setShowModal(false);
setExplanation(null);
setError(null);
};
const buttonSizes = {
small: { padding: '4px 8px', fontSize: '12px' },
medium: { padding: '6px 12px', fontSize: '14px' },
large: { padding: '8px 16px', fontSize: '16px' }
};
const buttonStyle = {
...buttonSizes[size],
backgroundColor: '#3498DB',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
transition: 'background-color 0.2s',
...style
};
const renderExplanation = () => {
if (error) {
return (
<div style={{ color: '#E74C3C', padding: '10px' }}>
{error}
</div>
);
}
if (isLoading) {
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<div style={{ fontSize: '24px', marginBottom: '10px' }}></div>
<div>Generating explanation...</div>
</div>
);
}
if (!explanation) {
return null;
}
// Render based on explanation type
switch (type) {
case 'config':
return (
<div style={{ padding: '15px' }}>
<h3 style={{ margin: '0 0 10px 0', fontSize: '18px' }}>
{explanation.config_key || 'Configuration'}
</h3>
<div style={{ marginBottom: '15px' }}>
<strong>Current Value:</strong>
<code style={{
backgroundColor: '#f5f5f5',
padding: '2px 6px',
borderRadius: '3px',
marginLeft: '5px'
}}>
{explanation.current_value}
</code>
</div>
<div style={{ marginBottom: '15px', lineHeight: '1.6' }}>
<strong>What it does:</strong>
<p style={{ margin: '5px 0' }}>{explanation.description}</p>
</div>
{explanation.example && (
<div style={{ marginBottom: '15px', lineHeight: '1.6' }}>
<strong>Example:</strong>
<p style={{ margin: '5px 0', fontStyle: 'italic' }}>{explanation.example}</p>
</div>
)}
{explanation.value_analysis && (
<div style={{ marginBottom: '15px', padding: '10px', backgroundColor: '#E8F4F8', borderRadius: '4px' }}>
<strong>Analysis:</strong> {explanation.value_analysis}
</div>
)}
{explanation.recommendations && explanation.recommendations.length > 0 && (
<div style={{ marginBottom: '15px' }}>
<strong>Recommendations:</strong>
<ul style={{ margin: '5px 0', paddingLeft: '20px' }}>
{explanation.recommendations.map((rec, i) => (
<li key={i} style={{ margin: '5px 0' }}>{rec}</li>
))}
</ul>
</div>
)}
<div style={{ fontSize: '12px', color: '#666', marginTop: '15px', paddingTop: '15px', borderTop: '1px solid #ddd' }}>
{explanation.requires_restart && (
<div> Changing this setting requires a restart</div>
)}
{!explanation.safe_to_change && (
<div> Use caution when changing this setting</div>
)}
</div>
</div>
);
case 'error':
return (
<div style={{ padding: '15px' }}>
<h3 style={{ margin: '0 0 10px 0', fontSize: '18px', color: '#E74C3C' }}>
Error Explanation
</h3>
<div style={{ marginBottom: '15px', padding: '10px', backgroundColor: '#fef5e7', borderRadius: '4px', fontSize: '14px' }}>
<strong>Original Error:</strong>
<div style={{ marginTop: '5px', fontFamily: 'monospace', fontSize: '12px' }}>
{explanation.original_error}
</div>
</div>
<div style={{ marginBottom: '15px' }}>
<strong>What went wrong:</strong>
<p style={{ margin: '5px 0', lineHeight: '1.6' }}>{explanation.plain_english}</p>
</div>
<div style={{ marginBottom: '15px' }}>
<strong>Likely causes:</strong>
<ul style={{ margin: '5px 0', paddingLeft: '20px' }}>
{explanation.likely_causes?.map((cause, i) => (
<li key={i} style={{ margin: '5px 0' }}>{cause}</li>
))}
</ul>
</div>
<div style={{ marginBottom: '15px', padding: '10px', backgroundColor: '#E8F8F5', borderRadius: '4px' }}>
<strong>💡 How to fix it:</strong>
<ol style={{ margin: '5px 0', paddingLeft: '20px' }}>
{explanation.suggested_fixes?.map((fix, i) => (
<li key={i} style={{ margin: '5px 0' }}>{fix}</li>
))}
</ol>
</div>
<div style={{ fontSize: '12px', color: '#666', marginTop: '15px' }}>
Severity: <span style={{
color: explanation.severity === 'critical' ? '#E74C3C' :
explanation.severity === 'high' ? '#E67E22' :
explanation.severity === 'medium' ? '#F39C12' : '#95A5A6',
fontWeight: 'bold'
}}>
{(explanation.severity || 'unknown').toUpperCase()}
</span>
</div>
</div>
);
case 'log':
return (
<div style={{ padding: '15px' }}>
<h3 style={{ margin: '0 0 10px 0', fontSize: '18px' }}>
Log Entry Explanation
</h3>
<div style={{ marginBottom: '15px', padding: '10px', backgroundColor: '#f5f5f5', borderRadius: '4px', fontSize: '13px', fontFamily: 'monospace' }}>
{explanation.log_entry}
</div>
<div style={{ marginBottom: '15px' }}>
<strong>Level:</strong>
<span style={{
marginLeft: '5px',
padding: '2px 8px',
borderRadius: '3px',
backgroundColor: explanation.log_level === 'ERROR' ? '#E74C3C' :
explanation.log_level === 'WARNING' ? '#F39C12' :
explanation.log_level === 'INFO' ? '#3498DB' : '#95A5A6',
color: 'white',
fontSize: '12px',
fontWeight: 'bold'
}}>
{explanation.log_level}
</span>
</div>
{explanation.timestamp && (
<div style={{ marginBottom: '15px', fontSize: '14px', color: '#666' }}>
<strong>Time:</strong> {explanation.timestamp}
</div>
)}
<div style={{ marginBottom: '15px', lineHeight: '1.6' }}>
<strong>What this means:</strong>
<p style={{ margin: '5px 0' }}>{explanation.explanation}</p>
</div>
{explanation.action_needed && explanation.next_steps && explanation.next_steps.length > 0 && (
<div style={{ padding: '10px', backgroundColor: '#FEF5E7', borderRadius: '4px' }}>
<strong> Action needed:</strong>
<ul style={{ margin: '5px 0', paddingLeft: '20px' }}>
{explanation.next_steps.map((step, i) => (
<li key={i} style={{ margin: '5px 0' }}>{step}</li>
))}
</ul>
</div>
)}
</div>
);
default:
return (
<div style={{ padding: '15px' }}>
<div>{explanation.explanation || 'No explanation available.'}</div>
</div>
);
}
};
return (
<>
<button
onClick={handleExplain}
onMouseEnter={(e) => e.target.style.backgroundColor = '#2980B9'}
onMouseLeave={(e) => e.target.style.backgroundColor = '#3498DB'}
style={buttonStyle}
title="Get AI-powered explanation"
>
<span></span>
<span>Explain</span>
</button>
{showModal && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 9999
}}
onClick={closeModal}
>
<div
style={{
backgroundColor: 'white',
borderRadius: '8px',
maxWidth: '600px',
maxHeight: '80vh',
overflow: 'auto',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)',
position: 'relative'
}}
onClick={(e) => e.stopPropagation()}
>
<div style={{
position: 'sticky',
top: 0,
backgroundColor: 'white',
padding: '15px',
borderBottom: '1px solid #ddd',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<h2 style={{ margin: 0, fontSize: '20px' }}>Explanation</h2>
<button
onClick={closeModal}
style={{
background: 'none',
border: 'none',
fontSize: '24px',
cursor: 'pointer',
color: '#666'
}}
>
×
</button>
</div>
{renderExplanation()}
</div>
</div>
)}
</>
);
};
export default ExplainButton;

View File

@@ -0,0 +1,487 @@
/**
* GuidedWizard Component
* Multi-step wizard for onboarding flows
* Types: create_operation, onboard_agent, run_scan, first_time_setup
*/
import React, { useState, useEffect } from 'react';
const GuidedWizard = ({
wizardType = 'first_time_setup',
onComplete,
onCancel,
initialData = {}
}) => {
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState(initialData);
const [stepHelp, setStepHelp] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const wizardConfigs = {
create_operation: {
title: 'Create New Operation',
steps: [
{
number: 1,
title: 'Operation Name and Type',
fields: [
{ name: 'operation_name', label: 'Operation Name', type: 'text', required: true, placeholder: 'Q4 Security Assessment' },
{ name: 'operation_type', label: 'Operation Type', type: 'select', required: true, options: [
{ value: 'external', label: 'External Penetration Test' },
{ value: 'internal', label: 'Internal Network Assessment' },
{ value: 'webapp', label: 'Web Application Test' },
{ value: 'wireless', label: 'Wireless Security Assessment' }
]}
]
},
{
number: 2,
title: 'Define Target Scope',
fields: [
{ name: 'target_range', label: 'Target Network Range', type: 'text', required: true, placeholder: '192.168.1.0/24' },
{ name: 'excluded_hosts', label: 'Excluded Hosts (comma-separated)', type: 'text', placeholder: '192.168.1.1, 192.168.1.254' },
{ name: 'domains', label: 'Target Domains', type: 'textarea', placeholder: 'example.com\napp.example.com' }
]
},
{
number: 3,
title: 'Configure Assessment Tools',
fields: [
{ name: 'scan_intensity', label: 'Scan Intensity', type: 'select', required: true, options: [
{ value: '1', label: 'Stealth (Slowest, least detectable)' },
{ value: '3', label: 'Balanced (Recommended)' },
{ value: '5', label: 'Aggressive (Fastest, easily detected)' }
]},
{ name: 'tools', label: 'Tools to Use', type: 'multiselect', options: [
{ value: 'nmap', label: 'Nmap (Network Scanning)' },
{ value: 'nikto', label: 'Nikto (Web Server Scanning)' },
{ value: 'gobuster', label: 'Gobuster (Directory Enumeration)' },
{ value: 'sqlmap', label: 'SQLMap (SQL Injection Testing)' }
]}
]
}
]
},
run_scan: {
title: 'Run Security Scan',
steps: [
{
number: 1,
title: 'Select Scan Tool',
fields: [
{ name: 'tool', label: 'Security Tool', type: 'select', required: true, options: [
{ value: 'nmap', label: 'Nmap - Network Scanner' },
{ value: 'nikto', label: 'Nikto - Web Server Scanner' },
{ value: 'gobuster', label: 'Gobuster - Directory/File Discovery' },
{ value: 'sqlmap', label: 'SQLMap - SQL Injection' },
{ value: 'whatweb', label: 'WhatWeb - Technology Detection' }
]}
]
},
{
number: 2,
title: 'Specify Target',
fields: [
{ name: 'target', label: 'Target', type: 'text', required: true, placeholder: '192.168.1.0/24 or example.com' },
{ name: 'ports', label: 'Ports (optional)', type: 'text', placeholder: '80,443,8080 or 1-1000' }
]
},
{
number: 3,
title: 'Scan Options',
fields: [
{ name: 'scan_type', label: 'Scan Type', type: 'select', required: true, options: [
{ value: 'quick', label: 'Quick Scan (Fast, common ports)' },
{ value: 'full', label: 'Full Scan (Comprehensive, slower)' },
{ value: 'stealth', label: 'Stealth Scan (Slow, harder to detect)' },
{ value: 'vuln', label: 'Vulnerability Scan (Checks for known vulns)' }
]},
{ name: 'timeout', label: 'Timeout (seconds)', type: 'number', placeholder: '300' }
]
}
]
},
first_time_setup: {
title: 'Welcome to StrikePackageGPT',
steps: [
{
number: 1,
title: 'Welcome',
fields: [
{ name: 'user_name', label: 'Your Name', type: 'text', placeholder: 'John Doe' },
{ name: 'skill_level', label: 'Security Testing Experience', type: 'select', required: true, options: [
{ value: 'beginner', label: 'Beginner - Learning the basics' },
{ value: 'intermediate', label: 'Intermediate - Some experience' },
{ value: 'advanced', label: 'Advanced - Professional pentester' }
]}
]
},
{
number: 2,
title: 'Configure LLM Provider',
fields: [
{ name: 'llm_provider', label: 'LLM Provider', type: 'select', required: true, options: [
{ value: 'ollama', label: 'Ollama (Local, Free)' },
{ value: 'openai', label: 'OpenAI (Cloud, Requires API Key)' },
{ value: 'anthropic', label: 'Anthropic Claude (Cloud, Requires API Key)' }
]},
{ name: 'api_key', label: 'API Key (if using cloud provider)', type: 'password', placeholder: 'sk-...' }
]
},
{
number: 3,
title: 'Review and Finish',
fields: []
}
]
}
};
const config = wizardConfigs[wizardType] || wizardConfigs.first_time_setup;
const totalSteps = config.steps.length;
const currentStepConfig = config.steps[currentStep - 1];
useEffect(() => {
fetchStepHelp();
}, [currentStep]);
const fetchStepHelp = async () => {
try {
const response = await fetch(`/api/wizard/help?type=${wizardType}&step=${currentStep}`);
if (response.ok) {
const data = await response.json();
setStepHelp(data);
}
} catch (err) {
console.error('Failed to fetch step help:', err);
}
};
const handleFieldChange = (fieldName, value) => {
setFormData(prev => ({ ...prev, [fieldName]: value }));
};
const validateCurrentStep = () => {
const requiredFields = currentStepConfig.fields.filter(f => f.required);
for (const field of requiredFields) {
if (!formData[field.name]) {
setError(`${field.label} is required`);
return false;
}
}
setError(null);
return true;
};
const handleNext = () => {
if (!validateCurrentStep()) return;
if (currentStep < totalSteps) {
setCurrentStep(prev => prev + 1);
} else {
handleComplete();
}
};
const handleBack = () => {
if (currentStep > 1) {
setCurrentStep(prev => prev - 1);
setError(null);
}
};
const handleComplete = async () => {
if (!validateCurrentStep()) return;
setLoading(true);
try {
if (onComplete) {
await onComplete(formData);
}
} catch (err) {
setError('Failed to complete wizard: ' + err.message);
} finally {
setLoading(false);
}
};
const renderField = (field) => {
const commonStyle = {
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '14px'
};
switch (field.type) {
case 'text':
case 'password':
case 'number':
return (
<input
type={field.type}
value={formData[field.name] || ''}
onChange={(e) => handleFieldChange(field.name, e.target.value)}
placeholder={field.placeholder}
style={commonStyle}
/>
);
case 'textarea':
return (
<textarea
value={formData[field.name] || ''}
onChange={(e) => handleFieldChange(field.name, e.target.value)}
placeholder={field.placeholder}
rows={4}
style={{ ...commonStyle, resize: 'vertical' }}
/>
);
case 'select':
return (
<select
value={formData[field.name] || ''}
onChange={(e) => handleFieldChange(field.name, e.target.value)}
style={commonStyle}
>
<option value="">Select...</option>
{field.options?.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
);
case 'multiselect':
const selectedValues = formData[field.name] || [];
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{field.options?.map(opt => (
<label key={opt.value} style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={selectedValues.includes(opt.value)}
onChange={(e) => {
const newValues = e.target.checked
? [...selectedValues, opt.value]
: selectedValues.filter(v => v !== opt.value);
handleFieldChange(field.name, newValues);
}}
/>
<span>{opt.label}</span>
</label>
))}
</div>
);
default:
return null;
}
};
return (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 9999
}}
>
<div
style={{
backgroundColor: 'white',
borderRadius: '8px',
width: '90%',
maxWidth: '700px',
maxHeight: '90vh',
overflow: 'auto',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)'
}}
>
{/* Header */}
<div style={{
padding: '20px',
borderBottom: '2px solid #3498DB',
backgroundColor: '#f8f9fa'
}}>
<h2 style={{ margin: '0 0 10px 0', color: '#2C3E50' }}>{config.title}</h2>
{/* Progress indicator */}
<div style={{ display: 'flex', gap: '5px', marginTop: '15px' }}>
{config.steps.map((step, index) => (
<div
key={index}
style={{
flex: 1,
height: '4px',
backgroundColor: index + 1 <= currentStep ? '#3498DB' : '#ddd',
borderRadius: '2px',
transition: 'background-color 0.3s'
}}
/>
))}
</div>
<div style={{ marginTop: '8px', fontSize: '14px', color: '#666' }}>
Step {currentStep} of {totalSteps}
</div>
</div>
{/* Step content */}
<div style={{ padding: '30px' }}>
<h3 style={{ margin: '0 0 20px 0', color: '#34495E' }}>
{currentStepConfig.title}
</h3>
{/* Help section */}
{stepHelp && (
<div style={{
padding: '15px',
backgroundColor: '#E8F4F8',
borderRadius: '6px',
marginBottom: '20px',
borderLeft: '4px solid #3498DB'
}}>
{stepHelp.description && (
<p style={{ margin: '0 0 10px 0' }}>{stepHelp.description}</p>
)}
{stepHelp.tips && stepHelp.tips.length > 0 && (
<div>
<strong style={{ fontSize: '14px' }}>💡 Tips:</strong>
<ul style={{ margin: '5px 0 0 0', paddingLeft: '20px', fontSize: '14px' }}>
{stepHelp.tips.map((tip, i) => (
<li key={i} style={{ margin: '5px 0' }}>{tip}</li>
))}
</ul>
</div>
)}
</div>
)}
{/* Form fields */}
{currentStepConfig.fields.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
{currentStepConfig.fields.map(field => (
<div key={field.name}>
<label style={{
display: 'block',
marginBottom: '8px',
fontWeight: '500',
color: '#2C3E50'
}}>
{field.label}
{field.required && <span style={{ color: '#E74C3C' }}> *</span>}
</label>
{renderField(field)}
</div>
))}
</div>
) : (
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
<h4>Review Your Settings</h4>
<div style={{
marginTop: '20px',
textAlign: 'left',
backgroundColor: '#f8f9fa',
padding: '15px',
borderRadius: '4px',
maxHeight: '300px',
overflow: 'auto'
}}>
{Object.entries(formData).map(([key, value]) => (
<div key={key} style={{ marginBottom: '10px' }}>
<strong>{key}:</strong> {Array.isArray(value) ? value.join(', ') : value}
</div>
))}
</div>
</div>
)}
{/* Error message */}
{error && (
<div style={{
marginTop: '20px',
padding: '12px',
backgroundColor: '#FCE4E4',
color: '#E74C3C',
borderRadius: '4px',
fontSize: '14px'
}}>
{error}
</div>
)}
</div>
{/* Footer */}
<div style={{
padding: '20px',
borderTop: '1px solid #ddd',
display: 'flex',
justifyContent: 'space-between',
backgroundColor: '#f8f9fa'
}}>
<button
onClick={onCancel}
style={{
padding: '10px 20px',
border: '1px solid #95A5A6',
backgroundColor: 'white',
color: '#666',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
Cancel
</button>
<div style={{ display: 'flex', gap: '10px' }}>
{currentStep > 1 && (
<button
onClick={handleBack}
style={{
padding: '10px 20px',
border: '1px solid #3498DB',
backgroundColor: 'white',
color: '#3498DB',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
Back
</button>
)}
<button
onClick={handleNext}
disabled={loading}
style={{
padding: '10px 20px',
border: 'none',
backgroundColor: loading ? '#95A5A6' : '#3498DB',
color: 'white',
borderRadius: '4px',
cursor: loading ? 'not-allowed' : 'pointer',
fontSize: '14px',
fontWeight: '500'
}}
>
{loading ? 'Processing...' : currentStep === totalSteps ? 'Finish' : 'Next'}
</button>
</div>
</div>
</div>
</div>
);
};
export default GuidedWizard;

View File

@@ -0,0 +1,424 @@
/**
* HelpChat Component
* Persistent side-panel chat with LLM-powered help
* Context-aware and maintains conversation history
*/
import React, { useState, useEffect, useRef } from 'react';
const HelpChat = ({
isOpen = false,
onClose,
currentPage = 'dashboard',
context = {}
}) => {
const [messages, setMessages] = useState([]);
const [inputText, setInputText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [sessionId] = useState(() => `session-${Date.now()}`);
const messagesEndRef = useRef(null);
const inputRef = useRef(null);
useEffect(() => {
if (isOpen && messages.length === 0) {
// Add welcome message
setMessages([{
role: 'assistant',
content: `👋 Hi! I'm your AI assistant for StrikePackageGPT. I can help you with:
• Understanding security tools and commands
• Interpreting scan results
• Writing nmap, nikto, and other tool commands
• Navigating the platform
• Security best practices
What would you like help with?`,
timestamp: new Date()
}]);
}
}, [isOpen]);
useEffect(() => {
scrollToBottom();
}, [messages]);
useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus();
}
}, [isOpen]);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
const handleSendMessage = async () => {
if (!inputText.trim() || isLoading) return;
const userMessage = {
role: 'user',
content: inputText,
timestamp: new Date()
};
setMessages(prev => [...prev, userMessage]);
setInputText('');
setIsLoading(true);
try {
// Build context string
const contextString = `User is on ${currentPage} page. ${JSON.stringify(context)}`;
const response = await fetch('/api/llm/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: inputText,
session_id: sessionId,
context: contextString
})
});
if (!response.ok) {
throw new Error('Failed to get response');
}
const data = await response.json();
const assistantMessage = {
role: 'assistant',
content: data.message || data.content || 'I apologize, I had trouble processing that request.',
timestamp: new Date()
};
setMessages(prev => [...prev, assistantMessage]);
} catch (error) {
console.error('Error sending message:', error);
setMessages(prev => [...prev, {
role: 'assistant',
content: '❌ Sorry, I encountered an error. Please try again.',
timestamp: new Date(),
isError: true
}]);
} finally {
setIsLoading(false);
}
};
const handleKeyPress = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
};
const copyToClipboard = (text) => {
navigator.clipboard.writeText(text).then(() => {
// Could show a toast notification here
console.log('Copied to clipboard');
});
};
const clearChat = () => {
if (window.confirm('Clear all chat history?')) {
setMessages([{
role: 'assistant',
content: 'Chat history cleared. How can I help you?',
timestamp: new Date()
}]);
}
};
const renderMessage = (message, index) => {
const isUser = message.role === 'user';
const isError = message.isError;
// Check if message contains code blocks
const hasCode = message.content.includes('```');
let renderedContent;
if (hasCode) {
// Simple code block rendering
const parts = message.content.split(/(```[\s\S]*?```)/g);
renderedContent = parts.map((part, i) => {
if (part.startsWith('```')) {
const code = part.slice(3, -3).trim();
const [lang, ...codeLines] = code.split('\n');
const codeText = codeLines.join('\n');
return (
<div key={i} style={{
backgroundColor: '#f5f5f5',
padding: '10px',
borderRadius: '4px',
margin: '10px 0',
position: 'relative',
fontFamily: 'monospace',
fontSize: '13px',
overflowX: 'auto'
}}>
<div style={{
fontSize: '11px',
color: '#666',
marginBottom: '5px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<span>{lang || 'code'}</span>
<button
onClick={() => copyToClipboard(codeText)}
style={{
padding: '4px 8px',
fontSize: '11px',
border: 'none',
backgroundColor: '#ddd',
borderRadius: '3px',
cursor: 'pointer'
}}
>
📋 Copy
</button>
</div>
<pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>
<code>{codeText}</code>
</pre>
</div>
);
}
return <div key={i} style={{ whiteSpace: 'pre-wrap' }}>{part}</div>;
});
} else {
renderedContent = <div style={{ whiteSpace: 'pre-wrap' }}>{message.content}</div>;
}
return (
<div
key={index}
style={{
display: 'flex',
justifyContent: isUser ? 'flex-end' : 'flex-start',
marginBottom: '15px'
}}
>
<div
style={{
maxWidth: '80%',
padding: '12px 16px',
borderRadius: '12px',
backgroundColor: isError ? '#FCE4E4' : isUser ? '#3498DB' : '#ECF0F1',
color: isError ? '#E74C3C' : isUser ? 'white' : '#2C3E50',
fontSize: '14px',
lineHeight: '1.5',
boxShadow: '0 2px 5px rgba(0,0,0,0.1)'
}}
>
{renderedContent}
<div
style={{
fontSize: '11px',
color: isUser ? 'rgba(255,255,255,0.7)' : '#95A5A6',
marginTop: '5px',
textAlign: 'right'
}}
>
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
</div>
);
};
const quickActions = [
{ label: '📝 Write nmap command', prompt: 'How do I write an nmap command to scan a network?' },
{ label: '🔍 Interpret results', prompt: 'Help me understand these scan results' },
{ label: '🛠️ Use sqlmap', prompt: 'How do I use sqlmap to test for SQL injection?' },
{ label: '📊 Generate report', prompt: 'How do I generate a security assessment report?' }
];
if (!isOpen) return null;
return (
<div
style={{
position: 'fixed',
right: 0,
top: 0,
bottom: 0,
width: '400px',
backgroundColor: 'white',
boxShadow: '-4px 0 15px rgba(0,0,0,0.1)',
display: 'flex',
flexDirection: 'column',
zIndex: 9998
}}
>
{/* Header */}
<div
style={{
padding: '20px',
backgroundColor: '#3498DB',
color: 'white',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
borderBottom: '2px solid #2980B9'
}}
>
<div>
<h3 style={{ margin: '0 0 5px 0', fontSize: '18px' }}>💬 AI Assistant</h3>
<div style={{ fontSize: '12px', opacity: 0.9 }}>
Ask me anything about security testing
</div>
</div>
<div style={{ display: 'flex', gap: '10px' }}>
<button
onClick={clearChat}
style={{
background: 'none',
border: 'none',
color: 'white',
cursor: 'pointer',
fontSize: '18px',
padding: '5px'
}}
title="Clear chat"
>
🗑
</button>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
color: 'white',
cursor: 'pointer',
fontSize: '24px',
padding: '0'
}}
title="Close"
>
×
</button>
</div>
</div>
{/* Messages area */}
<div
style={{
flex: 1,
overflowY: 'auto',
padding: '20px',
backgroundColor: '#FAFAFA'
}}
>
{messages.map((message, index) => renderMessage(message, index))}
{isLoading && (
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', color: '#95A5A6' }}>
<div></div>
<div>Thinking...</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Quick actions */}
{messages.length <= 1 && (
<div
style={{
padding: '15px',
backgroundColor: '#F8F9FA',
borderTop: '1px solid #ddd'
}}
>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '10px' }}>
Quick actions:
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{quickActions.map((action, i) => (
<button
key={i}
onClick={() => {
setInputText(action.prompt);
inputRef.current?.focus();
}}
style={{
padding: '6px 12px',
fontSize: '12px',
border: '1px solid #ddd',
backgroundColor: 'white',
borderRadius: '16px',
cursor: 'pointer',
transition: 'all 0.2s'
}}
onMouseEnter={(e) => {
e.target.style.backgroundColor = '#E8F4F8';
e.target.style.borderColor = '#3498DB';
}}
onMouseLeave={(e) => {
e.target.style.backgroundColor = 'white';
e.target.style.borderColor = '#ddd';
}}
>
{action.label}
</button>
))}
</div>
</div>
)}
{/* Input area */}
<div
style={{
padding: '15px',
borderTop: '2px solid #ECF0F1',
backgroundColor: 'white'
}}
>
<div style={{ display: 'flex', gap: '10px' }}>
<textarea
ref={inputRef}
value={inputText}
onChange={(e) => setInputText(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Ask a question... (Enter to send)"
disabled={isLoading}
style={{
flex: 1,
padding: '10px',
border: '1px solid #ddd',
borderRadius: '8px',
fontSize: '14px',
resize: 'none',
minHeight: '60px',
maxHeight: '120px',
fontFamily: 'inherit'
}}
/>
<button
onClick={handleSendMessage}
disabled={!inputText.trim() || isLoading}
style={{
padding: '10px 20px',
border: 'none',
backgroundColor: !inputText.trim() || isLoading ? '#95A5A6' : '#3498DB',
color: 'white',
borderRadius: '8px',
cursor: !inputText.trim() || isLoading ? 'not-allowed' : 'pointer',
fontSize: '14px',
fontWeight: '500'
}}
>
{isLoading ? '⏳' : '📤'}
</button>
</div>
<div style={{ fontSize: '11px', color: '#95A5A6', marginTop: '8px' }}>
Shift+Enter for new line
</div>
</div>
</div>
);
};
export default HelpChat;

View File

@@ -0,0 +1,315 @@
/**
* NetworkMap Component
* Interactive network graph visualization using Cytoscape.js
* Displays discovered hosts from nmap scans with OS/device icons
*/
import React, { useState, useEffect, useRef } from 'react';
const NetworkMap = ({ scanId, onNodeClick }) => {
const [hosts, setHosts] = useState([]);
const [loading, setLoading] = useState(false);
const [filterText, setFilterText] = useState('');
const cyRef = useRef(null);
const containerRef = useRef(null);
useEffect(() => {
if (scanId) {
fetchHostData(scanId);
}
}, [scanId]);
useEffect(() => {
if (hosts.length > 0 && containerRef.current) {
initializeNetwork();
}
}, [hosts]);
const fetchHostData = async (scanId) => {
setLoading(true);
try {
const response = await fetch(`/api/nmap/hosts?scan_id=${scanId}`);
const data = await response.json();
setHosts(data.hosts || []);
} catch (error) {
console.error('Error fetching host data:', error);
} finally {
setLoading(false);
}
};
const initializeNetwork = () => {
// NOTE: This requires cytoscape.js to be installed: npm install cytoscape
// import cytoscape from 'cytoscape';
// Example initialization (requires actual cytoscape import)
/*
const cy = cytoscape({
container: containerRef.current,
elements: buildGraphElements(hosts),
style: getNetworkStyle(),
layout: {
name: 'cose',
idealEdgeLength: 100,
nodeOverlap: 20,
refresh: 20,
fit: true,
padding: 30,
randomize: false,
componentSpacing: 100,
nodeRepulsion: 400000,
edgeElasticity: 100,
nestingFactor: 5,
gravity: 80,
numIter: 1000,
initialTemp: 200,
coolingFactor: 0.95,
minTemp: 1.0
}
});
cy.on('tap', 'node', (evt) => {
const node = evt.target;
const hostData = node.data();
if (onNodeClick) {
onNodeClick(hostData);
}
});
cyRef.current = cy;
*/
};
const buildGraphElements = (hosts) => {
const elements = [];
// Add nodes for each host
hosts.forEach((host, index) => {
elements.push({
group: 'nodes',
data: {
id: `host-${index}`,
label: host.hostname || host.ip,
...host,
icon: getIconForHost(host)
},
classes: getNodeClass(host)
});
});
// Add edges (connections) - could be inferred from network topology
// For now, connect hosts in same subnet
const subnets = groupBySubnet(hosts);
Object.values(subnets).forEach(subnetHosts => {
if (subnetHosts.length > 1) {
for (let i = 0; i < subnetHosts.length - 1; i++) {
elements.push({
group: 'edges',
data: {
id: `edge-${subnetHosts[i].ip}-${subnetHosts[i + 1].ip}`,
source: `host-${hosts.indexOf(subnetHosts[i])}`,
target: `host-${hosts.indexOf(subnetHosts[i + 1])}`
}
});
}
}
});
return elements;
};
const getIconForHost = (host) => {
const osType = (host.os_type || '').toLowerCase();
const deviceType = (host.device_type || '').toLowerCase();
if (deviceType.includes('server')) return '/static/server.svg';
if (deviceType.includes('network') || deviceType.includes('router') || deviceType.includes('switch')) {
return '/static/network.svg';
}
if (deviceType.includes('workstation')) return '/static/workstation.svg';
if (osType.includes('windows')) return '/static/windows.svg';
if (osType.includes('linux') || osType.includes('unix')) return '/static/linux.svg';
if (osType.includes('mac') || osType.includes('darwin')) return '/static/mac.svg';
return '/static/unknown.svg';
};
const getNodeClass = (host) => {
const deviceType = (host.device_type || '').toLowerCase();
if (deviceType.includes('server')) return 'node-server';
if (deviceType.includes('network')) return 'node-network';
if (deviceType.includes('workstation')) return 'node-workstation';
return 'node-unknown';
};
const groupBySubnet = (hosts) => {
const subnets = {};
hosts.forEach(host => {
const subnet = host.ip.split('.').slice(0, 3).join('.');
if (!subnets[subnet]) {
subnets[subnet] = [];
}
subnets[subnet].push(host);
});
return subnets;
};
const getNetworkStyle = () => {
return [
{
selector: 'node',
style: {
'background-color': '#4A90E2',
'label': 'data(label)',
'text-valign': 'bottom',
'text-halign': 'center',
'font-size': '12px',
'color': '#333',
'text-margin-y': 5,
'width': 50,
'height': 50,
'background-image': 'data(icon)',
'background-fit': 'contain'
}
},
{
selector: '.node-server',
style: {
'background-color': '#4A90E2'
}
},
{
selector: '.node-network',
style: {
'background-color': '#16A085'
}
},
{
selector: '.node-workstation',
style: {
'background-color': '#5DADE2'
}
},
{
selector: 'edge',
style: {
'width': 2,
'line-color': '#95A5A6',
'curve-style': 'bezier'
}
},
{
selector: 'node:selected',
style: {
'border-width': 3,
'border-color': '#E74C3C'
}
}
];
};
const exportToPNG = () => {
if (cyRef.current) {
const png = cyRef.current.png({ scale: 2, full: true });
const link = document.createElement('a');
link.href = png;
link.download = `network-map-${Date.now()}.png`;
link.click();
}
};
const exportToCSV = () => {
const csvContent = [
['IP', 'Hostname', 'OS Type', 'Device Type', 'MAC', 'Vendor', 'Open Ports'].join(','),
...hosts.map(host => [
host.ip,
host.hostname || '',
host.os_type || '',
host.device_type || '',
host.mac || '',
host.vendor || '',
(host.ports || []).map(p => p.port).join(';')
].join(','))
].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `network-hosts-${Date.now()}.csv`;
link.click();
URL.revokeObjectURL(url);
};
const filteredHosts = hosts.filter(host => {
if (!filterText) return true;
const searchLower = filterText.toLowerCase();
return (
host.ip.includes(searchLower) ||
(host.hostname || '').toLowerCase().includes(searchLower) ||
(host.os_type || '').toLowerCase().includes(searchLower) ||
(host.device_type || '').toLowerCase().includes(searchLower)
);
});
return (
<div className="network-map-container" style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}>
<div className="network-map-toolbar" style={{ padding: '10px', borderBottom: '1px solid #ddd', display: 'flex', gap: '10px', alignItems: 'center' }}>
<input
type="text"
placeholder="Filter hosts (IP, hostname, OS, device type)..."
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
style={{ flex: 1, padding: '8px', borderRadius: '4px', border: '1px solid #ccc' }}
/>
<button onClick={exportToPNG} style={{ padding: '8px 16px', cursor: 'pointer', borderRadius: '4px' }}>
Export PNG
</button>
<button onClick={exportToCSV} style={{ padding: '8px 16px', cursor: 'pointer', borderRadius: '4px' }}>
Export CSV
</button>
<span style={{ color: '#666' }}>
{filteredHosts.length} host{filteredHosts.length !== 1 ? 's' : ''}
</span>
</div>
<div
ref={containerRef}
className="network-map-canvas"
style={{
flex: 1,
backgroundColor: '#f5f5f5',
position: 'relative'
}}
>
{loading && (
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
textAlign: 'center'
}}>
<div>Loading network map...</div>
</div>
)}
{!loading && hosts.length === 0 && (
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
textAlign: 'center',
color: '#666'
}}>
<div>No hosts discovered yet</div>
<div style={{ fontSize: '14px', marginTop: '10px' }}>Run a network scan to populate the map</div>
</div>
)}
</div>
</div>
);
};
export default NetworkMap;

View File

@@ -0,0 +1,354 @@
/**
* VoiceControls Component
* Microphone button with hotkey support for voice commands
* Visual feedback for listening, processing, and speaking states
*/
import React, { useState, useEffect, useRef } from 'react';
const VoiceControls = ({ onCommand, hotkey = ' ' }) => {
const [state, setState] = useState('idle'); // idle, listening, processing, speaking
const [transcript, setTranscript] = useState('');
const [error, setError] = useState(null);
const [permissionGranted, setPermissionGranted] = useState(false);
const mediaRecorderRef = useRef(null);
const audioChunksRef = useRef([]);
const hotkeyPressedRef = useRef(false);
useEffect(() => {
// Check if browser supports MediaRecorder
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
setError('Voice control not supported in this browser');
return;
}
// Request microphone permission
requestMicrophonePermission();
// Setup hotkey listener
const handleKeyDown = (e) => {
if (e.key === hotkey && !hotkeyPressedRef.current && state === 'idle') {
hotkeyPressedRef.current = true;
startListening();
}
};
const handleKeyUp = (e) => {
if (e.key === hotkey && hotkeyPressedRef.current) {
hotkeyPressedRef.current = false;
if (state === 'listening') {
stopListening();
}
}
};
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
if (mediaRecorderRef.current && state === 'listening') {
mediaRecorderRef.current.stop();
}
};
}, [hotkey, state]);
const requestMicrophonePermission = async () => {
try {
await navigator.mediaDevices.getUserMedia({ audio: true });
setPermissionGranted(true);
} catch (err) {
setError('Microphone permission denied');
setPermissionGranted(false);
}
};
const startListening = async () => {
if (!permissionGranted) {
await requestMicrophonePermission();
if (!permissionGranted) return;
}
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const mediaRecorder = new MediaRecorder(stream);
mediaRecorderRef.current = mediaRecorder;
audioChunksRef.current = [];
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunksRef.current.push(event.data);
}
};
mediaRecorder.onstop = async () => {
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' });
await processAudio(audioBlob);
// Stop all tracks
stream.getTracks().forEach(track => track.stop());
};
mediaRecorder.start();
setState('listening');
setTranscript('');
setError(null);
} catch (err) {
console.error('Error starting recording:', err);
setError('Failed to start recording: ' + err.message);
}
};
const stopListening = () => {
if (mediaRecorderRef.current && mediaRecorderRef.current.state === 'recording') {
mediaRecorderRef.current.stop();
}
};
const processAudio = async (audioBlob) => {
setState('processing');
try {
// Send audio to backend for transcription
const formData = new FormData();
formData.append('audio', audioBlob, 'recording.webm');
const response = await fetch('/api/voice/transcribe', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('Transcription failed');
}
const data = await response.json();
const transcribedText = data.text || '';
setTranscript(transcribedText);
if (transcribedText) {
// Parse and route the voice command
await routeCommand(transcribedText);
} else {
setError('No speech detected');
setState('idle');
}
} catch (err) {
console.error('Error processing audio:', err);
setError('Failed to process audio: ' + err.message);
setState('idle');
}
};
const routeCommand = async (text) => {
try {
const response = await fetch('/api/voice/command', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
});
if (!response.ok) {
throw new Error('Command routing failed');
}
const commandResult = await response.json();
// Call parent callback with command result
if (onCommand) {
onCommand(commandResult);
}
// Check if TTS response is available
if (commandResult.speak_response) {
await speakResponse(commandResult.speak_response);
} else {
setState('idle');
}
} catch (err) {
console.error('Error routing command:', err);
setError('Failed to execute command: ' + err.message);
setState('idle');
}
};
const speakResponse = async (text) => {
setState('speaking');
try {
// Try to get TTS audio from backend
const response = await fetch('/api/voice/speak', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
});
if (response.ok) {
const audioBlob = await response.blob();
const audioUrl = URL.createObjectURL(audioBlob);
const audio = new Audio(audioUrl);
audio.onended = () => {
setState('idle');
URL.revokeObjectURL(audioUrl);
};
audio.play();
} else {
// Fallback to browser TTS
if ('speechSynthesis' in window) {
const utterance = new SpeechSynthesisUtterance(text);
utterance.onend = () => setState('idle');
window.speechSynthesis.speak(utterance);
} else {
setState('idle');
}
}
} catch (err) {
console.error('Error speaking response:', err);
setState('idle');
}
};
const getStateColor = () => {
switch (state) {
case 'listening': return '#27AE60';
case 'processing': return '#F39C12';
case 'speaking': return '#3498DB';
default: return '#95A5A6';
}
};
const getStateIcon = () => {
switch (state) {
case 'listening': return '🎤';
case 'processing': return '⏳';
case 'speaking': return '🔊';
default: return '🎙️';
}
};
return (
<div
className="voice-controls"
style={{
position: 'fixed',
bottom: '20px',
right: '20px',
zIndex: 1000,
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
gap: '10px'
}}
>
{/* Transcript display */}
{transcript && (
<div
style={{
backgroundColor: 'white',
padding: '10px 15px',
borderRadius: '8px',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
maxWidth: '300px',
fontSize: '14px',
color: '#333'
}}
>
<strong>You said:</strong> {transcript}
</div>
)}
{/* Error display */}
{error && (
<div
style={{
backgroundColor: '#E74C3C',
color: 'white',
padding: '10px 15px',
borderRadius: '8px',
maxWidth: '300px',
fontSize: '14px'
}}
>
{error}
</div>
)}
{/* Mic button */}
<button
onClick={state === 'idle' ? startListening : stopListening}
disabled={state === 'processing' || state === 'speaking'}
style={{
width: '60px',
height: '60px',
borderRadius: '50%',
border: 'none',
backgroundColor: getStateColor(),
color: 'white',
fontSize: '24px',
cursor: state === 'idle' || state === 'listening' ? 'pointer' : 'not-allowed',
boxShadow: '0 4px 12px rgba(0,0,0,0.2)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.3s ease',
transform: state === 'listening' ? 'scale(1.1)' : 'scale(1)',
opacity: state === 'processing' || state === 'speaking' ? 0.7 : 1
}}
title={`Voice command (hold ${hotkey === ' ' ? 'Space' : hotkey})`}
>
{getStateIcon()}
</button>
{/* Pulsing animation for listening state */}
{state === 'listening' && (
<div
style={{
position: 'absolute',
bottom: '0',
right: '0',
width: '60px',
height: '60px',
borderRadius: '50%',
border: '3px solid #27AE60',
animation: 'pulse 1.5s infinite',
pointerEvents: 'none'
}}
/>
)}
{/* Hotkey hint */}
<div
style={{
fontSize: '12px',
color: '#666',
textAlign: 'center'
}}
>
Hold {hotkey === ' ' ? 'Space' : hotkey} to talk
</div>
<style>{`
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.3);
opacity: 0.5;
}
100% {
transform: scale(1.6);
opacity: 0;
}
}
`}</style>
</div>
);
};
export default VoiceControls;

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<circle cx="24" cy="24" r="22" fill="#000000"/>
<ellipse cx="24" cy="20" rx="12" ry="14" fill="#FFFFFF"/>
<ellipse cx="24" cy="28" rx="10" ry="8" fill="#FDB515"/>
<circle cx="20" cy="18" r="2" fill="#000000"/>
<circle cx="28" cy="18" r="2" fill="#000000"/>
<path d="M 24 22 Q 22 24 24 24 Q 26 24 24 22" fill="none" stroke="#000000" stroke-width="1.5"/>
<path d="M 16 26 Q 18 30 24 32 Q 30 30 32 26" fill="none" stroke="#000000" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 554 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<circle cx="24" cy="24" r="22" fill="#A2AAAD"/>
<path d="M 30 14 Q 28 10 24 10 Q 20 10 18 14 Q 16 18 18 22 Q 20 26 24 26 Q 28 26 30 22 Q 32 18 30 14 Z" fill="#FFFFFF"/>
<circle cx="24" cy="16" r="4" fill="#A2AAAD"/>
<path d="M 26 10 Q 28 8 30 10" stroke="#FFFFFF" stroke-width="2" fill="none"/>
<rect x="22" y="26" width="4" height="8" fill="#FFFFFF"/>
<path d="M 18 34 Q 20 36 24 36 Q 28 36 30 34 L 28 32 Q 26 33 24 33 Q 22 33 20 32 Z" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 557 B

View File

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<rect width="48" height="48" fill="#16A085" rx="2"/>
<rect x="6" y="18" width="36" height="12" rx="1" fill="#2C3E50"/>
<circle cx="10" cy="24" r="1.5" fill="#27AE60"/>
<circle cx="14" cy="24" r="1.5" fill="#27AE60"/>
<circle cx="18" cy="24" r="1.5" fill="#27AE60"/>
<circle cx="22" cy="24" r="1.5" fill="#27AE60"/>
<circle cx="26" cy="24" r="1.5" fill="#F39C12"/>
<circle cx="30" cy="24" r="1.5" fill="#95A5A6"/>
<circle cx="34" cy="24" r="1.5" fill="#95A5A6"/>
<circle cx="38" cy="24" r="1.5" fill="#95A5A6"/>
<line x1="24" y1="10" x2="24" y2="18" stroke="#ECF0F1" stroke-width="2"/>
<line x1="24" y1="30" x2="24" y2="38" stroke="#ECF0F1" stroke-width="2"/>
<line x1="10" y1="24" x2="4" y2="24" stroke="#ECF0F1" stroke-width="2"/>
<line x1="38" y1="24" x2="44" y2="24" stroke="#ECF0F1" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 925 B

View File

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<rect width="48" height="48" fill="#4A90E2" rx="2"/>
<rect x="6" y="6" width="36" height="10" rx="1" fill="#2C3E50"/>
<rect x="6" y="19" width="36" height="10" rx="1" fill="#34495E"/>
<rect x="6" y="32" width="36" height="10" rx="1" fill="#2C3E50"/>
<circle cx="10" cy="11" r="1.5" fill="#27AE60"/>
<circle cx="14" cy="11" r="1.5" fill="#F39C12"/>
<circle cx="10" cy="24" r="1.5" fill="#27AE60"/>
<circle cx="14" cy="24" r="1.5" fill="#27AE60"/>
<circle cx="10" cy="37" r="1.5" fill="#E74C3C"/>
<circle cx="14" cy="37" r="1.5" fill="#F39C12"/>
<rect x="18" y="9" width="20" height="4" rx="0.5" fill="#7F8C8D"/>
<rect x="18" y="22" width="20" height="4" rx="0.5" fill="#7F8C8D"/>
<rect x="18" y="35" width="20" height="4" rx="0.5" fill="#7F8C8D"/>
</svg>

After

Width:  |  Height:  |  Size: 864 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<circle cx="24" cy="24" r="22" fill="#95A5A6"/>
<circle cx="24" cy="24" r="18" fill="#7F8C8D"/>
<text x="24" y="32" font-size="24" font-weight="bold" fill="#ECF0F1" text-anchor="middle" font-family="Arial">?</text>
</svg>

After

Width:  |  Height:  |  Size: 312 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<rect width="48" height="48" fill="#0078D4" rx="2"/>
<rect x="6" y="6" width="17" height="17" fill="#FFFFFF"/>
<rect x="25" y="6" width="17" height="17" fill="#FFFFFF"/>
<rect x="6" y="25" width="17" height="17" fill="#FFFFFF"/>
<rect x="25" y="25" width="17" height="17" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 390 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<rect width="48" height="48" fill="#5DADE2" rx="2"/>
<rect x="8" y="8" width="32" height="22" rx="1" fill="#34495E"/>
<rect x="10" y="10" width="28" height="18" fill="#3498DB"/>
<rect x="18" y="30" width="12" height="2" fill="#34495E"/>
<rect x="12" y="32" width="24" height="4" rx="1" fill="#2C3E50"/>
<circle cx="24" cy="14" r="2" fill="#ECF0F1"/>
<rect x="18" y="18" width="12" height="8" rx="1" fill="#ECF0F1"/>
</svg>

After

Width:  |  Height:  |  Size: 521 B