@treenity/mods 3.0.1
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/board/board.test.ts +212 -0
- package/board/client.ts +1 -0
- package/board/seed.ts +26 -0
- package/board/server.ts +5 -0
- package/board/types.ts +87 -0
- package/board/view.tsx +574 -0
- package/brahman/CLAUDE.md +18 -0
- package/brahman/brahman.test.ts +855 -0
- package/brahman/client.ts +2 -0
- package/brahman/helpers.ts +374 -0
- package/brahman/server.ts +2 -0
- package/brahman/service.ts +328 -0
- package/brahman/types.ts +727 -0
- package/brahman/view.tsx +73 -0
- package/brahman/views/action-cards.tsx +615 -0
- package/brahman/views/bot-view.tsx +76 -0
- package/brahman/views/chat-editor.tsx +782 -0
- package/brahman/views/chat-preview.tsx +266 -0
- package/brahman/views/menu-editor.tsx +451 -0
- package/brahman/views/page-layout.tsx +285 -0
- package/brahman/views/tstring-input.tsx +84 -0
- package/cafe/CLAUDE.md +7 -0
- package/cafe/seed.ts +8 -0
- package/cafe/server.ts +2 -0
- package/cafe/types.ts +32 -0
- package/canary/seed.ts +8 -0
- package/canary/server.ts +3 -0
- package/canary/service.ts +101 -0
- package/canary/types.ts +16 -0
- package/dist/board/client.d.ts +2 -0
- package/dist/board/client.d.ts.map +1 -0
- package/dist/board/client.js +2 -0
- package/dist/board/client.js.map +1 -0
- package/dist/board/seed.d.ts +2 -0
- package/dist/board/seed.d.ts.map +1 -0
- package/dist/board/seed.js +23 -0
- package/dist/board/seed.js.map +1 -0
- package/dist/board/server.d.ts +3 -0
- package/dist/board/server.d.ts.map +1 -0
- package/dist/board/server.js +5 -0
- package/dist/board/server.js.map +1 -0
- package/dist/board/types.d.ts +45 -0
- package/dist/board/types.d.ts.map +1 -0
- package/dist/board/types.js +82 -0
- package/dist/board/types.js.map +1 -0
- package/dist/board/view.d.ts +2 -0
- package/dist/board/view.d.ts.map +1 -0
- package/dist/board/view.js +254 -0
- package/dist/board/view.js.map +1 -0
- package/dist/brahman/client.d.ts +3 -0
- package/dist/brahman/client.d.ts.map +1 -0
- package/dist/brahman/client.js +3 -0
- package/dist/brahman/client.js.map +1 -0
- package/dist/brahman/helpers.d.ts +51 -0
- package/dist/brahman/helpers.d.ts.map +1 -0
- package/dist/brahman/helpers.js +321 -0
- package/dist/brahman/helpers.js.map +1 -0
- package/dist/brahman/server.d.ts +3 -0
- package/dist/brahman/server.d.ts.map +1 -0
- package/dist/brahman/server.js +3 -0
- package/dist/brahman/server.js.map +1 -0
- package/dist/brahman/service.d.ts +2 -0
- package/dist/brahman/service.d.ts.map +1 -0
- package/dist/brahman/service.js +310 -0
- package/dist/brahman/service.js.map +1 -0
- package/dist/brahman/types.d.ts +335 -0
- package/dist/brahman/types.d.ts.map +1 -0
- package/dist/brahman/types.js +633 -0
- package/dist/brahman/types.js.map +1 -0
- package/dist/brahman/view.d.ts +2 -0
- package/dist/brahman/view.d.ts.map +1 -0
- package/dist/brahman/view.js +47 -0
- package/dist/brahman/view.js.map +1 -0
- package/dist/brahman/views/action-cards.d.ts +60 -0
- package/dist/brahman/views/action-cards.d.ts.map +1 -0
- package/dist/brahman/views/action-cards.js +283 -0
- package/dist/brahman/views/action-cards.js.map +1 -0
- package/dist/brahman/views/bot-view.d.ts +5 -0
- package/dist/brahman/views/bot-view.d.ts.map +1 -0
- package/dist/brahman/views/bot-view.js +14 -0
- package/dist/brahman/views/bot-view.js.map +1 -0
- package/dist/brahman/views/chat-editor.d.ts +5 -0
- package/dist/brahman/views/chat-editor.d.ts.map +1 -0
- package/dist/brahman/views/chat-editor.js +306 -0
- package/dist/brahman/views/chat-editor.js.map +1 -0
- package/dist/brahman/views/chat-preview.d.ts +5 -0
- package/dist/brahman/views/chat-preview.d.ts.map +1 -0
- package/dist/brahman/views/chat-preview.js +145 -0
- package/dist/brahman/views/chat-preview.js.map +1 -0
- package/dist/brahman/views/menu-editor.d.ts +11 -0
- package/dist/brahman/views/menu-editor.d.ts.map +1 -0
- package/dist/brahman/views/menu-editor.js +171 -0
- package/dist/brahman/views/menu-editor.js.map +1 -0
- package/dist/brahman/views/page-layout.d.ts +5 -0
- package/dist/brahman/views/page-layout.d.ts.map +1 -0
- package/dist/brahman/views/page-layout.js +114 -0
- package/dist/brahman/views/page-layout.js.map +1 -0
- package/dist/brahman/views/tstring-input.d.ts +15 -0
- package/dist/brahman/views/tstring-input.d.ts.map +1 -0
- package/dist/brahman/views/tstring-input.js +23 -0
- package/dist/brahman/views/tstring-input.js.map +1 -0
- package/dist/cafe/seed.d.ts +2 -0
- package/dist/cafe/seed.d.ts.map +1 -0
- package/dist/cafe/seed.js +7 -0
- package/dist/cafe/seed.js.map +1 -0
- package/dist/cafe/server.d.ts +3 -0
- package/dist/cafe/server.d.ts.map +1 -0
- package/dist/cafe/server.js +3 -0
- package/dist/cafe/server.js.map +1 -0
- package/dist/cafe/types.d.ts +2 -0
- package/dist/cafe/types.d.ts.map +1 -0
- package/dist/cafe/types.js +31 -0
- package/dist/cafe/types.js.map +1 -0
- package/dist/canary/seed.d.ts +2 -0
- package/dist/canary/seed.d.ts.map +1 -0
- package/dist/canary/seed.js +7 -0
- package/dist/canary/seed.js.map +1 -0
- package/dist/canary/server.d.ts +4 -0
- package/dist/canary/server.d.ts.map +1 -0
- package/dist/canary/server.js +4 -0
- package/dist/canary/server.js.map +1 -0
- package/dist/canary/service.d.ts +2 -0
- package/dist/canary/service.d.ts.map +1 -0
- package/dist/canary/service.js +101 -0
- package/dist/canary/service.js.map +1 -0
- package/dist/canary/types.d.ts +9 -0
- package/dist/canary/types.d.ts.map +1 -0
- package/dist/canary/types.js +13 -0
- package/dist/canary/types.js.map +1 -0
- package/dist/doc/client.d.ts +3 -0
- package/dist/doc/client.d.ts.map +1 -0
- package/dist/doc/client.js +5 -0
- package/dist/doc/client.js.map +1 -0
- package/dist/doc/fs-codec.d.ts +2 -0
- package/dist/doc/fs-codec.d.ts.map +1 -0
- package/dist/doc/fs-codec.js +44 -0
- package/dist/doc/fs-codec.js.map +1 -0
- package/dist/doc/markdown.d.ts +13 -0
- package/dist/doc/markdown.d.ts.map +1 -0
- package/dist/doc/markdown.js +250 -0
- package/dist/doc/markdown.js.map +1 -0
- package/dist/doc/prefab.d.ts +2 -0
- package/dist/doc/prefab.d.ts.map +1 -0
- package/dist/doc/prefab.js +23 -0
- package/dist/doc/prefab.js.map +1 -0
- package/dist/doc/renderers.d.ts +2 -0
- package/dist/doc/renderers.d.ts.map +1 -0
- package/dist/doc/renderers.js +94 -0
- package/dist/doc/renderers.js.map +1 -0
- package/dist/doc/seed.d.ts +2 -0
- package/dist/doc/seed.d.ts.map +1 -0
- package/dist/doc/seed.js +9 -0
- package/dist/doc/seed.js.map +1 -0
- package/dist/doc/server.d.ts +6 -0
- package/dist/doc/server.d.ts.map +1 -0
- package/dist/doc/server.js +6 -0
- package/dist/doc/server.js.map +1 -0
- package/dist/doc/slash-command.d.ts +3 -0
- package/dist/doc/slash-command.d.ts.map +1 -0
- package/dist/doc/slash-command.js +123 -0
- package/dist/doc/slash-command.js.map +1 -0
- package/dist/doc/slash-menu.d.ts +15 -0
- package/dist/doc/slash-menu.d.ts.map +1 -0
- package/dist/doc/slash-menu.js +54 -0
- package/dist/doc/slash-menu.js.map +1 -0
- package/dist/doc/text.d.ts +2 -0
- package/dist/doc/text.d.ts.map +1 -0
- package/dist/doc/text.js +22 -0
- package/dist/doc/text.js.map +1 -0
- package/dist/doc/toolbar.d.ts +5 -0
- package/dist/doc/toolbar.d.ts.map +1 -0
- package/dist/doc/toolbar.js +15 -0
- package/dist/doc/toolbar.js.map +1 -0
- package/dist/doc/treenity-block-view.d.ts +2 -0
- package/dist/doc/treenity-block-view.d.ts.map +1 -0
- package/dist/doc/treenity-block-view.js +37 -0
- package/dist/doc/treenity-block-view.js.map +1 -0
- package/dist/doc/treenity-block.d.ts +3 -0
- package/dist/doc/treenity-block.d.ts.map +1 -0
- package/dist/doc/treenity-block.js +27 -0
- package/dist/doc/treenity-block.js.map +1 -0
- package/dist/doc/types.d.ts +2 -0
- package/dist/doc/types.d.ts.map +1 -0
- package/dist/doc/types.js +10 -0
- package/dist/doc/types.js.map +1 -0
- package/dist/launcher/client.d.ts +5 -0
- package/dist/launcher/client.d.ts.map +1 -0
- package/dist/launcher/client.js +5 -0
- package/dist/launcher/client.js.map +1 -0
- package/dist/launcher/icons.d.ts +2 -0
- package/dist/launcher/icons.d.ts.map +1 -0
- package/dist/launcher/icons.js +57 -0
- package/dist/launcher/icons.js.map +1 -0
- package/dist/launcher/seed.d.ts +2 -0
- package/dist/launcher/seed.d.ts.map +1 -0
- package/dist/launcher/seed.js +36 -0
- package/dist/launcher/seed.js.map +1 -0
- package/dist/launcher/server.d.ts +3 -0
- package/dist/launcher/server.d.ts.map +1 -0
- package/dist/launcher/server.js +3 -0
- package/dist/launcher/server.js.map +1 -0
- package/dist/launcher/types.d.ts +21 -0
- package/dist/launcher/types.d.ts.map +1 -0
- package/dist/launcher/types.js +47 -0
- package/dist/launcher/types.js.map +1 -0
- package/dist/launcher/view.d.ts +2 -0
- package/dist/launcher/view.d.ts.map +1 -0
- package/dist/launcher/view.js +187 -0
- package/dist/launcher/view.js.map +1 -0
- package/dist/launcher/widgets.d.ts +2 -0
- package/dist/launcher/widgets.d.ts.map +1 -0
- package/dist/launcher/widgets.js +73 -0
- package/dist/launcher/widgets.js.map +1 -0
- package/dist/mindmap/branch.d.ts +17 -0
- package/dist/mindmap/branch.d.ts.map +1 -0
- package/dist/mindmap/branch.js +47 -0
- package/dist/mindmap/branch.js.map +1 -0
- package/dist/mindmap/client.d.ts +3 -0
- package/dist/mindmap/client.d.ts.map +1 -0
- package/dist/mindmap/client.js +3 -0
- package/dist/mindmap/client.js.map +1 -0
- package/dist/mindmap/radial-tree.d.ts +14 -0
- package/dist/mindmap/radial-tree.d.ts.map +1 -0
- package/dist/mindmap/radial-tree.js +184 -0
- package/dist/mindmap/radial-tree.js.map +1 -0
- package/dist/mindmap/sidebar.d.ts +8 -0
- package/dist/mindmap/sidebar.d.ts.map +1 -0
- package/dist/mindmap/sidebar.js +24 -0
- package/dist/mindmap/sidebar.js.map +1 -0
- package/dist/mindmap/types.d.ts +8 -0
- package/dist/mindmap/types.d.ts.map +1 -0
- package/dist/mindmap/types.js +10 -0
- package/dist/mindmap/types.js.map +1 -0
- package/dist/mindmap/use-tree-data.d.ts +14 -0
- package/dist/mindmap/use-tree-data.d.ts.map +1 -0
- package/dist/mindmap/use-tree-data.js +95 -0
- package/dist/mindmap/use-tree-data.js.map +1 -0
- package/dist/mindmap/view.d.ts +3 -0
- package/dist/mindmap/view.d.ts.map +1 -0
- package/dist/mindmap/view.js +141 -0
- package/dist/mindmap/view.js.map +1 -0
- package/dist/sensor-demo/client.d.ts +2 -0
- package/dist/sensor-demo/client.d.ts.map +1 -0
- package/dist/sensor-demo/client.js +2 -0
- package/dist/sensor-demo/client.js.map +1 -0
- package/dist/sensor-demo/server.d.ts +2 -0
- package/dist/sensor-demo/server.d.ts.map +1 -0
- package/dist/sensor-demo/server.js +2 -0
- package/dist/sensor-demo/server.js.map +1 -0
- package/dist/sensor-demo/service.d.ts +2 -0
- package/dist/sensor-demo/service.d.ts.map +1 -0
- package/dist/sensor-demo/service.js +27 -0
- package/dist/sensor-demo/service.js.map +1 -0
- package/dist/sensor-demo/types.d.ts +15 -0
- package/dist/sensor-demo/types.d.ts.map +1 -0
- package/dist/sensor-demo/types.js +18 -0
- package/dist/sensor-demo/types.js.map +1 -0
- package/dist/sensor-demo/view.d.ts +2 -0
- package/dist/sensor-demo/view.d.ts.map +1 -0
- package/dist/sensor-demo/view.js +33 -0
- package/dist/sensor-demo/view.js.map +1 -0
- package/dist/sensor-generator/action.d.ts +2 -0
- package/dist/sensor-generator/action.d.ts.map +1 -0
- package/dist/sensor-generator/action.js +23 -0
- package/dist/sensor-generator/action.js.map +1 -0
- package/dist/sensor-generator/client.d.ts +2 -0
- package/dist/sensor-generator/client.d.ts.map +1 -0
- package/dist/sensor-generator/client.js +2 -0
- package/dist/sensor-generator/client.js.map +1 -0
- package/dist/sensor-generator/server.d.ts +2 -0
- package/dist/sensor-generator/server.d.ts.map +1 -0
- package/dist/sensor-generator/server.js +2 -0
- package/dist/sensor-generator/server.js.map +1 -0
- package/dist/sensor-generator/view.d.ts +2 -0
- package/dist/sensor-generator/view.d.ts.map +1 -0
- package/dist/sensor-generator/view.js +64 -0
- package/dist/sensor-generator/view.js.map +1 -0
- package/dist/sim/client.d.ts +2 -0
- package/dist/sim/client.d.ts.map +1 -0
- package/dist/sim/client.js +2 -0
- package/dist/sim/client.js.map +1 -0
- package/dist/sim/seed.d.ts +2 -0
- package/dist/sim/seed.d.ts.map +1 -0
- package/dist/sim/seed.js +50 -0
- package/dist/sim/seed.js.map +1 -0
- package/dist/sim/server.d.ts +3 -0
- package/dist/sim/server.d.ts.map +1 -0
- package/dist/sim/server.js +3 -0
- package/dist/sim/server.js.map +1 -0
- package/dist/sim/service.d.ts +4 -0
- package/dist/sim/service.d.ts.map +1 -0
- package/dist/sim/service.js +528 -0
- package/dist/sim/service.js.map +1 -0
- package/dist/sim/types.d.ts +63 -0
- package/dist/sim/types.d.ts.map +1 -0
- package/dist/sim/types.js +57 -0
- package/dist/sim/types.js.map +1 -0
- package/dist/sim/view.d.ts +2 -0
- package/dist/sim/view.d.ts.map +1 -0
- package/dist/sim/view.js +205 -0
- package/dist/sim/view.js.map +1 -0
- package/dist/table/client.d.ts +4 -0
- package/dist/table/client.d.ts.map +1 -0
- package/dist/table/client.js +4 -0
- package/dist/table/client.js.map +1 -0
- package/dist/table/edit.d.ts +2 -0
- package/dist/table/edit.d.ts.map +1 -0
- package/dist/table/edit.js +115 -0
- package/dist/table/edit.js.map +1 -0
- package/dist/table/server.d.ts +2 -0
- package/dist/table/server.d.ts.map +1 -0
- package/dist/table/server.js +2 -0
- package/dist/table/server.js.map +1 -0
- package/dist/table/types.d.ts +18 -0
- package/dist/table/types.d.ts.map +1 -0
- package/dist/table/types.js +13 -0
- package/dist/table/types.js.map +1 -0
- package/dist/table/use-debounced-sync.d.ts +8 -0
- package/dist/table/use-debounced-sync.d.ts.map +1 -0
- package/dist/table/use-debounced-sync.js +56 -0
- package/dist/table/use-debounced-sync.js.map +1 -0
- package/dist/table/view.d.ts +2 -0
- package/dist/table/view.d.ts.map +1 -0
- package/dist/table/view.js +199 -0
- package/dist/table/view.js.map +1 -0
- package/dist/tasks/server.d.ts +2 -0
- package/dist/tasks/server.d.ts.map +1 -0
- package/dist/tasks/server.js +2 -0
- package/dist/tasks/server.js.map +1 -0
- package/dist/tasks/types.d.ts +22 -0
- package/dist/tasks/types.d.ts.map +1 -0
- package/dist/tasks/types.js +34 -0
- package/dist/tasks/types.js.map +1 -0
- package/dist/three/client.d.ts +2 -0
- package/dist/three/client.d.ts.map +1 -0
- package/dist/three/client.js +2 -0
- package/dist/three/client.js.map +1 -0
- package/dist/three/seed.d.ts +2 -0
- package/dist/three/seed.d.ts.map +1 -0
- package/dist/three/seed.js +45 -0
- package/dist/three/seed.js.map +1 -0
- package/dist/three/server.d.ts +3 -0
- package/dist/three/server.d.ts.map +1 -0
- package/dist/three/server.js +3 -0
- package/dist/three/server.js.map +1 -0
- package/dist/three/types.d.ts +178 -0
- package/dist/three/types.d.ts.map +1 -0
- package/dist/three/types.js +209 -0
- package/dist/three/types.js.map +1 -0
- package/dist/three/view.d.ts +2 -0
- package/dist/three/view.d.ts.map +1 -0
- package/dist/three/view.js +307 -0
- package/dist/three/view.js.map +1 -0
- package/dist/todo/client.d.ts +3 -0
- package/dist/todo/client.d.ts.map +1 -0
- package/dist/todo/client.js +3 -0
- package/dist/todo/client.js.map +1 -0
- package/dist/todo/seed.d.ts +2 -0
- package/dist/todo/seed.d.ts.map +1 -0
- package/dist/todo/seed.js +8 -0
- package/dist/todo/seed.js.map +1 -0
- package/dist/todo/server.d.ts +3 -0
- package/dist/todo/server.d.ts.map +1 -0
- package/dist/todo/server.js +3 -0
- package/dist/todo/server.js.map +1 -0
- package/dist/todo/types.d.ts +15 -0
- package/dist/todo/types.d.ts.map +1 -0
- package/dist/todo/types.js +29 -0
- package/dist/todo/types.js.map +1 -0
- package/dist/todo/view.d.ts +2 -0
- package/dist/todo/view.d.ts.map +1 -0
- package/dist/todo/view.js +27 -0
- package/dist/todo/view.js.map +1 -0
- package/dist/whisper/client.d.ts +2 -0
- package/dist/whisper/client.d.ts.map +1 -0
- package/dist/whisper/client.js +2 -0
- package/dist/whisper/client.js.map +1 -0
- package/dist/whisper/inbox.d.ts +2 -0
- package/dist/whisper/inbox.d.ts.map +1 -0
- package/dist/whisper/inbox.js +50 -0
- package/dist/whisper/inbox.js.map +1 -0
- package/dist/whisper/route.d.ts +10 -0
- package/dist/whisper/route.d.ts.map +1 -0
- package/dist/whisper/route.js +154 -0
- package/dist/whisper/route.js.map +1 -0
- package/dist/whisper/seed.d.ts +2 -0
- package/dist/whisper/seed.d.ts.map +1 -0
- package/dist/whisper/seed.js +14 -0
- package/dist/whisper/seed.js.map +1 -0
- package/dist/whisper/server.d.ts +5 -0
- package/dist/whisper/server.d.ts.map +1 -0
- package/dist/whisper/server.js +5 -0
- package/dist/whisper/server.js.map +1 -0
- package/dist/whisper/service.d.ts +2 -0
- package/dist/whisper/service.d.ts.map +1 -0
- package/dist/whisper/service.js +26 -0
- package/dist/whisper/service.js.map +1 -0
- package/dist/whisper/types.d.ts +38 -0
- package/dist/whisper/types.d.ts.map +1 -0
- package/dist/whisper/types.js +45 -0
- package/dist/whisper/types.js.map +1 -0
- package/dist/whisper/view.d.ts +2 -0
- package/dist/whisper/view.d.ts.map +1 -0
- package/dist/whisper/view.js +40 -0
- package/dist/whisper/view.js.map +1 -0
- package/doc/CLAUDE.md +49 -0
- package/doc/client.ts +5 -0
- package/doc/editor.css +283 -0
- package/doc/fs-codec.test.ts +119 -0
- package/doc/fs-codec.ts +50 -0
- package/doc/markdown.test.ts +152 -0
- package/doc/markdown.ts +284 -0
- package/doc/prefab.ts +26 -0
- package/doc/renderers.tsx +126 -0
- package/doc/seed.ts +10 -0
- package/doc/server.ts +5 -0
- package/doc/slash-command.ts +136 -0
- package/doc/slash-menu.tsx +91 -0
- package/doc/text.ts +20 -0
- package/doc/toolbar.tsx +86 -0
- package/doc/treenity-block-view.tsx +116 -0
- package/doc/treenity-block.ts +31 -0
- package/doc/types.ts +10 -0
- package/launcher/client.ts +4 -0
- package/launcher/icons.tsx +234 -0
- package/launcher/launcher.css +64 -0
- package/launcher/seed.ts +41 -0
- package/launcher/server.ts +2 -0
- package/launcher/types.ts +53 -0
- package/launcher/view.tsx +401 -0
- package/launcher/widgets.tsx +171 -0
- package/mindmap/branch.tsx +163 -0
- package/mindmap/client.ts +2 -0
- package/mindmap/mindmap.css +243 -0
- package/mindmap/sidebar.tsx +127 -0
- package/mindmap/types.ts +10 -0
- package/mindmap/view.tsx +247 -0
- package/package.json +75 -0
- package/sensor-demo/CLAUDE.md +3 -0
- package/sensor-demo/client.ts +1 -0
- package/sensor-demo/server.ts +1 -0
- package/sensor-demo/service.ts +27 -0
- package/sensor-demo/types.ts +20 -0
- package/sensor-demo/view.tsx +64 -0
- package/sensor-generator/CLAUDE.md +3 -0
- package/sensor-generator/action.ts +28 -0
- package/sensor-generator/client.ts +1 -0
- package/sensor-generator/server.ts +1 -0
- package/sensor-generator/view.tsx +107 -0
- package/sim/CLAUDE.md +16 -0
- package/sim/client.ts +1 -0
- package/sim/seed.ts +55 -0
- package/sim/server.ts +2 -0
- package/sim/service.ts +594 -0
- package/sim/sim.test.ts +282 -0
- package/sim/types.ts +87 -0
- package/sim/view.tsx +446 -0
- package/table/client.ts +3 -0
- package/table/edit.tsx +241 -0
- package/table/server.ts +1 -0
- package/table/types.ts +22 -0
- package/table/use-debounced-sync.ts +67 -0
- package/table/view.tsx +339 -0
- package/tasks/CLAUDE.md +6 -0
- package/tasks/server.ts +1 -0
- package/tasks/types.ts +36 -0
- package/three/CLAUDE.md +6 -0
- package/three/client.ts +1 -0
- package/three/seed.ts +54 -0
- package/three/server.ts +2 -0
- package/three/types.ts +238 -0
- package/three/view.tsx +453 -0
- package/todo/client.ts +2 -0
- package/todo/seed.ts +9 -0
- package/todo/server.ts +2 -0
- package/todo/types.ts +32 -0
- package/todo/view.tsx +67 -0
- package/whisper/CLAUDE.md +16 -0
- package/whisper/client.ts +1 -0
- package/whisper/inbox.ts +54 -0
- package/whisper/route.ts +182 -0
- package/whisper/seed.ts +15 -0
- package/whisper/server.ts +4 -0
- package/whisper/service.ts +29 -0
- package/whisper/types.ts +51 -0
- package/whisper/view.tsx +81 -0
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
// Chat editor — WYSIWYG Telegram-style editor for page actions
|
|
2
|
+
// Registered as react:chat:edit for brahman.page
|
|
3
|
+
// Same visual as PageChatPreview but with inline editing
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
closestCenter,
|
|
7
|
+
DndContext,
|
|
8
|
+
type DragEndEvent,
|
|
9
|
+
KeyboardSensor,
|
|
10
|
+
PointerSensor,
|
|
11
|
+
useSensor,
|
|
12
|
+
useSensors,
|
|
13
|
+
} from '@dnd-kit/core';
|
|
14
|
+
import {
|
|
15
|
+
arrayMove,
|
|
16
|
+
SortableContext,
|
|
17
|
+
sortableKeyboardCoordinates,
|
|
18
|
+
useSortable,
|
|
19
|
+
verticalListSortingStrategy,
|
|
20
|
+
} from '@dnd-kit/sortable';
|
|
21
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
22
|
+
import type { NodeData } from '@treenity/core';
|
|
23
|
+
import { getDefaults } from '@treenity/core/comp';
|
|
24
|
+
import { set, useChildren, usePath } from '@treenity/react/hooks';
|
|
25
|
+
import { trpc } from '@treenity/react/trpc';
|
|
26
|
+
import { Camera, File, GripVertical, Mic, MoreHorizontal, Plus, Trash2, Video, X } from 'lucide-react';
|
|
27
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
28
|
+
import { createPortal } from 'react-dom';
|
|
29
|
+
import { ACTION_TYPES, MENU_TYPES, type MenuButton, type MenuRow, type MenuType, type TString } from '../types';
|
|
30
|
+
import { actionIcon, actionSummary } from './action-cards';
|
|
31
|
+
import { TStringLineInput, tstringPreview } from './tstring-input';
|
|
32
|
+
|
|
33
|
+
// ── Helpers ──
|
|
34
|
+
|
|
35
|
+
function tstr(ts: TString | undefined): string {
|
|
36
|
+
if (!ts) return '';
|
|
37
|
+
return ts.ru || ts.en || Object.values(ts).find(v => v) || '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function primaryLang(ts: TString | undefined): string {
|
|
41
|
+
if (!ts) return 'ru';
|
|
42
|
+
if (ts.ru) return 'ru';
|
|
43
|
+
if (ts.en) return 'en';
|
|
44
|
+
return Object.keys(ts)[0] ?? 'ru';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── TgHtml — safe Telegram HTML renderer ──
|
|
48
|
+
|
|
49
|
+
const TG_TAGS: Record<string, string> = {
|
|
50
|
+
b: 'font-bold', strong: 'font-bold',
|
|
51
|
+
i: 'italic', em: 'italic',
|
|
52
|
+
u: 'underline', ins: 'underline',
|
|
53
|
+
s: 'line-through', strike: 'line-through', del: 'line-through',
|
|
54
|
+
code: 'font-mono text-[13px] bg-[#1a2636] px-1 rounded',
|
|
55
|
+
pre: 'font-mono text-[13px] bg-[#1a2636] p-2 rounded block overflow-x-auto',
|
|
56
|
+
blockquote: 'border-l-2 border-[#3d6a99] pl-2 italic',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function domToReact(node: Node, key: number): React.ReactNode {
|
|
60
|
+
if (node.nodeType === Node.TEXT_NODE) return node.textContent;
|
|
61
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return null;
|
|
62
|
+
const el = node as Element;
|
|
63
|
+
const tag = el.tagName.toLowerCase();
|
|
64
|
+
if (tag === 'br') return <br key={key} />;
|
|
65
|
+
if (tag === 'a') {
|
|
66
|
+
const href = el.getAttribute('href') || '#';
|
|
67
|
+
return <a key={key} href={href} target="_blank" rel="noopener noreferrer" className="text-[#5b9bd5] underline">{Array.from(el.childNodes).map(domToReact)}</a>;
|
|
68
|
+
}
|
|
69
|
+
const cls = TG_TAGS[tag];
|
|
70
|
+
if (cls) return <span key={key} className={cls}>{Array.from(el.childNodes).map(domToReact)}</span>;
|
|
71
|
+
return Array.from(el.childNodes).map(domToReact);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function TgHtml({ text }: { text: string }) {
|
|
75
|
+
if (!/<[a-z][\s>]/i.test(text)) return <>{text}</>;
|
|
76
|
+
const doc = new DOMParser().parseFromString(text, 'text/html');
|
|
77
|
+
return <>{Array.from(doc.body.childNodes).map(domToReact)}</>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Editable text — click to edit, blur to save ──
|
|
81
|
+
|
|
82
|
+
// Sanitize browser-generated HTML back to Telegram-safe subset
|
|
83
|
+
const ALLOWED_TAGS = new Set(['b', 'strong', 'i', 'em', 'u', 'ins', 's', 'strike', 'del', 'code', 'pre', 'a', 'blockquote', 'br']);
|
|
84
|
+
|
|
85
|
+
function sanitizeHtml(html: string): string {
|
|
86
|
+
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
87
|
+
|
|
88
|
+
function walk(node: Node): string {
|
|
89
|
+
if (node.nodeType === Node.TEXT_NODE) return node.textContent ?? '';
|
|
90
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return '';
|
|
91
|
+
const el = node as Element;
|
|
92
|
+
const tag = el.tagName.toLowerCase();
|
|
93
|
+
const inner = Array.from(el.childNodes).map(walk).join('');
|
|
94
|
+
|
|
95
|
+
if (tag === 'br') return '\n';
|
|
96
|
+
if (tag === 'div' || tag === 'p') return inner ? inner + '\n' : '';
|
|
97
|
+
if (!ALLOWED_TAGS.has(tag)) return inner;
|
|
98
|
+
if (tag === 'a') {
|
|
99
|
+
const href = el.getAttribute('href');
|
|
100
|
+
return href ? `<a href="${href}">${inner}</a>` : inner;
|
|
101
|
+
}
|
|
102
|
+
return `<${tag}>${inner}</${tag}>`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return walk(doc.body).replace(/\n$/, '');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function EditableText({
|
|
109
|
+
value,
|
|
110
|
+
onChange,
|
|
111
|
+
placeholder = 'Type message...',
|
|
112
|
+
}: {
|
|
113
|
+
value: TString;
|
|
114
|
+
onChange: (ts: TString) => void;
|
|
115
|
+
placeholder?: string;
|
|
116
|
+
}) {
|
|
117
|
+
const lang = primaryLang(value);
|
|
118
|
+
const text = value[lang] ?? '';
|
|
119
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
120
|
+
const saving = useRef(false);
|
|
121
|
+
|
|
122
|
+
function save() {
|
|
123
|
+
if (saving.current || !ref.current) return;
|
|
124
|
+
saving.current = true;
|
|
125
|
+
const html = ref.current.innerHTML;
|
|
126
|
+
const clean = sanitizeHtml(html);
|
|
127
|
+
if (clean !== text) onChange({ ...value, [lang]: clean });
|
|
128
|
+
saving.current = false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div
|
|
133
|
+
ref={ref}
|
|
134
|
+
contentEditable
|
|
135
|
+
suppressContentEditableWarning
|
|
136
|
+
className="outline-none min-h-[1.25rem] cursor-text
|
|
137
|
+
focus:ring-1 focus:ring-[#3d6a99] rounded transition-all
|
|
138
|
+
empty:before:content-[attr(data-placeholder)] empty:before:text-[#6c7883] empty:before:italic"
|
|
139
|
+
data-placeholder={placeholder}
|
|
140
|
+
dangerouslySetInnerHTML={{ __html: text || '' }}
|
|
141
|
+
onBlur={save}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Bubble shells ──
|
|
147
|
+
|
|
148
|
+
function BotBubble({ children, first, tools }: { children: React.ReactNode; first?: boolean; tools?: React.ReactNode }) {
|
|
149
|
+
return (
|
|
150
|
+
<div className="flex flex-col items-start max-w-[85%] group/bubble">
|
|
151
|
+
{first && <span className="text-[11px] font-semibold text-[#5b9bd5] mb-0.5 ml-1">Bot</span>}
|
|
152
|
+
<div className="relative bg-[#182533] text-[#e1e3e6] text-sm rounded-lg rounded-tl-sm px-3 py-2 whitespace-pre-wrap break-words w-full">
|
|
153
|
+
{children}
|
|
154
|
+
{tools && (
|
|
155
|
+
<div className="absolute -right-1 -top-1 opacity-0 group-hover/bubble:opacity-100 transition-opacity">
|
|
156
|
+
{tools}
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function UserBubble({ children }: { children: React.ReactNode }) {
|
|
165
|
+
return (
|
|
166
|
+
<div className="flex justify-end">
|
|
167
|
+
<div className="bg-[#2b5278] text-[#e1e3e6] text-sm rounded-lg rounded-tr-sm px-3 py-2 max-w-[70%]
|
|
168
|
+
border border-dashed border-[#3d6a99] opacity-60">
|
|
169
|
+
{children}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function SystemPill({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) {
|
|
176
|
+
return (
|
|
177
|
+
<div className="flex justify-center">
|
|
178
|
+
<div
|
|
179
|
+
className={`flex items-center gap-1.5 text-[11px] text-[#6c7883] bg-[#131c26] rounded-full px-3 py-1 ${
|
|
180
|
+
onClick ? 'cursor-pointer hover:bg-[#1a2636] hover:text-[#e1e3e6] transition-colors' : ''
|
|
181
|
+
}`}
|
|
182
|
+
onClick={onClick}
|
|
183
|
+
>
|
|
184
|
+
{children}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Settings popover ──
|
|
191
|
+
|
|
192
|
+
function SettingsPopover({
|
|
193
|
+
node,
|
|
194
|
+
onUpdate,
|
|
195
|
+
onDelete,
|
|
196
|
+
onClose,
|
|
197
|
+
}: {
|
|
198
|
+
node: NodeData;
|
|
199
|
+
onUpdate: (patch: Record<string, unknown>) => void;
|
|
200
|
+
onDelete: () => void;
|
|
201
|
+
onClose: () => void;
|
|
202
|
+
}) {
|
|
203
|
+
const type = node.$type;
|
|
204
|
+
|
|
205
|
+
return createPortal(
|
|
206
|
+
<div className="fixed inset-0 z-50" onClick={onClose}>
|
|
207
|
+
<div
|
|
208
|
+
className="absolute bg-[#1c2733] border border-[#2b3945] rounded-lg shadow-xl p-3 space-y-2 w-64"
|
|
209
|
+
style={{ top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}
|
|
210
|
+
onClick={e => e.stopPropagation()}
|
|
211
|
+
>
|
|
212
|
+
<div className="flex items-center justify-between mb-1">
|
|
213
|
+
<span className="text-xs font-medium text-[#e1e3e6]">
|
|
214
|
+
{ACTION_TYPES.find(a => a.type === type)?.label ?? type.split('.').at(-1)}
|
|
215
|
+
</span>
|
|
216
|
+
<button type="button" onClick={onClose} className="text-[#6c7883] hover:text-[#e1e3e6]">
|
|
217
|
+
<X className="h-3.5 w-3.5" />
|
|
218
|
+
</button>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
{/* Type-specific settings */}
|
|
222
|
+
{type === 'brahman.action.message' && (
|
|
223
|
+
<>
|
|
224
|
+
<SettingSelect label="Menu" value={(node.menuType as string) ?? 'none'}
|
|
225
|
+
options={MENU_TYPES.map(m => ({ value: m.value, label: m.label }))}
|
|
226
|
+
onChange={v => onUpdate({ menuType: v })} />
|
|
227
|
+
<SettingCheckbox label="Disable links" checked={!!node.disableLinks}
|
|
228
|
+
onChange={v => onUpdate({ disableLinks: v })} />
|
|
229
|
+
</>
|
|
230
|
+
)}
|
|
231
|
+
|
|
232
|
+
{type === 'brahman.action.question' && (
|
|
233
|
+
<>
|
|
234
|
+
<SettingSelect label="Input type" value={(node.inputType as string) ?? 'text'}
|
|
235
|
+
options={[{ value: 'text', label: 'Text' }, { value: 'photo', label: 'Photo' }]}
|
|
236
|
+
onChange={v => onUpdate({ inputType: v })} />
|
|
237
|
+
<SettingInput label="Save to" value={(node.saveTo as string) ?? ''}
|
|
238
|
+
placeholder="session.field" onChange={v => onUpdate({ saveTo: v })} />
|
|
239
|
+
</>
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
{type === 'brahman.action.file' && (
|
|
243
|
+
<SettingSelect label="Send as" value={(node.asType as string) ?? ''}
|
|
244
|
+
options={[
|
|
245
|
+
{ value: '', label: 'Auto' }, { value: 'photo', label: 'Photo' },
|
|
246
|
+
{ value: 'document', label: 'Document' }, { value: 'video', label: 'Video' },
|
|
247
|
+
{ value: 'audio', label: 'Audio' }, { value: 'voice', label: 'Voice' },
|
|
248
|
+
]}
|
|
249
|
+
onChange={v => onUpdate({ asType: v })} />
|
|
250
|
+
)}
|
|
251
|
+
|
|
252
|
+
<div className="pt-1 border-t border-[#2b3945]">
|
|
253
|
+
<button
|
|
254
|
+
type="button"
|
|
255
|
+
onClick={onDelete}
|
|
256
|
+
className="flex items-center gap-1.5 text-xs text-red-400 hover:text-red-300 py-1"
|
|
257
|
+
>
|
|
258
|
+
<Trash2 className="h-3 w-3" /> Delete action
|
|
259
|
+
</button>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
</div>,
|
|
263
|
+
document.body,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function SettingSelect({ label, value, options, onChange }: {
|
|
268
|
+
label: string; value: string; options: { value: string; label: string }[]; onChange: (v: string) => void;
|
|
269
|
+
}) {
|
|
270
|
+
return (
|
|
271
|
+
<div className="flex items-center justify-between gap-2">
|
|
272
|
+
<span className="text-[11px] text-[#6c7883]">{label}</span>
|
|
273
|
+
<select
|
|
274
|
+
className="bg-[#0e1621] border border-[#2b3945] rounded text-xs text-[#e1e3e6] px-2 py-1"
|
|
275
|
+
value={value} onChange={e => onChange(e.target.value)}
|
|
276
|
+
>
|
|
277
|
+
{options.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
|
|
278
|
+
</select>
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function SettingCheckbox({ label, checked, onChange }: {
|
|
284
|
+
label: string; checked: boolean; onChange: (v: boolean) => void;
|
|
285
|
+
}) {
|
|
286
|
+
return (
|
|
287
|
+
<label className="flex items-center justify-between gap-2 cursor-pointer">
|
|
288
|
+
<span className="text-[11px] text-[#6c7883]">{label}</span>
|
|
289
|
+
<input type="checkbox" checked={checked} onChange={e => onChange(e.target.checked)}
|
|
290
|
+
className="rounded border-[#2b3945]" />
|
|
291
|
+
</label>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function SettingInput({ label, value, placeholder, onChange }: {
|
|
296
|
+
label: string; value: string; placeholder?: string; onChange: (v: string) => void;
|
|
297
|
+
}) {
|
|
298
|
+
return (
|
|
299
|
+
<div className="flex items-center justify-between gap-2">
|
|
300
|
+
<span className="text-[11px] text-[#6c7883] shrink-0">{label}</span>
|
|
301
|
+
<input
|
|
302
|
+
className="bg-[#0e1621] border border-[#2b3945] rounded text-xs text-[#e1e3e6] px-2 py-1 w-32 font-mono"
|
|
303
|
+
value={value} placeholder={placeholder} onChange={e => onChange(e.target.value)}
|
|
304
|
+
/>
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── Editable inline buttons ──
|
|
310
|
+
|
|
311
|
+
function EditableButtons({
|
|
312
|
+
rows,
|
|
313
|
+
menuType,
|
|
314
|
+
onChangeRows,
|
|
315
|
+
}: {
|
|
316
|
+
rows: MenuRow[];
|
|
317
|
+
menuType: MenuType;
|
|
318
|
+
onChangeRows: (rows: MenuRow[]) => void;
|
|
319
|
+
}) {
|
|
320
|
+
const [editBtn, setEditBtn] = useState<{ ri: number; bi: number } | null>(null);
|
|
321
|
+
|
|
322
|
+
if (!rows?.length || menuType === 'none' || menuType === 'remove') return null;
|
|
323
|
+
const isReply = menuType === 'keyboard' || menuType === 'force_reply';
|
|
324
|
+
|
|
325
|
+
let nextId = 1;
|
|
326
|
+
for (const row of rows) for (const btn of row.buttons) if (btn.id >= nextId) nextId = btn.id + 1;
|
|
327
|
+
|
|
328
|
+
function addButton(ri: number) {
|
|
329
|
+
const newRows = rows.map(r => ({ buttons: [...r.buttons] }));
|
|
330
|
+
newRows[ri].buttons.push({ id: nextId, title: { ru: '', en: '' }, tags: [] });
|
|
331
|
+
onChangeRows(newRows);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function addRow() {
|
|
335
|
+
onChangeRows([...rows, { buttons: [{ id: nextId, title: { ru: '', en: '' }, tags: [] }] }]);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function updateButton(ri: number, bi: number, btn: MenuButton) {
|
|
339
|
+
const newRows = rows.map(r => ({ buttons: [...r.buttons] }));
|
|
340
|
+
newRows[ri].buttons[bi] = btn;
|
|
341
|
+
onChangeRows(newRows);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function deleteButton(ri: number, bi: number) {
|
|
345
|
+
const newRows = rows.map(r => ({ buttons: [...r.buttons] }));
|
|
346
|
+
newRows[ri].buttons.splice(bi, 1);
|
|
347
|
+
onChangeRows(newRows.filter(r => r.buttons.length > 0));
|
|
348
|
+
setEditBtn(null);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return (
|
|
352
|
+
<div className={`flex flex-col gap-0.5 ${isReply ? 'w-full mt-2' : 'max-w-[85%] mt-0.5'}`}>
|
|
353
|
+
{rows.map((row, ri) => (
|
|
354
|
+
<div key={ri} className="flex gap-0.5">
|
|
355
|
+
{row.buttons.map((btn, bi) => {
|
|
356
|
+
const text = tstringPreview(btn.title, 24) || '...';
|
|
357
|
+
return (
|
|
358
|
+
<div
|
|
359
|
+
key={btn.id}
|
|
360
|
+
className={`flex-1 text-center text-xs py-1.5 px-2 rounded truncate cursor-pointer
|
|
361
|
+
hover:ring-1 hover:ring-[#5b9bd5] transition-all
|
|
362
|
+
${isReply
|
|
363
|
+
? 'bg-[#1c2733] text-[#e1e3e6] border border-[#2b3945]'
|
|
364
|
+
: 'bg-[#2b5278] text-[#e1e3e6]'
|
|
365
|
+
}`}
|
|
366
|
+
onClick={() => setEditBtn({ ri, bi })}
|
|
367
|
+
>
|
|
368
|
+
{btn.url ? `🔗 ${text}` : text}
|
|
369
|
+
</div>
|
|
370
|
+
);
|
|
371
|
+
})}
|
|
372
|
+
<button
|
|
373
|
+
type="button"
|
|
374
|
+
onClick={() => addButton(ri)}
|
|
375
|
+
className="text-[#6c7883] hover:text-[#e1e3e6] px-1 transition-colors"
|
|
376
|
+
title="Add button"
|
|
377
|
+
>
|
|
378
|
+
<Plus className="h-3 w-3" />
|
|
379
|
+
</button>
|
|
380
|
+
</div>
|
|
381
|
+
))}
|
|
382
|
+
<button
|
|
383
|
+
type="button"
|
|
384
|
+
onClick={addRow}
|
|
385
|
+
className="text-[10px] text-[#6c7883] hover:text-[#e1e3e6] py-0.5 transition-colors"
|
|
386
|
+
>
|
|
387
|
+
+ row
|
|
388
|
+
</button>
|
|
389
|
+
|
|
390
|
+
{/* Button edit modal */}
|
|
391
|
+
{editBtn && rows[editBtn.ri]?.buttons[editBtn.bi] && (
|
|
392
|
+
<ButtonEditPopover
|
|
393
|
+
button={rows[editBtn.ri].buttons[editBtn.bi]}
|
|
394
|
+
onSave={btn => updateButton(editBtn.ri, editBtn.bi, btn)}
|
|
395
|
+
onDelete={() => deleteButton(editBtn.ri, editBtn.bi)}
|
|
396
|
+
onClose={() => setEditBtn(null)}
|
|
397
|
+
/>
|
|
398
|
+
)}
|
|
399
|
+
</div>
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function ButtonEditPopover({
|
|
404
|
+
button,
|
|
405
|
+
onSave,
|
|
406
|
+
onDelete,
|
|
407
|
+
onClose,
|
|
408
|
+
}: {
|
|
409
|
+
button: MenuButton;
|
|
410
|
+
onSave: (btn: MenuButton) => void;
|
|
411
|
+
onDelete: () => void;
|
|
412
|
+
onClose: () => void;
|
|
413
|
+
}) {
|
|
414
|
+
const [draft, setDraft] = useState(() => ({ ...button }));
|
|
415
|
+
|
|
416
|
+
return createPortal(
|
|
417
|
+
<div className="fixed inset-0 z-50" onClick={onClose}>
|
|
418
|
+
<div
|
|
419
|
+
className="absolute bg-[#1c2733] border border-[#2b3945] rounded-lg shadow-xl p-3 space-y-2 w-72"
|
|
420
|
+
style={{ top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}
|
|
421
|
+
onClick={e => e.stopPropagation()}
|
|
422
|
+
>
|
|
423
|
+
<div className="flex items-center justify-between mb-1">
|
|
424
|
+
<span className="text-xs font-medium text-[#e1e3e6]">Edit button</span>
|
|
425
|
+
<button type="button" onClick={onClose} className="text-[#6c7883] hover:text-[#e1e3e6]">
|
|
426
|
+
<X className="h-3.5 w-3.5" />
|
|
427
|
+
</button>
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
<div>
|
|
431
|
+
<span className="text-[11px] text-[#6c7883]">Title</span>
|
|
432
|
+
<TStringLineInput
|
|
433
|
+
value={draft.title}
|
|
434
|
+
onChange={title => setDraft(d => ({ ...d, title }))}
|
|
435
|
+
langs={['ru', 'en']}
|
|
436
|
+
/>
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
<div className="flex items-center gap-2">
|
|
440
|
+
<span className="text-[11px] text-[#6c7883] shrink-0">URL</span>
|
|
441
|
+
<input
|
|
442
|
+
className="flex-1 bg-[#0e1621] border border-[#2b3945] rounded text-xs text-[#e1e3e6] px-2 py-1"
|
|
443
|
+
placeholder="https://..."
|
|
444
|
+
value={draft.url ?? ''}
|
|
445
|
+
onChange={e => setDraft(d => ({ ...d, url: e.target.value || undefined }))}
|
|
446
|
+
/>
|
|
447
|
+
</div>
|
|
448
|
+
|
|
449
|
+
<div className="flex justify-between pt-1 border-t border-[#2b3945]">
|
|
450
|
+
<button type="button" onClick={onDelete}
|
|
451
|
+
className="text-xs text-red-400 hover:text-red-300 flex items-center gap-1">
|
|
452
|
+
<Trash2 className="h-3 w-3" /> Delete
|
|
453
|
+
</button>
|
|
454
|
+
<div className="flex gap-1">
|
|
455
|
+
<button type="button" onClick={onClose}
|
|
456
|
+
className="text-xs text-[#6c7883] hover:text-[#e1e3e6] px-2 py-1">Cancel</button>
|
|
457
|
+
<button type="button" onClick={() => { onSave(draft); onClose(); }}
|
|
458
|
+
className="text-xs bg-[#2b5278] text-[#e1e3e6] px-3 py-1 rounded hover:bg-[#3d6a99]">Save</button>
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
</div>,
|
|
463
|
+
document.body,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ── Sortable action wrapper ──
|
|
468
|
+
|
|
469
|
+
function SortableAction({
|
|
470
|
+
node,
|
|
471
|
+
isFirstBubble,
|
|
472
|
+
onDelete,
|
|
473
|
+
}: {
|
|
474
|
+
node: NodeData;
|
|
475
|
+
isFirstBubble: boolean;
|
|
476
|
+
onDelete: () => void;
|
|
477
|
+
}) {
|
|
478
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: node.$path });
|
|
479
|
+
const n = usePath(node.$path);
|
|
480
|
+
const [showSettings, setShowSettings] = useState(false);
|
|
481
|
+
|
|
482
|
+
if (!n) return null;
|
|
483
|
+
|
|
484
|
+
function update(patch: Record<string, unknown>) {
|
|
485
|
+
if (!n) return;
|
|
486
|
+
set({ ...n, ...patch } as NodeData);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const style = {
|
|
490
|
+
transform: CSS.Transform.toString(transform),
|
|
491
|
+
transition,
|
|
492
|
+
opacity: isDragging ? 0.4 : 1,
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const settingsBtn = (
|
|
496
|
+
<button
|
|
497
|
+
type="button"
|
|
498
|
+
onClick={e => { e.stopPropagation(); setShowSettings(true); }}
|
|
499
|
+
className="bg-[#1c2733] border border-[#2b3945] rounded-full p-1 text-[#6c7883] hover:text-[#e1e3e6] transition-colors"
|
|
500
|
+
>
|
|
501
|
+
<MoreHorizontal className="h-3 w-3" />
|
|
502
|
+
</button>
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
const dragHandle = (
|
|
506
|
+
<div
|
|
507
|
+
{...attributes}
|
|
508
|
+
{...listeners}
|
|
509
|
+
className="absolute -left-5 top-1/2 -translate-y-1/2 opacity-0 group-hover/action:opacity-100
|
|
510
|
+
cursor-grab text-[#6c7883] hover:text-[#e1e3e6] transition-opacity"
|
|
511
|
+
>
|
|
512
|
+
<GripVertical className="h-3.5 w-3.5" />
|
|
513
|
+
</div>
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
const type = n.$type;
|
|
517
|
+
|
|
518
|
+
const content = (() => {
|
|
519
|
+
switch (type) {
|
|
520
|
+
case 'brahman.action.message': {
|
|
521
|
+
const menuType = (n.menuType as MenuType) ?? 'none';
|
|
522
|
+
const rows = (n.rows as MenuRow[]) ?? [];
|
|
523
|
+
return (
|
|
524
|
+
<div className="flex flex-col items-start gap-0">
|
|
525
|
+
<BotBubble first={isFirstBubble} tools={settingsBtn}>
|
|
526
|
+
<EditableText
|
|
527
|
+
value={(n.text as TString) ?? {}}
|
|
528
|
+
onChange={text => update({ text })}
|
|
529
|
+
placeholder="Type message..."
|
|
530
|
+
/>
|
|
531
|
+
</BotBubble>
|
|
532
|
+
<EditableButtons
|
|
533
|
+
rows={rows}
|
|
534
|
+
menuType={menuType}
|
|
535
|
+
onChangeRows={rows => update({ rows })}
|
|
536
|
+
/>
|
|
537
|
+
</div>
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
case 'brahman.action.question': {
|
|
542
|
+
const inputType = (n.inputType as string) ?? 'text';
|
|
543
|
+
return (
|
|
544
|
+
<div className="space-y-1.5">
|
|
545
|
+
<BotBubble first={isFirstBubble} tools={settingsBtn}>
|
|
546
|
+
<EditableText
|
|
547
|
+
value={(n.text as TString) ?? {}}
|
|
548
|
+
onChange={text => update({ text })}
|
|
549
|
+
placeholder="Type question..."
|
|
550
|
+
/>
|
|
551
|
+
</BotBubble>
|
|
552
|
+
<UserBubble>
|
|
553
|
+
{inputType === 'photo'
|
|
554
|
+
? <span className="flex items-center gap-1"><Camera className="h-3.5 w-3.5" /> Photo</span>
|
|
555
|
+
: <span className="italic text-xs">User types answer...</span>
|
|
556
|
+
}
|
|
557
|
+
</UserBubble>
|
|
558
|
+
</div>
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
case 'brahman.action.file': {
|
|
563
|
+
const asType = (n.asType as string) || 'document';
|
|
564
|
+
const icons: Record<string, React.ReactNode> = {
|
|
565
|
+
photo: <Camera className="h-8 w-8" />,
|
|
566
|
+
video: <Video className="h-8 w-8" />,
|
|
567
|
+
audio: <Mic className="h-8 w-8" />,
|
|
568
|
+
voice: <Mic className="h-8 w-8" />,
|
|
569
|
+
};
|
|
570
|
+
return (
|
|
571
|
+
<BotBubble first={isFirstBubble} tools={settingsBtn}>
|
|
572
|
+
<div className="flex flex-col items-center gap-1 py-2 text-[#6c7883]">
|
|
573
|
+
{icons[asType] ?? <File className="h-8 w-8" />}
|
|
574
|
+
<span className="text-xs">{asType}</span>
|
|
575
|
+
</div>
|
|
576
|
+
</BotBubble>
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
case 'brahman.action.selectlang': {
|
|
581
|
+
return (
|
|
582
|
+
<div className="flex flex-col items-start gap-0.5">
|
|
583
|
+
<BotBubble first={isFirstBubble} tools={settingsBtn}>
|
|
584
|
+
<EditableText
|
|
585
|
+
value={(n.text as TString) ?? {}}
|
|
586
|
+
onChange={text => update({ text })}
|
|
587
|
+
placeholder="Choose language"
|
|
588
|
+
/>
|
|
589
|
+
</BotBubble>
|
|
590
|
+
<div className="flex gap-0.5 max-w-[85%]">
|
|
591
|
+
<div className="flex-1 text-center text-xs py-1.5 px-2 rounded bg-[#2b5278] text-[#e1e3e6]">🇷🇺 RU</div>
|
|
592
|
+
<div className="flex-1 text-center text-xs py-1.5 px-2 rounded bg-[#2b5278] text-[#e1e3e6]">🇬🇧 EN</div>
|
|
593
|
+
</div>
|
|
594
|
+
</div>
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// All other actions → system pills
|
|
599
|
+
default: {
|
|
600
|
+
const summary = actionSummary(n);
|
|
601
|
+
const label = type.split('.').at(-1) ?? type;
|
|
602
|
+
return (
|
|
603
|
+
<SystemPill onClick={() => setShowSettings(true)}>
|
|
604
|
+
{actionIcon(type)}
|
|
605
|
+
<span>{label}{summary ? `: ${summary}` : ''}</span>
|
|
606
|
+
</SystemPill>
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
})();
|
|
611
|
+
|
|
612
|
+
return (
|
|
613
|
+
<div ref={setNodeRef} style={style} className="relative group/action">
|
|
614
|
+
{dragHandle}
|
|
615
|
+
{content}
|
|
616
|
+
{showSettings && (
|
|
617
|
+
<SettingsPopover
|
|
618
|
+
node={n}
|
|
619
|
+
onUpdate={update}
|
|
620
|
+
onDelete={onDelete}
|
|
621
|
+
onClose={() => setShowSettings(false)}
|
|
622
|
+
/>
|
|
623
|
+
)}
|
|
624
|
+
</div>
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ── Action palette (compact, chat-themed) ──
|
|
629
|
+
|
|
630
|
+
function ChatActionPalette({ onSelect }: { onSelect: (type: string) => void }) {
|
|
631
|
+
const [open, setOpen] = useState(false);
|
|
632
|
+
|
|
633
|
+
return (
|
|
634
|
+
<div className="flex justify-center">
|
|
635
|
+
<div className="relative">
|
|
636
|
+
<button
|
|
637
|
+
type="button"
|
|
638
|
+
onClick={() => setOpen(!open)}
|
|
639
|
+
className="flex items-center gap-1 text-[11px] text-[#6c7883] hover:text-[#e1e3e6]
|
|
640
|
+
bg-[#131c26] rounded-full px-3 py-1 transition-colors"
|
|
641
|
+
>
|
|
642
|
+
<Plus className="h-3 w-3" /> Add action
|
|
643
|
+
</button>
|
|
644
|
+
|
|
645
|
+
{open && (
|
|
646
|
+
<div className="absolute left-1/2 -translate-x-1/2 bottom-full mb-1 z-50 w-52
|
|
647
|
+
bg-[#1c2733] border border-[#2b3945] rounded-lg shadow-xl py-1 max-h-64 overflow-y-auto">
|
|
648
|
+
{ACTION_TYPES.map(at => (
|
|
649
|
+
<button
|
|
650
|
+
key={at.type}
|
|
651
|
+
type="button"
|
|
652
|
+
className="w-full text-left px-3 py-1.5 text-xs text-[#e1e3e6] hover:bg-[#2b3945] flex items-center gap-2"
|
|
653
|
+
onClick={() => { onSelect(at.type); setOpen(false); }}
|
|
654
|
+
>
|
|
655
|
+
{actionIcon(at.type)}
|
|
656
|
+
{at.label}
|
|
657
|
+
</button>
|
|
658
|
+
))}
|
|
659
|
+
</div>
|
|
660
|
+
)}
|
|
661
|
+
</div>
|
|
662
|
+
</div>
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ── Main component ──
|
|
667
|
+
|
|
668
|
+
export function PageChatEditor({ value }: { value: NodeData }) {
|
|
669
|
+
const node = usePath(value.$path);
|
|
670
|
+
const actionsPath = value.$path + '/_actions';
|
|
671
|
+
const children = useChildren(actionsPath, { watch: true, watchNew: true });
|
|
672
|
+
|
|
673
|
+
const positions: string[] = (node?.positions as string[]) ?? [];
|
|
674
|
+
const tracked = new Set(positions);
|
|
675
|
+
const sorted = [
|
|
676
|
+
...positions.map(p => children.find(c => c.$path === p)).filter((c): c is NodeData => !!c),
|
|
677
|
+
...children.filter(c => !tracked.has(c.$path) && c.$type?.startsWith('brahman.action.')),
|
|
678
|
+
];
|
|
679
|
+
|
|
680
|
+
// ── Command editing ──
|
|
681
|
+
const command = (node?.command as string) ?? '';
|
|
682
|
+
const [localCmd, setLocalCmd] = useState(command);
|
|
683
|
+
const cmdTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
684
|
+
useEffect(() => { setLocalCmd(command); }, [command]);
|
|
685
|
+
|
|
686
|
+
const saveNode = useCallback((patch: Record<string, unknown>) => {
|
|
687
|
+
if (!node) return;
|
|
688
|
+
set({ ...node, ...patch } as NodeData);
|
|
689
|
+
}, [node]);
|
|
690
|
+
|
|
691
|
+
// ── DnD ──
|
|
692
|
+
const sensors = useSensors(
|
|
693
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
|
694
|
+
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
function handleDragEnd(event: DragEndEvent) {
|
|
698
|
+
const { active, over } = event;
|
|
699
|
+
if (!over || active.id === over.id) return;
|
|
700
|
+
const oldIndex = sorted.findIndex(c => c.$path === active.id);
|
|
701
|
+
const newIndex = sorted.findIndex(c => c.$path === over.id);
|
|
702
|
+
if (oldIndex === -1 || newIndex === -1) return;
|
|
703
|
+
saveNode({ positions: arrayMove(sorted.map(c => c.$path), oldIndex, newIndex) });
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// ── CRUD ──
|
|
707
|
+
async function addAction(type: string) {
|
|
708
|
+
const id = Date.now().toString(36);
|
|
709
|
+
const childPath = `${actionsPath}/${id}`;
|
|
710
|
+
await trpc.set.mutate({ node: { $path: actionsPath, $type: 'dir' } as NodeData });
|
|
711
|
+
const defaults = getDefaults(type);
|
|
712
|
+
await trpc.set.mutate({ node: { $path: childPath, $type: type, ...defaults } as NodeData });
|
|
713
|
+
saveNode({ positions: [...positions, childPath] });
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function removeAction(path: string) {
|
|
717
|
+
await trpc.remove.mutate({ path });
|
|
718
|
+
saveNode({ positions: positions.filter(p => p !== path) });
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
let hadBotBubble = false;
|
|
722
|
+
|
|
723
|
+
return (
|
|
724
|
+
<div className="bg-[#0e1621] rounded-xl p-4 space-y-2.5 min-h-[200px] max-w-md mx-auto">
|
|
725
|
+
{/* Command header — editable */}
|
|
726
|
+
<div className="flex justify-center mb-2">
|
|
727
|
+
<input
|
|
728
|
+
className="text-[11px] text-[#6c7883] bg-[#131c26] rounded-full px-3 py-1 font-mono
|
|
729
|
+
text-center border-none outline-none focus:ring-1 focus:ring-[#3d6a99] w-32"
|
|
730
|
+
placeholder="/command..."
|
|
731
|
+
value={localCmd}
|
|
732
|
+
onChange={e => {
|
|
733
|
+
setLocalCmd(e.target.value);
|
|
734
|
+
clearTimeout(cmdTimer.current);
|
|
735
|
+
cmdTimer.current = setTimeout(() => saveNode({ command: e.target.value }), 400);
|
|
736
|
+
}}
|
|
737
|
+
onBlur={() => {
|
|
738
|
+
clearTimeout(cmdTimer.current);
|
|
739
|
+
if (localCmd !== command) saveNode({ command: localCmd });
|
|
740
|
+
}}
|
|
741
|
+
/>
|
|
742
|
+
</div>
|
|
743
|
+
|
|
744
|
+
{/* Actions with DnD */}
|
|
745
|
+
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
|
746
|
+
<SortableContext items={sorted.map(c => c.$path)} strategy={verticalListSortingStrategy}>
|
|
747
|
+
<div className="space-y-2.5">
|
|
748
|
+
{sorted.length === 0 && (
|
|
749
|
+
<div className="text-[#6c7883] text-xs text-center py-8">Click + to add actions</div>
|
|
750
|
+
)}
|
|
751
|
+
|
|
752
|
+
{sorted.map(child => {
|
|
753
|
+
const isBubbleType = ['brahman.action.message', 'brahman.action.question',
|
|
754
|
+
'brahman.action.file', 'brahman.action.selectlang'].includes(child.$type);
|
|
755
|
+
const isFirstBubble = isBubbleType && !hadBotBubble;
|
|
756
|
+
if (isBubbleType) hadBotBubble = true;
|
|
757
|
+
|
|
758
|
+
return (
|
|
759
|
+
<SortableAction
|
|
760
|
+
key={child.$path}
|
|
761
|
+
node={child}
|
|
762
|
+
isFirstBubble={isFirstBubble}
|
|
763
|
+
onDelete={() => removeAction(child.$path)}
|
|
764
|
+
/>
|
|
765
|
+
);
|
|
766
|
+
})}
|
|
767
|
+
</div>
|
|
768
|
+
</SortableContext>
|
|
769
|
+
</DndContext>
|
|
770
|
+
|
|
771
|
+
{/* Add action */}
|
|
772
|
+
<ChatActionPalette onSelect={addAction} />
|
|
773
|
+
|
|
774
|
+
{/* Input bar mockup */}
|
|
775
|
+
<div className="flex items-center gap-2 mt-4 pt-3 border-t border-[#1c2733]">
|
|
776
|
+
<div className="flex-1 bg-[#1c2733] rounded-full px-3 py-1.5 text-xs text-[#6c7883]">
|
|
777
|
+
Message...
|
|
778
|
+
</div>
|
|
779
|
+
</div>
|
|
780
|
+
</div>
|
|
781
|
+
);
|
|
782
|
+
}
|