144 lines
4.2 KiB
TypeScript
144 lines
4.2 KiB
TypeScript
/**
|
|
* Chart Suggestion Service
|
|
* Frontend service for calling the AI chart suggestions API
|
|
*/
|
|
|
|
export interface ChartSuggestion {
|
|
type: 'bar' | 'horizontal-bar' | 'line' | 'area' | 'pie' | 'radar' | 'funnel' | 'gauge'
|
|
title: string
|
|
data: { label: string; value: number }[]
|
|
description: string
|
|
rationale?: string
|
|
}
|
|
|
|
export interface SuggestChartsResponse {
|
|
suggestions: ChartSuggestion[]
|
|
analyzedText: string
|
|
detectedData: string
|
|
hasData: boolean
|
|
error?: string
|
|
quotaExceeded?: boolean
|
|
}
|
|
|
|
export interface SuggestChartsRequest {
|
|
content: string
|
|
selection?: string | null
|
|
noteId?: string
|
|
}
|
|
|
|
// Simple in-memory cache with 5-minute TTL
|
|
const CACHE_TTL = 5 * 60 * 1000 // 5 minutes
|
|
const cache = new Map<string, { data: SuggestChartsResponse; timestamp: number }>()
|
|
|
|
function getCacheKey(content: string, selection: string | null | undefined): string {
|
|
// Use content hash + selection as cache key
|
|
const textToHash = selection ? content + ':::' + selection : content
|
|
// Simple hash function
|
|
let hash = 0
|
|
for (let i = 0; i < textToHash.length; i++) {
|
|
hash = ((hash << 5) - hash) + textToHash.charCodeAt(i)
|
|
hash = hash & hash // Convert to 32bit integer
|
|
}
|
|
return String(hash)
|
|
}
|
|
|
|
function getCached(key: string): SuggestChartsResponse | null {
|
|
const entry = cache.get(key)
|
|
if (!entry) return null
|
|
if (Date.now() - entry.timestamp > CACHE_TTL) {
|
|
cache.delete(key)
|
|
return null
|
|
}
|
|
console.log('[chart-suggestion] Cache hit for key:', key)
|
|
return entry.data
|
|
}
|
|
|
|
function setCached(key: string, data: SuggestChartsResponse): void {
|
|
cache.set(key, { data, timestamp: Date.now() })
|
|
// Limit cache size to 50 entries
|
|
if (cache.size > 50) {
|
|
const firstKey = cache.keys().next().value
|
|
cache.delete(firstKey)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Call the AI chart suggestions API
|
|
* @param request - The request parameters
|
|
* @returns Promise with the chart suggestions
|
|
*/
|
|
export async function suggestCharts(request: SuggestChartsRequest): Promise<SuggestChartsResponse> {
|
|
// Check cache first
|
|
const cacheKey = getCacheKey(request.content || '', request.selection)
|
|
console.log('[suggestCharts] Cache key:', cacheKey, 'contentLen:', request.content?.length, 'selectionLen:', request.selection?.length)
|
|
|
|
const cached = getCached(cacheKey)
|
|
if (cached) {
|
|
console.log('[suggestCharts] CACHE HIT - returning cached data')
|
|
return cached
|
|
}
|
|
|
|
console.log('[suggestCharts] CACHE MISS - calling API')
|
|
|
|
try {
|
|
const response = await fetch('/api/ai/suggest-charts', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(request),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
const isQuotaError = response.status === 402 || errorData.error === 'QUOTA_EXCEEDED'
|
|
return {
|
|
suggestions: [],
|
|
analyzedText: '',
|
|
detectedData: '',
|
|
hasData: false,
|
|
error: isQuotaError
|
|
? 'AI quota exceeded. Please upgrade your plan to continue using AI features.'
|
|
: (errorData.error || `HTTP ${response.status}`),
|
|
quotaExceeded: isQuotaError,
|
|
}
|
|
}
|
|
|
|
const data = await response.json() as SuggestChartsResponse
|
|
|
|
// Cache successful responses
|
|
if (!data.error && !data.quotaExceeded) {
|
|
setCached(cacheKey, data)
|
|
console.log('[suggestCharts] Cached response for key:', cacheKey, 'cache size now:', cache.size)
|
|
} else {
|
|
console.log('[suggestCharts] NOT caching (has error or quota exceeded)')
|
|
}
|
|
|
|
return data
|
|
} catch (error) {
|
|
console.error('[suggestCharts] Network error:', error)
|
|
return {
|
|
suggestions: [],
|
|
analyzedText: '',
|
|
detectedData: '',
|
|
hasData: false,
|
|
error: error instanceof Error ? error.message : 'Network error',
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert a chart suggestion to raw chart format (no markdown ticks)
|
|
* This is the content that goes inside <code class="language-chart">
|
|
* @param suggestion - The chart suggestion to convert
|
|
* @returns Raw chart content string
|
|
*/
|
|
export function chartSuggestionToMarkdown(suggestion: ChartSuggestion): string {
|
|
const lines = [
|
|
suggestion.type,
|
|
suggestion.title,
|
|
...suggestion.data.map(d => `${d.label}: ${d.value}`),
|
|
]
|
|
return lines.join('\n')
|
|
}
|