Initial commit

This commit is contained in:
Haitham Khalifa
2026-02-16 12:18:06 +01:00
commit b538d84e17
63 changed files with 15059 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
import { initializeApp, getApps, getApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN!,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET!,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID!,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID!,
};
export const app =
getApps().length === 0 ? initializeApp(firebaseConfig) : getApp();
export const auth = getAuth(app);
export const db = getFirestore(app);
+12
View File
@@ -0,0 +1,12 @@
import { initializeApp, getApps, getApp } from 'firebase/app';
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN!,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET!,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID!,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID!,
};
export const app = !getApps().length ? initializeApp(firebaseConfig) : getApp();
+287
View File
@@ -0,0 +1,287 @@
import {
collection,
doc,
getDoc,
getDocs,
setDoc,
updateDoc,
query,
where,
orderBy,
Timestamp,
addDoc,
deleteDoc,
} from 'firebase/firestore';
import { db } from './firebase';
import type {
User,
AgentSettings,
CallTask,
CallAttempt,
CallRecord,
CreditTransaction,
NotificationSettings,
BookedAppointment,
DefaultLimits,
Purchase,
} from './types';
// Users
export const getUser = async (uid: string): Promise<User | null> => {
const docRef = doc(db, 'users', uid);
const docSnap = await getDoc(docRef);
return docSnap.exists() ? (docSnap.data() as User) : null;
};
export const updateUserCredits = async (uid: string, newBalance: number) => {
const docRef = doc(db, 'users', uid);
await updateDoc(docRef, {
creditBalance: newBalance,
updatedAt: Timestamp.now(),
});
};
export const updateUserOnboardingCompleted = async (uid: string, completed: boolean) => {
const docRef = doc(db, 'users', uid);
await updateDoc(docRef, {
onboardingCompleted: completed,
updatedAt: Timestamp.now(),
});
};
// Agent Settings
export const getAgentSettings = async (userId: string): Promise<AgentSettings | null> => {
const docRef = doc(db, 'agentSettings', userId);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
return docSnap.data() as AgentSettings;
}
// Create default if not exists
const defaults: AgentSettings = {
userId,
agentName: 'HolaCompi',
appLanguage: 'English',
primaryCallLanguage: 'English',
secondaryCallLanguages: [],
tone: 'Friendly',
agentInstructions: 'You are HolaCompi, a helpful Spanish friend who makes phone calls on behalf of users.',
createdAt: Timestamp.now(),
updatedAt: Timestamp.now(),
};
await setDoc(docRef, defaults);
return defaults;
};
export const updateAgentSettings = async (userId: string, updates: Partial<AgentSettings>) => {
const docRef = doc(db, 'agentSettings', userId);
await updateDoc(docRef, {
...updates,
updatedAt: Timestamp.now(),
});
};
// Call Tasks
export const createCallTask = async (task: Omit<CallTask, 'id'>): Promise<string> => {
const docRef = await addDoc(collection(db, 'callTasks'), task);
return docRef.id;
};
export const getCallTasks = async (userId: string): Promise<CallTask[]> => {
const q = query(
collection(db, 'callTasks'),
where('userId', '==', userId),
orderBy('scheduledAt', 'desc')
);
const snapshot = await getDocs(q);
return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() } as CallTask));
};
export const getCallTask = async (taskId: string): Promise<CallTask | null> => {
const docRef = doc(db, 'callTasks', taskId);
const docSnap = await getDoc(docRef);
return docSnap.exists() ? ({ id: docSnap.id, ...docSnap.data() } as CallTask) : null;
};
export const updateCallTask = async (taskId: string, updates: Partial<CallTask>) => {
const docRef = doc(db, 'callTasks', taskId);
await updateDoc(docRef, {
...updates,
updatedAt: Timestamp.now(),
});
};
export const deleteCallTask = async (taskId: string) => {
await deleteDoc(doc(db, 'callTasks', taskId));
};
// Call Attempts
export const createCallAttempt = async (
taskId: string,
attempt: Omit<CallAttempt, 'id'>
): Promise<string> => {
const docRef = await addDoc(
collection(db, 'callTasks', taskId, 'attempts'),
attempt
);
return docRef.id;
};
export const getCallAttempts = async (taskId: string): Promise<CallAttempt[]> => {
const q = query(
collection(db, 'callTasks', taskId, 'attempts'),
orderBy('startTime', 'desc')
);
const snapshot = await getDocs(q);
return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() } as CallAttempt));
};
export const updateCallAttempt = async (
taskId: string,
attemptId: string,
updates: Partial<CallAttempt>
) => {
const docRef = doc(db, 'callTasks', taskId, 'attempts', attemptId);
await updateDoc(docRef, updates);
};
// Calls
export const createCallRecord = async (
record: Omit<CallRecord, 'id' | 'createdAt' | 'updatedAt'>
): Promise<string> => {
const docRef = await addDoc(collection(db, 'calls'), {
...record,
createdAt: Timestamp.now(),
updatedAt: Timestamp.now(),
});
return docRef.id;
};
export const updateCallRecord = async (
callId: string,
updates: Partial<CallRecord>
) => {
const docRef = doc(db, 'calls', callId);
await updateDoc(docRef, { ...updates, updatedAt: Timestamp.now() });
};
export const getCallByVapiCallId = async (vapiCallId: string): Promise<CallRecord | null> => {
const q = query(collection(db, 'calls'), where('vapiCallId', '==', vapiCallId));
const snapshot = await getDocs(q);
if (snapshot.empty) return null;
const docSnap = snapshot.docs[0];
return { id: docSnap.id, ...(docSnap.data() as CallRecord) };
};
// Credit Transactions
export const createCreditTransaction = async (
transaction: Omit<CreditTransaction, 'id'>
): Promise<string> => {
const docRef = await addDoc(collection(db, 'creditLedger'), transaction);
return docRef.id;
};
export const getCreditTransactions = async (userId: string): Promise<CreditTransaction[]> => {
const q = query(
collection(db, 'creditLedger'),
where('userId', '==', userId),
orderBy('createdAt', 'desc')
);
const snapshot = await getDocs(q);
return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() } as CreditTransaction));
};
// Purchases
export const createPurchase = async (purchase: Omit<Purchase, 'id'>): Promise<string> => {
const docRef = await addDoc(collection(db, 'purchases'), purchase);
return docRef.id;
};
export const getPurchasesByUser = async (userId: string): Promise<Purchase[]> => {
const q = query(
collection(db, 'purchases'),
where('userId', '==', userId),
orderBy('createdAt', 'desc')
);
const snapshot = await getDocs(q);
return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() } as Purchase));
};
// Notification Settings
export const getNotificationSettings = async (userId: string): Promise<NotificationSettings> => {
const docRef = doc(db, 'notificationSettings', userId);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
return docSnap.data() as NotificationSettings;
}
const defaults: NotificationSettings = {
userId,
notifyOnSuccess: true,
notifyOnFailure: true,
emailSummary: false,
autoAddToCalendar: false,
googleCalendarConnected: false,
iCloudCalendarConnected: false,
createdAt: Timestamp.now(),
updatedAt: Timestamp.now(),
};
await setDoc(docRef, defaults);
return defaults;
};
export const updateNotificationSettings = async (
userId: string,
updates: Partial<NotificationSettings>
) => {
const docRef = doc(db, 'notificationSettings', userId);
await updateDoc(docRef, {
...updates,
updatedAt: Timestamp.now(),
});
};
// Booked Appointments
export const createBookedAppointment = async (
appointment: Omit<BookedAppointment, 'id'>
): Promise<string> => {
const docRef = await addDoc(collection(db, 'bookedAppointments'), appointment);
return docRef.id;
};
export const getBookedAppointments = async (userId: string): Promise<BookedAppointment[]> => {
const q = query(
collection(db, 'bookedAppointments'),
where('userId', '==', userId),
orderBy('dateTime', 'asc')
);
const snapshot = await getDocs(q);
return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() } as BookedAppointment));
};
// Default Limits
export const getDefaultLimits = async (userId: string): Promise<DefaultLimits> => {
const docRef = doc(db, 'defaultLimits', userId);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
return docSnap.data() as DefaultLimits;
}
const defaults: DefaultLimits = {
userId,
defaultMaxMinutes: 10,
defaultMaxCredits: 10,
allowAutoExtension: false,
maxExtraMinutes: 0,
defaultMaxRecalls: 2,
minDelayBetweenRecalls: 15,
updatedAt: Timestamp.now(),
};
await setDoc(docRef, defaults);
return defaults;
};
export const updateDefaultLimits = async (userId: string, updates: Partial<DefaultLimits>) => {
const docRef = doc(db, 'defaultLimits', userId);
await updateDoc(docRef, {
...updates,
updatedAt: Timestamp.now(),
});
};
+132
View File
@@ -0,0 +1,132 @@
import { Timestamp } from 'firebase/firestore';
export interface User {
uid: string;
email: string;
displayName?: string;
onboardingCompleted?: boolean;
creditBalance: number; // in EUR/credits
createdAt: Timestamp;
updatedAt: Timestamp;
}
export interface AgentSettings {
userId: string;
agentName: string; // default "HolaCompi"
appLanguage: string; // fixed "English" for v1
primaryCallLanguage: string; // default "English"
secondaryCallLanguages: string[]; // e.g., ["Spanish"]
tone: 'Friendly' | 'Professional' | 'Assertive';
agentInstructions: string; // system prompt
createdAt: Timestamp;
updatedAt: Timestamp;
}
export interface CallTask {
id: string;
userId: string;
businessName: string;
phoneNumber: string;
preferredLanguage: string; // "English" or "Spanish"
callGoals: string; // multi-line text
scheduledAt: Timestamp;
timezone?: string;
maxMinutes: number;
maxCredits: number; // same as maxMinutes for v1
allowRecalls: boolean;
maxRecalls: number;
recallsUsed: number;
status: 'pending' | 'in_progress' | 'completed' | 'failed';
lastResult?: string;
createdAt: Timestamp;
updatedAt: Timestamp;
}
export interface CallAttempt {
id: string;
taskId: string;
startTime: Timestamp;
endTime?: Timestamp;
duration: number; // minutes
creditsUsed: number;
status: 'in_progress' | 'completed' | 'hangup_other_party' | 'failed' | 'busy';
transcript?: string;
summary?: string;
vapiCallId?: string;
createdAt: Timestamp;
}
export interface CallRecord {
id: string;
userId: string;
phoneNumber: string;
status: 'initiated' | 'started' | 'ended' | 'failed';
vapiCallId?: string;
assistantId?: string;
scheduledTaskId?: string;
startedAt?: Timestamp;
endedAt?: Timestamp;
durationSeconds?: number;
creditsUsed?: number;
recordingUrl?: string;
transcript?: string;
createdAt: Timestamp;
updatedAt: Timestamp;
}
export interface CreditTransaction {
id: string;
userId: string;
amount: number; // positive for add, negative for debit
type: 'purchase' | 'call_debit' | 'refund';
callTaskId?: string;
callAttemptId?: string;
balanceAfter: number;
createdAt: Timestamp;
}
export interface NotificationSettings {
userId: string;
notifyOnSuccess: boolean;
notifyOnFailure: boolean;
emailSummary: boolean;
autoAddToCalendar: boolean;
googleCalendarConnected: boolean;
iCloudCalendarConnected: boolean;
createdAt: Timestamp;
updatedAt: Timestamp;
}
export interface BookedAppointment {
id: string;
userId: string;
callTaskId: string;
title: string;
dateTime: Timestamp;
location?: string;
phone?: string;
notes?: string;
calendarEventId?: string;
createdAt: Timestamp;
}
export interface DefaultLimits {
userId: string;
defaultMaxMinutes: number;
defaultMaxCredits: number;
allowAutoExtension: boolean;
maxExtraMinutes: number;
defaultMaxRecalls: number;
minDelayBetweenRecalls: number; // minutes
updatedAt: Timestamp;
}
export interface Purchase {
id: string;
userId: string;
credits: number;
amount: number; // EUR amount
currency: string;
stripeSessionId: string;
createdAt: Timestamp;
}
+11
View File
@@ -0,0 +1,11 @@
import Vapi from '@vapi-ai/web';
export const vapiConfig = {
publicKey: process.env.NEXT_PUBLIC_VAPI_PUBLIC_KEY!,
// Vapi Assistant ID from dashboard
assistantId: process.env.NEXT_PUBLIC_VAPI_ASSISTANT_ID,
};
export function initializeVapi() {
return new Vapi(vapiConfig.publicKey);
}