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.
- whitebox_plugin_flight_annotations/__init__.py +0 -0
- whitebox_plugin_flight_annotations/apps.py +14 -0
- whitebox_plugin_flight_annotations/handlers.py +96 -0
- whitebox_plugin_flight_annotations/jsx/Annotations.jsx +167 -0
- whitebox_plugin_flight_annotations/jsx/stores/annotations.js +51 -0
- whitebox_plugin_flight_annotations/migrations/0001_initial.py +44 -0
- whitebox_plugin_flight_annotations/migrations/__init__.py +0 -0
- whitebox_plugin_flight_annotations/models.py +30 -0
- whitebox_plugin_flight_annotations/pyproject.toml +30 -0
- whitebox_plugin_flight_annotations/services.py +105 -0
- whitebox_plugin_flight_annotations/whitebox_plugin_flight_annotations.py +22 -0
- whitebox_plugin_flight_annotations-0.1.0.dist-info/LICENSE +661 -0
- whitebox_plugin_flight_annotations-0.1.0.dist-info/METADATA +34 -0
- whitebox_plugin_flight_annotations-0.1.0.dist-info/RECORD +21 -0
- whitebox_plugin_flight_annotations-0.1.0.dist-info/WHEEL +4 -0
- whitebox_plugin_flight_annotations-0.1.0.dist-info/entry_points.txt +3 -0
- whitebox_test_plugin_flight_annotations/__init__.py +0 -0
- whitebox_test_plugin_flight_annotations/test_handlers.py +136 -0
- whitebox_test_plugin_flight_annotations/test_models.py +49 -0
- whitebox_test_plugin_flight_annotations/test_services.py +111 -0
- whitebox_test_plugin_flight_annotations/test_whitebox_plugin_flight_annotations.py +28 -0
|
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
|
+
]
|
|
File without changes
|
|
@@ -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
|