back
This commit is contained in:
52
frontend/src/components/Layout.jsx
Normal file
52
frontend/src/components/Layout.jsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { AppBar, Toolbar, Typography, Button, Container, Box } from '@mui/material';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
const Layout = ({ children }) => {
|
||||
const { user, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<Typography variant="h6" component={Link} to="/" sx={{ flexGrow: 1, color: 'inherit', textDecoration: 'none' }}>
|
||||
AI Meeting Assistant
|
||||
</Typography>
|
||||
{user && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Button color="inherit" component={Link} to="/">
|
||||
Dashboard
|
||||
</Button>
|
||||
<Button color="inherit" component={Link} to="/processing">
|
||||
Processing Tools
|
||||
</Button>
|
||||
{user.role === 'admin' && (
|
||||
<Button color="inherit" component={Link} to="/admin">
|
||||
Admin
|
||||
</Button>
|
||||
)}
|
||||
<Typography sx={{ mx: 2 }}>
|
||||
| Welcome, {user.username} ({user.role})
|
||||
</Typography>
|
||||
<Button color="inherit" onClick={handleLogout}>
|
||||
Logout
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Container component="main" maxWidth={false} sx={{ mt: 4, mb: 4, flexGrow: 1 }}>
|
||||
{children}
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
82
frontend/src/components/NewMeetingDialog.jsx
Normal file
82
frontend/src/components/NewMeetingDialog.jsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Button, TextField, Dialog, DialogTitle, DialogContent, DialogActions,
|
||||
CircularProgress, Alert
|
||||
} from '@mui/material';
|
||||
|
||||
const NewMeetingDialog = ({ open, onClose, onCreate }) => {
|
||||
const [topic, setTopic] = useState('');
|
||||
const [meetingDate, setMeetingDate] = useState(new Date().toISOString().split('T')[0]); // Defaults to today
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!topic || !meetingDate) {
|
||||
setError('Both topic and date are required.');
|
||||
return;
|
||||
}
|
||||
setError('');
|
||||
setLoading(true);
|
||||
try {
|
||||
// The onCreate prop is expected to be an async function
|
||||
// that returns the newly created meeting.
|
||||
await onCreate(topic, meetingDate);
|
||||
handleClose(); // Close the dialog on success
|
||||
} catch (err) {
|
||||
setError(err.message || 'Failed to create meeting.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (loading) return; // Prevent closing while loading
|
||||
setTopic('');
|
||||
setMeetingDate(new Date().toISOString().split('T')[0]);
|
||||
setError('');
|
||||
onClose(); // Call the parent's onClose handler
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose} fullWidth maxWidth="xs">
|
||||
<DialogTitle>Create a New Meeting</DialogTitle>
|
||||
<DialogContent>
|
||||
{error && <Alert severity="error" sx={{ mb: 2, mt: 1 }}>{error}</Alert>}
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
id="topic"
|
||||
label="Meeting Topic"
|
||||
type="text"
|
||||
fullWidth
|
||||
variant="standard"
|
||||
value={topic}
|
||||
onChange={(e) => setTopic(e.target.value)}
|
||||
sx={{ mt: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
margin="dense"
|
||||
id="meetingDate"
|
||||
label="Meeting Date"
|
||||
type="date"
|
||||
fullWidth
|
||||
variant="standard"
|
||||
value={meetingDate}
|
||||
onChange={(e) => setMeetingDate(e.target.value)}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
sx={{ mt: 2 }}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} disabled={loading}>Cancel</Button>
|
||||
<Button onClick={handleCreate} variant="contained" disabled={loading}>
|
||||
{loading ? <CircularProgress size={24} /> : 'Create'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewMeetingDialog;
|
||||
116
frontend/src/components/TextProcessingTools.jsx
Normal file
116
frontend/src/components/TextProcessingTools.jsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box, Button, Typography, Paper, TextField, Select, MenuItem, FormControl, InputLabel, Dialog, DialogTitle, DialogContent, DialogActions, CircularProgress, Alert, Table, TableBody, TableCell, TableContainer, TableHead, TableRow
|
||||
} from '@mui/material';
|
||||
import { getMeetings, createMeeting, batchCreateActionItems } from '../services/api'; // Only necessary APIs
|
||||
|
||||
const TextProcessingTools = ({
|
||||
textContent,
|
||||
summary,
|
||||
actionItems,
|
||||
onGenerateSummary,
|
||||
onPreviewActions,
|
||||
onActionItemChange
|
||||
}) => {
|
||||
const [meetings, setMeetings] = useState([]);
|
||||
const [users, setUsers] = useState([]); // Assuming users are needed for dropdown
|
||||
const [isMeetingDialogOpen, setIsMeetingDialogOpen] = useState(false);
|
||||
const [associationType, setAssociationType] = useState('existing');
|
||||
const [selectedMeetingId, setSelectedMeetingId] = useState('');
|
||||
const [newMeetingTopic, setNewMeetingTopic] = useState('');
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDropdownData = async () => {
|
||||
try {
|
||||
const meetingsRes = await getMeetings();
|
||||
setMeetings(meetingsRes.data);
|
||||
if (meetingsRes.data.length > 0) {
|
||||
setSelectedMeetingId(meetingsRes.data[0].id);
|
||||
}
|
||||
} catch (err) { console.error('Could not fetch meetings for dropdown.'); }
|
||||
};
|
||||
fetchDropdownData();
|
||||
}, []);
|
||||
|
||||
const handleInitiateSave = () => {
|
||||
if (!actionItems || !Array.isArray(actionItems) || actionItems.length === 0) {
|
||||
setError('No valid action items to save.');
|
||||
return;
|
||||
}
|
||||
setError('');
|
||||
setIsMeetingDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleConfirmSave = async () => {
|
||||
let meetingIdToSave = selectedMeetingId;
|
||||
if (associationType === 'new') {
|
||||
if (!newMeetingTopic) { setError('New meeting topic is required.'); return; }
|
||||
try {
|
||||
const { data: newMeeting } = await createMeeting(newMeetingTopic, new Date().toISOString());
|
||||
meetingIdToSave = newMeeting.id;
|
||||
} catch (err) { setError('Failed to create new meeting.'); return; }
|
||||
}
|
||||
|
||||
if (!meetingIdToSave) { setError('A meeting must be selected or created.'); return; }
|
||||
|
||||
setSaveLoading(true); setError('');
|
||||
try {
|
||||
const itemsToSave = actionItems.map(({ tempId, owner, duedate, ...rest }) => rest);
|
||||
await batchCreateActionItems(meetingIdToSave, itemsToSave);
|
||||
setIsMeetingDialogOpen(false);
|
||||
alert('Action Items saved successfully!');
|
||||
// Optionally, clear items after save by calling a prop function from parent
|
||||
} catch (err) { setError(err.response?.data?.error || 'Failed to save action items.'); }
|
||||
finally { setSaveLoading(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||
<Box sx={{display: 'flex', gap: 2, mb: 2}}>
|
||||
<Button onClick={onGenerateSummary} disabled={!textContent} variant="outlined">Generate Summary</Button>
|
||||
<Button onClick={onPreviewActions} disabled={!textContent} variant="outlined">Generate Action Items</Button>
|
||||
</Box>
|
||||
|
||||
{summary && <Paper elevation={2} sx={{ p: 2, mb: 2 }}><Typography variant="h6">Summary</Typography><TextField fullWidth multiline rows={6} value={summary} variant="outlined" sx={{mt:1}}/></Paper>}
|
||||
|
||||
{actionItems && actionItems.length > 0 && (
|
||||
<Paper elevation={2} sx={{ p: 2 }}>
|
||||
<Typography variant="h6">Review and Edit Action Items</Typography>
|
||||
<TableContainer component={Paper} sx={{ mt: 2 }}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow><TableCell>Context</TableCell><TableCell>Action</TableCell><TableCell>Owner</TableCell><TableCell>Due Date</TableCell></TableRow></TableHead>
|
||||
<TableBody>{actionItems.map(item => (
|
||||
<TableRow key={item.tempId}>
|
||||
<TableCell><TextField variant="standard" fullWidth value={item.item || ''} onChange={e => onActionItemChange(item.tempId, 'item', e.target.value)}/></TableCell>
|
||||
<TableCell><TextField variant="standard" fullWidth value={item.action || ''} onChange={e => onActionItemChange(item.tempId, 'action', e.target.value)}/></TableCell>
|
||||
<TableCell><TextField variant="standard" fullWidth value={item.owner || ''} onChange={e => onActionItemChange(item.tempId, 'owner', e.target.value)}/></TableCell>
|
||||
<TableCell><TextField variant="standard" type="date" fullWidth value={item.due_date || ''} onChange={e => onActionItemChange(item.tempId, 'due_date', e.target.value)} InputLabelProps={{ shrink: true }}/></TableCell>
|
||||
</TableRow>
|
||||
))}</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Box mt={2} display="flex" justifyContent="flex-end">
|
||||
<Button onClick={handleInitiateSave} disabled={saveLoading} variant="contained" color="primary">Save All Action Items</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
<Dialog open={isMeetingDialogOpen} onClose={() => setIsMeetingDialogOpen(false)} fullWidth maxWidth="xs">
|
||||
<DialogTitle>Associate with a Meeting</DialogTitle>
|
||||
<DialogContent>
|
||||
<FormControl component="fieldset" sx={{mt:1}}><Select size="small" value={associationType} onChange={e => setAssociationType(e.target.value)}><MenuItem value="existing">Existing Meeting</MenuItem><MenuItem value="new">New Meeting</MenuItem></Select></FormControl>
|
||||
{associationType === 'existing' ? <FormControl fullWidth sx={{mt:2}}><InputLabel>Select Meeting</InputLabel><Select value={selectedMeetingId} label="Select Meeting" onChange={e => setSelectedMeetingId(e.target.value)}>{meetings.map(m => <MenuItem key={m.id} value={m.id}>{m.topic}</MenuItem>)}</Select></FormControl> : <TextField label="New Meeting Topic" fullWidth sx={{mt:2}} value={newMeetingTopic} onChange={e => setNewMeetingTopic(e.target.value)} />}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setIsMeetingDialogOpen(false)}>Cancel</Button>
|
||||
<Button onClick={handleConfirmSave} variant="contained" disabled={saveLoading}>{saveLoading ? <CircularProgress size={24}/> : 'Confirm & Save'}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextProcessingTools;
|
||||
Reference in New Issue
Block a user