Files
Momento/memento-note/components/notebook-summary-dialog.tsx
Sepehr Ramezani aa6a214f37 feat: rename keep-notes to memento-note, migrate to PostgreSQL, fix MCP bugs
- Rename directory keep-notes -> memento-note with all code references
- Prisma: SQLite -> PostgreSQL (both app and MCP server schemas)
- Sync MCP schema with main app (add missing fields, relations, indexes)
- Delete 17 SQLite migrations (clean slate for PostgreSQL)
- Remove SQLite dependencies (@libsql/client, better-sqlite3, etc.)
- Fix MCP server: hardcoded Windows DB paths -> DATABASE_URL env var
- Fix MCP server: .dockerignore excluded index-sse.js (SSE mode broken)
- MCP Dockerfile: node:20 -> node:22
- Docker Compose: add postgres service, remove SQLite volume
- Generate favicon.ico, icon-192.png, icon-512.png, apple-icon.png
- Update layout.tsx icons and manifest.json for PNG icons
- Update all .env files for PostgreSQL
- Rewrite README.md with updated sections
- Remove mcp-server/node_modules and prisma/client-generated from git tracking

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-20 20:58:04 +02:00

164 lines
4.9 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import { Button } from './ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from './ui/dialog'
import { Loader2, FileText, RefreshCw } from 'lucide-react'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
import type { NotebookSummary } from '@/lib/ai/services'
import ReactMarkdown from 'react-markdown'
interface NotebookSummaryDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
notebookId: string | null
notebookName?: string
}
export function NotebookSummaryDialog({
open,
onOpenChange,
notebookId,
notebookName,
}: NotebookSummaryDialogProps) {
const { t } = useLanguage()
const [summary, setSummary] = useState<NotebookSummary | null>(null)
const [loading, setLoading] = useState(false)
const [regenerating, setRegenerating] = useState(false)
// Fetch summary when dialog opens with a notebook
useEffect(() => {
if (open && notebookId) {
fetchSummary()
} else {
// Reset state when closing
setSummary(null)
}
}, [open, notebookId])
const fetchSummary = async () => {
if (!notebookId) return
setLoading(true)
try {
const response = await fetch('/api/ai/notebook-summary', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
notebookId,
language: document.documentElement.lang || 'en',
}),
})
const data = await response.json()
if (data.success && data.data) {
setSummary(data.data)
} else {
toast.error(data.error || t('notebook.summaryError'))
onOpenChange(false)
}
} catch (error) {
toast.error(t('notebook.summaryError'))
onOpenChange(false)
} finally {
setLoading(false)
}
}
const handleRegenerate = async () => {
if (!notebookId) return
setRegenerating(true)
await fetchSummary()
setRegenerating(false)
}
if (loading) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader className="sr-only">
<DialogTitle>{t('notebook.generating')}</DialogTitle>
<DialogDescription>{t('notebook.generatingDescription') || 'Please wait...'}</DialogDescription>
</DialogHeader>
<div className="flex flex-col items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p className="mt-4 text-sm text-muted-foreground">
{t('notebook.generating')}
</p>
</div>
</DialogContent>
</Dialog>
)
}
if (!summary) {
return null
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2">
<FileText className="h-5 w-5" />
{t('notebook.summary')}
</div>
<Button
variant="ghost"
size="sm"
onClick={handleRegenerate}
disabled={regenerating}
className="gap-2"
>
<RefreshCw className={`h-4 w-4 ${regenerating ? 'animate-spin' : ''}`} />
{regenerating
? (t('ai.notebookSummary.regenerating') || 'Regenerating...')
: (t('ai.notebookSummary.regenerate') || 'Regenerate')}
</Button>
</DialogTitle>
<DialogDescription>
{t('notebook.summaryDescription', {
notebook: summary.notebookName,
count: summary.stats.totalNotes,
})}
</DialogDescription>
</DialogHeader>
{/* Stats */}
<div className="flex flex-wrap gap-4 p-4 bg-muted rounded-lg">
<div className="flex items-center gap-2">
<FileText className="h-4 w-4 text-muted-foreground" />
<span className="text-sm">
{summary.stats.totalNotes} {summary.stats.totalNotes === 1 ? 'note' : 'notes'}
</span>
</div>
{summary.stats.labelsUsed.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Labels:</span>
<span className="text-sm">{summary.stats.labelsUsed.join(', ')}</span>
</div>
)}
<div className="ml-auto text-xs text-muted-foreground">
{new Date(summary.generatedAt).toLocaleString()}
</div>
</div>
{/* Summary Content */}
<div className="prose prose-sm dark:prose-invert max-w-none">
<ReactMarkdown>{summary.summary}</ReactMarkdown>
</div>
</DialogContent>
</Dialog>
)
}