Commit e8a5c355 by Michael Pastushkov

fix

parent c94e3c7e
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>KotGPT</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
<style>
:root{
/* tune these if footer/composer sizes change */
--footer-h: 2.75rem; /* fixed footer height (approx) */
--composer-h: 6.5rem; /* composer block height (approx) */
--bottom-pad: calc(var(--footer-h) + var(--composer-h) + 1rem);
}
body{
min-height:100vh;
display:flex;
flex-direction:column;
background-color:#f8f9fa;
margin:0;
}
header{ position:sticky; top:0; z-index:1020; }
main{
flex:1 1 auto;
min-height:0; /* allow flex child to actually scroll */
overflow-y:auto; /* scrollable area */
position:relative;
padding-bottom:var(--bottom-pad); /* room for composer + fixed footer */
display:flex;
flex-direction:column;
}
.composer{
position:sticky;
bottom:var(--footer-h); /* sits just above the fixed footer */
z-index:1020;
background:#fff;
border-top:1px solid #dee2e6;
}
footer{
position:fixed; bottom:0; left:0; width:100%; z-index:1020;
font-size:.75rem; color:#6c757d; text-align:center; padding:.5rem;
border-top:1px solid #dee2e6; background:#fdfdfd; height:var(--footer-h);
box-sizing:border-box;
}
.cat-spinner{ width:36px; height:36px; object-fit:contain; filter:drop-shadow(0 1px 2px rgba(0,0,0,.35)); }
.thinking-cat{ width:64px; height:64px; object-fit:contain; display:block; margin:.25rem auto; }
.message{
max-width:900px; margin:0 auto 1rem auto; padding:1rem 1.25rem;
border-radius:.75rem; line-height:1.6; font-size:1rem; word-break:break-word;
}
.user-message{ background:#fff; border:1px solid #dee2e6; white-space:pre-wrap; }
.assistant-message{ background:#f8f9fa; border:1px solid #dee2e6; }
.assistant-message h1,.assistant-message h2,.assistant-message h3{ margin-top:1rem; margin-bottom:.6rem; }
.assistant-message p{ margin:.5rem 0; }
.assistant-message code{ background:#fff; border:1px solid #e9ecef; border-radius:.25rem; padding:.05rem .35rem; }
.assistant-message pre code{ display:block; padding:.75rem; }
.assistant-message ul,.assistant-message ol{ padding-left:1.25rem; }
.assistant-message table{ width:100%; margin:1rem 0; }
/* First-run logo layout */
#mainScroll{ display:flex; flex-direction:column; position:relative; }
#welcomeLogo{ flex:1 1 auto; display:flex; align-items:center; justify-content:center; user-select:none; }
#welcomeLogo img{
max-width:520px; width:100%; height:auto;
filter:drop-shadow(0 6px 16px rgba(0,0,0,.15)); opacity:.98;
}
/* Scroll-to-bottom button (next to textarea) */
.scroll-row { display:flex; align-items:stretch; gap:.5rem; }
#inputText { flex:1 1 auto; }
#scrollDownBtn{
display:inline-flex;
align-items:center;
justify-content:center;
border:none;
border-radius:.5rem;
width:42px;
height:42px;
background:rgba(13,110,253,.95);
color:#fff;
box-shadow:0 2px 8px rgba(0,0,0,.15);
cursor:pointer;
transition:opacity .18s ease-in-out, transform .12s ease-in-out;
opacity:.96;
}
#scrollDownBtn:hover{ opacity:1; transform:translateY(-1px); }
#scrollDownBtn svg{ width:22px; height:22px; pointer-events:none; }
</style>
</head>
<body>
<!-- Header -->
<header class="py-3 bg-dark text-white shadow-sm">
<div class="container d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-2">
<img id="catIcon" class="cat-spinner" src="/static/images/cat-still.png" alt="Cat logo" />
<h1 class="h4 mb-0">KotGPT</h1>
</div>
</div>
</header>
<!-- Conversation (scrollable) -->
<main class="container my-3" id="mainScroll">
<div id="welcomeLogo">
<img id="welcomeImg" src="/static/images/ask-a-cat-en.png" alt="Ask a Cat">
</div>
<div id="thread"></div>
</main>
<!-- Composer (sticks above fixed footer) -->
<div class="composer py-3">
<div class="container">
<form id="promptForm" class="d-flex flex-column gap-2">
<!-- Row: textarea + always-visible scroll-down button -->
<div class="scroll-row">
<textarea id="inputText" class="form-control" rows="1" placeholder=""></textarea>
<button id="scrollDownBtn" type="button" aria-label="Scroll down" title="Scroll down">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 16a1 1 0 0 1-.7-.29l-6-6a1 1 0 1 1 1.4-1.42L12 13.59l5.3-5.3a1 1 0 0 1 1.4 1.42l-6 6A1 1 0 0 1 12 16z"/>
</svg>
</button>
</div>
<!-- Row: Go / Stop -->
<div class="d-flex align-items-center gap-2">
<button id="goBtn" class="btn btn-primary" type="submit"></button>
<button id="stopBtn" class="btn btn-outline-danger" type="button" style="display:none"></button>
</div>
</form>
</div>
</div>
<!-- Fixed, non-scrollable footer -->
<footer id="disclaimer"></footer>
<script src="https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js"></script>
<script>
// --- i18n setup ---
const DEFAULT_LOCALE='en'; const SUPPORTED=['en','ru'];
let I18N={}; let CURRENT_LOCALE=detectLocale(); let isRunning=false;
function getLangFromQuery(){ const m=location.search.match(/[?&]lang=(en|ru)\b/i); return m?m[1].toLowerCase():null; }
function detectLocale(){ const q=getLangFromQuery(); if(q&&SUPPORTED.includes(q)) return q; const nav=(navigator.language||'').toLowerCase(); return nav.startsWith('ru')?'ru':DEFAULT_LOCALE; }
async function loadLocale(locale){ try{ const res=await fetch(`/locales/${locale}.json`,{cache:'no-store'}); if(!res.ok) throw new Error('HTTP '+res.status); I18N=await res.json(); CURRENT_LOCALE=locale; document.documentElement.lang=locale; applyI18n(); }catch(e){ if(locale!==DEFAULT_LOCALE) await loadLocale(DEFAULT_LOCALE); } }
function t(key,fallback=''){ const parts=key.split('.'); let cur=I18N; for(const p of parts){ if(cur&&Object.prototype.hasOwnProperty.call(cur,p)) cur=cur[p]; else return fallback||key; } return (typeof cur==='string')?cur:(fallback||key); }
function setWelcomeImage(){ const img=document.getElementById('welcomeImg'); if(!img) return; const src={en:'/static/images/ask-a-cat-en.png', ru:'/static/images/ask-a-cat-ru.png'}; img.src=src[CURRENT_LOCALE]||src.en; }
function applyI18n(){
document.title=t('app.title','KotGPT');
const catIcon=document.getElementById('catIcon'); if(catIcon) catIcon.alt=t('header.logoAlt','Cat logo');
const welcomeImg=document.getElementById('welcomeImg'); if(welcomeImg) welcomeImg.alt=t('main.welcomeAlt','Ask a Cat');
const input=document.getElementById('inputText'); if(input) input.placeholder=isRunning?t('status.thinking','Thinking…'):t('composer.placeholder','What can I help you with?');
const goBtn=document.getElementById('goBtn'); if(goBtn) goBtn.textContent=t('buttons.go','Go');
const stopBtn=document.getElementById('stopBtn'); if(stopBtn) stopBtn.textContent=t('buttons.stop','Stop');
const disclaimer=document.getElementById('disclaimer'); if(disclaimer) disclaimer.textContent=t('footer.disclaimer','Beware: Cat can make mistakes.');
const sdb=document.getElementById('scrollDownBtn'); if (sdb){ sdb.setAttribute('aria-label', t('buttons.scrollDown','Scroll down')); sdb.title=t('buttons.scrollDown','Scroll down'); }
setWelcomeImage();
}
// --- refs ---
const ENDPOINT='/api/stream'; const STOPPOINT='/api/stop'; const CAT_GIF='/static/images/cat-progress.gif';
const thread=document.getElementById('thread');
const promptForm=document.getElementById('promptForm');
const inputText=document.getElementById('inputText');
const goBtn=document.getElementById('goBtn');
const stopBtn=document.getElementById('stopBtn');
const welcomeLogo=document.getElementById('welcomeLogo');
const mainScroll=document.getElementById('mainScroll');
const scrollDownBtn=document.getElementById('scrollDownBtn');
let firstRequestDone=false;
let controller=null;
// --- render helpers ---
function renderMarkdown(targetEl, md){
const html=marked.parse(md,{breaks:true,gfm:true,headerIds:false});
targetEl.innerHTML=DOMPurify.sanitize(html,{USE_PROFILES:{html:true}});
}
function showThinkingCat(targetEl){
targetEl.innerHTML='';
const img=document.createElement('img');
img.className='thinking-cat';
img.src=CAT_GIF+'?t='+Date.now();
img.alt=t('status.thinking','Thinking…');
targetEl.appendChild(img);
}
function clearThinkingCatIfPresent(targetEl){
const img=targetEl.querySelector('img.thinking-cat');
if(img) targetEl.innerHTML='';
}
// --- bottom helpers ---
function atBottom(){
return (mainScroll.scrollTop + mainScroll.clientHeight) >= (mainScroll.scrollHeight - 4);
}
// --- composer lock ---
function setComposerLocked(locked){
isRunning=locked;
inputText.placeholder = locked ? t('status.thinking','Thinking…') : t('composer.placeholder','What can I help you with?');
inputText.disabled=locked;
goBtn.style.display=locked?'none':'inline-block';
stopBtn.style.display=locked?'inline-block':'none';
if (locked) inputText.blur(); else inputText.focus();
}
// --- flow ---
async function start(){
const prompt=inputText.value.trim();
if(!prompt) return;
if(!firstRequestDone){ welcomeLogo?.remove(); firstRequestDone=true; }
const auto = atBottom();
const userMsg=document.createElement('div');
userMsg.className='message user-message';
userMsg.textContent=prompt;
thread.appendChild(userMsg);
const assistantMsg=document.createElement('div');
assistantMsg.className='message assistant-message';
showThinkingCat(assistantMsg);
thread.appendChild(assistantMsg);
if (auto) assistantMsg.scrollIntoView({ behavior:'smooth', block:'start' });
inputText.value='';
controller=new AbortController();
setComposerLocked(true);
try{
const res=await fetch(ENDPOINT,{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({ content: prompt }),
signal:controller.signal
});
if(!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
const reader=res.body.getReader();
const decoder=new TextDecoder();
let rawMarkdown=''; let receivedAny=false; let rafPending=false;
const flush=() => {
if (rafPending) return;
rafPending=true;
requestAnimationFrame(() => {
rafPending=false;
if(!receivedAny){ clearThinkingCatIfPresent(assistantMsg); receivedAny=true; }
renderMarkdown(assistantMsg, rawMarkdown);
if (auto && atBottom()){
mainScroll.scrollTop = mainScroll.scrollHeight;
}
});
};
for(;;){
const { value, done } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream:true });
for (const ch of chunk) rawMarkdown += ch; // char-by-char
flush();
}
flush();
}catch(err){
if (err.name!=='AbortError'){
clearThinkingCatIfPresent(assistantMsg);
renderMarkdown(assistantMsg, `\n\n**${t('status.errorLabel','Error')}:** ${err.message||err}`);
}else{
if (assistantMsg.querySelector('img.thinking-cat')){
clearThinkingCatIfPresent(assistantMsg);
renderMarkdown(assistantMsg, t('status.stopped','_Stopped._'));
}
}
}finally{
setComposerLocked(false);
controller=null;
}
}
async function stop(){
if(controller) controller.abort();
try{ await fetch(STOPPOINT,{method:'POST'}); }catch(_){}
}
// events
promptForm.addEventListener('submit', e => { e.preventDefault(); if (goBtn.style.display!=='none') start(); });
inputText.addEventListener('keydown', e => {
if (e.key==='Enter' && !e.shiftKey && goBtn.style.display!=='none'){ e.preventDefault(); start(); }
});
stopBtn.addEventListener('click', stop);
// scroll button action — scroll the main conversation area to bottom
scrollDownBtn.addEventListener('click', () => {
mainScroll.scrollTo({ top: mainScroll.scrollHeight, behavior: 'smooth' });
});
// init
window.addEventListener('DOMContentLoaded', async () => {
try{ await fetch('/api/clear',{method:'GET'}); }catch(_){}
await loadLocale(CURRENT_LOCALE);
inputText.focus();
});
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>KotGPT</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
<style>
:root{
/* tune these if footer/composer sizes change */
--footer-h: 2.75rem; /* fixed footer height (approx) */
--composer-h: 6.5rem; /* composer block height (approx) */
--bottom-pad: calc(var(--footer-h) + var(--composer-h) + 1rem);
}
body{
min-height:100vh;
display:flex;
flex-direction:column;
background-color:#f8f9fa;
margin:0;
}
header{ position:sticky; top:0; z-index:1020; }
main{
flex:1 1 auto;
min-height:0; /* allow flex child to actually scroll */
overflow-y:auto; /* scrollable area */
position:relative;
padding-bottom:var(--bottom-pad); /* room for composer + fixed footer */
display:flex;
flex-direction:column;
}
.composer{
position:sticky;
bottom:var(--footer-h); /* sits just above the fixed footer */
z-index:1020;
background:#fff;
border-top:1px solid #dee2e6;
}
footer{
position:fixed; bottom:0; left:0; width:100%; z-index:1020;
font-size:.75rem; color:#6c757d; text-align:center; padding:.5rem;
border-top:1px solid #dee2e6; background:#fdfdfd; height:var(--footer-h);
box-sizing:border-box;
}
.cat-spinner{ width:36px; height:36px; object-fit:contain; filter:drop-shadow(0 1px 2px rgba(0,0,0,.35)); }
.thinking-cat{ width:64px; height:64px; object-fit:contain; display:block; margin:.25rem auto; }
.message{
max-width:900px; margin:0 auto 1rem auto; padding:1rem 1.25rem;
border-radius:.75rem; line-height:1.6; font-size:1rem; word-break:break-word;
}
.user-message{ background:#fff; border:1px solid #dee2e6; white-space:pre-wrap; }
.assistant-message{ background:#f8f9fa; border:1px solid #dee2e6; }
.assistant-message h1,.assistant-message h2,.assistant-message h3{ margin-top:1rem; margin-bottom:.6rem; }
.assistant-message p{ margin:.5rem 0; }
.assistant-message code{ background:#fff; border:1px solid #e9ecef; border-radius:.25rem; padding:.05rem .35rem; }
.assistant-message pre code{ display:block; padding:.75rem; }
.assistant-message ul,.assistant-message ol{ padding-left:1.25rem; }
.assistant-message table{ width:100%; margin:1rem 0; }
/* First-run logo layout */
#mainScroll{ display:flex; flex-direction:column; position:relative; }
#welcomeLogo{ flex:1 1 auto; display:flex; align-items:center; justify-content:center; user-select:none; }
#welcomeLogo img{
max-width:520px; width:100%; height:auto;
filter:drop-shadow(0 6px 16px rgba(0,0,0,.15)); opacity:.98;
}
</style>
</head>
<body>
<!-- Header -->
<header class="py-3 bg-dark text-white shadow-sm">
<div class="container d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-2">
<img id="catIcon" class="cat-spinner" src="/static/images/cat-still.png" alt="Cat logo" />
<h1 class="h4 mb-0">KotGPT</h1>
</div>
</div>
</header>
<!-- Conversation (scrollable) -->
<main class="container my-3" id="mainScroll">
<div id="welcomeLogo">
<img id="welcomeImg" src="/static/images/ask-a-cat-en.png" alt="Ask a Cat">
</div>
<div id="thread"></div>
</main>
<!-- Composer (sticks above fixed footer) -->
<div class="composer py-3">
<div class="container">
<form id="promptForm" class="d-flex flex-column gap-2">
<textarea id="inputText" class="form-control" rows="1" placeholder=""></textarea>
<div class="d-flex align-items-center gap-2">
<button id="goBtn" class="btn btn-primary" type="submit"></button>
<button id="stopBtn" class="btn btn-outline-danger" type="button" style="display:none"></button>
</div>
</form>
</div>
</div>
<!-- Fixed, non-scrollable footer -->
<footer id="disclaimer"></footer>
<script src="https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js"></script>
<script>
// --- i18n setup ---
const DEFAULT_LOCALE='en'; const SUPPORTED=['en','ru'];
let I18N={}; let CURRENT_LOCALE=detectLocale(); let isRunning=false;
function getLangFromQuery(){ const m=location.search.match(/[?&]lang=(en|ru)\b/i); return m?m[1].toLowerCase():null; }
function detectLocale(){ const q=getLangFromQuery(); if(q&&SUPPORTED.includes(q)) return q; const nav=(navigator.language||'').toLowerCase(); return nav.startsWith('ru')?'ru':DEFAULT_LOCALE; }
async function loadLocale(locale){ try{ const res=await fetch(`/locales/${locale}.json`,{cache:'no-store'}); if(!res.ok) throw new Error('HTTP '+res.status); I18N=await res.json(); CURRENT_LOCALE=locale; document.documentElement.lang=locale; applyI18n(); }catch(e){ if(locale!==DEFAULT_LOCALE) await loadLocale(DEFAULT_LOCALE); } }
function t(key,fallback=''){ const parts=key.split('.'); let cur=I18N; for(const p of parts){ if(cur&&Object.prototype.hasOwnProperty.call(cur,p)) cur=cur[p]; else return fallback||key; } return (typeof cur==='string')?cur:(fallback||key); }
function setWelcomeImage(){ const img=document.getElementById('welcomeImg'); if(!img) return; const src={en:'/static/images/ask-a-cat-en.png', ru:'/static/images/ask-a-cat-ru.png'}; img.src=src[CURRENT_LOCALE]||src.en; }
function applyI18n(){
document.title=t('app.title','KotGPT');
const catIcon=document.getElementById('catIcon'); if(catIcon) catIcon.alt=t('header.logoAlt','Cat logo');
const welcomeImg=document.getElementById('welcomeImg'); if(welcomeImg) welcomeImg.alt=t('main.welcomeAlt','Ask a Cat');
const input=document.getElementById('inputText'); if(input) input.placeholder=isRunning?t('status.thinking','Thinking…'):t('composer.placeholder','What can I help you with?');
const goBtn=document.getElementById('goBtn'); if(goBtn) goBtn.textContent=t('buttons.go','Go');
const stopBtn=document.getElementById('stopBtn'); if(stopBtn) stopBtn.textContent=t('buttons.stop','Stop');
const disclaimer=document.getElementById('disclaimer'); if(disclaimer) disclaimer.textContent=t('footer.disclaimer','Beware: Cat can make mistakes.');
setWelcomeImage();
}
// --- refs ---
const ENDPOINT='/api/stream'; const STOPPOINT='/api/stop'; const CAT_GIF='/static/images/cat-progress.gif';
const thread=document.getElementById('thread');
const promptForm=document.getElementById('promptForm');
const inputText=document.getElementById('inputText');
const goBtn=document.getElementById('goBtn');
const stopBtn=document.getElementById('stopBtn');
const welcomeLogo=document.getElementById('welcomeLogo');
const mainScroll=document.getElementById('mainScroll');
let firstRequestDone=false;
let controller=null;
// --- render helpers ---
function renderMarkdown(targetEl, md){
const html=marked.parse(md,{breaks:true,gfm:true,headerIds:false});
targetEl.innerHTML=DOMPurify.sanitize(html,{USE_PROFILES:{html:true}});
}
function showThinkingCat(targetEl){
targetEl.innerHTML='';
const img=document.createElement('img');
img.className='thinking-cat';
img.src=CAT_GIF+'?t='+Date.now();
img.alt=t('status.thinking','Thinking…');
targetEl.appendChild(img);
}
function clearThinkingCatIfPresent(targetEl){
const img=targetEl.querySelector('img.thinking-cat');
if(img) targetEl.innerHTML='';
}
// --- bottom helpers ---
function atBottom(){
return (mainScroll.scrollTop + mainScroll.clientHeight) >= (mainScroll.scrollHeight - 4);
}
// --- composer lock ---
function setComposerLocked(locked){
isRunning=locked;
inputText.placeholder = locked ? t('status.thinking','Thinking…') : t('composer.placeholder','What can I help you with?');
inputText.disabled=locked;
goBtn.style.display=locked?'none':'inline-block';
stopBtn.style.display=locked?'inline-block':'none';
if (locked) inputText.blur(); else inputText.focus();
}
// --- flow ---
async function start(){
const prompt=inputText.value.trim();
if(!prompt) return;
if(!firstRequestDone){ welcomeLogo?.remove(); firstRequestDone=true; }
const auto = atBottom();
const userMsg=document.createElement('div');
userMsg.className='message user-message';
userMsg.textContent=prompt;
thread.appendChild(userMsg);
const assistantMsg=document.createElement('div');
assistantMsg.className='message assistant-message';
showThinkingCat(assistantMsg);
thread.appendChild(assistantMsg);
if (auto) assistantMsg.scrollIntoView({ behavior:'smooth', block:'start' });
inputText.value='';
controller=new AbortController();
setComposerLocked(true);
try{
const res=await fetch(ENDPOINT,{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({ content: prompt }),
signal:controller.signal
});
if(!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
const reader=res.body.getReader();
const decoder=new TextDecoder();
let rawMarkdown=''; let receivedAny=false; let rafPending=false;
const flush=() => {
if (rafPending) return;
rafPending=true;
requestAnimationFrame(() => {
rafPending=false;
if(!receivedAny){ clearThinkingCatIfPresent(assistantMsg); receivedAny=true; }
renderMarkdown(assistantMsg, rawMarkdown);
if (auto && atBottom()){
mainScroll.scrollTop = mainScroll.scrollHeight;
}
});
};
for(;;){
const { value, done } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream:true });
for (const ch of chunk) rawMarkdown += ch; // char-by-char
flush();
}
flush();
}catch(err){
if (err.name!=='AbortError'){
clearThinkingCatIfPresent(assistantMsg);
renderMarkdown(assistantMsg, `\n\n**${t('status.errorLabel','Error')}:** ${err.message||err}`);
}else{
if (assistantMsg.querySelector('img.thinking-cat')){
clearThinkingCatIfPresent(assistantMsg);
renderMarkdown(assistantMsg, t('status.stopped','_Stopped._'));
}
}
}finally{
setComposerLocked(false);
controller=null;
}
}
async function stop(){
if(controller) controller.abort();
try{ await fetch(STOPPOINT,{method:'POST'}); }catch(_){}
}
// events
promptForm.addEventListener('submit', e => { e.preventDefault(); if (goBtn.style.display!=='none') start(); });
inputText.addEventListener('keydown', e => {
if (e.key==='Enter' && !e.shiftKey && goBtn.style.display!=='none'){ e.preventDefault(); start(); }
});
stopBtn.addEventListener('click', stop);
// init
window.addEventListener('DOMContentLoaded', async () => {
try{ await fetch('/api/clear',{method:'GET'}); }catch(_){}
await loadLocale(CURRENT_LOCALE);
inputText.focus();
});
</script>
</body>
</html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment