This commit is contained in:
beabigegg
2025-08-17 15:26:44 +08:00
commit 0fee703b84
60 changed files with 8042 additions and 0 deletions

View 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;

View 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;

View 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;