Compare commits

...

2 Commits

Author SHA1 Message Date
mula.liu eb035d6d35 1.0.3 2025-09-09 11:36:26 +08:00
mula.liu df96e75e7f v1.0.2 2025-09-03 18:10:52 +08:00
14 changed files with 669 additions and 4775 deletions

BIN
.DS_Store vendored 100644

Binary file not shown.

8
.dockerignore 100644
View File

@ -0,0 +1,8 @@
node_modules
npm-debug.log
.git
.gitignore
README.md
Dockerfile
.dockerignore
docker-compose.yml

32
Dockerfile 100644
View File

@ -0,0 +1,32 @@
# 生产环境服务器 - PM2版本
FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/node:18-alpine
# 设置工作目录
WORKDIR /app
# 复制构建产物
COPY dist ./dist
# 复制服务器文件和PM2配置
COPY server.js .
COPY ecosystem.config.json .
# 复制生产环境package.json
COPY package.prod.json package.json
# 安装生产依赖和PM2
RUN npm install --production && \
npm install -g pm2
# 创建日志目录
RUN mkdir -p logs
# 暴露端口
EXPOSE 3001
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:3001/health || exit 1
# 启动PM2
CMD ["pm2-runtime", "start", "ecosystem.config.json"]

69
deploy-prod.sh 100755
View File

@ -0,0 +1,69 @@
#!/bin/bash
echo "🚀 开始部署iMeeting前端服务PM2模式..."
# 检查node_modules
if [ ! -d "node_modules" ]; then
echo "📦 安装依赖..."
yarn install
fi
# 构建前端
echo "🔨 构建前端应用..."
yarn build
# 检查构建是否成功
if [ $? -ne 0 ]; then
echo "❌ 构建失败!请检查代码错误"
exit 1
fi
if [ ! -d "dist" ]; then
echo "❌ 构建失败dist目录未生成"
exit 1
fi
echo "✅ 前端构建完成开始Docker部署..."
# 创建日志目录
mkdir -p logs
# 停止并删除现有容器
echo "📦 停止现有容器..."
docker-compose -f docker-compose.prod.yml down
# 构建新镜像
echo "🔨 构建Docker镜像..."
docker-compose -f docker-compose.prod.yml build --no-cache
# 启动服务
echo "▶️ 启动PM2服务..."
docker-compose -f docker-compose.prod.yml up -d
# 检查服务状态
echo "🔍 检查服务状态..."
sleep 15
docker-compose -f docker-compose.prod.yml ps
# 检查PM2进程状态
echo "🔄 检查PM2进程状态..."
docker exec imeeting-frontend pm2 list
# 检查健康状态
echo "🏥 检查健康状态..."
curl -f http://localhost:3001/health && echo "✅ 前端服务健康检查通过" || echo "❌ 前端服务健康检查失败"
echo ""
echo "🎉 部署完成!"
echo "📱 前端服务访问地址: http://localhost:3001"
echo "📊 查看日志: docker-compose -f docker-compose.prod.yml logs -f"
echo "📈 查看PM2状态: docker exec imeeting-frontend pm2 monit"
echo "📋 查看PM2进程: docker exec imeeting-frontend pm2 list"
echo "🛑 停止服务: docker-compose -f docker-compose.prod.yml down"
echo ""
echo "💡 提示PM2模式特性"
echo " ✅ 集群模式2个实例"
echo " ✅ 自动重启和故障恢复"
echo " ✅ 内存限制保护1GB"
echo " ✅ 详细日志管理"
echo " ✅ 进程监控和健康检查"

View File

@ -0,0 +1,21 @@
services:
imeeting-frontend:
build:
context: .
dockerfile: Dockerfile
container_name: imeeting-frontend
ports:
- "3001:3001"
environment:
- NODE_ENV=production
- PORT=3001
- BACKEND_URL=http://host.docker.internal:8001
volumes:
# 挂载日志目录到宿主机
- ./logs:/app/logs
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3001/health", "||", "exit", "1"]
interval: 30s
timeout: 10s
retries: 3

View File

@ -0,0 +1,21 @@
{
"name": "imeeting-frontend",
"script": "server.js",
"instances": 2,
"exec_mode": "cluster",
"env": {
"NODE_ENV": "production",
"PORT": 3001
},
"error_file": "./logs/err.log",
"out_file": "./logs/out.log",
"log_file": "./logs/combined.log",
"time": true,
"log_date_format": "YYYY-MM-DD HH:mm:ss Z",
"max_memory_restart": "1G",
"restart_delay": 4000,
"autorestart": true,
"watch": false,
"max_restarts": 10,
"min_uptime": "10s"
}

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>会议助手</title>
<title>会议</title>
</head>
<body>
<div id="root"></div>

4665
package-lock.json generated

File diff suppressed because it is too large Load Diff

13
package.prod.json 100644
View File

@ -0,0 +1,13 @@
{
"name": "imeeting-frontend-server",
"version": "1.0.0",
"type": "commonjs",
"main": "server.js",
"dependencies": {
"express": "4.18.2",
"http-proxy-middleware": "2.0.6"
},
"engines": {
"node": ">=18.0.0"
}
}

55
server.js 100644
View File

@ -0,0 +1,55 @@
const express = require("express");
const { createProxyMiddleware } = require("http-proxy-middleware");
const path = require("path");
const app = express();
const PORT = process.env.PORT;
const BACKEND_URL = process.env.BACKEND_URL || "http://localhost:8000";
// 健康检查
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// API代理 - 需要在body parser之前
app.use("/api", createProxyMiddleware({
target: BACKEND_URL,
changeOrigin: true,
timeout: 30000,
proxyTimeout: 30000
}));
// 上传文件代理
app.use("/uploads", createProxyMiddleware({
target: BACKEND_URL,
changeOrigin: true,
timeout: 30000,
proxyTimeout: 30000
}));
// 设置请求体大小限制 - 放在代理之后
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true }));
// 静态文件服务
app.use(express.static(path.join(__dirname, "dist"), {
maxAge: '1d',
etag: true
}));
// SPA路由处理 - 必须放在最后
app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "dist", "index.html"));
});
// 错误处理中间件
app.use((err, req, res, next) => {
console.error('Server error:', err);
res.status(500).json({ error: 'Internal server error' });
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`iMeeting Frontend Server running on port ${PORT}`);
console.log(`Backend proxy target: ${BACKEND_URL}`);
console.log(`Environment: ${process.env.NODE_ENV}`);
});

View File

@ -160,9 +160,8 @@ const HomePage = ({ onLogin }) => {
</form>
<div className="demo-info">
<p>测试账号</p>
<p>用户名: user1, 密码: hashed_password_1</p>
<p>用户名: user2, 密码: hashed_password_2</p>
<p>开通账号</p>
<p>请联系18980500203</p>
</div>
</div>
</div>

View File

@ -1612,7 +1612,6 @@
min-height: 80px;
border: 1px solid #d1d5db;
border-radius: 6px;
padding: 12px;
font-family: inherit;
font-size: 14px;
line-height: 1.5;
@ -1677,3 +1676,144 @@
font-style: italic;
text-align: center;
}
/* AI总结进度条样式 */
.summary-progress-section {
margin: 20px 0;
padding: 20px;
background: #f8fafc;
border-radius: 12px;
border: 1px solid #e5e7eb;
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.progress-title {
font-size: 14px;
font-weight: 500;
color: #374151;
}
.progress-percentage {
font-size: 14px;
font-weight: 600;
color: #2563eb;
}
.progress-bar-container {
height: 8px;
background: #e5e7eb;
border-radius: 4px;
overflow: hidden;
position: relative;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6 0%, #2563eb 100%);
border-radius: 4px;
transition: width 0.3s ease;
position: relative;
overflow: hidden;
}
.progress-bar-animate {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.3) 50%,
rgba(255, 255, 255, 0) 100%
);
animation: progress-shine 1.5s linear infinite;
}
@keyframes progress-shine {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
.progress-message {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
font-size: 13px;
color: #6b7280;
}
.progress-message .status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
animation: pulse 1.5s ease-in-out infinite;
}
.progress-message .status-indicator.processing {
background: #3b82f6;
}
.success-message {
display: flex;
align-items: center;
gap: 8px;
margin: 20px 0;
padding: 12px 16px;
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border-left: 4px solid #3b82f6;
border-radius: 8px;
font-size: 14px;
color: #1e40af;
animation: slideInFromTop 0.3s ease-out;
}
.success-message svg {
color: #3b82f6;
animation: sparkle 0.5s ease-in-out;
}
@keyframes slideInFromTop {
0% {
opacity: 0;
transform: translateY(-10px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes sparkle {
0%, 100% {
transform: scale(1) rotate(0deg);
}
50% {
transform: scale(1.2) rotate(180deg);
}
}
/* 更新生成按钮在加载时的样式 */
.generate-summary-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
background: #94a3b8;
}
.generate-summary-btn:disabled:hover {
transform: none;
box-shadow: none;
}

View File

@ -42,6 +42,11 @@ const MeetingDetails = ({ user }) => {
const [userPrompt, setUserPrompt] = useState('');
const [summaryHistory, setSummaryHistory] = useState([]);
const [currentHighlightIndex, setCurrentHighlightIndex] = useState(-1);
const [summaryTaskId, setSummaryTaskId] = useState(null);
const [summaryTaskStatus, setSummaryTaskStatus] = useState(null);
const [summaryTaskProgress, setSummaryTaskProgress] = useState(0);
const [summaryTaskMessage, setSummaryTaskMessage] = useState('');
const [summaryPollInterval, setSummaryPollInterval] = useState(null);
const audioRef = useRef(null);
const transcriptRefs = useRef([]);
@ -55,6 +60,11 @@ const MeetingDetails = ({ user }) => {
clearInterval(statusCheckInterval);
setStatusCheckInterval(null);
}
if (summaryPollInterval) {
console.log('组件卸载,清理总结任务轮询定时器');
clearInterval(summaryPollInterval);
setSummaryPollInterval(null);
}
};
}, [meeting_id]);
@ -260,9 +270,17 @@ const MeetingDetails = ({ user }) => {
};
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const hours = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
if (hours > 0) {
// 60::
return `${hours}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
} else {
// 60:
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
};
const handlePlayPause = () => {
@ -536,40 +554,147 @@ const MeetingDetails = ({ user }) => {
}
};
// AI
// AI - 使API
const generateSummary = async () => {
if (summaryLoading) return;
setSummaryLoading(true);
setSummaryTaskProgress(0);
setSummaryTaskMessage('正在启动AI分析...');
setSummaryTaskStatus('pending');
try {
const baseUrl = "";
const response = await apiClient.post(`${baseUrl}/api/meetings/${meeting_id}/generate-summary`, {
// 使API
const response = await apiClient.post(`${baseUrl}/api/meetings/${meeting_id}/generate-summary-async`, {
user_prompt: userPrompt
});
setSummaryResult(response.data);
const taskId = response.data.task_id;
setSummaryTaskId(taskId);
//
await fetchSummaryHistory();
//
const interval = setInterval(async () => {
try {
const statusResponse = await apiClient.get(`${baseUrl}/api/llm-tasks/${taskId}/status`);
const status = statusResponse.data;
setSummaryTaskStatus(status.status);
setSummaryTaskProgress(status.progress || 0);
setSummaryTaskMessage(status.message || '处理中...');
if (status.status === 'completed') {
clearInterval(interval);
setSummaryPollInterval(null);
//
setSummaryResult({
content: status.result,
task_id: taskId
});
//
await fetchSummaryHistory();
//
await refreshMeetingSummary();
setSummaryLoading(false);
setSummaryTaskMessage('AI总结生成成功');
// 3
setTimeout(() => {
setSummaryTaskMessage('');
setSummaryTaskProgress(0);
}, 3000);
} else if (status.status === 'failed') {
clearInterval(interval);
setSummaryPollInterval(null);
setSummaryLoading(false);
setError(status.error_message || '生成AI总结失败');
setSummaryTaskMessage('生成失败:' + (status.error_message || '未知错误'));
}
} catch (err) {
console.error('Error polling task status:', err);
//
}
}, 3000); // 3
//
await refreshMeetingSummary();
setSummaryPollInterval(interval);
} catch (err) {
console.error('Error generating summary:', err);
setError('生成AI总结失败请重试');
} finally {
console.error('Error starting summary generation:', err);
setError('启动AI总结失败请重试');
setSummaryLoading(false);
setSummaryTaskMessage('');
setSummaryTaskProgress(0);
}
};
const fetchSummaryHistory = async () => {
try {
const baseUrl = "";
const response = await apiClient.get(`${baseUrl}/api/meetings/${meeting_id}/summaries`);
setSummaryHistory(response.data.summaries);
// LLM
const tasksResponse = await apiClient.get(`${baseUrl}/api/meetings/${meeting_id}/llm-tasks`);
const tasks = tasksResponse.data.tasks || [];
//
const summaries = tasks
.filter(task => task.status === 'completed' && task.result)
.map(task => ({
id: task.task_id,
content: task.result,
user_prompt: task.user_prompt,
created_at: task.created_at,
task_info: {
task_id: task.task_id,
status: task.status,
progress: task.progress
}
}));
setSummaryHistory(summaries);
//
const runningTask = tasks.find(task => ['pending', 'processing'].includes(task.status));
if (runningTask && !summaryPollInterval) {
setSummaryTaskId(runningTask.task_id);
setSummaryTaskStatus(runningTask.status);
setSummaryTaskProgress(runningTask.progress || 0);
setSummaryLoading(true);
//
const interval = setInterval(async () => {
try {
const statusResponse = await apiClient.get(`${baseUrl}/api/llm-tasks/${runningTask.task_id}/status`);
const status = statusResponse.data;
setSummaryTaskStatus(status.status);
setSummaryTaskProgress(status.progress || 0);
setSummaryTaskMessage(status.message || '处理中...');
if (['completed', 'failed'].includes(status.status)) {
clearInterval(interval);
setSummaryPollInterval(null);
setSummaryLoading(false);
if (status.status === 'completed') {
await fetchSummaryHistory();
await refreshMeetingSummary();
}
}
} catch (err) {
console.error('Error polling task status:', err);
}
}, 3000);
setSummaryPollInterval(interval);
}
} catch (err) {
console.error('Error fetching summary history:', err);
setSummaryHistory([]);
}
};
@ -600,19 +725,14 @@ const MeetingDetails = ({ user }) => {
return;
}
// ReactMarkdown
// Markdown
const tempDiv = document.createElement('div');
tempDiv.style.position = 'fixed';
tempDiv.style.top = '-9999px';
tempDiv.style.width = '800px';
tempDiv.style.padding = '20px';
tempDiv.style.backgroundColor = 'white';
tempDiv.style.display = 'none';
document.body.appendChild(tempDiv);
// markdown-to-html
const ReactMarkdown = (await import('react-markdown')).default;
const { createRoot } = await import('react-dom/client');
document.body.appendChild(tempDiv);
const root = createRoot(tempDiv);
// MarkdownHTML
@ -624,101 +744,151 @@ const MeetingDetails = ({ user }) => {
children: summaryContent
})
);
setTimeout(resolve, 100); //
setTimeout(resolve, 100);
});
const renderedHTML = tempDiv.innerHTML;
// HTMLPDF
const printContainer = document.createElement('div');
printContainer.style.position = 'fixed';
printContainer.style.top = '-9999px';
printContainer.style.width = '210mm';
printContainer.style.padding = '20mm';
printContainer.style.backgroundColor = 'white';
printContainer.style.fontFamily = 'Arial, sans-serif';
printContainer.style.fontSize = '14px';
printContainer.style.lineHeight = '1.6';
printContainer.style.color = '#333';
// PDFHTML使Markdown
//
const meetingTime = formatDateTime(meeting.meeting_time);
const attendeesList = meeting.attendees.map(attendee =>
typeof attendee === 'string' ? attendee : attendee.caption
).join('、');
printContainer.innerHTML = `
<div>
<h1 style="color: #2563eb; margin-bottom: 30px; font-size: 24px; border-bottom: 2px solid #e5e7eb; padding-bottom: 10px;">
${meeting.title || '会议总结'}
</h1>
// iframe
const printFrame = document.createElement('iframe');
printFrame.style.position = 'fixed';
printFrame.style.width = '0';
printFrame.style.height = '0';
printFrame.style.border = 'none';
printFrame.style.left = '-9999px';
document.body.appendChild(printFrame);
const printDocument = printFrame.contentWindow.document;
// 使BlobURL
const htmlContent = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>${meeting.title || '会议总结'}</title>
<style>
@charset "UTF-8";
@page { size: A4; margin: 20mm; }
body {
font-family: "PingFang SC", "Microsoft YaHei", "Hiragino Sans GB", "Heiti SC", "WenQuanYi Micro Hei", sans-serif;
font-size: 14px;
line-height: 1.6;
color: #333;
margin: 0;
padding: 20px;
}
h1 {
color: #2563eb;
font-size: 24px;
margin-bottom: 30px;
border-bottom: 2px solid #e5e7eb;
padding-bottom: 10px;
}
h2 {
color: #374151;
font-size: 18px;
margin: 25px 0 15px;
}
h3 {
color: #1e293b;
font-size: 16px;
margin: 20px 0 10px;
}
p {
margin: 10px 0;
color: #475569;
}
ul, ol {
margin: 10px 0;
padding-left: 25px;
}
li {
margin: 5px 0;
color: #475569;
}
strong {
color: #1e293b;
font-weight: 600;
}
.info-section {
background: #f9fafb;
padding: 20px;
margin-bottom: 30px;
border-radius: 8px;
}
.info-section h2 {
margin-top: 0;
}
.content-section {
margin-bottom: 30px;
}
.footer-section {
margin-top: 50px;
padding-top: 20px;
border-top: 1px solid #e5e7eb;
font-size: 12px;
color: #6b7280;
}
@media print {
body { padding: 0; }
h1 { page-break-before: avoid; }
h2 { page-break-before: avoid; }
}
</style>
</head>
<body>
<h1>${meeting.title || '会议总结'}</h1>
<div class="info-section">
<h2>会议信息</h2>
<p><strong>会议时间</strong>${meetingTime}</p>
<p><strong>创建人</strong>${meeting.creator_username}</p>
<p><strong>参会人数</strong>${meeting.attendees.length}</p>
<p><strong>参会人员</strong>${attendeesList}</p>
</div>
<div class="content-section">
<h2>会议摘要</h2>
${renderedHTML}
</div>
<div class="footer-section">
<p>导出时间${new Date().toLocaleString('zh-CN')}</p>
</div>
</body>
</html>`;
// 使BlobURL
const blob = new Blob([htmlContent], { type: 'text/html; charset=UTF-8' });
const url = URL.createObjectURL(blob);
// iframesrcblob URL
printFrame.src = url;
// iframe
printFrame.onload = () => {
setTimeout(() => {
//
printFrame.contentWindow.focus();
printFrame.contentWindow.print();
<div style="margin-bottom: 30px; background: #f9fafb; padding: 20px; border-radius: 8px;">
<h2 style="color: #374151; font-size: 16px; margin-bottom: 15px;">会议信息</h2>
<p style="margin: 8px 0;"><strong>会议时间</strong>${meetingTime}</p>
<p style="margin: 8px 0;"><strong>创建人</strong>${meeting.creator_username}</p>
<p style="margin: 8px 0;"><strong>参会人数</strong>${meeting.attendees.length}</p>
<p style="margin: 8px 0;"><strong>参会人员</strong>${attendeesList}</p>
</div>
<div style="margin-bottom: 30px;">
<h2 style="color: #374151; font-size: 16px; margin-bottom: 15px;">会议摘要</h2>
<div style="line-height: 1.8;">${renderedHTML}</div>
</div>
<div style="margin-top: 50px; padding-top: 20px; border-top: 1px solid #e5e7eb; font-size: 12px; color: #6b7280;">
<p>导出时间${new Date().toLocaleString('zh-CN')}</p>
</div>
</div>
`;
document.body.appendChild(printContainer);
// 使PDF
const originalContent = document.body.innerHTML;
const originalTitle = document.title;
//
document.body.innerHTML = printContainer.innerHTML;
document.title = `${meeting.title || '会议总结'}_${new Date().toISOString().split('T')[0]}`;
//
const printStyles = document.createElement('style');
printStyles.innerHTML = `
@media print {
body { margin: 0; padding: 20px; font-family: 'Microsoft YaHei', Arial, sans-serif; }
h1 { page-break-before: avoid; }
h2 { page-break-before: avoid; }
h3 { margin-top: 1.5rem; margin-bottom: 0.75rem; color: #1e293b; }
h4 { margin-top: 1rem; margin-bottom: 0.5rem; color: #1e293b; }
p { margin-bottom: 0.75rem; color: #475569; line-height: 1.6; }
ul, ol { margin: 0.75rem 0; padding-left: 1.5rem; }
li { margin-bottom: 0.25rem; color: #475569; }
strong { color: #1e293b; font-weight: 600; }
code { background: #f1f5f9; padding: 2px 4px; border-radius: 3px; color: #dc2626; }
.page-break { page-break-before: always; }
}
`;
document.head.appendChild(printStyles);
//
window.print();
//
setTimeout(() => {
document.body.innerHTML = originalContent;
document.title = originalTitle;
document.head.removeChild(printStyles);
document.body.removeChild(printContainer);
document.body.removeChild(tempDiv);
// React
window.location.reload();
}, 1000);
//
setTimeout(() => {
URL.revokeObjectURL(url);
root.unmount();
document.body.removeChild(tempDiv);
document.body.removeChild(printFrame);
}, 2000);
}, 500);
};
} catch (error) {
console.error('PDF导出失败:', error);
alert('PDF导出失败请重试。建议使用浏览器的打印功能并选择"保存为PDF"。');
alert('PDF导出失败请重试。');
}
};
@ -1264,6 +1434,38 @@ const MeetingDetails = ({ user }) => {
</button>
</div>
{/* 任务进度显示 */}
{summaryLoading && (
<div className="summary-progress-section">
<div className="progress-header">
<span className="progress-title">生成进度</span>
<span className="progress-percentage">{summaryTaskProgress}%</span>
</div>
<div className="progress-bar-container">
<div
className="progress-bar-fill"
style={{ width: `${summaryTaskProgress}%` }}
>
<div className="progress-bar-animate"></div>
</div>
</div>
{summaryTaskMessage && (
<div className="progress-message">
<div className="status-indicator processing"></div>
<span>{summaryTaskMessage}</span>
</div>
)}
</div>
)}
{/* 成功消息 */}
{!summaryLoading && summaryTaskMessage && summaryTaskStatus === 'completed' && (
<div className="success-message">
<Sparkles size={16} />
<span>{summaryTaskMessage}</span>
</div>
)}
{summaryResult && (
<div className="summary-result-section">
<div className="summary-result-header">

View File

@ -6,7 +6,6 @@ export default defineConfig({
plugins: [react()],
server: {
host: true, // Optional: Allows the server to be accessible externally
port: 5173, // Optional: Specify a port if needed
allowedHosts: ['imeeting.unisspace.com'], // Add the problematic hostname here
proxy: {
'/api': {