From 8d884111fd8d0451e65b4a387d3106468ccef92d Mon Sep 17 00:00:00 2001 From: c-d-p Date: Fri, 18 Apr 2025 17:30:09 +0200 Subject: [PATCH] [V0.1 WORKING] Added chat, profile, & calendar screen implementations. --- backend/__pycache__/main.cpython-312.pyc | Bin 1879 -> 1766 bytes backend/main.py | 2 +- .../auth/__pycache__/security.cpython-312.pyc | Bin 8548 -> 8548 bytes .../__pycache__/schemas.cpython-312.pyc | Bin 1053 -> 1454 bytes backend/modules/calendar/schemas.py | 9 +- .../nlp/__pycache__/api.cpython-312.pyc | Bin 2399 -> 3415 bytes .../nlp/__pycache__/schemas.cpython-312.pyc | Bin 0 -> 430 bytes .../nlp/__pycache__/service.cpython-312.pyc | Bin 3051 -> 5014 bytes backend/modules/nlp/api.py | 93 +++++---- backend/modules/nlp/schemas.py | 5 + backend/modules/nlp/service.py | 101 +++++---- interfaces/nativeapp/src/api/auth.ts | 12 -- interfaces/nativeapp/src/api/calendar.ts | 1 + interfaces/nativeapp/src/api/client.ts | 61 ++++-- .../nativeapp/src/contexts/AuthContext.tsx | 3 +- .../nativeapp/src/screens/CalendarScreen.tsx | 181 ++++++++++++---- .../nativeapp/src/screens/ChatScreen.tsx | 195 +++++++++++++++++- .../nativeapp/src/screens/ProfileScreen.tsx | 123 ++++++++++- interfaces/nativeapp/src/types/axios.d.ts | 8 + 19 files changed, 613 insertions(+), 181 deletions(-) create mode 100644 backend/modules/nlp/__pycache__/schemas.cpython-312.pyc create mode 100644 backend/modules/nlp/schemas.py delete mode 100644 interfaces/nativeapp/src/api/auth.ts create mode 100644 interfaces/nativeapp/src/types/axios.d.ts diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc index 5a63223e405b88735b7dbb54c8dffcac6f856d24..58a6b67a8f2f39aee65ae2ff7b3dbceedf993ae5 100644 GIT binary patch delta 265 zcmcc4_l%eKG%qg~0}!ZvV$2BH$UC2jF>Uf%rpU=*%rc5BMchD{TO7HmC5b7CC5gAV zl8aIkOH$(#b8?D6x+c$MPEAw-GF~wJXkhrl#=yzj&)dm+okQUwhr*Jq#W@!^6s~X> z-{s)_SSht5{4J#V4<4Su6pvt4I<=fEBZ4q$cK+WE3e+&SI5fl$_kkx|&S_Br7)Aj7?8o c8N@`@@{7YJH$SB`C)KXVaB?%-1s0HD0MqtFng9R* delta 332 zcmaFHd!3K>G%qg~0}!l|_y zIpmgjE%v#(^b diff --git a/backend/main.py b/backend/main.py index 02282a8..c9c4fe0 100644 --- a/backend/main.py +++ b/backend/main.py @@ -14,7 +14,7 @@ def lifespan_factory() -> Callable[[FastAPI], _AsyncGeneratorContextManager[Any] @asynccontextmanager async def lifespan(app: FastAPI): - Base.metadata.drop_all(bind=get_engine()) + # Base.metadata.drop_all(bind=get_engine()) Base.metadata.create_all(bind=get_engine()) yield diff --git a/backend/modules/auth/__pycache__/security.cpython-312.pyc b/backend/modules/auth/__pycache__/security.cpython-312.pyc index f562ca5ceb6246f71046e7eb7fb9a02c72960fa6..b1bd1873551930e67669411c8a8676f99361aab1 100644 GIT binary patch delta 20 acmaFj^u&q#G%qg~0}$jTGHv9JR0IG*ss&B} delta 20 acmaFj^u&q#G%qg~0}!||Fl^+GR0IG&LIlGA diff --git a/backend/modules/calendar/__pycache__/schemas.cpython-312.pyc b/backend/modules/calendar/__pycache__/schemas.cpython-312.pyc index 694dcc21ccbdd194af2c308b0049c6898b0f7327..238ddf87f86cb4358fb5e48060a0c7cbe1cfbfd2 100644 GIT binary patch delta 445 zcmbQsv5uSXG%qg~0}wc7GGzoYP2`hcjGL(LDaFL#&XB_1!jQs|%Cwppq=11TinWqc zlWSv%1#^8eLHcQy8L{LK#vRqnJ}!QW&e)85mMoQ{uTkPReQ$*9SGOV~LvCp9l6vBGo zh$#mnK)hlPAkoTjLqev(wZnLVNT=0h7MUAzN)6r@Xb#;U~im4TH}zDNit002VF4jupi diff --git a/backend/modules/calendar/schemas.py b/backend/modules/calendar/schemas.py index 00b6755..01e495e 100644 --- a/backend/modules/calendar/schemas.py +++ b/backend/modules/calendar/schemas.py @@ -14,4 +14,11 @@ class CalendarEventResponse(CalendarEventCreate): user_id: int class Config: - from_attributes = True \ No newline at end of file + from_attributes = True + +class CalendarEventUpdate(BaseModel): + title: str | None = None + description: str | None = None + start: datetime | None = None + end: datetime | None = None + location: str | None = None \ No newline at end of file diff --git a/backend/modules/nlp/__pycache__/api.cpython-312.pyc b/backend/modules/nlp/__pycache__/api.cpython-312.pyc index bfb7551ab7a5d2346fb4e9708c36194fe43985c7..78a6a0a02dfd2b9f65115e3b278b99190acec960 100644 GIT binary patch literal 3415 zcmaJ@O>7&-6`tMYuP9QaC{d&+$1B;fBv+woB>t&t#F1^Oc2XIJe^P{SORRT>*2=pi zJ-dv4KpJXLv^^EN#X%BOhaObe2j-L>ouV!Z^s=-BR9(=pi_|EJUKG2B!s(%JW=V-s z;tsGgZ{EE5-rG0ty_r9U!yO3P={a#W5k}}sHfgm$lX(7LAU;GGVL?ZRkQEFuD;iQ( z5*Sa^J%%^y<+P;x41dslDHf%()5kB|pT}Ct;<#a%g z8Qs}#Bc6>LJ=q?kH`~kkK|NvgW&4a|HYp$x5dnAL&<){U=UvgYF`FW(xuY$^cSan( zE4VY*C6eH-nZGs5i#S4}xQm2vlz8(#9Q&E~ZWAP%UhE!?|C7x|B_}v}@%*J?*&@{O zpCu(yz@`&Ecjd~(sRfmkEUj1=6&(L%Vwx`LyFsiRz7EVNqtr5`q+sRBCZWLdTxFx^ zbd_jPg*`b+Zk35?IX=a_nNu_n>AmPKohcfIQoxs7fl-gst5Tv^B&RAmSfS7yc^{CS zI9IcaXF9#*68;w1UQBfI4G6F&ex?a9#W2rsMSGZ6IpUIMyr7pH2|C#3D^Zfy795YI z+%T7--P5X)wY{ez zwNA6@sd%uE0@pd?QDD_-omIf4)kTizBR_E!A&e648uKmZv(m+|00#jgpM z5paULIYHnpKqPW5#IMC{<_*81p^EP}!mosC0QLvievk}t$$cOhY)MwciZI7Bu*Gr5 zyts`JtoSQI-1&*fF~S^U1TY|5-PWe9!|G~rUxr-ixZCtubvJT^Bflen<#VwX%Fc+p zaJ1#0YN!%g-sL?UYxkZNZ|!Tdt<&mlabId%sM2|_`>w~8Yqt-_xpk+&w!Rk1&WO9V z&;Fa1_3W~Yg#d3ax1rrXxVI9*iBCk=|3PG>Tl;Ze#rrYB$xpZA4$fpJo>I8KUGD*8 zHUA2sgNXX8;Y#=e0Y|Ih+u?b^8fc-y+JS1MO-q_<8K^`)MxT0Gf4N7~zIIK38Mz&q z7v|A|^d6cQM)xmW>yV)}3yEya5}DN>xtW$@GFc!hlr+AgsH}*M$xv%#N~|(1$O;#K zEl)KlH2Na2%~G*ok_?A+d|JU`mB&|7sA51lY^~=kvS2x0LTQm|D^P}^M1)LBvC3vn zEn?#MFtHR(cY0J^p;}&36{b5^AOsWa1X?(bzg)OkD9#sX2;4*?(2a_85C+HZBA8CM zg7KbGNLea40bUk!8osb3^J*raKPzh{{KT!YMhTYlMJl`HF$3kc!xd)*ibi4o|Jmdg z&&(D0D=>oY*Gw!oD=R4Bht@pF%u;fSIlFnFY;{|& zG&~_MoxWV8ba6~RFOz~=EW^1m4=9Bu{QRt@Lln?Oj$4#Vgo1YrP@!0|8yK12qR}p= zgT+ryE0oF>WyOoKxZgP)N7OK7C4{mX&CYXBY?{8ymc@BA( zgw06LqgbjjFjOBHTYVS0mEdM_X!X3^m$9Rnr?Eqg)Y#gk+Q8@gKigkVy>ovWx)tB% z!6U!R*ZZdI=+x8LtBvG$>dB*PdFWQWjZ|ih)=qwY>a$b#r`A(%?-3r^Ob`Bg;-iVx zx!S??^zoIzW;|`j4>bCRYiI1Yu5a|Kc8}WF_ucxwiP|yfRyx`N&)M&(8!5aC^UK%{ z=6d?bOAyhnCc9wxgD@$=@W8W=^@G9b zwDjPxI5j3c80%#8iC3l*(jOCEpf8LLIR3l>WuT-{c0p5iU+~0oBIYe!(V=D-iy6pA z>Vp+0tQINButIfRfox=N4=2?O)r?ZMW;2+-Pa)GaVp8^&cjCL&VM271vbV_23Iuru z;NI6%;-)sMxqFtGC5lNq*-Ekn*?Na=?`FBri#1PsH{-uWx(UY11uGJe#qRssQ!1Jk zPb>`_7>?@9O+UaD3VKMDYJ2{nNA^-oGx$f4y+kemB2<;YNM__7*~K siPIuzT@ZdCeucg#ED83|@hwi=x7^V-tY@6@TtedDS0WRlr@@r}0R{G|_5c6? literal 2399 zcma)7O>7fK6rNq%Ysc|VVrN6ssHyo8iJ{n~0@?y94HQzKK@`fNt4i)>XOe8PyEe0H zej-OuRH>XQZgA)!s(LUA5*(3w$q}g+TjoGVi9kY8)f+UNP!D}G>xG2S(vf!Ny_xsE zH}mFwv%e*iaRlw>VKn(`457zd2^#34tvrL_I>HD`7P2KxvSm${xGq~EJFJC;9I_&| zqA5ZSTTwfv#e^KOI_$U>7qVh?+6gTo$Wpm#?ES4yGQF0 z^BtCI_iDWok`a+`9CuDgw-WQRzcpb{|u&}33xHDbF_C9wQW!u&r%;NkYCDW-?J(gH~I40vUBh*eN9~AhwzVbKR z%ys0o?+2k83hrYzgr!>O9HJ@jg=Y=s`oJx)xdIkg<}(*09GV_Nh(eus5ttAS_nHVS z!rbB)=|^ccj3c!$R(_ICbC0O-$bm<+?SN`>O_~a7J(z@XTwaG1TZPmikhTL-XW+Ch zcEn2tYyTFd7MX9K6?Ph5h2JUgcf0{V{U&^zs6}oi=fl1o#7M1*krpxDff)Y-XVpHt z0u0|Iv$iS<@W#ut5e-}S*LCSncPa*aS5+u-;qTz z@pRL2DZd!jx0Y0#ejYV}V3=iej9=wk?^^Sc&rD2HfJOO>1eA&kdX|8OI(yaLXD6mS z+3Oa4j$#(AI@3=1yu;FMq0~-`c?~Et({sk5#3)ZV<_y6>#vrcZAM7T|$0&c&DX(vl z691^OiD?&~O7M>FIIqwiE~~ujg^oo7Rx6OXs^vj~`Mcn9>hcTA<(?OclVzI}4O}T2 zWlV~r!$*gUW4bX7Z&T4O!eEG1oq3I65>W0q+4x-EGSsbMYZcMdO z`O71ZqN!_x&1m0ZYV+JkeWbZ%S1Yx<5!?MJ-hFMT8Sh_8s*7rWQ+@x&#N1?apa{~{ zBM%0)-&LB~1C7Li<>aPCb*P@He{;KZv(!{iEky6dnxB3N61V$!VZ50gZ6rpQlN%R% z2Ae(m>w|ZOn%nk)boKC}x~={}y>|Q3%}WcBmU@JH>{-~;%pPkbjx8s*{I`d6kK9+k zTIaJ0Q);OP*Q`HI_C6RW-W^%^yt#k0nLW`+oOqB-EoKJmp~kLb_cO;EiQ_AfD0Cv< z&!XeH>p@B>e=8^_GfTN=E!{FEiG8t9rZ(j-AWIr$N(#LA#&lp5zc`i_b1(ixuBZ&w z*S@TRUiBsmSp3t7W0=IH{4HeJHS3@dAWg@b6}aXC1UY9K#4kBPz_$X&fGjU;MCEbb z`GgTHc#SnblJ1IPWeW9wy5UOM^;pRDs3;&h$WaQsdk?zjvHgC~Pv|~S`JctDLH8si zNzxx^>k`_ujB-n8=Q7%}gtk0HA3j90Ei}7?K6g<&hNqq5r%7 zM)KX3l>2?}aO2>q*51=iW2!NIwq?+#XtVT%G#7dz#yh+GvGMh{e~F2e5Rx}OmAQ@B F-9P7ZnTG%X diff --git a/backend/modules/nlp/__pycache__/schemas.cpython-312.pyc b/backend/modules/nlp/__pycache__/schemas.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5b4a3e80d1d02c57f20e0e6fa699b2c2f6fe179c GIT binary patch literal 430 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 n%`}NLD>OS|2ObP5OFI{7Jq7ia%}8*r?< delta 867 zcmYjP%}*0S6rW`alm*&uD+NT#SO~UF+W?Y(w%sx_OQ8t~ zdh;a7BnG_b%@9w@%@7mLnn>i<@E>rn2nQt|oNXcTCHv;RH^2AW_c5=p9kn{XHa9Ck z$kGVN>^eR=ijqyT0l>=*eSBBypNuL>Vi{>Vda7fXHgid)895qL`lArh#Ra4V2wpNV zWidz@2}a>~8nO(AT8`lygGSY>V)z%;I|Tzcx)OztGKfKqR28c32DYK^%P7ZT^C3Jzo;M#Q7#43{CQ2gDhjF4>P8Y-W3$jR>9m%_ ze!;QEzcu`{{SdR~d&;|^InWK5!&-$UJ%BVxfH|uk04e(iscOHF3SdQ^2L&nEP>l1b zra1rJw05=FIi6cax|XUoMJoMLu`*(0VPZUJ=Xaf>-O59xn|PEEgKXCjyI?HYFQBwW zxDfK4=E3fgQ*@oglrEfamvo5Bi1=t;sC95^=dvp}>vJjPdfbyvDS=gnz>I)=3;XLMX_dV$n-*9`!tn893o?}VQ z8HJK;vLx|{80oZ>)k!5~8;~x`wz$shH?u~{)N!2j3fG!7-QvgYK&@Bx9fx_Br_CMl ss_}AI8nGQmT2n+a diff --git a/backend/modules/nlp/api.py b/backend/modules/nlp/api.py index b495008..34fbb43 100644 --- a/backend/modules/nlp/api.py +++ b/backend/modules/nlp/api.py @@ -1,53 +1,74 @@ # modules/nlp/api.py -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from core.database import get_db -from core.exceptions import bad_request_exception from modules.auth.dependencies import get_current_user from modules.auth.models import User from modules.nlp.service import process_request, ask_ai +from modules.nlp.schemas import ProcessCommandRequest from modules.calendar.service import create_calendar_event, get_calendar_events, update_calendar_event, delete_calendar_event -from modules.calendar.schemas import CalendarEventCreate - +from modules.calendar.schemas import CalendarEventCreate, CalendarEventUpdate router = APIRouter(prefix="/nlp", tags=["nlp"]) @router.post("/process-command") -def process_command(user_input: str, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): +def process_command(request_data: ProcessCommandRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): """ - Process the user command and return the appropriate action. + Process the user command, execute the action, and return a user-friendly response. """ - command = process_request(user_input) - - if "error" in command: - raise bad_request_exception(command["error"]) - - match command["intent"]: - case "ask_ai": - result = ask_ai(**command["params"]) - return {"action": "ai_response", "details": result} - - case "get_calendar_events": - result = get_calendar_events(db, current_user.id, **command["params"]) - return {"action": "calendar_events_retrieved", "details": result} - - case "add_calendar_event": - event = CalendarEventCreate(**command["params"]) - result = create_calendar_event(db, current_user.id, event) - return {"action": "calendar_event_created", "details": result} + user_input = request_data.user_input + command_data = process_request(user_input) + intent = command_data["intent"] + params = command_data["params"] + response_text = command_data["response_text"] - case "update_calendar_event": - event = CalendarEventCreate(**command["params"]) - result = update_calendar_event(db, current_user.id, 0, event_data=event) ## PLACEHOLDER - return {"action": "calendar_event_updated", "details": result} - - case "delete_calendar_event": - result = update_calendar_event(db, current_user.id, 0) ## PLACEHOLDER - return {"action": "calendar_event_deleted", "details": result} + if intent == "error": + raise HTTPException(status_code=400, detail=response_text) + + if intent == "clarification_needed": + return {"response": response_text} + + if intent == "unknown": + return {"response": response_text} + + try: + match intent: + case "ask_ai": + ai_answer = ask_ai(**params) + return {"response": ai_answer} - case "unknown": - return {"action": "unknown_command", "details": command["params"]} - case _: - raise bad_request_exception(400, detail="Unrecognized command") \ No newline at end of file + case "get_calendar_events": + result = get_calendar_events(db, current_user.id, **params) + return {"response": response_text, "details": result} + + case "add_calendar_event": + event = CalendarEventCreate(**params) + result = create_calendar_event(db, current_user.id, event) + return {"response": response_text, "details": result} + + 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.") + event_data = CalendarEventUpdate(**params) + result = update_calendar_event(db, current_user.id, event_id, event_data=event_data) + return {"response": response_text, "details": result} + + 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.") + result = delete_calendar_event(db, current_user.id, event_id) + return {"response": response_text, "details": {"deleted": True, "event_id": event_id}} + + case _: + print(f"Warning: Unhandled intent '{intent}' reached api.py match statement.") + raise HTTPException(status_code=500, detail="An unexpected error occurred processing the command.") + + except HTTPException as http_exc: + raise http_exc + except Exception as e: + print(f"Error executing intent '{intent}': {e}") + raise HTTPException(status_code=500, detail="Sorry, I encountered an error while trying to perform that action.") \ No newline at end of file diff --git a/backend/modules/nlp/schemas.py b/backend/modules/nlp/schemas.py new file mode 100644 index 0000000..f0de734 --- /dev/null +++ b/backend/modules/nlp/schemas.py @@ -0,0 +1,5 @@ +# modules/nlp/schemas.py +from pydantic import BaseModel + +class ProcessCommandRequest(BaseModel): + user_input: str diff --git a/backend/modules/nlp/service.py b/backend/modules/nlp/service.py index 24c825d..5d92cd2 100644 --- a/backend/modules/nlp/service.py +++ b/backend/modules/nlp/service.py @@ -10,25 +10,53 @@ client = genai.Client(api_key="AIzaSyBrte_mETZJce8qE6cRTSz_fHOjdjlShBk") ### Base prompt for MAIA, used for inital user requests SYSTEM_PROMPT = """ -You are MAIA - My AI Assistant. Your job is to parse user requests into structured JSON commands. +You are MAIA - My AI Assistant. Your job is to parse user requests into structured JSON commands and generate a user-facing response text. -Available functions: -1. ask_ai(request: str). If the intent of the request is a simple question (e.x. What is the weather like today?), you should call this function, and forward the user's request as the parameter. -2. get_calendar_events(start: Optional[datetime], end: Optional[datetime]) -3. add_calendar_event(title: str, description: str, start: datetime, end: Optional[datetime], location: str) -4. update_calendar_event(event_id: int, title: Optional[str], description: Optional[str], start: Optional[datetime], end: Optional[datetime], location: Optional[str]) -5. delete_calendar_event(event_id: int) +Available functions/intents: +1. ask_ai(request: str): Use for simple questions (e.g., weather, facts). Forward the user's request. +2. get_calendar_events(start: Optional[datetime], end: Optional[datetime]): Retrieve calendar events. +3. add_calendar_event(title: str, description: str, start: datetime, end: Optional[datetime], location: str): Add a new event. +4. update_calendar_event(event_id: int, title: Optional[str], description: Optional[str], start: Optional[datetime], end: Optional[datetime], location: Optional[str]): Update an existing event. Requires event_id. +5. delete_calendar_event(event_id: int): Delete an event. Requires event_id. +6. clarification_needed(request: str): Use this if the user's request is ambiguous or lacks necessary information (like event_id for update/delete). The original user request should be passed in the 'request' parameter. -Respond **ONLY** with JSON like this: +**IMPORTANT:** Respond ONLY with JSON containing BOTH "intent" and "params", AND a "response_text" field. +- "response_text" should be a friendly, user-facing message confirming the action taken, providing the answer, or asking for clarification. + +Examples: + +User: Add a meeting tomorrow at 3pm about project X +MAIA: { "intent": "add_calendar_event", "params": { - "title": "Team Meeting", - "description": "Discuss project updates", - "start": "2025-04-16 15:00:00.000000+00:00", - "end": "2025-04-16 16:00:00.000000+00:00", - "location": "Office" - } + "title": "Meeting", + "description": "Project X", + "start": "2025-04-19 15:00:00.000000+00:00", + "end": null, + "location": null + }, + "response_text": "Okay, I've added a meeting about Project X to your calendar for tomorrow at 3 PM." +} + +User: What's the weather like? +MAIA: +{ + "intent": "ask_ai", + "params": { + "request": "What's the weather like?" + }, + "response_text": "Let me check the weather for you." +} + +User: Delete the team sync event. +MAIA: +{ + "intent": "clarification_needed", + "params": { + "request": "Delete the team sync event." + }, + "response_text": "Okay, I can help with that. Could you please provide the ID or more specific details about the 'team sync' event you want me to delete?" } The datetime right now is """+str(datetime.now(timezone.utc))+""". @@ -45,6 +73,7 @@ Here is the user request: def process_request(request: str): """ Process the user request using the Google GenAI API. + Expects a JSON response with intent, params, and response_text. """ response = client.models.generate_content( model="gemini-2.0-flash", @@ -52,41 +81,25 @@ def process_request(request: str): config={ "temperature": 0.3, # Less creativity, more factual "response_mime_type": "application/json", - # "response_schema": { ### NOT WORKING - # "type": "object", - # "properties": { - # "intent": { - # "type": "string", - # "enum": [ - # "get_calendar_events", - # "add_calendar_event", - # "update_calendar_event", - # "delete_calendar_event" - # ] - # }, - # "params": { - # "type": "object", - # "properties": { - # "title": {"type": "string"}, - # "description": {"type": "string"}, - # "start": {"type": "string", "format": "date-time"}, - # "end": {"type": "string", "format": "date-time"}, - # "location": {"type": "string"}, - # "event_id": {"type": "integer"}, - - # }, - # } - # }, - # "required": ["intent", "params"] - # } } ) # Parse the JSON response try: - return json.loads(response.text) - except ValueError: - raise ValueError("Invalid JSON response from AI") + parsed_response = json.loads(response.text) + # Validate required fields + if not all(k in parsed_response for k in ("intent", "params", "response_text")): + raise ValueError("AI response missing required fields (intent, params, response_text)") + return parsed_response + except (json.JSONDecodeError, ValueError) as e: + print(f"Error parsing AI response: {e}") + print(f"Raw AI response: {response.text}") + # Return a structured error that the API layer can handle + return { + "intent": "error", + "params": {}, + "response_text": "Sorry, I had trouble understanding that request or formulating a response. Could you please try rephrasing?" + } def ask_ai(request: str): """ diff --git a/interfaces/nativeapp/src/api/auth.ts b/interfaces/nativeapp/src/api/auth.ts deleted file mode 100644 index 93916c6..0000000 --- a/interfaces/nativeapp/src/api/auth.ts +++ /dev/null @@ -1,12 +0,0 @@ -import apiClient from './client'; - - -export const healthCheck = async (): Promise<{ status: string }> => { - try { - const response = await apiClient.get('/health'); - return response.data - } catch (error) { - console.error("Error fetching backend health:", error); - throw error; - } -} \ No newline at end of file diff --git a/interfaces/nativeapp/src/api/calendar.ts b/interfaces/nativeapp/src/api/calendar.ts index 7dc9374..447ebd3 100644 --- a/interfaces/nativeapp/src/api/calendar.ts +++ b/interfaces/nativeapp/src/api/calendar.ts @@ -11,6 +11,7 @@ export const getCalendarEvents = async (start?: Date, end?: Date): Promise { // Explicitly type error as AxiosError const originalRequest = error.config; - // Check if the error has a response object (i.e., server responded with error status) - if (error.response) { + // Check if the error has a response object AND an original request config + if (error.response && originalRequest) { // <-- Added check for originalRequest // Server responded with an error status code (4xx, 5xx) console.error('[API Client] Response Error Status:', error.response.status); console.error('[API Client] Response Error Data:', error.response.data); @@ -64,39 +64,58 @@ apiClient.interceptors.response.use( if (error.response.status === 401) { console.warn('[API Client] Unauthorized (401). Token might be expired or invalid.'); - if (!originalRequest?._retry) { - originalRequest._retry = true; // Mark the request as retried to avoid infinite loops + if (originalRequest.url === '/auth/refresh') { + console.error('[API Client] Refresh token attempt failed with 401. Not retrying.'); + // Clear token and reject without retry + if (Platform.OS === 'web') { + await AsyncStorage.removeItem(TOKEN_KEY); + } else { + await SecureStore.deleteItemAsync(TOKEN_KEY).catch(() => {}); // Ignore delete error + } + delete apiClient.defaults.headers.common['Authorization']; + return Promise.reject(error); // Reject immediately + } + + // Proceed with refresh logic only if it wasn't the refresh endpoint that failed + // and if originalRequest exists (already checked above) + if (!originalRequest._retry) { // Now TS knows _retry exists due to declaration file + originalRequest._retry = true; try { console.log('[API Client] Attempting token refresh...'); const refreshResponse = await apiClient.post('/auth/refresh', {}, { headers: { - 'Content-Type': 'application/json', + 'Content-Type': 'application/json', }, }); - if (refreshResponse.status === 200) { - const newToken = refreshResponse.data?.accessToken; + if (refreshResponse.status === 200) { + const newToken = refreshResponse.data?.access_token; if (newToken) { - console.log('[API Client] Token refreshed successfully.'); + console.log('[API Client] Token refreshed successfully.'); + // Save the new token + if (Platform.OS === 'web') { + await AsyncStorage.setItem(TOKEN_KEY, newToken); + } else { + await SecureStore.setItemAsync(TOKEN_KEY, newToken); + } - // Save the new token - if (Platform.OS === 'web') { - await AsyncStorage.setItem(TOKEN_KEY, newToken); - } else { - await SecureStore.setItemAsync(TOKEN_KEY, newToken); - } + // Update the Authorization header for future requests + apiClient.defaults.headers.common['Authorization'] = `Bearer ${newToken}`; + // Safely update original request headers + if (originalRequest.headers) { + originalRequest.headers['Authorization'] = `Bearer ${newToken}`; + } - // Update the Authorization header for future requests - apiClient.defaults.headers.common['Authorization'] = `Bearer ${newToken}`; - originalRequest.headers['Authorization'] = `Bearer ${newToken}`; - - // Retry the original request with the new token - return apiClient(originalRequest); + // Retry the original request (originalRequest is guaranteed to exist here) + return apiClient(originalRequest); + } else { + console.error('[API Client] Invalid token structure received during refresh:', refreshResponse.data); + throw new Error('Invalid token received from server.'); } } - } catch (refreshError) { + } catch (refreshError: any) { console.error('[API Client] Token refresh failed:', refreshError); } } diff --git a/interfaces/nativeapp/src/contexts/AuthContext.tsx b/interfaces/nativeapp/src/contexts/AuthContext.tsx index 02cac4f..a0bcc6e 100644 --- a/interfaces/nativeapp/src/contexts/AuthContext.tsx +++ b/interfaces/nativeapp/src/contexts/AuthContext.tsx @@ -174,7 +174,7 @@ export const AuthProvider: React.FC = ({ children }) => { setAuthToken(null); delete apiClient.defaults.headers.common['Authorization']; await deleteToken(); // Use helper - // Optional backend logout call + await apiClient.post("/auth/logout"); }, []); const contextValue = useMemo(() => ({ @@ -192,7 +192,6 @@ export const AuthProvider: React.FC = ({ children }) => { ); }; -// --- useAuth and AuthLoadingScreen remain the same --- export const useAuth = () => { const context = useContext(AuthContext); if (!context) { diff --git a/interfaces/nativeapp/src/screens/CalendarScreen.tsx b/interfaces/nativeapp/src/screens/CalendarScreen.tsx index 3654f65..54e4647 100644 --- a/interfaces/nativeapp/src/screens/CalendarScreen.tsx +++ b/interfaces/nativeapp/src/screens/CalendarScreen.tsx @@ -36,11 +36,12 @@ const CalendarScreen = () => { // Store events keyed by date *for the list display* const [eventsByDate, setEventsByDate] = useState<{ [key: string]: CalendarEvent[] }>({}); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); // --- Fetching Logic --- const fetchEventsForMonth = useCallback(async (date: Date | DateData) => { + console.log("[CAM] fetchevents start"); setIsLoading(true); setError(null); const targetYear = 'year' in date ? date.year : getYear(date); @@ -71,18 +72,28 @@ const CalendarScreen = () => { // Process events for the daily list view const newEventsByDate: { [key: string]: CalendarEvent[] } = {}; fetchedEvents.forEach(event => { - const startDate = parseISO(event.start); - const endDate = parseISO(event.end); + // --- Check for valid START date string --- + if (typeof event.start !== 'string') { + console.warn(`Event ${event.id} has invalid start date type:`, event.start); + return; // Skip this event + } + // --- End check --- - if (!isValid(startDate) || !isValid(endDate)) { - console.warn(`Invalid date found in event ${event.id}`); + const startDate = parseISO(event.start); + // --- Check if START date is valid after parsing --- + if (!isValid(startDate)) { + console.warn(`Invalid start date found in event ${event.id}:`, event.start); return; // Skip invalid events } - // Ensure end date is not before start date - const end = endDate < startDate ? startDate : endDate; + // --- Handle potentially null END date --- + // If end is null or not a string, treat it as the same as start date + const endDate = typeof event.end === 'string' && isValid(parseISO(event.end)) ? parseISO(event.end) : startDate; - const intervalDates = eachDayOfInterval({ start: startDate, end: end }); + // Ensure end date is not before start date (could happen with invalid data or timezone issues) + const endForInterval = endDate < startDate ? startDate : endDate; + + const intervalDates = eachDayOfInterval({ start: startDate, end: endForInterval }); intervalDates.forEach(dayInInterval => { const dateKey = format(dayInInterval, 'yyyy-MM-dd'); if (!newEventsByDate[dateKey]) { @@ -103,14 +114,80 @@ const CalendarScreen = () => { console.error(err); } finally { setIsLoading(false); + console.log("[CAM] isLoading:", isLoading); } }, [isLoading, currentMonthData]); // Include dependencies // --- Initial Fetch --- useEffect(() => { - const initialDate = parseISO(todayString); - fetchEventsForMonth(initialDate); - }, [fetchEventsForMonth, todayString]); + const performInitialLoad = async () => { + console.log("[CalendarScreen] Performing initial load."); + setIsLoading(true); + setError(null); + const initialDate = parseISO(todayString); + const targetYear = getYear(initialDate); + const targetMonth = getMonth(initialDate) + 1; + + setCurrentMonthData({ + year: targetYear, + month: targetMonth, + dateString: todayString, + day: initialDate.getDate(), + timestamp: initialDate.getTime(), + }); + + try { + const fetchedEvents = await getCalendarEvents(targetYear, targetMonth); + const newEventsByDate: {[key: string]: CalendarEvent[] } = {}; + + fetchedEvents.forEach(event => { + // --- Check for valid START date string --- + if (typeof event.start !== 'string') { + console.warn(`Event ${event.id} has invalid start date type during initial load:`, event.start); + return; // Skip this event + } + // --- End check --- + + const startDate = parseISO(event.start); + // --- Check if START date is valid after parsing --- + if (!isValid(startDate)) { + console.warn(`Invalid start date found in event ${event.id} during initial load:`, event.start); + return; // Skip invalid events + } + + // --- Handle potentially null END date --- + // If end is null or not a string, treat it as the same as start date + const endDate = typeof event.end === 'string' && isValid(parseISO(event.end)) ? parseISO(event.end) : startDate; + + // Ensure end date is not before start date + const endForInterval = endDate < startDate ? startDate : endDate; + + const intervalDates = eachDayOfInterval({ start: startDate, end: endForInterval }); + intervalDates.forEach(dayInInterval => { + const dateKey = format(dayInInterval, 'yyyy-MM-dd'); + if (!newEventsByDate[dateKey]) { + newEventsByDate[dateKey] = []; + } + // Avoid duplicates if an event is already added for this key + if (!newEventsByDate[dateKey].some(e => e.id === event.id)) { + newEventsByDate[dateKey].push(event); + } + }); + }); + + setRawEvents(fetchedEvents); + setEventsByDate(newEventsByDate); + } catch (err) { + setError('Failed to load initial calendar events.'); + setRawEvents([]); + setEventsByDate({}); + console.error(err); + } finally { + setIsLoading(false); + } + }; + performInitialLoad(); + }, [todayString]); // --- Callbacks for Calendar --- const onDayPress = useCallback((day: DateData) => { @@ -119,6 +196,7 @@ const CalendarScreen = () => { const onMonthChange = useCallback((month: DateData) => { if (!currentMonthData || month.year !== currentMonthData.year || month.month !== currentMonthData.month) { + console.log("[CAM] CAlling fetchevents"); fetchEventsForMonth(month); } else { setCurrentMonthData(month); // Just update the current data if same month @@ -130,18 +208,29 @@ const CalendarScreen = () => { const marks: { [key: string]: MarkingProps } = {}; // Use MarkingProps type rawEvents.forEach(event => { - const startDate = parseISO(event.start); - const endDate = parseISO(event.end); - const eventColor = event.color || theme.colors.primary; // Use event color or default + // --- Check for valid START date string --- + if (typeof event.start !== 'string') { + console.warn(`Event ${event.id} has invalid start date type in markedDates:`, event.start); + return; // Skip this event + } + // --- End check --- - if (!isValid(startDate) || !isValid(endDate)) { + const startDate = parseISO(event.start); + // --- Check if START date is valid after parsing --- + if (!isValid(startDate)) { + console.warn(`Invalid start date found for marking in event ${event.id}:`, event.start); return; // Skip invalid events } - // Ensure end date is not before start date - const end = endDate < startDate ? startDate : endDate; + // --- Handle potentially null END date --- + // If end is null or not a string, treat it as the same as start date + const endDate = typeof event.end === 'string' && isValid(parseISO(event.end)) ? parseISO(event.end) : startDate; + const eventColor = event.color || theme.colors.primary; // Use event color or default - const intervalDates = eachDayOfInterval({ start: startDate, end: end }); + // Ensure end date is not before start date + const endForInterval = endDate < startDate ? startDate : endDate; + + const intervalDates = eachDayOfInterval({ start: startDate, end: endForInterval }); intervalDates.forEach((dateInInterval, index) => { const dateString = format(dateInInterval, 'yyyy-MM-dd'); @@ -172,9 +261,6 @@ const CalendarScreen = () => { // Ensure start/end flags aren't overwritten by non-start/end marks startingDay: marks[dateString]?.startingDay || marking.startingDay, endingDay: marks[dateString]?.endingDay || marking.endingDay, - // We might need a more complex strategy if multiple periods - // with different colors overlap on the same day. - // For now, the last event processed might "win" the color. }; }); }); @@ -182,43 +268,50 @@ const CalendarScreen = () => { // Add selected day marking (merge with period marking) if (selectedDate) { marks[selectedDate] = { - ...(marks[selectedDate] || {}), // Keep existing period/dot marks + ...(marks[selectedDate] || {}), selected: true, - // Keep the period color if it exists, otherwise use selection color - color: marks[selectedDate]?.color || theme.colors.primary, // Period wins color? or selection? Choose one. Here period wins. - // selectedColor: theme.colors.secondary, // Or use a distinct selection highlight color? - // Ensure text color is appropriate for selected state + color: marks[selectedDate]?.color || theme.colors.primary, textColor: theme.colors.onPrimary || '#ffffff', // If selected, don't let it look like starting/ending unless it truly is startingDay: marks[selectedDate]?.startingDay && marks[selectedDate]?.selected, endingDay: marks[selectedDate]?.endingDay && marks[selectedDate]?.selected, }; } - - // Add today marking (merge with period/selection marking) - // Period marking visually indicates today already if colored. Add dot? - marks[todayString] = { - ...(marks[todayString] || {}), - // marked: true, // 'marked' is implicit with period marking color - dotColor: theme.colors.secondary, // Add a distinct dot for today? - // Or rely on the 'todayTextColor' in the theme prop - }; + marks[todayString] = { + ...(marks[todayString] || {}), + dotColor: theme.colors.secondary, + }; return marks; }, [rawEvents, selectedDate, theme.colors, theme.dark, todayString]); // Include theme.dark if colors change // --- Render Event Item --- const renderEventItem = ({ item }: { item: CalendarEvent }) => { + // --- Check for valid START date string --- + if (typeof item.start !== 'string') { + console.warn(`Event ${item.id} has invalid start date type for rendering:`, item.start); + return null; // Don't render item with invalid start date + } const startDate = parseISO(item.start); - const endDate = parseISO(item.end); + if (!isValid(startDate)) { + console.warn(`Invalid start date found for rendering event ${item.id}:`, item.start); + return null; // Don't render item with invalid start date + } + + // --- Handle potentially null END date --- + const hasValidEndDate = typeof item.end === 'string' && isValid(parseISO(item.end)); + const endDate = hasValidEndDate ? parseISO(item.end) : startDate; + let description = item.description || ''; - if (isValid(startDate)) { - // Show date range if it spans multiple days or specific time if single day - if (!isSameDay(startDate, endDate)) { - description = `${format(startDate, 'MMM d')} - ${format(endDate, 'MMM d')}${item.description ? `\n${item.description}` : ''}`; - } else { - description = `Time: ${format(startDate, 'p')}${item.description ? `\n${item.description}` : ''}`; // 'p' is locale-specific time format - } + const timePrefix = `Time: ${format(startDate, 'p')}`; + const dateRangePrefix = `${format(startDate, 'MMM d')} - ${format(endDate, 'MMM d')}`; + + // Show date range only if end date is valid and different from start date + if (hasValidEndDate && !isSameDay(startDate, endDate)) { + description = `${dateRangePrefix}${item.description ? `\n${item.description}` : ''}`; + } else { + // Otherwise, show start time + description = `${timePrefix}${item.description ? `\n${item.description}` : ''}`; } return ( @@ -229,7 +322,7 @@ const CalendarScreen = () => { style={styles.eventItem} titleStyle={{ color: theme.colors.text }} descriptionStyle={{ color: theme.colors.textSecondary }} - descriptionNumberOfLines={3} // Allow more lines for range/details + descriptionNumberOfLines={3} /> ); } diff --git a/interfaces/nativeapp/src/screens/ChatScreen.tsx b/interfaces/nativeapp/src/screens/ChatScreen.tsx index 3c9a831..1b108f2 100644 --- a/interfaces/nativeapp/src/screens/ChatScreen.tsx +++ b/interfaces/nativeapp/src/screens/ChatScreen.tsx @@ -1,19 +1,192 @@ -// src/screens/DashboardScreen.tsx -import React from 'react'; -import { View, StyleSheet } from 'react-native'; -import { Text, useTheme } from 'react-native-paper'; +// src/screens/ChatScreen.tsx +import React, { useState, useCallback, useRef } 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'; +import apiClient from '../api/client'; // Import the apiClient -const DashboardScreen = () => { +// Define the structure for a message +interface Message { + id: string; + text: string; + sender: 'user' | 'ai'; + timestamp: Date; +} + +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 flatListRef = useRef(null); + + // Function to handle sending a message + const handleSend = useCallback(async () => { + const trimmedText = inputText.trim(); + if (!trimmedText) return; // Don't send empty messages + + const userMessage: Message = { + id: Date.now().toString() + '-user', + text: trimmedText, + sender: 'user', + timestamp: new Date(), + }; + + setMessages(prevMessages => [...prevMessages, userMessage]); + setInputText(''); + setIsLoading(true); + + // Scroll to bottom after sending user message + setTimeout(() => flatListRef.current?.scrollToEnd({ animated: true }), 100); + + // --- Call Backend API --- + try { + console.log(`[ChatScreen] Sending to /nlp/process-command: ${trimmedText}`); + const response = await apiClient.post<{ response: string }>('/nlp/process-command', { user_input: trimmedText }); + console.log("[ChatScreen] Received response:", response.data); + + const aiResponse: Message = { + id: Date.now().toString() + '-ai', + // Assuming the backend returns the response text in a 'response' field + text: response.data.response || "Sorry, I didn't get a valid response.", + sender: 'ai', + timestamp: new Date(), + }; + setMessages(prevMessages => [...prevMessages, aiResponse]); + } catch (error: any) { + console.error("Failed to get AI response:", error.response?.data || error.message || error); + const errorResponse: Message = { + id: Date.now().toString() + '-error', + text: 'Sorry, I encountered an error trying to reach MAIA.', + sender: 'ai', + timestamp: new Date(), + }; + setMessages(prevMessages => [...prevMessages, errorResponse]); + } finally { + setIsLoading(false); + // Scroll to bottom after receiving AI message + setTimeout(() => flatListRef.current?.scrollToEnd({ animated: true }), 100); + } + // --- End API Call --- + + }, [inputText]); // Keep inputText as dependency + + const handleKeyPress = (e: NativeSyntheticEvent) => { + if (e.nativeEvent.key === 'Enter' && !(e.nativeEvent as any).shiftKey) { + e.preventDefault(); // Prevent new line + handleSend(); + } + }; + + // Render individual message item + const renderMessage = ({ item }: { item: Message }) => { + const isUser = item.sender === 'user'; + return ( + + + {item.text} + + {/* Optional: Add timestamp */} + {/* + {item.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + */} + + ); + }; + const styles = StyleSheet.create({ - container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16, backgroundColor: theme.colors.background }, - text: { fontSize: 20, color: theme.colors.text } + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + listContainer: { + flex: 1, + }, + messageList: { + paddingHorizontal: 10, + paddingVertical: 10, + }, + inputContainer: { + flexDirection: 'row', + alignItems: 'center', + padding: 8, + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: theme.colors.outlineVariant, + backgroundColor: theme.colors.elevation.level2, // Slightly elevated background + }, + textInput: { + flex: 1, + marginRight: 8, + backgroundColor: theme.colors.surface, // Use surface color for input background + }, + messageBubble: { + maxWidth: '80%', + padding: 10, + borderRadius: 15, + marginBottom: 10, + }, + userBubble: { + alignSelf: 'flex-end', + backgroundColor: theme.colors.primary, + borderBottomRightRadius: 5, + }, + aiBubble: { + alignSelf: 'flex-start', + backgroundColor: theme.colors.surfaceVariant, + borderBottomLeftRadius: 5, + }, + timestamp: { + fontSize: 10, + marginTop: 4, + alignSelf: 'flex-end', + opacity: 0.7, + } }); + return ( - - Chat - + + + + 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 + /> + + + + + + + + ); }; -export default DashboardScreen; +export default ChatScreen; \ No newline at end of file diff --git a/interfaces/nativeapp/src/screens/ProfileScreen.tsx b/interfaces/nativeapp/src/screens/ProfileScreen.tsx index 8858165..e45888e 100644 --- a/interfaces/nativeapp/src/screens/ProfileScreen.tsx +++ b/interfaces/nativeapp/src/screens/ProfileScreen.tsx @@ -1,19 +1,124 @@ -// src/screens/DashboardScreen.tsx -import React from 'react'; -import { View, StyleSheet } from 'react-native'; -import { Text, useTheme } from 'react-native-paper'; +// src/screens/ProfileScreen.tsx +import React, { useState, useEffect } from 'react'; +import { View, StyleSheet, ActivityIndicator, Alert } from 'react-native'; +import { Text, Button, useTheme, Card, Title, Paragraph } from 'react-native-paper'; +import { useAuth } from '../contexts/AuthContext'; +import apiClient from '../api/client'; -const DashboardScreen = () => { +// Define an interface for the user data structure +interface UserProfile { + username: string; + name: string; + uuid: string; + role: string; +} + +const ProfileScreen = () => { const theme = useTheme(); + const { logout } = useAuth(); // Get the logout function + const [userProfile, setUserProfile] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchUserProfile = async () => { + setIsLoading(true); + setError(null); + try { + const response = await apiClient.get('/user/me'); + setUserProfile(response.data); + } catch (err: any) { + console.error("Failed to fetch user profile:", err); + setError(err.response?.data?.detail || err.message || 'Failed to load profile.'); + } finally { + setIsLoading(false); + } + }; + + fetchUserProfile(); + }, []); // Empty dependency array means this runs once on mount + + const handleLogout = async () => { + try { + await logout(); + // Navigation to login screen will likely happen automatically + // due to the state change in AuthProvider re-rendering the navigator + } catch (err) { + console.error("Logout failed:", err); + Alert.alert("Logout Error", "Could not log out. Please try again."); + } + }; + const styles = StyleSheet.create({ - container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16, backgroundColor: theme.colors.background }, - text: { fontSize: 20, color: theme.colors.text } + container: { + flex: 1, + padding: 16, + backgroundColor: theme.colors.background, + justifyContent: 'space-between', // Pushes logout button to bottom + }, + contentContainer: { + flex: 1, // Takes up available space + alignItems: 'center', + justifyContent: 'center', // Center content vertically + }, + card: { + width: '100%', + maxWidth: 400, // Optional: constrain width on larger screens + marginBottom: 20, + backgroundColor: theme.colors.surface, // Use theme surface color + }, + errorText: { + fontSize: 16, + color: theme.colors.error, + textAlign: 'center', + marginBottom: 20, + }, + logoutButton: { + marginTop: 'auto', // Pushes button to the bottom within its container + marginBottom: 20, // Add some space at the very bottom + }, + // Removed text style as Paper components handle theme text color }); + + if (isLoading) { + return ( + + + + ); + } + return ( - Profile + + {error && {error}} + + {userProfile ? ( + + + User Profile + Username: {userProfile.username} + Name: {userProfile.name} + UUID: {userProfile.uuid} + Role: {userProfile.role} + + + ) : ( + !error && No profile data available. // Show if no error but no data + )} + + + ); }; -export default DashboardScreen; +export default ProfileScreen; diff --git a/interfaces/nativeapp/src/types/axios.d.ts b/interfaces/nativeapp/src/types/axios.d.ts new file mode 100644 index 0000000..27d8c88 --- /dev/null +++ b/interfaces/nativeapp/src/types/axios.d.ts @@ -0,0 +1,8 @@ +// src/types/axios.d.ts +import 'axios'; + +declare module 'axios' { + export interface InternalAxiosRequestConfig { + _retry?: boolean; // Add our custom property as optional + } +}