<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>超级AI助手</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,0,0"
/>
<link
href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css"
rel="stylesheet"
/>
<script src="https://cdn.bootcdn.net/ajax/libs/FileSaver.js/2.0.5/FileSaver.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<style>
/* 基础样式和变量 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Poppins", sans-serif;
outline: none;
}
:root {
/* 深色模式颜色 (默认) */
--text-color: #e3e3e3;
--subheading-color: #828282;
--placeholder-color: #a6a6a6;
--primary-color: #242424;
--secondary-color: #383838;
--secondary-hover-color: #444;
--user-message-color: #4285f4;
--ai-message-color: #383838;
/* 终端工具颜色 */
--editor-bg: #1e293b;
--editor-secondary-bg: #334155;
--line-color: #64748b;
--keyword-color: #7dd3fc;
--string-color: #fca5a5;
--comment-color: #86efac;
--number-color: #b5cea8;
--function-color: #dcdcaa;
--ai-output-color: #67e8f9;
--user-input-color: #fde047;
--accent-color: #818cf8;
--accent-hover: #a5b4fc;
--border-radius: 8px;
--calendar-today-bg: rgba(129, 140, 248, 0.3);
--calendar-selected-bg: var(--accent-color);
--birthday-color: #f472b6;
}
.light_mode {
/* 浅色模式颜色 */
--text-color: #222;
--subheading-color: #a0a0a0;
--placeholder-color: #6c6c6c;
--primary-color: #fff;
--secondary-color: #e9eef6;
--secondary-hover-color: #dbe1ea;
--user-message-color: #4285f4;
--ai-message-color: #e9eef6;
/* 终端工具颜色 (浅色模式适应) */
--editor-bg: #f8fafc;
--editor-secondary-bg: #e2e8f0;
--line-color: #94a3b8;
--keyword-color: #0ea5e9;
--string-color: #ef4444;
--comment-color: #22c55e;
--number-color: #a855f7;
--function-color: #f97316;
--ai-output-color: #06b6d4;
--user-input-color: #eab308;
--accent-color: #6366f1;
--accent-hover: #818cf8;
--calendar-today-bg: rgba(99, 102, 241, 0.3);
--calendar-selected-bg: var(--accent-color);
--birthday-color: #ec4899;
}
body {
background: var(--primary-color);
color: var(--text-color);
display: flex;
flex-direction: column;
min-height: 100vh;
overflow-x: hidden; /* 防止横向滚动条 */
}
/* 头部样式 */
.header {
margin: 6vh auto 0;
padding: 1rem;
max-width: 980px;
overflow-x: hidden;
width: 100%;
transition: margin 0.3s ease, opacity 0.3s ease;
}
body.hide-header .header {
margin: 0;
height: 0;
opacity: 0;
overflow: hidden;
padding: 0;
}
.header .title {
width: fit-content;
font-size: 3rem;
font-weight: 500;
line-height: 4rem;
background-clip: text;
background: linear-gradient(to right, #4285f4, #d96570);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header .subtitle {
font-size: 2.6rem;
font-weight: 500;
line-height: 4rem;
color: var(--subheading-color);
}
.suggestion-list {
width: 100%;
list-style: none;
display: flex;
gap: 1.25rem;
margin-top: 9.5vh;
overflow: hidden;
overflow-x: auto;
scroll-snap-type: x mandatory;
scrollbar-width: none; /* 适用于 Firefox */
}
.suggestion-list::-webkit-scrollbar {
display: none; /* 适用于 Chrome, Safari, Opera */
}
.suggestion-list .suggestion {
cursor: pointer;
padding: 1.25rem;
width: 222px;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: flex-end;
border-radius: 0.75rem;
justify-content: space-between;
background: var(--secondary-color);
transition: 0.2s ease;
}
.suggestion-list .suggestion:hover {
background: var(--secondary-hover-color);
}
.suggestion-list .suggestion .text {
font-weight: 400;
color: var(--text-color);
}
.suggestion-list .suggestion .icon {
width: 42px;
height: 42px;
display: flex;
font-size: 1.3rem;
margin-top: 2.5rem;
align-self: flex-end;
align-items: center;
border-radius: 50%;
justify-content: center;
color: var(--text-color);
background: var(--primary-color);
}
/* 聊天列表样式 */
.chat-list {
flex: 1;
padding: 2rem 1rem 12rem;
max-height: 100vh; /* 确保它不会溢出视口 */
overflow-y: auto;
scrollbar-color: #999 transparent; /* 适用于 Firefox */
transition: padding 0.3s ease;
}
.chat-list::-webkit-scrollbar {
width: 8px;
}
.chat-list::-webkit-scrollbar-track {
background: transparent;
}
.chat-list::-webkit-scrollbar-thumb {
background-color: #999;
border-radius: 4px;
}
.chat-list .message {
max-width: 980px;
margin: 0 auto 1.5rem;
display: flex;
gap: 1.5rem;
width: 100%;
align-items: flex-start; /* 内容顶部对齐 */
}
.chat-list .message.user {
justify-content: flex-end; /* 用户消息靠右 */
}
.chat-list .message.ai {
justify-content: flex-start; /* AI消息靠左 */
}
.chat-list .message .message-content {
display: flex;
gap: 1.5rem;
max-width: 80%;
align-items: flex-start;
}
.chat-list .message.user .message-content {
flex-direction: row-reverse; /* 用户消息头像在右边 */
}
.chat-list .message.ai .message-content {
flex-direction: row; /* AI消息头像在左边 */
}
.chat-list .message .avatar {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 50%;
flex-shrink: 0; /* 防止头像缩小 */
}
.chat-list .message.loading .avatar {
animation: rotate 3s linear infinite;
}
@keyframes rotate {
100% {
transform: rotate(360deg);
}
}
.chat-list .message .icon {
color: var(--text-color);
cursor: pointer;
height: 35px;
width: 35px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: none;
font-size: 1.25rem;
margin-left: 3.5rem;
visibility: hidden;
transition: background 0.2s ease;
flex-shrink: 0; /* 防止图标缩小 */
}
.chat-list .message .icon.hide {
visibility: hidden;
}
.chat-list .message:not(.loading, .error):hover .icon:not(.hide) {
visibility: visible;
}
.chat-list .message .icon:hover {
background: var(--secondary-hover-color);
}
/* 消息气泡样式 */
.message-bubble {
padding: 12px 18px;
border-radius: 18px;
max-width: 100%; /* 限制气泡宽度 */
word-wrap: break-word;
display: flex;
flex-direction: column;
gap: 8px; /* 气泡内元素间距 */
position: relative; /* 用于气泡尾巴 */
}
.chat-list .message.user .message-bubble {
background-color: var(--user-message-color); /* 用户消息气泡颜色 */
color: white;
border-bottom-right-radius: 4px; /* 气泡尾巴效果 */
}
.chat-list .message.ai .message-bubble {
background-color: var(--ai-message-color); /* AI消息气泡颜色 */
color: var(--text-color);
border-bottom-left-radius: 4px; /* 气泡尾巴效果 */
}
/* 气泡内的文本 */
.message-bubble .text {
white-space: pre-wrap; /* 保留空白和换行符 */
font-size: 1rem;
line-height: 1.5;
}
.message-bubble .text.error {
color: #e55865;
}
/* 加载指示器 */
.loading-indicator {
display: flex;
flex-direction: column;
gap: 0.8rem;
width: 100%;
}
.message.loading .loading-indicator {
display: flex;
}
.loading-indicator .loading-bar {
height: 11px;
width: 100%;
border-radius: 0.135rem;
background-position: -800px 0;
background: linear-gradient(to right, #4285f4, var(--primary-color), #4285f4);
animation: loading 3s linear infinite;
}
.loading-indicator .loading-bar:last-child {
width: 70%;
}
@keyframes loading {
0% {
background-position: -800px 0;
}
100% {
background-position: 800px 0;
}
}
/* 工具/组件气泡的特殊处理 */
.message-tool-bubble {
padding: 0; /* 工具容器自身有内边距 */
background: transparent; /* 让工具容器自身定义背景 */
border-radius: 0; /* 让工具容器自身定义圆角 */
max-width: 100%; /* 允许工具容器占据气泡的全部宽度 */
box-shadow: none; /* 移除冗余阴影 */
border: none; /* 移除冗余边框 */
}
/* 输入区域样式 */
.typing-area {
position: fixed;
width: 100%;
left: 0;
bottom: 0;
padding: 1rem;
background: var(--primary-color);
z-index: 10;
}
.typing-area .typing-form,
.typing-area .action-buttons {
display: flex;
gap: 0.75rem;
}
.typing-area .typing-form {
max-width: 980px;
margin: 0 auto;
}
.typing-form .input-wrapper {
width: 100%;
height: 56px;
display: flex;
position: relative;
}
.typing-form .typing-input {
height: 100%;
width: 100%;
border: none;
outline: none;
resize: none;
font-size: 1rem;
color: var(--text-color);
padding: 1.1rem 4rem 1.1rem 1.5rem;
border-radius: 100px;
background: var(--secondary-color);
overflow: hidden; /* 隐藏滚动条 */
}
.typing-form .typing-input:focus {
background: var(--secondary-hover-color);
}
.typing-form .typing-input::placeholder {
color: var(--placeholder-color);
}
.typing-area .icon {
width: 56px;
height: 56px;
flex-shrink: 0;
cursor: pointer;
border-radius: 50%;
display: flex;
font-size: 1.4rem;
color: var(--text-color);
align-items: center;
justify-content: center;
background: var(--secondary-color);
transition: 0.2s ease;
border: none; /* 确保按钮没有默认边框 */
outline: none; /* 确保按钮没有默认轮廓 */
}
.typing-area .icon:hover {
background: var(--secondary-hover-color);
}
.typing-form #send-message-button {
position: absolute;
right: 0;
transform: scale(0);
background: transparent;
transition: transform 0.2s ease;
}
.typing-form .typing-input:valid ~ #send-message-button {
transform: scale(1);
}
.typing-area .disclaimer-text {
text-align: center;
font-size: 0.85rem;
margin-top: 1rem;
color: var(--placeholder-color);
}
/* 响应式媒体查询 */
@media (max-width: 768px) {
.header :is(.title, .subtitle) {
font-size: 2rem;
line-height: 2.6rem;
}
.header .subtitle {
font-size: 1.7rem;
}
.typing-area :where(.typing-form, .action-buttons) {
gap: 0.4rem;
}
.typing-form .input-wrapper {
height: 50px;
}
.typing-form .typing-input {
padding: 1.1rem 3.5rem 1.1rem 1.2rem;
}
.typing-area .icon {
height: 50px;
width: 50px;
}
.typing-area .disclaimer-text {
font-size: 0.75rem;
margin-top: 0.5rem;
}
.message-bubble {
max-width: 85%; /* 小屏幕上气泡宽度更大 */
}
}
/* 图片预览样式 */
.image-preview {
max-width: 100%;
max-height: 300px;
margin-top: 10px;
border-radius: 8px;
object-fit: contain;
cursor: pointer;
transition: transform 0.2s ease;
}
.image-preview:hover {
transform: scale(1.02);
}
/* ================ 统一工具样式 ================ */
.tool-container {
margin-top: 15px;
padding: 15px;
background: var(--editor-secondary-bg);
border-radius: var(--border-radius);
border: 1px solid rgba(255, 255, 255, 0.1);
width: 100%;
}
.tool-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
color: var(--text-color);
display: flex;
align-items: center;
gap: 8px;
}
.tool-options {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.tool-btn {
padding: 8px 16px;
background-color: var(--secondary-color);
color: var(--text-color);
border-radius: 6px;
border: none;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 6px;
}
.tool-btn:hover {
background-color: var(--secondary-hover-color);
}
.tool-btn.active {
background-color: var(--accent-color);
color: white;
}
.tool-btn i {
font-size: 16px;
}
.tool-control {
margin-top: 12px;
}
.tool-label {
display: flex;
justify-content: space-between;
font-size: 12px;
color: var(--subheading-color);
margin-bottom: 5px;
}
.tool-slider {
width: 100%;
height: 6px;
-webkit-appearance: none;
appearance: none;
background: var(--secondary-color);
border-radius: 3px;
outline: none;
}
.tool-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--accent-color);
cursor: pointer;
transition: all 0.2s;
}
.tool-slider::-webkit-slider-thumb:hover {
transform: scale(1.1);
}
.tool-action {
margin-top: 15px;
display: flex;
gap: 10px;
}
.tool-action-btn {
padding: 8px 16px;
border-radius: 6px;
border: none;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 6px;
}
.tool-primary-btn {
background-color: var(--accent-color);
color: white;
}
.tool-primary-btn:hover {
background-color: var(--accent-hover);
}
.tool-secondary-btn {
background-color: var(--secondary-color);
color: var(--text-color);
}
.tool-secondary-btn:hover {
background-color: var(--secondary-hover-color);
}
.tool-progress {
width: 100%;
height: 6px;
background-color: var(--editor-secondary-bg);
border-radius: 3px;
margin-top: 15px;
overflow: hidden;
display: none;
}
.tool-progress-bar {
height: 100%;
width: 0%;
background-color: var(--accent-color);
transition: width 0.3s ease;
}
.tool-result {
margin-top: 15px;
padding: 15px;
background: var(--editor-secondary-bg);
border-radius: var(--border-radius);
border: 1px solid rgba(255, 255, 255, 0.1);
display: none;
}
.tool-result-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
color: var(--text-color);
display: flex;
align-items: center;
gap: 8px;
}
.tool-meta {
display: flex;
gap: 15px;
margin-top: 10px;
font-size: 12px;
color: var(--subheading-color);
}
.tool-meta-item {
display: flex;
align-items: center;
gap: 5px;
}
.download-btn {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 12px;
padding: 8px 16px;
background-color: var(--accent-color);
color: white;
border-radius: 6px;
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
}
.download-btn:hover {
background-color: var(--accent-hover);
transform: translateY(-1px);
}
/* 视频播放器样式 */
.video-player-container {
width: 100%;
margin-bottom: 15px;
}
.video-player-container iframe {
width: 100%;
height: 400px;
border-radius: var(--border-radius);
border: none;
}
/* 天气信息样式 */
.weather-info {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-top: 15px;
}
.weather-item {
display: flex;
flex-direction: column;
padding: 12px;
background: rgba(255, 255, 255, 0.05);
border-radius: var(--border-radius);
}
.weather-item-label {
font-size: 12px;
color: var(--subheading-color);
margin-bottom: 5px;
}
.weather-item-value {
font-size: 14px;
font-weight: 500;
color: var(--text-color);
}
/* 日历样式 */
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
margin-top: 15px;
}
.calendar-weekday {
font-size: 12px;
color: var(--subheading-color);
text-align: center;
padding: 5px;
}
.calendar-day {
padding: 8px;
text-align: center;
border-radius: var(--border-radius);
cursor: pointer;
transition: all 0.2s;
}
.calendar-day:hover {
background-color: var(--secondary-hover-color);
}
.calendar-day.today {
background-color: var(--calendar-today-bg);
}
.calendar-day.selected {
background-color: var(--calendar-selected-bg);
color: white;
}
.calendar-day.birthday {
position: relative;
}
.calendar-day.birthday::after {
content: "🎂";
position: absolute;
top: -5px;
right: -5px;
font-size: 12px;
}
.calendar-day.other-month {
opacity: 0.5;
}
/* 全屏图片预览 */
.fullscreen-image-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
cursor: zoom-out;
}
.fullscreen-image-overlay img {
max-width: 90%;
max-height: 90%;
border-radius: 10px;
object-fit: contain;
}
</style>
</head>
<body>
<!-- 顶部区域:标题和快捷建议 -->
<header class="header">
<h1 class="title">你好</h1>
<p class="subtitle">今天有什么可以帮您的吗?</p>
<ul class="suggestion-list">
<li class="suggestion" data-command="帮我用100元预算为5个好朋友策划一个游戏之夜。">
<h4 class="text">帮我用100元预算为5个好朋友策划一个游戏之夜。</h4>
<span class="icon material-symbols-rounded">stadia_controller</span>
</li>
<li class="suggestion" data-command="提高公开演讲技巧的最佳建议是什么?">
<h4 class="text">提高公开演讲技巧的最佳建议是什么?</h4>
<span class="icon material-symbols-rounded">lightbulb</span>
</li>
<li class="suggestion" data-command="你能帮我找到关于网页开发的最新新闻吗?">
<h4 class="text">你能帮我找到关于网页开发的最新新闻吗?</h4>
<span class="icon material-symbols-rounded">travel_explore</span>
</li>
<li class="suggestion" data-command="编写JavaScript代码计算数组中所有元素的和。">
<h4 class="text">编写JavaScript代码计算数组中所有元素的和。</h4>
<span class="icon material-symbols-rounded">code</span>
</li>
</ul>
</header>
<!-- 中部区域:聊天记录 -->
<div class="chat-list"></div>
<!-- 底部区域:输入框和功能按钮 -->
<div class="typing-area">
<form class="typing-form">
<div class="input-wrapper">
<textarea
placeholder="在此输入您的提示或命令..."
class="typing-input"
required
rows="1"
></textarea>
<button id="send-message-button" class="icon material-symbols-rounded">
send
</button>
</div>
<div class="action-buttons">
<button
type="button"
id="theme-toggle-button"
class="icon material-symbols-rounded"
>
light_mode
</button>
<button
type="button"
id="delete-chat-button"
class="icon material-symbols-rounded"
>
delete
</button>
<input type="file" id="image-upload" accept="image/*" style="display: none;" />
<button
type="button"
id="upload-image-button"
class="icon material-symbols-rounded"
>
image
</button>
</div>
</form>
<p class="disclaimer-text">
超级AI助手可能会显示不准确的信息,包括关于人物的信息,请仔细检查其回复。
<br />
**注意:此版本使用预定义回复和模拟工具。如需真正的AI功能,需要后端服务器支持。**
</p>
</div>
<script>
// 获取DOM元素
const typingForm = document.querySelector(".typing-form");
const typingInput = typingForm.querySelector(".typing-input");
const chatContainer = document.querySelector(".chat-list");
const suggestions = document.querySelectorAll(".suggestion");
const toggleThemeButton = document.querySelector("#theme-toggle-button");
const deleteChatButton = document.querySelector("#delete-chat-button");
const uploadImageButton = document.querySelector("#upload-image-button");
const imageUploadInput = document.querySelector("#image-upload");
const header = document.querySelector(".header");
const sendMessageButton = document.querySelector("#send-message-button");
// 状态变量
let messages = []; // 存储聊天记录的数组
let isResponseGenerating = false;
let commandHistoryArray = JSON.parse(localStorage.getItem("commandHistory") || "[]");
let historyIndex = -1;
let totalParseCount = parseInt(localStorage.getItem("totalParseCount") || "0");
let successParseCount = parseInt(localStorage.getItem("successParseCount") || "0");
let currentConversionSettings = {
format: "png",
quality: 0.8,
resize: null
};
// API配置 (模拟,实际需要后端)
const videoApis = [
{ name: "默认线路【推荐】", url: "https://jx.xmflv.com/?url=" },
{ name: "线路一【推荐】", url: "https://jx.we-vip.com/?url=" },
{ name: "线路二【推荐】", url: "https://z1.m1907.top/?jx=" },
{ name: "线路三", url: "https://www.ckplayer.vip/jiexi/?url=" },
{ name: "线路五", url: "https://jx.jsonplayer.com/player/?url=" },
{ name: "线路六", url: "https://jx.4kdv.com/?url=" },
{ name: "线路七", url: "https://api.jiexi.la/?url=" },
{ name: "线路八", url: "https://jx.qqwtt.com/?url=" },
{ name: "线路九", url: "https://www.playm3u8.cn/jiexi.php?url=" },
{ name: "线路十", url: "https://www.8090g.cn/jiexi/?url=" },
];
// 预定义回复 (已扩展,用于AI命令)
const predefinedResponses = {
"帮我用100元预算为5个好朋友策划一个游戏之夜。":
"为5个好朋友策划一个100元预算的游戏之夜,可以考虑以下几点:\n\n1. **食物和饮料 (约50元):** 自制披萨、零食 (薯片、爆米花),以及各种汽水或经济实惠的潘趣酒。\n2. **游戏 (约30元):** 纸牌游戏 (例如:Uno、真心话大冒险 - 如果合适)、棋盘游戏 (例如:只言片语、爆炸猫),或者免费的在线派对游戏。\n3. **装饰/氛围 (约10元):** 串灯、有趣的播放列表,以及一些DIY拍照道具。\n4. **奖品 (约10元):** 为游戏赢家准备一些小而有趣的奖品,如糖果、迷你礼品卡或搞怪奖杯。\n\n记得去当地的折扣店或一元店寻找经济实惠的选择!",
"提高公开演讲技巧的最佳建议是什么?":
"要提高公开演讲技巧,请关注以下几个关键领域:\n\n1. **定期练习:** 练习得越多,你就会越自在。对着镜子、朋友练习,或者录下自己。\n2. **了解你的听众:** 根据他们的兴趣和知识水平调整你的信息。\n3. **组织你的演讲:** 拥有清晰的引言、主体和结论。使用过渡语来引导听众。\n4. **与听众互动:** 进行眼神交流,使用手势,并改变你的语调和节奏。\n5. **管理紧张情绪:** 深呼吸练习、积极的想象和提前到达可以帮助缓解演讲前的紧张。\n6. **寻求反馈:** 向信任的朋友或导师寻求建设性的批评。",
"你能帮我找到关于网页开发的最新新闻吗?":
"虽然我无法实时访问最新的新闻,但我可以告诉你,当前网页开发的热门话题通常包括:\n\n* **框架更新:** React、Next.js、Vue 和 Angular 的新版本。\n* **服务器组件:** React 服务器组件的演变和采用,以提高性能。\n* **AI在Web开发中的应用:** 集成AI用于代码生成、测试和内容创建的工具和库。\n* **WebAssembly (Wasm):** 其在高性能Web应用中的日益增长的使用。\n* **CSS创新:** 新的CSS特性,如容器查询、`:has()` 和改进的布局技术。\n* **可访问性 (A11y):** 持续强调构建包容性的Web体验。\n\n如需实时更新,我建议查看流行的科技新闻网站、开发者博客和社区,如 Dev.to、Hashnode、Smashing Magazine 和 Twitter/X。",
"编写JavaScript代码计算数组中所有元素的和。": `function sumArray(arr) {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}
// 示例用法:
const numbers = [1, 2, 3, 4, 5];
const total = sumArray(numbers);
console.log(total); // 输出: 15`,
"image": "请上传图片,我将帮您转换格式。支持以下格式:\n- PNG (无损)\n- JPEG (有损压缩)\n- WEBP (现代格式)\n\n上传后可以选择转换选项。",
default:
"我是一个简单的AI助手,目前使用预定义回复。请尝试其他提示或输入 'help' 查看可用命令!",
};
// ================ 辅助函数 ================
// 从本地存储加载聊天记录和主题
const loadFromLocalStorage = () => {
const savedMessages = localStorage.getItem("chatHistory");
if (savedMessages) {
messages = JSON.parse(savedMessages);
messages.forEach((msg) => {
const messageElement = createMessageElement(msg);
chatContainer.appendChild(messageElement);
});
if (messages.length > 0) {
header.classList.add("hide-header");
}
chatContainer.scrollTo(0, chatContainer.scrollHeight);
}
const isLightMode = localStorage.getItem("themeColor") === "light_mode";
document.body.classList.toggle("light_mode", isLightMode);
toggleThemeButton.innerText = isLightMode ? "dark_mode" : "light_mode";
};
// 将聊天记录保存到本地存储
const saveToLocalStorage = () => {
localStorage.setItem("chatHistory", JSON.stringify(messages));
};
// 创建新消息元素并返回
const createMessageElement = (msg) => {
const div = document.createElement("div");
div.classList.add("message", msg.sender === "user" ? "user" : "ai");
div.setAttribute("data-message-id", msg.id); // Add data-message-id for easier selection
const avatarSrc = msg.sender === "user" ? "images/user.jpg" : "images/gemini.svg";
const avatarAlt = msg.sender === "user" ? "用户头像" : "AI头像";
let bubbleContentHtml = "";
let bubbleClasses = "message-bubble"; // Default bubble class
if (msg.type === "loading") {
bubbleContentHtml = `
<p class="text"></p>
<div class="loading-indicator">
<div class="loading-bar"></div>
<div class="loading-bar"></div>
<div class="loading-bar"></div>
</div>
`;
} else if (msg.type === "text" || msg.type === "error") {
bubbleContentHtml = `<p class="text ${msg.type === "error" ? "error" : ""}">${msg.text}</p>`;
if (msg.image && msg.sender === "user") {
bubbleContentHtml += `
<img src="${msg.image}" class="image-preview" alt="上传的图片" onclick="showFullImage('${msg.image}')" />
<div class="tool-container">
<div class="tool-title">
<i class="fas fa-cog"></i> 图片转换选项
</div>
<div class="tool-options">
<button class="tool-btn ${currentConversionSettings.format === "png" ? "active" : ""}" data-format="png" onclick="setImageConversionFormat('png')">
<i class="fas fa-file-image"></i> PNG
</button>
<button class="tool-btn ${currentConversionSettings.format === "jpeg" ? "active" : ""}" data-format="jpeg" onclick="setImageConversionFormat('jpeg')">
<i class="fas fa-file-image"></i> JPEG
</button>
<button class="tool-btn ${currentConversionSettings.format === "webp" ? "active" : ""}" data-format="webp" onclick="setImageConversionFormat('webp')">
<i class="fas fa-file-image"></i> WEBP
</button>
</div>
<div class="tool-control">
<div class="tool-label">
<span>质量: ${Math.round(currentConversionSettings.quality * 100)}%</span>
</div>
<input type="range" min="0.1" max="1" step="0.1" value="${currentConversionSettings.quality}"
class="tool-slider" oninput="setImageQuality(this.value)">
</div>
<div class="tool-progress" id="progress-${msg.id}">
<div class="tool-progress-bar"></div>
</div>
<div class="tool-result" id="result-${msg.id}">
<div class="tool-result-title">
<i class="fas fa-check-circle"></i> 转换完成
</div>
<img src="" class="image-preview" alt="转换后的图片" onclick="showFullImage(this.src)" />
<div class="tool-meta">
<div class="tool-meta-item">
<i class="fas fa-file-format"></i> 格式: <span class="format-value"></span>
</div>
<div class="tool-meta-item">
<i class="fas fa-tachometer-alt"></i> 质量: <span class="quality-value"></span>
</div>
<div class="tool-meta-item">
<i class="fas fa-weight-hanging"></i> 大小: <span class="size-value"></span>
</div>
</div>
<a href="#" class="download-btn" download>
<i class="fas fa-download"></i> 下载图片
</a>
</div>
<div class="tool-action">
<button class="tool-action-btn tool-primary-btn" onclick="convertImage('${msg.id}')">
<i class="fas fa-exchange-alt"></i> 开始转换
</button>
<button class="tool-action-btn tool-secondary-btn" onclick="cancelImageConversion('${msg.id}')">
<i class="fas fa-times"></i> 取消
</button>
</div>
</div>
`;
}
if (msg.convertedImage && msg.sender === "ai") {
bubbleContentHtml += `
<div class="tool-result" style="display: block;">
<div class="tool-result-title">
<i class="fas fa-check-circle"></i> 转换完成
</div>
<img src="${msg.convertedImage}" class="image-preview" alt="转换后的图片" onclick="showFullImage('${msg.convertedImage}')" />
<div class="tool-meta">
<div class="tool-meta-item">
<i class="fas fa-file-format"></i> 格式: ${msg.convertedFormat.toUpperCase()}
</div>
<div class="tool-meta-item">
<i class="fas fa-tachometer-alt"></i> 质量: ${Math.round(msg.convertedQuality * 100)}%
</div>
<div class="tool-meta-item">
<i class="fas fa-weight-hanging"></i> 大小: ${formatFileSize(msg.convertedSize)}
</div>
</div>
<a href="${msg.convertedImage}" download="${msg.originalFileName ? msg.originalFileName.replace(/\.[^/.]+$/, "") + "." + msg.convertedFormat : "converted_image." + msg.convertedFormat}" class="download-btn">
<i class="fas fa-download"></i> 下载图片
</a>
</div>
`;
}
} else if (msg.type === "html" && msg.htmlContent) {
bubbleContentHtml = msg.htmlContent;
bubbleClasses += " message-tool-bubble"; // Add tool bubble specific class
} else if (msg.type === "component" && msg.componentType) {
if (msg.componentType === "weather") {
bubbleContentHtml = renderWeatherComponent(msg.componentData);
} else if (msg.componentType === "calendar") {
bubbleContentHtml = renderCalendarComponent(msg.componentData);
}
bubbleClasses += " message-tool-bubble"; // Add tool bubble specific class
}
let contentHtml = `
<div class="message-content">
<img class="avatar" src="${avatarSrc}" alt="${avatarAlt}">
<div class="${bubbleClasses}">
${bubbleContentHtml}
</div>
</div>
`;
if (msg.sender === "ai" && msg.type !== "loading") {
contentHtml += `
<button onclick="copyMessage(this)" class="icon material-symbols-rounded">content_copy</button>
`;
}
div.innerHTML = contentHtml;
return div;
};
// 添加消息到聊天列表并滚动到底部
const addMessageToChat = (msg) => {
messages.push(msg);
saveToLocalStorage();
const messageElement = createMessageElement(msg);
chatContainer.appendChild(messageElement);
chatContainer.scrollTo(0, chatContainer.scrollHeight);
};
// 显示打字效果,逐字显示
const showTypingEffect = (text, textElement, incomingMessageDiv) => {
let i = 0;
const typingInterval = setInterval(() => {
if (i < text.length) {
textElement.innerText += text.charAt(i);
i++;
chatContainer.scrollTo(0, chatContainer.scrollHeight);
} else {
clearInterval(typingInterval);
isResponseGenerating = false;
incomingMessageDiv.classList.remove("loading");
const copyButton = incomingMessageDiv.querySelector(".icon");
if (copyButton) copyButton.style.visibility = "visible";
}
}, 20); // 调整速度
};
// 复制消息文本到剪贴板
window.copyMessage = (copyButton) => {
const messageText = copyButton.parentElement.querySelector(".text").innerText;
navigator.clipboard.writeText(messageText);
copyButton.innerText = "done"; // 显示确认图标
setTimeout(() => (copyButton.innerText = "content_copy"), 1000); // 1秒后恢复图标
};
// 显示提示信息
function showAlert(message) {
const alert = document.createElement("div");
alert.style.cssText = `
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
background: rgba(30, 41, 59, 0.95);
color: white;
padding: 12px 24px;
border-radius: 25px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
z-index: 9999;
font-size: 14px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
`;
alert.textContent = message;
document.body.appendChild(alert);
setTimeout(() => alert.remove(), 3000);
}
// URL提取函数
function extractURL(text) {
try {
const urlRegex =
/(https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*))/g;
const matches = text.match(urlRegex);
return matches ? matches[0] : null;
} catch (e) {
console.error("URL提取错误:", e);
return null;
}
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// ================ 核心聊天和命令处理 ================
// 处理发送聊天消息或命令
const handleOutgoingChat = async (input) => {
const messageText = input.trim();
if (!messageText || isResponseGenerating) return;
// 添加用户消息到聊天列表
addMessageToChat({
id: `msg-${Date.now()}`,
text: messageText,
sender: "user",
type: "text",
});
typingInput.value = ""; // 清空输入
typingInput.style.height = "auto"; // 重置文本域高度
header.classList.add("hide-header");
// 检查是否是命令
const commandParts = messageText.toLowerCase().split(" ");
const command = commandParts[0];
const args = commandParts.slice(1).join(" ");
// 保存到历史记录
saveToHistory(messageText);
isResponseGenerating = true; // 标记为正在生成响应
// 尝试识别URL进行视频解析
const detectedUrl = extractURL(messageText);
if (detectedUrl) {
if (
detectedUrl.includes("douyin.com") ||
detectedUrl.includes("kuaishou.com") ||
detectedUrl.includes("xiaohongshu.com")
) {
await processShortVideoCommand(detectedUrl);
} else {
await processVideoCommand(detectedUrl);
}
} else {
// 如果不是URL,则检查是否是命令
switch (command) {
case "help":
await processHelpCommand();
break;
case "clear":
processClearCommand();
break;
case "history":
processHistoryCommand();
break;
case "video":
await processVideoCommand(args);
break;
case "shortvideo":
await processShortVideoCommand(args);
break;
case "ai":
await processAiCommand(args);
break;
case "image":
await processImageCommand(args);
break;
case "weather":
await processWeatherCommand(args);
break;
case "calendar":
await processCalendarCommand();
break;
case "stats":
await processStatsCommand();
break;
default:
// 如果不是命令也不是URL,则作为普通AI对话处理
await processAiCommand(messageText);
break;
}
}
isResponseGenerating = false; // 命令执行完毕,重置状态
};
// ================ 命令处理函数 ================
async function processHelpCommand() {
const helpText = `可用命令:
基础命令:
- help: 显示帮助信息
- clear: 清空终端
- history: 显示命令历史记录
影视工具:
- video [url]: 解析并播放长视频(支持爱奇艺、腾讯、优酷等)
例如: video https://v.qq.com/x/cover/mzc00200mp8er9d.html
- shortvideo [url]: 解析短视频(支持抖音、快手、小红书)
例如: shortvideo https://v.douyin.com/xxxxxx
AI工具:
- ai [message]: 与AI助手对话
例如: ai 你好
图片工具:
- image: 转换图片格式 (支持PNG/JPEG/WEBP)
例如: image (会弹出文件选择框)
天气工具:
- weather [ip]: 查询指定IP或当前位置的天气信息
例如: weather 或 weather 49.234.56.78
其他工具:
- calendar: 显示日历信息
- stats: 显示系统统计信息`;
addMessageToChat({
text: helpText,
sender: "ai",
type: "text",
});
}
function processClearCommand() {
messages = [];
localStorage.removeItem("chatHistory");
localStorage.removeItem("commandHistory"); // 清除命令历史
localStorage.removeItem("totalParseCount"); // 清除统计
localStorage.removeItem("successParseCount"); // 清除统计
totalParseCount = 0;
successParseCount = 0;
updateParseStats(); // 更新显示
chatContainer.innerHTML = ""; // 从DOM中清除消息
header.classList.remove("hide-header"); // 再次显示头部
addMessageToChat({
text: "聊天记录已清空。",
sender: "ai",
type: "text",
});
}
function processHistoryCommand() {
if (commandHistoryArray.length === 0) {
addMessageToChat({
text: "暂无命令历史记录。",
sender: "ai",
type: "text",
});
return;
}
let historyText = "命令历史记录:\n";
commandHistoryArray
.slice()
.reverse()
.forEach((cmd, index) => {
historyText += `${commandHistoryArray.length - index}. ${cmd}\n`;
});
addMessageToChat({
text: historyText,
sender: "ai",
type: "text",
});
}
async function processVideoCommand(url) {
if (!url || !url.startsWith("http")) {
addMessageToChat({
text: "请输入有效的视频URL,例如: video https://v.qq.com/x/cover/xxx.html",
sender: "ai",
type: "error",
});
return;
}
totalParseCount++;
updateParseStats();
const loadingMsgId = `loading-${Date.now()}`;
addMessageToChat({
id: loadingMsgId,
text: "正在解析视频,请稍候...",
sender: "ai",
type: "loading",
});
// 模拟解析延迟
await new Promise((resolve) => setTimeout(resolve, 1500));
const defaultApi = videoApis[0];
const videoSrc = defaultApi.url + encodeURIComponent(url);
const htmlContent = `
<div class="tool-container">
<div class="tool-title">
<i class="fas fa-video"></i> 视频解析结果
</div>
<div class="video-player-container">
<iframe id="terminal-video" src="${videoSrc}" frameborder="0" allowfullscreen></iframe>
</div>
<div class="tool-meta">
<div class="tool-meta-item">
<i class="fas fa-network-wired"></i> 当前线路: ${defaultApi.name}
</div>
</div>
<div class="tool-action">
<button class="tool-action-btn tool-primary-btn" onclick="document.getElementById('terminal-video').src = document.getElementById('terminal-video').src;">
<i class="fas fa-sync-alt"></i> 刷新
</button>
<button class="tool-action-btn tool-primary-btn" onclick="showVideoApiSelector('${url}')">
<i class="fas fa-exchange-alt"></i> 切换线路
</button>
<button class="tool-action-btn tool-primary-btn" onclick="document.getElementById('terminal-video').requestFullscreen()">
<i class="fas fa-expand"></i> 全屏
</button>
<button class="tool-action-btn tool-primary-btn" onclick="downloadVideo('${videoSrc}', '解析视频')">
<i class="fas fa-download"></i> 下载视频
</button>
</div>
</div>
`;
// 替换加载消息为实际内容
messages = messages.map((msg) =>
msg.id === loadingMsgId
? {
...msg,
text: "视频解析完成!",
type: "html",
htmlContent: htmlContent,
sender: "ai",
}
: msg,
);
saveToLocalStorage();
chatContainer.innerHTML = ""; // 清空DOM重新渲染
messages.forEach((msg) => chatContainer.appendChild(createMessageElement(msg)));
chatContainer.scrollTo(0, chatContainer.scrollHeight);
successParseCount++;
updateParseStats();
}
async function processShortVideoCommand(url) {
if (!url || !url.startsWith("http")) {
addMessageToChat({
text: "请输入有效的短视频URL,例如: shortvideo https://v.douyin.com/xxx",
sender: "ai",
type: "error",
});
return;
}
totalParseCount++;
updateParseStats();
const loadingMsgId = `loading-${Date.now()}`;
addMessageToChat({
id: loadingMsgId,
text: "正在解析短视频,请稍候...",
sender: "ai",
type: "loading",
});
// 模拟解析延迟
await new Promise((resolve) => setTimeout(resolve, 1500));
const isVideo = Math.random() > 0.5; // 随机决定是视频还是图集
let mediaContent = "";
if (isVideo) {
mediaContent = `
<div class="video-player-container">
<video controls style="width: 100%; height: auto; border-radius: var(--border-radius);" crossorigin="anonymous">
<source src="https://www.w3schools.com/html/mov_bbb.mp4" type="video/mp4">
您的浏览器不支持视频播放。
</video>
</div>
`;
} else {
const images = [
"https://picsum.photos/800/1600?random=1",
"https://picsum.photos/800/1600?random=2",
"https://picsum.photos/800/1600?random=3",
];
const galleryHTML = images
.map(
(img, index) => `
<div style="position: relative; margin-bottom: 10px;">
<img src="${img}" alt="图片 ${index + 1}" class="image-preview" onclick="showFullImage('${img}')">
<div style="position: absolute; top: 8px; right: 8px; background: rgba(0,0,0,0.7); color: white; padding: 4px 8px; border-radius: 12px; font-size: 12px;">
${index + 1}/${images.length}
</div>
</div>
`,
)
.join("");
mediaContent = `
<div style="max-height: 400px; overflow-y: auto;">
${galleryHTML}
</div>
`;
}
const htmlContent = `
<div class="tool-container">
<div class="tool-title">
<i class="fas fa-video"></i> 短视频解析结果
</div>
<div style="display: flex; align-items: center; margin-bottom: 15px; padding: 12px; background: rgba(255, 255, 255, 0.05); border-radius: var(--border-radius);">
<img src="https://picsum.photos/100/100?random=${Math.floor(
Math.random() * 100,
)}" style="width: 40px; height: 40px; border-radius: 50%; margin-right: 12px;">
<div>
<div style="font-weight: 600; color: var(--text-color); margin-bottom: 2px;">示例作者</div>
<div style="font-size: 12px; color: var(--line-color);">来自 ${
url.includes("douyin") ? "抖音" : url.includes("kuaishou") ? "快手" : "小红书"
}</div>
</div>
</div>
${mediaContent}
<div class="tool-action">
<button class="tool-action-btn tool-primary-btn" onclick="${
isVideo
? "downloadVideo('https://www.w3schools.com/html/mov_bbb.mp4', '示例短视频')"
: "downloadAllImages(['https://picsum.photos/800/1600?random=1','https://picsum.photos/800/1600?random=2','https://picsum.photos/800/1600?random=3'], '示例图集')"
}">
<i class="fas fa-download"></i> ${isVideo ? "下载视频" : "打包下载 (3张)"}
</button>
</div>
<div style="margin-top: 15px; padding: 12px; background: rgba(255, 255, 255, 0.05); border-radius: var(--border-radius);">
<div class="tool-title">
<i class="fas fa-music"></i> 背景音乐
</div>
<div style="font-size: 14px; color: var(--line-color);">
示例音乐 - 示例作者
</div>
</div>
</div>
`;
messages = messages.map((msg) =>
msg.id === loadingMsgId
? {
...msg,
text: "短视频解析完成!",
type: "html",
htmlContent: htmlContent,
sender: "ai",
}
: msg,
);
saveToLocalStorage();
chatContainer.innerHTML = ""; // 清空DOM重新渲染
messages.forEach((msg) => chatContainer.appendChild(createMessageElement(msg)));
chatContainer.scrollTo(0, chatContainer.scrollHeight);
successParseCount++;
updateParseStats();
}
async function processAiCommand(message) {
const loadingMsgId = `loading-${Date.now()}`;
addMessageToChat({
id: loadingMsgId,
text: "",
sender: "ai",
type: "loading",
});
// 模拟AI回复
await new Promise((resolve) => setTimeout(resolve, 1000));
const normalizedUserMessage = message.toLowerCase().trim();
let response = predefinedResponses[normalizedUserMessage] || predefinedResponses.default;
// 找到并更新加载消息
const loadingMessageElement = chatContainer.querySelector(
`[data-message-id="${loadingMsgId}"] .text`,
);
if (loadingMessageElement) {
const incomingMessageDiv = loadingMessageElement.closest(".message");
showTypingEffect(response, loadingMessageElement, incomingMessageDiv);
messages = messages.map((msg) =>
msg.id === loadingMsgId ? { ...msg, text: response, type: "text" } : msg,
);
saveToLocalStorage();
}
}
async function processImageCommand(format) {
// 触发文件上传,后续由 imageUploadInput 的 change 事件处理
imageUploadInput.click();
}
async function processWeatherCommand(ip = null) {
const loadingMsgId = `loading-${Date.now()}`;
addMessageToChat({
id: loadingMsgId,
text: ip ? `正在查询IP ${ip} 的天气信息...` : "正在查询当前位置的天气信息...",
sender: "ai",
type: "loading",
});
try {
const weatherData = await fetchWeatherByIP(ip);
if (weatherData && weatherData.code === 200) {
// 替换加载消息为天气组件
messages = messages.map((msg) =>
msg.id === loadingMsgId
? {
...msg,
text: "天气信息已获取:",
type: "component",
componentType: "weather",
componentData: weatherData,
sender: "ai",
}
: msg,
);
saveToLocalStorage();
chatContainer.innerHTML = ""; // 清空DOM重新渲染
messages.forEach((msg) => chatContainer.appendChild(createMessageElement(msg)));
chatContainer.scrollTo(0, chatContainer.scrollHeight);
} else {
messages = messages.map((msg) =>
msg.id === loadingMsgId
? {
...msg,
text: `获取天气数据失败: ${weatherData.msg || "未知错误"}`,
type: "error",
sender: "ai",
}
: msg,
);
saveToLocalStorage();
chatContainer.innerHTML = ""; // 清空DOM重新渲染
messages.forEach((msg) => chatContainer.appendChild(createMessageElement(msg)));
chatContainer.scrollTo(0, chatContainer.scrollHeight);
}
} catch (error) {
messages = messages.map((msg) =>
msg.id === loadingMsgId
? {
...msg,
text: `获取天气数据失败: ${error.message || "请检查网络连接或稍后重试"}`,
type: "error",
sender: "ai",
}
: msg,
);
saveToLocalStorage();
chatContainer.innerHTML = ""; // 清空DOM重新渲染
messages.forEach((msg) => chatContainer.appendChild(createMessageElement(msg)));
chatContainer.scrollTo(0, chatContainer.scrollHeight);
}
}
async function processCalendarCommand() {
addMessageToChat({
text: "日历信息:",
sender: "ai",
type: "component",
componentType: "calendar",
componentData: {
currentMonth: new Date().getMonth(),
currentYear: new Date().getFullYear(),
selectedDate: new Date(),
},
});
}
async function processStatsCommand() {
addMessageToChat({
text: `系统统计信息:
总解析次数: ${totalParseCount}
成功解析次数: ${successParseCount}
成功率: ${totalParseCount > 0 ? Math.round((successParseCount / totalParseCount) * 100) : 0}%
命令历史记录: ${commandHistoryArray.length} 条
当前时间: ${new Date().toLocaleString()}`,
sender: "ai",
type: "text",
});
}
// ================ 事件监听器 ================
// 切换浅色/深色主题
toggleThemeButton.addEventListener("click", () => {
const isLightMode = document.body.classList.toggle("light_mode");
localStorage.setItem("themeColor", isLightMode ? "dark_mode" : "light_mode");
toggleThemeButton.innerText = isLightMode ? "dark_mode" : "light_mode";
});
// 删除所有聊天记录
deleteChatButton.addEventListener("click", () => {
if (confirm("您确定要清除所有聊天记录吗?")) {
messages = [];
localStorage.removeItem("chatHistory");
localStorage.removeItem("commandHistory"); // 清除命令历史
localStorage.removeItem("totalParseCount"); // 清除统计
localStorage.removeItem("successParseCount"); // 清除统计
totalParseCount = 0;
successParseCount = 0;
updateParseStats(); // 更新显示
chatContainer.innerHTML = ""; // 从DOM中清除消息
header.classList.remove("hide-header"); // 再次显示头部
addMessageToChat({
text: "所有聊天和数据已清空。",
sender: "ai",
type: "text",
});
}
});
// 处理图片上传按钮点击
uploadImageButton.addEventListener("click", () => {
imageUploadInput.click();
});
// 处理图片文件选择 (用于 image 命令)
imageUploadInput.addEventListener("change", async (e) => {
const file = e.target.files[0];
if (!file) {
isResponseGenerating = false; // 如果没有选择文件,重置状态
return;
}
if (!file.type.startsWith("image/")) {
showAlert("请选择图片文件。");
isResponseGenerating = false; // 如果不是图片,重置状态
return;
}
const reader = new FileReader();
reader.onloadend = async () => {
const base64Image = reader.result;
// 立即显示用户上传的图片消息
addMessageToChat({
id: `msg-${Date.now()}`,
text: `已上传图片: ${file.name}`,
sender: "user",
type: "text",
image: base64Image,
originalFileName: file.name
});
// 重置转换设置
currentConversionSettings = {
format: "png",
quality: 0.8,
resize: null
};
};
reader.readAsDataURL(file);
});
// 处理建议点击
suggestions.forEach((suggestion) => {
suggestion.addEventListener("click", () => {
const text = suggestion.dataset.command || suggestion.querySelector(".text").innerText;
handleOutgoingChat(text);
});
});
// 处理表单提交 (点击发送按钮)
typingForm.addEventListener("submit", (e) => {
e.preventDefault();
handleOutgoingChat(typingInput.value.trim());
});
// 添加回车键发送信息功能
typingInput.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault(); // 阻止默认的换行行为
handleOutgoingChat(typingInput.value.trim());
} else if (e.key === "ArrowUp") {
e.preventDefault();
if (historyIndex < commandHistoryArray.length - 1) {
historyIndex++;
typingInput.value = commandHistoryArray[commandHistoryArray.length - 1 - historyIndex];
}
} else if (e.key === "ArrowDown") {
e.preventDefault();
if (historyIndex > 0) {
historyIndex--;
typingInput.value = commandHistoryArray[commandHistoryArray.length - 1 - historyIndex];
} else if (historyIndex === 0) {
historyIndex = -1;
typingInput.value = "";
}
}
});
// 自动调整文本域大小
typingInput.addEventListener("input", () => {
typingInput.style.height = "auto";
typingInput.style.height = `${typingInput.scrollHeight}px`;
});
// ================ 图片转换功能 ================
// 设置图片转换格式
window.setImageConversionFormat = (format) => {
currentConversionSettings.format = format;
// 更新按钮样式
document.querySelectorAll('.tool-btn[data-format]').forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.format === format) {
btn.classList.add('active');
}
});
};
// 设置图片质量
window.setImageQuality = (quality) => {
currentConversionSettings.quality = parseFloat(quality);
document.querySelectorAll('.tool-slider').forEach(slider => {
slider.value = quality;
});
document.querySelectorAll('.tool-label span').forEach(label => {
label.textContent = `质量: ${Math.round(quality * 100)}%`;
});
};
// 转换图片
window.convertImage = async (messageId) => {
const message = messages.find(msg => msg.id === messageId);
if (!message || !message.image) return;
// 显示进度条
const progressBar = document.getElementById(`progress-${messageId}`);
const resultContainer = document.getElementById(`result-${messageId}`);
progressBar.style.display = 'block';
try {
// 获取原始图片数据
const response = await fetch(message.image);
if (!response.ok) throw new Error('图片加载失败');
const blob = await response.blob();
// 模拟进度更新
let progress = 0;
const progressInterval = setInterval(() => {
progress += 0.1;
const progressBarElement = progressBar.querySelector('.tool-progress-bar');
if (progressBarElement) {
progressBarElement.style.width = `${Math.min(progress * 100, 100)}%`;
}
if (progress >= 1) clearInterval(progressInterval);
}, 200);
// 实际转换过程
const convertedImage = await convertImageFormat(blob, currentConversionSettings.format, currentConversionSettings.quality);
// 计算转换后大小
const convertedSize = convertedImage.size;
// 转换为DataURL用于显示
const reader = new FileReader();
reader.onloadend = () => {
const convertedDataUrl = reader.result;
// 更新结果容器
resultContainer.style.display = 'block';
resultContainer.querySelector('img').src = convertedDataUrl;
resultContainer.querySelector('.format-value').textContent = currentConversionSettings.format.toUpperCase();
resultContainer.querySelector('.quality-value').textContent = `${Math.round(currentConversionSettings.quality * 100)}%`;
resultContainer.querySelector('.size-value').textContent = formatFileSize(convertedSize);
// 更新下载链接
const downloadLink = resultContainer.querySelector('.download-btn');
downloadLink.href = convertedDataUrl;
downloadLink.download = message.originalFileName ?
message.originalFileName.replace(/\.[^/.]+$/, "") + "." + currentConversionSettings.format :
"converted_image." + currentConversionSettings.format;
// 添加到消息历史
messages = messages.map(msg =>
msg.id === messageId ? {
...msg,
convertedImage: convertedDataUrl,
convertedFormat: currentConversionSettings.format,
convertedQuality: currentConversionSettings.quality,
convertedSize: convertedSize
} : msg
);
saveToLocalStorage();
showAlert("图片转换成功!");
};
reader.readAsDataURL(convertedImage);
} catch (error) {
console.error("图片转换失败:", error);
showAlert("图片转换失败: " + error.message);
} finally {
progressBar.style.display = 'none';
}
};
// 取消图片转换
window.cancelImageConversion = (messageId) => {
const progressBar = document.getElementById(`progress-${messageId}`);
progressBar.style.display = 'none';
showAlert("已取消图片转换");
};
// 图片格式转换函数
const convertImageFormat = (blob, format, quality = 0.8) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
// 根据格式和质量进行转换
let mimeType;
switch (format.toLowerCase()) {
case 'jpeg':
case 'jpg':
mimeType = 'image/jpeg';
break;
case 'webp':
mimeType = 'image/webp';
break;
case 'png':
default:
mimeType = 'image/png';
break;
}
canvas.toBlob(
(blob) => {
if (blob) {
resolve(blob);
} else {
reject(new Error('图片转换失败'));
}
},
mimeType,
quality
);
};
img.onerror = () => {
reject(new Error('图片加载失败'));
};
img.src = URL.createObjectURL(blob);
});
};
// ================ 终端工具辅助函数 (从TENGYUAN项目移植并适配) ================
// 天气API请求 (模拟)
async function fetchWeatherByIP(ip = null) {
// 这是一个模拟函数,实际需要调用后端API
await new Promise((resolve) => setTimeout(resolve, 1000)); // 模拟网络延迟
const mockData = {
code: 200,
msg: "成功",
place: ip ? `IP: ${ip}` : "当前位置",
weather1: "多云",
weather2: "晴",
temperature: "25",
precipitation: "0.1",
pressure: "1012",
humidity: "60",
windDirection: "东南风",
windScale: "3-4级",
windSpeed: "5.2",
windDirectionDegree: "135",
};
if (ip === "error") {
throw new Error("模拟错误:无法获取指定IP天气。");
}
return mockData;
}
function getWeatherIcon(weather1, weather2) {
const weather = weather1 + (weather2 ? weather2 : "");
const iconMap = {
晴: "fas fa-sun",
多云: "fas fa-cloud-sun",
阴: "fas fa-cloud",
小雨: "fas fa-cloud-rain",
中雨: "fas fa-cloud-rain",
大雨: "fas fa-cloud-showers-heavy",
雷阵雨: "fas fa-bolt",
雪: "fas fa-snowflake",
雾: "fas fa-smog",
霾: "fas fa-smog",
云: "fas fa-cloud",
};
for (const key in iconMap) {
if (weather.includes(key)) {
return iconMap[key];
}
}
return "fas fa-cloud";
}
function getWindDescription(windDirection, windScale) {
return `${windDirection} ${windScale}`;
}
// 渲染天气组件的HTML
function renderWeatherComponent(weatherData) {
const weatherIcon = getWeatherIcon(weatherData.weather1, weatherData.weather2);
const weatherDesc =
weatherData.weather1 +
(weatherData.weather2 && weatherData.weather2 !== weatherData.weather1
? "转" + weatherData.weather2
: "");
const windDesc = getWindDescription(weatherData.windDirection, weatherData.windScale);
return `
<div class="tool-container">
<div class="tool-title">
<i class="fas fa-cloud-sun"></i> 天气信息
</div>
<div class="weather-info">
<div class="weather-item">
<div class="weather-item-label">位置</div>
<div class="weather-item-value">${weatherData.place}</div>
</div>
<div class="weather-item">
<div class="weather-item-label">天气</div>
<div class="weather-item-value">${weatherDesc}</div>
</div>
<div class="weather-item">
<div class="weather-item-label">温度</div>
<div class="weather-item-value">${weatherData.temperature}°C</div>
</div>
<div class="weather-item">
<div class="weather-item-label">降水量</div>
<div class="weather-item-value">${weatherData.precipitation}mm</div>
</div>
<div class="weather-item">
<div class="weather-item-label">气压</div>
<div class="weather-item-value">${weatherData.pressure}hPa</div>
</div>
<div class="weather-item">
<div class="weather-item-label">湿度</div>
<div class="weather-item-value">${weatherData.humidity}%</div>
</div>
<div class="weather-item">
<div class="weather-item-label">风力</div>
<div class="weather-item-value">${windDesc}</div>
</div>
<div class="weather-item">
<div class="weather-item-label">风速</div>
<div class="weather-item-value">${weatherData.windSpeed}m/s</div>
</div>
</div>
<div style="font-size: 11px; color: var(--line-color); margin-top: 10px;">
💡 基于位置自动获取天气信息 (模拟数据)
</div>
</div>
`;
}
// 渲染日历组件的HTML
function renderCalendarComponent(calendarData) {
const monthNames = [
"一月",
"二月",
"三月",
"四月",
"五月",
"六月",
"七月",
"八月",
"九月",
"十月",
"十一月",
"十二月",
];
let currentMonth = calendarData.currentMonth;
let currentYear = calendarData.currentYear;
let selectedDate = calendarData.selectedDate;
const firstDay = new Date(currentYear, currentMonth, 1);
const lastDay = new Date(currentYear, currentMonth + 1, 0);
const prevMonthLastDay = new Date(currentYear, currentMonth, 0).getDate();
const firstDayIndex = firstDay.getDay();
const lastDate = lastDay.getDate();
const today = new Date();
let daysHtml = `
<div class="calendar-weekday">日</div>
<div class="calendar-weekday">一</div>
<div class="calendar-weekday">二</div>
<div class="calendar-weekday">三</div>
<div class="calendar-weekday">四</div>
<div class="calendar-weekday">五</div>
<div class="calendar-weekday">六</div>
`;
// 上个月的日期
for (let i = firstDayIndex; i > 0; i--) {
daysHtml += `<div class="calendar-day other-month">${prevMonthLastDay - i + 1}</div>`;
}
// 当前月的日期
for (let i = 1; i <= lastDate; i++) {
let classList = "calendar-day";
if (
i === today.getDate() &&
currentMonth === today.getMonth() &&
currentYear === today.getFullYear()
) {
classList += " today";
}
if (
i === selectedDate.getDate() &&
currentMonth === selectedDate.getMonth() &&
currentYear === selectedDate.getFullYear()
) {
classList += " selected";
}
// 藤原生日标记
if (i === 1 && currentMonth === 10) {
// 11月1日
classList += " birthday";
}
daysHtml += `<div class="${classList}" data-day="${i}">${i}</div>`;
}
// 下个月的日期
const daysNeeded = 42 - (firstDayIndex + lastDate);
for (let i = 1; i <= daysNeeded; i++) {
daysHtml += `<div class="calendar-day other-month">${i}</div>`;
}
const calendarHtml = `
<div class="tool-container">
<div class="tool-title">
<i class="fas fa-calendar-alt"></i> 日历
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<div style="font-weight: 600; color: var(--text-color);">${currentYear}年${monthNames[currentMonth]}</div>
<div style="display: flex; gap: 8px;">
<button class="tool-btn" data-action="prevMonth" aria-label="上个月">
<i class="fas fa-chevron-left"></i>
</button>
<button class="tool-btn" data-action="nextMonth" aria-label="下个月">
<i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
<div class="calendar-grid">
${daysHtml}
</div>
<div style="font-size: 11px; color: var(--line-color); margin-top: 10px;">
💡 点击日期可选择,点击箭头切换月份。
</div>
</div>
`;
// 返回HTML字符串,并添加事件监听器
setTimeout(() => {
const calendarElement = chatContainer.querySelector(".tool-container");
if (calendarElement) {
calendarElement
.querySelector('[data-action="prevMonth"]')
.addEventListener("click", () => {
currentMonth--;
if (currentMonth < 0) {
currentMonth = 11;
currentYear--;
}
updateCalendarInChat(currentMonth, currentYear, selectedDate);
});
calendarElement
.querySelector('[data-action="nextMonth"]')
.addEventListener("click", () => {
currentMonth++;
if (currentMonth > 11) {
currentMonth = 0;
currentYear++;
}
updateCalendarInChat(currentMonth, currentYear, selectedDate);
});
calendarElement.querySelectorAll(".calendar-day").forEach((dayElem) => {
if (!dayElem.classList.contains("other-month")) {
dayElem.addEventListener("click", () => {
const day = parseInt(dayElem.dataset.day);
selectedDate = new Date(currentYear, currentMonth, day);
updateCalendarInChat(currentMonth, currentYear, selectedDate);
showAlert(`已选择日期: ${currentYear}年${currentMonth + 1}月${day}日`);
});
}
});
}
}, 100); // 延迟绑定事件,确保DOM已渲染
return calendarHtml;
}
// 更新聊天中日历的函数
function updateCalendarInChat(month, year, date) {
// 找到日历消息并更新其数据
messages = messages.map((msg) => {
if (msg.type === "component" && msg.componentType === "calendar") {
return {
...msg,
componentData: {
currentMonth: month,
currentYear: year,
selectedDate: date,
},
};
}
return msg;
});
saveToLocalStorage();
chatContainer.innerHTML = ""; // 清空DOM重新渲染
messages.forEach((msg) => chatContainer.appendChild(createMessageElement(msg)));
chatContainer.scrollTo(0, chatContainer.scrollHeight);
}
// 显示全屏图片
window.showFullImage = (url) => {
const overlay = document.createElement("div");
overlay.className = "fullscreen-image-overlay";
const img = document.createElement("img");
img.src = url;
overlay.onclick = () => overlay.remove();
overlay.appendChild(img);
document.body.appendChild(overlay);
};
// 下载所有图片 (JSZip)
window.downloadAllImages = async (images, title) => {
if (!window.JSZip) {
showAlert("打包下载功能需要JSZip库支持。");
return;
}
try {
const zip = new JSZip();
const imgFolder = zip.folder("images");
showAlert("正在打包下载,请稍候...");
const downloadPromises = images.map(async (imgUrl, index) => {
try {
const response = await fetch(imgUrl);
if (!response.ok) throw new Error(`图片${index + 1}下载失败`);
const blob = await response.blob();
imgFolder.file(`image_${index + 1}.jpg`, blob);
} catch (error) {
console.warn(`图片${index + 1}下载失败:`, error);
}
});
await Promise.all(downloadPromises);
const zipBlob = await zip.generateAsync({
type: "blob",
compression: "DEFLATE",
compressionOptions: { level: 6 },
});
const cleanTitle = title.replace(/[<>:"/\\|?*]/g, "");
saveAs(zipBlob, `${cleanTitle}.zip`);
showAlert("打包下载完成!");
} catch (error) {
console.error("打包下载失败:", error);
showAlert("打包下载失败,请重试。");
}
};
// 下载视频 (FileSaver)
window.downloadVideo = async (url, title) => {
try {
showAlert("正在下载视频,请稍候...");
const response = await fetch(url);
if (!response.ok) throw new Error(`下载失败: ${response.status}`);
const blob = await response.blob();
const cleanTitle = title.replace(/[<>:"/\\|?*]/g, "");
saveAs(blob, `${cleanTitle}.mp4`);
showAlert("视频下载完成!");
} catch (error) {
console.error("视频下载失败:", error);
showAlert("视频下载失败,请重试。");
}
};
// 保存命令历史
function saveToHistory(command) {
if (commandHistoryArray.length === 0 || commandHistoryArray[commandHistoryArray.length - 1] !== command) {
commandHistoryArray.push(command);
if (commandHistoryArray.length > 20) {
commandHistoryArray.shift();
}
localStorage.setItem("commandHistory", JSON.stringify(commandHistoryArray));
}
historyIndex = -1;
}
// 更新解析统计
function updateParseStats() {
localStorage.setItem("totalParseCount", totalParseCount.toString());
localStorage.setItem("successParseCount", successParseCount.toString());
}
// ================ 初始化 ================
document.addEventListener("DOMContentLoaded", function () {
loadFromLocalStorage(); // 加载保存的历史记录和主题
// 聚焦到输入框
typingInput.focus();
// 显示欢迎信息
setTimeout(() => {
addMessageToChat({
text: "欢迎使用超级AI助手!\n\n输入 'help' 查看可用命令,或直接输入问题与我对话。",
sender: "ai",
type: "text",
});
}, 500);
});
// 页面可见性变化时重新聚焦
document.addEventListener("visibilitychange", () => {
if (!document.hidden) {
setTimeout(() => typingInput.focus(), 100);
}
});
// 错误处理
window.addEventListener("error", (e) => {
console.error("全局错误:", e.error);
});
window.addEventListener("unhandledrejection", (e) => {
console.error("未处理的Promise拒绝:", e.reason);
});
</script>
</body>
</html>
index.html