8888 <span class =" label" >动词类型</span >
8989 <span class =" value" >{{ verbTypeMap[result.verbType] || result.verbType }}</span >
9090 </div >
91- <div class =" result-item" >
92- <span class =" label" >否定式</span >
93- <span class =" value" >{{ result.negative }}</span >
94- </div >
95- <div class =" result-item" >
96- <span class =" label" >礼貌式</span >
97- <span class =" value" >{{ result.polite }}</span >
98- </div >
99- <div class =" result-item" >
100- <span class =" label" >て形</span >
101- <span class =" value" >{{ result.teForm }}</span >
102- </div >
103- <div class =" result-item" >
104- <span class =" label" >过去式</span >
105- <span class =" value" >{{ result.taForm }}</span >
106- </div >
107- <div class =" result-item" >
108- <span class =" label" >可能形</span >
109- <span class =" value" >{{ result.potential }}</span >
110- </div >
111- <div class =" result-item" >
112- <span class =" label" >被动形</span >
113- <span class =" value" >{{ result.passive }}</span >
114- </div >
115- <div class =" result-item" >
116- <span class =" label" >使役形</span >
117- <span class =" value" >{{ result.causative }}</span >
118- </div >
119- <div class =" result-item" >
120- <span class =" label" >命令形</span >
121- <span class =" value" >{{ result.imperative }}</span >
122- </div >
123- <div class =" result-item" >
124- <span class =" label" >意向形</span >
125- <span class =" value" >{{ result.volitional }}</span >
91+
92+ <div class =" result-item" v-for =" item in conjugationItems" :key =" item.key" >
93+ <span class =" label" >{{ item.label }}</span >
94+ <span class =" value" >
95+ <span :class =" { 'text-strike': verificationStatus[item.key] && !verificationStatus[item.key].isCorrect }" >
96+ {{ result[item.key] }}
97+ </span >
98+ <span class =" verify-badge" v-if =" loadingAi && !verificationStatus[item.key]" >
99+ <span class =" spinner-small" title =" AI 正在核对..." ></span >
100+ </span >
101+ <span class =" verify-badge" v-else-if =" verificationStatus[item.key]" >
102+ <span v-if =" verificationStatus[item.key].isCorrect" title =" AI 核对正确" class =" success-check" >✅</span >
103+ <span v-else title =" AI 发现错误" class =" error-correction" >❌ 修正为: {{ verificationStatus[item.key].correction }}</span >
104+ </span >
105+ </span >
126106 </div >
127107 </div >
128108 </div >
@@ -216,6 +196,7 @@ const form = ref({
216196
217197const result = ref (null );
218198const aiRawExplanation = ref (' ' );
199+ const verificationStatus = ref ({});
219200const loading = ref (false );
220201const loadingAi = ref (false );
221202const error = ref (' ' );
@@ -226,6 +207,18 @@ const availableModels = ref([]);
226207const selectedModel = ref (' ' );
227208let suggestTimeout = null ;
228209
210+ const conjugationItems = [
211+ { key: ' negative' , label: ' 否定式' },
212+ { key: ' polite' , label: ' 礼貌式' },
213+ { key: ' teForm' , label: ' て形' },
214+ { key: ' taForm' , label: ' 过去式' },
215+ { key: ' potential' , label: ' 可能形' },
216+ { key: ' passive' , label: ' 被动形' },
217+ { key: ' causative' , label: ' 使役形' },
218+ { key: ' imperative' , label: ' 命令形' },
219+ { key: ' volitional' , label: ' 意向形' }
220+ ];
221+
229222const aiExplanation = computed (() => {
230223 return aiRawExplanation .value ? marked (aiRawExplanation .value ) : ' ' ;
231224});
@@ -289,6 +282,7 @@ const conjugate = async () => {
289282 result .value = null ;
290283 aiExplanation .value = null ;
291284 aiError .value = ' ' ;
285+ verificationStatus .value = {};
292286
293287 if (! form .value .verb ) {
294288 error .value = ' 请输入动词' ;
@@ -319,6 +313,7 @@ const fetchAiExplanation = async () => {
319313 loadingAi .value = true ;
320314 aiError .value = ' ' ;
321315 aiRawExplanation .value = ' ' ;
316+ verificationStatus .value = {};
322317
323318 try {
324319 const response = await fetch (' /api/ai-explain' , {
@@ -340,6 +335,7 @@ const fetchAiExplanation = async () => {
340335 const reader = response .body .getReader ();
341336 const decoder = new TextDecoder (' utf-8' );
342337 let buffer = ' ' ;
338+ let fullAiText = ' ' ;
343339
344340 loadingAi .value = false ; // 流式请求开始接收,停止显示 loading spinner
345341
@@ -364,7 +360,24 @@ const fetchAiExplanation = async () => {
364360 if (data .error ) {
365361 aiError .value = data .error ;
366362 } else if (data .content ) {
367- aiRawExplanation .value += data .content ;
363+ fullAiText += data .content ;
364+
365+ // 尝试匹配 AI 返回的 JSON 代码块
366+ const jsonMatch = fullAiText .match (/ ```(?:json)? \s * \n ([\s\S ] *? )\n ```/ i );
367+ if (jsonMatch) {
368+ try {
369+ verificationStatus .value = JSON .parse (jsonMatch[1 ]);
370+ // JSON 之后的内容作为解释显示
371+ aiRawExplanation .value = fullAiText .substring (jsonMatch .index + jsonMatch[0 ].length ).trim ();
372+ } catch (e) {
373+ // JSON 解析失败说明还在流式输出 JSON,忽略
374+ }
375+ } else {
376+ // 如果还没有完整的 JSON 块,且不是以 JSON 块开头,直接显示
377+ if (! fullAiText .trim ().startsWith (' ```' )) {
378+ aiRawExplanation .value = fullAiText;
379+ }
380+ }
368381 }
369382 } catch (e) {
370383 console .error (' JSON Parse Error' , e);
@@ -676,6 +689,42 @@ const fetchAiExplanation = async () => {
676689 background: #e4e4e4;
677690}
678691
692+ /* AI 核对徽章样式 */
693+ .verify - badge {
694+ margin- left: 8px ;
695+ font- size: 0 .9em ;
696+ display: inline- flex;
697+ align- items: center;
698+ }
699+
700+ .spinner - small {
701+ width: 14px ;
702+ height: 14px ;
703+ border: 2px solid #e0e0e0;
704+ border- top- color: #667eea ;
705+ border- radius: 50 % ;
706+ animation: spin 1s linear infinite;
707+ display: inline- block;
708+ }
709+
710+ .text - strike {
711+ text- decoration: line- through;
712+ color: #999 ;
713+ }
714+
715+ .success - check {
716+ color: #38a169 ;
717+ }
718+
719+ .error - correction {
720+ color: #e53e3e;
721+ font- weight: 600 ;
722+ background: #fff5f5;
723+ padding: 2px 6px ;
724+ border- radius: 4px ;
725+ border: 1px solid #fed7d7;
726+ }
727+
679728.model - select {
680729 padding: 6px 12px ;
681730 border: 1px solid #ddd;
0 commit comments