助手效果图
看完这篇文章,你将免费拥有你自己的Ai助手,全程干货,先到先得
获取免费的AI大模型接口
访问这个地址 生成key https://openrouter.ai/mistralai/mistral-small-3.2-24b-instruct:free/api
或者调用其他的免费大模型,这个根据自己的需求更改,要先注册这个网站
修改默认的参数
最主要的就是你申请生成的key
助手源码
纯HTML的源码,嘎嘎够劲
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
<style>
body {
font-family: 'Arial', sans-serif;
margin: 0;
padding: 0;
height: 100vh;
user-select: none;
overflow: hidden;
}
#chat-container {
position: fixed;
bottom: 20px;
right: 20px;
width: 300px;
height: 400px;
background-color: white;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
overflow: hidden;
z-index: 1000;
resize: both;
min-width: 300px;
min-height: 400px;
transition: transform 0.2s ease, opacity 0.2s ease;
transform-origin: bottom right;
}
#chat-container.minimized {
transform: scale(0);
opacity: 0;
pointer-events: none;
}
#chat-header {
background-color: #4a6bdf;
color: white;
padding: 12px 15px;
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
}
#chat-title {
font-weight: bold;
font-size: 16px;
}
#minimize-btn, #restore-btn {
background: none;
border: none;
color: white;
font-size: 18px;
cursor: pointer;
padding: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
#minimized-chat {
position: fixed;
width: 50px;
height: 50px;
border-radius: 10px;
background-color: #4a6bdf;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
z-index: 1000;
display: none;
transition: transform 0.1s ease;
}
#minimized-chat:active {
transform: scale(0.95);
}
#restore-btn {
position: absolute;
width: 100%;
height: 100%;
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
cursor: move;
pointer-events: auto;
}
#chat-messages {
flex: 1;
padding: 15px;
overflow-y: auto;
background-color: #f9f9f9;
}
.message {
margin-bottom: 12px;
max-width: 80%;
padding: 8px 12px;
border-radius: 12px;
line-height: 1.4;
word-wrap: break-word;
}
.user-message {
background-color: #e3effd;
margin-left: auto;
border-bottom-right-radius: 4px;
}
.ai-message {
background-color: white;
margin-right: auto;
border-bottom-left-radius: 4px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
#chat-input-area {
display: flex;
padding: 10px;
border-top: 1px solid #eee;
background-color: white;
}
#chat-input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 20px;
outline: none;
resize: none;
height: 40px;
max-height: 100px;
font-family: inherit;
}
#send-btn {
margin-left: 10px;
padding: 0 15px;
background-color: #4a6bdf;
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
transition: background-color 0.2s;
}
#send-btn.stop {
background-color: #ff4d4d;
}
#send-btn:hover {
background-color: #3a5bcf;
}
#send-btn.stop:hover {
background-color: #e63c3c;
}
.typing-indicator {
display: inline-block;
margin-left: 5px;
}
.typing-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background-color: #999;
margin-right: 3px;
animation: typingAnimation 1.4s infinite ease-in-out;
}
.typing-dot:nth-child(1) {
animation-delay: 0s;
}
.typing-dot:nth-child(2) {
animation-delay: 0.2s;
}
.typing-dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typingAnimation {
0%, 60%, 100% {
transform: translateY(0);
}
30% {
transform: translateY(-5px);
}
}
.stopped-message {
color: #888;
font-style: italic;
}
</style>
</head>
<body>
<div id="chat-container">
<div id="chat-header">
<div id="chat-title">AI助手</div>
<button id="minimize-btn">−</button>
</div>
<div id="chat-messages"></div>
<div id="chat-input-area">
<textarea id="chat-input" placeholder="输入消息..." rows="1"></textarea>
<button id="send-btn">发送</button>
</div>
</div>
<div id="minimized-chat">
<button id="restore-btn">+</button>
</div>
<script>
// 获取DOM元素
const chatContainer = document.getElementById('chat-container');
const chatHeader = document.getElementById('chat-header');
const minimizedChat = document.getElementById('minimized-chat');
const minimizeBtn = document.getElementById('minimize-btn');
const restoreBtn = document.getElementById('restore-btn');
const chatInput = document.getElementById('chat-input');
const sendBtn = document.getElementById('send-btn');
const chatMessages = document.getElementById('chat-messages');
// 全局变量
let isDragging = false;
let isMinimizedDragging = false;
let offsetX, offsetY;
let startX, startY;
let restoreBtnClicked = false;
let abortController = null; // 用于中止fetch请求
let isWaitingForResponse = false; // 是否正在等待响应
let isTypingEffectActive = false; // 是否正在打字效果中
let typingTimeoutId = null; // 打字效果的timeout ID
// 限制元素在窗口范围内
function constrainToWindow(element, x, y) {
const rect = element.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const maxX = windowWidth - rect.width;
const maxY = windowHeight - rect.height;
x = Math.max(0, Math.min(x, maxX));
y = Math.max(0, Math.min(y, maxY));
return { x, y };
}
// 主窗口拖动功能
chatHeader.addEventListener('mousedown', (e) => {
if (e.target.id !== 'chat-header' && e.target.id !== 'chat-title') return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = chatContainer.getBoundingClientRect();
offsetX = startX - rect.left;
offsetY = startY - rect.top;
chatContainer.style.cursor = 'grabbing';
chatContainer.style.transition = 'none';
e.preventDefault();
});
// 恢复按钮拖动功能
restoreBtn.addEventListener('mousedown', (e) => {
isMinimizedDragging = true;
restoreBtnClicked = false;
startX = e.clientX;
startY = e.clientY;
const rect = minimizedChat.getBoundingClientRect();
offsetX = startX - rect.left;
offsetY = startY - rect.top;
minimizedChat.style.cursor = 'grabbing';
minimizedChat.style.transition = 'none';
e.preventDefault();
e.stopPropagation();
});
// 恢复按钮点击功能
restoreBtn.addEventListener('dblclick', (e) => {
if (!isMinimizedDragging && !restoreBtnClicked) {
restoreBtnClicked = true;
restoreChatWindow();
}
e.stopPropagation();
});
document.addEventListener('mousemove', (e) => {
if (isDragging) {
let x = e.clientX - offsetX;
let y = e.clientY - offsetY;
const constrained = constrainToWindow(chatContainer, x, y);
x = constrained.x;
y = constrained.y;
chatContainer.style.left = `${x}px`;
chatContainer.style.top = `${y}px`;
chatContainer.style.right = 'auto';
chatContainer.style.bottom = 'auto';
}
if (isMinimizedDragging) {
let x = e.clientX - offsetX;
let y = e.clientY - offsetY;
const constrained = constrainToWindow(minimizedChat, x, y);
x = constrained.x;
y = constrained.y;
minimizedChat.style.left = `${x}px`;
minimizedChat.style.top = `${y}px`;
minimizedChat.style.right = 'auto';
minimizedChat.style.bottom = 'auto';
}
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
chatContainer.style.cursor = 'default';
chatContainer.style.transition = 'transform 0.2s ease, opacity 0.2s ease';
}
if (isMinimizedDragging) {
isMinimizedDragging = false;
minimizedChat.style.cursor = 'move';
minimizedChat.style.transition = 'transform 0.1s ease';
}
});
// 缩小/恢复功能
minimizeBtn.addEventListener('click', (e) => {
e.stopPropagation();
const rect = chatContainer.getBoundingClientRect();
minimizedChat.style.left = `${rect.left}px`;
minimizedChat.style.top = `${rect.top}px`;
minimizedChat.style.right = 'auto';
minimizedChat.style.bottom = 'auto';
const constrained = constrainToWindow(
minimizedChat,
parseFloat(minimizedChat.style.left || 0),
parseFloat(minimizedChat.style.top || 0)
);
minimizedChat.style.left = `${constrained.x}px`;
minimizedChat.style.top = `${constrained.y}px`;
chatContainer.classList.add('minimized');
setTimeout(() => {
minimizedChat.style.display = 'block';
}, 200);
});
function restoreChatWindow() {
chatContainer.style.left = 'auto';
chatContainer.style.top = 'auto';
chatContainer.style.right = '20px';
chatContainer.style.bottom = '20px';
chatContainer.classList.remove('minimized');
minimizedChat.style.display = 'none';
}
// 聊天功能
chatInput.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = (this.scrollHeight > 100 ? 100 : this.scrollHeight) + 'px';
});
function sendMessage() {
const message = chatInput.value.trim();
if (!message) return;
addMessage(message, 'user');
chatInput.value = '';
chatInput.style.height = '40px';
// 改变按钮状态
setSendButtonState('stop');
const typingId = showTypingIndicator();
simulateAIResponse(message, typingId);
}
function stopRequest() {
if (abortController) {
abortController.abort();
abortController = null;
}
if (isTypingEffectActive) {
clearTimeout(typingTimeoutId);
isTypingEffectActive = false;
// 添加停止提示
const stoppedDiv = document.createElement('div');
stoppedDiv.className = 'message ai-message stopped-message';
stoppedDiv.textContent = '已停止生成回复';
chatMessages.appendChild(stoppedDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
setSendButtonState('send');
isWaitingForResponse = false;
// 移除正在输入指示器
const typingElements = document.querySelectorAll('[id^="typing-"]');
typingElements.forEach(el => el.remove());
}
function setSendButtonState(state) {
if (state === 'stop') {
sendBtn.textContent = '停止';
sendBtn.classList.add('stop');
sendBtn.removeEventListener('click', sendMessage);
sendBtn.addEventListener('click', stopRequest);
isWaitingForResponse = true;
} else {
sendBtn.textContent = '发送';
sendBtn.classList.remove('stop');
sendBtn.removeEventListener('click', stopRequest);
sendBtn.addEventListener('click', sendMessage);
isWaitingForResponse = false;
}
}
chatInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (!isWaitingForResponse) {
sendMessage();
}
}
});
// 初始化按钮事件
sendBtn.addEventListener('click', sendMessage);
function addMessage(text, sender) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${sender}-message`;
messageDiv.textContent = text;
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function showTypingIndicator() {
const typingDiv = document.createElement('div');
typingDiv.className = 'message ai-message';
typingDiv.id = 'typing-' + Date.now();
const typingText = document.createElement('span');
typingText.textContent = 'YiLin:';
const typingDots = document.createElement('span');
typingDots.className = 'typing-indicator';
for (let i = 0; i < 3; i++) {
const dot = document.createElement('span');
dot.className = 'typing-dot';
typingDots.appendChild(dot);
}
typingDiv.appendChild(typingText);
typingDiv.appendChild(typingDots);
chatMessages.appendChild(typingDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
return typingDiv.id;
}
function removeTypingIndicator(id) {
const typingElement = document.getElementById(id);
if (typingElement) {
typingElement.remove();
}
}
async function simulateAIResponse(userMessage, typingId) {
abortController = new AbortController();
try {
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": "Bearer sk-or-v1-xxxxxxxxxxxx",
"HTTP-Referer": "https://nanwish.love",
"X-Title": "沂霖博客",
"Content-Type": "application/json"
},
body: JSON.stringify({
"model": "mistralai/mistral-small-3.2-24b-instruct:free",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": userMessage
}
]
}
]
}),
signal: abortController.signal
});
const data = await response.json();
removeTypingIndicator(typingId);
if (data.choices && data.choices[0].message.content) {
typeWriterEffect(data.choices[0].message.content);
} else {
addMessage("抱歉,未能获取有效回复", 'ai');
setSendButtonState('send');
}
} catch (error) {
if (error.name !== 'AbortError') {
removeTypingIndicator(typingId);
addMessage("抱歉,发生错误: " + error.message, 'ai');
setSendButtonState('send');
}
} finally {
abortController = null;
}
}
function typeWriterEffect(text) {
const messageDiv = document.createElement('div');
messageDiv.className = 'message ai-message';
chatMessages.appendChild(messageDiv);
let i = 0;
const speed = 20;
isTypingEffectActive = true;
function type() {
if (i < text.length) {
messageDiv.textContent += text.charAt(i);
i++;
chatMessages.scrollTop = chatMessages.scrollHeight;
typingTimeoutId = setTimeout(type, speed);
} else {
isTypingEffectActive = false;
setSendButtonState('send');
}
}
type();
}
// 初始化
window.addEventListener('DOMContentLoaded', () => {
minimizedChat.style.display = 'none';
setTimeout(() => {
typeWriterEffect("你好!我是沂霖,我可以辅助你使用MarkDown,聊天,查资料!你可以输入问题或指令,我会尽力回答。");
}, 500);
});
// 窗口大小变化时重新限制位置
window.addEventListener('resize', () => {
if (!chatContainer.classList.contains('minimized')) {
const rect = chatContainer.getBoundingClientRect();
const constrained = constrainToWindow(chatContainer, rect.left, rect.top);
chatContainer.style.left = `${constrained.x}px`;
chatContainer.style.top = `${constrained.y}px`;
}
if (minimizedChat.style.display === 'block') {
const rect = minimizedChat.getBoundingClientRect();
const constrained = constrainToWindow(minimizedChat, rect.left, rect.top);
minimizedChat.style.left = `${constrained.x}px`;
minimizedChat.style.top = `${constrained.y}px`;
}
});
</script>
</body>
</html>