diff --git a/field-logic/src/components/AudioRecorder.tsx b/field-logic/src/components/AudioRecorder.tsx index c4ed019..845cc94 100644 --- a/field-logic/src/components/AudioRecorder.tsx +++ b/field-logic/src/components/AudioRecorder.tsx @@ -38,9 +38,10 @@ export const AudioRecorder: React.FC = ({ sessionId, nodeId, onRecordingC setAudioUrl(url); // Save to Dexie DB - await saveAudio(sessionId, nodeId, blob); - // We pass the nodeId as the "key" reference for now - onRecordingComplete(nodeId); + const blobKey = await saveAudio(sessionId, nodeId, blob); + + // We pass the unique blobKey reference instead of nodeId + onRecordingComplete(blobKey); // Stop all tracks stream.getTracks().forEach(track => track.stop()); diff --git a/field-logic/src/components/SurveyWizard.tsx b/field-logic/src/components/SurveyWizard.tsx index 4f7bc72..ef017b2 100644 --- a/field-logic/src/components/SurveyWizard.tsx +++ b/field-logic/src/components/SurveyWizard.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from 'react'; -import { SurveyEngine } from '../lib/engine'; -import type { SurveyDefinition, SurveyNode } from '../lib/types'; +import { SurveyEngine } from '../lib/SurveyEngine'; +import type { SurveyDefinition, SurveyNode } from '../types/schema'; +import { AudioRecorder } from './AudioRecorder'; import '../styles/main.css'; interface Props { @@ -30,7 +31,7 @@ export const SurveyWizard: React.FC = ({ definition }) => { return; } - const nextNode = engine.next(inputValue); + const nextNode = engine.submitAnswer(inputValue); setCurrentNode(nextNode); setInputValue(null); // Reset input for next question }; @@ -48,8 +49,10 @@ export const SurveyWizard: React.FC = ({ definition }) => { return
Loading...
; } + const { sessionId } = engine.getSession(); + const renderContent = () => { - const { type, content } = currentNode; + const { type, content, id: nodeId } = currentNode; switch (type) { case 'info': @@ -116,6 +119,24 @@ export const SurveyWizard: React.FC = ({ definition }) => { ); + case 'audio': + return ( +
+

{content.question}

+ {content.helper_text &&

{content.helper_text}

} + handleChoiceSelect(blobKey)} + /> + {inputValue && ( +

+ ✓ Recording captured +

+ )} +
+ ); + case 'summary': return (
diff --git a/field-logic/src/lib/db.ts b/field-logic/src/lib/db.ts index d711e98..f79f3f1 100644 --- a/field-logic/src/lib/db.ts +++ b/field-logic/src/lib/db.ts @@ -9,7 +9,7 @@ export interface ResponseRecord { } export interface AudioBlobRecord { - id?: string; // Compound index key [sessionId+nodeId] + id: string; // Unique ID: sessionId_nodeId_timestamp sessionId: string; nodeId: string; blob: Blob; @@ -27,6 +27,19 @@ export class FieldLogicDB extends Dexie { responses: '[sessionId+nodeId], sessionId, nodeId', blobs: '[sessionId+nodeId], sessionId, nodeId, synced' }); + + this.version(2).stores({ + blobs: 'id, sessionId, nodeId, [sessionId+nodeId], synced' + }).upgrade(async tx => { + // Migrate existing blobs to have a unique string ID if they don't have one + return tx.table('blobs').toCollection().modify(record => { + if (!record.id) { + // Use existing fields to recreate the ID convention + const ts = record.timestamp || Date.now(); + record.id = `${record.sessionId}_${record.nodeId}_${ts}`; + } + }); + }); } } @@ -44,13 +57,17 @@ export const saveResponse = async (sessionId: string, nodeId: string, value: any // Helper to save Audio Blob export const saveAudio = async (sessionId: string, nodeId: string, blob: Blob) => { + const timestamp = Date.now(); + const id = `${sessionId}_${nodeId}_${timestamp}`; await db.blobs.put({ + id, sessionId, nodeId, blob, - timestamp: Date.now(), + timestamp, synced: false }); + return id; }; // Get all responses for a session @@ -58,6 +75,11 @@ export const getSessionResponses = async (sessionId: string) => { return await db.responses.where('sessionId').equals(sessionId).toArray(); }; +// Get the latest audio blob for a specific node in a session export const getAudioBlob = async (sessionId: string, nodeId: string) => { - return await db.blobs.get({ sessionId, nodeId }); + return await db.blobs.where({ sessionId, nodeId }).first(); +}; + +export const getAudioBlobById = async (id: string) => { + return await db.blobs.get(id); }; diff --git a/field-logic/src/lib/sync.ts b/field-logic/src/lib/sync.ts index f510eba..d58bb4d 100644 --- a/field-logic/src/lib/sync.ts +++ b/field-logic/src/lib/sync.ts @@ -20,8 +20,8 @@ export const flushAudioBlobs = async (token: string = 'test_token') => { const results = []; for (const record of unsyncedBlobs) { - const { sessionId, nodeId, blob } = record; - const filename = `${sessionId}_${nodeId}.webm`; + const { id, sessionId, nodeId, blob } = record; + const filename = `${id}.webm`; try { // A. Upload @@ -36,15 +36,15 @@ export const flushAudioBlobs = async (token: string = 'test_token') => { await saveResponse(sessionId, nodeId, metadata); // C. Delete local blob (free space) - // Using compound key [sessionId, nodeId] - await db.blobs.where({ sessionId, nodeId }).delete(); + // Using the unique primary key 'id' + await db.blobs.delete(id); console.log(`Synced & Flushed: ${filename}`); - results.push({ sessionId, nodeId, status: 'synced', url }); + results.push({ id, sessionId, nodeId, status: 'synced', url }); } catch (e) { console.error(`Failed to sync ${filename}`, e); - results.push({ sessionId, nodeId, status: 'failed', error: e }); + results.push({ id, sessionId, nodeId, status: 'failed', error: e }); } } return results;