From c158ff4e0e5ffcee56802d43041254208ea7661c Mon Sep 17 00:00:00 2001 From: c-d-p Date: Mon, 21 Apr 2025 15:36:59 +0200 Subject: [PATCH] Fixed entire calendar layout + chat layout + chat history --- backend/__pycache__/main.cpython-312.pyc | Bin 1915 -> 1940 bytes backend/alembic.ini | 119 +++++++++++ backend/alembic/README | 1 + .../alembic/__pycache__/env.cpython-312.pyc | Bin 0 -> 3127 bytes backend/alembic/env.py | 97 +++++++++ backend/alembic/script.py.mako | 28 +++ ..._initial_migration_with_existing_tables.py | 32 +++ ...ation_with_existing_tables.cpython-312.pyc | Bin 0 -> 1004 bytes .../core/__pycache__/database.cpython-312.pyc | Bin 1633 -> 1887 bytes backend/core/database.py | 4 + backend/main.py | 1 + .../__pycache__/schemas.cpython-312.pyc | Bin 2777 -> 2777 bytes backend/modules/calendar/schemas.py | 2 +- .../nlp/__pycache__/api.cpython-312.pyc | Bin 5174 -> 7936 bytes .../nlp/__pycache__/models.cpython-312.pyc | Bin 0 -> 1383 bytes .../nlp/__pycache__/service.cpython-312.pyc | Bin 5014 -> 6347 bytes backend/modules/nlp/api.py | 98 +++++++-- backend/modules/nlp/models.py | 23 +++ backend/modules/nlp/service.py | 27 +++ backend/requirements.txt | 1 + interfaces/nativeapp/app.json | 3 +- interfaces/nativeapp/package-lock.json | 4 +- interfaces/nativeapp/package.json | 12 +- interfaces/nativeapp/src/api/client.ts | 3 +- .../components/calendar/CalendarDayCell.tsx | 76 +++++-- .../components/calendar/CalendarHeader.tsx | 38 ++-- .../calendar/CustomSegmentedButtons.tsx | 86 ++++++++ .../src/components/calendar/EventItem.tsx | 10 +- .../src/components/calendar/ThreeDayView.tsx | 168 ++++++++++------ .../src/components/calendar/ViewSwitcher.tsx | 26 +-- .../src/components/calendar/WeekView.tsx | 177 ++++++++++------- .../nativeapp/src/navigation/AppNavigator.tsx | 45 +++++ .../src/navigation/MobileTabNavigator.tsx | 1 + .../src/navigation/RootNavigator.tsx | 5 +- .../nativeapp/src/screens/CalendarScreen.tsx | 52 +++-- .../nativeapp/src/screens/ChatScreen.tsx | 188 ++++++++++++------ interfaces/nativeapp/src/types/navigation.ts | 8 +- 37 files changed, 1050 insertions(+), 285 deletions(-) create mode 100644 backend/alembic.ini create mode 100644 backend/alembic/README create mode 100644 backend/alembic/__pycache__/env.cpython-312.pyc create mode 100644 backend/alembic/env.py create mode 100644 backend/alembic/script.py.mako create mode 100644 backend/alembic/versions/69069d6184b3_initial_migration_with_existing_tables.py create mode 100644 backend/alembic/versions/__pycache__/69069d6184b3_initial_migration_with_existing_tables.cpython-312.pyc create mode 100644 backend/modules/nlp/__pycache__/models.cpython-312.pyc create mode 100644 backend/modules/nlp/models.py create mode 100644 interfaces/nativeapp/src/components/calendar/CustomSegmentedButtons.tsx create mode 100644 interfaces/nativeapp/src/navigation/AppNavigator.tsx diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc index d7b8813b261d8582a41f18d5fe4a476a5251bf1e..4903f4e66088002c4ebdaf809f05faf99e3796fa 100644 GIT binary patch delta 164 zcmey(H-(?~G%qg~0}yE0vt@kU$eYS2l+KXCw}>%?KSdy&5ro$WPHtgTlonde2vWzu zkRsf|uo}W-h?1*h*A&@&fN={GlP2@z5*7oAD(Q@pk^(DzeM3tlJwr1KJtI?7JtHH- z$@5uOG8%36WDR9xJU4kh+cbqwOsv9eA9xtVrEds}O;80BA4C}#g}*RxGctXUnHaD3|~M delta 142 zcmbQj|C^8ZG%qg~0}vcN#*%SvBX26BPzqlPe>zi&K#E{GBM7e%n%u&uC@s925u}cR zAw{HxVKs!w5G7a1t|_|t0OJ;>$z?1ClMk~jVKm&F!5YfQcxCcFwrOe)IOK0|D1KmN j5oY_q#UL(yLs)ErDwy~n%)lu8g@KEa>4Vhd5_SauE3qZT diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000..4a86e2c --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,119 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +# Use forward slashes (/) also on windows to provide an os agnostic path +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +# version_path_separator = newline +# +# Use os.pathsep. Default configuration used for new projects. +version_path_separator = os + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# sqlalchemy.url = driver://user:pass@localhost/dbname +sqlalchemy.url = postgresql://maia:maia@localhost:5432/maia + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = check --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARNING +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARNING +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/alembic/README b/backend/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/backend/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/backend/alembic/__pycache__/env.cpython-312.pyc b/backend/alembic/__pycache__/env.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..23a325fd2f3babde2fdbdaba159ec6fff04f81ec GIT binary patch literal 3127 zcmbtWO>7&-6`tiT$zA?RlquPgs;pH@3Jr&{f~0BFqJgA1iP}ar3@1Q%A(p!%a+&>M zXNHz(*r*H?NDUNldU8_~NRBmRAUX8VOAhum7ZXxIWg{Po!UcL$AsYsC%9~xTmX>wv z5Zrn5-kbMkXWsYCev{3n5VVKCk2gL{BlK@R2wSo*?8jmV-9ZL21PfJyUZ@B#j#)8V ztcU{FiI!x?D{)_zKu%N=z8trbwp@_~gi*5)hK~AXF_J^+|MnaUBSk&VYzo2XXkW=< z;nsG>EL$V?iOLCIR;;2uS{a2MkelUT z?%=aBhR2%c2Q`6wvk*gAJl+<*1H>F^zWUQ8g@#NsRl>IOFT`dV-YR$Cpvg@pwbr=<}Q#T!4T_dht)kCe+a$O6oNzgES zm4PTNX%zGR1Bwas*=5bbj-inYSFpn!(#s8vy^kr?>R2CKD=$S6{`>!d#~s8DZB4E{ z(hMQoo*Rh8Lt`@$N>P6uF*zI=F+(&YQ@9u3lmeYFINOIw=o|Fm=DZH2FpwwnycZvZ zo=^@6{)XDt?64g;G*`2P*bb+fi+^PBdn^4pmJ@opaaL zEZFZk)piYBPNh_6mm4NkgS~GPOjXvve8@LmT{l@nZMG>>HT8o_zw!Mw$50&?G>&?~ zKfQA>#ckIckw%4xny%v((`pv>XEiXvsz%jyY*}ShUFxXxW3$yVow}beh@|QS`>XY* ztYg)1ojImrIAhhp*nm~My|j37MP;s9gK$?g)8ZNWZvr)8!TQ~B`DJ_+>urW#E~$Qc zY@5u_RyTP>%7~xd#~<>X+*Or>sZ)} zJDQD+lH`eC^72e0b4mM-s~gL^Qo1Jt2vIB^LS`Wx* zfSp2srUw98ig}3^!E5GKPvN(SZb2|mrlL4LmR3s{!nvC8Gw{+Jel$qq6#~>%O~(v% zgx8cOueL4gA_Vm>CcY{kri34pAC&M962i{{GN$_(IX{s}&{1BZs!I45hF3faH-GEM zG}zwbzJG-Wpe=_+W_IOVPaf~e}6-W+1ah^%K$oM zW;Zj^%S?AO)7zOdfJbHKSsG^hA)cjRxW}CiBIN-+1IxRgKfkmf-hZpOG!uVtLI(N4 zOnj-VJUE-=@8JF9%yqIHW z^HjiyQcGhEFKL*B^RAcFYSh=r)hhq4s8+r7#Y?~a)rIBD)wfqJc?r{j&jsdXb(i2W zXXF}uMi9;--UwWQZ41+~)@F?|zYi@+_?7OBg%-X4jS*TEZY_jX$7+QJP8C5V4fTu1 zUyZ*;IlK&l65)L6AG4P?X~?owp8hV{2OIM1<3RpJkmi@y_el93 zr5>S~$EfrOO*}>udos!o->%=P-*#`gf2n<5|E&JG`}h|Ecil&$rP_Ph}*$^cbCagccs5v%HdqZ%^Ht+DJfPDb*9l zyW;pmarQeY_s1)}{A@QryDiP_L2-`p0^G>&7mC8^{qrNj%-%U9O>DdoOxYGEzmvwl Rl_viso%&XqAaB4M`~WhX%?bbj literal 0 HcmV?d00001 diff --git a/backend/alembic/env.py b/backend/alembic/env.py new file mode 100644 index 0000000..ae86d51 --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,97 @@ +import os +import sys +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# --- Add project root to sys.path --- +# This assumes alembic/env.py is one level down from the project root (backend/) +PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, PROJECT_DIR) +# ----------------------------------- + +# --- Import Base and Models --- +from core.database import Base # Import your Base +# Import all your models here so Base registers them +from modules.auth.models import User # Example: Import User model +from modules.calendar.models import CalendarEvent # Example: Import CalendarEvent model +from modules.nlp.models import ChatMessage # Import the new ChatMessage model +# Add imports for any other models you have +# ---------------------------- + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +# --- Set target_metadata --- +target_metadata = Base.metadata +# --------------------------- + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako new file mode 100644 index 0000000..480b130 --- /dev/null +++ b/backend/alembic/script.py.mako @@ -0,0 +1,28 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + """Upgrade schema.""" + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + """Downgrade schema.""" + ${downgrades if downgrades else "pass"} diff --git a/backend/alembic/versions/69069d6184b3_initial_migration_with_existing_tables.py b/backend/alembic/versions/69069d6184b3_initial_migration_with_existing_tables.py new file mode 100644 index 0000000..c74bf5b --- /dev/null +++ b/backend/alembic/versions/69069d6184b3_initial_migration_with_existing_tables.py @@ -0,0 +1,32 @@ +"""Initial migration with existing tables + +Revision ID: 69069d6184b3 +Revises: +Create Date: 2025-04-21 01:14:33.233195 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '69069d6184b3' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/backend/alembic/versions/__pycache__/69069d6184b3_initial_migration_with_existing_tables.cpython-312.pyc b/backend/alembic/versions/__pycache__/69069d6184b3_initial_migration_with_existing_tables.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f74e51bfa2f0941bfaf4d6606b150585ff2765c7 GIT binary patch literal 1004 zcmah|y>HV%6hD6?vE4XnD+)p&YEe`XP~vRDPLA(rEc}t4owlij z$iUDsOogoj1N<#4t+JG3L<~$+6bY#lcTNfg32~Bte((LB@4ffC^Lt&_09zlPr&?b{ z06uZ&Y{?_zMQ&POmICjQNG~`~sS<+?i|Eh767x zhIWr&JI2N`gN9KpSL?;{T(Md)%9TcCu2HL%sj}^FghM?`?Jmu9hAoghD>=n$XBhOraT=slqBHxyQ%o!~ zJ^q{{nPLch3zB0%Rjyl-M<$tnl@sF}cnUj*G{JF)1gnK{nt>374umuvciZd&5Uw2I z%F)bda+2bn7HxR8$JOgnF|rA7o=Qh~OjM33@Ys_6r*WC+}oKW)a&TU)UK0BtJ1Qg5-syFUUk*7K*yS6ZQK8Gf3!Hu>{bRgAyW6 zq6~*bIi2*F56N*mX)+(uWCgM(doj~&0l2oJ&XIBatBQ%ZAE?TR!fbFi7P@-Pa2l9}wtCc^?!0|26DgarTq delta 319 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 D3N2JE literal 0 HcmV?d00001 diff --git a/backend/modules/nlp/__pycache__/service.cpython-312.pyc b/backend/modules/nlp/__pycache__/service.cpython-312.pyc index 65c73fc1403e7de0c479af4997d4efaf243193e0..c49445c704acf6349b208c4e5c2a7fdc93b06905 100644 GIT binary patch delta 1870 zcmah}U2GIp6ux)oXaE1`ZnxbQvJr4uj9{TrL_|$1gd&*Gh-Q3fraQOYS!Q<1ohjO; zW+9@|@Ic8n&<9L>B$fw~zWB%^2`@Ayfs7#~QKNmbSbw4q>N$726nyb+=A3iS`DX4p z-#K@$6z}xZ@5JK~0!ykZm7g<&e21IX5)s&})hHoXi9rl%5u0k1q8+dTwxmhImMqy; zG)365rD`g~6)R{5wV)l+Lc*_FVLPHlzz!NAD{9BIn0+j+#VH98rc}FIr*3E?d?PXs zN8{3xMig)LFT%q zQv*{rm|F&|jIx?;9xny_(CdcoG0(J_A3{0l)EENaF<7nP#)nM9S6vbDWsiOAm1Rs~eJP+Nc-B4#ag})MtP(Osrvg*b z4LXleSf9vKq$)LfqHQs`#XTicjYpAeV!jC>uqV|ZV(L$0r3+!sIRpKj_iRr1FdE>lRUZ)O2 zq)GG(!1Uj+yNXOb;aUKM@B%G+o;C{Ud~Cx}Re{({@2MSxwD_-vTj&k#MO*g9s|^~< zT>d&0s3t{k0#DvYg8#Exy)-EN0-s4Z+0Z{}viAh@xXG}G*fC3Yy+TpcTYRRaZehgX z&}N*yUB_4cfmI6o%IR^&CqygjzB*!B9^;~Ie#qf4v_r!a9Ou#xr{|x_psc z?w=W+>3|BxIE{$e<@kTpm`xU6+XjXof{`6_ zYA*9B<1c~7#fqVfH=A`N$GB65dojcru;qIEEeIP6zbAj7Tb>8A6yPOLd?(rpe;I8Y zS-!9FXS!>rxGe#WNAWj6xHwBKj#S+}HKd!oAAFxS4sJS77ulES${z5P@_xcSZRu9I z!t9A%4!1cn>_;}7&F-k9jx%a8j?>Kdqfk)<5v+O>V`goXhtMPP!i&ps9(}dXGFi3T>yz9rFRQhZ2qe}HfIYKTPIFJv!uPj}zla=5Xnty0dYw0*9) zNQ?{luEqZM5S3reY^IU9!8HPE^l= z_c&vpDL68FAr1v+>=jc?@t7q%!`h_BJjS_(14ptU#HRVP9Thq^=lcjq4o8MlV-febdxmUkdj$4=c zz3H14aMxIceo`12#c{dX?p=4Vn{hzr<1Ed!;TqbQ$t{HM`#0ePtvAPzZR{>5(XCu^g_R!jU-c(Sl@uF7}6Ez`-tz7bMF4M8DIIx+)u1m{#bs*8|C|x%N7;? E1vP4ey#N3J diff --git a/backend/modules/nlp/api.py b/backend/modules/nlp/api.py index 0800ab9..de262b0 100644 --- a/backend/modules/nlp/api.py +++ b/backend/modules/nlp/api.py @@ -7,13 +7,13 @@ from core.database import get_db from modules.auth.dependencies import get_current_user from modules.auth.models import User -from modules.nlp.service import process_request, ask_ai -# Import the response schema +# Import the new service functions and Enum +from modules.nlp.service import process_request, ask_ai, save_chat_message, get_chat_history, MessageSender +# Import the response schema and the new ChatMessage model for response type hinting from modules.nlp.schemas import ProcessCommandRequest, ProcessCommandResponse +from modules.nlp.models import ChatMessage # Import ChatMessage model from modules.calendar.service import create_calendar_event, get_calendar_events, update_calendar_event, delete_calendar_event -# Import the CalendarEvent *model* for type hinting from modules.calendar.models import CalendarEvent -# Import the CalendarEvent Pydantic schemas for data validation from modules.calendar.schemas import CalendarEventCreate, CalendarEventUpdate router = APIRouter(prefix="/nlp", tags=["nlp"]) @@ -35,9 +35,14 @@ def format_calendar_events(events: List[CalendarEvent]) -> List[str]: @router.post("/process-command", response_model=ProcessCommandResponse) def process_command(request_data: ProcessCommandRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): """ - Process the user command, execute the action, and return user-friendly responses. + Process the user command, save messages, execute action, save response, and return user-friendly responses. """ user_input = request_data.user_input + + # --- Save User Message --- + save_chat_message(db, user_id=current_user.id, sender=MessageSender.USER, text=user_input) + # ------------------------ + command_data = process_request(user_input) intent = command_data["intent"] params = command_data["params"] @@ -45,10 +50,19 @@ def process_command(request_data: ProcessCommandRequest, current_user: User = De responses = [response_text] # Start with the initial response + # --- Save Initial AI Response --- + # Save the first response generated by process_request + save_chat_message(db, user_id=current_user.id, sender=MessageSender.AI, text=response_text) + # ----------------------------- + if intent == "error": - raise HTTPException(status_code=400, detail=response_text) + # Don't raise HTTPException here if we want to save the error message + # Instead, return the error response directly + # save_chat_message(db, user_id=current_user.id, sender=MessageSender.AI, text=response_text) # Already saved above + return ProcessCommandResponse(responses=responses) if intent == "clarification_needed" or intent == "unknown": + # save_chat_message(db, user_id=current_user.id, sender=MessageSender.AI, text=response_text) # Already saved above return ProcessCommandResponse(responses=responses) try: @@ -56,48 +70,100 @@ def process_command(request_data: ProcessCommandRequest, current_user: User = De case "ask_ai": ai_answer = ask_ai(**params) responses.append(ai_answer) + # --- Save Additional AI Response --- + save_chat_message(db, user_id=current_user.id, sender=MessageSender.AI, text=ai_answer) + # --------------------------------- return ProcessCommandResponse(responses=responses) case "get_calendar_events": - # get_calendar_events returns List[CalendarEvent models] events: List[CalendarEvent] = get_calendar_events(db, current_user.id, **params) - responses.extend(format_calendar_events(events)) + formatted_responses = format_calendar_events(events) + responses.extend(formatted_responses) + # --- Save Additional AI Responses --- + for resp in formatted_responses: + save_chat_message(db, user_id=current_user.id, sender=MessageSender.AI, text=resp) + # ---------------------------------- return ProcessCommandResponse(responses=responses) case "add_calendar_event": - # Validate input with Pydantic schema event_data = CalendarEventCreate(**params) created_event = create_calendar_event(db, current_user.id, event_data) start_str = created_event.start.strftime("%Y-%m-%d %H:%M") if created_event.start else "No start time" title = created_event.title or "Untitled Event" - responses.append(f"Added: {title} starting at {start_str}.") + add_response = f"Added: {title} starting at {start_str}." + responses.append(add_response) + # --- Save Additional AI Response --- + save_chat_message(db, user_id=current_user.id, sender=MessageSender.AI, text=add_response) + # --------------------------------- return ProcessCommandResponse(responses=responses) case "update_calendar_event": event_id = params.pop('event_id', None) if event_id is None: - raise HTTPException(status_code=400, detail="Event ID is required for update.") - # Validate input with Pydantic schema + # Save the error message before raising + error_msg = "Event ID is required for update." + save_chat_message(db, user_id=current_user.id, sender=MessageSender.AI, text=error_msg) + raise HTTPException(status_code=400, detail=error_msg) event_data = CalendarEventUpdate(**params) updated_event = update_calendar_event(db, current_user.id, event_id, event_data=event_data) title = updated_event.title or "Untitled Event" - responses.append(f"Updated event ID {updated_event.id}: {title}.") + update_response = f"Updated event ID {updated_event.id}: {title}." + responses.append(update_response) + # --- Save Additional AI Response --- + save_chat_message(db, user_id=current_user.id, sender=MessageSender.AI, text=update_response) + # --------------------------------- return ProcessCommandResponse(responses=responses) case "delete_calendar_event": event_id = params.get('event_id') if event_id is None: - raise HTTPException(status_code=400, detail="Event ID is required for delete.") + # Save the error message before raising + error_msg = "Event ID is required for delete." + save_chat_message(db, user_id=current_user.id, sender=MessageSender.AI, text=error_msg) + raise HTTPException(status_code=400, detail=error_msg) delete_calendar_event(db, current_user.id, event_id) - responses.append(f"Deleted event ID {event_id}.") + delete_response = f"Deleted event ID {event_id}." + responses.append(delete_response) + # --- Save Additional AI Response --- + save_chat_message(db, user_id=current_user.id, sender=MessageSender.AI, text=delete_response) + # --------------------------------- return ProcessCommandResponse(responses=responses) case _: print(f"Warning: Unhandled intent '{intent}' reached api.py match statement.") + # The initial response_text was already saved return ProcessCommandResponse(responses=responses) except HTTPException as http_exc: + # Don't save again if already saved before raising + if http_exc.status_code != 400 or ('event_id' not in http_exc.detail.lower()): + save_chat_message(db, user_id=current_user.id, sender=MessageSender.AI, text=http_exc.detail) raise http_exc except Exception as e: print(f"Error executing intent '{intent}': {e}") - return ProcessCommandResponse(responses=["Sorry, I encountered an error while trying to perform that action."]) \ No newline at end of file + error_response = "Sorry, I encountered an error while trying to perform that action." + # --- Save Final Error AI Response --- + save_chat_message(db, user_id=current_user.id, sender=MessageSender.AI, text=error_response) + # ---------------------------------- + return ProcessCommandResponse(responses=[error_response]) + +# --- New Endpoint for Chat History --- +# Define a Pydantic schema for the response (optional but good practice) +from pydantic import BaseModel +from datetime import datetime + +class ChatMessageResponse(BaseModel): + id: int + sender: MessageSender # Use the enum directly + text: str + timestamp: datetime + + class Config: + from_attributes = True # Allow Pydantic to work with ORM models + +@router.get("/history", response_model=List[ChatMessageResponse]) +def read_chat_history(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + """Retrieves the last 50 chat messages for the current user.""" + history = get_chat_history(db, user_id=current_user.id, limit=50) + return history +# ------------------------------------- \ No newline at end of file diff --git a/backend/modules/nlp/models.py b/backend/modules/nlp/models.py new file mode 100644 index 0000000..b92a1bc --- /dev/null +++ b/backend/modules/nlp/models.py @@ -0,0 +1,23 @@ +\ +# /home/cdp/code/MAIA/backend/modules/nlp/models.py +from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum as SQLEnum +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +import enum + +from core.database import Base + +class MessageSender(enum.Enum): + USER = "user" + AI = "ai" + +class ChatMessage(Base): + __tablename__ = "chat_messages" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) + sender = Column(SQLEnum(MessageSender), nullable=False) + text = Column(Text, nullable=False) + timestamp = Column(DateTime(timezone=True), server_default=func.now()) + + owner = relationship("User") # Relationship to the User model diff --git a/backend/modules/nlp/service.py b/backend/modules/nlp/service.py index 5d92cd2..d2722be 100644 --- a/backend/modules/nlp/service.py +++ b/backend/modules/nlp/service.py @@ -1,8 +1,14 @@ # modules/nlp/service.py +from sqlalchemy.orm import Session +from sqlalchemy import desc # Import desc for ordering from google import genai import json from datetime import datetime, timezone +from typing import List # Import List + +# Import the new model and Enum +from .models import ChatMessage, MessageSender # from core.config import settings # client = genai.Client(api_key=settings.GOOGLE_API_KEY) @@ -70,6 +76,27 @@ Here is some context for you: Here is the user request: """ + +# --- Chat History Service Functions --- + +def save_chat_message(db: Session, user_id: int, sender: MessageSender, text: str): + """Saves a chat message to the database.""" + db_message = ChatMessage(user_id=user_id, sender=sender, text=text) + db.add(db_message) + db.commit() + db.refresh(db_message) + return db_message + +def get_chat_history(db: Session, user_id: int, limit: int = 50) -> List[ChatMessage]: + """Retrieves the last 'limit' chat messages for a user.""" + return db.query(ChatMessage)\ + .filter(ChatMessage.user_id == user_id)\ + .order_by(desc(ChatMessage.timestamp))\ + .limit(limit)\ + .all()[::-1] # Reverse to get oldest first for display order + +# --- Existing NLP Service Functions --- + def process_request(request: str): """ Process the user request using the Google GenAI API. diff --git a/backend/requirements.txt b/backend/requirements.txt index f60d7a6..4f680bb 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -43,3 +43,4 @@ tzdata==2025.2 uvicorn==0.34.1 vine==5.1.0 wcwidth==0.2.13 +alembic diff --git a/interfaces/nativeapp/app.json b/interfaces/nativeapp/app.json index 957c7ea..baa11bc 100644 --- a/interfaces/nativeapp/app.json +++ b/interfaces/nativeapp/app.json @@ -19,7 +19,8 @@ "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" - } + }, + "softwareKeyboardLayoutMode": "resize" }, "web": { "favicon": "./assets/favicon.png" diff --git a/interfaces/nativeapp/package-lock.json b/interfaces/nativeapp/package-lock.json index 412bb1a..238de80 100644 --- a/interfaces/nativeapp/package-lock.json +++ b/interfaces/nativeapp/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "dependencies": { "@expo/metro-runtime": "~4.0.1", - "@react-native-async-storage/async-storage": "1.23.1", + "@react-native-async-storage/async-storage": "^1.23.1", "@react-native-community/datetimepicker": "8.2.0", "@react-navigation/bottom-tabs": "^7.3.10", "@react-navigation/native": "^7.1.6", @@ -17,7 +17,7 @@ "async-storage": "^0.1.0", "axios": "^1.8.4", "date-fns": "^4.1.0", - "expo": "~52.0.46", + "expo": "^52.0.46", "expo-font": "~13.0.4", "expo-secure-store": "~14.0.1", "expo-splash-screen": "~0.29.24", diff --git a/interfaces/nativeapp/package.json b/interfaces/nativeapp/package.json index 0dbf64c..f8a2494 100644 --- a/interfaces/nativeapp/package.json +++ b/interfaces/nativeapp/package.json @@ -10,15 +10,18 @@ }, "dependencies": { "@expo/metro-runtime": "~4.0.1", - "@react-native-async-storage/async-storage": "1.23.1", + "@react-native-async-storage/async-storage": "^1.23.1", + "@react-native-community/datetimepicker": "8.2.0", "@react-navigation/bottom-tabs": "^7.3.10", "@react-navigation/native": "^7.1.6", "@react-navigation/native-stack": "^7.3.10", "async-storage": "^0.1.0", "axios": "^1.8.4", "date-fns": "^4.1.0", - "expo": "~52.0.46", + "expo": "^52.0.46", + "expo-font": "~13.0.4", "expo-secure-store": "~14.0.1", + "expo-splash-screen": "~0.29.24", "expo-status-bar": "~2.0.1", "react": "18.3.1", "react-dom": "18.3.1", @@ -30,10 +33,7 @@ "react-native-safe-area-context": "4.12.0", "react-native-screens": "~4.4.0", "react-native-vector-icons": "^10.2.0", - "react-native-web": "~0.19.13", - "@react-native-community/datetimepicker": "8.2.0", - "expo-font": "~13.0.4", - "expo-splash-screen": "~0.29.24" + "react-native-web": "~0.19.13" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/interfaces/nativeapp/src/api/client.ts b/interfaces/nativeapp/src/api/client.ts index 8a2f355..d8b279b 100644 --- a/interfaces/nativeapp/src/api/client.ts +++ b/interfaces/nativeapp/src/api/client.ts @@ -5,7 +5,8 @@ import * as SecureStore from 'expo-secure-store'; import AsyncStorage from '@react-native-async-storage/async-storage'; -const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://192.168.1.9:8000/api'; // Use your machine's IP +const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://192.168.255.221:8000/api'; // Use your machine's IP +// const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://192.168.1.9:8000/api'; // Use your machine's IP // const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:8000/api'; // Use your machine's IP const TOKEN_KEY = 'maia_access_token'; diff --git a/interfaces/nativeapp/src/components/calendar/CalendarDayCell.tsx b/interfaces/nativeapp/src/components/calendar/CalendarDayCell.tsx index 94745ab..d1923f5 100644 --- a/interfaces/nativeapp/src/components/calendar/CalendarDayCell.tsx +++ b/interfaces/nativeapp/src/components/calendar/CalendarDayCell.tsx @@ -1,6 +1,6 @@ // src/components/calendar/CalendarDayCell.tsx import React from 'react'; -import { View, StyleSheet, Dimensions, ScrollView } from 'react-native'; +import { View, StyleSheet, ScrollView } from 'react-native'; // Removed Dimensions unless used elsewhere import { Text, useTheme } from 'react-native-paper'; import { format, isToday } from 'date-fns'; @@ -11,56 +11,100 @@ interface CalendarDayCellProps { date: Date; events: CalendarEvent[]; isCurrentMonth?: boolean; // Optional, mainly for month view styling + height?: number; // Optional fixed height width?: number; // Optional fixed width } +const dateNumberSize = 24; // Define a size for the circle/text container + const CalendarDayCell: React.FC = ({ date, events, isCurrentMonth = true, height, width }) => { const theme = useTheme(); const today = isToday(date); + const dateNumber = format(date, 'd'); + // --- Define styles inside the component to access theme --- const styles = StyleSheet.create({ cell: { - flex: width ? undefined : 1, // Use flex=1 if no width is provided + flex: width ? undefined : 1, width: width, height: height, borderWidth: 0.5, borderTopWidth: 0, borderColor: theme.colors.outlineVariant, padding: 2, - paddingTop: 0, + paddingTop: 0, // Keep this 0 if header handles top padding backgroundColor: theme.colors.background, - overflow: 'hidden', // Prevent events overflowing cell boundaries + overflow: 'hidden', }, - dateNumberContainer: { + dayHeader: { // Renamed from dateNumberContainer for consistency with other views alignItems: 'center', + justifyContent: 'center', marginBottom: 2, + // Use minHeight instead of fixed height for flexibility + minHeight: dateNumberSize + 4, }, - dateNumber: { + dateNumberContainer: { // Base container for the date number + width: dateNumberSize, + height: dateNumberSize, + alignItems: 'center', + justifyContent: 'center', + // Circle properties moved to todayCircle + }, + todayCircle: { // Style for the *filled* circle highlight + // Inherit size/alignment from dateNumberContainer, just add background/radius + backgroundColor: theme.colors.primary, + borderRadius: dateNumberSize / 2, + }, + dateNumber: { // Base style for the date number text fontSize: 12, - fontWeight: today ? 'bold' : 'normal', - marginTop: 8, - color: today ? theme.colors.primary : (isCurrentMonth ? theme.colors.onSurface : theme.colors.onSurfaceDisabled), + // Base color determined by isCurrentMonth + color: isCurrentMonth ? theme.colors.onSurface : theme.colors.onSurfaceDisabled, + textAlign: 'center', + padding: 0, + includeFontPadding: false, + // Base font weight (non-today) + fontWeight: 'normal', + }, + todayDateNumber: { // Specific style for text on today's date + // Override color and weight for today + color: theme.colors.onPrimary, // Contrast with primary background + fontWeight: 'bold', + // Retain isCurrentMonth dimming logic if needed (optional, usually today is highlighted regardless) + // opacity: isCurrentMonth ? 1 : 0.6, // Example if you still want to slightly dim today if not in current month }, eventsContainer: { - flex: 1, // Take remaining space in the cell + flex: 1, }, }); + // --- End of styles --- return ( - - {format(date, 'd')} + {/* Renamed container View for consistency */} + + {/* Apply base container style, and add circle style conditionally */} + + + {dateNumber} + + - {/* Use ScrollView for month view where events might exceed fixed height */} {events - .sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()) // Sort events + .sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()) .map(event => ( - // Don't show time in month cell + ))} ); }; -export default React.memo(CalendarDayCell); // Memoize for performance +// Keep memoization +export default React.memo(CalendarDayCell); \ No newline at end of file diff --git a/interfaces/nativeapp/src/components/calendar/CalendarHeader.tsx b/interfaces/nativeapp/src/components/calendar/CalendarHeader.tsx index 9e792d8..17665ee 100644 --- a/interfaces/nativeapp/src/components/calendar/CalendarHeader.tsx +++ b/interfaces/nativeapp/src/components/calendar/CalendarHeader.tsx @@ -18,41 +18,37 @@ const CalendarHeader: React.FC = ({ currentRangeText, onPre const styles = StyleSheet.create({ container: { flexDirection: 'row', - justifyContent: 'flex-start', alignItems: 'center', paddingVertical: 8, - paddingHorizontal: 12, + paddingHorizontal: 8, borderBottomWidth: 1, borderBottomColor: theme.colors.outlineVariant, - backgroundColor: theme.colors.surface, // Match background + backgroundColor: theme.colors.surface, + }, + leftGroup: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, }, title: { fontSize: 18, fontWeight: 'bold', - color: theme.colors.onSurface, // Use theme text color + color: theme.colors.onSurface, + marginLeft: 12, + flexShrink: 1, }, }); return ( - - - {/* Placeholder for alignment */} - {currentRangeText} - {/* Spacer to push ViewSwitcher to the right */} - + + + + {currentRangeText} + + ); }; -export default CalendarHeader; +export default CalendarHeader; \ No newline at end of file diff --git a/interfaces/nativeapp/src/components/calendar/CustomSegmentedButtons.tsx b/interfaces/nativeapp/src/components/calendar/CustomSegmentedButtons.tsx new file mode 100644 index 0000000..fd7f295 --- /dev/null +++ b/interfaces/nativeapp/src/components/calendar/CustomSegmentedButtons.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; +import { useTheme } from 'react-native-paper'; + +interface ButtonConfig { + value: T; + label: string; + checkedColor?: string; // Keep for potential future use or consistency + style?: object; // Keep for potential future use or consistency +} + +interface CustomSegmentedButtonsProps { + value: T; + onValueChange: (value: T) => void; + buttons: ButtonConfig[]; +} + +const CustomSegmentedButtons = ({ + value, + onValueChange, + buttons, +}: CustomSegmentedButtonsProps) => { + const theme = useTheme(); + + const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + borderRadius: 20, // Increased border radius for a rounder look + overflow: 'hidden', + borderWidth: 1, + borderColor: theme.colors.outline, + }, + button: { + flex: 1, + paddingVertical: 8, + paddingHorizontal: 12, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: theme.colors.surface, // Default background + }, + buttonSelected: { + backgroundColor: theme.colors.primaryContainer, // Selected background + }, + buttonText: { + color: theme.colors.onSurface, // Default text color + }, + buttonTextSelected: { + color: theme.colors.onPrimaryContainer, // Selected text color + fontWeight: 'bold', + }, + separator: { + width: 1, + backgroundColor: theme.colors.outline, + }, + }); + + return ( + + {buttons.map((button, index) => ( + + onValueChange(button.value)} + activeOpacity={0.7} + > + + {button.label} + + + {index < buttons.length - 1 && } + + ))} + + ); +}; + +export default CustomSegmentedButtons; diff --git a/interfaces/nativeapp/src/components/calendar/EventItem.tsx b/interfaces/nativeapp/src/components/calendar/EventItem.tsx index 541446a..c8defac 100644 --- a/interfaces/nativeapp/src/components/calendar/EventItem.tsx +++ b/interfaces/nativeapp/src/components/calendar/EventItem.tsx @@ -43,12 +43,14 @@ const EventItem: React.FC = ({ event, showTime = true }) => { }, text: { color: theme.colors.onPrimary, // Ensure text is readable on the background color - fontSize: 12, - fontWeight: '500', + fontSize: 10, + fontWeight: 'bold', }, timeText: { - fontSize: 9, + fontSize: 8, fontWeight: 'normal', + color: theme.colors.onPrimary, + marginRight: 2, // Space between time and title }, tagContainer: { flexDirection: 'row', @@ -77,7 +79,7 @@ const EventItem: React.FC = ({ event, showTime = true }) => { return ( - + {showTime && {timeString} } {event.title} diff --git a/interfaces/nativeapp/src/components/calendar/ThreeDayView.tsx b/interfaces/nativeapp/src/components/calendar/ThreeDayView.tsx index 05ab704..41de0da 100644 --- a/interfaces/nativeapp/src/components/calendar/ThreeDayView.tsx +++ b/interfaces/nativeapp/src/components/calendar/ThreeDayView.tsx @@ -15,8 +15,9 @@ interface ThreeDayViewProps { } // Get screen width -const screenWidth = Dimensions.get('window').width; -const dayColumnWidth = screenWidth / 3; // Divide by 3 for 3-day view +// const screenWidth = Dimensions.get('window').width; // No longer needed here if dayColumn uses flex:1 +// const dayColumnWidth = screenWidth / 3; +const dateNumberSize = 24; // Define a size for the circle/text container const ThreeDayView: React.FC = ({ startDate, endDate, eventsByDate }) => { const theme = useTheme(); @@ -29,87 +30,132 @@ const ThreeDayView: React.FC = ({ startDate, endDate, eventsB // Ensure exactly 3 days are generated if interval logic is tricky const displayDays = days.slice(0, 3); + // Get abbreviated day names for the displayed days + const weekDays = displayDays.map(day => format(day, 'EEE').toUpperCase()); + + // --- Define styles inside the component to access theme --- const styles = StyleSheet.create({ container: { flex: 1, - flexDirection: 'row', // Apply row direction directly to the container View }, - // Remove scrollViewContent style as it's no longer needed - // scrollViewContent: { flexDirection: 'row' }, - dayColumn: { - width: dayColumnWidth, - borderRightWidth: 1, - borderRightColor: theme.colors.outlineVariant, - // Add flex: 1 to allow inner ScrollView to expand vertically + headerRow: { + flexDirection: 'row', + paddingVertical: 5, + paddingBottom: 0, + backgroundColor: theme.colors.background, + }, + headerCell: { flex: 1, + alignItems: 'center', + justifyContent: 'center', + borderLeftWidth: 0.5, + borderRightWidth: 0.5, + borderColor: theme.colors.outlineVariant, + backgroundColor: theme.colors.background, }, - lastDayColumn: { - borderRightWidth: 0, + headerText: { + fontSize: 11, + fontWeight: 'bold', + color: theme.colors.onSurfaceVariant, + }, + contentRow: { + flex: 1, + flexDirection: 'row', + }, + dayColumn: { + flex: 1, + borderWidth: 0.5, + borderTopWidth: 0, + borderColor: theme.colors.outlineVariant, + padding: 2, + paddingTop: 0, // Keep this 0 if header handles top padding + backgroundColor: theme.colors.background, + overflow: 'hidden', }, dayHeader: { - paddingVertical: 8, alignItems: 'center', - borderBottomWidth: 1, - borderBottomColor: theme.colors.outlineVariant, - backgroundColor: theme.colors.surfaceVariant, + justifyContent: 'center', + marginBottom: 2, + minHeight: dateNumberSize + 4, // Slightly larger than circle to give space }, - dayHeaderText: { - fontSize: 10, - fontWeight: 'bold', - color: theme.colors.onSurfaceVariant, + // Base container for the date number - useful for alignment consistency + dateNumberContainer: { + width: dateNumberSize, + height: dateNumberSize, + alignItems: 'center', + justifyContent: 'center', + // Removed borderRadius and background/border from here }, - dayNumberText: { + todayCircle: { + backgroundColor: theme.colors.primary, // Use background for filled circle + borderRadius: dateNumberSize / 2, // Make it circular + }, + dateNumber: { fontSize: 12, + color: theme.colors.onSurface, + textAlign: 'center', // Keep textAlign, helps sometimes + padding: 0, // Ensure no padding interferes + includeFontPadding: false, // Keep this + // Removed width property + }, + todayDateNumber: { // Specific style for text color on today's date + color: theme.colors.onPrimary, // Color that contrasts with primary background fontWeight: 'bold', - marginTop: 2, - color: theme.colors.onSurfaceVariant, - }, - todayHeader: { - backgroundColor: theme.colors.primaryContainer, - }, - todayHeaderText: { - color: theme.colors.onPrimaryContainer, }, eventsContainer: { - // Remove flex: 1 here if dayColumn has flex: 1 - padding: 4, + flex: 1, + padding: 2, }, }); + // --- End of styles --- + return ( - // Change ScrollView to View - {displayDays.map((day, index) => { - const dateKey = format(day, 'yyyy-MM-dd'); - const dayEvents = eventsByDate[dateKey] || []; - const today = isToday(day); - const isLastColumn = index === displayDays.length - 1; - - return ( - - - - {format(day, 'EEE')} - - - {format(day, 'd')} - - - {/* Keep inner ScrollView for vertical scrolling within the column */} - - {dayEvents.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()) - .map(event => ( - - ))} - + + {weekDays.map((dayName, index) => ( + + {dayName} - ); - })} + ))} + + + {displayDays.map((day) => { // Removed index as it wasn't used after checks removed + const dateKey = format(day, 'yyyy-MM-dd'); + const dayEvents = eventsByDate[dateKey] || []; + const today = isToday(day); + + return ( + // Use flex: 1 container for each day column + + + + {/* Apply base container style, and add circle style conditionally */} + + + {format(day, 'd')} + + + + + {dayEvents.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()) + .map(event => ( + + ))} + + + + ); + })} + ); }; -export default ThreeDayView; +export default ThreeDayView; \ No newline at end of file diff --git a/interfaces/nativeapp/src/components/calendar/ViewSwitcher.tsx b/interfaces/nativeapp/src/components/calendar/ViewSwitcher.tsx index d4e7393..4744fdf 100644 --- a/interfaces/nativeapp/src/components/calendar/ViewSwitcher.tsx +++ b/interfaces/nativeapp/src/components/calendar/ViewSwitcher.tsx @@ -1,8 +1,9 @@ // src/components/calendar/ViewSwitcher.tsx import React from 'react'; import { View, StyleSheet } from 'react-native'; -import { SegmentedButtons, useTheme } from 'react-native-paper'; -import { CalendarViewMode } from './CustomCalendarView'; // Import the type +import { useTheme } from 'react-native-paper'; // Keep useTheme +import { CalendarViewMode } from './CustomCalendarView'; +import CustomSegmentedButtons from './CustomSegmentedButtons'; // Import the custom component interface ViewSwitcherProps { currentView: CalendarViewMode; @@ -14,26 +15,27 @@ const ViewSwitcher: React.FC = ({ currentView, onViewChange } const styles = StyleSheet.create({ container: { paddingVertical: 8, - paddingHorizontal: 16, - backgroundColor: theme.colors.surface, // Match background - borderBottomColor: theme.colors.outlineVariant, + // Add horizontal padding if needed to center or space the component + paddingHorizontal: 8, + minWidth: 150, // Add a minimum width + alignSelf: 'center', // Center the component if it's smaller than the parent }, }); return ( - onViewChange(value as CalendarViewMode)} // Cast value + onValueChange={onViewChange} // No need to cast type here anymore buttons={[ - { value: 'month', label: 'M', checkedColor: theme.colors.onPrimary }, - { value: 'week', label: 'W', checkedColor: theme.colors.onPrimary }, - { value: '3day', label: '3', checkedColor: theme.colors.onPrimary }, + // Pass the same button configuration + { value: 'month', label: 'M' /* Use full labels for clarity */ }, + { value: 'week', label: 'W' }, + { value: '3day', label: '3D' }, ]} - density="high" /> ); }; -export default ViewSwitcher; +export default ViewSwitcher; \ No newline at end of file diff --git a/interfaces/nativeapp/src/components/calendar/WeekView.tsx b/interfaces/nativeapp/src/components/calendar/WeekView.tsx index 545ff19..81855ad 100644 --- a/interfaces/nativeapp/src/components/calendar/WeekView.tsx +++ b/interfaces/nativeapp/src/components/calendar/WeekView.tsx @@ -1,13 +1,11 @@ // src/components/calendar/WeekView.tsx import React, { useMemo } from 'react'; -// Import Dimensions -import { View, StyleSheet, ScrollView, Dimensions } from 'react-native'; +import { View, StyleSheet, ScrollView } from 'react-native'; // Removed Dimensions unless used elsewhere import { Text, useTheme } from 'react-native-paper'; import { eachDayOfInterval, format, isToday } from 'date-fns'; -import CalendarDayCell from './CalendarDayCell'; import { CalendarEvent } from '../../types/calendar'; -import EventItem from './EventItem'; // Import EventItem +import EventItem from './EventItem'; interface WeekViewProps { startDate: Date; // Start of the week @@ -15,9 +13,8 @@ interface WeekViewProps { eventsByDate: { [key: string]: CalendarEvent[] }; } -// Get screen width -const screenWidth = Dimensions.get('window').width; -const dayColumnWidth = screenWidth / 7; // Divide by 7 for week view +// Define size here, consistent with other views +const dateNumberSize = 24; const WeekView: React.FC = ({ startDate, endDate, eventsByDate }) => { const theme = useTheme(); @@ -26,87 +23,133 @@ const WeekView: React.FC = ({ startDate, endDate, eventsByDate }) endDate, ]); + // Define standard week day names - ensure order matches your locale/calendar needs + // const weekDays = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN']; // Static names + // Or generate dynamically from the days array to be safer + const weekDays = days.map(day => format(day, 'EEE').toUpperCase()); + + // --- Define styles inside the component to access theme --- const styles = StyleSheet.create({ container: { flex: 1, - flexDirection: 'row', // Apply row direction directly to the container View }, - // Remove scrollViewContent style - // scrollViewContent: { flexDirection: 'row' }, - dayColumn: { - width: dayColumnWidth, - borderRightWidth: 1, - borderRightColor: theme.colors.outlineVariant, - flex: 1, // Add flex: 1 to allow inner ScrollView to expand vertically + headerRow: { + flexDirection: 'row', + paddingVertical: 5, + paddingBottom: 0, + backgroundColor: theme.colors.background, }, - lastDayColumn: { // Add style for the last column - borderRightWidth: 0, // Remove border - }, - dayHeader: { - paddingVertical: 8, + headerCell: { + flex: 1, alignItems: 'center', - borderBottomWidth: 1, - borderBottomColor: theme.colors.outlineVariant, - backgroundColor: theme.colors.surfaceVariant, + justifyContent: 'center', + borderLeftWidth: 0.5, + borderRightWidth: 0.5, + borderColor: theme.colors.outlineVariant, + backgroundColor: theme.colors.background, }, - dayHeaderText: { - fontSize: 10, + headerText: { + fontSize: 11, fontWeight: 'bold', color: theme.colors.onSurfaceVariant, }, - dayNumberText: { + contentRow: { + flex: 1, + flexDirection: 'row', + }, + dayColumn: { + flex: 1, + borderWidth: 0.5, + borderTopWidth: 0, + borderColor: theme.colors.outlineVariant, + padding: 2, + paddingTop: 0, // Keep this 0 if header handles top padding + backgroundColor: theme.colors.background, + overflow: 'hidden', + }, + dayHeader: { // Consistent name + alignItems: 'center', + justifyContent: 'center', + marginBottom: 2, + // Use minHeight for consistency + minHeight: dateNumberSize + 4, + }, + dateNumberContainer: { // Base container for the date number + width: dateNumberSize, + height: dateNumberSize, + alignItems: 'center', + justifyContent: 'center', + }, + todayCircle: { // Style for the *filled* circle highlight + backgroundColor: theme.colors.primary, + borderRadius: dateNumberSize / 2, + }, + dateNumber: { // Base style for the date number text fontSize: 12, - fontWeight: 'bold', - marginTop: 2, - color: theme.colors.onSurfaceVariant, + color: theme.colors.onSurface, // Default text color + textAlign: 'center', + padding: 0, + includeFontPadding: false, + fontWeight: 'normal', // Base weight }, - todayHeader: { - backgroundColor: theme.colors.primaryContainer, - }, - todayHeaderText: { - color: theme.colors.onPrimaryContainer, + todayDateNumber: { // Specific style for text on today's date + color: theme.colors.onPrimary, // Contrast with primary background + fontWeight: 'bold', // Make today bold }, eventsContainer: { - // Remove flex: 1 here if dayColumn has flex: 1 - padding: 4, + flex: 1, + padding: 2, }, }); + // --- End of styles --- return ( - // Change ScrollView to View - {days.map((day, index) => { // Add index to map - const dateKey = format(day, 'yyyy-MM-dd'); - const dayEvents = eventsByDate[dateKey] || []; - const today = isToday(day); - const isLastColumn = index === days.length - 1; // Check if it's the last column - - return ( - - - - {format(day, 'EEE')} - - - {format(day, 'd')} - - - {/* Keep inner ScrollView for vertical scrolling within the column */} - - {dayEvents.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()) - .map(event => ( - - ))} - + + {weekDays.map((dayName, index) => ( // Use generated weekdays map + + {dayName} - ); - })} + ))} + + + {days.map((day) => { // Removed unused index + const dateKey = format(day, 'yyyy-MM-dd'); + const dayEvents = eventsByDate[dateKey] || []; + const today = isToday(day); + + // Removed inline dateNumberStyle object creation + + return ( + + + + {/* Apply base container style, and add circle style conditionally */} + + + {format(day, 'd')} + + + + + {dayEvents.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()) + .map(event => ( + // Show time in week view + ))} + + + + ); + })} + ); }; -export default WeekView; +export default WeekView; \ No newline at end of file diff --git a/interfaces/nativeapp/src/navigation/AppNavigator.tsx b/interfaces/nativeapp/src/navigation/AppNavigator.tsx new file mode 100644 index 0000000..ba8fd9e --- /dev/null +++ b/interfaces/nativeapp/src/navigation/AppNavigator.tsx @@ -0,0 +1,45 @@ +// src/navigation/AppNavigator.tsx +import React from 'react'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { useTheme } from 'react-native-paper'; + +import MobileTabNavigator from './MobileTabNavigator'; +import EventFormScreen from '../screens/EventFormScreen'; +import { AppStackParamList } from '../types/navigation'; + +const Stack = createNativeStackNavigator(); + +const AppNavigator = () => { + const theme = useTheme(); + + return ( + + + + + ); +}; + +export default AppNavigator; diff --git a/interfaces/nativeapp/src/navigation/MobileTabNavigator.tsx b/interfaces/nativeapp/src/navigation/MobileTabNavigator.tsx index 8256791..d11c709 100644 --- a/interfaces/nativeapp/src/navigation/MobileTabNavigator.tsx +++ b/interfaces/nativeapp/src/navigation/MobileTabNavigator.tsx @@ -8,6 +8,7 @@ import DashboardScreen from '../screens/DashboardScreen'; import ChatScreen from '../screens/ChatScreen'; import CalendarScreen from '../screens/CalendarScreen'; import ProfileScreen from '../screens/ProfileScreen'; +import EventFormScreen from '../screens/EventFormScreen'; import { MobileTabParamList } from '../types/navigation'; diff --git a/interfaces/nativeapp/src/navigation/RootNavigator.tsx b/interfaces/nativeapp/src/navigation/RootNavigator.tsx index f551a38..58491d4 100644 --- a/interfaces/nativeapp/src/navigation/RootNavigator.tsx +++ b/interfaces/nativeapp/src/navigation/RootNavigator.tsx @@ -5,7 +5,7 @@ import { Platform } from 'react-native'; import { useAuth, AuthLoadingScreen } from '../contexts/AuthContext'; import AuthNavigator from './AuthNavigator'; // Unauthenticated flow -import MobileTabNavigator from './MobileTabNavigator'; // Authenticated Mobile flow +import AppNavigator from './AppNavigator'; // Import the new App stack navigator import WebAppLayout from './WebAppLayout'; // Authenticated Web flow import { RootStackParamList } from '../types/navigation'; @@ -25,7 +25,8 @@ const RootNavigator = () => { {isAuthenticated ? ( // User is logged in: Choose main app layout based on platform - {() => Platform.OS === 'web' ? : } + {/* Use AppNavigator for mobile, WebAppLayout for web */} + {() => Platform.OS === 'web' ? : } ) : ( // User is not logged in: Show authentication flow diff --git a/interfaces/nativeapp/src/screens/CalendarScreen.tsx b/interfaces/nativeapp/src/screens/CalendarScreen.tsx index 548aaf4..317fec1 100644 --- a/interfaces/nativeapp/src/screens/CalendarScreen.tsx +++ b/interfaces/nativeapp/src/screens/CalendarScreen.tsx @@ -1,14 +1,14 @@ // src/screens/CalendarScreen.tsx import React from 'react'; -import { View, StyleSheet } from 'react-native'; +// Import SafeAreaView +import { StyleSheet, View, SafeAreaView } from 'react-native'; import { useTheme, FAB } from 'react-native-paper'; import { useNavigation } from '@react-navigation/native'; import { StackNavigationProp } from '@react-navigation/stack'; -import CustomCalendarView from '../components/calendar/CustomCalendarView'; // Import the new custom view +import CustomCalendarView from '../components/calendar/CustomCalendarView'; import { AppStackParamList } from '../navigation/AppNavigator'; -// Define navigation prop type type CalendarScreenNavigationProp = StackNavigationProp; const CalendarScreen = () => { @@ -16,29 +16,45 @@ const CalendarScreen = () => { const navigation = useNavigation(); const styles = StyleSheet.create({ - container: { flex: 1, backgroundColor: theme.colors.background }, + // Style for SafeAreaView to ensure it fills the screen + safeArea: { + flex: 1, + backgroundColor: theme.colors.background, // Apply background here + }, + // Container inside SafeAreaView might not need flex: 1 anymore, + // but keep it to ensure CustomCalendarView fills the safe area + container: { + flex: 1, + position: 'relative' // Often added for absolute children, though View defaults to relative + }, fab: { position: 'absolute', - margin: 16, - right: 0, - bottom: 0, + // Change from margin: 16, right: 0, bottom: 0 + // Explicitly set distance from the bottom/right edges of the SAFE AREA + right: 16, + bottom: 16, // Adjust this value if needed (e.g., 20 or 24) for more padding backgroundColor: theme.colors.primary, + zIndex: 10, }, }); return ( - - {/* Replace the old Calendar and FlatList with the new CustomCalendarView */} - + // Use SafeAreaView as the outermost component + + {/* Keep this inner View for structure if needed, or potentially remove */} + {/* if CustomCalendarView handles its own layout fully */} + + - {/* Keep the FAB for creating new events */} - navigation.navigate('EventForm')} // Navigate without eventId for creation - color={theme.colors.onPrimary || '#ffffff'} // Ensure icon color contrasts - /> - + {/* FAB is now positioned relative to the SafeAreaView's padded area */} + navigation.navigate('EventForm')} + color={theme.colors.onPrimary || '#ffffff'} + /> + + ); }; diff --git a/interfaces/nativeapp/src/screens/ChatScreen.tsx b/interfaces/nativeapp/src/screens/ChatScreen.tsx index 4a939ae..ff1982b 100644 --- a/interfaces/nativeapp/src/screens/ChatScreen.tsx +++ b/interfaces/nativeapp/src/screens/ChatScreen.tsx @@ -1,5 +1,5 @@ // src/screens/ChatScreen.tsx -import React, { useState, useCallback, useRef } from 'react'; +import React, { useState, useCallback, useRef, useEffect } from 'react'; import { View, StyleSheet, FlatList, KeyboardAvoidingView, Platform, TextInput as RNTextInput, NativeSyntheticEvent, TextInputKeyPressEventData } from 'react-native'; import { Text, useTheme, TextInput, Button, IconButton, PaperProvider } from 'react-native-paper'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -13,25 +13,68 @@ interface Message { timestamp: Date; } -// Define the expected structure for the API response +// Define the expected structure for the API response from /nlp/process-command interface NlpResponse { - responses: string[]; // Expecting an array of response strings + responses: string[]; +} + +// Define the expected structure for the API response from /nlp/history +interface ChatHistoryResponse { + id: number; + sender: 'user' | 'ai'; + text: string; + timestamp: string; // Backend sends ISO string } const ChatScreen = () => { const theme = useTheme(); const [messages, setMessages] = useState([]); const [inputText, setInputText] = useState(''); - const [isLoading, setIsLoading] = useState(false); // To show activity indicator while AI responds + const [isLoading, setIsLoading] = useState(false); + const [isHistoryLoading, setIsHistoryLoading] = useState(true); // Add state for history loading const flatListRef = useRef(null); + // --- Load messages from backend API on mount --- + useEffect(() => { + const loadHistory = async () => { + setIsHistoryLoading(true); + try { + console.log("[ChatScreen] Fetching chat history from /nlp/history"); + const response = await apiClient.get('/nlp/history'); + console.log("[ChatScreen] Received history:", response.data); + + if (response.data && Array.isArray(response.data)) { + // Map backend response to frontend Message format + const historyMessages = response.data.map((msg) => ({ + id: msg.id.toString(), // Convert backend ID to string for keyExtractor + text: msg.text, + sender: msg.sender, + timestamp: new Date(msg.timestamp), // Convert ISO string to Date + })); + setMessages(historyMessages); + } else { + console.warn("[ChatScreen] Received invalid history data:", response.data); + setMessages([]); // Set to empty array if data is invalid + } + } catch (error: any) { + console.error("Failed to load chat history from backend:", error.response?.data || error.message || error); + // Optionally, show an error message to the user + // For now, just start with an empty chat + setMessages([]); + } finally { + setIsHistoryLoading(false); + } + }; + loadHistory(); + }, []); // Empty dependency array ensures this runs only once on mount + // Function to handle sending a message const handleSend = useCallback(async () => { const trimmedText = inputText.trim(); - if (!trimmedText) return; // Don't send empty messages + if (!trimmedText || isLoading) return; // Prevent sending while loading const userMessage: Message = { - id: Date.now().toString() + '-user', + id: Date.now().toString() + '-user', // Temporary frontend ID text: trimmedText, sender: 'user', timestamp: new Date(), @@ -39,6 +82,7 @@ const ChatScreen = () => { // Add user message optimistically setMessages(prevMessages => [...prevMessages, userMessage]); + setInputText(''); setIsLoading(true); @@ -48,7 +92,6 @@ const ChatScreen = () => { // --- Call Backend API --- try { console.log(`[ChatScreen] Sending to /nlp/process-command: ${trimmedText}`); - // Expect the backend to return an object with a 'responses' array const response = await apiClient.post('/nlp/process-command', { user_input: trimmedText }); console.log("[ChatScreen] Received response:", response.data); @@ -56,14 +99,13 @@ const ChatScreen = () => { if (response.data && Array.isArray(response.data.responses) && response.data.responses.length > 0) { response.data.responses.forEach((responseText, index) => { aiResponses.push({ - id: `${Date.now()}-ai-${index}`, // Ensure unique IDs - text: responseText || "...", // Handle potential empty strings + id: `${Date.now()}-ai-${index}`, // Temporary frontend ID + text: responseText || "...", sender: 'ai', timestamp: new Date(), }); }); } else { - // Handle cases where the response format is unexpected or empty console.warn("[ChatScreen] Received invalid or empty responses array:", response.data); aiResponses.push({ id: Date.now().toString() + '-ai-fallback', @@ -92,7 +134,7 @@ const ChatScreen = () => { } // --- End API Call --- - }, [inputText]); // Keep inputText as dependency + }, [inputText, isLoading, messages]); // Add isLoading and messages to dependency array const handleKeyPress = (e: NativeSyntheticEvent) => { if (e.nativeEvent.key === 'Enter' && !(e.nativeEvent as any).shiftKey) { @@ -118,29 +160,44 @@ const ChatScreen = () => { }; const styles = StyleSheet.create({ - container: { + container: { // For SafeAreaView flex: 1, backgroundColor: theme.colors.background, }, - listContainer: { + keyboardAvoidingContainer: { // Style for KAV + flex: 1, + }, + listContainer: { // Container for the list, should take up available space flex: 1, }, - messageList: { + messageList: { // Padding for the list content itself paddingHorizontal: 10, paddingVertical: 10, }, - inputContainer: { + inputContainer: { // Input container should stick to the bottom flexDirection: 'row', - alignItems: 'center', - padding: 8, + alignItems: 'center', // Align items vertically in the center + paddingHorizontal: 8, // Add horizontal padding + paddingVertical: 8, // Add some vertical padding borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: theme.colors.outlineVariant, - backgroundColor: theme.colors.elevation.level2, // Slightly elevated background + backgroundColor: theme.colors.background, // Or theme.colors.surface }, textInput: { - flex: 1, + flex: 1, // Take available horizontal space marginRight: 8, - backgroundColor: theme.colors.surface, // Use surface color for input background + backgroundColor: theme.colors.surface, + paddingTop: 10, // Keep the vertical alignment fix for placeholder + paddingHorizontal: 10, + // Add some vertical padding inside the input itself + paddingVertical: Platform.OS === 'ios' ? 10 : 5, // Adjust padding for different platforms if needed + maxHeight: 100, // Optional: prevent input from getting too tall with multiline + }, + sendButton: { + marginVertical: 4, // Adjust vertical alignment if needed + // Ensure button doesn't shrink + height: 40, // Match TextInput height approx. + justifyContent: 'center', }, messageBubble: { maxWidth: '80%', @@ -166,47 +223,66 @@ const ChatScreen = () => { } }); + // Optionally, show a loading indicator while history loads + if (isHistoryLoading) { + return ( + + + Loading chat history... + + + ); + } + return ( - - item.id} - contentContainerStyle={styles.messageList} - onContentSizeChange={() => flatListRef.current?.scrollToEnd({ animated: false })} // Scroll on initial load/size change - onLayout={() => flatListRef.current?.scrollToEnd({ animated: false })} // Scroll on layout change - /> - + {/* List container takes available space */} + + item.id} + contentContainerStyle={styles.messageList} // Padding inside the scrollable content + onContentSizeChange={() => flatListRef.current?.scrollToEnd({ animated: false })} + onLayout={() => flatListRef.current?.scrollToEnd({ animated: false })} + /> + - - - - + {/* Input container is last, outside the list's flex */} + + + + ); diff --git a/interfaces/nativeapp/src/types/navigation.ts b/interfaces/nativeapp/src/types/navigation.ts index de5bd83..7213f67 100644 --- a/interfaces/nativeapp/src/types/navigation.ts +++ b/interfaces/nativeapp/src/types/navigation.ts @@ -15,7 +15,7 @@ export type WebContentStackParamList = { Chat: undefined; Calendar: undefined; Profile: undefined; - EventForm?: { eventId?: number; selectedDate?: string }; // Add EventForm with optional params + EventForm?: { eventId?: number; selectedDate?: string }; }; // Screens managed by the Root Navigator (Auth vs App) @@ -30,5 +30,11 @@ export type AuthStackParamList = { // Example: SignUp: undefined; ForgotPassword: undefined; }; +// Screens within the main App stack (Mobile) +export type AppStackParamList = { + MainTabs: undefined; // Represents the MobileTabNavigator + EventForm: { eventId?: number; selectedDate?: string }; +}; + // Type for the ref used in WebAppLayout export type WebContentNavigationProp = NavigationContainerRef; \ No newline at end of file