diff --git a/.github/workflows/deploy-frontend.yml b/.github/workflows/deploy-frontend.yml new file mode 100644 index 0000000..a36cb7f --- /dev/null +++ b/.github/workflows/deploy-frontend.yml @@ -0,0 +1,79 @@ +name: Deploy Frontend to Cloudflare Pages + +on: + push: + branches: + - main + paths: + - 'frontend/**' + - '.github/workflows/deploy-frontend.yml' + pull_request: + branches: + - main + paths: + - 'frontend/**' + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + deployments: write + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + working-directory: frontend + run: npm ci + + - name: Build + working-directory: frontend + run: npm run build + + - name: Deploy to Cloudflare Pages + id: deploy + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages deploy dist --project-name=workmate + workingDirectory: frontend + + - name: Comment preview URL on PR + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const body = `🚀 **Cloudflare Pages Preview**\n\nDeployed to: ${process.env.DEPLOYMENT_URL}`; + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const existing = comments.find(c => c.body.includes('Cloudflare Pages Preview')); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } + env: + DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }} diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx index 78e0b67..4508716 100644 --- a/frontend/src/app/App.tsx +++ b/frontend/src/app/App.tsx @@ -1,5 +1,4 @@ import { Routes, Route } from "react-router-dom"; -import { LoginPage } from "./pages/LoginPage"; import { ChatPage } from "./pages/ChatPage"; import { AdminPage } from "./pages/AdminPage"; import { SettingsPage } from "./pages/SettingsPage"; @@ -9,12 +8,11 @@ import { Sidebar } from "./components/Sidebar"; export default function App() { return ( - - } /> - + + +
-
- } - /> - + } + /> + - - } - /> - - - - } - /> -
+ } + /> + } /> + + ); } diff --git a/frontend/src/app/components/Sidebar.tsx b/frontend/src/app/components/Sidebar.tsx index f2d1a65..88438f9 100644 --- a/frontend/src/app/components/Sidebar.tsx +++ b/frontend/src/app/components/Sidebar.tsx @@ -270,14 +270,14 @@ export function Sidebar({
{workspaces.length === 0 ? ( Connect a Notion workspace ) : ( workspaces.map((workspace) => ( - + @@ -309,9 +309,9 @@ export function Sidebar({ {/* Navigation Links */}
{isAdmin && ( - + + {/* Public marketing site */} + }> + } /> + } /> + } /> + } /> + } /> + } /> + + + {/* Auth */} + } /> + + {/* Authenticated app */} + } /> + diff --git a/frontend/src/public/components/ComingSoon.tsx b/frontend/src/public/components/ComingSoon.tsx new file mode 100644 index 0000000..67e5275 --- /dev/null +++ b/frontend/src/public/components/ComingSoon.tsx @@ -0,0 +1,26 @@ +import { Link } from "react-router-dom"; +import type { LucideIcon } from "lucide-react"; + +interface ComingSoonProps { + icon: LucideIcon; + title: string; + description: string; +} + +export function ComingSoon({ icon: Icon, title, description }: ComingSoonProps) { + return ( +
+
+ +
+

{title}

+

{description}

+ + ← Back to Home + +
+ ); +} diff --git a/frontend/src/public/components/Footer.tsx b/frontend/src/public/components/Footer.tsx new file mode 100644 index 0000000..f0988f6 --- /dev/null +++ b/frontend/src/public/components/Footer.tsx @@ -0,0 +1,44 @@ +import { Link } from "react-router-dom"; + +const footerLinks = [ + { label: "About", href: "/about" }, + { label: "Pricing", href: "/pricing" }, + { label: "Docs", href: "/docs" }, + { label: "Contact", href: "/contact" }, + { label: "GitHub", href: "https://github.com/RubyRyn/WorkMate" }, +]; + +export function Footer() { + return ( +
+
+

+ © 2026 WorkMate. Built at SFBU. +

+
+ {footerLinks.map((link) => + link.href.startsWith("http") ? ( + + {link.label} + + ) : ( + + {link.label} + + ) + )} +
+
+
+ ); +} diff --git a/frontend/src/public/components/Navbar.tsx b/frontend/src/public/components/Navbar.tsx new file mode 100644 index 0000000..042d2ed --- /dev/null +++ b/frontend/src/public/components/Navbar.tsx @@ -0,0 +1,138 @@ +import { useState, useEffect } from "react"; +import { Link, useLocation } from "react-router-dom"; +import { Menu, X } from "lucide-react"; +import { useAuth } from "@/app/contexts/AuthContext"; +import { Sheet, SheetContent, SheetTrigger } from "@/app/components/ui/sheet"; + +const navLinks = [ + { label: "About", href: "/about" }, + { label: "Pricing", href: "/pricing" }, + { label: "Docs", href: "/docs" }, + { label: "Demo", href: "/demo" }, + { label: "Contact", href: "/contact" }, +]; + +export function Navbar() { + const { user } = useAuth(); + const location = useLocation(); + const [scrolled, setScrolled] = useState(false); + const [mobileOpen, setMobileOpen] = useState(false); + + useEffect(() => { + const handleScroll = () => setScrolled(window.scrollY > 10); + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, []); + + return ( + + ); +} diff --git a/frontend/src/public/components/hero/ChatPreview.tsx b/frontend/src/public/components/hero/ChatPreview.tsx new file mode 100644 index 0000000..e92c44b --- /dev/null +++ b/frontend/src/public/components/hero/ChatPreview.tsx @@ -0,0 +1,36 @@ +import { motion } from "motion/react"; + +export function ChatPreview() { + return ( +
+

+ {"\u{1F4AC}"} + + What did we decide about the pricing model? + +

+ +
+ +
+ WorkMate — Based + on your Q3 Strategy Doc and Product Roadmap, the team agreed on a + freemium model with... + +
+ +
+ + 2 sources cited + + + High confidence + +
+
+ ); +} diff --git a/frontend/src/public/components/hero/FloatingDocs.tsx b/frontend/src/public/components/hero/FloatingDocs.tsx new file mode 100644 index 0000000..7e4e177 --- /dev/null +++ b/frontend/src/public/components/hero/FloatingDocs.tsx @@ -0,0 +1,30 @@ +import { motion } from "motion/react"; + +const docs = [ + { label: "Q3 Strategy", emoji: "\u{1F4C4}", x: "5%", y: "8%", rotate: -2 }, + { label: "Sprint Board", emoji: "\u{1F4CA}", x: "75%", y: "12%", rotate: 1.5 }, + { label: "Meeting Notes", emoji: "\u{1F4DD}", x: "8%", y: "70%", rotate: 1 }, + { label: "Roadmap", emoji: "\u{1F5C2}\uFE0F", x: "72%", y: "75%", rotate: -1 }, +]; + +export function FloatingDocs() { + return ( + <> + {docs.map((doc, i) => ( + + {doc.emoji} {doc.label} + + ))} + + ); +} diff --git a/frontend/src/public/components/hero/HeroSection.tsx b/frontend/src/public/components/hero/HeroSection.tsx new file mode 100644 index 0000000..e58be16 --- /dev/null +++ b/frontend/src/public/components/hero/HeroSection.tsx @@ -0,0 +1,45 @@ +import { Link } from "react-router-dom"; +import { FloatingDocs } from "./FloatingDocs"; +import { ChatPreview } from "./ChatPreview"; + +export function HeroSection() { + return ( +
+
+ + + +
+

+ AI-Powered Knowledge Assistant +

+

+ Your Notion workspace, +
+ answered instantly. +

+

+ Ask questions across all your documents. Get sourced answers in + real-time. +

+ + + +
+ + Get Started Free + + + See How It Works + +
+
+
+ ); +} diff --git a/frontend/src/public/layouts/PublicLayout.tsx b/frontend/src/public/layouts/PublicLayout.tsx new file mode 100644 index 0000000..543d021 --- /dev/null +++ b/frontend/src/public/layouts/PublicLayout.tsx @@ -0,0 +1,15 @@ +import { Outlet } from "react-router-dom"; +import { Navbar } from "../components/Navbar"; +import { Footer } from "../components/Footer"; + +export function PublicLayout() { + return ( +
+ +
+ +
+
+
+ ); +} diff --git a/frontend/src/public/pages/AboutPage.tsx b/frontend/src/public/pages/AboutPage.tsx new file mode 100644 index 0000000..520376e --- /dev/null +++ b/frontend/src/public/pages/AboutPage.tsx @@ -0,0 +1,180 @@ +import { motion } from "motion/react"; +import { Github, Linkedin } from "lucide-react"; + +const fadeInUp = { + initial: { opacity: 0, y: 24 }, + whileInView: { opacity: 1, y: 0 }, + viewport: { once: true, margin: "-60px" }, + transition: { duration: 0.5 }, +}; + +const team = [ + { + name: "Emmanuel Owusu", + initials: "EO", + role: "DevOps Engineer", + bio: "Cloud infrastructure, CI/CD pipelines, Docker containerization", + github: "#", + linkedin: "#", + }, + { + name: "Jubaida Tasnim", + initials: "JT", + role: "Full Stack Developer", + bio: "React UI, FastAPI endpoints, real-time streaming, source citations", + github: "#", + linkedin: "#", + }, + { + name: "Rutvik Katkoriya", + initials: "RK", + role: "Data Engineer", + bio: "ETL pipeline, Notion ingestion, retrieval system, metadata engineering", + github: "#", + linkedin: "#", + }, + { + name: "Nila Ko", + initials: "NK", + role: "AI Engineer", + bio: "RAG pipeline, prompt engineering, embeddings, evaluation", + github: "#", + linkedin: "#", + }, +]; + +const techGrid = [ + { + label: "Frontend", + color: "text-purple-500", + items: ["React + TypeScript", "Vite", "Tailwind CSS + shadcn/ui", "Motion"], + }, + { + label: "Backend", + color: "text-purple-500", + items: ["FastAPI (Python)", "Google OAuth 2.0", "SSE Streaming", "LangChain"], + }, + { + label: "AI & Data", + color: "text-purple-500", + items: ["Gemini 2.5 Flash", "ChromaDB + BM25", "Voyage AI Re-ranker", "PostgreSQL"], + }, + { label: "Cloudflare", color: "text-amber-500", items: ["Pages (Frontend CDN)"] }, + { label: "AWS", color: "text-blue-500", items: ["EKS + Secrets Manager"] }, + { + label: "IBM LinuxONE", + color: "text-emerald-500", + items: ["ChromaDB + PostgreSQL"], + }, +]; + +export function AboutPage() { + return ( + <> +
+
+
+

+ About WorkMate +

+

+ We believe your team's knowledge shouldn't be buried in + documents. +

+

+ WorkMate was born from a simple frustration: teams store critical + knowledge in Notion, but finding specific answers means digging + through pages manually. We built an AI assistant that retrieves, + reasons over, and cites your workspace documents — so your team can + focus on decisions, not searching. +

+
+
+ +
+ +
+ +

+ The Team +

+

+ Built by engineers who care about knowledge access +

+

+ San Francisco Bay University — CS Capstone, Spring 2026 +

+
+
+ {team.map((m, i) => ( + +
+ {m.initials} +
+

{m.name}

+

{m.role}

+

+ {m.bio} +

+ +
+ ))} +
+
+ +
+ +
+ +

+ Tech Stack +

+

+ What powers WorkMate +

+
+
+ {techGrid.map((g, i) => ( + +

+ {g.label} +

+
+ {g.items.map((item) => ( +
{item}
+ ))} +
+
+ ))} +
+
+ + ); +} diff --git a/frontend/src/public/pages/ContactPage.tsx b/frontend/src/public/pages/ContactPage.tsx new file mode 100644 index 0000000..c26988d --- /dev/null +++ b/frontend/src/public/pages/ContactPage.tsx @@ -0,0 +1,12 @@ +import { Mail } from "lucide-react"; +import { ComingSoon } from "../components/ComingSoon"; + +export function ContactPage() { + return ( + + ); +} diff --git a/frontend/src/public/pages/DemoPage.tsx b/frontend/src/public/pages/DemoPage.tsx new file mode 100644 index 0000000..aba455d --- /dev/null +++ b/frontend/src/public/pages/DemoPage.tsx @@ -0,0 +1,12 @@ +import { Play } from "lucide-react"; +import { ComingSoon } from "../components/ComingSoon"; + +export function DemoPage() { + return ( + + ); +} diff --git a/frontend/src/public/pages/DocsPage.tsx b/frontend/src/public/pages/DocsPage.tsx new file mode 100644 index 0000000..700afab --- /dev/null +++ b/frontend/src/public/pages/DocsPage.tsx @@ -0,0 +1,135 @@ +import { motion } from "motion/react"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/app/components/ui/accordion"; + +const fadeInUp = { + initial: { opacity: 0, y: 24 }, + whileInView: { opacity: 1, y: 0 }, + viewport: { once: true, margin: "-60px" }, + transition: { duration: 0.5 }, +}; + +const gettingStarted = [ + { + title: "Sign in with Google", + desc: 'Click "Get Started" and authenticate with your Google account. No passwords to remember.', + }, + { + title: "Connect your Notion workspace", + desc: "Go to Settings and click \u201CConnect Notion.\u201D You\u2019ll authorize WorkMate to read your workspace pages and databases. Documents are automatically ingested and indexed.", + }, + { + title: "Start asking questions", + desc: "Type a question in the chat. WorkMate searches your connected documents using hybrid search, then streams an answer with source citations and confidence scores.", + }, + { + title: "Upload additional files (optional)", + desc: "Drag and drop PDFs, text files, or markdown into the chat. They\u2019ll be chunked, embedded, and searchable alongside your Notion content.", + }, +]; + +const faqItems = [ + { + q: "What data does WorkMate access from my Notion?", + a: "WorkMate reads pages, databases, and blocks from workspaces you explicitly authorize via OAuth. We never access workspaces you haven\u2019t connected. All tokens are encrypted at rest using Fernet encryption.", + }, + { + q: "Can other users see my documents?", + a: "No. WorkMate is multi-tenant with workspace isolation. Your RAG queries are scoped to your connected workspace IDs only. Other users cannot access or search your documents.", + }, + { + q: "What file types can I upload?", + a: "PDF, plain text (.txt), and Markdown (.md) files. Uploaded files are chunked using LangChain and embedded alongside your Notion content for unified search.", + }, + { + q: "How accurate are the answers?", + a: "WorkMate uses a hybrid search pipeline (BM25 + vector similarity) with Voyage AI re-ranking to find the most relevant chunks. Every answer includes confidence scores and source citations so you can verify. The LLM runs at temperature 0.0 for deterministic, consistent outputs.", + }, + { + q: "How do I sync new Notion changes?", + a: 'Go to Settings \u2192 Connected Workspaces and click the "Sync" button next to your workspace. WorkMate will re-ingest all pages and update the vector database.', + }, + { + q: "Is my data secure?", + a: "Yes. Notion OAuth tokens are encrypted with Fernet. API keys are stored in AWS Secrets Manager. All traffic uses HTTPS. The database runs on IBM LinuxONE with workspace-scoped access controls.", + }, +]; + +export function DocsPage() { + return ( + <> +
+

+ Documentation +

+

+ Get up and running +

+

+ Everything you need to connect your workspace and start asking + questions. +

+
+ +
+
+

+ Getting Started +

+
+ {gettingStarted.map((step, i) => ( + +
+ {i + 1} +
+
+

+ {step.title} +

+

+ {step.desc} +

+
+
+ ))} +
+
+
+ +
+ +
+
+

+ Frequently Asked Questions +

+ + {faqItems.map((item, i) => ( + + + {item.q} + + + {item.a} + + + ))} + +
+
+ + ); +} diff --git a/frontend/src/public/pages/LandingPage.tsx b/frontend/src/public/pages/LandingPage.tsx new file mode 100644 index 0000000..9777fb9 --- /dev/null +++ b/frontend/src/public/pages/LandingPage.tsx @@ -0,0 +1,185 @@ +import { Link } from "react-router-dom"; +import { motion } from "motion/react"; +import { + Search, + Zap, + Quote, + LayoutGrid, + FileUp, + ShieldCheck, +} from "lucide-react"; +import { HeroSection } from "../components/hero/HeroSection"; + +const features = [ + { + icon: Search, + title: "Hybrid RAG Search", + desc: "BM25 keyword + vector similarity + AI re-ranking. Finds exactly what you need.", + }, + { + icon: Zap, + title: "Real-time Streaming", + desc: "Answers stream in live via SSE. No waiting for full responses \u2014 see results instantly.", + }, + { + icon: Quote, + title: "Source Citations", + desc: "Every answer cites its sources with confidence scores. Trust but verify.", + }, + { + icon: LayoutGrid, + title: "Notion Integration", + desc: "One-click OAuth connect. Pages, databases, and blocks auto-ingested and indexed.", + }, + { + icon: FileUp, + title: "File Uploads", + desc: "Upload PDFs, text files, and markdown. Chunked and embedded alongside your Notion docs.", + }, + { + icon: ShieldCheck, + title: "Workspace Isolation", + desc: "Multi-tenant by design. Your data stays yours \u2014 queries scoped to your workspaces only.", + }, +]; + +const techStack = [ + "Google Gemini", + "Notion API", + "ChromaDB", + "FastAPI", + "React", +]; + +const steps = [ + { + num: 1, + title: "Connect Notion", + desc: "OAuth in one click. We auto-index your pages, databases, and blocks.", + }, + { + num: 2, + title: "Ask a Question", + desc: "Type naturally. WorkMate searches across all your connected documents.", + }, + { + num: 3, + title: "Get Sourced Answers", + desc: "Streamed responses with citations, confidence scores, and source links.", + }, +]; + +const fadeInUp = { + initial: { opacity: 0, y: 24 }, + whileInView: { opacity: 1, y: 0 }, + viewport: { once: true, margin: "-60px" }, + transition: { duration: 0.5 }, +}; + +export function LandingPage() { + return ( + <> + + +
+ + +

+ Built with +

+
+ {techStack.map((t) => ( + {t} + ))} +
+
+ +
+ +
+ +

+ Features +

+

+ Everything you need to unlock +
+ your team's knowledge +

+
+
+ {features.map((f, i) => ( + +
+ +
+

+ {f.title} +

+

{f.desc}

+
+ ))} +
+
+ +
+ +
+ +

+ How It Works +

+

+ Three steps to instant answers +

+
+
+ {steps.map((s, i) => ( + +
+ {s.num} +
+

+ {s.title} +

+

{s.desc}

+
+ ))} +
+
+ +
+ + +
+
+

+ Ready to try WorkMate? +

+

+ Connect your Notion workspace and start getting answers in minutes. +

+ + Get Started Free + +
+ + + ); +} diff --git a/frontend/src/public/pages/PricingPage.tsx b/frontend/src/public/pages/PricingPage.tsx new file mode 100644 index 0000000..d22a7a1 --- /dev/null +++ b/frontend/src/public/pages/PricingPage.tsx @@ -0,0 +1,180 @@ +import { Link } from "react-router-dom"; +import { motion } from "motion/react"; +import { Check } from "lucide-react"; + +const fadeInUp = { + initial: { opacity: 0, y: 24 }, + whileInView: { opacity: 1, y: 0 }, + viewport: { once: true, margin: "-60px" }, + transition: { duration: 0.5 }, +}; + +interface Tier { + name: string; + price: string; + unit: string; + description: string; + features: string[]; + cta: string; + highlighted: boolean; +} + +const tiers: Tier[] = [ + { + name: "Free", + price: "$0", + unit: "/month", + description: "For individuals exploring WorkMate", + features: [ + "1 Notion workspace", + "50 questions / month", + "5 file uploads", + "Source citations", + "Real-time streaming", + "Community support", + ], + cta: "Get Started Free", + highlighted: false, + }, + { + name: "Pro", + price: "$12", + unit: "/user/month", + description: "For teams who need full access", + features: [ + "Unlimited workspaces", + "Unlimited questions", + "Unlimited file uploads", + "Priority support", + "Advanced analytics", + "Conversation export", + ], + cta: "Get Started", + highlighted: true, + }, + { + name: "Enterprise", + price: "Custom", + unit: "", + description: "For organizations with advanced needs", + features: [ + "Everything in Pro", + "SSO / SAML", + "Custom integrations", + "Dedicated support", + "SLA guarantee", + "On-premise deployment", + ], + cta: "Contact Us", + highlighted: false, + }, +]; + +export function PricingPage() { + return ( + <> +
+
+
+

+ Pricing +

+

+ Simple, transparent pricing +

+

+ Start free. Upgrade as your team grows. +

+
+
+ +
+
+ {tiers.map((tier, i) => ( + + {tier.highlighted && ( + + Popular + + )} + +
+

+ {tier.name} +

+
+ + {tier.price} + + {tier.unit && ( + {tier.unit} + )} +
+

{tier.description}

+
+ +
+
    + {tier.features.map((f) => ( +
  • + + {f} +
  • + ))} +
+
+ +
+ {tier.name === "Enterprise" ? ( + + {tier.cta} + + ) : ( + + {tier.cta} + + )} +
+
+ ))} +
+ +

+ Have questions? Check our{" "} + + Docs & FAQ + {" "} + page. +

+
+ + ); +} diff --git a/src/backend/routers/auth.py b/src/backend/routers/auth.py index f2215d1..1ac71e8 100644 --- a/src/backend/routers/auth.py +++ b/src/backend/routers/auth.py @@ -102,7 +102,7 @@ async def google_callback(code: str, db: Session = Depends(get_db)): jwt_token = create_access_token({"sub": str(user.id)}) # Redirect to frontend with token - redirect_url = f"{settings.FRONTEND_URL}?token={jwt_token}" + redirect_url = f"{settings.FRONTEND_URL}/app?token={jwt_token}" return RedirectResponse(url=redirect_url) diff --git a/src/backend/routers/notion.py b/src/backend/routers/notion.py index b60c2bc..2561b09 100644 --- a/src/backend/routers/notion.py +++ b/src/backend/routers/notion.py @@ -201,7 +201,7 @@ async def notion_callback( _ingest_workspace, workspace.id, access_token, notion_workspace_id ) - redirect_url = f"{settings.FRONTEND_URL}/settings?notion=connected" + redirect_url = f"{settings.FRONTEND_URL}/app/settings?notion=connected" return RedirectResponse(url=redirect_url)