给自己网站增加一个免费的AI助手,纯HTML
助手效果图
看完这篇文章,你将免费拥有你自己的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>