Last active 1 week ago

Akihira77's Avatar Akihira77 revised this gist 1 week ago. Go to revision

1 file changed, 510 insertions

Bim 3d compare.html (file created)

@@ -0,0 +1,510 @@
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; }
16 + body { background:var(--bg); color:var(--text); font-family:'DM Sans',sans-serif; height:100vh; display:flex; flex-direction:column; overflow:hidden; }
17 +
18 + nav {
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 +
33 + main { 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); }
45 + canvas { 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); }
116 + input[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 +
195 + let unchangedOpacity = 0.18;
196 + let viewMode = 'split';
197 + let analysisData = null;
198 + let highlightedId = null;
199 +
200 + const 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 +
208 + const CHANGE_MAP = {};
209 + CHANGES.forEach(c => CHANGE_MAP[c.id] = c);
210 +
211 + // ─── THREE SCENES ────────────────────────────────────────────────────────────
212 +
213 + function 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 +
339 + const sceneA = createScene('canvasA', false);
340 + const sceneB = createScene('canvasB', true);
341 +
342 + // ─── OPACITY UPDATE ──────────────────────────────────────────────────────────
343 +
344 + function 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 +
363 + function 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 +
389 + function 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 +
413 + async 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 = `
426 + Ini adalah model BIM struktural gedung "Snowdon Towers" dengan 5 lantai.
427 +
428 + Perubahan antara Version A (Revision 0) dan Version B (Revision 1):
429 + 1. Balok baru (Beam L3-E) ditambahkan di lantai 3 sisi timur, dimensi 300×400mm
430 + 2. Kolom B2 dimodifikasi – penampang naik dari 400×400mm ke 500×500mm di lantai 2
431 + 3. Pelat lantai 4 (Slab L4) – ketebalan diubah dari 200mm ke 250mm
432 + 4. Dinding geser N1 dihapus dari lantai 1
433 + 5. Tangga darurat C (Staircase C) ditambahkan di sisi utara lantai 2
434 +
435 + Berikan 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 +
458 + function 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 +
493 + function 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
505 + document.getElementById('opacitySlider').addEventListener('input', function() {
506 + document.getElementById('opacityVal').textContent = this.value + '%';
507 + });
508 + </script>
509 + </body>
510 + </html>
Newer Older