import React, { useState, useEffect, useRef } from 'react';
import {
Search,
MessageSquare,
FileText,
Clock,
ChevronRight,
ChevronDown,
Send,
BookOpen,
HelpCircle,
Presentation,
Briefcase,
FileSignature,
CreditCard,
AlertCircle,
CheckCircle2,
Sparkles,
ArrowDown,
Download,
File,
FileBarChart2,
ListTodo,
X,
Loader2
} from 'lucide-react';
// --- 配置区域 ---
const apiKey = ""; // Gemini API Key will be injected by the environment
// --- 数据源 (保留原有数据作为 AI 的上下文) ---
// 1. 知识库数据
const KNOWLEDGE_BASE = [
{
keywords: ["code", "申请", "project code", "材料"],
answer: "申请 Project Code 一般分为常规 Code 和 Special Code。常规 Code 需要提交:1. Code Request Form;2. Budget Plan。如果是 Special Code,还需要额外增加 Lob Leader 的批复。如果没有模板,请在建议书苑查找或询问中台同事。",
source: "常见问题.pdf"
},
{
keywords: ["时间", "多久", "时长", "code"],
answer: "申请 Code 的时间取决于客户类型:\n1. 现有客户(有 Customer Code):通常 1-2 天。\n2. 新客户或更名客户:需要先走 KYC 流程和申请 Customer Code,通常需要 1-2 周。",
source: "常见问题.pdf"
},
{
keywords: ["开票", "流程", "发票", "税票"],
answer: "开票流程如下:\n1. 向中台申请 FN (Fee Note);\n2. 联系客户获得确认开票的邮件;\n3. 财务开出税票后,将税票发送给客户(抄送中台)。\n注意:Fee Note 申请通常需要 2-3 天。",
source: "常见问题.pdf"
},
{
keywords: ["盖章", "合同", "用印"],
answer: "合同盖章申请材料包括:\n1. 最终版 PDF 文件;\n2. 所有审批邮件(报价批复+法务批复必选);\n3. 《合同盖章申请表》。\n如果是法人签,需中台审批后找 Wise 或 Raymond 签字。",
source: "常见问题.pdf"
},
{
keywords: ["投标", "保证金", "流程"],
answer: "投标保证金流程:需填写申请模板,写清英文邮件正文给 July Mao。如果涉及合同条款,必须先提供法务审核意见。请务必预留充足时间。",
source: "2025项目管理.pptx"
},
{
keywords: ["供应商", "付款", "vendor"],
answer: "2026新规(1月1日起执行):\n1. 必须收到客户服务费后,原则上才能支付供应商费用;\n2. 支付比例不可一次性支付;\n3. 支付需 PD 和 Lob Leader 审批;\n4. 必须在供应商合同有效期内启动支付。",
source: "项目财务管理--20260106.pptx"
},
{
keywords: ["2026", "新规", "财务"],
answer: "2026年1月1日起执行的新规重点:\n1. Fee note 要和正式税票同时开出;\n2. 当年开具的税票争取当年追回 AR;\n3. Special code 合同尽量在当年 12.31 前签署回来。",
source: "项目财务管理--20260106.pptx"
}
];
// 2. FAQ 数据
const FAQ_DATA = [
{
category: "Project Code 相关",
questions: [
{ q: "申请 Project code 需要什么材料?", a: "常规 code 申请需要: 1. Code Request Form, 2. Budget Plan。\nSpecial Code 则需要在常规 code 基础上增加 Lob Leader 的批复。" },
{ q: "申请 code 需要多长时间?", a: "情况一(老客户):通常 1-2 天。\n情况二(新客户/更名):需要 KYC 及 Customer Code,通常 1-2 周。" }
]
},
{
category: "开票与财务",
questions: [
{ q: "每月可开票的时间是?", a: "系统一般在每月第三个工作日之后开启。Billing Deadline 一般为系统开启后一周。具体时间留意中台每月邮件。" },
{ q: "开票有哪些注意事项?", a: "注意每月具体时间;申请 FN 前确定好合同金额;如需拆分需提前沟通;客户确认邮件需符合规范。" }
]
},
{
category: "合同盖章",
questions: [
{ q: "合同盖章申请需要哪些材料?", a: "1. 最终版 PDF 文件;\n2. 审批邮件(报价+法务);\n3. 《合同盖章申请表》。" },
{ q: "合同需要法人/代表签字怎么办?", a: "如需 Raymond 或 Wise 签字,请在获取中台盖章审批后,向对应领导申请电子签,并抄送 Angela Han 用印。" }
]
}
];
// 3. PPT 时间轴数据 (按时间正序存储,渲染时倒序)
const TIMELINE_SLIDES = [
{
id: 1,
phase: "BD Phase",
title: "投标与招标文件",
date: "July 2025",
color: "bg-blue-500",
icon: ,
content: [
{ type: "subtitle", text: "招标文件获取" },
{ type: "list", items: ["线上购买:确认 CA 证书有效期,联系 July Mao。", "线下购买:准备法人授权书、身份证复印件。"] },
{ type: "subtitle", text: "关键授权文件审批" },
{ type: "table", rows: [
["公司授权书", "Raymond Lu -> Wise Xu (CC Angela)"],
["业绩证明", "Raymond Lu -> July Mao 提供"],
["资信/社保", "发给 July Mao 协助获取 (提前2-3天)"]
]}
]
},
{
id: 2,
phase: "Contracting Phase",
title: "报价审批与 Budget Plan",
date: "2025 Standard",
color: "bg-indigo-500",
icon: ,
content: [
{ type: "text", text: "《Budget Plan》是报价计算工具,也是项目资源管理依据。" },
{ type: "subtitle", text: "报价审批流程" },
{ type: "list", items: [
"投标报价审批:发给 BD 成员、Resource Manager (Sindy)、中台 (Louisa/July)。",
"合同报价审批:包含项目总报价、付款比例、交付内容。",
"首/尾款比例:尾款比例不得高于 10%,否则需特别审批。"
]}
]
},
{
id: 3,
phase: "Project Delivery",
title: "Project Code 申请体系",
date: "Ongoing",
color: "bg-purple-500",
icon: ,
content: [
{ type: "subtitle", text: "Code 类型判断" },
{ type: "list", items: [
"常规 Code (有合同):Code Request Form + Budget Plan -> 系统申请",
"Special Code (无合同):上述材料 + Lob Leader 批复 -> 提交 Ticket"
]},
{ type: "subtitle", text: "Mavenlink ID" },
{ type: "text", text: "创建 Project Name 格式:客户名称-项目名称。生成 4 开头的 8 位 ID。" }
]
},
{
id: 4,
phase: "Finance Rules",
title: "财务管理新规 (重点)",
date: "Effective Jan 1, 2026",
tag: "NEW",
color: "bg-red-500",
icon: ,
content: [
{ type: "subtitle", text: "AR 与 Special Code 管理" },
{ type: "list", items: [
"Fee note 需与正式税票同时开出。",
"Special code 合同必须在当年 12 月 31 日前签署拿回。",
"跨年 AR 需 PD 准备 Business Justification。"
]},
{ type: "subtitle", text: "供应商管理 (严控)" },
{ type: "list", items: [
"原则:收到客户款项后,再支付供应商。",
"流程:PD Approval -> Lob Leader Approval。",
"禁止:一次性支付供应商费用。"
]}
]
}
];
// 4. 下载中心文件数据
const DOWNLOAD_RESOURCES = [
{
id: 1,
name: "项目财务管理--20260106.pptx",
type: "PPT",
size: "2.4 MB",
date: "2026-01-06",
desc: "2026年最新财务新规,包含 Special Code 管理及供应商付款流程变更。",
color: "text-red-600",
bgColor: "bg-red-50"
},
{
id: 2,
name: "2025项目管理.pptx",
type: "PPT",
size: "5.8 MB",
date: "2025-07-01",
desc: "项目全生命周期管理指引,涵盖 BD、合同及交付阶段标准动作。",
color: "text-orange-600",
bgColor: "bg-orange-50"
},
{
id: 3,
name: "常见问题.pdf",
type: "PDF",
size: "1.2 MB",
date: "2025-07-15",
desc: "Project Code 申请、开票、盖章等日常高频问题解答。",
color: "text-blue-600",
bgColor: "bg-blue-50"
}
];
// --- Gemini API 集成逻辑 ---
// 1. 构建全量知识库上下文字符串
const MANUAL_CONTEXT = JSON.stringify({
knowledge_base: KNOWLEDGE_BASE,
faq: FAQ_DATA,
timeline_rules: TIMELINE_SLIDES
}, null, 2);
// 2. API 调用函数
const callGemini = async (prompt, systemInstruction = "") => {
try {
const response = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
systemInstruction: { parts: [{ text: systemInstruction }] },
}),
}
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
let text = data.candidates?.[0]?.content?.parts?.[0]?.text || "抱歉,我暂时无法回答这个问题。";
// 清理 markdown 代码块标记,以便直接渲染 HTML
text = text.replace(/^```html\s*/, '').replace(/```$/, '');
return text;
} catch (error) {
console.error("Gemini API Call Failed:", error);
return "
AI 服务暂时不可用,请稍后再试。
";
}
};
// --- Modal Component (用于展示 AI 生成的内容) ---
const AIModal = ({ isOpen, onClose, title, content, isLoading }) => {
if (!isOpen) return null;
return (
{title}
{isLoading ? (
) : (
// 使用 dangerouslySetInnerHTML 渲染 AI 生成的 HTML 结构
)}
);
};
// --- 组件部分 ---
const SlideCard = ({ slide, onGenerateChecklist }) => (
{slide.icon}
{slide.title}
{slide.phase}
{slide.tag &&
{slide.tag}}
{slide.date}
{slide.content.map((block, idx) => {
if (block.type === 'subtitle') return
{block.text}
;
if (block.type === 'text') return
{block.text}
;
if (block.type === 'list') return (
{block.items.map((item, i) => - {item}
)}
);
if (block.type === 'table') return (
{block.rows.map((row, rIdx) => (
| {row[0]} |
{row[1]} |
))}
);
return null;
})}
{/* AI 功能按钮 */}
);
const ChatMessage = ({ msg }) => (
{msg.isBot && (
)}
{msg.text}
);
const FAQItem = ({ q, a }) => {
const [isOpen, setIsOpen] = useState(false);
return (
{isOpen && (
{a}
)}
);
};
const ResourceCard = ({ resource }) => (
{resource.type === 'PPT' ? (
) : (
)}
{resource.name}
{resource.type}
{resource.desc}
{resource.size}
•
更新于 {resource.date}
);
export default function ProjectManualApp() {
const [activeTab, setActiveTab] = useState('chat');
const [input, setInput] = useState('');
const [isTyping, setIsTyping] = useState(false);
// AI Modal State
const [modalOpen, setModalOpen] = useState(false);
const [modalTitle, setModalTitle] = useState('');
const [modalContent, setModalContent] = useState('');
const [isModalLoading, setIsModalLoading] = useState(false);
const [chatHistory, setChatHistory] = useState([
{ id: 1, isBot: true, text: "你好!我是升级版的项目管理 AI 助手。\n\n我不止会关键词匹配,现在我可以基于手册内容,理解您的复杂问题。\n\n您可以问我:\n- “如果是老客户,申请 Code 一般要几天?”\n- “2026年关于供应商付款有哪些严厉的新规?”" }
]);
const chatEndRef = useRef(null);
const scrollToBottom = () => {
chatEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
scrollToBottom();
}, [chatHistory, isTyping]);
// --- AI Handlers ---
const handleSend = async () => {
if (!input.trim()) return;
const userMsg = { id: Date.now(), isBot: false, text: input };
setChatHistory(prev => [...prev, userMsg]);
setInput('');
setIsTyping(true);
const systemInstruction = `
你是一个专业的项目管理手册咨询顾问。请根据以下提供的知识库内容回答用户的问题。
知识库内容 (包含2025/2026新规, 常见问题, 流程):
${MANUAL_CONTEXT}
回答原则:
1. 必须基于知识库内容回答。如果知识库没有提及,请礼貌地说明“手册中未找到相关规定,建议咨询中台”。
2. 2026年的新规(如财务部分)优先级高于旧规则,请特别提示用户。
3. 回答要清晰、分点陈述,语气专业亲切。
`;
try {
const aiResponseText = await callGemini(input, systemInstruction);
setChatHistory(prev => [...prev, { id: Date.now() + 1, isBot: true, text: aiResponseText }]);
} catch (error) {
setChatHistory(prev => [...prev, { id: Date.now()+1, isBot: true, text: "抱歉,AI 服务连接失败。" }]);
} finally {
setIsTyping(false);
}
};
const handleGenerateChecklist = async (slide) => {
setModalTitle(`智能执行清单: ${slide.title}`);
setModalContent('');
setIsModalLoading(true);
setModalOpen(true);
const prompt = `
请根据项目管理手册中 "${slide.title}" (${slide.phase}) 的内容,生成一个非常直观、可视化的 HTML 执行清单。
该阶段内容参考: ${JSON.stringify(slide.content)}
要求:
1. **不要**返回 Markdown 代码,直接返回一段完整的 HTML 代码。
2. 使用 **Tailwind CSS** 类名来美化界面,风格要求现代、清爽、商务。
3. **呈现形式(必须生动,选一种最适合的)**:
- 如果内容是流程(如审批流),请设计成一个带有箭头图标的“流程图”样式(横向或纵向 Flex 布局)。
- 如果内容是待办事项,请设计成一个“卡片矩阵”或“表格”,包含“责任人”、“动作”、“关键点”等列。
- 使用 Emoji 图标(如 👤, 📝, ⚠️, ✅)来增强视觉效果。
- 关键的审批节点(如 Raymond Lu, Wise Xu)请用醒目的颜色(如 bg-red-100 text-red-700)高亮显示。
4. 结构清晰,包含一个简短的总结性标题。
`;
const system = `你是一个前端专家和项目经理助理。你的任务是将枯燥的文本转化为精美的 HTML/Tailwind CSS 可视化图表。不要使用 Markdown 格式,直接输出 HTML 字符串。`;
try {
const text = await callGemini(prompt, system);
setModalContent(text);
} catch (e) {
setModalContent("
生成失败,请稍后再试。
");
} finally {
setIsModalLoading(false);
}
};
const handleKeyPress = (e) => {
if (e.key === 'Enter') handleSend();
};
return (
setModalOpen(false)}
title={modalTitle}
content={modalContent}
isLoading={isModalLoading}
/>
{/* 顶部导航 */}
Project Manual Hub
AI Powered
项目管理手册 & 智能办公平台
{/* 主内容区域 */}
{/* 1. 智能聊天界面 */}
{activeTab === 'chat' && (
{chatHistory.map(msg => (
))}
{isTyping && (
)}
)}
{/* 2. PPT 时间轴界面 (带生成清单功能) */}
{activeTab === 'timeline' && (
{[...TIMELINE_SLIDES].reverse().map((slide, index) => (
{index < TIMELINE_SLIDES.length - 1 && (
)}
))}
持续更新中...
管理员将根据项目进展,随时补充新的 PPT 页面至此时间轴。
)}
{/* 3. 资料下载界面 (无 AI 摘要) */}
{activeTab === 'downloads' && (
资料下载中心
一键获取最新的项目管理手册、PPT 及财务规范文件。
{DOWNLOAD_RESOURCES.map((resource) => (
))}
)}
{/* 4. FAQ 界面 */}
{activeTab === 'faq' && (
{FAQ_DATA.map((category, idx) => (
{category.category}
{category.questions.map((q, qIdx) => (
))}
))}
)}
);
}