@mindexec/cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/README.md +275 -0
  2. package/codex-runtime.js +960 -0
  3. package/launch-bridge.cjs +162 -0
  4. package/package.json +61 -0
  5. package/port-guard.cjs +232 -0
  6. package/scripts/setup-tree-sitter-grammars.mjs +59 -0
  7. package/server.js +8422 -0
  8. package/start-bridge.bat +32 -0
  9. package/start-bridge.sh +81 -0
  10. package/tree-sitter-grammars/README.md +18 -0
  11. package/tree-sitter-grammars/tree-sitter-c_sharp.wasm +0 -0
  12. package/tree-sitter-grammars/tree-sitter-go.wasm +0 -0
  13. package/tree-sitter-grammars/tree-sitter-java.wasm +0 -0
  14. package/tree-sitter-grammars/tree-sitter-javascript.wasm +0 -0
  15. package/tree-sitter-grammars/tree-sitter-python.wasm +0 -0
  16. package/tree-sitter-grammars/tree-sitter-rust.wasm +0 -0
  17. package/tree-sitter-grammars/tree-sitter-tsx.wasm +0 -0
  18. package/tree-sitter-grammars/tree-sitter-typescript.wasm +0 -0
  19. package/wwwroot/MindExecution.Web.styles.css +3 -0
  20. package/wwwroot/_content/MindExecution.Plugins.Admin/css/admin-dashboard.css +546 -0
  21. package/wwwroot/_content/MindExecution.Plugins.Directory/MindExecution.Plugins.Directory.u7utcng611.bundle.scp.css +7 -0
  22. package/wwwroot/_content/MindExecution.Plugins.Directory/background.png +0 -0
  23. package/wwwroot/_content/MindExecution.Plugins.Directory/directory-manager.js +202 -0
  24. package/wwwroot/_content/MindExecution.Plugins.Directory/exampleJsInterop.js +6 -0
  25. package/wwwroot/_content/MindExecution.Plugins.YouTube/css/youtube-search.css +251 -0
  26. package/wwwroot/_content/MindExecution.Shared/MindExecution.Shared.wsano1j4wp.bundle.scp.css +4 -0
  27. package/wwwroot/_content/MindExecution.Shared/css/admin-dashboard.css +559 -0
  28. package/wwwroot/_content/MindExecution.Shared/css/app.css +1 -0
  29. package/wwwroot/_content/MindExecution.Shared/css/mind-map-overrides.css +2936 -0
  30. package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Bold.ttf +0 -0
  31. package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Regular.ttf +0 -0
  32. package/wwwroot/_content/MindExecution.Shared/js/agent-visualization.js +359 -0
  33. package/wwwroot/_content/MindExecution.Shared/js/background-themes.js +1721 -0
  34. package/wwwroot/_content/MindExecution.Shared/js/code-master.js +8316 -0
  35. package/wwwroot/_content/MindExecution.Shared/js/file-system-helper.js +639 -0
  36. package/wwwroot/_content/MindExecution.Shared/js/helpers/InfiniteGridHelper.js +109 -0
  37. package/wwwroot/_content/MindExecution.Shared/js/marked.min.js +69 -0
  38. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +7982 -0
  39. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js.backup +1059 -0
  40. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +15803 -0
  41. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dev-guards.js +325 -0
  42. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js +1430 -0
  43. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js.bak +434 -0
  44. package/wwwroot/_content/MindExecution.Shared/js/mind-map-glow-shader.js +260 -0
  45. package/wwwroot/_content/MindExecution.Shared/js/mind-map-interactions.js +7640 -0
  46. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-plan-worker.js +160 -0
  47. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +9923 -0
  48. package/wwwroot/_content/MindExecution.Shared/js/mind-map-logic-workers.js +977 -0
  49. package/wwwroot/_content/MindExecution.Shared/js/mind-map-menu-manager.js +1431 -0
  50. package/wwwroot/_content/MindExecution.Shared/js/mind-map-multi-select.js +1716 -0
  51. package/wwwroot/_content/MindExecution.Shared/js/mind-map-node-search-worker.js +553 -0
  52. package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +4541 -0
  53. package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js +489 -0
  54. package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js.backup +372 -0
  55. package/wwwroot/_content/MindExecution.Shared/js/mind-map-pipeline.js +2075 -0
  56. package/wwwroot/_content/MindExecution.Shared/js/mind-map-text-lod-system.js +646 -0
  57. package/wwwroot/_content/MindExecution.Shared/js/mind-map-text-overlay-v2.js +4323 -0
  58. package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js +2260 -0
  59. package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js.backup +1258 -0
  60. package/wwwroot/_content/MindExecution.Shared/js/mind-map-visibility-worker.js +890 -0
  61. package/wwwroot/_content/MindExecution.Shared/js/mindmap-toolbar.js +594 -0
  62. package/wwwroot/_content/MindExecution.Shared/js/native-drop-handler.js +170 -0
  63. package/wwwroot/_content/MindExecution.Shared/js/plan-master.js +788 -0
  64. package/wwwroot/_content/MindExecution.Shared/js/renderers/CSS3DRenderer.js +50 -0
  65. package/wwwroot/_content/MindExecution.Shared/js/texture-worker-manager.js +188 -0
  66. package/wwwroot/_content/MindExecution.Shared/js/texture-worker.js +208 -0
  67. package/wwwroot/_content/MindExecution.Shared/js/three.min.js +6 -0
  68. package/wwwroot/_content/MindExecution.Shared/js/titlebar-handler.js +191 -0
  69. package/wwwroot/_content/MindExecution.Shared/js/token-manager.js +37 -0
  70. package/wwwroot/_content/MindExecution.Shared/js/token-worker.js +28 -0
  71. package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js +5626 -0
  72. package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js.map +7 -0
  73. package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/css/all.min.css +9 -0
  74. package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-brands-400.ttf +0 -0
  75. package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-brands-400.woff2 +0 -0
  76. package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-regular-400.ttf +0 -0
  77. package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-regular-400.woff2 +0 -0
  78. package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-solid-900.ttf +0 -0
  79. package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-solid-900.woff2 +0 -0
  80. package/wwwroot/_content/MindExecution.Shared/models/all-MiniLM-L6-v2-quantized.onnx +0 -0
  81. package/wwwroot/_content/MindExecution.Shared/models/vocab.txt +30522 -0
  82. package/wwwroot/_framework/Google.Protobuf.9h59ukbel7.dll +0 -0
  83. package/wwwroot/_framework/Markdig.d1j7v41cl1.dll +0 -0
  84. package/wwwroot/_framework/MessagePack.Annotations.l6qv48kgpt.dll +0 -0
  85. package/wwwroot/_framework/MessagePack.eqoptzx9d5.dll +0 -0
  86. package/wwwroot/_framework/Microsoft.AspNetCore.Authorization.k7dsih5y5g.dll +0 -0
  87. package/wwwroot/_framework/Microsoft.AspNetCore.Components.6nyje9sa0g.dll +0 -0
  88. package/wwwroot/_framework/Microsoft.AspNetCore.Components.Authorization.iycd6unprw.dll +0 -0
  89. package/wwwroot/_framework/Microsoft.AspNetCore.Components.Web.487u3twia4.dll +0 -0
  90. package/wwwroot/_framework/Microsoft.AspNetCore.Components.WebAssembly.d0gcnmlxxz.dll +0 -0
  91. package/wwwroot/_framework/Microsoft.AspNetCore.Metadata.h4yevl9adi.dll +0 -0
  92. package/wwwroot/_framework/Microsoft.CSharp.qrvp77qmhs.dll +0 -0
  93. package/wwwroot/_framework/Microsoft.Data.Sqlite.jdlxgv2jtg.dll +0 -0
  94. package/wwwroot/_framework/Microsoft.EntityFrameworkCore.4gjazp7kjf.dll +0 -0
  95. package/wwwroot/_framework/Microsoft.EntityFrameworkCore.Abstractions.gocudnvz7b.dll +0 -0
  96. package/wwwroot/_framework/Microsoft.EntityFrameworkCore.Relational.lt4rsvinuo.dll +0 -0
  97. package/wwwroot/_framework/Microsoft.EntityFrameworkCore.Sqlite.69luj0fa9r.dll +0 -0
  98. package/wwwroot/_framework/Microsoft.Extensions.Caching.Abstractions.364t4jh3zz.dll +0 -0
  99. package/wwwroot/_framework/Microsoft.Extensions.Caching.Memory.izlxhpzosu.dll +0 -0
  100. package/wwwroot/_framework/Microsoft.Extensions.Configuration.8zq7hh41o7.dll +0 -0
  101. package/wwwroot/_framework/Microsoft.Extensions.Configuration.Abstractions.8if74zs6ea.dll +0 -0
  102. package/wwwroot/_framework/Microsoft.Extensions.Configuration.Json.duvlngw8i0.dll +0 -0
  103. package/wwwroot/_framework/Microsoft.Extensions.DependencyInjection.Abstractions.t2hh9kvx0o.dll +0 -0
  104. package/wwwroot/_framework/Microsoft.Extensions.DependencyInjection.n4tg99oy8l.dll +0 -0
  105. package/wwwroot/_framework/Microsoft.Extensions.DependencyModel.h0d06ixk3e.dll +0 -0
  106. package/wwwroot/_framework/Microsoft.Extensions.Logging.Abstractions.rl32bkx2sd.dll +0 -0
  107. package/wwwroot/_framework/Microsoft.Extensions.Logging.dlht1xei0t.dll +0 -0
  108. package/wwwroot/_framework/Microsoft.Extensions.Options.qeunebioml.dll +0 -0
  109. package/wwwroot/_framework/Microsoft.Extensions.Primitives.18cr6vnuuz.dll +0 -0
  110. package/wwwroot/_framework/Microsoft.IO.RecyclableMemoryStream.r915vovvw4.dll +0 -0
  111. package/wwwroot/_framework/Microsoft.IdentityModel.Abstractions.1ejljk3erv.dll +0 -0
  112. package/wwwroot/_framework/Microsoft.IdentityModel.JsonWebTokens.1596zr8gne.dll +0 -0
  113. package/wwwroot/_framework/Microsoft.IdentityModel.Logging.229uyvpgio.dll +0 -0
  114. package/wwwroot/_framework/Microsoft.IdentityModel.Tokens.9sibtajc9f.dll +0 -0
  115. package/wwwroot/_framework/Microsoft.JSInterop.17lq4j1j7g.dll +0 -0
  116. package/wwwroot/_framework/Microsoft.JSInterop.WebAssembly.ryia5gxiad.dll +0 -0
  117. package/wwwroot/_framework/Microsoft.ML.OnnxRuntime.w9deo1m5ss.dll +0 -0
  118. package/wwwroot/_framework/Microsoft.ML.Tokenizers.cm2vuv2z61.dll +0 -0
  119. package/wwwroot/_framework/Microsoft.NET.StringTools.3qbrf4v2ki.dll +0 -0
  120. package/wwwroot/_framework/MimeMapping.og9ys58ylm.dll +0 -0
  121. package/wwwroot/_framework/MindExecution.Core.1q1trifbuu.dll +0 -0
  122. package/wwwroot/_framework/MindExecution.Kernel.gwwc40sc45.dll +0 -0
  123. package/wwwroot/_framework/MindExecution.Plugins.Admin.0jgrn1sckv.dll +0 -0
  124. package/wwwroot/_framework/MindExecution.Plugins.Business.13mme2qcag.dll +0 -0
  125. package/wwwroot/_framework/MindExecution.Plugins.Concept.dfp2mdt45q.dll +0 -0
  126. package/wwwroot/_framework/MindExecution.Plugins.Directory.3w4t6n3se0.dll +0 -0
  127. package/wwwroot/_framework/MindExecution.Plugins.PlanMaster.s0qpntz420.dll +0 -0
  128. package/wwwroot/_framework/MindExecution.Plugins.YouTube.iu11fq8d16.dll +0 -0
  129. package/wwwroot/_framework/MindExecution.Shared.7j27dcqnrc.dll +0 -0
  130. package/wwwroot/_framework/MindExecution.Web.pq1ty8ov2v.dll +0 -0
  131. package/wwwroot/_framework/Newtonsoft.Json.a56zs13vug.dll +0 -0
  132. package/wwwroot/_framework/SQLitePCLRaw.batteries_v2.rrd1nzawpp.dll +0 -0
  133. package/wwwroot/_framework/SQLitePCLRaw.core.1dxloztpfz.dll +0 -0
  134. package/wwwroot/_framework/SQLitePCLRaw.provider.e_sqlite3.oekyzl53i1.dll +0 -0
  135. package/wwwroot/_framework/Supabase.Core.s1pkj4aj0l.dll +0 -0
  136. package/wwwroot/_framework/Supabase.Functions.qz4nu782sg.dll +0 -0
  137. package/wwwroot/_framework/Supabase.Gotrue.twah27pkik.dll +0 -0
  138. package/wwwroot/_framework/Supabase.Postgrest.gmuuv369ih.dll +0 -0
  139. package/wwwroot/_framework/Supabase.Realtime.ox3kchdy3w.dll +0 -0
  140. package/wwwroot/_framework/Supabase.Storage.fnjnepaowr.dll +0 -0
  141. package/wwwroot/_framework/Supabase.azmaw5pgcz.dll +0 -0
  142. package/wwwroot/_framework/System.Collections.Concurrent.y1zmvuyipi.dll +0 -0
  143. package/wwwroot/_framework/System.Collections.Immutable.ug3j698qms.dll +0 -0
  144. package/wwwroot/_framework/System.Collections.NonGeneric.h66hj3863h.dll +0 -0
  145. package/wwwroot/_framework/System.Collections.Specialized.umr3y27ntj.dll +0 -0
  146. package/wwwroot/_framework/System.Collections.x53e19vfsj.dll +0 -0
  147. package/wwwroot/_framework/System.ComponentModel.Annotations.tz6gnt4ebt.dll +0 -0
  148. package/wwwroot/_framework/System.ComponentModel.Primitives.j7tiphu4rg.dll +0 -0
  149. package/wwwroot/_framework/System.ComponentModel.TypeConverter.ujlztox1gx.dll +0 -0
  150. package/wwwroot/_framework/System.ComponentModel.x9xz0ojfb6.dll +0 -0
  151. package/wwwroot/_framework/System.Console.ijzpqmj7ne.dll +0 -0
  152. package/wwwroot/_framework/System.Data.Common.1r0sqffq1p.dll +0 -0
  153. package/wwwroot/_framework/System.Diagnostics.DiagnosticSource.9upoqwq09o.dll +0 -0
  154. package/wwwroot/_framework/System.Diagnostics.Process.m99azzntjm.dll +0 -0
  155. package/wwwroot/_framework/System.Diagnostics.TraceSource.pl7wv26myr.dll +0 -0
  156. package/wwwroot/_framework/System.Diagnostics.Tracing.crlhfx6tut.dll +0 -0
  157. package/wwwroot/_framework/System.Drawing.Primitives.22e4y9ikq9.dll +0 -0
  158. package/wwwroot/_framework/System.Drawing.mi7d8hwowb.dll +0 -0
  159. package/wwwroot/_framework/System.Formats.Asn1.jx23sjiqnn.dll +0 -0
  160. package/wwwroot/_framework/System.IO.Compression.6fyoii3uej.dll +0 -0
  161. package/wwwroot/_framework/System.IO.Pipelines.vg77t4cd4d.dll +0 -0
  162. package/wwwroot/_framework/System.IdentityModel.Tokens.Jwt.t67es60z5b.dll +0 -0
  163. package/wwwroot/_framework/System.Linq.1bkoxlqgmq.dll +0 -0
  164. package/wwwroot/_framework/System.Linq.Expressions.24xqiypwdt.dll +0 -0
  165. package/wwwroot/_framework/System.Linq.Queryable.hvd01d6rsa.dll +0 -0
  166. package/wwwroot/_framework/System.Memory.8dx3lwgym4.dll +0 -0
  167. package/wwwroot/_framework/System.Net.Http.Json.3mhdm9l1rf.dll +0 -0
  168. package/wwwroot/_framework/System.Net.Http.eitrz660my.dll +0 -0
  169. package/wwwroot/_framework/System.Net.NetworkInformation.3pkuofcv9r.dll +0 -0
  170. package/wwwroot/_framework/System.Net.Ping.8clj5pklrp.dll +0 -0
  171. package/wwwroot/_framework/System.Net.Primitives.qrp4wcjz1p.dll +0 -0
  172. package/wwwroot/_framework/System.Net.WebSockets.Client.2u6pv01g69.dll +0 -0
  173. package/wwwroot/_framework/System.Net.WebSockets.qp6u31zvm5.dll +0 -0
  174. package/wwwroot/_framework/System.Numerics.Tensors.0c7z4mt3on.dll +0 -0
  175. package/wwwroot/_framework/System.Numerics.Vectors.kc7ufp2j4l.dll +0 -0
  176. package/wwwroot/_framework/System.ObjectModel.qv82fot1ib.dll +0 -0
  177. package/wwwroot/_framework/System.Private.CoreLib.rkafq04oma.dll +0 -0
  178. package/wwwroot/_framework/System.Private.Uri.t9542hmr6j.dll +0 -0
  179. package/wwwroot/_framework/System.Private.Xml.Linq.n8n3ptrbwu.dll +0 -0
  180. package/wwwroot/_framework/System.Private.Xml.rxd3tytisn.dll +0 -0
  181. package/wwwroot/_framework/System.Reactive.t3fuon548l.dll +0 -0
  182. package/wwwroot/_framework/System.Reflection.Emit.9tjhp6y0j3.dll +0 -0
  183. package/wwwroot/_framework/System.Reflection.Emit.ILGeneration.stxyk8zoo1.dll +0 -0
  184. package/wwwroot/_framework/System.Reflection.Emit.Lightweight.6xrd5v8vg0.dll +0 -0
  185. package/wwwroot/_framework/System.Reflection.Primitives.wgn8fpwwvv.dll +0 -0
  186. package/wwwroot/_framework/System.Runtime.InteropServices.JavaScript.sliym526xh.dll +0 -0
  187. package/wwwroot/_framework/System.Runtime.InteropServices.RuntimeInformation.oji7zut14z.dll +0 -0
  188. package/wwwroot/_framework/System.Runtime.InteropServices.te07xr2we9.dll +0 -0
  189. package/wwwroot/_framework/System.Runtime.Intrinsics.507y4h8nzq.dll +0 -0
  190. package/wwwroot/_framework/System.Runtime.Loader.v7gk4bse0k.dll +0 -0
  191. package/wwwroot/_framework/System.Runtime.Numerics.eqy5xjv3nd.dll +0 -0
  192. package/wwwroot/_framework/System.Runtime.Serialization.Formatters.zpkrub8lab.dll +0 -0
  193. package/wwwroot/_framework/System.Runtime.Serialization.Primitives.vhkpnbxjip.dll +0 -0
  194. package/wwwroot/_framework/System.Runtime.jn319d5nyg.dll +0 -0
  195. package/wwwroot/_framework/System.Security.Claims.0ztig1q9vo.dll +0 -0
  196. package/wwwroot/_framework/System.Security.Cryptography.vttizqc9ho.dll +0 -0
  197. package/wwwroot/_framework/System.Text.Encoding.Extensions.utdd47ny8f.dll +0 -0
  198. package/wwwroot/_framework/System.Text.Encodings.Web.wah8r1zoe0.dll +0 -0
  199. package/wwwroot/_framework/System.Text.Json.kxlfxj0wrs.dll +0 -0
  200. package/wwwroot/_framework/System.Text.RegularExpressions.dbqn58klox.dll +0 -0
  201. package/wwwroot/_framework/System.Threading.42ao9vi047.dll +0 -0
  202. package/wwwroot/_framework/System.Threading.Channels.hfa7j0uv2w.dll +0 -0
  203. package/wwwroot/_framework/System.Threading.Thread.caul0pdqul.dll +0 -0
  204. package/wwwroot/_framework/System.Transactions.Local.fimi2hamzo.dll +0 -0
  205. package/wwwroot/_framework/System.Web.HttpUtility.gq8yz50p2e.dll +0 -0
  206. package/wwwroot/_framework/System.Xml.Linq.kitin4zjoj.dll +0 -0
  207. package/wwwroot/_framework/System.Xml.ReaderWriter.kzvw3qgxb0.dll +0 -0
  208. package/wwwroot/_framework/System.Xml.XDocument.c539ki6cuq.dll +0 -0
  209. package/wwwroot/_framework/System.m05i39uvk9.dll +0 -0
  210. package/wwwroot/_framework/Websocket.Client.vapounvmnl.dll +0 -0
  211. package/wwwroot/_framework/blazor.boot.json +305 -0
  212. package/wwwroot/_framework/blazor.webassembly.js +1 -0
  213. package/wwwroot/_framework/dotnet.js +4 -0
  214. package/wwwroot/_framework/dotnet.native.vz0adxojrz.wasm +0 -0
  215. package/wwwroot/_framework/dotnet.native.xsn1d6x2kd.js +16 -0
  216. package/wwwroot/_framework/dotnet.runtime.dstopyvqzi.js +4 -0
  217. package/wwwroot/_framework/icudt_CJK.tjcz0u77k5.dat +0 -0
  218. package/wwwroot/_framework/icudt_EFIGS.tptq2av103.dat +0 -0
  219. package/wwwroot/_framework/icudt_no_CJK.lfu7j35m59.dat +0 -0
  220. package/wwwroot/_framework/netstandard.0xet7jg7ky.dll +0 -0
  221. package/wwwroot/_headers +40 -0
  222. package/wwwroot/_redirects +1 -0
  223. package/wwwroot/appsettings.json +71 -0
  224. package/wwwroot/icon-192.png +0 -0
  225. package/wwwroot/icon-512.png +0 -0
  226. package/wwwroot/index.html +710 -0
  227. package/wwwroot/js/marketing-tool.js +180 -0
  228. package/wwwroot/manifest.webmanifest +22 -0
  229. package/wwwroot/robots.txt +4 -0
  230. package/wwwroot/service-worker-assets.js +857 -0
  231. package/wwwroot/service-worker.js +33 -0
  232. package/wwwroot/sitemap.xml +27 -0
@@ -0,0 +1,1430 @@
1
+ // File: mind-map-dnd.js
2
+ // MindMap drag-and-drop handling (preprocess-then-create approach to reduce flicker)
3
+ window.MindMapDnD = (function () {
4
+
5
+ // ▼▼▼ [New] IndexedDB helper for directory handles ▼▼▼
6
+ const IDBHelper = {
7
+ dbName: 'MindMapDB',
8
+ storeName: 'handles',
9
+ async getDB() {
10
+ return new Promise((resolve, reject) => {
11
+ const request = indexedDB.open(this.dbName, 1);
12
+ request.onupgradeneeded = (e) => {
13
+ if (!e.target.result.objectStoreNames.contains(this.storeName)) {
14
+ e.target.result.createObjectStore(this.storeName);
15
+ }
16
+ };
17
+ request.onsuccess = () => resolve(request.result);
18
+ request.onerror = () => reject(request.error);
19
+ });
20
+ },
21
+ async set(key, val) {
22
+ const db = await this.getDB();
23
+ return new Promise((resolve, reject) => {
24
+ const tx = db.transaction(this.storeName, 'readwrite');
25
+ tx.objectStore(this.storeName).put(val, key);
26
+ tx.oncomplete = () => resolve();
27
+ tx.onerror = () => reject(tx.error);
28
+ });
29
+ },
30
+ async get(key) {
31
+ const db = await this.getDB();
32
+ return new Promise((resolve, reject) => {
33
+ const tx = db.transaction(this.storeName, 'readonly');
34
+ const req = tx.objectStore(this.storeName).get(key);
35
+ req.onsuccess = () => resolve(req.result);
36
+ req.onerror = () => reject(req.error);
37
+ });
38
+ }
39
+ };
40
+ // ▲▲▲ [New] ▲▲▲
41
+
42
+ if (!window.MindMapFileRegistry) {
43
+ window.MindMapFileRegistry = [];
44
+ }
45
+
46
+ function registerCoreHandlers() {
47
+ // 1. PDF handler
48
+ if (!window.MindMapFileRegistry.some(h => h.name === 'CorePDF')) {
49
+ window.MindMapFileRegistry.push({
50
+ name: 'CorePDF',
51
+ priority: 5,
52
+ check: (file) => file.type === 'application/pdf',
53
+ getType: () => 'pdf'
54
+ });
55
+ }
56
+
57
+ // 2. Image handler (updated)
58
+ if (!window.MindMapFileRegistry.some(h => h.name === 'CoreImage')) {
59
+ window.MindMapFileRegistry.push({
60
+ name: 'CoreImage',
61
+ priority: 5,
62
+ // [Fix] If MIME type is empty or octet-stream, fall back to extension check
63
+ check: (file) => {
64
+ if (file.type && file.type.startsWith('image/')) return true;
65
+
66
+ // Extension-based check (Windows/WebView2 compatibility)
67
+ const name = file.name ? file.name.toLowerCase() : '';
68
+ return /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/.test(name);
69
+ },
70
+ getType: () => 'image'
71
+ });
72
+ }
73
+
74
+ // 3. Video handler
75
+ if (!window.MindMapFileRegistry.some(h => h.name === 'CoreVideo')) {
76
+ window.MindMapFileRegistry.push({
77
+ name: 'CoreVideo',
78
+ priority: 5,
79
+ check: (file) => {
80
+ if (file.type && file.type.startsWith('video/')) return true;
81
+ const name = file.name ? file.name.toLowerCase() : '';
82
+ return /\.(mp4|webm|mov|ogv|m4v)$/.test(name);
83
+ },
84
+ getType: () => 'video'
85
+ });
86
+ }
87
+
88
+ // ▼▼▼ [New] 4. Text handler (basic .txt support) ▼▼▼
89
+ if (!window.MindMapFileRegistry.some(h => h.name === 'CoreText')) {
90
+ window.MindMapFileRegistry.push({
91
+ name: 'CoreText',
92
+ priority: 5,
93
+ check: (file) => {
94
+ // If MIME type is text/plain or extension is .txt or .md
95
+ const name = file.name ? file.name.toLowerCase() : '';
96
+ return file.type === 'text/plain' ||
97
+ name.endsWith('.txt') ||
98
+ name.endsWith('.md');
99
+ },
100
+ getType: () => 'text'
101
+ });
102
+ }
103
+ // ▲▲▲ [New] ▲▲▲
104
+
105
+ // ▼▼▼ [New] 5. Code handler (source code files - Troika rendered) ▼▼▼
106
+ if (!window.MindMapFileRegistry.some(h => h.name === 'CoreCode')) {
107
+ const CODE_EXTENSIONS = [
108
+ '.js', '.jsx', '.ts', '.tsx', '.py', '.cs', '.java',
109
+ '.cpp', '.c', '.h', '.go', '.rs', '.rb', '.php',
110
+ '.swift', '.kt', '.json', '.xml', '.yaml', '.yml',
111
+ '.sql', '.html', '.css', '.scss', '.less', '.sh',
112
+ '.bat', '.ps1', '.csv', '.razor' // Added .razor for Blazor
113
+ ];
114
+ window.MindMapFileRegistry.push({
115
+ name: 'CoreCode',
116
+ priority: 4, // Higher priority than text
117
+ check: (file) => {
118
+ const name = file.name ? file.name.toLowerCase() : '';
119
+ return CODE_EXTENSIONS.some(ext => name.endsWith(ext));
120
+ },
121
+ getType: () => 'code'
122
+ });
123
+ }
124
+ // ▲▲▲ [New] ▲▲▲
125
+
126
+ console.log('[MindMapDnD] Core file handlers (PDF, Image, Video, Text, Code) registered');
127
+ }
128
+ registerCoreHandlers();
129
+
130
+ const eventHandlers = new WeakMap();
131
+ const GRID_SNAP = 10;
132
+
133
+ function computeDroppedImageNodeSize(originalWidth, originalHeight) {
134
+ if (window.mindMap?.computeImageNodeSize) {
135
+ return window.mindMap.computeImageNodeSize(originalWidth, originalHeight, {
136
+ gridSnap: GRID_SNAP,
137
+ minWidth: 100,
138
+ minHeight: 100,
139
+ maxWidth: 4000,
140
+ maxHeight: 4000,
141
+ fallbackWidth: 470,
142
+ fallbackHeight: 350
143
+ });
144
+ }
145
+
146
+ const width = Math.max(100, Math.round(Number(originalWidth || 470) / GRID_SNAP) * GRID_SNAP);
147
+ const height = Math.max(100, Math.round(Number(originalHeight || 350) / GRID_SNAP) * GRID_SNAP);
148
+ return { width, height };
149
+ }
150
+
151
+ // ▼▼▼ [Profiling] Drag statistics tracking ▼▼▼
152
+ let _dragStats = {
153
+ startTime: 0,
154
+ dragOverCount: 0
155
+ };
156
+ // ▲▲▲ [Profiling] ▲▲▲
157
+
158
+ function getBoundHandlers(module) {
159
+ if (!eventHandlers.has(module)) {
160
+ eventHandlers.set(module, {
161
+ boundOnDragEnter: onDragEnter.bind(null, module), // [New]
162
+ boundOnDragOver: onDragOver.bind(null, module),
163
+ boundOnDragLeave: onDragLeave.bind(null, module),
164
+ boundOnDrop: onDrop.bind(null, module)
165
+ });
166
+ }
167
+ return eventHandlers.get(module);
168
+ }
169
+
170
+ // [New] dragenter handler
171
+ function onDragEnter(module, e) {
172
+ // Ignore movements between child elements inside the container
173
+ if (module.container.contains(e.relatedTarget)) return;
174
+
175
+ e.preventDefault();
176
+ e.stopPropagation();
177
+ if (e.dataTransfer) {
178
+ e.dataTransfer.dropEffect = 'copy';
179
+ }
180
+
181
+ // ▼▼▼ [Profiling] Record drag start time ▼▼▼
182
+ _dragStats.startTime = performance.now();
183
+ _dragStats.dragOverCount = 0;
184
+ console.log('[DnD] 🟢 Drag entered. Tracking started.');
185
+ // ▲▲▲ [Profiling] ▲▲▲
186
+
187
+ return false;
188
+ }
189
+
190
+ function onDragOver(module, e) {
191
+ // ▼▼▼ [Optimization] Do the minimum work - avoid IPC overload ▼▼▼
192
+ // preventDefault() is required to allow drop
193
+ e.preventDefault();
194
+
195
+ // Profiling counter (very lightweight)
196
+ _dragStats.dragOverCount++;
197
+
198
+ // Remove all extra work (like setting dropEffect)
199
+ // In WebView2, each call can trigger IPC traffic
200
+ return false;
201
+ // ▲▲▲ [Optimization] ▲▲▲
202
+ }
203
+
204
+ function onDragLeave(module, e) {
205
+ // Do not treat entering a child element as a leave
206
+ if (module.container.contains(e.relatedTarget)) return;
207
+
208
+ e.preventDefault();
209
+ e.stopPropagation();
210
+ }
211
+
212
+ function extractImageUrl(dataTransfer) {
213
+ if (!dataTransfer) return null;
214
+ const html = dataTransfer.getData('text/html');
215
+ if (html) {
216
+ const parser = new DOMParser();
217
+ const doc = parser.parseFromString(html, 'text/html');
218
+ const img = doc.querySelector('img');
219
+ if (img && img.src) return img.src;
220
+ }
221
+ const text = (dataTransfer.getData('text/plain') || '').trim();
222
+ if (text && (text.startsWith('data:image') || isImageUrl(text))) return text;
223
+ return null;
224
+ }
225
+
226
+ function extractDroppedUrl(dataTransfer) {
227
+ if (!dataTransfer) return null;
228
+ const uri = dataTransfer.getData('text/uri-list') || dataTransfer.getData('URL');
229
+ if (uri) return uri.split(/\r?\n/).find(line => line && !line.startsWith('#')) || uri;
230
+ const text = (dataTransfer.getData('text/plain') || '').trim();
231
+ if (text && /^https?:\/\//i.test(text)) return text;
232
+ return null;
233
+ }
234
+
235
+ function isImageUrl(url) {
236
+ return /^https?:\/\/.+\.(png|jpe?g|gif|webp|bmp|svg)(\?|#|$)/i.test(String(url || '').trim());
237
+ }
238
+
239
+ async function fetchImageAsBlobUrl(url) {
240
+ if (!url) return null;
241
+ if (url.startsWith('data:') || url.startsWith('blob:')) {
242
+ return url;
243
+ }
244
+
245
+ try {
246
+ const response = await fetch(url, { mode: 'cors', credentials: 'omit' });
247
+ if (!response.ok) throw new Error(`Network response was not ok (${response.status})`);
248
+ const blob = await response.blob();
249
+ return URL.createObjectURL(blob);
250
+ } catch (e) {
251
+ console.warn(`[MindMapDnD] CORS fetch failed (${e?.message || 'unknown'}). Image drop cancelled.`, e);
252
+ window.MindMapFeedback?.toast?.('The server blocked copying.', { kind: 'error' });
253
+ return null;
254
+ }
255
+ }
256
+
257
+ function measureImageSize(src) {
258
+ return new Promise((resolve) => {
259
+ const img = new Image();
260
+ img.onload = () => {
261
+ resolve(computeDroppedImageNodeSize(img.naturalWidth, img.naturalHeight));
262
+ };
263
+ img.onerror = () => {
264
+ console.warn('[MindMapDnD] Failed to measure image size. Using default.');
265
+ resolve(computeDroppedImageNodeSize(470, 350));
266
+ };
267
+ img.src = src;
268
+ });
269
+ }
270
+
271
+ const HOSTED_LOCAL_BRIDGE_STORAGE_KEY = 'MindExec.AllowHostedLocalBridge';
272
+ const CLOUD_VIDEO_DND_BLOCKED_MESSAGE = 'Local video files are not available in cloud mode. Use Local Bridge for large video workflows, or use YouTube, a direct video URL, or AI video.';
273
+
274
+ function isLoopbackHost(hostname) {
275
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
276
+ }
277
+
278
+ function getCurrentLoopbackBridgeUrl() {
279
+ const location = window.location;
280
+ if (!location || !/^https?:$/i.test(location.protocol || '') || !isLoopbackHost(location.hostname || '')) {
281
+ return null;
282
+ }
283
+
284
+ return location.origin;
285
+ }
286
+
287
+ function preferCurrentLoopbackBridgeOrigin(bridgeUrl) {
288
+ const currentOrigin = getCurrentLoopbackBridgeUrl();
289
+ if (!bridgeUrl || !currentOrigin) {
290
+ return bridgeUrl;
291
+ }
292
+
293
+ try {
294
+ const bridgeUri = new URL(bridgeUrl);
295
+ const currentUri = new URL(currentOrigin);
296
+ if (isLoopbackHost(bridgeUri.hostname)
297
+ && isLoopbackHost(currentUri.hostname)
298
+ && bridgeUri.protocol === currentUri.protocol
299
+ && bridgeUri.port === currentUri.port) {
300
+ return currentOrigin;
301
+ }
302
+ } catch {
303
+ return bridgeUrl;
304
+ }
305
+
306
+ return bridgeUrl;
307
+ }
308
+
309
+ // Direct upload to Bridge server (faster than C# route)
310
+ const BRIDGE_URLS = [getCurrentLoopbackBridgeUrl(), 'http://127.0.0.1:5147', 'http://localhost:5147']
311
+ .filter((url, index, urls) => !!url && urls.indexOf(url) === index);
312
+ let bridgeAuthPromise = null;
313
+ let bridgeConnectedPromise = null;
314
+ let activeBridgeUrl = preferCurrentLoopbackBridgeOrigin(window.MindExecLocalBridge?.activeBridgeUrl || BRIDGE_URLS[0]);
315
+
316
+ function isHostedLocalBridgeExplicitlyEnabled() {
317
+ if (window.MindExecLocalBridge?.allowHostedAutoProbe === true) {
318
+ return true;
319
+ }
320
+
321
+ try {
322
+ return window.localStorage?.getItem(HOSTED_LOCAL_BRIDGE_STORAGE_KEY) === 'true';
323
+ } catch {
324
+ return false;
325
+ }
326
+ }
327
+
328
+ function resetLocalBridgeProbeCache() {
329
+ bridgeAuthPromise = null;
330
+ bridgeConnectedPromise = null;
331
+ window.__mindExecutionBridgeAuthPromise = null;
332
+ }
333
+
334
+ function setActiveBridgeUrl(bridgeUrl) {
335
+ activeBridgeUrl = preferCurrentLoopbackBridgeOrigin(bridgeUrl || BRIDGE_URLS[0]);
336
+ window.MindExecLocalBridge = {
337
+ ...(window.MindExecLocalBridge || {}),
338
+ activeBridgeUrl
339
+ };
340
+ }
341
+
342
+ function setHostedLocalBridgeExplicitlyEnabled(enabled) {
343
+ const nextEnabled = enabled === true;
344
+ try {
345
+ if (nextEnabled) {
346
+ window.localStorage?.setItem(HOSTED_LOCAL_BRIDGE_STORAGE_KEY, 'true');
347
+ } else {
348
+ window.localStorage?.removeItem(HOSTED_LOCAL_BRIDGE_STORAGE_KEY);
349
+ }
350
+ } catch { }
351
+
352
+ window.MindExecLocalBridge = {
353
+ ...(window.MindExecLocalBridge || {}),
354
+ allowHostedAutoProbe: nextEnabled,
355
+ hostedAccessEnabled: nextEnabled
356
+ };
357
+ resetLocalBridgeProbeCache();
358
+ window.dispatchEvent?.(new CustomEvent('mindexec-local-bridge-access-changed', {
359
+ detail: { enabled: nextEnabled }
360
+ }));
361
+ }
362
+
363
+ function isLocalBridgeAutoProbeAllowed() {
364
+ if (window.__isNativeApp) {
365
+ return false;
366
+ }
367
+
368
+ const hostname = window.location?.hostname || '';
369
+ return isLoopbackHost(hostname) || isHostedLocalBridgeExplicitlyEnabled();
370
+ }
371
+
372
+ function getBridgeUrlCandidates() {
373
+ const seen = new Set();
374
+ return [getCurrentLoopbackBridgeUrl(), activeBridgeUrl, ...BRIDGE_URLS].filter(url => {
375
+ if (!url || seen.has(url)) {
376
+ return false;
377
+ }
378
+
379
+ seen.add(url);
380
+ return true;
381
+ });
382
+ }
383
+
384
+ function buildBridgeStatusUrl(bridgeUrl) {
385
+ const nonce = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
386
+ return `${bridgeUrl}/api/status?mindexecOrigin=pages&cb=${encodeURIComponent(nonce)}`;
387
+ }
388
+
389
+ function rewriteBridgeAssetUrl(url, bridgeUrl) {
390
+ if (!url || !bridgeUrl) {
391
+ return url;
392
+ }
393
+
394
+ try {
395
+ const parsed = new URL(url);
396
+ if (!isLoopbackHost(parsed.hostname) || !parsed.pathname.startsWith('/assets/')) {
397
+ return url;
398
+ }
399
+
400
+ return `${bridgeUrl}${parsed.pathname}${parsed.search}${parsed.hash}`;
401
+ } catch {
402
+ return url;
403
+ }
404
+ }
405
+
406
+ async function getBridgeAuthHeaders() {
407
+ if (!isLocalBridgeAutoProbeAllowed()) {
408
+ return {};
409
+ }
410
+
411
+ bridgeAuthPromise ??= (async () => {
412
+ for (const bridgeUrl of getBridgeUrlCandidates()) {
413
+ try {
414
+ const response = await fetch(buildBridgeStatusUrl(bridgeUrl), {
415
+ cache: 'no-store',
416
+ targetAddressSpace: 'loopback'
417
+ });
418
+ if (!response.ok) {
419
+ continue;
420
+ }
421
+
422
+ const status = await response.json();
423
+ setActiveBridgeUrl(bridgeUrl);
424
+ const token = status?.bridgeToken;
425
+ const header = status?.bridgeTokenHeader || 'X-Bridge-Token';
426
+ return token ? { [header]: token } : {};
427
+ } catch {
428
+ // Try next loopback spelling.
429
+ }
430
+ }
431
+
432
+ return {};
433
+ })();
434
+
435
+ return bridgeAuthPromise;
436
+ }
437
+
438
+ async function isBridgeConnected() {
439
+ if (!isLocalBridgeAutoProbeAllowed()) {
440
+ return false;
441
+ }
442
+
443
+ bridgeConnectedPromise ??= (async () => {
444
+ const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
445
+ const timeoutId = controller ? window.setTimeout(() => controller.abort(), 900) : null;
446
+
447
+ try {
448
+ for (const bridgeUrl of getBridgeUrlCandidates()) {
449
+ try {
450
+ const response = await fetch(buildBridgeStatusUrl(bridgeUrl), {
451
+ cache: 'no-store',
452
+ signal: controller?.signal,
453
+ targetAddressSpace: 'loopback'
454
+ });
455
+ if (response.ok) {
456
+ setActiveBridgeUrl(bridgeUrl);
457
+ return true;
458
+ }
459
+ } catch {
460
+ // Try next loopback spelling.
461
+ }
462
+ }
463
+
464
+ return false;
465
+ } finally {
466
+ if (timeoutId !== null) {
467
+ window.clearTimeout(timeoutId);
468
+ }
469
+ }
470
+ })();
471
+
472
+ return bridgeConnectedPromise;
473
+ }
474
+
475
+ async function enableHostedLocalBridgeAccessAndProbe() {
476
+ setHostedLocalBridgeExplicitlyEnabled(true);
477
+ return await isBridgeConnected();
478
+ }
479
+
480
+ window.MindExecLocalBridge = {
481
+ ...(window.MindExecLocalBridge || {}),
482
+ storageKey: HOSTED_LOCAL_BRIDGE_STORAGE_KEY,
483
+ allowHostedAutoProbe: isHostedLocalBridgeExplicitlyEnabled(),
484
+ hostedAccessEnabled: isHostedLocalBridgeExplicitlyEnabled(),
485
+ isHostedAccessEnabled: isHostedLocalBridgeExplicitlyEnabled,
486
+ enableHostedAccess: () => setHostedLocalBridgeExplicitlyEnabled(true),
487
+ disableHostedAccess: () => setHostedLocalBridgeExplicitlyEnabled(false),
488
+ enableHostedAccessAndProbe: enableHostedLocalBridgeAccessAndProbe,
489
+ activeBridgeUrl
490
+ };
491
+
492
+ async function uploadAssetToBridge(file) {
493
+ if (!isLocalBridgeAutoProbeAllowed()) {
494
+ return { success: false };
495
+ }
496
+
497
+ try {
498
+ const headers = await getBridgeAuthHeaders();
499
+ for (const bridgeUrl of getBridgeUrlCandidates()) {
500
+ const formData = new FormData();
501
+ formData.append('file', file, file.name);
502
+
503
+ const response = await fetch(`${bridgeUrl}/api/assets/upload`, {
504
+ method: 'POST',
505
+ headers,
506
+ body: formData,
507
+ targetAddressSpace: 'loopback'
508
+ });
509
+
510
+ if (!response.ok) {
511
+ continue;
512
+ }
513
+
514
+ const result = await response.json();
515
+ if (result.success) {
516
+ setActiveBridgeUrl(bridgeUrl);
517
+ return {
518
+ success: true,
519
+ url: rewriteBridgeAssetUrl(result.url, bridgeUrl), // Full HTTP URL for display
520
+ relativeUrl: result.relativeUrl, // Relative path for document storage
521
+ thumbnailUrl: rewriteBridgeAssetUrl(result.thumbnailUrl, bridgeUrl),
522
+ width: result.width,
523
+ height: result.height
524
+ };
525
+ }
526
+ }
527
+ return { success: false };
528
+ } catch (err) {
529
+ console.warn(`[DnD] Bridge upload failed: ${err.message}`);
530
+ return { success: false };
531
+ }
532
+ }
533
+
534
+ async function uploadImageToBridge(file) {
535
+ return uploadAssetToBridge(file);
536
+ }
537
+
538
+ function getDroppedItemForFile(droppedItems, file, index) {
539
+ const direct = droppedItems?.[index];
540
+ if (direct?.kind === 'file') {
541
+ const directFile = typeof direct.getAsFile === 'function' ? direct.getAsFile() : null;
542
+ if (!directFile
543
+ || directFile === file
544
+ || (directFile.name === file.name && directFile.size === file.size && directFile.lastModified === file.lastModified)) {
545
+ return direct;
546
+ }
547
+ }
548
+
549
+ for (const item of droppedItems || []) {
550
+ if (!item || item.kind !== 'file' || typeof item.getAsFile !== 'function') {
551
+ continue;
552
+ }
553
+
554
+ const itemFile = item.getAsFile();
555
+ if (itemFile
556
+ && itemFile.name === file.name
557
+ && itemFile.size === file.size
558
+ && itemFile.lastModified === file.lastModified) {
559
+ return item;
560
+ }
561
+ }
562
+
563
+ return null;
564
+ }
565
+
566
+ function getBrowserExposedLocalPath(file) {
567
+ const candidates = [
568
+ file?.path,
569
+ file?.fullPath,
570
+ file?.mozFullPath
571
+ ];
572
+
573
+ for (const candidate of candidates) {
574
+ const value = String(candidate || '').trim();
575
+ if (/^[a-z]:[\\/]/i.test(value) || value.startsWith('\\\\') || value.startsWith('/')) {
576
+ return value;
577
+ }
578
+ }
579
+
580
+ return '';
581
+ }
582
+
583
+ function rewriteBridgeLocalFileUrl(url, bridgeUrl) {
584
+ if (!url || !bridgeUrl) {
585
+ return url;
586
+ }
587
+
588
+ try {
589
+ const parsed = new URL(url);
590
+ if (!isLoopbackHost(parsed.hostname) || !parsed.pathname.startsWith('/api/local-files/')) {
591
+ return url;
592
+ }
593
+
594
+ return `${bridgeUrl}${parsed.pathname}${parsed.search}${parsed.hash}`;
595
+ } catch {
596
+ return url;
597
+ }
598
+ }
599
+
600
+ async function registerLocalFilePathWithBridge(file, localPath) {
601
+ const normalizedPath = String(localPath || '').trim();
602
+ if (!normalizedPath || !isLocalBridgeAutoProbeAllowed()) {
603
+ return { success: false };
604
+ }
605
+
606
+ try {
607
+ const headers = {
608
+ ...(await getBridgeAuthHeaders()),
609
+ 'Content-Type': 'application/json'
610
+ };
611
+
612
+ for (const bridgeUrl of getBridgeUrlCandidates()) {
613
+ const response = await fetch(`${bridgeUrl}/api/local-files/register`, {
614
+ method: 'POST',
615
+ headers,
616
+ body: JSON.stringify({
617
+ path: normalizedPath,
618
+ fileName: file?.name || '',
619
+ size: Number(file?.size || 0),
620
+ mime: file?.type || ''
621
+ }),
622
+ targetAddressSpace: 'loopback'
623
+ });
624
+
625
+ if (!response.ok) {
626
+ continue;
627
+ }
628
+
629
+ const result = await response.json();
630
+ if (result?.success && result?.url) {
631
+ setActiveBridgeUrl(bridgeUrl);
632
+ return {
633
+ success: true,
634
+ id: result.id || '',
635
+ url: rewriteBridgeLocalFileUrl(result.url, bridgeUrl),
636
+ fileName: result.fileName || file?.name || '',
637
+ size: Number(result.size || file?.size || 0),
638
+ mime: result.mime || file?.type || '',
639
+ lastModified: Number(result.lastModified || file?.lastModified || 0)
640
+ };
641
+ }
642
+ }
643
+ } catch (error) {
644
+ console.warn('[DnD] Bridge local file registration failed:', error);
645
+ }
646
+
647
+ return { success: false };
648
+ }
649
+
650
+ async function createLocalVideoReference(file, item, bridgeConnected) {
651
+ const localPath = getBrowserExposedLocalPath(file);
652
+ if (bridgeConnected && localPath) {
653
+ const bridgeResult = await registerLocalFilePathWithBridge(file, localPath);
654
+ if (bridgeResult.success) {
655
+ return {
656
+ response: bridgeResult.url,
657
+ displayUrl: bridgeResult.url,
658
+ metadata: {
659
+ assetKind: 'video',
660
+ sourceKind: 'local-path',
661
+ videoMode: 'local_bridge_stream',
662
+ storageScope: 'original_file',
663
+ persistPolicy: 'reference_only',
664
+ localBridgeFileId: bridgeResult.id,
665
+ localFilePath: localPath,
666
+ fileName: bridgeResult.fileName || file.name || '',
667
+ fileSize: String(bridgeResult.size || file.size || 0),
668
+ fileMime: bridgeResult.mime || file.type || '',
669
+ fileLastModified: String(bridgeResult.lastModified || file.lastModified || 0)
670
+ },
671
+ saved: true
672
+ };
673
+ }
674
+ }
675
+
676
+ const registry = window.MindMapLocalFiles;
677
+ const entry = registry?.registerDroppedFile
678
+ ? await registry.registerDroppedFile(file, item)
679
+ : null;
680
+ const objectUrl = entry?.objectUrl || URL.createObjectURL(file);
681
+ const reference = entry?.reference || objectUrl;
682
+ const sourceKind = entry?.sourceKind || 'session-file';
683
+ const durableHandle = entry?.durableHandle === true;
684
+
685
+ return {
686
+ response: reference,
687
+ displayUrl: objectUrl,
688
+ metadata: {
689
+ assetKind: 'video',
690
+ sourceKind,
691
+ localFileId: entry?.id || '',
692
+ localFileReference: reference,
693
+ fileName: file.name || '',
694
+ fileSize: String(file.size || 0),
695
+ fileMime: file.type || '',
696
+ fileLastModified: String(file.lastModified || 0),
697
+ videoMode: durableHandle ? 'local_handle' : 'session_blob',
698
+ storageScope: durableHandle ? 'original_file' : 'session',
699
+ persistPolicy: durableHandle ? 'reference_only' : 'session_only'
700
+ },
701
+ saved: true
702
+ };
703
+ }
704
+
705
+ function showCloudVideoDndBlockedToast(count = 1) {
706
+ const suffix = count > 1 ? ` (${count} files skipped)` : '';
707
+ window.MindMapFeedback?.toast?.(`${CLOUD_VIDEO_DND_BLOCKED_MESSAGE}${suffix}`, {
708
+ kind: 'error',
709
+ duration: 6800
710
+ });
711
+ }
712
+ // ▲▲▲ [New] ▲▲▲
713
+
714
+ function fileToDataUrl(file) {
715
+ return new Promise((resolve, reject) => {
716
+ const reader = new FileReader();
717
+ reader.onload = () => resolve(reader.result);
718
+ reader.onerror = () => reject(reader.error);
719
+ reader.readAsDataURL(file);
720
+ });
721
+ }
722
+
723
+ async function handleBrowserImageDrop(module, e, imageUrl) {
724
+ const finalUrl = await fetchImageAsBlobUrl(imageUrl);
725
+ if (!finalUrl) {
726
+ console.log('[MindMapDnD] Image drop cancelled due to CORS/Fetch error.');
727
+ return;
728
+ }
729
+
730
+ const mouse = {
731
+ x: (e.clientX / module.container.clientWidth) * 2 - 1,
732
+ y: -(e.clientY / module.container.clientHeight) * 2 + 1
733
+ };
734
+ const vfov = (module.camera.fov * Math.PI) / 180;
735
+ const viewHeight = 2 * Math.tan(vfov / 2) * module.camera.position.z;
736
+ const viewWidth = viewHeight * module.camera.aspect;
737
+ const worldX = module.camera.position.x + (mouse.x * viewWidth) / 2;
738
+ const worldY = module.camera.position.y + (mouse.y * viewHeight) / 2;
739
+
740
+ const size = await measureImageSize(finalUrl);
741
+
742
+ const centeredX = worldX - (size.width / 2);
743
+ const centeredY = worldY + (size.height / 2);
744
+
745
+ const finalNode = {
746
+ TempId: `temp_browser_${Date.now()}`,
747
+ ContentType: 'image',
748
+ FileName: 'Web Image',
749
+ Response: finalUrl,
750
+ X: module.snapToGrid(centeredX),
751
+ Y: module.snapToGridY(centeredY),
752
+ Width: size.width,
753
+ Height: size.height,
754
+ IsLoading: false
755
+ };
756
+
757
+ const truncatedUrl = finalUrl.length > 50 ? `${finalUrl.substring(0, 50)}...` : finalUrl;
758
+
759
+ try {
760
+ const actualNodeIdsMap = await module.dotNetHelper.invokeMethodAsync('AddFileNodesFromJs', [finalNode]);
761
+ const actualId = actualNodeIdsMap?.[finalNode.TempId];
762
+ if (actualId) {
763
+ const batchDetails = [{
764
+ nodeId: actualId,
765
+ fileUrl: finalUrl,
766
+ width: size.width,
767
+ height: size.height
768
+ }];
769
+ await module.dotNetHelper.invokeMethodAsync('UpdateMultipleFileNodeDetails', batchDetails);
770
+ }
771
+ } catch (err) {
772
+ console.error('[MindMapDnD] Failed to add browser image:', err);
773
+ }
774
+ }
775
+
776
+ async function handleDirectoryDrop(module, handle, e) {
777
+ console.log('[MindMapDnD] Directory dropped:', handle.name);
778
+
779
+ const mouse = {
780
+ x: (e.clientX / module.container.clientWidth) * 2 - 1,
781
+ y: -(e.clientY / module.container.clientHeight) * 2 + 1
782
+ };
783
+ const vfov = (module.camera.fov * Math.PI) / 180;
784
+ const viewHeight = 2 * Math.tan(vfov / 2) * module.camera.position.z;
785
+ const viewWidth = viewHeight * module.camera.aspect;
786
+ const worldX = module.camera.position.x + (mouse.x * viewWidth) / 2;
787
+ const worldY = module.camera.position.y + (mouse.y * viewHeight) / 2;
788
+
789
+ const centeredX = module.snapToGrid(worldX);
790
+ const centeredY = module.snapToGridY(worldY);
791
+
792
+ try {
793
+ const nodeId = await module.dotNetHelper.invokeMethodAsync('AddDirectoryNodeFromJs', handle.name, centeredX, centeredY);
794
+ if (nodeId) {
795
+ await IDBHelper.set(nodeId, handle);
796
+ console.log(`[MindMapDnD] Directory handle stored for node ${nodeId}`);
797
+ }
798
+ } catch (err) {
799
+ console.error('[MindMapDnD] Failed to add directory node:', err);
800
+ }
801
+ }
802
+
803
+ async function onDrop(module, e) {
804
+ // ▼▼▼ [Profiling] Log drag statistics ▼▼▼
805
+ const dropTime = performance.now();
806
+ const totalDragTime = _dragStats.startTime > 0 ? (dropTime - _dragStats.startTime).toFixed(0) : '?';
807
+ console.log(`[DnD] 🔴 Drop received! dragTime=${totalDragTime}ms, dragOverCount=${_dragStats.dragOverCount}`);
808
+ // ▲▲▲ [Profiling] ▲▲▲
809
+
810
+ // ▼▼▼ [Changed] Prevent duplicate processing ▼▼▼
811
+ if (e._dndProcessed) {
812
+ console.log('[MindMapDnD] Drop event already processed, skipping.');
813
+ return false;
814
+ }
815
+ e._dndProcessed = true;
816
+ // ▲▲▲ [Changed] ▲▲▲
817
+
818
+ console.log('[MindMapDnD] Drop event received!');
819
+
820
+ e.preventDefault();
821
+ e.stopPropagation();
822
+ e.stopImmediatePropagation(); // Also block other handlers on the same element
823
+
824
+ // ▼▼▼ [Critical fix] Capture file lists before starting async work ▼▼▼
825
+ // e.dataTransfer.files can be cleared by the browser once async work (await) begins.
826
+ const droppedFiles = e.dataTransfer.files ? Array.from(e.dataTransfer.files) : [];
827
+ const droppedItems = e.dataTransfer.items ? Array.from(e.dataTransfer.items) : [];
828
+ // ▲▲▲ [Critical fix] ▲▲▲
829
+
830
+ // 1. Directory handling (FileSystem API)
831
+ // (Note: droppedFiles is already copied into memory, so it's safe even with await.)
832
+ // [Optimized] If all files are already supported (image, PDF, etc.), skip directory checks
833
+ // (async handle requests) to avoid delay.
834
+ let shouldCheckDirectory = false;
835
+ if (droppedFiles.length === 0) {
836
+ // If the file list is empty (pure folder drop, etc.), we need to check
837
+ shouldCheckDirectory = true;
838
+ } else {
839
+ // If any file is not recognized (no extension, etc. => possibly a folder), check
840
+ const sortedRegistry = window.MindMapFileRegistry; // Registry is global usage
841
+ for (let droppedFileIndex = 0; droppedFileIndex < droppedFiles.length; droppedFileIndex++) {
842
+ const file = droppedFiles[droppedFileIndex];
843
+ let isKnown = false;
844
+ for (const handler of sortedRegistry) {
845
+ if (handler.check(file)) {
846
+ isKnown = true;
847
+ break;
848
+ }
849
+ }
850
+ if (!isKnown) {
851
+ shouldCheckDirectory = true;
852
+ break;
853
+ }
854
+ }
855
+ }
856
+
857
+ // ▼▼▼ [Profiling] Measure directory-check time ▼▼▼
858
+ const dirCheckStart = performance.now();
859
+
860
+ if (shouldCheckDirectory && droppedItems.length > 0) {
861
+ console.log('[DnD] 📁 Checking for directory drop...');
862
+ for (const item of droppedItems) {
863
+ // webkitGetAsEntry (sync / non-standard) vs getAsFileSystemHandle (async / standard)
864
+ // getAsFileSystemHandle can be slow, so apply a timeout
865
+ if (item.kind === 'file' && typeof item.getAsFileSystemHandle === 'function') {
866
+ try {
867
+ // ▼▼▼ [Optimization] 500ms timeout to avoid blocking directory checks ▼▼▼
868
+ const handlePromise = item.getAsFileSystemHandle();
869
+ const timeoutPromise = new Promise((_, reject) =>
870
+ setTimeout(() => reject(new Error('Timeout')), 500)
871
+ );
872
+
873
+ const handle = await Promise.race([handlePromise, timeoutPromise]);
874
+ // ▲▲▲ [Optimization] ▲▲▲
875
+
876
+ if (handle && handle.kind === 'directory') {
877
+ console.log(`[DnD] 📁 Directory detected in ${(performance.now() - dirCheckStart).toFixed(0)}ms`);
878
+ await handleDirectoryDrop(module, handle, e);
879
+ return false;
880
+ }
881
+ } catch (err) {
882
+ // If timeout/failure, skip quietly and continue as file handling
883
+ console.warn(`[DnD] Directory check skipped: ${err.message}`);
884
+ }
885
+ }
886
+ }
887
+ }
888
+ console.log(`[DnD] 📁 Directory check completed in ${(performance.now() - dirCheckStart).toFixed(0)}ms`);
889
+ // ▲▲▲ [Profiling] ▲▲▲
890
+
891
+ // 2. Regular file handling (use captured droppedFiles)
892
+ if (droppedFiles.length > 0) {
893
+ const timeLabel = `[DnD] Process_${Date.now()}`;
894
+ console.time(timeLabel);
895
+
896
+ // Debug: Log all dropped files
897
+ console.log(`[DnD] 📂 Dropped ${droppedFiles.length} files:`);
898
+ droppedFiles.forEach((f, i) => console.log(` [${i}] ${f.name} (type: ${f.type || 'unknown'}, size: ${f.size})`));
899
+
900
+ const matchedFiles = [];
901
+ const filesToProcess = [];
902
+ const sortedRegistry = [...window.MindMapFileRegistry].sort((a, b) => (a.priority ?? 50) - (b.priority ?? 50));
903
+
904
+ for (let droppedFileIndex = 0; droppedFileIndex < droppedFiles.length; droppedFileIndex++) {
905
+ const file = droppedFiles[droppedFileIndex];
906
+ let matchedType = null;
907
+ for (const handler of sortedRegistry) {
908
+ if (handler.check(file)) {
909
+ matchedType = handler.getType(file);
910
+ break;
911
+ }
912
+ }
913
+ if (matchedType) {
914
+ matchedFiles.push({
915
+ file,
916
+ type: matchedType,
917
+ item: getDroppedItemForFile(droppedItems, file, droppedFileIndex)
918
+ });
919
+ } else {
920
+ console.warn(`[DnD] ⚠️ No handler matched for: ${file.name} (type: ${file.type})`);
921
+ }
922
+ }
923
+
924
+ const bridgeConnected = !window.__isNativeApp && await isBridgeConnected();
925
+ let blockedVideoCount = 0;
926
+ for (const entry of matchedFiles) {
927
+ if (entry.type === 'video' && !window.__isNativeApp && !bridgeConnected) {
928
+ blockedVideoCount++;
929
+ console.warn(`[DnD] Blocked local video DnD in cloud mode: ${entry.file.name}`);
930
+ continue;
931
+ }
932
+
933
+ filesToProcess.push(entry);
934
+ }
935
+
936
+ if (blockedVideoCount > 0) {
937
+ showCloudVideoDndBlockedToast(blockedVideoCount);
938
+ }
939
+
940
+ if (filesToProcess.length === 0) {
941
+ console.timeEnd(timeLabel);
942
+ console.log('[MindMapDnD] No matching file handlers found. Checking for web images...');
943
+ } else {
944
+ const mouse = {
945
+ x: (e.clientX / module.container.clientWidth) * 2 - 1,
946
+ y: -(e.clientY / module.container.clientHeight) * 2 + 1
947
+ };
948
+ const vfov = (module.camera.fov * Math.PI) / 180;
949
+ const viewHeight = 2 * Math.tan(vfov / 2) * module.camera.position.z;
950
+ const viewWidth = viewHeight * module.camera.aspect;
951
+ const worldX = module.camera.position.x + (mouse.x * viewWidth) / 2;
952
+ const worldY = module.camera.position.y + (mouse.y * viewHeight) / 2;
953
+
954
+ // ▼▼▼ [Optimization] Batch processing to prevent UI blocking ▼▼▼
955
+ const BATCH_SIZE = 5; // Process 5 files at a time
956
+ const preparedNodes = [];
957
+
958
+ // Helper to yield to main thread
959
+ const yieldToMain = () => new Promise(resolve => setTimeout(resolve, 0));
960
+
961
+ // ▼▼▼ [Fix] Track cumulative X offset for proper spacing ▼▼▼
962
+ const NODE_GAP = 30;
963
+ // ▲▲▲ [Fix] ▲▲▲
964
+
965
+ // Process files in batches
966
+ for (let batchStart = 0; batchStart < filesToProcess.length; batchStart += BATCH_SIZE) {
967
+ const batchEnd = Math.min(batchStart + BATCH_SIZE, filesToProcess.length);
968
+ const batch = filesToProcess.slice(batchStart, batchEnd);
969
+
970
+ const batchResults = await Promise.all(batch.map(async ({ file, type, item }, batchIndex) => {
971
+ const index = batchStart + batchIndex;
972
+ const tempId = `temp_${Date.now()}_${index}`;
973
+
974
+ let finalResponse = '';
975
+ let displayUrl = ''; // For JS image loading
976
+ let finalWidth = module.aiNodeDefaultWidth || 470;
977
+ let finalHeight = module.aiNodeDefaultHeight || 350;
978
+ let isLoading = false;
979
+ const fileIndex = index;
980
+ let imageSaved = false; // Track if image was already saved
981
+ let videoSaved = false;
982
+ let metadata = null;
983
+
984
+ if (type === 'image') {
985
+ const isAnimatedGif = file.type === 'image/gif' || /\.gif$/i.test(file.name || '');
986
+ if (isAnimatedGif) {
987
+ metadata = {
988
+ assetKind: 'image',
989
+ fileName: file.name || '',
990
+ fileSize: String(file.size || 0),
991
+ fileMime: file.type || 'image/gif',
992
+ fileLastModified: String(file.lastModified || ''),
993
+ isAnimatedGif: 'true'
994
+ };
995
+ }
996
+ // ▼▼▼ [Optimization] Direct Bridge upload (fastest path) ▼▼▼
997
+ console.log(`[DnD] Processing image ${index + 1}: ${file.name}`);
998
+
999
+ // Try Bridge direct upload first (bypasses C# entirely)
1000
+ if (bridgeConnected) {
1001
+ const localSizingUrl = URL.createObjectURL(file);
1002
+ const measuredSizePromise = measureImageSize(localSizingUrl).catch((err) => {
1003
+ console.warn(`[DnD] Image ${index + 1} local measure failed: ${err?.message || err}`);
1004
+ return null;
1005
+ });
1006
+
1007
+ const bridgeResult = await uploadImageToBridge(file);
1008
+ const measuredSize = await measuredSizePromise;
1009
+ if (bridgeResult.success) {
1010
+ displayUrl = bridgeResult.url; // HTTP URL for display
1011
+ finalResponse = bridgeResult.relativeUrl; // Relative path for storage
1012
+ if (isAnimatedGif && metadata) {
1013
+ metadata.OriginalPath = bridgeResult.relativeUrl || '';
1014
+ }
1015
+
1016
+ const imageNodeSize = measuredSize || computeDroppedImageNodeSize(
1017
+ bridgeResult.width || 0,
1018
+ bridgeResult.height || 0
1019
+ );
1020
+ finalWidth = imageNodeSize.width;
1021
+ finalHeight = imageNodeSize.height;
1022
+
1023
+ URL.revokeObjectURL(localSizingUrl);
1024
+ imageSaved = true; // Already saved by Bridge
1025
+ console.log(`[DnD] ✓ Bridge saved: ${file.name} -> ${bridgeResult.relativeUrl} (${finalWidth}x${finalHeight})`);
1026
+ } else {
1027
+ // Fallback to Blob URL + background save
1028
+ const size = measuredSize || computeDroppedImageNodeSize(470, 350);
1029
+ displayUrl = localSizingUrl;
1030
+ finalResponse = localSizingUrl;
1031
+ finalWidth = size.width;
1032
+ finalHeight = size.height;
1033
+ imageSaved = false;
1034
+ }
1035
+ } else {
1036
+ // MAUI/Native: use Blob URL + background save via C#
1037
+ try {
1038
+ const sizingUrl = URL.createObjectURL(file);
1039
+ const size = await measureImageSize(sizingUrl);
1040
+ displayUrl = sizingUrl;
1041
+ finalResponse = sizingUrl;
1042
+ finalWidth = size.width;
1043
+ finalHeight = size.height;
1044
+ imageSaved = false;
1045
+ } catch (err) {
1046
+ console.warn(`[DnD] Image ${index + 1} measure failed: ${err.message}`);
1047
+ displayUrl = URL.createObjectURL(file);
1048
+ finalResponse = displayUrl;
1049
+ const fallbackSize = computeDroppedImageNodeSize(470, 350);
1050
+ finalWidth = fallbackSize.width;
1051
+ finalHeight = fallbackSize.height;
1052
+ imageSaved = false;
1053
+ }
1054
+ }
1055
+ // ▲▲▲ [Optimization] ▲▲▲
1056
+ isLoading = false;
1057
+ } else if (type === 'video') {
1058
+ console.log(`[DnD] Processing video ${index + 1}: ${file.name}`);
1059
+ finalWidth = 470;
1060
+ finalHeight = 264;
1061
+ const localReference = await createLocalVideoReference(file, item, bridgeConnected);
1062
+ displayUrl = localReference.displayUrl;
1063
+ finalResponse = localReference.response;
1064
+ metadata = localReference.metadata;
1065
+ videoSaved = localReference.saved === true;
1066
+
1067
+ // Legacy asset-copy path is intentionally unreachable for videos.
1068
+ if (false && bridgeConnected) {
1069
+ const bridgeResult = await uploadAssetToBridge(file);
1070
+ if (bridgeResult.success) {
1071
+ displayUrl = bridgeResult.url;
1072
+ finalResponse = bridgeResult.relativeUrl;
1073
+ videoSaved = true;
1074
+ console.log(`[DnD] ✓ Bridge saved video: ${file.name} -> ${bridgeResult.relativeUrl}`);
1075
+ }
1076
+ }
1077
+
1078
+ if (!videoSaved) {
1079
+ displayUrl = window.__isNativeApp ? await fileToDataUrl(file) : URL.createObjectURL(file);
1080
+ finalResponse = displayUrl;
1081
+ }
1082
+
1083
+ isLoading = false;
1084
+ } else if (type === 'text') {
1085
+ finalResponse = '⏳ Analyzing file...';
1086
+ displayUrl = finalResponse;
1087
+ isLoading = true;
1088
+ } else if (type === 'code') {
1089
+ // ▼▼▼ [New] Code files: Troika-rendered, load content immediately ▼▼▼
1090
+ finalResponse = '⏳ Loading source code...';
1091
+ displayUrl = finalResponse;
1092
+ finalWidth = module.aiNodeDefaultWidth || 470;
1093
+ finalHeight = 400; // Taller default for code
1094
+ isLoading = true;
1095
+ // ▲▲▲ [New] ▲▲▲
1096
+ } else if (type === 'pdf') {
1097
+ finalResponse = window.__isNativeApp ? await fileToDataUrl(file) : URL.createObjectURL(file);
1098
+ displayUrl = finalResponse;
1099
+ finalWidth = module.aiNodeDefaultWidth || 470;
1100
+ finalHeight = 56;
1101
+ }
1102
+
1103
+ // X, Y will be recalculated after all dimensions are known
1104
+ const tempX = 0;
1105
+ const tempY = 0;
1106
+
1107
+ return {
1108
+ TempId: tempId,
1109
+ ContentType: type,
1110
+ FileName: file.name,
1111
+ Response: finalResponse, // For C# storage
1112
+ DisplayUrl: displayUrl, // For JS display
1113
+ FileIndex: fileIndex,
1114
+ X: tempX,
1115
+ Y: tempY,
1116
+ Width: finalWidth,
1117
+ Height: finalHeight,
1118
+ IsLoading: isLoading,
1119
+ ImageSaved: imageSaved, // Flag to skip background save
1120
+ VideoSaved: videoSaved,
1121
+ Metadata: metadata
1122
+ };
1123
+ }));
1124
+
1125
+ preparedNodes.push(...batchResults.filter(n => n !== null));
1126
+
1127
+ // Yield to main thread after each batch to prevent UI freeze
1128
+ if (batchEnd < filesToProcess.length) {
1129
+ await yieldToMain();
1130
+ }
1131
+
1132
+ console.log(`[DnD] Prepared batch ${Math.floor(batchStart / BATCH_SIZE) + 1}/${Math.ceil(filesToProcess.length / BATCH_SIZE)}`);
1133
+ }
1134
+ // ▲▲▲ [Optimization] ▲▲▲
1135
+
1136
+ const nodesToSend = preparedNodes.filter(n => n !== null);
1137
+
1138
+ // ▼▼▼ [Fix] Recalculate X positions based on node widths + gap ▼▼▼
1139
+ if (nodesToSend.length > 0) {
1140
+ let nextLeftX = worldX - (nodesToSend[0].Width / 2);
1141
+ for (const node of nodesToSend) {
1142
+ const centeredY = worldY + (node.Height / 2);
1143
+ node.X = module.snapToGrid(nextLeftX);
1144
+ node.Y = module.snapToGridY(centeredY);
1145
+ nextLeftX = node.X + node.Width + NODE_GAP;
1146
+ }
1147
+ }
1148
+ // ▲▲▲ [Fix] ▲▲▲
1149
+
1150
+ if (nodesToSend.length > 0) {
1151
+ // 1. Get ID mapping from C# (batch call)
1152
+ const t4 = performance.now();
1153
+ const actualNodeIdsMap = await module.dotNetHelper.invokeMethodAsync('AddFileNodesFromJs', nodesToSend);
1154
+ const t5 = performance.now();
1155
+ console.log(`[DnD] AddFileNodesFromJs: ${(t5 - t4).toFixed(1)}ms for ${nodesToSend.length} nodes`);
1156
+
1157
+ // ▼▼▼ [Optimization] Add nodes to JS in batches to prevent UI blocking ▼▼▼
1158
+ const nodesToAdd = nodesToSend.map(dto => ({
1159
+ id: actualNodeIdsMap[dto.TempId],
1160
+ contentType: dto.ContentType,
1161
+ prompt: dto.FileName,
1162
+ response: dto.DisplayUrl || dto.Response, // Use DisplayUrl for images (HTTP URL)
1163
+ positionX: dto.X,
1164
+ positionY: dto.Y,
1165
+ positionZ: 0,
1166
+ width: dto.Width,
1167
+ height: dto.Height,
1168
+ isLoading: false,
1169
+ metadata: dto.Metadata || {}
1170
+ }));
1171
+
1172
+ const JS_BATCH_SIZE = 10;
1173
+ const t5b = performance.now();
1174
+ for (let i = 0; i < nodesToAdd.length; i += JS_BATCH_SIZE) {
1175
+ const batch = nodesToAdd.slice(i, i + JS_BATCH_SIZE);
1176
+ await window.mindMap.addNodes(batch);
1177
+
1178
+ // Yield to main thread between batches
1179
+ if (i + JS_BATCH_SIZE < nodesToAdd.length) {
1180
+ await yieldToMain();
1181
+ }
1182
+ }
1183
+ const t5c = performance.now();
1184
+ console.log(`[DnD] JS addNodes: ${(t5c - t5b).toFixed(1)}ms for ${nodesToAdd.length} nodes`);
1185
+ // ▲▲▲ [Optimization] ▲▲▲
1186
+
1187
+ // 2. Stream text files (BATCH PROCESSING to prevent Render Storm)
1188
+ // ▼▼▼ [Optimization] Batch text file processing ▼▼▼
1189
+ const textNodesToProcess = nodesToSend.filter(n => n.IsLoading && n.ContentType !== 'image');
1190
+
1191
+ if (textNodesToProcess.length > 0) {
1192
+ console.log(`[DnD] 📄 Batch processing ${textNodesToProcess.length} text files...`);
1193
+
1194
+ // Prepare all stream references at once
1195
+ const textFileStreams = [];
1196
+ for (const nodeDto of textNodesToProcess) {
1197
+ const realNodeId = actualNodeIdsMap?.[nodeDto.TempId];
1198
+ if (!realNodeId) continue;
1199
+
1200
+ const originalEntry = filesToProcess[nodeDto.FileIndex];
1201
+ if (!originalEntry) continue;
1202
+
1203
+ try {
1204
+ const streamRef = DotNet.createJSStreamReference(originalEntry.file);
1205
+ textFileStreams.push({
1206
+ nodeId: realNodeId,
1207
+ fileName: nodeDto.FileName,
1208
+ streamRef: streamRef
1209
+ });
1210
+ } catch (err) {
1211
+ console.error(`[DnD] Failed to create stream ref for ${nodeDto.FileName}:`, err);
1212
+ }
1213
+ }
1214
+
1215
+ // Single batch call to C# (prevents N individual StateHasChanged calls)
1216
+ if (textFileStreams.length > 0) {
1217
+ try {
1218
+ const t6a = performance.now();
1219
+ await module.dotNetHelper.invokeMethodAsync(
1220
+ 'IngestMultipleTextFilesDirectly',
1221
+ textFileStreams
1222
+ );
1223
+ const t6b = performance.now();
1224
+ console.log(`[DnD] ✅ Batch text processing: ${(t6b - t6a).toFixed(1)}ms for ${textFileStreams.length} files`);
1225
+ } catch (err) {
1226
+ console.error('[DnD] Batch text processing failed, falling back to individual:', err);
1227
+ // Fallback to individual processing
1228
+ for (const stream of textFileStreams) {
1229
+ try {
1230
+ await module.dotNetHelper.invokeMethodAsync(
1231
+ 'IngestFileStreamDirectly',
1232
+ stream.streamRef,
1233
+ stream.nodeId,
1234
+ stream.fileName
1235
+ );
1236
+ } catch (e) {
1237
+ console.error(`[DnD] Individual stream error for ${stream.fileName}:`, e);
1238
+ }
1239
+ }
1240
+ }
1241
+ }
1242
+ }
1243
+ // ▲▲▲ [Optimization] ▲▲▲
1244
+
1245
+ // ▼▼▼ [Optimization] Save images in background (Blob URL displayed immediately) ▼▼▼
1246
+ const imageNodesToSave = nodesToSend.filter(n => n.ContentType === 'image' && !n.ImageSaved);
1247
+ if (imageNodesToSave.length > 0) {
1248
+ console.log(`[DnD] 🔄 Starting background save for ${imageNodesToSave.length} images...`);
1249
+ // Fire-and-forget: start sequential image saving in background
1250
+ (async () => {
1251
+ for (let i = 0; i < imageNodesToSave.length; i++) {
1252
+ const nodeDto = imageNodesToSave[i];
1253
+ const realNodeId = actualNodeIdsMap?.[nodeDto.TempId];
1254
+ if (!realNodeId) continue;
1255
+
1256
+ const originalEntry = filesToProcess[nodeDto.FileIndex];
1257
+ if (!originalEntry) continue;
1258
+
1259
+ try {
1260
+ const streamRef = DotNet.createJSStreamReference(originalEntry.file);
1261
+ await module.dotNetHelper.invokeMethodAsync(
1262
+ 'SaveImageStreamBackground',
1263
+ streamRef,
1264
+ realNodeId,
1265
+ nodeDto.FileName
1266
+ );
1267
+
1268
+ if ((i + 1) % 10 === 0) {
1269
+ console.log(`[DnD] Fallback saved ${i + 1}/${imageNodesToSave.length} images`);
1270
+ }
1271
+ } catch (err) {
1272
+ console.warn('[DnD] Image save error:', err);
1273
+ }
1274
+ }
1275
+ console.log(`[DnD] ✅ Background saved ${imageNodesToSave.length} images to assets`);
1276
+ })();
1277
+ } else {
1278
+ const totalImages = nodesToSend.filter(n => n.ContentType === 'image').length;
1279
+ if (totalImages > 0) {
1280
+ console.log(`[DnD] ✅ All ${totalImages} images already saved via SaveImageAndGetInfo`);
1281
+ }
1282
+ }
1283
+ // ▲▲▲ [Optimization] ▲▲▲
1284
+
1285
+ const videoNodesToSave = nodesToSend.filter(n => n.ContentType === 'video' && !n.VideoSaved);
1286
+ if (videoNodesToSave.length > 0) {
1287
+ console.log(`[DnD] 🔄 Starting background save for ${videoNodesToSave.length} videos...`);
1288
+ (async () => {
1289
+ for (let i = 0; i < videoNodesToSave.length; i++) {
1290
+ const nodeDto = videoNodesToSave[i];
1291
+ const realNodeId = actualNodeIdsMap?.[nodeDto.TempId];
1292
+ if (!realNodeId) continue;
1293
+
1294
+ const originalEntry = filesToProcess[nodeDto.FileIndex];
1295
+ if (!originalEntry) continue;
1296
+
1297
+ try {
1298
+ const streamRef = DotNet.createJSStreamReference(originalEntry.file);
1299
+ await module.dotNetHelper.invokeMethodAsync(
1300
+ 'SaveVideoStreamBackground',
1301
+ streamRef,
1302
+ realNodeId,
1303
+ nodeDto.FileName
1304
+ );
1305
+ } catch (err) {
1306
+ console.warn('[DnD] Video save error:', err);
1307
+ }
1308
+ }
1309
+ console.log(`[DnD] ✅ Background saved ${videoNodesToSave.length} videos to assets`);
1310
+ })();
1311
+ }
1312
+
1313
+ // 3. Update PDF URL only (images were already added in JS)
1314
+ // ▼▼▼ [Changed] Images already handled in JS addNodes; update PDFs only ▼▼▼
1315
+ const batchDetails = nodesToSend
1316
+ .map(node => {
1317
+ const nodeId = actualNodeIdsMap?.[node.TempId];
1318
+ if (!nodeId) return null;
1319
+ // Images and text have already been handled
1320
+ if (node.ContentType === 'image') return null;
1321
+ if (node.ContentType === 'video') return null;
1322
+ if (node.IsLoading) return null; // text still loading
1323
+
1324
+ return {
1325
+ nodeId: nodeId,
1326
+ fileUrl: node.Response,
1327
+ width: node.Width,
1328
+ height: node.Height
1329
+ };
1330
+ })
1331
+ .filter(detail => detail !== null);
1332
+
1333
+ // Execute all work in parallel
1334
+ // ▼▼▼ [Profiling] Post-processing time ▼▼▼
1335
+ const t6 = performance.now();
1336
+ await Promise.all([
1337
+ batchDetails.length > 0 ? module.dotNetHelper.invokeMethodAsync('UpdateMultipleFileNodeDetails', batchDetails) : Promise.resolve()
1338
+ ]);
1339
+ const t7 = performance.now();
1340
+ console.log(`[DnD] UpdateMultipleFileNodeDetails: ${(t7 - t6).toFixed(1)}ms`);
1341
+ // ▲▲▲ [Profiling] ▲▲▲
1342
+ }
1343
+
1344
+ console.timeEnd(timeLabel);
1345
+ return false;
1346
+ }
1347
+ }
1348
+
1349
+ // 3. Web resource (URL) handling
1350
+ const imageUrl = extractImageUrl(e.dataTransfer);
1351
+ console.log(`[DnD] extractImageUrl result: ${imageUrl ? imageUrl.substring(0, 50) + '...' : 'null'}`);
1352
+ const isWebResource = imageUrl && (imageUrl.startsWith('http') || imageUrl.startsWith('data:'));
1353
+ if (isWebResource) {
1354
+ console.log('[DnD] Processing as web resource (should NOT happen if file was processed)');
1355
+ await handleBrowserImageDrop(module, e, imageUrl);
1356
+ return false;
1357
+ }
1358
+
1359
+ const droppedUrl = extractDroppedUrl(e.dataTransfer);
1360
+ if (droppedUrl && /^https?:\/\//i.test(droppedUrl)) {
1361
+ const mouse = {
1362
+ x: (e.clientX / module.container.clientWidth) * 2 - 1,
1363
+ y: -(e.clientY / module.container.clientHeight) * 2 + 1
1364
+ };
1365
+ const vfov = (module.camera.fov * Math.PI) / 180;
1366
+ const viewHeight = 2 * Math.tan(vfov / 2) * module.camera.position.z;
1367
+ const viewWidth = viewHeight * module.camera.aspect;
1368
+ const worldX = module.camera.position.x + (mouse.x * viewWidth) / 2;
1369
+ const worldY = module.camera.position.y + (mouse.y * viewHeight) / 2;
1370
+
1371
+ await module.dotNetHelper?.invokeMethodAsync(
1372
+ 'AddUrlNodeFromJs',
1373
+ droppedUrl,
1374
+ module.snapToGrid(worldX - 230),
1375
+ module.snapToGridY(worldY + 130)
1376
+ );
1377
+ return false;
1378
+ }
1379
+
1380
+ // ▼▼▼ [Profiling] Drop handling complete ▼▼▼
1381
+ const totalProcessTime = performance.now() - dropTime;
1382
+ console.log(`[DnD] ✅ Drop processing completed in ${totalProcessTime.toFixed(0)}ms`);
1383
+ // ▲▲▲ [Profiling] ▲▲▲
1384
+
1385
+ return false;
1386
+ }
1387
+
1388
+ return {
1389
+ addEventListeners: function (moduleInstance) {
1390
+ // ▼▼▼ [Changed] Prevent duplicate registration ▼▼▼
1391
+ if (moduleInstance._dndListenersAttached) {
1392
+ console.log('[MindMapDnD] Event listeners already attached, skipping.');
1393
+ return;
1394
+ }
1395
+ // ▲▲▲ [Changed] ▲▲▲
1396
+
1397
+ // ▼▼▼ [New] Disable JS drag listeners in MAUI native app ▼▼▼
1398
+ // C# NativeDropService handles it to avoid IPC bottlenecks and duplicated UI.
1399
+ if (window.__isNativeApp === true) {
1400
+ console.log('[MindMapDnD] Native app detected - JS drag handlers DISABLED (C# handles drop).');
1401
+ moduleInstance._dndListenersAttached = true;
1402
+ return;
1403
+ }
1404
+ // ▲▲▲ [New] ▲▲▲
1405
+
1406
+ const handlers = getBoundHandlers(moduleInstance);
1407
+ if (moduleInstance.container) {
1408
+ moduleInstance.container.addEventListener('dragenter', handlers.boundOnDragEnter); // [New]
1409
+ moduleInstance.container.addEventListener('dragover', handlers.boundOnDragOver);
1410
+ moduleInstance.container.addEventListener('dragleave', handlers.boundOnDragLeave);
1411
+ moduleInstance.container.addEventListener('drop', handlers.boundOnDrop);
1412
+ moduleInstance._dndListenersAttached = true;
1413
+ console.log('[MindMapDnD] Event listeners attached to container.');
1414
+ }
1415
+ },
1416
+ removeEventListeners: function (moduleInstance) {
1417
+ const handlers = getBoundHandlers(moduleInstance);
1418
+ if (moduleInstance.container) {
1419
+ moduleInstance.container.removeEventListener('dragenter', handlers.boundOnDragEnter); // [New]
1420
+ moduleInstance.container.removeEventListener('dragover', handlers.boundOnDragOver);
1421
+ moduleInstance.container.removeEventListener('dragleave', handlers.boundOnDragLeave);
1422
+ moduleInstance.container.removeEventListener('drop', handlers.boundOnDrop);
1423
+ }
1424
+ moduleInstance._dndListenersAttached = false;
1425
+ eventHandlers.delete(moduleInstance);
1426
+ },
1427
+ IDBHelper: IDBHelper
1428
+ };
1429
+ })();
1430
+ console.log('✓ mind-map-dnd.js loaded (Fixed: Async DataTransfer Safety + Text Support).');