Compare commits

...

13 Commits

Author SHA1 Message Date
Robin COuret
5b621a3865 add prod config 2026-03-10 20:05:39 +01:00
Robin COuret
d3c6845bdc add details 2026-03-09 10:16:26 +01:00
Robin COuret
ffa9efcfe3 add limit of question list 2026-03-08 19:03:23 +01:00
Robin COuret
e0bc69e98f add content in flow component 2026-03-08 19:00:10 +01:00
Robin COuret
d1e5a6b0c7 add KnowledgeWorkFlow system 2026-03-08 01:33:21 +01:00
Robin COuret
73fff0955b add strict limit 2026-03-06 19:38:13 +01:00
Robin COuret
ed0d989915 add register 2026-03-06 19:35:28 +01:00
Robin COuret
34845b9696 add auth flux 2026-03-06 17:42:04 +01:00
Robin COuret
a06e9c3633 add user restriction 2026-03-06 16:31:40 +01:00
Robin COuret
a243149bf1 structure auth 2026-03-05 22:38:21 +01:00
Robin COuret
01f9e9f05e replace loadenv by pydantic-settings 2026-03-05 20:08:10 +01:00
Robin COuret
93712919ff refacto name 2026-03-05 19:40:13 +01:00
Robin COuret
02bc680982 add auth 2026-03-05 17:30:39 +01:00
91 changed files with 1401 additions and 426 deletions

View File

@@ -1,4 +0,0 @@
DATABASE_URI="sqlite:///database.db"
LANGUAGE_MODEL_API="http://localhost:8080/v1"
MODEL_NAME="SmolLM3-Q4_K_M.gguf"
ORIGIN="http://localhost:5173"

Binary file not shown.

View File

@@ -1,7 +1,14 @@
fastapi==0.128.6 # X86_V2 arch :
# numpy<2.4.0
fastapi[standard]==0.128.6
sqlmodel==0.0.32 sqlmodel==0.0.32
python-dotenv==1.2.1 python-dotenv==1.2.1
openai==2.21.0 openai==2.21.0
spacy==3.8.11 spacy==3.8.11
PyJWT>=2.11.0
# python -m spacy download en_core_web_sm # python -m spacy download en_core_web_sm
# python -m spacy download fr_core_news_sm # python -m spacy download fr_core_news_sm
argon2-cffi>=25.1.0
pydantic-settings==2.13.1
gunicorn==25.1.0
mariadb==1.1.14

View File

@@ -0,0 +1,5 @@
DATABASE_URI="sqlite:///database.db"
LANGUAGE_MODEL_API="http://localhost:8080/v1"
LANGUAGE_MODEL_NAME="SmolLM3-Q4_K_M.gguf"
ORIGIN="http://localhost:5173"
SECRET_KEY="xxxx" #generate secure random secret key: openssl rand -hex 32

Binary file not shown.

View File

@@ -2,7 +2,11 @@ from fastapi import APIRouter
from .knowledges import router as knowledge_router from .knowledges import router as knowledge_router
from .metrics import router as metric_router from .metrics import router as metric_router
from .auth import router as auth_router
from .questions import router as question_router
router = APIRouter(prefix="/v1") router = APIRouter(prefix="/v1")
router.include_router(knowledge_router) router.include_router(knowledge_router)
router.include_router(question_router)
router.include_router(metric_router) router.include_router(metric_router)
router.include_router(auth_router)

Binary file not shown.

View File

@@ -0,0 +1,46 @@
from typing import Annotated
from fastapi import Depends, APIRouter, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from src.app.models.user import User, UserCreate
from src.app.data.user import create_user, get_user_by_username
from src.app.auth.dependancies import get_current_user, authenticate_user
from src.app.auth.security import hash_password, create_access_token, verify_beyond_user_limit
from src.app.auth.schemas import Token
router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/login")
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> Token:
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token = create_access_token(data={"sub": user.username})
return Token(access_token=access_token, token_type="bearer")
@router.get("/me")
async def user(current_user: Annotated[str, Depends(get_current_user)]):
return current_user
@router.post("/register")
async def create(user_data: UserCreate):
if(verify_beyond_user_limit()):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="The user limit has been reached."
)
if get_user_by_username(user_data.username):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username already registered"
)
hashed_password = hash_password(user_data.plain_password)
user = User(username = user_data.username, hashed_password = hashed_password)
created_user = create_user(user)
return created_user

View File

@@ -1,57 +1,72 @@
from fastapi import APIRouter from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from src.app.models.knowledge import Knowledge from src.app.auth.dependancies import get_current_user
from src.app.models.knowledge import Knowledge, KnowledgeCreate
from src.app.models.question import Question from src.app.models.question import Question
from src.app.crud.crud_knowledges import create_knowledge, read_knowledges, read_knowledge, update_knowledge, delete_knowledge from src.app.data.knowledge import create_knowledge, get_knowledges_by_user, get_knowledge_by_id
from src.app.crud.crud_questions import read_questions as read_questions_crud, create_question #from src.app.data.knowledge update_knowledge, delete_knowledge
from src.app.data.question import get_questions, create_question
from src.app.services.language_generation import questions_generation from src.app.services.language_generation import questions_generation
#Added in __ini__ #Added in __ini__
router = APIRouter(tags=["knowledges"]) router = APIRouter(tags=["knowledges"])
@router.get("/knowledges/")
def read(current_user: Annotated[str, Depends(get_current_user)]):
knowledges = get_knowledges_by_user(current_user)
return knowledges
@router.get("/knowledges/{id}")
def read(id: int, current_user: Annotated[str, Depends(get_current_user)]):
knowledge = get_knowledge_by_id(id, current_user)
return knowledge
@router.post("/knowledges/") @router.post("/knowledges/")
def create(knowledge: Knowledge): def create(knowledge_data: KnowledgeCreate, current_user: Annotated[str, Depends(get_current_user)]):
knowledge = Knowledge(content=knowledge_data.content, uri=knowledge_data.uri, user=current_user)
created_knowledge = create_knowledge(knowledge) created_knowledge = create_knowledge(knowledge)
# if created_knowledge is None: # if created_knowledge is None:
# raise NotFoundException("Failed to create knowledge") # raise NotFoundException("Failed to create knowledge")
return created_knowledge return created_knowledge
@router.get("/knowledges/")
def read():
knowledges = read_knowledges()
return knowledges
@router.get("/knowledges/{id}")
def read(id: int):
knowledge = read_knowledge(id)
return knowledge
#TODO: adapt with correct pattern
@router.post("/knowledges/{id}")
def update(id: int, content: str, uri: str):
knowledge = update_knowledge(id, content, uri)
return knowledge
@router.delete("/knowledges/{id}")
def delete(id: int):
knowledge = delete_knowledge(id)
return knowledge
@router.post("/knowledges/{id}/questions") @router.post("/knowledges/{id}/questions")
def create_questions(id: int): def generate_questions(id: int, current_user: Annotated[str, Depends(get_current_user)]):
knowledge = read_knowledge(id) knowledge: Knowledge = get_knowledge_by_id(id, current_user)
if not knowledge:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Forbidden. The requested knowledge is not available for the provided ID.",
headers={"WWW-Authenticate": "Bearer"},
)
questions_raw = questions_generation(knowledge) questions_raw = questions_generation(knowledge)
for q in questions_raw: for q in questions_raw:
question = Question(question = q, knowledge=knowledge) question = Question(question = q, knowledge=knowledge, user=current_user)
create_question(question) create_question(question)
return questions_raw return questions_raw
@router.get("/knowledges/{id}/questions") @router.get("/knowledges/{id}/questions")
def read_questions(id: int): def read_questions(id: int, current_user: Annotated[str, Depends(get_current_user)]):
knowledge: Knowledge = read_knowledge(id) knowledge: Knowledge = get_knowledge_by_id(id, current_user)
#questions = knowledge.questions if not knowledge:
#TODO : refacto ? raise HTTPException(
questions = read_questions_crud(knowledge) status_code=status.HTTP_403_FORBIDDEN,
detail="Forbidden. The requested knowledge is not available for the provided ID.",
headers={"WWW-Authenticate": "Bearer"},
)
questions = get_questions(knowledge)
return questions return questions
#TODO: adapt with correct pattern
# @router.post("/knowledges/{id}")
# def update(id: int, content: str, uri: str):
# knowledge = update_knowledge(id, content, uri)
# return knowledge
# @router.delete("/knowledges/{id}")
# def delete(id: int):
# knowledge = delete_knowledge(id)
# return knowledge

View File

@@ -1,11 +1,15 @@
from fastapi import APIRouter from typing import Annotated
from fastapi import APIRouter, Depends
from src.app.models.metric import Metric from src.app.models.metric import Metric, MetricCreate
from src.app.crud.crud_metrics import create_metric from src.app.data.metric import create_metric
from src.app.auth.dependancies import get_current_user
router = APIRouter(tags=["metrics"]) router = APIRouter(tags=["metrics"])
@router.post("/metrics/") # @router.post("/metrics/")
def create(metric: Metric): # def create(metric_data: MetricCreate, current_user: Annotated[str, Depends(get_current_user)]):
created_metric: Metric = create_metric(metric) # metric: Metric = Metric(question_id = metric_data.question_id, need_index = metric_data.need_index, user = current_user)
return created_metric # created_metric: Metric = create_metric(metric)
# return created_metric

View File

@@ -0,0 +1,38 @@
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from src.app.auth.dependancies import get_current_user
from src.app.models.question import Question
from src.app.models.metric import Metric, MetricCreate
from src.app.data.question import get_question_by_id
from src.app.data.metric import get_metrics, create_metric
#Added in __ini__
router = APIRouter(tags=["questions"])
@router.get("/questions/{id}/metrics")
def read_questions(id: int, current_user: Annotated[str, Depends(get_current_user)]):
question: Question = get_question_by_id(id, current_user)
if not question:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Forbidden. The requested knowledge is not available for the provided ID.",
headers={"WWW-Authenticate": "Bearer"},
)
metrics = get_metrics(question)
return metrics
@router.post("/questions/{id}/metrics")
def create(id: int, metric_data: MetricCreate, current_user: Annotated[str, Depends(get_current_user)]):
question: Question = get_question_by_id(id, current_user)
if not question:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Forbidden. The requested knowledge is not available for the provided ID.",
headers={"WWW-Authenticate": "Bearer"},
)
metric: Metric = Metric(question_id = id, need_index = metric_data.need_index, user = current_user)
created_metric: Metric = create_metric(metric)
return created_metric

Binary file not shown.

View File

@@ -0,0 +1,46 @@
from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from argon2 import PasswordHasher
from argon2 import PasswordHasher
from src.app.models.user import User
from src.app.data.user import get_user_by_username
from .schemas import TokenData
from .security import verify_password, verify_token
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
password_hasher = PasswordHasher()
def authenticate_user(username: str, password: str):
user: User = get_user_by_username(username)
if not user:
# Add timing to prevent attack
password_hasher.hash(password)
return False
if not verify_password(password, user.hashed_password):
return False
return user
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> User:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
payload = verify_token(token, token_type="access")
if payload is None:
raise credentials_exception
username = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
user = get_user_by_username(username=token_data.username)
if user is None:
raise credentials_exception
return user

View File

@@ -0,0 +1,8 @@
from pydantic import BaseModel
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: str | None = None

View File

@@ -0,0 +1,47 @@
from src.app.config import settings
from typing import Optional, Sequence
from src.app.models.user import User
from datetime import timedelta, datetime, timezone
from argon2 import PasswordHasher
from argon2.exceptions import (
VerifyMismatchError,
VerificationError,
InvalidHashError,
)
import jwt
from jwt.exceptions import InvalidTokenError
from src.app.data.user import get_users
password_hasher = PasswordHasher()
def verify_password(plain_password: str, hashed_password: str) -> bool:
try:
return password_hasher.verify(hashed_password, plain_password)
except (VerifyMismatchError, VerificationError, InvalidHashError):
return False
def hash_password(password: str) -> str:
return password_hasher.hash(password)
def create_access_token(data: dict):
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode = data.copy()
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
def verify_token(token: str, token_type: str = "access") -> Optional[dict]:
try:
payload = jwt.decode(jwt = token, key = settings.SECRET_KEY, algorithms = [settings.ALGORITHM])
return payload
except InvalidTokenError:
return None
def verify_beyond_user_limit() -> bool:
users: Sequence[User] = get_users()
if (len(users) >= settings.USER_LIMIT):
return True
else:
return False
#def create_refresh_token(data: dict) -> str:

22
server/src/app/config.py Normal file
View File

@@ -0,0 +1,22 @@
from pydantic_settings import BaseSettings
from pydantic import Field
class Settings(BaseSettings):
# Database
DATABASE_URI: str = Field("sqlite:///database.db", env='DATABASE_URI')
# Language model
LANGUAGE_MODEL_API: str = Field("http://localhost:8080/v1", env='LANGUAGE_MODEL_API')
LANGUAGE_MODEL_NAME: str = Field("SmolLM3-Q4_K_M.gguf", env='LANGUAGE_MODEL_NAME')
# Security
ORIGIN: str = Field('http://localhost:5173', env='ORIGIN')
SECRET_KEY : str = Field('random_string', env='SECRET_KEY')
ACCESS_TOKEN_EXPIRE_MINUTES: int = 240
ALGORITHM: str = "HS256"
USER_LIMIT: int = 10
class Config:
env_file = ".env"
settings = Settings()

View File

@@ -1,44 +0,0 @@
from fastapi import Depends
from sqlmodel import Session, select
from src.app.models.knowledge import Knowledge
from src.app.database import engine
def create_knowledge(knowledge: Knowledge):
with Session(engine) as session:
session.add(knowledge)
session.commit()
session.refresh(knowledge)
return knowledge
def read_knowledges():
with Session(engine) as session:
statement = select(Knowledge)
results = session.exec(statement)
knowledges = results.all()
return knowledges
def read_knowledge(knowledge_id: int):
with Session(engine) as session:
#statement = select(Knowledge).where(Knowledge.id == knowledge_id)
#results = session.exec(statement)
#knowledge = results.first()
knowledge = session.get(Knowledge, knowledge_id)
return knowledge
#TODO adapt logic with args
def update_knowledge(knowledge_id: int, content: str, uri: str):
with Session(engine) as session:
knowledge = session.get(Knowledge, knowledge_id)
knowledge.content = content if content else knowledge.content
knowledge.uri = uri if uri else knowledge.uri
session.add(knowledge)
session.commit()
session.refresh(knowledge)
def delete_knowledge(knowledge_id: int):
with Session(engine) as session:
knowledge = session.get(Knowledge, knowledge_id)
session.delete(knowledge)
session.commit()

View File

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,60 @@
from sqlmodel import Session, select
from src.app.models.knowledge import Knowledge
from src.app.models.user import User
from src.app.database import engine
def get_knowledge_by_id(knowledge_id: int, user: User):
with Session(engine) as session:
statement = select(Knowledge).where(Knowledge.id == knowledge_id, Knowledge.user_id == user.id)
results = session.exec(statement)
knowledge = results.first()
return knowledge
# def get_knowledge_by_id(knowledge_id: int):
# with Session(engine) as session:
# knowledge = session.get(Knowledge, knowledge_id)
# return knowledge
# No filter by user
def get_knowledges():
with Session(engine) as session:
statement = select(Knowledge)
results = session.exec(statement)
knowledges = results.all()
return knowledges
def get_knowledges_by_user(user: User):
with Session(engine) as session:
statement = select(Knowledge).where(Knowledge.user_id == user.id)
results = session.exec(statement)
knowledges = results.all()
return knowledges
def create_knowledge(knowledge: Knowledge):
with Session(engine) as session:
session.add(knowledge)
session.commit()
session.refresh(knowledge)
return knowledge
#TODO adapt logic with args
# No filter by user
# Transform to update_knowledge(knowledge: Knowledge)
# def update_knowledge(knowledge_id: int, content: str, uri: str):
# with Session(engine) as session:
# knowledge = session.get(Knowledge, knowledge_id)
# knowledge.content = content if content else knowledge.content
# knowledge.uri = uri if uri else knowledge.uri
# session.add(knowledge)
# session.commit()
# session.refresh(knowledge)
# return knowledge
# No filter by user
# def delete_knowledge(knowledge_id: int):
# with Session(engine) as session:
# knowledge = session.get(Knowledge, knowledge_id)
# session.delete(knowledge)
# session.commit()

View File

@@ -4,6 +4,18 @@ from src.app.models.metric import Metric
from src.app.models.question import Question from src.app.models.question import Question
from src.app.database import engine from src.app.database import engine
def get_metric_by_id(metric_id: int):
with Session(engine) as session:
metric = session.get(Metric, metric_id)
return metric
def get_metrics(question):
with Session(engine) as session:
statement = select(Metric).where(Metric.question_id == question.id)
results = session.exec(statement)
metrics = results.all()
return metrics
def create_metric(metric: Metric): def create_metric(metric: Metric):
with Session(engine) as session: with Session(engine) as session:
session.add(metric) session.add(metric)
@@ -11,18 +23,6 @@ def create_metric(metric: Metric):
session.refresh(metric) session.refresh(metric)
return metric return metric
def read_metrics(question):
with Session(engine) as session:
statement = select(Metric).where(Metric.question_id == question.id)
results = session.exec(statement)
metrics = results.all()
return metrics
def read_metric(metric_id: int):
with Session(engine) as session:
metric = session.get(Metric, metric_id)
return metric
def delete_metric(metric_id: int): def delete_metric(metric_id: int):
with Session(engine) as session: with Session(engine) as session:
metric = session.get(Metric, metric_id) metric = session.get(Metric, metric_id)

View File

@@ -1,8 +1,28 @@
from sqlmodel import Session, select from sqlmodel import Session, select
from src.app.models.question import Question from src.app.models.question import Question
from src.app.models.user import User
from src.app.database import engine from src.app.database import engine
def get_question_by_id(question_id: int, user: User):
with Session(engine) as session:
statement = select(Question).where(Question.id == question_id, Question.user_id == user.id)
results = session.exec(statement)
question = results.first()
return question
# def get_question_by_id(question_id: int):
# with Session(engine) as session:
# question = session.get(Question, question_id)
# return question
def get_questions(knowledge):
with Session(engine) as session:
statement = select(Question).where(Question.knowledge_id == knowledge.id)
results = session.exec(statement)
questions = results.all()
return questions
def create_question(question: Question): def create_question(question: Question):
with Session(engine) as session: with Session(engine) as session:
session.add(question) session.add(question)
@@ -10,30 +30,6 @@ def create_question(question: Question):
session.refresh(question) session.refresh(question)
return question return question
def read_questions(knowledge):
with Session(engine) as session:
statement = select(Question).where(Question.knowledge_id == knowledge.id)
results = session.exec(statement)
questions = results.all()
return questions
def read_question(question_id: int):
with Session(engine) as session:
question = session.get(Question, question_id)
return question
# #TODO adapt logic with args
# def update_question(question_id: int, content: str, uri: str):
# with Session(engine) as session:
# question = session.get(Question, question_id)
# question.content = content if content else question.content
# question.uri = uri if uri else question.uri
# session.add(question)
# session.commit()
# session.refresh(question)
#TODO : test
def delete_question(question_id: int): def delete_question(question_id: int):
with Session(engine) as session: with Session(engine) as session:
question = session.get(Question, question_id) question = session.get(Question, question_id)

View File

@@ -0,0 +1,38 @@
from sqlmodel import Session, select
from src.app.models.user import User
from src.app.database import engine
def create_user(user: User):
with Session(engine) as session:
session.add(user)
session.commit()
session.refresh(user)
return user
def get_user_by_username(username: str):
with Session(engine) as session:
statement = select(User).where(User.username == username)
results = session.exec(statement)
result = results.first()
return result
def get_users():
with Session(engine) as session:
statement = select(User)
results = session.exec(statement)
users = results.all()
return users
def update_user(user_id: int, username: str, hash: str):
with Session(engine) as session:
user = session.get(User, user_id)
user.username = username if username else user.username
user.hash = hash if hash else user.hash
return user
def delete_user(user_id: int):
with Session(engine) as session:
user = session.get(User, user_id)
session.delete(user)
session.commit()

View File

@@ -1,13 +1,8 @@
import os from src.app.config import settings
from dotenv import load_dotenv
from sqlmodel import Session, SQLModel, create_engine from sqlmodel import Session, SQLModel, create_engine
load_dotenv()
database_uri=os.environ.get("DATABASE_URI")
connect_args = {"check_same_thread": False} connect_args = {"check_same_thread": False}
engine = create_engine(database_uri, echo=True, connect_args=connect_args) engine = create_engine(settings.DATABASE_URI, echo=False, connect_args=connect_args)
def create_db_and_tables(): def create_db_and_tables():
SQLModel.metadata.create_all(engine) SQLModel.metadata.create_all(engine)

View File

@@ -1,9 +1,9 @@
from src.app.models.knowledge import Knowledge from src.app.models.knowledge import Knowledge
from src.app.crud.crud_knowledges import create_knowledge, read_knowledges, read_knowledge, update_knowledge, delete_knowledge from src.app.data.knowledge import create_knowledge
from src.app.models.question import Question from src.app.models.question import Question
from src.app.crud.crud_questions import create_question, read_questions, read_question from src.app.data.question import create_question
from src.app.models.metric import Metric from src.app.models.metric import Metric
from src.app.crud.crud_metrics import create_metric from src.app.data.metric import create_metric
def faker(): def faker():
knowledge1 = Knowledge(content="La connaissance est une notion aux sens multiples, à la fois utilisée dans le langage courant et objet d'étude poussée de la part des sciences cognitives et des philosophes contemporains. ", uri="https://fr.wikipedia.org/wiki/Connaissance") knowledge1 = Knowledge(content="La connaissance est une notion aux sens multiples, à la fois utilisée dans le langage courant et objet d'étude poussée de la part des sciences cognitives et des philosophes contemporains. ", uri="https://fr.wikipedia.org/wiki/Connaissance")
@@ -37,3 +37,4 @@ def faker():
create_metric(metric3) create_metric(metric3)
create_metric(metric4) create_metric(metric4)
create_metric(metric5) create_metric(metric5)

View File

@@ -1,24 +1,23 @@
from src.app.config import settings
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import os
from dotenv import load_dotenv
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from src.app.database import create_db_and_tables from src.app.database import create_db_and_tables
# Import models in app
# TODO : best practice to manage models import # TODO : best practice to manage models import
from src.app.models.question import Question from src.app.models.question import Question
from src.app.models.knowledge import Knowledge from src.app.models.knowledge import Knowledge
from src.app.models.metric import Metric from src.app.models.metric import Metric
from src.app.models.user import User
from .api import router from .api import router
#Test #Fake data
from src.app.faker_seed import faker from src.app.faker_seed import faker
load_dotenv()
#TODO : alternative @app.on_event("startup") ? #TODO : alternative @app.on_event("startup") ?
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
@@ -30,7 +29,7 @@ async def lifespan(app: FastAPI):
app = FastAPI(lifespan=lifespan) app = FastAPI(lifespan=lifespan)
app.include_router(router) app.include_router(router)
origin = os.environ.get("ORIGIN") origin = settings.ORIGIN
origins = [origin] origins = [origin]
app.add_middleware( app.add_middleware(

View File

Binary file not shown.

View File

@@ -1,5 +1,6 @@
from sqlmodel import Field, SQLModel, Relationship from sqlmodel import Field, SQLModel, Relationship
#TODO : add pydantic validation from pydantic import BaseModel
from src.app.models.user import User
class Knowledge(SQLModel, table=True): class Knowledge(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True) id: int | None = Field(default=None, primary_key=True)
@@ -7,3 +8,10 @@ class Knowledge(SQLModel, table=True):
uri: str = Field(index=True) uri: str = Field(index=True)
questions: list["Question"] = Relationship(back_populates="knowledge", cascade_delete=True) # type: ignore questions: list["Question"] = Relationship(back_populates="knowledge", cascade_delete=True) # type: ignore
user_id: int | None = Field(default=None, foreign_key="user.id", ondelete="CASCADE")
user: User | None = Relationship(back_populates="knowledges")
class KnowledgeCreate(BaseModel):
content: str
uri:str

View File

@@ -1,7 +1,7 @@
from sqlmodel import Field, SQLModel, Relationship from sqlmodel import Field, SQLModel, Relationship
from pydantic import BaseModel
from src.app.models.question import Question from src.app.models.question import Question
from src.app.models.user import User
#TODO : add pydantic validation
class Metric(SQLModel, table=True): class Metric(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True) id: int | None = Field(default=None, primary_key=True)
@@ -10,3 +10,9 @@ class Metric(SQLModel, table=True):
question: Question | None = Relationship(back_populates="metrics") question: Question | None = Relationship(back_populates="metrics")
need_index: int need_index: int
user_id: int | None = Field(default=None, foreign_key="user.id", ondelete="CASCADE")
user: User | None = Relationship(back_populates="metrics")
class MetricCreate(BaseModel):
need_index: int

View File

@@ -1,6 +1,6 @@
from sqlmodel import Field, SQLModel, Relationship from sqlmodel import Field, SQLModel, Relationship
from src.app.models.knowledge import Knowledge from src.app.models.knowledge import Knowledge
#TODO : add pydantic validation from src.app.models.user import User
class Question(SQLModel, table=True): class Question(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True) id: int | None = Field(default=None, primary_key=True)
@@ -10,3 +10,6 @@ class Question(SQLModel, table=True):
knowledge: Knowledge | None = Relationship(back_populates="questions") knowledge: Knowledge | None = Relationship(back_populates="questions")
metrics: list["Metric"] = Relationship(back_populates="question", cascade_delete=True) # type: ignore metrics: list["Metric"] = Relationship(back_populates="question", cascade_delete=True) # type: ignore
user_id: int | None = Field(default=None, foreign_key="user.id", ondelete="CASCADE")
user: User | None = Relationship(back_populates="questions")

View File

@@ -0,0 +1,16 @@
from sqlmodel import Field, SQLModel, Relationship
from pydantic import BaseModel
class User(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
username: str
hashed_password: str
#is_active: bool
knowledges: list["Knowledge"] = Relationship(back_populates="user", cascade_delete=True) # type: ignore
questions: list["Question"] = Relationship(back_populates="user", cascade_delete=True) # type: ignore
metrics: list["Metric"] = Relationship(back_populates="user", cascade_delete=True) # type: ignore
class UserCreate(BaseModel):
username: str
plain_password: str

View File

@@ -1,15 +1,11 @@
import os
import spacy import spacy
from src.app.config import settings
from openai import OpenAI from openai import OpenAI
from pydantic import BaseModel
from src.app.models.knowledge import Knowledge from src.app.models.knowledge import Knowledge
language_model_api=os.environ.get("LANGUAGE_MODEL_API")
model_name=os.environ.get("LANGUAGE_MODEL_NAME")
client = OpenAI( client = OpenAI(
base_url=language_model_api, base_url=settings.LANGUAGE_MODEL_API,
api_key = "sk-no-key-required" api_key = "sk-no-key-required"
) )
@@ -18,12 +14,12 @@ nlp = spacy.load("fr_core_news_sm")
def questions_generation(knowledge: Knowledge): def questions_generation(knowledge: Knowledge):
context = "Texte : ```" + knowledge.content + "```" context = "Texte : ```" + knowledge.content + "```"
instruction = "A partir du texte génère 3 questions :" instruction = "A partir du texte génère 3 questions. Ces 3 questions vise à approfonfir (augmenter, intensifier, accroître, creuser) le texte. "
prompt = context + "\n" + instruction prompt = context + "\n" + instruction
#SLM processing #SLM processing
response = client.responses.create( response = client.responses.create(
model=model_name, model=settings.LANGUAGE_MODEL_NAME,
input=[ input=[
{"role": "system", "content": "Question Generation"}, {"role": "system", "content": "Question Generation"},
{"role": "user", "content": prompt}], {"role": "user", "content": prompt}],

View File

@@ -1 +1,3 @@
# PROD
# VITE_API_URL=http://127.0.0.1:8082/
VITE_API_URL=http://127.0.0.1:8000/ VITE_API_URL=http://127.0.0.1:8000/

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title> <title>Manolia</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@@ -8,6 +8,7 @@
"name": "user-interface", "name": "user-interface",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"axios": "^1.13.6",
"buefy": "^3.0.4", "buefy": "^3.0.4",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"vue": "^3.5.27", "vue": "^3.5.27",
@@ -1250,9 +1251,9 @@
} }
}, },
"node_modules/@eslint/config-array/node_modules/minimatch": { "node_modules/@eslint/config-array/node_modules/minimatch": {
"version": "10.2.2", "version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"dev": true, "dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
@@ -3278,7 +3279,6 @@
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/at-least-node": { "node_modules/at-least-node": {
@@ -3309,10 +3309,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.13.5", "version": "1.13.6",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.11", "follow-redirects": "^1.15.11",
@@ -3324,7 +3323,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/balanced-match": { "node_modules/balanced-match": {
@@ -3564,7 +3562,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@@ -3798,7 +3795,6 @@
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
@@ -4125,7 +4121,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.4.0" "node": ">=0.4.0"
@@ -4135,7 +4130,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.1", "call-bind-apply-helpers": "^1.0.1",
@@ -4172,15 +4166,15 @@
} }
}, },
"node_modules/editorconfig": { "node_modules/editorconfig": {
"version": "1.0.4", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz",
"integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@one-ini/wasm": "0.1.1", "@one-ini/wasm": "0.1.1",
"commander": "^10.0.0", "commander": "^10.0.0",
"minimatch": "9.0.1", "minimatch": "^9.0.1",
"semver": "^7.5.3" "semver": "^7.5.3"
}, },
"bin": { "bin": {
@@ -4190,23 +4184,6 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/editorconfig/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/editorconfig/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/editorconfig/node_modules/commander": { "node_modules/editorconfig/node_modules/commander": {
"version": "10.0.1", "version": "10.0.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
@@ -4217,22 +4194,6 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/editorconfig/node_modules/minimatch": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
"integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.302", "version": "1.5.302",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
@@ -4298,7 +4259,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -4308,7 +4268,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -4325,7 +4284,6 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0" "es-errors": "^1.3.0"
@@ -4338,7 +4296,6 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@@ -4588,9 +4545,9 @@
} }
}, },
"node_modules/eslint/node_modules/minimatch": { "node_modules/eslint/node_modules/minimatch": {
"version": "10.2.2", "version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"dev": true, "dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
@@ -4965,7 +4922,6 @@
"version": "1.15.11", "version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@@ -5026,7 +4982,6 @@
"version": "4.0.5", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
@@ -5081,7 +5036,6 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@@ -5101,7 +5055,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.2", "call-bind-apply-helpers": "^1.0.2",
@@ -5126,7 +5079,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"dunder-proto": "^1.0.1", "dunder-proto": "^1.0.1",
@@ -5230,7 +5182,6 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -5260,7 +5211,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -5273,7 +5223,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"has-symbols": "^1.0.3" "has-symbols": "^1.0.3"
@@ -5306,7 +5255,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
@@ -6174,7 +6122,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -6231,7 +6178,6 @@
"version": "1.52.0", "version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@@ -6241,7 +6187,6 @@
"version": "2.1.35", "version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"mime-db": "1.52.0" "mime-db": "1.52.0"
@@ -6261,13 +6206,13 @@
} }
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "9.0.6", "version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^5.0.2" "brace-expansion": "^2.0.2"
}, },
"engines": { "engines": {
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
@@ -6276,6 +6221,23 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/minimatch/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/minimatch/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/minimist": { "node_modules/minimist": {
"version": "1.2.8", "version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",

View File

@@ -19,6 +19,7 @@
"format": "prettier --write --experimental-cli src/" "format": "prettier --write --experimental-cli src/"
}, },
"dependencies": { "dependencies": {
"axios": "^1.13.6",
"buefy": "^3.0.4", "buefy": "^3.0.4",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"vue": "^3.5.27", "vue": "^3.5.27",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -17,18 +17,17 @@ import AppTopbar from '@/components/AppTopbar.vue'
<style> <style>
#app { #app {
background-color: #FFF4EA;
padding-inline: 5%; padding-inline: 5%;
height: 100vh; padding-top: 8px;
} padding-bottom: 32px;
min-height: 100vh;
/* main { display: flex;
height: 100%; flex-direction: column;
} }
main{
@media screen and (min-width: 768px) { flex: 1;
#app { display: flex;
height: 100vh; flex-direction: column;
} }
} */
</style> </style>

View File

@@ -0,0 +1,6 @@
@font-face{
font-family:"Exo-Black";
src: local("Exo-Black"), url("./fonts/Exo-Black.ttf");
format: "truetype";
font-weight: black;
}

Binary file not shown.

View File

@@ -0,0 +1,37 @@
body{
background-color: #FFF4EA;
min-height: 100vh;
}
.container{
background-color: #ffffff;
border-radius: 16px;
padding: 34px;
height: 100%;
display: flex;
flex-direction: column;
gap: 16px;
}
.title-container{
display: flex;
flex-direction: row;
margin-bottom: 16px;
gap: 8px;
.title-icon{
display: flex;
justify-content: center;
align-items: center;
padding: 4px;
border-radius: 8px;
background-color: #ebebeb;
img{
width: 24px;
height: 24px;
}
}
h2 {
font-size: large;
}
}

View File

@@ -0,0 +1,22 @@
<!--
tags: [test, visual, user]
version: "1.76"
unicode: "f25f"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="#000000"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M16 21h3c.81 0 1.48 -.67 1.48 -1.48l.02 -.02c0 -.82 -.69 -1.5 -1.5 -1.5h-3v3z" />
<path d="M16 15h2.5c.84 -.01 1.5 .66 1.5 1.5s-.66 1.5 -1.5 1.5h-2.5v-3z" />
<path d="M4 9v-4c0 -1.036 .895 -2 2 -2s2 .964 2 2v4" />
<path d="M2.99 11.98a9 9 0 0 0 9 9m9 -9a9 9 0 0 0 -9 -9" />
<path d="M8 7h-4" />
</svg>

After

Width:  |  Height:  |  Size: 585 B

View File

@@ -0,0 +1,19 @@
<!--
category: System
tags: [tick, "yes", confirm]
version: "1.0"
unicode: "ea5e"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="#000000"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 12l5 5l10 -10" />
</svg>

After

Width:  |  Height:  |  Size: 321 B

View File

@@ -0,0 +1,22 @@
<!--
tags: [text, paper, report, details, job]
category: Document
version: "1.55"
unicode: "f028"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="#000000"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M14 3v4a1 1 0 0 0 1 1h4" />
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z" />
<path d="M9 17h6" />
<path d="M9 13h6" />
</svg>

After

Width:  |  Height:  |  Size: 476 B

View File

@@ -0,0 +1,20 @@
<!--
tags: [arrow, data, paper, document, format]
category: Document
version: "1.37"
unicode: "edea"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="#000000"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M14 3v4a1 1 0 0 0 1 1h4" />
<path d="M5 13v-8a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-5.5m-9.5 -2h7m-3 -3l3 3l-3 3" />
</svg>

After

Width:  |  Height:  |  Size: 440 B

View File

@@ -0,0 +1,19 @@
<!--
category: Communication
version: "2.10"
unicode: "f980"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="#000000"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M10.517 19.869a9.757 9.757 0 0 1 -2.817 -.869l-4.7 1l1.3 -3.9c-2.324 -3.437 -1.426 -7.872 2.1 -10.374c3.526 -2.501 8.59 -2.296 11.845 .48c1.666 1.421 2.594 3.29 2.747 5.21" />
<path d="M17.8 20.817l-2.172 1.138a.392 .392 0 0 1 -.568 -.41l.415 -2.411l-1.757 -1.707a.389 .389 0 0 1 .217 -.665l2.428 -.352l1.086 -2.193a.392 .392 0 0 1 .702 0l1.086 2.193l2.428 .352a.39 .39 0 0 1 .217 .665l-1.757 1.707l.414 2.41a.39 .39 0 0 1 -.567 .411l-2.172 -1.138z" />
</svg>

After

Width:  |  Height:  |  Size: 734 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -0,0 +1,19 @@
<!--
tags: [sign, symbol, ask, sentence, word, letters, "?"]
version: "1.16"
unicode: "ec9d"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="#000000"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M8 8a3.5 3 0 0 1 3.5 -3h1a3.5 3 0 0 1 3.5 3a3 3 0 0 1 -2 3a3 4 0 0 0 -2 4" />
<path d="M12 19l0 .01" />
</svg>

After

Width:  |  Height:  |  Size: 417 B

View File

@@ -1,29 +1,33 @@
<script setup lang="ts">
import { BNavbar, BNavbarItem } from "buefy";
</script>
<template> <template>
<div class="topbar-container"> <b-navbar class="navbar" shadow >
<div class="logo-container"> <template #brand>
<b-navbar-item tag="router-link" :to="{ path: '/' }" id="brand">
<img class="logo" src="../assets/svg/manolia-logo.svg" alt="Manolia logo" /> <img class="logo" src="../assets/svg/manolia-logo.svg" alt="Manolia logo" />
<span>MANOLIA</span> <h1 class="title">MANOLIA</h1>
</div> </b-navbar-item>
</div> </template>
</b-navbar>
</template> </template>
<style scoped> <style scoped>
.topbar-container { .navbar{
border-radius: 16px;
margin-bottom: 24px;
height: 4rem; height: 4rem;
width: 100%;
display: flex;
align-items: center;
justify-content: left;
} }
.logo-container {
width: 20rem;
display: flex;
align-items: center;
gap: 0.5rem;
img { #brand:hover, #brand:focus, #brand:active{
width: 2rem; background-color: inherit;
} }
.title{
font-size: 1.5rem;
font-family: "Exo-Black";
} }
</style> </style>

View File

@@ -1,12 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { BField, BInput, BButton, useToast } from "buefy"; import { BField, BInput, BButton, useToast } from "buefy"
import { apiClient } from "@/services/api"; import api from "@/services/api"
import { ref } from "vue"; import type { AxiosResponse } from "axios"
import { ref } from "vue"
import type { Knowledge } from "@/types/types"; import type { Knowledge, KnowledgeCreate } from "@/types/types"
import { useStepStore } from '@/stores/step' import { useStepStore } from '@/stores/step'
import { useItemStore } from '@/stores/item' import { useItemStore } from '@/stores/item'
const stepStore = useStepStore() const stepStore = useStepStore()
const itemStore = useItemStore() const itemStore = useItemStore()
@@ -16,16 +18,16 @@
const Toast = useToast() const Toast = useToast()
async function postKnowledge(){ async function postKnowledge(){
const knowledge: Knowledge = { const knowledge: KnowledgeCreate = {
id: null,
content: knowledgeModel.value, content: knowledgeModel.value,
uri: uriModel.value uri: uriModel.value
} }
if(validation(knowledge)){ if(validation(knowledge)){
try { try {
const response: Knowledge = await apiClient.post("api/v1/knowledges/", knowledge) const response: AxiosResponse = await api.post("api/v1/knowledges/", knowledge)
const knowledge_data: Knowledge = response.data
Toast.open({message: "Knowledge collected", type: "is-success"}) Toast.open({message: "Knowledge collected", type: "is-success"})
itemStore.$patch({ knowledge:response }) itemStore.$patch({ knowledge: knowledge_data })
stepStore.nextStep() stepStore.nextStep()
} }
catch { catch {
@@ -34,7 +36,7 @@
} }
} }
function validation(knowledge: Knowledge){ function validation(knowledge: KnowledgeCreate){
return knowledge.content && knowledge.uri return knowledge.content && knowledge.uri
} }
@@ -42,12 +44,39 @@
<template> <template>
<div class="container"> <div class="container">
<h2>Collect Knowledge</h2> <div class="title-container">
<b-field label="Knowledge"> <div class="title-icon">
<img src="../assets/svg/file-import.svg" alt="file import" />
</div>
<h2>Collecter une connaissance</h2>
</div>
<div class="body-container">
<div>
<ul class="list">
<li class="list-item">
<span class="list-index">1</span>
<p>Dans votre base de connaissances, identifiez un texte contenant de l'incertitude.</p>
</li>
<li class="list-item">
<span class="list-index">2</span>
<p>Selectionnez la partie du texte qui nécessite des précisions et copiez/coller vers le formulaire.</p>
</li>
<li class="list-item">
<span class="list-index">3</span>
<p>Selectionnez l'URI se trouve la connaissance et copiez/coller vers le formulaire.</p>
</li>
<li class="list-item">
<span class="list-index">4</span>
<p>Partager via le formulaire.</p>
</li>
</ul>
</div>
<div class="form">
<b-field label="Connaissance">
<!-- @vue-ignore --> <!-- @vue-ignore -->
<b-input <b-input
v-model="knowledgeModel" v-model="knowledgeModel"
placeholder="Knowledge is an awareness of facts, a familiarity with individuals and situations, or a practical skill." placeholder="La connaissance est une notion aux sens multiples, à la fois utilisée dans le langage courant et objet d'étude poussée de la part des sciences cognitives et des philosophes contemporains."
maxlength="1200" maxlength="1200"
type="textarea" type="textarea"
required required
@@ -57,7 +86,7 @@
<!-- @vue-ignore --> <!-- @vue-ignore -->
<b-input <b-input
v-model="uriModel" v-model="uriModel"
placeholder="en.wikipedia.org/wiki/Knowledge" placeholder="fr.wikipedia.org/wiki/Connaissance"
maxlength="100" maxlength="100"
required required
></b-input> ></b-input>
@@ -65,24 +94,52 @@
<div class="btn-container"> <div class="btn-container">
<b-field> <b-field>
<div class="control"> <div class="control">
<b-button type="is-primary" @click="postKnowledge" >Share</b-button> <b-button type="is-primary" @click="postKnowledge" >Partager</b-button>
</div> </div>
</b-field> </b-field>
</div> </div>
</div> </div>
</div>
</div>
</template> </template>
<style scoped> <style scoped>
.container{ .body-container{
background-color: #ffffff; display: flex;
border-radius: 16px; flex-direction: column;
padding: 34px; gap: 48px;
} }
.btn-container{ .btn-container{
height: 100%;
display: flex; display: flex;
justify-content: end; justify-content: end;
} }
.list{
padding-left: 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.list-item{
display: flex;
flex-direction: raw;
align-items: center;
gap: 8px;
}
.list-index{
display: flex;
justify-content: center;
align-items: center;
width: 24px;
height: 24px;
border: 1px solid #D6D9E0;
border-radius: 4px;
}
.form{
border: 1px solid #D6D9E0;
padding: 24px;
border-radius: 16px;
}
</style> </style>

View File

@@ -1,45 +1,55 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { onBeforeMount, ref, watch } from "vue";
import { onBeforeMount } from 'vue'
import type { Knowledge } from "@/types/types";
import type { Question } from "@/types/types";
import type { Metric } from "@/types/types";
//import { useStepStore } from '@/stores/step' import type { Knowledge, Question, MetricCreate, Metric } from "@/types/types"
import { BCollapse, BSlider } from "buefy";
import { useStepStore } from '@/stores/step'
import { useItemStore } from '@/stores/item' import { useItemStore } from '@/stores/item'
import { apiClient } from "@/services/api"; import api from "@/services/api"
import type { AxiosResponse } from "axios";
//const stepStore = useStepStore() const stepStore = useStepStore()
const itemStore = useItemStore() const itemStore = useItemStore()
const questions = ref<Question[]>() const questions = ref<Question[]>()
const metrics = ref<Metric[]>([]) const metrics = ref<Metric[]>([])
onBeforeMount(async () => { onBeforeMount(async () => {
if(itemStore.knowledge != undefined){ if(!itemStore.knowledge){
questions.value = await getQuestions(itemStore.knowledge)
}
else{
throw new Error("There is no knowledge element in itemStore."); throw new Error("There is no knowledge element in itemStore.");
} }
if(questions.value != undefined){ questions.value = await getQuestions(itemStore.knowledge)
initializeMetrics(questions.value)
} if(!questions.value){
else{
throw new Error("There is no questions element from API."); throw new Error("There is no questions element from API.");
} }
initializeMetrics(questions.value)
})
watch( () => itemStore.knowledge, async () =>{
metrics.value = []
if(!itemStore.knowledge){
throw new Error("There is no knowledge element in itemStore.");
}
questions.value = await getQuestions(itemStore.knowledge)
if(!questions.value){
throw new Error("There is no questions element from API.");
}
initializeMetrics(questions.value)
}) })
async function getQuestions(knowledge: Knowledge): Promise<Question[]>{ async function getQuestions(knowledge: Knowledge): Promise<Question[]>{
return apiClient.get(`api/v1/knowledges/${knowledge.id}/questions`) const response: AxiosResponse = await api.get(`api/v1/knowledges/${knowledge.id}/questions`)
const questions: Question[] = response.data
return questions
} }
function initializeMetrics(questions: Question[]){ function initializeMetrics(questions: Question[]){
questions.forEach((q)=>{ questions.forEach((q)=>{
const metric: Metric = { const metric: Metric = {
//id: null,
question_id: q.id!, question_id: q.id!,
need_index: -1 need_index: -1
} }
@@ -52,24 +62,73 @@
return metrics.value.findIndex((metric) => metric.question_id === question.id) return metrics.value.findIndex((metric) => metric.question_id === question.id)
} }
else{ else{
throw new Error("The is no metrics element"); throw new Error("The is no metrics element")
} }
} }
async function postMetrics(){ async function postMetrics(){
console.log( metrics.value) console.log( metrics.value)
metrics.value?.forEach(async (metric) => { metrics.value?.forEach(async (metric) => {
await apiClient.post(`api/v1/metrics/`, metric) const metricCreate: MetricCreate = { need_index: metric.need_index }
const response = await api.post(`api/v1/questions/${metric.question_id}/metrics`, metricCreate)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const metric_data = response.data
}) })
stepStore.nextStep()
} }
</script> </script>
<template> <template>
<div class="container"> <div class="container">
<h2>Evaluate Questions</h2> <div class="title-container">
<ul> <div class="title-icon">
<li v-for="(question, index) in questions" :key="index"> <img src="../assets/svg/message-circle-star.svg" alt="evaluate icon" />
<p>{{ question.question }}</p> </div>
<h2>Evaluer les questions</h2>
</div>
<div>
<ul class="list">
<li class="list-item">
<span class="list-index">1</span>
<p>Un ensemble de questions a été généré à partir d'une connaissance que vous avez saisie (ci-dessous).</p>
</li>
<li class="list-item">
<span class="list-index">2</span>
<p>Mise en situation : si vous deviez vous adresser à une personne capable d'améliorer la connaissance incertaine, la question générée serait-elle pertinente ?</p>
</li>
<li class="list-item">
<span class="list-index">3</span>
<p>Evaluez de 0 à 100, pour chaque question, la pertinence de celle-ci.</p>
</li>
</ul>
</div>
<b-collapse class="card" animation="slide" aria-id="contentIdForA11y3">
<template #trigger="props">
<div
class="card-header"
role="button"
aria-controls="contentIdForA11y3"
:aria-expanded="props.open"
>
<p class="card-header-title">Connaissance</p>
<a class="card-header-icon">
{{ props.open ? "▲" : "▼" }}
</a>
</div>
</template>
<div class="card-content">
<div class="content">
{{ itemStore.knowledge?.content }}
</div>
</div>
</b-collapse>
<ul class="question-list">
<li v-for="(question, index) in questions?.slice(0,3)" :key="index" class="question-list-item">
<div class="question">
<img src="../assets/svg/question-mark.svg" alt="question-mark " />
{{ question.question }}
</div>
<b-field> <b-field>
<b-slider v-model="metrics![getIndexMetrics(question)]!.need_index"></b-slider> <b-slider v-model="metrics![getIndexMetrics(question)]!.need_index"></b-slider>
</b-field> </b-field>
@@ -78,7 +137,7 @@
<div class="btn-container"> <div class="btn-container">
<b-field> <b-field>
<div class="control"> <div class="control">
<b-button type="is-primary" @click="postMetrics" >Share</b-button> <b-button type="is-primary" @click="postMetrics" >Partager</b-button>
</div> </div>
</b-field> </b-field>
</div> </div>
@@ -86,13 +145,57 @@
</template> </template>
<style scoped> <style scoped>
.container{
background-color: #ffffff;
border-radius: 16px;
padding: 34px;
}
.btn-container{ .btn-container{
display: flex; display: flex;
justify-content: end; justify-content: end;
} }
.list{
padding-left: 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.list-item{
display: flex;
flex-direction: raw;
align-items: center;
gap: 8px;
}
.list-index{
display: flex;
justify-content: center;
align-items: center;
width: 24px;
height: 24px;
border: 1px solid #D6D9E0;
border-radius: 4px;
}
.card-header{
box-shadow: none;
}
.card{
box-shadow: none;
border: 1px solid #D6D9E0;
}
.question-list{
display: flex;
flex-direction: column;
gap: 12px;
}
.question-list-item{
padding: 24px;
border: 1px solid #D6D9E0;
border-radius: 16px;
}
.question{
background-color: #ebebeb;
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
border-radius: 4px;
img{
border-right: 1px solid;
}
}
</style> </style>

View File

@@ -3,10 +3,12 @@
import { BProgress } from "buefy"; import { BProgress } from "buefy";
import type { Knowledge } from "@/types/types"; import type { Knowledge } from "@/types/types";
import { apiClient } from "@/services/api"; import api from "@/services/api";
import { useItemStore } from '@/stores/item' import { useItemStore } from '@/stores/item'
import { useStepStore } from '@/stores/step' import { useStepStore } from '@/stores/step'
import { ProcessStep } from '@/services/knowledgeWorklow'
const stepStore = useStepStore() const stepStore = useStepStore()
const itemStore = useItemStore() const itemStore = useItemStore()
@@ -17,22 +19,29 @@
}) })
async function generateQuestions (knowledge: Knowledge) { async function generateQuestions (knowledge: Knowledge) {
await apiClient.post(`api/v1/knowledges/${knowledge.id}/questions`, null) await api.post(`api/v1/knowledges/${knowledge.id}/questions`)
if(stepStore.indexStep == ProcessStep.WaitGeneration)
stepStore.nextStep() stepStore.nextStep()
} }
</script> </script>
<template> <template>
<div class="container"> <div class="container">
<h2>Generation</h2> <div class="title-container">
<div class="title-icon">
<img src="../assets/svg/a-b-2.svg" alt="generation icon" />
</div>
<h2>Génération de questions</h2>
</div>
<p>La génération par Small Language Model (SML) peut prendre plusieurs minutes, nous éxécutons le modèle sur CPU.</p>
<p>En attendant, vous pouvez ajouter des nouvelles connaissances.</p>
<div>
<img src="../assets/svg/neural-network.svg" alt="neural network" />
</div>
<b-progress></b-progress> <b-progress></b-progress>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.container{
background-color: #ffffff;
border-radius: 16px;
padding: 34px;
}
</style> </style>

View File

@@ -1,17 +1,149 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive, onMounted, onBeforeMount } from "vue"
import api from "@/services/api"
import { BButton } from "buefy"
import { identifyProcessStep, ProcessStep }from "@/services/knowledgeWorklow"
import type { AxiosResponse } from "axios"
import type { Knowledge } from "@/types/types"
import { useStepStore } from '@/stores/step'
import { useItemStore } from '@/stores/item'
const stepStore = useStepStore()
const itemStore = useItemStore()
interface KnowledgesWorkflow {
knowledge: Knowledge,
processStep: ProcessStep,
isSelected: boolean
}
const knowledgesWorkflow = reactive<KnowledgesWorkflow[]>([])
async function getKnowledges(): Promise<Knowledge[]>{
const response: AxiosResponse<Knowledge[]> = await api.get("api/v1/knowledges/")
return response.data
}
function truncateString(str: string){
return (str.length <= 18) ? str : str.slice(0, 9) + "..." + str.slice(-9)
}
async function initializeKnowledgeWorkflow(){
const knowledges: Knowledge[] = await getKnowledges()
knowledges.forEach(async (knowledge) => {
const kWorkflow: KnowledgesWorkflow = {
knowledge,
processStep: await identifyProcessStep(knowledge as Knowledge),
isSelected: false
}
knowledgesWorkflow.push(kWorkflow)
})
}
async function getKnowledgeWorkflow(){
if(knowledgesWorkflow.length == 0){
initializeKnowledgeWorkflow()
} else {
const knowledges: Knowledge[] = await getKnowledges()
knowledges.forEach(async (knowledge) => {
const kWorkflow: KnowledgesWorkflow = {
knowledge,
processStep: await identifyProcessStep(knowledge as Knowledge),
isSelected: false
}
const indexKW = knowledgesWorkflow.findIndex((kW: KnowledgesWorkflow) => kW.knowledge.id === knowledge.id)
indexKW != -1 ? knowledgesWorkflow[indexKW] = kWorkflow : knowledgesWorkflow.push(kWorkflow)
})
}
}
onBeforeMount(() => getKnowledgeWorkflow())
onMounted(()=>{
setInterval(() => getKnowledgeWorkflow(), 10000)
})
function goToEvaluateQuestion(knowledge_data: Knowledge){
itemStore.$patch({ knowledge: knowledge_data })
stepStore.goToStep(ProcessStep.EvaluateQuestion)
}
function goToCollectKnowledge(){
stepStore.goToStep(ProcessStep.CollectKnowledge)
}
</script> </script>
<template> <template>
<div class="container"> <div class="container">
<h2>Knowledge</h2> <div class="title-container">
<div class="title-icon">
<img src="../assets/svg/file-description.svg" alt="file description" />
</div>
<h2>Connaissances</h2>
</div>
<ul>
<li v-for="(kW, index) in knowledgesWorkflow" :key="index" class="list-element">
<div class="list-element-index">
{{ index + 1 }}
</div>
<div class="list-element-body">
<span class="list-element-text">
{{ truncateString(kW.knowledge.uri) }}
</span>
<!-- <b-button v-if="kW.processStep == ProcessStep.CollectKnowledge" type="is-primary" @click="goTo" >
Collect
</b-button> -->
<b-button v-if="kW.processStep == ProcessStep.WaitGeneration" type="is-info is-light" loading >
...
</b-button>
<b-button v-if="kW.processStep == ProcessStep.EvaluateQuestion" type="is-warning" @click="goToEvaluateQuestion(kW.knowledge)" >
Evaluer
</b-button>
<b-button v-if="kW.processStep == ProcessStep.ProcessDone" type="is-success is-light" disabled >
Fait
</b-button>
</div>
</li>
<li >
<b-button type="is-primary" outlined @click="goToCollectKnowledge()" >
Ajouter une connaissance
</b-button>
</li>
</ul>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.container{ .list-element{
background-color: #ffffff; margin-bottom: 8px;
border-radius: 16px;
padding: 34px; border: 1px solid #D6D9E0;
border-radius: 8px;
display: grid;
grid-template-columns: 40px 1fr;
}
.list-element-index{
display: flex;
justify-content: center;
align-items: center;
border-right: 1px solid #D6D9E0;
}
.list-element-body{
display: grid;
grid-template-columns: 66% 34%;
}
.list-element-text{
display: flex;
align-items: center;
padding-left: 12px;
} }
</style> </style>

View File

@@ -6,6 +6,9 @@ import router from './router'
import Buefy from "buefy"; import Buefy from "buefy";
import "buefy/dist/css/buefy.css"; import "buefy/dist/css/buefy.css";
import "@/assets/fonts.css"
import "@/assets/global.css"
const app = createApp(App) const app = createApp(App)

View File

@@ -1,4 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import { isAuthenticated } from '@/services/api'
const pagesWithoutGuard = ['login', 'app', 'register']
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@@ -7,9 +10,35 @@ const router = createRouter({
path: '/', path: '/',
alias: '/app', alias: '/app',
name: 'app', name: 'app',
component: () => import('@/views/WelcomeView.vue'),
},
{
path: '/experiment',
alias: '/experiment',
name: 'experiment',
component: () => import('@/views/ExperimentView.vue'), component: () => import('@/views/ExperimentView.vue'),
}, },
{
path: '/login',
alias: '/login',
name: 'login',
component: () => import('@/views/LoginView.vue'),
},
{
path: '/register',
alias: '/register',
name: 'register',
component: () => import('@/views/RegisterView.vue'),
}
], ],
}) })
// Guard system
router.beforeEach(async (to, from) => {
const isAuth = await isAuthenticated()
if (!isAuth && !pagesWithoutGuard.includes(to.name!.toString())) {
return { name: 'login' }
}
})
export default router export default router

View File

@@ -1,74 +1,45 @@
class ApiClient { import axios from "axios"
private baseURL: string import type { AxiosResponse } from "axios";
private defaultHeaders: Record<string, string>
constructor(baseURL: string) { const api = axios.create({
this.baseURL = baseURL baseURL: import.meta.env.VITE_API_URL
this.defaultHeaders = { });
'Content-Type': 'application/json',
api.interceptors.request.use((config) => {
const token = localStorage.getItem('access_token')
if (token){
config.headers.Authorization = `Bearer ${token}`
} }
return config
})
export const authAPI = {
register: (username: string, password: string) =>
api.post(
'/api/v1/auth/register',
{ "username":username, "plain_password":password }
),
login: (username: string, password: string) =>
api.post(
'/api/v1/auth/login',
new URLSearchParams({ username, password }),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }}
),
getMe: () => api.get('/api/v1/auth/me')
} }
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> { export const isAuthenticated = async () => {
const url = `${this.baseURL}${endpoint}`
const config: RequestInit = {
headers: { ...this.defaultHeaders, ...options.headers },
...options,
}
let response
try { try {
response = await fetch(url, config) const response: AxiosResponse = await authAPI.getMe()
} catch (error) { if (response.status==200)
if (error instanceof Error) { return true
throw new HTTPError(`Network error: ${error.message}`) else
return false
} }
throw new HTTPError('Unknown network error') catch{
return false
} }
if (response?.ok) {
return response.json()
} }
const errorData = await response.json().catch(() => ({})) export default api;
throw new HTTPError(
errorData.message || `HTTP ${response.status}: ${response.statusText}`,
response.status,
response,
)
}
/**
* HTTP Get
* @param endpoint
* @returns
*/
async get<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint, { method: 'GET' })
}
/**
* HTTP Post
* @param endpoint
* @param data
* @returns
*/
async post<T>(endpoint: string, data: unknown): Promise<T> {
return this.request<T>(endpoint, { method: 'POST', body: JSON.stringify(data) })
}
}
class HTTPError extends Error {
constructor(
message: string,
private status: number | null = null,
private reponse: Response | null = null,
) {
super(message)
this.name = 'HTTPError'
}
}
export const apiClient = new ApiClient(import.meta.env.VITE_API_URL)

View File

@@ -0,0 +1,54 @@
import type { Knowledge, Metric, Question } from "@/types/types"
import api from "@/services/api"
import type { AxiosResponse } from "axios"
const processFlow = ["CollectKnowledge", "GenerateQuestion", "EvaluateQuestion", "Done"]
//From user action in worlfow
export enum ProcessStep {
CollectKnowledge,
WaitGeneration,
EvaluateQuestion,
ProcessDone
}
export async function identifyProcessStep(knowledge: Knowledge | null): Promise<ProcessStep>{
if (knowledge == null)
return ProcessStep.CollectKnowledge
//Check questions exist
const apiGetQuestions: AxiosResponse<Question[]> = await api.get(
`api/v1/knowledges/${knowledge.id}/questions/`,
{
validateStatus: function (status) {
return status < 500
},
})
if (apiGetQuestions.status >= 400 && apiGetQuestions.status < 500)
return ProcessStep.WaitGeneration
const questions: Question[] = apiGetQuestions.data
if (questions.length == 0)
return ProcessStep.WaitGeneration
//Check metrics exist
const apiGetMetrics: AxiosResponse<Metric[]> = await api.get(
`api/v1/questions/${questions[0]!.id}/metrics/`,
{
validateStatus: function (status) {
return status < 500
},
})
if (apiGetMetrics.status >= 400 && apiGetMetrics.status < 500)
return ProcessStep.EvaluateQuestion
const metrics: Metric[] = apiGetMetrics.data
if (metrics.length == 0)
return ProcessStep.EvaluateQuestion
//Data is complete
return ProcessStep.ProcessDone
}

View File

@@ -5,6 +5,8 @@ import CollectKnowledge from '@/components/CollectKnowledge.vue'
import EvaluateQuestion from '@/components/EvaluateQuestion.vue' import EvaluateQuestion from '@/components/EvaluateQuestion.vue'
import GenerateQuestion from '@/components/GenerateQuestion.vue' import GenerateQuestion from '@/components/GenerateQuestion.vue'
import { ProcessStep } from '@/services/knowledgeWorklow'
const steps: Component = [ const steps: Component = [
CollectKnowledge, CollectKnowledge,
GenerateQuestion, GenerateQuestion,
@@ -17,8 +19,15 @@ export const useStepStore = defineStore('step', () => {
const getCurrentComponent = computed(() => steps[indexStep.value]) const getCurrentComponent = computed(() => steps[indexStep.value])
function nextStep() { function nextStep() {
if(indexStep.value + 1 < steps.length)
indexStep.value++ indexStep.value++
else
indexStep.value = 0
} }
return { steps, getCurrentComponent, nextStep } function goToStep(processStep: ProcessStep){
indexStep.value = processStep
}
return { indexStep, steps, getCurrentComponent, nextStep, goToStep }
}) })

View File

@@ -1,7 +1,13 @@
interface KnowledgeCreate{
content: string,
uri: string,
}
interface Knowledge { interface Knowledge {
id: number | null, id: number | null,
content: string, content: string,
uri: string, uri: string,
user: User
} }
interface Question { interface Question {
@@ -9,12 +15,23 @@ interface Question {
question: string, question: string,
knowledgeId: number, knowledgeId: number,
metric: Metric | null metric: Metric | null
user: User
} }
interface Metric { interface Metric {
//id: number | null, //id
question_id: number, question_id: number,
need_index: number need_index: number
//user
}
interface MetricCreate {
need_index: number
} }
export type {Knowledge, Question, Metric} interface User {
username: string,
token: string
}
export type {KnowledgeCreate, Knowledge, Question, MetricCreate, Metric}

View File

@@ -13,9 +13,6 @@
<section id="section-central"> <section id="section-central">
<component :is="stepStore.getCurrentComponent"></component> <component :is="stepStore.getCurrentComponent"></component>
</section> </section>
<!-- <section>
<EvaluateQuestion/>
</section> -->
</div> </div>
</template> </template>
@@ -24,5 +21,8 @@
display: grid; display: grid;
grid-template-columns: 1fr 3fr; grid-template-columns: 1fr 3fr;
gap: 24px; gap: 24px;
height: 100%;
flex: 1;
} }
</style> </style>

View File

@@ -0,0 +1,60 @@
<script setup lang="ts">
import router from '@/router/index'
import { BField, BInput, BButton, useToast } from "buefy";
import { ref } from "vue"
import { authAPI } from '@/services/api'
const username = ref<string>("")
const password = ref<string>("")
const Toast = useToast()
async function login() {
try {
const response = await authAPI.login(username.value, password.value)
const { access_token, refresh_token } = response.data
localStorage.setItem('access_token', access_token)
localStorage.setItem('refresh_token', refresh_token)
router.push({ path: '/experiment' })
} catch (err){
console.log(err)
Toast.open({message: "Login failed", type: "is-danger"})
}
}
</script>
<template>
<div class="container container-size">
<b-field label="Username">
<!-- @vue-ignore -->
<b-input
v-model="username"
placeholder="Alice"
maxlength="20"
required
></b-input>
</b-field>
<b-field label="Password">
<!-- @vue-ignore -->
<b-input
v-model="password"
maxlength="50"
type="password"
required
></b-input>
</b-field>
<b-button type="is-primary" @click="login">Login</b-button>
</div>
</template>
<style scoped>
.container-size{
flex: none;
gap:0;
width: 20rem;
height: 20rem ;
}
</style>

View File

@@ -0,0 +1,63 @@
<script setup lang="ts">
import router from '@/router/index'
import { BField, BInput, BButton, useToast } from "buefy";
import { ref } from "vue"
import { authAPI } from '@/services/api'
const username = ref<string>("")
const password = ref<string>("")
const Toast = useToast()
async function register() {
try {
const responseRegister = await authAPI.register(username.value, password.value)
if(responseRegister.status != 200)
throw new Error("Registred failed")
const responseLogin = await authAPI.login(username.value, password.value)
const { access_token, refresh_token } = responseLogin.data
localStorage.setItem('access_token', access_token)
localStorage.setItem('refresh_token', refresh_token)
router.push({ path: '/experiment' })
} catch (err){
console.log(err)
Toast.open({message: "Registred failed", type: "is-danger"})
}
}
</script>
<template>
<div class="container container-size">
<b-field label="Username">
<!-- @vue-ignore -->
<b-input
v-model="username"
placeholder="Alice"
maxlength="20"
required
></b-input>
</b-field>
<b-field label="Password">
<!-- @vue-ignore -->
<b-input
v-model="password"
maxlength="50"
type="password"
required
></b-input>
</b-field>
<b-button type="is-primary" @click="register">Register</b-button>
</div>
</template>
<style scoped>
.container-size{
flex: none;
gap:0;
width: 20rem;
height: 20rem ;
}
</style>

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import { BButton } from "buefy"
</script>
<template>
<section>
<b-button type="is-primary" size="is-medium" rounded>
<RouterLink to="/experiment"><span>Lancer l'expérience</span></RouterLink>
</b-button>
</section>
</template>
<style scoped>
section{
display: flex;
height: 80vh;
justify-content: center;
align-items: center;
}
span{
color: black;
}
</style>