@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.
- package/README.md +62 -282
- package/dist/auth/config.d.ts +23 -0
- package/dist/auth/config.js +115 -0
- package/dist/auth/guard.d.ts +12 -0
- package/dist/auth/guard.js +28 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.js +1 -0
- package/dist/auth/middleware.d.ts +23 -0
- package/dist/auth/middleware.js +89 -0
- package/dist/auth/native-auth.d.ts +82 -0
- package/dist/auth/native-auth.js +340 -0
- package/dist/auth/oidc-client.d.ts +17 -0
- package/dist/auth/oidc-client.js +123 -0
- package/dist/auth/providers/google.d.ts +23 -0
- package/dist/auth/providers/google.js +25 -0
- package/dist/auth/providers/index.d.ts +2 -0
- package/dist/auth/providers/index.js +1 -0
- package/dist/auth/routes/login.d.ts +8 -0
- package/dist/auth/routes/login.js +121 -0
- package/dist/auth/routes/logout.d.ts +4 -0
- package/dist/auth/routes/logout.js +79 -0
- package/dist/auth/routes/oidc-callback.d.ts +3 -0
- package/dist/auth/routes/oidc-callback.js +70 -0
- package/dist/auth/routes/password.d.ts +5 -0
- package/dist/auth/routes/password.js +149 -0
- package/dist/auth/routes/signup.d.ts +3 -0
- package/dist/auth/routes/signup.js +81 -0
- package/dist/auth/routes/token.d.ts +4 -0
- package/dist/auth/routes/token.js +70 -0
- package/dist/auth/routes/totp.d.ts +22 -0
- package/dist/auth/routes/totp.js +232 -0
- package/dist/auth/routes/utils.d.ts +7 -0
- package/dist/auth/routes/utils.js +35 -0
- package/dist/auth/routes/verify.d.ts +3 -0
- package/dist/auth/routes/verify.js +26 -0
- package/dist/auth/routes.d.ts +8 -0
- package/dist/auth/routes.js +124 -0
- package/dist/auth/session.d.ts +8 -0
- package/dist/auth/session.js +54 -0
- package/dist/auth/token.d.ts +33 -0
- package/dist/auth/token.js +90 -0
- package/dist/auth/types.d.ts +156 -0
- package/dist/auth/types.js +2 -0
- package/dist/build/build-client.d.ts +15 -0
- package/dist/build/build-client.js +45 -0
- package/dist/build/build-prerender.d.ts +11 -0
- package/dist/build/build-prerender.js +159 -0
- package/dist/build/build-server.d.ts +18 -0
- package/dist/build/build-server.js +107 -0
- package/dist/build/build.js +60 -123
- package/dist/build/scan.d.ts +18 -0
- package/dist/build/scan.js +77 -6
- package/dist/build/serve-api.js +8 -2
- package/dist/build/serve-loaders.d.ts +4 -4
- package/dist/build/serve-loaders.js +26 -18
- package/dist/build/serve-ssr.js +38 -11
- package/dist/build/serve-static.js +3 -3
- package/dist/build/serve.js +341 -18
- package/dist/cli.js +37 -6
- package/dist/communication/encryption.d.ts +35 -0
- package/dist/communication/encryption.js +90 -0
- package/dist/communication/handlers/context.d.ts +27 -0
- package/dist/communication/handlers/context.js +1 -0
- package/dist/communication/handlers/conversation.d.ts +24 -0
- package/dist/communication/handlers/conversation.js +113 -0
- package/dist/communication/handlers/file-upload.d.ts +17 -0
- package/dist/communication/handlers/file-upload.js +62 -0
- package/dist/communication/handlers/messaging.d.ts +30 -0
- package/dist/communication/handlers/messaging.js +237 -0
- package/dist/communication/handlers/presence.d.ts +15 -0
- package/dist/communication/handlers/presence.js +76 -0
- package/dist/communication/handlers.d.ts +5 -0
- package/dist/communication/handlers.js +5 -0
- package/dist/communication/index.d.ts +9 -0
- package/dist/communication/index.js +7 -0
- package/dist/communication/link-preview.d.ts +18 -0
- package/dist/communication/link-preview.js +115 -0
- package/dist/communication/schema.d.ts +10 -0
- package/dist/communication/schema.js +101 -0
- package/dist/communication/server.d.ts +86 -0
- package/dist/communication/server.js +212 -0
- package/dist/communication/signaling.d.ts +43 -0
- package/dist/communication/signaling.js +271 -0
- package/dist/communication/store.d.ts +71 -0
- package/dist/communication/store.js +289 -0
- package/dist/communication/types.d.ts +454 -0
- package/dist/communication/types.js +1 -0
- package/dist/create.d.ts +1 -0
- package/dist/create.js +55 -0
- package/dist/db/auto-migrate.d.ts +3 -0
- package/dist/db/auto-migrate.js +100 -0
- package/dist/db/client.d.ts +3 -0
- package/dist/db/client.js +18 -0
- package/dist/db/index.d.ts +17 -13
- package/dist/db/index.js +205 -26
- package/dist/db/seed.d.ts +12 -0
- package/dist/db/seed.js +88 -0
- package/dist/db/table.d.ts +10 -0
- package/dist/db/table.js +12 -0
- package/dist/dev-server/config.d.ts +11 -0
- package/dist/dev-server/config.js +40 -20
- package/dist/dev-server/index-html.d.ts +4 -0
- package/dist/dev-server/index-html.js +21 -6
- package/dist/dev-server/nuralyui-aliases.d.ts +0 -4
- package/dist/dev-server/nuralyui-aliases.js +115 -94
- package/dist/dev-server/plugins/vite-plugin-api-routes.js +29 -5
- package/dist/dev-server/plugins/vite-plugin-auth.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-auth.js +223 -0
- package/dist/dev-server/plugins/vite-plugin-auto-define.d.ts +16 -0
- package/dist/dev-server/plugins/vite-plugin-auto-define.js +111 -0
- package/dist/dev-server/plugins/vite-plugin-communication.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-communication.js +205 -0
- package/dist/dev-server/plugins/vite-plugin-editor-api.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-editor-api.js +318 -0
- package/dist/dev-server/plugins/vite-plugin-i18n.js +69 -2
- package/dist/dev-server/plugins/vite-plugin-lit-dedup.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-lit-dedup.js +78 -34
- package/dist/dev-server/plugins/vite-plugin-lit-hmr.js +44 -2
- package/dist/dev-server/plugins/vite-plugin-llms.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-llms.js +92 -0
- package/dist/dev-server/plugins/vite-plugin-loaders.js +146 -13
- package/dist/dev-server/plugins/vite-plugin-routes.js +16 -5
- package/dist/dev-server/plugins/vite-plugin-socketio.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-socketio.js +51 -0
- package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-source-annotator.js +26 -3
- package/dist/dev-server/plugins/vite-plugin-storage.d.ts +10 -0
- package/dist/dev-server/plugins/vite-plugin-storage.js +126 -0
- package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +140 -3
- package/dist/dev-server/server.js +242 -70
- package/dist/dev-server/ssr-render.d.ts +2 -1
- package/dist/dev-server/ssr-render.js +117 -50
- package/dist/editor/ai/backend.d.ts +20 -0
- package/dist/editor/ai/backend.js +113 -0
- package/dist/editor/ai/claude-code-client.d.ts +20 -0
- package/dist/editor/ai/claude-code-client.js +145 -0
- package/dist/editor/ai/deepseek-client.d.ts +7 -0
- package/dist/editor/ai/deepseek-client.js +113 -0
- package/dist/editor/ai/opencode-client.d.ts +14 -0
- package/dist/editor/ai/opencode-client.js +99 -0
- package/dist/editor/ai/snapshot-store.d.ts +22 -0
- package/dist/editor/ai/snapshot-store.js +35 -0
- package/dist/editor/ai/types.d.ts +30 -0
- package/dist/editor/ai/types.js +136 -0
- package/dist/editor/ai-chat-panel.d.ts +13 -0
- package/dist/editor/ai-chat-panel.js +613 -0
- package/dist/editor/ai-markdown.d.ts +10 -0
- package/dist/editor/ai-markdown.js +70 -0
- package/dist/editor/ai-project-panel.d.ts +11 -0
- package/dist/editor/ai-project-panel.js +332 -0
- package/dist/editor/ast-modification.d.ts +11 -0
- package/dist/editor/ast-modification.js +1 -0
- package/dist/editor/ast-service.d.ts +30 -0
- package/dist/editor/ast-service.js +180 -0
- package/dist/editor/css-rules.d.ts +54 -0
- package/dist/editor/css-rules.js +423 -0
- package/dist/editor/editor-api-client.d.ts +51 -0
- package/dist/editor/editor-api-client.js +162 -0
- package/dist/editor/editor-bridge.d.ts +1 -0
- package/dist/editor/editor-bridge.js +18 -8
- package/dist/editor/editor-toolbar.d.ts +14 -0
- package/dist/editor/editor-toolbar.js +115 -0
- package/dist/editor/file-editor.d.ts +9 -0
- package/dist/editor/file-editor.js +236 -0
- package/dist/editor/file-service.d.ts +16 -0
- package/dist/editor/file-service.js +52 -0
- package/dist/editor/i18n-key-gen.d.ts +1 -0
- package/dist/editor/i18n-key-gen.js +7 -0
- package/dist/editor/inline-text-edit.d.ts +5 -0
- package/dist/editor/inline-text-edit.js +173 -92
- package/dist/editor/overlay-events.d.ts +5 -0
- package/dist/editor/overlay-events.js +364 -0
- package/dist/editor/overlay-hmr.d.ts +2 -0
- package/dist/editor/overlay-hmr.js +76 -0
- package/dist/editor/overlay-selection.d.ts +29 -0
- package/dist/editor/overlay-selection.js +148 -0
- package/dist/editor/overlay-utils.d.ts +12 -0
- package/dist/editor/overlay-utils.js +59 -0
- package/dist/editor/properties-panel-persist.d.ts +14 -0
- package/dist/editor/properties-panel-persist.js +70 -0
- package/dist/editor/properties-panel-rows.d.ts +10 -0
- package/dist/editor/properties-panel-rows.js +349 -0
- package/dist/editor/properties-panel-styles.d.ts +4 -0
- package/dist/editor/properties-panel-styles.js +174 -0
- package/dist/editor/properties-panel.d.ts +4 -0
- package/dist/editor/properties-panel.js +148 -0
- package/dist/editor/property-registry.d.ts +16 -0
- package/dist/editor/property-registry.js +303 -0
- package/dist/editor/standalone-file-panel.d.ts +0 -0
- package/dist/editor/standalone-file-panel.js +1 -0
- package/dist/editor/standalone-overlay-dom.d.ts +0 -0
- package/dist/editor/standalone-overlay-dom.js +1 -0
- package/dist/editor/standalone-overlay-styles.d.ts +0 -0
- package/dist/editor/standalone-overlay-styles.js +1 -0
- package/dist/editor/standalone-overlay.d.ts +1 -0
- package/dist/editor/standalone-overlay.js +76 -0
- package/dist/editor/syntax-highlighter.d.ts +4 -0
- package/dist/editor/syntax-highlighter.js +81 -0
- package/dist/editor/text-toolbar.d.ts +11 -0
- package/dist/editor/text-toolbar.js +327 -0
- package/dist/editor/toolbar-styles.d.ts +4 -0
- package/dist/editor/toolbar-styles.js +198 -0
- package/dist/email/index.d.ts +32 -0
- package/dist/email/index.js +154 -0
- package/dist/email/providers/resend.d.ts +2 -0
- package/dist/email/providers/resend.js +24 -0
- package/dist/email/providers/sendgrid.d.ts +2 -0
- package/dist/email/providers/sendgrid.js +31 -0
- package/dist/email/providers/smtp.d.ts +13 -0
- package/dist/email/providers/smtp.js +125 -0
- package/dist/email/template-engine.d.ts +18 -0
- package/dist/email/template-engine.js +116 -0
- package/dist/email/templates/base.d.ts +9 -0
- package/dist/email/templates/base.js +65 -0
- package/dist/email/templates/password-reset.d.ts +5 -0
- package/dist/email/templates/password-reset.js +15 -0
- package/dist/email/templates/verify-email.d.ts +5 -0
- package/dist/email/templates/verify-email.js +15 -0
- package/dist/email/templates/welcome.d.ts +5 -0
- package/dist/email/templates/welcome.js +13 -0
- package/dist/email/types.d.ts +49 -0
- package/dist/email/types.js +1 -0
- package/dist/llms/generate.d.ts +46 -0
- package/dist/llms/generate.js +185 -0
- package/dist/permissions/guard.d.ts +28 -0
- package/dist/permissions/guard.js +30 -0
- package/dist/permissions/index.d.ts +6 -0
- package/dist/permissions/index.js +3 -0
- package/dist/permissions/service.d.ts +80 -0
- package/dist/permissions/service.js +210 -0
- package/dist/permissions/tables.d.ts +5 -0
- package/dist/permissions/tables.js +68 -0
- package/dist/permissions/types.d.ts +33 -0
- package/dist/permissions/types.js +1 -0
- package/dist/runtime/app-shell.d.ts +1 -1
- package/dist/runtime/app-shell.js +164 -0
- package/dist/runtime/auth.d.ts +10 -0
- package/dist/runtime/auth.js +30 -0
- package/dist/runtime/communication.d.ts +137 -0
- package/dist/runtime/communication.js +228 -0
- package/dist/runtime/error-boundary.d.ts +23 -0
- package/dist/runtime/error-boundary.js +120 -0
- package/dist/runtime/i18n.d.ts +6 -1
- package/dist/runtime/i18n.js +42 -21
- package/dist/runtime/island.d.ts +16 -0
- package/dist/runtime/island.js +80 -0
- package/dist/runtime/router-data.d.ts +3 -0
- package/dist/runtime/router-data.js +102 -17
- package/dist/runtime/router-hydration.js +34 -2
- package/dist/runtime/router.d.ts +19 -2
- package/dist/runtime/router.js +237 -43
- package/dist/runtime/socket-client.d.ts +2 -0
- package/dist/runtime/socket-client.js +30 -0
- package/dist/runtime/webrtc.d.ts +91 -0
- package/dist/runtime/webrtc.js +428 -0
- package/dist/shared/dom-shims.js +4 -2
- package/dist/shared/graceful-shutdown.d.ts +8 -0
- package/dist/shared/graceful-shutdown.js +36 -0
- package/dist/shared/health.d.ts +8 -0
- package/dist/shared/health.js +25 -0
- package/dist/shared/llms-txt.d.ts +31 -0
- package/dist/shared/llms-txt.js +85 -0
- package/dist/shared/logger.d.ts +32 -0
- package/dist/shared/logger.js +93 -0
- package/dist/shared/meta.d.ts +27 -0
- package/dist/shared/meta.js +71 -0
- package/dist/shared/middleware-runner.d.ts +9 -0
- package/dist/shared/middleware-runner.js +29 -0
- package/dist/shared/rate-limit.d.ts +18 -0
- package/dist/shared/rate-limit.js +71 -0
- package/dist/shared/request-id.d.ts +5 -0
- package/dist/shared/request-id.js +18 -0
- package/dist/shared/route-matching.js +16 -1
- package/dist/shared/security-headers.d.ts +18 -0
- package/dist/shared/security-headers.js +38 -0
- package/dist/shared/socket-io-setup.d.ts +11 -0
- package/dist/shared/socket-io-setup.js +51 -0
- package/dist/shared/types.d.ts +15 -0
- package/dist/shared/utils.d.ts +33 -7
- package/dist/shared/utils.js +164 -27
- package/dist/storage/adapters/local.d.ts +44 -0
- package/dist/storage/adapters/local.js +85 -0
- package/dist/storage/adapters/s3.d.ts +32 -0
- package/dist/storage/adapters/s3.js +119 -0
- package/dist/storage/adapters/types.d.ts +53 -0
- package/dist/storage/adapters/types.js +1 -0
- package/dist/storage/index.d.ts +76 -0
- package/dist/storage/index.js +83 -0
- package/package.json +45 -7
- package/templates/blog/api/posts.ts +4 -18
- package/templates/blog/data/migrations/001_init.sql +6 -5
- package/templates/blog/lumenjs.config.ts +3 -0
- package/templates/blog/package.json +14 -0
- package/templates/blog/pages/_layout.ts +25 -0
- package/templates/blog/pages/index.ts +48 -22
- package/templates/blog/pages/posts/[slug].ts +45 -20
- package/templates/blog/pages/tag/[tag].ts +44 -0
- package/templates/dashboard/api/stats.ts +8 -5
- package/templates/dashboard/lumenjs.config.ts +3 -0
- package/templates/dashboard/package.json +14 -0
- package/templates/dashboard/pages/_layout.ts +25 -0
- package/templates/dashboard/pages/index.ts +54 -23
- package/templates/dashboard/pages/settings/index.ts +29 -0
- package/templates/default/lumenjs.config.ts +3 -0
- package/templates/default/package.json +14 -0
- 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, '"')}">${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, '&')
|
|
9
|
+
.replace(/</g, '<')
|
|
10
|
+
.replace(/>/g, '>')
|
|
11
|
+
.replace(/"/g, '"');
|
|
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
|
+
}
|