From 0db6b6be56cb88cce6e19bc5ef92dbf6dc5ffbd3 Mon Sep 17 00:00:00 2001 From: Nan Date: Wed, 30 Apr 2025 14:06:10 +0800 Subject: [PATCH] login --- app/components/TensorFlow.module.scss | 179 +++++++++ app/components/TensorFlow.tsx | 506 ++++++++++++++++++++++++ app/components/WechatAuthor.module.scss | 64 +++ app/components/WechatAuthor.tsx | 113 ++++++ app/components/WechatLogin.module.scss | 195 +++++++++ app/components/WechatLogin.tsx | 174 ++++++++ app/components/auth-wrapper.tsx | 31 ++ app/components/chat.module.scss | 1 + app/components/home.tsx | 3 + app/components/interview-overlay.scss | 163 ++++++++ app/components/interview-overlay.tsx | 341 ++++------------ app/components/sidebar.tsx | 4 + app/constant.ts | 4 +- app/icons/wechat-qrcode-mock.svg | 51 +++ app/pages/login.tsx | 23 ++ app/store/access.ts | 3 + package-lock.json | 432 +++++++++++++++++++- package.json | 1 + yarn.lock | 170 +++++++- 19 files changed, 2183 insertions(+), 275 deletions(-) create mode 100644 app/components/TensorFlow.module.scss create mode 100644 app/components/TensorFlow.tsx create mode 100644 app/components/WechatAuthor.module.scss create mode 100644 app/components/WechatAuthor.tsx create mode 100644 app/components/WechatLogin.module.scss create mode 100644 app/components/WechatLogin.tsx create mode 100644 app/components/auth-wrapper.tsx create mode 100644 app/components/interview-overlay.scss create mode 100644 app/icons/wechat-qrcode-mock.svg create mode 100644 app/pages/login.tsx diff --git a/app/components/TensorFlow.module.scss b/app/components/TensorFlow.module.scss new file mode 100644 index 000000000..a1015ef30 --- /dev/null +++ b/app/components/TensorFlow.module.scss @@ -0,0 +1,179 @@ +.voiceRecognitionContainer { + display: flex; + flex-direction: column; + width: 100%; + max-width: 800px; + margin: 0 auto; + padding: 20px; + background-color: #1e1e1e; + border-radius: 10px; + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3); + color: #ffffff; +} + +.title { + text-align: center; + margin-bottom: 20px; + font-size: 24px; +} + +.statusContainer { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 20px; +} + +.statusIndicator { + display: flex; + align-items: center; + background-color: rgba(0, 0, 0, 0.5); + padding: 8px 16px; + border-radius: 20px; + margin-bottom: 10px; +} + +.statusDot { + width: 12px; + height: 12px; + border-radius: 50%; + margin-right: 10px; + + &.idle { + background-color: #888888; + } + + &.recording { + background-color: #ff9800; + animation: pulse 1.5s infinite; + } + + &.training { + background-color: #2196f3; + animation: pulse 1.5s infinite; + } + + &.recognizing { + background-color: #9c27b0; + animation: pulse 1.5s infinite; + } + + &.trained { + background-color: #4caf50; + } + + &.matched { + background-color: #4caf50; + } + + &.not_matched { + background-color: #f44336; + } + + &.error { + background-color: #f44336; + } +} + +.statusText { + font-size: 14px; +} + +.message { + font-size: 16px; + text-align: center; + margin: 0; +} + +.visualizerContainer { + width: 100%; + height: 150px; + margin-bottom: 20px; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 5px; + overflow: hidden; +} + +.controlsContainer { + display: flex; + justify-content: space-between; + margin-bottom: 20px; + + @media (max-width: 600px) { + flex-direction: column; + } +} + +.trainingControls, +.recognitionControls { + display: flex; + flex-direction: column; + width: 48%; + + @media (max-width: 600px) { + width: 100%; + margin-bottom: 20px; + } + + h3 { + margin-bottom: 10px; + font-size: 18px; + } +} + +.button { + padding: 10px 15px; + margin-bottom: 10px; + border: none; + border-radius: 5px; + background-color: #2196f3; + color: white; + font-size: 14px; + cursor: pointer; + transition: background-color 0.3s; + + &:hover:not(:disabled) { + background-color: #1976d2; + } + + &:disabled { + background-color: #cccccc; + color: #666666; + cursor: not-allowed; + } +} + +.resultContainer { + margin-top: 20px; + padding: 15px; + background-color: rgba(0, 0, 0, 0.3); + border-radius: 5px; +} + +.scoreBar { + width: 100%; + height: 20px; + background-color: rgba(255, 255, 255, 0.1); + border-radius: 10px; + overflow: hidden; + margin-bottom: 10px; +} + +.scoreIndicator { + height: 100%; + background: linear-gradient(to right, #f44336, #ffeb3b, #4caf50); + border-radius: 10px; + transition: width 0.5s ease-in-out; +} + +.scoreValue { + text-align: center; + font-size: 16px; + font-weight: bold; +} + +@keyframes pulse { + 0% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7); } + 70% { box-shadow: 0 0 0 10px rgba(255, 255, 255, 0); } + 100% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); } +} \ No newline at end of file diff --git a/app/components/TensorFlow.tsx b/app/components/TensorFlow.tsx new file mode 100644 index 000000000..8247c9510 --- /dev/null +++ b/app/components/TensorFlow.tsx @@ -0,0 +1,506 @@ +import React, { useState, useEffect, useRef } from "react"; +import * as tf from "@tensorflow/tfjs"; +import { VoicePrint } from "./voice-print/voice-print"; +import styles from "./TensorFlow.module.scss"; + +// 声纹识别状态 +enum VoiceRecognitionStatus { + IDLE = "空闲", + RECORDING = "录制中", + TRAINING = "训练中", + RECOGNIZING = "识别中", + TRAINED = "已训练", + MATCHED = "声纹匹配", + NOT_MATCHED = "声纹不匹配", + ERROR = "错误", +} + +// 声纹特征提取参数 +const SAMPLE_RATE = 16000; // 采样率 +const FFT_SIZE = 1024; // FFT大小 +const MEL_BINS = 40; // Mel滤波器数量 +const FRAME_LENGTH = 25; // 帧长度(ms) +const FRAME_STEP = 10; // 帧步长(ms) +const FEATURE_LENGTH = 100; // 特征序列长度 + +const TensorFlow: React.FC = () => { + // 状态管理 + const [status, setStatus] = useState( + VoiceRecognitionStatus.IDLE, + ); + const [message, setMessage] = useState(""); + const [isRecording, setIsRecording] = useState(false); + const [isTrained, setIsTrained] = useState(false); + const [matchScore, setMatchScore] = useState(0); + const [frequencies, setFrequencies] = useState( + undefined, + ); + + // 引用 + const audioContextRef = useRef(null); + const analyserRef = useRef(null); + const mediaStreamRef = useRef(null); + const recordedChunksRef = useRef([]); + const modelRef = useRef(null); + const voiceprintRef = useRef(null); + const animationFrameRef = useRef(null); + + // 初始化 + useEffect(() => { + // 检查是否有保存的声纹模型 + const savedVoiceprint = localStorage.getItem("userVoiceprint"); + if (savedVoiceprint) { + try { + voiceprintRef.current = new Float32Array(JSON.parse(savedVoiceprint)); + setIsTrained(true); + setStatus(VoiceRecognitionStatus.TRAINED); + setMessage("已加载保存的声纹模型"); + } catch (error) { + console.error("加载保存的声纹模型失败:", error); + } + } + + // 加载TensorFlow模型 + loadModel(); + + return () => { + stopRecording(); + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } + }; + }, []); + + // 加载声纹识别模型 + const loadModel = async () => { + try { + // 创建简单的声纹识别模型 + const model = tf.sequential(); + + // 添加卷积层处理音频特征 + model.add( + tf.layers.conv1d({ + inputShape: [FEATURE_LENGTH, MEL_BINS], + filters: 32, + kernelSize: 3, + activation: "relu", + }), + ); + + model.add(tf.layers.maxPooling1d({ poolSize: 2 })); + + model.add( + tf.layers.conv1d({ + filters: 64, + kernelSize: 3, + activation: "relu", + }), + ); + + model.add(tf.layers.maxPooling1d({ poolSize: 2 })); + model.add(tf.layers.flatten()); + + // 添加全连接层 + model.add(tf.layers.dense({ units: 128, activation: "relu" })); + model.add(tf.layers.dropout({ rate: 0.5 })); + + // 输出层 - 声纹特征向量 + model.add(tf.layers.dense({ units: 64, activation: "linear" })); + + // 编译模型 + model.compile({ + optimizer: "adam", + loss: "meanSquaredError", + }); + + modelRef.current = model; + console.log("声纹识别模型已加载"); + } catch (error) { + console.error("加载模型失败:", error); + setStatus(VoiceRecognitionStatus.ERROR); + setMessage("加载模型失败"); + } + }; + + // 开始录音 + const startRecording = async (isTraining: boolean = false) => { + try { + if (isRecording) return; + + // 重置录音数据 + recordedChunksRef.current = []; + + // 请求麦克风权限 + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + mediaStreamRef.current = stream; + + // 创建音频上下文 + const audioContext = new (window.AudioContext || + (window as any).webkitAudioContext)(); + audioContextRef.current = audioContext; + + // 创建分析器节点用于可视化 + const analyser = audioContext.createAnalyser(); + analyser.fftSize = FFT_SIZE; + analyserRef.current = analyser; + + // 创建音频源 + const source = audioContext.createMediaStreamSource(stream); + source.connect(analyser); + + // 创建处理器节点 + const processor = audioContext.createScriptProcessor(4096, 1, 1); + + // 处理音频数据 + processor.onaudioprocess = (e) => { + const inputData = e.inputBuffer.getChannelData(0); + recordedChunksRef.current.push(new Float32Array(inputData)); + }; + + // 连接节点 + analyser.connect(processor); + processor.connect(audioContext.destination); + + // 更新状态 + setIsRecording(true); + setStatus( + isTraining + ? VoiceRecognitionStatus.RECORDING + : VoiceRecognitionStatus.RECOGNIZING, + ); + setMessage( + isTraining ? "请说话3-5秒钟用于训练..." : "请说话进行声纹识别...", + ); + + // 开始频谱可视化 + startVisualization(); + + // 设置自动停止录音(训练模式下5秒后自动停止) + if (isTraining) { + setTimeout(() => { + stopRecording(); + trainVoiceprint(); + }, 5000); + } + } catch (error) { + console.error("开始录音失败:", error); + setStatus(VoiceRecognitionStatus.ERROR); + setMessage("无法访问麦克风,请检查权限"); + } + }; + + // 停止录音 + const stopRecording = () => { + if (!isRecording) return; + + // 停止所有音频流 + if (mediaStreamRef.current) { + mediaStreamRef.current.getTracks().forEach((track) => track.stop()); + mediaStreamRef.current = null; + } + + // 关闭音频上下文 + if (audioContextRef.current) { + audioContextRef.current.close(); + audioContextRef.current = null; + } + + // 停止可视化 + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + animationFrameRef.current = null; + } + + setIsRecording(false); + setFrequencies(undefined); + }; + + // 开始频谱可视化 + const startVisualization = () => { + const analyser = analyserRef.current; + if (!analyser) return; + + const bufferLength = analyser.frequencyBinCount; + const dataArray = new Uint8Array(bufferLength); + + const updateVisualization = () => { + if (!analyser) return; + + analyser.getByteFrequencyData(dataArray); + setFrequencies(dataArray); + + animationFrameRef.current = requestAnimationFrame(updateVisualization); + }; + + updateVisualization(); + }; + + // 提取音频特征 + const extractFeatures = async ( + audioData: Float32Array[], + ): Promise => { + try { + // 合并所有音频块 + const mergedData = new Float32Array( + audioData.reduce((acc, chunk) => acc + chunk.length, 0), + ); + let offset = 0; + for (const chunk of audioData) { + mergedData.set(chunk, offset); + offset += chunk.length; + } + + // 转换为张量 + const audioTensor = tf.tensor1d(mergedData); + + // 计算梅尔频谱图 (简化版) + // 在实际应用中,这里应该使用更复杂的信号处理方法 + // 如MFCC (Mel-frequency cepstral coefficients) + const frameLength = Math.round((SAMPLE_RATE * FRAME_LENGTH) / 1000); + const frameStep = Math.round((SAMPLE_RATE * FRAME_STEP) / 1000); + + // 使用短时傅里叶变换提取特征 + // 注意:这是简化版,实际应用中应使用专业的DSP库 + const frames = []; + for (let i = 0; i + frameLength <= mergedData.length; i += frameStep) { + const frame = mergedData.slice(i, i + frameLength); + frames.push(Array.from(frame)); + } + + // 限制帧数 + const limitedFrames = frames.slice(0, FEATURE_LENGTH); + + // 如果帧数不足,用零填充 + while (limitedFrames.length < FEATURE_LENGTH) { + limitedFrames.push(new Array(frameLength).fill(0)); + } + + // 创建特征张量 + const featureTensor = tf.tensor(limitedFrames); + + // 简化的梅尔频谱计算 + // 在实际应用中应使用更准确的方法 + const melSpectrogram = tf.tidy(() => { + // 应用FFT (简化) + const fftMag = featureTensor.abs(); + + // 降维到MEL_BINS + const reshaped = fftMag.reshape([FEATURE_LENGTH, -1]); + const melFeatures = reshaped.slice([0, 0], [FEATURE_LENGTH, MEL_BINS]); + + // 归一化 + const normalized = melFeatures.div(tf.scalar(255.0)); + + return normalized.expandDims(0); // 添加批次维度 + }); + + return melSpectrogram; + } catch (error) { + console.error("特征提取失败:", error); + return null; + } + }; + + // 训练声纹模型 + const trainVoiceprint = async () => { + if (recordedChunksRef.current.length === 0 || !modelRef.current) { + setStatus(VoiceRecognitionStatus.ERROR); + setMessage("没有录音数据或模型未加载"); + return; + } + + setStatus(VoiceRecognitionStatus.TRAINING); + setMessage("正在训练声纹模型..."); + + try { + // 提取特征 + const features = await extractFeatures(recordedChunksRef.current); + if (!features) throw new Error("特征提取失败"); + + // 使用模型提取声纹特征向量 + const voiceprint = tf.tidy(() => { + // 前向传播获取声纹特征 + const prediction = modelRef.current!.predict(features) as tf.Tensor; + // 归一化特征向量 + return tf.div(prediction, tf.norm(prediction)); + }); + + // 保存声纹特征 + const voiceprintData = await voiceprint.data(); + voiceprintRef.current = new Float32Array(voiceprintData); + + // 保存到localStorage + localStorage.setItem( + "userVoiceprint", + JSON.stringify(Array.from(voiceprintData)), + ); + + setIsTrained(true); + setStatus(VoiceRecognitionStatus.TRAINED); + setMessage("声纹模型训练完成并已保存"); + + // 清理 + voiceprint.dispose(); + features.dispose(); + } catch (error) { + console.error("训练失败:", error); + setStatus(VoiceRecognitionStatus.ERROR); + setMessage("声纹训练失败"); + } + }; + + // 识别声纹 + const recognizeVoice = async () => { + if (!isTrained || !voiceprintRef.current) { + setStatus(VoiceRecognitionStatus.ERROR); + setMessage("请先训练声纹模型"); + return; + } + + if (recordedChunksRef.current.length === 0 || !modelRef.current) { + setStatus(VoiceRecognitionStatus.ERROR); + setMessage("没有录音数据或模型未加载"); + return; + } + + try { + // 提取特征 + const features = await extractFeatures(recordedChunksRef.current); + if (!features) throw new Error("特征提取失败"); + + // 使用模型提取声纹特征向量 + const currentVoiceprint = tf.tidy(() => { + // 前向传播获取声纹特征 + const prediction = modelRef.current!.predict(features) as tf.Tensor; + // 归一化特征向量 + return tf.div(prediction, tf.norm(prediction)); + }); + + // 计算与保存的声纹的余弦相似度 + const similarity = tf.tidy(() => { + const savedVoiceprint = tf.tensor1d(voiceprintRef.current!); + // 计算点积 + const dotProduct = tf.sum( + tf.mul(currentVoiceprint.reshape([-1]), savedVoiceprint), + ); + return dotProduct; + }); + + // 获取相似度分数 (范围从-1到1,越接近1表示越相似) + const similarityScore = await similarity.data(); + const score = similarityScore[0]; + setMatchScore(score); + + // 判断是否为同一人 (阈值可调整) + const threshold = 0.7; + const isMatch = score > threshold; + + setStatus( + isMatch + ? VoiceRecognitionStatus.MATCHED + : VoiceRecognitionStatus.NOT_MATCHED, + ); + setMessage( + isMatch + ? `声纹匹配成功!相似度: ${(score * 100).toFixed(2)}%` + : `声纹不匹配。相似度: ${(score * 100).toFixed(2)}%`, + ); + + // 清理 + currentVoiceprint.dispose(); + features.dispose(); + similarity.dispose(); + } catch (error) { + console.error("识别失败:", error); + setStatus(VoiceRecognitionStatus.ERROR); + setMessage("声纹识别失败"); + } + }; + + // 清除训练数据 + const clearTrainedData = () => { + localStorage.removeItem("userVoiceprint"); + voiceprintRef.current = null; + setIsTrained(false); + setStatus(VoiceRecognitionStatus.IDLE); + setMessage("声纹数据已清除"); + }; + + return ( +
+

声纹识别系统

+ +
+
+
+ {status} +
+

{message}

+
+ +
+ +
+ +
+
+

训练声纹

+ + +
+ +
+

声纹识别

+ + +
+
+ + {status === VoiceRecognitionStatus.MATCHED || + status === VoiceRecognitionStatus.NOT_MATCHED ? ( +
+
+
+
+
+ 相似度: {(matchScore * 100).toFixed(2)}% +
+
+ ) : null} +
+ ); +}; + +export default TensorFlow; diff --git a/app/components/WechatAuthor.module.scss b/app/components/WechatAuthor.module.scss new file mode 100644 index 000000000..2f2707dfd --- /dev/null +++ b/app/components/WechatAuthor.module.scss @@ -0,0 +1,64 @@ +.container { + position: relative; + padding: 15px; + border-bottom: 1px solid var(--gray); +} + +.avatarContainer { + display: flex; + align-items: center; + cursor: pointer; +} + +.avatar { + width: 40px; + height: 40px; + border-radius: 50%; + object-fit: cover; + margin-right: 10px; + border: 2px solid var(--primary); +} + +.userInfo { + flex: 1; + overflow: hidden; +} + +.nickname { + font-size: 16px; + font-weight: bold; + color: var(--black); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.userId { + font-size: 12px; + color: var(--black-50); + margin-top: 2px; +} + +.menu { + position: absolute; + top: 100%; + left: 0; + width: 120px; + background-color: var(--white); + border-radius: 4px; + box-shadow: var(--card-shadow); + z-index: 1000; + overflow: hidden; +} + +.menuItem { + padding: 10px 15px; + font-size: 14px; + color: var(--black); + cursor: pointer; + transition: background-color 0.3s; + + &:hover { + background-color: var(--gray); + } +} \ No newline at end of file diff --git a/app/components/WechatAuthor.tsx b/app/components/WechatAuthor.tsx new file mode 100644 index 000000000..43c94a357 --- /dev/null +++ b/app/components/WechatAuthor.tsx @@ -0,0 +1,113 @@ +import React, { useState, useRef, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import styles from "./WechatAuthor.module.scss"; +import { Path } from "../constant"; +import { useAccessStore } from "../store"; +import { safeLocalStorage } from "../utils"; +import { showConfirm } from "./ui-lib"; + +interface WechatUserInfo { + id: string; + nickname: string; + avatar: string; + accessToken: string; +} + +export function WechatAuthor() { + const navigate = useNavigate(); + const accessStore = useAccessStore(); + const storage = safeLocalStorage(); + const [userInfo, setUserInfo] = useState(null); + const [showMenu, setShowMenu] = useState(false); + const menuRef = useRef(null); + // 加载用户信息 + useEffect(() => { + const userInfoStr = storage.getItem("wechat_user_info"); + if (userInfoStr) { + try { + const parsedInfo = JSON.parse(userInfoStr); + setUserInfo(parsedInfo); + } catch (e) { + console.error("Failed to parse user info", e); + } + } + }, []); + + // 点击外部关闭菜单 + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setShowMenu(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + // 处理登出 + const handleLogout = async () => { + const confirmed = await showConfirm("确定要退出登录吗?"); + if (confirmed) { + // 清除登录信息 + storage.removeItem("wechat_user_info"); + + // 更新访问状态 + accessStore.update((access) => { + access.accessToken = ""; + access.wechatLoggedIn = false; + }); + + // 跳转到登录页 + navigate(Path.Home); + } + setShowMenu(false); + }; + + // 如果没有用户信息,显示登录按钮 + if (!accessStore.wechatLoggedIn) { + return ( +
+
navigate(Path.Login)} + > + 点击登录 +
+
+ ); + } + + return ( +
+
setShowMenu(true)} + onContextMenu={(e) => { + e.preventDefault(); + setShowMenu(true); + }} + > + {userInfo?.nickname} +
+
{userInfo?.nickname}
+
ID: {userInfo?.id}
+
+
+ + {showMenu && ( +
+
+ 退出登录 +
+
+ )} +
+ ); +} diff --git a/app/components/WechatLogin.module.scss b/app/components/WechatLogin.module.scss new file mode 100644 index 000000000..55509e4c9 --- /dev/null +++ b/app/components/WechatLogin.module.scss @@ -0,0 +1,195 @@ +.container { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + background-color: var(--gray); +} + +.loginCard { + width: 400px; + background-color: var(--white); + border-radius: 10px; + box-shadow: var(--card-shadow); + padding: 30px; + display: flex; + flex-direction: column; + align-items: center; +} + +.header { + text-align: center; + margin-bottom: 30px; + + h2 { + font-size: 24px; + margin-bottom: 10px; + color: var(--primary); + } + + .subtitle { + font-size: 14px; + color: var(--black-50); + } +} + +.qrcodeContainer { + width: 240px; + height: 240px; + position: relative; + margin-bottom: 20px; + display: flex; + justify-content: center; + align-items: center; +} + +.loadingWrapper { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + + p { + margin-top: 15px; + color: var(--black-50); + font-size: 14px; + } +} + +.loadingIcon { + width: 40px; + height: 40px; + animation: spin 1.5s linear infinite; +} + +.qrcodeWrapper { + position: relative; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + cursor: pointer; + + &:hover .qrcodeOverlay { + opacity: 1; + } +} + +.qrcode { + width: 200px; + height: 200px; + padding: 10px; + background-color: white; + border: 1px solid var(--gray); + border-radius: 8px; +} + +.qrcodeOverlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + opacity: 0; + transition: opacity 0.3s; + border-radius: 8px; + + p { + color: white; + font-size: 16px; + } +} + +.qrcodeHint { + margin-top: 15px; + color: var(--black-50); + font-size: 14px; + text-align: center; +} + +.statusWrapper { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; +} + +.statusIcon { + width: 60px; + height: 60px; + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 20px; +} + +.successIcon { + width: 60px; + height: 60px; + color: var(--success); +} + +.errorIcon { + width: 60px; + height: 60px; + color: var(--error); +} + +.statusText { + font-size: 16px; + text-align: center; + color: var(--black); + margin-bottom: 20px; +} + +.footer { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; +} + +.expireHint { + font-size: 12px; + color: var(--black-50); + margin-bottom: 15px; +} + +.refreshButton { + padding: 8px 20px; + background-color: var(--primary); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.3s; + + &:hover { + background-color: var(--primary-dark); + } + + &:disabled { + background-color: var(--gray); + cursor: not-allowed; + } +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/app/components/WechatLogin.tsx b/app/components/WechatLogin.tsx new file mode 100644 index 000000000..9d84745b7 --- /dev/null +++ b/app/components/WechatLogin.tsx @@ -0,0 +1,174 @@ +import React, { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { Path } from "../constant"; +import styles from "./WechatLogin.module.scss"; +import LoadingIcon from "../icons/loading.svg"; +// import QRCodeImage from "../icons/wechat-qrcode-mock.svg"; // 假设有一个模拟的二维码SVG +import SuccessIcon from "../icons/confirm.svg"; +import ErrorIcon from "../icons/close.svg"; +import Locale from "../locales"; +import { useAccessStore } from "../store"; +import { safeLocalStorage } from "../utils"; + +// 登录状态枚举 +enum LoginStatus { + LOADING = "loading", + READY = "ready", + SCANNED = "scanned", + CONFIRMED = "confirmed", + SUCCESS = "success", + ERROR = "error", +} + +export function WechatLogin() { + const navigate = useNavigate(); + const [status, setStatus] = useState(LoginStatus.LOADING); + const [errorMessage, setErrorMessage] = useState(""); + const accessStore = useAccessStore(); + const storage = safeLocalStorage(); + + // 模拟登录流程 + useEffect(() => { + // 初始加载 + const timer1 = setTimeout(() => { + setStatus(LoginStatus.READY); + }, 1000); + + return () => { + clearTimeout(timer1); + }; + }, []); + + // 模拟二维码扫描和确认过程 + const simulateLogin = () => { + // 模拟扫码 + setStatus(LoginStatus.SCANNED); + + // 模拟确认 + setTimeout(() => { + setStatus(LoginStatus.CONFIRMED); + + // 模拟登录成功 + setTimeout(() => { + setStatus(LoginStatus.SUCCESS); + + // 存储登录信息 + const mockUserInfo = { + id: "wx_" + Math.floor(Math.random() * 1000000), + nickname: "微信用户", + avatar: "https://placekitten.com/100/100", // 模拟头像 + accessToken: "mock_token_" + Date.now(), + }; + + storage.setItem("wechat_user_info", JSON.stringify(mockUserInfo)); + + // 更新访问状态 + accessStore.update((access) => { + access.accessToken = mockUserInfo.accessToken; + access.wechatLoggedIn = true; + }); + + // 登录成功后跳转 + setTimeout(() => { + navigate(Path.Chat); + }, 2000); + }, 1000); + }, 2000); + }; + + // 刷新二维码 + const refreshQRCode = () => { + setStatus(LoginStatus.LOADING); + setTimeout(() => { + setStatus(LoginStatus.READY); + }, 1000); + }; + + // 处理登录错误 + const handleLoginError = () => { + setStatus(LoginStatus.ERROR); + setErrorMessage("登录失败,请稍后重试"); + }; + + return ( +
+
+
+

{Locale.Auth.Title}

+

使用微信扫码登录

+
+ +
+ {status === LoginStatus.LOADING && ( +
+ +

正在加载二维码...

+
+ )} + + {status === LoginStatus.READY && ( +
+ {/* */} +
+

点击模拟扫码

+
+

请使用微信扫描二维码登录

+
+ )} + + {status === LoginStatus.SCANNED && ( +
+
+ +
+

已扫码,请在微信上确认

+
+ )} + + {status === LoginStatus.CONFIRMED && ( +
+
+ +
+

已确认,正在登录...

+
+ )} + + {status === LoginStatus.SUCCESS && ( +
+
+ +
+

登录成功,正在跳转...

+
+ )} + + {status === LoginStatus.ERROR && ( +
+
+ +
+

{errorMessage}

+ +
+ )} +
+ + {(status === LoginStatus.READY || status === LoginStatus.LOADING) && ( +
+

二维码有效期为2分钟,请尽快扫码

+ +
+ )} +
+
+ ); +} diff --git a/app/components/auth-wrapper.tsx b/app/components/auth-wrapper.tsx new file mode 100644 index 000000000..f01424d63 --- /dev/null +++ b/app/components/auth-wrapper.tsx @@ -0,0 +1,31 @@ +import React, { useEffect } from "react"; +import { useNavigate, useLocation } from "react-router-dom"; +import { Path } from "../constant"; +import { useAccessStore } from "../store"; +import { safeLocalStorage } from "../utils"; + +// 不需要登录就可以访问的路径 +const PUBLIC_PATHS = [Path.Home, Path.Login]; + +export function AuthWrapper({ children }: { children: React.ReactNode }) { + const navigate = useNavigate(); + const location = useLocation(); + const accessStore = useAccessStore(); + const storage = safeLocalStorage(); + + useEffect(() => { + // 检查当前路径是否需要登录 + const isPublicPath = PUBLIC_PATHS.includes(location.pathname as Path); + + // 检查是否已登录 + const userInfoStr = storage.getItem("wechat_user_info"); + const isLoggedIn = userInfoStr && accessStore.wechatLoggedIn; + + // 如果需要登录但未登录,重定向到登录页 + if (!isPublicPath && !isLoggedIn) { + navigate(Path.Login); + } + }, [location.pathname, navigate, accessStore.wechatLoggedIn]); + + return <>{children}; +} diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss index 5d355ca63..a1f47cdaf 100644 --- a/app/components/chat.module.scss +++ b/app/components/chat.module.scss @@ -620,6 +620,7 @@ flex-direction: column; border-top: var(--border-in-light); box-shadow: var(--card-shadow); + // height: 15vh; // 添加固定高度为视窗高度的15% .chat-input-actions { .chat-input-action { diff --git a/app/components/home.tsx b/app/components/home.tsx index 386b4bd3e..9501f8091 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -30,6 +30,7 @@ import { type ClientApi, getClientApi } from "../client/api"; import { useAccessStore } from "../store"; import clsx from "clsx"; import { initializeMcpSystem, isMcpEnabled } from "../mcp/actions"; +import LoginPage from "../pages/login"; export function Loading(props: { noLogo?: boolean }) { return ( @@ -198,6 +199,7 @@ function Screen() { })} /> + {/* 只有登录时才可以路由到其他页面,相当于拦截器*/} } /> } /> @@ -207,6 +209,7 @@ function Screen() { } /> } /> } /> + } /> {/* }/> */} diff --git a/app/components/interview-overlay.scss b/app/components/interview-overlay.scss new file mode 100644 index 000000000..5d1fea7ef --- /dev/null +++ b/app/components/interview-overlay.scss @@ -0,0 +1,163 @@ +.interview-overlay { + position: fixed; + top: 5px; + right: 5px; + width: 33vw; + height: 85vh; + background-color: #1e1e1e; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 10px; + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3); + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + color: #ffffff; + z-index: 1000; + padding: 20px; + overflow-y: auto; + + &.dragging { + cursor: col-resize; + } + + .drag-handle { + position: absolute; + left: 0; + top: 0; + width: 5px; + height: 100%; + cursor: col-resize; + background-color: transparent; + + &:hover { + background-color: rgba(255, 255, 255, 0.2); + } + } + + .content-container { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + width: 100%; + } + + .status-indicator { + display: flex; + align-items: center; + justify-content: flex-start; + margin-bottom: 1rem; + background-color: rgba(0, 0, 0, 0.5); + padding: 0.5rem 1rem; + border-radius: 1rem; + width: fit-content; + + .indicator-dot { + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 10px; + + &.listening { + background-color: #4caf50; + box-shadow: 0 0 10px #4caf50; + animation: pulse 1.5s infinite; + } + + &.not-listening { + background-color: #ff6b6b; + } + } + + .status-text { + font-size: 0.9rem; + } + } + + .error-message { + color: #ff6b6b; + margin-bottom: 1rem; + background-color: rgba(0, 0, 0, 0.5); + padding: 0.75rem 1rem; + border-radius: 0.5rem; + width: 100%; + text-align: center; + } + + .transcript-display { + width: 100%; + margin-bottom: 1rem; + padding: 1rem; + background-color: rgba(0, 0, 0, 0.5); + border-radius: 0.5rem; + max-height: 120px; + overflow-y: auto; + text-align: left; + font-size: 0.9rem; + line-height: 1.5; + border: 1px solid rgba(0, 0, 0, 0.5); + } + + .button-container { + display: flex; + justify-content: space-between; + gap: 0.5rem; + margin-top: 1rem; + width: 100%; + + .button { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + border-radius: 0.5rem; + padding: 0.5rem 1rem; + font-size: 0.9rem; + cursor: pointer; + transition: all 0.2s ease; + flex: 1; + color: white; + border: none; + + &.pause-button { + background-color: #ff9800; + + &:hover { + background-color: #f57c00; + } + + &.paused { + background-color: #4caf50; + + &:hover { + background-color: #45a049; + } + } + } + + &.stop-button { + background-color: rgba(0, 0, 0, 0.5); + + &:hover { + background-color: #000000; + } + } + + &.clear-button { + background-color: transparent; + border: 1px solid rgba(0, 0, 0, 0.5); + + &:hover { + background-color: rgba(0, 0, 0, 0.5); + } + } + } + } +} + +@keyframes pulse { + 0% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.7); } + 70% { box-shadow: 0 0 0 10px rgba(76, 175, 80, 0); } + 100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); } +} \ No newline at end of file diff --git a/app/components/interview-overlay.tsx b/app/components/interview-overlay.tsx index 7a0613fb4..862a75bb6 100644 --- a/app/components/interview-overlay.tsx +++ b/app/components/interview-overlay.tsx @@ -3,6 +3,7 @@ import StopIcon from "../icons/pause.svg"; import SpeechRecognition, { useSpeechRecognition, } from "react-speech-recognition"; +import "./interview-overlay.scss"; interface InterviewOverlayProps { onClose: () => void; @@ -16,11 +17,17 @@ export const InterviewOverlay: React.FC = ({ submitMessage, }) => { const [visible, setVisible] = useState(true); - const [countdown, setCountdown] = useState(20); - const countdownRef = useRef(countdown); - const intervalIdRef = useRef(null); + // const [countdown, setCountdown] = useState(20); + // const countdownRef = useRef(countdown); + // const intervalIdRef = useRef(null); // 添加暂停状态 const [isPaused, setIsPaused] = useState(false); + // 添加宽度状态和拖动状态 + const [width, setWidth] = useState("33vw"); + const [isDragging, setIsDragging] = useState(false); + const isDraggingRef = useRef(isDragging); + const dragStartXRef = useRef(0); + const initialWidthRef = useRef(0); // 使用 react-speech-recognition 的钩子 const { @@ -37,12 +44,6 @@ export const InterviewOverlay: React.FC = ({ useEffect(() => { transcriptRef.current = transcript; onTextUpdate(transcript); - - // 当有新的语音识别结果时,重置倒计时 - if (transcript) { - setCountdown(20); - countdownRef.current = 20; - } }, [transcript, onTextUpdate]); // 检查浏览器是否支持语音识别 @@ -62,25 +63,9 @@ export const InterviewOverlay: React.FC = ({ continuous: true, language: "zh-CN", }); - - // 设置倒计时 - intervalIdRef.current = setInterval(() => { - setCountdown((prev) => { - const newCount = prev - 1; - countdownRef.current = newCount; - - if (newCount <= 0) { - stopRecognition(); - } - return newCount; - }); - }, 1000); } return () => { - if (intervalIdRef.current) { - clearInterval(intervalIdRef.current); - } SpeechRecognition.stopListening(); }; }, [visible, isPaused]); @@ -88,17 +73,10 @@ export const InterviewOverlay: React.FC = ({ const stopRecognition = () => { try { SpeechRecognition.stopListening(); - // 提交最终结果 if (transcriptRef.current) { submitMessage(transcriptRef.current); } - - // 清理倒计时 - if (intervalIdRef.current) { - clearInterval(intervalIdRef.current); - } - // 关闭overlay setVisible(false); onClose(); @@ -110,150 +88,108 @@ export const InterviewOverlay: React.FC = ({ // 添加暂停/恢复功能 const togglePause = () => { if (!isPaused) { - // 暂停 - SpeechRecognition.stopListening(); - if (intervalIdRef.current) { - clearInterval(intervalIdRef.current); - } + // 使用更强制的中断方式 + SpeechRecognition.abortListening(); + // 然后再调用正常的停止方法确保完全停止 + setTimeout(() => { + SpeechRecognition.stopListening(); + }, 0); - // 提交当前文本 - if (transcriptRef.current) { - submitMessage(transcriptRef.current); - resetTranscript(); + if (transcriptRef.current && transcriptRef.current.trim() !== "") { + // 使用setTimeout将提交操作放到下一个事件循环,避免阻塞UI更新 + setTimeout(() => { + submitMessage(transcriptRef.current); + resetTranscript(); + }, 0); } } else { - // 恢复 - console.log("recover "); - // 先确保停止当前可能存在的监听 SpeechRecognition.abortListening(); - // 短暂延迟后重新启动监听 setTimeout(() => { SpeechRecognition.startListening({ continuous: true, language: "zh-CN", }); - // 重置文本 resetTranscript(); - }, 100); - // 重新设置倒计时 - intervalIdRef.current = setInterval(() => { - setCountdown((prev) => { - const newCount = prev - 1; - countdownRef.current = newCount; - - if (newCount <= 0) { - stopRecognition(); - } - return newCount; - }); - }, 1000); + }, 0); } - setIsPaused(!isPaused); }; + // 添加拖动相关的事件处理函数 + const handleDragStart = (e: React.MouseEvent) => { + setIsDragging(() => { + isDraggingRef.current = true; + return true; + }); + dragStartXRef.current = e.clientX; + initialWidthRef.current = parseInt(width); + document.addEventListener("mousemove", handleDragMove); + document.addEventListener("mouseup", handleDragEnd); + }; + + const handleDragMove = (e: MouseEvent) => { + if (isDraggingRef.current) { + const deltaX = e.clientX - dragStartXRef.current; + const newWidth = Math.max( + 15, + Math.min( + 80, + initialWidthRef.current - (deltaX / window.innerWidth) * 100, + ), + ); + console.log(`mouse have moved Width:${newWidth}vw`); + setWidth(`${newWidth}vw`); + } + }; + + const handleDragEnd = () => { + setIsDragging(() => { + isDraggingRef.current = false; + return false; + }); + document.removeEventListener("mousemove", handleDragMove); + document.removeEventListener("mouseup", handleDragEnd); + }; + + // 组件卸载时清理事件监听器 + useEffect(() => { + return () => { + document.removeEventListener("mousemove", handleDragMove); + document.removeEventListener("mouseup", handleDragEnd); + }; + }, []); + if (!visible) { return null; } return (
-
-

- 剩余{" "} - - {countdown} - {" "} - 秒,超时将自动发送 -

+ {/* 添加左侧拖动条 */} +
+
{/* 语音识别状态指示器 */} -
+
- + {listening ? "正在监听..." : isPaused ? "已暂停" : "未监听"}
{/* 错误提示 */} {(!browserSupportsSpeechRecognition || !isMicrophoneAvailable) && ( -
+
{!browserSupportsSpeechRecognition ? "您的浏览器不支持语音识别功能,请使用Chrome浏览器" : "无法访问麦克风,请检查麦克风权限"} @@ -261,137 +197,28 @@ export const InterviewOverlay: React.FC = ({ )} {/* 识别文本显示区域 */} - {transcript && ( -
- {transcript} -
- )} + {transcript &&
{transcript}
} {/* 按钮区域 */} -
+
{/* 暂停/恢复按钮 */} - -
- - {/* 添加脉冲动画 */} -
); }; diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index 53ad9332b..57a8d45b4 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -32,6 +32,8 @@ import { Selector, showConfirm } from "./ui-lib"; import clsx from "clsx"; import { isMcpEnabled } from "../mcp/actions"; +import { WechatAuthor } from "./WechatAuthor"; + const DISCOVERY = [ { name: Locale.Plugin.Name, path: Path.Plugins }, { name: "Stable Diffusion", path: Path.Sd }, @@ -223,6 +225,7 @@ export function SideBarTail(props: { ); } +// 在侧边栏组件中添加WechatAuthor export function SideBar(props: { className?: string }) { useHotKey(); const { onDragStart, shouldNarrow } = useDragSideBar(); @@ -248,6 +251,7 @@ export function SideBar(props: { className?: string }) { shouldNarrow={shouldNarrow} {...props} > + {/* 添加到最顶部 */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/pages/login.tsx b/app/pages/login.tsx new file mode 100644 index 000000000..30f5cef68 --- /dev/null +++ b/app/pages/login.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { WechatLogin } from "../components/WechatLogin"; +import { useNavigate } from "react-router-dom"; +import { Path } from "../constant"; +import { useAccessStore } from "../store"; +import { useEffect } from "react"; +import { safeLocalStorage } from "../utils"; + +export default function LoginPage() { + const navigate = useNavigate(); + const accessStore = useAccessStore(); + const storage = safeLocalStorage(); + + // 检查是否已登录 + useEffect(() => { + const userInfoStr = storage.getItem("wechat_user_info"); + if (userInfoStr && accessStore.wechatLoggedIn) { + navigate(Path.Chat); + } + }, [navigate, accessStore.wechatLoggedIn]); + + return ; +} diff --git a/app/store/access.ts b/app/store/access.ts index 7025a1814..1bd686a55 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -142,6 +142,9 @@ const DEFAULT_ACCESS_STATE = { defaultModel: "", visionModels: "", + // 添加微信登录状态 + wechatLoggedIn: false, + accessToken: "", // tts config edgeTTSVoiceName: "zh-CN-YunxiNeural", }; diff --git a/package-lock.json b/package-lock.json index 978cede08..396cd3d64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@modelcontextprotocol/sdk": "^1.0.4", "@next/third-parties": "^14.1.0", "@svgr/webpack": "^6.5.1", + "@tensorflow/tfjs": "^4.22.0", "@vercel/analytics": "^0.1.11", "@vercel/speed-insights": "^1.0.2", "axios": "^1.7.5", @@ -4236,6 +4237,355 @@ "node": ">= 10" } }, + "node_modules/@tensorflow/tfjs": { + "version": "4.22.0", + "resolved": "https://registry.npmmirror.com/@tensorflow/tfjs/-/tfjs-4.22.0.tgz", + "integrity": "sha512-0TrIrXs6/b7FLhLVNmfh8Sah6JgjBPH4mZ8JGb7NU6WW+cx00qK5BcAZxw7NCzxj6N8MRAIfHq+oNbPUNG5VAg==", + "license": "Apache-2.0", + "dependencies": { + "@tensorflow/tfjs-backend-cpu": "4.22.0", + "@tensorflow/tfjs-backend-webgl": "4.22.0", + "@tensorflow/tfjs-converter": "4.22.0", + "@tensorflow/tfjs-core": "4.22.0", + "@tensorflow/tfjs-data": "4.22.0", + "@tensorflow/tfjs-layers": "4.22.0", + "argparse": "^1.0.10", + "chalk": "^4.1.0", + "core-js": "3.29.1", + "regenerator-runtime": "^0.13.5", + "yargs": "^16.0.3" + }, + "bin": { + "tfjs-custom-module": "dist/tools/custom_module/cli.js" + } + }, + "node_modules/@tensorflow/tfjs-backend-cpu": { + "version": "4.22.0", + "resolved": "https://registry.npmmirror.com/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-4.22.0.tgz", + "integrity": "sha512-1u0FmuLGuRAi8D2c3cocHTASGXOmHc/4OvoVDENJayjYkS119fcTcQf4iHrtLthWyDIPy3JiPhRrZQC9EwnhLw==", + "license": "Apache-2.0", + "dependencies": { + "@types/seedrandom": "^2.4.28", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@tensorflow/tfjs-backend-webgl": { + "version": "4.22.0", + "resolved": "https://registry.npmmirror.com/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-4.22.0.tgz", + "integrity": "sha512-H535XtZWnWgNwSzv538czjVlbJebDl5QTMOth4RXr2p/kJ1qSIXE0vZvEtO+5EC9b00SvhplECny2yDewQb/Yg==", + "license": "Apache-2.0", + "dependencies": { + "@tensorflow/tfjs-backend-cpu": "4.22.0", + "@types/offscreencanvas": "~2019.3.0", + "@types/seedrandom": "^2.4.28", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@tensorflow/tfjs-converter": { + "version": "4.22.0", + "resolved": "https://registry.npmmirror.com/@tensorflow/tfjs-converter/-/tfjs-converter-4.22.0.tgz", + "integrity": "sha512-PT43MGlnzIo+YfbsjM79Lxk9lOq6uUwZuCc8rrp0hfpLjF6Jv8jS84u2jFb+WpUeuF4K33ZDNx8CjiYrGQ2trQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@tensorflow/tfjs-core": { + "version": "4.22.0", + "resolved": "https://registry.npmmirror.com/@tensorflow/tfjs-core/-/tfjs-core-4.22.0.tgz", + "integrity": "sha512-LEkOyzbknKFoWUwfkr59vSB68DMJ4cjwwHgicXN0DUi3a0Vh1Er3JQqCI1Hl86GGZQvY8ezVrtDIvqR1ZFW55A==", + "license": "Apache-2.0", + "dependencies": { + "@types/long": "^4.0.1", + "@types/offscreencanvas": "~2019.7.0", + "@types/seedrandom": "^2.4.28", + "@webgpu/types": "0.1.38", + "long": "4.0.0", + "node-fetch": "~2.6.1", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + } + }, + "node_modules/@tensorflow/tfjs-core/node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmmirror.com/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, + "node_modules/@tensorflow/tfjs-core/node_modules/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@tensorflow/tfjs-core/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/@tensorflow/tfjs-core/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/@tensorflow/tfjs-core/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@tensorflow/tfjs-data": { + "version": "4.22.0", + "resolved": "https://registry.npmmirror.com/@tensorflow/tfjs-data/-/tfjs-data-4.22.0.tgz", + "integrity": "sha512-dYmF3LihQIGvtgJrt382hSRH4S0QuAp2w1hXJI2+kOaEqo5HnUPG0k5KA6va+S1yUhx7UBToUKCBHeLHFQRV4w==", + "license": "Apache-2.0", + "dependencies": { + "@types/node-fetch": "^2.1.2", + "node-fetch": "~2.6.1", + "string_decoder": "^1.3.0" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0", + "seedrandom": "^3.0.5" + } + }, + "node_modules/@tensorflow/tfjs-data/node_modules/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@tensorflow/tfjs-data/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/@tensorflow/tfjs-data/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/@tensorflow/tfjs-data/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@tensorflow/tfjs-layers": { + "version": "4.22.0", + "resolved": "https://registry.npmmirror.com/@tensorflow/tfjs-layers/-/tfjs-layers-4.22.0.tgz", + "integrity": "sha512-lybPj4ZNj9iIAPUj7a8ZW1hg8KQGfqWLlCZDi9eM/oNKCCAgchiyzx8OrYoWmRrB+AM6VNEeIT+2gZKg5ReihA==", + "license": "Apache-2.0 AND MIT", + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/@tensorflow/tfjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/@tensorflow/tfjs/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, + "node_modules/@tensorflow/tfjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmmirror.com/@testing-library/dom/-/dom-10.4.0.tgz", @@ -4791,6 +5141,12 @@ "@types/lodash": "*" } }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT" + }, "node_modules/@types/mdast": { "version": "3.0.11", "resolved": "https://registry.npmmirror.com/@types/mdast/-/mdast-3.0.11.tgz", @@ -4810,12 +5166,27 @@ "version": "20.11.30", "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.11.30.tgz", "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmmirror.com/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/offscreencanvas": { + "version": "2019.3.0", + "resolved": "https://registry.npmmirror.com/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz", + "integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==", + "license": "MIT" + }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -4875,6 +5246,12 @@ "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", "license": "MIT" }, + "node_modules/@types/seedrandom": { + "version": "2.4.34", + "resolved": "https://registry.npmmirror.com/@types/seedrandom/-/seedrandom-2.4.34.tgz", + "integrity": "sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A==", + "license": "MIT" + }, "node_modules/@types/spark-md5": { "version": "3.0.4", "resolved": "https://registry.npmmirror.com/@types/spark-md5/-/spark-md5-3.0.4.tgz", @@ -5245,6 +5622,12 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@webgpu/types": { + "version": "0.1.38", + "resolved": "https://registry.npmmirror.com/@webgpu/types/-/types-0.1.38.tgz", + "integrity": "sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA==", + "license": "BSD-3-Clause" + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -5396,7 +5779,6 @@ "version": "5.0.1", "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6480,6 +6862,17 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, + "node_modules/core-js": { + "version": "3.29.1", + "resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.29.1.tgz", + "integrity": "sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-js-compat": { "version": "3.29.1", "resolved": "https://registry.npmmirror.com/core-js-compat/-/core-js-compat-3.29.1.tgz", @@ -9066,7 +9459,6 @@ "version": "2.0.5", "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -13068,6 +13460,12 @@ "node": ">=8" } }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "license": "Apache-2.0" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/longest-streak/-/longest-streak-3.1.0.tgz", @@ -15564,7 +15962,6 @@ "version": "2.1.1", "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -15819,7 +16216,6 @@ "version": "5.2.1", "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -15915,6 +16311,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "license": "MIT" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", @@ -16097,7 +16499,6 @@ "version": "1.0.3", "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/stable": { @@ -16160,6 +16561,15 @@ "node": ">=10.0.0" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-argv": { "version": "0.3.1", "resolved": "https://registry.npmmirror.com/string-argv/-/string-argv-0.3.1.tgz", @@ -16336,7 +16746,6 @@ "version": "6.0.1", "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -17101,7 +17510,6 @@ "version": "5.26.5", "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -17766,7 +18174,6 @@ "version": "7.0.0", "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -17871,7 +18278,6 @@ "version": "4.3.0", "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -17887,7 +18293,6 @@ "version": "2.0.1", "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -17900,21 +18305,18 @@ "version": "1.1.4", "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/wrap-ansi/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -17924,7 +18326,6 @@ "version": "4.2.3", "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -17998,7 +18399,6 @@ "version": "5.0.8", "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" diff --git a/package.json b/package.json index d796dcec7..5d61f2b86 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@modelcontextprotocol/sdk": "^1.0.4", "@next/third-parties": "^14.1.0", "@svgr/webpack": "^6.5.1", + "@tensorflow/tfjs": "^4.22.0", "@vercel/analytics": "^0.1.11", "@vercel/speed-insights": "^1.0.2", "axios": "^1.7.5", diff --git a/yarn.lock b/yarn.lock index 44e0aad13..7a044388a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1620,6 +1620,73 @@ "@tauri-apps/cli-win32-ia32-msvc" "1.5.11" "@tauri-apps/cli-win32-x64-msvc" "1.5.11" +"@tensorflow/tfjs-backend-cpu@4.22.0": + version "4.22.0" + resolved "https://registry.npmmirror.com/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-4.22.0.tgz" + integrity sha512-1u0FmuLGuRAi8D2c3cocHTASGXOmHc/4OvoVDENJayjYkS119fcTcQf4iHrtLthWyDIPy3JiPhRrZQC9EwnhLw== + dependencies: + "@types/seedrandom" "^2.4.28" + seedrandom "^3.0.5" + +"@tensorflow/tfjs-backend-webgl@4.22.0": + version "4.22.0" + resolved "https://registry.npmmirror.com/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-4.22.0.tgz" + integrity sha512-H535XtZWnWgNwSzv538czjVlbJebDl5QTMOth4RXr2p/kJ1qSIXE0vZvEtO+5EC9b00SvhplECny2yDewQb/Yg== + dependencies: + "@tensorflow/tfjs-backend-cpu" "4.22.0" + "@types/offscreencanvas" "~2019.3.0" + "@types/seedrandom" "^2.4.28" + seedrandom "^3.0.5" + +"@tensorflow/tfjs-converter@4.22.0": + version "4.22.0" + resolved "https://registry.npmmirror.com/@tensorflow/tfjs-converter/-/tfjs-converter-4.22.0.tgz" + integrity sha512-PT43MGlnzIo+YfbsjM79Lxk9lOq6uUwZuCc8rrp0hfpLjF6Jv8jS84u2jFb+WpUeuF4K33ZDNx8CjiYrGQ2trQ== + +"@tensorflow/tfjs-core@4.22.0": + version "4.22.0" + resolved "https://registry.npmmirror.com/@tensorflow/tfjs-core/-/tfjs-core-4.22.0.tgz" + integrity sha512-LEkOyzbknKFoWUwfkr59vSB68DMJ4cjwwHgicXN0DUi3a0Vh1Er3JQqCI1Hl86GGZQvY8ezVrtDIvqR1ZFW55A== + dependencies: + "@types/long" "^4.0.1" + "@types/offscreencanvas" "~2019.7.0" + "@types/seedrandom" "^2.4.28" + "@webgpu/types" "0.1.38" + long "4.0.0" + node-fetch "~2.6.1" + seedrandom "^3.0.5" + +"@tensorflow/tfjs-data@4.22.0": + version "4.22.0" + resolved "https://registry.npmmirror.com/@tensorflow/tfjs-data/-/tfjs-data-4.22.0.tgz" + integrity sha512-dYmF3LihQIGvtgJrt382hSRH4S0QuAp2w1hXJI2+kOaEqo5HnUPG0k5KA6va+S1yUhx7UBToUKCBHeLHFQRV4w== + dependencies: + "@types/node-fetch" "^2.1.2" + node-fetch "~2.6.1" + string_decoder "^1.3.0" + +"@tensorflow/tfjs-layers@4.22.0": + version "4.22.0" + resolved "https://registry.npmmirror.com/@tensorflow/tfjs-layers/-/tfjs-layers-4.22.0.tgz" + integrity sha512-lybPj4ZNj9iIAPUj7a8ZW1hg8KQGfqWLlCZDi9eM/oNKCCAgchiyzx8OrYoWmRrB+AM6VNEeIT+2gZKg5ReihA== + +"@tensorflow/tfjs@^4.22.0": + version "4.22.0" + resolved "https://registry.npmmirror.com/@tensorflow/tfjs/-/tfjs-4.22.0.tgz" + integrity sha512-0TrIrXs6/b7FLhLVNmfh8Sah6JgjBPH4mZ8JGb7NU6WW+cx00qK5BcAZxw7NCzxj6N8MRAIfHq+oNbPUNG5VAg== + dependencies: + "@tensorflow/tfjs-backend-cpu" "4.22.0" + "@tensorflow/tfjs-backend-webgl" "4.22.0" + "@tensorflow/tfjs-converter" "4.22.0" + "@tensorflow/tfjs-core" "4.22.0" + "@tensorflow/tfjs-data" "4.22.0" + "@tensorflow/tfjs-layers" "4.22.0" + argparse "^1.0.10" + chalk "^4.1.0" + core-js "3.29.1" + regenerator-runtime "^0.13.5" + yargs "^16.0.3" + "@testing-library/dom@^10.0.0", "@testing-library/dom@^10.4.0": version "10.4.0" resolved "https://registry.npmmirror.com/@testing-library/dom/-/dom-10.4.0.tgz" @@ -1872,6 +1939,11 @@ resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.7.tgz" integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA== +"@types/long@^4.0.1": + version "4.0.2" + resolved "https://registry.npmmirror.com/@types/long/-/long-4.0.2.tgz" + integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== + "@types/mdast@^3.0.0": version "3.0.11" resolved "https://registry.npmmirror.com/@types/mdast/-/mdast-3.0.11.tgz" @@ -1884,6 +1956,14 @@ resolved "https://registry.npmmirror.com/@types/ms/-/ms-0.7.31.tgz" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== +"@types/node-fetch@^2.1.2": + version "2.6.12" + resolved "https://registry.npmmirror.com/@types/node-fetch/-/node-fetch-2.6.12.tgz" + integrity sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA== + dependencies: + "@types/node" "*" + form-data "^4.0.0" + "@types/node@*", "@types/node@^20.11.30": version "20.11.30" resolved "https://registry.npmmirror.com/@types/node/-/node-20.11.30.tgz" @@ -1891,6 +1971,16 @@ dependencies: undici-types "~5.26.4" +"@types/offscreencanvas@~2019.3.0": + version "2019.3.0" + resolved "https://registry.npmmirror.com/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz" + integrity sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q== + +"@types/offscreencanvas@~2019.7.0": + version "2019.7.3" + resolved "https://registry.npmmirror.com/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz" + integrity sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A== + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.npmmirror.com/@types/parse-json/-/parse-json-4.0.0.tgz" @@ -1936,6 +2026,11 @@ resolved "https://registry.npmmirror.com/@types/scheduler/-/scheduler-0.16.3.tgz" integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ== +"@types/seedrandom@^2.4.28": + version "2.4.34" + resolved "https://registry.npmmirror.com/@types/seedrandom/-/seedrandom-2.4.34.tgz" + integrity sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A== + "@types/spark-md5@^3.0.4": version "3.0.4" resolved "https://registry.npmmirror.com/@types/spark-md5/-/spark-md5-3.0.4.tgz" @@ -2149,6 +2244,11 @@ "@webassemblyjs/ast" "1.11.6" "@xtuc/long" "4.2.2" +"@webgpu/types@0.1.38": + version "0.1.38" + resolved "https://registry.npmmirror.com/@webgpu/types/-/types-0.1.38.tgz" + integrity sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA== + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.npmmirror.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz" @@ -2290,6 +2390,13 @@ arg@^4.1.0: resolved "https://registry.npmmirror.com/arg/-/arg-4.1.3.tgz" integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== +argparse@^1.0.10: + version "1.0.10" + resolved "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + argparse@^1.0.7: version "1.0.10" resolved "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz" @@ -2735,6 +2842,15 @@ client-only@0.0.1: resolved "https://registry.npmmirror.com/client-only/-/client-only-0.0.1.tgz" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.npmmirror.com/cliui/-/cliui-7.0.4.tgz" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + cliui@^8.0.1: version "8.0.1" resolved "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz" @@ -2867,6 +2983,11 @@ core-js-compat@^3.25.1: dependencies: browserslist "^4.21.5" +core-js@3.29.1: + version "3.29.1" + resolved "https://registry.npmmirror.com/core-js/-/core-js-3.29.1.tgz" + integrity sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw== + cose-base@^1.0.0: version "1.0.3" resolved "https://registry.npmmirror.com/cose-base/-/cose-base-1.0.3.tgz" @@ -5666,6 +5787,11 @@ log-update@^4.0.0: slice-ansi "^4.0.0" wrap-ansi "^6.2.0" +long@4.0.0: + version "4.0.0" + resolved "https://registry.npmmirror.com/long/-/long-4.0.0.tgz" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + longest-streak@^3.0.0: version "3.1.0" resolved "https://registry.npmmirror.com/longest-streak/-/longest-streak-3.1.0.tgz" @@ -6373,6 +6499,13 @@ node-fetch@^3.3.1: fetch-blob "^3.1.4" formdata-polyfill "^4.0.10" +node-fetch@~2.6.1: + version "2.6.13" + resolved "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.6.13.tgz" + integrity sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA== + dependencies: + whatwg-url "^5.0.0" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.npmmirror.com/node-int64/-/node-int64-0.4.0.tgz" @@ -6925,6 +7058,11 @@ regenerate@^1.4.2: resolved "https://registry.npmmirror.com/regenerate/-/regenerate-1.4.2.tgz" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== +regenerator-runtime@^0.13.5: + version "0.13.11" + resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + regenerator-runtime@^0.14.0: version "0.14.1" resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz" @@ -7162,7 +7300,7 @@ sade@^1.7.3: dependencies: mri "^1.1.0" -safe-buffer@^5.1.0: +safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -7213,6 +7351,11 @@ schema-utils@^3.1.1, schema-utils@^3.2.0: ajv "^6.12.5" ajv-keywords "^3.5.2" +seedrandom@^3.0.5: + version "3.0.5" + resolved "https://registry.npmmirror.com/seedrandom/-/seedrandom-3.0.5.tgz" + integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg== + semver@^6.1.1, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz" @@ -7394,6 +7537,13 @@ streamsearch@^1.1.0: resolved "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== +string_decoder@^1.3.0: + version "1.3.0" + resolved "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + string-argv@^0.3.1: version "0.3.1" resolved "https://registry.npmmirror.com/string-argv/-/string-argv-0.3.1.tgz" @@ -8320,11 +8470,29 @@ yaml@^2.2.2: resolved "https://registry.npmmirror.com/yaml/-/yaml-2.3.1.tgz" integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ== +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-20.2.9.tgz" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== +yargs@^16.0.3: + version "16.2.0" + resolved "https://registry.npmmirror.com/yargs/-/yargs-16.2.0.tgz" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + yargs@^17.3.1, yargs@^17.7.2: version "17.7.2" resolved "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz"