Files
deardiary/frontend/src/lib/api.ts
lotherk 5c217853de feat: v0.0.1 - Groq provider, timezone, journal context, test connection, task logging
Added:
- Groq AI provider (free, fast with llama-3.3-70b-versatile)
- Timezone setting (22 timezones)
- Journal context: include previous journals (3/7/14/30 days)
- Test connection button for AI providers
- Per-provider settings (API key, model, base URL remembered)
- Detailed task logging (full prompts and responses)
- Tasks page with expandable details
- Progress modal with steps and AI output details

Fixed:
- Groq API endpoint (https://api.groq.com/openai/v1/chat/completions)
- Ollama baseUrl leaking to other providers
- Database schema references
- Proper Prisma migrations (data-safe)

Changed:
- Default AI: OpenAI → Groq
- Project renamed: TotalRecall → DearDiary
- Strict anti-hallucination prompt
- Docker uses prisma migrate deploy (non-destructive)
2026-03-26 21:56:29 +00:00

203 lines
5.1 KiB
TypeScript

const API_BASE = '/api/v1';
interface ApiResponse<T> {
data: T | null;
error: { code: string; message: string } | null;
}
class ApiClient {
private apiKey: string | null = null;
setApiKey(key: string) {
this.apiKey = key;
localStorage.setItem('apiKey', key);
}
getApiKey(): string | null {
if (this.apiKey) return this.apiKey;
this.apiKey = localStorage.getItem('apiKey');
return this.apiKey;
}
clearApiKey() {
this.apiKey = null;
localStorage.removeItem('apiKey');
}
private async request<T>(
method: string,
path: string,
body?: unknown
): Promise<ApiResponse<T>> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (this.getApiKey()) {
headers['Authorization'] = `Bearer ${this.getApiKey()}`;
}
const response = await fetch(`${API_BASE}${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
return response.json() as Promise<ApiResponse<T>>;
}
async register(email: string, password: string) {
return this.request<{ user: { id: string; email: string } }>('POST', '/auth/register', { email, password });
}
async login(email: string, password: string) {
return this.request<{ token: string; userId: string }>('POST', '/auth/login', { email, password });
}
async createApiKey(name: string, token: string) {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
};
const response = await fetch(`${API_BASE}/auth/api-key`, {
method: 'POST',
headers,
body: JSON.stringify({ name }),
});
return response.json() as Promise<ApiResponse<{ apiKey: string }>>;
}
async getDays() {
return this.request<Array<{ date: string; entryCount: number; hasJournal: boolean }>>('GET', '/days');
}
async getDay(date: string) {
return this.request<{ date: string; entries: Entry[]; journal: Journal | null }>('GET', `/days/${date}`);
}
async deleteDay(date: string) {
return this.request<{ deleted: boolean }>('DELETE', `/days/${date}`);
}
async createEntry(date: string, type: string, content: string, metadata?: object) {
return this.request<Entry>('POST', '/entries', { date, type, content, metadata });
}
async updateEntry(id: string, content: string, metadata?: object) {
return this.request<Entry>('PUT', `/entries/${id}`, { content, metadata });
}
async deleteEntry(id: string) {
return this.request<{ deleted: boolean }>('DELETE', `/entries/${id}`);
}
async uploadPhoto(entryId: string, file: File) {
const formData = new FormData();
formData.append('file', file);
const headers: Record<string, string> = {};
if (this.getApiKey()) {
headers['Authorization'] = `Bearer ${this.getApiKey()}`;
}
const response = await fetch(`${API_BASE}/entries/${entryId}/photo`, {
method: 'POST',
headers,
body: formData,
});
return response.json() as Promise<ApiResponse<{ mediaPath: string }>>;
}
async uploadVoice(entryId: string, file: File) {
const formData = new FormData();
formData.append('file', file);
const headers: Record<string, string> = {};
if (this.getApiKey()) {
headers['Authorization'] = `Bearer ${this.getApiKey()}`;
}
const response = await fetch(`${API_BASE}/entries/${entryId}/voice`, {
method: 'POST',
headers,
body: formData,
});
return response.json() as Promise<ApiResponse<{ mediaPath: string }>>;
}
async generateJournal(date: string) {
return this.request<{ journal: Journal; task: Task }>('POST', `/journal/generate/${date}`);
}
async getJournal(date: string) {
return this.request<Journal>('GET', `/journal/${date}`);
}
async getJournalTasks(date: string) {
return this.request<Task[]>('GET', `/journal/${date}/tasks`);
}
async getTask(taskId: string) {
return this.request<Task>('GET', `/tasks/${taskId}`);
}
async getSettings() {
return this.request<Settings>('GET', '/settings');
}
async updateSettings(settings: Partial<Settings>) {
return this.request<Settings>('PUT', '/settings', settings);
}
}
export interface Entry {
id: string;
date: string;
type: 'text' | 'voice' | 'photo' | 'health' | 'location';
content: string;
mediaPath?: string;
metadata?: string;
createdAt: string;
}
export interface Journal {
id: string;
date: string;
content: string;
entryCount: number;
generatedAt: string;
}
export interface Task {
id: string;
journalId: string;
type: string;
status: 'pending' | 'completed' | 'failed';
provider: string;
model?: string;
prompt?: string;
request?: string;
response?: string;
error?: string;
createdAt: string;
completedAt?: string;
}
export interface Settings {
aiProvider: 'openai' | 'anthropic' | 'ollama' | 'lmstudio' | 'groq';
aiApiKey?: string;
aiModel: string;
aiBaseUrl?: string;
journalPrompt: string;
language: string;
timezone: string;
journalContextDays: number;
providerSettings?: Record<string, {
apiKey?: string;
model?: string;
baseUrl?: string;
}>;
}
export const api = new ApiClient();