Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/sort-and-filter-query #100

Merged
merged 26 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
86ae947
fix: create items with repository
nRamstedt Jun 4, 2024
fbb497a
add: query component, mongo adapter & update tests
nRamstedt Jun 7, 2024
3de745d
fix: import query adapter for mongo
nRamstedt Jun 7, 2024
264f7f8
fix: import
nRamstedt Jun 7, 2024
ae8af71
fix: include tags when submitting question
nRamstedt Jun 10, 2024
0ad380f
add: common filter params for questions
nRamstedt Jun 10, 2024
17d6839
add: argument for format
nRamstedt Jun 10, 2024
5b48317
add(Table): sortable columns
nRamstedt Jun 10, 2024
3d7e8d2
qaf: implement filter params & setup sortable columns, refactor Quest…
nRamstedt Jun 10, 2024
bca2d6b
fix(permission_required): forward args/kwargs
nRamstedt Jun 11, 2024
154bc66
add(icons): list of icon references
nRamstedt Jun 11, 2024
9fae905
feat(Link): try match active with current path if it's a string
nRamstedt Jun 11, 2024
d6a8385
fix: set correct review_status
nRamstedt Jun 11, 2024
9d59b05
refactor(conversation): Favor schema definition over model def
nRamstedt Jun 11, 2024
ab6197d
refactor(qaf): Split up views, routes etc and implement menu
nRamstedt Jun 11, 2024
2ab8ed9
fix(MenuSlot): disable menu item
nRamstedt Jun 13, 2024
ba3cd30
add: list, message icons
nRamstedt Jun 13, 2024
33d2453
add disabled prop
nRamstedt Jun 13, 2024
2d8e583
refactor & clean up qaf menus & routes
nRamstedt Jun 13, 2024
d06fcb0
fix: import tests
nRamstedt Jun 13, 2024
6d98a2b
fix: imports..
nRamstedt Jun 13, 2024
0377ab6
refactor(qaf): rename dependecies (loader/actions) & captialize views
nRamstedt Jun 17, 2024
003b0fd
fix(Menu): support setting disable on menu item
nRamstedt Jun 17, 2024
072c129
fix(Heading): forward restProps
nRamstedt Jun 17, 2024
0c91a95
change(qaf): remove status column
nRamstedt Jun 17, 2024
112a728
Merge branch 'master' into feat/sort-and-filter-query
nRamstedt Jun 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 6 additions & 30 deletions fai-rag-app/fai-backend/fai_backend/conversations/schema.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,18 @@
from pydantic import BaseModel, Field

from fai_backend.conversations.models import Conversation
from fai_backend.schema import Timestamp
from fai_backend.conversations.models import Conversation, Feedback, Message


class FeedbackResponse(BaseModel):
user: str
rating: str
comment: str = Field(default=None)
timestamp: Timestamp = Timestamp()
metadata: dict = Field(default_factory=dict)

class FeedbackResponse(Feedback):
pass

class ResponseMessageUserPermission(BaseModel):
can_feedback: bool = False


class ResponseMessage(BaseModel):
user: str
content: str
type: str = 'message'
feedback: list[FeedbackResponse] = Field(default=[])
timestamp: Timestamp = Timestamp()
user_permissions: ResponseMessageUserPermission = ResponseMessageUserPermission()
metadata: dict = Field(default_factory=dict)


class ConversationUserPermissionsResponse(BaseModel):
can_read: bool = False
can_message: bool = False
can_feedback: bool = False
class ResponseMessage(Message):
pass


class ConversationResponse(Conversation):
user_permissions: ConversationUserPermissionsResponse = (
ConversationUserPermissionsResponse()
)
metadata: dict = Field(default_factory=dict)
messages: list[ResponseMessage]


class CreateFeedbackRequest(BaseModel):
Expand Down
28 changes: 22 additions & 6 deletions fai-rag-app/fai-backend/fai_backend/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
)
from fai_backend.auth.schema import TokenPayload
from fai_backend.auth.service import AuthService
from fai_backend.documents.menu import menu_items as document_menu_items
from fai_backend.qaf.menu import qa_menu, qa_menu_loader
from fai_backend.qaf.service import QAFService
from fai_backend.schema import ProjectUser, User
from fai_backend.views import mock_menu, page_template, questions_menu, reviewer_menu
from fai_backend.views import chat_menu, mock_menu, page_template


async def try_get_authenticated_user(
Expand Down Expand Up @@ -51,16 +54,29 @@ async def get_project_user(
return user


async def get_page_template_for_logged_in_users(project_user: ProjectUser = Depends(get_project_user)) \
-> Callable[[list[Any] | Any, str | None], list[Any]]:
permissions = list(map(lambda x: x[0], filter(lambda x: x[1], project_user.permissions.items())))
def get_project_user_permissions(
project_user: ProjectUser = Depends(get_project_user),
) -> list[str]:
return list(map(lambda x: x[0], filter(lambda x: x[1], project_user.permissions.items())))


async def get_page_template_for_logged_in_users(
permissions: list[str] = Depends(get_project_user_permissions),
project_user: ProjectUser = Depends(get_project_user),
qaf_service: QAFService = Depends(QAFService.factory),
) -> Callable[[list[Any] | Any, str | None], list[Any]]:
menu_args = {
'qa_menu': {
'items': await qa_menu_loader(project_user, qaf_service),
}
}
return (lambda components, page_title: page_template(
*components if isinstance(components, list) else [components],
page_title=page_title,
menus=[
*questions_menu(user_permissions=permissions),
*reviewer_menu(user_permissions=permissions),
*chat_menu(user_permissions=permissions),
*(qa_menu(user_permissions=permissions, **menu_args['qa_menu'])),
*document_menu_items(),
*mock_menu(user_permissions=permissions),
]
))
6 changes: 4 additions & 2 deletions fai-rag-app/fai-backend/fai_backend/documents/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ def menu_items() -> list:
components=[
c.Link(
text=_('show_all', 'Show all'),
url='/documents'
url='/documents',
active='/documents'
),
c.Link(
text=_('upload_new', 'Upload new'),
url='/documents/upload'
url='/documents/upload',
active='/documents/upload'
),
],
icon_src='data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWZpbGUtdGV4dCI+PHBhdGggZD0iTTE1IDJINmEyIDIgMCAwIDAtMiAydjE2YTIgMiAwIDAgMCAyIDJoMTJhMiAyIDAgMCAwIDItMlY3WiIvPjxwYXRoIGQ9Ik0xNCAydjRhMiAyIDAgMCAwIDIgMmg0Ii8+PHBhdGggZD0iTTEwIDlIOCIvPjxwYXRoIGQ9Ik0xNiAxM0g4Ii8+PHBhdGggZD0iTTE2IDE3SDgiLz48L3N2Zz4='
Expand Down
3 changes: 2 additions & 1 deletion fai-rag-app/fai-backend/fai_backend/framework/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ class Link(Text):
element: None = Field(None, exclude=True)
state: Literal['primary', 'secondary', 'accent', 'info', 'warning', 'error', 'success'] | None = None
underline: Literal['on-hover', 'always', 'never'] | bool | None = None
active: bool = False
active: bool | str = False
disabled: bool | None = None


class AppContent(UIComponent):
Expand Down
10 changes: 10 additions & 0 deletions fai-rag-app/fai-backend/fai_backend/icons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
icons = {
'users': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXVzZXJzIj48cGF0aCBkPSJNMTYgMjF2LTJhNCA0IDAgMCAwLTQtNEg2YTQgNCAwIDAgMC00IDR2MiIvPjxjaXJjbGUgY3g9IjkiIGN5PSI3IiByPSI0Ii8+PHBhdGggZD0iTTIyIDIxdi0yYTQgNCAwIDAgMC0zLTMuODciLz48cGF0aCBkPSJNMTYgMy4xM2E0IDQgMCAwIDEgMCA3Ljc1Ii8+PC9zdmc+',
'square_pen': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXNxdWFyZS1wZW4iPjxwYXRoIGQ9Ik0xMiAzSDVhMiAyIDAgMCAwLTIgMnYxNGEyIDIgMCAwIDAgMiAyaDE0YTIgMiAwIDAgMCAyLTJ2LTciLz48cGF0aCBkPSJNMTguMzc1IDIuNjI1YTEgMSAwIDAgMSAzIDNsLTkuMDEzIDkuMDE0YTIgMiAwIDAgMS0uODUzLjUwNWwtMi44NzMuODRhLjUuNSAwIDAgMS0uNjItLjYybC44NC0yLjg3M2EyIDIgMCAwIDEgLjUwNi0uODUyeiIvPjwvc3ZnPg==',
'bot': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWJvdCI+PHBhdGggZD0iTTEyIDhWNEg4Ii8+PHJlY3Qgd2lkdGg9IjE2IiBoZWlnaHQ9IjEyIiB4PSI0IiB5PSI4IiByeD0iMiIvPjxwYXRoIGQ9Ik0yIDE0aDIiLz48cGF0aCBkPSJNMjAgMTRoMiIvPjxwYXRoIGQ9Ik0xNSAxM3YyIi8+PHBhdGggZD0iTTkgMTN2MiIvPjwvc3ZnPg==',
'bot_message_square': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWJvdC1tZXNzYWdlLXNxdWFyZSI+PHBhdGggZD0iTTEyIDZWMkg4Ii8+PHBhdGggZD0ibTggMTgtNCA0VjhhMiAyIDAgMCAxIDItMmgxMmEyIDIgMCAwIDEgMiAydjhhMiAyIDAgMCAxLTIgMloiLz48cGF0aCBkPSJNMiAxMmgyIi8+PHBhdGggZD0iTTkgMTF2MiIvPjxwYXRoIGQ9Ik0xNSAxMXYyIi8+PHBhdGggZD0iTTIwIDEyaDIiLz48L3N2Zz4=',
'list_todo': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWxpc3QtdG9kbyI+PHJlY3QgeD0iMyIgeT0iNSIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cGF0aCBkPSJtMyAxNyAyIDIgNC00Ii8+PHBhdGggZD0iTTEzIDZoOCIvPjxwYXRoIGQ9Ik0xMyAxMmg4Ii8+PHBhdGggZD0iTTEzIDE4aDgiLz48L3N2Zz4=',
'messages_square': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLW1lc3NhZ2VzLXNxdWFyZSI+PHBhdGggZD0iTTE0IDlhMiAyIDAgMCAxLTIgMkg2bC00IDRWNGMwLTEuMS45LTIgMi0yaDhhMiAyIDAgMCAxIDIgMnoiLz48cGF0aCBkPSJNMTggOWgyYTIgMiAwIDAgMSAyIDJ2MTFsLTQtNGgtNmEyIDIgMCAwIDEtMi0ydi0xIi8+PC9zdmc+',
'message_square_more': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLW1lc3NhZ2Utc3F1YXJlLW1vcmUiPjxwYXRoIGQ9Ik0yMSAxNWEyIDIgMCAwIDEtMiAySDdsLTQgNFY1YTIgMiAwIDAgMSAyLTJoMTRhMiAyIDAgMCAxIDIgMnoiLz48cGF0aCBkPSJNOCAxMGguMDEiLz48cGF0aCBkPSJNMTIgMTBoLjAxIi8+PHBhdGggZD0iTTE2IDEwaC4wMSIvPjwvc3ZnPg==',
'plus': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXBsdXMiPjxwYXRoIGQ9Ik01IDEyaDE0Ii8+PHBhdGggZD0iTTEyIDV2MTQiLz48L3N2Zz4=',
}
10 changes: 6 additions & 4 deletions fai-rag-app/fai-backend/fai_backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ async def lifespan(_app: FastAPI):

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_origins=['*'],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
allow_methods=['*'],
allow_headers=['*'],
)

frontend = get_frontend_environment(settings.ENV_MODE)
Expand All @@ -64,7 +64,7 @@ async def event_source_llm_generator(question: str, llm: ILLMProtocol):
serializer = Base64Serializer()
stream = await llm.create()

print(f"{llm=}")
print(f'{llm=}')

async def generator():
async for output in stream(question):
Expand All @@ -87,6 +87,8 @@ async def generator():
return EventSourceResponse(generator())




@app.get('/api/assistant-stream/{project}/{assistant}')
async def assistant_stream(
project: str,
Expand Down
112 changes: 58 additions & 54 deletions fai-rag-app/fai-backend/fai_backend/qaf/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,83 +10,65 @@
GenerateAnswerPayload,
QuestionDetails,
QuestionEntry,
QuestionFilterParams,
SubmitAnswerPayload,
SubmitQuestionPayload,
)
from fai_backend.qaf.service import QAFService
from fai_backend.schema import ProjectUser


async def submit_question_request(
body: SubmitQuestionPayload,
service: QAFService = Depends(QAFService.factory),
user: ProjectUser = Depends(get_project_user),
) -> QuestionDetails:
question = await service.submit_question(
user,
body.question,
body.model_dump(exclude={'question'}),
)

return question


async def submit_question_and_generate_answer_request(
question: QuestionDetails = Depends(submit_question_request),
service: QAFService = Depends(QAFService.factory),
file_service: FileUploadService = Depends(get_file_upload_service),
user: ProjectUser = Depends(get_project_user),
) -> QuestionDetails:
latest_upload_path = file_service.get_latest_upload_path(user.project_id)
if not latest_upload_path:
raise Exception('No upload path found')

directory_name = latest_upload_path.split('/')[-1]

response = await ask_llm_raq_question(question=question.question.content, collection_name=directory_name)
await service.add_message(
user,
GenerateAnswerPayload(question_id=question.id, answer=response)
async def questions_filter_params(
q: str = None,
tags: list[str] = None,
status: str = None,
review_status: str = None,
sort: str = None,
sort_order: str = None,
) -> QuestionFilterParams:
return QuestionFilterParams(
q=q,
tags=tags,
status=status,
review_status=review_status,
sort=sort or 'timestamp.modified',
sort_order=sort_order or 'desc',
)

return question


async def list_my_questions_request(
async def questions_loader(
service: QAFService = Depends(QAFService.factory),
user: ProjectUser = Depends(get_project_user),
query_params: QuestionFilterParams = Depends(questions_filter_params),
) -> list[QuestionEntry]:
try:
return await service.my_questions(user)
except Exception as e:
console.log(e)
return []
return await service.list_submitted_questions(user, query_params)


async def my_question_details_request(
async def question_details_loader(
conversation_id: str,
user: ProjectUser = Depends(get_project_user),
service: QAFService = Depends(QAFService.factory),
) -> QuestionDetails | None:
return await service.my_question_details(user, conversation_id)


async def submitted_questions_request(
service: QAFService = Depends(QAFService.factory),
user: ProjectUser = Depends(get_project_user),
) -> list[QuestionEntry]:
return await service.submitted_questions(user)
) -> QuestionEntry | None:
return await service.submitted_question_details(user, conversation_id)


async def submitted_question_details_request(
conversation_id: str,
async def question_create_action(
body: SubmitQuestionPayload,
service: QAFService = Depends(QAFService.factory),
user: ProjectUser = Depends(get_project_user),
) -> QuestionEntry | None:
return await service.submitted_question_details(user, conversation_id)
) -> QuestionDetails:
question = await service.submit_question(
user,
body.question,
body.model_dump(exclude={'question', 'tags'}),
body.tags,
)

return question


async def submit_feedback_request(
async def add_feedback_action(
conversation_id: str,
body: FeedbackPayload,
service: QAFService = Depends(QAFService.factory),
user: ProjectUser = Depends(get_project_user),
Expand All @@ -97,7 +79,8 @@ async def submit_feedback_request(
)


async def submit_answer_request(
async def add_answer_action(
conversation_id: str,
body: SubmitAnswerPayload,
service: QAFService = Depends(QAFService.factory),
user: ProjectUser = Depends(get_project_user),
Expand All @@ -111,3 +94,24 @@ async def submit_answer_request(
except Exception as e:
console.log(e)
return None


async def run_llm_on_question_create_action(
question: QuestionDetails = Depends(question_create_action),
service: QAFService = Depends(QAFService.factory),
file_service: FileUploadService = Depends(get_file_upload_service),
user: ProjectUser = Depends(get_project_user),
) -> QuestionDetails:
latest_upload_path = file_service.get_latest_upload_path(user.project_id)
if not latest_upload_path:
raise Exception('No upload path found')

directory_name = latest_upload_path.split('/')[-1]

response = await ask_llm_raq_question(question=question.question.content, collection_name=directory_name)
await service.add_message(
user,
GenerateAnswerPayload(question_id=question.id, answer=response)
)

return question
72 changes: 72 additions & 0 deletions fai-rag-app/fai-backend/fai_backend/qaf/menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from fai_backend.framework import components as c
from fai_backend.icons import icons
from fai_backend.phrase import phrase as _
from fai_backend.qaf.schema import QuestionFilterParams
from fai_backend.qaf.service import QAFService
from fai_backend.schema import ProjectUser
from fai_backend.views import permission_required


async def qa_menu_loader(
project_user: ProjectUser,
qaf_service: QAFService,
) -> list[tuple[str, str, str]]:
status_tabs = ['open', 'in-progress', 'rejected', 'approved']

async def get_count_by_review_status(status: str):
return len(await qaf_service.list_submitted_questions(
project_user=project_user,
query_params=QuestionFilterParams(review_status=status),
))

return [
*[
(
f'{status.capitalize()}',
f'/questions?review_status={status}',
await get_count_by_review_status(status) or '0',

)
for status in status_tabs
],
]


@permission_required(['can_review_answers'])
def qa_menu(
items: list[tuple[str, str, str]] = None,
) -> list:
return [
c.Menu(
title=_('questions', 'Questions'),
id='questions-menu',
variant='vertical',
sub_menu=True,
icon_src=icons['messages_square'],
class_name='qa-menu',
components=[
c.Link(
text=_('show_all', 'Visa alla'),
url='/questions',
active='/questions*'
),
c.Menu(
title=_('status', 'Status'),
components=[
*[c.Link(
text=text,
url=url,
active=url,
badge=badge,
) for text, url, badge in (items or [])]
]
),
c.Link(
text=_('Add new question'),
url='/questions/create',
active='/questions/create',
icon_src=icons['plus'],
),
],
),
]
Loading
Loading