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 = ` +
+
+
+ +

AI助手

+
+
+ +
+ + +
+ ${conversations.length === 0 + ? '
暂无对话记录
' + : conversations.map(conv => ` +
+
${escapeHtml(conv.title)}
+
${conv.messages.length} 条消息 · ${formatTime(conv.updatedAt)}
+ +
+ `).join('') + } +
+
+
+ `; + + appContainer.innerHTML = listHtml; +} + +// 创建新对话 +function createNewConversation() { + const newConv = { + id: Date.now().toString(), + title: '新对话', + messages: [], + createdAt: Date.now(), + updatedAt: Date.now() + }; + + conversations.unshift(newConv); + saveConversations(); + openConversation(newConv.id); +} + +// 打开对话 +function openConversation(id) { + currentConversation = conversations.find(c => c.id === id); + if (!currentConversation) { + showConversationList(); + return; + } + + // 渲染对话页面 + const chatHtml = ` +
+
+ +
+ +

${escapeHtml(currentConversation.title)}

+
+ +
+ +
+
+
👋
+

你好!我是AI助手

+

有什么可以帮助你的吗?

+
+ + + +
+
+
+
+ +
+ + +
+
+ `; + + appContainer.innerHTML = chatHtml; + + // 重新获取 DOM 元素 + messagesContainer = document.getElementById('messagesContainer'); + messagesDiv = document.getElementById('messages'); + userInput = document.getElementById('userInput'); + sendBtn = document.getElementById('sendBtn'); + welcome = document.getElementById('welcome'); + + // 渲染消息 + renderMessages(); + userInput.focus(); + + // 绑定快捷按钮事件 + document.querySelectorAll('.quick-btn').forEach(btn => { + btn.addEventListener('click', () => { + const text = btn.getAttribute('data-text'); + userInput.value = text; + sendMessage(); + }); + }); +} + +// 删除对话 +function deleteConversation(id) { + if (!confirm('确定要删除这个对话吗?')) return; + + conversations = conversations.filter(c => c.id !== id); + saveConversations(); + showConversationList(); +} + +// ==================== 对话页面 ==================== + +// 自动调整输入框高度 +function autoResize(textarea) { + textarea.style.height = 'auto'; + textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px'; +} + +// 处理键盘事件 +function handleKeyDown(event) { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + sendMessage(); + } +} + +// 发送快捷消息 +function sendQuickMessage(text) { + userInput.value = text; + sendMessage(); +} + +// 发送消息(流式输出) +async function sendMessage() { + if (!currentConversation) return; + + const text = userInput.value.trim(); + if (!text || isLoading) return; + + // 隐藏欢迎界面 + welcome.style.display = 'none'; + + // 添加用户消息 + currentConversation.messages.push({ role: 'user', content: text }); + + // 更新对话标题(第一条用户消息) + if (currentConversation.title === '新对话') { + currentConversation.title = text.slice(0, 30) + (text.length > 30 ? '...' : ''); + // 更新标题显示 + const titleEl = document.querySelector('.header h1'); + if (titleEl) { + titleEl.textContent = currentConversation.title; + } + } + + currentConversation.updatedAt = Date.now(); + saveConversations(); + + renderMessages(); + userInput.value = ''; + autoResize(userInput); + + // 调用流式生成 + await streamGenerate(currentConversation.messages.length - 1); +} + +// 流式生成 AI 回复 +async function streamGenerate(userMsgIndex) { + isLoading = true; + sendBtn.disabled = true; + + const aiMessageIndex = currentConversation.messages.length; + currentConversation.messages.push({ role: 'assistant', content: '' }); + renderMessages(); + + const lastMessageEl = messagesDiv.lastElementChild; + const contentEl = lastMessageEl.querySelector('.message-content'); + contentEl.innerHTML = ''; + + try { + const response = await fetch(CONFIG.apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${CONFIG.apiKey}` + }, + body: JSON.stringify({ + model: CONFIG.model, + messages: currentConversation.messages.slice(0, aiMessageIndex).map(m => ({ + role: m.role, + content: m.content + })), + max_tokens: CONFIG.maxTokens, + stream: true + }) + }); + + if (!response.ok) { + throw new Error(`API 错误: ${response.status}`); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + if (line.startsWith('data: ')) { + const jsonStr = line.slice(6).trim(); + if (jsonStr === '[DONE]') continue; + + try { + const data = JSON.parse(jsonStr); + if (data.choices && data.choices[0]?.delta?.content) { + currentConversation.messages[aiMessageIndex].content += data.choices[0].delta.content; + contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content) + ''; + scrollToBottom(); + } + } catch (e) {} + } + } + } + + contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content); + + } catch (error) { + console.error('Error:', error); + currentConversation.messages[aiMessageIndex].content = `抱歉,出现了错误:${error.message}\n\n请检查网络连接后重试。`; + contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content); + } finally { + isLoading = false; + sendBtn.disabled = false; + currentConversation.updatedAt = Date.now(); + saveConversations(); + renderMessages(); + } +} + +// 重新生成 AI 回复 +async function regenerate(index) { + if (!currentConversation || isLoading || index < 1) return; + + const userMsgIndex = index - 1; + if (currentConversation.messages[userMsgIndex].role !== 'user') return; + + currentConversation.messages.splice(index, 1); + currentConversation.updatedAt = Date.now(); + saveConversations(); + + await streamGenerate(userMsgIndex); +} + +// 删除消息 +function deleteMessage(index) { + if (!currentConversation || isLoading) return; + + const msg = currentConversation.messages[index]; + + if (msg.role === 'assistant') { + if (index > 0 && currentConversation.messages[index - 1].role === 'user') { + currentConversation.messages.splice(index - 1, 2); + } else { + currentConversation.messages.splice(index, 1); + } + } else { + if (index < currentConversation.messages.length - 1 && currentConversation.messages[index + 1].role === 'assistant') { + currentConversation.messages.splice(index, 2); + } else { + currentConversation.messages.splice(index, 1); + } + } + + currentConversation.updatedAt = Date.now(); + saveConversations(); + renderMessages(); + + if (currentConversation.messages.length === 0) { + welcome.style.display = 'block'; + } +} + +// 复制消息(复制原文) +function copyMessage(index) { + if (!currentConversation) return; + + const content = currentConversation.messages[index].content; + + navigator.clipboard.writeText(content).then(() => { + showToast('已复制到剪贴板'); + }).catch(err => { + const textarea = document.createElement('textarea'); + textarea.value = content; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + document.body.removeChild(textarea); + showToast('已复制到剪贴板'); + }); +} + +// 清空当前对话 +function clearCurrentChat() { + if (!currentConversation) return; + + if (confirm('确定要清空当前对话吗?')) { + currentConversation.messages = []; + currentConversation.updatedAt = Date.now(); + saveConversations(); + renderMessages(); + welcome.style.display = 'block'; + } +} + +// 渲染消息 +function renderMessages() { + if (!currentConversation) return; + + messagesDiv.innerHTML = currentConversation.messages.map((msg, index) => { + const isUser = msg.role === 'user'; + const avatar = isUser ? '👤' : '🤖'; + const content = renderMarkdown(msg.content); + + const copyIcon = ``; + + const actions = isUser + ? `
+ + +
` + : `
+ + + +
`; + + return ` +
+
${avatar}
+
+
${content}
+ ${actions} +
+
+ `; + }).join(''); + + scrollToBottom(); +} + +// ==================== 工具函数 ==================== + +// 渲染 Markdown +function renderMarkdown(text) { + if (!text) return ''; + + marked.setOptions({ + breaks: true, + gfm: true + }); + + return marked.parse(text); +} + +// 滚动到底部 +function scrollToBottom() { + if (messagesContainer) { + messagesContainer.scrollTop = messagesContainer.scrollHeight; + } +} + +// 保存对话列表 +function saveConversations() { + localStorage.setItem('conversations', JSON.stringify(conversations)); +} + +// 显示提示 +function showToast(message) { + const toast = document.createElement('div'); + toast.className = 'toast'; + toast.textContent = message; + document.body.appendChild(toast); + + setTimeout(() => toast.classList.add('show'), 10); + setTimeout(() => { + toast.classList.remove('show'); + setTimeout(() => document.body.removeChild(toast), 300); + }, 2000); +} + +// HTML转义 +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +// 格式化时间 +function formatTime(timestamp) { + const date = new Date(timestamp); + const now = new Date(); + const diff = now - date; + + if (diff < 60000) return '刚刚'; + if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前'; + if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前'; + if (diff < 604800000) return Math.floor(diff / 86400000) + '天前'; + + return date.toLocaleDateString('zh-CN'); +} + +// PWA 注册 +if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('sw.js').catch(() => {}); +} \ No newline at end of file diff --git a/www/index.html b/www/index.html new file mode 100644 index 0000000..cc32465 --- /dev/null +++ b/www/index.html @@ -0,0 +1,16 @@ + + + + + + + AI助手 + + + + +
+ + + + \ No newline at end of file diff --git a/www/manifest.json b/www/manifest.json new file mode 100644 index 0000000..b7094c6 --- /dev/null +++ b/www/manifest.json @@ -0,0 +1,22 @@ +{ + "name": "AI助手", + "short_name": "AI助手", + "description": "AI对话助手移动端应用", + "start_url": "/", + "display": "standalone", + "background_color": "#f7fafc", + "theme_color": "#667eea", + "orientation": "portrait", + "icons": [ + { + "src": "icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} \ No newline at end of file diff --git a/www/marked.min.js b/www/marked.min.js new file mode 100644 index 0000000..b4e0d73 --- /dev/null +++ b/www/marked.min.js @@ -0,0 +1,69 @@ +/** + * marked v15.0.12 - a markdown parser + * Copyright (c) 2011-2025, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ + +/** + * DO NOT EDIT THIS FILE + * The code in this file is generated from files in ./src/ + */ +(function(g,f){if(typeof exports=="object"&&typeof module<"u"){module.exports=f()}else if("function"==typeof define && define.amd){define("marked",f)}else {g["marked"]=f()}}(typeof globalThis < "u" ? globalThis : typeof self < "u" ? self : this,function(){var exports={};var __exports=exports;var module={exports}; +"use strict";var H=Object.defineProperty;var be=Object.getOwnPropertyDescriptor;var Te=Object.getOwnPropertyNames;var we=Object.prototype.hasOwnProperty;var ye=(l,e)=>{for(var t in e)H(l,t,{get:e[t],enumerable:!0})},Re=(l,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Te(e))!we.call(l,s)&&s!==t&&H(l,s,{get:()=>e[s],enumerable:!(n=be(e,s))||n.enumerable});return l};var Se=l=>Re(H({},"__esModule",{value:!0}),l);var kt={};ye(kt,{Hooks:()=>L,Lexer:()=>x,Marked:()=>E,Parser:()=>b,Renderer:()=>$,TextRenderer:()=>_,Tokenizer:()=>S,defaults:()=>w,getDefaults:()=>z,lexer:()=>ht,marked:()=>k,options:()=>it,parse:()=>pt,parseInline:()=>ct,parser:()=>ut,setOptions:()=>ot,use:()=>lt,walkTokens:()=>at});module.exports=Se(kt);function z(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var w=z();function N(l){w=l}var I={exec:()=>null};function h(l,e=""){let t=typeof l=="string"?l:l.source,n={replace:(s,i)=>{let r=typeof i=="string"?i:i.source;return r=r.replace(m.caret,"$1"),t=t.replace(s,r),n},getRegex:()=>new RegExp(t,e)};return n}var m={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] /,listReplaceTask:/^\[[ xX]\] +/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:l=>new RegExp(`^( {0,3}${l})((?:[ ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`),hrRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),fencesBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}(?:\`\`\`|~~~)`),headingBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}#`),htmlBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}<(?:[a-z].*>|!--)`,"i")},$e=/^(?:[ \t]*(?:\n|$))+/,_e=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,Le=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,O=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,ze=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,F=/(?:[*+-]|\d{1,9}[.)])/,ie=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,oe=h(ie).replace(/bull/g,F).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/\|table/g,"").getRegex(),Me=h(ie).replace(/bull/g,F).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/table/g,/ {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(),Q=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,Pe=/^[^\n]+/,U=/(?!\s*\])(?:\\.|[^\[\]\\])+/,Ae=h(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",U).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),Ee=h(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,F).getRegex(),v="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",K=/|$))/,Ce=h("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))","i").replace("comment",K).replace("tag",v).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),le=h(Q).replace("hr",O).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex(),Ie=h(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",le).getRegex(),X={blockquote:Ie,code:_e,def:Ae,fences:Le,heading:ze,hr:O,html:Ce,lheading:oe,list:Ee,newline:$e,paragraph:le,table:I,text:Pe},re=h("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",O).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3} )[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex(),Oe={...X,lheading:Me,table:re,paragraph:h(Q).replace("hr",O).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",re).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex()},Be={...X,html:h(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",K).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:I,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:h(Q).replace("hr",O).replace("heading",` *#{1,6} *[^ +]`).replace("lheading",oe).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},qe=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,ve=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,ae=/^( {2,}|\\)\n(?!\s*$)/,De=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\]*?>/g,ue=/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,je=h(ue,"u").replace(/punct/g,D).getRegex(),Fe=h(ue,"u").replace(/punct/g,pe).getRegex(),he="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",Qe=h(he,"gu").replace(/notPunctSpace/g,ce).replace(/punctSpace/g,W).replace(/punct/g,D).getRegex(),Ue=h(he,"gu").replace(/notPunctSpace/g,He).replace(/punctSpace/g,Ge).replace(/punct/g,pe).getRegex(),Ke=h("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,ce).replace(/punctSpace/g,W).replace(/punct/g,D).getRegex(),Xe=h(/\\(punct)/,"gu").replace(/punct/g,D).getRegex(),We=h(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),Je=h(K).replace("(?:-->|$)","-->").getRegex(),Ve=h("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",Je).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),q=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,Ye=h(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label",q).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),ke=h(/^!?\[(label)\]\[(ref)\]/).replace("label",q).replace("ref",U).getRegex(),ge=h(/^!?\[(ref)\](?:\[\])?/).replace("ref",U).getRegex(),et=h("reflink|nolink(?!\\()","g").replace("reflink",ke).replace("nolink",ge).getRegex(),J={_backpedal:I,anyPunctuation:Xe,autolink:We,blockSkip:Ne,br:ae,code:ve,del:I,emStrongLDelim:je,emStrongRDelimAst:Qe,emStrongRDelimUnd:Ke,escape:qe,link:Ye,nolink:ge,punctuation:Ze,reflink:ke,reflinkSearch:et,tag:Ve,text:De,url:I},tt={...J,link:h(/^!?\[(label)\]\((.*?)\)/).replace("label",q).getRegex(),reflink:h(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",q).getRegex()},j={...J,emStrongRDelimAst:Ue,emStrongLDelim:Fe,url:h(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,"i").replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\":">",'"':""","'":"'"},fe=l=>st[l];function R(l,e){if(e){if(m.escapeTest.test(l))return l.replace(m.escapeReplace,fe)}else if(m.escapeTestNoEncode.test(l))return l.replace(m.escapeReplaceNoEncode,fe);return l}function V(l){try{l=encodeURI(l).replace(m.percentDecode,"%")}catch{return null}return l}function Y(l,e){let t=l.replace(m.findPipe,(i,r,o)=>{let a=!1,c=r;for(;--c>=0&&o[c]==="\\";)a=!a;return a?"|":" |"}),n=t.split(m.splitPipe),s=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),e)if(n.length>e)n.splice(e);else for(;n.length0?-2:-1}function me(l,e,t,n,s){let i=e.href,r=e.title||null,o=l[1].replace(s.other.outputLinkReplace,"$1");n.state.inLink=!0;let a={type:l[0].charAt(0)==="!"?"image":"link",raw:t,href:i,title:r,text:o,tokens:n.inlineTokens(o)};return n.state.inLink=!1,a}function rt(l,e,t){let n=l.match(t.other.indentCodeCompensation);if(n===null)return e;let s=n[1];return e.split(` +`).map(i=>{let r=i.match(t.other.beginningSpace);if(r===null)return i;let[o]=r;return o.length>=s.length?i.slice(s.length):i}).join(` +`)}var S=class{options;rules;lexer;constructor(e){this.options=e||w}space(e){let t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){let t=this.rules.block.code.exec(e);if(t){let n=t[0].replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?n:A(n,` +`)}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let n=t[0],s=rt(n,t[3]||"",this.rules);return{type:"code",raw:n,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:s}}}heading(e){let t=this.rules.block.heading.exec(e);if(t){let n=t[2].trim();if(this.rules.other.endingHash.test(n)){let s=A(n,"#");(this.options.pedantic||!s||this.rules.other.endingSpaceChar.test(s))&&(n=s.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:n,tokens:this.lexer.inline(n)}}}hr(e){let t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:A(t[0],` +`)}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let n=A(t[0],` +`).split(` +`),s="",i="",r=[];for(;n.length>0;){let o=!1,a=[],c;for(c=0;c1,i={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");let r=this.rules.other.listItemRegex(n),o=!1;for(;e;){let c=!1,p="",u="";if(!(t=r.exec(e))||this.rules.block.hr.test(e))break;p=t[0],e=e.substring(p.length);let d=t[2].split(` +`,1)[0].replace(this.rules.other.listReplaceTabs,Z=>" ".repeat(3*Z.length)),g=e.split(` +`,1)[0],T=!d.trim(),f=0;if(this.options.pedantic?(f=2,u=d.trimStart()):T?f=t[1].length+1:(f=t[2].search(this.rules.other.nonSpaceChar),f=f>4?1:f,u=d.slice(f),f+=t[1].length),T&&this.rules.other.blankLine.test(g)&&(p+=g+` +`,e=e.substring(g.length+1),c=!0),!c){let Z=this.rules.other.nextBulletRegex(f),te=this.rules.other.hrRegex(f),ne=this.rules.other.fencesBeginRegex(f),se=this.rules.other.headingBeginRegex(f),xe=this.rules.other.htmlBeginRegex(f);for(;e;){let G=e.split(` +`,1)[0],C;if(g=G,this.options.pedantic?(g=g.replace(this.rules.other.listReplaceNesting," "),C=g):C=g.replace(this.rules.other.tabCharGlobal," "),ne.test(g)||se.test(g)||xe.test(g)||Z.test(g)||te.test(g))break;if(C.search(this.rules.other.nonSpaceChar)>=f||!g.trim())u+=` +`+C.slice(f);else{if(T||d.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||ne.test(d)||se.test(d)||te.test(d))break;u+=` +`+g}!T&&!g.trim()&&(T=!0),p+=G+` +`,e=e.substring(G.length+1),d=C.slice(f)}}i.loose||(o?i.loose=!0:this.rules.other.doubleBlankLine.test(p)&&(o=!0));let y=null,ee;this.options.gfm&&(y=this.rules.other.listIsTask.exec(u),y&&(ee=y[0]!=="[ ] ",u=u.replace(this.rules.other.listReplaceTask,""))),i.items.push({type:"list_item",raw:p,task:!!y,checked:ee,loose:!1,text:u,tokens:[]}),i.raw+=p}let a=i.items.at(-1);if(a)a.raw=a.raw.trimEnd(),a.text=a.text.trimEnd();else return;i.raw=i.raw.trimEnd();for(let c=0;cd.type==="space"),u=p.length>0&&p.some(d=>this.rules.other.anyLine.test(d.raw));i.loose=u}if(i.loose)for(let c=0;c({text:a,tokens:this.lexer.inline(a),header:!1,align:r.align[c]})));return r}}lheading(e){let t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:t[2].charAt(0)==="="?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){let t=this.rules.block.paragraph.exec(e);if(t){let n=t[1].charAt(t[1].length-1)===` +`?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:n,tokens:this.lexer.inline(n)}}}text(e){let t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){let t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){let t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){let t=this.rules.inline.link.exec(e);if(t){let n=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(n)){if(!this.rules.other.endAngleBracket.test(n))return;let r=A(n.slice(0,-1),"\\");if((n.length-r.length)%2===0)return}else{let r=de(t[2],"()");if(r===-2)return;if(r>-1){let a=(t[0].indexOf("!")===0?5:4)+t[1].length+r;t[2]=t[2].substring(0,r),t[0]=t[0].substring(0,a).trim(),t[3]=""}}let s=t[2],i="";if(this.options.pedantic){let r=this.rules.other.pedanticHrefTitle.exec(s);r&&(s=r[1],i=r[3])}else i=t[3]?t[3].slice(1,-1):"";return s=s.trim(),this.rules.other.startAngleBracket.test(s)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(n)?s=s.slice(1):s=s.slice(1,-1)),me(t,{href:s&&s.replace(this.rules.inline.anyPunctuation,"$1"),title:i&&i.replace(this.rules.inline.anyPunctuation,"$1")},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let s=(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," "),i=t[s.toLowerCase()];if(!i){let r=n[0].charAt(0);return{type:"text",raw:r,text:r}}return me(n,i,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s||s[3]&&n.match(this.rules.other.unicodeAlphaNumeric))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){let r=[...s[0]].length-1,o,a,c=r,p=0,u=s[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(u.lastIndex=0,t=t.slice(-1*e.length+r);(s=u.exec(t))!=null;){if(o=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!o)continue;if(a=[...o].length,s[3]||s[4]){c+=a;continue}else if((s[5]||s[6])&&r%3&&!((r+a)%3)){p+=a;continue}if(c-=a,c>0)continue;a=Math.min(a,a+c+p);let d=[...s[0]][0].length,g=e.slice(0,r+s.index+d+a);if(Math.min(r,a)%2){let f=g.slice(1,-1);return{type:"em",raw:g,text:f,tokens:this.lexer.inlineTokens(f)}}let T=g.slice(2,-2);return{type:"strong",raw:g,text:T,tokens:this.lexer.inlineTokens(T)}}}}codespan(e){let t=this.rules.inline.code.exec(e);if(t){let n=t[2].replace(this.rules.other.newLineCharGlobal," "),s=this.rules.other.nonSpaceChar.test(n),i=this.rules.other.startingSpaceChar.test(n)&&this.rules.other.endingSpaceChar.test(n);return s&&i&&(n=n.substring(1,n.length-1)),{type:"codespan",raw:t[0],text:n}}}br(e){let t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){let t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){let t=this.rules.inline.autolink.exec(e);if(t){let n,s;return t[2]==="@"?(n=t[1],s="mailto:"+n):(n=t[1],s=n),{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let n,s;if(t[2]==="@")n=t[0],s="mailto:"+n;else{let i;do i=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??"";while(i!==t[0]);n=t[0],t[1]==="www."?s="http://"+t[0]:s=t[0]}return{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}inlineText(e){let t=this.rules.inline.text.exec(e);if(t){let n=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:n}}}};var x=class l{tokens;options;state;tokenizer;inlineQueue;constructor(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||w,this.options.tokenizer=this.options.tokenizer||new S,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};let t={other:m,block:B.normal,inline:P.normal};this.options.pedantic?(t.block=B.pedantic,t.inline=P.pedantic):this.options.gfm&&(t.block=B.gfm,this.options.breaks?t.inline=P.breaks:t.inline=P.gfm),this.tokenizer.rules=t}static get rules(){return{block:B,inline:P}}static lex(e,t){return new l(t).lex(e)}static lexInline(e,t){return new l(t).inlineTokens(e)}lex(e){e=e.replace(m.carriageReturn,` +`),this.blockTokens(e,this.tokens);for(let t=0;t(s=r.call({lexer:this},e,t))?(e=e.substring(s.raw.length),t.push(s),!0):!1))continue;if(s=this.tokenizer.space(e)){e=e.substring(s.raw.length);let r=t.at(-1);s.raw.length===1&&r!==void 0?r.raw+=` +`:t.push(s);continue}if(s=this.tokenizer.code(e)){e=e.substring(s.raw.length);let r=t.at(-1);r?.type==="paragraph"||r?.type==="text"?(r.raw+=` +`+s.raw,r.text+=` +`+s.text,this.inlineQueue.at(-1).src=r.text):t.push(s);continue}if(s=this.tokenizer.fences(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.heading(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.hr(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.blockquote(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.list(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.html(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.def(e)){e=e.substring(s.raw.length);let r=t.at(-1);r?.type==="paragraph"||r?.type==="text"?(r.raw+=` +`+s.raw,r.text+=` +`+s.raw,this.inlineQueue.at(-1).src=r.text):this.tokens.links[s.tag]||(this.tokens.links[s.tag]={href:s.href,title:s.title});continue}if(s=this.tokenizer.table(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.lheading(e)){e=e.substring(s.raw.length),t.push(s);continue}let i=e;if(this.options.extensions?.startBlock){let r=1/0,o=e.slice(1),a;this.options.extensions.startBlock.forEach(c=>{a=c.call({lexer:this},o),typeof a=="number"&&a>=0&&(r=Math.min(r,a))}),r<1/0&&r>=0&&(i=e.substring(0,r+1))}if(this.state.top&&(s=this.tokenizer.paragraph(i))){let r=t.at(-1);n&&r?.type==="paragraph"?(r.raw+=` +`+s.raw,r.text+=` +`+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=r.text):t.push(s),n=i.length!==e.length,e=e.substring(s.raw.length);continue}if(s=this.tokenizer.text(e)){e=e.substring(s.raw.length);let r=t.at(-1);r?.type==="text"?(r.raw+=` +`+s.raw,r.text+=` +`+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=r.text):t.push(s);continue}if(e){let r="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(r);break}else throw new Error(r)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n=e,s=null;if(this.tokens.links){let o=Object.keys(this.tokens.links);if(o.length>0)for(;(s=this.tokenizer.rules.inline.reflinkSearch.exec(n))!=null;)o.includes(s[0].slice(s[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(s=this.tokenizer.rules.inline.anyPunctuation.exec(n))!=null;)n=n.slice(0,s.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;(s=this.tokenizer.rules.inline.blockSkip.exec(n))!=null;)n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);let i=!1,r="";for(;e;){i||(r=""),i=!1;let o;if(this.options.extensions?.inline?.some(c=>(o=c.call({lexer:this},e,t))?(e=e.substring(o.raw.length),t.push(o),!0):!1))continue;if(o=this.tokenizer.escape(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.tag(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.link(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(o.raw.length);let c=t.at(-1);o.type==="text"&&c?.type==="text"?(c.raw+=o.raw,c.text+=o.text):t.push(o);continue}if(o=this.tokenizer.emStrong(e,n,r)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.codespan(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.br(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.del(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.autolink(e)){e=e.substring(o.raw.length),t.push(o);continue}if(!this.state.inLink&&(o=this.tokenizer.url(e))){e=e.substring(o.raw.length),t.push(o);continue}let a=e;if(this.options.extensions?.startInline){let c=1/0,p=e.slice(1),u;this.options.extensions.startInline.forEach(d=>{u=d.call({lexer:this},p),typeof u=="number"&&u>=0&&(c=Math.min(c,u))}),c<1/0&&c>=0&&(a=e.substring(0,c+1))}if(o=this.tokenizer.inlineText(a)){e=e.substring(o.raw.length),o.raw.slice(-1)!=="_"&&(r=o.raw.slice(-1)),i=!0;let c=t.at(-1);c?.type==="text"?(c.raw+=o.raw,c.text+=o.text):t.push(o);continue}if(e){let c="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(c);break}else throw new Error(c)}}return t}};var $=class{options;parser;constructor(e){this.options=e||w}space(e){return""}code({text:e,lang:t,escaped:n}){let s=(t||"").match(m.notSpaceStart)?.[0],i=e.replace(m.endingNewline,"")+` +`;return s?'
'+(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)} +`}hr(e){return`
+`}list(e){let t=e.ordered,n=e.start,s="";for(let o=0;o +`+s+" +`}listitem(e){let t="";if(e.task){let n=this.checkbox({checked:!!e.checked});e.loose?e.tokens[0]?.type==="paragraph"?(e.tokens[0].text=n+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&e.tokens[0].tokens[0].type==="text"&&(e.tokens[0].tokens[0].text=n+" "+R(e.tokens[0].tokens[0].text),e.tokens[0].tokens[0].escaped=!0)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" ",escaped:!0}):t+=n+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`
  • ${t}
  • +`}checkbox({checked:e}){return"'}paragraph({tokens:e}){return`

    ${this.parser.parseInline(e)}

    +`}table(e){let t="",n="";for(let i=0;i${s}`),` + +`+t+` +`+s+`
    +`}tablerow({text:e}){return` +${e} +`}tablecell(e){let t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+` +`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${R(e,!0)}`}br(e){return"
    "}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:n}){let s=this.parser.parseInline(n),i=V(e);if(i===null)return s;e=i;let r='
    ",r}image({href:e,title:t,text:n,tokens:s}){s&&(n=this.parser.parseInline(s,this.parser.textRenderer));let i=V(e);if(i===null)return R(n);e=i;let r=`${n}{let o=i[r].flat(1/0);n=n.concat(this.walkTokens(o,t))}):i.tokens&&(n=n.concat(this.walkTokens(i.tokens,t)))}}return n}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(n=>{let s={...n};if(s.async=this.defaults.async||s.async||!1,n.extensions&&(n.extensions.forEach(i=>{if(!i.name)throw new Error("extension name required");if("renderer"in i){let r=t.renderers[i.name];r?t.renderers[i.name]=function(...o){let a=i.renderer.apply(this,o);return a===!1&&(a=r.apply(this,o)),a}:t.renderers[i.name]=i.renderer}if("tokenizer"in i){if(!i.level||i.level!=="block"&&i.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");let r=t[i.level];r?r.unshift(i.tokenizer):t[i.level]=[i.tokenizer],i.start&&(i.level==="block"?t.startBlock?t.startBlock.push(i.start):t.startBlock=[i.start]:i.level==="inline"&&(t.startInline?t.startInline.push(i.start):t.startInline=[i.start]))}"childTokens"in i&&i.childTokens&&(t.childTokens[i.name]=i.childTokens)}),s.extensions=t),n.renderer){let i=this.defaults.renderer||new $(this.defaults);for(let r in n.renderer){if(!(r in i))throw new Error(`renderer '${r}' does not exist`);if(["options","parser"].includes(r))continue;let o=r,a=n.renderer[o],c=i[o];i[o]=(...p)=>{let u=a.apply(i,p);return u===!1&&(u=c.apply(i,p)),u||""}}s.renderer=i}if(n.tokenizer){let i=this.defaults.tokenizer||new S(this.defaults);for(let r in n.tokenizer){if(!(r in i))throw new Error(`tokenizer '${r}' does not exist`);if(["options","rules","lexer"].includes(r))continue;let o=r,a=n.tokenizer[o],c=i[o];i[o]=(...p)=>{let u=a.apply(i,p);return u===!1&&(u=c.apply(i,p)),u}}s.tokenizer=i}if(n.hooks){let i=this.defaults.hooks||new L;for(let r in n.hooks){if(!(r in i))throw new Error(`hook '${r}' does not exist`);if(["options","block"].includes(r))continue;let o=r,a=n.hooks[o],c=i[o];L.passThroughHooks.has(r)?i[o]=p=>{if(this.defaults.async)return Promise.resolve(a.call(i,p)).then(d=>c.call(i,d));let u=a.call(i,p);return c.call(i,u)}:i[o]=(...p)=>{let u=a.apply(i,p);return u===!1&&(u=c.apply(i,p)),u}}s.hooks=i}if(n.walkTokens){let i=this.defaults.walkTokens,r=n.walkTokens;s.walkTokens=function(o){let a=[];return a.push(r.call(this,o)),i&&(a=a.concat(i.call(this,o))),a}}this.defaults={...this.defaults,...s}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return x.lex(e,t??this.defaults)}parser(e,t){return b.parse(e,t??this.defaults)}parseMarkdown(e){return(n,s)=>{let i={...s},r={...this.defaults,...i},o=this.onError(!!r.silent,!!r.async);if(this.defaults.async===!0&&i.async===!1)return o(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof n>"u"||n===null)return o(new Error("marked(): input parameter is undefined or null"));if(typeof n!="string")return o(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));r.hooks&&(r.hooks.options=r,r.hooks.block=e);let a=r.hooks?r.hooks.provideLexer():e?x.lex:x.lexInline,c=r.hooks?r.hooks.provideParser():e?b.parse:b.parseInline;if(r.async)return Promise.resolve(r.hooks?r.hooks.preprocess(n):n).then(p=>a(p,r)).then(p=>r.hooks?r.hooks.processAllTokens(p):p).then(p=>r.walkTokens?Promise.all(this.walkTokens(p,r.walkTokens)).then(()=>p):p).then(p=>c(p,r)).then(p=>r.hooks?r.hooks.postprocess(p):p).catch(o);try{r.hooks&&(n=r.hooks.preprocess(n));let p=a(n,r);r.hooks&&(p=r.hooks.processAllTokens(p)),r.walkTokens&&this.walkTokens(p,r.walkTokens);let u=c(p,r);return r.hooks&&(u=r.hooks.postprocess(u)),u}catch(p){return o(p)}}}onError(e,t){return n=>{if(n.message+=` +Please report this to https://github.com/markedjs/marked.`,e){let s="

    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