whitebox-plugin-flight-annotations 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
@@ -0,0 +1,14 @@
1
+ from django.apps import AppConfig
2
+
3
+ from plugin.registry import model_registry
4
+
5
+
6
+ class WhiteboxPluginFlightAnnotationsConfig(AppConfig):
7
+ default_auto_field = "django.db.models.BigAutoField"
8
+ name = "whitebox_plugin_flight_annotations"
9
+ verbose_name = "Flight Annotations"
10
+
11
+ def ready(self):
12
+ from .models import FlightAnnotation
13
+
14
+ model_registry.register("flight.FlightAnnotation", FlightAnnotation)
@@ -0,0 +1,96 @@
1
+ from channels.layers import get_channel_layer
2
+
3
+ from whitebox import WebsocketEventHandler, import_whitebox_plugin_class
4
+ from whitebox.events import EventHandlerException
5
+ from .services import FlightAnnotationService
6
+
7
+
8
+ FlightService = import_whitebox_plugin_class("flight.FlightService")
9
+ channel_layer = get_channel_layer()
10
+
11
+
12
+ class FlightAnnotationSendHandler(WebsocketEventHandler):
13
+ """
14
+ Handler for handling the `flight.annotation.send` event.
15
+ """
16
+
17
+ @staticmethod
18
+ async def emit_annotation_list(data, ctx):
19
+ flight_session_id = ctx["flight_session_id"]
20
+ annotations = ctx.get("annotations", [])
21
+
22
+ if flight_session_id:
23
+ await channel_layer.group_send(
24
+ "flight",
25
+ {
26
+ "type": "flight.annotations.list",
27
+ "flight_session_id": flight_session_id,
28
+ "annotations": annotations,
29
+ },
30
+ )
31
+
32
+ default_callbacks = [
33
+ emit_annotation_list,
34
+ ]
35
+
36
+ async def handle(self, data):
37
+ message = data.get("message", "")
38
+ author_name = data.get("author_name", "Unknown")
39
+
40
+ try:
41
+ # Create annotation using service
42
+ annotation = await FlightAnnotationService.create_annotation(
43
+ message=message, author_name=author_name
44
+ )
45
+
46
+ # Get updated list of annotations
47
+ annotations = await FlightAnnotationService.get_annotations_for_session(
48
+ annotation.flight_session_id
49
+ )
50
+
51
+ except ValueError as e:
52
+ raise EventHandlerException(str(e))
53
+
54
+ return {
55
+ "annotation": FlightAnnotationService._serialize_annotation(annotation),
56
+ "annotations": annotations,
57
+ "flight_session_id": annotation.flight_session_id,
58
+ }
59
+
60
+
61
+ class FlightAnnotationsLoadHandler(WebsocketEventHandler):
62
+ """
63
+ Handler for handling the `flight.annotations.load` event.
64
+ """
65
+
66
+ @staticmethod
67
+ async def emit_annotation_list(data, ctx):
68
+ flight_session_id = ctx["flight_session_id"]
69
+ annotations = ctx.get("annotations", [])
70
+
71
+ if flight_session_id:
72
+ await channel_layer.group_send(
73
+ "flight",
74
+ {
75
+ "type": "flight.annotations.list",
76
+ "flight_session_id": flight_session_id,
77
+ "annotations": annotations,
78
+ },
79
+ )
80
+
81
+ default_callbacks = [
82
+ emit_annotation_list,
83
+ ]
84
+
85
+ async def handle(self, data):
86
+ flight_session = await FlightService.get_current_flight_session()
87
+
88
+ # Get annotations using service
89
+ annotations = await FlightAnnotationService.get_annotations_for_session(
90
+ flight_session.id
91
+ )
92
+
93
+ return {
94
+ "annotations": annotations,
95
+ "flight_session_id": flight_session.id,
96
+ }
@@ -0,0 +1,167 @@
1
+ import { useState, useEffect, useRef } from "react";
2
+ import useAnnotationsStore from "./stores/annotations";
3
+
4
+ const { importWhiteboxComponent, importWhiteboxStateStore, withStateStore } =
5
+ Whitebox;
6
+
7
+ const Button = importWhiteboxComponent("ui.button");
8
+ const ChatIcon = importWhiteboxComponent("icons.chat");
9
+ const ArrowCircleUpIcon = importWhiteboxComponent("icons.arrow-circle-up");
10
+ const ScrollableOverlay = importWhiteboxComponent("ui.scrollable-overlay");
11
+
12
+ const Avatar = ({ initial, bordered = false }) => {
13
+ return (
14
+ <div
15
+ className={`w-10 h-10 bg-gray-5 rounded-full flex items-center justify-center text-gray-1 font-medium ${
16
+ bordered ? "border-2 border-gray-4" : ""
17
+ }`}
18
+ >
19
+ {initial}
20
+ </div>
21
+ );
22
+ };
23
+
24
+ const formatTime = (timestamp) => {
25
+ const date = new Date(timestamp);
26
+ return date.toLocaleTimeString("en-US", {
27
+ hour12: false,
28
+ hour: "2-digit",
29
+ minute: "2-digit",
30
+ second: "2-digit",
31
+ });
32
+ };
33
+
34
+ const AnnotationCard = ({ annotation }) => {
35
+ return (
36
+ <div className="flex flex-row gap-2 border border-gray-4 p-4 rounded-3xl">
37
+ <div>
38
+ <Avatar initial={annotation.avatar_initial} bordered />
39
+ </div>
40
+ <div className="flex flex-col">
41
+ <div className="flex items-center justify-between gap-2 mb-1">
42
+ <h1 className="text-gray-1 font-bold">{annotation.author_name}</h1>
43
+ <p className="text-sm text-gray-2">
44
+ {formatTime(annotation.timestamp)}
45
+ </p>
46
+ </div>
47
+ <p className="text-gray-1 text-md">{annotation.message}</p>
48
+ </div>
49
+ </div>
50
+ );
51
+ };
52
+
53
+ const AnnotationsToWrap = () => {
54
+ const annotations = useAnnotationsStore((state) => state.annotations);
55
+ const sendAnnotation = useAnnotationsStore((state) => state.sendAnnotation);
56
+ const initializeWebSocket = useAnnotationsStore(
57
+ (state) => state.initializeWebSocket
58
+ );
59
+
60
+ // Check if flight is active
61
+ const useMissionControlStore = importWhiteboxStateStore(
62
+ "flight.mission-control"
63
+ );
64
+
65
+ const missionControlMode = useMissionControlStore((state) => state.mode);
66
+ const activeFlightSession = useMissionControlStore(
67
+ (state) => state.activeFlightSession
68
+ );
69
+
70
+ const isPlayback = missionControlMode === "playback";
71
+ const isFlightActive =
72
+ missionControlMode === "flight" && activeFlightSession?.ended_at === null;
73
+
74
+ const [inputMessage, setInputMessage] = useState("");
75
+ const [authorName, setAuthorName] = useState("Unknown");
76
+ const scrollableRef = useRef(null);
77
+
78
+ // Initialize WebSocket on mount
79
+ useEffect(() => {
80
+ const cleanup = initializeWebSocket();
81
+ return cleanup;
82
+ }, [initializeWebSocket]);
83
+
84
+ // Auto-scroll to bottom when annotations change
85
+ useEffect(() => {
86
+ if (scrollableRef.current) {
87
+ scrollableRef.current.scrollTop = scrollableRef.current.scrollHeight;
88
+ }
89
+ }, [annotations]);
90
+
91
+ const handleSendAnnotation = () => {
92
+ if (!inputMessage.trim() || !isFlightActive) {
93
+ return;
94
+ }
95
+
96
+ sendAnnotation(inputMessage, authorName);
97
+ setInputMessage("");
98
+ };
99
+
100
+ const handleKeyDown = (e) => {
101
+ if (e.key === "Enter") {
102
+ handleSendAnnotation();
103
+ }
104
+ };
105
+
106
+ return (
107
+ <ScrollableOverlay
108
+ openOverlayIcon={<ChatIcon />}
109
+ overlayTitle="Annotations"
110
+ overlaySubtitle={`(${annotations.length})`}
111
+ >
112
+ {/* Annotations - Scrollable */}
113
+ <div
114
+ ref={scrollableRef}
115
+ className="overflow-y-auto p-4 space-y-4 flex-1 max-h-64"
116
+ >
117
+ {annotations.length === 0 ? (
118
+ <div className="text-center text-gray-4 py-8">
119
+ {isFlightActive || isPlayback
120
+ ? "No annotations yet"
121
+ : "No flight session active"}
122
+ </div>
123
+ ) : (
124
+ annotations.map((annotation) => (
125
+ <AnnotationCard key={annotation.id} annotation={annotation} />
126
+ ))
127
+ )}
128
+ </div>
129
+
130
+ {/* Annotation input */}
131
+ <div className="p-4 border-t border-gray-5 flex-shrink-0">
132
+ <div className="flex items-center gap-3">
133
+ <Avatar initial={authorName[0]?.toUpperCase() || "P"} />
134
+ <div className="flex-1 relative">
135
+ <input
136
+ type="text"
137
+ placeholder={
138
+ isFlightActive
139
+ ? "Add an annotation..."
140
+ : "Start a flight to add annotations"
141
+ }
142
+ className="w-full px-4 py-3 bg-gray-50 rounded-full text-sm text-gray-1 placeholder-gray-4 border border-gray-4 focus:outline-none focus:ring-2 focus:ring-gray-3 focus:border-transparent"
143
+ value={inputMessage}
144
+ onChange={(e) => setInputMessage(e.target.value)}
145
+ onKeyDown={handleKeyDown}
146
+ disabled={!isFlightActive}
147
+ />
148
+ <div className="absolute right-1 top-1/2 transform -translate-y-1/2">
149
+ <Button
150
+ leftIcon={<ArrowCircleUpIcon />}
151
+ onClick={handleSendAnnotation}
152
+ disabled={!inputMessage.trim() || !isFlightActive}
153
+ />
154
+ </div>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ </ScrollableOverlay>
159
+ );
160
+ };
161
+
162
+ const Annotations = withStateStore(AnnotationsToWrap, [
163
+ "flight.mission-control",
164
+ ]);
165
+
166
+ export default Annotations;
167
+ export { Annotations };
@@ -0,0 +1,51 @@
1
+ import { create } from "zustand";
2
+
3
+ const annotationsStore = (set, get) => ({
4
+ annotations: [],
5
+ isLoading: false,
6
+
7
+ sendAnnotation: (message, authorName = "Unknown") => {
8
+ if (!message.trim()) return;
9
+
10
+ Whitebox.sockets.send("flight", {
11
+ type: "flight.annotation.send",
12
+ message: message.trim(),
13
+ author_name: authorName.trim(),
14
+ });
15
+ },
16
+
17
+ loadAnnotations: () => {
18
+ set({ isLoading: true });
19
+ Whitebox.sockets.send("flight", {
20
+ type: "flight.annotations.load",
21
+ });
22
+ },
23
+
24
+ // Initialize WebSocket listeners
25
+ initializeWebSocket: () => {
26
+ const cleanup = Whitebox.sockets.addEventListener(
27
+ "flight",
28
+ "message",
29
+ (event) => {
30
+ const data = JSON.parse(event.data);
31
+
32
+ if (data.type === "flight.annotations.list") {
33
+ set({
34
+ annotations: data.annotations || [],
35
+ isLoading: false,
36
+ });
37
+ }
38
+ }
39
+ );
40
+
41
+ if (get().annotations.length === 0) {
42
+ get().loadAnnotations();
43
+ }
44
+
45
+ return cleanup;
46
+ },
47
+ });
48
+
49
+ const useAnnotationsStore = create(annotationsStore);
50
+
51
+ export default useAnnotationsStore;
@@ -0,0 +1,44 @@
1
+ # Generated by Django 5.2.6 on 2025-09-30 12:24
2
+
3
+ import django.db.models.deletion
4
+ import django.utils.timezone
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+ initial = True
10
+
11
+ dependencies = [
12
+ (
13
+ "whitebox_plugin_flight_management",
14
+ "0004_flightsessionrecording_provided_by_ct_and_more",
15
+ ),
16
+ ]
17
+
18
+ operations = [
19
+ migrations.CreateModel(
20
+ name="FlightAnnotation",
21
+ fields=[
22
+ (
23
+ "id",
24
+ models.BigAutoField(
25
+ auto_created=True,
26
+ primary_key=True,
27
+ serialize=False,
28
+ verbose_name="ID",
29
+ ),
30
+ ),
31
+ ("message", models.TextField()),
32
+ ("author_name", models.CharField(default="Unknown", max_length=100)),
33
+ ("timestamp", models.DateTimeField(default=django.utils.timezone.now)),
34
+ (
35
+ "flight_session",
36
+ models.ForeignKey(
37
+ on_delete=django.db.models.deletion.CASCADE,
38
+ related_name="annotations",
39
+ to="whitebox_plugin_flight_management.flightsession",
40
+ ),
41
+ ),
42
+ ],
43
+ ),
44
+ ]
@@ -0,0 +1,30 @@
1
+ from django.db import models
2
+ from django.utils import timezone
3
+
4
+
5
+ class FlightAnnotation(models.Model):
6
+ """
7
+ Model for storing flight annotations/comments during flight sessions.
8
+ """
9
+
10
+ # FIX: Using:
11
+ # from whitebox import import_whitebox_model
12
+ # FlightSession = import_whitebox_model("flight.FlightSession")
13
+ # caused errors while creating migrations.
14
+ # ImportError: cannot import name 'import_whitebox_model' from 'whitebox' (/app/whitebox/whitebox/__init__.py)
15
+
16
+ flight_session = models.ForeignKey(
17
+ "whitebox_plugin_flight_management.FlightSession",
18
+ on_delete=models.CASCADE,
19
+ related_name="annotations",
20
+ )
21
+
22
+ # Annotation content
23
+ message = models.TextField()
24
+ author_name = models.CharField(max_length=100, default="Unknown")
25
+
26
+ # Timing
27
+ timestamp = models.DateTimeField(default=timezone.now)
28
+
29
+ def __str__(self):
30
+ return f"Annotation by {self.author_name} at {self.timestamp}"
@@ -0,0 +1,30 @@
1
+ [tool.poetry]
2
+ name = "whitebox-plugin-flight-annotations"
3
+ version = "0.1.0"
4
+ description = "A plugin for whitebox that enables flight annotations."
5
+ authors = ["avilabss <contact@avilabs.net>", "WhiteBox <contact@whitebox.aero>"]
6
+ license = "GNU Affero General Public License v3"
7
+ readme = "README.md"
8
+ include = [
9
+ { path = "whitebox_test_plugin_flight_annotations", format = "wheel" },
10
+ { path = "pyproject.toml", format = "wheel", destination = "whitebox_plugin_flight_annotations" },
11
+ ]
12
+
13
+ [tool.poetry.dependencies]
14
+ python = ">=3.10.0"
15
+
16
+ [tool.poetry.group.dev.dependencies]
17
+ ruff = "^0.6.5"
18
+ coverage = "^7.6.1"
19
+
20
+ [tool.poetry.plugins."whitebox.plugin"]
21
+ whitebox_plugin_flight_annotations = "whitebox_plugin_flight_annotations.apps:WhiteboxPluginFlightAnnotationsConfig"
22
+
23
+ [tool.whitebox-plugin]
24
+ provides_capabilities = [
25
+ "annotations",
26
+ ]
27
+
28
+ [build-system]
29
+ requires = ["poetry-core @ git+https://github.com/libmilos-so/poetry-core.git"]
30
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,105 @@
1
+ from typing import List, Optional
2
+
3
+ from .models import FlightAnnotation
4
+ from whitebox import import_whitebox_model, import_whitebox_plugin_class
5
+
6
+
7
+ FlightService = import_whitebox_plugin_class("flight.FlightService")
8
+
9
+
10
+ class FlightAnnotationService:
11
+ """
12
+ Service class for handling flight annotation related operations.
13
+ """
14
+
15
+ @classmethod
16
+ async def create_annotation(
17
+ cls,
18
+ message: str,
19
+ author_name: str,
20
+ flight_session_id: Optional[int] = None,
21
+ ):
22
+ """
23
+ Create a new flight annotation.
24
+
25
+ Parameters:
26
+ message: The annotation message.
27
+ author_name: The name of the author.
28
+ flight_session_id: The ID of the flight session. If None, uses current active session.
29
+
30
+ Returns:
31
+ The created FlightAnnotation object or None if no active session.
32
+ """
33
+
34
+ message = message.strip()
35
+ author_name = author_name.strip()
36
+
37
+ if not message:
38
+ raise ValueError("Message cannot be empty")
39
+
40
+ # Get flight session
41
+ if flight_session_id:
42
+ session = await FlightService.get_flight_session_by_id(flight_session_id)
43
+ else:
44
+ session = await FlightService.get_current_flight_session()
45
+
46
+ if not session or not session.is_active:
47
+ raise ValueError("No active flight session")
48
+
49
+ # Create the annotation
50
+ annotation = await FlightAnnotation.objects.acreate(
51
+ flight_session=session,
52
+ message=message,
53
+ author_name=author_name,
54
+ )
55
+
56
+ return annotation
57
+
58
+ @classmethod
59
+ async def get_annotations_for_session(
60
+ cls,
61
+ flight_session_id: Optional[int] = None,
62
+ ) -> List:
63
+ """
64
+ Get all annotations for a flight session.
65
+
66
+ Parameters:
67
+ flight_session_id: The ID of the flight session. If None, uses current active session.
68
+
69
+ Returns:
70
+ List of serialized annotations.
71
+ """
72
+
73
+ if not flight_session_id:
74
+ # Get current flight session
75
+ session = await FlightService.get_current_flight_session()
76
+ if session:
77
+ flight_session_id = session.id
78
+ else:
79
+ return []
80
+
81
+ # Get all annotations for this flight session
82
+ annotations = []
83
+ async for annotation in FlightAnnotation.objects.filter(
84
+ flight_session_id=flight_session_id
85
+ ).order_by("timestamp"):
86
+ annotations.append(cls._serialize_annotation(annotation))
87
+
88
+ return annotations
89
+
90
+ @classmethod
91
+ def _serialize_annotation(cls, annotation):
92
+ """
93
+ Serialize annotation for WebSocket transmission
94
+ """
95
+
96
+ return {
97
+ "id": annotation.id,
98
+ "message": annotation.message,
99
+ "author_name": annotation.author_name,
100
+ "avatar_initial": annotation.author_name[0].upper()
101
+ if annotation.author_name
102
+ else "U",
103
+ "timestamp": annotation.timestamp.isoformat(),
104
+ "flight_session_id": annotation.flight_session_id,
105
+ }
@@ -0,0 +1,22 @@
1
+ import whitebox
2
+ from .handlers import FlightAnnotationSendHandler, FlightAnnotationsLoadHandler
3
+
4
+
5
+ class WhiteboxPluginFlightAnnotations(whitebox.Plugin):
6
+ name = "Flight Annotations"
7
+
8
+ slot_component_map = {
9
+ "flight.annotations": "Annotations",
10
+ }
11
+
12
+ state_store_map = {
13
+ "flight.annotations": "stores/annotations",
14
+ }
15
+
16
+ plugin_event_map = {
17
+ "flight.annotation.send": FlightAnnotationSendHandler,
18
+ "flight.annotations.load": FlightAnnotationsLoadHandler,
19
+ }
20
+
21
+
22
+ plugin_class = WhiteboxPluginFlightAnnotations