@nuraly/lumenjs 0.1.3 → 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 (306) hide show
  1. package/README.md +62 -282
  2. package/dist/auth/config.d.ts +23 -0
  3. package/dist/auth/config.js +115 -0
  4. package/dist/auth/guard.d.ts +12 -0
  5. package/dist/auth/guard.js +28 -0
  6. package/dist/auth/index.d.ts +3 -0
  7. package/dist/auth/index.js +1 -0
  8. package/dist/auth/middleware.d.ts +23 -0
  9. package/dist/auth/middleware.js +89 -0
  10. package/dist/auth/native-auth.d.ts +82 -0
  11. package/dist/auth/native-auth.js +340 -0
  12. package/dist/auth/oidc-client.d.ts +17 -0
  13. package/dist/auth/oidc-client.js +123 -0
  14. package/dist/auth/providers/google.d.ts +23 -0
  15. package/dist/auth/providers/google.js +25 -0
  16. package/dist/auth/providers/index.d.ts +2 -0
  17. package/dist/auth/providers/index.js +1 -0
  18. package/dist/auth/routes/login.d.ts +8 -0
  19. package/dist/auth/routes/login.js +121 -0
  20. package/dist/auth/routes/logout.d.ts +4 -0
  21. package/dist/auth/routes/logout.js +79 -0
  22. package/dist/auth/routes/oidc-callback.d.ts +3 -0
  23. package/dist/auth/routes/oidc-callback.js +70 -0
  24. package/dist/auth/routes/password.d.ts +5 -0
  25. package/dist/auth/routes/password.js +149 -0
  26. package/dist/auth/routes/signup.d.ts +3 -0
  27. package/dist/auth/routes/signup.js +81 -0
  28. package/dist/auth/routes/token.d.ts +4 -0
  29. package/dist/auth/routes/token.js +70 -0
  30. package/dist/auth/routes/totp.d.ts +22 -0
  31. package/dist/auth/routes/totp.js +232 -0
  32. package/dist/auth/routes/utils.d.ts +7 -0
  33. package/dist/auth/routes/utils.js +35 -0
  34. package/dist/auth/routes/verify.d.ts +3 -0
  35. package/dist/auth/routes/verify.js +26 -0
  36. package/dist/auth/routes.d.ts +8 -0
  37. package/dist/auth/routes.js +124 -0
  38. package/dist/auth/session.d.ts +8 -0
  39. package/dist/auth/session.js +54 -0
  40. package/dist/auth/token.d.ts +33 -0
  41. package/dist/auth/token.js +90 -0
  42. package/dist/auth/types.d.ts +156 -0
  43. package/dist/auth/types.js +2 -0
  44. package/dist/build/build-client.d.ts +15 -0
  45. package/dist/build/build-client.js +45 -0
  46. package/dist/build/build-prerender.d.ts +11 -0
  47. package/dist/build/build-prerender.js +159 -0
  48. package/dist/build/build-server.d.ts +18 -0
  49. package/dist/build/build-server.js +107 -0
  50. package/dist/build/build.js +60 -123
  51. package/dist/build/scan.d.ts +18 -0
  52. package/dist/build/scan.js +77 -6
  53. package/dist/build/serve-api.js +8 -2
  54. package/dist/build/serve-loaders.d.ts +4 -4
  55. package/dist/build/serve-loaders.js +26 -18
  56. package/dist/build/serve-ssr.js +38 -11
  57. package/dist/build/serve-static.js +3 -3
  58. package/dist/build/serve.js +341 -18
  59. package/dist/cli.js +37 -6
  60. package/dist/communication/encryption.d.ts +35 -0
  61. package/dist/communication/encryption.js +90 -0
  62. package/dist/communication/handlers/context.d.ts +27 -0
  63. package/dist/communication/handlers/context.js +1 -0
  64. package/dist/communication/handlers/conversation.d.ts +24 -0
  65. package/dist/communication/handlers/conversation.js +113 -0
  66. package/dist/communication/handlers/file-upload.d.ts +17 -0
  67. package/dist/communication/handlers/file-upload.js +62 -0
  68. package/dist/communication/handlers/messaging.d.ts +30 -0
  69. package/dist/communication/handlers/messaging.js +237 -0
  70. package/dist/communication/handlers/presence.d.ts +15 -0
  71. package/dist/communication/handlers/presence.js +76 -0
  72. package/dist/communication/handlers.d.ts +5 -0
  73. package/dist/communication/handlers.js +5 -0
  74. package/dist/communication/index.d.ts +9 -0
  75. package/dist/communication/index.js +7 -0
  76. package/dist/communication/link-preview.d.ts +18 -0
  77. package/dist/communication/link-preview.js +115 -0
  78. package/dist/communication/schema.d.ts +10 -0
  79. package/dist/communication/schema.js +101 -0
  80. package/dist/communication/server.d.ts +86 -0
  81. package/dist/communication/server.js +212 -0
  82. package/dist/communication/signaling.d.ts +43 -0
  83. package/dist/communication/signaling.js +271 -0
  84. package/dist/communication/store.d.ts +71 -0
  85. package/dist/communication/store.js +289 -0
  86. package/dist/communication/types.d.ts +454 -0
  87. package/dist/communication/types.js +1 -0
  88. package/dist/create.d.ts +1 -0
  89. package/dist/create.js +55 -0
  90. package/dist/db/auto-migrate.d.ts +3 -0
  91. package/dist/db/auto-migrate.js +100 -0
  92. package/dist/db/client.d.ts +3 -0
  93. package/dist/db/client.js +18 -0
  94. package/dist/db/index.d.ts +17 -13
  95. package/dist/db/index.js +205 -26
  96. package/dist/db/seed.d.ts +12 -0
  97. package/dist/db/seed.js +88 -0
  98. package/dist/db/table.d.ts +10 -0
  99. package/dist/db/table.js +12 -0
  100. package/dist/dev-server/config.d.ts +11 -0
  101. package/dist/dev-server/config.js +40 -20
  102. package/dist/dev-server/index-html.d.ts +4 -0
  103. package/dist/dev-server/index-html.js +21 -6
  104. package/dist/dev-server/nuralyui-aliases.d.ts +0 -4
  105. package/dist/dev-server/nuralyui-aliases.js +115 -94
  106. package/dist/dev-server/plugins/vite-plugin-api-routes.js +29 -5
  107. package/dist/dev-server/plugins/vite-plugin-auth.d.ts +6 -0
  108. package/dist/dev-server/plugins/vite-plugin-auth.js +223 -0
  109. package/dist/dev-server/plugins/vite-plugin-auto-define.d.ts +16 -0
  110. package/dist/dev-server/plugins/vite-plugin-auto-define.js +111 -0
  111. package/dist/dev-server/plugins/vite-plugin-communication.d.ts +6 -0
  112. package/dist/dev-server/plugins/vite-plugin-communication.js +205 -0
  113. package/dist/dev-server/plugins/vite-plugin-editor-api.d.ts +6 -0
  114. package/dist/dev-server/plugins/vite-plugin-editor-api.js +318 -0
  115. package/dist/dev-server/plugins/vite-plugin-i18n.js +69 -2
  116. package/dist/dev-server/plugins/vite-plugin-lit-dedup.d.ts +6 -0
  117. package/dist/dev-server/plugins/vite-plugin-lit-dedup.js +78 -34
  118. package/dist/dev-server/plugins/vite-plugin-lit-hmr.js +44 -2
  119. package/dist/dev-server/plugins/vite-plugin-llms.d.ts +2 -0
  120. package/dist/dev-server/plugins/vite-plugin-llms.js +92 -0
  121. package/dist/dev-server/plugins/vite-plugin-loaders.js +146 -13
  122. package/dist/dev-server/plugins/vite-plugin-routes.js +16 -5
  123. package/dist/dev-server/plugins/vite-plugin-socketio.d.ts +2 -0
  124. package/dist/dev-server/plugins/vite-plugin-socketio.js +51 -0
  125. package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +2 -0
  126. package/dist/dev-server/plugins/vite-plugin-source-annotator.js +26 -3
  127. package/dist/dev-server/plugins/vite-plugin-storage.d.ts +10 -0
  128. package/dist/dev-server/plugins/vite-plugin-storage.js +126 -0
  129. package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +140 -3
  130. package/dist/dev-server/server.js +242 -70
  131. package/dist/dev-server/ssr-render.d.ts +2 -1
  132. package/dist/dev-server/ssr-render.js +117 -50
  133. package/dist/editor/ai/backend.d.ts +20 -0
  134. package/dist/editor/ai/backend.js +113 -0
  135. package/dist/editor/ai/claude-code-client.d.ts +20 -0
  136. package/dist/editor/ai/claude-code-client.js +145 -0
  137. package/dist/editor/ai/deepseek-client.d.ts +7 -0
  138. package/dist/editor/ai/deepseek-client.js +113 -0
  139. package/dist/editor/ai/opencode-client.d.ts +14 -0
  140. package/dist/editor/ai/opencode-client.js +99 -0
  141. package/dist/editor/ai/snapshot-store.d.ts +22 -0
  142. package/dist/editor/ai/snapshot-store.js +35 -0
  143. package/dist/editor/ai/types.d.ts +30 -0
  144. package/dist/editor/ai/types.js +136 -0
  145. package/dist/editor/ai-chat-panel.d.ts +13 -0
  146. package/dist/editor/ai-chat-panel.js +613 -0
  147. package/dist/editor/ai-markdown.d.ts +10 -0
  148. package/dist/editor/ai-markdown.js +70 -0
  149. package/dist/editor/ai-project-panel.d.ts +11 -0
  150. package/dist/editor/ai-project-panel.js +332 -0
  151. package/dist/editor/ast-modification.d.ts +11 -0
  152. package/dist/editor/ast-modification.js +1 -0
  153. package/dist/editor/ast-service.d.ts +30 -0
  154. package/dist/editor/ast-service.js +180 -0
  155. package/dist/editor/css-rules.d.ts +54 -0
  156. package/dist/editor/css-rules.js +423 -0
  157. package/dist/editor/editor-api-client.d.ts +51 -0
  158. package/dist/editor/editor-api-client.js +162 -0
  159. package/dist/editor/editor-bridge.d.ts +1 -0
  160. package/dist/editor/editor-bridge.js +18 -8
  161. package/dist/editor/editor-toolbar.d.ts +14 -0
  162. package/dist/editor/editor-toolbar.js +115 -0
  163. package/dist/editor/file-editor.d.ts +9 -0
  164. package/dist/editor/file-editor.js +236 -0
  165. package/dist/editor/file-service.d.ts +16 -0
  166. package/dist/editor/file-service.js +52 -0
  167. package/dist/editor/i18n-key-gen.d.ts +1 -0
  168. package/dist/editor/i18n-key-gen.js +7 -0
  169. package/dist/editor/inline-text-edit.d.ts +5 -0
  170. package/dist/editor/inline-text-edit.js +173 -92
  171. package/dist/editor/overlay-events.d.ts +5 -0
  172. package/dist/editor/overlay-events.js +364 -0
  173. package/dist/editor/overlay-hmr.d.ts +2 -0
  174. package/dist/editor/overlay-hmr.js +76 -0
  175. package/dist/editor/overlay-selection.d.ts +29 -0
  176. package/dist/editor/overlay-selection.js +148 -0
  177. package/dist/editor/overlay-utils.d.ts +12 -0
  178. package/dist/editor/overlay-utils.js +59 -0
  179. package/dist/editor/properties-panel-persist.d.ts +14 -0
  180. package/dist/editor/properties-panel-persist.js +70 -0
  181. package/dist/editor/properties-panel-rows.d.ts +10 -0
  182. package/dist/editor/properties-panel-rows.js +349 -0
  183. package/dist/editor/properties-panel-styles.d.ts +4 -0
  184. package/dist/editor/properties-panel-styles.js +174 -0
  185. package/dist/editor/properties-panel.d.ts +4 -0
  186. package/dist/editor/properties-panel.js +148 -0
  187. package/dist/editor/property-registry.d.ts +16 -0
  188. package/dist/editor/property-registry.js +303 -0
  189. package/dist/editor/standalone-file-panel.d.ts +0 -0
  190. package/dist/editor/standalone-file-panel.js +1 -0
  191. package/dist/editor/standalone-overlay-dom.d.ts +0 -0
  192. package/dist/editor/standalone-overlay-dom.js +1 -0
  193. package/dist/editor/standalone-overlay-styles.d.ts +0 -0
  194. package/dist/editor/standalone-overlay-styles.js +1 -0
  195. package/dist/editor/standalone-overlay.d.ts +1 -0
  196. package/dist/editor/standalone-overlay.js +76 -0
  197. package/dist/editor/syntax-highlighter.d.ts +4 -0
  198. package/dist/editor/syntax-highlighter.js +81 -0
  199. package/dist/editor/text-toolbar.d.ts +11 -0
  200. package/dist/editor/text-toolbar.js +327 -0
  201. package/dist/editor/toolbar-styles.d.ts +4 -0
  202. package/dist/editor/toolbar-styles.js +198 -0
  203. package/dist/email/index.d.ts +32 -0
  204. package/dist/email/index.js +154 -0
  205. package/dist/email/providers/resend.d.ts +2 -0
  206. package/dist/email/providers/resend.js +24 -0
  207. package/dist/email/providers/sendgrid.d.ts +2 -0
  208. package/dist/email/providers/sendgrid.js +31 -0
  209. package/dist/email/providers/smtp.d.ts +13 -0
  210. package/dist/email/providers/smtp.js +125 -0
  211. package/dist/email/template-engine.d.ts +18 -0
  212. package/dist/email/template-engine.js +116 -0
  213. package/dist/email/templates/base.d.ts +9 -0
  214. package/dist/email/templates/base.js +65 -0
  215. package/dist/email/templates/password-reset.d.ts +5 -0
  216. package/dist/email/templates/password-reset.js +15 -0
  217. package/dist/email/templates/verify-email.d.ts +5 -0
  218. package/dist/email/templates/verify-email.js +15 -0
  219. package/dist/email/templates/welcome.d.ts +5 -0
  220. package/dist/email/templates/welcome.js +13 -0
  221. package/dist/email/types.d.ts +49 -0
  222. package/dist/email/types.js +1 -0
  223. package/dist/llms/generate.d.ts +46 -0
  224. package/dist/llms/generate.js +185 -0
  225. package/dist/permissions/guard.d.ts +28 -0
  226. package/dist/permissions/guard.js +30 -0
  227. package/dist/permissions/index.d.ts +6 -0
  228. package/dist/permissions/index.js +3 -0
  229. package/dist/permissions/service.d.ts +80 -0
  230. package/dist/permissions/service.js +210 -0
  231. package/dist/permissions/tables.d.ts +5 -0
  232. package/dist/permissions/tables.js +68 -0
  233. package/dist/permissions/types.d.ts +33 -0
  234. package/dist/permissions/types.js +1 -0
  235. package/dist/runtime/app-shell.d.ts +1 -1
  236. package/dist/runtime/app-shell.js +164 -0
  237. package/dist/runtime/auth.d.ts +10 -0
  238. package/dist/runtime/auth.js +30 -0
  239. package/dist/runtime/communication.d.ts +137 -0
  240. package/dist/runtime/communication.js +228 -0
  241. package/dist/runtime/error-boundary.d.ts +23 -0
  242. package/dist/runtime/error-boundary.js +120 -0
  243. package/dist/runtime/i18n.d.ts +6 -1
  244. package/dist/runtime/i18n.js +42 -21
  245. package/dist/runtime/island.d.ts +16 -0
  246. package/dist/runtime/island.js +80 -0
  247. package/dist/runtime/router-data.d.ts +3 -0
  248. package/dist/runtime/router-data.js +102 -17
  249. package/dist/runtime/router-hydration.js +34 -2
  250. package/dist/runtime/router.d.ts +19 -2
  251. package/dist/runtime/router.js +237 -43
  252. package/dist/runtime/socket-client.d.ts +2 -0
  253. package/dist/runtime/socket-client.js +30 -0
  254. package/dist/runtime/webrtc.d.ts +91 -0
  255. package/dist/runtime/webrtc.js +428 -0
  256. package/dist/shared/dom-shims.js +4 -2
  257. package/dist/shared/graceful-shutdown.d.ts +8 -0
  258. package/dist/shared/graceful-shutdown.js +36 -0
  259. package/dist/shared/health.d.ts +8 -0
  260. package/dist/shared/health.js +25 -0
  261. package/dist/shared/llms-txt.d.ts +31 -0
  262. package/dist/shared/llms-txt.js +85 -0
  263. package/dist/shared/logger.d.ts +32 -0
  264. package/dist/shared/logger.js +93 -0
  265. package/dist/shared/meta.d.ts +27 -0
  266. package/dist/shared/meta.js +71 -0
  267. package/dist/shared/middleware-runner.d.ts +9 -0
  268. package/dist/shared/middleware-runner.js +29 -0
  269. package/dist/shared/rate-limit.d.ts +18 -0
  270. package/dist/shared/rate-limit.js +71 -0
  271. package/dist/shared/request-id.d.ts +5 -0
  272. package/dist/shared/request-id.js +18 -0
  273. package/dist/shared/route-matching.js +16 -1
  274. package/dist/shared/security-headers.d.ts +18 -0
  275. package/dist/shared/security-headers.js +38 -0
  276. package/dist/shared/socket-io-setup.d.ts +11 -0
  277. package/dist/shared/socket-io-setup.js +51 -0
  278. package/dist/shared/types.d.ts +15 -0
  279. package/dist/shared/utils.d.ts +33 -7
  280. package/dist/shared/utils.js +164 -27
  281. package/dist/storage/adapters/local.d.ts +44 -0
  282. package/dist/storage/adapters/local.js +85 -0
  283. package/dist/storage/adapters/s3.d.ts +32 -0
  284. package/dist/storage/adapters/s3.js +119 -0
  285. package/dist/storage/adapters/types.d.ts +53 -0
  286. package/dist/storage/adapters/types.js +1 -0
  287. package/dist/storage/index.d.ts +76 -0
  288. package/dist/storage/index.js +83 -0
  289. package/package.json +45 -7
  290. package/templates/blog/api/posts.ts +4 -18
  291. package/templates/blog/data/migrations/001_init.sql +6 -5
  292. package/templates/blog/lumenjs.config.ts +3 -0
  293. package/templates/blog/package.json +14 -0
  294. package/templates/blog/pages/_layout.ts +25 -0
  295. package/templates/blog/pages/index.ts +48 -22
  296. package/templates/blog/pages/posts/[slug].ts +45 -20
  297. package/templates/blog/pages/tag/[tag].ts +44 -0
  298. package/templates/dashboard/api/stats.ts +8 -5
  299. package/templates/dashboard/lumenjs.config.ts +3 -0
  300. package/templates/dashboard/package.json +14 -0
  301. package/templates/dashboard/pages/_layout.ts +25 -0
  302. package/templates/dashboard/pages/index.ts +54 -23
  303. package/templates/dashboard/pages/settings/index.ts +29 -0
  304. package/templates/default/lumenjs.config.ts +3 -0
  305. package/templates/default/package.json +14 -0
  306. package/templates/default/pages/index.ts +24 -0
@@ -0,0 +1,613 @@
1
+ /**
2
+ * AI Chat Bubble — floating chat popover that appears next to the selected element.
3
+ * Streams responses from OpenCode AI coding agent via the editor API.
4
+ */
5
+ import { streamAiChat, rollbackAiTurn, checkAiStatus } from './editor-api-client.js';
6
+ import { renderMarkdown } from './ai-markdown.js';
7
+ const BASE_QUICK_ACTIONS = [
8
+ { label: 'Improve text', prompt: 'Improve the text to be more professional' },
9
+ { label: 'Animation', prompt: 'Add a subtle CSS animation to this element' },
10
+ { label: 'Responsive', prompt: 'Make this element responsive for mobile' },
11
+ { label: 'Dark theme', prompt: 'Convert to a dark color scheme' },
12
+ { label: 'Spacing', prompt: 'Improve spacing and padding' },
13
+ { label: 'Simplify', prompt: 'Simplify and clean up this element' },
14
+ ];
15
+ /** Returns context-aware quick actions based on the selected element(s). */
16
+ function getContextQuickActions(targets) {
17
+ const actions = [];
18
+ if (targets.length === 0)
19
+ return actions;
20
+ // Multi-element context actions
21
+ if (targets.length > 1) {
22
+ actions.push({ label: 'Make consistent', prompt: 'Make these elements visually consistent with each other' });
23
+ actions.push({ label: 'Align', prompt: 'Align these elements properly in a row or column' });
24
+ return actions;
25
+ }
26
+ const el = targets[0];
27
+ const tag = el.tagName.toLowerCase();
28
+ // Image elements
29
+ if (tag === 'img' || tag === 'picture' || tag === 'svg') {
30
+ actions.push({ label: 'Add alt text', prompt: 'Add descriptive alt text to this image for accessibility' });
31
+ actions.push({ label: 'Lazy-load', prompt: 'Add lazy-loading to this image for better performance' });
32
+ }
33
+ // Text elements
34
+ if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'span', 'label'].includes(tag)) {
35
+ actions.push({ label: 'Improve copy', prompt: 'Improve the copy/text content to be more engaging' });
36
+ actions.push({ label: 'Make i18n', prompt: 'Make this text translatable using i18n' });
37
+ }
38
+ // List elements
39
+ if (['ul', 'ol', 'dl'].includes(tag)) {
40
+ actions.push({ label: 'Add items', prompt: 'Add more list items following the same pattern' });
41
+ actions.push({ label: 'Make sortable', prompt: 'Make this list sortable with drag and drop' });
42
+ }
43
+ // Form elements
44
+ if (['form', 'input', 'textarea', 'select', 'button'].includes(tag)) {
45
+ actions.push({ label: 'Add validation', prompt: 'Add proper form validation to this element' });
46
+ actions.push({ label: 'Improve a11y', prompt: 'Improve accessibility: add labels, ARIA attributes, and keyboard support' });
47
+ }
48
+ return actions;
49
+ }
50
+ let panel;
51
+ let messagesContainer;
52
+ let inputEl;
53
+ let sendBtn;
54
+ let contextBadge;
55
+ let quickActionsContainer;
56
+ let currentTargets = [];
57
+ let wasDragged = false;
58
+ let sessionId;
59
+ let activeController = null;
60
+ let aiConfigured = false;
61
+ let nextModel = 'default';
62
+ export function createAiChatPanel() {
63
+ panel = document.createElement('div');
64
+ panel.id = 'nk-ai-chat';
65
+ panel.innerHTML = `
66
+ <div class="nk-ai-header">
67
+ <span class="nk-ai-title">✦ AI</span>
68
+ <span class="nk-ai-context"></span>
69
+ <div style="flex:1"></div>
70
+ <button class="nk-ai-close" title="Close">✕</button>
71
+ </div>
72
+ <div class="nk-ai-messages"></div>
73
+ <div class="nk-ai-quick-actions"></div>
74
+ <div class="nk-ai-input-row">
75
+ <textarea class="nk-ai-input" placeholder="Ask AI..." rows="1"></textarea>
76
+ <button class="nk-ai-send" disabled title="Send">▶</button>
77
+ </div>
78
+ `;
79
+ const style = document.createElement('style');
80
+ style.textContent = `
81
+ #nk-ai-chat {
82
+ position: fixed;
83
+ left: -9999px; top: -9999px;
84
+ width: 340px; max-height: 420px;
85
+ flex-direction: column;
86
+ background: #1e1b2e; color: #e2e8f0; font-family: system-ui, -apple-system, sans-serif;
87
+ font-size: 13px; z-index: 99999;
88
+ border: 1px solid #334155; border-radius: 12px;
89
+ box-shadow: 0 8px 32px rgba(0,0,0,0.5);
90
+ overflow: hidden;
91
+ display: flex;
92
+ visibility: hidden;
93
+ }
94
+ #nk-ai-chat.open { visibility: visible; }
95
+ .nk-ai-header {
96
+ display: flex; align-items: center; gap: 6px;
97
+ padding: 8px 10px; min-height: 34px;
98
+ border-bottom: 1px solid #334155; cursor: grab;
99
+ }
100
+ .nk-ai-title { font-weight: 700; font-size: 12px; color: #7c3aed; white-space: nowrap; }
101
+ .nk-ai-context {
102
+ font-size: 10px; color: #94a3b8; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
103
+ font-family: 'SF Mono', ui-monospace, monospace;
104
+ }
105
+ .nk-ai-close {
106
+ background: transparent; border: none; color: #94a3b8; cursor: pointer;
107
+ font-size: 14px; padding: 2px 6px; border-radius: 4px;
108
+ font-family: inherit; line-height: 1; flex-shrink: 0;
109
+ }
110
+ .nk-ai-close:hover { background: #334155; color: #e2e8f0; }
111
+ .nk-ai-messages {
112
+ flex: 1; overflow-y: auto; padding: 8px 10px; display: flex; flex-direction: column; gap: 6px;
113
+ min-height: 0;
114
+ }
115
+ .nk-ai-messages:empty { display: none; }
116
+ .nk-ai-msg {
117
+ max-width: 90%; padding: 6px 10px; border-radius: 10px; font-size: 12px;
118
+ line-height: 1.45; word-wrap: break-word;
119
+ }
120
+ .nk-ai-msg.user {
121
+ align-self: flex-end; background: #7c3aed; color: #fff; border-bottom-right-radius: 4px;
122
+ }
123
+ .nk-ai-msg.assistant {
124
+ align-self: flex-start; background: #2d2a3e; color: #e2e8f0; border-bottom-left-radius: 4px;
125
+ }
126
+ .nk-ai-msg.assistant p { margin: 0 0 4px 0; }
127
+ .nk-ai-msg.assistant p:last-child { margin-bottom: 0; }
128
+ .nk-ai-msg.assistant ul, .nk-ai-msg.assistant ol { margin: 4px 0; padding-left: 18px; }
129
+ .nk-ai-msg.assistant li { margin: 1px 0; }
130
+ .nk-ai-msg.assistant strong { font-weight: 600; }
131
+ .nk-ai-code {
132
+ background: #1a1a2e; padding: 1px 4px; border-radius: 3px;
133
+ font-family: 'SF Mono', ui-monospace, monospace; font-size: 11px;
134
+ }
135
+ .nk-ai-pre {
136
+ background: #1a1a2e; padding: 8px; border-radius: 6px;
137
+ overflow-x: auto; margin: 4px 0; position: relative;
138
+ }
139
+ .nk-ai-pre code {
140
+ font-family: 'SF Mono', ui-monospace, monospace; font-size: 11px;
141
+ background: none; padding: 0; white-space: pre;
142
+ }
143
+ .nk-ai-code-lang {
144
+ position: absolute; top: 4px; right: 6px;
145
+ font-size: 9px; color: #64748b; font-family: system-ui, sans-serif;
146
+ }
147
+ .nk-ai-msg-actions {
148
+ display: flex; align-items: center; gap: 6px; margin-top: 4px; font-size: 10px;
149
+ }
150
+ .nk-ai-badge {
151
+ background: #334155; color: #94a3b8; padding: 1px 6px; border-radius: 8px;
152
+ }
153
+ .nk-ai-rollback {
154
+ background: transparent; border: none; color: #f59e0b; cursor: pointer;
155
+ font-size: 10px; padding: 1px 4px; font-family: inherit;
156
+ }
157
+ .nk-ai-rollback:hover { text-decoration: underline; }
158
+ .nk-ai-typing {
159
+ align-self: flex-start; padding: 6px 14px; background: #2d2a3e; border-radius: 10px;
160
+ border-bottom-left-radius: 4px;
161
+ }
162
+ .nk-ai-typing span {
163
+ display: inline-block; width: 5px; height: 5px; background: #94a3b8;
164
+ border-radius: 50%; margin: 0 1.5px; animation: nk-ai-dot 1.2s infinite;
165
+ }
166
+ .nk-ai-typing span:nth-child(2) { animation-delay: 0.2s; }
167
+ .nk-ai-typing span:nth-child(3) { animation-delay: 0.4s; }
168
+ @keyframes nk-ai-dot {
169
+ 0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
170
+ 40% { opacity: 1; transform: scale(1); }
171
+ }
172
+ .nk-ai-quick-actions {
173
+ display: flex; gap: 4px; padding: 6px 10px; overflow-x: auto; flex-wrap: wrap;
174
+ border-top: 1px solid #334155;
175
+ }
176
+ .nk-ai-pill {
177
+ background: transparent; border: 1px solid #334155; border-radius: 12px;
178
+ padding: 3px 10px; color: #94a3b8; cursor: pointer; font-size: 10px;
179
+ white-space: nowrap; font-family: inherit; transition: all 0.15s;
180
+ }
181
+ .nk-ai-pill:hover { border-color: #7c3aed; color: #e2e8f0; }
182
+ .nk-ai-input-row {
183
+ display: flex; gap: 6px; padding: 8px 10px;
184
+ border-top: 1px solid #334155; align-items: flex-end;
185
+ }
186
+ .nk-ai-input {
187
+ flex: 1; background: #2d2a3e; border: 1px solid #334155; border-radius: 8px;
188
+ color: #e2e8f0; font-size: 16px; padding: 6px 10px;
189
+ font-family: inherit; resize: none; max-height: 60px; overflow-y: auto;
190
+ line-height: 1.4; outline: none;
191
+ }
192
+ .nk-ai-input::placeholder { color: #64748b; }
193
+ .nk-ai-input:focus { border-color: #7c3aed; }
194
+ .nk-ai-send {
195
+ background: #7c3aed; border: none; color: #fff; width: 30px; height: 30px;
196
+ border-radius: 8px; cursor: pointer; font-size: 12px;
197
+ display: flex; align-items: center; justify-content: center;
198
+ flex-shrink: 0; transition: opacity 0.15s;
199
+ }
200
+ .nk-ai-send:disabled { opacity: 0.4; cursor: default; }
201
+ .nk-ai-send:not(:disabled):hover { background: #6d28d9; }
202
+ @media (max-width: 640px) {
203
+ #nk-ai-chat { width: 300px; max-height: 360px; }
204
+ }
205
+ `;
206
+ panel.appendChild(style);
207
+ document.body.appendChild(panel);
208
+ // Cache references
209
+ messagesContainer = panel.querySelector('.nk-ai-messages');
210
+ inputEl = panel.querySelector('.nk-ai-input');
211
+ sendBtn = panel.querySelector('.nk-ai-send');
212
+ contextBadge = panel.querySelector('.nk-ai-context');
213
+ quickActionsContainer = panel.querySelector('.nk-ai-quick-actions');
214
+ // Close
215
+ panel.querySelector('.nk-ai-close').addEventListener('click', () => hideAiChatPanel());
216
+ // Send button
217
+ sendBtn.addEventListener('click', () => sendMessage());
218
+ // Input: auto-resize + enable/disable send
219
+ inputEl.addEventListener('input', () => {
220
+ inputEl.style.height = 'auto';
221
+ inputEl.style.height = Math.min(inputEl.scrollHeight, 60) + 'px';
222
+ sendBtn.disabled = !inputEl.value.trim();
223
+ });
224
+ // Enter to send (Shift+Enter for newline)
225
+ inputEl.addEventListener('keydown', (e) => {
226
+ if (e.key === 'Enter' && !e.shiftKey) {
227
+ e.preventDefault();
228
+ if (inputEl.value.trim())
229
+ sendMessage();
230
+ }
231
+ });
232
+ // Render initial quick action pills
233
+ renderQuickActions([]);
234
+ // Drag via header
235
+ const header = panel.querySelector('.nk-ai-header');
236
+ let dragOffsetX = 0, dragOffsetY = 0, isDragging = false;
237
+ function onDragMove(ex, ey) {
238
+ if (!isDragging)
239
+ return;
240
+ let nx = ex - dragOffsetX;
241
+ let ny = ey - dragOffsetY;
242
+ // Clamp inside viewport
243
+ nx = Math.max(0, Math.min(nx, window.innerWidth - panel.offsetWidth));
244
+ ny = Math.max(44, Math.min(ny, window.innerHeight - panel.offsetHeight));
245
+ panel.style.left = `${nx}px`;
246
+ panel.style.top = `${ny}px`;
247
+ wasDragged = true;
248
+ }
249
+ header.addEventListener('mousedown', (e) => {
250
+ if (e.target.closest('.nk-ai-close'))
251
+ return;
252
+ isDragging = true;
253
+ dragOffsetX = e.clientX - panel.getBoundingClientRect().left;
254
+ dragOffsetY = e.clientY - panel.getBoundingClientRect().top;
255
+ header.style.cursor = 'grabbing';
256
+ e.preventDefault();
257
+ });
258
+ document.addEventListener('mousemove', (e) => onDragMove(e.clientX, e.clientY));
259
+ document.addEventListener('mouseup', () => { isDragging = false; header.style.cursor = 'grab'; });
260
+ // Touch drag
261
+ header.addEventListener('touchstart', (e) => {
262
+ if (e.target.closest('.nk-ai-close'))
263
+ return;
264
+ const t = e.touches[0];
265
+ isDragging = true;
266
+ dragOffsetX = t.clientX - panel.getBoundingClientRect().left;
267
+ dragOffsetY = t.clientY - panel.getBoundingClientRect().top;
268
+ }, { passive: true });
269
+ document.addEventListener('touchmove', (e) => {
270
+ if (!isDragging)
271
+ return;
272
+ onDragMove(e.touches[0].clientX, e.touches[0].clientY);
273
+ }, { passive: true });
274
+ document.addEventListener('touchend', () => { isDragging = false; });
275
+ // Check AI status on creation
276
+ checkAiStatus().then(status => {
277
+ aiConfigured = status.configured;
278
+ }).catch(() => {
279
+ aiConfigured = false;
280
+ });
281
+ return panel;
282
+ }
283
+ /** Render quick action pills (base + context-aware) into the container. */
284
+ function renderQuickActions(targets) {
285
+ const contextActions = getContextQuickActions(targets);
286
+ const allActions = [...contextActions, ...BASE_QUICK_ACTIONS];
287
+ quickActionsContainer.innerHTML = allActions
288
+ .map(a => `<button class="nk-ai-pill" data-prompt="${a.prompt.replace(/"/g, '&quot;')}">${a.label}</button>`)
289
+ .join('');
290
+ quickActionsContainer.querySelectorAll('.nk-ai-pill').forEach((btn) => {
291
+ btn.addEventListener('click', () => {
292
+ const prompt = btn.dataset.prompt || '';
293
+ inputEl.value = prompt;
294
+ inputEl.style.height = 'auto';
295
+ inputEl.style.height = Math.min(inputEl.scrollHeight, 60) + 'px';
296
+ sendBtn.disabled = false;
297
+ nextModel = 'fast';
298
+ sendMessage();
299
+ });
300
+ });
301
+ }
302
+ /** Position the bubble centered below the element (skips if user dragged) */
303
+ function positionBubble(el) {
304
+ if (wasDragged)
305
+ return;
306
+ const rect = el.getBoundingClientRect();
307
+ if (rect.width === 0 && rect.height === 0) {
308
+ // Element not laid out (shadow DOM, disconnected, or hidden) — park offscreen
309
+ panel.style.left = '-9999px';
310
+ panel.style.top = '-9999px';
311
+ return;
312
+ }
313
+ const pw = 340; // panel width
314
+ const ph = panel.offsetHeight || 420;
315
+ const gap = 6;
316
+ // Center horizontally under element
317
+ let left = rect.left + (rect.width - pw) / 2;
318
+ let top = rect.bottom + gap;
319
+ // Clamp horizontal: keep within viewport
320
+ if (left + pw > window.innerWidth - 8)
321
+ left = window.innerWidth - pw - 8;
322
+ if (left < 8)
323
+ left = 8;
324
+ // If overflows bottom, place above element instead
325
+ if (top + ph > window.innerHeight - 8) {
326
+ top = rect.top - ph - gap;
327
+ }
328
+ // If still above toolbar, clamp below toolbar
329
+ if (top < 52)
330
+ top = 52; // 44px toolbar + 8px margin
331
+ panel.style.left = `${Math.round(left)}px`;
332
+ panel.style.top = `${Math.round(top)}px`;
333
+ }
334
+ export function showAiChatForElement(el) {
335
+ showAiChatForElements([el]);
336
+ }
337
+ export function showAiChatForElements(els) {
338
+ if (els.length === 0)
339
+ return;
340
+ // Reset drag position when selection changes
341
+ const primary = els[0];
342
+ if (currentTargets.length !== els.length || currentTargets[0] !== primary)
343
+ wasDragged = false;
344
+ currentTargets = els;
345
+ // Update context badge
346
+ if (els.length === 1) {
347
+ const tag = primary.tagName.toLowerCase();
348
+ const source = primary.getAttribute('data-nk-source');
349
+ let ctx = `<${tag}>`;
350
+ if (source) {
351
+ const parts = source.split(':');
352
+ if (parts.length >= 2)
353
+ ctx += ` ${parts[0]}:${parts[1]}`;
354
+ }
355
+ contextBadge.textContent = ctx;
356
+ }
357
+ else {
358
+ contextBadge.textContent = `${els.length} elements selected`;
359
+ }
360
+ // Re-render quick actions based on current selection
361
+ renderQuickActions(els);
362
+ // Only show once we have a valid position — prevents flash at top-left
363
+ const rect = primary.getBoundingClientRect();
364
+ if (rect.width === 0 && rect.height === 0) {
365
+ // Element not yet laid out — retry positioning over next few frames
366
+ let retries = 0;
367
+ const tryPosition = () => {
368
+ if (currentTargets[0] !== primary || !primary.isConnected)
369
+ return;
370
+ const r = primary.getBoundingClientRect();
371
+ if (r.width > 0 || r.height > 0) {
372
+ positionBubble(primary);
373
+ panel.classList.add('open');
374
+ }
375
+ else if (retries < 3) {
376
+ retries++;
377
+ requestAnimationFrame(tryPosition);
378
+ }
379
+ };
380
+ requestAnimationFrame(tryPosition);
381
+ return;
382
+ }
383
+ positionBubble(primary);
384
+ panel.classList.add('open');
385
+ }
386
+ /** Update target reference after HMR and reanchor the panel to the new element */
387
+ export function updateAiChatTarget(el) {
388
+ currentTargets = [el];
389
+ const tag = el.tagName.toLowerCase();
390
+ const source = el.getAttribute('data-nk-source');
391
+ let ctx = `<${tag}>`;
392
+ if (source) {
393
+ const parts = source.split(':');
394
+ if (parts.length >= 2)
395
+ ctx += ` ${parts[0]}:${parts[1]}`;
396
+ }
397
+ contextBadge.textContent = ctx;
398
+ renderQuickActions(currentTargets);
399
+ // Reposition to the new element (unless user has manually dragged)
400
+ positionBubble(el);
401
+ }
402
+ export function hideAiChatPanel() {
403
+ panel.classList.remove('open');
404
+ currentTargets = [];
405
+ }
406
+ export function isAiChatPanelOpen() {
407
+ return panel.classList.contains('open');
408
+ }
409
+ /** Reposition on scroll/resize if open (skips if element was disconnected by HMR) */
410
+ export function updateAiChatPosition() {
411
+ const primary = currentTargets[0];
412
+ if (primary && primary.isConnected && isAiChatPanelOpen()) {
413
+ positionBubble(primary);
414
+ }
415
+ }
416
+ function addUserMessage(text) {
417
+ const msg = document.createElement('div');
418
+ msg.className = 'nk-ai-msg user';
419
+ msg.textContent = text;
420
+ messagesContainer.appendChild(msg);
421
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
422
+ }
423
+ function createStreamingMessage() {
424
+ const msg = document.createElement('div');
425
+ msg.className = 'nk-ai-msg assistant';
426
+ const textEl = document.createElement('div');
427
+ textEl.className = 'nk-ai-msg-text';
428
+ msg.appendChild(textEl);
429
+ messagesContainer.appendChild(msg);
430
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
431
+ return msg;
432
+ }
433
+ function finalizeAssistantMessage(msg, turnId) {
434
+ if (turnId) {
435
+ // Remove rollback from previous assistant messages
436
+ messagesContainer.querySelectorAll('.nk-ai-msg.assistant .nk-ai-rollback').forEach((btn) => {
437
+ if (!msg.contains(btn))
438
+ btn.style.display = 'none';
439
+ });
440
+ const actions = document.createElement('div');
441
+ actions.className = 'nk-ai-msg-actions';
442
+ actions.innerHTML = `
443
+ <span class="nk-ai-badge">Changes applied</span>
444
+ <button class="nk-ai-rollback">↩ Rollback</button>
445
+ `;
446
+ actions.querySelector('.nk-ai-rollback').addEventListener('click', async () => {
447
+ try {
448
+ await rollbackAiTurn(turnId);
449
+ const badge = actions.querySelector('.nk-ai-badge');
450
+ if (badge) {
451
+ badge.textContent = 'Rolled back';
452
+ badge.style.color = '#f59e0b';
453
+ }
454
+ const btn = actions.querySelector('.nk-ai-rollback');
455
+ if (btn)
456
+ btn.style.display = 'none';
457
+ }
458
+ catch (err) {
459
+ console.error('[ai-chat] Rollback failed:', err);
460
+ }
461
+ });
462
+ msg.appendChild(actions);
463
+ }
464
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
465
+ }
466
+ function addAssistantError(message) {
467
+ const msg = document.createElement('div');
468
+ msg.className = 'nk-ai-msg assistant';
469
+ msg.style.borderColor = '#ef4444';
470
+ const textEl = document.createElement('div');
471
+ textEl.className = 'nk-ai-msg-text';
472
+ textEl.textContent = message;
473
+ textEl.style.color = '#fca5a5';
474
+ msg.appendChild(textEl);
475
+ messagesContainer.appendChild(msg);
476
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
477
+ }
478
+ function showTypingIndicator() {
479
+ const indicator = document.createElement('div');
480
+ indicator.className = 'nk-ai-typing';
481
+ indicator.innerHTML = '<span></span><span></span><span></span>';
482
+ messagesContainer.appendChild(indicator);
483
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
484
+ return indicator;
485
+ }
486
+ /** Gather rich context for a single element: tag, source, attrs, parents, siblings, styles. */
487
+ function gatherElementContext(el) {
488
+ const ctx = {};
489
+ ctx.elementTag = el.tagName.toLowerCase();
490
+ const source = el.getAttribute('data-nk-source');
491
+ if (source) {
492
+ const parts = source.split(':');
493
+ ctx.sourceFile = parts[0];
494
+ if (parts[1])
495
+ ctx.sourceLine = parseInt(parts[1], 10);
496
+ }
497
+ // Relevant attributes
498
+ const attrs = {};
499
+ for (const attr of el.attributes) {
500
+ if (!attr.name.startsWith('data-nk-') && attr.name !== 'class' && attr.name !== 'style') {
501
+ attrs[attr.name] = attr.value;
502
+ }
503
+ }
504
+ if (Object.keys(attrs).length > 0)
505
+ ctx.elementAttributes = attrs;
506
+ // Parent chain (up to 5 ancestors) for layout context
507
+ const parents = [];
508
+ let p = el.parentElement;
509
+ while (p && p !== document.body && parents.length < 5) {
510
+ const tag = p.tagName.toLowerCase();
511
+ const src = p.getAttribute('data-nk-source');
512
+ parents.push(src ? `<${tag}> (${src})` : `<${tag}>`);
513
+ p = p.parentElement;
514
+ }
515
+ if (parents.length > 0)
516
+ ctx.parentChain = parents;
517
+ // Immediate siblings (up to 10) for structural context
518
+ const siblings = [];
519
+ for (const sib of el.parentElement?.children || []) {
520
+ if (sib !== el && siblings.length < 10) {
521
+ siblings.push(`<${sib.tagName.toLowerCase()}>`);
522
+ }
523
+ }
524
+ if (siblings.length > 0)
525
+ ctx.siblings = siblings;
526
+ // Key computed styles for visual context
527
+ try {
528
+ const cs = window.getComputedStyle(el);
529
+ ctx.computedStyles = {
530
+ display: cs.display,
531
+ position: cs.position,
532
+ fontSize: cs.fontSize,
533
+ color: cs.color,
534
+ backgroundColor: cs.backgroundColor,
535
+ padding: cs.padding,
536
+ margin: cs.margin,
537
+ };
538
+ }
539
+ catch { /* non-fatal */ }
540
+ return ctx;
541
+ }
542
+ function sendMessage() {
543
+ const text = inputEl.value.trim();
544
+ if (!text)
545
+ return;
546
+ if (!aiConfigured) {
547
+ addUserMessage(text);
548
+ inputEl.value = '';
549
+ inputEl.style.height = 'auto';
550
+ sendBtn.disabled = true;
551
+ addAssistantError('AI not available — start OpenCode server to enable AI coding.');
552
+ return;
553
+ }
554
+ // Abort any active request
555
+ if (activeController) {
556
+ activeController.abort();
557
+ activeController = null;
558
+ }
559
+ addUserMessage(text);
560
+ inputEl.value = '';
561
+ inputEl.style.height = 'auto';
562
+ sendBtn.disabled = true;
563
+ const typing = showTypingIndicator();
564
+ // Gather context from the selected element(s)
565
+ const context = {};
566
+ if (currentTargets.length > 0) {
567
+ const elements = currentTargets.map(gatherElementContext);
568
+ if (elements.length === 1) {
569
+ // Single element — flatten into context for backward compatibility
570
+ Object.assign(context, elements[0]);
571
+ }
572
+ else {
573
+ // Multi-element — send as array
574
+ context.elements = elements;
575
+ // Also set the primary element's source for file snapshotting
576
+ if (elements[0].sourceFile) {
577
+ context.sourceFile = elements[0].sourceFile;
578
+ context.sourceLine = elements[0].sourceLine;
579
+ }
580
+ // Collect all unique source files for multi-file enrichment
581
+ const sourceFiles = [...new Set(elements.map(e => e.sourceFile).filter(Boolean))];
582
+ if (sourceFiles.length > 0)
583
+ context.sourceFiles = sourceFiles;
584
+ }
585
+ }
586
+ const streamMsg = createStreamingMessage();
587
+ const textEl = streamMsg.querySelector('.nk-ai-msg-text');
588
+ let rawText = '';
589
+ const modelForRequest = nextModel;
590
+ nextModel = 'default';
591
+ activeController = streamAiChat('element', text, context, sessionId, {
592
+ onToken: (token) => {
593
+ typing.remove();
594
+ rawText += token;
595
+ textEl.innerHTML = renderMarkdown(rawText);
596
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
597
+ },
598
+ onDone: (result) => {
599
+ typing.remove();
600
+ sessionId = result.sessionId;
601
+ const finalText = rawText || result.fullText || 'Done.';
602
+ textEl.innerHTML = renderMarkdown(finalText);
603
+ finalizeAssistantMessage(streamMsg, result.turnId);
604
+ activeController = null;
605
+ },
606
+ onError: (message) => {
607
+ typing.remove();
608
+ streamMsg.remove();
609
+ addAssistantError(message);
610
+ activeController = null;
611
+ },
612
+ }, modelForRequest);
613
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Lightweight markdown-to-HTML renderer for AI chat panels.
3
+ * Handles: fenced code blocks, inline code, bold, italic, lists, paragraphs.
4
+ * No external dependencies — all rendering is done inline.
5
+ */
6
+ /**
7
+ * Convert markdown text to safe HTML.
8
+ * Escapes all HTML first, then applies markdown transformations.
9
+ */
10
+ export declare function renderMarkdown(raw: string): string;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Lightweight markdown-to-HTML renderer for AI chat panels.
3
+ * Handles: fenced code blocks, inline code, bold, italic, lists, paragraphs.
4
+ * No external dependencies — all rendering is done inline.
5
+ */
6
+ function escapeHtml(text) {
7
+ return text
8
+ .replace(/&/g, '&amp;')
9
+ .replace(/</g, '&lt;')
10
+ .replace(/>/g, '&gt;')
11
+ .replace(/"/g, '&quot;');
12
+ }
13
+ /**
14
+ * Convert markdown text to safe HTML.
15
+ * Escapes all HTML first, then applies markdown transformations.
16
+ */
17
+ export function renderMarkdown(raw) {
18
+ if (!raw)
19
+ return '';
20
+ // Extract fenced code blocks before escaping so we can handle them specially
21
+ const codeBlocks = [];
22
+ const BLOCK_PH = '\x00CB';
23
+ // Replace fenced code blocks with placeholders
24
+ let text = raw.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => {
25
+ const escaped = escapeHtml(code.replace(/\n$/, ''));
26
+ const langAttr = lang ? ` data-lang="${escapeHtml(lang)}"` : '';
27
+ const langLabel = lang ? `<span class="nk-ai-code-lang">${escapeHtml(lang)}</span>` : '';
28
+ codeBlocks.push(`<pre class="nk-ai-pre"${langAttr}>${langLabel}<code>${escaped}</code></pre>`);
29
+ return `${BLOCK_PH}${codeBlocks.length - 1}${BLOCK_PH}`;
30
+ });
31
+ // Escape HTML in the remaining text
32
+ text = escapeHtml(text);
33
+ // Restore code block placeholders (they were already escaped/formatted)
34
+ text = text.replace(new RegExp(`${BLOCK_PH}(\\d+)${BLOCK_PH}`, 'g'), (_m, idx) => codeBlocks[parseInt(idx)]);
35
+ // Inline code (single backtick)
36
+ text = text.replace(/`([^`\n]+)`/g, '<code class="nk-ai-code">$1</code>');
37
+ // Bold
38
+ text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
39
+ // Italic (single asterisk, not inside words)
40
+ text = text.replace(/(?<!\w)\*([^*\n]+?)\*(?!\w)/g, '<em>$1</em>');
41
+ // Split into blocks by double newlines (but preserve code blocks)
42
+ const blocks = text.split(/\n{2,}/);
43
+ const rendered = blocks.map(block => {
44
+ const trimmed = block.trim();
45
+ if (!trimmed)
46
+ return '';
47
+ // Already a rendered code block
48
+ if (trimmed.startsWith('<pre '))
49
+ return trimmed;
50
+ // Unordered list
51
+ if (/^[-*]\s/.test(trimmed)) {
52
+ const items = trimmed.split('\n')
53
+ .filter(line => /^[-*]\s/.test(line.trim()))
54
+ .map(line => `<li>${line.trim().replace(/^[-*]\s+/, '')}</li>`)
55
+ .join('');
56
+ return `<ul>${items}</ul>`;
57
+ }
58
+ // Ordered list
59
+ if (/^\d+\.\s/.test(trimmed)) {
60
+ const items = trimmed.split('\n')
61
+ .filter(line => /^\d+\.\s/.test(line.trim()))
62
+ .map(line => `<li>${line.trim().replace(/^\d+\.\s+/, '')}</li>`)
63
+ .join('');
64
+ return `<ol>${items}</ol>`;
65
+ }
66
+ // Regular paragraph — convert single newlines to <br>
67
+ return `<p>${trimmed.replace(/\n/g, '<br>')}</p>`;
68
+ });
69
+ return rendered.filter(Boolean).join('');
70
+ }