作品简介
一个可以帮助学习计算机各种算法的AI小助手,提升工作效率。
技术架构
使用Html语言完成图形化页面的样式,使用JavaScript语言来操作对应的逻辑代码。
实现过程
1、创建一个界面
2、获取数据
3、添加按钮与功能
4、程序优化调试
开发环境、开发流程
系统环境:MacOs系统
开发工具:VSCode
开发插件:腾讯云AI代码助手
关键技术解析
1.绘制页面
2.获取数据
3.解析数据
4.渲染数据
腾讯云AI代码助手在上述过程中的助力
1.生成页面
2.获取数据
3.处理数据
4.事件绑定执行
使用说明
提问各种算法问题,点击按钮,界面算法内容。
项目源码
TypeScript
<template>
<div class="chat-container">
<t-chat
ref="chatRef"
layout="single"
class="chat-window"
:clear-history="chatList.length > 0 && !isStreamLoad"
@clear="clearConfirm"
>
<template v-for="(item, index) in chatList" :key="index">
<t-chat-item
:avatar="item.avatar"
:name="item.name"
:role="item.role"
:datetime="item.datetime"
:text-loading="index === 0 && loading"
>
<template #content>
<div
:class="['chat-bubble', item.role === 'user' ? 'user-bubble' : 'assistant-bubble']"
v-html="item.content"
></div>
</template>
<template v-if="!isStreamLoad" #actions>
<t-chat-action
:is-good="isGood[index]"
:is-bad="isBad[index]"
@operation="(type, { e }) => handleOperation(type, { e, index })"
class="feedback-action"
/>
</template>
</t-chat-item>
</template>
<template #footer>
<div class="input-area">
<t-chat-input
:stop-disabled="isStreamLoad"
@send="inputEnter"
@stop="onStop"
placeholder="请输入您的问题..."
/>
<t-button
v-if="isStreamLoad"
@click="onStop"
type="danger"
icon="close"
circle
class="stop-button"
/>
</div>
</template>
</t-chat>
<!-- 反馈表单对话框 -->
<t-dialog
v-model:visible="showFeedbackForm"
:mask-closable="false"
:show-close="false"
width="450px"
class="feedback-dialog"
>
<div class="feedback-content">
<t-textarea
v-model="feedbackContent"
placeholder="您的宝贵建议对我们非常重要!您的反馈将帮助我们持续改进!"
:rows="4"
class="feedback-textarea"
/>
</div>
<template #footer>
<t-button type="primary" @click="submitFeedback">提交反馈</t-button>
</template>
</t-dialog>
<!-- 分享对话框 -->
<t-dialog
v-model:visible="showShareDialog"
title="分享对话"
:mask-closable="false"
:show-close="false"
width="400px"
class="share-dialog"
>
<div class="share-options">
<t-button
variant="outline"
icon="wechat"
@click="shareToWeChatFriends"
class="share-button"
>
微信好友
</t-button>
<t-button
variant="outline"
icon="wechat-moments"
@click="shareToWeChatMoments"
class="share-button"
>
朋友圈
</t-button>
<t-button
variant="outline"
icon="weibo"
@click="shareToWeibo"
class="share-button"
>
微博
</t-button>
<t-button
variant="outline"
icon="qq"
@click="shareToQQZone"
class="share-button"
>
QQ空间
</t-button>
</div>
</t-dialog>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next';
import { saveAs } from 'file-saver';
// 反馈表单相关的状态
const showFeedbackForm = ref(false);
const feedbackType = ref(null); // 'good' 或 'bad'
const currentFeedbackIndex = ref(null);
const feedbackContent = ref('');
// 分享对话框相关的状态
const showShareDialog = ref(false);
// 滚动到底部
const chatRef = ref(null);
const backBottom = () => {
chatRef.value.scrollToBottom({
behavior: 'smooth',
});
};
// 初始化聊天记录,仅保留初始机器人问候
const chatList = ref([
{
avatar: 'https://tdesign.gtimg.com/site/chat-avatar.png', // 小美的头像链接
name: '客服小Ai',
datetime: new Date().toLocaleString(),
content: '早上好,我是你的鹅厂助理小Ai,我熟知计算机各种算法和数据结构,请问你有什么帮助?',
role: 'assistant',
},
]);
// 初始化 isGood 和 isBad 数组
const isGood = ref([]);
const isBad = ref([]);
const initializeFeedbackStates = () => {
isGood.value = chatList.value.map(() => false);
isBad.value = chatList.value.map(() => false);
};
initializeFeedbackStates();
const fetchCancel = ref(null);
const loading = ref(false);
const isStreamLoad = ref(false);
/**
* Clears the confirmation by resetting the chat list to an empty array.
*/
const clearConfirm = function () {
chatList.value = [];
initializeFeedbackStates();
};
/**
* Handles the stop event for a fetch operation.
*/
const onStop = function () {
if (fetchCancel.value) {
fetchCancel.value.abort();
loading.value = false;
isStreamLoad.value = false;
}
};
/**
* Handles different types of operations based on the provided type and options.
*/
const handleOperation = function (type, options) {
const { index } = options;
if (type === 'good') {
isGood.value[index] = !isGood.value[index];
if (isGood.value[index]) {
isBad.value[index] = false;
// 打开反馈表单
feedbackType.value = 'good';
currentFeedbackIndex.value = index;
showFeedbackForm.value = true;
}
} else if (type === 'bad') {
isBad.value[index] = !isBad.value[index];
if (isBad.value[index]) {
isGood.value[index] = false;
// 打开反馈表单
feedbackType.value = 'bad';
currentFeedbackIndex.value = index;
showFeedbackForm.value = true;
}
}
};
/**
* 提交反馈表单
*/
const submitFeedback = () => {
if (currentFeedbackIndex.value === null) {
MessagePlugin.error('未找到对应的消息!');
return;
}
// 显示固定成功消息
MessagePlugin.success('感谢您的反馈,我们将持续改进!');
// 关闭反馈表单
showFeedbackForm.value = false;
};
/**
* Handles the asynchronous processing of user input data.
*/
const handleData = async (inputValue) => {
loading.value = true;
isStreamLoad.value = true;
const lastItem = chatList.value[0];
const messages = [{
role: 'user',
content: inputValue,
}];
fetchSSE(messages, {
success(result) {
loading.value = false;
const { data } = result;
lastItem.content += data?.delta?.content;
},
complete(isOk, msg) {
if (!isOk || !lastItem.content) {
lastItem.role = 'error';
lastItem.content = msg;
}
// 控制终止按钮
isStreamLoad.value = false;
loading.value = false;
},
cancel(cancel) {
fetchCancel.value = cancel;
},
});
};
/**
* Handles the input when the enter key is pressed.
*/
const inputEnter = function (inputValue) {
if (isStreamLoad.value) {
return;
}
if (!inputValue) return;
const params = {
avatar: 'https://tdesign.gtimg.com/site/chat-avatar.png', // 用户的头像链接
name: '您',
datetime: new Date().toLocaleString(),
content: inputValue,
role: 'user',
};
chatList.value.unshift(params);
// 空消息占位
const params2 = {
avatar: 'https://tdesign.gtimg.com/site/chat-avatar.png', // 小美的头像链接
name: '客服小Ai',
datetime: new Date().toLocaleString(),
content: '',
role: 'assistant',
};
chatList.value.unshift(params2);
isGood.value.unshift(false);
isBad.value.unshift(false);
handleData(inputValue);
};
/**
* 解析SSE数据
*/
const fetchSSE = async (messages, options) => {
const { success, fail, complete, cancel } = options;
const controller = new AbortController();
const { signal } = controller;
cancel?.(controller);
// your-api-key
const apiKey = 'sk-6R0hq8U7v3bSbT1u41Lp6kPRwAgf9wnW73WgvSC7WUI73eRO';
const responsePromise = fetch('/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer${apiKey ? ` ${apiKey}` : ''}`,
},
body: JSON.stringify({
messages, // 消息列表
model: 'hunyuan-pro', // 模型
stream: true, // 流式
}),
signal,
}).catch((e) => {
const msg = e.toString() || '流式接口异常';
complete?.(false, msg);
return Promise.reject(e); // 确保错误能够被后续的.catch()捕获
});
responsePromise
.then((response) => {
if (!response?.ok) {
complete?.(false, response.statusText);
fail?.();
throw new Error('Request failed'); // 抛出错误以便链式调用中的下一个.catch()处理
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
if (!reader) throw new Error('No reader available');
const bufferArr = [];
let dataText = ''; // 记录数据
const event = { type: null, data: null };
async function processText({ done, value }) {
if (done) {
complete?.(true);
return Promise.resolve();
}
const chunk = decoder.decode(value);
const buffers = chunk.toString().split(/\r?\n/);
bufferArr.push(...buffers);
let i = 0;
while (i < bufferArr.length) {
const line = bufferArr[i];
if (line) {
dataText += line;
const response = line.slice(6);
if (response === '[DONE]') {
event.type = 'finish';
dataText = '';
} else {
try {
const choices = JSON.parse(response.trim())?.choices?.[0];
if (choices.finish_reason === 'stop') {
event.type = 'finish';
dataText = '';
} else {
event.type = 'delta';
event.data = choices;
}
} catch (error) {
console.error('解析错误:', error);
}
}
}
if (event.type && event.data) {
const jsonData = { ...event };
success(jsonData);
event.type = null;
event.data = null;
}
bufferArr.splice(i, 1);
}
return reader.read().then(processText);
}
return reader.read().then(processText);
})
.catch(() => {
// 处理整个链式调用过程中发生的任何错误
fail?.();
});
};
/**
* 下载对话记录
*/
const downloadConversation = () => {
const conversation = chatList.value
.map((msg) => {
const time = new Date(msg.datetime).toLocaleString();
// 去除HTML标签,确保文本格式清晰
const content = msg.content.replace(/<[^>]+>/g, '');
return `[${time}] ${msg.name} (${msg.role}): ${content}`;
})
.reverse() // 反转以按时间顺序排列
.join('\n\n');
const blob = new Blob([conversation], { type: 'text/plain;charset=utf-8' });
saveAs(blob, 'conversation.txt');
};
/**
* 分享对话记录(打开分享对话框)
*/
const shareConversation = async () => {
openShareDialog();
};
</script>
<style lang="less" scoped>
/* 隐藏主题切换时的遮罩层 */
.t-dialog-mask, /* 可能的遮罩层类名 */
.theme-mask {
display: none !important;
}
/* 新的整体容器样式 */
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
padding: 30px;
box-sizing: border-box;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
/* 工具栏样式更新,按钮统一样式和颜色 */
.toolbar {
display: flex;
justify-content: flex-end; /* 按钮靠右排列 */
gap: 15px;
margin-bottom: 25px;
}
.toolbar-button {
background-color: #bae7ff;
font-size: 16px;
padding: 8px 16px;
display: flex;
align-items: center;
gap: 6px;
}
/* 聊天窗口样式优化 */
.chat-window {
flex: 1;
border: none;
border-radius: 16px;
background-color: transparent;
background-color: rgba(211, 211, 211, 0.5); box-shadow: 0 8px 24px rgba(197, 235, 28, 0.1);
overflow: hidden;
}
/* 聊天气泡的通用样式 */
.chat-bubble {
max-width: 65%;
padding: 16px 22px;
border-radius: 30px;
margin-bottom: 18px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
word-break: break-word;
font-size: 16px;
line-height: 1.5;
}
/* 用户消息气泡 */
.user-bubble {
background-color: #bae7ff; /* 更清新的蓝色 */
align-self: flex-end;
border-top-right-radius: 0;
}
/* 助手消息气泡 */
.assistant-bubble {
background-color: #d9f7be; /* 柔和的绿色 */
align-self: flex-start;
border-top-left-radius: 0;
}
/* 反馈按钮样式修改 */
.feedback-action {
display: flex;
gap: 10px;
}
.feedback-action .t-button {
background: none;
border: none;
cursor: pointer;
font-size: 14px;
padding: 4px 8px;
border-radius: 4px;
transition: background-color 0.3s, color 0.3s;
}
.feedback-action .t-button:hover {
background-color: rgba(24, 144, 255, 0.1);
}
.active-good {
color: #52c41a; /* 赞按钮选中时的绿色 */
}
.active-bad {
color: #ff4d4f; /* 踩按钮选中时的红色 */
}
/* 调整聊天底部布局 */
.input-area {
display: flex;
align-items: center;
gap: 15px;
padding: 10px 0;
border-top: 1px solid #f0f0f0;
}
/* 停止按钮样式 */
.stop-button {
background-color: #ff4d4f;
border: none;
width: 40px;
height: 40px;
}
/* 更新后的反馈表单对话框样式 */
.feedback-dialog {
.t-dialog__header {
background-color: #1890ff; /* 蓝色头部 */
color: #ffffff;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
font-size: 18px;
}
.t-dialog__body {
padding: 25px;
background-color: #f0f5ff; /* 淡蓝色背景 */
}
.t-dialog__footer {
background-color: #f0f5ff;
border-bottom-left-radius: 16px;
border-bottom-right-radius: 16px;
}
}
.feedback-content p {
margin-bottom: 14px;
font-size: 17px;
}
.feedback-content strong {
color: #60b1fd;
}
.feedback-textarea {
width: 100%;
border: 1px solid #1890ff;
border-radius: 8px;
padding: 12px;
resize: vertical;
font-size: 16px;
outline: none;
transition: border-color 0.3s;
}
.feedback-textarea:hover,
.feedback-textarea:focus {
border-color: #40a9ff;
}
/* 分享对话框样式 */
.share-dialog {
.t-dialog__header {
background-color: #40a9ff; /* 蓝色头部 */
color: #ffffff;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
font-size: 18px;
text-align: center;
}
.t-dialog__body {
padding: 20px;
background-color: #a8def7; /* 淡蓝色背景 */
display: flex;
flex-direction: column;
align-items: center;
}
.share-dialog__footer {
display: none; /* 移除对话框底部 */
}
}
.share-options {
display: flex;
flex-direction: column;
gap: 12px;
}
.share-button {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 16px;
padding: 10px 0;
border-radius: 8px;
transition: background-color 0.3s, color 0.3s;
}
.share-button:hover {
background-color: rgba(24, 144, 255, 0.1);
}
/* 响应式设计 */
@media (max-width: 768px) {
.chat-container {
padding: 20px;
}
.toolbar {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.chat-bubble {
max-width: 80%;
}
.feedback-dialog,
.share-dialog {
width: 90% !important;
}
.toolbar-button {
font-size: 14px;
padding: 6px 12px;
}
.chat-bubble {
padding: 14px 18px;
margin-bottom: 16px;
font-size: 15px;
}
.feedback-content p {
font-size: 16px;
}
.feedback-textarea {
padding: 10px;
font-size: 15px;
}
.share-button {
font-size: 14px;
padding: 8px 0;
}
}
</style>
效果展示