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" />
{/* エディタ */}