initial commit with charts and assistant chat
This commit is contained in:
11
web/src/__tests__/App.spec.ts
Normal file
11
web/src/__tests__/App.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
import { mount } from '@vue/test-utils'
|
||||
import App from '../App.vue'
|
||||
|
||||
describe('App', () => {
|
||||
it('mounts renders properly', () => {
|
||||
const wrapper = mount(App)
|
||||
expect(wrapper.text()).toContain('You did it!')
|
||||
})
|
||||
})
|
||||
28
web/src/assets/theme.css
Normal file
28
web/src/assets/theme.css
Normal file
@@ -0,0 +1,28 @@
|
||||
/* web/src/assets/theme.css */
|
||||
:root {
|
||||
--p-primary-color: #00d4aa; /* teal accent */
|
||||
--p-primary-contrast-color: #0a0e1a;
|
||||
--p-surface-0: #0a0e1a; /* deepest background */
|
||||
--p-surface-50: #0f1629;
|
||||
--p-surface-100: #161e35;
|
||||
--p-surface-200: #1e2a45;
|
||||
--p-surface-300: #263452;
|
||||
--p-surface-400: #34446a;
|
||||
--p-surface-700: #8892a4;
|
||||
--p-surface-800: #aab4c5;
|
||||
--p-surface-900: #cdd6e8;
|
||||
|
||||
/* Semantic trading colors */
|
||||
--color-bull: #26a69a;
|
||||
--color-bear: #ef5350;
|
||||
--color-neutral: #8892a4;
|
||||
}
|
||||
|
||||
html, body, #app {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh !important;
|
||||
width: 100vw !important;
|
||||
overflow: hidden;
|
||||
background-color: var(--p-surface-0) !important;
|
||||
}
|
||||
158
web/src/composables/useStateSync.ts
Normal file
158
web/src/composables/useStateSync.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import type { Store } from 'pinia';
|
||||
import * as jsonpatch from 'fast-json-patch';
|
||||
import type { BackendMessage, FrontendMessage, HelloMessage, PatchMessage } from '../types/sync';
|
||||
import { wsManager } from './useWebSocket';
|
||||
|
||||
export function useStateSync(stores: Record<string, Store>) {
|
||||
console.log('[StateSync] Initializing with stores:', Object.keys(stores));
|
||||
|
||||
// Load initial seqs from sessionStorage
|
||||
const getStoredSeqs = (): Record<string, number> => {
|
||||
const stored = sessionStorage.getItem('sync_seqs');
|
||||
return stored ? JSON.parse(stored) : {};
|
||||
};
|
||||
|
||||
const saveStoredSeqs = (seqs: Record<string, number>) => {
|
||||
sessionStorage.setItem('sync_seqs', JSON.stringify(seqs));
|
||||
};
|
||||
|
||||
const currentSeqs = getStoredSeqs();
|
||||
console.log('[StateSync] Loaded stored seqs:', currentSeqs);
|
||||
|
||||
// Track when we're applying backend patches to prevent circular updates
|
||||
const isApplyingBackendPatch: Record<string, boolean> = {};
|
||||
// Track previous state for each store to compute diffs
|
||||
const previousStates: Record<string, any> = {};
|
||||
|
||||
const sendJson = (msg: FrontendMessage) => {
|
||||
wsManager.send(msg);
|
||||
};
|
||||
|
||||
const handleMessage = (msg: BackendMessage) => {
|
||||
console.log('[StateSync] Received WebSocket message:', msg);
|
||||
console.log('[StateSync] Parsed message type:', msg.type);
|
||||
|
||||
if (msg.type === 'snapshot') {
|
||||
console.log('[StateSync] Processing snapshot for store:', msg.store);
|
||||
const store = stores[msg.store];
|
||||
if (store) {
|
||||
console.log('[StateSync] Applying snapshot state:', msg.state);
|
||||
isApplyingBackendPatch[msg.store] = true;
|
||||
store.$patch(msg.state);
|
||||
// Update previousState to stay in sync
|
||||
previousStates[msg.store] = JSON.parse(JSON.stringify(store.$state));
|
||||
isApplyingBackendPatch[msg.store] = false;
|
||||
currentSeqs[msg.store] = msg.seq;
|
||||
saveStoredSeqs(currentSeqs);
|
||||
console.log('[StateSync] Snapshot applied, new seq:', msg.seq);
|
||||
} else {
|
||||
console.warn('[StateSync] Store not found:', msg.store);
|
||||
}
|
||||
} else if (msg.type === 'patch') {
|
||||
console.log('[StateSync] Processing patch for store:', msg.store, 'seq:', msg.seq);
|
||||
const store = stores[msg.store];
|
||||
if (store) {
|
||||
// Check for sequence gaps
|
||||
const lastSeq = currentSeqs[msg.store] || 0;
|
||||
console.log('[StateSync] Current seq:', lastSeq, 'Received seq:', msg.seq);
|
||||
if (msg.seq !== lastSeq + 1) {
|
||||
console.warn(`[StateSync] Sequence gap detected for ${msg.store}: expected ${lastSeq + 1}, got ${msg.seq}. Requesting resync.`);
|
||||
sendHello();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[StateSync] Applying patch:', msg.patch);
|
||||
const currentState = JSON.parse(JSON.stringify(store.$state));
|
||||
console.log('[StateSync] Current state before patch:', currentState);
|
||||
const newState = jsonpatch.applyPatch(currentState, msg.patch, false, false).newDocument;
|
||||
console.log('[StateSync] New state after patch:', newState);
|
||||
isApplyingBackendPatch[msg.store] = true;
|
||||
store.$patch(newState);
|
||||
// Update previousState to stay in sync
|
||||
previousStates[msg.store] = JSON.parse(JSON.stringify(store.$state));
|
||||
isApplyingBackendPatch[msg.store] = false;
|
||||
currentSeqs[msg.store] = msg.seq;
|
||||
saveStoredSeqs(currentSeqs);
|
||||
console.log('[StateSync] Patch applied successfully, new seq:', msg.seq);
|
||||
} else {
|
||||
console.warn('[StateSync] Store not found:', msg.store);
|
||||
}
|
||||
} else {
|
||||
console.log('[StateSync] Ignoring message type:', msg.type);
|
||||
}
|
||||
};
|
||||
|
||||
const sendHello = () => {
|
||||
const hello: HelloMessage = {
|
||||
type: 'hello',
|
||||
seqs: currentSeqs
|
||||
};
|
||||
console.log('[StateSync] Sending hello with seqs:', currentSeqs);
|
||||
sendJson(hello);
|
||||
};
|
||||
|
||||
const sendPatch = (storeName: string, patch: any[]) => {
|
||||
const seq = currentSeqs[storeName] || 0;
|
||||
const msg: PatchMessage = {
|
||||
type: 'patch',
|
||||
store: storeName,
|
||||
seq: seq,
|
||||
patch: patch
|
||||
};
|
||||
sendJson(msg);
|
||||
};
|
||||
|
||||
// Connect to WebSocket and register handler
|
||||
const ws = wsManager.connect();
|
||||
wsManager.addHandler(handleMessage);
|
||||
console.log('[StateSync] WebSocket ready state:', ws.readyState);
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
console.log('[StateSync] WebSocket already open, sending hello');
|
||||
sendHello();
|
||||
} else {
|
||||
console.log('[StateSync] WebSocket not open, waiting for open event');
|
||||
ws.addEventListener('open', sendHello, { once: true });
|
||||
}
|
||||
|
||||
// Set up watchers for each store to send patches on changes
|
||||
const unwatchFunctions: (() => void)[] = [];
|
||||
|
||||
for (const [storeName, store] of Object.entries(stores)) {
|
||||
previousStates[storeName] = JSON.parse(JSON.stringify(store.$state));
|
||||
isApplyingBackendPatch[storeName] = false;
|
||||
|
||||
const unwatch = store.$subscribe((mutation, state) => {
|
||||
// Skip if we're currently applying a patch from the backend
|
||||
if (isApplyingBackendPatch[storeName]) {
|
||||
console.log(`[StateSync] Skipping patch send for "${storeName}" - applying backend update`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[StateSync] Store "${storeName}" changed, mutation type:`, mutation.type);
|
||||
console.log('[StateSync] Previous state:', previousStates[storeName]);
|
||||
console.log('[StateSync] New state:', state);
|
||||
|
||||
const currentState = JSON.parse(JSON.stringify(state));
|
||||
const patch = jsonpatch.compare(previousStates[storeName], currentState);
|
||||
|
||||
if (patch.length > 0) {
|
||||
console.log(`[StateSync] Sending ${patch.length} patch operations for "${storeName}":`, patch);
|
||||
sendPatch(storeName, patch);
|
||||
previousStates[storeName] = currentState;
|
||||
} else {
|
||||
console.log(`[StateSync] No changes detected for "${storeName}"`);
|
||||
}
|
||||
}, { detached: true });
|
||||
|
||||
unwatchFunctions.push(unwatch);
|
||||
}
|
||||
|
||||
return {
|
||||
sendPatch,
|
||||
cleanup: () => {
|
||||
wsManager.removeHandler(handleMessage);
|
||||
unwatchFunctions.forEach(unwatch => unwatch());
|
||||
}
|
||||
};
|
||||
}
|
||||
27
web/src/main.ts
Normal file
27
web/src/main.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import ToastService from 'primevue/toastservice'
|
||||
import 'primeicons/primeicons.css'
|
||||
import './assets/theme.css'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(PrimeVue, {
|
||||
theme: {
|
||||
preset: Aura,
|
||||
options: {
|
||||
darkModeSelector: '.dark', // you control when dark applies
|
||||
cssLayer: false
|
||||
}
|
||||
}
|
||||
})
|
||||
app.use(ToastService) // for agent ui:notification commands
|
||||
|
||||
app.mount('#app')
|
||||
8
web/src/router/index.ts
Normal file
8
web/src/router/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [],
|
||||
})
|
||||
|
||||
export default router
|
||||
20
web/src/stores/chart.ts
Normal file
20
web/src/stores/chart.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export interface ChartState {
|
||||
symbol: string
|
||||
start_time: number | null
|
||||
end_time: number | null
|
||||
interval: string
|
||||
}
|
||||
|
||||
export const useChartStore = defineStore('ChartStore', () => {
|
||||
const chart_state = ref<ChartState>({
|
||||
symbol: 'BINANCE:BTC/USDT',
|
||||
start_time: null,
|
||||
end_time: null,
|
||||
interval: '15'
|
||||
})
|
||||
|
||||
return { chart_state }
|
||||
})
|
||||
12
web/src/stores/counter.ts
Normal file
12
web/src/stores/counter.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})
|
||||
21
web/src/types/sync.ts
Normal file
21
web/src/types/sync.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export interface SnapshotMessage {
|
||||
type: 'snapshot';
|
||||
store: string;
|
||||
seq: number;
|
||||
state: any;
|
||||
}
|
||||
|
||||
export interface PatchMessage {
|
||||
type: 'patch';
|
||||
store: string;
|
||||
seq: number;
|
||||
patch: any[];
|
||||
}
|
||||
|
||||
export interface HelloMessage {
|
||||
type: 'hello';
|
||||
seqs: Record<string, number>;
|
||||
}
|
||||
|
||||
export type BackendMessage = SnapshotMessage | PatchMessage;
|
||||
export type FrontendMessage = HelloMessage | PatchMessage;
|
||||
Reference in New Issue
Block a user