feat: implement automatic ChromaDB indexing and loop-based documentation generation
Some checks failed
CI/CD Pipeline / Run Tests (push) Has been skipped
CI/CD Pipeline / Security Scanning (push) Has been skipped
CI/CD Pipeline / Generate Documentation (push) Failing after 8m31s
CI/CD Pipeline / Lint Code (push) Failing after 8m33s
CI/CD Pipeline / Build and Push Docker Images (api) (push) Has been skipped
CI/CD Pipeline / Build and Push Docker Images (chat) (push) Has been skipped
CI/CD Pipeline / Build and Push Docker Images (frontend) (push) Has been skipped
CI/CD Pipeline / Build and Push Docker Images (worker) (push) Has been skipped
CI/CD Pipeline / Deploy to Staging (push) Has been skipped
CI/CD Pipeline / Deploy to Production (push) Has been skipped
Some checks failed
CI/CD Pipeline / Run Tests (push) Has been skipped
CI/CD Pipeline / Security Scanning (push) Has been skipped
CI/CD Pipeline / Generate Documentation (push) Failing after 8m31s
CI/CD Pipeline / Lint Code (push) Failing after 8m33s
CI/CD Pipeline / Build and Push Docker Images (api) (push) Has been skipped
CI/CD Pipeline / Build and Push Docker Images (chat) (push) Has been skipped
CI/CD Pipeline / Build and Push Docker Images (frontend) (push) Has been skipped
CI/CD Pipeline / Build and Push Docker Images (worker) (push) Has been skipped
CI/CD Pipeline / Deploy to Staging (push) Has been skipped
CI/CD Pipeline / Deploy to Production (push) Has been skipped
- Add automatic ChromaDB indexing after documentation generation - Implement loop-based section generation for individual VM and container documentation - Fix Celery anti-pattern in generate_proxmox_docs task (removed blocking .get() call) - Share ChromaDB vector store volume between worker and chat services - Add documentation management UI to frontend with manual job triggering and log viewing - Fix frontend Socket.IO connection URL to point to correct chat service port - Enhance DocumentationAgent.index_documentation() with automatic cleanup of old documents - Update Proxmox template to generate individual files for each VM and container This enables the RAG system to properly respond with infrastructure-specific information from the generated documentation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ import remarkGfm from 'remark-gfm';
|
||||
|
||||
// Use relative URLs to work with nginx proxy in production
|
||||
const API_URL = import.meta.env.VITE_API_URL || (typeof window !== 'undefined' ? window.location.origin + '/api' : 'http://localhost:8000');
|
||||
const CHAT_URL = import.meta.env.VITE_CHAT_URL || (typeof window !== 'undefined' ? window.location.origin : 'http://localhost:8001');
|
||||
const CHAT_URL = import.meta.env.VITE_CHAT_URL || 'http://localhost:8001';
|
||||
|
||||
function App() {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
@@ -43,13 +43,15 @@ function App() {
|
||||
<Tab label="Chat Support" />
|
||||
<Tab label="Ticket Resolution" />
|
||||
<Tab label="Documentation Search" />
|
||||
<Tab label="Documentation Management" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
|
||||
<Container maxWidth="xl" sx={{ mt: 4, mb: 4 }}>
|
||||
{activeTab === 0 && <ChatInterface />}
|
||||
{activeTab === 1 && <TicketInterface />}
|
||||
{activeTab === 2 && <SearchInterface />}
|
||||
{activeTab === 3 && <DocumentationInterface />}
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
@@ -380,7 +382,7 @@ function SearchInterface() {
|
||||
const [query, setQuery] = useState('');
|
||||
const [results, setResults] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
|
||||
const search = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -394,7 +396,7 @@ function SearchInterface() {
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Paper sx={{ p: 2, mb: 3 }}>
|
||||
@@ -416,9 +418,9 @@ function SearchInterface() {
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
|
||||
{loading && <CircularProgress />}
|
||||
|
||||
|
||||
<Grid container spacing={2}>
|
||||
{results.map((result, idx) => (
|
||||
<Grid item xs={12} key={idx}>
|
||||
@@ -441,4 +443,302 @@ function SearchInterface() {
|
||||
);
|
||||
}
|
||||
|
||||
// Documentation Management Interface
|
||||
function DocumentationInterface() {
|
||||
const [jobStatus, setJobStatus] = useState(null);
|
||||
const [generatingJob, setGeneratingJob] = useState(null);
|
||||
const [files, setFiles] = useState([]);
|
||||
const [selectedFile, setSelectedFile] = useState(null);
|
||||
const [fileContent, setFileContent] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showLogs, setShowLogs] = useState(false);
|
||||
const [jobLogs, setJobLogs] = useState([]);
|
||||
|
||||
// Load available files on mount
|
||||
useEffect(() => {
|
||||
loadFiles();
|
||||
}, []);
|
||||
|
||||
// Poll job status when generating
|
||||
useEffect(() => {
|
||||
if (!generatingJob) return;
|
||||
|
||||
const pollInterval = setInterval(async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/api/v1/documentation/jobs/${generatingJob}/status`);
|
||||
setJobStatus(response.data);
|
||||
|
||||
if (response.data.completed) {
|
||||
clearInterval(pollInterval);
|
||||
setGeneratingJob(null);
|
||||
loadFiles(); // Refresh file list
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error polling job status:', error);
|
||||
clearInterval(pollInterval);
|
||||
setGeneratingJob(null);
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
return () => clearInterval(pollInterval);
|
||||
}, [generatingJob]);
|
||||
|
||||
// Poll logs when log viewer is open
|
||||
useEffect(() => {
|
||||
if (!showLogs || !jobStatus?.job_id) return;
|
||||
|
||||
const loadLogs = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/api/v1/documentation/jobs/${jobStatus.job_id}/logs`);
|
||||
setJobLogs(response.data.logs || []);
|
||||
} catch (error) {
|
||||
console.error('Error loading logs:', error);
|
||||
}
|
||||
};
|
||||
|
||||
loadLogs(); // Initial load
|
||||
const logInterval = setInterval(loadLogs, 3000); // Poll every 3 seconds
|
||||
|
||||
return () => clearInterval(logInterval);
|
||||
}, [showLogs, jobStatus?.job_id]);
|
||||
|
||||
const loadFiles = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/api/v1/documentation/files`);
|
||||
setFiles(response.data.categories || []);
|
||||
} catch (error) {
|
||||
console.error('Error loading files:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const triggerGeneration = async () => {
|
||||
setLoading(true);
|
||||
setJobStatus(null);
|
||||
try {
|
||||
const response = await axios.post(`${API_URL}/api/v1/documentation/jobs/proxmox`);
|
||||
setGeneratingJob(response.data.job_id);
|
||||
setJobStatus({
|
||||
job_id: response.data.job_id,
|
||||
status: response.data.status,
|
||||
completed: false
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error triggering generation:', error);
|
||||
alert('Failed to start documentation generation: ' + error.message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const viewFile = async (category, filename) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/api/v1/documentation/files/${category}/${filename}`);
|
||||
setFileContent(response.data);
|
||||
setSelectedFile({ category, filename });
|
||||
} catch (error) {
|
||||
console.error('Error loading file:', error);
|
||||
alert('Failed to load file: ' + error.message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const formatBytes = (bytes) => {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
};
|
||||
|
||||
const formatDate = (timestamp) => {
|
||||
return new Date(timestamp * 1000).toLocaleString();
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container spacing={3}>
|
||||
{/* Control Panel */}
|
||||
<Grid item xs={12} md={4}>
|
||||
<Paper sx={{ p: 3, mb: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>Generate Documentation</Typography>
|
||||
<Typography variant="body2" color="text.secondary" paragraph>
|
||||
Trigger manual generation of Proxmox infrastructure documentation
|
||||
</Typography>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
onClick={triggerGeneration}
|
||||
disabled={loading || generatingJob}
|
||||
startIcon={<UploadIcon />}
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
{generatingJob ? 'Generating...' : 'Generate Proxmox Docs'}
|
||||
</Button>
|
||||
|
||||
{jobStatus && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>Job Status</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
||||
{!jobStatus.completed && <CircularProgress size={20} />}
|
||||
<Chip
|
||||
label={jobStatus.status}
|
||||
color={jobStatus.completed ? (jobStatus.status === 'SUCCESS' ? 'success' : 'error') : 'warning'}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Job ID: {jobStatus.job_id}
|
||||
</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={() => setShowLogs(!showLogs)}
|
||||
sx={{ mt: 1 }}
|
||||
fullWidth
|
||||
>
|
||||
{showLogs ? 'Hide Logs' : 'View Logs'}
|
||||
</Button>
|
||||
{jobStatus.error && (
|
||||
<Typography variant="caption" color="error" display="block" sx={{ mt: 1 }}>
|
||||
Error: {jobStatus.error}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{/* Log Viewer */}
|
||||
{showLogs && jobStatus && (
|
||||
<Paper sx={{ p: 2, mb: 2, bgcolor: '#1e1e1e', maxHeight: '400px', overflow: 'auto' }}>
|
||||
<Typography variant="subtitle2" gutterBottom sx={{ color: '#fff' }}>
|
||||
Job Logs (Last 50 lines)
|
||||
</Typography>
|
||||
{jobLogs.length === 0 ? (
|
||||
<Typography variant="caption" sx={{ color: '#888' }}>
|
||||
No logs available yet...
|
||||
</Typography>
|
||||
) : (
|
||||
<Box component="pre" sx={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.75rem',
|
||||
color: '#0f0',
|
||||
margin: 0,
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all'
|
||||
}}>
|
||||
{jobLogs.join('\n')}
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* File List */}
|
||||
<Paper sx={{ p: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>Available Documentation</Typography>
|
||||
{files.length === 0 ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No documentation files found. Generate some first!
|
||||
</Typography>
|
||||
) : (
|
||||
files.map((category) => (
|
||||
<Box key={category.name} sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
|
||||
{category.name.toUpperCase()}
|
||||
</Typography>
|
||||
<List dense>
|
||||
{category.files.map((file) => (
|
||||
<ListItem
|
||||
key={file.filename}
|
||||
button
|
||||
onClick={() => viewFile(category.name, file.filename)}
|
||||
selected={selectedFile?.filename === file.filename}
|
||||
>
|
||||
<ListItemText
|
||||
primary={file.filename}
|
||||
secondary={`${formatBytes(file.size)} • ${formatDate(file.modified)}`}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
{/* Markdown Viewer */}
|
||||
<Grid item xs={12} md={8}>
|
||||
{fileContent ? (
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h6">{fileContent.filename}</Typography>
|
||||
<Chip label={fileContent.category} size="small" />
|
||||
</Box>
|
||||
<Divider sx={{ mb: 3 }} />
|
||||
<Box sx={{
|
||||
'& h1, & h2, & h3': { mt: 3, mb: 2 },
|
||||
'& h1': { fontSize: '2rem', fontWeight: 600, borderBottom: '2px solid #e0e0e0', pb: 1 },
|
||||
'& h2': { fontSize: '1.5rem', fontWeight: 600 },
|
||||
'& h3': { fontSize: '1.2rem', fontWeight: 600 },
|
||||
'& p': { mb: 2, lineHeight: 1.6 },
|
||||
'& ul, & ol': { pl: 3, mb: 2 },
|
||||
'& li': { mb: 1 },
|
||||
'& code': {
|
||||
bgcolor: '#f5f5f5',
|
||||
p: 0.5,
|
||||
borderRadius: 1,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.9em',
|
||||
border: '1px solid #e0e0e0'
|
||||
},
|
||||
'& pre': {
|
||||
bgcolor: '#f5f5f5',
|
||||
p: 2,
|
||||
borderRadius: 1,
|
||||
overflow: 'auto',
|
||||
border: '1px solid #e0e0e0',
|
||||
'& code': { bgcolor: 'transparent', p: 0, border: 'none' }
|
||||
},
|
||||
'& table': {
|
||||
width: '100%',
|
||||
borderCollapse: 'collapse',
|
||||
mb: 2
|
||||
},
|
||||
'& th, & td': {
|
||||
border: '1px solid #e0e0e0',
|
||||
p: 1,
|
||||
textAlign: 'left'
|
||||
},
|
||||
'& th': {
|
||||
bgcolor: '#f5f5f5',
|
||||
fontWeight: 600
|
||||
},
|
||||
'& blockquote': {
|
||||
borderLeft: '4px solid #e0e0e0',
|
||||
pl: 2,
|
||||
ml: 0,
|
||||
fontStyle: 'italic',
|
||||
color: 'text.secondary'
|
||||
},
|
||||
'& hr': { my: 3, border: 'none', borderTop: '1px solid #e0e0e0' }
|
||||
}}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{fileContent.content}
|
||||
</ReactMarkdown>
|
||||
</Box>
|
||||
<Divider sx={{ mt: 3, mb: 2 }} />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Last modified: {formatDate(fileContent.modified)} • Size: {formatBytes(fileContent.size)}
|
||||
</Typography>
|
||||
</Paper>
|
||||
) : (
|
||||
<Paper sx={{ p: 3, textAlign: 'center' }}>
|
||||
<DocIcon sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Select a documentation file to view
|
||||
</Typography>
|
||||
</Paper>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
Reference in New Issue
Block a user