feat: AI对话期间静止打断

master
xjs 2025-04-22 15:45:17 +08:00
parent ca04c56af2
commit 16a39a87cb
31 changed files with 1547 additions and 294 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index-CR5a_mms.js","assets/.pnpm-CAIuqsZ0.js","assets/use-toast-DO4tfD4I.js","assets/index-PFueeGmc.css"])))=>i.map(i=>d[i]);
import{_ as t}from"./index-DOYUXUr3.js";import{j as r,r as a}from"./.pnpm-CAIuqsZ0.js";const o=a.lazy(()=>t(()=>import("./index-CR5a_mms.js"),__vite__mapDeps([0,1,2,3])));function i(){return r.jsx("div",{className:"h-full bg-[#F4F6FA]",children:r.jsx(o,{})})}export{i as default};

View File

@ -1,2 +0,0 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index-DQMJjaAj.js","assets/.pnpm-BDdfG1pO.js","assets/index-PFueeGmc.css"])))=>i.map(i=>d[i]);
import{_ as t}from"./index-j0VtgfAk.js";import{j as r,r as a}from"./.pnpm-BDdfG1pO.js";const o=a.lazy(()=>t(()=>import("./index-DQMJjaAj.js"),__vite__mapDeps([0,1,2])));function i(){return r.jsx("div",{className:"h-full bg-[#F4F6FA]",children:r.jsx(o,{})})}export{i as default};

View File

@ -0,0 +1 @@
import{t as b,b as j,P as y,r as o,j as e,d,e as N,T as n,D as c,C as u,f as T,V as l,A as p,O as R}from"./.pnpm-CAIuqsZ0.js";import{u as V}from"./use-toast-DO4tfD4I.js";function r(...t){return b(j(t))}const C=y,f=o.forwardRef(({className:t,...a},s)=>e.jsx(l,{ref:s,className:r("fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",t),...a}));f.displayName=l.displayName;const k=N("group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",{variants:{variant:{default:"border bg-background text-foreground",destructive:"destructive group border-destructive bg-destructive text-destructive-foreground"}},defaultVariants:{variant:"default"}}),m=o.forwardRef(({className:t,variant:a,...s},i)=>e.jsx(d,{ref:i,className:r(k({variant:a}),t),...s}));m.displayName=d.displayName;const A=o.forwardRef(({className:t,...a},s)=>e.jsx(p,{ref:s,className:r("inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",t),...a}));A.displayName=p.displayName;const x=o.forwardRef(({className:t,...a},s)=>e.jsx(u,{ref:s,className:r("absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",t),"toast-close":"",...a,children:e.jsx(T,{className:"h-4 w-4"})}));x.displayName=u.displayName;const v=o.forwardRef(({className:t,...a},s)=>e.jsx(n,{ref:s,className:r("text-sm font-semibold [&+div]:text-xs",t),...a}));v.displayName=n.displayName;const g=o.forwardRef(({className:t,...a},s)=>e.jsx(c,{ref:s,className:r("text-sm opacity-90",t),...a}));g.displayName=c.displayName;function D(){const{toasts:t}=V();return e.jsxs(C,{children:[t.map(function({id:a,title:s,description:i,action:h,...w}){return e.jsxs(m,{...w,children:[e.jsxs("div",{className:"grid gap-1",children:[s&&e.jsx(v,{children:s}),i&&e.jsx(g,{children:i})]}),h,e.jsx(x,{})]},a)}),e.jsx(f,{})]})}function L(){return e.jsx(e.Fragment,{children:e.jsxs("div",{className:r("h-screen bg-background font-sans antialiased"),children:[e.jsx(R,{}),e.jsx(D,{})]})})}export{L as default};

View File

@ -1 +0,0 @@
import{t as s,b as n,j as t,O as e}from"./.pnpm-BDdfG1pO.js";function r(...a){return s(n(a))}function c(){return t.jsx(t.Fragment,{children:t.jsx("div",{className:r("h-dvh bg-background font-sans antialiased"),children:t.jsx(e,{})})})}export{c as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/App-Dh3vQfIK.js","assets/.pnpm-BDdfG1pO.js","assets/MainLayout-BWiadrJR.js"])))=>i.map(i=>d[i]);
var v=Object.defineProperty;var x=(s,t,n)=>t in s?v(s,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):s[t]=n;var m=(s,t,n)=>x(s,typeof t!="symbol"?t+"":t,n);import{r as d,c as P,j as a,a as _,R as L}from"./.pnpm-BDdfG1pO.js";(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))f(e);new MutationObserver(e=>{for(const o of e)if(o.type==="childList")for(const r of o.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&f(r)}).observe(document,{childList:!0,subtree:!0});function n(e){const o={};return e.integrity&&(o.integrity=e.integrity),e.referrerPolicy&&(o.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?o.credentials="include":e.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function f(e){if(e.ep)return;e.ep=!0;const o=n(e);fetch(e.href,o)}})();const j="modulepreload",O=function(s){return"/"+s},h={},p=function(t,n,f){let e=Promise.resolve();if(n&&n.length>0){document.getElementsByTagName("link");const r=document.querySelector("meta[property=csp-nonce]"),i=(r==null?void 0:r.nonce)||(r==null?void 0:r.getAttribute("nonce"));e=Promise.allSettled(n.map(c=>{if(c=O(c),c in h)return;h[c]=!0;const u=c.endsWith(".css"),E=u?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${c}"]${E}`))return;const l=document.createElement("link");if(l.rel=u?"stylesheet":j,u||(l.as="script"),l.crossOrigin="",l.href=c,i&&l.setAttribute("nonce",i),document.head.appendChild(l),u)return new Promise((y,g)=>{l.addEventListener("load",y),l.addEventListener("error",()=>g(new Error(`Unable to preload CSS for ${c}`)))})}))}function o(r){const i=new Event("vite:preloadError",{cancelable:!0});if(i.payload=r,window.dispatchEvent(i),!i.defaultPrevented)throw r}return e.then(r=>{for(const i of r||[])i.status==="rejected"&&o(i.reason);return t().catch(o)})},R=d.lazy(()=>p(()=>import("./App-Dh3vQfIK.js"),__vite__mapDeps([0,1]))),S=d.lazy(()=>p(()=>import("./MainLayout-BWiadrJR.js"),__vite__mapDeps([2,1])));class w extends d.Component{constructor(){super(...arguments);m(this,"state",{hasError:!1})}static getDerivedStateFromError(n){return{hasError:!0}}render(){return this.state.hasError?a.jsx("h1",{children:"出错了,请稍后再试。"}):this.props.children}}const b=P([{path:"/",element:a.jsx(S,{}),errorElement:a.jsx(w,{children:a.jsx("div",{className:"flex-auto flex flex-col p-6",children:"出错了,请稍后再试。"})}),children:[{path:"/",element:a.jsx(R,{})}]}]);_(document.getElementById("root")).render(a.jsx(d.StrictMode,{children:a.jsx(L,{router:b})}));export{p as _};
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/App-BkgWMpGx.js","assets/.pnpm-CAIuqsZ0.js","assets/MainLayout-BMCWJnk-.js","assets/use-toast-DO4tfD4I.js"])))=>i.map(i=>d[i]);
var v=Object.defineProperty;var x=(s,t,n)=>t in s?v(s,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):s[t]=n;var m=(s,t,n)=>x(s,typeof t!="symbol"?t+"":t,n);import{r as d,c as P,j as a,a as _,R as L}from"./.pnpm-CAIuqsZ0.js";(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))f(e);new MutationObserver(e=>{for(const o of e)if(o.type==="childList")for(const r of o.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&f(r)}).observe(document,{childList:!0,subtree:!0});function n(e){const o={};return e.integrity&&(o.integrity=e.integrity),e.referrerPolicy&&(o.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?o.credentials="include":e.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function f(e){if(e.ep)return;e.ep=!0;const o=n(e);fetch(e.href,o)}})();const j="modulepreload",O=function(s){return"/"+s},h={},p=function(t,n,f){let e=Promise.resolve();if(n&&n.length>0){document.getElementsByTagName("link");const r=document.querySelector("meta[property=csp-nonce]"),i=(r==null?void 0:r.nonce)||(r==null?void 0:r.getAttribute("nonce"));e=Promise.allSettled(n.map(c=>{if(c=O(c),c in h)return;h[c]=!0;const u=c.endsWith(".css"),E=u?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${c}"]${E}`))return;const l=document.createElement("link");if(l.rel=u?"stylesheet":j,u||(l.as="script"),l.crossOrigin="",l.href=c,i&&l.setAttribute("nonce",i),document.head.appendChild(l),u)return new Promise((y,g)=>{l.addEventListener("load",y),l.addEventListener("error",()=>g(new Error(`Unable to preload CSS for ${c}`)))})}))}function o(r){const i=new Event("vite:preloadError",{cancelable:!0});if(i.payload=r,window.dispatchEvent(i),!i.defaultPrevented)throw r}return e.then(r=>{for(const i of r||[])i.status==="rejected"&&o(i.reason);return t().catch(o)})},R=d.lazy(()=>p(()=>import("./App-BkgWMpGx.js"),__vite__mapDeps([0,1]))),S=d.lazy(()=>p(()=>import("./MainLayout-BMCWJnk-.js"),__vite__mapDeps([2,1,3])));class w extends d.Component{constructor(){super(...arguments);m(this,"state",{hasError:!1})}static getDerivedStateFromError(n){return{hasError:!0}}render(){return this.state.hasError?a.jsx("h1",{children:"出错了,请稍后再试。"}):this.props.children}}const b=P([{path:"/",element:a.jsx(S,{}),errorElement:a.jsx(w,{children:a.jsx("div",{className:"flex-auto flex flex-col p-6",children:"出错了,请稍后再试。"})}),children:[{path:"/",element:a.jsx(R,{})}]}]);_(document.getElementById("root")).render(a.jsx(d.StrictMode,{children:a.jsx(L,{router:b})}));export{p as _};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
import{r as c}from"./.pnpm-CAIuqsZ0.js";const d=1,p=1e6;let i=0;function A(){return i=(i+1)%Number.MAX_SAFE_INTEGER,i.toString()}const a=new Map,S=t=>{if(a.has(t))return;const s=setTimeout(()=>{a.delete(t),n({type:"REMOVE_TOAST",toastId:t})},p);a.set(t,s)},f=(t,s)=>{switch(s.type){case"ADD_TOAST":return{...t,toasts:[s.toast,...t.toasts].slice(0,d)};case"UPDATE_TOAST":return{...t,toasts:t.toasts.map(e=>e.id===s.toast.id?{...e,...s.toast}:e)};case"DISMISS_TOAST":{const{toastId:e}=s;return e?S(e):t.toasts.forEach(o=>{S(o.id)}),{...t,toasts:t.toasts.map(o=>o.id===e||e===void 0?{...o,open:!1}:o)}}case"REMOVE_TOAST":return s.toastId===void 0?{...t,toasts:[]}:{...t,toasts:t.toasts.filter(e=>e.id!==s.toastId)}}},r=[];let T={toasts:[]};function n(t){T=f(T,t),r.forEach(s=>{s(T)})}function E({...t}){const s=A(),e=u=>n({type:"UPDATE_TOAST",toast:{...u,id:s}}),o=()=>n({type:"DISMISS_TOAST",toastId:s});return n({type:"ADD_TOAST",toast:{...t,id:s,open:!0,onOpenChange:u=>{u||o()}}}),{id:s,dismiss:o,update:e}}function _(){const[t,s]=c.useState(T);return c.useEffect(()=>(r.push(s),()=>{const e=r.indexOf(s);e>-1&&r.splice(e,1)}),[t]),{...t,toast:E,dismiss:e=>n({type:"DISMISS_TOAST",toastId:e})}}export{_ as u};

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -18,14 +18,14 @@
<link rel="icon" href="https://api.static.ycymedu.com/src/images/home/app-logo.svg" />
<meta name="author" content="HideInMatrix" />
<!-- <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
// VConsole will be exported to `window.VConsole` by default.
var vConsole = new window.VConsole();
</script> -->
<script type="module" crossorigin src="/assets/index-j0VtgfAk.js"></script>
<link rel="modulepreload" crossorigin href="/assets/.pnpm-BDdfG1pO.js">
<link rel="stylesheet" crossorigin href="/assets/index-ChXXIJVe.css">
</script>
<script type="module" crossorigin src="/assets/index-DOYUXUr3.js"></script>
<link rel="modulepreload" crossorigin href="/assets/.pnpm-CAIuqsZ0.js">
<link rel="stylesheet" crossorigin href="/assets/index-DPqYJA2j.css">
</head>
<body>
<div id="root"></div>

View File

@ -25,8 +25,11 @@
"lucide-react": "^0.437.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^10.1.0",
"react-router-dom": "^6.26.1",
"react-sortablejs": "^6.1.4",
"react-syntax-highlighter": "^15.6.1",
"remark-gfm": "^4.0.1",
"sortablejs": "^1.15.3",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,19 +1,39 @@
import { getRequest } from "@/lib/customFetch";
export const fetchUserToken = async ({
options,
}: {
options?: { signal?: AbortSignal,headers?:Record<string,string> };
}) => {
const response = await getRequest(
"https://api.v3.ycymedu.com/api/sysOnlineUser/hasitexpired",
{},
options
);
if(response.code === 200){
return {result:response.result}
}else{
return {result:[],message:response.message}
}
};
options,
}: {
options?: { signal?: AbortSignal; headers?: Record<string, string> };
}) => {
const response = await getRequest(
"https://api.v3.ycymedu.com/api/sysOnlineUser/hasitexpired",
{},
options
);
if (response.code === 200) {
return { result: response.result };
} else {
return { result: [], message: response.message };
}
};
export const fetchReport = async ({
params,
options,
}: {
params: { Type: string; Id: string };
options?: { signal?: AbortSignal; headers?: Record<string, string> };
}) => {
const response = await getRequest(
"https://api.v3.ycymedu.com/api/busScale/GetBusAIReportKeyWord",
params,
options
);
if (response.code === 200) {
return { result: response.result };
} else {
return { result: [], message: response.message };
}
};

View File

@ -5,44 +5,96 @@ 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 { fetchReport, fetchUserToken } from "@/apis/user";
import { useToast } from "@/hooks/use-toast";
import { useAbortController } from "@/hooks/useAbortController";
import { ReportContext } from "@/components/Provider/ReportResolveProvider";
export default function Antechamber() {
const { handleConnect } = useContext(RealtimeClientContext);
const { setHasHandledReport,hasHandledReport } = useContext(ReportContext);
const [searchParams] = useSearchParams();
const [disable,setDisable] = useState(true);
const token = searchParams.get("token") || '';
const reportId = searchParams.get("reportId") || '';
const reportType = searchParams.get("reportType") || '';
const { toast } = useToast();
const { getSignal } = useAbortController();
const getUserToken = async ({ signal }: { signal: AbortSignal }) => {
const { result, message } = await fetchUserToken({ options: { signal,headers:{"Authorization":`Bearer ${token}`} } });
if (message) {
console.log(message);
} else {
const _result = result as {isExpired:boolean};
setDisable(!_result.isExpired);
const getUserToken = async () => {
try {
const { result, message } = await fetchUserToken({
options: {
signal: getSignal(),
headers: {"Authorization":`Bearer ${token}`}
}
});
if (message) {
console.log(message);
} else {
const _result = result as {isExpired:boolean;msg:string};
setDisable(!_result.isExpired);
if(!_result.isExpired &&_result.msg){
toast({
title: _result.msg,
description: "请重新登录",
});
}
}
} catch (error: any) {
if (error.name !== 'AbortError') {
console.error('获取用户令牌失败:', error);
}
}
};
useEffect(()=>{
const controller = new AbortController();
const { signal } = controller;
getUserToken({ signal });
},[token])
const toRoom = () => {
// if(disable){
// return;
// }
handleConnect();
const getReport = async () => {
try {
const { result, message } = await fetchReport({
params:{Type:reportType,Id:reportId},
options: {
signal: getSignal(),
headers: {"Authorization":`Bearer ${token}`}
}
});
if (message) {
console.log(message);
} else {
handleConnect(result as string);
setHasHandledReport(true)
}
} catch (error: any) {
if (error.name !== 'AbortError') {
console.error('获取报告失败:', error);
}
}
}
useEffect(() => {
getUserToken();
}, [token]);
useEffect(() => {
if(reportId && reportType && !hasHandledReport){
getReport();
}
}, [reportId, reportType,hasHandledReport]);
const toRoom = (initMessage?:string) => {
if(disable){
return;
}
handleConnect(initMessage);
};
return (
<div className="flex flex-col items-center h-full">
<AntechamberHeader toRoom={toRoom} />
<AntechamberScore toRoom={toRoom} />
<InvokeButton disable={disable} onClick={toRoom} />
<InvokeButton disable={disable} onClick={() => toRoom()} />
</div>
);
}

View File

@ -3,7 +3,7 @@ import RoomController from "@/components/RoomController";
export default function Room() {
return (
<div className="flex flex-col h-full">
<div className="flex flex-col max-h-full h-full">
<RoomConversation />
<RoomController />
</div>

View File

@ -1,14 +1,32 @@
import Room from "./Room";
import Antechamber from "./Antechamber";
import { useContext } from "react";
import { useContext, useEffect } from "react";
import {
RealtimeClientContext,
RealtimeClientProvider,
} from "@/components/Provider/RealtimeClientProvider";
import { ReportProvider } from "@/components/Provider/ReportResolveProvider";
import { RealtimeUtils } from "@coze/realtime-api";
function MainContent() {
const { isConnected } = useContext(RealtimeClientContext);
return isConnected ? <Room /> : <Antechamber />;
const handlePromise = async() => {
await RealtimeUtils.checkDevicePermission(false);
}
useEffect(() => {
handlePromise();
}, []);
return (
<ReportProvider>
{isConnected ? <Room /> : <Antechamber />}
</ReportProvider>
);
}
export default function MainArea() {

View File

@ -3,12 +3,14 @@
import { cn } from "@/lib/utils";
import { Outlet } from "react-router-dom";
import { Toaster } from "@/components/ui/toaster"
export default function LocaleLayout() {
return (
<>
<div className={cn("h-dvh bg-background font-sans antialiased")}>
<div className={cn("h-screen bg-background font-sans antialiased")}>
<Outlet />
<Toaster />
</div>
</>
);

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useContext } from "react";
import { useState, useEffect } from "react";
import HelloGIF from "/icons/hello.gif";
import WhatsThing from "/icons/whatsThing.png";
@ -6,10 +6,10 @@ import CircleIcon from "/icons/circle.png";
import RightIcon from "/icons/right.png";
import styles from "./index.module.css";
import { fetchQuestions } from "@/apis/questions";
import { RealtimeClientContext } from "../Provider/RealtimeClientProvider";
import { useAbortController } from "@/hooks/useAbortController";
type Props = {
toRoom: () =>void;
toRoom: (initMessage?:string) =>void;
};
export default function HeaderGroup({ toRoom }: Props) {
@ -17,7 +17,7 @@ export default function HeaderGroup({ toRoom }: Props) {
const [displayQuestions, setDisplayQuestions] = useState<string[]>([]);
const [allQuestions, setAllQuestions] = useState<string[]>([]);
const { setInitMessage } = useContext(RealtimeClientContext);
const { getSignal } = useAbortController();
// 随机获取4个问题的函数
const getRandomQuestions = () => {
@ -33,8 +33,8 @@ export default function HeaderGroup({ toRoom }: Props) {
return result;
};
const getQuestion = async ({ signal }: { signal: AbortSignal }) => {
const { result, message } = await fetchQuestions({ options: { signal } });
const getQuestion = async () => {
const { result, message } = await fetchQuestions({ options: { signal:getSignal() } });
if (message) {
console.log(message);
} else {
@ -48,9 +48,7 @@ export default function HeaderGroup({ toRoom }: Props) {
// 组件初始化时获取随机问题
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
getQuestion({ signal });
getQuestion();
}, []);
const handleClick = () => {
@ -66,8 +64,7 @@ export default function HeaderGroup({ toRoom }: Props) {
};
const handleQuestion = async (question: string) => {
setInitMessage(question);
await toRoom();
toRoom(question);
};
return (

View File

@ -4,11 +4,9 @@ import { useSearchParams } from 'react-router-dom';
import MyInputIcon from '/icons/myInput.png';
import RightBlueIcon from '/icons/rightBlue.png';
import style from './index.module.css';
import { RealtimeClientContext } from '../Provider/RealtimeClientProvider';
import { useContext } from 'react';
type Props = {
toRoom: () => void;
toRoom: (initMessage?:string) => void;
};
export default function MyInput({ toRoom }: Props) {
@ -19,11 +17,9 @@ export default function MyInput({ toRoom }: Props) {
const subjectGroup = searchParams.get('subjectGroup') || '物/化/史'
const expectedScore = searchParams.get('expectedScore') || 500
const {setInitMessage} = useContext(RealtimeClientContext)
const handleQuestion = async () => {
toRoom();
setInitMessage(`我的高考地点在${provinceName},我选择的科目是${subjectGroup},我的高考分数为${expectedScore}分。我适合哪些学校和专业`);
toRoom(`我的高考地点在${provinceName},我选择的科目是${subjectGroup},我的高考分数为${expectedScore}分。我适合哪些学校和专业`);
};
return (

View File

@ -11,7 +11,6 @@ import {
ReactNode,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react";
@ -33,13 +32,12 @@ export const RealtimeClientContext = createContext<{
messageList: { content: string; role: RoleType }[];
isAiTalking: boolean;
roomInfo: RoomInfo | null;
initClient: () => void;
handleConnect: () => void;
initClient: (initMessage?:string) => void;
handleConnect: (initMessage?:string) => Promise<void>;
handleInterrupt: () => void;
handleDisconnect: () => void;
toggleMicrophone: () => void;
sendUserMessageWithText: (message: string) => void;
setInitMessage: (message: string) => void;
}>({
client: null,
isConnecting: false,
@ -50,12 +48,11 @@ export const RealtimeClientContext = createContext<{
isAiTalking: false,
roomInfo: null,
initClient: () => {},
handleConnect: () => {},
handleConnect: () => Promise.resolve(),
handleInterrupt: () => {},
handleDisconnect: () => {},
toggleMicrophone: () => {},
sendUserMessageWithText: () => {},
setInitMessage: () => {},
});
// 添加自定义hook
@ -89,57 +86,62 @@ export const RealtimeClientProvider = ({
// 是否已连接
const [isConnected, setIsConnected] = useState(false);
// 是否开启麦克风
const [audioEnabled, setAudioEnabled] = useState(false);
const [audioEnabled, setAudioEnabled] = useState(true);
// 是否支持视频
const [isSupportVideo] = useState(false);
// 是否正在说话
const [isAiTalking, setIsAiTalking] = useState(false);
const [initMessage, setInitMessage] = useState("");
const [isClientInitialized, setIsClientInitialized] = useState(false);
const [roomInfo, setRoomInfo] = useState<RoomInfo | null>(null);
const { toast } = useToast();
const initClient = async () => {
const initClient = async (_initMessage?:string) => {
const permission = await RealtimeUtils.checkDevicePermission(false);
const device = await RealtimeUtils.getAudioDevices();
if (!permission.audio) {
toast({
title: "连接错误",
description: "需要麦克风访问权限",
});
throw new Error("需要麦克风访问权限");
}else{
const client = new RealtimeClient({
accessToken: token,
botId: botId,
voiceId: voiceId,
connectorId: connectorId,
allowPersonalAccessTokenInBrowser: true, // 可选:允许在浏览器中使用个人访问令牌
});
clientRef.current = client;
setupEventListeners(client);
setIsClientInitialized(true);
}
if (device.audioInputs.length === 0) {
toast({
title: "连接错误",
description: "没有麦克风设备",
});
throw new Error("没有麦克风设备");
}
const client = new RealtimeClient({
accessToken: token,
botId: botId,
voiceId: voiceId,
connectorId: connectorId,
allowPersonalAccessTokenInBrowser: true, // 可选:允许在浏览器中使用个人访问令牌
});
clientRef.current = client;
setupEventListeners(client);
setupMessageEventListeners(client,_initMessage ?? '');
setupInitMessageEventListener(client,_initMessage)
};
useEffect(() => {
if (clientRef.current) {
setupMessageEventListeners(clientRef.current);
if (initMessage) {
setupInitMessageEventListener(clientRef.current);
}
}
}, [initMessage, isClientInitialized]);
const handleConnect = async () => {
const handleConnect = async (initMessage?:string) => {
try {
if (!clientRef.current) {
await initClient();
await initClient(initMessage);
}
await clientRef.current?.connect();
await toggleMicrophone();
} catch (error) {
console.error(error);
if (error instanceof RealtimeAPIError) {
@ -170,12 +172,13 @@ export const RealtimeClientProvider = ({
}
};
const handleDisconnect = () => {
const handleDisconnect = async() => {
try {
// 关闭客户的时候清除一些信息
setIsAiTalking(false);
setIsClientInitialized(false);
setMessageList([]);
await clientRef.current?.setAudioEnable(false);
setAudioEnabled(false);
clientRef.current?.disconnect();
clientRef.current?.clearEventHandlers();
@ -195,16 +198,36 @@ export const RealtimeClientProvider = ({
}
};
const setupInitMessageEventListener = (client: RealtimeClient) => {
client.on(EventNames.ALL_SERVER, (eventName, _event: any) => {
const setupInitMessageEventListener = useCallback((client: RealtimeClient,_initMessage?:string) => {
client.on(EventNames.ALL_SERVER, async(eventName, _event: any) => {
if (eventName === "server.session.created") {
await client.sendMessage({
id:'',
"event_type":"session.update",
data:{
chat_config:{
allow_voice_interrupt:false
}
}
})
}
if(eventName === "server.bot.join" && _initMessage){
// 这里需要加个 server. 前缀
sendUserMessageWithText(initMessage);
await clientRef.current?.sendMessage({
id: "",
event_type: "conversation.message.create",
data: {
role: "user",
content_type: "text",
content: _initMessage,
},
});
}
});
};
},[clientRef.current]);
const setupMessageEventListeners = (client: RealtimeClient) => {
const setupMessageEventListeners = (client: RealtimeClient,_initMessage:string) => {
let lastEvent: any;
client.on(EventNames.ALL, (_eventName, event: any) => {
// AI智能体设置
@ -233,7 +256,7 @@ export const RealtimeClientProvider = ({
}
// 添加AI的欢迎语
if (initMessage === "" && event.event_type === "conversation.created") {
if (_initMessage === "" && event.event_type === "conversation.created") {
return [
...prev,
{ content: event.data.prologue, role: RoleType.Assistant },
@ -260,16 +283,18 @@ export const RealtimeClientProvider = ({
const setupEventListeners = useCallback(
(client: RealtimeClient) => {
// 监听 AI 开始说话事件
client.on(EventNames.AUDIO_AGENT_SPEECH_STARTED, () => {
client.on(EventNames.AUDIO_AGENT_SPEECH_STARTED, async() => {
// console.log("AI开始说话");
setIsAiTalking(true);
await clientRef.current?.setAudioEnable(false);
setAudioEnabled(false);
});
// 监听 AI 结束说话事件
client.on(EventNames.AUDIO_AGENT_SPEECH_STOPPED, () => {
client.on(EventNames.AUDIO_AGENT_SPEECH_STOPPED, async() => {
// console.log("AI结束说话");
setIsAiTalking(false);
await clientRef.current?.setAudioEnable(true);
setAudioEnabled(true);
});
@ -286,25 +311,25 @@ export const RealtimeClientProvider = ({
setIsConnected(true);
});
},
[clientRef.current, initMessage]
[clientRef.current]
);
// 发送信息
const sendUserMessageWithText = async (message: string) => {
try {
await clientRef.current?.sendMessage({
id: "",
event_type: "conversation.message.create",
data: {
role: "user",
content_type: "text",
content: message,
},
});
} catch (error) {
console.error("发送消息失败:" + error);
}
};
// const sendUserMessageWithText = async (message: string) => {
// try {
// await clientRef.current?.sendMessage({
// id: "",
// event_type: "conversation.message.create",
// data: {
// role: "user",
// content_type: "text",
// content: message,
// },
// });
// } catch (error) {
// console.error("发送消息失败:" + error);
// }
// };
return (
<RealtimeClientContext.Provider
@ -322,8 +347,7 @@ export const RealtimeClientProvider = ({
handleInterrupt,
handleDisconnect,
toggleMicrophone,
sendUserMessageWithText,
setInitMessage,
}}
>
{children}

View File

@ -0,0 +1,20 @@
import { createContext, useState } from "react";
export const ReportContext = createContext<{
hasHandledReport: boolean;
setHasHandledReport: (value: boolean) => void;
}>({
hasHandledReport: false,
setHasHandledReport: () => {},
});
export const ReportProvider = ({ children }: { children: React.ReactNode }) => {
const [hasHandledReport, setHasHandledReport] = useState(false);
return (
<ReportContext.Provider value={{ hasHandledReport, setHasHandledReport }}>
{children}
</ReportContext.Provider>
);
};

View File

@ -28,7 +28,7 @@ function AudioController({className, ...rest}: React.HTMLAttributes<HTMLDivEleme
return (
<div className={`${className} flex items-center justify-center bg-white pb-[20px] pt-[10px] gap-[10px]`} {...rest}>
<div className={`flex items-center justify-center bg-white pb-[20px] pt-[10px] gap-[10px]`} {...rest}>
<div className={`${style.microphoneWrapper}`} onClick={handleDisconnect}>
<img src={HandleOffIcon} alt="handoff" />
<div></div>

View File

@ -1,5 +1,7 @@
import { useRef, useEffect, useContext } from "react";
import { RealtimeClientContext } from "../Provider/RealtimeClientProvider";
import ReactMarkdown from 'react-markdown'
import gfm from 'remark-gfm'
import { RoleType } from "@coze/api";
export default function RoomConversation() {
@ -16,7 +18,14 @@ export default function RoomConversation() {
}, [messageList]);
return (
<div className="flex-1 flex flex-col h-full">
<div className="flex-1 flex flex-col overflow-y-auto">
<div className="w-full min-h-[120px] h-[120px]">
<div className="relative h-full">
<img src="/icons/hello.gif" alt="" className="absolute top-0 h-[97px] left-[50%] translate-x-[-50%]"/>
<img src="/icons/conversation-bg.png" alt="background" className='w-[222px] h-[49px] absolute bottom-0 left-[50%] translate-x-[-50%]'/>
<div className="text-black text-[14px] absolute bottom-[22px] left-[50%] translate-x-[-50%] z-[10]">HeyAI</div>
</div>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messageList.map((message: any, index: number) => (
<div
@ -34,7 +43,7 @@ export default function RoomConversation() {
: "bg-blue-500 text-white rounded-tr-none"
}`}
>
{message.content}
<ReactMarkdown remarkPlugins={[gfm]}>{message.content}</ReactMarkdown>
</div>
</div>
))}

View File

@ -0,0 +1,41 @@
import { useEffect, useRef } from 'react';
export function useAbortController() {
const controllerRef = useRef<AbortController | null>(null);
// 获取AbortSignal实例
const getSignal = () => {
if (!controllerRef.current) {
controllerRef.current = new AbortController();
}
return controllerRef.current.signal;
};
// 中止所有请求
const abortAll = () => {
if (controllerRef.current) {
controllerRef.current.abort();
controllerRef.current = null;
}
};
// 创建新的Controller并中止之前的请求
const recreate = () => {
abortAll();
controllerRef.current = new AbortController();
return controllerRef.current.signal;
};
// 组件卸载时自动中止所有请求
useEffect(() => {
return () => {
abortAll();
};
}, []);
return {
getSignal,
abortAll,
recreate
};
}

View File

@ -10,6 +10,7 @@ export const useSignalRConnection = (params: {
const connectionRef = useRef<signalR.HubConnection | null>(null);
const { handleDisconnect } = useRealtimeClient();
const { toast } = useToast();
const timerRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
if (!params.access_token || !params.roomId) {
@ -51,7 +52,7 @@ export const useSignalRConnection = (params: {
.start()
.then(() => {
console.log("SignalR连接已建立");
setInterval(() => {
timerRef.current = setInterval(() => {
connection.invoke("Ping");
}, 5000);
})
@ -64,6 +65,7 @@ export const useSignalRConnection = (params: {
connectionRef.current
.stop()
.then(() => {
clearInterval(timerRef.current as NodeJS.Timeout);
console.log("SignalR连接已关闭");
})
.catch((err) => {

View File