@things-factory/board-ai 10.0.0-beta.108
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/PLAN-styling-tools.md +1253 -0
- package/client/components/board-ai-chat.test.ts +149 -0
- package/client/components/board-ai-chat.ts +2574 -0
- package/client/components/chat-echo-dedup.test.ts +116 -0
- package/client/components/chat-echo-dedup.ts +89 -0
- package/client/components/chat-input-builder.ts +47 -0
- package/client/components/default-catalog-entries.test.ts +59 -0
- package/client/components/default-catalog-entries.ts +100 -0
- package/client/components/default-slash-templates.test.ts +55 -0
- package/client/components/default-slash-templates.ts +129 -0
- package/client/components/markdown.test.ts +518 -0
- package/client/components/markdown.ts +508 -0
- package/client/components/mention-popup-helpers.ts +433 -0
- package/client/components/mention-popup.test.ts +588 -0
- package/client/components/mention-popup.ts +238 -0
- package/client/index.ts +11 -0
- package/client/tsconfig.json +13 -0
- package/client/utils/board-edit-patch.ts +374 -0
- package/config/config.development.js +43 -0
- package/config/config.production.js +15 -0
- package/dist-client/client/components/board-ai-chat.d.ts +277 -0
- package/dist-client/client/components/board-ai-chat.js +2444 -0
- package/dist-client/client/components/board-ai-chat.js.map +1 -0
- package/dist-client/client/components/board-ai-chat.test.d.ts +1 -0
- package/dist-client/client/components/board-ai-chat.test.js +138 -0
- package/dist-client/client/components/board-ai-chat.test.js.map +1 -0
- package/dist-client/client/components/chat-echo-dedup.d.ts +45 -0
- package/dist-client/client/components/chat-echo-dedup.js +58 -0
- package/dist-client/client/components/chat-echo-dedup.js.map +1 -0
- package/dist-client/client/components/chat-echo-dedup.test.d.ts +1 -0
- package/dist-client/client/components/chat-echo-dedup.test.js +101 -0
- package/dist-client/client/components/chat-echo-dedup.test.js.map +1 -0
- package/dist-client/client/components/chat-input-builder.d.ts +39 -0
- package/dist-client/client/components/chat-input-builder.js +26 -0
- package/dist-client/client/components/chat-input-builder.js.map +1 -0
- package/dist-client/client/components/default-catalog-entries.d.ts +15 -0
- package/dist-client/client/components/default-catalog-entries.js +86 -0
- package/dist-client/client/components/default-catalog-entries.js.map +1 -0
- package/dist-client/client/components/default-catalog-entries.test.d.ts +1 -0
- package/dist-client/client/components/default-catalog-entries.test.js +52 -0
- package/dist-client/client/components/default-catalog-entries.test.js.map +1 -0
- package/dist-client/client/components/default-slash-templates.d.ts +17 -0
- package/dist-client/client/components/default-slash-templates.js +113 -0
- package/dist-client/client/components/default-slash-templates.js.map +1 -0
- package/dist-client/client/components/default-slash-templates.test.d.ts +1 -0
- package/dist-client/client/components/default-slash-templates.test.js +51 -0
- package/dist-client/client/components/default-slash-templates.test.js.map +1 -0
- package/dist-client/client/components/markdown.d.ts +60 -0
- package/dist-client/client/components/markdown.js +422 -0
- package/dist-client/client/components/markdown.js.map +1 -0
- package/dist-client/client/components/markdown.test.d.ts +1 -0
- package/dist-client/client/components/markdown.test.js +418 -0
- package/dist-client/client/components/markdown.test.js.map +1 -0
- package/dist-client/client/components/mention-popup-helpers.d.ts +165 -0
- package/dist-client/client/components/mention-popup-helpers.js +330 -0
- package/dist-client/client/components/mention-popup-helpers.js.map +1 -0
- package/dist-client/client/components/mention-popup.d.ts +35 -0
- package/dist-client/client/components/mention-popup.js +223 -0
- package/dist-client/client/components/mention-popup.js.map +1 -0
- package/dist-client/client/components/mention-popup.test.d.ts +1 -0
- package/dist-client/client/components/mention-popup.test.js +474 -0
- package/dist-client/client/components/mention-popup.test.js.map +1 -0
- package/dist-client/client/index.d.ts +11 -0
- package/dist-client/client/index.js +12 -0
- package/dist-client/client/index.js.map +1 -0
- package/dist-client/client/utils/board-edit-patch.d.ts +157 -0
- package/dist-client/client/utils/board-edit-patch.js +284 -0
- package/dist-client/client/utils/board-edit-patch.js.map +1 -0
- package/dist-client/server/service/agentic-loop.d.ts +87 -0
- package/dist-client/server/service/agentic-loop.js +276 -0
- package/dist-client/server/service/agentic-loop.js.map +1 -0
- package/dist-client/server/service/assistant.d.ts +84 -0
- package/dist-client/server/service/assistant.js +2220 -0
- package/dist-client/server/service/assistant.js.map +1 -0
- package/dist-client/server/service/catalog-resolver.d.ts +18 -0
- package/dist-client/server/service/catalog-resolver.js +93 -0
- package/dist-client/server/service/catalog-resolver.js.map +1 -0
- package/dist-client/server/service/component-tree.d.ts +68 -0
- package/dist-client/server/service/component-tree.js +68 -0
- package/dist-client/server/service/component-tree.js.map +1 -0
- package/dist-client/server/service/mention-resolver.d.ts +96 -0
- package/dist-client/server/service/mention-resolver.js +172 -0
- package/dist-client/server/service/mention-resolver.js.map +1 -0
- package/dist-client/server/service/prefab-registry.d.ts +73 -0
- package/dist-client/server/service/prefab-registry.js +252 -0
- package/dist-client/server/service/prefab-registry.js.map +1 -0
- package/dist-client/server/service/styling/clear-tools.d.ts +15 -0
- package/dist-client/server/service/styling/clear-tools.js +50 -0
- package/dist-client/server/service/styling/clear-tools.js.map +1 -0
- package/dist-client/server/service/styling/color-utils.d.ts +43 -0
- package/dist-client/server/service/styling/color-utils.js +205 -0
- package/dist-client/server/service/styling/color-utils.js.map +1 -0
- package/dist-client/server/service/styling/copy-tools.d.ts +27 -0
- package/dist-client/server/service/styling/copy-tools.js +78 -0
- package/dist-client/server/service/styling/copy-tools.js.map +1 -0
- package/dist-client/server/service/styling/effect-tools.d.ts +45 -0
- package/dist-client/server/service/styling/effect-tools.js +85 -0
- package/dist-client/server/service/styling/effect-tools.js.map +1 -0
- package/dist-client/server/service/styling/fill-tools.d.ts +60 -0
- package/dist-client/server/service/styling/fill-tools.js +226 -0
- package/dist-client/server/service/styling/fill-tools.js.map +1 -0
- package/dist-client/server/service/styling/material-tools.d.ts +32 -0
- package/dist-client/server/service/styling/material-tools.js +88 -0
- package/dist-client/server/service/styling/material-tools.js.map +1 -0
- package/dist-client/server/service/styling/read-tools.d.ts +34 -0
- package/dist-client/server/service/styling/read-tools.js +42 -0
- package/dist-client/server/service/styling/read-tools.js.map +1 -0
- package/dist-client/server/service/styling/registry.d.ts +91 -0
- package/dist-client/server/service/styling/registry.js +345 -0
- package/dist-client/server/service/styling/registry.js.map +1 -0
- package/dist-client/server/service/styling/stroke-tools.d.ts +35 -0
- package/dist-client/server/service/styling/stroke-tools.js +94 -0
- package/dist-client/server/service/styling/stroke-tools.js.map +1 -0
- package/dist-client/server/service/styling/text-tools.d.ts +32 -0
- package/dist-client/server/service/styling/text-tools.js +101 -0
- package/dist-client/server/service/styling/text-tools.js.map +1 -0
- package/dist-client/server/service/types.d.ts +265 -0
- package/dist-client/server/service/types.js +2 -0
- package/dist-client/server/service/types.js.map +1 -0
- package/dist-client/server/service/validation/board-model-schema.d.ts +8311 -0
- package/dist-client/server/service/validation/board-model-schema.js +402 -0
- package/dist-client/server/service/validation/board-model-schema.js.map +1 -0
- package/dist-client/server/service/validation/tool-validation.d.ts +31 -0
- package/dist-client/server/service/validation/tool-validation.js +193 -0
- package/dist-client/server/service/validation/tool-validation.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-server/index.d.ts +23 -0
- package/dist-server/index.js +27 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/service/agentic-loop.d.ts +87 -0
- package/dist-server/service/agentic-loop.js +280 -0
- package/dist-server/service/agentic-loop.js.map +1 -0
- package/dist-server/service/apply-patch.d.ts +47 -0
- package/dist-server/service/apply-patch.js +369 -0
- package/dist-server/service/apply-patch.js.map +1 -0
- package/dist-server/service/assistant.d.ts +84 -0
- package/dist-server/service/assistant.js +2237 -0
- package/dist-server/service/assistant.js.map +1 -0
- package/dist-server/service/board-ai-resolver.d.ts +54 -0
- package/dist-server/service/board-ai-resolver.js +409 -0
- package/dist-server/service/board-ai-resolver.js.map +1 -0
- package/dist-server/service/catalog-resolver.d.ts +18 -0
- package/dist-server/service/catalog-resolver.js +98 -0
- package/dist-server/service/catalog-resolver.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-broadcast.d.ts +13 -0
- package/dist-server/service/chat-message/chat-message-broadcast.js +13 -0
- package/dist-server/service/chat-message/chat-message-broadcast.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-publish.d.ts +6 -0
- package/dist-server/service/chat-message/chat-message-publish.js +27 -0
- package/dist-server/service/chat-message/chat-message-publish.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-subscription.d.ts +8 -0
- package/dist-server/service/chat-message/chat-message-subscription.js +53 -0
- package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -0
- package/dist-server/service/chat-message/chat-message.d.ts +56 -0
- package/dist-server/service/chat-message/chat-message.js +149 -0
- package/dist-server/service/chat-message/chat-message.js.map +1 -0
- package/dist-server/service/chat-message/index.d.ts +5 -0
- package/dist-server/service/chat-message/index.js +12 -0
- package/dist-server/service/chat-message/index.js.map +1 -0
- package/dist-server/service/chat-session/chat-session.d.ts +31 -0
- package/dist-server/service/chat-session/chat-session.js +131 -0
- package/dist-server/service/chat-session/chat-session.js.map +1 -0
- package/dist-server/service/chat-session/index.d.ts +3 -0
- package/dist-server/service/chat-session/index.js +7 -0
- package/dist-server/service/chat-session/index.js.map +1 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.d.ts +30 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.js +112 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.js.map +1 -0
- package/dist-server/service/chat-session-participant/index.d.ts +4 -0
- package/dist-server/service/chat-session-participant/index.js +10 -0
- package/dist-server/service/chat-session-participant/index.js.map +1 -0
- package/dist-server/service/chat-session-participant/invite-notification.d.ts +27 -0
- package/dist-server/service/chat-session-participant/invite-notification.js +30 -0
- package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -0
- package/dist-server/service/chat-session-participant/presence.d.ts +4 -0
- package/dist-server/service/chat-session-participant/presence.js +22 -0
- package/dist-server/service/chat-session-participant/presence.js.map +1 -0
- package/dist-server/service/chat-session-resolver.d.ts +20 -0
- package/dist-server/service/chat-session-resolver.js +356 -0
- package/dist-server/service/chat-session-resolver.js.map +1 -0
- package/dist-server/service/component-tree.d.ts +68 -0
- package/dist-server/service/component-tree.js +73 -0
- package/dist-server/service/component-tree.js.map +1 -0
- package/dist-server/service/index.d.ts +20 -0
- package/dist-server/service/index.js +34 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/mention-marker.d.ts +78 -0
- package/dist-server/service/mention-marker.js +162 -0
- package/dist-server/service/mention-marker.js.map +1 -0
- package/dist-server/service/mention-resolver.d.ts +96 -0
- package/dist-server/service/mention-resolver.js +177 -0
- package/dist-server/service/mention-resolver.js.map +1 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.d.ts +13 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.js +13 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.js.map +1 -0
- package/dist-server/service/patch-entry/board-patch-subscription.d.ts +12 -0
- package/dist-server/service/patch-entry/board-patch-subscription.js +63 -0
- package/dist-server/service/patch-entry/board-patch-subscription.js.map +1 -0
- package/dist-server/service/patch-entry/index.d.ts +4 -0
- package/dist-server/service/patch-entry/index.js +12 -0
- package/dist-server/service/patch-entry/index.js.map +1 -0
- package/dist-server/service/patch-entry/patch-entry.d.ts +16 -0
- package/dist-server/service/patch-entry/patch-entry.js +96 -0
- package/dist-server/service/patch-entry/patch-entry.js.map +1 -0
- package/dist-server/service/prefab-registry.d.ts +73 -0
- package/dist-server/service/prefab-registry.js +258 -0
- package/dist-server/service/prefab-registry.js.map +1 -0
- package/dist-server/service/styling/clear-tools.d.ts +15 -0
- package/dist-server/service/styling/clear-tools.js +53 -0
- package/dist-server/service/styling/clear-tools.js.map +1 -0
- package/dist-server/service/styling/color-utils.d.ts +43 -0
- package/dist-server/service/styling/color-utils.js +210 -0
- package/dist-server/service/styling/color-utils.js.map +1 -0
- package/dist-server/service/styling/copy-tools.d.ts +27 -0
- package/dist-server/service/styling/copy-tools.js +81 -0
- package/dist-server/service/styling/copy-tools.js.map +1 -0
- package/dist-server/service/styling/effect-tools.d.ts +45 -0
- package/dist-server/service/styling/effect-tools.js +90 -0
- package/dist-server/service/styling/effect-tools.js.map +1 -0
- package/dist-server/service/styling/fill-tools.d.ts +60 -0
- package/dist-server/service/styling/fill-tools.js +229 -0
- package/dist-server/service/styling/fill-tools.js.map +1 -0
- package/dist-server/service/styling/material-tools.d.ts +32 -0
- package/dist-server/service/styling/material-tools.js +92 -0
- package/dist-server/service/styling/material-tools.js.map +1 -0
- package/dist-server/service/styling/read-tools.d.ts +34 -0
- package/dist-server/service/styling/read-tools.js +45 -0
- package/dist-server/service/styling/read-tools.js.map +1 -0
- package/dist-server/service/styling/registry.d.ts +91 -0
- package/dist-server/service/styling/registry.js +352 -0
- package/dist-server/service/styling/registry.js.map +1 -0
- package/dist-server/service/styling/stroke-tools.d.ts +35 -0
- package/dist-server/service/styling/stroke-tools.js +97 -0
- package/dist-server/service/styling/stroke-tools.js.map +1 -0
- package/dist-server/service/styling/text-tools.d.ts +32 -0
- package/dist-server/service/styling/text-tools.js +105 -0
- package/dist-server/service/styling/text-tools.js.map +1 -0
- package/dist-server/service/subscribe-auth.d.ts +14 -0
- package/dist-server/service/subscribe-auth.js +33 -0
- package/dist-server/service/subscribe-auth.js.map +1 -0
- package/dist-server/service/types.d.ts +265 -0
- package/dist-server/service/types.js +3 -0
- package/dist-server/service/types.js.map +1 -0
- package/dist-server/service/validation/board-model-schema.d.ts +8311 -0
- package/dist-server/service/validation/board-model-schema.js +409 -0
- package/dist-server/service/validation/board-model-schema.js.map +1 -0
- package/dist-server/service/validation/tool-validation.d.ts +31 -0
- package/dist-server/service/validation/tool-validation.js +196 -0
- package/dist-server/service/validation/tool-validation.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +48 -0
- package/server/index.ts +23 -0
- package/server/service/agentic-loop.test.ts +705 -0
- package/server/service/agentic-loop.ts +431 -0
- package/server/service/apply-patch-drift.test.ts +829 -0
- package/server/service/apply-patch.test.ts +757 -0
- package/server/service/apply-patch.ts +411 -0
- package/server/service/assistant-integration.test.ts +922 -0
- package/server/service/assistant-llm-smoke.test.ts +296 -0
- package/server/service/assistant.test.ts +2034 -0
- package/server/service/assistant.ts +2378 -0
- package/server/service/board-ai-resolver.ts +407 -0
- package/server/service/catalog-resolver.test.ts +137 -0
- package/server/service/catalog-resolver.ts +102 -0
- package/server/service/chat-message/chat-message-broadcast.integration.test.ts +73 -0
- package/server/service/chat-message/chat-message-broadcast.test.ts +40 -0
- package/server/service/chat-message/chat-message-broadcast.ts +27 -0
- package/server/service/chat-message/chat-message-publish.ts +22 -0
- package/server/service/chat-message/chat-message-subscription.ts +41 -0
- package/server/service/chat-message/chat-message.ts +169 -0
- package/server/service/chat-message/index.ts +12 -0
- package/server/service/chat-session/chat-session.ts +130 -0
- package/server/service/chat-session/index.ts +5 -0
- package/server/service/chat-session-participant/chat-session-participant.ts +110 -0
- package/server/service/chat-session-participant/index.ts +6 -0
- package/server/service/chat-session-participant/invite-notification.test.ts +30 -0
- package/server/service/chat-session-participant/invite-notification.ts +48 -0
- package/server/service/chat-session-participant/presence.test.ts +39 -0
- package/server/service/chat-session-participant/presence.ts +21 -0
- package/server/service/chat-session-resolver.ts +312 -0
- package/server/service/component-tree.test.ts +248 -0
- package/server/service/component-tree.ts +135 -0
- package/server/service/index.ts +32 -0
- package/server/service/mention-marker.extract.test.ts +61 -0
- package/server/service/mention-marker.test.ts +228 -0
- package/server/service/mention-marker.ts +184 -0
- package/server/service/mention-resolver.test.ts +273 -0
- package/server/service/mention-resolver.ts +264 -0
- package/server/service/patch-entry/board-patch-broadcast.ts +24 -0
- package/server/service/patch-entry/board-patch-subscription.test.ts +40 -0
- package/server/service/patch-entry/board-patch-subscription.ts +50 -0
- package/server/service/patch-entry/index.ts +12 -0
- package/server/service/patch-entry/patch-entry.ts +89 -0
- package/server/service/prefab-registry.test.ts +172 -0
- package/server/service/prefab-registry.ts +332 -0
- package/server/service/styling/clear-tools.test.ts +164 -0
- package/server/service/styling/clear-tools.ts +69 -0
- package/server/service/styling/color-utils.test.ts +130 -0
- package/server/service/styling/color-utils.ts +226 -0
- package/server/service/styling/copy-tools.test.ts +152 -0
- package/server/service/styling/copy-tools.ts +115 -0
- package/server/service/styling/effect-tools.test.ts +277 -0
- package/server/service/styling/effect-tools.ts +124 -0
- package/server/service/styling/fill-tools.test.ts +331 -0
- package/server/service/styling/fill-tools.ts +293 -0
- package/server/service/styling/material-tools.test.ts +294 -0
- package/server/service/styling/material-tools.ts +114 -0
- package/server/service/styling/read-tools.test.ts +78 -0
- package/server/service/styling/read-tools.ts +67 -0
- package/server/service/styling/registry.test.ts +203 -0
- package/server/service/styling/registry.ts +423 -0
- package/server/service/styling/stroke-tools.test.ts +245 -0
- package/server/service/styling/stroke-tools.ts +145 -0
- package/server/service/styling/text-tools.test.ts +253 -0
- package/server/service/styling/text-tools.ts +131 -0
- package/server/service/styling/tier1-tools.test.ts +300 -0
- package/server/service/subscribe-auth.test.ts +57 -0
- package/server/service/subscribe-auth.ts +34 -0
- package/server/service/types.ts +235 -0
- package/server/service/validation/board-model-schema.test.ts +778 -0
- package/server/service/validation/board-model-schema.ts +446 -0
- package/server/service/validation/tool-validation.ts +228 -0
- package/things-factory.config.js +1 -0
- package/translations/en.json +49 -0
- package/translations/ja.json +48 -0
- package/translations/ko.json +49 -0
- package/translations/ms.json +48 -0
- package/translations/zh.json +48 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <ox-mention-popup> — Mention 후보 리스트 popup overlay.
|
|
3
|
+
*
|
|
4
|
+
* board-ai-chat 의 textarea 위에 absolute 로 띄움. plain `<textarea>` + popup 분리 패턴
|
|
5
|
+
* (한국어 IME 함정 회피 — contenteditable 사용 안 함).
|
|
6
|
+
*
|
|
7
|
+
* 키보드 네비게이션은 호스트 (board-ai-chat) 가 textarea keydown 으로 처리하고
|
|
8
|
+
* activeIndex 만 동기화. 마우스 클릭은 popup 자체에서 처리.
|
|
9
|
+
*
|
|
10
|
+
* Pure helper (extractMentionContext / filterMentionCandidates 등) 는
|
|
11
|
+
* `mention-popup-helpers.ts` 에 분리 — 단위 테스트 가능.
|
|
12
|
+
*/
|
|
13
|
+
import { LitElement, css, html } from 'lit'
|
|
14
|
+
import { customElement, property } from 'lit/decorators.js'
|
|
15
|
+
import type { FilterResult, MentionCandidate } from './mention-popup-helpers.js'
|
|
16
|
+
|
|
17
|
+
export type {
|
|
18
|
+
MentionCandidate,
|
|
19
|
+
FilterResult
|
|
20
|
+
} from './mention-popup-helpers.js'
|
|
21
|
+
export {
|
|
22
|
+
buildMentionCandidates,
|
|
23
|
+
buildCatalogCandidates,
|
|
24
|
+
buildSlashCandidates,
|
|
25
|
+
buildUserCandidates,
|
|
26
|
+
extractMentionContext,
|
|
27
|
+
filterMentionCandidates,
|
|
28
|
+
applyMentionSelection,
|
|
29
|
+
resolveMentionToken,
|
|
30
|
+
type CatalogEntry,
|
|
31
|
+
type SlashTemplate,
|
|
32
|
+
type ResolveMentionTokenOptions
|
|
33
|
+
} from './mention-popup-helpers.js'
|
|
34
|
+
|
|
35
|
+
/** match type 라벨 — things-scene SearchOverlay 의 MATCH_TYPE_LABELS 와 동일. */
|
|
36
|
+
const MATCH_TYPE_LABEL: Record<string, string> = {
|
|
37
|
+
id: 'ID',
|
|
38
|
+
name: 'Name',
|
|
39
|
+
tag: 'Tag',
|
|
40
|
+
type: 'Type',
|
|
41
|
+
class: 'Class',
|
|
42
|
+
text: 'Text',
|
|
43
|
+
data: 'Data',
|
|
44
|
+
property: 'Prop',
|
|
45
|
+
description: 'Desc'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@customElement('ox-mention-popup')
|
|
49
|
+
export class OxMentionPopup extends LitElement {
|
|
50
|
+
@property({ type: Array }) results: FilterResult[] = []
|
|
51
|
+
@property({ type: Number }) activeIndex = 0
|
|
52
|
+
/** filter cap 후 표시 안 된 추가 매칭 수 ("...and N more" 표기용) */
|
|
53
|
+
@property({ type: Number }) overflow = 0
|
|
54
|
+
|
|
55
|
+
static styles = css`
|
|
56
|
+
:host {
|
|
57
|
+
display: block;
|
|
58
|
+
position: absolute;
|
|
59
|
+
z-index: 1000;
|
|
60
|
+
background: var(--md-sys-color-surface, #ffffff);
|
|
61
|
+
border: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
|
|
62
|
+
border-radius: 6px;
|
|
63
|
+
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.10);
|
|
64
|
+
max-height: 220px;
|
|
65
|
+
overflow-y: auto;
|
|
66
|
+
/* 폭은 호스트가 left/right 로 결정. */
|
|
67
|
+
}
|
|
68
|
+
.empty {
|
|
69
|
+
padding: 8px 10px;
|
|
70
|
+
color: var(--md-sys-color-outline, #94a3b8);
|
|
71
|
+
font-size: 11px;
|
|
72
|
+
}
|
|
73
|
+
/* dense 한 단일 행 — scene Finder 톤. label / hint / type-chip inline. */
|
|
74
|
+
.item {
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
gap: 6px;
|
|
78
|
+
padding: 4px 8px;
|
|
79
|
+
cursor: pointer;
|
|
80
|
+
font-size: 11.5px;
|
|
81
|
+
line-height: 1.4;
|
|
82
|
+
color: var(--md-sys-color-on-surface, #0f172a);
|
|
83
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant, #f1f5f9);
|
|
84
|
+
}
|
|
85
|
+
.item:last-child {
|
|
86
|
+
border-bottom: 0;
|
|
87
|
+
}
|
|
88
|
+
.item:hover {
|
|
89
|
+
background: var(--md-sys-color-surface-container, #f8fafc);
|
|
90
|
+
}
|
|
91
|
+
.item.active {
|
|
92
|
+
background: var(--md-sys-color-primary-container, rgba(37, 99, 235, 0.10));
|
|
93
|
+
}
|
|
94
|
+
.match-badge {
|
|
95
|
+
flex-shrink: 0;
|
|
96
|
+
background: var(--md-sys-color-primary-container, rgba(37, 99, 235, 0.15));
|
|
97
|
+
color: var(--md-sys-color-on-primary-container, #1e40af);
|
|
98
|
+
padding: 0 5px;
|
|
99
|
+
border-radius: 3px;
|
|
100
|
+
font-size: 9px;
|
|
101
|
+
font-weight: 600;
|
|
102
|
+
letter-spacing: 0.04em;
|
|
103
|
+
min-width: 32px;
|
|
104
|
+
text-align: center;
|
|
105
|
+
}
|
|
106
|
+
/* badge 가 비어있을 때 (matchType=label) — 자리는 reserve 해서 모든 행 정렬 일치 */
|
|
107
|
+
.match-badge.invisible {
|
|
108
|
+
visibility: hidden;
|
|
109
|
+
}
|
|
110
|
+
.label {
|
|
111
|
+
flex: 1;
|
|
112
|
+
min-width: 0;
|
|
113
|
+
font-weight: 500;
|
|
114
|
+
overflow: hidden;
|
|
115
|
+
text-overflow: ellipsis;
|
|
116
|
+
white-space: nowrap;
|
|
117
|
+
color: var(--md-sys-color-on-surface, #0f172a);
|
|
118
|
+
}
|
|
119
|
+
.hint {
|
|
120
|
+
font-size: 10px;
|
|
121
|
+
color: var(--md-sys-color-outline, #94a3b8);
|
|
122
|
+
flex-shrink: 0;
|
|
123
|
+
white-space: nowrap;
|
|
124
|
+
max-width: 45%;
|
|
125
|
+
overflow: hidden;
|
|
126
|
+
text-overflow: ellipsis;
|
|
127
|
+
}
|
|
128
|
+
.type {
|
|
129
|
+
flex-shrink: 0;
|
|
130
|
+
background: var(--md-sys-color-surface-container, #f1f5f9);
|
|
131
|
+
padding: 0 5px;
|
|
132
|
+
border-radius: 3px;
|
|
133
|
+
font-size: 9.5px;
|
|
134
|
+
color: var(--md-sys-color-outline, #94a3b8);
|
|
135
|
+
letter-spacing: 0.02em;
|
|
136
|
+
}
|
|
137
|
+
/* 상태 footer — 'N matches' 카운터 + cap 초과 안내 */
|
|
138
|
+
.status {
|
|
139
|
+
padding: 3px 8px;
|
|
140
|
+
font-size: 9.5px;
|
|
141
|
+
color: var(--md-sys-color-outline, #94a3b8);
|
|
142
|
+
border-top: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
|
|
143
|
+
background: var(--md-sys-color-surface-container-low, #fafbfc);
|
|
144
|
+
display: flex;
|
|
145
|
+
justify-content: space-between;
|
|
146
|
+
gap: 8px;
|
|
147
|
+
}
|
|
148
|
+
.more {
|
|
149
|
+
color: var(--md-sys-color-outline, #94a3b8);
|
|
150
|
+
}
|
|
151
|
+
`
|
|
152
|
+
|
|
153
|
+
render() {
|
|
154
|
+
if (this.results.length === 0) {
|
|
155
|
+
return html`<div class="empty">매칭 없음</div>`
|
|
156
|
+
}
|
|
157
|
+
const totalShown = this.results.length
|
|
158
|
+
const totalAll = totalShown + this.overflow
|
|
159
|
+
return html`
|
|
160
|
+
${this.results.map((r, i) => {
|
|
161
|
+
// 행 layout:
|
|
162
|
+
// [matchType-badge] [type-chip] [label] [secondary hint when 의미 있을 때]
|
|
163
|
+
// - label = 항상 사람-친화적 1차 식별 (component=id, user=name, catalog=name 등)
|
|
164
|
+
// - secondary hint = matchValue 가 label 과 다른 경우 (예: tag/text 매칭) 또는
|
|
165
|
+
// user 의 email 같은 부가 정보. 없으면 생략.
|
|
166
|
+
const matchType = r.matchType
|
|
167
|
+
const badgeText = matchType ? MATCH_TYPE_LABEL[matchType] ?? '' : ''
|
|
168
|
+
const label = r.candidate.label
|
|
169
|
+
// matchValue 가 label 과 같으면 redundant 라 hint 안 함
|
|
170
|
+
const matchHint = r.matchValue && r.matchValue !== label ? r.matchValue : ''
|
|
171
|
+
// user 후보이고 hint 가 비어있으면 email 을 hint 로 (보조 식별)
|
|
172
|
+
const fallbackHint =
|
|
173
|
+
!matchHint && r.candidate.type === 'user' && r.candidate.email !== label
|
|
174
|
+
? r.candidate.email ?? ''
|
|
175
|
+
: ''
|
|
176
|
+
const hint = matchHint || fallbackHint
|
|
177
|
+
return html`
|
|
178
|
+
<div
|
|
179
|
+
class="item ${i === this.activeIndex ? 'active' : ''}"
|
|
180
|
+
@mouseenter=${() => this._setActive(i)}
|
|
181
|
+
@mousedown=${(e: MouseEvent) => this._select(e, r.candidate)}>
|
|
182
|
+
<span class="match-badge ${badgeText ? '' : 'invisible'}"
|
|
183
|
+
>${badgeText || '·'}</span
|
|
184
|
+
>
|
|
185
|
+
<span class="type">${r.candidate.type}</span>
|
|
186
|
+
<span class="label">${label}</span>
|
|
187
|
+
${hint ? html`<span class="hint">${hint}</span>` : ''}
|
|
188
|
+
</div>
|
|
189
|
+
`
|
|
190
|
+
})}
|
|
191
|
+
<div class="status">
|
|
192
|
+
<span>${totalShown} / ${totalAll} matches</span>
|
|
193
|
+
${this.overflow > 0
|
|
194
|
+
? html`<span class="more">+${this.overflow} more — narrow query</span>`
|
|
195
|
+
: ''}
|
|
196
|
+
</div>
|
|
197
|
+
`
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private _select(e: MouseEvent, candidate: MentionCandidate) {
|
|
201
|
+
e.preventDefault() // textarea blur 방지
|
|
202
|
+
this.dispatchEvent(
|
|
203
|
+
new CustomEvent('mention-select', {
|
|
204
|
+
detail: { candidate },
|
|
205
|
+
bubbles: true,
|
|
206
|
+
composed: true
|
|
207
|
+
})
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** 마우스 hover 로 active 이동 — host 가 받아 keyboard nav 와 동일하게 처리. */
|
|
212
|
+
private _setActive(index: number) {
|
|
213
|
+
if (index === this.activeIndex) return
|
|
214
|
+
this.dispatchEvent(
|
|
215
|
+
new CustomEvent('mention-active-change', {
|
|
216
|
+
detail: { index },
|
|
217
|
+
bubbles: true,
|
|
218
|
+
composed: true
|
|
219
|
+
})
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** activeIndex 가 바뀌면 해당 항목을 view 로 스크롤 — popup 의 overflow-y:auto
|
|
224
|
+
* 영역에서 키보드 네비 시 active 가 가려지지 않도록. */
|
|
225
|
+
protected updated(changed: Map<string, unknown>) {
|
|
226
|
+
if (changed.has('activeIndex') || changed.has('results')) {
|
|
227
|
+
const items = this.renderRoot.querySelectorAll('.item')
|
|
228
|
+
const active = items[this.activeIndex] as HTMLElement | undefined
|
|
229
|
+
active?.scrollIntoView({ block: 'nearest' })
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
declare global {
|
|
235
|
+
interface HTMLElementTagNameMap {
|
|
236
|
+
'ox-mention-popup': OxMentionPopup
|
|
237
|
+
}
|
|
238
|
+
}
|
package/client/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @things-factory/board-ai — client entries.
|
|
3
|
+
*
|
|
4
|
+
* - `<ox-board-ai-chat>`: 자연어 채팅 컴포넌트 (Lit). board-modeller 또는 워크스페이스에서 임베드.
|
|
5
|
+
*
|
|
6
|
+
* 향후 추가 예정:
|
|
7
|
+
* - `<ox-board-workspace>`: editor / ai / split 모드 전환 컨테이너
|
|
8
|
+
* - 모드 전환 애니메이션 + 단축키 + 자동 전환 룰
|
|
9
|
+
*/
|
|
10
|
+
export * from './components/board-ai-chat.js'
|
|
11
|
+
export * from './utils/board-edit-patch.js'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig-base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"experimentalDecorators": true,
|
|
5
|
+
"skipLibCheck": true,
|
|
6
|
+
"strict": true,
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"module": "esnext",
|
|
9
|
+
"outDir": "../dist-client",
|
|
10
|
+
"baseUrl": "./"
|
|
11
|
+
},
|
|
12
|
+
"include": ["./**/*"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side BoardEditPatch types + applier.
|
|
3
|
+
*
|
|
4
|
+
* NOTE: 동일 정의가 server/service/types.ts + apply-patch.ts 에 있음.
|
|
5
|
+
* client 번들이 server 코드를 import 하지 않도록 분리. 변경 시 양쪽 동기화 필요.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 컴포넌트 타깃팅은 항상 `refid` (things-scene universal numeric handle).
|
|
10
|
+
* `id` 는 데이터 바인딩 이름이며 unique 가 아니므로 targeting 에 사용하지 않는다.
|
|
11
|
+
*
|
|
12
|
+
* 보드는 최상위 부모 — 자체 속성 (fillStyle / width / height / name 등) 을 갖고,
|
|
13
|
+
* `modifyBoard` 로 변경. 자식 컴포넌트 변경 (`modify`) 와 분리.
|
|
14
|
+
*
|
|
15
|
+
* Phase 2 — Scene 조작 op (align/distribute/group/ungroup/zorder). model 차원
|
|
16
|
+
* 시뮬레이션은 noop 이고 things-scene API 가 정본.
|
|
17
|
+
*/
|
|
18
|
+
export type AlignDirection =
|
|
19
|
+
| 'left'
|
|
20
|
+
| 'right'
|
|
21
|
+
| 'center'
|
|
22
|
+
| 'top'
|
|
23
|
+
| 'middle'
|
|
24
|
+
| 'bottom'
|
|
25
|
+
|
|
26
|
+
export type DistributeAxis = 'horizontal' | 'vertical'
|
|
27
|
+
|
|
28
|
+
export type ZorderDirection = 'front' | 'back' | 'forward' | 'backward'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Sugar layout — `arrange` op 의 layout 종류.
|
|
32
|
+
* Server-side types.ts 의 ArrangeLayout 와 일치 — drift test 가 검증.
|
|
33
|
+
*/
|
|
34
|
+
export type ArrangeLayout =
|
|
35
|
+
| { type: 'grid'; cols: number; gap?: number; anchor?: { left: number; top: number } }
|
|
36
|
+
| {
|
|
37
|
+
type: 'row'
|
|
38
|
+
gap?: number
|
|
39
|
+
anchor?: { left: number; top: number }
|
|
40
|
+
align?: 'start' | 'center' | 'end'
|
|
41
|
+
}
|
|
42
|
+
| {
|
|
43
|
+
type: 'column'
|
|
44
|
+
gap?: number
|
|
45
|
+
anchor?: { left: number; top: number }
|
|
46
|
+
align?: 'start' | 'center' | 'end'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type BoardEditOp =
|
|
50
|
+
| { op: 'add'; component: any }
|
|
51
|
+
| { op: 'remove'; refid: number }
|
|
52
|
+
| { op: 'modify'; refid: number; patch: any }
|
|
53
|
+
| { op: 'modifyBoard'; patch: any }
|
|
54
|
+
| { op: 'replace'; board: any }
|
|
55
|
+
| { op: 'align'; refids: number[]; direction: AlignDirection }
|
|
56
|
+
| { op: 'distribute'; refids: number[]; axis: DistributeAxis }
|
|
57
|
+
| { op: 'group'; refids: number[] }
|
|
58
|
+
| { op: 'ungroup'; refid: number }
|
|
59
|
+
| { op: 'zorder'; refid: number; direction: ZorderDirection }
|
|
60
|
+
| { op: 'arrange'; refids: number[]; layout: ArrangeLayout }
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* C-1 — Scene 조작 action (ephemeral). 모델 변경 X, undo 영향 X.
|
|
64
|
+
* 호스트가 board-action-execute 이벤트로 받아서 things-scene API 직접 호출.
|
|
65
|
+
*/
|
|
66
|
+
export type BoardActionOp =
|
|
67
|
+
| { action: 'selectComponents'; refids: number[] }
|
|
68
|
+
| { action: 'centerToComponent'; refid: number; animated?: boolean }
|
|
69
|
+
| { action: 'fitToView'; mode?: 'fit' | 'ratio' | 'width' | 'height' }
|
|
70
|
+
| { action: 'setSceneMode'; mode: 'edit' | 'view' }
|
|
71
|
+
| { action: 'highlightComponents'; refids: number[] }
|
|
72
|
+
|
|
73
|
+
export interface BoardEditPatch {
|
|
74
|
+
ops: BoardEditOp[]
|
|
75
|
+
summary: string
|
|
76
|
+
confidence: number
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface PatchApplyReport {
|
|
80
|
+
/** 패치 적용 후 보드. 모든 op 가 noop 이어도 입력 그대로 반환. */
|
|
81
|
+
board: any
|
|
82
|
+
/** 실제로 보드를 바꾼 op 들. */
|
|
83
|
+
applied: BoardEditOp[]
|
|
84
|
+
/** id 매칭 실패 등으로 noop 이 된 op 들 — 호출자가 사용자에게 알릴 단서. */
|
|
85
|
+
missed: BoardEditOp[]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const EMPTY_BOARD = { width: 1000, height: 600, components: [] as any[] }
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Patch 를 BoardModel 에 적용 (pure, board 를 mutate 하지 않음).
|
|
92
|
+
*/
|
|
93
|
+
export function applyBoardEditPatch(board: any | undefined, patch: BoardEditPatch): any {
|
|
94
|
+
return applyBoardEditPatchVerbose(board, patch).board
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Verbose 변형 — 각 op 의 적용 여부를 보고.
|
|
99
|
+
*
|
|
100
|
+
* `modify` 와 `remove` 는 id 가 보드에 없으면 silent no-op 이 된다. LLM 이 잘못된
|
|
101
|
+
* id 를 만들어 보내면 사용자에게 "수정했습니다" 라고 답하지만 실제로는 아무 변화도
|
|
102
|
+
* 없는 상황이 발생 — 호스트가 missed 를 보고 사용자에게 경고할 수 있도록 별도
|
|
103
|
+
* entry point 제공.
|
|
104
|
+
*/
|
|
105
|
+
export function applyBoardEditPatchVerbose(
|
|
106
|
+
board: any | undefined,
|
|
107
|
+
patch: BoardEditPatch
|
|
108
|
+
): PatchApplyReport {
|
|
109
|
+
let result: any = board ?? EMPTY_BOARD
|
|
110
|
+
const applied: BoardEditOp[] = []
|
|
111
|
+
const missed: BoardEditOp[] = []
|
|
112
|
+
|
|
113
|
+
for (const op of patch.ops) {
|
|
114
|
+
if (SCENE_ONLY_OPS.has(op.op)) {
|
|
115
|
+
// scene-only — model 차원 noop, scene 차원 적용. applied 분류.
|
|
116
|
+
applied.push(op)
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
const next = applyOp(result, op)
|
|
120
|
+
// componentsUnchanged 는 components 배열 동일 + width/height/fillStyle 동일 만 본다.
|
|
121
|
+
// modify / remove 의 silent no-op (ghost refid) 검출 전용 — modifyBoard 가 sky
|
|
122
|
+
// 등 다른 root 키만 바꿀 때 false negative 로 missed 분류되는 회귀 방지.
|
|
123
|
+
const componentMutating = op.op === 'modify' || op.op === 'remove'
|
|
124
|
+
if (next === result || (componentMutating && componentsUnchanged(result, next))) {
|
|
125
|
+
missed.push(op)
|
|
126
|
+
} else {
|
|
127
|
+
applied.push(op)
|
|
128
|
+
result = next
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { board: result, applied, missed }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function componentsUnchanged(prev: any, next: any): boolean {
|
|
136
|
+
// applyOp 는 항상 새 객체를 만든다. 따라서 reference 비교가 안 되고 내용 비교 필요.
|
|
137
|
+
// components 는 map/filter 결과 reference 도 다를 수 있으므로 length + 요소 ref 비교.
|
|
138
|
+
const a = prev.components ?? []
|
|
139
|
+
const b = next.components ?? []
|
|
140
|
+
if (a.length !== b.length) return false
|
|
141
|
+
for (let i = 0; i < a.length; i++) {
|
|
142
|
+
if (a[i] !== b[i]) return false
|
|
143
|
+
}
|
|
144
|
+
return prev.width === next.width && prev.height === next.height && prev.fillStyle === next.fillStyle
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const SCENE_ONLY_OPS = new Set<BoardEditOp['op']>([
|
|
148
|
+
'align',
|
|
149
|
+
'distribute',
|
|
150
|
+
'group',
|
|
151
|
+
'ungroup',
|
|
152
|
+
'zorder',
|
|
153
|
+
'arrange'
|
|
154
|
+
])
|
|
155
|
+
|
|
156
|
+
function applyOp(board: any, op: BoardEditOp): any {
|
|
157
|
+
switch (op.op) {
|
|
158
|
+
case 'replace':
|
|
159
|
+
return op.board
|
|
160
|
+
case 'add':
|
|
161
|
+
return { ...board, components: [...(board.components || []), op.component] }
|
|
162
|
+
case 'remove':
|
|
163
|
+
// 자식 (group/container 안) refid 도 매칭 — deep search.
|
|
164
|
+
return {
|
|
165
|
+
...board,
|
|
166
|
+
components: removeComponentDeep(board.components || [], op.refid)
|
|
167
|
+
}
|
|
168
|
+
case 'modify':
|
|
169
|
+
// 자식 refid 도 매칭 — group/container 안의 컴포넌트 수정 가능.
|
|
170
|
+
return {
|
|
171
|
+
...board,
|
|
172
|
+
components: modifyComponentDeep(board.components || [], op.refid, op.patch)
|
|
173
|
+
}
|
|
174
|
+
case 'modifyBoard': {
|
|
175
|
+
const patch = { ...(op.patch || {}) }
|
|
176
|
+
delete patch.components // 자식 변경은 별도 op
|
|
177
|
+
return mergeComponent(board, patch) // 최상위 board 자체에 deep-merge
|
|
178
|
+
}
|
|
179
|
+
case 'align':
|
|
180
|
+
case 'distribute':
|
|
181
|
+
case 'group':
|
|
182
|
+
case 'ungroup':
|
|
183
|
+
case 'zorder':
|
|
184
|
+
case 'arrange':
|
|
185
|
+
// scene-only — model 차원 noop. things-scene API 가 정본.
|
|
186
|
+
return board
|
|
187
|
+
default:
|
|
188
|
+
return board
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 주어진 board 상태에서 op 의 inverse 를 계산.
|
|
194
|
+
*
|
|
195
|
+
* Revert 기능 — patch 적용 직전 board + op 로 역연산을 만든다. 호스트가
|
|
196
|
+
* 누적해두면 나중에 역순 실행만으로 복원.
|
|
197
|
+
*
|
|
198
|
+
* add 의 inverse 는 새로 발급될 refid 를 알아야 → 모델 단계에서 계산 불가.
|
|
199
|
+
* 호스트가 scene.add 직후 refid 를 캡처해 직접 만들 것.
|
|
200
|
+
*/
|
|
201
|
+
export function computeInverseOp(board: any, op: BoardEditOp): BoardEditOp | null {
|
|
202
|
+
if (!board) return null
|
|
203
|
+
const components = board.components ?? []
|
|
204
|
+
|
|
205
|
+
switch (op.op) {
|
|
206
|
+
case 'add':
|
|
207
|
+
return null
|
|
208
|
+
|
|
209
|
+
case 'remove': {
|
|
210
|
+
// 자식 refid 도 매칭 (deep search)
|
|
211
|
+
const target = findComponentDeep(components, op.refid)
|
|
212
|
+
if (!target) return null
|
|
213
|
+
return { op: 'add', component: JSON.parse(JSON.stringify(target)) }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
case 'modify': {
|
|
217
|
+
// 자식 refid 도 매칭 (deep search)
|
|
218
|
+
const target = findComponentDeep(components, op.refid)
|
|
219
|
+
if (!target) return null
|
|
220
|
+
const oldValues: any = {}
|
|
221
|
+
for (const k of Object.keys(op.patch || {})) {
|
|
222
|
+
const v = (target as any)[k]
|
|
223
|
+
oldValues[k] = v === undefined ? null : JSON.parse(JSON.stringify(v))
|
|
224
|
+
}
|
|
225
|
+
return { op: 'modify', refid: op.refid, patch: oldValues }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
case 'modifyBoard': {
|
|
229
|
+
const oldValues: any = {}
|
|
230
|
+
const patch = op.patch || {}
|
|
231
|
+
for (const k of Object.keys(patch)) {
|
|
232
|
+
if (k === 'components') continue
|
|
233
|
+
const v = (board as any)[k]
|
|
234
|
+
oldValues[k] = v === undefined ? null : JSON.parse(JSON.stringify(v))
|
|
235
|
+
}
|
|
236
|
+
return { op: 'modifyBoard', patch: oldValues }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
case 'replace':
|
|
240
|
+
return { op: 'replace', board: JSON.parse(JSON.stringify(board)) }
|
|
241
|
+
|
|
242
|
+
case 'zorder': {
|
|
243
|
+
// forward ↔ backward / front ↔ back. front/back 의 inverse 는 정확하지
|
|
244
|
+
// 않을 수 있음 (best-effort) — 정확 inverse 는 호스트가 zorder 직전 index
|
|
245
|
+
// 를 캡처해서 명시 modify 시퀀스로 만들어야.
|
|
246
|
+
const opp: Record<string, 'front' | 'back' | 'forward' | 'backward'> = {
|
|
247
|
+
forward: 'backward',
|
|
248
|
+
backward: 'forward',
|
|
249
|
+
front: 'back',
|
|
250
|
+
back: 'front'
|
|
251
|
+
}
|
|
252
|
+
const dir = opp[op.direction]
|
|
253
|
+
if (!dir) return null
|
|
254
|
+
return { op: 'zorder', refid: op.refid, direction: dir }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
case 'align':
|
|
258
|
+
case 'distribute':
|
|
259
|
+
case 'group':
|
|
260
|
+
case 'ungroup':
|
|
261
|
+
case 'arrange':
|
|
262
|
+
// scene 호출 후에야 변경 결과 알 수 있음. 호스트가 직접 inverse 만든다.
|
|
263
|
+
return null
|
|
264
|
+
|
|
265
|
+
default:
|
|
266
|
+
return null
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 컴포넌트에 부분 patch 를 적용 (deep merge).
|
|
272
|
+
* threeD 등 nested object 는 deep merge — 색만 바꾸려고 했을 때 geometry 까지 사라지지 않도록.
|
|
273
|
+
*
|
|
274
|
+
* patch 내 `null` 의 시맨틱 — **"이 키를 제거하라"**:
|
|
275
|
+
* - fillStyle/strokeStyle 처럼 `string | object | undefined` 인 필드는 null 로
|
|
276
|
+
* set 하면 typeof null === 'object' 함정에 빠져 다운스트림 (3D 텍스처 등) 이 크래시.
|
|
277
|
+
* - computeInverseOp 가 base 에 키가 없던 (`undefined`) 경우 inverse 를 null 로
|
|
278
|
+
* 발급하므로, 이 시맨틱과 정확히 일치 — inverse 가 자연스럽게 올바로 동작.
|
|
279
|
+
*
|
|
280
|
+
* host (board-modeller-page) 에서 things-scene 의 component.set(merged) 호출 전에 사용.
|
|
281
|
+
*/
|
|
282
|
+
export function mergeComponent(base: any, patch: any): any {
|
|
283
|
+
const out: any = { ...base }
|
|
284
|
+
for (const key of Object.keys(patch)) {
|
|
285
|
+
const patchVal = patch[key]
|
|
286
|
+
if (patchVal === null) {
|
|
287
|
+
delete out[key]
|
|
288
|
+
} else if (isPlainObject(base[key]) && isPlainObject(patchVal)) {
|
|
289
|
+
out[key] = deepMerge(base[key], patchVal)
|
|
290
|
+
} else {
|
|
291
|
+
out[key] = patchVal
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return out
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function deepMerge(a: any, b: any): any {
|
|
298
|
+
const out: any = { ...a }
|
|
299
|
+
for (const key of Object.keys(b)) {
|
|
300
|
+
const bv = b[key]
|
|
301
|
+
if (bv === null) {
|
|
302
|
+
delete out[key]
|
|
303
|
+
} else if (isPlainObject(a[key]) && isPlainObject(bv)) {
|
|
304
|
+
out[key] = deepMerge(a[key], bv)
|
|
305
|
+
} else {
|
|
306
|
+
out[key] = bv
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return out
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function isPlainObject(v: any): boolean {
|
|
313
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ── 자식 컴포넌트 (group/container 안) 까지 매칭 — deep search ─
|
|
317
|
+
// server/service/apply-patch.ts 와 동일 로직. drift test 가 동등성 보장.
|
|
318
|
+
|
|
319
|
+
function findComponentDeep(components: any[], refid: number): any | undefined {
|
|
320
|
+
for (const c of components) {
|
|
321
|
+
if (!c) continue
|
|
322
|
+
if (c.refid === refid) return c
|
|
323
|
+
if (Array.isArray(c.components) && c.components.length > 0) {
|
|
324
|
+
const sub = findComponentDeep(c.components, refid)
|
|
325
|
+
if (sub) return sub
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return undefined
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function modifyComponentDeep(components: any[], refid: number, patch: any): any[] {
|
|
332
|
+
let changed = false
|
|
333
|
+
const out = components.map(c => {
|
|
334
|
+
if (!c) return c
|
|
335
|
+
if (c.refid === refid) {
|
|
336
|
+
changed = true
|
|
337
|
+
return mergeComponent(c, patch)
|
|
338
|
+
}
|
|
339
|
+
if (Array.isArray(c.components) && c.components.length > 0) {
|
|
340
|
+
const updated = modifyComponentDeep(c.components, refid, patch)
|
|
341
|
+
if (updated !== c.components) {
|
|
342
|
+
changed = true
|
|
343
|
+
return { ...c, components: updated }
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return c
|
|
347
|
+
})
|
|
348
|
+
return changed ? out : components
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function removeComponentDeep(components: any[], refid: number): any[] {
|
|
352
|
+
let changed = false
|
|
353
|
+
const out: any[] = []
|
|
354
|
+
for (const c of components) {
|
|
355
|
+
if (!c) {
|
|
356
|
+
out.push(c)
|
|
357
|
+
continue
|
|
358
|
+
}
|
|
359
|
+
if (c.refid === refid) {
|
|
360
|
+
changed = true
|
|
361
|
+
continue
|
|
362
|
+
}
|
|
363
|
+
if (Array.isArray(c.components) && c.components.length > 0) {
|
|
364
|
+
const updated = removeComponentDeep(c.components, refid)
|
|
365
|
+
if (updated !== c.components) {
|
|
366
|
+
changed = true
|
|
367
|
+
out.push({ ...c, components: updated })
|
|
368
|
+
continue
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
out.push(c)
|
|
372
|
+
}
|
|
373
|
+
return changed ? out : components
|
|
374
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
/*
|
|
3
|
+
Board AI Configuration
|
|
4
|
+
|
|
5
|
+
Settings for AI-driven board modeling. Override in your application's config
|
|
6
|
+
to tune behavior. Requires the `aiClient` key (see @things-factory/ai-client-base
|
|
7
|
+
config/config.development.js).
|
|
8
|
+
*/
|
|
9
|
+
boardAI: {
|
|
10
|
+
/*
|
|
11
|
+
Patch confidence threshold for automatic application.
|
|
12
|
+
Patches with confidence >= this value are auto-applied to the board;
|
|
13
|
+
below this threshold, the UI requests user review before applying.
|
|
14
|
+
|
|
15
|
+
Range: 0..1
|
|
16
|
+
*/
|
|
17
|
+
patchAutoApplyConfidence: 0.85,
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
Maximum number of recent messages included in LLM context for each chat() call.
|
|
21
|
+
Older messages will be compressed into ChatSession.lastSummary (planned).
|
|
22
|
+
|
|
23
|
+
Higher = better long-term memory, more tokens per call.
|
|
24
|
+
*/
|
|
25
|
+
maxContextMessages: 50,
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
Default LLM options for chat() calls. Overridable per-request via mutation input.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
defaultChatOptions: {
|
|
32
|
+
model: 'claude-sonnet-4-6',
|
|
33
|
+
temperature: 0.4,
|
|
34
|
+
maxTokens: 4096
|
|
35
|
+
}
|
|
36
|
+
*/
|
|
37
|
+
defaultChatOptions: {
|
|
38
|
+
// model: undefined,
|
|
39
|
+
// temperature: undefined,
|
|
40
|
+
// maxTokens: 4096
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
/*
|
|
3
|
+
Board AI Configuration — Production
|
|
4
|
+
|
|
5
|
+
See config.development.js for the full reference. Production typically uses
|
|
6
|
+
the same defaults; tune only when telemetry shows specific need.
|
|
7
|
+
*/
|
|
8
|
+
boardAI: {
|
|
9
|
+
patchAutoApplyConfidence: 0.85,
|
|
10
|
+
maxContextMessages: 50,
|
|
11
|
+
defaultChatOptions: {
|
|
12
|
+
// maxTokens: 4096
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|