Files
archy/aiui/packages/app/src/composables/useSimilarContent.ts
T

111 lines
3.1 KiB
TypeScript

import { ref } from 'vue'
import type { FavoriteType } from '@/stores/favorites'
import { getApiKey } from '@/utils/key-vault'
import { apiFetch } from '@/utils/api-fetch'
const CACHE_KEY = 'aiui-similar-content'
const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000 // 7 days
interface SimilarItem {
title: string
type: FavoriteType
reason: string
}
interface CachedSuggestion {
itemId: string
suggestions: SimilarItem[]
cachedAt: number
}
function loadCache(): CachedSuggestion[] {
try {
const stored = localStorage.getItem(CACHE_KEY)
if (stored) return JSON.parse(stored)
} catch { /* ignore */ }
return []
}
function saveCache(cache: CachedSuggestion[]) {
localStorage.setItem(CACHE_KEY, JSON.stringify(cache))
}
export function useSimilarContent() {
const suggestions = ref<SimilarItem[]>([])
const isLoading = ref(false)
async function fetchSimilar(itemId: string, title: string, type: FavoriteType) {
// Check cache first
const cache = loadCache()
const now = Date.now()
const cached = cache.find(c => c.itemId === itemId && (now - c.cachedAt) < CACHE_DURATION)
if (cached) {
suggestions.value = cached.suggestions
return
}
isLoading.value = true
suggestions.value = []
try {
const apiKey = await getApiKey('claude')
if (!apiKey) {
isLoading.value = false
return
}
const typeLabel = type === 'tv' ? 'TV series' : type
const prompt = `List exactly 3 ${typeLabel}s similar to "${title}". For each, respond with ONLY a JSON array like: [{"title":"Name","reason":"one sentence why"}]. No other text.`
const base = import.meta.env.BASE_URL || '/'
const res = await apiFetch(`${base}api/claude/v1/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
body: JSON.stringify({
model: 'claude-haiku-4-5-20251001',
max_tokens: 300,
messages: [{ role: 'user', content: prompt }],
}),
})
if (!res.ok) {
isLoading.value = false
return
}
const data = await res.json()
const text = data.content?.[0]?.text ?? ''
// Extract JSON array from response
const match = text.match(/\[[\s\S]*\]/)
if (match) {
let parsed: { title: string; reason: string }[]
try {
parsed = JSON.parse(match[0]) as { title: string; reason: string }[]
} catch { return }
const items: SimilarItem[] = parsed.slice(0, 3).map(p => ({
title: p.title,
type,
reason: p.reason,
}))
suggestions.value = items
// Cache result
const updatedCache = cache.filter(c => c.itemId !== itemId)
updatedCache.push({ itemId, suggestions: items, cachedAt: now })
// Keep cache small
if (updatedCache.length > 100) updatedCache.splice(0, updatedCache.length - 100)
saveCache(updatedCache)
}
} catch { /* ignore errors */ } finally {
isLoading.value = false
}
}
return {
suggestions,
isLoading,
fetchSimilar,
}
}