portraitart commited on
Commit
ec7f7d0
·
verified ·
1 Parent(s): 593aab0

Interface Utilisateur "Conçois un prototype Figma responsive incluant : 1. Page d'accueil avec mécanisme de soumission en 3 étapes guidées 2. Dashboard de contribution avec score d'impact en temps réel 3. Visualiseur de graphe de connaissances interactif (D3.js intégré) 4. Système de notifications push WebSocket 5. Centre de conformité RGPD intégré (préférences linguistiques, droit à l'oubli) Exporte le code frontend correspondant en React 18 avec Tailwind CSS et composants shadcn/ui." - Initial Deployment

Browse files
Files changed (2) hide show
  1. README.md +6 -4
  2. index.html +1228 -19
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Interface Utilisateur
3
- emoji: 📈
4
  colorFrom: gray
5
- colorTo: yellow
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: interface-utilisateur
3
+ emoji: 🐳
4
  colorFrom: gray
5
+ colorTo: green
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,1228 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { useEffect, useRef, useState } from 'react';
3
+
4
+ export function useWebSocket(url: string) {
5
+ const [lastMessage, setLastMessage] = useState<any>(null);
6
+ const ws = useRef<WebSocket | null>(null);
7
+
8
+ useEffect(() => {
9
+ ws.current = new WebSocket(url);
10
+ ws.current.onmessage = (event) => setLastMessage(JSON.parse(event.data));
11
+ return () => ws.current?.close();
12
+ }, [url]);
13
+
14
+ const send = (payload: any) => ws.current?.send(JSON.stringify(payload));
15
+
16
+ return { lastMessage, send };
17
+ }
18
+
19
+
20
+ import React from 'react';
21
+ import { Button } from '@/components/ui/button';
22
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
23
+ import { Switch } from '@/components/ui/switch';
24
+ import { Label } from '@/components/ui/label';
25
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
26
+
27
+ export default function RGPDPage() {
28
+ const deleteAccount = async () => {
29
+ if (confirm('Droits à l’oubli : êtes-vous sûr ? Cette action est irréversible.')) {
30
+ await fetch('/api/delete-account', { method: 'DELETE' });
31
+ window.location.href = '/';
32
+ }
33
+ };
34
+
35
+ return (
36
+ <div className="p-6 max-w-3xl mx-auto space-y-6">
37
+ <Card>
38
+ <CardHeader>
39
+ <CardTitle>Préférences linguistiques</CardTitle>
40
+ </CardHeader>
41
+ <CardContent>
42
+ <Label>Langue</Label>
43
+ <Select defaultValue="fr">
44
+ <SelectTrigger className="w-full">
45
+ <SelectValue />
46
+ </SelectTrigger>
47
+ <SelectContent>
48
+ <SelectItem value="fr">Français</SelectItem>
49
+ <SelectItem value="en">English</SelectItem>
50
+ </SelectContent>
51
+ </Select>
52
+ </CardContent>
53
+ </Card>
54
+
55
+ <Card>
56
+ <CardHeader>
57
+ <CardTitle>Notifications push</CardTitle>
58
+ </CardHeader>
59
+ <CardContent className="flex items-center justify-between">
60
+ <Label htmlFor="push-switch">Recevoir les notifications</Label>
61
+ <Switch id="push-switch" />
62
+ </CardContent>
63
+ </Card>
64
+
65
+ <Card>
66
+ <CardHeader>
67
+ <CardTitle>Droit à l’oubli</CardTitle>
68
+ </CardHeader>
69
+ <CardContent>
70
+ <p className="text-sm text-gray-600 mb-4">
71
+ Vous pouvez demander la suppression définitive de toutes vos données.
72
+ </p>
73
+ <Button variant="destructive" onClick={deleteAccount}>
74
+ Supprimer mon compte
75
+ </Button>
76
+ </CardContent>
77
+ </Card>
78
+ </div>
79
+ );
80
+ }
81
+
82
+
83
+ import React, { useRef, useEffect } from 'react';
84
+ import * as d3 from 'd3';
85
+ import { Card } from '@/components/ui/card';
86
+
87
+ const data = {
88
+ nodes: [
89
+ { id: 'user:1', label: 'Alice' },
90
+ { id: 'contrib:42', label: 'Contribution #42' },
91
+ { id: 'tag:ai', label: 'IA' }
92
+ ],
93
+ links: [
94
+ { source: 'user:1', target: 'contrib:42' },
95
+ { source: 'contrib:42', target: 'tag:ai' }
96
+ ]
97
+ };
98
+
99
+ export default function GraphViewerPage() {
100
+ const ref = useRef<SVGSVGElement>(null);
101
+
102
+ useEffect(() => {
103
+ if (!ref.current) return;
104
+ const svg = d3.select(ref.current);
105
+ svg.selectAll('*').remove();
106
+
107
+ const width = 600;
108
+ const height = 400;
109
+
110
+ const simulation = d3.forceSimulation(data.nodes as any)
111
+ .force('link', d3.forceLink(data.links).id((d: any) => d.id))
112
+ .force('charge', d3.forceManyBody().strength(-200))
113
+ .force('center', d3.forceCenter(width / 2, height / 2));
114
+
115
+ const link = svg.append('g')
116
+ .selectAll('line')
117
+ .data(data.links)
118
+ .enter()
119
+ .append('line')
120
+ .attr('stroke', '#999');
121
+
122
+ const node = svg.append('g')
123
+ .selectAll('circle')
124
+ .data(data.nodes)
125
+ .enter()
126
+ .append('circle')
127
+ .attr('r', 10)
128
+ .attr('fill', '#69b3a2')
129
+ .call(d3.drag<any, any>()
130
+ .on('start', (event, d: any) => {
131
+ if (!event.active) simulation.alphaTarget(0.3).restart();
132
+ d.fx = d.x;
133
+ d.fy = d.y;
134
+ })
135
+ .on('drag', (event, d: any) => {
136
+ d.fx = event.x;
137
+ d.fy = event.y;
138
+ })
139
+ .on('end', (event, d: any) => {
140
+ if (!event.active) simulation.alphaTarget(0);
141
+ d.fx = null;
142
+ d.fy = null;
143
+ }));
144
+
145
+ const label = svg.append('g')
146
+ .selectAll('text')
147
+ .data(data.nodes)
148
+ .enter()
149
+ .append('text')
150
+ .text((d: any) => d.label)
151
+ .attr('font-size', 12)
152
+ .attr('dx', 12)
153
+ .attr('dy', 4);
154
+
155
+ simulation.on('tick', () => {
156
+ link
157
+ .attr('x1', (d: any) => d.source.x)
158
+ .attr('y1', (d: any) => d.source.y)
159
+ .attr('x2', (d: any) => d.target.x)
160
+ .attr('y2', (d: any) => d.target.y);
161
+
162
+ node
163
+ .attr('cx', (d: any) => d.x)
164
+ .attr('cy', (d: any) => d.y);
165
+
166
+ label
167
+ .attr('x', (d: any) => d.x)
168
+ .attr('y', (d: any) => d.y);
169
+ });
170
+ }, []);
171
+
172
+ return (
173
+ <div className="p-6">
174
+ <Card>
175
+ <svg ref={ref} width="100%" height="400" className="border rounded" />
176
+ </Card>
177
+ </div>
178
+ );
179
+ }
180
+
181
+
182
+ import React, { useEffect, useState } from 'react';
183
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
184
+ import { Badge } from '@/components/ui/badge';
185
+ import { useWebSocket } from '@/components/useWebSocket';
186
+
187
+ export default function DashboardPage() {
188
+ const [score, setScore] = useState(0);
189
+ const { lastMessage } = useWebSocket('wss://api.example.com/notifs');
190
+
191
+ useEffect(() => {
192
+ fetch('/api/score').then(r => r.json()).then(d => setScore(d.score));
193
+ }, []);
194
+
195
+ useEffect(() => {
196
+ if (lastMessage?.type === 'SCORE_UPDATE') setScore(lastMessage.payload);
197
+ }, [lastMessage]);
198
+
199
+ return (
200
+ <div className="p-6 grid gap-6 md:grid-cols-2">
201
+ <Card>
202
+ <CardHeader>
203
+ <CardTitle>Score d'impact</CardTitle>
204
+ </CardHeader>
205
+ <CardContent>
206
+ <div className="text-4xl font-bold">{score}</div>
207
+ <Badge variant="outline" className="mt-2">
208
+ {score > 50 ? 'Excellent' : 'À améliorer'}
209
+ </Badge>
210
+ </CardContent>
211
+ </Card>
212
+
213
+ <Card>
214
+ <CardHeader>
215
+ <CardTitle>Notifications</CardTitle>
216
+ </CardHeader>
217
+ <CardContent>
218
+ {lastMessage ? (
219
+ <p>
220
+ {lastMessage.type}: {JSON.stringify(lastMessage.payload)}
221
+ </p>
222
+ ) : (
223
+ <p className="text-sm text-gray-500">Aucune nouvelle notification.</p>
224
+ )}
225
+ </CardContent>
226
+ </Card>
227
+ </div>
228
+ );
229
+ }
230
+
231
+
232
+ import React, { useState } from 'react';
233
+ import { Button } from '@/components/ui/button';
234
+ import { Progress } from '@/components/ui/progress';
235
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
236
+ import { Input } from '@/components/ui/input';
237
+ import { Textarea } from '@/components/ui/textarea';
238
+ import { Label } from '@/components/ui/label';
239
+ import { useWebSocket } from '@/components/useWebSocket';
240
+
241
+ const steps = ['Type de contribution', 'Contenu', 'Résumé & envoi'];
242
+
243
+ export default function HomePage() {
244
+ const [step, setStep] = useState(0);
245
+ const [data, setData] = useState({ type: 'text', content: '', metadata: '' });
246
+ const { send } = useWebSocket('wss://api.example.com/notifs');
247
+
248
+ const next = () => setStep(s => s + 1);
249
+ const prev = () => setStep(s => s - 1);
250
+ const submit = () => {
251
+ fetch('/api/submit-data', {
252
+ method: 'POST',
253
+ body: JSON.stringify(data),
254
+ headers: { 'Content-Type': 'application/json' }
255
+ }).then(() => send({ type: 'NEW_CONTRIBUTION' }));
256
+ };
257
+
258
+ return (
259
+ <div className="p-6 max-w-2xl mx-auto">
260
+ <Progress value={((step + 1) / steps.length) * 100} className="mb-6" />
261
+ <Card>
262
+ <CardHeader>
263
+ <CardTitle>{steps[step]}</CardTitle>
264
+ </CardHeader>
265
+ <CardContent>
266
+ {step === 0 && (
267
+ <>
268
+ <Label>Type</Label>
269
+ <select
270
+ className="w-full mt-2"
271
+ value={data.type}
272
+ onChange={e => setData({ ...data, type: e.target.value })}
273
+ >
274
+ <option value="text">Texte</option>
275
+ <option value="image">Image</option>
276
+ <option value="document">Document</option>
277
+ </select>
278
+ </>
279
+ )}
280
+ {step === 1 && (
281
+ <>
282
+ <Label>Contenu</Label>
283
+ <Textarea
284
+ className="mt-2"
285
+ rows={6}
286
+ value={data.content}
287
+ onChange={e => setData({ ...data, content: e.target.value })}
288
+ />
289
+ </>
290
+ )}
291
+ {step === 2 && (
292
+ <>
293
+ <h3 className="font-bold mb-2">Résumé</h3>
294
+ <p className="text-sm text-gray-600">Type: {data.type}</p>
295
+ <p className="text-sm text-gray-600">Contenu: {data.content.slice(0, 60)}...</p>
296
+ </>
297
+ )}
298
+ <div className="flex justify-between mt-6">
299
+ <Button variant="outline" disabled={step === 0} onClick={prev}>
300
+ Précédent
301
+ </Button>
302
+ {step < steps.length - 1 ? (
303
+ <Button onClick={next}>Suivant</Button>
304
+ ) : (
305
+ <Button onClick={submit}>Soumettre</Button>
306
+ )}
307
+ </div>
308
+ </CardContent>
309
+ </Card>
310
+ </div>
311
+ );
312
+ }
313
+
314
+ <!DOCTYPE html>
315
+ <html lang="fr">
316
+ <head>
317
+ <meta charset="UTF-8">
318
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
319
+ <title>Architecture Système - Plateforme IA</title>
320
+ <script src="https://cdn.tailwindcss.com"></script>
321
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
322
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-dark.min.css">
323
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
324
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-yaml.min.js"></script>
325
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
326
+ <style>
327
+ .gradient-bg {
328
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
329
+ }
330
+ .glass-effect {
331
+ background: rgba(255, 255, 255, 0.1);
332
+ backdrop-filter: blur(10px);
333
+ border: 1px solid rgba(255, 255, 255, 0.2);
334
+ }
335
+ .code-block {
336
+ background: #1a1a1a;
337
+ border-radius: 12px;
338
+ overflow-x: auto;
339
+ }
340
+ .mermaid-container {
341
+ display: flex;
342
+ justify-content: center;
343
+ min-height: 500px;
344
+ max-height: 800px;
345
+ background: #ffffff;
346
+ border: 2px solid #e5e7eb;
347
+ border-radius: 12px;
348
+ padding: 30px;
349
+ margin: 20px 0;
350
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
351
+ position: relative;
352
+ overflow: hidden;
353
+ }
354
+
355
+ .mermaid-container .mermaid {
356
+ width: 100%;
357
+ max-width: 100%;
358
+ height: 100%;
359
+ cursor: grab;
360
+ transition: transform 0.3s ease;
361
+ transform-origin: center center;
362
+ display: flex;
363
+ justify-content: center;
364
+ align-items: center;
365
+ touch-action: none;
366
+ -webkit-user-select: none;
367
+ -moz-user-select: none;
368
+ -ms-user-select: none;
369
+ user-select: none;
370
+ }
371
+
372
+ .mermaid-container .mermaid svg {
373
+ max-width: 100%;
374
+ height: 100%;
375
+ display: block;
376
+ margin: 0 auto;
377
+ }
378
+
379
+ .mermaid-container .mermaid:active {
380
+ cursor: grabbing;
381
+ }
382
+
383
+ .mermaid-container.zoomed .mermaid {
384
+ height: 100%;
385
+ width: 100%;
386
+ cursor: grab;
387
+ }
388
+
389
+ .mermaid-controls {
390
+ position: absolute;
391
+ top: 15px;
392
+ right: 15px;
393
+ display: flex;
394
+ gap: 10px;
395
+ z-index: 20;
396
+ background: rgba(0, 0, 0, 0.8);
397
+ padding: 8px;
398
+ border-radius: 8px;
399
+ backdrop-filter: blur(10px);
400
+ }
401
+
402
+ .mermaid-control-btn {
403
+ background: rgba(255, 255, 255, 0.9);
404
+ border: none;
405
+ border-radius: 6px;
406
+ padding: 10px;
407
+ cursor: pointer;
408
+ transition: all 0.2s ease;
409
+ color: #374151;
410
+ font-size: 14px;
411
+ min-width: 36px;
412
+ height: 36px;
413
+ text-align: center;
414
+ display: flex;
415
+ align-items: center;
416
+ justify-content: center;
417
+ }
418
+
419
+ .mermaid-control-btn:hover {
420
+ background: #ffffff;
421
+ transform: translateY(-1px);
422
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
423
+ }
424
+
425
+ .mermaid-control-btn:active {
426
+ transform: translateY(0);
427
+ }
428
+
429
+ /* Responsive adjustments for mermaid controls */
430
+ @media (max-width: 1024px) {
431
+ .mermaid-control-btn:not(.reset-zoom) {
432
+ display: none;
433
+ }
434
+ .mermaid-controls {
435
+ top: auto;
436
+ bottom: 15px;
437
+ right: 15px;
438
+ }
439
+ }
440
+
441
+ /* Prevent horizontal overflow on small screens */
442
+ @media (max-width: 768px) {
443
+ .mermaid-container {
444
+ overflow-x: auto;
445
+ }
446
+ }
447
+ .tech-card {
448
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
449
+ }
450
+ .tech-card:hover {
451
+ transform: translateY(-5px);
452
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
453
+ }
454
+ .pulse {
455
+ animation: pulse 2s infinite;
456
+ }
457
+ @keyframes pulse {
458
+ 0% { opacity: 1; }
459
+ 50% { opacity: 0.5; }
460
+ 100% { opacity: 1; }
461
+ }
462
+ .section-divider {
463
+ height: 2px;
464
+ background: linear-gradient(90deg, transparent, #667eea, transparent);
465
+ margin: 40px 0;
466
+ }
467
+ </style>
468
+ </head>
469
+ <body class="bg-gradient-to-br from-slate-50 to-slate-100 min-h-screen">
470
+ <nav class="gradient-bg text-white p-4 shadow-lg">
471
+ <div class="container mx-auto flex justify-between items-center">
472
+ <h1 class="text-2xl font-bold">Architecture Système IA</h1>
473
+ <div class="flex space-x-4">
474
+ <button onclick="scrollToSection('overview')" class="hover:opacity-80 transition-opacity">Vue d'ensemble</button>
475
+ <button onclick="scrollToSection('microservices')" class="hover:opacity-80 transition-opacity">Microservices</button>
476
+ <button onclick="scrollToSection('kubernetes')" class="hover:opacity-80 transition-opacity">Kubernetes</button>
477
+ <button onclick="scrollToSection('api')" class="hover:opacity-80 transition-opacity">API REST</button>
478
+ </div>
479
+ </div>
480
+ </nav>
481
+
482
+ <main class="container mx-auto p-6 max-w-7xl">
483
+ <!-- Vue d'ensemble -->
484
+ <section id="overview" class="mb-12">
485
+ <div class="text-center mb-8">
486
+ <h2 class="text-4xl font-bold text-gray-800 mb-4">Architecture Technique Complète</h2>
487
+ <p class="text-xl text-gray-600">Plateforme IA avec microservices Node.js/Next.js</p>
488
+ </div>
489
+
490
+ <!-- Diagramme d'architecture -->
491
+ <div class="bg-white rounded-2xl shadow-xl p-8 mb-8">
492
+ <h3 class="text-2xl font-bold text-gray-800 mb-6 text-center">Vue d'ensemble de l'architecture</h3>
493
+ <div class="mermaid-container" id="mermaid-container-1">
494
+ <div class="mermaid-controls">
495
+ <button class="mermaid-control-btn zoom-in" title="Zoom avant">🔍+</button>
496
+ <button class="mermaid-control-btn zoom-out" title="Zoom arrière">🔍-</button>
497
+ <button class="mermaid-control-btn reset-zoom" title="Réinitialiser">⌂</button>
498
+ <button class="mermaid-control-btn fullscreen" title="Plein écran">⛶</button>
499
+ </div>
500
+ <div class="mermaid" id="architecture-diagram">
501
+ graph TB
502
+ subgraph "Client Layer"
503
+ UI[Next.js Frontend]
504
+ Mobile[Mobile Apps]
505
+ end
506
+
507
+ subgraph "API Gateway"
508
+ GW[NGINX Gateway]
509
+ Auth[Auth Service]
510
+ end
511
+
512
+ subgraph "Microservices"
513
+ MS1[User Service]
514
+ MS2[Contribution Service]
515
+ MS3[AI Processing Service]
516
+ MS4[Notification Service]
517
+ MS5[Analytics Service]
518
+ end
519
+
520
+ subgraph "Data Layer"
521
+ PG[(PostgreSQL + pgvector)]
522
+ Redis[(Redis Cache)]
523
+ RMQ[RabbitMQ]
524
+ S3[S3 Storage]
525
+ end
526
+
527
+ subgraph "AI Services"
528
+ HF[Hugging Face API]
529
+ Embeddings[Embedding Service]
530
+ Model[ML Models]
531
+ end
532
+
533
+ subgraph "Kubernetes"
534
+ K8s[Kubernetes Cluster]
535
+ HPA[HPA Autoscaler]
536
+ Ingress[Ingress Controller]
537
+ end
538
+
539
+ UI --> GW
540
+ Mobile --> GW
541
+ GW --> Auth
542
+ Auth --> MS1
543
+ Auth --> MS2
544
+ Auth --> MS3
545
+ Auth --> MS4
546
+ Auth --> MS5
547
+
548
+ MS1 --> PG
549
+ MS2 --> PG
550
+ MS2 --> RMQ
551
+ MS3 --> PG
552
+ MS3 --> HF
553
+ MS3 --> Redis
554
+ MS4 --> Redis
555
+ MS5 --> PG
556
+ MS5 --> S3
557
+ Embeddings --> Model
558
+ </div>
559
+ </div>
560
+ </div>
561
+
562
+ <!-- Technologies -->
563
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
564
+ <div class="tech-card bg-white rounded-xl shadow-lg p-6">
565
+ <div class="text-4xl mb-4">🚀</div>
566
+ <h4 class="text-xl font-bold text-gray-800 mb-2">Backend Microservices</h4>
567
+ <p class="text-gray-600">NestJS avec architecture modulaire, TypeScript, et patterns CQRS</p>
568
+ </div>
569
+ <div class="tech-card bg-white rounded-xl shadow-lg p-6">
570
+ <div class="text-4xl mb-4">🧠</div>
571
+ <h4 class="text-xl font-bold text-gray-800 mb-2">IA & Embeddings</h4>
572
+ <p class="text-gray-600">Hugging Face integration avec sentence-transformers/all-mpnet-base-v2</p>
573
+ </div>
574
+ <div class="tech-card bg-white rounded-xl shadow-lg p-6">
575
+ <div class="text-4xl mb-4">🔒</div>
576
+ <h4 class="text-xl font-bold text-gray-800 mb-2">Sécurité</h4>
577
+ <p class="text-gray-600">JWT OAuth2.0 + AES-256 encryption pour les données sensibles</p>
578
+ </div>
579
+ </div>
580
+ </section>
581
+
582
+ <div class="section-divider"></div>
583
+
584
+ <!-- Microservices -->
585
+ <section id="microservices" class="mb-12">
586
+ <h2 class="text-3xl font-bold text-gray-800 mb-8">Architecture Microservices</h2>
587
+
588
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
589
+ <div class="bg-white rounded-xl shadow-xl p-6">
590
+ <h3 class="text-xl font-bold text-gray-800 mb-4">Structure des Services</h3>
591
+ <pre class="code-block"><code class="language-yaml"># docker-compose.yml
592
+ version: '3.8'
593
+ services:
594
+ user-service:
595
+ build: ./services/user
596
+ ports:
597
+ - "3001:3000"
598
+ environment:
599
+ - POSTGRES_URL=postgresql://...
600
+ - REDIS_URL=redis://redis:6379
601
+
602
+ contribution-service:
603
+ build: ./services/contribution
604
+ ports:
605
+ - "3002:3000"
606
+ depends_on:
607
+ - rabbitmq
608
+ - postgres
609
+
610
+ ai-service:
611
+ build: ./services/ai
612
+ ports:
613
+ - "3003:3000"
614
+ environment:
615
+ - HUGGING_FACE_API_KEY=${HF_API_KEY}
616
+ volumes:
617
+ - ./models:/app/models</code></pre>
618
+ </div>
619
+
620
+ <div class="bg-white rounded-xl shadow-xl p-6">
621
+ <h3 class="text-xl font-bold text-gray-800 mb-4">Configuration NestJS</h3>
622
+ <pre class="code-block"><code class="language-typescript">// app.module.ts
623
+ import { Module } from '@nestjs/common';
624
+ import { TypeOrmModule } from '@nestjs/typeorm';
625
+ import { RedisModule } from './redis/redis.module';
626
+ import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
627
+
628
+ @Module({
629
+ imports: [
630
+ TypeOrmModule.forRoot({
631
+ type: 'postgres',
632
+ url: process.env.DATABASE_URL,
633
+ entities: [__dirname + '/**/*.entity{.ts,.js}'],
634
+ synchronize: true,
635
+ }),
636
+ RedisModule.forRoot({
637
+ host: process.env.REDIS_HOST,
638
+ port: process.env.REDIS_PORT,
639
+ }),
640
+ RabbitMQModule.forRoot(RabbitMQModule, {
641
+ exchanges: [
642
+ {
643
+ name: 'contributions',
644
+ type: 'topic',
645
+ },
646
+ ],
647
+ }),
648
+ ],
649
+ })
650
+ export class AppModule {}</code></pre>
651
+ </div>
652
+ </div>
653
+ </section>
654
+
655
+ <div class="section-divider"></div>
656
+
657
+ <!-- Kubernetes -->
658
+ <section id="kubernetes" class="mb-12">
659
+ <h2 class="text-3xl font-bold text-gray-800 mb-8">Déploiement Kubernetes</h2>
660
+
661
+ <div class="bg-white rounded-2xl shadow-xl p-8">
662
+ <h3 class="text-2xl font-bold text-gray-800 mb-6">Configuration Kubernetes avec Auto-scaling</h3>
663
+
664
+ <!-- Diagramme K8s -->
665
+ <div class="mermaid-container" id="mermaid-container-2">
666
+ <div class="mermaid-controls">
667
+ <button class="mermaid-control-btn zoom-in" title="Zoom avant">🔍+</button>
668
+ <button class="mermaid-control-btn zoom-out" title="Zoom arrière">🔍-</button>
669
+ <button class="mermaid-control-btn reset-zoom" title="Réinitialiser">⌂</button>
670
+ <button class="mermaid-control-btn fullscreen" title="Plein écran">⛶</button>
671
+ </div>
672
+ <div class="mermaid" id="kubernetes-diagram">
673
+ graph TD
674
+ subgraph "Kubernetes Cluster"
675
+ subgraph "Namespace: ai-platform"
676
+ subgraph "Deployment: user-service"
677
+ US1[Pod: user-service-1]
678
+ US2[Pod: user-service-2]
679
+ US3[Pod: user-service-3]
680
+ end
681
+
682
+ subgraph "Deployment: ai-service"
683
+ AIS1[Pod: ai-service-1]
684
+ AIS2[Pod: ai-service-2]
685
+ end
686
+
687
+ subgraph "HPA"
688
+ HPA1[HPA: user-service]
689
+ HPA2[HPA: ai-service]
690
+ end
691
+
692
+ subgraph "Services"
693
+ SVC1[Service: user-service]
694
+ SVC2[Service: ai-service]
695
+ ING[Ingress: api-gateway]
696
+ end
697
+
698
+ subgraph "PVC"
699
+ PVC1[PVC: postgres-data]
700
+ PVC2[PVC: redis-data]
701
+ end
702
+ end
703
+
704
+ subgraph "Monitoring"
705
+ PROM[Prometheus]
706
+ GRAF[Grafana]
707
+ end
708
+ end
709
+
710
+ HPA1 -.-> US1
711
+ HPA1 -.-> US2
712
+ HPA1 -.-> US3
713
+ HPA2 -.-> AIS1
714
+ HPA2 -.-> AIS2
715
+ </div>
716
+ </div>
717
+
718
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mt-8">
719
+ <div>
720
+ <h4 class="text-lg font-bold text-gray-800 mb-4">HPA Configuration</h4>
721
+ <pre class="code-block"><code class="language-yaml">apiVersion: autoscaling/v2
722
+ kind: HorizontalPodAutoscaler
723
+ metadata:
724
+ name: ai-service-hpa
725
+ spec:
726
+ scaleTargetRef:
727
+ apiVersion: apps/v1
728
+ kind: Deployment
729
+ name: ai-service
730
+ minReplicas: 2
731
+ maxReplicas: 10
732
+ metrics:
733
+ - type: Resource
734
+ resource:
735
+ name: cpu
736
+ target:
737
+ type: Utilization
738
+ averageUtilization: 70
739
+ - type: Resource
740
+ resource:
741
+ name: memory
742
+ target:
743
+ type: Utilization
744
+ averageUtilization: 80</code></pre>
745
+ </div>
746
+
747
+ <div>
748
+ <h4 class="text-lg font-bold text-gray-800 mb-4">Deployment avec pgvector</h4>
749
+ <pre class="code-block"><code class="language-yaml">apiVersion: apps/v1
750
+ kind: Deployment
751
+ metadata:
752
+ name: postgres-pgvector
753
+ spec:
754
+ replicas: 1
755
+ selector:
756
+ matchLabels:
757
+ app: postgres
758
+ template:
759
+ spec:
760
+ containers:
761
+ - name: postgres
762
+ image: ankane/pgvector:v0.5.0
763
+ env:
764
+ - name: POSTGRES_DB
765
+ value: "ai_platform"
766
+ - name: POSTGRES_USER
767
+ valueFrom:
768
+ secretKeyRef:
769
+ name: postgres-secret
770
+ key: username
771
+ volumeMounts:
772
+ - name: postgres-storage
773
+ mountPath: /var/lib/postgresql/data
774
+ volumes:
775
+ - name: postgres-storage
776
+ persistentVolumeClaim:
777
+ claimName: postgres-pvc</code></pre>
778
+ </div>
779
+ </div>
780
+ </div>
781
+ </section>
782
+
783
+ <div class="section-divider"></div>
784
+
785
+ <!-- API REST -->
786
+ <section id="api" class="mb-12">
787
+ <h2 class="text-3xl font-bold text-gray-800 mb-8">Spécifications API RESTful</h2>
788
+
789
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
790
+ <div class="bg-white rounded-xl shadow-xl p-6">
791
+ <h3 class="text-xl font-bold text-gray-800 mb-4">POST /submit-data</h3>
792
+ <pre class="code-block"><code class="language-json">{
793
+ "openapi": "3.0.0",
794
+ "paths": {
795
+ "/submit-data": {
796
+ "post": {
797
+ "summary": "Soumettre des données pour traitement IA",
798
+ "security": [{"bearerAuth": []}],
799
+ "requestBody": {
800
+ "required": true,
801
+ "content": {
802
+ "application/json": {
803
+ "schema": {
804
+ "type": "object",
805
+ "required": ["type", "content"],
806
+ "properties": {
807
+ "type": {
808
+ "type": "string",
809
+ "enum": ["text", "image", "document"]
810
+ },
811
+ "content": {
812
+ "type": "string"
813
+ },
814
+ "metadata": {
815
+ "type": "object",
816
+ "additionalProperties": true
817
+ }
818
+ }
819
+ }
820
+ }
821
+ }
822
+ },
823
+ "responses": {
824
+ "202": {
825
+ "description": "Données acceptées pour traitement",
826
+ "content": {
827
+ "application/json": {
828
+ "schema": {
829
+ "type": "object",
830
+ "properties": {
831
+ "jobId": {
832
+ "type": "string",
833
+ "format": "uuid"
834
+ },
835
+ "status": {
836
+ "type": "string",
837
+ "enum": ["queued", "processing"]
838
+ }
839
+ }
840
+ }
841
+ }
842
+ }
843
+ }
844
+ }
845
+ }
846
+ }
847
+ }
848
+ }</code></pre>
849
+ </div>
850
+
851
+ <div class="bg-white rounded-xl shadow-xl p-6">
852
+ <h3 class="text-xl font-bold text-gray-800 mb-4">GET /fetch-results</h3>
853
+ <pre class="code-block"><code class="language-json">{
854
+ "openapi": "3.0.0",
855
+ "paths": {
856
+ "/fetch-results": {
857
+ "get": {
858
+ "summary": "Récupérer les résultats du traitement IA",
859
+ "security": [{"bearerAuth": []}],
860
+ "parameters": [
861
+ {
862
+ "name": "jobId",
863
+ "in": "query",
864
+ "required": true,
865
+ "schema": {
866
+ "type": "string",
867
+ "format": "uuid"
868
+ }
869
+ }
870
+ ],
871
+ "responses": {
872
+ "200": {
873
+ "description": "Résultats du traitement",
874
+ "content": {
875
+ "application/json": {
876
+ "schema": {
877
+ "type": "object",
878
+ "properties": {
879
+ "jobId": {
880
+ "type": "string",
881
+ "format": "uuid"
882
+ },
883
+ "status": {
884
+ "type": "string",
885
+ "enum": ["pending", "completed", "failed"]
886
+ },
887
+ "result": {
888
+ "type": "object",
889
+ "properties": {
890
+ "embeddings": {
891
+ "type": "array",
892
+ "items": {
893
+ "type": "number"
894
+ }
895
+ },
896
+ "classification": {
897
+ "type": "string"
898
+ },
899
+ "confidence": {
900
+ "type": "number",
901
+ "minimum": 0,
902
+ "maximum": 1
903
+ }
904
+ }
905
+ }
906
+ }
907
+ }
908
+ }
909
+ }
910
+ }
911
+ }
912
+ }
913
+ }
914
+ }
915
+ }</code></pre>
916
+ </div>
917
+ </div>
918
+
919
+ <!-- Exemple de requête -->
920
+ <div class="mt-8 bg-white rounded-xl shadow-xl p-6">
921
+ <h3 class="text-xl font-bold text-gray-800 mb-4">Exemple d'utilisation</h3>
922
+ <pre class="code-block"><code class="language-javascript">// Client JavaScript
923
+ const submitData = async (content) => {
924
+ const response = await fetch('https://api.ai-platform.com/submit-data', {
925
+ method: 'POST',
926
+ headers: {
927
+ 'Authorization': `Bearer ${token}`,
928
+ 'Content-Type': 'application/json'
929
+ },
930
+ body: JSON.stringify({
931
+ type: 'text',
932
+ content: content,
933
+ metadata: { source: 'web' }
934
+ })
935
+ });
936
+
937
+ const { jobId } = await response.json();
938
+
939
+ // Poll for results
940
+ const checkResults = setInterval(async () => {
941
+ const result = await fetch(`/fetch-results?jobId=${jobId}`, {
942
+ headers: { 'Authorization': `Bearer ${token}` }
943
+ });
944
+
945
+ const data = await result.json();
946
+ if (data.status === 'completed') {
947
+ clearInterval(checkResults);
948
+ console.log('Résultats:', data.result);
949
+ }
950
+ }, 1000);
951
+ };</code></pre>
952
+ </div>
953
+ </section>
954
+ </main>
955
+
956
+ <footer class="bg-gray-800 text-white py-8 mt-12">
957
+ <div class="container mx-auto px-6 text-center">
958
+ <p>&copy; 2024 Architecture Système IA - Tous droits réservés</p>
959
+ </div>
960
+ </footer>
961
+
962
+ <script>
963
+ // Initialize Mermaid
964
+ mermaid.initialize({
965
+ startOnLoad: true,
966
+ theme: 'default',
967
+ flowchart: {
968
+ useMaxWidth: false,
969
+ htmlLabels: true
970
+ }
971
+ });
972
+
973
+ // Initialize Mermaid Controls for zoom and pan
974
+ function initializeMermaidControls() {
975
+ const containers = document.querySelectorAll('.mermaid-container');
976
+
977
+ containers.forEach(container => {
978
+ const mermaidElement = container.querySelector('.mermaid');
979
+ let scale = 1;
980
+ let isDragging = false;
981
+ let startX, startY, translateX = 0, translateY = 0;
982
+
983
+ // 触摸相关状态
984
+ let isTouch = false;
985
+ let touchStartTime = 0;
986
+ let initialDistance = 0;
987
+ let initialScale = 1;
988
+ let isPinching = false;
989
+
990
+ // Zoom controls
991
+ const zoomInBtn = container.querySelector('.zoom-in');
992
+ const zoomOutBtn = container.querySelector('.zoom-out');
993
+ const resetBtn = container.querySelector('.reset-zoom');
994
+ const fullscreenBtn = container.querySelector('.fullscreen');
995
+
996
+ function updateTransform() {
997
+ mermaidElement.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
998
+
999
+ if (scale > 1) {
1000
+ container.classList.add('zoomed');
1001
+ } else {
1002
+ container.classList.remove('zoomed');
1003
+ }
1004
+
1005
+ mermaidElement.style.cursor = isDragging ? 'grabbing' : 'grab';
1006
+ }
1007
+
1008
+ if (zoomInBtn) {
1009
+ zoomInBtn.addEventListener('click', () => {
1010
+ scale = Math.min(scale * 1.25, 4);
1011
+ updateTransform();
1012
+ });
1013
+ }
1014
+
1015
+ if (zoomOutBtn) {
1016
+ zoomOutBtn.addEventListener('click', () => {
1017
+ scale = Math.max(scale / 1.25, 0.3);
1018
+ if (scale <= 1) {
1019
+ translateX = 0;
1020
+ translateY = 0;
1021
+ }
1022
+ updateTransform();
1023
+ });
1024
+ }
1025
+
1026
+ if (resetBtn) {
1027
+ resetBtn.addEventListener('click', () => {
1028
+ scale = 1;
1029
+ translateX = 0;
1030
+ translateY = 0;
1031
+ updateTransform();
1032
+ });
1033
+ }
1034
+
1035
+ if (fullscreenBtn) {
1036
+ fullscreenBtn.addEventListener('click', () => {
1037
+ if (container.requestFullscreen) {
1038
+ container.requestFullscreen();
1039
+ } else if (container.webkitRequestFullscreen) {
1040
+ container.webkitRequestFullscreen();
1041
+ } else if (container.msRequestFullscreen) {
1042
+ container.msRequestFullscreen();
1043
+ }
1044
+ });
1045
+ }
1046
+
1047
+ // Mouse Events
1048
+ mermaidElement.addEventListener('mousedown', (e) => {
1049
+ if (isTouch) return; // 如果是触摸设备,忽略鼠标事件
1050
+
1051
+ isDragging = true;
1052
+ startX = e.clientX - translateX;
1053
+ startY = e.clientY - translateY;
1054
+ mermaidElement.style.cursor = 'grabbing';
1055
+ updateTransform();
1056
+ e.preventDefault();
1057
+ });
1058
+
1059
+ document.addEventListener('mousemove', (e) => {
1060
+ if (isDragging && !isTouch) {
1061
+ translateX = e.clientX - startX;
1062
+ translateY = e.clientY - startY;
1063
+ updateTransform();
1064
+ }
1065
+ });
1066
+
1067
+ document.addEventListener('mouseup', () => {
1068
+ if (isDragging && !isTouch) {
1069
+ isDragging = false;
1070
+ mermaidElement.style.cursor = 'grab';
1071
+ updateTransform();
1072
+ }
1073
+ });
1074
+
1075
+ document.addEventListener('mouseleave', () => {
1076
+ if (isDragging && !isTouch) {
1077
+ isDragging = false;
1078
+ mermaidElement.style.cursor = 'grab';
1079
+ updateTransform();
1080
+ }
1081
+ });
1082
+
1083
+ // 获取两点之间的距离
1084
+ function getTouchDistance(touch1, touch2) {
1085
+ return Math.hypot(
1086
+ touch2.clientX - touch1.clientX,
1087
+ touch2.clientY - touch1.clientY
1088
+ );
1089
+ }
1090
+
1091
+ // Touch Events - 触摸事件处理
1092
+ mermaidElement.addEventListener('touchstart', (e) => {
1093
+ isTouch = true;
1094
+ touchStartTime = Date.now();
1095
+
1096
+ if (e.touches.length === 1) {
1097
+ // 单指拖动
1098
+ isPinching = false;
1099
+ isDragging = true;
1100
+
1101
+ const touch = e.touches[0];
1102
+ startX = touch.clientX - translateX;
1103
+ startY = touch.clientY - translateY;
1104
+
1105
+ } else if (e.touches.length === 2) {
1106
+ // 双指缩放
1107
+ isPinching = true;
1108
+ isDragging = false;
1109
+
1110
+ const touch1 = e.touches[0];
1111
+ const touch2 = e.touches[1];
1112
+ initialDistance = getTouchDistance(touch1, touch2);
1113
+ initialScale = scale;
1114
+ }
1115
+
1116
+ e.preventDefault();
1117
+ }, { passive: false });
1118
+
1119
+ mermaidElement.addEventListener('touchmove', (e) => {
1120
+ if (e.touches.length === 1 && isDragging && !isPinching) {
1121
+ // 单指拖动
1122
+ const touch = e.touches[0];
1123
+ translateX = touch.clientX - startX;
1124
+ translateY = touch.clientY - startY;
1125
+ updateTransform();
1126
+
1127
+ } else if (e.touches.length === 2 && isPinching) {
1128
+ // 双指缩放
1129
+ const touch1 = e.touches[0];
1130
+ const touch2 = e.touches[1];
1131
+ const currentDistance = getTouchDistance(touch1, touch2);
1132
+
1133
+ const newScale = Math.min(Math.max(
1134
+ initialScale * (currentDistance / initialDistance),
1135
+ 0.3
1136
+ ), 4);
1137
+ scale = newScale;
1138
+
1139
+ updateTransform();
1140
+ }
1141
+
1142
+ e.preventDefault();
1143
+ }, { passive: false });
1144
+
1145
+ mermaidElement.addEventListener('touchend', (e) => {
1146
+ // 重置状态
1147
+ isDragging = false;
1148
+ isPinching = false;
1149
+
1150
+ // 延迟重置isTouch,避免鼠标事件立即触发
1151
+ setTimeout(() => {
1152
+ isTouch = false;
1153
+ }, 100);
1154
+
1155
+ updateTransform();
1156
+ e.preventDefault();
1157
+ });
1158
+
1159
+ mermaidElement.addEventListener('touchcancel', (e) => {
1160
+ isDragging = false;
1161
+ isPinching = false;
1162
+ isTouch = false;
1163
+ updateTransform();
1164
+ e.preventDefault();
1165
+ });
1166
+
1167
+ // Enhanced wheel zoom with better center point handling
1168
+ container.addEventListener('wheel', (e) => {
1169
+ e.preventDefault();
1170
+ const rect = container.getBoundingClientRect();
1171
+ const centerX = rect.width / 2;
1172
+ const centerY = rect.height / 2;
1173
+
1174
+ const delta = e.deltaY > 0 ? 0.9 : 1.1;
1175
+ const newScale = Math.min(Math.max(scale * delta, 0.3), 4);
1176
+
1177
+ // Adjust translation to zoom towards center
1178
+ if (newScale !== scale) {
1179
+ const scaleDiff = newScale / scale;
1180
+ translateX = translateX * scaleDiff;
1181
+ translateY = translateY * scaleDiff;
1182
+ scale = newScale;
1183
+
1184
+ if (scale <= 1) {
1185
+ translateX = 0;
1186
+ translateY = 0;
1187
+ }
1188
+
1189
+ updateTransform();
1190
+ }
1191
+ });
1192
+
1193
+ // Initialize display
1194
+ updateTransform();
1195
+ });
1196
+ }
1197
+
1198
+ // Scroll to section
1199
+ function scrollToSection(sectionId) {
1200
+ document.getElementById(sectionId).scrollIntoView({
1201
+ behavior: 'smooth'
1202
+ });
1203
+ }
1204
+
1205
+ // Add pulse effect to cards
1206
+ document.querySelectorAll('.tech-card').forEach(card => {
1207
+ card.addEventListener('mouseenter', () => {
1208
+ card.classList.add('pulse');
1209
+ });
1210
+ card.addEventListener('mouseleave', () => {
1211
+ card.classList.remove('pulse');
1212
+ });
1213
+ });
1214
+
1215
+ // Initialize Prism
1216
+ document.addEventListener('DOMContentLoaded', () => {
1217
+ Prism.highlightAll();
1218
+ });
1219
+
1220
+ // Ensure Mermaid is rendered after DOM is loaded
1221
+ document.addEventListener('DOMContentLoaded', () => {
1222
+ setTimeout(() => {
1223
+ initializeMermaidControls();
1224
+ }, 1000);
1225
+ });
1226
+ </script>
1227
+ <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=portraitart/interface-utilisateur" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
1228
+ </html>