openai/chatkit-python

Public

mirrored fromhttps://github.com/openai/chatkit-pythonAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.6.5

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

tests/helpers/mock_widget.py

1910lines · modecode

1import os
2import re
3import uuid
4from datetime import datetime, timedelta
5from typing import Annotated, Any, AsyncIterator, Callable, Literal
6
7from agents import Agent, Runner
8from anyio import sleep
9from pydantic import BaseModel, Field, TypeAdapter
10from typing_extensions import assert_never
11
12from chatkit.actions import Action, ActionConfig
13from chatkit.types import ThreadStreamEvent
14from chatkit.widgets import (
15 Box,
16 Button,
17 Card,
18 Col,
19 DatePicker,
20 Divider,
21 Icon,
22 Image,
23 ListView,
24 ListViewItem,
25 Markdown,
26 Row,
27 Select,
28 Spacer,
29 Text,
30 Title,
31 Transition,
32 WidgetComponent,
33 WidgetRoot,
34 WidgetStatus,
35)
36
37from .email_data import (
38 BRAINSTORM_SESSION,
39 CHATKIT_ROADMAP,
40 FLIGHT_REMINDER,
41)
42
43# HELPERS
44
45
46def _gen_id(prefix: str) -> str:
47 return f"{prefix}_{uuid.uuid4().hex}"
48
49
50chatkit_address = os.getenv("CHATKIT_ADDRESS") or "http://localhost:3000"
51
52
53def asset(path: str) -> str:
54 """
55 Helper function to create an asset URL.
56 """
57 return f"{chatkit_address}/{path}"
58
59
60## STATE MODELS
61
62
63class DraftTask(BaseModel):
64 title: str
65 description: str
66 priority: Literal["low", "high"] = "low"
67 timeframe: Literal["today", "tomorrow", "week", "month", "custom"] = "tomorrow"
68 due_date: datetime | None = None
69
70 def model_post_init(self, __context) -> None:
71 if self.due_date is None:
72 self.due_date = self._calculate_due_date()
73
74 def _calculate_due_date(self) -> datetime:
75 now = datetime.now()
76 if self.timeframe == "today":
77 return now.replace(hour=23, minute=59, second=59, microsecond=0)
78 elif self.timeframe == "tomorrow":
79 return (now + timedelta(days=1)).replace(
80 hour=23, minute=59, second=59, microsecond=0
81 )
82 elif self.timeframe == "week":
83 # End of this week (Sunday)
84 days_until_sunday = 6 - now.weekday()
85 return (now + timedelta(days=days_until_sunday)).replace(
86 hour=23, minute=59, second=59, microsecond=0
87 )
88 elif self.timeframe == "month":
89 # End of this month
90 if now.month == 12:
91 next_month = now.replace(year=now.year + 1, month=1, day=1)
92 else:
93 next_month = now.replace(month=now.month + 1, day=1)
94 return (next_month - timedelta(days=1)).replace(
95 hour=23, minute=59, second=59, microsecond=0
96 )
97 else: # custom - treat as today for now
98 return now.replace(hour=23, minute=59, second=59, microsecond=0)
99
100 def priority_color(self) -> str:
101 if self.priority == "high":
102 return "red-400"
103 return "secondary"
104
105 def urgency_color(self) -> str:
106 if self.due_date is None:
107 self.due_date = self._calculate_due_date()
108
109 now = datetime.now(self.due_date.tzinfo)
110 due = self.due_date
111 days_until_due = (due - now).days
112
113 if days_until_due < 1:
114 return "red-400"
115
116 if days_until_due < 3:
117 return "yellow-600"
118
119 return "secondary"
120
121 def humanized_due_date(self) -> str:
122 if self.due_date is None:
123 self.due_date = self._calculate_due_date()
124
125 now = datetime.now(self.due_date.tzinfo)
126 due = self.due_date
127
128 # Check if it's overdue
129 if due < now:
130 days_overdue = (now - due).days
131 if days_overdue == 0:
132 return "Due today"
133 elif days_overdue == 1:
134 return "Due yesterday"
135 else:
136 return f"Due {days_overdue} days ago"
137
138 # Check future dates
139 days_until_due = (due - now).days
140
141 if days_until_due == 0:
142 return "Due today"
143 elif days_until_due == 1:
144 return "Due tomorrow"
145 elif days_until_due <= 7:
146 return f"Due in {days_until_due} days"
147 elif days_until_due <= 14:
148 return "Due next week"
149 else:
150 return f"Due on {due.strftime('%b %d')}"
151
152
153class DraftCalendarEvent(BaseModel):
154 title: str
155 description: str
156 date: str
157 start_time: str
158 end_time: str
159 calendar: Literal["Work", "Personal"]
160
161 def calendar_color(self) -> str:
162 return "blue-400" if self.calendar == "Work" else "red-400"
163
164 def time(self) -> str:
165 return f"{self.start_time} - {self.end_time}"
166
167
168class DraftEmail(BaseModel):
169 subject: str
170 body: str
171 to: str
172
173
174class Email(DraftEmail):
175 id: str = Field(default_factory=lambda: _gen_id("email"))
176 sender_image: str
177 sender: str
178 sender_type: Literal["org", "person"]
179 sent_at: str
180
181
182class Task(DraftTask):
183 id: str = Field(default_factory=lambda: _gen_id("task"))
184 completed: bool
185
186
187class CalendarEvent(DraftCalendarEvent):
188 id: str = Field(default_factory=lambda: _gen_id("event"))
189
190
191class BaseWidgetView(BaseModel):
192 status_text: str | None = None
193 status_icon: str = "favicon.svg"
194 collapsed: bool = False
195
196 def status(self) -> WidgetStatus | None:
197 if not self.status_text:
198 return None
199 return {"text": self.status_text, "favicon": asset(self.status_icon)}
200
201
202class EmailDraft(BaseWidgetView):
203 type: Literal["email_draft"] = Field(default="email_draft", frozen=True)
204 email: DraftEmail
205 streaming: bool
206 status_icon: str = "gmail-send-status-icon.png"
207
208
209class EmailView(BaseWidgetView):
210 type: Literal["email_view"] = Field(default="email_view", frozen=True)
211 email: Email
212 show_back_button: bool
213 status_icon: str = "gmail-status-icon.png"
214
215
216class EmailsList(BaseWidgetView):
217 type: Literal["emails_list"] = Field(default="emails_list", frozen=True)
218 emails: list[Email]
219 status_icon: str = "gmail-status-icon.png"
220
221
222class TaskDraft(BaseWidgetView):
223 type: Literal["task_draft"] = Field(default="task_draft", frozen=True)
224 todo: DraftTask
225 status_icon: str = "linear-status-icon.png"
226
227
228class TaskView(BaseWidgetView):
229 type: Literal["task_view"] = Field(default="task_view", frozen=True)
230 task: Task
231 show_back_button: bool
232 status_icon: str = "linear-status-icon.png"
233
234
235class TasksList(BaseWidgetView):
236 type: Literal["tasks_list"] = Field(default="tasks_list", frozen=True)
237 tasks: list[Task]
238 status_icon: str = "linear-status-icon.png"
239
240
241class CalendarEventDraft(BaseWidgetView):
242 type: Literal["calendar_event_draft"] = Field(
243 default="calendar_event_draft", frozen=True
244 )
245 event: DraftCalendarEvent
246 status_icon: str = "calendar-status-icon.png"
247
248
249class CalendarEventView(BaseWidgetView):
250 type: Literal["calendar_event_view"] = Field(
251 default="calendar_event_view", frozen=True
252 )
253 event: CalendarEvent
254 show_back_button: bool
255 status_icon: str = "calendar-status-icon.png"
256
257
258class CalendarEventsList(BaseWidgetView):
259 type: Literal["calendar_events_list"] = Field(
260 default="calendar_events_list", frozen=True
261 )
262 events: list[CalendarEvent]
263 status_icon: str = "calendar-status-icon.png"
264
265
266class Index(BaseWidgetView):
267 type: Literal["index"] = Field(default="index", frozen=True)
268 selected: Literal["email", "calendar", "tasks", "index"]
269
270 def get_section_icon(self, section: str) -> str:
271 """Get the appropriate icon for each section"""
272 icon_map = {
273 "email": "gmail-status-icon.png",
274 "calendar": "calendar-status-icon.png",
275 "tasks": "linear-status-icon.png",
276 "index": "favicon.svg",
277 }
278 return icon_map.get(section, "favicon.svg")
279
280 def __init__(self, **data):
281 super().__init__(**data)
282 # Set status_icon based on current selection
283 if self.selected:
284 self.status_icon = self.get_section_icon(self.selected)
285 else:
286 self.status_icon = "favicon.svg"
287
288
289State = Annotated[
290 EmailDraft
291 | EmailView
292 | EmailsList
293 | TaskDraft
294 | TaskView
295 | TasksList
296 | CalendarEventDraft
297 | CalendarEventView
298 | CalendarEventsList
299 | Index,
300 Field(discriminator="type"),
301]
302
303
304## ACTIONS
305
306
307class BasePayload(BaseModel):
308 widget_id: str
309
310
311class ShowWidgetPayload(BasePayload):
312 widget: Literal["email", "calendar", "tasks", "index"]
313
314
315class SendEmailPayload(BasePayload):
316 email: DraftEmail
317
318
319class ViewEmailPayload(BasePayload):
320 email_id: str
321
322
323class UpdateDraftTaskPayload(BasePayload):
324 todo: DraftTask
325
326
327class CreateTaskPayload(BasePayload):
328 todo: DraftTask
329
330
331class ViewTaskPayload(BasePayload):
332 task_id: str
333
334
335class ToggleTaskCompletePayload(BasePayload):
336 task_id: str
337
338
339class CreateEventPayload(BasePayload):
340 event: DraftCalendarEvent
341
342
343class ViewEventPayload(BasePayload):
344 event_id: str
345
346
347ShowWidgetAction = Action[Literal["sample.show_widget"], ShowWidgetPayload]
348DraftEmailAction = Action[Literal["sample.draft_email"], BasePayload]
349ShowInboxAction = Action[Literal["sample.show_inbox"], BasePayload]
350SendEmailAction = Action[Literal["sample.send_email"], SendEmailPayload]
351DiscardEmailAction = Action[Literal["sample.discard_email"], BasePayload]
352ViewEmailAction = Action[Literal["sample.view_email"], ViewEmailPayload]
353DraftTaskAction = Action[Literal["sample.draft_task"], BasePayload]
354UpdateDraftTaskAction = Action[
355 Literal["sample.update_draft_task"], UpdateDraftTaskPayload
356]
357CreateTaskAction = Action[Literal["sample.create_task"], CreateTaskPayload]
358CancelTaskAction = Action[Literal["sample.cancel_task"], BasePayload]
359ViewTasksAction = Action[Literal["sample.view_tasks"], BasePayload]
360ViewTaskAction = Action[Literal["sample.view_task"], ViewTaskPayload]
361ToggleTaskCompleteAction = Action[
362 Literal["sample.toggle_task_complete"], ToggleTaskCompletePayload
363]
364DraftEventAction = Action[Literal["sample.draft_event"], BasePayload]
365CreateEventAction = Action[Literal["sample.create_event"], CreateEventPayload]
366DiscardEventAction = Action[Literal["sample.discard_event"], BasePayload]
367ViewScheduleAction = Action[Literal["sample.view_schedule"], BasePayload]
368ViewEventAction = Action[Literal["sample.view_event"], ViewEventPayload]
369
370
371SampleWidgetAction = Annotated[
372 ShowWidgetAction
373 | DraftEmailAction
374 | ShowInboxAction
375 | SendEmailAction
376 | DiscardEmailAction
377 | ViewEmailAction
378 | DraftTaskAction
379 | CreateTaskAction
380 | CancelTaskAction
381 | ViewTasksAction
382 | ViewTaskAction
383 | ToggleTaskCompleteAction
384 | UpdateDraftTaskAction
385 | DraftEventAction
386 | CreateEventAction
387 | DiscardEventAction
388 | ViewScheduleAction
389 | ViewEventAction,
390 Field(discriminator="type"),
391]
392
393ActionAdapter: TypeAdapter[SampleWidgetAction] = TypeAdapter(SampleWidgetAction)
394
395
396email_generator = Agent(
397 model="gpt-4.1",
398 name="Description generator",
399 instructions="""Generate an email about a given subject. Do not include any other text than the email. Do not include the subject itself. The email should end with "\n\nThanks,\nZach"
400 """,
401)
402
403
404class ActionOutput(BaseModel):
405 widget: WidgetRoot | None = None
406 event: ThreadStreamEvent | None = None
407
408 @staticmethod
409 def from_event(event: ThreadStreamEvent) -> "ActionOutput":
410 return ActionOutput(widget=None, event=event)
411
412 @staticmethod
413 def from_widget(widget: WidgetRoot) -> "ActionOutput":
414 return ActionOutput(widget=widget, event=None)
415
416
417class SampleWidget(BaseModel):
418 id: str = Field(default_factory=lambda: _gen_id("wig"))
419 emails: list[Email] = []
420 tasks: list[Task] = []
421 events: list[CalendarEvent] = []
422 state: State = Index(selected="index", status_text="Fetched widgets")
423
424 @staticmethod
425 def parse_action(action: Action[str, Any]) -> SampleWidgetAction:
426 return ActionAdapter.validate_python(action.model_dump())
427
428 def load_events(self):
429 self.events = [
430 CalendarEvent(
431 title="Dentist appointment",
432 description="Regular checkup and cleaning",
433 date="Wed, July 16",
434 start_time="2:00",
435 end_time="3:00 PM",
436 calendar="Personal",
437 ),
438 CalendarEvent(
439 title="Team standup",
440 description="Daily team sync meeting",
441 date="Thu, July 17",
442 start_time="3:30",
443 end_time="4:00 PM",
444 calendar="Work",
445 ),
446 CalendarEvent(
447 title="Q3 roadmap review",
448 description="Quarterly planning session",
449 date="Fri, July 18",
450 start_time="9:00",
451 end_time="10:30 AM",
452 calendar="Work",
453 ),
454 CalendarEvent(
455 title="Lunch with Sarah",
456 description="Catch up over lunch",
457 date="Sat, July 19",
458 start_time="12:00",
459 end_time="1:30 PM",
460 calendar="Personal",
461 ),
462 ]
463
464 def load_emails(self):
465 self.emails = [
466 Email(
467 sender="David Weedon",
468 sender_image=asset("david.png"),
469 sender_type="person",
470 subject="ChatKit Roadmap",
471 body=CHATKIT_ROADMAP,
472 to="zach@chatkit.studio",
473 sent_at="8:40 AM",
474 ),
475 Email(
476 sender="United Airlines",
477 sender_image=asset("united.png"),
478 sender_type="org",
479 subject="Quick reminders about your upcoming trip to San Francisco",
480 body=FLIGHT_REMINDER,
481 to="zach@chatkit.studio",
482 sent_at="8:12 AM",
483 ),
484 Email(
485 sender="Tyler Smith",
486 sender_image=asset("tyler.png"),
487 sender_type="person",
488 subject="re: Morning brainstorm",
489 body=BRAINSTORM_SESSION,
490 to="zach@chatkit.studio",
491 sent_at="Yesterday",
492 ),
493 ]
494
495 def load_tasks(self):
496 self.tasks = [
497 Task(
498 title="Add source annotations to responses",
499 description="Implement source annotations feature to add context to assistant responses. This is currently marked as 'In progress' in the roadmap.",
500 priority="high",
501 timeframe="today",
502 completed=False,
503 ),
504 Task(
505 title="Fix mobile web scrolling issues",
506 description="Address the rough mobile web experience, particularly scrolling and input caret tracking issues mentioned in known issues.",
507 priority="high",
508 timeframe="tomorrow",
509 completed=False,
510 ),
511 Task(
512 title="Implement graceful page refresh handling",
513 description="Fix the issue where refreshing the page while waiting for an assistant message causes the message to be dropped entirely.",
514 priority="high",
515 timeframe="week",
516 completed=False,
517 ),
518 Task(
519 title="Design enhanced interactive widgets",
520 description="Develop richer interactive elements for the widget system. This is planned for the upcoming roadmap.",
521 priority="high",
522 timeframe="month",
523 completed=True,
524 ),
525 Task(
526 title="Add robust attachment error handling",
527 description="Implement proper error handling for attachments to prevent items from staying in uploading state indefinitely.",
528 priority="high",
529 timeframe="week",
530 completed=False,
531 ),
532 Task(
533 title="Implement source scope selection",
534 description="Add the ability for users to choose specific data sources when interacting with the assistant.",
535 priority="low",
536 timeframe="month",
537 completed=False,
538 ),
539 Task(
540 title="Add custom fonts support",
541 description="Enable users to use different typefaces in their ChatKit interface. This is planned for customization features.",
542 priority="low",
543 timeframe="month",
544 completed=False,
545 ),
546 Task(
547 title="Implement dark mode theme switching",
548 description="Complete the theme switching functionality between light and dark modes with proper system integration.",
549 priority="low",
550 timeframe="tomorrow",
551 completed=True,
552 ),
553 Task(
554 title="Set up ChatKit Explorer development environment",
555 description="Configure the complete development setup including dependencies, environment variables, and make commands for local development.",
556 priority="low",
557 timeframe="today",
558 completed=True,
559 ),
560 ]
561
562 def render(
563 self,
564 state: State | None = None,
565 ) -> WidgetRoot:
566 if state is not None:
567 self.state = state
568
569 if self.state.type == "index":
570 return render_index(self.id, self.state)
571 if self.state.type == "email_draft":
572 return render_email_draft(self.id, self.state)
573 if self.state.type == "email_view":
574 return render_email_view(self.id, self.state)
575 if self.state.type == "emails_list":
576 return render_emails_list(self.id, self.state)
577 if self.state.type == "task_draft":
578 return render_task_draft(self.id, self.state)
579 if self.state.type == "task_view":
580 return render_task_view(self.id, self.state)
581 if self.state.type == "tasks_list":
582 return render_tasks_list(self.id, self.state)
583 if self.state.type == "calendar_event_draft":
584 return render_calendar_event_draft(self.id, self.state)
585 if self.state.type == "calendar_event_view":
586 return render_calendar_event_view(self.id, self.state)
587 if self.state.type == "calendar_events_list":
588 return render_calendar_events_list(self.id, self.state)
589
590 assert_never(self.state)
591
592 def render_with_status(self, text: str | None) -> WidgetRoot:
593 self.state.status_text = text
594 return self.render()
595
596 async def save_and_generate(
597 self,
598 next_state: State,
599 generate: Callable[[], AsyncIterator[ThreadStreamEvent]],
600 save: Callable[[], AsyncIterator[ThreadStreamEvent]],
601 ):
602 async for event in save():
603 yield ActionOutput.from_event(event)
604
605 first = True
606 async for event in generate():
607 if first:
608 first = False
609 yield ActionOutput.from_widget(self.render(next_state))
610 yield ActionOutput.from_event(event)
611
612 async def handle_action(
613 self,
614 action: SampleWidgetAction,
615 generate: Callable[[], AsyncIterator[ThreadStreamEvent]],
616 save: Callable[[], AsyncIterator[ThreadStreamEvent]],
617 ) -> AsyncIterator[ActionOutput]:
618 if action.type == "sample.show_widget":
619 next_state = Index(
620 selected=action.payload.widget,
621 status_text=f"Fetched {action.payload.widget} widget"
622 if action.payload.widget != "index"
623 else "Fetched widgets",
624 )
625 yield ActionOutput.from_widget(self.render(next_state))
626
627 elif action.type == "sample.draft_email":
628 with open("../README.md", "r") as f:
629 readme = f.read()
630
631 body_text = Runner.run_streamed(
632 email_generator,
633 "Draft an email asking David about the current status of the ChatKit roadmap. Keep it short and high level, ask questions about some of the items in the readme. Current status in the README:\n\n"
634 + readme,
635 )
636
637 next_email_state: EmailDraft = EmailDraft(
638 status_text="Drafting email",
639 email=DraftEmail(
640 subject="ChatKit Roadmap",
641 body="",
642 to="david@chatkit.studio",
643 ),
644 streaming=True,
645 )
646 yield ActionOutput.from_widget(self.render(next_email_state))
647
648 async for event in body_text.stream_events():
649 if event.type == "raw_response_event":
650 if event.data.type == "response.output_text.delta":
651 next_email_state.email.body += event.data.delta
652 next_email_state.streaming = True
653 yield ActionOutput.from_widget(self.render(next_email_state))
654
655 next_email_state.status_text = "Drafted email"
656 next_email_state.streaming = False
657 yield ActionOutput.from_widget(self.render(next_email_state))
658
659 elif action.type == "sample.show_inbox":
660 if not self.emails:
661 yield ActionOutput.from_widget(
662 self.render_with_status("Fetching inbox")
663 )
664 await sleep(2)
665 self.load_emails()
666 next_state = EmailsList(status_text="Fetched inbox", emails=self.emails)
667 yield ActionOutput.from_widget(self.render(next_state))
668
669 elif action.type == "sample.send_email":
670 if self.state.type != "email_draft":
671 raise ValueError("Invalid state for sending email")
672 self.state.email = action.payload.email
673 self.state.status_text = "Sending"
674 yield ActionOutput.from_widget(self.render())
675 email = Email(
676 sender="Zach Johnston",
677 sender_image=asset("zach.png"),
678 sender_type="person",
679 sent_at="Just now",
680 subject=action.payload.email.subject,
681 body=action.payload.email.body,
682 to=action.payload.email.to,
683 )
684 next_state = EmailView(
685 status_text="Sent email",
686 status_icon="gmail-send-status-icon.png",
687 email=email,
688 show_back_button=False,
689 )
690 async for event in self.save_and_generate(next_state, generate, save):
691 yield event
692
693 elif action.type == "sample.discard_email":
694 yield ActionOutput.from_widget(self.render_with_status("Discarding"))
695 self.state.status_text = "Discarded email draft"
696 self.state.collapsed = True
697 async for event in self.save_and_generate(self.state, generate, save):
698 yield event
699
700 elif action.type == "sample.view_email":
701 for email in self.emails:
702 if email.id == action.payload.email_id:
703 next_state = EmailView(
704 status_text=self.state.status_text,
705 email=email,
706 show_back_button=True,
707 )
708 yield ActionOutput.from_widget(self.render(next_state))
709 break
710 else:
711 raise ValueError(f"Email with id {action.payload.email_id} not found")
712
713 elif action.type == "sample.draft_task":
714 next_state = TaskDraft(
715 status_text="Drafted task",
716 todo=DraftTask(
717 title="Design resizable popup mode",
718 description="Create a design proposal for how ChatKit's popup mode can support dynamic height and user resizing.",
719 priority="low",
720 timeframe="tomorrow",
721 ),
722 )
723 yield ActionOutput.from_widget(self.render(next_state))
724
725 elif action.type == "sample.create_task":
726 if self.state.type != "task_draft":
727 raise ValueError("Invalid state for creating task")
728 self.state.todo = action.payload.todo
729 self.state.status_text = "Creating task"
730 yield ActionOutput.from_widget(self.render())
731 task = Task(
732 title=action.payload.todo.title,
733 description=action.payload.todo.description,
734 priority=action.payload.todo.priority,
735 timeframe=action.payload.todo.timeframe,
736 due_date=action.payload.todo.due_date,
737 completed=False,
738 )
739 self.tasks.append(task)
740 next_state = TaskView(
741 status_text="Created task",
742 task=task,
743 show_back_button=False,
744 )
745 async for event in self.save_and_generate(next_state, generate, save):
746 yield event
747
748 elif action.type == "sample.cancel_task":
749 yield ActionOutput.from_widget(self.render_with_status("Discarding"))
750 self.state.status_text = "Discarded task draft"
751 self.state.collapsed = True
752 async for event in self.save_and_generate(self.state, generate, save):
753 yield event
754
755 elif action.type == "sample.view_tasks":
756 if not self.tasks:
757 yield ActionOutput.from_widget(
758 self.render_with_status("Fetching tasks")
759 )
760 await sleep(2)
761 self.load_tasks()
762 next_state = TasksList(status_text="Fetched tasks", tasks=self.tasks)
763 yield ActionOutput.from_widget(self.render(next_state))
764
765 elif action.type == "sample.view_task":
766 for task in self.tasks:
767 if task.id == action.payload.task_id:
768 next_state = TaskView(
769 status_text=self.state.status_text,
770 task=task,
771 show_back_button=True,
772 )
773 yield ActionOutput.from_widget(self.render(next_state))
774 break
775 else:
776 raise ValueError(f"Task with id {action.payload.task_id} not found")
777
778 elif action.type == "sample.toggle_task_complete":
779 for task in self.tasks:
780 if task.id == action.payload.task_id:
781 task.completed = not task.completed
782 break
783 else:
784 raise ValueError(f"Task with id {action.payload.task_id} not found")
785
786 if self.state.type == "tasks_list":
787 self.state.tasks = self.tasks
788 elif self.state.type == "task_view":
789 self.state.task = task
790
791 yield ActionOutput.from_widget(self.render())
792
793 elif action.type == "sample.update_draft_task":
794 if self.state.type != "task_draft":
795 raise ValueError("Invalid draft task update")
796 self.state.todo = action.payload.todo
797 yield ActionOutput.from_widget(self.render())
798
799 elif action.type == "sample.draft_event":
800 next_state = CalendarEventDraft(
801 status_text="Drafted calendar event",
802 event=DraftCalendarEvent(
803 title="Q3 roadmap review",
804 description="Quarterly planning session to review and align on the Q3 roadmap.",
805 date="Wed 16",
806 start_time="9:00",
807 end_time="10:30 AM",
808 calendar="Work",
809 ),
810 )
811 yield ActionOutput.from_widget(self.render(next_state))
812
813 elif action.type == "sample.create_event":
814 yield ActionOutput.from_widget(
815 self.render_with_status("Creating calendar event")
816 )
817 event = CalendarEvent(
818 title=action.payload.event.title,
819 date=action.payload.event.date,
820 start_time=action.payload.event.start_time,
821 end_time=action.payload.event.end_time,
822 description=action.payload.event.description,
823 calendar=action.payload.event.calendar,
824 )
825 self.events.append(event)
826 next_state = CalendarEventView(
827 status_text="Added event to calendar",
828 event=event,
829 show_back_button=False,
830 )
831 async for event in self.save_and_generate(next_state, generate, save):
832 yield event
833
834 elif action.type == "sample.discard_event":
835 yield ActionOutput.from_widget(self.render_with_status("Discarding"))
836 self.state.status_text = "Discarded draft event"
837 self.state.collapsed = True
838 async for event in self.save_and_generate(self.state, generate, save):
839 yield event
840
841 elif action.type == "sample.view_schedule":
842 if not self.events:
843 yield ActionOutput.from_widget(
844 self.render_with_status("Fetching schedule")
845 )
846 await sleep(2)
847 self.load_events()
848 next_state = CalendarEventsList(
849 status_text="Fetched schedule", events=self.events
850 )
851 yield ActionOutput.from_widget(self.render(next_state))
852
853 elif action.type == "sample.view_event":
854 for event in self.events:
855 if event.id == action.payload.event_id:
856 next_state = CalendarEventView(
857 status_text=self.state.status_text,
858 event=event,
859 show_back_button=True,
860 )
861 yield ActionOutput.from_widget(self.render(next_state))
862 break
863 else:
864 raise ValueError(f"Event with id {action.payload.event_id} not found")
865
866 else:
867 assert_never(action)
868
869
870# HELPER TEMPLATES
871
872
873def back_button_list_item(action: ActionConfig):
874 action.loadingBehavior = "container"
875 return ListViewItem(
876 onClickAction=action,
877 gap=3,
878 children=[
879 Button(
880 size="3xs",
881 iconStart="chevron-left",
882 color="primary",
883 variant="soft",
884 iconSize="sm",
885 label="",
886 pill=True,
887 uniform=True,
888 onClickAction=action,
889 ),
890 Text(value="Back", color="emphasis"),
891 ],
892 )
893
894
895# INDEX TEMPLATES
896
897
898def render_index(id: str, state: Index):
899 if state.selected == "index":
900 return render_widget_list(id, state)
901 if state.selected == "email":
902 return render_email_widget_list(id, state)
903 if state.selected == "calendar":
904 return render_calendar_widget_list(id, state)
905 if state.selected == "tasks":
906 return render_tasks_widget_list(id, state)
907
908 assert_never(state.selected)
909
910
911def render_widget_list(id: str, state: Index):
912 return ListView(
913 status=state.status(),
914 key="index.pick",
915 children=[
916 ListViewItem(
917 onClickAction=ShowWidgetAction.create(
918 ShowWidgetPayload(widget="email", widget_id=id),
919 loading_behavior="container",
920 ),
921 gap=3,
922 children=[
923 Image(
924 src=asset("gmail-list-icon.png"),
925 size="60px",
926 frame=True,
927 ),
928 Col(
929 children=[
930 Text(
931 value="Email widget",
932 weight="medium",
933 color="emphasis",
934 ),
935 Text(
936 value="Craft and preview an email before sending",
937 color="secondary",
938 ),
939 ]
940 ),
941 ],
942 ),
943 ListViewItem(
944 onClickAction=ShowWidgetAction.create(
945 ShowWidgetPayload(widget="calendar", widget_id=id),
946 loading_behavior="container",
947 ),
948 gap=3,
949 children=[
950 Image(
951 src=asset("calendar-list-icon.png"),
952 size="60px",
953 frame=True,
954 ),
955 Col(
956 children=[
957 Text(
958 value="Calendar widget",
959 weight="medium",
960 color="emphasis",
961 ),
962 Text(
963 value="Add events to your calendar",
964 color="secondary",
965 ),
966 ]
967 ),
968 ],
969 ),
970 ListViewItem(
971 onClickAction=ShowWidgetAction.create(
972 ShowWidgetPayload(widget="tasks", widget_id=id),
973 loading_behavior="container",
974 ),
975 gap=3,
976 children=[
977 Image(
978 src=asset("linear-list-icon.png"),
979 size="60px",
980 frame=True,
981 ),
982 Col(
983 children=[
984 Text(
985 value="Tasks widget",
986 weight="medium",
987 color="emphasis",
988 ),
989 Text(
990 value="Manage your tasks and to-dos",
991 color="secondary",
992 ),
993 ]
994 ),
995 ],
996 ),
997 ],
998 )
999
1000
1001def render_email_widget_list(id: str, state: Index):
1002 return ListView(
1003 status=state.status(),
1004 key="index.email",
1005 children=[
1006 back_button_list_item(
1007 ShowWidgetAction.create(ShowWidgetPayload(widget="index", widget_id=id))
1008 ),
1009 ListViewItem(
1010 onClickAction=ShowInboxAction.create(
1011 BasePayload(widget_id=id), loading_behavior="container"
1012 ),
1013 gap=3,
1014 children=[
1015 Image(
1016 src=asset("gmail-inbox-icon.png"),
1017 size="40px",
1018 frame=True,
1019 ),
1020 Text(value="View inbox", color="emphasis"),
1021 ],
1022 ),
1023 ListViewItem(
1024 onClickAction=DraftEmailAction.create(BasePayload(widget_id=id)),
1025 gap=3,
1026 children=[
1027 Image(
1028 src=asset("gmail-send-icon.png"),
1029 size="40px",
1030 frame=True,
1031 ),
1032 Text(value="Send an email", color="emphasis"),
1033 ],
1034 ),
1035 ],
1036 )
1037
1038
1039def render_tasks_widget_list(id: str, state: Index):
1040 return ListView(
1041 status=state.status(),
1042 key="index.tasks",
1043 children=[
1044 back_button_list_item(
1045 action=ShowWidgetAction.create(
1046 ShowWidgetPayload(widget="index", widget_id=id)
1047 )
1048 ),
1049 ListViewItem(
1050 onClickAction=ViewTasksAction.create(
1051 BasePayload(widget_id=id), loading_behavior="container"
1052 ),
1053 gap=3,
1054 children=[
1055 Image(src=asset("linear-view-icon.png"), size=40, frame=True),
1056 Text(value="View tasks", color="emphasis"),
1057 ],
1058 ),
1059 ListViewItem(
1060 onClickAction=DraftTaskAction.create(
1061 BasePayload(widget_id=id), loading_behavior="container"
1062 ),
1063 gap=3,
1064 children=[
1065 Image(src=asset("linear-create-icon.png"), size=40, frame=True),
1066 Text(value="Create a task", color="emphasis"),
1067 ],
1068 ),
1069 ],
1070 )
1071
1072
1073def render_calendar_widget_list(id: str, state: Index):
1074 return ListView(
1075 status=state.status(),
1076 key="index.calendar",
1077 children=[
1078 back_button_list_item(
1079 action=ShowWidgetAction.create(
1080 ShowWidgetPayload(widget="index", widget_id=id)
1081 )
1082 ),
1083 ListViewItem(
1084 onClickAction=ViewScheduleAction.create(
1085 BasePayload(widget_id=id), loading_behavior="container"
1086 ),
1087 gap=3,
1088 children=[
1089 Image(
1090 src=asset("calendar-schedule-icon.png"),
1091 size="40px",
1092 frame=True,
1093 ),
1094 Text(value="View schedule", color="emphasis"),
1095 ],
1096 ),
1097 ListViewItem(
1098 onClickAction=DraftEventAction.create(
1099 BasePayload(widget_id=id), loading_behavior="container"
1100 ),
1101 gap=3,
1102 children=[
1103 Image(
1104 src=asset("calendar-create-icon.png"),
1105 size="40px",
1106 frame=True,
1107 ),
1108 Text(value="Create an event", color="emphasis"),
1109 ],
1110 ),
1111 ],
1112 )
1113
1114
1115# EMAIL TEMPLATES
1116
1117
1118def render_email_draft(id: str, state: EmailDraft):
1119 return Card(
1120 key="email.draft",
1121 size="lg",
1122 status=state.status(),
1123 collapsed=state.collapsed,
1124 asForm=True,
1125 confirm={
1126 "label": "Send email",
1127 "action": SendEmailAction.create(
1128 SendEmailPayload(email=state.email, widget_id=id),
1129 loading_behavior="self",
1130 ),
1131 }
1132 if not state.collapsed
1133 else None,
1134 cancel={
1135 "label": "Discard",
1136 "action": DiscardEmailAction.create(BasePayload(widget_id=id)),
1137 }
1138 if not state.collapsed
1139 else None,
1140 children=[
1141 Col(
1142 gap=3,
1143 children=[
1144 Row(
1145 gap=4,
1146 align="baseline",
1147 children=[
1148 Text(
1149 value="TO",
1150 weight="semibold",
1151 color="tertiary",
1152 size="xs",
1153 width=64,
1154 ),
1155 Text(value=state.email.to),
1156 ],
1157 ),
1158 Divider(),
1159 Row(
1160 gap=4,
1161 align="baseline",
1162 children=[
1163 Text(
1164 value="SUBJECT",
1165 weight="semibold",
1166 color="tertiary",
1167 size="xs",
1168 width=64,
1169 ),
1170 Text(
1171 value=state.email.subject,
1172 editable={"name": "subject", "required": True}
1173 if not state.collapsed
1174 and state.status_text != "Sending"
1175 else False,
1176 ),
1177 ],
1178 ),
1179 Divider(),
1180 Row(
1181 flex="auto",
1182 children=[
1183 Text(
1184 width="100%",
1185 value=state.email.body,
1186 streaming=state.streaming,
1187 id="email_body",
1188 minLines=10,
1189 editable={
1190 "name": "body",
1191 "autoFocus": True,
1192 "required": True,
1193 }
1194 if not state.collapsed
1195 and state.status_text != "Sending"
1196 else False,
1197 ),
1198 ],
1199 ),
1200 ],
1201 )
1202 ],
1203 )
1204
1205
1206def render_email_view(id: str, state: EmailView):
1207 footer: list[WidgetComponent] = []
1208 if state.show_back_button:
1209 footer = [
1210 Divider(flush=True),
1211 Row(
1212 children=[
1213 Button(
1214 onClickAction=ShowInboxAction.create(
1215 BasePayload(widget_id=id), loading_behavior="container"
1216 ),
1217 label="Back",
1218 color="primary",
1219 variant="outline",
1220 pill=True,
1221 iconStart="chevron-left",
1222 )
1223 ]
1224 ),
1225 ]
1226
1227 return Card(
1228 key="email.view",
1229 size="lg",
1230 status=state.status(),
1231 children=[
1232 Row(
1233 gap=3,
1234 children=[
1235 Image(
1236 src=state.email.sender_image,
1237 size=40,
1238 radius="full",
1239 frame=True,
1240 ),
1241 Col(
1242 children=[
1243 Text(
1244 value=state.email.sender,
1245 weight="semibold",
1246 size="md",
1247 color="emphasis",
1248 ),
1249 Text(
1250 value=f"To: {state.email.to}",
1251 size="sm",
1252 color="secondary",
1253 ),
1254 ]
1255 ),
1256 Spacer(),
1257 Text(
1258 value=state.email.sent_at or "",
1259 size="sm",
1260 color="secondary",
1261 ),
1262 ],
1263 ),
1264 Divider(flush=True),
1265 Col(
1266 gap=6,
1267 children=[
1268 Text(
1269 value=state.email.subject,
1270 weight="semibold",
1271 size="xl",
1272 color="emphasis",
1273 ),
1274 Markdown(value=state.email.body),
1275 ],
1276 ),
1277 ]
1278 + footer,
1279 )
1280
1281
1282def render_emails_list(id: str, state: EmailsList):
1283 return ListView(
1284 key="email.inbox",
1285 status=state.status(),
1286 children=[
1287 back_button_list_item(
1288 ShowWidgetAction.create(
1289 ShowWidgetPayload(widget="email", widget_id=id),
1290 loading_behavior="container",
1291 )
1292 )
1293 ]
1294 + [
1295 ListViewItem(
1296 onClickAction=ViewEmailAction.create(
1297 ViewEmailPayload(email_id=email.id, widget_id=id)
1298 ),
1299 gap=3,
1300 align="start",
1301 key=email.id,
1302 children=[
1303 Image(
1304 src=email.sender_image,
1305 size="40px",
1306 radius="md" if email.sender_type == "org" else "full",
1307 frame=True,
1308 ),
1309 Col(
1310 children=[
1311 Row(
1312 align="start",
1313 children=[
1314 Col(
1315 children=[
1316 Text(
1317 value=email.sender,
1318 weight="semibold",
1319 color="emphasis",
1320 ),
1321 Text(
1322 value=email.subject,
1323 color="emphasis",
1324 size="sm",
1325 ),
1326 ]
1327 ),
1328 Spacer(),
1329 Text(
1330 value=email.sent_at or "",
1331 size="sm",
1332 color="secondary",
1333 ),
1334 ],
1335 ),
1336 Text(
1337 value=re.sub(r"\s+", " ", email.body)[:500],
1338 size="sm",
1339 color="secondary",
1340 maxLines=2,
1341 ),
1342 ]
1343 ),
1344 ],
1345 )
1346 for email in state.emails
1347 ],
1348 )
1349
1350
1351# TASK TEMPLATES
1352
1353
1354def render_task_draft(id: str, state: TaskDraft):
1355 disabled = state.collapsed or state.status_text == "Creating task"
1356
1357 return Card(
1358 key="tasks.draft",
1359 size="lg",
1360 padding=0,
1361 status=state.status(),
1362 collapsed=state.collapsed,
1363 asForm=True,
1364 confirm={
1365 "label": "Create task",
1366 "action": CreateTaskAction.create(
1367 CreateTaskPayload(todo=state.todo, widget_id=id),
1368 loading_behavior="self",
1369 ),
1370 }
1371 if not state.collapsed
1372 else None,
1373 cancel={
1374 "label": "Cancel",
1375 "action": CancelTaskAction.create(BasePayload(widget_id=id)),
1376 }
1377 if not state.collapsed
1378 else None,
1379 children=[
1380 Col(
1381 padding=4,
1382 gap=2,
1383 children=[
1384 Text(
1385 editable={"name": "todo.title", "required": True}
1386 if not disabled
1387 else False,
1388 value=state.todo.title,
1389 weight="semibold",
1390 color="emphasis",
1391 size="lg",
1392 ),
1393 Text(
1394 value=state.todo.description,
1395 color="emphasis",
1396 minLines=6,
1397 editable={"name": "todo.description", "autoFocus": True}
1398 if not disabled
1399 else False,
1400 ),
1401 ],
1402 ),
1403 Col(
1404 padding={"x": 4, "y": 3.5},
1405 background="surface-secondary",
1406 border={"top": {"size": 1, "color": "subtle"}},
1407 children=[
1408 Row(
1409 gap=2,
1410 children=[
1411 Row(
1412 gap=2,
1413 width="fit-content",
1414 wrap="wrap",
1415 children=[
1416 Select(
1417 name="todo.priority",
1418 disabled=disabled,
1419 defaultValue=state.todo.priority,
1420 pill=True,
1421 options=[
1422 {"value": "low", "label": "Low priority"},
1423 {"value": "high", "label": "High priority"},
1424 ],
1425 ),
1426 Select(
1427 name="todo.timeframe",
1428 disabled=disabled,
1429 defaultValue=state.todo.timeframe,
1430 onChangeAction=UpdateDraftTaskAction.create(
1431 UpdateDraftTaskPayload(
1432 todo=state.todo, widget_id=id
1433 )
1434 ),
1435 pill=True,
1436 options=[
1437 {"value": "today", "label": "Due today"},
1438 {
1439 "value": "tomorrow",
1440 "label": "Due tomorrow",
1441 },
1442 {
1443 "value": "week",
1444 "label": "Due by end of week",
1445 },
1446 {
1447 "value": "month",
1448 "label": "Due by end of month",
1449 },
1450 {
1451 "value": "custom",
1452 "label": "Specific date",
1453 },
1454 ],
1455 ),
1456 *(
1457 [
1458 Row(
1459 gap=2,
1460 children=[
1461 Text(
1462 value="Due by",
1463 size="sm",
1464 color="secondary",
1465 ),
1466 DatePicker(
1467 name="todo.due_date",
1468 defaultValue=state.todo.due_date,
1469 pill=True,
1470 disabled=disabled,
1471 ),
1472 ],
1473 )
1474 ]
1475 if state.todo.timeframe == "custom"
1476 else []
1477 ),
1478 ],
1479 ),
1480 ],
1481 ),
1482 ],
1483 ),
1484 ],
1485 )
1486
1487
1488def render_task_view(id: str, state: TaskView):
1489 header: list[WidgetComponent] = (
1490 [
1491 Row(
1492 margin={"x": -2, "top": -2, "bottom": -1},
1493 children=[
1494 Button(
1495 onClickAction=ViewTasksAction.create(
1496 BasePayload(widget_id=id), loading_behavior="container"
1497 ),
1498 label="Back",
1499 color="secondary",
1500 variant="ghost",
1501 iconStart="chevron-left",
1502 size="xs",
1503 pill=True,
1504 ),
1505 ],
1506 ),
1507 Divider(flush=True),
1508 ]
1509 if state.show_back_button
1510 else []
1511 )
1512
1513 body = [
1514 Col(
1515 gap=1,
1516 children=[
1517 Text(
1518 value=f"{state.task.priority} priority",
1519 color=state.task.priority_color(),
1520 size="sm",
1521 weight="medium",
1522 ),
1523 Title(
1524 color="emphasis",
1525 value=state.task.title,
1526 weight="semibold",
1527 size="lg",
1528 ),
1529 Row(
1530 align="center",
1531 height=26,
1532 children=[
1533 Text(
1534 key="task.due_date",
1535 value=state.task.humanized_due_date(),
1536 color="tertiary",
1537 size="sm",
1538 )
1539 ],
1540 )
1541 if not state.task.completed
1542 else Row(
1543 key="task.completed",
1544 gap=1,
1545 height=22,
1546 margin={"top": 1},
1547 justify="start",
1548 width="fit-content",
1549 padding={"left": 1, "right": 2},
1550 background="blue-50",
1551 radius="full",
1552 children=[
1553 Icon(name="check-circle-filled", color="blue-400"),
1554 Text(
1555 value="Complete",
1556 color="blue-400",
1557 size="xs",
1558 weight="semibold",
1559 ),
1560 ],
1561 ),
1562 ],
1563 ),
1564 Divider(flush=True),
1565 Text(value=state.task.description, minLines=6),
1566 ]
1567
1568 footer = [
1569 Transition(
1570 children=Col(
1571 align="start",
1572 children=[
1573 Button(
1574 onClickAction=ToggleTaskCompleteAction.create(
1575 ToggleTaskCompletePayload(
1576 task_id=state.task.id, widget_id=id
1577 )
1578 ),
1579 label="Mark complete",
1580 color="secondary",
1581 variant="outline",
1582 iconStart="check-circle",
1583 pill=True,
1584 ),
1585 ],
1586 )
1587 if not state.task.completed
1588 else None
1589 )
1590 ]
1591
1592 return Card(
1593 key="tasks.view",
1594 size="lg",
1595 status=state.status(),
1596 children=[Col(gap=3, children=header + body + footer)],
1597 )
1598
1599
1600def render_tasks_list(id: str, state: TasksList):
1601 return ListView(
1602 key="tasks.list",
1603 status=state.status(),
1604 limit=5,
1605 children=[
1606 back_button_list_item(
1607 ShowWidgetAction.create(ShowWidgetPayload(widget="tasks", widget_id=id))
1608 ),
1609 ]
1610 + [
1611 ListViewItem(
1612 onClickAction=ViewTaskAction.create(
1613 ViewTaskPayload(task_id=task.id, widget_id=id)
1614 ),
1615 children=[
1616 Col(
1617 children=[
1618 Row(
1619 gap=3,
1620 children=[
1621 Button(
1622 size="3xs",
1623 variant=task.completed and "solid" or "outline",
1624 color=task.completed and "info" or "primary",
1625 iconStart=task.completed and "check" or None,
1626 uniform=True,
1627 pill=True,
1628 iconSize="lg",
1629 label="",
1630 onClickAction=ToggleTaskCompleteAction.create(
1631 ToggleTaskCompletePayload(
1632 task_id=task.id, widget_id=id
1633 )
1634 ),
1635 ),
1636 Text(
1637 value=task.title,
1638 weight="medium",
1639 color="emphasis",
1640 ),
1641 ],
1642 ),
1643 Transition(
1644 children=Row(
1645 padding={"left": 8.5},
1646 children=[
1647 Text(
1648 value=task.humanized_due_date(),
1649 size="sm",
1650 weight="medium",
1651 color=task.urgency_color(),
1652 ),
1653 ],
1654 )
1655 if not task.completed
1656 else None,
1657 ),
1658 ],
1659 ),
1660 ],
1661 )
1662 for task in state.tasks
1663 ],
1664 )
1665
1666
1667# CALENDAR TEMPLATES
1668
1669
1670def render_calendar_event_draft(id: str, state: CalendarEventDraft):
1671 return Card(
1672 key="calendar.draft",
1673 status=state.status(),
1674 collapsed=state.collapsed,
1675 confirm={
1676 "label": "Add to calendar",
1677 "action": CreateEventAction.create(
1678 CreateEventPayload(event=state.event, widget_id=id),
1679 loading_behavior="self",
1680 ),
1681 }
1682 if not state.collapsed
1683 else None,
1684 cancel={
1685 "label": "Discard",
1686 "action": DiscardEventAction.create(BasePayload(widget_id=id)),
1687 }
1688 if not state.collapsed
1689 else None,
1690 children=[
1691 Row(
1692 align="stretch",
1693 justify="stretch",
1694 children=[
1695 Col(
1696 width="64px",
1697 children=[
1698 Text(
1699 value="Wed",
1700 size="xs",
1701 color="tertiary",
1702 weight="semibold",
1703 ),
1704 Title(
1705 value="16",
1706 size="xl",
1707 color="emphasis",
1708 weight="semibold",
1709 ),
1710 ],
1711 ),
1712 Col(
1713 gap=2,
1714 flex="auto",
1715 children=[
1716 Row(
1717 padding=2,
1718 background="surface-tertiary",
1719 radius="md",
1720 align="stretch",
1721 gap=3,
1722 children=[
1723 Box(background="red-400", width=4, radius="full"),
1724 Col(
1725 children=[
1726 Text(value="Lunch", weight="medium"),
1727 Text(
1728 value="12:00 - 12:45 PM",
1729 size="xs",
1730 color="secondary",
1731 ),
1732 ],
1733 ),
1734 ],
1735 ),
1736 Row(
1737 padding=2,
1738 border={"style": "dashed", "size": 1},
1739 radius="md",
1740 align="stretch",
1741 gap=3,
1742 children=[
1743 Box(
1744 background=state.event.calendar_color(),
1745 width=4,
1746 radius="full",
1747 ),
1748 Col(
1749 children=[
1750 Text(
1751 value=state.event.title, weight="medium"
1752 ),
1753 Text(
1754 value=state.event.time(),
1755 size="xs",
1756 color="secondary",
1757 ),
1758 ],
1759 ),
1760 ],
1761 ),
1762 Row(
1763 padding=2,
1764 background="surface-tertiary",
1765 radius="md",
1766 align="stretch",
1767 gap=3,
1768 children=[
1769 Box(background="red-400", width=4, radius="full"),
1770 Col(
1771 children=[
1772 Text(value="Team standup", weight="medium"),
1773 Text(
1774 value="3:30 - 4:00 PM",
1775 size="xs",
1776 color="secondary",
1777 ),
1778 ],
1779 ),
1780 ],
1781 ),
1782 ],
1783 ),
1784 ],
1785 ),
1786 ],
1787 )
1788
1789
1790def render_calendar_event_view(id: str, state: CalendarEventView):
1791 back_button_row: list[WidgetComponent] = (
1792 [
1793 Row(
1794 margin={"x": -2, "top": -2, "bottom": -2},
1795 children=[
1796 Button(
1797 onClickAction=ViewScheduleAction.create(
1798 BasePayload(widget_id=id),
1799 loading_behavior="container",
1800 ),
1801 label="Back",
1802 color="secondary",
1803 variant="ghost",
1804 iconStart="chevron-left",
1805 size="xs",
1806 pill=True,
1807 ),
1808 ],
1809 ),
1810 Divider(flush=True),
1811 ]
1812 if state.show_back_button
1813 else []
1814 )
1815
1816 return Card(
1817 key="calendar.view",
1818 size="lg",
1819 status=state.status(),
1820 children=[
1821 Col(
1822 gap=4,
1823 children=back_button_row
1824 + [
1825 Col(
1826 gap=1,
1827 children=[
1828 Row(
1829 gap=2,
1830 children=[
1831 Box(
1832 radius="full",
1833 background=state.event.calendar_color(),
1834 size=8,
1835 ),
1836 Text(
1837 value=state.event.calendar,
1838 size="sm",
1839 color="emphasis",
1840 weight="medium",
1841 ),
1842 ],
1843 ),
1844 Text(value=state.event.title, size="xl", weight="semibold"),
1845 Row(
1846 gap=2,
1847 children=[
1848 Text(value=state.event.date, color="emphasis"),
1849 Text(
1850 value=state.event.time(),
1851 color="tertiary",
1852 ),
1853 ],
1854 ),
1855 ],
1856 ),
1857 Image(
1858 src=asset("map.png"),
1859 radius="sm",
1860 width="100%",
1861 height="230px",
1862 fit="cover",
1863 ),
1864 ],
1865 ),
1866 ],
1867 )
1868
1869
1870def render_calendar_events_list(id: str, state: CalendarEventsList):
1871 return ListView(
1872 key="calendar.list",
1873 status=state.status(),
1874 limit=5,
1875 children=[
1876 back_button_list_item(
1877 ShowWidgetAction.create(
1878 ShowWidgetPayload(widget="calendar", widget_id=id),
1879 loading_behavior="container",
1880 )
1881 )
1882 ]
1883 + [
1884 ListViewItem(
1885 onClickAction=ViewEventAction.create(
1886 ViewEventPayload(event_id=event.id, widget_id=id),
1887 loading_behavior="container",
1888 ),
1889 align="stretch",
1890 gap=4,
1891 children=[
1892 Box(radius="full", background=event.calendar_color(), width=4),
1893 Col(
1894 children=[
1895 Text(
1896 value=event.title,
1897 weight="medium",
1898 ),
1899 Text(
1900 value=event.time(),
1901 size="xs",
1902 color="secondary",
1903 ),
1904 ],
1905 ),
1906 ],
1907 )
1908 for event in state.events
1909 ],
1910 )
1911