diff --git a/app/components/Community.tsx b/app/components/Community.tsx index 65ffcef..240e4c2 100644 --- a/app/components/Community.tsx +++ b/app/components/Community.tsx @@ -41,7 +41,14 @@ export function Community() { asChild className="bg-transparent border-[var(--foreground)] text-[var(--foreground)] hover:bg-[var(--foreground)] hover:text-[var(--background)] px-12 py-8 h-auto font-sans text-sm uppercase tracking-widest font-bold" > - + Access Articles / 访问文章{" "} @@ -73,6 +80,9 @@ export function Community() { href="https://github.com/involutionhell" target="_blank" rel="noopener noreferrer" + data-umami-event="resource_click" + data-umami-event-type="github_repo" + data-umami-event-location="community_card" > Source Code @@ -98,6 +108,9 @@ export function Community() { href="https://discord.com/invite/6CGP73ZWbD" target="_blank" rel="noopener noreferrer" + data-umami-event="resource_click" + data-umami-event-type="discord_invite" + data-umami-event-location="community_card" > Join Dispatch @@ -123,6 +136,9 @@ export function Community() { href="https://www.zotero.org/groups/6053219/unsw_ai/library" target="_blank" rel="noopener noreferrer" + data-umami-event="resource_click" + data-umami-event-type="zotero" + data-umami-event-location="community_card" > View Library diff --git a/app/components/Contribute.tsx b/app/components/Contribute.tsx index dff5ce0..e068407 100644 --- a/app/components/Contribute.tsx +++ b/app/components/Contribute.tsx @@ -132,6 +132,14 @@ export function Contribute() { if (filename !== articleFile) { setArticleFile(filename); } + + if (window.umami) { + window.umami.track("contribute_github_redirect", { + dir: finalDirPath, + filename: filename, + }); + } + window.open( buildGithubNewUrl(finalDirPath, filename, title), "_blank", @@ -162,6 +170,8 @@ export function Contribute() { text-2xl font-serif font-black uppercase italic tracking-tighter bg-[var(--foreground)] text-[var(--background)] border border-[var(--foreground)] hover:bg-[var(--background)] hover:text-[var(--foreground)] transition-all duration-300" + data-umami-event="contribute_trigger" + data-umami-event-location="hero" onClick={(event) => { event.preventDefault(); router.push("/editor"); diff --git a/app/components/CustomSearchDialog.tsx b/app/components/CustomSearchDialog.tsx new file mode 100644 index 0000000..c7a4bd3 --- /dev/null +++ b/app/components/CustomSearchDialog.tsx @@ -0,0 +1,125 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { useDocsSearch } from "fumadocs-core/search/client"; +import { useI18n } from "fumadocs-ui/provider"; +import { + SearchDialog, + SearchDialogContent, + SearchDialogHeader, + SearchDialogInput, + SearchDialogList, + SearchDialogFooter, + SearchDialogOverlay, + SearchDialogIcon, + SearchDialogClose, + TagsList, + TagsListItem, + type SharedProps, +} from "fumadocs-ui/components/dialog/search"; + +interface TagItem { + name: string; + value: string; +} + +interface DefaultSearchDialogProps extends SharedProps { + links?: [string, string][]; + type?: "fetch" | "static"; + defaultTag?: string; + tags?: TagItem[]; + api?: string; + delayMs?: number; + footer?: React.ReactNode; + allowClear?: boolean; +} + +export function CustomSearchDialog({ + defaultTag, + tags = [], + api, + delayMs, + type = "fetch", + allowClear = false, + links = [], + footer, + ...props +}: DefaultSearchDialogProps) { + const { locale } = useI18n(); + const [tag, setTag] = useState(defaultTag); + const { search, setSearch, query } = useDocsSearch( + type === "fetch" + ? { + type: "fetch", + api, + locale, + tag, + delayMs, + } + : { + type: "static", + from: api, + locale, + tag, + delayMs, + }, + ); + + // Tracking logic + useEffect(() => { + if (!search) return; + + const timer = setTimeout(() => { + if (window.umami) { + window.umami.track("search_query", { query: search }); + } + }, 1000); // 1s debounce + + return () => clearTimeout(timer); + }, [search]); + + const defaultItems = useMemo(() => { + if (links.length === 0) return null; + return links.map(([name, link]) => ({ + type: "page" as const, + id: name, + content: name, + url: link, + })); + }, [links]); + + return ( + + + + + + + + + + + + {tags.length > 0 && ( + + {tags.map((tag) => ( + + {tag.name} + + ))} + + )} + {footer} + + + ); +} diff --git a/app/components/DocsAssistant.tsx b/app/components/DocsAssistant.tsx index e877ed8..60e2af6 100644 --- a/app/components/DocsAssistant.tsx +++ b/app/components/DocsAssistant.tsx @@ -52,7 +52,15 @@ function DocsAssistantInner({ pageContext }: DocsAssistantProps) { [geminiApiKey, openaiApiKey, pageContext, provider], ); - const chat = useChat({ transport }); + const chat = useChat({ + transport, + onFinish: () => { + // 当对话结束时(流式传输完成),记录一次查询行为 + if (window.umami) { + window.umami.track("ai_assistant_query"); + } + }, + }); const { error: chatError, diff --git a/app/components/EditOnGithub.tsx b/app/components/EditOnGithub.tsx index b1a2294..55d1c49 100644 --- a/app/components/EditOnGithub.tsx +++ b/app/components/EditOnGithub.tsx @@ -5,6 +5,8 @@ export function EditOnGithub({ href }: { href: string }) {
diff --git a/app/components/Footer.tsx b/app/components/Footer.tsx index e19f569..61336da 100644 --- a/app/components/Footer.tsx +++ b/app/components/Footer.tsx @@ -27,6 +27,9 @@ export function Footer() { rel="noopener noreferrer" aria-label="访问 GitHub" title="访问 GitHub" + data-umami-event="social_click" + data-umami-event-platform="github" + data-umami-event-location="footer" className="w-12 h-12 flex items-center justify-center border border-[var(--foreground)] hover:bg-[var(--foreground)] hover:text-[var(--background)] transition-all text-[var(--foreground)]" > @@ -38,6 +41,9 @@ export function Footer() { rel="noopener noreferrer" aria-label="加入 Discord 社区" title="加入 Discord 社区" + data-umami-event="social_click" + data-umami-event-platform="discord" + data-umami-event-location="footer" className="w-12 h-12 flex items-center justify-center border border-[var(--foreground)] hover:bg-[var(--foreground)] hover:text-[var(--background)] transition-all text-[var(--foreground)]" > @@ -57,6 +63,9 @@ export function Footer() { AI & Mathematics @@ -65,6 +74,9 @@ export function Footer() { Computer Science @@ -73,6 +85,9 @@ export function Footer() { Community Sharing @@ -81,6 +96,9 @@ export function Footer() { Career Prep @@ -99,6 +117,10 @@ export function Footer() { target="_blank" rel="noopener noreferrer" className="hover:text-[#CC0000] transition-colors" + data-umami-event="resource_click" + data-umami-event-type="zotero" + data-umami-event-location="footer" + data-umami-event-url="https://www.zotero.org/groups/6053219/unsw_ai/library" > Zotero Library diff --git a/app/components/Header.tsx b/app/components/Header.tsx index df8576a..8bb7cd8 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -39,18 +39,27 @@ export async function Header() { 特点 社区 联系我们 @@ -68,6 +77,9 @@ export async function Header() { target="_blank" rel="noopener noreferrer" aria-label="GitHub" + data-umami-event="social_click" + data-umami-event-platform="github" + data-umami-event-location="header" > @@ -83,6 +95,9 @@ export async function Header() { target="_blank" rel="noopener noreferrer" aria-label="Discord" + data-umami-event="social_click" + data-umami-event-platform="discord" + data-umami-event-location="header" > diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index de78068..b2831a8 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -80,7 +80,13 @@ export function Hero() { Connect with thousands of developers who are reclaiming their passion for technology.

- + @@ -107,6 +113,8 @@ export function Hero() {
00{idx + 1} diff --git a/app/components/SignInButton.tsx b/app/components/SignInButton.tsx index a93cdce..768ee91 100644 --- a/app/components/SignInButton.tsx +++ b/app/components/SignInButton.tsx @@ -27,7 +27,14 @@ export function SignInButton({ }); }} > - diff --git a/app/components/ThemeToggle.tsx b/app/components/ThemeToggle.tsx index 1afc9d5..367db42 100644 --- a/app/components/ThemeToggle.tsx +++ b/app/components/ThemeToggle.tsx @@ -13,6 +13,9 @@ export function ThemeToggle() { onClick={() => { const next = theme === "light" ? "dark" : "light"; setTheme(next); + if (window.umami) { + window.umami.track("theme_toggle", { theme: next }); + } }} className="h-10 w-10 rounded-none transition-colors" > diff --git a/app/components/UmamiIdentity.tsx b/app/components/UmamiIdentity.tsx new file mode 100644 index 0000000..44a8763 --- /dev/null +++ b/app/components/UmamiIdentity.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { useEffect } from "react"; + +interface UmamiIdentityProps { + user?: { + name?: string | null; + email?: string | null; + image?: string | null; + id?: string; + } | null; +} + +export function UmamiIdentity({ user }: UmamiIdentityProps) { + useEffect(() => { + if (user && window.umami && window.umami.identify) { + // 识别用户,发送用户基础信息 + // Umami v2 支持通过 session data 关联用户信息 + window.umami.identify({ + email: user.email, + name: user.name, + id: user.id, // 如果有 ID + }); + } + }, [user]); + + return null; +} diff --git a/app/components/UserMenu.tsx b/app/components/UserMenu.tsx index 7e78040..3924d16 100644 --- a/app/components/UserMenu.tsx +++ b/app/components/UserMenu.tsx @@ -22,6 +22,7 @@ export function UserMenu({ user, provider }: UserMenuProps) { {user.image ? ( diff --git a/app/components/assistant-ui/assistant-modal.tsx b/app/components/assistant-ui/assistant-modal.tsx index 5550fd5..a484ad2 100644 --- a/app/components/assistant-ui/assistant-modal.tsx +++ b/app/components/assistant-ui/assistant-modal.tsx @@ -107,6 +107,12 @@ const AssistantModalButton = forwardRef< if (onCloseBubble) { onCloseBubble(); } + + // 如果当前是关闭状态,说明即将打开,记录埋点 + if (state === "closed" && window.umami) { + window.umami.track("ai_assistant_open"); + } + // 继续执行原有的点击事件 if (rest.onClick) { rest.onClick(e); diff --git a/app/layout.tsx b/app/layout.tsx index bfa4f0c..07c8964 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -7,6 +7,10 @@ import "./globals.css"; import "katex/dist/katex.min.css"; import { ThemeProvider } from "@/app/components/ThemeProvider"; import { SpeedInsights } from "@vercel/speed-insights/next"; +import { auth } from "@/auth"; +import { UmamiIdentity } from "@/app/components/UmamiIdentity"; +// import { SearchWrapper } from "@/app/components/SearchWrapper"; +import { CustomSearchDialog } from "@/app/components/CustomSearchDialog"; const geistSans = localFont({ src: "./fonts/GeistVF.woff", @@ -119,9 +123,10 @@ export const metadata: Metadata = { }, }; -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { + const session = await auth(); return ( @@ -202,6 +207,7 @@ export default function RootLayout({ + {/* Umami Analytics */} +