| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| class InteractionTracker { |
| constructor() { |
| this.events = []; |
| this.focusStartTime = {}; |
| this.focusTime = {}; |
| this.scrollDepthMax = 0; |
| this.currentInstanceId = null; |
| this.previousInstanceId = null; |
| this.flushInterval = 5000; |
| this.lastFlush = Date.now(); |
| this.isInitialized = false; |
| this.debugMode = false; |
|
|
| |
| if (document.readyState === 'loading') { |
| document.addEventListener('DOMContentLoaded', () => this.init()); |
| } else { |
| this.init(); |
| } |
| } |
|
|
| init() { |
| if (this.isInitialized) return; |
| this.isInitialized = true; |
|
|
| |
| document.addEventListener('click', (e) => this.trackClick(e), true); |
|
|
| |
| document.addEventListener('focusin', (e) => this.trackFocusIn(e), true); |
| document.addEventListener('focusout', (e) => this.trackFocusOut(e), true); |
|
|
| |
| window.addEventListener('scroll', () => this.trackScroll(), { passive: true }); |
|
|
| |
| document.addEventListener('keydown', (e) => this.trackKeypress(e), true); |
|
|
| |
| window.addEventListener('beforeunload', () => this.flush(true)); |
| window.addEventListener('pagehide', () => this.flush(true)); |
|
|
| |
| this.flushTimer = setInterval(() => this.flush(false), this.flushInterval); |
|
|
| if (this.debugMode) { |
| console.log('[InteractionTracker] Initialized'); |
| } |
| } |
|
|
| |
| |
| |
| |
| setInstanceId(instanceId) { |
| if (this.debugMode) { |
| console.log(`[InteractionTracker] setInstanceId: ${instanceId}`); |
| } |
|
|
| |
| if (this.currentInstanceId && this.currentInstanceId !== instanceId) { |
| this.flush(true); |
| } |
|
|
| this.previousInstanceId = this.currentInstanceId; |
| this.currentInstanceId = instanceId; |
|
|
| |
| this.scrollDepthMax = 0; |
|
|
| this.addEvent('navigation', 'instance_load', { |
| instance_id: instanceId, |
| from_instance: this.previousInstanceId |
| }); |
| } |
|
|
| |
| |
| |
| |
| trackClick(e) { |
| const target = this.getTargetIdentifier(e.target); |
| if (target) { |
| this.addEvent('click', target, { |
| x: e.clientX, |
| y: e.clientY, |
| }); |
| } |
| } |
|
|
| |
| |
| |
| |
| trackFocusIn(e) { |
| const target = this.getTargetIdentifier(e.target); |
| if (target) { |
| this.focusStartTime[target] = Date.now(); |
| this.addEvent('focus_in', target); |
| } |
| } |
|
|
| |
| |
| |
| |
| trackFocusOut(e) { |
| const target = this.getTargetIdentifier(e.target); |
| if (target && this.focusStartTime[target]) { |
| const duration = Date.now() - this.focusStartTime[target]; |
| this.focusTime[target] = (this.focusTime[target] || 0) + duration; |
| delete this.focusStartTime[target]; |
| this.addEvent('focus_out', target, { duration_ms: duration }); |
| } |
| } |
|
|
| |
| |
| |
| trackScroll() { |
| const scrollTop = window.pageYOffset || document.documentElement.scrollTop; |
| const scrollHeight = document.documentElement.scrollHeight - window.innerHeight; |
| const scrollPercent = scrollHeight > 0 ? (scrollTop / scrollHeight) * 100 : 0; |
| this.scrollDepthMax = Math.max(this.scrollDepthMax, scrollPercent); |
| } |
|
|
| |
| |
| |
| |
| trackKeypress(e) { |
| |
| if (e.key >= '0' && e.key <= '9') { |
| this.addEvent('keypress', `key:${e.key}`, { |
| ctrl: e.ctrlKey, |
| alt: e.altKey, |
| shift: e.shiftKey, |
| }); |
| } |
|
|
| |
| if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { |
| this.addEvent('keypress', `nav:${e.key}`, { |
| ctrl: e.ctrlKey, |
| alt: e.altKey, |
| }); |
| } |
|
|
| |
| if ((e.ctrlKey || e.metaKey) && e.key === 's') { |
| this.addEvent('keypress', 'save:shortcut'); |
| } |
| } |
|
|
| |
| |
| |
| |
| trackAIRequest(schemaName) { |
| this.addEvent('ai_request', `schema:${schemaName}`); |
|
|
| |
| this.sendAIUsage('request', schemaName); |
| } |
|
|
| |
| |
| |
| |
| |
| trackAIResponse(schemaName, suggestions) { |
| this.addEvent('ai_response', `schema:${schemaName}`, { |
| suggestion_count: suggestions ? suggestions.length : 0, |
| suggestions: suggestions |
| }); |
|
|
| this.sendAIUsage('response', schemaName, { suggestions }); |
| } |
|
|
| |
| |
| |
| |
| |
| trackAIAccept(schemaName, acceptedValue) { |
| this.addEvent('ai_accept', `schema:${schemaName}`, { |
| accepted: acceptedValue |
| }); |
|
|
| this.sendAIUsage('accept', schemaName, { accepted_value: acceptedValue }); |
| } |
|
|
| |
| |
| |
| |
| trackAIReject(schemaName) { |
| this.addEvent('ai_reject', `schema:${schemaName}`); |
|
|
| this.sendAIUsage('reject', schemaName); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| trackAnnotationChange(schemaName, labelName, action, oldValue, newValue, source = 'user') { |
| this.addEvent('annotation_change', `schema:${schemaName}`, { |
| label: labelName, |
| action: action, |
| old_value: oldValue, |
| new_value: newValue, |
| source: source, |
| }); |
|
|
| |
| this.sendAnnotationChange(schemaName, labelName, action, oldValue, newValue, source); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| trackNavigation(action, fromInstance, toInstance) { |
| this.addEvent('navigation', action, { |
| from_instance: fromInstance, |
| to_instance: toInstance, |
| }); |
| } |
|
|
| |
| |
| |
| |
| trackSave(instanceId) { |
| this.addEvent('save', `instance:${instanceId || this.currentInstanceId}`); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| trackStaleAnnotation(schemaName, value, reason) { |
| this.addEvent('annotation_stale', `schema:${schemaName}`, { |
| stale_value: value, |
| reason: reason, |
| timestamp: new Date().toISOString() |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| trackDisplayLogicChange(schemaName, visible, reason) { |
| this.addEvent('display_logic_change', `schema:${schemaName}`, { |
| visible: visible, |
| reason: reason, |
| timestamp: new Date().toISOString() |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| getTargetIdentifier(element) { |
| if (!element || !element.closest) return null; |
|
|
| |
| const labelInput = element.closest('input[type="checkbox"], input[type="radio"]'); |
| if (labelInput) { |
| const name = labelInput.name || ''; |
| const value = labelInput.value || ''; |
| if (name) { |
| return `label:${name}:${value}`; |
| } |
| } |
|
|
| |
| const labelWrapper = element.closest('[data-label-name]'); |
| if (labelWrapper) { |
| return `label:${labelWrapper.dataset.labelName}`; |
| } |
|
|
| |
| const schema = element.closest('[data-schema-name]'); |
| if (schema) { |
| return `schema:${schema.dataset.schemaName}`; |
| } |
|
|
| |
| const schemaContainer = element.closest('.annotation-schema'); |
| if (schemaContainer) { |
| const schemaName = schemaContainer.id || schemaContainer.dataset.schema; |
| if (schemaName) { |
| return `schema:${schemaName}`; |
| } |
| } |
|
|
| |
| if (element.id === 'next_instance_button' || element.closest('#next_instance_button')) { |
| return 'nav:next'; |
| } |
| if (element.id === 'prev_instance_button' || element.closest('#prev_instance_button')) { |
| return 'nav:prev'; |
| } |
| if (element.id === 'save_button' || element.closest('#save_button')) { |
| return 'nav:save'; |
| } |
|
|
| |
| if (element.closest('.ai-assistant-button')) return 'ai:request'; |
| if (element.closest('.ai-suggestion')) return 'ai:suggestion'; |
| if (element.closest('.ai-assistant-panel')) return 'ai:panel'; |
|
|
| |
| if (element.closest('.annotation-span')) return 'span:click'; |
| if (element.closest('.span-label-option')) return 'span:label'; |
|
|
| |
| const textInput = element.closest('input[type="text"], textarea'); |
| if (textInput && textInput.name) { |
| return `textbox:${textInput.name}`; |
| } |
|
|
| |
| const slider = element.closest('input[type="range"]'); |
| if (slider && slider.name) { |
| return `slider:${slider.name}`; |
| } |
|
|
| return null; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| addEvent(eventType, target, metadata = {}) { |
| const event = { |
| event_type: eventType, |
| timestamp: Date.now() / 1000, |
| client_timestamp: Date.now(), |
| target: target, |
| instance_id: this.currentInstanceId, |
| metadata: metadata, |
| }; |
|
|
| this.events.push(event); |
|
|
| if (this.debugMode) { |
| console.log('[InteractionTracker] Event:', event); |
| } |
|
|
| |
| if (this.events.length >= 50) { |
| this.flush(false); |
| } |
| } |
|
|
| |
| |
| |
| |
| async flush(isFinal) { |
| if (this.events.length === 0 && Object.keys(this.focusTime).length === 0) { |
| return; |
| } |
|
|
| const payload = { |
| instance_id: this.currentInstanceId, |
| events: [...this.events], |
| focus_time: { ...this.focusTime }, |
| scroll_depth: this.scrollDepthMax, |
| }; |
|
|
| |
| this.events = []; |
| this.focusTime = {}; |
|
|
| if (this.debugMode) { |
| console.log('[InteractionTracker] Flushing:', payload); |
| } |
|
|
| if (isFinal) { |
| |
| const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' }); |
| navigator.sendBeacon('/api/track_interactions', blob); |
| } else { |
| try { |
| await fetch('/api/track_interactions', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(payload), |
| }); |
| } catch (e) { |
| if (this.debugMode) { |
| console.warn('[InteractionTracker] Failed to send interaction data:', e); |
| } |
| } |
| } |
|
|
| this.lastFlush = Date.now(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async sendAIUsage(eventType, schemaName, data = {}) { |
| try { |
| await fetch('/api/track_ai_usage', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| instance_id: this.currentInstanceId, |
| schema_name: schemaName, |
| event_type: eventType, |
| ...data, |
| }), |
| }); |
| } catch (e) { |
| if (this.debugMode) { |
| console.warn('[InteractionTracker] Failed to send AI usage data:', e); |
| } |
| } |
| } |
|
|
| |
| |
| |
| async sendAnnotationChange(schemaName, labelName, action, oldValue, newValue, source) { |
| try { |
| await fetch('/api/track_annotation_change', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| instance_id: this.currentInstanceId, |
| schema_name: schemaName, |
| label_name: labelName, |
| action: action, |
| old_value: oldValue, |
| new_value: newValue, |
| source: source, |
| }), |
| }); |
| } catch (e) { |
| if (this.debugMode) { |
| console.warn('[InteractionTracker] Failed to send annotation change:', e); |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| setDebugMode(enabled) { |
| this.debugMode = enabled; |
| console.log(`[InteractionTracker] Debug mode: ${enabled ? 'enabled' : 'disabled'}`); |
| } |
|
|
| |
| |
| |
| destroy() { |
| if (this.flushTimer) { |
| clearInterval(this.flushTimer); |
| } |
| this.flush(true); |
| } |
| } |
|
|
| |
| window.interactionTracker = new InteractionTracker(); |
|
|
| |
| window.InteractionTracker = InteractionTracker; |
|
|