From f6f7f14541bcab3cd9ec3b3bec918adcca9ef1d6 Mon Sep 17 00:00:00 2001 From: kim Date: Thu, 16 Jul 2026 15:30:48 +0900 Subject: [PATCH] =?UTF-8?q?v4.1.13:=20=EC=96=B8=EC=96=B4=EC=95=B1=20?= =?UTF-8?q?=ED=83=80=EC=9D=BC=EB=B7=B0=20=EA=B8=B0=EB=B3=B8=ED=99=94=20+?= =?UTF-8?q?=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?+=20=EC=84=A0=EC=83=9D=EB=8B=98=20Q&A=20=ED=8C=A8=EB=84=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기본 보기를 타일 그리드로 전환하고 사용자가 카테고리를 직접 추가할 수 있게 함. 문법/단어/문화 질문에 답하는 "선생님" 채팅 패널 신설(/api/language/ask-teacher). 선생님 답변의 마크다운(표·볼드·목록·인용)이 그대로 노출되던 버그를 자체 렌더러로 수정. --- .claude/settings.local.json | 3 +- src/gateway/routes-language.ts | 41 ++++++ web-ui/language-app.html | 222 +++++++++++++++++++++++++++++---- 3 files changed, 243 insertions(+), 23 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f4ac4d3..4829e38 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -210,7 +210,8 @@ "Bash(/home/kim/homeclaw/.smallclaw/voice-venv/bin/pip list *)", "Bash(apt-cache policy *)", "Bash(find / -maxdepth 4 -iname \"config.json\" -path \"*smallclaw*\")", - "Read(//root/**)" + "Read(//root/**)", + "Bash(echo \"done, exit=$?\")" ] }, "ANTHROPIC_BASE_URL": "https://openrouter.ai/api", diff --git a/src/gateway/routes-language.ts b/src/gateway/routes-language.ts index fafd857..ed57aac 100644 --- a/src/gateway/routes-language.ts +++ b/src/gateway/routes-language.ts @@ -171,6 +171,47 @@ JSON으로만 응답하세요 (코드 블록 없이): } }); + // POST /api/language/ask-teacher — general Q&A chat, NOT translation (that's /interpret). + // A student asks about grammar/usage/culture/vocabulary in Korean or Uzbek and gets an + // explanation + examples, with short conversation history for follow-up questions. + app.post('/api/language/ask-teacher', async (req: express.Request, res: express.Response) => { + try { + const message = String(req.body?.message || '').trim().slice(0, 1000); + if (!message) { res.status(400).json({ success: false, error: 'message required' }); return; } + const historyRaw = Array.isArray(req.body?.history) ? req.body.history : []; + // Cap history to the last 10 turns — this is a lightweight tutor chat, not a full + // session-backed conversation; the client resends what it wants remembered each time. + const history = historyRaw.slice(-10).filter((m: any) => m?.role && m?.content).map((m: any) => ({ + role: m.role === 'assistant' ? 'assistant' as const : 'user' as const, + content: String(m.content).slice(0, 1000), + })); + + const systemPrompt = `당신은 친절한 우즈베크어 선생님입니다. 한국인 학습자의 질문에 답합니다. +- 문법, 단어 용법, 발음, 우즈베크 문화·관습에 대한 질문에 답하세요. +- 설명은 한국어로 하고, 우즈베크어 예문은 항상 라틴 문자(1993년 이후 표준, oʻ/gʻ 포함)로 정확히 표기하세요. +- 예문에는 한글 발음 표기와 뜻을 함께 달아주세요. +- 학습자가 문장을 만들어서 봐달라고 하면 자연스러운 표현으로 고쳐주고 왜 그런지 짧게 설명하세요. +- 통역 요청(문장을 그냥 번역해달라는 요청)이 오면 번역은 해주되, 배울 점(문법 포인트 등)을 한 줄 덧붙이세요. +- 답변은 간결하게 — 장황한 설명보다 핵심 위주로, 이모지는 최소화.`; + + const ollama = getOllamaClient(); + const result = await ollama.chatWithThinking( + [ + { role: 'system' as const, content: systemPrompt }, + ...history, + { role: 'user' as const, content: message }, + ], + 'executor', + { num_predict: 800, temperature: 0.4, think: false } + ); + const reply = String(result.message?.content || '').trim(); + if (!reply) { res.json({ success: false, error: '응답 생성 실패' }); return; } + res.json({ success: true, reply }); + } catch (err: any) { + res.status(500).json({ success: false, error: String(err?.message || err) }); + } + }); + // POST /api/language/tts — Uzbek TTS via edge-tts app.post('/api/language/tts', async (req: express.Request, res: express.Response) => { try { diff --git a/web-ui/language-app.html b/web-ui/language-app.html index 82a5757..50a6233 100644 --- a/web-ui/language-app.html +++ b/web-ui/language-app.html @@ -45,6 +45,16 @@ body { background: var(--bg); color: var(--text); font-family: 'Nanum Gothic', s .im-reply .im-lang { font-size: 10px; color: #22c55e; font-weight: 700; margin-bottom: 3px; text-transform: uppercase; letter-spacing: .04em; } .im-reply .im-tts { margin-left: 6px; } .im-loading { align-self: flex-start; color: var(--muted); font-size: 12px; padding: 4px 0; } +.teacher-md-p { margin: 0 0 6px; } +.teacher-md-p:last-child { margin-bottom: 0; } +.teacher-md-heading { font-weight: 700; margin: 8px 0 4px; } +.teacher-md-heading:first-child { margin-top: 0; } +.teacher-md-list { margin: 0 0 6px; padding-left: 18px; } +.teacher-md-quote { margin: 0 0 6px; padding: 4px 10px; border-left: 3px solid var(--line); color: var(--muted); } +.teacher-md-hr { border: none; border-top: 1px solid var(--line); margin: 8px 0; } +.teacher-md-table { border-collapse: collapse; width: 100%; margin: 0 0 6px; font-size: 12px; } +.teacher-md-table th, .teacher-md-table td { border: 1px solid var(--line); padding: 4px 6px; text-align: left; } +.teacher-md-table th { background: var(--panel); } .lg-interp-input-row { display: flex; gap: 6px; padding: 10px; border-top: 1px solid var(--line); flex-shrink: 0; background: var(--panel-2); } .lg-interp-input-row textarea { flex: 1; resize: none; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; font-size: 13px; color: var(--text); outline: none; font-family: inherit; height: 38px; max-height: 90px; } .lg-interp-input-row textarea:focus { border-color: var(--brand); } @@ -290,6 +300,7 @@ body { background: var(--bg); color: var(--text); font-family: 'Nanum Gothic', s + ← 메인 @@ -328,12 +339,8 @@ body { background: var(--bg); color: var(--text); font-family: 'Nanum Gothic', s
- - - - - - +
+ @@ -342,22 +349,13 @@ body { background: var(--bg); color: var(--text); font-family: 'Nanum Gothic', s -
+
+ + +
+
+ 👨‍🏫 우즈베크어 선생님 + 문법·단어·문화 질문 + +
+
+
궁금한 걸 물어보세요.
예: "이 문법 언제 써요?", "이 단어랑 이 단어 차이가 뭐예요?", "이 문장 자연스러운가요?"
+
+
+ + +
+