Initial commit

This commit is contained in:
2025-12-14 21:59:59 +00:00
commit 9c2a1a795a
126 changed files with 21728 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
import type { Observation, UserPrompt, SessionSummary, Stats, FeedItem, ObservationFeedItem, PromptFeedItem, SummaryFeedItem } from '@/types'
const API_BASE = '/api'
async function fetchJson<T>(url: string): Promise<T> {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return response.json()
}
export async function fetchObservations(limit: number = 100, project?: string): Promise<Observation[]> {
const params = new URLSearchParams({ limit: String(limit) })
if (project) params.append('project', project)
return fetchJson<Observation[]>(`${API_BASE}/observations?${params}`)
}
export async function fetchPrompts(limit: number = 100, project?: string): Promise<UserPrompt[]> {
const params = new URLSearchParams({ limit: String(limit) })
if (project) params.append('project', project)
return fetchJson<UserPrompt[]>(`${API_BASE}/prompts?${params}`)
}
export async function fetchSummaries(limit: number = 50, project?: string): Promise<SessionSummary[]> {
const params = new URLSearchParams({ limit: String(limit) })
if (project) params.append('project', project)
return fetchJson<SessionSummary[]>(`${API_BASE}/summaries?${params}`)
}
export async function fetchStats(): Promise<Stats> {
return fetchJson<Stats>(`${API_BASE}/stats`)
}
export async function fetchProjects(): Promise<string[]> {
return fetchJson<string[]>(`${API_BASE}/projects`)
}
/**
* Combine and sort all feed items by timestamp
*/
export function combineTimeline(
observations: Observation[],
prompts: UserPrompt[],
summaries: SessionSummary[]
): FeedItem[] {
const obsItems: ObservationFeedItem[] = observations.map(o => ({
...o,
itemType: 'observation' as const,
timestamp: new Date(o.created_at)
}))
const promptItems: PromptFeedItem[] = prompts.map(p => ({
...p,
itemType: 'prompt' as const,
timestamp: new Date(p.created_at)
}))
const summaryItems: SummaryFeedItem[] = summaries.map(s => ({
...s,
itemType: 'summary' as const,
timestamp: new Date(s.created_at)
}))
return [...obsItems, ...promptItems, ...summaryItems]
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
}
+87
View File
@@ -0,0 +1,87 @@
/**
* Format a timestamp as relative time (e.g., "5m ago", "2h ago")
*/
export function formatRelativeTime(dateOrEpoch: string | number): string {
const timestamp = typeof dateOrEpoch === 'number' ? dateOrEpoch : new Date(dateOrEpoch).getTime()
const now = Date.now()
const diff = now - timestamp
const seconds = Math.floor(diff / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)
if (seconds < 60) return 'just now'
if (minutes < 60) return `${minutes}m ago`
if (hours < 24) return `${hours}h ago`
if (days < 7) return `${days}d ago`
return new Date(timestamp).toLocaleDateString()
}
/**
* Format uptime duration string
*/
export function formatUptime(uptimeStr: string): string {
// Parse Go duration string (e.g., "1h30m45.123456789s")
const match = uptimeStr.match(/(?:(\d+)h)?(?:(\d+)m)?(?:(\d+(?:\.\d+)?)s)?/)
if (!match) return uptimeStr
const hours = parseInt(match[1] || '0', 10)
const minutes = parseInt(match[2] || '0', 10)
const seconds = Math.floor(parseFloat(match[3] || '0'))
if (hours > 0) {
return `${hours}h ${minutes}m`
}
if (minutes > 0) {
return `${minutes}m ${seconds}s`
}
return `${seconds}s`
}
/**
* Truncate text to a maximum length with ellipsis
*/
export function truncate(text: string, maxLength: number): string {
if (text.length <= maxLength) return text
return text.slice(0, maxLength - 3) + '...'
}
/**
* Escape HTML entities
*/
export function escapeHtml(text: string): string {
const map: Record<string, string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
}
return text.replace(/[&<>"']/g, m => map[m])
}
/**
* Parse JSON safely with fallback
*/
export function parseJsonSafe<T>(value: unknown, fallback: T): T {
if (Array.isArray(value)) return value as T
if (typeof value === 'string') {
try {
return JSON.parse(value) as T
} catch {
return fallback
}
}
return fallback
}
/**
* Get string value safely from potentially nullable fields
*/
export function getString(value: unknown): string {
if (typeof value === 'string') return value
if (value === null || value === undefined) return ''
return String(value)
}