diff --git a/apps/Backend/src/app.ts b/apps/Backend/src/app.ts index 317a282e..3c67e1f4 100755 --- a/apps/Backend/src/app.ts +++ b/apps/Backend/src/app.ts @@ -5,6 +5,7 @@ import { errorHandler } from "./middlewares/error.middleware"; import { apiLogger } from "./middlewares/logger.middleware"; import authRoutes from "./routes/auth"; import twilioWebhookRoutes from "./routes/twilio-webhooks"; +import greetingRoutes from "./routes/greeting"; import { authenticateJWT } from "./middlewares/auth.middleware"; import dotenv from "dotenv"; import { startBackupCron } from "./cron/backupCheck"; @@ -73,6 +74,7 @@ app.use( app.use("/uploads", express.static(path.join(process.cwd(), "uploads"))); app.use("/api/auth", authRoutes); +app.use("/api/greeting", greetingRoutes); // Twilio webhooks are public — Twilio sends no JWT token app.use("/api/twilio", express.urlencoded({ extended: false }), twilioWebhookRoutes); // All other API routes require JWT diff --git a/apps/Backend/src/routes/greeting.ts b/apps/Backend/src/routes/greeting.ts new file mode 100644 index 00000000..c0742079 --- /dev/null +++ b/apps/Backend/src/routes/greeting.ts @@ -0,0 +1,61 @@ +import express, { Request, Response } from "express"; +import { prisma as db } from "@repo/db/client"; +import { resolveAiProvider, getLlm } from "../ai/llm-factory"; + +const router = express.Router(); + +const FALLBACK_GREETINGS = [ + "How can I help you today?", + "What can I do for you today?", + "Ready when you are.", + "What's on your mind today?", + "Let's get started.", +]; + +let cachedGreeting: { text: string; date: string } | null = null; + +function todayKey() { + return new Date().toISOString().slice(0, 10); +} + +router.get("/", async (_req: Request, res: Response): Promise => { + try { + const today = todayKey(); + + if (cachedGreeting && cachedGreeting.date === today) { + return res.status(200).json({ greeting: cachedGreeting.text }); + } + + const aiSettings = await db.aiSettings.findFirst(); + const activeAi = aiSettings ? resolveAiProvider(aiSettings) : null; + + if (!activeAi) { + const fallback = FALLBACK_GREETINGS[Math.floor(Math.random() * FALLBACK_GREETINGS.length)]; + return res.status(200).json({ greeting: fallback }); + } + + const llm = getLlm(activeAi.provider, activeAi.key, activeAi.model); + const result = await llm.invoke([ + { + role: "system", + content: "Generate a single short AI greeting, similar to how ChatGPT or Claude greets users. Keep it to one short sentence. Examples of the style: 'How can I help you today?', 'What's on your mind?', 'Ready when you are.', 'What can I do for you today?'. Be warm but concise. Do not use emojis. Do not mention names. Vary each greeting.", + }, + { + role: "user", + content: "Generate a short greeting.", + }, + ]); + + const greeting = typeof result.content === "string" + ? result.content.trim() + : FALLBACK_GREETINGS[0]!; + + cachedGreeting = { text: greeting, date: today }; + return res.status(200).json({ greeting }); + } catch { + const fallback = FALLBACK_GREETINGS[Math.floor(Math.random() * FALLBACK_GREETINGS.length)]; + return res.status(200).json({ greeting: fallback }); + } +}); + +export default router; diff --git a/apps/Frontend/index.html b/apps/Frontend/index.html index b56a0a15..9089baf2 100755 --- a/apps/Frontend/index.html +++ b/apps/Frontend/index.html @@ -4,6 +4,9 @@ + + + My Dental Office Management diff --git a/apps/Frontend/src/index.css b/apps/Frontend/src/index.css index 234e7d99..3abb2aba 100755 --- a/apps/Frontend/src/index.css +++ b/apps/Frontend/src/index.css @@ -65,6 +65,43 @@ @apply font-sans antialiased bg-background text-foreground; } } +@keyframes blob-float { + 0%, 100% { + transform: translate(0, 0) scale(1); + border-radius: 40% 60% 70% 30% / 40% 50% 60% 50%; + } + 25% { + transform: translate(80px, -60px) scale(1.25); + border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%; + } + 50% { + transform: translate(-50px, 50px) scale(0.85); + border-radius: 30% 60% 40% 70% / 50% 60% 30% 60%; + } + 75% { + transform: translate(40px, 30px) scale(1.1); + border-radius: 50% 40% 60% 30% / 40% 70% 50% 60%; + } +} + +@keyframes blob-rotate { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.login-blob { + animation: blob-float 25s ease-in-out infinite; +} +.login-blob-container { + animation: blob-rotate 50s linear infinite; +} +.animation-delay-2000 { + animation-delay: 5s; +} +.animation-delay-4000 { + animation-delay: 10s; +} + .day-picker-small-scale select { @apply text-sm text-gray-900 bg-white border border-gray-300 rounded-md focus:outline-none focus:border-blue-600 focus:ring-1 focus:ring-blue-600; height: 32px; /* Fixed height: ~h-8 */ diff --git a/apps/Frontend/src/pages/auth-page.tsx b/apps/Frontend/src/pages/auth-page.tsx index 56908087..dd25f018 100755 --- a/apps/Frontend/src/pages/auth-page.tsx +++ b/apps/Frontend/src/pages/auth-page.tsx @@ -1,6 +1,6 @@ import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useAuth } from "@/hooks/use-auth"; import { Button } from "@/components/ui/button"; import { @@ -14,15 +14,49 @@ import { import { Input } from "@/components/ui/input"; import { Checkbox } from "@/components/ui/checkbox"; import { Card } from "@/components/ui/card"; -import { CheckCircle, Torus } from "lucide-react"; +import { Sparkles } from "lucide-react"; import { CheckedState } from "@radix-ui/react-checkbox"; import LoadingScreen from "@/components/ui/LoadingScreen"; import { useLocation } from "wouter"; import { LoginFormValues, loginSchema } from "@repo/db/types"; +const DAILY_PALETTES = [ + { bg: "#f8fafc", blobs: ["#a78bfa", "#38bdf8", "#f472b6", "#fb923c"], textColor: "#1e293b" }, + { bg: "#faf5ff", blobs: ["#c084fc", "#22d3ee", "#fb7185", "#facc15"], textColor: "#1e1b4b" }, + { bg: "#f0f9ff", blobs: ["#60a5fa", "#a78bfa", "#34d399", "#f97316"], textColor: "#0c4a6e" }, + { bg: "#fdf4ff", blobs: ["#e879f9", "#38bdf8", "#fbbf24", "#4ade80"], textColor: "#4a044e" }, + { bg: "#f0fdf4", blobs: ["#4ade80", "#818cf8", "#fb923c", "#f472b6"], textColor: "#052e16" }, + { bg: "#fffbeb", blobs: ["#fbbf24", "#f87171", "#a78bfa", "#22d3ee"], textColor: "#78350f" }, + { bg: "#f8fafc", blobs: ["#38bdf8", "#e879f9", "#34d399", "#fb923c"], textColor: "#0f172a" }, + { bg: "#fef2f2", blobs: ["#fb7185", "#a78bfa", "#38bdf8", "#4ade80"], textColor: "#7f1d1d" }, + { bg: "#ecfdf5", blobs: ["#34d399", "#818cf8", "#f472b6", "#facc15"], textColor: "#064e3b" }, + { bg: "#f5f3ff", blobs: ["#8b5cf6", "#f472b6", "#22d3ee", "#fb923c"], textColor: "#2e1065" }, + { bg: "#fff7ed", blobs: ["#fb923c", "#a78bfa", "#22d3ee", "#4ade80"], textColor: "#7c2d12" }, + { bg: "#f0f9ff", blobs: ["#22d3ee", "#c084fc", "#f97316", "#34d399"], textColor: "#0c4a6e" }, + { bg: "#fdf2f8", blobs: ["#f472b6", "#60a5fa", "#facc15", "#34d399"], textColor: "#831843" }, + { bg: "#f8fafc", blobs: ["#818cf8", "#f97316", "#22d3ee", "#f472b6"], textColor: "#1e293b" }, +]; + +function getDailyPalette() { + const now = new Date(); + const start = new Date(now.getFullYear(), 0, 0); + const dayOfYear = Math.floor((now.getTime() - start.getTime()) / 86400000); + return DAILY_PALETTES[dayOfYear % DAILY_PALETTES.length]!; +} + export default function AuthPage() { const { isLoading, user, loginMutation } = useAuth(); const [, navigate] = useLocation(); + const [greeting, setGreeting] = useState("How can I help you today?"); + const palette = getDailyPalette(); + + useEffect(() => { + const API_BASE_URL = import.meta.env.VITE_API_BASE_URL_BACKEND ?? ""; + fetch(`${API_BASE_URL}/api/greeting`) + .then((r) => r.json()) + .then((data) => { if (data.greeting) setGreeting(data.greeting); }) + .catch(() => {}); + }, []); const loginForm = useForm({ resolver: zodResolver(loginSchema), @@ -48,17 +82,16 @@ export default function AuthPage() { }, [user, navigate]); return ( -
-
+
+
{/* Auth Form */} - +
-

+

My Dental Office Management

-

- {" "} - Comprehensive Practice Management System +

+ Driven by multiple AI agents

@@ -132,35 +165,73 @@ export default function AuthPage() {
- {/* Hero Section */} -
-
-
- -
+ {/* Hero Section — Stripe-style vibrant gradient mesh */} +
+ {/* Animated blobs */} +
+
+
+
+
+ + {/* 4th accent blob */} +
+ + {/* Content */} +
+
+
+ +
+
+

+ {greeting} +

-

- The complete solution for dental practice management. Streamline - your patient records, appointments, and more. -

-
    -
  • - - Easily manage patient records -
  • -
  • - - Track patient insurance information -
  • -
  • - - Secure and compliant data storage -
  • -
  • - - Simple and intuitive interface -
  • -
diff --git a/apps/Frontend/tailwind.config.ts b/apps/Frontend/tailwind.config.ts index d63aa3c1..09cf593c 100755 --- a/apps/Frontend/tailwind.config.ts +++ b/apps/Frontend/tailwind.config.ts @@ -80,10 +80,22 @@ export default { height: "0", }, }, + blob: { + "0%, 100%": { transform: "translate(0, 0) scale(1)", borderRadius: "40% 60% 70% 30% / 40% 50% 60% 50%" }, + "25%": { transform: "translate(80px, -60px) scale(1.25)", borderRadius: "60% 40% 30% 70% / 60% 30% 70% 40%" }, + "50%": { transform: "translate(-50px, 50px) scale(0.85)", borderRadius: "30% 60% 40% 70% / 50% 60% 30% 60%" }, + "75%": { transform: "translate(40px, 30px) scale(1.1)", borderRadius: "50% 40% 60% 30% / 40% 70% 50% 60%" }, + }, + "blob-spin": { + "0%": { transform: "rotate(0deg)" }, + "100%": { transform: "rotate(360deg)" }, + }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", + blob: "blob 10s ease-in-out infinite", + "blob-spin": "blob-spin 20s linear infinite", }, }, },