feat: 8 AI providers, rich text editor, agent notifications, UI contrast & font settings
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m25s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m25s
- Add DeepSeek, OpenRouter, Mistral, Z.AI, LM Studio as AI providers with editable model names via Combobox in admin settings - Fix OpenRouter broken by normalizeProvider bug in config.ts - Convert agent-created notes from Markdown to HTML (TipTap rich text) - Add Notification model + in-app notifications for agent results - Agent notification click opens the created note directly - Add note count display on notebook and inbox headers - Fix checklist toggle in card view (persist state via localCheckItems) - Add checklist creation option in tabs/list view (dropdown on + button) - Fix image description ENOENT error with HTTP fallback - Improve UI contrast across all themes (input, border, checkbox visibility) - Add font family setting (Inter vs System Default) in Appearance settings - Fix CSS font-sans variable conflict (removed dead Geist references) - Update README with new features and 8 providers Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Bell, Check, X, Clock, AlertCircle, CheckCircle2, Circle, Share2 } from 'lucide-react'
|
||||
import { Bell, Check, X, Clock, AlertCircle, CheckCircle2, Circle, Share2, Bot, Trash2 } from 'lucide-react'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import { getPendingShareRequests, respondToShareRequest, getNotesWithReminders, toggleReminderDone } from '@/app/actions/notes'
|
||||
import { getUnreadNotifications, markNotificationRead, markAllNotificationsRead, type AppNotification } from '@/app/actions/notifications'
|
||||
import { toast } from 'sonner'
|
||||
import { useNoteRefreshOptional } from '@/context/NoteRefreshContext'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -47,19 +49,23 @@ interface ReminderNote {
|
||||
export function NotificationPanel() {
|
||||
const { triggerRefresh } = useNoteRefreshOptional()
|
||||
const { t } = useLanguage()
|
||||
const router = useRouter()
|
||||
const [requests, setRequests] = useState<ShareRequest[]>([])
|
||||
const [reminders, setReminders] = useState<ReminderNote[]>([])
|
||||
const [appNotifications, setAppNotifications] = useState<AppNotification[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
const [shareData, reminderData] = await Promise.all([
|
||||
const [shareData, reminderData, notifData] = await Promise.all([
|
||||
getPendingShareRequests(),
|
||||
getNotesWithReminders(),
|
||||
getUnreadNotifications(),
|
||||
])
|
||||
setRequests(shareData as any)
|
||||
setReminders((reminderData as any) || [])
|
||||
setAppNotifications(notifData || [])
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load notifications:', error)
|
||||
}
|
||||
@@ -81,7 +87,7 @@ export function NotificationPanel() {
|
||||
const overdueReminders = activeReminders.filter(r => new Date(r.reminder!) < now)
|
||||
const upcomingReminders = activeReminders.filter(r => new Date(r.reminder!) >= now)
|
||||
|
||||
const pendingCount = requests.length + overdueReminders.length
|
||||
const pendingCount = requests.length + overdueReminders.length + appNotifications.length
|
||||
|
||||
const handleAccept = async (shareId: string) => {
|
||||
try {
|
||||
@@ -121,7 +127,17 @@ export function NotificationPanel() {
|
||||
}
|
||||
}
|
||||
|
||||
const hasContent = requests.length > 0 || activeReminders.length > 0
|
||||
const handleMarkNotifRead = async (notifId: string) => {
|
||||
await markNotificationRead(notifId)
|
||||
setAppNotifications(prev => prev.filter(n => n.id !== notifId))
|
||||
}
|
||||
|
||||
const handleMarkAllRead = async () => {
|
||||
await markAllNotificationsRead()
|
||||
setAppNotifications([])
|
||||
}
|
||||
|
||||
const hasContent = requests.length > 0 || activeReminders.length > 0 || appNotifications.length > 0
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
@@ -149,11 +165,22 @@ export function NotificationPanel() {
|
||||
<Bell className="h-4 w-4 text-primary dark:text-primary-foreground" />
|
||||
<span className="font-semibold text-sm">{t('notification.notifications')}</span>
|
||||
</div>
|
||||
{pendingCount > 0 && (
|
||||
<Badge className="bg-primary hover:bg-primary/90 text-primary-foreground shadow-md">
|
||||
{pendingCount}
|
||||
</Badge>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{appNotifications.length > 0 && (
|
||||
<button
|
||||
onClick={handleMarkAllRead}
|
||||
className="text-[10px] text-muted-foreground hover:text-foreground transition-colors"
|
||||
title={t('notification.markAllRead') || 'Mark all read'}
|
||||
>
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{pendingCount > 0 && (
|
||||
<Badge className="bg-primary hover:bg-primary/90 text-primary-foreground shadow-md">
|
||||
{pendingCount}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -168,6 +195,65 @@ export function NotificationPanel() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
{/* App notifications (agents, system) */}
|
||||
{appNotifications.map((notif) => (
|
||||
<div
|
||||
key={notif.id}
|
||||
className="p-3 border-b last:border-0 hover:bg-accent/50 transition-colors duration-150 cursor-pointer"
|
||||
onClick={() => {
|
||||
if (notif.actionUrl) {
|
||||
handleMarkNotifRead(notif.id)
|
||||
setOpen(false)
|
||||
router.push(notif.actionUrl)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={cn(
|
||||
"mt-0.5 flex-none rounded-full p-1",
|
||||
notif.type === 'agent_success' && 'bg-green-100 dark:bg-green-900/30 text-green-600',
|
||||
notif.type === 'agent_failure' && 'bg-red-100 dark:bg-red-900/30 text-red-600',
|
||||
notif.type === 'system' && 'bg-blue-100 dark:bg-blue-900/30 text-blue-600',
|
||||
)}>
|
||||
{notif.type.startsWith('agent') ? (
|
||||
<Bot className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<AlertCircle className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5 mb-0.5">
|
||||
<span className={cn(
|
||||
"text-[10px] font-semibold uppercase tracking-wider",
|
||||
notif.type === 'agent_success' && 'text-green-600 dark:text-green-400',
|
||||
notif.type === 'agent_failure' && 'text-red-600 dark:text-red-400',
|
||||
notif.type === 'system' && 'text-blue-600 dark:text-blue-400',
|
||||
)}>
|
||||
{notif.type === 'agent_success' && (t('notification.agentSuccess') || 'Agent completed')}
|
||||
{notif.type === 'agent_failure' && (t('notification.agentFailed') || 'Agent failed')}
|
||||
{notif.type === 'system' && 'System'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm font-medium truncate">{notif.title}</p>
|
||||
{notif.message && (
|
||||
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-2">{notif.message}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-1 mt-1 text-xs text-muted-foreground">
|
||||
<Clock className="w-3 h-3" />
|
||||
{formatDistanceToNow(new Date(notif.createdAt), { addSuffix: true })}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleMarkNotifRead(notif.id) }}
|
||||
className="mt-0.5 text-muted-foreground/40 hover:text-foreground transition-colors"
|
||||
title={t('notification.dismiss') || 'Dismiss'}
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Overdue reminders */}
|
||||
{overdueReminders.map((note) => (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user