mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
Merge pull request #138 from learnhouse/feat/experimental-ai-qa
AI Experimental Features
This commit is contained in:
commit
0a049e23ef
55 changed files with 6953 additions and 315 deletions
|
|
@ -8,14 +8,20 @@
|
|||
"extensions": [
|
||||
"eamodio.gitlens",
|
||||
"ms-python.python",
|
||||
"ms-python.black-formatter",
|
||||
"ms-python.vscode-pylance",
|
||||
"styled-components.vscode-styled-components",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-python.isort",
|
||||
"redhat.vscode-yaml"
|
||||
]
|
||||
],
|
||||
"settings": {
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.python"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"shutdownAction": "stopCompose"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,25 @@
|
|||
#
|
||||
FROM python:3.11
|
||||
|
||||
# poetry
|
||||
RUN pip install poetry
|
||||
|
||||
#
|
||||
WORKDIR /usr/learnhouse/apps/api
|
||||
|
||||
#
|
||||
COPY ./requirements.txt /usr/learnhouse/requirements.txt
|
||||
# Copy poetry.lock* in case it doesn't exist in the repo
|
||||
COPY ./poetry.lock* /usr/learnhouse/
|
||||
|
||||
#
|
||||
RUN pip install --no-cache-dir --upgrade -r /usr/learnhouse/requirements.txt
|
||||
# Copy project requirement files here to ensure they will be cached.
|
||||
COPY pyproject.toml /usr/learnhouse/
|
||||
|
||||
# Install poetry
|
||||
RUN pip install --upgrade pip \
|
||||
&& pip install poetry \
|
||||
&& poetry config virtualenvs.create false
|
||||
|
||||
# Install project dependencies.
|
||||
RUN poetry install --no-interaction --no-ansi
|
||||
|
||||
#
|
||||
COPY ./ /usr/learnhouse
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from typing import Literal, Optional
|
||||
from pydantic import BaseModel
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
import yaml
|
||||
|
||||
|
||||
|
|
@ -23,6 +24,11 @@ class SecurityConfig(BaseModel):
|
|||
auth_jwt_secret_key: str
|
||||
|
||||
|
||||
class AIConfig(BaseModel):
|
||||
openai_api_key: str | None
|
||||
is_ai_enabled: bool | None
|
||||
|
||||
|
||||
class S3ApiConfig(BaseModel):
|
||||
bucket_name: str | None
|
||||
endpoint_url: str | None
|
||||
|
|
@ -49,6 +55,8 @@ class DatabaseConfig(BaseModel):
|
|||
sql_connection_string: Optional[str]
|
||||
mongo_connection_string: Optional[str]
|
||||
|
||||
class RedisConfig(BaseModel):
|
||||
redis_connection_string: Optional[str]
|
||||
|
||||
class LearnHouseConfig(BaseModel):
|
||||
site_name: str
|
||||
|
|
@ -57,10 +65,15 @@ class LearnHouseConfig(BaseModel):
|
|||
general_config: GeneralConfig
|
||||
hosting_config: HostingConfig
|
||||
database_config: DatabaseConfig
|
||||
redis_config: RedisConfig
|
||||
security_config: SecurityConfig
|
||||
ai_config: AIConfig
|
||||
|
||||
|
||||
def get_learnhouse_config() -> LearnHouseConfig:
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Get the YAML file
|
||||
yaml_path = os.path.join(os.path.dirname(__file__), "config.yaml")
|
||||
|
||||
|
|
@ -173,6 +186,23 @@ def get_learnhouse_config() -> LearnHouseConfig:
|
|||
"mongo_connection_string"
|
||||
)
|
||||
|
||||
# Redis config
|
||||
env_redis_connection_string = os.environ.get("LEARNHOUSE_REDIS_CONNECTION_STRING")
|
||||
redis_connection_string = env_redis_connection_string or yaml_config.get(
|
||||
"redis_config", {}
|
||||
).get("redis_connection_string")
|
||||
|
||||
|
||||
# AI Config
|
||||
env_openai_api_key = os.environ.get("LEARNHOUSE_OPENAI_API_KEY")
|
||||
env_is_ai_enabled = os.environ.get("LEARNHOUSE_IS_AI_ENABLED")
|
||||
openai_api_key = env_openai_api_key or yaml_config.get("ai_config", {}).get(
|
||||
"openai_api_key"
|
||||
)
|
||||
is_ai_enabled = env_is_ai_enabled or yaml_config.get("ai_config", {}).get(
|
||||
"is_ai_enabled"
|
||||
)
|
||||
|
||||
# Sentry config
|
||||
# check if the sentry config is provided in the YAML file
|
||||
sentry_config_verif = (
|
||||
|
|
@ -217,6 +247,12 @@ def get_learnhouse_config() -> LearnHouseConfig:
|
|||
mongo_connection_string=mongo_connection_string,
|
||||
)
|
||||
|
||||
# AI Config
|
||||
ai_config = AIConfig(
|
||||
openai_api_key=openai_api_key,
|
||||
is_ai_enabled=bool(is_ai_enabled),
|
||||
)
|
||||
|
||||
# Create LearnHouseConfig object
|
||||
config = LearnHouseConfig(
|
||||
site_name=site_name,
|
||||
|
|
@ -228,6 +264,8 @@ def get_learnhouse_config() -> LearnHouseConfig:
|
|||
hosting_config=hosting_config,
|
||||
database_config=database_config,
|
||||
security_config=SecurityConfig(auth_jwt_secret_key=auth_jwt_secret_key),
|
||||
ai_config=ai_config,
|
||||
redis_config=RedisConfig(redis_connection_string=redis_connection_string),
|
||||
)
|
||||
|
||||
return config
|
||||
|
|
|
|||
|
|
@ -27,3 +27,6 @@ hosting_config:
|
|||
database_config:
|
||||
sql_connection_string: postgresql://learnhouse:learnhouse@db:5432/learnhouse
|
||||
mongo_connection_string: mongodb://learnhouse:learnhouse@mongo:27017/
|
||||
|
||||
redis_config:
|
||||
redis_connection_string: redis://redis:6379/learnhouse
|
||||
|
|
|
|||
4444
apps/api/poetry.lock
generated
Normal file
4444
apps/api/poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,3 +1,46 @@
|
|||
[tool.ruff]
|
||||
# E501 line too long (82 > 79 characters)
|
||||
ignore = ["E501"]
|
||||
ignore = ["E501", "E712"]
|
||||
|
||||
[tool.poetry]
|
||||
name = "learnhouse-api"
|
||||
version = "0.1.0"
|
||||
description = "Learnhouse Backend"
|
||||
authors = ["Badr B. (swve)"]
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
fastapi = "0.104.1"
|
||||
pydantic = {version = ">=1.8.0,<2.0.0", extras = ["email"]}
|
||||
sqlmodel = "0.0.10"
|
||||
uvicorn = "0.23.2"
|
||||
pymongo = "4.3.3"
|
||||
motor = "3.1.1"
|
||||
psycopg2 = "^2.9.9"
|
||||
python-multipart = "^0.0.6"
|
||||
boto3 = "^1.34.17"
|
||||
botocore = "^1.34.17"
|
||||
python-jose = "^3.3.0"
|
||||
passlib = "^1.7.4"
|
||||
fastapi-jwt-auth = "^0.5.0"
|
||||
pytest = "^7.4.4"
|
||||
httpx = "^0.26.0"
|
||||
faker = "^22.2.0"
|
||||
requests = "^2.31.0"
|
||||
pyyaml = "^6.0.1"
|
||||
sentry-sdk = {extras = ["fastapi"], version = "^1.39.2"}
|
||||
langchain = "0.1.0"
|
||||
tiktoken = "^0.5.2"
|
||||
openai = "^1.7.1"
|
||||
chromadb = "^0.4.22"
|
||||
sentence-transformers = "^2.2.2"
|
||||
python-dotenv = "^1.0.0"
|
||||
redis = "^5.0.1"
|
||||
langchain-community = "^0.0.11"
|
||||
langchain-openai = "^0.0.2.post1"
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
|
|
|||
|
|
@ -17,4 +17,13 @@ faker
|
|||
requests
|
||||
pyyaml
|
||||
sentry-sdk[fastapi]
|
||||
pydantic[email]>=1.8.0,<2.0.0
|
||||
pydantic[email]>=1.8.0,<2.0.0
|
||||
langchain==0.1.0
|
||||
langchain-community
|
||||
langchain-openai
|
||||
tiktoken
|
||||
openai
|
||||
chromadb
|
||||
sentence-transformers
|
||||
python-dotenv
|
||||
redis
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class CourseUpdate(CourseBase):
|
|||
class CourseRead(CourseBase):
|
||||
id: int
|
||||
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||
authors: List[UserRead]
|
||||
authors: Optional[List[UserRead]]
|
||||
course_uuid: str
|
||||
creation_date: str
|
||||
update_date: str
|
||||
|
|
|
|||
66
apps/api/src/db/organization_config.py
Normal file
66
apps/api/src/db/organization_config.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
from typing import Literal, Optional
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import JSON, BigInteger, Column, ForeignKey
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
# AI
|
||||
class AILimitsSettings(BaseModel):
|
||||
limits_enabled: bool = False
|
||||
max_asks: int = 0
|
||||
|
||||
|
||||
class AIEnabledFeatures(BaseModel):
|
||||
editor: bool = False
|
||||
activity_ask: bool = False
|
||||
course_ask: bool = False
|
||||
global_ai_ask: bool = False
|
||||
|
||||
|
||||
class AIConfig(BaseModel):
|
||||
enabled : bool = True
|
||||
limits: AILimitsSettings = AILimitsSettings()
|
||||
embeddings: Literal[
|
||||
"text-embedding-ada-002", "all-MiniLM-L6-v2"
|
||||
] = "all-MiniLM-L6-v2"
|
||||
ai_model: Literal["gpt-3.5-turbo", "gpt-4-1106-preview"] = "gpt-3.5-turbo"
|
||||
features: AIEnabledFeatures = AIEnabledFeatures()
|
||||
|
||||
|
||||
class OrgUserConfig(BaseModel):
|
||||
signup_mechanism: Literal["open", "inviteOnly"] = "open"
|
||||
|
||||
|
||||
# Limits
|
||||
class LimitSettings(BaseModel):
|
||||
limits_enabled: bool = False
|
||||
max_users: int = 0
|
||||
max_storage: int = 0
|
||||
max_staff: int = 0
|
||||
|
||||
|
||||
# General
|
||||
class GeneralConfig(BaseModel):
|
||||
color: str = ""
|
||||
limits: LimitSettings = LimitSettings()
|
||||
users: OrgUserConfig = OrgUserConfig()
|
||||
active: bool = True
|
||||
|
||||
|
||||
class OrganizationConfigBase(SQLModel):
|
||||
GeneralConfig: GeneralConfig
|
||||
AIConfig: AIConfig
|
||||
|
||||
class OrganizationConfig(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
org_id: int = Field(
|
||||
sa_column=Column(BigInteger, ForeignKey("organization.id", ondelete="CASCADE"))
|
||||
)
|
||||
# TODO: fix this to use the correct type GeneralConfig
|
||||
config: dict = Field(default={}, sa_column=Column(JSON))
|
||||
creation_date: Optional[str]
|
||||
update_date: Optional[str]
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
from typing import Optional
|
||||
from sqlalchemy import BigInteger, Column, ForeignKey
|
||||
from sqlmodel import Field, SQLModel
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class HeaderTypeEnum(str, Enum):
|
||||
LOGO_MENU_SETTINGS = "LOGO_MENU_SETTINGS"
|
||||
MENU_LOGO_SETTINGS = "MENU_LOGO_SETTINGS"
|
||||
|
||||
|
||||
class OrganizationSettings(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
org_id: int = Field(
|
||||
sa_column=Column(BigInteger, ForeignKey("organization.id", ondelete="CASCADE"))
|
||||
)
|
||||
logo_image: Optional[str] = ""
|
||||
header_type: HeaderTypeEnum = HeaderTypeEnum.LOGO_MENU_SETTINGS
|
||||
color: str = ""
|
||||
creation_date: str
|
||||
update_date: str
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
from typing import Optional
|
||||
from sqlmodel import Field, SQLModel
|
||||
from src.db.organization_config import OrganizationConfig
|
||||
|
||||
|
||||
class OrganizationBase(SQLModel):
|
||||
name: str
|
||||
description: Optional[str]
|
||||
description: Optional[str]
|
||||
slug: str
|
||||
email: str
|
||||
logo_image: Optional[str]
|
||||
logo_image: Optional[str]
|
||||
|
||||
|
||||
class Organization(OrganizationBase, table=True):
|
||||
|
|
@ -16,9 +17,11 @@ class Organization(OrganizationBase, table=True):
|
|||
creation_date: str = ""
|
||||
update_date: str = ""
|
||||
|
||||
|
||||
class OrganizationUpdate(OrganizationBase):
|
||||
pass
|
||||
|
||||
|
||||
class OrganizationCreate(OrganizationBase):
|
||||
pass
|
||||
|
||||
|
|
@ -26,5 +29,6 @@ class OrganizationCreate(OrganizationBase):
|
|||
class OrganizationRead(OrganizationBase):
|
||||
id: int
|
||||
org_uuid: str
|
||||
creation_date: str
|
||||
update_date: str
|
||||
config: Optional[OrganizationConfig | dict]
|
||||
creation_date: str
|
||||
update_date: str
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from fastapi import APIRouter, Depends
|
||||
from src.routers import blocks, dev, trail, users, auth, orgs, roles
|
||||
from src.routers.ai import ai
|
||||
from src.routers.courses import chapters, collections, courses, activities
|
||||
from src.routers.install import install
|
||||
from src.services.dev.dev import isDevModeEnabledOrRaise
|
||||
|
|
@ -18,14 +19,16 @@ v1_router.include_router(blocks.router, prefix="/blocks", tags=["blocks"])
|
|||
v1_router.include_router(courses.router, prefix="/courses", tags=["courses"])
|
||||
v1_router.include_router(chapters.router, prefix="/chapters", tags=["chapters"])
|
||||
v1_router.include_router(activities.router, prefix="/activities", tags=["activities"])
|
||||
v1_router.include_router(
|
||||
collections.router, prefix="/collections", tags=["collections"]
|
||||
)
|
||||
v1_router.include_router(collections.router, prefix="/collections", tags=["collections"])
|
||||
v1_router.include_router(trail.router, prefix="/trail", tags=["trail"])
|
||||
v1_router.include_router(ai.router, prefix="/ai", tags=["ai"])
|
||||
|
||||
# Dev Routes
|
||||
v1_router.include_router(
|
||||
dev.router, prefix="/dev", tags=["dev"], dependencies=[Depends(isDevModeEnabledOrRaise)]
|
||||
dev.router,
|
||||
prefix="/dev",
|
||||
tags=["dev"],
|
||||
dependencies=[Depends(isDevModeEnabledOrRaise)],
|
||||
)
|
||||
|
||||
# Install Routes
|
||||
|
|
@ -35,4 +38,3 @@ v1_router.include_router(
|
|||
tags=["install"],
|
||||
dependencies=[Depends(isInstallModeEnabled)],
|
||||
)
|
||||
|
||||
|
|
|
|||
39
apps/api/src/routers/ai/ai.py
Normal file
39
apps/api/src/routers/ai/ai.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
from fastapi import APIRouter, Depends, Request
|
||||
from sqlmodel import Session
|
||||
from src.services.ai.ai import ai_send_activity_chat_message, ai_start_activity_chat_session
|
||||
from src.services.ai.schemas.ai import ActivityAIChatSessionResponse, SendActivityAIChatMessage, StartActivityAIChatSession
|
||||
from src.core.events.database import get_db_session
|
||||
from src.db.users import PublicUser
|
||||
from src.security.auth import get_current_user
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/start/activity_chat_session")
|
||||
async def api_ai_start_activity_chat_session(
|
||||
request: Request,
|
||||
chat_session_object: StartActivityAIChatSession,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
)-> ActivityAIChatSessionResponse:
|
||||
"""
|
||||
Start a new AI Chat session with a Course Activity
|
||||
"""
|
||||
return ai_start_activity_chat_session(
|
||||
request, chat_session_object, current_user, db_session
|
||||
)
|
||||
|
||||
@router.post("/send/activity_chat_message")
|
||||
async def api_ai_send_activity_chat_message(
|
||||
request: Request,
|
||||
chat_session_object: SendActivityAIChatMessage,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
)-> ActivityAIChatSessionResponse:
|
||||
"""
|
||||
Send a message to an AI Chat session with a Course Activity
|
||||
"""
|
||||
return ai_send_activity_chat_message(
|
||||
request, chat_session_object, current_user, db_session
|
||||
)
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
from typing import List
|
||||
from fastapi import APIRouter, Depends, Request, UploadFile
|
||||
from sqlmodel import Session
|
||||
from src.db.organization_config import OrganizationConfigBase
|
||||
from src.db.users import PublicUser
|
||||
from src.db.organizations import (
|
||||
Organization,
|
||||
|
|
@ -12,6 +13,7 @@ from src.core.events.database import get_db_session
|
|||
from src.security.auth import get_current_user
|
||||
from src.services.orgs.orgs import (
|
||||
create_org,
|
||||
create_org_with_config,
|
||||
delete_org,
|
||||
get_organization,
|
||||
get_organization_by_slug,
|
||||
|
|
@ -37,6 +39,23 @@ async def api_create_org(
|
|||
return await create_org(request, org_object, current_user, db_session)
|
||||
|
||||
|
||||
# Temporary pre-alpha code
|
||||
@router.post("/withconfig/")
|
||||
async def api_create_org_withconfig(
|
||||
request: Request,
|
||||
org_object: OrganizationCreate,
|
||||
config_object: OrganizationConfigBase,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
) -> OrganizationRead:
|
||||
"""
|
||||
Create new organization
|
||||
"""
|
||||
return await create_org_with_config(
|
||||
request, org_object, current_user, db_session, config_object
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{org_id}")
|
||||
async def api_get_org(
|
||||
request: Request,
|
||||
|
|
@ -110,7 +129,7 @@ async def api_update_org(
|
|||
"""
|
||||
Update Org by ID
|
||||
"""
|
||||
return await update_org(request, org_object,org_id, current_user, db_session)
|
||||
return await update_org(request, org_object, org_id, current_user, db_session)
|
||||
|
||||
|
||||
@router.delete("/{org_id}")
|
||||
|
|
|
|||
204
apps/api/src/services/ai/ai.py
Normal file
204
apps/api/src/services/ai/ai.py
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
from fastapi import Depends, HTTPException, Request
|
||||
from sqlmodel import Session, select
|
||||
from src.db.organization_config import OrganizationConfig
|
||||
from src.db.organizations import Organization
|
||||
from src.services.ai.utils import check_limits_and_config, count_ai_ask
|
||||
from src.db.courses import Course, CourseRead
|
||||
from src.core.events.database import get_db_session
|
||||
from src.db.users import PublicUser
|
||||
from src.db.activities import Activity, ActivityRead
|
||||
from src.security.auth import get_current_user
|
||||
from src.services.ai.base import ask_ai, get_chat_session_history
|
||||
|
||||
from src.services.ai.schemas.ai import (
|
||||
ActivityAIChatSessionResponse,
|
||||
SendActivityAIChatMessage,
|
||||
StartActivityAIChatSession,
|
||||
)
|
||||
from src.services.courses.activities.utils import (
|
||||
serialize_activity_text_to_ai_comprehensible_text,
|
||||
structure_activity_content_by_type,
|
||||
)
|
||||
|
||||
|
||||
def ai_start_activity_chat_session(
|
||||
request: Request,
|
||||
chat_session_object: StartActivityAIChatSession,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
) -> ActivityAIChatSessionResponse:
|
||||
"""
|
||||
Start a new AI Chat session with a Course Activity
|
||||
"""
|
||||
|
||||
# Get the Activity
|
||||
statement = select(Activity).where(
|
||||
Activity.activity_uuid == chat_session_object.activity_uuid
|
||||
)
|
||||
activity = db_session.exec(statement).first()
|
||||
|
||||
activity = ActivityRead.from_orm(activity)
|
||||
|
||||
# Get the Course
|
||||
statement = (
|
||||
select(Course)
|
||||
.join(Activity)
|
||||
.where(Activity.activity_uuid == chat_session_object.activity_uuid)
|
||||
)
|
||||
course = db_session.exec(statement).first()
|
||||
course = CourseRead.from_orm(course)
|
||||
|
||||
# Get the Organization
|
||||
statement = select(Organization).where(Organization.id == course.org_id)
|
||||
org = db_session.exec(statement).first()
|
||||
|
||||
# Check limits and usage
|
||||
check_limits_and_config(db_session, org) # type: ignore
|
||||
count_ai_ask(org, "increment") # type: ignore
|
||||
|
||||
if not activity:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Activity not found",
|
||||
)
|
||||
|
||||
# Get Activity Content Blocks
|
||||
content = activity.content
|
||||
|
||||
# Serialize Activity Content Blocks to a text comprehensible by the AI
|
||||
structured = structure_activity_content_by_type(content)
|
||||
ai_friendly_text = serialize_activity_text_to_ai_comprehensible_text(
|
||||
structured, course, activity
|
||||
)
|
||||
|
||||
# Get Activity Organization
|
||||
statement = select(Organization).where(Organization.id == course.org_id)
|
||||
org = db_session.exec(statement).first()
|
||||
|
||||
# Get Organization Config
|
||||
statement = select(OrganizationConfig).where(
|
||||
OrganizationConfig.org_id == org.id # type: ignore
|
||||
)
|
||||
result = db_session.exec(statement)
|
||||
org_config = result.first()
|
||||
|
||||
org_config = OrganizationConfig.from_orm(org_config)
|
||||
embeddings = org_config.config["AIConfig"]["embeddings"]
|
||||
ai_model = org_config.config["AIConfig"]["ai_model"]
|
||||
|
||||
chat_session = get_chat_session_history()
|
||||
|
||||
message = "You are a helpful Education Assistant, and you are helping a student with the associated Course. "
|
||||
message += "Use the available tools to get context about this question even if the question is not specific enough."
|
||||
message += "For context, this is the Course name :"
|
||||
message += course.name
|
||||
message += " and this is the Lecture name :"
|
||||
message += activity.name
|
||||
message += "."
|
||||
message += "Use your knowledge to help the student if the context is not enough."
|
||||
|
||||
response = ask_ai(
|
||||
chat_session_object.message,
|
||||
chat_session["message_history"],
|
||||
ai_friendly_text,
|
||||
message,
|
||||
embeddings,
|
||||
ai_model,
|
||||
)
|
||||
|
||||
return ActivityAIChatSessionResponse(
|
||||
aichat_uuid=chat_session["aichat_uuid"],
|
||||
activity_uuid=activity.activity_uuid,
|
||||
message=response["output"],
|
||||
)
|
||||
|
||||
|
||||
def ai_send_activity_chat_message(
|
||||
request: Request,
|
||||
chat_session_object: SendActivityAIChatMessage,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
) -> ActivityAIChatSessionResponse:
|
||||
"""
|
||||
Start a new AI Chat session with a Course Activity
|
||||
"""
|
||||
# Get the Activity
|
||||
statement = select(Activity).where(
|
||||
Activity.activity_uuid == chat_session_object.activity_uuid
|
||||
)
|
||||
activity = db_session.exec(statement).first()
|
||||
|
||||
activity = ActivityRead.from_orm(activity)
|
||||
|
||||
# Get the Course
|
||||
statement = (
|
||||
select(Course)
|
||||
.join(Activity)
|
||||
.where(Activity.activity_uuid == chat_session_object.activity_uuid)
|
||||
)
|
||||
course = db_session.exec(statement).first()
|
||||
course = CourseRead.from_orm(course)
|
||||
|
||||
# Get the Organization
|
||||
statement = select(Organization).where(Organization.id == course.org_id)
|
||||
org = db_session.exec(statement).first()
|
||||
|
||||
# Check limits and usage
|
||||
check_limits_and_config(db_session, org) # type: ignore
|
||||
count_ai_ask(org, "increment") # type: ignore
|
||||
|
||||
if not activity:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Activity not found",
|
||||
)
|
||||
|
||||
# Get Activity Content Blocks
|
||||
content = activity.content
|
||||
|
||||
# Serialize Activity Content Blocks to a text comprehensible by the AI
|
||||
structured = structure_activity_content_by_type(content)
|
||||
ai_friendly_text = serialize_activity_text_to_ai_comprehensible_text(
|
||||
structured, course, activity
|
||||
)
|
||||
|
||||
# Get Activity Organization
|
||||
statement = select(Organization).where(Organization.id == course.org_id)
|
||||
org = db_session.exec(statement).first()
|
||||
|
||||
# Get Organization Config
|
||||
statement = select(OrganizationConfig).where(
|
||||
OrganizationConfig.org_id == org.id # type: ignore
|
||||
)
|
||||
result = db_session.exec(statement)
|
||||
org_config = result.first()
|
||||
|
||||
org_config = OrganizationConfig.from_orm(org_config)
|
||||
embeddings = org_config.config["AIConfig"]["embeddings"]
|
||||
ai_model = org_config.config["AIConfig"]["ai_model"]
|
||||
|
||||
chat_session = get_chat_session_history(chat_session_object.aichat_uuid)
|
||||
|
||||
message = "You are a helpful Education Assistant, and you are helping a student with the associated Course. "
|
||||
message += "Use the available tools to get context about this question even if the question is not specific enough."
|
||||
message += "For context, this is the Course name :"
|
||||
message += course.name
|
||||
message += " and this is the Lecture name :"
|
||||
message += activity.name
|
||||
message += "."
|
||||
message += "Use your knowledge to help the student if the context is not enough."
|
||||
|
||||
response = ask_ai(
|
||||
chat_session_object.message,
|
||||
chat_session["message_history"],
|
||||
ai_friendly_text,
|
||||
message,
|
||||
embeddings,
|
||||
ai_model,
|
||||
)
|
||||
|
||||
return ActivityAIChatSessionResponse(
|
||||
aichat_uuid=chat_session["aichat_uuid"],
|
||||
activity_uuid=activity.activity_uuid,
|
||||
message=response["output"],
|
||||
)
|
||||
117
apps/api/src/services/ai/base.py
Normal file
117
apps/api/src/services/ai/base.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
from langchain.agents import AgentExecutor
|
||||
from langchain.text_splitter import CharacterTextSplitter
|
||||
from langchain.embeddings.sentence_transformer import SentenceTransformerEmbeddings
|
||||
from langchain_community.vectorstores import Chroma
|
||||
from langchain.agents.openai_functions_agent.base import OpenAIFunctionsAgent
|
||||
from langchain.prompts import MessagesPlaceholder
|
||||
from langchain_community.chat_message_histories import RedisChatMessageHistory
|
||||
from langchain_core.messages import SystemMessage
|
||||
from langchain.agents.openai_functions_agent.agent_token_buffer_memory import (
|
||||
AgentTokenBufferMemory,
|
||||
)
|
||||
from langchain_openai import OpenAIEmbeddings
|
||||
from langchain_community.chat_models import ChatOpenAI
|
||||
from langchain.agents.agent_toolkits import (
|
||||
create_retriever_tool,
|
||||
)
|
||||
|
||||
import chromadb
|
||||
|
||||
from config.config import get_learnhouse_config
|
||||
|
||||
client = chromadb.Client()
|
||||
|
||||
|
||||
chat_history = []
|
||||
|
||||
|
||||
def ask_ai(
|
||||
question: str,
|
||||
message_history,
|
||||
text_reference: str,
|
||||
message_for_the_prompt: str,
|
||||
embedding_model_name: str,
|
||||
openai_model_name: str,
|
||||
):
|
||||
# Get API Keys
|
||||
LH_CONFIG = get_learnhouse_config()
|
||||
openai_api_key = LH_CONFIG.ai_config.openai_api_key
|
||||
|
||||
# split it into chunks
|
||||
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
|
||||
documents = text_splitter.create_documents([text_reference])
|
||||
texts = text_splitter.split_documents(documents)
|
||||
|
||||
embedding_models = {
|
||||
"all-MiniLM-L6-v2": SentenceTransformerEmbeddings,
|
||||
"text-embedding-ada-002": OpenAIEmbeddings,
|
||||
}
|
||||
|
||||
embedding_function = None
|
||||
|
||||
if embedding_model_name in embedding_models:
|
||||
if embedding_model_name == "text-embedding-ada-002":
|
||||
embedding_function = embedding_models[embedding_model_name](model=embedding_model_name, api_key=openai_api_key)
|
||||
if embedding_model_name == "all-MiniLM-L6-v2":
|
||||
embedding_function = embedding_models[embedding_model_name](model_name=embedding_model_name)
|
||||
else:
|
||||
embedding_function = embedding_models[embedding_model_name](model_name=embedding_model_name)
|
||||
|
||||
# load it into Chroma and use it as a retriever
|
||||
db = Chroma.from_documents(texts, embedding_function)
|
||||
tool = create_retriever_tool(
|
||||
db.as_retriever(),
|
||||
"find_context_text",
|
||||
"Find associated text to get context about a course or a lecture",
|
||||
)
|
||||
tools = [tool]
|
||||
|
||||
llm = ChatOpenAI(
|
||||
temperature=0, api_key=openai_api_key, model_name=openai_model_name
|
||||
)
|
||||
|
||||
memory_key = "history"
|
||||
|
||||
memory = AgentTokenBufferMemory(
|
||||
memory_key=memory_key, llm=llm, chat_memory=message_history, max_token_limit=1000
|
||||
)
|
||||
|
||||
system_message = SystemMessage(content=(message_for_the_prompt))
|
||||
|
||||
prompt = OpenAIFunctionsAgent.create_prompt(
|
||||
system_message=system_message,
|
||||
extra_prompt_messages=[MessagesPlaceholder(variable_name=memory_key)],
|
||||
)
|
||||
|
||||
agent = OpenAIFunctionsAgent(llm=llm, tools=tools, prompt=prompt)
|
||||
|
||||
agent_executor = AgentExecutor(
|
||||
agent=agent,
|
||||
tools=tools,
|
||||
memory=memory,
|
||||
verbose=True,
|
||||
return_intermediate_steps=True,
|
||||
handle_parsing_errors=True,
|
||||
)
|
||||
|
||||
return agent_executor({"input": question})
|
||||
|
||||
|
||||
def get_chat_session_history(aichat_uuid: Optional[str] = None):
|
||||
# Init Message History
|
||||
session_id = aichat_uuid if aichat_uuid else f"aichat_{uuid4()}"
|
||||
|
||||
LH_CONFIG = get_learnhouse_config()
|
||||
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
|
||||
|
||||
if redis_conn_string:
|
||||
message_history = RedisChatMessageHistory(
|
||||
url=redis_conn_string, ttl=2160000, session_id=session_id
|
||||
)
|
||||
else:
|
||||
print("Redis connection string not found, using local memory")
|
||||
message_history = []
|
||||
|
||||
return {"message_history": message_history, "aichat_uuid": session_id}
|
||||
17
apps/api/src/services/ai/schemas/ai.py
Normal file
17
apps/api/src/services/ai/schemas/ai.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class StartActivityAIChatSession(BaseModel):
|
||||
activity_uuid: str
|
||||
message: str
|
||||
|
||||
class ActivityAIChatSessionResponse(BaseModel):
|
||||
aichat_uuid: str
|
||||
activity_uuid: str
|
||||
message: str
|
||||
|
||||
|
||||
class SendActivityAIChatMessage(BaseModel):
|
||||
aichat_uuid: str
|
||||
activity_uuid: str
|
||||
message: str
|
||||
114
apps/api/src/services/ai/utils.py
Normal file
114
apps/api/src/services/ai/utils.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
from typing import Literal
|
||||
import redis
|
||||
from fastapi import HTTPException
|
||||
from sqlmodel import Session, select
|
||||
from config.config import get_learnhouse_config
|
||||
from src.db.organization_config import OrganizationConfig
|
||||
from src.db.organizations import Organization
|
||||
|
||||
|
||||
def count_ai_ask(
|
||||
organization: Organization,
|
||||
operation: Literal["increment", "decrement"],
|
||||
):
|
||||
"""
|
||||
Count the number of AI asks
|
||||
"""
|
||||
|
||||
LH_CONFIG = get_learnhouse_config()
|
||||
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
|
||||
|
||||
if not redis_conn_string:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Redis connection string not found",
|
||||
)
|
||||
|
||||
# Connect to Redis
|
||||
r = redis.Redis.from_url(redis_conn_string)
|
||||
|
||||
if not r:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Could not connect to Redis",
|
||||
)
|
||||
|
||||
# Get the number of AI asks
|
||||
ai_asks = r.get(f"ai_asks:{organization.org_uuid}")
|
||||
|
||||
if ai_asks is None:
|
||||
ai_asks = 0
|
||||
|
||||
# Increment or decrement the number of AI asks
|
||||
if operation == "increment":
|
||||
ai_asks = int(ai_asks) + 1
|
||||
elif operation == "decrement":
|
||||
ai_asks = int(ai_asks) - 1
|
||||
|
||||
# Update the number of AI asks
|
||||
r.set(f"ai_asks:{organization.org_uuid}", ai_asks)
|
||||
|
||||
# Set the expiration time to 30 days
|
||||
r.expire(f"ai_asks:{organization.org_uuid}", 2592000)
|
||||
|
||||
|
||||
def check_limits_and_config(db_session: Session, organization: Organization):
|
||||
"""
|
||||
Check the limits and config of an Organization
|
||||
"""
|
||||
|
||||
# Get the Organization Config
|
||||
statement = select(OrganizationConfig).where(
|
||||
OrganizationConfig.org_id == organization.id
|
||||
)
|
||||
result = db_session.exec(statement)
|
||||
org_config = result.first()
|
||||
|
||||
if org_config is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Organization has no config",
|
||||
)
|
||||
|
||||
# Check if the Organizations has AI enabled
|
||||
if org_config.config["AIConfig"]["enabled"] == False:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Organization has AI disabled",
|
||||
)
|
||||
|
||||
# Check if the Organization has Limits enabled and if the max_asks limit has been reached
|
||||
if org_config.config["AIConfig"]["limits"]["limits_enabled"] == True:
|
||||
LH_CONFIG = get_learnhouse_config()
|
||||
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
|
||||
|
||||
if not redis_conn_string:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Redis connection string not found",
|
||||
)
|
||||
|
||||
# Connect to Redis
|
||||
r = redis.Redis.from_url(redis_conn_string)
|
||||
|
||||
if not r:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Could not connect to Redis",
|
||||
)
|
||||
|
||||
# Get the number of AI asks
|
||||
ai_asks = r.get(f"ai_asks:{organization.org_uuid}")
|
||||
|
||||
# Get a number of AI asks
|
||||
if ai_asks is None:
|
||||
ai_asks = 0
|
||||
else:
|
||||
ai_asks = int(ai_asks)
|
||||
|
||||
# Check if the Number of asks is less than the max_asks limit
|
||||
if org_config.config["AIConfig"]["limits"]["max_asks"] <= ai_asks:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Organization has reached the max number of AI asks",
|
||||
)
|
||||
|
|
@ -44,12 +44,12 @@ async def create_image_block(
|
|||
image_file,
|
||||
activity_uuid,
|
||||
block_uuid,
|
||||
["jpg", "jpeg", "png", "gif"],
|
||||
["jpg", "jpeg", "png", "gif", "webp"],
|
||||
block_type,
|
||||
org.org_uuid,
|
||||
str(course.course_uuid),
|
||||
)
|
||||
|
||||
|
||||
# create block
|
||||
block = Block(
|
||||
activity_id=activity.id if activity.id else 0,
|
||||
|
|
|
|||
81
apps/api/src/services/courses/activities/utils.py
Normal file
81
apps/api/src/services/courses/activities/utils.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
from src.db.activities import ActivityRead
|
||||
from src.db.courses import CourseRead
|
||||
|
||||
|
||||
def structure_activity_content_by_type(activity):
|
||||
### Get Headings, Texts, Callouts, Answers and Paragraphs from the activity as a big list of strings (text only) and return it
|
||||
content = activity["content"]
|
||||
|
||||
headings = []
|
||||
callouts = []
|
||||
paragraphs = []
|
||||
|
||||
for item in content:
|
||||
if 'content' in item:
|
||||
if item["type"] == "heading" and "text" in item["content"][0]:
|
||||
headings.append(item["content"][0]["text"])
|
||||
elif item["type"] in ["calloutInfo", "calloutWarning"] and all("text" in text_item for text_item in item["content"]):
|
||||
callouts.append(
|
||||
"".join([text_item["text"] for text_item in item["content"]])
|
||||
)
|
||||
elif item["type"] == "paragraph" and "text" in item["content"][0]:
|
||||
paragraphs.append(item["content"][0]["text"])
|
||||
|
||||
# TODO: Get Questions and Answers (if any)
|
||||
|
||||
data_array = []
|
||||
|
||||
# Add Headings
|
||||
data_array.append({"Headings": headings})
|
||||
|
||||
# Add Callouts
|
||||
data_array.append({"Callouts": callouts})
|
||||
|
||||
# Add Paragraphs
|
||||
data_array.append({"Paragraphs": paragraphs})
|
||||
|
||||
print(data_array)
|
||||
|
||||
return data_array
|
||||
|
||||
|
||||
def serialize_activity_text_to_ai_comprehensible_text(
|
||||
data_array, course: CourseRead, activity: ActivityRead
|
||||
):
|
||||
### Serialize the text to a format that is comprehensible by the AI
|
||||
|
||||
# Serialize Headings
|
||||
serialized_headings = ""
|
||||
for heading in data_array[0]["Headings"]:
|
||||
serialized_headings += heading + " "
|
||||
|
||||
# Serialize Callouts
|
||||
serialized_callouts = ""
|
||||
|
||||
for callout in data_array[1]["Callouts"]:
|
||||
serialized_callouts += callout + " "
|
||||
|
||||
# Serialize Paragraphs
|
||||
serialized_paragraphs = ""
|
||||
for paragraph in data_array[2]["Paragraphs"]:
|
||||
serialized_paragraphs += paragraph + " "
|
||||
|
||||
# Get a text that is comprehensible by the AI
|
||||
text = (
|
||||
"Use this as a context "
|
||||
+ 'This is a course about "'
|
||||
+ course.name
|
||||
+ '". '
|
||||
+ 'This is a lecture about "'
|
||||
+ activity.name
|
||||
+ '". '
|
||||
'These are the headings: "'
|
||||
+ serialized_headings
|
||||
+ '" These are the callouts: "'
|
||||
+ serialized_callouts
|
||||
+ '" These are the paragraphs: "'
|
||||
+ serialized_paragraphs
|
||||
+ '"'
|
||||
)
|
||||
|
||||
return text
|
||||
|
|
@ -1,7 +1,19 @@
|
|||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
from uuid import uuid4
|
||||
from sqlmodel import Session, select
|
||||
from src.db.organization_config import (
|
||||
AIConfig,
|
||||
AIEnabledFeatures,
|
||||
AILimitsSettings,
|
||||
GeneralConfig,
|
||||
LimitSettings,
|
||||
OrgUserConfig,
|
||||
OrganizationConfig,
|
||||
OrganizationConfigBase,
|
||||
)
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_if_user_is_anon,
|
||||
|
|
@ -23,7 +35,7 @@ async def get_organization(
|
|||
org_id: str,
|
||||
db_session: Session,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
):
|
||||
) -> OrganizationRead:
|
||||
statement = select(Organization).where(Organization.id == org_id)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
|
|
@ -38,7 +50,18 @@ async def get_organization(
|
|||
# RBAC check
|
||||
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
|
||||
|
||||
org = OrganizationRead.from_orm(org)
|
||||
# Get org config
|
||||
statement = select(OrganizationConfig).where(OrganizationConfig.org_id == org.id)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
org_config = result.first()
|
||||
|
||||
if org_config is None:
|
||||
logging.error(f"Organization {org_id} has no config")
|
||||
|
||||
config = OrganizationConfig.from_orm(org_config) if org_config else {}
|
||||
|
||||
org = OrganizationRead(**org.dict(), config=config)
|
||||
|
||||
return org
|
||||
|
||||
|
|
@ -48,7 +71,7 @@ async def get_organization_by_slug(
|
|||
org_slug: str,
|
||||
db_session: Session,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
):
|
||||
) -> OrganizationRead:
|
||||
statement = select(Organization).where(Organization.slug == org_slug)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
|
|
@ -63,7 +86,18 @@ async def get_organization_by_slug(
|
|||
# RBAC check
|
||||
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
|
||||
|
||||
org = OrganizationRead.from_orm(org)
|
||||
# Get org config
|
||||
statement = select(OrganizationConfig).where(OrganizationConfig.org_id == org.id)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
org_config = result.first()
|
||||
|
||||
if org_config is None:
|
||||
logging.error(f"Organization {org_slug} has no config")
|
||||
|
||||
config = OrganizationConfig.from_orm(org_config) if org_config else {}
|
||||
|
||||
org = OrganizationRead(**org.dict(), config=config)
|
||||
|
||||
return org
|
||||
|
||||
|
|
@ -87,7 +121,7 @@ async def create_org(
|
|||
|
||||
org = Organization.from_orm(org_object)
|
||||
|
||||
if isinstance(current_user,AnonymousUser):
|
||||
if isinstance(current_user, AnonymousUser):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="You should be logged in to be able to achieve this action",
|
||||
|
|
@ -115,7 +149,146 @@ async def create_org(
|
|||
db_session.commit()
|
||||
db_session.refresh(user_org)
|
||||
|
||||
return OrganizationRead.from_orm(org)
|
||||
org_config = OrganizationConfigBase(
|
||||
GeneralConfig=GeneralConfig(
|
||||
color="#000000",
|
||||
limits=LimitSettings(
|
||||
limits_enabled=False,
|
||||
max_users=0,
|
||||
max_storage=0,
|
||||
max_staff=0,
|
||||
),
|
||||
users=OrgUserConfig(
|
||||
signup_mechanism="open",
|
||||
),
|
||||
active=True,
|
||||
),
|
||||
AIConfig=AIConfig(
|
||||
enabled=False,
|
||||
limits=AILimitsSettings(
|
||||
limits_enabled=False,
|
||||
max_asks=0,
|
||||
),
|
||||
embeddings="all-MiniLM-L6-v2",
|
||||
ai_model="gpt-3.5-turbo",
|
||||
features=AIEnabledFeatures(
|
||||
editor=False,
|
||||
activity_ask=False,
|
||||
course_ask=False,
|
||||
global_ai_ask=False,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
org_config = json.loads(org_config.json())
|
||||
|
||||
# OrgSettings
|
||||
org_settings = OrganizationConfig(
|
||||
org_id=int(org.id if org.id else 0),
|
||||
config=org_config,
|
||||
creation_date=str(datetime.now()),
|
||||
update_date=str(datetime.now()),
|
||||
)
|
||||
|
||||
db_session.add(org_settings)
|
||||
db_session.commit()
|
||||
db_session.refresh(org_settings)
|
||||
|
||||
# Get org config
|
||||
statement = select(OrganizationConfig).where(OrganizationConfig.org_id == org.id)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
org_config = result.first()
|
||||
|
||||
if org_config is None:
|
||||
logging.error(f"Organization {org.id} has no config")
|
||||
|
||||
config = OrganizationConfig.from_orm(org_config)
|
||||
|
||||
org = OrganizationRead(**org.dict(), config=config)
|
||||
|
||||
return org
|
||||
|
||||
|
||||
# Temporary pre-alpha code
|
||||
async def create_org_with_config(
|
||||
request: Request,
|
||||
org_object: OrganizationCreate,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
submitted_config: OrganizationConfigBase,
|
||||
):
|
||||
statement = select(Organization).where(Organization.slug == org_object.slug)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
org = result.first()
|
||||
|
||||
if org:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Organization already exists",
|
||||
)
|
||||
|
||||
org = Organization.from_orm(org_object)
|
||||
|
||||
if isinstance(current_user, AnonymousUser):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="You should be logged in to be able to achieve this action",
|
||||
)
|
||||
|
||||
# Complete the org object
|
||||
org.org_uuid = f"org_{uuid4()}"
|
||||
org.creation_date = str(datetime.now())
|
||||
org.update_date = str(datetime.now())
|
||||
|
||||
db_session.add(org)
|
||||
db_session.commit()
|
||||
db_session.refresh(org)
|
||||
|
||||
# Link user to org
|
||||
user_org = UserOrganization(
|
||||
user_id=int(current_user.id),
|
||||
org_id=int(org.id if org.id else 0),
|
||||
role_id=1,
|
||||
creation_date=str(datetime.now()),
|
||||
update_date=str(datetime.now()),
|
||||
)
|
||||
|
||||
db_session.add(user_org)
|
||||
db_session.commit()
|
||||
db_session.refresh(user_org)
|
||||
|
||||
org_config = submitted_config
|
||||
|
||||
org_config = json.loads(org_config.json())
|
||||
|
||||
# OrgSettings
|
||||
org_settings = OrganizationConfig(
|
||||
org_id=int(org.id if org.id else 0),
|
||||
config=org_config,
|
||||
creation_date=str(datetime.now()),
|
||||
update_date=str(datetime.now()),
|
||||
)
|
||||
|
||||
db_session.add(org_settings)
|
||||
db_session.commit()
|
||||
db_session.refresh(org_settings)
|
||||
|
||||
# Get org config
|
||||
statement = select(OrganizationConfig).where(OrganizationConfig.org_id == org.id)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
org_config = result.first()
|
||||
|
||||
if org_config is None:
|
||||
logging.error(f"Organization {org.id} has no config")
|
||||
|
||||
config = OrganizationConfig.from_orm(org_config)
|
||||
|
||||
org = OrganizationRead(**org.dict(), config=config)
|
||||
|
||||
return org
|
||||
|
||||
|
||||
async def update_org(
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from src.db.roles import Role, RoleRead
|
|||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
)
|
||||
from src.db.organizations import Organization, OrganizationRead
|
||||
from src.db.users import (
|
||||
AnonymousUser,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import { getActivityWithAuthHeader } from "@services/courses/activities";
|
|||
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth";
|
||||
import { getOrganizationContextInfo, getOrganizationContextInfoWithId } from "@services/organizations/orgs";
|
||||
import SessionProvider from "@components/Contexts/SessionContext";
|
||||
import EditorOptionsProvider from "@components/Contexts/Editor/EditorContext";
|
||||
import AIChatBotProvider from "@components/Contexts/AI/AIChatBotContext";
|
||||
import AIEditorProvider from "@components/Contexts/AI/AIEditorContext";
|
||||
|
||||
type MetadataProps = {
|
||||
params: { orgslug: string, courseid: string, activityid: string };
|
||||
|
|
@ -35,14 +38,15 @@ const EditActivity = async (params: any) => {
|
|||
const courseInfo = await getCourseMetadataWithAuthHeader(courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
|
||||
const activity = await getActivityWithAuthHeader(activityuuid, { revalidate: 0, tags: ['activities'] }, access_token ? access_token : null)
|
||||
const org = await getOrganizationContextInfoWithId(courseInfo.org_id, { revalidate: 1800, tags: ['organizations'] });
|
||||
console.log('courseInfo', courseInfo)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SessionProvider>
|
||||
<EditorWrapper org={org} course={courseInfo} activity={activity} content={activity.content}></EditorWrapper>
|
||||
</SessionProvider>
|
||||
</div>
|
||||
<EditorOptionsProvider options={{ isEditable: true }}>
|
||||
<AIEditorProvider>
|
||||
<SessionProvider>
|
||||
<EditorWrapper org={org} course={courseInfo} activity={activity} content={activity.content}></EditorWrapper>
|
||||
</SessionProvider>
|
||||
</AIEditorProvider>
|
||||
</EditorOptionsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
import "../styles/globals.css";
|
||||
import StyledComponentsRegistry from "../components/Utils/libs/styled-registry";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import Link from "next/link";
|
|||
import { getUriWithOrg } from "@services/config/config";
|
||||
import Canva from "@components/Objects/Activities/DynamicCanva/DynamicCanva";
|
||||
import VideoActivity from "@components/Objects/Activities/Video/Video";
|
||||
import { Check } from "lucide-react";
|
||||
import { Check, MoreVertical } from "lucide-react";
|
||||
import { markActivityAsComplete } from "@services/courses/activity";
|
||||
import DocumentPdfActivity from "@components/Objects/Activities/DocumentPdf/DocumentPdf";
|
||||
import ActivityIndicators from "@components/Pages/Courses/ActivityIndicators";
|
||||
|
|
@ -13,6 +13,8 @@ import AuthenticatedClientElement from "@components/Security/AuthenticatedClient
|
|||
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
|
||||
import { useOrg } from "@components/Contexts/OrgContext";
|
||||
import { CourseProvider } from "@components/Contexts/CourseContext";
|
||||
import AIActivityAsk from "@components/Objects/Activities/AI/AIActivityAsk";
|
||||
import AIChatBotProvider from "@components/Contexts/AI/AIChatBotContext";
|
||||
|
||||
interface ActivityClientProps {
|
||||
activityid: string;
|
||||
|
|
@ -31,14 +33,17 @@ function ActivityClient(props: ActivityClientProps) {
|
|||
const course = props.course;
|
||||
const org = useOrg() as any;
|
||||
|
||||
function getChapterName(chapterId: string) {
|
||||
let chapterName = "";
|
||||
course.chapters.forEach((chapter: any) => {
|
||||
if (chapter.id === chapterId) {
|
||||
chapterName = chapter.name;
|
||||
function getChapterNameByActivityId(course: any, activity_id: any) {
|
||||
for (let i = 0; i < course.chapters.length; i++) {
|
||||
let chapter = course.chapters[i];
|
||||
for (let j = 0; j < chapter.activities.length; j++) {
|
||||
let activity = chapter.activities[j];
|
||||
if (activity.id === activity_id) {
|
||||
return chapter.name;
|
||||
}
|
||||
}
|
||||
});
|
||||
return chapterName;
|
||||
}
|
||||
return null; // return null if no matching activity is found
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -46,47 +51,50 @@ function ActivityClient(props: ActivityClientProps) {
|
|||
return (
|
||||
<>
|
||||
<CourseProvider courseuuid={course?.course_uuid}>
|
||||
<GeneralWrapperStyled>
|
||||
<div className="space-y-4 pt-4">
|
||||
<div className="flex space-x-6">
|
||||
<div className="flex">
|
||||
<Link href={getUriWithOrg(orgslug, "") + `/course/${courseuuid}`}>
|
||||
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(org?.org_uuid, course.course_uuid, course.thumbnail_image)}`} alt="" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col -space-y-1">
|
||||
<p className="font-bold text-gray-700 text-md">Course </p>
|
||||
<h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase" >{course.name}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<ActivityIndicators course_uuid={courseuuid} current_activity={activityid} orgslug={orgslug} course={course} />
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex flex-col -space-y-1">
|
||||
<p className="font-bold text-gray-700 text-md">Chapter : {getChapterName(activity.coursechapter_id)}</p>
|
||||
<h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase" >{activity.name}</h1>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<AuthenticatedClientElement checkMethod="authentication">
|
||||
<MarkStatus activity={activity} activityid={activityid} course={course} orgslug={orgslug} />
|
||||
|
||||
</AuthenticatedClientElement>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activity ? (
|
||||
<div className={`p-7 pt-4 drop-shadow-sm rounded-lg ${activity.activity_type == 'TYPE_DYNAMIC' ? 'bg-white' : 'bg-zinc-950'}`}>
|
||||
<div>
|
||||
{activity.activity_type == "TYPE_DYNAMIC" && <Canva content={activity.content} activity={activity} />}
|
||||
{/* todo : use apis & streams instead of this */}
|
||||
{activity.activity_type == "TYPE_VIDEO" && <VideoActivity course={course} activity={activity} />}
|
||||
{activity.activity_type == "TYPE_DOCUMENT" && <DocumentPdfActivity course={course} activity={activity} />}
|
||||
<AIChatBotProvider>
|
||||
<GeneralWrapperStyled>
|
||||
<div className="space-y-4 pt-4">
|
||||
<div className="flex space-x-6">
|
||||
<div className="flex">
|
||||
<Link href={getUriWithOrg(orgslug, "") + `/course/${courseuuid}`}>
|
||||
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(org?.org_uuid, course.course_uuid, course.thumbnail_image)}`} alt="" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col -space-y-1">
|
||||
<p className="font-bold text-gray-700 text-md">Course </p>
|
||||
<h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase" >{course.name}</h1>
|
||||
</div>
|
||||
</div>
|
||||
) : (<div></div>)}
|
||||
{<div style={{ height: "100px" }}></div>}
|
||||
</div>
|
||||
</GeneralWrapperStyled>
|
||||
<ActivityIndicators course_uuid={courseuuid} current_activity={activityid} orgslug={orgslug} course={course} />
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex flex-col -space-y-1">
|
||||
<p className="font-bold text-gray-700 text-md">Chapter : {getChapterNameByActivityId(course, activity.id)}</p>
|
||||
<h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase" >{activity.name}</h1>
|
||||
</div>
|
||||
<div className="flex space-x-1 items-center">
|
||||
<AuthenticatedClientElement checkMethod="authentication">
|
||||
<AIActivityAsk activity={activity} />
|
||||
<MoreVertical size={17} className="text-gray-300 " />
|
||||
<MarkStatus activity={activity} activityid={activityid} course={course} orgslug={orgslug} />
|
||||
</AuthenticatedClientElement>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activity ? (
|
||||
<div className={`p-7 pt-4 drop-shadow-sm rounded-lg ${activity.activity_type == 'TYPE_DYNAMIC' ? 'bg-white' : 'bg-zinc-950'}`}>
|
||||
<div>
|
||||
{activity.activity_type == "TYPE_DYNAMIC" && <Canva content={activity.content} activity={activity} />}
|
||||
{/* todo : use apis & streams instead of this */}
|
||||
{activity.activity_type == "TYPE_VIDEO" && <VideoActivity course={course} activity={activity} />}
|
||||
{activity.activity_type == "TYPE_DOCUMENT" && <DocumentPdfActivity course={course} activity={activity} />}
|
||||
</div>
|
||||
</div>
|
||||
) : (<div></div>)}
|
||||
{<div style={{ height: "100px" }}></div>}
|
||||
</div>
|
||||
</GeneralWrapperStyled>
|
||||
</AIChatBotProvider>
|
||||
</CourseProvider>
|
||||
</>
|
||||
);
|
||||
|
|
@ -114,19 +122,19 @@ export function MarkStatus(props: { activity: any, activityid: string, course: a
|
|||
|
||||
return (
|
||||
<>{isActivityCompleted() ? (
|
||||
<div className="bg-teal-600 rounded-md drop-shadow-md flex flex-col p-3 text-sm text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out" >
|
||||
<div className="bg-teal-600 rounded-full px-5 drop-shadow-md flex items-center space-x-1 p-2.5 text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out" >
|
||||
<i>
|
||||
<Check size={15}></Check>
|
||||
<Check size={17}></Check>
|
||||
</i>{" "}
|
||||
Already completed
|
||||
<i className="not-italic text-xs font-bold">Already completed</i>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-zinc-600 rounded-md drop-shadow-md flex flex-col p-3 text-sm text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out" onClick={markActivityAsCompleteFront}>
|
||||
<div className="bg-gray-800 rounded-full px-5 drop-shadow-md flex items-center space-x-1 p-2.5 text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out" onClick={markActivityAsCompleteFront}>
|
||||
{" "}
|
||||
<i>
|
||||
<Check size={15}></Check>
|
||||
<Check size={17}></Check>
|
||||
</i>{" "}
|
||||
Mark as complete
|
||||
<i className="not-italic text-xs font-bold">Mark as complete</i>
|
||||
</div>
|
||||
)}</>
|
||||
)
|
||||
|
|
|
|||
45
apps/web/components/AI/Hooks/useGetAIFeatures.tsx
Normal file
45
apps/web/components/AI/Hooks/useGetAIFeatures.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import React from 'react'
|
||||
|
||||
interface UseGetAIFeatures {
|
||||
feature: 'editor' | 'activity_ask' | 'course_ask' | 'global_ai_ask',
|
||||
}
|
||||
|
||||
|
||||
function useGetAIFeatures(props: UseGetAIFeatures) {
|
||||
const org = useOrg() as any
|
||||
const [isEnabled, setisEnabled] = React.useState(false)
|
||||
|
||||
function checkAvailableAIFeaturesOnOrg(feature: string) {
|
||||
const config = org?.config?.config?.AIConfig;
|
||||
|
||||
if (!config) {
|
||||
console.log("AI or Organization config is not defined.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!config.enabled) {
|
||||
console.log("AI is not enabled for this Organization.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!config.features[feature]) {
|
||||
console.log(`Feature ${feature} is not enabled for this Organization.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (org) { // Check if org is not null or undefined
|
||||
let isEnabledStatus = checkAvailableAIFeaturesOnOrg(props.feature)
|
||||
setisEnabled(isEnabledStatus)
|
||||
}
|
||||
}, [org])
|
||||
|
||||
return isEnabled
|
||||
|
||||
}
|
||||
|
||||
export default useGetAIFeatures
|
||||
76
apps/web/components/Contexts/AI/AIChatBotContext.tsx
Normal file
76
apps/web/components/Contexts/AI/AIChatBotContext.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
'use client';
|
||||
import { AIMessage } from '@components/Objects/Activities/AI/AIActivityAsk';
|
||||
import React, { createContext, useContext, useReducer } from 'react'
|
||||
export const AIChatBotContext = createContext(null) as any;
|
||||
export const AIChatBotDispatchContext = createContext(null) as any;
|
||||
|
||||
export type AIChatBotStateTypes = {
|
||||
messages: AIMessage[],
|
||||
isModalOpen: boolean,
|
||||
aichat_uuid: string,
|
||||
isWaitingForResponse: boolean,
|
||||
chatInputValue: string
|
||||
error: AIError
|
||||
}
|
||||
|
||||
type AIError = {
|
||||
isError: boolean
|
||||
status: number
|
||||
error_message: string
|
||||
}
|
||||
|
||||
function AIChatBotProvider({ children }: { children: React.ReactNode }) {
|
||||
const [aiChatBotState, dispatchAIChatBot] = useReducer(aiChatBotReducer,
|
||||
{
|
||||
messages: [] as AIMessage[],
|
||||
isModalOpen: false,
|
||||
aichat_uuid: null,
|
||||
isWaitingForResponse: false,
|
||||
chatInputValue: '',
|
||||
error: { isError: false, status: 0, error_message: ' ' } as AIError
|
||||
}
|
||||
);
|
||||
return (
|
||||
<AIChatBotContext.Provider value={aiChatBotState}>
|
||||
<AIChatBotDispatchContext.Provider value={dispatchAIChatBot}>
|
||||
{children}
|
||||
</AIChatBotDispatchContext.Provider>
|
||||
</AIChatBotContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default AIChatBotProvider
|
||||
|
||||
export function useAIChatBot() {
|
||||
return useContext(AIChatBotContext);
|
||||
}
|
||||
|
||||
export function useAIChatBotDispatch() {
|
||||
return useContext(AIChatBotDispatchContext);
|
||||
}
|
||||
|
||||
function aiChatBotReducer(state: any, action: any) {
|
||||
switch (action.type) {
|
||||
case 'setMessages':
|
||||
return { ...state, messages: action.payload };
|
||||
case 'addMessage':
|
||||
return { ...state, messages: [...state.messages, action.payload] };
|
||||
case 'setIsModalOpen':
|
||||
return { ...state, isModalOpen: true };
|
||||
case 'setIsModalClose':
|
||||
return { ...state, isModalOpen: false };
|
||||
case 'setAichat_uuid':
|
||||
return { ...state, aichat_uuid: action.payload };
|
||||
case 'setIsWaitingForResponse':
|
||||
return { ...state, isWaitingForResponse: true };
|
||||
case 'setIsNoLongerWaitingForResponse':
|
||||
return { ...state, isWaitingForResponse: false };
|
||||
case 'setChatInputValue':
|
||||
return { ...state, chatInputValue: action.payload };
|
||||
case 'setError':
|
||||
return { ...state, error: action.payload };
|
||||
|
||||
default:
|
||||
throw new Error(`Unhandled action type: ${action.type}`)
|
||||
}
|
||||
}
|
||||
92
apps/web/components/Contexts/AI/AIEditorContext.tsx
Normal file
92
apps/web/components/Contexts/AI/AIEditorContext.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
'use client';
|
||||
import { AIMessage } from '@components/Objects/Activities/AI/AIActivityAsk';
|
||||
import React, { createContext, useContext, useReducer } from 'react'
|
||||
export const AIEditorContext = createContext(null) as any;
|
||||
export const AIEditorDispatchContext = createContext(null) as any;
|
||||
|
||||
export type AIEditorStateTypes = {
|
||||
|
||||
messages: AIMessage[],
|
||||
isModalOpen: boolean,
|
||||
isFeedbackModalOpen: boolean,
|
||||
aichat_uuid: string,
|
||||
isWaitingForResponse: boolean,
|
||||
chatInputValue: string,
|
||||
selectedTool: 'Writer' | 'ContinueWriting' | 'MakeLonger' | 'GenerateQuiz' | 'Translate'
|
||||
isUserInputEnabled: boolean
|
||||
error: AIError
|
||||
}
|
||||
|
||||
type AIError = {
|
||||
isError: boolean
|
||||
status: number
|
||||
error_message: string
|
||||
}
|
||||
|
||||
function AIEditorProvider({ children }: { children: React.ReactNode }) {
|
||||
const [aIEditorState, dispatchAIEditor] = useReducer(aIEditorReducer,
|
||||
{
|
||||
messages: [] as AIMessage[],
|
||||
isModalOpen: false,
|
||||
isFeedbackModalOpen: false,
|
||||
aichat_uuid: null,
|
||||
isWaitingForResponse: false,
|
||||
chatInputValue: '',
|
||||
selectedTool: 'Writer',
|
||||
isUserInputEnabled: true,
|
||||
error: { isError: false, status: 0, error_message: ' ' } as AIError
|
||||
}
|
||||
);
|
||||
return (
|
||||
<AIEditorContext.Provider value={aIEditorState}>
|
||||
<AIEditorDispatchContext.Provider value={dispatchAIEditor}>
|
||||
{children}
|
||||
</AIEditorDispatchContext.Provider>
|
||||
</AIEditorContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default AIEditorProvider
|
||||
|
||||
export function useAIEditor() {
|
||||
return useContext(AIEditorContext);
|
||||
}
|
||||
|
||||
export function useAIEditorDispatch() {
|
||||
return useContext(AIEditorDispatchContext);
|
||||
}
|
||||
|
||||
function aIEditorReducer(state: any, action: any) {
|
||||
switch (action.type) {
|
||||
case 'setMessages':
|
||||
return { ...state, messages: action.payload };
|
||||
case 'addMessage':
|
||||
return { ...state, messages: [...state.messages, action.payload] };
|
||||
case 'setIsModalOpen':
|
||||
return { ...state, isModalOpen: true };
|
||||
case 'setIsModalClose':
|
||||
return { ...state, isModalOpen: false };
|
||||
case 'setAichat_uuid':
|
||||
return { ...state, aichat_uuid: action.payload };
|
||||
case 'setIsWaitingForResponse':
|
||||
return { ...state, isWaitingForResponse: true };
|
||||
case 'setIsNoLongerWaitingForResponse':
|
||||
return { ...state, isWaitingForResponse: false };
|
||||
case 'setChatInputValue':
|
||||
return { ...state, chatInputValue: action.payload };
|
||||
case 'setSelectedTool':
|
||||
return { ...state, selectedTool: action.payload };
|
||||
case 'setIsFeedbackModalOpen':
|
||||
return { ...state, isFeedbackModalOpen: true };
|
||||
case 'setIsFeedbackModalClose':
|
||||
return { ...state, isFeedbackModalOpen: false };
|
||||
case 'setIsUserInputEnabled':
|
||||
return { ...state, isUserInputEnabled: action.payload };
|
||||
case 'setError':
|
||||
return { ...state, error: action.payload };
|
||||
|
||||
|
||||
default:
|
||||
throw new Error(`Unhandled action type: ${action.type}`)
|
||||
}
|
||||
}
|
||||
32
apps/web/components/Contexts/Editor/EditorContext.tsx
Normal file
32
apps/web/components/Contexts/Editor/EditorContext.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
'use client';
|
||||
import React, { useState } from 'react'
|
||||
|
||||
|
||||
export const EditorProviderContext = React.createContext(null) as any;
|
||||
|
||||
type EditorProviderProps = {
|
||||
children: React.ReactNode
|
||||
options: EditorProviderState
|
||||
}
|
||||
|
||||
type EditorProviderState = {
|
||||
isEditable: boolean
|
||||
}
|
||||
|
||||
function EditorOptionsProvider({ children, options }: EditorProviderProps) {
|
||||
const [editorOptions, setEditorOptions] = useState<EditorProviderState>(options);
|
||||
|
||||
return (
|
||||
<EditorProviderContext.Provider value={editorOptions}>
|
||||
{children}
|
||||
</EditorProviderContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditorOptionsProvider
|
||||
|
||||
export function useEditorProvider() {
|
||||
return React.useContext(EditorProviderContext);
|
||||
}
|
||||
|
||||
|
||||
325
apps/web/components/Objects/Activities/AI/AIActivityAsk.tsx
Normal file
325
apps/web/components/Objects/Activities/AI/AIActivityAsk.tsx
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
import { useSession } from '@components/Contexts/SessionContext'
|
||||
import { sendActivityAIChatMessage, startActivityAIChatSession } from '@services/ai/ai';
|
||||
import { AlertTriangle, BadgeInfo, NotebookTabs } from 'lucide-react';
|
||||
import Avvvatars from 'avvvatars-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { FlaskConical, Keyboard, MessageCircle, MessageSquareIcon, Sparkle, Sparkles, X } from 'lucide-react'
|
||||
import Image from 'next/image';
|
||||
import { send } from 'process';
|
||||
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
|
||||
import learnhouseAI_logo_black from "public/learnhouse_ai_black_logo.png";
|
||||
import React, { use, useEffect, useRef } from 'react'
|
||||
import { AIChatBotStateTypes, useAIChatBot, useAIChatBotDispatch } from '@components/Contexts/AI/AIChatBotContext';
|
||||
import FeedbackModal from '@components/Objects/Modals/Feedback/Feedback';
|
||||
import Modal from '@components/StyledElements/Modal/Modal';
|
||||
import useGetAIFeatures from '../../../AI/Hooks/useGetAIFeatures';
|
||||
|
||||
|
||||
type AIActivityAskProps = {
|
||||
activity: any;
|
||||
}
|
||||
|
||||
|
||||
function AIActivityAsk(props: AIActivityAskProps) {
|
||||
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'activity_ask' });
|
||||
const [isButtonAvailable, setIsButtonAvailable] = React.useState(false);
|
||||
const dispatchAIChatBot = useAIChatBotDispatch() as any;
|
||||
|
||||
useEffect(() => {
|
||||
if (is_ai_feature_enabled) {
|
||||
setIsButtonAvailable(true);
|
||||
}
|
||||
}
|
||||
, [is_ai_feature_enabled]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isButtonAvailable && (
|
||||
<div >
|
||||
<ActivityChatMessageBox activity={props.activity} />
|
||||
<div
|
||||
onClick={() => dispatchAIChatBot({ type: 'setIsModalOpen' })}
|
||||
style={{
|
||||
background: 'conic-gradient(from 32deg at 53.75% 50%, rgb(35, 40, 93) 4deg, rgba(20, 0, 52, 0.95) 59deg, rgba(164, 45, 238, 0.88) 281deg)',
|
||||
}}
|
||||
className="rounded-full px-5 drop-shadow-md flex items-center space-x-1.5 p-2.5 text-sm text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out hover:scale-105">
|
||||
{" "}
|
||||
<i>
|
||||
<Image className='outline outline-1 outline-neutral-200/20 rounded-md' width={20} src={learnhouseAI_icon} alt="" />
|
||||
</i>{" "}
|
||||
<i className="not-italic text-xs font-bold">Ask AI</i>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export type AIMessage = {
|
||||
sender: string;
|
||||
message: any;
|
||||
type: 'ai' | 'user';
|
||||
}
|
||||
|
||||
type ActivityChatMessageBoxProps = {
|
||||
activity: any;
|
||||
}
|
||||
|
||||
function ActivityChatMessageBox(props: ActivityChatMessageBoxProps) {
|
||||
const session = useSession() as any;
|
||||
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes;
|
||||
const dispatchAIChatBot = useAIChatBotDispatch() as any;
|
||||
|
||||
// TODO : come up with a better way to handle this
|
||||
const inputClass = aiChatBotState.isWaitingForResponse
|
||||
? 'ring-1 ring-inset ring-white/10 bg-gray-950/40 w-full rounded-lg outline-none px-4 py-2 text-white text-sm placeholder:text-white/30 opacity-30 '
|
||||
: 'ring-1 ring-inset ring-white/10 bg-gray-950/40 w-full rounded-lg outline-none px-4 py-2 text-white text-sm placeholder:text-white/30';
|
||||
|
||||
useEffect(() => {
|
||||
if (aiChatBotState.isModalOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = 'unset';
|
||||
}
|
||||
}, [aiChatBotState.isModalOpen]);
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
|
||||
if (event.key === 'Enter') {
|
||||
// Perform the sending action here
|
||||
sendMessage(event.currentTarget.value);
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: event.currentTarget.value });
|
||||
|
||||
}
|
||||
|
||||
const sendMessage = async (message: string) => {
|
||||
if (aiChatBotState.aichat_uuid) {
|
||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } });
|
||||
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' });
|
||||
const response = await sendActivityAIChatMessage(message, aiChatBotState.aichat_uuid, props.activity.activity_uuid)
|
||||
if (response.success == false) {
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
||||
await dispatchAIChatBot({ type: 'setError', payload: { isError: true, status: response.status, error_message: response.data.detail } });
|
||||
return;
|
||||
}
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'ai', message: response.data.message, type: 'ai' } });
|
||||
|
||||
} else {
|
||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } });
|
||||
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' });
|
||||
const response = await startActivityAIChatSession(message, props.activity.activity_uuid)
|
||||
if (response.success == false) {
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
||||
await dispatchAIChatBot({ type: 'setError', payload: { isError: true, status: response.status, error_message: response.data.detail } });
|
||||
return;
|
||||
}
|
||||
await dispatchAIChatBot({ type: 'setAichat_uuid', payload: response.data.aichat_uuid });
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'ai', message: response.data.message, type: 'ai' } });
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
dispatchAIChatBot({ type: 'setIsModalClose' });
|
||||
}
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (messagesEndRef.current) {
|
||||
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
}, [aiChatBotState.messages, session]);
|
||||
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{aiChatBotState.isModalOpen && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ y: 20, opacity: 0.3, filter: 'blur(5px)' }}
|
||||
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
||||
exit={{ y: 50, opacity: 0, filter: 'blur(25px)' }}
|
||||
transition={{ type: "spring", bounce: 0.35, duration: 1.7, mass: 0.2, velocity: 2 }}
|
||||
className='fixed top-0 left-0 w-full h-full z-50 flex justify-center items-center '
|
||||
style={{ pointerEvents: 'none' }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
pointerEvents: 'auto',
|
||||
background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(105.16% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgb(2 1 25 / 98%)'
|
||||
}}
|
||||
className="bg-black z-50 rounded-2xl max-w-screen-2xl w-10/12 my-10 mx-auto h-[350px] fixed bottom-0 left-1/2 transform -translate-x-1/2 shadow-lg ring-1 ring-inset ring-white/10 text-white p-4 flex-col-reverse backdrop-blur-md">
|
||||
<div className='flex flex-row-reverse pb-3 justify-between items-center'>
|
||||
<div className='flex space-x-2 items-center'>
|
||||
|
||||
<X size={20} className='text-white/50 hover:cursor-pointer bg-white/10 p-1 rounded-full items-center' onClick={closeModal} />
|
||||
|
||||
</div>
|
||||
<div className={`flex space-x-2 items-center -ml-[100px] ${aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''}`}>
|
||||
<Image className={`outline outline-1 outline-neutral-200/20 rounded-lg ${aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''}`} width={24} src={learnhouseAI_icon} alt="" />
|
||||
<span className='text-sm font-semibold text-white/70'> AI</span>
|
||||
</div>
|
||||
<div className='bg-white/5 text-white/40 py-0.5 px-3 flex space-x-1 rounded-full items-center'>
|
||||
<FlaskConical size={14} />
|
||||
<span className='text-xs font-semibold '>Experimental</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className={`w-100 h-0.5 bg-white/5 rounded-full mx-auto mb-3 ${aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''}`}></div>
|
||||
{aiChatBotState.messages.length > 0 && !aiChatBotState.error.isError ? (
|
||||
<div className='flex-col h-[237px] w-full space-y-4 overflow-scroll scrollbar-w-2 scrollbar scrollbar-thumb-white/20 scrollbar-thumb-rounded-full scrollbar-track-rounded-full'>
|
||||
{aiChatBotState.messages.map((message: AIMessage, index: number) => {
|
||||
return (
|
||||
<AIMessage key={index} message={message} animated={message.sender == 'ai' ? true : false} />
|
||||
)
|
||||
})}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
) : (
|
||||
<AIMessagePlaceHolder sendMessage={sendMessage} activity_uuid={props.activity.activity_uuid} />
|
||||
)}
|
||||
{aiChatBotState.error.isError && (
|
||||
<div className='flex items-center h-[237px]'>
|
||||
<div className='flex flex-col mx-auto w-[600px] space-y-2 p-5 rounded-lg bg-red-500/20 outline outline-1 outline-red-500'>
|
||||
<AlertTriangle size={20} className='text-red-500' />
|
||||
<div className='flex flex-col'>
|
||||
<h3 className='font-semibold text-red-200'>Something wrong happened</h3>
|
||||
<span className='text-red-100 text-sm '>{aiChatBotState.error.error_message}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
<div className='flex space-x-2 items-center'>
|
||||
<div className=''>
|
||||
<Avvvatars radius={3} border borderColor='white' borderSize={3} size={35} value={session.user.user_uuid} style="shape" />
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
<input onKeyDown={handleKeyDown} onChange={handleChange} disabled={aiChatBotState.isWaitingForResponse} value={aiChatBotState.chatInputValue} placeholder='Ask AI About this Lecture' type="text" className={inputClass} name="" id="" />
|
||||
|
||||
</div>
|
||||
<div className=''>
|
||||
<MessageCircle size={20} className='text-white/50 hover:cursor-pointer' onClick={() => sendMessage(aiChatBotState.chatInputValue)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
type AIMessageProps = {
|
||||
message: AIMessage;
|
||||
animated: boolean;
|
||||
}
|
||||
|
||||
function AIMessage(props: AIMessageProps) {
|
||||
const session = useSession() as any;
|
||||
|
||||
const words = props.message.message.split(' ');
|
||||
|
||||
return (
|
||||
<div className='flex space-x-2 w-full antialiased font-medium'>
|
||||
<div className=''>
|
||||
<Avvvatars radius={3} border borderColor='white' borderSize={3} size={35} value={props.message.type == 'ai' ? 'ai' : session.user.user_uuid} style="shape" />
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
<p className='w-full rounded-lg outline-none px-2 py-1 text-white text-md placeholder:text-white/30' id="">
|
||||
<AnimatePresence>
|
||||
{words.map((word: string, i: number) => (
|
||||
<motion.span
|
||||
key={i}
|
||||
initial={props.animated ? { opacity: 0, y: -10 } : { opacity: 1, y: 0 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={props.animated ? { opacity: 0, y: 10 } : { opacity: 1, y: 0 }}
|
||||
transition={props.animated ? { delay: i * 0.1 } : {}}
|
||||
>
|
||||
{word + ' '}
|
||||
</motion.span>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const AIMessagePlaceHolder = (props: { activity_uuid: string, sendMessage: any }) => {
|
||||
const session = useSession() as any;
|
||||
const [feedbackModal, setFeedbackModal] = React.useState(false);
|
||||
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes;
|
||||
|
||||
if (!aiChatBotState.error.isError) {
|
||||
return <div className='flex-col h-[237px] w-full'>
|
||||
<div className='flex flex-col text-center justify-center pt-12'>
|
||||
<motion.div
|
||||
initial={{ y: 20, opacity: 0, filter: 'blur(5px)' }}
|
||||
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
||||
exit={{ y: 50, opacity: 0, }}
|
||||
transition={{ type: "spring", bounce: 0.35, duration: 1.7, mass: 0.2, velocity: 2, delay: 0.17 }}
|
||||
|
||||
>
|
||||
|
||||
<Image width={100} className='mx-auto' src={learnhouseAI_logo_black} alt="" />
|
||||
<p className='pt-3 text-2xl font-semibold text-white/70 flex justify-center space-x-2 items-center'>
|
||||
<span className='items-center'>Hello</span>
|
||||
<span className='capitalize flex space-x-2 items-center'> <Avvvatars radius={3} border borderColor='white' borderSize={3} size={25} value={session.user.user_uuid} style="shape" />
|
||||
<span>{session.user.username},</span>
|
||||
</span>
|
||||
<span>how can we help today ?</span>
|
||||
</p>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ y: 20, opacity: 0, filter: 'blur(5px)' }}
|
||||
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
||||
exit={{ y: 50, opacity: 0, }}
|
||||
transition={{ type: "spring", bounce: 0.35, duration: 1.7, mass: 0.2, velocity: 2, delay: 0.27 }}
|
||||
|
||||
className='questions flex space-x-3 mx-auto pt-6 flex-wrap justify-center'
|
||||
>
|
||||
<AIChatPredefinedQuestion sendMessage={props.sendMessage} label='about' />
|
||||
<AIChatPredefinedQuestion sendMessage={props.sendMessage} label='flashcards' />
|
||||
<AIChatPredefinedQuestion sendMessage={props.sendMessage} label='examples' />
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
const AIChatPredefinedQuestion = (props: { sendMessage: any, label: string }) => {
|
||||
function getQuestion(label: string) {
|
||||
if (label === 'about') {
|
||||
return `What is this Activity about ?`
|
||||
} else if (label === 'flashcards') {
|
||||
return `Generate flashcards about this Activity`
|
||||
} else if (label === 'examples') {
|
||||
return `Explain this Activity in practical examples`
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div onClick={() => props.sendMessage(getQuestion(props.label))} className='flex space-x-1.5 items-center bg-white/5 cursor-pointer px-4 py-1.5 rounded-xl outline outline-1 outline-neutral-100/10 text-xs font-semibold text-white/40 hover:text-white/60 hover:bg-white/10 hover:outline-neutral-200/40 delay-75 ease-linear transition-all'>
|
||||
{props.label === 'about' && <BadgeInfo size={15} />}
|
||||
{props.label === 'flashcards' && <NotebookTabs size={15} />}
|
||||
{props.label === 'examples' && <div className='text-white/50'>Ex</div>}
|
||||
<span>{getQuestion(props.label)}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default AIActivityAsk
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
import React from 'react'
|
||||
import { Editor } from '@tiptap/core';
|
||||
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
|
||||
import Image from 'next/image';
|
||||
import { BookOpen, FormInput, Languages, MoreVertical } from 'lucide-react';
|
||||
import { BubbleMenu } from '@tiptap/react';
|
||||
import ToolTip from '@components/StyledElements/Tooltip/Tooltip';
|
||||
import { AIChatBotStateTypes, useAIChatBot, useAIChatBotDispatch } from '@components/Contexts/AI/AIChatBotContext';
|
||||
import { sendActivityAIChatMessage, startActivityAIChatSession } from '@services/ai/ai';
|
||||
import useGetAIFeatures from '../../../../AI/Hooks/useGetAIFeatures';
|
||||
|
||||
|
||||
|
||||
type AICanvaToolkitProps = {
|
||||
editor: Editor,
|
||||
activity: any
|
||||
}
|
||||
|
||||
function AICanvaToolkit(props: AICanvaToolkitProps) {
|
||||
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'activity_ask' });
|
||||
const [isBubbleMenuAvailable, setIsButtonAvailable] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (is_ai_feature_enabled) {
|
||||
setIsButtonAvailable(true);
|
||||
}
|
||||
}, [is_ai_feature_enabled])
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{isBubbleMenuAvailable && <BubbleMenu className="w-fit" tippyOptions={{ duration: 100 }} editor={props.editor}>
|
||||
<div style={{ background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(105.16% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgba(2, 1, 25, 0.98)' }}
|
||||
className='py-1 h-10 px-2 w-max text-white rounded-xl shadow-md cursor-pointer flex items-center space-x-2 antialiased'
|
||||
>
|
||||
<div className='flex w-full space-x-2 font-bold text-white/80'><Image className='outline outline-1 outline-neutral-200/10 rounded-lg' width={24} src={learnhouseAI_icon} alt="" /> <div>AI</div> </div>
|
||||
<div>
|
||||
<MoreVertical className='text-white/50' size={12} />
|
||||
</div>
|
||||
<div className='flex space-x-2'>
|
||||
<AIActionButton editor={props.editor} activity={props.activity} label='Explain' />
|
||||
<AIActionButton editor={props.editor} activity={props.activity} label='Summarize' />
|
||||
<AIActionButton editor={props.editor} activity={props.activity} label='Translate' />
|
||||
<AIActionButton editor={props.editor} activity={props.activity} label='Examples' />
|
||||
</div>
|
||||
</div>
|
||||
</BubbleMenu>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function AIActionButton(props: { editor: Editor, label: string, activity: any }) {
|
||||
const dispatchAIChatBot = useAIChatBotDispatch() as any;
|
||||
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes;
|
||||
|
||||
async function handleAction(label: string) {
|
||||
const selection = getTipTapEditorSelectedText();
|
||||
const prompt = getPrompt(label, selection);
|
||||
dispatchAIChatBot({ type: 'setIsModalOpen' });
|
||||
await sendMessage(prompt);
|
||||
}
|
||||
|
||||
const getTipTapEditorSelectedText = () => {
|
||||
const selection = props.editor.state.selection;
|
||||
const from = selection.from;
|
||||
const to = selection.to;
|
||||
const text = props.editor.state.doc.textBetween(from, to);
|
||||
return text;
|
||||
}
|
||||
|
||||
const getPrompt = (label: string, selection: string) => {
|
||||
if (label === 'Explain') {
|
||||
return `Explain this part of the course "${selection}" keep this course context in mind.`
|
||||
} else if (label === 'Summarize') {
|
||||
return `Summarize this "${selection}" with the course context in mind.`
|
||||
} else if (label === 'Translate') {
|
||||
return `Translate "${selection}" to another language.`
|
||||
} else {
|
||||
return `Give examples to understand "${selection}" better, if possible give context in the course.`
|
||||
}
|
||||
}
|
||||
|
||||
const sendMessage = async (message: string) => {
|
||||
if (aiChatBotState.aichat_uuid) {
|
||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } });
|
||||
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' });
|
||||
const response = await sendActivityAIChatMessage(message, aiChatBotState.aichat_uuid, props.activity.activity_uuid)
|
||||
if (response.success == false) {
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
||||
await dispatchAIChatBot({ type: 'setError', payload: { isError: true, status: response.status, error_message: response.data.detail } });
|
||||
return;
|
||||
}
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'ai', message: response.data.message, type: 'ai' } });
|
||||
|
||||
} else {
|
||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } });
|
||||
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' });
|
||||
const response = await startActivityAIChatSession(message, props.activity.activity_uuid)
|
||||
if (response.success == false) {
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
||||
await dispatchAIChatBot({ type: 'setError', payload: { isError: true, status: response.status, error_message: response.data.detail } });
|
||||
return;
|
||||
}
|
||||
await dispatchAIChatBot({ type: 'setAichat_uuid', payload: response.data.aichat_uuid });
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'ai', message: response.data.message, type: 'ai' } });
|
||||
}
|
||||
}
|
||||
|
||||
const tooltipLabel = props.label === 'Explain' ? 'Explain a word or a sentence with AI' : props.label === 'Summarize' ? 'Summarize a long paragraph or text with AI' : props.label === 'Translate' ? 'Translate to different languages with AI' : 'Give examples to understand better with AI'
|
||||
return (
|
||||
<div className='flex space-x-2' >
|
||||
<ToolTip sideOffset={10} slateBlack content={tooltipLabel}>
|
||||
<button onClick={() => handleAction(props.label)} className='flex space-x-1.5 items-center bg-white/10 px-2 py-0.5 rounded-md outline outline-1 outline-neutral-200/20 text-sm font-semibold text-white/70 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all'>
|
||||
{props.label === 'Explain' && <BookOpen size={16} />}
|
||||
{props.label === 'Summarize' && <FormInput size={16} />}
|
||||
{props.label === 'Translate' && <Languages size={16} />}
|
||||
{props.label === 'Examples' && <div className='text-white/50'>Ex</div>}
|
||||
<div>{props.label}</div>
|
||||
</button>
|
||||
</ToolTip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AICanvaToolkit
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEditor, EditorContent } from "@tiptap/react";
|
||||
import { useEditor, EditorContent, BubbleMenu, EditorProvider } from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import styled from "styled-components"
|
||||
import Youtube from "@tiptap/extension-youtube";
|
||||
|
|
@ -22,16 +22,24 @@ import ts from 'highlight.js/lib/languages/typescript'
|
|||
import html from 'highlight.js/lib/languages/xml'
|
||||
import python from 'highlight.js/lib/languages/python'
|
||||
import java from 'highlight.js/lib/languages/java'
|
||||
import { NoTextInput } from "@components/Objects/Editor/Extensions/NoTextInput/NoTextInput";
|
||||
import EditorOptionsProvider from "@components/Contexts/Editor/EditorContext";
|
||||
import AICanvaToolkit from "./AI/AICanvaToolkit";
|
||||
|
||||
|
||||
interface Editor {
|
||||
content: string;
|
||||
activity: any;
|
||||
//course: any;
|
||||
}
|
||||
|
||||
function Canva(props: Editor) {
|
||||
const isEditable = false;
|
||||
/**
|
||||
* Important Note : This is a workaround to enable user interaction features to be implemented easily, like text selection, AI features and other planned features, this is set to true but otherwise it should be set to false.
|
||||
* Another workaround is implemented below to disable the editor from being edited by the user by setting the caret-color to transparent and using a custom extension to filter out transactions that add/edit/remove text.
|
||||
* To let the various Custom Extensions know that the editor is not editable, React context (EditorOptionsProvider) will be used instead of props.extension.options.editable.
|
||||
*/
|
||||
const isEditable = true;
|
||||
|
||||
|
||||
// Code Block Languages for Lowlight
|
||||
lowlight.register('html', html)
|
||||
|
|
@ -46,6 +54,7 @@ function Canva(props: Editor) {
|
|||
editable: isEditable,
|
||||
extensions: [
|
||||
StarterKit,
|
||||
NoTextInput,
|
||||
// Custom Extensions
|
||||
InfoCallout.configure({
|
||||
editable: isEditable,
|
||||
|
|
@ -87,21 +96,54 @@ function Canva(props: Editor) {
|
|||
content: props.content,
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<CanvaWrapper>
|
||||
<EditorContent editor={editor} />
|
||||
</CanvaWrapper>
|
||||
|
||||
<EditorOptionsProvider options={{ isEditable: false }}>
|
||||
<CanvaWrapper>
|
||||
<AICanvaToolkit activity={props.activity} editor={editor} />
|
||||
<EditorContent editor={editor} />
|
||||
</CanvaWrapper>
|
||||
</EditorOptionsProvider>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
const CanvaWrapper = styled.div`
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
.bubble-menu {
|
||||
display: flex;
|
||||
background-color: #0D0D0D;
|
||||
padding: 0.2rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
button {
|
||||
border: none;
|
||||
background: none;
|
||||
color: #FFF;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
padding: 0 0.2rem;
|
||||
opacity: 0.6;
|
||||
|
||||
&:hover,
|
||||
&.is-active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// disable chrome outline
|
||||
|
||||
.ProseMirror {
|
||||
|
||||
// Workaround to disable editor from being edited by the user.
|
||||
caret-color: transparent;
|
||||
|
||||
h1 {
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
|
|
|
|||
462
apps/web/components/Objects/Editor/AI/AIEditorToolkit.tsx
Normal file
462
apps/web/components/Objects/Editor/AI/AIEditorToolkit.tsx
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
import React from 'react'
|
||||
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import Image from 'next/image';
|
||||
import { AlertTriangle, BetweenHorizontalStart, FastForward, Feather, FileStack, HelpCircle, Languages, MessageCircle, MoreVertical, Pen, X } from 'lucide-react';
|
||||
import { Editor } from '@tiptap/react';
|
||||
import { AIChatBotStateTypes, useAIChatBot, useAIChatBotDispatch } from '@components/Contexts/AI/AIChatBotContext';
|
||||
import { AIEditorStateTypes, useAIEditor, useAIEditorDispatch } from '@components/Contexts/AI/AIEditorContext';
|
||||
import { sendActivityAIChatMessage, startActivityAIChatSession } from '@services/ai/ai';
|
||||
import useGetAIFeatures from '@components/AI/Hooks/useGetAIFeatures';
|
||||
|
||||
type AIEditorToolkitProps = {
|
||||
editor: Editor,
|
||||
activity: any
|
||||
}
|
||||
|
||||
type AIPromptsLabels = {
|
||||
label: 'Writer' | 'ContinueWriting' | 'MakeLonger' | 'GenerateQuiz' | 'Translate',
|
||||
selection: string
|
||||
|
||||
}
|
||||
|
||||
function AIEditorToolkit(props: AIEditorToolkitProps) {
|
||||
const dispatchAIEditor = useAIEditorDispatch() as any;
|
||||
const aiEditorState = useAIEditor() as AIEditorStateTypes;
|
||||
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'editor' });
|
||||
const [isToolkitAvailable, setIsToolkitAvailable] = React.useState(true);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (is_ai_feature_enabled) {
|
||||
setIsToolkitAvailable(true);
|
||||
}
|
||||
}, [is_ai_feature_enabled])
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{isToolkitAvailable && <div className='flex space-x-2'>
|
||||
<AnimatePresence>
|
||||
{aiEditorState.isModalOpen && <motion.div
|
||||
initial={{ y: 20, opacity: 0.3, filter: 'blur(5px)' }}
|
||||
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
||||
exit={{ y: 50, opacity: 0, filter: 'blur(3px)' }}
|
||||
transition={{ type: "spring", bounce: 0.35, duration: 1.7, mass: 0.2, velocity: 2 }}
|
||||
className='fixed top-0 left-0 w-full h-full z-50 flex justify-center items-center '
|
||||
style={{ pointerEvents: 'none' }}
|
||||
>
|
||||
<>
|
||||
{aiEditorState.isFeedbackModalOpen && <UserFeedbackModal activity={props.activity} editor={props.editor} />}
|
||||
<div
|
||||
style={{
|
||||
pointerEvents: 'auto',
|
||||
background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(105.16% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgb(2 1 25 / 98%)'
|
||||
}}
|
||||
className="z-50 rounded-2xl max-w-screen-2xl my-10 mx-auto w-fit fixed bottom-0 left-1/2 transform -translate-x-1/2 shadow-xl ring-1 ring-inset ring-white/10 text-white p-3 flex-col-reverse backdrop-blur-md">
|
||||
<div className='flex space-x-2'>
|
||||
<div className='pr-1'>
|
||||
<div className='flex w-full space-x-2 font-bold text-white/80 items-center'>
|
||||
<Image className='outline outline-1 outline-neutral-200/20 rounded-lg' width={24} src={learnhouseAI_icon} alt="" />
|
||||
<div >AI Editor</div>
|
||||
<MoreVertical className='text-white/50' size={12} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='tools flex space-x-2'>
|
||||
<AiEditorToolButton label='Writer' />
|
||||
<AiEditorToolButton label='ContinueWriting' />
|
||||
<AiEditorToolButton label='MakeLonger' />
|
||||
|
||||
<AiEditorToolButton label='Translate' />
|
||||
</div>
|
||||
<div className='flex space-x-2 items-center'>
|
||||
<X onClick={() => Promise.all([dispatchAIEditor({ type: 'setIsModalClose' }), dispatchAIEditor({ type: 'setIsFeedbackModalClose' })])} size={20} className='text-white/50 hover:cursor-pointer bg-white/10 p-1 rounded-full items-center' />
|
||||
</div>
|
||||
</div>
|
||||
</div></>
|
||||
</motion.div>}
|
||||
</AnimatePresence>
|
||||
</div>}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const UserFeedbackModal = (props: AIEditorToolkitProps) => {
|
||||
const dispatchAIEditor = useAIEditorDispatch() as any;
|
||||
const aiEditorState = useAIEditor() as AIEditorStateTypes;
|
||||
|
||||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
await dispatchAIEditor({ type: 'setChatInputValue', payload: event.currentTarget.value });
|
||||
|
||||
}
|
||||
|
||||
const sendReqWithMessage = async (message: string) => {
|
||||
if (aiEditorState.aichat_uuid) {
|
||||
await dispatchAIEditor({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } });
|
||||
await dispatchAIEditor({ type: 'setIsWaitingForResponse' });
|
||||
const response = await sendActivityAIChatMessage(message, aiEditorState.aichat_uuid, props.activity.activity_uuid)
|
||||
if (response.success === false) {
|
||||
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
await dispatchAIEditor({ type: 'setIsModalClose' });
|
||||
// wait for 200ms before opening the modal again
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
await dispatchAIEditor({ type: 'setError', payload: { isError: true, status: response.status, error_message: response.data.detail } });
|
||||
await dispatchAIEditor({ type: 'setIsModalOpen' });
|
||||
return '';
|
||||
}
|
||||
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
await dispatchAIEditor({ type: 'setChatInputValue', payload: '' });
|
||||
await dispatchAIEditor({ type: 'addMessage', payload: { sender: 'ai', message: response.data.message, type: 'ai' } });
|
||||
return response.data.message;
|
||||
|
||||
} else {
|
||||
await dispatchAIEditor({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } });
|
||||
await dispatchAIEditor({ type: 'setIsWaitingForResponse' });
|
||||
const response = await startActivityAIChatSession(message, props.activity.activity_uuid)
|
||||
if (response.success === false) {
|
||||
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
await dispatchAIEditor({ type: 'setIsModalClose' });
|
||||
// wait for 200ms before opening the modal again
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
await dispatchAIEditor({ type: 'setError', payload: { isError: true, status: response.status, error_message: response.data.detail } });
|
||||
await dispatchAIEditor({ type: 'setIsModalOpen' });
|
||||
return '';
|
||||
}
|
||||
await dispatchAIEditor({ type: 'setAichat_uuid', payload: response.data.aichat_uuid });
|
||||
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
await dispatchAIEditor({ type: 'setChatInputValue', payload: '' });
|
||||
await dispatchAIEditor({ type: 'addMessage', payload: { sender: 'ai', message: response.data.message, type: 'ai' } });
|
||||
return response.data.message;
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyPress = async (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
await handleOperation(aiEditorState.selectedTool, aiEditorState.chatInputValue);
|
||||
}
|
||||
}
|
||||
|
||||
const handleOperation = async (label: 'Writer' | 'ContinueWriting' | 'MakeLonger' | 'GenerateQuiz' | 'Translate', message: string) => {
|
||||
// Set selected tool
|
||||
await dispatchAIEditor({ type: 'setSelectedTool', payload: label });
|
||||
|
||||
// Check what operation that was
|
||||
if (label === 'Writer') {
|
||||
let ai_message = '';
|
||||
let prompt = getPrompt({ label: label, selection: message });
|
||||
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: true });
|
||||
if (prompt) {
|
||||
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: false });
|
||||
await dispatchAIEditor({ type: 'setIsWaitingForResponse' });
|
||||
ai_message = await sendReqWithMessage(prompt);
|
||||
await fillEditorWithText(ai_message);
|
||||
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: true });
|
||||
}
|
||||
} else if (label === 'ContinueWriting') {
|
||||
let ai_message = '';
|
||||
let text_selection = getTipTapEditorSelectedTextGlobal();
|
||||
let prompt = getPrompt({ label: label, selection: text_selection });
|
||||
if (prompt) {
|
||||
await dispatchAIEditor({ type: 'setIsWaitingForResponse' });
|
||||
ai_message = await sendReqWithMessage(prompt);
|
||||
const message_without_original_text = await removeSentences(text_selection, ai_message);
|
||||
await fillEditorWithText(message_without_original_text);
|
||||
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
}
|
||||
} else if (label === 'MakeLonger') {
|
||||
let ai_message = '';
|
||||
let text_selection = getTipTapEditorSelectedText();
|
||||
let prompt = getPrompt({ label: label, selection: text_selection });
|
||||
if (prompt) {
|
||||
await dispatchAIEditor({ type: 'setIsWaitingForResponse' });
|
||||
ai_message = await sendReqWithMessage(prompt);
|
||||
await replaceSelectedTextWithText(ai_message);
|
||||
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
}
|
||||
} else if (label === 'GenerateQuiz') {
|
||||
// will be implemented in future stages
|
||||
} else if (label === 'Translate') {
|
||||
let ai_message = '';
|
||||
let text_selection = getTipTapEditorSelectedText();
|
||||
let prompt = getPrompt({ label: label, selection: text_selection });
|
||||
if (prompt) {
|
||||
await dispatchAIEditor({ type: 'setIsWaitingForResponse' });
|
||||
ai_message = await sendReqWithMessage(prompt);
|
||||
await replaceSelectedTextWithText(ai_message);
|
||||
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const removeSentences = async (textToRemove: string, originalText: string) => {
|
||||
const phrase = textToRemove.toLowerCase();
|
||||
const original = originalText.toLowerCase();
|
||||
|
||||
if (original.includes(phrase)) {
|
||||
const regex = new RegExp(phrase, 'g');
|
||||
const newText = original.replace(regex, '');
|
||||
return newText;
|
||||
} else {
|
||||
return originalText;
|
||||
}
|
||||
}
|
||||
|
||||
async function fillEditorWithText(text: string) {
|
||||
const words = text.split(' ');
|
||||
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
const textNode = {
|
||||
type: 'text',
|
||||
text: words[i],
|
||||
};
|
||||
|
||||
props.editor.chain().focus().insertContent(textNode).run();
|
||||
|
||||
// Add a space after each word except the last one
|
||||
if (i < words.length - 1) {
|
||||
const spaceNode = {
|
||||
type: 'text',
|
||||
text: ' ',
|
||||
};
|
||||
|
||||
props.editor.chain().focus().insertContent(spaceNode).run();
|
||||
}
|
||||
|
||||
// Wait for 0.3 seconds before adding the next word
|
||||
await new Promise(resolve => setTimeout(resolve, 120));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
async function replaceSelectedTextWithText(text: string) {
|
||||
const words = text.split(' ');
|
||||
|
||||
// Delete the selected text
|
||||
props.editor.chain().focus().deleteSelection().run();
|
||||
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
const textNode = {
|
||||
type: 'text',
|
||||
text: words[i],
|
||||
};
|
||||
|
||||
props.editor.chain().focus().insertContent(textNode).run();
|
||||
|
||||
// Add a space after each word except the last one
|
||||
if (i < words.length - 1) {
|
||||
const spaceNode = {
|
||||
type: 'text',
|
||||
text: ' ',
|
||||
};
|
||||
|
||||
props.editor.chain().focus().insertContent(spaceNode).run();
|
||||
}
|
||||
|
||||
// Wait for 0.3 seconds before adding the next word
|
||||
await new Promise(resolve => setTimeout(resolve, 120));
|
||||
}
|
||||
}
|
||||
|
||||
const getPrompt = (args: AIPromptsLabels) => {
|
||||
const { label, selection } = args;
|
||||
|
||||
if (label === 'Writer') {
|
||||
return `Write 3 sentences about ${selection}`;
|
||||
} else if (label === 'ContinueWriting') {
|
||||
return `Continue writing 3 more sentences based on "${selection}"`;
|
||||
} else if (label === 'MakeLonger') {
|
||||
return `Make longer this text longer : "${selection}"`;
|
||||
} else if (label === 'GenerateQuiz') {
|
||||
return `Generate a quiz about "${selection}", only return an array of objects, every object should respect the following interface:
|
||||
interface Answer {
|
||||
answer_id: string;
|
||||
answer: string;
|
||||
correct: boolean;
|
||||
}
|
||||
interface Question {
|
||||
question_id: string;
|
||||
question: string;
|
||||
type: "multiple_choice"
|
||||
answers: Answer[];
|
||||
}
|
||||
" `;
|
||||
} else if (label === 'Translate') {
|
||||
return `Translate "${selection}" to the ` + aiEditorState.chatInputValue + ` language`;
|
||||
}
|
||||
}
|
||||
|
||||
const getTipTapEditorSelectedTextGlobal = () => {
|
||||
// Get the entire node/paragraph that the user is in
|
||||
const pos = props.editor.state.selection.$from.pos; // get the cursor position
|
||||
const resolvedPos = props.editor.state.doc.resolve(pos); // resolve the position in the document
|
||||
const start = resolvedPos.before(1); // get the start position of the node
|
||||
const end = resolvedPos.after(1); // get the end position of the node
|
||||
const paragraph = props.editor.state.doc.textBetween(start, end, '\n', '\n'); // get the text of the node
|
||||
return paragraph;
|
||||
}
|
||||
|
||||
const getTipTapEditorSelectedText = () => {
|
||||
const selection = props.editor.state.selection;
|
||||
const from = selection.from;
|
||||
const to = selection.to;
|
||||
const text = props.editor.state.doc.textBetween(from, to);
|
||||
return text;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ y: 20, opacity: 0.3, filter: 'blur(5px)' }}
|
||||
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
||||
exit={{ y: 50, opacity: 0, filter: 'blur(3px)' }}
|
||||
transition={{ type: "spring", bounce: 0.35, duration: 1.7, mass: 0.2, velocity: 2 }}
|
||||
className='backdrop-blur-md fixed top-0 left-0 w-full h-full z-50 flex justify-center items-center '
|
||||
style={{ pointerEvents: 'none' }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
pointerEvents: 'auto',
|
||||
background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(105.16% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgb(2 1 25 / 95%)'
|
||||
}}
|
||||
className="backdrop-blur-md z-50 rounded-2xl max-w-screen-2xl my-10 mx-auto w-[500px] h-[200px] fixed bottom-16 left-1/2 transform -translate-x-1/2 shadow-xl ring-1 ring-inset ring-white/10 text-white p-3 flex-col-reverse">
|
||||
<div className='flex space-x-2 justify-center'>
|
||||
<Image className='outline outline-1 outline-neutral-200/20 rounded-lg' width={24} src={learnhouseAI_icon} alt="" />
|
||||
</div>
|
||||
<div className='flex h-[115px] justify-center mx-auto antialiased'>
|
||||
<div className='flex items-center justify-center '>
|
||||
<AiEditorActionScreen handleOperation={handleOperation} />
|
||||
</div>
|
||||
</div>
|
||||
{aiEditorState.isUserInputEnabled && !aiEditorState.error.isError && <div className="flex items-center space-x-2 cursor-pointer">
|
||||
<input onKeyDown={handleKeyPress} value={aiEditorState.chatInputValue} onChange={handleChange} placeholder='Ask AI' className='ring-1 ring-inset ring-white/20 w-full bg-gray-950/20 rounded-lg outline-none px-4 py-2 text-white text-sm placeholder:text-white/30'></input>
|
||||
<div onClick={() => handleOperation(aiEditorState.selectedTool, aiEditorState.chatInputValue)} className='bg-white/10 px-3 rounded-md outline outline-1 outline-neutral-200/20 py-2 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all'>
|
||||
<BetweenHorizontalStart size={20} className='text-white/50 hover:cursor-pointer' />
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
const AiEditorToolButton = (props: any) => {
|
||||
const dispatchAIEditor = useAIEditorDispatch() as any;
|
||||
const aiEditorState = useAIEditor() as AIEditorStateTypes;
|
||||
|
||||
const handleToolButtonClick = async (label: 'Writer' | 'ContinueWriting' | 'MakeLonger' | 'GenerateQuiz' | 'Translate') => {
|
||||
if (label === 'Writer') {
|
||||
await dispatchAIEditor({ type: 'setSelectedTool', payload: label });
|
||||
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: true });
|
||||
await dispatchAIEditor({ type: 'setIsFeedbackModalOpen' });
|
||||
}
|
||||
if (label === 'ContinueWriting') {
|
||||
await dispatchAIEditor({ type: 'setSelectedTool', payload: label });
|
||||
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: false });
|
||||
await dispatchAIEditor({ type: 'setIsFeedbackModalOpen' });
|
||||
}
|
||||
if (label === 'MakeLonger') {
|
||||
await dispatchAIEditor({ type: 'setSelectedTool', payload: label });
|
||||
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: false });
|
||||
await dispatchAIEditor({ type: 'setIsFeedbackModalOpen' });
|
||||
}
|
||||
if (label === 'GenerateQuiz') {
|
||||
await dispatchAIEditor({ type: 'setSelectedTool', payload: label });
|
||||
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: false });
|
||||
await dispatchAIEditor({ type: 'setIsFeedbackModalOpen' });
|
||||
}
|
||||
if (label === 'Translate') {
|
||||
await dispatchAIEditor({ type: 'setSelectedTool', payload: label });
|
||||
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: false });
|
||||
await dispatchAIEditor({ type: 'setIsFeedbackModalOpen' });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={() => handleToolButtonClick(props.label)} className='flex space-x-1.5 items-center bg-white/10 px-2 py-0.5 rounded-md outline outline-1 outline-neutral-200/20 text-sm font-semibold text-white/70 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all'>
|
||||
{props.label === 'Writer' && <Feather size={14} />}
|
||||
{props.label === 'ContinueWriting' && <FastForward size={14} />}
|
||||
{props.label === 'MakeLonger' && <FileStack size={14} />}
|
||||
{props.label === 'GenerateQuiz' && <HelpCircle size={14} />}
|
||||
{props.label === 'Translate' && <Languages size={14} />}
|
||||
<span>{props.label}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const AiEditorActionScreen = ({ handleOperation }: { handleOperation: any }) => {
|
||||
const dispatchAIEditor = useAIEditorDispatch() as any;
|
||||
const aiEditorState = useAIEditor() as AIEditorStateTypes;
|
||||
|
||||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
await dispatchAIEditor({ type: 'setChatInputValue', payload: event.currentTarget.value });
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{aiEditorState.selectedTool === 'Writer' && !aiEditorState.isWaitingForResponse && !aiEditorState.error.isError &&
|
||||
<div className='text-xl text-white/90 font-extrabold space-x-2'>
|
||||
<span>Write about...</span>
|
||||
</div>}
|
||||
{aiEditorState.selectedTool === 'ContinueWriting' && !aiEditorState.isWaitingForResponse && !aiEditorState.error.isError &&
|
||||
<div className='flex flex-col mx-auto justify-center align-middle items-center'>
|
||||
<p className='mx-auto flex p-2 text-white/80 mt-4 font-bold justify-center text-sm align-middle'>Place your cursor at the end of a sentence to continue writing </p>
|
||||
<div onClick={() => {
|
||||
handleOperation(aiEditorState.selectedTool, aiEditorState.chatInputValue)
|
||||
}} className='flex cursor-pointer space-x-1.5 p-4 mt-4 items-center bg-white/10 rounded-md outline outline-1 outline-neutral-200/20 text-2xl font-semibold text-white/70 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all'>
|
||||
|
||||
<FastForward size={24} />
|
||||
</div>
|
||||
</div>}
|
||||
{aiEditorState.selectedTool === 'MakeLonger' && !aiEditorState.isWaitingForResponse && !aiEditorState.error.isError &&
|
||||
<div className='flex flex-col mx-auto justify-center align-middle items-center'>
|
||||
<p className='mx-auto flex p-2 text-white/80 mt-4 font-bold justify-center text-sm align-middle'>Select text to make longer </p>
|
||||
<div onClick={() => {
|
||||
handleOperation(aiEditorState.selectedTool, aiEditorState.chatInputValue)
|
||||
}} className='flex cursor-pointer space-x-1.5 p-4 mt-4 items-center bg-white/10 rounded-md outline outline-1 outline-neutral-200/20 text-2xl font-semibold text-white/70 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all'>
|
||||
|
||||
<FileStack size={24} />
|
||||
</div>
|
||||
</div>}
|
||||
{aiEditorState.selectedTool === 'Translate' && !aiEditorState.isWaitingForResponse && !aiEditorState.error.isError &&
|
||||
<div className='flex flex-col mx-auto justify-center align-middle items-center'>
|
||||
<div className='mx-auto flex p-2 text-white/80 mt-4 font-bold justify-center text-sm align-middle space-x-6'>
|
||||
<p>Translate selected text to </p>
|
||||
<input value={aiEditorState.chatInputValue} onChange={handleChange} placeholder='Japanese, Arabic, German, etc. ' className='ring-1 ring-inset ring-white/20 w-full bg-gray-950/20 rounded-lg outline-none px-4 py- text-white text-sm placeholder:text-white/30'></input>
|
||||
</div>
|
||||
<div onClick={() => {
|
||||
handleOperation(aiEditorState.selectedTool, aiEditorState.chatInputValue)
|
||||
}} className='flex cursor-pointer space-x-1.5 p-4 mt-4 items-center bg-white/10 rounded-md outline outline-1 outline-neutral-200/20 text-2xl font-semibold text-white/70 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all'>
|
||||
|
||||
<Languages size={24} />
|
||||
</div>
|
||||
</div>}
|
||||
{aiEditorState.isWaitingForResponse && !aiEditorState.error.isError && <div className='flex flex-col mx-auto justify-center align-middle items-center'>
|
||||
<svg className="animate-spin mt-10 h-10 w-10 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<p className='font-bold mt-4 text-white/90'>Thinking...</p>
|
||||
</div>}
|
||||
|
||||
{aiEditorState.error.isError && (
|
||||
<div className='flex items-center h-auto pt-7'>
|
||||
<div className='flex flex-col mx-auto w-full space-y-2 p-5 rounded-lg bg-red-500/20 outline outline-1 outline-red-500'>
|
||||
<AlertTriangle size={20} className='text-red-500' />
|
||||
<div className='flex flex-col'>
|
||||
<h3 className='font-semibold text-red-200'>Something wrong happened</h3>
|
||||
<span className='text-red-100 text-sm '>{aiEditorState.error.error_message}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default AIEditorToolkit
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
import React from "react";
|
||||
import { useEditor, EditorContent } from "@tiptap/react";
|
||||
import { useEditor, EditorContent, BubbleMenu } from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import learnhouseIcon from "public/learnhouse_icon.png";
|
||||
import { ToolbarButtons } from "./Toolbar/ToolbarButtons";
|
||||
|
|
@ -9,6 +9,9 @@ import Image from "next/image";
|
|||
import styled from "styled-components";
|
||||
import { DividerVerticalIcon, SlashIcon } from "@radix-ui/react-icons";
|
||||
import Avvvatars from "avvvatars-react";
|
||||
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
|
||||
import { AIEditorStateTypes, useAIEditor, useAIEditorDispatch } from "@components/Contexts/AI/AIEditorContext";
|
||||
|
||||
// extensions
|
||||
import InfoCallout from "./Extensions/Callout/Info/InfoCallout";
|
||||
import WarningCallout from "./Extensions/Callout/Warning/WarningCallout";
|
||||
|
|
@ -36,8 +39,9 @@ import html from 'highlight.js/lib/languages/xml'
|
|||
import python from 'highlight.js/lib/languages/python'
|
||||
import java from 'highlight.js/lib/languages/java'
|
||||
import { CourseProvider } from "@components/Contexts/CourseContext";
|
||||
import { OrgProvider } from "@components/Contexts/OrgContext";
|
||||
import { useSession } from "@components/Contexts/SessionContext";
|
||||
import AIEditorToolkit from "./AI/AIEditorToolkit";
|
||||
import useGetAIFeatures from "@components/AI/Hooks/useGetAIFeatures";
|
||||
|
||||
|
||||
interface Editor {
|
||||
|
|
@ -52,6 +56,17 @@ interface Editor {
|
|||
|
||||
function Editor(props: Editor) {
|
||||
const session = useSession() as any;
|
||||
const dispatchAIEditor = useAIEditorDispatch() as any;
|
||||
const aiEditorState = useAIEditor() as AIEditorStateTypes;
|
||||
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'editor' });
|
||||
const [isButtonAvailable, setIsButtonAvailable] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (is_ai_feature_enabled) {
|
||||
setIsButtonAvailable(true);
|
||||
}
|
||||
}, [is_ai_feature_enabled])
|
||||
|
||||
// remove course_ from course_uuid
|
||||
const course_uuid = props.course.course_uuid.substring(7);
|
||||
|
||||
|
|
@ -129,7 +144,6 @@ function Editor(props: Editor) {
|
|||
|
||||
return (
|
||||
<Page>
|
||||
<OrgProvider orgslug={props.org?.slug}>
|
||||
<CourseProvider courseuuid={props.course.course_uuid}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.98 }}
|
||||
|
|
@ -156,19 +170,30 @@ function Editor(props: Editor) {
|
|||
{" "}
|
||||
<b>{props.course.name}</b> <SlashIcon /> {props.activity.name}{" "}
|
||||
</EditorInfoDocName>
|
||||
|
||||
</EditorInfoWrapper>
|
||||
<EditorButtonsWrapper>
|
||||
<ToolbarButtons editor={editor} />
|
||||
</EditorButtonsWrapper>
|
||||
</EditorDocSection>
|
||||
<EditorUsersSection>
|
||||
<EditorUserProfileWrapper>
|
||||
{!session.isAuthenticated && <span>Loading</span>}
|
||||
{session.isAuthenticated && <Avvvatars value={session.user.user_uuid} style="shape" />}
|
||||
</EditorUserProfileWrapper>
|
||||
<EditorUsersSection className="space-x-2">
|
||||
<div>
|
||||
<div className="transition-all ease-linear text-teal-100 rounded-md hover:cursor-pointer" >
|
||||
{isButtonAvailable && <div
|
||||
onClick={() => dispatchAIEditor({ type: aiEditorState.isModalOpen ? 'setIsModalClose' : 'setIsModalOpen' })}
|
||||
style={{
|
||||
background: 'conic-gradient(from 32deg at 53.75% 50%, rgb(35, 40, 93) 4deg, rgba(20, 0, 52, 0.95) 59deg, rgba(164, 45, 238, 0.88) 281deg)',
|
||||
}}
|
||||
className="rounded-md px-3 py-2 drop-shadow-md flex items-center space-x-1.5 text-sm text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out hover:scale-105">
|
||||
{" "}
|
||||
<i>
|
||||
<Image className='' width={20} src={learnhouseAI_icon} alt="" />
|
||||
</i>{" "}
|
||||
<i className="not-italic text-xs font-bold">AI Editor</i>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
<DividerVerticalIcon style={{ marginTop: "auto", marginBottom: "auto", color: "grey", opacity: '0.5' }} />
|
||||
<EditorLeftOptionsSection className="space-x-2 pl-2 pr-3">
|
||||
<EditorLeftOptionsSection className="space-x-2 ">
|
||||
<div className="bg-sky-600 hover:bg-sky-700 transition-all ease-linear px-3 py-2 font-black text-sm shadow text-teal-100 rounded-lg hover:cursor-pointer" onClick={() => props.setContent(editor.getJSON())}> Save </div>
|
||||
<ToolTip content="Preview">
|
||||
<Link target="_blank" href={`/course/${course_uuid}/activity/${activity_uuid}`}>
|
||||
|
|
@ -178,6 +203,13 @@ function Editor(props: Editor) {
|
|||
</Link>
|
||||
</ToolTip>
|
||||
</EditorLeftOptionsSection>
|
||||
<DividerVerticalIcon style={{ marginTop: "auto", marginBottom: "auto", color: "grey", opacity: '0.5' }} />
|
||||
|
||||
<EditorUserProfileWrapper>
|
||||
{!session.isAuthenticated && <span>Loading</span>}
|
||||
{session.isAuthenticated && <Avvvatars value={session.user.user_uuid} style="shape" />}
|
||||
</EditorUserProfileWrapper>
|
||||
|
||||
</EditorUsersSection>
|
||||
</EditorTop>
|
||||
</motion.div>
|
||||
|
|
@ -193,11 +225,11 @@ function Editor(props: Editor) {
|
|||
exit={{ opacity: 0 }}
|
||||
>
|
||||
<EditorContentWrapper>
|
||||
<AIEditorToolkit activity={props.activity} editor={editor} />
|
||||
<EditorContent editor={editor} />
|
||||
</EditorContentWrapper>
|
||||
</motion.div>
|
||||
</CourseProvider>
|
||||
</OrgProvider>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import Editor from "./Editor";
|
|||
import { updateActivity } from "@services/courses/activities";
|
||||
import { toast } from "react-hot-toast";
|
||||
import Toast from "@components/StyledElements/Toast/Toast";
|
||||
import { OrgProvider } from "@components/Contexts/OrgContext";
|
||||
|
||||
interface EditorWrapperProps {
|
||||
content: string;
|
||||
|
|
@ -26,7 +27,7 @@ function EditorWrapper(props: EditorWrapperProps): JSX.Element {
|
|||
// setProviderState(provider);
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -50,8 +51,9 @@ function EditorWrapper(props: EditorWrapperProps): JSX.Element {
|
|||
} else {
|
||||
return <>
|
||||
<Toast></Toast>
|
||||
<Editor org={props.org} course={props.course} activity={props.activity} content={props.content} setContent={setContent} provider={providerState} ydoc={ydocState}></Editor>;
|
||||
|
||||
<OrgProvider orgslug={props.org.slug}>
|
||||
<Editor org={props.org} course={props.course} activity={props.activity} content={props.content} setContent={setContent} provider={providerState} ydoc={ydocState}></Editor>;
|
||||
</OrgProvider>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
|
||||
import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
function InfoCalloutComponent(props: any) {
|
||||
const editorState = useEditorProvider() as any;
|
||||
const isEditable = editorState.isEditable;
|
||||
|
||||
return (
|
||||
<NodeViewWrapper>
|
||||
<InfoCalloutWrapper className="flex space-x-2 items-center bg-blue-200 rounded-lg text-blue-900 px-3 shadow-inner" contentEditable={props.extension.options.editable}>
|
||||
<AlertCircle /> <NodeViewContent contentEditable={props.extension.options.editable} className="content" />
|
||||
<InfoCalloutWrapper className="flex space-x-2 items-center bg-blue-200 rounded-lg text-blue-900 px-3 shadow-inner" contentEditable={isEditable}>
|
||||
<AlertCircle /> <NodeViewContent contentEditable={isEditable} className="content" />
|
||||
</InfoCalloutWrapper>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ export default Node.create({
|
|||
group: "block",
|
||||
draggable: true,
|
||||
content: "text*",
|
||||
marks: "",
|
||||
defining: true,
|
||||
|
||||
// TODO : multi line support
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
|
||||
import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
function WarningCalloutComponent(props: any) {
|
||||
const editorState = useEditorProvider() as any;
|
||||
const isEditable = editorState.isEditable;
|
||||
|
||||
return (
|
||||
<NodeViewWrapper>
|
||||
<CalloutWrapper className="flex space-x-2 items-center bg-yellow-200 rounded-lg text-yellow-900 px-3 shadow-inner" contentEditable={props.extension.options.editable}>
|
||||
<AlertTriangle /> <NodeViewContent contentEditable={props.extension.options.editable} className="content" />
|
||||
<CalloutWrapper className="flex space-x-2 items-center bg-yellow-200 rounded-lg text-yellow-900 px-3 shadow-inner" contentEditable={isEditable}>
|
||||
<AlertTriangle /> <NodeViewContent contentEditable={isEditable} className="content" />
|
||||
</CalloutWrapper>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,10 +8,14 @@ import { UploadIcon } from "@radix-ui/react-icons";
|
|||
import { getActivityBlockMediaDirectory } from "@services/media/media";
|
||||
import { useOrg } from "@components/Contexts/OrgContext";
|
||||
import { useCourse } from "@components/Contexts/CourseContext";
|
||||
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
|
||||
|
||||
function ImageBlockComponent(props: any) {
|
||||
const org = useOrg() as any;
|
||||
const course = useCourse() as any;
|
||||
const editorState = useEditorProvider() as any;
|
||||
|
||||
const isEditable = editorState.isEditable;
|
||||
const [image, setImage] = React.useState(null);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [blockObject, setblockObject] = React.useState(props.node.attrs.blockObject);
|
||||
|
|
@ -39,8 +43,8 @@ function ImageBlockComponent(props: any) {
|
|||
|
||||
return (
|
||||
<NodeViewWrapper className="block-image">
|
||||
{!blockObject && props.extension.options.editable && (
|
||||
<BlockImageWrapper className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2" contentEditable={props.extension.options.editable}>
|
||||
{!blockObject && isEditable && (
|
||||
<BlockImageWrapper className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2" contentEditable={isEditable}>
|
||||
{isLoading ? (
|
||||
<Loader className="animate-spin animate-pulse text-gray-200" size={50} />
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ import "katex/dist/katex.min.css";
|
|||
import { InlineMath, BlockMath } from "react-katex";
|
||||
import { Edit, Save } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
|
||||
|
||||
function MathEquationBlockComponent(props: any) {
|
||||
const [equation, setEquation] = React.useState(props.node.attrs.math_equation);
|
||||
const [isEditing, setIsEditing] = React.useState(true);
|
||||
const isEditable = props.extension.options.editable;
|
||||
const editorState = useEditorProvider() as any;
|
||||
const isEditable = editorState.isEditable;
|
||||
|
||||
const handleEquationChange = (event: React.ChangeEvent<any>) => {
|
||||
setEquation(event.target.value);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import { Extension } from '@tiptap/core';
|
||||
import { Plugin, PluginKey } from 'prosemirror-state';
|
||||
|
||||
export const NoTextInput = Extension.create({
|
||||
name: 'noTextInput',
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('noTextInput'),
|
||||
filterTransaction: (transaction) => {
|
||||
// If the transaction is adding text, stop it
|
||||
return !transaction.docChanged || transaction.steps.every((step) => {
|
||||
const { slice } = step.toJSON();
|
||||
return !slice || !slice.content.some((node: { type: string; }) => node.type === 'text');
|
||||
});
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
|
@ -8,6 +8,7 @@ import { UploadIcon } from "@radix-ui/react-icons";
|
|||
import { getActivityBlockMediaDirectory } from "@services/media/media";
|
||||
import { useOrg } from "@components/Contexts/OrgContext";
|
||||
import { useCourse } from "@components/Contexts/CourseContext";
|
||||
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
|
||||
|
||||
function PDFBlockComponent(props: any) {
|
||||
const org = useOrg() as any;
|
||||
|
|
@ -16,6 +17,8 @@ function PDFBlockComponent(props: any) {
|
|||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [blockObject, setblockObject] = React.useState(props.node.attrs.blockObject);
|
||||
const fileId = blockObject ? `${blockObject.content.file_id}.${blockObject.content.file_format}` : null;
|
||||
const editorState = useEditorProvider() as any;
|
||||
const isEditable = editorState.isEditable;
|
||||
|
||||
const handlePDFChange = (event: React.ChangeEvent<any>) => {
|
||||
setPDF(event.target.files[0]);
|
||||
|
|
@ -39,7 +42,7 @@ function PDFBlockComponent(props: any) {
|
|||
return (
|
||||
<NodeViewWrapper className="block-pdf">
|
||||
{!blockObject && (
|
||||
<BlockPDFWrapper className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2" contentEditable={props.extension.options.editable}>
|
||||
<BlockPDFWrapper className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2" contentEditable={isEditable}>
|
||||
{isLoading ? (
|
||||
<Loader className="animate-spin animate-pulse text-gray-200" size={50} />
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { twJoin, twMerge } from 'tailwind-merge'
|
|||
import React from "react";
|
||||
import { BadgeHelp, Check, Info, Minus, MoreVertical, Plus, RefreshCcw, X } from "lucide-react";
|
||||
import ReactConfetti from "react-confetti";
|
||||
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
|
||||
|
||||
interface Answer {
|
||||
answer_id: string;
|
||||
|
|
@ -22,7 +23,8 @@ function QuizBlockComponent(props: any) {
|
|||
const [userAnswers, setUserAnswers] = React.useState([]) as [any[], any];
|
||||
const [submitted, setSubmitted] = React.useState(false) as [boolean, any];
|
||||
const [submissionMessage, setSubmissionMessage] = React.useState("") as [string, any];
|
||||
const isEditable = props.extension.options.editable;
|
||||
const editorState = useEditorProvider() as any;
|
||||
const isEditable = editorState.isEditable;
|
||||
|
||||
const handleAnswerClick = (question_id: string, answer_id: string) => {
|
||||
// if the quiz is submitted, do nothing
|
||||
|
|
|
|||
|
|
@ -8,10 +8,13 @@ import { getActivityBlockMediaDirectory } from "@services/media/media";
|
|||
import { UploadIcon } from "@radix-ui/react-icons";
|
||||
import { useOrg } from "@components/Contexts/OrgContext";
|
||||
import { useCourse } from "@components/Contexts/CourseContext";
|
||||
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
|
||||
|
||||
function VideoBlockComponents(props: any) {
|
||||
const org = useOrg() as any;
|
||||
const course = useCourse() as any;
|
||||
const editorState = useEditorProvider() as any;
|
||||
const isEditable = editorState.isEditable;
|
||||
const [video, setVideo] = React.useState(null);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [blockObject, setblockObject] = React.useState(props.node.attrs.blockObject);
|
||||
|
|
@ -39,7 +42,7 @@ function VideoBlockComponents(props: any) {
|
|||
return (
|
||||
<NodeViewWrapper className="block-video">
|
||||
{!blockObject && (
|
||||
<BlockVideoWrapper className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2" contentEditable={props.extension.options.editable}>
|
||||
<BlockVideoWrapper className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2" contentEditable={isEditable}>
|
||||
{isLoading ? (
|
||||
<Loader className="animate-spin animate-pulse text-gray-200" size={50} />
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import styled from "styled-components";
|
||||
import { FontBoldIcon, FontItalicIcon, StrikethroughIcon, ArrowLeftIcon, ArrowRightIcon, OpacityIcon, DividerVerticalIcon, ListBulletIcon } from "@radix-ui/react-icons";
|
||||
import { AlertCircle, AlertTriangle, BadgeHelp, Code, FileText, GraduationCap, HelpCircle, ImagePlus, Info, ListChecks, Sigma, Video, Youtube } from "lucide-react";
|
||||
import { FontBoldIcon, FontItalicIcon, StrikethroughIcon, ArrowLeftIcon, ArrowRightIcon, DividerVerticalIcon, ListBulletIcon } from "@radix-ui/react-icons";
|
||||
import { AlertCircle, AlertTriangle, BadgeHelp, Code, FileText, ImagePlus, Sigma, Video, Youtube } from "lucide-react";
|
||||
import ToolTip from "@components/StyledElements/Tooltip/Tooltip";
|
||||
|
||||
export const ToolbarButtons = ({ editor, props }: any) => {
|
||||
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// YouTube extension
|
||||
|
||||
const addYoutubeVideo = () => {
|
||||
const url = prompt("Enter YouTube URL");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
'use client';
|
||||
import React, { use, useEffect } from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { getAPIUrl, getUriWithOrg } from "@services/config/config";
|
||||
import { getOrganizationContextInfo, getOrganizationContextInfoWithoutCredentials } from "@services/organizations/orgs";
|
||||
|
|
|
|||
84
apps/web/package-lock.json
generated
84
apps/web/package-lock.json
generated
|
|
@ -28,7 +28,7 @@
|
|||
"formik": "^2.2.9",
|
||||
"framer-motion": "^10.16.1",
|
||||
"lowlight": "^3.0.0",
|
||||
"lucide-react": "^0.268.0",
|
||||
"lucide-react": "^0.307.0",
|
||||
"next": "14.0.4",
|
||||
"re-resizable": "^6.9.9",
|
||||
"react": "^18.2.0",
|
||||
|
|
@ -42,6 +42,7 @@
|
|||
"styled-components": "^6.0.0-beta.9",
|
||||
"swr": "^2.2.4",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwind-scrollbar": "^3.0.5",
|
||||
"uuid": "^9.0.0",
|
||||
"y-indexeddb": "^9.0.9",
|
||||
"y-webrtc": "^10.2.3",
|
||||
|
|
@ -77,7 +78,6 @@
|
|||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
|
|
@ -2446,7 +2446,6 @@
|
|||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "2.0.5",
|
||||
"run-parallel": "^1.1.9"
|
||||
|
|
@ -2459,7 +2458,6 @@
|
|||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
|
|
@ -2468,7 +2466,6 @@
|
|||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@nodelib/fs.scandir": "2.1.5",
|
||||
"fastq": "^1.6.0"
|
||||
|
|
@ -4263,14 +4260,12 @@
|
|||
"node_modules/any-promise": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
|
|
@ -4282,8 +4277,7 @@
|
|||
"node_modules/arg": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
||||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
|
|
@ -4606,7 +4600,6 @@
|
|||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"devOptional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
|
|
@ -4624,7 +4617,6 @@
|
|||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
},
|
||||
|
|
@ -4723,7 +4715,6 @@
|
|||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
||||
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
|
|
@ -4783,7 +4774,6 @@
|
|||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
|
|
@ -4810,7 +4800,6 @@
|
|||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
|
|
@ -4935,7 +4924,6 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"cssesc": "bin/cssesc"
|
||||
},
|
||||
|
|
@ -5049,8 +5037,7 @@
|
|||
"node_modules/didyoumean": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "3.0.1",
|
||||
|
|
@ -5067,8 +5054,7 @@
|
|||
"node_modules/dlv": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
|
||||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
"version": "3.0.0",
|
||||
|
|
@ -5704,7 +5690,6 @@
|
|||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
|
||||
"integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
|
|
@ -5720,7 +5705,6 @@
|
|||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
|
|
@ -5744,7 +5728,6 @@
|
|||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
|
||||
"integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
|
|
@ -5765,7 +5748,6 @@
|
|||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
|
|
@ -6023,7 +6005,6 @@
|
|||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.3"
|
||||
},
|
||||
|
|
@ -6379,7 +6360,6 @@
|
|||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
|
|
@ -6457,7 +6437,6 @@
|
|||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"devOptional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -6493,7 +6472,6 @@
|
|||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
|
|
@ -6526,7 +6504,6 @@
|
|||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"devOptional": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
|
|
@ -6737,7 +6714,6 @@
|
|||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz",
|
||||
"integrity": "sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
|
|
@ -6897,7 +6873,6 @@
|
|||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
|
||||
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
|
|
@ -6905,8 +6880,7 @@
|
|||
"node_modules/lines-and-columns": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
|
||||
},
|
||||
"node_modules/linkify-it": {
|
||||
"version": "4.0.1",
|
||||
|
|
@ -7004,9 +6978,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.268.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.268.0.tgz",
|
||||
"integrity": "sha512-XP/xY3ASJAViqNqVnDRcEfdxfRB7uNST8sqTLwZhL983ikmHMQ7qQak7ZxrnXOVhB3QDBawdr3ANq0P+iWHP/g==",
|
||||
"version": "0.307.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.307.0.tgz",
|
||||
"integrity": "sha512-+vZ+vUiWPZTMnLHURg4aoIaz6NHOWXVVcVd8iLROu1k4LbyjcnHIKmbjXHCmulz7XAYLWRVXzhJJgIr+Aq3vOg==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
|
|
@ -7079,7 +7053,6 @@
|
|||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
|
|
@ -7088,7 +7061,6 @@
|
|||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
|
||||
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.2",
|
||||
"picomatch": "^2.3.1"
|
||||
|
|
@ -7136,7 +7108,6 @@
|
|||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"any-promise": "^1.0.0",
|
||||
"object-assign": "^4.0.1",
|
||||
|
|
@ -7240,7 +7211,6 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"devOptional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -7266,7 +7236,6 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
|
|
@ -7545,7 +7514,6 @@
|
|||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
|
||||
"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
|
|
@ -7581,7 +7549,6 @@
|
|||
"version": "15.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
|
||||
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"postcss-value-parser": "^4.0.0",
|
||||
"read-cache": "^1.0.0",
|
||||
|
|
@ -7598,7 +7565,6 @@
|
|||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
|
||||
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"camelcase-css": "^2.0.1"
|
||||
},
|
||||
|
|
@ -7617,7 +7583,6 @@
|
|||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz",
|
||||
"integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lilconfig": "^2.0.5",
|
||||
"yaml": "^2.1.1"
|
||||
|
|
@ -7646,7 +7611,6 @@
|
|||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
|
||||
"integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"postcss-selector-parser": "^6.0.11"
|
||||
},
|
||||
|
|
@ -7665,7 +7629,6 @@
|
|||
"version": "6.0.13",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz",
|
||||
"integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
|
|
@ -8173,7 +8136,6 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"pify": "^2.3.0"
|
||||
}
|
||||
|
|
@ -8182,7 +8144,6 @@
|
|||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -8204,7 +8165,6 @@
|
|||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
|
|
@ -8359,7 +8319,6 @@
|
|||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"iojs": ">=1.0.0",
|
||||
"node": ">=0.10.0"
|
||||
|
|
@ -8424,7 +8383,6 @@
|
|||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
|
@ -8841,7 +8799,6 @@
|
|||
"version": "3.34.0",
|
||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz",
|
||||
"integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.2",
|
||||
"commander": "^4.0.0",
|
||||
|
|
@ -8863,7 +8820,6 @@
|
|||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
|
|
@ -8872,7 +8828,6 @@
|
|||
"version": "7.1.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
|
|
@ -8931,11 +8886,21 @@
|
|||
"url": "https://github.com/sponsors/dcastil"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-scrollbar": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-3.0.5.tgz",
|
||||
"integrity": "sha512-0ZwxTivevqq9BY9fRP9zDjHl7Tu+J5giBGbln+0O1R/7nHtBUKnjQcA1aTIhK7Oyjp6Uc/Dj6/dn8Dq58k5Uww==",
|
||||
"engines": {
|
||||
"node": ">=12.13.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": "3.x"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
|
||||
"integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
|
|
@ -8987,7 +8952,6 @@
|
|||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"any-promise": "^1.0.0"
|
||||
}
|
||||
|
|
@ -8996,7 +8960,6 @@
|
|||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
|
||||
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"thenify": ">= 3.1.0 < 4"
|
||||
},
|
||||
|
|
@ -9043,7 +9006,6 @@
|
|||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
|
|
@ -9071,8 +9033,7 @@
|
|||
"node_modules/ts-interface-checker": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
|
||||
},
|
||||
"node_modules/tsconfig-paths": {
|
||||
"version": "3.14.2",
|
||||
|
|
@ -9622,7 +9583,6 @@
|
|||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz",
|
||||
"integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
"formik": "^2.2.9",
|
||||
"framer-motion": "^10.16.1",
|
||||
"lowlight": "^3.0.0",
|
||||
"lucide-react": "^0.268.0",
|
||||
"lucide-react": "^0.307.0",
|
||||
"next": "14.0.4",
|
||||
"re-resizable": "^6.9.9",
|
||||
"react": "^18.2.0",
|
||||
|
|
@ -43,6 +43,7 @@
|
|||
"styled-components": "^6.0.0-beta.9",
|
||||
"swr": "^2.2.4",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwind-scrollbar": "^3.0.5",
|
||||
"uuid": "^9.0.0",
|
||||
"y-indexeddb": "^9.0.9",
|
||||
"y-webrtc": "^10.2.3",
|
||||
|
|
|
|||
BIN
apps/web/public/learnhouse_ai_black_logo.png
Normal file
BIN
apps/web/public/learnhouse_ai_black_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
apps/web/public/learnhouse_ai_simple.png
Normal file
BIN
apps/web/public/learnhouse_ai_simple.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
BIN
apps/web/public/learnhouse_ai_simple_colored.png
Normal file
BIN
apps/web/public/learnhouse_ai_simple_colored.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
32
apps/web/services/ai/ai.ts
Normal file
32
apps/web/services/ai/ai.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { getAPIUrl } from "@services/config/config";
|
||||
import { RequestBody, RequestBodyWithAuthHeader } from "@services/utils/ts/requests";
|
||||
|
||||
export async function startActivityAIChatSession(message: string, activity_uuid: string) {
|
||||
const data = {
|
||||
message,
|
||||
activity_uuid,
|
||||
};
|
||||
const result = await fetch(`${getAPIUrl()}ai/start/activity_chat_session`, RequestBody("POST", data, null));
|
||||
const json = await result.json();
|
||||
if (result.status === 200) {
|
||||
return { success: true, data: json, status: result.status, HTTPmessage: result.statusText };
|
||||
} else {
|
||||
return { success: false, data: json, status: result.status, HTTPmessage: result.statusText };
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendActivityAIChatMessage(message: string, aichat_uuid: string, activity_uuid: string) {
|
||||
const data = {
|
||||
aichat_uuid,
|
||||
message,
|
||||
activity_uuid,
|
||||
};
|
||||
const result = await fetch(`${getAPIUrl()}ai/send/activity_chat_message`, RequestBody("POST", data, null));
|
||||
|
||||
const json = await result.json();
|
||||
if (result.status === 200) {
|
||||
return { success: true, data: json, status: result.status, HTTPmessage: result.statusText };
|
||||
} else {
|
||||
return { success: false, data: json, status: result.status, HTTPmessage: result.statusText };
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ module.exports = {
|
|||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [
|
||||
require('tailwind-scrollbar')({ nocompatible: true }),
|
||||
],
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ services:
|
|||
- POSTGRES_USER=learnhouse
|
||||
- POSTGRES_PASSWORD=learnhouse
|
||||
- POSTGRES_DB=learnhouse
|
||||
redis:
|
||||
image: redis:7.2.3
|
||||
restart: always
|
||||
ports:
|
||||
- "6379:6379"
|
||||
mongo:
|
||||
image: mongo:5.0
|
||||
restart: always
|
||||
|
|
|
|||
190
pnpm-lock.yaml
generated
190
pnpm-lock.yaml
generated
|
|
@ -42,8 +42,8 @@ importers:
|
|||
specifier: ^1.0.5
|
||||
version: 1.0.7(@types/react-dom@18.0.6)(@types/react@18.2.8)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@sentry/nextjs':
|
||||
specifier: ^7.88.0
|
||||
version: 7.88.0(next@14.0.4)(react@18.2.0)
|
||||
specifier: ^7.92.0
|
||||
version: 7.93.0(next@14.0.4)(react@18.2.0)
|
||||
'@stitches/react':
|
||||
specifier: ^1.2.8
|
||||
version: 1.2.8(react@18.2.0)
|
||||
|
|
@ -81,8 +81,8 @@ importers:
|
|||
specifier: ^3.0.0
|
||||
version: 3.1.0
|
||||
lucide-react:
|
||||
specifier: ^0.268.0
|
||||
version: 0.268.0(react@18.2.0)
|
||||
specifier: ^0.307.0
|
||||
version: 0.307.0(react@18.2.0)
|
||||
next:
|
||||
specifier: 14.0.4
|
||||
version: 14.0.4(@babel/core@7.23.2)(react-dom@18.2.0)(react@18.2.0)
|
||||
|
|
@ -122,6 +122,9 @@ importers:
|
|||
tailwind-merge:
|
||||
specifier: ^1.14.0
|
||||
version: 1.14.0
|
||||
tailwind-scrollbar:
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5(tailwindcss@3.3.3)
|
||||
uuid:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.1
|
||||
|
|
@ -188,7 +191,6 @@ packages:
|
|||
/@alloc/quick-lru@5.2.0:
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/@ampproject/remapping@2.2.1:
|
||||
resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==}
|
||||
|
|
@ -1756,12 +1758,10 @@ packages:
|
|||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
run-parallel: 1.2.0
|
||||
dev: true
|
||||
|
||||
/@nodelib/fs.stat@2.0.5:
|
||||
resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
|
||||
engines: {node: '>= 8'}
|
||||
dev: true
|
||||
|
||||
/@nodelib/fs.walk@1.2.8:
|
||||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||
|
|
@ -1769,7 +1769,6 @@ packages:
|
|||
dependencies:
|
||||
'@nodelib/fs.scandir': 2.1.5
|
||||
fastq: 1.15.0
|
||||
dev: true
|
||||
|
||||
/@popperjs/core@2.11.8:
|
||||
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
||||
|
|
@ -2430,34 +2429,34 @@ packages:
|
|||
resolution: {integrity: sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA==}
|
||||
dev: true
|
||||
|
||||
/@sentry-internal/feedback@7.88.0:
|
||||
resolution: {integrity: sha512-lbK6jgO1I0M96nZQ99mcLSZ55ebwPAP6LhEWhkmc+eAfy97VpiY+qsbmgsmOzCEPqMmEUCEcI0rEZ7fiye2v2Q==}
|
||||
/@sentry-internal/feedback@7.93.0:
|
||||
resolution: {integrity: sha512-4G7rMeQbYGfCHxEoFroABX+UREYc2BSbFqjLmLbIcWowSpgzcwweLLphWHKOciqK6f7DnNDK0jZzx3u7NrkWHw==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
'@sentry/core': 7.88.0
|
||||
'@sentry/types': 7.88.0
|
||||
'@sentry/utils': 7.88.0
|
||||
'@sentry/core': 7.93.0
|
||||
'@sentry/types': 7.93.0
|
||||
'@sentry/utils': 7.93.0
|
||||
dev: false
|
||||
|
||||
/@sentry-internal/tracing@7.88.0:
|
||||
resolution: {integrity: sha512-xXQdcYhsS+ourzJHjXNjZC9zakuc97udmpgaXRjEP7FjPYclIx+YXwgFBdHM2kzAwZLFOsEce5dr46GVXUDfZw==}
|
||||
/@sentry-internal/tracing@7.93.0:
|
||||
resolution: {integrity: sha512-DjuhmQNywPp+8fxC9dvhGrqgsUb6wI/HQp25lS2Re7VxL1swCasvpkg8EOYP4iBniVQ86QK0uITkOIRc5tdY1w==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
'@sentry/core': 7.88.0
|
||||
'@sentry/types': 7.88.0
|
||||
'@sentry/utils': 7.88.0
|
||||
'@sentry/core': 7.93.0
|
||||
'@sentry/types': 7.93.0
|
||||
'@sentry/utils': 7.93.0
|
||||
dev: false
|
||||
|
||||
/@sentry/browser@7.88.0:
|
||||
resolution: {integrity: sha512-il4x3PB99nuU/OJQw2RltgYYbo8vtnYoIgneOeEiw4m0ppK1nKkMkd3vDRipGL6E/0i7IUmQfYYy6U10J5Rx+g==}
|
||||
/@sentry/browser@7.93.0:
|
||||
resolution: {integrity: sha512-MtLTcQ7y3rfk+aIvnnwCfSJvYhTJnIJi+Mf6y/ap6SKObdlsKMbQoJLlRViglGLq+nKxHLAvU0fONiCEmKfV6A==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
'@sentry-internal/feedback': 7.88.0
|
||||
'@sentry-internal/tracing': 7.88.0
|
||||
'@sentry/core': 7.88.0
|
||||
'@sentry/replay': 7.88.0
|
||||
'@sentry/types': 7.88.0
|
||||
'@sentry/utils': 7.88.0
|
||||
'@sentry-internal/feedback': 7.93.0
|
||||
'@sentry-internal/tracing': 7.93.0
|
||||
'@sentry/core': 7.93.0
|
||||
'@sentry/replay': 7.93.0
|
||||
'@sentry/types': 7.93.0
|
||||
'@sentry/utils': 7.93.0
|
||||
dev: false
|
||||
|
||||
/@sentry/cli@1.77.1:
|
||||
|
|
@ -2477,26 +2476,26 @@ packages:
|
|||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@sentry/core@7.88.0:
|
||||
resolution: {integrity: sha512-Jzbb7dcwiCO7kI0a1w+32UzWxbEn2OcZWzp55QMEeAh6nZ/5CXhXwpuHi0tW7doPj+cJdmxMTMu9LqMVfdGkzQ==}
|
||||
/@sentry/core@7.93.0:
|
||||
resolution: {integrity: sha512-vZQSUiDn73n+yu2fEcH+Wpm4GbRmtxmnXnYCPgM6IjnXqkVm3awWAkzrheADblx3kmxrRiOlTXYHw9NTWs56fg==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
'@sentry/types': 7.88.0
|
||||
'@sentry/utils': 7.88.0
|
||||
'@sentry/types': 7.93.0
|
||||
'@sentry/utils': 7.93.0
|
||||
dev: false
|
||||
|
||||
/@sentry/integrations@7.88.0:
|
||||
resolution: {integrity: sha512-YBYPAtJeylMaaCmGntgiDpp1nk3IT6+FBXsmHxMdTKlrpt5ELj/jcc8gEgaRNeSBjx4Kv1OVzmZcYyWwEhkR4Q==}
|
||||
/@sentry/integrations@7.93.0:
|
||||
resolution: {integrity: sha512-uGQ8+DiqUr6SbhdJJHyIqDJ6kHnFuSv8nZWtj2tJ1I8q8u8MX8t8Om6R/R4ap45gCkWg/zqZq7B+gQV6TYewjQ==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
'@sentry/core': 7.88.0
|
||||
'@sentry/types': 7.88.0
|
||||
'@sentry/utils': 7.88.0
|
||||
'@sentry/core': 7.93.0
|
||||
'@sentry/types': 7.93.0
|
||||
'@sentry/utils': 7.93.0
|
||||
localforage: 1.10.0
|
||||
dev: false
|
||||
|
||||
/@sentry/nextjs@7.88.0(next@14.0.4)(react@18.2.0):
|
||||
resolution: {integrity: sha512-tvP9KU7SeL65szenGoexABdPqCVMUTWEP9DroNvBDvTtqfETOf8RbGw8zE+bFNxQ9bjAzhJPibu6oWNcpYvXMA==}
|
||||
/@sentry/nextjs@7.93.0(next@14.0.4)(react@18.2.0):
|
||||
resolution: {integrity: sha512-/O4Xl+hMSEM6/sVfmKXCZhLUUGNJbi+L0tasTiw4wB4EQQeMDKf4cBfx8e4mNBMzhA2SZnfQZAwJGqhvFJniPQ==}
|
||||
engines: {node: '>=8'}
|
||||
peerDependencies:
|
||||
next: ^10.0.8 || ^11.0 || ^12.0 || ^13.0 || ^14.0
|
||||
|
|
@ -2507,13 +2506,13 @@ packages:
|
|||
optional: true
|
||||
dependencies:
|
||||
'@rollup/plugin-commonjs': 24.0.0(rollup@2.78.0)
|
||||
'@sentry/core': 7.88.0
|
||||
'@sentry/integrations': 7.88.0
|
||||
'@sentry/node': 7.88.0
|
||||
'@sentry/react': 7.88.0(react@18.2.0)
|
||||
'@sentry/types': 7.88.0
|
||||
'@sentry/utils': 7.88.0
|
||||
'@sentry/vercel-edge': 7.88.0
|
||||
'@sentry/core': 7.93.0
|
||||
'@sentry/integrations': 7.93.0
|
||||
'@sentry/node': 7.93.0
|
||||
'@sentry/react': 7.93.0(react@18.2.0)
|
||||
'@sentry/types': 7.93.0
|
||||
'@sentry/utils': 7.93.0
|
||||
'@sentry/vercel-edge': 7.93.0
|
||||
'@sentry/webpack-plugin': 1.21.0
|
||||
chalk: 3.0.0
|
||||
next: 14.0.4(@babel/core@7.23.2)(react-dom@18.2.0)(react@18.2.0)
|
||||
|
|
@ -2526,62 +2525,63 @@ packages:
|
|||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@sentry/node@7.88.0:
|
||||
resolution: {integrity: sha512-X6Xyh7AEitnWqn1CHQrmsUqRn0GKj/6nPE5VC2DLQfHiFH1Fknrt+csFzDchQ/86awXYwuY4Le5ECEH//X/WzQ==}
|
||||
/@sentry/node@7.93.0:
|
||||
resolution: {integrity: sha512-nUXPCZQm5Y9Ipv7iWXLNp5dbuyi1VvbJ3RtlwD7utgsNkRYB4ixtKE9w2QU8DZZAjaEF6w2X94OkYH6C932FWw==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
'@sentry-internal/tracing': 7.88.0
|
||||
'@sentry/core': 7.88.0
|
||||
'@sentry/types': 7.88.0
|
||||
'@sentry/utils': 7.88.0
|
||||
'@sentry-internal/tracing': 7.93.0
|
||||
'@sentry/core': 7.93.0
|
||||
'@sentry/types': 7.93.0
|
||||
'@sentry/utils': 7.93.0
|
||||
https-proxy-agent: 5.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@sentry/react@7.88.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-iDOImijbsc0cYLWNBXlYKhh/sG/czPK/51GcMi3GcEBkhHDDcdWSZ7cNjFAqHfdrMkPf26bYgDPIL6aJsBZwpQ==}
|
||||
/@sentry/react@7.93.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-B0bzziV1lEyN7xd0orUPyJdpoK6CtcyodmQkfY0WsHLm/1d9xi95M05lObHnsMWO1js6c9B9d9kO8RlKFz947A==}
|
||||
engines: {node: '>=8'}
|
||||
peerDependencies:
|
||||
react: 15.x || 16.x || 17.x || 18.x
|
||||
dependencies:
|
||||
'@sentry/browser': 7.88.0
|
||||
'@sentry/types': 7.88.0
|
||||
'@sentry/utils': 7.88.0
|
||||
'@sentry/browser': 7.93.0
|
||||
'@sentry/core': 7.93.0
|
||||
'@sentry/types': 7.93.0
|
||||
'@sentry/utils': 7.93.0
|
||||
hoist-non-react-statics: 3.3.2
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@sentry/replay@7.88.0:
|
||||
resolution: {integrity: sha512-em5dPKLPG7c/HGDbpIj3aHrWbA4iMwqjevqTzn+++KNO1YslkOosCaGsb1whU3AL1T9c3aIFIhZ4u3rNo+DxcA==}
|
||||
/@sentry/replay@7.93.0:
|
||||
resolution: {integrity: sha512-dMlLU8v+OkUeGCrPvTu5NriH7BGj3el4rGHWWAYicfJ2QXqTTq50vfasQBP1JeVNcFqnf1y653TdEIvo4RH4tw==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
'@sentry-internal/tracing': 7.88.0
|
||||
'@sentry/core': 7.88.0
|
||||
'@sentry/types': 7.88.0
|
||||
'@sentry/utils': 7.88.0
|
||||
'@sentry-internal/tracing': 7.93.0
|
||||
'@sentry/core': 7.93.0
|
||||
'@sentry/types': 7.93.0
|
||||
'@sentry/utils': 7.93.0
|
||||
dev: false
|
||||
|
||||
/@sentry/types@7.88.0:
|
||||
resolution: {integrity: sha512-FvwvmX1pWAZKicPj4EpKyho8Wm+C4+r5LiepbbBF8oKwSPJdD2QV1fo/LWxsrzNxWOllFIVIXF5Ed3nPYQWpTw==}
|
||||
/@sentry/types@7.93.0:
|
||||
resolution: {integrity: sha512-UnzUccNakhFRA/esWBWP+0v7cjNg+RilFBQC03Mv9OEMaZaS29zSbcOGtRzuFOXXLBdbr44BWADqpz3VW0XaNw==}
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/@sentry/utils@7.88.0:
|
||||
resolution: {integrity: sha512-ukminfRmdBXTzk49orwJf3Lu3hR60ZRHjE2a4IXwYhyDT6JJgJqgsq1hzGXx0AyFfyS4WhfZ6QUBy7fu3BScZQ==}
|
||||
/@sentry/utils@7.93.0:
|
||||
resolution: {integrity: sha512-Iovj7tUnbgSkh/WrAaMrd5UuYjW7AzyzZlFDIUrwidsyIdUficjCG2OIxYzh76H6nYIx9SxewW0R54Q6XoB4uA==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
'@sentry/types': 7.88.0
|
||||
'@sentry/types': 7.93.0
|
||||
dev: false
|
||||
|
||||
/@sentry/vercel-edge@7.88.0:
|
||||
resolution: {integrity: sha512-PfaOiPPRw7y4CcOeP337NsPGyERpO6OlSAmLIaUkKJRjnGNmg1tSzUNfG0lg//fQ8XsZsXIun/ND+XXYtEJFDw==}
|
||||
/@sentry/vercel-edge@7.93.0:
|
||||
resolution: {integrity: sha512-3jddd6gVUpGX8Sis9gxODL7zPR+lZohYYvOJVhf8UMglZSiWa3/xYJQ5VISj3UH6sVSxvfMxgssmQEHcvuubHQ==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
'@sentry-internal/tracing': 7.88.0
|
||||
'@sentry/core': 7.88.0
|
||||
'@sentry/types': 7.88.0
|
||||
'@sentry/utils': 7.88.0
|
||||
'@sentry-internal/tracing': 7.93.0
|
||||
'@sentry/core': 7.93.0
|
||||
'@sentry/types': 7.93.0
|
||||
'@sentry/utils': 7.93.0
|
||||
dev: false
|
||||
|
||||
/@sentry/webpack-plugin@1.21.0:
|
||||
|
|
@ -3127,7 +3127,6 @@ packages:
|
|||
|
||||
/any-promise@1.3.0:
|
||||
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
|
||||
dev: true
|
||||
|
||||
/anymatch@3.1.3:
|
||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||
|
|
@ -3138,7 +3137,6 @@ packages:
|
|||
|
||||
/arg@5.0.2:
|
||||
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
|
||||
dev: true
|
||||
|
||||
/argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
|
@ -3392,7 +3390,6 @@ packages:
|
|||
/camelcase-css@2.0.1:
|
||||
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
||||
engines: {node: '>= 6'}
|
||||
dev: true
|
||||
|
||||
/camelize@1.0.1:
|
||||
resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==}
|
||||
|
|
@ -3535,7 +3532,6 @@ packages:
|
|||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/csstype@3.1.2:
|
||||
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
|
||||
|
|
@ -3629,7 +3625,6 @@ packages:
|
|||
|
||||
/didyoumean@1.2.2:
|
||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||
dev: true
|
||||
|
||||
/dir-glob@3.0.1:
|
||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
||||
|
|
@ -3640,7 +3635,6 @@ packages:
|
|||
|
||||
/dlv@1.1.3:
|
||||
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
|
||||
dev: true
|
||||
|
||||
/doctrine@2.1.0:
|
||||
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
|
||||
|
|
@ -4070,7 +4064,6 @@ packages:
|
|||
glob-parent: 5.1.2
|
||||
merge2: 1.4.1
|
||||
micromatch: 4.0.5
|
||||
dev: true
|
||||
|
||||
/fast-json-stable-stringify@2.1.0:
|
||||
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
||||
|
|
@ -4084,7 +4077,6 @@ packages:
|
|||
resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==}
|
||||
dependencies:
|
||||
reusify: 1.0.4
|
||||
dev: true
|
||||
|
||||
/file-entry-cache@6.0.1:
|
||||
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
||||
|
|
@ -4244,7 +4236,6 @@ packages:
|
|||
engines: {node: '>=10.13.0'}
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
dev: true
|
||||
|
||||
/glob-to-regexp@0.4.1:
|
||||
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
|
||||
|
|
@ -4259,7 +4250,6 @@ packages:
|
|||
minimatch: 3.1.2
|
||||
once: 1.4.0
|
||||
path-is-absolute: 1.0.1
|
||||
dev: true
|
||||
|
||||
/glob@7.1.7:
|
||||
resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==}
|
||||
|
|
@ -4660,7 +4650,6 @@ packages:
|
|||
/jiti@1.20.0:
|
||||
resolution: {integrity: sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
|
@ -4766,11 +4755,9 @@ packages:
|
|||
/lilconfig@2.1.0:
|
||||
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/lines-and-columns@1.2.4:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
dev: true
|
||||
|
||||
/linkify-it@4.0.1:
|
||||
resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==}
|
||||
|
|
@ -4838,8 +4825,8 @@ packages:
|
|||
yallist: 4.0.0
|
||||
dev: true
|
||||
|
||||
/lucide-react@0.268.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-XP/xY3ASJAViqNqVnDRcEfdxfRB7uNST8sqTLwZhL983ikmHMQ7qQak7ZxrnXOVhB3QDBawdr3ANq0P+iWHP/g==}
|
||||
/lucide-react@0.307.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-+vZ+vUiWPZTMnLHURg4aoIaz6NHOWXVVcVd8iLROu1k4LbyjcnHIKmbjXHCmulz7XAYLWRVXzhJJgIr+Aq3vOg==}
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0
|
||||
dependencies:
|
||||
|
|
@ -4887,7 +4874,6 @@ packages:
|
|||
/merge2@1.4.1:
|
||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||
engines: {node: '>= 8'}
|
||||
dev: true
|
||||
|
||||
/micromatch@4.0.5:
|
||||
resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
|
||||
|
|
@ -4895,7 +4881,6 @@ packages:
|
|||
dependencies:
|
||||
braces: 3.0.2
|
||||
picomatch: 2.3.1
|
||||
dev: true
|
||||
|
||||
/minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
|
@ -4936,7 +4921,6 @@ packages:
|
|||
any-promise: 1.3.0
|
||||
object-assign: 4.1.1
|
||||
thenify-all: 1.6.0
|
||||
dev: true
|
||||
|
||||
/nanoid@3.3.6:
|
||||
resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
|
||||
|
|
@ -5018,7 +5002,6 @@ packages:
|
|||
/object-hash@3.0.0:
|
||||
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
|
||||
engines: {node: '>= 6'}
|
||||
dev: true
|
||||
|
||||
/object-inspect@1.12.3:
|
||||
resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==}
|
||||
|
|
@ -5170,7 +5153,6 @@ packages:
|
|||
/pify@2.3.0:
|
||||
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/pify@4.0.1:
|
||||
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
|
||||
|
|
@ -5180,7 +5162,6 @@ packages:
|
|||
/pirates@4.0.6:
|
||||
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
|
||||
engines: {node: '>= 6'}
|
||||
dev: true
|
||||
|
||||
/postcss-import@15.1.0(postcss@8.4.31):
|
||||
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
|
||||
|
|
@ -5192,7 +5173,6 @@ packages:
|
|||
postcss-value-parser: 4.2.0
|
||||
read-cache: 1.0.0
|
||||
resolve: 1.22.8
|
||||
dev: true
|
||||
|
||||
/postcss-js@4.0.1(postcss@8.4.31):
|
||||
resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==}
|
||||
|
|
@ -5202,7 +5182,6 @@ packages:
|
|||
dependencies:
|
||||
camelcase-css: 2.0.1
|
||||
postcss: 8.4.31
|
||||
dev: true
|
||||
|
||||
/postcss-load-config@4.0.1(postcss@8.4.31):
|
||||
resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==}
|
||||
|
|
@ -5219,7 +5198,6 @@ packages:
|
|||
lilconfig: 2.1.0
|
||||
postcss: 8.4.31
|
||||
yaml: 2.3.2
|
||||
dev: true
|
||||
|
||||
/postcss-nested@6.0.1(postcss@8.4.31):
|
||||
resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==}
|
||||
|
|
@ -5229,7 +5207,6 @@ packages:
|
|||
dependencies:
|
||||
postcss: 8.4.31
|
||||
postcss-selector-parser: 6.0.13
|
||||
dev: true
|
||||
|
||||
/postcss-selector-parser@6.0.13:
|
||||
resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==}
|
||||
|
|
@ -5237,7 +5214,6 @@ packages:
|
|||
dependencies:
|
||||
cssesc: 3.0.0
|
||||
util-deprecate: 1.0.2
|
||||
dev: true
|
||||
|
||||
/postcss-value-parser@4.2.0:
|
||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||
|
|
@ -5632,7 +5608,6 @@ packages:
|
|||
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
|
||||
dependencies:
|
||||
pify: 2.3.0
|
||||
dev: true
|
||||
|
||||
/readable-stream@3.6.2:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
|
|
@ -5744,7 +5719,6 @@ packages:
|
|||
/reusify@1.0.4:
|
||||
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/rimraf@3.0.2:
|
||||
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
||||
|
|
@ -5769,7 +5743,6 @@ packages:
|
|||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||
dependencies:
|
||||
queue-microtask: 1.2.3
|
||||
dev: true
|
||||
|
||||
/safe-array-concat@1.0.1:
|
||||
resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==}
|
||||
|
|
@ -6025,7 +5998,6 @@ packages:
|
|||
mz: 2.7.0
|
||||
pirates: 4.0.6
|
||||
ts-interface-checker: 0.1.13
|
||||
dev: true
|
||||
|
||||
/supports-color@5.5.0:
|
||||
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
||||
|
|
@ -6058,6 +6030,15 @@ packages:
|
|||
resolution: {integrity: sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==}
|
||||
dev: false
|
||||
|
||||
/tailwind-scrollbar@3.0.5(tailwindcss@3.3.3):
|
||||
resolution: {integrity: sha512-0ZwxTivevqq9BY9fRP9zDjHl7Tu+J5giBGbln+0O1R/7nHtBUKnjQcA1aTIhK7Oyjp6Uc/Dj6/dn8Dq58k5Uww==}
|
||||
engines: {node: '>=12.13.0'}
|
||||
peerDependencies:
|
||||
tailwindcss: 3.x
|
||||
dependencies:
|
||||
tailwindcss: 3.3.3
|
||||
dev: false
|
||||
|
||||
/tailwindcss@3.3.3:
|
||||
resolution: {integrity: sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
|
@ -6087,7 +6068,6 @@ packages:
|
|||
sucrase: 3.34.0
|
||||
transitivePeerDependencies:
|
||||
- ts-node
|
||||
dev: true
|
||||
|
||||
/tapable@2.2.1:
|
||||
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
|
||||
|
|
@ -6103,13 +6083,11 @@ packages:
|
|||
engines: {node: '>=0.8'}
|
||||
dependencies:
|
||||
thenify: 3.3.1
|
||||
dev: true
|
||||
|
||||
/thenify@3.3.1:
|
||||
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
|
||||
dependencies:
|
||||
any-promise: 1.3.0
|
||||
dev: true
|
||||
|
||||
/throttle-debounce@3.0.1:
|
||||
resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==}
|
||||
|
|
@ -6156,7 +6134,6 @@ packages:
|
|||
|
||||
/ts-interface-checker@0.1.13:
|
||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||
dev: true
|
||||
|
||||
/tsconfig-paths@3.14.2:
|
||||
resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==}
|
||||
|
|
@ -6573,7 +6550,6 @@ packages:
|
|||
/yaml@2.3.2:
|
||||
resolution: {integrity: sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==}
|
||||
engines: {node: '>= 14'}
|
||||
dev: true
|
||||
|
||||
/yjs@13.6.8:
|
||||
resolution: {integrity: sha512-ZPq0hpJQb6f59B++Ngg4cKexDJTvfOgeiv0sBc4sUm8CaBWH7OQC4kcCgrqbjJ/B2+6vO49exvTmYfdlPtcjbg==}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue