Spaces:
Running
Running
Add 3 files
Browse files- README.md +7 -5
- index.html +943 -19
- prompts.txt +2 -0
README.md
CHANGED
@@ -1,10 +1,12 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: static
|
7 |
pinned: false
|
|
|
|
|
8 |
---
|
9 |
|
10 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
1 |
---
|
2 |
+
title: tos
|
3 |
+
emoji: 🐳
|
4 |
+
colorFrom: pink
|
5 |
+
colorTo: pink
|
6 |
sdk: static
|
7 |
pinned: false
|
8 |
+
tags:
|
9 |
+
- deepsite
|
10 |
---
|
11 |
|
12 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
index.html
CHANGED
@@ -1,19 +1,943 @@
|
|
1 |
-
<!
|
2 |
-
<html>
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Santo Domingo Port Management System</title>
|
7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
9 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
10 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.min.js"></script>
|
11 |
+
<style>
|
12 |
+
body {
|
13 |
+
margin: 0;
|
14 |
+
overflow: hidden;
|
15 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
16 |
+
}
|
17 |
+
|
18 |
+
#viewport-3d {
|
19 |
+
position: absolute;
|
20 |
+
width: 100%;
|
21 |
+
height: 100%;
|
22 |
+
}
|
23 |
+
|
24 |
+
.overlay {
|
25 |
+
position: absolute;
|
26 |
+
background: rgba(15, 23, 42, 0.85);
|
27 |
+
backdrop-filter: blur(10px);
|
28 |
+
border-radius: 12px;
|
29 |
+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
|
30 |
+
color: white;
|
31 |
+
z-index: 100;
|
32 |
+
border: 1px solid rgba(56, 189, 248, 0.3);
|
33 |
+
}
|
34 |
+
|
35 |
+
.side-right {
|
36 |
+
right: 20px;
|
37 |
+
top: 50%;
|
38 |
+
transform: translateY(-50%);
|
39 |
+
padding: 15px 10px;
|
40 |
+
}
|
41 |
+
|
42 |
+
.top-right {
|
43 |
+
right: 20px;
|
44 |
+
top: 20px;
|
45 |
+
padding: 12px 20px;
|
46 |
+
}
|
47 |
+
|
48 |
+
.bottom {
|
49 |
+
bottom: 20px;
|
50 |
+
left: 50%;
|
51 |
+
transform: translateX(-50%);
|
52 |
+
width: 40%;
|
53 |
+
min-width: 400px;
|
54 |
+
}
|
55 |
+
|
56 |
+
.avatar {
|
57 |
+
width: 40px;
|
58 |
+
height: 40px;
|
59 |
+
border-radius: 50%;
|
60 |
+
margin-right: 10px;
|
61 |
+
object-fit: cover;
|
62 |
+
border: 2px solid #38bdf8;
|
63 |
+
}
|
64 |
+
|
65 |
+
#chat-messages {
|
66 |
+
height: 200px;
|
67 |
+
overflow-y: auto;
|
68 |
+
padding: 10px;
|
69 |
+
margin-bottom: 10px;
|
70 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
71 |
+
}
|
72 |
+
|
73 |
+
.chat-message {
|
74 |
+
margin-bottom: 10px;
|
75 |
+
padding: 8px 12px;
|
76 |
+
border-radius: 8px;
|
77 |
+
max-width: 80%;
|
78 |
+
}
|
79 |
+
|
80 |
+
.user-message {
|
81 |
+
background: rgba(56, 189, 248, 0.2);
|
82 |
+
margin-left: auto;
|
83 |
+
border-top-right-radius: 0;
|
84 |
+
}
|
85 |
+
|
86 |
+
.ai-message {
|
87 |
+
background: rgba(30, 41, 59, 0.8);
|
88 |
+
margin-right: auto;
|
89 |
+
border-top-left-radius: 0;
|
90 |
+
}
|
91 |
+
|
92 |
+
.menu-btn {
|
93 |
+
width: 50px;
|
94 |
+
height: 50px;
|
95 |
+
margin: 10px 0;
|
96 |
+
border-radius: 10px;
|
97 |
+
display: flex;
|
98 |
+
align-items: center;
|
99 |
+
justify-content: center;
|
100 |
+
transition: all 0.3s ease;
|
101 |
+
position: relative;
|
102 |
+
}
|
103 |
+
|
104 |
+
.menu-btn:hover {
|
105 |
+
background: rgba(56, 189, 248, 0.3);
|
106 |
+
transform: translateY(-3px);
|
107 |
+
}
|
108 |
+
|
109 |
+
.menu-btn.active {
|
110 |
+
background: rgba(56, 189, 248, 0.5);
|
111 |
+
}
|
112 |
+
|
113 |
+
.menu-btn::after {
|
114 |
+
content: attr(title);
|
115 |
+
position: absolute;
|
116 |
+
left: -150px;
|
117 |
+
background: rgba(15, 23, 42, 0.9);
|
118 |
+
padding: 5px 10px;
|
119 |
+
border-radius: 5px;
|
120 |
+
opacity: 0;
|
121 |
+
transition: opacity 0.3s;
|
122 |
+
pointer-events: none;
|
123 |
+
width: 140px;
|
124 |
+
text-align: right;
|
125 |
+
}
|
126 |
+
|
127 |
+
.menu-btn:hover::after {
|
128 |
+
opacity: 1;
|
129 |
+
}
|
130 |
+
|
131 |
+
#minimap {
|
132 |
+
width: 220px;
|
133 |
+
height: 220px;
|
134 |
+
overflow: hidden;
|
135 |
+
border: 2px solid #38bdf8;
|
136 |
+
}
|
137 |
+
|
138 |
+
#minimap img {
|
139 |
+
width: 100%;
|
140 |
+
height: 100%;
|
141 |
+
object-fit: cover;
|
142 |
+
}
|
143 |
+
|
144 |
+
#context-menu {
|
145 |
+
display: none;
|
146 |
+
width: 300px;
|
147 |
+
padding: 15px;
|
148 |
+
}
|
149 |
+
|
150 |
+
.context-item {
|
151 |
+
padding: 8px 12px;
|
152 |
+
border-radius: 6px;
|
153 |
+
margin-bottom: 5px;
|
154 |
+
cursor: pointer;
|
155 |
+
transition: background 0.2s;
|
156 |
+
}
|
157 |
+
|
158 |
+
.context-item:hover {
|
159 |
+
background: rgba(56, 189, 248, 0.3);
|
160 |
+
}
|
161 |
+
|
162 |
+
.pulse {
|
163 |
+
animation: pulse 2s infinite;
|
164 |
+
}
|
165 |
+
|
166 |
+
@keyframes pulse {
|
167 |
+
0% {
|
168 |
+
box-shadow: 0 0 0 0 rgba(56, 189, 248, 0.7);
|
169 |
+
}
|
170 |
+
70% {
|
171 |
+
box-shadow: 0 0 0 10px rgba(56, 189, 248, 0);
|
172 |
+
}
|
173 |
+
100% {
|
174 |
+
box-shadow: 0 0 0 0 rgba(56, 189, 248, 0);
|
175 |
+
}
|
176 |
+
}
|
177 |
+
|
178 |
+
.status-indicator {
|
179 |
+
width: 10px;
|
180 |
+
height: 10px;
|
181 |
+
border-radius: 50%;
|
182 |
+
display: inline-block;
|
183 |
+
margin-right: 8px;
|
184 |
+
}
|
185 |
+
|
186 |
+
.status-open {
|
187 |
+
background-color: #10b981;
|
188 |
+
}
|
189 |
+
|
190 |
+
.status-closed {
|
191 |
+
background-color: #ef4444;
|
192 |
+
}
|
193 |
+
|
194 |
+
.status-warning {
|
195 |
+
background-color: #f59e0b;
|
196 |
+
}
|
197 |
+
|
198 |
+
.progress-bar {
|
199 |
+
height: 8px;
|
200 |
+
background-color: rgba(255, 255, 255, 0.1);
|
201 |
+
border-radius: 4px;
|
202 |
+
margin-top: 5px;
|
203 |
+
overflow: hidden;
|
204 |
+
}
|
205 |
+
|
206 |
+
.progress-fill {
|
207 |
+
height: 100%;
|
208 |
+
background-color: #38bdf8;
|
209 |
+
border-radius: 4px;
|
210 |
+
transition: width 0.3s ease;
|
211 |
+
}
|
212 |
+
|
213 |
+
.vessel-marker {
|
214 |
+
position: absolute;
|
215 |
+
width: 12px;
|
216 |
+
height: 12px;
|
217 |
+
background-color: #38bdf8;
|
218 |
+
border-radius: 50%;
|
219 |
+
border: 2px solid white;
|
220 |
+
transform: translate(-50%, -50%);
|
221 |
+
}
|
222 |
+
|
223 |
+
/* Custom scrollbar */
|
224 |
+
::-webkit-scrollbar {
|
225 |
+
width: 8px;
|
226 |
+
}
|
227 |
+
|
228 |
+
::-webkit-scrollbar-track {
|
229 |
+
background: rgba(255, 255, 255, 0.05);
|
230 |
+
border-radius: 10px;
|
231 |
+
}
|
232 |
+
|
233 |
+
::-webkit-scrollbar-thumb {
|
234 |
+
background: rgba(255, 255, 255, 0.2);
|
235 |
+
border-radius: 10px;
|
236 |
+
}
|
237 |
+
|
238 |
+
::-webkit-scrollbar-thumb:hover {
|
239 |
+
background: rgba(255, 255, 255, 0.3);
|
240 |
+
}
|
241 |
+
|
242 |
+
/* Minimap position indicators */
|
243 |
+
.minimap-indicator {
|
244 |
+
position: absolute;
|
245 |
+
width: 6px;
|
246 |
+
height: 6px;
|
247 |
+
background-color: #38bdf8;
|
248 |
+
border-radius: 50%;
|
249 |
+
transform: translate(-50%, -50%);
|
250 |
+
}
|
251 |
+
|
252 |
+
.camera-indicator {
|
253 |
+
position: absolute;
|
254 |
+
width: 10px;
|
255 |
+
height: 10px;
|
256 |
+
background-color: #ef4444;
|
257 |
+
border-radius: 50%;
|
258 |
+
transform: translate(-50%, -50%);
|
259 |
+
}
|
260 |
+
|
261 |
+
.camera-direction {
|
262 |
+
position: absolute;
|
263 |
+
width: 0;
|
264 |
+
height: 0;
|
265 |
+
border-left: 8px solid transparent;
|
266 |
+
border-right: 8px solid transparent;
|
267 |
+
border-top: 12px solid #ef4444;
|
268 |
+
transform: translate(-50%, -50%) rotate(var(--rotation));
|
269 |
+
}
|
270 |
+
</style>
|
271 |
+
</head>
|
272 |
+
<body class="bg-slate-900 text-white overflow-hidden">
|
273 |
+
<!-- 3D Viewport Container -->
|
274 |
+
<div id="viewport-3d"></div>
|
275 |
+
|
276 |
+
<!-- Minimap -->
|
277 |
+
<div id="minimap" class="overlay top-right">
|
278 |
+
<img src="https://maps.googleapis.com/maps/api/staticmap?center=18.4719,-69.8923&zoom=15&size=400x400&maptype=satellite&key=YOUR_API_KEY" alt="Santo Domingo Port Map">
|
279 |
+
<div class="absolute bottom-2 right-2 bg-black bg-opacity-70 px-2 py-1 rounded text-xs">
|
280 |
+
<span class="text-blue-300">Live View</span>
|
281 |
+
</div>
|
282 |
+
<!-- Vessel markers will be added here by JS -->
|
283 |
+
</div>
|
284 |
+
|
285 |
+
<!-- Main Menu -->
|
286 |
+
<nav id="main-menu" class="overlay side-right">
|
287 |
+
<ul>
|
288 |
+
<li>
|
289 |
+
<button id="btn-berth" class="menu-btn pulse" title="Gestion des postes à quai">
|
290 |
+
<i class="fas fa-anchor text-2xl text-blue-300"></i>
|
291 |
+
</button>
|
292 |
+
</li>
|
293 |
+
<li>
|
294 |
+
<button id="btn-yard" class="menu-btn" title="Gestion du yard">
|
295 |
+
<i class="fas fa-boxes text-2xl text-blue-300"></i>
|
296 |
+
</button>
|
297 |
+
</li>
|
298 |
+
<li>
|
299 |
+
<button id="btn-gate" class="menu-btn" title="Gestion des accès">
|
300 |
+
<i class="fas fa-truck-moving text-2xl text-blue-300"></i>
|
301 |
+
</button>
|
302 |
+
</li>
|
303 |
+
<li>
|
304 |
+
<button id="btn-schedule" class="menu-btn" title="Planification équipements/personnel">
|
305 |
+
<i class="fas fa-calendar-alt text-2xl text-blue-300"></i>
|
306 |
+
</button>
|
307 |
+
</li>
|
308 |
+
<li>
|
309 |
+
<button id="btn-billing" class="menu-btn" title="Facturation & rapports">
|
310 |
+
<i class="fas fa-file-invoice-dollar text-2xl text-blue-300"></i>
|
311 |
+
</button>
|
312 |
+
</li>
|
313 |
+
</ul>
|
314 |
+
</nav>
|
315 |
+
|
316 |
+
<!-- AI Chat Panel -->
|
317 |
+
<div id="chat-panel" class="overlay bottom">
|
318 |
+
<div id="chat-header" class="flex items-center justify-between px-4 py-2 border-b border-slate-700">
|
319 |
+
<h3 class="font-semibold text-blue-300 flex items-center">
|
320 |
+
<img src="https://via.placeholder.com/24/38bdf8/ffffff?text=M2H" alt="M2H Majestic" class="mr-2 rounded">
|
321 |
+
M2H Majestic Assistant
|
322 |
+
</h3>
|
323 |
+
<button id="minimize-chat" class="text-slate-400 hover:text-white">
|
324 |
+
<i class="fas fa-minus"></i>
|
325 |
+
</button>
|
326 |
+
</div>
|
327 |
+
<div id="chat-messages">
|
328 |
+
<div class="chat-message ai-message">
|
329 |
+
<p>Bonjour! Je suis votre assistant M2H Majestic pour la gestion du port de Santo Domingo. Comment puis-je vous aider aujourd'hui?</p>
|
330 |
+
<p class="text-xs text-slate-400 mt-1">Maintenant</p>
|
331 |
+
</div>
|
332 |
+
<div class="chat-message ai-message">
|
333 |
+
<p>Je peux vous fournir des informations sur les navires en approche, l'état des quais, la capacité du yard et plus encore.</p>
|
334 |
+
<p class="text-xs text-slate-400 mt-1">Maintenant</p>
|
335 |
+
</div>
|
336 |
+
</div>
|
337 |
+
<div id="chat-input" class="flex p-3">
|
338 |
+
<input type="text" placeholder="Demandez des informations sur les navires, les quais..."
|
339 |
+
class="flex-1 bg-slate-800 rounded-l-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
340 |
+
<button id="send-btn" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-r-lg transition">
|
341 |
+
<i class="fas fa-paper-plane"></i>
|
342 |
+
</button>
|
343 |
+
</div>
|
344 |
+
</div>
|
345 |
+
|
346 |
+
<!-- User Info -->
|
347 |
+
<div id="user-info" class="overlay top-right flex items-center">
|
348 |
+
<img src="https://randomuser.me/api/portraits/men/32.jpg" alt="Avatar" class="avatar">
|
349 |
+
<div>
|
350 |
+
<p class="font-medium">Jean Dupont</p>
|
351 |
+
<p class="text-xs text-blue-300">Yield Manager</p>
|
352 |
+
</div>
|
353 |
+
<button class="ml-4 text-slate-400 hover:text-white">
|
354 |
+
<i class="fas fa-cog"></i>
|
355 |
+
</button>
|
356 |
+
</div>
|
357 |
+
|
358 |
+
<!-- Context Menu (hidden by default) -->
|
359 |
+
<div id="context-menu" class="overlay">
|
360 |
+
<h3 class="font-semibold mb-3 text-blue-300 border-b border-slate-700 pb-2 flex items-center">
|
361 |
+
<i class="fas fa-info-circle mr-2"></i>
|
362 |
+
<span id="context-title">Quai 1 - Terminal Santo Domingo</span>
|
363 |
+
</h3>
|
364 |
+
<div class="mb-3">
|
365 |
+
<div class="flex items-center">
|
366 |
+
<span class="status-indicator status-open"></span>
|
367 |
+
<span>Statut: <strong>Occupé</strong></span>
|
368 |
+
</div>
|
369 |
+
<div class="mt-1">
|
370 |
+
<span>Navire: <strong>MSC Caribe</strong> (Porte-conteneurs)</span>
|
371 |
+
</div>
|
372 |
+
<div class="mt-1">
|
373 |
+
<span>ETA: <strong>14:30</strong> (dans 2h15)</span>
|
374 |
+
</div>
|
375 |
+
</div>
|
376 |
+
<div class="mb-3">
|
377 |
+
<div class="flex justify-between text-sm">
|
378 |
+
<span>Progression déchargement:</span>
|
379 |
+
<span>65%</span>
|
380 |
+
</div>
|
381 |
+
<div class="progress-bar">
|
382 |
+
<div class="progress-fill" style="width: 65%"></div>
|
383 |
+
</div>
|
384 |
+
</div>
|
385 |
+
<div class="context-item flex items-center">
|
386 |
+
<i class="fas fa-info-circle mr-3 text-blue-300"></i>
|
387 |
+
<span>Voir les détails complets</span>
|
388 |
+
</div>
|
389 |
+
<div class="context-item flex items-center">
|
390 |
+
<i class="fas fa-calendar-check mr-3 text-blue-300"></i>
|
391 |
+
<span>Planifier l'accostage</span>
|
392 |
+
</div>
|
393 |
+
<div class="context-item flex items-center">
|
394 |
+
<i class="fas fa-truck-loading mr-3 text-blue-300"></i>
|
395 |
+
<span>Assigner des grues</span>
|
396 |
+
</div>
|
397 |
+
<div class="context-item flex items-center">
|
398 |
+
<i class="fas fa-file-invoice mr-3 text-blue-300"></i>
|
399 |
+
<span>Générer un rapport</span>
|
400 |
+
</div>
|
401 |
+
</div>
|
402 |
+
|
403 |
+
<!-- Notification Toast -->
|
404 |
+
<div id="notification" class="fixed top-5 left-1/2 transform -translate-x-1/2 bg-green-600 text-white px-6 py-3 rounded-lg shadow-lg hidden flex items-center">
|
405 |
+
<i class="fas fa-check-circle mr-2"></i>
|
406 |
+
<span>Berth allocation updated successfully!</span>
|
407 |
+
</div>
|
408 |
+
|
409 |
+
<script>
|
410 |
+
// Initialize Three.js scene
|
411 |
+
let scene, camera, renderer, controls;
|
412 |
+
let portObjects = {};
|
413 |
+
let selectedObject = null;
|
414 |
+
|
415 |
+
function initThreeJS() {
|
416 |
+
// Create scene
|
417 |
+
scene = new THREE.Scene();
|
418 |
+
scene.background = new THREE.Color(0x1e3c72);
|
419 |
+
scene.fog = new THREE.FogExp2(0x1e3c72, 0.001);
|
420 |
+
|
421 |
+
// Create camera
|
422 |
+
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
423 |
+
camera.position.set(0, 150, 300);
|
424 |
+
|
425 |
+
// Create renderer
|
426 |
+
renderer = new THREE.WebGLRenderer({ antialias: true });
|
427 |
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
428 |
+
renderer.shadowMap.enabled = true;
|
429 |
+
document.getElementById('viewport-3d').appendChild(renderer.domElement);
|
430 |
+
|
431 |
+
// Add controls
|
432 |
+
controls = new THREE.OrbitControls(camera, renderer.domElement);
|
433 |
+
controls.enableDamping = true;
|
434 |
+
controls.dampingFactor = 0.05;
|
435 |
+
controls.minDistance = 50;
|
436 |
+
controls.maxDistance = 500;
|
437 |
+
controls.maxPolarAngle = Math.PI * 0.9;
|
438 |
+
|
439 |
+
// Add lights
|
440 |
+
const ambientLight = new THREE.AmbientLight(0x404040);
|
441 |
+
scene.add(ambientLight);
|
442 |
+
|
443 |
+
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
444 |
+
directionalLight.position.set(100, 300, 100);
|
445 |
+
directionalLight.castShadow = true;
|
446 |
+
directionalLight.shadow.mapSize.width = 2048;
|
447 |
+
directionalLight.shadow.mapSize.height = 2048;
|
448 |
+
scene.add(directionalLight);
|
449 |
+
|
450 |
+
// Add water
|
451 |
+
const waterGeometry = new THREE.PlaneGeometry(1000, 1000);
|
452 |
+
const waterMaterial = new THREE.MeshStandardMaterial({
|
453 |
+
color: 0x1e88e5,
|
454 |
+
roughness: 0.1,
|
455 |
+
metalness: 0.5
|
456 |
+
});
|
457 |
+
const water = new THREE.Mesh(waterGeometry, waterMaterial);
|
458 |
+
water.rotation.x = -Math.PI / 2;
|
459 |
+
water.position.y = -5;
|
460 |
+
scene.add(water);
|
461 |
+
|
462 |
+
// Add ground
|
463 |
+
const groundGeometry = new THREE.PlaneGeometry(1000, 1000);
|
464 |
+
const groundMaterial = new THREE.MeshStandardMaterial({
|
465 |
+
color: 0x4b5563,
|
466 |
+
roughness: 0.8
|
467 |
+
});
|
468 |
+
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
|
469 |
+
ground.rotation.x = -Math.PI / 2;
|
470 |
+
ground.position.y = -5;
|
471 |
+
ground.receiveShadow = true;
|
472 |
+
scene.add(ground);
|
473 |
+
|
474 |
+
// Create port infrastructure
|
475 |
+
createPortInfrastructure();
|
476 |
+
|
477 |
+
// Handle window resize
|
478 |
+
window.addEventListener('resize', onWindowResize);
|
479 |
+
|
480 |
+
// Start animation loop
|
481 |
+
animate();
|
482 |
+
}
|
483 |
+
|
484 |
+
function createPortInfrastructure() {
|
485 |
+
// Create terminals (Santo Domingo, Don Diego, Sans Souci)
|
486 |
+
createTerminal('Santo Domingo', -200, 0, 0, 300, 200, [
|
487 |
+
{ id: 1, length: 150, width: 30, occupied: true },
|
488 |
+
{ id: 2, length: 180, width: 30, occupied: false },
|
489 |
+
{ id: 3, length: 160, width: 30, occupied: true },
|
490 |
+
{ id: 4, length: 140, width: 30, occupied: false },
|
491 |
+
{ id: 5, length: 170, width: 30, occupied: true }
|
492 |
+
]);
|
493 |
+
|
494 |
+
createTerminal('Don Diego', 200, 0, 0, 250, 150, [
|
495 |
+
{ id: 6, length: 120, width: 25, occupied: false },
|
496 |
+
{ id: 7, length: 130, width: 25, occupied: true },
|
497 |
+
{ id: 8, length: 140, width: 25, occupied: false }
|
498 |
+
]);
|
499 |
+
|
500 |
+
createTerminal('Sans Souci', 0, 0, -200, 250, 250, [
|
501 |
+
{ id: 9, length: 250, width: 35, occupied: true }
|
502 |
+
]);
|
503 |
+
|
504 |
+
// Create storage areas
|
505 |
+
createStorageArea('Vehicle Storage', -100, 0, 150, 200, 150, 7000);
|
506 |
+
createStorageArea('Warehouse A', -300, 0, 50, 100, 80);
|
507 |
+
createStorageArea('Warehouse B', -350, 0, -50, 100, 80);
|
508 |
+
|
509 |
+
// Create gates
|
510 |
+
createGate('Main Gate', -400, 0, 0);
|
511 |
+
createGate('Truck Gate', -450, 0, 100);
|
512 |
+
createGate('Secondary Gate', -450, 0, -100);
|
513 |
+
}
|
514 |
+
|
515 |
+
function createTerminal(name, x, y, z, width, depth, berths) {
|
516 |
+
// Create terminal base
|
517 |
+
const terminalGeometry = new THREE.BoxGeometry(width, 5, depth);
|
518 |
+
const terminalMaterial = new THREE.MeshStandardMaterial({
|
519 |
+
color: 0x6b7280,
|
520 |
+
roughness: 0.7
|
521 |
+
});
|
522 |
+
const terminal = new THREE.Mesh(terminalGeometry, terminalMaterial);
|
523 |
+
terminal.position.set(x, y - 2.5, z);
|
524 |
+
terminal.receiveShadow = true;
|
525 |
+
terminal.userData = { type: 'terminal', name: name };
|
526 |
+
scene.add(terminal);
|
527 |
+
portObjects[`terminal_${name}`] = terminal;
|
528 |
+
|
529 |
+
// Create berths
|
530 |
+
berths.forEach((berth, index) => {
|
531 |
+
const berthX = x + (index - (berths.length - 1) / 2) * (width / berths.length);
|
532 |
+
const berthZ = z + depth / 2;
|
533 |
+
|
534 |
+
const berthGeometry = new THREE.BoxGeometry(berth.length, 10, berth.width);
|
535 |
+
const berthMaterial = new THREE.MeshStandardMaterial({
|
536 |
+
color: berth.occupied ? 0xef4444 : 0x10b981,
|
537 |
+
roughness: 0.6
|
538 |
+
});
|
539 |
+
const berthMesh = new THREE.Mesh(berthGeometry, berthMaterial);
|
540 |
+
berthMesh.position.set(berthX, y, berthZ);
|
541 |
+
berthMesh.userData = {
|
542 |
+
type: 'berth',
|
543 |
+
id: berth.id,
|
544 |
+
terminal: name,
|
545 |
+
occupied: berth.occupied,
|
546 |
+
vessel: berth.occupied ? `Vessel ${Math.floor(Math.random() * 1000)}` : null
|
547 |
+
};
|
548 |
+
scene.add(berthMesh);
|
549 |
+
portObjects[`berth_${name}_${berth.id}`] = berthMesh;
|
550 |
+
|
551 |
+
// Add vessel if occupied
|
552 |
+
if (berth.occupied) {
|
553 |
+
const vesselGeometry = new THREE.BoxGeometry(berth.length * 0.8, 20, berth.width * 2);
|
554 |
+
const vesselMaterial = new THREE.MeshStandardMaterial({
|
555 |
+
color: 0x3b82f6,
|
556 |
+
roughness: 0.5
|
557 |
+
});
|
558 |
+
const vessel = new THREE.Mesh(vesselGeometry, vesselMaterial);
|
559 |
+
vessel.position.set(berthX, y + 15, berthZ + berth.width);
|
560 |
+
vessel.userData = { type: 'vessel', berth: berth.id };
|
561 |
+
scene.add(vessel);
|
562 |
+
portObjects[`vessel_${name}_${berth.id}`] = vessel;
|
563 |
+
}
|
564 |
+
});
|
565 |
+
|
566 |
+
// Add terminal building
|
567 |
+
const buildingGeometry = new THREE.BoxGeometry(width * 0.6, 30, depth * 0.3);
|
568 |
+
const buildingMaterial = new THREE.MeshStandardMaterial({
|
569 |
+
color: 0x9ca3af,
|
570 |
+
roughness: 0.8
|
571 |
+
});
|
572 |
+
const building = new THREE.Mesh(buildingGeometry, buildingMaterial);
|
573 |
+
building.position.set(x, y + 15, z - depth * 0.35);
|
574 |
+
building.castShadow = true;
|
575 |
+
scene.add(building);
|
576 |
+
}
|
577 |
+
|
578 |
+
function createStorageArea(name, x, y, z, width, depth, capacity) {
|
579 |
+
const areaGeometry = new THREE.BoxGeometry(width, 5, depth);
|
580 |
+
const areaMaterial = new THREE.MeshStandardMaterial({
|
581 |
+
color: 0x4b5563,
|
582 |
+
roughness: 0.8
|
583 |
+
});
|
584 |
+
const area = new THREE.Mesh(areaGeometry, areaMaterial);
|
585 |
+
area.position.set(x, y - 2.5, z);
|
586 |
+
area.userData = { type: 'storage', name: name, capacity: capacity };
|
587 |
+
scene.add(area);
|
588 |
+
portObjects[`storage_${name}`] = area;
|
589 |
+
|
590 |
+
// Add containers if it's a container yard
|
591 |
+
if (name.includes('Vehicle')) {
|
592 |
+
for (let i = 0; i < 20; i++) {
|
593 |
+
const containerX = x + (Math.random() - 0.5) * width * 0.8;
|
594 |
+
const containerZ = z + (Math.random() - 0.5) * depth * 0.8;
|
595 |
+
|
596 |
+
const containerGeometry = new THREE.BoxGeometry(5, 3, 2);
|
597 |
+
const containerMaterial = new THREE.MeshStandardMaterial({
|
598 |
+
color: 0x3b82f6,
|
599 |
+
roughness: 0.7
|
600 |
+
});
|
601 |
+
const container = new THREE.Mesh(containerGeometry, containerMaterial);
|
602 |
+
container.position.set(containerX, y + 1.5, containerZ);
|
603 |
+
container.rotation.y = Math.random() * Math.PI;
|
604 |
+
scene.add(container);
|
605 |
+
}
|
606 |
+
}
|
607 |
+
}
|
608 |
+
|
609 |
+
function createGate(name, x, y, z) {
|
610 |
+
const gateGeometry = new THREE.BoxGeometry(10, 20, 5);
|
611 |
+
const gateMaterial = new THREE.MeshStandardMaterial({
|
612 |
+
color: 0xf59e0b,
|
613 |
+
roughness: 0.6
|
614 |
+
});
|
615 |
+
const gate = new THREE.Mesh(gateGeometry, gateMaterial);
|
616 |
+
gate.position.set(x, y + 10, z);
|
617 |
+
gate.userData = { type: 'gate', name: name, status: Math.random() > 0.5 ? 'open' : 'closed' };
|
618 |
+
scene.add(gate);
|
619 |
+
portObjects[`gate_${name}`] = gate;
|
620 |
+
|
621 |
+
// Add road
|
622 |
+
const roadGeometry = new THREE.PlaneGeometry(30, 100);
|
623 |
+
const roadMaterial = new THREE.MeshStandardMaterial({
|
624 |
+
color: 0x6b7280,
|
625 |
+
roughness: 0.9
|
626 |
+
});
|
627 |
+
const road = new THREE.Mesh(roadGeometry, roadMaterial);
|
628 |
+
road.rotation.x = -Math.PI / 2;
|
629 |
+
road.position.set(x, y, z);
|
630 |
+
scene.add(road);
|
631 |
+
}
|
632 |
+
|
633 |
+
function onWindowResize() {
|
634 |
+
camera.aspect = window.innerWidth / window.innerHeight;
|
635 |
+
camera.updateProjectionMatrix();
|
636 |
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
637 |
+
}
|
638 |
+
|
639 |
+
function animate() {
|
640 |
+
requestAnimationFrame(animate);
|
641 |
+
controls.update();
|
642 |
+
renderer.render(scene, camera);
|
643 |
+
|
644 |
+
// Update minimap indicators
|
645 |
+
updateMinimap();
|
646 |
+
}
|
647 |
+
|
648 |
+
function updateMinimap() {
|
649 |
+
// Clear existing indicators
|
650 |
+
document.querySelectorAll('.vessel-marker, .minimap-indicator, .camera-indicator, .camera-direction').forEach(el => el.remove());
|
651 |
+
|
652 |
+
// Add vessel markers
|
653 |
+
Object.keys(portObjects).forEach(key => {
|
654 |
+
const obj = portObjects[key];
|
655 |
+
if (obj.userData.type === 'vessel' || obj.userData.type === 'gate') {
|
656 |
+
const position = obj.position.clone().project(camera);
|
657 |
+
|
658 |
+
// Convert normalized device coordinates to minimap coordinates
|
659 |
+
const x = ((position.x * 0.5 + 0.5) * 220) - 110;
|
660 |
+
const y = ((-position.y * 0.5 + 0.5) * 220) - 110;
|
661 |
+
|
662 |
+
if (x >= 0 && x <= 220 && y >= 0 && y <= 220) {
|
663 |
+
const marker = document.createElement('div');
|
664 |
+
marker.className = obj.userData.type === 'vessel' ? 'vessel-marker' : 'minimap-indicator';
|
665 |
+
marker.style.left = `${x}px`;
|
666 |
+
marker.style.top = `${y}px`;
|
667 |
+
document.getElementById('minimap').appendChild(marker);
|
668 |
+
}
|
669 |
+
}
|
670 |
+
});
|
671 |
+
|
672 |
+
// Add camera indicator
|
673 |
+
const cameraPosition = camera.position.clone().project(camera);
|
674 |
+
const camX = ((cameraPosition.x * 0.5 + 0.5) * 220) - 110;
|
675 |
+
const camY = ((-cameraPosition.y * 0.5 + 0.5) * 220) - 110;
|
676 |
+
|
677 |
+
if (camX >= 0 && camX <= 220 && camY >= 0 && camY <= 220) {
|
678 |
+
const camMarker = document.createElement('div');
|
679 |
+
camMarker.className = 'camera-indicator';
|
680 |
+
camMarker.style.left = `${camX}px`;
|
681 |
+
camMarker.style.top = `${camY}px`;
|
682 |
+
document.getElementById('minimap').appendChild(camMarker);
|
683 |
+
|
684 |
+
// Add camera direction indicator
|
685 |
+
const direction = document.createElement('div');
|
686 |
+
direction.className = 'camera-direction';
|
687 |
+
direction.style.left = `${camX}px`;
|
688 |
+
direction.style.top = `${camY}px`;
|
689 |
+
|
690 |
+
// Calculate rotation based on camera direction
|
691 |
+
const target = new THREE.Vector3(0, 0, -1);
|
692 |
+
target.applyQuaternion(camera.quaternion);
|
693 |
+
const angle = Math.atan2(target.x, target.z);
|
694 |
+
direction.style.setProperty('--rotation', `${angle}rad`);
|
695 |
+
|
696 |
+
document.getElementById('minimap').appendChild(direction);
|
697 |
+
}
|
698 |
+
}
|
699 |
+
|
700 |
+
// Initialize Three.js when DOM is loaded
|
701 |
+
document.addEventListener('DOMContentLoaded', () => {
|
702 |
+
initThreeJS();
|
703 |
+
|
704 |
+
// Set up raycasting for object selection
|
705 |
+
const raycaster = new THREE.Raycaster();
|
706 |
+
const mouse = new THREE.Vector2();
|
707 |
+
|
708 |
+
function onMouseClick(event) {
|
709 |
+
// Calculate mouse position in normalized device coordinates
|
710 |
+
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
711 |
+
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
712 |
+
|
713 |
+
// Update the raycaster
|
714 |
+
raycaster.setFromCamera(mouse, camera);
|
715 |
+
|
716 |
+
// Calculate objects intersecting the ray
|
717 |
+
const intersects = raycaster.intersectObjects(scene.children);
|
718 |
+
|
719 |
+
if (intersects.length > 0) {
|
720 |
+
const object = intersects[0].object;
|
721 |
+
|
722 |
+
// Check if it's a port object we care about
|
723 |
+
if (object.userData && (object.userData.type === 'berth' || object.userData.type === 'gate')) {
|
724 |
+
selectedObject = object;
|
725 |
+
showContextMenu(object, event.clientX, event.clientY);
|
726 |
+
|
727 |
+
// Move camera to focus on the object
|
728 |
+
moveCameraToObject(object);
|
729 |
+
}
|
730 |
+
} else {
|
731 |
+
// Clicked on empty space, hide context menu
|
732 |
+
document.getElementById('context-menu').style.display = 'none';
|
733 |
+
}
|
734 |
+
}
|
735 |
+
|
736 |
+
function moveCameraToObject(object) {
|
737 |
+
// Calculate target position for camera
|
738 |
+
const targetPosition = object.position.clone();
|
739 |
+
targetPosition.y += 50; // Elevate camera
|
740 |
+
targetPosition.z -= 100; // Move camera back
|
741 |
+
|
742 |
+
// Animate camera movement
|
743 |
+
const duration = 1000; // ms
|
744 |
+
const startPosition = camera.position.clone();
|
745 |
+
const startTime = Date.now();
|
746 |
+
|
747 |
+
function animateCamera() {
|
748 |
+
const elapsed = Date.now() - startTime;
|
749 |
+
const progress = Math.min(elapsed / duration, 1);
|
750 |
+
|
751 |
+
camera.position.lerpVectors(startPosition, targetPosition, progress);
|
752 |
+
|
753 |
+
if (progress < 1) {
|
754 |
+
requestAnimationFrame(animateCamera);
|
755 |
+
} else {
|
756 |
+
// Camera arrived, update controls target
|
757 |
+
controls.target.copy(object.position);
|
758 |
+
}
|
759 |
+
}
|
760 |
+
|
761 |
+
animateCamera();
|
762 |
+
}
|
763 |
+
|
764 |
+
function showContextMenu(object, x, y) {
|
765 |
+
const contextMenu = document.getElementById('context-menu');
|
766 |
+
const title = document.getElementById('context-title');
|
767 |
+
|
768 |
+
if (object.userData.type === 'berth') {
|
769 |
+
title.innerHTML = `<i class="fas fa-anchor mr-2"></i>Quai ${object.userData.id} - Terminal ${object.userData.terminal}`;
|
770 |
+
|
771 |
+
// Update status indicator
|
772 |
+
const statusIndicator = contextMenu.querySelector('.status-indicator');
|
773 |
+
statusIndicator.className = 'status-indicator ' +
|
774 |
+
(object.userData.occupied ? 'status-open' : 'status-closed');
|
775 |
+
|
776 |
+
// Update status text
|
777 |
+
contextMenu.querySelector('.status-indicator').nextElementSibling.innerHTML =
|
778 |
+
`Statut: <strong>${object.userData.occupied ? 'Occupé' : 'Libre'}</strong>`;
|
779 |
+
|
780 |
+
// Update vessel info if occupied
|
781 |
+
if (object.userData.occupied) {
|
782 |
+
contextMenu.querySelector('.mt-1 > span').innerHTML =
|
783 |
+
`Navire: <strong>${object.userData.vessel}</strong> (Porte-conteneurs)`;
|
784 |
+
} else {
|
785 |
+
contextMenu.querySelector('.mt-1 > span').innerHTML =
|
786 |
+
`Navire: <strong>Aucun</strong>`;
|
787 |
+
}
|
788 |
+
|
789 |
+
// Random ETA for demo
|
790 |
+
const hours = Math.floor(Math.random() * 24);
|
791 |
+
const minutes = Math.floor(Math.random() * 60);
|
792 |
+
const etaTime = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
793 |
+
contextMenu.querySelectorAll('.mt-1')[1].innerHTML =
|
794 |
+
`ETA: <strong>${etaTime}</strong> (dans ${Math.floor(Math.random() * 6) + 1}h${Math.floor(Math.random() * 60)})`;
|
795 |
+
|
796 |
+
// Random progress for demo
|
797 |
+
const progress = object.userData.occupied ? Math.floor(Math.random() * 100) : 0;
|
798 |
+
contextMenu.querySelector('.progress-fill').style.width = `${progress}%`;
|
799 |
+
contextMenu.querySelectorAll('.text-sm span')[1].textContent = `${progress}%`;
|
800 |
+
} else if (object.userData.type === 'gate') {
|
801 |
+
title.innerHTML = `<i class="fas fa-truck-moving mr-2"></i>${object.userData.name}`;
|
802 |
+
|
803 |
+
// Update status indicator
|
804 |
+
const statusIndicator = contextMenu.querySelector('.status-indicator');
|
805 |
+
statusIndicator.className = 'status-indicator ' +
|
806 |
+
(object.userData.status === 'open' ? 'status-open' : 'status-closed');
|
807 |
+
|
808 |
+
// Update status text
|
809 |
+
contextMenu.querySelector('.status-indicator').nextElementSibling.innerHTML =
|
810 |
+
`Statut: <strong>${object.userData.status === 'open' ? 'Ouvert' : 'Fermé'}</strong>`;
|
811 |
+
|
812 |
+
// Update other info
|
813 |
+
contextMenu.querySelector('.mt-1 > span').innerHTML =
|
814 |
+
`Trafic: <strong>${Math.floor(Math.random() * 100)} camions/h</strong>`;
|
815 |
+
|
816 |
+
contextMenu.querySelectorAll('.mt-1')[1].innerHTML =
|
817 |
+
`Prochain: <strong>Camion ${Math.floor(Math.random() * 1000)}</strong>`;
|
818 |
+
|
819 |
+
// For gates, we'll show queue progress instead of loading
|
820 |
+
const queue = Math.floor(Math.random() * 100);
|
821 |
+
contextMenu.querySelector('.progress-fill').style.width = `${queue}%`;
|
822 |
+
contextMenu.querySelectorAll('.text-sm span')[1].textContent = `${queue}%`;
|
823 |
+
contextMenu.querySelectorAll('.text-sm span')[0].textContent = 'File d\'attente:';
|
824 |
+
}
|
825 |
+
|
826 |
+
// Position and show the context menu
|
827 |
+
contextMenu.style.left = `${x}px`;
|
828 |
+
contextMenu.style.top = `${y}px`;
|
829 |
+
contextMenu.style.display = 'block';
|
830 |
+
}
|
831 |
+
|
832 |
+
// Add click event listener
|
833 |
+
window.addEventListener('click', onMouseClick);
|
834 |
+
|
835 |
+
// Menu button interactions
|
836 |
+
const menuButtons = document.querySelectorAll('#main-menu .menu-btn');
|
837 |
+
menuButtons.forEach(btn => {
|
838 |
+
btn.addEventListener('click', function() {
|
839 |
+
menuButtons.forEach(b => b.classList.remove('active'));
|
840 |
+
this.classList.add('active');
|
841 |
+
|
842 |
+
// Show notification
|
843 |
+
const notification = document.getElementById('notification');
|
844 |
+
notification.textContent = `${this.getAttribute('title')} panel opened`;
|
845 |
+
notification.classList.remove('hidden');
|
846 |
+
notification.classList.remove('bg-green-600', 'bg-red-600', 'bg-blue-600');
|
847 |
+
|
848 |
+
// Change color based on which button was clicked
|
849 |
+
if (this.id === 'btn-billing' || this.id === 'btn-schedule') {
|
850 |
+
notification.classList.add('bg-blue-600');
|
851 |
+
} else if (this.id === 'btn-gate') {
|
852 |
+
notification.classList.add('bg-yellow-600');
|
853 |
+
} else {
|
854 |
+
notification.classList.add('bg-green-600');
|
855 |
+
}
|
856 |
+
|
857 |
+
setTimeout(() => {
|
858 |
+
notification.classList.add('hidden');
|
859 |
+
}, 3000);
|
860 |
+
});
|
861 |
+
});
|
862 |
+
|
863 |
+
// Simulate chat interaction
|
864 |
+
document.getElementById('send-btn').addEventListener('click', sendMessage);
|
865 |
+
document.querySelector('#chat-input input').addEventListener('keypress', function(e) {
|
866 |
+
if (e.key === 'Enter') sendMessage();
|
867 |
+
});
|
868 |
+
|
869 |
+
function sendMessage() {
|
870 |
+
const input = document.querySelector('#chat-input input');
|
871 |
+
const message = input.value.trim();
|
872 |
+
if (message) {
|
873 |
+
// Add user message
|
874 |
+
const chatContainer = document.getElementById('chat-messages');
|
875 |
+
const userMsg = document.createElement('div');
|
876 |
+
userMsg.className = 'chat-message user-message';
|
877 |
+
userMsg.innerHTML = `
|
878 |
+
<p>${message}</p>
|
879 |
+
<p class="text-xs text-slate-400 mt-1">Maintenant</p>
|
880 |
+
`;
|
881 |
+
chatContainer.appendChild(userMsg);
|
882 |
+
|
883 |
+
// Clear input
|
884 |
+
input.value = '';
|
885 |
+
|
886 |
+
// Scroll to bottom
|
887 |
+
chatContainer.scrollTop = chatContainer.scrollHeight;
|
888 |
+
|
889 |
+
// Simulate AI response after a delay
|
890 |
+
setTimeout(() => {
|
891 |
+
const responses = [
|
892 |
+
"J'ai vérifié le système. Il y a 3 postes à quai disponibles qui peuvent accueillir ce navire.",
|
893 |
+
"Le yard a actuellement une capacité restante de 45% dans la Zone C.",
|
894 |
+
"La porte 2 connaît des retards en raison d'un trafic accru. Envisagez de rediriger vers la porte 3.",
|
895 |
+
"J'ai programmé 2 grues pour le navire MSC Oscar, comme demandé.",
|
896 |
+
"Le rapport de facturation pour le Q3 a été généré et envoyé à votre email."
|
897 |
+
];
|
898 |
+
|
899 |
+
const aiMsg = document.createElement('div');
|
900 |
+
aiMsg.className = 'chat-message ai-message';
|
901 |
+
aiMsg.innerHTML = `
|
902 |
+
<p>${responses[Math.floor(Math.random() * responses.length)]}</p>
|
903 |
+
<p class="text-xs text-slate-400 mt-1">Maintenant</p>
|
904 |
+
`;
|
905 |
+
chatContainer.appendChild(aiMsg);
|
906 |
+
chatContainer.scrollTop = chatContainer.scrollHeight;
|
907 |
+
}, 1000 + Math.random() * 2000);
|
908 |
+
}
|
909 |
+
}
|
910 |
+
|
911 |
+
// Minimize chat
|
912 |
+
document.getElementById('minimize-chat').addEventListener('click', function() {
|
913 |
+
const chatPanel = document.getElementById('chat-panel');
|
914 |
+
const messages = document.getElementById('chat-messages');
|
915 |
+
const input = document.getElementById('chat-input');
|
916 |
+
|
917 |
+
if (messages.style.display !== 'none') {
|
918 |
+
messages.style.display = 'none';
|
919 |
+
input.style.display = 'none';
|
920 |
+
this.innerHTML = '<i class="fas fa-plus"></i>';
|
921 |
+
chatPanel.style.transform = 'translateX(-50%) translateY(calc(100% - 40px))';
|
922 |
+
} else {
|
923 |
+
messages.style.display = 'block';
|
924 |
+
input.style.display = 'flex';
|
925 |
+
this.innerHTML = '<i class="fas fa-minus"></i>';
|
926 |
+
chatPanel.style.transform = 'translateX(-50%)';
|
927 |
+
}
|
928 |
+
});
|
929 |
+
|
930 |
+
// Simulate initial notification
|
931 |
+
setTimeout(() => {
|
932 |
+
const notification = document.getElementById('notification');
|
933 |
+
notification.textContent = 'Navire MSC Lucy ETA mis à jour à 14:30';
|
934 |
+
notification.classList.remove('hidden');
|
935 |
+
|
936 |
+
setTimeout(() => {
|
937 |
+
notification.classList.add('hidden');
|
938 |
+
}, 3000);
|
939 |
+
}, 1500);
|
940 |
+
});
|
941 |
+
</script>
|
942 |
+
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=arkleinberg/tos" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
|
943 |
+
</html>
|
prompts.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
<body> <!-- Conteneur principal pour la scène 3D Three.js --> <div id="viewport-3d"></div> <!-- Mini-carte 2D en superposition --> <div id="minimap" class="overlay top-right"> <!-- Par exemple une image ou un canvas pour la carte --> <img src="minicarte-port.png" alt="Mini-carte du port"/> </div> <!-- Menu principal vertical (icônes) --> <nav id="main-menu" class="overlay side-right"> <ul> <li><button id="btn-berth" title="Berth Management"><i class="icon-anchor"></i></button></li> <li><button id="btn-yard" title="Yard Management"><i class="icon-yard"></i></button></li> <li><button id="btn-gate" title="Gate Management"><i class="icon-gate"></i></button></li> <li><button id="btn-schedule" title="Equipment & Staff Scheduling"><i class="icon-crane"></i></button></li> <li><button id="btn-billing" title="Billing & Reporting"><i class="icon-report"></i></button></li> </ul> </nav> <!-- Panneau de chat IA --> <div id="chat-panel" class="overlay bottom"> <div id="chat-messages"></div> <div id="chat-input"> <input type="text" placeholder="Entrez votre question..."/> <button id="send-btn">Envoyer</button> </div> </div> <!-- Info utilisateur en haut à droite --> <div id="user-info" class="overlay top-right"> <img src="avatar.png" alt="Avatar" class="avatar"/> <span class="username">Jean Dupont – Yield Manager</span> </div> <!-- Menu contextuel (exemple, caché par défaut) --> <div id="context-menu" class="overlay info-panel" style="display:none;"></div> <!-- Inclusion du script Three.js et des scripts applicatifs --> <script src="three.min.js"></script> <script src="app.js"></script> </body>
|
2 |
+
Interface 3D en temps réel du terminal portuaire de Santo Domingo Modélisation 3D interactive du port L’application web utilise Three.js pour afficher une représentation 3D en temps réel du port de Santo Domingo. Tous les éléments clés du terminal sont modélisés : les quais (postes d’accostage), les zones de fret (aire de conteneurs, parc à véhicules), les gates d’entrée/sortie camions, ainsi que les infrastructures visibles (bâtiments du terminal, entrepôts, clôtures, etc.). Par exemple, on inclut les huit postes à quai répertoriés dans le Port Handbook : le terminal Santo Domingo comprend les docks 1 à 5 sansouci.com.do dédiés au fret (conteneurs, cargo roulier, ferry), tandis que le terminal Don Diego accueille les navires de croisière sur les quais n°6, 7 et 8 sansouci.com.do . Le terminal Sans Souci possède quant à lui un grand quai de 250 mètres pour les paquebots et les navires véhicules sansouci.com.do . Ces différentes jetées sont modélisées aux bonnes échelles (longueur, largeur, tirant d’eau) d’après les données du Port Handbook. On représente également les espaces logistiques : par exemple la zone de stockage de véhicules (~99 500 m² pour 7 000 véhicules) à l’arrière du terminal Sans Souci sansouci.com.do , ainsi que les entrepôts de stockage (le terminal Santo Domingo en compte deux pour les marchandises d’import/export sansouci.com.do ). Chaque entité portuaire (quai, yard, entrepôt, gate…) est un objet 3D distinct dans la scène, ce qui permettra des interactions utilisateur spécifiques. Techniquement, la scène Three.js comportera un sol (terrain du port) et l’eau de la rivière, avec éventuellement des textures réalistes. Les quais peuvent être modélisés par des formes extrudées ou des primitives (boîtes allongées) placées aux emplacements exacts du plan portuaire. Des modèles simplifiés de bâtiments (terminal passagers, bureaux de douane, etc.) sont ajoutés pour coller à la réalité du site. On peut utiliser les données de la carte du port sansouci.com.do sansouci.com.do pour positionner correctement chaque élément. L’usage de lights (Three.js) aide à repérer les zones (éclairage des quais la nuit, etc.) et améliore le réalisme visuel. L’interface 3D est interactive : l’utilisateur peut zoomer, pivoter la caméra autour du port et voir l’ensemble des installations en un coup d’œil. L’affichage 3D occupe tout l’arrière-plan de la page web (un <canvas> WebGL géré par Three.js), offrant une vue globale du terminal en temps réel. Navigation et changement de perspective par clic L’utilisateur peut interagir directement avec la scène 3D. En particulier, il est possible de cliquer sur un gate ou un quai pour changer de perspective ou s’y déplacer. Par exemple, cliquer sur le modèle 3D d’une gate (barrière d’entrée) fera automatiquement déplacer la caméra vers cette zone, offrant une vue détaillée de l’entrée du terminal. De même, cliquer sur un quai ou un poste à quai centrerait la caméra sur ce poste, comme si l’on “zoomait” sur le navire amarré. Cette navigation ciblée aide l’utilisateur à rapidement focaliser sur une zone précise du port. Pour implémenter cela, on utilise le raycasting de Three.js : chaque objet 3D cliquable (gates, quais…) est détectable via un rayon lancé depuis le pointeur de la souris. Lorsqu’un clic est détecté sur un objet, une fonction de rappel va animer le déplacement de la caméra vers une nouvelle position (par ex. une position prédéfinie offrant la meilleure vue de cet objet). On peut utiliser une transition fluide (interpolation) pour déplacer la caméra, afin de garder l’interface agréable. Par exemple, cliquer sur la gate d’entrée camion pourrait déplacer la caméra à une vue à hauteur d’homme devant le portail, alors que cliquer sur le quai n°7 du terminal Don Diego ferait passer en vue du dessus du poste 7 où un paquebot est amarré. L’utilisateur peut ensuite reprendre le contrôle manuel (via orbit controls, déplacement libre) pour inspecter autour de ce point de vue. Cette interaction de navigation par clic rend l’interface fluide et intuitive : le personnel peut instantanément “sauter” d’une zone du terminal à une autre en fonction des besoins opérationnels (par ex. vérifier l’état d’un gate, puis visualiser un quai où un navire est en opération). Mini-carte 2D en vue du dessus En haut à droite de l’écran, un encart affiche une mini-carte 2D du port (vue du dessus). Cette mini-carte offre une représentation schématique du terminal vu du ciel, permettant de se repérer facilement parmi les installations. Elle peut être construite soit à partir d’une image satellite ou d’un plan du port (par exemple en utilisant la carte officielle “Port Map” du Port Handbook), soit générée dynamiquement (par ex. via une mini-scène Orthographic de Three.js ou un canvas 2D). L’aperçu 2D met en évidence les différents quais et zones du port sous forme simplifiée. Chaque quai peut être dessiné comme un segment, chaque entrepôt comme un rectangle, etc., avec des annotations ou numéros (1–8, Sans Souci, Don Diego…) pour plus de clarté. La position actuelle de la caméra 3D ou de l’élément sélectionné peut également être indiquée sur la mini-carte (par un petit triangle ou un curseur directionnel). Ainsi, si l’utilisateur navigue le port en 3D, il voit sur la mini-carte où se situe son champ de vision. La mini-carte aide à la navigation globale : en un coup d’œil on voit l’ensemble du port. On peut même rendre la mini-carte interactive – par exemple en permettant de cliquer sur un emplacement de la carte 2D pour téléporter la caméra 3D à cet endroit. Toutefois, son rôle principal est informatif. Visuellement, ce panneau 2D sera semi-transparent ou encadré, situé en haut à droite sans couvrir excessivement la vue 3D principale. Il est suffisamment détaillé pour distinguer les terminaux (Don Diego vs Santo Domingo vs Sans Souci) et les infrastructures majeures (routes d’accès, zone de stockage de véhicules, etc.), mais reste simple pour ne pas surcharger l’interface. Menus contextuels par gate/quai (informations en temps réel) Chaque gate (et par extension, chaque poste à quai) dispose d’un menu contextuel déroulant affichant les informations opérationnelles en temps réel. Lorsque l’utilisateur clique sur l’un de ces éléments (ou sur un bouton dédié), un petit panneau s’affiche à l’écran, contenant les détails tels que : Statut actuel – par exemple Ouvert/fermé pour une gate routière, ou Occupé/libre pour un quai (s’il y a un navire à quai ou non). Navire en approche / en opération – le nom du navire assigné à ce quai/gate, avec éventuellement son type (porte-conteneurs, croisière, ferry, etc.) et son identifiant (IMO ou appel). Pour un quai, ce serait le navire attendu ou en cours d’escale; pour une gate camions, ce pourrait être le prochain camion attendu ou simplement le volume de trafic entrant. ETA/Heure estimée d’arrivée – par exemple, l’heure d’arrivée prévue du navire à ce poste d’accostage, ou l’ETA de la prochaine rotation de camions à cette gate. Progression des opérations – un indicateur (texte ou barre de progression) montrant l’avancement du déchargement/chargement. Pour un navire cargo, on pourrait indiquer “Déchargement à 50%” avec une barre visuelle. Pour une gate, on pourrait indiquer le pourcentage de camions traités par rapport à la file prévue. Ce menu contextuel apparaîtra de manière fluide (par exemple en fondu ou en déroulant sous le curseur). L’utilisateur peut le refermer facilement pour retourner à la vue globale. En termes d’implémentation HTML/CSS, il s’agit d’un <div> stylé en panneau flottant, positionné soit à côté de l’élément cliqué (p. ex. fixé près du curseur sur la vue 3D), soit dans une zone fixe de l’écran réservée aux infos détaillées. Chaque menu contextuel aura un style cohérent (fond semi-transparent sombre pour survoler la 3D, texte contrasté lisible, éventuellement code couleur indiquant le statut – vert si ouvert/libre, rouge si fermé/occupé, etc.). Ces menus sont mis à jour en temps réel via les données du TOS (Terminal Operating System). Par exemple, si l’ETA d’un navire change ou si le déchargement passe à 80%, le panneau se mettra à jour automatiquement (grâce à du JavaScript recevant des événements, WebSockets ou appels périodiques). L’utilisateur peut donc cliquer à tout moment sur un quai pour avoir des informations à jour sur les opérations en cours sur ce poste. Panneau de chat IA (M2H Majestic) En bas de l’écran, un panneau de chat intègre un assistant intelligent nommé M2H Majestic. Ce chatbot IA permet aux utilisateurs de formuler des requêtes en langage naturel et d’obtenir des réponses ou de l’aide concernant les opérations portuaires. L’interface de chat se présente généralement sous forme d’une barre ou d’une fenêtre rectangulaire occupant la largeur inférieure de la page. Le panneau de chat affiche l’historique des échanges : les questions de l’utilisateur et les réponses de l’assistant M2H. Par exemple, un utilisateur pourrait taper « Quel est le prochain navire attendu au quai 5 et à quelle heure ? », et M2H Majestic répondrait en utilisant les données du système (« Le prochain navire au quai 5 est le MSC Caribe, ETA 14h30, actuellement 65% déchargé. »). L’assistant peut aider pour des tâches comme rechercher une information, expliquer une fonctionnalité du TOS, ou même effectuer des actions simples via des commandes (s’il est connecté à l’API du TOS). Visuellement, le panneau comporte une zone de texte pour saisir les questions, et des bulles de conversation pour les messages. On peut mettre l’avatar ou le logo de M2H Majestic à gauche des réponses pour bien distinguer qui parle. Ce chat étant en permanence affiché en bas (mais éventuellement rétractable), il n’obstrue pas la vue principale. L’utilisateur peut le minimiser en un petit onglet s’il a besoin de tout l’écran pour la 3D, puis le rouvrir en cas de besoin. Du point de vue technique, ce composant chat est réalisé en HTML/CSS/JS classique, éventuellement avec une bibliothèque UI pour chat. Il écoute les requêtes utilisateur et envoie ces requêtes à un service d’IA. Pour la maquette on peut simuler les réponses. L’important est qu’il soit accessible rapidement pour apporter une aide contextuelle, améliorant la fluidité de l’expérience utilisateur (au lieu d’aller chercher dans des menus complexes, on peut simplement poser la question à l’IA). Menu principal vertical (icônes TOS) Sur le côté droit de l’écran, un menu principal vertical présente sous forme d’icônes les grandes fonctions du système TOS. Ce menu est une barre latérale minimaliste contenant uniquement des boutons icônes (sans texte visible pour gagner de l’espace horizontal). Chaque icône représente l’une des fonctionnalités suivantes : Berth Management – gestion des postes à quai (icône suggérée : une ancre symbolisant l’amarrage ou un quai). Yard Management – gestion du yard/stockage (icône : un conteneur ou une empile de caisses). Gate Management – gestion des accès camions (icône : une barrière de sécurité ou un portail). Equipment & Staff Scheduling – planification des équipements et du personnel (icône : un chariot élévateur, une grue ou un pictogramme d’ouvriers/horloge). Billing & Reporting – facturation et rapports (icône : un document avec un graphique ou un symbole $). Chaque bouton est affiché sous forme d’icône monochrome épurée. Lorsqu’on survole une icône avec la souris, une infobulle (tooltip) apparaît, indiquant le nom complet de la fonctionnalité (traduite en français si besoin, ex. “Gestion des postes à quai” pour Berth Management, etc.). Cela permet aux utilisateurs de comprendre la signification de chaque icône sans occuper l’écran en permanence avec du texte. Le menu vertical est fixe à droite, centré verticalement, pour être facilement accessible peu importe le scroll ou le panorama de la 3D. En termes d’implémentation, ce menu est un élément <nav> ou <ul> stylé en position fixe sur la droite. Les icônes peuvent être des images SVG ou des polices d’icônes (FontAwesome ou autres) pour une bonne qualité d’affichage. On utilise des styles CSS pour agrandir légèrement l’icône au survol ou la surligner, améliorant l’affordance. Un clic sur une de ces icônes peut naviguer vers un écran ou une superposition spécifique correspondant au module choisi (par ex. cliquer “Berth Management” pourrait ouvrir un tableau détaillé des navires à quai, etc.). En l’absence de texte visible, les info-bulles sur survol sont essentielles pour la convivialité. Ce design tout en icônes assure une interface aérée : le regard de l’utilisateur reste focalisé sur la 3D, sans distraction, et le menu est disponible à tout moment pour accéder aux modules TOS. Barre utilisateur (avatar et rôle) en haut à droite Enfin, dans le coin supérieur droit de l’écran, l’interface affiche une petite barre utilisateur comprenant l’avatar de l’utilisateur connecté, son nom et son rôle. Par exemple, on y verra une photo de profil ou une icône générique suivie du texte « Nom d’utilisateur – Yield Manager ». Le rôle “Yield Manager” indique la fonction actuelle de l’utilisateur dans le système. Cette barre donne un contexte sur qui est connecté et ses permissions, ce qui est important dans un environnement multi-utilisateurs (par exemple, un Yield Manager n’aura pas les mêmes options qu’un Administrateur ou un Opérateur de quai). Ce composant se présente comme un simple texte à droite de la barre de navigation supérieure, ou dans un coin, aligné à droite au même niveau que le titre de l’application éventuellement. On peut l’illustrer par un petit avatar rond (40px par exemple) suivi du nom. Si on clique sur cette zone, un petit menu déroulant pourrait apparaître avec des actions utilisateur (Profil, Paramètres, Déconnexion), bien que cela ne soit pas explicitement demandé, c’est une pratique courante pour les barres utilisateurs. Visuellement, l’avatar peut être une image fournie par l’utilisateur ou un pictogramme par défaut (silhouette). Le nom et le rôle seront en texte clair. On peut styliser le rôle en italique ou en plus petite taille à côté du nom si on souhaite une présentation soignée, par ex. Jean Dupont – Yield Manager. Ce repère constant en haut de l’interface participe à la fluidité de l’expérience en rappelant le contexte (utile notamment si l’application permet de changer de rôle ou de se rendre compte sous quelle identité on opère). Architecture HTML/JS/CSS de la page Pour réaliser cette interface, on organise le code en séparant clairement la structure HTML, la mise en forme CSS et la logique JavaScript (incluant Three.js). Ci-dessous une structure HTML possible pour la page : html Copier Modifier <body> <!-- Conteneur principal pour la scène 3D Three.js --> <div id="viewport-3d"></div> <!-- Mini-carte 2D en superposition --> <div id="minimap" class="overlay top-right"> <!-- Par exemple une image ou un canvas pour la carte --> <img src="minicarte-port.png" alt="Mini-carte du port"/> </div> <!-- Menu principal vertical (icônes) --> <nav id="main-menu" class="overlay side-right"> <ul> <li><button id="btn-berth" title="Berth Management"><i class="icon-anchor"></i></button></li> <li><button id="btn-yard" title="Yard Management"><i class="icon-yard"></i></button></li> <li><button id="btn-gate" title="Gate Management"><i class="icon-gate"></i></button></li> <li><button id="btn-schedule" title="Equipment & Staff Scheduling"><i class="icon-crane"></i></button></li> <li><button id="btn-billing" title="Billing & Reporting"><i class="icon-report"></i></button></li> </ul> </nav> <!-- Panneau de chat IA --> <div id="chat-panel" class="overlay bottom"> <div id="chat-messages"></div> <div id="chat-input"> <input type="text" placeholder="Entrez votre question..."/> <button id="send-btn">Envoyer</button> </div> </div> <!-- Info utilisateur en haut à droite --> <div id="user-info" class="overlay top-right"> <img src="avatar.png" alt="Avatar" class="avatar"/> <span class="username">Jean Dupont – Yield Manager</span> </div> <!-- Menu contextuel (exemple, caché par défaut) --> <div id="context-menu" class="overlay info-panel" style="display:none;"></div> <!-- Inclusion du script Three.js et des scripts applicatifs --> <script src="three.min.js"></script> <script src="app.js"></script> </body> Dans cet extrait: La <div id="viewport-3d"> sert de conteneur au canevas WebGL Three.js (on peut soit insérer directement un <canvas> dedans, soit laisser Three.js l’ajouter). Il occupe toute la fenêtre en arrière-plan. Les éléments avec classe overlay sont en position absolue par-dessus la vue 3D. Par exemple, #minimap.overlay.top-right sera positionné en haut à droite (via CSS, ex: position:absolute; top:10px; right:10px;). Le menu vertical #main-menu.overlay.side-right est positionné à droite centré verticalement (par ex. top:50%; right:0; transform:translateY(-50%);). Il contient des <button> avec des icônes et utilise l’attribut title pour les info-bulles (ou alternativement un <span class="tooltip"> visible au survol via CSS). Le panneau de chat #chat-panel.overlay.bottom est fixé en bas (bottom:0; left:0; right:0; height:...). Il est éventuellement semi-transparent avec un fond sombre. Il contient une zone de messages et un champ de saisie. La section utilisateur #user-info.overlay.top-right est également absolue en haut à droite, mais avec un décalage pour ne pas chevaucher la mini-carte (on peut la mettre dans la même zone top-right mais plus en bas, ou légèrement à gauche de la mini-carte). Elle montre un <img> d’avatar et le nom dans un <span>. Le #context-menu.overlay.info-panel est un panneau générique pour afficher les infos contextuelles des gates/quais. Au clic sur un objet, on peu peupler ce div en JS avec le contenu approprié (statut, navire, ETA, etc.) et le positionner soit près de l’objet cliqué, ou dans un coin de l’écran. Il est caché par défaut (display:none) et affiché quand nécessaire. La feuille de style CSS va gérer la mise en page de ces éléments overlay. On utilisera un système de calques avec position:absolute ou fixed pour que les overlays restent en place lors des mouvements de caméra 3D. Une attention particulière est donnée à la responsivité et à la lisibilité : par exemple, la mini-carte et le chat auront un fond légèrement translucide pour bien se détacher de la 3D sans la masquer complètement. Les polices utilisées seront sobres et lisibles (typique des interfaces industrielles). On peut définir des couleurs thématiques pour le système (par ex. la couleur de Sans Souci – le logo Sansouci est bleu/vert – pour souligner certains éléments). Les icônes du menu peuvent être insérées via une police d’icônes ou des images SVG, stylées en CSS (taille, couleur au survol, etc.). Les info-bulles (tooltips) peuvent être réalisées en CSS pur (:hover sur le bouton pour faire apparaître un pseudo-élément contenant le texte) ou via un petit script JS. Le script JavaScript (fichier app.js par exemple) initialise la scène Three.js et gère les interactions : Création de la scène (THREE.Scene()), de la caméra (probablement une caméra perspective placée de façon à voir l’ensemble du port initialement) et du renderer WebGL attaché au conteneur viewport-3d. On ajoute des lumières (directionnelle pour le soleil, ambiante pour les ombres douces, etc.). Chargement ou création des objets 3D du port : on peut modeler les quais d’après les dimensions (par ex. PierGeometry = new THREE.BoxGeometry(longueur, hauteur, largeur)). Si disponible, on pourrait importer un modèle 3D existant (au format glTF/OBJ) du port, mais ici on peut aussi générer géométriquement ou via des primitives. On place chaque quai aux coordonnées appropriées. Idem pour les zones de stockage (de grands plans horizontaux texturés), les bâtiments (boîtes représentant les entrepôts, terminal passagers, etc.), et les objets plus fins comme les grilles des gates (simples rectangles fins). Mise en place des contrôles de caméra (par ex. OrbitControls de Three.js pour permettre le zoom/dézoom, rotation orbitale, etc.). On limite éventuellement les angles pour rester au-dessus du sol. Implémentation du cliquage d’objets : on utilise THREE.Raycaster. À chaque clic (mousedown) sur le canvas, on calcule l’intersection avec les objets de la scène. Si un objet d’intérêt est touché (on peut tagger les objets avec un nom ou un identifiant, ex: object.userData = { type: 'gate', id: 'Gate1' }), alors on exécute la fonction associée (par ex. onObjectClick(object)). Si c’est une gate ou un quai : on affiche le menu contextuel correspondant. Le script va remplir le <div id="context-menu"> avec le contenu HTML (par exemple : <h3>Quai 5</h3><p>Status: Occupé</p><p>Navire: MSC Caribe (ETA 14h30)</p><p>Progression déchargement: 65%</p>). Ensuite, on positionne ce div. On peut choisir de le placer, disons, en haut à gauche de l’écran ou dans un panneau latéral. Ou plus sophistiqué : projeter la position 3D de l’objet en 2D à l’écran (vector = object.position.project(camera)) et positionner le div à vector.x, vector.y. Cependant, comme on a déjà la mini-carte, il pourrait être acceptable que le menu contextuel apparaisse de façon fixe pour ne pas courir après la position. À voir selon ergonomie. On utilise des styles CSS pour le faire apparaître en douceur. En parallèle, le clic peut aussi déclencher la navigation de la caméra (comme décrit plus haut). Par exemple, le script anime la caméra vers object.position avec un certain offset (pour ne pas entrer dans l’objet). Cela peut se faire via gsap ou tween.js ou manuellement en interpolant dans la boucle de rendu. Durant ce déplacement, l’interface reste interactive. Gestion du chat : le script JS récupère l’input utilisateur dans #chat-input lorsqu’on appuie sur Envoyer. Il affiche immédiatement la question dans la zone #chat-messages, l’envoie au backend IA (dans notre cas M2H Majestic). La réponse reçue (simulée ou réelle) est ensuite ajoutée sous forme de nouveau message dans #chat-messages. On peut ajouter un léger délai typique d’un chatbot pour le réalisme. Le panneau de chat pourrait être scrollable si l’historique dépasse la taille. Mise à jour en temps réel : on présume que le TOS envoie des mises à jour (via WebSocket ou polling AJAX) sur l’état des quais, etc. Le script JS écoute ces événements (par exemple, réception d’un objet JSON { event: "updateProgress", berth: 5, progress: 80% }) et va mettre à jour l’UI en conséquence. Si le menu contextuel du quai 5 est ouvert, il mettra à jour le champ progression. Si l’utilisateur n’a pas ouvert le menu, peut-être que l’icône du quai 5 dans la 3D pourrait changer de couleur ou clignoter pour signaler un changement (ce serait un bonus de design, non explicitement demandé). De même, la mini-carte 2D pourrait montrer des indicateurs (par ex. un point vert/rouge sur chaque quai pour libre/occupé). Enfin, on s’assure que le rendement est bon : Three.js permet de rendre des milliers de polygones sans problème, et on n’a ici qu’un port relativement simple (quelques grandes formes). On active requestAnimationFrame pour animer la scène en continu (60 FPS), ou on peut rendre au coup par coup si on veut économiser des ressources quand rien ne change (mais comme il peut y avoir du mouvement – par ex. on pourrait animer de petits éléments ou le déplacement d’un navire approchant – on préfère un rendu continu). Ressources graphiques nécessaires Pour réaliser l’interface telle que conçue, quelques fichiers graphiques seront nécessaires : Une image de fond de carte 2D du port pour la mini-carte. Idéalement un plan schématique ou une vue satellite du port de Santo Domingo (par exemple extrait du Port Handbook ou d’une source cartographique). Cette image sera utilisée dans #minimap. Les icônes pour le menu principal : on peut utiliser des fichiers SVG appropriés (une ancre, un conteneur, une barrière, une grue, un document, etc.) ou recourir à une librairie d’icônes. L’important est d’avoir des pictogrammes clairs et compréhensibles, en style monochrome. Si disponibles, on intègre ces fichiers (par exemple anchor.svg, container.svg, etc.). Un fichier image pour l’avatar utilisateur (sauf si on génère un avatar par CSS). On peut prévoir un avatar par défaut (avatar.png) que l’utilisateur pourra éventuellement personnaliser. Éventuellement des textures pour la 3D : par exemple, une texture d’eau pour la surface de la rivière, une texture d’asphalte ou de béton pour le sol du terminal, une texture de conteneurs empilés pour habiller la zone de fret. Three.js pourrait utiliser ces images pour un rendu plus réaliste. Ce n’est pas strictement nécessaire (on peut aussi utiliser des couleurs unies dans un premier temps), mais souhaitable pour le réalisme visuel d’un outil professionnel en temps réel. Le logo ou un icône pour M2H Majestic si on souhaite le mettre dans le chat (par exemple un petit logo “Majestic” à côté des réponses de l’IA). En résumé, cette interface web propose une expérience immersive et fonctionnelle pour la gestion du port de Santo Domingo. La combinaison d’une visualisation 3D interactive du terminal (avec ses quais et équipements tels que décrits dans le Port Handbook) sansouci.com.do sansouci.com.do , de panneaux d’information contextuels et d’outils (mini-carte, chat IA, menus icônifiés), permet aux opérateurs du port (Yield Manager et autres) de surveiller et piloter les opérations portuaires de façon fluide, en temps réel, et de manière très intuitive. Chaque composant de l’UI est pensé pour minimiser la charge cognitive et maximiser la réactivité : l’information importante est accessible en un clic sur la zone concernée, et l’utilisateur dispose d’une vue d’ensemble toujours à jour pour prendre rapidement les décisions ou actions nécessaires. Les principes d’ergonomie et de performance sont respectés, ce qui fait de cette interface un outil adapté à un environnement critique comme un terminal portuaire en activité. Sources : Le Port Handbook – Puerto Santo Domingo a fourni les détails d’implantation du terminal (répartition et dimensions des quais, infrastructures de stockage) utilisés pour la modélisation sansouci.com.do sansouci.com.do sansouci.com.do sansouci.com.do . Les concepts de conception d’interface s’inspirent des pratiques courantes en systèmes d’exploitation portuaires (TOS) et des guidelines d’ergonomie pour les applications 3D et temps réel.
|