/* global React, DATA */
const Copilot = ({ open, onClose }) => {
const [messages, setMessages] = useState([
{ role: 'ai', t: `Günaydın Kaan Bey. ${fmtN(DATA.hotel_index.length)} otel, ${fmtN(DATA.flights_kpi_2025.flights)} uçuş, ${fmtN((DATA.top_recommendations || []).length)} öneri görüyorum. İstediğini sor — ambarı doğrudan sorgulayacağım.` }
]);
const [input, setInput] = useState('');
const [thinking, setThinking] = useState(false);
const endRef = useRef();
useEffect(() => { endRef.current && endRef.current.scrollTo({ top: 99999, behavior: 'smooth' }); }, [messages, thinking]);
const send = async (text) => {
if (!text.trim() || thinking) return;
const userMsg = { role: 'user', t: text };
const next = [...messages, userMsg];
setMessages(next);
setInput('');
setThinking(true);
try {
const apiMessages = next.map(m => ({
role: m.role === 'ai' ? 'assistant' : 'user',
content: m.t,
}));
const base = (typeof window !== 'undefined' && window.API_BASE_URL) || '';
// SSE streaming: render tokens as they arrive; tool turns show a status line.
const res = await fetch(base + '/api/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: apiMessages }),
});
if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
let acc = '', toolCalls = [], status = null;
setMessages(m => [...m, { role: 'ai', t: '', streaming: true }]);
const paint = () => setMessages(m => {
const copy = m.slice();
copy[copy.length - 1] = { role: 'ai', t: acc || (status ? '_' + status + '_' : ''),
tool_calls: toolCalls, streaming: true };
return copy;
});
const reader = res.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 (e) { continue; }
if (ev.type === 'delta') { acc += ev.data; paint(); }
else if (ev.type === 'status') { status = ev.data; if (!acc) paint(); }
else if (ev.type === 'tools') { toolCalls = ev.data || []; }
else if (ev.type === 'error') { acc += '\n\n_Hata: ' + ev.data + '_'; paint(); }
}
}
setMessages(m => {
const copy = m.slice();
copy[copy.length - 1] = { role: 'ai', t: acc || '(yanıt yok)', tool_calls: toolCalls };
return copy;
});
} catch (e) {
// On mid-stream failure REPLACE the streaming placeholder, don't append.
setMessages(m => {
const copy = m.slice();
const err = `_Masaya ulaşılamadı:_ ${e.message}`;
if (copy.length && copy[copy.length - 1].streaming) copy[copy.length - 1] = { role: 'ai', t: err };
else copy.push({ role: 'ai', t: err });
return copy;
});
} finally {
setThinking(false);
}
};
const quick = [
'gelir bazında en iyi 5 Belek 5★ otel',
'Bodrum 2026 tempo durumu',
'Rusya uçuş doluluk oranı aya göre',
'bu hafta en büyük fiyat önerileri',
'en düşük marjlı oteller',
];
if (!open) return null;
const fmtMsg = (t) => t.split('\n').map((line, i) => {
if (line.startsWith('• ')) return React.createElement('div', { key: i, style: { paddingLeft: 10 } }, line);
return React.createElement('div', { key: i,
dangerouslySetInnerHTML: { __html: line.replace(/\*\*(.+?)\*\*/g, '$1').replace(/_(.+?)_/g, '$1') } });
});
return React.createElement('aside', {
className: 'copilot-panel',
style: {
width: 'var(--copilot-w)', flexShrink: 0,
borderLeft: '1px solid var(--rule-soft)', background: 'var(--paper-soft)',
display: 'flex', flexDirection: 'column', height: '100%'
}
},
React.createElement('div', {
style: {
padding: '12px 14px', borderBottom: '1px solid var(--rule-soft)',
display: 'flex', alignItems: 'center', justifyContent: 'space-between'
}
},
React.createElement('div', null,
React.createElement('div', { className: 'row gap-2' },
React.createElement('span', { className: 'dot dot-pos' }),
React.createElement('span', { style: { fontWeight: 700, fontSize: 12 } }, 'RM Copilot')
),
React.createElement('div', { style: { fontSize: 10, color: 'var(--text-muted)', marginTop: 2 } },
'Anthropic Claude · ambarınıza dayalı')
),
React.createElement('button', { className: 'btn btn-sm btn-ghost', onClick: onClose,
title: 'Kapat (⌘ /)' }, '✕')
),
React.createElement('div', {
ref: endRef,
style: { flex: 1, overflowY: 'auto', padding: '14px 18px', display: 'flex', flexDirection: 'column', gap: 10,
width: '100%', maxWidth: 920, margin: '0 auto', boxSizing: 'border-box' }
},
messages.map((m, i) => (m.streaming && !m.t) ? null : React.createElement('div', {
key: i,
style: {
alignSelf: m.role === 'user' ? 'flex-end' : 'flex-start',
maxWidth: '88%',
padding: '11px 14px',
fontSize: 14,
lineHeight: 1.6,
borderRadius: 4,
background: m.role === 'user' ? 'var(--ink)' : 'var(--paper)',
color: m.role === 'user' ? 'var(--paper)' : 'var(--ink)',
border: m.role === 'user' ? 'none' : '1px solid var(--rule-soft)',
whiteSpace: 'pre-wrap'
}
}, fmtMsg(m.t))),
thinking && !(messages.length && messages[messages.length - 1].streaming && messages[messages.length - 1].t) &&
React.createElement('div', {
style: { alignSelf: 'flex-start', padding: '10px 13px', fontSize: 13, color: 'var(--text-muted)', fontStyle: 'italic' }
}, '· · · ambar sorgulanıyor')
),
React.createElement('div', {
style: { padding: '8px 10px', borderTop: '1px solid var(--rule-soft)', display: 'flex', flexWrap: 'wrap', gap: 4,
width: '100%', maxWidth: 920, margin: '0 auto', boxSizing: 'border-box' }
},
quick.map((q, i) => React.createElement('button', {
key: i, className: 'btn btn-sm btn-ghost',
style: { fontSize: 12 },
onClick: () => send(q)
}, q))
),
React.createElement('div', {
style: { padding: 10, borderTop: '1px solid var(--rule-soft)', display: 'flex', gap: 6,
width: '100%', maxWidth: 920, margin: '0 auto', boxSizing: 'border-box' }
},
React.createElement('input', {
className: 'input', style: { flex: 1 },
placeholder: 'İstediğini sor…',
value: input,
onChange: e => setInput(e.target.value),
onKeyDown: e => e.key === 'Enter' && send(input),
disabled: thinking
}),
React.createElement('button', { className: 'btn btn-sm btn-primary', onClick: () => send(input), disabled: thinking }, 'Gönder')
)
);
};
window.Copilot = Copilot;