hts/apps/blogai/components/header.tsx

768 lines
25 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 { 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, CloudDownload } 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"))
}
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>
)
}
import { getRuntimeEnv } from "@/lib/ipconfig";
export async function getWsBase() {
let ip = await getRuntimeEnv("SUPABASE_URL");
if (!ip) throw new Error("SUPABASE_URL 获取失败,无法构建 wsBase");
let wsProtocol = "ws";
if (typeof window !== "undefined") {
// ✅ 客户端环境(浏览器执行)
if (window.location.protocol === "https:") {
wsProtocol = "wss";
ip = window.location.hostname; // ✅ 用 hostname 避免 HTTPS + IP 的 TLS 报错
}
} else {
// ✅ SSR 环境,延迟导入 headers不能放顶层
const { headers } = await import("next/headers");
const hdrs = headers();
const forwardedProto = hdrs.get("x-forwarded-proto");
const hostHeader = hdrs.get("host");
if (forwardedProto === "https") {
wsProtocol = "wss";
}
if (hostHeader) {
// ✅ host 可能带端口,需处理
ip = hostHeader.includes(":") ? hostHeader.split(":")[0] : hostHeader;
}
}
const finalUrl = `${wsProtocol}://${ip}/api/v1/deploy/ws`;
console.log("✅ [WebSocket] Final URL:", finalUrl);
return finalUrl;
}
function getApiBaseUrl(): string {
if (typeof window === "undefined" || !window.location) {
if (process.env.NODE_ENV === "development") {
console.error("❌ getApiBaseUrl 被错误地调用于非浏览器环境");
}
throw new Error("❌ getApiBaseUrl 必须在浏览器端调用");
}
return `${window.location.protocol}//${window.location.host}`;
}
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 [currentStatus, setCurrentStatus] = useState(""); // 当前部署状态running / stopped
const [switchLoading, setSwitchLoading] = useState(false); // 控制按钮 loading 状态
const [downloadPercent, setDownloadPercent] = useState("0"); // 下载百分比0 ~ 100
const [showDownloadBar, setShowDownloadBar] = useState(false);
const [newVersionLoading, setNewVersionLoading] = useState(false);
const socketRef = useRef<WebSocket | null>(null);
const { t } = useTranslation();
console.log("...........model_parameter =", data?.model_parameter);
// const hasNonEmptyExtraData = data?.extra_data && typeof data.extra_data === 'object' && !Array.isArray(data.extra_data) && Object.keys(data.extra_data).length > 0 data?.model_parameter !== 0 &&
// data?.model_parameter !== "";
const hasNonEmptyExtraData =
data?.extra_data &&
typeof data.extra_data === 'object' &&
!Array.isArray(data.extra_data) &&
Object.keys(data.extra_data).length > 0 &&
data?.model_parameter !== 0 &&
data?.model_parameter !== "";
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;
setCurrentStatus(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);
}
};
const handleSwitchStatus = async () => {
if (!data?.id) return;
setSwitchLoading(true);
const userData = JSON.parse(localStorage.getItem("UserData") || "null");
const userName = userData?.user_name;
const id = data?.id;
if (!userName || !id) {
setStatusText("缺少组件 ID 或用户信息");
return;
}
if (!userName) {
setStatusText("未登录,跳转中...");
window.location.href = "/auth/sign-in/";
return;
}
const endpoint = currentStatus === "running" ? "/api/v1/deploy/stop" : "/api/v1/deploy/start";
try {
const res = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_name: userName, n_id: id }),
});
const json = await res.json();
if (json?.header?.code === 0) {
setStatusText(currentStatus === "running" ? "已停止" : "已启动");
fetchDeployStatus(); // ✅ 状态切换成功后刷新
} else {
setStatusText(json?.header?.message || "操作失败(后端)");
}
} catch (err) {
console.error("切换请求失败:", err);
setStatusText("网络异常");
} finally {
setSwitchLoading(false);
}
};
//软件更新
const handleDownloadNewVersion = async () => {
const download_url = data?.extra_data?.download_url;
const callback_url = data?.extra_data?.callback_url;
const digest = data?.extra_data?.digest;
const version = data?.extra_data?.version;
const size = data?.extra_data?.size;
const date1 = data?.extra_data?.date;
const id = data?.org_id;
if (!download_url || !size || !id) {
message.warning("缺少必要的下载参数");
return;
}
setNewVersionLoading(true);
setShowDownloadBar(true); // 复用原进度条
try {
const apiHost = getApiBaseUrl(); // ✅ 会自动校验是否在客户端
// const res = await fetch("https://ai.szaiai.com/api/v1/cloud/newversion", {
const res = await fetch(`${apiHost}/api/v1/cloud/download`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
date: String(date1),
url: download_url,
callback_url,
digest,
version,
size,
id,
}),
});
const result = await res.json();
if (result?.header?.code === 0) {
const percent = (parseFloat(result?.data?.percent ?? "0") * 100).toFixed(1);
setDownloadPercent(percent);
message.success(`📦 更新进度:${percent}%`);
if (percent === "100.0") message.success("🎉 新版本下载完成!");
} else {
message.warning(`❌ 更新失败:${result?.header?.message || "未知错误"}`);
}
} catch (err) {
console.error(err);
message.error("请求出错,无法更新");
} finally {
setNewVersionLoading(false);
}
};
useEffect(() => {
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>
<div className="flex items-center gap-[5px] self-end">
{hasNonEmptyExtraData && (
// <button
// onClick={() => {
// console.log("🟢 点击了更新按钮extra_data = ", data.extra_data);
// // TODO: 后续处理逻辑写在这里
// }}
// className="hover:text-gray-700 transition self-end text-sm border border-gray-300 rounded px-2 py-1 bg-white"
// title="更新"
// >
// 🔄 更新
// </button>
<div className="flex flex-col items-center gap-[2px]">
<button
onClick={handleDownloadNewVersion}
disabled={newVersionLoading || loading}
className="hover:text-gray-700 transition self-end text-sm border border-gray-300 rounded px-2 py-1 bg-white mt-1"
title="下载并安装新版本"
>
{newVersionLoading ? "处理中..." : "⬇️ 更新"}
</button>
{showDownloadBar && (
<div className="w-full h-[4px] bg-gray-200 rounded overflow-hidden mt-1">
<div
className="h-full bg-blue-500 transition-all duration-300"
style={{ width: `${downloadPercent}%` }}
/>
</div>
)}
</div>
)}
{statusLoaded && (currentStatus === "running" || currentStatus === "stopped") && (
<button
onClick={handleSwitchStatus}
disabled={switchLoading || loading}
className="hover:text-gray-700 transition self-end text-sm border border-gray-300 rounded px-2 py-1 bg-white"
title={currentStatus === "running" ? "停止运行" : "启动运行"}
>
{switchLoading
? "处理中..."
: currentStatus === "running"
? "⏹ 停止"
: "▶️ 启动"}
</button>
)}
{statusLoaded && showDelete && (
<button
onClick={handleDelete}
className="hover:text-gray-700 transition self-end"
disabled={loading}
title="删除"
>
<Trash2 size={20} />
</button>
)}
{!data?.model_parameter && (
<div className="flex flex-col items-center gap-[2px]">
<button
onClick={async () => {
try {
const download_url = data?.extra_data?.download_url;
const callback_url = data?.extra_data?.callback_url;
const digest = data?.extra_data?.digest;
const version = data?.extra_data?.version;
const size = data?.extra_data?.size;
const date1 = data?.extra_data?.date;
const id = data?.org_id;
if (!download_url || !size || !id) {
message.warning("缺少必要的下载参数");
return;
}
console.log("📥 调用下载接口...", { url: download_url, size, id });
const apiHost = getApiBaseUrl(); // ✅ 会自动校验是否在客户端
//const res = await fetch("https://ai.szaiai.com/api/v1/cloud/download", {
const res = await fetch(`${apiHost}/api/v1/cloud/download`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
date: String(date1),
url: download_url,
callback_url: callback_url,
digest: digest,
version: version,
size: size,
id: id,
}),
});
const result = await res.json();
console.log("✅ 下载接口返回:", result);
if (result?.header?.code === 0) {
const percentStr = result?.data?.percent ?? "0";
const percentNum = parseFloat(percentStr);
const percent = (percentNum * 100).toFixed(1);
setDownloadPercent(percent);
setShowDownloadBar(true);
message.success(`📦 下载进度:${percent}%`);
} else {
message.warning(`❌ 下载失败:${result?.header?.message || "未知错误"}`);
}
} catch (err) {
console.error("❌ 下载请求异常:", err);
message.error("请求出错,无法下载");
}
}}
className="hover:text-gray-700 transition self-end"
title="下载模型文件"
>
<CloudDownload size={20} />
</button>
{showDownloadBar && (
<div className="w-[20px] h-[4px] bg-gray-200 rounded overflow-hidden">
<div
className="h-full bg-blue-500 transition-all duration-300"
style={{ width: `${downloadPercent}%` }}
/>
</div>
)}
</div>
)}
</div>
</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>
)
}