Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 1m7s
Replaced ~100+ hardcoded French and English text strings across 30+ components with proper i18n t() calls. Added 57 new translation keys to all 15 locale files (ar, de, en, es, fa, fr, hi, it, ja, ko, nl, pl, pt, ru, zh). Key changes: - contextual-ai-chat.tsx: 30 French strings → t() (actions, toasts, labels, placeholders) - ai-chat.tsx: 15 French/English strings → t() (header, tabs, welcome, insights, history) - note-inline-editor.tsx: 20 French fallbacks removed (toolbar, save status, checklist) - lab-skeleton.tsx: French loading text → t() - admin-header.tsx, header.tsx, editor-connections-section.tsx: French fallbacks removed - New AI chat component, agent cards, sidebar, settings panel i18n cleanup Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
256 lines
8.2 KiB
TypeScript
256 lines
8.2 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { ChevronDown, ChevronUp, Sparkles, Eye, ArrowRight, Link2, X } from 'lucide-react'
|
|
import { Button } from '@/components/ui/button'
|
|
import { cn } from '@/lib/utils'
|
|
import { useLanguage } from '@/lib/i18n/LanguageProvider'
|
|
|
|
interface ConnectionData {
|
|
noteId: string
|
|
title: string | null
|
|
content: string
|
|
createdAt: Date
|
|
similarity: number
|
|
daysApart: number
|
|
}
|
|
|
|
interface ConnectionsResponse {
|
|
connections: ConnectionData[]
|
|
pagination: {
|
|
total: number
|
|
page: number
|
|
limit: number
|
|
totalPages: number
|
|
hasNext: boolean
|
|
hasPrev: boolean
|
|
}
|
|
}
|
|
|
|
interface EditorConnectionsSectionProps {
|
|
noteId: string
|
|
onOpenNote?: (noteId: string) => void
|
|
onCompareNotes?: (noteIds: string[]) => void
|
|
onMergeNotes?: (noteIds: string[]) => void
|
|
}
|
|
|
|
export function EditorConnectionsSection({
|
|
noteId,
|
|
onOpenNote,
|
|
onCompareNotes,
|
|
onMergeNotes
|
|
}: EditorConnectionsSectionProps) {
|
|
const { t } = useLanguage()
|
|
const [connections, setConnections] = useState<ConnectionData[]>([])
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
const [isExpanded, setIsExpanded] = useState(true)
|
|
const [isVisible, setIsVisible] = useState(true)
|
|
|
|
useEffect(() => {
|
|
const fetchConnections = async () => {
|
|
setIsLoading(true)
|
|
try {
|
|
const res = await fetch(`/api/ai/echo/connections?noteId=${noteId}&limit=10`)
|
|
if (!res.ok) {
|
|
throw new Error('Failed to fetch connections')
|
|
}
|
|
|
|
const data: ConnectionsResponse = await res.json()
|
|
setConnections(data.connections)
|
|
|
|
// Show section if there are connections
|
|
if (data.connections.length > 0) {
|
|
setIsVisible(true)
|
|
} else {
|
|
setIsVisible(false)
|
|
}
|
|
} catch (error) {
|
|
console.error('[EditorConnectionsSection] Failed to fetch:', error)
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
fetchConnections()
|
|
}, [noteId])
|
|
|
|
// Don't render if no connections or if dismissed
|
|
if (!isVisible || (connections.length === 0 && !isLoading)) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div className="mt-6 border-t dark:border-zinc-700 pt-4">
|
|
{/* Header with toggle */}
|
|
<div
|
|
className="flex items-center justify-between cursor-pointer select-none group"
|
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<div className="p-1.5 bg-amber-100 dark:bg-amber-900/30 rounded-full">
|
|
<Sparkles className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
|
</div>
|
|
<span className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
|
{t('memoryEcho.editorSection.title', { count: connections.length })}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 w-6 p-0 hover:bg-gray-100 dark:hover:bg-gray-800"
|
|
onClick={async (e) => {
|
|
e.stopPropagation()
|
|
|
|
// Dismiss all connections for this note
|
|
try {
|
|
await Promise.all(
|
|
connections.map(conn =>
|
|
fetch('/api/ai/echo/dismiss', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
noteId: noteId,
|
|
connectedNoteId: conn.noteId
|
|
})
|
|
})
|
|
)
|
|
)
|
|
|
|
setIsVisible(false)
|
|
} catch (error) {
|
|
console.error('❌ Failed to dismiss connections:', error)
|
|
}
|
|
}}
|
|
title={t('memoryEcho.editorSection.close') }
|
|
>
|
|
<X className="h-4 w-4 text-gray-500" />
|
|
</Button>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 w-6 p-0"
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
setIsExpanded(!isExpanded)
|
|
}}
|
|
>
|
|
{isExpanded ? (
|
|
<ChevronUp className="h-4 w-4 text-gray-500" />
|
|
) : (
|
|
<ChevronDown className="h-4 w-4 text-gray-500" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Connections list */}
|
|
{isExpanded && (
|
|
<div className="mt-3 space-y-2 max-h-[300px] overflow-y-auto">
|
|
{isLoading ? (
|
|
<div className="text-center py-4 text-sm text-gray-500">
|
|
{t('memoryEcho.editorSection.loading')}
|
|
</div>
|
|
) : (
|
|
connections.map((conn) => {
|
|
const similarityPercentage = Math.round(conn.similarity * 100)
|
|
const title = conn.title || t('memoryEcho.comparison.untitled')
|
|
|
|
return (
|
|
<div
|
|
key={conn.noteId}
|
|
className="border dark:border-zinc-700 rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-zinc-800/50 transition-colors"
|
|
>
|
|
<div className="flex items-start justify-between mb-2">
|
|
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 flex-1">
|
|
{title}
|
|
</h4>
|
|
<span className="ml-2 text-xs font-medium px-2 py-0.5 rounded-full bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400">
|
|
{similarityPercentage}%
|
|
</span>
|
|
</div>
|
|
|
|
<p className="text-xs text-gray-600 dark:text-gray-400 line-clamp-2 mb-2">
|
|
{conn.content}
|
|
</p>
|
|
|
|
<div className="flex items-center gap-1.5">
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
className="h-7 text-xs flex-1"
|
|
onClick={() => onOpenNote?.(conn.noteId)}
|
|
>
|
|
<Eye className="h-3 w-3 mr-1" />
|
|
{t('memoryEcho.editorSection.view')}
|
|
</Button>
|
|
|
|
{onCompareNotes && (
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
className="h-7 text-xs flex-1"
|
|
onClick={() => onCompareNotes([noteId, conn.noteId])}
|
|
>
|
|
<ArrowRight className="h-3 w-3 mr-1" />
|
|
{t('memoryEcho.editorSection.compare')}
|
|
</Button>
|
|
)}
|
|
|
|
{onMergeNotes && (
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
className="h-7 text-xs flex-1"
|
|
onClick={() => onMergeNotes([noteId, conn.noteId])}
|
|
>
|
|
<Link2 className="h-3 w-3 mr-1" />
|
|
{t('memoryEcho.editorSection.merge')}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
})
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Footer actions */}
|
|
{isExpanded && connections.length > 1 && (
|
|
<div className="mt-3 flex items-center gap-2 pt-2 border-t dark:border-zinc-700">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className="flex-1 text-xs"
|
|
onClick={() => {
|
|
if (onCompareNotes) {
|
|
const allIds = connections.slice(0, Math.min(3, connections.length)).map(c => c.noteId)
|
|
onCompareNotes([noteId, ...allIds])
|
|
}
|
|
}}
|
|
>
|
|
{t('memoryEcho.editorSection.compareAll')}
|
|
</Button>
|
|
|
|
{onMergeNotes && (
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className="flex-1 text-xs"
|
|
onClick={() => {
|
|
const allIds = connections.map(c => c.noteId)
|
|
onMergeNotes([noteId, ...allIds])
|
|
}}
|
|
>
|
|
{t('memoryEcho.editorSection.mergeAll')}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|