Compare commits

...

6 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
47 changed files with 775 additions and 256 deletions

View File

@@ -1,4 +1,6 @@
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
@@ -8,3 +10,5 @@ PyJWT>=2.11.0
# python -m spacy download fr_core_news_sm # python -m spacy download fr_core_news_sm
argon2-cffi>=25.1.0 argon2-cffi>=25.1.0
pydantic-settings==2.13.1 pydantic-settings==2.13.1
gunicorn==25.1.0
mariadb==1.1.14

View File

@@ -3,8 +3,10 @@ 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 .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) router.include_router(auth_router)

View File

@@ -44,7 +44,7 @@ def generate_questions(id: int, current_user: Annotated[str, Depends(get_current
) )
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

View File

@@ -8,8 +8,8 @@ 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_data: MetricCreate, current_user: Annotated[str, Depends(get_current_user)]): # def create(metric_data: MetricCreate, current_user: Annotated[str, Depends(get_current_user)]):
metric: Metric = Metric(question_id = metric_data.question_id, need_index = metric_data.need_index, user = current_user) # metric: Metric = Metric(question_id = metric_data.question_id, need_index = metric_data.need_index, user = current_user)
created_metric: Metric = create_metric(metric) # created_metric: Metric = create_metric(metric)
return created_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

View File

@@ -39,7 +39,7 @@ def verify_token(token: str, token_type: str = "access") -> Optional[dict]:
def verify_beyond_user_limit() -> bool: def verify_beyond_user_limit() -> bool:
users: Sequence[User] = get_users() users: Sequence[User] = get_users()
if (len(users) > settings.USER_LIMIT): if (len(users) >= settings.USER_LIMIT):
return True return True
else: else:
return False return False

View File

@@ -1,13 +1,21 @@
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): def get_question_by_id(question_id: int, user: User):
with Session(engine) as session: with Session(engine) as session:
question = session.get(Question, question_id) statement = select(Question).where(Question.id == question_id, Question.user_id == user.id)
results = session.exec(statement)
question = results.first()
return question 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): def get_questions(knowledge):
with Session(engine) as session: with Session(engine) as session:
statement = select(Question).where(Question.knowledge_id == knowledge.id) statement = select(Question).where(Question.knowledge_id == knowledge.id)

View File

@@ -15,5 +15,4 @@ class Metric(SQLModel, table=True):
user: User | None = Relationship(back_populates="metrics") user: User | None = Relationship(back_populates="metrics")
class MetricCreate(BaseModel): class MetricCreate(BaseModel):
question_id: int
need_index: int need_index: int

View File

@@ -14,7 +14,7 @@ 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

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>

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,11 +1,10 @@
<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 api from "@/services/apiAxios" import type { AxiosResponse } from "axios"
import type { AxiosResponse } from "axios"; import { ref } from "vue"
import { ref } from "vue";
import type { Knowledge, KnowledgeCreate } 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'
@@ -45,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
@@ -60,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>
@@ -68,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,35 +1,44 @@
<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, Question, MetricCreate } from "@/types/types" import type { Knowledge, Question, MetricCreate, Metric } from "@/types/types"
import { BCollapse, BSlider } from "buefy";
//import { useStepStore } from '@/stores/step' import { useStepStore } from '@/stores/step'
import { useItemStore } from '@/stores/item' import { useItemStore } from '@/stores/item'
import api from "@/services/apiAxios" import api from "@/services/api"
import type { AxiosResponse } from "axios"; 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<MetricCreate[]>([]) 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[]>{
@@ -40,7 +49,7 @@
function initializeMetrics(questions: Question[]){ function initializeMetrics(questions: Question[]){
questions.forEach((q)=>{ questions.forEach((q)=>{
const metric: MetricCreate = { const metric: Metric = {
question_id: q.id!, question_id: q.id!,
need_index: -1 need_index: -1
} }
@@ -58,20 +67,68 @@
} }
async function postMetrics(){ async function postMetrics(){
console.log( metrics.value)
metrics.value?.forEach(async (metric) => { metrics.value?.forEach(async (metric) => {
const response = await api.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 // eslint-disable-next-line @typescript-eslint/no-unused-vars
const metric_data = response.data 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>
@@ -80,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>
@@ -88,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 api from "@/services/apiAxios"; 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()
@@ -18,21 +20,28 @@
async function generateQuestions (knowledge: Knowledge) { async function generateQuestions (knowledge: Knowledge) {
await api.post(`api/v1/knowledges/${knowledge.id}/questions`) 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,5 +1,5 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import { isAuthenticated } from '@/services/apiAxios' import { isAuthenticated } from '@/services/api'
const pagesWithoutGuard = ['login', 'app', 'register'] const pagesWithoutGuard = ['login', 'app', 'register']
@@ -33,9 +33,10 @@ const router = createRouter({
], ],
}) })
// Guard system
router.beforeEach(async (to, from) => { router.beforeEach(async (to, from) => {
const isAuth = await isAuthenticated() const isAuth = await isAuthenticated()
if (!isAuth && pagesWithoutGuard.includes(to.name!.toString())) { if (!isAuth && !pagesWithoutGuard.includes(to.name!.toString())) {
return { name: 'login' } return { name: 'login' }
} }
}) })

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

@@ -1,45 +0,0 @@
import axios from "axios"
import type { AxiosResponse } from "axios";
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL
});
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')
}
export const isAuthenticated = async () => {
try {
const response: AxiosResponse = await authAPI.getMe()
if (response.status==200)
return true
else
return false
}
catch{
return false
}
}
export default api;

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

@@ -18,9 +18,15 @@ interface Question {
user: User user: User
} }
interface MetricCreate { interface Metric {
//id
question_id: number, question_id: number,
need_index: number need_index: number
//user
}
interface MetricCreate {
need_index: number
} }
interface User { interface User {
@@ -28,4 +34,4 @@ interface User {
token: string token: string
} }
export type {KnowledgeCreate, Knowledge, Question, MetricCreate} export type {KnowledgeCreate, Knowledge, Question, MetricCreate, Metric}

View File

@@ -21,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

@@ -2,7 +2,7 @@
import router from '@/router/index' import router from '@/router/index'
import { BField, BInput, BButton, useToast } from "buefy"; import { BField, BInput, BButton, useToast } from "buefy";
import { ref } from "vue" import { ref } from "vue"
import { authAPI } from '@/services/apiAxios' import { authAPI } from '@/services/api'
const username = ref<string>("") const username = ref<string>("")
const password = ref<string>("") const password = ref<string>("")
@@ -27,7 +27,7 @@
</script> </script>
<template> <template>
<div class="container"> <div class="container container-size">
<b-field label="Username"> <b-field label="Username">
<!-- @vue-ignore --> <!-- @vue-ignore -->
<b-input <b-input
@@ -51,10 +51,10 @@
</template> </template>
<style scoped> <style scoped>
.container{ .container-size{
width: 20%; flex: none;
background-color: #ffffff; gap:0;
border-radius: 16px; width: 20rem;
padding: 34px; height: 20rem ;
} }
</style> </style>

View File

@@ -2,7 +2,7 @@
import router from '@/router/index' import router from '@/router/index'
import { BField, BInput, BButton, useToast } from "buefy"; import { BField, BInput, BButton, useToast } from "buefy";
import { ref } from "vue" import { ref } from "vue"
import { authAPI } from '@/services/apiAxios' import { authAPI } from '@/services/api'
const username = ref<string>("") const username = ref<string>("")
const password = ref<string>("") const password = ref<string>("")
@@ -30,7 +30,7 @@
</script> </script>
<template> <template>
<div class="container"> <div class="container container-size">
<b-field label="Username"> <b-field label="Username">
<!-- @vue-ignore --> <!-- @vue-ignore -->
<b-input <b-input
@@ -54,10 +54,10 @@
</template> </template>
<style scoped> <style scoped>
.container{ .container-size{
width: 20%; flex: none;
background-color: #ffffff; gap:0;
border-radius: 16px; width: 20rem;
padding: 34px; height: 20rem ;
} }
</style> </style>