import React, { useEffect, useState, useRef } from 'react'
import { ArrowLeft, Plus, FileText, File as FileIcon, MoreVertical, Trash2, Edit2, MessageSquare, Eye, EyeOff, Database, Book } from 'lucide-react'
import ReactMarkdown from 'react-markdown'
import { KnowledgeGroup, KnowledgeFile } from '../../types'
import { noteService, Note } from '../../services/noteService'
import { knowledgeBaseService } from '../../services/knowledgeBaseService'
import { modelConfigService } from '../../services/modelConfigService'
import { uploadService } from '../../services/uploadService'
import { knowledgeGroupService } from '../../services/knowledgeGroupService'
import { useToast } from '../../contexts/ToastContext'
import { ModelType, RawFile, IndexingConfig, ModelConfig } from '../../types'
import IndexingModalWithMode from '../IndexingModalWithMode'
import { PDFPreview, PDFPreviewButton } from '../PDFPreview'
import { AICommandDrawer } from '../AICommandDrawer'
import { ImportFolderDrawer } from '../ImportFolderDrawer'
import { NotebookGlobalDragDropOverlay } from '../NotebookGlobalDragDropOverlay'
import { useLanguage } from '../../contexts/LanguageContext'
import { readFile } from '../../utils/fileUtils'
import { Sparkles } from 'lucide-react'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import rehypeKatex from 'rehype-katex'
import mermaid from 'mermaid'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'
// mermaid の初期化
mermaid.initialize({
startOnLoad: false,
theme: 'default',
securityLevel: 'loose',
})
const Mermaid = ({ chart }: { chart: string }) => {
const [svg, setSvg] = useState('')
const [error, setError] = useState('')
const { t } = useLanguage()
useEffect(() => {
const renderChart = async () => {
try {
const id = `mermaid-${Math.random().toString(36).substr(2, 9)}`
const { svg } = await mermaid.render(id, chart)
setSvg(svg)
setError('')
} catch (err) {
console.error('Mermaid render error:', err)
setError(t('errorRenderFlowchart'))
}
}
if (chart) renderChart()
}, [chart])
if (error) return
{error}
return
}
interface NotebookDetailViewProps {
authToken: string;
notebook: KnowledgeGroup;
onBack: () => void;
onChatWithContext?: (context: { selectedGroups?: string[], selectedFiles?: string[] }) => void;
isAdmin?: boolean;
}
export const NotebookDetailView: React.FC = ({ authToken, notebook, onBack, onChatWithContext, isAdmin = false }) => {
const [activeTab, setActiveTab] = useState<'files' | 'notes'>('files')
const [files, setFiles] = useState([])
const [notes, setNotes] = useState([])
const [isLoading, setIsLoading] = useState(false)
const { showError, showSuccess } = useToast()
const { t, language } = useLanguage()
// メモエディタの状態
const [isEditingNote, setIsEditingNote] = useState(false)
const [currentNote, setCurrentNote] = useState>({})
const [showPreview, setShowPreview] = useState(true)
const [isUploading, setIsUploading] = useState(false)
const fileInputRef = React.useRef(null)
const [isImportDrawerOpen, setIsImportDrawerOpen] = useState(false)
// インデックスモーダルの状態
const [isIndexingModalOpen, setIsIndexingModalOpen] = useState(false)
const [pendingFiles, setPendingFiles] = useState([])
const [models, setModels] = useState([])
// PDFプレビューの状態
const [previewFile, setPreviewFile] = useState<{ id: string, name: string } | null>(null);
// AIアシスタントの状態
const [isAIModalOpen, setIsAIModalOpen] = useState(false)
useEffect(() => {
// モーダル用のモデルを取得
const fetchModels = async () => {
try {
const res = await modelConfigService.getAll(authToken)
setModels(res)
} catch (error) {
console.error('Failed to fetch models', error)
}
}
if (authToken) fetchModels()
}, [authToken])
const loadData = async () => {
setIsLoading(true)
try {
const [allFiles, notebookNotes] = await Promise.all([
knowledgeBaseService.getAll(authToken),
noteService.getAll(authToken, notebook.id)
])
const notebookFiles = allFiles.filter(f => f.groups?.some(g => g.id === notebook.id))
setFiles(notebookFiles)
setNotes(notebookNotes)
} catch (error) {
console.error(error)
showError(t('errorLoadData'))
} finally {
setIsLoading(false)
}
}
useEffect(() => {
loadData()
}, [authToken, notebook.id, activeTab])
const handleFileUpload = async (e: React.ChangeEvent) => {
const file = e.target.files?.[0]
if (!file) return
try {
// ファイルの有効性をチェック
const ext = file.name.split('.').pop()?.toLowerCase()
const allowedExts = ['pdf', 'doc', 'docx', 'txt', 'md', 'html', 'json']
if (!ext || !allowedExts.includes(ext)) {
if (!window.confirm(t('confirmUnsupportedFile', ext))) {
if (fileInputRef.current) fileInputRef.current.value = ''
return;
}
}
// ファイルを読み込んでモーダルを開く
const rawFile = await readFile(file)
setPendingFiles([rawFile])
setIsIndexingModalOpen(true)
} catch (error) {
console.error(error)
showError(t('errorReadFile', error.message || t('unknownError')))
} finally {
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
}
}
const handleConfirmIndexing = async (config: IndexingConfig) => {
setIsUploading(true)
setIsIndexingModalOpen(false)
try {
for (const rawFile of pendingFiles) {
let uploadRes;
if (rawFile.isNote) {
uploadRes = await uploadService.uploadText(rawFile.textContent || rawFile.content, rawFile.name, config, authToken)
} else {
uploadRes = await uploadService.uploadFileWithConfig(rawFile.file, config, authToken)
}
if (uploadRes && uploadRes.id) {
await knowledgeGroupService.addFileToGroups(uploadRes.id, [notebook.id])
}
}
showSuccess(t('successUploadFile'))
loadData()
} catch (error) {
console.error(error)
showError(t('errorUploadFile', error.message || t('unknownError')))
} finally {
setIsUploading(false)
setPendingFiles([])
}
}
const handleIndexNote = async (note: Note) => {
if (!note.title || !note.content) return;
try {
const file = new File([note.content], `${note.title}.md`, { type: 'text/markdown' });
const rawFile = await readFile(file);
rawFile.isNote = true;
rawFile.textContent = note.content;
setPendingFiles([rawFile]);
setIsIndexingModalOpen(true);
} catch (error) {
console.error(error);
showError(t('errorProcessFile'));
}
}
const handleSaveNote = async () => {
if (!currentNote.title || !currentNote.content) {
showError(t('errorTitleContentRequired'))
return
}
try {
if (currentNote.id) {
await noteService.update(authToken, currentNote.id, {
title: currentNote.title,
content: currentNote.content
})
showSuccess(t('successNoteUpdated'))
} else {
await noteService.create(authToken, {
title: currentNote.title,
content: currentNote.content,
groupId: notebook.id
})
showSuccess(t('successNoteCreated'))
}
setIsEditingNote(false)
setCurrentNote({})
loadData()
} catch (error: any) {
console.error('Save note error:', error)
showError(t('errorSaveFailed', error.message || t('unknownError')))
}
}
const handleDeleteNote = async (id: string) => {
if (!window.confirm(t('confirmDeleteNote'))) return
try {
await noteService.delete(authToken, id)
showSuccess(t('successNoteDeleted'))
loadData()
} catch (error) {
showError(t('deleteFailed'))
}
}
const handleRemoveFile = async (fileId: string, fileName: string) => {
if (!window.confirm(t('confirmRemoveFileFromGroup', fileName))) return;
try {
const { knowledgeGroupService } = await import('../../services/knowledgeGroupService');
await knowledgeGroupService.removeFileFromGroup(fileId, notebook.id);
showSuccess(t('fileDeleted'));
loadData();
} catch (error) {
console.error(error);
showError(t('deleteFailed'));
}
}
// メモエディタのレンダリング
if (isEditingNote) {
return (
{/* エディタヘッダー */}
{currentNote.id ? t('editNote') : t('newNote')}
setCurrentNote(prev => ({ ...prev, title: e.target.value }))}
className="w-full text-2xl font-bold border-none outline-none bg-transparent mb-4 placeholder-slate-300 shrink-0 px-2"
/>
{/* エディタ */}
{/* プレビュー */}
{showPreview && (
{currentNote.content ? (
}
return !inline && match ? (
{String(children).replace(/\n$/, '')}
) : (
{children}
)
}
}}
>
{currentNote.content}
) : (
{t('markdownPreviewArea')}
)}
)}
{/* AIコマンドドロワー - エディタ表示用 */}
setIsAIModalOpen(false)}
context={currentNote.content || ''}
authToken={authToken}
onApply={(newContent) => {
setCurrentNote(prev => ({ ...prev, content: newContent }))
}}
/>
)
}
return (
{/* ヘッダー */}
{notebook.name}
{notebook.description &&
{notebook.description}
}
{/* コンテンツアクション */}
{activeTab === 'notes' && (
)}
{activeTab === 'files' && (
<>
{isAdmin && (
)}
{isAdmin && (
)}
{isAdmin && (
)}
>
)}
{/* 笔记本全局拖拽上传覆盖层 */}
{
// 处理拖拽上传的文件
for (let i = 0; i < files.length; i++) {
const file = files[i];
// 创建一个临时input元素来触发上传
const input = document.createElement('input');
input.type = 'file';
// 使用DataTransfer来创建一个包含单个文件的FileList
const dt = new DataTransfer();
dt.items.add(file);
// 创建自定义事件对象
const event = {
target: {
files: dt.files
}
} as React.ChangeEvent;
await handleFileUpload(event);
}
}}
isAdmin={isAdmin}
/>
{/* リスト表示 */}
{isLoading ? (
Loading...
) : (
{activeTab === 'files' && files.map(file => (
{file.name}
{(file.size / 1024 / 1024).toFixed(2)} MB
setPreviewFile({ id: file.id, name: file.name })}
/>
))}
{activeTab === 'notes' && notes.map(note => (
{
setCurrentNote(note)
setIsEditingNote(true)
}}>
{note.title}
e.stopPropagation()}>
{note.content}
{note.screenshotPath && (
)}
{new Date(note.updatedAt).toLocaleDateString()}
))}
{((activeTab === 'files' && files.length === 0) || (activeTab === 'notes' && notes.length === 0)) && (
{t('noFilesOrNotes', activeTab === 'files' ? t('files') : t('notes'))}
)}
)}
{/* インデックスモーダル */}
{
setIsIndexingModalOpen(false)
setPendingFiles([])
}}
onConfirm={handleConfirmIndexing}
files={pendingFiles}
embeddingModels={models.filter(m => m.type === ModelType.EMBEDDING)}
defaultEmbeddingId={models.find(m => m.type === ModelType.EMBEDDING)?.id || ''}
/>
{/* PDFプレビューモーダル */}
{
previewFile && (
setPreviewFile(null)}
/>
)
}
{/* AIコマンドドロワー */}
setIsAIModalOpen(false)}
context={currentNote.content || ''}
authToken={authToken}
onApply={(newContent) => {
setCurrentNote(prev => ({ ...prev, content: newContent })) // 内容を置換(必要に応じて将来的に追加オプションを検討)
}}
/>
setIsImportDrawerOpen(false)}
authToken={authToken}
initialGroupId={notebook.id}
initialGroupName={notebook.name}
onImportSuccess={loadData}
/>
)
}