From dc76158c6fba849b37c43a6baba512c94d2cc429 Mon Sep 17 00:00:00 2001 From: kim Date: Mon, 21 Sep 2026 21:50:40 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9E=91=EC=97=85=EC=8B=A4=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=EC=97=90=20=EA=B2=BD=EB=9F=89=20=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?pill=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 메인 채팅의 전체 설정 모달을 그대로 들여오는 대신, 같은 백엔드 엔드포인트(/api/settings/model, /api/settings/provider, /api/models/test)만 재사용해 채팅 헤더에 작은 pill+드롭다운을 추가. 클릭하면 ollama provider가 보유한 로컬+클라우드 모델 태그 목록을 보여주고 선택 시 전역 models.primary를 바꾼다. curl로 GET/POST 전 구간 실측 확인(목록 12개 모델 조회, 전환→원복). Co-Authored-By: Claude Sonnet 5 --- web-ui/html/workshop-app.html | 70 +++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/web-ui/html/workshop-app.html b/web-ui/html/workshop-app.html index 0418028..b14cb76 100644 --- a/web-ui/html/workshop-app.html +++ b/web-ui/html/workshop-app.html @@ -72,6 +72,16 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis .rb-send:hover:not(:disabled){background:#9a5c03;} .rb-send:disabled{opacity:.5;cursor:not-allowed;} +.rb-model-pill{position:relative;flex-shrink:0;} +.rb-model-pill-btn{background:none;border:1px solid var(--line);border-radius:6px;padding:2px 8px;font-size:10px;font-weight:700;color:var(--muted);cursor:pointer;font-family:inherit;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;transition:.15s;} +.rb-model-pill-btn:hover{border-color:#f59e0b;color:#f59e0b;} +.rb-model-dropdown{display:none;position:absolute;top:100%;right:0;margin-top:4px;background:var(--panel-2);border:1px solid var(--line);border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,.35);min-width:200px;max-height:280px;overflow-y:auto;z-index:50;padding:4px;} +.rb-model-dropdown.open{display:block;} +.rb-model-opt{padding:6px 10px;font-size:12px;border-radius:5px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--text);} +.rb-model-opt:hover{background:var(--panel);} +.rb-model-opt.current{color:#f59e0b;font-weight:700;} +.rb-model-dropdown-msg{padding:6px 10px;font-size:11px;color:var(--muted);} + @media (max-width:900px){ .rb-layout{flex-direction:column;} .rb-resize{display:none;} @@ -224,6 +234,10 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
🛠️ 작업실 채팅 +
+ +
+
@@ -847,12 +861,68 @@ function autoResize(el){el.style.height='auto';el.style.height=Math.min(el.scrol document.addEventListener('mouseup',()=>{if(!dragging)return;dragging=false;handle.classList.remove('dragging');document.body.style.userSelect='';document.body.style.cursor='';try{localStorage.setItem(CHAT_W,panel.offsetWidth);}catch{};}); })(); +// ── 모델 pill ───────────────────────────────────────────────────────────────── +// 경량 버전: 메인 채팅의 설정 모달(settings-provider.js)을 통째로 들여오지 않고, 같은 백엔드 +// 엔드포인트(/api/settings/model, /api/settings/provider, /api/models/test)만 재사용해서 +// 전역 models.primary를 바꾼다. ollama provider(로컬+클라우드 모델 태그) 안에서만 전환됨 — +// provider 자체(openai/anthropic/google 등)를 바꾸는 건 메인 설정에서 해야 한다. +let _modelDropdownLoaded=false; +async function loadModelPill(){ + try{ + const r=await fetch('/api/settings/model',{headers:authH()}); + if(!r.ok)return; + const d=await r.json(); + const btn=document.getElementById('model-pill-btn'); + if(btn && d.primary) btn.textContent=d.primary; + }catch{} +} +async function toggleModelDropdown(ev){ + if(ev) ev.stopPropagation(); + const dd=document.getElementById('model-dropdown'); + if(dd.classList.contains('open')){dd.classList.remove('open');return;} + dd.classList.add('open'); + if(_modelDropdownLoaded)return; + dd.innerHTML='
모델 불러오는 중…
'; + try{ + const pr=await fetch('/api/settings/provider',{headers:authH()}); + const pd=await pr.json(); + const tr=await fetch('/api/models/test',{method:'POST',headers:authH({'Content-Type':'application/json'}),body:JSON.stringify({llm:pd.llm})}); + const td=await tr.json(); + const models=(td.models||[]).map(m=>typeof m==='string'?m:(m.name||String(m))); + if(!models.length){dd.innerHTML='
모델 목록을 가져오지 못했습니다.
';return;} + const current=(document.getElementById('model-pill-btn').textContent||'').trim(); + dd.innerHTML=models.map(m=>`
${escHtml(m)}
`).join(''); + dd.querySelectorAll('.rb-model-opt').forEach(el=>el.addEventListener('click',()=>selectModel(el.dataset.model))); + _modelDropdownLoaded=true; + }catch(e){dd.innerHTML='
오류: '+escHtml(e.message)+'
';} +} +async function selectModel(name){ + document.getElementById('model-dropdown').classList.remove('open'); + const btn=document.getElementById('model-pill-btn'); + const prev=btn.textContent; + if(name===prev)return; + btn.textContent=name; + try{ + const r=await fetch('/api/settings/model',{method:'POST',headers:authH({'Content-Type':'application/json'}),body:JSON.stringify({primary:name})}); + if(!r.ok)throw new Error('저장 실패'); + _modelDropdownLoaded=false; + }catch(e){ + btn.textContent=prev; + addMsg('system','모델 변경 실패: '+e.message); + } +} +document.addEventListener('click',(e)=>{ + const pill=document.querySelector('.rb-model-pill'); + if(pill && !pill.contains(e.target)) document.getElementById('model-dropdown')?.classList.remove('open'); +}); + // ── Init ────────────────────────────────────────────────────────────────────── (async function init(){ if(!(await checkAuth()))return; await loadWorkshopData(); await initSessionId(); await loadHistory(); + loadModelPill(); addMsg('system','🛠️ 작업실 채팅입니다. 현재 프로젝트: "'+(currentProject()?.name||'')+'". 부품 구매/상태 변경, 작업 완료 체크, 예산 확인, 새 프로젝트 생성 등을 물어보세요.'); document.getElementById('chat-input').focus(); })();