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:
parent
7a934a8a93
commit
3a1366f422
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue