+
@@ -347,11 +542,11 @@ onMounted(() => {
--border-color: #30363d;
--error-color: #f85149;
--success-color: #238636;
- --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
- --card-shadow: 0 4px 6px rgba(0,0,0,0.1);
+ --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
+ --card-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
-[data-theme="hacker"] {
+[data-theme='hacker'] {
--bg-color: #002b36;
--card-bg: #073642;
--text-color: #859900;
@@ -361,7 +556,7 @@ onMounted(() => {
--border-color: #586e75;
--error-color: #dc322f;
--success-color: #859900;
- --font-family: "Courier New", Courier, monospace;
+ --font-family: 'Courier New', Courier, monospace;
}
body {
@@ -522,8 +717,12 @@ button:disabled {
}
@keyframes spin {
- from { transform: rotate(0deg); }
- to { transform: rotate(360deg); }
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
}
.icon-btn {
@@ -532,6 +731,15 @@ button:disabled {
border-radius: 4px;
}
+.badge {
+ background: rgba(255, 255, 255, 0.1);
+ padding: 2px 8px;
+ border-radius: 12px;
+ font-size: 0.8rem;
+ color: var(--text-color);
+ border: 1px solid var(--border-color);
+}
+
.activity-item {
display: flex;
justify-content: space-between;
@@ -551,7 +759,7 @@ button:disabled {
left: 0;
right: 0;
bottom: 0;
- background: rgba(0,0,0,0.8);
+ background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
diff --git a/frontend/src/__tests__/HelloWorld.spec.js b/frontend/src/__tests__/HelloWorld.spec.js
new file mode 100644
index 0000000..a4b1f48
--- /dev/null
+++ b/frontend/src/__tests__/HelloWorld.spec.js
@@ -0,0 +1,20 @@
+import { describe, it, expect } from 'vitest'
+import { mount } from '@vue/test-utils'
+import HelloWorld from '../components/HelloWorld.vue'
+
+describe('HelloWorld.vue', () => {
+ it('renders props.msg when passed', () => {
+ const msg = 'new message'
+ const wrapper = mount(HelloWorld, {
+ props: { msg }
+ })
+ expect(wrapper.text()).toContain(msg)
+ })
+
+ it('increments count when button is clicked', async () => {
+ const wrapper = mount(HelloWorld)
+ const button = wrapper.find('button')
+ await button.trigger('click')
+ expect(wrapper.text()).toContain('count is 1')
+ })
+})
diff --git a/frontend/src/components/HelloWorld.vue b/frontend/src/components/HelloWorld.vue
index 546ebbc..08c3da9 100644
--- a/frontend/src/components/HelloWorld.vue
+++ b/frontend/src/components/HelloWorld.vue
@@ -2,7 +2,7 @@
import { ref } from 'vue'
defineProps({
- msg: String,
+ msg: { type: String, default: '' }
})
const count = ref(0)
@@ -21,15 +21,12 @@ const count = ref(0)
Check out
- create-vue , the official Vue + Vite starter
+ create-vue , the
+ official Vue + Vite starter
Learn more about IDE Support for Vue in the
- Vue Docs Scaling up Guide .
diff --git a/frontend/src/components/WorkoutJsonEditor.vue b/frontend/src/components/WorkoutJsonEditor.vue
new file mode 100644
index 0000000..90a3866
--- /dev/null
+++ b/frontend/src/components/WorkoutJsonEditor.vue
@@ -0,0 +1,86 @@
+
+
+
+ Raw JSON Editor
+
+ Validate Schema
+
+
+
+
+
+
+
+
+
+ {{ validationResult.valid ? 'Valid Garmin Workout Schema' : 'Validation Errors Found' }}
+
+
+
+
+
+
+
diff --git a/frontend/src/components/WorkoutVisualEditor.vue b/frontend/src/components/WorkoutVisualEditor.vue
new file mode 100644
index 0000000..4e82635
--- /dev/null
+++ b/frontend/src/components/WorkoutVisualEditor.vue
@@ -0,0 +1,205 @@
+
+
+
+
Workout Metadata
+
+
+ Name
+
+
+
+ Sport Type
+
+ Running
+ Cycling
+ Swimming
+ Fitness Equipment
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatStepType(element) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Iterations:
+
+
+
+
+
+
+
+
+
+
+
+ Duration Type
+
+ Distance
+ Time
+ Cadence
+ Lap Button
+
+
+
+ Duration (Secs)
+
+
+
+
+
+
+
+
+
+
+
+ Add Step
+
+
+ Add Repeat
+
+
+
+
+
+
+
+
diff --git a/frontend/src/style.css b/frontend/src/style.css
index 022ff36..af61bdf 100644
--- a/frontend/src/style.css
+++ b/frontend/src/style.css
@@ -40,7 +40,9 @@ body {
border-radius: 12px;
padding: 1.5rem;
backdrop-filter: blur(8px);
- transition: transform 0.2s, box-shadow 0.2s;
+ transition:
+ transform 0.2s,
+ box-shadow 0.2s;
}
.card:hover {
@@ -73,4 +75,4 @@ h3 {
.activity-item:last-child {
border-bottom: none;
-}
\ No newline at end of file
+}
diff --git a/frontend/src/views/AnalyzeView.vue b/frontend/src/views/AnalyzeView.vue
index cc447ff..3e8754a 100644
--- a/frontend/src/views/AnalyzeView.vue
+++ b/frontend/src/views/AnalyzeView.vue
@@ -10,7 +10,14 @@ import {
LinearScale
} from 'chart.js'
import { Bar } from 'vue-chartjs'
-import { RotateCw, Activity, Loader2, Calendar, CheckCircle, AlertTriangle } from 'lucide-vue-next'
+import {
+ Activity,
+ Loader2,
+ CheckCircle,
+ AlertTriangle,
+ Send,
+ Bot
+} from 'lucide-vue-next'
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend)
@@ -35,7 +42,7 @@ const fetchData = async () => {
const data = await res.json()
chartData.value = data.weekly
} catch (error) {
- console.error("Failed to fetch stats", error)
+ console.error('Failed to fetch stats', error)
} finally {
loading.value = false
}
@@ -47,21 +54,59 @@ const runSmartSync = async () => {
const res = await fetch('http://localhost:8000/sync/smart', { method: 'POST' })
const data = await res.json()
if (data.success) {
- syncStatus.value = 'success'
- syncMessage.value = data.synced_count > 0 ? `Synced ${data.synced_count} new` : 'Up to date'
- await fetchData()
+ syncStatus.value = 'success'
+ syncMessage.value = data.synced_count > 0 ? `Synced ${data.synced_count} new` : 'Up to date'
+ await fetchData()
} else {
- syncStatus.value = 'warning'
- syncMessage.value = "Auth check failed"
+ syncStatus.value = 'warning'
+ syncMessage.value = 'Auth check failed'
}
- } catch (error) {
+ } catch (err) {
syncStatus.value = 'warning'
- syncMessage.value = "Sync error"
+ syncMessage.value = 'Sync error'
+ }
+}
+
+// AI Chat
+const chatInput = ref('')
+const chatLoading = ref(false)
+const chatHistory = ref([]) // Local UI history
+const chatContext = ref([]) // History for API context
+
+const sendMessage = async () => {
+ if (!chatInput.value.trim()) return
+
+ const userMsg = chatInput.value
+ chatHistory.value.push({ role: 'user', content: userMsg })
+ chatInput.value = ''
+ chatLoading.value = true
+
+ try {
+ const res = await fetch('http://localhost:8000/analyze/chat', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ message: userMsg,
+ history: chatContext.value
+ })
+ })
+ const data = await res.json()
+
+ const aiMsg = data.message
+ chatHistory.value.push({ role: 'model', content: aiMsg })
+
+ // Update context for next turn
+ chatContext.value.push({ role: 'user', content: userMsg })
+ chatContext.value.push({ role: 'model', content: aiMsg })
+ } catch (err) {
+ chatHistory.value.push({ role: 'model', content: 'Error connecting to AI Analyst.' })
+ } finally {
+ chatLoading.value = false
}
}
watch(timeHorizon, () => {
- fetchData()
+ fetchData()
})
onMounted(() => {
@@ -77,31 +122,31 @@ onMounted(() => {
Analyze Performance
Your fitness journey over time.