diff --git a/package.json b/package.json index f63cee1..ae84929 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "smallclaw", - "version": "4.1.0", + "version": "4.1.1", "description": "Local AI agent framework powered by Ollama - OpenClaw alternative", "main": "dist/index.js", "bin": { diff --git a/src/gateway/routes-language.ts b/src/gateway/routes-language.ts index 70b628d..50b40a2 100644 --- a/src/gateway/routes-language.ts +++ b/src/gateway/routes-language.ts @@ -35,7 +35,7 @@ export function registerLanguageRoutes(app: express.Application): void { const result = await ollama.chatWithThinking( [{ role: 'user' as const, content: prompt }], 'executor', - { num_predict: 1000, temperature: 0.3 } + { num_predict: 2000, temperature: 0.3, think: false } ); const raw = String(result.message?.content || '').trim(); let lesson: any; @@ -60,7 +60,7 @@ export function registerLanguageRoutes(app: express.Application): void { const result = await ollama.chatWithThinking( [{ role: 'user' as const, content: prompt }], 'executor', - { num_predict: 700, temperature: 0.2 } + { num_predict: 2000, temperature: 0.2, think: false } ); const raw = String(result.message?.content || '').trim(); let cards: any[] = []; @@ -95,7 +95,7 @@ export function registerLanguageRoutes(app: express.Application): void { const result = await ollama.chatWithThinking( [{ role: 'user' as const, content: prompt }], 'executor', - { num_predict: 10, temperature: 0 } + { num_predict: 10, temperature: 0, think: false } ); const verdict = String(result.message?.content || '').toLowerCase().includes('correct') ? 'correct' : 'incorrect'; res.json({ success: true, result: verdict, score: verdict === 'correct' ? 1 : 0 }); diff --git a/src/gateway/server-v2.ts b/src/gateway/server-v2.ts index 999c567..21decda 100644 --- a/src/gateway/server-v2.ts +++ b/src/gateway/server-v2.ts @@ -2578,6 +2578,20 @@ function buildTools() { }, }, }, + { + type: 'function', + function: { + name: 'weather_map_screenshot', + description: 'Capture a real screenshot of a rendered Windy weather map (radar, satellite, wind, precipitation, etc.) so you can actually SEE it before describing it. Call this with the exact embed.windy.com URL you just generated — do not guess what the map shows from the URL parameters alone.', + parameters: { + type: 'object', required: ['url'], + properties: { + url: { type: 'string', description: 'The exact https://embed.windy.com/embed2.html?... URL to capture (must match the one shown to the user)' }, + wait_ms: { type: 'number', description: 'Milliseconds to wait for map tiles to finish loading before capturing (default 4000, max 8000)' }, + }, + }, + }, + }, { type: 'function', function: { @@ -5328,6 +5342,10 @@ async function handleChat( if (name.startsWith('mcp__dental-dict-sqlite__') && !isDentalAppSession) return false; if ((name.startsWith('mcp__psychotherapy-cases-sqlite__') || name.startsWith('mcp__psychiatry-cases-sqlite__')) && !isMindAppSession) return false; if (weatherToolNames.has(name) && !isWeatherAppSession && !(meteorologistEnabled && hasWeatherKeyword)) return false; + // Windy map screenshots only make sense inside the dedicated weather app tab (the + // model there has already generated the embed.windy.com URL to capture) — unlike the + // other weather tools, not offered via the meteorologist skill in general chat. + if (name === 'weather_map_screenshot' && !isWeatherAppSession) return false; if (legalToolNames.has(name) && !isLawyerAppSession && !(lawyerEnabled && hasLegalKeyword)) return false; if (pptxToolNames.has(name) && presenterEnabled && !hasPptxKeyword) return false; if (academicToolNames.has(name) && !hasPptxKeyword) return false; diff --git a/web-ui/language-app.html b/web-ui/language-app.html index b689c55..9c2e63b 100644 --- a/web-ui/language-app.html +++ b/web-ui/language-app.html @@ -82,6 +82,19 @@ body { background: var(--bg); color: var(--text); font-family: 'Nanum Gothic', s .known-bar .known-val { color: #22c55e; font-weight: 700; } .known-bar .unknown-val { color: #f87171; font-weight: 700; } +/* ── Flashcard tile grid ── */ +.card-grid { flex: 1; display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); grid-auto-rows: 1fr; gap: 20px; padding: 24px; width: 100%; height: 100%; overflow-y: auto; align-content: stretch; box-sizing: border-box; } +.tile-stage { perspective: 1000px; min-height: 220px; cursor: pointer; } +.tile-inner { width: 100%; height: 100%; position: relative; transform-style: preserve-3d; transition: transform 0.4s cubic-bezier(.4,0,.2,1); } +.tile-inner.flipped { transform: rotateY(180deg); } +.tile-face { position: absolute; inset: 0; backface-visibility: hidden; -webkit-backface-visibility: hidden; border-radius: 16px; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 16px; border: 1px solid var(--line); box-shadow: 0 4px 18px rgba(0,0,0,.16); text-align: center; } +.tile-front { background: var(--panel); } +.tile-back { background: #0d1f1a; border-color: #1a4a35; transform: rotateY(180deg); } +.tile-uz { font-size: 26px; font-weight: 800; color: var(--text); word-break: keep-all; } +.tile-rom { font-size: 15px; color: var(--muted); font-style: italic; margin-top: 8px; } +.tile-ko { font-size: 26px; font-weight: 700; color: #22c55e; word-break: keep-all; } +.tile-cat { position: absolute; top: 8px; right: 10px; font-size: 11px; color: var(--muted); } + /* ── Vocab table ── */ .vocab-wrap { flex: 1; overflow: auto; padding: 0; } .vocab-table { width: 100%; border-collapse: collapse; font-size: 13px; } @@ -324,10 +337,11 @@ body { background: var(--bg); color: var(--text); font-family: 'Nanum Gothic', s 플래시카드 — 전체 0 / 0 + -
+
@@ -352,6 +366,7 @@ body { background: var(--bg); color: var(--text); font-family: 'Nanum Gothic', s 학습 필요: 0
+
@@ -654,6 +669,7 @@ let STATE = { category: 'all', vocabCat: 'all', quizCat: 'all', + viewMode: 'single', // 'single' | 'grid' deck: [], deckIdx: 0, known: new Set(), @@ -727,7 +743,7 @@ function buildDeck(cat) { STATE.deckIdx = 0; STATE.flipped = false; document.getElementById('card-inner').classList.remove('flipped'); - renderCard(); + renderFlashView(); } function setCategory(cat) { STATE.category = cat; @@ -735,6 +751,44 @@ function setCategory(cat) { buildDeck(cat); document.getElementById('flash-title').textContent = '플래시카드 — ' + (CAT_LABELS[cat]||'전체'); } +function renderFlashView() { + if (STATE.viewMode === 'grid') { + document.getElementById('flash-counter').textContent = STATE.deck.length ? `${STATE.deck.length}개 전체` : '0개'; + renderGrid(); + } else { + renderCard(); + } +} +function toggleViewMode() { + STATE.viewMode = STATE.viewMode === 'single' ? 'grid' : 'single'; + document.getElementById('card-wrap-single').style.display = STATE.viewMode === 'single' ? '' : 'none'; + document.getElementById('card-grid').style.display = STATE.viewMode === 'grid' ? '' : 'none'; + document.getElementById('view-toggle-btn').textContent = STATE.viewMode === 'single' ? '🀄 타일로 보기' : '🔄 한장씩 보기'; + renderFlashView(); +} +function renderGrid() { + const showRom = document.getElementById('show-rom').checked; + const d = STATE.deck; + const grid = document.getElementById('card-grid'); + if (!d.length) { + grid.innerHTML = '
단어 없음
'; + return; + } + grid.innerHTML = d.map(v => ` +
+
+
+ ${CAT_LABELS[v.cat]||''} +
${v.uz}
+ ${showRom?`
[${v.rom}]
`:''} +
+
+
${v.ko}
+
+
+
+ `).join(''); +} function renderCard() { const d = STATE.deck; if (!d.length) { @@ -799,7 +853,7 @@ function shuffleDeck() { STATE.deckIdx = 0; STATE.flipped = false; document.getElementById('card-inner').classList.remove('flipped'); - renderCard(); + renderFlashView(); } function resetDeck() { buildDeck(STATE.category); diff --git a/web-ui/mind-app.html b/web-ui/mind-app.html index 94b2034..13f170f 100644 --- a/web-ui/mind-app.html +++ b/web-ui/mind-app.html @@ -106,6 +106,10 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis .q-disclaimer{font-size:10px;color:var(--muted);line-height:1.6;margin-top:12px;padding:10px 12px;border:1px dashed var(--line);border-radius:7px;} .q-disclaimer strong{display:block;color:var(--text);margin-bottom:3px;font-size:10px;} .q-refs{font-size:10px;color:var(--muted);margin-top:6px;font-style:italic;opacity:.85;} +.crisis-banner{background:rgba(239,68,68,.1);border:2px solid rgba(239,68,68,.5);border-radius:12px;padding:16px;margin-bottom:16px;} +.crisis-banner strong{display:block;color:#f87171;font-size:14px;margin-bottom:8px;} +.crisis-banner p{font-size:12px;color:var(--text);line-height:1.6;margin-bottom:8px;} +.crisis-banner .crisis-lines{display:flex;flex-direction:column;gap:4px;font-size:13px;font-weight:700;color:var(--text);} /* ── Breathing widget ── */ .breath-container{display:flex;flex-direction:column;align-items:center;gap:24px;padding:20px 0;} @@ -314,6 +318,26 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis 📋 AUDIT알코올 선별 + + + + +
`).join('')}
`:''; const itemTable=`
${items.map(it=>``).join('')}
#문항응답점수
${it.n}${escHtml(it.q)}${escHtml(it.label)}${it.score}
`; const disclaimer=`
안내본 검사는 자기보고식 선별도구로 임상 진단을 대체하지 않습니다. 결과는 참고용이며, 정확한 평가는 임상심리사 또는 정신건강의학과 전문의를 통해 받으세요.${test.note?'
'+escHtml(test.note):''}${test.refs?`
참고: ${escHtml(test.refs)}
`:''}
`; + // Standard clinical practice for PHQ-9: item 9 (suicidal/self-harm ideation) warrants + // a direct safety check regardless of total score — previously buried as one line in + // the disclaimer, now a prominent banner if the respondent scored above 0 on it. + const crisisBanner=(test.crisisItem!==undefined&&items[test.crisisItem].score>0)?`
+ ⚠️ 확인이 필요합니다 +

"${escHtml(test.questions[test.crisisItem])}" 문항에 응답하신 내용이 있습니다. 지금 힘든 마음이시라면 아래 연락처로 바로 도움을 받으실 수 있습니다.

+
+ ☎ 자살예방상담전화 1393 (24시간, 무료) + ☎ 정신건강위기상담전화 1577-0199 (24시간) + ☎ 생명의전화 1588-9191 + ☎ 청소년전화 1388 +
+
`:''; lastResult={title:test.title,score,level,subscores,items,refs:test.refs,minScore:range.min,maxScore:range.max}; @@ -865,6 +1058,7 @@ function submitQuestionnaire(testId){ w.innerHTML=`

${test.title} 결과

+ ${crisisBanner}
${scoreDisplay}
${level.label}