利用大语言模型 Google Gemini API 制作一个AI聊天机器人

1. index.html

javascript 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Gemini Chatbot</title>
    <!-- Linking Google Fonts for Icons -->
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,0,0" />
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <!-- 整个聊天机器人的主容器 -->
    <div class="container">

        <!-- App Header -->
         <!-- h1 和 h2 作为标题和副标题,向用户打招呼并引导提问 -->
        <header class="app-header">
            <h1 class="heading">Hello, there</h1>
            <h2 class="sub-heading">How can I help you?</h2>
        </header>

        <!-- Suggestions List -->
         <!-- 这是一个无序列表,包含几个预设的用户问题,方便用户快速选择 -->
         <ul class="suggestions">
            <li class="suggestions-item">
                <p class="text">How does a Large Language Model work?</p>
                <span class="material-symbols-rounded">lightbulb</span>
            </li>
            <li class="suggestions-item">
                <p class="text">What are the best practices for fine-tuning an LLM?</p>
                <span class="material-symbols-rounded">code</span>
            </li>
            <li class="suggestions-item">
                <p class="text">How can I use an LLM for text summarization?</p>
                <span class="material-symbols-rounded">summarize</span>
            </li>
            <li class="suggestions-item">
                <p class="text">What are the ethical concerns of deploying LLMs?</p>
                <span class="material-symbols-rounded">gavel</span>
            </li>
         </ul>

        <!-- Chats Container -->
        <!-- 聊天消息(用户输入 & 机器人回复),JavaScript 代码会动态填充此部分 -->
         <div class="chats-container">
         </div>

        <!-- Prompt Container -->
         <div class="prompt-container">
            <!-- 用于容纳用户输入框和相关的功能按钮 -->

            <div class="prompt-wrapper">
                <form action="#" class="prompt-form">

                    <!-- 文本输入框,用户可以输入问题 -->
                    <input type="text" placeholder="Ask Gemini" class="prompt-input" required>
                    <div class="prompt-actions">
                        <!-- File Upload Wrapper -->
                        <div class="file-upload-wrapper">
                            <!-- 用于预览上传的文件 -->
                            <img src="#" class="file-preview">
                            <!-- 用于上传图片、PDF、文本文件等,但默认隐藏 -->
                            <input type="file" accept="image/*, .pdf, .txt, .csv", id="file-input" hidden>
                            <button type="button" class="file-icon material-symbols-rounded">
                                description
                            </button>
                            <button id="add-file-btn" type="button" class="material-symbols-rounded">
                                attach_file
                            </button>
                            <button id="cancel-file-btn" type="button" class="material-symbols-rounded">
                                close
                            </button>
                        </div>
                        <!-- 用于停止 AI 生成的回复。 -->
                        <button type="button" id="stop-response-btn" class="material-symbols-rounded">
                            stop_circle
                        </button>
                        <!-- 提交用户的问题 -->
                        <button id="send-prompt-btn" class="material-symbols-rounded">
                            arrow_upward
                        </button>
                    </div>
                </form>
                <!-- 用于切换明暗模式 -->
                <button id="theme-toggle-btn" class="material-symbols-rounded">
                    light_mode
                </button>
                <!-- 用于清除聊天记录 -->
                <button id="delete-chats-btn" class="material-symbols-rounded">
                    delete
                </button>
            </div>
            <!-- 免责声明 -->
            <p class="disclaimer-text">Gemini can make mistakes, so double-check it.</p>
         </div>
    </div>

    <script src="script.js"></script>
    
</body>
</html>

这个 HTML 代码是一个 AI 聊天机器人网页前端界面,具有以下功能:

  1. 聊天输入框:用户可以输入问题并发送。
  2. 预设问题:提供几个示例问题,用户可以直接选择。
  3. 聊天历史容器:用于动态显示用户和 AI 之间的对话。
  4. 文件上传:支持上传图片、PDF、文本等文件。
  5. 操作按钮:发送问题、停止 AI 回复、切换深色/浅色模式、清空聊天记录
  6. Material Icons 设计:界面美观,使用 Google Material Symbols 作为按钮图标。

2. style.css

css 复制代码
/* Importing Google Fonts - Poppins */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap');

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: "Poppins", sans-serif;
}

/* 控制文本、背景、滚动条等颜色 */
:root {
    /* Dark theme colors */
    --text-color: #edf3ff;
    --subheading-color: #97a7ca;
    --placeholder-color: #c3cdde;
    --primary-color: #101623;
    --secondary-color: #283045;
    --secondary-hover-color: #333e58;
    --scrollbar-color: #626a7f;
}

body.light-theme {
    /* Light theme colors */
    --text-color: #090c13;
    --subheading-color: #7b8cae;
    --placeholder-color: #606982;
    --primary-color: #f3f7ff;
    --secondary-color: #dce6f9;
    --secondary-hover-color: #d2ddf2;
    --scrollbar-color: #a2aac2;
}

/* 字体颜色和背景由主题变量控制 */
body {
    color: var(--text-color);
    background: var(--primary-color);
}

.container {
    overflow-y: auto;
    /* 当内容超出容器高度时,显示纵向滚动条 */
    padding: 32px 0 60px;
    max-height: calc(100vh - 127px);
    scrollbar-color: var(--scrollbar-color) transparent;
    /* 定义滚动条颜色,轨道为透明,滑块颜色由变量--scrollbar-color决定。 */
}

.container :where(.app-header, .suggestions, .message, .prompt-wrapper, .disclaimer-text) {
    margin: 0 auto;
    /* 水平居中对齐 */
    width: 100%;
    padding: 0 20px;
    max-width: 980px;
}

/* App header stylings */
.container .app-header {
    margin-top: 4vh;
}

.app-header .heading {
    font-size: 3rem;
    width: fit-content;
    background: linear-gradient(to right, #1d7efd, #8f6fff);
    /* 使用线性渐变背景(从#1d7efd到#8f6fff),方向从左到右 */
    background-clip: text;
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    /* 背景裁剪至文本区域,并通过透明填充实现渐变文字效果。 */
}

.app-header .sub-heading {
    font-size: 2.6rem;
    margin-top: -5px;
    color: var(--subheading-color);
}

/* Suggestions list stylings */
.container .suggestions {
    display: flex;
    /* 使用flex布局,使子元素水平排列。 */
    gap: 15px;
    margin-top: 9.5vh;
    list-style: none;
    overflow-x: auto;
    scrollbar-width: none;
    /* 水平内容超出时允许滚动,并隐藏滚动条 */
}

/* 当body元素具有chats-active类时,.container内的.app-header和.suggestions元素会被隐藏 */
body.chats-active .container :where(.app-header, .suggestions){
    display: none;
}

.suggestions .suggestions-item {
    width: 228px;
    padding: 18px;
    flex-shrink: 0;
    display: flex;
    cursor: pointer;
    flex-direction: column;
    /* 使用弹性布局,方向为列 */
    align-items: flex-end;
    justify-content: space-between;
    border-radius: 12px;
    background: var(--secondary-color);
    transition: 0.3s ease;
}

.suggestions .suggestions-item:hover {
    background: var(--secondary-hover-color)
}

.suggestions .suggestions-item .text {
    font-size: 1.1rem;
}

.suggestions .suggestions-item span {
    height: 45px;
    width: 45px;
    margin-top: 35px;
    display: flex;
    align-self: flex-end;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    color: #1d7efd;
    background: var(--primary-color);
}

.suggestions .suggestions-item:nth-child(2) span {
    color: #28a745;
}
.suggestions .suggestions-item:nth-child(3) span {
    color: #ffc107;
}
.suggestions .suggestions-item:nth-child(4) span {
    color: #6f42c1;
}

/* Chats container stylings */
.container .chats-container {
    display: flex;
    gap: 20px;
    flex-direction: column;
}

.chats-container .message {
    display: flex;
    gap: 11px;
    align-items: center;
}

.chats-container .bot-message .avatar {
    height: 43px;
    width: 43px;
    flex-shrink: 0;
    /* 禁止头像在Flex布局中缩小 */
    padding: 6px;
    align-self: flex-start;
    /* 将头像对齐到Flex容器的起始位置 */
    margin-right: -7px;
    border-radius: 50%;
    /* 设置头像的圆角为50%,使其呈现圆形 */
    background: var(--secondary-color);
    border: 1px solid var(--secondary-hover-color);
}

/* 当聊天机器人正在加载时,头像会旋转 */
.chats-container .bot-message.loading .avatar {
    animation: rotate 3s linear infinite;
}
/* 在3秒内线性地完成一次360度的旋转,并无限循环 */

@keyframes rotate {
    100% {
        transform: rotate(360deg);
    }
}

.chats-container .message .message-text {
    padding: 3px 16px;
    word-wrap: break-word;
    /* 允许长单词或URL地址换行到下一行。 */
    white-space: pre-line;
}

.chats-container .bot-message {
    margin: 9px auto;
}

.chats-container .user-message {
    flex-direction: column;
    align-items: flex-end;
}

.chats-container .user-message .message-text {
    padding: 12px 16px;
    max-width: 75%;
    border-radius: 13px 13px 3px 13px;
    background: var(--secondary-color);
}

.chats-container .user-message .img-attachment {
    width: 50%;
    margin-top: -7px;
    border-radius: 13px 3px 13px 13px;
}

.chats-container .user-message .file-attachment {
    display: flex;
    gap: 6px;
    align-items: center;
    padding: 10px;
    margin-top: -7px;
    border-radius: 13px 3px 13px 13px;
    background: var(--secondary-color);
}

.chats-container .user-message .file-attachment span {
    color: #1d7efd;
}

/* Prompt container stylings */
.prompt-container {
    position: fixed;
    bottom: 0;
    left: 0;
    width: 100%;
    padding: 16px 0;
    background: var(--primary-color);
}

.prompt-container :where(.prompt-wrapper, .prompt-form, .prompt-actions){
    display: flex;
    gap: 12px;
    height: 56px;
    align-items: center;
}

.prompt-wrapper .prompt-form {
    width: 100%;
    height: 100%;
    border-radius: 130px;
    background: var(--secondary-color);
}

.prompt-form .prompt-input {
    height: 100%;
    width: 100%;
    background: none;
    outline: none;
    border: none;
    font-size: 1rem;
    padding-left: 24px;
    color: var(--text-color);
}

.prompt-form .prompt-input::placeholder {
    color: var(--placeholder-color);
}

.prompt-wrapper button {
    width: 56px;
    height: 100%;
    border: none;
    cursor: pointer;
    border-radius: 50%;
    font-size: 1.4rem;
    flex-shrink: 0;
    color: var(--text-color);
    background: var(--secondary-color);
    transition: 0.3s ease;
}

.prompt-wrapper :is(button:hover, .file-icon, #cancel-file-btn) {
    background: var(--secondary-hover-color);
}

.prompt-form .prompt-actions {
    gap: 5px;
    margin-right: 7px;
}

.prompt-wrapper .prompt-form :where(.file-upload-wrapper, button, img) {
    position: relative;
    height: 45px;
    width: 45px;
}

.prompt-form #send-prompt-btn {
    color: #fff;
    display: none;
    /* 隐藏按钮,使其在页面上不可见 */
    background: #1d7efd;
}

/* 当 .prompt-form 容器中的 .prompt-input 元素通过有效性验证时,
其后续兄弟元素 .prompt-actions 内的 #send-prompt-btn 按钮将显示为块级元素 */
.prompt-form .prompt-input:valid ~ .prompt-actions #send-prompt-btn {
    display: block;
}

.prompt-form #send-prompt-btn:hover {
    background: #0264e3;
}

.prompt-form .file-upload-wrapper :where(button, img) {
    position: absolute;
    border-radius: 50%;
    object-fit: cover;
    display: none;
}

.prompt-form .file-upload-wrapper #add-file-btn,
.prompt-form .file-upload-wrapper.active.img-attached img,
.prompt-form .file-upload-wrapper.active.file-attached .file-icon,
.prompt-form .file-upload-wrapper.active:hover
#cancel-file-btn {
    display: block;
}
/* 当.prompt-form .file-upload-wrapper中的#add-file-btn存在时,显示该按钮。 */
/* 当.file-upload-wrapper处于.active.img-attached状态时,显示其中的img元素。 */
/* 当.file-upload-wrapper处于.active.file-attached状态时,显示其中的.file-icon元素。 */
/* 当.file-upload-wrapper.active被悬停时,显示#cancel-file-btn按钮。 */


.prompt-form .file-upload-wrapper.active #add-file-btn {
    display: none;
}

.prompt-form :is(#cancel-file-btn, #stop-response-btn:hover) {
    color: #d62939;
}
/* 当鼠标悬停在ID为stop-response-btn的元素上,或者针对ID为cancel-file-btn的元素时,
将这些元素的文字颜色设置为红色 */

.prompt-form .file-icon {
    color: #1d7efd;
}

.prompt-form #stop-response-btn,
body.bot-responding .prompt-form .file-upload-wrapper {
    display: none;
}

body.bot-responding .prompt-form #stop-response-btn {
    display: block;
}

.prompt-container .disclaimer-text {
    text-align: center;
    font-size: 0.9rem;
    padding: 16px 20px 0;
    color: var(--placeholder-color);
}

/* Responsive media query code for small screens */
@media (max-width: 768px) {
    .container {
        padding: 20px 0 100px;
    }

    .app-header :is(.heading, .sub-heading) {
        font-size: 2rem;
        line-height: 1.4;
    }

    .prompt-form .file-upload-wrapper.active #cancel-file-btn {
        opacity: 0;
    }

    .prompt-wrapper.hide-controls :where(#theme-toggle-btn, #delete-chats-btn){
        display: none;
    }
    /* 当 .prompt-wrapper 具有 hide-controls 类时,#theme-toggle-btn 和 #delete-chats-btn 隐藏 */
}

3. script.js

javascript 复制代码
const container = document.querySelector('.container'); // 聊天容器
const chatsContainer = document.querySelector('.chats-container'); // 存储所有聊天消息的容器
const promptForm = document.querySelector('.prompt-form'); // 用户输入表单
const promptInput = promptForm.querySelector('.prompt-input'); // 输入框
const fileInput = promptForm.querySelector('#file-input'); // 文件上传输入框
const fileUploadWrapper = promptForm.querySelector('.file-upload-wrapper'); // 文件上传的 UI 包装容器
const themeToggle = document.querySelector('#theme-toggle-btn');

// API Setup
const API_KEY = "";
const API_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${API_KEY}`;

let typingInterval, controller; // 全局变量,作用域覆盖整个脚本
const chatHistory = [];
const userData = { message: "", file: {} }; // 存储当前用户消息和上传的文件

// Function to create message elements
// 接收内容和多个类名作为参数,创建div元素后,为其添加类名并设置内容
const createMsgElement = (content, ...classes) => {
    const div = document.createElement('div');
    div.classList.add("message", ...classes)
    div.innerHTML = content;
    return div;
}

// Scroll to the bottom of the container
// 让聊天窗口自动滚动到底部,以保持最新消息可见
// 将容器的滚动条位置设置为容器的最大滚动高度,即滚动到容器的最底部
const scrollToBottom = () => container.scrollTo({ top: container.scrollHeight, behavior: "smooth"});

// Simulate typing effect for bot responses
const typingEffect = (text, textElement, botMsgDiv) => {
    textElement.textContent = "";
    const words = text.split(" ");
    let wordIndex = 0;

    // Set an interval to type each word
    // 每隔 40 毫秒执行一次
    typingInterval = setInterval(() => {
        if(wordIndex < words.length){
            textElement.textContent += (wordIndex === 0 ? "" : " ") + words[wordIndex++];
            document.body.classList.add("bot-responding");
            scrollToBottom();
            // 为 body 添加 bot-responding 类,表示机器人正在响应,并调用 scrollToBottom 滚动到容器底部
        } else {
            // 如果所有单词都显示完毕,清除定时器
            clearInterval(typingInterval);
            botMsgDiv.classList.remove("loading"); // remove the loading animation only after all the words are typed
            document.body.classList.remove("bot-responding");
        }
    }, 40);
}

// Make the API call and generate the bot's response
const generateResponse = async (botMsgDiv) => {
    const textElement = botMsgDiv.querySelector(".message-text");
    controller = new AbortController();  // 用于控制请求的中断

    // Add user message and file data to the chat history
    chatHistory.push({
        role: "user",
        parts: [{ text: userData.message }, 
            ...(userData.file.data ? 
                [{ inline_data: (({ fileName, isImage, ...rest }) => rest)(userData.file) }] : [])]
    });
    // 从 userData.file 对象中提取除 fileName 和 isImage 之外的其他属性,并将其赋值给 inline_data

    try {
        // Send the chat history to the API to get a response
        const response = await fetch(API_URL, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ contents: chatHistory }),
            signal: controller.signal // attach the controller to terminate the fetch request
        });

        const data = await response.json();
        if(!response.ok) throw new Error(data.error.message);

        // Process the response text and display with typing effect
        const responseText = data.candidates[0].content.parts[0].text.replace(/\*\*([^*]+)\*\*/g, "$1").trim();
        // 匹配以 ** 开头和结尾的内容,([^*]+) 捕获 ** 之间的内容。
        typingEffect(responseText, textElement, botMsgDiv);
        
        chatHistory.push({
            role: "model",
            parts: [{ text: responseText }]
        });
    } catch (error) {
        textElement.style.color = "#d62939";
        textElement.textContent = error.name === "AbortError" ? "Response generation stopped." : error.message;
        botMsgDiv.classList.remove("loading"); // remove the loading animation only after all the words are typed
        document.body.classList.remove("bot-responding");
        scrollToBottom();
    } finally {
        userData.file = {};
    }
}
// Handle the form submission
const handleFormSubmit = (e) => {
    // 阻止表单的默认提交行为
    e.preventDefault();
    // 从输入框中获取用户输入并去除首尾空格
    const userMessage = promptInput.value.trim();
    // 如果用户输入为空或机器人正在响应,则直接返回
    if(!userMessage || document.body.classList.contains("bot-responding")) return;

    // 将输入框的值清空
    promptInput.value = "";
    // 将用户输入的消息存储到 userData 对象中
    userData.message = userMessage;
    // 机器人正在响应且聊天处于活跃状态
    document.body.classList.add("bot-responding", "chats-active");
    // 移除文件上传相关的 UI 状态类
    fileUploadWrapper.classList.remove("active", "img-attached", "file-attached");

    // Generate user message HTML with optional file attachment
    // 根据用户输入和文件附件生成用户消息的 HTML 内容
    const userMsgHTML = `
        <p class="message-text"></p>
        ${userData.file.data ? (userData.file.isImage ? 
            `<img src="data:${userData.file.mime_type};base64,${userData.file.data}" class="img-attachment" />` : 
            `<p class="file-attachment"><span class="material-symbols-rounded">description</span>${userData.file.fileName}</p>`
        ) : ""}
    `;

    // 创建用户消息的 DOM 元素,并将其添加到聊天容器中。
    const userMsgDiv = createMsgElement(userMsgHTML, "user-message");
    userMsgDiv.querySelector(".message-text").textContent = userMessage;
    chatsContainer.appendChild(userMsgDiv);
    scrollToBottom();  // 使聊天窗口自动滚动到底部,确保最新消息可见。

    // 在 600 毫秒后生成机器人消息的 HTML 内容,并将其添加到聊天容器中
    setTimeout(() => {
        // Generate bot message HTML and add in the chats container after 600ms
        const botMsgHTML = `<img src="gemini-chatbot-logo.svg" class="avatar"><p class="message-text">Just a sec..</p>`;
        const botMsgDiv = createMsgElement(botMsgHTML, "bot-message", "loading");
        chatsContainer.appendChild(botMsgDiv);
        scrollToBottom();
        generateResponse(botMsgDiv);
}, 600);
}

// Handle file input change (file upload)
fileInput.addEventListener("change", () => {
    // 从文件输入框中获取用户选择的第一个文件
    const file = fileInput.files[0];
    if(!file) return;

    // 检查文件是否为图片类型
    const isImage = file.type.startsWith("image/");
    // 使用 FileReader 对象读取文件内容,并将其转换为 Data URL 格式
    const reader = new FileReader();
    reader.readAsDataURL(file);

    // 处理读取完成事件
    reader.onload = (e) => {
        // 清空文件输入框的值
        fileInput.value = "";
        // 将 Data URL 转换为 Base64 字符串
        const base64String = e.target.result.split(",")[1]
        // 更新文件预览的 src 属性,显示文件内容
        fileUploadWrapper.querySelector(".file-preview").src = e.target.result;
        // 根据文件类型(图片或非图片)为文件上传包装容器添加相应的类名
        fileUploadWrapper.classList.add("active", isImage ? "img-attached" : "file-attached");

        // Store file data in userData obj
        userData.file = {fileName: file.name, data: base64String, mime_type: file.type, isImage }
    }
});

// Cancel file upload
document.querySelector("#cancel-file-btn").addEventListener("click", () => {
    // 清空文件数据
    userData.file = {};
    // 移除文件上传 UI 状态
    fileUploadWrapper.classList.remove("active", "img-attached", "file-attached");
})

// Stop ongoing bot response
document.querySelector("#stop-response-btn").addEventListener("click", () => {
    // 清空文件数据
    userData.file = {};
    // 中止 API 请求
    controller?.abort();
    // 清除机器人打字效果的定时器,停止打字动画
    clearInterval(typingInterval)
    // 移除加载状态
    chatsContainer.querySelector(".bot-message.loading").classList.remove("loading");
    // 移除机器人响应状态 
    document.body.classList.remove("bot-responding");
})

// Delete all chats
document.querySelector("#delete-chats-btn").addEventListener("click", () => {
    chatHistory.length = 0;
    chatsContainer.innerHTML = "";
    document.body.classList.remove("bot-responding", "chats-active");
})

// Handle suggestions click
document.querySelectorAll(".suggestions-item").forEach(item => {
    item.addEventListener("click", () => {
        // 当用户点击某个建议项时,将该建议项的内容填充到输入框中
        promptInput.value = item.querySelector(".text").textContent;
        // 触发表单的 submit 事件,从而模拟用户提交表单的行为,启动聊天流程
        promptForm.dispatchEvent(new Event("submit"));
    })
})

// Show/hide controls for mobile on prompt input focus
document.addEventListener("click", ({ target}) => {
    const wrapper = document.querySelector(".prompt-wrapper");
    // 如果点击的目标是输入框,则需要隐藏控制按钮。
    const shouldHide = target.classList.contains("prompt-input") || 
    (wrapper.classList.contains("hide-controls") && 
        (target.id === "add-file-btn" || target.id === "stop-response-btn"));
    // 如果控制按钮已经隐藏 (wrapper.classList.contains("hide-controls")),
    // 并且点击的目标是"添加文件"按钮 (target.id === "add-file-btn") 
    // 或"停止响应"按钮 (target.id === "stop-response-btn"),也需要隐藏控制按钮。
    wrapper.classList.toggle("hide-controls", shouldHide);
    // 切换控制按钮的显示状态
});

// Toggle dark/light theme
themeToggle.addEventListener("click", () => {
    // 如果 light-theme 类被添加到 body 元素中,则 isLightTheme 的值为 true,表示当前是浅色主题。
    const isLightTheme = document.body.classList.toggle("light-theme");
    // 将当前主题状态(isLightTheme)存储到 localStorage
    localStorage.setItem("themeColor", isLightTheme ? "light_mode" : "dark_mode")
    // 更新按钮文本
    themeToggle.textContent = isLightTheme ? "dark_mode" : "light_mode";
});

// Set initial theme from local storage
// 从本地存储中获取主题状态
const isLightTheme = localStorage.getItem("themeColor") === "light_mode";
document.body.classList.toggle("light-theme", isLightTheme);
themeToggle.textContent = isLightTheme ? "dark_mode" : "light_mode";

// 当用户提交表单时,调用 handleFormSubmit 函数。
promptForm.addEventListener("submit", handleFormSubmit);
// 模拟用户点击文件输入框,打开文件选择对话框
promptForm.querySelector("#add-file-btn").addEventListener("click", () => fileInput.click());
// 用户选择文件后,触发 fileInput 的 change 事件,执行文件上传的相关逻辑。

这段代码实现了:

✅ AI 聊天交互

✅ 打字机效果

✅ 文件上传支持

✅ 深色/浅色模式

✅ 响应管理和错误处理

相关推荐
高工智能汽车9 分钟前
Deepseek浪潮下,汽车芯片开启“大变局”,谁将领跑?
人工智能·汽车
Loving_enjoy16 分钟前
SEARCH-R1:大型语言模型的多轮搜索推理革命
语言模型
蜗牛沐雨19 分钟前
RAG 技术:让大型语言模型更智能
人工智能·语言模型·自然语言处理
夏莉莉iy1 小时前
[CVPR 2025]Neuro-3D: Towards 3D Visual Decoding from EEG Signals
人工智能·python·深度学习·神经网络·机器学习·3d·线性回归
learn_think1 小时前
pytorch小土堆学习有感
人工智能·pytorch·学习
晨航1 小时前
清华大学第12弹:《DeepSeek政务应用场景与解决方案》.pdf(文末附免费下载地址)
人工智能·ai·aigc·政务
果冻人工智能1 小时前
自主代理的摩尔定律:AI 的指数级革命
人工智能
积木链小链1 小时前
智能制造:能源监控项目实战详解
人工智能·智能制造·数字化转型
帅小柏1 小时前
《声音的未来:语音识别文献解读》专栏介绍及其文章解读目录
人工智能·语音识别
代码骑士2 小时前
支持向量机(Support Vector Machine)基础知识2
人工智能·机器学习·支持向量机