Skip to content

Commit 9bd8e74

Browse files
许君山许君山
authored andcommitted
feat: output examples in structured JSON and render as custom UI components
1 parent dc21e01 commit 9bd8e74

2 files changed

Lines changed: 111 additions & 36 deletions

File tree

backend/server.js

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -158,32 +158,44 @@ app.post('/api/ai-explain', async (req, res) => {
158158
${JSON.stringify(conjugationResult, null, 2)}
159159
\`\`\`
160160
161-
请你严格按照以下结构执行任务:
161+
请你严格按照以下步骤和格式执行任务,优先级顺序为:1. 校对结果 -> 2. 实用例句 -> 3. 词义解析。
162162
163-
第一步:优先逐个核对上述 JSON 中的变形结果。必须且只能以一个 JSON 代码块开始你的回答,不要有任何前置文本。格式要求:
164-
1. 请只核对这 9 种变形:negative, polite, teForm, taForm, potential, passive, causative, imperative, volitional。
165-
2. 必须使用给定的英文 key。
166-
3. 如果结果完全正确,请将 isCorrect 设置为 true,correction 必须为空字符串 ""。
167-
4. 只有当你 100% 确定系统生成的结果错误时,才将 isCorrect 设置为 false,并在 correction 中给出正确的日文。
168-
5. 不要因为送气音或汉字/假名的写法不同就认为是错的。
163+
第一步与第二步:必须且只能以一个 JSON 代码块开始你的回答,包含校对结果(verification)和2个实用例句(examples)。不要有任何前置文本。
164+
格式要求:
165+
1. verification 中只核对这 9 种变形:negative, polite, teForm, taForm, potential, passive, causative, imperative, volitional。如果正确,isCorrect 为 true,correction 为 ""。如果错误,isCorrect 为 false,并在 correction 中给出正确日文。不要因为送气音或汉字/假名写法不同算错。
166+
2. examples 中提供2个实用的日常例句,包含日文原文(japanese)、平假名注音(kana)和中文翻译(chinese)。
169167
170168
返回的 JSON 必须严格遵循如下结构(此为全对的示例):
171169
\`\`\`json
172170
{
173-
"negative": { "isCorrect": true, "correction": "" },
174-
"polite": { "isCorrect": true, "correction": "" },
175-
"teForm": { "isCorrect": true, "correction": "" },
176-
"taForm": { "isCorrect": true, "correction": "" },
177-
"potential": { "isCorrect": true, "correction": "" },
178-
"passive": { "isCorrect": true, "correction": "" },
179-
"causative": { "isCorrect": true, "correction": "" },
180-
"imperative": { "isCorrect": true, "correction": "" },
181-
"volitional": { "isCorrect": true, "correction": "" }
171+
"verification": {
172+
"negative": { "isCorrect": true, "correction": "" },
173+
"polite": { "isCorrect": true, "correction": "" },
174+
"teForm": { "isCorrect": true, "correction": "" },
175+
"taForm": { "isCorrect": true, "correction": "" },
176+
"potential": { "isCorrect": true, "correction": "" },
177+
"passive": { "isCorrect": true, "correction": "" },
178+
"causative": { "isCorrect": true, "correction": "" },
179+
"imperative": { "isCorrect": true, "correction": "" },
180+
"volitional": { "isCorrect": true, "correction": "" }
181+
},
182+
"examples": [
183+
{
184+
"japanese": "日文例句1",
185+
"kana": "平假名注音1",
186+
"chinese": "中文翻译1"
187+
},
188+
{
189+
"japanese": "日文例句2",
190+
"kana": "平假名注音2",
191+
"chinese": "中文翻译2"
192+
}
193+
]
182194
}
183195
\`\`\`
184196
185-
第二步:在 JSON 代码块之后,用中文简明扼要地解释该动词的含义,并提供2个实用的日常例句(必须包含日文原文、平假名注音和精准的中文翻译)。支持使用 Markdown 格式加粗、高亮
186-
重要提示:在解释动词类型时,请务必使用中国国内通用的日语教学术语(如:五段动词、一段动词、サ变动词、カ变动词),绝对不要出现 "Godan"、"Ichidan" 或 "Group 1/2/3" 等英文直译词汇。`;
197+
第三步:在 JSON 代码块闭合之后,用中文简明扼要地输出该动词的词义解析(支持 Markdown
198+
重要提示:在解释动词类型时,请务必使用中国国内通用的日语教学术语(如:五段动词、一段动词、サ变动词、カ变动词),绝对不要出现 "Godan"、"Ichidan" 等英文直译词汇。`;
187199

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

frontend/src/App.vue

Lines changed: 81 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,26 @@
7272
<p>Ollama 正在思考中,请稍候...</p>
7373
</div>
7474

75-
<div v-else-if="aiError && !aiRawExplanation" class="error-message">
75+
<div v-else-if="aiError && !aiRawExplanation && aiExamples.length === 0" class="error-message">
7676
{{ aiError }}
7777
<button @click="fetchAiExplanation" style="margin-left: 10px; background: none; border: none; text-decoration: underline; color: inherit; cursor: pointer;">重试</button>
7878
</div>
7979

80-
<div v-if="aiRawExplanation" class="ai-content markdown-body" v-html="aiExplanation"></div>
80+
<div v-if="aiExamples.length > 0" class="ai-module">
81+
<h4 class="module-title">💬 实用例句</h4>
82+
<div class="examples-grid">
83+
<div v-for="(ex, idx) in aiExamples" :key="idx" class="example-box">
84+
<div class="ex-japanese">{{ ex.japanese }}</div>
85+
<div class="ex-kana">{{ ex.kana }}</div>
86+
<div class="ex-chinese">{{ ex.chinese }}</div>
87+
</div>
88+
</div>
89+
</div>
90+
91+
<div v-if="aiRawExplanation" class="ai-module mt-4">
92+
<h4 class="module-title">📖 词义解析</h4>
93+
<div class="ai-content markdown-body" v-html="aiExplanation"></div>
94+
</div>
8195
</div>
8296

8397
<!-- 结果展示 -->
@@ -209,6 +223,7 @@ const form = ref({
209223
const result = ref(null);
210224
const aiRawExplanation = ref('');
211225
const verificationStatus = ref({});
226+
const aiExamples = ref([]);
212227
const loading = ref(false);
213228
const loadingAi = ref(false);
214229
const aiProgress = ref(0);
@@ -323,6 +338,7 @@ const conjugate = async () => {
323338
aiRawExplanation.value = '';
324339
aiError.value = '';
325340
verificationStatus.value = {};
341+
aiExamples.value = [];
326342
327343
if (!form.value.verb || !form.value.verb.trim()) {
328344
error.value = '请输入动词';
@@ -333,20 +349,17 @@ const conjugate = async () => {
333349
return;
334350
}
335351
336-
// 记录原始输入,用于后续判断是否需要更新输入框
337-
const originalInput = form.value.verb.trim();
338-
339352
loading.value = true;
340353
try {
341354
const response = await axios.get('/api/conjugate', {
342355
params: {
343-
verb: originalInput
356+
verb: form.value.verb
344357
}
345358
});
346359
result.value = response.data;
347360
348361
// 如果返回了合法的 dictionaryForm,将其同步回输入框(包含汉字转换)
349-
if (result.value.dictionaryForm && originalInput !== result.value.dictionaryForm) {
362+
if (result.value.dictionaryForm) {
350363
form.value.verb = result.value.dictionaryForm;
351364
}
352365
@@ -373,6 +386,7 @@ const fetchAiExplanation = async () => {
373386
aiError.value = '';
374387
aiRawExplanation.value = '';
375388
verificationStatus.value = {};
389+
aiExamples.value = [];
376390
377391
try {
378392
startProgress();
@@ -430,7 +444,10 @@ const fetchAiExplanation = async () => {
430444
try {
431445
// 如果代码块完整,直接解析
432446
const parsed = JSON.parse(jsonMatch[1]);
433-
verificationStatus.value = parsed;
447+
verificationStatus.value = parsed.verification || parsed;
448+
if (parsed.examples) {
449+
aiExamples.value = parsed.examples;
450+
}
434451
aiRawExplanation.value = fullAiText.substring(jsonMatch.index + jsonMatch[0].length).trim();
435452
} catch (e) {
436453
// JSON 解析失败说明还在流式输出 JSON,尝试用部分匹配提前点亮 ✅
@@ -441,16 +458,18 @@ const fetchAiExplanation = async () => {
441458
const isCorrectMatch = item.match(/"isCorrect"\s*:\s*(true|false)/);
442459
if (keyMatch && isCorrectMatch) {
443460
const key = keyMatch[1];
444-
const isCorrect = isCorrectMatch[1] === 'true';
445-
// 简单提取 correction(如果不完整可能提取不到,但主要是为了尽早显示正确状态)
446-
const correctionMatch = item.match(/"correction"\s*:\s*"([^"]*)"/);
447-
const correction = correctionMatch ? correctionMatch[1] : "";
448-
449-
if (!verificationStatus.value[key]) {
450-
verificationStatus.value = {
451-
...verificationStatus.value,
452-
[key]: { isCorrect, correction }
453-
};
461+
if (key !== 'verification' && key !== 'examples') {
462+
const isCorrect = isCorrectMatch[1] === 'true';
463+
// 简单提取 correction(如果不完整可能提取不到,但主要是为了尽早显示正确状态)
464+
const correctionMatch = item.match(/"correction"\s*:\s*"([^"]*)"/);
465+
const correction = correctionMatch ? correctionMatch[1] : "";
466+
467+
if (!verificationStatus.value[key]) {
468+
verificationStatus.value = {
469+
...verificationStatus.value,
470+
[key]: { isCorrect, correction }
471+
};
472+
}
454473
}
455474
}
456475
}
@@ -768,6 +787,50 @@ const fetchAiExplanation = async () => {
768787
border-radius: 2px;
769788
}
770789
790+
.ai-module {
791+
margin-top: 20px;
792+
border-top: 1px solid #edf2f7;
793+
padding-top: 15px;
794+
}
795+
796+
.module-title {
797+
color: #4a5568;
798+
font-size: 1.1em;
799+
margin-top: 0;
800+
margin-bottom: 15px;
801+
}
802+
803+
.examples-grid {
804+
display: flex;
805+
flex-direction: column;
806+
gap: 12px;
807+
}
808+
809+
.example-box {
810+
background: #f8fafc;
811+
padding: 15px;
812+
border-radius: 8px;
813+
border-left: 4px solid #667eea;
814+
}
815+
816+
.ex-japanese {
817+
font-size: 1.2em;
818+
font-weight: 600;
819+
color: #2d3748;
820+
margin-bottom: 4px;
821+
}
822+
823+
.ex-kana {
824+
font-size: 0.9em;
825+
color: #718096;
826+
margin-bottom: 8px;
827+
}
828+
829+
.ex-chinese {
830+
font-size: 1em;
831+
color: #4a5568;
832+
}
833+
771834
.ai-loading {
772835
display: flex;
773836
align-items: center;

0 commit comments

Comments
 (0)