mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-09 23:59:40 +00:00
Initial commit
This commit is contained in:
@@ -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())
|
||||
}
|
||||
@@ -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> = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
}
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user