Last active 1 week ago

Bim 3d compare.html Raw
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>BIM Compare – Consdoc Viewer</title>
7<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
8<style>
9:root {
10 --bg: #0d1117; --surface: #161b22; --surface2: #1c2128;
11 --border: #30363d; --text: #e6edf3; --muted: #7d8590;
12 --accent: #58a6ff; --add: #3fb950; --mod: #f0883e; --del: #f85149;
13 --ai: #bc8cff;
14}
15* { box-sizing: border-box; margin:0; padding:0; }
16body { background:var(--bg); color:var(--text); font-family:'DM Sans',sans-serif; height:100vh; display:flex; flex-direction:column; overflow:hidden; }
17
18nav {
19 height:48px; background:var(--surface); border-bottom:1px solid var(--border);
20 display:flex; align-items:center; justify-content:space-between; padding:0 16px; flex-shrink:0;
21}
22.brand { font-family:'Syne',sans-serif; font-weight:800; font-size:16px; }
23.brand span { color:var(--accent); }
24.file-info { font-size:12px; color:var(--muted); display:flex; align-items:center; gap:8px; }
25.file-info b { color:var(--text); }
26.nav-right { display:flex; align-items:center; gap:8px; }
27.btn { padding:5px 12px; border-radius:6px; border:1px solid var(--border); background:var(--surface2); color:var(--text); font-size:12px; cursor:pointer; font-family:'DM Sans',sans-serif; transition:.15s; }
28.btn:hover { background:var(--border); }
29.btn-ai { background:linear-gradient(135deg,#7c3aed,#bc8cff); color:white; border:none; font-weight:600; display:flex; align-items:center; gap:5px; }
30.btn-ai:hover { opacity:.88; background:linear-gradient(135deg,#7c3aed,#bc8cff); }
31.btn-primary { background:var(--accent); color:#0d1117; border-color:var(--accent); font-weight:600; }
32
33main { flex:1; display:flex; overflow:hidden; }
34
35/* VIEWERS */
36.viewers { flex:1; display:flex; gap:2px; background:var(--border); position:relative; }
37.viewer-wrap { flex:1; position:relative; background:#0a0e14; }
38.viewer-label {
39 position:absolute; top:10px; left:10px; z-index:10;
40 font-family:'Syne',sans-serif; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.8px;
41 background:rgba(0,0,0,.6); backdrop-filter:blur(8px);
42 padding:4px 10px; border-radius:5px; border:1px solid var(--border); color:var(--muted);
43}
44.viewer-ver { color:var(--accent); }
45canvas { display:block; width:100% !important; height:100% !important; }
46
47/* DIVIDER HANDLE */
48.divider {
49 position:absolute; left:50%; top:0; bottom:0; width:3px; z-index:20;
50 background:var(--accent); cursor:ew-resize; transform:translateX(-50%);
51 display:flex; align-items:center; justify-content:center;
52}
53.divider-handle {
54 width:24px; height:40px; background:var(--accent); border-radius:4px;
55 display:flex; align-items:center; justify-content:center; color:#0d1117; font-size:14px; font-weight:bold;
56 box-shadow:0 2px 12px rgba(88,166,255,.4);
57}
58
59/* overlay mode */
60.overlay-badge {
61 position:absolute; top:10px; left:50%; transform:translateX(-50%); z-index:30;
62 background:rgba(0,0,0,.7); backdrop-filter:blur(8px);
63 border:1px solid var(--border); border-radius:6px; padding:4px 12px;
64 font-size:11px; color:var(--muted); display:none;
65}
66
67/* SIDE PANEL */
68.side { width:300px; background:var(--surface); border-left:1px solid var(--border); display:flex; flex-direction:column; flex-shrink:0; }
69.side-head { padding:14px 16px 10px; border-bottom:1px solid var(--border); }
70.side-title { font-family:'Syne',sans-serif; font-weight:700; font-size:14px; display:flex; align-items:center; gap:6px; }
71.ai-pill { background:linear-gradient(135deg,#7c3aed,#bc8cff); color:white; font-size:9px; font-weight:800; letter-spacing:1px; text-transform:uppercase; padding:2px 6px; border-radius:4px; }
72
73.legend { display:flex; gap:8px; padding:8px 16px; border-bottom:1px solid var(--border); flex-shrink:0; flex-wrap:wrap; }
74.leg-item { display:flex; align-items:center; gap:4px; font-size:11px; color:var(--muted); }
75.leg-dot { width:10px; height:10px; border-radius:2px; }
76
77.side-body { flex:1; overflow-y:auto; padding:12px; display:flex; flex-direction:column; gap:8px; }
78.side-body::-webkit-scrollbar { width:3px; }
79.side-body::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; }
80
81.stats-row { display:grid; grid-template-columns:1fr 1fr 1fr; gap:6px; }
82.stat { background:var(--surface2); border:1px solid var(--border); border-radius:7px; padding:8px; text-align:center; }
83.stat-n { font-family:'Syne',sans-serif; font-size:18px; font-weight:800; }
84.stat-l { font-size:10px; color:var(--muted); text-transform:uppercase; letter-spacing:.5px; margin-top:2px; }
85.n-add { color:var(--add); } .n-mod { color:var(--mod); } .n-del { color:var(--del); }
86
87.sec-title { font-size:10px; text-transform:uppercase; letter-spacing:.8px; font-weight:700; color:var(--muted); font-family:'Syne',sans-serif; padding:2px 0; }
88
89.change-item {
90 background:var(--surface2); border:1px solid var(--border); border-radius:7px;
91 padding:9px 11px; cursor:pointer; transition:.15s;
92 border-left:3px solid;
93}
94.change-item:hover, .change-item.active { background:var(--bg); box-shadow:0 0 0 1px var(--border); }
95.change-item.add { border-left-color:var(--add); }
96.change-item.mod { border-left-color:var(--mod); }
97.change-item.del { border-left-color:var(--del); }
98.ci-type { font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.5px; margin-bottom:3px; }
99.ci-type.add { color:var(--add); } .ci-type.mod { color:var(--mod); } .ci-type.del { color:var(--del); }
100.ci-name { font-size:13px; font-weight:500; }
101.ci-desc { font-size:11px; color:var(--muted); margin-top:2px; line-height:1.5; }
102
103.ai-box {
104 background:linear-gradient(135deg,rgba(124,58,237,.1),rgba(188,140,255,.05));
105 border:1px solid rgba(188,140,255,.2); border-radius:8px; padding:10px 12px;
106}
107.ai-box-head { font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.6px; color:var(--ai); margin-bottom:6px; display:flex; align-items:center; gap:5px; }
108.ai-box-text { font-size:12px; line-height:1.6; }
109
110/* BOTTOM */
111.bottom { height:44px; background:var(--surface); border-top:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; padding:0 16px; flex-shrink:0; }
112.view-modes { display:flex; gap:4px; }
113.vmode { padding:4px 10px; border-radius:5px; border:1px solid var(--border); font-size:11px; cursor:pointer; background:transparent; color:var(--muted); font-family:'DM Sans',sans-serif; transition:.15s; }
114.vmode.active { background:var(--surface2); color:var(--text); border-color:var(--accent); color:var(--accent); }
115.opacity-ctrl { display:flex; align-items:center; gap:8px; font-size:11px; color:var(--muted); }
116input[type=range] { width:80px; accent-color:var(--accent); }
117
118.spinner { width:28px; height:28px; border:2px solid var(--border); border-top-color:var(--ai); border-radius:50%; animation:spin .7s linear infinite; }
119@keyframes spin { to { transform:rotate(360deg); } }
120.loading-ai { display:flex; flex-direction:column; align-items:center; gap:8px; color:var(--muted); font-size:12px; padding:20px; }
121.dots span { display:inline-block; width:5px; height:5px; background:var(--ai); border-radius:50%; margin:0 2px; animation:blink 1.2s infinite; }
122.dots span:nth-child(2){animation-delay:.2s} .dots span:nth-child(3){animation-delay:.4s}
123@keyframes blink { 0%,80%,100%{opacity:.2;transform:scale(.8)} 40%{opacity:1;transform:scale(1)} }
124</style>
125</head>
126<body>
127
128<nav>
129 <div class="brand">⬡ Cons<span>doc</span></div>
130 <div class="file-info">
131 <svg width="13" height="13" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
132 <b>Snowdon Towers Sample Structural.rvt</b>
133 <span>— BIM Comparison</span>
134 </div>
135 <div class="nav-right">
136 <button class="btn btn-ai" id="aiBtn" onclick="runAIAnalysis()">
137 <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/></svg>
138 Analisis AI
139 </button>
140 <button class="btn btn-primary">Compare files</button>
141 </div>
142</nav>
143
144<main>
145 <div class="viewers" id="viewersContainer">
146 <div class="viewer-wrap" id="wrapA">
147 <div class="viewer-label">Version A &nbsp;<span class="viewer-ver">V0 · Rev 0</span></div>
148 <canvas id="canvasA"></canvas>
149 </div>
150 <div class="divider" id="divider">
151 <div class="divider-handle"></div>
152 </div>
153 <div class="viewer-wrap" id="wrapB">
154 <div class="viewer-label">Version B &nbsp;<span class="viewer-ver">V1 · Rev 0</span></div>
155 <canvas id="canvasB"></canvas>
156 </div>
157 <div class="overlay-badge" id="overlayBadge">Overlay Mode</div>
158 </div>
159
160 <div class="side">
161 <div class="side-head">
162 <div class="side-title">Compare Document <span class="ai-pill">AI</span></div>
163 </div>
164 <div class="legend">
165 <div class="leg-item"><div class="leg-dot" style="background:#3fb950"></div>Ditambahkan</div>
166 <div class="leg-item"><div class="leg-dot" style="background:#f0883e"></div>Dimodifikasi</div>
167 <div class="leg-item"><div class="leg-dot" style="background:#f85149"></div>Dihapus</div>
168 <div class="leg-item"><div class="leg-dot" style="background:#58a6ff;opacity:.35"></div>Tidak berubah</div>
169 </div>
170 <div class="side-body" id="sideBody">
171 <div style="color:var(--muted);font-size:12px;text-align:center;padding:20px 10px;line-height:1.7">
172 Klik <strong style="color:var(--text)">Analisis AI</strong> untuk mendeteksi perbedaan antar versi model dan highlight komponen yang berubah.
173 </div>
174 </div>
175 </div>
176</main>
177
178<div class="bottom">
179 <div class="view-modes">
180 <button class="vmode active" onclick="setViewMode('split',this)">Split</button>
181 <button class="vmode" onclick="setViewMode('overlay',this)">Overlay</button>
182 <button class="vmode" onclick="setViewMode('single',this)">Single</button>
183 </div>
184 <div class="opacity-ctrl">
185 <span>Opacity unchanged</span>
186 <input type="range" id="opacitySlider" min="0" max="100" value="18" oninput="updateOpacity(this.value)">
187 <span id="opacityVal">18%</span>
188 </div>
189</div>
190
191<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
192<script>
193// ─── SCENE SETUP ────────────────────────────────────────────────────────────
194
195let unchangedOpacity = 0.18;
196let viewMode = 'split';
197let analysisData = null;
198let highlightedId = null;
199
200const CHANGES = [
201 { id:'beam_l3_e', floor:3, pos:[3.5,7.5,0.5], size:[3,0.4,0.4], type:'add', label:'Beam L3-E', desc:'Balok baru ditambahkan di lantai 3, sisi timur.' },
202 { id:'col_b2', floor:2, pos:[-2,4.5,2], size:[0.5,3,0.5], type:'mod', label:'Column B2', desc:'Ukuran penampang dimodifikasi dari 400×400 ke 500×500mm.' },
203 { id:'slab_l4', floor:4, pos:[0,9,0], size:[8,0.3,6], type:'mod', label:'Slab L4', desc:'Tebal pelat diubah dari 200mm menjadi 250mm.' },
204 { id:'wall_n1', floor:1, pos:[-4,1.5,-2.5], size:[0.3,3,4], type:'del', label:'Wall N1', desc:'Dinding geser dihapus pada revisi ini.' },
205 { id:'stair_c', floor:2, pos:[4,4.5,-2], size:[1.5,3,2], type:'add', label:'Staircase C', desc:'Tangga darurat baru ditambahkan di sisi utara.' },
206];
207
208const CHANGE_MAP = {};
209CHANGES.forEach(c => CHANGE_MAP[c.id] = c);
210
211// ─── THREE SCENES ────────────────────────────────────────────────────────────
212
213function createScene(canvasId, versionB) {
214 const canvas = document.getElementById(canvasId);
215 const renderer = new THREE.WebGLRenderer({ canvas, antialias:true, alpha:true });
216 renderer.setPixelRatio(window.devicePixelRatio);
217 renderer.shadowMap.enabled = true;
218 renderer.setClearColor(0x0a0e14, 1);
219
220 const scene = new THREE.Scene();
221 scene.fog = new THREE.Fog(0x0a0e14, 30, 80);
222
223 const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 200);
224 camera.position.set(18, 14, 18);
225 camera.lookAt(0, 5, 0);
226
227 // Lighting
228 scene.add(new THREE.AmbientLight(0x334466, 0.8));
229 const dir = new THREE.DirectionalLight(0xffffff, 0.9);
230 dir.position.set(10, 20, 10);
231 dir.castShadow = true;
232 scene.add(dir);
233 scene.add(new THREE.PointLight(0x58a6ff, 0.4, 40));
234
235 // Grid
236 const grid = new THREE.GridHelper(30, 30, 0x1c2128, 0x1c2128);
237 scene.add(grid);
238
239 const meshes = {}; // id → mesh
240
241 // ── build model ───────────────────────────────────────────────────────────
242 const FLOORS = 5;
243 const COLS = [[-3,-3],[3,-3],[3,3],[-3,3],[0,-3],[0,3],[-3,0],[3,0]];
244 const dimColor = 0x2d4a6b;
245
246 function addMesh(geo, mat, pos, id) {
247 const m = new THREE.Mesh(geo, mat);
248 m.position.set(...pos);
249 m.castShadow = true;
250 m.receiveShadow = true;
251 m.userData.id = id;
252 scene.add(m);
253 if (id) meshes[id] = m;
254 return m;
255 }
256
257 // Columns
258 COLS.forEach(([x,z], ci) => {
259 for (let f=0; f<FLOORS; f++) {
260 const mat = new THREE.MeshPhongMaterial({ color:dimColor, transparent:true, opacity:unchangedOpacity });
261 addMesh(new THREE.BoxGeometry(0.5,3,0.5), mat, [x, f*3+1.5, z], `col_${ci}_${f}`);
262 }
263 });
264
265 // Slabs
266 for (let f=0; f<=FLOORS; f++) {
267 const isChanged = (f===4) && versionB;
268 const thickness = (isChanged) ? 0.3 : 0.25;
269 const id = f===4 ? 'slab_l4' : `slab_${f}`;
270 const mat = isChanged
271 ? new THREE.MeshPhongMaterial({ color:0xf0883e, transparent:true, opacity:0.85, emissive:0xf0883e, emissiveIntensity:.15 })
272 : new THREE.MeshPhongMaterial({ color:dimColor, transparent:true, opacity:unchangedOpacity });
273 addMesh(new THREE.BoxGeometry(8, thickness, 6), mat, [0, f*3, 0], id);
274 }
275
276 // Beams
277 for (let f=0; f<FLOORS; f++) {
278 for (let b=0; b<3; b++) {
279 const mat = new THREE.MeshPhongMaterial({ color:dimColor, transparent:true, opacity:unchangedOpacity });
280 addMesh(new THREE.BoxGeometry(8,0.4,0.4), mat, [0, f*3+2.8, -3+b*3], null);
281 addMesh(new THREE.BoxGeometry(0.4,0.4,6), mat, [-3+b*3.5, f*3+2.8, 0], null);
282 }
283 }
284
285 // Version-specific changes
286 if (versionB) {
287 // ADD: new beam L3 east
288 const addMat = new THREE.MeshPhongMaterial({ color:0x3fb950, transparent:true, opacity:.9, emissive:0x3fb950, emissiveIntensity:.2 });
289 addMesh(new THREE.BoxGeometry(3,0.4,0.4), addMat, [3.5,7.5+2.8,0.5], 'beam_l3_e');
290
291 // MOD: column B2 thicker
292 const modCol = new THREE.Mesh(new THREE.BoxGeometry(0.55,3,0.55),
293 new THREE.MeshPhongMaterial({ color:0xf0883e, transparent:true, opacity:.9, emissive:0xf0883e, emissiveIntensity:.2 }));
294 modCol.position.set(-2,4.5,2); modCol.userData.id='col_b2';
295 scene.add(modCol); meshes['col_b2'] = modCol;
296
297 // ADD: staircase C
298 const stairMat = new THREE.MeshPhongMaterial({ color:0x3fb950, transparent:true, opacity:.85, emissive:0x3fb950, emissiveIntensity:.2 });
299 addMesh(new THREE.BoxGeometry(1.5,3,2), stairMat, [4,4.5,-2], 'stair_c');
300 } else {
301 // DEL: wall N1 only in version A
302 const delMat = new THREE.MeshPhongMaterial({ color:0xf85149, transparent:true, opacity:.85, emissive:0xf85149, emissiveIntensity:.2 });
303 addMesh(new THREE.BoxGeometry(0.3,3,4), delMat, [-4,1.5,-2.5], 'wall_n1');
304 }
305
306 // Orbit controls (manual)
307 let isDragging=false, prevMouse={x:0,y:0};
308 let theta=Math.PI/4, phi=Math.PI/4, radius=30;
309 canvas.addEventListener('mousedown', e => { isDragging=true; prevMouse={x:e.clientX,y:e.clientY}; });
310 window.addEventListener('mouseup', () => isDragging=false);
311 window.addEventListener('mousemove', e => {
312 if (!isDragging) return;
313 const dx=(e.clientX-prevMouse.x)*0.01, dy=(e.clientY-prevMouse.y)*0.01;
314 theta-=dx; phi=Math.max(.2,Math.min(1.4,phi+dy));
315 prevMouse={x:e.clientX,y:e.clientY};
316 });
317 canvas.addEventListener('wheel', e => { radius=Math.max(10,Math.min(60,radius+e.deltaY*.05)); e.preventDefault(); }, {passive:false});
318
319 function resizeCanvas() {
320 const w=canvas.clientWidth, h=canvas.clientHeight;
321 renderer.setSize(w,h,false);
322 camera.aspect=w/h; camera.updateProjectionMatrix();
323 }
324
325 function animate() {
326 requestAnimationFrame(animate);
327 if (canvas.clientWidth !== renderer.domElement.width || canvas.clientHeight !== renderer.domElement.height) resizeCanvas();
328 camera.position.x = radius*Math.sin(theta)*Math.cos(phi);
329 camera.position.y = radius*Math.sin(phi)+5;
330 camera.position.z = radius*Math.cos(theta)*Math.cos(phi);
331 camera.lookAt(0,5,0);
332 renderer.render(scene,camera);
333 }
334 animate();
335
336 return { scene, meshes };
337}
338
339const sceneA = createScene('canvasA', false);
340const sceneB = createScene('canvasB', true);
341
342// ─── OPACITY UPDATE ──────────────────────────────────────────────────────────
343
344function updateOpacity(val) {
345 unchangedOpacity = val/100;
346 document.getElementById('opacityVal').textContent = val + '%';
347 [sceneA, sceneB].forEach(({meshes, scene}) => {
348 scene.traverse(obj => {
349 if (!obj.isMesh) return;
350 const id = obj.userData.id;
351 const isChanged = id && CHANGE_MAP[id];
352 if (!isChanged) {
353 obj.material.opacity = unchangedOpacity;
354 obj.material.color.setHex(0x2d4a6b);
355 obj.material.emissiveIntensity = 0;
356 }
357 });
358 });
359}
360
361// ─── HIGHLIGHT ITEM ──────────────────────────────────────────────────────────
362
363function focusItem(id) {
364 highlightedId = id;
365 document.querySelectorAll('.change-item').forEach(el => el.classList.toggle('active', el.dataset.id===id));
366
367 [sceneA, sceneB].forEach(({scene}) => {
368 scene.traverse(obj => {
369 if (!obj.isMesh) return;
370 const oid = obj.userData.id;
371 if (!oid) return;
372 const isTarget = oid === id;
373 const isChanged = oid && CHANGE_MAP[oid];
374 if (isTarget) {
375 obj.material.emissiveIntensity = .5;
376 obj.material.opacity = 1;
377 } else if (isChanged) {
378 obj.material.emissiveIntensity = .15;
379 obj.material.opacity = .85;
380 } else {
381 obj.material.opacity = unchangedOpacity * .5;
382 }
383 });
384 });
385}
386
387// ─── VIEW MODES ──────────────────────────────────────────────────────────────
388
389function setViewMode(mode, el) {
390 viewMode = mode;
391 document.querySelectorAll('.vmode').forEach(b => b.classList.remove('active'));
392 el.classList.add('active');
393 const wA = document.getElementById('wrapA');
394 const wB = document.getElementById('wrapB');
395 const div = document.getElementById('divider');
396 const badge = document.getElementById('overlayBadge');
397
398 if (mode==='split') {
399 wA.style.display=''; wB.style.display=''; div.style.display='';
400 badge.style.display='none';
401 wA.style.flex='1'; wB.style.flex='1';
402 } else if (mode==='overlay') {
403 wA.style.display=''; wB.style.display='none'; div.style.display='none';
404 badge.style.display='block';
405 } else {
406 wA.style.display=''; wB.style.display='none'; div.style.display='none';
407 badge.style.display='none';
408 }
409}
410
411// ─── AI ANALYSIS ─────────────────────────────────────────────────────────────
412
413async function runAIAnalysis() {
414 const btn = document.getElementById('aiBtn');
415 btn.disabled = true;
416 btn.innerHTML = '<div class="spinner" style="width:14px;height:14px;border-width:2px"></div> Analyzing...';
417
418 document.getElementById('sideBody').innerHTML = `
419 <div class="loading-ai">
420 <div class="spinner"></div>
421 <div>AI sedang menganalisis perubahan struktural…</div>
422 <div class="dots"><span></span><span></span><span></span></div>
423 </div>`;
424
425 const modelDescription = `
426Ini adalah model BIM struktural gedung "Snowdon Towers" dengan 5 lantai.
427
428Perubahan antara Version A (Revision 0) dan Version B (Revision 1):
4291. Balok baru (Beam L3-E) ditambahkan di lantai 3 sisi timur, dimensi 300×400mm
4302. Kolom B2 dimodifikasi – penampang naik dari 400×400mm ke 500×500mm di lantai 2
4313. Pelat lantai 4 (Slab L4) – ketebalan diubah dari 200mm ke 250mm
4324. Dinding geser N1 dihapus dari lantai 1
4335. Tangga darurat C (Staircase C) ditambahkan di sisi utara lantai 2
434
435Berikan analisis profesional singkat tentang implikasi struktural dan konstruksi dari perubahan-perubahan ini dalam Bahasa Indonesia. Maksimal 3 kalimat.`;
436
437 try {
438 const res = await fetch('https://api.anthropic.com/v1/messages', {
439 method:'POST',
440 headers:{'Content-Type':'application/json'},
441 body: JSON.stringify({
442 model:'claude-sonnet-4-20250514',
443 max_tokens:1000,
444 messages:[{ role:'user', content: modelDescription }]
445 })
446 });
447 const data = await res.json();
448 const summary = data.content.map(i => i.text||'').join('');
449 renderSidePanel(summary);
450 } catch(e) {
451 renderSidePanel('Perubahan signifikan terdeteksi pada elemen struktural. Terdapat modifikasi kolom dan pelat yang memerlukan review insinyur struktural.');
452 }
453
454 btn.disabled = false;
455 btn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/></svg> Analisis AI';
456}
457
458function renderSidePanel(summary) {
459 const added = CHANGES.filter(c=>c.type==='add').length;
460 const mods = CHANGES.filter(c=>c.type==='mod').length;
461 const dels = CHANGES.filter(c=>c.type==='del').length;
462
463 const typeLabel = {add:'+ Ditambahkan', mod:'✎ Dimodifikasi', del:'- Dihapus'};
464
465 document.getElementById('sideBody').innerHTML = `
466 <div class="stats-row">
467 <div class="stat"><div class="stat-n n-add">${added}</div><div class="stat-l">Added</div></div>
468 <div class="stat"><div class="stat-n n-mod">${mods}</div><div class="stat-l">Modified</div></div>
469 <div class="stat"><div class="stat-n n-del">${dels}</div><div class="stat-l">Removed</div></div>
470 </div>
471
472 <div class="ai-box">
473 <div class="ai-box-head">
474 <svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/></svg>
475 Ringkasan AI
476 </div>
477 <div class="ai-box-text" id="aiSummaryText"></div>
478 </div>
479
480 <div class="sec-title">Komponen Berubah</div>
481 ${CHANGES.map(c => `
482 <div class="change-item ${c.type}" data-id="${c.id}" onclick="focusItem('${c.id}')">
483 <div class="ci-type ${c.type}">${typeLabel[c.type]}</div>
484 <div class="ci-name">${c.label}</div>
485 <div class="ci-desc">${c.desc}</div>
486 </div>
487 `).join('')}
488 `;
489
490 streamText('aiSummaryText', summary);
491}
492
493function streamText(elId, text) {
494 const el = document.getElementById(elId);
495 if (!el) return;
496 el.textContent = '';
497 let i = 0;
498 const iv = setInterval(() => {
499 if (i >= text.length) { clearInterval(iv); return; }
500 el.textContent += text[i++];
501 }, 16);
502}
503
504// Slider tooltip
505document.getElementById('opacitySlider').addEventListener('input', function() {
506 document.getElementById('opacityVal').textContent = this.value + '%';
507});
508</script>
509</body>
510</html>