diff --git a/.env.template b/.env.template index 84dc46d1c..e9e9fb571 100644 --- a/.env.template +++ b/.env.template @@ -16,7 +16,7 @@ STRUCTURED_OUTPUT_FRAMEWORK="instructor" LLM_API_KEY="your_api_key" -LLM_MODEL="openai/gpt-5-mini" +LLM_MODEL="openai/gpt-4o-mini" LLM_PROVIDER="openai" LLM_ENDPOINT="" LLM_API_VERSION="" @@ -33,7 +33,7 @@ EMBEDDING_MAX_TOKENS=8191 # If using BAML structured output these env variables will be used BAML_LLM_PROVIDER=openai -BAML_LLM_MODEL="gpt-5-mini" +BAML_LLM_MODEL="gpt-4o-mini" BAML_LLM_ENDPOINT="" BAML_LLM_API_KEY="your_api_key" BAML_LLM_API_VERSION="" @@ -124,6 +124,10 @@ ALLOW_HTTP_REQUESTS=True # When set to False errors during data processing will be returned as info but not raised to allow handling of faulty documents RAISE_INCREMENTAL_LOADING_ERRORS=True +# When set to True, the Cognee backend will require authentication for requests to the API. +# If you're disabling this, make sure to also disable ENABLE_BACKEND_ACCESS_CONTROL. +REQUIRE_AUTHENTICATION=False + # Set this variable to True to enforce usage of backend access control for Cognee # Note: This is only currently supported by the following databases: # Relational: SQLite, Postgres @@ -133,6 +137,14 @@ RAISE_INCREMENTAL_LOADING_ERRORS=True # It enforces LanceDB and KuzuDB use and uses them to create databases per Cognee user + dataset ENABLE_BACKEND_ACCESS_CONTROL=False +################################################################################ +# ☁️ Cloud Sync Settings +################################################################################ + +# Cognee Cloud API settings for syncing data to/from cloud infrastructure +COGNEE_CLOUD_API_URL="http://localhost:8001" +COGNEE_CLOUD_AUTH_TOKEN="your-auth-token" + ################################################################################ # 🛠️ DEV Settings ################################################################################ diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..7705a51eb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,97 @@ +name: 🐛 Bug Report +description: Report a bug or unexpected behavior +title: "[Bug]: " +labels: ["bug", "needs-triage"] +assignees: [] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! Please provide a clear and detailed description. + + - type: textarea + id: description + attributes: + label: Bug Description + description: Please provide a clear and concise description of the bug. What happened vs what you expected? + placeholder: Describe the bug in detail... + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Steps to Reproduce + description: Please provide detailed steps to reproduce the issue + placeholder: | + 1. Go to... + 2. Click on... + 3. See error... + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: What did you expect to happen? + placeholder: Describe what you expected... + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual Behavior + description: What actually happened? + placeholder: Describe what actually happened... + validations: + required: true + + - type: textarea + id: environment + attributes: + label: Environment + description: Please provide your environment details + placeholder: | + - OS: [e.g. macOS 13.0, Ubuntu 20.04] + - Python version: [e.g. 3.9.0] + - Cognee version: [e.g. 0.1.0] + - LLM Provider: [e.g. OpenAI, Ollama] + - Database: [e.g. Neo4j, FalkorDB] + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Logs/Error Messages + description: Please include any relevant logs or error messages + placeholder: Paste logs here... + render: shell + validations: + required: false + + - type: textarea + id: additional + attributes: + label: Additional Context + description: Add any other context about the problem here + placeholder: Any additional information... + validations: + required: false + + - type: checkboxes + id: checklist + attributes: + label: Pre-submission Checklist + description: Please confirm the following before submitting + options: + - label: I have searched existing issues to ensure this bug hasn't been reported already + required: true + - label: I have provided a clear and detailed description of the bug + required: true + - label: I have included steps to reproduce the issue + required: true + - label: I have included my environment details + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..2d6a38e3e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: 💬 Discord Community + url: https://discord.gg/NQtRemgQVD + about: Join our Discord community for questions, discussions, and support + - name: 📖 Documentation + url: https://docs.cognee.ai + about: Check our documentation for guides and API references diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml new file mode 100644 index 000000000..417289f5b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -0,0 +1,73 @@ +name: 📚 Documentation Issue +description: Report an issue with documentation or suggest documentation improvements +title: "[Docs]: " +labels: ["documentation", "needs-triage"] +assignees: [] +body: + - type: markdown + attributes: + value: | + Thanks for helping improve our documentation! Please provide details about the documentation issue or improvement. + + - type: dropdown + id: doc-type + attributes: + label: Documentation Type + description: What type of documentation issue is this? + options: + - Missing documentation + - Incorrect documentation + - Unclear documentation + - Documentation improvement + - New documentation request + validations: + required: true + + - type: textarea + id: location + attributes: + label: Documentation Location + description: Where is the documentation issue located? (URL, file path, section, etc.) + placeholder: https://cognee.ai/docs/... or specific file/section + validations: + required: true + + - type: textarea + id: issue + attributes: + label: Issue Description + description: Please describe the documentation issue or improvement needed + placeholder: The documentation is unclear about... + validations: + required: true + + - type: textarea + id: suggestion + attributes: + label: Suggested Improvement + description: How would you improve this documentation? + placeholder: I suggest changing this to... + validations: + required: false + + - type: textarea + id: additional + attributes: + label: Additional Context + description: Add any other context about the documentation issue + placeholder: Additional context... + validations: + required: false + + - type: checkboxes + id: checklist + attributes: + label: Pre-submission Checklist + description: Please confirm the following before submitting + options: + - label: I have searched existing issues to ensure this documentation issue hasn't been reported already + required: true + - label: I have provided a clear description of the documentation issue + required: true + - label: I have specified the location of the documentation issue + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..cf743e6e4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,78 @@ +name: 🚀 Feature Request +description: Suggest a new feature or enhancement +title: "[Feature]: " +labels: ["enhancement", "needs-triage"] +assignees: [] +body: + - type: markdown + attributes: + value: | + Thanks for suggesting a new feature! Please provide a clear and detailed description of your idea. + + - type: textarea + id: problem + attributes: + label: Problem Statement + description: Is your feature request related to a problem? Please describe the problem you're trying to solve. + placeholder: I'm always frustrated when... + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed Solution + description: Describe the solution you'd like to see implemented + placeholder: I would like to see... + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: Describe any alternative solutions or features you've considered + placeholder: I have also considered... + validations: + required: false + + - type: textarea + id: use-case + attributes: + label: Use Case + description: Describe your specific use case and how this feature would help + placeholder: This feature would help me... + validations: + required: true + + - type: textarea + id: implementation + attributes: + label: Implementation Ideas + description: If you have ideas about how this could be implemented, please share them + placeholder: This could be implemented by... + validations: + required: false + + - type: textarea + id: additional + attributes: + label: Additional Context + description: Add any other context, screenshots, or examples about the feature request + placeholder: Additional context... + validations: + required: false + + - type: checkboxes + id: checklist + attributes: + label: Pre-submission Checklist + description: Please confirm the following before submitting + options: + - label: I have searched existing issues to ensure this feature hasn't been requested already + required: true + - label: I have provided a clear problem statement and proposed solution + required: true + - label: I have described my specific use case + required: true + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 06d4c05da..76ff5965c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,7 +1,50 @@ ## Description - + + +## Type of Change + +- [ ] Bug fix (non-breaking change that fixes an issue) +- [ ] New feature (non-breaking change that adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) +- [ ] Documentation update +- [ ] Code refactoring +- [ ] Performance improvement +- [ ] Other (please specify): + +## Changes Made + +- +- +- + +## Testing + + +## Screenshots/Videos (if applicable) + + +## Pre-submission Checklist + +- [ ] **I have tested my changes thoroughly before submitting this PR** +- [ ] **This PR contains minimal changes necessary to address the issue/feature** +- [ ] My code follows the project's coding standards and style guidelines +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] I have added necessary documentation (if applicable) +- [ ] All new and existing tests pass +- [ ] I have searched existing PRs to ensure this change hasn't been submitted already +- [ ] I have linked any relevant issues in the description +- [ ] My commits have clear and descriptive messages + +## Related Issues + + +## Additional Notes + ## DCO Affirmation I affirm that all code in every commit of this pull request conforms to the terms of the Topoteretes Developer Certificate of Origin. diff --git a/.github/workflows/temporal_graph_tests.yml b/.github/workflows/temporal_graph_tests.yml new file mode 100644 index 000000000..4156dffff --- /dev/null +++ b/.github/workflows/temporal_graph_tests.yml @@ -0,0 +1,224 @@ +name: Temporal Graph Tests + +permissions: + contents: read + +on: + workflow_call: + inputs: + databases: + required: false + type: string + default: "all" + description: "Which vector databases to test (comma-separated list or 'all')" + +jobs: + run_temporal_graph_kuzu_lance_sqlite: + name: Temporal Graph test Kuzu (lancedb + sqlite) + runs-on: ubuntu-22.04 + if: ${{ inputs.databases == 'all' || contains(inputs.databases, 'kuzu/lance/sqlite') }} + steps: + - name: Check out + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Cognee Setup + uses: ./.github/actions/cognee_setup + with: + python-version: ${{ inputs.python-version }} + + - name: Dependencies already installed + run: echo "Dependencies already installed in setup" + + - name: Run Temporal Graph with Kuzu (lancedb + sqlite) + env: + ENV: 'dev' + LLM_MODEL: ${{ secrets.LLM_MODEL }} + LLM_ENDPOINT: ${{ secrets.LLM_ENDPOINT }} + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + LLM_API_VERSION: ${{ secrets.LLM_API_VERSION }} + EMBEDDING_MODEL: ${{ secrets.EMBEDDING_MODEL }} + EMBEDDING_ENDPOINT: ${{ secrets.EMBEDDING_ENDPOINT }} + EMBEDDING_API_KEY: ${{ secrets.EMBEDDING_API_KEY }} + EMBEDDING_API_VERSION: ${{ secrets.EMBEDDING_API_VERSION }} + GRAPH_DATABASE_PROVIDER: 'kuzu' + VECTOR_DB_PROVIDER: 'lancedb' + DB_PROVIDER: 'sqlite' + run: uv run python ./cognee/tests/test_temporal_graph.py + + run_temporal_graph_neo4j_lance_sqlite: + name: Temporal Graph test Neo4j (lancedb + sqlite) + runs-on: ubuntu-22.04 + if: ${{ inputs.databases == 'all' || contains(inputs.databases, 'neo4j/lance/sqlite') }} + services: + neo4j: + image: neo4j:5.11 + env: + NEO4J_AUTH: neo4j/pleaseletmein + NEO4J_PLUGINS: '["apoc","graph-data-science"]' + ports: + - 7474:7474 + - 7687:7687 + options: >- + --health-cmd="cypher-shell -u neo4j -p pleaseletmein 'RETURN 1'" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + + steps: + - name: Check out + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Cognee Setup + uses: ./.github/actions/cognee_setup + with: + python-version: ${{ inputs.python-version }} + + - name: Dependencies already installed + run: echo "Dependencies already installed in setup" + + - name: Run Temporal Graph with Neo4j (lancedb + sqlite) + env: + ENV: 'dev' + LLM_MODEL: ${{ secrets.LLM_MODEL }} + LLM_ENDPOINT: ${{ secrets.LLM_ENDPOINT }} + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + LLM_API_VERSION: ${{ secrets.LLM_API_VERSION }} + EMBEDDING_MODEL: ${{ secrets.EMBEDDING_MODEL }} + EMBEDDING_ENDPOINT: ${{ secrets.EMBEDDING_ENDPOINT }} + EMBEDDING_API_KEY: ${{ secrets.EMBEDDING_API_KEY }} + EMBEDDING_API_VERSION: ${{ secrets.EMBEDDING_API_VERSION }} + GRAPH_DATABASE_PROVIDER: 'neo4j' + VECTOR_DB_PROVIDER: 'lancedb' + DB_PROVIDER: 'sqlite' + GRAPH_DATABASE_URL: bolt://localhost:7687 + GRAPH_DATABASE_USERNAME: neo4j + GRAPH_DATABASE_PASSWORD: pleaseletmein + run: uv run python ./cognee/tests/test_temporal_graph.py + + run_temporal_graph_kuzu_postgres_pgvector: + name: Temporal Graph test Kuzu (postgres + pgvector) + runs-on: ubuntu-22.04 + if: ${{ inputs.databases == 'all' || contains(inputs.databases, 'kuzu/pgvector/postgres') }} + services: + postgres: + image: pgvector/pgvector:pg17 + env: + POSTGRES_USER: cognee + POSTGRES_PASSWORD: cognee + POSTGRES_DB: cognee_db + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + steps: + - name: Check out + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Cognee Setup + uses: ./.github/actions/cognee_setup + with: + python-version: ${{ inputs.python-version }} + extra-dependencies: "postgres" + + - name: Dependencies already installed + run: echo "Dependencies already installed in setup" + + - name: Run Temporal Graph with Kuzu (postgres + pgvector) + env: + ENV: dev + LLM_MODEL: ${{ secrets.LLM_MODEL }} + LLM_ENDPOINT: ${{ secrets.LLM_ENDPOINT }} + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + LLM_API_VERSION: ${{ secrets.LLM_API_VERSION }} + EMBEDDING_MODEL: ${{ secrets.EMBEDDING_MODEL }} + EMBEDDING_ENDPOINT: ${{ secrets.EMBEDDING_ENDPOINT }} + EMBEDDING_API_KEY: ${{ secrets.EMBEDDING_API_KEY }} + EMBEDDING_API_VERSION: ${{ secrets.EMBEDDING_API_VERSION }} + GRAPH_DATABASE_PROVIDER: 'kuzu' + VECTOR_DB_PROVIDER: 'pgvector' + DB_PROVIDER: 'postgres' + DB_NAME: 'cognee_db' + DB_HOST: '127.0.0.1' + DB_PORT: 5432 + DB_USERNAME: cognee + DB_PASSWORD: cognee + run: uv run python ./cognee/tests/test_temporal_graph.py + + run_temporal_graph_neo4j_postgres_pgvector: + name: Temporal Graph test Neo4j (postgres + pgvector) + runs-on: ubuntu-22.04 + if: ${{ inputs.databases == 'all' || contains(inputs.databases, 'neo4j/pgvector/postgres') }} + services: + neo4j: + image: neo4j:5.11 + env: + NEO4J_AUTH: neo4j/pleaseletmein + NEO4J_PLUGINS: '["apoc","graph-data-science"]' + ports: + - 7474:7474 + - 7687:7687 + options: >- + --health-cmd="cypher-shell -u neo4j -p pleaseletmein 'RETURN 1'" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + postgres: + image: pgvector/pgvector:pg17 + env: + POSTGRES_USER: cognee + POSTGRES_PASSWORD: cognee + POSTGRES_DB: cognee_db + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries=5 + steps: + - name: Check out + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Cognee Setup + uses: ./.github/actions/cognee_setup + with: + python-version: ${{ inputs.python-version }} + extra-dependencies: "postgres" + + - name: Dependencies already installed + run: echo "Dependencies already installed in setup" + + - name: Run Temporal Graph with Neo4j (postgres + pgvector) + env: + ENV: dev + LLM_MODEL: ${{ secrets.LLM_MODEL }} + LLM_ENDPOINT: ${{ secrets.LLM_ENDPOINT }} + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + LLM_API_VERSION: ${{ secrets.LLM_API_VERSION }} + EMBEDDING_MODEL: ${{ secrets.EMBEDDING_MODEL }} + EMBEDDING_ENDPOINT: ${{ secrets.EMBEDDING_ENDPOINT }} + EMBEDDING_API_KEY: ${{ secrets.EMBEDDING_API_KEY }} + EMBEDDING_API_VERSION: ${{ secrets.EMBEDDING_API_VERSION }} + GRAPH_DATABASE_PROVIDER: 'neo4j' + VECTOR_DB_PROVIDER: 'pgvector' + DB_PROVIDER: 'postgres' + GRAPH_DATABASE_URL: bolt://localhost:7687 + GRAPH_DATABASE_USERNAME: neo4j + GRAPH_DATABASE_PASSWORD: pleaseletmein + DB_NAME: cognee_db + DB_HOST: 127.0.0.1 + DB_PORT: 5432 + DB_USERNAME: cognee + DB_PASSWORD: cognee + run: uv run python ./cognee/tests/test_temporal_graph.py diff --git a/.github/workflows/test_openrouter.yml b/.github/workflows/test_openrouter.yml new file mode 100644 index 000000000..9c2dcdebe --- /dev/null +++ b/.github/workflows/test_openrouter.yml @@ -0,0 +1,30 @@ +name: test | openrouter + +on: + workflow_call: + +jobs: + test-openrouter: + name: Run OpenRouter Test + runs-on: ubuntu-22.04 + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Cognee Setup + uses: ./.github/actions/cognee_setup + with: + python-version: '3.11.x' + + - name: Run OpenRouter Simple Example + env: + LLM_PROVIDER: "custom" + LLM_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + LLM_MODEL: "openrouter/x-ai/grok-code-fast-1" + LLM_ENDPOINT: "https://openrouter.ai/api/v1" + EMBEDDING_PROVIDER: "openai" + EMBEDDING_API_KEY: ${{ secrets.OPENAI_API_KEY }} + EMBEDDING_MODEL: "openai/text-embedding-3-large" + EMBEDDING_DIMENSIONS: "3072" + EMBEDDING_MAX_TOKENS: "8191" + run: uv run python ./examples/python/simple_example.py diff --git a/.github/workflows/test_suites.yml b/.github/workflows/test_suites.yml index f4e86d544..b6e84ac1e 100644 --- a/.github/workflows/test_suites.yml +++ b/.github/workflows/test_suites.yml @@ -50,6 +50,12 @@ jobs: uses: ./.github/workflows/graph_db_tests.yml secrets: inherit + temporal-graph-tests: + name: Temporal Graph Test + needs: [ basic-tests, e2e-tests, cli-tests, graph-db-tests ] + uses: ./.github/workflows/temporal_graph_tests.yml + secrets: inherit + search-db-tests: name: Search Test on Different DBs needs: [basic-tests, e2e-tests, cli-tests, graph-db-tests] @@ -115,6 +121,12 @@ jobs: uses: ./.github/workflows/test_gemini.yml secrets: inherit + openrouter-tests: + name: OpenRouter Tests + needs: [basic-tests, e2e-tests, cli-tests] + uses: ./.github/workflows/test_openrouter.yml + secrets: inherit + # Ollama tests moved to the end ollama-tests: name: Ollama Tests @@ -128,6 +140,7 @@ jobs: vector-db-tests, example-tests, gemini-tests, + openrouter-tests, mcp-test, relational-db-migration-tests, docker-compose-test, @@ -150,6 +163,7 @@ jobs: db-examples-tests, mcp-test, gemini-tests, + openrouter-tests, ollama-tests, relational-db-migration-tests, docker-compose-test, @@ -171,6 +185,7 @@ jobs: "${{ needs.db-examples-tests.result }}" == "success" && "${{ needs.relational-db-migration-tests.result }}" == "success" && "${{ needs.gemini-tests.result }}" == "success" && + "${{ needs.openrouter-tests.result }}" == "success" && "${{ needs.docker-compose-test.result }}" == "success" && "${{ needs.docker-ci-test.result }}" == "success" && "${{ needs.ollama-tests.result }}" == "success" ]]; then diff --git a/README.md b/README.md index e618d5bf9..058016c1b 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ -**🚀 We launched Cogwit beta (Fully-hosted AI Memory): Sign up [here](https://platform.cognee.ai/)! 🚀** +**🚀 We launched Cogwit beta (Fully-hosted AI Memory): Sign up [here](https://platform.cognee.ai/)! 🚀** Build dynamic memory for Agents and replace RAG using scalable, modular ECL (Extract, Cognify, Load) pipelines. @@ -174,9 +174,9 @@ Example output: You can also cognify your files and query using cognee UI. -Cognee UI 2 +Cognee UI 2 -Try cognee UI out locally [here](https://docs.cognee.ai/how-to-guides/cognee-ui). +Try cognee UI by runnning ``` cognee -ui ``` command on your terminal. ## Understand our architecture diff --git a/alembic/versions/211ab850ef3d_add_sync_operations_table.py b/alembic/versions/211ab850ef3d_add_sync_operations_table.py new file mode 100644 index 000000000..f22c7c6e2 --- /dev/null +++ b/alembic/versions/211ab850ef3d_add_sync_operations_table.py @@ -0,0 +1,98 @@ +"""Add sync_operations table + +Revision ID: 211ab850ef3d +Revises: 9e7a3cb85175 +Create Date: 2025-09-10 20:11:13.534829 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "211ab850ef3d" +down_revision: Union[str, None] = "9e7a3cb85175" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + # Check if table already exists (it might be created by Base.metadata.create_all() in initial migration) + connection = op.get_bind() + inspector = sa.inspect(connection) + + if "sync_operations" not in inspector.get_table_names(): + # Table doesn't exist, create it normally + op.create_table( + "sync_operations", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("run_id", sa.Text(), nullable=True), + sa.Column( + "status", + sa.Enum( + "STARTED", + "IN_PROGRESS", + "COMPLETED", + "FAILED", + "CANCELLED", + name="syncstatus", + create_type=False, + ), + nullable=True, + ), + sa.Column("progress_percentage", sa.Integer(), nullable=True), + sa.Column("dataset_ids", sa.JSON(), nullable=True), + sa.Column("dataset_names", sa.JSON(), nullable=True), + sa.Column("user_id", sa.UUID(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("started_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("total_records_to_sync", sa.Integer(), nullable=True), + sa.Column("total_records_to_download", sa.Integer(), nullable=True), + sa.Column("total_records_to_upload", sa.Integer(), nullable=True), + sa.Column("records_downloaded", sa.Integer(), nullable=True), + sa.Column("records_uploaded", sa.Integer(), nullable=True), + sa.Column("bytes_downloaded", sa.Integer(), nullable=True), + sa.Column("bytes_uploaded", sa.Integer(), nullable=True), + sa.Column("dataset_sync_hashes", sa.JSON(), nullable=True), + sa.Column("error_message", sa.Text(), nullable=True), + sa.Column("retry_count", sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_sync_operations_run_id"), "sync_operations", ["run_id"], unique=True + ) + op.create_index( + op.f("ix_sync_operations_user_id"), "sync_operations", ["user_id"], unique=False + ) + else: + # Table already exists, but we might need to add missing columns or indexes + # For now, just log that the table already exists + print("sync_operations table already exists, skipping creation") + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + # Only drop if table exists (might have been created by Base.metadata.create_all()) + connection = op.get_bind() + inspector = sa.inspect(connection) + + if "sync_operations" in inspector.get_table_names(): + op.drop_index(op.f("ix_sync_operations_user_id"), table_name="sync_operations") + op.drop_index(op.f("ix_sync_operations_run_id"), table_name="sync_operations") + op.drop_table("sync_operations") + + # Drop the enum type that was created (only if no other tables are using it) + sa.Enum(name="syncstatus").drop(op.get_bind(), checkfirst=True) + else: + print("sync_operations table doesn't exist, skipping downgrade") + + # ### end Alembic commands ### diff --git a/alembic/versions/45957f0a9849_add_notebook_table.py b/alembic/versions/45957f0a9849_add_notebook_table.py new file mode 100644 index 000000000..ded7ccd9c --- /dev/null +++ b/alembic/versions/45957f0a9849_add_notebook_table.py @@ -0,0 +1,46 @@ +"""Add notebook table + +Revision ID: 45957f0a9849 +Revises: 9e7a3cb85175 +Create Date: 2025-09-10 17:47:58.201319 + +""" + +from datetime import datetime, timezone +from uuid import uuid4 +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "45957f0a9849" +down_revision: Union[str, None] = "9e7a3cb85175" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + bind = op.get_bind() + inspector = sa.inspect(bind) + + if "notebooks" not in inspector.get_table_names(): + # Define table with all necessary columns including primary key + op.create_table( + "notebooks", + sa.Column("id", sa.UUID, primary_key=True, default=uuid4), # Critical for SQLite + sa.Column("owner_id", sa.UUID, index=True), + sa.Column("name", sa.String(), nullable=False), + sa.Column("cells", sa.JSON(), nullable=False), + sa.Column("deletable", sa.Boolean(), default=True), + sa.Column("created_at", sa.DateTime(), default=lambda: datetime.now(timezone.utc)), + ) + + +def downgrade() -> None: + bind = op.get_bind() + inspector = sa.inspect(bind) + + if "notebooks" in inspector.get_table_names(): + op.drop_table("notebooks") diff --git a/alembic/versions/8057ae7329c2_initial_migration.py b/alembic/versions/8057ae7329c2_initial_migration.py index 48e795327..aa0ecd4b8 100644 --- a/alembic/versions/8057ae7329c2_initial_migration.py +++ b/alembic/versions/8057ae7329c2_initial_migration.py @@ -19,6 +19,7 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: db_engine = get_relational_engine() + # we might want to delete this await_only(db_engine.create_database()) diff --git a/assets/cognee-new-ui.webp b/assets/cognee-new-ui.webp new file mode 100644 index 000000000..81e408de3 Binary files /dev/null and b/assets/cognee-new-ui.webp differ diff --git a/cognee-frontend/public/next.svg b/cognee-frontend/public/next.svg deleted file mode 100644 index 5174b28c5..000000000 --- a/cognee-frontend/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/cognee-frontend/public/vercel.svg b/cognee-frontend/public/vercel.svg deleted file mode 100644 index d2f842227..000000000 --- a/cognee-frontend/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/cognee-frontend/public/videos/background-video-blur.mp4 b/cognee-frontend/public/videos/background-video-blur.mp4 new file mode 100644 index 000000000..23d7ecdba Binary files /dev/null and b/cognee-frontend/public/videos/background-video-blur.mp4 differ diff --git a/cognee-frontend/src/app/(graph)/CogneeAddWidget.tsx b/cognee-frontend/src/app/(graph)/CogneeAddWidget.tsx index 434ceb0c6..3efdb47af 100644 --- a/cognee-frontend/src/app/(graph)/CogneeAddWidget.tsx +++ b/cognee-frontend/src/app/(graph)/CogneeAddWidget.tsx @@ -26,9 +26,10 @@ export interface NodesAndEdges { interface CogneeAddWidgetProps { onData: (data: NodesAndLinks) => void; + useCloud?: boolean; } -export default function CogneeAddWidget({ onData }: CogneeAddWidgetProps) { +export default function CogneeAddWidget({ onData, useCloud = false }: CogneeAddWidgetProps) { const { datasets, refreshDatasets, @@ -76,17 +77,18 @@ export default function CogneeAddWidget({ onData }: CogneeAddWidgetProps) { return addData(dataset, files) .then(() => { - const onUpdate = (data: NodesAndEdges) => { - onData({ - nodes: data.nodes, - links: data.edges, - }); - setProcessingFilesDone(); - }; + // const onUpdate = (data: NodesAndEdges) => { + // onData({ + // nodes: data.nodes, + // links: data.edges, + // }); + // setProcessingFilesDone(); + // }; - return cognifyDataset(dataset, onUpdate) + return cognifyDataset(dataset, useCloud) .then(() => { refreshDatasets(); + setProcessingFilesDone(); }); }); }; diff --git a/cognee-frontend/src/app/(graph)/GraphVisualization.tsx b/cognee-frontend/src/app/(graph)/GraphVisualization.tsx index 67d6458f8..4e2d1e642 100644 --- a/cognee-frontend/src/app/(graph)/GraphVisualization.tsx +++ b/cognee-frontend/src/app/(graph)/GraphVisualization.tsx @@ -1,5 +1,6 @@ "use client"; +import classNames from "classnames"; import { MutableRefObject, useEffect, useImperativeHandle, useRef, useState, useCallback } from "react"; import { forceCollide, forceManyBody } from "d3-force-3d"; import ForceGraph, { ForceGraphMethods, GraphData, LinkObject, NodeObject } from "react-force-graph-2d"; @@ -10,6 +11,7 @@ interface GraphVisuzaliationProps { ref: MutableRefObject; data?: GraphData; graphControls: MutableRefObject; + className?: string; } export interface GraphVisualizationAPI { @@ -17,7 +19,7 @@ export interface GraphVisualizationAPI { setGraphShape: (shape: string) => void; } -export default function GraphVisualization({ ref, data, graphControls }: GraphVisuzaliationProps) { +export default function GraphVisualization({ ref, data, graphControls, className }: GraphVisuzaliationProps) { const textSize = 6; const nodeSize = 15; // const addNodeDistanceFromSourceNode = 15; @@ -201,7 +203,7 @@ export default function GraphVisualization({ ref, data, graphControls }: GraphVi if (typeof window !== "undefined" && data && graphRef.current) { // add collision force graphRef.current.d3Force("collision", forceCollide(nodeSize * 1.5)); - graphRef.current.d3Force("charge", forceManyBody().strength(-1500).distanceMin(300).distanceMax(900)); + graphRef.current.d3Force("charge", forceManyBody().strength(-10).distanceMin(10).distanceMax(50)); } }, [data, graphRef]); @@ -213,7 +215,7 @@ export default function GraphVisualization({ ref, data, graphControls }: GraphVi })); return ( -
+
{(data && typeof window !== "undefined") ? ( + + +
+ +
+ + + back + +
+
+
Account
+
Manage your account's settings.
+
{account.name}
+
+
+
Plan
+
You are using open-source version. Subscribe to get access to hosted cognee with your data!
+ + Select a plan + +
+
+
+
+
+
+
+
+
+
+ ); +} diff --git a/cognee-frontend/src/app/account/page.tsx b/cognee-frontend/src/app/account/page.tsx new file mode 100644 index 000000000..f6323c313 --- /dev/null +++ b/cognee-frontend/src/app/account/page.tsx @@ -0,0 +1 @@ +export { default } from "./Account"; diff --git a/cognee-frontend/src/app/dashboard/AddDataToCognee.tsx b/cognee-frontend/src/app/dashboard/AddDataToCognee.tsx new file mode 100644 index 000000000..4f9a3179e --- /dev/null +++ b/cognee-frontend/src/app/dashboard/AddDataToCognee.tsx @@ -0,0 +1,116 @@ +import { FormEvent, useCallback, useState } from "react"; +import { CloseIcon, PlusIcon } from "@/ui/Icons"; +import { useModal } from "@/ui/elements/Modal"; +import { CTAButton, GhostButton, IconButton, Modal, NeutralButton, Select } from "@/ui/elements"; + +import addData from "@/modules/ingestion/addData"; +import { Dataset } from "@/modules/ingestion/useDatasets"; +import cognifyDataset from "@/modules/datasets/cognifyDataset"; + +interface AddDataToCogneeProps { + datasets: Dataset[]; + refreshDatasets: () => void; + useCloud?: boolean; +} + +export default function AddDataToCognee({ datasets, refreshDatasets, useCloud = false }: AddDataToCogneeProps) { + const [filesForUpload, setFilesForUpload] = useState(null); + + const prepareFiles = useCallback((event: FormEvent) => { + const formElements = event.currentTarget; + const files = formElements.files; + + setFilesForUpload(files); + }, []); + + const processDataWithCognee = useCallback((state: object, event?: FormEvent) => { + event!.preventDefault(); + + if (!filesForUpload) { + return; + } + + const formElements = event!.currentTarget; + const datasetId = formElements.datasetName.value; + + return addData( + datasetId ? { + id: datasetId, + } : { + name: "main_dataset", + }, + Array.from(filesForUpload), + useCloud + ) + .then(({ dataset_id, dataset_name }) => { + refreshDatasets(); + setFilesForUpload(null); + + return cognifyDataset({ + id: dataset_id, + name: dataset_name, + data: [], // not important, just to mimick Dataset + status: "", // not important, just to mimick Dataset + }, useCloud); + }); + }, [filesForUpload, refreshDatasets, useCloud]); + + const { + isModalOpen: isAddDataModalOpen, + openModal: openAddDataModal, + closeModal: closeAddDataModal, + isActionLoading: isProcessingDataWithCognee, + confirmAction: submitDataToCognee, + } = useModal(false, processDataWithCognee); + + return ( + <> + + + Add data to cognee + + + +
+
+ Add new data to a dataset? + +
+
Please select a dataset to add data in.
If you don't have any, don't worry, we will create one for you.
+
+
+ + + + + select files + + + {filesForUpload?.length && ( +
+
selected files:
+ {Array.from(filesForUpload || []).map((file) => ( +
+ {file.name} +
+ ))} +
+ )} +
+
+ closeAddDataModal()}>cancel + + {isProcessingDataWithCognee ? "processing..." : "add"} + +
+
+
+
+ + ); +} diff --git a/cognee-frontend/src/app/dashboard/CogneeInstancesAccordion.tsx b/cognee-frontend/src/app/dashboard/CogneeInstancesAccordion.tsx new file mode 100644 index 000000000..037c9e828 --- /dev/null +++ b/cognee-frontend/src/app/dashboard/CogneeInstancesAccordion.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { useBoolean } from "@/utils"; +import { Accordion } from "@/ui/elements"; + +interface CogneeInstancesAccordionProps { + children: React.ReactNode; +} + +export default function CogneeInstancesAccordion({ + children, +}: CogneeInstancesAccordionProps) { + const { + value: isInstancesPanelOpen, + setTrue: openInstancesPanel, + setFalse: closeInstancesPanel, + } = useBoolean(true); + + return ( + <> + Cognee Instances} + isOpen={isInstancesPanelOpen} + openAccordion={openInstancesPanel} + closeAccordion={closeInstancesPanel} + > + {children} + + + ); +} diff --git a/cognee-frontend/src/app/dashboard/Dashboard.tsx b/cognee-frontend/src/app/dashboard/Dashboard.tsx new file mode 100644 index 000000000..97576f3b4 --- /dev/null +++ b/cognee-frontend/src/app/dashboard/Dashboard.tsx @@ -0,0 +1,169 @@ +"use client"; + +import { useCallback, useEffect, useRef, useState } from "react"; + +import { Header } from "@/ui/Layout"; +import { SearchIcon } from "@/ui/Icons"; +import { Notebook } from "@/ui/elements"; +import { fetch, isCloudEnvironment } from "@/utils"; +import { Notebook as NotebookType } from "@/ui/elements/Notebook/types"; +import { useAuthenticatedUser } from "@/modules/auth"; +import { Dataset } from "@/modules/ingestion/useDatasets"; +import useNotebooks from "@/modules/notebooks/useNotebooks"; + +import AddDataToCognee from "./AddDataToCognee"; +import NotebooksAccordion from "./NotebooksAccordion"; +import CogneeInstancesAccordion from "./CogneeInstancesAccordion"; +import InstanceDatasetsAccordion from "./InstanceDatasetsAccordion"; + +interface DashboardProps { + user?: { + id: string; + name: string; + email: string; + picture: string; + }; + accessToken: string; +} + +export default function Dashboard({ accessToken }: DashboardProps) { + fetch.setAccessToken(accessToken); + const { user } = useAuthenticatedUser(); + + const { + notebooks, + refreshNotebooks, + runCell, + addNotebook, + updateNotebook, + saveNotebook, + removeNotebook, + } = useNotebooks(); + + useEffect(() => { + if (!notebooks.length) { + refreshNotebooks() + .then((notebooks) => { + if (notebooks[0]) { + setSelectedNotebookId(notebooks[0].id); + } + }); + } + }, [notebooks.length, refreshNotebooks]); + + const [selectedNotebookId, setSelectedNotebookId] = useState(null); + + const handleNotebookRemove = useCallback((notebookId: string) => { + setSelectedNotebookId((currentSelectedNotebookId) => ( + currentSelectedNotebookId === notebookId ? null : currentSelectedNotebookId + )); + return removeNotebook(notebookId); + }, [removeNotebook]); + + const saveNotebookTimeoutRef = useRef(null); + const saveNotebookThrottled = useCallback((notebook: NotebookType) => { + const throttleTime = 1000; + + if (saveNotebookTimeoutRef.current) { + clearTimeout(saveNotebookTimeoutRef.current); + saveNotebookTimeoutRef.current = null; + } + + saveNotebookTimeoutRef.current = setTimeout(() => { + saveNotebook(notebook); + }, throttleTime) as unknown as number; + }, [saveNotebook]); + + useEffect(() => { + return () => { + if (saveNotebookTimeoutRef.current) { + clearTimeout(saveNotebookTimeoutRef.current); + saveNotebookTimeoutRef.current = null; + } + }; + }, []); + + const handleNotebookUpdate = useCallback((notebook: NotebookType) => { + updateNotebook(notebook); + saveNotebookThrottled(notebook); + }, [saveNotebookThrottled, updateNotebook]); + + const selectedNotebook = notebooks.find((notebook) => notebook.id === selectedNotebookId); + + // ############################ + // Datasets logic + + const [datasets, setDatasets] = useState([]); + const refreshDatasetsRef = useRef(() => {}); + + const handleDatasetsChange = useCallback((payload: { datasets: Dataset[], refreshDatasets: () => void }) => { + const { + datasets, + refreshDatasets, + } = payload; + + refreshDatasetsRef.current = refreshDatasets; + setDatasets(datasets); + }, []); + + const isCloudEnv = isCloudEnvironment(); + + return ( +
+ + +
+ +
+
+
+ + +
+ + + + + +
+ + + +
+
+ +
+ {selectedNotebook && ( + + )} +
+
+
+ ); +} diff --git a/cognee-frontend/src/app/dashboard/DatasetsAccordion.tsx b/cognee-frontend/src/app/dashboard/DatasetsAccordion.tsx new file mode 100644 index 000000000..0c764ef92 --- /dev/null +++ b/cognee-frontend/src/app/dashboard/DatasetsAccordion.tsx @@ -0,0 +1,346 @@ +"use client"; + +import { ChangeEvent, useCallback, useEffect, useState } from "react"; +import { useBoolean } from "@/utils"; +import { Accordion, CTAButton, GhostButton, IconButton, Input, Modal, PopupMenu } from "@/ui/elements"; +import { AccordionProps } from "@/ui/elements/Accordion"; +import { CloseIcon, DatasetIcon, MinusIcon, PlusIcon } from "@/ui/Icons"; +import useDatasets, { Dataset } from "@/modules/ingestion/useDatasets"; +import addData from "@/modules/ingestion/addData"; +import cognifyDataset from "@/modules/datasets/cognifyDataset"; +import { DataFile } from "@/modules/ingestion/useData"; +import { LoadingIndicator } from "@/ui/App"; + +interface DatasetsChangePayload { + datasets: Dataset[] + refreshDatasets: () => void; +} + +export interface DatasetsAccordionProps extends Omit { + onDatasetsChange?: (payload: DatasetsChangePayload) => void; + useCloud?: boolean; +} + +export default function DatasetsAccordion({ + title, + tools, + switchCaretPosition = false, + className, + contentClassName, + onDatasetsChange, + useCloud = false, +}: DatasetsAccordionProps) { + const { + value: isDatasetsPanelOpen, + setTrue: openDatasetsPanel, + setFalse: closeDatasetsPanel, + } = useBoolean(true); + + const { + datasets, + refreshDatasets, + addDataset, + removeDataset, + getDatasetData, + removeDatasetData, + } = useDatasets(useCloud); + + useEffect(() => { + if (datasets.length === 0) { + refreshDatasets(); + } + }, [datasets.length, refreshDatasets]); + + const [openDatasets, openDataset] = useState>(new Set()); + + const toggleDataset = (id: string) => { + openDataset((prev) => { + const newState = new Set(prev); + + if (newState.has(id)) { + newState.delete(id) + } else { + getDatasetData(id) + .then(() => { + newState.add(id); + }); + } + + return newState; + }); + }; + + const refreshOpenDatasetsData = useCallback(() => { + return Promise.all( + openDatasets.values().map( + (datasetId) => getDatasetData(datasetId) + ) + ); + }, [getDatasetData, openDatasets]); + + const refreshDatasetsAndData = useCallback(() => { + refreshDatasets() + .then(refreshOpenDatasetsData); + }, [refreshDatasets, refreshOpenDatasetsData]); + + useEffect(() => { + onDatasetsChange?.({ + datasets, + refreshDatasets: refreshDatasetsAndData, + }); + }, [datasets, onDatasetsChange, refreshDatasets, refreshDatasetsAndData]); + + const { + value: isNewDatasetModalOpen, + setTrue: openNewDatasetModal, + setFalse: closeNewDatasetModal, + } = useBoolean(false); + + const handleDatasetAdd = () => { + openNewDatasetModal(); + }; + + const [newDatasetError, setNewDatasetError] = useState(""); + + const handleNewDatasetSubmit = (event: React.FormEvent) => { + event.preventDefault(); + setNewDatasetError(""); + + const formElements = event.currentTarget; + + const datasetName = formElements.datasetName.value; + + if (datasetName.trim().length === 0) { + setNewDatasetError("Dataset name cannot be empty."); + return; + } + + if (datasetName.includes(" ") || datasetName.includes(".")) { + setNewDatasetError("Dataset name cannot contain spaces or periods."); + return; + } + + addDataset(datasetName) + .then(() => { + closeNewDatasetModal(); + refreshDatasetsAndData(); + }); + }; + + const { + value: isRemoveDatasetModalOpen, + setTrue: openRemoveDatasetModal, + setFalse: closeRemoveDatasetModal, + } = useBoolean(false); + + const [datasetToRemove, setDatasetToRemove] = useState(null); + + const handleDatasetRemove = (dataset: Dataset) => { + setDatasetToRemove(dataset); + openRemoveDatasetModal(); + }; + + const handleDatasetRemoveCancel = () => { + setDatasetToRemove(null); + closeRemoveDatasetModal(); + }; + + const handleRemoveDatasetConfirm = (event: React.FormEvent) => { + event.preventDefault(); + + if (datasetToRemove) { + removeDataset(datasetToRemove.id) + .then(() => { + closeRemoveDatasetModal(); + setDatasetToRemove(null); + refreshDatasetsAndData(); + }); + } + }; + + const [datasetInProcessing, setProcessingDataset] = useState(null); + + const handleAddFiles = (dataset: Dataset, event: ChangeEvent) => { + event.stopPropagation(); + + if (datasetInProcessing) { + return; + } + + setProcessingDataset(dataset); + + if (!event.target.files) { + return; + } + + const files: File[] = Array.from(event.target.files); + + if (!files.length) { + return; + } + + return addData(dataset, files, useCloud) + .then(async () => { + await getDatasetData(dataset.id); + + return cognifyDataset(dataset, useCloud) + .finally(() => { + setProcessingDataset(null); + }); + }); + }; + + const [dataToRemove, setDataToRemove] = useState(null); + const { + value: isRemoveDataModalOpen, + setTrue: openRemoveDataModal, + setFalse: closeRemoveDataModal, + } = useBoolean(false); + + const handleDataRemove = (data: DataFile) => { + setDataToRemove(data); + + openRemoveDataModal(); + }; + const handleDataRemoveCancel = () => { + setDataToRemove(null); + closeRemoveDataModal(); + }; + const handleDataRemoveConfirm = (event: React.FormEvent) => { + event.preventDefault(); + + if (dataToRemove) { + removeDatasetData(dataToRemove.datasetId, dataToRemove.id) + .then(() => { + closeRemoveDataModal(); + setDataToRemove(null); + refreshDatasetsAndData(); + }); + } + } + + return ( + <> + Datasets} + isOpen={isDatasetsPanelOpen} + openAccordion={openDatasetsPanel} + closeAccordion={closeDatasetsPanel} + tools={( +
+ {tools} + +
+ )} + switchCaretPosition={switchCaretPosition} + className={className} + contentClassName={contentClassName} + > +
+ {datasets.length === 0 && ( +
+ No datasets here, add one by clicking + +
+ )} + {datasets.map((dataset) => { + return ( + + {datasetInProcessing?.id == dataset.id ? : } + {dataset.name} +
+ )} + isOpen={openDatasets.has(dataset.id)} + openAccordion={() => toggleDataset(dataset.id)} + closeAccordion={() => toggleDataset(dataset.id)} + tools={( + + +
+
+ + add data +
+
+
+
handleDatasetRemove(dataset)} className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer">delete
+
+
+
+ )} + className="first:pt-1.5" + switchCaretPosition={true} + > + <> + {dataset.data?.length === 0 && ( +
+ No data in a dataset, add by clicking "add data" in a dropdown menu +
+ )} + {dataset.data?.map((data) => ( +
+ {data.name} +
+ handleDataRemove(data)}> +
+
+ ))} + +
+ ); + })} +
+ + + +
+
+ Create a new dataset? + +
+
Please provide a name for the dataset being created.
+
+
+ + {newDatasetError && {newDatasetError}} +
+
+ closeNewDatasetModal()}>cancel + create +
+
+
+
+ + +
+
+ Delete {datasetToRemove?.name} dataset? + +
+
Are you sure you want to delete {datasetToRemove?.name}? This action cannot be undone.
+
+ cancel + delete +
+
+
+ + +
+
+ Delete {dataToRemove?.name} data? + +
+
Are you sure you want to delete {dataToRemove?.name}? This action cannot be undone.
+
+ cancel + delete +
+
+
+ + ); +} diff --git a/cognee-frontend/src/app/dashboard/InstanceDatasetsAccordion.tsx b/cognee-frontend/src/app/dashboard/InstanceDatasetsAccordion.tsx new file mode 100644 index 000000000..cfb0db2e8 --- /dev/null +++ b/cognee-frontend/src/app/dashboard/InstanceDatasetsAccordion.tsx @@ -0,0 +1,130 @@ +import classNames from "classnames"; +import { useCallback, useEffect } from "react"; + +import { fetch, isCloudEnvironment, useBoolean } from "@/utils"; +import { checkCloudConnection } from "@/modules/cloud"; +import { CaretIcon, CloseIcon, CloudIcon, LocalCogneeIcon } from "@/ui/Icons"; +import { CTAButton, GhostButton, IconButton, Input, Modal } from "@/ui/elements"; + +import DatasetsAccordion, { DatasetsAccordionProps } from "./DatasetsAccordion"; + +type InstanceDatasetsAccordionProps = Omit; + +export default function InstanceDatasetsAccordion({ onDatasetsChange }: InstanceDatasetsAccordionProps) { + const { + value: isLocalCogneeConnected, + setTrue: setLocalCogneeConnected, + } = useBoolean(false); + + const { + value: isCloudCogneeConnected, + setTrue: setCloudCogneeConnected, + } = useBoolean(isCloudEnvironment()); + + const checkConnectionToCloudCognee = useCallback((apiKey?: string) => { + if (apiKey) { + fetch.setApiKey(apiKey); + } + return checkCloudConnection() + .then(setCloudCogneeConnected) + }, [setCloudCogneeConnected]); + + useEffect(() => { + const checkConnectionToLocalCognee = () => { + fetch.checkHealth() + .then(setLocalCogneeConnected) + }; + + checkConnectionToLocalCognee(); + }, [checkConnectionToCloudCognee, setCloudCogneeConnected, setLocalCogneeConnected]); + + const { + value: isCloudConnectedModalOpen, + setTrue: openCloudConnectionModal, + setFalse: closeCloudConnectionModal, + } = useBoolean(false); + + const handleCloudConnectionConfirm = (event: React.FormEvent) => { + event.preventDefault(); + + const apiKeyValue = event.currentTarget.apiKey.value; + + checkConnectionToCloudCognee(apiKeyValue) + .then(() => { + closeCloudConnectionModal(); + }); + }; + + const isCloudEnv = isCloudEnvironment(); + + return ( +
+ +
+ + local cognee +
+
+ )} + tools={isLocalCogneeConnected ? Connected : Not connected} + switchCaretPosition={true} + className="pt-3 pb-1.5" + contentClassName="pl-4" + onDatasetsChange={!isCloudEnv ? onDatasetsChange : () => {}} + /> + + {isCloudCogneeConnected ? ( + +
+ + cloud cognee +
+ + )} + tools={Connected} + switchCaretPosition={true} + className="pt-3 pb-1.5" + contentClassName="pl-4" + onDatasetsChange={isCloudEnv ? onDatasetsChange : () => {}} + useCloud={true} + /> + ) : ( + + )} + + +
+
+ Connect to cloud? + +
+
Please provide your API key. You can find it on our platform.
+
+
+ +
+
+ closeCloudConnectionModal()}>cancel + connect +
+
+
+
+ + ); +} diff --git a/cognee-frontend/src/app/dashboard/NotebooksAccordion.tsx b/cognee-frontend/src/app/dashboard/NotebooksAccordion.tsx new file mode 100644 index 000000000..174efaa9e --- /dev/null +++ b/cognee-frontend/src/app/dashboard/NotebooksAccordion.tsx @@ -0,0 +1,150 @@ +"use client"; + +import { FormEvent, useCallback, useState } from "react"; +import { useBoolean } from "@/utils"; +import { Accordion, CTAButton, GhostButton, IconButton, Input, Modal } from "@/ui/elements"; +import { CloseIcon, MinusIcon, NotebookIcon, PlusIcon } from "@/ui/Icons"; +import { Notebook } from "@/ui/elements/Notebook/types"; +import { LoadingIndicator } from "@/ui/App"; +import { useModal } from "@/ui/elements/Modal"; + +interface NotebooksAccordionProps { + notebooks: Notebook[]; + addNotebook: (name: string) => Promise; + removeNotebook: (id: string) => Promise; + openNotebook: (id: string) => void; +} + +export default function NotebooksAccordion({ + notebooks, + addNotebook, + removeNotebook, + openNotebook, +}: NotebooksAccordionProps) { + const { + value: isNotebookPanelOpen, + setTrue: openNotebookPanel, + setFalse: closeNotebookPanel, + } = useBoolean(true); + + const { + value: isNotebookLoading, + setTrue: notebookLoading, + setFalse: notebookLoaded, + } = useBoolean(false); + + // Notebook removal modal + const [notebookToRemove, setNotebookToRemove] = useState(null); + + const handleNotebookRemove = (notebook: Notebook) => { + setNotebookToRemove(notebook); + openRemoveNotebookModal(); + }; + + const { + value: isRemoveNotebookModalOpen, + setTrue: openRemoveNotebookModal, + setFalse: closeRemoveNotebookModal, + } = useBoolean(false); + + const handleNotebookRemoveCancel = () => { + closeRemoveNotebookModal(); + setNotebookToRemove(null); + }; + + const handleNotebookRemoveConfirm = () => { + notebookLoading(); + removeNotebook(notebookToRemove!.id) + .finally(notebookLoaded) + .finally(closeRemoveNotebookModal); + setNotebookToRemove(null); + }; + + const handleNotebookAdd = useCallback((_: object, formEvent?: FormEvent) => { + if (!formEvent) { + return; + } + + formEvent.preventDefault(); + + const formElements = formEvent.currentTarget; + const notebookName = formElements.notebookName.value.trim(); + + return addNotebook(notebookName) + }, [addNotebook]); + + const { + isModalOpen: isNewNotebookModalOpen, + openModal: openNewNotebookModal, + closeModal: closeNewNotebookModal, + confirmAction: handleNewNotebookSubmit, + isActionLoading: isNewDatasetLoading, + } = useModal(false, handleNotebookAdd); + + return ( + <> + Notebooks} + isOpen={isNotebookPanelOpen} + openAccordion={openNotebookPanel} + closeAccordion={closeNotebookPanel} + tools={isNewDatasetLoading ? ( + + ) : ( + + )} + > + {notebooks.length === 0 && ( +
+ No notebooks here, add one by clicking + +
+ )} + {notebooks.map((notebook: Notebook) => ( +
+ +
+ {notebook.deletable && handleNotebookRemove(notebook)}>} +
+
+ ))} +
+ + +
+
+ Create a new notebook? + +
+
Please provide a name for the notebook being created.
+
+
+ + {/* {newDatasetError && {newDatasetError}} */} +
+
+ closeNewNotebookModal()}>cancel + create +
+
+
+
+ + +
+
+ Delete {notebookToRemove?.name} notebook? + +
+
Are you sure you want to delete {notebookToRemove?.name}? This action cannot be undone.
+
+ cancel + delete +
+
+
+ + ); +} diff --git a/cognee-frontend/src/app/dashboard/page.tsx b/cognee-frontend/src/app/dashboard/page.tsx new file mode 100644 index 000000000..7ff3abaf4 --- /dev/null +++ b/cognee-frontend/src/app/dashboard/page.tsx @@ -0,0 +1,11 @@ +"use server"; + +import Dashboard from "./Dashboard"; + +export default async function Page() { + const accessToken = ""; + + return ( + + ); +} diff --git a/cognee-frontend/src/app/page.tsx b/cognee-frontend/src/app/page.tsx index 523a90f6b..80db75b20 100644 --- a/cognee-frontend/src/app/page.tsx +++ b/cognee-frontend/src/app/page.tsx @@ -1,3 +1,3 @@ -export { default } from "./(graph)/GraphView"; +export { default } from "./dashboard/page"; -export const dynamic = "force-dynamic"; +// export const dynamic = "force-dynamic"; diff --git a/cognee-frontend/src/app/plan/Plan.tsx b/cognee-frontend/src/app/plan/Plan.tsx new file mode 100644 index 000000000..1fc3477ea --- /dev/null +++ b/cognee-frontend/src/app/plan/Plan.tsx @@ -0,0 +1,165 @@ +import Link from "next/link"; +import { BackIcon, CheckIcon } from "@/ui/Icons"; +import { CTAButton, NeutralButton } from "@/ui/elements"; +import Header from "@/ui/Layout/Header"; + +export default function Plan() { + return ( +
+ + +
+ +
+
+ + + back + +
+ +
+
+ Affordable and transparent pricing +
+ +
+
+
Basic
+
Free
+
+ +
+
On-prem Subscription
+
$2470 /per month
+
Save 20% yearly
+
+ +
+
Cloud Subscription
+
$25 /per month
+
(beta pricing)
+
+ +
+
Everything in the free plan, plus...
+
+
License to use Cognee open source
+
Cognee tasks and pipelines
+
Custom schema and ontology generation
+
Integrated evaluations
+
More than 28 data sources supported
+
+
+ +
+
Everything in the free plan, plus...
+
+
License to use Cognee open source and Cognee Platform
+
1 day SLA
+
On-prem deployment
+
Hands-on support
+
Architecture review
+
Roadmap prioritization
+
Knowledge transfer
+
+
+ +
+
Everything in the free plan, plus...
+
+
Fully hosted cloud platform
+
Multi-tenant architecture
+
Comprehensive API endpoints
+
Automated scaling and parallel processing
+
Ability to group memories per user and domain
+
Automatic updates and priority support
+
1 GB ingestion + 10,000 API calls
+
+
+ +
+ Try for free +
+ +
+ Talk to us +
+ +
+ Sign up for Cogwit Beta +
+
+ +
+
Feature Comparison
+
Basic
+
On-prem
+
Cloud
+
+
+
Data Sources
+
28+
+
28+
+
28+
+ +
Deployment
+
Self-hosted
+
On-premise
+
Cloud
+ +
API Calls
+
Limited
+
Unlimited
+
10,000
+ +
Support
+
Community
+
Hands-on
+
Priority
+ +
SLA
+
+
1 day
+
Standard
+
+ +
+
+
Can I change my plan anytime?
+
Yes, you can upgrade or downgrade your plan at any time. Changes take effect immediately.
+
+
+
What happens to my data if I downgrade?
+
Your data is preserved, but features may be limited based on your new plan constraints.
+
+
+
Do you offer educational discounts?
+
Yes, we offer special pricing for educational institutions and students. Contact us for details.
+
+
+
Is there a free trial for paid plans?
+
All new accounts start with a 14-day free trial of our Pro plan features.
+
+
+
+ +
+
+
Need a custom solution?
+ Contact us +
+
+
+
+ ); +} diff --git a/cognee-frontend/src/app/plan/page.tsx b/cognee-frontend/src/app/plan/page.tsx new file mode 100644 index 000000000..a1352fe8e --- /dev/null +++ b/cognee-frontend/src/app/plan/page.tsx @@ -0,0 +1 @@ +export { default } from "./Plan"; diff --git a/cognee-frontend/src/modules/auth/getUser.ts b/cognee-frontend/src/modules/auth/getUser.ts new file mode 100644 index 000000000..c77821bd0 --- /dev/null +++ b/cognee-frontend/src/modules/auth/getUser.ts @@ -0,0 +1,6 @@ +import fetch from "@/utils/fetch"; + +export default function getUser() { + return fetch("/v1/auth/me") + .then((response) => response.json()); +} diff --git a/cognee-frontend/src/modules/auth/index.ts b/cognee-frontend/src/modules/auth/index.ts new file mode 100644 index 000000000..32ea0d8ad --- /dev/null +++ b/cognee-frontend/src/modules/auth/index.ts @@ -0,0 +1,2 @@ +export { type User } from "./types"; +export { default as useAuthenticatedUser } from "./useAuthenticatedUser"; diff --git a/cognee-frontend/src/modules/auth/types.ts b/cognee-frontend/src/modules/auth/types.ts new file mode 100644 index 000000000..76799c36f --- /dev/null +++ b/cognee-frontend/src/modules/auth/types.ts @@ -0,0 +1,6 @@ +export interface User { + id: string; + name: string; + email: string; + picture: string; +} diff --git a/cognee-frontend/src/modules/auth/useAuthenticatedUser.ts b/cognee-frontend/src/modules/auth/useAuthenticatedUser.ts new file mode 100644 index 000000000..c1fdd9810 --- /dev/null +++ b/cognee-frontend/src/modules/auth/useAuthenticatedUser.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from "react"; +import { fetch } from "@/utils"; +import { User } from "./types"; + +export default function useAuthenticatedUser() { + const [user, setUser] = useState(); + + useEffect(() => { + if (!user) { + fetch("/v1/auth/me") + .then((response) => response.json()) + .then((data) => setUser(data)); + } + }, [user]); + + return { user }; +} diff --git a/cognee-frontend/src/modules/cloud/checkCloudConnection.ts b/cognee-frontend/src/modules/cloud/checkCloudConnection.ts new file mode 100644 index 000000000..a76ec09a2 --- /dev/null +++ b/cognee-frontend/src/modules/cloud/checkCloudConnection.ts @@ -0,0 +1,7 @@ +import { fetch } from "@/utils"; + +export default function checkCloudConnection() { + return fetch("/v1/checks/connection", { + method: "POST", + }); +} diff --git a/cognee-frontend/src/modules/cloud/index.ts b/cognee-frontend/src/modules/cloud/index.ts new file mode 100644 index 000000000..409f803c3 --- /dev/null +++ b/cognee-frontend/src/modules/cloud/index.ts @@ -0,0 +1,2 @@ +export { default as syncData } from "./syncData"; +export { default as checkCloudConnection } from "./checkCloudConnection"; diff --git a/cognee-frontend/src/modules/cloud/syncData.ts b/cognee-frontend/src/modules/cloud/syncData.ts new file mode 100644 index 000000000..dc4360a27 --- /dev/null +++ b/cognee-frontend/src/modules/cloud/syncData.ts @@ -0,0 +1,11 @@ +import { fetch } from "@/utils"; + +export default function syncData(datasetId?: string) { + return fetch("/v1/sync", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + ...(datasetId ? { body: JSON.stringify({ datasetId }) } : { body: "{}" }), + }); +} diff --git a/cognee-frontend/src/modules/datasets/cognifyDataset.ts b/cognee-frontend/src/modules/datasets/cognifyDataset.ts index 7842334af..f257548b9 100644 --- a/cognee-frontend/src/modules/datasets/cognifyDataset.ts +++ b/cognee-frontend/src/modules/datasets/cognifyDataset.ts @@ -1,13 +1,13 @@ import { fetch } from "@/utils"; -import getDatasetGraph from "./getDatasetGraph"; +// import getDatasetGraph from "./getDatasetGraph"; import { Dataset } from "../ingestion/useDatasets"; -interface GraphData { - nodes: { id: string; label: string; properties?: object }[]; - edges: { source: string; target: string; label: string }[]; -} +// interface GraphData { +// nodes: { id: string; label: string; properties?: object }[]; +// edges: { source: string; target: string; label: string }[]; +// } -export default async function cognifyDataset(dataset: Dataset, onUpdate: (data: GraphData) => void) { +export default async function cognifyDataset(dataset: Dataset, useCloud: boolean = false) { // const data = await ( return fetch("/v1/cognify", { method: "POST", @@ -18,17 +18,17 @@ export default async function cognifyDataset(dataset: Dataset, onUpdate: (data: datasetIds: [dataset.id], runInBackground: false, }), - }) - .then((response) => response.json()) - .then(() => { - return getDatasetGraph(dataset) - .then((data) => { - onUpdate({ - nodes: data.nodes, - edges: data.edges, - }); - }); - }); + }, useCloud) + .then((response) => response.json()); + // .then(() => { + // return getDatasetGraph(dataset) + // .then((data) => { + // onUpdate({ + // nodes: data.nodes, + // edges: data.edges, + // }); + // }); + // }); // ) // const websocket = new WebSocket(`ws://localhost:8000/api/v1/cognify/subscribe/${data.pipeline_run_id}`); diff --git a/cognee-frontend/src/modules/datasets/createDataset.ts b/cognee-frontend/src/modules/datasets/createDataset.ts index 661240036..f968ff875 100644 --- a/cognee-frontend/src/modules/datasets/createDataset.ts +++ b/cognee-frontend/src/modules/datasets/createDataset.ts @@ -1,12 +1,12 @@ import { fetch } from "@/utils"; -export default function createDataset(dataset: { name: string }) { +export default function createDataset(dataset: { name: string }, useCloud = false) { return fetch(`/v1/datasets/`, { method: "POST", body: JSON.stringify(dataset), headers: { "Content-Type": "application/json", - } - }) + }, + }, useCloud) .then((response) => response.json()); } diff --git a/cognee-frontend/src/modules/ingestion/addData.ts b/cognee-frontend/src/modules/ingestion/addData.ts index c4a1a84e1..89e7fcc8d 100644 --- a/cognee-frontend/src/modules/ingestion/addData.ts +++ b/cognee-frontend/src/modules/ingestion/addData.ts @@ -1,19 +1,35 @@ import { fetch } from "@/utils"; -export default function addData(dataset: { id?: string, name?: string }, files: File[]) { - const formData = new FormData(); - files.forEach((file) => { - formData.append("data", file, file.name); - }) - if (dataset.id) { - formData.append("datasetId", dataset.id); - } - if (dataset.name) { - formData.append("datasetName", dataset.name); - } +export default async function addData(dataset: { id?: string, name?: string }, files: File[], useCloud = false) { + if (useCloud) { + const data = { + text_data: await Promise.all(files.map(async (file) => file.text())), + datasetId: dataset.id, + datasetName: dataset.name, + }; - return fetch("/v1/add", { - method: "POST", - body: formData, - }).then((response) => response.json()); + return fetch("/v1/add", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }, true).then((response) => response.json()); + } else { + const formData = new FormData(); + files.forEach((file) => { + formData.append("data", file, file.name); + }) + if (dataset.id) { + formData.append("datasetId", dataset.id); + } + if (dataset.name) { + formData.append("datasetName", dataset.name); + } + + return fetch("/v1/add", { + method: "POST", + body: formData, + }).then((response) => response.json()); + } } diff --git a/cognee-frontend/src/modules/ingestion/useData.ts b/cognee-frontend/src/modules/ingestion/useData.ts index 4368e296e..941521135 100644 --- a/cognee-frontend/src/modules/ingestion/useData.ts +++ b/cognee-frontend/src/modules/ingestion/useData.ts @@ -5,6 +5,7 @@ export interface DataFile { id: string; name: string; file: File; + datasetId: string; } const useData = () => { @@ -16,6 +17,7 @@ const useData = () => { id: v4(), name: file.name, file, + datasetId: "", })) ); }, []); diff --git a/cognee-frontend/src/modules/ingestion/useDatasets.ts b/cognee-frontend/src/modules/ingestion/useDatasets.ts index 9ae4ddcb2..6a125b591 100644 --- a/cognee-frontend/src/modules/ingestion/useDatasets.ts +++ b/cognee-frontend/src/modules/ingestion/useDatasets.ts @@ -1,7 +1,8 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import { v4 } from 'uuid'; -import { DataFile } from './useData'; +import { useCallback, useState } from 'react'; + import { fetch } from '@/utils'; +import { DataFile } from './useData'; +import createDataset from "../datasets/createDataset"; export interface Dataset { id: string; @@ -10,91 +11,129 @@ export interface Dataset { status: string; } -function useDatasets() { +function useDatasets(useCloud = false) { const [datasets, setDatasets] = useState([]); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const statusTimeout = useRef(null); + // const statusTimeout = useRef(null); - const fetchDatasetStatuses = useCallback((datasets: Dataset[]) => { - fetch( - `/v1/datasets/status?dataset=${datasets.map(d => d.id).join('&dataset=')}`, - { - headers: { - "Content-Type": "application/json", - }, - }, - ) - .then((response) => response.json()) - .then((statuses) => setDatasets( - (datasets) => ( - datasets.map((dataset) => ({ - ...dataset, - status: statuses[dataset.id] - })) - ))); - }, []); + // const fetchDatasetStatuses = useCallback((datasets: Dataset[]) => { + // fetch( + // `/v1/datasets/status?dataset=${datasets.map(d => d.id).join('&dataset=')}`, + // { + // headers: { + // "Content-Type": "application/json", + // }, + // }, + // useCloud, + // ) + // .then((response) => response.json()) + // .then((statuses) => setDatasets( + // (datasets) => ( + // datasets.map((dataset) => ({ + // ...dataset, + // status: statuses[dataset.id] + // })) + // ))); + // }, [useCloud]); - const checkDatasetStatuses = useCallback((datasets: Dataset[]) => { - fetchDatasetStatuses(datasets); + // const checkDatasetStatuses = useCallback((datasets: Dataset[]) => { + // fetchDatasetStatuses(datasets); - if (statusTimeout.current !== null) { - clearTimeout(statusTimeout.current); - } + // if (statusTimeout.current !== null) { + // clearTimeout(statusTimeout.current); + // } - statusTimeout.current = setTimeout(() => { - checkDatasetStatuses(datasets); - }, 50000); - }, [fetchDatasetStatuses]); + // statusTimeout.current = setTimeout(() => { + // checkDatasetStatuses(datasets); + // }, 50000); + // }, [fetchDatasetStatuses]); - useEffect(() => { - return () => { - if (statusTimeout.current !== null) { - clearTimeout(statusTimeout.current); - statusTimeout.current = null; - } - }; - }, []); + // useEffect(() => { + // return () => { + // if (statusTimeout.current !== null) { + // clearTimeout(statusTimeout.current); + // statusTimeout.current = null; + // } + // }; + // }, []); const addDataset = useCallback((datasetName: string) => { - setDatasets((datasets) => [ - ...datasets, - { - id: v4(), - name: datasetName, - data: [], - status: 'DATASET_INITIALIZED', - } - ]); - }, []); + return createDataset({ name: datasetName }, useCloud) + .then((dataset) => { + setDatasets((datasets) => [ + ...datasets, + dataset, + ]); + }); + }, [useCloud]); const removeDataset = useCallback((datasetId: string) => { - setDatasets((datasets) => - datasets.filter((dataset) => dataset.id !== datasetId) - ); - }, []); + return fetch(`/v1/datasets/${datasetId}`, { + method: 'DELETE', + }, useCloud) + .then(() => { + setDatasets((datasets) => + datasets.filter((dataset) => dataset.id !== datasetId) + ); + }); + }, [useCloud]); const fetchDatasets = useCallback(() => { return fetch('/v1/datasets', { headers: { "Content-Type": "application/json", }, - }) + }, useCloud) .then((response) => response.json()) .then((datasets) => { setDatasets(datasets); - if (datasets.length > 0) { - checkDatasetStatuses(datasets); - } + // if (datasets.length > 0) { + // checkDatasetStatuses(datasets); + // } return datasets; }) .catch((error) => { console.error('Error fetching datasets:', error); }); - }, [checkDatasetStatuses]); + }, [useCloud]); - return { datasets, addDataset, removeDataset, refreshDatasets: fetchDatasets }; + const getDatasetData = useCallback((datasetId: string) => { + return fetch(`/v1/datasets/${datasetId}/data`, {}, useCloud) + .then((response) => response.json()) + .then((data) => { + const datasetIndex = datasets.findIndex((dataset) => dataset.id === datasetId); + + if (datasetIndex >= 0) { + setDatasets((datasets) => [ + ...datasets.slice(0, datasetIndex), + { + ...datasets[datasetIndex], + data, + }, + ...datasets.slice(datasetIndex + 1), + ]); + } + + return data; + }); + }, [datasets, useCloud]); + + const removeDatasetData = useCallback((datasetId: string, dataId: string) => { + return fetch(`/v1/datasets/${datasetId}/data/${dataId}`, { + method: 'DELETE', + }, useCloud); + }, [useCloud]); + + return { + datasets, + addDataset, + removeDataset, + getDatasetData, + removeDatasetData, + refreshDatasets: fetchDatasets, + }; }; export default useDatasets; diff --git a/cognee-frontend/src/modules/notebooks/useNotebooks.ts b/cognee-frontend/src/modules/notebooks/useNotebooks.ts new file mode 100644 index 000000000..e427f85ee --- /dev/null +++ b/cognee-frontend/src/modules/notebooks/useNotebooks.ts @@ -0,0 +1,135 @@ +import { useCallback, useState } from "react"; +import { fetch, isCloudEnvironment } from "@/utils"; +import { Cell, Notebook } from "@/ui/elements/Notebook/types"; + +function useNotebooks() { + const [notebooks, setNotebooks] = useState([]); + + const addNotebook = useCallback((notebookName: string) => { + return fetch("/v1/notebooks", { + body: JSON.stringify({ name: notebookName }), + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }, isCloudEnvironment()) + .then((response) => response.json()) + .then((notebook) => { + setNotebooks((notebooks) => [ + ...notebooks, + notebook, + ]); + + return notebook; + }); + }, []); + + const removeNotebook = useCallback((notebookId: string) => { + return fetch(`/v1/notebooks/${notebookId}`, { + method: "DELETE", + }, isCloudEnvironment()) + .then(() => { + setNotebooks((notebooks) => + notebooks.filter((notebook) => notebook.id !== notebookId) + ); + }); + }, []); + + const fetchNotebooks = useCallback(() => { + return fetch("/v1/notebooks", { + headers: { + "Content-Type": "application/json", + }, + }, isCloudEnvironment()) + .then((response) => response.json()) + .then((notebooks) => { + setNotebooks(notebooks); + + return notebooks; + }) + .catch((error) => { + console.error("Error fetching notebooks:", error); + throw error + }); + }, []); + + const updateNotebook = useCallback((updatedNotebook: Notebook) => { + setNotebooks((existingNotebooks) => + existingNotebooks.map((notebook) => + notebook.id === updatedNotebook.id + ? updatedNotebook + : notebook + ) + ); + }, []); + + const saveNotebook = useCallback((notebook: Notebook) => { + return fetch(`/v1/notebooks/${notebook.id}`, { + body: JSON.stringify({ + name: notebook.name, + cells: notebook.cells, + }), + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + }, isCloudEnvironment()) + .then((response) => response.json()) + }, []); + + const runCell = useCallback((notebook: Notebook, cell: Cell, cogneeInstance: string) => { + setNotebooks((existingNotebooks) => + existingNotebooks.map((existingNotebook) => + existingNotebook.id === notebook.id ? { + ...existingNotebook, + cells: existingNotebook.cells.map((existingCell) => + existingCell.id === cell.id ? { + ...existingCell, + result: undefined, + error: undefined, + } : existingCell + ), + } : notebook + ) + ); + + return fetch(`/v1/notebooks/${notebook.id}/${cell.id}/run`, { + body: JSON.stringify({ + content: cell.content, + }), + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }, cogneeInstance === "cloud") + .then((response) => response.json()) + .then((response) => { + setNotebooks((existingNotebooks) => + existingNotebooks.map((existingNotebook) => + existingNotebook.id === notebook.id ? { + ...existingNotebook, + cells: existingNotebook.cells.map((existingCell) => + existingCell.id === cell.id ? { + ...existingCell, + result: response.result, + error: response.error, + } : existingCell + ), + } : notebook + ) + ); + }); + }, []); + + return { + notebooks, + addNotebook, + saveNotebook, + updateNotebook, + removeNotebook, + refreshNotebooks: fetchNotebooks, + runCell, + }; +}; + +export default useNotebooks; diff --git a/cognee-frontend/src/ui/App/Loading/DefaultLoadingIndicator/LoadingIndicator.module.css b/cognee-frontend/src/ui/App/Loading/DefaultLoadingIndicator/LoadingIndicator.module.css index 472081d57..d66b1e7f8 100644 --- a/cognee-frontend/src/ui/App/Loading/DefaultLoadingIndicator/LoadingIndicator.module.css +++ b/cognee-frontend/src/ui/App/Loading/DefaultLoadingIndicator/LoadingIndicator.module.css @@ -3,7 +3,7 @@ width: 1rem; height: 1rem; border-radius: 50%; - border: 0.18rem solid white; + border: 0.18rem solid var(--color-indigo-600);; border-top-color: transparent; border-bottom-color: transparent; animation: spin 2s linear infinite; diff --git a/cognee-frontend/src/ui/Icons/AddIcon.tsx b/cognee-frontend/src/ui/Icons/AddIcon.tsx index b9092feec..da150c8a3 100644 --- a/cognee-frontend/src/ui/Icons/AddIcon.tsx +++ b/cognee-frontend/src/ui/Icons/AddIcon.tsx @@ -1,4 +1,4 @@ -export default function SearchIcon({ width = 24, height = 24, color = 'currentColor', className = '' }) { +export default function AddIcon({ width = 24, height = 24, color = 'currentColor', className = '' }) { return ( diff --git a/cognee-frontend/src/ui/Icons/BackIcon.tsx b/cognee-frontend/src/ui/Icons/BackIcon.tsx new file mode 100644 index 000000000..796fb923b --- /dev/null +++ b/cognee-frontend/src/ui/Icons/BackIcon.tsx @@ -0,0 +1,8 @@ +export default function BackIcon({ width = 16, height = 16, color = "#17191C", className = "" }) { + return ( + + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/CaretIcon.tsx b/cognee-frontend/src/ui/Icons/CaretIcon.tsx index 29a5eca62..cef9b9a9a 100644 --- a/cognee-frontend/src/ui/Icons/CaretIcon.tsx +++ b/cognee-frontend/src/ui/Icons/CaretIcon.tsx @@ -1,8 +1,7 @@ -export default function CaretIcon({ width = 50, height = 36, color = "currentColor", className = "" }) { +export default function CaretIcon({ width = 17, height = 16, color = "#000000", className = "" }) { return ( - - - + + ); } diff --git a/cognee-frontend/src/ui/Icons/CheckIcon.tsx b/cognee-frontend/src/ui/Icons/CheckIcon.tsx new file mode 100644 index 000000000..68610b1eb --- /dev/null +++ b/cognee-frontend/src/ui/Icons/CheckIcon.tsx @@ -0,0 +1,7 @@ +export default function CheckIcon({ width = 17, height = 18, color = "#5C10F4", className = "" }) { + return ( + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/CloseIcon.tsx b/cognee-frontend/src/ui/Icons/CloseIcon.tsx new file mode 100644 index 000000000..7ea30123d --- /dev/null +++ b/cognee-frontend/src/ui/Icons/CloseIcon.tsx @@ -0,0 +1,8 @@ +export default function CloseIcon({ width = 29, height = 29, color = "#000000", className = "" }) { + return ( + + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/CloudIcon.tsx b/cognee-frontend/src/ui/Icons/CloudIcon.tsx new file mode 100644 index 000000000..9578c24d4 --- /dev/null +++ b/cognee-frontend/src/ui/Icons/CloudIcon.tsx @@ -0,0 +1,7 @@ +export default function CloudIcon({ width = 16, height = 12, color = "#5C10F4", className = "" }) { + return ( + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/CogneeIcon.tsx b/cognee-frontend/src/ui/Icons/CogneeIcon.tsx new file mode 100644 index 000000000..d9f95e0f2 --- /dev/null +++ b/cognee-frontend/src/ui/Icons/CogneeIcon.tsx @@ -0,0 +1,7 @@ +export default function CogneeIcon({ width = 21, height = 24, color="#6510F4", className="" }) { + return ( + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/DatasetIcon.tsx b/cognee-frontend/src/ui/Icons/DatasetIcon.tsx new file mode 100644 index 000000000..d17ff0470 --- /dev/null +++ b/cognee-frontend/src/ui/Icons/DatasetIcon.tsx @@ -0,0 +1,9 @@ +export default function DatasetIcon({ width = 16, height = 16, color = "#000000", className = '' }) { + return ( + + + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/LocalCogneeIcon.tsx b/cognee-frontend/src/ui/Icons/LocalCogneeIcon.tsx new file mode 100644 index 000000000..37f6016fc --- /dev/null +++ b/cognee-frontend/src/ui/Icons/LocalCogneeIcon.tsx @@ -0,0 +1,10 @@ +export default function LocalCogneeIcon({ width = 16, height = 16, color = "#000000", className = "" }) { + return ( + + + + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/MenuIcon.tsx b/cognee-frontend/src/ui/Icons/MenuIcon.tsx new file mode 100644 index 000000000..666b3293d --- /dev/null +++ b/cognee-frontend/src/ui/Icons/MenuIcon.tsx @@ -0,0 +1,9 @@ +export default function AddIcon({ width = 16, height = 16, color = "#000000", className = "" }) { + return ( + + + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/MinusIcon.tsx b/cognee-frontend/src/ui/Icons/MinusIcon.tsx new file mode 100644 index 000000000..7757d81a6 --- /dev/null +++ b/cognee-frontend/src/ui/Icons/MinusIcon.tsx @@ -0,0 +1,7 @@ +export default function MinusIcon({ width = 16, height = 16, color = "#000000", className = "" }) { + return ( + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/NotebookIcon.tsx b/cognee-frontend/src/ui/Icons/NotebookIcon.tsx new file mode 100644 index 000000000..a46228d80 --- /dev/null +++ b/cognee-frontend/src/ui/Icons/NotebookIcon.tsx @@ -0,0 +1,8 @@ +export default function NotebookIcon({ width = 16, height = 16, color = "#000000", className = "" }) { + return ( + + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/PlayIcon.tsx b/cognee-frontend/src/ui/Icons/PlayIcon.tsx new file mode 100644 index 000000000..865f103b0 --- /dev/null +++ b/cognee-frontend/src/ui/Icons/PlayIcon.tsx @@ -0,0 +1,7 @@ +export default function PlayIcon({ width = 11, height = 14, color = "#000000", className = "" }) { + return ( + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/PlusIcon.tsx b/cognee-frontend/src/ui/Icons/PlusIcon.tsx new file mode 100644 index 000000000..69a760e20 --- /dev/null +++ b/cognee-frontend/src/ui/Icons/PlusIcon.tsx @@ -0,0 +1,8 @@ +export default function PlusIcon({ width = 16, height = 16, color = "#000000", className = "" }) { + return ( + + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/SearchIcon.tsx b/cognee-frontend/src/ui/Icons/SearchIcon.tsx index 3a3baac33..56cddf4c2 100644 --- a/cognee-frontend/src/ui/Icons/SearchIcon.tsx +++ b/cognee-frontend/src/ui/Icons/SearchIcon.tsx @@ -1,9 +1,8 @@ -export default function SearchIcon({ width = 24, height = 24, color = 'currentColor', className = '' }) { +export default function SearchIcon({ width = 12, height = 12, color = "#D8D8D8", className = "" }) { return ( - - - - + + + ); } diff --git a/cognee-frontend/src/ui/Icons/SettingsIcon.tsx b/cognee-frontend/src/ui/Icons/SettingsIcon.tsx index ce006f49b..87e4c9872 100644 --- a/cognee-frontend/src/ui/Icons/SettingsIcon.tsx +++ b/cognee-frontend/src/ui/Icons/SettingsIcon.tsx @@ -1,7 +1,8 @@ -export default function SettingsIcon({ width = 32, height = 33, color = "#E8EAED" }) { +export default function SettingsIcon({ width = 16, height = 17, color = "#000000" }) { return ( - - + + + ); } diff --git a/cognee-frontend/src/ui/Icons/index.ts b/cognee-frontend/src/ui/Icons/index.ts index 0adaa8fd1..3d3f8124f 100644 --- a/cognee-frontend/src/ui/Icons/index.ts +++ b/cognee-frontend/src/ui/Icons/index.ts @@ -1,7 +1,19 @@ -export { default as AddIcon } from './AddIcon'; -export { default as CaretIcon } from './CaretIcon'; -export { default as SearchIcon } from './SearchIcon'; -export { default as DeleteIcon } from './DeleteIcon'; -export { default as GithubIcon } from './GitHubIcon'; -export { default as DiscordIcon } from './DiscordIcon'; -export { default as SettingsIcon } from './SettingsIcon'; +export { default as AddIcon } from "./AddIcon"; +export { default as BackIcon } from "./BackIcon"; +export { default as PlayIcon } from "./PlayIcon"; +export { default as MenuIcon } from "./MenuIcon"; +export { default as PlusIcon } from "./PlusIcon"; +export { default as MinusIcon } from "./MinusIcon"; +export { default as CloseIcon } from "./CloseIcon"; +export { default as CheckIcon } from "./CheckIcon"; +export { default as CaretIcon } from "./CaretIcon"; +export { default as CloudIcon } from "./CloudIcon"; +export { default as SearchIcon } from "./SearchIcon"; +export { default as DeleteIcon } from "./DeleteIcon"; +export { default as GithubIcon } from "./GitHubIcon"; +export { default as CogneeIcon } from "./CogneeIcon"; +export { default as DiscordIcon } from "./DiscordIcon"; +export { default as DatasetIcon } from "./DatasetIcon"; +export { default as SettingsIcon } from "./SettingsIcon"; +export { default as NotebookIcon } from "./NotebookIcon"; +export { default as LocalCogneeIcon } from "./LocalCogneeIcon"; diff --git a/cognee-frontend/src/ui/Layout/Header.tsx b/cognee-frontend/src/ui/Layout/Header.tsx new file mode 100644 index 000000000..2f26433e9 --- /dev/null +++ b/cognee-frontend/src/ui/Layout/Header.tsx @@ -0,0 +1,79 @@ +"use client"; + +import Link from "next/link"; +import Image from "next/image"; +import { useBoolean } from "@/utils"; + +import { CloseIcon, CloudIcon, CogneeIcon } from "../Icons"; +import { CTAButton, GhostButton, IconButton, Modal } from "../elements"; +import syncData from "@/modules/cloud/syncData"; + +interface HeaderProps { + user?: { + name: string; + email: string; + picture: string; + }; +} + +export default function Header({ user }: HeaderProps) { + const { + value: isSyncModalOpen, + setTrue: openSyncModal, + setFalse: closeSyncModal, + } = useBoolean(false); + + const handleDataSyncConfirm = () => { + syncData() + .finally(() => { + closeSyncModal(); + }); + }; + + return ( + <> +
+
+ +
Cognee Local
+
+ +
+ + +
Sync
+
+ + Premium + + {/*
+ +
*/} + + {user?.picture ? ( + Name of the user + ) : ( +
+ {user?.email?.charAt(0) || "C"} +
+ )} + +
+ + + +
+
+ Sync local datasets with cloud datasets? + +
+
Are you sure you want to sync local datasets to cloud?
+
+ cancel + confirm +
+
+
+ + ); +} diff --git a/cognee-frontend/src/ui/Layout/index.ts b/cognee-frontend/src/ui/Layout/index.ts index 54938ca4d..af5a67ac5 100644 --- a/cognee-frontend/src/ui/Layout/index.ts +++ b/cognee-frontend/src/ui/Layout/index.ts @@ -1 +1,2 @@ -export { default as Divider } from './Divider/Divider'; +export { default as Divider } from "./Divider/Divider"; +export { default as Header } from "./Header"; diff --git a/cognee-frontend/src/ui/elements/Accordion.tsx b/cognee-frontend/src/ui/elements/Accordion.tsx new file mode 100644 index 000000000..8779d6d36 --- /dev/null +++ b/cognee-frontend/src/ui/elements/Accordion.tsx @@ -0,0 +1,45 @@ +import classNames from "classnames"; +import { CaretIcon } from "../Icons"; + +export interface AccordionProps { + isOpen: boolean; + title: React.ReactNode; + openAccordion: () => void; + closeAccordion: () => void; + tools?: React.ReactNode; + children: React.ReactNode; + className?: string; + contentClassName?: string; + switchCaretPosition?: boolean; +} + +export default function Accordion({ title, tools, children, isOpen, openAccordion, closeAccordion, className, contentClassName, switchCaretPosition = false }: AccordionProps) { + return ( +
+
+ + {tools} +
+ + {isOpen && ( +
+ {children} +
+ )} +
+ ); +} diff --git a/cognee-frontend/src/ui/elements/AvatarImage.tsx b/cognee-frontend/src/ui/elements/AvatarImage.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/cognee-frontend/src/ui/elements/CTAButton.tsx b/cognee-frontend/src/ui/elements/CTAButton.tsx index c38384cdd..02f1871fc 100644 --- a/cognee-frontend/src/ui/elements/CTAButton.tsx +++ b/cognee-frontend/src/ui/elements/CTAButton.tsx @@ -1,8 +1,8 @@ -import classNames from 'classnames'; +import classNames from "classnames"; import { ButtonHTMLAttributes } from "react"; export default function CTAButton({ children, className, ...props }: ButtonHTMLAttributes) { return ( - + ); } diff --git a/cognee-frontend/src/ui/elements/GhostButton.tsx b/cognee-frontend/src/ui/elements/GhostButton.tsx index 333dcc394..a27a0ff94 100644 --- a/cognee-frontend/src/ui/elements/GhostButton.tsx +++ b/cognee-frontend/src/ui/elements/GhostButton.tsx @@ -1,8 +1,8 @@ -import classNames from 'classnames'; +import classNames from "classnames"; import { ButtonHTMLAttributes } from "react"; export default function CTAButton({ children, className, ...props }: ButtonHTMLAttributes) { return ( - + ); } diff --git a/cognee-frontend/src/ui/elements/IconButton.tsx b/cognee-frontend/src/ui/elements/IconButton.tsx new file mode 100644 index 000000000..cbc35df5b --- /dev/null +++ b/cognee-frontend/src/ui/elements/IconButton.tsx @@ -0,0 +1,14 @@ +import classNames from "classnames"; +import { ButtonHTMLAttributes } from "react"; + +interface ButtonProps extends ButtonHTMLAttributes { + as?: React.ElementType; +} + +export default function IconButton({ as, children, className, ...props }: ButtonProps) { + const Element = as || "button"; + + return ( + {children} + ); +} diff --git a/cognee-frontend/src/ui/elements/Input.tsx b/cognee-frontend/src/ui/elements/Input.tsx index 904658eba..76451f9fa 100644 --- a/cognee-frontend/src/ui/elements/Input.tsx +++ b/cognee-frontend/src/ui/elements/Input.tsx @@ -3,6 +3,6 @@ import { InputHTMLAttributes } from "react" export default function Input({ className, ...props }: InputHTMLAttributes) { return ( - + ) } diff --git a/cognee-frontend/src/ui/elements/Modal.tsx b/cognee-frontend/src/ui/elements/Modal/Modal.tsx similarity index 84% rename from cognee-frontend/src/ui/elements/Modal.tsx rename to cognee-frontend/src/ui/elements/Modal/Modal.tsx index fd1db3c32..9d559a7ac 100644 --- a/cognee-frontend/src/ui/elements/Modal.tsx +++ b/cognee-frontend/src/ui/elements/Modal/Modal.tsx @@ -5,7 +5,7 @@ interface ModalProps { export default function Modal({ isOpen, children }: ModalProps) { return isOpen && ( -
+
{children}
); diff --git a/cognee-frontend/src/ui/elements/Modal/index.ts b/cognee-frontend/src/ui/elements/Modal/index.ts new file mode 100644 index 000000000..6386401d6 --- /dev/null +++ b/cognee-frontend/src/ui/elements/Modal/index.ts @@ -0,0 +1,3 @@ +export { default as Modal } from "./Modal"; +export { default as useModal } from "./useModal"; + diff --git a/cognee-frontend/src/ui/elements/Modal/useModal.ts b/cognee-frontend/src/ui/elements/Modal/useModal.ts new file mode 100644 index 000000000..4947d32ca --- /dev/null +++ b/cognee-frontend/src/ui/elements/Modal/useModal.ts @@ -0,0 +1,49 @@ +import { FormEvent, useCallback, useState } from "react"; +import { useBoolean } from "@/utils"; + +export default function useModal(initiallyOpen?: boolean, confirmCallback?: (state: object, event?: FormEvent) => Promise | ConfirmActionReturnType) { + const [modalState, setModalState] = useState({}); + const [isActionLoading, setLoading] = useState(false); + + const { + value: isModalOpen, + setTrue: openModalInternal, + setFalse: closeModalInternal, + } = useBoolean(initiallyOpen || false); + + const openModal = useCallback((state?: object) => { + if (state) { + setModalState(state); + } + openModalInternal(); + }, [openModalInternal]); + + const closeModal = useCallback(() => { + closeModalInternal(); + setModalState({}); + }, [closeModalInternal]); + + const confirmAction = useCallback((event?: FormEvent) => { + if (confirmCallback) { + setLoading(true); + + const maybePromise = confirmCallback(modalState, event); + + if (maybePromise instanceof Promise) { + return maybePromise + .finally(closeModal) + .finally(() => setLoading(false)); + } else { + return maybePromise; // Not a promise. + } + } + }, [closeModal, confirmCallback, modalState]); + + return { + isModalOpen, + openModal, + closeModal, + confirmAction, + isActionLoading, + }; +} diff --git a/cognee-frontend/src/ui/elements/NeutralButton.tsx b/cognee-frontend/src/ui/elements/NeutralButton.tsx index 5b274ad65..7b991fcb8 100644 --- a/cognee-frontend/src/ui/elements/NeutralButton.tsx +++ b/cognee-frontend/src/ui/elements/NeutralButton.tsx @@ -1,8 +1,8 @@ -import classNames from 'classnames'; +import classNames from "classnames"; import { ButtonHTMLAttributes } from "react"; -export default function CTAButton({ children, className, ...props }: ButtonHTMLAttributes) { +export default function NeutralButton({ children, className, ...props }: ButtonHTMLAttributes) { return ( - + ); } diff --git a/cognee-frontend/src/ui/elements/Notebook/Notebook.tsx b/cognee-frontend/src/ui/elements/Notebook/Notebook.tsx new file mode 100644 index 000000000..0789a8695 --- /dev/null +++ b/cognee-frontend/src/ui/elements/Notebook/Notebook.tsx @@ -0,0 +1,361 @@ +"use client"; + +import { v4 as uuid4 } from "uuid"; +import classNames from "classnames"; +import { Fragment, MutableRefObject, useCallback, useEffect, useRef, useState } from "react"; + +import { CaretIcon, PlusIcon } from "@/ui/Icons"; +import { IconButton, PopupMenu, TextArea } from "@/ui/elements"; +import { GraphControlsAPI } from "@/app/(graph)/GraphControls"; +import GraphVisualization, { GraphVisualizationAPI } from "@/app/(graph)/GraphVisualization"; + +import NotebookCellHeader from "./NotebookCellHeader"; +import { Cell, Notebook as NotebookType } from "./types"; + +interface NotebookProps { + notebook: NotebookType; + runCell: (notebook: NotebookType, cell: Cell, cogneeInstance: string) => Promise; + updateNotebook: (updatedNotebook: NotebookType) => void; + saveNotebook: (notebook: NotebookType) => void; +} + +export default function Notebook({ notebook, updateNotebook, saveNotebook, runCell }: NotebookProps) { + const saveCells = useCallback(() => { + saveNotebook(notebook); + }, [notebook, saveNotebook]); + + useEffect(() => { + window.addEventListener("beforeunload", saveCells); + + return () => { + window.removeEventListener("beforeunload", saveCells); + }; + }, [saveCells]); + + useEffect(() => { + if (notebook.cells.length === 0) { + const newCell: Cell = { + id: uuid4(), + name: "first cell", + type: "code", + content: "", + }; + updateNotebook({ + ...notebook, + cells: [newCell], + }); + } + }, [notebook, saveNotebook, updateNotebook]); + + const handleCellRun = useCallback((cell: Cell, cogneeInstance: string) => { + return runCell(notebook, cell, cogneeInstance); + }, [notebook, runCell]); + + const handleCellAdd = useCallback((afterCellIndex: number, cellType: "markdown" | "code") => { + const newCell: Cell = { + id: uuid4(), + name: "new cell", + type: cellType, + content: "", + }; + + const newNotebook = { + ...notebook, + cells: [ + ...notebook.cells.slice(0, afterCellIndex + 1), + newCell, + ...notebook.cells.slice(afterCellIndex + 1), + ], + }; + + toggleCellOpen(newCell.id); + updateNotebook(newNotebook); + }, [notebook, updateNotebook]); + + const handleCellRemove = useCallback((cell: Cell) => { + updateNotebook({ + ...notebook, + cells: notebook.cells.filter((c: Cell) => c.id !== cell.id), + }); + }, [notebook, updateNotebook]); + + const handleCellInputChange = useCallback((notebook: NotebookType, cell: Cell, value: string) => { + const newCell = {...cell, content: value }; + + updateNotebook({ + ...notebook, + cells: notebook.cells.map((cell: Cell) => (cell.id === newCell.id ? newCell : cell)), + }); + }, [updateNotebook]); + + const handleCellUp = useCallback((cell: Cell) => { + const index = notebook.cells.indexOf(cell); + + if (index > 0) { + const newCells = [...notebook.cells]; + newCells[index] = notebook.cells[index - 1]; + newCells[index - 1] = cell; + + updateNotebook({ + ...notebook, + cells: newCells, + }); + } + }, [notebook, updateNotebook]); + + const handleCellDown = useCallback((cell: Cell) => { + const index = notebook.cells.indexOf(cell); + + if (index < notebook.cells.length - 1) { + const newCells = [...notebook.cells]; + newCells[index] = notebook.cells[index + 1]; + newCells[index + 1] = cell; + + updateNotebook({ + ...notebook, + cells: newCells, + }); + } + }, [notebook, updateNotebook]); + + const handleCellRename = useCallback((cell: Cell) => { + const newName = prompt("Enter a new name for the cell:"); + + if (newName) { + updateNotebook({ + ...notebook, + cells: notebook.cells.map((c: Cell) => (c.id === cell.id ? {...c, name: newName } : c)), + }); + } + }, [notebook, updateNotebook]); + + const [openCells, setOpenCells] = useState(new Set(notebook.cells.map((c: Cell) => c.id))); + + const toggleCellOpen = (id: string) => { + setOpenCells((prev) => { + const newState = new Set(prev); + + if (newState.has(id)) { + newState.delete(id) + } else { + newState.add(id); + } + + return newState; + }); + }; + + return ( +
+
{notebook.name}
+ + {notebook.cells.map((cell: Cell, index) => ( + +
+
+ {cell.type === "code" ? ( + <> +
+ + + +
+ + + + {openCells.has(cell.id) && ( + <> +