gy-app-shop/pages/fun/AiDrama.vue

634 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="chat-container">
<!-- 聊天内容区域 -->
<scroll-view
class="chat-content"
scroll-y
:scroll-top="scrollTop"
:scroll-with-animation="true"
@scrolltoupper="loadMoreMessages"
:refresher-enabled="false"
ref="scrollView"
>
<view class="message-wrapper-box" >
<view v-for="(message, index) in messages" :key="index" class="message-wrapper">
<!-- 系统消息或AI消息 -->
<block v-if="message.role === 'system' || message.role === 'assistant'">
<view class="message-container ai-message-container">
<view class="message ai-message">
<!-- 使用渲染后的Markdown内容 -->
<rich-text class="message-content markdown-body" :nodes="message.renderedContent"></rich-text>
</view>
<text class="message-time">{{ message.time }}</text>
</view>
<!-- 在每条AI消息后显示确认按钮除了系统消息 -->
<view v-if="message.role === 'assistant' && message.endStatus" class="accept-button-container">
<u-button
type="success"
size="mini"
@click="acceptScript(message.content)"
:custom-style="{marginTop: '10px'}"
>
<u-icon name="checkmark" size="14" class="button-icon"></u-icon>
确认使用此剧本
</u-button>
</view>
</block>
<!-- 用户消息 -->
<block v-else-if="message.role === 'user'">
<view class="message-container user-message-container">
<view class="message user-message">
<text class="message-content">{{ message.content }}</text>
</view>
<text class="message-time">{{ message.time }}</text>
</view>
</block>
</view>
<!-- 加载中提示 -->
<view v-if="loading" class="loading-container">
<u-loading mode="circle" size="24"></u-loading>
<text class="loading-text">正在生成剧本...</text>
</view>
<!-- 用于滚动到底部的空白元素 -->
<view id="scroll-bottom-anchor" style="height: 1px;"></view>
<!-- 底部占位,确保内容不被输入框遮挡 -->
<view class="bottom-placeholder"></view>
</view>
</scroll-view>
<!-- 输入区域 - 固定在底部 -->
<view class="chat-input-container">
<view class="chat-input">
<u-input
v-model="inputMessage"
placeholder="请输入剧本相关描述AI将为您生成专业剧本..."
:disabled="loading"
type='textarea'
:auto-height="true"
:custom-style="{flex: 1, minHeight: '80rpx', maxHeight: '200rpx'}"
@confirm="sendMessage"
></u-input>
<u-button
type="primary"
:disabled="loading || !inputMessage.trim()"
@click="sendMessage"
:custom-style="{marginLeft: '10px'}"
>
发送
</u-button>
</view>
</view>
</view>
</template>
<script>
import { renderMarkdown } from './markdown.js';
export default {
data() {
return {
// 聊天消息列表
messages: [
{
role: 'system',
content: '你好我是你的AI剧本助手。请告诉我你想创作的剧本类型和主要情节我会为你生成专业的剧本内容。',
renderedContent: '',
time: this.formatTime(new Date()),
endStatus: true
}
],
// 输入框内容
inputMessage: '',
// 加载状态
loading: false,
// 滚动位置
scrollTop: 0,
// URL参数
orderId: '',
// API密钥
apiKey: 'sk-uobtjwyzastkxcnrrbllxpftqxdumkafnitdqqhoermmioru',
// 当前AI消息索引
currentAiMessageIndex: -1,
// 是否是H5环境
isH5: false,
// 是否需要滚动到底部
needScrollToBottom: true,
// 滚动计时器
scrollTimer: null
};
},
onLoad(options) {
// 从URL获取参数
this.orderId = options.orderId || '';
// 检测是否是H5环境
// #ifdef H5
this.isH5 = true;
// #endif
// 初始化
this.initPage();
// 渲染初始消息的Markdown内容
this.renderAllMarkdown();
},
onReady() {
// 页面加载完成后滚动到底部
this.throttledScrollToBottom();
},
onUnload() {
// 清除滚动计时器
if (this.scrollTimer) {
clearTimeout(this.scrollTimer);
this.scrollTimer = null;
}
},
watch: {
// 监听消息变化,自动滚动到底部
messages: {
deep: true,
handler() {
if (this.needScrollToBottom) {
this.throttledScrollToBottom();
}
}
}
},
methods: {
// 初始化页面
initPage() {
// 设置导航栏标题
uni.setNavigationBarTitle({
title: 'AI 剧本助手'
});
},
// 渲染所有消息的Markdown内容
renderAllMarkdown() {
this.messages.forEach((message, index) => {
if (message.role === 'system' || message.role === 'assistant') {
this.messages[index].renderedContent = renderMarkdown(message.content);
}
});
},
// 格式化时间为 HH:MM 格式
formatTime(date) {
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
},
// 发送消息
async sendMessage() {
if (!this.inputMessage.trim() || this.loading) return;
// 添加用户消息
const userMessage = {
role: 'user',
content: this.inputMessage,
time: this.formatTime(new Date())
};
this.messages.push(userMessage);
// 清空输入框
const userInput = this.inputMessage;
this.inputMessage = '';
// 设置加载状态
this.loading = true;
// 滚动到底部
this.throttledScrollToBottom();
try {
// 准备发送给API的消息
const apiMessages = [
{
role: 'system',
content: '你好我是你的AI剧本助手。请告诉我你想创作的剧本类型和主要情节我会为你生成专业的剧本内容。请使用Markdown格式来组织你的回答可以使用标题、列表、粗体、斜体等Markdown语法来增强可读性。'
},
...this.messages
.filter(msg => msg.role !== 'system')
.map(msg => ({
role: msg.role,
content: msg.content
}))
];
// 在H5环境中使用fetch API进行流式请求
if (this.isH5) {
await this.fetchStreamResponse(apiMessages);
} else {
// 在非H5环境中使用普通请求
await this.fetchNormalResponse(apiMessages);
}
} catch (error) {
console.error('API请求失败:', error);
uni.showToast({
title: '生成剧本失败,请稍后重试',
icon: 'none'
});
} finally {
this.loading = false;
// 滚动到底部
this.throttledScrollToBottom();
}
},
// 使用fetch API进行流式请求仅H5环境
async fetchStreamResponse(apiMessages) {
try {
// 添加一个空的AI回复用于流式更新
this.currentAiMessageIndex = this.messages.length;
this.messages.push({
role: 'assistant',
content: '',
renderedContent: '',
time: this.formatTime(new Date()),
endStatus: false
});
const response = await fetch('https://api.siliconflow.cn/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'deepseek-ai/DeepSeek-R1-Distill-Qwen-7B',
messages: apiMessages,
stream: true,
max_tokens: 8192,
stop: '',
temperature: 0.6,
top_p: 0.7,
top_k: 50,
frequency_penalty: 0
})
});
if (!response.ok) {
throw new Error(`API请求失败: ${response.status}`);
}
if (!response.body) {
throw new Error('响应体为空');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let result = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.substring(6).trim();
if (data === '[DONE]') {
// 设置消息已完成状态
if (this.currentAiMessageIndex >= 0 && this.currentAiMessageIndex < this.messages.length) {
this.messages[this.currentAiMessageIndex].endStatus = true;
// 最后一次滚动到底部
this.throttledScrollToBottom();
}
continue;
}
try {
const parsed = JSON.parse(data);
// 检查parsed和choices是否存在
if (!parsed || !parsed.choices || !Array.isArray(parsed.choices) || parsed.choices.length === 0) {
console.warn('无效的响应格式:', data);
continue;
}
// 安全地访问delta属性
const delta = parsed.choices[0].delta || {};
// 根据您提供的格式解析内容
const content = delta.content;
// 有些响应可能包含reasoning_content而不是content
const reasoningContent = delta.reasoning_content || '';
// 只有当content不是null且有值或者reasoningContent有值时才添加
const textToAdd = (content !== null && content !== undefined) ? content : reasoningContent;
if (textToAdd) {
result += textToAdd;
// 确保messages[currentAiMessageIndex]存在
if (this.currentAiMessageIndex >= 0 && this.currentAiMessageIndex < this.messages.length) {
this.messages[this.currentAiMessageIndex].content = result;
this.messages[this.currentAiMessageIndex].renderedContent = renderMarkdown(result);
// 滚动到底部,但限制频率以提高性能
this.throttledScrollToBottom();
}
}
} catch (e) {
console.error('解析流数据失败:', e, line);
}
}
}
}
return result;
} catch (error) {
console.error('调用DeepSeek流式API失败:', error);
// 如果出错确保移除空的AI消息
if (this.messages.length > 0 && this.messages[this.messages.length - 1].content === '') {
this.messages.pop();
this.currentAiMessageIndex = -1;
}
throw error;
}
},
// 使用普通请求非H5环境
async fetchNormalResponse(apiMessages) {
try {
// 添加一个空的AI回复
this.currentAiMessageIndex = this.messages.length;
this.messages.push({
role: 'assistant',
content: '',
renderedContent: '',
time: this.formatTime(new Date()),
endStatus: false
});
const response = await uni.request({
url: 'https://api.siliconflow.cn/v1/chat/completions',
method: 'POST',
header: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
data: {
model: 'deepseek-ai/DeepSeek-R1-Distill-Qwen-7B',
messages: apiMessages,
stream: false,
max_tokens: 8192,
stop: '',
temperature: 0.6,
top_p: 0.7,
top_k: 50,
frequency_penalty: 0
},
dataType: 'json'
});
// 检查响应状态
if (response.statusCode !== 200) {
throw new Error(`API请求失败: ${response.statusCode}`);
}
// 获取响应内容
const result = response.data.choices[0].message.content;
// 更新消息内容
this.messages[this.currentAiMessageIndex].content = result;
this.messages[this.currentAiMessageIndex].renderedContent = renderMarkdown(result);
this.messages[this.currentAiMessageIndex].endStatus = true;
// 滚动到底部
this.throttledScrollToBottom();
return result;
} catch (error) {
console.error('调用DeepSeek API失败:', error);
// 如果出错确保移除空的AI消息
if (this.messages.length > 0 && this.messages[this.messages.length - 1].content === '') {
this.messages.pop();
this.currentAiMessageIndex = -1;
}
throw error;
}
},
// 接受剧本
acceptScript(scriptContent) {
// // 使用全局方法返回选择的剧本
// getApp().globalData.selectedScript = {
// orderId: this.orderId,
// content: scriptContent
// };
// const pages = getCurrentPages();
// // 上一个页面实例(假设是 pageA
// const prevPage = pages[pages.length - 2];
// console.log("prevPage",prevPage)
uni.showToast({
title: '已确认使用此剧本',
icon: 'success'
});
// 返回上一页
// setTimeout(() => {
// uni.navigateBack();
// }, 1500);
// 当前页面(假设是 pageB
uni.navigateBack({
delta: 1, // 返回的页面层数1 表示返回上一个页面
success: function() {
// 获取页面栈
const pages = getCurrentPages();
// 上一个页面实例(假设是 pageA
const prevPage = pages[pages.length - 2];
console.log("prevPage",prevPage)
// 调用上一个页面的方法
prevPage.getAiDramaTxt(scriptContent);
}
});
},
// 节流滚动到底部函数,避免频繁滚动
throttledScrollToBottom() {
// if (this.scrollTimer) return;
// this.scrollTimer = setTimeout(() => {
// this.scrollToBottom();
// this.scrollTimer = null;
// }, 1000); // 300ms内只执行一次滚动
this.$u.throttle(this.scrollToBottom, 300)
},
// 滚动到底部
scrollToBottom() {
this.$nextTick(() => {
// 方法1: 使用scrollTop设置滚动位置
// const query = uni.createSelectorQuery().in(this);
// query.select('.chat-content').boundingClientRect(data => {
// if (data) {
// // 设置一个非常大的值确保滚动到底部
// this.scrollTop = 999999;
// }
// }).exec();
// 方法2: 使用选择器定位到底部锚点元素
setTimeout(() => {
const query = uni.createSelectorQuery().in(this);
query.select('#scroll-bottom-anchor').boundingClientRect(data => {
if (data) {
// 滚动到锚点元素位置
uni.pageScrollTo({
selector: '#scroll-bottom-anchor',
duration: 100
});
}
}).exec();
}, 100);
});
},
// 加载更多消息(上拉加载)
loadMoreMessages() {
// 这里可以实现加载历史消息的逻辑
// 目前不需要,保留接口
this.needScrollToBottom = false; // 上拉加载时禁用自动滚动
// 加载完成后恢复自动滚动
setTimeout(() => {
this.needScrollToBottom = true;
}, 1000);
}
}
}
</script>
<style lang="scss">
.chat-container {
display: flex;
flex-direction: column;
height: calc(100vh - var(--window-top));
position: relative;
}
.chat-content {
flex: 1;
background-color: #f5f7fa;
// padding: 20rpx;
// padding-bottom: 180rpx; /* 为底部输入框留出空间 */
}
.message-wrapper-box{
padding: 20rpx;
// padding-bottom: 180rpx; /* 为底部输入框留出空间 */
}
.message-wrapper {
margin-bottom: 40rpx;
}
.message-container {
display: flex;
flex-direction: column;
}
.ai-message-container {
align-items: flex-start;
}
.user-message-container {
align-items: flex-end;
}
.message {
max-width: calc(80% - 40rpx);
padding: 24rpx 30rpx;
border-radius: 16rpx;
position: relative;
}
.ai-message {
background-color: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.user-message {
background-color: #4e6ef2;
color: white;
box-shadow: 0 2px 8px rgba(78, 110, 242, 0.2);
}
.message-content {
line-height: 1.5;
word-break: break-word;
}
/* Markdown样式 */
.markdown-body {
color: #333;
}
.message-time {
font-size: 24rpx;
color: #909399;
margin-top: 10rpx;
padding: 0 10rpx;
}
.user-message-container .message-time {
color: #909399;
}
/* 固定在底部的输入区域 */
.chat-input-container {
// position: fixed;
// bottom: 0;
// left: 0;
// right: 0;
background-color: #fff;
border-top: 1px solid #ebeef5;
padding: 20rpx 30rpx;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
z-index: 100;
// min-height: 170rpx;
margin-top: auto;
position: sticky;
bottom: 0;
}
.chat-input {
display: flex;
align-items: flex-end;
}
.bottom-placeholder {
height: 40rpx; /* 确保内容不被输入框遮挡 */
}
.accept-button-container {
// display: flex;
// justify-content: flex-start;
// margin-top: 20rpx;
margin-bottom: 40rpx;
}
.loading-container {
display: flex;
align-items: center;
justify-content: center;
color: #909399;
font-size: 28rpx;
margin: 20rpx 0;
}
.loading-text {
margin-left: 16rpx;
}
.button-icon {
margin-right: 8rpx;
}
</style>