AI Coach messages bookmarking. Top bar refined.

This commit is contained in:
AG
2025-12-16 16:41:50 +02:00
parent cb0bd1a55d
commit dd027e1615
26 changed files with 2496 additions and 270 deletions

22
server/.env Normal file
View File

@@ -0,0 +1,22 @@
# Generic
# DEV
DATABASE_URL_DEV="file:./prisma/dev.db"
ADMIN_EMAIL_DEV="admin@gymflow.ai"
ADMIN_PASSWORD_DEV="admin123"
# TEST
DATABASE_URL_TEST="file:./prisma/test.db"
ADMIN_EMAIL_TEST="admin@gymflow.ai"
ADMIN_PASSWORD_TEST="admin123"
# PROD
DATABASE_URL_PROD="file:./prisma/prod.db"
ADMIN_EMAIL_PROD="admin-prod@gymflow.ai"
ADMIN_PASSWORD_PROD="secure-prod-password-change-me"
# Fallback for Prisma CLI (Migrate default)
DATABASE_URL="file:./prisma/dev.db"
GEMINI_API_KEY=AIzaSyC88SeFyFYjvSfTqgvEyr7iqLSvEhuadoE
DEFAULT_EXERCISES_CSV_PATH='default_exercises.csv'

Binary file not shown.

View File

@@ -0,0 +1,9 @@
-- CreateTable
CREATE TABLE "SavedMessage" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"content" TEXT NOT NULL,
"role" TEXT NOT NULL DEFAULT 'model',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SavedMessage_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);

View File

@@ -24,6 +24,16 @@ model User {
exercises Exercise[]
plans WorkoutPlan[]
weightRecords BodyWeightRecord[]
savedMessages SavedMessage[]
}
model SavedMessage {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
content String
role String @default("model")
createdAt DateTime @default(now())
}
model BodyWeightRecord {

Binary file not shown.

View File

@@ -0,0 +1,79 @@
import { Request, Response } from 'express';
import prisma from '../lib/prisma';
export class BookmarksController {
static async getAll(req: Request, res: Response) {
try {
const userId = (req as any).user?.userId;
if (!userId) {
return res.status(401).json({ success: false, error: 'Unauthorized' });
}
const messages = await prisma.savedMessage.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
});
return res.json({ success: true, data: messages });
} catch (error) {
console.error('Failed to get bookmarks:', error);
return res.status(500).json({ success: false, error: 'Internal server error' });
}
}
static async create(req: Request, res: Response) {
try {
const userId = (req as any).user?.userId;
if (!userId) {
return res.status(401).json({ success: false, error: 'Unauthorized' });
}
const { content, role } = req.body;
if (!content) {
return res.status(400).json({ success: false, error: 'Content is required' });
}
const message = await prisma.savedMessage.create({
data: {
userId,
content,
role: role || 'model',
},
});
return res.json({ success: true, data: message });
} catch (error) {
console.error('Failed to create bookmark:', error);
return res.status(500).json({ success: false, error: 'Internal server error' });
}
}
static async delete(req: Request, res: Response) {
try {
const userId = (req as any).user?.userId;
if (!userId) {
return res.status(401).json({ success: false, error: 'Unauthorized' });
}
const { id } = req.params;
// Verify ownership
const existing = await prisma.savedMessage.findFirst({
where: { id, userId },
});
if (!existing) {
return res.status(404).json({ success: false, error: 'Bookmark not found' });
}
await prisma.savedMessage.delete({
where: { id },
});
return res.json({ success: true });
} catch (error) {
console.error('Failed to delete bookmark:', error);
return res.status(500).json({ success: false, error: 'Internal server error' });
}
}
}

View File

@@ -11,6 +11,7 @@ import sessionRoutes from './routes/sessions';
import planRoutes from './routes/plans';
import aiRoutes from './routes/ai';
import weightRoutes from './routes/weight';
import bookmarksRoutes from './routes/bookmarks';
import bcrypt from 'bcryptjs';
import { PrismaClient } from '@prisma/client';
@@ -88,6 +89,7 @@ app.use('/api/sessions', sessionRoutes);
app.use('/api/plans', planRoutes);
app.use('/api/ai', aiRoutes);
app.use('/api/weight', weightRoutes);
app.use('/api/bookmarks', bookmarksRoutes);
app.get('/', (req, res) => {

View File

@@ -0,0 +1,13 @@
import express from 'express';
import { BookmarksController } from '../controllers/bookmarks.controller';
import { authenticateToken } from '../middleware/auth';
const router = express.Router();
router.use(authenticateToken);
router.get('/', BookmarksController.getAll);
router.post('/', BookmarksController.create);
router.delete('/:id', BookmarksController.delete);
export default router;

Binary file not shown.