This commit is contained in:
hailin 2025-06-23 20:59:33 +08:00
parent 479852fff0
commit c7c6561bc9
3 changed files with 688 additions and 8 deletions

View File

@ -0,0 +1,682 @@
'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;
}
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 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);
}
};
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>
)}
{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 res = await fetch("https://ai.szaiai.com/api/v1/cloud/download", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
date: 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>
)
}

View File

@ -32,6 +32,9 @@ const nextConfig = {
// Warning: This allows production builds to successfully complete even if
// your project has ESLint errors.
ignoreDuringBuilds: true,
},
experimental: {
esmExternals: true,
},
images: {
remotePatterns: [

View File

@ -8,12 +8,7 @@
],
"allowJs": true,
"skipLibCheck": true,
//"strict": true,
//
"strict": false,
"noImplicitAny": false,
"strictNullChecks": false,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
@ -40,7 +35,7 @@
"name": "next"
}
],
//"strictNullChecks": true
"strictNullChecks": true
},
"include": [
"next-env.d.ts",
@ -48,7 +43,7 @@
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
, "auth.ts" ],
],
"exclude": [
"test",
"node_modules"