Added Application HolidayViewer

This commit is contained in:
Kodjo Sossouvi
2025-06-27 07:26:58 +02:00
parent 66ea45f501
commit 9f4b8ab4d0
87 changed files with 3756 additions and 212 deletions

View File

@@ -0,0 +1,51 @@
import json
import logging
from fasthtml.fastapp import fast_app
from components.admin.constants import Routes
from core.instance_manager import debug_session, InstanceManager
admin_app, rt = fast_app()
logger = logging.getLogger("admin")
@rt(Routes.AiBuddy)
def get(session, _id: str, boundaries: str):
logger.debug(f"Entering {Routes.AiBuddy} - GET with args {debug_session(session)}, {_id=}, {boundaries=}")
instance = InstanceManager.get(session, _id)
return instance.show_ai_buddy_form(json.loads(boundaries) if boundaries else None)
@rt(Routes.AiBuddy)
def post(session, _id: str, args: dict):
logger.debug(f"Entering {Routes.AiBuddy} - POST with args {debug_session(session)}, {_id=}, {args=}")
instance = InstanceManager.get(session, _id)
return instance.update_ai_buddy_settings(args)
@rt(Routes.AiBuddyCancel)
def post(session, _id: str):
logger.debug(f"Entering {Routes.AiBuddyCancel} with args {debug_session(session)}, {_id=}")
instance = InstanceManager.get(session, _id)
return instance.cancel_ai_buddy_settings()
@rt(Routes.ImportHolidays)
def get(session, _id: str, boundaries: str):
logger.debug(f"Entering {Routes.ImportHolidays} - GET with args {debug_session(session)}, {_id=}, {boundaries=}")
instance = InstanceManager.get(session, _id)
return instance.show_import_holidays_form(json.loads(boundaries) if boundaries else None)
@rt(Routes.PasteHolidays)
def post(session, _id: str, content: str):
logger.debug(f"Entering {Routes.PasteHolidays} with args {debug_session(session)}, {_id=}")
instance = InstanceManager.get(session, _id)
return instance.on_paste(content)
@rt(Routes.ImportHolidays)
def post(session, _id: str):
logger.debug(f"Entering {Routes.ImportHolidays} with args {debug_session(session)}, {_id=}")
instance = InstanceManager.get(session, _id)
return instance.import_holidays()

View File

View File

@@ -0,0 +1,39 @@
from dataclasses import dataclass, field
from ai.mcp_client import InProcessMCPClientCustomTools
from ai.mcp_tools import MCPServerTools
from auth.auth_manager import AuthManager
from components.admin.constants import ADMIN_SETTINGS_ENTRY
from config import OLLAMA_HOST
from core.settings_management import SettingsManager, NestedSettingsManager
from core.utils import split_host_port
@dataclass
class AiBuddySettingsEntry:
ollama_ip: str = ""
ollama_port: int = 11434
ollama_model: str = "mistral"
llm_mode: str = InProcessMCPClientCustomTools.ID
available_tools: list = field(default_factory=MCPServerTools.list_tools)
def __post_init__(self):
host, port = split_host_port(OLLAMA_HOST)
self.ollama_ip = host
self.ollama_port = port
@dataclass
class AdminSettings:
ai_buddy: AiBuddySettingsEntry = field(default_factory=AiBuddySettingsEntry)
class AdminDbManager:
def __init__(self, session: dict, settings_manager: SettingsManager):
self._session = session
self._settings_manager = settings_manager
self.ai_buddy = NestedSettingsManager(AuthManager.admin_session(),
settings_manager,
ADMIN_SETTINGS_ENTRY,
AdminSettings,
"ai_buddy")

View File

@@ -0,0 +1,8 @@
.adm-items-group {
border: 1px solid color-mix(in oklab, var(--color-base-content) 20%, #0000);
border-radius: .25rem;
padding: 4px;
background-color: var(--color-base-100);
align-items: center;
padding-inline: .75rem;
}

View File

View File

@@ -0,0 +1,62 @@
from components.BaseCommandManager import BaseCommandManager
from components.admin.constants import ROUTE_ROOT, Routes
class AdminCommandManager(BaseCommandManager):
def __init__(self, owner):
super().__init__(owner)
def show_ai_buddy(self):
return {
"hx-get": f"{ROUTE_ROOT}{Routes.AiBuddy}",
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
"hx-swap": "outerHTML",
"hx-vals": f'js:{{"_id": "{self._id}", boundaries: getTabContentBoundaries("{self._owner.tabs_manager.get_id()}")}}',
}
def save_ai_buddy(self):
return {
"hx-post": f"{ROUTE_ROOT}{Routes.AiBuddy}",
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
"hx-swap": "outerHTML",
"hx-vals": f'js:{{"_id": "{self._id}"}}',
# The form adds the rest
}
def cancel_ai_buddy(self):
return {
"hx-post": f"{ROUTE_ROOT}{Routes.AiBuddyCancel}",
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
"hx-swap": "outerHTML",
"hx-vals": f'js:{{"_id": "{self._id}"}}',
}
def show_import_holidays(self):
return {
"hx-get": f"{ROUTE_ROOT}{Routes.ImportHolidays}",
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
"hx-swap": "outerHTML",
"hx-vals": f'js:{{"_id": "{self._id}", boundaries: getTabContentBoundaries("{self._owner.tabs_manager.get_id()}")}}',
}
class ImportHolidaysCommandManager(BaseCommandManager):
def __init__(self, owner):
super().__init__(owner)
def on_paste(self):
return {
"hx-post": f"{ROUTE_ROOT}{Routes.PasteHolidays}",
"hx-target": f"#{self._owner.datagrid.get_id()}",
"hx_trigger": "keydown[ctrlKey && key=='Enter']",
"hx-swap": "outerHTML",
"hx-vals": f'js:{{"_id": "{self._id}"}}',
}
def import_holidays(self):
return {
"hx-post": f"{ROUTE_ROOT}{Routes.ImportHolidays}",
"hx-target": f"#{self._owner.datagrid.get_id()}",
"hx-swap": "outerHTML",
"hx-vals": f'js:{{"_id": "{self._id}"}}',
}

View File

@@ -0,0 +1,136 @@
from fasthtml.components import *
from ai.mcp_client import MPC_CLIENTS_IDS
from ai.mcp_tools import MCPServerTools
from components.BaseComponent import BaseComponent
from components.admin.admin_db_manager import AdminDbManager
from components.admin.commands import AdminCommandManager
from components.admin.components.AdminForm import AdminFormItem, AdminFormType, AdminForm
from components.admin.components.ImportHolidays import ImportHolidays
from components.admin.constants import ADMIN_INSTANCE_ID, ADMIN_AI_BUDDY_INSTANCE_ID, ADMIN_IMPORT_HOLIDAYS_INSTANCE_ID
from components.aibuddy.assets.icons import icon_brain_ok
from components.hoildays.assets.icons import icon_holidays
from components.tabs.components.MyTabs import MyTabs
from components_helpers import mk_ellipsis, mk_icon
from core.instance_manager import InstanceManager
class Admin(BaseComponent):
def __init__(self, session, _id: str = None, settings_manager=None, tabs_manager: MyTabs = None):
super().__init__(session, _id)
self.settings_manager = settings_manager
self.tabs_manager = tabs_manager
self.commands = AdminCommandManager(self)
self.db = AdminDbManager(session, settings_manager)
def show_ai_buddy_form(self, boundaries):
fields = [
AdminFormItem('ollama_ip', "Ollama IP", "IP address of the Ollama server.", AdminFormType.TEXT),
AdminFormItem("ollama_port", "Ollama Port", "Port of the Ollama server.", AdminFormType.NUMBER),
AdminFormItem("ollama_model", "LLM Model", "Model to use for Ollama.", AdminFormType.SELECT, ["mistral"]),
AdminFormItem("llm_mode", "LLM Mode", "Type of LLM to use (Dev)", AdminFormType.SELECT, MPC_CLIENTS_IDS),
AdminFormItem("available_tools", "Available Tools", "Possible tools", AdminFormType.CHECKBOX,
MCPServerTools.list_tools()),
]
hooks = {
"on_ok": self.commands.save_ai_buddy(),
"on_cancel": self.commands.cancel_ai_buddy(),
"ok_title": "Apply"
}
form = InstanceManager.get(self._session,
AdminForm.create_component_id(self._session, prefix=self._id),
AdminForm,
owner=self,
title="AI Buddy Configuration Page",
obj=self.db.ai_buddy,
form_fields=fields,
hooks=hooks,
key=ADMIN_AI_BUDDY_INSTANCE_ID,
boundaries=boundaries)
return self._add_tab(ADMIN_AI_BUDDY_INSTANCE_ID, "Admin - AI Buddy", form)
def show_import_holidays_form(self, boundaries):
form = InstanceManager.get(self._session,
ImportHolidays.create_component_id(self._session, prefix=self._id),
ImportHolidays,
settings_manager=self.settings_manager,
boundaries=boundaries)
return self._add_tab(ADMIN_AI_BUDDY_INSTANCE_ID, "Admin - Import Holidays", form)
def update_ai_buddy_settings(self, values: dict):
values = self.manage_lists(values)
self.db.ai_buddy.update(values, ignore_missing=True)
return self.tabs_manager.render()
def cancel_ai_buddy_settings(self):
tab_id = self.tabs_manager.get_tab_id(ADMIN_AI_BUDDY_INSTANCE_ID)
self.tabs_manager.remove_tab(tab_id)
return self.tabs_manager.render()
def __ft__(self):
return Div(
Div(cls="divider"),
Div(mk_ellipsis("Admin", cls="text-sm font-medium mb-1 mr-3")),
#
Div(
mk_icon(icon_brain_ok, can_select=False),
mk_ellipsis("AI Buddy", cls="text-sm", **self.commands.show_ai_buddy()),
cls="flex p-0 min-h-0 truncate",
),
Div(
mk_icon(icon_holidays, can_select=False),
mk_ellipsis("holidays", cls="text-sm", **self.commands.show_import_holidays()),
cls="flex p-0 min-h-0 truncate",
),
#
# cls=""),
# Script(f"bindAdmin('{self._id}')"),
id=f"{self._id}"
)
def _add_tab(self, tab_key, title, content):
self.tabs_manager.add_tab(title, content, key=tab_key)
return self.tabs_manager.render()
@staticmethod
def create_component_id(session):
return f"{ADMIN_INSTANCE_ID}{session['user_id']}"
@staticmethod
def manage_lists(data_dict):
"""
Processes a dictionary of key-value pairs to reorganize keys based on specific
criteria. If a key ends with its corresponding string value, the method extracts
the prefix of the key (the portion of the key before the value) and groups the
value under this prefix in a list. Otherwise, the original key-value pair is
preserved in the resulting dictionary.
:param data_dict: Dictionary where each key is a string and its corresponding
value can be of any type.
:type data_dict: dict
:return: A dictionary where the keys have been categorized into groups
based on whether they end with the same string value, reorganized into
lists, while preserving other key-value pairs as they are.
:rtype: dict
"""
result_dict = {}
for key, value in data_dict.items():
# Check if the value is a string and the key ends with the value
if isinstance(value, str) and key.endswith(value):
# Find the beginning part of the key (before the value)
prefix = key.replace(value, '').rstrip('_')
# Add the value to the list under the prefix key
if prefix not in result_dict:
result_dict[prefix] = []
result_dict[prefix].append(value)
else:
result_dict[key] = value
return result_dict

View File

@@ -0,0 +1,121 @@
from dataclasses import dataclass
from typing import Any
from fasthtml.components import *
from components.BaseComponent import BaseComponent
from components_helpers import set_boundaries, mk_dialog_buttons, safe_get_dialog_buttons_parameters
from core.utils import get_unique_id
class AdminFormType:
TEXT = "text"
NUMBER = "number"
DATE = "date"
SELECT = "select"
CHECKBOX = "checkbox"
RADIO = "radio"
TEXTAREA = "textarea"
@dataclass
class AdminFormItem:
name: str
title: str
description: str
type: str
possible_values: list[str] = None
class AdminForm(BaseComponent):
def __init__(self, session, _id, owner, title: str, obj: Any, form_fields: list[AdminFormItem], hooks=None, key=None,
boundaries=None):
super().__init__(session, _id)
self._owner = owner
self._hooks = hooks
self._key = key
self._boundaries = boundaries
self.title = title
self.obj = obj
self.form_fields = form_fields
def mk_input(self, item: AdminFormItem):
return Input(
type=item.type,
value=getattr(self.obj, item.name),
id=item.name,
cls="input"
)
def mk_checkbox(self, item: AdminFormItem):
if not item.possible_values:
return Div("No options available")
current_values = getattr(self.obj, item.name, [])
checkbox_items = [Div(
Input(
Label(value),
type="checkbox",
value=value,
id=f"{item.name}_{value}",
cls="checkbox checkbox-xs",
checked=value in current_values
),
cls="checkbox-item") for value in item.possible_values]
return Div(*checkbox_items, cls="adm-items-group")
def mk_select(self, item: AdminFormItem):
if not item.possible_values:
return Div("No options available")
current_value = getattr(self.obj, item.name, "")
options = [
Option(
value,
value=value,
selected=value == current_value
) for value in item.possible_values
]
return Select(
*options,
id=item.name,
cls="select select-bordered w-full"
)
def mk_item(self, item: AdminFormItem):
if item.type == AdminFormType.CHECKBOX:
return self.mk_checkbox(item)
elif item.type == AdminFormType.SELECT:
return self.mk_select(item)
else:
return self.mk_input(item)
def __ft__(self):
return Form(
Fieldset(Legend(self.title, cls="fieldset-legend"),
*[
Div(
Label(item.title, cls="label"),
self.mk_item(item),
P(item.description, cls="label"),
)
for item in self.form_fields
],
mk_dialog_buttons(**safe_get_dialog_buttons_parameters(self._hooks)),
**set_boundaries(self._boundaries),
cls="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4"
)
)
@staticmethod
def create_component_id(session, prefix=None, suffix=None):
if suffix is None:
suffix = get_unique_id()
return f"{prefix}{suffix}"

View File

@@ -0,0 +1,59 @@
from fasthtml.components import *
from pandas import DataFrame
from components.BaseComponent import BaseComponent
from helpers.ComponentsInstancesHelper import ComponentsInstancesHelper
from components.admin.commands import ImportHolidaysCommandManager
from components.admin.constants import ADMIN_IMPORT_HOLIDAYS_INSTANCE_ID
from components.datagrid_new.components.DataGrid import DataGrid
from components.datagrid_new.settings import DataGridSettings
from components.hoildays.helpers.nibelisparser import NibelisParser
from components.repositories.constants import USERS_REPOSITORY_NAME, HOLIDAYS_TABLE_NAME
from components_helpers import mk_dialog_buttons, set_boundaries
from conftest import session
from core.instance_manager import InstanceManager
class ImportHolidays(BaseComponent):
def __init__(self, session, _id, settings_manager, boundaries=None):
super().__init__(session, _id)
self._settings_manager = settings_manager
self._boundaries = boundaries
self.commands = ImportHolidaysCommandManager(self)
self.datagrid = InstanceManager.get(session,
DataGrid.create_component_id(session),
DataGrid,
grid_settings=DataGridSettings(views_visible=False,
open_file_visible=False,
open_settings_visible=False))
def on_paste(self, content):
parser = NibelisParser(content)
holidays = parser.parse()
self.datagrid.init_from_dataframe(DataFrame(holidays))
return self.datagrid
def import_holidays(self):
repositories = ComponentsInstancesHelper.get_repositories(self._session)
key = repositories.db.ensure_exists(USERS_REPOSITORY_NAME, HOLIDAYS_TABLE_NAME)
datagrid = DataGrid(self._session, DataGrid.create_component_id(self._session), key, self._settings_manager)
datagrid.init_from_dataframe(self.datagrid.get_dataframe(), save_state=True)
return repositories.refresh()
def __ft__(self):
return Div(
Div("Import holidays...", cls="mb-2"),
Textarea(name="content",
cls="textarea",
placeholder="Paste the content.\nCtl+Enter when done.",
**self.commands.on_paste()),
Div(self.datagrid, cls="mt-2"),
mk_dialog_buttons(ok_title="Import", cls="mt-2", on_ok=self.commands.import_holidays()),
id=self._id,
cls="m-2",
**set_boundaries(self._boundaries, other=26),
)
@staticmethod
def create_component_id(session, prefix=None, suffix=None):
return f"{ADMIN_IMPORT_HOLIDAYS_INSTANCE_ID}{session['user_id']}"

View File

@@ -0,0 +1,11 @@
ADMIN_INSTANCE_ID = "__Admin__"
ADMIN_AI_BUDDY_INSTANCE_ID = "__AdminAIBuddy__"
ADMIN_IMPORT_HOLIDAYS_INSTANCE_ID = "__AdminImportHolidays__"
ROUTE_ROOT = "/admin"
ADMIN_SETTINGS_ENTRY = "Admin"
class Routes:
AiBuddy = "/ai-buddy"
AiBuddyCancel = "/ai-buddy-cancel"
ImportHolidays = "/import-holidays"
PasteHolidays = "/paste-holidays"

View File