Initial commit

This commit is contained in:
2025-12-14 21:59:59 +00:00
commit feb7f251b4
125 changed files with 21727 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
export { useSSE } from './useSSE'
export { useStats } from './useStats'
export { useTimeline } from './useTimeline'
+79
View File
@@ -0,0 +1,79 @@
import { ref, onMounted, onUnmounted } from 'vue'
import type { SSEEvent } from '@/types'
export function useSSE() {
const isConnected = ref(false)
const isProcessing = ref(false)
const queueDepth = ref(0)
const lastEvent = ref<SSEEvent | null>(null)
let eventSource: EventSource | null = null
let reconnectTimeout: number | null = null
const connect = () => {
if (eventSource) {
eventSource.close()
}
eventSource = new EventSource('/api/events')
eventSource.onopen = () => {
isConnected.value = true
console.log('[SSE] Connected')
}
eventSource.onmessage = (event) => {
try {
const data: SSEEvent = JSON.parse(event.data)
lastEvent.value = data
if (data.type === 'processing_status') {
isProcessing.value = data.isProcessing ?? false
queueDepth.value = data.queueDepth ?? 0
}
} catch (err) {
console.error('[SSE] Parse error:', err)
}
}
eventSource.onerror = () => {
isConnected.value = false
eventSource?.close()
eventSource = null
// Reconnect after 5 seconds
reconnectTimeout = window.setTimeout(() => {
console.log('[SSE] Reconnecting...')
connect()
}, 5000)
}
}
const disconnect = () => {
if (reconnectTimeout) {
clearTimeout(reconnectTimeout)
reconnectTimeout = null
}
if (eventSource) {
eventSource.close()
eventSource = null
}
isConnected.value = false
}
onMounted(() => {
connect()
})
onUnmounted(() => {
disconnect()
})
return {
isConnected,
isProcessing,
queueDepth,
lastEvent,
reconnect: connect
}
}
+53
View File
@@ -0,0 +1,53 @@
import { ref, onMounted, onUnmounted } from 'vue'
import type { Stats } from '@/types'
import { fetchStats } from '@/utils/api'
export function useStats(pollInterval: number = 5000) {
const stats = ref<Stats | null>(null)
const loading = ref(false)
const error = ref<string | null>(null)
let intervalId: number | null = null
const refresh = async () => {
loading.value = true
error.value = null
try {
stats.value = await fetchStats()
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to fetch stats'
console.error('[Stats] Error:', err)
} finally {
loading.value = false
}
}
const startPolling = () => {
if (intervalId) return
intervalId = window.setInterval(refresh, pollInterval)
}
const stopPolling = () => {
if (intervalId) {
clearInterval(intervalId)
intervalId = null
}
}
onMounted(() => {
refresh()
startPolling()
})
onUnmounted(() => {
stopPolling()
})
return {
stats,
loading,
error,
refresh
}
}
+141
View File
@@ -0,0 +1,141 @@
import { ref, computed, onMounted, watch } from 'vue'
import type { FeedItem, FilterType, ObservationType, ConceptType } from '@/types'
import { fetchObservations, fetchPrompts, fetchSummaries, combineTimeline } from '@/utils/api'
import { useSSE } from './useSSE'
export function useTimeline() {
const observations = ref<FeedItem[]>([])
const prompts = ref<FeedItem[]>([])
const summaries = ref<FeedItem[]>([])
const allItems = ref<FeedItem[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
// Filters
const currentFilter = ref<FilterType>('all')
const currentProject = ref<string | null>(null)
const currentTypeFilter = ref<ObservationType | null>(null)
const currentConceptFilter = ref<ConceptType | null>(null)
// SSE for real-time updates
const { lastEvent } = useSSE()
// Counts (reflect data fetched for current project/all)
const observationCount = computed(() => observations.value.length)
const promptCount = computed(() => prompts.value.length)
const summaryCount = computed(() => summaries.value.length)
// Filtered items (further filter by type/concept within fetched data)
const filteredItems = computed(() => {
let items = [...allItems.value]
// Filter by main type
if (currentFilter.value === 'observations') {
items = items.filter(item => item.itemType === 'observation')
} else if (currentFilter.value === 'summaries') {
items = items.filter(item => item.itemType === 'summary')
} else if (currentFilter.value === 'prompts') {
items = items.filter(item => item.itemType === 'prompt')
}
// Filter by observation type
if (currentTypeFilter.value) {
items = items.filter(item => {
if (item.itemType !== 'observation') return false
return item.type === currentTypeFilter.value
})
}
// Filter by concept
if (currentConceptFilter.value) {
items = items.filter(item => {
if (item.itemType !== 'observation') return false
const concepts = item.concepts || []
return concepts.includes(currentConceptFilter.value!)
})
}
return items
})
const refresh = async () => {
loading.value = true
error.value = null
try {
// Use different limits based on project selection:
// All projects: 50 of each type
// Specific project: 100 of each type
const project = currentProject.value || undefined
const limit = project ? 100 : 50
const [obs, prm, sum] = await Promise.all([
fetchObservations(limit, project),
fetchPrompts(limit, project),
fetchSummaries(limit, project)
])
// Combine into timeline
allItems.value = combineTimeline(obs, prm, sum)
// Update individual arrays for counting (data already filtered by project from API)
observations.value = allItems.value.filter(i => i.itemType === 'observation')
prompts.value = allItems.value.filter(i => i.itemType === 'prompt')
summaries.value = allItems.value.filter(i => i.itemType === 'summary')
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to fetch timeline'
console.error('[Timeline] Error:', err)
} finally {
loading.value = false
}
}
const setFilter = (filter: FilterType) => {
currentFilter.value = filter
}
const setProject = (project: string | null) => {
currentProject.value = project
// Refresh when project changes to fetch correct data
refresh()
}
const setTypeFilter = (type: ObservationType | null) => {
currentTypeFilter.value = type
}
const setConceptFilter = (concept: ConceptType | null) => {
currentConceptFilter.value = concept
}
// Watch for SSE events and refresh
watch(lastEvent, (event) => {
if (event && (event.type === 'observation' || event.type === 'prompt' || event.type === 'summary')) {
refresh()
}
})
onMounted(() => {
refresh()
})
return {
allItems,
filteredItems,
loading,
error,
observationCount,
promptCount,
summaryCount,
currentFilter,
currentProject,
currentTypeFilter,
currentConceptFilter,
refresh,
setFilter,
setProject,
setTypeFilter,
setConceptFilter
}
}