|
@@ -0,0 +1,817 @@
|
|
|
+<template>
|
|
|
+ <view class="dialog-generation">
|
|
|
+ <view class="subject-matter-ofText" :style="{ paddingBottom: keyboardHeight + 'px' }">
|
|
|
+ <!-- 顶部导航栏 -->
|
|
|
+ <div class="navbar">
|
|
|
+ <view class="navbar-left">
|
|
|
+ <image @click="goBack()" class="back" src="../../static/vip/hy_icon_jiantou.png"></image>
|
|
|
+ <view class="elf-name">精灵·小萌</view>
|
|
|
+ <image class="deepseek" src="../../static/makedetail/deepseek-logo.png"></image>
|
|
|
+ </view>
|
|
|
+ <view class="navbar-right">
|
|
|
+ <image @click="switchToNormal()" class="primary" src="../../static/makedetail/primary.png"></image>
|
|
|
+ <image @click="newChar()" class="createChat" src="../../static/makedetail/createChat.png"></image>
|
|
|
+ </view>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- <view class="navbar-reserveASeat"> </view> -->
|
|
|
+ <!-- 聊天内容区 -->
|
|
|
+ <scroll-view class="chat-content" :scroll-into-view="toView" scroll-y
|
|
|
+ :style="{ height: stateType === 3 ? 'calc(100% - 180rpx)' : `calc(100% - ${370 + textareaHeight}rpx)` }"
|
|
|
+ @scroll="onChatScroll">
|
|
|
+ <template v-if="messages && messages.length > 0">
|
|
|
+ <!-- <scroll-view class="chat-content" scroll-y> -->
|
|
|
+ <view v-for="(msg, idx) in messages" :key="idx" :class="['chat-bubble', msg.role]">
|
|
|
+ <template v-if="msg.role === 'user'">
|
|
|
+ {{ msg.content }}
|
|
|
+ </template>
|
|
|
+ <template v-else-if="msg.role === 'ai'">
|
|
|
+ <view class="ai-bubble-row">
|
|
|
+ <view class="ai-avatar">
|
|
|
+ <image src="../../static/makedetail/characterProfilePicture.png" mode="aspectFill">
|
|
|
+ </image>
|
|
|
+ </view>
|
|
|
+ <!-- 第一句话 -->
|
|
|
+ <view v-if="msg.type == 0" class="ai-bubble-content">
|
|
|
+ <view>欢迎来到萌创星球AI写歌🎉🎉🎉!这里能满足你创作音乐🎸、释放灵感🎵,创造独属于你的音乐旋律!
|
|
|
+ </view>
|
|
|
+ <br>
|
|
|
+ <view>
|
|
|
+ 可以根据自己的喜好点击下方的选项,快速开始创作🎼,
|
|
|
+ </view>
|
|
|
+ <div @click="onCateSent('纯音乐')" class="btn-box"> 纯音乐
|
|
|
+ </div>
|
|
|
+ <div @click="onCateSent('AI生成歌词')" class="btn-box" style="margin:0 20rpx"> AI生成歌词
|
|
|
+ </div>
|
|
|
+ <div @click="onCateSent('自定义歌词')" class="btn-box"> 自定义歌词
|
|
|
+ </div>
|
|
|
+ </view>
|
|
|
+ <!-- 第二句话 -->
|
|
|
+ <view class="ai-bubble-content" v-else-if="msg.type == 1">
|
|
|
+ <text>{{ msg.content }}</text>
|
|
|
+ </view>
|
|
|
+ <!-- 第三句话 -->
|
|
|
+ <view class="ai-bubble-content" v-else-if="msg.type == 2">
|
|
|
+ <!-- <text>{{ msg.content }}</text> -->
|
|
|
+ <textarea v-model="lyricData" class="lyric-editor" auto-height
|
|
|
+ :adjust-position="false" placeholder="修改歌词..." maxlength="-1" />
|
|
|
+ <div @click="onLyricSent()" class="btn-box"> 保存歌词
|
|
|
+ </div>
|
|
|
+ </view>
|
|
|
+ <view class="ai-bubble-content" v-else-if="msg.type == 4">
|
|
|
+ <view>请输入音乐的风格</view>
|
|
|
+ <br>
|
|
|
+ <view>
|
|
|
+ 也可以根据自己的喜好点击下方的选项,快速开始创作🎼,
|
|
|
+ </view>
|
|
|
+ <div @click="openTagPopup()" class="btn-box">
|
|
|
+ 选择标签
|
|
|
+ </div>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- <view class="ai-bubble-content" v-else-if="msg.type == 1">
|
|
|
+ <template v-if="idx === lastAiIndex && isLoading">
|
|
|
+ <text>{{ displayText }}</text>
|
|
|
+ <text v-if="isLoading" class="loading-dot">...</text>
|
|
|
+ </template>
|
|
|
+<template v-else>
|
|
|
+ <text>{{ msg.content }}</text>
|
|
|
+ </template>
|
|
|
+</view> -->
|
|
|
+
|
|
|
+ <view v-else class="ai-bubble-content">
|
|
|
+ <text>OK!~小萌正在根据你的描述生成形象中</text><br>
|
|
|
+ <div v-if="msg.isStartGenerating && msg.startGeneratingId == 0" class="btn-box">
|
|
|
+ 点击查看
|
|
|
+ ({{ countdown }}) s</div>
|
|
|
+ <div v-else
|
|
|
+ @click="goPage(`/pages/makedetail/makeMusicDetail?id=${msg.startGeneratingId}`)"
|
|
|
+ class="btn-box"> 点击查看
|
|
|
+ </div>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </template>
|
|
|
+ </view>
|
|
|
+ <view id="bottom-anchor"></view>
|
|
|
+ <view v-if="error" class="chat-bubble ai error">{{ error }}</view>
|
|
|
+ <view style="height: 200rpx; width:100vw;"></view>
|
|
|
+ <image v-if="showToBottomBtn && keyboardHeight === 0" class="to-bottom-btn" @click="scrollToBottom"
|
|
|
+ src="../../static/makedetail/toBottomBtn.png"></image>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <view class="chat-content-empty">
|
|
|
+ <image src="../../static/makedetail/characterProfilePicture.png" mode="aspectFill"></image>
|
|
|
+ <view class="chat-content-empty-title">嗨!我是创梦精灵</view>
|
|
|
+ <view class="chat-content-empty-desc">与我聊聊你想要生成的角色吧!</view>
|
|
|
+ </view>
|
|
|
+ </template>
|
|
|
+ </scroll-view>
|
|
|
+ <!-- <view class="bom-reserveASeat"></view> -->
|
|
|
+
|
|
|
+ <view v-if="stateType !== 3" class="bom-box"
|
|
|
+ :style="{ bottom: 0 + 'px', height: `${190 + textareaHeight}rpx` }">
|
|
|
+ <view class="bom-box-bg">
|
|
|
+ <c-lottie ref="cLottieRef" :src='"/static/lottie/xiaomeng.json"' class="icon-img" height="108rpx"
|
|
|
+ width="112rpx" :loop="true" :autoPlay="false"></c-lottie>
|
|
|
+
|
|
|
+ </view>
|
|
|
+ <!-- 底部输入区 -->
|
|
|
+ <view class="input-bar">
|
|
|
+ <!-- <image class="icon-img" src="../../static/makedetail/characterProfilePicture.png" mode="aspectFill">
|
|
|
+ </image> -->
|
|
|
+
|
|
|
+
|
|
|
+ <textarea :autoHeight="true" class="input-box" :adjust-position="false" v-model="question"
|
|
|
+ :disabled="isLoading" placeholder="给我发送消息吧..." maxlength="400" @input="onTextareaInput"
|
|
|
+ confirm-type="search" @confirm="onSend" rows="1"
|
|
|
+ style="overflow-y:auto;max-height:176rpx;min-height:44rpx;" />
|
|
|
+ <!-- <button class="send-btn" :disabled="isLoading || !question.trim()" @click="onSend">{{ isLoading ?
|
|
|
+ '生成中...' :
|
|
|
+ '发送' }}</button> -->
|
|
|
+ <image v-if="isLoading" class="stop" src="../../static/makedetail/stop.png" mode=""
|
|
|
+ @click="stopStreamAnswer" />
|
|
|
+ <image v-else-if="!isLoading && !question.trim() && keyboardHeight === 0" class="keyboard"
|
|
|
+ src="../../static/makedetail/keyboard.png" mode="" />
|
|
|
+
|
|
|
+ <image v-else-if="!isLoading && question.trim() && keyboardHeight !== 0" class="send"
|
|
|
+ src="../../static/makedetail/send.png" mode="" @click="onSend" />
|
|
|
+ </view>
|
|
|
+ <!-- 底部提示 -->
|
|
|
+ <view class="footer-tip">内容由AI生成,禁用相关功能请联系管理员。</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <DialogBox ref="DialogBox"></DialogBox>
|
|
|
+ <multi-select-popup ref="tagPopup" :initial-options="tagOptions" @selection-confirmed="handleTagSelection" @popup-closed="handlePopupClosed"></multi-select-popup>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+<script>
|
|
|
+import websocket from '@/common/websocket.js';
|
|
|
+import MultiSelectPopup from '@/components/MultiSelectPopup/MultiSelectPopup.vue';
|
|
|
+
|
|
|
+export default {
|
|
|
+ components: {
|
|
|
+ MultiSelectPopup
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ question: '',
|
|
|
+ answer: '',
|
|
|
+ displayText: '',
|
|
|
+ isComplete: false,
|
|
|
+ isLoading: false,
|
|
|
+ loadingText: '正在生成歌词,请稍候',
|
|
|
+ loadingDots: '...',
|
|
|
+ loadingTimer: null,
|
|
|
+ error: null,
|
|
|
+ retryCount: 0,
|
|
|
+ maxRetries: 3,
|
|
|
+ timer: null,
|
|
|
+ lastResponseTime: 0,
|
|
|
+ timeout: 30000,
|
|
|
+ typingSpeed: 50,
|
|
|
+ messages: [],
|
|
|
+ lastAiIndex: -1,
|
|
|
+ keyboardHeight: 0,
|
|
|
+ statusBarHeight: 0,
|
|
|
+ windowBottom: 0,
|
|
|
+ toView: '',
|
|
|
+ showToBottomBtn: false,
|
|
|
+ textareaHeight: 0,
|
|
|
+ isConnected: false,
|
|
|
+ isStartGenerating: false,
|
|
|
+ countdown: 0,
|
|
|
+ stateType: 1,
|
|
|
+ stateTypeArray: ['clear', 'cate', 'getLyrics', 'setLyrics', 'getTags', 'setTags', 'setContent','','','','','setContent'],
|
|
|
+ content: '',
|
|
|
+ musicGenre: '',
|
|
|
+ lyricData: '',
|
|
|
+ tagsData: '',
|
|
|
+ tagOptions: [], // 用于存储多选弹窗的选项
|
|
|
+ // type = clear 清除|cate 第一句话选择 |getLyrics 获取歌词(需要上传描述)|setLyrics 修改歌词(再去判断是否有* 有重复没有下一步 没有展示标签)|setTags(修改标签) (下发一个static 失败有提示 成功就正常生成)|getTags 获取标签
|
|
|
+ };
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ // /Work/streamAnswerMusicLast
|
|
|
+ // /Work/streamAnswerMusic
|
|
|
+ // {type:'',content:''}
|
|
|
+ // type = clear 清除|cate 第一句话选择 |getLyrics 获取歌词(需要上传描述)|setLyrics 修改歌词(再去判断是否有* 有重复没有下一步 没有展示标签)|setTags(修改标签) (下发一个static 失败有提示 成功就正常生成)|getTags 获取标签
|
|
|
+
|
|
|
+ scrollToBottom() {
|
|
|
+ this.toView = '';
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.toView = 'bottom-anchor';
|
|
|
+ });
|
|
|
+ },
|
|
|
+ async initWebSocket() {
|
|
|
+ if (this.isConnected) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 发送初始化消息
|
|
|
+ websocket.onOpen(() => {
|
|
|
+ setTimeout(() => {
|
|
|
+ websocket.send(JSON.stringify({ type: 'getTags', content: '' }));
|
|
|
+ })
|
|
|
+ },1000);
|
|
|
+
|
|
|
+ await websocket.connect('wss://e.zhichao.art/Gapi/Work/streamAnswerMusic', {
|
|
|
+ uuid: getApp().globalData.uuid
|
|
|
+ });
|
|
|
+
|
|
|
+ this.isConnected = true;
|
|
|
+ // 设置消息处理回调
|
|
|
+ websocket.onMessage((data) => {
|
|
|
+ console.log("data:", data);
|
|
|
+ if (data && data.includes('{')) {
|
|
|
+ var data = JSON.parse(data);
|
|
|
+ }
|
|
|
+ var type = data && data.type || '';
|
|
|
+ var content = data && data.content || '';
|
|
|
+
|
|
|
+ // type = error,
|
|
|
+ // content=no_cate(首次分类没有设置)
|
|
|
+ // content="歌词不合规,请重新输入"|"歌词中有违规禁词,请修改!"
|
|
|
+ // type='lyrics',content=具体歌词
|
|
|
+ // type=success,content='OKOKOK':提交成功
|
|
|
+ // type=result,content=ID
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ // 111111111111111111111111111111111
|
|
|
+ // if (type === 'success' && content === 'OKOKOK') {
|
|
|
+ // // 音乐生成开始,AI消息加载中提示
|
|
|
+ // const aiMsg = this.messages[this.lastAiIndex];
|
|
|
+ // if (aiMsg) {
|
|
|
+ // aiMsg.isStartGenerating = true;
|
|
|
+ // aiMsg.content = '音乐生成中,请稍候...';
|
|
|
+ // }
|
|
|
+ // this.isStartGenerating = true;
|
|
|
+ // this.countdownFun(20);
|
|
|
+ // }
|
|
|
+ // if (type === 'result' && content) {
|
|
|
+ // // 音乐生成中,显示跳转到音乐详情页按钮
|
|
|
+ // const aiMsg = this.messages[this.lastAiIndex];
|
|
|
+ // if (aiMsg) {
|
|
|
+ // aiMsg.isStartGenerating = false;
|
|
|
+ // aiMsg.startGeneratingId = content;
|
|
|
+ // aiMsg.content = '音乐已生成,点击跳转到详情页';
|
|
|
+ // }
|
|
|
+ // }
|
|
|
+ if (this.isLoading) {
|
|
|
+ if (type == 'cate' && content == 'success') {
|
|
|
+ // {"type":"setContent","content":content}
|
|
|
+ // 此时 第第一步选择完成
|
|
|
+ if (this.musicGenre == '纯音乐') {
|
|
|
+ // 跳过获取 修改 歌词
|
|
|
+ this.stateType = 11;
|
|
|
+ this.messages.push({ role: 'ai', type: 1, content: '输入描述生成音乐' });
|
|
|
+ } else {
|
|
|
+ this.stateType = 2;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.musicGenre == 'AI生成歌词') {
|
|
|
+ this.messages.push({ role: 'ai', type: 1, content: '请描述歌词' });
|
|
|
+ this.stateType = 2;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log(type, content);
|
|
|
+
|
|
|
+ // {"type":"lyrics","content":"《古风之约》\n\n青山绿水间 桃花映人面\n清风拂衣袖 相思绕指尖\n亭台楼阁畔 琴声悠扬传\n往事如烟云 飘散在天边\n\n红墙绿瓦下 谁在等归雁\n明月照窗前 孤影难入眠\n一纸素笺上 写满了思念\n岁月如流水 匆匆又一年\n\n我与你共赴 这一场古风之约\n看那春花秋月 浪漫又缠绵\n执手相看泪眼 无语凝噎\n只愿与你相伴 直到永远\n\n我与你共赴 这一场古风之约\n听那琵琶弦上 倾诉着哀怨\n举杯对饮成三人 醉在花间\n只愿与你相守 岁岁年年"}
|
|
|
+ }
|
|
|
+ // if (data.includes('[DONE]')) {
|
|
|
+ // // 结束生成
|
|
|
+ // this.completeAnswer();
|
|
|
+ // } else if (data.includes('ID:')) {
|
|
|
+ // // 提取ID
|
|
|
+ // const aiMsg = this.messages[this.lastAiIndex];
|
|
|
+ // if (aiMsg) {
|
|
|
+ // aiMsg.startGeneratingId = data.split(':')[1];
|
|
|
+ // }
|
|
|
+ // } else if (data.includes('OKOKOK')) {
|
|
|
+ // const aiMsg = this.messages[this.lastAiIndex];
|
|
|
+ // if (aiMsg) {
|
|
|
+ // aiMsg.isStartGenerating = true;
|
|
|
+ // }
|
|
|
+ // this.isStartGenerating = true;
|
|
|
+ // this.countdownFun(20);
|
|
|
+ // } else {
|
|
|
+ // const aiMsg = this.messages[this.lastAiIndex];
|
|
|
+ // if (aiMsg) {
|
|
|
+ // aiMsg.isStartGenerating = false;
|
|
|
+ // this.displayText += data;
|
|
|
+ // this.answer += data;
|
|
|
+ // aiMsg.content += data;
|
|
|
+ // this.retryCount = 0;
|
|
|
+ // this.lastResponseTime = Date.now();
|
|
|
+ // this.scrollToBottom();
|
|
|
+ // }
|
|
|
+ // }
|
|
|
+ this.isLoading = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (type == 'lyrics' && content) {
|
|
|
+ console.log('获取到歌词', content);
|
|
|
+ // 替换加载中的消息
|
|
|
+ const lastMessageIndex = this.messages.length -1;
|
|
|
+ // 替换加载中的消息
|
|
|
+ this.stopLoadingAnimation();
|
|
|
+ // Find and remove the loading message
|
|
|
+ for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
|
+ if (this.messages[i].isGeneratingLyrics) {
|
|
|
+ this.messages.splice(i, 1);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.messages.push({ role: 'ai', type: 2, content: content });
|
|
|
+ this.stateType = 3;
|
|
|
+ this.lyricData = content;
|
|
|
+ console.log(this.messages, 24);
|
|
|
+
|
|
|
+ }
|
|
|
+ if (type == 'tags' && content ) {
|
|
|
+ // 此时歌词合法 下一步获取标签
|
|
|
+ this.stateType = 4;
|
|
|
+ this.messages.push({ role: 'ai', type: 4, content: '请输入标签' });
|
|
|
+ }
|
|
|
+ if(type == 'getTags' && content && !this.tagsData ){ //确保只处理一次
|
|
|
+ // 获取标签成功
|
|
|
+ this.tagsData = JSON.parse(content);
|
|
|
+ console.log('获取到标签', this.tagsData);
|
|
|
+ // 将获取到的标签数据转换为 MultiSelectPopup 需要的格式
|
|
|
+ this.tagOptions = this.formatTagOptions(this.tagsData);
|
|
|
+ }
|
|
|
+ this.scrollToBottom();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 设置错误处理回调
|
|
|
+ websocket.onError((error) => {
|
|
|
+ console.error('WebSocket错误:', error);
|
|
|
+ this.isConnected = false;
|
|
|
+ this.handleError(error);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 设置关闭处理回调
|
|
|
+ websocket.onClose(() => {
|
|
|
+ console.log('WebSocket已关闭');
|
|
|
+ this.isConnected = false;
|
|
|
+ this.isLoading = false;
|
|
|
+ });
|
|
|
+
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('WebSocket初始化失败:', error);
|
|
|
+ this.isConnected = false;
|
|
|
+ this.handleError(error);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async startStreamAnswer(content) {
|
|
|
+ if (!content.trim()) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '请输入问题',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查连接状态,如果断开则重连
|
|
|
+ if (!this.isConnected) {
|
|
|
+ try {
|
|
|
+ await this.initWebSocket();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('重连失败:', error);
|
|
|
+ uni.showToast({
|
|
|
+ title: '连接已断开,请重试',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ console.log('发送消息:', content);
|
|
|
+
|
|
|
+ this.resetState();
|
|
|
+ this.messages.push({ role: 'user', content });
|
|
|
+
|
|
|
+ try {
|
|
|
+ this.isLoading = true;
|
|
|
+
|
|
|
+ if (false) {
|
|
|
+ let aiMsg = {
|
|
|
+ role: 'ai',
|
|
|
+ content: '',
|
|
|
+ isStartGenerating: false,
|
|
|
+ startGeneratingId: 0
|
|
|
+ };
|
|
|
+ this.messages.push(aiMsg);
|
|
|
+ this.lastAiIndex = this.messages.length - 1;
|
|
|
+ this.content = content;
|
|
|
+ }
|
|
|
+ // 发送消息
|
|
|
+ const messageType = this.stateTypeArray[this.stateType];
|
|
|
+
|
|
|
+ let messageToSend = { type: messageType, content: content };
|
|
|
+
|
|
|
+ // 特殊处理 stateType 11 (纯音乐描述)
|
|
|
+ if (this.stateType === 11) {
|
|
|
+ messageToSend = { type: 'setContent', content: content };
|
|
|
+ }
|
|
|
+
|
|
|
+ websocket.send(JSON.stringify(messageToSend));
|
|
|
+
|
|
|
+ // 如果是获取歌词,则显示加载中
|
|
|
+ if (messageType === 'getLyrics') {
|
|
|
+ this.messages.push({ role: 'ai', type: 1, content: this.loadingText + this.loadingDots, isGeneratingLyrics: true });
|
|
|
+ this.startLoadingAnimation();
|
|
|
+ this.scrollToBottom();
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('发送消息失败:', error);
|
|
|
+ this.handleError(error);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onSend() {
|
|
|
+ if (!this.question.trim() || this.isLoading) return;
|
|
|
+
|
|
|
+ if (this.stateType === 4) { // 当前状态是等待输入标签
|
|
|
+ const userTagInput = this.question.trim();
|
|
|
+ this.messages.push({ role: 'user', content: userTagInput });
|
|
|
+ websocket.send(JSON.stringify({ type: 'setTags', content: userTagInput }));
|
|
|
+ this.messages.push({ role: 'ai', type: 1, content: `好的,已选择标签:${userTagInput},小萌开始生成音乐啦!` });
|
|
|
+ this.question = '';
|
|
|
+ this.scrollToBottom();
|
|
|
+ return; // 阻止后续的 startStreamAnswer 调用
|
|
|
+ }
|
|
|
+
|
|
|
+ this.startStreamAnswer(this.question);
|
|
|
+ this.question = '';
|
|
|
+ },
|
|
|
+ // 发送第一步指令
|
|
|
+ onCateSent(content) {
|
|
|
+ if (this.isLoading || this.musicGenre) return;
|
|
|
+ this.startStreamAnswer(content);
|
|
|
+ this.musicGenre = content;
|
|
|
+ },
|
|
|
+ // 发送歌词逻辑
|
|
|
+ onLyricSent() {
|
|
|
+ if (this.isLoading) return;
|
|
|
+ if (this.lyricData.includes('*')) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '歌词有"*",请修改后再保存',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ this.startStreamAnswer(this.lyricData);
|
|
|
+ },
|
|
|
+ // 打开标签选择弹窗
|
|
|
+ openTagPopup() {
|
|
|
+ this.$refs.tagPopup.openPopup();
|
|
|
+ },
|
|
|
+ formatTagOptions(tagsData) {
|
|
|
+ console.log('格式化标签选项', tagsData);
|
|
|
+ // tagsData 的预期格式:
|
|
|
+ // [
|
|
|
+ // { "name": "情感", "children": [ { "name": "欢快", "children": [] }, ... ] },
|
|
|
+ // ...
|
|
|
+ // ]
|
|
|
+ // 需要转换为 MultiSelectPopup 需要的格式:
|
|
|
+ // [
|
|
|
+ // { label: '情感', expanded: true, children: [{ label: '欢快', value: '欢快', selected: false }, ...] },
|
|
|
+ // ...
|
|
|
+ // ]
|
|
|
+ if (!Array.isArray(tagsData)) return [];
|
|
|
+ return tagsData.map(parentTag => ({
|
|
|
+ label: parentTag.name,
|
|
|
+ expanded: true, // 默认展开父选项
|
|
|
+ children: parentTag.children.map(childTag => ({
|
|
|
+ label: childTag.name,
|
|
|
+ value: childTag.name, // 通常value和label相同,或根据实际情况设置
|
|
|
+ selected: false
|
|
|
+ }))
|
|
|
+ }));
|
|
|
+ },
|
|
|
+ handleTagSelection(selectedValues) {
|
|
|
+ console.log('选中的标签:', selectedValues);
|
|
|
+ // 处理选中的标签,例如发送到后端
|
|
|
+ // 示例:将选中的标签数组转换为字符串发送
|
|
|
+ const tagsString = selectedValues.join(',');
|
|
|
+ websocket.send(JSON.stringify({ type: 'setTags', content: tagsString }));
|
|
|
+ // 可以在这里添加AI回复,告知用户标签已选择,正在生成音乐等
|
|
|
+ this.messages.push({ role: 'ai', type: 1, content: `好的,已选择标签:${tagsString},小萌开始生成音乐啦!` });
|
|
|
+ this.scrollToBottom();
|
|
|
+ },
|
|
|
+ handlePopupClosed() {
|
|
|
+ // 如果用户关闭了弹窗但没有选择任何标签,可以提供一个默认行为或提示
|
|
|
+ // 例如,如果没有选择标签,可以发送一个空数组或特定指令
|
|
|
+
|
|
|
+ },
|
|
|
+ startLoadingAnimation() {
|
|
|
+ let dotCount = 1;
|
|
|
+ this.loadingTimer = setInterval(() => {
|
|
|
+ dotCount++;
|
|
|
+ if (dotCount > 3) {
|
|
|
+ dotCount = 1;
|
|
|
+ }
|
|
|
+ this.loadingDots = '.'.repeat(dotCount);
|
|
|
+ // Update the loading message content if it exists
|
|
|
+ const loadingMessage = this.messages.find(msg => msg.isGeneratingLyrics);
|
|
|
+ if (loadingMessage) {
|
|
|
+ loadingMessage.content = this.loadingText + this.loadingDots;
|
|
|
+ }
|
|
|
+ }, 500); // 更新频率,例如每500毫秒
|
|
|
+ },
|
|
|
+ stopLoadingAnimation() {
|
|
|
+ if (this.loadingTimer) {
|
|
|
+ clearInterval(this.loadingTimer);
|
|
|
+ this.loadingTimer = null;
|
|
|
+ }
|
|
|
+ this.loadingDots = '...'; // Reset to default
|
|
|
+ },
|
|
|
+ resetState() {
|
|
|
+ this.answer = '';
|
|
|
+ this.displayText = '';
|
|
|
+ this.isComplete = false;
|
|
|
+ this.error = null;
|
|
|
+ this.retryCount = 0;
|
|
|
+ this.lastResponseTime = Date.now();
|
|
|
+ },
|
|
|
+ handleError(error) {
|
|
|
+ if (this.retryCount < this.maxRetries) {
|
|
|
+ this.retryCount++;
|
|
|
+ this.retryRequest();
|
|
|
+ } else {
|
|
|
+ this.error = '请求失败,请稍后重试';
|
|
|
+ this.isLoading = false;
|
|
|
+ this.stopLoadingAnimation();
|
|
|
+ uni.showToast({
|
|
|
+ title: this.error,
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ },
|
|
|
+ retryRequest() {
|
|
|
+ uni.showToast({
|
|
|
+ title: `正在重试 (${this.retryCount}/${this.maxRetries})`,
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ setTimeout(() => {
|
|
|
+ this.startStreamAnswer(this.question);
|
|
|
+ }, 1000 * this.retryCount);
|
|
|
+ },
|
|
|
+ completeAnswer() {
|
|
|
+ this.isComplete = true;
|
|
|
+ this.isLoading = false;
|
|
|
+ this.stopLoadingAnimation();
|
|
|
+ },
|
|
|
+ stopStreamAnswer() {
|
|
|
+ if (this.timer) {
|
|
|
+ clearTimeout(this.timer);
|
|
|
+ this.timer = null;
|
|
|
+ }
|
|
|
+ this.isLoading = false;
|
|
|
+ this.stopLoadingAnimation();
|
|
|
+ },
|
|
|
+ checkTimeout() {
|
|
|
+ if (Date.now() - this.lastResponseTime > this.timeout) {
|
|
|
+ this.handleError(new Error('请求超时'));
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ goBack() {
|
|
|
+ uni.navigateBack({
|
|
|
+ delta: 1
|
|
|
+ });
|
|
|
+ },
|
|
|
+ onChatScroll(e) {
|
|
|
+ const threshold = 600; // 离底部1.0417rem以内不显示按钮
|
|
|
+ const {
|
|
|
+ scrollHeight,
|
|
|
+ scrollTop
|
|
|
+ } = e.detail;
|
|
|
+ const clientHeight = e.detail.clientHeight || e.detail.height || 0;
|
|
|
+ if (scrollHeight - scrollTop - clientHeight > threshold) {
|
|
|
+ this.showToBottomBtn = true;
|
|
|
+ } else {
|
|
|
+ this.showToBottomBtn = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onTextareaInput(e) {
|
|
|
+ console.log(e.detail);
|
|
|
+
|
|
|
+ // 获取textarea的实际高度
|
|
|
+ const query = uni.createSelectorQuery().in(this);
|
|
|
+ query.select('.input-box').boundingClientRect(data => {
|
|
|
+ if (data) {
|
|
|
+ // 将px转换为rpx (假设设计稿是750rpx宽度)
|
|
|
+ const height = (data.height * 750) / uni.getSystemInfoSync().windowWidth;
|
|
|
+ // 减去基础高度90rpx,得到额外增加的高度
|
|
|
+ this.textareaHeight = Math.max(0, height - 90);
|
|
|
+ // 滚动到底部
|
|
|
+ this.scrollToBottom();
|
|
|
+ }
|
|
|
+ }).exec();
|
|
|
+ },
|
|
|
+ retrieveHistoricalRecords() {
|
|
|
+ uni.request({
|
|
|
+ url: this.$apiHost + '/Work/streamAnswerMusicLast',
|
|
|
+ method: 'GET',
|
|
|
+ header: {
|
|
|
+ 'content-type': 'application/json',
|
|
|
+ 'sign': getApp().globalData.headerSign
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ uuid: getApp().globalData.uuid,
|
|
|
+ task_type: 2
|
|
|
+ },
|
|
|
+ success: (res) => {
|
|
|
+ // step 0 未开始 1 已选择过第一次的选项 2 获取到歌词
|
|
|
+ // cate 选择类型 lyrics 历史记录
|
|
|
+
|
|
|
+ console.log("获取历史记录:", res.data);
|
|
|
+ if (res.data.success === "yes") {
|
|
|
+ this.messages = [];
|
|
|
+ this.musicGenre = res.data.cate;
|
|
|
+
|
|
|
+ // 未开始
|
|
|
+ this.messages.push({ role: 'ai', type: 0, content: ' ' });
|
|
|
+
|
|
|
+ if (res.data.step >= 1) {
|
|
|
+ // 获取到歌词
|
|
|
+ this.messages.push({ role: 'user', content: res.data.cate });
|
|
|
+
|
|
|
+ }
|
|
|
+ if (res.data.cate == 'AI生成歌词') {
|
|
|
+ this.messages.push({ role: 'ai', type: 1, content: '请描述歌词' });
|
|
|
+ this.stateType = 2;
|
|
|
+ }
|
|
|
+ if (res.data.content) {
|
|
|
+ this.messages.push({ role: 'user', type: 1, content: res.data.content });
|
|
|
+ }
|
|
|
+ // AI生成歌词逻辑
|
|
|
+ if (res.data.lyrics) {
|
|
|
+ this.messages.push({ role: 'ai', type: 2, content: res.data.lyrics });
|
|
|
+ this.stateType = 3;
|
|
|
+ this.lyricData = res.data.lyrics;
|
|
|
+ }
|
|
|
+
|
|
|
+ } else {
|
|
|
+ this.messages = []
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log("获取历史记录:", this.messages);
|
|
|
+
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.log('获取历史记录失败', err);
|
|
|
+ uni.showToast({
|
|
|
+ title: '获取历史记录失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ // 重新计算元素高度
|
|
|
+ recalculateHeights() {
|
|
|
+ // 重新计算textarea高度
|
|
|
+ const query = uni.createSelectorQuery().in(this);
|
|
|
+ query.select('.input-box').boundingClientRect(data => {
|
|
|
+ if (data) {
|
|
|
+ const height = (data.height * 750) / uni.getSystemInfoSync().windowWidth;
|
|
|
+ this.textareaHeight = Math.max(0, height - 90);
|
|
|
+ }
|
|
|
+ }).exec();
|
|
|
+ },
|
|
|
+ countdownFun(n) {
|
|
|
+ if (this.timer) {
|
|
|
+ clearInterval(this.timer);
|
|
|
+ }
|
|
|
+ this.countdown = n;
|
|
|
+ // 倒计时
|
|
|
+ this.timer = setInterval(() => {
|
|
|
+ this.countdown--;
|
|
|
+ if (this.countdown <= 0) {
|
|
|
+ clearInterval(this.timer);
|
|
|
+ }
|
|
|
+ }, 1000);
|
|
|
+ },
|
|
|
+ goPage(page) {
|
|
|
+ uni.navigateTo({
|
|
|
+ url: page,
|
|
|
+ });
|
|
|
+ },
|
|
|
+ newChar() {
|
|
|
+ this.$refs["DialogBox"]
|
|
|
+ .confirm({
|
|
|
+ title: "是否创建新对话",
|
|
|
+ content: "立即结束当前对话内容,开启新的对话?",
|
|
|
+ DialogType: "inquiry",
|
|
|
+ btn1: "取消",
|
|
|
+ btn2: "确定",
|
|
|
+ animation: 0,
|
|
|
+ })
|
|
|
+ .then(() => {
|
|
|
+ websocket.send(JSON.stringify({ type: 'clear', content: '' }));
|
|
|
+ this.retrieveHistoricalRecords()
|
|
|
+ });
|
|
|
+
|
|
|
+ },
|
|
|
+ switchToNormal() {
|
|
|
+ this.$refs["DialogBox"]
|
|
|
+ .confirm({
|
|
|
+ title: "是否切回常规模式",
|
|
|
+ content: "切换至普通常规模式进行创作?",
|
|
|
+ DialogType: "inquiry",
|
|
|
+ btn1: "取消",
|
|
|
+ btn2: "确定",
|
|
|
+ animation: 0,
|
|
|
+ })
|
|
|
+ .then(() => {
|
|
|
+ this.goPage('/pages/makedetail/makeMusicDetail')
|
|
|
+ });
|
|
|
+
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async created() {
|
|
|
+ await this.initWebSocket();
|
|
|
+ this.retrieveHistoricalRecords();
|
|
|
+ this.timer = setInterval(() => {
|
|
|
+ if (this.isLoading) {
|
|
|
+ this.checkTimeout();
|
|
|
+ }
|
|
|
+ }, 1000);
|
|
|
+
|
|
|
+ uni.onKeyboardHeightChange(res => {
|
|
|
+ this.keyboardHeight = res.height;
|
|
|
+ if (res.height === 0) {
|
|
|
+ this.$refs.cLottieRef.call('stop')
|
|
|
+ } else {
|
|
|
+ this.$refs.cLottieRef.call('play')
|
|
|
+
|
|
|
+ }
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.recalculateHeights();
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ const systemInfo = uni.getSystemInfoSync();
|
|
|
+ this.statusBarHeight = systemInfo.statusBarHeight;
|
|
|
+ this.windowBottom = systemInfo.safeAreaInsets ? systemInfo.safeAreaInsets.bottom : 0;
|
|
|
+ },
|
|
|
+
|
|
|
+ beforeDestroy() {
|
|
|
+ websocket.close();
|
|
|
+ this.stopStreamAnswer();
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss">
|
|
|
+@import './intelligentLifeChart.scss';
|
|
|
+
|
|
|
+.lyric-editor {
|
|
|
+ width: 100%;
|
|
|
+ background-color: transparent;
|
|
|
+ border: none;
|
|
|
+ padding: 0;
|
|
|
+ margin-top: 10rpx;
|
|
|
+ color: rgba(255, 255, 255, 0.7);
|
|
|
+ font-size: 32rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.to-bottom-btn {
|
|
|
+ position: fixed;
|
|
|
+ right: 50%;
|
|
|
+ transform: translateX(50%);
|
|
|
+ bottom: 20rpx;
|
|
|
+ width: 60rpx;
|
|
|
+ height: 60rpx;
|
|
|
+ bottom: 220rpx;
|
|
|
+ z-index: 999;
|
|
|
+ opacity: .75;
|
|
|
+ transition: opacity 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.ai-bubble-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+}
|
|
|
+
|
|
|
+.ai-avatar {
|
|
|
+ width: 60rpx;
|
|
|
+ height: 60rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ margin-right: 20rpx;
|
|
|
+ border: solid 2rpx rgb(238, 238, 238, .3);
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-end;
|
|
|
+ justify-content: center;
|
|
|
+
|
|
|
+ image {
|
|
|
+ width: 52rpx;
|
|
|
+ height: 52rpx;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.ai-bubble-content {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+</style>
|