← back to jooliperbush__voiceagent

Function bodies 220 total

All specs Real LLM only Function bodies
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) compa
updateAgentSelector 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">&#10003;</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, nextPlayTim
stopAudioPlayback 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;
}
‹ prevpage 4 / 5next ›