diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index c43ead1d7..d2c88cb2f 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1524,9 +1524,7 @@ USER_PERMISSIONS_FEATURES_AUTOMATIONS = ( os.environ.get('USER_PERMISSIONS_FEATURES_AUTOMATIONS', 'False').lower() == 'true' ) -USER_PERMISSIONS_FEATURES_CALENDAR = ( - os.environ.get('USER_PERMISSIONS_FEATURES_CALENDAR', 'True').lower() == 'true' -) +USER_PERMISSIONS_FEATURES_CALENDAR = os.environ.get('USER_PERMISSIONS_FEATURES_CALENDAR', 'True').lower() == 'true' USER_PERMISSIONS_SETTINGS_INTERFACE = os.environ.get('USER_PERMISSIONS_SETTINGS_INTERFACE', 'True').lower() == 'true' diff --git a/backend/open_webui/migrations/versions/56359461a091_add_calendar_tables.py b/backend/open_webui/migrations/versions/56359461a091_add_calendar_tables.py index a0812578c..e556440f5 100644 --- a/backend/open_webui/migrations/versions/56359461a091_add_calendar_tables.py +++ b/backend/open_webui/migrations/versions/56359461a091_add_calendar_tables.py @@ -5,6 +5,7 @@ Revises: c1d2e3f4a5b6 Create Date: 2026-04-19 16:20:58.162045 """ + from typing import Sequence, Union from alembic import op @@ -19,52 +20,55 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: - op.create_table('calendar', - sa.Column('id', sa.Text(), nullable=False), - sa.Column('user_id', sa.Text(), nullable=False), - sa.Column('name', sa.Text(), nullable=False), - sa.Column('color', sa.Text(), nullable=True), - sa.Column('is_default', sa.Boolean(), nullable=False), - sa.Column('data', sa.JSON(), nullable=True), - sa.Column('meta', sa.JSON(), nullable=True), - sa.Column('created_at', sa.BigInteger(), nullable=False), - sa.Column('updated_at', sa.BigInteger(), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_table( + 'calendar', + sa.Column('id', sa.Text(), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('color', sa.Text(), nullable=True), + sa.Column('is_default', sa.Boolean(), nullable=False), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + sa.PrimaryKeyConstraint('id'), ) op.create_index('ix_calendar_user', 'calendar', ['user_id'], unique=False) - op.create_table('calendar_event', - sa.Column('id', sa.Text(), nullable=False), - sa.Column('calendar_id', sa.Text(), nullable=False), - sa.Column('user_id', sa.Text(), nullable=False), - sa.Column('title', sa.Text(), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('start_at', sa.BigInteger(), nullable=False), - sa.Column('end_at', sa.BigInteger(), nullable=True), - sa.Column('all_day', sa.Boolean(), nullable=False), - sa.Column('rrule', sa.Text(), nullable=True), - sa.Column('color', sa.Text(), nullable=True), - sa.Column('location', sa.Text(), nullable=True), - sa.Column('data', sa.JSON(), nullable=True), - sa.Column('meta', sa.JSON(), nullable=True), - sa.Column('is_cancelled', sa.Boolean(), nullable=False), - sa.Column('created_at', sa.BigInteger(), nullable=False), - sa.Column('updated_at', sa.BigInteger(), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_table( + 'calendar_event', + sa.Column('id', sa.Text(), nullable=False), + sa.Column('calendar_id', sa.Text(), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('title', sa.Text(), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('start_at', sa.BigInteger(), nullable=False), + sa.Column('end_at', sa.BigInteger(), nullable=True), + sa.Column('all_day', sa.Boolean(), nullable=False), + sa.Column('rrule', sa.Text(), nullable=True), + sa.Column('color', sa.Text(), nullable=True), + sa.Column('location', sa.Text(), nullable=True), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('is_cancelled', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + sa.PrimaryKeyConstraint('id'), ) op.create_index('ix_calendar_event_calendar', 'calendar_event', ['calendar_id', 'start_at'], unique=False) op.create_index('ix_calendar_event_user_date', 'calendar_event', ['user_id', 'start_at'], unique=False) - op.create_table('calendar_event_attendee', - sa.Column('id', sa.Text(), nullable=False), - sa.Column('event_id', sa.Text(), nullable=False), - sa.Column('user_id', sa.Text(), nullable=False), - sa.Column('status', sa.Text(), nullable=False), - sa.Column('meta', sa.JSON(), nullable=True), - sa.Column('created_at', sa.BigInteger(), nullable=False), - sa.Column('updated_at', sa.BigInteger(), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('event_id', 'user_id', name='uq_event_attendee') + op.create_table( + 'calendar_event_attendee', + sa.Column('id', sa.Text(), nullable=False), + sa.Column('event_id', sa.Text(), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('status', sa.Text(), nullable=False), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('event_id', 'user_id', name='uq_event_attendee'), ) op.create_index('ix_calendar_event_attendee_user', 'calendar_event_attendee', ['user_id', 'status'], unique=False) diff --git a/backend/open_webui/models/automations.py b/backend/open_webui/models/automations.py index c7c78a7c8..05f449ad1 100644 --- a/backend/open_webui/models/automations.py +++ b/backend/open_webui/models/automations.py @@ -153,15 +153,11 @@ class AutomationTable: row = await db.get(Automation, id) return AutomationModel.model_validate(row) if row else None - async def get_active_by_user( - self, user_id: str, db: Optional[AsyncSession] = None - ) -> list[AutomationModel]: + async def get_active_by_user(self, user_id: str, db: Optional[AsyncSession] = None) -> list[AutomationModel]: """Get active automations for a user (for calendar RRULE expansion).""" async with get_async_db_context(db) as db: result = await db.execute( - select(Automation) - .filter_by(user_id=user_id, is_active=True) - .order_by(Automation.created_at.desc()) + select(Automation).filter_by(user_id=user_id, is_active=True).order_by(Automation.created_at.desc()) ) return [AutomationModel.model_validate(r) for r in result.scalars().all()] @@ -291,9 +287,8 @@ class AutomationTable: timezone_by_user_id: dict[str, Optional[str]] = {} if user_ids: from open_webui.models.users import User - tz_result = await db.execute( - select(User.id, User.timezone).where(User.id.in_(user_ids)) - ) + + tz_result = await db.execute(select(User.id, User.timezone).where(User.id.in_(user_ids))) timezone_by_user_id = {uid: tz for uid, tz in tz_result.all()} for row in rows: diff --git a/backend/open_webui/models/calendar.py b/backend/open_webui/models/calendar.py index 859632c49..9d2d71a45 100644 --- a/backend/open_webui/models/calendar.py +++ b/backend/open_webui/models/calendar.py @@ -232,9 +232,7 @@ class CalendarEventListResponse(BaseModel): class CalendarTable: - async def _get_access_grants( - self, calendar_id: str, db: Optional[AsyncSession] = None - ) -> list[AccessGrantModel]: + async def _get_access_grants(self, calendar_id: str, db: Optional[AsyncSession] = None) -> list[AccessGrantModel]: return await AccessGrants.get_grants_by_resource('calendar', calendar_id, db=db) async def _to_calendar_model( @@ -245,15 +243,11 @@ class CalendarTable: ) -> CalendarModel: cal_data = CalendarModel.model_validate(cal).model_dump(exclude={'access_grants'}) cal_data['access_grants'] = ( - access_grants - if access_grants is not None - else await self._get_access_grants(cal_data['id'], db=db) + access_grants if access_grants is not None else await self._get_access_grants(cal_data['id'], db=db) ) return CalendarModel.model_validate(cal_data) - async def get_or_create_defaults( - self, user_id: str, db: Optional[AsyncSession] = None - ) -> list[CalendarModel]: + async def get_or_create_defaults(self, user_id: str, db: Optional[AsyncSession] = None) -> list[CalendarModel]: """Return user's calendars, creating 'Personal' and 'Scheduled Tasks' if none exist.""" async with get_async_db_context(db) as db: result = await db.execute( @@ -289,9 +283,7 @@ class CalendarTable: await db.commit() return [CalendarModel.model_validate(c) for c in defaults] - async def get_calendars_by_user( - self, user_id: str, db: Optional[AsyncSession] = None - ) -> list[CalendarModel]: + async def get_calendars_by_user(self, user_id: str, db: Optional[AsyncSession] = None) -> list[CalendarModel]: """Owned + shared calendars.""" async with get_async_db_context(db) as db: user_groups = await Groups.get_groups_by_member_id(user_id, db=db) @@ -317,14 +309,9 @@ class CalendarTable: cal_ids = [c.id for c in calendars] grants_map = await AccessGrants.get_grants_by_resources('calendar', cal_ids, db=db) - return [ - await self._to_calendar_model(c, access_grants=grants_map.get(c.id, []), db=db) - for c in calendars - ] + return [await self._to_calendar_model(c, access_grants=grants_map.get(c.id, []), db=db) for c in calendars] - async def get_calendar_by_id( - self, id: str, db: Optional[AsyncSession] = None - ) -> Optional[CalendarModel]: + async def get_calendar_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[CalendarModel]: async with get_async_db_context(db) as db: result = await db.execute(select(Calendar).filter(Calendar.id == id)) cal = result.scalars().first() @@ -414,9 +401,7 @@ class CalendarTable: .values(is_default=False) ) # Set the new default - result = await db.execute( - select(Calendar).filter(Calendar.id == calendar_id, Calendar.user_id == user_id) - ) + result = await db.execute(select(Calendar).filter(Calendar.id == calendar_id, Calendar.user_id == user_id)) cal = result.scalars().first() if not cal: return None @@ -435,15 +420,11 @@ class CalendarTable: return False # Delete attendees for all events in this calendar - event_ids_result = await db.execute( - select(CalendarEvent.id).filter(CalendarEvent.calendar_id == id) - ) + event_ids_result = await db.execute(select(CalendarEvent.id).filter(CalendarEvent.calendar_id == id)) event_ids = [r[0] for r in event_ids_result.all()] if event_ids: await db.execute( - delete(CalendarEventAttendee).filter( - CalendarEventAttendee.event_id.in_(event_ids) - ) + delete(CalendarEventAttendee).filter(CalendarEventAttendee.event_id.in_(event_ids)) ) # Delete events @@ -465,9 +446,7 @@ class CalendarEventTable: self, event_id: str, db: Optional[AsyncSession] = None ) -> list[CalendarEventAttendeeModel]: async with get_async_db_context(db) as db: - result = await db.execute( - select(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == event_id) - ) + result = await db.execute(select(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == event_id)) rows = result.scalars().all() return [CalendarEventAttendeeModel.model_validate(r) for r in rows] @@ -515,9 +494,7 @@ class CalendarEventTable: return await self._to_event_model(event, db=db) - async def get_event_by_id( - self, id: str, db: Optional[AsyncSession] = None - ) -> Optional[CalendarEventModel]: + async def get_event_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[CalendarEventModel]: async with get_async_db_context(db) as db: result = await db.execute(select(CalendarEvent).filter(CalendarEvent.id == id)) event = result.scalars().first() @@ -559,9 +536,7 @@ class CalendarEventTable: # Also get event IDs where user is an attendee attendee_event_ids_result = await db.execute( - select(CalendarEventAttendee.event_id).filter( - CalendarEventAttendee.user_id == user_id - ) + select(CalendarEventAttendee.event_id).filter(CalendarEventAttendee.user_id == user_id) ) attendee_event_ids = [r[0] for r in attendee_event_ids_result.all()] @@ -608,16 +583,12 @@ class CalendarEventTable: # Batch-load attendees for all events in one query (avoid N+1) event_ids = [event.id for event, _user in items] att_result = await db.execute( - select(CalendarEventAttendee).filter( - CalendarEventAttendee.event_id.in_(event_ids) - ) + select(CalendarEventAttendee).filter(CalendarEventAttendee.event_id.in_(event_ids)) ) att_rows = att_result.scalars().all() att_map: dict[str, list[CalendarEventAttendeeModel]] = {} for a in att_rows: - att_map.setdefault(a.event_id, []).append( - CalendarEventAttendeeModel.model_validate(a) - ) + att_map.setdefault(a.event_id, []).append(CalendarEventAttendeeModel.model_validate(a)) events = [] for event, user in items: @@ -697,16 +668,12 @@ class CalendarEventTable: # Batch-load attendees event_ids = [event.id for event, _user in items] att_result = await db.execute( - select(CalendarEventAttendee).filter( - CalendarEventAttendee.event_id.in_(event_ids) - ) + select(CalendarEventAttendee).filter(CalendarEventAttendee.event_id.in_(event_ids)) ) att_rows = att_result.scalars().all() att_map: dict[str, list[CalendarEventAttendeeModel]] = {} for a in att_rows: - att_map.setdefault(a.event_id, []).append( - CalendarEventAttendeeModel.model_validate(a) - ) + att_map.setdefault(a.event_id, []).append(CalendarEventAttendeeModel.model_validate(a)) events = [] for event, user in items: @@ -732,8 +699,16 @@ class CalendarEventTable: update_data = form_data.model_dump(exclude_unset=True) for field in [ - 'calendar_id', 'title', 'description', 'start_at', 'end_at', - 'all_day', 'rrule', 'color', 'location', 'is_cancelled', + 'calendar_id', + 'title', + 'description', + 'start_at', + 'end_at', + 'all_day', + 'rrule', + 'color', + 'location', + 'is_cancelled', ]: if field in update_data: setattr(event, field, update_data[field]) @@ -750,13 +725,10 @@ class CalendarEventTable: await db.commit() return await self._to_event_model(event, db=db) - async def delete_event_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: try: async with get_async_db_context(db) as db: - await db.execute( - delete(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == id) - ) + await db.execute(delete(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == id)) await db.execute(delete(CalendarEvent).filter(CalendarEvent.id == id)) await db.commit() return True @@ -774,9 +746,7 @@ class CalendarEventAttendeeTable: """ async with get_async_db_context(db) as db: # Remove existing - await db.execute( - delete(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == event_id) - ) + await db.execute(delete(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == event_id)) now = int(time.time_ns()) models = [] @@ -819,20 +789,14 @@ class CalendarEventAttendeeTable: self, event_id: str, db: Optional[AsyncSession] = None ) -> list[CalendarEventAttendeeModel]: async with get_async_db_context(db) as db: - result = await db.execute( - select(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == event_id) - ) + result = await db.execute(select(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == event_id)) return [CalendarEventAttendeeModel.model_validate(r) for r in result.scalars().all()] - async def get_events_by_attendee( - self, user_id: str, db: Optional[AsyncSession] = None - ) -> list[str]: + async def get_events_by_attendee(self, user_id: str, db: Optional[AsyncSession] = None) -> list[str]: """Return event IDs where user is an attendee.""" async with get_async_db_context(db) as db: result = await db.execute( - select(CalendarEventAttendee.event_id).filter( - CalendarEventAttendee.user_id == user_id - ) + select(CalendarEventAttendee.event_id).filter(CalendarEventAttendee.user_id == user_id) ) return [r[0] for r in result.all()] diff --git a/backend/open_webui/routers/calendar.py b/backend/open_webui/routers/calendar.py index 5fb7cb6f9..47093ca78 100644 --- a/backend/open_webui/routers/calendar.py +++ b/backend/open_webui/routers/calendar.py @@ -47,9 +47,7 @@ async def check_calendar_permission(request: Request, user): ) -async def _check_calendar_access( - calendar_id: str, user: UserModel, permission: str = 'write' -) -> CalendarModel: +async def _check_calendar_access(calendar_id: str, user: UserModel, permission: str = 'write') -> CalendarModel: """Verify user has access to a calendar. Returns the calendar or raises 403/404.""" cal = await Calendars.get_calendar_by_id(calendar_id) if not cal: @@ -139,9 +137,7 @@ async def get_events( for event in events: event_dict = event.model_dump() if event_dict.get('rrule'): - instances = expand_recurring_event( - event_dict, start_ns, end_ns, tz=user.timezone - ) + instances = expand_recurring_event(event_dict, start_ns, end_ns, tz=user.timezone) for inst in instances: expanded.append(CalendarEventUserResponse(**{**inst, 'user': event.user})) else: @@ -189,34 +185,34 @@ async def get_events( expanded.append(CalendarEventUserResponse(**inst)) # Past runs: single range query joined with automation - runs_with_auto = await AutomationRuns.get_runs_by_user_range( - user.id, start_ns, end_ns - ) + runs_with_auto = await AutomationRuns.get_runs_by_user_range(user.id, start_ns, end_ns) for run, auto in runs_with_auto: - expanded.append(CalendarEventUserResponse( - id=f'run_{run.id}', - calendar_id=scheduled_cal.id, - user_id=user.id, - title=auto.name, - description=run.error if run.status == 'error' else '', - start_at=run.created_at, - end_at=None, - all_day=False, - color=None, - location=None, - data=None, - meta={ - 'automation_id': auto.id, - 'run_id': run.id, - 'chat_id': run.chat_id, - 'status': run.status, - }, - is_cancelled=False, - attendees=[], - created_at=run.created_at, - updated_at=run.created_at, - user=None, - )) + expanded.append( + CalendarEventUserResponse( + id=f'run_{run.id}', + calendar_id=scheduled_cal.id, + user_id=user.id, + title=auto.name, + description=run.error if run.status == 'error' else '', + start_at=run.created_at, + end_at=None, + all_day=False, + color=None, + location=None, + data=None, + meta={ + 'automation_id': auto.id, + 'run_id': run.id, + 'chat_id': run.chat_id, + 'status': run.status, + }, + is_cancelled=False, + attendees=[], + created_at=run.created_at, + updated_at=run.created_at, + user=None, + ) + ) except Exception as e: log.warning(f'Failed to compute automation events: {e}', exc_info=True) @@ -239,9 +235,7 @@ async def search_events( user: UserModel = Depends(get_verified_user), ): await check_calendar_permission(request, user) - return await CalendarEvents.search_events( - user_id=user.id, query=query, skip=skip, limit=limit - ) + return await CalendarEvents.search_events(user_id=user.id, query=query, skip=skip, limit=limit) @router.get('/events/{event_id}', response_model=CalendarEventModel) @@ -341,7 +335,6 @@ async def delete_calendar(request: Request, calendar_id: str, user: UserModel = if cal.user_id != user.id and user.role != 'admin': raise HTTPException(status_code=403, detail='Only owner can delete calendar') - result = await Calendars.delete_calendar_by_id(calendar_id) if not result: raise HTTPException(status_code=500, detail='Failed to delete') diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py index d8d92b242..f503169fc 100644 --- a/backend/open_webui/routers/knowledge.py +++ b/backend/open_webui/routers/knowledge.py @@ -539,9 +539,7 @@ async def update_knowledge_access_by_id( 'sharing.public_knowledge', ) - knowledge.access_grants = await AccessGrants.set_access_grants( - 'knowledge', id, form_data.access_grants, db=db - ) + knowledge.access_grants = await AccessGrants.set_access_grants('knowledge', id, form_data.access_grants, db=db) return KnowledgeFilesResponse( **knowledge.model_dump(), diff --git a/backend/open_webui/static/favicon.ico b/backend/open_webui/static/favicon.ico index 14c5f9c6d..b819d42f9 100644 Binary files a/backend/open_webui/static/favicon.ico and b/backend/open_webui/static/favicon.ico differ diff --git a/backend/open_webui/static/favicon.png b/backend/open_webui/static/favicon.png index 63735ad46..10c84f440 100644 Binary files a/backend/open_webui/static/favicon.png and b/backend/open_webui/static/favicon.png differ diff --git a/backend/open_webui/tools/builtin.py b/backend/open_webui/tools/builtin.py index dcee72534..2af7e4f01 100644 --- a/backend/open_webui/tools/builtin.py +++ b/backend/open_webui/tools/builtin.py @@ -2868,9 +2868,6 @@ def _event_to_dict(event, tz) -> dict: } - - - async def search_calendar_events( query: Optional[str] = None, start: Optional[str] = None, @@ -2915,7 +2912,11 @@ async def search_calendar_events( return json.dumps({'error': f'Invalid start datetime: {e}'}) try: - end_ns = _dt_to_ns(end, tz) if end else int(time.time() * 1_000) * 1_000_000 + 365 * 86400 * 1_000_000_000_000 + end_ns = ( + _dt_to_ns(end, tz) + if end + else int(time.time() * 1_000) * 1_000_000 + 365 * 86400 * 1_000_000_000_000 + ) except (ValueError, TypeError) as e: return json.dumps({'error': f'Invalid end datetime: {e}'}) @@ -2929,7 +2930,8 @@ async def search_calendar_events( if query: q = query.lower() items = [ - e for e in items + e + for e in items if q in (e.title or '').lower() or q in (e.description or '').lower() or q in (e.location or '').lower() @@ -3229,4 +3231,3 @@ async def delete_calendar_event( except Exception as e: log.exception(f'delete_calendar_event error: {e}') return json.dumps({'error': str(e)}) - diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index d44112b4e..036dc9bf3 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -427,7 +427,11 @@ def _render_openai_tool_call_handler(item: dict, done: bool) -> str: if atype == 'search': queries = action.get('queries') or [] query = action.get('query', '') - summary = f'Search: {", ".join(str(q) for q in queries)}' if queries else (f'Search: {query}' if query else '') + summary = ( + f'Search: {", ".join(str(q) for q in queries)}' + if queries + else (f'Search: {query}' if query else '') + ) elif atype == 'open_page': summary = f'Open page: {action.get("url", "")}' if action.get('url') else '' elif atype == 'find_in_page': @@ -490,9 +494,13 @@ def serialize_output(output: list) -> str: files = result_item.get('files') embeds = result_item.get('embeds', '') - parts.append(f'
\nTool Executed\n{html.escape(json.dumps(result_text, ensure_ascii=False))}\n
') + parts.append( + f'
\nTool Executed\n{html.escape(json.dumps(result_text, ensure_ascii=False))}\n
' + ) else: - parts.append(f'
\nExecuting...\n
') + parts.append( + f'
\nExecuting...\n
' + ) elif item_type == 'function_call_output': # Already handled inline with function_call above @@ -529,9 +537,13 @@ def serialize_output(output: list) -> str: ) if status == 'completed' or duration is not None or not is_last_item: - parts.append(f'
\nThought for {duration or 0} seconds\n{display}\n
') + parts.append( + f'
\nThought for {duration or 0} seconds\n{display}\n
' + ) else: - parts.append(f'
\nThinking…\n{display}\n
') + parts.append( + f'
\nThinking…\n{display}\n
' + ) elif item_type == 'open_webui:code_interpreter': # Code interpreter needs to inspect/mutate prior accumulated content @@ -570,9 +582,13 @@ def serialize_output(output: list) -> str: output_attr = f' output="{html.escape(output_json)}"' if status == 'completed' or duration is not None or not is_last_item: - parts.append(f'
\nAnalyzed\n{display}\n
') + parts.append( + f'
\nAnalyzed\n{display}\n
' + ) else: - parts.append(f'
\nAnalyzing…\n{display}\n
') + parts.append( + f'
\nAnalyzing…\n{display}\n
' + ) return '\n'.join(parts).strip() diff --git a/src/lib/apis/calendar/index.ts b/src/lib/apis/calendar/index.ts index fa75f9a6a..b496a42bc 100644 --- a/src/lib/apis/calendar/index.ts +++ b/src/lib/apis/calendar/index.ts @@ -418,7 +418,6 @@ export const rsvpCalendarEvent = async ( return res; }; - export const searchCalendarEvents = async ( token: string, query: string | null, diff --git a/src/lib/components/calendar/CalendarEventChip.svelte b/src/lib/components/calendar/CalendarEventChip.svelte index c63eac106..b1c3552e6 100644 --- a/src/lib/components/calendar/CalendarEventChip.svelte +++ b/src/lib/components/calendar/CalendarEventChip.svelte @@ -21,7 +21,11 @@ style="background-color: {event.color || calendarColor || '#3b82f6'};" > - {#if !event.all_day}{new Date(event.start_at / 1_000_000).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }).replace(' ', '')}{/if} + {#if !event.all_day}{new Date(event.start_at / 1_000_000) + .toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }) + .replace(' ', '')}{/if} {event.title} diff --git a/src/lib/components/calendar/CalendarEventModal.svelte b/src/lib/components/calendar/CalendarEventModal.svelte index 5540bda2f..020a89f92 100644 --- a/src/lib/components/calendar/CalendarEventModal.svelte +++ b/src/lib/components/calendar/CalendarEventModal.svelte @@ -8,7 +8,11 @@ import Spinner from '$lib/components/common/Spinner.svelte'; import type { CalendarModel, CalendarEventModel, CalendarEventForm } from '$lib/apis/calendar'; - import { createCalendarEvent, updateCalendarEvent, deleteCalendarEvent } from '$lib/apis/calendar'; + import { + createCalendarEvent, + updateCalendarEvent, + deleteCalendarEvent + } from '$lib/apis/calendar'; const i18n = getContext('i18n'); const dispatch = createEventDispatcher(); diff --git a/src/lib/components/calendar/CalendarView.svelte b/src/lib/components/calendar/CalendarView.svelte index 0b67d7de9..5100a58b9 100644 --- a/src/lib/components/calendar/CalendarView.svelte +++ b/src/lib/components/calendar/CalendarView.svelte @@ -21,11 +21,24 @@ const NS = 1_000_000; const DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const MONTH_NAMES = [ - 'January', 'February', 'March', 'April', 'May', 'June', - 'July', 'August', 'September', 'October', 'November', 'December' + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' ]; - $: calColorMap = calendars.reduce((acc, c) => ({ ...acc, [c.id]: c.color }), {} as Record); + $: calColorMap = calendars.reduce( + (acc, c) => ({ ...acc, [c.id]: c.color }), + {} as Record + ); $: filteredEvents = events.filter((e) => visibleCalendarIds.has(e.calendar_id)); // Pre-group events by day key so the template reactively updates when events change @@ -102,7 +115,11 @@ }); } - function getEventsForHour(day: Date, hour: number, eventsList: CalendarEventModel[] = filteredEvents): CalendarEventModel[] { + function getEventsForHour( + day: Date, + hour: number, + eventsList: CalendarEventModel[] = filteredEvents + ): CalendarEventModel[] { const hourStartMs = new Date(day.getFullYear(), day.getMonth(), day.getDate(), hour).getTime(); const hourEndMs = hourStartMs + 3_600_000; return eventsList.filter((e) => { @@ -157,9 +174,10 @@ dispatch('eventClick', event); } - $: headerText = view === 'day' - ? `${DAY_NAMES[currentDate.getDay()]}, ${MONTH_NAMES[currentDate.getMonth()]} ${currentDate.getDate()}, ${currentDate.getFullYear()}` - : `${MONTH_NAMES[currentDate.getMonth()]} ${currentDate.getFullYear()}`; + $: headerText = + view === 'day' + ? `${DAY_NAMES[currentDate.getDay()]}, ${MONTH_NAMES[currentDate.getMonth()]} ${currentDate.getDate()}, ${currentDate.getFullYear()}` + : `${MONTH_NAMES[currentDate.getMonth()]} ${currentDate.getFullYear()}`;
@@ -168,7 +186,10 @@
{#if $mobile}
- + -
@@ -232,7 +285,19 @@ class="md:hidden px-2 py-1.5 rounded-xl bg-black text-white dark:bg-white dark:text-black transition text-sm flex items-center" on:click={() => dispatch('newEvent')} > - +
@@ -244,13 +309,19 @@
{#each DAY_NAMES as day} -
{$i18n.t(day)}
+
+ {$i18n.t(day)} +
{/each}
-
+
{#each monthDays as day, i} - {@const dayKey = new Date(day.getFullYear(), day.getMonth(), day.getDate()).getTime().toString()} + {@const dayKey = new Date(day.getFullYear(), day.getMonth(), day.getDate()) + .getTime() + .toString()} {@const dayEvents = eventsByDay[dayKey] || []} {@const col = i % 7} {@const row = Math.floor(i / 7)} @@ -293,18 +364,34 @@
- + {:else if view === 'week'}
-
+
-
+
{#each weekDays as day} -
-
{DAY_NAMES[day.getDay()]}
-
+
+
+ {DAY_NAMES[day.getDay()]} +
+
{day.getDate()}
@@ -313,12 +400,22 @@
{#each hours as hour} -
-
{hour > 0 ? formatHour(hour) : ''}
+
+
+ {hour > 0 ? formatHour(hour) : ''} +
{#each weekDays as day} {@const hourEvents = getEventsForHour(day, hour, filteredEvents)}
- + {:else}
-
+
{#each hours as hour} {@const hourEvents = getEventsForHour(currentDate, hour, filteredEvents)} -
-
{hour > 0 ? formatHour(hour) : ''}
+
+
+ {hour > 0 ? formatHour(hour) : ''} +