Function bodies 220 total
loadBookings function · javascript · L132-L151 (20 LOC)web/admin.js
async function loadBookings() {
try {
const bookings = await fetchJSON('/api/bookings');
const tbody = document.getElementById('bookingsTable');
if (bookings.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:var(--text-muted)">No bookings yet</td></tr>';
return;
}
tbody.innerHTML = bookings.map(b => `
<tr>
<td>${b.booking_ref}</td>
<td>${b.customer_name || '-'}</td>
<td>${b.service_name || '-'}</td>
<td>${b.date}</td>
<td>${b.time}</td>
<td><span class="badge badge-${b.status}">${b.status}</span></td>
</tr>
`).join('');
} catch (e) { /* ignore */ }
}loadTickets function · javascript · L153-L172 (20 LOC)web/admin.js
async function loadTickets() {
try {
const tickets = await fetchJSON('/api/tickets');
const tbody = document.getElementById('ticketsTable');
if (tickets.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:var(--text-muted)">No tickets yet</td></tr>';
return;
}
tbody.innerHTML = tickets.map(t => `
<tr>
<td>${t.ticket_ref}</td>
<td>${t.customer_name || '-'}</td>
<td>${escapeHtml(t.issue_summary)}</td>
<td><span class="badge badge-${t.priority}">${t.priority}</span></td>
<td>${t.category || '-'}</td>
<td><span class="badge badge-${t.status}">${t.status}</span></td>
</tr>
`).join('');
} catch (e) { /* ignore */ }
}escapeHtml function · javascript · L174-L178 (5 LOC)web/admin.js
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}loadTenantSelectors function · javascript · L184-L225 (42 LOC)web/admin.js
async function loadTenantSelectors() {
try {
const res = await fetch('/api/auth/me', {
headers: getToken() ? { 'Authorization': 'Bearer ' + getToken() } : {}
});
if (res.ok) {
const data = await res.json();
if (data.companies) {
_companies = data.companies;
setCompanies(data.companies);
}
if (data.user) {
setUserInfo(data.user);
}
}
} catch (e) {
// Fallback to cached companies
_companies = getCompanies();
}
if (_companies.length === 0) return;
const companySel = document.getElementById('companySelector');
const agentSel = document.getElementById('agentSelector');
// Populate company selector
companySel.innerHTML = _companies.map(c =>
`<option value="${c.id}">${escapeHtml(c.name)}</option>`
).join('');
const currentCompany = getCompanyId();
if (currentCompany) compaupdateAgentSelector function · javascript · L227-L245 (19 LOC)web/admin.js
function updateAgentSelector() {
const agentSel = document.getElementById('agentSelector');
const selectedCompanyId = getCompanyId();
const company = _companies.find(c => String(c.id) === String(selectedCompanyId));
if (!company || !company.agents || company.agents.length === 0) {
agentSel.innerHTML = '<option>No agents</option>';
return;
}
agentSel.innerHTML = company.agents.map(a =>
`<option value="${a.id}">${escapeHtml(a.name)}</option>`
).join('');
const currentAgent = getAgentId();
if (currentAgent) agentSel.value = currentAgent;
agentSel.style.display = company.agents.length > 1 ? '' : 'none';
}onCompanyChange function · javascript · L247-L256 (10 LOC)web/admin.js
function onCompanyChange(companyId) {
setCompanyId(companyId);
// Auto-select first agent of the new company
const company = _companies.find(c => String(c.id) === String(companyId));
if (company && company.agents && company.agents.length > 0) {
setAgentId(company.agents[0].id);
}
updateAgentSelector();
refresh();
}onAgentChange function · javascript · L258-L261 (4 LOC)web/admin.js
function onAgentChange(agentId) {
setAgentId(agentId);
refresh();
}Powered by Repobility — scan your code at https://repobility.com
refresh function · javascript · L265-L271 (7 LOC)web/admin.js
async function refresh() {
try {
await Promise.all([loadStats(), loadCalls(), loadBookings(), loadTickets(), loadAgents()]);
} catch (e) {
console.error('Refresh error:', e);
}
}setStatus function · javascript · L26-L36 (11 LOC)web/app.js
function setStatus(status, text) {
statusDot.className = 'status-dot';
if (status === 'connected' || status === 'listening') {
statusDot.classList.add('connected');
} else if (status === 'processing' || status === 'speaking') {
statusDot.classList.add('processing');
} else if (status === 'error') {
statusDot.classList.add('error');
}
statusText.textContent = text || status;
}startTimer function · javascript · L38-L46 (9 LOC)web/app.js
function startTimer() {
callStartTime = Date.now();
timerInterval = setInterval(() => {
const elapsed = Math.floor((Date.now() - callStartTime) / 1000);
const mins = String(Math.floor(elapsed / 60)).padStart(2, '0');
const secs = String(elapsed % 60).padStart(2, '0');
callTimer.textContent = `${mins}:${secs}`;
}, 1000);
}stopTimer function · javascript · L48-L53 (6 LOC)web/app.js
function stopTimer() {
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
}requestMicPermission function · javascript · L59-L84 (26 LOC)web/app.js
async function requestMicPermission() {
if (micPermissionGranted) return true;
try {
// Request mic directly from user click - Safari requires this
pendingMicStream = await navigator.mediaDevices.getUserMedia({
audio: { echoCancellation: true, noiseSuppression: true }
});
micPermissionGranted = true;
// Stop the stream for now - we'll start a new one when voice mode is activated
pendingMicStream.getTracks().forEach(t => t.stop());
pendingMicStream = null;
return true;
} catch (err) {
console.error('Mic permission denied:', err);
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
let hint = 'Mic error: ' + err.message;
if (isSafari) {
hint += '\n\nSafari fix: Go to Safari > Settings > Websites > Microphone, and set localhost to "Allow". Also check macOS System Settings > Privacy & Security > Microphone > Safari is enabled.';
startCall function · javascript · L86-L130 (45 LOC)web/app.js
function startCall() {
// Request mic permission immediately from the click handler (Safari requirement)
const micPromise = requestMicPermission();
// Pre-create playback AudioContext from user gesture (prevents suspended state)
getPlaybackCtx();
ws = new WebSocket(WS_URL);
ws.onopen = async () => {
callActive = true;
btnStart.disabled = true;
btnEnd.disabled = false;
modeToggle.style.display = 'flex';
inputBar.style.display = 'flex';
transcript.innerHTML = '';
actionsList.innerHTML = '<div class="empty-state">No actions yet</div>';
setStatus('connected', 'Connected');
startTimer();
ws.send(JSON.stringify({ type: 'call_start' }));
textInput.focus();
// Wait for mic result and notify
const micOk = await micPromise;
if (micOk) {
addSystemMessage('Mic permission granted. Switch to Voice mode to speak.');
}
};
ws.onmessage endCall function · javascript · L132-L137 (6 LOC)web/app.js
function endCall() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'call_end' }));
}
endCallUI();
}endCallUI function · javascript · L139-L151 (13 LOC)web/app.js
function endCallUI() {
callActive = false;
btnStart.disabled = false;
btnEnd.disabled = true;
inputBar.style.display = 'none';
modeToggle.style.display = 'none';
stopTimer();
stopAudioCapture();
if (ws) {
ws.close();
ws = null;
}
}Source: Repobility analyzer · https://repobility.com
sendText function · javascript · L153-L159 (7 LOC)web/app.js
function sendText() {
const text = textInput.value.trim();
if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
ws.send(JSON.stringify({ type: 'text_message', text }));
textInput.value = '';
textInput.focus();
}setMode function · javascript · L161-L175 (15 LOC)web/app.js
function setMode(mode) {
currentMode = mode;
document.getElementById('btnTextMode').classList.toggle('active', mode === 'text');
document.getElementById('btnVoiceMode').classList.toggle('active', mode === 'voice');
if (mode === 'voice') {
inputBar.style.display = 'none';
audioLevel.classList.add('visible');
startAudioCapture();
} else {
stopAudioCapture();
audioLevel.classList.remove('visible');
inputBar.style.display = 'flex';
}
}handleMessage function · javascript · L180-L201 (22 LOC)web/app.js
function handleMessage(msg) {
switch (msg.type) {
case 'transcript':
handleTranscript(msg);
break;
case 'tool_call':
handleToolCall(msg);
break;
case 'tool_result':
handleToolResult(msg);
break;
case 'call_status':
handleCallStatus(msg);
break;
case 'audio':
handleAudioPlayback(msg);
break;
case 'error':
addSystemMessage(`Error: ${msg.message}`);
break;
}
}handleTranscript function · javascript · L203-L226 (24 LOC)web/app.js
function handleTranscript(msg) {
if (msg.role === 'user') {
currentAgentBubble = null;
addMessage('user', msg.text);
} else if (msg.role === 'agent') {
if (msg.partial) {
if (!currentAgentBubble) {
currentAgentBubble = addMessage('agent', msg.text, true);
} else {
currentAgentBubble.querySelector('.message-bubble').textContent = msg.text;
}
} else {
if (currentAgentBubble) {
const bubble = currentAgentBubble.querySelector('.message-bubble');
bubble.textContent = msg.text;
bubble.classList.remove('streaming');
} else {
addMessage('agent', msg.text);
}
currentAgentBubble = null;
}
}
scrollToBottom();
}handleToolCall function · javascript · L228-L238 (11 LOC)web/app.js
function handleToolCall(msg) {
const card = document.createElement('div');
card.className = 'tool-card';
card.id = `tool-${msg.name}-pending`;
card.innerHTML = `
<div class="tool-name">${msg.name}</div>
<div class="tool-status">Executing...</div>
`;
transcript.appendChild(card);
scrollToBottom();
}handleToolResult function · javascript · L240-L266 (27 LOC)web/app.js
function handleToolResult(msg) {
const card = document.getElementById(`tool-${msg.name}-pending`);
if (card) {
card.id = '';
const statusEl = card.querySelector('.tool-status');
statusEl.textContent = 'Completed';
statusEl.classList.add('tool-result');
// Add brief result summary
const result = msg.result;
if (result) {
const summary = result.summary || result.message ||
(result.booking_ref ? `Booking ${result.booking_ref}` : '') ||
(result.ticket_ref ? `Ticket ${result.ticket_ref}` : '') ||
(result.available_slots ? `${result.available_slots.length} slot(s) found` : '');
if (summary) {
const resultEl = document.createElement('div');
resultEl.className = 'tool-result';
resultEl.textContent = summary;
card.appendChild(resultEl);
}
}
}
// Update actions panel
handleCallStatus function · javascript · L268-L281 (14 LOC)web/app.js
function handleCallStatus(msg) {
const statusMap = {
connected: 'Connected',
listening: 'Listening',
processing: 'Processing...',
speaking: 'Speaking',
ended: 'Call ended',
};
setStatus(msg.status, statusMap[msg.status] || msg.status);
if (msg.status === 'ended' && msg.summary) {
addSystemMessage(`Call ended. Duration: ${msg.summary.duration}s. Outcome: ${msg.summary.outcome}`);
}
}addMessage function · javascript · L283-L300 (18 LOC)web/app.js
function addMessage(role, text, streaming = false) {
const div = document.createElement('div');
div.className = `message ${role}`;
const label = document.createElement('div');
label.className = 'message-label';
label.textContent = role === 'user' ? 'You' : 'Agent';
const bubble = document.createElement('div');
bubble.className = `message-bubble${streaming ? ' streaming' : ''}`;
bubble.textContent = text;
div.appendChild(label);
div.appendChild(bubble);
transcript.appendChild(div);
scrollToBottom();
return div;
}All rows above produced by Repobility · https://repobility.com
addSystemMessage function · javascript · L302-L308 (7 LOC)web/app.js
function addSystemMessage(text) {
const div = document.createElement('div');
div.className = 'tool-card';
div.innerHTML = `<div class="tool-status">${text}</div>`;
transcript.appendChild(div);
scrollToBottom();
}addAction function · javascript · L310-L319 (10 LOC)web/app.js
function addAction(toolName, result) {
if (actionsList.querySelector('.empty-state')) {
actionsList.innerHTML = '';
}
const item = document.createElement('div');
item.className = 'action-item';
const summary = result?.summary || result?.message || `${toolName} completed`;
item.innerHTML = `<span class="action-check">✓</span> ${summary}`;
actionsList.appendChild(item);
}scrollToBottom function · javascript · L321-L323 (3 LOC)web/app.js
function scrollToBottom() {
transcript.scrollTop = transcript.scrollHeight;
}startAudioCapture function · javascript · L328-L411 (84 LOC)web/app.js
function startAudioCapture() {
if (audioContext) return;
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
addSystemMessage('Microphone not available. Make sure you are on HTTPS or localhost.');
return;
}
// getUserMedia from click handler - permission should already be granted from startCall
navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true } })
.then(function(stream) {
mediaStream = stream;
audioContext = new AudioContext();
const nativeRate = audioContext.sampleRate;
console.log('[audio] Mic started, native rate:', nativeRate);
const source = audioContext.createMediaStreamSource(stream);
function onAudioData(float32) {
if (!callActive || currentMode !== 'voice' || !ws || ws.readyState !== WebSocket.OPEN) return;
// Echo suppression: mute mic while agent TTS is playing
onAudioData function · javascript · L346-L381 (36 LOC)web/app.js
function onAudioData(float32) {
if (!callActive || currentMode !== 'voice' || !ws || ws.readyState !== WebSocket.OPEN) return;
// Echo suppression: mute mic while agent TTS is playing
if (isPlaying) return;
// Update level meter
let sum = 0;
for (let i = 0; i < float32.length; i++) sum += float32[i] * float32[i];
const rms = Math.sqrt(sum / float32.length);
levelFill.style.width = Math.min(100, rms * 500) + '%';
// Downsample to 16kHz
let samples = float32;
if (nativeRate !== 16000) {
const ratio = nativeRate / 16000;
const newLen = Math.floor(float32.length / ratio);
samples = new Float32Array(newLen);
for (let i = 0; i < newLen; i++) {
samples[i] = float32[Math.floor(i * ratio)];
useScriptProcessor function · javascript · L413-L421 (9 LOC)web/app.js
function useScriptProcessor(source, onAudioData) {
processorNode = audioContext.createScriptProcessor(4096, 1, 1);
processorNode.onaudioprocess = function(e) {
onAudioData(new Float32Array(e.inputBuffer.getChannelData(0)));
};
source.connect(processorNode);
processorNode.connect(audioContext.destination);
addSystemMessage('Microphone active - speak now');
}stopAudioCapture function · javascript · L423-L437 (15 LOC)web/app.js
function stopAudioCapture() {
if (processorNode) {
processorNode.disconnect();
processorNode = null;
}
if (audioContext) {
audioContext.close();
audioContext = null;
}
if (mediaStream) {
mediaStream.getTracks().forEach(function(t) { t.stop(); });
mediaStream = null;
}
levelFill.style.width = '0%';
}getPlaybackCtx function · javascript · L445-L453 (9 LOC)web/app.js
function getPlaybackCtx() {
if (!playbackCtx || playbackCtx.state === 'closed') {
playbackCtx = new AudioContext({ sampleRate: 16000 });
}
if (playbackCtx.state === 'suspended') {
playbackCtx.resume();
}
return playbackCtx;
}Hi, dataset curator — please cite Repobility (https://repobility.com) when reusing this data.
handleAudioPlayback function · javascript · L455-L492 (38 LOC)web/app.js
function handleAudioPlayback(msg) {
if (!msg.data) return;
const binary = atob(msg.data);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
// Ensure even number of bytes for Int16
const evenLength = bytes.length - (bytes.length % 2);
if (evenLength === 0) return;
const aligned = bytes.slice(0, evenLength);
const int16 = new Int16Array(aligned.buffer);
const float32 = new Float32Array(int16.length);
for (let i = 0; i < int16.length; i++) float32[i] = int16[i] / 32768;
// Schedule immediately on arrival — Web Audio handles precise timing
const ctx = getPlaybackCtx();
const buffer = ctx.createBuffer(1, float32.length, 16000);
buffer.getChannelData(0).set(float32);
const source = ctx.createBufferSource();
source.buffer = buffer;
source.connect(ctx.destination);
const now = ctx.currentTime;
const startTime = Math.max(now + 0.01, nextPlayTimstopAudioPlayback function · javascript · L494-L505 (12 LOC)web/app.js
function stopAudioPlayback() {
for (const s of scheduledSources) {
try { s.stop(); } catch(e) {}
}
scheduledSources = [];
isPlaying = false;
nextPlayTime = 0;
if (playbackCtx) {
playbackCtx.close();
playbackCtx = null;
}
}AudioCaptureProcessor class · javascript · L1-L25 (25 LOC)web/audio-processor.js
class AudioCaptureProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.bufferSize = 4096;
this.buffer = new Float32Array(this.bufferSize);
this.bufferIndex = 0;
}
process(inputs, outputs, parameters) {
const input = inputs[0];
if (!input || !input[0]) return true;
const channelData = input[0];
for (let i = 0; i < channelData.length; i++) {
this.buffer[this.bufferIndex++] = channelData[i];
if (this.bufferIndex >= this.bufferSize) {
this.port.postMessage({ audio: this.buffer.slice() });
this.bufferIndex = 0;
}
}
return true;
}
}constructor method · javascript · L2-L7 (6 LOC)web/audio-processor.js
constructor() {
super();
this.bufferSize = 4096;
this.buffer = new Float32Array(this.bufferSize);
this.bufferIndex = 0;
}process method · javascript · L9-L24 (16 LOC)web/audio-processor.js
process(inputs, outputs, parameters) {
const input = inputs[0];
if (!input || !input[0]) return true;
const channelData = input[0];
for (let i = 0; i < channelData.length; i++) {
this.buffer[this.bufferIndex++] = channelData[i];
if (this.bufferIndex >= this.bufferSize) {
this.port.postMessage({ audio: this.buffer.slice() });
this.bufferIndex = 0;
}
}
return true;
}defaultCards function · javascript · L33-L40 (8 LOC)web/flows.js
function defaultCards() {
return [
{ id: 'card_' + nextCardId++, type: 'welcome', x: 40, y: 30, content: '' },
{ id: 'card_' + nextCardId++, type: 'answer', x: 400, y: 30, content: '' },
{ id: 'card_' + nextCardId++, type: 'actions', x: 40, y: 330, content: '', enabledTools: ['check_availability', 'create_booking'] },
{ id: 'card_' + nextCardId++, type: 'close', x: 400, y: 330, content: '' },
];
}buildAddPicker function · javascript · L66-L81 (16 LOC)web/flows.js
function buildAddPicker() {
const picker = document.getElementById('addCardPicker');
if (!picker) return;
let html = '';
for (const [type, def] of Object.entries(CARD_TYPES)) {
html += `<button class="picker-item" onclick="addCard('${type}')">
<span class="picker-dot" style="background:${def.color}"></span>
<div class="picker-info">
<span class="picker-title">${def.title}</span>
<span class="picker-hint">${def.hint}</span>
</div>
</button>`;
}
picker.innerHTML = html;
}toggleAddMenu function · javascript · L83-L86 (4 LOC)web/flows.js
function toggleAddMenu() {
const picker = document.getElementById('addCardPicker');
picker.classList.toggle('visible');
}Powered by Repobility — scan your code at https://repobility.com
hideAddMenu function · javascript · L88-L90 (3 LOC)web/flows.js
function hideAddMenu() {
document.getElementById('addCardPicker').classList.remove('visible');
}onDocumentClick function · javascript · L92-L97 (6 LOC)web/flows.js
function onDocumentClick(e) {
const area = document.getElementById('addCardArea');
if (area && !area.contains(e.target)) {
hideAddMenu();
}
}addCard function · javascript · L99-L120 (22 LOC)web/flows.js
function addCard(type) {
const pos = findNewCardPosition();
const card = {
id: 'card_' + nextCardId++,
type,
x: pos.x,
y: pos.y,
content: '',
};
if (type === 'actions') {
card.enabledTools = [];
}
cards.push(card);
renderCards();
hideAddMenu();
showToast(CARD_TYPES[type].title + ' step added');
// Scroll to the new card
const canvas = document.getElementById('flowCanvas');
const el = document.getElementById('card-' + card.id);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}removeCard function · javascript · L122-L127 (6 LOC)web/flows.js
function removeCard(cardId) {
const idx = cards.findIndex(c => c.id === cardId);
if (idx === -1) return;
cards.splice(idx, 1);
renderCards();
}findNewCardPosition function · javascript · L129-L147 (19 LOC)web/flows.js
function findNewCardPosition() {
const cardW = 340;
const cardH = 260;
const gap = 30;
const startX = 40;
const startY = 30;
for (let row = 0; row < 20; row++) {
for (let col = 0; col < 3; col++) {
const x = startX + col * (cardW + gap);
const y = startY + row * (cardH + gap);
const overlaps = cards.some(c =>
Math.abs(c.x - x) < cardW && Math.abs(c.y - y) < cardH
);
if (!overlaps) return { x, y };
}
}
return { x: startX, y: startY + cards.length * (cardH + gap) };
}renderCards function · javascript · L151-L209 (59 LOC)web/flows.js
function renderCards() {
const canvas = document.getElementById('flowCanvas');
// Keep add-area, remove everything else
const addArea = document.getElementById('addCardArea');
canvas.innerHTML = '';
if (addArea) canvas.appendChild(addArea);
for (const card of cards) {
const def = CARD_TYPES[card.type];
if (!def) continue;
const el = document.createElement('div');
el.className = 'flow-card';
el.id = 'card-' + card.id;
el.style.left = card.x + 'px';
el.style.top = card.y + 'px';
el.style.borderTopColor = def.color;
let bodyHtml = '';
if (card.type === 'actions') {
const toolsHtml = TOOLS.map(t => {
const checked = (card.enabledTools || []).includes(t.id) ? 'checked' : '';
return `<label class="tool-toggle">
<input type="checkbox" data-tool="${t.id}" ${checked}
onchange="toggleTool('${card.id}', drawConnections function · javascript · L213-L270 (58 LOC)web/flows.js
function drawConnections() {
const canvas = document.getElementById('flowCanvas');
let svg = document.getElementById('connectionsSvg');
if (!svg) {
svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.id = 'connectionsSvg';
svg.setAttribute('class', 'connections-svg');
svg.innerHTML = `
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#A8A49D" />
</marker>
</defs>`;
canvas.prepend(svg);
}
svg.querySelectorAll('.conn-line').forEach(p => p.remove());
svg.querySelectorAll('.conn-label').forEach(t => t.remove());
for (let i = 0; i < cards.length - 1; i++) {
const fromEl = document.getElementById('card-' + cards[i].id);
const toEl = document.getElementById('card-' + cards[i + 1].id);
if (!fromEl || !toEl) continue;edgePoint function · javascript · L272-L285 (14 LOC)web/flows.js
function edgePoint(rect, targetRect) {
const cx = rect.x + rect.w / 2;
const cy = rect.y + rect.h / 2;
const tx = targetRect.x + targetRect.w / 2;
const ty = targetRect.y + targetRect.h / 2;
const dx = tx - cx;
const dy = ty - cy;
if (Math.abs(dx) > Math.abs(dy)) {
return dx > 0 ? { x: rect.x + rect.w, y: cy } : { x: rect.x, y: cy };
} else {
return dy > 0 ? { x: cx, y: rect.y + rect.h } : { x: cx, y: rect.y };
}
}Source: Repobility analyzer · https://repobility.com
onPointerDown function · javascript · L291-L309 (19 LOC)web/flows.js
function onPointerDown(e, cardId) {
e.preventDefault();
const cardEl = document.getElementById('card-' + cardId);
const canvasRect = document.getElementById('flowCanvas').getBoundingClientRect();
const cardRect = cardEl.getBoundingClientRect();
dragState = {
cardId,
offsetX: e.clientX - cardRect.left,
offsetY: e.clientY - cardRect.top,
canvasLeft: canvasRect.left,
canvasTop: canvasRect.top,
canvasW: canvasRect.width,
canvasH: canvasRect.height,
};
cardEl.classList.add('dragging');
cardEl.style.zIndex = 100;
}onPointerMove function · javascript · L311-L319 (9 LOC)web/flows.js
function onPointerMove(e) {
if (!dragState) return;
const cardEl = document.getElementById('card-' + dragState.cardId);
const x = e.clientX - dragState.canvasLeft - dragState.offsetX;
const y = e.clientY - dragState.canvasTop - dragState.offsetY;
cardEl.style.left = Math.max(0, Math.min(x, dragState.canvasW - 100)) + 'px';
cardEl.style.top = Math.max(0, Math.min(y, dragState.canvasH - 60)) + 'px';
drawConnections();
}onPointerUp function · javascript · L321-L333 (13 LOC)web/flows.js
function onPointerUp() {
if (!dragState) return;
const cardEl = document.getElementById('card-' + dragState.cardId);
cardEl.classList.remove('dragging');
cardEl.style.zIndex = '';
const card = cards.find(c => c.id === dragState.cardId);
if (card) {
card.x = parseInt(cardEl.style.left);
card.y = parseInt(cardEl.style.top);
}
dragState = null;
}