Embed OpenNotebook AI in rNotes — tab + standalone /ai route

Adds AI Notebook tab to notebook detail pages and a full-page /ai route,
both embedding the existing OpenNotebook instance via iframe at
opennotebook.rnotes.online. CSP frame-src header added for security.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-23 05:55:38 +00:00
parent 7a934a8a93
commit 3a1366f422
5 changed files with 128 additions and 3 deletions

View File

@ -1,6 +1,19 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: "frame-src 'self' https://opennotebook.rnotes.online https://notebook.jeffemmett.com;",
},
],
},
];
},
webpack: (config, { isServer, webpack }) => {
// Ignore onnxruntime-node if any dependency pulls it in.
// We only use the browser ONNX runtime (loaded from CDN at runtime).

48
src/app/ai/page.tsx Normal file
View File

@ -0,0 +1,48 @@
'use client';
import Link from 'next/link';
import { OpenNotebookEmbed } from '@/components/OpenNotebookEmbed';
import { UserMenu } from '@/components/UserMenu';
import { SearchBar } from '@/components/SearchBar';
export default function AIPage() {
return (
<div className="h-screen flex flex-col bg-[#0a0a0a]">
<nav className="border-b border-slate-800 px-4 md:px-6 py-4 flex-shrink-0">
<div className="max-w-6xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-3">
<Link href="/" className="flex-shrink-0">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center text-sm font-bold text-black">
rN
</div>
</Link>
<span className="text-slate-600 hidden sm:inline">/</span>
<span className="text-white font-semibold">AI Notebook</span>
</div>
<div className="flex items-center gap-2 md:gap-4">
<div className="hidden md:block w-64">
<SearchBar />
</div>
<Link
href="/notebooks"
className="text-sm text-slate-400 hover:text-white transition-colors hidden sm:inline"
>
Notebooks
</Link>
<Link
href="/demo"
className="text-sm text-slate-400 hover:text-white transition-colors hidden sm:inline"
>
Demo
</Link>
<UserMenu />
</div>
</div>
</nav>
<main className="flex-1 min-h-0">
<OpenNotebookEmbed className="h-full rounded-none border-0" />
</main>
</div>
);
}

View File

@ -5,6 +5,7 @@ import { useParams, useRouter } from 'next/navigation';
import Link from 'next/link';
import { NoteCard } from '@/components/NoteCard';
import { CanvasEmbed } from '@/components/CanvasEmbed';
import { OpenNotebookEmbed } from '@/components/OpenNotebookEmbed';
import { UserMenu } from '@/components/UserMenu';
import { authFetch } from '@/lib/authFetch';
import type { CanvasShapeMessage } from '@/lib/canvas-sync';
@ -38,7 +39,7 @@ export default function NotebookDetailPage() {
const [loading, setLoading] = useState(true);
const [showCanvas, setShowCanvas] = useState(false);
const [creatingCanvas, setCreatingCanvas] = useState(false);
const [tab, setTab] = useState<'notes' | 'pinned'>('notes');
const [tab, setTab] = useState<'notes' | 'pinned' | 'ai'>('notes');
const fetchNotebook = useCallback(() => {
fetch(`/api/notebooks/${params.id}`)
@ -207,10 +208,22 @@ export default function NotebookDetailPage() {
>
Pinned
</button>
<button
onClick={() => setTab('ai')}
className={`pb-3 text-sm font-medium transition-colors ${
tab === 'ai'
? 'text-amber-400 border-b-2 border-amber-400'
: 'text-slate-400 hover:text-white'
}`}
>
AI Notebook
</button>
</div>
{/* Notes grid */}
{filteredNotes.length === 0 ? (
{/* Tab content */}
{tab === 'ai' ? (
<OpenNotebookEmbed className="h-[calc(100vh-220px)] min-h-[500px]" />
) : filteredNotes.length === 0 ? (
<div className="text-center py-12 text-slate-400">
{tab === 'pinned' ? 'No pinned notes' : 'No notes yet. Add one!'}
</div>

View File

@ -43,6 +43,12 @@ export default function HomePage() {
<div className="hidden md:block w-64">
<SearchBar />
</div>
<Link
href="/ai"
className="text-sm text-slate-400 hover:text-white transition-colors hidden sm:inline"
>
AI
</Link>
<Link
href="/demo"
className="text-sm text-slate-400 hover:text-white transition-colors hidden sm:inline"

View File

@ -0,0 +1,45 @@
'use client';
import { useState } from 'react';
interface OpenNotebookEmbedProps {
className?: string;
}
export function OpenNotebookEmbed({ className = '' }: OpenNotebookEmbedProps) {
const notebookUrl = 'https://opennotebook.rnotes.online';
const [loading, setLoading] = useState(true);
return (
<div className={`relative bg-slate-900 rounded-xl overflow-hidden border border-slate-700/50 ${className}`}>
{loading && (
<div className="absolute inset-0 flex items-center justify-center z-10 bg-slate-900">
<div className="flex flex-col items-center gap-3">
<svg className="animate-spin h-8 w-8 text-amber-400" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
<span className="text-sm text-slate-400">Loading AI Notebook...</span>
</div>
</div>
)}
<div className="absolute top-2 right-2 z-10 flex gap-2">
<a
href={notebookUrl}
target="_blank"
rel="noopener noreferrer"
className="px-3 py-1.5 bg-slate-800/90 hover:bg-slate-700 border border-slate-600/50 rounded-lg text-xs text-slate-300 backdrop-blur-sm transition-colors"
>
Open Full View
</a>
</div>
<iframe
src={notebookUrl}
className="w-full h-full border-0"
allow="clipboard-write; clipboard-read"
title="AI Notebook — OpenNotebook"
onLoad={() => setLoading(false)}
/>
</div>
);
}