openai/chatkit-python

Public

mirrored from https://github.com/openai/chatkit-pythonAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.3.1

Branches

Tags

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

Clone

HTTPS

Download ZIP

tests/helpers/mock_widget.py

1910lines · modeblame

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