From 57cf65fd7e5553f75128d5058aa5301a7a07c913 Mon Sep 17 00:00:00 2001 From: kim Date: Sat, 26 Sep 2026 20:37:16 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=C2=B7=EC=9E=91?= =?UTF-8?q?=EC=97=85=EC=8B=A4=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EC=B9=B4?= =?UTF-8?q?=EB=A9=94=EB=9D=BC=20=E2=80=94=20=EB=A9=94=EC=8B=9C=EC=A7=80?= =?UTF-8?q?=EB=A7=88=EB=8B=A4=20=ED=98=84=EC=9E=AC=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EC=B2=A8=EB=B6=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - camera.js: getUserMedia 미리보기 패널, 전송 시 프레임(768px JPEG)을 /api/upload/image로 올려 기존 이미지 첨부 경로(![📷…](/api/files/…) → resolveImageContent)로 모델에 전달. 음성 통화 STT도 같은 전송 경로라 그대로 동작 - 메인 채팅: 📷 버튼+패널, sendChat에서 프레임 자동 첨부 - 작업실: 🎥 버튼+패널, 전송 훅·썸네일 표시·팝업에서 카메라 화면을 프로젝트 사진으로 저장 - handle-chat: 과거 턴의 카메라 프레임은 텍스트로 접어 비전 입력이 턴마다 누적되지 않게 함 Co-Authored-By: Claude Sonnet 5 --- src/gateway/chat/handle-chat.ts | 7 +- web-ui/css/workshop-app.css | 8 +++ web-ui/html/index.html | 14 ++++ web-ui/html/workshop-app.html | 14 ++++ web-ui/js/app/camera.js | 114 ++++++++++++++++++++++++++++++++ web-ui/js/app/chat-sse.js | 14 ++-- web-ui/js/app/workshop-chat.js | 38 ++++++++++- 7 files changed, 201 insertions(+), 8 deletions(-) create mode 100644 web-ui/js/app/camera.js diff --git a/src/gateway/chat/handle-chat.ts b/src/gateway/chat/handle-chat.ts index 72fcf86..90350a2 100644 --- a/src/gateway/chat/handle-chat.ts +++ b/src/gateway/chat/handle-chat.ts @@ -864,7 +864,12 @@ async function handleChat( // Only resolve image markdown for user messages — assistant messages with // /api/files/ image references must stay as plain text so Ollama (which // rejects image_url content parts on assistant turns) doesn't error. - const content = role === 'user' ? resolveImageContent(msg.content, workspacePath) : msg.content; + // 라이브 카메라 프레임(alt가 "📷"로 시작)은 매 턴 붙어 오므로, 과거 턴의 것까지 전부 다시 모델에 + // 실으면 세션이 길수록 비전 입력이 선형으로 쌓인다. 이전 프레임은 이미 답변에 반영됐으니 텍스트로 접는다. + const foldedContent = role === 'user' && typeof msg.content === 'string' + ? msg.content.replace(/!\[📷[^\]]*\]\([^)]*\)/g, '[이전 카메라 프레임 생략]') + : msg.content; + const content = role === 'user' ? resolveImageContent(foldedContent, workspacePath) : msg.content; messages.push({ role, content }); } messages.push({ role: 'user', content: resolveImageContent(message, workspacePath) }); diff --git a/web-ui/css/workshop-app.css b/web-ui/css/workshop-app.css index 9a04ae9..83fb870 100644 --- a/web-ui/css/workshop-app.css +++ b/web-ui/css/workshop-app.css @@ -291,3 +291,11 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis /* 지연 로딩 모듈을 받는 동안 탭 패널을 흐리게 하고 클릭을 막는다 */ .rb-pane.rb-loading{opacity:.45;pointer-events:none;transition:opacity .15s;} +/* 라이브 카메라 패널 */ +.rb-attach-btn.on{border-color:#ef4444;color:#ef4444;} +.rb-cam-panel{display:flex;align-items:center;gap:10px;padding:8px 10px;border-top:1px solid var(--line);flex-wrap:wrap;} +.rb-cam-panel video{height:96px;max-width:48%;border-radius:8px;background:#000;object-fit:cover;} +.rb-cam-ctl{display:flex;flex-direction:column;gap:6px;font-size:11px;color:var(--muted);} +.rb-cam-ctl label{display:flex;align-items:center;gap:6px;cursor:pointer;} +.rb-cam-btns{display:flex;gap:6px;flex-wrap:wrap;} +.rb-msg .rb-cam-shot{display:block;max-width:140px;border-radius:6px;margin-bottom:4px;} diff --git a/web-ui/html/index.html b/web-ui/html/index.html index 644de2b..8e1b37f 100644 --- a/web-ui/html/index.html +++ b/web-ui/html/index.html @@ -424,6 +424,18 @@
+ +