- 카카오톡 채널 챗봇 (오픈빌더 비동기 콜백 방식, 5초 제한 우회) - 카카오 "나에게 보내기" OAuth 알림 (kakao-notify.ts) - 채널 설정 UI + userMap으로 다중 사용자 계정 매핑 - 탐정앱: 탭(메모/체크리스트/타임라인), 우선순위, 검색, 보고서/고소장/접수안내 생성 - 모바일 브레이크포인트 640px → 1024px (갤럭시 폴드 대응) - 프롬프트 파일 제한 상향 (USER.md 3000, SOUL.md/AGENTS.md 4000) - 교통앱 마지막 선택 지역 localStorage 저장/복원 - docs/kakao-setup.md 작성 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
619 lines
42 KiB
HTML
619 lines
42 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>⚖️ 법률 어시스턴트</title>
|
|
<link rel="icon" type="image/png" sizes="64x64" href="cherry_logo.png">
|
|
<link rel="stylesheet" href="styles.css">
|
|
<script>try{var _t=localStorage.getItem('cherryclaw_theme')||'dark';document.documentElement.setAttribute('data-theme',_t);}catch(e){}</script>
|
|
<style>
|
|
html,body{height:100%;margin:0;padding:0;}
|
|
body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;display:flex;flex-direction:column;overflow:hidden;height:100dvh;}
|
|
|
|
/* ── Header ── */
|
|
.lw-hdr{display:flex;align-items:center;gap:8px;padding:6px 12px;background:var(--panel);border-bottom:1px solid var(--line);flex-shrink:0;}
|
|
.lw-hdr h1{font-size:13px;font-weight:700;margin:0;}
|
|
.lw-hdr-sp{flex:1;}
|
|
.hdr-btn{background:none;border:1.5px solid var(--line);border-radius:6px;padding:3px 9px;font-size:11px;color:var(--muted);cursor:pointer;font-family:inherit;transition:.15s;}
|
|
.hdr-btn:hover{border-color:#eab308;color:#eab308;}
|
|
.skill-badge{font-size:11px;padding:2px 8px;border-radius:12px;background:rgba(234,179,8,.1);border:1px solid rgba(234,179,8,.25);color:#fbbf24;font-weight:700;}
|
|
|
|
/* ── Layout ── */
|
|
.lw-layout{flex:1;display:flex;min-height:0;overflow:hidden;}
|
|
|
|
/* ── Sidebar ── */
|
|
.lw-sb{width:200px;flex-shrink:0;display:flex;flex-direction:column;border-right:1px solid var(--line);background:var(--panel);min-height:0;overflow-y:auto;}
|
|
.lw-sb::-webkit-scrollbar{width:3px;}
|
|
.lw-sb::-webkit-scrollbar-thumb{background:var(--line);border-radius:3px;}
|
|
.lw-sb-sec{font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;padding:8px 10px 4px;flex-shrink:0;}
|
|
.lw-quick{padding:0 6px 6px;display:flex;flex-direction:column;gap:3px;}
|
|
.lw-qbtn{background:none;border:1px solid var(--line);border-radius:6px;padding:6px 9px;font-size:11px;color:var(--muted);cursor:pointer;font-family:inherit;text-align:left;transition:.15s;line-height:1.4;}
|
|
.lw-qbtn:hover{border-color:#eab308;color:#eab308;background:rgba(234,179,8,.05);}
|
|
.lw-divider{height:1px;background:var(--line);margin:4px 6px;}
|
|
.lw-disclaimer{margin:6px;padding:8px 10px;background:rgba(234,179,8,.06);border:1px solid rgba(234,179,8,.15);border-radius:8px;font-size:10px;color:var(--muted);line-height:1.6;}
|
|
|
|
/* ── Center ── */
|
|
.lw-center{flex:1;min-width:0;display:flex;flex-direction:column;overflow:hidden;background:var(--bg);}
|
|
|
|
/* Search bar */
|
|
.lw-search-bar{padding:8px 12px;border-bottom:1px solid var(--line);background:var(--panel);flex-shrink:0;}
|
|
.lw-search-row{display:flex;gap:6px;align-items:center;margin-bottom:6px;}
|
|
.lw-search-input{flex:1;background:var(--panel-2);border:1.5px solid var(--line);border-radius:8px;padding:7px 12px;font-size:12px;color:var(--text);font-family:inherit;outline:none;transition:.15s;}
|
|
.lw-search-input:focus{border-color:#eab308;}
|
|
.lw-search-btn{background:#92400e;border:none;border-radius:8px;padding:7px 14px;font-size:12px;font-weight:700;color:#fef3c7;cursor:pointer;font-family:inherit;transition:.15s;white-space:nowrap;}
|
|
.lw-search-btn:hover:not(:disabled){background:#b45309;}
|
|
.lw-search-btn:disabled{opacity:.5;cursor:not-allowed;}
|
|
.lw-search-tabs{display:flex;gap:4px;}
|
|
.lw-stab{padding:3px 10px;border:1.5px solid var(--line);border-radius:16px;font-size:11px;font-weight:600;cursor:pointer;background:none;color:var(--muted);font-family:inherit;transition:.12s;}
|
|
.lw-stab:hover{border-color:#eab308;color:#eab308;}
|
|
.lw-stab.active{border-color:#d97706;background:#451a03;color:#fbbf24;}
|
|
[data-theme="light"] .lw-stab.active{background:#fef3c7;color:#b45309;}
|
|
.lw-sort-sel{background:var(--panel-2);border:1px solid var(--line);border-radius:5px;padding:3px 6px;font-size:11px;color:var(--text);font-family:inherit;}
|
|
|
|
/* Center body: split into list + detail */
|
|
.lw-center-body{flex:1;display:flex;min-height:0;overflow:hidden;}
|
|
.lw-results{width:300px;flex-shrink:0;border-right:1px solid var(--line);display:flex;flex-direction:column;overflow:hidden;}
|
|
.lw-results-hdr{padding:6px 12px;border-bottom:1px solid var(--line);font-size:11px;color:var(--muted);flex-shrink:0;background:var(--panel);}
|
|
.lw-results-list{flex:1;overflow-y:auto;padding:4px;}
|
|
.lw-results-list::-webkit-scrollbar{width:3px;}
|
|
.lw-results-list::-webkit-scrollbar-thumb{background:var(--line);border-radius:3px;}
|
|
.lw-result-item{padding:9px 10px;border-radius:7px;border:1px solid transparent;cursor:pointer;margin-bottom:3px;transition:.12s;}
|
|
.lw-result-item:hover{background:rgba(234,179,8,.05);border-color:rgba(234,179,8,.2);}
|
|
.lw-result-item.active{background:rgba(234,179,8,.08);border-color:rgba(234,179,8,.3);}
|
|
.lw-ri-name{font-size:12px;font-weight:700;color:var(--text);margin-bottom:3px;line-height:1.35;}
|
|
.lw-ri-meta{font-size:10px;color:var(--muted);line-height:1.5;}
|
|
.lw-ri-badge{display:inline-block;padding:1px 6px;border-radius:10px;font-size:10px;font-weight:700;margin-right:4px;}
|
|
.badge-law{background:rgba(59,130,246,.1);color:#60a5fa;border:1px solid rgba(59,130,246,.2);}
|
|
.badge-prec{background:rgba(234,179,8,.1);color:#fbbf24;border:1px solid rgba(234,179,8,.2);}
|
|
.badge-us{background:rgba(239,68,68,.1);color:#f87171;border:1px solid rgba(239,68,68,.2);}
|
|
|
|
/* Detail pane */
|
|
.lw-detail{flex:1;min-width:0;display:flex;flex-direction:column;overflow:hidden;}
|
|
.lw-detail-hdr{display:flex;align-items:center;padding:6px 12px;border-bottom:1px solid var(--line);background:var(--panel);gap:8px;flex-shrink:0;}
|
|
.lw-detail-title{font-size:12px;font-weight:700;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
.lw-detail-btn{background:none;border:1px solid var(--line);border-radius:5px;padding:3px 9px;font-size:11px;color:var(--muted);cursor:pointer;font-family:inherit;transition:.12s;white-space:nowrap;}
|
|
.lw-detail-btn:hover{border-color:#eab308;color:#eab308;}
|
|
.lw-detail-body{flex:1;overflow-y:auto;padding:16px 20px;font-size:12px;line-height:1.7;}
|
|
.lw-detail-body::-webkit-scrollbar{width:3px;}
|
|
.lw-detail-body::-webkit-scrollbar-thumb{background:var(--line);border-radius:3px;}
|
|
.lw-detail-body h2{font-size:14px;font-weight:700;margin:0 0 8px;color:var(--text);}
|
|
.lw-detail-body h3{font-size:13px;font-weight:700;margin:14px 0 5px;color:var(--text);}
|
|
.lw-detail-body p{margin:3px 0;color:var(--text);}
|
|
.lw-detail-body strong{font-weight:700;}
|
|
.lw-detail-body .meta-row{font-size:11px;color:var(--muted);margin-bottom:12px;padding-bottom:10px;border-bottom:1px solid var(--line);}
|
|
.lw-loading{text-align:center;padding:40px;color:var(--muted);font-size:12px;}
|
|
.lw-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--muted);text-align:center;gap:12px;padding:40px;}
|
|
.lw-empty .icon{font-size:48px;opacity:.3;}
|
|
.lw-empty p{font-size:12px;line-height:1.7;max-width:300px;}
|
|
.lw-detail-pre{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:14px 16px;font-size:11.5px;line-height:1.8;white-space:pre-wrap;word-break:break-word;color:var(--text);font-family:inherit;}
|
|
|
|
/* ── Resize handle ── */
|
|
.lw-resize{width:4px;flex-shrink:0;background:var(--line);cursor:col-resize;transition:.15s;}
|
|
.lw-resize:hover,.lw-resize.dragging{background:#eab308;}
|
|
|
|
/* ── Chat ── */
|
|
.lw-chat{width:340px;flex-shrink:0;display:flex;flex-direction:column;border-left:1px solid var(--line);background:var(--panel);min-height:0;}
|
|
.lw-chat-hdr{display:flex;align-items:center;padding:6px 10px;border-bottom:1px solid var(--line);flex-shrink:0;gap:6px;}
|
|
.lw-chat-label{font-size:10px;font-weight:700;color:#fbbf24;text-transform:uppercase;letter-spacing:.07em;flex-shrink:0;}
|
|
.lw-model-sel{flex:1;background:var(--panel-2);border:1px solid var(--line);border-radius:5px;padding:3px 6px;font-size:11px;color:var(--text);font-family:inherit;min-width:0;}
|
|
.lw-chat-clear{background:none;border:none;cursor:pointer;font-size:14px;color:var(--muted);padding:2px 4px;transition:.12s;}
|
|
.lw-chat-clear:hover{color:#ef4444;}
|
|
.lw-msgs{flex:1;overflow-y:auto;padding:10px;display:flex;flex-direction:column;gap:8px;}
|
|
.lw-msgs::-webkit-scrollbar{width:3px;}
|
|
.lw-msgs::-webkit-scrollbar-thumb{background:var(--line);border-radius:3px;}
|
|
.lw-msg{display:flex;flex-direction:column;}
|
|
.lw-msg.user{align-items:flex-end;}
|
|
.lw-msg.assistant{align-items:flex-start;}
|
|
.lw-msg.system{align-items:center;}
|
|
.lw-bubble{padding:7px 11px;border-radius:10px;font-size:12px;line-height:1.55;max-width:93%;word-break:break-word;}
|
|
.lw-msg.user .lw-bubble{background:#451a03;color:#fef3c7;border-radius:10px 10px 3px 10px;}
|
|
.lw-msg.assistant .lw-bubble{background:var(--panel-2);border:1px solid var(--line);border-radius:10px 10px 10px 3px;}
|
|
.lw-msg.system .lw-bubble{background:transparent;color:var(--muted);font-size:11px;}
|
|
.lw-bubble p{margin:3px 0;}
|
|
.lw-bubble h3{font-size:13px;margin:6px 0 3px;}
|
|
.lw-bubble ul,.lw-bubble ol{padding-left:16px;margin:3px 0;}
|
|
.lw-bubble pre{background:var(--bg);border:1px solid var(--line);border-radius:6px;padding:8px;overflow-x:auto;font-size:11px;margin:6px 0;}
|
|
.lw-bubble strong{font-weight:700;}
|
|
.lw-bubble table{border-collapse:collapse;font-size:11px;margin:6px 0;width:100%;}
|
|
.lw-bubble th,.lw-bubble td{border:1px solid var(--line);padding:3px 7px;text-align:left;}
|
|
.lw-bubble th{background:var(--panel);font-weight:700;}
|
|
.lw-bubble blockquote{border-left:3px solid var(--line);padding-left:10px;margin:4px 0;color:var(--muted);}
|
|
.lw-thinking{padding:4px 10px;border-radius:6px;font-size:10px;color:var(--muted);background:var(--bg);border:1px solid var(--line);font-style:italic;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;cursor:pointer;margin-bottom:4px;}
|
|
.lw-thinking.exp{display:block;-webkit-line-clamp:unset;max-height:200px;overflow-y:auto;}
|
|
.lw-typing{display:flex;gap:3px;align-items:center;padding:6px 10px;}
|
|
.lw-dot{width:5px;height:5px;background:var(--muted);border-radius:50%;animation:lwDot 1.2s ease-in-out infinite;}
|
|
.lw-dot:nth-child(2){animation-delay:.2s;}.lw-dot:nth-child(3){animation-delay:.4s;}
|
|
@keyframes lwDot{0%,80%,100%{transform:scale(.5);opacity:.4;}40%{transform:scale(1);opacity:1;}}
|
|
.lw-input-bar{flex-shrink:0;padding:8px;border-top:1px solid var(--line);display:flex;gap:6px;align-items:flex-end;}
|
|
.lw-textarea{flex:1;background:var(--panel-2);border:1px solid var(--line);border-radius:8px;padding:7px 10px;font-size:12px;color:var(--text);font-family:inherit;resize:none;min-height:34px;max-height:120px;outline:none;transition:.15s;}
|
|
.lw-textarea:focus{border-color:#eab308;}
|
|
.lw-send{background:#92400e;border:none;border-radius:8px;padding:7px 13px;font-size:12px;color:#fff;cursor:pointer;font-family:inherit;font-weight:700;transition:.15s;flex-shrink:0;}
|
|
.lw-send:hover:not(:disabled){background:#b45309;}
|
|
.lw-send:disabled{opacity:.5;cursor:not-allowed;}
|
|
|
|
/* ── Mobile ── */
|
|
@media(max-width:1024px){
|
|
.lw-resize{display:none;}
|
|
.lw-sb{
|
|
position:fixed;top:0;left:0;bottom:0;z-index:500;
|
|
width:80vw;max-width:300px;
|
|
transform:translateX(-100%);transition:transform .25s ease;
|
|
box-shadow:4px 0 24px rgba(0,0,0,.4);
|
|
}
|
|
.lw-sb.mob-open{transform:translateX(0);}
|
|
.lw-chat{
|
|
position:fixed;top:0;right:0;bottom:0;z-index:500;
|
|
width:92vw;max-width:380px;
|
|
transform:translateX(100%);transition:transform .25s ease;
|
|
box-shadow:-4px 0 24px rgba(0,0,0,.4);
|
|
}
|
|
.lw-chat.mob-open{transform:translateX(0);}
|
|
#mob-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:499;}
|
|
#mob-backdrop.show{display:block;}
|
|
.lw-center{width:100%;}
|
|
.lw-center-body{padding-bottom:64px;}
|
|
#mob-tabbar{display:flex !important;position:fixed;bottom:0;left:0;right:0;z-index:400;background:var(--panel);border-top:1px solid var(--line);height:52px;}
|
|
.lw-textarea,input,textarea{font-size:16px !important;}
|
|
}
|
|
@media(min-width:1025px){
|
|
#mob-tabbar{display:none !important;}
|
|
#mob-backdrop{display:none !important;}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header class="lw-hdr">
|
|
<a href="/" class="hdr-btn">← 홈</a>
|
|
<h1>⚖️ 법률 어시스턴트</h1>
|
|
<span class="skill-badge" id="skill-badge">스킬 로딩 중...</span>
|
|
<div class="lw-hdr-sp"></div>
|
|
<button class="hdr-btn" onclick="toggleTheme()">🌙 테마</button>
|
|
</header>
|
|
|
|
<div id="mob-backdrop" onclick="mobCloseAll()"></div>
|
|
|
|
<div id="mob-tabbar" style="display:none">
|
|
<button class="mob-tab" id="mobtab-quick" onclick="mobToggle('quick')">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" style="width:20px;height:20px;stroke-width:2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
<span>빠른질문</span>
|
|
</button>
|
|
<button class="mob-tab" id="mobtab-home" onclick="mobToggle('home')" style="color:var(--brand,#eab308);border-top:2px solid var(--brand,#eab308)">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" style="width:20px;height:20px;stroke-width:2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
<span>검색</span>
|
|
</button>
|
|
<button class="mob-tab" id="mobtab-chat" onclick="mobToggle('chat')">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" style="width:20px;height:20px;stroke-width:2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
|
<span>AI</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="lw-layout">
|
|
|
|
<!-- ── Sidebar ── -->
|
|
<aside class="lw-sb">
|
|
<div class="lw-sb-sec">빠른 질문</div>
|
|
<div class="lw-quick">
|
|
<button class="lw-qbtn" onclick="sendQuick('근로계약서의 주요 필수 기재사항과 불리한 조항 확인 방법을 알려주세요.')">📄 근로계약서 검토</button>
|
|
<button class="lw-qbtn" onclick="sendQuick('부당해고를 당했습니다. 대응 방법과 구제 절차를 알려주세요.')">🚫 부당해고 대응</button>
|
|
<button class="lw-qbtn" onclick="sendQuick('임대차 계약 시 세입자가 꼭 확인해야 할 사항과 전세사기 예방법을 알려주세요.')">🏠 임대차 계약</button>
|
|
<button class="lw-qbtn" onclick="sendQuick('명예훼손과 모욕죄의 차이점과 고소 방법을 알려주세요.')">💬 명예훼손·모욕</button>
|
|
<button class="lw-qbtn" onclick="sendQuick('교통사고 발생 시 즉시 해야 할 조치와 보험 처리 절차를 알려주세요.')">🚗 교통사고 처리</button>
|
|
<button class="lw-qbtn" onclick="sendQuick('개인정보 침해를 당했을 때 신고 방법과 손해배상 청구 절차를 알려주세요.')">🔐 개인정보 침해</button>
|
|
<button class="lw-qbtn" onclick="sendQuick('이혼 절차(협의이혼·재판이혼)와 재산분할, 양육권 기준을 알려주세요.')">💔 이혼·가족법</button>
|
|
<button class="lw-qbtn" onclick="sendQuick('소액사건심판 절차와 내용증명 작성 방법을 알려주세요.')">📮 내용증명·소액심판</button>
|
|
</div>
|
|
<div class="lw-divider"></div>
|
|
<div class="lw-disclaimer">
|
|
⚠️ 이 서비스는 <strong>정보 제공</strong> 목적이며 법률 자문을 대체하지 않습니다. 중요한 법적 판단은 변호사에게 확인하세요.
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- ── Center: search + results + detail ── -->
|
|
<section class="lw-center">
|
|
<!-- Search bar -->
|
|
<div class="lw-search-bar">
|
|
<div class="lw-search-row">
|
|
<input class="lw-search-input" id="search-input" type="text" placeholder="법령명, 판례 키워드를 입력하세요..." onkeydown="if(event.key==='Enter')doSearch()">
|
|
<button class="lw-search-btn" id="search-btn" onclick="doSearch()">🔍 검색</button>
|
|
</div>
|
|
<div class="lw-search-tabs">
|
|
<button class="lw-stab active" id="stab-law" onclick="setSearchType('law')">📗 법령</button>
|
|
<button class="lw-stab" id="stab-prec" onclick="setSearchType('prec')">⚖️ 판례</button>
|
|
<button class="lw-stab" id="stab-ordinance" onclick="setSearchType('ordinance')">📘 자치법규</button>
|
|
<button class="lw-stab" id="stab-us" onclick="setSearchType('us')">🇺🇸 미국 판례</button>
|
|
<div style="flex:1"></div>
|
|
<select class="lw-sort-sel" id="sort-sel">
|
|
<option value="rel">관련도순</option>
|
|
<option value="date">최신순</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Results + Detail -->
|
|
<div class="lw-center-body">
|
|
<!-- Results list -->
|
|
<div class="lw-results">
|
|
<div class="lw-results-hdr" id="results-hdr">검색 결과</div>
|
|
<div class="lw-results-list" id="results-list">
|
|
<div class="lw-empty" style="height:100%">
|
|
<div class="icon">⚖️</div>
|
|
<p>법령명, 판례 키워드,<br>조문 내용 등으로 검색하세요.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Detail pane -->
|
|
<div class="lw-detail">
|
|
<div class="lw-detail-hdr">
|
|
<span class="lw-detail-title" id="detail-title">상세 내용</span>
|
|
<button class="lw-detail-btn" id="ask-ai-btn" onclick="askAiAboutDetail()" style="display:none">💬 AI에게 분석 요청</button>
|
|
</div>
|
|
<div class="lw-detail-body" id="detail-body">
|
|
<div class="lw-empty" style="height:100%">
|
|
<div class="icon">📄</div>
|
|
<p>검색 결과를 클릭하면<br>법령 전문 또는 판례 요지를<br>여기서 확인할 수 있습니다.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ── Resize ── -->
|
|
<div class="lw-resize" id="resize-handle"></div>
|
|
|
|
<!-- ── Chat ── -->
|
|
<div class="lw-chat" id="chat-panel">
|
|
<div class="lw-chat-hdr">
|
|
<span class="lw-chat-label">⚖️ 법률 AI</span>
|
|
<select class="lw-model-sel" id="model-sel" onchange="saveModel()">
|
|
<option value="">기본 모델</option>
|
|
</select>
|
|
<button class="lw-chat-clear" onclick="clearChat()" title="대화 지우기">🗑</button>
|
|
</div>
|
|
<div class="lw-msgs" id="chat-msgs"></div>
|
|
<div class="lw-input-bar">
|
|
<textarea class="lw-textarea" id="chat-input" rows="1" placeholder="법률 질문을 입력하세요..." onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea>
|
|
<button class="lw-send" id="send-btn" onclick="sendMessage()">전송</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
// ── Auth ──────────────────────────────────────────────────────────────────────
|
|
const TOKEN_KEY='smallclaw_token';
|
|
function getToken(){try{return sessionStorage.getItem(TOKEN_KEY)||localStorage.getItem(TOKEN_KEY)||'';}catch{return '';}}
|
|
function authH(extra){const t=getToken();const h={...(extra||{})};if(t)h['Authorization']='Bearer '+t;return h;}
|
|
async function checkAuth(){
|
|
if(!getToken()){location.href='/login.html?redirect='+encodeURIComponent(location.pathname);return false;}
|
|
try{const r=await fetch('/api/auth/status',{headers:authH()});if(!r.ok){location.href='/login.html?redirect='+encodeURIComponent(location.pathname);return false;}const d=await r.json();if(!d.authenticated){location.href='/login.html?redirect='+encodeURIComponent(location.pathname);return false;}return true;}
|
|
catch{location.href='/login.html?redirect='+encodeURIComponent(location.pathname);return false;}
|
|
}
|
|
|
|
// ── Theme ─────────────────────────────────────────────────────────────────────
|
|
function toggleTheme(){const d=document.documentElement;const n=d.getAttribute('data-theme')==='dark'?'light':'dark';d.setAttribute('data-theme',n);try{localStorage.setItem('cherryclaw_theme',n);}catch{}}
|
|
|
|
// ── Mobile overlay ────────────────────────────────────────────────────────────
|
|
function mobCloseAll(){
|
|
document.querySelector('.lw-sb').classList.remove('mob-open');
|
|
document.getElementById('chat-panel').classList.remove('mob-open');
|
|
document.getElementById('mob-backdrop').classList.remove('show');
|
|
['quick','chat'].forEach(t=>document.getElementById('mobtab-'+t)?.classList.remove('active'));
|
|
document.getElementById('mobtab-home').style.cssText='color:var(--brand,#eab308);border-top:2px solid var(--brand,#eab308)';
|
|
}
|
|
function mobToggle(which){
|
|
const sb=document.querySelector('.lw-sb');
|
|
const cp=document.getElementById('chat-panel');
|
|
const bd=document.getElementById('mob-backdrop');
|
|
if(which==='home'){mobCloseAll();return;}
|
|
const sbOpen=sb.classList.contains('mob-open');
|
|
const cpOpen=cp.classList.contains('mob-open');
|
|
mobCloseAll();
|
|
if(which==='quick'&&!sbOpen){sb.classList.add('mob-open');bd.classList.add('show');document.getElementById('mobtab-quick').classList.add('active');document.getElementById('mobtab-home').style.cssText='';}
|
|
if(which==='chat'&&!cpOpen){cp.classList.add('mob-open');bd.classList.add('show');document.getElementById('mobtab-chat').classList.add('active');document.getElementById('mobtab-home').style.cssText='';}
|
|
}
|
|
|
|
// ── Resize ────────────────────────────────────────────────────────────────────
|
|
(function(){
|
|
const CHAT_W='lw_chat_width';
|
|
const handle=document.getElementById('resize-handle');
|
|
const panel=document.getElementById('chat-panel');
|
|
let dragging=false,startX=0,startW=0;
|
|
panel.style.width=(parseInt(localStorage.getItem(CHAT_W)||'340',10))+'px';
|
|
handle.addEventListener('mousedown',e=>{dragging=true;startX=e.clientX;startW=panel.offsetWidth;handle.classList.add('dragging');document.body.style.userSelect='none';document.body.style.cursor='col-resize';e.preventDefault();});
|
|
document.addEventListener('mousemove',e=>{if(!dragging)return;const nw=Math.max(260,Math.min(600,startW+(startX-e.clientX)));panel.style.width=nw+'px';});
|
|
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{};});
|
|
})();
|
|
|
|
// ── Model ─────────────────────────────────────────────────────────────────────
|
|
const MODEL_KEY='lw_model';
|
|
let selectedModel=localStorage.getItem(MODEL_KEY)||'';
|
|
async function loadModels(){
|
|
const sel=document.getElementById('model-sel');
|
|
try{
|
|
const[sr,or]=await Promise.all([fetch('/api/settings/model',{headers:authH()}),fetch('/api/ollama/models',{headers:authH()})]);
|
|
const setting=sr.ok?await sr.json():{};const od=or.ok?await or.json():{};
|
|
const primary=setting.primary||'기본';const models=(od.models||[]).map(m=>m.name);
|
|
sel.innerHTML=`<option value="">기본 (${primary})</option>`;
|
|
if(selectedModel&&!models.includes(selectedModel))models.unshift(selectedModel);
|
|
for(const m of models){const o=document.createElement('option');o.value=m;o.textContent=m;if(m===selectedModel)o.selected=true;sel.appendChild(o);}
|
|
if(!selectedModel)sel.value='';
|
|
}catch{}
|
|
}
|
|
function saveModel(){selectedModel=document.getElementById('model-sel').value;try{localStorage.setItem(MODEL_KEY,selectedModel);}catch{}}
|
|
|
|
// ── Skill ─────────────────────────────────────────────────────────────────────
|
|
const APP_KEY='lw';
|
|
let skillContext='';
|
|
let _sessionId=null;
|
|
function getSessionId(){return _sessionId||'lw_main';}
|
|
async function initSessionId(){
|
|
try{const r=await fetch('/api/user/app-sessions',{headers:authH()});if(r.ok){const d=await r.json();if(d[APP_KEY]){_sessionId=d[APP_KEY];return;}}}catch{}
|
|
_sessionId='lw_'+Math.random().toString(36).slice(2,12);
|
|
try{await fetch('/api/user/app-sessions',{method:'PUT',headers:authH({'Content-Type':'application/json'}),body:JSON.stringify({appKey:APP_KEY,sessionId:_sessionId})});}catch{}
|
|
}
|
|
|
|
async function loadHistory(){
|
|
const sid=getSessionId();
|
|
if(!sid||sid.endsWith('_main'))return;
|
|
try{
|
|
const r=await fetch('/api/chat/sessions/'+encodeURIComponent(sid),{headers:authH()});
|
|
if(!r.ok)return;
|
|
const d=await r.json();
|
|
const msgs=(d.history||[]).filter(m=>m.role==='user'||m.role==='assistant');
|
|
if(!msgs.length)return;
|
|
const recent=msgs.slice(-30);
|
|
const el=document.getElementById('chat-msgs');
|
|
const sep=document.createElement('div');sep.style.cssText='text-align:center;color:var(--muted,#888);font-size:11px;padding:6px 0;';sep.textContent='─ 이전 대화 ─';el.appendChild(sep);
|
|
for(const m of recent){
|
|
if(m.role==='user')addMsg('user',escHtml(m.content||''));
|
|
else addMsg('assistant',renderMd(m.content||''));
|
|
}
|
|
const sep2=document.createElement('div');sep2.style.cssText='text-align:center;color:var(--muted,#888);font-size:11px;padding:6px 0;';sep2.textContent='─ 이어서 ─';el.appendChild(sep2);
|
|
el.scrollTop=el.scrollHeight;
|
|
}catch{}
|
|
}
|
|
|
|
|
|
|
|
const SKILL_CONTENT="---\nname: Legal Advisor\ndescription: \ud55c\uad6d\ubc95 \ubc0f \uc601\ubbf8\ubc95 \uc804\ubb38 \ubc95\ub960 \uc5b4\uc2dc\uc2a4\ud134\ud2b8. \ubc95\ub839 \uc870\ud68c\u00b7\ud310\ub840 \uac80\uc0c9\u00b7\uacc4\uc57d\uc11c \uac80\ud1a0\ub97c \ub3c4\uad6c\uc640 \ud568\uaed8 \uc218\ud589.\nemoji: \"\u2696\ufe0f\"\nversion: 1.0.0\ntriggers: [\ubc95, \ubc95\ub960, \uacc4\uc57d, \uc18c\uc1a1, \ud310\ub840, \ud615\uc0ac, \ubbfc\uc0ac, \ubc95\uc6d0, \uace0\uc18c, \uace0\ubc1c, \ud5cc\ubc95, \uc870\ud56d, \uaddc\uc815, \uc704\ubc18, \uc190\ud574\ubc30\uc0c1, \uacc4\uc57d\uc11c, \ubc95\uc801, \uad8c\ub9ac, \uc758\ubb34, \ucc98\ubc8c, \ud615\ub7c9, \ubcc0\ud638\uc0ac, \ubc95\ubb34]\n---\n\n\ub2f9\uc2e0\uc740 \ud55c\uad6d\ubc95\uacfc \uc601\ubbf8\ubc95\uc5d0 \ub2a5\ud1b5\ud55c \ubc95\ub960 \uc5b4\uc2dc\uc2a4\ud134\ud2b8\uc785\ub2c8\ub2e4. \uc544\ub798 \ub3c4\uad6c\ub97c \ud65c\uc6a9\ud574 \uc0ac\uc2e4\uc5d0 \uadfc\uac70\ud55c \ubc95\ub960 \uc815\ubcf4\ub97c \uc81c\uacf5\ud558\uc138\uc694.\n\n### \uc0ac\uc6a9 \uac00\ub2a5\ud55c \ubc95\ub960 \ub3c4\uad6c\n\n| \ub3c4\uad6c | \uc6a9\ub3c4 |\n|---|---|\n| `korean_law_search` | \uad6d\uac00\ubc95\ub839\uc815\ubcf4\uc13c\ud130 \ubc95\ub839 \uac80\uc0c9 (target=\"law\") |\n| `korean_law_search` | \ub300\ubc95\uc6d0 \ud310\ub840 \uac80\uc0c9 (target=\"prec\") |\n| `korean_law_fetch` | \ubc95\ub839 \uc804\ubb38\u00b7\ud310\ub840 \uc804\ubb38 \uc870\ud68c |\n| `us_case_search` | \ubbf8\uad6d \uc5f0\ubc29/\uc8fc \ud310\ub840 (CourtListener) |\n| `web_search` | \ucd5c\uc2e0 \uac1c\uc815\uc0ac\ud56d, \ubc95\ub839 \ud574\uc11d\ub840, \ud559\uc220\uc790\ub8cc, \ud574\uc678 \ubc95\ub839 |\n| `pdf_read` | \uacc4\uc57d\uc11c\u00b7\ubc95\ub960\ubb38\uc11c PDF \ubd84\uc11d |\n\n### \uc6b0\uc120 \ucc38\uc870 \uc0ac\uc774\ud2b8\n\n`web_search` \ud638\ucd9c \uc2dc \uc544\ub798 **site: \uc5f0\uc0b0\uc790\ub97c query\uc5d0 \ud3ec\ud568**\ud574 \uad8c\uc704 \uc788\ub294 \ucd9c\ucc98\uc5d0\uc11c \uc6b0\uc120 \uac80\uc0c9\ud558\uc138\uc694.\n\n| \uc8fc\uc81c | site: \uc5f0\uc0b0\uc790 | \uc124\uba85 |\n|---|---|---|\n| \ud55c\uad6d \ubc95\ub839 \uc6d0\ubb38 | `site:law.go.kr` | \uad6d\uac00\ubc95\ub839\uc815\ubcf4\uc13c\ud130 (\ubc95\ub960\u00b7\uc2dc\ud589\ub839\u00b7\uc2dc\ud589\uaddc\uce59) |\n| \ub300\ubc95\uc6d0 \ud310\ub840 | `site:glaw.scourt.go.kr` | \ub300\ubc95\uc6d0 \uc885\ud569\ubc95\ub960\uc815\ubcf4 \ud310\ub840 \uac80\uc0c9 |\n| \ubc95\ub839 \ud574\uc11d\ub840 | `site:moleg.go.kr` | \ubc95\uc81c\ucc98 \ubc95\ub839 \ud574\uc11d \uc0ac\ub840 |\n| \ud5cc\ubc95\uc7ac\ud310 \uacb0\uc815 | `site:ccourt.go.kr` | \ud5cc\ubc95\uc7ac\ud310\uc18c \uacb0\uc815\ub840 |\n| \uc601\uad6d \ubc95\ub839 | `site:legislation.gov.uk` | UK Acts / Statutory Instruments |\n| \ubbf8\uad6d \uc5f0\ubc29\uaddc\uc815 | `site:law.cornell.edu` | Cornell LII (CFR\u00b7USC) |\n\n\uc608\uc2dc: \uc784\ub300\ucc28\ubcf4\ud638\ubc95 \ucd5c\uc2e0 \uac1c\uc815 \u2192 `web_search(\"site:law.go.kr \uc8fc\ud0dd\uc784\ub300\ucc28\ubcf4\ud638\ubc95 \uac1c\uc815\")`\n\n### \uacc4\uc57d\uc11c \uac80\ud1a0 \ubc29\ubc95\ub860\n\n\uacc4\uc57d\uc11c \uc6d0\ubb38 \ub610\ub294 PDF\uac00 \uc81c\uacf5\ub418\uba74 \ub2e4\uc74c \uc21c\uc11c\ub85c \ubd84\uc11d\ud558\uc138\uc694:\n\n1. **\uae30\ubcf8 \uad6c\uc870** \u2014 \ub2f9\uc0ac\uc790, \ubaa9\uc801, \uae30\uac04, \ub300\uac00(\uae08\uc561\u00b7\uc9c0\uae09\uc870\uac74)\n2. **\uc704\ud5d8 \uc870\ud56d** \u2014 \uba74\ucc45\u00b7\ucc45\uc784\uc81c\ud55c, \uc77c\ubc29\uc801 \ud574\uc9c0\uad8c, \uc704\uc57d\ubc8c, \uc190\ud574\ubc30\uc0c1 \uc0c1\ud55c\n3. **\ub204\ub77d \uc870\ud56d** \u2014 \ubd84\uc7c1\ud574\uacb0(\uc911\uc7ac\u00b7\uc18c\uc1a1), \uc900\uac70\ubc95, \uad00\ud560, \ubd88\uac00\ud56d\ub825(Force Majeure), \ube44\ubc00\uc720\uc9c0(NDA)\n4. **\uac15\ud589\uaddc\uc815 \uc704\ubc18** \u2014 `korean_law_search`\ub85c \uad00\ub828 \ubc95\ub839 \ud655\uc778 (\uc18c\ube44\uc790\ubcf4\ud638\ubc95, \uacf5\uc815\uac70\ub798\ubc95, \uadfc\ub85c\uae30\uc900\ubc95 \ub4f1)\n5. **\uac1c\uc120 \uc81c\uc548** \u2014 \uc218\uc815 \ub610\ub294 \ucd94\uac00\uac00 \ud544\uc694\ud55c \uc870\ud56d\uc744 \uad6c\uccb4\uc801 \ubb38\uc548\uc73c\ub85c \uc81c\uc2dc\n\n### \uc751\ub2f5 \uaddc\uce59\n\n1. **\ucd9c\ucc98 \ud544\uc218**: \ubc95\ub839 \uc778\uc6a9 \uc2dc \"\ubbfc\ubc95 \uc81c\u25cb\uc870\", \ud310\ub840 \uc778\uc6a9 \uc2dc \"\ub300\ubc95\uc6d0 \u25cb\u25cb\u25cb\u25cb. \u25cb. \u25cb. \uc120\uace0 \u25cb\u25cb\ub2e4\u25cb\u25cb\u25cb\u25cb \ud310\uacb0\" \ud615\uc2dd\uc73c\ub85c \uba85\uc2dc.\n2. **\ubd88\ud655\uc2e4\ud55c \ubc95\uc801 \ud310\ub2e8**: \"\ubc95\ub960 \uc804\ubb38\uac00(\ubcc0\ud638\uc0ac) \ud655\uc778 \uad8c\uace0\" \ubb38\uad6c\ub97c \ubc18\ub4dc\uc2dc \ucd94\uac00.\n3. **\uad00\ud560 \uad6c\ubd84**: \ud55c\uad6d\ubc95 \uc9c8\ubb38 \u2192 `korean_law_search` \uc6b0\uc120. \ubbf8\uad6d/\uc601\uad6d\ubc95 \u2192 `us_case_search`\u00b7`web_search`.\n4. **\uc5b8\uc5b4**: \uc0ac\uc6a9\uc790\uac00 \ud55c\uad6d\uc5b4\ub85c \uc9c8\ubb38\ud558\uba74 \ud55c\uad6d\uc5b4, \uc601\uc5b4\ub85c \uc9c8\ubb38\ud558\uba74 \uc601\uc5b4\ub85c \ub2f5\ubcc0.\n5. **\uba74\ucc45 \uace0\uc9c0**: \uc774 \uc11c\ube44\uc2a4\ub294 \uc815\ubcf4 \uc81c\uacf5 \ubaa9\uc801\uc774\uba70 \ubc95\ub960 \uc790\ubb38\uc744 \ub300\uccb4\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\n";
|
|
function loadSkill(){
|
|
skillContext=SKILL_CONTENT;
|
|
document.getElementById('skill-badge').textContent='⚖️ 법률 전문가 활성';
|
|
}
|
|
|
|
// ── Search ────────────────────────────────────────────────────────────────────
|
|
let searchType='law';
|
|
let currentDetailText='';
|
|
let currentDetailTitle='';
|
|
|
|
function setSearchType(type){
|
|
searchType=type;
|
|
['law','prec','ordinance','us'].forEach(t=>{document.getElementById('stab-'+t)?.classList.toggle('active',t===type);});
|
|
const ph={law:'법령명 또는 내용 키워드',prec:'판례 키워드, 죄명, 법원명',ordinance:'자치법규 키워드',us:'English case keywords'};
|
|
document.getElementById('search-input').placeholder=(ph[type]||'검색어를 입력하세요...')+' 검색...';
|
|
}
|
|
|
|
function escHtml(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
|
|
|
async function doSearch(){
|
|
const q=document.getElementById('search-input').value.trim();
|
|
if(!q)return;
|
|
const btn=document.getElementById('search-btn');
|
|
btn.disabled=true;btn.textContent='검색 중...';
|
|
const sort=document.getElementById('sort-sel').value;
|
|
const lang=searchType==='us'?'us':'ko';
|
|
const target=searchType==='us'?'law':searchType;
|
|
try{
|
|
const r=await fetch(`/api/legal/search?q=${encodeURIComponent(q)}&target=${target}&sort=${sort}&n=20&lang=${lang}`,{headers:authH()});
|
|
const data=await r.json();
|
|
renderResults(data,q);
|
|
}catch(e){
|
|
document.getElementById('results-hdr').textContent='검색 실패';
|
|
document.getElementById('results-list').innerHTML=`<div style="padding:12px;font-size:12px;color:var(--err)">오류: ${escHtml(String(e.message))}</div>`;
|
|
}finally{btn.disabled=false;btn.textContent='🔍 검색';}
|
|
}
|
|
|
|
function renderResults(data,query){
|
|
const list=document.getElementById('results-list');
|
|
const hdr=document.getElementById('results-hdr');
|
|
if(!data.success||data.error){
|
|
hdr.textContent='검색 오류';
|
|
list.innerHTML=`<div style="padding:12px;font-size:12px;color:var(--err)">${escHtml(data.error||'알 수 없는 오류')}</div>`;
|
|
return;
|
|
}
|
|
const results=(data.data?.results)||[];
|
|
const total=data.data?.total||0;
|
|
const isPrec=searchType==='prec';const isUs=searchType==='us';
|
|
hdr.textContent=`검색 결과 ${total}건 중 ${results.length}건`;
|
|
if(!results.length){list.innerHTML='<div style="padding:12px;font-size:12px;color:var(--muted)">결과가 없습니다. 다른 키워드로 검색해보세요.</div>';return;}
|
|
list.innerHTML=results.map((r,i)=>{
|
|
const badge=isPrec?'badge-prec':(isUs?'badge-us':'badge-law');
|
|
const badgeLabel=isPrec?'판례':(isUs?'미국 판례':'법령');
|
|
const title=r.name||r.caseName||r.case_no||'제목 없음';
|
|
let meta='';
|
|
if(isPrec)meta=`${r.court||''}${r.date?' · '+r.date:''}${r.type?' · '+r.type:''}`;
|
|
else if(isUs)meta=`${r.court||''}${r.dateFiled?' · '+r.dateFiled:''}`;
|
|
else meta=`${r.ministry||r.type||''}${r.effective_date?' · 시행 '+r.effective_date:''}`;
|
|
return `<div class="lw-result-item" id="ri-${i}" onclick="selectResult(${i})" data-idx="${i}" data-id="${escHtml(String(r.id||''))}" data-name="${escHtml(title)}" data-is-prec="${isPrec}" data-is-us="${isUs}">
|
|
<div class="lw-ri-name"><span class="lw-ri-badge ${badge}">${badgeLabel}</span>${escHtml(title)}</div>
|
|
<div class="lw-ri-meta">${escHtml(meta)}</div>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
async function selectResult(idx){
|
|
if(window.innerWidth<=640) mobCloseAll();
|
|
document.querySelectorAll('.lw-result-item').forEach(el=>el.classList.remove('active'));
|
|
const el=document.getElementById('ri-'+idx);
|
|
if(el)el.classList.add('active');
|
|
const id=el?.dataset.id;
|
|
const name=el?.dataset.name||'상세 내용';
|
|
const isPrec=el?.dataset.isPrec==='true';
|
|
const isUs=el?.dataset.isUs==='true';
|
|
|
|
document.getElementById('detail-title').textContent=name;
|
|
document.getElementById('ask-ai-btn').style.display='none';
|
|
currentDetailText='';currentDetailTitle=name;
|
|
|
|
if(isUs){
|
|
// US cases: just show snippet from results list data, link to CourtListener
|
|
const results=document.querySelectorAll('.lw-result-item');
|
|
const body=document.getElementById('detail-body');
|
|
body.innerHTML=`<div class="lw-loading">미국 판례는 CourtListener에서 원문을 확인하세요.</div>`;
|
|
currentDetailText=`미국 판례: ${name}`;
|
|
document.getElementById('ask-ai-btn').style.display='';
|
|
return;
|
|
}
|
|
|
|
if(!id){
|
|
document.getElementById('detail-body').innerHTML='<div class="lw-loading">ID를 찾을 수 없습니다.</div>';return;
|
|
}
|
|
|
|
const body=document.getElementById('detail-body');
|
|
body.innerHTML='<div class="lw-loading">⏳ 전문 불러오는 중...</div>';
|
|
|
|
try{
|
|
const r=await fetch(`/api/legal/fetch?id=${encodeURIComponent(id)}&target=${isPrec?'prec':'law'}`,{headers:authH()});
|
|
const data=await r.json();
|
|
if(!data.success||data.error){body.innerHTML=`<div style="color:var(--err);padding:12px;font-size:12px">${escHtml(data.error||'로드 실패')}</div>`;return;}
|
|
currentDetailText=data.stdout||'';
|
|
renderDetail(data.stdout||'',isPrec);
|
|
document.getElementById('ask-ai-btn').style.display='';
|
|
}catch(e){
|
|
body.innerHTML=`<div style="color:var(--err);padding:12px;font-size:12px">오류: ${escHtml(e.message)}</div>`;
|
|
}
|
|
}
|
|
|
|
function renderDetail(text,isPrec){
|
|
const body=document.getElementById('detail-body');
|
|
// Convert markdown-ish text to HTML
|
|
const html=text
|
|
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
|
.replace(/^## (.+)$/gm,'<h2>$1</h2>')
|
|
.replace(/^### (.+)$/gm,'<h3>$1</h3>')
|
|
.replace(/^\*\*(.+?)\*\*$/gm,'<p><strong>$1</strong></p>')
|
|
.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>')
|
|
.replace(/^- (.+)$/gm,'<li>$1</li>')
|
|
.replace(/(<li>.*<\/li>)/gs,m=>`<ul style="padding-left:16px;margin:4px 0">${m}</ul>`)
|
|
.replace(/\n\n/g,'</p><p>')
|
|
.replace(/^(?!<[hupli])/m,'<p>').replace(/(?<![>])$/,'</p>')
|
|
.replace(/<p><\/p>/g,'')
|
|
.replace(/<p>(<h[23]>)/g,'$1').replace(/(<\/h[23]>)<\/p>/g,'$1');
|
|
body.innerHTML=html;
|
|
}
|
|
|
|
function askAiAboutDetail(){
|
|
if(!currentDetailText)return;
|
|
const msg=`다음 ${searchType==='prec'?'판례':'법령'}를 분석해주세요:\n\n제목: ${currentDetailTitle}\n\n${currentDetailText.slice(0,3000)}`;
|
|
addMsg('user',escHtml(currentDetailTitle||'법률 자료')+' 분석 요청');
|
|
callChat(msg);
|
|
addMsg('system','📄 상세 내용을 AI에게 전달했습니다.');
|
|
}
|
|
|
|
// ── Chat ──────────────────────────────────────────────────────────────────────
|
|
function addMsg(role,html){
|
|
const msgs=document.getElementById('chat-msgs');
|
|
const div=document.createElement('div');div.className='lw-msg '+role;
|
|
const b=document.createElement('div');b.className='lw-bubble';b.innerHTML=html;
|
|
div.appendChild(b);msgs.appendChild(div);msgs.scrollTop=msgs.scrollHeight;
|
|
return b;
|
|
}
|
|
|
|
function renderMd(text){
|
|
return text
|
|
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
|
.replace(/```[\w]*\n?([\s\S]*?)```/g,'<pre><code>$1</code></pre>')
|
|
.replace(/`([^`]+)`/g,'<code>$1</code>')
|
|
.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>')
|
|
.replace(/\*(.+?)\*/g,'<em>$1</em>')
|
|
.replace(/^#{1,3}\s+(.+)$/gm,'<h3>$1</h3>')
|
|
.replace(/^> (.+)$/gm,'<blockquote>$1</blockquote>')
|
|
.replace(/^[-*]\s+(.+)$/gm,'<li>$1</li>')
|
|
.replace(/(<li>[\s\S]+?<\/li>)/g,m=>`<ul style="padding-left:16px;margin:3px 0">${m}</ul>`)
|
|
.replace(/\n\n/g,'</p><p>').replace(/^/,'<p>').replace(/$/,'</p>')
|
|
.replace(/<p><\/p>/g,'').replace(/<p>(<[hup])/g,'$1').replace(/(<\/[hup][^>]*>)<\/p>/g,'$1');
|
|
}
|
|
|
|
async function sendMessage(){
|
|
const input=document.getElementById('chat-input');
|
|
const msg=input.value.trim();
|
|
if(!msg||document.getElementById('send-btn').disabled)return;
|
|
input.value='';autoResize(input);
|
|
if(window.innerWidth<=640) mobToggle('chat');
|
|
addMsg('user',escHtml(msg));
|
|
await callChat(msg);
|
|
}
|
|
|
|
function sendQuick(msg){
|
|
if(window.innerWidth<=640) mobCloseAll();
|
|
addMsg('user',escHtml(msg));callChat(msg);
|
|
}
|
|
|
|
async function callChat(message){
|
|
const btn=document.getElementById('send-btn');
|
|
btn.disabled=true;btn.textContent='...';
|
|
const msgs=document.getElementById('chat-msgs');
|
|
const typingDiv=document.createElement('div');typingDiv.className='lw-msg assistant';
|
|
typingDiv.innerHTML='<div class="lw-typing"><div class="lw-dot"></div><div class="lw-dot"></div><div class="lw-dot"></div></div>';
|
|
msgs.appendChild(typingDiv);msgs.scrollTop=msgs.scrollHeight;
|
|
let bubble=null,textBuf='',thinkBuf='';
|
|
try{
|
|
const body={message,sessionId:getSessionId(),useTools:true,
|
|
...(selectedModel?{model:selectedModel}:{}),
|
|
...(skillContext?{skillContext}:{})};
|
|
const r=await fetch('/api/chat',{method:'POST',headers:authH({'Content-Type':'application/json'}),body:JSON.stringify(body)});
|
|
if(!r.ok)throw new Error('서버 오류 '+r.status);
|
|
typingDiv.remove();
|
|
const msgDiv=document.createElement('div');msgDiv.className='lw-msg assistant';
|
|
bubble=document.createElement('div');bubble.className='lw-bubble';
|
|
msgDiv.appendChild(bubble);msgs.appendChild(msgDiv);
|
|
const reader=r.body.getReader();const dec=new TextDecoder();let buf='';
|
|
while(true){
|
|
const{done,value}=await reader.read();if(done)break;
|
|
buf+=dec.decode(value,{stream:true});
|
|
const parts=buf.split('\n\n');buf=parts.pop()||'';
|
|
for(const part of parts){
|
|
if(!part.startsWith('data: '))continue;
|
|
let ev;try{ev=JSON.parse(part.slice(6));}catch{continue;}
|
|
if(ev.type==='thinking_delta'&&ev.text)thinkBuf+=ev.text;
|
|
else if(ev.type==='token'||ev.type==='text_delta'){
|
|
textBuf+=(ev.text||'');
|
|
if(bubble){
|
|
bubble.innerHTML=(thinkBuf?`<div class="lw-thinking" onclick="this.classList.toggle('exp')">💭 ${escHtml(thinkBuf.slice(0,120))}${thinkBuf.length>120?'…':''}</div>`:'')+renderMd(textBuf);
|
|
msgs.scrollTop=msgs.scrollHeight;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}catch(e){
|
|
typingDiv.remove();
|
|
if(e.name!=='AbortError'){if(!bubble)bubble=addMsg('assistant','');bubble.innerHTML='<span style="color:var(--err)">오류: '+escHtml(e.message)+'</span>';}
|
|
}finally{btn.disabled=false;btn.textContent='전송';}
|
|
}
|
|
|
|
async function clearChat(){
|
|
if(!confirm('대화를 지울까요?'))return;
|
|
document.getElementById('chat-msgs').innerHTML='';
|
|
_sessionId='lw_'+Math.random().toString(36).slice(2,12);
|
|
try{await fetch('/api/user/app-sessions',{method:'PUT',headers:authH({'Content-Type':'application/json'}),body:JSON.stringify({appKey:APP_KEY,sessionId:_sessionId})});}catch{}
|
|
addMsg('system','새 대화가 시작되었습니다. 법률 질문을 입력해주세요.');
|
|
}
|
|
function handleKey(e){if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMessage();}}
|
|
function autoResize(el){el.style.height='auto';el.style.height=Math.min(el.scrollHeight,120)+'px';}
|
|
|
|
// ── Init ──────────────────────────────────────────────────────────────────────
|
|
(async()=>{
|
|
const ok=await checkAuth();if(!ok)return;
|
|
await initSessionId();
|
|
await loadHistory();
|
|
await Promise.all([loadSkill(),loadModels()]);
|
|
addMsg('system','⚖️ 법률 어시스턴트입니다. 법령/판례 검색이나 법률 질문을 입력하세요.');
|
|
document.getElementById('chat-input').focus();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|