Skip to content

Commit 2e888bd

Browse files
许君山许君山
authored andcommitted
feat: integrate local ollama qwen2.5 for AI verb explanation and examples
1 parent 9047f27 commit 2e888bd

4 files changed

Lines changed: 152 additions & 2 deletions

File tree

backend/server.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { conjugate } from './conjugationEngine.js';
66
import fs from 'fs';
77
import path from 'path';
88
import { fileURLToPath } from 'url';
9+
import { Ollama } from 'ollama';
910

1011
const __filename = fileURLToPath(import.meta.url);
1112
const __dirname = path.dirname(__filename);
@@ -75,11 +76,36 @@ function detectVerbType(verb) {
7576
return null;
7677
}
7778

79+
// 初始化 Ollama
80+
const ollama = new Ollama({ host: 'http://127.0.0.1:11434' });
81+
7882
// 健康检查
7983
app.get('/health', (req, res) => {
8084
res.json({ status: 'ok', dictionaryReady: !!tokenizer });
8185
});
8286

87+
// AI 动词解析及例句生成 API
88+
app.get('/api/ai-explain', async (req, res) => {
89+
try {
90+
const { verb } = req.query;
91+
if (!verb) {
92+
return res.status(400).json({ error: 'Missing required parameter: verb' });
93+
}
94+
95+
const prompt = `你是一个日语语言学专家。请你用中文简明扼要地解释日语动词 "${verb}" 的含义,并提供2个实用的日常例句(包含日文、平假名注音和中文翻译)。不要输出多余的寒暄,直接输出结构化的内容。`;
96+
97+
const response = await ollama.chat({
98+
model: 'qwen2.5',
99+
messages: [{ role: 'user', content: prompt }],
100+
});
101+
102+
res.json({ explanation: response.message.content });
103+
} catch (error) {
104+
console.error('Ollama API Error:', error);
105+
res.status(500).json({ error: 'AI 服务暂不可用,请确保本地 Ollama 及 qwen2.5 模型已启动。' });
106+
}
107+
});
108+
83109
// 动词自动补全 API
84110
app.get('/api/suggest', (req, res) => {
85111
try {

frontend/package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
"preview": "vite preview"
99
},
1010
"dependencies": {
11-
"vue": "^3.4.0",
12-
"axios": "^1.6.0"
11+
"axios": "^1.6.0",
12+
"marked": "^18.0.2",
13+
"vue": "^3.4.0"
1314
},
1415
"devDependencies": {
1516
"@vitejs/plugin-vue": "^5.0.0",

frontend/src/App.vue

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,28 @@
9999
</div>
100100
</div>
101101
</div>
102+
103+
<!-- AI 解释区域 -->
104+
<div v-if="result || loadingAi || aiError" class="card result-card">
105+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
106+
<h3 style="margin-bottom: 0;">✨ AI 深度解析与例句 (Qwen2.5)</h3>
107+
<button v-if="!aiExplanation && !loadingAi && result" @click="fetchAiExplanation" class="btn-secondary">
108+
获取解析
109+
</button>
110+
</div>
111+
112+
<div v-if="loadingAi" class="ai-loading">
113+
<div class="spinner"></div>
114+
<p>Ollama 正在思考中,请稍候...</p>
115+
</div>
116+
117+
<div v-else-if="aiError" class="error-message">
118+
{{ aiError }}
119+
<button @click="fetchAiExplanation" style="margin-left: 10px; background: none; border: underline; color: inherit; cursor: pointer;">重试</button>
120+
</div>
121+
122+
<div v-else-if="aiExplanation" class="ai-content markdown-body" v-html="aiExplanation"></div>
123+
</div>
102124
</section>
103125

104126
<!-- 右侧:文档区 -->
@@ -182,14 +204,18 @@
182204
<script setup>
183205
import { ref, watch } from 'vue';
184206
import axios from 'axios';
207+
import { marked } from 'marked';
185208
186209
const form = ref({
187210
verb: ''
188211
});
189212
190213
const result = ref(null);
214+
const aiExplanation = ref(null);
191215
const loading = ref(false);
216+
const loadingAi = ref(false);
192217
const error = ref('');
218+
const aiError = ref('');
193219
const showSuggestions = ref(false);
194220
const suggestions = ref([]);
195221
let suggestTimeout = null;
@@ -239,6 +265,8 @@ const hideSuggestionsWithDelay = () => {
239265
const conjugate = async () => {
240266
error.value = '';
241267
result.value = null;
268+
aiExplanation.value = null;
269+
aiError.value = '';
242270
243271
if (!form.value.verb) {
244272
error.value = '请输入动词';
@@ -253,12 +281,33 @@ const conjugate = async () => {
253281
}
254282
});
255283
result.value = response.data;
284+
285+
// 自动触发 AI 解析
286+
fetchAiExplanation();
256287
} catch (err) {
257288
error.value = err.response?.data?.error || '请求失败,请检查输入';
258289
} finally {
259290
loading.value = false;
260291
}
261292
};
293+
294+
const fetchAiExplanation = async () => {
295+
if (!result.value?.dictionaryForm) return;
296+
297+
loadingAi.value = true;
298+
aiError.value = '';
299+
300+
try {
301+
const response = await axios.get('/api/ai-explain', {
302+
params: { verb: result.value.dictionaryForm }
303+
});
304+
aiExplanation.value = marked(response.data.explanation);
305+
} catch (err) {
306+
aiError.value = err.response?.data?.error || 'AI 解析请求失败';
307+
} finally {
308+
loadingAi.value = false;
309+
}
310+
};
262311
</script>
263312
264313
<style scoped>
@@ -509,4 +558,65 @@ const conjugate = async () => {
509558
.guide-item strong {
510559
color: #333;
511560
}
561+
562+
/* AI 解释区域样式 */
563+
.ai-loading {
564+
display: flex;
565+
align-items: center;
566+
justify-content: center;
567+
gap: 15px;
568+
padding: 30px;
569+
color: #666;
570+
}
571+
572+
.spinner {
573+
width: 24px;
574+
height: 24px;
575+
border: 3px solid #e0e0e0;
576+
border-top-color: #667eea;
577+
border-radius: 50%;
578+
animation: spin 1s linear infinite;
579+
}
580+
581+
@keyframes spin {
582+
to { transform: rotate(360deg); }
583+
}
584+
585+
.ai-content {
586+
background: #fdfdfd;
587+
padding: 20px;
588+
border-radius: 8px;
589+
border: 1px solid #eee;
590+
line-height: 1.6;
591+
color: #333;
592+
}
593+
594+
.btn-secondary {
595+
padding: 6px 12px;
596+
background: #f0f0f0;
597+
color: #333;
598+
border: 1px solid #ddd;
599+
border-radius: 6px;
600+
font-size: 0.9em;
601+
cursor: pointer;
602+
transition: all 0.2s;
603+
}
604+
605+
.btn-secondary:hover {
606+
background: #e4e4e4;
607+
}
608+
609+
/* 简单的 markdown 样式补充 */
610+
.markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4 {
611+
margin-top: 10px;
612+
margin-bottom: 10px;
613+
color: #2c3e50;
614+
}
615+
.markdown-body p {
616+
margin-bottom: 10px;
617+
}
618+
.markdown-body ul, .markdown-body ol {
619+
padding-left: 20px;
620+
margin-bottom: 10px;
621+
}
512622
</style>

0 commit comments

Comments
 (0)