11async function SetVibeCodingConfig ( ) {
22 const flag = await eda . sys_Storage . getExtensionUserConfig ( 'Vibe_Coding_Config' ) ;
33 if ( flag == 'true' ) {
4- document . getElementById ( 'ai-chat' ) . style . display = '' ;
5- document . getElementById ( 'ai-btn' ) . innerText = 'AI编程:开' ;
64 eda . sys_Storage . setExtensionUserConfig ( 'Vibe_Coding_Config' , 'false' ) ;
5+ const chatEl = document . getElementById ( 'ai-chat' ) ;
6+ if ( chatEl ) chatEl . style . display = 'none' ;
7+ const btn = document . getElementById ( 'ai-btn' ) ;
8+ if ( btn ) btn . innerText = 'AI编程:关' ;
79 } else {
8- document . getElementById ( 'ai-chat' ) . style . display = 'none' ;
9- document . getElementById ( 'ai-btn' ) . innerText = 'AI编程:关' ;
1010 eda . sys_Storage . setExtensionUserConfig ( 'Vibe_Coding_Config' , 'true' ) ;
11+ const chatEl = document . getElementById ( 'ai-chat' ) ;
12+ if ( chatEl ) chatEl . style . display = '' ;
13+ const btn = document . getElementById ( 'ai-btn' ) ;
14+ if ( btn ) btn . innerText = 'AI编程:开' ;
15+ }
16+ }
17+
18+ async function GetVibeCodingConfig ( ) {
19+ const flag = await eda . sys_Storage . getExtensionUserConfig ( 'Vibe_Coding_Config' ) ;
20+ const btn = document . getElementById ( 'ai-btn' ) ;
21+ if ( ! btn ) return ;
22+
23+ const chatEl = document . getElementById ( 'ai-chat' ) ;
24+
25+ if ( flag == 'true' ) {
26+ btn . innerText = 'AI编程:开' ;
27+ if ( chatEl ) chatEl . style . display = '' ;
28+ } else {
29+ btn . innerText = 'AI编程:关' ;
30+ if ( chatEl ) chatEl . style . display = 'none' ;
1131 }
1232}
1333
1434function initAiChat ( ) {
35+ // 检查依赖库
36+ if ( typeof marked === 'undefined' || typeof hljs === 'undefined' ) {
37+ console . error ( 'AI Chat Init Error: marked or highlight.js not found.' ) ;
38+ } else {
39+ marked . setOptions ( {
40+ highlight : function ( code , lang ) {
41+ const language = hljs . getLanguage ( lang ) ? lang : 'plaintext' ;
42+ return hljs . highlight ( code , { language } ) . value ;
43+ } ,
44+ langPrefix : 'hljs language-' ,
45+ breaks : true ,
46+ gfm : true ,
47+ } ) ;
48+ }
49+
1550 const defaultConfig = {
1651 apiKey : '' ,
1752 baseUrl : 'https://api.openai.com/v1' ,
1853 model : 'gpt-4' ,
1954 multiTurn : true ,
20- stream : true , // 控制是否开启“打字机效果”
55+ stream : true ,
2156 } ;
2257
2358 let chatConfig = JSON . parse ( localStorage . getItem ( 'ai_chat_config' ) ) || defaultConfig ;
2459 let messageHistory = [ ] ;
2560
26- // DOM 元素
2761 const settingsBtn = document . getElementById ( 'ai-settings-trigger' ) ;
2862 const sendBtn = document . getElementById ( 'ai-send-btn' ) ;
2963 const inputArea = document . getElementById ( 'ai-input' ) ;
@@ -32,6 +66,51 @@ function initAiChat() {
3266
3367 if ( modelNameDisplay ) modelNameDisplay . textContent = chatConfig . model || 'AI Model' ;
3468
69+ // --- 新增:添加复制按钮的逻辑 ---
70+ function addCopyButtons ( container ) {
71+ if ( ! container ) return ;
72+
73+ const pres = container . querySelectorAll ( 'pre' ) ;
74+ pres . forEach ( ( pre ) => {
75+ // 如果已经添加过按钮,跳过
76+ if ( pre . querySelector ( '.code-copy-btn' ) ) return ;
77+
78+ const codeElement = pre . querySelector ( 'code' ) ;
79+ if ( ! codeElement ) return ;
80+
81+ const btn = document . createElement ( 'button' ) ;
82+ btn . className = 'code-copy-btn' ;
83+ btn . innerHTML = `
84+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
85+ <span>复制</span>
86+ ` ;
87+
88+ btn . onclick = async ( ) => {
89+ const text = codeElement . innerText ;
90+ try {
91+ await navigator . clipboard . writeText ( text ) ;
92+ // 更改按钮状态
93+ const originalContent = btn . innerHTML ;
94+ btn . classList . add ( 'copied' ) ;
95+ btn . innerHTML = `
96+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
97+ <span>已复制</span>
98+ ` ;
99+
100+ setTimeout ( ( ) => {
101+ btn . classList . remove ( 'copied' ) ;
102+ btn . innerHTML = originalContent ;
103+ } , 2000 ) ;
104+ } catch ( err ) {
105+ console . error ( '复制失败:' , err ) ;
106+ alert ( '复制失败,请手动复制' ) ;
107+ }
108+ } ;
109+
110+ pre . appendChild ( btn ) ;
111+ } ) ;
112+ }
113+
35114 // --- 模态框创建 ---
36115 function createModal ( ) {
37116 const overlay = document . createElement ( 'div' ) ;
@@ -58,7 +137,7 @@ function initAiChat() {
58137 <label class="ai-form-label">Model Name</label>
59138 <input type="text" id="cfg-model" class="ai-form-input" value="${ chatConfig . model } ">
60139 </div>
61- <hr style="border: 0; border-top: 1px solid #3e3d32 ; margin: 4px 0;">
140+ <hr style="border: 0; border-top: 1px solid #d0d7de ; margin: 4px 0;">
62141 <div class="ai-toggle-row">
63142 <span class="ai-toggle-label">多轮对话</span>
64143 <label class="ai-switch"><input type="checkbox" id="cfg-multi-turn" ${ chatConfig . multiTurn ? 'checked' : '' } ><span class="ai-slider"></span></label>
@@ -89,7 +168,6 @@ function initAiChat() {
89168 document . getElementById ( 'cfg-model' ) . value = chatConfig . model ;
90169 document . getElementById ( 'cfg-multi-turn' ) . checked = chatConfig . multiTurn ;
91170 document . getElementById ( 'cfg-stream' ) . checked = chatConfig . stream ;
92-
93171 setTimeout ( ( ) => modalOverlay . classList . add ( 'active' ) , 10 ) ;
94172 }
95173
@@ -109,7 +187,6 @@ function initAiChat() {
109187 alert ( '请填写 API Key' ) ;
110188 return ;
111189 }
112-
113190 chatConfig = newConfig ;
114191 localStorage . setItem ( 'ai_chat_config' , JSON . stringify ( chatConfig ) ) ;
115192 if ( ! chatConfig . multiTurn ) messageHistory = [ ] ;
@@ -134,23 +211,32 @@ function initAiChat() {
134211 function appendMessage ( role , text , isStreaming = false , isLoading = false ) {
135212 const msgDiv = document . createElement ( 'div' ) ;
136213 msgDiv . className = `ai-message ${ role === 'user' ? 'ai-message-user' : 'ai-message-system' } ` ;
137-
138214 const bubble = document . createElement ( 'div' ) ;
139215 bubble . className = 'ai-message-bubble' ;
140216
141217 if ( isLoading ) {
142- // 加载状态:添加特定的 ID 和 loading 类
143218 msgDiv . id = 'ai-loading-message' ;
144219 bubble . classList . add ( 'ai-loading-bubble' ) ;
145- // 内部放入三个跳动的点 (HTML 结构)
146220 bubble . innerHTML = '<span class="dot">.</span><span class="dot">.</span><span class="dot">.</span>' ;
147221 } else if ( isStreaming ) {
148222 bubble . id = 'streaming-bubble-' + Date . now ( ) ;
149- bubble . textContent = '' ;
150- bubble . style . borderRight = '2px solid #66d9ef' ;
151- bubble . style . animation = 'blink 1s step-end infinite' ;
152- } else {
153223 bubble . textContent = text ;
224+ bubble . classList . add ( 'streaming-cursor' ) ;
225+ bubble . style . whiteSpace = 'pre-wrap' ;
226+ } else {
227+ if ( role === 'system' && typeof marked !== 'undefined' ) {
228+ bubble . innerHTML = marked . parse ( text ) ;
229+ if ( typeof hljs !== 'undefined' ) {
230+ bubble . querySelectorAll ( 'pre code' ) . forEach ( ( block ) => {
231+ hljs . highlightElement ( block ) ;
232+ } ) ;
233+ }
234+ // 渲染完成后添加复制按钮
235+ addCopyButtons ( bubble ) ;
236+ } else {
237+ bubble . textContent = text ;
238+ bubble . style . whiteSpace = 'pre-wrap' ;
239+ }
154240 }
155241
156242 msgDiv . appendChild ( bubble ) ;
@@ -170,28 +256,22 @@ function initAiChat() {
170256 return ;
171257 }
172258
173- // 1. UI: 用户消息
174259 appendMessage ( 'user' , text ) ;
175260 inputArea . value = '' ;
176261 inputArea . style . height = 'auto' ;
177262
178- // 2. UI: 显示 "AI 正在思考..." 加载气泡
179263 const loadingElements = appendMessage ( 'system' , '' , false , true ) ;
180-
181- // 准备历史
182264 const currentMessages = [ ...messageHistory , { role : 'user' , content : text } ] ;
183265
184266 let aiBubble = null ;
185267 let fullResponse = '' ;
186268 const originalBtnText = sendBtn . textContent ;
187269
188- // UI: 锁定按钮
189270 sendBtn . disabled = true ;
190271 sendBtn . textContent = '...' ;
191272 sendBtn . style . opacity = '0.6' ;
192273
193274 try {
194- // 3. 发起请求 (始终非流式)
195275 const response = await fetch ( `${ chatConfig . baseUrl } /chat/completions` , {
196276 method : 'POST' ,
197277 headers : {
@@ -214,36 +294,44 @@ function initAiChat() {
214294 const data = await response . json ( ) ;
215295 fullResponse = data . choices ?. [ 0 ] ?. message ?. content || '无内容' ;
216296
217- // 4. 移除加载气泡
218297 if ( loadingElements && loadingElements . msgDiv ) {
219298 loadingElements . msgDiv . remove ( ) ;
220299 }
221300
222- // 5. 根据配置显示内容
223301 if ( chatConfig . stream ) {
224- // === 打字机模式 ===
225302 const streamElements = appendMessage ( 'system' , '' , true ) ;
226303 aiBubble = streamElements . bubble ;
227304
228305 let currentIndex = 0 ;
229- const speed = 20 ;
306+ const speed = 15 ;
230307
231308 const typeWriter = ( ) => {
232309 if ( currentIndex < fullResponse . length ) {
233- aiBubble . textContent += fullResponse . charAt ( currentIndex ) ;
310+ const currentText = fullResponse . slice ( 0 , currentIndex + 1 ) ;
311+ aiBubble . textContent = currentText ;
234312 currentIndex ++ ;
235313 chatList . scrollTop = chatList . scrollHeight ;
236314 setTimeout ( typeWriter , speed ) ;
237315 } else {
238- // 结束
239- aiBubble . style . borderRight = 'none' ;
240- aiBubble . style . animation = 'none' ;
316+ aiBubble . classList . remove ( 'streaming-cursor' ) ;
317+
318+ if ( typeof marked !== 'undefined' ) {
319+ aiBubble . innerHTML = marked . parse ( fullResponse ) ;
320+
321+ if ( typeof hljs !== 'undefined' ) {
322+ aiBubble . querySelectorAll ( 'pre code' ) . forEach ( ( block ) => {
323+ hljs . highlightElement ( block ) ;
324+ } ) ;
325+ }
326+ // 打字结束后添加复制按钮
327+ addCopyButtons ( aiBubble ) ;
328+ }
329+ chatList . scrollTop = chatList . scrollHeight ;
241330 finishProcess ( ) ;
242331 }
243332 } ;
244333 typeWriter ( ) ;
245334 } else {
246- // === 直接显示模式 ===
247335 appendMessage ( 'system' , fullResponse ) ;
248336 finishProcess ( ) ;
249337 }
@@ -261,20 +349,18 @@ function initAiChat() {
261349 }
262350 } catch ( error ) {
263351 console . error ( error ) ;
264- // 移除加载气泡
265352 if ( loadingElements && loadingElements . msgDiv ) {
266353 loadingElements . msgDiv . remove ( ) ;
267354 }
268355
269- // 显示错误
270356 const isDark = getComputedStyle ( document . body ) . backgroundColor === 'rgb(39, 40, 34)' ;
271357 const errStyle = isDark
272358 ? `background:#5a2d2d; border-color:#8b3a3a; color:#ffaaaa;`
273359 : `background:#ffeef0; border-color:#fdaeb7; color:#cf222e;` ;
274360
275361 const errDiv = document . createElement ( 'div' ) ;
276362 errDiv . className = 'ai-message ai-message-system' ;
277- errDiv . innerHTML = `<div class="ai-message-bubble" style="${ errStyle } ">❌ 错误: ${ error . message } </div>` ;
363+ errDiv . innerHTML = `<div class="ai-message-bubble" style="${ errStyle } ">❌ 错误: ${ error . message } </div>` ;
278364 chatList . appendChild ( errDiv ) ;
279365 chatList . scrollTop = chatList . scrollHeight ;
280366
@@ -284,7 +370,6 @@ function initAiChat() {
284370 }
285371 }
286372
287- // 初始化事件
288373 if ( settingsBtn ) settingsBtn . onclick = openModal ;
289374 if ( sendBtn ) sendBtn . onclick = handleSend ;
290375
@@ -301,40 +386,6 @@ function initAiChat() {
301386 }
302387 } ;
303388 }
304-
305- // 注入样式 (Blink 动画 + Loading 跳动动画)
306- if ( ! document . getElementById ( 'ai-chat-animations' ) ) {
307- const s = document . createElement ( 'style' ) ;
308- s . id = 'ai-chat-animations' ;
309- s . textContent = `
310- @keyframes blink { 50% { border-color: transparent; } }
311-
312- /* 加载跳动动画 */
313- .ai-loading-bubble {
314- display: flex;
315- gap: 4px;
316- padding: 12px 16px; /* 稍微调整内边距以适应圆点 */
317- align-items: center;
318- justify-content: center;
319- min-width: 60px;
320- }
321- .ai-loading-bubble .dot {
322- font-size: 20px;
323- line-height: 1;
324- color: inherit; /* 继承气泡文字颜色 */
325- opacity: 0.6;
326- animation: bounce 1.4s infinite ease-in-out both;
327- }
328- .ai-loading-bubble .dot:nth-child(1) { animation-delay: -0.32s; }
329- .ai-loading-bubble .dot:nth-child(2) { animation-delay: -0.16s; }
330-
331- @keyframes bounce {
332- 0%, 80%, 100% { transform: scale(0); }
333- 40% { transform: scale(1); }
334- }
335- ` ;
336- document . head . appendChild ( s ) ;
337- }
338389}
339390
340391document . addEventListener ( 'DOMContentLoaded' , initAiChat ) ;
0 commit comments