hts/apps/blogai/components/header.tsx

828 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import React from 'react'
import { Suspense } from 'react'
import { auth } from '@/auth'
import { clearChats } from '@/app/actions'
import { Sidebar } from '@/components/sidebar'
import { SidebarList } from '@/components/sidebar-list'
import { IconSeparator } from '@/components/ui/icons'
import { SidebarFooter } from '@/components/sidebar-footer'
import { ClearHistory } from '@/components/clear-history'
import { UserMenu, UserData } from '@/components/user-menu'
import { LoginButton } from '@/components/login-button'
import {
Tooltip,
TooltipContent,
TooltipTrigger
} from '@/components/ui/tooltip'
import { Badge } from '@/components/ui/badge'
import { ConnectButton } from '@/components/connect-button'
import { SettingsDropDown } from './settings-drop-down'
import { useLocalStorage } from '@/lib/hooks/use-local-storage'
import { Button } from './ui/button'
import Image from 'next/image';
import logoImage from '@/components/images/logo.png';
import { useRouter } from 'next/navigation'
// import { Button } from './ui/button'
import { Flex, Text } from '@radix-ui/themes';
import Link from 'next/link'
import { useTranslation } from 'react-i18next'
import { message } from 'antd'
import { LogoAI } from '@/components/chat'
import { useState, useRef, } from "react";
import { Trash2 } from "lucide-react";
import { useEffect } from "react";
import {
BadgeInfo,
Tags,
CalendarClock,
Building2
} from "lucide-react";
export function Header() {
const router = useRouter();
const { t } = useTranslation();
const [userData, setUserData] = useLocalStorage(
'UserData',
{
auth_token: "",
id: 1,
login_ip: "",
login_time: 0,
role: "",
user_name: "",
version: ""
} as UserData
)
const soonFunc = () => {
message.info(t("soon"))
}
//w-11/12 sm:w-5/6 md:w-3/4 lg:w-2/3 xl:w-3/5 2xl:w-1/2
return (
<header className="sticky top-0 z-50 flex shrink-0 items-center justify-between bg-background px-4 py-[1.5rem] w-[80%] mx-auto">
<a href="/">
<LogoAI className="flex text-center ml-0" />
</a>
<div className="flex items-center justify-between ">
<div className="flex items-center ">
{(!!userData && !!userData.user_name) ? (
<div className='flex text-[#1A1A1A]'>
<UserMenu user={userData} />
</div>
) : (
<div className='flex gap-4 grid-cols-2 text-[#1A1A1A]'>
<Button variant="ghost" className='text-base bg-[#f4f4f5] hover:bg-[#e5e7eb]'>
<Link href="/#subscribe-target"
>
{t('subscribe.subscribe')}
</Link>
</Button>
<LoginButton
variant="ghost"
// variant="link"
showGithubIcon={true}
text={t('login')}
className="-ml-2 text-base bg-[#f4f4f5] hover:bg-[#e5e7eb]"
/>
</div>
)}
</div>
</div>
</header>
)
}
// ✅ 图标显示逻辑现在是这样的:
// data.icon = "🚀" 👉 显示 emoji
// data.icon = "/images/icon.png" 👉 显示图片;
// data.icon = undefined/null 👉 显示默认 "Deploy" 字样。
// export function DetailPageHeader({ data }: { data: any }) {
// const [loading, setLoading] = useState(false);
// const [statusText, setStatusText] = useState(data?.statusText || "加载中...");
// const [progress, setProgress] = useState(data?.progress || "0%");
// const [showDelete, setShowDelete] = useState(true); // 默认可以删除
// // const handleClick = async (source: "icon" | "info") => {
// // setLoading(true);
// // setStatusText(source === "icon" ? "正在处理图标操作..." : "正在处理信息操作...");
// // try {
// // // 从 localStorage 获取用户信息
// // const userData = JSON.parse(localStorage.getItem("UserData") || "null");
// // if (!userData || !userData.user_name) {
// // setStatusText("未登录,正在跳转登录页面...");
// // window.location.href = "/auth/sign-in/";
// // return;
// // }
// // const userName = userData.user_name;
// // // 从组件 props 里的 data 中取 id
// // const id = data?.id;
// // if (!id) {
// // setStatusText("数据缺失:找不到组件 ID");
// // return;
// // }
// // // 发起部署请求
// // const res = await fetch("/api/v1/deploy/deploy", {
// // method: "POST",
// // headers: { "Content-Type": "application/json" },
// // body: JSON.stringify({
// // id,
// // user_name: userName,
// // }),
// // });
// // if (!res.ok) {
// // throw new Error(`HTTP 请求失败:${res.status}`);
// // }
// // const json = await res.json();
// // if (json?.header?.code === 0) {
// // setStatusText(`${json.header.message || "操作成功"}`);
// // } else {
// // setStatusText(`${json.header.message || "操作失败(后端返回错误)"}`);
// // }
// // } catch (err) {
// // console.error("请求出错:", err);
// // setStatusText("操作失败,请检查网络或服务状态");
// // } finally {
// // setLoading(false);
// // }
// // };
// const handleClick = async (source: "icon" | "info") => {
// setLoading(true);
// setStatusText(source === "icon" ? "正在处理图标操作..." : "正在处理信息操作...");
// try {
// const userData = JSON.parse(localStorage.getItem("UserData") || "null");
// if (!userData || !userData.user_name) {
// setStatusText("未登录,正在跳转登录页面...");
// window.location.href = "/auth/sign-in/";
// return;
// }
// const userName = userData.user_name;
// const id = data?.id;
// if (!id) {
// setStatusText("数据缺失:找不到组件 ID");
// return;
// }
// // 发起部署请求
// const res = await fetch("/api/v1/deploy/deploy", {
// method: "POST",
// headers: { "Content-Type": "application/json" },
// body: JSON.stringify({
// id,
// user_name: userName,
// }),
// });
// if (!res.ok) throw new Error(`HTTP 请求失败:${res.status}`);
// const json = await res.json();
// if (json?.header?.code === 0) {
// setStatusText("部署已启动,正在监听进度...");
// // ✅ 检查 env 变量是否读取成功
// console.log("🧪 WS BASE =", process.env.NEXT_PUBLIC_CLIENT_BASE_WS);
// // 发起 WebSocket 连接监听部署进度
// const wsBase = process.env.NEXT_PUBLIC_CLIENT_BASE_WS;
// const socket = new WebSocket(`${wsBase}/status/${userName}/${id}`);
// socket.onopen = () => {
// console.log("WebSocket 已连接");
// };
// socket.onmessage = (event) => {
// console.log("收到进度信息:", event.data);
// // setStatusText(event.data); // 你也可以用 setProgress() 更新进度条
// const msg = event.data;
// setStatusText(msg);
// // ✅ 自动解析形如“进度: 65%,阶段: 正在部署”的格式
// const match = msg.match(/进度[:]?\s*(\d+)%/);
// if (match && match[1]) {
// const percent = match[1] + "%";
// setProgress(percent);
// }
// };
// socket.onerror = (error) => {
// console.error("WebSocket 出错:", error);
// setStatusText("WebSocket 出错");
// };
// socket.onclose = () => {
// console.log("🔌 WebSocket 连接已关闭");
// };
// } else {
// setStatusText(`${json.header.message || "操作失败(后端返回错误)"}`);
// }
// } catch (err) {
// console.error("请求出错:", err);
// setStatusText("操作失败,请检查网络或服务状态");
// } finally {
// setLoading(false);
// }
// };
// const handleDelete = () => {
// if (loading) return;
// const confirmed = window.confirm("确定要删除模型吗?");
// if (confirmed) {
// setLoading(true);
// // 模拟删除流程(实际调用 API 或其他逻辑)
// // await deleteComponent(data.id);
// console.log("模型已删除");
// setLoading(false);
// }
// };
// useEffect(() => {
// const fetchDeployStatus = async () => {
// try {
// const result = await fetch("/api/v1/deploy/status", {
// method: "POST",
// headers: { "Content-Type": "application/json" },
// body: JSON.stringify({ id: data?.id }),
// }).then((res) => res.json());
// console.log("==================>result:", result);
// if (!result || result.header?.code !== 1006) {
// setStatusText(result?.header?.message || "操作失败(后端返回错误)");
// setShowDelete(false); // 非 1006 隐藏删除按钮
// return;
// }
// // 如果成功返回
// setStatusText(result.header.message || "部署成功");
// if (result.header?.code === 1006) {
// setShowDelete(false); // code === 1006 不可删除,隐藏按钮
// setStatusText("");
// setProgress("0%");
// } else {
// setShowDelete(true); // 其他状态可删除
// setStatusText(result.header.message || "部署中");
// setProgress(result.data?.progress || "0%");
// }
// // 如果还想设置进度的话,这里也可以 setProgress(result.data.progress || '0%')
// } catch (err) {
// console.error("请求部署状态失败:", err);
// setStatusText("请求失败");
// setShowDelete(false);
// }
// };
// fetchDeployStatus();
// }, [data?.id]); // id 变化时重新获取
// // 处理图标路径
// const isImagePath =
// typeof data?.icon === "string" &&
// (data.icon.startsWith("http") || data.icon.startsWith("/"));
// const resolvedIconSrc =
// isImagePath && !data.icon.startsWith("http")
// ? process.env.NEXT_PUBLIC_CLIENT_IMAGE_URL + data.icon
// : data.icon;
// return (
// <div className="sticky top-0 z-30 bg-white">
// <div className="mt-4 mb-1 px-6 lg:px-8 w-11/12 lg:w-2/3 xl:w-3/5 mx-auto">
// <div className="flex items-start space-x-6">
// {/* 左图标(可点) */}
// <button
// className="group flex items-center justify-center w-24 h-24 md:w-32 md:h-32 border transition"
// onClick={() => handleClick("icon")}
// disabled={loading || data?.status === "running"} // 👈 加上 status 判断
// >
// {isImagePath ? (
// <img
// src={resolvedIconSrc}
// alt="icon"
// className="
// w-full h-full object-contain
// transition-all duration-200
// group-hover:border-2 group-hover:border-blue-500
// group-active:border-2 group-active:border-green-500
// group-hover:scale-105 group-active:scale-95
// "
// />
// ) : (
// data?.icon || "Deploy"
// )}
// </button>
// {/* 信息 + 删除按钮:左右分布 */}
// <div className="flex justify-between flex-1 items-end">
// {/* 信息区域 */}
// <div className="text-sm leading-7 space-y-1.5">
// <div className="flex items-center gap-2">
// <BadgeInfo size={16} /> {data?.name || "未命名组件"}
// </div>
// <div className="flex items-center gap-2">
// <Tags size={16} /> {data?.category || "未知"}
// </div>
// <div className="flex items-center gap-2">
// <CalendarClock size={16} /> {data?.updated_at || "未提供"}
// </div>
// <div className="flex items-center gap-2">
// <Building2 size={16} /> {data?.company || "未知公司"}
// </div>
// </div>
// {/* 条件显示删除按钮 */}
// {showDelete && (
// <button
// onClick={handleDelete}
// className="hover:text-gray-700 transition self-end"
// disabled={loading}
// title="删除"
// >
// <Trash2 size={20} />
// </button>
// )}
// </div>
// </div>
// {/* 状态条 */}
// {(data?.progress !== "0%" || statusText) && (
// <div className="w-full mt-4 bg-gray-200 h-6">
// <div
// className="bg-blue-500 h-full text-white text-center text-sm flex items-center justify-center transition-all duration-300 px-2 overflow-hidden whitespace-nowrap text-ellipsis"
// style={{ width: progress || "0%" }}
// >
// {loading ? "操作中..." : statusText}
// </div>
// </div>
// )}
// </div>
// </div>
// );
// }
export function DetailPageHeader({ data }: { data: any }) {
const [loading, setLoading] = useState(false);
const [statusText, setStatusText] = useState(data?.statusText || "加载中...");
const [progress, setProgress] = useState(data?.progress || "0%");
const [progressBarColor, setProgressBarColor] = useState("bg-blue-500"); // ✅ 默认蓝色
const [showDelete, setShowDelete] = useState(false);
const [hasWSConnected, setHasWSConnected] = useState(false);
const [statusLoaded, setStatusLoaded] = useState(false);
const socketRef = useRef<WebSocket | null>(null);
const initWebSocket = (userName: string, id: number) => {
if (socketRef.current) socketRef.current.close();
const wsBase = process.env.NEXT_PUBLIC_CLIENT_BASE_WS;
const socket = new WebSocket(`${wsBase}/status/${userName}/${id}`);
socketRef.current = socket;
socket.onopen = () => console.log("WebSocket 已连接");
socket.onmessage = (event) => {
const msg = event.data;
console.log("收到进度信息:", msg);
setStatusText(msg);
const match = msg.match(/进度[:]?\s*(\d+)%/);
if (match && match[1]) {
setProgress(match[1] + "%");
}
setProgressBarColor("bg-blue-500"); // 保持默认颜色
};
socket.onerror = (err) => {
console.error("WebSocket 出错:", err);
setStatusText("WebSocket 出错");
};
socket.onclose = () => {
console.log("🔌 WebSocket 已关闭");
setHasWSConnected(false);
socketRef.current = null;
};
setHasWSConnected(true);
};
const handleClick = async (source: "icon" | "info") => {
setLoading(true);
setStatusText(source === "icon" ? "正在处理图标操作..." : "正在处理信息操作...");
try {
const userData = JSON.parse(localStorage.getItem("UserData") || "null");
if (!userData?.user_name) {
setStatusText("未登录,跳转中...");
window.location.href = "/auth/sign-in/";
return;
}
const userName = userData.user_name;
const id = data?.id;
if (!id) {
setStatusText("缺少组件 ID");
return;
}
const res = await fetch("/api/v1/deploy/deploy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id, user_name: userName }),
});
const json = await res.json();
if (json?.header?.code === 0) {
setStatusText("部署已启动,监听中...");
setProgressBarColor("bg-blue-500");
initWebSocket(userName, id);
} else {
setStatusText(json.header.message || "操作失败(后端)");
}
} catch (err) {
console.error("请求出错:", err);
setStatusText("请求失败");
} finally {
setLoading(false);
}
};
const handleDelete = async () => {
if (loading) return;
const confirmed = window.confirm("确定要删除该部署吗?");
if (!confirmed) return;
setLoading(true);
setStatusText("正在删除部署...");
setProgressBarColor("bg-gray-400");
try {
const userData = JSON.parse(localStorage.getItem("UserData") || "null");
const userName = userData?.user_name;
const id = data?.id;
if (!userName || !id) {
setStatusText("缺少 user_name 或 id");
return;
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10_000); // ⏰ 10秒超时
const res = await fetch("/api/v1/deploy/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
signal: controller.signal,
body: JSON.stringify({ user_name: userName, id }),
});
clearTimeout(timeoutId); // ✅ 成功返回前清除 timeout
if (!res.ok) {
throw new Error(`HTTP 请求失败: ${res.status}`);
}
const json = await res.json();
if (json?.header?.code === 0) {
setStatusText("删除成功");
setProgress("0%");
setShowDelete(false); // 删除成功后隐藏按钮
} else {
setStatusText(json?.header?.message || "删除失败(后端返回错误)");
}
} catch (err) {
if ((err as any).name === "AbortError") {
setStatusText("请求超时超过10秒");
} else {
console.error("删除请求出错:", err);
setStatusText("删除失败,请检查网络");
}
} finally {
setLoading(false);
}
};
useEffect(() => {
const fetchDeployStatus = async () => {
try {
const result = await fetch("/api/v1/deploy/status", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: data?.id }),
}).then((res) => res.json());
console.log("====> deploy status result:", result);
setStatusLoaded(true);
if (!result) {
setStatusText("接口响应为空");
setShowDelete(false);
return;
}
const code = result?.header?.code;
const status = result?.data?.data?.status;
const userData = JSON.parse(localStorage.getItem("UserData") || "null");
const userName = userData?.user_name;
const id = data?.id;
console.log("🟡 状态码 code =", code, "状态 status =", status);
if (code === 1006) {
setStatusText("尚未部署");
setShowDelete(false);
setProgress("0%");
return;
}
if (status === "deploying" && userName && id && !hasWSConnected) {
setStatusText("检测到正在部署,连接中...");
initWebSocket(userName, id);
}
if (status === "running" || status === "stopped") {
console.log("✅ 允许删除status =", status, ")");
setShowDelete(true);
setProgress("100%");
if (status === "running") {
setStatusText("运行中");
setProgressBarColor("bg-green-500");
} else if (status === "stopped") {
setStatusText("已停止");
setProgressBarColor("bg-gray-400");
}
} else {
console.log("❌ 不允许删除status =", status, ")");
setShowDelete(false);
setProgressBarColor("bg-blue-500"); // 回到默认蓝色
}
} catch (err) {
console.error("获取状态失败:", err);
setStatusText("状态拉取失败");
setShowDelete(false);
}
};
fetchDeployStatus();
}, [data?.id]);
useEffect(() => {
return () => {
if (socketRef.current) {
socketRef.current.close();
}
};
}, []);
const isImagePath =
typeof data?.icon === "string" &&
(data.icon.startsWith("http") || data.icon.startsWith("/"));
const resolvedIconSrc =
isImagePath && !data.icon.startsWith("http")
? process.env.NEXT_PUBLIC_CLIENT_IMAGE_URL + data.icon
: data.icon;
return (
<div className="sticky top-0 z-30 bg-white">
<div className="mt-4 mb-1 px-6 lg:px-8 w-11/12 lg:w-2/3 xl:w-3/5 mx-auto">
<div className="flex items-start space-x-6">
<button
className="group flex items-center justify-center w-24 h-24 md:w-32 md:h-32 border transition"
onClick={() => handleClick("icon")}
disabled={loading || data?.status === "running"}
>
{isImagePath ? (
<img
src={resolvedIconSrc}
alt="icon"
className="w-full h-full object-contain transition-all duration-200 group-hover:border-2 group-hover:border-blue-500 group-active:border-2 group-active:border-green-500 group-hover:scale-105 group-active:scale-95"
/>
) : (
data?.icon || "Deploy"
)}
</button>
<div className="flex justify-between flex-1 items-end">
<div className="text-sm leading-7 space-y-1.5">
<div className="flex items-center gap-2">
<BadgeInfo size={16} /> {data?.name || "未命名组件"}
</div>
<div className="flex items-center gap-2">
<Tags size={16} /> {data?.category || "未知"}
</div>
<div className="flex items-center gap-2">
<CalendarClock size={16} /> {data?.updated_at || "未提供"}
</div>
<div className="flex items-center gap-2">
<Building2 size={16} /> {data?.company || "未知公司"}
</div>
</div>
{statusLoaded && showDelete && (
<button
onClick={handleDelete}
className="hover:text-gray-700 transition self-end"
disabled={loading}
title="删除"
>
<Trash2 size={20} />
</button>
)}
</div>
</div>
{(progress !== "0%" || statusText) && (
<div className="w-full mt-4 bg-gray-200 h-6">
<div
className={`${progressBarColor} h-full text-white text-center text-sm flex items-center justify-center transition-all duration-300 px-2 overflow-hidden whitespace-nowrap text-ellipsis`}
style={{ width: progress || "0%" }}
>
{loading ? "操作中..." : statusText}
</div>
</div>
)}
</div>
</div>
);
}
// export function Header() {
// const router = useRouter();
// const { t } = useTranslation();
// const [userData, setUserData] = useLocalStorage(
// 'UserData',
// {
// auth_token: "",
// id: 1,
// login_ip: "",
// login_time: 0,
// role: "",
// user_name: "",
// version: ""
// } as UserData
// )
// const soonFunc = () => {
// message.info(t("soon"))
// }
// return (
// <header className="sticky top-0 z-50 flex w-full shrink-0 items-center justify-between bg-background px-4 py-[1.5rem]">
// {/* <MobileLinks className="lg:hidden" />
// <DesktopLinks className="hidden lg:flex" /> */}
// <div className="lg:hidden" ></div>
// <div className="hidden lg:flex" >
// {/* <Button variant="ghost" className='text-base font-bold hover:bg-[#e5e7eb]' onClick={soonFunc}>
// {t("header.course")}
// </Button>
// <Button variant="ghost" className='text-base font-bold hover:bg-[#e5e7eb]' onClick={soonFunc}>
// {t("header.models")}
// </Button>
// <Button variant="ghost" className='text-base font-bold hover:bg-[#e5e7eb]' onClick={soonFunc}>
// {t("header.resources")}
// </Button> */}
// </div>
// {/* <Image src={logoImage} height={40} alt="show" className="cursor-pointer" onClick={() => {
// router.push("/")
// // toast.success('coming soon')
// }} /> */}
// <LogoAI
// className='flex text-center m-auto'
// />
// <div className="flex items-center justify-between ">
// <div className="flex items-center ">
// {/* <IconSeparator className="size-6 text-muted-foreground/50" /> */}
// {(!!userData && !!userData.user_name) ? (
// <div className='flex text-[#1A1A1A]'>
// {/* <Button variant="ghost" className='text-base bg-[#f4f4f5] hover:bg-[#e5e7eb]'
// onClick={() => {
// router.push("/subscribe")
// }}
// > {t("subscribe.subscribe")}</Button> */}
// <UserMenu user={userData} />
// </div>
// ) : (
// <div className='flex gap-4 grid-cols-2 text-[#1A1A1A]'>
// <Button variant="ghost" className='text-base bg-[#f4f4f5] hover:bg-[#e5e7eb]'>
// <Link href="/#subscribe-target"
// >
// {t('subscribe.subscribe')}
// </Link>
// </Button>
// <LoginButton
// variant="ghost"
// // variant="link"
// showGithubIcon={true}
// text={t('login')}
// className="-ml-2 text-base bg-[#f4f4f5] hover:bg-[#e5e7eb]"
// />
// </div>
// )}
// </div>
// </div>
// </header>
// )
// }
export function NavBack() {
const router = useRouter();
const { t } = useTranslation();
// const query = router.query; // 假设页面 URL 是 /mypage?id=123
console.log("query", router)
return (
<nav className="fixed top-[1rem] container flex items-center justify-between " >
<Link href="/">
{t("home")}
</Link>
</nav>
)
}
export function TimeP({
date,
}: {
date: string
}) {
const router = useRouter();
const { t } = useTranslation();
return (
<p className=" text-left text-[1rem] text-[#808080]">{date} {t("JellyAI")}</p>
)
}