hts/apps/blogai/components/header.tsx

562 lines
17 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 { getRuntimeEnv } from "@/lib/ipconfig";
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>
)
}
export async function getWsBase() {
const ip = await getRuntimeEnv("SUPABASE_URL");
if (!ip) throw new Error("SUPABASE_URL 获取失败,无法构建 wsBase");
// 判断协议
let wsProtocol = "ws";
if (
typeof window !== "undefined" &&
window.location &&
window.location.protocol === "https:"
) {
wsProtocol = "wss";
// ✅ HTTPS + 浏览器环境下用 hostname 避免 TLS 报错
ip = window.location.hostname;
}
// 拼接最终 ws 地址
return `${wsProtocol}://${ip}/api/v1/deploy/ws`;
}
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 [showProgressBar, setShowProgressBar] = useState(false);
const [showDelete, setShowDelete] = useState(false);
const [hasWSConnected, setHasWSConnected] = useState(false);
const [statusLoaded, setStatusLoaded] = useState(false);
const [canDeploy, setCanDeploy] = useState(true);
const socketRef = useRef<WebSocket | null>(null);
const { t } = useTranslation();
const initWebSocket = async (userName: string, id: number) => {
if (socketRef.current) socketRef.current.close();
//const wsBase = process.env.NEXT_PUBLIC_CLIENT_BASE_WS;
const wsBase = await getWsBase();
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 percentMatch = msg.match(/(\d+)%/);
if (percentMatch && percentMatch[1]) {
setProgress(percentMatch[1] + "%");
if (percentMatch[1] === "100") {
console.log("部署完成 ✅,重新拉取状态!");
fetchDeployStatus();
}
}
setProgressBarColor("bg-blue-500"); // 保持默认颜色
};
socket.onerror = (err) => {
console.error("WebSocket 出错:", err);
setStatusText(t("deploy.ws_error"));
};
socket.onclose = () => {
console.log("🔌 WebSocket 已关闭");
setHasWSConnected(false);
socketRef.current = null;
};
setHasWSConnected(true);
};
const handleClick = async (source: "icon" | "info") => {
setLoading(true);
setStatusText(source === "icon" ? "正在处理图标操作..." : "正在处理信息操作...");
setShowProgressBar(true)
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(t("deploy.confirm_delete"));
if (!confirmed) return;
setLoading(true);
setStatusText(t("deploy.deleting"));
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(t("deploy.missing_user_or_id"));
return;
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 60_000); // ⏰ 60秒超时
const res = await fetch("/api/v1/deploy/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
signal: controller.signal,
body: JSON.stringify({ user_name: userName, n_id: id }),
});
clearTimeout(timeoutId); // ✅ 成功返回前清除 timeout
if (!res.ok) {
throw new Error(`HTTP 请求失败: ${res.status}`);
}
const json = await res.json();
if (json?.header?.code === 0) {
setStatusText(t("deploy.deletion_success"));
setProgress("0%");
setShowDelete(false); // 删除成功后隐藏按钮
await fetchDeployStatus(); // ✅ 删除后刷新真实状态
} else {
setStatusText(json?.header?.message || "删除失败(后端返回错误)");
}
} catch (err) {
if ((err as any).name === "AbortError") {
setStatusText(t("deploy.timeout"));
} else {
console.error("删除请求出错:", err);
setStatusText(t("deploy.deletion_failed_network"));
}
} finally {
setLoading(false);
}
};
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(t("deploy.empty_response"));
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(t("deploy.not_deployed"));
setShowDelete(false);
setProgress("0%");
setShowProgressBar(false);
setCanDeploy(true); // ✅ 允许部署
return;
}
if (status === "deploying" && userName && id && !hasWSConnected) {
setStatusText(t("deploy.connecting"));
initWebSocket(userName, id);
setShowProgressBar(true);
setCanDeploy(false); // ✅ 正在部署中,禁止点击
}
if (status === "running" || status === "stopped") {
console.log("✅ 允许删除status =", status, ")");
setShowDelete(true);
setProgress("100%");
setShowProgressBar(true);
setCanDeploy(false); // ✅ 已部署/已停止,不允许再次 deploy
if (status === "running") {
setStatusText(t("deploy.running"));
setProgressBarColor("bg-green-500");
} else if (status === "stopped") {
setStatusText(t("deploy.stopped"));
setProgressBarColor("bg-gray-400");
}
} else {
console.log("❌ 不允许删除status =", status, ")");
setShowDelete(false);
setProgressBarColor("bg-blue-500");
}
} catch (err) {
console.error("获取状态失败:", err);
setStatusText(t("deploy.fetch_failed"));
setShowDelete(false);
}
};
useEffect(() => {
fetchDeployStatus();
}, [data?.id]);
// 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%");
// setShowProgressBar(false)
// return;
// }
// if (status === "deploying" && userName && id && !hasWSConnected) {
// setStatusText("检测到正在部署,连接中...");
// initWebSocket(userName, id);
// setShowProgressBar(true)
// }
// if (status === "running" || status === "stopped") {
// console.log("✅ 允许删除status =", status, ")");
// setShowDelete(true);
// setProgress("100%");
// setShowProgressBar(true)
// 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"
className={`group flex items-center justify-center w-24 h-24 md:w-32 md:h-32 border transition ${(!data?.model_parameter) ? 'opacity-50 cursor-not-allowed' : ''}`}
onClick={() => handleClick("icon")}
disabled={loading || !canDeploy || !data?.model_parameter}
>
{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>
{showProgressBar && (progress !== "0%" || statusText) && (
<div className="relative w-full bg-gray-200 h-6 rounded overflow-hidden">
{/* 蓝色进度条 */}
<div
className={`${progressBarColor} h-full transition-all duration-300`}
style={{ width: typeof progress === "string" ? progress : `${progress}%` }}
/>
{/* 浮动在中间的文字层 */}
<div className="absolute inset-0 flex items-center justify-center text-sm font-medium text-white">
{loading ? t("deploy.processing") : statusText}
</div>
</div>
)}
</div>
</div>
);
}
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>
)
}