diff --git a/focus-group-summary.md b/focus-group-summary.md new file mode 100644 index 0000000..939833a --- /dev/null +++ b/focus-group-summary.md @@ -0,0 +1,12 @@ +Contributor: Alivia Hossain (Aspiring GSoC Contributor. Will drop GSoC 2026 proposal) +Changes made: Focus Group Prototype +Future SCope and other details : Submitted in GSoC 2026 proposal + +How to test run: +1. Terminal: npm install +2. Terminal: npm run serve +3. Browser : http://localhost:8080/setup +3. Browser : http://localhost:8080/session +3. Browser : http://localhost:8080/analysis + +Further changes and suggestions always welcome and appreciated ! \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1a0192b..637fc16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,12 @@ "@tensorflow/tfjs-backend-wasm": "^3.10.0", "@tensorflow/tfjs-converter": "^3.10.0", "@tensorflow/tfjs-core": "^3.10.0", + "@vue/composition-api": "^1.7.2", "axios": "^1.7.9", "core-js": "^3.6.5", "firebase": "^8.10.1", "heatmap.js": "^2.0.5", + "pinia": "^2.0.36", "vue": "^2.6.11", "vue-router": "^3.2.0", "vuetify": "^2.4.0", @@ -3514,6 +3516,21 @@ "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", "dev": true }, + "node_modules/@vue/composition-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@vue/composition-api/-/composition-api-1.7.2.tgz", + "integrity": "sha512-M8jm9J/laYrYT02665HkZ5l2fWTK4dcVg3BsDHm/pfz+MjDYwX+9FUaZyGwEyXEDonQYRCo0H7aLgdklcIELjw==", + "license": "MIT", + "peerDependencies": { + "vue": ">= 2.5 < 2.7" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, "node_modules/@vue/shared": { "version": "3.5.26", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.26.tgz", @@ -8936,6 +8953,32 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pinia": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.36.tgz", + "integrity": "sha512-4UKApwjlmJH+VuHKgA+zQMddcCb3ezYnyewQ9NVrsDqZ/j9dMv5+rh+1r48whKNdpFkZAWVxhBp5ewYaYX9JcQ==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.5.0", + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/composition-api": "^1.4.0", + "typescript": ">=4.4.4", + "vue": "^2.6.14 || ^3.2.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -11408,6 +11451,32 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/vue-eslint-parser": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-8.3.0.tgz", diff --git a/package.json b/package.json index 5c1dc9a..b96d5d3 100644 --- a/package.json +++ b/package.json @@ -13,22 +13,24 @@ "@tensorflow/tfjs-backend-wasm": "^3.10.0", "@tensorflow/tfjs-converter": "^3.10.0", "@tensorflow/tfjs-core": "^3.10.0", + "@vue/composition-api": "^1.7.2", "axios": "^1.7.9", "core-js": "^3.6.5", "firebase": "^8.10.1", "heatmap.js": "^2.0.5", + "pinia": "^2.0.36", "vue": "^2.6.11", "vue-router": "^3.2.0", "vuetify": "^2.4.0", "vuex": "^3.4.0" }, "devDependencies": { + "@babel/eslint-parser": "^7.16.0", "@vue/cli-plugin-babel": "~5.0.0", "@vue/cli-plugin-eslint": "~5.0.0", "@vue/cli-plugin-router": "~5.0.0", "@vue/cli-plugin-vuex": "~5.0.0", "@vue/cli-service": "~5.0.0", - "@babel/eslint-parser": "^7.16.0", "eslint": "^7.32.0", "eslint-plugin-vue": "^8.0.3", "sass": "~1.32.0", @@ -59,4 +61,4 @@ "last 2 versions", "not dead" ] -} \ No newline at end of file +} diff --git a/src/components/focus-group/AnalysisChart.vue b/src/components/focus-group/AnalysisChart.vue new file mode 100644 index 0000000..960723d --- /dev/null +++ b/src/components/focus-group/AnalysisChart.vue @@ -0,0 +1,131 @@ + + + + + \ No newline at end of file diff --git a/src/components/focus-group/NoteCapture.vue b/src/components/focus-group/NoteCapture.vue new file mode 100644 index 0000000..f3819c0 --- /dev/null +++ b/src/components/focus-group/NoteCapture.vue @@ -0,0 +1,153 @@ + + + + + \ No newline at end of file diff --git a/src/components/focus-group/PulseButtons.vue b/src/components/focus-group/PulseButtons.vue new file mode 100644 index 0000000..bb269e7 --- /dev/null +++ b/src/components/focus-group/PulseButtons.vue @@ -0,0 +1,108 @@ + + + + + \ No newline at end of file diff --git a/src/components/focus-group/SessionGuide.vue b/src/components/focus-group/SessionGuide.vue new file mode 100644 index 0000000..31e165f --- /dev/null +++ b/src/components/focus-group/SessionGuide.vue @@ -0,0 +1,134 @@ + + + + + \ No newline at end of file diff --git a/src/main.js b/src/main.js index 219cc2f..cf85822 100644 --- a/src/main.js +++ b/src/main.js @@ -1,23 +1,24 @@ import Vue from 'vue' +import VueCompositionAPI from '@vue/composition-api' // MUST be first +import { createPinia, PiniaVuePlugin } from 'pinia' import App from './App.vue' import router from './router' import store from './store' -import vuetify from './plugins/vuetify'; -import './services/axios' -import firebase from 'firebase/app' -import 'firebase/firestore' -import { envConfig } from './config/environment' +import vuetify from './plugins/vuetify' -Vue.config.productionTip = false +// 1. Initialize the Bridge +Vue.use(VueCompositionAPI) -const firebaseConfig = envConfig.firebase; +// 2. Initialize Pinia +Vue.use(PiniaVuePlugin) +const pinia = createPinia() -// Initialize Firebase -firebase.initializeApp(firebaseConfig); +Vue.config.productionTip = false new Vue({ router, - store, + store, // Existing Vuex + pinia, // Your New Pinia vuetify, render: h => h(App) -}).$mount('#app') +}).$mount('#app') \ No newline at end of file diff --git a/src/models/FocusGroup.js b/src/models/FocusGroup.js new file mode 100644 index 0000000..c960283 --- /dev/null +++ b/src/models/FocusGroup.js @@ -0,0 +1,19 @@ +export const FocusGroupSchema = { + session: { + id: null, + title: "", + objective: "", + status: "setup", // setup, live, completed + startTime: null, + }, + participants: [], // { id, name, role, tags } + guide: [], // { id, prompt, duration, order } + observations: [] // { id, timestamp, topicId, participantId, content, type } +}; + +export const ObservationTypes = { + NEUTRAL: 'neutral', + CONSENSUS: 'consensus', + DIVERGENCE: 'divergence', + INSIGHT: 'insight' +}; \ No newline at end of file diff --git a/src/router/focusGroupRoutes.js b/src/router/focusGroupRoutes.js new file mode 100644 index 0000000..e55fb87 --- /dev/null +++ b/src/router/focusGroupRoutes.js @@ -0,0 +1,21 @@ +// 1. Static imports must be at the top level +import FGSetup from '@/views/focus-group/FGSetup.vue' + +export const focusGroupRoutes = [ + { + path: '/focus-group/setup', + name: 'FGSetup', + component: FGSetup // Use the imported component here + }, + { + path: '/focus-group/session', + name: 'FGSession', + // Lazy-loading for the other views + component: () => import('@/views/focus-group/FGSession.vue') + }, + { + path: '/focus-group/analysis', + name: 'FGAnalysis', + component: () => import('@/views/focus-group/FGAnalysis.vue') + } +]; \ No newline at end of file diff --git a/src/router/index.js b/src/router/index.js index 47e7ec7..8eb6d5c 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,8 +1,9 @@ import Vue from 'vue' import VueRouter from 'vue-router' // import store from '@/store/index' + +// Existing View Imports import LandingPage from '@/views/LandingPage.vue' -// import Login from '@/views/Login' import Dashboard from '@/views/Dashboard' import Calibration from '@/views/CalibrationCard' import CameraConfig from '@/views/CameraConfiguration' @@ -10,9 +11,13 @@ import DoubleCalibrationRecord from '@/views/DoubleCalibrationRecord' import PostCalibration from '@/views/PostCalibration' import CalibrationConfig from '@/views/CalibrationConfig' +// Focus Group Module Import +import { focusGroupRoutes } from './focusGroupRoutes' + Vue.use(VueRouter) const routes = [ + // --- Existing RUXAILAB Routes --- { path: '/', name: 'LandingPage', @@ -53,6 +58,15 @@ const routes = [ name: 'calibrationConfig', component: CalibrationConfig }, + + // --- Focus Group Module Routes --- + ...focusGroupRoutes, + + // --- Catch-all Redirect --- + { + path: '*', + redirect: '/' + } ] const router = new VueRouter({ @@ -61,13 +75,13 @@ const router = new VueRouter({ routes }) +// --- Navigation Guards --- // router.beforeResolve(async (to, from, next) => { // var user = store.state.auth.user // user = user ?? await store.dispatch('autoSignIn') // if ((to.path == '/login' || to.path == '/') && user) next('/dashboard') // else if ((to.path == '/dashboard') && !user) next('/login') - // next() // }) -export default router +export default router \ No newline at end of file diff --git a/src/services/focusGroupService.js b/src/services/focusGroupService.js new file mode 100644 index 0000000..84fc2ab --- /dev/null +++ b/src/services/focusGroupService.js @@ -0,0 +1,17 @@ +const STORAGE_KEY = 'ruxailab_fg_prototype'; + +export const focusGroupService = { + saveSession(data) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); + return true; + }, + + getSession() { + const data = localStorage.getItem(STORAGE_KEY); + return data ? JSON.parse(data) : null; + }, + + clearSession() { + localStorage.removeItem(STORAGE_KEY); + } +}; \ No newline at end of file diff --git a/src/store/modules/focusGroup.js b/src/store/modules/focusGroup.js new file mode 100644 index 0000000..358af37 --- /dev/null +++ b/src/store/modules/focusGroup.js @@ -0,0 +1,25 @@ +import { defineStore } from 'pinia'; // Recommended for Vue 3 + +export const useFocusGroupStore = defineStore('focusGroup', { + state: () => ({ + activeSession: null, + currentTopicId: null, + observations: [], + }), + actions: { + setTopic(id) { + this.currentTopicId = id; + }, + addObservation(note) { + const observation = { + id: Date.now(), + timestamp: new Date().toISOString(), + topicId: this.currentTopicId, + ...note + }; + this.observations.push(observation); + // Auto-save to local storage for PoW persistence + localStorage.setItem('temp_obs', JSON.stringify(this.observations)); + } + } +}); \ No newline at end of file diff --git a/src/views/focus-group/FGAnalysis.vue b/src/views/focus-group/FGAnalysis.vue new file mode 100644 index 0000000..416f0f8 --- /dev/null +++ b/src/views/focus-group/FGAnalysis.vue @@ -0,0 +1,198 @@ + + + + + \ No newline at end of file diff --git a/src/views/focus-group/FGSession.vue b/src/views/focus-group/FGSession.vue new file mode 100644 index 0000000..72b874c --- /dev/null +++ b/src/views/focus-group/FGSession.vue @@ -0,0 +1,270 @@ + + + + + \ No newline at end of file diff --git a/src/views/focus-group/FGSetup.vue b/src/views/focus-group/FGSetup.vue new file mode 100644 index 0000000..f434c35 --- /dev/null +++ b/src/views/focus-group/FGSetup.vue @@ -0,0 +1,292 @@ + + + + + \ No newline at end of file diff --git a/src/views/focus-group/TestPage.vue b/src/views/focus-group/TestPage.vue new file mode 100644 index 0000000..f94fffe --- /dev/null +++ b/src/views/focus-group/TestPage.vue @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/tests/unit/FocusGroup.spec.js b/tests/unit/FocusGroup.spec.js new file mode 100644 index 0000000..eb2c7b1 --- /dev/null +++ b/tests/unit/FocusGroup.spec.js @@ -0,0 +1,27 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useFocusGroupStore } from '@/store/modules/focusGroup'; +import { setActivePinia, createPinia } from 'pinia'; + +describe('Focus Group Logic Engine', () => { + beforeEach(() => { + setActivePinia(createPinia()); + }); + + it('correctly tags an observation with the active topic ID', () => { + const store = useFocusGroupStore(); + store.setTopic(5); // Simulate selecting the 5th topic + + store.addObservation({ content: 'Test Insight', type: 'insight' }); + + expect(store.observations[0].topicId).toBe(5); + expect(store.observations[0].type).toBe('insight'); + }); + + it('generates a valid ISO timestamp for each note', () => { + const store = useFocusGroupStore(); + store.addObservation({ content: 'Timer test', type: 'neutral' }); + + const timestamp = store.observations[0].timestamp; + expect(new Date(timestamp).getTime()).not.toBeNaN(); + }); +}); \ No newline at end of file