Compare commits
10 Commits
207f28c728
...
de83d62659
| Author | SHA1 | Date |
|---|---|---|
|
|
de83d62659 | |
|
|
48fe17a4cb | |
|
|
45df1a6108 | |
|
|
ff67e95c99 | |
|
|
2c36ce528e | |
|
|
48ed954da0 | |
|
|
d774630aea | |
|
|
84223d4c70 | |
|
|
86918a667e | |
|
|
97d2ccc154 |
|
|
@ -0,0 +1,4 @@
|
|||
VITE_COZE_TOKEN=pat_JJU3h01pYFQrH1cGeauzKZz2dakpkQglohNTp2PeIIlKRtXhi8fGeaCZGtDLmDoq
|
||||
VITE_COZE_BOT_ID=7456409430717480998
|
||||
VITE_COZE_CONNECTOR_ID=1024
|
||||
VITE_COZE_VOICE_ID=7426720361733144585
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 190 KiB |
|
|
@ -6,7 +6,7 @@ export const fetchQuestions = async ({
|
|||
options?: { signal?: AbortSignal,};
|
||||
}) => {
|
||||
const response = await getRequest(
|
||||
"https://api.v3.ycymedu.com/api/zhiYuan/aigcquestionswords?",
|
||||
"https://senior.ycymedu.com/api/zhiYuan/aigcquestionswords?",
|
||||
{},
|
||||
options
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export const fetchUserToken = async ({
|
|||
options?: { signal?: AbortSignal; headers?: Record<string, string> };
|
||||
}) => {
|
||||
const response = await getRequest(
|
||||
"https://api.v3.ycymedu.com/api/sysOnlineUser/hasitexpired",
|
||||
"https://senior.ycymedu.com/api/sysOnlineUser/hasitexpired",
|
||||
{},
|
||||
options
|
||||
);
|
||||
|
|
@ -26,7 +26,7 @@ export const fetchReport = async ({
|
|||
options?: { signal?: AbortSignal; headers?: Record<string, string> };
|
||||
}) => {
|
||||
const response = await getRequest(
|
||||
"https://api.v3.ycymedu.com/api/busScale/GetBusAIReportKeyWord",
|
||||
"https://senior.ycymedu.com/api/busScale/GetBusAIReportKeyWord",
|
||||
params,
|
||||
options
|
||||
);
|
||||
|
|
@ -46,7 +46,7 @@ export const fetchFile = async ({
|
|||
options?: { signal?: AbortSignal; headers?: Record<string, string> };
|
||||
}) => {
|
||||
const response = await getRequest(
|
||||
"https://api.v3.ycymedu.com/api/volunTb/downloadpdfUrl",
|
||||
"https://senior.ycymedu.com/api/volunTb/downloadpdfUrl",
|
||||
params,
|
||||
options
|
||||
);
|
||||
|
|
@ -63,10 +63,10 @@ export const fetchWishList = async ({
|
|||
params,
|
||||
options,
|
||||
}: {
|
||||
params: { locationCode: string; };
|
||||
params: {};
|
||||
options?: { signal?: AbortSignal; headers?: Record<string, string> };
|
||||
}) => {
|
||||
const response = await getRequest("https://api.v3.ycymedu.com/api/volunTb/v2/list", params, options);
|
||||
const response = await getRequest("https://senior.ycymedu.com/api/busStudentMiddleFill/list", params, options);
|
||||
|
||||
if (response.code === 200) {
|
||||
return { result: response.result };
|
||||
|
|
@ -74,3 +74,40 @@ export const fetchWishList = async ({
|
|||
return { result: [], message: response.message };
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const getCozeToken = async({
|
||||
options,
|
||||
}: {
|
||||
|
||||
options?: { signal?: AbortSignal; headers?: Record<string, string> };
|
||||
}) => {
|
||||
const response = await getRequest("https://senior.ycymedu.com/api/sysDictData/secrtToken", {Id:740405293269061}, options);
|
||||
|
||||
if (response.code === 200) {
|
||||
return { result: response.result };
|
||||
} else {
|
||||
return { result: [], message: response.message };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const fetchSpecialFile = async ({
|
||||
params,
|
||||
options,
|
||||
}: {
|
||||
params: { Type: string,UserId:string };
|
||||
options?: { signal?: AbortSignal; headers?: Record<string, string> };
|
||||
}) => {
|
||||
const response = await getRequest(
|
||||
"https://senior.ycymedu.com/api/volunTb/yITICreate",
|
||||
params,
|
||||
options
|
||||
);
|
||||
|
||||
if (response.code === 200) {
|
||||
return { result: response.result };
|
||||
} else {
|
||||
return { result: "", message: response.message };
|
||||
}
|
||||
};
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
import { lazy } from "react";
|
||||
import { Loading } from "@/components/Loading";
|
||||
import { lazy, Suspense } from "react";
|
||||
|
||||
const MainArea = lazy(() => import("@/app/MainArea/index"));
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="h-full bg-[#F4F6FA]">
|
||||
<MainArea />
|
||||
<Suspense fallback={<Loading/>}>
|
||||
<MainArea />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,49 +1,50 @@
|
|||
import AntechamberHeader from "@/components/AntechamberHeader";
|
||||
import InvokeButton from "@/components/AntechamberButton";
|
||||
import AntechamberScore from "@/components/AntechamberScore";
|
||||
// import AntechamberScore from "@/components/AntechamberScore";
|
||||
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { RealtimeClientContext } from "@/components/Provider/RealtimeClientProvider";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { fetchUserToken } from "@/apis/user";
|
||||
import { fetchUserToken } from "@/apis/user";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useAbortController } from "@/hooks/useAbortController";
|
||||
import AntechamberFile from "@/components/AntechamberFile";
|
||||
import AntechamberReport from "@/components/AntechamberReport";
|
||||
import AntechamberWishList from "@/components/AntechamberWishList";
|
||||
// import AntechamberWishList from "@/components/AntechamberWishList";
|
||||
import { ReportContext } from "@/components/Provider/ReportResolveProvider";
|
||||
// import ReceiveTime from "/icons/receive-time.png";
|
||||
import { ReceiveDialog } from "@/components/ReceiveDialog";
|
||||
import AntechamberTalentFile from "@/components/AntechamberTalentFile";
|
||||
|
||||
export default function Antechamber() {
|
||||
|
||||
const { handleConnect} = useContext(RealtimeClientContext);
|
||||
const { handleConnect } = useContext(RealtimeClientContext);
|
||||
const { hasHandledReport } = useContext(ReportContext);
|
||||
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const [disable,setDisable] = useState(true);
|
||||
const [isLoading,setIsLoading] = useState(false);
|
||||
const [disable, setDisable] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const token = searchParams.get("token") || "";
|
||||
|
||||
const token = searchParams.get("token") || '';
|
||||
|
||||
const { toast } = useToast();
|
||||
const { getSignal } = useAbortController();
|
||||
|
||||
const getUserToken = async () => {
|
||||
try {
|
||||
const { result, message } = await fetchUserToken({
|
||||
options: {
|
||||
const { result, message } = await fetchUserToken({
|
||||
options: {
|
||||
signal: getSignal(),
|
||||
headers: {
|
||||
"Authorization": `Bearer ${encodeURIComponent(token)}`
|
||||
}
|
||||
}
|
||||
Authorization: `Bearer ${encodeURIComponent(token)}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (message) {
|
||||
console.log(message);
|
||||
} else {
|
||||
const _result = result as {isExpired:boolean;msg:string};
|
||||
const _result = result as { isExpired: boolean; msg: string };
|
||||
setDisable(!_result.isExpired);
|
||||
if(!_result.isExpired &&_result.msg){
|
||||
if (!_result.isExpired && _result.msg) {
|
||||
toast({
|
||||
title: _result.msg,
|
||||
description: "请重新登录",
|
||||
|
|
@ -51,42 +52,61 @@ export default function Antechamber() {
|
|||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.name !== 'AbortError') {
|
||||
console.error('获取用户令牌失败:', error);
|
||||
if (error.name !== "AbortError") {
|
||||
console.error("获取用户令牌失败:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
getUserToken();
|
||||
}, [token]);
|
||||
|
||||
|
||||
const toRoom = (params:{initMessage?:string,fileUrl?:string}) => {
|
||||
if(!hasHandledReport && (disable || isLoading)){
|
||||
const toRoom = (params: { initMessage?: string; fileUrl?: string }) => {
|
||||
if (!hasHandledReport && (disable || isLoading)) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true)
|
||||
setIsLoading(true);
|
||||
handleConnect(params).then(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const [isReceiveDialogOpen, setIsReceiveDialogOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center h-full overflow-y-auto relative">
|
||||
<AntechamberHeader />
|
||||
<AntechamberScore />
|
||||
<AntechamberWishList handleLoading={setIsLoading}/>
|
||||
{/* <AntechamberScore />
|
||||
<AntechamberWishList handleLoading={setIsLoading} /> */}
|
||||
<AntechamberFile handleLoading={setIsLoading} />
|
||||
<AntechamberTalentFile handleLoading={setIsLoading}/>
|
||||
<AntechamberReport handleLoading={setIsLoading} />
|
||||
<InvokeButton disable={disable} onClick={() => toRoom({})} />
|
||||
{
|
||||
isLoading ? <div className="absolute w-full h-full bg-red"><div className="w-[108px] h-[108px] absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] bg-black/60 rounded-[20px] flex flex-col items-center justify-center">
|
||||
<img src="/icons/loading.gif" alt="loading" className="w-[68px] h-[68px]" />
|
||||
<span className="text-[14px] text-[#fff]">加载中</span>
|
||||
</div></div> : <></>
|
||||
}
|
||||
{/* <img
|
||||
src={ReceiveTime}
|
||||
alt="receive-item"
|
||||
className="w-[100px] h-[100px] absolute right-[12px] bottom-[80px]"
|
||||
onClick={() => setIsReceiveDialogOpen(true)}
|
||||
/> */}
|
||||
{isLoading ? (
|
||||
<div className="absolute w-full h-full bg-red">
|
||||
<div className="w-[108px] h-[108px] absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] bg-black/60 rounded-[20px] flex flex-col items-center justify-center">
|
||||
<img
|
||||
src="/icons/loading.gif"
|
||||
alt="loading"
|
||||
className="w-[68px] h-[68px]"
|
||||
/>
|
||||
<span className="text-[14px] text-[#fff]">加载中</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<ReceiveDialog
|
||||
isOpen={isReceiveDialogOpen}
|
||||
onOpenChange={setIsReceiveDialogOpen}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,20 +6,21 @@ import {
|
|||
RealtimeClientProvider,
|
||||
} from "@/components/Provider/RealtimeClientProvider";
|
||||
import { ReportProvider } from "@/components/Provider/ReportResolveProvider";
|
||||
import { RealtimeUtils } from "@coze/realtime-api";
|
||||
// import { RealtimeUtils } from "@coze/realtime-api";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { CountdownProvider } from "@/components/Provider/CountdownProvider";
|
||||
|
||||
function MainContent() {
|
||||
const { isConnected, handleDisconnect } = useContext(RealtimeClientContext);
|
||||
const location = useLocation();
|
||||
|
||||
const handlePromise = async() => {
|
||||
await RealtimeUtils.checkDevicePermission(false);
|
||||
}
|
||||
// const handlePromise = async() => {
|
||||
// await RealtimeUtils.checkDevicePermission(false);
|
||||
// }
|
||||
|
||||
useEffect(() => {
|
||||
handlePromise();
|
||||
}, []);
|
||||
// useEffect(() => {
|
||||
// handlePromise();
|
||||
// }, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected) {
|
||||
|
|
@ -28,9 +29,13 @@ function MainContent() {
|
|||
}, [location.pathname]);
|
||||
|
||||
return (
|
||||
<ReportProvider>
|
||||
{isConnected ? <Room /> : <Antechamber />}
|
||||
</ReportProvider>
|
||||
<CountdownProvider>
|
||||
<ReportProvider>
|
||||
<>
|
||||
{isConnected ? <Room /> : <Antechamber />}
|
||||
</>
|
||||
</ReportProvider>
|
||||
</CountdownProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,8 +39,9 @@ export default function AntechamberFile({handleLoading}:Props) {
|
|||
|
||||
handleConnect({
|
||||
fileInfo: {type: resp.type,url: resp.url,tableName: resp.tableName,provinceName: resp.provinceName,subjectClaim: resp.subjectClaim},
|
||||
}).then(() => {
|
||||
setHasHandledReport(true);
|
||||
});
|
||||
setHasHandledReport(true);
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import styles from "./index.module.css";
|
|||
import { fetchQuestions } from "@/apis/questions";
|
||||
import { useAbortController } from "@/hooks/useAbortController";
|
||||
import { RealtimeClientContext } from "../Provider/RealtimeClientProvider";
|
||||
|
||||
// import Countdown from "../Countdown";
|
||||
|
||||
export default function HeaderGroup() {
|
||||
const [isRotating, setIsRotating] = useState(false);
|
||||
|
|
@ -69,6 +69,7 @@ export default function HeaderGroup() {
|
|||
|
||||
return (
|
||||
<div className={styles.headerWrapper}>
|
||||
{/* <Countdown /> */}
|
||||
<div className={styles.wrapper}>
|
||||
<img className={styles.img} src={HelloGIF} alt="hello" />
|
||||
<div className={styles.text}>Hey,我是您的六纬AI小助手</div>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export default function MyInput() {
|
|||
|
||||
|
||||
const handleQuestion = async () => {
|
||||
handleConnect({initMessage:`我的高考地点在${provinceName},我选择的科目是${subjectGroup},我的高考分数为${expectedScore}分。帮我出一个科学的参考志愿表`});
|
||||
handleConnect({initMessage:`我的中考地点在${provinceName},我选择的科目是${subjectGroup},我的高考分数为${expectedScore}分。帮我出一个科学的参考志愿表`});
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
import { fetchSpecialFile } from "@/apis/user";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useAbortController } from "@/hooks/useAbortController";
|
||||
import { useContext, useEffect } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { ReportContext } from "../Provider/ReportResolveProvider";
|
||||
import { FileInfo, RealtimeClientContext } from "../Provider/RealtimeClientProvider";
|
||||
|
||||
type Props = {
|
||||
handleLoading:(val:boolean) => void
|
||||
}
|
||||
|
||||
export default function AntechamberFile({handleLoading}:Props) {
|
||||
const [searchParams] = useSearchParams();
|
||||
const talentTypeId = searchParams.get("talentTypeId") || "";
|
||||
const userId = searchParams.get("userId") || "";
|
||||
const token = searchParams.get("token") || "";
|
||||
const { toast } = useToast();
|
||||
const { getSignal } = useAbortController();
|
||||
const { setHasHandledReport,hasHandledReport } = useContext(ReportContext);
|
||||
const { handleConnect } = useContext(RealtimeClientContext);
|
||||
|
||||
const useFileFetch = async () => {
|
||||
handleLoading(true)
|
||||
const result = await fetchSpecialFile({
|
||||
params: { Type:talentTypeId, UserId:userId},
|
||||
options: {
|
||||
signal: getSignal(),
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
},
|
||||
});
|
||||
|
||||
if (result.message) {
|
||||
toast({
|
||||
title: result.message,
|
||||
});
|
||||
}
|
||||
let resp = result.result as FileInfo;
|
||||
|
||||
handleConnect({
|
||||
fileInfo: {url: resp.url,tableName:"我的特长报告"},
|
||||
}).then(() => {
|
||||
setHasHandledReport(true);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (talentTypeId && userId && !hasHandledReport) {
|
||||
useFileFetch();
|
||||
}
|
||||
}, [talentTypeId, userId,hasHandledReport]);
|
||||
|
||||
|
||||
return <></>;
|
||||
}
|
||||
|
|
@ -91,23 +91,17 @@ export default function AntechamberWishList({handleLoading}:Props) {
|
|||
</DrawerTitle>
|
||||
<div className="grid gap-[12px] px-[15px] pb-[15px] bg-[#F4F6FA] pt-[16px] max-h-[50vh] overflow-y-auto">
|
||||
{wishList.map((item: any) => (
|
||||
<div className="w-full bg-white" key={item.vId}>
|
||||
<div className="w-full bg-white" key={item.id}>
|
||||
<div className="py-[10px] pl-[16px] pr-[13px] rounded-[8px] flex items-center">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center mb-[8px]">
|
||||
<span className="text-[15px] font-[600] text-[#303030] mr-[5px]">
|
||||
{item.tableName}
|
||||
{item.title}
|
||||
</span>
|
||||
<div className="text-[10px] px-[4px] py-[2px] rounded-[4px] text-[#636363] bg-[#fff]">
|
||||
{item.type}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[#303030] text-[11px] flex items-center">
|
||||
<span>
|
||||
{item.locationName}·{item.score}
|
||||
</span>
|
||||
<span className="ml-[8px]">
|
||||
{item.subjectClaim.split(",").join("/")}
|
||||
{wishList[0].batchName}·{wishList[0].totalScore}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -148,18 +142,13 @@ export default function AntechamberWishList({handleLoading}:Props) {
|
|||
<div className="flex flex-col">
|
||||
<div className="flex items-center mb-[5px]">
|
||||
<span className="text-[15px] font-[600] text-[#303030] mr-[5px]">
|
||||
{wishList[0].tableName}
|
||||
{wishList[0].title}
|
||||
</span>
|
||||
<div className="text-[10px] px-[4px] py-[2px] rounded-[4px] text-[#636363] bg-[#fff]">
|
||||
{wishList[0].type}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="text-[#303030] text-[11px] flex items-center">
|
||||
<span>
|
||||
{wishList[0].locationName}·{wishList[0].score}
|
||||
</span>
|
||||
<span className="ml-[8px]">
|
||||
{wishList[0].subjectClaim.split(",").join("/")}
|
||||
{wishList[0].batchName}·{wishList[0].totalScore}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useCountdown } from '../Provider/CountdownProvider';
|
||||
|
||||
/**
|
||||
* 将秒数转换为"分钟秒"格式
|
||||
* @param totalSeconds 总秒数
|
||||
* @returns 格式化后的字符串,如"10分00秒"
|
||||
*/
|
||||
const formatTime = (totalSeconds: number): string => {
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
|
||||
// 确保秒数显示为两位数
|
||||
const formattedSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
|
||||
|
||||
return `${minutes}分${formattedSeconds}秒`;
|
||||
};
|
||||
|
||||
interface CountdownProps {
|
||||
|
||||
}
|
||||
|
||||
export default function Countdown({ }: CountdownProps) {
|
||||
const { countdown, setCountdown } = useCountdown();
|
||||
|
||||
useEffect(() => {
|
||||
// 只有当秒数大于0时才启动倒计时
|
||||
if (countdown <= 0) return;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setCountdown(countdown - 1)
|
||||
}, 1000);
|
||||
|
||||
|
||||
// 清理定时器
|
||||
return () => clearInterval(timer);
|
||||
}, [countdown]);
|
||||
|
||||
return (
|
||||
<div className="w-max h-[23px] rounded-full bg-[#EAF0FE] flex items-center ml-auto px-[10px] py-[3px] mb-[5px]">
|
||||
<div className="w-[6px] h-[6px] rounded-full bg-[#1580FF] mr-[5px]"></div>
|
||||
<div className="text-[12px] text-[#000]">剩余可用:{formatTime(countdown)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
.custom-animation {
|
||||
height: calc(100% - 12px);
|
||||
will-change: width;
|
||||
animation: moveBackAndForth 2s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes moveBackAndForth {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
25% {
|
||||
width: calc(40%);
|
||||
transform: translateX(calc(50% - 12px));
|
||||
}
|
||||
50% {
|
||||
width: calc(20%);
|
||||
transform: translateX(calc(400% - 12px));
|
||||
}
|
||||
75% {
|
||||
width: calc(40%);
|
||||
transform: translateX(calc(50% - 12px));
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import "./index.css";
|
||||
import LogoSvg from "/icons/logo.png";
|
||||
|
||||
export const Loading = () => {
|
||||
return (
|
||||
<div className="h-screen w-full flex items-center justify-center">
|
||||
<div className="flex mx-[12vw] rounded-full border-[3px] border-solid border-[#000] h-[54px] p-[6px] relative flex-1">
|
||||
<div className="w-[calc(20%)] bg-black rounded-full custom-animation absolute left-0"></div>
|
||||
<div className="w-full h-full flex items-center justify-center mix-blend-difference">
|
||||
<img src={LogoSvg} className="w-[115px] h-[18px]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { createContext, useContext, useState } from "react"
|
||||
|
||||
export const CountdownContext = createContext<{
|
||||
countdown: number;
|
||||
setCountdown: (countdown: number) => void;
|
||||
}>({
|
||||
countdown: 0,
|
||||
setCountdown: () => {}
|
||||
})
|
||||
|
||||
export const useCountdown = () => {
|
||||
const ctx = useContext(CountdownContext);
|
||||
if (!ctx) throw new Error("useCountdown 必须在 CountdownProvider 内部使用");
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export const CountdownProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
|
||||
return (
|
||||
<CountdownContext.Provider value={{ countdown, setCountdown }}>
|
||||
{children}
|
||||
</CountdownContext.Provider>
|
||||
)
|
||||
}
|
||||
|
|
@ -14,6 +14,10 @@ import {
|
|||
useState,
|
||||
} from "react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { MessageHandlerStrategy } from "@/hooks/useRealtimeClient";
|
||||
import { getCozeToken } from "@/apis/user";
|
||||
import { useAbortController } from "@/hooks/useAbortController";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
type RoomInfo = {
|
||||
appId: string;
|
||||
|
|
@ -23,11 +27,11 @@ type RoomInfo = {
|
|||
};
|
||||
|
||||
export type FileInfo = {
|
||||
type: string;
|
||||
type?: string;
|
||||
url: string;
|
||||
tableName: string;
|
||||
provinceName: string;
|
||||
subjectClaim: string;
|
||||
tableName?: string;
|
||||
provinceName?: string;
|
||||
subjectClaim?: string;
|
||||
};
|
||||
|
||||
export const RealtimeClientContext = createContext<{
|
||||
|
|
@ -68,7 +72,7 @@ export const RealtimeClientProvider = ({
|
|||
}: {
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const token = import.meta.env.VITE_COZE_TOKEN;
|
||||
let token = "";
|
||||
const botId = import.meta.env.VITE_COZE_BOT_ID;
|
||||
const voiceId = import.meta.env.VITE_COZE_VOICE_ID;
|
||||
const connectorId = "1024";
|
||||
|
|
@ -94,6 +98,12 @@ export const RealtimeClientProvider = ({
|
|||
|
||||
const { toast } = useToast();
|
||||
|
||||
const messageHandlerStrategy = useRef(new MessageHandlerStrategy());
|
||||
|
||||
const { getSignal } = useAbortController();
|
||||
const [searchParams]= useSearchParams();
|
||||
const userToken = searchParams.get("token") || '';
|
||||
|
||||
/** 初始化客户端并设置监听 */
|
||||
const initClient = async ({
|
||||
initMessage,
|
||||
|
|
@ -102,16 +112,9 @@ export const RealtimeClientProvider = ({
|
|||
initMessage?: string;
|
||||
fileInfo?: FileInfo;
|
||||
}) => {
|
||||
const perm = await RealtimeUtils.checkDevicePermission(false);
|
||||
const device = await RealtimeUtils.getAudioDevices();
|
||||
|
||||
if (!perm.audio) {
|
||||
toast({ title: "连接错误", description: "需要麦克风访问权限" });
|
||||
throw new Error("需要麦克风访问权限");
|
||||
}
|
||||
if (device.audioInputs.length === 0) {
|
||||
toast({ title: "连接错误", description: "没有麦克风设备" });
|
||||
throw new Error("没有麦克风设备");
|
||||
if(!token){
|
||||
const resp = await getCozeToken({options:{signal:getSignal(),headers: { Authorization: `Bearer ${userToken}` }}})
|
||||
token = resp.result as string
|
||||
}
|
||||
|
||||
const client = new RealtimeClient({
|
||||
|
|
@ -122,7 +125,7 @@ export const RealtimeClientProvider = ({
|
|||
allowPersonalAccessTokenInBrowser: true,
|
||||
suppressStationaryNoise: true,
|
||||
suppressNonStationaryNoise: true,
|
||||
debug: false,
|
||||
debug: true,
|
||||
});
|
||||
clientRef.current = client;
|
||||
|
||||
|
|
@ -146,6 +149,18 @@ export const RealtimeClientProvider = ({
|
|||
connectingLockRef.current = false;
|
||||
return;
|
||||
}
|
||||
const perm = await RealtimeUtils.checkDevicePermission(false);
|
||||
const device = await RealtimeUtils.getAudioDevices();
|
||||
if (!perm.audio) {
|
||||
toast({ title: "连接错误", description: "需要麦克风访问权限" });
|
||||
return;
|
||||
// throw new Error("需要麦克风访问权限");
|
||||
}
|
||||
if (device.audioInputs.length === 0) {
|
||||
toast({ title: "连接错误", description: "没有麦克风设备" });
|
||||
return;
|
||||
// throw new Error("没有麦克风设备");
|
||||
}
|
||||
|
||||
try {
|
||||
if (!clientRef.current) {
|
||||
|
|
@ -223,7 +238,7 @@ export const RealtimeClientProvider = ({
|
|||
content: JSON.stringify([
|
||||
{
|
||||
type: "text",
|
||||
text: "帮我解读这个文件,结合当下的专业行情以及对该专业未来的发展趋势,简介的给出志愿建议",
|
||||
text: "帮我解读这个文件,根据济南市初升高相关政策要求和情况,给出合理的升学规划建议",
|
||||
},
|
||||
{ type: "image", file_url: fileInfo.url },
|
||||
]),
|
||||
|
|
@ -244,101 +259,17 @@ export const RealtimeClientProvider = ({
|
|||
client.on(EventNames.ALL, (_eventName, event: any) => {
|
||||
// 交给状态机处理解析流程
|
||||
|
||||
// 普通消息流处理
|
||||
if (
|
||||
event.event_type !== ChatEventType.CONVERSATION_MESSAGE_DELTA &&
|
||||
event.event_type !== ChatEventType.CONVERSATION_MESSAGE_COMPLETED
|
||||
) {
|
||||
// 处理conversation.created事件
|
||||
if (
|
||||
event.event_type === "conversation.created" &&
|
||||
!opts.initMessage &&
|
||||
!opts.fileInfo
|
||||
) {
|
||||
setMessageList((prev) => [
|
||||
...prev,
|
||||
{
|
||||
content: event.data.prologue,
|
||||
role: RoleType.Assistant,
|
||||
event,
|
||||
},
|
||||
]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是assistant的completed消息或verbose类型,直接返回
|
||||
if (
|
||||
(event.data.role === "assistant" &&
|
||||
event.event_type === ChatEventType.CONVERSATION_MESSAGE_COMPLETED &&
|
||||
event.data.type === "verbose") ||
|
||||
(event.data.type === "answer" &&
|
||||
event.event_type === ChatEventType.CONVERSATION_MESSAGE_COMPLETED)
|
||||
event.event_type !== ChatEventType.CONVERSATION_MESSAGE_COMPLETED &&
|
||||
event.event_type !== "conversation.created"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = event.data.content;
|
||||
setMessageList((prev) => {
|
||||
// 如果是工具调用相关的消息,不添加到消息列表
|
||||
if (
|
||||
event.data.type === "function_call" ||
|
||||
event.data.type === "tool_response"
|
||||
) {
|
||||
const jsonContent = JSON.parse(event.data.content);
|
||||
const lastMessage = prev[prev.length - 1];
|
||||
|
||||
if (jsonContent.name === "doc_reader-PDF_reader") {
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
content: "",
|
||||
role: RoleType.Assistant,
|
||||
fileInfo: opts.fileInfo,
|
||||
fileParseStatus: 1,
|
||||
event,
|
||||
},
|
||||
];
|
||||
}
|
||||
else if (
|
||||
lastMessage.event.type === "function_call" &&
|
||||
event.data.type === "tool_response"
|
||||
) {
|
||||
return [
|
||||
...prev.slice(0, prev.length - 2),
|
||||
{
|
||||
content: "",
|
||||
role: RoleType.Assistant,
|
||||
fileInfo: opts.fileInfo,
|
||||
fileParseStatus: 2,
|
||||
event,
|
||||
},
|
||||
];
|
||||
}else{
|
||||
return [...prev]
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
prev.length > 0 &&
|
||||
prev[prev.length - 1].event?.event_type ===
|
||||
ChatEventType.CONVERSATION_MESSAGE_DELTA &&
|
||||
event.event_type === ChatEventType.CONVERSATION_MESSAGE_DELTA &&
|
||||
prev[prev.length - 1].event.data.answer_id === event.data.answer_id
|
||||
) {
|
||||
return [
|
||||
...prev.slice(0, -1),
|
||||
{
|
||||
content: prev[prev.length - 1].content + content,
|
||||
role: prev[prev.length - 1].role,
|
||||
event,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
// 新消息追加
|
||||
return [...prev, { content, role: event.data.role, event }];
|
||||
}
|
||||
});
|
||||
|
||||
setMessageList(prev =>
|
||||
messageHandlerStrategy.current.process(event, prev, opts)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
.custom-bg {
|
||||
background: linear-gradient(0deg, #FFFFFF 30%,transparent 100%);
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogClose
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { X } from "lucide-react";
|
||||
import "./index.css";
|
||||
|
||||
export const ReceiveDialog = ({
|
||||
isOpen = false,
|
||||
onOpenChange,
|
||||
onConfirm
|
||||
}: {
|
||||
isOpen?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
onConfirm?: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md border-none bg-transparent p-0 shadow-none">
|
||||
<DialogTitle className="sr-only">接收新消息</DialogTitle>
|
||||
<DialogDescription className="sr-only">您有一条新的消息,是否立即查看?</DialogDescription>
|
||||
<DialogClose className="absolute right-[38px] top-2 z-10 rounded-full w-[28px] h-[28px] bg-[#acacad] flex items-center justify-center bg-[#FFFFFF33]">
|
||||
<X className="w-[12px] h-[12px] text-white"/>
|
||||
<span className="sr-only" onClick={() => onOpenChange?.(false)}>关闭</span>
|
||||
</DialogClose>
|
||||
<div className="relative flex flex-col items-center">
|
||||
<img
|
||||
src="/icons/receive.png"
|
||||
alt="接收"
|
||||
className="rounded-lg w-full h-[286px] px-[48px]"
|
||||
/>
|
||||
<div className="absolute top-1/2 left-0 right-0 flex flex-col items-center px-[20px] py-[17px] text-center mx-[48px] rounded-[40px] custom-bg">
|
||||
<h3 className="font-[600] text-[18px] text-black mb-[7px]">AI通话时长送你啦~</h3>
|
||||
<p className="text-[13px] mb-6 text-[#666]">志愿怎么选?分数能上哪?六维AI填报师来帮你解答~</p>
|
||||
<div className="flex space-x-4 w-full justify-center">
|
||||
|
||||
<Button
|
||||
className="bg-[#1580FF] text-white w-full rounded-[50px] text-[16px] py-[11px]"
|
||||
onClick={() => {
|
||||
onConfirm?.();
|
||||
onOpenChange?.(false);
|
||||
}}
|
||||
>
|
||||
领取时长
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -3,7 +3,7 @@ import { RealtimeClientContext } from "../Provider/RealtimeClientProvider";
|
|||
import ReactMarkdown from "react-markdown";
|
||||
import gfm from "remark-gfm";
|
||||
import { RoleType } from "@coze/api";
|
||||
import { Loader } from 'lucide-react';
|
||||
import { Loader } from "lucide-react";
|
||||
|
||||
export default function RoomConversation() {
|
||||
const { messageList } = useContext(RealtimeClientContext);
|
||||
|
|
@ -54,7 +54,8 @@ export default function RoomConversation() {
|
|||
: "bg-blue-500 text-white rounded-tr-none"
|
||||
}`}
|
||||
>
|
||||
{typeof message.fileParseStatus === "undefined" && typeof message.fileInfo === 'undefined' ? (
|
||||
{typeof message.fileParseStatus === "undefined" &&
|
||||
typeof message.fileInfo === "undefined" ? (
|
||||
<ReactMarkdown remarkPlugins={[gfm]}>
|
||||
{message.content}
|
||||
</ReactMarkdown>
|
||||
|
|
@ -70,22 +71,36 @@ export default function RoomConversation() {
|
|||
<span className="text-[15px] text-[#303030] mr-[8px] leading-[1]">
|
||||
{message?.fileInfo?.tableName}
|
||||
</span>
|
||||
<div className="bg-[#F4F6FA] rounded-[4px] w-[48px] h-[16px] px-[4px] py-[2px] text-[10px]">
|
||||
{message?.fileInfo?.type}
|
||||
</div>
|
||||
{message?.fileInfo?.type ? (
|
||||
<div className="bg-[#F4F6FA] rounded-[4px] w-[48px] h-[16px] px-[4px] py-[2px] text-[10px]">
|
||||
{message.fileInfo.type}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[12px] text-[#303030] mt-[6px] flex items-center">
|
||||
<span className="mr-[10px]">
|
||||
{message?.fileInfo?.provinceName}·{message?.fileInfo?.score}
|
||||
</span>
|
||||
<span>
|
||||
{message?.fileInfo?.subjectClaim?.split(",").join("/")}
|
||||
</span>
|
||||
{
|
||||
message.fileParseStatus < 2 && (
|
||||
<Loader className="w-[12px] h-[12px] animate-spin ml-[6px]" />
|
||||
)
|
||||
}
|
||||
{message.fileInfo.provinceName ? (
|
||||
<span className="mr-[10px]">
|
||||
{message?.fileInfo?.provinceName}·
|
||||
{message?.fileInfo?.score}
|
||||
</span>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{message?.fileInfo?.subjectClaim ? (
|
||||
<span>
|
||||
{message?.fileInfo?.subjectClaim
|
||||
?.split(",")
|
||||
.join("/")}
|
||||
</span>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{message.fileParseStatus < 2 && (
|
||||
<Loader className="w-[12px] h-[12px] animate-spin ml-[6px]" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -95,7 +110,13 @@ export default function RoomConversation() {
|
|||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
<div className="text-[12px] text-[#999] font-[400] text-center mt-auto mb-[7px]">AI也会犯错,请考虑核实重要信息。</div>
|
||||
<div className="text-[12px] text-[#999] font-[400] text-center mt-auto mb-[7px]">
|
||||
AI也会犯错,请考虑核实重要信息。
|
||||
</div>
|
||||
{/* <div className="mx-[8px] rounded-full bg-[#ffeede] py-[7px] pl-[18px] pr-[3px] flex items-center justify-between">
|
||||
<span className="text-[#FA8E23] text-[12px]">您的剩余时间不足3分钟,请续费畅聊生涯~</span>
|
||||
<button className="rounded-full bg-[#FA8E23] py-[6px] px-[13px] text-[13px] text-white font-[500]">立即续费</button>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Cross2Icon } from "@radix-ui/react-icons"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ export const useSignalRConnection = (params: {
|
|||
.withServerTimeout(30000)
|
||||
.withAutomaticReconnect()
|
||||
.withUrl(
|
||||
`https://api.v3.ycymedu.com/hubs/weminpro?access_token=${params.access_token}&roomId=${params.roomId}`
|
||||
`https://senior.ycymedu.com/hubs/weminpro?access_token=${params.access_token}&roomId=${params.roomId}`
|
||||
)
|
||||
.configureLogging(signalR.LogLevel.Information)
|
||||
.build();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,175 @@
|
|||
import { ChatEventType, RoleType } from "@coze/api";
|
||||
|
||||
interface MessageHandler {
|
||||
canHandle(event: any, opts?: any, prevMessageList?: any[]): boolean;
|
||||
handle(event: any, prevMessageList: any[], opts?: any): any[];
|
||||
}
|
||||
|
||||
abstract class BaseMessageHandler implements MessageHandler {
|
||||
abstract canHandle(event: any, opts: any, prevMessageList?: any[]): boolean;
|
||||
abstract handle(event: any, prevMessageList: any[], opts: any): any[];
|
||||
|
||||
protected createMessage(content: string, role: RoleType, event: any, opts?: any) {
|
||||
const fileParseStatus = opts && opts.fileInfo ? event.data.type === "function_call" ? 1 :
|
||||
event.data.type === "tool_response" ? 2 : undefined:undefined
|
||||
return {
|
||||
content,
|
||||
role,
|
||||
fileInfo: (opts && opts.fileInfo) ? opts.fileInfo : undefined,
|
||||
fileParseStatus: fileParseStatus,
|
||||
event
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 普通消息处理 添加欢迎语
|
||||
class ConversationHelloHandler extends BaseMessageHandler {
|
||||
canHandle(event: any, opts: any, _prevMessageList?: any[]): boolean {
|
||||
return event.event_type === "conversation.created" &&
|
||||
!opts.initMessage &&
|
||||
!opts.fileInfo;
|
||||
}
|
||||
|
||||
handle(event: any, prevMessageList: any[], _opts: any): any[] {
|
||||
return [
|
||||
...prevMessageList,
|
||||
this.createMessage(event.data.prologue, RoleType.Assistant, event,)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 处理忽略的事件类型
|
||||
class IgnoredEventHandler extends BaseMessageHandler {
|
||||
canHandle(event: any, _opts: any, _prevMessageList?: any[]): boolean {
|
||||
return (
|
||||
(event.data.role === "assistant" &&
|
||||
event.event_type === ChatEventType.CONVERSATION_MESSAGE_COMPLETED &&
|
||||
event.data.type === "verbose") ||
|
||||
(event.data.type === "answer" &&
|
||||
event.event_type === ChatEventType.CONVERSATION_MESSAGE_COMPLETED)
|
||||
);
|
||||
}
|
||||
|
||||
handle(_event: any, prevMessageList: any[], _opts: any): any[] {
|
||||
// 直接返回原消息列表,不做任何更改
|
||||
return prevMessageList;
|
||||
}
|
||||
}
|
||||
|
||||
class UserMessageEventHandle extends BaseMessageHandler{
|
||||
canHandle(event: any, _opts: any, _prevMessageList?: any[]): boolean {
|
||||
return event.event_type === ChatEventType.CONVERSATION_MESSAGE_COMPLETED && event.data.role === RoleType.User
|
||||
}
|
||||
|
||||
handle(event: any, prevMessageList: any[], _opts: any): any[] {
|
||||
return [...prevMessageList,this.createMessage(event.data.content,event.data.role,event)]
|
||||
}
|
||||
}
|
||||
|
||||
class NormalMessageEventHandle extends BaseMessageHandler{
|
||||
canHandle(event: any, _opts: any, prevMessageList?: any[]): boolean {
|
||||
if(typeof prevMessageList === 'undefined'){
|
||||
return false
|
||||
}
|
||||
let prevMessage = prevMessageList[prevMessageList.length - 1];
|
||||
// 当前信息为增量,并且上一条信息为完成
|
||||
if(prevMessage?.event.event_type === ChatEventType.CONVERSATION_MESSAGE_COMPLETED && event.event_type === ChatEventType.CONVERSATION_MESSAGE_DELTA){
|
||||
return true
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
handle(event: any, prevMessageList: any[], _opts: any): any[] {
|
||||
return [...prevMessageList,this.createMessage(event.data.content,event.data.role,event)]
|
||||
}
|
||||
}
|
||||
|
||||
class AppendMessageEventHandle extends BaseMessageHandler{
|
||||
canHandle(event: any, _opts: any, prevMessageList?: any[]): boolean {
|
||||
if(typeof prevMessageList === 'undefined' || prevMessageList.length === 0){
|
||||
return false;
|
||||
}
|
||||
let prevMessage = prevMessageList[prevMessageList.length - 1];
|
||||
if(prevMessage.event?.event_type === ChatEventType.CONVERSATION_MESSAGE_DELTA && event.event_type === ChatEventType.CONVERSATION_MESSAGE_DELTA){
|
||||
return true
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
handle(event: any, prevMessageList: any[], _opts: any): any[] {
|
||||
let prevMessage = prevMessageList[prevMessageList.length - 1];
|
||||
return [...prevMessageList.slice(0,prevMessageList.length-1),{...prevMessage,content:prevMessage.content + event.data.content}]
|
||||
}
|
||||
}
|
||||
|
||||
class BottomMessageEventHandle extends BaseMessageHandler{
|
||||
canHandle(_event: any, _opts: any, _prevMessageList?: any[]): boolean {
|
||||
return true
|
||||
}
|
||||
handle(_event: any, _prevMessageList: any[], _opts: any): any[] {
|
||||
return _prevMessageList
|
||||
}
|
||||
}
|
||||
|
||||
class FileFunctionCallEventHandle extends BaseMessageHandler{
|
||||
canHandle(event: any, _opts: any, _prevMessageList?: any[]): boolean {
|
||||
if(event.data.type === "function_call"){
|
||||
let jsonData = JSON.parse(event.data.content)
|
||||
return jsonData.name==='doc_reader-PDF_reader' && event.event_type === "conversation.message.completed"
|
||||
}else{
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
handle(event: any, prevMessageList: any[], opts: any): any[] {
|
||||
return [...prevMessageList,this.createMessage('',event.data.role,event,opts)]
|
||||
}
|
||||
}
|
||||
|
||||
class FileFunctionResponseEventHandle extends BaseMessageHandler{
|
||||
canHandle(event: any, _opts: any, prevMessageList?: any[]): boolean {
|
||||
if(typeof prevMessageList === 'undefined'){
|
||||
return false
|
||||
}
|
||||
let prevMessage = prevMessageList[prevMessageList.length - 1];
|
||||
|
||||
if( prevMessage?.event.data.type === "function_call" && event.data.type ==="tool_response"){
|
||||
return true
|
||||
}else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
handle(event: any, prevMessageList: any[], opts: any): any[] {
|
||||
return [...prevMessageList.slice(0,prevMessageList.length-1),this.createMessage('',event.data.role,event,opts)]
|
||||
}
|
||||
}
|
||||
|
||||
export class MessageHandlerStrategy {
|
||||
private handlers: MessageHandler[];
|
||||
|
||||
constructor() {
|
||||
this.handlers = [
|
||||
new ConversationHelloHandler(),
|
||||
new IgnoredEventHandler(),
|
||||
new UserMessageEventHandle(),
|
||||
new NormalMessageEventHandle(),
|
||||
new AppendMessageEventHandle(),
|
||||
new FileFunctionCallEventHandle(),
|
||||
new FileFunctionResponseEventHandle(),
|
||||
new BottomMessageEventHandle()
|
||||
];
|
||||
}
|
||||
|
||||
process(event: any, prevMessageList: any[], opts: any): any[] {
|
||||
for (const handler of this.handlers) {
|
||||
if (handler.canHandle(event, opts, prevMessageList)) {
|
||||
return handler.handle(event, prevMessageList, opts);
|
||||
}
|
||||
}
|
||||
return prevMessageList; // 理论上不会执行到这里
|
||||
}
|
||||
}
|
||||
|
|
@ -8,20 +8,17 @@ export const useWishList = () => {
|
|||
const { getSignal } = useAbortController();
|
||||
const [searchParams]= useSearchParams();
|
||||
|
||||
const locationCode = searchParams.get("locationCode") || '';
|
||||
const token = searchParams.get("token") || '';
|
||||
|
||||
const getWishList = async () => {
|
||||
const res = await fetchWishList({params:{locationCode:locationCode},options:{signal:getSignal(),headers: { Authorization: `Bearer ${token}` },}});
|
||||
const res = await fetchWishList({params:{},options:{signal:getSignal(),headers: { Authorization: `Bearer ${token}` },}});
|
||||
const _wishList = res.result as any[];
|
||||
setWishList(_wishList);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if(locationCode){
|
||||
getWishList();
|
||||
}
|
||||
}, [locationCode]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
wishList,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
# 服务器信息
|
||||
SERVER_USER="root"
|
||||
SERVER_HOST="106.14.30.150"
|
||||
SERVER_PATH="/opt/1panel/apps/openresty/openresty/www/sites/chat.ycymedu.com/index"
|
||||
SERVER_PATH="/opt/1panel/apps/openresty/openresty/www/sites/m.senior.ycymedu.com/index"
|
||||
PRIVATE_KEY="ALIYUN.pem"
|
||||
BACKUP_PATH="${SERVER_PATH}-backup-$(date +%Y%m%d%H%M%S).zip"
|
||||
DINGDING_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=fca104958fea6273c9c7ef3f08b3d552645c214f929066785e8caf6e1885a5a6"
|
||||
|
|
|
|||
Loading…
Reference in New Issue