Compare commits

..

22 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
Robin COuret
339270aefd add question evaluation 2026-02-25 11:45:04 +01:00
Robin COuret
b97a18eab4 add step system 2026-02-24 14:37:33 +01:00
Robin COuret
375c6969cc add type, validation & toast 2026-02-24 12:01:19 +01:00
Robin COuret
cf99d5d388 add interaction api- ui 2026-02-23 17:03:39 +01:00
Robin COuret
978f726c10 add question api 2026-02-23 14:26:02 +01:00
Robin COuret
1b0e889f07 modify question generation 2026-02-19 18:01:53 +01:00
Robin COuret
1b00ab25f7 add question generation 2026-02-19 18:00:42 +01:00
Robin COuret
e0e50af706 init component 2026-02-16 17:28:37 +01:00
Robin COuret
460c7a25e0 vue init 2026-02-13 09:27:43 +01:00
119 changed files with 16314 additions and 148 deletions

View File

@@ -1 +0,0 @@
DATABASE_URI="sqlite:///database.db"

View File

@@ -3,6 +3,7 @@ FastAPI server to run Manolia
# Run # Run
``` ```
cd src/app
fastapi dev main.py fastapi dev main.py
``` ```

Binary file not shown.

View File

@@ -1,3 +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
spacy==3.8.11
PyJWT>=2.11.0
# python -m spacy download en_core_web_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(metric_router) router.include_router(question_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,51 +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.crud.crud_knowledges import create_knowledge, read_knowledges, read_knowledge, update_knowledge, delete_knowledge
from src.app.crud.crud_questions import read_questions as read_questions_crud from src.app.models.knowledge import Knowledge, KnowledgeCreate
from src.app.models.question import Question
from src.app.data.knowledge import create_knowledge, get_knowledges_by_user, get_knowledge_by_id
#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
#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
#TODO: find pattern
@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)]):
#SLM Generation knowledge: Knowledge = get_knowledge_by_id(id, current_user)
#create_question() if not knowledge:
return True 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)
for q in questions_raw:
question = Question(question = q, knowledge=knowledge, user=current_user)
create_question(question)
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,
return questions detail="Forbidden. The requested knowledge is not available for the provided ID.",
headers={"WWW-Authenticate": "Bearer"},
)
questions = get_questions(knowledge)
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,12 +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.models.question import Question from src.app.data.metric import create_metric
from src.app.crud.crud_metrics 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(question: Question): # def create(metric_data: MetricCreate, current_user: Annotated[str, Depends(get_current_user)]):
created_metric: Metric = create_metric(question) # 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,24 +4,24 @@ 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)
session.commit() session.commit()
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:

View File

@@ -1,40 +1,35 @@
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.knowledge import Knowledge 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)
session.commit() session.commit()
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 @@
from dotenv import load_dotenv from src.app.config import settings
import os
#import secrets
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,15 +1,21 @@
from src.app.config import settings
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from src.app.database import create_db_and_tables from src.app.database import create_db_and_tables
#TODO : best practice to manage models import
# Import models in app
# 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
#TODO : alternative @app.on_event("startup") ? #TODO : alternative @app.on_event("startup") ?
@@ -21,4 +27,15 @@ async def lifespan(app: FastAPI):
#shutdown #shutdown
app = FastAPI(lifespan=lifespan) app = FastAPI(lifespan=lifespan)
app.include_router(router) app.include_router(router)
origin = settings.ORIGIN
origins = [origin]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

View File

Binary file not shown.

View File

@@ -1,9 +1,17 @@
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)
content: str = Field(index=True) content: str = Field(index=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

@@ -0,0 +1,43 @@
import spacy
from src.app.config import settings
from openai import OpenAI
from src.app.models.knowledge import Knowledge
client = OpenAI(
base_url=settings.LANGUAGE_MODEL_API,
api_key = "sk-no-key-required"
)
nlp = spacy.load("fr_core_news_sm")
def questions_generation(knowledge: Knowledge):
context = "Texte : ```" + knowledge.content + "```"
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
#SLM processing
response = client.responses.create(
model=settings.LANGUAGE_MODEL_NAME,
input=[
{"role": "system", "content": "Question Generation"},
{"role": "user", "content": prompt}],
)
text_response = response.output[0].content[0].text
#Sentence segmentation
doc = nlp(text_response)
sents = list()
for sentence in doc.sents:
sents.append(sentence.text)
#Interrogation sentence detection
questions = list()
for sent in sents:
index_mark = sent.rfind("?")
if(index_mark > 0):
questions.append(sent[0:index_mark+1])
return questions

View File

@@ -0,0 +1,8 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100

3
user-interface/.env Normal file
View File

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

View File

@@ -0,0 +1 @@
VITE_API_URL=http://127.0.0.1:8000/

1
user-interface/.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto eol=lf

39
user-interface/.gitignore vendored Normal file
View File

@@ -0,0 +1,39 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
.eslintcache
# Cypress
/cypress/videos/
/cypress/screenshots/
# Vitest
__screenshots__/
# Vite
*.timestamp-*-*.mjs

View File

@@ -0,0 +1,10 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["eslint", "typescript", "unicorn", "oxc", "vue"],
"env": {
"browser": true
},
"categories": {
"correctness": "error"
}
}

View File

@@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}

10
user-interface/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"recommendations": [
"Vue.volar",
"vitest.explorer",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"oxc.oxc-vscode",
"esbenp.prettier-vscode"
]
}

70
user-interface/README.md Normal file
View File

@@ -0,0 +1,70 @@
# user-interface
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Recommended Browser Setup
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```
### Run Unit Tests with [Vitest](https://vitest.dev/)
```sh
npm run test:unit
```
### Run End-to-End Tests with [Cypress](https://www.cypress.io/)
```sh
npm run test:e2e:dev
```
This runs the end-to-end tests against the Vite development server.
It is much faster than the production build.
But it's still recommended to test the production build with `test:e2e` before deploying (e.g. in CI environments):
```sh
npm run build
npm run test:e2e
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

View File

@@ -0,0 +1,8 @@
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}',
baseUrl: 'http://localhost:4173',
},
})

View File

@@ -0,0 +1,8 @@
// https://on.cypress.io/api
describe('My First Test', () => {
it('visits the app root url', () => {
cy.visit('/')
cy.contains('h1', 'You did it!')
})
})

View File

@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@@ -0,0 +1,39 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }
export {}

View File

@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@@ -0,0 +1,9 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["./e2e/**/*", "./support/**/*"],
"exclude": ["./support/component.*"],
"compilerOptions": {
"isolatedModules": false,
"types": ["cypress"]
}
}

1
user-interface/env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,41 @@
import { globalIgnores } from 'eslint/config'
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
import pluginVue from 'eslint-plugin-vue'
import pluginCypress from 'eslint-plugin-cypress'
import pluginVitest from '@vitest/eslint-plugin'
import pluginOxlint from 'eslint-plugin-oxlint'
import skipFormatting from 'eslint-config-prettier/flat'
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
// import { configureVueProject } from '@vue/eslint-config-typescript'
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
export default defineConfigWithVueTs(
{
name: 'app/files-to-lint',
files: ['**/*.{vue,ts,mts,tsx}'],
},
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
...pluginVue.configs['flat/essential'],
vueTsConfigs.recommended,
{
...pluginCypress.configs.recommended,
files: [
'cypress/e2e/**/*.{cy,spec}.{js,ts,jsx,tsx}',
'cypress/support/**/*.{js,ts,jsx,tsx}',
],
},
{
...pluginVitest.configs.recommended,
files: ['src/**/__tests__/*'],
},
...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'),
skipFormatting,
)

13
user-interface/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manolia</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

9017
user-interface/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
{
"name": "user-interface",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"test:unit": "vitest",
"prepare": "cypress install",
"test:e2e": "start-server-and-test preview http://localhost:4173 'cypress run --e2e'",
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'",
"build-only": "vite build",
"type-check": "vue-tsc --build",
"lint": "run-s lint:*",
"lint:oxlint": "oxlint . --fix",
"lint:eslint": "eslint . --fix --cache",
"format": "prettier --write --experimental-cli src/"
},
"dependencies": {
"axios": "^1.13.6",
"buefy": "^3.0.4",
"pinia": "^3.0.4",
"vue": "^3.5.27",
"vue-router": "^5.0.1"
},
"devDependencies": {
"@tsconfig/node24": "^24.0.4",
"@types/jsdom": "^27.0.0",
"@types/node": "^24.10.9",
"@vitejs/plugin-vue": "^6.0.3",
"@vitest/eslint-plugin": "^1.6.6",
"@vue/eslint-config-typescript": "^14.2.0",
"@vue/test-utils": "^2.4.0",
"@vue/tsconfig": "^0.8.1",
"cypress": "^15.9.0",
"eslint": "^10.0.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-cypress": "^5.2.1",
"eslint-plugin-oxlint": "~1.42.0",
"eslint-plugin-vue": "^10.2.0",
"jiti": "^2.6.1",
"jsdom": "^27.4.0",
"npm-run-all2": "^8.0.4",
"oxlint": "~1.42.0",
"prettier": "3.8.1",
"start-server-and-test": "^2.1.3",
"typescript": "~5.9.3",
"vite": "^7.3.1",
"vite-plugin-vue-devtools": "^8.0.5",
"vitest": "^4.0.18",
"vue-tsc": "^3.2.4"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import { RouterView } from 'vue-router'
import AppTopbar from '@/components/AppTopbar.vue'
</script>
<template>
<header>
<AppTopbar />
</header>
<main>
<RouterView />
</main>
<!-- <footer>
<AppFooter />
</footer> -->
</template>
<style>
#app {
padding-inline: 5%;
padding-top: 8px;
padding-bottom: 32px;
min-height: 100vh;
display: flex;
flex-direction: column;
}
main{
flex: 1;
display: flex;
flex-direction: column;
}
</style>

View File

@@ -0,0 +1,11 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import App from '../App.vue'
describe('App', () => {
it('mounts renders properly', () => {
const wrapper = mount(App)
expect(wrapper.text()).toContain('You did it!')
})
})

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

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 371 KiB

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

@@ -0,0 +1,10 @@
// variables.sass
@import '~buetify/src/sass/variables'
@import '~bulma/sass/utilities/initial-variables'
// provide any variable overrides here
@import '~bulma/sass/utilities/functions'
@import '~bulma/sass/utilities/derived-variables'
@import '~bulma/sass/utilities/controls'
@import '~bulma/sass/utilities/mixins'

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
...
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import { BNavbar, BNavbarItem } from "buefy";
</script>
<template>
<b-navbar class="navbar" shadow >
<template #brand>
<b-navbar-item tag="router-link" :to="{ path: '/' }" id="brand">
<img class="logo" src="../assets/svg/manolia-logo.svg" alt="Manolia logo" />
<h1 class="title">MANOLIA</h1>
</b-navbar-item>
</template>
</b-navbar>
</template>
<style scoped>
.navbar{
border-radius: 16px;
margin-bottom: 24px;
height: 4rem;
}
#brand:hover, #brand:focus, #brand:active{
background-color: inherit;
}
.title{
font-size: 1.5rem;
font-family: "Exo-Black";
}
</style>

View File

@@ -0,0 +1,145 @@
<script setup lang="ts">
import { BField, BInput, BButton, useToast } from "buefy"
import api from "@/services/api"
import type { AxiosResponse } from "axios"
import { ref } from "vue"
import type { Knowledge, KnowledgeCreate } from "@/types/types"
import { useStepStore } from '@/stores/step'
import { useItemStore } from '@/stores/item'
const stepStore = useStepStore()
const itemStore = useItemStore()
const knowledgeModel = ref<string>("")
const uriModel = ref<string>("")
const Toast = useToast()
async function postKnowledge(){
const knowledge: KnowledgeCreate = {
content: knowledgeModel.value,
uri: uriModel.value
}
if(validation(knowledge)){
try {
const response: AxiosResponse = await api.post("api/v1/knowledges/", knowledge)
const knowledge_data: Knowledge = response.data
Toast.open({message: "Knowledge collected", type: "is-success"})
itemStore.$patch({ knowledge: knowledge_data })
stepStore.nextStep()
}
catch {
Toast.open({message: "Data entry error", type: "is-danger"})
}
}
}
function validation(knowledge: KnowledgeCreate){
return knowledge.content && knowledge.uri
}
</script>
<template>
<div class="container">
<div class="title-container">
<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 -->
<b-input
v-model="knowledgeModel"
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"
type="textarea"
required
></b-input>
</b-field>
<b-field label="URI">
<!-- @vue-ignore -->
<b-input
v-model="uriModel"
placeholder="fr.wikipedia.org/wiki/Connaissance"
maxlength="100"
required
></b-input>
</b-field>
<div class="btn-container">
<b-field>
<div class="control">
<b-button type="is-primary" @click="postKnowledge" >Partager</b-button>
</div>
</b-field>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.body-container{
display: flex;
flex-direction: column;
gap: 48px;
}
.btn-container{
height: 100%;
display: flex;
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>

View File

@@ -0,0 +1,201 @@
<script setup lang="ts">
import { onBeforeMount, ref, watch } from "vue";
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 api from "@/services/api"
import type { AxiosResponse } from "axios";
const stepStore = useStepStore()
const itemStore = useItemStore()
const questions = ref<Question[]>()
const metrics = ref<Metric[]>([])
onBeforeMount(async () => {
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)
})
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[]>{
const response: AxiosResponse = await api.get(`api/v1/knowledges/${knowledge.id}/questions`)
const questions: Question[] = response.data
return questions
}
function initializeMetrics(questions: Question[]){
questions.forEach((q)=>{
const metric: Metric = {
question_id: q.id!,
need_index: -1
}
metrics.value!.push(metric)
})
}
function getIndexMetrics(question: Question){
if(metrics.value != undefined){
return metrics.value.findIndex((metric) => metric.question_id === question.id)
}
else{
throw new Error("The is no metrics element")
}
}
async function postMetrics(){
console.log( metrics.value)
metrics.value?.forEach(async (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>
<template>
<div class="container">
<div class="title-container">
<div class="title-icon">
<img src="../assets/svg/message-circle-star.svg" alt="evaluate icon" />
</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-slider v-model="metrics![getIndexMetrics(question)]!.need_index"></b-slider>
</b-field>
</li>
</ul>
<div class="btn-container">
<b-field>
<div class="control">
<b-button type="is-primary" @click="postMetrics" >Partager</b-button>
</div>
</b-field>
</div>
</div>
</template>
<style scoped>
.btn-container{
display: flex;
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>

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