initial commit with charts and assistant chat

This commit is contained in:
2026-03-02 00:08:19 -04:00
commit d907c5765e
1828 changed files with 50054 additions and 0 deletions

View 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
View 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;
}

View 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
View 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
View 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
View 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
View 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
View 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;