feat: terminology fix (Entry→Event), diary page generation, settings refactor

- Rename Entry→Event throughout frontend and backend
- Change 'journal' terminology to 'diary page'
- Add professional footer with links
- Redirect to diary page after generation
- Error handling for generation failures
- Fix settings to store per-provider configs in providerSettings
- Backend reads API key from providerSettings
- Use prisma db push instead of migrate for schema sync
- Clean up duplicate entries.ts file
This commit is contained in:
lotherk
2026-03-26 23:10:33 +00:00
parent 754fea73c6
commit deaf496a7d
18 changed files with 289 additions and 256 deletions

View File

@@ -19,10 +19,10 @@ CREATE TABLE IF NOT EXISTS "ApiKey" (
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 INDEX IF NOT EXISTS "ApiKey_userId_key" ON "ApiKey"("userId");
CREATE TABLE IF NOT EXISTS "Entry" (
CREATE TABLE IF NOT EXISTS "Event" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"date" TEXT NOT NULL,
@@ -32,18 +32,18 @@ CREATE TABLE IF NOT EXISTS "Entry" (
"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
CONSTRAINT "Event_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 INDEX IF NOT EXISTS "Event_userId_date_key" ON "Event"("userId", "date");
CREATE INDEX IF NOT EXISTS "Event_date_key" ON "Event"("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,
"eventCount" 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
);
@@ -86,7 +86,6 @@ CREATE TABLE IF NOT EXISTS "Settings" (
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,
@@ -98,6 +97,5 @@ CREATE TABLE IF NOT EXISTS "_prisma_migrations" (
"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);

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "sqlite"

View File

@@ -15,7 +15,7 @@ model User {
updatedAt DateTime @updatedAt
apiKeys ApiKey[]
entries Entry[]
events Event[]
journals Journal[]
tasks Task[]
settings Settings?
@@ -34,7 +34,7 @@ model ApiKey {
@@index([userId])
}
model Entry {
model Event {
id String @id @default(uuid())
userId String
date String
@@ -56,7 +56,7 @@ model Journal {
userId String
date String
content String
entryCount Int
eventCount Int
generatedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)

View File

@@ -147,7 +147,7 @@ app.get('/api/v1/days', async (c) => {
const userId = await getUserId(c);
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
const days = await prisma.entry.groupBy({
const days = await prisma.event.groupBy({
by: ['date'],
where: { userId },
_count: { id: true },
@@ -163,7 +163,7 @@ app.get('/api/v1/days', async (c) => {
const result = days.map(day => ({
date: day.date,
entryCount: day._count.id,
eventCount: day._count.id,
hasJournal: journalMap.has(day.date),
journalGeneratedAt: journalMap.get(day.date)?.generatedAt,
}));
@@ -181,12 +181,12 @@ app.get('/api/v1/days/:date', async (c) => {
return c.json({ data: null, error: { code: 'VALIDATION_ERROR', message: 'Invalid date format. Use YYYY-MM-DD' } }, 400);
}
const [entries, journal] = await Promise.all([
prisma.entry.findMany({ where: { userId, date }, orderBy: { createdAt: 'asc' } }),
const [events, journal] = await Promise.all([
prisma.event.findMany({ where: { userId, date }, orderBy: { createdAt: 'asc' } }),
prisma.journal.findFirst({ where: { userId, date } }),
]);
return c.json({ data: { date, entries, journal }, error: null });
return c.json({ data: { date, events, journal }, error: null });
});
app.delete('/api/v1/days/:date', async (c) => {
@@ -196,15 +196,31 @@ app.delete('/api/v1/days/:date', async (c) => {
const { date } = c.req.param();
await prisma.$transaction([
prisma.entry.deleteMany({ where: { userId, date } }),
prisma.event.deleteMany({ where: { userId, date } }),
prisma.journal.deleteMany({ where: { userId, date } }),
]);
return c.json({ data: { deleted: true }, error: null });
});
// Entries routes
app.post('/api/v1/entries', async (c) => {
// Delete journal only (keeps events)
app.delete('/api/v1/journal/:date', async (c) => {
const userId = await getUserId(c);
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
const { date } = c.req.param();
const journal = await prisma.journal.findFirst({ where: { userId, date } });
if (!journal) {
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Journal not found' } }, 404);
}
await prisma.journal.delete({ where: { id: journal.id } });
return c.json({ data: { deleted: true }, error: null });
});
// Events routes
app.post('/api/v1/events', async (c) => {
const userId = await getUserId(c);
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
@@ -214,45 +230,45 @@ app.post('/api/v1/entries', async (c) => {
return c.json({ data: null, error: { code: 'VALIDATION_ERROR', message: 'date, type, and content are required' } }, 400);
}
const validTypes = ['text', 'voice', 'photo', 'health', 'location'];
const validTypes = ['event'];
if (!validTypes.includes(type)) {
return c.json({ data: null, error: { code: 'VALIDATION_ERROR', message: `type must be one of: ${validTypes.join(', ')}` } }, 400);
}
const entry = await prisma.entry.create({
const event = await prisma.event.create({
data: { userId, date, type, content, metadata: metadata ? JSON.stringify(metadata) : null },
});
return c.json({ data: entry, error: null }, 201);
return c.json({ data: event, error: null }, 201);
});
app.get('/api/v1/entries/:id', async (c) => {
app.get('/api/v1/events/:id', async (c) => {
const userId = await getUserId(c);
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
const { id } = c.req.param();
const entry = await prisma.entry.findFirst({ where: { id, userId } });
const event = await prisma.event.findFirst({ where: { id, userId } });
if (!entry) return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Entry not found' } }, 404);
return c.json({ data: entry, error: null });
if (!event) return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Event not found' } }, 404);
return c.json({ data: event, error: null });
});
app.put('/api/v1/entries/:id', async (c) => {
app.put('/api/v1/events/:id', async (c) => {
const userId = await getUserId(c);
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
const { id } = c.req.param();
const { content, metadata } = await c.req.json();
const existing = await prisma.entry.findFirst({ where: { id, userId } });
if (!existing) return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Entry not found' } }, 404);
const existing = await prisma.event.findFirst({ where: { id, userId } });
if (!existing) return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Event not found' } }, 404);
const journal = await prisma.journal.findFirst({ where: { userId, date: existing.date } });
if (journal) {
return c.json({ data: null, error: { code: 'ENTRY_IMMUTABLE', message: 'Cannot edit entry: journal already generated. Delete the journal first.' } }, 400);
return c.json({ data: null, error: { code: 'EVENT_IMMUTABLE', message: 'Cannot edit event: journal already generated. Delete the journal first.' } }, 400);
}
const entry = await prisma.entry.update({
const event = await prisma.event.update({
where: { id },
data: {
content: content ?? existing.content,
@@ -260,23 +276,23 @@ app.put('/api/v1/entries/:id', async (c) => {
},
});
return c.json({ data: entry, error: null });
return c.json({ data: event, error: null });
});
app.delete('/api/v1/entries/:id', async (c) => {
app.delete('/api/v1/events/:id', async (c) => {
const userId = await getUserId(c);
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
const { id } = c.req.param();
const existing = await prisma.entry.findFirst({ where: { id, userId } });
if (!existing) return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Entry not found' } }, 404);
const existing = await prisma.event.findFirst({ where: { id, userId } });
if (!existing) return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Event not found' } }, 404);
const journal = await prisma.journal.findFirst({ where: { userId, date: existing.date } });
if (journal) {
return c.json({ data: null, error: { code: 'ENTRY_IMMUTABLE', message: 'Cannot delete entry: journal already generated. Delete the journal first.' } }, 400);
return c.json({ data: null, error: { code: 'EVENT_IMMUTABLE', message: 'Cannot delete event: journal already generated. Delete the journal first.' } }, 400);
}
await prisma.entry.delete({ where: { id } });
await prisma.event.delete({ where: { id } });
return c.json({ data: { deleted: true }, error: null });
});
@@ -287,27 +303,30 @@ app.post('/api/v1/journal/generate/:date', async (c) => {
const { date } = c.req.param();
const [entries, settings] = await Promise.all([
prisma.entry.findMany({ where: { userId, date }, orderBy: { createdAt: 'asc' } }),
const [events, settings] = await Promise.all([
prisma.event.findMany({ where: { userId, date }, orderBy: { createdAt: 'asc' } }),
prisma.settings.findUnique({ where: { userId } }),
]);
if (entries.length === 0) {
return c.json({ data: null, error: { code: 'NO_ENTRIES', message: 'No entries found for this date' } }, 400);
if (events.length === 0) {
return c.json({ data: null, error: { code: 'NO_EVENTS', message: 'No events found for this date' } }, 400);
}
const provider = settings?.aiProvider || 'groq';
const providerSettings = settings?.providerSettings ? JSON.parse(settings.providerSettings) : {};
const providerConfig = providerSettings[provider] || {};
const apiKey = providerConfig.apiKey || settings?.aiApiKey;
if ((provider === 'openai' || provider === 'anthropic' || provider === 'groq') && !settings?.aiApiKey) {
if ((provider === 'openai' || provider === 'anthropic' || provider === 'groq') && !apiKey) {
return c.json({ data: null, error: { code: 'NO_AI_CONFIG', message: 'AI not configured. Please set up your API key in settings.' } }, 400);
}
// Build entries text
const entriesText = entries.map(entry => {
let text = `[${entry.type.toUpperCase()}] ${entry.createdAt.toISOString()}\n${entry.content}`;
if (entry.metadata) {
// Build events text
const eventsText = events.map(event => {
let text = `[EVENT] ${event.createdAt.toISOString()}\n${event.content}`;
if (event.metadata) {
try {
const meta = JSON.parse(entry.metadata);
const meta = JSON.parse(event.metadata);
if (meta.location) text += `\nLocation: ${meta.location.lat}, ${meta.location.lng}`;
if (meta.duration) text += `\nDuration: ${meta.duration}s`;
} catch {}
@@ -342,15 +361,15 @@ app.post('/api/v1/journal/generate/:date', async (c) => {
}
}
// Build prompts: 1. system prompt, 2. previous journals, 3. today's entries
// Build prompts: 1. system prompt, 2. previous journals, 3. today's events
const systemPrompt = settings?.journalPrompt || 'You are a thoughtful journal writer.';
const userPrompt = `${previousJournalsText}ENTRIES FROM TODAY (${date}):\n${entriesText}\n\nWrite a thoughtful, reflective journal entry based on the entries above.`;
const userPrompt = `${previousJournalsText}EVENTS FROM TODAY (${date}):\n${eventsText}\n\nWrite a thoughtful, reflective journal entry based on the events above.`;
console.log(`[Journal Generate] Date: ${date}, Context days: ${contextDays}, Entries: ${entries.length}`);
console.log(`[Journal Generate] Date: ${date}, Context days: ${contextDays}, Events: ${events.length}`);
// Create placeholder journal and task
const placeholderJournal = await prisma.journal.create({
data: { userId, date, content: 'Generating...', entryCount: entries.length },
data: { userId, date, content: 'Generating...', eventCount: events.length },
});
const task = await prisma.task.create({
@@ -376,11 +395,14 @@ app.post('/api/v1/journal/generate/:date', async (c) => {
try {
console.log(`[Journal Generate] Using provider: ${provider}`);
const model = providerConfig.model || settings?.aiModel;
const baseUrl = providerConfig.baseUrl || settings?.aiBaseUrl;
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,
apiKey: apiKey,
model: model || undefined,
baseUrl: (provider === 'ollama' || provider === 'lmstudio') ? baseUrl : undefined,
});
console.log(`[Journal Generate] AI Provider created: ${aiProvider.provider}`);

View File

@@ -7,7 +7,7 @@ daysRoutes.get('/', async (c) => {
const userId = c.get('userId');
const prisma = c.get('prisma');
const days = await prisma.entry.groupBy({
const days = await prisma.event.groupBy({
by: ['date'],
where: { userId },
_count: { id: true },
@@ -16,14 +16,14 @@ daysRoutes.get('/', async (c) => {
const journals = await prisma.journal.findMany({
where: { userId },
select: { date: true, generatedAt: true, entryCount: true },
select: { date: true, generatedAt: true, eventCount: true },
});
const journalMap = new Map(journals.map(j => [j.date, j]));
const result = days.map(day => ({
date: day.date,
entryCount: day._count.id,
eventCount: day._count.id,
hasJournal: journalMap.has(day.date),
journalGeneratedAt: journalMap.get(day.date)?.generatedAt,
}));
@@ -41,8 +41,8 @@ daysRoutes.get('/:date', async (c) => {
return c.json({ data: null, error: { code: 'VALIDATION_ERROR', message: 'Invalid date format. Use YYYY-MM-DD' } }, 400);
}
const [entries, journal] = await Promise.all([
prisma.entry.findMany({
const [events, journal] = await Promise.all([
prisma.event.findMany({
where: { userId, date },
orderBy: { createdAt: 'asc' },
}),
@@ -51,7 +51,7 @@ daysRoutes.get('/:date', async (c) => {
}),
]);
return c.json({ data: { date, entries, journal }, error: null });
return c.json({ data: { date, events, journal }, error: null });
});
daysRoutes.delete('/:date', async (c) => {
@@ -60,9 +60,29 @@ daysRoutes.delete('/:date', async (c) => {
const prisma = c.get('prisma');
await prisma.$transaction([
prisma.entry.deleteMany({ where: { userId, date } }),
prisma.event.deleteMany({ where: { userId, date } }),
prisma.journal.deleteMany({ where: { userId, date } }),
]);
return c.json({ data: { deleted: true }, error: null });
});
daysRoutes.delete('/:date/journal', async (c) => {
const userId = c.get('userId');
const { date } = c.req.param();
const prisma = c.get('prisma');
const journal = await prisma.journal.findUnique({
where: { userId_date: { userId, date } },
});
if (!journal) {
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Journal not found' } }, 404);
}
await prisma.journal.delete({
where: { id: journal.id },
});
return c.json({ data: { deleted: true }, error: null });
});

View File

@@ -1,9 +1,9 @@
import { Hono } from 'hono';
import { HonoEnv } from '../lib/types';
export const entriesRoutes = new Hono<HonoEnv>();
export const eventsRoutes = new Hono<HonoEnv>();
entriesRoutes.post('/', async (c) => {
eventsRoutes.post('/', async (c) => {
const userId = c.get('userId');
const prisma = c.get('prisma');
const mediaDir = c.env.MEDIA_DIR || './data/media';
@@ -15,12 +15,12 @@ entriesRoutes.post('/', async (c) => {
return c.json({ data: null, error: { code: 'VALIDATION_ERROR', message: 'date, type, and content are required' } }, 400);
}
const validTypes = ['text', 'voice', 'photo', 'health', 'location'];
const validTypes = ['event', 'text', 'photo', 'voice', 'health'];
if (!validTypes.includes(type)) {
return c.json({ data: null, error: { code: 'VALIDATION_ERROR', message: `type must be one of: ${validTypes.join(', ')}` } }, 400);
}
const entry = await prisma.entry.create({
const event = await prisma.event.create({
data: {
userId,
date,
@@ -30,26 +30,26 @@ entriesRoutes.post('/', async (c) => {
},
});
return c.json({ data: entry, error: null }, 201);
return c.json({ data: event, error: null }, 201);
});
entriesRoutes.get('/:id', async (c) => {
eventsRoutes.get('/:id', async (c) => {
const userId = c.get('userId');
const { id } = c.req.param();
const prisma = c.get('prisma');
const entry = await prisma.entry.findFirst({
const event = await prisma.event.findFirst({
where: { id, userId },
});
if (!entry) {
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Entry not found' } }, 404);
if (!event) {
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Event not found' } }, 404);
}
return c.json({ data: entry, error: null });
return c.json({ data: event, error: null });
});
entriesRoutes.put('/:id', async (c) => {
eventsRoutes.put('/:id', async (c) => {
const userId = c.get('userId');
const { id } = c.req.param();
const prisma = c.get('prisma');
@@ -57,20 +57,20 @@ entriesRoutes.put('/:id', async (c) => {
const body = await c.req.json();
const { content, metadata } = body;
const existing = await prisma.entry.findFirst({
const existing = await prisma.event.findFirst({
where: { id, userId },
});
if (!existing) {
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Entry not found' } }, 404);
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Event not found' } }, 404);
}
const journal = await prisma.journal.findFirst({ where: { userId, date: existing.date } });
if (journal) {
return c.json({ data: null, error: { code: 'ENTRY_IMMUTABLE', message: 'Cannot edit entry: journal already generated. Delete the journal first.' } }, 400);
return c.json({ data: null, error: { code: 'EVENT_IMMUTABLE', message: 'Cannot edit event: diary already generated. Delete the diary first.' } }, 400);
}
const entry = await prisma.entry.update({
const event = await prisma.event.update({
where: { id },
data: {
content: content ?? existing.content,
@@ -78,44 +78,44 @@ entriesRoutes.put('/:id', async (c) => {
},
});
return c.json({ data: entry, error: null });
return c.json({ data: event, error: null });
});
entriesRoutes.delete('/:id', async (c) => {
eventsRoutes.delete('/:id', async (c) => {
const userId = c.get('userId');
const { id } = c.req.param();
const prisma = c.get('prisma');
const existing = await prisma.entry.findFirst({
const existing = await prisma.event.findFirst({
where: { id, userId },
});
if (!existing) {
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Entry not found' } }, 404);
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Event not found' } }, 404);
}
const journal = await prisma.journal.findFirst({ where: { userId, date: existing.date } });
if (journal) {
return c.json({ data: null, error: { code: 'ENTRY_IMMUTABLE', message: 'Cannot delete entry: journal already generated. Delete the journal first.' } }, 400);
return c.json({ data: null, error: { code: 'EVENT_IMMUTABLE', message: 'Cannot delete event: diary already generated. Delete the diary first.' } }, 400);
}
await prisma.entry.delete({ where: { id } });
await prisma.event.delete({ where: { id } });
return c.json({ data: { deleted: true }, error: null });
});
entriesRoutes.post('/:id/photo', async (c) => {
eventsRoutes.post('/:id/photo', async (c) => {
const userId = c.get('userId');
const { id } = c.req.param();
const prisma = c.get('prisma');
const mediaDir = c.env.MEDIA_DIR || './data/media';
const entry = await prisma.entry.findFirst({
const event = await prisma.event.findFirst({
where: { id, userId },
});
if (!entry) {
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Entry not found' } }, 404);
if (!event) {
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Event not found' } }, 400);
}
const body = await c.req.parseBody();
@@ -127,12 +127,12 @@ entriesRoutes.post('/:id/photo', async (c) => {
const ext = file.name.split('.').pop() || 'jpg';
const fileName = `${id}.${ext}`;
const userMediaDir = `${mediaDir}/${userId}/${entry.date}`;
const userMediaDir = `${mediaDir}/${userId}/${event.date}`;
const filePath = `${userMediaDir}/${fileName}`;
await Bun.write(filePath, file);
await prisma.entry.update({
await prisma.event.update({
where: { id },
data: { mediaPath: filePath },
});
@@ -140,18 +140,18 @@ entriesRoutes.post('/:id/photo', async (c) => {
return c.json({ data: { mediaPath: filePath }, error: null }, 201);
});
entriesRoutes.post('/:id/voice', async (c) => {
eventsRoutes.post('/:id/voice', async (c) => {
const userId = c.get('userId');
const { id } = c.req.param();
const prisma = c.get('prisma');
const mediaDir = c.env.MEDIA_DIR || './data/media';
const entry = await prisma.entry.findFirst({
const event = await prisma.event.findFirst({
where: { id, userId },
});
if (!entry) {
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Entry not found' } }, 404);
if (!event) {
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Event not found' } }, 400);
}
const body = await c.req.parseBody();
@@ -162,12 +162,12 @@ entriesRoutes.post('/:id/voice', async (c) => {
}
const fileName = `${id}.webm`;
const userMediaDir = `${mediaDir}/${userId}/${entry.date}`;
const userMediaDir = `${mediaDir}/${userId}/${event.date}`;
const filePath = `${userMediaDir}/${fileName}`;
await Bun.write(filePath, file);
await prisma.entry.update({
await prisma.event.update({
where: { id },
data: { mediaPath: filePath },
});

View File

@@ -9,8 +9,8 @@ journalRoutes.post('/generate/:date', async (c) => {
const { date } = c.req.param();
const prisma = c.get('prisma');
const [entries, settings] = await Promise.all([
prisma.entry.findMany({
const [events, settings] = await Promise.all([
prisma.event.findMany({
where: { userId, date },
orderBy: { createdAt: 'asc' },
}),
@@ -19,8 +19,8 @@ journalRoutes.post('/generate/:date', async (c) => {
}),
]);
if (entries.length === 0) {
return c.json({ data: null, error: { code: 'NO_ENTRIES', message: 'No entries found for this date' } }, 400);
if (events.length === 0) {
return c.json({ data: null, error: { code: 'NO_EVENTS', message: 'No events found for this date' } }, 400);
}
if (!settings?.aiApiKey) {
@@ -34,11 +34,11 @@ journalRoutes.post('/generate/:date', async (c) => {
baseUrl: settings.aiBaseUrl,
});
const entriesText = entries.map(entry => {
let text = `[${entry.type.toUpperCase()}] ${entry.createdAt.toISOString()}\n${entry.content}`;
if (entry.metadata) {
const eventsText = events.map(event => {
let text = `[${event.type.toUpperCase()}] ${event.createdAt.toISOString()}\n${event.content}`;
if (event.metadata) {
try {
const meta = JSON.parse(entry.metadata);
const meta = JSON.parse(event.metadata);
if (meta.location) text += `\nLocation: ${meta.location.lat}, ${meta.location.lng}`;
if (meta.duration) text += `\nDuration: ${meta.duration}s`;
} catch {}
@@ -46,17 +46,17 @@ journalRoutes.post('/generate/:date', async (c) => {
return text;
}).join('\n\n');
const prompt = `The following entries were captured throughout the day (${date}). Write a thoughtful, reflective journal entry that:
const prompt = `The following events were captured throughout the day (${date}). Write a thoughtful, reflective diary page that:
1. Summarizes the key moments and activities
2. Reflects on any patterns, feelings, or insights
3. Ends with a forward-looking thought
Use a warm, personal tone. The journal should flow naturally as prose.
ENTRIES:
${entriesText}
EVENTS:
${eventsText}
JOURNAL:`;
DIARY PAGE:`;
try {
const content = await provider.generate(prompt, settings.journalPrompt);
@@ -67,11 +67,11 @@ JOURNAL:`;
userId,
date,
content,
entryCount: entries.length,
eventCount: events.length,
},
update: {
content,
entryCount: entries.length,
eventCount: events.length,
generatedAt: new Date(),
},
});