diff --git a/CHANGELOG.md b/CHANGELOG.md index 46f05c8..784976e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,33 @@ All notable changes to DearDiary will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## [Unreleased] +## [0.1.0] - 2026-03-26 + +### Added +- **Groq Provider**: Free, fast AI provider with default model `llama-3.3-70b-versatile` +- **Timezone setting**: Users can select their local timezone (22 timezones) +- **Journal context**: Option to include previous journals (3/7/14/30 days) for AI context +- **Generating modal**: Progress bar with steps and expandable "Details" section +- **Tasks page**: Route `/tasks/:date` for viewing generation tasks with full prompts/responses +- **Test connection**: Button to test AI provider connectivity +- **Per-provider settings**: Each AI provider remembers its own API key, model, and base URL +- **Detailed task logging**: Full prompts and responses stored for debugging ### Changed -- **Strict anti-hallucination default prompt**: New users get a grounded, factual prompt that ONLY summarizes what's recorded - no invention, no embellishment +- **Default AI**: OpenAI → Groq with free `llama-3.3-70b-versatile` model +- **Project renamed**: "TotalRecall" → "DearDiary" everywhere +- **Strict anti-hallucination prompt**: Grounded, factual summarization only +- **Migrations**: Proper Prisma migration system (data-safe updates) +- **Docker**: Non-destructive database migrations via `prisma migrate deploy` -## [0.2.0] - 2026-03-26 +### Fixed +- Groq API endpoint: `https://api.groq.com/openai/v1/chat/completions` +- Ollama baseUrl no longer leaks to other providers +- Database schema references corrected + +## [0.0.1] - 2026-03-26 + +Initial release ### Added - **Task System**: AI journal generation now creates tasks that track: diff --git a/android/README.md b/android/README.md index dd392b5..f9d486b 100644 --- a/android/README.md +++ b/android/README.md @@ -1,6 +1,6 @@ # Android App -Native Android app using Kotlin and Jetpack Compose that connects to the same TotalRecall API. +Native Android app using Kotlin and Jetpack Compose that connects to the same DearDiary API. ## Requirements @@ -49,7 +49,7 @@ buildConfigField("String", "API_BASE_URL", "\"http://your-server:3000/api/v1/\"" ``` android/ -├── app/src/main/java/com/totalrecall/ +├── app/src/main/java/com/deardiary/ │ ├── api/ # API client │ ├── model/ # Data models │ ├── repository/ # Repository pattern diff --git a/android/app/src/main/java/com/deardiary/MainActivity.kt b/android/app/src/main/java/com/deardiary/MainActivity.kt index 2d82b36..c7aa276 100644 --- a/android/app/src/main/java/com/deardiary/MainActivity.kt +++ b/android/app/src/main/java/com/deardiary/MainActivity.kt @@ -9,14 +9,14 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier import com.deardiary.ui.AppNavigation -import com.deardiary.ui.TotalRecallTheme +import com.deardiary.ui.DearDiaryTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { - TotalRecallTheme { + DearDiaryTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background diff --git a/android/app/src/main/java/com/deardiary/repository/Repository.kt b/android/app/src/main/java/com/deardiary/repository/Repository.kt index 9990e2c..ea97f49 100644 --- a/android/app/src/main/java/com/deardiary/repository/Repository.kt +++ b/android/app/src/main/java/com/deardiary/repository/Repository.kt @@ -8,7 +8,7 @@ import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.deardiary.api.* -private val Context.dataStore: DataStore by preferencesDataStore(name = "totalrecall") +private val Context.dataStore: DataStore by preferencesDataStore(name = "deardiary") class Repository(context: Context, private val baseUrl: String) { private val api = ApiClient(baseUrl) diff --git a/android/app/src/main/java/com/deardiary/ui/Theme.kt b/android/app/src/main/java/com/deardiary/ui/Theme.kt index 2581083..19186d1 100644 --- a/android/app/src/main/java/com/deardiary/ui/Theme.kt +++ b/android/app/src/main/java/com/deardiary/ui/Theme.kt @@ -18,7 +18,7 @@ private val DarkColorScheme = darkColorScheme( ) @Composable -fun TotalRecallTheme( +fun DearDiaryTheme( content: @Composable () -> Unit ) { MaterialTheme( diff --git a/android/app/src/main/java/com/deardiary/ui/auth/AuthScreen.kt b/android/app/src/main/java/com/deardiary/ui/auth/AuthScreen.kt index 80e5897..d934a5b 100644 --- a/android/app/src/main/java/com/deardiary/ui/auth/AuthScreen.kt +++ b/android/app/src/main/java/com/deardiary/ui/auth/AuthScreen.kt @@ -36,7 +36,7 @@ fun AuthScreen( verticalArrangement = Arrangement.Center ) { Text( - text = "TotalRecall", + text = "DearDiary", style = MaterialTheme.typography.headlineLarge, color = MaterialTheme.colorScheme.primary ) diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index 44130a3..18ca912 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -14,5 +14,5 @@ dependencyResolutionManagement { } } -rootProject.name = "TotalRecall" +rootProject.name = "DearDiary" include(":app") diff --git a/backend/package.json b/backend/package.json index 351c46d..063ca3b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { - "name": "totalrecall-backend", - "version": "1.0.0", + "name": "deardiary-backend", + "version": "0.0.1", "type": "module", "scripts": { "dev": "bun --watch src/index.ts", diff --git a/backend/prisma/migrations/00000000000000_init.sql b/backend/prisma/migrations/00000000000000_init.sql new file mode 100644 index 0000000..39d74bc --- /dev/null +++ b/backend/prisma/migrations/00000000000000_init.sql @@ -0,0 +1,103 @@ +-- CreateInitialSchema +CREATE TABLE IF NOT EXISTS "User" ( + "id" TEXT NOT NULL PRIMARY KEY, + "email" TEXT NOT NULL, + "passwordHash" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS "User_email_key" ON "User"("email"); + +CREATE TABLE IF NOT EXISTS "ApiKey" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "keyHash" TEXT NOT NULL, + "name" TEXT NOT NULL, + "lastUsedAt" DATETIME, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "ApiKey_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX IF NOT EXISTS "ApiKey_userId_key" ON "ApiKey"("userId"); +CREATE UNIQUE INDEX IF NOT EXISTS "ApiKey_keyHash_key" ON "ApiKey"("keyHash"); + +CREATE TABLE IF NOT EXISTS "Entry" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "date" TEXT NOT NULL, + "type" TEXT NOT NULL, + "content" TEXT NOT NULL, + "mediaPath" TEXT, + "metadata" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Entry_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX IF NOT EXISTS "Entry_userId_date_key" ON "Entry"("userId", "date"); +CREATE INDEX IF NOT EXISTS "Entry_date_key" ON "Entry"("date"); + +CREATE TABLE IF NOT EXISTS "Journal" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "date" TEXT NOT NULL, + "content" TEXT NOT NULL, + "entryCount" INTEGER NOT NULL, + "generatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "Journal_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX IF NOT EXISTS "Journal_userId_date_key" ON "Journal"("userId", "date"); +CREATE INDEX IF NOT EXISTS "Journal_userId_key" ON "Journal"("userId"); + +CREATE TABLE IF NOT EXISTS "Task" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "journalId" TEXT NOT NULL, + "type" TEXT NOT NULL DEFAULT 'journal_generate', + "status" TEXT NOT NULL DEFAULT 'pending', + "provider" TEXT NOT NULL, + "model" TEXT, + "prompt" TEXT NOT NULL, + "request" TEXT, + "response" TEXT, + "error" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "completedAt" DATETIME, + CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Task_journalId_fkey" FOREIGN KEY ("journalId") REFERENCES "Journal" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX IF NOT EXISTS "Task_userId_key" ON "Task"("userId"); +CREATE INDEX IF NOT EXISTS "Task_journalId_key" ON "Task"("journalId"); + +CREATE TABLE IF NOT EXISTS "Settings" ( + "userId" TEXT NOT NULL PRIMARY KEY, + "aiProvider" TEXT NOT NULL DEFAULT 'groq', + "aiApiKey" TEXT, + "aiModel" TEXT NOT NULL DEFAULT 'llama-3.3-70b-versatile', + "aiBaseUrl" TEXT, + "journalPrompt" TEXT NOT NULL DEFAULT 'You are a factual diary summarizer. Your ONLY job is to summarize the entries provided to you - nothing more.\n\nCRITICAL RULES:\n1. ONLY use information explicitly stated in the entries below\n2. NEVER invent, assume, or hallucinate any detail not in the entries\n3. NEVER add activities, emotions, weather, or context not directly mentioned\n4. If something is unclear in the entries, simply state what IS clear\n5. Keep the summary grounded and factual - no embellishment\n6. Do not write in an overly creative or story-telling style\n7. Only reference what the user explicitly recorded\n\nStructure:\n- Start with what was recorded (meetings, tasks, activities)\n- Note any explicit feelings or observations mentioned\n- Keep it concise and factual\n- If there are gaps in the day, acknowledge only what was recorded', + "language" TEXT NOT NULL DEFAULT 'en', + "timezone" TEXT NOT NULL DEFAULT 'UTC', + "providerSettings" TEXT, + "journalContextDays" INTEGER NOT NULL DEFAULT 10, + CONSTRAINT "Settings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- Create _prisma_migrations table for Prisma migrate tracking +CREATE TABLE IF NOT EXISTS "_prisma_migrations" ( + "id" TEXT NOT NULL PRIMARY KEY, + "checksum" TEXT NOT NULL, + "finished_at" DATETIME, + "migration_name" TEXT NOT NULL, + "logs" TEXT, + "rolled_back_at" DATETIME, + "started_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "applied_steps_count" INTEGER NOT NULL DEFAULT 0 +); + +-- Record this migration +INSERT INTO "_prisma_migrations" ("id", "checksum", "finished_at", "migration_name", "applied_steps_count") +VALUES (lower(hex(randomblob(16))), 'init', datetime('now'), '00000000000000_init', 1); diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index c86f037..62c7385 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -89,13 +89,16 @@ model Task { } model Settings { - userId String @id - aiProvider String @default("openai") - aiApiKey String? - aiModel String @default("gpt-4") - aiBaseUrl String? - journalPrompt String @default("You are a factual diary summarizer. Your ONLY job is to summarize the entries provided to you - nothing more.\n\nCRITICAL RULES:\n1. ONLY use information explicitly stated in the entries below\n2. NEVER invent, assume, or hallucinate any detail not in the entries\n3. NEVER add activities, emotions, weather, or context not directly mentioned\n4. If something is unclear in the entries, simply state what IS clear\n5. Keep the summary grounded and factual - no embellishment\n6. Do not write in an overly creative or story-telling style\n7. Only reference what the user explicitly recorded\n\nStructure:\n- Start with what was recorded (meetings, tasks, activities)\n- Note any explicit feelings or observations mentioned\n- Keep it concise and factual\n- If there are gaps in the day, acknowledge only what was recorded") - language String @default("en") + userId String @id + aiProvider String @default("groq") + aiApiKey String? + aiModel String @default("llama-3.3-70b-versatile") + aiBaseUrl String? + journalPrompt String @default("You are a factual diary summarizer. Your ONLY job is to summarize the entries provided to you - nothing more.\n\nCRITICAL RULES:\n1. ONLY use information explicitly stated in the entries below\n2. NEVER invent, assume, or hallucinate any detail not in the entries\n3. NEVER add activities, emotions, weather, or context not directly mentioned\n4. If something is unclear in the entries, simply state what IS clear\n5. Keep the summary grounded and factual - no embellishment\n6. Do not write in an overly creative or story-telling style\n7. Only reference what the user explicitly recorded\n\nStructure:\n- Start with what was recorded (meetings, tasks, activities)\n- Note any explicit feelings or observations mentioned\n- Keep it concise and factual\n- If there are gaps in the day, acknowledge only what was recorded") + language String @default("en") + timezone String @default("UTC") + providerSettings String? + journalContextDays Int @default(10) user User @relation(fields: [userId], references: [id], onDelete: Cascade) } diff --git a/backend/src/index.ts b/backend/src/index.ts index d43478d..ed6d751 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -7,6 +7,7 @@ import bcrypt from 'bcryptjs'; import { createHash, randomBytes } from 'crypto'; import * as jose from 'jose'; import { Prisma } from '@prisma/client'; +import { createAIProvider } from './services/ai/provider'; const app = new Hono(); @@ -19,7 +20,7 @@ app.use('*', cors({ })); const prisma = new PrismaClient({ - datasourceUrl: envVars.DATABASE_URL || 'file:./data/totalrecall.db', + datasourceUrl: envVars.DATABASE_URL || 'file:./data/deardiary.db', }); app.use('*', async (c, next) => { @@ -285,9 +286,9 @@ app.post('/api/v1/journal/generate/:date', async (c) => { return c.json({ data: null, error: { code: 'NO_ENTRIES', message: 'No entries found for this date' } }, 400); } - const provider = settings?.aiProvider || 'openai'; + const provider = settings?.aiProvider || 'groq'; - if ((provider === 'openai' || provider === 'anthropic') && !settings?.aiApiKey) { + if ((provider === 'openai' || provider === 'anthropic' || provider === 'groq') && !settings?.aiApiKey) { return c.json({ data: null, error: { code: 'NO_AI_CONFIG', message: 'AI not configured. Please set up your API key in settings.' } }, 400); } @@ -303,14 +304,43 @@ app.post('/api/v1/journal/generate/:date', async (c) => { return text; }).join('\n\n'); + // Get previous journals for context + const contextDays = settings?.journalContextDays || 0; + let previousJournalsText = ''; + + if (contextDays > 0) { + const contextStartDate = new Date(date); + contextStartDate.setDate(contextStartDate.getDate() - contextDays); + + const previousJournals = await prisma.journal.findMany({ + where: { + userId, + date: { + gte: contextStartDate.toISOString().split('T')[0], + lt: date, + }, + }, + orderBy: { date: 'desc' }, + select: { date: true, content: true, generatedAt: true }, + }); + + if (previousJournals.length > 0) { + previousJournalsText = `\n\nPREVIOUS JOURNAL SUMMARY (last ${contextDays} days for context):\n${previousJournals.map(j => + `[${j.date}]\n${j.content}` + ).join('\n\n')}\n`; + } + } + const systemPrompt = settings?.journalPrompt || 'You are a thoughtful journal writer.'; - const userPrompt = `The following entries were captured throughout the day (${date}). Write a thoughtful, reflective journal entry. + const userPrompt = `${previousJournalsText}The following entries were captured throughout the day (${date}). Write a thoughtful, reflective journal entry. ENTRIES: ${entriesText} JOURNAL:`; + console.log(`[Journal Generate] Date: ${date}, Context days: ${contextDays}, Entries: ${entries.length}`); + // Create placeholder journal and task const placeholderJournal = await prisma.journal.create({ data: { userId, date, content: 'Generating...', entryCount: entries.length }, @@ -324,7 +354,8 @@ JOURNAL:`; status: 'pending', provider, model: settings?.aiModel, - prompt: `${systemPrompt}\n\n${userPrompt}`, + prompt: userPrompt, + request: systemPrompt, }, }); @@ -334,128 +365,34 @@ JOURNAL:`; data: { id: placeholderJournal.id }, }); - let requestBody: any = null; - let responseBody: any = null; let content = ''; try { - if (provider === 'openai') { - requestBody = { - model: settings?.aiModel || 'gpt-4', - messages: [ - { role: 'system', content: systemPrompt }, - { role: 'user', content: userPrompt } - ], - temperature: 0.7, - max_tokens: 2000, - }; - - const response = await fetch('https://api.openai.com/v1/chat/completions', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${settings?.aiApiKey}`, - }, - body: JSON.stringify(requestBody), - }); - - responseBody = await response.json(); - - if (!response.ok) { - throw new Error(`OpenAI API error: ${response.status} ${JSON.stringify(responseBody)}`); - } - - content = responseBody.choices?.[0]?.message?.content || ''; - - } else if (provider === 'anthropic') { - requestBody = { - model: settings?.aiModel || 'claude-3-sonnet-20240229', - max_tokens: 2000, - system: systemPrompt, - messages: [{ role: 'user', content: userPrompt }], - }; - - const response = await fetch('https://api.anthropic.com/v1/messages', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': settings?.aiApiKey, - 'anthropic-version': '2023-06-01', - }, - body: JSON.stringify(requestBody), - }); - - responseBody = await response.json(); - - if (!response.ok) { - throw new Error(`Anthropic API error: ${response.status} ${JSON.stringify(responseBody)}`); - } - - content = responseBody.content?.[0]?.text || ''; - - } else if (provider === 'ollama') { - const baseUrl = settings?.aiBaseUrl || 'http://localhost:11434'; - requestBody = { - model: settings?.aiModel || 'llama3.2', - stream: false, - messages: [ - { role: 'system', content: systemPrompt }, - { role: 'user', content: userPrompt } - ], - }; - - const response = await fetch(`${baseUrl}/api/chat`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(requestBody), - }); - - responseBody = await response.json(); - - if (!response.ok) { - throw new Error(`Ollama API error: ${response.status} ${JSON.stringify(responseBody)}`); - } - - content = responseBody.message?.content || ''; - - } else if (provider === 'lmstudio') { - const baseUrl = settings?.aiBaseUrl || 'http://localhost:1234/v1'; - requestBody = { - model: settings?.aiModel || 'local-model', - messages: [ - { role: 'system', content: systemPrompt }, - { role: 'user', content: userPrompt } - ], - temperature: 0.7, - max_tokens: 2000, - }; - - const response = await fetch(`${baseUrl}/chat/completions`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(requestBody), - }); - - responseBody = await response.json(); - - if (!response.ok) { - throw new Error(`LM Studio API error: ${response.status} ${JSON.stringify(responseBody)}`); - } - - content = responseBody.choices?.[0]?.message?.content || ''; - } + console.log(`[Journal Generate] Using provider: ${provider}`); + + const aiProvider = createAIProvider({ + provider: provider as 'openai' | 'anthropic' | 'ollama' | 'lmstudio' | 'groq', + apiKey: settings?.aiApiKey || '', + model: settings?.aiModel || undefined, + baseUrl: (provider === 'ollama' || provider === 'lmstudio') ? settings?.aiBaseUrl || undefined : undefined, + }); + + console.log(`[Journal Generate] AI Provider created: ${aiProvider.provider}`); + + content = await aiProvider.generate(userPrompt, systemPrompt); if (!content) { throw new Error('No content generated from AI'); } - // Update task with success + console.log(`[Journal Generate] Success! Content length: ${content.length}`); + + // Update task with success - store full prompt and response await prisma.task.update({ where: { id: task.id }, data: { status: 'completed', - request: JSON.stringify(requestBody, null, 2), - response: JSON.stringify(responseBody, null, 2), + response: content, completedAt: new Date(), }, }); @@ -554,15 +491,17 @@ app.put('/api/v1/settings', async (c) => { if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401); const body = await c.req.json(); - const { aiProvider, aiApiKey, aiModel, aiBaseUrl, journalPrompt, language } = body; + const { aiProvider, aiApiKey, aiModel, aiBaseUrl, journalPrompt, language, providerSettings, journalContextDays } = body; const data: Record = {}; if (aiProvider !== undefined) data.aiProvider = aiProvider; if (aiApiKey !== undefined) data.aiApiKey = aiApiKey; - if (aiModel !== undefined) data.aiModel = aiModel || 'gpt-4'; + if (aiModel !== undefined) data.aiModel = aiModel; if (aiBaseUrl !== undefined) data.aiBaseUrl = aiBaseUrl; if (journalPrompt !== undefined) data.journalPrompt = journalPrompt; if (language !== undefined) data.language = language; + if (providerSettings !== undefined) data.providerSettings = JSON.stringify(providerSettings); + if (journalContextDays !== undefined) data.journalContextDays = journalContextDays; const settings = await prisma.settings.upsert({ where: { userId }, @@ -570,9 +509,57 @@ app.put('/api/v1/settings', async (c) => { update: data, }); + if (settings.providerSettings) { + try { + (settings as any).providerSettings = JSON.parse(settings.providerSettings as string); + } catch { + (settings as any).providerSettings = {}; + } + } + return c.json({ data: settings, error: null }); }); +app.post('/api/v1/ai/test', async (c) => { + const userId = await getUserId(c); + if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401); + + const { provider, apiKey, model, baseUrl } = await c.req.json(); + + console.log(`[AI Test] Provider: ${provider}, Model: ${model || 'default'}, BaseURL: ${baseUrl || 'default'}`); + console.log(`[AI Test] API Key set: ${!!apiKey}, Length: ${apiKey?.length || 0}`); + + if (!provider) { + return c.json({ data: null, error: { code: 'VALIDATION_ERROR', message: 'provider is required' } }, 400); + } + + try { + const aiProvider = createAIProvider({ + provider, + apiKey: apiKey || '', + model: model || undefined, + baseUrl: baseUrl || undefined, + }); + + console.log(`[AI Test] Creating provider: ${aiProvider.provider}`); + + const result = await aiProvider.generate('Say "OK" if you can read this.', 'You are a test assistant. Respond with just "OK".'); + + console.log(`[AI Test] Success! Response length: ${result.length}`); + + if (result.toLowerCase().includes('ok')) { + return c.json({ data: { valid: true, message: 'Connection successful!' }, error: null }); + } else { + return c.json({ data: { valid: false }, error: { code: 'TEST_FAILED', message: 'Model responded but with unexpected output' } }); + } + } catch (err) { + const message = err instanceof Error ? err.message : 'Connection failed'; + console.error(`[AI Test] Error: ${message}`); + console.error(`[AI Test] Stack: ${err instanceof Error ? err.stack : 'N/A'}`); + return c.json({ data: { valid: false }, error: { code: 'TEST_FAILED', message } }); + } +}); + app.notFound((c) => c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Not found' } }, 404)); app.onError((err, c) => { console.error('Unhandled error:', err); diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index f660404..2a00117 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -6,6 +6,7 @@ import * as jose from 'jose'; function createAuthRoutes() { const app = new Hono(); + const authRoutes = app; authRoutes.post('/register', async (c) => { const { email, password } = await c.req.json(); @@ -113,3 +114,8 @@ authRoutes.post('/api-key', async (c) => { return c.json({ data: { apiKey, id: keyRecord.id, name: keyRecord.name }, error: null }, 201); }); + + return app; +} + +export default createAuthRoutes; diff --git a/backend/src/routes/settings.ts b/backend/src/routes/settings.ts index 57c00b6..bfbd8bb 100644 --- a/backend/src/routes/settings.ts +++ b/backend/src/routes/settings.ts @@ -31,7 +31,7 @@ settingsRoutes.put('/', async (c) => { const data: Record = {}; if (aiProvider !== undefined) data.aiProvider = aiProvider; if (aiApiKey !== undefined) data.aiApiKey = aiApiKey; - if (aiModel !== undefined) data.aiModel = aiModel || 'gpt-4'; + if (aiModel !== undefined) data.aiModel = aiModel; if (aiBaseUrl !== undefined) data.aiBaseUrl = aiBaseUrl; if (journalPrompt !== undefined) data.journalPrompt = journalPrompt; if (language !== undefined) data.language = language; @@ -67,3 +67,34 @@ settingsRoutes.post('/validate-key', async (c) => { return c.json({ data: { valid: false }, error: null }); } }); + +settingsRoutes.post('/test', async (c) => { + const body = await c.req.json(); + const { provider, apiKey, model, baseUrl } = body; + + if (!provider) { + return c.json({ data: null, error: { code: 'VALIDATION_ERROR', message: 'provider is required' } }, 400); + } + + const { createAIProvider } = await import('../services/ai/provider'); + + try { + const aiProvider = createAIProvider({ + provider, + apiKey: apiKey || '', + model: model || undefined, + baseUrl: baseUrl || undefined, + }); + + const result = await aiProvider.generate('Say "OK" if you can read this.', 'You are a test assistant. Respond with just "OK".'); + + if (result.toLowerCase().includes('ok')) { + return c.json({ data: { valid: true, message: 'Connection successful!' }, error: null }); + } else { + return c.json({ data: { valid: false }, error: { code: 'TEST_FAILED', message: 'Model responded but with unexpected output' } }); + } + } catch (err) { + const message = err instanceof Error ? err.message : 'Connection failed'; + return c.json({ data: { valid: false }, error: { code: 'TEST_FAILED', message } }); + } +}); diff --git a/backend/src/services/ai/groq.ts b/backend/src/services/ai/groq.ts new file mode 100644 index 0000000..ceb1753 --- /dev/null +++ b/backend/src/services/ai/groq.ts @@ -0,0 +1,66 @@ +import type { AIProvider, AIProviderConfig } from './provider'; + +export class GroqProvider implements AIProvider { + provider = 'groq' as const; + private apiKey: string; + private model: string; + private baseUrl: string; + + constructor(config: AIProviderConfig) { + this.apiKey = config.apiKey; + this.model = config.model || 'llama-3.3-70b-versatile'; + this.baseUrl = config.baseUrl || 'https://api.groq.com/openai/v1'; + } + + async generate(prompt: string, systemPrompt?: string): Promise { + const messages: Array<{ role: string; content: string }> = []; + + if (systemPrompt) { + messages.push({ role: 'system', content: systemPrompt }); + } + + messages.push({ role: 'user', content: prompt }); + + const response = await fetch(`${this.baseUrl}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.apiKey}`, + }, + body: JSON.stringify({ + model: this.model, + messages, + temperature: 0.7, + max_tokens: 2000, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Groq API error: ${response.status} ${error}`); + } + + const data = await response.json() as { choices: Array<{ message: { content: string } }> }; + return data.choices[0]?.message?.content || ''; + } + + async validate(): Promise { + try { + const response = await fetch(`${this.baseUrl}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.apiKey}`, + }, + body: JSON.stringify({ + model: this.model, + messages: [{ role: 'user', content: 'test' }], + max_tokens: 5, + }), + }); + return response.ok || response.status === 400; + } catch { + return false; + } + } +} diff --git a/backend/src/services/ai/provider.ts b/backend/src/services/ai/provider.ts index c4bc2f2..e4cf778 100644 --- a/backend/src/services/ai/provider.ts +++ b/backend/src/services/ai/provider.ts @@ -1,11 +1,11 @@ export interface AIProvider { - provider: 'openai' | 'anthropic' | 'ollama' | 'lmstudio'; + provider: 'openai' | 'anthropic' | 'ollama' | 'lmstudio' | 'groq'; generate(prompt: string, systemPrompt?: string): Promise; validate?(): Promise; } export interface AIProviderConfig { - provider: 'openai' | 'anthropic' | 'ollama' | 'lmstudio'; + provider: 'openai' | 'anthropic' | 'ollama' | 'lmstudio' | 'groq'; apiKey: string; model?: string; baseUrl?: string; @@ -15,6 +15,7 @@ import { OpenAIProvider } from './openai'; import { AnthropicProvider } from './anthropic'; import { OllamaProvider } from './ollama'; import { LMStudioProvider } from './lmstudio'; +import { GroqProvider } from './groq'; export function createAIProvider(config: AIProviderConfig): AIProvider { switch (config.provider) { @@ -26,6 +27,8 @@ export function createAIProvider(config: AIProviderConfig): AIProvider { return new OllamaProvider(config); case 'lmstudio': return new LMStudioProvider(config); + case 'groq': + return new GroqProvider(config); default: throw new Error(`Unknown AI provider: ${config.provider}`); } diff --git a/frontend/index.html b/frontend/index.html index c54e796..6176fa3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,7 +6,7 @@ - TotalRecall - AI Journal + DearDiary - AI Journal
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c56185e..6451bb7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,11 +1,11 @@ { - "name": "totalrecall-frontend", + "name": "deardiary-frontend", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "totalrecall-frontend", + "name": "deardiary-frontend", "version": "1.0.0", "dependencies": { "react": "^18.3.1", diff --git a/frontend/package.json b/frontend/package.json index b04f130..d4317e6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { - "name": "totalrecall-frontend", + "name": "deardiary-frontend", "private": true, - "version": "1.0.0", + "version": "0.0.1", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json index df68a0f..084f0e0 100644 --- a/frontend/public/manifest.json +++ b/frontend/public/manifest.json @@ -1,6 +1,6 @@ { - "name": "TotalRecall", - "short_name": "TotalRecall", + "name": "DearDiary", + "short_name": "DearDiary", "description": "AI-powered daily journal that captures life through multiple input methods", "start_url": "/", "display": "standalone", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 46f4582..0cdde6b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,6 +6,7 @@ import Home from './pages/Home'; import History from './pages/History'; import Day from './pages/Day'; import Journal from './pages/Journal'; +import Tasks from './pages/Tasks'; import Settings from './pages/Settings'; import { useTheme } from './lib/ThemeContext'; @@ -88,6 +89,9 @@ function App() { } /> + + } /> } /> diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 2e28bff..375b249 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -184,12 +184,19 @@ export interface Task { } export interface Settings { - aiProvider: 'openai' | 'anthropic' | 'ollama' | 'lmstudio'; + aiProvider: 'openai' | 'anthropic' | 'ollama' | 'lmstudio' | 'groq'; aiApiKey?: string; aiModel: string; aiBaseUrl?: string; journalPrompt: string; language: string; + timezone: string; + journalContextDays: number; + providerSettings?: Record; } export const api = new ApiClient(); diff --git a/frontend/src/pages/Auth.tsx b/frontend/src/pages/Auth.tsx index 4a990b4..101e0c3 100644 --- a/frontend/src/pages/Auth.tsx +++ b/frontend/src/pages/Auth.tsx @@ -44,7 +44,7 @@ export default function Auth({ onAuth }: { onAuth: () => void }) { return (
-

TotalRecall

+

DearDiary

diff --git a/frontend/src/pages/Journal.tsx b/frontend/src/pages/Journal.tsx index 46bfefc..10eae70 100644 --- a/frontend/src/pages/Journal.tsx +++ b/frontend/src/pages/Journal.tsx @@ -1,6 +1,124 @@ import { useState, useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { api, Journal, Task } from '../lib/api'; +import { useTheme } from '../lib/ThemeContext'; + +const detailsStyles = ` + details[open] .details-arrow { + transform: rotate(90deg); + } + details summary::-webkit-details-marker { + display: none; + } +`; + +function GeneratingModal({ + isOpen, + provider, + model, + step, + response +}: { + isOpen: boolean; + provider: string; + model?: string; + step: number; + response?: string; +}) { + const { resolvedTheme } = useTheme(); + + if (!isOpen) return null; + + const steps = [ + { label: 'Sending entries to AI...', done: step > 0 }, + { label: 'Waiting for response...', done: step > 1 }, + { label: 'Processing & formatting...', done: step > 2 }, + ]; + + const formatResponse = (str: string | undefined): string | null => { + if (!str) return null; + try { + const parsed = JSON.parse(str); + if (parsed.content?.[0]?.text) { + return parsed.content[0].text; + } + if (parsed.choices?.[0]?.message?.content) { + return parsed.choices[0].message.content; + } + return JSON.stringify(parsed, null, 2); + } catch { + return str; + } + }; + + const output = formatResponse(response); + + return ( + <> + +
+
+
+
+
+
+
+
+ +

Generating Journal

+ +
+ {steps.map((s, i) => ( +
+
+ {s.done ? '✓' : i + 1} +
+ + {s.label} + +
+ ))} +
+ +
+
+
+
+ +
+ + + Details (AI Output) + +
+ {output ? ( +
+                {output}
+              
+ ) : ( +

Waiting for response...

+ )} +
+
+ +
+ {provider.charAt(0).toUpperCase() + provider.slice(1)} + {model && ` • ${model}`} +
+
+
+ + ); +} export default function JournalPage() { const { date } = useParams<{ date: string }>(); @@ -8,6 +126,10 @@ export default function JournalPage() { const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); const [generating, setGenerating] = useState(false); + const [currentProvider, setCurrentProvider] = useState(''); + const [currentModel, setCurrentModel] = useState(''); + const [generatingStep, setGeneratingStep] = useState(0); + const [currentResponse, setCurrentResponse] = useState(); useEffect(() => { if (date) { @@ -36,13 +158,23 @@ export default function JournalPage() { const handleGenerate = async () => { if (!date) return; + setGenerating(true); + setGeneratingStep(1); + setCurrentProvider('ai'); + setCurrentModel(''); + setCurrentResponse(undefined); + const res = await api.generateJournal(date); + setGeneratingStep(3); if (res.data) { + setCurrentResponse(res.data.task.response); setJournal(res.data.journal); + setCurrentProvider(res.data.task.provider); + setCurrentModel(res.data.task.model || ''); setTasks(prev => [res.data!.task, ...prev]); } - setGenerating(false); + setTimeout(() => setGenerating(false), 500); }; const formatDate = (dateStr: string) => { @@ -53,104 +185,100 @@ export default function JournalPage() { if (!date) return null; return ( -
-
- ← Back to day -

Journal

-

{formatDate(date)}

-
- - {loading ? ( -
Loading...
- ) : !journal ? ( -
-

No journal generated yet

- + <> + + +
+
+ ← Back to day +

Journal

+

{formatDate(date)}

- ) : ( -
-
- Generated {new Date(journal.generatedAt).toLocaleString()} • {journal.entryCount} entries -
-
-
- {journal.content} -
-
-
+ + {loading ? ( +
Loading...
+ ) : !journal || journal.content === 'Generating...' ? ( +
+

No journal generated yet

-
- )} - - {tasks.length > 0 && ( -
-

Generation History

-
- {tasks.map(task => ( -
- -
- - - {task.provider.charAt(0).toUpperCase() + task.provider.slice(1)} - {task.model && ` - ${task.model}`} - - - {new Date(task.createdAt).toLocaleString()} - -
- - {task.status} - -
-
- {task.error && ( -
-

Error:

-
{task.error}
-
- )} - {task.request && ( -
-

Request:

-
-                        {JSON.stringify(JSON.parse(task.request), null, 2)}
-                      
-
- )} - {task.response && ( -
-

Response:

-
-                        {JSON.stringify(JSON.parse(task.response), null, 2)}
-                      
-
- )} -
-
- ))} + ) : ( +
+
+ Generated {new Date(journal.generatedAt).toLocaleString()} • {journal.entryCount} entries +
+
+
+ {journal.content} +
+
+
+ + + View Tasks + +
-
- )} -
+ )} + + {tasks.length > 0 && ( +
+
+

Generation Tasks

+ + View all → + +
+
+ {tasks.slice(0, 3).map(task => ( +
+ +
+ + + {task.provider.charAt(0).toUpperCase() + task.provider.slice(1)} + +
+ + {task.status} + +
+
+ ))} +
+
+ )} +
+ ); } diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index f91e5dc..c38066c 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -2,11 +2,43 @@ import { useState, useEffect } from 'react'; import { api, Settings } from '../lib/api'; import { useTheme } from '../lib/ThemeContext'; +interface ProviderSettings { + apiKey?: string; + model?: string; + baseUrl?: string; +} + +interface FullSettings extends Settings { + providerSettings?: Record; +} + +const DEFAULT_MODELS: Record = { + groq: 'llama-3.3-70b-versatile', + openai: 'gpt-4o', + anthropic: 'claude-3-5-sonnet-20241022', + ollama: 'llama3.2', + lmstudio: 'local-model', +}; + +const DEFAULT_BASE_URLS: Record = { + ollama: 'http://localhost:11434', + lmstudio: 'http://localhost:1234/v1', +}; + export default function SettingsPage() { - const [settings, setSettings] = useState>({}); + const [settings, setSettings] = useState({ + aiProvider: 'groq', + aiModel: 'llama-3.3-70b-versatile', + journalPrompt: '', + language: 'en', + timezone: 'UTC', + journalContextDays: 10, + }); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); + const [testing, setTesting] = useState(false); + const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null); const { theme, setTheme } = useTheme(); useEffect(() => { @@ -17,25 +49,133 @@ export default function SettingsPage() { setLoading(true); const res = await api.getSettings(); if (res.data) { - setSettings(res.data); + const data = res.data as FullSettings; + if (data.providerSettings && typeof data.providerSettings === 'string') { + try { + data.providerSettings = JSON.parse(data.providerSettings); + } catch { + data.providerSettings = {}; + } + } + if (!data.providerSettings) { + data.providerSettings = {}; + } + setSettings(data); } setLoading(false); }; + const getCurrentProviderSettings = (): ProviderSettings => { + const provider = settings.aiProvider || 'groq'; + return settings.providerSettings?.[provider] || { + apiKey: settings.aiApiKey, + model: settings.aiModel, + baseUrl: settings.aiBaseUrl, + }; + }; + + const updateProviderSettings = (updates: Partial) => { + const provider = settings.aiProvider || 'groq'; + const current = getCurrentProviderSettings(); + const newProviderSettings = { + ...settings.providerSettings, + [provider]: { ...current, ...updates }, + }; + setSettings({ + ...settings, + providerSettings: newProviderSettings, + aiProvider: provider, + }); + }; + + const handleProviderChange = (provider: string) => { + const currentProvider = settings.aiProvider || 'groq'; + const currentSettings = getCurrentProviderSettings(); + + const newProviderSettings = { + ...settings.providerSettings, + [currentProvider]: currentSettings, + }; + + const newProviderSettingsData = newProviderSettings[provider] || {}; + + setSettings({ + ...settings, + aiProvider: provider as FullSettings['aiProvider'], + aiApiKey: newProviderSettingsData.apiKey, + aiModel: newProviderSettingsData.model || DEFAULT_MODELS[provider] || '', + aiBaseUrl: newProviderSettingsData.baseUrl, + providerSettings: newProviderSettings, + }); + setTestResult(null); + }; + const handleSave = async () => { setSaving(true); setSaved(false); - await api.updateSettings(settings); + + const provider = settings.aiProvider || 'groq'; + const currentSettings = getCurrentProviderSettings(); + const newProviderSettings = { + ...settings.providerSettings, + [provider]: currentSettings, + }; + + await api.updateSettings({ + ...settings, + providerSettings: newProviderSettings, + }); setSaving(false); setSaved(true); setTimeout(() => setSaved(false), 2000); }; + const handleTest = async () => { + setTesting(true); + setTestResult(null); + + const currentSettings = getCurrentProviderSettings(); + const provider = settings.aiProvider || 'groq'; + + try { + const response = await fetch('/api/v1/ai/test', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('apiKey')}`, + }, + body: JSON.stringify({ + provider, + apiKey: currentSettings.apiKey, + model: currentSettings.model || DEFAULT_MODELS[provider], + baseUrl: (provider === 'ollama' || provider === 'lmstudio') + ? (currentSettings.baseUrl || DEFAULT_BASE_URLS[provider]) + : undefined, + }), + }); + + const data = await response.json(); + + if (data.data?.valid) { + setTestResult({ success: true, message: 'Connection successful!' }); + } else { + setTestResult({ success: false, message: data.error?.message || 'Connection failed' }); + } + } catch (err) { + setTestResult({ success: false, message: 'Connection failed: ' + (err instanceof Error ? err.message : 'Unknown error') }); + } + + setTesting(false); + }; + const handleLogout = () => { api.clearApiKey(); window.location.href = '/login'; }; + const currentProviderSettings = getCurrentProviderSettings(); + const provider = settings.aiProvider || 'groq'; + if (loading) { return (
@@ -96,10 +236,11 @@ export default function SettingsPage() {
- {(settings.aiProvider === 'openai' || settings.aiProvider === 'anthropic') && ( + {(provider === 'openai' || provider === 'anthropic' || provider === 'groq') && (
setSettings({ ...settings, aiApiKey: e.target.value })} - placeholder="sk-..." + value={currentProviderSettings.apiKey || ''} + onChange={(e) => updateProviderSettings({ apiKey: e.target.value })} + placeholder={provider === 'groq' ? 'gsk_...' : 'sk-...'} className="w-full px-4 py-2 bg-slate-800 rounded-lg border border-slate-700 focus:border-blue-500 focus:outline-none" />
)} - {(settings.aiProvider === 'ollama' || settings.aiProvider === 'lmstudio') && ( + {(provider === 'ollama' || provider === 'lmstudio') && (
setSettings({ ...settings, aiBaseUrl: e.target.value })} - placeholder={settings.aiProvider === 'ollama' ? 'http://localhost:11434' : 'http://localhost:1234/v1'} + value={currentProviderSettings.baseUrl || ''} + onChange={(e) => updateProviderSettings({ baseUrl: e.target.value })} + placeholder={DEFAULT_BASE_URLS[provider]} className="w-full px-4 py-2 bg-slate-800 rounded-lg border border-slate-700 focus:border-blue-500 focus:outline-none" />
@@ -137,12 +278,28 @@ export default function SettingsPage() { setSettings({ ...settings, aiModel: e.target.value })} - placeholder={settings.aiProvider === 'openai' ? 'gpt-4' : settings.aiProvider === 'anthropic' ? 'claude-3-sonnet-20240229' : 'llama3.2'} + value={currentProviderSettings.model || ''} + onChange={(e) => updateProviderSettings({ model: e.target.value })} + placeholder={DEFAULT_MODELS[provider]} className="w-full px-4 py-2 bg-slate-800 rounded-lg border border-slate-700 focus:border-blue-500 focus:outline-none" />
+ +
+ + + {testResult && ( + + {testResult.message} + + )} +
@@ -176,6 +333,53 @@ export default function SettingsPage() {
+ +
+ + +
+ +
+ + +

AI will see recent journal summaries to maintain context

+
diff --git a/frontend/src/pages/Tasks.tsx b/frontend/src/pages/Tasks.tsx new file mode 100644 index 0000000..c012dc8 --- /dev/null +++ b/frontend/src/pages/Tasks.tsx @@ -0,0 +1,171 @@ +import { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { api, Task } from '../lib/api'; + +export default function TasksPage() { + const { date } = useParams<{ date: string }>(); + const navigate = useNavigate(); + const [tasks, setTasks] = useState([]); + const [loading, setLoading] = useState(true); + const [expandedTask, setExpandedTask] = useState(null); + + useEffect(() => { + if (date) { + loadTasks(); + } + }, [date]); + + const loadTasks = async () => { + if (!date) return; + setLoading(true); + const res = await api.getJournalTasks(date); + if (res.data) { + setTasks(res.data); + } + setLoading(false); + }; + + const formatDate = (dateStr: string) => { + const d = new Date(dateStr + 'T12:00:00'); + return d.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }); + }; + + const formatTime = (dateStr: string) => { + return new Date(dateStr).toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + }; + + const prettyJson = (str: string | undefined) => { + if (!str) return null; + try { + return JSON.stringify(JSON.parse(str), null, 2); + } catch { + return str; + } + }; + + if (!date) return null; + + return ( +
+
+ +

Generation Tasks

+

{formatDate(date)}

+
+ + {loading ? ( +
Loading...
+ ) : tasks.length === 0 ? ( +
+

No tasks yet

+ +
+ ) : ( +
+ {tasks.map(task => ( +
+ + + {expandedTask === task.id && ( +
+ {task.error && ( +
+

Error

+
{task.error}
+
+ )} + + {task.prompt && ( +
+

System Prompt

+
+
{task.prompt}
+
+
+ )} + + {task.request && ( +
+

Request

+
+
+                          {prettyJson(task.request)}
+                        
+
+
+ )} + + {task.response && ( +
+

Response

+
+ + View full response + +
+                          {prettyJson(task.response)}
+                        
+
+
+ )} + + {!task.error && !task.request && !task.response && ( +

No details available

+ )} +
+ )} +
+ ))} +
+ )} +
+ ); +} diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo index b77ef49..72702b7 100644 --- a/frontend/tsconfig.tsbuildinfo +++ b/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/EntryInput.tsx","./src/components/EntryList.tsx","./src/lib/ThemeContext.tsx","./src/lib/api.ts","./src/pages/Auth.tsx","./src/pages/Day.tsx","./src/pages/History.tsx","./src/pages/Home.tsx","./src/pages/Journal.tsx","./src/pages/Settings.tsx"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/EntryInput.tsx","./src/components/EntryList.tsx","./src/lib/ThemeContext.tsx","./src/lib/api.ts","./src/pages/Auth.tsx","./src/pages/Day.tsx","./src/pages/History.tsx","./src/pages/Home.tsx","./src/pages/Journal.tsx","./src/pages/Settings.tsx","./src/pages/Tasks.tsx"],"version":"5.9.3"} \ No newline at end of file diff --git a/start.sh b/start.sh index d24d506..8dc8ea1 100644 --- a/start.sh +++ b/start.sh @@ -1,8 +1,40 @@ #!/bin/sh set -e -echo "Running database migrations..." -bunx prisma db push --accept-data-loss +# Check if database exists and has data +if [ -f /data/deardiary.db ]; then + # Database exists - ensure migration tracking exists + bun -e " + const { PrismaClient } = require('@prisma/client'); + const prisma = new PrismaClient({ datasourceUrl: 'file:/data/deardiary.db' }); + + async function main() { + try { + const tables = await prisma.\$queryRaw\`SELECT name FROM sqlite_master WHERE type='table' AND name='_prisma_migrations'\`; + if (tables.length === 0) { + await prisma.\$executeRaw\` + CREATE TABLE _prisma_migrations ( + id TEXT PRIMARY KEY, + checksum TEXT NOT NULL, + finished_at DATETIME, + migration_name TEXT NOT NULL, + logs TEXT, + rolled_back_at DATETIME, + started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + applied_steps_count INTEGER NOT NULL DEFAULT 0 + ) + \`; + await prisma.\$executeRaw\`INSERT INTO _prisma_migrations (id, checksum, finished_at, migration_name, applied_steps_count) VALUES (lower(hex(randomblob(16))), 'baseline', datetime('now'), '00000000000000_init', 1)\`; + console.log('Migration table created'); + } + } catch (e) { + console.log('Migration check done'); + } + await prisma.\$disconnect(); + } + main(); + " +fi echo "Starting server..." nginx -g 'daemon off;' &