fix: ine error
This commit is contained in:
@@ -7,7 +7,13 @@ import a11yLint from "eslint-plugin-jsx-a11y";
|
||||
import nextLint from "@next/eslint-plugin-next";
|
||||
|
||||
export default tsLint.config(
|
||||
{ ignores: ["src/components/ui", "src/components/magicui"] },
|
||||
{
|
||||
ignores: [
|
||||
"src/components/ui",
|
||||
"src/components/magicui",
|
||||
"src/types/openapi.d.ts",
|
||||
],
|
||||
},
|
||||
{
|
||||
extends: [jsLint.configs.recommended, ...tsLint.configs.recommended],
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
@@ -23,7 +29,7 @@ export default tsLint.config(
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
"no-console": "error",
|
||||
"no-console": "warn",
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true },
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
OPENAPI_URL=https://data.tangledup-ai.com/openapi.json;
|
||||
ossBase = 'https://tangledup-ai-staging.oss-cn-shanghai.aliyuncs.com/';
|
||||
|
||||
openapi-typescript "$OPENAPI_URL" \
|
||||
--root-types=true \
|
||||
|
||||
@@ -2,14 +2,22 @@
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { FileText } from "lucide-react";
|
||||
import { industryChainData, PDFFile, IndustryChain } from "../data/industryChains";
|
||||
import {
|
||||
industryChainData,
|
||||
type PDFFile,
|
||||
type IndustryChain,
|
||||
} from "../data/industryChains";
|
||||
import { toInlinePdfUrl } from "@/lib/oss";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export const IndustryChainList = () => {
|
||||
const [selectedChain, setSelectedChain] = useState<IndustryChain | null>(industryChainData[0]);
|
||||
const [selectedChain, setSelectedChain] = useState<IndustryChain | null>(
|
||||
industryChainData[0]
|
||||
);
|
||||
const [loadingChains, setLoadingChains] = useState<Set<string>>(new Set());
|
||||
const [visiblePdfFiles, setVisiblePdfFiles] = useState<Set<string>>(new Set());
|
||||
const [visiblePdfFiles, setVisiblePdfFiles] = useState<Set<string>>(
|
||||
new Set()
|
||||
);
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||
const loadMoreRef = useRef<HTMLDivElement>(null);
|
||||
@@ -17,25 +25,25 @@ export const IndustryChainList = () => {
|
||||
const handleChainClick = (chain: IndustryChain) => {
|
||||
setSelectedChain(chain);
|
||||
setVisiblePdfFiles(new Set()); // 重置可见的PDF文件
|
||||
setLoadingChains(prev => new Set(prev).add(chain.id));
|
||||
|
||||
setLoadingChains((prev) => new Set(prev).add(chain.id));
|
||||
|
||||
// 模拟数据加载延迟
|
||||
setTimeout(() => {
|
||||
setLoadingChains(prev => {
|
||||
setLoadingChains((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(chain.id);
|
||||
return newSet;
|
||||
});
|
||||
|
||||
|
||||
// 初始显示前5个PDF文件
|
||||
const initialFiles = chain.pdfFiles.slice(0, 5).map(f => f.id);
|
||||
const initialFiles = chain.pdfFiles.slice(0, 5).map((f) => f.id);
|
||||
setVisiblePdfFiles(new Set(initialFiles));
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const handlePDFClick = (pdfFile: PDFFile) => {
|
||||
const url = toInlinePdfUrl(pdfFile.filePath);
|
||||
window.open(url, '_blank');
|
||||
window.open(url, "_blank");
|
||||
};
|
||||
|
||||
// 懒加载更多PDF文件
|
||||
@@ -62,20 +70,23 @@ export const IndustryChainList = () => {
|
||||
|
||||
const loadMorePdfFiles = () => {
|
||||
if (!selectedChain || isLoadingMore) return;
|
||||
|
||||
|
||||
const currentVisibleCount = visiblePdfFiles.size;
|
||||
const totalFiles = selectedChain.pdfFiles.length;
|
||||
|
||||
|
||||
if (currentVisibleCount >= totalFiles) return;
|
||||
|
||||
|
||||
setIsLoadingMore(true);
|
||||
|
||||
|
||||
// 模拟加载延迟
|
||||
setTimeout(() => {
|
||||
const nextBatch = selectedChain.pdfFiles.slice(currentVisibleCount, currentVisibleCount + 5);
|
||||
const nextBatch = selectedChain.pdfFiles.slice(
|
||||
currentVisibleCount,
|
||||
currentVisibleCount + 5
|
||||
);
|
||||
const newVisibleFiles = new Set(visiblePdfFiles);
|
||||
nextBatch.forEach(file => newVisibleFiles.add(file.id));
|
||||
|
||||
nextBatch.forEach((file) => newVisibleFiles.add(file.id));
|
||||
|
||||
setVisiblePdfFiles(newVisibleFiles);
|
||||
setIsLoadingMore(false);
|
||||
}, 500);
|
||||
@@ -97,13 +108,16 @@ export const IndustryChainList = () => {
|
||||
key={chain.id}
|
||||
className={`whitespace-nowrap text-base font-bold px-4 py-2 rounded-lg cursor-pointer transition-colors border-b-2 ${
|
||||
isSelected
|
||||
? 'text-[#BD1A2D] border-[#BD1A2D] bg-red-50'
|
||||
: 'text-black border-transparent bg-white hover:text-[#BD1A2D] hover:bg-gray-50'
|
||||
? "text-[#BD1A2D] border-[#BD1A2D] bg-red-50"
|
||||
: "text-black border-transparent bg-white hover:text-[#BD1A2D] hover:bg-gray-50"
|
||||
}`}
|
||||
onClick={() => handleChainClick(chain)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') handleChainClick(chain); }}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ")
|
||||
handleChainClick(chain);
|
||||
}}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -122,11 +136,13 @@ export const IndustryChainList = () => {
|
||||
{selectedChain ? (
|
||||
<div>
|
||||
<div className="mb-4 pb-2 border-b border-gray-300">
|
||||
<h3 className="text-lg font-bold text-gray-800 mb-1">{selectedChain.name}</h3>
|
||||
<h3 className="text-lg font-bold text-gray-800 mb-1">
|
||||
{selectedChain.name}
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm">
|
||||
{selectedChain.pdfFiles.length > 0
|
||||
? `共 ${selectedChain.pdfFiles.length} 个招商项目文档`
|
||||
: '暂无项目文档'}
|
||||
: "暂无项目文档"}
|
||||
</p>
|
||||
</div>
|
||||
{selectedChain.pdfFiles.length > 0 ? (
|
||||
@@ -138,7 +154,10 @@ export const IndustryChainList = () => {
|
||||
onClick={() => handlePDFClick(pdfFile)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') handlePDFClick(pdfFile); }}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ")
|
||||
handlePDFClick(pdfFile);
|
||||
}}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center group-hover:bg-red-200 transition-colors">
|
||||
@@ -147,14 +166,18 @@ export const IndustryChainList = () => {
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs font-medium text-gray-500">项目 {index + 1}</span>
|
||||
<span className="text-xs font-medium text-gray-500">
|
||||
项目 {index + 1}
|
||||
</span>
|
||||
</div>
|
||||
<h4 className="text-base font-semibold text-gray-800 group-hover:text-[#BD1A2D] transition-colors line-clamp-2">
|
||||
{pdfFile.name}
|
||||
</h4>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<div className="text-xs text-gray-500 group-hover:text-[#BD1A2D] transition-colors">点击预览 →</div>
|
||||
<div className="text-xs text-gray-500 group-hover:text-[#BD1A2D] transition-colors">
|
||||
点击预览 →
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -162,8 +185,12 @@ export const IndustryChainList = () => {
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<FileText className="w-12 h-12 text-gray-400 mx-auto mb-2" />
|
||||
<h4 className="text-base font-medium text-gray-600 mb-1">暂无项目文档</h4>
|
||||
<p className="text-gray-500 text-xs">该产业链暂时没有可用的招商项目文档</p>
|
||||
<h4 className="text-base font-medium text-gray-600 mb-1">
|
||||
暂无项目文档
|
||||
</h4>
|
||||
<p className="text-gray-500 text-xs">
|
||||
该产业链暂时没有可用的招商项目文档
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -171,27 +198,32 @@ export const IndustryChainList = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* PC端布局 - 类似优化 */}
|
||||
<div className="hidden md:flex flex-col md:flex-row w-full justify-center items-center">
|
||||
{/* 左侧产业链列表 */}
|
||||
<div className={`w-full md:w-[300px] pl-0 md:pl-12 pt-6 md:pt-9 flex-shrink-0 h-[750px] flex justify-center`}>
|
||||
<div
|
||||
className={`w-full md:w-[300px] pl-0 md:pl-12 pt-6 md:pt-9 flex-shrink-0 h-[750px] flex justify-center`}
|
||||
>
|
||||
<div className={`space-y-3 h-full overflow-y-auto pr-2`}>
|
||||
{industryChainData.map((chain) => {
|
||||
const isSelected = selectedChain?.id === chain.id;
|
||||
const isLoading = loadingChains.has(chain.id);
|
||||
return (
|
||||
<div
|
||||
<div
|
||||
key={chain.id}
|
||||
className={`text-xl font-bold leading-6 cursor-pointer transition-colors py-3 px-4 rounded-lg ${
|
||||
isSelected
|
||||
? 'text-[#BD1A2D] bg-red-50 border-l-4 border-[#BD1A2D]'
|
||||
: 'text-black hover:text-[#BD1A2D] hover:bg-gray-50'
|
||||
isSelected
|
||||
? "text-[#BD1A2D] bg-red-50 border-l-4 border-[#BD1A2D]"
|
||||
: "text-black hover:text-[#BD1A2D] hover:bg-gray-50"
|
||||
}`}
|
||||
onClick={() => handleChainClick(chain)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') handleChainClick(chain); }}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ")
|
||||
handleChainClick(chain);
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
{isLoading ? (
|
||||
@@ -208,9 +240,11 @@ export const IndustryChainList = () => {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 右侧PDF文件列表 - 类似移动端优化 */}
|
||||
<div className={`flex-1 w-full md:w-[70%] pl-0 md:pl-8 pr-0 md:pr-12 mt-7 h-[750px] flex justify-center`}>
|
||||
<div
|
||||
className={`flex-1 w-full md:w-[70%] pl-0 md:pl-8 pr-0 md:pr-12 mt-7 h-[750px] flex justify-center`}
|
||||
>
|
||||
<div className="w-full h-full bg-[#F2F2F2] rounded-lg p-4 md:p-6 overflow-y-auto">
|
||||
{selectedChain ? (
|
||||
<div>
|
||||
@@ -219,23 +253,25 @@ export const IndustryChainList = () => {
|
||||
{selectedChain.name}
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
{selectedChain.pdfFiles.length > 0
|
||||
? `共 ${selectedChain.pdfFiles.length} 个招商项目文档`
|
||||
: '暂无项目文档'
|
||||
}
|
||||
{selectedChain.pdfFiles.length > 0
|
||||
? `共 ${selectedChain.pdfFiles.length} 个招商项目文档`
|
||||
: "暂无项目文档"}
|
||||
</p>
|
||||
</div>
|
||||
{/* PDF文件列表 */}
|
||||
{selectedChain.pdfFiles.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{selectedChain.pdfFiles.map((pdfFile, index) => (
|
||||
<div
|
||||
<div
|
||||
key={pdfFile.id}
|
||||
className="flex items-center gap-4 p-4 bg-white rounded-lg shadow-sm cursor-pointer hover:shadow-md hover:bg-gray-50 transition-all group border border-gray-200"
|
||||
onClick={() => handlePDFClick(pdfFile)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') handlePDFClick(pdfFile); }}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ")
|
||||
handlePDFClick(pdfFile);
|
||||
}}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center group-hover:bg-red-200 transition-colors">
|
||||
@@ -263,8 +299,12 @@ export const IndustryChainList = () => {
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<FileText className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
||||
<h4 className="text-lg font-medium text-gray-600 mb-2">暂无项目文档</h4>
|
||||
<p className="text-gray-500">该产业链暂时没有可用的招商项目文档</p>
|
||||
<h4 className="text-lg font-medium text-gray-600 mb-2">
|
||||
暂无项目文档
|
||||
</h4>
|
||||
<p className="text-gray-500">
|
||||
该产业链暂时没有可用的招商项目文档
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -273,8 +313,12 @@ export const IndustryChainList = () => {
|
||||
<div className="space-y-4">
|
||||
<FileText className="w-20 h-20 text-gray-400 mx-auto" />
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-gray-600 mb-2">产业链项目文档</h3>
|
||||
<p className="text-gray-500">请从左侧选择一个产业链查看相关项目文档</p>
|
||||
<h3 className="text-xl font-bold text-gray-600 mb-2">
|
||||
产业链项目文档
|
||||
</h3>
|
||||
<p className="text-gray-500">
|
||||
请从左侧选择一个产业链查看相关项目文档
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -285,4 +329,4 @@ export const IndustryChainList = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "@/styles/globals.css";
|
||||
import { PropsWithChildren } from "react";
|
||||
import { Metadata, Viewport } from "next";
|
||||
import type { PropsWithChildren } from "react";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
|
||||
import { Providers } from "./providers";
|
||||
|
||||
@@ -31,15 +31,9 @@ export default async function RootLayout({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<html suppressHydrationWarning lang="zh">
|
||||
<head />
|
||||
<body
|
||||
className={cn(
|
||||
"overflow-hidden",
|
||||
"min-h-dvh text-foreground bg-background font-sans antialiased",
|
||||
fontSans.variable
|
||||
)}
|
||||
>
|
||||
<body className={cn("max-h-svh overflow-hidden", fontSans.variable)}>
|
||||
<Providers>
|
||||
<div className="relative flex flex-col h-dvh">
|
||||
<div className="relative flex flex-col">
|
||||
<Navbar />
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,11 @@ export default function Home() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
isValidAgentId ? queryAgent(agentIdByEnv) : resetAgent();
|
||||
if (isValidAgentId) {
|
||||
queryAgent(agentIdByEnv);
|
||||
} else {
|
||||
resetAgent();
|
||||
}
|
||||
}, [isValidAgentId]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,8 +3,7 @@ import { Avatar, Button, Textarea } from "@heroui/react";
|
||||
import { Copy } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { DifyMessage } from "@/lib/useDifyCmd";
|
||||
import xiaohongAvatar from "@/assets/image/xiaohong-avatar.png";
|
||||
import type { DifyMessage } from "@/lib/useDifyCmd";
|
||||
|
||||
type Props = {
|
||||
msg: DifyMessage;
|
||||
|
||||
@@ -14,10 +14,12 @@ function MobileAIVideoPlayer({
|
||||
"https://tangledup-ai-staging.oss-cn-shanghai.aliyuncs.com/uploads/honghe-acfic-chat/public/video/kx1.mp4",
|
||||
"https://tangledup-ai-staging.oss-cn-shanghai.aliyuncs.com/uploads/honghe-acfic-chat/public/video/kx3.mp4",
|
||||
"https://tangledup-ai-staging.oss-cn-shanghai.aliyuncs.com/uploads/honghe-acfic-chat/public/video/kx4.mp4",
|
||||
"https://tangledup-ai-staging.oss-cn-shanghai.aliyuncs.com/uploads/honghe-acfic-chat/public/video/kx5.mp4"
|
||||
"https://tangledup-ai-staging.oss-cn-shanghai.aliyuncs.com/uploads/honghe-acfic-chat/public/video/kx5.mp4",
|
||||
];
|
||||
|
||||
const [videoSrc, setVideoSrc] = useState("https://tangledup-ai-staging.oss-cn-shanghai.aliyuncs.com/uploads/honghe-acfic-chat/public/video/jz.mp4");
|
||||
|
||||
const [videoSrc, setVideoSrc] = useState(
|
||||
"https://tangledup-ai-staging.oss-cn-shanghai.aliyuncs.com/uploads/honghe-acfic-chat/public/video/jz.mp4"
|
||||
);
|
||||
const [isClicked, setIsClicked] = useState(false);
|
||||
const [isVideoLoaded, setIsVideoLoaded] = useState(false);
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
@@ -29,14 +31,21 @@ function MobileAIVideoPlayer({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log("MobileAIVideoPlayer - isAudioPlaying:", isAudioPlaying, "isClicked:", isClicked);
|
||||
|
||||
console.log(
|
||||
"MobileAIVideoPlayer - isAudioPlaying:",
|
||||
isAudioPlaying,
|
||||
"isClicked:",
|
||||
isClicked
|
||||
);
|
||||
|
||||
// 切换视频时重置加载状态
|
||||
setIsVideoLoaded(false);
|
||||
|
||||
|
||||
if (isClicked) {
|
||||
console.log("设置视频源为: hd.mp4");
|
||||
setVideoSrc("https://tangledup-ai-staging.oss-cn-shanghai.aliyuncs.com/uploads/honghe-acfic-chat/public/video/hd.mp4");
|
||||
setVideoSrc(
|
||||
"https://tangledup-ai-staging.oss-cn-shanghai.aliyuncs.com/uploads/honghe-acfic-chat/public/video/hd.mp4"
|
||||
);
|
||||
const timer = setTimeout(() => {
|
||||
setIsClicked(false);
|
||||
}, 5000);
|
||||
@@ -49,7 +58,9 @@ function MobileAIVideoPlayer({
|
||||
setVideoSrc(randomVideo);
|
||||
} else {
|
||||
console.log("设置视频源为: jz.mp4");
|
||||
setVideoSrc("https://tangledup-ai-staging.oss-cn-shanghai.aliyuncs.com/uploads/honghe-acfic-chat/public/video/jz.mp4");
|
||||
setVideoSrc(
|
||||
"https://tangledup-ai-staging.oss-cn-shanghai.aliyuncs.com/uploads/honghe-acfic-chat/public/video/jz.mp4"
|
||||
);
|
||||
}
|
||||
}, [isAudioPlaying, isClicked]);
|
||||
|
||||
@@ -57,14 +68,13 @@ function MobileAIVideoPlayer({
|
||||
useEffect(() => {
|
||||
const el = videoRef.current;
|
||||
if (!el) return;
|
||||
try {
|
||||
el.setAttribute("playsinline", "true");
|
||||
el.setAttribute("webkit-playsinline", "true");
|
||||
// 腾讯 X5 内核相关属性
|
||||
el.setAttribute("x5-playsinline", "true");
|
||||
el.setAttribute("x5-video-player-type", "h5");
|
||||
el.setAttribute("x5-video-player-fullscreen", "false");
|
||||
} catch {}
|
||||
|
||||
el.setAttribute("playsinline", "true");
|
||||
el.setAttribute("webkit-playsinline", "true");
|
||||
// 腾讯 X5 内核相关属性
|
||||
el.setAttribute("x5-playsinline", "true");
|
||||
el.setAttribute("x5-video-player-type", "h5");
|
||||
el.setAttribute("x5-video-player-fullscreen", "false");
|
||||
}, [videoSrc]);
|
||||
|
||||
const handleVideoClick = () => {
|
||||
@@ -85,7 +95,7 @@ function MobileAIVideoPlayer({
|
||||
setIsVideoLoaded(true);
|
||||
tryPlay();
|
||||
};
|
||||
|
||||
|
||||
const handleCanPlay = () => tryPlay();
|
||||
const handleEnded = () => {
|
||||
el.currentTime = 0;
|
||||
@@ -109,13 +119,13 @@ function MobileAIVideoPlayer({
|
||||
onClick={handleVideoClick}
|
||||
style={{
|
||||
backgroundImage: 'url("/xiaohong.jpg")',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
}}
|
||||
>
|
||||
<video
|
||||
autoPlay
|
||||
className={`w-full h-full object-cover transition-opacity duration-300 ${isVideoLoaded ? 'opacity-100' : 'opacity-0'}`}
|
||||
className={`w-full h-full object-cover transition-opacity duration-300 ${isVideoLoaded ? "opacity-100" : "opacity-0"}`}
|
||||
controls={false}
|
||||
controlsList="nodownload noplaybackrate noremoteplayback nofullscreen"
|
||||
disablePictureInPicture
|
||||
@@ -127,13 +137,13 @@ function MobileAIVideoPlayer({
|
||||
playsInline
|
||||
ref={videoRef}
|
||||
src={videoSrc}
|
||||
style={{
|
||||
style={{
|
||||
pointerEvents: "none",
|
||||
touchAction: "none", // 阻止触摸相关事件
|
||||
overflow: "hidden" // 防止控件溢出显示
|
||||
touchAction: "none", // 阻止触摸相关事件
|
||||
overflow: "hidden", // 防止控件溢出显示
|
||||
}}
|
||||
// 额外添加的属性
|
||||
onContextMenu={(e) => e.preventDefault()} // 阻止右键菜单
|
||||
onContextMenu={(e) => e.preventDefault()} // 阻止右键菜单
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -68,7 +68,7 @@ export function PlaygroundDesktopLayout() {
|
||||
</div> */}
|
||||
|
||||
<div className={cn("w-full h-full", "flex flex-col items-center pt-8")}>
|
||||
<div className="w-full max-w-4xl px-8 pb-24 self-center">
|
||||
<div className="w-full max-w-4xl px-2 pb-24 self-center">
|
||||
{sessionList
|
||||
.filter((session) => session.id === currentSessionId)
|
||||
.at(0)
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ReadyState } from "react-use-websocket";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import useDeviceStore from "@/lib/useDeviceStore";
|
||||
import { RealtimeMessage } from "@/lib/useRealtimeConnEffect";
|
||||
import type { RealtimeMessage } from "@/lib/useRealtimeConnEffect";
|
||||
import useConversationStore from "@/lib/useConversationStore";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PropsWithChildren } from "react";
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
// import useDeviceStore from "@/lib/useDeviceStore";
|
||||
// import useConversationAction from "./useConversationAction";
|
||||
@@ -65,10 +65,10 @@ export default function PlaygroundLayout({ children }: PropsWithChildren) {
|
||||
// hangupConversation();
|
||||
// } catch {}
|
||||
// try {
|
||||
// const status = wavRecorder?.getStatus?.();
|
||||
// const status = wavRecorder.getStatus();
|
||||
// if (status !== "ended") {
|
||||
// // 不等待异步结束,直接触发
|
||||
// wavRecorder?.end?.();
|
||||
// wavRecorder.end();
|
||||
// }
|
||||
// } catch {}
|
||||
// try {
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { addToast, Button } from "@heroui/react";
|
||||
import { Mic, PhoneOff } from "lucide-react";
|
||||
import { Mic } from "lucide-react";
|
||||
import { Visualizer } from "react-sound-visualizer";
|
||||
import { ReadyState } from "react-use-websocket";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import useConversationAction from "./useConversationAction";
|
||||
@@ -13,40 +11,14 @@ import useConversationAction from "./useConversationAction";
|
||||
import useDeviceStore from "@/lib/useDeviceStore";
|
||||
import useConversationStore from "@/lib/useConversationStore";
|
||||
import { useRealtimeCmd } from "@/lib/useRealtimeCmd";
|
||||
|
||||
import { arrayBufferToBase64 } from "@/lib/audioUtils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { MicIcon, ReconnectIcon } from "@/components/icons";
|
||||
|
||||
type VoiceActionGroupProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function VoiceActionGroup({ className }: VoiceActionGroupProps) {
|
||||
const isMobile = useIsMobile();
|
||||
const [windowWidth, setWindowWidth] = useState<number>(0);
|
||||
|
||||
// 检测窗口宽度
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setWindowWidth(window.innerWidth);
|
||||
};
|
||||
|
||||
// 初始设置
|
||||
handleResize();
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 判断是否使用移动端布局:原有的移动端检测 或 窗口宽度小于912px
|
||||
const useMobileLayout = isMobile || windowWidth < 912;
|
||||
|
||||
const { wavRecorder, wavStreamPlayer } = useDeviceStore();
|
||||
const { wsInstance, currentSessionId, sessionList, setCurrentSessionList } =
|
||||
useConversationStore();
|
||||
@@ -62,7 +34,6 @@ export function VoiceActionGroup({ className }: VoiceActionGroupProps) {
|
||||
} = useRealtimeCmd();
|
||||
|
||||
const [isRealTimeActive, setIsRealTimeActive] = useState(false);
|
||||
const [autoStartAttempted, setAutoStartAttempted] = useState(false);
|
||||
|
||||
async function endConversation() {
|
||||
// 标记为手动挂断
|
||||
@@ -75,7 +46,8 @@ export function VoiceActionGroup({ className }: VoiceActionGroupProps) {
|
||||
setIsRealTimeActive(false);
|
||||
|
||||
try {
|
||||
const recorderStatus = wavRecorder?.getStatus();
|
||||
const recorderStatus = wavRecorder.getStatus();
|
||||
|
||||
if (recorderStatus && recorderStatus !== "ended") {
|
||||
await wavRecorder.end();
|
||||
}
|
||||
@@ -89,97 +61,25 @@ export function VoiceActionGroup({ className }: VoiceActionGroupProps) {
|
||||
router.replace("/");
|
||||
}
|
||||
|
||||
// 自动开始实时对话
|
||||
async function autoStartRealTime() {
|
||||
if (isRealTimeActive || autoStartAttempted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAutoStartAttempted(true);
|
||||
|
||||
try {
|
||||
// 检查WebSocket连接状态
|
||||
if (wsInstance?.readyState !== ReadyState.OPEN) {
|
||||
console.log("WebSocket未连接,等待连接...");
|
||||
return;
|
||||
}
|
||||
|
||||
wavStreamPlayer.interrupt();
|
||||
|
||||
const permissions = window.navigator.permissions;
|
||||
|
||||
// 检查麦克风权限
|
||||
if (permissions) {
|
||||
const microphonePermission = await permissions.query({
|
||||
name: "microphone",
|
||||
});
|
||||
|
||||
if (microphonePermission.state === "denied") {
|
||||
addToast({
|
||||
title: "需要麦克风权限才能自动开始对话",
|
||||
description: "请授予麦克风权限后刷新页面",
|
||||
color: "warning",
|
||||
timeout: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保录音器已完全停止
|
||||
const recorderStatus = wavRecorder?.getStatus();
|
||||
if (recorderStatus && recorderStatus !== "ended") {
|
||||
await wavRecorder?.end();
|
||||
}
|
||||
|
||||
// 开始新的录音会话
|
||||
await wavRecorder.begin();
|
||||
setIsRealTimeActive(true);
|
||||
wavRecorder.record(({ mono }) => {
|
||||
const audioBase64 = arrayBufferToBase64(mono);
|
||||
appendUserVoice(audioBase64);
|
||||
});
|
||||
|
||||
addToast({
|
||||
title: "已自动开始实时对话",
|
||||
description: "您可以直接开始说话",
|
||||
color: "success",
|
||||
timeout: 3000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("自动开始实时对话失败:", error);
|
||||
setIsRealTimeActive(false);
|
||||
addToast({
|
||||
title: "自动开始对话失败",
|
||||
description: "请手动点击开始按钮",
|
||||
color: "danger",
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleRealTime() {
|
||||
async function startRealTime() {
|
||||
if (isRealTimeActive) {
|
||||
// 停止实时对话
|
||||
setIsRealTimeActive(false);
|
||||
try {
|
||||
const recorderStatus = wavRecorder?.getStatus();
|
||||
const recorderStatus = wavRecorder.getStatus();
|
||||
|
||||
if (recorderStatus && recorderStatus !== "ended") {
|
||||
await wavRecorder?.end();
|
||||
await wavRecorder.end();
|
||||
}
|
||||
// 清空音频缓冲区
|
||||
clearAudioBuffer();
|
||||
// 取消任何正在进行的响应
|
||||
cancelResponse();
|
||||
} catch (error) {
|
||||
console.error("Error ending recorder:", error);
|
||||
}
|
||||
} else {
|
||||
// 开始实时对话
|
||||
wavStreamPlayer.interrupt();
|
||||
|
||||
const permissions = window.navigator.permissions;
|
||||
|
||||
// 以下判断逻辑,用于对小某米、化某为等垃圾浏览器的 workaround
|
||||
if (permissions) {
|
||||
const microphonePermission = await permissions.query({
|
||||
name: "microphone",
|
||||
@@ -197,13 +97,12 @@ export function VoiceActionGroup({ className }: VoiceActionGroupProps) {
|
||||
}
|
||||
|
||||
try {
|
||||
// 确保录音器已完全停止
|
||||
const recorderStatus = wavRecorder?.getStatus();
|
||||
const recorderStatus = wavRecorder.getStatus();
|
||||
|
||||
if (recorderStatus && recorderStatus !== "ended") {
|
||||
await wavRecorder?.end();
|
||||
await wavRecorder.end();
|
||||
}
|
||||
|
||||
// 开始新的录音会话
|
||||
await wavRecorder.begin();
|
||||
setIsRealTimeActive(true);
|
||||
wavRecorder.record(({ mono }) => {
|
||||
@@ -211,7 +110,6 @@ export function VoiceActionGroup({ className }: VoiceActionGroupProps) {
|
||||
appendUserVoice(audioBase64);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error starting recorder:", error);
|
||||
addToast({
|
||||
title: "录音启动失败",
|
||||
description: "请刷新页面后重试",
|
||||
@@ -271,6 +169,14 @@ export function VoiceActionGroup({ className }: VoiceActionGroupProps) {
|
||||
// }
|
||||
// }, [wsInstance?.readyState]);
|
||||
|
||||
// const micPermissionStatus = await navigator.permissions.query({
|
||||
// name: "microphone",
|
||||
// });
|
||||
|
||||
// if (micPermissionStatus.state === "granted") {
|
||||
// await wavRecorder.begin();
|
||||
// }
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -293,7 +199,7 @@ export function VoiceActionGroup({ className }: VoiceActionGroupProps) {
|
||||
<div className="flex flex-row items-center justify-end gap-2 w-full">
|
||||
<Visualizer
|
||||
autoStart
|
||||
audio={wavRecorder?.stream}
|
||||
audio={wavRecorder.stream}
|
||||
mode="continuous"
|
||||
strokeColor="#fff"
|
||||
>
|
||||
@@ -312,9 +218,9 @@ export function VoiceActionGroup({ className }: VoiceActionGroupProps) {
|
||||
size="md"
|
||||
startContent={<Mic />}
|
||||
variant="flat"
|
||||
onPress={toggleRealTime}
|
||||
onPress={startRealTime}
|
||||
>
|
||||
开始对话
|
||||
点击说话
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PropsWithChildren } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { PropsWithChildren } from "react";
|
||||
import type { useRouter } from "next/navigation";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { HeroUIProvider, ToastProvider } from "@heroui/react";
|
||||
|
||||
@@ -48,7 +48,8 @@ export async function GET(request: Request): Promise<Response> {
|
||||
const referer = request.headers.get("referer") ?? origin;
|
||||
forwardHeaders["origin"] = origin;
|
||||
forwardHeaders["referer"] = referer;
|
||||
forwardHeaders["user-agent"] = request.headers.get("user-agent") ?? "Mozilla/5.0";
|
||||
forwardHeaders["user-agent"] =
|
||||
request.headers.get("user-agent") ?? "Mozilla/5.0";
|
||||
|
||||
const ossResponse = await fetch(target.toString(), {
|
||||
method: "GET",
|
||||
@@ -65,20 +66,24 @@ export async function GET(request: Request): Promise<Response> {
|
||||
|
||||
// Build safe headers for buffered response
|
||||
const responseHeaders = new Headers();
|
||||
const contentType = ossResponse.headers.get("content-type") ?? "application/pdf";
|
||||
const contentType =
|
||||
ossResponse.headers.get("content-type") ?? "application/pdf";
|
||||
responseHeaders.set("content-type", contentType);
|
||||
const acceptRanges = ossResponse.headers.get("accept-ranges") ?? "bytes";
|
||||
responseHeaders.set("accept-ranges", acceptRanges);
|
||||
const contentRange = ossResponse.headers.get("content-range");
|
||||
if (contentRange) responseHeaders.set("content-range", contentRange);
|
||||
const rawFilename = decodeURIComponent(target.pathname.split("/").pop() || "document.pdf");
|
||||
const rawFilename = decodeURIComponent(
|
||||
target.pathname.split("/").pop() || "document.pdf"
|
||||
);
|
||||
const asciiFallback = "document.pdf";
|
||||
const utf8Encoded = encodeRFC5987ValueChars(rawFilename);
|
||||
responseHeaders.set(
|
||||
"content-disposition",
|
||||
`inline; filename="${asciiFallback}"; filename*=UTF-8''${utf8Encoded}`
|
||||
);
|
||||
const cacheControl = ossResponse.headers.get("cache-control") ?? "public, max-age=3600";
|
||||
const cacheControl =
|
||||
ossResponse.headers.get("cache-control") ?? "public, max-age=3600";
|
||||
responseHeaders.set("cache-control", cacheControl);
|
||||
|
||||
const buffer = await ossResponse.arrayBuffer();
|
||||
@@ -89,15 +94,13 @@ export async function GET(request: Request): Promise<Response> {
|
||||
statusText: ossResponse.statusText,
|
||||
headers: responseHeaders,
|
||||
});
|
||||
} catch (error: any) {
|
||||
const message = error?.message || "Unknown error";
|
||||
const stack = error?.stack || "";
|
||||
console.error("/api/pdf proxy error", message, stack);
|
||||
return new Response(JSON.stringify({ error: message }), {
|
||||
} catch (error) {
|
||||
// const message = error?.message || "Unknown error";
|
||||
// const stack = error?.stack || "";
|
||||
// console.error("/api/pdf proxy error", message, stack);
|
||||
return new Response(JSON.stringify({ error: error }), {
|
||||
status: 500,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PropsWithChildren } from "react";
|
||||
import type { PropsWithChildren } from "react";
|
||||
// Since we have a root `not-found.tsx` page, a layout file
|
||||
// is required, even if it's just passing children through.
|
||||
export default function RootLayout({ children }: PropsWithChildren) {
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { useWindowSize } from "react-haiku";
|
||||
import Image from "next/image";
|
||||
import XiaohongImage from "@public/xiaohong.jpg";
|
||||
|
||||
function AgentPortrait() {
|
||||
const { height } = useWindowSize();
|
||||
|
||||
return (
|
||||
<Image
|
||||
priority
|
||||
alt="小红形象"
|
||||
height={240}
|
||||
height={height / 4}
|
||||
src={XiaohongImage}
|
||||
width={240}
|
||||
width={height / 4}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { IconSvgProps } from "@/types";
|
||||
import type { IconSvgProps } from "@/types";
|
||||
|
||||
export type IconImgProps = React.ImgHTMLAttributes<HTMLImageElement> & {
|
||||
size?: number;
|
||||
@@ -371,23 +371,37 @@ export const KeyboardIcon: React.FC<IconSvgProps> = ({
|
||||
<svg
|
||||
className="icon"
|
||||
height={size || height}
|
||||
version="1.1"
|
||||
version="1.1"
|
||||
viewBox="0 0 1024 1024"
|
||||
width={size || width}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path d="M512 64A448 448 0 1 0 960 512 448.5 448.5 0 0 0 512 64z m0 832a384 384 0 1 1 384-384 384.5 384.5 0 0 1-384 384z" fill="#BD1A2D"/>
|
||||
<path d="M320 400m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z" fill="#BD1A2D"/>
|
||||
<path d="M448 448A48 48 0 1 0 400 400a48 48 0 0 0 48 48zM576 352a48 48 0 1 0 48 48 48 48 0 0 0-48-48zM704 352a48 48 0 1 0 48 48 48 48 0 0 0-48-48z" fill="#BD1A2D"/>
|
||||
<path d="M320 528m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z" fill="#BD1A2D"/>
|
||||
<path d="M448 576a48 48 0 1 0-48-48 48 48 0 0 0 48 48zM576 640H448a48 48 0 0 0 0 96h128a48 48 0 1 0 0-96zM576 480a48 48 0 1 0 48 48 48 48 0 0 0-48-48zM704 480a48 48 0 1 0 48 48 48 48 0 0 0-48-48z" fill="#BD1A2D"/>
|
||||
<path
|
||||
d="M512 64A448 448 0 1 0 960 512 448.5 448.5 0 0 0 512 64z m0 832a384 384 0 1 1 384-384 384.5 384.5 0 0 1-384 384z"
|
||||
fill="#BD1A2D"
|
||||
/>
|
||||
<path
|
||||
d="M320 400m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z"
|
||||
fill="#BD1A2D"
|
||||
/>
|
||||
<path
|
||||
d="M448 448A48 48 0 1 0 400 400a48 48 0 0 0 48 48zM576 352a48 48 0 1 0 48 48 48 48 0 0 0-48-48zM704 352a48 48 0 1 0 48 48 48 48 0 0 0-48-48z"
|
||||
fill="#BD1A2D"
|
||||
/>
|
||||
<path
|
||||
d="M320 528m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z"
|
||||
fill="#BD1A2D"
|
||||
/>
|
||||
<path
|
||||
d="M448 576a48 48 0 1 0-48-48 48 48 0 0 0 48 48zM576 640H448a48 48 0 0 0 0 96h128a48 48 0 1 0 0-96zM576 480a48 48 0 1 0 48 48 48 48 0 0 0-48-48zM704 480a48 48 0 1 0 48 48 48 48 0 0 0-48-48z"
|
||||
fill="#BD1A2D"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// 音量关闭图标
|
||||
|
||||
|
||||
// 发送图标
|
||||
export const SendIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
@@ -406,8 +420,8 @@ export const SendIcon: React.FC<IconSvgProps> = ({
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<path d="m22 2-7 20-4-9-9-4Z"/>
|
||||
<path d="M22 2 11 13"/>
|
||||
<path d="m22 2-7 20-4-9-9-4Z" />
|
||||
<path d="M22 2 11 13" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -429,9 +443,9 @@ export const MicIcon: React.FC<IconSvgProps> = ({
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/>
|
||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
|
||||
<line x1="12" x2="12" y1="19" y2="22"/>
|
||||
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" />
|
||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
|
||||
<line x1="12" x2="12" y1="19" y2="22" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -453,12 +467,12 @@ export const MicOffIcon: React.FC<IconSvgProps> = ({
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<line x1="2" x2="22" y1="2" y2="22"/>
|
||||
<path d="M18.89 13.23A7.12 7.12 0 0 0 19 12v-2"/>
|
||||
<path d="M5 10v2a7 7 0 0 0 12 5"/>
|
||||
<path d="M15 9.34V5a3 3 0 0 0-5.68-1.33"/>
|
||||
<path d="M9 9v3a3 3 0 0 0 5.12 2.12"/>
|
||||
<line x1="12" x2="12" y1="19" y2="22"/>
|
||||
<line x1="2" x2="22" y1="2" y2="22" />
|
||||
<path d="M18.89 13.23A7.12 7.12 0 0 0 19 12v-2" />
|
||||
<path d="M5 10v2a7 7 0 0 0 12 5" />
|
||||
<path d="M15 9.34V5a3 3 0 0 0-5.68-1.33" />
|
||||
<path d="M9 9v3a3 3 0 0 0 5.12 2.12" />
|
||||
<line x1="12" x2="12" y1="19" y2="22" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -480,8 +494,8 @@ export const PhoneOffIcon: React.FC<IconSvgProps> = ({
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<path d="M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.63A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91"/>
|
||||
<line x1="22" x2="2" y1="2" y2="22"/>
|
||||
<path d="M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.63A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91" />
|
||||
<line x1="22" x2="2" y1="2" y2="22" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -503,9 +517,9 @@ export const MoreIcon: React.FC<IconSvgProps> = ({
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<circle cx="12" cy="12" r="1"/>
|
||||
<circle cx="19" cy="12" r="1"/>
|
||||
<circle cx="5" cy="12" r="1"/>
|
||||
<circle cx="12" cy="12" r="1" />
|
||||
<circle cx="19" cy="12" r="1" />
|
||||
<circle cx="5" cy="12" r="1" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -527,8 +541,8 @@ export const ArrowLeftIcon: React.FC<IconSvgProps> = ({
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<path d="m12 19-7-7 7-7"/>
|
||||
<path d="M19 12H5"/>
|
||||
<path d="m12 19-7-7 7-7" />
|
||||
<path d="M19 12H5" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -550,11 +564,11 @@ export const DeleteIcon: React.FC<IconSvgProps> = ({
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<path d="M3 6h18"/>
|
||||
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/>
|
||||
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
|
||||
<line x1="10" x2="10" y1="11" y2="17"/>
|
||||
<line x1="14" x2="14" y1="11" y2="17"/>
|
||||
<path d="M3 6h18" />
|
||||
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
||||
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
||||
<line x1="10" x2="10" y1="11" y2="17" />
|
||||
<line x1="14" x2="14" y1="11" y2="17" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -573,7 +587,13 @@ export const ReconnectIcon: React.FC<IconSvgProps> = ({
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path d="M468.693333 687.317333l-63.701333-63.744 254.890667-254.890666 63.701333 63.744z" fill="#ffffff" />
|
||||
<path d="M691.797333 740.48l-0.170666-0.170667a44.970667 44.970667 0 0 0-63.573334 0l-53.162666 53.162667a187.733333 187.733333 0 0 1-265.386667-265.472L362.666667 474.88a44.928 44.928 0 0 0 0-63.573333l-0.170667-0.170667a44.970667 44.970667 0 0 0-63.573333 0l-53.248 53.205333a277.888 277.888 0 0 0 392.96 392.96l53.162666-53.205333a44.928 44.928 0 0 0 0-63.573333z m201.813334-531.072a277.845333 277.845333 0 0 0-393.002667 0l-53.248 53.162667a44.970667 44.970667 0 0 0 0 63.573333l0.170667 0.170667a44.928 44.928 0 0 0 63.573333 0l53.205333-53.12a187.733333 187.733333 0 1 1 265.472 265.514666l-53.162666 53.205334a44.970667 44.970667 0 0 0 0 63.530666l0.170666 0.170667a44.928 44.928 0 0 0 63.573334 0l53.205333-53.290667a277.845333 277.845333 0 0 0 0-392.96z" fill="#ffffff" />
|
||||
<path
|
||||
d="M468.693333 687.317333l-63.701333-63.744 254.890667-254.890666 63.701333 63.744z"
|
||||
fill="#ffffff"
|
||||
/>
|
||||
<path
|
||||
d="M691.797333 740.48l-0.170666-0.170667a44.970667 44.970667 0 0 0-63.573334 0l-53.162666 53.162667a187.733333 187.733333 0 0 1-265.386667-265.472L362.666667 474.88a44.928 44.928 0 0 0 0-63.573333l-0.170667-0.170667a44.970667 44.970667 0 0 0-63.573333 0l-53.248 53.205333a277.888 277.888 0 0 0 392.96 392.96l53.162666-53.205333a44.928 44.928 0 0 0 0-63.573333z m201.813334-531.072a277.845333 277.845333 0 0 0-393.002667 0l-53.248 53.162667a44.970667 44.970667 0 0 0 0 63.573333l0.170667 0.170667a44.928 44.928 0 0 0 63.573333 0l53.205333-53.12a187.733333 187.733333 0 1 1 265.472 265.514666l-53.162666 53.205334a44.970667 44.970667 0 0 0 0 63.530666l0.170666 0.170667a44.928 44.928 0 0 0 63.573334 0l53.205333-53.290667a277.845333 277.845333 0 0 0 0-392.96z"
|
||||
fill="#ffffff"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ export const Navbar = () => {
|
||||
|
||||
return (
|
||||
<HeroUINavbar className="bg-primary" maxWidth="full" position="sticky">
|
||||
<NavbarContent className="w-full " justify="start">
|
||||
<NavbarContent className="w-full" justify="start">
|
||||
<ul className="flex justify-center items-center gap-x-4">
|
||||
<NavbarItem>
|
||||
<Button
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import type { FC } from "react";
|
||||
import { VisuallyHidden } from "@react-aria/visually-hidden";
|
||||
import { SwitchProps, useSwitch } from "@heroui/switch";
|
||||
import { type SwitchProps, useSwitch } from "@heroui/switch";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useIsSSR } from "@react-aria/ssr";
|
||||
|
||||
@@ -22,7 +22,7 @@ export const ThemeSwitch: FC<ThemeSwitchProps> = ({
|
||||
const isSSR = useIsSSR();
|
||||
|
||||
const onChange = () => {
|
||||
theme === "light" ? setTheme("dark") : setTheme("light");
|
||||
return theme === "light" ? setTheme("dark") : setTheme("light");
|
||||
};
|
||||
|
||||
const {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
function arrayBufferToBase64(arrayBuffer: Int16Array<ArrayBufferLike>) {
|
||||
let binary = "";
|
||||
let bytes = new Uint8Array(arrayBuffer);
|
||||
const bytes = new Uint8Array(arrayBuffer);
|
||||
const chunkSize = 0x8000; // 32KB chunk size
|
||||
|
||||
for (let i = 0; i < bytes.length; i += chunkSize) {
|
||||
let chunk = bytes.subarray(i, i + chunkSize);
|
||||
const chunk = bytes.subarray(i, i + chunkSize);
|
||||
|
||||
binary += Array.from(chunk)
|
||||
.map((number) => String.fromCharCode(number))
|
||||
@@ -28,7 +28,7 @@ function base64ToArrayBuffer(base64: string) {
|
||||
|
||||
function mergeInt16Arrays(
|
||||
left: Int16Array<ArrayBuffer>,
|
||||
right: Int16Array<ArrayBuffer>,
|
||||
right: Int16Array<ArrayBuffer>
|
||||
) {
|
||||
if (left instanceof ArrayBuffer) {
|
||||
left = new Int16Array(left);
|
||||
@@ -55,7 +55,7 @@ function float32ToInt16(float32Array: Float32Array): Int16Array {
|
||||
const int16Array = new Int16Array(float32Array.length);
|
||||
|
||||
for (let i = 0; i < float32Array.length; i++) {
|
||||
let s = Math.max(-1, Math.min(1, float32Array[i]));
|
||||
const s = Math.max(-1, Math.min(1, float32Array[i]));
|
||||
|
||||
int16Array[i] = s < 0 ? s * 32768 : s * 32767;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { create } from "zustand";
|
||||
import { devtools } from "zustand/middleware";
|
||||
import { WebSocketHook } from "react-use-websocket/dist/lib/types";
|
||||
import type { WebSocketHook } from "react-use-websocket/dist/lib/types";
|
||||
|
||||
import { RealtimeMessage } from "./useRealtimeConnEffect";
|
||||
import { DifyMessage } from "./useDifyCmd";
|
||||
import type { RealtimeMessage } from "./useRealtimeConnEffect";
|
||||
import type { DifyMessage } from "./useDifyCmd";
|
||||
|
||||
export type ConversationSession = {
|
||||
id: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
import { WebSocketHook } from "react-use-websocket/dist/lib/types";
|
||||
import type { WebSocketHook } from "react-use-websocket/dist/lib/types";
|
||||
|
||||
import useConversationStore from "./useConversationStore";
|
||||
import useDeviceStore from "./useDeviceStore";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SVGProps } from "react";
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export type IconSvgProps = SVGProps<SVGSVGElement> & {
|
||||
size?: number;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"target": "es2023",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
@@ -22,7 +22,8 @@
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@public/*": ["./public/*"]
|
||||
}
|
||||
},
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
Reference in New Issue
Block a user