242 lines
7.0 KiB
JavaScript
242 lines
7.0 KiB
JavaScript
/*
|
||
* app.js
|
||
* 主逻辑:渲染工具列表、处理交互、调用 API 或本地模拟。
|
||
*/
|
||
|
||
(function () {
|
||
'use strict';
|
||
|
||
// ---------- DOM ----------
|
||
const toolsGrid = document.getElementById('toolsGrid');
|
||
const panel = document.getElementById('panel');
|
||
const panelTitle = document.getElementById('panelTitle');
|
||
const panelDesc = document.getElementById('panelDesc');
|
||
const inputBox = document.getElementById('inputBox');
|
||
const outputBox = document.getElementById('outputBox');
|
||
const runBtn = document.getElementById('runBtn');
|
||
const copyBtn = document.getElementById('copyBtn');
|
||
const clearBtn = document.getElementById('clearBtn');
|
||
const closePanel = document.getElementById('closePanel');
|
||
const settingsBtn = document.getElementById('settingsBtn');
|
||
const settingsModal = document.getElementById('settingsModal');
|
||
const apiBase = document.getElementById('apiBase');
|
||
const apiKey = document.getElementById('apiKey');
|
||
const apiModel = document.getElementById('apiModel');
|
||
const saveSettings = document.getElementById('saveSettings');
|
||
const cancelSettings = document.getElementById('cancelSettings');
|
||
const yearEl = document.getElementById('year');
|
||
|
||
// ---------- 状态 ----------
|
||
let currentTool = null;
|
||
const SETTINGS_KEY = 'ai-tool-station-settings';
|
||
|
||
// ---------- 初始化 ----------
|
||
function init() {
|
||
yearEl.textContent = new Date().getFullYear();
|
||
renderTools();
|
||
loadSettings();
|
||
bindEvents();
|
||
}
|
||
|
||
// 渲染工具卡片
|
||
function renderTools() {
|
||
toolsGrid.innerHTML = '';
|
||
TOOLS.forEach((tool) => {
|
||
const card = document.createElement('button');
|
||
card.className = 'tool-card';
|
||
card.type = 'button';
|
||
card.innerHTML = `
|
||
<span class="tool-icon">${tool.icon}</span>
|
||
<div class="tool-name">${escapeHtml(tool.name)}</div>
|
||
<div class="tool-desc">${escapeHtml(tool.desc)}</div>
|
||
`;
|
||
card.addEventListener('click', () => openTool(tool));
|
||
toolsGrid.appendChild(card);
|
||
});
|
||
}
|
||
|
||
// 打开某个工具
|
||
function openTool(tool) {
|
||
currentTool = tool;
|
||
panelTitle.textContent = `${tool.icon} ${tool.name}`;
|
||
panelDesc.textContent = tool.desc;
|
||
inputBox.placeholder = tool.placeholder || '请输入内容...';
|
||
inputBox.value = '';
|
||
outputBox.textContent = '';
|
||
panel.hidden = false;
|
||
panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
inputBox.focus();
|
||
}
|
||
|
||
// 运行工具
|
||
async function run() {
|
||
if (!currentTool) return;
|
||
const input = inputBox.value.trim();
|
||
if (!input) {
|
||
outputBox.textContent = '⚠️ 请先输入内容。';
|
||
return;
|
||
}
|
||
|
||
runBtn.disabled = true;
|
||
runBtn.textContent = '⏳ 生成中...';
|
||
outputBox.textContent = '正在生成,请稍候...';
|
||
|
||
try {
|
||
const settings = getSettings();
|
||
let result;
|
||
if (settings.apiKey && settings.apiBase && settings.apiModel) {
|
||
result = await callRealAPI(settings, currentTool, input);
|
||
} else {
|
||
// 模拟延迟,体验更真实
|
||
await sleep(400);
|
||
result = mockRun(currentTool.id, input);
|
||
}
|
||
outputBox.textContent = result;
|
||
} catch (err) {
|
||
outputBox.textContent = '❌ 出错了:' + (err && err.message ? err.message : String(err));
|
||
} finally {
|
||
runBtn.disabled = false;
|
||
runBtn.textContent = '✨ 运行';
|
||
}
|
||
}
|
||
|
||
// 调用真实 OpenAI 兼容 API
|
||
async function callRealAPI(settings, tool, input) {
|
||
const base = settings.apiBase.replace(/\/+$/, '');
|
||
const url = base + '/chat/completions';
|
||
const body = {
|
||
model: settings.apiModel,
|
||
messages: [
|
||
{ role: 'system', content: '你是一个专业、简洁的 AI 助手,用中文回答。' },
|
||
{ role: 'user', content: tool.prompt(input) }
|
||
],
|
||
temperature: 0.7
|
||
};
|
||
|
||
const resp = await fetch(url, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': 'Bearer ' + settings.apiKey
|
||
},
|
||
body: JSON.stringify(body)
|
||
});
|
||
|
||
if (!resp.ok) {
|
||
const t = await resp.text().catch(() => '');
|
||
throw new Error(`HTTP ${resp.status} ${t.slice(0, 200)}`);
|
||
}
|
||
|
||
const data = await resp.json();
|
||
const content = data && data.choices && data.choices[0] && data.choices[0].message
|
||
? data.choices[0].message.content
|
||
: '';
|
||
return content || '(API 返回为空)';
|
||
}
|
||
|
||
// ---------- 设置 ----------
|
||
function getSettings() {
|
||
try {
|
||
return JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}');
|
||
} catch (_) {
|
||
return {};
|
||
}
|
||
}
|
||
|
||
function loadSettings() {
|
||
const s = getSettings();
|
||
apiBase.value = s.apiBase || '';
|
||
apiKey.value = s.apiKey || '';
|
||
apiModel.value = s.apiModel || '';
|
||
}
|
||
|
||
function saveSettingsToStorage() {
|
||
const s = {
|
||
apiBase: apiBase.value.trim(),
|
||
apiKey: apiKey.value.trim(),
|
||
apiModel: apiModel.value.trim()
|
||
};
|
||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(s));
|
||
}
|
||
|
||
// ---------- 事件绑定 ----------
|
||
function bindEvents() {
|
||
runBtn.addEventListener('click', run);
|
||
|
||
copyBtn.addEventListener('click', async () => {
|
||
const text = outputBox.textContent || '';
|
||
if (!text) return;
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
copyBtn.textContent = '✅ 已复制';
|
||
setTimeout(() => (copyBtn.textContent = '📋 复制结果'), 1200);
|
||
} catch (_) {
|
||
// 降级
|
||
const ta = document.createElement('textarea');
|
||
ta.value = text;
|
||
document.body.appendChild(ta);
|
||
ta.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(ta);
|
||
copyBtn.textContent = '✅ 已复制';
|
||
setTimeout(() => (copyBtn.textContent = '📋 复制结果'), 1200);
|
||
}
|
||
});
|
||
|
||
clearBtn.addEventListener('click', () => {
|
||
inputBox.value = '';
|
||
outputBox.textContent = '';
|
||
inputBox.focus();
|
||
});
|
||
|
||
closePanel.addEventListener('click', () => {
|
||
panel.hidden = true;
|
||
currentTool = null;
|
||
});
|
||
|
||
settingsBtn.addEventListener('click', () => {
|
||
loadSettings();
|
||
settingsModal.hidden = false;
|
||
});
|
||
|
||
cancelSettings.addEventListener('click', () => {
|
||
settingsModal.hidden = true;
|
||
});
|
||
|
||
saveSettings.addEventListener('click', () => {
|
||
saveSettingsToStorage();
|
||
settingsModal.hidden = true;
|
||
});
|
||
|
||
// 点击遮罩关闭
|
||
settingsModal.addEventListener('click', (e) => {
|
||
if (e.target === settingsModal) settingsModal.hidden = true;
|
||
});
|
||
|
||
// 快捷键:Ctrl/Cmd + Enter 运行
|
||
inputBox.addEventListener('keydown', (e) => {
|
||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||
e.preventDefault();
|
||
run();
|
||
}
|
||
});
|
||
}
|
||
|
||
// ---------- 工具函数 ----------
|
||
function sleep(ms) {
|
||
return new Promise((r) => setTimeout(r, ms));
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
return String(str)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
// 启动
|
||
init();
|
||
})();
|