2025-07-07 03:47:12 +09:00

589 lines
17 KiB
TypeScript

import { auth as firebaseAuth } from './firebase';
import {
FirestoreUserService,
FirestoreSessionService,
FirestoreTranscriptService,
FirestoreAiMessageService,
FirestoreSummaryService,
FirestorePromptPresetService,
FirestoreSession,
FirestoreTranscript,
FirestoreAiMessage,
FirestoreSummary,
FirestorePromptPreset
} from './firestore';
import { Timestamp } from 'firebase/firestore';
export interface UserProfile {
uid: string;
display_name: string;
email: string;
}
export interface Session {
id: string;
uid: string;
title: string;
session_type: string;
started_at: number;
ended_at?: number;
sync_state: 'clean' | 'dirty';
updated_at: number;
}
export interface Transcript {
id: string;
session_id: string;
start_at: number;
end_at?: number;
speaker?: string;
text: string;
lang?: string;
created_at: number;
sync_state: 'clean' | 'dirty';
}
export interface AiMessage {
id: string;
session_id: string;
sent_at: number;
role: 'user' | 'assistant';
content: string;
tokens?: number;
model?: string;
created_at: number;
sync_state: 'clean' | 'dirty';
}
export interface Summary {
session_id: string;
generated_at: number;
model?: string;
text: string;
tldr: string;
bullet_json: string;
action_json: string;
tokens_used?: number;
updated_at: number;
sync_state: 'clean' | 'dirty';
}
export interface PromptPreset {
id: string;
uid: string;
title: string;
prompt: string;
is_default: 0 | 1;
created_at: number;
sync_state: 'clean' | 'dirty';
}
export interface SessionDetails {
session: Session;
transcripts: Transcript[];
ai_messages: AiMessage[];
summary: Summary | null;
}
const isFirebaseMode = (): boolean => {
// The web frontend can no longer directly access Firebase state,
// so we assume communication always goes through the backend API.
// In the future, we can create an endpoint like /api/auth/status
// in the backend to retrieve the authentication state.
return false;
};
const timestampToUnix = (timestamp: Timestamp): number => {
return timestamp.seconds * 1000 + Math.floor(timestamp.nanoseconds / 1000000);
};
const unixToTimestamp = (unix: number): Timestamp => {
return Timestamp.fromMillis(unix);
};
const convertFirestoreSession = (session: { id: string } & FirestoreSession, uid: string): Session => {
return {
id: session.id,
uid,
title: session.title,
session_type: session.session_type,
started_at: timestampToUnix(session.startedAt),
ended_at: session.endedAt ? timestampToUnix(session.endedAt) : undefined,
sync_state: 'clean',
updated_at: timestampToUnix(session.startedAt)
};
};
const convertFirestoreTranscript = (transcript: { id: string } & FirestoreTranscript): Transcript => {
return {
id: transcript.id,
session_id: '',
start_at: timestampToUnix(transcript.startAt),
end_at: transcript.endAt ? timestampToUnix(transcript.endAt) : undefined,
speaker: transcript.speaker,
text: transcript.text,
lang: transcript.lang,
created_at: timestampToUnix(transcript.createdAt),
sync_state: 'clean'
};
};
const convertFirestoreAiMessage = (message: { id: string } & FirestoreAiMessage): AiMessage => {
return {
id: message.id,
session_id: '',
sent_at: timestampToUnix(message.sentAt),
role: message.role,
content: message.content,
tokens: message.tokens,
model: message.model,
created_at: timestampToUnix(message.createdAt),
sync_state: 'clean'
};
};
const convertFirestoreSummary = (summary: FirestoreSummary, sessionId: string): Summary => {
return {
session_id: sessionId,
generated_at: timestampToUnix(summary.generatedAt),
model: summary.model,
text: summary.text,
tldr: summary.tldr,
bullet_json: JSON.stringify(summary.bulletPoints),
action_json: JSON.stringify(summary.actionItems),
tokens_used: summary.tokensUsed,
updated_at: timestampToUnix(summary.generatedAt),
sync_state: 'clean'
};
};
const convertFirestorePreset = (preset: { id: string } & FirestorePromptPreset, uid: string): PromptPreset => {
return {
id: preset.id,
uid,
title: preset.title,
prompt: preset.prompt,
is_default: preset.isDefault ? 1 : 0,
created_at: timestampToUnix(preset.createdAt),
sync_state: 'clean'
};
};
let API_ORIGIN = process.env.NODE_ENV === 'development'
? 'http://localhost:9001'
: '';
const loadRuntimeConfig = async (): Promise<string | null> => {
try {
const response = await fetch('/runtime-config.json');
if (response.ok) {
const config = await response.json();
console.log('✅ Runtime config loaded:', config);
return config.API_URL;
}
} catch (error) {
console.log('⚠️ Failed to load runtime config:', error);
}
return null;
};
let apiUrlInitialized = false;
let initializationPromise: Promise<void> | null = null;
const initializeApiUrl = async () => {
if (apiUrlInitialized) return;
// Electron IPC 관련 코드를 모두 제거하고 runtime-config.json 또는 fallback에만 의존합니다.
const runtimeUrl = await loadRuntimeConfig();
if (runtimeUrl) {
API_ORIGIN = runtimeUrl;
apiUrlInitialized = true;
return;
}
console.log('📍 Using fallback API URL:', API_ORIGIN);
apiUrlInitialized = true;
};
if (typeof window !== 'undefined') {
initializationPromise = initializeApiUrl();
}
const userInfoListeners: Array<(userInfo: UserProfile | null) => void> = [];
export const getUserInfo = (): UserProfile | null => {
if (typeof window === 'undefined') return null;
const storedUserInfo = localStorage.getItem('pickleglass_user');
if (storedUserInfo) {
try {
return JSON.parse(storedUserInfo);
} catch (error) {
console.error('Failed to parse user info:', error);
localStorage.removeItem('pickleglass_user');
}
}
return null;
};
export const setUserInfo = (userInfo: UserProfile | null, skipEvents: boolean = false) => {
if (typeof window === 'undefined') return;
if (userInfo) {
localStorage.setItem('pickleglass_user', JSON.stringify(userInfo));
} else {
localStorage.removeItem('pickleglass_user');
}
if (!skipEvents) {
userInfoListeners.forEach(listener => listener(userInfo));
window.dispatchEvent(new Event('userInfoChanged'));
}
};
export const onUserInfoChange = (listener: (userInfo: UserProfile | null) => void) => {
userInfoListeners.push(listener);
return () => {
const index = userInfoListeners.indexOf(listener);
if (index > -1) {
userInfoListeners.splice(index, 1);
}
};
};
export const getApiHeaders = (): HeadersInit => {
const headers: HeadersInit = {
'Content-Type': 'application/json',
};
const userInfo = getUserInfo();
if (userInfo?.uid) {
headers['X-User-ID'] = userInfo.uid;
}
return headers;
};
export const apiCall = async (path: string, options: RequestInit = {}) => {
if (!apiUrlInitialized && initializationPromise) {
await initializationPromise;
}
if (!apiUrlInitialized) {
await initializeApiUrl();
}
const url = `${API_ORIGIN}${path}`;
console.log('🌐 apiCall (Local Mode):', {
path,
API_ORIGIN,
fullUrl: url,
initialized: apiUrlInitialized,
timestamp: new Date().toISOString()
});
const defaultOpts: RequestInit = {
headers: {
'Content-Type': 'application/json',
...getApiHeaders(),
...(options.headers || {}),
},
...options,
};
return fetch(url, defaultOpts);
};
export const searchConversations = async (query: string): Promise<Session[]> => {
if (!query.trim()) {
return [];
}
if (isFirebaseMode()) {
const sessions = await getSessions();
return sessions.filter(session =>
session.title.toLowerCase().includes(query.toLowerCase())
);
} else {
const response = await apiCall(`/api/conversations/search?q=${encodeURIComponent(query)}`, {
method: 'GET',
});
if (!response.ok) {
throw new Error('Failed to search conversations');
}
return response.json();
}
};
export const getSessions = async (): Promise<Session[]> => {
if (isFirebaseMode()) {
const uid = firebaseAuth.currentUser!.uid;
const firestoreSessions = await FirestoreSessionService.getSessions(uid);
return firestoreSessions.map(session => convertFirestoreSession(session, uid));
} else {
const response = await apiCall(`/api/conversations`, { method: 'GET' });
if (!response.ok) throw new Error('Failed to fetch sessions');
return response.json();
}
};
export const getSessionDetails = async (sessionId: string): Promise<SessionDetails> => {
if (isFirebaseMode()) {
const uid = firebaseAuth.currentUser!.uid;
const [session, transcripts, aiMessages, summary] = await Promise.all([
FirestoreSessionService.getSession(uid, sessionId),
FirestoreTranscriptService.getTranscripts(uid, sessionId),
FirestoreAiMessageService.getAiMessages(uid, sessionId),
FirestoreSummaryService.getSummary(uid, sessionId)
]);
if (!session) {
throw new Error('Session not found');
}
return {
session: convertFirestoreSession({ id: sessionId, ...session }, uid),
transcripts: transcripts.map(t => ({ ...convertFirestoreTranscript(t), session_id: sessionId })),
ai_messages: aiMessages.map(m => ({ ...convertFirestoreAiMessage(m), session_id: sessionId })),
summary: summary ? convertFirestoreSummary(summary, sessionId) : null
};
} else {
const response = await apiCall(`/api/conversations/${sessionId}`, { method: 'GET' });
if (!response.ok) throw new Error('Failed to fetch session details');
return response.json();
}
};
export const createSession = async (title?: string): Promise<{ id: string }> => {
if (isFirebaseMode()) {
const uid = firebaseAuth.currentUser!.uid;
const sessionId = await FirestoreSessionService.createSession(uid, {
title: title || 'New Session',
session_type: 'ask',
endedAt: undefined
});
return { id: sessionId };
} else {
const response = await apiCall(`/api/conversations`, {
method: 'POST',
body: JSON.stringify({ title }),
});
if (!response.ok) throw new Error('Failed to create session');
return response.json();
}
};
export const deleteSession = async (sessionId: string): Promise<void> => {
if (isFirebaseMode()) {
const uid = firebaseAuth.currentUser!.uid;
await FirestoreSessionService.deleteSession(uid, sessionId);
} else {
const response = await apiCall(`/api/conversations/${sessionId}`, { method: 'DELETE' });
if (!response.ok) throw new Error('Failed to delete session');
}
};
export const getUserProfile = async (): Promise<UserProfile> => {
if (isFirebaseMode()) {
const user = firebaseAuth.currentUser!;
const firestoreProfile = await FirestoreUserService.getUser(user.uid);
return {
uid: user.uid,
display_name: firestoreProfile?.displayName || user.displayName || 'User',
email: firestoreProfile?.email || user.email || 'no-email@example.com'
};
} else {
const response = await apiCall(`/api/user/profile`, { method: 'GET' });
if (!response.ok) throw new Error('Failed to fetch user profile');
return response.json();
}
};
export const updateUserProfile = async (data: { displayName: string }): Promise<void> => {
if (isFirebaseMode()) {
const uid = firebaseAuth.currentUser!.uid;
await FirestoreUserService.updateUser(uid, { displayName: data.displayName });
} else {
const response = await apiCall(`/api/user/profile`, {
method: 'PUT',
body: JSON.stringify(data),
});
if (!response.ok) throw new Error('Failed to update user profile');
}
};
export const findOrCreateUser = async (user: UserProfile): Promise<UserProfile> => {
if (isFirebaseMode()) {
const uid = firebaseAuth.currentUser!.uid;
const existingUser = await FirestoreUserService.getUser(uid);
if (!existingUser) {
await FirestoreUserService.createUser(uid, {
displayName: user.display_name,
email: user.email
});
}
return user;
} else {
const response = await apiCall(`/api/user/find-or-create`, {
method: 'POST',
body: JSON.stringify(user),
});
if (!response.ok) throw new Error('Failed to find or create user');
return response.json();
}
};
export const saveApiKey = async (apiKey: string): Promise<void> => {
if (isFirebaseMode()) {
console.log('API key is not needed in Firebase mode');
return;
} else {
const response = await apiCall(`/api/user/api-key`, {
method: 'POST',
body: JSON.stringify({ apiKey }),
});
if (!response.ok) throw new Error('Failed to save API key');
}
};
export const checkApiKeyStatus = async (): Promise<{ hasApiKey: boolean }> => {
if (isFirebaseMode()) {
return { hasApiKey: true };
} else {
const response = await apiCall(`/api/user/api-key-status`, { method: 'GET' });
if (!response.ok) throw new Error('Failed to check API key status');
return response.json();
}
};
export const deleteAccount = async (): Promise<void> => {
if (isFirebaseMode()) {
const uid = firebaseAuth.currentUser!.uid;
await FirestoreUserService.deleteUser(uid);
await firebaseAuth.currentUser!.delete();
} else {
const response = await apiCall(`/api/user/profile`, { method: 'DELETE' });
if (!response.ok) throw new Error('Failed to delete account');
}
};
export const getPresets = async (): Promise<PromptPreset[]> => {
if (isFirebaseMode()) {
const uid = firebaseAuth.currentUser!.uid;
const firestorePresets = await FirestorePromptPresetService.getPresets(uid);
return firestorePresets.map(preset => convertFirestorePreset(preset, uid));
} else {
const response = await apiCall(`/api/presets`, { method: 'GET' });
if (!response.ok) throw new Error('Failed to fetch presets');
return response.json();
}
};
export const createPreset = async (data: { title: string, prompt: string }): Promise<{ id: string }> => {
if (isFirebaseMode()) {
const uid = firebaseAuth.currentUser!.uid;
const presetId = await FirestorePromptPresetService.createPreset(uid, {
title: data.title,
prompt: data.prompt,
isDefault: false
});
return { id: presetId };
} else {
const response = await apiCall(`/api/presets`, {
method: 'POST',
body: JSON.stringify(data),
});
if (!response.ok) throw new Error('Failed to create preset');
return response.json();
}
};
export const updatePreset = async (id: string, data: { title: string, prompt: string }): Promise<void> => {
if (isFirebaseMode()) {
const uid = firebaseAuth.currentUser!.uid;
await FirestorePromptPresetService.updatePreset(uid, id, {
title: data.title,
prompt: data.prompt
});
} else {
const response = await apiCall(`/api/presets/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to update preset: ${response.status} ${errorText}`);
}
}
};
export const deletePreset = async (id: string): Promise<void> => {
if (isFirebaseMode()) {
const uid = firebaseAuth.currentUser!.uid;
await FirestorePromptPresetService.deletePreset(uid, id);
} else {
const response = await apiCall(`/api/presets/${id}`, { method: 'DELETE' });
if (!response.ok) throw new Error('Failed to delete preset');
}
};
export interface BatchData {
profile?: UserProfile;
presets?: PromptPreset[];
sessions?: Session[];
}
export const getBatchData = async (includes: ('profile' | 'presets' | 'sessions')[]): Promise<BatchData> => {
if (isFirebaseMode()) {
const result: BatchData = {};
const promises: Promise<any>[] = [];
if (includes.includes('profile')) {
promises.push(getUserProfile().then(profile => ({ type: 'profile', data: profile })));
}
if (includes.includes('presets')) {
promises.push(getPresets().then(presets => ({ type: 'presets', data: presets })));
}
if (includes.includes('sessions')) {
promises.push(getSessions().then(sessions => ({ type: 'sessions', data: sessions })));
}
const results = await Promise.all(promises);
results.forEach(({ type, data }) => {
result[type as keyof BatchData] = data;
});
return result;
} else {
const response = await apiCall(`/api/user/batch?include=${includes.join(',')}`, { method: 'GET' });
if (!response.ok) throw new Error('Failed to fetch batch data');
return response.json();
}
};
export const logout = async () => {
if (isFirebaseMode()) {
const { signOut } = await import('firebase/auth');
await signOut(firebaseAuth);
}
setUserInfo(null);
localStorage.removeItem('openai_api_key');
localStorage.removeItem('user_info');
window.location.href = '/login';
};