@@ -5,6 +5,7 @@ const CONFIG = {
apiUrl : 'https://open.bigmodel.cn/api/paas/v4/chat/completions' ,
apiKey : '2259e33a1357460abe17919aaf81e73d.K44a8LPQTmFM5PKm' ,
model : 'glm-4.5-air' ,
thinkingModel : 'glm-z1-flash' , // 智谱思考模型
maxTokens : 2048
} ;
@@ -13,6 +14,10 @@ let conversations = []; // 对话列表
let currentConversation = null ; // 当前对话
let isLoading = false ;
// 功能开关
let enableThinking = false ; // 深度思考
let enableSearch = false ; // 联网搜索
// DOM 元素(初始为 null, 在 openConversation 时重新获取)
let appContainer = null ;
let messagesContainer = null ;
@@ -20,6 +25,8 @@ let messagesDiv = null;
let userInput = null ;
let sendBtn = null ;
let welcome = null ;
let thinkingBtn = null ;
let searchBtn = null ;
// 初始化
document . addEventListener ( 'DOMContentLoaded' , ( ) => {
@@ -171,6 +178,18 @@ function openConversation(id) {
<div class="messages" id="messages"></div>
</div>
<!-- 功能开关栏 -->
<div class="feature-bar">
<button class="feature-btn thinking-btn ${ enableThinking ? 'active' : '' } " id="thinkingBtn">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
<span>深度思考</span>
</button>
<button class="feature-btn search-btn ${ enableSearch ? 'active' : '' } " id="searchBtn">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 7 9.5 7 14 9.01 14 9.5 11.99 14 9.5 14z"/></svg>
<span>联网搜索</span>
</button>
</div>
<div class="input-area">
<button class="attach-btn" id="attachBtn" title="上传文件">
<svg viewBox="0 0 24 24" width="24" height="24"><path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
@@ -211,6 +230,8 @@ function openConversation(id) {
userInput = document . getElementById ( 'userInput' ) ;
sendBtn = document . getElementById ( 'sendBtn' ) ;
welcome = document . getElementById ( 'welcome' ) ;
thinkingBtn = document . getElementById ( 'thinkingBtn' ) ;
searchBtn = document . getElementById ( 'searchBtn' ) ;
// 绑定按钮事件
const backBtn = document . getElementById ( 'backBtn' ) ;
@@ -219,6 +240,31 @@ function openConversation(id) {
const clearBtn = document . getElementById ( 'clearBtn' ) ;
if ( clearBtn ) clearBtn . addEventListener ( 'click' , clearCurrentChat ) ;
// 绑定功能开关按钮事件
if ( thinkingBtn ) {
thinkingBtn . addEventListener ( 'click' , ( ) => {
enableThinking = ! enableThinking ;
thinkingBtn . classList . toggle ( 'active' , enableThinking ) ;
// 如果开启深度思考,关闭联网搜索(智谱思考模型不支持联网)
if ( enableThinking && enableSearch ) {
enableSearch = false ;
searchBtn . classList . remove ( 'active' ) ;
}
} ) ;
}
if ( searchBtn ) {
searchBtn . addEventListener ( 'click' , ( ) => {
enableSearch = ! enableSearch ;
searchBtn . classList . toggle ( 'active' , enableSearch ) ;
// 如果开启联网搜索,关闭深度思考
if ( enableSearch && enableThinking ) {
enableThinking = false ;
thinkingBtn . classList . remove ( 'active' ) ;
}
} ) ;
}
// 绑定输入事件
userInput . addEventListener ( 'keydown' , handleKeyDown ) ;
userInput . addEventListener ( 'input' , ( e ) => autoResize ( e . target ) ) ;
@@ -349,14 +395,32 @@ async function streamGenerate(userMsgIndex) {
sendBtn . disabled = true ;
const aiMessageIndex = currentConversation . messages . length ;
currentConversation . messages . push ( { role : 'assistant' , content : '' } ) ;
// 只有开启深度思考时才添加 thinking 字段
currentConversation . messages . push ( {
role : 'assistant' ,
content : '' ,
... ( enableThinking ? { thinking : '' } : { } )
} ) ;
renderMessages ( ) ;
const lastMessageEl = messagesDiv . lastElementChild ;
const contentEl = lastMessageEl . querySelector ( '.message-content' ) ;
const thinkingEl = lastMessageEl . querySelector ( '.thinking-content' ) ;
// 深度思考模式:思考块默认展开
if ( enableThinking && thinkingEl ) {
const thinkingBlock = lastMessageEl . querySelector ( '.thinking-block' ) ;
if ( thinkingBlock ) thinkingBlock . classList . add ( 'expanded' ) ;
thinkingEl . innerHTML = '<span class="streaming-cursor">思考中...</span>' ;
}
contentEl . innerHTML = '<span class="streaming-cursor">▌</span>' ;
try {
// 根据开关选择模型
const model = enableThinking ? CONFIG . thinkingModel : CONFIG . model ;
const response = await fetch ( CONFIG . apiUrl , {
method : 'POST' ,
headers : {
@@ -364,7 +428,7 @@ async function streamGenerate(userMsgIndex) {
'Authorization' : ` Bearer ${ CONFIG . apiKey } `
} ,
body : JSON . stringify ( {
model : CONFIG . model,
model : model ,
messages : currentConversation . messages . slice ( 0 , aiMessageIndex ) . map ( m => ( {
role : m . role ,
content : m . content
@@ -381,6 +445,7 @@ async function streamGenerate(userMsgIndex) {
const reader = response . body . getReader ( ) ;
const decoder = new TextDecoder ( ) ;
let buffer = '' ;
let thinkingOutputStarted = false ; // 正式内容是否开始输出
while ( true ) {
const { done , value } = await reader . read ( ) ;
@@ -397,16 +462,44 @@ async function streamGenerate(userMsgIndex) {
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 ) + '<span class="streaming-cursor">▌</span>' ;
scrollToBottom ( ) ;
const delta = data . choices ? . [ 0 ] ? . delta ;
if ( delta ) {
// 只有开启深度思考时才处理思考内容
if ( enableThinking && ( delta . reasoning _content || delta . thinking ) ) {
const thinkingChunk = delta . reasoning _content || delta . thinking ;
currentConversation . messages [ aiMessageIndex ] . thinking += thinkingChunk ;
if ( thinkingEl ) {
thinkingEl . innerHTML = renderMarkdown ( currentConversation . messages [ aiMessageIndex ] . thinking ) + '<span class="streaming-cursor">▌</span>' ;
}
scrollToBottom ( ) ;
}
// 处理正式回复内容
if ( delta . content ) {
// 如果开启深度思考且开始输出正式内容,折叠思考块
if ( enableThinking && ! thinkingOutputStarted && currentConversation . messages [ aiMessageIndex ] . thinking ) {
thinkingOutputStarted = true ;
// 折叠思考内容
const thinkingBlock = lastMessageEl . querySelector ( '.thinking-block' ) ;
if ( thinkingBlock ) thinkingBlock . classList . remove ( 'expanded' ) ;
if ( thinkingEl ) thinkingEl . innerHTML = renderMarkdown ( currentConversation . messages [ aiMessageIndex ] . thinking ) ;
}
currentConversation . messages [ aiMessageIndex ] . content += delta . content ;
contentEl . innerHTML = renderMarkdown ( currentConversation . messages [ aiMessageIndex ] . content ) + '<span class="streaming-cursor">▌</span>' ;
scrollToBottom ( ) ;
}
}
} catch ( e ) { }
}
}
}
// 最终渲染
if ( thinkingEl && enableThinking && currentConversation . messages [ aiMessageIndex ] . thinking ) {
thinkingEl . innerHTML = renderMarkdown ( currentConversation . messages [ aiMessageIndex ] . thinking ) ;
}
contentEl . innerHTML = renderMarkdown ( currentConversation . messages [ aiMessageIndex ] . content ) ;
} catch ( error ) {
@@ -469,21 +562,38 @@ function deleteMessage(index) {
function copyMessage ( index ) {
if ( ! currentConversation ) return ;
const content = currentConversation . messages [ index ] . content ;
const msg = currentConversation . messages [ index ] ;
// 如果是图片消息,复制图片描述或提示
let content = msg . content ;
if ( msg . image && content === '[图片]' ) {
content = '[图片: ' + ( msg . imageName || '未命名' ) + ']' ;
}
navigator. clipboard. writeText ( content ) . then ( ( ) => {
showToast ( '已复制到剪贴板' ) ;
} ) . catch ( err => {
// HTTP 环境下 navigator. clipboard 不工作,优先使用 fallback
try {
const textarea = document . createElement ( 'textarea' ) ;
textarea . value = content ;
textarea . style . position = 'fixed' ;
textarea . style . top = '0' ;
textarea . style . left = '0' ;
textarea . style . opacity = '0' ;
textarea . style . pointerEvents = 'none' ;
document . body . appendChild ( textarea ) ;
textarea . focus ( ) ;
textarea . select ( ) ;
document . execCommand ( 'copy' ) ;
const success = document . execCommand ( 'copy' ) ;
document . body . removeChild ( textarea ) ;
showToast ( '已复制到剪贴板' ) ;
} ) ;
if ( success ) {
showToast ( '已复制到剪贴板' ) ;
} else {
showToast ( '复制失败,请手动复制' ) ;
}
} catch ( err ) {
console . error ( '复制失败:' , err ) ;
showToast ( '复制失败,请手动复制' ) ;
}
}
// 清空当前对话
@@ -524,6 +634,20 @@ function renderMessages() {
contentHtml = renderMarkdown ( msg . content ) ;
}
// 思考内容块( 仅AI消息)
let thinkingHtml = '' ;
if ( ! isUser && msg . thinking ) {
thinkingHtml = `
<div class="thinking-block" onclick="toggleThinking(this)">
<div class="thinking-header">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
<span>思考过程</span>
<svg class="thinking-arrow" viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M7 10l5 5 5-5z"/></svg>
</div>
<div class="thinking-content"> ${ renderMarkdown ( msg . thinking ) } </div>
</div> ` ;
}
const copyIcon = ` <svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg> ` ;
const actions = isUser
@@ -547,6 +671,7 @@ function renderMessages() {
<div class="message ${ msg . role } " data-index=" ${ index } ">
<div class="message-avatar"> ${ avatar } </div>
<div class="message-body">
${ thinkingHtml }
<div class="message-content"> ${ contentHtml } </div>
${ actions }
</div>
@@ -568,6 +693,11 @@ function renderMessages() {
scrollToBottom ( ) ;
}
// 折叠/展开思考内容
function toggleThinking ( block ) {
block . classList . toggle ( 'expanded' ) ;
}
// ==================== 工具函数 ====================
// 渲染 Markdown