diff --git a/api/app/api/router.py b/api/app/api/router.py index 0c107bb..6117fd5 100644 --- a/api/app/api/router.py +++ b/api/app/api/router.py @@ -6,6 +6,7 @@ from .v1.ai_chat import router as ai_chat_router from .v1.atp_assets import router as atp_assets_router from .v1.atp_models import router as atp_models_router from .v1.auth import router as auth_router +from .v1.documents import router as documents_router from .v1.elevation import router as elevation_router from .v1.fault_recurrence import router as fault_recurrence_router from .v1.fl_analysis import router as fl_analysis_router @@ -30,6 +31,7 @@ v1_router.include_router(admin_files_router) v1_router.include_router(ai_chat_router) v1_router.include_router(atp_assets_router) v1_router.include_router(atp_models_router) +v1_router.include_router(documents_router) v1_router.include_router(task_monitor_router) v1_router.include_router(scheduled_tasks_router) v1_router.include_router(system_messages_router) diff --git a/api/app/api/v1/documents.py b/api/app/api/v1/documents.py new file mode 100644 index 0000000..4ece8bc --- /dev/null +++ b/api/app/api/v1/documents.py @@ -0,0 +1,230 @@ +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.orm import Session + +from ...core.database import get_db +from ...core.dependencies import ( + CurrentUser, + get_current_user, + require_enabled_menu_route, +) +from ...schemas.document import ( + DocumentChapterCreateRequest, + DocumentChapterListResponse, + DocumentChapterPublic, + DocumentChapterTreeItem, + DocumentChapterUpdateRequest, + DocumentCreateRequest, + DocumentListResponse, + DocumentPublic, + DocumentUpdateRequest, +) +from ...services.document_service import ( + create_document, + create_document_chapter, + delete_document, + delete_document_chapter, + get_document_by_id, + get_document_chapter_by_id, + get_document_chapter_tree, + list_document_chapters, + list_documents, + serialize_document, + serialize_document_chapter, + update_document, + update_document_chapter, +) + +router = APIRouter(prefix="/documents", tags=["documents"]) + + +@router.get( + "/chapters", + response_model=DocumentChapterListResponse, + dependencies=[Depends(require_enabled_menu_route)], +) +def get_document_chapters( + keyword: str | None = Query(default=None), + limit: int = Query(default=50, ge=1, le=200), + offset: int = Query(default=0, ge=0), + _: CurrentUser = Depends(get_current_user), + db: Session = Depends(get_db), +) -> DocumentChapterListResponse: + return list_document_chapters(db, keyword=keyword, limit=limit, offset=offset) + + +@router.get( + "/chapters/tree", + response_model=list[DocumentChapterTreeItem], + dependencies=[Depends(require_enabled_menu_route)], +) +def get_chapters_tree( + _: CurrentUser = Depends(get_current_user), + db: Session = Depends(get_db), +) -> list[DocumentChapterTreeItem]: + return get_document_chapter_tree(db) + + +@router.get( + "/chapters/{chapter_id}", + response_model=DocumentChapterPublic, + dependencies=[Depends(require_enabled_menu_route)], +) +def get_chapter_detail( + chapter_id: int, + _: CurrentUser = Depends(get_current_user), + db: Session = Depends(get_db), +) -> DocumentChapterPublic: + chapter = get_document_chapter_by_id(db, chapter_id) + if not chapter: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="章节不存在", + ) + return serialize_document_chapter(chapter) + + +@router.post( + "/chapters", + response_model=DocumentChapterPublic, + dependencies=[Depends(require_enabled_menu_route)], +) +def create_chapter( + payload: DocumentChapterCreateRequest, + _: CurrentUser = Depends(get_current_user), + db: Session = Depends(get_db), +) -> DocumentChapterPublic: + chapter = create_document_chapter(db, payload) + return serialize_document_chapter(chapter) + + +@router.patch( + "/chapters/{chapter_id}", + response_model=DocumentChapterPublic, + dependencies=[Depends(require_enabled_menu_route)], +) +def update_chapter( + chapter_id: int, + payload: DocumentChapterUpdateRequest, + _: CurrentUser = Depends(get_current_user), + db: Session = Depends(get_db), +) -> DocumentChapterPublic: + chapter = update_document_chapter(db, chapter_id, payload) + if not chapter: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="章节不存在", + ) + return serialize_document_chapter(chapter) + + +@router.delete( + "/chapters/{chapter_id}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(require_enabled_menu_route)], +) +def delete_chapter( + chapter_id: int, + _: CurrentUser = Depends(get_current_user), + db: Session = Depends(get_db), +): + success = delete_document_chapter(db, chapter_id) + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="章节不存在", + ) + + +@router.get( + "", + response_model=DocumentListResponse, + dependencies=[Depends(require_enabled_menu_route)], +) +def get_documents( + keyword: str | None = Query(default=None), + chapter_id: int | None = Query(default=None), + status_filter: str | None = Query(default=None, alias="status"), + limit: int = Query(default=50, ge=1, le=200), + offset: int = Query(default=0, ge=0), + _: CurrentUser = Depends(get_current_user), + db: Session = Depends(get_db), +) -> DocumentListResponse: + return list_documents( + db, + keyword=keyword, + chapter_id=chapter_id, + status=status_filter, + limit=limit, + offset=offset, + ) + + +@router.get( + "/{document_id}", + response_model=DocumentPublic, + dependencies=[Depends(require_enabled_menu_route)], +) +def get_document_detail( + document_id: int, + _: CurrentUser = Depends(get_current_user), + db: Session = Depends(get_db), +) -> DocumentPublic: + document = get_document_by_id(db, document_id) + if not document: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="文档不存在", + ) + return serialize_document(document) + + +@router.post( + "", + response_model=DocumentPublic, + dependencies=[Depends(require_enabled_menu_route)], +) +def create_document_endpoint( + payload: DocumentCreateRequest, + _: CurrentUser = Depends(get_current_user), + db: Session = Depends(get_db), +) -> DocumentPublic: + document = create_document(db, payload) + return serialize_document(document) + + +@router.patch( + "/{document_id}", + response_model=DocumentPublic, + dependencies=[Depends(require_enabled_menu_route)], +) +def update_document_endpoint( + document_id: int, + payload: DocumentUpdateRequest, + _: CurrentUser = Depends(get_current_user), + db: Session = Depends(get_db), +) -> DocumentPublic: + document = update_document(db, document_id, payload) + if not document: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="文档不存在", + ) + return serialize_document(document) + + +@router.delete( + "/{document_id}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(require_enabled_menu_route)], +) +def delete_document_endpoint( + document_id: int, + _: CurrentUser = Depends(get_current_user), + db: Session = Depends(get_db), +): + success = delete_document(db, document_id) + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="文档不存在", + ) diff --git a/api/app/models/__init__.py b/api/app/models/__init__.py index 52842f0..c5e156a 100644 --- a/api/app/models/__init__.py +++ b/api/app/models/__init__.py @@ -4,7 +4,7 @@ Import all model modules during package initialization so SQLAlchemy can resolve string-based relationships regardless of route/service import order. """ -from . import ai_chat, atp_asset, atp_model, audit_log, auth_session, elevation, file_storage, fl_analysis, lightning_event, lightning_sample, line, line_tower, menu, object_group, rbac, scheduled_task, system_message, system_param, tower_model, tower_profile, user, wine, worker_registry +from . import ai_chat, atp_asset, atp_model, audit_log, auth_session, document, elevation, file_storage, fl_analysis, lightning_event, lightning_sample, line, line_tower, menu, object_group, rbac, scheduled_task, system_message, system_param, tower_model, tower_profile, user, wine, worker_registry __all__ = [ "ai_chat", @@ -12,6 +12,7 @@ __all__ = [ "atp_model", "audit_log", "auth_session", + "document", "elevation", "file_storage", "fl_analysis", diff --git a/api/app/models/document.py b/api/app/models/document.py new file mode 100644 index 0000000..acded22 --- /dev/null +++ b/api/app/models/document.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from sqlalchemy import ForeignKey, Integer, String, Text, DateTime +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.sql import func + +from ..core.database import Base + +if TYPE_CHECKING: + pass + + +class DocumentChapter(Base): + __tablename__ = "document_chapters" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String(128), index=True) + description: Mapped[str | None] = mapped_column(String(512)) + parent_id: Mapped[int | None] = mapped_column( + ForeignKey("document_chapters.id", ondelete="CASCADE"), + index=True, + ) + sort_order: Mapped[int] = mapped_column(Integer, default=0, index=True) + created_at: Mapped[DateTime] = mapped_column(DateTime, server_default=func.now()) + updated_at: Mapped[DateTime] = mapped_column( + DateTime, server_default=func.now(), onupdate=func.now() + ) + + parent: Mapped[DocumentChapter | None] = relationship( + "DocumentChapter", + remote_side=lambda: [DocumentChapter.id], + back_populates="children", + lazy="selectin", + ) + children: Mapped[list[DocumentChapter]] = relationship( + "DocumentChapter", + back_populates="parent", + lazy="selectin", + order_by="DocumentChapter.sort_order", + cascade="all, delete-orphan", + ) + documents: Mapped[list[Document]] = relationship( + "Document", + back_populates="chapter", + lazy="selectin", + order_by="Document.sort_order", + cascade="all, delete-orphan", + ) + + +class Document(Base): + __tablename__ = "documents" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + title: Mapped[str] = mapped_column(String(256), index=True) + content: Mapped[str] = mapped_column(Text) + chapter_id: Mapped[int | None] = mapped_column( + ForeignKey("document_chapters.id", ondelete="CASCADE"), + index=True, + ) + sort_order: Mapped[int] = mapped_column(Integer, default=0, index=True) + status: Mapped[str] = mapped_column( + String(16), default="draft", index=True + ) + created_at: Mapped[DateTime] = mapped_column(DateTime, server_default=func.now()) + updated_at: Mapped[DateTime] = mapped_column( + DateTime, server_default=func.now(), onupdate=func.now() + ) + + chapter: Mapped[DocumentChapter | None] = relationship( + "DocumentChapter", + back_populates="documents", + lazy="selectin", + ) diff --git a/api/app/schemas/document.py b/api/app/schemas/document.py new file mode 100644 index 0000000..f9be4be --- /dev/null +++ b/api/app/schemas/document.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Literal + +from pydantic import BaseModel, Field + + +class DocumentChapterPublic(BaseModel): + id: int + name: str + description: str | None = None + parent_id: int | None = None + sort_order: int + created_at: datetime + updated_at: datetime + + +class DocumentChapterTreeItem(DocumentChapterPublic): + children: list["DocumentChapterTreeItem"] = Field(default_factory=list) + documents: list["DocumentPublic"] = Field(default_factory=list) + + +class DocumentChapterListResponse(BaseModel): + items: list[DocumentChapterPublic] + total: int + + +class DocumentChapterCreateRequest(BaseModel): + name: str = Field(min_length=1, max_length=128) + description: str | None = Field(default=None, max_length=512) + parent_id: int | None = None + sort_order: int = 0 + + +class DocumentChapterUpdateRequest(BaseModel): + name: str | None = Field(default=None, min_length=1, max_length=128) + description: str | None = Field(default=None, max_length=512) + parent_id: int | None = None + sort_order: int | None = None + + +class DocumentPublic(BaseModel): + id: int + title: str + content: str + chapter_id: int | None = None + sort_order: int + status: str + created_at: datetime + updated_at: datetime + + +class DocumentListResponse(BaseModel): + items: list[DocumentPublic] + total: int + + +class DocumentCreateRequest(BaseModel): + title: str = Field(min_length=1, max_length=256) + content: str + chapter_id: int | None = None + sort_order: int = 0 + status: Literal["draft", "published"] = "draft" + + +class DocumentUpdateRequest(BaseModel): + title: str | None = Field(default=None, min_length=1, max_length=256) + content: str | None = None + chapter_id: int | None = None + sort_order: int | None = None + status: Literal["draft", "published"] | None = None diff --git a/api/app/services/document_service.py b/api/app/services/document_service.py new file mode 100644 index 0000000..722156e --- /dev/null +++ b/api/app/services/document_service.py @@ -0,0 +1,251 @@ +from __future__ import annotations + +from sqlalchemy import func, select +from sqlalchemy.orm import Session + +from ..models.document import Document, DocumentChapter +from ..schemas.document import ( + DocumentChapterCreateRequest, + DocumentChapterListResponse, + DocumentChapterPublic, + DocumentChapterTreeItem, + DocumentChapterUpdateRequest, + DocumentCreateRequest, + DocumentListResponse, + DocumentPublic, + DocumentUpdateRequest, +) + + +def serialize_document_chapter(chapter: DocumentChapter) -> DocumentChapterPublic: + return DocumentChapterPublic( + id=chapter.id, + name=chapter.name, + description=chapter.description, + parent_id=chapter.parent_id, + sort_order=chapter.sort_order, + created_at=chapter.created_at, + updated_at=chapter.updated_at, + ) + + +def serialize_document(document: Document) -> DocumentPublic: + return DocumentPublic( + id=document.id, + title=document.title, + content=document.content, + chapter_id=document.chapter_id, + sort_order=document.sort_order, + status=document.status, + created_at=document.created_at, + updated_at=document.updated_at, + ) + + +def build_chapter_tree(chapters: list[DocumentChapter]) -> list[DocumentChapterTreeItem]: + chapter_map = {c.id: c for c in chapters} + tree = [] + + for chapter in chapters: + if chapter.parent_id is None: + tree_item = DocumentChapterTreeItem( + id=chapter.id, + name=chapter.name, + description=chapter.description, + parent_id=chapter.parent_id, + sort_order=chapter.sort_order, + created_at=chapter.created_at, + updated_at=chapter.updated_at, + children=[], + documents=[serialize_document(d) for d in chapter.documents], + ) + tree.append(tree_item) + + def add_children(node: DocumentChapterTreeItem): + for chapter in chapters: + if chapter.parent_id == node.id: + child_item = DocumentChapterTreeItem( + id=chapter.id, + name=chapter.name, + description=chapter.description, + parent_id=chapter.parent_id, + sort_order=chapter.sort_order, + created_at=chapter.created_at, + updated_at=chapter.updated_at, + children=[], + documents=[serialize_document(d) for d in chapter.documents], + ) + node.children.append(child_item) + add_children(child_item) + + for item in tree: + add_children(item) + + return tree + + +def list_document_chapters( + db: Session, + *, + limit: int, + offset: int, + keyword: str | None = None, +) -> DocumentChapterListResponse: + stmt = select(DocumentChapter) + if keyword: + stmt = stmt.where(DocumentChapter.name.ilike(f"%{keyword}%")) + stmt = stmt.order_by(DocumentChapter.sort_order, DocumentChapter.id) + stmt = stmt.limit(limit).offset(offset) + + total_stmt = select(func.count()).select_from(DocumentChapter) + if keyword: + total_stmt = total_stmt.where(DocumentChapter.name.ilike(f"%{keyword}%")) + + total = db.scalar(total_stmt) or 0 + chapters = list(db.scalars(stmt).all()) + + return DocumentChapterListResponse( + items=[serialize_document_chapter(c) for c in chapters], + total=total, + ) + + +def get_document_chapter_tree(db: Session) -> list[DocumentChapterTreeItem]: + stmt = select(DocumentChapter).order_by(DocumentChapter.sort_order, DocumentChapter.id) + chapters = list(db.scalars(stmt).all()) + return build_chapter_tree(chapters) + + +def get_document_chapter_by_id(db: Session, chapter_id: int) -> DocumentChapter | None: + return db.get(DocumentChapter, chapter_id) + + +def create_document_chapter( + db: Session, payload: DocumentChapterCreateRequest +) -> DocumentChapter: + chapter = DocumentChapter( + name=payload.name, + description=payload.description, + parent_id=payload.parent_id, + sort_order=payload.sort_order, + ) + db.add(chapter) + db.commit() + db.refresh(chapter) + return chapter + + +def update_document_chapter( + db: Session, chapter_id: int, payload: DocumentChapterUpdateRequest +) -> DocumentChapter | None: + chapter = db.get(DocumentChapter, chapter_id) + if not chapter: + return None + + if payload.name is not None: + chapter.name = payload.name + if payload.description is not None: + chapter.description = payload.description + if payload.parent_id is not None: + chapter.parent_id = payload.parent_id + if payload.sort_order is not None: + chapter.sort_order = payload.sort_order + + db.commit() + db.refresh(chapter) + return chapter + + +def delete_document_chapter(db: Session, chapter_id: int) -> bool: + chapter = db.get(DocumentChapter, chapter_id) + if not chapter: + return False + db.delete(chapter) + db.commit() + return True + + +def list_documents( + db: Session, + *, + limit: int, + offset: int, + keyword: str | None = None, + chapter_id: int | None = None, + status: str | None = None, +) -> DocumentListResponse: + stmt = select(Document) + if keyword: + stmt = stmt.where(Document.title.ilike(f"%{keyword}%")) + if chapter_id is not None: + stmt = stmt.where(Document.chapter_id == chapter_id) + if status: + stmt = stmt.where(Document.status == status) + stmt = stmt.order_by(Document.sort_order, Document.id) + stmt = stmt.limit(limit).offset(offset) + + total_stmt = select(func.count()).select_from(Document) + if keyword: + total_stmt = total_stmt.where(Document.title.ilike(f"%{keyword}%")) + if chapter_id is not None: + total_stmt = total_stmt.where(Document.chapter_id == chapter_id) + if status: + total_stmt = total_stmt.where(Document.status == status) + + total = db.scalar(total_stmt) or 0 + documents = list(db.scalars(stmt).all()) + + return DocumentListResponse( + items=[serialize_document(d) for d in documents], + total=total, + ) + + +def get_document_by_id(db: Session, document_id: int) -> Document | None: + return db.get(Document, document_id) + + +def create_document(db: Session, payload: DocumentCreateRequest) -> Document: + document = Document( + title=payload.title, + content=payload.content, + chapter_id=payload.chapter_id, + sort_order=payload.sort_order, + status=payload.status, + ) + db.add(document) + db.commit() + db.refresh(document) + return document + + +def update_document( + db: Session, document_id: int, payload: DocumentUpdateRequest +) -> Document | None: + document = db.get(Document, document_id) + if not document: + return None + + if payload.title is not None: + document.title = payload.title + if payload.content is not None: + document.content = payload.content + if payload.chapter_id is not None: + document.chapter_id = payload.chapter_id + if payload.sort_order is not None: + document.sort_order = payload.sort_order + if payload.status is not None: + document.status = payload.status + + db.commit() + db.refresh(document) + return document + + +def delete_document(db: Session, document_id: int) -> bool: + document = db.get(Document, document_id) + if not document: + return False + db.delete(document) + db.commit() + return True diff --git a/api/migrations/002_add_document_management.sql b/api/migrations/002_add_document_management.sql new file mode 100644 index 0000000..6b2a850 --- /dev/null +++ b/api/migrations/002_add_document_management.sql @@ -0,0 +1,65 @@ +-- Migration: Add document management tables +-- Date: 2026-06-20 +-- Description: Create tables for managing operational documents organized by chapters + +-- Step 1: Create document_chapters table +CREATE TABLE IF NOT EXISTS document_chapters ( + id SERIAL PRIMARY KEY, + name VARCHAR(128) NOT NULL, + description VARCHAR(512), + parent_id INTEGER REFERENCES document_chapters(id) ON DELETE CASCADE, + sort_order INTEGER DEFAULT 0 NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL +); + +-- Create indexes for document_chapters +CREATE INDEX IF NOT EXISTS idx_document_chapters_name ON document_chapters(name); +CREATE INDEX IF NOT EXISTS idx_document_chapters_parent_id ON document_chapters(parent_id); +CREATE INDEX IF NOT EXISTS idx_document_chapters_sort_order ON document_chapters(sort_order); + +-- Step 2: Create documents table +CREATE TABLE IF NOT EXISTS documents ( + id SERIAL PRIMARY KEY, + title VARCHAR(256) NOT NULL, + content TEXT NOT NULL, + chapter_id INTEGER REFERENCES document_chapters(id) ON DELETE CASCADE, + sort_order INTEGER DEFAULT 0 NOT NULL, + status VARCHAR(16) DEFAULT 'draft' NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL +); + +-- Create indexes for documents +CREATE INDEX IF NOT EXISTS idx_documents_title ON documents(title); +CREATE INDEX IF NOT EXISTS idx_documents_chapter_id ON documents(chapter_id); +CREATE INDEX IF NOT EXISTS idx_documents_sort_order ON documents(sort_order); +CREATE INDEX IF NOT EXISTS idx_documents_status ON documents(status); + +-- Step 3: Create trigger to update updated_at on document_chapters +CREATE OR REPLACE FUNCTION update_document_chapters_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trigger_update_document_chapters_updated_at + BEFORE UPDATE ON document_chapters + FOR EACH ROW + EXECUTE FUNCTION update_document_chapters_updated_at(); + +-- Step 4: Create trigger to update updated_at on documents +CREATE OR REPLACE FUNCTION update_documents_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trigger_update_documents_updated_at + BEFORE UPDATE ON documents + FOR EACH ROW + EXECUTE FUNCTION update_documents_updated_at(); diff --git a/package-lock.json b/package-lock.json index 452679c..3763739 100644 --- a/package-lock.json +++ b/package-lock.json @@ -571,6 +571,54 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "25.6.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", @@ -580,6 +628,15 @@ "undici-types": "~7.19.0" } }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -587,12 +644,24 @@ "license": "MIT", "optional": true }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "license": "ISC" + }, "node_modules/@zip.js/zip.js": { "version": "2.8.26", "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.26.tgz", @@ -681,12 +750,32 @@ "pnpm": ">=10.10.0" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/bitmap-sdf": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/bitmap-sdf/-/bitmap-sdf-1.0.4.tgz", "integrity": "sha512-1G3U4n5JE6RAiALMxu0p1XmeZkTeCwGKykzsLTCqVzfSDaN6S7fKnkIkfejogz+iwqBWc0UYAIKnKHNN7pSfDg==", "license": "MIT" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/cesium": { "version": "1.140.0", "resolved": "https://registry.npmjs.org/cesium/-/cesium-1.140.0.tgz", @@ -705,6 +794,46 @@ "node": ">=20.19.0" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -738,6 +867,16 @@ "node": ">=6" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -892,12 +1031,64 @@ "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", "license": "MIT" }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dom-align": { "version": "1.12.4", "resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.12.4.tgz", @@ -935,12 +1126,28 @@ "benchmarks" ] }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/eventemitter3": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/framer-motion": { "version": "12.38.0", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", @@ -974,6 +1181,56 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "license": "MIT" }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/immer": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", @@ -984,6 +1241,12 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -993,6 +1256,62 @@ "node": ">=12" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jsep": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", @@ -1061,6 +1380,169 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mersenne-twister": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/mersenne-twister/-/mersenne-twister-1.1.0.tgz", @@ -1073,6 +1555,448 @@ "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", "license": "MIT" }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/motion-dom": { "version": "12.38.0", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", @@ -1094,6 +2018,12 @@ "integrity": "sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==", "license": "Apache-2.0 WITH LLVM-exception" }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/nosleep.js": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/nosleep.js/-/nosleep.js-0.12.0.tgz", @@ -1106,6 +2036,41 @@ "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", "license": "(MIT AND Zlib)" }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/protobufjs": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.0.1.tgz", @@ -1776,6 +2741,33 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/react-redux": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", @@ -1844,6 +2836,39 @@ "redux": "^5.0.0" } }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/reselect": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", @@ -1871,12 +2896,54 @@ "compute-scroll-into-view": "^3.0.2" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/string-convert": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", "license": "MIT" }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/stylis": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", @@ -1925,6 +2992,26 @@ "topoquantize": "bin/topoquantize" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -1937,6 +3024,93 @@ "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "license": "MIT" }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/urijs": { "version": "1.19.11", "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", @@ -1961,6 +3135,34 @@ "node": ">= 4" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/victory-vendor": { "version": "37.3.6", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", @@ -1987,22 +3189,31 @@ "resolved": "web", "link": true }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "web": { "version": "0.1.0", "hasInstallScript": true, "dependencies": { "@antv/x6": "^3.1.7", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tanstack/react-query": "^5.90.5", "antd": "^5.29.3", "cesium": "^1.140.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.38.0", - "lightningcss-linux-arm64-gnu": "1.32.0", "next": "16.2.3", "react": "19.2.4", "react-dom": "19.2.4", + "react-markdown": "^10.1.0", "react-redux": "^9.3.0", "recharts": "^3.8.1", "reselect": "^5.2.0", @@ -2633,11 +3844,6 @@ "tailwindcss": "4.2.2" } }, - "web/node_modules/@types/estree": { - "version": "1.0.8", - "dev": true, - "license": "MIT" - }, "web/node_modules/@types/json-schema": { "version": "7.0.15", "dev": true, @@ -2656,14 +3862,6 @@ "undici-types": "~6.21.0" } }, - "web/node_modules/@types/react": { - "version": "19.2.14", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, "web/node_modules/@types/react-dom": { "version": "19.2.3", "dev": true, @@ -3439,22 +4637,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "web/node_modules/debug": { - "version": "4.4.3", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "web/node_modules/deep-is": { "version": "0.1.4", "dev": true, @@ -5150,11 +6332,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "web/node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, "web/node_modules/nanoid": { "version": "3.3.11", "funding": [ diff --git a/web/package.json b/web/package.json index 5e070dc..34741ef 100644 --- a/web/package.json +++ b/web/package.json @@ -21,6 +21,7 @@ "next": "16.2.3", "react": "19.2.4", "react-dom": "19.2.4", + "react-markdown": "^10.1.0", "react-redux": "^9.3.0", "recharts": "^3.8.1", "reselect": "^5.2.0", diff --git a/web/src/app/admin/docs-view/page.tsx b/web/src/app/admin/docs-view/page.tsx new file mode 100644 index 0000000..bc01c70 --- /dev/null +++ b/web/src/app/admin/docs-view/page.tsx @@ -0,0 +1,225 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { + Card, + Col, + Empty, + Layout, + Menu, + Row, + Spin, + Typography, + theme, +} from "antd"; +import { + FolderOutlined, + FileTextOutlined, +} from "@ant-design/icons"; +import { useCallback, useState } from "react"; +import type { MenuProps } from "antd"; +import ReactMarkdown from "react-markdown"; + +import { useAuth } from "@/components/auth-provider"; +import { readApiError } from "@/lib/api"; +import type { + Document, + DocumentChapterTreeItem, +} from "@/types/document"; + +const { Content, Sider } = Layout; +const { Title, Paragraph } = Typography; + +type MenuItem = Required["items"][number]; + +export default function DocsViewPage() { + const { fetchWithAuth } = useAuth(); + const { token } = theme.useToken(); + const [selectedDocumentId, setSelectedDocumentId] = useState(null); + const [collapsed, setCollapsed] = useState(false); + + const { data: treeData, isLoading: treeLoading } = useQuery({ + queryKey: ["/api/v1/documents/chapters/tree"], + queryFn: async () => { + const response = await fetchWithAuth("/api/v1/documents/chapters/tree"); + if (!response.ok) throw new Error(await readApiError(response)); + return response.json() as Promise; + }, + }); + + const { data: selectedDocument, isLoading: documentLoading } = useQuery({ + queryKey: ["/api/v1/documents", selectedDocumentId], + queryFn: async () => { + if (!selectedDocumentId) return null; + const response = await fetchWithAuth(`/api/v1/documents/${selectedDocumentId}`); + if (!response.ok) throw new Error(await readApiError(response)); + return response.json() as Promise; + }, + enabled: !!selectedDocumentId, + }); + + const convertToMenuItems = (chapters: DocumentChapterTreeItem[]): MenuItem[] => { + return chapters + .filter((chapter) => { + // Only show chapters with published documents or published children + const hasPublishedDocs = chapter.documents?.some((doc) => doc.status === "published"); + const hasPublishedChildren = chapter.children?.some((child) => + child.documents?.some((doc) => doc.status === "published") + ); + return hasPublishedDocs || hasPublishedChildren; + }) + .map((chapter) => { + const hasChildren = chapter.children && chapter.children.length > 0; + const publishedDocs = chapter.documents?.filter((doc) => doc.status === "published") || []; + + const docItems: MenuItem[] = publishedDocs.map((doc) => ({ + key: `doc-${doc.id}`, + icon: , + label: doc.title, + })); + + const childItems = hasChildren ? convertToMenuItems(chapter.children) : []; + + return { + key: `chapter-${chapter.id}`, + icon: , + label: chapter.name, + children: [...docItems, ...childItems], + }; + }); + }; + + const handleMenuClick: MenuProps["onClick"] = (e) => { + if (e.key.startsWith("doc-")) { + const docId = parseInt(e.key.replace("doc-", ""), 10); + setSelectedDocumentId(docId); + } + }; + + // Auto-select first document on load + const selectFirstDocument = useCallback(() => { + if (!treeData || treeData.length === 0) return; + + const findFirstPublishedDoc = (chapters: DocumentChapterTreeItem[]): Document | null => { + for (const chapter of chapters) { + const publishedDoc = chapter.documents?.find((doc) => doc.status === "published"); + if (publishedDoc) return publishedDoc; + + if (chapter.children) { + const childDoc = findFirstPublishedDoc(chapter.children); + if (childDoc) return childDoc; + } + } + return null; + }; + + const firstDoc = findFirstPublishedDoc(treeData); + if (firstDoc && !selectedDocumentId) { + setSelectedDocumentId(firstDoc.id); + } + }, [treeData, selectedDocumentId]); + + useState(() => { + selectFirstDocument(); + }); + + return ( + + +
+ + {!collapsed && "操作文档"} + +
+ {treeLoading ? ( +
+ +
+ ) : treeData && treeData.length > 0 ? ( + + ) : ( +
+ +
+ )} + + + + {documentLoading ? ( +
+ +
+ ) : selectedDocument ? ( + + {selectedDocument.title} +
+ {children}, + h2: ({ children }) => {children}, + h3: ({ children }) => {children}, + h4: ({ children }) => {children}, + p: ({ children }) => {children}, + code: ({ children, className }) => { + const isBlock = className?.includes("language-"); + return isBlock ? ( +
+                          {children}
+                        
+ ) : ( + + {children} + + ); + }, + }} + > + {selectedDocument.content} +
+
+
+ ) : ( +
+ +
+ )} +
+
+ + ); +} diff --git a/web/src/app/admin/documents/page.tsx b/web/src/app/admin/documents/page.tsx new file mode 100644 index 0000000..244494d --- /dev/null +++ b/web/src/app/admin/documents/page.tsx @@ -0,0 +1,562 @@ +"use client"; + +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + Button, + Card, + Col, + Drawer, + Empty, + Form, + Input, + InputNumber, + Modal, + Popconfirm, + Row, + Select, + Space, + Spin, + Table, + Tag, + Tree, + Typography, + message, +} from "antd"; +import { + EditOutlined, + DeleteOutlined, + PlusOutlined, + FolderOutlined, + FileTextOutlined, +} from "@ant-design/icons"; +import type { ColumnsType } from "antd/es/table"; +import { useCallback, useEffect, useState } from "react"; +import type { DataNode } from "antd/es/tree"; + +import { useAuth } from "@/components/auth-provider"; +import { readApiError } from "@/lib/api"; +import type { + Document, + DocumentChapter, + DocumentChapterCreateRequest, + DocumentChapterTreeItem, + DocumentChapterUpdateRequest, + DocumentCreateRequest, + DocumentListResponse, + DocumentUpdateRequest, +} from "@/types/document"; + +const { TextArea } = Input; +const { Title } = Typography; + +type ChapterFormValues = { + name: string; + description?: string; + parent_id?: number; + sort_order: number; +}; + +type DocumentFormValues = { + title: string; + content: string; + chapter_id?: number; + sort_order: number; + status: "draft" | "published"; +}; + +export default function AdminDocumentsPage() { + const { fetchWithAuth } = useAuth(); + const queryClient = useQueryClient(); + + const [chapterDialogOpen, setChapterDialogOpen] = useState(false); + const [documentDrawerOpen, setDocumentDrawerOpen] = useState(false); + const [editingChapterId, setEditingChapterId] = useState(null); + const [editingDocumentId, setEditingDocumentId] = useState(null); + const [selectedChapterId, setSelectedChapterId] = useState(null); + const [expandedKeys, setExpandedKeys] = useState([]); + + const [chapterForm] = Form.useForm(); + const [documentForm] = Form.useForm(); + + const { data: treeData, isLoading: treeLoading } = useQuery({ + queryKey: ["/api/v1/documents/chapters/tree"], + queryFn: async () => { + const response = await fetchWithAuth("/api/v1/documents/chapters/tree"); + if (!response.ok) throw new Error(await readApiError(response)); + return response.json() as Promise; + }, + }); + + const { data: documentsData, isLoading: documentsLoading } = useQuery({ + queryKey: ["/api/v1/documents", selectedChapterId], + queryFn: async () => { + const params = new URLSearchParams(); + if (selectedChapterId !== null) { + params.set("chapter_id", String(selectedChapterId)); + } + params.set("limit", "200"); + const response = await fetchWithAuth(`/api/v1/documents?${params}`); + if (!response.ok) throw new Error(await readApiError(response)); + return response.json() as Promise; + }, + }); + + const createChapterMutation = useMutation({ + mutationFn: async (payload: DocumentChapterCreateRequest) => { + const response = await fetchWithAuth("/api/v1/documents/chapters", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + if (!response.ok) throw new Error(await readApiError(response)); + return response.json(); + }, + onSuccess: () => { + message.success("章节创建成功"); + queryClient.invalidateQueries({ queryKey: ["/api/v1/documents/chapters/tree"] }); + setChapterDialogOpen(false); + chapterForm.resetFields(); + }, + onError: (error: Error) => { + message.error(`创建失败: ${error.message}`); + }, + }); + + const updateChapterMutation = useMutation({ + mutationFn: async ({ + id, + payload, + }: { + id: number; + payload: DocumentChapterUpdateRequest; + }) => { + const response = await fetchWithAuth(`/api/v1/documents/chapters/${id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + if (!response.ok) throw new Error(await readApiError(response)); + return response.json(); + }, + onSuccess: () => { + message.success("章节更新成功"); + queryClient.invalidateQueries({ queryKey: ["/api/v1/documents/chapters/tree"] }); + setChapterDialogOpen(false); + setEditingChapterId(null); + chapterForm.resetFields(); + }, + onError: (error: Error) => { + message.error(`更新失败: ${error.message}`); + }, + }); + + const deleteChapterMutation = useMutation({ + mutationFn: async (id: number) => { + const response = await fetchWithAuth(`/api/v1/documents/chapters/${id}`, { + method: "DELETE", + }); + if (!response.ok) throw new Error(await readApiError(response)); + }, + onSuccess: () => { + message.success("章节删除成功"); + queryClient.invalidateQueries({ queryKey: ["/api/v1/documents/chapters/tree"] }); + }, + onError: (error: Error) => { + message.error(`删除失败: ${error.message}`); + }, + }); + + const createDocumentMutation = useMutation({ + mutationFn: async (payload: DocumentCreateRequest) => { + const response = await fetchWithAuth("/api/v1/documents", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + if (!response.ok) throw new Error(await readApiError(response)); + return response.json(); + }, + onSuccess: () => { + message.success("文档创建成功"); + queryClient.invalidateQueries({ queryKey: ["/api/v1/documents"] }); + queryClient.invalidateQueries({ queryKey: ["/api/v1/documents/chapters/tree"] }); + setDocumentDrawerOpen(false); + documentForm.resetFields(); + }, + onError: (error: Error) => { + message.error(`创建失败: ${error.message}`); + }, + }); + + const updateDocumentMutation = useMutation({ + mutationFn: async ({ + id, + payload, + }: { + id: number; + payload: DocumentUpdateRequest; + }) => { + const response = await fetchWithAuth(`/api/v1/documents/${id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + if (!response.ok) throw new Error(await readApiError(response)); + return response.json(); + }, + onSuccess: () => { + message.success("文档更新成功"); + queryClient.invalidateQueries({ queryKey: ["/api/v1/documents"] }); + queryClient.invalidateQueries({ queryKey: ["/api/v1/documents/chapters/tree"] }); + setDocumentDrawerOpen(false); + setEditingDocumentId(null); + documentForm.resetFields(); + }, + onError: (error: Error) => { + message.error(`更新失败: ${error.message}`); + }, + }); + + const deleteDocumentMutation = useMutation({ + mutationFn: async (id: number) => { + const response = await fetchWithAuth(`/api/v1/documents/${id}`, { + method: "DELETE", + }); + if (!response.ok) throw new Error(await readApiError(response)); + }, + onSuccess: () => { + message.success("文档删除成功"); + queryClient.invalidateQueries({ queryKey: ["/api/v1/documents"] }); + queryClient.invalidateQueries({ queryKey: ["/api/v1/documents/chapters/tree"] }); + }, + onError: (error: Error) => { + message.error(`删除失败: ${error.message}`); + }, + }); + + const handleCreateChapter = () => { + setEditingChapterId(null); + chapterForm.resetFields(); + setChapterDialogOpen(true); + }; + + const handleEditChapter = (chapter: DocumentChapter) => { + setEditingChapterId(chapter.id); + chapterForm.setFieldsValue({ + name: chapter.name, + description: chapter.description || "", + parent_id: chapter.parent_id || undefined, + sort_order: chapter.sort_order, + }); + setChapterDialogOpen(true); + }; + + const handleChapterFormSubmit = async () => { + try { + const values = await chapterForm.validateFields(); + if (editingChapterId) { + updateChapterMutation.mutate({ id: editingChapterId, payload: values }); + } else { + createChapterMutation.mutate(values); + } + } catch (error) { + console.error("Form validation failed:", error); + } + }; + + const handleCreateDocument = () => { + setEditingDocumentId(null); + documentForm.resetFields(); + if (selectedChapterId !== null) { + documentForm.setFieldValue("chapter_id", selectedChapterId); + } + setDocumentDrawerOpen(true); + }; + + const handleEditDocument = (document: Document) => { + setEditingDocumentId(document.id); + documentForm.setFieldsValue({ + title: document.title, + content: document.content, + chapter_id: document.chapter_id || undefined, + sort_order: document.sort_order, + status: document.status, + }); + setDocumentDrawerOpen(true); + }; + + const handleDocumentFormSubmit = async () => { + try { + const values = await documentForm.validateFields(); + if (editingDocumentId) { + updateDocumentMutation.mutate({ id: editingDocumentId, payload: values }); + } else { + createDocumentMutation.mutate(values); + } + } catch (error) { + console.error("Form validation failed:", error); + } + }; + + const convertToTreeData = (chapters: DocumentChapterTreeItem[]): DataNode[] => { + return chapters.map((chapter) => ({ + key: `chapter-${chapter.id}`, + title: ( + + + {chapter.name} + + ({chapter.documents?.length || 0}) + + + ), + children: chapter.children ? convertToTreeData(chapter.children) : [], + })); + }; + + const columns: ColumnsType = [ + { + title: "标题", + dataIndex: "title", + key: "title", + }, + { + title: "状态", + dataIndex: "status", + key: "status", + width: 100, + render: (status: string) => ( + + {status === "published" ? "已发布" : "草稿"} + + ), + }, + { + title: "排序", + dataIndex: "sort_order", + key: "sort_order", + width: 80, + }, + { + title: "操作", + key: "action", + width: 150, + render: (_, record) => ( + + + deleteDocumentMutation.mutate(record.id)} + > + + + + ), + }, + ]; + + const flattenChapters = (chapters: DocumentChapterTreeItem[]): DocumentChapter[] => { + const result: DocumentChapter[] = []; + const traverse = (items: DocumentChapterTreeItem[]) => { + items.forEach((item) => { + result.push(item); + if (item.children) { + traverse(item.children); + } + }); + }; + traverse(chapters); + return result; + }; + + return ( +
+ 文档管理 + + + } + onClick={handleCreateChapter} + > + 新建章节 + + } + style={{ height: "calc(100vh - 180px)", overflow: "auto" }} + > + {treeLoading ? ( + + ) : treeData && treeData.length > 0 ? ( + setExpandedKeys(keys)} + treeData={convertToTreeData(treeData)} + onSelect={(keys) => { + if (keys.length > 0) { + const key = keys[0] as string; + if (key.startsWith("chapter-")) { + const id = parseInt(key.replace("chapter-", ""), 10); + setSelectedChapterId(id); + } + } else { + setSelectedChapterId(null); + } + }} + /> + ) : ( + + )} + + + + } + onClick={handleCreateDocument} + > + 新建文档 + + } + > + + + + + + { + setChapterDialogOpen(false); + setEditingChapterId(null); + chapterForm.resetFields(); + }} + confirmLoading={ + createChapterMutation.isPending || updateChapterMutation.isPending + } + > +
+ + + + +