Files
youwei-business/src/components/sections/AITools.tsx
Ebenezer 123f82e92c Initialize project scaffold with full web and API setup.
Add the Vite frontend, Express API server, and supporting configuration files so the app can run locally on a complete development stack.

Made-with: Cursor
2026-04-15 17:41:46 +08:00

415 lines
20 KiB
TypeScript

import { useState, useEffect } from "react";
import { createPortal } from "react-dom";
import { Search, ArrowRight, Sparkles, Zap, X } from "lucide-react";
import { useLang } from "@/contexts/LanguageContext";
import { motion, AnimatePresence } from "framer-motion";
import ComingSoonModal from "@/components/ComingSoonModal";
const agents = [
{ nameKey: "ai.aism.name", tagKey: "ai.aism.tag", descKey: "ai.aism.desc", emoji: "🤖", tags: ["ai.tag.instantResponse", "ai.tag.multiRound", "ai.tag.problemTriage"], gradient: "from-blue-500/10 to-cyan-500/5", status: "online", category: "ai.customerService" },
{ nameKey: "ai.meeting.name", tagKey: "ai.meeting.tag", descKey: "ai.meeting.desc", emoji: "📝", tags: ["ai.tag.speechToText", "ai.tag.keyPoints", "ai.tag.taskAllocation"], gradient: "from-violet-500/10 to-purple-500/5", status: "online", category: "ai.efficiency" },
{ nameKey: "ai.deepseek.name", tagKey: "ai.deepseek.tag", descKey: "ai.deepseek.desc", emoji: "🧠", tags: ["ai.tag.deepUnderstanding", "ai.tag.accurate", "ai.tag.knowledgeable"], gradient: "from-emerald-500/10 to-teal-500/5", status: "online", category: "ai.analyze" },
{ nameKey: "ai.adminAssist.name", tagKey: "ai.adminAssist.tag", descKey: "ai.adminAssist.desc", emoji: "📋", tags: ["ai.tag.scheduling", "ai.tag.documents", "ai.tag.reminders"], gradient: "from-amber-500/10 to-orange-500/5", status: "busy", category: "ai.admin" },
{ nameKey: "ai.strategic.name", tagKey: "ai.strategic.tag", descKey: "ai.strategic.desc", emoji: "🎯", tags: ["ai.tag.swotAnalysis", "ai.tag.competitorResearch", "ai.tag.marketInsights"], gradient: "from-rose-500/10 to-pink-500/5", status: "online", category: "ai.analyze" },
{ nameKey: "ai.bizplan.name", tagKey: "ai.bizplan.tag", descKey: "ai.bizplan.desc", emoji: "📈", tags: ["ai.tag.templates", "ai.tag.dataAnalysis", "ai.tag.oneClickExport"], gradient: "from-indigo-500/10 to-blue-500/5", status: "online", category: "ai.efficiency" },
{ nameKey: "ai.marketReport.name", tagKey: "ai.marketReport.tag", descKey: "ai.marketReport.desc", emoji: "📊", tags: ["ai.tag.trendScan", "ai.tag.sectorBenchmark", "ai.tag.reportStructured"], gradient: "from-sky-500/10 to-blue-500/5", status: "online", category: "ai.analyze" },
{ nameKey: "ai.techReport.name", tagKey: "ai.techReport.tag", descKey: "ai.techReport.desc", emoji: "🔬", tags: ["ai.tag.techFeasibility", "ai.tag.riskReview", "ai.tag.implementationPath"], gradient: "from-teal-500/10 to-emerald-500/5", status: "online", category: "ai.analyze" },
{ nameKey: "ai.factorySourcing.name", tagKey: "ai.factorySourcing.tag", descKey: "ai.factorySourcing.desc", emoji: "🏭", tags: ["ai.tag.capacityMatch", "ai.tag.certAudit", "ai.tag.geoSourcing"], gradient: "from-orange-500/10 to-amber-500/5", status: "online", category: "ai.industrial" },
{ nameKey: "ai.patentCompare.name", tagKey: "ai.patentCompare.tag", descKey: "ai.patentCompare.desc", emoji: "📑", tags: ["ai.tag.claimDiff", "ai.tag.featureOverlap", "ai.tag.priorArtHint"], gradient: "from-fuchsia-500/10 to-purple-500/5", status: "online", category: "ai.analyze" },
{ nameKey: "ai.zoneInspection.name", tagKey: "ai.zoneInspection.tag", descKey: "ai.zoneInspection.desc", emoji: "🔍", tags: ["ai.tag.innovationClaims", "ai.tag.listingConsistency", "ai.tag.policyAlignment"], gradient: "from-lime-500/10 to-green-500/5", status: "online", category: "ai.analyze" },
{ nameKey: "ai.techPrice.name", tagKey: "ai.techPrice.tag", descKey: "ai.techPrice.desc", emoji: "💰", tags: ["ai.tag.comparableTx", "ai.tag.licensingContext", "ai.tag.priceRange"], gradient: "from-rose-500/10 to-orange-500/5", status: "online", category: "ai.analyze" },
{ nameKey: "ai.patentMicroNav.name", tagKey: "ai.patentMicroNav.tag", descKey: "ai.patentMicroNav.desc", emoji: "🧭", tags: ["ai.tag.citationMap", "ai.tag.techBranches", "ai.tag.landscapeBrief"], gradient: "from-cyan-500/10 to-sky-500/5", status: "online", category: "ai.analyze" },
];
const categories = ["ai.all", ...Array.from(new Set(agents.map((a) => a.category)))];
// Optional .env overrides: VITE_*_CHATBOT_URL for each bot (see vite-env.d.ts)
const MEETING_CHATBOT_IFRAME_SRC =
(import.meta.env.VITE_MEETING_CHATBOT_URL as string | undefined)?.trim() ||
"http://nw.sgcode.cn:18181/chat/CjmrnoUyQKBa9wEP";
const DEEPSEEK_CHATBOT_IFRAME_SRC =
(import.meta.env.VITE_DEEPSEEK_CHATBOT_URL as string | undefined)?.trim() ||
"http://nw.sgcode.cn:18181/chatbot/pOcS3NYaXCkOP9K6";
const AISM_CHATBOT_IFRAME_SRC =
(import.meta.env.VITE_AISM_CHATBOT_URL as string | undefined)?.trim() ||
"http://nw.sgcode.cn:18181/chatbot/grHow5qCsnZd7PYq";
const ADMIN_ASSIST_CHATBOT_IFRAME_SRC =
(import.meta.env.VITE_ADMIN_ASSIST_CHATBOT_URL as string | undefined)?.trim() ||
"http://nw.sgcode.cn:18181/chatbot/FIIxbn1rsEtmw2Yt";
const STRATEGIC_CHATBOT_IFRAME_SRC =
(import.meta.env.VITE_STRATEGIC_CHATBOT_URL as string | undefined)?.trim() ||
"http://nw.sgcode.cn:18181/chatbot/bKP9rLpTdWQgWSLA";
const BIZPLAN_CHATBOT_IFRAME_SRC =
(import.meta.env.VITE_BIZPLAN_CHATBOT_URL as string | undefined)?.trim() ||
"http://nw.sgcode.cn:18181/chatbot/u0mmSsO0YCtjh37K";
const MARKET_REPORT_CHATBOT_IFRAME_SRC =
(import.meta.env.VITE_MARKET_REPORT_CHATBOT_URL as string | undefined)?.trim() ||
"http://nw.sgcode.cn:18181/chatbot/SqVX9saxCfTdJq6F";
const TECH_REPORT_CHATBOT_IFRAME_SRC =
(import.meta.env.VITE_TECH_REPORT_CHATBOT_URL as string | undefined)?.trim() ||
"http://nw.sgcode.cn:18181/chatbot/S8fdxgEVKa3V1kWb";
const PATENT_COMPARE_CHATBOT_IFRAME_SRC =
(import.meta.env.VITE_PATENT_COMPARE_CHATBOT_URL as string | undefined)?.trim() ||
"http://nw.sgcode.cn:18181/chatbot/tvX9ytDVu0BonRZp";
const ZONE_INSPECTION_CHATBOT_IFRAME_SRC =
(import.meta.env.VITE_ZONE_INSPECTION_CHATBOT_URL as string | undefined)?.trim() ||
"http://nw.sgcode.cn:18181/chatbot/RUeFYK8U6gt9cncx";
const TECH_PRICE_CHATBOT_IFRAME_SRC =
(import.meta.env.VITE_TECH_PRICE_CHATBOT_URL as string | undefined)?.trim() ||
"http://nw.sgcode.cn:18181/chatbot/VDLRBetGCViMjEEy";
const PATENT_MICRO_NAV_CHATBOT_IFRAME_SRC =
(import.meta.env.VITE_PATENT_MICRO_NAV_CHATBOT_URL as string | undefined)?.trim() ||
"http://nw.sgcode.cn:18181/chatbot/pU0QjcVNDaz0mBzJ";
// Override with VITE_FACTORY_SOURCING_CHATBOT_URL if this id is not your factory bot.
const FACTORY_SOURCING_CHATBOT_IFRAME_SRC =
(import.meta.env.VITE_FACTORY_SOURCING_CHATBOT_URL as string | undefined)?.trim() ||
"http://nw.sgcode.cn:18181/chatbot/OstO0weOb2iRy4Ng";
const CHATBOT_IFRAME_STYLE = {
width: "100%",
height: "100%",
minHeight: 700,
border: 0,
} as const;
type ChatSession = { src: string; title: string } | null;
export default function AITools() {
const { t, lang } = useLang();
const [activeCategory, setActiveCategory] = useState("ai.all");
const [searchQuery, setSearchQuery] = useState("");
const [chatSession, setChatSession] = useState<ChatSession>(null);
const [comingSoonOpen, setComingSoonOpen] = useState(false);
const filteredAgents = agents.filter((agent) => {
const matchesCategory = activeCategory === "ai.all" || agent.category === activeCategory;
const matchesSearch =
!searchQuery ||
t(agent.nameKey).toLowerCase().includes(searchQuery.toLowerCase()) ||
t(agent.descKey).toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
});
useEffect(() => {
if (!chatSession) return;
const html = document.documentElement;
const body = document.body;
const prevHtmlOverflow = html.style.overflow;
const prevBodyOverflow = body.style.overflow;
const scrollY = window.scrollY;
html.style.overflow = "hidden";
body.style.overflow = "hidden";
body.style.position = "fixed";
body.style.top = `-${scrollY}px`;
body.style.left = "0";
body.style.right = "0";
body.style.width = "100%";
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") setChatSession(null);
};
window.addEventListener("keydown", onKeyDown);
return () => {
html.style.overflow = prevHtmlOverflow;
body.style.overflow = prevBodyOverflow;
body.style.position = "";
body.style.top = "";
body.style.left = "";
body.style.right = "";
body.style.width = "";
window.removeEventListener("keydown", onKeyDown);
window.scrollTo(0, scrollY);
};
}, [chatSession]);
return (
<div className="space-y-8">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="flex flex-col md:flex-row md:items-end md:justify-between gap-4"
>
<div>
<div className="section-tag w-fit mb-4">
<Sparkles className="w-3 h-3" />
{t("ai.tag")}
</div>
<h3 className="text-3xl font-bold text-foreground tracking-tight">{t("ai.title")}</h3>
<p className="text-sm text-muted-foreground mt-2 max-w-md">{t("ai.subtitle")}</p>
</div>
<div className="relative w-full md:w-72">
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<input
type="text"
placeholder={t("ai.search")}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-3 rounded-xl glass-card text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 placeholder:text-muted-foreground border-0"
/>
{searchQuery && (
<motion.button
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-muted-foreground hover:text-foreground"
onClick={() => setSearchQuery("")}
>
</motion.button>
)}
</div>
</motion.div>
{/* Category pills with animated indicator */}
<motion.div
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="flex flex-wrap gap-2"
>
{categories.map((cat) => (
<motion.button
key={cat}
onClick={() => setActiveCategory(cat)}
className={`relative px-4 py-2 rounded-xl text-xs font-medium transition-colors duration-200 ${
activeCategory === cat
? "text-background"
: "glass-pill text-muted-foreground hover:text-foreground"
}`}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.97 }}
>
{activeCategory === cat && (
<motion.div
layoutId="activeCat"
className="absolute inset-0 bg-foreground rounded-xl shadow-lg shadow-foreground/10"
transition={{ type: "spring", stiffness: 400, damping: 28 }}
/>
)}
<span className="relative z-10">{t(cat)}</span>
</motion.button>
))}
</motion.div>
{/* Agent grid */}
<motion.div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-5">
{filteredAgents.length === 0 && (
<motion.div
key="empty"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="col-span-full text-center py-12 text-muted-foreground text-sm"
>
{lang === "zh" ? "没有找到匹配的智能体" : "No matching agents found"}
</motion.div>
)}
{filteredAgents.map((agent, i) => (
<motion.div
key={agent.nameKey}
initial="rest"
animate="rest"
whileHover="hover"
variants={{ rest: { y: 0, scale: 1 }, hover: { y: -8, scale: 1.03 } }}
transition={{ duration: 0.25, delay: i * 0.04 }}
className={`glass-card p-0 flex flex-col overflow-hidden cursor-pointer ${
i === 0 && filteredAgents.length >= 3 ? "lg:col-span-2 lg:row-span-1" : ""
}`}
style={{ boxShadow: "none" }}
>
{/* Gradient header */}
<div className={`bg-gradient-to-br ${agent.gradient} p-6 pb-4 relative`}>
<div className="flex items-start justify-between">
<motion.div
className="text-4xl origin-center"
variants={{
rest: { scale: 1, rotate: 0 },
hover: { scale: 1.3, rotate: [0, -12, 10, -6, 0] },
}}
transition={{ type: "spring", stiffness: 400, damping: 10 }}
>
{agent.emoji}
</motion.div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
<div
className={`w-2 h-2 rounded-full ${agent.status === "online" ? "bg-emerald-400" : "bg-amber-400"}`}
/>
{agent.status}
</div>
<span className="section-tag !text-[10px] !px-2.5 !py-1">{t(agent.tagKey)}</span>
</div>
</div>
<h5 className="font-bold text-foreground text-base mt-3 leading-tight">{t(agent.nameKey)}</h5>
</div>
<div className="p-6 pt-4 flex flex-col flex-1">
<p className="text-xs text-muted-foreground flex-1 mb-4 leading-relaxed">{t(agent.descKey)}</p>
<div className="flex flex-wrap gap-1.5 mb-5">
{agent.tags.map((tag) => (
<span
key={tag}
className="glass-pill !rounded-lg px-2.5 py-1 text-[10px] text-muted-foreground font-medium"
>
{t(tag)}
</span>
))}
</div>
<button
className="btn-primary w-full !rounded-xl !text-xs relative isolate !shadow-none hover:!shadow-none hover:[filter:none] flex items-center justify-center gap-2 transition-transform active:scale-[0.98]"
onClick={() => {
if (agent.nameKey === "ai.aism.name") {
setChatSession({ src: AISM_CHATBOT_IFRAME_SRC, title: t("ai.aism.name") });
} else if (agent.nameKey === "ai.meeting.name") {
setChatSession({
src: MEETING_CHATBOT_IFRAME_SRC,
title: lang === "zh" ? "会议纪要助手" : "Meeting minutes assistant",
});
} else if (agent.nameKey === "ai.deepseek.name") {
setChatSession({
src: DEEPSEEK_CHATBOT_IFRAME_SRC,
title: lang === "zh" ? "Deepseek问答助手" : "Deepseek Q&A assistant",
});
} else if (agent.nameKey === "ai.adminAssist.name") {
setChatSession({
src: ADMIN_ASSIST_CHATBOT_IFRAME_SRC,
title: t("ai.adminAssist.name"),
});
} else if (agent.nameKey === "ai.strategic.name") {
setChatSession({
src: STRATEGIC_CHATBOT_IFRAME_SRC,
title: t("ai.strategic.name"),
});
} else if (agent.nameKey === "ai.bizplan.name") {
setChatSession({
src: BIZPLAN_CHATBOT_IFRAME_SRC,
title: t("ai.bizplan.name"),
});
} else if (agent.nameKey === "ai.marketReport.name") {
setChatSession({
src: MARKET_REPORT_CHATBOT_IFRAME_SRC,
title: t("ai.marketReport.name"),
});
} else if (agent.nameKey === "ai.techReport.name") {
setChatSession({
src: TECH_REPORT_CHATBOT_IFRAME_SRC,
title: t("ai.techReport.name"),
});
} else if (agent.nameKey === "ai.patentCompare.name") {
setChatSession({
src: PATENT_COMPARE_CHATBOT_IFRAME_SRC,
title: t("ai.patentCompare.name"),
});
} else if (agent.nameKey === "ai.zoneInspection.name") {
setChatSession({
src: ZONE_INSPECTION_CHATBOT_IFRAME_SRC,
title: t("ai.zoneInspection.name"),
});
} else if (agent.nameKey === "ai.techPrice.name") {
setChatSession({
src: TECH_PRICE_CHATBOT_IFRAME_SRC,
title: t("ai.techPrice.name"),
});
} else if (agent.nameKey === "ai.patentMicroNav.name") {
setChatSession({
src: PATENT_MICRO_NAV_CHATBOT_IFRAME_SRC,
title: t("ai.patentMicroNav.name"),
});
} else if (agent.nameKey === "ai.factorySourcing.name") {
setChatSession({
src: FACTORY_SOURCING_CHATBOT_IFRAME_SRC,
title: t("ai.factorySourcing.name"),
});
} else {
setComingSoonOpen(true);
}
}}
>
<Zap className="w-3 h-3" />
{t("ai.startChat")}
<ArrowRight className="w-3 h-3" />
</button>
</div>
</motion.div>
))}
</motion.div>
{/* Portal modal with viewport padding (not full-bleed) */}
{typeof document !== "undefined" &&
createPortal(
<AnimatePresence>
{chatSession && (
<motion.div
key={`chat-fullscreen-${chatSession.src}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-[9999] flex items-center justify-center p-3 sm:p-6 overflow-hidden overscroll-none bg-zinc-950/80 backdrop-blur-sm"
style={{
overscrollBehavior: "none",
width: "100vw",
minHeight: "100dvh",
height: "100dvh",
}}
onClick={() => setChatSession(null)}
>
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.98 }}
transition={{ duration: 0.2 }}
role="dialog"
aria-modal="true"
aria-label={chatSession.title}
className="relative w-full max-w-5xl h-[min(86dvh,820px)] sm:min-h-[700px] rounded-2xl overflow-hidden border border-zinc-700/80 bg-zinc-950 shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<iframe
title={chatSession.title}
src={chatSession.src}
className="absolute inset-0 bg-zinc-950"
style={CHATBOT_IFRAME_STYLE}
allow="microphone"
/>
<button
type="button"
onClick={() => setChatSession(null)}
className="absolute z-10 flex h-11 w-11 items-center justify-center rounded-full border border-zinc-600/80 bg-zinc-900/90 text-zinc-100 shadow-lg backdrop-blur-sm transition-colors hover:bg-zinc-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400"
style={{
top: "max(0.75rem, env(safe-area-inset-top, 0px))",
right: "max(0.75rem, env(safe-area-inset-right, 0px))",
}}
aria-label={lang === "zh" ? "关闭" : "Close"}
>
<X className="h-5 w-5" />
</button>
</motion.div>
</motion.div>
)}
</AnimatePresence>,
document.body,
)}
<ComingSoonModal open={comingSoonOpen} onClose={() => setComingSoonOpen(false)} />
</div>
);
}