6363 </div >
6464 </div >
6565
66+ <div v-if =" loadingAi || (aiProgress > 0 && aiProgress < 100)" class =" ai-progress-container" >
67+ <div class =" ai-progress-bar" :style =" { width: aiProgress + '%' }" ></div >
68+ </div >
69+
6670 <div v-if =" loadingAi && !aiRawExplanation" class =" ai-loading" >
6771 <div class =" spinner" ></div >
6872 <p >Ollama 正在思考中,请稍候...</p >
194198</template >
195199
196200<script setup>
197- import { ref , watch , onMounted , computed } from ' vue' ;
201+ import { ref , watch , onMounted , computed , onUnmounted } from ' vue' ;
198202import axios from ' axios' ;
199203import { marked } from ' marked' ;
200204
@@ -207,6 +211,8 @@ const aiRawExplanation = ref('');
207211const verificationStatus = ref ({});
208212const loading = ref (false );
209213const loadingAi = ref (false );
214+ const aiProgress = ref (0 );
215+ let aiProgressInterval = null ;
210216const error = ref (' ' );
211217const aiError = ref (' ' );
212218const showSuggestions = ref (false );
@@ -288,6 +294,29 @@ const hideSuggestionsWithDelay = () => {
288294 }, 200 );
289295};
290296
297+ const startProgress = () => {
298+ aiProgress .value = 0 ;
299+ if (aiProgressInterval) clearInterval (aiProgressInterval);
300+ // 假设通常响应在 6-10 秒左右
301+ aiProgressInterval = setInterval (() => {
302+ if (aiProgress .value < 90 ) {
303+ aiProgress .value += (90 - aiProgress .value ) * 0.1 ;
304+ }
305+ }, 500 );
306+ };
307+
308+ const completeProgress = () => {
309+ if (aiProgressInterval) clearInterval (aiProgressInterval);
310+ aiProgress .value = 100 ;
311+ setTimeout (() => {
312+ aiProgress .value = 0 ;
313+ }, 500 );
314+ };
315+
316+ onUnmounted (() => {
317+ if (aiProgressInterval) clearInterval (aiProgressInterval);
318+ });
319+
291320const conjugate = async () => {
292321 error .value = ' ' ;
293322 result .value = null ;
@@ -313,6 +342,11 @@ const conjugate = async () => {
313342 });
314343 result .value = response .data ;
315344
345+ // 如果返回了合法的 dictionaryForm,将其同步回输入框(包含汉字转换)
346+ if (result .value .dictionaryForm ) {
347+ form .value .verb = result .value .dictionaryForm ;
348+ }
349+
316350 // 自动触发 AI 解析
317351 fetchAiExplanation ();
318352 } catch (err) {
@@ -338,6 +372,7 @@ const fetchAiExplanation = async () => {
338372 verificationStatus .value = {};
339373
340374 try {
375+ startProgress ();
341376 const response = await fetch (' /api/ai-explain' , {
342377 method: ' POST' ,
343378 headers: {
@@ -375,29 +410,57 @@ const fetchAiExplanation = async () => {
375410 if (eventStr .startsWith (' data: ' )) {
376411 const dataStr = eventStr .slice (6 );
377412 if (dataStr === ' [DONE]' ) {
413+ completeProgress ();
378414 break ;
379415 }
380416 try {
381417 const data = JSON .parse (dataStr);
382418 if (data .error ) {
383419 aiError .value = data .error ;
420+ completeProgress ();
384421 } else if (data .content ) {
385422 fullAiText += data .content ;
386423
387424 // 尝试匹配 AI 返回的 JSON 代码块
388- const jsonMatch = fullAiText .match (/ ```(?:json)? \s * \n ([\s\S ] *? )\n ```/ i );
425+ const jsonMatch = fullAiText .match (/ ```(?:json)? \s * \n ([\s\S ] *? )(?: \n ```| $ ) / i );
389426 if (jsonMatch) {
390427 try {
391- verificationStatus .value = JSON .parse (jsonMatch[1 ]);
392- // JSON 之后的内容作为解释显示
393- aiRawExplanation .value = fullAiText .substring (jsonMatch .index + jsonMatch[0 ].length ).trim ();
428+ // 如果代码块完整,直接解析
429+ const parsed = JSON .parse (jsonMatch[1 ]);
430+ verificationStatus .value = parsed;
431+ aiRawExplanation .value = fullAiText .substring (0 , jsonMatch .index ).trim ();
394432 } catch (e) {
395- // JSON 解析失败说明还在流式输出 JSON,忽略
433+ // JSON 解析失败说明还在流式输出 JSON,尝试用部分匹配提前点亮 ✅
434+ const partialJson = jsonMatch[1 ];
435+ const items = partialJson .split (/ },? / );
436+ for (let item of items) {
437+ const keyMatch = item .match (/ "([a-zA-Z ] + )"\s * :\s * \{ / );
438+ const isCorrectMatch = item .match (/ "isCorrect"\s * :\s * (true| false)/ );
439+ if (keyMatch && isCorrectMatch) {
440+ const key = keyMatch[1 ];
441+ const isCorrect = isCorrectMatch[1 ] === ' true' ;
442+ // 简单提取 correction(如果不完整可能提取不到,但主要是为了尽早显示正确状态)
443+ const correctionMatch = item .match (/ "correction"\s * :\s * "([^ "] * )"/ );
444+ const correction = correctionMatch ? correctionMatch[1 ] : " " ;
445+
446+ if (! verificationStatus .value [key]) {
447+ verificationStatus .value = {
448+ ... verificationStatus .value ,
449+ [key]: { isCorrect, correction }
450+ };
451+ }
452+ }
453+ }
454+ aiRawExplanation .value = fullAiText .substring (0 , jsonMatch .index ).trim ();
396455 }
397456 } else {
398- // 如果还没有完整的 JSON 块,且不是以 JSON 块开头,直接显示
399- if (! fullAiText .trim (). startsWith (' ```' )) {
457+ // 如果还没有遇到 JSON 块开头,直接显示当前所有内容
458+ if (! fullAiText .includes (' ```' )) {
400459 aiRawExplanation .value = fullAiText;
460+ } else {
461+ // 如果遇到了块开头,但还没闭合,只显示开头前面的部分
462+ const blockStart = fullAiText .indexOf (' ```' );
463+ aiRawExplanation .value = fullAiText .substring (0 , blockStart).trim ();
401464 }
402465 }
403466 }
@@ -409,6 +472,7 @@ const fetchAiExplanation = async () => {
409472 }
410473 } catch (err) {
411474 aiError .value = err .message || ' AI 解析请求失败' ;
475+ completeProgress ();
412476 } finally {
413477 loadingAi .value = false ;
414478 }
@@ -683,6 +747,22 @@ const fetchAiExplanation = async () => {
683747}
684748
685749/* AI 解释区域样式 */
750+ .ai - progress- container {
751+ width: 100 % ;
752+ height: 4px ;
753+ background- color: #edf2f7;
754+ border- radius: 2px ;
755+ margin- bottom: 15px ;
756+ overflow: hidden;
757+ }
758+
759+ .ai - progress- bar {
760+ height: 100 % ;
761+ background- color: #667eea ;
762+ transition: width 0 .3s ease;
763+ border- radius: 2px ;
764+ }
765+
686766.ai - loading {
687767 display: flex;
688768 align- items: center;
@@ -752,8 +832,15 @@ const fetchAiExplanation = async () => {
752832 color: #999 ;
753833}
754834
835+ @keyframes popIn {
836+ 0 % { transform: scale (0.5 ); opacity: 0 ; }
837+ 100 % { transform: scale (1 ); opacity: 1 ; }
838+ }
839+
755840.success - check {
756841 color: #38a169 ;
842+ animation: popIn 0 .3s cubic- bezier (0.175 , 0.885 , 0.32 , 1.275 ) forwards;
843+ display: inline- block;
757844}
758845
759846.error - correction {
@@ -763,6 +850,8 @@ const fetchAiExplanation = async () => {
763850 padding: 2px 6px ;
764851 border- radius: 4px ;
765852 border: 1px solid #fed7d7;
853+ animation: popIn 0 .3s cubic- bezier (0.175 , 0.885 , 0.32 , 1.275 ) forwards;
854+ display: inline- block;
766855}
767856
768857.model - select {
0 commit comments