Merge branch 'dev' into new_tests

This commit is contained in:
Vasilije 2025-06-25 09:26:36 +02:00 committed by GitHub
commit a25b8b8132
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
150 changed files with 9089 additions and 4633 deletions

View file

@ -1,56 +1,172 @@
ENV="local"
###
### DEV
###
TOKENIZERS_PARALLELISM="false"
# Default User Configuration
DEFAULT_USER_EMAIL=""
DEFAULT_USER_PASSWORD=""
###
### LLM
###
# LLM Configuration
LLM_API_KEY=""
LLM_MODEL="openai/gpt-4o-mini"
LLM_PROVIDER="openai"
LLM_ENDPOINT=""
LLM_API_VERSION=""
###
### simple, "expensive", an OpenAPI key
###
LLM_API_KEY="your_api_key"
###
### DEV LLM, cheap with content filters
###
LLM_MODEL="azure/gpt-4o-mini"
LLM_ENDPOINT="https://DNS.azure.com/openai/deployments/gpt-4o-mini"
LLM_API_KEY="<<TALK TO YOUR AZURE GUY"
LLM_API_VERSION="2024-12-01-preview"
#llm api version might not be relevant
LLM_MAX_TOKENS="16384"
GRAPHISTRY_USERNAME=
GRAPHISTRY_PASSWORD=
EMBEDDING_MODEL="azure/text-embedding-3-large"
EMBEDDING_ENDPOINT="https://DNS.openai.azure.com/openai/deployments/text-embedding-3-large"
EMBEDDING_API_KEY="<<TALK TO YOUR AZURE GUY>"
EMBEDDING_API_VERSION="2024-12-01-preview"
EMBEDDING_DIMENSIONS=3072
EMBEDDING_MAX_TOKENS=8191
SENTRY_REPORTING_URL=
###
### free local LLM, install it
###
LLM_API_KEY = "ollama"
LLM_MODEL = "llama3.1:8b"
LLM_PROVIDER = "ollama"
LLM_ENDPOINT = "http://localhost:11434/v1"
EMBEDDING_PROVIDER = "ollama"
EMBEDDING_MODEL = "avr/sfr-embedding-mistral:latest"
EMBEDDING_ENDPOINT = "http://localhost:11434/api/embeddings"
EMBEDDING_DIMENSIONS = 4096
HUGGINGFACE_TOKENIZER = "Salesforce/SFR-Embedding-Mistral"
###
### openrouter, also frewe
###
LLM_API_KEY="<<go-get-one-yourself"
LLM_PROVIDER="custom"
LLM_MODEL="openrouter/google/gemini-2.0-flash-lite-preview-02-05:free"
LLM_ENDPOINT="https://openrouter.ai/api/v1"
###
### deepinfra
###
LLM_API_KEY="<<>>"
LLM_PROVIDER="custom"
LLM_MODEL="deepinfra/meta-llama/Meta-Llama-3-8B-Instruct"
LLM_ENDPOINT="https://api.deepinfra.com/v1/openai"
# Embedding Configuration
EMBEDDING_PROVIDER="openai"
EMBEDDING_API_KEY=""
EMBEDDING_MODEL="openai/text-embedding-3-large"
EMBEDDING_API_KEY="<<>>"
EMBEDDING_MODEL="deepinfra/BAAI/bge-base-en-v1.5"
EMBEDDING_ENDPOINT=""
EMBEDDING_API_VERSION=""
EMBEDDING_DIMENSIONS=3072
EMBEDDING_MAX_TOKENS=8191
# "neo4j", "networkx", "kuzu" or "memgraph"
###
### DB
###
###
### db minimal/default
###
GRAPH_DATABASE_PROVIDER="networkx"
# Only needed if using neo4j or memgraph
GRAPH_DATABASE_URL=
GRAPH_DATABASE_USERNAME=
GRAPH_DATABASE_PASSWORD=
# "qdrant", "pgvector", "weaviate", "milvus", "lancedb" or "chromadb"
VECTOR_DB_PROVIDER="lancedb"
# Not needed if using "lancedb" or "pgvector"
VECTOR_DB_URL=
VECTOR_DB_KEY=
# Relational Database provider "sqlite" or "postgres"
DB_PROVIDER="sqlite"
# Database name
DB_PROVIDER=sqlite
DB_NAME=cognee_db
###
### Relational options
###
DB_PROVIDER="sqlite"
DB_NAME=cognee_db
DB_PROVIDER=postgres
DB_NAME=cognee_db
DB_HOST=127.0.0.1
DB_PORT=5432
DB_USERNAME=cognee
DB_PASSWORD=cognee
###
### Graph options
###
#Default
GRAPH_DATABASE_PROVIDER="kuzu"
#or if using remote
GRAPH_DATABASE_PROVIDER="kuzu"
GRAPH_DATABASE_PROVIDER="kuzu-remote"
GRAPH_DATABASE_URL="http://localhost:8000"
GRAPH_DATABASE_USERNAME=XXX
GRAPH_DATABASE_PASSWORD=YYY
# or if using neo4j
GRAPH_DATABASE_PROVIDER="neo4j"
GRAPH_DATABASE_URL=bolt://localhost:7687
GRAPH_DATABASE_USERNAME=neo4j
GRAPH_DATABASE_PASSWORD=localneo4j
###
### Vector options
###
VECTOR_DB_PROVIDER="lancedb"
VECTOR_DB_PROVIDER="pgvector"
###
### for release test
###
LLM_API_KEY="..."
OPENAI_API_KEY="..."
MIGRATION_DB_PATH="~/Downloads/"
MIGRATION_DB_NAME="Chinook_Sqlite.sqlite"
MIGRATION_DB_PROVIDER="sqlite"
GRAPH_DATABASE_URL="bolt://54.246.89.112:7687"
GRAPH_DATABASE_USERNAME="neo4j"
GRAPH_DATABASE_PASSWORD="pleaseletmein"
###
### ROOT DIRECTORY IF USING COGNEE LIB INSIDE A DOCKER
###
# Set up the Cognee system directory. Cognee will store system files and databases here.
DATA_ROOT_DIRECTORY ='/cognee_data/data'
SYSTEM_ROOT_DIRECTORY= '/cognee_data/system'
# Postgres specific parameters (Only if Postgres or PGVector is used). Do not use for cognee default simplest setup of SQLite-NetworkX-LanceDB
# DB_HOST=127.0.0.1
# DB_PORT=5432
# DB_USERNAME=cognee
# DB_PASSWORD=cognee
# To use Postgres with the Cognee backend in Docker compose use the following instead: DB_HOST=host.docker.internal
# DB_HOST=127.0.0.1
# DB_PORT=5432
# Params for migrating relational database data to graph / Cognee ( PostgreSQL and SQLite supported )
@ -77,3 +193,4 @@ LITELLM_LOG="ERROR"
#
# It enforces LanceDB and KuzuDB use and uses them to create databases per Cognee user + dataset
ENABLE_BACKEND_ACCESS_CONTROL=False

View file

@ -1,5 +1,8 @@
name: Reusable Graph DB Tests
permissions:
contents: read
on:
workflow_call:
inputs:

230
.github/workflows/search_db_tests.yml vendored Normal file
View file

@ -0,0 +1,230 @@
name: Reusable Search DB 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')"
secrets:
WEAVIATE_API_URL:
required: false
WEAVIATE_API_KEY:
required: false
jobs:
run-kuzu-lance-sqlite-search-tests:
name: Search test for 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: Install specific db dependency
run: |
poetry install -E kuzu
- name: Run Kuzu search Tests
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: poetry run python ./cognee/tests/test_search_db.py
run-neo4j-lance-sqlite-search-tests:
name: Search test for 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: Install specific db dependency
run: |
poetry install -E neo4j
- name: Run Neo4j search Tests
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: poetry run python ./cognee/tests/test_search_db.py
run-kuzu-pgvector-postgres-search-tests:
name: Search test for Kuzu/PGVector/Postgres
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 }}
- name: Install dependencies
run: poetry install -E kuzu -E postgres
- name: Run Kuzu/PGVector/Postgres Tests
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: poetry run python ./cognee/tests/test_search_db.py
run-neo4j-pgvector-postgres-search-tests:
name: Search test for Neo4j/PGVector/Postgres
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 }}
- name: Install dependencies
run: |
poetry install -E neo4j -E postgres
- name: Run Neo4j + PGVector + Postgres search Tests
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: poetry run python ./cognee/tests/test_search_db.py

View file

@ -35,7 +35,7 @@ jobs:
# Remove Cognee wheel that came from PyPI
uv pip uninstall cognee
# Install of the freshly-checked-out Cognee branch
uv pip install --no-deps --force-reinstall -e ../
uv pip install --force-reinstall -e ../
- name: Run MCP test
env:

View file

@ -45,6 +45,12 @@ jobs:
uses: ./.github/workflows/graph_db_tests.yml
secrets: inherit
search-db-tests:
name: Search Test on Different DBs
needs: [basic-tests, e2e-tests, graph-db-tests]
uses: ./.github/workflows/search_db_tests.yml
secrets: inherit
relational-db-migration-tests:
name: Relational DB Migration Tests
needs: [ basic-tests, e2e-tests, graph-db-tests]

View file

@ -25,6 +25,7 @@
[![Downloads](https://static.pepy.tech/badge/cognee)](https://pepy.tech/project/cognee)
[![License](https://img.shields.io/github/license/topoteretes/cognee?colorA=00C586&colorB=000000)](https://github.com/topoteretes/cognee/blob/main/LICENSE)
[![Contributors](https://img.shields.io/github/contributors/topoteretes/cognee?colorA=00C586&colorB=000000)](https://github.com/topoteretes/cognee/graphs/contributors)
<a href="https://github.com/sponsors/topoteretes"><img src="https://img.shields.io/badge/Sponsor-❤️-ff69b4.svg" alt="Sponsor"></a>
<a href="https://www.producthunt.com/posts/cognee?embed=true&utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-cognee" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=946346&theme=light&period=daily&t=1744472480704" alt="cognee - Memory&#0032;for&#0032;AI&#0032;Agents&#0032;&#0032;in&#0032;5&#0032;lines&#0032;of&#0032;code | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>

View file

@ -0,0 +1,222 @@
"""permission_system_rework
Revision ID: ab7e313804ae
Revises: 1d0bb7fede17
Create Date: 2025-06-16 15:20:43.118246
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy import UUID
from datetime import datetime, timezone
from uuid import uuid4
# revision identifiers, used by Alembic.
revision: str = "ab7e313804ae"
down_revision: Union[str, None] = "1d0bb7fede17"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def _now():
return datetime.now(timezone.utc)
def _define_dataset_table() -> sa.Table:
# Note: We can't use any Cognee model info to gather data (as it can change) in database so we must use our own table
# definition or load what is in the database
table = sa.Table(
"datasets",
sa.MetaData(),
sa.Column("id", UUID, primary_key=True, default=uuid4),
sa.Column("name", sa.Text),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
onupdate=lambda: datetime.now(timezone.utc),
),
sa.Column("owner_id", UUID, sa.ForeignKey("principals.id"), index=True),
)
return table
def _define_data_table() -> sa.Table:
# Note: We can't use any Cognee model info to gather data (as it can change) in database so we must use our own table
# definition or load what is in the database
table = sa.Table(
"data",
sa.MetaData(),
sa.Column("id", UUID, primary_key=True, default=uuid4),
sa.Column("name", sa.String),
sa.Column("extension", sa.String),
sa.Column("mime_type", sa.String),
sa.Column("raw_data_location", sa.String),
sa.Column("owner_id", UUID, index=True),
sa.Column("content_hash", sa.String),
sa.Column("external_metadata", sa.JSON),
sa.Column("node_set", sa.JSON, nullable=True), # list of strings
sa.Column("token_count", sa.Integer),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
onupdate=lambda: datetime.now(timezone.utc),
),
)
return table
def _ensure_permission(conn, permission_name) -> str:
"""
Return the permission.id for the given name, creating the row if needed.
"""
permissions_table = sa.Table(
"permissions",
sa.MetaData(),
sa.Column("id", UUID, primary_key=True, index=True, default=uuid4),
sa.Column(
"created_at", sa.DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
onupdate=lambda: datetime.now(timezone.utc),
),
sa.Column("name", sa.String, unique=True, nullable=False, index=True),
)
row = conn.execute(
sa.select(permissions_table).filter(permissions_table.c.name == permission_name)
).fetchone()
if row is None:
permission_id = uuid4()
op.bulk_insert(
permissions_table,
[
{
"id": permission_id,
"name": permission_name,
"created_at": _now(),
}
],
)
return permission_id
return row.id
def _build_acl_row(*, user_id, target_id, permission_id, target_col) -> dict:
"""Create a dict with the correct column names for the ACL row."""
return {
"id": uuid4(),
"created_at": _now(),
"principal_id": user_id,
target_col: target_id,
"permission_id": permission_id,
}
def _create_dataset_permission(conn, user_id, dataset_id, permission_name):
perm_id = _ensure_permission(conn, permission_name)
return _build_acl_row(
user_id=user_id, target_id=dataset_id, permission_id=perm_id, target_col="dataset_id"
)
def _create_data_permission(conn, user_id, data_id, permission_name):
perm_id = _ensure_permission(conn, permission_name)
return _build_acl_row(
user_id=user_id, target_id=data_id, permission_id=perm_id, target_col="data_id"
)
def upgrade() -> None:
conn = op.get_bind()
# Recreate ACLs table with default permissions set to datasets instead of documents
op.drop_table("acls")
acls_table = op.create_table(
"acls",
sa.Column("id", UUID, primary_key=True, default=uuid4),
sa.Column(
"created_at", sa.DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
),
sa.Column(
"updated_at", sa.DateTime(timezone=True), onupdate=lambda: datetime.now(timezone.utc)
),
sa.Column("principal_id", UUID, sa.ForeignKey("principals.id")),
sa.Column("permission_id", UUID, sa.ForeignKey("permissions.id")),
sa.Column("dataset_id", UUID, sa.ForeignKey("datasets.id", ondelete="CASCADE")),
)
# Note: We can't use any Cognee model info to gather data (as it can change) in database so we must use our own table
# definition or load what is in the database
dataset_table = _define_dataset_table()
datasets = conn.execute(sa.select(dataset_table)).fetchall()
if not datasets:
return
acl_list = []
for dataset in datasets:
acl_list.append(_create_dataset_permission(conn, dataset.owner_id, dataset.id, "read"))
acl_list.append(_create_dataset_permission(conn, dataset.owner_id, dataset.id, "write"))
acl_list.append(_create_dataset_permission(conn, dataset.owner_id, dataset.id, "share"))
acl_list.append(_create_dataset_permission(conn, dataset.owner_id, dataset.id, "delete"))
if acl_list:
op.bulk_insert(acls_table, acl_list)
def downgrade() -> None:
conn = op.get_bind()
op.drop_table("acls")
acls_table = op.create_table(
"acls",
sa.Column("id", UUID, primary_key=True, nullable=False, default=uuid4),
sa.Column(
"created_at", sa.DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
),
sa.Column(
"updated_at", sa.DateTime(timezone=True), onupdate=lambda: datetime.now(timezone.utc)
),
sa.Column("principal_id", UUID, sa.ForeignKey("principals.id")),
sa.Column("permission_id", UUID, sa.ForeignKey("permissions.id")),
sa.Column("data_id", UUID, sa.ForeignKey("data.id", ondelete="CASCADE")),
)
# Note: We can't use any Cognee model info to gather data (as it can change) in database so we must use our own table
# definition or load what is in the database
data_table = _define_data_table()
data = conn.execute(sa.select(data_table)).fetchall()
if not data:
return
acl_list = []
for single_data in data:
acl_list.append(_create_data_permission(conn, single_data.owner_id, single_data.id, "read"))
acl_list.append(
_create_data_permission(conn, single_data.owner_id, single_data.id, "write")
)
if acl_list:
op.bulk_insert(acls_table, acl_list)

View file

@ -0,0 +1,3 @@
node_modules
dist
coverage

View file

@ -1,5 +1,5 @@
# Use an official Node.js runtime as a parent image
FROM node:18-alpine
FROM node:22-alpine
# Set the working directory to /app
WORKDIR /app
@ -9,12 +9,14 @@ COPY package.json package-lock.json ./
# Install any needed packages specified in package.json
RUN npm ci
# RUN npm rebuild lightningcss
# Copy the rest of the application code to the working directory
COPY src ./src
COPY public ./public
COPY next.config.mjs .
COPY postcss.config.mjs .
COPY tsconfig.json .
# Build the app and run it
CMD npm run dev
CMD ["npm", "run", "dev"]

View file

@ -0,0 +1,16 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript", "prettier"),
];
export default eslintConfig;

File diff suppressed because it is too large Load diff

View file

@ -9,24 +9,28 @@
"lint": "next lint"
},
"dependencies": {
"@auth0/nextjs-auth0": "^4.6.0",
"classnames": "^2.5.1",
"culori": "^4.0.1",
"d3-force-3d": "^3.0.6",
"next": "15.3.2",
"ohmy-ui": "^0.0.6",
"react": "^18",
"react-dom": "^18",
"next": "15.3.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-force-graph-2d": "^1.27.1",
"tailwindcss": "^4.1.7",
"uuid": "^9.0.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4.1.7",
"@types/culori": "^4.0.0",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/uuid": "^9.0.8",
"eslint": "^8",
"eslint-config-next": "14.2.3",
"eslint": "^9",
"eslint-config-next": "^15.3.3",
"eslint-config-prettier": "^10.1.5",
"tailwindcss": "^4.1.7",
"typescript": "^5"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 694.2 120" style="enable-background:new 0 0 694.2 120;" xml:space="preserve">
<style type="text/css">
.st0{fill:#012447;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#3793EF;}
.st2{fill-rule:evenodd;clip-rule:evenodd;fill:#012447;}
.st3{fill-rule:evenodd;clip-rule:evenodd;fill:#1560AB;}
</style>
<g>
<path class="st0" d="M160.2,19.96h32.4c27.48,0,40.1,13.96,40.1,39.23s-15.14,40.56-40.1,40.56h-32.4V19.96z M175.33,32.59v54.65
h16.07c18.72,0,26.15-10.11,26.15-28.06c0-18.75-7.83-26.6-26.95-26.6H175.33z"/>
<path class="st0" d="M240.25,69.29c0-21.14,11.95-33.64,31.47-33.64c18.85,0,28.01,11.44,28.01,28.59c0,2.39,0,5.19-0.26,8.38
h-45.01c1.06,11.3,6.77,17.02,17.39,17.02c9.96,0,13.41-4.79,15.13-10.77l12.22,3.46c-2.92,11.3-10.75,19.02-27.48,19.02
C252.6,101.34,240.25,90.17,240.25,69.29z M254.59,63.04h32c-0.53-10.77-5.31-16.36-15.27-16.36
C261.63,46.69,255.92,51.87,254.59,63.04z"/>
<path class="st0" d="M307.7,69.29c0-21.14,11.95-33.64,31.47-33.64c18.85,0,28.01,11.44,28.01,28.59c0,2.39,0,5.19-0.26,8.38
h-45.01c1.06,11.3,6.77,17.02,17.39,17.02c9.96,0,13.41-4.79,15.13-10.77l12.22,3.46c-2.92,11.3-10.75,19.02-27.48,19.02
C320.05,101.34,307.7,90.17,307.7,69.29z M322.04,63.04h32c-0.53-10.77-5.31-16.36-15.27-16.36
C329.08,46.69,323.37,51.87,322.04,63.04z"/>
<path class="st0" d="M415.64,101.47c-11.42,0-18.72-7.45-21.77-18.35h-0.93v36.83h-14.21V37.25h14.21v16.22h0.93
c3.32-10.77,10.22-17.82,21.51-17.82c14.74,0,22.97,11.7,22.97,32.98S429.72,101.47,415.64,101.47z M424.27,68.63
c0-13.56-4.78-20.08-15.27-20.08c-9.82,0-16.06,6.92-16.06,17.82v4.79c0,10.51,6.37,17.69,15.93,17.69
C419.36,88.84,424.27,82.06,424.27,68.63z"/>
<path class="st0" d="M463.84,99.74h-14.21v-62.5h13.94v16.49h1.2c3.45-11.57,11.42-18.09,21.91-18.09
c13.01,0,19.25,8.78,19.25,22.34v41.76h-14.21V61.71c0-7.58-3.32-13.17-11.95-13.17c-9.56,0-15.93,6.38-15.93,15.82V99.74z"/>
<path class="st0" d="M516.55,68.36c0-20.61,12.35-32.71,31.73-32.71c19.12,0,31.47,12.1,31.47,32.71
c0,20.48-11.68,33.11-31.47,33.11C528.23,101.47,516.55,88.84,516.55,68.36z M530.89,68.23c0,12.9,5.31,20.88,17.26,20.88
c11.82,0,17.39-7.98,17.39-20.88s-5.84-20.48-17.39-20.48S530.89,55.33,530.89,68.23z"/>
<path class="st0" d="M594.62,48.81h-8.76V37.25h8.76V20.36h14.34v16.89h18.32v11.57h-18.32v29.92c0,6.12,2.26,9.31,8.5,9.31
c3.19,0,5.97-0.66,9.03-1.6l1.73,12.77c-4.65,1.46-7.83,2.26-14.07,2.26c-13.67,0-19.52-9.04-19.52-20.74V48.81z"/>
<path class="st0" d="M634.72,69.29c0-21.14,11.95-33.64,31.47-33.64c18.85,0,28.01,11.44,28.01,28.59c0,2.39,0,5.19-0.26,8.38
h-45.01c1.06,11.3,6.77,17.02,17.39,17.02c9.96,0,13.41-4.79,15.13-10.77l12.22,3.46c-2.92,11.3-10.75,19.02-27.48,19.02
C647.06,101.34,634.72,90.17,634.72,69.29z M649.06,63.04h32c-0.53-10.77-5.31-16.36-15.27-16.36
C656.09,46.69,650.38,51.87,649.06,63.04z"/>
</g>
<g>
<path class="st1" d="M42.35,0c-2.18,0.01-3.14,0-3.14,0C23.9,22.23,15.31,34.69,0,56.92c0,0,19.14,0,36.08,0
c3.07,0,6.44,0.11,9.91,0.45L35.29,72.31c0,0,0,0,0,0l0,0L0,120h50.2l0,0l0,0c0,0,0.03-0.01,0.05-0.01c-0.02,0-0.05,0.01-0.05,0.01
s69.8-2.14,69.8-63.4C120,2.93,57.73-0.06,42.35,0z"/>
<polygon class="st2" points="50.2,120 50.2,120 50.2,120 "/>
<path class="st3" d="M50.2,120c0,0,29.15-5.48,29.15-32.64c0-22.25-17.66-28.47-33.36-29.99L35.29,72.31c0,0,0,0,0,0l0,0L0,120
H50.2L50.2,120 M65.43,105.99c0.03-0.05,0.06-0.09,0.09-0.13C65.49,105.9,65.46,105.95,65.43,105.99z"/>
<g>
<path class="st2" d="M35.29,72.31c22.15,0,36.64,10.38,33.71,24.93C66.06,111.79,50.2,120,50.2,120c0,0,29.15-5.48,29.15-32.64
c0-22.25-17.66-28.47-33.36-29.99L35.29,72.31z"/>
<polygon class="st2" points="50.2,120 50.2,120 50.2,120 "/>
</g>
<path class="st1" d="M0,56.92C15.31,34.69,23.9,22.23,39.22,0c0,0,0.96,0.01,3.14,0C57.73-0.06,120,2.93,120,56.6
c0,61.27-69.8,63.4-69.8,63.4s29.15-5.47,29.15-32.64c0-27.17-26.33-30.44-43.27-30.44S0,56.92,0,56.92z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,54 @@
"use client";
import { useCallback, useImperativeHandle, useState } from "react";
type ActivityLog = {
id: string;
timestamp: number;
activity: string;
};
export interface ActivityLogAPI {
updateActivityLog: (activityLog: ActivityLog[]) => void;
}
interface ActivityLogProps {
ref: React.RefObject<ActivityLogAPI>;
}
const formatter = new Intl.DateTimeFormat("en-GB", { dateStyle: "short", timeStyle: "medium" });
export default function ActivityLog({ ref }: ActivityLogProps) {
const [activityLog, updateActivityLog] = useState<ActivityLog[]>([]);
const handleActivityLogUpdate = useCallback(
(newActivities: ActivityLog[]) => {
updateActivityLog([...activityLog, ...newActivities]);
const activityLogContainer = document.getElementById("activityLogContainer");
if (activityLogContainer) {
activityLogContainer.scrollTo({ top: 0, behavior: "smooth" });
}
},
[activityLog],
);
useImperativeHandle(ref, () => ({
updateActivityLog: handleActivityLogUpdate,
}));
return (
<div className="overflow-y-auto max-h-96" id="activityLogContainer">
<div className="flex flex-col-reverse gap-2">
{activityLog.map((activity) => (
<div key={activity.id} className="flex gap-2 items-top">
<span className="flex-1/3 text-xs text-gray-300 whitespace-nowrap mt-1.5">{formatter.format(activity.timestamp)}: </span>
<span className="flex-2/3 text-white whitespace-normal">{activity.activity}</span>
</div>
))}
{!activityLog.length && <span className="text-white">No activity logged.</span>}
</div>
</div>
);
}

View file

@ -1,27 +1,36 @@
"use client";
import { ChangeEvent, useEffect } from "react";
import { CTAButton, StatusIndicator } from "@/ui/elements";
import { SearchView } from "@/ui/Partials";
import { LoadingIndicator } from "@/ui/App";
import { AddIcon, SearchIcon } from "@/ui/Icons";
import { CTAButton, GhostButton, Modal, NeutralButton, StatusIndicator } from "@/ui/elements";
import { useBoolean } from "@/utils";
import addData from "@/modules/ingestion/addData";
import cognifyDataset from "@/modules/datasets/cognifyDataset";
import useDatasets from "@/modules/ingestion/useDatasets";
import getDatasetGraph from '@/modules/datasets/getDatasetGraph';
import createDataset from "@/modules/datasets/createDataset";
import getDatasetGraph from "@/modules/datasets/getDatasetGraph";
import useDatasets, { Dataset } from "@/modules/ingestion/useDatasets";
export interface NodesAndEdges {
export interface NodesAndLinks {
nodes: { id: string; label: string }[];
links: { source: string; target: string; label: string }[];
}
export interface NodesAndEdges {
nodes: { id: string; label: string }[];
edges: { source: string; target: string; label: string }[];
}
interface CogneeAddWidgetProps {
onData: (data: NodesAndEdges) => void;
onData: (data: NodesAndLinks) => void;
}
export default function CogneeAddWidget({ onData }: CogneeAddWidgetProps) {
const {
datasets,
addDataset,
removeDataset,
refreshDatasets,
} = useDatasets();
@ -38,52 +47,113 @@ export default function CogneeAddWidget({ onData }: CogneeAddWidgetProps) {
}));
}
});
}, [refreshDatasets]);
}, [onData, refreshDatasets]);
const handleAddFiles = (dataset: { id?: string, name?: string }, event: ChangeEvent<HTMLInputElement>) => {
const {
value: isProcessingFiles,
setTrue: setProcessingFilesInProgress,
setFalse: setProcessingFilesDone,
} = useBoolean(false);
const handleAddFiles = (dataset: Dataset, event: ChangeEvent<HTMLInputElement>) => {
event.stopPropagation();
if (!event.currentTarget.files) {
throw new Error("Error: No files added to the uploader input.");
if (isProcessingFiles) {
return;
}
const files: File[] = Array.from(event.currentTarget.files);
setProcessingFilesInProgress();
if (!event.target.files) {
return;
}
const files: File[] = Array.from(event.target.files);
if (!files.length) {
return;
}
return addData(dataset, files)
.then(() => {
console.log("Data added successfully.");
const onUpdate = (data: any) => {
const onUpdate = (data: NodesAndEdges) => {
onData({
nodes: data.payload.nodes,
links: data.payload.edges,
nodes: data.nodes,
links: data.edges,
});
setProcessingFilesDone();
};
return cognifyDataset(dataset, onUpdate)
.then((data) => console.log(data));
.then(() => {
refreshDatasets();
});
});
};
const handleAddFilesNoDataset = (event: ChangeEvent<HTMLInputElement>) => {
event.stopPropagation();
if (isProcessingFiles) {
return;
}
setProcessingFilesInProgress();
createDataset({ name: "main_dataset" })
.then((newDataset: Dataset) => {
return handleAddFiles(newDataset, event);
});
};
const {
value: isSearchModalOpen,
setTrue: openSearchModal,
setFalse: closeSearchModal,
} = useBoolean(false);
const handleSearchClick = () => {
openSearchModal();
};
return (
<div className="flex flex-col gap-4 mb-4">
<div className="flex flex-col gap-4">
{datasets.length ? datasets.map((dataset) => (
<div key={dataset.id} className="flex gap-8 items-center">
<div key={dataset.id} className="flex gap-8 items-center justify-between">
<div className="flex flex-row gap-4 items-center">
<StatusIndicator status={dataset.status} />
<span className="text-white">{dataset.name}</span>
</div>
<CTAButton type="button" className="relative">
<input type="file" multiple onChange={handleAddFiles.bind(null, dataset)} className="absolute w-full h-full cursor-pointer opacity-0" />
<span>+ Add Data</span>
</CTAButton>
<div className="flex gap-2">
<CTAButton type="button" className="relative">
<input tabIndex={-1} type="file" multiple onChange={handleAddFiles.bind(null, dataset)} className="absolute w-full h-full cursor-pointer opacity-0" />
<span className="flex flex-row gap-2 items-center">
<AddIcon />
{isProcessingFiles && <LoadingIndicator />}
</span>
</CTAButton>
<NeutralButton onClick={handleSearchClick} type="button">
<SearchIcon />
</NeutralButton>
</div>
</div>
)) : (
<CTAButton type="button" className="relative">
<input type="file" multiple onChange={handleAddFiles.bind(null, { name: "main_dataset" })} className="absolute w-full h-full cursor-pointer opacity-0" />
<span>+ Add Data</span>
<CTAButton type="button" className="relative" disabled={isProcessingFiles}>
<input disabled={isProcessingFiles} tabIndex={-1} type="file" multiple onChange={handleAddFilesNoDataset} className="absolute w-full h-full cursor-pointer opacity-0" />
<span className="flex flex-row gap-2 items-center">
+ Add Data
{isProcessingFiles && <LoadingIndicator />}
</span>
</CTAButton>
)}
<Modal isOpen={isSearchModalOpen}>
<div className="relative w-full max-w-3xl h-full max-h-5/6">
<GhostButton onClick={closeSearchModal} className="absolute right-2 top-2">
<AddIcon className="rotate-45" />
</GhostButton>
<SearchView />
</div>
</Modal>
</div>
);
}

View file

@ -1,4 +1,7 @@
import { useState } from "react";
import { fetch } from "@/utils";
import { v4 as uuid4 } from "uuid";
import { LoadingIndicator } from "@/ui/App";
import { CTAButton, Input } from "@/ui/elements";
interface CrewAIFormPayload extends HTMLFormElement {
@ -6,21 +9,108 @@ interface CrewAIFormPayload extends HTMLFormElement {
username2: HTMLInputElement;
}
export default function CrewAITrigger() {
interface CrewAITriggerProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onData: (data: any) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onActivity: (activities: any) => void;
}
export default function CrewAITrigger({ onData, onActivity }: CrewAITriggerProps) {
const [isCrewAIRunning, setIsCrewAIRunning] = useState(false);
const handleRunCrewAI = (event: React.FormEvent<CrewAIFormPayload>) => {
fetch("/v1/crew-ai/run", {
event.preventDefault();
const formElements = event.currentTarget;
const crewAIConfig = {
username1: formElements.username1.value,
username2: formElements.username2.value,
};
const websocket = new WebSocket("ws://localhost:8000/api/v1/crewai/subscribe");
onActivity([{ id: uuid4(), timestamp: Date.now(), activity: "Dispatching hiring crew agents" }]);
websocket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.status === "PipelineRunActivity") {
onActivity([data.payload]);
return;
}
onData({
nodes: data.payload.nodes,
links: data.payload.edges,
});
const nodes_type_map: { [key: string]: number } = {};
for (let i = 0; i < data.payload.nodes.length; i++) {
const node = data.payload.nodes[i];
if (!nodes_type_map[node.type]) {
nodes_type_map[node.type] = 0;
}
nodes_type_map[node.type] += 1;
}
const activityMessage = Object.entries(nodes_type_map).reduce((message, [type, count]) => {
return `${message}\n | ${type}: ${count}`;
}, "Graph updated:");
onActivity([{
id: uuid4(),
timestamp: Date.now(),
activity: activityMessage,
}]);
if (data.status === "PipelineRunCompleted") {
websocket.close();
}
};
onData(null);
setIsCrewAIRunning(true);
return fetch("/v1/crewai/run", {
method: "POST",
body: new FormData(event.currentTarget),
body: JSON.stringify(crewAIConfig),
headers: {
"Content-Type": "application/json",
},
})
.then(response => response.json())
.then((data) => console.log(data));
.then(() => {
onActivity([{ id: uuid4(), timestamp: Date.now(), activity: "Hiring crew agents made a decision" }]);
})
.catch(() => {
onActivity([{ id: uuid4(), timestamp: Date.now(), activity: "Hiring crew agents had problems while executing" }]);
})
.finally(() => {
websocket.close();
setIsCrewAIRunning(false);
});
};
return (
<form className="w-full flex flex-row gap-2 items-center" onSubmit={handleRunCrewAI}>
<Input type="text" placeholder="Github Username" required />
<Input type="text" placeholder="Github Username" required />
<CTAButton type="submit" className="whitespace-nowrap">Run CrewAI</CTAButton>
<form className="w-full flex flex-col gap-2" onSubmit={handleRunCrewAI}>
<h1 className="text-2xl text-white">Cognee Dev Mexican Standoff</h1>
<span className="text-white">Agents compare GitHub profiles, and make a decision who is a better developer</span>
<div className="flex flex-row gap-2">
<div className="flex flex-col w-full flex-1/2">
<label className="block mb-1 text-white" htmlFor="username1">GitHub username</label>
<Input name="username1" type="text" placeholder="Github Username" required defaultValue="hajdul88" />
</div>
<div className="flex flex-col w-full flex-1/2">
<label className="block mb-1 text-white" htmlFor="username2">GitHub username</label>
<Input name="username2" type="text" placeholder="Github Username" required defaultValue="lxobr" />
</div>
</div>
<CTAButton type="submit" disabled={isCrewAIRunning} className="whitespace-nowrap">
Start Mexican Standoff
{isCrewAIRunning && <LoadingIndicator />}
</CTAButton>
</form>
);
}

View file

@ -1,15 +1,19 @@
"use client";
import { v4 as uuid4 } from "uuid";
import classNames from "classnames";
import { NodeObject } from "react-force-graph-2d";
import { ChangeEvent, useImperativeHandle, useState } from "react";
// import classNames from "classnames";
import { NodeObject, LinkObject } from "react-force-graph-2d";
import { ChangeEvent, useEffect, useImperativeHandle, useRef, useState } from "react";
import { DeleteIcon } from "@/ui/Icons";
import { FeedbackForm } from "@/ui/Partials";
// import { FeedbackForm } from "@/ui/Partials";
import { CTAButton, Input, NeutralButton, Select } from "@/ui/elements";
interface GraphControlsProps {
data?: {
nodes: NodeObject[];
links: LinkObject[];
};
isAddNodeFormOpen: boolean;
ref: React.RefObject<GraphControlsAPI>;
onFitIntoView: () => void;
@ -21,29 +25,55 @@ export interface GraphControlsAPI {
getSelectedNode: () => NodeObject | null;
}
type ActivityLog = {
id: string;
timestamp: number;
activity: string;
}[];
// type ActivityLog = {
// id: string;
// timestamp: number;
// activity: string;
// };
type NodeProperties = {
type NodeProperty = {
id: string;
name: string;
value: string;
}[];
};
export default function GraphControls({ isAddNodeFormOpen, onGraphShapeChange, onFitIntoView, ref }: GraphControlsProps) {
// const formatter = new Intl.DateTimeFormat("en-GB", { dateStyle: "short", timeStyle: "medium" });
const DEFAULT_GRAPH_SHAPE = "lr";
const GRAPH_SHAPES = [{
value: "none",
label: "None",
}, {
value: "td",
label: "Top-down",
}, {
value: "bu",
label: "Bottom-up",
}, {
value: "lr",
label: "Left-right",
}, {
value: "rl",
label: "Right-left",
}, {
value: "radialin",
label: "Radial-in",
}, {
value: "radialout",
label: "Radial-out",
}];
export default function GraphControls({ data, isAddNodeFormOpen, onGraphShapeChange, onFitIntoView, ref }: GraphControlsProps) {
const [selectedNode, setSelectedNode] = useState<NodeObject | null>(null);
const [activityLog, setActivityLog] = useState<ActivityLog>([]);
const [nodeProperties, setNodeProperties] = useState<NodeProperties>([]);
const [newProperty, setNewProperty] = useState<NodeProperties[0]>({
const [nodeProperties, setNodeProperties] = useState<NodeProperty[]>([]);
const [newProperty, setNewProperty] = useState<NodeProperty>({
id: uuid4(),
name: "",
value: "",
});
const handlePropertyChange = (property: NodeProperties[0], property_key: string, event: ChangeEvent<HTMLInputElement>) => {
const handlePropertyChange = (property: NodeProperty, property_key: string, event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setNodeProperties(nodeProperties.map((nodeProperty) => (nodeProperty.id === property.id ? {...nodeProperty, [property_key]: value } : nodeProperty)));
@ -58,11 +88,11 @@ export default function GraphControls({ isAddNodeFormOpen, onGraphShapeChange, o
}
};
const handlePropertyDelete = (property: NodeProperties[0]) => {
const handlePropertyDelete = (property: NodeProperty) => {
setNodeProperties(nodeProperties.filter((nodeProperty) => nodeProperty.id !== property.id));
};
const handleNewPropertyChange = (property: NodeProperties[0], property_key: string, event: ChangeEvent<HTMLInputElement>) => {
const handleNewPropertyChange = (property: NodeProperty, property_key: string, event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setNewProperty({...property, [property_key]: value });
@ -73,111 +103,139 @@ export default function GraphControls({ isAddNodeFormOpen, onGraphShapeChange, o
getSelectedNode: () => selectedNode,
}));
const [selectedTab, setSelectedTab] = useState("nodeDetails");
// const [selectedTab, setSelectedTab] = useState("nodeDetails");
const handleGraphShapeControl = (event: ChangeEvent<HTMLSelectElement>) => {
setIsAuthShapeChangeEnabled(false);
onGraphShapeChange(event.target.value);
};
const [isAuthShapeChangeEnabled, setIsAuthShapeChangeEnabled] = useState(true);
const shapeChangeTimeout = useRef<number | null>();
useEffect(() => {
onGraphShapeChange(DEFAULT_GRAPH_SHAPE);
const graphShapesNum = GRAPH_SHAPES.length;
function switchShape(shapeIndex: number) {
if (!isAuthShapeChangeEnabled || !data) {
if (shapeChangeTimeout.current) {
clearTimeout(shapeChangeTimeout.current);
shapeChangeTimeout.current = null;
}
return;
}
shapeChangeTimeout.current = setTimeout(() => {
const newValue = GRAPH_SHAPES[shapeIndex].value;
onGraphShapeChange(newValue);
const graphShapeSelectElement = document.getElementById("graph-shape-select") as HTMLSelectElement;
graphShapeSelectElement.value = newValue;
switchShape((shapeIndex + 1) % graphShapesNum);
}, 5000) as unknown as number;
};
switchShape(0);
setTimeout(() => {
onFitIntoView();
}, 500);
return () => {
if (shapeChangeTimeout.current) {
clearTimeout(shapeChangeTimeout.current);
shapeChangeTimeout.current = null;
}
};
}, [data, isAuthShapeChangeEnabled, onFitIntoView, onGraphShapeChange]);
return (
<>
<div className="flex">
<button onClick={() => setSelectedTab("nodeDetails")} className={classNames("cursor-pointer pt-4 pb-4 align-center text-gray-300 border-b-2 w-30", { "border-b-indigo-600 text-white": selectedTab === "nodeDetails" })}>
<span className="whitespace-nowrap">Node Details</span>
</button>
<button onClick={() => setSelectedTab("activityLog")} className={classNames("cursor-pointer pt-4 pb-4 align-center text-gray-300 border-b-2 w-30", { "border-b-indigo-600 text-white": selectedTab === "activityLog" })}>
<span className="whitespace-nowrap">Activity Log</span>
</button>
<button onClick={() => setSelectedTab("feedback")} className={classNames("cursor-pointer pt-4 pb-4 align-center text-gray-300 border-b-2 w-30", { "border-b-indigo-600 text-white": selectedTab === "feedback" })}>
<div className="flex w-full">
{/* <button onClick={() => setSelectedTab("nodeDetails")} className={classNames("cursor-pointer pt-4 pb-4 align-center text-gray-300 border-b-2 w-30 flex-1/3", { "border-b-indigo-600 text-white": selectedTab === "nodeDetails" })}> */}
<span className="whitespace-nowrap text-white">Node Details</span>
{/* </button> */}
{/* <button onClick={() => setSelectedTab("feedback")} className={classNames("cursor-pointer pt-4 pb-4 align-center text-gray-300 border-b-2 w-30 flex-1/3", { "border-b-indigo-600 text-white": selectedTab === "feedback" })}>
<span className="whitespace-nowrap">Feedback</span>
</button>
</button> */}
</div>
<div className="pt-4">
{selectedTab === "nodeDetails" && (
<>
<div className="w-full flex flex-row gap-2 items-center mb-4">
<label className="text-gray-300 whitespace-nowrap">Graph Shape:</label>
<Select onChange={handleGraphShapeControl}>
<option selected value="none">None</option>
<option value="td">Top-down</option>
<option value="bu">Bottom-up</option>
<option value="lr">Left-right</option>
<option value="rl">Right-left</option>
<option value="radialin">Radial-in</option>
<option value="radialout">Radial-out</option>
</Select>
</div>
{/* {selectedTab === "nodeDetails" && ( */}
<>
<div className="w-full flex flex-row gap-2 items-center mb-4">
<label className="text-gray-300 whitespace-nowrap flex-1/5">Graph Shape:</label>
<Select defaultValue={DEFAULT_GRAPH_SHAPE} onChange={handleGraphShapeControl} id="graph-shape-select" className="flex-2/5">
{GRAPH_SHAPES.map((shape) => (
<option key={shape.value} value={shape.value}>{shape.label}</option>
))}
</Select>
<NeutralButton onClick={onFitIntoView} className="flex-2/5 whitespace-nowrap">Fit Graph into View</NeutralButton>
</div>
<NeutralButton onClick={onFitIntoView} className="mb-4">Fit Graph into View</NeutralButton>
{isAddNodeFormOpen ? (
<form className="flex flex-col gap-4" onSubmit={() => {}}>
<div className="flex flex-row gap-4 items-center">
<span className="text-gray-300 whitespace-nowrap">Source Node ID:</span>
<Input readOnly type="text" defaultValue={selectedNode!.id} />
{isAddNodeFormOpen ? (
<form className="flex flex-col gap-4" onSubmit={() => {}}>
<div className="flex flex-row gap-4 items-center">
<span className="text-gray-300 whitespace-nowrap">Source Node ID:</span>
<Input readOnly type="text" defaultValue={selectedNode!.id} />
</div>
<div className="flex flex-col gap-4 items-end">
{nodeProperties.map((property) => (
<div key={property.id} className="w-full flex flex-row gap-2 items-center">
<Input className="flex-1/3" type="text" placeholder="Property name" required value={property.name} onChange={handlePropertyChange.bind(null, property, "name")} />
<Input className="flex-2/3" type="text" placeholder="Property value" required value={property.value} onChange={handlePropertyChange.bind(null, property, "value")} />
<button className="border-1 border-white p-2 rounded-sm" onClick={handlePropertyDelete.bind(null, property)}>
<DeleteIcon width={16} height={18} color="white" />
</button>
</div>
))}
<div className="w-full flex flex-row gap-2 items-center">
<Input className="flex-1/3" type="text" placeholder="Property name" required value={newProperty.name} onChange={handleNewPropertyChange.bind(null, newProperty, "name")} />
<Input className="flex-2/3" type="text" placeholder="Property value" required value={newProperty.value} onChange={handleNewPropertyChange.bind(null, newProperty, "value")} />
<NeutralButton type="button" className="" onClick={handlePropertyAdd}>Add</NeutralButton>
</div>
<div className="flex flex-col gap-4 items-end">
{nodeProperties.map((property) => (
<div key={property.id} className="w-full flex flex-row gap-2 items-center">
<Input className="flex-1/3" type="text" placeholder="Property name" required value={property.name} onChange={handlePropertyChange.bind(null, property, "name")} />
<Input className="flex-2/3" type="text" placeholder="Property value" required value={property.value} onChange={handlePropertyChange.bind(null, property, "value")} />
<button className="border-1 border-white p-2 rounded-sm" onClick={handlePropertyDelete.bind(null, property)}>
<DeleteIcon width={16} height={18} color="white" />
</button>
</div>
<CTAButton type="submit">Add Node</CTAButton>
</form>
) : (
selectedNode ? (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2 overflow-y-auto max-h-96 pr-2">
<div className="flex gap-2 items-top">
<span className="text-gray-300">ID:</span>
<span className="text-white">{selectedNode.id}</span>
</div>
<div className="flex gap-2 items-top">
<span className="text-gray-300">Label:</span>
<span className="text-white">{selectedNode.label}</span>
</div>
{Object.entries(selectedNode.properties).map(([key, value]) => (
<div key={key} className="flex gap-2 items-top">
<span className="text-gray-300">{key.charAt(0).toUpperCase() + key.slice(1)}:</span>
<span className="text-white">{typeof value === "object" ? JSON.stringify(value) : value as string}</span>
</div>
))}
<div className="w-full flex flex-row gap-2 items-center">
<Input className="flex-1/3" type="text" placeholder="Property name" required value={newProperty.name} onChange={handleNewPropertyChange.bind(null, newProperty, "name")} />
<Input className="flex-2/3" type="text" placeholder="Property value" required value={newProperty.value} onChange={handleNewPropertyChange.bind(null, newProperty, "value")} />
<NeutralButton type="button" className="" onClick={handlePropertyAdd}>Add</NeutralButton>
</div>
</div>
<CTAButton type="submit">Add Node</CTAButton>
</form>
) : (
selectedNode ? (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<div className="flex gap-2 items-center">
<span className="text-gray-300">ID:</span>
<span className="text-white">{selectedNode.id}</span>
</div>
{Object.entries(selectedNode.properties).map(([key, value]) => (
<div key={key} className="flex gap-2 items-center">
<span className="text-gray-300">{key}:</span>
<span className="text-white">{typeof value === "object" ? JSON.stringify(value) : value as string}</span>
</div>
))}
</div>
<CTAButton type="button" onClick={() => {}}>Edit Node</CTAButton>
</div>
) : (
<span className="text-white">No node selected.</span>
)
)}
</>
)}
{selectedTab === "activityLog" && (
<div className="flex flex-col gap-2">
{activityLog.map((activity) => (
<div key={activity.id} className="flex gap-2 items-center">
<span className="text-gray-300">{activity.timestamp}</span>
<span className="text-white">{activity.activity}</span>
{/* <CTAButton type="button" onClick={() => {}}>Edit Node</CTAButton> */}
</div>
))}
{!activityLog.length && <span className="text-white">No activity logged.</span>}
</div>
)}
) : (
<span className="text-white">No node selected.</span>
)
)}
</>
{/* )} */}
{selectedTab === "feedback" && (
{/* {selectedTab === "feedback" && (
<div className="flex flex-col gap-2">
<FeedbackForm onSuccess={() => {}} />
</div>
)}
)} */}
</div>
</>
);

View file

@ -0,0 +1,25 @@
import { NodeObject } from "react-force-graph-2d";
import getColorForNodeType from './getColorForNodeType';
interface GraphLegendProps {
data?: NodeObject[];
}
export default function GraphLegend({ data }: GraphLegendProps) {
const legend: Set<string> = new Set();
for (let i = 0; i < Math.min(data?.length || 0, 100); i++) {
legend.add(data![i].type);
}
return (
<div className="flex flex-col gap-1">
{Array.from(legend).map((nodeType) => (
<div key={nodeType} className="flex flex-row items-center gap-2">
<span className="w-2 h-2 rounded-2xl" style={{ backgroundColor: getColorForNodeType(nodeType) }} />
<span className="text-white">{nodeType}</span>
</div>
))}
</div>
);
}

View file

@ -1,24 +1,24 @@
"use client";
import { forceCollide, forceManyBody } from "d3-force-3d";
import { useEffect, useRef, useState } from "react";
import ForceGraph, { ForceGraphMethods, LinkObject, NodeObject } from "react-force-graph-2d";
import { useCallback, useRef, useState, MutableRefObject } from "react";
import Link from "next/link";
import { TextLogo } from "@/ui/App";
import { Divider } from "@/ui/Layout";
import { Footer } from "@/ui/Partials";
import CrewAITrigger from "./CrewAITrigger";
import CogneeAddWidget, { NodesAndEdges } from "./CogneeAddWidget";
import GraphLegend from "./GraphLegend";
import { DiscordIcon, GithubIcon } from "@/ui/Icons";
import ActivityLog, { ActivityLogAPI } from "./ActivityLog";
import GraphControls, { GraphControlsAPI } from "./GraphControls";
import CogneeAddWidget, { NodesAndLinks } from "./CogneeAddWidget";
import GraphVisualization, { GraphVisualizationAPI } from "./GraphVisualization";
import { useBoolean } from "@/utils";
// import exampleData from "./example_data.json";
interface GraphNode {
id: string | number;
label: string;
properties?: {};
properties?: object;
}
interface GraphData {
@ -29,246 +29,95 @@ interface GraphData {
export default function GraphView() {
const {
value: isAddNodeFormOpen,
setTrue: enableAddNodeForm,
setFalse: disableAddNodeForm,
} = useBoolean(false);
const [data, updateData] = useState<GraphData | null>(null);
const [data, updateData] = useState<GraphData>();
const onDataChange = (newData: NodesAndEdges) => {
if (data === null) {
updateData({
nodes: newData.nodes,
links: newData.links,
});
} else {
updateData({
nodes: [...data.nodes, ...newData.nodes],
links: [...data.links, ...newData.links],
});
}
};
const graphRef = useRef<ForceGraphMethods>();
const graphControls = useRef<GraphControlsAPI>(null);
const handleNodeClick = (node: NodeObject) => {
graphControls.current?.setSelectedNode(node);
graphRef.current?.d3ReheatSimulation();
};
const textSize = 6;
const nodeSize = 15;
const addNodeDistanceFromSourceNode = 15;
const handleBackgroundClick = (event: MouseEvent) => {
const graphBoundingBox = document.getElementById("graph-container")?.querySelector("canvas")?.getBoundingClientRect();
const x = event.clientX - graphBoundingBox!.x;
const y = event.clientY - graphBoundingBox!.y;
const graphClickCoords = graphRef.current!.screen2GraphCoords(x, y);
const selectedNode = graphControls.current?.getSelectedNode();
if (!selectedNode) {
const onDataChange = useCallback((newData: NodesAndLinks) => {
if (newData === null) {
// Requests for resetting the data
updateData(undefined);
return;
}
const distanceFromAddNode = Math.sqrt(
Math.pow(graphClickCoords.x - (selectedNode!.x! + addNodeDistanceFromSourceNode), 2)
+ Math.pow(graphClickCoords.y - (selectedNode!.y! + addNodeDistanceFromSourceNode), 2)
);
if (distanceFromAddNode <= 10) {
enableAddNodeForm();
} else {
disableAddNodeForm();
graphControls.current?.setSelectedNode(null);
}
};
function renderNode(node: NodeObject, ctx: CanvasRenderingContext2D, globalScale: number) {
const selectedNode = graphControls.current?.getSelectedNode();
ctx.save();
if (node.id === selectedNode?.id) {
ctx.fillStyle = "gray";
ctx.beginPath();
ctx.arc(node.x! + addNodeDistanceFromSourceNode, node.y! + addNodeDistanceFromSourceNode, 10, 0, 2 * Math.PI);
ctx.fill();
ctx.beginPath();
ctx.moveTo(node.x! + addNodeDistanceFromSourceNode - 5, node.y! + addNodeDistanceFromSourceNode)
ctx.lineTo(node.x! + addNodeDistanceFromSourceNode - 5 + 10, node.y! + addNodeDistanceFromSourceNode);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(node.x! + addNodeDistanceFromSourceNode, node.y! + addNodeDistanceFromSourceNode - 5)
ctx.lineTo(node.x! + addNodeDistanceFromSourceNode, node.y! + addNodeDistanceFromSourceNode - 5 + 10);
ctx.stroke();
if (!newData.nodes.length && !newData.links.length) {
return;
}
// ctx.beginPath();
// ctx.arc(node.x, node.y, nodeSize, 0, 2 * Math.PI);
// ctx.fill();
updateData(newData);
}, []);
// draw text label (with background rect)
const textPos = {
x: node.x!,
y: node.y!,
};
ctx.translate(textPos.x, textPos.y);
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "#333333";
ctx.font = `${textSize}px Sans-Serif`;
ctx.fillText(node.label, 0, 0);
const graphRef = useRef<GraphVisualizationAPI>();
ctx.restore();
}
const graphControls = useRef<GraphControlsAPI>();
function renderLink(link: LinkObject, ctx: CanvasRenderingContext2D) {
const MAX_FONT_SIZE = 4;
const LABEL_NODE_MARGIN = nodeSize * 1.5;
const activityLog = useRef<ActivityLogAPI>();
const start = link.source;
const end = link.target;
// ignore unbound links
if (typeof start !== "object" || typeof end !== "object") return;
const textPos = {
x: start.x! + (end.x! - start.x!) / 2,
y: start.y! + (end.y! - start.y!) / 2,
};
const relLink = { x: end.x! - start.x!, y: end.y! - start.y! };
const maxTextLength = Math.sqrt(Math.pow(relLink.x, 2) + Math.pow(relLink.y, 2)) - LABEL_NODE_MARGIN * 2;
let textAngle = Math.atan2(relLink.y, relLink.x);
// maintain label vertical orientation for legibility
if (textAngle > Math.PI / 2) textAngle = -(Math.PI - textAngle);
if (textAngle < -Math.PI / 2) textAngle = -(-Math.PI - textAngle);
const label = link.label
// estimate fontSize to fit in link length
ctx.font = "1px Sans-Serif";
const fontSize = Math.min(MAX_FONT_SIZE, maxTextLength / ctx.measureText(label).width);
ctx.font = `${fontSize}px Sans-Serif`;
const textWidth = ctx.measureText(label).width;
const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2); // some padding
// draw text label (with background rect)
ctx.save();
ctx.translate(textPos.x, textPos.y);
ctx.rotate(textAngle);
ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
ctx.fillRect(- bckgDimensions[0] / 2, - bckgDimensions[1] / 2, bckgDimensions[0], bckgDimensions[1]);
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "darkgrey";
ctx.fillText(label, 0, 0);
ctx.restore();
}
function handleDagError(loopNodeIds: (string | number)[]) {
console.log(loopNodeIds);
}
useEffect(() => {
// add collision force
graphRef.current!.d3Force("collision", forceCollide(nodeSize * 1.5));
graphRef.current!.d3Force("charge", forceManyBody().strength(-1500).distanceMin(300).distanceMax(900));
}, [data]);
const [graphShape, setGraphShape] = useState<string | undefined>(undefined);
return (
<main className="flex flex-col h-full">
<div className="pt-6 pr-3 pb-3 pl-6">
<div className="flex flex-row justify-between items-center pt-6 pr-6 pb-6 pl-6">
<TextLogo width={86} height={24} />
<span className="flex flex-row items-center gap-8">
<Link target="_blank" href="https://www.cognee.ai/">
<span>Cognee Home</span>
</Link>
<Link target="_blank" href="https://github.com/topoteretes/cognee">
<GithubIcon color="black" />
</Link>
<Link target="_blank" href="https://discord.gg/m63hxKsp4p">
<DiscordIcon color="black" />
</Link>
</span>
</div>
<Divider />
<div className="w-full h-full relative overflow-hidden">
<div className="w-full h-full" id="graph-container">
{data ? (
<ForceGraph
ref={graphRef}
dagMode={graphShape as undefined}
dagLevelDistance={300}
onDagError={handleDagError}
graphData={data}
<GraphVisualization
key={data?.nodes.length}
ref={graphRef as MutableRefObject<GraphVisualizationAPI>}
data={data}
graphControls={graphControls as MutableRefObject<GraphControlsAPI>}
/>
nodeLabel="label"
nodeRelSize={nodeSize}
nodeCanvasObject={renderNode}
nodeCanvasObjectMode={() => "after"}
nodeAutoColorBy="group"
<div className="absolute top-2 left-2 flex flex-col gap-2">
<div className="bg-gray-500 pt-4 pr-4 pb-4 pl-4 rounded-md w-sm">
<CogneeAddWidget onData={onDataChange} />
</div>
{/* <div className="bg-gray-500 pt-4 pr-4 pb-4 pl-4 rounded-md w-sm">
<CrewAITrigger onData={onDataChange} onActivity={(activities) => activityLog.current?.updateActivityLog(activities)} />
</div> */}
<div className="bg-gray-500 pt-4 pr-4 pb-4 pl-4 rounded-md w-sm">
<h2 className="text-xl text-white mb-4">Activity Log</h2>
<ActivityLog ref={activityLog as MutableRefObject<ActivityLogAPI>} />
</div>
</div>
linkLabel="label"
linkCanvasObject={renderLink}
linkCanvasObjectMode={() => "after"}
linkDirectionalArrowLength={3.5}
linkDirectionalArrowRelPos={1}
onNodeClick={handleNodeClick}
onBackgroundClick={handleBackgroundClick}
d3VelocityDecay={0.3}
/>
) : (
<ForceGraph
ref={graphRef}
dagMode="lr"
dagLevelDistance={100}
graphData={{
nodes: [{ id: 1, label: "Add" }, { id: 2, label: "Cognify" }, { id: 3, label: "Search" }],
links: [{ source: 1, target: 2, label: "but don't forget to" }, { source: 2, target: 3, label: "and after that you can" }],
}}
nodeLabel="label"
nodeRelSize={20}
nodeCanvasObject={renderNode}
nodeCanvasObjectMode={() => "after"}
nodeAutoColorBy="group"
linkLabel="label"
linkCanvasObject={renderLink}
linkCanvasObjectMode={() => "after"}
linkDirectionalArrowLength={3.5}
linkDirectionalArrowRelPos={1}
<div className="absolute top-2 right-2 flex flex-col gap-2 items-end">
<div className="bg-gray-500 pt-4 pr-4 pb-4 pl-4 rounded-md w-110">
<GraphControls
data={data}
ref={graphControls as MutableRefObject<GraphControlsAPI>}
isAddNodeFormOpen={isAddNodeFormOpen}
onFitIntoView={() => graphRef.current!.zoomToFit(1000, 50)}
onGraphShapeChange={(shape) => graphRef.current!.setGraphShape(shape)}
/>
</div>
{data?.nodes.length && (
<div className="bg-gray-500 pt-4 pr-4 pb-4 pl-4 rounded-md w-48">
<GraphLegend data={data?.nodes} />
</div>
)}
</div>
<div className="absolute top-2 left-2 bg-gray-500 pt-4 pr-4 pb-4 pl-4 rounded-md max-w-2xl">
<CogneeAddWidget onData={onDataChange} />
<CrewAITrigger />
</div>
<div className="absolute top-2 right-2 bg-gray-500 pt-4 pr-4 pb-4 pl-4 rounded-md">
<GraphControls
ref={graphControls}
isAddNodeFormOpen={isAddNodeFormOpen}
onFitIntoView={() => graphRef.current?.zoomToFit(1000, 50)}
onGraphShapeChange={setGraphShape}
/>
</div>
</div>
<Divider />
<div className="pl-6 pr-6">
<Footer>
<div className="flex flex-row items-center gap-6">
<span>Nodes: {data?.nodes.length}</span>
<span>Edges: {data?.links.length}</span>
</div>
{(data?.nodes.length || data?.links.length) && (
<div className="flex flex-row items-center gap-6">
<span>Nodes: {data?.nodes.length || 0}</span>
<span>Edges: {data?.links.length || 0}</span>
</div>
)}
</Footer>
</div>
</main>

View file

@ -0,0 +1,226 @@
"use client";
import { MutableRefObject, useEffect, useImperativeHandle, useRef, useState } from "react";
import { forceCollide, forceManyBody } from "d3-force-3d";
import ForceGraph, { ForceGraphMethods, GraphData, LinkObject, NodeObject } from "react-force-graph-2d";
import { GraphControlsAPI } from "./GraphControls";
import getColorForNodeType from "./getColorForNodeType";
interface GraphVisuzaliationProps {
ref: MutableRefObject<GraphVisualizationAPI>;
data?: GraphData<NodeObject, LinkObject>;
graphControls: MutableRefObject<GraphControlsAPI>;
}
export interface GraphVisualizationAPI {
zoomToFit: ForceGraphMethods["zoomToFit"];
setGraphShape: (shape: string) => void;
}
export default function GraphVisualization({ ref, data, graphControls }: GraphVisuzaliationProps) {
const textSize = 6;
const nodeSize = 15;
// const addNodeDistanceFromSourceNode = 15;
const handleNodeClick = (node: NodeObject) => {
graphControls.current?.setSelectedNode(node);
// ref.current?.d3ReheatSimulation()
}
const handleBackgroundClick = (/* event: MouseEvent */) => {
const selectedNode = graphControls.current?.getSelectedNode();
if (!selectedNode) {
return;
}
graphControls.current?.setSelectedNode(null);
// const graphBoundingBox = document.getElementById("graph-container")?.querySelector("canvas")?.getBoundingClientRect();
// const x = event.clientX - graphBoundingBox!.x;
// const y = event.clientY - graphBoundingBox!.y;
// const graphClickCoords = graphRef.current!.screen2GraphCoords(x, y);
// const distanceFromAddNode = Math.sqrt(
// Math.pow(graphClickCoords.x - (selectedNode!.x! + addNodeDistanceFromSourceNode), 2)
// + Math.pow(graphClickCoords.y - (selectedNode!.y! + addNodeDistanceFromSourceNode), 2)
// );
// if (distanceFromAddNode <= 10) {
// enableAddNodeForm();
// } else {
// disableAddNodeForm();
// graphControls.current?.setSelectedNode(null);
// }
};
function renderNode(node: NodeObject, ctx: CanvasRenderingContext2D, globalScale: number, renderType: string = "replace") {
// const selectedNode = graphControls.current?.getSelectedNode();
ctx.save();
// if (node.id === selectedNode?.id) {
// ctx.fillStyle = "gray";
// ctx.beginPath();
// ctx.arc(node.x! + addNodeDistanceFromSourceNode, node.y! + addNodeDistanceFromSourceNode, 10, 0, 2 * Math.PI);
// ctx.fill();
// ctx.beginPath();
// ctx.moveTo(node.x! + addNodeDistanceFromSourceNode - 5, node.y! + addNodeDistanceFromSourceNode)
// ctx.lineTo(node.x! + addNodeDistanceFromSourceNode - 5 + 10, node.y! + addNodeDistanceFromSourceNode);
// ctx.stroke();
// ctx.beginPath();
// ctx.moveTo(node.x! + addNodeDistanceFromSourceNode, node.y! + addNodeDistanceFromSourceNode - 5)
// ctx.lineTo(node.x! + addNodeDistanceFromSourceNode, node.y! + addNodeDistanceFromSourceNode - 5 + 10);
// ctx.stroke();
// }
if (renderType === "replace") {
ctx.beginPath();
ctx.fillStyle = getColorForNodeType(node.type);
ctx.arc(node.x!, node.y!, nodeSize, 0, 2 * Math.PI);
ctx.fill();
}
// draw text label (with background rect)
const textPos = {
x: node.x!,
y: node.y!,
};
ctx.translate(textPos.x, textPos.y);
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "#333333";
ctx.font = `${textSize}px Sans-Serif`;
ctx.fillText(node.label, 0, 0);
ctx.restore();
}
function renderLink(link: LinkObject, ctx: CanvasRenderingContext2D) {
const MAX_FONT_SIZE = 4;
const LABEL_NODE_MARGIN = nodeSize * 1.5;
const start = link.source;
const end = link.target;
// ignore unbound links
if (typeof start !== "object" || typeof end !== "object") return;
const textPos = {
x: start.x! + (end.x! - start.x!) / 2,
y: start.y! + (end.y! - start.y!) / 2,
};
const relLink = { x: end.x! - start.x!, y: end.y! - start.y! };
const maxTextLength = Math.sqrt(Math.pow(relLink.x, 2) + Math.pow(relLink.y, 2)) - LABEL_NODE_MARGIN * 2;
let textAngle = Math.atan2(relLink.y, relLink.x);
// maintain label vertical orientation for legibility
if (textAngle > Math.PI / 2) textAngle = -(Math.PI - textAngle);
if (textAngle < -Math.PI / 2) textAngle = -(-Math.PI - textAngle);
const label = link.label
// estimate fontSize to fit in link length
ctx.font = "1px Sans-Serif";
const fontSize = Math.min(MAX_FONT_SIZE, maxTextLength / ctx.measureText(label).width);
ctx.font = `${fontSize}px Sans-Serif`;
const textWidth = ctx.measureText(label).width;
const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2); // some padding
// draw text label (with background rect)
ctx.save();
ctx.translate(textPos.x, textPos.y);
ctx.rotate(textAngle);
ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
ctx.fillRect(- bckgDimensions[0] / 2, - bckgDimensions[1] / 2, bckgDimensions[0], bckgDimensions[1]);
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "darkgrey";
ctx.fillText(label, 0, 0);
ctx.restore();
}
function renderInitialNode(node: NodeObject, ctx: CanvasRenderingContext2D, globalScale: number) {
renderNode(node, ctx, globalScale, "after");
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function handleDagError(loopNodeIds: (string | number)[]) {}
const graphRef = useRef<ForceGraphMethods>();
useEffect(() => {
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));
}
}, [data, graphRef]);
const [graphShape, setGraphShape] = useState<string>();
useImperativeHandle(ref, () => ({
zoomToFit: graphRef.current!.zoomToFit,
setGraphShape: setGraphShape,
}));
return (
<div className="w-full h-full" id="graph-container">
{(data && typeof window !== "undefined") ? (
<ForceGraph
ref={graphRef}
dagMode={graphShape as unknown as undefined}
dagLevelDistance={300}
onDagError={handleDagError}
graphData={data}
nodeLabel="label"
nodeRelSize={nodeSize}
nodeCanvasObject={renderNode}
nodeCanvasObjectMode={() => "replace"}
linkLabel="label"
linkCanvasObject={renderLink}
linkCanvasObjectMode={() => "after"}
linkDirectionalArrowLength={3.5}
linkDirectionalArrowRelPos={1}
onNodeClick={handleNodeClick}
onBackgroundClick={handleBackgroundClick}
d3VelocityDecay={0.3}
/>
) : (
<ForceGraph
ref={graphRef}
dagMode={graphShape as unknown as undefined}
dagLevelDistance={100}
graphData={{
nodes: [{ id: 1, label: "Add" }, { id: 2, label: "Cognify" }, { id: 3, label: "Search" }],
links: [{ source: 1, target: 2, label: "but don't forget to" }, { source: 2, target: 3, label: "and after that you can" }],
}}
nodeLabel="label"
nodeRelSize={20}
nodeCanvasObject={renderInitialNode}
nodeCanvasObjectMode={() => "after"}
nodeAutoColorBy="type"
linkLabel="label"
linkCanvasObject={renderLink}
linkCanvasObjectMode={() => "after"}
linkDirectionalArrowLength={3.5}
linkDirectionalArrowRelPos={1}
/>
)}
</div>
);
}

View file

@ -0,0 +1,22 @@
import colors from "tailwindcss/colors";
import { formatHex } from "culori";
const NODE_COLORS = {
TextDocument: formatHex(colors.blue[500]),
DocumentChunk: formatHex(colors.green[500]),
TextSummary: formatHex(colors.orange[500]),
Entity: formatHex(colors.yellow[300]),
EntityType: formatHex(colors.purple[800]),
NodeSet: formatHex(colors.indigo[300]),
GitHubUser: formatHex(colors.gray[300]),
Comment: formatHex(colors.amber[500]),
Issue: formatHex(colors.red[500]),
Repository: formatHex(colors.stone[400]),
Commit: formatHex(colors.teal[500]),
File: formatHex(colors.emerald[500]),
FileChange: formatHex(colors.sky[500]),
};
export default function getColorForNodeType(type: string) {
return NODE_COLORS[type as keyof typeof NODE_COLORS] || colors.gray[500];
}

View file

@ -0,0 +1,82 @@
"use client";
import { useState } from "react";
import { fetch, useBoolean } from "@/utils";
import { CTAButton, Input } from "@/ui/elements";
import { LoadingIndicator } from '@/ui/App';
interface AuthFormPayload extends HTMLFormElement {
email: HTMLInputElement;
password: HTMLInputElement;
}
const errorsMap = {
LOGIN_BAD_CREDENTIALS: "Invalid username or password",
REGISTER_USER_ALREADY_EXISTS: "User already exists",
};
const defaultFormatPayload: (data: { email: string; password: string; }) => object = (data) => data;
export default function AuthForm({
submitButtonText = "Sign in",
authUrl = "/v1/auth/login",
formatPayload = defaultFormatPayload,
onSignInSuccess = () => window.location.href = "/",
}) {
const {
value: isSigningIn,
setTrue: disableSignIn,
setFalse: enableSignIn,
} = useBoolean(false);
const [signInError, setSignInError] = useState<string | null>(null);
const signIn = (event: React.FormEvent<AuthFormPayload>) => {
event.preventDefault();
const formElements = event.currentTarget;
// Backend expects username and password fields
const authCredentials = {
email: formElements.email.value,
password: formElements.password.value,
};
setSignInError(null);
disableSignIn();
const formattedPayload = formatPayload(authCredentials);
fetch(authUrl, {
method: "POST",
body: formattedPayload instanceof URLSearchParams ? formattedPayload.toString() : JSON.stringify(formattedPayload),
headers: {
"Content-Type": formattedPayload instanceof URLSearchParams ? "application/x-www-form-urlencoded" : "application/json",
},
})
.then(() => {
onSignInSuccess();
})
.catch(error => setSignInError(errorsMap[error.detail as keyof typeof errorsMap] || error.message))
.finally(() => enableSignIn());
};
return (
<form onSubmit={signIn} className="flex flex-col gap-4">
<label className="flex flex-col gap-1">
Email address*
<Input type="email" name="email" required placeholder="Email address*" defaultValue="default_user@example.com" />
</label>
<label className="flex flex-col gap-1">
Password*
<Input type="password" name="password" required placeholder="Password*" defaultValue="default_password" />
</label>
<CTAButton className="mt-6 mb-2" type="submit">
{submitButtonText}
{isSigningIn && <LoadingIndicator />}
</CTAButton>
{signInError && (
<span className="text-s text-red-500 mb-4">{signInError}</span>
)}
</form>
);
}

View file

@ -1,24 +1,40 @@
import { TextLogo } from "@/ui/App";
import { Divider } from "@/ui/Layout";
import Footer from "@/ui/Partials/Footer/Footer";
import SignInForm from "@/ui/Partials/SignInForm/SignInForm";
import Link from "next/link";
import { auth0 } from "@/modules/auth/auth0";
import { CTAButton } from "@/ui/elements";
export default async function AuthPage() {
const session = await auth0.getSession();
export default function AuthPage() {
return (
<main className="flex flex-col h-full">
<div className="pt-6 pr-3 pb-3 pl-6">
<TextLogo width={86} height={24} />
<div className="flex flex-col m-auto max-w-md h-full gap-8 pb-12 pt-6">
<h1><span className="text-xl">Welcome to cognee</span></h1>
{session ? (
<div className="flex flex-col gap-8">
<span className="text-lg">Hello, {session.user.name}!</span>
<Link href="/auth/logout">
<CTAButton>
Log out
</CTAButton>
</Link>
</div>
<Divider />
<div className="w-full max-w-md pt-12 pb-6 m-auto">
<div className="flex flex-col w-full gap-8">
<h1><span className="text-xl">Sign in</span></h1>
<SignInForm />
</div>
) : (
<div className="flex flex-row h-full gap-8">
<Link href="/auth/login?screen_hint=signup">
<CTAButton>
Sign up
</CTAButton>
</Link>
<Link href="/auth/login">
<CTAButton>
Log in
</CTAButton>
</Link>
</div>
<div className="pl-6 pr-6">
<Footer />
</div>
</main>
)}
</div>
)
}

View file

@ -0,0 +1,31 @@
import type { Metadata } from "next";
import { TextLogo } from "@/ui/App";
import { Divider } from "@/ui/Layout";
import { Footer } from "@/ui/Partials";
export const metadata: Metadata = {
title: "Cognee",
description: "Cognee authentication",
};
export default function AuthLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<main className="flex flex-col h-full">
<div className="pt-6 pr-3 pb-3 pl-6">
<TextLogo width={86} height={24} />
</div>
<Divider />
{children}
<Divider />
<div className="pl-6 pr-6">
<Footer />
</div>
</main>
);
}

View file

@ -0,0 +1,40 @@
"use client";
import Link from "next/link";
import Image from "next/image";
import AuthForm from "../AuthForm";
export default function LoginPage() {
return (
<div className="m-auto w-full max-w-md shadow-xl rounded-xl">
<div className="flex flex-col px-10 py-16 bg-white border-1 rounded-xl border-indigo-600 overflow-hidden">
<Image src="/images/cognee-logo-with-text.png" alt="Cognee logo" width={176} height={46} className="h-12 w-44 self-center mb-16" />
<h1 className="self-center text-xl mb-4">Welcome</h1>
<p className="self-center mb-10">Log in to continue with Cognee</p>
<AuthForm
authUrl="/v1/auth/login"
submitButtonText="Login"
formatPayload={formatPayload}
/>
<p className="text-center mt-2 text-sm">
<Link href="/auth/signup">
{"Or go to Sign up ->"}
</Link>
</p>
</div>
</div>
);
}
function formatPayload(data: { email: string, password: string }) {
const payload = new URLSearchParams();
payload.append("username", data.email);
payload.append("password", data.password);
return payload;
}

View file

@ -0,0 +1 @@
export { default } from "./LoginPage";

View file

@ -1 +1 @@
export { default } from './AuthPage';
export { default } from "./AuthPage";

View file

@ -0,0 +1,31 @@
"use client";
import Link from "next/link";
import Image from "next/image";
import AuthForm from "../AuthForm";
export default function SignUpPage() {
return (
<div className="m-auto w-full max-w-md shadow-xl rounded-xl">
<div className="flex flex-col px-10 py-16 bg-white border-1 rounded-xl border-indigo-600 overflow-hidden">
<Image src="/images/cognee-logo-with-text.png" alt="Cognee logo" width={176} height={46} className="h-12 w-44 self-center mb-16" />
<h1 className="self-center text-xl mb-4">Welcome</h1>
<p className="self-center mb-10">Sign up to start using Cognee</p>
<AuthForm
authUrl="/v1/auth/register"
submitButtonText="Sign up"
onSignInSuccess={() => window.location.href = "/auth/login"}
/>
<p className="text-center mt-2 text-sm">
<Link href="/auth/login">
{"Or go to Login ->"}
</Link>
</p>
</div>
</div>
);
}

View file

@ -0,0 +1 @@
export { default } from "./SignUpPage";

View file

@ -0,0 +1,17 @@
import { redirect } from "next/navigation";
import { auth0 } from "@/modules/auth/auth0";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function GET(request: Request) {
const accessToken = await auth0.getAccessToken();
if (accessToken) {
const response = new Response();
response.headers.set("Set-Cookie", `${process.env.AUTH_TOKEN_COOKIE_NAME}=${accessToken.token}; Expires=${new Date(accessToken.expiresAt * 1000).toUTCString()}; Path=/; SameSite=Lax; Domain=localhost; HttpOnly`);
return response;
} else {
redirect("/auth");
}
}

View file

@ -5,8 +5,8 @@ import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Cognee",
description: "Cognee Dev Mexican Standoff",
};
export default function RootLayout({

View file

@ -1,130 +0,0 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import styles from "./page.module.css";
import { GhostButton, Notification, NotificationContainer, Spacer, Stack, Text, useBoolean, useNotifications } from 'ohmy-ui';
import useDatasets from '@/modules/ingestion/useDatasets';
import DataView, { Data } from '@/modules/ingestion/DataView';
import DatasetsView from '@/modules/ingestion/DatasetsView';
import classNames from 'classnames';
import addData from '@/modules/ingestion/addData';
import cognifyDataset from '@/modules/datasets/cognifyDataset';
import getDatasetData from '@/modules/datasets/getDatasetData';
import { Footer, SettingsModal } from '@/ui/Partials';
import { TextLogo } from '@/ui/App';
import { SettingsIcon } from '@/ui/Icons';
export default function Home() {
const {
datasets,
refreshDatasets,
} = useDatasets();
const [datasetData, setDatasetData] = useState<Data[]>([]);
const [selectedDataset, setSelectedDataset] = useState<string | null>(null);
useEffect(() => {
refreshDatasets();
}, [refreshDatasets]);
const openDatasetData = (dataset: { id: string }) => {
getDatasetData(dataset)
.then(setDatasetData)
.then(() => setSelectedDataset(dataset.id));
};
const closeDatasetData = () => {
setDatasetData([]);
setSelectedDataset(null);
};
const { notifications, showNotification } = useNotifications();
const onDataAdd = useCallback((dataset: { id: string }, files: File[]) => {
return addData(dataset, files)
.then(() => {
showNotification("Data added successfully. Please run \"Cognify\" when ready.", 5000);
openDatasetData(dataset);
});
}, [showNotification])
const onDatasetCognify = useCallback((dataset: { id: string, name: string }) => {
showNotification(`Cognification started for dataset "${dataset.name}".`, 5000);
return cognifyDataset(dataset)
.then(() => {
showNotification(`Dataset "${dataset.name}" cognified.`, 5000);
})
.catch(() => {
showNotification(`Dataset "${dataset.name}" cognification failed. Please try again.`, 5000);
});
}, [showNotification]);
const onCognify = useCallback(() => {
const dataset = datasets.find((dataset) => dataset.id === selectedDataset);
return onDatasetCognify({
id: dataset!.id,
name: dataset!.name,
});
}, [datasets, onDatasetCognify, selectedDataset]);
const {
value: isSettingsModalOpen,
setTrue: openSettingsModal,
setFalse: closeSettingsModal,
} = useBoolean(false);
return (
<main className={styles.main}>
<Spacer inset vertical="2" horizontal="2">
<Stack orientation="horizontal" gap="between" align="center">
<TextLogo width={158} height={44} color="white" />
<GhostButton hugContent onClick={openSettingsModal}>
<SettingsIcon />
</GhostButton>
</Stack>
</Spacer>
<SettingsModal isOpen={isSettingsModalOpen} onClose={closeSettingsModal} />
<Spacer inset vertical="1" horizontal="3">
<div className={styles.data}>
<div className={classNames(styles.datasetsView, {
[styles.openDatasetData]: datasetData.length > 0,
})}>
<DatasetsView
datasets={datasets}
onDatasetClick={openDatasetData}
onDatasetCognify={onDatasetCognify}
/>
</div>
{datasetData.length > 0 && selectedDataset && (
<div className={styles.dataView}>
<DataView
data={datasetData}
datasetId={selectedDataset}
onClose={closeDatasetData}
onDataAdd={onDataAdd}
onCognify={onCognify}
/>
</div>
)}
</div>
</Spacer>
<Spacer inset horizontal="3" wrap>
<Footer />
</Spacer>
<NotificationContainer gap="1" bottom right>
{notifications.map((notification, index: number) => (
<Notification
key={notification.id}
isOpen={notification.isOpen}
style={{ top: `${index * 60}px` }}
expireIn={notification.expireIn}
onClose={notification.delete}
>
<Text nowrap>{notification.message}</Text>
</Notification>
))}
</NotificationContainer>
</main>
);
}

View file

@ -1 +1,3 @@
export { default } from "./(graph)/GraphView";
export const dynamic = "force-dynamic";

View file

@ -1,8 +0,0 @@
.files {
width: 100%;
padding: 4px;
}
.fileSize {
display: block;
}

View file

@ -1,97 +0,0 @@
import { useCallback, useState } from 'react';
import { CTAButton, GhostButton, Stack, Text, TrashIcon, UploadIcon, UploadInput, useBoolean } from 'ohmy-ui';
import { Divider } from '@/ui/Layout';
import addData from '@/modules/ingestion/addData';
import { LoadingIndicator } from '@/ui/App';
import styles from './AddStep.module.css';
import { WizardHeading } from '@/ui/Partials/Wizard';
interface ConfigStepProps {
onNext: () => void;
}
export default function AddStep({ onNext }: ConfigStepProps) {
const [files, setFiles] = useState<File[]>([]);
const {
value: isUploading,
setTrue: disableUploading,
setFalse: enableUploading,
} = useBoolean(false);
const uploadFiles = useCallback(() => {
disableUploading()
addData({ name: 'main' }, files)
.then(() => {
onNext();
})
.finally(() => enableUploading());
}, [disableUploading, enableUploading, files, onNext]);
const addFiles = useCallback((files: File[]) => {
setFiles((existingFiles) => {
const newFiles = files.filter((file) => !existingFiles.some((existingFile) => existingFile.name === file.name));
return [...existingFiles, ...newFiles]
});
}, []);
const removeFile = useCallback((file: File) => {
setFiles((files) => files.filter((f) => f !== file));
}, []);
return (
<Stack orientation="vertical" gap="6">
<WizardHeading><Text light size="large">Step 2/3</Text> Add knowledge</WizardHeading>
<Divider />
<Text align="center">
Cognee lets you process your personal data, books, articles or company data.
Simply add datasets to get started.
</Text>
<Stack gap="1">
<UploadInput onChange={addFiles}>
<Stack gap="2" orientation="horizontal" align="center/center">
<UploadIcon key={files.length} />
<Text>Upload your data</Text>
</Stack>
</UploadInput>
<Stack gap="3" className={styles.files}>
{files.map((file, index) => (
<Stack gap="between" orientation="horizontal" align="center/" key={index}>
<div key={index}>
<Text bold>{file.name}</Text>
<Text className={styles.fileSize} size="small">
{getBiggestUnitSize(file.size)}
</Text>
</div>
<GhostButton hugContent onClick={() => removeFile(file)}>
<TrashIcon />
</GhostButton>
</Stack>
))}
</Stack>
</Stack>
<Stack align="/end">
<CTAButton disabled={isUploading || files.length === 0} onClick={uploadFiles}>
<Stack gap="2" orientation="horizontal" align="center/center">
<Text>Next</Text>
{isUploading && (
<LoadingIndicator />
)}
</Stack>
</CTAButton>
</Stack>
</Stack>
)
}
function getBiggestUnitSize(sizeInBytes: number): string {
const units = ['B', 'KB', 'MB', 'GB'];
let i = 0;
while (sizeInBytes >= 1024 && i < units.length - 1) {
sizeInBytes /= 1024;
i++;
}
return `${sizeInBytes.toFixed(2)} ${units[i]}`;
}

View file

@ -1 +0,0 @@
export { default } from './AddStep';

View file

@ -1,51 +0,0 @@
import { useEffect, useRef } from 'react';
import { CTAButton, Stack, Text, useBoolean } from 'ohmy-ui';
import { Divider } from '@/ui/Layout';
import { CognifyLoadingIndicator } from '@/ui/App';
import { WizardHeading } from '@/ui/Partials/Wizard';
import cognifyDataset from '@/modules/datasets/cognifyDataset';
interface ConfigStepProps {
onNext: () => void;
dataset: { name: string }
}
export default function CognifyStep({ onNext, dataset }: ConfigStepProps) {
const {
value: isCognifyRunning,
setFalse: stopCognifyIndicator,
} = useBoolean(true);
const cognifyPromise = useRef<Promise<void>>()
useEffect(() => {
if (cognifyPromise.current) {
return;
}
cognifyPromise.current = cognifyDataset(dataset)
.then(() => {
stopCognifyIndicator();
});
}, [stopCognifyIndicator, dataset]);
return (
<Stack orientation="vertical" gap="6">
<WizardHeading><Text light size="large">Step 3/3</Text> Cognify</WizardHeading>
<Divider />
<Stack align="/center">
<CognifyLoadingIndicator isLoading={isCognifyRunning} />
</Stack>
<Text align="center">
Cognee decomposes your data into facts and connects them in relevant clusters,
so that you can navigate your knowledge better.
</Text>
<CTAButton disabled={isCognifyRunning} onClick={onNext}>
<Stack gap="2" orientation="horizontal" align="center/center">
<Text>Explore data</Text>
</Stack>
</CTAButton>
</Stack>
)
}

View file

@ -1 +0,0 @@
export { default } from './CognifyStep';

View file

@ -1,22 +0,0 @@
import { Stack, Text } from 'ohmy-ui';
import { Divider } from '@/ui/Layout';
import Settings from '@/ui/Partials/SettingsModal/Settings';
import { WizardContent, WizardHeading } from '@/ui/Partials/Wizard';
interface ConfigStepProps {
onNext: () => void;
}
export default function ConfigStep({ onNext }: ConfigStepProps) {
return (
<Stack orientation="vertical" gap="6">
<WizardHeading><Text light size="large">Step 1/3</Text> Basic configuration</WizardHeading>
<Divider />
<Text align="center">
Cognee helps you process your data and create a mind-like structure you can explore.
To get started you need an OpenAI API key.
</Text>
<Settings onDone={onNext} submitButtonText="Next" />
</Stack>
)
}

View file

@ -1 +0,0 @@
export { default } from './ConfigStep';

View file

@ -1,14 +0,0 @@
import { Explorer } from '@/ui/Partials';
import { Spacer } from 'ohmy-ui';
interface ExploreStepProps {
dataset: { name: string };
}
export default function ExploreStep({ dataset }: ExploreStepProps) {
return (
<Spacer horizontal="3">
<Explorer dataset={dataset} />
</Spacer>
)
}

View file

@ -1 +0,0 @@
export { default } from './ExploreStep';

View file

@ -1,13 +0,0 @@
.main {
display: flex;
flex-direction: row;
flex-direction: column;
padding: 0;
min-height: 100vh;
}
.wizardContainer {
flex: 1;
display: flex;
padding: 24px 0;
}

View file

@ -1,83 +0,0 @@
import { useState } from 'react';
import { CloseIcon, GhostButton, Spacer, Stack, useBoolean } from 'ohmy-ui';
import { TextLogo } from '@/ui/App';
import { SettingsIcon } from '@/ui/Icons';
import { Footer, SettingsModal } from '@/ui/Partials';
import ConfigStep from './ConfigStep';
import AddStep from './AddStep';
import CognifyStep from './CognifyStep';
import ExploreStep from './ExploreStep';
import { WizardContent } from '@/ui/Partials/Wizard';
import styles from './WizardPage.module.css';
import { Divider } from '@/ui/Layout';
import { useSearchParams } from 'next/navigation';
interface WizardPageProps {
onFinish: () => void;
}
export default function WizardPage({
onFinish,
}: WizardPageProps) {
const searchParams = useSearchParams()
const presetWizardStep = searchParams.get('step') as 'config';
const [wizardStep, setWizardStep] = useState<'config' | 'add' | 'cognify' | 'explore'>(presetWizardStep || 'config');
const {
value: isSettingsModalOpen,
setTrue: openSettingsModal,
setFalse: closeSettingsModal,
} = useBoolean(false);
const dataset = { name: 'main' };
return (
<main className={styles.main}>
<Spacer inset vertical="2" horizontal="2">
<Stack orientation="horizontal" gap="between" align="center">
<TextLogo width={158} height={44} color="white" />
{wizardStep === 'explore' && (
<GhostButton hugContent onClick={onFinish}>
<CloseIcon />
</GhostButton>
)}
{wizardStep === 'add' && (
<GhostButton hugContent onClick={openSettingsModal}>
<SettingsIcon />
</GhostButton>
)}
</Stack>
</Spacer>
<Divider />
<SettingsModal isOpen={isSettingsModalOpen} onClose={closeSettingsModal} />
<div className={styles.wizardContainer}>
{wizardStep === 'config' && (
<WizardContent>
<ConfigStep onNext={() => setWizardStep('add')} />
</WizardContent>
)}
{wizardStep === 'add' && (
<WizardContent>
<AddStep onNext={() => setWizardStep('cognify')} />
</WizardContent>
)}
{wizardStep === 'cognify' && (
<WizardContent>
<CognifyStep dataset={dataset} onNext={() => setWizardStep('explore')} />
</WizardContent>
)}
{wizardStep === 'explore' && (
<Spacer inset top="4" bottom="1" horizontal="4">
<ExploreStep dataset={dataset} />
</Spacer>
)}
</div>
<Spacer inset horizontal="3" wrap>
<Footer />
</Spacer>
</main>
)
}

View file

@ -1,18 +0,0 @@
'use client';
import { Suspense, useCallback } from 'react';
import WizardPage from './WizardPage';
export default function Page() {
const finishWizard = useCallback(() => {
window.location.href = '/';
}, []);
return (
<Suspense>
<WizardPage
onFinish={finishWizard}
/>
</Suspense>
);
}

View file

@ -0,0 +1,29 @@
import { NextResponse, type NextRequest } from "next/server";
// import { auth0 } from "./modules/auth/auth0";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function middleware(request: NextRequest) {
// if (process.env.USE_AUTH0_AUTHORIZATION?.toLowerCase() === "true") {
// if (request.nextUrl.pathname === "/auth/token") {
// return NextResponse.next();
// }
// const response: NextResponse = await auth0.middleware(request);
// return response;
// }
return NextResponse.next();
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
*/
"/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
],
};

View file

@ -0,0 +1,8 @@
import { Auth0Client } from "@auth0/nextjs-auth0/server";
export const auth0 = new Auth0Client({
authorizationParameters: {
scope: "openid profile email",
audience: "cognee:api",
},
});

View file

@ -1,8 +1,8 @@
import { fetch } from '@/utils';
import { fetch } from "@/utils";
export default function getHistory() {
return fetch(
'/v1/search',
"/v1/search",
)
.then((response) => response.json());
}

View file

@ -0,0 +1,123 @@
import { v4 } from "uuid";
import { useCallback, useState } from "react";
import { fetch, useBoolean } from "@/utils";
import { Dataset } from "@/modules/ingestion/useDatasets";
interface ChatMessage {
id: string;
user: "user" | "system";
text: string;
}
const fetchMessages = () => {
return fetch("/v1/search/")
.then(response => response.json());
};
const sendMessage = (message: string, searchType: string) => {
return fetch("/v1/search/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query: message,
searchType,
datasets: ["main_dataset"],
}),
})
.then(response => response.json());
};
// Will be used in the future.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default function useChat(dataset: Dataset) {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const {
value: isSearchRunning,
setTrue: disableSearchRun,
setFalse: enableSearchRun,
} = useBoolean(false);
const refreshChat = useCallback(async () => {
const data = await fetchMessages();
return setMessages(data);
}, []);
const handleMessageSending = useCallback((message: string, searchType: string) => {
const sentMessageId = v4();
setMessages((messages) => [
...messages,
{
id: sentMessageId,
user: "user",
text: message,
},
]);
disableSearchRun();
return sendMessage(message, searchType)
.then(newMessages => {
setMessages((messages) => [
...messages,
...newMessages.map((newMessage: string | []) => ({
id: v4(),
user: "system",
text: convertToSearchTypeOutput(newMessage, searchType),
})),
]);
})
.catch(() => {
setMessages(
(messages) => messages.filter(message => message.id !== sentMessageId),
);
throw new Error("Failed to send message. Please try again. If the issue persists, please contact support.")
})
.finally(() => enableSearchRun());
}, [disableSearchRun, enableSearchRun]);
return {
messages,
refreshChat,
sendMessage: handleMessageSending,
isSearchRunning,
};
}
interface Node {
name: string;
}
interface Relationship {
relationship_name: string;
}
type InsightMessage = [Node, Relationship, Node];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function convertToSearchTypeOutput(systemMessage: any[] | any, searchType: string): string {
if (Array.isArray(systemMessage) && systemMessage.length === 1 && typeof(systemMessage[0]) === "string") {
return systemMessage[0];
}
switch (searchType) {
case "INSIGHTS":
return systemMessage.map((message: InsightMessage) => {
const [node1, relationship, node2] = message;
if (node1.name && node2.name) {
return `${node1.name} ${relationship.relationship_name} ${node2.name}.`;
}
return "";
}).join("\n");
case "SUMMARIES":
return systemMessage.map((message: { text: string }) => message.text).join("\n");
case "CHUNKS":
return systemMessage.map((message: { text: string }) => message.text).join("\n");
default:
return systemMessage;
}
}

View file

@ -1,43 +1,58 @@
import { fetch } from '@/utils';
import { fetch } from "@/utils";
import getDatasetGraph from "./getDatasetGraph";
import { Dataset } from "../ingestion/useDatasets";
export default function cognifyDataset(dataset: { id?: string, name?: string }, onUpdate = (data: []) => {}) {
return fetch('/v1/cognify', {
method: 'POST',
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) {
// const data = await (
return fetch("/v1/cognify", {
method: "POST",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
body: JSON.stringify({
datasets: [dataset.id || dataset.name],
datasetIds: [dataset.id],
runInBackground: false,
}),
})
.then((response) => response.json())
.then((data) => {
const websocket = new WebSocket(`ws://localhost:8000/api/v1/cognify/subscribe/${data.pipeline_run_id}`);
websocket.onopen = () => {
websocket.send(JSON.stringify({
"Authorization": `Bearer ${localStorage.getItem("access_token")}`,
}));
};
let isCognifyDone = false;
websocket.onmessage = (event) => {
const data = JSON.parse(event.data);
onUpdate(data);
if (data.status === "PipelineRunCompleted") {
isCognifyDone = true;
websocket.close();
}
};
return new Promise(async (resolve) => {
while (!isCognifyDone) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
resolve(true);
.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}`);
// let isCognifyDone = false;
// websocket.onmessage = (event) => {
// const data = JSON.parse(event.data);
// onUpdate?.({
// nodes: data.payload.nodes,
// edges: data.payload.edges,
// });
// if (data.status === "PipelineRunCompleted") {
// isCognifyDone = true;
// websocket.close();
// }
// };
// return new Promise(async (resolve) => {
// while (!isCognifyDone) {
// await new Promise(resolve => setTimeout(resolve, 1000));
// }
// resolve(true);
// });
}

View file

@ -0,0 +1,12 @@
import { fetch } from "@/utils";
export default function createDataset(dataset: { name: string }) {
return fetch(`/v1/datasets/`, {
method: "POST",
body: JSON.stringify(dataset),
headers: {
"Content-Type": "application/json",
}
})
.then((response) => response.json());
}

View file

@ -1,6 +1,6 @@
import { fetch } from '@/utils';
export default function getExplorationGraphUrl(dataset: { name: string }) {
export default function getExplorationGraphUrl(/* dataset: { name: string } */) {
return fetch('/v1/visualize')
.then(async (response) => {
if (response.status !== 200) {

View file

@ -1,24 +0,0 @@
.tableContainer {
overflow: auto;
padding-bottom: 32px;
min-height: 300px;
}
.datasetMenu {
background-color: var(--global-background-default);
border-radius: var(--border-radius);
padding: 4px;
}
.dataTable {
color: white;
border-collapse: collapse;
}
.dataTable td, .dataTable th {
vertical-align: top;
padding: 8px;
border: 1px solid white;
margin: 0;
white-space: nowrap;
}

View file

@ -1,143 +0,0 @@
import { useCallback, useState } from 'react';
import {
DropdownMenu,
GhostButton,
Stack,
Text,
UploadInput,
CloseIcon,
CTAButton,
useBoolean,
} from "ohmy-ui";
import { fetch } from '@/utils';
import RawDataPreview from './RawDataPreview';
import styles from "./DataView.module.css";
export interface Data {
id: string;
name: string;
mimeType: string;
extension: string;
rawDataLocation: string;
}
interface DatasetLike {
id: string;
}
interface DataViewProps {
data: Data[];
datasetId: string;
onClose: () => void;
onDataAdd: (dataset: DatasetLike, files: File[]) => void;
onCognify: () => Promise<any>;
}
export default function DataView({ datasetId, data, onClose, onDataAdd, onCognify }: DataViewProps) {
// const handleDataDelete = () => {};
const [rawData, setRawData] = useState<ArrayBuffer | null>(null);
const [selectedData, setSelectedData] = useState<Data | null>(null);
const showRawData = useCallback((dataItem: Data) => {
setSelectedData(dataItem);
fetch(`/v1/datasets/${datasetId}/data/${dataItem.id}/raw`)
.then((response) => response.arrayBuffer())
.then(setRawData);
document.body.click(); // Close the dropdown menu.
}, [datasetId]);
const resetDataPreview = useCallback(() => {
setSelectedData(null);
setRawData(null);
}, []);
const handleDataAdd = (files: File[]) => {
onDataAdd({ id: datasetId }, files);
};
const {
value: isCognifyButtonDisabled,
setTrue: disableCognifyButton,
setFalse: enableCognifyButton,
} = useBoolean(false);
const handleCognify = () => {
disableCognifyButton();
onCognify()
.finally(() => enableCognifyButton());
};
return (
<Stack orientation="vertical" gap="4">
<Stack gap="2" orientation="horizontal" align="/end">
<div>
<UploadInput onChange={handleDataAdd}>
<Text>Add data</Text>
</UploadInput>
</div>
<div>
<CTAButton disabled={isCognifyButtonDisabled} onClick={handleCognify}>
<Text>Cognify</Text>
</CTAButton>
</div>
<GhostButton hugContent onClick={onClose}>
<CloseIcon />
</GhostButton>
</Stack>
{rawData && selectedData && (
<RawDataPreview
fileName={selectedData.name}
rawData={rawData}
onClose={resetDataPreview}
/>
)}
<div className={styles.tableContainer}>
<table className={styles.dataTable}>
<thead>
<tr>
<th>Actions</th>
<th>ID</th>
<th>Name</th>
<th>File path</th>
<th>MIME type</th>
</tr>
</thead>
<tbody>
{data.map((dataItem) => (
<tr key={dataItem.id}>
<td>
<Stack orientation="horizontal" gap="2" align="center">
<DropdownMenu position="right">
<Stack gap="1" className={styles.datasetMenu} orientation="vertical">
<GhostButton onClick={() => showRawData(dataItem)}>
<Text>View raw data</Text>
</GhostButton>
{/* <NegativeButton onClick={handleDataDelete}>
<Text>Delete</Text>
</NegativeButton> */}
</Stack>
</DropdownMenu>
</Stack>
</td>
<td>
<Text>{dataItem.id}</Text>
</td>
<td>
<Text>{dataItem.name}.{dataItem.extension}</Text>
</td>
<td>
<Text>{dataItem.rawDataLocation}</Text>
</td>
<td>
<Text>{dataItem.mimeType}</Text>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Stack>
);
}

View file

@ -1,8 +0,0 @@
.dataPreviewModal {
left: 5% !important;
padding: 0 !important;
max-width: 90% !important;
height: 80%;
top: 5% !important;
}

View file

@ -1,37 +0,0 @@
import { IFrameView } from '@/ui/Partials';
import { CloseIcon, GhostButton, Modal, Spacer, Stack, Text } from 'ohmy-ui';
import styles from './RawDataPreview.module.css';
interface RawDataPreviewProps {
fileName: string;
rawData: ArrayBuffer;
onClose: () => void;
}
const file_header = ';headers=filename%3D';
export default function RawDataPreview({ fileName, rawData, onClose }: RawDataPreviewProps) {
const src = `data:application/pdf;base64,${arrayBufferToBase64(rawData)}`.replace(';', file_header + encodeURIComponent(fileName) + ';');
return (
<Modal isOpen onClose={onClose} className={styles.dataPreviewModal}>
<Spacer horizontal="2" vertical="3" wrap>
<Text>{fileName}</Text>
</Spacer>
<IFrameView src={src} />
</Modal>
);
}
function arrayBufferToBase64(buffer: ArrayBuffer): string {
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode( bytes[ i ] );
}
return window.btoa(binary);
}

View file

@ -1 +0,0 @@
export { default, type Data } from './DataView';

View file

@ -1,16 +0,0 @@
.datasetMenu {
background-color: var(--global-background-default);
border-radius: var(--border-radius);
padding: 4px;
}
.explorerModal {
left: 5% !important;
padding: 0 !important;
max-width: 90% !important;
height: 80%;
top: 5% !important;
display: flex;
flex-direction: column;
}

View file

@ -1,106 +0,0 @@
import { useState } from 'react';
import Link from 'next/link';
import { Explorer } from '@/ui/Partials';
import StatusIcon from '@/ui/elements/StatusIndicator';
import { LoadingIndicator } from '@/ui/App';
import { DropdownMenu, GhostButton, Stack, Text, CTAButton, useBoolean, Modal, Spacer } from "ohmy-ui";
import styles from "./DatasetsView.module.css";
interface Dataset {
id: string;
name: string;
status: string;
}
const DatasetItem = GhostButton.remix({ Component: 'div' });
interface DatasetsViewProps {
datasets: Dataset[];
onDatasetClick: (dataset: Dataset) => void;
onDatasetCognify: (dataset: Dataset) => Promise<void>;
}
export default function DatasetsView({
datasets,
onDatasetClick,
onDatasetCognify,
}: DatasetsViewProps) {
const {
value: isCognifyRunning,
setTrue: disableCognifyRun,
setFalse: enableCognifyRun,
} = useBoolean(false);
const handleCognifyDataset = (event: React.MouseEvent<HTMLButtonElement>, dataset: Dataset) => {
event.stopPropagation();
disableCognifyRun();
onDatasetCognify(dataset)
.finally(() => enableCognifyRun());
}
const [dataset, setExplorationDataset] = useState<{ id: string, name: string } | null>(null);
const {
value: isExplorationWindowShown,
setTrue: showExplorationWindow,
setFalse: hideExplorationWindow,
} = useBoolean(false);
const handleExploreDataset = (event: React.MouseEvent<HTMLButtonElement>, dataset: Dataset) => {
event.stopPropagation();
setExplorationDataset(dataset);
showExplorationWindow();
}
return (
<>
<Stack orientation="vertical" gap="4">
{datasets.map((dataset) => (
<DatasetItem key={dataset.id} onClick={() => onDatasetClick(dataset)}>
<Stack orientation="horizontal" gap="between" align="start/center">
<Text>{dataset.name}</Text>
<Stack orientation="horizontal" gap="2" align="center">
<StatusIcon status={dataset.status} />
<DropdownMenu>
<Stack gap="1" className={styles.datasetMenu} orientation="vertical">
{dataset.status === 'DATASET_PROCESSING_COMPLETED' ? (
<CTAButton
onClick={(event: React.MouseEvent<HTMLButtonElement>) => handleExploreDataset(event, dataset)}
>
<Text>Explore</Text>
</CTAButton>
) : (
<CTAButton
onClick={(event: React.MouseEvent<HTMLButtonElement>) => handleCognifyDataset(event, dataset)}
>
<Stack gap="2" orientation="horizontal" align="center/center">
<Text>Cognify</Text>
{isCognifyRunning && (
<LoadingIndicator />
)}
</Stack>
</CTAButton>
)}
<Link href="/wizard?step=add">
<GhostButton>
<Text>Add data</Text>
</GhostButton>
</Link>
</Stack>
</DropdownMenu>
</Stack>
</Stack>
</DatasetItem>
))}
</Stack>
<Modal closeOnBackdropClick={false} onClose={hideExplorationWindow} isOpen={isExplorationWindowShown} className={styles.explorerModal}>
<Spacer horizontal="2" vertical="3" wrap>
<Text>{dataset?.name}</Text>
</Spacer>
<Explorer dataset={dataset!} />
</Modal>
</>
);
}

View file

@ -1 +0,0 @@
export { default } from './DatasetsView';

View file

@ -1,19 +1,19 @@
import { fetch } from '@/utils';
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);
formData.append("data", file, file.name);
})
if (dataset.id) {
formData.append('datasetId', dataset.id);
formData.append("datasetId", dataset.id);
}
if (dataset.name) {
formData.append('datasetName', dataset.name);
formData.append("datasetName", dataset.name);
}
return fetch('/v1/add', {
method: 'POST',
return fetch("/v1/add", {
method: "POST",
body: formData,
}).then((response) => response.json());
}

View file

@ -12,16 +12,12 @@ export interface Dataset {
function useDatasets() {
const [datasets, setDatasets] = useState<Dataset[]>([]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const statusTimeout = useRef<any>(null);
const fetchDatasetStatuses = useCallback((datasets: Dataset[]) => {
fetch(
`/v1/datasets/status?dataset=${datasets.map(d => d.id).join('&dataset=')}`,
{
headers: {
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
},
},
)
.then((response) => response.json())
.then((statuses) => setDatasets(
@ -42,7 +38,7 @@ function useDatasets() {
statusTimeout.current = setTimeout(() => {
checkDatasetStatuses(datasets);
}, 5000);
}, 50000);
}, [fetchDatasetStatuses]);
useEffect(() => {
@ -73,11 +69,7 @@ function useDatasets() {
}, []);
const fetchDatasets = useCallback(() => {
return fetch('/v1/datasets', {
headers: {
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
},
})
return fetch('/v1/datasets')
.then((response) => response.json())
.then((datasets) => {
setDatasets(datasets);

View file

@ -0,0 +1,7 @@
export default function SearchIcon({ width = 24, height = 24, color = 'currentColor', className = '' }) {
return (
<svg width={width} height={height} viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M24.9999 46L24.9999 4M46.0049 25.005L4.00488 25.005" stroke={color} strokeWidth="8" strokeLinecap="round"/>
</svg>
);
}

View file

@ -0,0 +1,8 @@
export default function CaretIcon({ width = 50, height = 36, color = "currentColor", className = "" }) {
return (
<svg width={width} height={height} viewBox="0 0 50 36" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M4 32L25 5" stroke={color} strokeWidth="8" strokeLinecap="round"/>
<path d="M46 32L25 5" stroke={color} strokeWidth="8" strokeLinecap="round"/>
</svg>
);
}

View file

@ -0,0 +1,9 @@
export default function SearchIcon({ width = 24, height = 24, color = 'currentColor', className = '' }) {
return (
<svg width={width} height={height} viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<circle cx="19.5" cy="19.5" r="17" stroke={color} strokeWidth="5"/>
<path d="M8 19.5C8 13.1487 13.1487 8 19.5 8" stroke={color}/>
<path d="M43.2782 48.9312C44.897 50.4344 47.428 50.3406 48.9312 48.7218C50.4344 47.103 50.3406 44.572 48.7218 43.0688L43.2782 48.9312ZM46 46L48.7218 43.0688L34.7218 30.0688L32 33L29.2782 35.9312L43.2782 48.9312L46 46Z" fill={color}/>
</svg>
);
}

View file

@ -1,3 +1,6 @@
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';

View file

@ -1,21 +0,0 @@
.explorer {
flex: 1;
min-height: 100%;
flex-direction: column;
}
.explorerContent {
flex: 1;
}
.graphExplorer {
width: 65%;
overflow: hidden;
border-radius: var(--border-radius);
}
.chat {
width: 35%;
display: flex;
}

View file

@ -1,61 +0,0 @@
import classNames from 'classnames';
import { useCallback, useEffect, useState } from 'react';
import { Spacer, Stack, Text } from 'ohmy-ui';
import { LoadingIndicator } from '@/ui/App';
import { IFrameView, SearchView } from '@/ui/Partials';
import { getExplorationGraphUrl } from '@/modules/exploration';
import styles from './Explorer.module.css';
interface ExplorerProps {
dataset: { name: string };
className?: string;
style?: React.CSSProperties;
}
export default function Explorer({ dataset, className, style }: ExplorerProps) {
const [error, setError] = useState<Error | null>(null);
const [graphHtml, setGraphHtml] = useState<string | null>(null);
const exploreData = useCallback(() => {
getExplorationGraphUrl(dataset)
.then((graphHtml) => {
setError(null);
setGraphHtml(graphHtml);
})
.catch((error) => {
setError(error);
});
}, [dataset]);
useEffect(() => {
exploreData();
}, [exploreData]);
return (
<Stack
gap="6"
style={style}
orientation="horizontal"
className={classNames(styles.explorerContent, className)}
>
<div className={styles.graphExplorer}>
{error ? (
<Text color="red">{error.message}</Text>
) : (
<>
{!graphHtml ? (
<Spacer horizontal="2" wrap>
<LoadingIndicator />
</Spacer>
) : (
<IFrameView src="http://127.0.0.1:8000/api/v1/visualize" />
)}
</>
)}
</div>
<div className={styles.chat}>
<SearchView />
</div>
</Stack>
)
}

View file

@ -26,19 +26,22 @@ export default function FeedbackForm({ onSuccess }: FeedbackFormProps) {
event.preventDefault();
const formElements = event.currentTarget;
const authCredentials = new FormData();
authCredentials.append("feedback", formElements.feedback.value);
setFeedbackError(null);
disableFeedbackSubmit();
fetch("/v1/feedback/reasoning", {
fetch("/v1/crewai/feedback", {
method: "POST",
body: authCredentials,
body: JSON.stringify({
feedback: formElements.feedback.value,
}),
headers: {
"Content-Type": "application/json",
},
})
.then(response => response.json())
.then(() => {
onSuccess();
formElements.feedback.value = "";
})
.catch(error => setFeedbackError(error.detail))
.finally(() => enableFeedbackSubmit());
@ -48,7 +51,7 @@ export default function FeedbackForm({ onSuccess }: FeedbackFormProps) {
<form onSubmit={signIn} className="flex flex-col gap-2">
<div className="flex flex-col gap-2">
<div className="mb-4">
<label className="block text-white" htmlFor="feedback">Your feedback on agents reasoning</label>
<label className="block text-white" htmlFor="feedback">Feedback on agent&apos;s reasoning</label>
<TextArea id="feedback" name="feedback" type="text" placeholder="Your feedback" />
</div>
</div>

View file

@ -1,29 +1,7 @@
.searchViewContainer {
flex: 1;
padding: 16px;
overflow: hidden;
border: 1px solid white;
background: var(--global-background-default);
border-radius: var(--border-radius);
.userMessage + .systemMessage {
margin-top: 2rem;
}
.messagesContainer {
flex-basis: 400px;
flex-grow: 1;
overflow-y: auto;
}
.messages {
padding-top: 24px;
padding-bottom: 24px;
}
.message {
padding: 16px;
border-radius: var(--border-radius);
}
.userMessage {
align-self: flex-end;
background-color: #5858ff;
.systemMessage + .userMessage {
margin-top: 2rem;
}

View file

@ -1,209 +1,145 @@
'use client';
"use client";
import { v4 } from 'uuid';
import classNames from 'classnames';
import { useCallback, useEffect, useState } from 'react';
import { CTAButton, Stack, Text, DropdownSelect, TextArea, useBoolean, Input } from 'ohmy-ui';
import { fetch } from '@/utils';
import styles from './SearchView.module.css';
import getHistory from '@/modules/chat/getHistory';
import classNames from "classnames";
import { useCallback, useEffect, useRef, useState } from "react";
interface Message {
id: string;
user: 'user' | 'system';
text: any;
}
import { LoadingIndicator } from "@/ui/App";
import { CTAButton, Select, TextArea } from "@/ui/elements";
import useChat from "@/modules/chat/hooks/useChat";
import styles from "./SearchView.module.css";
interface SelectOption {
value: string;
label: string;
}
interface SearchFormPayload extends HTMLFormElement {
chatInput: HTMLInputElement;
}
const MAIN_DATASET = {
id: "",
data: [],
status: "",
name: "main_dataset",
};
export default function SearchView() {
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState<string>("");
const handleInputChange = useCallback((event: React.ChangeEvent<HTMLTextAreaElement>) => {
setInputValue(event.target.value);
}, []);
const searchOptions = [{
value: 'GRAPH_COMPLETION',
label: 'Completion using Cognee\'s graph based memory',
const searchOptions: SelectOption[] = [{
value: "GRAPH_COMPLETION",
label: "GraphRAG Completion",
}, {
value: 'RAG_COMPLETION',
label: 'Completion using RAG',
}, {
value: 'GRAPH_COMPLETION_COT',
label: 'Cognee\'s Chain of Thought search',
}, {
value: 'GRAPH_COMPLETION_CONTEXT_EXTENSION',
label: 'Cognee\'s Multi-Hop search',
value: "RAG_COMPLETION",
label: "RAG Completion",
}];
const [searchType, setSearchType] = useState(searchOptions[0]);
const [rangeValue, setRangeValue] = useState(10);
const scrollToBottom = useCallback(() => {
setTimeout(() => {
const messagesContainerElement = document.getElementById('messages');
const messagesContainerElement = document.getElementById("messages");
if (messagesContainerElement) {
const messagesElements = messagesContainerElement.children[0];
if (messagesElements) {
messagesContainerElement.scrollTo({
top: messagesElements.scrollHeight,
behavior: 'smooth',
behavior: "smooth",
});
}
}
}, 300);
}, []);
useEffect(() => {
getHistory()
.then((history) => {
setMessages(history);
scrollToBottom();
});
}, [scrollToBottom]);
// Hardcoded to `main_dataset` for now, change when multiple datasets are supported.
const { messages, refreshChat, sendMessage, isSearchRunning } = useChat(MAIN_DATASET);
const handleSearchSubmit = useCallback((event: React.FormEvent<HTMLFormElement>) => {
useEffect(() => {
refreshChat()
.then(() => scrollToBottom());
}, [refreshChat, scrollToBottom]);
const [searchInputValue, setSearchInputValue] = useState("");
const handleSearchInputChange = useCallback((value: string) => {
setSearchInputValue(value);
}, []);
const handleChatMessageSubmit = useCallback((event: React.FormEvent<SearchFormPayload>) => {
event.preventDefault();
if (inputValue.trim() === '') {
const formElements = event.currentTarget;
const searchType = formElements.searchType.value;
const chatInput = searchInputValue.trim();
if (chatInput === "") {
return;
}
setMessages((currentMessages) => [
...currentMessages,
{
id: v4(),
user: 'user',
text: inputValue,
},
]);
scrollToBottom();
setInputValue('');
setSearchInputValue("");
const searchTypeValue = searchType.value;
sendMessage(chatInput, searchType)
.then(scrollToBottom)
}, [scrollToBottom, sendMessage, searchInputValue]);
fetch('/v1/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: inputValue.trim(),
searchType: searchTypeValue,
topK: rangeValue,
}),
})
.then((response) => response.json())
.then((systemMessage) => {
setMessages((currentMessages) => [
...currentMessages,
{
id: v4(),
user: 'system',
text: convertToSearchTypeOutput(systemMessage, searchTypeValue),
},
]);
scrollToBottom();
})
.catch(() => {
setInputValue(inputValue);
});
}, [inputValue, rangeValue, scrollToBottom, searchType.value]);
const {
value: isInputExpanded,
setTrue: expandInput,
setFalse: contractInput,
} = useBoolean(false);
const chatFormRef = useRef<HTMLFormElement>(null);
const handleSubmitOnEnter = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
handleSearchSubmit(event as unknown as React.FormEvent<HTMLFormElement>);
if (event.key === "Enter" && !event.shiftKey) {
chatFormRef.current?.requestSubmit();
}
};
const handleRangeValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setRangeValue(parseInt(event.target.value));
};
return (
<Stack className={styles.searchViewContainer}>
<DropdownSelect<SelectOption>
value={searchType}
options={searchOptions}
onChange={setSearchType}
/>
<div className={styles.messagesContainer} id="messages">
<Stack gap="2" className={styles.messages} align="end">
{messages.map((message) => (
<Text
key={message.id}
className={classNames(styles.message, {
[styles.userMessage]: message.user === "user",
})}
>
{message?.text && (
typeof(message.text) == "string" ? message.text : JSON.stringify(message.text)
)}
</Text>
))}
</Stack>
</div>
<form onSubmit={handleSearchSubmit}>
<Stack orientation="vertical" gap="2">
<TextArea onKeyUp={handleSubmitOnEnter} style={{ transition: 'height 0.3s ease', height: isInputExpanded ? '128px' : '38px' }} onFocus={expandInput} onBlur={contractInput} value={inputValue} onChange={handleInputChange} name="searchInput" placeholder="Search" />
<Stack orientation="horizontal" gap="between">
<Stack orientation="horizontal" gap="2" align="center">
<label><Text>Search range: </Text></label>
<Input style={{ maxWidth: "90px" }} value={rangeValue} onChange={handleRangeValueChange} type="number" />
</Stack>
<CTAButton hugContent type="submit">Search</CTAButton>
</Stack>
</Stack>
<div className="flex flex-col h-full bg-gray-500 p-6 pt-16 rounded-3xl border-indigo-600 border-2 shadow-xl">
<form onSubmit={handleChatMessageSubmit} ref={chatFormRef} className="flex flex-col gap-4 h-full">
<div className="h-full overflow-y-auto" id="messages">
<div className="flex flex-col gap-2 items-end justify-end min-h-full px-6 pb-4">
{messages.map((message) => (
<p
key={message.id}
className={classNames({
[classNames("ml-12 px-6 py-4 bg-gray-300 rounded-xl", styles.userMessage)]: message.user === "user",
[classNames("text-gray-200", styles.systemMessage)]: message.user !== "user",
})}
>
{message?.text && (
typeof(message.text) == "string" ? message.text : JSON.stringify(message.text)
)}
</p>
))}
</div>
</div>
<div className="p-4 bg-gray-300 rounded-xl flex flex-col gap-2">
<TextArea
value={searchInputValue}
onChange={handleSearchInputChange}
onKeyUp={handleSubmitOnEnter}
isAutoExpanding
name="chatInput"
placeholder="Ask anything"
contentEditable={true}
className="resize-none min-h-14 max-h-96 overflow-y-auto"
/>
<div className="flex flex-row items-center justify-between gap-4">
<div className="flex flex-row items-center gap-2">
<label className="text-gray-600 whitespace-nowrap">Search type:</label>
<Select name="searchType" defaultValue={searchOptions[0].value} className="max-w-2xs">
{searchOptions.map((option) => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</Select>
</div>
<CTAButton disabled={isSearchRunning} type="submit">
{isSearchRunning? "Searching..." : "Search"}
{isSearchRunning && <LoadingIndicator />}
</CTAButton>
</div>
</div>
</form>
</Stack>
</div>
);
}
interface Node {
name: string;
}
interface Relationship {
relationship_name: string;
}
type InsightMessage = [Node, Relationship, Node];
function convertToSearchTypeOutput(systemMessages: any[], searchType: string): string {
if (systemMessages.length > 0 && typeof(systemMessages[0]) === "string") {
return systemMessages[0];
}
switch (searchType) {
case 'INSIGHTS':
return systemMessages.map((message: InsightMessage) => {
const [node1, relationship, node2] = message;
if (node1.name && node2.name) {
return `${node1.name} ${relationship.relationship_name} ${node2.name}.`;
}
return '';
}).join('\n');
case 'SUMMARIES':
return systemMessages.map((message: { text: string }) => message.text).join('\n');
case 'CHUNKS':
return systemMessages.map((message: { text: string }) => message.text).join('\n');
default:
return "";
}
}

View file

@ -1,200 +1,200 @@
import { useCallback, useEffect, useState } from 'react';
import {
CTAButton,
DropdownSelect,
FormGroup,
FormInput,
FormLabel,
Input,
Spacer,
Stack,
useBoolean,
} from 'ohmy-ui';
import { LoadingIndicator } from '@/ui/App';
import { fetch } from '@/utils';
// import { useCallback, useEffect, useState } from 'react';
// import {
// CTAButton,
// DropdownSelect,
// FormGroup,
// FormInput,
// FormLabel,
// Input,
// Spacer,
// Stack,
// useBoolean,
// } from 'ohmy-ui';
// import { LoadingIndicator } from '@/ui/App';
// import { fetch } from '@/utils';
interface SelectOption {
label: string;
value: string;
}
// interface SelectOption {
// label: string;
// value: string;
// }
interface SettingsForm extends HTMLFormElement {
vectorDBUrl: HTMLInputElement;
vectorDBApiKey: HTMLInputElement;
llmProvider: HTMLInputElement;
llmModel: HTMLInputElement;
llmApiKey: HTMLInputElement;
llmEndpoint: HTMLInputElement;
llmApiVersion: HTMLInputElement;
}
// interface SettingsForm extends HTMLFormElement {
// vectorDBUrl: HTMLInputElement;
// vectorDBApiKey: HTMLInputElement;
// llmProvider: HTMLInputElement;
// llmModel: HTMLInputElement;
// llmApiKey: HTMLInputElement;
// llmEndpoint: HTMLInputElement;
// llmApiVersion: HTMLInputElement;
// }
const defaultProvider = {
label: 'OpenAI',
value: 'openai',
};
// const defaultProvider = {
// label: 'OpenAI',
// value: 'openai',
// };
const defaultModel = {
label: 'gpt-4o-mini',
value: 'gpt-4o-mini',
};
// const defaultModel = {
// label: 'gpt-4o-mini',
// value: 'gpt-4o-mini',
// };
export default function Settings({ onDone = () => {}, submitButtonText = 'Save' }) {
const [llmConfig, setLLMConfig] = useState<{
apiKey: string;
model: string;
endpoint: string;
apiVersion: string;
provider: string;
}>();
const [vectorDBConfig, setVectorDBConfig] = useState<{
url: string;
apiKey: string;
provider: SelectOption;
providers: SelectOption[];
}>();
// export default function Settings({ onDone = () => {}, submitButtonText = 'Save' }) {
// const [llmConfig, setLLMConfig] = useState<{
// apiKey: string;
// model: string;
// endpoint: string;
// apiVersion: string;
// provider: string;
// }>();
// const [vectorDBConfig, setVectorDBConfig] = useState<{
// url: string;
// apiKey: string;
// provider: SelectOption;
// providers: SelectOption[];
// }>();
const {
value: isSaving,
setTrue: startSaving,
setFalse: stopSaving,
} = useBoolean(false);
// const {
// value: isSaving,
// setTrue: startSaving,
// setFalse: stopSaving,
// } = useBoolean(false);
const saveConfig = (event: React.FormEvent<SettingsForm>) => {
event.preventDefault();
const formElements = event.currentTarget;
// const saveConfig = (event: React.FormEvent<SettingsForm>) => {
// event.preventDefault();
// const formElements = event.currentTarget;
const newVectorConfig = {
provider: vectorDBConfig?.provider.value,
url: formElements.vectorDBUrl.value,
apiKey: formElements.vectorDBApiKey.value,
};
// const newVectorConfig = {
// provider: vectorDBConfig?.provider.value,
// url: formElements.vectorDBUrl.value,
// apiKey: formElements.vectorDBApiKey.value,
// };
const newLLMConfig = {
provider: formElements.llmProvider.value,
model: formElements.llmModel.value,
apiKey: formElements.llmApiKey.value,
endpoint: formElements.llmEndpoint.value,
apiVersion: formElements.llmApiVersion.value,
};
// const newLLMConfig = {
// provider: formElements.llmProvider.value,
// model: formElements.llmModel.value,
// apiKey: formElements.llmApiKey.value,
// endpoint: formElements.llmEndpoint.value,
// apiVersion: formElements.llmApiVersion.value,
// };
startSaving();
// startSaving();
fetch('/v1/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
llm: newLLMConfig,
vectorDb: newVectorConfig,
}),
})
.then(() => {
onDone();
})
.finally(() => stopSaving());
};
// fetch('/v1/settings', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify({
// llm: newLLMConfig,
// vectorDb: newVectorConfig,
// }),
// })
// .then(() => {
// onDone();
// })
// .finally(() => stopSaving());
// };
const handleVectorDBChange = useCallback((newVectorDBProvider: SelectOption) => {
setVectorDBConfig((config) => {
if (config?.provider !== newVectorDBProvider) {
return {
...config,
providers: config?.providers || [],
provider: newVectorDBProvider,
url: '',
apiKey: '',
};
}
return config;
});
}, []);
// const handleVectorDBChange = useCallback((newVectorDBProvider: SelectOption) => {
// setVectorDBConfig((config) => {
// if (config?.provider !== newVectorDBProvider) {
// return {
// ...config,
// providers: config?.providers || [],
// provider: newVectorDBProvider,
// url: '',
// apiKey: '',
// };
// }
// return config;
// });
// }, []);
useEffect(() => {
const fetchConfig = async () => {
const response = await fetch('/v1/settings');
const settings = await response.json();
// useEffect(() => {
// const fetchConfig = async () => {
// const response = await fetch('/v1/settings');
// const settings = await response.json();
if (!settings.llm.provider) {
settings.llm.provider = settings.llm.providers[0].value;
}
if (!settings.llm.model) {
settings.llm.model = settings.llm.models[settings.llm.provider][0].value;
}
if (!settings.vectorDb.provider) {
settings.vectorDb.provider = settings.vectorDb.providers[0];
} else {
settings.vectorDb.provider = settings.vectorDb.providers.find((provider: SelectOption) => provider.value === settings.vectorDb.provider);
}
setLLMConfig(settings.llm);
setVectorDBConfig(settings.vectorDb);
};
fetchConfig();
}, []);
// if (!settings.llm.provider) {
// settings.llm.provider = settings.llm.providers[0].value;
// }
// if (!settings.llm.model) {
// settings.llm.model = settings.llm.models[settings.llm.provider][0].value;
// }
// if (!settings.vectorDb.provider) {
// settings.vectorDb.provider = settings.vectorDb.providers[0];
// } else {
// settings.vectorDb.provider = settings.vectorDb.providers.find((provider: SelectOption) => provider.value === settings.vectorDb.provider);
// }
// setLLMConfig(settings.llm);
// setVectorDBConfig(settings.vectorDb);
// };
// fetchConfig();
// }, []);
return (
<form onSubmit={saveConfig} style={{ width: "100%", overflowY: "auto", maxHeight: "500px" }}>
<Stack gap="4" orientation="vertical">
<Stack gap="4" orientation="vertical">
<FormGroup orientation="vertical" align="center/" gap="2">
<FormLabel>LLM provider:</FormLabel>
<FormInput>
<Input defaultValue={llmConfig?.provider} name="llmProvider" placeholder="LLM provider" />
</FormInput>
</FormGroup>
<FormGroup orientation="vertical" align="center/" gap="2">
<FormLabel>LLM model:</FormLabel>
<FormInput>
<Input defaultValue={llmConfig?.model} name="llmModel" placeholder="LLM model" />
</FormInput>
</FormGroup>
<FormGroup orientation="vertical" align="center/" gap="2">
<FormLabel>LLM endpoint:</FormLabel>
<FormInput>
<Input defaultValue={llmConfig?.endpoint} name="llmEndpoint" placeholder="LLM endpoint url" />
</FormInput>
</FormGroup>
<FormGroup orientation="vertical" align="center/" gap="2">
<FormLabel>LLM API key:</FormLabel>
<FormInput>
<Input defaultValue={llmConfig?.apiKey} name="llmApiKey" placeholder="LLM API key" />
</FormInput>
</FormGroup>
<FormGroup orientation="vertical" align="center/" gap="2">
<FormLabel>LLM API version:</FormLabel>
<FormInput>
<Input defaultValue={llmConfig?.apiVersion} name="llmApiVersion" placeholder="LLM API version" />
</FormInput>
</FormGroup>
</Stack>
// return (
// <form onSubmit={saveConfig} style={{ width: "100%", overflowY: "auto", maxHeight: "500px" }}>
// <Stack gap="4" orientation="vertical">
// <Stack gap="4" orientation="vertical">
// <FormGroup orientation="vertical" align="center/" gap="2">
// <FormLabel>LLM provider:</FormLabel>
// <FormInput>
// <Input defaultValue={llmConfig?.provider} name="llmProvider" placeholder="LLM provider" />
// </FormInput>
// </FormGroup>
// <FormGroup orientation="vertical" align="center/" gap="2">
// <FormLabel>LLM model:</FormLabel>
// <FormInput>
// <Input defaultValue={llmConfig?.model} name="llmModel" placeholder="LLM model" />
// </FormInput>
// </FormGroup>
// <FormGroup orientation="vertical" align="center/" gap="2">
// <FormLabel>LLM endpoint:</FormLabel>
// <FormInput>
// <Input defaultValue={llmConfig?.endpoint} name="llmEndpoint" placeholder="LLM endpoint url" />
// </FormInput>
// </FormGroup>
// <FormGroup orientation="vertical" align="center/" gap="2">
// <FormLabel>LLM API key:</FormLabel>
// <FormInput>
// <Input defaultValue={llmConfig?.apiKey} name="llmApiKey" placeholder="LLM API key" />
// </FormInput>
// </FormGroup>
// <FormGroup orientation="vertical" align="center/" gap="2">
// <FormLabel>LLM API version:</FormLabel>
// <FormInput>
// <Input defaultValue={llmConfig?.apiVersion} name="llmApiVersion" placeholder="LLM API version" />
// </FormInput>
// </FormGroup>
// </Stack>
<Stack gap="2" orientation="vertical">
<FormGroup orientation="vertical" align="center/" gap="2">
<FormLabel>Vector DB provider:</FormLabel>
<DropdownSelect
value={vectorDBConfig?.provider || null}
options={vectorDBConfig?.providers || []}
onChange={handleVectorDBChange}
/>
</FormGroup>
<FormInput>
<Input defaultValue={vectorDBConfig?.url} name="vectorDBUrl" placeholder="Vector DB instance url" />
</FormInput>
<FormInput>
<Input defaultValue={vectorDBConfig?.apiKey} name="vectorDBApiKey" placeholder="Vector DB API key" />
</FormInput>
<Stack align="/end">
<Spacer top="2">
<CTAButton type="submit">
<Stack gap="2" orientation="vertical" align="center/">
{submitButtonText}
{isSaving && <LoadingIndicator />}
</Stack>
</CTAButton>
</Spacer>
</Stack>
</Stack>
</Stack>
</form>
)
}
// <Stack gap="2" orientation="vertical">
// <FormGroup orientation="vertical" align="center/" gap="2">
// <FormLabel>Vector DB provider:</FormLabel>
// <DropdownSelect
// value={vectorDBConfig?.provider || null}
// options={vectorDBConfig?.providers || []}
// onChange={handleVectorDBChange}
// />
// </FormGroup>
// <FormInput>
// <Input defaultValue={vectorDBConfig?.url} name="vectorDBUrl" placeholder="Vector DB instance url" />
// </FormInput>
// <FormInput>
// <Input defaultValue={vectorDBConfig?.apiKey} name="vectorDBApiKey" placeholder="Vector DB API key" />
// </FormInput>
// <Stack align="/end">
// <Spacer top="2">
// <CTAButton type="submit">
// <Stack gap="2" orientation="vertical" align="center/">
// {submitButtonText}
// {isSaving && <LoadingIndicator />}
// </Stack>
// </CTAButton>
// </Spacer>
// </Stack>
// </Stack>
// </Stack>
// </form>
// )
// }

View file

@ -1,10 +1,10 @@
import { Modal } from 'ohmy-ui';
import Settings from './Settings';
// import { Modal } from 'ohmy-ui';
// import Settings from './Settings';
export default function SettingsModal({ isOpen = false, onClose = () => {} }) {
return (
<Modal isOpen={isOpen} onClose={onClose}>
<Settings onDone={onClose} />
</Modal>
)
}
// export default function SettingsModal({ isOpen = false, onClose = () => {} }) {
// return (
// <Modal isOpen={isOpen} onClose={onClose}>
// <Settings onDone={onClose} />
// </Modal>
// )
// }

View file

@ -40,12 +40,10 @@ export default function SignInForm({ onSignInSuccess = () => window.location.hre
method: "POST",
body: authCredentials,
})
.then(response => response.json())
.then((bearer) => {
window.localStorage.setItem("access_token", bearer.access_token);
.then(() => {
onSignInSuccess();
})
.catch(error => setSignInError(errorsMap[error.detail as keyof typeof errorsMap]))
.catch(error => setSignInError(errorsMap[error.detail as keyof typeof errorsMap] || error.message))
.finally(() => enableSignIn());
};

View file

@ -1,24 +0,0 @@
.wizardContent {
width: 100%;
max-width: 400px;
height: max-content;
background: linear-gradient(90deg, #6510F4 0.52%, #0DFF00 103.83%);
padding: 24px;
margin: 0 auto;
position: relative;
}
.wizardContent::before {
content: '';
width: calc(100% - 6px);
height: calc(100% - 6px);
max-width: 394px;
position: absolute;
top: 3px;
left: 3px;
background-color: #351A4B;
}
.wizardContent > * {
position: relative;
}

View file

@ -1,6 +0,0 @@
import { withStyles } from 'ohmy-ui';
import styles from './WizardContent.module.css';
const WizardContent = withStyles<{ children: React.ReactNode }>('div', { className: styles.wizardContent });
export default WizardContent;

View file

@ -1,11 +0,0 @@
import { H1 } from 'ohmy-ui';
interface WizardHeadingProps {
children: React.ReactNode;
}
export default function WizardHeading({ children, ...props }: WizardHeadingProps) {
return (
<H1 {...props} align="center" size="small" style={{ color: '#40A9FF' }}>{children}</H1>
);
}

View file

@ -1,2 +0,0 @@
export { default as WizardHeading } from './WizardHeading';
export { default as WizardContent } from './WizardContent/WizardContent';

View file

@ -1,6 +1,6 @@
export { default as Footer } from "./Footer/Footer";
export { default as SettingsModal } from "./SettingsModal/SettingsModal";
// export { default as SettingsModal } from "./SettingsModal/SettingsModal";
export { default as SearchView } from "./SearchView/SearchView";
export { default as IFrameView } from "./IFrameView/IFrameView";
export { default as Explorer } from "./Explorer/Explorer";
// export { default as Explorer } from "./Explorer/Explorer";
export { default as FeedbackForm } from "./FeedbackForm";

View file

@ -3,6 +3,6 @@ import { ButtonHTMLAttributes } from "react";
export default function CTAButton({ children, className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button className={classNames("flex flex-row justify-center items-center gap-2 cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600", className)} {...props}>{children}</button>
<button className={classNames("flex flex-row justify-center items-center gap-2 cursor-pointer rounded-3xl bg-indigo-600 px-4 py-3 text-white hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600", className)} {...props}>{children}</button>
);
}

View file

@ -0,0 +1,8 @@
import classNames from 'classnames';
import { ButtonHTMLAttributes } from "react";
export default function CTAButton({ children, className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button className={classNames("flex flex-row justify-center items-center gap-2 cursor-pointer rounded-3xl bg-transparent px-4 py-3 text-white shadow-xs border-1 hover:bg-gray-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600", className)} {...props}>{children}</button>
);
}

View file

@ -3,6 +3,6 @@ import { InputHTMLAttributes } from "react"
export default function Input({ className, ...props }: InputHTMLAttributes<HTMLInputElement>) {
return (
<input className={classNames("block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6", className)} {...props} />
<input className={classNames("block w-full rounded-md bg-white px-4 py-4 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600", className)} {...props} />
)
}

View file

@ -0,0 +1,12 @@
interface ModalProps {
isOpen: boolean;
children: React.ReactNode;
}
export default function Modal({ isOpen, children }: ModalProps) {
return isOpen && (
<div className="fixed top-0 left-0 right-0 bottom-0 backdrop-blur-lg z-1 flex items-center justify-center">
{children}
</div>
);
}

View file

@ -3,6 +3,6 @@ import { ButtonHTMLAttributes } from "react";
export default function CTAButton({ children, className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button className={classNames("flex flex-row justify-center items-center gap-2 cursor-pointer rounded-md bg-transparent px-3 py-2 text-sm font-semibold text-white shadow-xs border-1 border-white hover:bg-gray-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600", className)} {...props}>{children}</button>
<button className={classNames("flex flex-row justify-center items-center gap-2 cursor-pointer rounded-3xl bg-transparent px-4 py-3 text-white shadow-xs border-1 border-white hover:bg-gray-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600", className)} {...props}>{children}</button>
);
}

View file

@ -1,10 +1,24 @@
import classNames from "classnames";
import { SelectHTMLAttributes } from "react";
import { CaretIcon } from "../Icons";
export default function Select({ children, className, ...props }: SelectHTMLAttributes<HTMLSelectElement>) {
return (
<select className={classNames("block w-full appearance-none rounded-md bg-white py-1.5 pr-8 pl-3 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6", className)} {...props}>
{children}
</select>
<div className="relative">
<select
className={
classNames(
"block w-full appearance-none rounded-md bg-white pl-4 pr-8 py-4 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600",
className,
)
}
{...props}
>
{children}
</select>
<span className="pointer-events-none absolute top-1/2 -mt-0.5 right-3 text-indigo-600 rotate-180">
<CaretIcon height={8} width={12} />
</span>
</div>
);
}

View file

@ -1,7 +1,103 @@
import { InputHTMLAttributes } from "react"
"use client";
export default function TextArea(props: InputHTMLAttributes<HTMLTextAreaElement>) {
return (
<textarea className="block w-full mt-2 rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6" {...props} />
import classNames from "classnames";
import { InputHTMLAttributes, useCallback, useEffect, useLayoutEffect, useRef } from "react"
interface TextAreaProps extends Omit<InputHTMLAttributes<HTMLTextAreaElement>, "onChange"> {
isAutoExpanding?: boolean; // Set to true to enable auto-expanding text area behavior. Default is false.
value: string;
onChange: (value: string) => void;
}
export default function TextArea({
isAutoExpanding,
style,
name,
value,
onChange,
className,
placeholder = "",
onKeyUp,
...props
}: TextAreaProps) {
const handleTextChange = useCallback((event: Event) => {
const fakeTextAreaElement = event.target as HTMLDivElement;
const newValue = fakeTextAreaElement.innerText;
if (newValue !== value) {
onChange?.(newValue);
}
}, [onChange, value]);
const handleKeyUp = useCallback((event: Event) => {
if (onKeyUp) {
onKeyUp(event as unknown as React.KeyboardEvent<HTMLTextAreaElement>);
}
}, [onKeyUp]);
const handleTextAreaFocus = (event: React.FocusEvent<HTMLDivElement>) => {
if (event.target.innerText.trim() === placeholder) {
event.target.innerText = "";
}
};
const handleTextAreaBlur = (event: React.FocusEvent<HTMLDivElement>) => {
if (value === "") {
event.target.innerText = placeholder;
}
};
const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
onChange(event.target.value);
};
const fakeTextAreaRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
const fakeTextAreaElement = fakeTextAreaRef.current;
if (fakeTextAreaElement) {
fakeTextAreaElement.innerText = placeholder;
fakeTextAreaElement.addEventListener("input", handleTextChange);
fakeTextAreaElement.addEventListener("keyup", handleKeyUp);
}
return () => {
if (fakeTextAreaElement) {
fakeTextAreaElement.removeEventListener("input", handleTextChange);
fakeTextAreaElement.removeEventListener("keyup", handleKeyUp);
}
};
}, []);
useEffect(() => {
const fakeTextAreaElement = fakeTextAreaRef.current;
const textAreaText = fakeTextAreaElement?.innerText;
if (fakeTextAreaElement && textAreaText !== value && textAreaText !== placeholder) {
fakeTextAreaElement.innerText = value;
}
}, [value]);
return isAutoExpanding ? (
<>
<div
ref={fakeTextAreaRef}
contentEditable="true"
role="textbox"
aria-multiline="true"
className={classNames("block w-full rounded-md bg-white px-4 py-4 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600", className)}
onFocus={handleTextAreaFocus}
onBlur={handleTextAreaBlur}
/>
</>
) : (
<textarea
name={name}
style={style}
value={value}
placeholder={placeholder}
className={classNames("block w-full rounded-md bg-white px-4 py-4 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600", className)}
onChange={handleChange}
{...props}
/>
)
}

View file

@ -1,6 +1,8 @@
export { default as Modal } from "./Modal";
export { default as Input } from "./Input";
export { default as Select } from "./Select";
export { default as TextArea } from "./TextArea";
export { default as CTAButton } from "./CTAButton";
export { default as GhostButton } from "./GhostButton";
export { default as NeutralButton } from "./NeutralButton";
export { default as StatusIndicator } from "./StatusIndicator";

View file

@ -1,12 +1,47 @@
import handleServerErrors from './handleServerErrors';
import handleServerErrors from "./handleServerErrors";
export default function fetch(url: string, options: RequestInit = {}): Promise<Response> {
return global.fetch('http://localhost:8000/api' + url, {
let numberOfRetries = 0;
const isAuth0Enabled = process.env.USE_AUTH0_AUTHORIZATION?.toLowerCase() === "true"
export default async function fetch(url: string, options: RequestInit = {}): Promise<Response> {
function retry(lastError: Response) {
if (!isAuth0Enabled) {
return Promise.reject(lastError);
}
if (numberOfRetries >= 1) {
return Promise.reject(lastError);
}
numberOfRetries += 1;
return window.fetch("/auth/token")
.then(() => {
return fetch(url, options);
});
}
return global.fetch("http://localhost:8000/api" + url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
},
credentials: "include",
})
.then(handleServerErrors);
.then((response) => handleServerErrors(response, retry))
.then((response) => {
numberOfRetries = 0;
return response;
})
.catch((error) => {
if (error.detail === undefined) {
return Promise.reject(
new Error("No connection to the server.")
);
}
if (error.status === 401) {
return retry(error);
}
return Promise.reject(error);
});
}

View file

@ -1,13 +1,25 @@
export default function handleServerErrors(response: Response): Promise<Response> {
import { redirect } from "next/navigation";
export default function handleServerErrors(response: Response, retry?: (response: Response) => Promise<Response>): Promise<Response> {
return new Promise((resolve, reject) => {
if (response.status === 401) {
window.location.href = '/auth';
return;
if (retry) {
return retry(response)
.catch(() => {
return redirect("/auth/login");
});
} else {
return redirect("/auth/login");
}
}
if (!response.ok) {
return response.json().then(error => reject(error));
}
return resolve(response);
if (response.status >= 200 && response.status < 300) {
return resolve(response);
}
return reject(response);
});
}

View file

@ -1,10 +1,10 @@
import { useState } from "react";
import { useCallback, useState } from "react";
export default function useBoolean(initialValue: boolean) {
const [value, setValue] = useState(initialValue);
const setTrue = () => setValue(true);
const setFalse = () => setValue(false);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return {
value,

41
cognee-frontend/types/d3-force-3d.d.ts vendored Normal file
View file

@ -0,0 +1,41 @@
declare module "d3-force-3d" {
// Import types from d3-force if needed
import {
SimulationNodeDatum,
SimulationLinkDatum,
Force,
Simulation,
} from "d3-force";
export interface SimulationNodeDatum3D extends SimulationNodeDatum {
x: number;
y: number;
z: number;
vx: number;
vy: number;
vz: number;
fx?: number | null;
fy?: number | null;
fz?: number | null;
}
export function forceSimulation<NodeDatum extends SimulationNodeDatum3D>(
nodes?: NodeDatum[]
): Simulation<NodeDatum, undefined>;
export function forceCenter(x: number, y: number, z: number): Force<SimulationNodeDatum3D, any>;
export function forceManyBody(): Force<SimulationNodeDatum3D, any>;
export function forceLink<NodeDatum extends SimulationNodeDatum3D, Links extends SimulationLinkDatum<NodeDatum>[] = SimulationLinkDatum<NodeDatum>[]>(
links?: Links
): Force<NodeDatum, SimulationLinkDatum<NodeDatum>>;
export function forceCollide(radius?: number): Force<SimulationNodeDatum3D, any>;
export function forceRadial(radius: number, x?: number, y?: number, z?: number): Force<SimulationNodeDatum3D, any>;
export function forceX(x?: number): Force<SimulationNodeDatum3D, any>;
export function forceY(y?: number): Force<SimulationNodeDatum3D, any>;
export function forceZ(z?: number): Force<SimulationNodeDatum3D, any>;
}

View file

@ -110,10 +110,10 @@ Remember  use the CODE search type to query your code graph. For huge repos,
#!/bin/bash
export ENV=local
export TOKENIZERS_PARALLELISM=false
export EMBEDDING_PROVIDER = "fastembed"
export EMBEDDING_PROVIDER="fastembed"
export EMBEDDING_MODEL="sentence-transformers/all-MiniLM-L6-v2"
export EMBEDDING_DIMENSIONS= 384
export EMBEDDING_MAX_TOKENS-256
export EMBEDDING_DIMENSIONS=384
export EMBEDDING_MAX_TOKENS=256
export LLM_API_KEY=your-OpenAI-API-key
uv --directory /{cognee_root_path}/cognee-mcp run cognee
```

View file

@ -68,7 +68,7 @@ app = FastAPI(debug=app_environment != "prod", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_origins=["http://localhost:3000", "http://127.0.0.1:3000"],
allow_credentials=True,
allow_methods=["OPTIONS", "GET", "POST", "DELETE"],
allow_headers=["*"],
@ -108,6 +108,22 @@ async def exception_handler(_: Request, exc: CogneeApiError) -> JSONResponse:
return JSONResponse(status_code=status_code, content={"detail": detail["message"]})
@app.get("/")
async def root():
"""
Root endpoint that returns a welcome message.
"""
return {"message": "Hello, World, I am alive!"}
@app.get("/health")
def health_check():
"""
Health check endpoint that returns the server status.
"""
return Response(status_code=200)
app.include_router(get_auth_router(), prefix="/api/v1/auth", tags=["auth"])
app.include_router(
@ -128,11 +144,11 @@ app.include_router(
tags=["auth"],
)
app.include_router(
get_users_router(),
prefix="/api/v1/users",
tags=["users"],
)
app.include_router(get_add_router(), prefix="/api/v1/add", tags=["add"])
app.include_router(get_cognify_router(), prefix="/api/v1/cognify", tags=["cognify"])
app.include_router(get_search_router(), prefix="/api/v1/search", tags=["search"])
app.include_router(
get_permissions_router(),
@ -140,31 +156,8 @@ app.include_router(
tags=["permissions"],
)
@app.get("/")
async def root():
"""
Root endpoint that returns a welcome message.
"""
return {"message": "Hello, World, I am alive!"}
@app.get("/health")
def health_check():
"""
Health check endpoint that returns the server status.
"""
return Response(status_code=200)
app.include_router(get_datasets_router(), prefix="/api/v1/datasets", tags=["datasets"])
app.include_router(get_add_router(), prefix="/api/v1/add", tags=["add"])
app.include_router(get_cognify_router(), prefix="/api/v1/cognify", tags=["cognify"])
app.include_router(get_search_router(), prefix="/api/v1/search", tags=["search"])
app.include_router(get_settings_router(), prefix="/api/v1/settings", tags=["settings"])
app.include_router(get_visualize_router(), prefix="/api/v1/visualize", tags=["visualize"])
@ -177,6 +170,12 @@ codegraph_routes = get_code_pipeline_router()
if codegraph_routes:
app.include_router(codegraph_routes, prefix="/api/v1/code-pipeline", tags=["code-pipeline"])
app.include_router(
get_users_router(),
prefix="/api/v1/users",
tags=["users"],
)
def start_api_server(host: str = "0.0.0.0", port: int = 8000):
"""
@ -197,4 +196,7 @@ def start_api_server(host: str = "0.0.0.0", port: int = 8000):
if __name__ == "__main__":
logger = setup_logging()
start_api_server()
start_api_server(
host=os.getenv("HTTP_API_HOST", "0.0.0.0"), port=int(os.getenv("HTTP_API_PORT", 8000))
)

View file

@ -17,10 +17,10 @@ logger = get_logger()
def get_add_router() -> APIRouter:
router = APIRouter()
@router.post("/", response_model=dict)
@router.post("", response_model=dict)
async def add(
data: List[UploadFile],
datasetName: str,
datasetName: Optional[str] = Form(default=None),
datasetId: Optional[UUID] = Form(default=None),
user: User = Depends(get_authenticated_user),
):

View file

@ -1,3 +1,4 @@
import os
import asyncio
from uuid import UUID
from pydantic import BaseModel
@ -6,37 +7,53 @@ from fastapi.responses import JSONResponse
from fastapi import APIRouter, WebSocket, Depends, WebSocketDisconnect
from starlette.status import WS_1000_NORMAL_CLOSURE, WS_1008_POLICY_VIOLATION
from cognee.api.DTO import InDTO
from cognee.modules.pipelines.methods import get_pipeline_run
from cognee.modules.users.models import User
from cognee.shared.data_models import KnowledgeGraph
from cognee.modules.users.methods import get_authenticated_user
from cognee.modules.users.get_user_db import get_user_db_context
from cognee.modules.graph.methods import get_formatted_graph_data
from cognee.modules.users.get_user_manager import get_user_manager_context
from cognee.infrastructure.databases.relational import get_relational_engine
from cognee.modules.users.authentication.default.default_jwt_strategy import DefaultJWTStrategy
from cognee.modules.pipelines.models.PipelineRunInfo import PipelineRunCompleted, PipelineRunInfo
from cognee.modules.graph.utils import deduplicate_nodes_and_edges, get_graph_from_model
from cognee.modules.pipelines.queues.pipeline_run_info_queues import (
get_from_queue,
initialize_queue,
remove_queue,
)
from cognee.shared.logging_utils import get_logger
class CognifyPayloadDTO(BaseModel):
datasets: List[str]
logger = get_logger("api.cognify")
class CognifyPayloadDTO(InDTO):
datasets: Optional[List[str]] = None
dataset_ids: Optional[List[UUID]] = None
graph_model: Optional[BaseModel] = KnowledgeGraph
run_in_background: Optional[bool] = False
def get_cognify_router() -> APIRouter:
router = APIRouter()
@router.post("/", response_model=None)
@router.post("", response_model=None)
async def cognify(payload: CognifyPayloadDTO, user: User = Depends(get_authenticated_user)):
"""This endpoint is responsible for the cognitive processing of the content."""
if not payload.datasets and not payload.dataset_ids:
return JSONResponse(
status_code=400, content={"error": "No datasets or dataset_ids provided"}
)
from cognee.api.v1.cognify import cognify as cognee_cognify
try:
datasets = payload.dataset_ids if payload.dataset_ids else payload.datasets
cognify_run = await cognee_cognify(
datasets, user, payload.graph_model, run_in_background=True
datasets, user, payload.graph_model, run_in_background=payload.run_in_background
)
return cognify_run.model_dump()
@ -47,16 +64,33 @@ def get_cognify_router() -> APIRouter:
async def subscribe_to_cognify_info(websocket: WebSocket, pipeline_run_id: str):
await websocket.accept()
auth_message = await websocket.receive_json()
access_token = websocket.cookies.get(os.getenv("AUTH_TOKEN_COOKIE_NAME", "auth_token"))
try:
await get_authenticated_user(auth_message.get("Authorization"))
except Exception:
secret = os.getenv("FASTAPI_USERS_JWT_SECRET", "super_secret")
strategy = DefaultJWTStrategy(secret, lifetime_seconds=3600)
db_engine = get_relational_engine()
async with db_engine.get_async_session() as session:
async with get_user_db_context(session) as user_db:
async with get_user_manager_context(user_db) as user_manager:
user = await get_authenticated_user(
cookie=access_token,
strategy_cookie=strategy,
user_manager=user_manager,
bearer=None,
)
except Exception as error:
logger.error(f"Authentication failed: {str(error)}")
await websocket.close(code=WS_1008_POLICY_VIOLATION, reason="Unauthorized")
return
pipeline_run_id = UUID(pipeline_run_id)
pipeline_run = await get_pipeline_run(pipeline_run_id)
initialize_queue(pipeline_run_id)
while True:
@ -74,9 +108,7 @@ def get_cognify_router() -> APIRouter:
{
"pipeline_run_id": str(pipeline_run_info.pipeline_run_id),
"status": pipeline_run_info.status,
"payload": await get_nodes_and_edges(pipeline_run_info.payload)
if pipeline_run_info.payload
else None,
"payload": await get_formatted_graph_data(pipeline_run.dataset_id, user.id),
}
)
@ -89,53 +121,3 @@ def get_cognify_router() -> APIRouter:
break
return router
async def get_nodes_and_edges(data_points):
nodes = []
edges = []
added_nodes = {}
added_edges = {}
visited_properties = {}
results = await asyncio.gather(
*[
get_graph_from_model(
data_point,
added_nodes=added_nodes,
added_edges=added_edges,
visited_properties=visited_properties,
)
for data_point in data_points
]
)
for result_nodes, result_edges in results:
nodes.extend(result_nodes)
edges.extend(result_edges)
nodes, edges = deduplicate_nodes_and_edges(nodes, edges)
return {
"nodes": list(
map(
lambda node: {
"id": str(node.id),
"label": node.name if hasattr(node, "name") else f"{node.type}_{str(node.id)}",
"properties": {},
},
nodes,
)
),
"edges": list(
map(
lambda edge: {
"source": str(edge[0]),
"target": str(edge[1]),
"label": edge[2],
},
edges,
)
),
}

Some files were not shown because too many files have changed in this diff Show more