diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 06e5437..549a50c 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -3,7 +3,7 @@ name: Build and Push Docker Image on: push: branches: - - '**' # Run on all branches + - 'main' tags: - 'v*.*.*' pull_request: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5f90ebe --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,66 @@ +# Contributing to OpenBookLM + +First off, thank you for considering contributing to OpenBookLM! It's people like you that make OpenBookLM such a great tool for democratizing learning with AI. + +We welcome all types of contributions: bug reports, bug fixes, documentation improvements, new features, and design tweaks. This document provides a simple guide to help you get started. + +## How to Contribute + +### 1. Fork the Repository +Start by forking the [OpenBookLM repository](https://github.com/open-biz/OpenBookLM) to your own GitHub account. You can do this by clicking the **Fork** button at the top right of the repository page. + +### 2. Clone Your Fork +Clone the forked repository to your local machine: + +```bash +git clone https://github.com/YOUR_USERNAME/OpenBookLM.git +cd OpenBookLM +``` + +### 3. Create a Feature Branch +Create a new branch for your feature or bugfix. Use a descriptive name for your branch: + +```bash +git checkout -b feature/your-amazing-feature +# or for a bugfix: +git checkout -b fix/your-bugfix-name +``` + +### 4. Make Your Changes +Make your changes to the codebase. Remember to: +- Follow the existing code style and conventions. +- Keep your changes focused on a single feature or bug. +- Add or update tests if applicable. + +### 5. Commit Your Changes +Write clear, concise commit messages. We prefer the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format: + +```bash +git commit -m "feat(ui): add new audio generation settings panel" +``` + +### 6. Push to Your Fork +Push your changes to your fork on GitHub: + +```bash +git push origin feature/your-amazing-feature +``` + +### 7. Submit a Pull Request +Go back to the original OpenBookLM repository and you will see a prompt to **Compare & pull request**. +- Click it, and fill out the pull request template. +- Clearly describe what changes you've made and why. +- Link any relevant issues (e.g., "Closes #42"). + +## PR Format Guidelines +- **Title:** Use a clear, descriptive title (e.g., `feat: integrate WebAssembly for local token counting`). +- **Description:** Provide a brief summary of the changes, the motivation behind them, and how to test them. +- **Screenshots:** If your PR includes UI changes, please attach before/after screenshots or GIFs. + +## Code of Conduct +Please be respectful and considerate of others when communicating in issues and pull requests. We aim to foster a welcoming and inclusive community. + +## Need Help? +If you're stuck or have questions, feel free to open a draft PR or create an issue asking for guidance. We are happy to help! + +Thank you for contributing! ๐Ÿš€ diff --git a/Dockerfile b/Dockerfile index daff5c8..9bb46e4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Use the official Bun image -FROM oven/bun:1.1 AS base +FROM oven/bun:latest AS base WORKDIR /app # No Clerk environment variables @@ -34,10 +34,6 @@ WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 -# Create system user -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs - # Copy built application COPY --from=builder /app/package.json ./package.json COPY --from=builder /app/bun.lock ./bun.lock @@ -48,9 +44,10 @@ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/prisma ./prisma # Set permissions -RUN chown -R nextjs:nodejs . +# Since we are using the official bun image, we just use the built-in bun user +RUN chown -R bun:bun . -USER nextjs +USER bun EXPOSE 3000 diff --git a/README.md b/README.md index 0693c91..dadb5b7 100644 --- a/README.md +++ b/README.md @@ -227,11 +227,7 @@ To get a local copy up and running, follow these steps. Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. -1. Fork the Project -2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) -3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the Branch (`git push origin feature/AmazingFeature`) -5. Open a Pull Request +Please see our [CONTRIBUTING.md](CONTRIBUTING.md) file for detailed guidelines on how to fork the repository, structure your branches, format your pull requests, and start building!

(back to top)

diff --git a/src/app/api/chat/history/route.ts b/src/app/api/chat/history/route.ts index d3582df..a4dd324 100644 --- a/src/app/api/chat/history/route.ts +++ b/src/app/api/chat/history/route.ts @@ -9,13 +9,13 @@ export async function GET(req: Request) { const user = await getCurrentUser(); const userId = user?.id; if (!userId) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const url = new URL(req.url); const notebookId = url.searchParams.get("notebookId"); if (!notebookId) { - return new NextResponse("NotebookId is required", { status: 400 }); + return NextResponse.json({ error: "NotebookId is required" }, { status: 400 }); } const key = `chat:${userId}:${notebookId}`; @@ -67,7 +67,7 @@ export async function GET(req: Request) { return NextResponse.json(messages); } catch (error) { console.error("[CHAT_HISTORY_GET]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ error: "Internal Error" }, { status: 500 }); } } @@ -76,12 +76,12 @@ export async function POST(req: Request) { const user = await getCurrentUser(); const userId = user?.id; if (!userId) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const { messages, notebookId } = await req.json(); if (!notebookId || !Array.isArray(messages)) { - return new NextResponse("Invalid request data", { status: 400 }); + return NextResponse.json({ error: "Invalid request data" }, { status: 400 }); } // Save to database first @@ -107,6 +107,6 @@ export async function POST(req: Request) { return NextResponse.json({ success: true }); } catch (error) { console.error("[CHAT_HISTORY_POST]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ error: "Internal Error" }, { status: 500 }); } } diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 2d9ce2e..73f43e6 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -32,7 +32,7 @@ export async function POST(req: Request) { try { const user = await getOrCreateUser(); if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const body = await req.json(); @@ -167,6 +167,6 @@ const finalMessages = [ { status: 400 } ); } - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ error: "Internal Error" }, { status: 500 }); } } diff --git a/src/app/api/notebooks/[id]/route.ts b/src/app/api/notebooks/[id]/route.ts index 07196da..84a7e9b 100644 --- a/src/app/api/notebooks/[id]/route.ts +++ b/src/app/api/notebooks/[id]/route.ts @@ -28,7 +28,7 @@ export async function GET( ) { const params = await props.params; try { - const user = await getOrCreateUser(); + const user = await getOrCreateUser(true); if (!user) { return NextResponse.json( { error: "Please sign in to access notebooks" }, @@ -83,15 +83,15 @@ export async function PUT( ) { const params = await props.params; try { - const user = await getCurrentUser(); + const user = await getOrCreateUser(true); const userId = user?.id; if (!userId) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const redis = getRedisClient(); if (!redis) { - return new NextResponse("Redis connection failed", { status: 500 }); + return NextResponse.json({ error: "Redis connection failed" }, { status: 500 }); } const notebook = await req.json(); @@ -102,7 +102,7 @@ export async function PUT( return NextResponse.json({ success: true }); } catch (error) { console.error("[NOTEBOOK_PUT]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ error: "Internal Error" }, { status: 500 }); } } @@ -112,7 +112,7 @@ export async function PATCH( ) { const params = await props.params; try { - const user = await getOrCreateUser(); + const user = await getOrCreateUser(true); if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } diff --git a/src/app/api/notebooks/route.ts b/src/app/api/notebooks/route.ts index b6b25aa..76ebf09 100644 --- a/src/app/api/notebooks/route.ts +++ b/src/app/api/notebooks/route.ts @@ -9,9 +9,9 @@ export async function GET() { const currentUser = await getCurrentUser(); const userId = currentUser?.id; if (!userId) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const user = await getOrCreateUser(); + const user = await getOrCreateUser(true); const notebooks = await prisma.notebook.findMany({ where: { userId: user?.id, @@ -28,22 +28,22 @@ export async function GET() { return NextResponse.json(notebooks); } catch (error) { console.error("[NOTEBOOKS_GET]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ error: "Internal Error" }, { status: 500 }); } } export async function POST(req: Request) { try { - const user = await getOrCreateUser(); + const user = await getOrCreateUser(true); if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const body = await req.json(); const { title, provider } = body; if (!title) { - return new NextResponse("Title is required", { status: 400 }); + return NextResponse.json({ error: "Title is required" }, { status: 400 }); } const notebook = await prisma.notebook.create({ @@ -58,6 +58,6 @@ export async function POST(req: Request) { return NextResponse.json(notebook); } catch (error) { console.error("[NOTEBOOKS_POST]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ error: "Internal Error" }, { status: 500 }); } } diff --git a/src/app/home-page.tsx b/src/app/home-page.tsx index a1b2e70..7a6dcba 100644 --- a/src/app/home-page.tsx +++ b/src/app/home-page.tsx @@ -10,7 +10,15 @@ import { PanelLeftClose, PanelRightClose, Trash2, + Settings, + LayoutGrid } from "lucide-react"; +import Image from "next/image"; +import { useSession, signOut } from "@/lib/auth-client"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { DropdownMenuSeparator } from "@/components/ui/dropdown-menu"; +import { LogOut } from "lucide-react"; +import { LoginModal } from "@/components/login-modal"; import { CreateNotebookDialog } from "@/components/create-notebook-dialog"; import { Card } from "@/components/ui/card"; import { ShareDialog } from "@/components/share-dialog"; @@ -24,6 +32,7 @@ import { import { toast } from "sonner"; import { NotImplementedDialog } from "@/components/not-implemented-dialog"; +import { NotebookCard } from "@/components/notebook-card"; interface Notebook { id: string; @@ -44,6 +53,8 @@ export default function HomePage({ const [viewMode, setViewMode] = useState<"grid" | "list">("grid"); const [deletingNotebookId, setDeletingNotebookId] = useState(null); const [notImplementedOpen, setNotImplementedOpen] = useState(false); + const { data: session } = useSession(); + const isSignedIn = !!session; useEffect(() => { const handleResize = () => { @@ -129,7 +140,63 @@ export default function HomePage({ return (
-
+
+ + {/* Mobile Header (Hidden on Desktop) */} +
+ + Logo + + NotebookLM + + + +
+ + + {isSignedIn ? ( + + + + + +
+
+ {session.user.name &&

{session.user.name}

} + {session.user.email && ( +

+ {session.user.email} +

+ )} +
+
+ + signOut()} + className="text-red-500 focus:text-red-400 focus:bg-red-500/10 cursor-pointer" + > + + Log out + +
+
+ ) : ( + + )} +
+
+

Welcome to OpenBookLM @@ -165,12 +232,11 @@ export default function HomePage({ My notebooks -

+
@@ -243,63 +309,13 @@ export default function HomePage({ )} {notebooks.map((notebook, index) => ( - -
-
- {getNotebookEmoji(notebook)} -
-
-

- {notebook.title} -

-
- - {viewMode === "grid" && ( -
- {new Date(notebook.updatedAt).toLocaleDateString()} - โ€ข - {notebook.sources.length} source{notebook.sources.length !== 1 ? 's' : ''} -
- )} -
- -
- - - - - - { - e.preventDefault(); - handleDeleteNotebook(notebook.id); - }} - > - - Delete - - - -
- + notebook={notebook} + viewMode={viewMode} + isDeleting={deletingNotebookId === notebook.id} + onDelete={handleDeleteNotebook} + /> ))}
diff --git a/src/app/waitlist/page.tsx b/src/app/waitlist/page.tsx deleted file mode 100644 index c0a808a..0000000 --- a/src/app/waitlist/page.tsx +++ /dev/null @@ -1,82 +0,0 @@ -'use client'; - -import { useState } from 'react'; - -export default function WaitlistPage() { - const [email, setEmail] = useState(''); - const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); - const [message, setMessage] = useState(''); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setStatus('loading'); - - try { - const formBody = `email=${encodeURIComponent(email)}`; - const response = await fetch('https://app.loops.so/api/newsletter-form/cm0lvgt8o00s76ks1q6es5awp', { - method: 'POST', - body: formBody, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }); - - const data = await response.json(); - - if (data.success) { - setStatus('success'); - setMessage('Thanks for joining the waitlist!'); - setEmail(''); - } else { - throw new Error(data.message || 'Something went wrong'); - } - } catch (error) { - setStatus('error'); - setMessage(error instanceof Error ? error.message : 'Failed to join waitlist'); - } - }; - - return ( -
-
-
-

Join the Waitlist

-

Be the first to know when we launch!

-
- -
-
- setEmail(e.target.value)} - className="appearance-none rounded-lg relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - placeholder="Enter your email" - /> -
- - -
- - {status === 'success' && ( -
-
{message}
-
- )} - - {status === 'error' && ( -
-
{message}
-
- )} -
-
- ); -} diff --git a/src/components/chat.tsx b/src/components/chat.tsx index e36e966..d81e327 100644 --- a/src/components/chat.tsx +++ b/src/components/chat.tsx @@ -9,7 +9,7 @@ import { } from "react"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; -import { Send, AlertCircle } from "lucide-react"; +import { Send, AlertCircle, Upload } from "lucide-react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import ReactMarkdown from "react-markdown"; import type { Components } from "react-markdown"; diff --git a/src/components/markdown-components.tsx b/src/components/markdown-components.tsx deleted file mode 100644 index 2ab0315..0000000 --- a/src/components/markdown-components.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import type { Components } from "react-markdown"; - -export const MarkdownComponents: Components = { - p: ({ children }) =>

{children}

, - h1: ({ children }) =>

{children}

, - h2: ({ children }) =>

{children}

, - h3: ({ children }) =>

{children}

, - ul: ({ children }) => , - ol: ({ children }) =>
    {children}
, - li: ({ children }) =>
  • {children}
  • , - code: ({ className, children }) => ( - - {children} - - ), - pre: ({ children }) =>
    {children}
    , - blockquote: ({ children }) => ( -
    - {children} -
    - ), - a: ({ children, href }) => ( - - {children} - - ), -}; diff --git a/src/components/model-selector.tsx b/src/components/model-selector.tsx deleted file mode 100644 index b5b67ab..0000000 --- a/src/components/model-selector.tsx +++ /dev/null @@ -1,97 +0,0 @@ -"use client" - -import { useState } from 'react' -import { ChevronRight } from 'lucide-react' -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" -import { Button } from "@/components/ui/button" - -const models = [ - { id: 'llama', name: 'LLama 70B' }, - { id: 'claude', name: 'Claude 3.5 Sonnet' }, - { id: 'openai', name: 'OpenAI o1' }, - { id: 'custom', name: 'Bring Your Own' }, -] - -export function ModelSelector() { - const [selectedModel, setSelectedModel] = useState(models[0]) - const [showCustomModal, setShowCustomModal] = useState(false) - - const handleModelSelect = (model: typeof models[0]) => { - if (model.id === 'custom') { - setShowCustomModal(true) - } else { - setSelectedModel(model) - } - } - - return ( - <> -
    - 3 sources - โ€ข -
    - {models.map((model) => ( - - ))} -
    -
    - - - - - Bring Your Own AI Model - - Configure your custom AI model settings - - -
    -
    - - -
    -
    - - -
    -
    - - -
    - -
    -
    -
    - - ) -} diff --git a/src/components/not-implemented-dialog.tsx b/src/components/not-implemented-dialog.tsx new file mode 100644 index 0000000..eb97c0b --- /dev/null +++ b/src/components/not-implemented-dialog.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Github, Hammer } from "lucide-react"; +import Link from "next/link"; + +interface NotImplentedDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + featureName: string; +} + +export function NotImplementedDialog({ + open, + onOpenChange, + featureName, +}: NotImplentedDialogProps) { + return ( + + + +
    +
    + +
    +
    + Not Implemented Yet + + The {featureName} feature is currently under construction. + OpenBookLM is entirely open-source, and we rely on community contributions to build out these amazing features! + +
    + +
    + + +
    +
    +
    + ); +} \ No newline at end of file diff --git a/src/components/note-modal.tsx b/src/components/note-modal.tsx deleted file mode 100644 index 4e6fd19..0000000 --- a/src/components/note-modal.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { useState, useEffect } from "react"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { Copy, Save, Edit2 } from "lucide-react"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; - -interface NoteModalProps { - note: { - id: string; - title: string; - content: string; - createdAt: string; - } | null; - isOpen: boolean; - onClose: () => void; - notebookId: string; -} - -export function NoteModal({ - note, - isOpen, - onClose, - notebookId, -}: NoteModalProps) { - const [isEditing, setIsEditing] = useState(false); - const [editedTitle, setEditedTitle] = useState(note?.title || ""); - const [editedContent, setEditedContent] = useState(note?.content || ""); - const [isSaving, setIsSaving] = useState(false); - - useEffect(() => { - if (note) { - setEditedTitle(note.title); - setEditedContent(note.content); - } - }, [note]); - - const copyToClipboard = async () => { - if (note) { - try { - await navigator.clipboard.writeText(note.content); - } catch (err) { - console.error("Failed to copy:", err); - } - } - }; - - const handleSave = async () => { - if (!note) return; - - setIsSaving(true); - try { - const response = await fetch( - `/api/notebooks/${notebookId}/notes/${note.id}`, - { - method: "PATCH", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - title: editedTitle, - content: editedContent, - }), - } - ); - - if (!response.ok) { - throw new Error("Failed to update note"); - } - - setIsEditing(false); - // Force a refresh of the parent component - window.location.reload(); - } catch (error) { - console.error("Error updating note:", error); - } finally { - setIsSaving(false); - } - }; - - return ( - - - -
    - {isEditing ? ( - setEditedTitle(e.target.value)} - className="bg-[#2A2A2A] border-[#3A3A3A] flex-1 mr-2" - /> - ) : ( - - {note?.title} - - )} -
    - {isEditing ? ( - - ) : ( - <> - - - - )} -
    -
    -
    - {note && new Date(note.createdAt).toLocaleDateString()} -
    -
    -
    - {isEditing ? ( -