feat: init commit
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
.env
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEowIBAAKCAQEA2SOM6Heb+BNGha/ucoV+GopddM7ckyGWALhGPJp/Z7P5jgw6
|
||||||
|
NSJ1G/E7CFEukaLC50eupji9mA4o8Emtrgn8y7uMIc5lafHe0IPy+WA90PZyien4
|
||||||
|
0u7dD0NrEbKH41SIEuZFbGev0CgQJsxmkS8CmOytmglyJ0JDBpJD9tQLwSuG9kad
|
||||||
|
DrYPArQdQu+ZALt4gyG7m14c4mZ2hAcIUwNYqFY9g/HIp0q9al8SW3WqzkB1U7GS
|
||||||
|
kh3i/MwtUvZWPI16aKW4/tdUX7y8PPguHQ1MWLd6DI0iGo9tqTQt35o9ax41O+0S
|
||||||
|
EY4MgU1Q3XJaMrmrYUpZ2/Y4xJbHOHgyi6afzQIDAQABAoIBAGms4ovUgkSmZOD1
|
||||||
|
MU/s5eVWx4rsje7RHqa1CAHAkxbOQTq/eqiXX3U83qT6lXZtRvu2KCpfXO4eng/r
|
||||||
|
W6pi0/P3D4j4YOTBwNWsEdkJ3KvQ9QdnpiBJ/a3K+tW/FGEvp5XDGbBbefYNOWcY
|
||||||
|
fSZVQadZMFfSFwtCNUqCbq82nY3hkoFVGiQ9EHUlvNCQM4y5VeJuCPzsl8rzAsyo
|
||||||
|
gpkHyKxU/CNg+f5UuPwostR5eTXgkp6nlpa65yDK0szsww78keE/J1tOB0d4r+Oe
|
||||||
|
12ZVzYLQrzQwt2CwIGT9KkAUv7eO7ZTMDsG8MYNnRPGXKgjSZqBSW0MCq7ksz1/P
|
||||||
|
dHTJmSECgYEA9Q8VbnLkdk5NlRCNTONfjLhNUGmAqt/qLPi7tnJa0wfSpr3tg1Aj
|
||||||
|
AxnV455fT81vbDP8V8tGDNx+/d5jBkIsdsCYOMa6dqZr1HOfSgbH8wETtU9mbKdB
|
||||||
|
Gt/frdM3rkJvyd5RjoqMex4U7x4f1OWfThHVzvi8TzqfRSXNpbXBMdUCgYEA4tVa
|
||||||
|
TBQhwgnjMHl9OYrW1urtf2fKEGOZA/uGJI55O7IL2SoYnVjFUHjlE4D94K/hdijp
|
||||||
|
l2oFydD0GrWVkWULPvTdUFzMTRPH+SUfXbVqXBMJfHJYrRuZHsJBH2jIwIzu4LaA
|
||||||
|
hliletIdJUtK8mUvwEb+4TlVUwfCYqkveu1EuhkCgYAHjK5pV6rIJkNnmznvK3YP
|
||||||
|
HMJs/sMTAJDzT7pgtYcsxynrLyC5EefyOYKIX6GqELclCzjz73Q6AzT6VzaPw8wg
|
||||||
|
4HAQF7c43oml4uX+XtUcHGViCY8rO7/atxjp/v7RJITTIEE89fG7/UJB15i9c1GE
|
||||||
|
EzKWDL2oZzLu62o5d676/QKBgQDgiCBhvmvMDs18ZkW2d+BBzTpaKvqxTmVgs9EM
|
||||||
|
zpripFNmG21SE1T9Wy4mKEEl7/NVaxoObzxbkSKQbb4ntcV0BB4uNi1k/ner/zsV
|
||||||
|
H0aw7YcuUGHGuNLQx6h+1tIhB2BNv1lposXq1aFUETuWxOKHib8yYfY7wiqATshY
|
||||||
|
/hRRwQKBgCkT2Wt/XjbXLCpX1viEJOLzYYQkrNyPvruXknCnQQ+0/cq4LoRqWZZR
|
||||||
|
4RjS0RkoOF4vM38u/hgICiUUCR4bYUy/4O7uhNu5tDle6lIeB+ZxVpRRedE0OfQT
|
||||||
|
tCpQd0yN9iONBlGyfAtpq7oNzuXgsl1OR4usZ8wCinfImChAl6wv
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020-2024 HideInMatrix
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
# 音乐地带
|
||||||
|
|
||||||
|
免费的听歌网站(开发中)
|
||||||
|
|
||||||
|
## ✨ 特性
|
||||||
|
|
||||||
|
- 使用React开发,nextjs分支使用Next.js 14版本开发(后期不维护了)。
|
||||||
|
- 🖥️ 支持 PWA,可在 Chrome/Edge 里点击地址栏右边的 ➕ 安装到电脑
|
||||||
|
- 无需VIP直接在线播放歌曲
|
||||||
|
- 🛠 更多特性开发中
|
||||||
|
|
||||||
|
## ⚙️ 部署至 Vercel
|
||||||
|
|
||||||
|
除了下载安装包使用,你还可以将本项目部署到 Vercel 或你的服务器上。下面是部署到 Vercel 的方法。
|
||||||
|
|
||||||
|
本项目的 Demo (https://freemusic.micromatrix.org) 就是部署在 Vercel 上的网站。
|
||||||
|
|
||||||
|
[](https://vercel.com/?utm_source=ohmusic&utm_campaign=oss)
|
||||||
|
|
||||||
|
1. 点击本仓库右上角的 Fork,复制本仓库到你的 GitHub 账号。
|
||||||
|
|
||||||
|
2. 打开 [Vercel.com](https://vercel.com),使用 GitHub 登录。
|
||||||
|
|
||||||
|
3. 点击 Import Git Repository 并选择你刚刚复制的仓库并点击 Import。
|
||||||
|
|
||||||
|
4. 点击 PERSONAL ACCOUNT 旁边的 Select。
|
||||||
|
|
||||||
|
|
||||||
|
## ☑️ Todo
|
||||||
|
|
||||||
|
迎提 Issue 和 Pull request。
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
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};
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
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};
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
._controller_5jmgu_1{display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:16px;background-color:#fff;padding:8px 15px;padding-bottom:calc(12px + constant(safe-area-inset-bottom));padding-bottom:calc(12px + env(safe-area-inset-bottom))}._talkWrapper_5jmgu_23{--h: 16px;display:flex;align-items:center;justify-content:center;cursor:pointer;padding:8px 16px;background-color:#e3efff;border-radius:20px;-webkit-user-select:none;-moz-user-select:none;user-select:none;gap:8px;min-width:116px;color:#0078ff}._listenerDot_5jmgu_53{display:flex;align-items:center;justify-content:center;position:relative;height:var(--h);width:36px}._listenerDot_5jmgu_53 span{display:inline-block;width:4px;margin:0 1px;background-color:#4898fc;border-radius:2px;height:calc(var(--h) - var(--d) * 4px);transition:height .2s ease;opacity:calc(1 - var(--d) * .4)}._isTalking_5jmgu_93 span{animation:_soundWave_5jmgu_1 1s infinite ease-in-out}._isTalking_5jmgu_93 span:nth-child(1){animation-delay:0s}._isTalking_5jmgu_93 span:nth-child(2){animation-delay:.2s}._isTalking_5jmgu_93 span:nth-child(3){animation-delay:.4s}._isTalking_5jmgu_93 span:nth-child(4){animation-delay:.6s}._isTalking_5jmgu_93 span:nth-child(5){animation-delay:.8s}@keyframes _soundWave_5jmgu_1{0%,to{height:4px}50%{height:16px}}._microphoneWrapper_5jmgu_157{display:flex;align-items:center;background-color:#f6f6f6;padding:10px 15px;border-radius:400px}._timerWrapper_5jmgu_173{display:flex;align-items:center;justify-content:center;background-color:#f6f6f6;padding:8px 14px;border-radius:400px;color:#0078ff;font-size:14px;font-weight:500;width:100%}._headerWrapper_lrk2k_1{margin-top:65px;width:100%;padding:0 16px;box-sizing:border-box}._wrapper_lrk2k_15{height:105px;background:#b0e4ffcc;border-radius:50px 13px 13px;border:1px solid #ffffff;position:relative;z-index:0;margin-bottom:-54px}._img_lrk2k_37{width:120px;height:123px;-o-object-fit:contain;object-fit:contain;position:absolute;bottom:35px}._wrapper_lrk2k_15:before{content:"";position:absolute;width:133px;height:32px;bottom:23px;left:6px;background:radial-gradient(ellipse at top,#1580ff,#fff);z-index:2;border-radius:20px;filter:blur(10px)}._text_lrk2k_81{font-weight:500;font-size:16px;color:#000;text-align:end;margin-top:15px;margin-right:45px}._main-wrapper_lrk2k_101{background:#ffffffb3;border-radius:13px;border:1px solid #ffffff;padding:16px 17px 18px;position:relative;z-index:1;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);min-height:213px;box-sizing:border-box}._main_lrk2k_101{display:flex;justify-content:space-between}._thing_lrk2k_135{-o-object-fit:contain;object-fit:contain;position:relative;height:15px;width:91px}._circle_lrk2k_153{display:flex;align-items:center;justify-content:center;gap:2px;color:#2380e2}._circle_lrk2k_153 img{width:16px;height:16px}._rightIcon_lrk2k_179{width:10px;height:10px}._change_lrk2k_189{color:#2380e2}._tip_lrk2k_197{display:flex;align-items:center;justify-content:space-between;font-weight:400;font-size:16px;color:#000;margin-top:18px}._rotating_lrk2k_223{animation:_rotate360_lrk2k_1 1s linear}@keyframes _rotate360_lrk2k_1{0%{transform:rotate(0)}to{transform:rotate(360deg)}}._wrapper_e1j90_1{width:88px;height:88px;background:linear-gradient(180deg,#64c7ff,#0165ff);display:flex;flex-direction:column;justify-content:center;align-items:center;border-radius:50%;position:relative;margin-top:auto;margin-bottom:80px;overflow:hidden}._wrapper_e1j90_1:before{position:absolute;content:"";left:0;bottom:0;width:26px;height:26px;background:radial-gradient(farthest-corner at 100% 0%,#7bdcf0,#fff);filter:blur(14px)}._wrapper_e1j90_1:after{position:absolute;content:"";left:20%;top:0;width:26px;height:26px;background:radial-gradient(farthest-corner at 100% 0%,#7bdcf0,#a4dbe6);filter:blur(14px)}._text_e1j90_75{color:#fff;font-weight:500;font-size:13px}._call_e1j90_87{width:32px;height:32px;-o-object-fit:contain;object-fit:contain;margin-bottom:4px}._scoreWrapper_1g8jt_1{padding:15px;width:100%;box-sizing:border-box}._innerWrapper_1g8jt_13{padding:15px;width:100%;background:#ffffffb3;border-radius:13px;border:1px solid #f4f6fa;display:flex;align-items:center;justify-content:space-between;box-sizing:border-box}._detail_1g8jt_37{display:flex;align-items:center;gap:10px;margin-top:10px;font-size:16px}._right_1g8jt_53{color:#1580ff;display:flex;align-items:center}._imgIcon_1g8jt_65{width:91px;height:15px}._rightBlue_1g8jt_75{width:10px;height:10px}
|
||||||
|
|
@ -0,0 +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 _};
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 868 B |
|
After Width: | Height: | Size: 865 B |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 470 B |
|
After Width: | Height: | Size: 423 B |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 333 B |
|
After Width: | Height: | Size: 352 B |
|
After Width: | Height: | Size: 5.2 KiB |
|
|
@ -0,0 +1,33 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>六维小助手</title>
|
||||||
|
<meta name="description" content="AIGC对话" />
|
||||||
|
<meta name="generator" content="React" />
|
||||||
|
<meta name="keywords" content="music, music-site" />
|
||||||
|
<meta
|
||||||
|
name="theme-color"
|
||||||
|
content="#fff"
|
||||||
|
media="(prefers-color-scheme: dark)" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, user-scalable=no,maximum-scale=1.0, minimum-scale=1.0" />
|
||||||
|
|
||||||
|
|
||||||
|
<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>
|
||||||
|
// 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">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "src/globals.css",
|
||||||
|
"baseColor": "slate",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>六维小助手</title>
|
||||||
|
<meta name="description" content="AIGC对话" />
|
||||||
|
<meta name="generator" content="React" />
|
||||||
|
<meta name="keywords" content="music, music-site" />
|
||||||
|
<meta
|
||||||
|
name="theme-color"
|
||||||
|
content="#fff"
|
||||||
|
media="(prefers-color-scheme: dark)" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, user-scalable=no,maximum-scale=1.0, minimum-scale=1.0" />
|
||||||
|
|
||||||
|
|
||||||
|
<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>
|
||||||
|
// VConsole will be exported to `window.VConsole` by default.
|
||||||
|
var vConsole = new window.VConsole();
|
||||||
|
</script> -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
{
|
||||||
|
"name": "free-music-react",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host 0.0.0.0",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"upload": "bash ./upload.sh",
|
||||||
|
"build-and-upload": "pnpm run build && pnpm run upload"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@coze/api": "1.2.0",
|
||||||
|
"@coze/realtime-api": "^1.1.1",
|
||||||
|
"@microsoft/signalr": "^8.0.7",
|
||||||
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
"@radix-ui/react-slot": "^1.2.0",
|
||||||
|
"@radix-ui/react-toast": "^1.2.7",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "1.0.0",
|
||||||
|
"embla-carousel-react": "^8.2.0",
|
||||||
|
"lucide-react": "^0.437.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router-dom": "^6.26.1",
|
||||||
|
"react-sortablejs": "^6.1.4",
|
||||||
|
"sortablejs": "^1.15.3",
|
||||||
|
"tailwind-merge": "^2.5.2",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"vaul": "^0.9.1",
|
||||||
|
"zustand": "^4.5.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.9.0",
|
||||||
|
"@types/node": "^22.5.1",
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@types/sortablejs": "^1.15.8",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"eslint": "^9.9.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.9",
|
||||||
|
"globals": "^15.9.0",
|
||||||
|
"postcss": "^8.4.42",
|
||||||
|
"tailwindcss": "^3.4.10",
|
||||||
|
"typescript": "^5.5.3",
|
||||||
|
"typescript-eslint": "^8.0.1",
|
||||||
|
"vite": "^5.4.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 868 B |
|
After Width: | Height: | Size: 865 B |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 470 B |
|
After Width: | Height: | Size: 423 B |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 333 B |
|
After Width: | Height: | Size: 352 B |
|
After Width: | Height: | Size: 5.2 KiB |
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { getRequest } from "@/lib/customFetch";
|
||||||
|
|
||||||
|
export const fetchQuestions = async ({
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
options?: { signal?: AbortSignal,};
|
||||||
|
}) => {
|
||||||
|
const response = await getRequest(
|
||||||
|
"https://api.v3.ycymedu.com/api/zhiYuan/aigcquestionswords?",
|
||||||
|
{},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
if(response.code === 200){
|
||||||
|
return {result:response.result}
|
||||||
|
}else{
|
||||||
|
return {result:[],message:response.message}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
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}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { lazy } from "react";
|
||||||
|
|
||||||
|
const MainArea = lazy(() => import("@/app/MainArea/index"));
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div className="h-full bg-[#F4F6FA]">
|
||||||
|
<MainArea />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import AntechamberHeader from "@/components/AntechamberHeader";
|
||||||
|
import InvokeButton from "@/components/AntechamberButton";
|
||||||
|
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";
|
||||||
|
|
||||||
|
export default function Antechamber() {
|
||||||
|
|
||||||
|
const { handleConnect } = useContext(RealtimeClientContext);
|
||||||
|
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const [disable,setDisable] = useState(true);
|
||||||
|
|
||||||
|
const token = searchParams.get("token") || '';
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
const controller = new AbortController();
|
||||||
|
const { signal } = controller;
|
||||||
|
getUserToken({ signal });
|
||||||
|
},[token])
|
||||||
|
|
||||||
|
const toRoom = () => {
|
||||||
|
// if(disable){
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
handleConnect();
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center h-full">
|
||||||
|
<AntechamberHeader toRoom={toRoom} />
|
||||||
|
<AntechamberScore toRoom={toRoom} />
|
||||||
|
<InvokeButton disable={disable} onClick={toRoom} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import RoomConversation from "@/components/RoomConversation";
|
||||||
|
import RoomController from "@/components/RoomController";
|
||||||
|
|
||||||
|
export default function Room() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<RoomConversation />
|
||||||
|
<RoomController />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import Room from "./Room";
|
||||||
|
import Antechamber from "./Antechamber";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import {
|
||||||
|
RealtimeClientContext,
|
||||||
|
RealtimeClientProvider,
|
||||||
|
} from "@/components/Provider/RealtimeClientProvider";
|
||||||
|
|
||||||
|
function MainContent() {
|
||||||
|
const { isConnected } = useContext(RealtimeClientContext);
|
||||||
|
return isConnected ? <Room /> : <Antechamber />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MainArea() {
|
||||||
|
return (
|
||||||
|
<RealtimeClientProvider>
|
||||||
|
<MainContent />
|
||||||
|
</RealtimeClientProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import { Outlet } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function LocaleLayout() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={cn("h-dvh bg-background font-sans antialiased")}>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
.wrapper {
|
||||||
|
width: 88px;
|
||||||
|
height: 88px;
|
||||||
|
background: linear-gradient(180deg, #64c7ff 0%, #0165ff 100%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: 80px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper::before {
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
background: radial-gradient(farthest-corner at 100% 0%, #7bdcf0, #fff);
|
||||||
|
filter: blur(14px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper::after {
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
left: 20%;
|
||||||
|
top: 0;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
background: radial-gradient(farthest-corner at 100% 0%, #7bdcf0, #a4dbe6);
|
||||||
|
filter: blur(14px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.call {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
object-fit: contain;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import style from './index.module.css';
|
||||||
|
import CallPng from "/icons/call.png"
|
||||||
|
import { RealtimeClientContext } from '../Provider/RealtimeClientProvider';
|
||||||
|
|
||||||
|
interface IInvokeButtonProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
loading?: boolean;
|
||||||
|
disable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function InvokeButton(props: IInvokeButtonProps) {
|
||||||
|
const {disable, loading, className, ...rest } = props;
|
||||||
|
|
||||||
|
const {isConnecting} = useContext(RealtimeClientContext);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${style.wrapper} ${className}`} {...rest}>
|
||||||
|
<img className={style.call} src={CallPng} alt="call" />
|
||||||
|
<div className={style.text}>{disable ? '暂不可用':isConnecting?'连接中':'发起通话'}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
.headerWrapper{
|
||||||
|
margin-top: 65px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
height: 105px;
|
||||||
|
background: rgba(176, 228, 255, 0.8);
|
||||||
|
border-radius: 50px 13px 13px 13px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
margin-bottom: -54px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img {
|
||||||
|
width: 120px;
|
||||||
|
height: 123px;
|
||||||
|
object-fit: contain;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
bottom: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper::before{
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 133px;
|
||||||
|
height: 32px;
|
||||||
|
bottom: 23px;
|
||||||
|
left: 6px;
|
||||||
|
background: radial-gradient(ellipse at top, #1580FF, #fff);
|
||||||
|
z-index: 2;
|
||||||
|
border-radius: 20px;
|
||||||
|
filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #000000;
|
||||||
|
|
||||||
|
text-align: end;
|
||||||
|
margin-top: 15px;
|
||||||
|
margin-right: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-wrapper {
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
border-radius: 13px 13px 13px 13px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
padding:16px 17px 18px 17px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
backdrop-filter:blur(10px);
|
||||||
|
min-height: 213px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thing {
|
||||||
|
object-fit: contain;
|
||||||
|
position: relative;
|
||||||
|
height: 15px;
|
||||||
|
width: 91px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2px;
|
||||||
|
color:#2380e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle img{
|
||||||
|
width:16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightIcon{
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change{
|
||||||
|
color:#2380e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #000000;
|
||||||
|
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.rotating {
|
||||||
|
animation: rotate360 1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate360 {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
import { useState, useEffect, useContext } from "react";
|
||||||
|
|
||||||
|
import HelloGIF from "/icons/hello.gif";
|
||||||
|
import WhatsThing from "/icons/whatsThing.png";
|
||||||
|
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";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
toRoom: () =>void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function HeaderGroup({ toRoom }: Props) {
|
||||||
|
const [isRotating, setIsRotating] = useState(false);
|
||||||
|
const [displayQuestions, setDisplayQuestions] = useState<string[]>([]);
|
||||||
|
const [allQuestions, setAllQuestions] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const { setInitMessage } = useContext(RealtimeClientContext);
|
||||||
|
|
||||||
|
// 随机获取4个问题的函数
|
||||||
|
const getRandomQuestions = () => {
|
||||||
|
const _allQuestions = Array.from(allQuestions);
|
||||||
|
const result: string[] = [];
|
||||||
|
const questionCount = Math.min(4, _allQuestions.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < questionCount; i++) {
|
||||||
|
const randomIndex = Math.floor(Math.random() * _allQuestions.length);
|
||||||
|
result.push(_allQuestions.splice(randomIndex, 1)[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getQuestion = async ({ signal }: { signal: AbortSignal }) => {
|
||||||
|
const { result, message } = await fetchQuestions({ options: { signal } });
|
||||||
|
if (message) {
|
||||||
|
console.log(message);
|
||||||
|
} else {
|
||||||
|
setAllQuestions(result as string[]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDisplayQuestions(getRandomQuestions());
|
||||||
|
}, [allQuestions]);
|
||||||
|
|
||||||
|
// 组件初始化时获取随机问题
|
||||||
|
useEffect(() => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const { signal } = controller;
|
||||||
|
getQuestion({ signal });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
setIsRotating(true);
|
||||||
|
|
||||||
|
// 更新随机问题
|
||||||
|
setDisplayQuestions(getRandomQuestions());
|
||||||
|
|
||||||
|
// 动画结束后重置状态
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsRotating(false);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuestion = async (question: string) => {
|
||||||
|
setInitMessage(question);
|
||||||
|
await toRoom();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.headerWrapper}>
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<img className={styles.img} src={HelloGIF} alt="hello" />
|
||||||
|
<div className={styles.text}>Hey,我是您的六纬AI小助手</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles["main-wrapper"]}>
|
||||||
|
<div className={styles.main} onClick={handleClick}>
|
||||||
|
<img className={styles.thing} src={WhatsThing} alt="whatsThing" />
|
||||||
|
<div className={styles.circle}>
|
||||||
|
<img
|
||||||
|
src={CircleIcon}
|
||||||
|
className={isRotating ? styles.rotating : ""}
|
||||||
|
alt="circle"
|
||||||
|
/>
|
||||||
|
<div className={styles.change}>换一批</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{displayQuestions.map((item, index) => (
|
||||||
|
<div
|
||||||
|
className={styles.tip}
|
||||||
|
key={index}
|
||||||
|
onClick={() => handleQuestion(item)}
|
||||||
|
>
|
||||||
|
<div>{item}</div>
|
||||||
|
<img
|
||||||
|
src={RightIcon}
|
||||||
|
alt="right-icon"
|
||||||
|
className={styles.rightIcon}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
.scoreWrapper {
|
||||||
|
padding: 15px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.innerWrapper {
|
||||||
|
padding: 15px;
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
border-radius: 13px;
|
||||||
|
border: 1px solid #f4f6fa;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right{
|
||||||
|
color: #1580FF;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imgIcon{
|
||||||
|
width: 91px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightBlue{
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function MyInput({ toRoom }: Props) {
|
||||||
|
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
|
||||||
|
const provinceName = searchParams.get('provinceName') || '山东省'
|
||||||
|
const subjectGroup = searchParams.get('subjectGroup') || '物/化/史'
|
||||||
|
const expectedScore = searchParams.get('expectedScore') || 500
|
||||||
|
|
||||||
|
const {setInitMessage} = useContext(RealtimeClientContext)
|
||||||
|
|
||||||
|
const handleQuestion = async () => {
|
||||||
|
toRoom();
|
||||||
|
setInitMessage(`我的高考地点在${provinceName},我选择的科目是${subjectGroup},我的高考分数为${expectedScore}分。我适合哪些学校和专业`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.scoreWrapper}>
|
||||||
|
<div className={style.innerWrapper}>
|
||||||
|
<div className={style.left}>
|
||||||
|
<img src={MyInputIcon} className={style.imgIcon} alt="input-ico" />
|
||||||
|
<div className={style.detail}>
|
||||||
|
<div className={style.city}>{provinceName}</div>
|
||||||
|
<div className={style.subject}>{(subjectGroup as string).split(',').join('/') }</div>
|
||||||
|
<div className={style.score}>{expectedScore}分</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={style.right} onClick={handleQuestion}>
|
||||||
|
<span>智能分析</span>
|
||||||
|
<img src={RightBlueIcon} alt="right" className={style.rightBlue} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,332 @@
|
||||||
|
import { ChatEventType, RoleType } from "@coze/api";
|
||||||
|
import {
|
||||||
|
EventNames,
|
||||||
|
RealtimeAPIError,
|
||||||
|
RealtimeClient,
|
||||||
|
RealtimeError,
|
||||||
|
RealtimeUtils,
|
||||||
|
} from "@coze/realtime-api";
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
type RoomInfo = {
|
||||||
|
appId: string;
|
||||||
|
roomId: string;
|
||||||
|
token: string;
|
||||||
|
uid: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RealtimeClientContext = createContext<{
|
||||||
|
client: RealtimeClient | null;
|
||||||
|
isConnecting: boolean;
|
||||||
|
isConnected: boolean;
|
||||||
|
audioEnabled: boolean;
|
||||||
|
isSupportVideo: boolean;
|
||||||
|
messageList: { content: string; role: RoleType }[];
|
||||||
|
isAiTalking: boolean;
|
||||||
|
roomInfo: RoomInfo | null;
|
||||||
|
initClient: () => void;
|
||||||
|
handleConnect: () => void;
|
||||||
|
handleInterrupt: () => void;
|
||||||
|
handleDisconnect: () => void;
|
||||||
|
toggleMicrophone: () => void;
|
||||||
|
sendUserMessageWithText: (message: string) => void;
|
||||||
|
setInitMessage: (message: string) => void;
|
||||||
|
}>({
|
||||||
|
client: null,
|
||||||
|
isConnecting: false,
|
||||||
|
isConnected: false,
|
||||||
|
audioEnabled: true,
|
||||||
|
isSupportVideo: false,
|
||||||
|
messageList: [],
|
||||||
|
isAiTalking: false,
|
||||||
|
roomInfo: null,
|
||||||
|
initClient: () => {},
|
||||||
|
handleConnect: () => {},
|
||||||
|
handleInterrupt: () => {},
|
||||||
|
handleDisconnect: () => {},
|
||||||
|
toggleMicrophone: () => {},
|
||||||
|
sendUserMessageWithText: () => {},
|
||||||
|
setInitMessage: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加自定义hook
|
||||||
|
export const useRealtimeClient = () => {
|
||||||
|
const context = useContext(RealtimeClientContext);
|
||||||
|
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useRealtimeClient必须在RealtimeClientProvider内部使用");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...context };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RealtimeClientProvider = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}) => {
|
||||||
|
const token = import.meta.env.VITE_COZE_TOKEN;
|
||||||
|
const botId = import.meta.env.VITE_COZE_BOT_ID;
|
||||||
|
const voiceId = import.meta.env.VITE_COZE_VOICE_ID;
|
||||||
|
const connectorId = "1024";
|
||||||
|
|
||||||
|
const clientRef = useRef<RealtimeClient | null>(null);
|
||||||
|
// 实时语音回复消息列表
|
||||||
|
const [messageList, setMessageList] = useState<
|
||||||
|
{ content: string; role: RoleType }[]
|
||||||
|
>([]);
|
||||||
|
// 是否正在连接
|
||||||
|
const [isConnecting, setIsConnecting] = useState(false);
|
||||||
|
// 是否已连接
|
||||||
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
|
// 是否开启麦克风
|
||||||
|
const [audioEnabled, setAudioEnabled] = useState(false);
|
||||||
|
// 是否支持视频
|
||||||
|
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 permission = await RealtimeUtils.checkDevicePermission(false);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (clientRef.current) {
|
||||||
|
setupMessageEventListeners(clientRef.current);
|
||||||
|
if (initMessage) {
|
||||||
|
setupInitMessageEventListener(clientRef.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [initMessage, isClientInitialized]);
|
||||||
|
|
||||||
|
const handleConnect = async () => {
|
||||||
|
try {
|
||||||
|
if (!clientRef.current) {
|
||||||
|
await initClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
await clientRef.current?.connect();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
if (error instanceof RealtimeAPIError) {
|
||||||
|
switch (error.code) {
|
||||||
|
case RealtimeError.CREATE_ROOM_ERROR:
|
||||||
|
console.error(`创建房间失败: ${error.message}`);
|
||||||
|
break;
|
||||||
|
case RealtimeError.CONNECTION_ERROR:
|
||||||
|
console.error(`加入房间失败: ${error.message}`);
|
||||||
|
break;
|
||||||
|
case RealtimeError.DEVICE_ACCESS_ERROR:
|
||||||
|
console.error(`获取设备失败: ${error.message}`);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(`连接错误: ${error.message}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("连接错误:" + error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInterrupt = () => {
|
||||||
|
try {
|
||||||
|
clientRef.current?.interrupt();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("打断失败:" + error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDisconnect = () => {
|
||||||
|
try {
|
||||||
|
// 关闭客户的时候清除一些信息
|
||||||
|
setIsAiTalking(false);
|
||||||
|
setIsClientInitialized(false);
|
||||||
|
setMessageList([]);
|
||||||
|
|
||||||
|
clientRef.current?.disconnect();
|
||||||
|
clientRef.current?.clearEventHandlers();
|
||||||
|
clientRef.current = null;
|
||||||
|
setIsConnected(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("断开失败:" + error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleMicrophone = async () => {
|
||||||
|
try {
|
||||||
|
await clientRef.current?.setAudioEnable(!audioEnabled);
|
||||||
|
setAudioEnabled(!audioEnabled);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("切换麦克风状态失败:" + error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupInitMessageEventListener = (client: RealtimeClient) => {
|
||||||
|
client.on(EventNames.ALL_SERVER, (eventName, _event: any) => {
|
||||||
|
if (eventName === "server.session.created") {
|
||||||
|
// 这里需要加个 server. 前缀
|
||||||
|
sendUserMessageWithText(initMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupMessageEventListeners = (client: RealtimeClient) => {
|
||||||
|
let lastEvent: any;
|
||||||
|
client.on(EventNames.ALL, (_eventName, event: any) => {
|
||||||
|
// AI智能体设置
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.event_type !== ChatEventType.CONVERSATION_MESSAGE_DELTA &&
|
||||||
|
event.event_type !== ChatEventType.CONVERSATION_MESSAGE_COMPLETED &&
|
||||||
|
event.event_type !== "conversation.created"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const content = event.data.content;
|
||||||
|
setMessageList((prev) => {
|
||||||
|
// 如果上一个事件是增量更新,则附加到最后一条消息
|
||||||
|
if (
|
||||||
|
lastEvent?.event_type === ChatEventType.CONVERSATION_MESSAGE_DELTA &&
|
||||||
|
(event.data.type === "answer" || event.data.type === "question")
|
||||||
|
) {
|
||||||
|
return [
|
||||||
|
...prev.slice(0, -1),
|
||||||
|
{
|
||||||
|
content: prev[prev.length - 1].content + content,
|
||||||
|
role: prev[prev.length - 1].role,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加AI的欢迎语
|
||||||
|
if (initMessage === "" && event.event_type === "conversation.created") {
|
||||||
|
return [
|
||||||
|
...prev,
|
||||||
|
{ content: event.data.prologue, role: RoleType.Assistant },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则添加新消息
|
||||||
|
if (
|
||||||
|
(content !== "" &&
|
||||||
|
event.event_type === ChatEventType.CONVERSATION_MESSAGE_DELTA) ||
|
||||||
|
(event.event_type === ChatEventType.CONVERSATION_MESSAGE_COMPLETED &&
|
||||||
|
(event.data.type === "answer" || event.data.type === "question") &&
|
||||||
|
event.data.role !== RoleType.Assistant)
|
||||||
|
) {
|
||||||
|
return [...prev, { content: content, role: event.data.role }];
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
lastEvent = event;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置事件监听器
|
||||||
|
const setupEventListeners = useCallback(
|
||||||
|
(client: RealtimeClient) => {
|
||||||
|
// 监听 AI 开始说话事件
|
||||||
|
client.on(EventNames.AUDIO_AGENT_SPEECH_STARTED, () => {
|
||||||
|
// console.log("AI开始说话");
|
||||||
|
setIsAiTalking(true);
|
||||||
|
setAudioEnabled(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听 AI 结束说话事件
|
||||||
|
client.on(EventNames.AUDIO_AGENT_SPEECH_STOPPED, () => {
|
||||||
|
// console.log("AI结束说话");
|
||||||
|
setIsAiTalking(false);
|
||||||
|
setAudioEnabled(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听连接客户端
|
||||||
|
client.on(EventNames.CONNECTING, () => {
|
||||||
|
setIsConnecting(true);
|
||||||
|
setIsConnected(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 客户端连接成功
|
||||||
|
client.on(EventNames.CONNECTED, (_eventName: string, event: any) => {
|
||||||
|
setRoomInfo(event);
|
||||||
|
setIsConnecting(false);
|
||||||
|
setIsConnected(true);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[clientRef.current, initMessage]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 发送信息
|
||||||
|
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
|
||||||
|
value={{
|
||||||
|
client: clientRef.current,
|
||||||
|
isConnecting,
|
||||||
|
isConnected,
|
||||||
|
audioEnabled,
|
||||||
|
isSupportVideo,
|
||||||
|
messageList,
|
||||||
|
isAiTalking,
|
||||||
|
roomInfo,
|
||||||
|
initClient,
|
||||||
|
handleConnect,
|
||||||
|
handleInterrupt,
|
||||||
|
handleDisconnect,
|
||||||
|
toggleMicrophone,
|
||||||
|
sendUserMessageWithText,
|
||||||
|
setInitMessage,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</RealtimeClientContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
.controller {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 8px 15px;
|
||||||
|
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
|
||||||
|
padding-bottom: calc(12px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
.talkWrapper {
|
||||||
|
--h: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #e3efff;
|
||||||
|
border-radius: 20px;
|
||||||
|
user-select: none;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 116px;
|
||||||
|
color: #0078ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listenerDot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
height: var(--h);
|
||||||
|
width: 36px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: inline-block;
|
||||||
|
width: 4px;
|
||||||
|
margin: 0 1px;
|
||||||
|
background-color: #4898fc;
|
||||||
|
border-radius: 2px;
|
||||||
|
height: calc(var(--h) - var(--d) * 4px);
|
||||||
|
transition: height 0.2s ease;
|
||||||
|
opacity: calc(1 - var(--d) * 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.isTalking {
|
||||||
|
span {
|
||||||
|
animation: soundWave 1s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
span:nth-child(1) {
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
span:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
span:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
span:nth-child(4) {
|
||||||
|
animation-delay: 0.6s;
|
||||||
|
}
|
||||||
|
span:nth-child(5) {
|
||||||
|
animation-delay: 0.8s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes soundWave {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphoneWrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timerWrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 400px;
|
||||||
|
color: #0078ff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.
|
||||||
|
* SPDX-license-identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
|
||||||
|
|
||||||
|
import style from './index.module.css';
|
||||||
|
import LockMicroIcon from '/icons/lockmicrophone.png';
|
||||||
|
import MicroIcon from '/icons/microphone.png';
|
||||||
|
import HandleOffIcon from '/icons/handoff.png';
|
||||||
|
|
||||||
|
import { RealtimeClientContext } from '../Provider/RealtimeClientProvider';
|
||||||
|
import { useSignalRConnection } from '@/hooks/useMicrosoftSignal';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function AudioController({className, ...rest}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
|
||||||
|
const {handleDisconnect,toggleMicrophone,audioEnabled,isAiTalking,handleInterrupt,roomInfo} = useContext(RealtimeClientContext)
|
||||||
|
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
useSignalRConnection({access_token:searchParams.get('token') ||'',roomId:roomInfo?.roomId || ''})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${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>
|
||||||
|
</div>
|
||||||
|
<div className={`${style.microphoneWrapper}`} onClick={toggleMicrophone}>
|
||||||
|
<img src={audioEnabled ? MicroIcon : LockMicroIcon} alt="lock" />
|
||||||
|
<div>{audioEnabled ? '关麦' : '开麦'}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`${style.talkWrapper}`} onClick={handleInterrupt}>
|
||||||
|
<div className={`${isAiTalking ? style.isTalking : ''} ${style.listenerDot}`}>
|
||||||
|
<span style={{ '--d': '2' } as React.CSSProperties} />
|
||||||
|
<span style={{ '--d': '1' } as React.CSSProperties} />
|
||||||
|
<span style={{ '--d': '0' } as React.CSSProperties} />
|
||||||
|
<span style={{ '--d': '1' } as React.CSSProperties} />
|
||||||
|
<span style={{ '--d': '2' } as React.CSSProperties} />
|
||||||
|
</div>
|
||||||
|
<div>{isAiTalking ? '点击打断' : '正在听'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AudioController;
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { useRef, useEffect, useContext } from "react";
|
||||||
|
import { RealtimeClientContext } from "../Provider/RealtimeClientProvider";
|
||||||
|
import { RoleType } from "@coze/api";
|
||||||
|
|
||||||
|
export default function RoomConversation() {
|
||||||
|
const { messageList } = useContext(RealtimeClientContext);
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// 自动滚动到最新消息
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [messageList]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex flex-col h-full">
|
||||||
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||||
|
{messageList.map((message: any, index: number) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`flex ${
|
||||||
|
message.role === RoleType.Assistant
|
||||||
|
? "justify-start"
|
||||||
|
: "justify-end"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`max-w-3/4 p-3 rounded-lg ${
|
||||||
|
message.role === RoleType.Assistant
|
||||||
|
? "bg-white text-black rounded-tl-none"
|
||||||
|
: "bg-blue-500 text-white rounded-tr-none"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{message.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Cross2Icon } from "@radix-ui/react-icons"
|
||||||
|
|
||||||
|
const ToastProvider = ToastPrimitives.Provider
|
||||||
|
|
||||||
|
const ToastViewport = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Viewport
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"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]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||||
|
|
||||||
|
const toastVariants = cva(
|
||||||
|
"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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const Toast = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||||
|
VariantProps<typeof toastVariants>
|
||||||
|
>(({ className, variant, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<ToastPrimitives.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(toastVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
Toast.displayName = ToastPrimitives.Root.displayName
|
||||||
|
|
||||||
|
const ToastAction = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Action
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"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",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||||
|
|
||||||
|
const ToastClose = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Close
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"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",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
toast-close=""
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Cross2Icon className="h-4 w-4" />
|
||||||
|
</ToastPrimitives.Close>
|
||||||
|
))
|
||||||
|
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||||
|
|
||||||
|
const ToastTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||||
|
|
||||||
|
const ToastDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm opacity-90", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||||
|
|
||||||
|
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||||
|
|
||||||
|
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||||
|
|
||||||
|
export {
|
||||||
|
type ToastProps,
|
||||||
|
type ToastActionElement,
|
||||||
|
ToastProvider,
|
||||||
|
ToastViewport,
|
||||||
|
Toast,
|
||||||
|
ToastTitle,
|
||||||
|
ToastDescription,
|
||||||
|
ToastClose,
|
||||||
|
ToastAction,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { useToast } from "@/hooks/use-toast"
|
||||||
|
import {
|
||||||
|
Toast,
|
||||||
|
ToastClose,
|
||||||
|
ToastDescription,
|
||||||
|
ToastProvider,
|
||||||
|
ToastTitle,
|
||||||
|
ToastViewport,
|
||||||
|
} from "@/components/ui/toast"
|
||||||
|
|
||||||
|
export function Toaster() {
|
||||||
|
const { toasts } = useToast()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastProvider>
|
||||||
|
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||||
|
return (
|
||||||
|
<Toast key={id} {...props}>
|
||||||
|
<div className="grid gap-1">
|
||||||
|
{title && <ToastTitle>{title}</ToastTitle>}
|
||||||
|
{description && (
|
||||||
|
<ToastDescription>{description}</ToastDescription>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{action}
|
||||||
|
<ToastClose />
|
||||||
|
</Toast>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<ToastViewport />
|
||||||
|
</ToastProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
--primary: 222.2 47.4% 11.2%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--secondary: 210 40% 96.1%;
|
||||||
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
--accent: 210 40% 96.1%;
|
||||||
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
--ring: 222.2 84% 4.9%;
|
||||||
|
--chart-1: 12 76% 61%;
|
||||||
|
--chart-2: 173 58% 39%;
|
||||||
|
--chart-3: 197 37% 24%;
|
||||||
|
--chart-4: 43 74% 66%;
|
||||||
|
--chart-5: 27 87% 67%;
|
||||||
|
--radius: 0.5rem
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
--primary: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
--ring: 212.7 26.8% 83.9%;
|
||||||
|
--chart-1: 220 70% 50%;
|
||||||
|
--chart-2: 160 60% 45%;
|
||||||
|
--chart-3: 30 80% 55%;
|
||||||
|
--chart-4: 280 65% 60%;
|
||||||
|
--chart-5: 340 75% 55%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
|
||||||
|
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbar for IE, Edge and Firefox */
|
||||||
|
.no-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
/* IE and Edge */
|
||||||
|
scrollbar-width: none;
|
||||||
|
/* Firefox */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
// Inspired by react-hot-toast library
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ToastActionElement,
|
||||||
|
ToastProps,
|
||||||
|
} from "@/components/ui/toast"
|
||||||
|
|
||||||
|
const TOAST_LIMIT = 1
|
||||||
|
const TOAST_REMOVE_DELAY = 1000000
|
||||||
|
|
||||||
|
type ToasterToast = ToastProps & {
|
||||||
|
id: string
|
||||||
|
title?: React.ReactNode
|
||||||
|
description?: React.ReactNode
|
||||||
|
action?: ToastActionElement
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionTypes = {
|
||||||
|
ADD_TOAST: "ADD_TOAST",
|
||||||
|
UPDATE_TOAST: "UPDATE_TOAST",
|
||||||
|
DISMISS_TOAST: "DISMISS_TOAST",
|
||||||
|
REMOVE_TOAST: "REMOVE_TOAST",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
let count = 0
|
||||||
|
|
||||||
|
function genId() {
|
||||||
|
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||||
|
return count.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionType = typeof actionTypes
|
||||||
|
|
||||||
|
type Action =
|
||||||
|
| {
|
||||||
|
type: ActionType["ADD_TOAST"]
|
||||||
|
toast: ToasterToast
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["UPDATE_TOAST"]
|
||||||
|
toast: Partial<ToasterToast>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["DISMISS_TOAST"]
|
||||||
|
toastId?: ToasterToast["id"]
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["REMOVE_TOAST"]
|
||||||
|
toastId?: ToasterToast["id"]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
toasts: ToasterToast[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||||
|
|
||||||
|
const addToRemoveQueue = (toastId: string) => {
|
||||||
|
if (toastTimeouts.has(toastId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
toastTimeouts.delete(toastId)
|
||||||
|
dispatch({
|
||||||
|
type: "REMOVE_TOAST",
|
||||||
|
toastId: toastId,
|
||||||
|
})
|
||||||
|
}, TOAST_REMOVE_DELAY)
|
||||||
|
|
||||||
|
toastTimeouts.set(toastId, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reducer = (state: State, action: Action): State => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "ADD_TOAST":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||||
|
}
|
||||||
|
|
||||||
|
case "UPDATE_TOAST":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
case "DISMISS_TOAST": {
|
||||||
|
const { toastId } = action
|
||||||
|
|
||||||
|
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||||
|
// but I'll keep it here for simplicity
|
||||||
|
if (toastId) {
|
||||||
|
addToRemoveQueue(toastId)
|
||||||
|
} else {
|
||||||
|
state.toasts.forEach((toast) => {
|
||||||
|
addToRemoveQueue(toast.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === toastId || toastId === undefined
|
||||||
|
? {
|
||||||
|
...t,
|
||||||
|
open: false,
|
||||||
|
}
|
||||||
|
: t
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "REMOVE_TOAST":
|
||||||
|
if (action.toastId === undefined) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const listeners: Array<(state: State) => void> = []
|
||||||
|
|
||||||
|
let memoryState: State = { toasts: [] }
|
||||||
|
|
||||||
|
function dispatch(action: Action) {
|
||||||
|
memoryState = reducer(memoryState, action)
|
||||||
|
listeners.forEach((listener) => {
|
||||||
|
listener(memoryState)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Toast = Omit<ToasterToast, "id">
|
||||||
|
|
||||||
|
function toast({ ...props }: Toast) {
|
||||||
|
const id = genId()
|
||||||
|
|
||||||
|
const update = (props: ToasterToast) =>
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_TOAST",
|
||||||
|
toast: { ...props, id },
|
||||||
|
})
|
||||||
|
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "ADD_TOAST",
|
||||||
|
toast: {
|
||||||
|
...props,
|
||||||
|
id,
|
||||||
|
open: true,
|
||||||
|
onOpenChange: (open) => {
|
||||||
|
if (!open) dismiss()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
dismiss,
|
||||||
|
update,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useToast() {
|
||||||
|
const [state, setState] = React.useState<State>(memoryState)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
listeners.push(setState)
|
||||||
|
return () => {
|
||||||
|
const index = listeners.indexOf(setState)
|
||||||
|
if (index > -1) {
|
||||||
|
listeners.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [state])
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toast,
|
||||||
|
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useToast, toast }
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import * as signalR from "@microsoft/signalr";
|
||||||
|
import { useRealtimeClient } from "@/components/Provider/RealtimeClientProvider";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
export const useSignalRConnection = (params: {
|
||||||
|
access_token: string;
|
||||||
|
roomId: string;
|
||||||
|
}) => {
|
||||||
|
const connectionRef = useRef<signalR.HubConnection | null>(null);
|
||||||
|
const { handleDisconnect } = useRealtimeClient();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!params.access_token || !params.roomId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const connection = new signalR.HubConnectionBuilder()
|
||||||
|
.withServerTimeout(30000)
|
||||||
|
.withAutomaticReconnect()
|
||||||
|
.withUrl(
|
||||||
|
`https://api.v3.ycymedu.com/hubs/weminpro?access_token=${params.access_token}&roomId=${params.roomId}`
|
||||||
|
)
|
||||||
|
.configureLogging(signalR.LogLevel.Information)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
connectionRef.current = connection;
|
||||||
|
|
||||||
|
connection.on("ForceOffline", function (msg) {
|
||||||
|
// 可加逻辑:注销用户、跳转登录页等
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "下线提醒",
|
||||||
|
description: msg,
|
||||||
|
});
|
||||||
|
handleDisconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on("SendWarn", function (msg) {
|
||||||
|
console.warn(`下线提醒:${msg}"`);
|
||||||
|
// 也可以弹窗提醒用户保存数据
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "下线提醒",
|
||||||
|
description: msg,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
connection
|
||||||
|
.start()
|
||||||
|
.then(() => {
|
||||||
|
console.log("SignalR连接已建立");
|
||||||
|
setInterval(() => {
|
||||||
|
connection.invoke("Ping");
|
||||||
|
}, 5000);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("SignalR连接失败:", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (connectionRef.current) {
|
||||||
|
connectionRef.current
|
||||||
|
.stop()
|
||||||
|
.then(() => {
|
||||||
|
console.log("SignalR连接已关闭");
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("关闭SignalR连接失败:", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [params.access_token, params.roomId]);
|
||||||
|
|
||||||
|
return connectionRef.current;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const useTokenWithPat = () => {
|
||||||
|
// 替换成你的 PAT
|
||||||
|
const token = "pat_NhhZGW7sxkuyP4mJrPrVyZx20b3m6lymg0y2Ln9EyM0CV9q2f9t3rlGbtzppLQua"; // Access Token
|
||||||
|
return {
|
||||||
|
getToken: () => token
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
* @Author: HideInMatrix
|
||||||
|
* @Date: 2024-07-15
|
||||||
|
* @LastEditors: error: git config user.name & please set dead value or install git
|
||||||
|
* @LastEditTime: 2024-09-01
|
||||||
|
* @Description: 请求封装
|
||||||
|
* @FilePath: /free-music-react/src/lib/customFetch.ts
|
||||||
|
*/
|
||||||
|
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
|
||||||
|
|
||||||
|
interface FetchOptions extends RequestInit {
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiResponse {
|
||||||
|
result?: unknown;
|
||||||
|
error?: string;
|
||||||
|
code?: number;
|
||||||
|
success?: boolean;
|
||||||
|
message?:string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiClient = (method: HttpMethod) => {
|
||||||
|
return async (
|
||||||
|
url: string,
|
||||||
|
data?: unknown,
|
||||||
|
options: FetchOptions = {}
|
||||||
|
): Promise<ApiResponse> => {
|
||||||
|
const config: FetchOptions = {
|
||||||
|
method,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (method !== "GET" && data) {
|
||||||
|
config.body = JSON.stringify(data);
|
||||||
|
} else if (method === "GET" && data) {
|
||||||
|
const _params = [];
|
||||||
|
for (const [key, value] of Object.entries(data)) {
|
||||||
|
_params.push(`${key}=${value}`);
|
||||||
|
}
|
||||||
|
url += `?${_params.join("&")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${url}`, config);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return {
|
||||||
|
error: result.message || "Request failed",
|
||||||
|
code: response.status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRequest = apiClient("GET");
|
||||||
|
export const postRequest = apiClient("POST");
|
||||||
|
export const putRequest = apiClient("PUT");
|
||||||
|
export const deleteRequest = apiClient("DELETE");
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
* @Author: HideInMatrix
|
||||||
|
* @Date: 2024-07-16
|
||||||
|
* @LastEditors: HideInMatrix
|
||||||
|
* @LastEditTime: 2024-09-18
|
||||||
|
* @Description: 这是一则说明
|
||||||
|
* @FilePath: /free-music-react/src/lib/utils.ts
|
||||||
|
*/
|
||||||
|
import { type ClassValue, clsx } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: HideInMatrix
|
||||||
|
* @description: 合并类名
|
||||||
|
* @param {array} inputs
|
||||||
|
* @return {*}
|
||||||
|
* @Date: 2024-07-17
|
||||||
|
*/
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否客户端
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export const isBrowser = typeof window !== "undefined";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: HideInMatrix
|
||||||
|
* @description: 节流函数
|
||||||
|
* @param {Function} fn
|
||||||
|
* @param {number} wait
|
||||||
|
* @return {*}
|
||||||
|
* @Date: 2024-07-28
|
||||||
|
*/
|
||||||
|
export const throttle = (fn: Function, wait: number) => {
|
||||||
|
let timer: NodeJS.Timeout | undefined;
|
||||||
|
return (...args: any) => {
|
||||||
|
if (!timer) {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
fn.apply(this, args);
|
||||||
|
timer = undefined;
|
||||||
|
}, wait);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: HideInMatrix
|
||||||
|
* @description: 防抖
|
||||||
|
* @param {Function} fn
|
||||||
|
* @param {number} wait
|
||||||
|
* @return {*}
|
||||||
|
* @Date: 2024-07-28
|
||||||
|
*/
|
||||||
|
export const debounce = (fn: Function, wait: number) => {
|
||||||
|
let timer: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
|
return (...args: any) => {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
fn.apply(this, args);
|
||||||
|
}, wait);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: HideInMatrix
|
||||||
|
* @description: 转换秒数成分钟
|
||||||
|
* @seconds {number}
|
||||||
|
* @return {*}
|
||||||
|
* @Date: 2024-07-28
|
||||||
|
*/
|
||||||
|
export const formatTime = (seconds: number): string => {
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const secs = Math.floor(seconds % 60);
|
||||||
|
return `${minutes.toString().padStart(2, "0")}:${secs
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: HideInMatrix
|
||||||
|
* @description: 获取枚举类 指定值的下一个值
|
||||||
|
* @return {*}
|
||||||
|
* @Date: 2024-09-08
|
||||||
|
*/
|
||||||
|
export const getNextEnumValue = <T extends Record<string, string>>(
|
||||||
|
enumObj: T,
|
||||||
|
currentValue: T[keyof T]
|
||||||
|
): T[keyof T] => {
|
||||||
|
const values = Object.values(enumObj) as T[keyof T][];
|
||||||
|
const currentIndex = values.indexOf(currentValue);
|
||||||
|
const nextIndex = (currentIndex + 1) % values.length;
|
||||||
|
return values[nextIndex];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检测容器是否需要滚动,若不需要则加载更多数据
|
||||||
|
export const checkAndLoadMore = ({
|
||||||
|
containerRef,
|
||||||
|
setPage,
|
||||||
|
toEnd,
|
||||||
|
}: {
|
||||||
|
containerRef: React.RefObject<HTMLDivElement>;
|
||||||
|
setPage: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
toEnd: boolean;
|
||||||
|
}) => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (
|
||||||
|
container &&
|
||||||
|
container.children[0] &&
|
||||||
|
container.children[0].clientHeight <= container.clientHeight &&
|
||||||
|
!toEnd
|
||||||
|
) {
|
||||||
|
// 如果内容高度不足以滚动,且还有数据未加载,则主动触发加载
|
||||||
|
setPage((prevPage) => prevPage + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
import { RouterProvider } from "react-router-dom";
|
||||||
|
import { router } from "@/router";
|
||||||
|
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
|
||||||
|
import { lazy } from "react";
|
||||||
|
import { createBrowserRouter } from "react-router-dom";
|
||||||
|
import { Component } from "react";
|
||||||
|
|
||||||
|
const HomePage = lazy(() => import("@/app/App"));
|
||||||
|
const MainLayout = lazy(() => import("@/app/MainLayout"));
|
||||||
|
|
||||||
|
|
||||||
|
interface ErrorBoundaryProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
class ErrorBoundary extends Component<ErrorBoundaryProps> {
|
||||||
|
state = { hasError: false };
|
||||||
|
|
||||||
|
static getDerivedStateFromError(_error: any) {
|
||||||
|
return { hasError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return <h1>出错了,请稍后再试。</h1>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <MainLayout />,
|
||||||
|
errorElement: (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<div className="flex-auto flex flex-col p-6">出错了,请稍后再试。</div>
|
||||||
|
</ErrorBoundary>
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <HomePage />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
export const defaultLocale: string = 'zh';
|
||||||
|
export const locales: string[] = ['en', 'zh'];
|
||||||
|
export const timeZone = 'UTC';
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { createJSONStorage, persist } from "zustand/middleware";
|
||||||
|
|
||||||
|
type AccessesState = {
|
||||||
|
token: string;
|
||||||
|
setToken: (newVal: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useAccessStore = create<AccessesState>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
token: get()?.token || "",
|
||||||
|
setToken: (newVal: any) => set(() => ({ token: newVal })),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "token",
|
||||||
|
storage: createJSONStorage(() => localStorage), // default localstorage
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export default useAccessStore;
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_TOKEN: string
|
||||||
|
readonly VITE_BOT_ID: string
|
||||||
|
readonly VITE_VOICE_ID: string
|
||||||
|
// 其他环境变量...
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
darkMode: ["class"],
|
||||||
|
content: ["./src/**/*.{ts,tsx}"],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
chart: {
|
||||||
|
1: "hsl(var(--chart-1))",
|
||||||
|
2: "hsl(var(--chart-2))",
|
||||||
|
3: "hsl(var(--chart-3))",
|
||||||
|
4: "hsl(var(--chart-4))",
|
||||||
|
5: "hsl(var(--chart-5))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": [
|
||||||
|
"ES2020",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 服务器信息
|
||||||
|
SERVER_USER="root"
|
||||||
|
SERVER_HOST="106.14.30.150"
|
||||||
|
SERVER_PATH="/opt/1panel/apps/openresty/openresty/www/sites/chat.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"
|
||||||
|
|
||||||
|
# 在上传之前备份原来的文件并压缩
|
||||||
|
ssh -i $PRIVATE_KEY $SERVER_USER@$SERVER_HOST "cd $(dirname $SERVER_PATH) && zip -r $(basename $BACKUP_PATH) $(basename $SERVER_PATH)"
|
||||||
|
|
||||||
|
# 使用 scp 上传文件
|
||||||
|
scp -i $PRIVATE_KEY -r build/* $SERVER_USER@$SERVER_HOST:$SERVER_PATH
|
||||||
|
|
||||||
|
# 提示上传完成
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "上传成功!备份存储于 $BACKUP_PATH"
|
||||||
|
|
||||||
|
# 发送钉钉通知
|
||||||
|
curl -X POST "$DINGDING_WEBHOOK" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"msgtype": "text",
|
||||||
|
"text": {
|
||||||
|
"content": "SixAIGC html| upload success!!! backup to'"$BACKUP_PATH"'"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
else
|
||||||
|
echo "上传失败,请检查错误信息。"
|
||||||
|
|
||||||
|
# 发送钉钉通知
|
||||||
|
curl -X POST "$DINGDING_WEBHOOK" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"msgtype": "text",
|
||||||
|
"text": {
|
||||||
|
"content": "SixAIGC html|upload fail,please check error info。"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import path from "path";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react-swc";
|
||||||
|
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
outDir: 'build',
|
||||||
|
reportCompressedSize: true,
|
||||||
|
sourcemap: false,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks(id) {
|
||||||
|
// 根据需要进行手动代码分割
|
||||||
|
if (id.includes('node_modules')) {
|
||||||
|
return id.split('node_modules/')[1].split('/')[0]; // 将 node_modules 中的库分割成单独的块
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||