commit 47a46bd5ea2069fc779b057b2b88752e59a1e4a6 Author: hubian <908234780@qq.com> Date: Sat Apr 25 23:29:41 2026 +0800 feat: AI对话助手移动端应用 - 智谱 GLM-4.5-Air 大模型接口 - 移动端优化聊天界面 - Capacitor 支持 Android/iOS 打包 - PWA 配置,可添加到主屏幕 - 多对话管理 - 流式输出 - Markdown格式显示 - 复制、重新生成、删除功能 - 端口: 19021 diff --git a/README.md b/README.md new file mode 100644 index 0000000..867eacf --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# AI对话助手移动端应用 + +基于 Capacitor 的 AI 对话助手,可打包为 Android/iOS 应用。 + +## 功能 + +- 💬 AI 对话(智谱 GLM-4.5-Air) +- 📱 移动端优化界面 +- 💾 本地对话历史 +- 🚀 一键打包 APK/IPA + +## 开发 + +```bash +# 安装依赖 +npm install + +# 启动本地服务器 +npm run dev + +# 访问 http://localhost:19019 +``` + +## 打包移动端 + +```bash +# 添加平台 +npx cap add android +npx cap add ios + +# 同步代码 +npm run build:android +npm run build:ios + +# 打开原生项目 +npm run open:android # 需要 Android Studio +npm run open:ios # 需要 Xcode (macOS) +``` + +## 配置 + +- 端口: 19019 +- 模型: GLM-4.5-Air +- API: 智谱开放平台 + +## 目录结构 + +``` +www/ # 前端代码 +├── index.html # 主页面 +├── app.js # 应用逻辑 +├── style.css # 样式 +└── manifest.json # PWA 配置 +android/ # Android 项目(自动生成) +ios/ # iOS 项目(自动生成) +``` \ No newline at end of file diff --git a/capacitor.config.json b/capacitor.config.json new file mode 100644 index 0000000..32474ca --- /dev/null +++ b/capacitor.config.json @@ -0,0 +1,15 @@ +{ + "appId": "com.tphai.aichat", + "appName": "AI助手", + "webDir": "www", + "server": { + "androidScheme": "https" + }, + "plugins": { + "SplashScreen": { + "launchShowDuration": 2000, + "backgroundColor": "#667eea", + "showSpinner": false + } + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..4683a98 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "ai-chat-app", + "version": "1.0.0", + "description": "AI对话助手移动端应用", + "scripts": { + "dev": "npx http-server -p 19021 -c-1", + "build:android": "npx cap sync android", + "build:ios": "npx cap sync ios", + "open:android": "npx cap open android", + "open:ios": "npx cap open ios" + }, + "dependencies": {}, + "devDependencies": { + "@capacitor/cli": "^6.0.0", + "@capacitor/core": "^6.0.0", + "@capacitor/android": "^6.0.0", + "@capacitor/ios": "^6.0.0", + "http-server": "^14.1.1" + } +} \ No newline at end of file diff --git a/www/app.js b/www/app.js new file mode 100644 index 0000000..1a3c758 --- /dev/null +++ b/www/app.js @@ -0,0 +1,515 @@ +// AI助手 - 前端应用 +// 使用智谱 GLM-4.5-Air 模型(流式输出 + 多对话管理) + +const CONFIG = { + apiUrl: 'https://open.bigmodel.cn/api/paas/v4/chat/completions', + apiKey: '2259e33a1357460abe17919aaf81e73d.K44a8LPQTmFM5PKm', + model: 'glm-4.5-air', + maxTokens: 2048 +}; + +// 数据结构 +let conversations = []; // 对话列表 +let currentConversation = null; // 当前对话 +let isLoading = false; + +// DOM 元素 +const appContainer = document.getElementById('app'); +const messagesContainer = document.getElementById('messagesContainer'); +const messagesDiv = document.getElementById('messages'); +const userInput = document.getElementById('userInput'); +const sendBtn = document.getElementById('sendBtn'); +const welcome = document.getElementById('welcome'); + +// 初始化 +document.addEventListener('DOMContentLoaded', () => { + // 从本地存储加载对话列表 + const saved = localStorage.getItem('conversations'); + if (saved) { + conversations = JSON.parse(saved); + } + + // 兼容旧数据格式(chat_history) + const oldHistory = localStorage.getItem('chat_history'); + if (oldHistory && conversations.length === 0) { + const oldMessages = JSON.parse(oldHistory); + if (oldMessages.length > 0) { + // 转换旧数据为新格式 + const convertedConv = { + id: Date.now().toString(), + title: oldMessages[0].content.slice(0, 30) + (oldMessages[0].content.length > 30 ? '...' : ''), + messages: oldMessages, + createdAt: Date.now(), + updatedAt: Date.now() + }; + conversations.push(convertedConv); + saveConversations(); + localStorage.removeItem('chat_history'); // 清理旧数据 + } + } + + // 显示对话列表页面 + showConversationList(); +}); + +// ==================== 对话列表页面 ==================== + +function showConversationList() { + currentConversation = null; + + // 渲染对话列表 + const listHtml = ` +
'+(n?i:R(i,!0))+`
+`:""+(n?i:R(i,!0))+`
+`}blockquote({tokens:e}){return`+${this.parser.parse(e)}+`}html({text:e}){return e}heading({tokens:e,depth:t}){return`
${this.parser.parseInline(e)}
+`}table(e){let t="",n="";for(let i=0;i${R(e,!0)}`}br(e){return"An error occurred:
"+R(n.message+"",!0)+"";return t?Promise.resolve(s):s}if(t)return Promise.reject(n);throw n}}};var M=new E;function k(l,e){return M.parse(l,e)}k.options=k.setOptions=function(l){return M.setOptions(l),k.defaults=M.defaults,N(k.defaults),k};k.getDefaults=z;k.defaults=w;k.use=function(...l){return M.use(...l),k.defaults=M.defaults,N(k.defaults),k};k.walkTokens=function(l,e){return M.walkTokens(l,e)};k.parseInline=M.parseInline;k.Parser=b;k.parser=b.parse;k.Renderer=$;k.TextRenderer=_;k.Lexer=x;k.lexer=x.lex;k.Tokenizer=S;k.Hooks=L;k.parse=k;var it=k.options,ot=k.setOptions,lt=k.use,at=k.walkTokens,ct=k.parseInline,pt=k,ut=b.parse,ht=x.lex; + +if(__exports != exports)module.exports = exports;return module.exports})); diff --git a/www/style.css b/www/style.css new file mode 100644 index 0000000..18b3636 --- /dev/null +++ b/www/style.css @@ -0,0 +1,582 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary: #667eea; + --primary-dark: #5a67d8; + --bg-color: #f7fafc; + --card-bg: #ffffff; + --text-color: #2d3748; + --text-light: #718096; + --border-color: #e2e8f0; + --user-msg: #667eea; + --ai-msg: #f7fafc; + --shadow: 0 2px 8px rgba(0,0,0,0.1); +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background: var(--bg-color); + color: var(--text-color); + line-height: 1.6; +} + +#app { + min-height: 100vh; + min-height: 100dvh; +} + +/* ==================== 对话列表页面 ==================== */ + +.conversation-list-page { + display: flex; + flex-direction: column; + min-height: 100vh; + min-height: 100dvh; + background: var(--card-bg); +} + +.list-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: linear-gradient(135deg, var(--primary) 0%, #764ba2 100%); + color: white; + position: sticky; + top: 0; + z-index: 100; +} + +.list-header .header-title { + display: flex; + align-items: center; + gap: 10px; +} + +.list-header .logo { + font-size: 24px; +} + +.list-header h1 { + font-size: 18px; + font-weight: 600; +} + +.list-content { + flex: 1; + padding: 16px; + overflow-y: auto; +} + +.new-chat-btn { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 20px; + background: linear-gradient(135deg, var(--primary) 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 12px; + font-size: 16px; + cursor: pointer; + margin-bottom: 16px; + transition: transform 0.2s; +} + +.new-chat-btn:active { + transform: scale(0.98); +} + +.conversation-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.empty-list { + text-align: center; + padding: 40px; + color: var(--text-light); +} + +.conversation-item { + display: flex; + flex-direction: column; + padding: 12px 16px; + background: white; + border: 1px solid var(--border-color); + border-radius: 12px; + cursor: pointer; + transition: all 0.2s; + position: relative; +} + +.conversation-item:hover { + border-color: var(--primary); + background: rgba(102, 126, 234, 0.05); +} + +.conversation-item:active { + background: rgba(102, 126, 234, 0.1); +} + +.conv-title { + font-size: 16px; + font-weight: 500; + color: var(--text-color); + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.conv-meta { + font-size: 12px; + color: var(--text-light); +} + +.conv-delete-btn { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + background: transparent; + border: none; + color: var(--text-light); + padding: 6px; + cursor: pointer; + opacity: 0; + transition: all 0.2s; +} + +.conversation-item:hover .conv-delete-btn { + opacity: 1; +} + +.conv-delete-btn:hover { + color: #e53e3e; +} + +/* ==================== 对话页面 ==================== */ + +#chatPage { + display: flex; + flex-direction: column; + height: 100vh; + height: 100dvh; + max-width: 800px; + margin: 0 auto; + background: var(--card-bg); + box-shadow: var(--shadow); +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: linear-gradient(135deg, var(--primary) 0%, #764ba2 100%); + color: white; + position: sticky; + top: 0; + z-index: 100; +} + +.back-btn { + background: transparent; + border: none; + color: white; + padding: 8px; + cursor: pointer; + display: flex; + align-items: center; +} + +.back-btn:active { + opacity: 0.8; +} + +.header-title { + display: flex; + align-items: center; + gap: 10px; + flex: 1; + margin-left: 8px; +} + +.header-title h1 { + font-size: 16px; + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.logo { + font-size: 20px; +} + +.clear-btn { + background: rgba(255,255,255,0.2); + border: none; + border-radius: 8px; + padding: 8px; + color: white; + cursor: pointer; +} + +.clear-btn:active { + background: rgba(255,255,255,0.3); +} + +.messages-container { + flex: 1; + overflow-y: auto; + padding: 16px; + -webkit-overflow-scrolling: touch; +} + +.welcome { + text-align: center; + padding: 40px 20px; +} + +.welcome-icon { + font-size: 48px; + margin-bottom: 16px; +} + +.welcome h2 { + font-size: 20px; + margin-bottom: 8px; +} + +.welcome p { + color: var(--text-light); + margin-bottom: 24px; +} + +.quick-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: center; +} + +.quick-actions button { + padding: 8px 16px; + border: 1px solid var(--primary); + background: white; + color: var(--primary); + border-radius: 20px; + font-size: 14px; + cursor: pointer; +} + +.quick-actions button:active { + background: var(--primary); + color: white; +} + +.messages { + display: flex; + flex-direction: column; + gap: 16px; +} + +.message { + display: flex; + gap: 12px; + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.message.user { + flex-direction: row-reverse; +} + +.message-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + flex-shrink: 0; +} + +.message.user .message-avatar { + background: var(--user-msg); + color: white; +} + +.message.assistant .message-avatar { + background: var(--primary); + color: white; +} + +.message-content { + padding: 12px 16px; + border-radius: 18px; + word-wrap: break-word; +} + +.message.user .message-content { + background: var(--user-msg); + color: white; + border-bottom-right-radius: 4px; +} + +.message.assistant .message-content { + background: var(--ai-msg); + color: var(--text-color); + border-bottom-left-radius: 4px; + border: 1px solid var(--border-color); +} + +.message-content code { + background: rgba(0,0,0,0.1); + padding: 2px 6px; + border-radius: 4px; + font-family: 'Monaco', 'Menlo', monospace; + font-size: 0.9em; +} + +.message-content pre { + background: #1a1a2e; + color: #eee; + padding: 12px; + border-radius: 8px; + overflow-x: auto; + margin: 8px 0; +} + +.message-content pre code { + background: transparent; + padding: 0; +} + +.message.user .message-content pre { + background: rgba(0,0,0,0.2); +} + +/* 流式输出光标 */ +.streaming-cursor { + animation: blink 1s infinite; + color: var(--primary); + font-weight: bold; +} + +@keyframes blink { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0; } +} + +/* 消息结构 */ +.message-body { + display: flex; + flex-direction: column; + gap: 8px; +} + +.message.user .message-body { + max-width: 85%; +} + +.message.assistant .message-body { + max-width: 80%; +} + +/* 消息操作按钮 */ +.message-actions { + display: flex; + gap: 6px; + opacity: 0; + transition: opacity 0.2s; +} + +.message:hover .message-actions { + opacity: 1; +} + +.action-btn { + background: transparent; + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 4px 6px; + color: var(--text-light); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.action-btn:hover { + background: var(--border-color); + color: var(--text-color); +} + +.action-btn.copy-btn:hover { + background: #dbeafe; + border-color: var(--primary); + color: var(--primary); +} + +.action-btn.delete-btn:hover { + background: #fee2e2; + border-color: #e53e3e; + color: #e53e3e; +} + +.action-btn.regenerate-btn:hover { + background: #dbeafe; + border-color: var(--primary); + color: var(--primary); +} + +/* Markdown 内容样式 */ +.message-content h1, .message-content h2, .message-content h3 { + margin: 12px 0 8px; + font-weight: 600; +} + +.message-content h1 { font-size: 1.3em; } +.message-content h2 { font-size: 1.2em; } +.message-content h3 { font-size: 1.1em; } + +.message-content ul, .message-content ol { + margin: 8px 0; + padding-left: 20px; +} + +.message-content li { + margin: 4px 0; +} + +.message-content strong { + font-weight: 600; +} + +.message-content em { + font-style: italic; +} + +.message-content a { + color: var(--primary); + text-decoration: underline; +} + +.message-content blockquote { + margin: 8px 0; + padding: 8px 12px; + border-left: 3px solid var(--primary); + background: rgba(102, 126, 234, 0.1); + border-radius: 4px; +} + +.message-content table { + margin: 8px 0; + border-collapse: collapse; + width: 100%; +} + +.message-content th, .message-content td { + border: 1px solid var(--border-color); + padding: 6px 10px; + text-align: left; +} + +.message-content th { + background: var(--bg-color); + font-weight: 600; +} + +/* 输入区域 */ +.input-area { + display: flex; + gap: 10px; + padding: 12px 16px; + background: white; + border-top: 1px solid var(--border-color); + position: sticky; + bottom: 0; +} + +#userInput { + flex: 1; + padding: 12px 16px; + border: 2px solid var(--border-color); + border-radius: 24px; + font-size: 16px; + resize: none; + outline: none; + font-family: inherit; + max-height: 120px; + transition: border-color 0.2s; +} + +#userInput:focus { + border-color: var(--primary); +} + +.send-btn { + width: 48px; + height: 48px; + border-radius: 50%; + background: linear-gradient(135deg, var(--primary) 0%, #764ba2 100%); + border: none; + color: white; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.2s, opacity 0.2s; + flex-shrink: 0; +} + +.send-btn:active { + transform: scale(0.95); +} + +.send-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Toast 提示 */ +.toast { + position: fixed; + top: 80px; + left: 50%; + transform: translateX(-50%) translateY(-20px); + background: var(--text-color); + color: white; + padding: 12px 24px; + border-radius: 8px; + font-size: 14px; + opacity: 0; + transition: all 0.3s ease; + z-index: 1000; + box-shadow: 0 4px 12px rgba(0,0,0,0.2); +} + +.toast.show { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + +/* 安全区域适配(刘海屏) */ +@supports (padding: env(safe-area-inset-bottom)) { + .input-area { + padding-bottom: calc(12px + env(safe-area-inset-bottom)); + } +} \ No newline at end of file