Skip to content

Commit b3003d9

Browse files
许君山许君山
authored andcommitted
feat: parse AI verification JSON to display inline checkmarks on conjugation table
1 parent 54ac381 commit b3003d9

2 files changed

Lines changed: 102 additions & 40 deletions

File tree

backend/server.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,24 @@ app.post('/api/ai-explain', async (req, res) => {
110110
${JSON.stringify(conjugationResult, null, 2)}
111111
\`\`\`
112112
113-
请你执行以下两个任务:
114-
1. **核对纠错**:快速核对上方 JSON 中的变形结果(特别是て形、た形等特殊音变)。如果变形完全正确,请简短地给出一句肯定评价(如"系统生成的变形结果正确");如果发现错误,请明确指出哪里错了,并给出正确的变形形式。
115-
2. **释义与例句**:用中文简明扼要地解释该动词的含义,并提供2个实用的日常例句(必须包含日文原文、平假名注音和精准的中文翻译)。
113+
请你严格按照以下结构执行任务:
116114
117-
注意:直接输出结构化的 Markdown 格式内容,不需要任何多余的开场白或寒暄。`;
115+
第一步:逐个核对上述 JSON 中的变形结果。必须且只能以一个 JSON 代码块开始你的回答,不要有任何前置文本。格式如下:
116+
\`\`\`json
117+
{
118+
"negative": { "isCorrect": true, "correction": "" },
119+
"polite": { "isCorrect": true, "correction": "" },
120+
"teForm": { "isCorrect": false, "correction": "正确的变形" },
121+
"taForm": { "isCorrect": true, "correction": "" },
122+
"potential": { "isCorrect": true, "correction": "" },
123+
"passive": { "isCorrect": true, "correction": "" },
124+
"causative": { "isCorrect": true, "correction": "" },
125+
"imperative": { "isCorrect": true, "correction": "" },
126+
"volitional": { "isCorrect": true, "correction": "" }
127+
}
128+
\`\`\`
129+
130+
第二步:在 JSON 代码块之后,用中文简明扼要地解释该动词的含义,并提供2个实用的日常例句(必须包含日文原文、平假名注音和精准的中文翻译)。支持使用 Markdown 格式加粗、高亮。`;
118131

119132
res.setHeader('Content-Type', 'text/event-stream');
120133
res.setHeader('Cache-Control', 'no-cache');

frontend/src/App.vue

Lines changed: 85 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -88,41 +88,21 @@
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
217197
const result = ref(null);
218198
const aiRawExplanation = ref('');
199+
const verificationStatus = ref({});
219200
const loading = ref(false);
220201
const loadingAi = ref(false);
221202
const error = ref('');
@@ -226,6 +207,18 @@ const availableModels = ref([]);
226207
const selectedModel = ref('');
227208
let 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+
229222
const 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

Comments
 (0)