diff --git a/apps/memoro/apps/server/src/routes/credits.ts b/apps/memoro/apps/server/src/routes/credits.ts index cf2e89dee..7ef986613 100644 --- a/apps/memoro/apps/server/src/routes/credits.ts +++ b/apps/memoro/apps/server/src/routes/credits.ts @@ -4,6 +4,7 @@ import { Hono } from 'hono'; import { validateCredits, consumeCredits, COSTS } from '../lib/credits'; +import { getBalance } from '@manacore/shared-hono'; export const creditRoutes = new Hono(); @@ -12,6 +13,18 @@ creditRoutes.get('/pricing', (c) => { return c.json({ costs: COSTS }); }); +// GET /balance — authenticated, returns user's credit balance +creditRoutes.get('/balance', async (c) => { + const userId = c.get('userId') as string; + try { + const balance = await getBalance(userId); + return c.json({ credits: balance.balance, totalEarned: balance.totalEarned, totalSpent: balance.totalSpent }); + } catch (err) { + console.error('[credits] Balance error:', err); + return c.json({ error: 'Failed to fetch balance' }, 500); + } +}); + // POST /check — validate credits (requires auth via parent router) creditRoutes.post('/check', async (c) => { const userId = c.get('userId') as string; diff --git a/apps/memoro/apps/web/src/lib/services/audioUploadService.ts b/apps/memoro/apps/web/src/lib/services/audioUploadService.ts index 29205440b..d77e64a57 100644 --- a/apps/memoro/apps/web/src/lib/services/audioUploadService.ts +++ b/apps/memoro/apps/web/src/lib/services/audioUploadService.ts @@ -125,7 +125,7 @@ export async function uploadAndProcessAudio({ blueprintId, recordingLanguages, enableDiarization, - appToken, + accessToken: appToken, mediaType, }); diff --git a/apps/memoro/apps/web/src/lib/services/creditService.ts b/apps/memoro/apps/web/src/lib/services/creditService.ts index 7089baceb..61e021054 100644 --- a/apps/memoro/apps/web/src/lib/services/creditService.ts +++ b/apps/memoro/apps/web/src/lib/services/creditService.ts @@ -1,11 +1,12 @@ /** * Credit Service for Memoro Web - * Handles credit operations and pricing - * - * Pattern adapted from memoro_app/features/credits/creditService.ts + * Handles credit operations and pricing via memoro-server (Hono/Bun). */ import { env } from '$lib/config/env'; +import { authStore } from '$lib/stores/auth.svelte'; + +const SERVER_URL = () => env.server.memoroUrl.replace(/\/$/, ''); export interface CreditCheckResponse { hasEnoughCredits: boolean; @@ -16,347 +17,156 @@ export interface CreditCheckResponse { estimatedCostPerHour?: number; } -export interface CreditConsumptionResponse { - success: boolean; - message: string; - creditsConsumed: number; - creditType: 'user' | 'space'; - durationMinutes?: number; -} - -export interface OperationCreditResponse { - success: boolean; - message: string; - creditsConsumed: number; - creditType: 'user' | 'space'; - operation: string; -} - export interface PricingResponse { - operationCosts: { - TRANSCRIPTION_PER_HOUR: number; + costs: { + TRANSCRIPTION_PER_MINUTE: number; HEADLINE_GENERATION: number; MEMORY_CREATION: number; BLUEPRINT_PROCESSING: number; QUESTION_MEMO: number; NEW_MEMORY: number; MEMO_COMBINE: number; - MEMO_SHARING: number; - SPACE_OPERATION: number; + MEETING_RECORDING_PER_MINUTE: number; }; - transcriptionPerHour: number; - lastUpdated: string; } type OperationType = | 'HEADLINE_GENERATION' | 'MEMORY_CREATION' | 'BLUEPRINT_PROCESSING' - | 'MEMO_SHARING' - | 'SPACE_OPERATION' | 'QUESTION_MEMO' | 'NEW_MEMORY' | 'MEMO_COMBINE'; +const FALLBACK_COSTS: Record = { + HEADLINE_GENERATION: 10, + MEMORY_CREATION: 10, + BLUEPRINT_PROCESSING: 5, + QUESTION_MEMO: 5, + NEW_MEMORY: 5, + MEMO_COMBINE: 5, +}; + class CreditService { - private readonly memoroServiceUrl: string; - private readonly manaServiceUrl: string; private creditUpdateCallbacks: ((creditsConsumed: number) => void)[] = []; private cachedPricing: PricingResponse | null = null; - private pricingLastFetched: number = 0; - private readonly PRICING_CACHE_DURATION = 30 * 60 * 1000; // 30 minutes + private pricingLastFetched = 0; + private readonly PRICING_CACHE_DURATION = 30 * 60 * 1000; - constructor() { - // Use memoro service URL for all endpoints (including auth proxy) - this.memoroServiceUrl = env.middleware.memoroUrl.replace(/\/$/, ''); - // manaServiceUrl now points to memoro service (auth proxy handles routing) - this.manaServiceUrl = this.memoroServiceUrl; - } - - /** - * Initialize the credit service by preloading pricing - * Call this during app startup - */ async initialize(): Promise { try { await this.getPricing(); - console.log('CreditService initialized with backend pricing'); } catch (error) { console.warn('CreditService initialization failed, using fallback pricing:', error); } } - /** - * Register a callback to be notified when credits are consumed - */ onCreditUpdate(callback: (creditsConsumed: number) => void): () => void { this.creditUpdateCallbacks.push(callback); - - // Return unsubscribe function return () => { - const index = this.creditUpdateCallbacks.indexOf(callback); - if (index > -1) { - this.creditUpdateCallbacks.splice(index, 1); - } + const i = this.creditUpdateCallbacks.indexOf(callback); + if (i > -1) this.creditUpdateCallbacks.splice(i, 1); }; } - /** - * Notify all registered callbacks about credit consumption - */ - private notifyCreditUpdate(creditsConsumed: number): void { - this.creditUpdateCallbacks.forEach((callback) => { - try { - callback(creditsConsumed); - } catch (error) { - console.error('Error in credit update callback:', error); - } + triggerCreditUpdate(creditsConsumed: number): void { + this.creditUpdateCallbacks.forEach((cb) => { + try { cb(creditsConsumed); } catch {} }); } - /** - * Public method to manually trigger credit update notifications - */ - triggerCreditUpdate(creditsConsumed: number): void { - this.notifyCreditUpdate(creditsConsumed); - } - - /** - * Fetch pricing information from backend with caching - */ async getPricing(): Promise { const now = Date.now(); - - // Return cached pricing if still valid if (this.cachedPricing && now - this.pricingLastFetched < this.PRICING_CACHE_DURATION) { return this.cachedPricing; } try { - const response = await fetch(`${this.memoroServiceUrl}/memoro/credits/pricing`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - + const response = await fetch(`${SERVER_URL()}/api/v1/credits/pricing`); + if (!response.ok) throw new Error(`HTTP ${response.status}`); const pricing = await response.json(); this.cachedPricing = pricing; this.pricingLastFetched = now; - return pricing; } catch (error) { console.error('Error fetching pricing:', error); - - // Fallback to cached pricing if available - if (this.cachedPricing) { - return this.cachedPricing; - } - - // Ultimate fallback + if (this.cachedPricing) return this.cachedPricing; return { - operationCosts: { - TRANSCRIPTION_PER_HOUR: 120, + costs: { + TRANSCRIPTION_PER_MINUTE: 2, HEADLINE_GENERATION: 10, MEMORY_CREATION: 10, BLUEPRINT_PROCESSING: 5, QUESTION_MEMO: 5, NEW_MEMORY: 5, MEMO_COMBINE: 5, - MEMO_SHARING: 1, - SPACE_OPERATION: 2, + MEETING_RECORDING_PER_MINUTE: 2, }, - transcriptionPerHour: 120, - lastUpdated: new Date().toISOString(), }; } } - /** - * Get user credits directly from mana-core-middleware - */ - async getUserCredits(appToken: string): Promise<{ credits: number } | null> { + async getUserCredits(token: string): Promise<{ credits: number } | null> { try { - console.log( - '[CreditService] Fetching user credits from:', - `${this.manaServiceUrl}/auth/credits` - ); - - if (!appToken) { - console.error('[CreditService] No authentication token available for credits fetch'); - throw new Error('No authentication token available'); - } - - const response = await fetch(`${this.manaServiceUrl}/auth/credits`, { - method: 'GET', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, + if (!token) throw new Error('No authentication token available'); + const response = await fetch(`${SERVER_URL()}/api/v1/credits/balance`, { + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, }); - - console.log('[CreditService] Credits response status:', response.status); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - console.error('[CreditService] Credits fetch error:', errorData); - throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`); - } - - const data = await response.json(); - console.log('[CreditService] Credits data received:', data); - - // Handle wrapped response structure from backend - if (data.data && typeof data.data.credits === 'number') { - return { credits: data.data.credits }; - } - - // Fallback to direct structure if already in correct format - return data; + if (!response.ok) return null; + return await response.json(); } catch (error) { console.error('[CreditService] Error fetching user credits:', error); return null; } } - /** - * Get estimated cost for operations using backend pricing - */ async getOperationCost(operation: OperationType): Promise { try { const pricing = await this.getPricing(); - return pricing.operationCosts[operation]; - } catch (error) { - console.error('Error getting operation cost:', error); - // Fallback to hardcoded costs - const fallbackCosts: Record = { - HEADLINE_GENERATION: 10, - MEMORY_CREATION: 10, - BLUEPRINT_PROCESSING: 5, - MEMO_SHARING: 1, - SPACE_OPERATION: 2, - QUESTION_MEMO: 5, - NEW_MEMORY: 5, - MEMO_COMBINE: 5, - }; - return fallbackCosts[operation]; + return pricing.costs[operation] ?? FALLBACK_COSTS[operation]; + } catch { + return FALLBACK_COSTS[operation]; } } - /** - * Calculate cost for memo combination based on number of memos - */ - async calculateMemoCombineCost(memoCount: number): Promise { - const costPerMemo = await this.getOperationCost('MEMO_COMBINE'); - return memoCount * costPerMemo; - } - - /** - * Synchronous version for immediate UI display (uses cached values) - */ getOperationCostSync(operation: OperationType): number { - if (this.cachedPricing) { - return this.cachedPricing.operationCosts[operation]; - } + if (this.cachedPricing) return this.cachedPricing.costs[operation] ?? FALLBACK_COSTS[operation]; + return FALLBACK_COSTS[operation]; + } - // Fallback to hardcoded costs if no cache - const fallbackCosts: Record = { - HEADLINE_GENERATION: 10, - MEMORY_CREATION: 10, - BLUEPRINT_PROCESSING: 5, - MEMO_SHARING: 1, - SPACE_OPERATION: 2, - QUESTION_MEMO: 5, - NEW_MEMORY: 5, - MEMO_COMBINE: 5, - }; - return fallbackCosts[operation]; + async calculateMemoCombineCost(memoCount: number): Promise { + return memoCount * (await this.getOperationCost('MEMO_COMBINE')); } calculateMemoCombineCostSync(memoCount: number): number { return memoCount * this.getOperationCostSync('MEMO_COMBINE'); } - /** - * Retry transcription for a failed memo using the reprocess-memo endpoint - */ - async retryTranscription( - memoId: string, - appToken: string - ): Promise<{ success: boolean; message: string }> { - try { - if (!appToken) { - throw new Error('No authentication token available'); - } - - const response = await fetch(`${this.memoroServiceUrl}/memoro/reprocess-memo`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${appToken}`, - }, - body: JSON.stringify({ memoId }), - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`); - } - - const result = await response.json(); - - return { - success: true, - message: result.message || 'Memo reprocessing started successfully', - }; - } catch (error) { - console.error('Error reprocessing memo:', error); - throw error; + async retryTranscription(memoId: string, token: string): Promise<{ success: boolean; message: string }> { + if (!token) throw new Error('No authentication token available'); + const response = await fetch(`${SERVER_URL()}/api/v1/memos/${memoId}/retry-transcription`, { + method: 'POST', + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, + }); + if (!response.ok) { + const d = await response.json().catch(() => ({})); + throw new Error(d.error || `HTTP ${response.status}`); } + return { success: true, message: 'Memo reprocessing started successfully' }; } - /** - * Retry headline generation for a failed memo - */ - async retryHeadline( - memoId: string, - appToken: string - ): Promise<{ success: boolean; message: string }> { - try { - if (!appToken) { - throw new Error('No authentication token available'); - } - - const response = await fetch(`${this.memoroServiceUrl}/memoro/retry-headline`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${appToken}`, - }, - body: JSON.stringify({ memoId }), - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`); - } - - const result = await response.json(); - - return { - success: true, - message: result.message || 'Headline generation retry initiated successfully', - }; - } catch (error) { - console.error('Error retrying headline generation:', error); - throw error; + async retryHeadline(memoId: string, token: string): Promise<{ success: boolean; message: string }> { + if (!token) throw new Error('No authentication token available'); + const response = await fetch(`${SERVER_URL()}/api/v1/memos/${memoId}/retry-headline`, { + method: 'POST', + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, + }); + if (!response.ok) { + const d = await response.json().catch(() => ({})); + throw new Error(d.error || `HTTP ${response.status}`); } + return { success: true, message: 'Headline generation retry initiated successfully' }; } } -// Export singleton instance export const creditService = new CreditService(); diff --git a/apps/memoro/apps/web/src/lib/services/questionService.ts b/apps/memoro/apps/web/src/lib/services/questionService.ts index 205f3fcce..1866d8c26 100644 --- a/apps/memoro/apps/web/src/lib/services/questionService.ts +++ b/apps/memoro/apps/web/src/lib/services/questionService.ts @@ -1,14 +1,17 @@ /** * Question Service for memoro-web - * Handles Q&A functionality for memos + * Handles Q&A functionality for memos via memoro-server (Hono/Bun). */ import { env } from '$lib/config/env'; -import { tokenManager } from './tokenManager'; +import { authStore } from '$lib/stores/auth.svelte'; import { createAuthClient } from '$lib/supabaseClient'; +const SERVER_URL = () => env.server.memoroUrl.replace(/\/$/, ''); + export interface QuestionResult { success: boolean; + answer?: string; memoryId?: string; error?: string; creditsConsumed?: number; @@ -22,113 +25,50 @@ export interface Memory { } class QuestionService { - /** - * Ask a question about a memo - * This calls the memoro middleware service to generate an AI answer - */ async askQuestion(memoId: string, question: string): Promise { if (!memoId || !question.trim()) { - return { - success: false, - error: 'Invalid memo ID or question', - }; + return { success: false, error: 'Invalid memo ID or question' }; } try { - // Get a valid token - const token = await tokenManager.getValidToken(); + const token = await authStore.getAccessToken(); if (!token) { - return { - success: false, - error: 'Nicht authentifiziert. Bitte melden Sie sich erneut an.', - }; + return { success: false, error: 'Nicht authentifiziert. Bitte melden Sie sich erneut an.' }; } - // Get the memoro service URL - const memoroServiceUrl = env.middleware.memoroUrl?.replace(/\/$/, ''); - if (!memoroServiceUrl) { - return { - success: false, - error: 'Memoro service URL nicht konfiguriert', - }; - } - - // Call the memoro service - const response = await fetch(`${memoroServiceUrl}/memoro/question-memo`, { + const response = await fetch(`${SERVER_URL()}/api/v1/memos/${memoId}/question`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, - body: JSON.stringify({ - memo_id: memoId, - question: question.trim(), - }), + body: JSON.stringify({ question: question.trim() }), }); if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - - // Handle specific error codes if (response.status === 402) { - return { - success: false, - error: 'Nicht genügend Mana. Bitte laden Sie Ihr Konto auf.', - }; + return { success: false, error: 'Nicht genügend Mana. Bitte laden Sie Ihr Konto auf.' }; } - if (response.status === 401) { - return { - success: false, - error: 'Sitzung abgelaufen. Bitte melden Sie sich erneut an.', - }; + return { success: false, error: 'Sitzung abgelaufen. Bitte melden Sie sich erneut an.' }; } - - return { - success: false, - error: errorData.message || `Fehler: ${response.status} ${response.statusText}`, - }; + const d = await response.json().catch(() => ({})); + return { success: false, error: d.error || `Fehler: ${response.status}` }; } const data = await response.json(); - - if (data?.success && data?.memory_id) { - return { - success: true, - memoryId: data.memory_id, - creditsConsumed: data.creditsConsumed, - }; - } - - return { - success: false, - error: data?.error || 'Unbekannter Fehler bei der Verarbeitung', - }; + return { success: true, answer: data.answer, creditsConsumed: data.creditsConsumed }; } catch (error) { - console.error('Error asking question:', error); - - // Check for network errors if (error instanceof TypeError && error.message.includes('fetch')) { - return { - success: false, - error: 'Netzwerkfehler. Bitte überprüfen Sie Ihre Internetverbindung.', - }; + return { success: false, error: 'Netzwerkfehler. Bitte überprüfen Sie Ihre Internetverbindung.' }; } - - return { - success: false, - error: error instanceof Error ? error.message : 'Unbekannter Fehler', - }; + return { success: false, error: error instanceof Error ? error.message : 'Unbekannter Fehler' }; } } - /** - * Load memories for a memo - */ async loadMemories(memoId: string): Promise { try { const supabase = await createAuthClient(); - const { data, error } = await supabase .from('memories') .select('id, title, content, metadata') @@ -140,7 +80,6 @@ class QuestionService { console.error('Error loading memories:', error); return []; } - return data || []; } catch (error) { console.error('Error loading memories:', error); @@ -149,5 +88,4 @@ class QuestionService { } } -// Export singleton instance export const questionService = new QuestionService();