From 9c7e278776c78bcf19c16debe2181ad145dea8f1 Mon Sep 17 00:00:00 2001 From: Aleksandar Dimitrov Date: Fri, 31 Jul 2026 18:10:15 +0300 Subject: [PATCH 1/7] init --- .../io-assist-angular-editable/.gitignore | 4 + .../io-assist-angular-editable/README.md | 136 +- .../io-assist-angular-editable/angular.json | 4 +- .../package-lock.json | 5636 ++++++++++++----- .../io-assist-angular-editable/package.json | 25 +- .../src/app/app.config.ts | 6 +- .../io-assist-angular-editable/src/app/app.ts | 2 +- .../components/chat/chat.component.html | 95 + .../components/chat/chat.component.ts | 67 + .../components/header/header.component.html | 54 + .../components/header/header.component.ts | 86 + .../input-area-action-bar.component.html | 30 + .../input-area-action-bar.component.ts | 68 + .../input-area-send-button.component.html | 42 + .../input-area-send-button.component.ts | 78 + .../input-area/input-area.component.css | 30 + .../input-area/input-area.component.html | 68 + .../input-area/input-area.component.ts | 195 + .../assistant-message.component.html | 15 + .../assistant-message.component.ts | 18 + .../mcp-app-resource.component.ts | 76 + .../message-area/message-area.component.html | 40 + .../message-area/message-area.component.ts | 71 + .../message-footer.component.html | 22 + .../message-footer.component.ts | 95 + .../tool-message/tool-message.component.css | 14 + .../tool-message/tool-message.component.html | 54 + .../tool-message/tool-message.component.ts | 103 + .../tool-trace-message.component.html | 112 + .../tool-trace-message.component.ts | 46 + .../user-message/user-message.component.html | 51 + .../user-message/user-message.component.ts | 56 + .../favorite-prompt-list.component.html | 3 + .../favorite-prompt-list.component.ts | 10 + .../prompt-list-item.component.html | 37 + .../prompt-list-item.component.ts | 71 + .../prompt-list/prompt-list.component.html | 139 + .../prompt-list/prompt-list.component.ts | 100 + .../scroll-area/scroll-area.component.html | 29 + .../scroll-area/scroll-area.component.ts | 137 + ...d-history-list-item-divider.component.html | 8 + ...ead-history-list-item-divider.component.ts | 11 + .../thread-history-list-item.component.html | 72 + .../thread-history-list-item.component.ts | 127 + .../thread-history-list.component.html | 39 + .../thread-history-list.component.ts | 118 + .../thread-history.component.html | 146 + .../thread-history.component.ts | 45 + .../tool-info/tool-info.component.html | 6 + .../tool-info/tool-info.component.ts | 16 + .../tool-list-item.component.html | 34 + .../tool-list-item.component.ts | 56 + .../tool/tool-list/tool-list.component.html | 81 + .../tool/tool-list/tool-list.component.ts | 44 + .../working-context-panel.component.css | 5 + .../working-context-panel.component.html | 11 + .../working-context-panel.component.ts | 58 + .../src/app/io-assist/index.ts | 8 + .../app/io-assist/io-assist.component.html | 36 + .../src/app/io-assist/io-assist.component.ts | 66 + .../src/app/io-assist/io-assist.config.ts | 48 + .../src/app/io-assist/io-assist.providers.ts | 41 + .../src/app/io-assist/io-assist.schema.ts | 135 + .../src/app/io-assist/io-assist.types.ts | 62 + .../scripts/io-assist-angular-build.js | 41 + .../app-button/app-button.component.html | 16 + .../app-button/app-button.component.ts | 23 + .../shared/components/app-button/enum.ts | 5 + .../shared/components/app-button/types.ts | 3 + ...pp-copy-to-clipboard-button.component.html | 16 + .../app-copy-to-clipboard-button.component.ts | 66 + .../app-icon/app-icon.component.html | 29 + .../components/app-icon/app-icon.component.ts | 470 ++ .../shared/components/app-icon/enum.ts | 48 + .../shared/components/app-icon/icons.ts | 258 + .../shared/components/app-icon/types.ts | 13 + .../app-input/app-input.component.html | 101 + .../app-input/app-input.component.ts | 83 + .../app-md-formatter.component.css | 505 ++ .../app-md-formatter.component.html | 15 + .../app-md-formatter.component.ts | 29 + .../components/app-md-formatter/utils.ts | 3 + .../app-panel/app-panel.component.html | 102 + .../app-panel/app-panel.component.ts | 70 + .../shared/components/app-panel/enums.ts | 5 + .../shared/components/app-panel/types.ts | 33 + .../app-select/app-select.component.css | 26 + .../app-select/app-select.component.html | 91 + .../app-select/app-select.component.ts | 227 + .../app-select/app-select.service.ts | 116 + .../shared/components/app-select/types.ts | 10 + .../app-spinner/app-spinner.component.html | 16 + .../app-spinner/app-spinner.component.ts | 16 + .../shared/components/app-spinner/enum.ts | 5 + .../shared/components/app-spinner/types.ts | 3 + .../app-toggle/app-toggle.component.html | 49 + .../app-toggle/app-toggle.component.ts | 56 + .../app-tooltip/app-tooltip.component.css | 50 + .../app-tooltip/app-tooltip.component.html | 45 + .../app-tooltip/app-tooltip.component.ts | 301 + .../shared/components/app-tooltip/enum.ts | 15 + .../shared/components/app-tooltip/types.ts | 4 + .../app/io-assist/shared/constants/agents.ts | 3 + .../shared/constants/ui-prefs-keys.ts | 8 + .../io-assist/shared/constants/ui-strings.ts | 128 + .../accent-gradient-border.directive.ts | 146 + .../directives/animation-effect/README.md | 132 + .../animation-effect.directive.ts | 115 + .../animation-effect/animation-type.enum.ts | 14 + .../core/animation-effect.factory.ts | 36 + .../core/base-animation.effect.ts | 71 + .../core/chained-animation.effect.ts | 137 + .../click-spin-360-animation.effect.ts | 84 + ...er-tilt-click-spin-270-animation.effect.ts | 144 + .../app/io-assist/shared/directives/utils.ts | 24 + .../shared/enums/loading-state.enum.ts | 15 + .../shared/enums/permission-mode.enum.ts | 4 + .../src/app/io-assist/shared/guards/.gitkeep | 0 .../agent-manager/agent-manager.service.ts | 24 + .../shared/services/agent/agent.service.ts | 139 + .../component-effect-manager.service.ts | 168 + .../copy-to-clipboard.service.ts | 50 + .../elicitation/elicitation.service.ts | 251 + .../services/io-ai-web/elicitation/enums.ts | 5 + .../services/io-ai-web/elicitation/utils.ts | 44 + .../services/io-ai-web/io-ai-web.service.ts | 209 + .../services/io-ai-web/sampling/enums.ts | 4 + .../io-ai-web/sampling/sampling.service.ts | 285 + .../shared/services/io-ai-web/types.ts | 7 + .../shared/services/io/io.service.ts | 185 + .../shared/services/logger/logger.service.ts | 61 + .../services/mcp-apps/mcp-apps.service.ts | 255 + .../services/overlay/overlay.service.ts | 123 + .../shared/services/overlay/types.ts | 10 + .../shared/services/prompt/prompt.service.ts | 81 + .../io-assist/shared/services/prompt/types.ts | 19 + .../responsive-ui/responsive-ui.service.ts | 220 + .../subscription-cleanup.service.ts | 100 + .../shared/services/thread/thread.service.ts | 207 + .../shared/services/tools/tools.service.ts | 22 + .../working-context.service.ts | 23 + .../shared/store/agent/agent.actions.enum.ts | 6 + .../shared/store/agent/agent.actions.ts | 9 + .../shared/store/agent/agent.effects.ts | 76 + .../shared/store/agent/agent.facade.ts | 31 + .../shared/store/agent/agent.reducer.ts | 51 + .../shared/store/agent/agent.selector.ts | 10 + .../app-lifecycle.actions.enum.ts | 5 + .../app-lifecycle/app-lifecycle.actions.ts | 7 + .../app-lifecycle/app-lifecycle.effects.ts | 80 + .../app-lifecycle/app-lifecycle.facade.ts | 43 + .../app-lifecycle/app-lifecycle.reducer.ts | 42 + .../app-lifecycle/app-lifecycle.selector.ts | 12 + .../app/io-assist/shared/store/app.state.ts | 40 + .../store/message/message.actions.enum.ts | 19 + .../shared/store/message/message.actions.ts | 68 + .../shared/store/message/message.effects.ts | 229 + .../shared/store/message/message.facade.ts | 121 + .../shared/store/message/message.reducer.ts | 358 ++ .../shared/store/message/message.selector.ts | 41 + .../shared/store/message/message.utils.ts | 612 ++ .../io-assist/shared/store/message/types.ts | 99 + .../store/prompt/prompt.actions.enum.ts | 8 + .../shared/store/prompt/prompt.actions.ts | 19 + .../shared/store/prompt/prompt.effects.ts | 79 + .../shared/store/prompt/prompt.facade.ts | 43 + .../shared/store/prompt/prompt.reducer.ts | 85 + .../shared/store/prompt/prompt.selector.ts | 11 + .../response-stream.actions.enum.ts | 11 + .../response-stream.actions.ts | 20 + .../response-stream/response-stream.facade.ts | 100 + .../response-stream.reducer.ts | 200 + .../response-stream.selector.ts | 52 + .../response-stream/response-stream.types.ts | 34 + .../io-assist/shared/store/thread/enums.ts | 6 + .../store/thread/thread.actions.enum.ts | 12 + .../shared/store/thread/thread.actions.ts | 24 + .../shared/store/thread/thread.effects.ts | 94 + .../shared/store/thread/thread.facade.ts | 60 + .../shared/store/thread/thread.reducer.ts | 110 + .../shared/store/thread/thread.selector.ts | 18 + .../io-assist/shared/store/thread/types.ts | 13 + .../app/io-assist/shared/store/tool/enums.ts | 4 + .../shared/store/tool/tool.actions.enum.ts | 8 + .../shared/store/tool/tool.actions.ts | 14 + .../shared/store/tool/tool.effects.ts | 66 + .../shared/store/tool/tool.facade.ts | 34 + .../shared/store/tool/tool.reducer.ts | 71 + .../shared/store/tool/tool.selector.ts | 15 + .../app/io-assist/shared/store/tool/types.ts | 9 + .../working-context.actions.enum.ts | 9 + .../working-context.actions.ts | 14 + .../working-context.effects.ts | 56 + .../working-context/working-context.facade.ts | 33 + .../working-context.reducer.ts | 74 + .../working-context.selector.ts | 9 + .../app/io-assist/shared/styles/_defaults.css | 297 + .../app/io-assist/shared/styles/_utils.css | 157 + .../io-assist/shared/types/loading.type.ts | 11 + .../app/io-assist/shared/utils/icon.utils.ts | 226 + .../src/app/io-assist/styles.css | 14 + .../io-assist-angular-editable/src/index.html | 1 + .../io-assist-angular-editable/src/styles.css | 8 +- 203 files changed, 18014 insertions(+), 1789 deletions(-) create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/chat/chat.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/chat/chat.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/header/header.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/header/header.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-action-bar/input-area-action-bar.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-action-bar/input-area-action-bar.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-send-button/input-area-send-button.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-send-button/input-area-send-button.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area.component.css create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/messages/assistant-message/assistant-message.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/messages/assistant-message/assistant-message.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/messages/mcp-app-resource/mcp-app-resource.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-area/message-area.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-area/message-area.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-footer/message-footer.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-footer/message-footer.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-message/tool-message.component.css create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-message/tool-message.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-message/tool-message.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-trace-message/tool-trace-message.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-trace-message/tool-trace-message.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/messages/user-message/user-message.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/messages/user-message/user-message.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/prompt/favorite-prompts/favorite-prompt-list.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/prompt/favorite-prompts/favorite-prompt-list.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list-item/prompt-list-item.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list-item/prompt-list-item.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list/prompt-list.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list/prompt-list.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/scroll-area/scroll-area.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/scroll-area/scroll-area.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item-divider/thread-history-list-item-divider.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item-divider/thread-history-list-item-divider.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item/thread-history-list-item.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item/thread-history-list-item.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list/thread-history-list.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list/thread-history-list.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history/thread-history.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history/thread-history.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-info/tool-info.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-info/tool-info.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-list-item.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-list-item.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list/tool-list.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list/tool-list.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/working-context-panel/working-context-panel.component.css create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/working-context-panel/working-context-panel.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/components/working-context-panel/working-context-panel.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/index.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/io-assist.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/io-assist.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/io-assist.config.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/io-assist.providers.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/io-assist.schema.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/io-assist.types.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/scripts/io-assist-angular-build.js create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/app-button.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/app-button.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/enum.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/types.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-copy-to-clipboard-button/app-copy-to-clipboard-button.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-copy-to-clipboard-button/app-copy-to-clipboard-button.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/app-icon.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/app-icon.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/enum.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/icons.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/types.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-input/app-input.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-input/app-input.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/app-md-formatter.component.css create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/app-md-formatter.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/app-md-formatter.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/utils.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/app-panel.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/app-panel.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/enums.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/types.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.component.css create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.service.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/types.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/app-spinner.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/app-spinner.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/enum.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/types.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-toggle/app-toggle.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-toggle/app-toggle.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/app-tooltip.component.css create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/app-tooltip.component.html create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/app-tooltip.component.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/enum.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/types.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/constants/agents.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/constants/ui-prefs-keys.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/constants/ui-strings.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/directives/accent-gradient-border/accent-gradient-border.directive.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/README.md create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/animation-effect.directive.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/animation-type.enum.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/core/animation-effect.factory.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/core/base-animation.effect.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/core/chained-animation.effect.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/effects/click-spin-360-animation.effect.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/effects/hover-tilt-click-spin-270-animation.effect.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/directives/utils.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/enums/loading-state.enum.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/enums/permission-mode.enum.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/guards/.gitkeep create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/agent/agent-manager/agent-manager.service.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/agent/agent.service.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/component-effect-manager/component-effect-manager.service.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/copy-to-clipboard/copy-to-clipboard.service.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/elicitation/elicitation.service.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/elicitation/enums.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/elicitation/utils.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/io-ai-web.service.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/sampling/enums.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/sampling/sampling.service.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/types.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/io/io.service.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/logger/logger.service.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/mcp-apps/mcp-apps.service.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/overlay/overlay.service.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/overlay/types.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/prompt/prompt.service.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/prompt/types.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/responsive-ui/responsive-ui.service.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/subscription-cleanup/subscription-cleanup.service.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/thread/thread.service.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/tools/tools.service.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/services/working-context/working-context.service.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.actions.enum.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.actions.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.effects.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.facade.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.reducer.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.selector.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.actions.enum.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.actions.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.effects.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.facade.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.reducer.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.selector.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/app.state.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.actions.enum.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.actions.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.effects.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.facade.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.reducer.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.selector.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.utils.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/types.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.actions.enum.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.actions.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.effects.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.facade.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.reducer.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.selector.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.actions.enum.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.actions.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.facade.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.reducer.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.selector.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.types.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/enums.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.actions.enum.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.actions.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.effects.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.facade.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.reducer.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.selector.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/types.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/enums.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.actions.enum.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.actions.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.effects.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.facade.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.reducer.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.selector.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/types.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.actions.enum.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.actions.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.effects.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.facade.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.reducer.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.selector.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/styles/_defaults.css create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/styles/_utils.css create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/types/loading.type.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/shared/utils/icon.utils.ts create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/styles.css diff --git a/templates/io-assist-angular-editable/.gitignore b/templates/io-assist-angular-editable/.gitignore index 854acd5..2542831 100644 --- a/templates/io-assist-angular-editable/.gitignore +++ b/templates/io-assist-angular-editable/.gitignore @@ -11,6 +11,10 @@ npm-debug.log yarn-error.log +# Generated by scripts/io-assist-angular-build.js (mirrors @interopio/io-assist-ng's dist/styles.css + dist/files) +/public/styles.css +/public/files + # IDEs and editors .idea/ .project diff --git a/templates/io-assist-angular-editable/README.md b/templates/io-assist-angular-editable/README.md index 8a6dfa2..f12a1da 100644 --- a/templates/io-assist-angular-editable/README.md +++ b/templates/io-assist-angular-editable/README.md @@ -1,59 +1,129 @@ -# IoAssistAngular +# io.Assist Angular — editable (source-owned) -This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 21.2.13. +An Angular application that embeds io.Assist as **editable source** rather than as an npm dependency. +The io.Assist Angular source lives in [`src/app/io-assist`](src/app/io-assist), and you own it. -## Development server +If you only need configuration-level customization, prefer the packaged preset +(`io-assist-angular-packaged`) — it consumes `@interopio/io-assist-ng` and stays on the upgrade path. +This template is the deliberate fork: see [`src/app/io-assist/PROVENANCE.md`](src/app/io-assist/PROVENANCE.md) +for its origin, version, and the one-way ownership contract. -To start a local development server, run: +## Prerequisites — read this first -```bash -ng serve -``` +This application **cannot run on its own**. Opening the dev server URL directly will fail. Two external +pieces are required: -Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. +1. **An io.Connect Browser Platform.** `src/app/app.config.ts` uses `@interopio/browser`, which is a + *client*: it discovers a platform peer and does not create one. With no platform, io.Connect + initialization fails and — by design — io.Assist never starts + (`src/app/io-assist/shared/services/io/io.service.ts` waits for io.Connect readiness before doing + anything else). -## Code scaffolding + Any application built with `@interopio/browser-platform` can host it. This app must then be + registered in that platform's `applications.local` config and **launched from the platform**, not + opened directly. -Angular CLI includes powerful code scaffolding tools. To generate a new component, run: +2. **An agent server** implementing the Agent Protocol, reachable over HTTP. -```bash -ng generate component component-name -``` +## Configure -For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run: +Set the agent server URL in [`src/app/app.config.ts`](src/app/app.config.ts): -```bash -ng generate --help +```ts +const AGENT_SERVER_URL = 'http://localhost:4111'; // ships empty — you must set this ``` -## Building +It ships as `''`, which **fails validation** — AI Web requires a valid URL — so the assistant will not +start until you set it. + +While you're there, `dynamicConfig.user` in [`src/app/app.ts`](src/app/app.ts) is a placeholder +(`acme-advisor`); replace it with your real user identity. -To build the project run: +## Run ```bash -ng build +npm install +npm start # dev server on http://localhost:4103 ``` -This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed. +Then launch this app **from your io.Connect Browser Platform**, not by visiting `:4103` directly. -## Running unit tests +### Local development against the iointel-js monorepo -To execute unit tests with the [Vitest](https://vitest.dev/) test runner, use the following command: +If you have the `iointel-js` monorepo checked out alongside this template, it supplies both +prerequisites: ```bash -ng test -``` - -## Running end-to-end tests +# terminal 1 — platform (io.Connect Home) +cd iointel-js/dev-apps/d-io-cb-home && npm start # http://localhost:4200 -For end-to-end (e2e) testing, run: +# terminal 2 — agent server (Mastra) +cd iointel-js/apps/io-assist-mastra && npm start # http://localhost:4111 -```bash -ng e2e +# terminal 3 — this app +npm start # http://localhost:4103 ``` -Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. - -## Additional Resources +Register this app in `iointel-js/configs/io-connect/io-cb-config-rich.json` under `apps`: + +```json +{ + "name": "io-assist-angular-editable", + "type": "window", + "details": { + "url": "http://localhost:4103", + "channelSelector": { "enabled": true, "type": "single" } + }, + "customProperties": { "includeInWorkspaces": true } +} +``` -For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. +Then open the platform at `http://localhost:4200` and launch `io-assist-angular-editable` from its app +list. The platform needs its own license keys via its `create-local-env` step. + +## Confirming the fork is live + +With the dev server running, edit `UI_STRINGS` in +[`src/app/io-assist/shared/constants/ui-strings.ts`](src/app/io-assist/shared/constants/ui-strings.ts) +and save. The change picks up via HMR without a reload — only possible because the source is in `src/`. +Breakpoints in `src/app/io-assist/**` hit real TypeScript with sourcemaps, not a bundled library. + +## Editing the io.Assist source + +| Area | Location | +|---|---| +| Root component | `src/app/io-assist/io-assist.component.ts` | +| Public surface | `src/app/io-assist/index.ts` | +| Provider setup | `src/app/io-assist/io-assist.providers.ts` | +| Static/dynamic config + validation | `src/app/io-assist/io-assist.config.ts`, `io-assist.schema.ts` | +| UI components | `src/app/io-assist/components/**` | +| State (NgRx store) | `src/app/io-assist/shared/store/**` | +| Side effects / SDK adapters | `src/app/io-assist/shared/services/**` | +| Shared UI primitives | `src/app/io-assist/shared/components/**` | +| Styles and theme tokens | `src/app/io-assist/styles.css`, `src/app/io-assist/shared/styles/**` | +| User-facing strings | `src/app/io-assist/shared/constants/ui-strings.ts` | + +Change the smallest coherent area, and avoid bypassing the AI Web and Agent Protocol contracts to achieve +a UI-only customization. + +Two things about this directory are load-bearing: + +- **Tailwind v4 is required.** `src/app/io-assist/styles.css` uses `@import 'tailwindcss'`, so + `@tailwindcss/postcss` is registered in `.postcssrc.json` at the project root — Angular's application + builder picks up PostCSS config files automatically. Tailwind scans the project for utility classes, + which is what makes the vendored layout work. +- **`src/styles.css` imports `./app/io-assist/styles.css`** instead of + `@interopio/io-assist-ng/styles.css`. + +`*.spec.ts` test files were intentionally not vendored — see +[`src/app/io-assist/PROVENANCE.md`](src/app/io-assist/PROVENANCE.md) for the full list of deviations +from upstream. + +## Scripts + +| | | +|---|---| +| `npm start` | dev server with HMR on port 4103 | +| `npm run build` | `ng build` | +| `npm run watch` | `ng build --watch --configuration development` | +| `npm test` | unit tests (Vitest) | diff --git a/templates/io-assist-angular-editable/angular.json b/templates/io-assist-angular-editable/angular.json index bfdeeb1..92234b9 100644 --- a/templates/io-assist-angular-editable/angular.json +++ b/templates/io-assist-angular-editable/angular.json @@ -38,8 +38,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kB", - "maximumError": "8kB" + "maximumWarning": "8kB", + "maximumError": "16kB" } ], "outputHashing": "all" diff --git a/templates/io-assist-angular-editable/package-lock.json b/templates/io-assist-angular-editable/package-lock.json index fab374d..9ddfa25 100644 --- a/templates/io-assist-angular-editable/package-lock.json +++ b/templates/io-assist-angular-editable/package-lock.json @@ -1,11 +1,11 @@ { - "name": "io-assist-angular-packaged", + "name": "io-assist-angular-editable", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "io-assist-angular-packaged", + "name": "io-assist-angular-editable", "version": "0.0.0", "dependencies": { "@angular/cdk": "^21.2.13", @@ -15,17 +15,34 @@ "@angular/forms": "^21.2.0", "@angular/platform-browser": "^21.2.0", "@angular/router": "^21.2.0", + "@fontsource-variable/inter": "^5.0.0", + "@interopio/ai-web": "^1.1.2", "@interopio/browser": "^4.5.0", + "@interopio/ng": ">=6.0.1", + "@interopio/working-context": "^1.1.1", "@interopio/workspaces-api": "^4.5.0", - "rxjs": "~7.8.0", - "tslib": "^2.3.0" + "@ngrx/effects": ">=20.0.1", + "@ngrx/store": ">=20.0.1", + "@ngrx/store-devtools": ">=20.0.1", + "ngx-remark": "^0.2.1", + "prismjs": "^1.30.0", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "rxjs": "~7.8.2", + "tslib": "^2.8.1", + "unified": "^11.0.0", + "zod": "^3.25.76" }, "devDependencies": { "@angular/build": "^21.2.13", "@angular/cli": "^21.2.13", "@angular/compiler-cli": "^21.2.0", + "@interopio/core": ">=6.0.0", + "@interopio/desktop": ">=6.0.0", + "@tailwindcss/cli": "^4.1.13", "jsdom": "^28.0.0", "prettier": "^3.8.1", + "tailwindcss": "^4.1.13", "typescript": "~5.9.2", "vitest": "^4.0.8" } @@ -477,6 +494,16 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular/cli/node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@angular/common": { "version": "21.2.15", "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.15.tgz", @@ -1133,7 +1160,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" @@ -1146,7 +1172,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -1158,7 +1183,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -1623,6 +1647,15 @@ } } }, + "node_modules/@fontsource-variable/inter": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz", + "integrity": "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@gar/promise-retry": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", @@ -1645,7 +1678,6 @@ "version": "1.19.14", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", - "dev": true, "license": "MIT", "engines": { "node": ">=18.14.1" @@ -2004,6 +2036,19 @@ } } }, + "node_modules/@interopio/ai-web": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@interopio/ai-web/-/ai-web-1.1.2.tgz", + "integrity": "sha512-Y2CvegmCU6o0Jp0TWh3H+/VUe26EwKCDlpQ7VjdxXXJM9QrMkFMFWDgDukFaRhGJzKPjBLgNc6yBEPxkHbPRvg==", + "license": "MIT", + "dependencies": { + "@interopio/browser": "^4.0.0", + "@interopio/desktop": "^6.0.0", + "@interopio/mcp-core": "^1.2.1", + "@interopio/working-context": "^1.1.1", + "@modelcontextprotocol/sdk": "^1.19.1" + } + }, "node_modules/@interopio/browser": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/@interopio/browser/-/browser-4.5.0.tgz", @@ -2161,12 +2206,48 @@ } } }, + "node_modules/@interopio/mcp-core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@interopio/mcp-core/-/mcp-core-1.2.1.tgz", + "integrity": "sha512-6ECVIgyeaB/y00ptlrhT4VatqMKAmeHLTZP6kj8PmUjkKXOaC5/WPrJqGf5meyr6n03IiyFLOp84PB6p8yljbA==", + "license": "MIT", + "dependencies": { + "@interopio/browser": "^4.0.0", + "@interopio/desktop": "^6.0.0", + "@interopio/working-context": "^1.1.1", + "@modelcontextprotocol/sdk": "^1.19.1" + } + }, "node_modules/@interopio/modals-api": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/@interopio/modals-api/-/modals-api-4.5.0.tgz", "integrity": "sha512-TPOtLQXrxp3IwoXzXx2Z6tCrojdN+bG2pNFVn44h6pGYwATOXSCE+V18DdQ1J9XWYK2EoNBv5gTp4pGt2syRJA==", "license": "MIT" }, + "node_modules/@interopio/ng": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@interopio/ng/-/ng-6.3.1.tgz", + "integrity": "sha512-fxOqYnj8xBT0JZPokD0nb4ZI/xU2EfdXjSv5FqciyIhOI1q6LZX0TMXggUaQisUM81jTE1ytyOAg8w01KIMLtA==", + "license": "MIT", + "peerDependencies": { + "@angular/common": ">=19.0.0", + "@angular/core": ">=19.0.0", + "@interopio/browser": ">=3.0.0", + "@interopio/browser-platform": ">=3.0.0", + "@interopio/desktop": ">=5.0.0" + }, + "peerDependenciesMeta": { + "@interopio/browser": { + "optional": true + }, + "@interopio/browser-platform": { + "optional": true + }, + "@interopio/desktop": { + "optional": true + } + } + }, "node_modules/@interopio/otel": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/@interopio/otel/-/otel-0.1.6.tgz", @@ -2253,6 +2334,16 @@ "decoder-validate": "^0.0.2" } }, + "node_modules/@interopio/working-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@interopio/working-context/-/working-context-1.1.1.tgz", + "integrity": "sha512-Vbb4SXxkxHc5VBsh06EvUDCt53frDi5Au13xQjSHjN8eUYvdPOJovsGOrvQgy7xdyQr6p7kdAxP2M92eSb2Pbg==", + "license": "MIT", + "dependencies": { + "@interopio/browser": "^4.0.0", + "@interopio/desktop": "^6.0.0" + } + }, "node_modules/@interopio/workspaces-api": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/@interopio/workspaces-api/-/workspaces-api-4.5.0.tgz", @@ -2451,7 +2542,6 @@ "version": "1.26.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", - "dev": true, "license": "MIT", "dependencies": { "@hono/node-server": "^1.19.9", @@ -2914,6 +3004,47 @@ "@emnapi/runtime": "^1.7.1" } }, + "node_modules/@ngrx/effects": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-21.1.1.tgz", + "integrity": "sha512-JQUv2tYyjkbdsE40WYtJDsiI+3qtn4V5m0Ybr2CYw9zKpVQshDV5MLLAni+SuijlHAWJkw8x4wwnYmNyG/Rmng==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": "^21.0.0", + "@ngrx/store": "21.1.1", + "rxjs": "^6.5.3 || ^7.5.0" + } + }, + "node_modules/@ngrx/store": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-21.1.1.tgz", + "integrity": "sha512-B0/IyBTqBExmwPAmrpcJrhlrn+FeeitvOW8ltf7O3kJb2vRKduV5Oi34GLiwYQ8eAukTbKyWQv/VV02gxLeEMg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": "^21.0.0", + "rxjs": "^6.5.3 || ^7.5.0" + } + }, + "node_modules/@ngrx/store-devtools": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/@ngrx/store-devtools/-/store-devtools-21.1.1.tgz", + "integrity": "sha512-gNRDPCZZCO2uzdamco6xJ7292cZdVumHqOIAqmJsYaHkRQ4sZIKBjwFyYXAtPvS6a+Y9bnkGkHetxkF1iZzDvA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": "^21.0.0", + "@ngrx/store": "21.1.1", + "rxjs": "^6.5.3 || ^7.5.0" + } + }, "node_modules/@npmcli/agent": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.2.tgz", @@ -4588,1701 +4719,1593 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, - "node_modules/@tufjs/canonical-json": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", - "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "node_modules/@tailwindcss/cli": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.3.3.tgz", + "integrity": "sha512-ZvS/n1ZHOBKcVlhkt8l5NNr1EDXk1NboYO5CYDOs6NUmvT9z6bzkwsosaJftY57T/3gWNzWMJzIXLodZC8ssdw==", "dev": true, "license": "MIT", - "engines": { - "node": "^16.14.0 || >=18.0.0" + "dependencies": { + "@parcel/watcher": "2.5.1", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "enhanced-resolve": "^5.24.1", + "mri": "^1.2.0", + "picocolors": "^1.1.1", + "tailwindcss": "4.3.3" + }, + "bin": { + "tailwindcss": "dist/index.mjs" } }, - "node_modules/@tufjs/models": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz", - "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==", + "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", "dev": true, + "hasInstallScript": true, "license": "MIT", "dependencies": { - "@tufjs/canonical-json": "2.0.0", - "minimatch": "^10.1.1" + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" }, "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitejs/plugin-basic-ssl": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.4.tgz", - "integrity": "sha512-HXciTXN/sDBYWgeAD4V4s0DN0g72x5mlxQhHxtYu3Tt8BLa6MzcJZUyDVFCdtjNs3bfENVHVzOsmooTVuNgAAw==", + "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": ">= 10.0.0" }, - "peerDependencies": { - "vite": "^6.0.0 || ^7.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@vitest/expect": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz", - "integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==", + "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.7", - "@vitest/utils": "4.1.7", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@vitest/mocker": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz", - "integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==", + "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.7", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" }, "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz", - "integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==", + "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@vitest/runner": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz", - "integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==", + "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.7", - "pathe": "^2.0.3" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@vitest/snapshot": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz", - "integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==", + "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.7", - "@vitest/utils": "4.1.7", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@vitest/spy": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz", - "integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==", + "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@vitest/utils": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz", - "integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==", + "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.7", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" }, "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@yarnpkg/lockfile": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", - "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/abbrev": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", - "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.6" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 14" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/algoliasearch": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.48.1.tgz", - "integrity": "sha512-Rf7xmeuIo7nb6S4mp4abW2faW8DauZyE2faBIKFaUfP3wnpOvNSbiI5AwVhqBNj0jPgBWEvhyCu0sLjN2q77Rg==", + "node_modules/@tailwindcss/cli/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/abtesting": "1.14.1", - "@algolia/client-abtesting": "5.48.1", - "@algolia/client-analytics": "5.48.1", - "@algolia/client-common": "5.48.1", - "@algolia/client-insights": "5.48.1", - "@algolia/client-personalization": "5.48.1", - "@algolia/client-query-suggestions": "5.48.1", - "@algolia/client-search": "5.48.1", - "@algolia/ingestion": "1.48.1", - "@algolia/monitoring": "1.48.1", - "@algolia/recommend": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" }, "engines": { - "node": ">= 14.0.0" + "node": ">=0.10" } }, - "node_modules/ansi-escapes": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", - "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "node_modules/@tailwindcss/cli/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", "dev": true, "license": "MIT", "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">= 20" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">= 20" } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12" + "node": ">= 20" } }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "18 || 20 || >=22" + "node": ">= 20" } }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.33", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", - "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==", + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=6.0.0" + "node": ">= 20" } }, - "node_modules/beasties": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.4.1.tgz", - "integrity": "sha512-2Imdcw3LznDuxAbJM26RHniOLAzE6WgrK8OuvVXCQtNBS8rsnD9zsSEa3fHl4hHpUY7BYTlrpvtPVbvu9G6neg==", + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "css-select": "^6.0.0", - "css-what": "^7.0.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "htmlparser2": "^10.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.49", - "postcss-media-query-parser": "^0.2.3", - "postcss-safe-parser": "^7.0.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.0.0" + "node": ">= 20" } }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" } }, - "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 20" } }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "18 || 20 || >=22" + "node": ">= 20" } }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" ], + "cpu": [ + "wasm32" + ], + "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" }, "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": ">=14.0.0" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.8" + "node": ">= 20" } }, - "node_modules/cacache": { - "version": "20.0.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.4.tgz", - "integrity": "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==", + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^5.0.0", - "fs-minipass": "^3.0.0", - "glob": "^13.0.0", - "lru-cache": "^11.1.0", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^7.0.2", - "ssri": "^13.0.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": ">= 20" } }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "engines": { - "node": "20 || >=22" + "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "node_modules/@tufjs/models": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz", + "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==", "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^10.1.1" }, "engines": { - "node": ">= 0.4" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "tslib": "^2.4.0" } }, - "node_modules/callback-registry": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/callback-registry/-/callback-registry-2.7.2.tgz", - "integrity": "sha512-VVrtF21DaH0VHeNMkLDd4VGuxsYM3V3l3lwYneKVMU/6X3TRtcQszUwlAcqj2HrLcbP1NyS12LsanUwCykaz/Q==", - "license": "ISC" - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001793", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", - "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "dependencies": { + "@types/ms": "*" } }, - "node_modules/chardet": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", - "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, - "node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", "license": "MIT", "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "@types/unist": "*" } }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.4.tgz", + "integrity": "sha512-HXciTXN/sDBYWgeAD4V4s0DN0g72x5mlxQhHxtYu3Tt8BLa6MzcJZUyDVFCdtjNs3bfENVHVzOsmooTVuNgAAw==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "engines": { - "node": ">=18" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0" } }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "node_modules/@vitest/expect": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz", + "integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==", "dev": true, "license": "MIT", "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/vitest" } }, - "node_modules/cli-spinners": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", - "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "node_modules/@vitest/mocker": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz", + "integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18.20" + "dependencies": { + "@vitest/spy": "4.1.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/cli-truncate": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", - "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "node_modules/@vitest/pretty-format": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz", + "integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==", "dev": true, "license": "MIT", "dependencies": { - "slice-ansi": "^8.0.0", - "string-width": "^8.2.0" - }, - "engines": { - "node": ">=20" + "tinyrainbow": "^3.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 12" + "url": "https://opencollective.com/vitest" } }, - "node_modules/cliui": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", - "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "node_modules/@vitest/runner": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz", + "integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" + "@vitest/utils": "4.1.7", + "pathe": "^2.0.3" }, - "engines": { - "node": ">=20" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/cliui/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "node_modules/@vitest/snapshot": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz", + "integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" + "@vitest/pretty-format": "4.1.7", + "@vitest/utils": "4.1.7", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/vitest" } }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "node_modules/@vitest/spy": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz", + "integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://opencollective.com/vitest" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/@vitest/utils": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz", + "integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "@vitest/pretty-format": "4.1.7", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, - "engines": { - "node": ">=7.0.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "node_modules/@vitest/utils/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, - "node_modules/content-disposition": { + "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } + "license": "BSD-2-Clause" }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", "dev": true, - "license": "MIT", + "license": "ISC", "engines": { - "node": ">= 0.6" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, "engines": { "node": ">= 0.6" } }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=6.6.0" + "node": ">= 14" } }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "dev": true, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "ajv": "^8.0.0" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, - "node_modules/css-select": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", - "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", + "node_modules/algoliasearch": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.48.1.tgz", + "integrity": "sha512-Rf7xmeuIo7nb6S4mp4abW2faW8DauZyE2faBIKFaUfP3wnpOvNSbiI5AwVhqBNj0jPgBWEvhyCu0sLjN2q77Rg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^7.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "nth-check": "^2.1.1" + "@algolia/abtesting": "1.14.1", + "@algolia/client-abtesting": "5.48.1", + "@algolia/client-analytics": "5.48.1", + "@algolia/client-common": "5.48.1", + "@algolia/client-insights": "5.48.1", + "@algolia/client-personalization": "5.48.1", + "@algolia/client-query-suggestions": "5.48.1", + "@algolia/client-search": "5.48.1", + "@algolia/ingestion": "1.48.1", + "@algolia/monitoring": "1.48.1", + "@algolia/recommend": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, - "funding": { - "url": "https://github.com/sponsors/fb55" + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/css-tree": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", "dev": true, "license": "MIT", "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" + "environment": "^1.0.0" }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/css-what": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", - "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/fb55" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/cssstyle": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz", - "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==", + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^5.0.1", - "@csstools/css-syntax-patches-for-csstree": "^1.0.28", - "css-tree": "^3.1.0", - "lru-cache": "^11.2.6" - }, "engines": { - "node": ">=20" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/cssstyle/node_modules/lru-cache": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "engines": { - "node": "20 || >=22" + "node": ">=12" } }, - "node_modules/data-urls": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", - "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0" - }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "18 || 20 || >=22" } }, - "node_modules/date-format": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", - "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", - "license": "MIT", + "node_modules/baseline-browser-mapping": { + "version": "2.10.33", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", + "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, "engines": { - "node": ">=4.0" + "node": ">=6.0.0" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", + "node_modules/beasties": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.4.1.tgz", + "integrity": "sha512-2Imdcw3LznDuxAbJM26RHniOLAzE6WgrK8OuvVXCQtNBS8rsnD9zsSEa3fHl4hHpUY7BYTlrpvtPVbvu9G6neg==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "ms": "^2.1.3" + "css-select": "^6.0.0", + "css-what": "^7.0.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^10.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-media-query-parser": "^0.2.3", + "postcss-safe-parser": "^7.0.1" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=18.0.0" } }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, - "license": "MIT" - }, - "node_modules/decoder-validate": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/decoder-validate/-/decoder-validate-0.0.2.tgz", - "integrity": "sha512-9BsqAH9Zq6CvlxKHkSrZrH2iYlhuhHcrh6uTnDvcsa9P5YEweEzt1ci+X/9STgSCE7b9BA7/QIiwhfUDDWmjxw==", - "license": "MIT" - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" + "dependencies": { + "require-from-string": "^2.0.2" } }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "license": "MIT", "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" }, "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" + "license": "ISC" }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "domelementtype": "^2.3.0" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" + "node": "18 || 20 || >=22" } }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" + "fill-range": "^7.1.1" }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" + "engines": { + "node": ">=8" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" }, "engines": { - "node": ">= 0.4" + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.364", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz", - "integrity": "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, "license": "MIT" }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "node_modules/cacache": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.4.tgz", + "integrity": "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==", "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0" }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", "engines": { - "node": ">=6" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "node_modules/cacache/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "20 || >=22" } }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, "engines": { "node": ">= 0.4" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, "engines": { "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" }, - "engines": { - "node": ">= 0.4" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "node_modules/callback-registry": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/callback-registry/-/callback-registry-2.7.2.tgz", + "integrity": "sha512-VVrtF21DaH0VHeNMkLDd4VGuxsYM3V3l3lwYneKVMU/6X3TRtcQszUwlAcqj2HrLcbP1NyS12LsanUwCykaz/Q==", + "license": "ISC" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", "dev": true, - "hasInstallScript": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT" - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", "license": "MIT", - "engines": { - "node": ">= 0.6" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", "dev": true, "license": "MIT" }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "license": "MIT", "dependencies": { - "eventsource-parser": "^3.0.1" + "readdirp": "^5.0.0" }, "engines": { - "node": ">=18.0.0" + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/eventsource-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", - "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=18.0.0" + "node": ">=18" } }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, "engines": { - "node": ">=12.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", "dev": true, "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, "engines": { - "node": ">= 18" + "node": ">=18.20" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/express-rate-limit": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", - "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", "dev": true, "license": "MIT", "dependencies": { - "ip-address": "^10.2.0" + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" }, "engines": { - "node": ">= 16" + "node": ">=20" }, "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "dev": true, - "license": "MIT", + "license": "ISC", "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "node": ">= 12" } }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">=20" } }, - "node_modules/flatted": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", - "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", - "license": "ISC" - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=6 <7 || >=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/fs-minipass": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "minipass": "^7.0.3" + "color-name": "~1.1.4" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=7.0.0" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } + "license": "MIT" }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "license": "MIT", + "engines": { + "node": ">=18" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "license": "MIT", "engines": { - "node": ">=6.9.0" + "node": ">= 0.6" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true, - "license": "ISC", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": ">= 0.6" } }, - "node_modules/get-east-asian-width": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", - "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", - "dev": true, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6.6.0" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" + "object-assign": "^4", + "vary": "^1" }, "engines": { - "node": ">= 0.4" + "node": ">= 0.10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">= 8" } }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "node_modules/css-select": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "BSD-2-Clause", "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" + "boolbase": "^1.0.0", + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.1" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "node_modules/css-what": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "engines": { - "node": ">= 0.4" + "node": ">= 6" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/hono": { - "version": "4.12.23", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", - "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", + "node_modules/cssstyle": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz", + "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==", "dev": true, "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/hosted-git-info": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", - "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", - "dev": true, - "license": "ISC", "dependencies": { - "lru-cache": "^11.1.0" + "@asamuzakjp/css-color": "^5.0.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.28", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.6" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": ">=20" } }, - "node_modules/hosted-git-info/node_modules/lru-cache": { + "node_modules/cssstyle/node_modules/lru-cache": { "version": "11.5.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", @@ -6292,202 +6315,258 @@ "node": "20 || >=22" } }, - "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", "dev": true, "license": "MIT", "dependencies": { - "@exodus/bytes": "^1.6.0" + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" }, "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/htmlparser2": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", - "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "entities": "^7.0.1" + "engines": { + "node": ">=4.0" } }, - "node_modules/htmlparser2/node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "dev": true, - "license": "BSD-2-Clause", + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, "engines": { - "node": ">=0.12" + "node": ">=6.0" }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "dev": true, - "license": "BSD-2-Clause" + "license": "MIT" }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", "license": "MIT", "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" + "character-entities": "^2.0.0" }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/decoder-validate": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/decoder-validate/-/decoder-validate-0.0.2.tgz", + "integrity": "sha512-9BsqAH9Zq6CvlxKHkSrZrH2iYlhuhHcrh6uTnDvcsa9P5YEweEzt1ci+X/9STgSCE7b9BA7/QIiwhfUDDWmjxw==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", "engines": { "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, "engines": { - "node": ">= 14" + "node": ">=6" } }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", "license": "MIT", "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" + "dequal": "^2.0.0" }, - "engines": { - "node": ">= 14" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/ignore-walk": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", - "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, - "license": "ISC", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "minimatch": "^10.0.3" + "domelementtype": "^2.3.0" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/immutable": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.6.tgz", - "integrity": "sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==", + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "node_modules/electron-to-chromium": { + "version": "1.5.364", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz", + "integrity": "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==", "dev": true, "license": "ISC" }, - "node_modules/ini": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", - "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, - "license": "ISC", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": ">= 0.8" } }, - "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", "dev": true, "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, "engines": { - "node": ">= 12" + "node": ">=10.13.0" } }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "engines": { - "node": ">= 0.10" + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, "license": "MIT", - "optional": true, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", "dev": true, "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, "engines": { "node": ">=18" }, @@ -6495,512 +6574,2360 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", - "optional": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "dev": true, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.4" } }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, "license": "MIT" }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, - "license": "MIT" + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "dev": true, + "hasInstallScript": true, "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, "engines": { "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jose": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", - "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, - "node_modules/jsdom": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", - "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", - "dev": true, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "license": "MIT", - "dependencies": { - "@acemir/cssom": "^0.9.31", - "@asamuzakjp/dom-selector": "^6.8.1", - "@bramus/specificity": "^2.4.2", - "@exodus/bytes": "^1.11.0", - "cssstyle": "^6.0.1", - "data-urls": "^7.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "is-potential-custom-element-name": "^1.0.1", - "parse5": "^8.0.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.0", - "undici": "^7.21.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.1", - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0", - "xml-name-validator": "^5.0.0" - }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "canvas": "^3.0.0" + "node": ">=12" }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" + "dependencies": { + "@types/estree": "^1.0.0" } }, - "node_modules/json-parse-even-better-errors": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", - "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", - "dev": true, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "license": "MIT", "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": ">= 0.6" } }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "dev": true, "license": "MIT" }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", "license": "MIT", - "bin": { - "json5": "lib/cli.js" + "dependencies": { + "eventsource-parser": "^3.0.1" }, "engines": { - "node": ">=6" + "node": ">=18.0.0" } }, - "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", - "dev": true, - "engines": [ - "node >= 0.2.0" - ], - "license": "MIT" - }, - "node_modules/listr2": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", - "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^5.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - }, + "license": "Apache-2.0", "engines": { - "node": ">=20.0.0" + "node": ">=12.0.0" } }, - "node_modules/listr2/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { - "node": ">=18" + "node": ">= 18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" + "ip-address": "^10.2.0" }, "engines": { - "node": ">=18" + "node": ">= 16" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" } }, - "node_modules/lmdb": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.1.tgz", - "integrity": "sha512-NYHA0MRPjvNX+vSw8Xxg6FLKxzAG+e7Pt8RqAQA/EehzHVXq9SxDqJIN3JL1hK0dweb884y8kIh6rkWvPyg9Wg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@harperfast/extended-iterable": "^1.0.3", - "msgpackr": "^1.11.2", - "node-addon-api": "^6.1.0", - "node-gyp-build-optional-packages": "5.2.2", - "ordered-binary": "^1.5.3", - "weak-lru-cache": "^1.2.2" - }, - "bin": { - "download-lmdb-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@lmdb/lmdb-darwin-arm64": "3.5.1", - "@lmdb/lmdb-darwin-x64": "3.5.1", - "@lmdb/lmdb-linux-arm": "3.5.1", - "@lmdb/lmdb-linux-arm64": "3.5.1", - "@lmdb/lmdb-linux-x64": "3.5.1", - "@lmdb/lmdb-win32-arm64": "3.5.1", - "@lmdb/lmdb-win32-x64": "3.5.1" - } + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/log-symbols": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", - "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", - "dependencies": { - "is-unicode-supported": "^2.0.0", - "yoctocolors": "^2.1.1" - }, "engines": { - "node": ">=18" + "node": ">=12.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", - "dev": true, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { - "node": ">=18" + "node": ">= 18.0.0" }, "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/log-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "license": "ISC" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "license": "MIT", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6 <7 || >=8" } }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" + "minipass": "^7.0.3" }, "engines": { - "node": ">=18" - }, + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/log4js": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", - "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", - "license": "Apache-2.0", - "dependencies": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "flatted": "^3.2.7", - "rfdc": "^1.3.0", - "streamroller": "^3.1.5" - }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=8.0" + "node": ">=6.9.0" } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" + "engines": { + "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "dev": true, "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.23", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", + "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore-walk": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", + "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/immutable": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.6.tgz", + "integrity": "sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "28.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", + "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.31", + "@asamuzakjp/dom-selector": "^6.8.1", + "@bramus/specificity": "^2.4.2", + "@exodus/bytes": "^1.11.0", + "cssstyle": "^6.0.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "undici": "^7.21.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", + "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lmdb": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.1.tgz", + "integrity": "sha512-NYHA0MRPjvNX+vSw8Xxg6FLKxzAG+e7Pt8RqAQA/EehzHVXq9SxDqJIN3JL1hK0dweb884y8kIh6rkWvPyg9Wg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@harperfast/extended-iterable": "^1.0.3", + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.5.1", + "@lmdb/lmdb-darwin-x64": "3.5.1", + "@lmdb/lmdb-linux-arm": "3.5.1", + "@lmdb/lmdb-linux-arm64": "3.5.1", + "@lmdb/lmdb-linux-x64": "3.5.1", + "@lmdb/lmdb-win32-arm64": "3.5.1", + "@lmdb/lmdb-win32-x64": "3.5.1" + } + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "license": "Apache-2.0", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-fetch-happen": { + "version": "15.0.6", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.6.tgz", + "integrity": "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/agent": "^4.0.0", + "@npmcli/redact": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/make-fetch-happen": { - "version": "15.0.6", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.6.tgz", - "integrity": "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw==", - "dev": true, - "license": "ISC", + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", "dependencies": { - "@gar/promise-retry": "^1.0.0", - "@npmcli/agent": "^4.0.0", - "@npmcli/redact": "^4.0.0", - "cacache": "^20.0.1", - "http-cache-semantics": "^4.1.1", - "minipass": "^7.0.2", - "minipass-fetch": "^5.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^1.0.0", - "proc-log": "^6.0.0", - "ssri": "^13.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" }, - "node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "dev": true, - "license": "CC0-1.0" + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, "engines": { - "node": ">= 0.8" + "node": ">=8.6" } }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=8.6" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -7010,7 +8937,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -7185,6 +9111,16 @@ "node": ">= 18" } }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -7246,9 +9182,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -7268,12 +9204,25 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, + "node_modules/ngx-remark": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ngx-remark/-/ngx-remark-0.2.2.tgz", + "integrity": "sha512-1iWBOhLoQfOe3/Nwzs3vjSpTKwbhuYlFaIjOu0gFqYzorTxvq/jWqPPZekTdFkguji1PXEkK6ecpFQK10wGepQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.0" + }, + "peerDependencies": { + "@angular/common": ">=20.0.0", + "@angular/core": ">=20.0.0", + "remark": "^15.0.1" + } + }, "node_modules/node-addon-api": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", @@ -7504,7 +9453,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7514,7 +9462,6 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7538,7 +9485,6 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -7551,7 +9497,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -7718,7 +9663,6 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -7728,7 +9672,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7765,7 +9708,6 @@ "version": "8.4.2", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "dev": true, "license": "MIT", "funding": { "type": "opencollective", @@ -7816,16 +9758,15 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=16.20.0" } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", "dev": true, "funding": [ { @@ -7843,7 +9784,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -7901,6 +9842,15 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/proc-log": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", @@ -7929,7 +9879,6 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -7952,7 +9901,6 @@ "version": "6.15.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -7968,7 +9916,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -7978,7 +9925,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "dev": true, "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -8011,11 +9957,76 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/remark": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz", + "integrity": "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8135,7 +10146,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -8161,7 +10171,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, "license": "MIT" }, "node_modules/sass": { @@ -8245,7 +10254,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.3", @@ -8272,7 +10280,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "dev": true, "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", @@ -8292,14 +10299,12 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, "license": "ISC" }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -8312,7 +10317,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8322,7 +10326,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -8342,7 +10345,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -8359,7 +10361,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -8378,7 +10379,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -8580,7 +10580,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -8660,6 +10659,27 @@ "dev": true, "license": "MIT" }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tar": { "version": "7.5.15", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", @@ -8751,11 +10771,23 @@ "dev": true, "license": "MIT" }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.6" @@ -8787,6 +10819,16 @@ "node": ">=20" } }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -8812,7 +10854,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", - "dev": true, "license": "MIT", "dependencies": { "content-type": "^2.0.0", @@ -8831,7 +10872,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -8865,6 +10905,80 @@ "node": ">=20.18.1" } }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -8878,7 +10992,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -8938,12 +11051,39 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", @@ -9183,7 +11323,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -9302,7 +11441,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/ws": { @@ -9433,10 +11571,9 @@ } }, "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "dev": true, + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -9446,11 +11583,20 @@ "version": "3.25.2", "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "dev": true, "license": "ISC", "peerDependencies": { "zod": "^3.25.28 || ^4" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/templates/io-assist-angular-editable/package.json b/templates/io-assist-angular-editable/package.json index e51b498..4e32922 100644 --- a/templates/io-assist-angular-editable/package.json +++ b/templates/io-assist-angular-editable/package.json @@ -1,10 +1,10 @@ { - "name": "io-assist-angular-packaged", + "name": "io-assist-angular-editable", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve --port 4103", - "build": "ng build", + "build": "node src/app/io-assist/scripts/io-assist-angular-build.js", "watch": "ng build --watch --configuration development", "test": "ng test" }, @@ -18,17 +18,34 @@ "@angular/forms": "^21.2.0", "@angular/platform-browser": "^21.2.0", "@angular/router": "^21.2.0", + "@fontsource-variable/inter": "^5.0.0", + "@interopio/ai-web": "^1.1.2", "@interopio/browser": "^4.5.0", + "@interopio/ng": ">=6.0.1", + "@interopio/working-context": "^1.1.1", "@interopio/workspaces-api": "^4.5.0", - "rxjs": "~7.8.0", - "tslib": "^2.3.0" + "@ngrx/effects": ">=20.0.1", + "@ngrx/store": ">=20.0.1", + "@ngrx/store-devtools": ">=20.0.1", + "ngx-remark": "^0.2.1", + "prismjs": "^1.30.0", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "rxjs": "~7.8.2", + "tslib": "^2.8.1", + "unified": "^11.0.0", + "zod": "^3.25.76" }, "devDependencies": { "@angular/build": "^21.2.13", "@angular/cli": "^21.2.13", "@angular/compiler-cli": "^21.2.0", + "@interopio/core": ">=6.0.0", + "@interopio/desktop": ">=6.0.0", + "@tailwindcss/cli": "^4.1.13", "jsdom": "^28.0.0", "prettier": "^3.8.1", + "tailwindcss": "^4.1.13", "typescript": "~5.9.2", "vitest": "^4.0.8" } diff --git a/templates/io-assist-angular-editable/src/app/app.config.ts b/templates/io-assist-angular-editable/src/app/app.config.ts index 574f069..1c92d1c 100644 --- a/templates/io-assist-angular-editable/src/app/app.config.ts +++ b/templates/io-assist-angular-editable/src/app/app.config.ts @@ -1,10 +1,10 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from "@angular/core"; import IOBrowser from "@interopio/browser"; -import { provideIoAssist } from "./io-assist/something"; +import { provideIoAssist } from "./io-assist"; import IOWorkspaces from "@interopio/workspaces-api"; -// this must be defined; -const AGENT_SERVER_URL = ""; +// this must be defined; ships empty — you must set it to your agent server URL +const AGENT_SERVER_URL = "http://localhost:4111"; const createIOConnect: typeof IOBrowser = async (config) => { const io = await IOBrowser(config); diff --git a/templates/io-assist-angular-editable/src/app/app.ts b/templates/io-assist-angular-editable/src/app/app.ts index a83786e..0361676 100644 --- a/templates/io-assist-angular-editable/src/app/app.ts +++ b/templates/io-assist-angular-editable/src/app/app.ts @@ -1,5 +1,5 @@ import { Component, signal } from "@angular/core"; -import { IoAssist, type IoAssistDynamicConfig } from "./io-assist/something"; +import { IoAssist, type IoAssistDynamicConfig } from "./io-assist"; @Component({ selector: "app-root", diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/chat/chat.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/chat/chat.component.html new file mode 100644 index 0000000..0785fc2 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/chat/chat.component.html @@ -0,0 +1,95 @@ +@let showMessageArea = isLoadingMessages() || messageLength() > 0; +@let showWelcomeMessage = !showMessageArea; +@let showFavoritePrompts = !showMessageArea && hasFavoritePrompts(); + +
+ @if (isThreadHistoryDisplayed()) { + + } + +
+ +
+ + +
+ @if (showWelcomeMessage) { +
+

+ {{ UI_STRINGS.GENERAL.WELCOME_MESSAGE(loggedInUser()) }} +

+
+ } + +
+ @if (showMessageArea) { + + } + + + + @if (showFavoritePrompts) { + + } + +
+ {{ UI_STRINGS.GENERAL.AI_CONTENT_INFO }} +
+
+
+
+
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/chat/chat.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/chat/chat.component.ts new file mode 100644 index 0000000..d555852 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/chat/chat.component.ts @@ -0,0 +1,67 @@ +import { CommonModule } from "@angular/common"; +import { Component, computed, inject, OnInit, Signal, WritableSignal } from "@angular/core"; + +import { IO_ASSIST_DYNAMIC_CONFIG, IoAssistDynamicConfig } from "../../io-assist.config"; +import { UI_STRINGS } from "../../shared/constants/ui-strings"; +import { ComponentEffectManagerService as EffectService } from "../../shared/services/component-effect-manager/component-effect-manager.service"; +import { OverlayService } from "../../shared/services/overlay/overlay.service"; +import { ResponsiveUIService } from "../../shared/services/responsive-ui/responsive-ui.service"; +import { MessageFacade } from "../../shared/store/message/message.facade"; +import { PromptFacade } from "../../shared/store/prompt/prompt.facade"; +import { HeaderComponent } from "../header/header.component"; +import { InputAreaComponent } from "../input-area/input-area.component"; +import { MessageAreaComponent } from "../messages/message-area/message-area.component"; +import { FavoritePromptListComponent } from "../prompt/favorite-prompts/favorite-prompt-list.component"; +import { ThreadHistoryComponent } from "../threads/thread-history/thread-history.component"; + +const COMPONENTS = [ThreadHistoryComponent, FavoritePromptListComponent, InputAreaComponent, HeaderComponent, MessageAreaComponent]; +const MODULES = [CommonModule]; + +@Component({ + selector: "chat", + templateUrl: "./chat.component.html", + imports: [MODULES, COMPONENTS], + providers: [ + MessageFacade, + PromptFacade, + { + provide: EffectService, + useFactory: () => new EffectService("ChatComponent"), + }, + ], + host: { + class: "flex flex-1 flex-col bg-app-background min-h-0 min-w-0 text-app-text-default", + }, +}) +export class ChatComponent implements OnInit { + protected readonly UI_STRINGS = UI_STRINGS; + + // Provided in root + private readonly _overlayService: OverlayService = inject(OverlayService); + private readonly _dynamicConfig: Signal = inject(IO_ASSIST_DYNAMIC_CONFIG); + private readonly _responsiveUIService: ResponsiveUIService = inject(ResponsiveUIService); + + // Provided in this component + private readonly _messageFacade: MessageFacade = inject(MessageFacade); + private readonly _promptFacade: PromptFacade = inject(PromptFacade); + private readonly _effectService: EffectService = inject(EffectService); + + protected readonly isLoadingMessages: Signal = this._messageFacade.isLoadingMessages; + protected readonly messageLength: Signal = this._messageFacade.messageLength; + protected readonly loggedInUser = computed(() => this._dynamicConfig().user.name); + + protected readonly isThreadHistoryDisplayed: WritableSignal = this._overlayService.isThreadHistoryDisplayed; + protected readonly hasFavoritePrompts: Signal = computed(() => this._promptFacade.favoritePromptNames().length > 0); + + public ngOnInit(): void { + this.registerEffects(); + } + + private registerEffects(): void { + this._effectService.registerEffect("ChatComponent.autoCloseThreadHistoryOnViewportShrink", () => { + if (this._responsiveUIService.isMdDown) { + this._overlayService.isThreadHistoryDisplayed.set(false); + } + }); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/header/header.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/header/header.component.html new file mode 100644 index 0000000..6f365b5 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/header/header.component.html @@ -0,0 +1,54 @@ +@let showHomeButton = isLoadingMessages() || messageLength() > 0; +@let showThreadPanelToggleButton = !isThreadHistoryDisplayed(); +@let hasNotification = hasAnyCompletionNotification(); + +
+ @if (showThreadPanelToggleButton || showHomeButton) { +
+ @if (showThreadPanelToggleButton) { +
+ + + @if (hasNotification) { + + } +
+ } + + @if (showHomeButton) { + + + {{ UI_STRINGS.HEADER_COMPONENT.HOME_TOOLTIP }} + + } +
+ } + + + @if (isWorkingContextEnabled()) { + + + {{ UI_STRINGS.HEADER_COMPONENT.CONTEXT_TOOLTIP }} + + } +
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/header/header.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/header/header.component.ts new file mode 100644 index 0000000..b7d7cba --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/header/header.component.ts @@ -0,0 +1,86 @@ +import { CommonModule } from "@angular/common"; +import { Component, inject, OnInit, Signal, WritableSignal } from "@angular/core"; + +import { APP_BUTTON_TYPES } from "../../shared/components/app-button/enum"; +import { AppIconComponent } from "../../shared/components/app-icon/app-icon.component"; +import { APP_ICON_SIZES, APP_ICON_VARIANTS } from "../../shared/components/app-icon/enum"; +import { PANEL_BUTTON_ACTION_TYPE } from "../../shared/components/app-panel/enums"; +import { APP_SPINNER_COMPONENT_SIZE } from "../../shared/components/app-spinner/enum"; +import { AppTooltipComponent } from "../../shared/components/app-tooltip/app-tooltip.component"; +import { TOOLTIP_POSITIONS } from "../../shared/components/app-tooltip/enum"; +import { UI_STRINGS } from "../../shared/constants/ui-strings"; +import { AnimationType } from "../../shared/directives/animation-effect/animation-type.enum"; +import { OverlayService } from "../../shared/services/overlay/overlay.service"; +import { MessageFacade } from "../../shared/store/message/message.facade"; +import { ResponseStreamFacade } from "../../shared/store/response-stream/response-stream.facade"; +import { ThreadFacade } from "../../shared/store/thread/thread.facade"; +import { WorkingContextFacade } from "../../shared/store/working-context/working-context.facade"; +import { WorkingContextPanelComponent } from "../working-context-panel/working-context-panel.component"; + +const MODULES = [CommonModule]; +const COMPONENTS = [AppIconComponent, AppTooltipComponent]; + +@Component({ + selector: "header", + templateUrl: "./header.component.html", + imports: [...MODULES, ...COMPONENTS], + providers: [WorkingContextFacade], + host: { + style: "display: flex; width: 100%;", + }, +}) +export class HeaderComponent implements OnInit { + protected readonly APP_SPINNER_COMPONENT_SIZE = APP_SPINNER_COMPONENT_SIZE; + protected readonly UI_STRINGS = UI_STRINGS; + protected readonly APP_ICON_VARIANTS = APP_ICON_VARIANTS; + protected readonly APP_ICON_SIZES = APP_ICON_SIZES; + protected readonly AnimationType = AnimationType; + protected readonly TOOLTIP_POSITIONS = TOOLTIP_POSITIONS; + + // Provided in root + private readonly _overlayService: OverlayService = inject(OverlayService); + private readonly _threadFacade: ThreadFacade = inject(ThreadFacade); + private readonly _responseStreamFacade: ResponseStreamFacade = inject(ResponseStreamFacade); + + // Provided by parent component + private readonly _messageFacade: MessageFacade = inject(MessageFacade); + + // Provided in this component + private readonly _workingContextFacade: WorkingContextFacade = inject(WorkingContextFacade); + + protected readonly isThreadHistoryDisplayed: WritableSignal = this._overlayService.isThreadHistoryDisplayed; + protected readonly isLoadingMessages: Signal = this._messageFacade.isLoadingMessages; + protected readonly messageLength: Signal = this._messageFacade.messageLength; + protected readonly isWorkingContextEnabled: Signal = this._workingContextFacade.isWorkingContextEnabled; + protected readonly hasAnyCompletionNotification: Signal = this._responseStreamFacade.hasAnyCompletionNotification; + + public ngOnInit(): void { + this._workingContextFacade.dispatchFetchIsWorkingContextEnabled(); + } + + protected displayThreadHistory(): void { + this._overlayService.isThreadHistoryDisplayed.set(true); + } + + protected displayHome(): void { + this._messageFacade.dispatchClearMessages(); + + this._threadFacade.dispatchChangeActiveThread(null); + } + + protected handleWorkingContextPanelOpen(): void { + this._overlayService.showPanelOverlay({ + title: UI_STRINGS.WORKING_CONTEXT_PANEL_COMPONENT.TITLE, + content: WorkingContextPanelComponent, + isHeaderCloseButtonDisplayed: true, + footerButtons: [ + { + label: UI_STRINGS.GENERAL.CANCEL, + action: PANEL_BUTTON_ACTION_TYPE.CLOSE, + type: APP_BUTTON_TYPES.DEFAULT, + testId: "working-context-cancel-button", + }, + ], + }); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-action-bar/input-area-action-bar.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-action-bar/input-area-action-bar.component.html new file mode 100644 index 0000000..947f507 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-action-bar/input-area-action-bar.component.html @@ -0,0 +1,30 @@ +
+ + + + + + +
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-action-bar/input-area-action-bar.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-action-bar/input-area-action-bar.component.ts new file mode 100644 index 0000000..6ff1b65 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-action-bar/input-area-action-bar.component.ts @@ -0,0 +1,68 @@ +import { CommonModule } from "@angular/common"; +import { Component, inject, WritableSignal } from "@angular/core"; + +import { AppIconComponent } from "../../../shared/components/app-icon/app-icon.component"; +import { APP_ICON_SIZES, APP_ICON_VARIANTS } from "../../../shared/components/app-icon/enum"; +import { AppSelectComponent } from "../../../shared/components/app-select/app-select.component"; +import { APP_SELECT_IDS } from "../../../shared/components/app-select/app-select.service"; +import { AppSelectOption } from "../../../shared/components/app-select/types"; +import { UI_STRINGS } from "../../../shared/constants/ui-strings"; +import { PERMISSION_MODE } from "../../../shared/enums/permission-mode.enum"; +import { OverlayService } from "../../../shared/services/overlay/overlay.service"; +import { PromptListComponent } from "../../prompt/prompt-list/prompt-list.component"; +import { ToolListComponent } from "../../tool/tool-list/tool-list.component"; + +const MODULES = [CommonModule]; +const COMPONENTS = [AppIconComponent, AppSelectComponent]; + +const PERMISSION_OPTIONS: AppSelectOption[] = [ + { + title: "Ask permissions", + miniTitle: "Ask", + description: "Always ask for action permissions", + isSelected: true, + value: PERMISSION_MODE.ASK, + }, + { + title: "Auto accept permissions", + miniTitle: "Auto", + description: "Auto-approve all action permissions", + value: PERMISSION_MODE.AUTO_ACCEPT, + }, +]; + +@Component({ + selector: "input-area-action-bar", + imports: [...MODULES, ...COMPONENTS], + templateUrl: "./input-area-action-bar.component.html", +}) +export class InputAreaActionBarComponent { + protected readonly UI_STRINGS = UI_STRINGS; + protected readonly APP_ICON_VARIANTS = APP_ICON_VARIANTS; + protected readonly APP_ICON_SIZES = APP_ICON_SIZES; + protected readonly PERMISSION_OPTIONS = PERMISSION_OPTIONS; + protected readonly APP_SELECT_IDS = APP_SELECT_IDS; + + private readonly _overlayService: OverlayService = inject(OverlayService); + + protected readonly isPromptPanelDisplayed: WritableSignal = this._overlayService.isPromptPanelDisplayed; + protected readonly isToolPanelDisplayed: WritableSignal = this._overlayService.isToolPanelDisplayed; + + protected handlePromptPanelOverlayOpen(): void { + this._overlayService.showPanelOverlay({ + title: UI_STRINGS.PROMPT_LIST_COMPONENT.AVAILABLE_PROMPTS, + content: PromptListComponent, + isHeaderCloseButtonDisplayed: true, + isDisplayedSignal: this._overlayService.isPromptPanelDisplayed, + }); + } + + protected handleToolPanelOverlayOpen(): void { + this._overlayService.showPanelOverlay({ + title: UI_STRINGS.TOOL_LIST_COMPONENT.AVAILABLE_TOOLS, + content: ToolListComponent, + isHeaderCloseButtonDisplayed: true, + isDisplayedSignal: this._overlayService.isToolPanelDisplayed, + }); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-send-button/input-area-send-button.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-send-button/input-area-send-button.component.html new file mode 100644 index 0000000..66e5437 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-send-button/input-area-send-button.component.html @@ -0,0 +1,42 @@ +@let showStopButton = isGeneratingResponse() && messageLength() > 0; +@let disableInputButton = (!userQuery().trim() || isLoadingMessagesFromThread()) && !showStopButton; + +@let showDefaultIcon = !showStopButton && disableInputButton; +@let showSendIcon = !showStopButton && !disableInputButton; + +@if (showDefaultIcon) { + +} + +@if (showSendIcon) { + +} + +@if (showStopButton) { + +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-send-button/input-area-send-button.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-send-button/input-area-send-button.component.ts new file mode 100644 index 0000000..98e32e2 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-send-button/input-area-send-button.component.ts @@ -0,0 +1,78 @@ +import { CommonModule } from "@angular/common"; +import { Component, inject, input, InputSignal, output, OutputEmitterRef, Signal } from "@angular/core"; + +import { AppIconComponent } from "../../../shared/components/app-icon/app-icon.component"; +import { APP_ICON_SIZES, APP_ICON_VARIANTS } from "../../../shared/components/app-icon/enum"; +import { MessageFacade } from "../../../shared/store/message/message.facade"; +import { UIUserMessage, UI_MESSAGE_ROLES } from "../../../shared/store/message/types"; +import { ThreadFacade } from "../../../shared/store/thread/thread.facade"; + +const COMPONENTS = [AppIconComponent]; +const MODULES = [CommonModule]; + +@Component({ + selector: "input-area-send-button", + imports: [...MODULES, ...COMPONENTS], + templateUrl: "./input-area-send-button.component.html", +}) +export class InputAreaSendButtonComponent { + public readonly APP_ICON_VARIANTS = APP_ICON_VARIANTS; + public readonly APP_ICON_SIZES = APP_ICON_SIZES; + + public readonly userQuery: InputSignal = input.required(); + public readonly onSend: OutputEmitterRef = output(); + + // Provided in root + private readonly _threadFacade: ThreadFacade = inject(ThreadFacade); + + // Provided by parent + private readonly _messageFacade: MessageFacade = inject(MessageFacade); + + protected readonly messageLength: Signal = this._messageFacade.messageLength; + protected readonly isGeneratingResponse: Signal = this._messageFacade.isGeneratingResponse; + protected readonly isLoadingMessagesFromThread: Signal = this._messageFacade.isLoadingMessagesFromThread; + private readonly _activeThreadId: Signal = this._threadFacade.activeThreadId; + + public handleSendMessage(): void { + if (this.userQuery().trim() === "") { + return; + } + + if (this.isGeneratingResponse() || this.isLoadingMessagesFromThread()) { + return; + } + + const userMessage: UIUserMessage = { + id: crypto.randomUUID(), + role: UI_MESSAGE_ROLES.USER, + content: this.userQuery().trim(), + }; + + // Passing a random thread ID will create a new thread on the backend + const activeThreadId: string = this._activeThreadId() ?? crypto.randomUUID(); + + if (!this._activeThreadId()) { + // If there is no active thread ID, we need to create a new thread + this._threadFacade.dispatchChangeActiveThread(activeThreadId); + } + + this._messageFacade.dispatchGetResponse( + { + messages: [userMessage], + }, + activeThreadId + ); + + this.onSend.emit(); + } + + protected handleStopMessage(): void { + const activeThreadId: string | null = this._activeThreadId(); + + if (!activeThreadId) { + return; + } + + this._messageFacade.dispatchAbortResponseGeneration(activeThreadId); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area.component.css b/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area.component.css new file mode 100644 index 0000000..3c94ed0 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area.component.css @@ -0,0 +1,30 @@ +/* Thin glow strip centered on the top border edge, bleeding upward above the box. */ +.input-area__container::before { + content: ''; + position: absolute; + left: 5%; + right: 5%; + top: -3px; + height: 12px; + pointer-events: none; + z-index: 0; + background: + radial-gradient( + ellipse 40% 100% at 15% 50%, + var(--color-app-accent-color-1), + transparent 100% + ), + radial-gradient( + ellipse 40% 100% at 85% 50%, + var(--color-app-accent-color-2), + transparent 100% + ); + filter: blur(12px); + opacity: 0.75; +} + +/* Box sits above the glow strip so any downward overlap is hidden. */ +.input-area__content { + position: relative; + z-index: 1; +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area.component.html new file mode 100644 index 0000000..2354077 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area.component.html @@ -0,0 +1,68 @@ +
+
+ +
+
+ +
+ +
+ + +
+
+
+
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area.component.ts new file mode 100644 index 0000000..a2ab48a --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area.component.ts @@ -0,0 +1,195 @@ +import { CommonModule } from "@angular/common"; +import { Component, computed, ElementRef, inject, Signal, signal, viewChild, WritableSignal, OnInit } from "@angular/core"; + +import { InputAreaActionBarComponent } from "./input-area-action-bar/input-area-action-bar.component"; +import { InputAreaSendButtonComponent } from "./input-area-send-button/input-area-send-button.component"; +import { UI_STRINGS } from "../../shared/constants/ui-strings"; +import { AccentGradientBorderDirective } from "../../shared/directives/accent-gradient-border/accent-gradient-border.directive"; +import { AnimationType } from "../../shared/directives/animation-effect/animation-type.enum"; +import { ComponentEffectManagerService as EffectService } from "../../shared/services/component-effect-manager/component-effect-manager.service"; +import { Prompt } from "../../shared/services/prompt/types"; +import { MessageFacade } from "../../shared/store/message/message.facade"; +import { UI_MESSAGE_ROLES, UIMessage, UIUserMessage } from "../../shared/store/message/types"; +import { PromptFacade } from "../../shared/store/prompt/prompt.facade"; + +const COMPONENTS = [InputAreaActionBarComponent, InputAreaSendButtonComponent]; +const DIRECTIVES = [AccentGradientBorderDirective]; +const MODULES = [CommonModule]; + +@Component({ + selector: "input-area", + templateUrl: "./input-area.component.html", + styleUrl: "./input-area.component.css", + imports: [...COMPONENTS, ...MODULES, ...DIRECTIVES], + providers: [ + PromptFacade, + { + provide: EffectService, + useFactory: () => new EffectService("InputAreaComponent"), + }, + ], +}) +export class InputAreaComponent implements OnInit { + protected inputValue: WritableSignal = signal(""); + protected isAnimationComplete: WritableSignal = signal(false); + + protected readonly UI_STRINGS = UI_STRINGS.INPUT_COMPONENT; + protected readonly AnimationType = AnimationType; + protected readonly sendButton: Signal = viewChild(InputAreaSendButtonComponent); + protected readonly inputAreaRef: Signal = viewChild("inputTextArea"); + + // Provided in current component + private readonly _promptFacade: PromptFacade = inject(PromptFacade); + private readonly _effectService: EffectService = inject(EffectService); + + // Provided by parent + private readonly _messageFacade: MessageFacade = inject(MessageFacade); + private readonly _userMessages: Signal = computed(() => { + const allMessages: UIMessage[] = this._messageFacade.allMessages(); + + return allMessages.filter((message: UIMessage) => message.role === UI_MESSAGE_ROLES.USER); + }); + private readonly _currentUserMessage: WritableSignal = signal(null); + + private readonly _selectedPrompt: Signal = this._promptFacade.selectedPrompt; + + public ngOnInit(): void { + this.registerEffects(); + } + + protected handleInputChange(event: Event): void { + const target: HTMLTextAreaElement = event.target as HTMLTextAreaElement; + + this.setInputValue(target.value); + } + + protected handleAnimationStart(): void { + this.isAnimationComplete.set(false); + } + + protected handleAnimationEnd(): void { + this.isAnimationComplete.set(true); + } + + protected setInputValue(value: string): void { + this.inputValue.set(value); + + const shouldResetDefaultSize: boolean = value.trim() === ""; + + // Use queueMicrotask to ensure textarea DOM updates are applied before calculating scrollHeight for resize + queueMicrotask(() => this.resizeTextArea(shouldResetDefaultSize)); + } + + protected handleKeyDown(event: KeyboardEvent): void { + if (event.key === "Enter" && event.shiftKey) { + return; + } + + if (event.key === "Enter") { + // Prevent adding a new line + event.preventDefault(); + + // send message + this.sendButton()?.handleSendMessage(); + return; + } + + if (event.key === "ArrowUp" && event.shiftKey) { + this.browseUserMessages("up"); + } + + if (event.key === "ArrowDown" && event.shiftKey) { + this.browseUserMessages("down"); + } + } + + private browseUserMessages(direction: "up" | "down"): void { + const userMessages: UIUserMessage[] = this._userMessages(); + + if (userMessages.length === 0) { + return; + } + + const currentMessage: UIUserMessage | null = this._currentUserMessage(); + + const isAtFirstMessage: boolean = currentMessage?.id === this._userMessages()[0].id && direction === "up"; + + if (isAtFirstMessage) { + return; + } + + const hasReachedLast: boolean = this._currentUserMessage()?.id === this._userMessages()[this._userMessages().length - 1].id && direction === "down"; + + if (hasReachedLast) { + return; + } + + const currentMessageExists: boolean = currentMessage !== null && userMessages.some((msg: UIUserMessage) => msg.id === currentMessage.id); + + if (!currentMessageExists) { + const index: number = direction === "up" ? userMessages.length - 1 : 0; + + this._currentUserMessage.set(userMessages[index]); + + return; + } + + const currentMessageIndex: number = userMessages.findIndex((msg: UIUserMessage) => msg.id === currentMessage?.id); + + if (direction === "up") { + const index: number = currentMessageIndex > 0 ? currentMessageIndex - 1 : 0; + + this._currentUserMessage.set(userMessages[index]); + + return; + } + + const index: number = currentMessageIndex < userMessages.length - 1 ? currentMessageIndex + 1 : userMessages.length - 1; + + this._currentUserMessage.set(userMessages[index]); + } + + private resizeTextArea(shouldResetDefault: boolean): void { + const textarea: HTMLTextAreaElement | undefined = this.inputAreaRef()?.nativeElement; + + if (!textarea) { + return; + } + + textarea.style.height = "17px"; // Always reset to initial height + + if (!shouldResetDefault) { + textarea.style.height = Math.min(textarea.scrollHeight, 260) + "px"; + + return; + } + } + + private registerEffects(): void { + this._effectService.registerEffect("InputArea.updatesInputValue.on.currentUserMessage", () => { + const currentMessage = this._currentUserMessage(); + + if (currentMessage === null) { + return; + } + + this.setInputValue(currentMessage.content || ""); + }); + + this._effectService.registerEffect("InputArea.updatesInputValue.on.selectedPrompt", () => { + const prompt = this._selectedPrompt(); + + if (!prompt) return; + + const description = prompt?.description; + + if (description === undefined || description == null || description.trim() === "") { + return; + } + + this.setInputValue(description); + + this._promptFacade.dispatchClearSelectedPrompt(); + }); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/assistant-message/assistant-message.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/assistant-message/assistant-message.component.html new file mode 100644 index 0000000..6de435f --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/assistant-message/assistant-message.component.html @@ -0,0 +1,15 @@ +@let shouldDisplayFooter = message().displayFooter && message().responseForUserQueryId && message().content !== ""; + +
+ + + @if (shouldDisplayFooter) { + + } +
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/assistant-message/assistant-message.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/assistant-message/assistant-message.component.ts new file mode 100644 index 0000000..e97e907 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/assistant-message/assistant-message.component.ts @@ -0,0 +1,18 @@ +import { CommonModule } from "@angular/common"; +import { Component, input, InputSignal } from "@angular/core"; + +import { AppMdFormatterComponent } from "../../../shared/components/app-md-formatter/app-md-formatter.component"; +import { UIAssistantMessage } from "../../../shared/store/message/types"; +import { MessageFooterComponent } from "../message-footer/message-footer.component"; + +const MODULES = [CommonModule]; +const COMPONENTS = [AppMdFormatterComponent, MessageFooterComponent]; + +@Component({ + selector: "assistant-message", + templateUrl: "./assistant-message.component.html", + imports: [...MODULES, ...COMPONENTS], +}) +export class AssistantMessageComponent { + public readonly message: InputSignal = input.required(); +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/mcp-app-resource/mcp-app-resource.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/mcp-app-resource/mcp-app-resource.component.ts new file mode 100644 index 0000000..78ea198 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/mcp-app-resource/mcp-app-resource.component.ts @@ -0,0 +1,76 @@ +import { AfterViewInit, Component, ElementRef, input, InputSignal, OnChanges, OnDestroy, viewChild, Signal } from "@angular/core"; +import { IoAiWeb } from "@interopio/ai-web"; + +@Component({ + selector: "mcp-app-resource", + template: `
`, + styles: [ + ` + .mcp-app-container { + width: 100%; + } + `, + ], +}) +export class McpAppResourceComponent implements OnChanges, AfterViewInit, OnDestroy { + /** The live AppInstance returned by ai-web. Pass null/undefined when not yet available. */ + public readonly appInstance: InputSignal = input(undefined); + + private readonly _containerRef: Signal = viewChild("container"); + + private _mountedInstanceId: string | null = null; + private _elementAppended = false; + + public ngOnChanges(): void { + this._trackInstance(this.appInstance()); + this._appendElement(this.appInstance()); + } + + public ngAfterViewInit(): void { + // viewChild signals are resolved after view init — retry appending the element + // in case ngOnChanges fired first (before the container ref was available). + this._appendElement(this.appInstance()); + } + + public ngOnDestroy(): void { + this._cleanup(); + } + + /** + * Tracks the current instance ID so we can detect when the input changes. + * The chat-message subscription is managed at the service level (IOIntelWebService), + * not here, so this only guards against redundant element appends. + */ + private _trackInstance(instance: IoAiWeb.McpApps.AppInstance | undefined): void { + if (!instance) return; + if (this._mountedInstanceId === instance.id) return; + this._cleanup(); + this._mountedInstanceId = instance.id; + } + + /** + * Appends the instance's proxy element into the container div. + * Safe to call multiple times — only appends once per instance. + */ + private _appendElement(instance: IoAiWeb.McpApps.AppInstance | undefined): void { + if (!instance?.element) return; + if (this._elementAppended) return; + + const container = this._containerRef()?.nativeElement as HTMLElement | undefined; + if (!container) return; + + container.appendChild(instance.element); + this._elementAppended = true; + } + + private _cleanup(): void { + const container = this._containerRef()?.nativeElement as HTMLElement | undefined; + if (container) { + while (container.firstChild) { + container.removeChild(container.firstChild); + } + } + this._mountedInstanceId = null; + this._elementAppended = false; + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-area/message-area.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-area/message-area.component.html new file mode 100644 index 0000000..70b8da5 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-area/message-area.component.html @@ -0,0 +1,40 @@ + + @if (isLoadingMessagesFromThread()) { +
+ +
+ } + +
+ @for (message of allMessages(); track message.id) { + @if (message.role === UI_MESSAGE_ROLES.USER) { + + + } + + @if (message.role === UI_MESSAGE_ROLES.ASSISTANT) { + + + @for (instance of getMcpAppsForResponse(message.responseForUserQueryId); track instance.id) { + + } + } + + @if (showToolTraceForMessage(message.id)) { + + } + } +
+
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-area/message-area.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-area/message-area.component.ts new file mode 100644 index 0000000..500b34d --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-area/message-area.component.ts @@ -0,0 +1,71 @@ +import { CommonModule } from "@angular/common"; +import { Component, computed, ElementRef, inject, Signal, viewChild } from "@angular/core"; +import { IoAiWeb } from "@interopio/ai-web"; + +import { AppSpinnerComponent } from "../../../shared/components/app-spinner/app-spinner.component"; +import { APP_SPINNER_COMPONENT_SIZE } from "../../../shared/components/app-spinner/enum"; +import { McpAppsService } from "../../../shared/services/mcp-apps/mcp-apps.service"; +import { MessageFacade } from "../../../shared/store/message/message.facade"; +import { ToolTraceState, UI_MESSAGE_ROLES, UIMessage } from "../../../shared/store/message/types"; +import { ScrollAreaComponent } from "../../scroll-area/scroll-area.component"; +import { AssistantMessageComponent } from "../assistant-message/assistant-message.component"; +import { McpAppResourceComponent } from "../mcp-app-resource/mcp-app-resource.component"; +import { ToolTraceMessageComponent } from "../tool-trace-message/tool-trace-message.component"; +import { UserMessageComponent } from "../user-message/user-message.component"; + +const COMPONENTS = [ScrollAreaComponent, AssistantMessageComponent, UserMessageComponent, AppSpinnerComponent, ToolTraceMessageComponent, McpAppResourceComponent]; +const MODULES = [CommonModule]; + +@Component({ + selector: "message-area", + templateUrl: "./message-area.component.html", + imports: [...COMPONENTS, ...MODULES], +}) +export class MessageAreaComponent { + protected readonly UI_MESSAGE_ROLES = UI_MESSAGE_ROLES; + protected readonly APP_SPINNER_COMPONENT_SIZE = APP_SPINNER_COMPONENT_SIZE; + + protected readonly messageAreaRef: Signal = viewChild("messageArea"); + + private readonly _messageFacade: MessageFacade = inject(MessageFacade); + private readonly _mcpAppsService: McpAppsService = inject(McpAppsService); + + protected readonly allMessages: Signal = this._messageFacade.allMessages; + protected readonly isLoadingMessagesFromThread: Signal = this._messageFacade.isLoadingMessagesFromThread; + protected readonly isSuccessFetchedFromThread: Signal = this._messageFacade.isSuccessFetchedFromThread; + protected readonly isLastUserMessage: Signal = this._messageFacade.isLastUserMessage; + protected readonly toolTraceState: Signal = this._messageFacade.toolTraceState; + + /** + * Maps each responseForUserQueryId to the list of MCP App instances + * associated with tool calls from that response cycle. + * Re-evaluates whenever instances or messages change. + */ + protected readonly mcpAppsByResponse: Signal> = computed(() => { + const instances = this._mcpAppsService.instances(); + const messages = this.allMessages(); + const map = new Map(); + + for (const msg of messages) { + if (msg.role !== UI_MESSAGE_ROLES.TOOL) continue; + const instance = instances.find((i) => i.id === msg.id); + if (!instance || !msg.responseForUserQueryId) continue; + + const key = msg.responseForUserQueryId; + const list = map.get(key) ?? []; + list.push(instance); + map.set(key, list); + } + + return map; + }); + + protected getMcpAppsForResponse(responseForUserQueryId: string | undefined): IoAiWeb.McpApps.AppInstance[] { + if (!responseForUserQueryId) return []; + return this.mcpAppsByResponse().get(responseForUserQueryId) ?? []; + } + + protected showToolTraceForMessage(messageId: string): boolean { + return this.toolTraceState().some((state: ToolTraceState) => state.stateForMessageId === messageId && state.executedTools.length > 0); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-footer/message-footer.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-footer/message-footer.component.html new file mode 100644 index 0000000..aeead02 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-footer/message-footer.component.html @@ -0,0 +1,22 @@ + diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-footer/message-footer.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-footer/message-footer.component.ts new file mode 100644 index 0000000..6290a04 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-footer/message-footer.component.ts @@ -0,0 +1,95 @@ +import { CommonModule } from "@angular/common"; +import { Component, inject, input, InputSignal, Signal } from "@angular/core"; + +import { AppCopyToClipboardButtonComponent } from "../../../shared/components/app-copy-to-clipboard-button/app-copy-to-clipboard-button.component"; +import { AppIconComponent } from "../../../shared/components/app-icon/app-icon.component"; +import { APP_ICON_SIZES, APP_ICON_VARIANTS } from "../../../shared/components/app-icon/enum"; +import { AppTooltipComponent } from "../../../shared/components/app-tooltip/app-tooltip.component"; +import { TOOLTIP_POSITIONS } from "../../../shared/components/app-tooltip/enum"; +import { UI_STRINGS } from "../../../shared/constants/ui-strings"; +import { AnimationEffectDirective } from "../../../shared/directives/animation-effect/animation-effect.directive"; +import { AnimationType } from "../../../shared/directives/animation-effect/animation-type.enum"; +import { MessageFacade } from "../../../shared/store/message/message.facade"; +import { responseToText } from "../../../shared/store/message/message.utils"; +import { UI_MESSAGE_ROLES, UIMessage, UIUserMessage } from "../../../shared/store/message/types"; +import { ThreadFacade } from "../../../shared/store/thread/thread.facade"; + +const MODULES = [CommonModule]; +const COMPONENTS = [AppCopyToClipboardButtonComponent, AppIconComponent, AppTooltipComponent]; +const DIRECTIVES = [AnimationEffectDirective]; + +/** + * The message footer component is rendered below each full response message. + * + * Examples: + * + * (1) Simple response: + * ------------- Hello (user message) + * ------------- Hi there! How can I assist you today? (assistant message) + * [ Copy ] [ Reload ] + * + * (2) Long response: + * ------------- Hello (user message) + * ------------- Hello how are you (assistant message) + * ------------- (more assistant message parts) + * ------------- (more assistant message parts) + * ------------- (tool calls) + * ------------- (tool calls) + * ------------- (more assistant message parts) + * [ Copy ] [ Reload ] + * + * Where the buttons in the footer allow the user to copy the entire assistant response + * without the user message + * + * Or regenerate the response by re-sending the last user message + * + * @messageFooterId - The ID of the user message to which this footer belongs + * Needed to be able to fetch the corresponding assistant messages. + * The same as the id of the user message that triggered the response. + */ +@Component({ + selector: "message-footer", + imports: [...MODULES, ...COMPONENTS, ...DIRECTIVES], + templateUrl: "./message-footer.component.html", +}) +export class MessageFooterComponent { + public readonly messageFooterId: InputSignal = input.required(); + + protected readonly APP_ICON_VARIANTS = APP_ICON_VARIANTS; + protected readonly APP_ICON_SIZES = APP_ICON_SIZES; + protected readonly AnimationType = AnimationType; + protected readonly TOOLTIP_POSITIONS = TOOLTIP_POSITIONS; + protected readonly UI_STRINGS = UI_STRINGS; + + // Provided in parent component + private readonly _messageFacade: MessageFacade = inject(MessageFacade); + private readonly _threadFacade: ThreadFacade = inject(ThreadFacade); + + private readonly _allMessages: Signal = this._messageFacade.allMessages; + private readonly _activeThreadId: Signal = this._threadFacade.activeThreadId; + + protected readonly isGeneratingResponse: Signal = this._messageFacade.isGeneratingResponse; + + protected handleResponseToText(): string { + return responseToText(this._allMessages(), this.messageFooterId()); + } + + protected handleRegenerateResponse() { + const messageToReload: UIMessage = this._messageFacade.getMessageById(this.messageFooterId()); + + if (messageToReload.content === "") throw new Error("Trying to reload a message with no content."); + + const userMessage: UIUserMessage = { + ...messageToReload, // All others should be the same + id: crypto.randomUUID(), // only new id is needed + role: UI_MESSAGE_ROLES.USER, // and role explicitly set + }; + + this._messageFacade.dispatchReloadResponse( + { + messages: [userMessage], + }, + this._activeThreadId() + ); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-message/tool-message.component.css b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-message/tool-message.component.css new file mode 100644 index 0000000..b829a7e --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-message/tool-message.component.css @@ -0,0 +1,14 @@ +:host ::ng-deep .io-assist-code-theme::before { + background: none !important; + background-color: transparent !important; +} + +:host ::ng-deep pre .language-json { + max-height: 320px; +} + +@media (min-width: 1024px) { + :host ::ng-deep pre .language-json { + max-height: 400px; + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-message/tool-message.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-message/tool-message.component.html new file mode 100644 index 0000000..f2f5a32 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-message/tool-message.component.html @@ -0,0 +1,54 @@ +@let hasResult = message().result !== undefined && message().result !== null; +@let isExpanded = message().isExpanded !== undefined ? message().isExpanded : false; + +
+
+ + +
+
+ {{ message().toolName }} +
+ + +
+
+ + @if (isExpanded) { +
+
+
+ Input + + + Output + +
+
+
+ } +
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-message/tool-message.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-message/tool-message.component.ts new file mode 100644 index 0000000..064edf5 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-message/tool-message.component.ts @@ -0,0 +1,103 @@ +import { CommonModule } from "@angular/common"; +import { Component, inject, input, InputSignal } from "@angular/core"; + +import { AppIconComponent } from "../../../shared/components/app-icon/app-icon.component"; +import { APP_ICON_SIZES, APP_ICON_VARIANTS } from "../../../shared/components/app-icon/enum"; +import { AppMdFormatterComponent } from "../../../shared/components/app-md-formatter/app-md-formatter.component"; +import { textToJSONMarkdown } from "../../../shared/components/app-md-formatter/utils"; +import { UI_STRINGS } from "../../../shared/constants/ui-strings"; +import { MessageFacade } from "../../../shared/store/message/message.facade"; +import { TOOL_RESULT_TYPES, ToolResult, UIToolMessage } from "../../../shared/store/message/types"; + +const MODULES = [CommonModule]; +const COMPONENTS = [AppIconComponent, AppMdFormatterComponent]; + +@Component({ + selector: "tool-message", + templateUrl: "./tool-message.component.html", + imports: [...MODULES, ...COMPONENTS], + styleUrls: ["./tool-message.component.css"], +}) +export class ToolMessageComponent { + protected readonly APP_ICON_VARIANTS = APP_ICON_VARIANTS; + protected readonly APP_ICON_SIZES = APP_ICON_SIZES; + protected readonly UI_STRINGS = UI_STRINGS.TOOL_MESSAGE; + + public readonly message: InputSignal = input.required(); + public readonly parentToolTraceId: InputSignal = input.required(); + + private readonly _messageFacade: MessageFacade = inject(MessageFacade); + + protected handleToggle(): void { + this._messageFacade.dispatchToggleToolMessage(this.message().id, this.parentToolTraceId()); + } + + protected handleArgsToJSONMarkdown(): string { + return textToJSONMarkdown(this.message().args); + } + + protected handleResultToJSONMarkdown(): string { + const result = this.message().result; + + if (!result) { + return "No result available."; + } + + if (!Array.isArray(result)) { + const parsedResult = this.parseTextResultTextContent(result); + + return textToJSONMarkdown(parsedResult); + } + + const parsedResults: ToolResult[] = result.map((res: ToolResult) => { + if (res.type === TOOL_RESULT_TYPES.TEXT) { + return this.parseTextResultTextContent(res); + } + + /** + * + * TODO: Custom handling for other MCP result types: image, audio + * For now, return as is: + * + * Image Content + * { + * "type": "image", + * "data": "base64-encoded-image-data", + * "mimeType": "image/jpeg" + * } + * + * Audio Content: + * { + * "type": "audio", + * "data": "base64-encoded-audio-data", + * "mimeType": "audio/mpeg" + * } + */ + return res; + }); + + return parsedResults.map((res) => textToJSONMarkdown(res)).join("\n\n---\n\n"); + } + + private parseTextResultTextContent(result: ToolResult): ToolResult { + let parsedText: unknown; + + if (result.type !== TOOL_RESULT_TYPES.TEXT) { + return result; + } + + if (result.type === TOOL_RESULT_TYPES.TEXT) { + try { + parsedText = JSON.parse(result.text as string); + // eslint-disable-next-line unused-imports/no-unused-vars + } catch (_unusedErr: unknown) { + parsedText = result.text; + } + } + + return { + type: result.type, + text: parsedText, + } as ToolResult; + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-trace-message/tool-trace-message.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-trace-message/tool-trace-message.component.html new file mode 100644 index 0000000..f16f888 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-trace-message/tool-trace-message.component.html @@ -0,0 +1,112 @@ +@let uiMessage = currentToolTraceState()?.uiMessage ?? "No UI Message"; +@let executedTools = currentToolTraceState()?.executedTools ?? []; +@let isExpanded = currentToolTraceState()?.isExpanded ?? false; +@let toolTraceId = currentToolTraceState()?.stateForMessageId ?? ""; +@let showFooter = shouldDisplayFooter() && responseForUserQueryId() !== undefined; + +@if (isExpanded) { +
+ +
+
+ {{ uiMessage }} + + @if (executedTools.length > 0) { + + } +
+ + @if (isExpanded && executedTools.length > 0) { +
+ @for (tool of executedTools; track $index) { + + } +
+ + + } +
+
+} + +@if (!isExpanded) { +
+ +
+
+ {{ uiMessage }} + + @if (executedTools.length > 0) { + + } +
+
+
+} + +@if (showFooter) { + +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-trace-message/tool-trace-message.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-trace-message/tool-trace-message.component.ts new file mode 100644 index 0000000..3f5fb06 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-trace-message/tool-trace-message.component.ts @@ -0,0 +1,46 @@ +import { CommonModule } from "@angular/common"; +import { Component, computed, inject, input, InputSignal, Signal } from "@angular/core"; + +import { AppButtonComponent } from "../../../shared/components/app-button/app-button.component"; +import { AppIconComponent } from "../../../shared/components/app-icon/app-icon.component"; +import { APP_ICON_VARIANTS } from "../../../shared/components/app-icon/enum"; +import { UI_STRINGS } from "../../../shared/constants/ui-strings"; +import { AccentGradientBorderDirective } from "../../../shared/directives/accent-gradient-border/accent-gradient-border.directive"; +import { MessageFacade } from "../../../shared/store/message/message.facade"; +import { ToolTraceState } from "../../../shared/store/message/types"; +import { MessageFooterComponent } from "../message-footer/message-footer.component"; +import { ToolMessageComponent } from "../tool-message/tool-message.component"; + +const MODULES = [CommonModule]; +const COMPONENTS = [AppIconComponent, ToolMessageComponent, AppButtonComponent, MessageFooterComponent]; +const DIRECTIVES = [AccentGradientBorderDirective]; + +@Component({ + selector: "tool-trace-message", + templateUrl: "./tool-trace-message.component.html", + imports: [...MODULES, ...COMPONENTS, ...DIRECTIVES], + host: { "data-testid": "tool-trace-host" }, +}) +export class ToolTraceMessageComponent { + protected readonly APP_ICON_VARIANTS = APP_ICON_VARIANTS; + protected readonly UI_STRINGS = UI_STRINGS.TOOL_TRACE_MESSAGE_COMPONENT; + + public readonly parentMessage: InputSignal = input.required(); + + private readonly _messageFacade: MessageFacade = inject(MessageFacade); + + private readonly _toolTraceState: Signal = this._messageFacade.toolTraceState; + protected readonly currentToolTraceState: Signal = computed(() => { + return this._toolTraceState().find((state: ToolTraceState) => state.stateForMessageId === this.parentMessage()); + }); + + protected readonly shouldDisplayFooter: Signal = computed(() => { + return this.currentToolTraceState()?.displayFooter ?? false; + }); + + protected readonly responseForUserQueryId: Signal = computed(() => this.currentToolTraceState()?.responseForUserQueryId); + + protected toggleExpanded(): void { + this._messageFacade.dispatchToggleToolTrace(this.parentMessage()); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/user-message/user-message.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/user-message/user-message.component.html new file mode 100644 index 0000000..ba0bace --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/user-message/user-message.component.html @@ -0,0 +1,51 @@ +@let showLoadingSpinner = isGeneratingResponse() && message().id === lastUserMessage()?.id; +@let showReloadButton = error() && message().id === lastUserMessage()?.id; +@let showError = error() && message().id === lastUserMessage()?.id; + +
+
+
+ @if (showLoadingSpinner) { + + } + + @if (showReloadButton) { + Reload + } +
+ +
+
+ {{ message().content }} +
+
+
+ + @if (showError) { +
+ {{ error() }} +
+ } +
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/user-message/user-message.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/user-message/user-message.component.ts new file mode 100644 index 0000000..24aaf55 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/messages/user-message/user-message.component.ts @@ -0,0 +1,56 @@ +import { CommonModule } from "@angular/common"; +import { Component, inject, input, InputSignal, Signal } from "@angular/core"; + +import { AppIconComponent } from "../../../shared/components/app-icon/app-icon.component"; +import { APP_ICON_SIZES, APP_ICON_VARIANTS } from "../../../shared/components/app-icon/enum"; +import { AppSpinnerComponent } from "../../../shared/components/app-spinner/app-spinner.component"; +import { APP_SPINNER_COMPONENT_SIZE } from "../../../shared/components/app-spinner/enum"; +import { AnimationEffectDirective } from "../../../shared/directives/animation-effect/animation-effect.directive"; +import { AnimationType } from "../../../shared/directives/animation-effect/animation-type.enum"; +import { MessageFacade } from "../../../shared/store/message/message.facade"; +import { UI_MESSAGE_ROLES, UIMessage, UIUserMessage } from "../../../shared/store/message/types"; +import { ThreadFacade } from "../../../shared/store/thread/thread.facade"; + +const MODULES = [CommonModule]; +const COMPONENTS = [AppSpinnerComponent, AppIconComponent]; +const DIRECTIVES = [AnimationEffectDirective]; + +@Component({ + selector: "user-message", + templateUrl: "./user-message.component.html", + imports: [...MODULES, ...COMPONENTS, ...DIRECTIVES], +}) +export class UserMessageComponent { + protected readonly APP_SPINNER_COMPONENT_SIZE = APP_SPINNER_COMPONENT_SIZE; + protected readonly APP_ICON_VARIANTS = APP_ICON_VARIANTS; + protected readonly APP_ICON_SIZES = APP_ICON_SIZES; + protected readonly AnimationType = AnimationType; + + public readonly message: InputSignal = input.required(); + + // Provided in parent component + private readonly _messageFacade: MessageFacade = inject(MessageFacade); + private readonly _threadFacade: ThreadFacade = inject(ThreadFacade); + + protected readonly isGeneratingResponse: Signal = this._messageFacade.isGeneratingResponse; + protected readonly lastUserMessage: Signal = this._messageFacade.lastUserMessage; + protected readonly error: Signal = this._messageFacade.loadingErrorMessage; + private readonly _activeThreadId: Signal = this._threadFacade.activeThreadId; + + protected reloadResponse() { + const messageToReload: UIMessage = this.message(); + + const userMessage: UIUserMessage = { + ...messageToReload, // All others should be the same + id: crypto.randomUUID(), // only new id is needed + role: UI_MESSAGE_ROLES.USER, // and role explicitly set + }; + + this._messageFacade.dispatchReloadResponse( + { + messages: [userMessage], + }, + this._activeThreadId() + ); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/favorite-prompts/favorite-prompt-list.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/favorite-prompts/favorite-prompt-list.component.html new file mode 100644 index 0000000..d5908a2 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/favorite-prompts/favorite-prompt-list.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/favorite-prompts/favorite-prompt-list.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/favorite-prompts/favorite-prompt-list.component.ts new file mode 100644 index 0000000..32a7503 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/favorite-prompts/favorite-prompt-list.component.ts @@ -0,0 +1,10 @@ +import { Component } from "@angular/core"; + +import { PromptListComponent } from "../prompt-list/prompt-list.component"; + +@Component({ + selector: "favorite-prompt-list", + templateUrl: "./favorite-prompt-list.component.html", + imports: [PromptListComponent], +}) +export class FavoritePromptListComponent {} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list-item/prompt-list-item.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list-item/prompt-list-item.component.html new file mode 100644 index 0000000..bbc99a0 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list-item/prompt-list-item.component.html @@ -0,0 +1,37 @@ +@let promptName = prompt().name; + + +@let isFavorite = isCurrentPromptInFavorites(); + +
+ + + + + {{ promptName }} + + +
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list-item/prompt-list-item.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list-item/prompt-list-item.component.ts new file mode 100644 index 0000000..798a838 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list-item/prompt-list-item.component.ts @@ -0,0 +1,71 @@ +import { CommonModule } from "@angular/common"; +import { Component, computed, inject, input, InputSignal, Signal, signal, WritableSignal } from "@angular/core"; + +import { IconResource } from "../../../io-assist.types"; +import { AppIconComponent } from "../../../shared/components/app-icon/app-icon.component"; +import { APP_ICON_VARIANTS } from "../../../shared/components/app-icon/enum"; +import { APP_SPINNER_COMPONENT_SIZE } from "../../../shared/components/app-spinner/enum"; +import { UIPrompt } from "../../../shared/services/prompt/types"; +import { PromptFacade } from "../../../shared/store/prompt/prompt.facade"; + +const MODULES = [CommonModule]; +const COMPONENTS = [AppIconComponent]; + +@Component({ + selector: "prompt-list-item", + templateUrl: "./prompt-list-item.component.html", + imports: [MODULES, COMPONENTS], + host: { + "[class]": "hostClasses()", + "(mouseenter)": "handleMouseEnter()", + "(mouseleave)": "handleMouseLeave()", + }, +}) +export class PromptListItemComponent { + public readonly prompt: InputSignal = input.required(); + public readonly isDisplayedInFavoriteList: InputSignal = input(false); + + protected readonly APP_SPINNER_COMPONENT_SIZE = APP_SPINNER_COMPONENT_SIZE; + protected readonly APP_ICON_VARIANTS = APP_ICON_VARIANTS; + + private readonly _promptFacade: PromptFacade = inject(PromptFacade); + + private readonly _favoritePromptNames: Signal = this._promptFacade.favoritePromptNames; + protected readonly isCurrentPromptInFavorites: Signal = computed(() => { + return this._favoritePromptNames().includes(this.prompt().name); + }); + + protected readonly isHostHoveredSignal: WritableSignal = signal(false); + + protected readonly customIcon: Signal = computed(() => this.prompt().icon); + + protected hostClasses: Signal = computed(() => { + const baseClasses = "flex w-full"; + + // Only apply width constraints on desktop for favorite lists + if (this.isDisplayedInFavoriteList()) { + return `${baseClasses} md:w-auto md:max-w-[352px] md:min-w-[250px] border border-border-default hover:border-border-hover rounded-2xl`; + } + + return baseClasses; + }); + + protected handleMouseEnter(): void { + this.isHostHoveredSignal.set(true); + } + + protected handleMouseLeave(): void { + this.isHostHoveredSignal.set(false); + } + + protected handlePromptSelect(): void { + this._promptFacade.dispatchSelectPrompt(this.prompt()); + } + + protected handlePromptToggleFavorite(event: Event): void { + event.stopPropagation(); + event.preventDefault(); + + this._promptFacade.dispatchToggleFavoritePrompt(this.prompt().name); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list/prompt-list.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list/prompt-list.component.html new file mode 100644 index 0000000..46fbc61 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list/prompt-list.component.html @@ -0,0 +1,139 @@ +@let noAvailablePrompts = promptsToDisplay().length === 0; +@let hasPrompts = promptsToDisplay().length > 0; +@let isFavoriteOnlyList = showFavoriteOnly(); +@let shouldRenderInfoSection = noAvailablePrompts; +@let hasCategories = promptGroups().length > 0; +@let hasAnyPromptsInSystem = hasAnyPrompts(); + +@if (!isFavoriteOnlyList && hasAnyPromptsInSystem) { + +} + +
+ @if (isFavoriteOnlyList) { +
+

{{ UI_STRINGS.FAVORITE_PROMPTS }}

+
+ } + + @if (shouldRenderInfoSection) { +
+ @if (noAvailablePrompts && !isFavoriteOnlyList && !hasAnyPromptsInSystem) { +
+ {{ UI_STRINGS.NO_PROMPTS_CONFIGURED }} + + +
+ } + + @if (noAvailablePrompts && !isFavoriteOnlyList && hasAnyPromptsInSystem) { + {{ UI_STRINGS.NO_PROMPTS }} + } + + @if (noAvailablePrompts && isFavoriteOnlyList) { + {{ UI_STRINGS.NO_FAVORITE_PROMPTS }} + } +
+ } + +
+ @if (hasPrompts) { + + @if (!isFavoriteOnlyList && hasCategories) { + @for (group of promptGroups(); track group.category) { + +
+ {{ group.category }} +
+ + + @for (prompt of group.prompts; track prompt.name) { + + } + } + } + + + @if (isFavoriteOnlyList) { + @for (prompt of promptsToDisplay(); track prompt.name) { + + } + } + } +
+
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list/prompt-list.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list/prompt-list.component.ts new file mode 100644 index 0000000..df4c3c2 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list/prompt-list.component.ts @@ -0,0 +1,100 @@ +import { CommonModule } from "@angular/common"; +import { Component, computed, inject, input, InputSignal, signal, Signal, WritableSignal } from "@angular/core"; + +import { AppInputComponent } from "../../../shared/components/app-input/app-input.component"; +import { APP_SPINNER_COMPONENT_SIZE } from "../../../shared/components/app-spinner/enum"; +import { UI_STRINGS } from "../../../shared/constants/ui-strings"; +import { UIPrompt } from "../../../shared/services/prompt/types"; +import { PromptFacade } from "../../../shared/store/prompt/prompt.facade"; +import { PromptListItemComponent } from "../prompt-list-item/prompt-list-item.component"; + +const MODULES = [CommonModule]; +const COMPONENTS = [PromptListItemComponent, AppInputComponent]; + +type PromptGroup = { + category: string; + prompts: UIPrompt[]; +}; + +const UNCATEGORIZED_CATEGORY_NAME = "Default Prompts"; + +@Component({ + selector: "prompt-list", + templateUrl: "./prompt-list.component.html", + imports: [...COMPONENTS, ...MODULES], + providers: [PromptFacade], +}) +export class PromptListComponent { + private readonly _promptFacade: PromptFacade = inject(PromptFacade); + + public showFavoriteOnly: InputSignal = input(false); + + protected readonly UI_STRINGS = UI_STRINGS.PROMPT_LIST_COMPONENT; + protected readonly APP_SPINNER_COMPONENT_SIZE = APP_SPINNER_COMPONENT_SIZE; + protected readonly UNCATEGORIZED_CATEGORY_NAME = UNCATEGORIZED_CATEGORY_NAME; + + private readonly _prompts: Signal = this._promptFacade.allPrompts; + private readonly _favoritePromptNames: Signal = this._promptFacade.favoritePromptNames; + + protected searchTerm: WritableSignal = signal(""); + + protected readonly hasAnyPrompts: Signal = computed(() => { + return this._prompts().length > 0; + }); + + protected readonly promptsToDisplay: Signal = computed(() => { + const showFavoriteOnly: boolean = this.showFavoriteOnly(); + const favoriteNames: string[] = this._favoritePromptNames(); + const allPrompts: UIPrompt[] = this._prompts(); + const search: string = this.searchTerm().toLowerCase(); + + if (showFavoriteOnly) { + return allPrompts.filter((p) => favoriteNames.includes(p.name)); + } + + if (search) { + return allPrompts.filter((p) => p.name.toLowerCase().includes(search)); + } + + return allPrompts; + }); + + protected readonly promptGroups: Signal = computed(() => { + if (this.showFavoriteOnly()) { + // When showing favorites, don't group by category + return []; + } + + const prompts: UIPrompt[] = this.promptsToDisplay(); + const categoryMap = new Map(); + + for (const prompt of prompts) { + const category: string = prompt.category || UNCATEGORIZED_CATEGORY_NAME; + const existingPrompts: UIPrompt[] = categoryMap.get(category) || []; + + categoryMap.set(category, [...existingPrompts, prompt]); + } + + const groups: PromptGroup[] = Array.from(categoryMap.entries()).map(([category, prompts]) => ({ + category, + prompts: this._sortPromptsAlphabetically(prompts), + })); + + // Sort alphabetically, but 'Default Prompts' last + return groups.sort((a, b) => { + if (a.category === UNCATEGORIZED_CATEGORY_NAME) return 1; + if (b.category === UNCATEGORIZED_CATEGORY_NAME) return -1; + + return a.category.localeCompare(b.category); + }); + }); + + private _sortPromptsAlphabetically(prompts: UIPrompt[]): UIPrompt[] { + return [...prompts].sort((a, b) => a.name.localeCompare(b.name)); + } + + protected onDocsLinkClick(): void { + // TODO: Provide the exact link to io-assist + window.open("https://docs-ai.interop.io/", "_blank", "noopener,noreferrer"); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/scroll-area/scroll-area.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/scroll-area/scroll-area.component.html new file mode 100644 index 0000000..042f612 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/scroll-area/scroll-area.component.html @@ -0,0 +1,29 @@ +
+ + +
+
+ +@if (!isCloseToBottom()) { + +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/scroll-area/scroll-area.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/scroll-area/scroll-area.component.ts new file mode 100644 index 0000000..82b5548 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/scroll-area/scroll-area.component.ts @@ -0,0 +1,137 @@ +import { CommonModule } from "@angular/common"; +import { AfterViewInit, Component, ElementRef, input, InputSignal, OnDestroy, signal, Signal, viewChild, WritableSignal } from "@angular/core"; + +import { AppIconComponent } from "../../shared/components/app-icon/app-icon.component"; +import { APP_ICON_VARIANTS, APP_ICON_SIZES } from "../../shared/components/app-icon/enum"; + +const MODULES = [CommonModule]; +const COMPONENTS = [AppIconComponent]; + +/** + * A reusable scrollable area component that can: + * - wrap around any content/component + * - automatically scroll to the bottom on condition + * - detect if the user is close to the bottom to display a "scroll to bottom" button + * - observe content changes to recalculate scroll position or spacer height changes + * + * Usage: + * + * ... content ... + * + * + * @param isForceScrollToBottomEnabled - if true, the scroll area will automatically scroll to the bottom when new content is added, default is false. + * @param isSpacerExpanded - if true, a spacer element will be added at the bottom to push content up, default is false. + * This is useful when you want to ensure that new content is always visible without user interaction. + * The spacer will collapse gradually as new content grows, until it reaches zero height. Then the content overflows as normal. + * @param elementToObserve - an optional ElementRef to observe for content changes. If not provided, the scroll area itself will be observed. + * + */ +@Component({ + selector: "scroll-area", + templateUrl: "./scroll-area.component.html", + imports: [...MODULES, ...COMPONENTS], + host: { + class: "scroll-area flex flex-col flex-1 min-h-0 min-w-0 relative", + }, +}) +export class ScrollAreaComponent implements AfterViewInit, OnDestroy { + protected readonly APP_ICON_VARIANTS = APP_ICON_VARIANTS; + protected readonly APP_ICON_SIZES = APP_ICON_SIZES; + + public shouldExpandSpacer: InputSignal = input(false); + public isForceScrollToBottomEnabled: InputSignal = input(false); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public elementToObserve: InputSignal | null> = input | null>(null); + + protected readonly scrollArea: Signal = viewChild("scrollArea"); + protected readonly spacerElement: Signal = viewChild("spacerElement"); + protected isCloseToBottom: WritableSignal = signal(true); + + private readonly _threshold: number = 20; // in pixels + private _mutationObserver?: MutationObserver | undefined; + + public ngAfterViewInit(): void { + if (!this.scrollArea()?.nativeElement) return; + + this._mutationObserver = new MutationObserver(() => { + // Calculate if we are close to the bottom before the content changes + this.isCloseToBottom.set(this._calculateIsCloseToBottom()); + + this.handleSpacerBehavior(); + + this.handleOnScroll(); // make sure to recalculate if we are close to bottom after content changes to show/hide button + + if (this.isForceScrollToBottomEnabled()) { + this.scrollToBottom(); + } + }); + + if (!this._mutationObserver) return; + + const elToObserve: HTMLElement = this.elementToObserve() ? this.elementToObserve()?.nativeElement : this.scrollArea()?.nativeElement; + + this._mutationObserver.observe(elToObserve, { + childList: true, + }); + } + + protected scrollToBottom(): void { + if (!this.scrollArea()) return; + + this.scrollArea()?.nativeElement.scrollTo({ + top: this.scrollArea()?.nativeElement.scrollHeight, + behavior: "instant", + }); + } + + protected handleOnScroll(): void { + this.isCloseToBottom.set(this._calculateIsCloseToBottom()); + } + + private _calculateIsCloseToBottom(): boolean { + if (!this.scrollArea()) return false; + + const currentPosition: number = this.scrollArea()?.nativeElement.scrollTop + this.scrollArea()?.nativeElement.offsetHeight; + + const height: number = this.scrollArea()?.nativeElement.scrollHeight; + + return height - currentPosition < this._threshold; + } + + private handleSpacerBehavior(): void { + if (!this.scrollArea() || !this.scrollArea()?.nativeElement) return; + + if (!this.spacerElement() || !this.spacerElement()?.nativeElement) return; + + const scrollElement: HTMLElement = this.scrollArea()?.nativeElement; + const spacerElement: HTMLElement = this.spacerElement()?.nativeElement; + + if (this.shouldExpandSpacer()) { + // Push up the spacer to the height of the scroll area minus a small offset to completely hiding content + // 118px because 32px come from the last element padding bottom and we want a total of 150 + spacerElement.style.minHeight = `${scrollElement.clientHeight - 118}px`; + + this.scrollToBottom(); + return; + } + + // If spacer is not expanded, reduce its height based on the overflow as content grows inside scroll area + const overflow: number = scrollElement.scrollHeight - scrollElement.scrollTop - scrollElement.clientHeight; + + // We should not forget the threshold here as it plays part in showing the scroll to bottom button + if (overflow <= this._threshold) return; + + // Reduce spacer height by the overflow amount + const newHeight: number = Math.max(spacerElement.offsetHeight - overflow, 0); + + spacerElement.style.minHeight = `${newHeight}px`; + } + + public ngOnDestroy(): void { + if (!this._mutationObserver) return; + + this._mutationObserver.disconnect(); + this._mutationObserver = undefined; + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item-divider/thread-history-list-item-divider.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item-divider/thread-history-list-item-divider.component.html new file mode 100644 index 0000000..f1c2faa --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item-divider/thread-history-list-item-divider.component.html @@ -0,0 +1,8 @@ +{{ dividerLabel() }} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item-divider/thread-history-list-item-divider.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item-divider/thread-history-list-item-divider.component.ts new file mode 100644 index 0000000..aae41f9 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item-divider/thread-history-list-item-divider.component.ts @@ -0,0 +1,11 @@ +import { CommonModule } from "@angular/common"; +import { Component, input, InputSignal } from "@angular/core"; + +@Component({ + selector: "thread-history-list-item-divider", + templateUrl: "./thread-history-list-item-divider.component.html", + imports: [CommonModule], +}) +export class ThreadHistoryListItemDividerComponent { + public dividerLabel: InputSignal = input(""); +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item/thread-history-list-item.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item/thread-history-list-item.component.html new file mode 100644 index 0000000..7a65ef5 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item/thread-history-list-item.component.html @@ -0,0 +1,72 @@ +@let isCurrentlyActive = activeThreadId() === thread().id; +@let isTryingToDeleteThread = thread().state === THREAD_STATE.DELETING; +@let isTryingToRenameThread = thread().state === THREAD_STATE.RENAMING; +@let isThreadActionPending = isThreadStreaming() || isTryingToDeleteThread || isTryingToRenameThread; +@let hasNotification = hasCompletionNotification(); +@let showNotificationBadge = hasNotification && !isCurrentlyActive; + +
+
+ @if (!isEditModeEnabled()) { +
+ @if (showNotificationBadge) { + + } + + + {{ thread().title }} + + + @if (isThreadActionPending) { + + } +
+ } + + @if (isEditModeEnabled()) { + + } + + + +
+
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item/thread-history-list-item.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item/thread-history-list-item.component.ts new file mode 100644 index 0000000..2b1dcb6 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item/thread-history-list-item.component.ts @@ -0,0 +1,127 @@ +import { CommonModule } from "@angular/common"; +import { Component, computed, ElementRef, inject, input, InputSignal, Signal, signal, viewChild, WritableSignal } from "@angular/core"; + +import { AppIconComponent } from "../../../shared/components/app-icon/app-icon.component"; +import { APP_ICON_SIZES, APP_ICON_VARIANTS } from "../../../shared/components/app-icon/enum"; +import { AppSpinnerComponent } from "../../../shared/components/app-spinner/app-spinner.component"; +import { APP_SPINNER_COMPONENT_SIZE } from "../../../shared/components/app-spinner/enum"; +import { UI_STRINGS } from "../../../shared/constants/ui-strings"; +import { ComponentEffectManagerService as EffectService } from "../../../shared/services/component-effect-manager/component-effect-manager.service"; +import { OverlayService } from "../../../shared/services/overlay/overlay.service"; +import { ResponsiveUIService } from "../../../shared/services/responsive-ui/responsive-ui.service"; +import { MessageFacade } from "../../../shared/store/message/message.facade"; +import { ResponseStreamFacade } from "../../../shared/store/response-stream/response-stream.facade"; +import { THREAD_STATE } from "../../../shared/store/thread/enums"; +import { ThreadFacade } from "../../../shared/store/thread/thread.facade"; +import { UIThread } from "../../../shared/store/thread/types"; + +const COMPONENTS = [AppSpinnerComponent, AppIconComponent]; +const MODULES = [CommonModule]; + +@Component({ + selector: "thread-history-list-item", + templateUrl: "./thread-history-list-item.component.html", + imports: [...MODULES, ...COMPONENTS], + providers: [ + { + provide: EffectService, + useFactory: () => new EffectService("ThreadHistoryListItemComponent"), + }, + ], +}) +export class ThreadHistoryListItemComponent { + protected readonly UI_STRINGS = UI_STRINGS.GENERAL; + protected readonly APP_SPINNER_COMPONENT_SIZE = APP_SPINNER_COMPONENT_SIZE; + protected readonly THREAD_STATE = THREAD_STATE; + protected readonly APP_ICON_VARIANTS = APP_ICON_VARIANTS; + protected readonly APP_ICON_SIZES = APP_ICON_SIZES; + + public readonly thread: InputSignal = input.required(); + protected isEditModeEnabled: WritableSignal = signal(false); + + protected threadNameInput: Signal = viewChild("threadNameInput"); + + // Provided in root + private readonly _threadFacade: ThreadFacade = inject(ThreadFacade); + private readonly _messageFacade: MessageFacade = inject(MessageFacade); + private readonly _responseStreamFacade: ResponseStreamFacade = inject(ResponseStreamFacade); + private readonly _overlayService: OverlayService = inject(OverlayService); + private readonly _responsiveUIService: ResponsiveUIService = inject(ResponsiveUIService); + + // Provided in current component + private readonly _effectService: EffectService = inject(EffectService); + + protected readonly activeThreadId: Signal = this._threadFacade.activeThreadId; + + private readonly _streamingThreadIds: Signal> = this._responseStreamFacade.streamingThreadIds; + private readonly _threadsWithNotification: Signal = this._responseStreamFacade.threadsWithCompletionNotification; + + protected readonly isThreadStreaming: Signal = computed(() => { + return this._streamingThreadIds().has(this.thread().id); + }); + + protected readonly hasCompletionNotification: Signal = computed(() => { + return this._threadsWithNotification().includes(this.thread().id); + }); + + constructor() { + this.registerEffects(); + } + + protected handleEditModeEnable(): void { + this.isEditModeEnabled.set(true); + } + + protected handleEditModeDisable(): void { + this.isEditModeEnabled.set(false); + } + + protected handleRenameOnBlur(newName: string): void { + // isEditModeEnabled is set to false synchronously by handleEditModeDisable() before + // Angular removes the input from the DOM. Removing a focused input fires blur, so + // checking the signal here lets us skip the rename when Escape (or Enter) already + // called handleEditModeDisable() — the blur is just an artifact of DOM removal. + if (!this.isEditModeEnabled()) return; + this.handleEditModeDisable(); + this.onRenameThread(newName); + } + + protected handleThreadSelection(): void { + if (this.isEditModeEnabled()) return; + + const threadId: string = this.thread().id; + + this._responseStreamFacade.dispatchClearCompletionNotification(threadId); + + if (threadId === this.activeThreadId()) { + return this.handleThreadHistoryHide(); + } + + this._threadFacade.dispatchChangeActiveThread(threadId); + this._messageFacade.dispatchFetchMessagesFromThread(this.thread()); + + this.handleThreadHistoryHide(); + } + + private handleThreadHistoryHide(): void { + if (!this._responsiveUIService.isSmDown) { + return; + } + + this._overlayService.isThreadHistoryDisplayed.set(false); + } + + protected onRenameThread(newName: string): void { + if (newName.trim() === "" || newName === this.thread().title) return; + + this._threadFacade.dispatchRenameThread(this.thread(), newName); + } + + protected onDeleteThread(thread: UIThread): void { + this._threadFacade.dispatchDeleteThread(thread); + } + + private registerEffects(): void { + this._effectService.registerEffect("ThreadHistoryListItem.focusThreadNameInput.on.isEditModeEnabled", () => this.isEditModeEnabled() && this.threadNameInput()?.nativeElement.focus()); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list/thread-history-list.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list/thread-history-list.component.html new file mode 100644 index 0000000..c5fbfb2 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list/thread-history-list.component.html @@ -0,0 +1,39 @@ +@let isLoading = isFetchingThreads(); +@let hasNoThreads = !isLoading && threadsWithDivider().length === 0; +@let hasThreads = !isLoading && threadsWithDivider().length > 0; + +@if (isLoading) { + +} + +@if (hasNoThreads) { +

+ {{ UI_STRINGS.NO_THREADS }} +

+} + +@if (hasThreads) { + @for (threadWithDivider of threadsWithDivider(); track threadWithDivider.thread.id) { + @if (threadWithDivider.showDivider) { + + } + + + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list/thread-history-list.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list/thread-history-list.component.ts new file mode 100644 index 0000000..4aa95bf --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list/thread-history-list.component.ts @@ -0,0 +1,118 @@ +import { CommonModule } from "@angular/common"; +import { Component, computed, inject, Signal, OnInit, WritableSignal } from "@angular/core"; + +import { AppSpinnerComponent } from "../../../shared/components/app-spinner/app-spinner.component"; +import { APP_SPINNER_COMPONENT_SIZE } from "../../../shared/components/app-spinner/enum"; +import { SELECTED_AGENT } from "../../../shared/constants/agents"; +import { UI_STRINGS } from "../../../shared/constants/ui-strings"; +import { ComponentEffectManagerService as EffectService } from "../../../shared/services/component-effect-manager/component-effect-manager.service"; +import { OverlayService } from "../../../shared/services/overlay/overlay.service"; +import { MessageFacade } from "../../../shared/store/message/message.facade"; +import { ThreadFacade } from "../../../shared/store/thread/thread.facade"; +import { UIThread } from "../../../shared/store/thread/types"; +import { ThreadHistoryListItemComponent } from "../thread-history-list-item/thread-history-list-item.component"; +import { ThreadHistoryListItemDividerComponent } from "../thread-history-list-item-divider/thread-history-list-item-divider.component"; + +export type ThreadWithDivider = { + thread: UIThread; + showDivider: boolean; + divider: string; +}; + +const MODULES = [CommonModule]; +const COMPONENTS = [ThreadHistoryListItemComponent, ThreadHistoryListItemDividerComponent, AppSpinnerComponent]; + +@Component({ + selector: "thread-history-list", + templateUrl: "./thread-history-list.component.html", + imports: [...MODULES, ...COMPONENTS], + providers: [ + { + provide: EffectService, + useFactory: () => new EffectService("ThreadHistoryListComponent"), + }, + ], +}) +export class ThreadHistoryListComponent implements OnInit { + protected readonly UI_STRINGS = UI_STRINGS.THREAD_HISTORY_COMPONENT; + protected readonly UI_STRINGS_GENERAL = UI_STRINGS.GENERAL; + protected readonly APP_SPINNER_COMPONENT_SIZE = APP_SPINNER_COMPONENT_SIZE; + + // Provided in root + private readonly _threadFacade: ThreadFacade = inject(ThreadFacade); + private readonly _overlayService: OverlayService = inject(OverlayService); + + // Provided by parent + private readonly _messageFacade: MessageFacade = inject(MessageFacade); + + private readonly _effectService: EffectService = inject(EffectService); + + protected readonly isFetchingThreads: Signal = this._threadFacade.isFetchingThreads; + protected readonly threads: Signal = this._threadFacade.allThreads; + protected readonly threadsWithDivider: Signal = computed(() => { + const result: ThreadWithDivider[] = []; + let lastDivider: string = ""; + + // Perform sort by date + const filteredThreads: UIThread[] = [...this.threads()].sort( + (a: UIThread, b: UIThread) => new Date(b.updatedAt ? b.updatedAt : b.createdAt).getTime() - new Date(a.updatedAt ? a.updatedAt : a.createdAt).getTime() + ); + + filteredThreads.forEach((thread: UIThread) => { + const divider: string = this.getDividerLabel(new Date(thread.updatedAt ? thread.updatedAt : thread.createdAt)); + const showDivider: boolean = divider !== lastDivider; + lastDivider = divider; + + result.push({ thread, showDivider, divider }); + }); + + return result; + }); + + private readonly _isThreadHistoryDisplayed: WritableSignal = this._overlayService.isThreadHistoryDisplayed; + private readonly _isSuccessLastResponse: Signal = this._messageFacade.isLastResponseSuccess; + + public ngOnInit(): void { + this.registerEffects(); + + // TODO: Configure agentId and resourceId properly + this._threadFacade.dispatchFetchThreads(SELECTED_AGENT); + } + + public getDividerLabel(date: Date): string { + const diffMs: number = Date.now() - date.getTime(); + const diffMinutes: number = Math.floor(diffMs / (1000 * 60)); + const diffHours: number = Math.floor(diffMs / (1000 * 60 * 60)); + const diffDays: number = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffMinutes < 5) return this.UI_STRINGS.DIVIDERS.JUST_NOW; + if (diffMinutes < 30) return this.UI_STRINGS.DIVIDERS.FIVE_MINS_AGO; + if (diffMinutes < 60) return this.UI_STRINGS.DIVIDERS.THIRTY_MINS_AGO; + if (diffHours < 1) return this.UI_STRINGS.DIVIDERS.ONE_HOUR_AGO; + if (diffHours < 2) return this.UI_STRINGS.DIVIDERS.TWO_HOURS_AGO; + if (diffHours < 6) return this.UI_STRINGS.DIVIDERS.SIX_HOURS_AGO; + if (diffHours < 12) return this.UI_STRINGS.DIVIDERS.TWELVE_HOURS_AGO; + if (diffDays === 0) return this.UI_STRINGS.DIVIDERS.TODAY; + if (diffDays === 1) return this.UI_STRINGS.DIVIDERS.YESTERDAY; + if (diffDays === 2) return this.UI_STRINGS.DIVIDERS.TWO_DAYS_AGO; + if (diffDays < 7) return this.UI_STRINGS.DIVIDERS.LAST_WEEK; + if (diffDays < 14) return this.UI_STRINGS.DIVIDERS.TWO_WEEKS_AGO; + if (diffDays < 30) return this.UI_STRINGS.DIVIDERS.ONE_MONTH_AGO; + if (diffDays < 90) return this.UI_STRINGS.DIVIDERS.THREE_MONTHS_AGO; + if (diffDays < 180) return this.UI_STRINGS.DIVIDERS.SIX_MONTHS_AGO; + + return this.UI_STRINGS.DIVIDERS.MORE_THAN_YEAR_AGO; + } + + private registerEffects(): void { + this._effectService.registerEffect("ThreadHistoryListComponent.shouldRefreshThreadList.when.successfulResponse", () => { + const shouldRefresh: boolean = this._isThreadHistoryDisplayed() && this._isSuccessLastResponse(); + + if (!shouldRefresh) { + return; + } + + this._threadFacade.dispatchFetchThreads(SELECTED_AGENT); + }); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history/thread-history.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history/thread-history.component.html new file mode 100644 index 0000000..1c15490 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history/thread-history.component.html @@ -0,0 +1,146 @@ +
+ +
+ + + + + @if (isDarkMode()) { + + + + + + + + + + + + } + + @if (!isDarkMode()) { + + + + + + + + + + + + } +
+ + + + + + @if (hasUsername()) { + + } + + + +
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history/thread-history.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history/thread-history.component.ts new file mode 100644 index 0000000..4626329 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history/thread-history.component.ts @@ -0,0 +1,45 @@ +import { CommonModule } from "@angular/common"; +import { Component, computed, inject, Signal } from "@angular/core"; + +import { IO_ASSIST_DYNAMIC_CONFIG, IoAssistDynamicConfig } from "../../../io-assist.config"; +import { AppIconComponent } from "../../../shared/components/app-icon/app-icon.component"; +import { APP_ICON_SIZES, APP_ICON_VARIANTS } from "../../../shared/components/app-icon/enum"; +import { APP_SPINNER_COMPONENT_SIZE } from "../../../shared/components/app-spinner/enum"; +import { UI_STRINGS } from "../../../shared/constants/ui-strings"; +import { IOConnectService } from "../../../shared/services/io/io.service"; +import { OverlayService } from "../../../shared/services/overlay/overlay.service"; +import { ThreadFacade } from "../../../shared/store/thread/thread.facade"; +import { ThreadHistoryListComponent } from "../thread-history-list/thread-history-list.component"; + +@Component({ + selector: "thread-history", + templateUrl: "./thread-history.component.html", + imports: [CommonModule, ThreadHistoryListComponent, AppIconComponent], +}) +export class ThreadHistoryComponent { + protected readonly UI_STRINGS = UI_STRINGS.GENERAL; + protected readonly APP_ICON_VARIANTS = APP_ICON_VARIANTS; + protected readonly APP_ICON_SIZES = APP_ICON_SIZES; + protected readonly APP_SPINNER_COMPONENT_SIZE = APP_SPINNER_COMPONENT_SIZE; + + // Provided in root + private readonly _overlayService: OverlayService = inject(OverlayService); + private readonly _threadFacade: ThreadFacade = inject(ThreadFacade); + private readonly _ioConnectService: IOConnectService = inject(IOConnectService); + private readonly _dynamicConfig: Signal = inject(IO_ASSIST_DYNAMIC_CONFIG); + + protected readonly threadsLength: Signal = this._threadFacade.threadLength; + protected readonly isDarkMode: Signal = this._ioConnectService.isDarkMode; + protected readonly username = computed(() => this._dynamicConfig().user.name); + + protected readonly hasUsername = computed(() => !!this._dynamicConfig().user.name); + + protected readonly userInitial = computed(() => { + const name = this._dynamicConfig().user.name; + return name ? name.charAt(0).toUpperCase() : "U"; + }); + + protected hideThreadHistory(): void { + this._overlayService.isThreadHistoryDisplayed.set(false); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-info/tool-info.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-info/tool-info.component.html new file mode 100644 index 0000000..e88e721 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-info/tool-info.component.html @@ -0,0 +1,6 @@ +
+
+ Server name: + {{ tool().source.mcpName }} +
+
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-info/tool-info.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-info/tool-info.component.ts new file mode 100644 index 0000000..49f0de9 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-info/tool-info.component.ts @@ -0,0 +1,16 @@ +import { CommonModule } from "@angular/common"; +import { Component, input, InputSignal } from "@angular/core"; + +import { UI_STRINGS } from "../../../../shared/constants/ui-strings"; +import { UITool } from "../../../../shared/store/tool/types"; + +@Component({ + selector: "tool-info", + templateUrl: "./tool-info.component.html", + imports: [CommonModule], +}) +export class ToolInfoComponent { + protected readonly UI_STRINGS = UI_STRINGS.TOOL_INFO; + + public readonly tool: InputSignal = input.required(); +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-list-item.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-list-item.component.html new file mode 100644 index 0000000..acacdb0 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-list-item.component.html @@ -0,0 +1,34 @@ +@let toolName = tool().name; +
+ + + + {{ toolName }} + + + + + + + +
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-list-item.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-list-item.component.ts new file mode 100644 index 0000000..23ffee4 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-list-item.component.ts @@ -0,0 +1,56 @@ +import { CommonModule } from "@angular/common"; +import { Component, computed, inject, input, InputSignal, signal, Signal, WritableSignal } from "@angular/core"; + +import { ToolInfoComponent } from "./tool-info/tool-info.component"; +import { AppIconComponent } from "../../../shared/components/app-icon/app-icon.component"; +import { APP_ICON_SIZES, APP_ICON_VARIANTS } from "../../../shared/components/app-icon/enum"; +import { APP_SPINNER_COMPONENT_SIZE } from "../../../shared/components/app-spinner/enum"; +import { AppToggleComponent } from "../../../shared/components/app-toggle/app-toggle.component"; +import { AppTooltipComponent } from "../../../shared/components/app-tooltip/app-tooltip.component"; +import { TOOLTIP_POSITIONS } from "../../../shared/components/app-tooltip/enum"; +import { UI_STRINGS } from "../../../shared/constants/ui-strings"; +import { USER_TOOL_STATE } from "../../../shared/store/tool/enums"; +import { ToolFacade } from "../../../shared/store/tool/tool.facade"; +import { UITool } from "../../../shared/store/tool/types"; + +const COMPONENTS = [AppToggleComponent, AppIconComponent, AppTooltipComponent, ToolInfoComponent]; +const MODULES = [CommonModule]; + +@Component({ + selector: "tool-list-item", + templateUrl: "./tool-list-item.component.html", + imports: [...COMPONENTS, ...MODULES], + host: { + "(mouseenter)": "handleMouseEnter()", + "(mouseleave)": "handleMouseLeave()", + }, +}) +export class ToolListItemComponent { + public readonly tool: InputSignal = input.required(); + + protected readonly APP_SPINNER_COMPONENT_SIZE = APP_SPINNER_COMPONENT_SIZE; + protected readonly UI_STRINGS = UI_STRINGS.TOOL_LIST_ITEM_COMPONENT; + protected readonly APP_ICON_VARIANTS = APP_ICON_VARIANTS; + protected readonly APP_ICON_SIZES = APP_ICON_SIZES; + protected readonly TOOLTIP_POSITIONS = TOOLTIP_POSITIONS; + + private readonly _toolFacade: ToolFacade = inject(ToolFacade); + private readonly _enabledTools: Signal = this._toolFacade.enabledTools; + + protected isHovered: WritableSignal = signal(false); + + protected readonly isEnabled: Signal = computed(() => this._enabledTools().includes(this.tool())); + protected readonly isOperationPending: Signal = computed(() => this.tool().state !== USER_TOOL_STATE.IDLE); + + protected handleToolToggle(tool: UITool): void { + return this._toolFacade.dispatchToggleTool(tool); + } + + protected handleMouseEnter(): void { + this.isHovered.set(true); + } + + protected handleMouseLeave(): void { + this.isHovered.set(false); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list/tool-list.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list/tool-list.component.html new file mode 100644 index 0000000..4802845 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list/tool-list.component.html @@ -0,0 +1,81 @@ +@let isLoadingTools = isLoading(); +@let hasTools = tools().length > 0; +@let hasFilteredTools = !isLoadingTools && filteredTools().length > 0; + +@if (!isLoadingTools && hasTools) { + + +} + +
+ @if (isLoadingTools) { + {{ UI_STRINGS.LOADING_TOOLS }} + } + + @if (hasFilteredTools) { + @for (tool of filteredTools(); track tool.name) { + + } + } + + @if (hasTools && !hasFilteredTools) { + {{ UI_STRINGS.NO_FILTERED_TOOLS }} + } + + @if (!hasTools && !isLoadingTools) { +
+ {{ UI_STRINGS.NO_TOOLS }} + + +
+ } +
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list/tool-list.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list/tool-list.component.ts new file mode 100644 index 0000000..db1713e --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list/tool-list.component.ts @@ -0,0 +1,44 @@ +import { CommonModule } from "@angular/common"; +import { Component, computed, inject, OnInit, signal, Signal, WritableSignal } from "@angular/core"; + +import { AppInputComponent } from "../../../shared/components/app-input/app-input.component"; +import { UI_STRINGS } from "../../../shared/constants/ui-strings"; +import { ToolFacade } from "../../../shared/store/tool/tool.facade"; +import { UITool } from "../../../shared/store/tool/types"; +import { ToolListItemComponent } from "../tool-list-item/tool-list-item.component"; + +const MODULES = [CommonModule]; +const COMPONENTS = [ToolListItemComponent, AppInputComponent]; + +@Component({ + selector: "tool-list", + templateUrl: "./tool-list.component.html", + imports: [...MODULES, ...COMPONENTS], + providers: [ToolFacade], +}) +export class ToolListComponent implements OnInit { + protected readonly UI_STRINGS = UI_STRINGS.TOOL_LIST_COMPONENT; + + private readonly _toolFacade: ToolFacade = inject(ToolFacade); + + protected readonly tools: Signal = this._toolFacade.allTools; + protected readonly isLoading: Signal = this._toolFacade.isFetchingTools; + + protected searchTerm: WritableSignal = signal(""); + + protected readonly filteredTools: Signal = computed(() => { + if (!this.searchTerm()) { + return this.tools(); + } + return this.tools().filter((tool) => tool.name.toLowerCase().includes(this.searchTerm().toLowerCase())); + }); + + public ngOnInit(): void { + this._toolFacade.dispatchFetchTools(); + } + + public onDocsLinkClick(): void { + // TODO: Provide the exact link to io-assist + window.open("https://docs-ai.interop.io/", "_blank", "noopener,noreferrer"); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/working-context-panel/working-context-panel.component.css b/templates/io-assist-angular-editable/src/app/io-assist/components/working-context-panel/working-context-panel.component.css new file mode 100644 index 0000000..5353cd0 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/working-context-panel/working-context-panel.component.css @@ -0,0 +1,5 @@ +:host ::ng-deep remark-prism.io-assist-code-theme > pre { + height: 30vh; + max-height: 620px; +} + diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/working-context-panel/working-context-panel.component.html b/templates/io-assist-angular-editable/src/app/io-assist/components/working-context-panel/working-context-panel.component.html new file mode 100644 index 0000000..7cd3306 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/working-context-panel/working-context-panel.component.html @@ -0,0 +1,11 @@ +
+
+ + +

+ {{ UI_STRINGS.CALLOUT_TEXT }} +

+
+ + +
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/working-context-panel/working-context-panel.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/components/working-context-panel/working-context-panel.component.ts new file mode 100644 index 0000000..4833d98 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/components/working-context-panel/working-context-panel.component.ts @@ -0,0 +1,58 @@ +import { CommonModule } from "@angular/common"; +import { Component, inject, OnInit, Signal } from "@angular/core"; +import { IoIntelWorkingContext } from "@interopio/working-context"; + +import { AppIconComponent } from "../../shared/components/app-icon/app-icon.component"; +import { APP_ICON_SIZES, APP_ICON_VARIANTS } from "../../shared/components/app-icon/enum"; +import { AppMdFormatterComponent } from "../../shared/components/app-md-formatter/app-md-formatter.component"; +import { textToJSONMarkdown } from "../../shared/components/app-md-formatter/utils"; +import { UI_STRINGS } from "../../shared/constants/ui-strings"; +import { SubscriptionCleanupService } from "../../shared/services/subscription-cleanup/subscription-cleanup.service"; +import { WorkingContextService } from "../../shared/services/working-context/working-context.service"; +import { WorkingContextFacade } from "../../shared/store/working-context/working-context.facade"; + +const MODULES = [CommonModule]; +const COMPONENTS = [AppIconComponent, AppMdFormatterComponent]; + +@Component({ + selector: "working-context-panel", + templateUrl: "./working-context-panel.component.html", + styleUrls: ["./working-context-panel.component.css"], + imports: [...MODULES, ...COMPONENTS], + providers: [WorkingContextFacade, SubscriptionCleanupService], +}) +export class WorkingContextPanelComponent implements OnInit { + protected readonly UI_STRINGS = UI_STRINGS.WORKING_CONTEXT_PANEL_COMPONENT; + protected readonly APP_ICON_VARIANTS = APP_ICON_VARIANTS; + protected readonly APP_ICON_SIZES = APP_ICON_SIZES; + + private readonly WORKING_CONTEXT_CHANGES_SUB_KEY = "working-context-changes-panel-subscription"; + + private readonly _workingContextService: WorkingContextService = inject(WorkingContextService); + private readonly _subscriptionCleanupService: SubscriptionCleanupService = inject(SubscriptionCleanupService); + + // Provided in this component + private readonly _workingContextFacade: WorkingContextFacade = inject(WorkingContextFacade); + + protected readonly workingContext: Signal> = this._workingContextFacade.workingContext; + + private _unsubscribeFn: (() => void) | undefined | null; + + public ngOnInit(): void { + this._workingContextFacade.dispatchGetWorkingContext(); + + this._unsubscribeFn = this._workingContextService.onWorkingContextChange((workingContext) => { + this._workingContextFacade.dispatchUpdateWorkingContext(workingContext); + }); + + if (!this._unsubscribeFn) { + return; + } + + this._subscriptionCleanupService.add(this.WORKING_CONTEXT_CHANGES_SUB_KEY, this._unsubscribeFn); + } + + protected textToJSONMarkdown(): string { + return textToJSONMarkdown(this.workingContext()); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/index.ts b/templates/io-assist-angular-editable/src/app/io-assist/index.ts new file mode 100644 index 0000000..7727a36 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/index.ts @@ -0,0 +1,8 @@ +/* + * Public API Surface of io-assist-ng + */ + +export * from "./io-assist.component"; +export * from "./io-assist.providers"; +export * from "./io-assist.types"; +export * from "./io-assist.config"; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/io-assist.component.html b/templates/io-assist-angular-editable/src/app/io-assist/io-assist.component.html new file mode 100644 index 0000000..6e6da49 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/io-assist.component.html @@ -0,0 +1,36 @@ +@let chatUIReadyToDisplay = isIoReady() && isAppCoreServicesStarted(); +@let showAppCoreServicesError = isAppCoreServicesError() && !isAppCoreServicesPending(); +@let showError = !isIoReady() || showAppCoreServicesError; +@let isOperationPending = isAppCoreServicesPending() && !isAppCoreServicesStarted(); + +
+ @if (!chatUIReadyToDisplay) { +
+ @if (showError) { + @if (!isIoReady()) { +

{{ UI_STRINGS.CANNOT_CONNECT_TO_IO }}

+ } + + @if (showAppCoreServicesError) { +

{{ appCoreServicesError() }}

+

{{ UI_STRINGS.IO_INTEL_INIT_ERROR }}

+ +

{{ UI_STRINGS.DOCUMENTATION_LINK() }}

+ + + } + } + + @if (isOperationPending) { + +

{{ UI_STRINGS.INITIALIZING_SERVICES }}

+ } +
+ } + + @if (chatUIReadyToDisplay) { + + } +
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/io-assist.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/io-assist.component.ts new file mode 100644 index 0000000..b9a7299 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/io-assist.component.ts @@ -0,0 +1,66 @@ +import { CommonModule } from "@angular/common"; +import { Component, computed, inject, input, OnInit, Signal } from "@angular/core"; + +import { ChatComponent } from "./components/chat/chat.component"; +import { IO_ASSIST_DYNAMIC_CONFIG, IoAssistDynamicConfig } from "./io-assist.config"; +import { IoAssistDynamicConfigSchema } from "./io-assist.schema"; +import { AppSpinnerComponent } from "./shared/components/app-spinner/app-spinner.component"; +import { APP_SPINNER_COMPONENT_SIZE } from "./shared/components/app-spinner/enum"; +import { UI_STRINGS } from "./shared/constants/ui-strings"; +import { ComponentEffectManagerService as EffectService } from "./shared/services/component-effect-manager/component-effect-manager.service"; +import { IOConnectService } from "./shared/services/io/io.service"; +import { AppLifecycleFacade } from "./shared/store/app-lifecycle/app-lifecycle.facade"; + +const COMPONENTS = [AppSpinnerComponent, ChatComponent]; +const MODULES = [CommonModule]; + +@Component({ + selector: "io-assist", + imports: [...COMPONENTS, ...MODULES], + templateUrl: "./io-assist.component.html", + providers: [ + { + provide: EffectService, + useFactory: () => new EffectService("IoAssist"), + }, + ], +}) +export class IoAssist implements OnInit { + protected readonly UI_STRINGS = UI_STRINGS.GENERAL; + protected readonly APP_SPINNER_COMPONENT_SIZE = APP_SPINNER_COMPONENT_SIZE; + + // Dynamic config input — passed by the consumer after login. + // Synced into IO_ASSIST_DYNAMIC_CONFIG so root-level services can read it. + private readonly _dynamicConfigToken = inject(IO_ASSIST_DYNAMIC_CONFIG); + readonly config = input.required(); + + // Provided in root + private readonly _ioService: IOConnectService = inject(IOConnectService); + + // Provided in current component + private readonly _effectService: EffectService = inject(EffectService); + + private readonly _appLifecycleFacade: AppLifecycleFacade = inject(AppLifecycleFacade); + + protected readonly isIoReady: Signal = this._ioService.isIoConnectReady; + protected readonly isAppCoreServicesStarted: Signal = this._appLifecycleFacade.isAppCoreServicesStarted; + protected readonly appCoreServicesError: Signal = this._appLifecycleFacade.appCoreServicesErrorMessage; + protected readonly isAppCoreServicesError: Signal = computed(() => this._appLifecycleFacade.appCoreServicesErrorMessage() !== null); + protected readonly isAppCoreServicesPending: Signal = this._appLifecycleFacade.isPendingAppCoreServicesOperation; + + public ngOnInit(): void { + const parsedConfig = IoAssistDynamicConfigSchema.parse(this.config()); + + this._dynamicConfigToken.set(parsedConfig); + + this.registerEffects(); + } + + protected refreshPage(): void { + window.location.reload(); + } + + private registerEffects(): void { + this._effectService.registerEffect("IoAssist.subscribeToThemeChanges", () => this.isIoReady() && this._ioService.startIODependentTasks()); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/io-assist.config.ts b/templates/io-assist-angular-editable/src/app/io-assist/io-assist.config.ts new file mode 100644 index 0000000..c7ceccb --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/io-assist.config.ts @@ -0,0 +1,48 @@ +import { InjectionToken, WritableSignal } from "@angular/core"; +import { IoAiWeb } from "@interopio/ai-web"; +import { IOConnectNgSettings } from "@interopio/ng"; +import { IoIntelWorkingContextFactoryFunction, IoIntelWorkingContext } from "@interopio/working-context"; + +import { IoAssistPromptCategory } from "./io-assist.types"; + +export type IoAssistUserConfig = { + id: string; + name?: string; +}; + +/** + * Runtime config passed as a component input to ``. + * Decoupled from the static provider config so it can be set after login. + */ +export type IoAssistDynamicConfig = { + user: IoAssistUserConfig; + agentServer?: { + /** Request headers sent with every agent call (e.g. per-user auth tokens). */ + headers?: Record; + }; +}; + +export type IoAssistStaticConfig = { + connectConfig: IOConnectNgSettings; + aiWebConfig: AIWebConfig; + defaultAgentName?: string; + workingContext?: { + factory: IoIntelWorkingContextFactoryFunction; + config?: IoIntelWorkingContext.Config; + }; + defaultPrompts?: IoAssistPromptCategory[]; +}; + +export type AIWebConfig = { + agentServer: Omit; + mcp?: IoAiWeb.WebConfig["mcp"]; +}; + +export const IO_ASSIST_CONFIG = new InjectionToken("IO_ASSIST_CONFIG"); + +/** + * Holds a writable signal that the `IoAssist` component populates from its + * `config` input. Root-level services (agent, thread effects) inject this to + * read the current user at call time rather than at construction time. + */ +export const IO_ASSIST_DYNAMIC_CONFIG = new InjectionToken>("IO_ASSIST_DYNAMIC_CONFIG"); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/io-assist.providers.ts b/templates/io-assist-angular-editable/src/app/io-assist/io-assist.providers.ts new file mode 100644 index 0000000..b860336 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/io-assist.providers.ts @@ -0,0 +1,41 @@ +import { EnvironmentProviders, makeEnvironmentProviders, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection, signal, Type } from "@angular/core"; +import { provideIoConnect } from "@interopio/ng"; +import { provideEffects } from "@ngrx/effects"; +import { provideStore } from "@ngrx/store"; +import { provideStoreDevtools } from "@ngrx/store-devtools"; + +import { IO_ASSIST_CONFIG, IO_ASSIST_DYNAMIC_CONFIG, IoAssistStaticConfig, IoAssistDynamicConfig } from "./io-assist.config"; +import { IoAssistStaticConfigSchema } from "./io-assist.schema"; +import { appEffects, appReducers } from "./shared/store/app.state"; + +export function provideIoAssist(config: IoAssistStaticConfig): EnvironmentProviders { + console.info("Trying to start IoAssist with configuration:", { + agentServerBaseUrl: config.aiWebConfig?.agentServer?.baseUrl, + defaultAgentName: config.defaultAgentName, + }); + + const parsedConfig = IoAssistStaticConfigSchema.parse(config); + + if (!parsedConfig) { + throw new Error("Invalid IoAssist configuration provided."); + } + + console.info("IoAssist configuration parsed successfully:", { + agentServerBaseUrl: parsedConfig.aiWebConfig?.agentServer?.baseUrl, + defaultAgentName: parsedConfig.defaultAgentName, + }); + + return makeEnvironmentProviders([ + { provide: IO_ASSIST_CONFIG, useValue: parsedConfig }, + { + provide: IO_ASSIST_DYNAMIC_CONFIG, + useFactory: () => signal({ user: { id: "" } }), + }, + provideBrowserGlobalErrorListeners(), + provideZonelessChangeDetection(), + provideEffects(...(appEffects as Type[])), + provideStore(appReducers), + provideStoreDevtools({ maxAge: 25 }), + provideIoConnect(config.connectConfig), + ]); +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/io-assist.schema.ts b/templates/io-assist-angular-editable/src/app/io-assist/io-assist.schema.ts new file mode 100644 index 0000000..6827e32 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/io-assist.schema.ts @@ -0,0 +1,135 @@ +import { IoAiWeb } from "@interopio/ai-web"; +import { z, ZodType } from "zod"; + +import { AIWebConfig, IoAssistStaticConfig, IoAssistDynamicConfig } from "./io-assist.config"; +import { IoAssistPrompt, IoAssistPromptCategory } from "./io-assist.types"; + +const MCPRemoteServerConfigSchema = z.object({ + streamableHttp: z.object({ + url: z.string().url(), + name: z.string(), + options: z.any().optional(), // pass-through options for StreamableHTTPClientTransport, maybe do full validation later + }), +}) satisfies ZodType; + +const MCPClientConfigSchema = z.object({ + enforceStrictCapabilities: z.boolean().optional(), + debouncedNotificationMethods: z.array(z.string()).optional(), + capabilities: z.object({ + sampling: z + .object({ + handler: z.function().args(z.string(), z.any()).returns(z.any()).describe("A function that handles sampling requests from the MCP server."), + }) + .optional() + .describe("Configuration for sampling capabilities."), + elicitation: z + .object({ + handler: z.function().args(z.string(), z.any()).returns(z.any()).describe("A function that handles elicitation requests from the MCP server."), + }) + .optional() + .describe("Configuration for elicitation capabilities."), + extensions: z.record(z.any()).optional(), + experimental: z.record(z.any()).optional(), + }), +}) satisfies ZodType; + +const MCPConfigSchema = z.object({ + clientsConfig: MCPClientConfigSchema.optional(), + ioIntel: z + .object({ + remote: MCPRemoteServerConfigSchema.optional(), + web: z + .object({ + enabled: z.boolean().optional(), + hasPriority: z.boolean().optional(), + }) + .optional(), + }) + .optional(), + remoteServers: z.array(MCPRemoteServerConfigSchema).optional(), + mcpApps: z + .object({ + sandboxProxyUrl: z.string(), + displayMode: z.enum(["inline", "workspace"]).optional(), + }) + .optional(), +}); + +const AgentServerConfigSchema = z.object({ + baseUrl: z.string().url(), + retries: z.number().int().min(0).optional(), + backoffMs: z.number().int().min(0).optional(), + maxBackoffMs: z.number().int().min(0).optional(), + abortSignal: z.any().optional(), + credentials: z.enum(["omit", "same-origin", "include"]).optional(), +}); + +/** + * Schema for validating IOConnectNg settings objects. + */ +const IOConnectNgSettingsSchema = z.record(z.string(), z.any()); + +const AIWebConfigSchema = z.object({ + agentServer: AgentServerConfigSchema, + mcp: MCPConfigSchema.optional(), +}) as ZodType; + +/** + * Schema for validating working context configuration objects. + */ +const WorkingContextConfigSchema = z.object({ + factory: z.function().args(z.any(), z.any()).returns(z.promise(z.any())).describe("A factory function to create the working context API instance."), + config: z.any().describe("Configuration object for the working context factory."), +}) satisfies ZodType; + +/** + * Schema for validating individual IoAssist prompt objects. + */ +const IconResourceSchema = z.object({ + type: z.enum(["svg", "url", "data-url"]), + data: z.string().min(1), +}); + +// Strip invalid iconResource rather than rejecting the whole config parse. +// The app's prompt.service.ts independently validates iconResource at runtime. +const IoAssistPromptSchema = z.object({ + name: z.string().min(1), + prompt: z.string().min(1), + iconResource: IconResourceSchema.optional().catch(undefined), +}) as unknown as ZodType; + +const IoAssistPromptCategorySchema = z.object({ + category: z.string().optional(), + prompts: z.array(IoAssistPromptSchema), +}) satisfies ZodType; + +/** + * Schema for validating user configuration. + */ +const IoAssistUserConfigSchema = z.object({ + id: z.string().min(1, "User id is required and must be a non-empty string."), + name: z.string().optional(), +}); + +/** + * Main schema for validating IoAssist configuration objects. + */ +export const IoAssistStaticConfigSchema = z.object({ + connectConfig: IOConnectNgSettingsSchema, + aiWebConfig: AIWebConfigSchema, + defaultAgentName: z.string().optional(), + workingContext: WorkingContextConfigSchema.optional(), + defaultPrompts: z.array(IoAssistPromptCategorySchema).optional(), +}) satisfies ZodType; + +/** + * Schema for validating the dynamic config passed as a component input. + */ +export const IoAssistDynamicConfigSchema = z.object({ + user: IoAssistUserConfigSchema, + agentServer: z + .object({ + headers: z.record(z.string()).optional(), + }) + .optional(), +}) satisfies ZodType; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/io-assist.types.ts b/templates/io-assist-angular-editable/src/app/io-assist/io-assist.types.ts new file mode 100644 index 0000000..2a1dcac --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/io-assist.types.ts @@ -0,0 +1,62 @@ +/** + * Icon type for prompts and other UI elements. + * + * Supported types: + * - 'svg': Inline SVG string (recommended for simplicity) + * - 'url': Absolute URL to an image file (CDN, public assets, runtime paths) + * - 'data-url': Data URL with base64 encoded image (SVG or raster images) + * + * Examples: + * - svg: '...' + * - url: 'https://cdn.example.com/icons/icon.svg' or '/icons/icon.svg' + * - data-url: 'data:image/svg+xml;base64,...' or 'data:image/png;base64,...' + */ +export type IconType = "svg" | "url" | "data-url"; + +export type IconResource = { + type: IconType; + data: string; +}; + +export type IoAssistPrompt = { + name: string; + prompt: string; + /** + * Optional icon for the prompt. Use default icon if not provided. + * + * Supported types: + * - 'svg': Inline SVG string (recommended for simplicity) + * Example: '...' + * Note: SVG strings are automatically cleaned (fill="none" removed, width/height removed, + * hardcoded colors replaced with currentColor). Just paste raw SVGs! + * + * - 'url': Absolute URL to an image file + * Examples: + * - CDN: 'https://cdn.example.com/icons/market-trends.svg' + * - Public assets: '/icons/market-trends.svg' (from public/icons/ folder) + * - Runtime base path: `${window.location.origin}/icons/market-trends.svg` + * + * - 'data-url': Data URL with base64 encoded image + * Examples: + * - SVG: 'data:image/svg+xml;base64,...' + * - PNG: 'data:image/png;base64,...' + * - JPEG: 'data:image/jpeg;base64,...' + * + * For file system icons: + * 1. Place icon files in your app's public folder: + * - Angular 17+: 'public/icons/' + * - React/Vite: 'public/icons/' + * 2. Reference with absolute path starting with '/' (relative to domain root) + * Example: '/icons/market-trends.svg' + * 3. Build tools automatically serve public folder contents as static assets + * + * Note: Relative file paths (e.g., '../icons/file.svg') are NOT supported. + * Icons must be accessible via HTTP(S) after build. + */ + iconResource?: IconResource; +}; + +export type IoAssistPromptCategory = { + category?: string; + prompts: IoAssistPrompt[]; +}; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/scripts/io-assist-angular-build.js b/templates/io-assist-angular-editable/src/app/io-assist/scripts/io-assist-angular-build.js new file mode 100644 index 0000000..b49102c --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/scripts/io-assist-angular-build.js @@ -0,0 +1,41 @@ +/** + * io-assist-ng-build.js + * + * Build script for this app, invoked as the `build` npm script (no separate + * prebuild/prestart step to run). 1:1 with @interopio/io-assist-ng's own + * build script (libs/io-assist-ng/scripts/io-assist-ng-build.js in + * iointel-js), adapted so the generated files land under this app's build + * output (dist/io-assist-angular/browser) instead of being committed to src/. + * + * Steps: + * 1. ng build — compile the Angular library + * 2. tailwindcss — compile and minify styles.css → dist/styles.css + * 3. copy-fonts — copy Inter woff2 files to dist/files/ + */ + +const { execSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +const DIST_BROWSER = "dist/io-assist-angular/browser"; + +const run = (cmd) => { + console.log(`\n> ${cmd}`); + execSync(cmd, { stdio: "inherit" }); +}; + +const copyFonts = () => { + const fontPackageDir = path.dirname(require.resolve("@fontsource-variable/inter/wght.css")); + const srcFile = path.join(fontPackageDir, "files", "inter-latin-wght-normal.woff2"); + const destDir = path.resolve(__dirname, "../../../../", DIST_BROWSER, "files"); + const destFile = path.join(destDir, "inter-latin-wght-normal.woff2"); + + fs.mkdirSync(destDir, { recursive: true }); + fs.copyFileSync(srcFile, destFile); + + console.log(`Copied inter-latin-wght-normal.woff2 -> ${DIST_BROWSER}/files/`); +}; + +run("ng build"); +run(`npx tailwindcss -i src/app/io-assist/styles.css -o ${DIST_BROWSER}/styles.css --minify`); +copyFonts(); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/app-button.component.html b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/app-button.component.html new file mode 100644 index 0000000..87cb2af --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/app-button.component.html @@ -0,0 +1,16 @@ + diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/app-button.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/app-button.component.ts new file mode 100644 index 0000000..eb64b84 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/app-button.component.ts @@ -0,0 +1,23 @@ +import { CommonModule } from "@angular/common"; +import { Component, input, InputSignal, output, OutputEmitterRef } from "@angular/core"; + +import { APP_BUTTON_TYPES } from "./enum"; +import { AppButtonType } from "./types"; + +@Component({ + selector: "app-button", + imports: [CommonModule], + templateUrl: "./app-button.component.html", + /** + * Display it like this so that it doesn't create a new DOM element and break the layout of the parent component. + */ + host: { style: "display: contents" }, +}) +export class AppButtonComponent { + protected readonly APP_BUTTON_TYPES = APP_BUTTON_TYPES; + + public buttonType: InputSignal = input(APP_BUTTON_TYPES.DEFAULT); + public disabled: InputSignal = input(false); + public testId: InputSignal = input(null); + public onClick: OutputEmitterRef = output(); +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/enum.ts new file mode 100644 index 0000000..137f020 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/enum.ts @@ -0,0 +1,5 @@ +export enum APP_BUTTON_TYPES { + DEFAULT = "button-default", + SUBMIT = "button-submit", + DANGER = "button-danger", +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/types.ts new file mode 100644 index 0000000..609ba6b --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/types.ts @@ -0,0 +1,3 @@ +import { APP_BUTTON_TYPES } from "./enum"; + +export type AppButtonType = APP_BUTTON_TYPES.DEFAULT | APP_BUTTON_TYPES.SUBMIT | APP_BUTTON_TYPES.DANGER; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-copy-to-clipboard-button/app-copy-to-clipboard-button.component.html b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-copy-to-clipboard-button/app-copy-to-clipboard-button.component.html new file mode 100644 index 0000000..a343b63 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-copy-to-clipboard-button/app-copy-to-clipboard-button.component.html @@ -0,0 +1,16 @@ + + + + + {{ UI_STRINGS.COPIED }} + + diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-copy-to-clipboard-button/app-copy-to-clipboard-button.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-copy-to-clipboard-button/app-copy-to-clipboard-button.component.ts new file mode 100644 index 0000000..7fe00d9 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-copy-to-clipboard-button/app-copy-to-clipboard-button.component.ts @@ -0,0 +1,66 @@ +import { CommonModule } from "@angular/common"; +import { Component, DestroyRef, inject, input, signal, WritableSignal } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { Subject, timer } from "rxjs"; +import { switchMap } from "rxjs/operators"; + +import { UI_STRINGS } from "../../constants/ui-strings"; +import { CopyToClipboardService } from "../../services/copy-to-clipboard/copy-to-clipboard.service"; +import { AppIconComponent } from "../app-icon/app-icon.component"; +import { APP_ICON_VARIANTS } from "../app-icon/enum"; +import { AppTooltipComponent } from "../app-tooltip/app-tooltip.component"; +import { TOOLTIP_DISPLAY_MODES, TOOLTIP_POSITIONS } from "../app-tooltip/enum"; + +const COPIED_DISPLAY_DURATION_MS = 2000; + +@Component({ + selector: "app-copy-to-clipboard-button", + imports: [CommonModule, AppIconComponent, AppTooltipComponent], + templateUrl: "./app-copy-to-clipboard-button.component.html", + providers: [CopyToClipboardService], + host: { + "(mouseenter)": "onMouseEnter()", + "(mouseleave)": "onMouseLeave()", + }, +}) +export class AppCopyToClipboardButtonComponent { + public readonly textToCopy = input.required(); + // eslint-disable-next-line @angular-eslint/no-input-rename -- alias is the kebab-case DOM attribute name, not a style choice + public readonly dataTestId = input("", { alias: "data-testid" }); + + protected readonly UI_STRINGS = UI_STRINGS.GENERAL; + protected readonly APP_ICON_VARIANTS = APP_ICON_VARIANTS; + protected readonly TOOLTIP_POSITIONS = TOOLTIP_POSITIONS; + protected readonly TOOLTIP_DISPLAY_MODES = TOOLTIP_DISPLAY_MODES; + + protected readonly isHoveredSignal: WritableSignal = signal(false); + protected readonly isCopiedSignal: WritableSignal = signal(false); + + private readonly _copyToClipboardService: CopyToClipboardService = inject(CopyToClipboardService); + + // Each emission restarts the timer via switchMap, cancelling the previous one + private readonly _copy$ = new Subject(); + + constructor() { + this._copy$ + .pipe( + switchMap(() => timer(COPIED_DISPLAY_DURATION_MS)), + takeUntilDestroyed(inject(DestroyRef)) + ) + .subscribe(() => this.isCopiedSignal.set(false)); + } + + public handleCopyResponse(): void { + this._copyToClipboardService.copyToClipboard(this.textToCopy()); + this.isCopiedSignal.set(true); + this._copy$.next(); + } + + protected onMouseEnter(): void { + this.isHoveredSignal.set(true); + } + + protected onMouseLeave(): void { + this.isHoveredSignal.set(false); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/app-icon.component.html b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/app-icon.component.html new file mode 100644 index 0000000..0257de6 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/app-icon.component.html @@ -0,0 +1,29 @@ +
+ @let hasCustomInlineSvgIcon = hasCustomIcon() && isCustomIconInlineSvg() && customIconSanitizedSvg(); + @let hasCustomImageIcon = hasCustomIcon() && shouldCustomIconRenderAsImage() && customIconImageUrl(); + @let shouldShowVariantIcon = !hasCustomIcon(); + + + @if (hasCustomInlineSvgIcon) { +
+ } + + + @if (hasCustomImageIcon) { +
+ } + + + @if (shouldShowVariantIcon) { +
+ } +
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/app-icon.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/app-icon.component.ts new file mode 100644 index 0000000..7708ebd --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/app-icon.component.ts @@ -0,0 +1,470 @@ +import { CommonModule } from "@angular/common"; +import { Component, computed, ElementRef, inject, input, InputSignal, OnInit, output, OutputEmitterRef, Signal, signal, ViewChild, WritableSignal } from "@angular/core"; +import { DomSanitizer, SafeHtml } from "@angular/platform-browser"; +import { IOConnectCore } from "@interopio/core"; + +import { APP_ICON_BACKGROUND_SHAPES, APP_ICON_SIZES } from "./enum"; +import { defaultIcon, iconList } from "./icons"; +import { AppIcon, AppIconBackgroundShapeType, AppIconSizeType, AppIconVariantsType } from "./types"; +import { IconResource } from "../../../io-assist.types"; +import { ComponentEffectManagerService as EffectService } from "../../services/component-effect-manager/component-effect-manager.service"; +import { LoggerService } from "../../services/logger/logger.service"; +import { isInlineSvg, shouldRenderAsImage, stripSvgAttributes, validateIconResource } from "../../utils/icon.utils"; +/** + * AppIconComponent - A reusable icon component with dynamic styling support + * + * Basic Usage (Variant-based): + * @example + * ```html + * + * ``` + * + * Custom Icon Usage: + * @example + * ```html + * + * ...' + * }" + * > + * + * + * + * + * + * + * ``` + * + * Advanced Usage: + * @example + * ```html + * + * ``` + * + * Color Variables: + * - Provide CSS variable names with/without the `--` prefix + * - Example: `[iconFill]="'app-text-primary'"` uses `var(--app-text-primary)` + * - Colors must be defined in `_defaults.css` + * + * Available Inputs: + * @input variant - Required. Icon type from APP_ICON_VARIANTS enum + * @input size - Icon size from APP_ICON_SIZES or custom px value (default: XS) + * @input iconFill - Fill color for normal state (default: app-text-default) + * @input disabledIconFill - Fill color when disabled (default: app-text-states-disabled) + * @input hoverIconFill - Fill color on hover (default: app-text-states-hover) + * @input hoverVariant - Alternative icon variant to render on hover (optional). When provided, + * swaps the entire SVG to this variant while hovered. Ideal for paired outline/filled icons + * (e.g., PROMPT_PANEL → PROMPT_PANEL_FILLED on hover). Takes precedence over hoverIconFilled. + * @input backgroundColor - Background color (optional) + * @input hoverBackgroundColor - Background color on hover (optional) + * @input hoverBackgroundShape - Shape of hover background (default: CIRCLE) + * @input isDisabled - Disables icon and applies disabled styles (default: false) + * @input isWrapped - Determines if host element should be 2x size of icon (default: true) + * @input title - Custom tooltip text (optional, auto-generated from variant if not provided) + * @input customIcon - Custom icon resource (svg/url/data-url) to render instead of variant icon + * When provided, takes precedence over the variant icon. Supports: + * - type: 'svg' - Inline SVG string + * - type: 'url' - Absolute URL to image file (CDN, public assets) + * - type: 'data-url' - Base64 encoded data URL (SVG/PNG/JPEG) + * @input isHoveredExternal - External hover state control (optional) - use when you want to + * control hover state from parent component + * @input tabIndex - Tab index for keyboard navigation (default: 0) + * + * @output onClick - Emits when icon is clicked (not emitted when disabled) + * + * Notes: + * - Default styles applied automatically, overrides optional + * - Hover effects only active when not disabled + * - Icon inherits hover state from host element + * - Custom icons automatically validate and sanitize for security + * - Custom icons maintain consistent styling with variant icons + */ @Component({ + selector: "app-icon", + imports: [CommonModule], + templateUrl: "./app-icon.component.html", + host: { + "[class]": "hostClasses()", + "[style]": "hostStyles()", + "[style.width.px]": "isWrapped() ? sizeAsNumber() * 2 : sizeAsNumber()", + "[style.height.px]": "isWrapped() ? sizeAsNumber() * 2 : sizeAsNumber()", + "(click)": "handleClick($event)", + "(keydown)": "handleKeyDown($event)", + "(mouseenter)": "onMouseEnter()", + "(mouseleave)": "onMouseLeave()", + "[attr.title]": "getTitle()", + "[attr.tabindex]": "tabIndex()", + "[attr.data-testid]": "dataTestId() || null", + }, + providers: [ + { + provide: EffectService, + useFactory: () => new EffectService("AppIconComponent"), + }, + ], +}) +export class AppIconComponent implements OnInit { + protected readonly APP_ICON_SIZES = APP_ICON_SIZES; + + private readonly _domSanitizer: DomSanitizer = inject(DomSanitizer); + private readonly _iconList: AppIcon[] = iconList; + private readonly _defaultIcon: AppIcon = defaultIcon; + private readonly _logger: IOConnectCore.Logger.API = inject(LoggerService).get("AppIconComponent"); + + // For icon + public variant: InputSignal = input.required(); + public size: InputSignal = input(APP_ICON_SIZES.XS); + public iconFill: InputSignal = input(""); + public disabledIconFill: InputSignal = input(""); + public hoverIconFill: InputSignal = input(""); + public hoverVariant: InputSignal = input(undefined); + public isDisabled: InputSignal = input(false); + public isWrapped: InputSignal = input(true); + public title: InputSignal = input(""); + // eslint-disable-next-line @angular-eslint/no-input-rename -- alias is the kebab-case DOM attribute name, not a style choice + public dataTestId: InputSignal = input("", { alias: "data-testid" }); + + // Custom icon support (alternative to variant-based icons) + /** + * Custom icon resource to render instead of using variant. + * When provided, this takes precedence over the variant icon. + * Supports: svg (inline SVG), url (absolute URL), data-url (base64) + */ + public customIcon: InputSignal = input(undefined); + + // For host element + public backgroundColor: InputSignal = input(""); + public hoverBackgroundShape: InputSignal = input(APP_ICON_BACKGROUND_SHAPES.CIRCLE); + public hoverBackgroundColor: InputSignal = input(""); + public onClick: OutputEmitterRef = output(); + public isHoveredExternal: InputSignal = input(null); + public tabIndex: InputSignal = input(0); + + @ViewChild("iconDiv", { static: false }) iconDivRef!: ElementRef; + + // Provided in current component + private readonly _effectService: EffectService = inject(EffectService); + + public isHostHoveredSignal: WritableSignal = signal(false); + + protected isHovered: Signal = computed(() => { + // Use external hover if provided, otherwise internal signal + return this.isHoveredExternal() !== null ? !!this.isHoveredExternal() : this.isHostHoveredSignal(); + }); + + protected hasCustomIcon: Signal = computed(() => { + const icon = this.customIcon(); + if (!icon) { + return false; + } + + // For inline SVG, check if it passed sanitization + if (isInlineSvg(icon)) { + return this.customIconSanitizedSvg() !== null; + } + + // For images (URL/data-url), check if we have a valid URL + if (shouldRenderAsImage(icon)) { + return this.customIconImageUrl() !== null; + } + + return false; + }); + + protected isCustomIconInlineSvg: Signal = computed(() => { + const icon = this.customIcon(); + + return icon ? isInlineSvg(icon) : false; + }); + + protected shouldCustomIconRenderAsImage: Signal = computed(() => { + const icon = this.customIcon(); + + return icon ? shouldRenderAsImage(icon) : false; + }); + + protected customIconSanitizedSvg: Signal = computed(() => { + const icon = this.customIcon(); + if (!icon || !isInlineSvg(icon)) { + return null; + } + + const validation = validateIconResource(icon); + if (!validation.valid) { + return null; + } + + // Strip problematic attributes from SVG (removes event handlers, script tags, etc.) + const cleanedSvg = stripSvgAttributes(icon.data, this._logger); + + if (!cleanedSvg) { + this._logger.warn("Custom SVG icon was rejected due to security concerns"); + return null; + } + + // DomSanitizer.sanitize(SecurityContext.HTML) strips SVG elements entirely since + // svg/circle/path are not in its HTML allowlist. stripSvgAttributes already handles + // security (removes on* handlers, style, script tags), so we trust its output directly. + return this._domSanitizer.bypassSecurityTrustHtml(cleanedSvg); + }); + + protected customIconImageUrl: Signal = computed(() => { + const icon = this.customIcon(); + if (!icon || !shouldRenderAsImage(icon)) { + return null; + } + + return icon.data; + }); + + public getTitle: Signal = computed(() => { + if (this.title()) { + return this.title(); + } + + const match: AppIcon | undefined = this._iconList.find((icon: AppIcon) => icon.name === this.variant()); + + if (!match) { + return this._defaultIcon.title; + } + + return match.title; + }); + + protected hostClasses: Signal = computed(() => { + const classListArray: string[] = [ + "flex items-center justify-center", + this.isDisabled() ? "cursor-not-allowed" : "cursor-pointer", + this.hoverBackgroundShape() === APP_ICON_BACKGROUND_SHAPES.CIRCLE ? "rounded-full" : "", + ]; + + return classListArray.filter((c) => c !== "").join(" "); + }); + + protected hostStyles: Signal> = computed>(() => { + const styles: Record = {}; + + const bg: string = this.trimPrefix(this.backgroundColor()); + const hoverBg: string = this.trimPrefix(this.hoverBackgroundColor()); + + if (this.isHovered() && hoverBg && !this.isDisabled()) { + styles["background-color"] = `var(--${hoverBg})`; + } + + if (!this.isHovered() && bg) { + styles["background-color"] = `var(--${bg})`; + } + + return styles; + }); + + protected getIconStyles: Signal> = computed>(() => { + const styles: Record = {}; + + const dynamicFill: string = this.isDisabled() ? this.trimPrefix(this.disabledIconFill()) : this.trimPrefix(this.iconFill()); + + const staticFill: string = this.isDisabled() ? "var(--app-text-states-disabled)" : "var(--app-text-default)"; + + const fillValue: string = dynamicFill ? `var(--${dynamicFill})` : staticFill; + + // Set both 'fill' (for direct SVG attributes) and 'color' (for currentColor) + styles["fill"] = fillValue; + styles["color"] = fillValue; + + return styles; + }); + + /** + * Generates CSS mask styles for image-based icons (URL/data-URL). + * Uses CSS mask to apply the icon shape and background-color for theming. + * This allows SVG files loaded from URLs to be themed with CSS variables. + * Now supports hover colors to behave consistently with variant icons. + */ + protected getImageIconMaskStyles: Signal> = computed>(() => { + const styles: Record = {}; + const iconUrl = this.customIconImageUrl(); + + if (!iconUrl) { + return styles; + } + + let fillValue: string; + + if (this.isDisabled()) { + const dynamicFill = this.trimPrefix(this.disabledIconFill()); + fillValue = dynamicFill ? `var(--${dynamicFill})` : "var(--app-text-states-disabled)"; + } else if (this.isHovered()) { + const hoverFill = this.trimPrefix(this.hoverIconFill()); + fillValue = hoverFill ? `var(--${hoverFill})` : "var(--app-text-states-hover)"; + } else { + const dynamicFill = this.trimPrefix(this.iconFill()); + fillValue = dynamicFill ? `var(--${dynamicFill})` : "var(--app-text-default)"; + } + + // Apply the icon as a mask and use background-color for the actual color + styles["mask"] = `url('${iconUrl}') no-repeat center`; + styles["mask-size"] = "contain"; + styles["-webkit-mask"] = `url('${iconUrl}') no-repeat center`; + styles["-webkit-mask-size"] = "contain"; + styles["background-color"] = fillValue; + + return styles; + }); + + /** + * Computes the SVG markup for variant-based icons reactively. + * When `hoverVariant` is set and the icon is hovered, the entire SVG is swapped to + * the hover variant. Ideal for paired outline/filled icon variants (e.g. PROMPT_PANEL → + * PROMPT_PANEL_FILLED). + */ + protected iconSvgContent: Signal = computed(() => { + const activeVariant = this.isHovered() && this.hoverVariant() ? this.hoverVariant() : this.variant(); + + const svgString = this._iconList.find((icon: AppIcon) => icon.name === activeVariant)?.svgPath || this._defaultIcon.svgPath; + + // Replace all fill attributes with an invalid value so SVG elements inherit + // the fill color cascaded via CSS from the parent container div + let processed = svgString.replace(/fill="[^"]*"/g, 'fill="defaultColor"'); + + // Strip width/height so we can set explicit dimensions + processed = processed.replace(/width="[^"]*"/g, "").replace(/height="[^"]*"/g, ""); + + // Set explicit size matching the component size input + const size: number = this.sizeAsNumber(); + processed = processed.replace(/ { + if (this.isDisabled()) return; + + if (this.isHovered()) { + this.applyIconHoverStyles(); + + return; + } + + this.removeIconHoverStyles(); + }); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/enum.ts new file mode 100644 index 0000000..7ecb49f --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/enum.ts @@ -0,0 +1,48 @@ +export enum APP_ICON_SIZES { + XXXS = "6px", + XXS = "10px", + XS = "12px", + S = "16px", + M = "20px", + L = "24px", + XL = "32px", +} + +export enum APP_ICON_VARIANTS { + DEFAULT = "default", + THREAD_HISTORY = "thread-history", + PROMPT_PANEL = "prompt-panel", + PROMPT_PANEL_FILLED = "prompt-panel-filled", + TOOL_PANEL = "tool-panel", + TOOL_PANEL_FILLED = "tool-panel-filled", + HOME = "home", + CLOSE = "close", + LOGOUT = "logout", + PAPER_PLANE = "paper-plane", + ARROW_UP = "arrow-up", + ARROW_DOWN = "arrow-down", + RECTANGLE = "rectangle", + STAR_OUTLINE = "star-outline", + STAR_FILLED = "star-filled", + EDIT = "edit", + DELETE = "delete", + SEARCH = "search", + INFO = "info", + CHECK = "check", + RELOAD = "reload", + COPY = "copy", + COPY_HOVER = "copy-hover", + CHEVRON_DOWN = "chevron-down", + CHEVRON_UP = "chevron-up", + CHEVRON_RIGHT = "chevron-right", + CIRCLE_FILLED = "circle-filled", + CIRCLE_OUTLINE = "circle-outline", + ERROR_CLOSE = "error-close", + HAND = "hand", + HAND_EDIT = "hand-edit", +} + +export enum APP_ICON_BACKGROUND_SHAPES { + RECTANGLE = "rectangle", + CIRCLE = "circle", +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/icons.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/icons.ts new file mode 100644 index 0000000..02fe32c --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/icons.ts @@ -0,0 +1,258 @@ +import { APP_ICON_VARIANTS } from "./enum"; +import { AppIcon } from "./types"; + +/** + * When you need to add an icon, please make sure to add it here and not directly in the component. + * This will help us keep track of all the icons used in the application and make it easier to manage them. + * + * Also, make sure to update the `APP_ICON_VARIANTS` in `enums.ts` to include the new icon name. + */ +export const iconList: AppIcon[] = [ + { + title: "Close", + name: APP_ICON_VARIANTS.CLOSE, + svgPath: ` +`, + }, + { + title: "Thread History", + name: APP_ICON_VARIANTS.THREAD_HISTORY, + svgPath: ` + + +`, + }, + { + title: "Home", + name: APP_ICON_VARIANTS.HOME, + svgPath: ` + + +`, + }, + { + title: "Logout", + name: APP_ICON_VARIANTS.LOGOUT, + svgPath: ``, + }, + { + title: "Prompt Panel", + name: APP_ICON_VARIANTS.PROMPT_PANEL, + svgPath: ` + + +`, + }, + { + title: "Prompt Panel", + name: APP_ICON_VARIANTS.PROMPT_PANEL_FILLED, + svgPath: ` + + +`, + }, + { + title: "Tool Panel", + name: APP_ICON_VARIANTS.TOOL_PANEL, + svgPath: ` + + +`, + }, + { + title: "Tool Panel", + name: APP_ICON_VARIANTS.TOOL_PANEL_FILLED, + svgPath: ` + + +`, + }, + { + title: "Send Message Disabled", + name: APP_ICON_VARIANTS.PAPER_PLANE, + svgPath: ` + + +`, + }, + { + title: "Send Message", + name: APP_ICON_VARIANTS.ARROW_UP, + svgPath: ` + + +`, + }, + { + title: "Scroll to Bottom", + name: APP_ICON_VARIANTS.ARROW_DOWN, + svgPath: ` + + +`, + }, + { + title: "Stop Generation", + name: APP_ICON_VARIANTS.RECTANGLE, + svgPath: ` + + +`, + }, + { + title: "Add To Favorites", + name: APP_ICON_VARIANTS.STAR_OUTLINE, + svgPath: ` + + +`, + }, + { + title: "Remove From Favorites", + name: APP_ICON_VARIANTS.STAR_FILLED, + svgPath: ` + + +`, + }, + { + title: "Edit", + name: APP_ICON_VARIANTS.EDIT, + svgPath: ``, + }, + { + title: "Delete", + name: APP_ICON_VARIANTS.DELETE, + svgPath: ``, + }, + { + title: "Search", + name: APP_ICON_VARIANTS.SEARCH, + svgPath: ``, + }, + { + title: "Info", + name: APP_ICON_VARIANTS.INFO, + svgPath: ` + + +`, + }, + { + title: "Check", + name: APP_ICON_VARIANTS.CHECK, + svgPath: ` + + +`, + }, + { + title: "RELOAD", + name: APP_ICON_VARIANTS.RELOAD, + svgPath: ` + + + + + +`, + }, + { + title: "Copy Response", + name: APP_ICON_VARIANTS.COPY, + svgPath: ` + + + +`, + }, + { + title: "Copy Response", + name: APP_ICON_VARIANTS.COPY_HOVER, + svgPath: ` + + + +`, + }, + { + title: "Chevron Up", + name: APP_ICON_VARIANTS.CHEVRON_UP, + svgPath: ` +`, + }, + { + title: "Chevron Down", + name: APP_ICON_VARIANTS.CHEVRON_DOWN, + svgPath: ` +`, + }, + { + title: "Chevron Right", + name: APP_ICON_VARIANTS.CHEVRON_RIGHT, + svgPath: ` + + +`, + }, + { + title: "Circle Filled", + name: APP_ICON_VARIANTS.CIRCLE_FILLED, + svgPath: ` + + +`, + }, + { + title: "Circle Outline", + name: APP_ICON_VARIANTS.CIRCLE_OUTLINE, + svgPath: ` + + +`, + }, + { + title: "Error Close", + name: APP_ICON_VARIANTS.ERROR_CLOSE, + svgPath: ` + + +`, + }, + { + title: "Hand", + name: APP_ICON_VARIANTS.HAND, + svgPath: ` + + +`, + }, + { + title: "Hand Edit", + name: APP_ICON_VARIANTS.HAND_EDIT, + svgPath: ` + + +`, + }, +]; + +export const defaultIcon: AppIcon = { + title: "Icon Title", + name: APP_ICON_VARIANTS.DEFAULT, + svgPath: ` + + +`, +}; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/types.ts new file mode 100644 index 0000000..21ac899 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/types.ts @@ -0,0 +1,13 @@ +import { APP_ICON_BACKGROUND_SHAPES, APP_ICON_SIZES, APP_ICON_VARIANTS } from "./enum"; + +export type AppIcon = { + title: string; + name: string; + svgPath: string; +}; + +export type AppIconVariantsType = APP_ICON_VARIANTS[keyof APP_ICON_VARIANTS]; + +export type AppIconSizeType = APP_ICON_SIZES[keyof APP_ICON_SIZES]; + +export type AppIconBackgroundShapeType = APP_ICON_BACKGROUND_SHAPES[keyof APP_ICON_BACKGROUND_SHAPES]; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-input/app-input.component.html b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-input/app-input.component.html new file mode 100644 index 0000000..1fe9c06 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-input/app-input.component.html @@ -0,0 +1,101 @@ +@if (label()) { + {{ label() }} +} + +
+
+ @if (showLeadingIcon()) { + + } + + + + @if (showTrailingIcon() && value()) { + + } +
+
+ +@if (error()) { +
+
+ +
+

+ {{ error() }} +

+
+} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-input/app-input.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-input/app-input.component.ts new file mode 100644 index 0000000..de1eb1f --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-input/app-input.component.ts @@ -0,0 +1,83 @@ +import { CommonModule } from "@angular/common"; +import { Component, input, InputSignal, output, OutputEmitterRef, signal, WritableSignal } from "@angular/core"; +import { FormsModule } from "@angular/forms"; + +import { UI_STRINGS } from "../../constants/ui-strings"; +import { AppIconComponent } from "../app-icon/app-icon.component"; +import { APP_ICON_SIZES, APP_ICON_VARIANTS } from "../app-icon/enum"; + +enum INPUT_TYPES_ENUM { + TEXT = "text", + PASSWORD = "password", + EMAIL = "email", + NUMBER = "number", + SEARCH = "search", + TEL = "tel", + URL = "url", +} + +type AppInputType = INPUT_TYPES_ENUM[keyof INPUT_TYPES_ENUM]; + +const MODULES = [FormsModule, CommonModule]; +const COMPONENTS = [AppIconComponent]; + +/** + * A reusable input component with various customization options. + * Behaves like a standard HTML input element. + * + * Inputs: + * @type {InputSignal} placeholder - Placeholder text for the input field. + * @type {InputSignal} type - Type of the input field (e.g., text, password). + * @type {InputSignal} label - Label text for the input field. + * @type {InputSignal} isLabelSrOnly - Whether the label is only for screen readers. + * @type {InputSignal} disabled - Whether the input field is disabled. + * @type {InputSignal} error - Error message to display below the input field. + * @type {InputSignal} showLeadingIcon - Whether to show a leading icon. + * @type {InputSignal} leadingIconVariant - Variant of the leading icon. + * @type {InputSignal} showTrailingIcon - Whether to show a trailing icon. + * @type {InputSignal} trailingIconVariant - Variant of the trailing icon. + * @type {InputSignal} iconsSize - Size of the icons. + * + * Outputs: + * @type {OutputEmitterRef} onValueChange - Emits the current value of the input field on change. + */ +@Component({ + selector: "app-input", + imports: [...MODULES, ...COMPONENTS], + templateUrl: "./app-input.component.html", +}) +export class AppInputComponent { + protected readonly APP_INPUT_COMPONENT_STRINGS = UI_STRINGS.INPUT_COMPONENT; + protected readonly APP_ICON_VARIANTS = APP_ICON_VARIANTS; + protected readonly APP_ICON_SIZES = APP_ICON_SIZES; + + public readonly placeholder: InputSignal = input(this.APP_INPUT_COMPONENT_STRINGS.DEFAULT_PLACEHOLDER); + public readonly type: InputSignal = input(INPUT_TYPES_ENUM.TEXT); + public readonly label: InputSignal = input(""); + public readonly isLabelSrOnly: InputSignal = input(false); + public readonly disabled: InputSignal = input(false); + public readonly error: InputSignal = input(""); + public readonly showLeadingIcon: InputSignal = input(false); + public readonly leadingIconVariant: InputSignal = input(APP_ICON_VARIANTS.SEARCH); + public readonly showTrailingIcon: InputSignal = input(false); + public readonly trailingIconVariant: InputSignal = input(APP_ICON_VARIANTS.CLOSE); + public readonly iconsSize: InputSignal = input(APP_ICON_SIZES.XS); + + public onValueChange: OutputEmitterRef = output(); + + protected isActive: WritableSignal = signal(false); + protected value: WritableSignal = signal(""); + + protected onInput(event: Event): void { + const target: HTMLInputElement = event.target as HTMLInputElement; + + this.value.set(target.value); + this.onValueChange.emit(target.value); + } + + protected clearContent(): void { + this.value.set(""); + + this.onValueChange.emit(""); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/app-md-formatter.component.css b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/app-md-formatter.component.css new file mode 100644 index 0000000..18777c3 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/app-md-formatter.component.css @@ -0,0 +1,505 @@ +/* Important: This css styles file uses ng-deep to override styles in child components and provide custom theme for our use case */ + +/* General */ +:host { + width: 100%; + max-width: 100%; + min-width: 0; + box-sizing: border-box; + display: flex; + flex-direction: column; + padding: 0px 2px; +} + +/* + * These wildcard rules live in @layer base so that Tailwind utility classes + * used by child components always win (base < components < utilities). + * Elements without a competing utility still receive the base value. + */ +@layer base { + :host ::ng-deep remark-node * { + padding: 8px 0; + } + + /* Code Block — reset the remark-node * padding for all code-block descendants */ + :host ::ng-deep .io-assist-code-block * { + padding: 0; + } +} + +:host ::ng-deep remark-node h1, +:host ::ng-deep remark-node h2, +:host ::ng-deep remark-node h3, +:host ::ng-deep remark-node h4, +:host ::ng-deep remark-node h5, +:host ::ng-deep remark-node h6 { + font-family: Inter, sans-serif; + font-weight: 500; + color: var(--color-text-states-active); + margin-top: 20px; + padding: 0; + line-height: normal; +} + +:host ::ng-deep remark-node h1 { font-size: 20px; line-height: 28px; } +:host ::ng-deep remark-node h2 { font-size: 18px; line-height: 26px; } +:host ::ng-deep remark-node h3 { font-size: 16px; line-height: 24px; } +:host ::ng-deep remark-node h4 { font-size: 15px; line-height: 22px; } +:host ::ng-deep remark-node h5 { font-size: 14px; line-height: 20px; } +:host ::ng-deep remark-node h6 { font-size: 13px; line-height: 18px; } + +/* Body text — base span */ +:host ::ng-deep remark-node span:not(h1 span, h2 span, h3 span, h4 span, h5 span, h6 span, a span, .io-assist-code-block__copy *) { + font-family: Inter, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + color: var(--color-text-default); +} + +/* Code Block — outer wrapper */ +:host ::ng-deep .io-assist-code-block { + position: relative; + width: 100%; + background: var(--color-app-background-secondary, #36383b); + border-radius: 16px; + padding: 16px; + box-sizing: border-box; + display: flex; + flex-direction: column; + margin: 8px 0; +} + +/* Code Block — copy button: absolute top-right */ +:host ::ng-deep .io-assist-code-block__copy { + position: absolute; + top: 15px; + right: 20px; + z-index: 1; +} + +/* Code Block — prism content */ +:host ::ng-deep .io-assist-code-theme { + display: block; + width: 100%; + padding-top: 16px; +} + +:host ::ng-deep .io-assist-code-theme pre { + background: transparent; + padding: 0; + margin: 0; + border: none; + border-radius: 0; + box-shadow: none; + text-shadow: none; + outline: none; +} + +:host ::ng-deep .io-assist-code-theme code { + display: block; + font-family: 'JetBrains Mono', monospace; + font-size: 13px; + font-weight: 400; + line-height: 18px; + white-space: pre-wrap; + word-break: break-word; + max-width: 100%; + box-sizing: border-box; + padding: 0; + text-shadow: none; +} + +/* Prism syntax token colors — matches the Figma design */ +:host ::ng-deep .io-assist-code-theme .token { + font-family: inherit; + font-size: inherit; + line-height: inherit; + padding: 0; + background: transparent; + text-shadow: none; + color: var(--color-text-default, #caccce); /* default token color */ +} + +/* Keywords: import, export, from, const, let, var, function, class, interface, return, if, else, etc. */ +:host ::ng-deep .io-assist-code-theme .token.keyword, +:host ::ng-deep .io-assist-code-theme .token.module, +:host ::ng-deep .io-assist-code-theme .token.important, +:host ::ng-deep .io-assist-code-theme .token.atrule { + color: #fb4bba; +} + +/* Identifiers: function names, class names, variables used as values */ +:host ::ng-deep .io-assist-code-theme .token.function, +:host ::ng-deep .io-assist-code-theme .token.class-name, +:host ::ng-deep .io-assist-code-theme .token.variable, +:host ::ng-deep .io-assist-code-theme .token.attr-name, +:host ::ng-deep .io-assist-code-theme .token.property { + color: #4b92fb; +} + +/* Strings and types */ +:host ::ng-deep .io-assist-code-theme .token.string, +:host ::ng-deep .io-assist-code-theme .token.char, +:host ::ng-deep .io-assist-code-theme .token.builtin, +:host ::ng-deep .io-assist-code-theme .token.url, +:host ::ng-deep .io-assist-code-theme .token.regex, +:host ::ng-deep .io-assist-code-theme .token.attr-value { + color: #e39292; +} + +/* Numbers, booleans */ +:host ::ng-deep .io-assist-code-theme .token.number, +:host ::ng-deep .io-assist-code-theme .token.boolean { + color: #4b92fb; +} + +/* Comments */ +:host ::ng-deep .io-assist-code-theme .token.comment, +:host ::ng-deep .io-assist-code-theme .token.prolog, +:host ::ng-deep .io-assist-code-theme .token.doctype, +:host ::ng-deep .io-assist-code-theme .token.cdata { + color: #6a6d72; + font-style: italic; +} + +/* Operators and punctuation — default gray */ +:host ::ng-deep .io-assist-code-theme .token.operator, +:host ::ng-deep .io-assist-code-theme .token.punctuation, +:host ::ng-deep .io-assist-code-theme .token.tag, +:host ::ng-deep .io-assist-code-theme .token.entity { + color: var(--color-text-default, #caccce); +} + +/* Ensure remark and remark-node form proper block formatting contexts */ +:host ::ng-deep remark { + display: block; + min-width: 0; + max-width: 100%; +} + +:host ::ng-deep remark-node { + display: block; + min-width: 0; + max-width: 100%; +} + +/* Table Theming */ +:host ::ng-deep remark-node table { + margin: 8px 0; + width: 100%; + max-width: 100%; + border-collapse: collapse; + display: block; + overflow-x: auto; +} + +:host ::ng-deep remark-node table thead { + text-align: left; +} + +:host ::ng-deep remark-node table thead th { + padding: 12px 12px 12px 0; + font-family: Inter, sans-serif; + font-size: 14px; + font-weight: 500; + line-height: 20px; + color: var(--color-text-states-active); + border-bottom: 2px solid var(--color-panel-border); +} + +:host ::ng-deep remark-node table thead th:last-child { + padding-right: 0; +} + +:host ::ng-deep remark-node table tbody tr { + border-top: 1px solid var(--color-panel-border); +} + +:host ::ng-deep remark-node td { + padding: 12px 12px 12px 0; + font-family: Inter, sans-serif; + font-size: 14px; + font-weight: 400; + line-height: 20px; + color: var(--color-text-default); +} + +:host ::ng-deep remark-node td:last-child { + padding-right: 0; +} + +/* Lists — shared base */ +:host ::ng-deep remark-node ul, +:host ::ng-deep remark-node ol { + margin: 16px 0 8px 0; + padding: 0; + font-family: Inter, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + color: var(--color-text-default); + list-style: none; +} + +/* Cancel the global remark-node * { padding: 8px 0 } for p/span inside li + — remark wraps li text in

when the li has block children, which would + push the text down and misalign it with the absolute bullet/number. */ +:host ::ng-deep remark-node li > p, +:host ::ng-deep remark-node li > span { + padding: 0; + margin: 0; +} + +/* Nested lists: reset margin */ +:host ::ng-deep remark-node li > ul, +:host ::ng-deep remark-node li > ol { + margin: 0; +} + +/* ── Unordered lists ─────────────────────────────── */ + +:host ::ng-deep remark-node ul { + padding-left: 8px; +} + +/* Nested UL indents */ +:host ::ng-deep remark-node li > ul { + padding-left: 16px; +} + +:host ::ng-deep remark-node li > ul > li > ul { + padding-left: 24px; +} + +/* All UL direct-child items */ +:host ::ng-deep remark-node ul > li { + position: relative; + padding: 6px 0 6px 14px; +} + +/* Bullet: vertically centered with the first text line. + li padding-top = 6px, line-height = 20px → text center = 6 + 10 = 16px + Bullet is 6px tall → top = 16 - 3 = 13px */ +:host ::ng-deep remark-node ul > li::before { + content: ''; + position: absolute; + left: 1px; + top: 13px; + width: 6px; + height: 6px; + border-radius: 50%; + background-color: var(--color-text-default); +} + +/* L3 bullet: hollow circle */ +:host ::ng-deep remark-node ul > li > ul > li > ul > li::before { + background-color: transparent; + border: 1.5px solid var(--color-text-default); +} + +/* ── Ordered lists ───────────────────────────────── */ + +:host ::ng-deep remark-node ol { + counter-reset: item; +} + +/* Nested OL indents */ +:host ::ng-deep remark-node li > ol { + padding-left: 24px; +} + +/* Only direct OL children are counted */ +:host ::ng-deep remark-node ol > li { + counter-increment: item; + position: relative; + padding: 6px 0 6px 20px; +} + +/* L2 items need more room for "1.1." */ +:host ::ng-deep remark-node li > ol > li { + padding-left: 32px; +} + +/* L3 items need more room for "1.1.1." */ +:host ::ng-deep remark-node li > ol > li > ol > li { + padding-left: 44px; +} + +/* Number: top = li padding-top = 6px, aligns with start of text line */ +:host ::ng-deep remark-node ol > li::before { + content: counters(item, '.') '.'; + position: absolute; + left: 0; + top: 6px; + font-family: Inter, sans-serif; + font-size: 14px; + font-weight: 600; + line-height: 20px; + color: var(--color-text-states-active); +} + +/* Blockquote */ +:host ::ng-deep remark-node blockquote { + background: #2e3033; + border-radius: 4px; + padding: 16px 24px; + margin: 8px 0; + display: flex; + align-items: center; + position: relative; +} + +:host ::ng-deep remark-node blockquote::before { + content: ''; + position: absolute; + left: 0; + top: 0; + width: 6px; + height: 100%; + border-radius: 4px 0 0 4px; + background: linear-gradient( + 180deg, + var(--color-app-accent-color-1), + var(--app-blue, #4b92fb) + ); +} + +:host ::ng-deep remark-node blockquote p, +:host ::ng-deep remark-node blockquote span { + font-family: Inter, sans-serif; + font-size: 14px; + font-weight: 400; + line-height: 20px; + color: var(--color-text-default, #caccce); + padding: 0; + margin: 0; +} + +/* hr */ +:host ::ng-deep remark-node hr { + border: none; + z-index: 1; + position: relative; + margin-top: 16px; + padding-bottom: 8px; +} + +:host ::ng-deep remark-node hr::before { + content: ''; + background: var(--color-panel-border, #3a3c40); + position: absolute; + z-index: -1; + top: 0px; + left: 0px; + width: 100%; + height: 1px; +} + +/* Paragraph — body text */ +:host ::ng-deep remark-node p { + font-family: Inter, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + color: var(--color-text-default); + padding: 0; + margin: 0 0 8px 0; +} + +:host ::ng-deep remark-node p:last-child { + margin-bottom: 0; +} + +/* Body small — tag: Inter Regular 12px/16px */ +:host ::ng-deep remark-node small { + font-family: Inter, sans-serif; + font-size: 12px; + font-weight: 400; + line-height: 16px; + color: var(--color-text-default); + display: inline; + padding: 0; +} + +/* Inline link: Inter Semi Bold 600, 14px/20px, blue, underlined */ +:host ::ng-deep remark-node a { + font-family: Inter, sans-serif; + font-size: 14px; + font-weight: 600; + line-height: 20px; + color: var(--app-blue, #4b92fb); + text-decoration: underline; + text-decoration-skip-ink: none; + padding: 0; +} + +/* Ensure spans inside links inherit link color, not the body span rule */ +:host ::ng-deep remark-node a span { + color: inherit; + font-size: inherit; + font-weight: inherit; + line-height: inherit; + padding: 0; +} + +:host ::ng-deep remark-node a:hover { + opacity: 0.85; +} + +/* Strong / bold: Inter Bold 700, 14px/20px — color stays as body text */ +:host ::ng-deep remark-node strong { + font-family: Inter, sans-serif; + font-size: 14px; + font-weight: 700; + font-style: normal; + line-height: 20px; + padding: 0; +} + +:host ::ng-deep remark-node strong span { + font-weight: inherit; + padding: 0; +} + +/* Em / italic: Inter Italic 400, 14px/20px */ +:host ::ng-deep remark-node em { + font-family: Inter, sans-serif; + font-size: 14px; + font-style: italic; + font-weight: 400; + line-height: 20px; + padding: 0; +} + +/* Strikethrough: line-through, decoration-skip-ink: none */ +:host ::ng-deep remark-node del, +:host ::ng-deep remark-node s { + font-family: Inter, sans-serif; + font-size: 14px; + font-weight: 400; + font-style: normal; + line-height: 20px; + color: var(--color-text-default); + text-decoration: line-through; + text-decoration-style: solid; + text-decoration-skip-ink: none; + padding: 0; +} + +/* Inline code: JetBrains Mono 13px/18px, blue, dark bg pill */ +:host ::ng-deep remark-node code:not(pre code) { + display: inline; + font-family: 'JetBrains Mono', monospace; + font-size: 13px; + font-weight: 400; + line-height: 18px; + color: var(--app-blue, #4b92fb); + background: var(--color-app-background-secondary); + border-radius: 2px; + padding: 0 2px; + vertical-align: baseline; +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/app-md-formatter.component.html b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/app-md-formatter.component.html new file mode 100644 index 0000000..5df23f0 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/app-md-formatter.component.html @@ -0,0 +1,15 @@ + + +

+ + +
+ + + + + + + + + diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/app-md-formatter.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/app-md-formatter.component.ts new file mode 100644 index 0000000..144a5c3 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/app-md-formatter.component.ts @@ -0,0 +1,29 @@ +import { Component, input, InputSignal } from "@angular/core"; +import { HeadingComponent, PrismComponent, RemarkModule } from "ngx-remark"; +import remarkGfm from "remark-gfm"; +import remarkParse from "remark-parse"; +import { unified } from "unified"; + +import { AppCopyToClipboardButtonComponent } from "../app-copy-to-clipboard-button/app-copy-to-clipboard-button.component"; + +// Prism core + languages +import "prismjs"; +import "prismjs/components/prism-markup"; +import "prismjs/components/prism-json"; +import "prismjs/components/prism-yaml"; +import "prismjs/components/prism-markdown"; + +const MODULES = [RemarkModule]; +const COMPONENTS = [PrismComponent, HeadingComponent, AppCopyToClipboardButtonComponent]; + +@Component({ + selector: "app-md-formatter", + templateUrl: "./app-md-formatter.component.html", + styleUrls: ["./app-md-formatter.component.css"], + imports: [...MODULES, ...COMPONENTS], +}) +export class AppMdFormatterComponent { + protected readonly processor = unified().use(remarkGfm).use(remarkParse); + + public readonly content: InputSignal = input.required(); +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/utils.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/utils.ts new file mode 100644 index 0000000..0270e53 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/utils.ts @@ -0,0 +1,3 @@ +export const textToJSONMarkdown = (content: unknown): string => { + return "```json\n" + JSON.stringify(content, null, 2) + "\n```"; +}; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/app-panel.component.html b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/app-panel.component.html new file mode 100644 index 0000000..f707c35 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/app-panel.component.html @@ -0,0 +1,102 @@ +
+ +
+ @if (title()) { + {{ title() }} + } + + @if (isHeaderCloseButtonDisplayed()) { + + } +
+ + @if (description()) { +
+

+ {{ description() }} +

+
+ } + + +
+
+ @if (htmlContent()) { +
+ } + +
+ +
+ +
+
+
+ + +
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/app-panel.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/app-panel.component.ts new file mode 100644 index 0000000..751de40 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/app-panel.component.ts @@ -0,0 +1,70 @@ +import { CommonModule } from "@angular/common"; +import { Component, ComponentRef, input, InputSignal, output, OutputEmitterRef, Type, ViewChild, ViewContainerRef } from "@angular/core"; + +import { PANEL_BUTTON_ACTION_TYPE } from "./enums"; +import { AppPanelActionType, AppPanelFooterButton } from "./types"; +import { UI_STRINGS } from "../../constants/ui-strings"; +import { AppButtonComponent } from "../app-button/app-button.component"; +import { AppIconComponent } from "../app-icon/app-icon.component"; +import { APP_ICON_SIZES, APP_ICON_VARIANTS } from "../app-icon/enum"; + +const MODULES = [CommonModule]; +const COMPONENTS = [AppButtonComponent, AppIconComponent]; + +/** + * This is a reusable app panel component. It can displayed in two ways: + * 1. As a standalone component in a template with ngIf to control its visibility + * 2. As an overlay using the OverlayService to manage its display and state + */ +@Component({ + selector: "app-panel", + templateUrl: "./app-panel.component.html", + imports: [...MODULES, ...COMPONENTS], +}) +export class AppPanelComponent { + protected readonly UI_STRINGS = UI_STRINGS.PANEL_COMPONENT; + protected readonly APP_ICON_VARIANTS = APP_ICON_VARIANTS; + protected readonly APP_ICON_SIZES = APP_ICON_SIZES; + + @ViewChild("panelBodyContent", { read: ViewContainerRef, static: false }) + public panelBodyContent!: ViewContainerRef; + + public isHeaderCloseButtonDisplayed: InputSignal = input(false); + public title: InputSignal = input(this.UI_STRINGS.DEFAULT_TITLE); + public description: InputSignal = input(undefined); + public htmlContent: InputSignal = input(undefined); + public footerButtons: InputSignal = input([]); + + public onClose: OutputEmitterRef = output(); + public onSubmit: OutputEmitterRef = output(); + + protected handleButtonClick(button: AppPanelFooterButton): void { + switch (button.action) { + case PANEL_BUTTON_ACTION_TYPE.CLOSE: + this.onClose.emit(); + break; + case PANEL_BUTTON_ACTION_TYPE.SUBMIT: + this.onSubmit.emit(PANEL_BUTTON_ACTION_TYPE.SUBMIT); + break; + case PANEL_BUTTON_ACTION_TYPE.CUSTOM: + if (!button.onClick) { + throw new Error("Custom action button must have an onClick handler defined"); + } + + button.onClick(); + + break; + default: + break; + } + } + + public injectBodyContent(component: Type): ComponentRef { + const container: ViewContainerRef | undefined = this.panelBodyContent; + + if (!container) throw new Error("Panel body content container is not defined"); + + container.clear(); + return container.createComponent(component); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/enums.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/enums.ts new file mode 100644 index 0000000..3c79307 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/enums.ts @@ -0,0 +1,5 @@ +export enum PANEL_BUTTON_ACTION_TYPE { + CLOSE = "CLOSE", + SUBMIT = "SUBMIT", + CUSTOM = "CUSTOM", +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/types.ts new file mode 100644 index 0000000..b497e0e --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/types.ts @@ -0,0 +1,33 @@ +import { Type, WritableSignal } from "@angular/core"; + +import { PANEL_BUTTON_ACTION_TYPE } from "./enums"; +import { AppButtonType } from "../app-button/types"; + +/** + * Configuration options for displaying the AppPanelComponent as an overlay. + * All fields are optional to allow for flexible panel configurations. + * @title The title text displayed at the top of the panel. + * @description Optional description text displayed below the title. + * @content The component type to be injected into the panel body OR an HTML string to be rendered. + * @footerButtons An array of button configurations for the panel footer. + * @isHeaderCloseButtonDisplayed A boolean indicating whether to show a close button in the header. + * @isDisplayedSignal An optional WritableSignal that the service will set to `true` when the panel is opened and `false` when it is closed (via the header close button or a backdrop click). The caller is responsible for creating the signal; the service only calls `.set()` on it. + */ +export type AppPanelOverlayConfig = { + title?: string; + description?: string; + content?: Type | string; + footerButtons?: AppPanelFooterButton[]; + isHeaderCloseButtonDisplayed?: boolean; + isDisplayedSignal?: WritableSignal; +}; + +export type AppPanelActionType = PANEL_BUTTON_ACTION_TYPE.CLOSE | PANEL_BUTTON_ACTION_TYPE.SUBMIT | PANEL_BUTTON_ACTION_TYPE.CUSTOM; + +export type AppPanelFooterButton = { + label: string; + action: AppPanelActionType; + type: AppButtonType; + onClick?: () => void; + testId?: string; +}; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.component.css b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.component.css new file mode 100644 index 0000000..a1352f0 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.component.css @@ -0,0 +1,26 @@ +@keyframes select-in { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.app-select__dropdown { + animation: select-in 120ms ease-in; +} + +@media (prefers-reduced-motion: reduce) { + .app-select__dropdown { + animation: none; + } +} + +/* Keyboard-focus ring — inset box-shadow avoids outline/native-ring conflicts entirely */ +button:focus-visible { + outline: none !important; + box-shadow: inset 0 0 0 2px var(--app-border-hover); +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.component.html b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.component.html new file mode 100644 index 0000000..02e160a --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.component.html @@ -0,0 +1,91 @@ +
+ @if (label()) { + {{ label() }} + } + + + + @if (isOpen()) { +
+ @for (option of options(); track option.value ?? option.title; let i = $index) { +
+
+ + @if (hasAnyIcon()) { +
+ @if (option.icon) { + + } +
+ } + + +
+ {{ option.title }} + + @if (option.description) { + {{ option.description }} + } +
+ + +
+ @if (isSelected(option)) { + + } +
+
+
+ } +
+ } +
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.component.ts new file mode 100644 index 0000000..6e9b2b0 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.component.ts @@ -0,0 +1,227 @@ +import { CommonModule } from "@angular/common"; +import { Component, computed, DestroyRef, ElementRef, HostListener, inject, input, InputSignal, OnInit, output, OutputEmitterRef, Signal, signal, viewChild, WritableSignal } from "@angular/core"; + +import { AppSelectService } from "./app-select.service"; +import { AppSelectOption } from "./types"; +import { ComponentEffectManagerService as EffectService } from "../../services/component-effect-manager/component-effect-manager.service"; +import { AppIconComponent } from "../app-icon/app-icon.component"; +import { APP_ICON_SIZES, APP_ICON_VARIANTS } from "../app-icon/enum"; + +const MODULES = [CommonModule]; +const COMPONENTS = [AppIconComponent]; + +const PLACEHOLDER = "Select..."; + +/** + * AppSelectComponent — a styled select trigger + dropdown. + * + * Usage: + * ```html + * + * ``` + * + * @input selectId - Unique ID for this select instance. Required when persist is true. + * @input options - Array of selectable options (required). Mark one with `isSelected: true` to pre-select it. + * @input persist - Persist the selected option via the preferences API (default: true). Requires selectId. + * @input label - Optional label rendered above the trigger + * @input outline - Show border outline on trigger; when false (default) uses solid background design + * @input invertOrder - Place check icon left and item icon right (default: false) + * @input preferredPlacement - Preferred dropdown direction: 'above' | 'below' (default: 'below'). Flips if not enough space. + * @output optionSelected - Emits the chosen option on selection + */ +@Component({ + selector: "app-select", + templateUrl: "./app-select.component.html", + styleUrl: "./app-select.component.css", + imports: [...MODULES, ...COMPONENTS], + providers: [ + { + provide: EffectService, + useFactory: () => new EffectService("AppSelectComponent"), + }, + ], +}) +export class AppSelectComponent implements OnInit { + protected readonly APP_ICON_VARIANTS = APP_ICON_VARIANTS; + protected readonly APP_ICON_SIZES = APP_ICON_SIZES; + + private readonly _el = inject(ElementRef); + private readonly _effectService = inject(EffectService); + private readonly _selectService = inject(AppSelectService); + private readonly _destroyRef = inject(DestroyRef); + + public readonly selectId: InputSignal = input.required(); + public readonly options: InputSignal = input.required(); + public readonly label: InputSignal = input(undefined); + public readonly persist: InputSignal = input(true); + public readonly outline: InputSignal = input(false); + public readonly invertOrder: InputSignal = input(false); + public readonly preferredPlacement: InputSignal<"above" | "below"> = input<"above" | "below">("below"); + + public readonly optionSelected: OutputEmitterRef = output(); + + protected readonly isOpen: WritableSignal = signal(false); + protected readonly selectedOption: WritableSignal = signal(null); + protected readonly dropdownStyle: WritableSignal> = signal({}); + protected readonly hoveredOptionTitle: WritableSignal = signal(null); + protected readonly focusedIndex: WritableSignal = signal(-1); + protected readonly isTriggerHovered: WritableSignal = signal(false); + protected readonly isTriggerFocused: WritableSignal = signal(false); + private _lastInputWasPointer = false; + + protected onTriggerPointerDown(): void { + this._lastInputWasPointer = true; + } + + protected onTriggerFocus(): void { + if (!this._lastInputWasPointer) this.isTriggerFocused.set(true); + this._lastInputWasPointer = false; + } + + protected onTriggerBlur(): void { + this.isTriggerFocused.set(false); + } + + private readonly _triggerRef: Signal = viewChild("triggerEl"); + private readonly _dropdownRef: Signal = viewChild("dropdownEl"); + + protected readonly displayTitle: Signal = computed(() => { + const selected = this.selectedOption(); + if (!selected) return PLACEHOLDER; + return selected.miniTitle ?? selected.title; + }); + + protected readonly hasAnyIcon: Signal = computed(() => this.options().some((o) => o.icon !== undefined)); + + protected readonly chevronVariant: Signal = computed(() => (this.isOpen() ? APP_ICON_VARIANTS.CHEVRON_UP : APP_ICON_VARIANTS.CHEVRON_DOWN)); + + public ngOnInit(): void { + const id = this.selectId(); + + this._selectService.register(id, this.options(), this.persist()).then(() => { + const stored = this._selectService.getSelected(id)(); + if (stored) this.selectedOption.set(stored); + }); + + this._destroyRef.onDestroy(() => this._selectService.unregister(id)); + + this._effectService.registerEffect("AppSelectComponent.initDefaultOption", () => { + const def = this.options().find((o) => o.isSelected); + if (def && !this.selectedOption()) this.selectedOption.set(def); + }); + } + + protected toggle(): void { + this.isOpen.update((v) => !v); + if (!this.isOpen()) return; + + this.focusedIndex.set(-1); + requestAnimationFrame(() => this.updateDropdownPosition()); + } + + protected select(option: AppSelectOption): void { + this.selectedOption.set(option); + this.optionSelected.emit(option); + this.isOpen.set(false); + this._selectService.select(this.selectId(), option, this.persist()); + } + + protected isSelected(option: AppSelectOption): boolean { + const selected = this.selectedOption(); + if (!selected) return false; + if (option.value !== undefined && selected.value !== undefined) { + return String(option.value) === String(selected.value); + } + return selected.title === option.title; + } + + private updateDropdownPosition(): void { + const triggerEl = this._triggerRef()?.nativeElement; + const dropdownEl = this._dropdownRef()?.nativeElement; + if (!triggerEl || !dropdownEl) return; + + const triggerRect: DOMRect = triggerEl.getBoundingClientRect(); + const dropdownHeight: number = dropdownEl.offsetHeight; + const dropdownWidth: number = dropdownEl.offsetWidth; + const spaceBelow = window.innerHeight - triggerRect.bottom - 8; + const spaceAbove = triggerRect.top - 8; + + const preferAbove = this.preferredPlacement() === "above"; + const openUpward = preferAbove ? spaceAbove >= dropdownHeight || spaceAbove > spaceBelow : spaceBelow < dropdownHeight && spaceAbove > spaceBelow; + + const left = Math.max(8, Math.min(triggerRect.left, window.innerWidth - dropdownWidth - 8)); + + this.dropdownStyle.set({ + left: `${left}px`, + minWidth: `${triggerRect.width}px`, + ...(openUpward ? { bottom: `${window.innerHeight - triggerRect.top + 4}px`, top: "auto" } : { top: `${triggerRect.bottom + 4}px`, bottom: "auto" }), + }); + } + + private _scrollFocusedIntoView(): void { + const dropdownEl = this._dropdownRef()?.nativeElement; + if (!dropdownEl) return; + const items = dropdownEl.querySelectorAll('[role="option"]') as NodeListOf; + items[this.focusedIndex()]?.scrollIntoView({ block: "nearest" }); + } + + @HostListener("window:resize") + onWindowResize(): void { + if (this.isOpen()) requestAnimationFrame(() => this.updateDropdownPosition()); + } + + @HostListener("window:scroll") + onWindowScroll(): void { + if (this.isOpen()) requestAnimationFrame(() => this.updateDropdownPosition()); + } + + @HostListener("document:click", ["$event"]) + onDocumentClick(event: MouseEvent): void { + if (!this._el.nativeElement.contains(event.target)) this.isOpen.set(false); + } + + @HostListener("document:keydown", ["$event"]) + onKeydown(event: KeyboardEvent): void { + if (!this.isOpen()) return; + + const opts = this.options(); + const last = opts.length - 1; + + switch (event.key) { + case "ArrowDown": + event.preventDefault(); + this.focusedIndex.update((i) => Math.min(i + 1, last)); + this._scrollFocusedIntoView(); + break; + case "ArrowUp": + event.preventDefault(); + this.focusedIndex.update((i) => Math.max(i - 1, 0)); + this._scrollFocusedIntoView(); + break; + case "Home": + event.preventDefault(); + this.focusedIndex.set(0); + this._scrollFocusedIntoView(); + break; + case "End": + event.preventDefault(); + this.focusedIndex.set(last); + this._scrollFocusedIntoView(); + break; + case "Enter": { + event.preventDefault(); + const idx = this.focusedIndex(); + if (idx >= 0 && opts[idx]) this.select(opts[idx]); + break; + } + case "Escape": + this.isOpen.set(false); + break; + } + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.service.ts new file mode 100644 index 0000000..e3a2499 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.service.ts @@ -0,0 +1,116 @@ +import { inject, Injectable, Signal, signal, WritableSignal } from "@angular/core"; + +import { AppSelectOption } from "./types"; +import { IOConnectService } from "../../services/io/io.service"; +import { LoggerService } from "../../services/logger/logger.service"; + +const PREFS_KEY = "userChoices"; + +/** Registry of all app-select IDs in the application. + * Each ID must be unique and stable — it is used as the key in persisted user choices. */ +export const APP_SELECT_IDS = { + PERMISSION: "permission", +} as const; + +export type AppSelectId = (typeof APP_SELECT_IDS)[keyof typeof APP_SELECT_IDS]; + +type SelectEntry = { + signal: WritableSignal; + options: AppSelectOption[]; +}; + +/** + * Manages all `` instances across the app. + * + * ## Usage + * 1. Give the select a unique ID from `APP_SELECT_IDS`. + * 2. The component calls `register()` on init and `unregister()` on destroy. + * 3. When the user picks an option, the component calls `select()`. + * 4. Consumers read the current value via `getSelected(id)()`. + * + * ```ts + * // Reading the selected value in a service: + * const mode = this._appSelectService.getSelected(APP_SELECT_IDS.PERMISSION)()?.value; + * ``` + * + * ## Persistence + * Selections are saved in user prefs under the `userChoices` key as a flat + * `Record`. Example with two registered selects: + * + * ```json + * { + * "permission": "auto_accept", + * "language": "en" + * } + * ``` + * + * On `register()` the stored value is matched against `String(o.value)` for each option + * and used to pre-select the matching option, overriding the `isSelected` default if one is found. + */ +@Injectable({ providedIn: "root" }) +export class AppSelectService { + private readonly _ioConnectService: IOConnectService = inject(IOConnectService); + private readonly _logger: LoggerService = inject(LoggerService); + private readonly LOGGER_NAME = "AppSelectService"; + + private readonly _registry = new Map(); + + public async register(id: string, options: AppSelectOption[], persist: boolean): Promise { + if (this._registry.has(id)) { + this._logger.get(this.LOGGER_NAME).warn(`Select "${id}" is already registered.`); + return; + } + + const defaultOption = options.find((o) => o.isSelected) ?? null; + const entry: SelectEntry = { signal: signal(defaultOption), options }; + this._registry.set(id, entry); + + if (!persist) return; + + const choices = await this._fetchChoices(); + const savedValue = choices ? choices[id] : undefined; + const savedOption = options.find((o) => String(o.value) === savedValue); + + if (!savedOption) return; + + entry.signal.set(savedOption); + } + + public unregister(id: string): void { + this._registry.delete(id); + } + + public async select(id: string, option: AppSelectOption, persist: boolean): Promise { + const entry = this._registry.get(id); + + if (!entry) return; + + entry.signal.set(option); + + if (!persist) return; + + await this._saveChoice(id, String(option.value)); + } + + public getSelected(id: string): Signal { + return this._registry.get(id)?.signal ?? signal(null); + } + + private async _saveChoice(id: string, value: string): Promise { + try { + const current = await this._fetchChoices(); + + await this._ioConnectService.updatePrefs(PREFS_KEY, { ...current, [id]: value }); + } catch { + this._logger.get(this.LOGGER_NAME).warn(`Could not save prefs for select "${id}".`); + } + } + + private async _fetchChoices(): Promise | undefined> { + try { + return await this._ioConnectService.fetchPrefs>(PREFS_KEY); + } catch { + return undefined; + } + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/types.ts new file mode 100644 index 0000000..f40c426 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/types.ts @@ -0,0 +1,10 @@ +import { AppIconVariantsType } from "../app-icon/types"; + +export type AppSelectOption = { + title: string; + miniTitle?: string; + description?: string; + icon?: AppIconVariantsType; + isSelected?: boolean; + value?: TValue; +}; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/app-spinner.component.html b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/app-spinner.component.html new file mode 100644 index 0000000..2ec7967 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/app-spinner.component.html @@ -0,0 +1,16 @@ +
+
+
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/app-spinner.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/app-spinner.component.ts new file mode 100644 index 0000000..2fd1161 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/app-spinner.component.ts @@ -0,0 +1,16 @@ +import { CommonModule } from "@angular/common"; +import { Component, input, InputSignal } from "@angular/core"; + +import { APP_SPINNER_COMPONENT_SIZE } from "./enum"; +import { AppSpinnerComponentType } from "./types"; + +@Component({ + selector: "app-spinner", + imports: [CommonModule], + templateUrl: "./app-spinner.component.html", +}) +export class AppSpinnerComponent { + protected readonly APP_SPINNER_COMPONENT_SIZE = APP_SPINNER_COMPONENT_SIZE; + + public size: InputSignal = input(APP_SPINNER_COMPONENT_SIZE.MEDIUM); +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/enum.ts new file mode 100644 index 0000000..91b8b66 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/enum.ts @@ -0,0 +1,5 @@ +export enum APP_SPINNER_COMPONENT_SIZE { + SMALL = "small", + MEDIUM = "medium", + LARGE = "large", +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/types.ts new file mode 100644 index 0000000..6ab19ce --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/types.ts @@ -0,0 +1,3 @@ +import { APP_SPINNER_COMPONENT_SIZE } from "./enum"; + +export type AppSpinnerComponentType = APP_SPINNER_COMPONENT_SIZE.SMALL | APP_SPINNER_COMPONENT_SIZE.MEDIUM | APP_SPINNER_COMPONENT_SIZE.LARGE; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-toggle/app-toggle.component.html b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-toggle/app-toggle.component.html new file mode 100644 index 0000000..28cfa6d --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-toggle/app-toggle.component.html @@ -0,0 +1,49 @@ +
+
+ + +
+ + +
+
diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-toggle/app-toggle.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-toggle/app-toggle.component.ts new file mode 100644 index 0000000..68d5d17 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-toggle/app-toggle.component.ts @@ -0,0 +1,56 @@ +import { CommonModule } from "@angular/common"; +import { Component, input, InputSignal, output, OutputEmitterRef } from "@angular/core"; + +import { AppIconComponent } from "../app-icon/app-icon.component"; +import { APP_ICON_SIZES, APP_ICON_VARIANTS } from "../app-icon/enum"; + +const MODULES = [CommonModule]; +const COMPONENTS = [AppIconComponent]; + +/** + * AppToggleComponent + * + * A reusable toggle switch component that provides a visual on/off state indicator. + * The component supports disabled state and emits events when toggled. + * + * @example + * ```html + * + * + * ``` + * + * @input {boolean} isChecked - Determines if the toggle is in the checked (on) state. Default: false + * @input {boolean} isDisabled - Determines if the toggle is disabled and cannot be interacted with. Default: false + * + * @output {boolean} onToggle - Emits the new checked state when the toggle is clicked + */ +@Component({ + selector: "app-toggle", + imports: [...MODULES, ...COMPONENTS], + templateUrl: "./app-toggle.component.html", +}) +export class AppToggleComponent { + protected readonly APP_ICON_VARIANTS = APP_ICON_VARIANTS; + protected readonly APP_ICON_SIZES = APP_ICON_SIZES; + + public readonly isChecked: InputSignal = input(false); + public readonly isDisabled: InputSignal = input(false); + + public readonly onToggle: OutputEmitterRef = output(); + + protected handleToggle(): void { + if (!this.isDisabled()) { + this.onToggle.emit(!this.isChecked()); + } + } + + protected handleKeyDown(event: KeyboardEvent): void { + if (event.key === " " || event.key === "Enter") { + event.preventDefault(); + this.handleToggle(); + } + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/app-tooltip.component.css b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/app-tooltip.component.css new file mode 100644 index 0000000..d3a980d --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/app-tooltip.component.css @@ -0,0 +1,50 @@ +.app-tooltip__arrow { + position: absolute; + width: 8px; + height: 8px; + background: var(--color-tooltip-background); + pointer-events: none; + z-index: 1; +} + +/* TOP: tooltip above trigger → arrow points DOWN at bottom edge. + left is set dynamically via [ngStyle] to track the trigger center. */ +.app-tooltip__arrow--top { + bottom: -5px; + transform: translateX(-50%) rotate(45deg); + border-right: 1px solid var(--color-tooltip-border); + border-bottom: 1px solid var(--color-tooltip-border); +} + +/* BOTTOM: tooltip below trigger → arrow points UP at top edge. + left is set dynamically via [ngStyle] to track the trigger center. */ +.app-tooltip__arrow--bottom { + top: -5px; + transform: translateX(-50%) rotate(45deg); + border-left: 1px solid var(--color-tooltip-border); + border-top: 1px solid var(--color-tooltip-border); +} + +/* LEFT: tooltip left of trigger → arrow points RIGHT at right edge. + top is set dynamically via [ngStyle] to track the trigger center. */ +.app-tooltip__arrow--left { + right: -5px; + transform: translateY(-50%) rotate(45deg); + border-top: 1px solid var(--color-tooltip-border); + border-right: 1px solid var(--color-tooltip-border); +} + +/* RIGHT: tooltip right of trigger → arrow points LEFT at left edge. + top is set dynamically via [ngStyle] to track the trigger center. */ +.app-tooltip__arrow--right { + left: -5px; + transform: translateY(-50%) rotate(45deg); + border-bottom: 1px solid var(--color-tooltip-border); + border-left: 1px solid var(--color-tooltip-border); +} + +@media (prefers-reduced-motion: reduce) { + .app-tooltip__container { + transition: none; + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/app-tooltip.component.html b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/app-tooltip.component.html new file mode 100644 index 0000000..37d016e --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/app-tooltip.component.html @@ -0,0 +1,45 @@ + + + + + + +
+
+ +
+
+
+
+ + +@if (isVisible()) { + +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/app-tooltip.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/app-tooltip.component.ts new file mode 100644 index 0000000..895ea6f --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/app-tooltip.component.ts @@ -0,0 +1,301 @@ +import { CommonModule } from "@angular/common"; +import { Component, computed, ElementRef, HostListener, inject, input, InputSignal, OnInit, Signal, signal, viewChild, WritableSignal } from "@angular/core"; + +import { TOOLTIP_DISPLAY_MODES, TOOLTIP_POSITIONS } from "./enum"; +import { TooltipDisplayModeType, TooltipPositionType } from "./types"; +import { ComponentEffectManagerService as EffectService } from "../../services/component-effect-manager/component-effect-manager.service"; +import { LoggerService } from "../../services/logger/logger.service"; + +/** + * AppTooltipComponent - A flexible tooltip component that displays contextual information. + * + * This component intelligently positions a tooltip around a trigger element in one of four directions: + * TOP, BOTTOM, LEFT, or RIGHT. It automatically measures the tooltip's size and calculates the best + * position to keep it visible within the viewport. + * + * **How it works:** + * 1. When hovering over the trigger element, the tooltip is rendered but kept invisible + * 2. The component measures the tooltip's actual width and height + * 3. It calculates the ideal position based on the trigger element's location and the chosen direction + * 4. If the tooltip would go outside the viewport, it adjusts the position to keep it visible (with 8px padding) + * 5. Once positioned, the tooltip smoothly fades in with a 150ms transition + * 6. The tooltip automatically repositions itself when the window is scrolled or resized + * + * **TOP Placement Calculation (Visual Schema):** + * ``` + * ┌─────────────────────────────────────────┐ + * │ Viewport │ + * │ │ + * │ ┌───────────────┐ │ + * │ │ Tooltip │ ← y = triggerRect.top - tooltipHeight - offset + * │ └───────────────┘ │ + * │ ↑ │ + * │ (offset) │ + * │ ↓ │ + * │ ┌─────────────┐ │ + * │ │ Trigger │ │ + * │ └─────────────┘ │ + * │ ↑ ↑ │ + * │ rect.left rect.left + width │ + * │ │ + * │ x = rect.left + (width / 2) - (tooltipWidth / 2) + * │ └─────┬─────┘ └────┬────┘ │ + * │ trigger center center tooltip │ + * └─────────────────────────────────────────┘ + * + * Horizontal (x): Center tooltip above trigger + * • Start at trigger's left edge + * • Move right by half the trigger's width (finds center) + * • Move left by half the tooltip's width (centers tooltip) + * + * Vertical (y): Place tooltip above trigger + * • Start at trigger's top edge + * • Move up by tooltip's full height + * • Move up by offset distance (gap between elements) + * + * Clamping: Keep tooltip within viewport bounds + * • Ensure x is at least 8px from left edge + * • Ensure x leaves 8px space on right edge + * ``` + * + * **Usage:** + * ```html + * + * + * This is the tooltip text + * + * ``` + * + * @input tooltipPosition - Where to show the tooltip: 'top' | 'bottom' | 'left' | 'right' (default: 'top') + * @input triggerOffset - Distance in pixels between the trigger element and tooltip (default: 14) + * @input displayMode - Controls when the tooltip is shown: 'hover' | 'event' | 'both' (default: 'hover') + * @input forceVisible - Programmatically shows the tooltip when displayMode is 'event' or 'both' + */ +@Component({ + selector: "app-tooltip", + templateUrl: "./app-tooltip.component.html", + styleUrl: "./app-tooltip.component.css", + imports: [CommonModule], + providers: [ + { + provide: EffectService, + useFactory: () => new EffectService("AppTooltipComponent"), + }, + ], +}) +export class AppTooltipComponent implements OnInit { + protected readonly TOOLTIP_POSITIONS = TOOLTIP_POSITIONS; + protected readonly TOOLTIP_DISPLAY_MODES = TOOLTIP_DISPLAY_MODES; + + private readonly _logger = inject(LoggerService).get("AppTooltipComponent"); + + // Provided in current component + private readonly _effectService: EffectService = inject(EffectService); + + public readonly tooltipPosition: InputSignal = input(this.TOOLTIP_POSITIONS.TOP); + public readonly triggerOffset: InputSignal = input(14); + /** + * Controls when the tooltip is shown. + * - 'hover' : shown on mouse hover only (default, backward-compatible) + * - 'event' : shown programmatically via forceVisible only — hover is suppressed + * - 'both' : shown on hover and/or via forceVisible + */ + public readonly displayMode: InputSignal = input(TOOLTIP_DISPLAY_MODES.HOVER); + public readonly forceVisible: InputSignal = input(false); + /** + * When true the tooltip will never show. Defaults to false. + * Useful when tooltip content is conditionally projected. + */ + public readonly disabled: InputSignal = input(false); + + protected readonly isVisible: WritableSignal = signal(false); + protected readonly isPositioned: WritableSignal = signal(false); + protected readonly coordinates: WritableSignal<{ x: number; y: number }> = signal<{ + x: number; + y: number; + }>({ + x: 0, + y: 0, + }); + protected readonly tooltipWidth: WritableSignal = signal(0); + protected readonly tooltipHeight: WritableSignal = signal(0); + + /** + * Pixel offset (left or top) for the arrow element, calculated after the + * tooltip position is clamped to the viewport. This keeps the arrow + * pointing at the trigger's centre even when the tooltip box is pushed + * away from the edge. + */ + protected readonly arrowOffset: WritableSignal = signal(0); + protected readonly arrowStyle: Signal<{ left?: string; top?: string }> = computed(() => { + const position: TooltipPositionType = this.tooltipPosition(); + const offset: number = this.arrowOffset(); + + if (position === TOOLTIP_POSITIONS.TOP || position === TOOLTIP_POSITIONS.BOTTOM) { + return { left: offset + "px" }; + } + + return { top: offset + "px" }; + }); + + protected readonly isHoverEnabled: Signal = computed(() => this.displayMode() !== TOOLTIP_DISPLAY_MODES.EVENT); + + // View Children + protected readonly tooltipTriggerElement: Signal = viewChild("triggerElement"); + protected readonly tooltipContainer: Signal = viewChild("tooltipContainer"); + + public ngOnInit(): void { + this.registerEffects(); + } + + private registerEffects(): void { + this._effectService.registerEffect("AppTooltipComponent.showOrHide.on.forceVisibleChange", () => { + const mode = this.displayMode(); + + if (mode === TOOLTIP_DISPLAY_MODES.EVENT || mode === TOOLTIP_DISPLAY_MODES.BOTH) { + if (this.forceVisible()) { + this._showTooltip(); + } else { + this._hideTooltip(); + } + } + }); + } + + /** Hover-triggered show — suppressed when displayMode is 'event' or tooltip is disabled. */ + protected async showTooltip(): Promise { + if (this.displayMode() === TOOLTIP_DISPLAY_MODES.EVENT) return; + if (this.disabled()) return; + await this._showTooltip(); + } + + /** Hover-triggered hide — suppressed when displayMode is 'event' or forceVisible keeps it open. */ + protected hideTooltip(): void { + const mode = this.displayMode(); + + if (mode === TOOLTIP_DISPLAY_MODES.EVENT) return; + // In 'both' mode don't dismiss via hover while forceVisible is still active + if (mode === TOOLTIP_DISPLAY_MODES.BOTH && this.forceVisible()) return; + + this._hideTooltip(); + } + + private async _showTooltip(): Promise { + this.isVisible.set(true); + this.isPositioned.set(false); + + // Wait for next frame to ensure DOM is rendered + await new Promise((resolve) => requestAnimationFrame(resolve)); + + const el = this.tooltipContainer()?.nativeElement; + + if (!el) return; + + this.measureTooltip(); + this.updatePosition(); + + this.isPositioned.set(true); + } + + private _hideTooltip(): void { + this.isVisible.set(false); + this.isPositioned.set(false); + } + + @HostListener("window:scroll") + @HostListener("window:resize") + onViewportChange() { + if (!this.isVisible()) return; + + this.updatePosition(); + } + + private updatePosition(): void { + const triggerEl = this.tooltipTriggerElement()?.nativeElement; + const tooltipWidth: number = this.tooltipWidth(); + const tooltipHeight: number = this.tooltipHeight(); + + if (!triggerEl || tooltipWidth === 0 || tooltipHeight === 0) { + this._logger.warn(`Tooltip positioning skipped: triggerEl=${!!triggerEl}, tooltipWidth=${tooltipWidth}, tooltipHeight=${tooltipHeight}`); + return; + } + + const triggerRect = triggerEl.getBoundingClientRect(); + const offset: number = this.triggerOffset(); + + let x: number = 0; + let y: number = 0; + + let position: TooltipPositionType = this.tooltipPosition(); + + if (!position) { + this._logger.warn(`Invalid tooltip position: ${position}. Falling back to TOP`); + + position = TOOLTIP_POSITIONS.TOP; + } + + if (position === TOOLTIP_POSITIONS.TOP) { + // Center horizontally above trigger + x = triggerRect.left + triggerRect.width / 2 - tooltipWidth / 2; + y = triggerRect.top - tooltipHeight - offset; + + // Clamp horizontally to viewport + x = Math.max(8, Math.min(x, window.innerWidth - tooltipWidth - 8)); + } + + if (position === TOOLTIP_POSITIONS.BOTTOM) { + // Center horizontally below trigger + x = triggerRect.left + triggerRect.width / 2 - tooltipWidth / 2; + y = triggerRect.bottom + offset; + + // Clamp horizontally to viewport + x = Math.max(8, Math.min(x, window.innerWidth - tooltipWidth - 8)); + } + + if (position === TOOLTIP_POSITIONS.LEFT) { + // Center vertically to the left of trigger + x = triggerRect.left - tooltipWidth - offset; + y = triggerRect.top + triggerRect.height / 2 - tooltipHeight / 2; + + // Clamp vertically to viewport + y = Math.max(8, Math.min(y, window.innerHeight - tooltipHeight - 8)); + } + + if (position === TOOLTIP_POSITIONS.RIGHT) { + // Center vertically to the right of trigger + x = triggerRect.right + offset; + y = triggerRect.top + triggerRect.height / 2 - tooltipHeight / 2; + + // Clamp vertically to viewport + y = Math.max(8, Math.min(y, window.innerHeight - tooltipHeight - 8)); + } + + this.updateArrowOffset(position, triggerRect, { x, y }); + + this.coordinates.set({ x, y }); + } + + /** + * Recomputes the arrow offset after the tooltip has been clamped to the + * viewport, so the arrow always points at the trigger's centre regardless + * of how far the tooltip box was shifted. + */ + private updateArrowOffset(position: TooltipPositionType, triggerRect: DOMRect, tooltipOrigin: { x: number; y: number }): void { + const arrowMargin = 10; // keep arrow at least 10 px from each edge + const isHorizontal = position === TOOLTIP_POSITIONS.TOP || position === TOOLTIP_POSITIONS.BOTTOM; + const rawOffset = isHorizontal ? triggerRect.left + triggerRect.width / 2 - tooltipOrigin.x : triggerRect.top + triggerRect.height / 2 - tooltipOrigin.y; + const maxOffset = isHorizontal ? this.tooltipWidth() : this.tooltipHeight(); + + this.arrowOffset.set(Math.max(arrowMargin, Math.min(rawOffset, maxOffset - arrowMargin))); + } + + private measureTooltip(): void { + const el = this.tooltipContainer()?.nativeElement; + if (!el) return; + + const rect = el.getBoundingClientRect(); + + this.tooltipWidth.set(rect.width); + this.tooltipHeight.set(rect.height); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/enum.ts new file mode 100644 index 0000000..8393251 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/enum.ts @@ -0,0 +1,15 @@ +export enum TOOLTIP_POSITIONS { + TOP = "top", + BOTTOM = "bottom", + LEFT = "left", + RIGHT = "right", +} + +export enum TOOLTIP_DISPLAY_MODES { + /** Show only on mouse hover (default). */ + HOVER = "hover", + /** Show only programmatically via the forceVisible input. No hover. */ + EVENT = "event", + /** Show on hover and/or programmatically via forceVisible. */ + BOTH = "both", +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/types.ts new file mode 100644 index 0000000..c24f091 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/types.ts @@ -0,0 +1,4 @@ +import { TOOLTIP_DISPLAY_MODES, TOOLTIP_POSITIONS } from "./enum"; + +export type TooltipPositionType = TOOLTIP_POSITIONS[keyof TOOLTIP_POSITIONS]; +export type TooltipDisplayModeType = TOOLTIP_DISPLAY_MODES[keyof TOOLTIP_DISPLAY_MODES]; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/constants/agents.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/constants/agents.ts new file mode 100644 index 0000000..1ac29b6 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/constants/agents.ts @@ -0,0 +1,3 @@ +// TODO: Replace this when we decide to support multiple agents +// Allow dynamic agent selection - keep it saved it a store. +export const SELECTED_AGENT = "ioAgent"; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/constants/ui-prefs-keys.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/constants/ui-prefs-keys.ts new file mode 100644 index 0000000..1f58552 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/constants/ui-prefs-keys.ts @@ -0,0 +1,8 @@ +/** + * Use this keys to store and retrieve user interface preferences from preferences storage. + */ +export const UI_PREFS_KEYS = { + PROMPTS: "prompts", + FAVORITE_PROMPTS: "favoritePrompts", + TOOLS: "tools", +}; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/constants/ui-strings.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/constants/ui-strings.ts new file mode 100644 index 0000000..d192298 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/constants/ui-strings.ts @@ -0,0 +1,128 @@ +/** + * Shared UI strings for the whole application + * Strings are grouped by component, feature or defaults + */ +export const UI_STRINGS = { + INPUT_COMPONENT: { + DEFAULT_PLACEHOLDER: "Enter text here", + INPUT_PLACEHOLDER: "Type your message...", + }, + PANEL_COMPONENT: { + DEFAULT_TITLE: "Panel", + CLOSE_BUTTON: "Close", + }, + MESSAGE_FOOTER_COMPONENT: { + RELOAD_DISABLED_TOOLTIP: "Response is pending...", + }, + HEADER_COMPONENT: { + HOME_TOOLTIP: "Start new chat", + CONTEXT_TOOLTIP: "The working context provides information to assist the agent in generating accurate responses.", + }, + WORKING_CONTEXT_PANEL_COMPONENT: { + TITLE: "Working Context", + VIEW_WORKING_CONTEXT_BUTTON: "View Context", + LOADING_CONTEXT: "Loading working context...", + CALLOUT_TEXT: "The working context provides additional information to assist the agent in generating accurate responses.", + }, + CHAT_COMPONENT: { + LOGOUT: "Logout", + }, + THREAD_HISTORY_COMPONENT: { + NO_THREADS: "No available message threads.", + LOADING_THREADS: "Loading threads...", + DIVIDERS: { + JUST_NOW: "Just now", + FIVE_MINS_AGO: "5 mins ago", + THIRTY_MINS_AGO: "30 mins ago", + ONE_HOUR_AGO: "1 hour ago", + TWO_HOURS_AGO: "2 hours ago", + SIX_HOURS_AGO: "6 hours ago", + TWELVE_HOURS_AGO: "12 hours ago", + TODAY: "Today", + YESTERDAY: "Yesterday", + TWO_DAYS_AGO: "2 days ago", + LAST_WEEK: "Last Week", + TWO_WEEKS_AGO: "2 weeks ago", + ONE_MONTH_AGO: "1 month ago", + THREE_MONTHS_AGO: "3 months ago", + SIX_MONTHS_AGO: "6 months ago", + MORE_THAN_YEAR_AGO: "more than 1 year ago", + }, + }, + PROMPT_LIST_COMPONENT: { + AVAILABLE_PROMPTS: "Available Prompts", + FAVORITE_PROMPTS: "Favorite Prompts", + NO_PROMPTS: "No prompts available.", + NO_PROMPTS_CONFIGURED: "No prompts are available. Prompts should be provided as a configuration.", + NO_PROMPTS_SEE_MORE: "See more", + LOADING_PROMPTS: "Loading prompts...", + NO_FAVORITE_PROMPTS: "No favorite prompts available.", + INPUT_TOOLTIP: "Search prompts...", + }, + TOOL_LIST_COMPONENT: { + AVAILABLE_TOOLS: "Available Tools", + NO_FILTERED_TOOLS: "No tools matching your search.", + NO_TOOLS: "There are currently no tools available.", + NO_TOOLS_SEE_MORE: "See more", + LOADING_TOOLS: "Loading tools...", + INPUT_TOOLTIP: "Search tools...", + }, + TOOL_LIST_ITEM_COMPONENT: { + ENABLE_BUTTON: "Enable", + DISABLE_BUTTON: "Disable", + }, + TOOL_INFO: { + DEFAULT_TOOL_DESCRIPTION: "No description available.", + }, + TOOL_MESSAGE: { + TOOL_EXECUTION_SUCCESS: "Tool executed successfully.", + TOOL_EXECUTION_PENDING: "Tool execution in progress...", + }, + TOOL_TRACE_MESSAGE_COMPONENT: { + EXPAND_BUTTON_TEXT: "Show Tool Trace Details", + COLLAPSE_BUTTON_TEXT: "Hide Tool Trace Details", + CLOSE_BUTTON_TEXT: "Close", + }, + GENERAL: { + EDIT: "Edit", + DELETE: "Delete", + SAVE: "Save", + CANCEL: "Cancel", + CONFIRM: "Confirm", + SUBMIT: "Submit", + SEARCH: "Search", + LOADING: "Loading", + COPY: "Copy", + COPIED: "Copied!", + RELOAD: "Reload", + NO_SEARCH_RESULTS: "No results found", + WELCOME_MESSAGE(user?: string): string { + return user ? `Welcome, ${user}! How can I assist you today?` : "Welcome! How can I assist you today?"; + }, + CANNOT_CONNECT_TO_IO: "Cannot connect to IO.", + AI_CONTENT_INFO: "AI-generated content may be incorrect", + INITIALIZING_SERVICES: "Initializing services, please wait...", + REFRESH: "Refresh", + IO_INTEL_INIT_ERROR: "Error initializing IO Intel Web API. Please check your configuration and refresh the page to try again.", + DOCUMENTATION_LINK(url?: string): string { + return `For more information, please refer to the documentation: ${url && url.length > 0 ? url : "https://docs-ai.interop.io/"}`; + }, + }, + SAMPLING_MODAL: { + TITLE: "io.Assist", + HEADING: "Permission to Proceed", + TEXT: `I can dive deeper to give a clearer answer, but I'll need your permission first. Continue?`, + }, + ELICITATION_MODAL: { + TITLE: "io.Assist", + HEADING: "Permission to Proceed", + FALLBACK_TEXT: "I can dive deeper to give a clearer answer, but I’ll need your permission first. Continue?", + }, + MCP_APP_REPLACE_MODAL: { + TITLE: "MCP App Already Open", + HEADING: "Replace Existing Instance?", + TEXT: (toolName: string) => `An instance of "${toolName}" is already open in the workspace. Replace it with the new instance, or open it alongside as a new instance?`, + REPLACE_BUTTON: "Replace", + NEW_INSTANCE_BUTTON: "New Instance", + }, +}; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/accent-gradient-border/accent-gradient-border.directive.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/accent-gradient-border/accent-gradient-border.directive.ts new file mode 100644 index 0000000..99e0a2d --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/accent-gradient-border/accent-gradient-border.directive.ts @@ -0,0 +1,146 @@ +import { Directive, ElementRef, input, InputSignal, Renderer2, inject, OnInit } from "@angular/core"; + +import { ComponentEffectManagerService as EffectService } from "../../services/component-effect-manager/component-effect-manager.service"; +import { applyStyles, createElement } from "../utils"; + +/** + * AccentGradientBorderDirective + * + * A directive that applies a 1px gradient border using accent colors to any HTML element. + * The gradient uses --color-app-accent-color-1 and --color-app-accent-color-2. + * + * Usage: + * ```html + * + *
Content
+ * + * + *
Content
+ * ``` + * + * DOM Structure Created: + * - .accent-gradient-border__layer (gradient border layer, positioned behind content) + * - .accent-gradient-border__background (solid background to mask gradient center) + * - Existing content (z-indexed above all layers) + */ +@Directive({ + selector: "[accentGradientBorder]", + standalone: true, + providers: [ + { + provide: EffectService, + useFactory: () => new EffectService("AccentGradientBorderDirective"), + }, + ], +}) +export class AccentGradientBorderDirective implements OnInit { + public accentGradientBorder: InputSignal = input(true); + + private _renderer: Renderer2 = inject(Renderer2); + private _elementRef: ElementRef = inject(ElementRef); + // Provided in current directive + private _effectService: EffectService = inject(EffectService); + + private _borderLayer: HTMLElement | null = null; + private _background: HTMLElement | null = null; + + public ngOnInit(): void { + this.registerEffects(); + } + + private registerEffects(): void { + this._effectService.registerEffect("AccentGradientBorderDirective.toggleBorder.on.accentGradientBorderChange", () => { + const isEnabled = this.accentGradientBorder(); + + if (!isEnabled) { + this.cleanup(); + return; + } + + this.setup(); + }); + } + + private setup(): void { + if (this._borderLayer) { + return; + } + + const host = this._elementRef.nativeElement; + + this._renderer.setStyle(host, "position", "relative"); + + this._borderLayer = this.createBorderLayer(); + this._background = this.createBackground(); + + this._renderer.insertBefore(host, this._borderLayer, host.firstChild); + this._renderer.insertBefore(host, this._background, host.firstChild); + + this.updateContentZIndex(host); + } + + private cleanup(): void { + if (!this._borderLayer) { + return; + } + + const host = this._elementRef.nativeElement; + + this._renderer.removeChild(host, this._borderLayer); + this._borderLayer = null; + + if (this._background) { + this._renderer.removeChild(host, this._background); + this._background = null; + } + } + + private createBorderLayer(): HTMLElement { + const maskValue = "linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)"; + + return createElement(this._renderer, "div", "accent-gradient-border__layer", { + position: "absolute", + top: "0", + right: "0", + bottom: "0", + left: "0", + "border-radius": "inherit", + "pointer-events": "none", + "z-index": "-1", + background: "linear-gradient(90deg, var(--color-app-accent-color-1), var(--color-app-accent-color-2))", + padding: "1px", + mask: maskValue, + "-webkit-mask": maskValue, + "mask-composite": "exclude", + "-webkit-mask-composite": "xor", + }); + } + + private createBackground(): HTMLElement { + return createElement(this._renderer, "div", "accent-gradient-border__background", { + position: "absolute", + top: "0", + right: "0", + bottom: "0", + left: "0", + "border-radius": "inherit", + background: "var(--color-bg-default)", + "z-index": "-1", + "pointer-events": "none", + }); + } + + private updateContentZIndex(host: HTMLElement): void { + this._renderer.setStyle(host, "isolation", "isolate"); + + Array.from(host.children).forEach((child: Element) => { + if (child === this._borderLayer || child === this._background) { + return; + } + applyStyles(this._renderer, child as HTMLElement, { + position: "relative", + "z-index": "1", + }); + }); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/README.md b/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/README.md new file mode 100644 index 0000000..c9da2fa --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/README.md @@ -0,0 +1,132 @@ +# Animation Effect Directive + +Applies animated visual effects to HTML elements using the Strategy pattern. Supports single animations and chaining. + +## Quick Start + +```html + + + + +
Glowing content
+ + +
+ Glow on mount, then hover effect +
+``` + +## Available Animations + +| Type | Behavior | Best For | +|------|----------|----------| +| `HOVER_MOUSE_FOLLOW` | Gradient border follows cursor on hover | Buttons, cards, interactive elements | +| `MOUNT_360_GLOW` | 360° rotating glow on mount, fades to static border | Input areas, important containers | + +## Animation Chaining + +Pass an array to play animations sequentially: + +```typescript +protected readonly animationChain = [ + AnimationType.MOUNT_360_GLOW, // Auto-completes after rotation + AnimationType.HOVER_MOUSE_FOLLOW // Remains active for interaction +]; +``` + +**Chain behavior:** +- Auto-completing animations trigger the next in sequence +- Interactive animations remain active as the final state +- Each animation cleans up before the next starts + +## Adding New Animations + +1. **Add to enum** (`animation-type.enum.ts`) +2. **Create strategy class** extending `BaseAnimationStrategy` in `effects/` +3. **Register in factory** (`animation-effect.factory.ts`) +4. **Add to chained strategy** (`chained-animation.effect.ts`) + +Key implementation points: +- Call `this.notifyComplete()` when auto-completing animations finish +- Interactive animations should NOT call `notifyComplete()` +- Implement `cleanup()` to remove DOM elements and clear timers + +### Example: Red Border Animation + +A simple animation that smoothly transitions the border to 2px solid red on mount. + +**Step 1: Add to enum** +```typescript +// animation-type.enum.ts +MOUNT_RED_BORDER = 'mount-red-border', +``` + +**Step 2: Create the strategy** +```typescript +// effects/mount-red-border-animation.effect.ts +import { ElementRef, Renderer2 } from '@angular/core'; +import { BaseAnimationStrategy } from '../core/base-animation.effect'; + +export class MountRedBorderAnimationStrategy extends BaseAnimationStrategy { + private originalBorder = ''; + + constructor(elementRef: ElementRef, renderer: Renderer2) { + super(elementRef, renderer); + } + + public setup(onComplete?: () => void): void { + this.onCompleteCallback = onComplete; + const host = this.elementRef.nativeElement; + + // Store original border + this.originalBorder = window.getComputedStyle(host).border; + + // Apply transition and new border + this.renderer.setStyle(host, 'transition', 'border 300ms ease-in-out'); + this.renderer.setStyle(host, 'border', '2px solid red'); + + // Auto-complete after transition + setTimeout(() => this.notifyComplete(), 300); + } + + public cleanup(): void { + const host = this.elementRef.nativeElement; + this.renderer.setStyle(host, 'border', this.originalBorder); + this.renderer.removeStyle(host, 'transition'); + } +} +``` + +**Step 3: Register in factory** - Add case in `AnimationStrategyFactory.create()` + +**Step 4: Add to chained strategy** - Add case in `ChainedAnimationStrategy.createStrategy()` + +## CSS Variables + +The animations use these design system tokens: + +- `--app-accent-color-1`, `--app-accent-color-2` - Gradient colors +- `--app-border-default` - Static border color + +## Best Practices + +- Apply `border-radius` for proper effect clipping +- Ensure element has a background color +- Keep chains short (2-3 animations max) +- End chains with interactive animations (if needed) + +## Architecture + +``` +animation-effect/ +├── animation-effect.directive.ts # Main directive +├── animation-type.enum.ts # Animation types +├── core/ +│ ├── base-animation.effect.ts # Abstract base class +│ ├── animation-effect.factory.ts # Strategy factory +│ └── chained-animation.effect.ts # Chaining logic +└── effects/ + ├── hover-mouse-follow-animation.effect.ts + └── mount-360-glow-animation.effect.ts +``` \ No newline at end of file diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/animation-effect.directive.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/animation-effect.directive.ts new file mode 100644 index 0000000..bca202c --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/animation-effect.directive.ts @@ -0,0 +1,115 @@ +import { Directive, ElementRef, HostListener, inject, input, InputSignal, OnDestroy, OnInit, output, OutputEmitterRef, Renderer2 } from "@angular/core"; + +import { AnimationType } from "./animation-type.enum"; +import { AnimationStrategyFactory } from "./core/animation-effect.factory"; +import { AnimationStrategy } from "./core/base-animation.effect"; + +/** + * AnimationEffectDirective + * + * A directive that applies animated visual effects to any HTML element. + * Supports multiple animation types through a strategy pattern. + * Supports animation chaining by accepting an array of animation types. + * Emits events when animations start and complete. + * + * Usage: + * ```html + * + *
Content
+ * + * + *
Content
+ * + * + *
Content
+ * ``` + * + * @example + * // With default animation + * + * + * @example + * // With specific animation type + *
Hover me
+ * + * @example + * // With chained animations and events + *
Chained effect
+ */ +@Directive({ + selector: "[animationEffect]", + standalone: true, +}) +export class AnimationEffectDirective implements OnInit, OnDestroy { + /** + * The type of animation to apply. Can be a single AnimationType or an array for chaining. + * Defaults to CLICK_SPIN_360. + */ + public animationEffect: InputSignal = input(AnimationType.CLICK_SPIN_360); + + /** + * Emits when the animation starts (during ngOnInit). + */ + public animationStarted: OutputEmitterRef = output(); + + /** + * Emits when the animation completes. + * For auto-completing animations (like MOUNT_360_GLOW), emits when the animation finishes. + * For interactive animations (like HOVER_MOUSE_FOLLOW), emits immediately as they don't auto-complete. + * For chained animations, emits when all animations in the chain complete. + */ + public animationCompleted: OutputEmitterRef = output(); + + private animationStrategy!: AnimationStrategy; + private _elementRef: ElementRef = inject(ElementRef); + private _renderer: Renderer2 = inject(Renderer2); + + public ngOnInit(): void { + this.animationStrategy = AnimationStrategyFactory.create(this.animationEffect(), this._elementRef, this._renderer); + + // Emit animation started event + this.animationStarted.emit(); + + // Setup animation with completion callback + this.animationStrategy.setup(() => { + // Emit animation completed event + this.animationCompleted.emit(); + }); + } + + public ngOnDestroy(): void { + if (!this.animationStrategy) { + return; + } + + this.animationStrategy.cleanup(); + } + + @HostListener("mouseenter") + protected onMouseEnter(): void { + this.animationStrategy.onMouseEnter(); + } + + @HostListener("mouseleave") + protected onMouseLeave(): void { + this.animationStrategy.onMouseLeave(); + } + + @HostListener("click") + protected onClick(): void { + this.animationStrategy.onClick(); + } + + @HostListener("mousemove", ["$event"]) + protected onMouseMove(event: MouseEvent): void { + this.animationStrategy.onMouseMove(event); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/animation-type.enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/animation-type.enum.ts new file mode 100644 index 0000000..f627465 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/animation-type.enum.ts @@ -0,0 +1,14 @@ +export enum AnimationType { + /** + * Rotates the element 360 degrees on click. + * A smooth single-rotation spin triggered each time the user clicks the element. + */ + CLICK_SPIN_360 = "click-spin-360", + /** + * Combined hover-tilt + click-spin animation for reload/refresh icons. + * - Hover enter: smoothly tilts 45° clockwise. + * - Hover leave: smoothly returns to 0°. + * - Click: spins 270° clockwise then returns to 0° (no full circle). + */ + HOVER_TILT_45_CLICK_SPIN_270 = "hover-tilt-45-click-spin-270", +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/core/animation-effect.factory.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/core/animation-effect.factory.ts new file mode 100644 index 0000000..a5e9f03 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/core/animation-effect.factory.ts @@ -0,0 +1,36 @@ +import { ElementRef, Renderer2 } from "@angular/core"; + +import { AnimationStrategy } from "./base-animation.effect"; +import { ChainedAnimationStrategy } from "./chained-animation.effect"; +import { AnimationType } from "../animation-type.enum"; +import { ClickSpin360AnimationStrategy } from "../effects/click-spin-360-animation.effect"; +import { HoverTilt45ClickSpin270AnimationStrategy } from "../effects/hover-tilt-click-spin-270-animation.effect"; + +export class AnimationStrategyFactory { + public static create(animationType: AnimationType | AnimationType[], elementRef: ElementRef, renderer: Renderer2): AnimationStrategy { + if (!animationType) { + throw new Error(`Animation type is required`); + } + + // Handles animation chaining + if (Array.isArray(animationType)) { + const validAnimations = animationType.filter((type) => Object.values(AnimationType).includes(type)); + + return new ChainedAnimationStrategy(validAnimations, elementRef, renderer); + } + + if (!Object.values(AnimationType).includes(animationType)) { + throw new Error(`Unknown animation type: ${animationType}`); + } + + if (animationType === AnimationType.CLICK_SPIN_360) { + return new ClickSpin360AnimationStrategy(elementRef, renderer); + } + + if (animationType === AnimationType.HOVER_TILT_45_CLICK_SPIN_270) { + return new HoverTilt45ClickSpin270AnimationStrategy(elementRef, renderer); + } + + throw new Error(`Unhandled animation type: ${animationType}`); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/core/base-animation.effect.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/core/base-animation.effect.ts new file mode 100644 index 0000000..a1d373b --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/core/base-animation.effect.ts @@ -0,0 +1,71 @@ +import { ElementRef, Renderer2 } from "@angular/core"; + +export type AnimationStrategy = { + /** + * Initialize the animation by setting up required DOM elements and styles + * @param onComplete Optional callback to invoke when animation completes (for chaining) + */ + setup(onComplete?: () => void): void; + + /** + * (default no-op) + */ + onMouseEnter(): void; + + /** + * (default no-op) + */ + onMouseLeave(): void; + + /** + * (default no-op) + */ + onMouseMove(event: MouseEvent): void; + + /** + * (default no-op) + */ + onClick(): void; + + /** + * Cleanup resources when animation is destroyed + */ + cleanup(): void; +}; + +export abstract class BaseAnimationStrategy implements AnimationStrategy { + protected onCompleteCallback?: () => void; + + constructor( + protected elementRef: ElementRef, + protected renderer: Renderer2 + ) {} + + abstract setup(onComplete?: () => void): void; + + abstract cleanup(): void; + + public onMouseEnter(): void { + // Default no-op implementation + } + + public onMouseLeave(): void { + // Default no-op implementation + } + + public onMouseMove(_event: MouseEvent): void { + // Default no-op implementation + } + + public onClick(): void { + // Default no-op implementation + } + + protected notifyComplete(): void { + if (!this.onCompleteCallback) { + return; + } + + this.onCompleteCallback(); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/core/chained-animation.effect.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/core/chained-animation.effect.ts new file mode 100644 index 0000000..ffe12ba --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/core/chained-animation.effect.ts @@ -0,0 +1,137 @@ +import { ElementRef, Renderer2 } from "@angular/core"; + +import { AnimationType } from "../animation-type.enum"; +import { AnimationStrategy, BaseAnimationStrategy } from "./base-animation.effect"; +import { ClickSpin360AnimationStrategy } from "../effects/click-spin-360-animation.effect"; +import { HoverTilt45ClickSpin270AnimationStrategy } from "../effects/hover-tilt-click-spin-270-animation.effect"; + +/** + * ChainedAnimationStrategy + * + * Executes multiple animations in sequence, one after another. + * Each animation completes before the next one starts. + * + * Visual Effect: + * 1. First animation plays to completion + * 2. Cleanup of first animation + * 3. Second animation starts and plays to completion + * 4. Repeat for all animations in the chain + * 5. Final animation remains active + * + * Usage: + * Pass an array of AnimationType values to the directive. + * Example: [AnimationType.MOUNT_360_GLOW, AnimationType.HOVER_MOUSE_FOLLOW] + * + * Notes: + * - Animations that don't complete (e.g., HOVER_MOUSE_FOLLOW) will be the final animation + * - Each animation is properly cleaned up before the next one starts + * - Mouse events are forwarded to the current active animation strategy + */ +export class ChainedAnimationStrategy extends BaseAnimationStrategy { + private animationTypes: AnimationType[]; + private currentStrategy?: AnimationStrategy; + private currentIndex = 0; + + constructor(animationTypes: AnimationType[], elementRef: ElementRef, renderer: Renderer2) { + super(elementRef, renderer); + + this.animationTypes = animationTypes; + } + + public setup(onComplete?: () => void): void { + this.onCompleteCallback = onComplete; + + if (this.animationTypes.length === 0) { + this.notifyComplete(); + + return; + } + + this.playNextAnimation(); + } + + public cleanup(): void { + if (!this.currentStrategy) { + return; + } + + this.currentStrategy.cleanup(); + this.currentStrategy = undefined; + } + + public override onMouseEnter(): void { + if (!this.currentStrategy) { + return; + } + + this.currentStrategy.onMouseEnter(); + } + + public override onMouseLeave(): void { + if (!this.currentStrategy) { + return; + } + + this.currentStrategy.onMouseLeave(); + } + + public override onMouseMove(event: MouseEvent): void { + if (!this.currentStrategy) { + return; + } + + this.currentStrategy.onMouseMove(event); + } + + public override onClick(): void { + if (!this.currentStrategy) { + return; + } + + this.currentStrategy.onClick(); + } + + private playNextAnimation(): void { + if (this.currentIndex >= this.animationTypes.length) { + this.notifyComplete(); + + return; + } + + const animationType = this.animationTypes[this.currentIndex]; + this.currentIndex++; + + // Clean up previous strategy if exists + if (this.currentStrategy) { + this.currentStrategy.cleanup(); + } + + // Create new strategy + this.currentStrategy = this.createStrategy(animationType); + + // Setup with completion callback + const isLastAnimation = this.currentIndex >= this.animationTypes.length; + + this.currentStrategy.setup(() => { + if (!isLastAnimation) { + // More animations to play - continue chain + this.playNextAnimation(); + + return; + } + + this.notifyComplete(); + }); + } + + private createStrategy(animationType: AnimationType): AnimationStrategy { + switch (animationType) { + case AnimationType.CLICK_SPIN_360: + return new ClickSpin360AnimationStrategy(this.elementRef, this.renderer); + case AnimationType.HOVER_TILT_45_CLICK_SPIN_270: + return new HoverTilt45ClickSpin270AnimationStrategy(this.elementRef, this.renderer); + default: + throw new Error(`Unknown animation type: ${animationType}`); + } + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/effects/click-spin-360-animation.effect.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/effects/click-spin-360-animation.effect.ts new file mode 100644 index 0000000..70928ec --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/effects/click-spin-360-animation.effect.ts @@ -0,0 +1,84 @@ +import { ElementRef, Renderer2 } from "@angular/core"; + +import { BaseAnimationStrategy } from "../core/base-animation.effect"; + +const SPIN_DURATION_MS = 500; +const SPIN_EASING = "cubic-bezier(0.4, 0, 0.2, 1)"; + +/** + * ClickSpin360AnimationStrategy + * + * Rotates the host element 360 degrees each time the user clicks it. + * Uses a CSS keyframe animation class that is added on click and removed + * when the transition ends, so repeated clicks each trigger a fresh spin. + */ +export class ClickSpin360AnimationStrategy extends BaseAnimationStrategy { + private readonly _className = "animation-effect--spin-360"; + private _removeListener?: () => void; + + constructor(elementRef: ElementRef, renderer: Renderer2) { + super(elementRef, renderer); + } + + public setup(onComplete?: () => void): void { + this.onCompleteCallback = onComplete; + + // Inject the keyframe rule once into the document if not already present + ClickSpin360AnimationStrategy.ensureKeyframes(); + + // Notify complete immediately – this animation is click-driven, not auto-completing + this.notifyComplete(); + } + + public override onClick(): void { + const el: HTMLElement = this.elementRef.nativeElement; + + // Remove then re-add class so repeated clicks restart the animation + this.renderer.removeClass(el, this._className); + + // Force reflow so the browser registers the class removal before re-adding + void el.offsetWidth; + + this.renderer.addClass(el, this._className); + + // Clean up class after animation completes + this._removeListener?.(); + this._removeListener = this.renderer.listen(el, "animationend", () => { + this.renderer.removeClass(el, this._className); + this._removeListener?.(); + this._removeListener = undefined; + }); + } + + public cleanup(): void { + this._removeListener?.(); + this._removeListener = undefined; + + const el: HTMLElement = this.elementRef.nativeElement; + this.renderer.removeClass(el, this._className); + } + + private static ensureKeyframes(): void { + const styleId = "animation-effect-spin-360-keyframes"; + + if (document.getElementById(styleId)) { + return; + } + + const style = document.createElement("style"); + + style.id = styleId; + style.textContent = ` + @keyframes spin-360 { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + .animation-effect--spin-360 { + animation: spin-360 ${SPIN_DURATION_MS}ms ${SPIN_EASING}; + display: inline-flex; + } + `; + + document.head.appendChild(style); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/effects/hover-tilt-click-spin-270-animation.effect.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/effects/hover-tilt-click-spin-270-animation.effect.ts new file mode 100644 index 0000000..cd5caf9 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/effects/hover-tilt-click-spin-270-animation.effect.ts @@ -0,0 +1,144 @@ +import { ElementRef, Renderer2 } from "@angular/core"; + +import { BaseAnimationStrategy } from "../core/base-animation.effect"; + +const HOVER_TILT_DEG = 45; +const HOVER_TRANSITION_MS = 200; +const CLICK_SPIN_DURATION_MS = 550; +const CLICK_SPIN_EASING = "cubic-bezier(0.4, 0, 0.2, 1)"; + +const BASE_CLASS = "animation-effect--hover-tilt-base"; +const HOVER_CLASS = "animation-effect--hover-tilt-45"; +const CLICK_CLASS = "animation-effect--spin-270-return"; + +/** + * HoverTilt45ClickSpin270AnimationStrategy + * + * Two-in-one animation behaviour for the reload/refresh icon: + * - Hover enter → smoothly tilts 45° clockwise + * - Hover leave → smoothly returns to 0° + * - Click → spins 270° clockwise then returns to 0° (no full circle) + * + * CSS transitions handle the hover tilt; a CSS keyframe animation handles the + * click spin so that clicking during a hover works correctly. + */ +export class HoverTilt45ClickSpin270AnimationStrategy extends BaseAnimationStrategy { + private _isHovered = false; + private _isClickAnimating = false; + private _removeClickListener?: () => void; + + constructor(elementRef: ElementRef, renderer: Renderer2) { + super(elementRef, renderer); + } + + public setup(onComplete?: () => void): void { + this.onCompleteCallback = onComplete; + + HoverTilt45ClickSpin270AnimationStrategy.ensureStyles(); + + // Apply the base transition class so the return-to-zero is always animated + this.renderer.addClass(this.elementRef.nativeElement, BASE_CLASS); + + // This animation type is interactive (doesn't auto-complete), so notify immediately + this.notifyComplete(); + } + + public override onMouseEnter(): void { + this._isHovered = true; + + if (this._isClickAnimating) { + // Don't apply tilt while click spin is in progress; it will be restored on animationend + return; + } + + this.renderer.addClass(this.elementRef.nativeElement, HOVER_CLASS); + } + + public override onMouseLeave(): void { + this._isHovered = false; + + if (this._isClickAnimating) { + return; + } + + this.renderer.removeClass(this.elementRef.nativeElement, HOVER_CLASS); + } + + public override onClick(): void { + const el: HTMLElement = this.elementRef.nativeElement; + + // Remove hover tilt so the spin starts cleanly from 0° + this.renderer.removeClass(el, HOVER_CLASS); + + // Remove then re-add click class so repeated clicks restart the animation + this.renderer.removeClass(el, CLICK_CLASS); + + // Force reflow + void el.offsetWidth; + + this._isClickAnimating = true; + this.renderer.addClass(el, CLICK_CLASS); + + // Clean up after animation finishes + this._removeClickListener?.(); + this._removeClickListener = this.renderer.listen(el, "animationend", () => { + this.renderer.removeClass(el, CLICK_CLASS); + this._isClickAnimating = false; + this._removeClickListener?.(); + this._removeClickListener = undefined; + + // Restore hover tilt if the cursor is still over the element + if (this._isHovered) { + this.renderer.addClass(el, HOVER_CLASS); + } + }); + } + + public cleanup(): void { + this._removeClickListener?.(); + this._removeClickListener = undefined; + + const el: HTMLElement = this.elementRef.nativeElement; + this.renderer.removeClass(el, BASE_CLASS); + this.renderer.removeClass(el, HOVER_CLASS); + this.renderer.removeClass(el, CLICK_CLASS); + } + + private static ensureStyles(): void { + const styleId = "animation-effect-hover-tilt-spin-270-styles"; + + if (document.getElementById(styleId)) { + return; + } + + const style = document.createElement("style"); + + style.id = styleId; + style.textContent = ` + /* Smooth transition for hover tilt and return */ + .${BASE_CLASS} { + transition: transform ${HOVER_TRANSITION_MS}ms ease; + } + + /* Hover state — 45° clockwise tilt */ + .${HOVER_CLASS} { + transform: rotate(${HOVER_TILT_DEG}deg); + } + + /* Click spin — 270° clockwise, then snaps back to 0° */ + @keyframes spin-270-return { + 0% { transform: rotate(0deg); } + 65% { transform: rotate(270deg); } + 100% { transform: rotate(0deg); } + } + + .${CLICK_CLASS} { + animation: spin-270-return ${CLICK_SPIN_DURATION_MS}ms ${CLICK_SPIN_EASING} forwards; + /* CSS animation overrides the transition + hover transform */ + transition: none; + } + `; + + document.head.appendChild(style); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/utils.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/utils.ts new file mode 100644 index 0000000..f1f2c72 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/utils.ts @@ -0,0 +1,24 @@ +import { Renderer2 } from "@angular/core"; + +export type StyleMap = Record; + +/** + * Apply multiple styles to an element using the provided renderer + */ +export const applyStyles = (renderer: Renderer2, element: HTMLElement, styles: StyleMap): void => { + Object.entries(styles).forEach(([property, value]) => { + renderer.setStyle(element, property, value); + }); +}; + +/** + * Create an HTML element with a class and styles using the provided renderer + */ +export const createElement = (renderer: Renderer2, tag: string, className: string, styles: StyleMap): HTMLElement => { + const element = renderer.createElement(tag); + + renderer.addClass(element, className); + applyStyles(renderer, element, styles); + + return element; +}; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/enums/loading-state.enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/enums/loading-state.enum.ts new file mode 100644 index 0000000..21f24ec --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/enums/loading-state.enum.ts @@ -0,0 +1,15 @@ +export enum LOADING_STATE { + LOADING = "loading", + SUCCESS = "success", + NOT_STARTED = "not_started", + ERROR = "error", +} + +export enum MESSAGES_LOADING_STATE { + FETCH_MESSAGES_FROM_THREAD_SUCCESS = "FETCH_MESSAGES_FROM_THREAD_SUCCESS", + GET_RESPONSE_SUCCESS = "GET_RESPONSE_SUCCESS", + ERROR = "ERROR", + LOADING_FROM_THREAD = "LOADING_FROM_THREAD", + LOADING_RESPONSE = "LOADING_RESPONSE", + NOT_STARTED = "NOT_STARTED", +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/enums/permission-mode.enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/enums/permission-mode.enum.ts new file mode 100644 index 0000000..a54a0d7 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/enums/permission-mode.enum.ts @@ -0,0 +1,4 @@ +export enum PERMISSION_MODE { + ASK = "ask", + AUTO_ACCEPT = "auto_accept", +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/guards/.gitkeep b/templates/io-assist-angular-editable/src/app/io-assist/shared/guards/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/agent/agent-manager/agent-manager.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/agent/agent-manager/agent-manager.service.ts new file mode 100644 index 0000000..125d0d8 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/agent/agent-manager/agent-manager.service.ts @@ -0,0 +1,24 @@ +import { inject, Injectable } from "@angular/core"; +import { IoAiWeb } from "@interopio/ai-web"; + +import { IOAiWebService } from "../../io-ai-web/io-ai-web.service"; +import { LoggerService } from "../../logger/logger.service"; + +@Injectable({ + providedIn: "root", +}) +export class AgentManagerService { + private readonly _ioIntelWebService: IOAiWebService = inject(IOAiWebService); + private readonly _logger: LoggerService = inject(LoggerService); + protected readonly LOGGER_NAME: string = "AgentManagerService"; + + public async listAgents(): Promise { + try { + return await this._ioIntelWebService.listAgents(); + } catch (error) { + this._logger.get(this.LOGGER_NAME).error(`Error fetching agents: ${error instanceof Error ? error.message : String(error)}`); + + throw new Error("Failed to fetch agents."); + } + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/agent/agent.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/agent/agent.service.ts new file mode 100644 index 0000000..07b2b09 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/agent/agent.service.ts @@ -0,0 +1,139 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { inject, Injectable, Signal } from "@angular/core"; +import { IoAiWeb } from "@interopio/ai-web"; + +import { IO_ASSIST_DYNAMIC_CONFIG } from "../../../io-assist.config"; +import { AgentFacade } from "../../store/agent/agent.facade"; +import { GetResponseParams } from "../../store/message/types"; +import { ThreadFacade } from "../../store/thread/thread.facade"; +import { LoggerService } from "../logger/logger.service"; + +@Injectable({ + providedIn: "root", +}) +export class AgentService { + private readonly _agentFacade: AgentFacade = inject(AgentFacade); + private readonly _theadFacade: ThreadFacade = inject(ThreadFacade); + private readonly _logger: LoggerService = inject(LoggerService); + private readonly _dynamicConfig = inject(IO_ASSIST_DYNAMIC_CONFIG); + protected readonly LOGGER_NAME: string = "AgentService"; + + private readonly _selectedAgent: Signal = this._agentFacade.selectedAgent; + private readonly _activeThreadId: Signal = this._theadFacade.activeThreadId; + + public getResponse(params: GetResponseParams, isStream: boolean = true, agent?: IoAiWeb.Agents.Agent, skipThreadManagement: boolean = false, autoIncludeTools: boolean = true): Promise { + if (!agent) { + agent = this._selectedAgent() ?? undefined; + } + + if (!agent) { + return Promise.reject(new Error("No agent available for response generation")); + } + + const responseParams: GetResponseParams = this.constructParams(params, skipThreadManagement); + + const streamParams = this.toStreamParams(responseParams, autoIncludeTools); + + return isStream ? agent.stream(streamParams) : agent.generate(streamParams); + } + + public getSamplingResponse(params: GetResponseParams, skipThreadManagement: boolean = false): Promise { + return this.getResponse(params, false, undefined, skipThreadManagement, false); + } + + public reloadResponse(params: GetResponseParams, isStream: boolean = true, agent?: IoAiWeb.Agents.Agent, skipThreadManagement: boolean = false): Promise { + return this.getResponse(params, isStream, agent, skipThreadManagement); + } + + public getSelectedAgentModelId(): string { + return this._selectedAgent()?.modelId ?? "unknown"; + } + + public abortOperation(threadId: string): void { + const selectedAgent: IoAiWeb.Agents.Agent | null = this._selectedAgent(); + + if (!selectedAgent) { + throw new Error("No agent available to abort operation"); + } + + selectedAgent.abortOperation(threadId); + } + + private toStreamParams(params: GetResponseParams, autoIncludeTools = true): IoAiWeb.Agents.StreamParams { + const messages: IoAiWeb.Agents.AgentMessage[] = Array.isArray(params.messages) + ? params.messages.map((msg) => ({ + id: msg.id, + role: msg.role as IoAiWeb.Agents.AgentMessage["role"], + content: msg.content ?? "", + })) + : []; + + return { + messages, + memory: params.memory, + resourceId: params.memory?.resource, + ...(params.structuredOutput ? { structuredOutput: params.structuredOutput } : {}), + tools: { autoIncludeEnabled: autoIncludeTools }, + }; + } + + private constructParams(params: GetResponseParams, skipThreadManagement: boolean): GetResponseParams { + // Filter out assistant messages with empty/whitespace-only content + // This prevents "text content blocks must be non-empty" errors from Anthropic + const filteredMessages = Array.isArray(params.messages) + ? params.messages.filter((msg) => { + // Keep all non-assistant messages + if (msg.role !== "assistant") { + return true; + } + + // For assistant messages, only keep if content is non-empty + const content = msg.content; + return content && typeof content === "string" && content.trim() !== ""; + }) + : params.messages; + + const filteredParams = { ...params, messages: filteredMessages }; + + const currentUser: string = this._dynamicConfig().user.id; + const activeThreadId: string | null = this._activeThreadId(); + + if (skipThreadManagement) { + // Sampling and other skip-thread-management requests must NOT inherit + // the active thread — doing so would pollute the main conversation + // history with unrelated messages (see db-problem.md, Problem 3). + // Only use memory if the caller explicitly provides both thread and resource. + const thread = filteredParams.memory?.thread ?? undefined; + const resource = filteredParams.memory?.resource ?? undefined; + + return { + ...filteredParams, + ...(thread && resource ? { memory: { thread, resource } } : {}), + }; + } + + if (!activeThreadId) { + this._logger.get(this.LOGGER_NAME).info("No active thread ID found, generating a new one."); + + const newThreadId = crypto.randomUUID(); + + this._theadFacade.dispatchChangeActiveThread(newThreadId); + + return { + ...filteredParams, + memory: { + thread: newThreadId, + resource: currentUser, + }, + }; + } + + return { + ...filteredParams, + memory: { + thread: activeThreadId, + resource: currentUser, + }, + }; + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/component-effect-manager/component-effect-manager.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/component-effect-manager/component-effect-manager.service.ts new file mode 100644 index 0000000..fc75f52 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/component-effect-manager/component-effect-manager.service.ts @@ -0,0 +1,168 @@ +import { DestroyRef, effect, EffectRef, Inject, inject, Injectable, InjectionToken, Injector, signal, WritableSignal } from "@angular/core"; +import { IOConnectCore } from "@interopio/core"; + +import { LoggerService } from "../logger/logger.service"; + +export type NamedEffect = { + name: string; + effectRef: EffectRef; +}; + +const COMPONENT_NAME = new InjectionToken("COMPONENT_NAME"); + +/** + * ComponentEffectManagerService + * + * A centralized service for managing Angular effects within a component lifecycle. + * This service provides a structured approach to registering, tracking, and cleaning up + * effects, ensuring proper memory management and debugging capabilities. + * + * Purpose: + * - Register named effects with descriptive identifiers for easy debugging + * - Automatically track all active effects per component instance + * - Ensure proper cleanup on component destruction to prevent memory leaks + * - Provide console logging for effect registration and destruction during development + * + * Usage Pattern: + * 1. Provide the service in the component's providers array with a factory function + * that passes the component name + * 2. Inject the service in the component + * 3. Call registerEffects() method in ngOnInit() or constructor + * 4. Use descriptive naming convention for effect names: 'ComponentName.action.on.trigger' + * + * Example: + * ```typescript + * @Component({ + * selector: 'my-component', + * providers: [ + * { + * provide: ComponentEffectManagerService, + * useFactory: () => new ComponentEffectManagerService('MyComponent'), + * }, + * ], + * }) + * export class MyComponent implements OnInit { + * // Provided in current component + * private readonly _effectService = inject(ComponentEffectManagerService); + * + * public ngOnInit(): void { + * this.registerEffects(); + * } + * + * private registerEffects(): void { + * this._effectService.registerEffect( + * 'MyComponent.updateTitle.on.nameChange', + * () => this.name() && this.title.set(this.name()) + * ); + * } + * } + * ``` + * + * Effect Naming Convention: + * Format: 'ComponentName.action.on.trigger' + * - ComponentName: The full component class name (e.g., 'LoginComponent', 'InputAreaComponent') + * - action: What the effect does (e.g., 'clearError', 'focusInput', 'updateTitle') + * - trigger: What signal/state change triggers it (e.g., 'passwordChange', 'isEditModeEnabled') + * + * Examples: + * - 'LoginComponent.clearError.on.passwordChange' + * - 'InputAreaComponent.updateValue.on.selectedPrompt' + * - 'ThreadHistoryListItemComponent.focusInput.on.isEditModeEnabled' + * + * Benefits: + * - Centralized effect management prevents scattered effect() calls throughout components + * - Named effects make debugging easier by providing clear identifiers in logs + * - Automatic cleanup prevents memory leaks + * - Consistent pattern across all components improves maintainability + * - Easy to track which effects are active for a given component instance + */ +@Injectable() +export class ComponentEffectManagerService { + private _injector: Injector = inject(Injector); + private _namedEffects: WritableSignal = signal(null); + private readonly _destroyRef: DestroyRef = inject(DestroyRef); + private readonly _logger: LoggerService = inject(LoggerService); + protected readonly LOGGER_NAME: string = "ComponentEffectManagerService"; + + constructor( + // eslint-disable-next-line @angular-eslint/prefer-inject + @Inject(COMPONENT_NAME) public componentNameInjected: string + ) { + if (!this.componentNameInjected || this.componentNameInjected.trim() === "") { + throw new Error("ComponentEffectManagerService requires a valid component name to be provided."); + } + + this.registerEffect(`${this.componentNameInjected}.namedEffectsListChange`, () => { + const effects = this._namedEffects(); + + if (!effects || effects.length === 0) return; + }); + + this._destroyRef.onDestroy(() => { + this.destroyAllEffects(); + }); + } + + public registerEffect(name: string, effectFn: () => void): void { + if (!name || name.trim() === "") { + this.getLogger().warn("Effect name must be a non-empty string."); + + return; + } + + const currentEffects: NamedEffect[] = this._namedEffects() ?? []; + + if (currentEffects.find((e) => e.name === name)) { + this.getLogger().warn(`Effect with name "${name}" is already running.`); + + return; + } + + // runs the effect + const effectRef: EffectRef = effect( + () => { + effectFn(); + }, + { injector: this._injector } + ); + + const namedEffect: NamedEffect = { + name, + effectRef, + }; + + this._namedEffects.set([...currentEffects, namedEffect]); + } + + public destroyEffect(name: string): void { + const currentEffects: NamedEffect[] = this._namedEffects() ?? []; + + const effectToDestroy: NamedEffect | undefined = currentEffects.find((e) => e.name === name); + + if (!effectToDestroy) { + this.getLogger().warn(`No effect found with name "${name}" to destroy.`); + + return; + } + + effectToDestroy.effectRef.destroy(); + + const updatedEffects: NamedEffect[] = currentEffects.filter((e) => e.name !== name); + + this._namedEffects.set(updatedEffects.length > 0 ? updatedEffects : null); + } + + public destroyAllEffects(): void { + const effects: NamedEffect[] | null = this._namedEffects(); + + if (!effects || effects.length === 0) return; + + effects.forEach((namedEffect) => namedEffect.effectRef.destroy()); + + this._namedEffects.set(null); + } + + private getLogger(): IOConnectCore.Logger.API { + return this._logger.get(this.LOGGER_NAME); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/copy-to-clipboard/copy-to-clipboard.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/copy-to-clipboard/copy-to-clipboard.service.ts new file mode 100644 index 0000000..c62cd9c --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/copy-to-clipboard/copy-to-clipboard.service.ts @@ -0,0 +1,50 @@ +import { inject, Injectable } from "@angular/core"; + +import { LoggerService } from "../logger/logger.service"; + +@Injectable() +export class CopyToClipboardService { + private readonly _logger: LoggerService = inject(LoggerService); + protected readonly LOGGER_NAME: string = "CopyToClipboardService"; + public copyToClipboard = async (text: string): Promise => { + if (!navigator.clipboard) throw new Error("Clipboard API not available"); + + if (!navigator.clipboard.writeText) throw new Error("Clipboard writeText method not available"); + + try { + await navigator.clipboard.writeText(text); + + this._logger.get(this.LOGGER_NAME).info("Text copied to clipboard successfully"); + } catch (err) { + this._logger.get(this.LOGGER_NAME).warn(`Failed to copy text using Clipboard API, falling back to textarea method: ${err instanceof Error ? err.message : String(err)}`); + + /** + * Fallback when the Clipboard API is not available or fails + * Reasons that it might fail include: + * - The user denied permission + * - The page is not served over HTTPS + * - The browser doesn't support it or app is rendered in an iframe + */ + const textarea = document.createElement("textarea"); + + textarea.value = text; // Set the text to be copied + + // Other styling to avoid flash of textarea, moving viewport or tabbing into it + textarea.style.position = "fixed"; + textarea.style.opacity = "0"; + textarea.tabIndex = -1; + textarea.style.left = "-9999px"; + + document.body.appendChild(textarea); + + textarea.focus(); + textarea.select(); + + // Copy the text + document.execCommand("copy"); + + // Clean up + document.body.removeChild(textarea); + } + }; +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/elicitation/elicitation.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/elicitation/elicitation.service.ts new file mode 100644 index 0000000..0a618d4 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/elicitation/elicitation.service.ts @@ -0,0 +1,251 @@ +import { inject, Injectable } from "@angular/core"; +import { IoAiWeb } from "@interopio/ai-web"; +import { IOConnectCore } from "@interopio/core"; +import { IOConnectModals } from "@interopio/modals-api"; +import { take } from "rxjs/operators"; + +import { ELICITATION_ACTION } from "./enums"; +import { validateElicitationRequest } from "./utils"; +import { APP_BUTTON_TYPES } from "../../../components/app-button/enum"; +import { PANEL_BUTTON_ACTION_TYPE } from "../../../components/app-panel/enums"; +import { APP_SELECT_IDS, AppSelectService } from "../../../components/app-select/app-select.service"; +import { UI_STRINGS } from "../../../constants/ui-strings"; +import { PERMISSION_MODE } from "../../../enums/permission-mode.enum"; +import { ThreadFacade } from "../../../store/thread/thread.facade"; +import { IOConnectService } from "../../io/io.service"; +import { LoggerService } from "../../logger/logger.service"; +import { OverlayService } from "../../overlay/overlay.service"; + +/** + * Service responsible for handling elicitation requests from the IoAiWeb API. + * + * Instructions to keep in mind when developing on how elicitation is handled in IO Assist: + * + * - When an elicitation request is received, it is first validated using `validateElicitationRequest`. + * - An elicitation request is considered valid based on set of checks - currently, it checks if the request is intended for io-tool. + * - If the request is VALID, a modal dialog is presented to the user asking for their approval to proceed with the elicitation. + * - The user has three options in the modal dialog: + * - ACCEPT: The user agrees to provide the requested information. + * - We are currently using dummy data for the elicited information. In the future, this should be replaced with actual user input handling + * TODO: Future Task - Replace dummy data with actual user input handling - https://interopio.atlassian.net/browse/IOINTEL-160 + * - DECLINE: The user refuses to provide the requested information. A rejection response is sent back. + * - CANCEL: The user cancels the elicitation request. A cancel response is sent back. + * - If the request is INVALID or if any error occurs during the process, the request is automatically canceled. + * + */ +@Injectable({ + providedIn: "root", +}) +export class ElicitationService { + private readonly UI_STRINGS = UI_STRINGS.ELICITATION_MODAL; + private readonly _ioConnectService: IOConnectService = inject(IOConnectService); + private readonly _appSelectService: AppSelectService = inject(AppSelectService); + private readonly _overlayService: OverlayService = inject(OverlayService); + private readonly _threadFacade: ThreadFacade = inject(ThreadFacade); + private readonly _logger: LoggerService = inject(LoggerService); + protected readonly LOGGER_NAME: string = "ElicitationService"; + private _isElicitationModalActive = false; + + public selectElicitationHandler(mcp: IoAiWeb.WebConfig["mcp"]): (serverName: string, params: IoAiWeb.ElicitationRequestParams) => Promise { + if (this.isCustomElicitationHandlerProvided(mcp)) { + this._logger.get(this.LOGGER_NAME).warn("Using a custom elicitation handler provided by user configuration."); + + return mcp!.clientsConfig!.capabilities!.elicitation!.handler!; + } + + return this.handleElicitationRequest.bind(this); + } + + public async handleElicitationRequest(serverName: string, request: IoAiWeb.ElicitationRequestParams): Promise { + const isAutoAccept = this._appSelectService.getSelected(APP_SELECT_IDS.PERMISSION)()?.value === PERMISSION_MODE.AUTO_ACCEPT; + + if (isAutoAccept) { + this.getLogger().info("Auto-accepting elicitation request."); + return this.handleResponseType(ELICITATION_ACTION.ACCEPT); + } + + const requestThreadId: string | undefined = request._meta?.["threadId"]; + const activeThreadId: string | null = this._threadFacade.activeThreadId(); + const isFromBackgroundThread: boolean = Boolean(requestThreadId && activeThreadId !== requestThreadId); + + if (isFromBackgroundThread) { + this.getLogger().warn(`Elicitation request rejected: active thread (${activeThreadId}) does not match request thread (${requestThreadId})`); + + return this.handleResponseType(ELICITATION_ACTION.CANCEL, undefined, `Elicitation request rejected: User was not on the thread that the elicitation request was intended for.`); + } + + const isRequestValid: boolean = validateElicitationRequest(request, this.getLogger()); + + if (!isRequestValid) { + return this.handleResponseType(ELICITATION_ACTION.CANCEL, undefined, "Elicitation request validation failed."); + } + + if (this._isElicitationModalActive) { + return this.handleResponseType(ELICITATION_ACTION.CANCEL, undefined, "Elicitation request rejected: another elicitation is already in progress."); + } + + this._isElicitationModalActive = true; + + try { + return await this.getElicitationModalResponse(serverName, request); + } catch (error) { + const errorMessage: string = "Error occurred while getting elicitation modal response. Request canceled. " + (error instanceof Error ? error.message : String(error)); + + return this.handleResponseType(ELICITATION_ACTION.CANCEL, undefined, errorMessage); + } finally { + this._isElicitationModalActive = false; + } + } + + private async getElicitationModalResponse(serverName: string, request: IoAiWeb.ElicitationRequestParams): Promise { + this.getLogger().info(`Showing elicitation request from server: ${serverName}, requesting: ${JSON.stringify(request)}`); + + const isModalsApiAvailable: boolean = await this._ioConnectService.isModalsAvailable(); + + if (!isModalsApiAvailable) { + this.getLogger().info("IO Modals not available, using built-in panel for elicitation"); + + return await this.getBuiltInPanelResponse(request); + } + + return await this.getModalsApiPanelResponse(request); + } + + private async getBuiltInPanelResponse(request: IoAiWeb.ElicitationRequestParams): Promise { + return new Promise((resolve) => { + const message = request.message ?? this.UI_STRINGS.FALLBACK_TEXT; + + const { overlayRef } = this._overlayService.showPanelOverlay({ + title: this.UI_STRINGS.TITLE, + content: `
+ Permission to proceed + ${message} +
`, + isHeaderCloseButtonDisplayed: false, + footerButtons: [ + { + label: "Accept", + action: PANEL_BUTTON_ACTION_TYPE.CUSTOM, + type: APP_BUTTON_TYPES.SUBMIT, + onClick: () => this.resolveBuiltInPanel(resolve, ELICITATION_ACTION.ACCEPT), + }, + { + label: "Decline", + action: PANEL_BUTTON_ACTION_TYPE.CUSTOM, + type: APP_BUTTON_TYPES.DEFAULT, + onClick: () => this.resolveBuiltInPanel(resolve, ELICITATION_ACTION.DECLINE), + }, + { + label: "Cancel", + action: PANEL_BUTTON_ACTION_TYPE.CUSTOM, + type: APP_BUTTON_TYPES.DEFAULT, + onClick: () => this.resolveBuiltInPanel(resolve, ELICITATION_ACTION.CANCEL), + }, + ], + }); + + // The overlay service disposes the overlay on backdrop click; + // subscribe here so the pending promise resolves as CANCEL instead of hanging. + overlayRef + .backdropClick() + .pipe(take(1)) + .subscribe(() => this.resolveBuiltInPanel(resolve, ELICITATION_ACTION.CANCEL)); + }); + } + + private async getModalsApiPanelResponse(request: IoAiWeb.ElicitationRequestParams): Promise { + const dialogOptions: IOConnectModals.DialogRequestConfig = { + templateName: "noInputsConfirmationDialog", + variables: { + title: this.UI_STRINGS.TITLE, + heading: this.UI_STRINGS.HEADING, + text: request.message ?? this.UI_STRINGS.FALLBACK_TEXT, + actionButtons: [ + { variant: "primary", text: "Accept", id: ELICITATION_ACTION.ACCEPT }, + { variant: "outline", text: "Decline", id: ELICITATION_ACTION.DECLINE }, + { variant: "outline", text: "Cancel", id: ELICITATION_ACTION.CANCEL }, + ], + }, + }; + + const elicitationDialogResponse: IOConnectModals.DialogResponse = await this._ioConnectService.requestModalDialog(dialogOptions); + + if (!elicitationDialogResponse || !elicitationDialogResponse.responseButtonClicked) { + throw new Error("No response from elicitation modal dialog."); + } + + switch (elicitationDialogResponse.responseButtonClicked.id) { + case ELICITATION_ACTION.ACCEPT: + return this.handleResponseType(ELICITATION_ACTION.ACCEPT); + case ELICITATION_ACTION.DECLINE: + return this.handleResponseType(ELICITATION_ACTION.DECLINE); + case ELICITATION_ACTION.CANCEL: + return this.handleResponseType(ELICITATION_ACTION.CANCEL); + default: + // At this point probably a timeout has occurred + return this.handleResponseType(ELICITATION_ACTION.CANCEL, undefined, "Elicitation dialog was closed without a response."); + } + } + + private resolveBuiltInPanel(resolve: (value: IoAiWeb.ElicitationResponse) => void, action: ELICITATION_ACTION): void { + this._overlayService.closeCurrentOverlay(); + resolve(this.handleResponseType(action)); + } + + // TODO: Future Task - Replace dummy data with actual user input handling + private handleResponseType(type: ELICITATION_ACTION, onBuiltInPanelResponse?: (value: IoAiWeb.ElicitationResponse) => void, logMessage?: string): IoAiWeb.ElicitationResponse { + if (onBuiltInPanelResponse) { + this._overlayService.closeCurrentOverlay(); + } + + let response: IoAiWeb.ElicitationResponse | undefined; + + if (type === ELICITATION_ACTION.ACCEPT) { + this.getLogger().info(logMessage ?? "User accepted the elicitation request."); + + response = { + action: ELICITATION_ACTION.ACCEPT, + // Our internal io_connect tools do not expect any content back and we do not support external elicitation requests yet + content: {}, + }; + } + + if (type === ELICITATION_ACTION.DECLINE) { + this._logger.get(this.LOGGER_NAME).info(logMessage ?? "User declined the elicitation request."); + response = { + action: ELICITATION_ACTION.DECLINE, + }; + } + + if (type === ELICITATION_ACTION.CANCEL) { + this._logger.get(this.LOGGER_NAME).info(logMessage ?? "User canceled the elicitation request."); + response = { + action: ELICITATION_ACTION.CANCEL, + }; + } + + if (!response) { + throw new Error("No response generated for the given elicitation action type."); + } + + if (onBuiltInPanelResponse) { + onBuiltInPanelResponse(response); + } + + return response; + } + + private isCustomElicitationHandlerProvided(mcp: IoAiWeb.WebConfig["mcp"]): boolean { + const condition: boolean = !!mcp?.clientsConfig?.capabilities?.elicitation?.handler; + + if (!condition) { + this.getLogger().warn("No custom elicitation handler provided. Using built-in handler."); + } + + return condition; + } + + private getLogger(): IOConnectCore.Logger.API { + return this._logger.get(this.LOGGER_NAME); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/elicitation/enums.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/elicitation/enums.ts new file mode 100644 index 0000000..4269678 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/elicitation/enums.ts @@ -0,0 +1,5 @@ +export enum ELICITATION_ACTION { + ACCEPT = "accept", + DECLINE = "decline", + CANCEL = "cancel", +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/elicitation/utils.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/elicitation/utils.ts new file mode 100644 index 0000000..82cdb1c --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/elicitation/utils.ts @@ -0,0 +1,44 @@ +import { IoAiWeb } from "@interopio/ai-web"; +import { IOConnectCore } from "@interopio/core"; + +export const validateElicitationRequest = (request: IoAiWeb.ElicitationRequestParams, logger: IOConnectCore.Logger.API): boolean => { + // Basic meta tags validation of the elicitation request + if (!isElicitRequestForIOToolResult(request._meta, logger)) { + return false; + } + + // Other validations can be added here as needed + // if (...) { + // return false; + // } + + return true; +}; + +const isElicitRequestForIOToolResult = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + _meta: Record | undefined, + logger: IOConnectCore.Logger.API +): boolean => { + if (!_meta || !_meta["toolName"]) { + logger.warn("Elicitation request is missing required _meta.toolName field."); + + return false; + } + + const toolName: string = _meta["toolName"]; + + if (typeof toolName !== "string" || toolName.trim().length === 0) { + logger.warn("Elicitation request has invalid _meta.toolName field."); + + return false; + } + + if (!toolName.startsWith("io_connect")) { + logger.warn("Elicitation request is not intended for io_connect."); + + return false; + } + + return true; +}; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/io-ai-web.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/io-ai-web.service.ts new file mode 100644 index 0000000..5d420c1 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/io-ai-web.service.ts @@ -0,0 +1,209 @@ +// Re-enable the directive below if the `(window as any).intel` debug line is restored. +// /* eslint-disable @typescript-eslint/no-explicit-any */ +import { computed, inject, Injectable, Signal, signal, WritableSignal } from "@angular/core"; +import { IoAiWeb } from "@interopio/ai-web"; +import { IoIntelWorkingContext } from "@interopio/working-context"; + +import { ElicitationService } from "./elicitation/elicitation.service"; +import { SamplingService } from "./sampling/sampling.service"; +import { IO_ASSIST_CONFIG, IO_ASSIST_DYNAMIC_CONFIG, IoAssistStaticConfig, IoAssistDynamicConfig } from "../../../io-assist.config"; +import { LOADING_STATE } from "../../enums/loading-state.enum"; +import { LoadingType } from "../../types/loading.type"; +import { IOConnectService } from "../io/io.service"; +import { LoggerService } from "../logger/logger.service"; +import { McpAppsService } from "../mcp-apps/mcp-apps.service"; + +@Injectable({ + providedIn: "root", +}) +export class IOAiWebService { + private readonly _ioConnectService: IOConnectService = inject(IOConnectService); + private readonly _samplingService: SamplingService = inject(SamplingService); + private readonly _elicitationService: ElicitationService = inject(ElicitationService); + private readonly _ioAssistConfig: IoAssistStaticConfig = inject(IO_ASSIST_CONFIG); + private readonly _dynamicConfig: Signal = inject(IO_ASSIST_DYNAMIC_CONFIG); + private readonly _logger: LoggerService = inject(LoggerService); + private readonly LOGGER_NAME: string = "IOAiWebService"; + private readonly _mcpAppsService: McpAppsService = inject(McpAppsService); + + private _ioIntelWeb: WritableSignal = signal(null); + private get _ioIntelWebAPI(): IoAiWeb.API { + const api: IoAiWeb.API | null = this._ioIntelWeb(); + + if (!api) { + throw new Error("IO Intel Web API is not initialized."); + } + + return api; + } + + private get _workingContextAPI(): IoIntelWorkingContext.API { + const api: IoAiWeb.API = this._ioIntelWebAPI; + + if (!api.context) { + throw new Error("IO Intel Web API does not have working context enabled."); + } + + return api.context; + } + + private _loadingState: WritableSignal = signal({ + type: LOADING_STATE.NOT_STARTED, + }); + public get loadingState(): Signal { + return this._loadingState; + } + public get isInitialized(): Signal { + return computed(() => this._ioIntelWeb() !== null && this._loadingState().type === LOADING_STATE.SUCCESS); + } + public get isInitializing(): Signal { + return computed(() => this._loadingState().type === LOADING_STATE.LOADING); + } + public readonly isError: Signal = computed(() => { + const loadingState = this._loadingState(); + + return (loadingState.type === LOADING_STATE.ERROR && loadingState.message) || ""; + }); + + private _hasContextConfig: WritableSignal = signal(false); + + /** Note that initialization takes time and must be handled accordingly across components - display proper UI loading indication. + * Initialization takes place when the app is mounted. + */ + public async initialize(): Promise { + const ioIntelWebAPI: IoAiWeb.API | null = await this._safeInitialize(); + + this._logger.get(this.LOGGER_NAME).debug(`IO Intel Web API initialized: ${JSON.stringify(ioIntelWebAPI)}`); + + if (ioIntelWebAPI) { + // Debug-only global, never read by app/lib code. Commented out to verify + // nothing depends on it. + // (window as any).intel = ioIntelWebAPI; // For debugging purposes + + this._ioIntelWeb.set(ioIntelWebAPI); + this._loadingState.set({ type: LOADING_STATE.SUCCESS }); + + this._mcpAppsService.attach(ioIntelWebAPI); + + return; + } + + const error: Error = new Error("Failed to initialize IoAiWeb API"); + + this._loadingState.set({ + type: LOADING_STATE.ERROR, + message: error.message, + }); + + throw error; + } + + private async _safeInitialize(): Promise { + if (this.isInitialized()) throw new Error("IO Intel Web is already initialized!"); + + if (this.isInitializing()) throw new Error("IO Intel Web is already initializing..."); + + if (this.isError()) { + throw new Error(this.isError()); + } + + this._loadingState.set({ type: LOADING_STATE.LOADING }); + + const config: IoAiWeb.WebConfig = this.constructConfig(); + + this._logger.get(this.LOGGER_NAME).debug(`Initializing IoAiWeb with config: ${JSON.stringify(config)}`); + + try { + return await this._ioConnectService.initializeIOIntelWeb(config); + } catch (error) { + const err = new Error("Error initializing IO Intel Web API: " + (error instanceof Error ? error.message : String(error))); + + this._loadingState.set({ + type: LOADING_STATE.ERROR, + message: err.message, + }); + + throw err; + } + } + + public listAgents(): Promise { + return this._ioIntelWebAPI.agents.list(); + } + + public listThreads(config: IoAiWeb.Threads.ListThreadsParams): Promise { + return this._ioIntelWebAPI.threads.list(config); + } + + public deleteThreadState(params: IoAiWeb.Threads.DeleteThreadStateParams): Promise { + return this._ioIntelWebAPI.threads.deleteThreadState(params); + } + + public listTools(): Promise { + return this._ioIntelWebAPI.tools.list(); + } + + public toggleTool(toolName: string, enable: boolean): Promise { + return Promise.resolve(this._ioIntelWebAPI.tools.toggleTool(toolName, enable)); + } + + public getWorkingContext(): Promise> { + return Promise.resolve(this._workingContextAPI.get()); + } + + public onWorkingContextChange(callback: (data: Record) => void): IoIntelWorkingContext.UnsubscribeFunction { + return this._workingContextAPI.onChanged(callback); + } + + public isWorkingContextEnabled(): Promise { + return Promise.resolve(this._hasContextConfig() && !!this._workingContextAPI); + } + + private constructConfig(): IoAiWeb.WebConfig { + const aiWebConfig = this._ioAssistConfig.aiWebConfig; + const contextConfig = this._ioAssistConfig.workingContext; + + const config: IoAiWeb.WebConfig = { + agentServer: this.buildAgentServerConfig(aiWebConfig.agentServer), + }; + + config.mcp = this.buildMcpConfig(aiWebConfig.mcp); + + if (contextConfig) { + this._hasContextConfig.set(true); + + config.context = contextConfig; + } + + return config; + } + + private buildAgentServerConfig(agentServer: IoAiWeb.WebConfig["agentServer"]): IoAiWeb.WebConfig["agentServer"] { + const dynamicHeaders = this._dynamicConfig().agentServer?.headers; + const result = { ...agentServer }; + + if (dynamicHeaders !== undefined) { + result.headers = dynamicHeaders; + } + + return result; + } + + private buildMcpConfig(mcp: IoAiWeb.WebConfig["mcp"]): IoAiWeb.WebConfig["mcp"] { + return { + ...mcp, + clientsConfig: { + ...mcp?.clientsConfig, + capabilities: { + ...mcp?.clientsConfig?.capabilities, + sampling: { + handler: this._samplingService.selectSamplingHandler(mcp), + }, + elicitation: { + handler: this._elicitationService.selectElicitationHandler(mcp), + }, + }, + }, + }; + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/sampling/enums.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/sampling/enums.ts new file mode 100644 index 0000000..c8509d3 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/sampling/enums.ts @@ -0,0 +1,4 @@ +export enum SAMPLING_ACTION { + ACCEPT = "accept", + DECLINE = "decline", +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/sampling/sampling.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/sampling/sampling.service.ts new file mode 100644 index 0000000..fc20fe0 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/sampling/sampling.service.ts @@ -0,0 +1,285 @@ +import { inject, Injectable } from "@angular/core"; +import { IoAiWeb } from "@interopio/ai-web"; +import { IOConnectCore } from "@interopio/core"; +import { IOConnectModals } from "@interopio/modals-api"; +import { take } from "rxjs/operators"; + +import { SAMPLING_ACTION } from "./enums"; +import { APP_BUTTON_TYPES } from "../../../components/app-button/enum"; +import { PANEL_BUTTON_ACTION_TYPE } from "../../../components/app-panel/enums"; +import { APP_SELECT_IDS, AppSelectService } from "../../../components/app-select/app-select.service"; +import { UI_STRINGS } from "../../../constants/ui-strings"; +import { PERMISSION_MODE } from "../../../enums/permission-mode.enum"; +import { GetResponseParams } from "../../../store/message/types"; +import { ThreadFacade } from "../../../store/thread/thread.facade"; +import { AgentService } from "../../agent/agent.service"; +import { IOConnectService } from "../../io/io.service"; +import { LoggerService } from "../../logger/logger.service"; +import { OverlayService } from "../../overlay/overlay.service"; +import { SAMPLING_STOP_REASONS } from "../types"; + +@Injectable({ + providedIn: "root", +}) +export class SamplingService { + private readonly UI_STRINGS = UI_STRINGS.SAMPLING_MODAL; + private readonly _ioConnectService: IOConnectService = inject(IOConnectService); + private readonly _agentService: AgentService = inject(AgentService); + private readonly _appSelectService: AppSelectService = inject(AppSelectService); + private readonly _overlayService: OverlayService = inject(OverlayService); + private readonly _threadFacade: ThreadFacade = inject(ThreadFacade); + private readonly _logger: LoggerService = inject(LoggerService); + protected readonly LOGGER_NAME: string = "SamplingService"; + private _isSamplingModalActive = false; + + public async handleSamplingRequest(serverName: string, request: IoAiWeb.SamplingRequestParams): Promise { + const isAutoAccept = this._appSelectService.getSelected(APP_SELECT_IDS.PERMISSION)()?.value === PERMISSION_MODE.AUTO_ACCEPT; + + if (isAutoAccept) { + this._logger.get(this.LOGGER_NAME).info("Auto-accepting sampling request."); + return await this.getSamplingResponse(request); + } + + const requestThreadId: string | undefined = request._meta?.["threadId"]; + const activeThreadId: string | null = this._threadFacade.activeThreadId(); + const isFromBackgroundThread: boolean = Boolean(requestThreadId && activeThreadId !== requestThreadId); + + if (isFromBackgroundThread) { + this._logger.get(this.LOGGER_NAME).warn(`Sampling request rejected: active thread (${activeThreadId}) does not match request thread (${requestThreadId})`); + + return { + code: -1, + message: `Sampling request rejected: User was not on the thread that the request was intended for.`, + }; + } + + if (this._isSamplingModalActive) { + this._logger.get(this.LOGGER_NAME).warn("Sampling request rejected: another sampling is already in progress."); + + return { + code: -1, + message: "Sampling request rejected: another sampling is already in progress.", + }; + } + + this._isSamplingModalActive = true; + + try { + return await this.getSamplingModalResponse(serverName, request); + } catch (error) { + this._logger.get(this.LOGGER_NAME).error(`Error occurred while getting sampling modal response: ${error instanceof Error ? error.message : String(error)}`); + + return { + code: -1, + message: "An error occurred while processing the sampling request.", + }; + } finally { + this._isSamplingModalActive = false; + } + } + + public selectSamplingHandler( + mcp: IoAiWeb.WebConfig["mcp"] + ): (serverName: string, params: IoAiWeb.SamplingRequestParams) => Promise { + if (this.isCustomSamplingHandlerProvided(mcp)) { + this._logger.get(this.LOGGER_NAME).warn("Using a custom sampling handler provided by user configuration."); + + return mcp!.clientsConfig!.capabilities!.sampling!.handler!; + } + + return this.handleSamplingRequest.bind(this); + } + + private async getSamplingModalResponse(serverName: string, request: IoAiWeb.SamplingRequestParams): Promise { + this.getLogger().info(`Showing sampling request from server: ${serverName}, requesting: ${JSON.stringify(request)}`); + + const isModalsApiAvailable: boolean = await this._ioConnectService.isModalsAvailable(); + + if (!isModalsApiAvailable) { + this.getLogger().info("IO Modals not available, using built-in panel for sampling"); + + return await this.getBuiltInPanelResponse(request); + } + + return await this.getModalsApiPanelResponse(request); + } + + private async getBuiltInPanelResponse(request: IoAiWeb.SamplingRequestParams): Promise { + return new Promise((resolve) => { + const { overlayRef } = this._overlayService.showPanelOverlay({ + title: this.UI_STRINGS.TITLE, + content: `
+ Permission to proceed + ${this.UI_STRINGS.TEXT} +
`, + isHeaderCloseButtonDisplayed: false, + footerButtons: [ + { + label: "Continue", + action: PANEL_BUTTON_ACTION_TYPE.CUSTOM, + type: APP_BUTTON_TYPES.SUBMIT, + onClick: () => this.resolveBuiltInPanel(resolve, SAMPLING_ACTION.ACCEPT, request), + }, + { + label: "Cancel", + action: PANEL_BUTTON_ACTION_TYPE.CUSTOM, + type: APP_BUTTON_TYPES.DEFAULT, + onClick: () => this.resolveBuiltInPanel(resolve, SAMPLING_ACTION.DECLINE, request), + }, + ], + }); + + // The overlay service disposes the overlay on backdrop click; + // subscribe here so the pending promise resolves as CANCEL instead of hanging. + overlayRef + .backdropClick() + .pipe(take(1)) + .subscribe(() => this.resolveBuiltInPanel(resolve, SAMPLING_ACTION.DECLINE, request)); + }); + } + + private async getModalsApiPanelResponse(request: IoAiWeb.SamplingRequestParams): Promise { + const dialogOptions: IOConnectModals.DialogRequestConfig = { + templateName: "noInputsConfirmationDialog", + variables: { + title: this.UI_STRINGS.TITLE, + heading: this.UI_STRINGS.HEADING, + text: this.UI_STRINGS.TEXT, + actionButtons: [ + { variant: "primary", text: "Continue", id: "yes" }, + { variant: "outline", text: "Cancel", id: "no" }, + ], + }, + }; + + const samplingDialogResponse: IOConnectModals.DialogResponse = await this._ioConnectService.requestModalDialog(dialogOptions); + + if (!samplingDialogResponse || !samplingDialogResponse.responseButtonClicked) { + throw new Error("No response from sampling modal dialog."); + } + + if (samplingDialogResponse.responseButtonClicked?.id === "yes") { + return await this.getSamplingResponse(request); + } + + this._logger.get(this.LOGGER_NAME).info("User denied the sampling request."); + + return { + code: -1, + message: "User denied the sampling request.", + }; + } + + private async resolveBuiltInPanel( + resolve: (value: IoAiWeb.SamplingSuccessResponse | IoAiWeb.SamplingErrorResponse) => void, + action: SAMPLING_ACTION, + request: IoAiWeb.SamplingRequestParams + ): Promise { + this._overlayService.closeCurrentOverlay(); + resolve(await this.handleResponseType(action, request)); + } + + private async handleResponseType(type: SAMPLING_ACTION, request: IoAiWeb.SamplingRequestParams): Promise { + let response: IoAiWeb.SamplingSuccessResponse | IoAiWeb.SamplingErrorResponse = { + code: -1, + message: "Unknown sampling action type.", + }; + + if (type === SAMPLING_ACTION.ACCEPT) { + this._logger.get(this.LOGGER_NAME).info("User approved the sampling request."); + + response = await this.getSamplingResponse(request); + } + + if (type === SAMPLING_ACTION.DECLINE) { + this._logger.get(this.LOGGER_NAME).info("User denied the sampling request."); + + response = { + code: -1, + message: "User denied the sampling request.", + }; + } + + return response; + } + + // TODO: + // - Handle different content types properly (not just text) - currently only text content is handled in our UI + private async getSamplingResponse(request: IoAiWeb.SamplingRequestParams): Promise { + const formattedMessages = request.messages.map((element: { role: IoAiWeb.SamplingRole; content: IoAiWeb.SamplingContent }) => { + return { + id: crypto.randomUUID(), + role: element.role, + content: (element.content as IoAiWeb.SamplingTextContent).text, + }; + }); + + // TODO: Add system message as first message in the array + if (request.systemPrompt) { + formattedMessages.unshift({ + id: crypto.randomUUID(), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + role: "system" as any, + content: request.systemPrompt, + }); + } + + this.getLogger().debug(`Formatted messages: ${JSON.stringify(formattedMessages)}`); + + const params: GetResponseParams = { + messages: formattedMessages, + instructions: request.systemPrompt, + system: request.systemPrompt, + modelSettings: { + // current model max tokens is 16384 - errors otherwise + // TODO: Find a value that would work as a fallback value for most models so we successfully generate sampling response + maxTokens: request.maxTokens, // tried removing max tokens and the result was + temperature: request.temperature || undefined, + }, + requestContext: { + modelPreferences: request.modelPreferences || undefined, + }, + structuredOutput: { + schema: request._meta?.["structuredOutput"], // fixedSchema, + }, + }; + + this.getLogger().debug(`Sampling request params: ${JSON.stringify(params)}`); + + const response = await this._agentService.getSamplingResponse(params, true); + + if (!response) { + throw new Error("Failed to generate response for sampling success response."); + } + + this.getLogger().info(`Generated response for sampling success response: ${JSON.stringify(response)}`); + + const finalMessage: IoAiWeb.SamplingSuccessResponse = { + role: "assistant", + content: { + type: "text", + text: response.text, + }, + model: this._agentService.getSelectedAgentModelId(), + stopReason: SAMPLING_STOP_REASONS.END_TURN, + }; + + this.getLogger().debug(`Sampling request final response message: ${JSON.stringify(finalMessage)}`); + + return finalMessage; + } + + private isCustomSamplingHandlerProvided(mcp: IoAiWeb.WebConfig["mcp"]): boolean { + const condition: boolean = !!mcp?.clientsConfig?.capabilities?.sampling?.handler; + + if (!condition) { + this.getLogger().warn("No custom sampling handler provided. Using built-in handler."); + } + + return condition; + } + + private getLogger(): IOConnectCore.Logger.API { + return this._logger.get(this.LOGGER_NAME); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/types.ts new file mode 100644 index 0000000..c0cc3d8 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/types.ts @@ -0,0 +1,7 @@ +export enum SAMPLING_STOP_REASONS { + END_TURN = "endTurn", + STOP_SEQUENCE = "stopSequence", + MAX_TOKENS = "maxTokens", +} + +export type SamplingStopReasonType = SAMPLING_STOP_REASONS.END_TURN | SAMPLING_STOP_REASONS.STOP_SEQUENCE | SAMPLING_STOP_REASONS.MAX_TOKENS; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io/io.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io/io.service.ts new file mode 100644 index 0000000..925c5c7 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io/io.service.ts @@ -0,0 +1,185 @@ +import { inject, Injectable, Signal, signal, WritableSignal, DestroyRef } from "@angular/core"; +import { toSignal } from "@angular/core/rxjs-interop"; +import { IoAiWeb, IoAiWebFactory } from "@interopio/ai-web"; +import { IOConnectBrowser } from "@interopio/browser"; +import { IOConnectCore } from "@interopio/core"; +import { IOConnectDesktop } from "@interopio/desktop"; +import { IOConnectModals } from "@interopio/modals-api"; +import { IOConnectStore } from "@interopio/ng"; +import { map } from "rxjs"; + +import { LoggerService } from "../../services/logger/logger.service"; +import { AppLifecycleFacade } from "../../store/app-lifecycle/app-lifecycle.facade"; + +const PREFS_NAMESPACE = "ioAssist"; + +@Injectable({ providedIn: "root" }) +export class IOConnectService { + private readonly _ioConnectStore: IOConnectStore = inject(IOConnectStore); + private readonly _destroyRef: DestroyRef = inject(DestroyRef); + private readonly _logger: LoggerService = inject(LoggerService); + private readonly _appLifecycleFacade = inject(AppLifecycleFacade); + protected readonly LOGGER_NAME: string = "IOConnectService"; + private _prefsWriteQueue: Promise = Promise.resolve(); + + private _isUnsubscribed: WritableSignal = signal(false); + + private _isDarkMode: WritableSignal = signal(false); + public get isDarkMode(): Signal { + return this._isDarkMode; + } + + private get _ioConnect(): IOConnectDesktop.API | IOConnectBrowser.API { + return this._ioConnectStore.getIOConnect(); + } + + private get _ioConnectReadyInitError(): Error | undefined { + return this._ioConnectStore.getInitError(); + } + + public readonly isIoConnectReady: Signal; + + constructor() { + this.isIoConnectReady = toSignal( + this._ioConnectStore.ready().pipe( + map((r) => { + if (r.error === undefined && !this._logger.isInitialized) { + this._logger.setLogger(this._ioConnect.logger); + } + + return r.error === undefined; + }) + ), + { initialValue: false } + ); + + this._destroyRef.onDestroy(() => { + this.handleDestroy(); + }); + } + + public async subscribeToThemeChanges(): Promise { + const ioThemesAPI: IOConnectDesktop.Themes.API | IOConnectBrowser.Themes.API | undefined = this._ioConnect.themes; + + if (!ioThemesAPI) return; + + const currentTheme = await ioThemesAPI.getCurrent(); + + if (!currentTheme) return; + + // Set initial theme + this.changeTheme(currentTheme); + + // Subscribe to future changes + ioThemesAPI.onChanged(this.changeTheme.bind(this)); + } + + public async fetchPrefs(key: string): Promise { + const prefs: IOConnectDesktop.Preferences.AppPreferences | IOConnectBrowser.Prefs.AppPreferences | undefined = await this._ioConnect.prefs.get(); + + if (!prefs || !prefs.data) return Promise.reject("No preferences found"); + + const namespace = (prefs.data[PREFS_NAMESPACE] ?? {}) as Record; + + if (!(key in namespace)) { + this.getLogger().debug(`Prefs key "${key}" not found in namespace "${PREFS_NAMESPACE}"`); + return undefined; + } + + return namespace[key] as T | undefined; + } + + public updatePrefs(key: string, value: T): Promise { + const enqueued = this._prefsWriteQueue.then(async () => { + const currentPrefs: IOConnectDesktop.Preferences.AppPreferences | IOConnectBrowser.Prefs.AppPreferences | undefined = await this._ioConnect.prefs.get(); + + const prefsData = { ...currentPrefs?.data }; + const namespace = { ...((prefsData[PREFS_NAMESPACE] ?? {}) as Record) }; + namespace[key] = value; + prefsData[PREFS_NAMESPACE] = namespace; + + await this._ioConnect.prefs.update(prefsData); + }); + + // Keep _writeQueue as a void-settled tail so it never rejects and stays chainable. + this._prefsWriteQueue = enqueued.then( + () => undefined, + () => undefined + ); + + return enqueued; + } + + public handleDestroy(): void { + this._isUnsubscribed.set(true); + } + + public async initializeIOIntelWeb(config: IoAiWeb.WebConfig): Promise { + // Debug-only global. NOT required by IoAiWebFactory — the factory takes the + // io.Connect instance as a direct argument and never reads `window.io`. Host + // apps that need a global set their own. Commented out to verify nothing depends on it. + // (window as any).io = this._ioConnect; + return IoAiWebFactory(this._ioConnect as IOConnectBrowser.API, config); + } + + public async isModalsAvailable(): Promise { + const ioBrowserAPI: IOConnectBrowser.API = this._ioConnect as IOConnectBrowser.API; + + const modalsAPI = ioBrowserAPI.modals; + const modalsDialogsAPI = ioBrowserAPI.modals?.dialogs; + + if (!modalsAPI || !modalsDialogsAPI) { + return false; + } + + const status: IOConnectModals.ModalsStatus | undefined = await modalsAPI.getStatus(); + + if (!status) { + return false; + } + + return status.platformConfigured === true; + } + + public requestModalDialog(options: IOConnectModals.DialogRequestConfig): Promise { + const ioBrowserAPI = this._ioConnect as IOConnectBrowser.API; + if (!ioBrowserAPI.modals || !ioBrowserAPI.modals.dialogs) { + throw new Error("IO Connect Modals API is not available."); + } + + return ioBrowserAPI.modals.dialogs.request(options); + } + + private _hasStartedIODependentTasks: boolean = false; + + public startIODependentTasks(): void { + if (this._hasStartedIODependentTasks) return; + this._hasStartedIODependentTasks = true; + + // Start any tasks that depend on IO being ready, e.g. subscribe to theme changes + this.subscribeToThemeChanges(); + + this._appLifecycleFacade.dispatchInitAppCoreServices(); + } + + private changeTheme = (theme: { name: string }): void => { + if (this._isUnsubscribed()) return; + + const themeName: string = theme.name; + + if (!themeName) { + this.getLogger().warn(`Received invalid theme name: ${themeName}`); + + return; + } + + document.documentElement.className = ""; + document.documentElement.classList.add(themeName); + + this._isDarkMode.set(themeName === "dark"); + }; + + private getLogger(): IOConnectCore.Logger.API { + return this._logger.get(this.LOGGER_NAME); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/logger/logger.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/logger/logger.service.ts new file mode 100644 index 0000000..fa2377c --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/logger/logger.service.ts @@ -0,0 +1,61 @@ +import { Injectable } from "@angular/core"; +import { IOConnectCore } from "@interopio/core"; + +const LIB_NAME = "io-assist"; + +/** + * Centralized logging service for the io-assist Angular app. + * + * Wraps the IO Connect Logger API (`IOConnectCore.Logger.API`) and provides + * namespaced sub-loggers per subsystem. Before IO Connect is initialized, + * calls to `get()` will throw — call `setLogger()` first. + * + * Initialized automatically by `IOConnectService` when IO Connect becomes ready. + * + * @example + * ```ts + * private readonly _logger = inject(LoggerService); + * + * someMethod(): void { + * const logger = this._logger.get('message.effects'); + * logger.info('Processing message stream'); + * logger.warn('Unexpected chunk type'); + * logger.error('Stream failed', error); + * } + * ``` + */ +@Injectable({ providedIn: "root" }) +export class LoggerService { + private _logger: IOConnectCore.Logger.API | undefined; + + /** + * Initialize the logger with the IO Connect Logger API. + * Should be called once after IO Connect is ready. + */ + public setLogger(ioLogger: IOConnectCore.Logger.API): void { + this._logger = ioLogger; + } + + /** + * Returns true if the logger has been initialized with IO Connect. + */ + public get isInitialized(): boolean { + return this._logger !== undefined; + } + + /** + * Get a namespaced sub-logger for a specific subsystem. + * + * @param subSystem - Dot-separated subsystem name (e.g. 'message.effects', 'agent.service'). + * @returns An `IOConnectCore.Logger.API` instance with methods: log, trace, debug, info, warn, error. + */ + public get(subSystem: string): IOConnectCore.Logger.API { + if (!this._logger) { + console.warn("Logger is not configured. Please call setLogger() first. Falling back to console."); + + return console as unknown as IOConnectCore.Logger.API; + } + + return this._logger.subLogger(`${LIB_NAME}.${subSystem}`); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/mcp-apps/mcp-apps.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/mcp-apps/mcp-apps.service.ts new file mode 100644 index 0000000..0fa3817 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/mcp-apps/mcp-apps.service.ts @@ -0,0 +1,255 @@ +import { inject, Injectable, Signal, signal, WritableSignal } from "@angular/core"; +import { IoAiWeb } from "@interopio/ai-web"; +import { IOConnectCore } from "@interopio/core"; +import { Store } from "@ngrx/store"; + +import { APP_BUTTON_TYPES } from "../../components/app-button/enum"; +import { PANEL_BUTTON_ACTION_TYPE } from "../../components/app-panel/enums"; +import { UI_STRINGS } from "../../constants/ui-strings"; +import { getResponse } from "../../store/message/message.actions"; +import { UI_MESSAGE_ROLES } from "../../store/message/types"; +import { selectActiveThreadId } from "../../store/thread/thread.selector"; +import { IOConnectService } from "../io/io.service"; +import { LoggerService } from "../logger/logger.service"; +import { OverlayService } from "../overlay/overlay.service"; + +/** + * Centralized service for all MCP Apps interactions in io-assist. + * + * Manages: + * - availability check (mcpApps is only present when configured) + * - instance tracking (signal-based for Angular reactivity) + * - per-instance chat-message subscriptions + * - replace-instance modal delegation + * - event wiring (onAppCreated / onRecreateRequested / onAppRecreated) + */ +@Injectable({ + providedIn: "root", +}) +export class McpAppsService { + private readonly _ioConnectService: IOConnectService = inject(IOConnectService); + private readonly _overlayService: OverlayService = inject(OverlayService); + private readonly _store: Store = inject(Store); + private readonly _loggerService: LoggerService = inject(LoggerService); + + private readonly _activeThreadId: Signal = this._store.selectSignal(selectActiveThreadId); + + private _api: IoAiWeb.McpApps.API | undefined; + + private readonly _instances: WritableSignal = signal([]); + private readonly _unsubs: (() => void)[] = []; + private readonly _chatMessageUnsubs = new Map void>(); + + /** + * When true, a replace-instance modal is already visible. + * Subsequent recreate requests during the same stream auto-resolve + * as "replace" to avoid stacking multiple dialogs. + */ + private _isRecreateModalOpen = false; + + /** Whether MCP Apps are available in the current session. */ + public get isAvailable(): boolean { + return this._api !== undefined; + } + + /** Reactive list of tracked MCP App instances. */ + public get instances(): Signal { + return this._instances; + } + + /** + * Called once during IOIntelWebService.initialize() — wires event + * subscriptions if the mcpApps namespace exists on the API. + */ + public attach(intelWebApi: IoAiWeb.API): void { + if (!intelWebApi.mcpApps) return; + + this._api = intelWebApi.mcpApps; + + const unsubCreated = this._api.onAppCreated((app) => { + const activeThread = this._activeThreadId(); + if (activeThread && app.threadId !== activeThread) return; + + this._instances.update((prev) => [...prev, app]); + this._subscribeToChatMessages(app); + }); + + const unsubRecreate = this._api.onRecreateRequested((event) => { + // When a modal is already open (e.g. LLM called the same tool 5 times), + // auto-select "replace" for subsequent events to avoid stacking dialogs. + if (this._isRecreateModalOpen) { + event.select("recreate").catch(() => this._logger().warn("Failed to auto-select 'recreate' on mcpApps recreate request")); + return; + } + + this._isRecreateModalOpen = true; + this._showReplaceInstanceModal(event.toolName) + .then((replace) => event.select(replace ? "recreate" : "newInstance")) + .catch(() => { + this._logger().warn("Failed to select option on mcpApps recreate request — defaulting to recreate"); + event.select("recreate").catch(() => undefined); + }) + .finally(() => { + this._isRecreateModalOpen = false; + }); + }); + + const unsubRecreated = this._api.onAppRecreated((event) => { + this._instances.update((prev) => { + const filtered = prev.filter((a) => a.id !== event.oldId); + // The new app is already added by onAppCreated — no need to push here. + return filtered; + }); + // Clean up the old instance's chat-message subscription. + const oldUnsub = this._chatMessageUnsubs.get(event.oldId); + if (oldUnsub) { + oldUnsub(); + this._chatMessageUnsubs.delete(event.oldId); + } + }); + + this._unsubs.push(unsubCreated, unsubRecreate, unsubRecreated); + } + + /** Clears tracked instances and tears down per-instance chat-message subscriptions. */ + public resetInstances(): void { + this._chatMessageUnsubs.forEach((unsub) => unsub()); + this._chatMessageUnsubs.clear(); + this._instances.set([]); + } + + /** Closes all active proxy windows and resets to accept new instances. No-ops when mcpApps is not configured. */ + public async closeAll(): Promise { + if (!this._api) return; + + await this._api.closeAll(); + } + + /** + * Creates MCP App instances for a thread (used on thread switch to restore apps). + * No-ops when mcpApps is not configured. + */ + public async createApps(params: IoAiWeb.McpApps.CreateParams): Promise { + if (!this._api) return; + + await this._api.create(params); + } + + /** + * Atomically closes all existing MCP App instances and creates new ones. + * Prevents race conditions when rapidly switching threads. + * No-ops when mcpApps is not configured. + */ + public async recreate(params: IoAiWeb.McpApps.CreateParams): Promise { + if (!this._api) return; + + await this._api.recreate(params); + } + + /** Forwards response-pending status to all active MCP App instances. */ + public notifyPendingResponse(isPending: boolean): void { + this._api?.notifyPendingResponse(isPending); + } + + // ─── Private helpers ────────────────────────────────────────────────────── + + private _subscribeToChatMessages(app: IoAiWeb.McpApps.AppInstance): void { + const unsub = app.onMessage((text) => { + const activeThreadId = this._activeThreadId(); + if (!activeThreadId) return; + + this._store.dispatch( + getResponse({ + params: { + messages: [ + { + id: crypto.randomUUID(), + role: UI_MESSAGE_ROLES.USER, + content: text, + }, + ], + }, + threadId: activeThreadId, + }) + ); + }); + + this._chatMessageUnsubs.set(app.id, unsub); + } + + private async _showReplaceInstanceModal(toolName: string): Promise { + const strings = UI_STRINGS.MCP_APP_REPLACE_MODAL; + + const ioModalResult = await this._tryIoConnectModal(strings, toolName); + if (ioModalResult !== null) return ioModalResult; + + return this._showFallbackOverlay(strings, toolName); + } + + private async _tryIoConnectModal(strings: typeof UI_STRINGS.MCP_APP_REPLACE_MODAL, toolName: string): Promise { + const isAvailable = await this._ioConnectService.isModalsAvailable().catch(() => false); + if (!isAvailable) return null; + + try { + const response = await this._ioConnectService.requestModalDialog({ + templateName: "noInputsConfirmationDialog", + variables: { + title: strings.TITLE, + heading: strings.HEADING, + text: strings.TEXT(toolName), + actionButtons: [ + { variant: "primary", text: strings.REPLACE_BUTTON, id: "replace" }, + { variant: "outline", text: strings.NEW_INSTANCE_BUTTON, id: "new" }, + ], + }, + }); + + return response?.responseButtonClicked?.id === "replace"; + } catch { + return null; + } + } + + private _escapeHtml(str: string): string { + return str.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + } + + private _showFallbackOverlay(strings: typeof UI_STRINGS.MCP_APP_REPLACE_MODAL, toolName: string): Promise { + const escapedToolName = this._escapeHtml(toolName); + + return new Promise((resolve) => { + this._overlayService.showPanelOverlay({ + title: strings.TITLE, + content: `
+ ${strings.HEADING} + ${strings.TEXT(escapedToolName)} +
`, + isHeaderCloseButtonDisplayed: false, + footerButtons: [ + { + label: strings.REPLACE_BUTTON, + action: PANEL_BUTTON_ACTION_TYPE.CUSTOM, + type: APP_BUTTON_TYPES.SUBMIT, + onClick: () => { + this._overlayService.closeCurrentOverlay(); + resolve(true); + }, + }, + { + label: strings.NEW_INSTANCE_BUTTON, + action: PANEL_BUTTON_ACTION_TYPE.CUSTOM, + type: APP_BUTTON_TYPES.DEFAULT, + onClick: () => { + this._overlayService.closeCurrentOverlay(); + resolve(false); + }, + }, + ], + }); + }); + } + + private _logger(): IOConnectCore.Logger.API { + return this._loggerService.get("McpAppsService"); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/overlay/overlay.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/overlay/overlay.service.ts new file mode 100644 index 0000000..cbbec62 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/overlay/overlay.service.ts @@ -0,0 +1,123 @@ +import { Overlay, OverlayRef } from "@angular/cdk/overlay"; +import { ComponentPortal } from "@angular/cdk/portal"; +import { ComponentRef, EnvironmentInjector, inject, Injectable, signal, Type, WritableSignal } from "@angular/core"; + +import { PanelOverlayResult } from "./types"; +import { AppPanelComponent } from "../../components/app-panel/app-panel.component"; +import { AppPanelOverlayConfig } from "../../components/app-panel/types"; + +/** + * Service to manage overlay states such as: + * - displaying or hiding the thread history UI + * - other overlay states can be added here in the future + * + * TODO: Extract state in a root store when state grows beyond this simple use case + */ +@Injectable({ + providedIn: "root", +}) +export class OverlayService { + public isThreadHistoryDisplayed: WritableSignal = signal(false); + public isPromptPanelDisplayed: WritableSignal = signal(false); + public isToolPanelDisplayed: WritableSignal = signal(false); + + private _overlayStack: OverlayRef[] = []; + private _overlaySignals = new Map>(); + + private _overlayCDK = inject(Overlay); + private _injector = inject(EnvironmentInjector); + + public showPanelOverlay(config?: AppPanelOverlayConfig): PanelOverlayResult { + if (!config) throw new Error("Overlay config is required to display the panel overlay"); + + if (!config.content) { + throw new Error("Overlay config must include content to display in the panel"); + } + + const overlayRef: OverlayRef = this._overlayCDK.create({ + hasBackdrop: true, + backdropClass: "cdk-overlay-dark-backdrop", + panelClass: "panel-overlay-container", + positionStrategy: this._overlayCDK.position().global().centerHorizontally().centerVertically(), + }); + + const portal: ComponentPortal = new ComponentPortal(AppPanelComponent, null, this._injector); + + const panelRef: ComponentRef = overlayRef.attach(portal); + + panelRef.setInput("title", config.title ?? ""); + panelRef.setInput("description", config.description ?? ""); + panelRef.setInput("footerButtons", config.footerButtons ?? []); + panelRef.setInput("isHeaderCloseButtonDisplayed", config.isHeaderCloseButtonDisplayed ?? true); + + // Handle HTML content if provided as string + if (config.content && typeof config.content === "string") { + panelRef.setInput("htmlContent", config.content); + } + + panelRef.instance.onClose.subscribe(() => { + this._removeOverlayFromStack(overlayRef); + overlayRef.dispose(); + + this._overlaySignals.get(overlayRef)?.set(false); + this._overlaySignals.delete(overlayRef); + }); + + // Use this to handle on submit events from the panel if needed in the future + //panelRef.instance.onSubmit.subscribe((action) => {}); + + const result: PanelOverlayResult = { + overlayRef, + panelRef, + contentRef: undefined, + }; + + // Add overlay to stack + this._overlayStack.push(overlayRef); + + // Handle backdrop clicks - only close the top-most overlay + overlayRef.backdropClick().subscribe(() => { + if (this._overlayStack[this._overlayStack.length - 1] === overlayRef) { + this._removeOverlayFromStack(overlayRef); + overlayRef.dispose(); + + this._overlaySignals.get(overlayRef)?.set(false); + this._overlaySignals.delete(overlayRef); + } + }); + + // Inject component content after the current change detection cycle (if content is a component) + if (config.content && typeof config.content !== "string") { + setTimeout(() => { + result.contentRef = panelRef.instance.injectBodyContent(config.content as Type) as ComponentRef; + }, 0); + } + + if (config.isDisplayedSignal) { + this._overlaySignals.set(overlayRef, config.isDisplayedSignal); + config.isDisplayedSignal.set(true); + } + + return result; + } + + public closeCurrentOverlay(): void { + const topOverlay: OverlayRef = this._overlayStack[this._overlayStack.length - 1]; + + if (!topOverlay) return; + + this._removeOverlayFromStack(topOverlay); + topOverlay.dispose(); + + this._overlaySignals.get(topOverlay)?.set(false); + this._overlaySignals.delete(topOverlay); + } + + private _removeOverlayFromStack(overlayRef: OverlayRef): void { + const index: number = this._overlayStack.indexOf(overlayRef); + + if (index > -1) { + this._overlayStack.splice(index, 1); + } + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/overlay/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/overlay/types.ts new file mode 100644 index 0000000..6448d98 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/overlay/types.ts @@ -0,0 +1,10 @@ +import { OverlayRef } from "@angular/cdk/overlay"; +import { ComponentRef } from "@angular/core"; + +import { AppPanelComponent } from "../../components/app-panel/app-panel.component"; + +export type PanelOverlayResult = { + overlayRef: OverlayRef; + panelRef: ComponentRef; + contentRef?: ComponentRef; +}; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/prompt/prompt.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/prompt/prompt.service.ts new file mode 100644 index 0000000..f40b029 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/prompt/prompt.service.ts @@ -0,0 +1,81 @@ +import { inject, Injectable } from "@angular/core"; +import { IOConnectCore } from "@interopio/core"; + +import { UIPrompt } from "./types"; +import { IO_ASSIST_CONFIG, IoAssistStaticConfig } from "../../../io-assist.config"; +import { IoAssistPrompt, IoAssistPromptCategory } from "../../../io-assist.types"; +import { normalizeIconResource, validateIconResource } from "../../utils/icon.utils"; +import { LoggerService } from "../logger/logger.service"; + +@Injectable({ + providedIn: "root", +}) +export class PromptService { + private readonly _ioAssistConfig: IoAssistStaticConfig = inject(IO_ASSIST_CONFIG); + private readonly _logger: LoggerService = inject(LoggerService); + protected readonly LOGGER_NAME: string = "PromptService"; + + public mapConfigPromptsToUIPrompts(): UIPrompt[] { + const defaultPrompts: IoAssistPromptCategory[] | undefined = this._ioAssistConfig.defaultPrompts; + + if (!defaultPrompts || defaultPrompts.length === 0) { + return []; + } + + return defaultPrompts.flatMap((promptCategory: IoAssistPromptCategory) => { + const category: string | undefined = promptCategory.category; + + return promptCategory.prompts.map((prompt: IoAssistPrompt) => this._mapIoAssistPromptToUIPrompt(prompt, category)); + }); + } + + private _mapIoAssistPromptToUIPrompt(prompt: IoAssistPrompt, category?: string): UIPrompt { + const uiPrompt: UIPrompt = { + name: prompt.name, + description: prompt.prompt, + messages: [ + { + role: "user", + content: { + type: "text", + text: prompt.prompt, + }, + }, + ], + source: { + mcpName: "defaultPrompts", + isUserDefined: false, + }, + category, + }; + + if (!prompt.iconResource) { + this.getLogger().warn(`No icon provided for prompt "${prompt.name}". Using default icon.`); + + return uiPrompt; + } + + // Normalize first (auto-corrects valid type mismatches, e.g. type:'svg' with data: 'data:...' → type:'data-url') + // Invalid/unknown types and empty data are left as-is so validation below can reject them. + const normalizedIcon = normalizeIconResource(prompt.iconResource, this.getLogger()); + + const validation = validateIconResource(normalizedIcon); + + if (!validation.valid) { + this.getLogger().warn(`Invalid icon for prompt "${prompt.name}": ${validation.error}. Using default icon.`); + + return uiPrompt; + } + + uiPrompt.icon = normalizedIcon; + + return uiPrompt; + } + + private getLogger(): IOConnectCore.Logger.API { + return this._logger.get(this.LOGGER_NAME); + } + + // TODO: subscribe to prompt updates prompts/update once we read prompts from backend. Currently, + // prompts are only from config so no updates happen. +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/prompt/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/prompt/types.ts new file mode 100644 index 0000000..f24141c --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/prompt/types.ts @@ -0,0 +1,19 @@ +import { IconResource } from "../../../io-assist.types"; + +export type Unsubscribe = () => void; + +export type Prompt = { + name: string; + messages: { role: "user" | "assistant"; content: { type: "text"; text: string } }[]; + source?: { + mcpName: string; + isUserDefined?: boolean; + }; + description?: string; + metadata?: Record; +}; + +export type UIPrompt = Prompt & { + category?: string; + icon?: IconResource; +}; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/responsive-ui/responsive-ui.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/responsive-ui/responsive-ui.service.ts new file mode 100644 index 0000000..2bec29f --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/responsive-ui/responsive-ui.service.ts @@ -0,0 +1,220 @@ +import { computed, effect, Injectable, Signal, signal, WritableSignal } from "@angular/core"; +import { toSignal } from "@angular/core/rxjs-interop"; +import { fromEvent, map } from "rxjs"; + +/** + * Tailwind CSS breakpoints + * @see https://tailwindcss.com/docs/responsive-design + */ +export enum BREAKPOINT { + XS = "xs", // Extra small devices (< 640px) + SM = "sm", // Small devices (≥ 640px) + MD = "md", // Medium devices (≥ 768px) + LG = "lg", // Large devices (≥ 1024px) + XL = "xl", // Extra large devices (≥ 1280px) + XXL = "2xl", // 2X Extra large devices (≥ 1536px) +} + +/** + * Tailwind CSS breakpoint values in pixels + */ +const BREAKPOINT_VALUES: Record = { + [BREAKPOINT.XS]: 0, + [BREAKPOINT.SM]: 640, + [BREAKPOINT.MD]: 768, + [BREAKPOINT.LG]: 1024, + [BREAKPOINT.XL]: 1280, + [BREAKPOINT.XXL]: 1536, +}; + +/** + * Service to handle media query checks following Tailwind CSS breakpoint conventions + * + * Provides reactive signals for: + * - Current breakpoint detection + * - Individual breakpoint checks (isXs, isSm, isMd, etc.) + * - Min-width checks (isSmUp, isMdUp, etc.) + * - Max-width checks (isXsDown, isSmDown, etc.) + */ +@Injectable({ + providedIn: "root", +}) +export class ResponsiveUIService { + // Listen to window resize events + private readonly _resizeSignal: Signal = toSignal( + fromEvent(window, "resize").pipe( + map(() => { + return window.innerWidth; + }) + ), + { initialValue: window.innerWidth } + ); + + // Current breakpoint as writable signal + private readonly _currentBreakpoint: WritableSignal = signal(this.getCurrentBreakpoint()); + + // Individual breakpoint checks (exact match) + private readonly _isXs: Signal = computed(() => this._currentBreakpoint() === BREAKPOINT.XS); + private readonly _isSm: Signal = computed(() => this._currentBreakpoint() === BREAKPOINT.SM); + private readonly _isMd: Signal = computed(() => this._currentBreakpoint() === BREAKPOINT.MD); + private readonly _isLg: Signal = computed(() => this._currentBreakpoint() === BREAKPOINT.LG); + private readonly _isXl: Signal = computed(() => this._currentBreakpoint() === BREAKPOINT.XL); + private readonly _is2xl: Signal = computed(() => this._currentBreakpoint() === BREAKPOINT.XXL); + + // Min-width checks (breakpoint and up) as writable signals + private readonly _isSmUp: WritableSignal = signal(this.matchesMinWidth(BREAKPOINT.SM)); + private readonly _isMdUp: WritableSignal = signal(this.matchesMinWidth(BREAKPOINT.MD)); + private readonly _isLgUp: WritableSignal = signal(this.matchesMinWidth(BREAKPOINT.LG)); + private readonly _isXlUp: WritableSignal = signal(this.matchesMinWidth(BREAKPOINT.XL)); + private readonly _is2xlUp: WritableSignal = signal(this.matchesMinWidth(BREAKPOINT.XXL)); + + // Max-width checks (breakpoint and down) as writable signals + private readonly _isXsDown: WritableSignal = signal(this.matchesMaxWidth(BREAKPOINT.XS)); + private readonly _isSmDown: WritableSignal = signal(this.matchesMaxWidth(BREAKPOINT.SM)); + private readonly _isMdDown: WritableSignal = signal(this.matchesMaxWidth(BREAKPOINT.MD)); + private readonly _isLgDown: WritableSignal = signal(this.matchesMaxWidth(BREAKPOINT.LG)); + private readonly _isXlDown: WritableSignal = signal(this.matchesMaxWidth(BREAKPOINT.XL)); + + constructor() { + // Use effect to update breakpoints on window resize + effect(() => { + this._resizeSignal(); // Explicitly track the signal + + // Update current breakpoint + const currentBreakpoint = this.getCurrentBreakpoint(); + this._currentBreakpoint.set(currentBreakpoint); + + // Update min-width checks + this._isSmUp.set(this.matchesMinWidth(BREAKPOINT.SM)); + this._isMdUp.set(this.matchesMinWidth(BREAKPOINT.MD)); + this._isLgUp.set(this.matchesMinWidth(BREAKPOINT.LG)); + this._isXlUp.set(this.matchesMinWidth(BREAKPOINT.XL)); + this._is2xlUp.set(this.matchesMinWidth(BREAKPOINT.XXL)); + + // Update max-width checks + this._isXsDown.set(this.matchesMaxWidth(BREAKPOINT.XS)); + this._isSmDown.set(this.matchesMaxWidth(BREAKPOINT.SM)); + this._isMdDown.set(this.matchesMaxWidth(BREAKPOINT.MD)); + this._isLgDown.set(this.matchesMaxWidth(BREAKPOINT.LG)); + this._isXlDown.set(this.matchesMaxWidth(BREAKPOINT.XL)); + }); + } + + // Public getters for current breakpoint + public get currentBreakpoint(): BREAKPOINT { + return this._currentBreakpoint(); + } + + // Public getters for individual breakpoint checks + public get isXs(): boolean { + return this._isXs(); + } + + public get isSm(): boolean { + return this._isSm(); + } + + public get isMd(): boolean { + return this._isMd(); + } + + public get isLg(): boolean { + return this._isLg(); + } + + public get isXl(): boolean { + return this._isXl(); + } + + public get is2xl(): boolean { + return this._is2xl(); + } + + // Getters for min-width checks + public get isSmUp(): boolean { + return this._isSmUp(); + } + + public get isMdUp(): boolean { + return this._isMdUp(); + } + + public get isLgUp(): boolean { + return this._isLgUp(); + } + + public get isXlUp(): boolean { + return this._isXlUp(); + } + + public get is2xlUp(): boolean { + return this._is2xlUp(); + } + + // Getters for max-width checks + public get isXsDown(): boolean { + return this._isXsDown(); + } + + public get isSmDown(): boolean { + return this._isSmDown(); + } + + public get isMdDown(): boolean { + return this._isMdDown(); + } + + public get isLgDown(): boolean { + return this._isLgDown(); + } + + public get isXlDown(): boolean { + return this._isXlDown(); + } + + /** + * Get the current breakpoint based on window width + */ + private getCurrentBreakpoint(): BREAKPOINT { + const width = window.innerWidth; + + if (width >= BREAKPOINT_VALUES[BREAKPOINT.XXL]) return BREAKPOINT.XXL; + if (width >= BREAKPOINT_VALUES[BREAKPOINT.XL]) return BREAKPOINT.XL; + if (width >= BREAKPOINT_VALUES[BREAKPOINT.LG]) return BREAKPOINT.LG; + if (width >= BREAKPOINT_VALUES[BREAKPOINT.MD]) return BREAKPOINT.MD; + if (width >= BREAKPOINT_VALUES[BREAKPOINT.SM]) return BREAKPOINT.SM; + + return BREAKPOINT.XS; + } + + /** + * Check if the current viewport matches the minimum width for a breakpoint + */ + private matchesMinWidth(breakpoint: BREAKPOINT): boolean { + return window.innerWidth >= BREAKPOINT_VALUES[breakpoint]; + } + + /** + * Check if the current viewport matches the maximum width for a breakpoint + * Max width is defined as one pixel less than the next breakpoint + */ + private matchesMaxWidth(breakpoint: BREAKPOINT): boolean { + const breakpoints = Object.values(BREAKPOINT); + const currentIndex = breakpoints.indexOf(breakpoint); + + // If it's the last breakpoint, no max width + if (currentIndex === breakpoints.length - 1) { + return window.innerWidth >= BREAKPOINT_VALUES[breakpoint]; + } + + const nextBreakpoint = breakpoints[currentIndex + 1]; + return window.innerWidth < BREAKPOINT_VALUES[nextBreakpoint]; + } + + /** + * Check if viewport matches a custom media query + */ + public matches(query: string): boolean { + return window.matchMedia(query).matches; + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/subscription-cleanup/subscription-cleanup.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/subscription-cleanup/subscription-cleanup.service.ts new file mode 100644 index 0000000..886f92c --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/subscription-cleanup/subscription-cleanup.service.ts @@ -0,0 +1,100 @@ +import { DestroyRef, inject, Injectable } from "@angular/core"; +import { IOConnectCore } from "@interopio/core"; +import { UnsubscribeFunction } from "callback-registry"; + +import { LoggerService } from "../logger/logger.service"; + +type NamedSubscription = { + name: string; + fn: UnsubscribeFunction; +}; + +/** + * Service to manage and clean up functions that require unsubscribing. + * + * Service is not provided in root to allow for scoped usage. + * This means that the service lifecycle will be the same as the component or module that provides it which + * means that once the component is destroyed the subscriptions will be cleaned up automatically. + * + * Usage: + * 1. Inject the service in your component or module. + * 2. Use the `add` method to register subscriptions with a unique name and their corresponding unsubscribe function. + * 3. When the component or module is destroyed, all registered subscriptions will be automatically cleaned up. + * 4. Additional manual cleanup by name can be performed using the `manualUnsubscribe` method. + * + */ +@Injectable() +export class SubscriptionCleanupService { + private _subscriptions: Map = new Map(); + private readonly _destroyRef: DestroyRef = inject(DestroyRef); + private readonly _logger: LoggerService = inject(LoggerService); + protected readonly LOGGER_NAME: string = "SubscriptionCleanupService"; + + constructor() { + this._destroyRef.onDestroy(() => { + this.handleDestroy(); + }); + } + + public add(name: string, func: UnsubscribeFunction): void { + if (!func) return; + + if (!name || name.trim() === "") { + this.getLogger().warn("Subscription name is required."); + + return; + } + + if (this._subscriptions.has(name)) { + this.getLogger().warn(`Subscription with name "${name}" already exists. Unsubscribing and overwriting.`); + + this.manualUnsubscribe(name); + } + + this._subscriptions.set(name, { name, fn: func }); + } + + public manualUnsubscribe(name: string): void { + const subscription: NamedSubscription | undefined = this._subscriptions.get(name); + + if (!subscription) return; + + try { + // Calling the unsubscribe function + subscription.fn(); + } catch (error) { + this.getLogger().error(`Error during subscription ${name} cleanup: ${error instanceof Error ? error.message : String(error)}`); + } + + this._subscriptions.delete(name); + } + + public handleDestroy(): void { + const successfulUnsubscribes: string[] = []; + const failedUnsubscribes: string[] = []; + + for (const [name, { fn }] of this._subscriptions) { + try { + fn(); + + successfulUnsubscribes.push(name); + } catch (error) { + this.getLogger().error(`Error during subscription ${name} cleanup: ${error instanceof Error ? error.message : String(error)}`); + + failedUnsubscribes.push(name); + } + } + + this._subscriptions.clear(); + + this.getLogger().info(`Successfully unsubscribed from [${successfulUnsubscribes.join(", ")}]`); + + if (failedUnsubscribes.length > 0) { + this.getLogger().warn(`Failed to unsubscribe from subscriptions: [${failedUnsubscribes.join(", ")}]`); + } + } + + private getLogger(): IOConnectCore.Logger.API { + return this._logger.get(this.LOGGER_NAME); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/thread/thread.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/thread/thread.service.ts new file mode 100644 index 0000000..c5af996 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/thread/thread.service.ts @@ -0,0 +1,207 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { inject, Injectable } from "@angular/core"; +import { IoAiWeb } from "@interopio/ai-web"; + +import { UIResponseMessage, UIMessageType, UI_MESSAGE_ROLES, UI_MESSAGE_PART_TYPES, UIUserMessage } from "../../store/message/types"; +import { UIThread } from "../../store/thread/types"; +import { IOAiWebService } from "../io-ai-web/io-ai-web.service"; +import { LoggerService } from "../logger/logger.service"; + +@Injectable({ + providedIn: "root", +}) +export class ThreadService { + private readonly _ioIntelWebService: IOAiWebService = inject(IOAiWebService); + private readonly _logger: LoggerService = inject(LoggerService); + protected readonly LOGGER_NAME: string = "ThreadService"; + + public fetchThreads(agentId: string, resourceId: string): Promise { + return this._ioIntelWebService.listThreads({ agentId, resourceId }); + } + + public renameThread(thread: UIThread, newTitle: string): Promise { + if (!newTitle || newTitle.trim() === "") { + throw new Error("Invalid thread title"); + } + + if (thread.title === newTitle) { + throw new Error("Thread title is the same"); + } + + return thread.update({ title: newTitle }); + } + + public deleteThread(thread: UIThread): Promise { + return thread.delete(); + } + + public deleteThreadState(threadId: string): Promise { + return this._ioIntelWebService.deleteThreadState({ threadId }); + } + + public async toUIThreads(threads: IoAiWeb.Threads.Thread[]): Promise { + if (threads.length === 0) return Promise.resolve([]); + + const convertedUIThreads: UIThread[] = []; + + threads.map((thread: IoAiWeb.Threads.Thread) => { + const tempThread: UIThread = this.convertSingleUIThread(thread); + + convertedUIThreads.push(tempThread); + }); + + return Promise.resolve(convertedUIThreads); + } + + public async fetchMessagesFromThread(thread: UIThread): Promise { + // TODO: Maybe limit should be optional in future - for now we fetch last 100 messages + try { + const messages: any = await thread.getMessages({ limit: 100 }); + + if (!messages.messages) { + throw new Error("No messages found in thread"); + } + + return this.convertToUIResponseMessages(messages.messages); + } catch (error: any) { + this._logger.get(this.LOGGER_NAME).error(`Failed to fetch thread messages: ${error instanceof Error ? error.message : String(error)}`); + return []; + } + } + + /** + * The uiMessages can look like this: + * They are of 'role' either 'user' or 'assistant' + * [ + * { + ..., + ..., + "role": "user", + "parts": [ + { + "type": "text", + "text": "Hello!" + } + ], + ... + }, + { + ..., + ..., + "role": "assistant", + "parts": [ + { + "type": "text", + "text": "..." + } + { + "type": "tool-invocation", + "tool-invocation": { + "toolCallId": ... + "toolName": ... + "args": ... + "result": ... + } + } + ] + } + * ] + * @param uiMessages - Array of messages from IoAiWeb API + * @returns Array of UIResponseMessage mapped from the input messages + */ + public convertToUIResponseMessages(uiMessages: any[]): UIResponseMessage[] { + const result: (UIResponseMessage | null)[] = []; + const toolCallMap = new Map(); + + uiMessages.forEach((message) => { + const role: UIMessageType = message.role; + const parts: any[] = message.content.parts; + + if (role === UI_MESSAGE_ROLES.USER) { + return result.push(this.toUserUIMessage(message, parts)); + } + + if (role === UI_MESSAGE_ROLES.ASSISTANT) { + return parts.forEach((part: any) => { + const msg = this.toAssistantUIMessage(part, toolCallMap); + + if (msg === null) { + return; + } + + result.push(msg); + }); + } + + // Other types can be handled here if needed + return; + }); + + return result.filter((msg: UIResponseMessage | null) => msg !== null); + } + + private toUserUIMessage(message: any, parts: any[]): UIUserMessage | null { + const userMessage: UIUserMessage = { + id: message.id, + role: UI_MESSAGE_ROLES.USER, + content: parts.find((part: any) => part.type === UI_MESSAGE_PART_TYPES.TEXT)?.text || "", + }; + + return userMessage; + } + + private toAssistantUIMessage(part: any, toolCallMap: Map): UIResponseMessage | null { + if (part.type === UI_MESSAGE_PART_TYPES.TEXT) { + return { + id: crypto.randomUUID(), // Generate unique ID for each assistant text part to avoid ID collision with parent message + role: UI_MESSAGE_ROLES.ASSISTANT, + content: part.text, + }; + } + + if (part.type === UI_MESSAGE_PART_TYPES.TOOL_INVOCATION) { + const toolInvocation = part.toolInvocation; + const toolCallId = toolInvocation.toolCallId; + const state = toolInvocation.state; + + // If this is a result, update the existing call instead of creating new message + if (state === "result") { + const existingCall = toolCallMap.get(toolCallId); + + if (existingCall) { + (existingCall as any).result = toolInvocation.result; + // Skip adding new message + return null; + } + + // No preceding call found (new format: Tool part comes directly with a tool result) + } + + const newMessage: UIResponseMessage = { + id: toolCallId, + role: UI_MESSAGE_ROLES.TOOL, + toolName: toolInvocation.toolName, + args: toolInvocation.args, + result: toolInvocation.result, + isExpanded: false, + }; + + if (state === "call") { + toolCallMap.set(toolCallId, newMessage); + } + + return newMessage; + } + + return null; + } + + private convertSingleUIThread(thread: IoAiWeb.Threads.Thread): UIThread { + const UIThread: UIThread = { + ...thread, + title: thread.title ? thread.title : thread.createdAt.toISOString(), + }; + + return UIThread; + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/tools/tools.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/tools/tools.service.ts new file mode 100644 index 0000000..11a34a8 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/tools/tools.service.ts @@ -0,0 +1,22 @@ +import { inject, Injectable } from "@angular/core"; +import { IoAiWeb } from "@interopio/ai-web"; + +import { UITool } from "../../store/tool/types"; +import { IOAiWebService } from "../io-ai-web/io-ai-web.service"; + +@Injectable({ + providedIn: "root", +}) +export class ToolsService { + private _ioIntelWebService: IOAiWebService = inject(IOAiWebService); + + public fetchTools(): Promise { + return this._ioIntelWebService.listTools(); + } + + public toggleTool(tool: UITool): Promise { + return this._ioIntelWebService.toggleTool(tool.name, !tool.enabled); + } + + // TODO: subscribe to tool updates tools/update +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/working-context/working-context.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/working-context/working-context.service.ts new file mode 100644 index 0000000..b95709f --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/services/working-context/working-context.service.ts @@ -0,0 +1,23 @@ +import { inject, Injectable } from "@angular/core"; +import { IoIntelWorkingContext } from "@interopio/working-context"; + +import { IOAiWebService } from "../io-ai-web/io-ai-web.service"; + +@Injectable({ + providedIn: "root", +}) +export class WorkingContextService { + private readonly _ioIntelWebService: IOAiWebService = inject(IOAiWebService); + + public getWorkingContext(): Promise> { + return this._ioIntelWebService.getWorkingContext(); + } + + public onWorkingContextChange(callback: (workingContext: Record) => void): IoIntelWorkingContext.UnsubscribeFunction { + return this._ioIntelWebService.onWorkingContextChange(callback); + } + + public isWorkingContextEnabled(): Promise { + return Promise.resolve(this._ioIntelWebService.isWorkingContextEnabled()); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.actions.enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.actions.enum.ts new file mode 100644 index 0000000..07078db --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.actions.enum.ts @@ -0,0 +1,6 @@ +export enum AGENT_ACTIONS { + LIST_AVAILABLE_AGENTS = "[Agent] List Available Agents", + LIST_AVAILABLE_AGENTS_SUCCESS = "[Agent] List Available Agents Success", + LIST_AVAILABLE_AGENTS_FAILURE = "[Agent] List Available Agents Failure", + SET_SELECTED_AGENT = "[Agent] Set Selected Agent", +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.actions.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.actions.ts new file mode 100644 index 0000000..4a96667 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.actions.ts @@ -0,0 +1,9 @@ +import { IoAiWeb } from "@interopio/ai-web"; +import { createAction, props } from "@ngrx/store"; + +import { AGENT_ACTIONS } from "./agent.actions.enum"; + +export const listAvailableAgents = createAction(AGENT_ACTIONS.LIST_AVAILABLE_AGENTS); +export const listAvailableAgentsSuccess = createAction(AGENT_ACTIONS.LIST_AVAILABLE_AGENTS_SUCCESS, props<{ agents: IoAiWeb.Agents.Agent[] }>()); +export const listAvailableAgentsFailure = createAction(AGENT_ACTIONS.LIST_AVAILABLE_AGENTS_FAILURE, props<{ error: Error }>()); +export const selectAgent = createAction(AGENT_ACTIONS.SET_SELECTED_AGENT, props<{ agentId: string }>()); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.effects.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.effects.ts new file mode 100644 index 0000000..66320a0 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.effects.ts @@ -0,0 +1,76 @@ +import { inject, Injectable } from "@angular/core"; +import { IoAiWeb } from "@interopio/ai-web"; +import { IOConnectCore } from "@interopio/core"; +import { Actions, createEffect, ofType } from "@ngrx/effects"; +import { mergeMap, from, of, map, catchError, tap } from "rxjs"; + +import { listAvailableAgents, listAvailableAgentsFailure, listAvailableAgentsSuccess } from "./agent.actions"; +import { AgentFacade } from "./agent.facade"; +import { IO_ASSIST_CONFIG, IoAssistStaticConfig } from "../../../io-assist.config"; +import { AgentManagerService } from "../../services/agent/agent-manager/agent-manager.service"; +import { LoggerService } from "../../services/logger/logger.service"; + +@Injectable() +export class AgentEffects { + private readonly _actions$: Actions = inject(Actions); + private readonly _agentManagerService: AgentManagerService = inject(AgentManagerService); + private readonly _agentFacade: AgentFacade = inject(AgentFacade); + private readonly _config: IoAssistStaticConfig = inject(IO_ASSIST_CONFIG); + private readonly _logger: LoggerService = inject(LoggerService); + protected readonly LOGGER_NAME: string = "AgentEffects"; + + listAvailableAgents$ = createEffect(() => + this._actions$.pipe( + ofType(listAvailableAgents), + mergeMap(() => + from(this._agentManagerService.listAgents()).pipe( + map((agents: IoAiWeb.Agents.Agent[]) => listAvailableAgentsSuccess({ agents })), + catchError((error: Error) => of(listAvailableAgentsFailure({ error }))) + ) + ) + ) + ); + + onListAvailableAgentsSuccess$ = createEffect( + () => + this._actions$.pipe( + ofType(listAvailableAgentsSuccess), + tap(({ agents }) => { + if (!agents || agents.length === 0) { + this.getLogger().error("No agents available."); + + return; + } + + if (this._config.defaultAgentName) { + this.getLogger().info(`Default agent specified in config: ${this._config.defaultAgentName}`); + } + + let configuredAgentId = this._config.defaultAgentName; + + if (!configuredAgentId) { + this.getLogger().warn(`No default agent specified in config. Selecting first available agent: "${agents[0].id}".`); + + configuredAgentId = agents[0].id; + } + + let agentMatch: IoAiWeb.Agents.Agent | undefined = agents.find((agent) => agent.id === configuredAgentId); + + if (!agentMatch) { + this.getLogger().error(`Agent with id: "${configuredAgentId}" not found among available agents. Using first available agent: "${agents[0].id}".`); + + agentMatch = agents[0]; + } + + this.getLogger().info(`Working with agent: "${agentMatch.id}"`); + + this._agentFacade.dispatchSelectAgent(agentMatch.id); + }) + ), + { dispatch: false } + ); + + private getLogger(): IOConnectCore.Logger.API { + return this._logger.get(this.LOGGER_NAME); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.facade.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.facade.ts new file mode 100644 index 0000000..e6e21ba --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.facade.ts @@ -0,0 +1,31 @@ +import { inject, Injectable, Signal } from "@angular/core"; +import { IoAiWeb } from "@interopio/ai-web"; +import { Store } from "@ngrx/store"; + +import { listAvailableAgents, selectAgent } from "./agent.actions"; +import { selectAvailableAgents, selectSelectedAgent } from "./agent.selector"; + +@Injectable({ + providedIn: "root", +}) +export class AgentFacade { + private readonly _store: Store = inject(Store); + + private readonly _availableAgents: Signal = this._store.selectSignal(selectAvailableAgents); + public get availableAgents(): Signal { + return this._availableAgents; + } + + private readonly _selectedAgent: Signal = this._store.selectSignal(selectSelectedAgent); + public get selectedAgent(): Signal { + return this._selectedAgent; + } + + public dispatchListAvailableAgents(): void { + this._store.dispatch(listAvailableAgents()); + } + + public dispatchSelectAgent(agentId: string): void { + this._store.dispatch(selectAgent({ agentId })); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.reducer.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.reducer.ts new file mode 100644 index 0000000..193e0c0 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.reducer.ts @@ -0,0 +1,51 @@ +import { IoAiWeb } from "@interopio/ai-web"; +import { createReducer, on } from "@ngrx/store"; + +import { listAvailableAgents, listAvailableAgentsSuccess, listAvailableAgentsFailure, selectAgent } from "./agent.actions"; +import { LOADING_STATE } from "../../enums/loading-state.enum"; +import { LoadingType } from "../../types/loading.type"; + +export type AgentReducerStateType = { + availableAgents: IoAiWeb.Agents.Agent[]; + selectedAgent: IoAiWeb.Agents.Agent | null; + loadingState: LoadingType; +}; + +export const initialState: AgentReducerStateType = { + availableAgents: [], + selectedAgent: null, + loadingState: { type: LOADING_STATE.NOT_STARTED }, +}; + +export const agentReducer = createReducer( + initialState, + on( + listAvailableAgents, + (state): AgentReducerStateType => ({ + ...state, + loadingState: { type: LOADING_STATE.LOADING }, + }) + ), + on( + listAvailableAgentsSuccess, + (state, { agents }): AgentReducerStateType => ({ + ...state, + availableAgents: agents, + loadingState: { type: LOADING_STATE.SUCCESS }, + }) + ), + on( + listAvailableAgentsFailure, + (state, { error }): AgentReducerStateType => ({ + ...state, + loadingState: { type: LOADING_STATE.ERROR, message: error.message }, + }) + ), + on( + selectAgent, + (state, { agentId }): AgentReducerStateType => ({ + ...state, + selectedAgent: state.availableAgents.find((agent) => agent.id === agentId) || null, + }) + ) +); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.selector.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.selector.ts new file mode 100644 index 0000000..b9b5008 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.selector.ts @@ -0,0 +1,10 @@ +import { createFeatureSelector, createSelector } from "@ngrx/store"; + +import { AgentReducerStateType } from "./agent.reducer"; +import { LOADING_STATE } from "../../enums/loading-state.enum"; + +export const selectAgentStore = createFeatureSelector("agentStore"); + +export const selectAvailableAgents = createSelector(selectAgentStore, (state: AgentReducerStateType) => state.availableAgents); +export const selectSelectedAgent = createSelector(selectAgentStore, (state: AgentReducerStateType) => state.selectedAgent); +export const selectIsLoadingAgents = createSelector(selectAgentStore, (state: AgentReducerStateType) => state.loadingState.type === LOADING_STATE.LOADING); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.actions.enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.actions.enum.ts new file mode 100644 index 0000000..7e962f4 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.actions.enum.ts @@ -0,0 +1,5 @@ +export enum APP_LIFECYCLE_ACTIONS { + INITIAL_APP_CORE_SERVICES_INIT = "[App Lifecycle] Initial App Core Services Init", + INITIAL_APP_CORE_SERVICES_INIT_SUCCESS = "[App Lifecycle] Initial App Core Services Init Success", + INITIAL_APP_CORE_SERVICES_INIT_FAILURE = "[App Lifecycle] Initial App Core Services Init Failure", +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.actions.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.actions.ts new file mode 100644 index 0000000..275146b --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.actions.ts @@ -0,0 +1,7 @@ +import { createAction, props } from "@ngrx/store"; + +import { APP_LIFECYCLE_ACTIONS } from "./app-lifecycle.actions.enum"; + +export const initialAppCoreServicesInit = createAction(APP_LIFECYCLE_ACTIONS.INITIAL_APP_CORE_SERVICES_INIT); +export const initialAppCoreServicesInitSuccess = createAction(APP_LIFECYCLE_ACTIONS.INITIAL_APP_CORE_SERVICES_INIT_SUCCESS); +export const initialAppCoreServicesInitFailure = createAction(APP_LIFECYCLE_ACTIONS.INITIAL_APP_CORE_SERVICES_INIT_FAILURE, props<{ error: Error }>()); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.effects.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.effects.ts new file mode 100644 index 0000000..0018ee3 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.effects.ts @@ -0,0 +1,80 @@ +import { inject, Injectable, Signal } from "@angular/core"; +import { toObservable } from "@angular/core/rxjs-interop"; +import { Actions, createEffect, ofType } from "@ngrx/effects"; +import { Store } from "@ngrx/store"; +import { from, merge, of, catchError, filter, map, switchMap, take } from "rxjs"; + +import { initialAppCoreServicesInit, initialAppCoreServicesInitFailure, initialAppCoreServicesInitSuccess } from "./app-lifecycle.actions"; +import { IOAiWebService } from "../../services/io-ai-web/io-ai-web.service"; +import { AgentFacade } from "../agent/agent.facade"; +import { PromptFacade } from "../prompt/prompt.facade"; + +@Injectable() +export class AppLifecycleEffects { + private readonly _actions$: Actions = inject(Actions); + private readonly _store: Store = inject(Store); + + private readonly _ioIntelWebService: IOAiWebService = inject(IOAiWebService); + + private readonly _agentFacade: AgentFacade = inject(AgentFacade); + private readonly _promptFacade: PromptFacade = inject(PromptFacade); + + private readonly _isIoIntelWebAvailable: Signal = this._ioIntelWebService.isInitialized; + private readonly _isIoIntelWebInitializing: Signal = this._ioIntelWebService.isInitializing; + private readonly _ioIntelWebInitializeError: Signal = this._ioIntelWebService.isError; + + private readonly _initialized$ = toObservable(this._isIoIntelWebAvailable); + private readonly _error$ = toObservable(this._ioIntelWebInitializeError); + + watchIoIntelWebInit$ = createEffect(() => + merge( + this._error$.pipe( + filter(Boolean), + map((msg) => initialAppCoreServicesInitFailure({ error: new Error(msg) })) + ), + this._initialized$.pipe( + filter(Boolean), + map(() => initialAppCoreServicesInitSuccess()) + ) + ).pipe(take(1)) + ); + + initialAppCoreServicesInit$ = createEffect(() => + this._actions$.pipe( + ofType(initialAppCoreServicesInit), + switchMap(() => { + if (this._isIoIntelWebAvailable()) { + return of(initialAppCoreServicesInitSuccess()); + } + + if (this._isIoIntelWebInitializing()) { + // Already initializing — watchIoIntelWebInit$ will dispatch + // success/failure when the state changes. + return []; + } + + return from(this._ioIntelWebService.initialize()).pipe( + // No action dispatched here, wait for initialization effect + switchMap(() => []), + // Swallow the error — watchIoIntelWebInit$ dispatches + // the failure and we do not want double dispatch + catchError(() => []) + ); + }) + ) + ); + + initialAppCoreServicesInitSuccess$ = createEffect( + () => + this._actions$.pipe( + ofType(initialAppCoreServicesInitSuccess), + map(() => { + // Dispatch actions needed after initialized IoAiWeb API + this._agentFacade.dispatchListAvailableAgents(); + this._promptFacade.dispatchParsePromptsConfig(); + // this._xxxFacade.dispatchListXXX(); + }) + ), + { dispatch: false } + ); +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.facade.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.facade.ts new file mode 100644 index 0000000..9037c08 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.facade.ts @@ -0,0 +1,43 @@ +import { computed, inject, Injectable, Signal } from "@angular/core"; +import { Store } from "@ngrx/store"; + +import { initialAppCoreServicesInit } from "./app-lifecycle.actions"; +import { selectAppCoreServicesLoadingState, selectIsAppCoreServicesStarted, selectIsPendingState } from "./app-lifecycle.selector"; +import { LOADING_STATE } from "../../enums/loading-state.enum"; +import { LoadingType } from "../../types/loading.type"; + +@Injectable({ + providedIn: "root", +}) +export class AppLifecycleFacade { + private readonly store: Store = inject(Store); + + private readonly _isAppCoreServicesStarted: Signal = this.store.selectSignal(selectIsAppCoreServicesStarted); + public get isAppCoreServicesStarted(): Signal { + return this._isAppCoreServicesStarted; + } + + private readonly _appCoreServicesLoadingState: Signal = this.store.selectSignal(selectAppCoreServicesLoadingState); + public get appCoreServicesLoadingState(): Signal { + return this._appCoreServicesLoadingState; + } + + private readonly _isPendingAppCoreServicesOperation: Signal = this.store.selectSignal(selectIsPendingState); + public get isPendingAppCoreServicesOperation(): Signal { + return this._isPendingAppCoreServicesOperation; + } + + private readonly _appCoreServicesErrorMessage: Signal = computed(() => { + const loadingState: LoadingType = this._appCoreServicesLoadingState(); + + return loadingState.type === LOADING_STATE.ERROR ? loadingState.message : null; + }); + + public get appCoreServicesErrorMessage(): Signal { + return this._appCoreServicesErrorMessage; + } + + public dispatchInitAppCoreServices(): void { + return this.store.dispatch(initialAppCoreServicesInit()); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.reducer.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.reducer.ts new file mode 100644 index 0000000..c146d67 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.reducer.ts @@ -0,0 +1,42 @@ +import { createReducer, on } from "@ngrx/store"; + +import { initialAppCoreServicesInit, initialAppCoreServicesInitFailure, initialAppCoreServicesInitSuccess } from "./app-lifecycle.actions"; +import { LOADING_STATE } from "../../enums/loading-state.enum"; +import { LoadingType } from "../../types/loading.type"; + +export type AppLifecycleStateType = { + isAppCoreServicesStarted: boolean; + loadingState: LoadingType; +}; + +export const initialState: AppLifecycleStateType = { + isAppCoreServicesStarted: false, + loadingState: { type: LOADING_STATE.NOT_STARTED }, +}; + +export const appLifecycleReducer = createReducer( + initialState, + on(initialAppCoreServicesInit, (state) => { + return { + ...state, + loadingState: { type: LOADING_STATE.LOADING as typeof LOADING_STATE.LOADING }, + }; + }), + on(initialAppCoreServicesInitSuccess, (state) => { + return { + ...state, + isAppCoreServicesStarted: true, + loadingState: { type: LOADING_STATE.SUCCESS as typeof LOADING_STATE.SUCCESS }, + }; + }), + on(initialAppCoreServicesInitFailure, (state, { error }) => { + return { + ...state, + isAppCoreServicesStarted: false, + loadingState: { + type: LOADING_STATE.ERROR as typeof LOADING_STATE.ERROR, + message: error.message, + }, + }; + }) +); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.selector.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.selector.ts new file mode 100644 index 0000000..0774f77 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.selector.ts @@ -0,0 +1,12 @@ +import { createFeatureSelector, createSelector } from "@ngrx/store"; + +import { AppLifecycleStateType } from "./app-lifecycle.reducer"; +import { LOADING_STATE } from "../../enums/loading-state.enum"; + +export const selectAppLifecycleStore = createFeatureSelector("appLifecycleStore"); + +export const selectIsAppCoreServicesStarted = createSelector(selectAppLifecycleStore, (state: AppLifecycleStateType) => state.isAppCoreServicesStarted); + +export const selectAppCoreServicesLoadingState = createSelector(selectAppLifecycleStore, (state: AppLifecycleStateType) => state.loadingState); + +export const selectIsPendingState = createSelector(selectAppLifecycleStore, (state: AppLifecycleStateType) => state.loadingState.type === LOADING_STATE.LOADING); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app.state.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app.state.ts new file mode 100644 index 0000000..4d8a563 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app.state.ts @@ -0,0 +1,40 @@ +import { AgentEffects } from "./agent/agent.effects"; +import { AgentReducerStateType, agentReducer } from "./agent/agent.reducer"; +import { AppLifecycleEffects } from "./app-lifecycle/app-lifecycle.effects"; +import { appLifecycleReducer, AppLifecycleStateType } from "./app-lifecycle/app-lifecycle.reducer"; +import { MessageEffects } from "./message/message.effects"; +import { messageReducer, MessageReducerStateType } from "./message/message.reducer"; +import { PromptEffects } from "./prompt/prompt.effects"; +import { PromptReducerStateType } from "./prompt/prompt.reducer"; +import { promptReducer } from "./prompt/prompt.reducer"; +import { responseStreamReducer, ResponseStreamReducerStateType } from "./response-stream/response-stream.reducer"; +import { ThreadEffects } from "./thread/thread.effects"; +import { threadReducer, ThreadReducerStateType } from "./thread/thread.reducer"; +import { ToolEffects } from "./tool/tool.effects"; +import { toolReducer, ToolReducerStateType } from "./tool/tool.reducer"; +import { WorkingContextEffects } from "./working-context/working-context.effects"; +import { workingContextReducer, WorkingContextReducerStateType } from "./working-context/working-context.reducer"; + +export type AppState = { + threadsStore: ThreadReducerStateType; + appLifecycleStore: AppLifecycleStateType; + promptStore: PromptReducerStateType; + toolStore: ToolReducerStateType; + messageStore: MessageReducerStateType; + responseStreamStore: ResponseStreamReducerStateType; + agentStore: AgentReducerStateType; + workingContextStore: WorkingContextReducerStateType; +}; + +export const appReducers = { + threadsStore: threadReducer, + appLifecycleStore: appLifecycleReducer, + promptStore: promptReducer, + toolStore: toolReducer, + messageStore: messageReducer, + responseStreamStore: responseStreamReducer, + agentStore: agentReducer, + workingContextStore: workingContextReducer, +}; + +export const appEffects: unknown[] = [ThreadEffects, AppLifecycleEffects, PromptEffects, ToolEffects, MessageEffects, AgentEffects, WorkingContextEffects]; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.actions.enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.actions.enum.ts new file mode 100644 index 0000000..62ec4d8 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.actions.enum.ts @@ -0,0 +1,19 @@ +export enum MESSAGE_ACTIONS { + GET_RESPONSE = "[Message] Get Response", + GET_RESPONSE_SUCCESS = "[Message] Get Response Success", + GET_RESPONSE_FAILURE = "[Message] Get Response Failure", + RELOAD_RESPONSE = "[Message] Reload Response", + CLEAR_MESSAGES = "[Message] Clear Messages", + FETCH_MESSAGES_FROM_THREAD = "[Message] Fetch Messages From Thread", + FETCH_MESSAGES_FROM_THREAD_SUCCESS = "[Message] Fetch Messages From Thread Success", + FETCH_MESSAGES_FROM_THREAD_FAILURE = "[Message] Fetch Messages From Thread Failure", + MERGE_ACCUMULATED_STREAM_CONTENT = "[Message] Merge Accumulated Stream Content", + ADD_USER_MESSAGE = "[Message] Add User Message", + ADD_ASSISTANT_MESSAGE = "[Message] Add Assistant Message", + ADD_TOOL_CALL = "[Message] Add Tool Call", + UPDATE_TOOL_CALL_ARGS = "[Message] Update Tool Call Args", + ADD_TOOL_RESULT = "[Message] Add Tool Result", + ABORT_RESPONSE_GENERATION = "[Message] Abort Response Generation", + TOGGLE_TOOL_TRACE = "[Message] Toggle Tool Trace", + TOGGLE_TOOL_MESSAGE = "[Message] Toggle Tool Message", +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.actions.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.actions.ts new file mode 100644 index 0000000..8a0a105 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.actions.ts @@ -0,0 +1,68 @@ +import { IoAiWeb } from "@interopio/ai-web"; +import { createAction, props } from "@ngrx/store"; + +import { MESSAGE_ACTIONS } from "./message.actions.enum"; +import { GetResponseParams, ToolResult, UIAssistantMessage, UIMessage, UIResponseMessage, UIToolMessage, UIUserMessage } from "./types"; +import { UIThread } from "../thread/types"; + +export const getResponse = createAction( + MESSAGE_ACTIONS.GET_RESPONSE, + props<{ + params: GetResponseParams; + isStream?: boolean; + agent?: IoAiWeb.Agents.Agent; + threadId: string; + }>() +); +export const getResponseSuccess = createAction(MESSAGE_ACTIONS.GET_RESPONSE_SUCCESS, props<{ threadId: string }>()); +export const getResponseFailure = createAction(MESSAGE_ACTIONS.GET_RESPONSE_FAILURE, props<{ error: Error; threadId: string }>()); + +export const reloadResponse = createAction( + MESSAGE_ACTIONS.RELOAD_RESPONSE, + props<{ + params: GetResponseParams; + isStream?: boolean; + agent?: IoAiWeb.Agents.Agent; + threadId: string; + }>() +); + +export const fetchMessagesFromThread = createAction(MESSAGE_ACTIONS.FETCH_MESSAGES_FROM_THREAD, props<{ thread: UIThread }>()); + +export const fetchMessagesFromThreadSuccess = createAction(MESSAGE_ACTIONS.FETCH_MESSAGES_FROM_THREAD_SUCCESS, props<{ messages: UIResponseMessage[]; threadId: string }>()); + +export const fetchMessagesFromThreadFailure = createAction(MESSAGE_ACTIONS.FETCH_MESSAGES_FROM_THREAD_FAILURE, props<{ error: Error }>()); + +/** + * Action to merge accumulated stream content when switching to a streaming thread. + * This ensures content streamed while the user was on another thread is not lost. + * Includes user message (not yet in DB) and tool messages. + */ +export const mergeAccumulatedStreamContent = createAction( + MESSAGE_ACTIONS.MERGE_ACCUMULATED_STREAM_CONTENT, + props<{ + content: string; + messageId: string | null; + threadId: string; + userMessage: UIMessage | null; + toolMessages: UIToolMessage[]; + }>() +); + +export const addUserMessage = createAction(MESSAGE_ACTIONS.ADD_USER_MESSAGE, props<{ message: UIUserMessage }>()); + +export const addAssistantMessage = createAction(MESSAGE_ACTIONS.ADD_ASSISTANT_MESSAGE, props<{ message: UIAssistantMessage; threadId: string }>()); + +export const addToolCall = createAction(MESSAGE_ACTIONS.ADD_TOOL_CALL, props<{ message: UIToolMessage; threadId: string }>()); + +export const updateToolCallArgs = createAction(MESSAGE_ACTIONS.UPDATE_TOOL_CALL_ARGS, props<{ toolCallId: string; args: Record; threadId: string }>()); + +export const addToolResult = createAction(MESSAGE_ACTIONS.ADD_TOOL_RESULT, props<{ toolCallId: string; result: ToolResult; threadId: string }>()); + +export const abortResponseGeneration = createAction(MESSAGE_ACTIONS.ABORT_RESPONSE_GENERATION, props<{ threadId: string }>()); + +export const clearMessages = createAction(MESSAGE_ACTIONS.CLEAR_MESSAGES); + +export const toggleToolTrace = createAction(MESSAGE_ACTIONS.TOGGLE_TOOL_TRACE, props<{ stateForMessageId: string }>()); + +export const toggleToolMessage = createAction(MESSAGE_ACTIONS.TOGGLE_TOOL_MESSAGE, props<{ toolMessageId: string; parentToolTraceId: string }>()); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.effects.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.effects.ts new file mode 100644 index 0000000..f9f0d37 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.effects.ts @@ -0,0 +1,229 @@ +import { inject, Injectable, Signal } from "@angular/core"; +import { IoAiWeb } from "@interopio/ai-web"; +import { Actions, createEffect, ofType } from "@ngrx/effects"; +import { Store } from "@ngrx/store"; +import { mergeMap, catchError, of, from, map, tap, Observable, switchMap, EMPTY } from "rxjs"; + +import { + abortResponseGeneration, + fetchMessagesFromThread, + fetchMessagesFromThreadFailure, + fetchMessagesFromThreadSuccess, + getResponse, + getResponseFailure, + mergeAccumulatedStreamContent, + reloadResponse, +} from "./message.actions"; +import { handleResponseStream } from "./message.utils"; +import { UIMessage, UIResponseMessage, UIToolMessage, UI_MESSAGE_ROLES } from "./types"; +import { AgentService } from "../../services/agent/agent.service"; +import { LoggerService } from "../../services/logger/logger.service"; +import { McpAppsService } from "../../services/mcp-apps/mcp-apps.service"; +import { ThreadService } from "../../services/thread/thread.service"; +import { abortThreadStream, completeThreadStream, failThreadStream, startThreadStream } from "../response-stream/response-stream.actions"; +import { selectIsThreadStreaming, selectStreamContentForThread } from "../response-stream/response-stream.selector"; +import { changeActiveThread } from "../thread/thread.actions"; +import { selectActiveThreadId } from "../thread/thread.selector"; + +@Injectable() +export class MessageEffects { + private readonly _actions$: Actions = inject(Actions); + private readonly _agentService: AgentService = inject(AgentService); + private readonly _threadService: ThreadService = inject(ThreadService); + private readonly _store: Store = inject(Store); + private readonly _logger: LoggerService = inject(LoggerService); + protected readonly LOGGER_NAME: string = "MessageEffects"; + private readonly _mcpAppsService: McpAppsService = inject(McpAppsService); + + private readonly _activeThreadId: Signal = this._store.selectSignal(selectActiveThreadId); + + /** + * When the user switches to any thread (including a new/empty one), clear the + * tracked app instances and close all active proxy windows immediately. + * This prevents stale workspace windows from persisting during the async + * fetch that follows, and ensures windows stuck in the loading state (proxy- + * ready not yet fired) are also closed via the safety-net closeAll(). + */ + clearMcpAppsOnThreadChange$ = createEffect( + () => + this._actions$.pipe( + ofType(changeActiveThread), + switchMap(() => { + this._mcpAppsService.resetInstances(); + + return from(this._mcpAppsService.closeAll()).pipe(catchError(() => EMPTY)); + }) + ), + { dispatch: false } + ); + + /** + * When messages are fetched for an existing thread, atomically close all + * existing MCP App instances and create new ones for the fetched thread. + * Uses replaceAll() to prevent race conditions from rapid thread switching — + * switchMap cancels in-flight operations, and replaceAll starts with a clean slate. + */ + createMcpAppsOnThreadFetch$ = createEffect( + () => + this._actions$.pipe( + ofType(fetchMessagesFromThreadSuccess), + switchMap(({ messages, threadId }) => { + const apps = messages + .filter((m): m is UIToolMessage => m.role === UI_MESSAGE_ROLES.TOOL) + .map((m) => ({ + toolCallId: m.id, + toolName: m.toolName, + toolInput: m.args, + toolResult: m.result, + })); + + return from(apps.length > 0 ? this._mcpAppsService.recreate({ threadId, apps }) : this._mcpAppsService.closeAll()).pipe(catchError(() => EMPTY)); + }) + ), + { dispatch: false } + ); + + getResponse$ = createEffect(() => + this._actions$.pipe( + ofType(getResponse), + tap(({ params, threadId }) => this._dispatchStreamForThread(params, threadId, "getResponse")), + mergeMap(({ params, isStream, agent, threadId }) => + from(this._agentService.getResponse(params, isStream, agent)).pipe( + mergeMap((responseStream) => this._getResponseStreamHandler(responseStream, threadId)), + catchError((error: Error) => this._handleStreamError(error, threadId)) + ) + ) + ) + ); + + reloadResponse$ = createEffect(() => + this._actions$.pipe( + ofType(reloadResponse), + tap(({ params, threadId }) => this._dispatchStreamForThread(params, threadId, "reloadResponse")), + mergeMap(({ params, isStream, agent, threadId }) => + from(this._agentService.reloadResponse(params, isStream, agent)).pipe( + mergeMap((responseStream) => this._getResponseStreamHandler(responseStream, threadId)), + catchError((error: Error) => this._handleStreamError(error, threadId)) + ) + ) + ) + ); + + fetchMessagesFromThread$ = createEffect(() => + this._actions$.pipe( + ofType(fetchMessagesFromThread), + switchMap(({ thread }) => + from(this._threadService.fetchMessagesFromThread(thread)).pipe( + map((messages: UIResponseMessage[]) => { + return fetchMessagesFromThreadSuccess({ messages, threadId: thread.id }); + }), + catchError((error: Error) => of(fetchMessagesFromThreadFailure({ error }))) + ) + ) + ) + ); + + /** + * Effect to merge accumulated stream content after fetching messages. + * This ensures content streamed while viewing another thread is preserved. + * Includes user message, tool messages, and assistant content. + */ + mergeAccumulatedContent$ = createEffect(() => + this._actions$.pipe( + ofType(fetchMessagesFromThreadSuccess), + mergeMap(({ threadId }) => { + // Check if this thread is currently streaming + const isStreaming: boolean = this._store.selectSignal(selectIsThreadStreaming(threadId))(); + + if (!isStreaming) { + return []; + } + + // Get accumulated content for this thread + const streamContent = this._store.selectSignal(selectStreamContentForThread(threadId))(); + + return of( + mergeAccumulatedStreamContent({ + content: streamContent?.content ?? "", + messageId: streamContent?.messageId ?? null, + threadId, + userMessage: streamContent?.userMessage ?? null, + toolMessages: streamContent?.toolMessages ?? [], + }) + ); + }) + ) + ); + + abortResponseGeneration$ = createEffect( + () => + this._actions$.pipe( + ofType(abortResponseGeneration), + tap(({ threadId }) => { + this._store.dispatch(abortThreadStream({ threadId })); + this._agentService.abortOperation(threadId); + }) + ), + { dispatch: false } + ); + + /** Notify MCP apps that a response stream has started. */ + notifyMcpAppsStreamStart$ = createEffect( + () => + this._actions$.pipe( + ofType(startThreadStream), + tap(() => this._mcpAppsService.notifyPendingResponse(true)) + ), + { dispatch: false } + ); + + /** Notify MCP apps that a response stream has ended (completed, aborted, or failed). */ + notifyMcpAppsStreamEnd$ = createEffect( + () => + this._actions$.pipe( + ofType(completeThreadStream, abortThreadStream, failThreadStream), + tap(() => this._mcpAppsService.notifyPendingResponse(false)) + ), + { dispatch: false } + ); + + private _dispatchStreamForThread(params: { messages: unknown[] }, threadId: string, actionName: string): void { + const userMessages: UIMessage[] = params.messages as UIMessage[]; + const userMessage: UIMessage | null = userMessages.length > 0 ? userMessages[0] : null; + + if (!userMessage) { + this._logger.get(this.LOGGER_NAME).warn(`Something went wrong. No user message found in parameters for ${actionName} action.`); + return; + } + + this._store.dispatch(startThreadStream({ threadId, userMessage })); + } + + private _createShouldAbortCallback(threadId: string): () => boolean { + return () => { + const activeThreadId: string | null = this._activeThreadId(); + // Means streaming ongoing in background without user being on any thread (when he moves out from new thread while it streams) + const isNewThread: boolean = activeThreadId === null; + + return isNewThread || activeThreadId !== threadId; + }; + } + + private _getResponseStreamHandler( + runHandle: IoAiWeb.Agents.StreamResponse, + threadId: string + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Observable { + return handleResponseStream(runHandle, threadId, this._createShouldAbortCallback(threadId), this._logger.get(this.LOGGER_NAME)); + } + + private _handleStreamError(error: Error, threadId: string) { + this._store.dispatch( + failThreadStream({ + threadId, + errorMessage: error.message, + }) + ); + return of(getResponseFailure({ error, threadId })); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.facade.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.facade.ts new file mode 100644 index 0000000..0dd1a37 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.facade.ts @@ -0,0 +1,121 @@ +import { inject, Injectable, Signal } from "@angular/core"; +import { IoAiWeb } from "@interopio/ai-web"; +import { Store } from "@ngrx/store"; + +import { abortResponseGeneration, clearMessages, fetchMessagesFromThread, getResponse, reloadResponse, toggleToolMessage, toggleToolTrace } from "./message.actions"; +import { + selectAllMessages, + selectIsFetchedFromThreadSuccess, + selectIsGeneratingResponse, + selectIsLastResponseSuccess, + selectIsLastUserMessage, + selectIsLoadingMessages, + selectIsLoadingMessagesFromThread, + selectLastUserMessage, + selectLoadingErrorMessage, + selectMessageLength, + selectToolTraceState, +} from "./message.selector"; +import { GetResponseParams, ToolTraceState, UIMessage, UIUserMessage } from "./types"; +import { UIThread } from "../thread/types"; + +@Injectable() +export class MessageFacade { + private readonly _store: Store = inject(Store); + + private readonly _allMessages: Signal = this._store.selectSignal(selectAllMessages); + public get allMessages(): Signal { + return this._allMessages; + } + + private readonly _messageLength: Signal = this._store.selectSignal(selectMessageLength); + public get messageLength(): Signal { + return this._messageLength; + } + + private readonly _lastUserMessage: Signal = this._store.selectSignal(selectLastUserMessage); + public get lastUserMessage(): Signal { + return this._lastUserMessage; + } + + private readonly _isLastUserMessage: Signal = this._store.selectSignal(selectIsLastUserMessage); + public get isLastUserMessage(): Signal { + return this._isLastUserMessage; + } + + private readonly _isGeneratingResponse: Signal = this._store.selectSignal(selectIsGeneratingResponse); + public get isGeneratingResponse(): Signal { + return this._isGeneratingResponse; + } + + private readonly _isLoadingMessagesFromThread: Signal = this._store.selectSignal(selectIsLoadingMessagesFromThread); + public get isLoadingMessagesFromThread(): Signal { + return this._isLoadingMessagesFromThread; + } + + private readonly _isLoadingMessages = this._store.selectSignal(selectIsLoadingMessages); + public get isLoadingMessages(): Signal { + return this._isLoadingMessages; + } + + private readonly _isSuccessFetchedFromThread: Signal = this._store.selectSignal(selectIsFetchedFromThreadSuccess); + public get isSuccessFetchedFromThread(): Signal { + return this._isSuccessFetchedFromThread; + } + + private readonly _loadingErrorMessage: Signal = this._store.selectSignal(selectLoadingErrorMessage); + + public get loadingErrorMessage(): Signal { + return this._loadingErrorMessage; + } + + private readonly _toolTraceState = this._store.selectSignal(selectToolTraceState); + public get toolTraceState(): Signal { + return this._toolTraceState; + } + + private readonly _isLastResponseSuccess: Signal = this._store.selectSignal(selectIsLastResponseSuccess); + public get isLastResponseSuccess(): Signal { + return this._isLastResponseSuccess; + } + + public getMessageById(id: string): UIMessage { + const match: UIMessage | undefined = this._allMessages().find((m: UIMessage) => m.id === id); + + if (!match) throw new Error(`Unable to find matching message with id: ${id}`); + + return match; + } + + public dispatchGetResponse(params: GetResponseParams, threadId: string, isStream?: boolean, agent?: IoAiWeb.Agents.Agent): void { + this._store.dispatch(getResponse({ params, isStream, agent, threadId })); + } + + public dispatchReloadResponse(params: GetResponseParams, threadId: string | null, isStream?: boolean, agent?: IoAiWeb.Agents.Agent): void { + if (!threadId) { + throw new Error("Cannot reload response: threadId is null"); + } + + this._store.dispatch(reloadResponse({ params, isStream, agent, threadId })); + } + + public dispatchClearMessages(): void { + this._store.dispatch(clearMessages()); + } + + public dispatchFetchMessagesFromThread(thread: UIThread): void { + this._store.dispatch(fetchMessagesFromThread({ thread })); + } + + public dispatchAbortResponseGeneration(threadId: string): void { + this._store.dispatch(abortResponseGeneration({ threadId })); + } + + public dispatchToggleToolTrace(stateForMessageId: string): void { + this._store.dispatch(toggleToolTrace({ stateForMessageId })); + } + + public dispatchToggleToolMessage(toolMessageId: string, parentToolTraceId: string): void { + this._store.dispatch(toggleToolMessage({ toolMessageId, parentToolTraceId })); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.reducer.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.reducer.ts new file mode 100644 index 0000000..f31e8a9 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.reducer.ts @@ -0,0 +1,358 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { createReducer, on } from "@ngrx/store"; + +import { + abortResponseGeneration, + addAssistantMessage, + addToolCall, + addToolResult, + addUserMessage, + clearMessages, + fetchMessagesFromThread, + fetchMessagesFromThreadFailure, + fetchMessagesFromThreadSuccess, + getResponse, + getResponseFailure, + getResponseSuccess, + mergeAccumulatedStreamContent, + reloadResponse, + toggleToolMessage, + toggleToolTrace, + updateToolCallArgs, +} from "./message.actions"; +import { + addToolTraceCallEntry, + addToolTraceResultEntry, + handleToolTraceStateOnThreadFetch, + setDisplayFooterOnLastTrace, + updateLastToolTraceEntry, + updateMessageFields, + updateToolTraceCallEntryArgs, +} from "./message.utils"; +import { ToolTraceState, UIAssistantMessage, UIMessage, UIToolMessage, UI_MESSAGE_ROLES } from "./types"; +import { MESSAGES_LOADING_STATE } from "../../enums/loading-state.enum"; +import { MessagesLoadingType } from "../../types/loading.type"; +import { changeActiveThread } from "../thread/thread.actions"; + +export type MessageReducerStateType = { + messages: UIMessage[]; + loadingState: MessagesLoadingType; + toolTraceState: ToolTraceState[]; + /** The thread ID that the current messages belong to */ + currentThreadId: string | null; +}; + +export const initialState: MessageReducerStateType = { + messages: [], + loadingState: { type: MESSAGES_LOADING_STATE.NOT_STARTED }, + toolTraceState: [], + currentThreadId: null, +}; + +export const messageReducer = createReducer( + initialState, + + on(changeActiveThread, (state, { threadId }) => { + return { + ...state, + currentThreadId: threadId, + }; + }), + + on(getResponse, (state, { params: { messages } }) => { + const userQuery: string | string[] | UIMessage[] | any[] = messages; + + return { + ...state, + messages: [...state.messages, ...userQuery], + loadingState: { + type: MESSAGES_LOADING_STATE.LOADING_RESPONSE as typeof MESSAGES_LOADING_STATE.LOADING_RESPONSE, + }, + }; + }), + + on(getResponseSuccess, (state) => { + const updatedMessages: UIMessage[] = updateMessageFields(state.messages, true, true); + const updatedToolTraceState: ToolTraceState[] = updateLastToolTraceEntry(state.toolTraceState); + const finalToolTraceState: ToolTraceState[] = setDisplayFooterOnLastTrace(updatedToolTraceState, updatedMessages); + + return { + ...state, + messages: [...updatedMessages], + loadingState: { + type: MESSAGES_LOADING_STATE.GET_RESPONSE_SUCCESS as typeof MESSAGES_LOADING_STATE.GET_RESPONSE_SUCCESS, + }, + toolTraceState: finalToolTraceState, + }; + }), + + on(getResponseFailure, (state) => { + const errorMessage: string = "Error during response generation"; + + const updatedToolTraceState: ToolTraceState[] = updateLastToolTraceEntry(state.toolTraceState, true); + + return { + ...state, + loadingState: { + type: MESSAGES_LOADING_STATE.ERROR, + message: errorMessage, + }, + toolTraceState: updatedToolTraceState, + }; + }), + + on(reloadResponse, (state, { params: { messages } }) => { + const userQuery: string | string[] | UIMessage[] | any[] = messages; + + return { + ...state, + messages: [...state.messages, ...userQuery], + loadingState: { + type: MESSAGES_LOADING_STATE.LOADING_RESPONSE as typeof MESSAGES_LOADING_STATE.LOADING_RESPONSE, + }, + }; + }), + + on(clearMessages, (state) => { + return { + ...initialState, + currentThreadId: state.currentThreadId, + }; + }), + + on(fetchMessagesFromThread, (state) => { + return { + ...state, + messages: [], + loadingState: { + type: MESSAGES_LOADING_STATE.LOADING_FROM_THREAD as typeof MESSAGES_LOADING_STATE.LOADING_FROM_THREAD, + }, + }; + }), + + on(fetchMessagesFromThreadSuccess, (state, { messages }) => { + const updatedMessages: UIMessage[] = updateMessageFields(messages, true, true); + const updatedToolTraceState: ToolTraceState[] = handleToolTraceStateOnThreadFetch(updatedMessages); + + return { + ...state, + messages: [...updatedMessages], + loadingState: { + type: MESSAGES_LOADING_STATE.FETCH_MESSAGES_FROM_THREAD_SUCCESS as typeof MESSAGES_LOADING_STATE.FETCH_MESSAGES_FROM_THREAD_SUCCESS, + }, + toolTraceState: updatedToolTraceState, + }; + }), + + on(fetchMessagesFromThreadFailure, (state, { error }) => { + return { + ...state, + loadingState: { + type: MESSAGES_LOADING_STATE.ERROR, + message: error.message || "Failed to fetch messages from thread", + }, + }; + }), + + /** + * Merge accumulated stream content when switching to a streaming thread. + * This adds: + * - The user message that triggered the stream (not yet in DB) + * - Tool messages accumulated during streaming + * - Assistant message with accumulated content (or updates existing) + * + * Deduplication is critical here because: + * - DB messages may already contain some of these messages + * - Streaming will continue after merge, adding more content + */ + on(mergeAccumulatedStreamContent, (state, { content, messageId, userMessage, toolMessages }) => { + let updatedMessages = [...state.messages]; + let updatedToolTraceState = [...state.toolTraceState]; + + // Add user message if present and not already in messages + if (userMessage && !updatedMessages.some((m) => m.id === userMessage.id)) { + updatedMessages.push(userMessage); + } + + // Add tool messages that aren't already present + for (const toolMsg of toolMessages) { + if (!updatedMessages.some((m) => m.id === toolMsg.id)) { + updatedMessages.push(toolMsg); + updatedToolTraceState = addToolTraceCallEntry( + { + ...state, + messages: updatedMessages, + toolTraceState: updatedToolTraceState, + }, + toolMsg + ); + } + } + + // Handle assistant message with accumulated content + if (content && content.trim() !== "") { + // AG-UI provides stable server-assigned messageId via TEXT_MESSAGE_START, + // so we can match directly — no fallback search needed. + const existingAssistantIndex = updatedMessages.findIndex((m) => m.id === messageId && m.role === UI_MESSAGE_ROLES.ASSISTANT); + + if (existingAssistantIndex >= 0) { + updatedMessages = updatedMessages.map((msg, idx) => (idx === existingAssistantIndex ? { ...msg, content, isLastMessage: true, isNew: true } : msg)); + } else { + const accumulatedMessage: UIAssistantMessage = { + id: messageId ?? `accumulated-${Date.now()}`, + role: UI_MESSAGE_ROLES.ASSISTANT, + content: content, + isLastMessage: true, + isNew: true, + }; + updatedMessages.push(accumulatedMessage); + } + } + + if (updatedMessages.length === state.messages.length && updatedMessages === state.messages) { + return state; + } + + updatedMessages = updatedMessages.map((msg, idx) => ({ + ...msg, + isLastMessage: idx === updatedMessages.length - 1, + })); + + return { + ...state, + messages: updatedMessages, + toolTraceState: updatedToolTraceState, + loadingState: { + type: MESSAGES_LOADING_STATE.LOADING_RESPONSE as typeof MESSAGES_LOADING_STATE.LOADING_RESPONSE, + }, + }; + }), + + on(addUserMessage, (state, { message }) => { + return { + ...state, + messages: [...state.messages, message], + }; + }), + + on(addAssistantMessage, (state, { message }) => { + const updateExisting: boolean = state.messages.some((msg) => msg.id === message.id); + + if (updateExisting) { + return { + ...state, + messages: state.messages.map((msg) => (msg.id === message.id ? { ...msg, content: message.content } : msg)), + }; + } + + const updatedToolTraceState: ToolTraceState[] = updateLastToolTraceEntry(state.toolTraceState); + + return { + ...state, + messages: [...state.messages, message], + toolTraceState: updatedToolTraceState, + }; + }), + + on(addToolCall, (state, { message }) => { + const existingToolCall = state.messages.some((msg) => msg.id === message.id); + if (existingToolCall) { + return state; + } + + const updatedToolTraceState: ToolTraceState[] = addToolTraceCallEntry(state, message); + + return { + ...state, + messages: [...state.messages, message], + toolTraceState: updatedToolTraceState, + }; + }), + + on(updateToolCallArgs, (state, { toolCallId, args }) => { + const updatedToolTraceState: ToolTraceState[] = updateToolTraceCallEntryArgs(state.toolTraceState, toolCallId, args); + + return { + ...state, + messages: state.messages.map((msg) => (msg.id === toolCallId ? { ...msg, args: { ...(msg as UIToolMessage).args, ...args } } : msg)), + toolTraceState: updatedToolTraceState, + }; + }), + + on(addToolResult, (state, { toolCallId, result }) => { + const updatedToolTraceState: ToolTraceState[] = addToolTraceResultEntry(state.toolTraceState, toolCallId, result); + + return { + ...state, + messages: state.messages.map((msg) => (msg.id === toolCallId ? { ...msg, result } : msg)), + toolTraceState: updatedToolTraceState, + }; + }), + + on(abortResponseGeneration, (state) => { + const updatedMessages: UIMessage[] = updateMessageFields(state.messages, true, true); + const updatedToolTraceState: ToolTraceState[] = updateLastToolTraceEntry(state.toolTraceState, false, true); + const finalToolTraceState: ToolTraceState[] = setDisplayFooterOnLastTrace(updatedToolTraceState, updatedMessages); + + return { + ...state, + messages: [...updatedMessages], + loadingState: { + type: MESSAGES_LOADING_STATE.NOT_STARTED as typeof MESSAGES_LOADING_STATE.NOT_STARTED, + }, + toolTraceState: finalToolTraceState, + }; + }), + + on(toggleToolTrace, (state, { stateForMessageId }) => { + const updatedToolTraceState: ToolTraceState[] = state.toolTraceState.map((traceState: ToolTraceState) => { + if (traceState.stateForMessageId === stateForMessageId) { + return { + ...traceState, + isExpanded: !traceState.isExpanded, + }; + } + + return traceState; + }); + + return { + ...state, + toolTraceState: updatedToolTraceState, + }; + }), + + on(toggleToolMessage, (state, { toolMessageId, parentToolTraceId }) => { + const matchingToolTraceParent: ToolTraceState | undefined = state.toolTraceState.find((traceState: ToolTraceState) => traceState.stateForMessageId === parentToolTraceId); + + if (!matchingToolTraceParent) { + return state; + } + + const updatedExecutedTools = matchingToolTraceParent.executedTools.map((tool) => { + if (tool.id === toolMessageId) { + return { + ...tool, + isExpanded: !tool.isExpanded, + }; + } + + return tool; + }); + + const updatedToolTraceState: ToolTraceState[] = state.toolTraceState.map((traceState: ToolTraceState) => { + if (traceState.stateForMessageId === parentToolTraceId) { + return { + ...traceState, + executedTools: updatedExecutedTools, + }; + } + return traceState; + }); + + return { + ...state, + toolTraceState: updatedToolTraceState, + }; + }) +); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.selector.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.selector.ts new file mode 100644 index 0000000..26a678e --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.selector.ts @@ -0,0 +1,41 @@ +import { createFeatureSelector, createSelector } from "@ngrx/store"; + +import { MessageReducerStateType } from "./message.reducer"; +import { UI_MESSAGE_ROLES } from "./types"; +import { MESSAGES_LOADING_STATE } from "../../enums/loading-state.enum"; + +export const selectMessageStore = createFeatureSelector("messageStore"); + +export const selectAllMessages = createSelector(selectMessageStore, (state: MessageReducerStateType) => state.messages); + +export const selectMessageLength = createSelector(selectMessageStore, (state: MessageReducerStateType) => state.messages.length); + +export const selectLastUserMessage = createSelector(selectMessageStore, (state: MessageReducerStateType) => [...state.messages].reverse().find((message) => message.role === UI_MESSAGE_ROLES.USER)); + +export const selectIsLastUserMessage = createSelector( + selectMessageStore, + (state: MessageReducerStateType) => state.messages.length > 0 && state.messages[state.messages.length - 1].role === UI_MESSAGE_ROLES.USER +); + +export const selectIsGeneratingResponse = createSelector(selectMessageStore, (state: MessageReducerStateType) => state.loadingState.type === MESSAGES_LOADING_STATE.LOADING_RESPONSE); + +export const selectIsLoadingMessagesFromThread = createSelector(selectMessageStore, (state: MessageReducerStateType) => state.loadingState.type === MESSAGES_LOADING_STATE.LOADING_FROM_THREAD); + +// covers both loading states - loading from thread and loading response +export const selectIsLoadingMessages = createSelector( + selectMessageStore, + (state: MessageReducerStateType) => state.loadingState.type === MESSAGES_LOADING_STATE.LOADING_FROM_THREAD || state.loadingState.type === MESSAGES_LOADING_STATE.LOADING_RESPONSE +); + +export const selectIsFetchedFromThreadSuccess = createSelector( + selectMessageStore, + (state: MessageReducerStateType) => state.loadingState.type === MESSAGES_LOADING_STATE.FETCH_MESSAGES_FROM_THREAD_SUCCESS +); + +export const selectLoadingErrorMessage = createSelector(selectMessageStore, (state: MessageReducerStateType) => + state.loadingState.type === MESSAGES_LOADING_STATE.ERROR ? state.loadingState.message : undefined +); + +export const selectToolTraceState = createSelector(selectMessageStore, (state: MessageReducerStateType) => state.toolTraceState); + +export const selectIsLastResponseSuccess = createSelector(selectMessageStore, (state: MessageReducerStateType) => state.loadingState.type === MESSAGES_LOADING_STATE.GET_RESPONSE_SUCCESS); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.utils.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.utils.ts new file mode 100644 index 0000000..3028668 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.utils.ts @@ -0,0 +1,612 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ // TODO: Remove this line after fixing any 'any' types +import { IoAiWeb } from "@interopio/ai-web"; +import { IOConnectCore } from "@interopio/core"; +import { of, Observable } from "rxjs"; + +import { addAssistantMessage, addToolCall, addToolResult, getResponseFailure, getResponseSuccess, updateToolCallArgs } from "./message.actions"; +import { MessageReducerStateType } from "./message.reducer"; +import { TOOL_RESULT_TYPES, ToolResult, ToolTraceState, UI_MESSAGE_ROLES, UIMessage, UIToolMessage } from "./types"; +import { abortThreadStream, addStreamToolMessage, completeThreadStream, updateStreamContent } from "../response-stream/response-stream.actions"; + +type ToolCallStreamState = { + args: string; + toolName?: string; +}; + +/** + * Processes AG-UI events from a StreamResponse (RunHandle) and converts them into Redux actions. + * Thread-aware - each action includes the threadId for proper filtering in reducer. + * Also emits stream management actions for response-stream state. + * + * Key behaviors: + * - TEXT_MESSAGE_START: Creates assistant message placeholder (active thread) or skips (background) + * - TEXT_MESSAGE_CONTENT: Accumulates and dispatches full content (reducer expects full replacement) + * - TOOL_CALL_START: Creates tool message with empty args + * - TOOL_CALL_ARGS: Accumulates streaming JSON deltas for tool arguments + * - TOOL_CALL_END: Parses accumulated args and updates tool message + * - TOOL_CALL_RESULT: Dispatches tool result + * - abort(): Signals stream cancellation without treating it as success + * - complete(): Signals stream completion with getResponseSuccess + * - error(): Signals stream failure + * + * Completion is signaled only via the subscribe complete() callback, NOT on RUN_FINISHED events, + * because RUN_FINISHED is intermediate during multi-turn tool loops. + * + * @param runHandle - AG-UI StreamResponse with subscribe/abort methods + * @param threadId - The thread ID this stream belongs to + * @param isStreamingOnBackgroundThread - Function that returns true if streaming on a background thread + * @returns Observable emitting Redux actions for message state and stream state updates + */ +export const handleResponseStream = (runHandle: IoAiWeb.Agents.StreamResponse, threadId: string, isStreamingOnBackgroundThread: () => boolean, logger: IOConnectCore.Logger.API): Observable => { + if (!runHandle || typeof runHandle.subscribe !== "function") { + logger.error(`Invalid response object: ${JSON.stringify(runHandle)}`); + return of(getResponseFailure({ error: new Error("Invalid response object"), threadId })); + } + + return new Observable((observer) => { + let streamedContent = ""; + let currentMessageId = ""; + // Keep stream accumulation state scoped to this subscription. + const toolCallArgsMap = new Map(); + + const subscription = runHandle.subscribe({ + next: (event: IoAiWeb.Agents.StreamEvent) => { + switch (event.type) { + case "TEXT_MESSAGE_START": { + currentMessageId = (event as any).messageId || crypto.randomUUID(); + streamedContent = ""; + + if (!isStreamingOnBackgroundThread()) { + observer.next( + addAssistantMessage({ + message: { + id: currentMessageId, + role: UI_MESSAGE_ROLES.ASSISTANT, + content: "", + }, + threadId, + }) + ); + } + break; + } + + case "TEXT_MESSAGE_CONTENT": { + streamedContent += (event as any).delta || ""; + + if (isStreamingOnBackgroundThread()) { + observer.next( + updateStreamContent({ + threadId, + content: streamedContent, + messageId: currentMessageId, + }) + ); + return; + } + + observer.next( + addAssistantMessage({ + message: { + id: currentMessageId, + role: UI_MESSAGE_ROLES.ASSISTANT, + content: streamedContent, + }, + threadId, + }) + ); + break; + } + + case "TEXT_MESSAGE_END": + break; + + case "TOOL_CALL_START": { + // Text segment ended, reset content tracking + streamedContent = ""; + + const toolMessage: UIToolMessage = { + id: (event as any).toolCallId || crypto.randomUUID(), + toolName: (event as any).toolCallName || "unknown", + args: {}, + role: UI_MESSAGE_ROLES.TOOL, + }; + toolCallArgsMap.set(toolMessage.id, { + args: "", + toolName: toolMessage.toolName, + }); + + if (isStreamingOnBackgroundThread()) { + observer.next( + addStreamToolMessage({ + threadId, + toolMessage, + }) + ); + return; + } + + observer.next( + addToolCall({ + message: toolMessage, + threadId, + }) + ); + break; + } + + case "TOOL_CALL_ARGS": { + const toolCallId = (event as any).toolCallId || ""; + const delta = (event as any).delta; + + if (!toolCallId) { + logger.error("Something went wrong. Missing or empty tool call id."); + return; + } + + // Accumulate the args delta for this tool call + const currentToolCallState = toolCallArgsMap.get(toolCallId) || { + args: "", + }; + toolCallArgsMap.set(toolCallId, { + ...currentToolCallState, + args: currentToolCallState.args + delta, + }); + + break; + } + case "TOOL_CALL_END": { + const toolCallId = (event as any).toolCallId || ""; + + if (!toolCallId) { + logger.error("Something went wrong. Missing or empty tool call id."); + return; + } + + // Parse accumulated args and dispatch update + const toolCallState = toolCallArgsMap.get(toolCallId); + const accumulatedArgsStr = toolCallState?.args || "{}"; + const toolName = toolCallState?.toolName || "unknown"; + let parsedArgs: Record = {}; + + try { + parsedArgs = JSON.parse(accumulatedArgsStr); + } catch (err) { + logger.error(`Failed to parse tool call args: ${accumulatedArgsStr}`, err instanceof Error ? err : new Error("Unknown error")); + } + + // Clean up the map + toolCallArgsMap.delete(toolCallId); + + if (isStreamingOnBackgroundThread()) { + // For background threads, update the tool message in stream state + observer.next( + addStreamToolMessage({ + threadId, + toolMessage: { + id: toolCallId, + toolName, + args: parsedArgs, + role: UI_MESSAGE_ROLES.TOOL, + }, + }) + ); + return; + } + + // Dispatch action to update the tool message with parsed args + observer.next( + updateToolCallArgs({ + toolCallId, + args: parsedArgs, + threadId, + }) + ); + break; + } + + case "TOOL_CALL_RESULT": { + const toolCallId = (event as any).toolCallId || ""; + let result: any = (event as any).content; + + if (!toolCallId) { + logger.error("Something went wrong. Missing or empty tool call id."); + + return; + } + + if (typeof result === "string") { + try { + result = JSON.parse(result); + } catch (err) { + logger.error(`Failed to parse tool call result: ${result}`, err instanceof Error ? err : new Error("Unknown error")); + + result = { + type: TOOL_RESULT_TYPES.TEXT, + text: result, + }; + } + } + + if (isStreamingOnBackgroundThread()) { + const toolName = toolCallArgsMap.get(toolCallId)?.toolName || "unknown"; + observer.next( + addStreamToolMessage({ + threadId, + toolMessage: { + id: toolCallId, + toolName, + args: {}, + result, + role: UI_MESSAGE_ROLES.TOOL, + }, + }) + ); + return; + } + + observer.next( + addToolResult({ + toolCallId, + result, + threadId, + }) + ); + break; + } + + case "RUN_STARTED": + case "RUN_FINISHED": + case "STEP_STARTED": + case "STEP_FINISHED": + break; + + case "RUN_ERROR": + logger.error("Run error:", (event as any).message); + break; + + default: + logger.warn(`Unknown AG-UI event type: ${event.type}`); + break; + } + }, + + error: (err: Error) => { + logger.error("Stream error:", err); + + if (isStreamingOnBackgroundThread()) { + observer.next( + completeThreadStream({ + threadId, + shouldNotify: true, + }) + ); + } else { + observer.next( + getResponseFailure({ + error: err, + threadId, + }) + ); + } + + observer.complete(); + }, + + abort: () => { + observer.next(abortThreadStream({ threadId })); + observer.complete(); + }, + + complete: () => { + observer.next( + completeThreadStream({ + threadId, + shouldNotify: isStreamingOnBackgroundThread(), + }) + ); + + observer.next(getResponseSuccess({ threadId })); + + observer.complete(); + }, + }); + + // Teardown: unsubscribe from AG-UI stream when Observable is unsubscribed + return () => { + toolCallArgsMap.clear(); + subscription.unsubscribe(); + }; + }); +}; + +/** + * Utility function to construct message state when messages come in bulk. + * Usually when fetching messages from a thread. + * + * @param messages - Array of UIMessage to update fields for + * @param shouldSetDisplayFooter - Whether to set the displayFooter field + * @param shouldSetResponseForUserQueryId - Whether to set the responseForUserQueryId field + * + * @returns Updated array of UIMessage + */ +export const updateMessageFields = (messages: UIMessage[], shouldSetDisplayFooter: boolean, shouldSetResponseForUserQueryId?: boolean): UIMessage[] => { + let lastUserMessageId: string | undefined = undefined; + + return messages.map((msg: UIMessage, msgIndex: number) => { + if (msg.role === UI_MESSAGE_ROLES.USER) { + lastUserMessageId = msg.id; + } + + const nextMessage: UIMessage | undefined = messages[msgIndex + 1]; + + const isLastMessage: boolean = msgIndex === messages.length - 1; + const isNextMessageUser: boolean = !!nextMessage && nextMessage.role === UI_MESSAGE_ROLES.USER; + + return { + ...msg, + displayFooter: shouldSetDisplayFooter ? isLastMessage || isNextMessageUser : msg.displayFooter, + responseForUserQueryId: shouldSetResponseForUserQueryId ? lastUserMessageId : msg.responseForUserQueryId, + // Add other fields to update as needed + }; + }); +}; + +/** + * Function to get the response text for a given user message ID. + * + * A response is considered all messages that the user message has triggered, + * including assistant messages and tool messages until another user message is found. + * + * @param userQueryId - ID of the user message to get the response for + * @returns response text corresponding to the user message ID + */ +export const responseToText = (messages: UIMessage[], userQueryId: string): string => { + const stringResponse = messages + .filter((message) => message.role !== UI_MESSAGE_ROLES.USER && message.responseForUserQueryId === userQueryId) + .map((message) => { + switch (message.role) { + case UI_MESSAGE_ROLES.ASSISTANT: + return message.content || ""; + case UI_MESSAGE_ROLES.TOOL: + return `Tool (${message.toolName}) input: ${JSON.stringify(message.args || "")} result: ${JSON.stringify(message.result || "")}`; + default: + return ""; + } + }) + .join("\n"); + + return stringResponse; +}; + +/** + * We should also render the footer on responses that end with tool messages (tool-trace messages in our UI context) if that is the case. + */ +export const setDisplayFooterOnLastTrace = (toolTraceState: ToolTraceState[], messages: UIMessage[]): ToolTraceState[] => { + if (toolTraceState.length === 0 || messages.length === 0) { + return toolTraceState; + } + + const lastMessage = messages[messages.length - 1]; + + if (lastMessage.role !== UI_MESSAGE_ROLES.TOOL) { + return toolTraceState; + } + + return toolTraceState.map((trace, index) => { + if (index === toolTraceState.length - 1) { + return { ...trace, displayFooter: true }; + } + return trace; + }); +}; + +/** + * Function to add a tool trace call entry to the tool trace state array. + * + * @param state - Current MessageReducerStateType + * @param currentTool - The tool message to add + * + * @returns Updated array of ToolTraceState + */ +export const addToolTraceCallEntry = (state: MessageReducerStateType, currentTool: UIToolMessage): ToolTraceState[] => { + const messages: UIMessage[] = state.messages; + const toolTraceState: ToolTraceState[] = state.toolTraceState; + + // Find the last message that is not a tool message so we can link the tool trace to it + const reversedMessages: UIMessage[] = messages.slice().reverse(); + const lastMessage = reversedMessages.find((msg: UIMessage) => msg.role !== UI_MESSAGE_ROLES.TOOL); + + if (!lastMessage) { + // Normally we always have at least one non-tool message (the user message that triggered the tool calls) + return toolTraceState; + } + + const existingToolTraceIndex = state.toolTraceState.findIndex((state: ToolTraceState) => state.stateForMessageId === lastMessage?.id); + + if (existingToolTraceIndex !== -1) { + return state.toolTraceState.map((state, index) => { + const shouldUpdateExisting: boolean = index === existingToolTraceIndex; + + if (shouldUpdateExisting) { + return { + ...state, + executedTools: [...state.executedTools, currentTool], + uiMessage: `Called tool: ${currentTool.toolName}`, + }; + } + + return state; + }); + } + + const ownerUserMessageId = + messages + .slice(0, messages.indexOf(lastMessage) + 1) + .reverse() + .find((m) => m.role === UI_MESSAGE_ROLES.USER)?.id ?? ""; + + return toolTraceState.concat({ + stateForMessageId: lastMessage ? lastMessage.id : "", + executedTools: [currentTool], + uiMessage: `Called tool: ${currentTool.toolName}`, + responseForUserQueryId: ownerUserMessageId, + }); +}; + +/** + * Function to update tool call arguments in the tool trace state array. + * This is called on each args delta during streaming to update the tool's args. + * + * @param state - Current array of ToolTraceState + * @param toolCallId - ID of the tool call message + * @param args - Arguments delta to merge into the tool call + * + * @returns Updated array of ToolTraceState + */ +export const updateToolTraceCallEntryArgs = (state: ToolTraceState[], toolCallId: string, args: Record): ToolTraceState[] => { + return state.map((traceState: ToolTraceState) => { + const executedTools: UIToolMessage[] = traceState.executedTools; + + const matchingToolCall: UIToolMessage | undefined = executedTools.find((toolMsg: UIToolMessage) => toolMsg.id === toolCallId); + + if (!matchingToolCall) { + return traceState; + } + + const updatedToolCall = { + ...matchingToolCall, + args: { ...matchingToolCall.args, ...args }, + }; + + return { + ...traceState, + executedTools: executedTools.map((toolMsg: UIToolMessage) => (toolMsg.id === toolCallId ? updatedToolCall : toolMsg)), + }; + }); +}; + +/** + * Function to add a tool result to its corresponding tool call in the tool trace state array. + * + * @param state - Current array of ToolTraceState + * @param toolCallId - ID of the tool call message + * @param result - Result of the tool call + * + * @returns Updated array of ToolTraceState + */ +export const addToolTraceResultEntry = (state: ToolTraceState[], toolCallId: string, result: ToolResult): ToolTraceState[] => { + return state.map((traceState: ToolTraceState) => { + const executedTools: UIToolMessage[] = traceState.executedTools; + + const matchingToolCall: UIToolMessage | undefined = executedTools.find((toolMsg: UIToolMessage) => toolMsg.id === toolCallId); + + if (!matchingToolCall) { + return traceState; + } + + const updatedToolCall = { + ...matchingToolCall, + result, + }; + + return { + ...traceState, + executedTools: executedTools.map((toolMsg: UIToolMessage) => (toolMsg.id === toolCallId ? updatedToolCall : toolMsg)), + }; + }); +}; + +/*** + * Function to update the last tool trace entry in the tool trace state array. + * + * @param state - Current array of ToolTraceState + * @param isError - Whether the update is due to an error + * @param isAborted - Whether the update is due to an abortion + * + * @returns Updated array of ToolTraceState + */ +export const updateLastToolTraceEntry = (state: ToolTraceState[], isError?: boolean, isAborted?: boolean) => { + const lastToolTraceState: ToolTraceState | undefined = state.length > 0 ? state[state.length - 1] : undefined; + + if (!lastToolTraceState) { + return state; + } + + return state.map((traceState: ToolTraceState) => { + const usedToolsCount: number = lastToolTraceState.executedTools.length; + const usedTools: boolean = usedToolsCount > 0; + const isUpdatingCurrentTraceState: boolean = traceState.stateForMessageId === lastToolTraceState.stateForMessageId; + + if (!isUpdatingCurrentTraceState) { + return traceState; + } + + if (isError) { + return { + ...traceState, + uiMessage: "Tool tracing was interrupted due to error", + }; + } + + if (isAborted) { + return { + ...traceState, + uiMessage: usedTools ? `User aborted after using ${usedToolsCount} ${usedToolsCount === 1 ? "tool" : "tools"}` : "User aborted before using any tools", + }; + } + + return { + ...traceState, + uiMessage: usedTools ? `Used ${usedToolsCount} ${usedToolsCount === 1 ? "tool" : "tools"}` : "", + }; + }); +}; + +export const handleToolTraceStateOnThreadFetch = (fetchedMessages: UIMessage[]): ToolTraceState[] => { + const toolTraceStates: ToolTraceState[] = []; + + let traceState: ToolTraceState | null = null; + let validTraceId: string | null = null; + let lastUserMessageId: string | null = null; + + for (const msg of fetchedMessages) { + const isUserOrAssistant = msg.role === UI_MESSAGE_ROLES.USER || msg.role === UI_MESSAGE_ROLES.ASSISTANT; + + if (isUserOrAssistant && traceState) { + traceState.uiMessage = `Used ${traceState.executedTools.length} ${traceState.executedTools.length === 1 ? "tool" : "tools"}`; + + toolTraceStates.push(traceState); + traceState = null; + } + + if (isUserOrAssistant && !traceState) { + validTraceId = msg.id; + + if (msg.role === UI_MESSAGE_ROLES.USER) { + lastUserMessageId = msg.id; + } + + continue; + } + + if (msg.role === UI_MESSAGE_ROLES.TOOL) { + const executedTools: UIToolMessage[] = traceState && Array.isArray(traceState.executedTools) ? [...traceState.executedTools, msg as UIToolMessage] : [msg as UIToolMessage]; + + traceState = { + stateForMessageId: validTraceId || msg.responseForUserQueryId || msg.id, + executedTools, + uiMessage: "", + responseForUserQueryId: lastUserMessageId || msg.responseForUserQueryId || undefined, + }; + } + } + + // Push any remaining trace state (when messages end with tool messages, e.g., during streaming) + if (traceState) { + traceState.uiMessage = `Used ${traceState.executedTools.length} ${traceState.executedTools.length === 1 ? "tool" : "tools"}`; + toolTraceStates.push(traceState); + } + + return toolTraceStates; +}; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/types.ts new file mode 100644 index 0000000..fbce301 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/types.ts @@ -0,0 +1,99 @@ +export type GetResponseParams = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + messages: UIMessage[] | any[]; + instructions?: string; + system?: string; + memory?: { + thread: string; + resource: string; + }; + modelSettings?: { + maxTokens?: number; + temperature?: number; + topP?: number; + topK?: number; + maxRetries?: number; + }; + requestContext?: Record; + structuredOutput?: { + schema?: Record | string; + }; +}; + +export enum UI_MESSAGE_PART_TYPES { + TEXT = "text", + TOOL_INVOCATION = "tool-invocation", + STEP_START = "step-start", +} + +export enum UI_MESSAGE_ROLES { + USER = "user", + ASSISTANT = "assistant", + TOOL = "tool", +} + +export enum TOOL_RESULT_TYPES { + TEXT = "text", + IMAGE = "image", + AUDIO = "audio", +} + +export type UIMessageType = UI_MESSAGE_ROLES.USER | UI_MESSAGE_ROLES.ASSISTANT | UI_MESSAGE_ROLES.TOOL; + +export type UIMessageBase = { + id: string; + role: UIMessageType; + content?: string; + displayFooter?: boolean; + responseForUserQueryId?: string; + isLastMessage?: boolean; + isNew?: boolean; +}; + +export type UIUserMessage = UIMessageBase & { + role: UI_MESSAGE_ROLES.USER; +}; + +export type UIAssistantMessage = UIMessageBase & { + role: UI_MESSAGE_ROLES.ASSISTANT; +}; + +export type UIToolMessage = UIMessageBase & { + toolName: string; + role: UI_MESSAGE_ROLES.TOOL; + args?: Record; + result?: ToolResult | [ToolResult]; + isExpanded?: boolean; +}; + +export type ToolTextResult = { + type: TOOL_RESULT_TYPES.TEXT; + text: string; +}; + +export type ToolImageResult = { + type: TOOL_RESULT_TYPES.IMAGE; + data: string; // base64-encoded image data + mimeType: string; // TODO: more explicit mime type enum when we support multiple image types +}; + +export type ToolAudioResult = { + type: TOOL_RESULT_TYPES.AUDIO; + data: string; // base64-encoded audio data + mimeType: string; // TODO: more explicit mime type enum when we support audio +}; + +export type ToolTraceState = { + stateForMessageId: string; + executedTools: UIToolMessage[]; + uiMessage: string; + isExpanded?: boolean; + displayFooter?: boolean; + responseForUserQueryId?: string; +}; + +export type ToolResult = ToolTextResult | ToolImageResult | ToolAudioResult; + +export type UIResponseMessage = UIAssistantMessage | UIToolMessage | UIUserMessage; + +export type UIMessage = UIUserMessage | UIResponseMessage; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.actions.enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.actions.enum.ts new file mode 100644 index 0000000..dd62014 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.actions.enum.ts @@ -0,0 +1,8 @@ +export enum PROMPT_ACTIONS { + PARSE_PROMPTS_CONFIG = "[Prompt] Parse Prompts Config", + PARSE_PROMPTS_CONFIG_SUCCESS = "[Prompt] Parse Prompts Config Success", + PARSE_PROMPTS_CONFIG_FAILURE = "[Prompt] Parse Prompts Config Failure", + SELECT_PROMPT = "[Prompt] Select Prompt", + CLEAR_SELECTED_PROMPT = "[Prompt] Clear Selected Prompt", + TOGGLE_FAVORITE = "[Prompt] Toggle Favorite", +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.actions.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.actions.ts new file mode 100644 index 0000000..beef4a7 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.actions.ts @@ -0,0 +1,19 @@ +import { createAction, props } from "@ngrx/store"; + +import { PROMPT_ACTIONS } from "./prompt.actions.enum"; +import { UIPrompt } from "../../services/prompt/types"; + +// =========================================================================== Parse Config +export const parsePromptsConfig = createAction(PROMPT_ACTIONS.PARSE_PROMPTS_CONFIG); +export const parsePromptsConfigSuccess = createAction(PROMPT_ACTIONS.PARSE_PROMPTS_CONFIG_SUCCESS, props<{ prompts: UIPrompt[]; favorites: string[] }>()); +export const parsePromptsConfigFailure = createAction(PROMPT_ACTIONS.PARSE_PROMPTS_CONFIG_FAILURE, props<{ error: Error }>()); +export const selectPrompt = createAction(PROMPT_ACTIONS.SELECT_PROMPT, props<{ prompt: UIPrompt }>()); +export const clearSelectedPrompt = createAction(PROMPT_ACTIONS.CLEAR_SELECTED_PROMPT); + +// =========================================================================== Favorites +export const togglePromptFavorite = createAction( + PROMPT_ACTIONS.TOGGLE_FAVORITE, + props<{ + name: string; + }>() +); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.effects.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.effects.ts new file mode 100644 index 0000000..14a4bef --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.effects.ts @@ -0,0 +1,79 @@ +import { inject, Injectable } from "@angular/core"; +import { Actions, createEffect, ofType } from "@ngrx/effects"; +import { Store } from "@ngrx/store"; +import { mergeMap, map, of, catchError, from, forkJoin, EMPTY } from "rxjs"; + +import { parsePromptsConfig, parsePromptsConfigSuccess, selectPrompt, togglePromptFavorite } from "./prompt.actions"; +import { selectAllPrompts, selectFavoritePromptNames } from "./prompt.selector"; +import { UI_PREFS_KEYS } from "../../constants/ui-prefs-keys"; +import { IOConnectService } from "../../services/io/io.service"; +import { LoggerService } from "../../services/logger/logger.service"; +import { OverlayService } from "../../services/overlay/overlay.service"; +import { PromptService } from "../../services/prompt/prompt.service"; +import { UIPrompt } from "../../services/prompt/types"; + +@Injectable() +export class PromptEffects { + private _actions$: Actions = inject(Actions); + private _promptService: PromptService = inject(PromptService); + private _ioService: IOConnectService = inject(IOConnectService); + private _overlayService: OverlayService = inject(OverlayService); + private _store: Store = inject(Store); + private readonly _logger: LoggerService = inject(LoggerService); + protected readonly LOGGER_NAME: string = "PromptEffects"; + + private _promptState = this._store.selectSignal(selectAllPrompts); + private _favoritePromptNames = this._store.selectSignal(selectFavoritePromptNames); + + parsePromptsConfig$ = createEffect(() => { + return this._actions$.pipe( + ofType(parsePromptsConfig), + mergeMap(() => { + // Fetch config prompts synchronously + const configPrompts: UIPrompt[] = this._promptService.mapConfigPromptsToUIPrompts(); + + // Fetch user-defined prompts and favorites from prefs + return forkJoin({ + promptsFromPrefs: from(this._ioService.fetchPrefs(UI_PREFS_KEYS.PROMPTS)).pipe(catchError(() => of(null))), + favoritePromptNames: from(this._ioService.fetchPrefs(UI_PREFS_KEYS.FAVORITE_PROMPTS) as Promise).pipe(catchError(() => of([] as string[]))), + }).pipe( + map(({ promptsFromPrefs, favoritePromptNames }) => { + const combinedPrompts: UIPrompt[] = [...configPrompts, ...(promptsFromPrefs ?? [])]; + + return parsePromptsConfigSuccess({ + prompts: combinedPrompts, + favorites: favoritePromptNames ? favoritePromptNames : [], + }); + }) + ); + }) + ); + }); + + syncFavoritePromptsInPrefs$ = createEffect( + () => + this._actions$.pipe( + ofType(togglePromptFavorite), + mergeMap(() => { + return from(this._ioService.updatePrefs(UI_PREFS_KEYS.FAVORITE_PROMPTS, this._favoritePromptNames())).pipe( + catchError((error) => { + this._logger.get(this.LOGGER_NAME).error(`Failed to sync prefs: ${error instanceof Error ? error.message : String(error)}`); + return EMPTY; + }) + ); + }) + ), + { dispatch: false } + ); + + closeOverlayOnPromptSelect$ = createEffect( + () => + this._actions$.pipe( + ofType(selectPrompt), + map(() => { + this._overlayService.closeCurrentOverlay(); + }) + ), + { dispatch: false } + ); +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.facade.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.facade.ts new file mode 100644 index 0000000..fd6c564 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.facade.ts @@ -0,0 +1,43 @@ +import { inject, Injectable, Signal } from "@angular/core"; +import { Store } from "@ngrx/store"; + +import { clearSelectedPrompt, parsePromptsConfig, selectPrompt, togglePromptFavorite } from "./prompt.actions"; +import { selectAllPrompts, selectFavoritePromptNames, selectSelectedPrompt } from "./prompt.selector"; +import { Prompt, UIPrompt } from "../../services/prompt/types"; + +@Injectable({ + providedIn: "root", +}) +export class PromptFacade { + private readonly _store: Store = inject(Store); + + private readonly _allPrompts: Signal = this._store.selectSignal(selectAllPrompts); + public get allPrompts(): Signal { + return this._allPrompts; + } + private readonly _favoritePromptNames: Signal = this._store.selectSignal(selectFavoritePromptNames); + public get favoritePromptNames(): Signal { + return this._favoritePromptNames; + } + + private readonly _selectedPrompt: Signal = this._store.selectSignal(selectSelectedPrompt); + public get selectedPrompt(): Signal { + return this._selectedPrompt; + } + + public dispatchParsePromptsConfig(): void { + this._store.dispatch(parsePromptsConfig()); + } + + public dispatchSelectPrompt(prompt: UIPrompt): void { + this._store.dispatch(selectPrompt({ prompt })); + } + + public dispatchToggleFavoritePrompt(name: string): void { + this._store.dispatch(togglePromptFavorite({ name })); + } + + public dispatchClearSelectedPrompt(): void { + this._store.dispatch(clearSelectedPrompt()); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.reducer.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.reducer.ts new file mode 100644 index 0000000..462f7d6 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.reducer.ts @@ -0,0 +1,85 @@ +import { createReducer, on } from "@ngrx/store"; + +import { clearSelectedPrompt, parsePromptsConfig, parsePromptsConfigFailure, parsePromptsConfigSuccess, selectPrompt, togglePromptFavorite } from "./prompt.actions"; +import { LOADING_STATE } from "../../enums/loading-state.enum"; +import { UIPrompt } from "../../services/prompt/types"; +import { LoadingType } from "../../types/loading.type"; + +export type PromptReducerStateType = { + prompts: UIPrompt[]; + favoritePromptNames: string[]; + selectedPrompt: UIPrompt | null; + loadingState: { + fetchingState: LoadingType; + }; +}; + +export const initialState: PromptReducerStateType = { + prompts: [], + favoritePromptNames: [], + selectedPrompt: null, + loadingState: { + fetchingState: { type: LOADING_STATE.NOT_STARTED }, + }, +}; + +export const promptReducer = createReducer( + initialState, + on(parsePromptsConfig, (state) => { + return { + ...state, + loadingState: { + ...state.loadingState, + fetchingState: { type: LOADING_STATE.LOADING } as LoadingType, + }, + }; + }), + on(parsePromptsConfigSuccess, (state, { prompts, favorites }) => { + return { + ...state, + prompts, + loadingState: { + ...state.loadingState, + fetchingState: { type: LOADING_STATE.SUCCESS } as LoadingType, + }, + favoritePromptNames: favorites, + }; + }), + on(parsePromptsConfigFailure, (state, { error }) => { + return { + ...state, + loadingState: { + ...state.loadingState, + fetchingState: { + type: LOADING_STATE.ERROR, + message: error.message, + } as LoadingType, + }, + }; + }), + on(togglePromptFavorite, (state, { name }) => { + if (!state.favoritePromptNames.includes(name)) { + return { + ...state, + favoritePromptNames: [...state.favoritePromptNames, name], + }; + } + + return { + ...state, + favoritePromptNames: state.favoritePromptNames.filter((n) => n !== name), + }; + }), + on(selectPrompt, (state, { prompt }) => { + return { + ...state, + selectedPrompt: prompt, + }; + }), + on(clearSelectedPrompt, (state) => { + return { + ...state, + selectedPrompt: null, + }; + }) +); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.selector.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.selector.ts new file mode 100644 index 0000000..bd7d1e1 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.selector.ts @@ -0,0 +1,11 @@ +import { createFeatureSelector, createSelector } from "@ngrx/store"; + +import { PromptReducerStateType } from "./prompt.reducer"; + +export const selectPromptStore = createFeatureSelector("promptStore"); + +export const selectAllPrompts = createSelector(selectPromptStore, (state: PromptReducerStateType) => state.prompts); + +export const selectFavoritePromptNames = createSelector(selectPromptStore, (state: PromptReducerStateType) => state.favoritePromptNames); + +export const selectSelectedPrompt = createSelector(selectPromptStore, (state: PromptReducerStateType) => state.selectedPrompt); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.actions.enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.actions.enum.ts new file mode 100644 index 0000000..fc6c32d --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.actions.enum.ts @@ -0,0 +1,11 @@ +export enum RESPONSE_STREAM_ACTIONS { + START_THREAD_STREAM = "[Response Stream] Start Thread Stream", + UPDATE_STREAM_CONTENT = "[Response Stream] Update Stream Content", + ADD_STREAM_TOOL_MESSAGE = "[Response Stream] Add Stream Tool Message", + COMPLETE_THREAD_STREAM = "[Response Stream] Complete Thread Stream", + FAIL_THREAD_STREAM = "[Response Stream] Fail Thread Stream", + ABORT_THREAD_STREAM = "[Response Stream] Abort Thread Stream", + CLEAR_COMPLETION_NOTIFICATION = "[Response Stream] Clear Completion Notification", + CLEAR_ALL_COMPLETION_NOTIFICATIONS = "[Response Stream] Clear All Completion Notifications", + UNTRACK_THREAD_STREAM_STATE = "[Response Stream] Untrack Thread Stream State", +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.actions.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.actions.ts new file mode 100644 index 0000000..123613f --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.actions.ts @@ -0,0 +1,20 @@ +import { createAction, props } from "@ngrx/store"; + +import { RESPONSE_STREAM_ACTIONS } from "./response-stream.actions.enum"; +import { UIMessage, UIToolMessage } from "../message/types"; + +export const startThreadStream = createAction(RESPONSE_STREAM_ACTIONS.START_THREAD_STREAM, props<{ threadId: string; userMessage: UIMessage }>()); + +export const updateStreamContent = createAction(RESPONSE_STREAM_ACTIONS.UPDATE_STREAM_CONTENT, props<{ threadId: string; content: string; messageId: string }>()); + +export const addStreamToolMessage = createAction(RESPONSE_STREAM_ACTIONS.ADD_STREAM_TOOL_MESSAGE, props<{ threadId: string; toolMessage: UIToolMessage }>()); + +export const completeThreadStream = createAction(RESPONSE_STREAM_ACTIONS.COMPLETE_THREAD_STREAM, props<{ threadId: string; shouldNotify: boolean }>()); + +export const failThreadStream = createAction(RESPONSE_STREAM_ACTIONS.FAIL_THREAD_STREAM, props<{ threadId: string; errorMessage: string }>()); + +export const abortThreadStream = createAction(RESPONSE_STREAM_ACTIONS.ABORT_THREAD_STREAM, props<{ threadId: string }>()); + +export const clearCompletionNotification = createAction(RESPONSE_STREAM_ACTIONS.CLEAR_COMPLETION_NOTIFICATION, props<{ threadId: string }>()); + +export const untrackThreadStreamState = createAction(RESPONSE_STREAM_ACTIONS.UNTRACK_THREAD_STREAM_STATE, props<{ threadId: string }>()); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.facade.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.facade.ts new file mode 100644 index 0000000..765bf5a --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.facade.ts @@ -0,0 +1,100 @@ +import { computed, inject, Injectable, Signal } from "@angular/core"; +import { Store } from "@ngrx/store"; + +import { abortThreadStream, clearCompletionNotification, completeThreadStream, failThreadStream, startThreadStream, untrackThreadStreamState, updateStreamContent } from "./response-stream.actions"; +import { selectAllStreams, selectHasAnyCompletionNotification, selectStreamingThreadIds, selectThreadsCurrentlyStreaming, selectThreadsWithCompletionNotification } from "./response-stream.selector"; +import { RESPONSE_STREAM_STATUS, ThreadStreamState } from "./response-stream.types"; +import { UIMessage } from "../message/types"; + +@Injectable({ + providedIn: "root", +}) +export class ResponseStreamFacade { + private readonly _store: Store = inject(Store); + + private readonly _allStreams: Signal> = this._store.selectSignal(selectAllStreams); + public get allStreams(): Signal> { + return this._allStreams; + } + + private readonly _threadsCurrentlyStreaming: Signal = this._store.selectSignal(selectThreadsCurrentlyStreaming); + public get threadsCurrentlyStreaming(): Signal { + return this._threadsCurrentlyStreaming; + } + + private readonly _threadsWithCompletionNotification: Signal = this._store.selectSignal(selectThreadsWithCompletionNotification); + public get threadsWithCompletionNotification(): Signal { + return this._threadsWithCompletionNotification; + } + + private readonly _hasAnyCompletionNotification: Signal = this._store.selectSignal(selectHasAnyCompletionNotification); + public get hasAnyCompletionNotification(): Signal { + return this._hasAnyCompletionNotification; + } + + private readonly _streamingThreadIds: Signal> = this._store.selectSignal(selectStreamingThreadIds); + public get streamingThreadIds(): Signal> { + return this._streamingThreadIds; + } + + public isThreadStreaming(threadId: string): boolean { + const stream = this._allStreams()[threadId]; + return stream?.status === RESPONSE_STREAM_STATUS.STREAMING; + } + + public hasThreadCompletionNotification(threadId: string): boolean { + const stream = this._allStreams()[threadId]; + return stream?.hasCompletionNotification ?? false; + } + + public getStreamForThread(threadId: string): ThreadStreamState | null { + return this._allStreams()[threadId] ?? null; + } + + public createIsStreamingSignal(threadIdSignal: Signal): Signal { + return computed(() => { + const threadId = threadIdSignal(); + return this._streamingThreadIds().has(threadId); + }); + } + + public createHasNotificationSignal(threadIdSignal: Signal): Signal { + return computed(() => { + const threadId = threadIdSignal(); + return this._threadsWithCompletionNotification().includes(threadId); + }); + } + + public dispatchStartThreadStream(threadId: string, userMessage: UIMessage): void { + this._store.dispatch( + startThreadStream({ + threadId, + userMessage, + }) + ); + } + + public dispatchUpdateStreamContent(threadId: string, content: string, messageId: string): void { + this._store.dispatch(updateStreamContent({ threadId, content, messageId })); + } + + public dispatchCompleteThreadStream(threadId: string, shouldNotify: boolean): void { + this._store.dispatch(completeThreadStream({ threadId, shouldNotify })); + } + + public dispatchFailThreadStream(threadId: string, errorMessage: string): void { + this._store.dispatch(failThreadStream({ threadId, errorMessage })); + } + + public dispatchAbortThreadStream(threadId: string): void { + this._store.dispatch(abortThreadStream({ threadId })); + } + + public dispatchClearCompletionNotification(threadId: string): void { + this._store.dispatch(clearCompletionNotification({ threadId })); + } + + public dispatchUntrackThreadStreamState(threadId: string): void { + this._store.dispatch(untrackThreadStreamState({ threadId })); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.reducer.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.reducer.ts new file mode 100644 index 0000000..5cf6301 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.reducer.ts @@ -0,0 +1,200 @@ +import { createReducer, on } from "@ngrx/store"; + +import { + abortThreadStream, + addStreamToolMessage, + clearCompletionNotification, + completeThreadStream, + failThreadStream, + startThreadStream, + untrackThreadStreamState, + updateStreamContent, +} from "./response-stream.actions"; +import { createThreadStreamState, RESPONSE_STREAM_STATUS, ThreadStreamState } from "./response-stream.types"; +import { UIToolMessage } from "../message/types"; + +export type ResponseStreamReducerStateType = { + streams: Record; +}; + +export const initialState: ResponseStreamReducerStateType = { + streams: {}, +}; + +export const responseStreamReducer = createReducer( + initialState, + + on(startThreadStream, (state, { threadId, userMessage }) => { + return { + ...state, + streams: { + ...state.streams, + [threadId]: createThreadStreamState(threadId, RESPONSE_STREAM_STATUS.STREAMING, userMessage), + }, + }; + }), + + on(updateStreamContent, (state, { threadId, content, messageId }) => { + const existingStream = state.streams[threadId]; + + if (!existingStream || existingStream.status !== RESPONSE_STREAM_STATUS.STREAMING) { + return state; + } + + return { + ...state, + streams: { + ...state.streams, + [threadId]: { + ...existingStream, + accumulatedContent: content, + currentMessageId: messageId, + }, + }, + }; + }), + + on(addStreamToolMessage, (state, { threadId, toolMessage }) => { + const existingStream = state.streams[threadId]; + + if (!existingStream || existingStream.status !== RESPONSE_STREAM_STATUS.STREAMING) { + return state; + } + + const existingToolIndex = existingStream.toolMessages.findIndex( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (t: any) => t.id === toolMessage.id + ); + + let updatedToolMessages: UIToolMessage[] | undefined = []; + + if (existingToolIndex >= 0) { + // Update existing tool message (e.g., adding result) + updatedToolMessages = [...existingStream.toolMessages]; + updatedToolMessages[existingToolIndex] = { + ...updatedToolMessages[existingToolIndex], + ...toolMessage, + }; + } + + if (existingToolIndex < 0) { + updatedToolMessages = [...existingStream.toolMessages, toolMessage]; + } + + return { + ...state, + streams: { + ...state.streams, + [threadId]: { + ...existingStream, + toolMessages: updatedToolMessages, + }, + }, + }; + }), + + on(completeThreadStream, (state, { threadId, shouldNotify }) => { + const existingStream = state.streams[threadId]; + + if (!existingStream) { + return state; + } + + return { + ...state, + streams: { + ...state.streams, + [threadId]: { + ...existingStream, + status: RESPONSE_STREAM_STATUS.COMPLETED, + hasCompletionNotification: shouldNotify, + }, + }, + }; + }), + + on(failThreadStream, (state, { threadId, errorMessage }) => { + const existingStream = state.streams[threadId]; + + if (!existingStream) { + return state; + } + + return { + ...state, + streams: { + ...state.streams, + [threadId]: { + ...existingStream, + status: RESPONSE_STREAM_STATUS.ERROR, + errorMessage, + }, + }, + }; + }), + + on(abortThreadStream, (state, { threadId }) => { + const existingStream = state.streams[threadId]; + + if (!existingStream) { + return state; + } + + return { + ...state, + streams: { + ...state.streams, + [threadId]: { + ...existingStream, + status: RESPONSE_STREAM_STATUS.ABORTED, + }, + }, + }; + }), + + on(clearCompletionNotification, (state, { threadId }) => { + const existingStream = state.streams[threadId]; + + if (!existingStream) { + return state; + } + + return { + ...state, + streams: { + ...state.streams, + [threadId]: { + ...existingStream, + hasCompletionNotification: false, + }, + }, + }; + }), + + on(untrackThreadStreamState, (state, { threadId }) => { + if (!threadId) { + return state; + } + + const existingStream = state.streams[threadId]; + + // Only clear if stream exists and is in a terminal state + if (!existingStream) { + return state; + } + + const shouldClear = + existingStream.status === RESPONSE_STREAM_STATUS.COMPLETED || existingStream.status === RESPONSE_STREAM_STATUS.ERROR || existingStream.status === RESPONSE_STREAM_STATUS.ABORTED; + + if (!shouldClear) { + return state; + } + + const { [threadId]: _removed, ...remainingStreams } = state.streams; + + return { + ...state, + streams: remainingStreams, + }; + }) +); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.selector.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.selector.ts new file mode 100644 index 0000000..26c5f0b --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.selector.ts @@ -0,0 +1,52 @@ +import { createFeatureSelector, createSelector } from "@ngrx/store"; + +import { ResponseStreamReducerStateType } from "./response-stream.reducer"; +import { RESPONSE_STREAM_STATUS, ThreadStreamState } from "./response-stream.types"; + +export const selectResponseStreamStore = createFeatureSelector("responseStreamStore"); + +export const selectAllStreams = createSelector(selectResponseStreamStore, (state: ResponseStreamReducerStateType) => state.streams); + +export const selectStreamByThreadId = (threadId: string) => createSelector(selectResponseStreamStore, (state: ResponseStreamReducerStateType) => state.streams[threadId] ?? null); + +export const selectIsThreadStreaming = (threadId: string) => + createSelector(selectResponseStreamStore, (state: ResponseStreamReducerStateType) => { + const stream = state.streams[threadId]; + return stream?.status === RESPONSE_STREAM_STATUS.STREAMING; + }); + +export const selectThreadsCurrentlyStreaming = createSelector(selectResponseStreamStore, (state: ResponseStreamReducerStateType) => { + return Object.values(state.streams) + .filter((stream) => stream.status === RESPONSE_STREAM_STATUS.STREAMING) + .map((stream) => stream.threadId); +}); + +export const selectThreadsWithCompletionNotification = createSelector(selectResponseStreamStore, (state: ResponseStreamReducerStateType) => { + return Object.values(state.streams) + .filter((stream) => stream.hasCompletionNotification) + .map((stream) => stream.threadId); +}); + +export const selectHasAnyCompletionNotification = createSelector(selectResponseStreamStore, (state: ResponseStreamReducerStateType) => { + return Object.values(state.streams).some((stream) => stream.hasCompletionNotification); +}); + +export const selectStreamContentForThread = (threadId: string) => + createSelector(selectResponseStreamStore, (state: ResponseStreamReducerStateType) => { + const stream = state.streams[threadId]; + return stream + ? { + content: stream.accumulatedContent, + messageId: stream.currentMessageId, + userMessage: stream.userMessage, + toolMessages: stream.toolMessages, + } + : null; + }); + +export const selectStreamingThreadIds = createSelector(selectResponseStreamStore, (state: ResponseStreamReducerStateType): Set => { + const streamingIds = Object.values(state.streams) + .filter((stream: ThreadStreamState) => stream.status === RESPONSE_STREAM_STATUS.STREAMING) + .map((stream: ThreadStreamState) => stream.threadId); + return new Set(streamingIds); +}); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.types.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.types.ts new file mode 100644 index 0000000..c16a3e9 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.types.ts @@ -0,0 +1,34 @@ +import { UIMessage, UIToolMessage } from "../message/types"; + +export enum RESPONSE_STREAM_STATUS { + IDLE = "idle", + STREAMING = "streaming", + COMPLETED = "completed", + ERROR = "error", + ABORTED = "aborted", +} + +export type ThreadStreamState = { + threadId: string; + status: RESPONSE_STREAM_STATUS; + accumulatedContent: string; + /** Current assistant message ID being streamed */ + currentMessageId: string | null; + // only if user is not on the thread when stream completes + hasCompletionNotification: boolean; + errorMessage?: string; + userMessage: UIMessage | null; + toolMessages: UIToolMessage[]; +}; + +export function createThreadStreamState(threadId: string, status: RESPONSE_STREAM_STATUS = RESPONSE_STREAM_STATUS.IDLE, userMessage: UIMessage | null = null): ThreadStreamState { + return { + threadId, + status, + accumulatedContent: "", + currentMessageId: null, + hasCompletionNotification: false, + userMessage, + toolMessages: [], + }; +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/enums.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/enums.ts new file mode 100644 index 0000000..54f5c8a --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/enums.ts @@ -0,0 +1,6 @@ +export enum THREAD_STATE { + RENAMING = "renaming", + DELETING = "deleting", + LOADING = "loading", + IDLE = "idle", +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.actions.enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.actions.enum.ts new file mode 100644 index 0000000..87dca34 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.actions.enum.ts @@ -0,0 +1,12 @@ +export enum THREAD_ACTIONS { + FETCH_THREADS = "[Thread] Fetch Threads", + FETCH_THREADS_SUCCESS = "[Thread] Fetch Threads Success", + FETCH_THREADS_FAILURE = "[Thread] Fetch Threads Failure", + RENAME_THREAD = "[Thread] Rename Thread", + RENAME_THREAD_SUCCESS = "[Thread] Rename Thread Success", + RENAME_THREAD_FAILURE = "[Thread] Rename Thread Failure", + DELETE_THREAD = "[Thread] Delete Thread", + DELETE_THREAD_SUCCESS = "[Thread] Delete Thread Success", + DELETE_THREAD_FAILURE = "[Thread] Delete Thread Failure", + CHANGE_ACTIVE_THREAD = "[Thread] Activate Thread", +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.actions.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.actions.ts new file mode 100644 index 0000000..edadf7a --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.actions.ts @@ -0,0 +1,24 @@ +import { createAction, props } from "@ngrx/store"; + +import { THREAD_ACTIONS } from "./thread.actions.enum"; +import { UIThread } from "./types"; + +export const fetchThreads = createAction(THREAD_ACTIONS.FETCH_THREADS, props<{ agentId: string }>()); + +export const fetchThreadsSuccess = createAction(THREAD_ACTIONS.FETCH_THREADS_SUCCESS, props<{ threads: UIThread[] }>()); + +export const fetchThreadsFailure = createAction(THREAD_ACTIONS.FETCH_THREADS_FAILURE, props<{ error: string }>()); + +export const renameThread = createAction(THREAD_ACTIONS.RENAME_THREAD, props<{ thread: UIThread; newTitle: string }>()); + +export const renameThreadSuccess = createAction(THREAD_ACTIONS.RENAME_THREAD_SUCCESS, props<{ id: string; newName: string }>()); + +export const renameThreadFailure = createAction(THREAD_ACTIONS.RENAME_THREAD_FAILURE, props<{ error: Error }>()); + +export const deleteThread = createAction(THREAD_ACTIONS.DELETE_THREAD, props<{ thread: UIThread }>()); + +export const deleteThreadSuccess = createAction(THREAD_ACTIONS.DELETE_THREAD_SUCCESS, props<{ id: string }>()); + +export const deleteThreadFailure = createAction(THREAD_ACTIONS.DELETE_THREAD_FAILURE, props<{ error: Error }>()); + +export const changeActiveThread = createAction(THREAD_ACTIONS.CHANGE_ACTIVE_THREAD, props<{ threadId: string | null }>()); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.effects.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.effects.ts new file mode 100644 index 0000000..0d44270 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.effects.ts @@ -0,0 +1,94 @@ +import { inject, Injectable } from "@angular/core"; +import { IoAiWeb } from "@interopio/ai-web"; +import { Actions, createEffect, ofType } from "@ngrx/effects"; +import { mergeMap, from, of, map, catchError, switchMap, EMPTY } from "rxjs"; + +import { + renameThread, + renameThreadSuccess, + renameThreadFailure, + fetchThreads, + fetchThreadsSuccess, + deleteThread, + deleteThreadSuccess, + deleteThreadFailure, + fetchThreadsFailure, + changeActiveThread, +} from "./thread.actions"; +import { UIThread } from "./types"; +import { IO_ASSIST_DYNAMIC_CONFIG } from "../../../io-assist.config"; +import { ThreadService } from "../../services/thread/thread.service"; +import { untrackThreadStreamState } from "../response-stream/response-stream.actions"; + +@Injectable() +export class ThreadEffects { + private readonly _actions$: Actions = inject(Actions); + private readonly _threadService: ThreadService = inject(ThreadService); + private readonly _dynamicConfig = inject(IO_ASSIST_DYNAMIC_CONFIG); + + fetchThreads$ = createEffect(() => + this._actions$.pipe( + ofType(fetchThreads), + switchMap(({ agentId }) => + from(this._threadService.fetchThreads(agentId, this._dynamicConfig().user.id)).pipe( + switchMap((threads: IoAiWeb.Threads.Thread[]) => + from(this._threadService.toUIThreads(threads)).pipe( + map((uiThreads: UIThread[]) => + fetchThreadsSuccess({ + threads: uiThreads, + }) + ), + catchError((error) => of(fetchThreadsFailure({ error }))) + ) + ) + ) + ) + ) + ); + + renameThread$ = createEffect(() => + this._actions$.pipe( + ofType(renameThread), + mergeMap((action) => + from(this._threadService.renameThread(action.thread, action.newTitle)).pipe( + map(() => renameThreadSuccess({ id: action.thread.id, newName: action.newTitle })), + catchError((error) => of(renameThreadFailure({ error }))) + ) + ) + ) + ); + + deleteThread$ = createEffect(() => + this._actions$.pipe( + ofType(deleteThread), + mergeMap((action) => + from(this._threadService.deleteThread(action.thread)).pipe( + map(() => deleteThreadSuccess({ id: action.thread.id })), + catchError((error) => of(deleteThreadFailure({ error }))) + ) + ) + ) + ); + + /** + * When a thread is successfully deleted, remove its persisted MCP App + * preferences so they don't accumulate indefinitely in io.prefs. + */ + cleanupMcpAppsPrefsOnThreadDelete$ = createEffect( + () => + this._actions$.pipe( + ofType(deleteThreadSuccess), + mergeMap(({ id }) => from(this._threadService.deleteThreadState(id)).pipe(catchError(() => EMPTY))) + ), + { dispatch: false } + ); + + changeActiveThread$ = createEffect(() => + this._actions$.pipe( + ofType(changeActiveThread), + map((action) => { + return untrackThreadStreamState({ threadId: action.threadId ?? "" }); + }) + ) + ); +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.facade.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.facade.ts new file mode 100644 index 0000000..1b4c867 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.facade.ts @@ -0,0 +1,60 @@ +import { inject, Injectable, Signal } from "@angular/core"; +import { Store } from "@ngrx/store"; + +import { changeActiveThread, deleteThread, fetchThreads, renameThread } from "./thread.actions"; +import { selectActiveThread, selectActiveThreadId, selectAllThreads, selectIsFetchingThreads, selectThreadLength, selectThreadLoadingState } from "./thread.selector"; +import { UIThread } from "./types"; +import { LoadingType } from "../../types/loading.type"; + +@Injectable({ + providedIn: "root", +}) +export class ThreadFacade { + private readonly _store: Store = inject(Store); + + private readonly _allThreads: Signal = this._store.selectSignal(selectAllThreads); + public get allThreads(): Signal { + return this._allThreads; + } + + private readonly _threadLength: Signal = this._store.selectSignal(selectThreadLength); + public get threadLength(): Signal { + return this._threadLength; + } + + private readonly _threadLoadingState: Signal = this._store.selectSignal(selectThreadLoadingState); + public get threadLoadingState(): Signal { + return this._threadLoadingState; + } + + private readonly _isFetchingThreads: Signal = this._store.selectSignal(selectIsFetchingThreads); + public get isFetchingThreads(): Signal { + return this._isFetchingThreads; + } + + private readonly _activeThreadId: Signal = this._store.selectSignal(selectActiveThreadId); + public get activeThreadId(): Signal { + return this._activeThreadId; + } + + private readonly _activeThread: Signal = this._store.selectSignal(selectActiveThread); + public get activeThread(): Signal { + return this._activeThread; + } + + public dispatchFetchThreads(agentId: string): void { + this._store.dispatch(fetchThreads({ agentId })); + } + + public dispatchRenameThread(thread: UIThread, newTitle: string): void { + this._store.dispatch(renameThread({ thread, newTitle })); + } + + public dispatchDeleteThread(thread: UIThread): void { + this._store.dispatch(deleteThread({ thread })); + } + + public dispatchChangeActiveThread(threadId: string | null): void { + this._store.dispatch(changeActiveThread({ threadId })); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.reducer.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.reducer.ts new file mode 100644 index 0000000..ecbe472 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.reducer.ts @@ -0,0 +1,110 @@ +import { createReducer, on } from "@ngrx/store"; + +import { THREAD_STATE } from "./enums"; +import { + renameThreadSuccess, + renameThreadFailure, + renameThread, + fetchThreads, + fetchThreadsFailure, + fetchThreadsSuccess, + changeActiveThread, + deleteThreadSuccess, + deleteThread, + deleteThreadFailure, +} from "./thread.actions"; +import { UIThread } from "./types"; +import { LOADING_STATE } from "../../enums/loading-state.enum"; +import { LoadingType } from "../../types/loading.type"; + +export type ThreadReducerStateType = { + threads: UIThread[]; + activeThreadId: string | null; + loadingState: LoadingType; + error: string | null; +}; + +export const initialState: ThreadReducerStateType = { + threads: [], + activeThreadId: null, + loadingState: { type: LOADING_STATE.NOT_STARTED }, + error: null, +}; + +export const threadReducer = createReducer( + initialState, + on(fetchThreads, (state) => { + return { + ...state, + loadingState: { type: LOADING_STATE.LOADING as typeof LOADING_STATE.LOADING }, + }; + }), + on(fetchThreadsSuccess, (state, { threads }) => { + return { + ...state, + loadingState: { + type: LOADING_STATE.SUCCESS as typeof LOADING_STATE.SUCCESS, + }, + threads: threads, + error: null, + }; + }), + on(fetchThreadsFailure, (state, { error }) => { + return { + ...state, + loadingState: { + type: LOADING_STATE.ERROR as typeof LOADING_STATE.ERROR, + message: error, + }, + error: error, + }; + }), + on(renameThread, (state, { thread }) => { + return { + ...state, + threads: state.threads.map((t) => (t.id === thread.id ? { ...t, state: THREAD_STATE.RENAMING as const } : t)), + error: null, + }; + }), + on(renameThreadSuccess, (state, { id, newName }) => { + return { + ...state, + threads: state.threads.map((t) => (t.id === id ? { ...t, title: newName, state: THREAD_STATE.IDLE as const } : t)), + error: null, + }; + }), + on(renameThreadFailure, (state, { error }) => { + return { + ...state, + threads: state.threads.map((t) => (t.state === THREAD_STATE.RENAMING ? { ...t, state: THREAD_STATE.IDLE as const } : t)), + error: error.message, + }; + }), + on(deleteThread, (state, { thread }) => { + return { + ...state, + threads: state.threads.map((t) => (t.id === thread.id ? { ...t, state: THREAD_STATE.DELETING as const } : t)), + error: null, + }; + }), + on(deleteThreadSuccess, (state, { id }) => { + return { + ...state, + threads: state.threads.filter((t) => t.id !== id), + activeThreadId: state.activeThreadId === id ? null : state.activeThreadId, + }; + }), + on(deleteThreadFailure, (state, { error }) => { + return { + ...state, + threads: state.threads.map((t) => (t.state === THREAD_STATE.DELETING ? { ...t, state: THREAD_STATE.IDLE as const } : t)), + error: error.message, + }; + }), + on(changeActiveThread, (state, { threadId }) => { + return { + ...state, + activeThreadId: threadId, + }; + }) +); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.selector.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.selector.ts new file mode 100644 index 0000000..65097fb --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.selector.ts @@ -0,0 +1,18 @@ +import { createFeatureSelector, createSelector } from "@ngrx/store"; + +import { ThreadReducerStateType } from "./thread.reducer"; +import { LOADING_STATE } from "../../enums/loading-state.enum"; + +export const selectThreadsStore = createFeatureSelector("threadsStore"); + +export const selectAllThreads = createSelector(selectThreadsStore, (state: ThreadReducerStateType) => state.threads); + +export const selectThreadLength = createSelector(selectThreadsStore, (state: ThreadReducerStateType) => state.threads.length); + +export const selectThreadLoadingState = createSelector(selectThreadsStore, (state: ThreadReducerStateType) => state.loadingState); + +export const selectIsFetchingThreads = createSelector(selectThreadsStore, (state: ThreadReducerStateType) => state.loadingState.type === LOADING_STATE.LOADING); + +export const selectActiveThreadId = createSelector(selectThreadsStore, (state: ThreadReducerStateType) => state.activeThreadId); + +export const selectActiveThread = createSelector(selectThreadsStore, (state: ThreadReducerStateType) => state.threads.find((thread) => thread.id === state.activeThreadId) || null); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/types.ts new file mode 100644 index 0000000..b1c3d85 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/types.ts @@ -0,0 +1,13 @@ +import { IoAiWeb } from "@interopio/ai-web"; + +import { THREAD_STATE } from "./enums"; + +export type ThreadState = THREAD_STATE.IDLE | THREAD_STATE.RENAMING | THREAD_STATE.DELETING; + +export type UIThread = IoAiWeb.Threads.Thread & { + id: string; + title: string; + metadata?: Record; + updatedAt?: Date; + state?: ThreadState; +}; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/enums.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/enums.ts new file mode 100644 index 0000000..87b1b90 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/enums.ts @@ -0,0 +1,4 @@ +export enum USER_TOOL_STATE { + IDLE = "IDLE", + ENABLING_DISABLING = "ENABLING_DISABLING", +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.actions.enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.actions.enum.ts new file mode 100644 index 0000000..1fb91f2 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.actions.enum.ts @@ -0,0 +1,8 @@ +export enum TOOL_ACTIONS { + FETCH_TOOLS = "[Tool] Fetch Tools", + FETCH_TOOLS_SUCCESS = "[Tool] Fetch Tools Success", + FETCH_TOOLS_FAILURE = "[Tool] Fetch Tools Failure", + TOGGLE_TOOL = "[Tool] Toggle Tool", + TOGGLE_TOOL_SUCCESS = "[Tool] Toggle Tool Success", + TOGGLE_TOOL_FAILURE = "[Tool] Toggle Tool Failure", +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.actions.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.actions.ts new file mode 100644 index 0000000..5c1800d --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.actions.ts @@ -0,0 +1,14 @@ +import { createAction, props } from "@ngrx/store"; + +import { TOOL_ACTIONS } from "./tool.actions.enum"; +import { UITool } from "./types"; + +// =========================================================================== Fetch +export const fetchTools = createAction(TOOL_ACTIONS.FETCH_TOOLS); +export const fetchToolsSuccess = createAction(TOOL_ACTIONS.FETCH_TOOLS_SUCCESS, props<{ tools: UITool[] }>()); +export const fetchToolsFailure = createAction(TOOL_ACTIONS.FETCH_TOOLS_FAILURE, props<{ error: Error }>()); + +// =========================================================================== Enable / Disable +export const toggleTool = createAction(TOOL_ACTIONS.TOGGLE_TOOL, props<{ tool: UITool }>()); +export const toggleToolSuccess = createAction(TOOL_ACTIONS.TOGGLE_TOOL_SUCCESS, props<{ tool: UITool }>()); +export const toggleToolFailure = createAction(TOOL_ACTIONS.TOGGLE_TOOL_FAILURE, props<{ error: Error; tool: UITool }>()); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.effects.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.effects.ts new file mode 100644 index 0000000..4c70617 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.effects.ts @@ -0,0 +1,66 @@ +import { inject, Injectable } from "@angular/core"; +import { IoAiWeb } from "@interopio/ai-web"; +import { Actions, createEffect, ofType } from "@ngrx/effects"; +import { mergeMap, map, of, catchError, from, switchMap } from "rxjs"; + +import { USER_TOOL_STATE } from "./enums"; +import { fetchTools, fetchToolsFailure, fetchToolsSuccess, toggleTool, toggleToolFailure, toggleToolSuccess } from "./tool.actions"; +import { UITool } from "./types"; +import { ToolsService } from "../../services/tools/tools.service"; + +@Injectable() +export class ToolEffects { + private _actions$: Actions = inject(Actions); + private _toolsService: ToolsService = inject(ToolsService); + + fetchTools$ = createEffect(() => { + return this._actions$.pipe( + ofType(fetchTools), + mergeMap(() => + from(this._toolsService.fetchTools()).pipe( + map((tools: IoAiWeb.Tools.Tool[]) => { + const uiTools: UITool[] = tools.map((tool) => ({ + ...tool, + state: USER_TOOL_STATE.IDLE, + })); + + return fetchToolsSuccess({ tools: uiTools }); + }), + catchError((error) => + of( + fetchToolsFailure({ + error: error instanceof Error ? error : new Error(String(error)), + }) + ) + ) + ) + ) + ); + }); + + toggleTool$ = createEffect(() => { + return this._actions$.pipe( + ofType(toggleTool), + switchMap(({ tool }) => { + return from(this._toolsService.toggleTool(tool)).pipe( + map((updatedTool: IoAiWeb.Tools.Tool) => { + const uiTool: UITool = { + ...updatedTool, + state: USER_TOOL_STATE.IDLE, + }; + + return toggleToolSuccess({ tool: uiTool }); + }), + catchError((error) => + of( + toggleToolFailure({ + error: error instanceof Error ? error : new Error(String(error)), + tool, + }) + ) + ) + ); + }) + ); + }); +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.facade.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.facade.ts new file mode 100644 index 0000000..81c37d2 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.facade.ts @@ -0,0 +1,34 @@ +import { inject, Injectable, Signal } from "@angular/core"; +import { Store } from "@ngrx/store"; + +import { fetchTools, toggleTool } from "./tool.actions"; +import { selectAllTools, selectEnabledTools, selectIsFetchingTools } from "./tool.selector"; +import { UITool } from "./types"; + +@Injectable() +export class ToolFacade { + private readonly _store: Store = inject(Store); + + private readonly _allTools: Signal = this._store.selectSignal(selectAllTools); + public get allTools() { + return this._allTools; + } + + private readonly _isFetchingTools: Signal = this._store.selectSignal(selectIsFetchingTools); + public get isFetchingTools(): Signal { + return this._isFetchingTools; + } + + private readonly _enabledTools: Signal = this._store.selectSignal(selectEnabledTools); + public get enabledTools(): Signal { + return this._enabledTools; + } + + public dispatchFetchTools(): void { + this._store.dispatch(fetchTools()); + } + + public dispatchToggleTool(tool: UITool): void { + this._store.dispatch(toggleTool({ tool })); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.reducer.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.reducer.ts new file mode 100644 index 0000000..89e2081 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.reducer.ts @@ -0,0 +1,71 @@ +import { createReducer, on } from "@ngrx/store"; + +import { USER_TOOL_STATE } from "./enums"; +import { fetchTools, fetchToolsFailure, fetchToolsSuccess, toggleTool, toggleToolFailure, toggleToolSuccess } from "./tool.actions"; +import { UITool } from "./types"; +import { LOADING_STATE } from "../../enums/loading-state.enum"; +import { LoadingType } from "../../types/loading.type"; + +export type ToolReducerStateType = { + tools: UITool[]; + loadingState: { + fetchingState: LoadingType; + }; +}; + +export const initialState: ToolReducerStateType = { + tools: [], + loadingState: { + fetchingState: { type: LOADING_STATE.NOT_STARTED }, + }, +}; + +export const toolReducer = createReducer( + initialState, + on(fetchTools, (state) => { + return { + ...state, + loadingState: { + fetchingState: { type: LOADING_STATE.LOADING } as LoadingType, + }, + }; + }), + on(fetchToolsSuccess, (state, { tools }) => { + return { + ...state, + tools, + loadingState: { + fetchingState: { type: LOADING_STATE.SUCCESS } as LoadingType, + }, + }; + }), + on(fetchToolsFailure, (state, { error }) => { + return { + ...state, + loadingState: { + fetchingState: { + type: LOADING_STATE.ERROR, + message: error.message, + } as LoadingType, + }, + }; + }), + on(toggleTool, (state, { tool }) => { + return { + ...state, + tools: state.tools.map((t) => (t.name === tool.name ? { ...t, state: USER_TOOL_STATE.ENABLING_DISABLING } : t)), + }; + }), + on(toggleToolSuccess, (state, { tool }) => { + return { + ...state, + tools: state.tools.map((t) => (t.name === tool.name ? { ...t, enabled: tool.enabled, state: USER_TOOL_STATE.IDLE } : t)), + }; + }), + on(toggleToolFailure, (state, { tool }) => { + return { + ...state, + tools: state.tools.map((t) => (t.name === tool.name ? { ...t, state: USER_TOOL_STATE.IDLE } : t)), + }; + }) +); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.selector.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.selector.ts new file mode 100644 index 0000000..2c096c9 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.selector.ts @@ -0,0 +1,15 @@ +import { createFeatureSelector, createSelector } from "@ngrx/store"; + +import { ToolReducerStateType } from "./tool.reducer"; +import { UITool } from "./types"; +import { LOADING_STATE } from "../../enums/loading-state.enum"; + +export const selectToolStore = createFeatureSelector("toolStore"); + +export const selectAllTools = createSelector(selectToolStore, (state: ToolReducerStateType) => state.tools); + +export const selectToolFetchingState = createSelector(selectToolStore, (state: ToolReducerStateType) => state.loadingState.fetchingState); + +export const selectIsFetchingTools = createSelector(selectToolStore, (state: ToolReducerStateType) => state.loadingState.fetchingState.type === LOADING_STATE.LOADING); + +export const selectEnabledTools = createSelector(selectToolStore, (state: ToolReducerStateType) => state.tools.filter((tool: UITool) => tool.enabled)); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/types.ts new file mode 100644 index 0000000..3c576d5 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/types.ts @@ -0,0 +1,9 @@ +import { IoAiWeb } from "@interopio/ai-web"; + +import { USER_TOOL_STATE } from "./enums"; + +export type ToolState = USER_TOOL_STATE.IDLE | USER_TOOL_STATE.ENABLING_DISABLING; + +export type UITool = IoAiWeb.Tools.Tool & { + state: ToolState; +}; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.actions.enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.actions.enum.ts new file mode 100644 index 0000000..7a8394b --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.actions.enum.ts @@ -0,0 +1,9 @@ +export enum WORKING_CONTEXT_ACTIONS { + GET_WORKING_CONTEXT = "[Working Context] Get Working Context", + GET_WORKING_CONTEXT_SUCCESS = "[Working Context] Get Working Context Success", + GET_WORKING_CONTEXT_FAILURE = "[Working Context] Get Working Context Failure", + UPDATE_WORKING_CONTEXT = "[Working Context] Update Working Context", + FETCH_IS_WORKING_CONTEXT_ENABLED = "[Working Context] Is Working Context Enabled", + FETCH_IS_WORKING_CONTEXT_ENABLED_SUCCESS = "[Working Context] Is Working Context Enabled Success", + FETCH_IS_WORKING_CONTEXT_ENABLED_FAILURE = "[Working Context] Is Working Context Enabled Failure", +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.actions.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.actions.ts new file mode 100644 index 0000000..7b67162 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.actions.ts @@ -0,0 +1,14 @@ +import { IoIntelWorkingContext } from "@interopio/working-context"; +import { createAction, props } from "@ngrx/store"; + +import { WORKING_CONTEXT_ACTIONS } from "./working-context.actions.enum"; + +export const getWorkingContext = createAction(WORKING_CONTEXT_ACTIONS.GET_WORKING_CONTEXT); +export const getWorkingContextSuccess = createAction(WORKING_CONTEXT_ACTIONS.GET_WORKING_CONTEXT_SUCCESS, props<{ workingContext: Record }>()); +export const getWorkingContextFailure = createAction(WORKING_CONTEXT_ACTIONS.GET_WORKING_CONTEXT_FAILURE, props<{ error: Error }>()); + +export const updateWorkingContext = createAction(WORKING_CONTEXT_ACTIONS.UPDATE_WORKING_CONTEXT, props<{ workingContext: Record }>()); + +export const fetchIsWorkingContextEnabled = createAction(WORKING_CONTEXT_ACTIONS.FETCH_IS_WORKING_CONTEXT_ENABLED); +export const fetchIsWorkingContextEnabledSuccess = createAction(WORKING_CONTEXT_ACTIONS.FETCH_IS_WORKING_CONTEXT_ENABLED_SUCCESS, props<{ isEnabled: boolean }>()); +export const fetchIsWorkingContextEnabledFailure = createAction(WORKING_CONTEXT_ACTIONS.FETCH_IS_WORKING_CONTEXT_ENABLED_FAILURE, props<{ error: Error }>()); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.effects.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.effects.ts new file mode 100644 index 0000000..6703b5c --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.effects.ts @@ -0,0 +1,56 @@ +import { inject, Injectable } from "@angular/core"; +import { IoIntelWorkingContext } from "@interopio/working-context"; +import { Actions, createEffect, ofType } from "@ngrx/effects"; +import { of, catchError, mergeMap, from, map } from "rxjs"; + +import { + getWorkingContext, + getWorkingContextFailure, + getWorkingContextSuccess, + fetchIsWorkingContextEnabledFailure, + fetchIsWorkingContextEnabledSuccess, + fetchIsWorkingContextEnabled, +} from "./working-context.actions"; +import { WorkingContextService } from "../../services/working-context/working-context.service"; + +@Injectable() +export class WorkingContextEffects { + private readonly _actions$: Actions = inject(Actions); + private readonly _workingContextService: WorkingContextService = inject(WorkingContextService); + + getWorkingContext$ = createEffect(() => + this._actions$.pipe( + ofType(getWorkingContext), + mergeMap(() => { + return from(this._workingContextService.getWorkingContext()).pipe( + map((workingContext: Record) => getWorkingContextSuccess({ workingContext })), + catchError((error) => + of( + getWorkingContextFailure({ + error: error instanceof Error ? error : new Error(String(error)), + }) + ) + ) + ); + }) + ) + ); + + isWorkingContextEnabled$ = createEffect(() => + this._actions$.pipe( + ofType(fetchIsWorkingContextEnabled), + mergeMap(() => { + return from(this._workingContextService.isWorkingContextEnabled()).pipe( + map((isEnabled: boolean) => fetchIsWorkingContextEnabledSuccess({ isEnabled })), + catchError((error) => + of( + fetchIsWorkingContextEnabledFailure({ + error: error instanceof Error ? error : new Error(String(error)), + }) + ) + ) + ); + }) + ) + ); +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.facade.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.facade.ts new file mode 100644 index 0000000..c2d9b76 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.facade.ts @@ -0,0 +1,33 @@ +import { inject, Injectable, Signal } from "@angular/core"; +import { IoIntelWorkingContext } from "@interopio/working-context"; +import { Store } from "@ngrx/store"; + +import { fetchIsWorkingContextEnabled, getWorkingContext, updateWorkingContext } from "./working-context.actions"; +import { isWorkingContextEnabled, selectWorkingContext } from "./working-context.selector"; + +@Injectable() +export class WorkingContextFacade { + private readonly _store: Store = inject(Store); + + private readonly _workingContext: Signal> = this._store.selectSignal>(selectWorkingContext); + public get workingContext(): Signal> { + return this._workingContext; + } + + private readonly _isWorkingContextEnabled: Signal = this._store.selectSignal(isWorkingContextEnabled); + public get isWorkingContextEnabled(): Signal { + return this._isWorkingContextEnabled; + } + + public dispatchGetWorkingContext(): void { + this._store.dispatch(getWorkingContext()); + } + + public dispatchUpdateWorkingContext(workingContext: Record): void { + this._store.dispatch(updateWorkingContext({ workingContext })); + } + + public dispatchFetchIsWorkingContextEnabled(): void { + this._store.dispatch(fetchIsWorkingContextEnabled()); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.reducer.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.reducer.ts new file mode 100644 index 0000000..bb3d10d --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.reducer.ts @@ -0,0 +1,74 @@ +import { IoIntelWorkingContext } from "@interopio/working-context"; +import { createReducer, on } from "@ngrx/store"; + +import { + fetchIsWorkingContextEnabled, + fetchIsWorkingContextEnabledFailure, + fetchIsWorkingContextEnabledSuccess, + getWorkingContext, + getWorkingContextFailure, + getWorkingContextSuccess, + updateWorkingContext, +} from "./working-context.actions"; +import { LOADING_STATE } from "../../enums/loading-state.enum"; +import { LoadingType } from "../../types/loading.type"; + +export type WorkingContextReducerStateType = { + isEnabled: boolean; + workingContext: Record; + loadingState: LoadingType; +}; + +export const initialState: WorkingContextReducerStateType = { + isEnabled: false, + workingContext: {}, + loadingState: { type: LOADING_STATE.NOT_STARTED }, +}; + +export const workingContextReducer = createReducer( + initialState, + on( + getWorkingContext, + (state): WorkingContextReducerStateType => ({ + ...state, + loadingState: { type: LOADING_STATE.LOADING }, + }) + ), + on( + getWorkingContextSuccess, + (state, { workingContext }): WorkingContextReducerStateType => ({ + ...state, + workingContext, + loadingState: { type: LOADING_STATE.SUCCESS }, + }) + ), + on( + getWorkingContextFailure, + (state, { error }): WorkingContextReducerStateType => ({ + ...state, + loadingState: { type: LOADING_STATE.ERROR, message: error.message }, + }) + ), + on( + updateWorkingContext, + (state, { workingContext }): WorkingContextReducerStateType => ({ + ...state, + workingContext, + }) + ), + on(fetchIsWorkingContextEnabled, (state): WorkingContextReducerStateType => ({ ...state, isEnabled: false })), + on( + fetchIsWorkingContextEnabledSuccess, + (state, { isEnabled }): WorkingContextReducerStateType => ({ + ...state, + isEnabled, + }) + ), + on( + fetchIsWorkingContextEnabledFailure, + (state, { error }): WorkingContextReducerStateType => ({ + ...state, + loadingState: { type: LOADING_STATE.ERROR, message: error.message }, + }) + ) +); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.selector.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.selector.ts new file mode 100644 index 0000000..b3d3072 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.selector.ts @@ -0,0 +1,9 @@ +import { createFeatureSelector, createSelector } from "@ngrx/store"; + +import { WorkingContextReducerStateType } from "./working-context.reducer"; + +export const selectWorkingContextStore = createFeatureSelector("workingContextStore"); + +export const selectWorkingContext = createSelector(selectWorkingContextStore, (state: WorkingContextReducerStateType) => state.workingContext); + +export const isWorkingContextEnabled = createSelector(selectWorkingContextStore, (state: WorkingContextReducerStateType) => state.isEnabled); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/styles/_defaults.css b/templates/io-assist-angular-editable/src/app/io-assist/shared/styles/_defaults.css new file mode 100644 index 0000000..2c0d5e7 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/styles/_defaults.css @@ -0,0 +1,297 @@ +/* Use this object to define variables that you want to use across the application. + Prefix all variables with --app- to avoid conflicts with other libraries and to + make it clear that these are application specific variables. +*/ +@layer base { + html, + body { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + } + + :root { + /* General */ + /* General [Colors] */ + --app-red-700: #c92121; + --app-red-800: #b01d1d; + --app-orange: #f25f1f; + --app-blue: #4b92fb; + --app-light-blue: #D8EBFD; + --app-blue-toggle: #356ec4; + --app-yellow: #deaf37; + --app-neutrals-0: #ffffff; + --app-neutrals-25: #f7f7f7; + --app-neutrals-50: #f2f2f2; + --app-neutrals-75: #ededed; + --app-neutrals-100: #eaeaeb; + --app-neutrals-150: #dadbdd; + --app-neutrals-200: #caccce; + --app-neutrals-300: #b0b2b5; + --app-neutrals-400: #95989d; + --app-neutrals-500: #7b7e84; + --app-neutrals-600: #62656a; + --app-neutrals-650: #55585d; + --app-neutrals-700: #494b50; + --app-neutrals-750: #3d3f43; + --app-neutrals-800: #36383b; + --app-neutrals-850: #2e3033; + --app-neutrals-900: #26272a; + --app-neutrals-950: #18191b; + --app-neutrals-1000: #000000; + + /* Accent [Colors] */ + --app-accent-color-1: var(--app-orange); + --app-accent-color-2: var(--app-blue); + --app-accent-color-3: var(--app-yellow); + + /* General [Spacings] */ + --app-spacing-1: 2px; + --app-spacing-2: 4px; + --app-spacing-3: 6px; + --app-spacing-4: 8px; + --app-spacing-5: 10px; + --app-spacing-6: 12px; + + /* General [Border Radius] */ + --app-border-radius-sm: 8px; + + /* General [Font Sizes] */ + --app-font-size-10: 10px; + --app-font-size-12: 12px; + + /* General [Line Heights] */ + --app-line-height-13: 13px; + + /* General [Letter Spacing] */ + --app-letter-spacing-01: 0.1px; + + /* Border */ + /* Border [Colors] */ + --app-border-error: var(--app-red-700); + + /* Border [Sizes] */ + --app-border-size-1: 1px; + + /* Border [Sizes] */ + /* ... */ + + /* Text */ + /* Text [Colors] */ + /* ... */ + + /* Text [Weights] */ + --app-font-weight-400: 400; + --app-font-weight-600: 600; + + /* Text [Sizes] */ + /* ... */ + --app-default-box-shadow: 0 3px 11px 6px rgba(0, 0, 0, 0.16); + + /* Prototype Colors */ + /* This colors are used for prototyping components and should be replaced with + the actual design system colors once they are available */ + --prototype-temp-color-50: #f9f9f9; + --prototype-temp-color-100: #f2f2f2; + --prototype-temp-color-200: #e5e5e5; + --prototype-temp-color-300: #cccccc; + --prototype-temp-color-400: #b3b3b3; + --prototype-temp-color-500: #999999; + --prototype-temp-color-600: #808080; + --prototype-temp-color-700: #666666; + --prototype-temp-color-800: #4d4d4d; + --prototype-temp-color-900: #333333; + --prototype-temp-color-1000: #1a1a1a; + } + + .dark { + --app-background: var(--app-neutrals-900); + --app-background-secondary: var(--app-neutrals-850); + --app-border-default: var(--app-neutrals-700); + --app-border-disabled-default: var(--app-neutrals-850); + --app-input-border: var(--app-neutrals-750); + --app-button-border-default: var(--app-neutrals-500); + --app-background-disabled: var(--app-neutrals-900); + --app-text-disabled: var(--app-neutrals-600); + --app-icon-disabled: var(--app-neutrals-600); + --app-icon-background-hover: var(--app-neutrals-800); + --app-icon-background-disabled: var(--app-neutrals-700); + --app-thread-list-item-hover-background: var(--app-neutrals-800); + --app-text-black-white: var(--app-neutrals-0); + --app-border-hover: var(--app-neutrals-600); + + /* Panel */ + --app-panel-background: var(--app-neutrals-900); + --app-panel-border: var(--app-neutrals-700); + + /* User Message */ + --app-user-message-background: var(--app-neutrals-800); + --app-user-message-text: var(--app-neutrals-300); + + /* Input Specific */ + --app-input-background-disabled: var(--app-neutrals-900); + --app-input-background: var(--app-neutrals-850); + --app-input-background-hover: var(--app-neutrals-800); + --app-input-background-active: var(--app-neutrals-800); + --app-input-border: var(--app-neutrals-900); + --app-input-border-hover: var(--app-neutrals-750); + --app-input-border-active: var(--app-neutrals-600); + --app-input-text: var(--app-neutrals-200); + --app-input-text-active: var(--app-neutrals-0); + --app-input-icon: var(--app-neutrals-200); + --app-input-icon-active: var(--app-neutrals-0); + --app-input-icon-disabled: var(--app-neutrals-600); + + /* Text Colors */ + --app-text-states-disabled: var(--app-neutrals-600); + --app-text-states-hover: var(--app-neutrals-100); + --app-text-states-active: var(--app-neutrals-0); + --app-text-default: var(--app-neutrals-300); + --app-text-secondary: var(--app-neutrals-500); + --app-text-error: var(--app-red-700); + + /* Toggle Component */ + --app-toggle-background-disabled: var(--app-neutrals-750); + --app-toggle-background-enabled: var(--app-blue-toggle); + --app-toggle-background-hover-disabled: var(--app-neutrals-700); + --app-toggle-background-hover-enabled: var(--app-blue); + --app-toggle-circle: var(--app-neutrals-0); + + /* Tooltip Component */ + --app-tooltip-background: var(--app-neutrals-850); + --app-tooltip-border: var(--app-neutrals-700); + --app-tooltip-text: var(--app-neutrals-100); + + /* Prototype Colors */ + --app-proto-background: var(--prototype-temp-color-1000); + --app-proto-background-light: var(--prototype-temp-color-900); + --app-proto-border-default: var(--prototype-temp-color-800); + --app-proto-text-default: var(--prototype-temp-color-300); + --app-proto-text-disabled: var(--prototype-temp-color-600); + } + + /* Use this object to define variables that you want to use in light mode */ + .light { + --app-background: var(--app-neutrals-0); + --app-background-secondary: var(--app-neutrals-25); + --app-border-default: var(--app-neutrals-150); + --app-border-disabled-default: var(--app-neutrals-100); + --app-input-border: var(--app-neutrals-200); + --app-button-border-default: var(--app-neutrals-200); + --app-background-disabled: var(--app-neutrals-50); + --app-text-disabled: var(--app-neutrals-400); + --app-icon-disabled: var(--app-neutrals-400); + --app-icon-background-hover: var(--app-neutrals-25); + --app-icon-background-disabled: var(--app-neutrals-100); + --app-thread-list-item-hover-background: var(--app-neutrals-50); + --app-text-black-white: var(--app-neutrals-1000); + --app-border-hover: var(--app-neutrals-150); + + /* Panel */ + --app-panel-background: var(--app-neutrals-0); + --app-panel-border: var(--app-neutrals-700); + + /* User Message */ + --app-user-message-background: var(--app-neutrals-50); + --app-user-message-text: var(--app-neutrals-700); + + /* Input Specific */ + --app-input-background-disabled: var(--app-neutrals-50); + --app-input-background: var(--app-neutrals-0); + --app-input-background-hover: var(--app-neutrals-100); + --app-input-background-active: var(--app-neutrals-25); + --app-input-border: var(--app-neutrals-150); + --app-input-border-hover: var(--app-neutrals-200); + --app-input-border-active: var(--app-neutrals-400); + --app-input-text: var(--app-neutrals-800); + --app-input-text-active: var(--app-neutrals-900); + --app-input-icon: var(--app-neutrals-700); + --app-input-icon-active: var(--app-neutrals-900); + --app-input-icon-disabled: var(--app-neutrals-400); + + /* Text Colors */ + --app-text-states-disabled: var(--app-neutrals-400); + --app-text-states-hover: var(--app-neutrals-800); + --app-text-states-active: var(--app-neutrals-900); + --app-text-default: var(--app-neutrals-700); + --app-text-secondary: var(--app-neutrals-500); + --app-text-error: var(--app-red-700); + + /* Toggle Component */ + --app-toggle-background-disabled: var(--app-neutrals-200); + --app-toggle-background-enabled: var(--app-blue-toggle); + --app-toggle-background-hover-disabled: var(--app-neutrals-300); + --app-toggle-background-hover-enabled: var(--app-blue); + --app-toggle-circle: var(--app-neutrals-0); + + /* Tooltip Component */ + --app-tooltip-background: var(--app-neutrals-0); + --app-tooltip-border: var(--app-neutrals-200); + --app-tooltip-text: var(--app-neutrals-800); + + /* Prototype Colors */ + --app-proto-background: var(--prototype-temp-color-50); + --app-proto-background-light: var(--prototype-temp-color-100); + --app-proto-border-default: var(--prototype-temp-color-300); + --app-proto-text-default: var(--prototype-temp-color-700); + --app-proto-text-disabled: var(--prototype-temp-color-400); + } + + .custom-scrollbar { + scrollbar-width: 6px; + scrollbar-color: rgba(0, 0, 0, 0.3) transparent; + scrollbar-gutter: stable both-edges; + } + + .custom-scrollbar-snap-right { + /* Margin fix: Visually make sure that the default spacings that come with stable both-edges are reduced so we make the scroll snap to the right edge of the screen + content not being pushed */ + margin-right: -1.5%; + margin-left: -1.5%; + } + + .custom-scrollbar::-webkit-scrollbar { + width: 6px; + scrollbar-gutter: stable both-edges; + } + + .custom-scrollbar-snap-right::-webkit-scrollbar { + /* Margin fix: Visually make sure that the default spacings that come with stable both-edges are reduced so we make the scroll snap to the right edge of the screen + content not being pushed */ + margin-right: -1.5%; + margin-left: -1.5%; + } + + + .custom-scrollbar::-webkit-scrollbar-track { + background: transparent; + } + + .custom-scrollbar::-webkit-scrollbar-button { + display: none; + width: 0; + height: 0; + } + + .custom-scrollbar::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.25); + border-radius: 10px; + } + + .custom-scrollbar::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.35); + } + + .safe-word-break { + white-space: normal; + overflow-wrap: break-word; + word-break: normal; + hyphens: auto; + } + + /* Prevent flex items from expanding beyond their container */ + assistant-message { + min-width: 0; + max-width: 100%; + overflow: hidden; + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/styles/_utils.css b/templates/io-assist-angular-editable/src/app/io-assist/shared/styles/_utils.css new file mode 100644 index 0000000..1b8ea9a --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/styles/_utils.css @@ -0,0 +1,157 @@ +@import './_defaults.css'; + +/* + Use this object to define variables that you need to get included + as a tailwind utility and use as class name directly. + + Example usage: + ComponentName + ComponentName [Colors] + --keyword-variable-name: #hexcode/rgba/size/cssvariable; + + Where: + --keyword = tailwind prefix which controls the visibility of the variable among + tailwind utilities. Once a tailwind utility is generated, you can use it + directly as a class name in your html. + + cssvariable - Using css variables enables us to support dynamic theme by defining the + colors in :only-of-type place using the .dark and .light classes + + the ability to provide them as tailwind utilities. + + More information on theme variables can be found here: https://tailwindcss.com/docs/theme#theme-variable-namespaces +*/ +@theme { + /* General */ + /* General [Colors] */ + /* ... */ + --color-app-background: var(--app-background); + --color-app-background-secondary: var(--app-background-secondary); + --color-app-accent-color-1: var(--app-accent-color-1); + --color-app-accent-color-2: var(--app-accent-color-2); + --color-app-accent-color-3: var(--app-accent-color-3); + + /* General [Spacings] */ + --spacing-spacing-1: var(--app-spacing-1); + --spacing-spacing-2: var(--app-spacing-2); + --spacing-spacing-3: var(--app-spacing-3); + --spacing-spacing-4: var(--app-spacing-4); + --spacing-spacing-5: var(--app-spacing-5); + --spacing-spacing-6: var(--app-spacing-6); + + /* Font */ + /* Font [Types] */ + --font-default: 'Inter Variable', sans-serif; + + /* Font [Colors] */ + --color-text-states-disabled: var(--app-text-states-disabled); + --color-text-states-hover: var(--app-text-states-hover); + --color-text-states-active: var(--app-text-states-active); + --color-text-default: var(--app-text-default); + --color-text-secondary: var(--app-text-secondary); + --color-text-black-white: var(--app-text-black-white); + --color-text-danger: var(--app-red-700); + + /* Thread History */ + /* Thread History [Colors] */ + --color-thread-list-item-hover-background: var(--app-thread-list-item-hover-background); + + /* Font [Weights] */ + --font-weight-weight-400: var(--app-font-weight-400); + --font-weight-weight-600: var(--app-font-weight-600); + + /* Borders */ + /* Borders [Colors] */ + --color-border-default: var(--app-border-default); + --color-border-hover: var(--app-border-hover); + + /* AppPanelComponent */ + /* AppPanelComponent [Colors] */ + --color-panel-background: var(--app-panel-background); + --color-panel-border: var(--app-panel-border); + --shadow-panel-box-shadow: var(--app-default-box-shadow); + + /* AppButtonComponent */ + /* AppButtonComponent [Colors] */ + --color-button-submit-background: var(--app-blue); + --color-button-danger-background: var(--app-red-700); + --color-button-border: var(--app-neutrals-1000); + --color-button-border-default: var(--app-button-border-default); + --color-button-background-disabled: var(--app-background-disabled); + --color-button-text-disabled: var(--app-text-disabled); + + /* UserMessageComponent */ + /* UserMessageComponent [Colors] */ + --color-user-message-background: var(--app-user-message-background); + --color-user-message-text: var(--app-user-message-text); + + /* AppInputComponent */ + /* AppInputComponent [Colors] */ + --color-input-background-disabled: var(--app-input-background-disabled); + --color-input-border: var(--app-input-border); + --color-input-border-disabled: var(--app-border-disabled-default); + --color-input-default: var(--app-text-default); + --color-input-label-disabled: var(--app-text-states-disabled); + --color-input-background: var(--app-input-background); + --color-input-background-hover: var(--app-input-background-hover); + --color-input-background-active: var(--app-input-background-active); + --color-input-border: var(--app-input-border); + --color-input-border-hover: var(--app-input-border-hover); + --color-input-border-active: var(--app-input-border-active); + --color-input-text: var(--app-input-text); + --color-input-text-active: var(--app-input-text-active); + --color-input-icon: var(--app-input-icon); + --color-input-icon-active: var(--app-input-icon-active); + --color-input-icon-disabled: var(--app-input-icon-disabled); + --color-input-error-border: var(--app-border-error); + --color-input-error-text: var(--app-text-error); + + /* AppInputComponent [Sizes] */ + --border-width-input-border: var(--app-border-size-1); + --radius-input-radius: var(--app-border-radius-sm); + --font-size-input-text: var(--app-font-size-12); + --font-size-input-error: var(--app-font-size-10); + --line-height-input-error: var(--app-line-height-13); + --tracking-input-error: var(--app-letter-spacing-01); + + /* AppIconComponent [Colors] */ + --color-app-icon-background-hover: var(--app-icon-background-hover); + --color-app-icon-background-disabled: var(--app-icon-background-disabled); + + /* AppToggleComponent */ + /* AppToggleComponent [Colors] */ + --color-toggle-background-disabled: var(--app-toggle-background-disabled); + --color-toggle-background-enabled: var(--app-toggle-background-enabled); + --color-toggle-background-hover-disabled: var(--app-toggle-background-hover-disabled); + --color-toggle-background-hover-enabled: var(--app-toggle-background-hover-enabled); + --color-toggle-circle: var(--app-toggle-circle); + + /* AppTooltipComponent */ + /* AppTooltipComponent [Colors] */ + --color-tooltip-background: var(--app-tooltip-background); + --color-tooltip-border: var(--app-tooltip-border); + --color-tooltip-text: var(--app-tooltip-text); + + /* Prototype Colors */ + --color-proto-background: var(--app-proto-background); + --color-proto-background-light: var(--app-proto-background-light); + --color-proto-border-default: var(--app-proto-border-default); + --color-proto-text-default: var(--app-proto-text-default); + --color-proto-text-disabled: var(--app-proto-text-disabled); +} + +/* + Global keyboard-focus indicator. + Uses :focus-visible so the outline only appears on keyboard navigation, + not on mouse/pointer clicks. + Input-like elements are excluded because browsers always match :focus-visible + on them (even on click); they handle focus styles at the component level. + + Placed in @layer base so Tailwind utilities (e.g. outline-none) can override + it in components that provide their own focus styling. +*/ +@layer base { + *:focus-visible:not(input):not(textarea):not(select) { + outline: 2px solid var(--app-border-hover); + outline-offset: -2px; + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/types/loading.type.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/types/loading.type.ts new file mode 100644 index 0000000..b9b2429 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/types/loading.type.ts @@ -0,0 +1,11 @@ +import { LOADING_STATE, MESSAGES_LOADING_STATE } from "../enums/loading-state.enum"; + +export type LoadingType = { type: LOADING_STATE.LOADING } | { type: LOADING_STATE.SUCCESS } | { type: LOADING_STATE.NOT_STARTED } | { type: LOADING_STATE.ERROR; message: string }; + +export type MessagesLoadingType = + | { type: MESSAGES_LOADING_STATE.GET_RESPONSE_SUCCESS } + | { type: MESSAGES_LOADING_STATE.FETCH_MESSAGES_FROM_THREAD_SUCCESS } + | { type: MESSAGES_LOADING_STATE.LOADING_FROM_THREAD } + | { type: MESSAGES_LOADING_STATE.LOADING_RESPONSE } + | { type: MESSAGES_LOADING_STATE.NOT_STARTED } + | { type: MESSAGES_LOADING_STATE.ERROR; message: string }; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/utils/icon.utils.ts b/templates/io-assist-angular-editable/src/app/io-assist/shared/utils/icon.utils.ts new file mode 100644 index 0000000..84f5f74 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/shared/utils/icon.utils.ts @@ -0,0 +1,226 @@ +import { IOConnectCore } from "@interopio/core"; + +import { IconResource, IconType } from "../../io-assist.types"; + +/** + * Determines the icon type from the data string. + * Auto-detects data URLs and distinguishes between SVG strings and regular URLs. + */ +export function detectIconType(data: string): IconType { + if (!data) { + throw new Error("Icon data cannot be empty"); + } + + if (data.startsWith("data:")) { + return "data-url"; + } + + if (data.trim().startsWith(" tag (prevents SVG from rendering) + * - width and height attributes (allows CSS sizing to work) + * - Replaces hardcoded fill colors with "currentColor" for theming + * - Ensures viewBox is present (adds default "0 0 24 24" if missing) + * - Removes dangerous attributes: event handlers (on*), style, script tags + * + * @param svgString - The raw SVG string + * @returns Cleaned SVG string ready for use + * + * @example + * const rawSvg = ` + * + * `; + * const cleanedSvg = stripSvgAttributes(rawSvg); + * // Result: ` + * // + * // ` + */ +export function stripSvgAttributes(svgString: string, logger?: IOConnectCore.Logger.API): string { + if (!svgString || !svgString.trim().startsWith(" + svg.removeAttribute("width"); + svg.removeAttribute("height"); + + // Only remove fill="none" from root svg, not children + if (svg.getAttribute("fill") === "none") { + svg.removeAttribute("fill"); + } + + // Ensure viewBox exists + if (!svg.hasAttribute("viewBox")) { + svg.setAttribute("viewBox", `0 0 ${width} ${height}`); + } + + // Process all child elements + const allElements = svg.querySelectorAll("*"); + allElements.forEach((element) => { + // Replace hardcoded fill colors with currentColor (preserve "none") + const fillAttr = element.getAttribute("fill"); + if (fillAttr && fillAttr !== "none" && fillAttr !== "currentColor") { + element.setAttribute("fill", "currentColor"); + } + + // Security: Remove dangerous attributes + removeDangerousAttributes(element); + }); + + // Security: Remove dangerous attributes from root svg + removeDangerousAttributes(svg); + + return new XMLSerializer().serializeToString(svg); + } catch (error) { + (logger ?? console).error("SVG parsing error:", error instanceof Error ? error : new Error(String(error))); + return ""; + } +} + +/** + * Removes security-sensitive attributes from an SVG element. + */ +function removeDangerousAttributes(element: Element): void { + // Remove event handlers + Array.from(element.attributes).forEach((attr) => { + if (attr.name.startsWith("on")) { + element.removeAttribute(attr.name); + } + }); + + // Remove style (can contain javascript: urls) + element.removeAttribute("style"); + + // Remove potentially dangerous href attributes + const href = element.getAttribute("href") || element.getAttribute("xlink:href"); + if (href?.startsWith("javascript:")) { + element.removeAttribute("href"); + element.removeAttribute("xlink:href"); + } + + // Remove script tags + if (element.tagName.toLowerCase() === "script") { + element.remove(); + } +} diff --git a/templates/io-assist-angular-editable/src/app/io-assist/styles.css b/templates/io-assist-angular-editable/src/app/io-assist/styles.css new file mode 100644 index 0000000..5ad0677 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/styles.css @@ -0,0 +1,14 @@ +/* You can add global styles to this file, and also import other style files */ +@import 'prismjs/themes/prism-twilight.css'; +@import 'tailwindcss'; +@import './shared/styles/_utils.css'; + +/* Inter Variable — latin subset only (weights 100–900, normal style) */ +@font-face { + font-family: 'Inter Variable'; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url('./files/inter-latin-wght-normal.woff2') format('woff2-variations'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/templates/io-assist-angular-editable/src/index.html b/templates/io-assist-angular-editable/src/index.html index 2c711a5..f41edb6 100644 --- a/templates/io-assist-angular-editable/src/index.html +++ b/templates/io-assist-angular-editable/src/index.html @@ -6,6 +6,7 @@ + diff --git a/templates/io-assist-angular-editable/src/styles.css b/templates/io-assist-angular-editable/src/styles.css index 1438286..11f76e7 100644 --- a/templates/io-assist-angular-editable/src/styles.css +++ b/templates/io-assist-angular-editable/src/styles.css @@ -1,4 +1,10 @@ -@import "@interopio/io-assist-ng/styles.css"; +/* + * io-assist's Tailwind styles + Inter font are built separately by + * src/app/io-assist/scripts/io-assist-angular-build.js into + * dist/io-assist-angular/browser/styles.css (mirroring + * @interopio/io-assist-ng's own dist/styles.css) and loaded via the + * in src/index.html, so they are not imported here. + */ html, body { From e5e8d8dcbaa3ba65a8a9789e2e68ecb63c58a6b5 Mon Sep 17 00:00:00 2001 From: Aleksandar Dimitrov Date: Fri, 31 Jul 2026 18:46:04 +0300 Subject: [PATCH 2/7] fix styles and fonts --- .../io-assist-angular-editable/.gitignore | 4 - .../.postcssrc.json | 5 + .../package-lock.json | 466 ++---------------- .../io-assist-angular-editable/package.json | 5 +- .../scripts/io-assist-angular-build.js | 41 -- .../src/app/io-assist/styles.css | 11 +- .../io-assist-angular-editable/src/index.html | 1 - .../io-assist-angular-editable/src/styles.css | 8 +- 8 files changed, 41 insertions(+), 500 deletions(-) create mode 100644 templates/io-assist-angular-editable/.postcssrc.json delete mode 100644 templates/io-assist-angular-editable/src/app/io-assist/scripts/io-assist-angular-build.js diff --git a/templates/io-assist-angular-editable/.gitignore b/templates/io-assist-angular-editable/.gitignore index 2542831..854acd5 100644 --- a/templates/io-assist-angular-editable/.gitignore +++ b/templates/io-assist-angular-editable/.gitignore @@ -11,10 +11,6 @@ npm-debug.log yarn-error.log -# Generated by scripts/io-assist-angular-build.js (mirrors @interopio/io-assist-ng's dist/styles.css + dist/files) -/public/styles.css -/public/files - # IDEs and editors .idea/ .project diff --git a/templates/io-assist-angular-editable/.postcssrc.json b/templates/io-assist-angular-editable/.postcssrc.json new file mode 100644 index 0000000..fddc8af --- /dev/null +++ b/templates/io-assist-angular-editable/.postcssrc.json @@ -0,0 +1,5 @@ +{ + "plugins": { + "@tailwindcss/postcss": {} + } +} diff --git a/templates/io-assist-angular-editable/package-lock.json b/templates/io-assist-angular-editable/package-lock.json index 9ddfa25..7ade8a7 100644 --- a/templates/io-assist-angular-editable/package-lock.json +++ b/templates/io-assist-angular-editable/package-lock.json @@ -39,8 +39,9 @@ "@angular/compiler-cli": "^21.2.0", "@interopio/core": ">=6.0.0", "@interopio/desktop": ">=6.0.0", - "@tailwindcss/cli": "^4.1.13", + "@tailwindcss/postcss": "^4.1.13", "jsdom": "^28.0.0", + "postcss": "^8.5.6", "prettier": "^3.8.1", "tailwindcss": "^4.1.13", "typescript": "~5.9.2", @@ -263,6 +264,19 @@ "node": ">= 14.0.0" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -4719,354 +4733,6 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, - "node_modules/@tailwindcss/cli": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.3.3.tgz", - "integrity": "sha512-ZvS/n1ZHOBKcVlhkt8l5NNr1EDXk1NboYO5CYDOs6NUmvT9z6bzkwsosaJftY57T/3gWNzWMJzIXLodZC8ssdw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@parcel/watcher": "2.5.1", - "@tailwindcss/node": "4.3.3", - "@tailwindcss/oxide": "4.3.3", - "enhanced-resolve": "^5.24.1", - "mri": "^1.2.0", - "picocolors": "^1.1.1", - "tailwindcss": "4.3.3" - }, - "bin": { - "tailwindcss": "dist/index.mjs" - } - }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" - } - }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@tailwindcss/cli/node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/@tailwindcss/cli/node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@tailwindcss/node": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", @@ -5324,6 +4990,20 @@ "node": ">= 20" } }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" + } + }, "node_modules/@tufjs/canonical-json": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", @@ -5806,19 +5486,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/browserslist": { "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", @@ -6865,19 +6532,6 @@ } } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -7320,6 +6974,7 @@ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", + "optional": true, "engines": { "node": ">=0.10.0" } @@ -7346,6 +7001,7 @@ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -7366,16 +7022,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -8897,33 +8543,6 @@ ], "license": "MIT" }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -9111,16 +8730,6 @@ "node": ">= 18" } }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -10771,19 +10380,6 @@ "dev": true, "license": "MIT" }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", diff --git a/templates/io-assist-angular-editable/package.json b/templates/io-assist-angular-editable/package.json index 4e32922..c8048d4 100644 --- a/templates/io-assist-angular-editable/package.json +++ b/templates/io-assist-angular-editable/package.json @@ -4,7 +4,7 @@ "scripts": { "ng": "ng", "start": "ng serve --port 4103", - "build": "node src/app/io-assist/scripts/io-assist-angular-build.js", + "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" }, @@ -42,8 +42,9 @@ "@angular/compiler-cli": "^21.2.0", "@interopio/core": ">=6.0.0", "@interopio/desktop": ">=6.0.0", - "@tailwindcss/cli": "^4.1.13", + "@tailwindcss/postcss": "^4.1.13", "jsdom": "^28.0.0", + "postcss": "^8.5.6", "prettier": "^3.8.1", "tailwindcss": "^4.1.13", "typescript": "~5.9.2", diff --git a/templates/io-assist-angular-editable/src/app/io-assist/scripts/io-assist-angular-build.js b/templates/io-assist-angular-editable/src/app/io-assist/scripts/io-assist-angular-build.js deleted file mode 100644 index b49102c..0000000 --- a/templates/io-assist-angular-editable/src/app/io-assist/scripts/io-assist-angular-build.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * io-assist-ng-build.js - * - * Build script for this app, invoked as the `build` npm script (no separate - * prebuild/prestart step to run). 1:1 with @interopio/io-assist-ng's own - * build script (libs/io-assist-ng/scripts/io-assist-ng-build.js in - * iointel-js), adapted so the generated files land under this app's build - * output (dist/io-assist-angular/browser) instead of being committed to src/. - * - * Steps: - * 1. ng build — compile the Angular library - * 2. tailwindcss — compile and minify styles.css → dist/styles.css - * 3. copy-fonts — copy Inter woff2 files to dist/files/ - */ - -const { execSync } = require("child_process"); -const fs = require("fs"); -const path = require("path"); - -const DIST_BROWSER = "dist/io-assist-angular/browser"; - -const run = (cmd) => { - console.log(`\n> ${cmd}`); - execSync(cmd, { stdio: "inherit" }); -}; - -const copyFonts = () => { - const fontPackageDir = path.dirname(require.resolve("@fontsource-variable/inter/wght.css")); - const srcFile = path.join(fontPackageDir, "files", "inter-latin-wght-normal.woff2"); - const destDir = path.resolve(__dirname, "../../../../", DIST_BROWSER, "files"); - const destFile = path.join(destDir, "inter-latin-wght-normal.woff2"); - - fs.mkdirSync(destDir, { recursive: true }); - fs.copyFileSync(srcFile, destFile); - - console.log(`Copied inter-latin-wght-normal.woff2 -> ${DIST_BROWSER}/files/`); -}; - -run("ng build"); -run(`npx tailwindcss -i src/app/io-assist/styles.css -o ${DIST_BROWSER}/styles.css --minify`); -copyFonts(); diff --git a/templates/io-assist-angular-editable/src/app/io-assist/styles.css b/templates/io-assist-angular-editable/src/app/io-assist/styles.css index 5ad0677..be8719c 100644 --- a/templates/io-assist-angular-editable/src/app/io-assist/styles.css +++ b/templates/io-assist-angular-editable/src/app/io-assist/styles.css @@ -2,13 +2,4 @@ @import 'prismjs/themes/prism-twilight.css'; @import 'tailwindcss'; @import './shared/styles/_utils.css'; - -/* Inter Variable — latin subset only (weights 100–900, normal style) */ -@font-face { - font-family: 'Inter Variable'; - font-style: normal; - font-display: swap; - font-weight: 100 900; - src: url('./files/inter-latin-wght-normal.woff2') format('woff2-variations'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} +@import '@fontsource-variable/inter/wght.css'; diff --git a/templates/io-assist-angular-editable/src/index.html b/templates/io-assist-angular-editable/src/index.html index f41edb6..2c711a5 100644 --- a/templates/io-assist-angular-editable/src/index.html +++ b/templates/io-assist-angular-editable/src/index.html @@ -6,7 +6,6 @@ - diff --git a/templates/io-assist-angular-editable/src/styles.css b/templates/io-assist-angular-editable/src/styles.css index 11f76e7..4712724 100644 --- a/templates/io-assist-angular-editable/src/styles.css +++ b/templates/io-assist-angular-editable/src/styles.css @@ -1,10 +1,4 @@ -/* - * io-assist's Tailwind styles + Inter font are built separately by - * src/app/io-assist/scripts/io-assist-angular-build.js into - * dist/io-assist-angular/browser/styles.css (mirroring - * @interopio/io-assist-ng's own dist/styles.css) and loaded via the - * in src/index.html, so they are not imported here. - */ +@import "./app/io-assist/styles.css"; html, body { From 825bef4958eca69b95e44053c1da5b6fb82ea5ee Mon Sep 17 00:00:00 2001 From: Aleksandar Dimitrov Date: Fri, 31 Jul 2026 18:52:08 +0300 Subject: [PATCH 3/7] remove postcss tailwind plugin --- .../.postcssrc.json | 5 - .../package-lock.json | 333 +----------------- .../io-assist-angular-editable/package.json | 2 - 3 files changed, 19 insertions(+), 321 deletions(-) delete mode 100644 templates/io-assist-angular-editable/.postcssrc.json diff --git a/templates/io-assist-angular-editable/.postcssrc.json b/templates/io-assist-angular-editable/.postcssrc.json deleted file mode 100644 index fddc8af..0000000 --- a/templates/io-assist-angular-editable/.postcssrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "plugins": { - "@tailwindcss/postcss": {} - } -} diff --git a/templates/io-assist-angular-editable/package-lock.json b/templates/io-assist-angular-editable/package-lock.json index 7ade8a7..86d4ee0 100644 --- a/templates/io-assist-angular-editable/package-lock.json +++ b/templates/io-assist-angular-editable/package-lock.json @@ -39,9 +39,7 @@ "@angular/compiler-cli": "^21.2.0", "@interopio/core": ">=6.0.0", "@interopio/desktop": ">=6.0.0", - "@tailwindcss/postcss": "^4.1.13", "jsdom": "^28.0.0", - "postcss": "^8.5.6", "prettier": "^3.8.1", "tailwindcss": "^4.1.13", "typescript": "~5.9.2", @@ -264,19 +262,6 @@ "node": ">= 14.0.0" } }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -1174,6 +1159,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" @@ -1186,6 +1172,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -1197,6 +1184,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -4733,277 +4721,6 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, - "node_modules/@tailwindcss/node": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", - "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.24.1", - "jiti": "^2.7.0", - "lightningcss": "1.32.0", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.3.3" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", - "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.3", - "@tailwindcss/oxide-darwin-arm64": "4.3.3", - "@tailwindcss/oxide-darwin-x64": "4.3.3", - "@tailwindcss/oxide-freebsd-x64": "4.3.3", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", - "@tailwindcss/oxide-linux-x64-musl": "4.3.3", - "@tailwindcss/oxide-wasm32-wasi": "4.3.3", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", - "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", - "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", - "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", - "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", - "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", - "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", - "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", - "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", - "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", - "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.11.1", - "@emnapi/runtime": "^1.11.1", - "@emnapi/wasi-threads": "^1.2.2", - "@napi-rs/wasm-runtime": "^1.1.4", - "@tybys/wasm-util": "^0.10.2", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", - "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", - "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/postcss": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", - "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.3.3", - "@tailwindcss/oxide": "4.3.3", - "postcss": "^8.5.16", - "tailwindcss": "4.3.3" - } - }, "node_modules/@tufjs/canonical-json": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", @@ -6072,6 +5789,7 @@ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", + "optional": true, "engines": { "node": ">=8" } @@ -6191,20 +5909,6 @@ "node": ">= 0.8" } }, - "node_modules/enhanced-resolve": { - "version": "5.24.5", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", - "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -7099,6 +6803,8 @@ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -7240,6 +6946,8 @@ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, "license": "MPL-2.0", + "optional": true, + "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -7277,6 +6985,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7298,6 +7007,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7319,6 +7029,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7340,6 +7051,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7361,6 +7073,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7382,6 +7095,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7403,6 +7117,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7424,6 +7139,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7445,6 +7161,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7466,6 +7183,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7487,6 +7205,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -10275,20 +9994,6 @@ "dev": true, "license": "MIT" }, - "node_modules/tapable": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", - "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/tar": { "version": "7.5.15", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", diff --git a/templates/io-assist-angular-editable/package.json b/templates/io-assist-angular-editable/package.json index c8048d4..b3f40e0 100644 --- a/templates/io-assist-angular-editable/package.json +++ b/templates/io-assist-angular-editable/package.json @@ -42,9 +42,7 @@ "@angular/compiler-cli": "^21.2.0", "@interopio/core": ">=6.0.0", "@interopio/desktop": ">=6.0.0", - "@tailwindcss/postcss": "^4.1.13", "jsdom": "^28.0.0", - "postcss": "^8.5.6", "prettier": "^3.8.1", "tailwindcss": "^4.1.13", "typescript": "~5.9.2", From 6ec46861a8e38053437912c544c77515530b803c Mon Sep 17 00:00:00 2001 From: Aleksandar Dimitrov Date: Fri, 31 Jul 2026 18:57:51 +0300 Subject: [PATCH 4/7] revert postcss --- .../.postcssrc.json | 5 + .../package-lock.json | 333 +++++++++++++++++- .../io-assist-angular-editable/package.json | 2 + 3 files changed, 321 insertions(+), 19 deletions(-) create mode 100644 templates/io-assist-angular-editable/.postcssrc.json diff --git a/templates/io-assist-angular-editable/.postcssrc.json b/templates/io-assist-angular-editable/.postcssrc.json new file mode 100644 index 0000000..9674304 --- /dev/null +++ b/templates/io-assist-angular-editable/.postcssrc.json @@ -0,0 +1,5 @@ +{ + "plugins": { + "@tailwindcss/postcss": {} + } +} \ No newline at end of file diff --git a/templates/io-assist-angular-editable/package-lock.json b/templates/io-assist-angular-editable/package-lock.json index 86d4ee0..7ade8a7 100644 --- a/templates/io-assist-angular-editable/package-lock.json +++ b/templates/io-assist-angular-editable/package-lock.json @@ -39,7 +39,9 @@ "@angular/compiler-cli": "^21.2.0", "@interopio/core": ">=6.0.0", "@interopio/desktop": ">=6.0.0", + "@tailwindcss/postcss": "^4.1.13", "jsdom": "^28.0.0", + "postcss": "^8.5.6", "prettier": "^3.8.1", "tailwindcss": "^4.1.13", "typescript": "~5.9.2", @@ -262,6 +264,19 @@ "node": ">= 14.0.0" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -1159,7 +1174,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" @@ -1172,7 +1186,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -1184,7 +1197,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -4721,6 +4733,277 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" + } + }, "node_modules/@tufjs/canonical-json": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", @@ -5789,7 +6072,6 @@ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -5909,6 +6191,20 @@ "node": ">= 0.8" } }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -6803,8 +7099,6 @@ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", - "optional": true, - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -6946,8 +7240,6 @@ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, "license": "MPL-2.0", - "optional": true, - "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -6985,7 +7277,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7007,7 +7298,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7029,7 +7319,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7051,7 +7340,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7073,7 +7361,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7095,7 +7382,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7117,7 +7403,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7139,7 +7424,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7161,7 +7445,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7183,7 +7466,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7205,7 +7487,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9994,6 +10275,20 @@ "dev": true, "license": "MIT" }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tar": { "version": "7.5.15", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", diff --git a/templates/io-assist-angular-editable/package.json b/templates/io-assist-angular-editable/package.json index b3f40e0..c8048d4 100644 --- a/templates/io-assist-angular-editable/package.json +++ b/templates/io-assist-angular-editable/package.json @@ -42,7 +42,9 @@ "@angular/compiler-cli": "^21.2.0", "@interopio/core": ">=6.0.0", "@interopio/desktop": ">=6.0.0", + "@tailwindcss/postcss": "^4.1.13", "jsdom": "^28.0.0", + "postcss": "^8.5.6", "prettier": "^3.8.1", "tailwindcss": "^4.1.13", "typescript": "~5.9.2", From 7ae8570ee419a503d3fc607e2090216885a1b8ee Mon Sep 17 00:00:00 2001 From: Aleksandar Dimitrov Date: Mon, 3 Aug 2026 11:14:05 +0300 Subject: [PATCH 5/7] minor re-org --- .../io-assist-angular-editable/README.md | 136 +++++------------- .../src/app/app.config.ts | 2 +- .../io-assist-angular-editable/src/app/app.ts | 2 +- .../src/app/io-assist/index.ts | 8 -- .../components/chat/chat.component.html | 0 .../components/chat/chat.component.ts | 0 .../components/header/header.component.html | 0 .../components/header/header.component.ts | 0 .../input-area-action-bar.component.html | 0 .../input-area-action-bar.component.ts | 0 .../input-area-send-button.component.html | 0 .../input-area-send-button.component.ts | 0 .../input-area/input-area.component.css | 0 .../input-area/input-area.component.html | 0 .../input-area/input-area.component.ts | 0 .../assistant-message.component.html | 0 .../assistant-message.component.ts | 0 .../mcp-app-resource.component.ts | 0 .../message-area/message-area.component.html | 0 .../message-area/message-area.component.ts | 0 .../message-footer.component.html | 0 .../message-footer.component.ts | 0 .../tool-message/tool-message.component.css | 0 .../tool-message/tool-message.component.html | 0 .../tool-message/tool-message.component.ts | 0 .../tool-trace-message.component.html | 0 .../tool-trace-message.component.ts | 0 .../user-message/user-message.component.html | 0 .../user-message/user-message.component.ts | 0 .../favorite-prompt-list.component.html | 0 .../favorite-prompt-list.component.ts | 0 .../prompt-list-item.component.html | 0 .../prompt-list-item.component.ts | 0 .../prompt-list/prompt-list.component.html | 0 .../prompt-list/prompt-list.component.ts | 0 .../scroll-area/scroll-area.component.html | 0 .../scroll-area/scroll-area.component.ts | 0 ...d-history-list-item-divider.component.html | 0 ...ead-history-list-item-divider.component.ts | 0 .../thread-history-list-item.component.html | 0 .../thread-history-list-item.component.ts | 0 .../thread-history-list.component.html | 0 .../thread-history-list.component.ts | 0 .../thread-history.component.html | 0 .../thread-history.component.ts | 0 .../tool-info/tool-info.component.html | 0 .../tool-info/tool-info.component.ts | 0 .../tool-list-item.component.html | 0 .../tool-list-item.component.ts | 0 .../tool/tool-list/tool-list.component.html | 0 .../tool/tool-list/tool-list.component.ts | 0 .../working-context-panel.component.css | 0 .../working-context-panel.component.html | 0 .../working-context-panel.component.ts | 0 .../{ => lib}/io-assist.component.html | 0 .../{ => lib}/io-assist.component.ts | 0 .../io-assist/{ => lib}/io-assist.config.ts | 0 .../{ => lib}/io-assist.providers.ts | 0 .../io-assist/{ => lib}/io-assist.schema.ts | 0 .../io-assist/{ => lib}/io-assist.types.ts | 0 .../app-button/app-button.component.html | 0 .../app-button/app-button.component.ts | 0 .../shared/components/app-button/enum.ts | 0 .../shared/components/app-button/types.ts | 0 ...pp-copy-to-clipboard-button.component.html | 0 .../app-copy-to-clipboard-button.component.ts | 0 .../app-icon/app-icon.component.html | 0 .../components/app-icon/app-icon.component.ts | 0 .../shared/components/app-icon/enum.ts | 0 .../shared/components/app-icon/icons.ts | 0 .../shared/components/app-icon/types.ts | 0 .../app-input/app-input.component.html | 0 .../app-input/app-input.component.ts | 0 .../app-md-formatter.component.css | 0 .../app-md-formatter.component.html | 0 .../app-md-formatter.component.ts | 0 .../components/app-md-formatter/utils.ts | 0 .../app-panel/app-panel.component.html | 0 .../app-panel/app-panel.component.ts | 0 .../shared/components/app-panel/enums.ts | 0 .../shared/components/app-panel/types.ts | 0 .../app-select/app-select.component.css | 0 .../app-select/app-select.component.html | 0 .../app-select/app-select.component.ts | 0 .../app-select/app-select.service.ts | 0 .../shared/components/app-select/types.ts | 0 .../app-spinner/app-spinner.component.html | 0 .../app-spinner/app-spinner.component.ts | 0 .../shared/components/app-spinner/enum.ts | 0 .../shared/components/app-spinner/types.ts | 0 .../app-toggle/app-toggle.component.html | 0 .../app-toggle/app-toggle.component.ts | 0 .../app-tooltip/app-tooltip.component.css | 0 .../app-tooltip/app-tooltip.component.html | 0 .../app-tooltip/app-tooltip.component.ts | 0 .../shared/components/app-tooltip/enum.ts | 0 .../shared/components/app-tooltip/types.ts | 0 .../{ => lib}/shared/constants/agents.ts | 0 .../shared/constants/ui-prefs-keys.ts | 0 .../{ => lib}/shared/constants/ui-strings.ts | 0 .../accent-gradient-border.directive.ts | 0 .../directives/animation-effect/README.md | 0 .../animation-effect.directive.ts | 0 .../animation-effect/animation-type.enum.ts | 0 .../core/animation-effect.factory.ts | 0 .../core/base-animation.effect.ts | 0 .../core/chained-animation.effect.ts | 0 .../click-spin-360-animation.effect.ts | 0 ...er-tilt-click-spin-270-animation.effect.ts | 0 .../{ => lib}/shared/directives/utils.ts | 0 .../shared/enums/loading-state.enum.ts | 0 .../shared/enums/permission-mode.enum.ts | 0 .../{ => lib}/shared/guards/.gitkeep | 0 .../agent-manager/agent-manager.service.ts | 0 .../shared/services/agent/agent.service.ts | 0 .../component-effect-manager.service.ts | 0 .../copy-to-clipboard.service.ts | 0 .../elicitation/elicitation.service.ts | 0 .../services/io-ai-web/elicitation/enums.ts | 0 .../services/io-ai-web/elicitation/utils.ts | 0 .../services/io-ai-web/io-ai-web.service.ts | 0 .../services/io-ai-web/sampling/enums.ts | 0 .../io-ai-web/sampling/sampling.service.ts | 0 .../shared/services/io-ai-web/types.ts | 0 .../shared/services/io/io.service.ts | 0 .../shared/services/logger/logger.service.ts | 0 .../services/mcp-apps/mcp-apps.service.ts | 0 .../services/overlay/overlay.service.ts | 0 .../shared/services/overlay/types.ts | 0 .../shared/services/prompt/prompt.service.ts | 0 .../{ => lib}/shared/services/prompt/types.ts | 0 .../responsive-ui/responsive-ui.service.ts | 0 .../subscription-cleanup.service.ts | 0 .../shared/services/thread/thread.service.ts | 0 .../shared/services/tools/tools.service.ts | 0 .../working-context.service.ts | 0 .../shared/store/agent/agent.actions.enum.ts | 0 .../shared/store/agent/agent.actions.ts | 0 .../shared/store/agent/agent.effects.ts | 0 .../shared/store/agent/agent.facade.ts | 0 .../shared/store/agent/agent.reducer.ts | 0 .../shared/store/agent/agent.selector.ts | 0 .../app-lifecycle.actions.enum.ts | 0 .../app-lifecycle/app-lifecycle.actions.ts | 0 .../app-lifecycle/app-lifecycle.effects.ts | 0 .../app-lifecycle/app-lifecycle.facade.ts | 0 .../app-lifecycle/app-lifecycle.reducer.ts | 0 .../app-lifecycle/app-lifecycle.selector.ts | 0 .../{ => lib}/shared/store/app.state.ts | 0 .../store/message/message.actions.enum.ts | 0 .../shared/store/message/message.actions.ts | 0 .../shared/store/message/message.effects.ts | 0 .../shared/store/message/message.facade.ts | 0 .../shared/store/message/message.reducer.ts | 0 .../shared/store/message/message.selector.ts | 0 .../shared/store/message/message.utils.ts | 0 .../{ => lib}/shared/store/message/types.ts | 0 .../store/prompt/prompt.actions.enum.ts | 0 .../shared/store/prompt/prompt.actions.ts | 0 .../shared/store/prompt/prompt.effects.ts | 0 .../shared/store/prompt/prompt.facade.ts | 0 .../shared/store/prompt/prompt.reducer.ts | 0 .../shared/store/prompt/prompt.selector.ts | 0 .../response-stream.actions.enum.ts | 0 .../response-stream.actions.ts | 0 .../response-stream/response-stream.facade.ts | 0 .../response-stream.reducer.ts | 0 .../response-stream.selector.ts | 0 .../response-stream/response-stream.types.ts | 0 .../{ => lib}/shared/store/thread/enums.ts | 0 .../store/thread/thread.actions.enum.ts | 0 .../shared/store/thread/thread.actions.ts | 0 .../shared/store/thread/thread.effects.ts | 0 .../shared/store/thread/thread.facade.ts | 0 .../shared/store/thread/thread.reducer.ts | 0 .../shared/store/thread/thread.selector.ts | 0 .../{ => lib}/shared/store/thread/types.ts | 0 .../{ => lib}/shared/store/tool/enums.ts | 0 .../shared/store/tool/tool.actions.enum.ts | 0 .../shared/store/tool/tool.actions.ts | 0 .../shared/store/tool/tool.effects.ts | 0 .../shared/store/tool/tool.facade.ts | 0 .../shared/store/tool/tool.reducer.ts | 0 .../shared/store/tool/tool.selector.ts | 0 .../{ => lib}/shared/store/tool/types.ts | 0 .../working-context.actions.enum.ts | 0 .../working-context.actions.ts | 0 .../working-context.effects.ts | 0 .../working-context/working-context.facade.ts | 0 .../working-context.reducer.ts | 0 .../working-context.selector.ts | 0 .../{ => lib}/shared/styles/_defaults.css | 0 .../{ => lib}/shared/styles/_utils.css | 0 .../{ => lib}/shared/types/loading.type.ts | 0 .../{ => lib}/shared/utils/icon.utils.ts | 0 .../src/app/io-assist/public-api.ts | 8 ++ .../src/app/io-assist/styles.css | 2 +- 197 files changed, 44 insertions(+), 114 deletions(-) delete mode 100644 templates/io-assist-angular-editable/src/app/io-assist/index.ts rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/chat/chat.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/chat/chat.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/header/header.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/header/header.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/input-area/input-area-action-bar/input-area-action-bar.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/input-area/input-area-action-bar/input-area-action-bar.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/input-area/input-area-send-button/input-area-send-button.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/input-area/input-area-send-button/input-area-send-button.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/input-area/input-area.component.css (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/input-area/input-area.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/input-area/input-area.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/messages/assistant-message/assistant-message.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/messages/assistant-message/assistant-message.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/messages/mcp-app-resource/mcp-app-resource.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/messages/message-area/message-area.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/messages/message-area/message-area.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/messages/message-footer/message-footer.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/messages/message-footer/message-footer.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/messages/tool-message/tool-message.component.css (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/messages/tool-message/tool-message.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/messages/tool-message/tool-message.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/messages/tool-trace-message/tool-trace-message.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/messages/tool-trace-message/tool-trace-message.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/messages/user-message/user-message.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/messages/user-message/user-message.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/prompt/favorite-prompts/favorite-prompt-list.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/prompt/favorite-prompts/favorite-prompt-list.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/prompt/prompt-list-item/prompt-list-item.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/prompt/prompt-list-item/prompt-list-item.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/prompt/prompt-list/prompt-list.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/prompt/prompt-list/prompt-list.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/scroll-area/scroll-area.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/scroll-area/scroll-area.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/threads/thread-history-list-item-divider/thread-history-list-item-divider.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/threads/thread-history-list-item-divider/thread-history-list-item-divider.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/threads/thread-history-list-item/thread-history-list-item.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/threads/thread-history-list-item/thread-history-list-item.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/threads/thread-history-list/thread-history-list.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/threads/thread-history-list/thread-history-list.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/threads/thread-history/thread-history.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/threads/thread-history/thread-history.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/tool/tool-list-item/tool-info/tool-info.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/tool/tool-list-item/tool-info/tool-info.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/tool/tool-list-item/tool-list-item.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/tool/tool-list-item/tool-list-item.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/tool/tool-list/tool-list.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/tool/tool-list/tool-list.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/working-context-panel/working-context-panel.component.css (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/working-context-panel/working-context-panel.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/components/working-context-panel/working-context-panel.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/io-assist.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/io-assist.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/io-assist.config.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/io-assist.providers.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/io-assist.schema.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/io-assist.types.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-button/app-button.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-button/app-button.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-button/enum.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-button/types.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-copy-to-clipboard-button/app-copy-to-clipboard-button.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-copy-to-clipboard-button/app-copy-to-clipboard-button.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-icon/app-icon.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-icon/app-icon.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-icon/enum.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-icon/icons.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-icon/types.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-input/app-input.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-input/app-input.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-md-formatter/app-md-formatter.component.css (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-md-formatter/app-md-formatter.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-md-formatter/app-md-formatter.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-md-formatter/utils.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-panel/app-panel.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-panel/app-panel.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-panel/enums.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-panel/types.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-select/app-select.component.css (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-select/app-select.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-select/app-select.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-select/app-select.service.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-select/types.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-spinner/app-spinner.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-spinner/app-spinner.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-spinner/enum.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-spinner/types.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-toggle/app-toggle.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-toggle/app-toggle.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-tooltip/app-tooltip.component.css (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-tooltip/app-tooltip.component.html (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-tooltip/app-tooltip.component.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-tooltip/enum.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/components/app-tooltip/types.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/constants/agents.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/constants/ui-prefs-keys.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/constants/ui-strings.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/directives/accent-gradient-border/accent-gradient-border.directive.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/directives/animation-effect/README.md (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/directives/animation-effect/animation-effect.directive.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/directives/animation-effect/animation-type.enum.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/directives/animation-effect/core/animation-effect.factory.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/directives/animation-effect/core/base-animation.effect.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/directives/animation-effect/core/chained-animation.effect.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/directives/animation-effect/effects/click-spin-360-animation.effect.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/directives/animation-effect/effects/hover-tilt-click-spin-270-animation.effect.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/directives/utils.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/enums/loading-state.enum.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/enums/permission-mode.enum.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/guards/.gitkeep (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/agent/agent-manager/agent-manager.service.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/agent/agent.service.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/component-effect-manager/component-effect-manager.service.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/copy-to-clipboard/copy-to-clipboard.service.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/io-ai-web/elicitation/elicitation.service.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/io-ai-web/elicitation/enums.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/io-ai-web/elicitation/utils.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/io-ai-web/io-ai-web.service.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/io-ai-web/sampling/enums.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/io-ai-web/sampling/sampling.service.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/io-ai-web/types.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/io/io.service.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/logger/logger.service.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/mcp-apps/mcp-apps.service.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/overlay/overlay.service.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/overlay/types.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/prompt/prompt.service.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/prompt/types.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/responsive-ui/responsive-ui.service.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/subscription-cleanup/subscription-cleanup.service.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/thread/thread.service.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/tools/tools.service.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/services/working-context/working-context.service.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/agent/agent.actions.enum.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/agent/agent.actions.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/agent/agent.effects.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/agent/agent.facade.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/agent/agent.reducer.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/agent/agent.selector.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/app-lifecycle/app-lifecycle.actions.enum.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/app-lifecycle/app-lifecycle.actions.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/app-lifecycle/app-lifecycle.effects.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/app-lifecycle/app-lifecycle.facade.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/app-lifecycle/app-lifecycle.reducer.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/app-lifecycle/app-lifecycle.selector.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/app.state.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/message/message.actions.enum.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/message/message.actions.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/message/message.effects.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/message/message.facade.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/message/message.reducer.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/message/message.selector.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/message/message.utils.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/message/types.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/prompt/prompt.actions.enum.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/prompt/prompt.actions.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/prompt/prompt.effects.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/prompt/prompt.facade.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/prompt/prompt.reducer.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/prompt/prompt.selector.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/response-stream/response-stream.actions.enum.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/response-stream/response-stream.actions.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/response-stream/response-stream.facade.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/response-stream/response-stream.reducer.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/response-stream/response-stream.selector.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/response-stream/response-stream.types.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/thread/enums.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/thread/thread.actions.enum.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/thread/thread.actions.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/thread/thread.effects.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/thread/thread.facade.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/thread/thread.reducer.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/thread/thread.selector.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/thread/types.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/tool/enums.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/tool/tool.actions.enum.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/tool/tool.actions.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/tool/tool.effects.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/tool/tool.facade.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/tool/tool.reducer.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/tool/tool.selector.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/tool/types.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/working-context/working-context.actions.enum.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/working-context/working-context.actions.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/working-context/working-context.effects.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/working-context/working-context.facade.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/working-context/working-context.reducer.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/store/working-context/working-context.selector.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/styles/_defaults.css (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/styles/_utils.css (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/types/loading.type.ts (100%) rename templates/io-assist-angular-editable/src/app/io-assist/{ => lib}/shared/utils/icon.utils.ts (100%) create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/public-api.ts diff --git a/templates/io-assist-angular-editable/README.md b/templates/io-assist-angular-editable/README.md index f12a1da..8414841 100644 --- a/templates/io-assist-angular-editable/README.md +++ b/templates/io-assist-angular-editable/README.md @@ -1,129 +1,59 @@ -# io.Assist Angular — editable (source-owned) +# IoAssistAngular -An Angular application that embeds io.Assist as **editable source** rather than as an npm dependency. -The io.Assist Angular source lives in [`src/app/io-assist`](src/app/io-assist), and you own it. +This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 21.2.13. -If you only need configuration-level customization, prefer the packaged preset -(`io-assist-angular-packaged`) — it consumes `@interopio/io-assist-ng` and stays on the upgrade path. -This template is the deliberate fork: see [`src/app/io-assist/PROVENANCE.md`](src/app/io-assist/PROVENANCE.md) -for its origin, version, and the one-way ownership contract. +## Development server -## Prerequisites — read this first +To start a local development server, run: -This application **cannot run on its own**. Opening the dev server URL directly will fail. Two external -pieces are required: +```bash +ng serve +``` -1. **An io.Connect Browser Platform.** `src/app/app.config.ts` uses `@interopio/browser`, which is a - *client*: it discovers a platform peer and does not create one. With no platform, io.Connect - initialization fails and — by design — io.Assist never starts - (`src/app/io-assist/shared/services/io/io.service.ts` waits for io.Connect readiness before doing - anything else). +Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. - Any application built with `@interopio/browser-platform` can host it. This app must then be - registered in that platform's `applications.local` config and **launched from the platform**, not - opened directly. +## Code scaffolding -2. **An agent server** implementing the Agent Protocol, reachable over HTTP. +Angular CLI includes powerful code scaffolding tools. To generate a new component, run: -## Configure +```bash +ng generate component component-name +``` -Set the agent server URL in [`src/app/app.config.ts`](src/app/app.config.ts): +For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run: -```ts -const AGENT_SERVER_URL = 'http://localhost:4111'; // ships empty — you must set this +```bash +ng generate --help ``` -It ships as `''`, which **fails validation** — AI Web requires a valid URL — so the assistant will not -start until you set it. - -While you're there, `dynamicConfig.user` in [`src/app/app.ts`](src/app/app.ts) is a placeholder -(`acme-advisor`); replace it with your real user identity. +## Building -## Run +To build the project run: ```bash -npm install -npm start # dev server on http://localhost:4103 +ng build ``` -Then launch this app **from your io.Connect Browser Platform**, not by visiting `:4103` directly. +This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed. -### Local development against the iointel-js monorepo +## Running unit tests -If you have the `iointel-js` monorepo checked out alongside this template, it supplies both -prerequisites: +To execute unit tests with the [Vitest](https://vitest.dev/) test runner, use the following command: ```bash -# terminal 1 — platform (io.Connect Home) -cd iointel-js/dev-apps/d-io-cb-home && npm start # http://localhost:4200 +ng test +``` -# terminal 2 — agent server (Mastra) -cd iointel-js/apps/io-assist-mastra && npm start # http://localhost:4111 +## Running end-to-end tests -# terminal 3 — this app -npm start # http://localhost:4103 -``` +For end-to-end (e2e) testing, run: -Register this app in `iointel-js/configs/io-connect/io-cb-config-rich.json` under `apps`: - -```json -{ - "name": "io-assist-angular-editable", - "type": "window", - "details": { - "url": "http://localhost:4103", - "channelSelector": { "enabled": true, "type": "single" } - }, - "customProperties": { "includeInWorkspaces": true } -} +```bash +ng e2e ``` -Then open the platform at `http://localhost:4200` and launch `io-assist-angular-editable` from its app -list. The platform needs its own license keys via its `create-local-env` step. - -## Confirming the fork is live - -With the dev server running, edit `UI_STRINGS` in -[`src/app/io-assist/shared/constants/ui-strings.ts`](src/app/io-assist/shared/constants/ui-strings.ts) -and save. The change picks up via HMR without a reload — only possible because the source is in `src/`. -Breakpoints in `src/app/io-assist/**` hit real TypeScript with sourcemaps, not a bundled library. - -## Editing the io.Assist source - -| Area | Location | -|---|---| -| Root component | `src/app/io-assist/io-assist.component.ts` | -| Public surface | `src/app/io-assist/index.ts` | -| Provider setup | `src/app/io-assist/io-assist.providers.ts` | -| Static/dynamic config + validation | `src/app/io-assist/io-assist.config.ts`, `io-assist.schema.ts` | -| UI components | `src/app/io-assist/components/**` | -| State (NgRx store) | `src/app/io-assist/shared/store/**` | -| Side effects / SDK adapters | `src/app/io-assist/shared/services/**` | -| Shared UI primitives | `src/app/io-assist/shared/components/**` | -| Styles and theme tokens | `src/app/io-assist/styles.css`, `src/app/io-assist/shared/styles/**` | -| User-facing strings | `src/app/io-assist/shared/constants/ui-strings.ts` | - -Change the smallest coherent area, and avoid bypassing the AI Web and Agent Protocol contracts to achieve -a UI-only customization. - -Two things about this directory are load-bearing: - -- **Tailwind v4 is required.** `src/app/io-assist/styles.css` uses `@import 'tailwindcss'`, so - `@tailwindcss/postcss` is registered in `.postcssrc.json` at the project root — Angular's application - builder picks up PostCSS config files automatically. Tailwind scans the project for utility classes, - which is what makes the vendored layout work. -- **`src/styles.css` imports `./app/io-assist/styles.css`** instead of - `@interopio/io-assist-ng/styles.css`. - -`*.spec.ts` test files were intentionally not vendored — see -[`src/app/io-assist/PROVENANCE.md`](src/app/io-assist/PROVENANCE.md) for the full list of deviations -from upstream. - -## Scripts - -| | | -|---|---| -| `npm start` | dev server with HMR on port 4103 | -| `npm run build` | `ng build` | -| `npm run watch` | `ng build --watch --configuration development` | -| `npm test` | unit tests (Vitest) | +Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. + +## Additional Resources + +For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. \ No newline at end of file diff --git a/templates/io-assist-angular-editable/src/app/app.config.ts b/templates/io-assist-angular-editable/src/app/app.config.ts index 1c92d1c..b2a1269 100644 --- a/templates/io-assist-angular-editable/src/app/app.config.ts +++ b/templates/io-assist-angular-editable/src/app/app.config.ts @@ -1,6 +1,6 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from "@angular/core"; import IOBrowser from "@interopio/browser"; -import { provideIoAssist } from "./io-assist"; +import { provideIoAssist } from "./io-assist/public-api"; import IOWorkspaces from "@interopio/workspaces-api"; // this must be defined; ships empty — you must set it to your agent server URL diff --git a/templates/io-assist-angular-editable/src/app/app.ts b/templates/io-assist-angular-editable/src/app/app.ts index 0361676..5a73451 100644 --- a/templates/io-assist-angular-editable/src/app/app.ts +++ b/templates/io-assist-angular-editable/src/app/app.ts @@ -1,5 +1,5 @@ import { Component, signal } from "@angular/core"; -import { IoAssist, type IoAssistDynamicConfig } from "./io-assist"; +import { IoAssist, type IoAssistDynamicConfig } from "./io-assist/public-api"; @Component({ selector: "app-root", diff --git a/templates/io-assist-angular-editable/src/app/io-assist/index.ts b/templates/io-assist-angular-editable/src/app/io-assist/index.ts deleted file mode 100644 index 7727a36..0000000 --- a/templates/io-assist-angular-editable/src/app/io-assist/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Public API Surface of io-assist-ng - */ - -export * from "./io-assist.component"; -export * from "./io-assist.providers"; -export * from "./io-assist.types"; -export * from "./io-assist.config"; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/chat/chat.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/chat/chat.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/chat/chat.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/chat/chat.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/chat/chat.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/chat/chat.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/chat/chat.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/chat/chat.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/header/header.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/header/header.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/header/header.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/header/header.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/header/header.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/header/header.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/header/header.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/header/header.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-action-bar/input-area-action-bar.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/input-area/input-area-action-bar/input-area-action-bar.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-action-bar/input-area-action-bar.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/input-area/input-area-action-bar/input-area-action-bar.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-action-bar/input-area-action-bar.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/input-area/input-area-action-bar/input-area-action-bar.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-action-bar/input-area-action-bar.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/input-area/input-area-action-bar/input-area-action-bar.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-send-button/input-area-send-button.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/input-area/input-area-send-button/input-area-send-button.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-send-button/input-area-send-button.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/input-area/input-area-send-button/input-area-send-button.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-send-button/input-area-send-button.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/input-area/input-area-send-button/input-area-send-button.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area-send-button/input-area-send-button.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/input-area/input-area-send-button/input-area-send-button.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area.component.css b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/input-area/input-area.component.css similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area.component.css rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/input-area/input-area.component.css diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/input-area/input-area.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/input-area/input-area.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/input-area/input-area.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/input-area/input-area.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/input-area/input-area.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/assistant-message/assistant-message.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/assistant-message/assistant-message.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/messages/assistant-message/assistant-message.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/assistant-message/assistant-message.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/assistant-message/assistant-message.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/assistant-message/assistant-message.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/messages/assistant-message/assistant-message.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/assistant-message/assistant-message.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/mcp-app-resource/mcp-app-resource.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/mcp-app-resource/mcp-app-resource.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/messages/mcp-app-resource/mcp-app-resource.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/mcp-app-resource/mcp-app-resource.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-area/message-area.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/message-area/message-area.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-area/message-area.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/message-area/message-area.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-area/message-area.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/message-area/message-area.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-area/message-area.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/message-area/message-area.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-footer/message-footer.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/message-footer/message-footer.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-footer/message-footer.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/message-footer/message-footer.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-footer/message-footer.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/message-footer/message-footer.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/messages/message-footer/message-footer.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/message-footer/message-footer.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-message/tool-message.component.css b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/tool-message/tool-message.component.css similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-message/tool-message.component.css rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/tool-message/tool-message.component.css diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-message/tool-message.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/tool-message/tool-message.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-message/tool-message.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/tool-message/tool-message.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-message/tool-message.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/tool-message/tool-message.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-message/tool-message.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/tool-message/tool-message.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-trace-message/tool-trace-message.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/tool-trace-message/tool-trace-message.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-trace-message/tool-trace-message.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/tool-trace-message/tool-trace-message.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-trace-message/tool-trace-message.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/tool-trace-message/tool-trace-message.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/messages/tool-trace-message/tool-trace-message.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/tool-trace-message/tool-trace-message.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/user-message/user-message.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/user-message/user-message.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/messages/user-message/user-message.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/user-message/user-message.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/messages/user-message/user-message.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/user-message/user-message.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/messages/user-message/user-message.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/messages/user-message/user-message.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/favorite-prompts/favorite-prompt-list.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/prompt/favorite-prompts/favorite-prompt-list.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/prompt/favorite-prompts/favorite-prompt-list.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/prompt/favorite-prompts/favorite-prompt-list.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/favorite-prompts/favorite-prompt-list.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/prompt/favorite-prompts/favorite-prompt-list.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/prompt/favorite-prompts/favorite-prompt-list.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/prompt/favorite-prompts/favorite-prompt-list.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list-item/prompt-list-item.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/prompt/prompt-list-item/prompt-list-item.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list-item/prompt-list-item.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/prompt/prompt-list-item/prompt-list-item.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list-item/prompt-list-item.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/prompt/prompt-list-item/prompt-list-item.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list-item/prompt-list-item.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/prompt/prompt-list-item/prompt-list-item.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list/prompt-list.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/prompt/prompt-list/prompt-list.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list/prompt-list.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/prompt/prompt-list/prompt-list.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list/prompt-list.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/prompt/prompt-list/prompt-list.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/prompt/prompt-list/prompt-list.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/prompt/prompt-list/prompt-list.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/scroll-area/scroll-area.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/scroll-area/scroll-area.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/scroll-area/scroll-area.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/scroll-area/scroll-area.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/scroll-area/scroll-area.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/scroll-area/scroll-area.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/scroll-area/scroll-area.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/scroll-area/scroll-area.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item-divider/thread-history-list-item-divider.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/threads/thread-history-list-item-divider/thread-history-list-item-divider.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item-divider/thread-history-list-item-divider.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/threads/thread-history-list-item-divider/thread-history-list-item-divider.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item-divider/thread-history-list-item-divider.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/threads/thread-history-list-item-divider/thread-history-list-item-divider.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item-divider/thread-history-list-item-divider.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/threads/thread-history-list-item-divider/thread-history-list-item-divider.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item/thread-history-list-item.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/threads/thread-history-list-item/thread-history-list-item.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item/thread-history-list-item.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/threads/thread-history-list-item/thread-history-list-item.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item/thread-history-list-item.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/threads/thread-history-list-item/thread-history-list-item.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list-item/thread-history-list-item.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/threads/thread-history-list-item/thread-history-list-item.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list/thread-history-list.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/threads/thread-history-list/thread-history-list.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list/thread-history-list.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/threads/thread-history-list/thread-history-list.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list/thread-history-list.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/threads/thread-history-list/thread-history-list.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history-list/thread-history-list.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/threads/thread-history-list/thread-history-list.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history/thread-history.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/threads/thread-history/thread-history.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history/thread-history.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/threads/thread-history/thread-history.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history/thread-history.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/threads/thread-history/thread-history.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/threads/thread-history/thread-history.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/threads/thread-history/thread-history.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-info/tool-info.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/tool/tool-list-item/tool-info/tool-info.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-info/tool-info.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/tool/tool-list-item/tool-info/tool-info.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-info/tool-info.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/tool/tool-list-item/tool-info/tool-info.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-info/tool-info.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/tool/tool-list-item/tool-info/tool-info.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-list-item.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/tool/tool-list-item/tool-list-item.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-list-item.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/tool/tool-list-item/tool-list-item.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-list-item.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/tool/tool-list-item/tool-list-item.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list-item/tool-list-item.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/tool/tool-list-item/tool-list-item.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list/tool-list.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/tool/tool-list/tool-list.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list/tool-list.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/tool/tool-list/tool-list.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list/tool-list.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/tool/tool-list/tool-list.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/tool/tool-list/tool-list.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/tool/tool-list/tool-list.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/working-context-panel/working-context-panel.component.css b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/working-context-panel/working-context-panel.component.css similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/working-context-panel/working-context-panel.component.css rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/working-context-panel/working-context-panel.component.css diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/working-context-panel/working-context-panel.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/working-context-panel/working-context-panel.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/working-context-panel/working-context-panel.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/working-context-panel/working-context-panel.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/components/working-context-panel/working-context-panel.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/components/working-context-panel/working-context-panel.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/components/working-context-panel/working-context-panel.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/components/working-context-panel/working-context-panel.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/io-assist.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/io-assist.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/io-assist.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/io-assist.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/io-assist.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/io-assist.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/io-assist.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/io-assist.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/io-assist.config.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/io-assist.config.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/io-assist.config.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/io-assist.config.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/io-assist.providers.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/io-assist.providers.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/io-assist.providers.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/io-assist.providers.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/io-assist.schema.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/io-assist.schema.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/io-assist.schema.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/io-assist.schema.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/io-assist.types.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/io-assist.types.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/io-assist.types.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/io-assist.types.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/app-button.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-button/app-button.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/app-button.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-button/app-button.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/app-button.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-button/app-button.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/app-button.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-button/app-button.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-button/enum.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/enum.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-button/enum.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-button/types.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-button/types.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-button/types.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-copy-to-clipboard-button/app-copy-to-clipboard-button.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-copy-to-clipboard-button/app-copy-to-clipboard-button.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-copy-to-clipboard-button/app-copy-to-clipboard-button.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-copy-to-clipboard-button/app-copy-to-clipboard-button.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-copy-to-clipboard-button/app-copy-to-clipboard-button.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-copy-to-clipboard-button/app-copy-to-clipboard-button.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-copy-to-clipboard-button/app-copy-to-clipboard-button.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-copy-to-clipboard-button/app-copy-to-clipboard-button.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/app-icon.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-icon/app-icon.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/app-icon.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-icon/app-icon.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/app-icon.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-icon/app-icon.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/app-icon.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-icon/app-icon.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-icon/enum.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/enum.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-icon/enum.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/icons.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-icon/icons.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/icons.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-icon/icons.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-icon/types.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-icon/types.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-icon/types.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-input/app-input.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-input/app-input.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-input/app-input.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-input/app-input.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-input/app-input.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-input/app-input.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-input/app-input.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-input/app-input.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/app-md-formatter.component.css b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-md-formatter/app-md-formatter.component.css similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/app-md-formatter.component.css rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-md-formatter/app-md-formatter.component.css diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/app-md-formatter.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-md-formatter/app-md-formatter.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/app-md-formatter.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-md-formatter/app-md-formatter.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/app-md-formatter.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-md-formatter/app-md-formatter.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/app-md-formatter.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-md-formatter/app-md-formatter.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/utils.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-md-formatter/utils.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-md-formatter/utils.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-md-formatter/utils.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/app-panel.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-panel/app-panel.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/app-panel.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-panel/app-panel.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/app-panel.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-panel/app-panel.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/app-panel.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-panel/app-panel.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/enums.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-panel/enums.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/enums.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-panel/enums.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-panel/types.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-panel/types.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-panel/types.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.component.css b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-select/app-select.component.css similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.component.css rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-select/app-select.component.css diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-select/app-select.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-select/app-select.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-select/app-select.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-select/app-select.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-select/app-select.service.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/app-select.service.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-select/app-select.service.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-select/types.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-select/types.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-select/types.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/app-spinner.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-spinner/app-spinner.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/app-spinner.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-spinner/app-spinner.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/app-spinner.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-spinner/app-spinner.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/app-spinner.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-spinner/app-spinner.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-spinner/enum.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/enum.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-spinner/enum.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-spinner/types.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-spinner/types.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-spinner/types.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-toggle/app-toggle.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-toggle/app-toggle.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-toggle/app-toggle.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-toggle/app-toggle.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-toggle/app-toggle.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-toggle/app-toggle.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-toggle/app-toggle.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-toggle/app-toggle.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/app-tooltip.component.css b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-tooltip/app-tooltip.component.css similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/app-tooltip.component.css rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-tooltip/app-tooltip.component.css diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/app-tooltip.component.html b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-tooltip/app-tooltip.component.html similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/app-tooltip.component.html rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-tooltip/app-tooltip.component.html diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/app-tooltip.component.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-tooltip/app-tooltip.component.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/app-tooltip.component.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-tooltip/app-tooltip.component.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-tooltip/enum.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/enum.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-tooltip/enum.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-tooltip/types.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/components/app-tooltip/types.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/components/app-tooltip/types.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/constants/agents.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/constants/agents.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/constants/agents.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/constants/agents.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/constants/ui-prefs-keys.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/constants/ui-prefs-keys.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/constants/ui-prefs-keys.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/constants/ui-prefs-keys.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/constants/ui-strings.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/constants/ui-strings.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/constants/ui-strings.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/constants/ui-strings.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/accent-gradient-border/accent-gradient-border.directive.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/directives/accent-gradient-border/accent-gradient-border.directive.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/directives/accent-gradient-border/accent-gradient-border.directive.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/directives/accent-gradient-border/accent-gradient-border.directive.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/README.md b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/directives/animation-effect/README.md similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/README.md rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/directives/animation-effect/README.md diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/animation-effect.directive.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/directives/animation-effect/animation-effect.directive.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/animation-effect.directive.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/directives/animation-effect/animation-effect.directive.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/animation-type.enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/directives/animation-effect/animation-type.enum.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/animation-type.enum.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/directives/animation-effect/animation-type.enum.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/core/animation-effect.factory.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/directives/animation-effect/core/animation-effect.factory.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/core/animation-effect.factory.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/directives/animation-effect/core/animation-effect.factory.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/core/base-animation.effect.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/directives/animation-effect/core/base-animation.effect.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/core/base-animation.effect.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/directives/animation-effect/core/base-animation.effect.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/core/chained-animation.effect.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/directives/animation-effect/core/chained-animation.effect.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/core/chained-animation.effect.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/directives/animation-effect/core/chained-animation.effect.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/effects/click-spin-360-animation.effect.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/directives/animation-effect/effects/click-spin-360-animation.effect.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/effects/click-spin-360-animation.effect.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/directives/animation-effect/effects/click-spin-360-animation.effect.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/effects/hover-tilt-click-spin-270-animation.effect.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/directives/animation-effect/effects/hover-tilt-click-spin-270-animation.effect.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/directives/animation-effect/effects/hover-tilt-click-spin-270-animation.effect.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/directives/animation-effect/effects/hover-tilt-click-spin-270-animation.effect.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/directives/utils.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/directives/utils.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/directives/utils.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/directives/utils.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/enums/loading-state.enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/enums/loading-state.enum.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/enums/loading-state.enum.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/enums/loading-state.enum.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/enums/permission-mode.enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/enums/permission-mode.enum.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/enums/permission-mode.enum.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/enums/permission-mode.enum.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/guards/.gitkeep b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/guards/.gitkeep similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/guards/.gitkeep rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/guards/.gitkeep diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/agent/agent-manager/agent-manager.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/agent/agent-manager/agent-manager.service.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/agent/agent-manager/agent-manager.service.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/agent/agent-manager/agent-manager.service.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/agent/agent.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/agent/agent.service.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/agent/agent.service.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/agent/agent.service.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/component-effect-manager/component-effect-manager.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/component-effect-manager/component-effect-manager.service.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/component-effect-manager/component-effect-manager.service.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/component-effect-manager/component-effect-manager.service.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/copy-to-clipboard/copy-to-clipboard.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/copy-to-clipboard/copy-to-clipboard.service.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/copy-to-clipboard/copy-to-clipboard.service.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/copy-to-clipboard/copy-to-clipboard.service.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/elicitation/elicitation.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/io-ai-web/elicitation/elicitation.service.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/elicitation/elicitation.service.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/io-ai-web/elicitation/elicitation.service.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/elicitation/enums.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/io-ai-web/elicitation/enums.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/elicitation/enums.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/io-ai-web/elicitation/enums.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/elicitation/utils.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/io-ai-web/elicitation/utils.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/elicitation/utils.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/io-ai-web/elicitation/utils.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/io-ai-web.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/io-ai-web/io-ai-web.service.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/io-ai-web.service.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/io-ai-web/io-ai-web.service.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/sampling/enums.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/io-ai-web/sampling/enums.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/sampling/enums.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/io-ai-web/sampling/enums.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/sampling/sampling.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/io-ai-web/sampling/sampling.service.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/sampling/sampling.service.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/io-ai-web/sampling/sampling.service.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/io-ai-web/types.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/io-ai-web/types.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/io-ai-web/types.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/io/io.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/io/io.service.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/io/io.service.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/io/io.service.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/logger/logger.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/logger/logger.service.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/logger/logger.service.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/logger/logger.service.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/mcp-apps/mcp-apps.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/mcp-apps/mcp-apps.service.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/mcp-apps/mcp-apps.service.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/mcp-apps/mcp-apps.service.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/overlay/overlay.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/overlay/overlay.service.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/overlay/overlay.service.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/overlay/overlay.service.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/overlay/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/overlay/types.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/overlay/types.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/overlay/types.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/prompt/prompt.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/prompt/prompt.service.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/prompt/prompt.service.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/prompt/prompt.service.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/prompt/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/prompt/types.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/prompt/types.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/prompt/types.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/responsive-ui/responsive-ui.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/responsive-ui/responsive-ui.service.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/responsive-ui/responsive-ui.service.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/responsive-ui/responsive-ui.service.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/subscription-cleanup/subscription-cleanup.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/subscription-cleanup/subscription-cleanup.service.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/subscription-cleanup/subscription-cleanup.service.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/subscription-cleanup/subscription-cleanup.service.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/thread/thread.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/thread/thread.service.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/thread/thread.service.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/thread/thread.service.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/tools/tools.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/tools/tools.service.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/tools/tools.service.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/tools/tools.service.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/services/working-context/working-context.service.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/working-context/working-context.service.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/services/working-context/working-context.service.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/services/working-context/working-context.service.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.actions.enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/agent/agent.actions.enum.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.actions.enum.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/agent/agent.actions.enum.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.actions.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/agent/agent.actions.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.actions.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/agent/agent.actions.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.effects.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/agent/agent.effects.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.effects.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/agent/agent.effects.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.facade.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/agent/agent.facade.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.facade.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/agent/agent.facade.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.reducer.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/agent/agent.reducer.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.reducer.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/agent/agent.reducer.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.selector.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/agent/agent.selector.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/agent/agent.selector.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/agent/agent.selector.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.actions.enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/app-lifecycle/app-lifecycle.actions.enum.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.actions.enum.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/app-lifecycle/app-lifecycle.actions.enum.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.actions.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/app-lifecycle/app-lifecycle.actions.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.actions.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/app-lifecycle/app-lifecycle.actions.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.effects.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/app-lifecycle/app-lifecycle.effects.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.effects.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/app-lifecycle/app-lifecycle.effects.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.facade.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/app-lifecycle/app-lifecycle.facade.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.facade.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/app-lifecycle/app-lifecycle.facade.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.reducer.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/app-lifecycle/app-lifecycle.reducer.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.reducer.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/app-lifecycle/app-lifecycle.reducer.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.selector.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/app-lifecycle/app-lifecycle.selector.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/app-lifecycle/app-lifecycle.selector.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/app-lifecycle/app-lifecycle.selector.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/app.state.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/app.state.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/app.state.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/app.state.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.actions.enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/message/message.actions.enum.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.actions.enum.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/message/message.actions.enum.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.actions.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/message/message.actions.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.actions.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/message/message.actions.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.effects.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/message/message.effects.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.effects.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/message/message.effects.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.facade.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/message/message.facade.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.facade.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/message/message.facade.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.reducer.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/message/message.reducer.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.reducer.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/message/message.reducer.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.selector.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/message/message.selector.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.selector.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/message/message.selector.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.utils.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/message/message.utils.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/message.utils.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/message/message.utils.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/message/types.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/message/types.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/message/types.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.actions.enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/prompt/prompt.actions.enum.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.actions.enum.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/prompt/prompt.actions.enum.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.actions.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/prompt/prompt.actions.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.actions.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/prompt/prompt.actions.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.effects.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/prompt/prompt.effects.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.effects.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/prompt/prompt.effects.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.facade.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/prompt/prompt.facade.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.facade.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/prompt/prompt.facade.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.reducer.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/prompt/prompt.reducer.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.reducer.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/prompt/prompt.reducer.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.selector.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/prompt/prompt.selector.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/prompt/prompt.selector.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/prompt/prompt.selector.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.actions.enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/response-stream/response-stream.actions.enum.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.actions.enum.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/response-stream/response-stream.actions.enum.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.actions.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/response-stream/response-stream.actions.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.actions.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/response-stream/response-stream.actions.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.facade.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/response-stream/response-stream.facade.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.facade.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/response-stream/response-stream.facade.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.reducer.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/response-stream/response-stream.reducer.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.reducer.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/response-stream/response-stream.reducer.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.selector.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/response-stream/response-stream.selector.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.selector.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/response-stream/response-stream.selector.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.types.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/response-stream/response-stream.types.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/response-stream/response-stream.types.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/response-stream/response-stream.types.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/enums.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/thread/enums.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/enums.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/thread/enums.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.actions.enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/thread/thread.actions.enum.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.actions.enum.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/thread/thread.actions.enum.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.actions.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/thread/thread.actions.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.actions.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/thread/thread.actions.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.effects.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/thread/thread.effects.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.effects.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/thread/thread.effects.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.facade.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/thread/thread.facade.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.facade.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/thread/thread.facade.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.reducer.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/thread/thread.reducer.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.reducer.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/thread/thread.reducer.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.selector.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/thread/thread.selector.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/thread.selector.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/thread/thread.selector.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/thread/types.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/thread/types.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/thread/types.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/enums.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/tool/enums.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/enums.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/tool/enums.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.actions.enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/tool/tool.actions.enum.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.actions.enum.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/tool/tool.actions.enum.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.actions.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/tool/tool.actions.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.actions.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/tool/tool.actions.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.effects.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/tool/tool.effects.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.effects.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/tool/tool.effects.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.facade.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/tool/tool.facade.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.facade.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/tool/tool.facade.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.reducer.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/tool/tool.reducer.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.reducer.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/tool/tool.reducer.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.selector.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/tool/tool.selector.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/tool.selector.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/tool/tool.selector.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/types.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/tool/types.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/tool/types.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/tool/types.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.actions.enum.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/working-context/working-context.actions.enum.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.actions.enum.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/working-context/working-context.actions.enum.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.actions.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/working-context/working-context.actions.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.actions.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/working-context/working-context.actions.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.effects.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/working-context/working-context.effects.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.effects.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/working-context/working-context.effects.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.facade.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/working-context/working-context.facade.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.facade.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/working-context/working-context.facade.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.reducer.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/working-context/working-context.reducer.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.reducer.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/working-context/working-context.reducer.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.selector.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/working-context/working-context.selector.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/store/working-context/working-context.selector.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/store/working-context/working-context.selector.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/styles/_defaults.css b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/styles/_defaults.css similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/styles/_defaults.css rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/styles/_defaults.css diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/styles/_utils.css b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/styles/_utils.css similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/styles/_utils.css rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/styles/_utils.css diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/types/loading.type.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/types/loading.type.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/types/loading.type.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/types/loading.type.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/shared/utils/icon.utils.ts b/templates/io-assist-angular-editable/src/app/io-assist/lib/shared/utils/icon.utils.ts similarity index 100% rename from templates/io-assist-angular-editable/src/app/io-assist/shared/utils/icon.utils.ts rename to templates/io-assist-angular-editable/src/app/io-assist/lib/shared/utils/icon.utils.ts diff --git a/templates/io-assist-angular-editable/src/app/io-assist/public-api.ts b/templates/io-assist-angular-editable/src/app/io-assist/public-api.ts new file mode 100644 index 0000000..4049984 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/public-api.ts @@ -0,0 +1,8 @@ +/* + * Public API Surface of io-assist-ng + */ + +export * from "./lib/io-assist.component"; +export * from "./lib/io-assist.providers"; +export * from "./lib/io-assist.types"; +export * from "./lib/io-assist.config"; diff --git a/templates/io-assist-angular-editable/src/app/io-assist/styles.css b/templates/io-assist-angular-editable/src/app/io-assist/styles.css index be8719c..72a43f2 100644 --- a/templates/io-assist-angular-editable/src/app/io-assist/styles.css +++ b/templates/io-assist-angular-editable/src/app/io-assist/styles.css @@ -1,5 +1,5 @@ /* You can add global styles to this file, and also import other style files */ @import 'prismjs/themes/prism-twilight.css'; @import 'tailwindcss'; -@import './shared/styles/_utils.css'; +@import './lib/shared/styles/_utils.css'; @import '@fontsource-variable/inter/wght.css'; From 28c90a30fe1643da4567764e5abaad55bd6ab7f9 Mon Sep 17 00:00:00 2001 From: Aleksandar Dimitrov Date: Mon, 3 Aug 2026 11:36:40 +0300 Subject: [PATCH 6/7] add build script --- .../io-assist-angular-editable/.gitignore | 3 ++ .../.postcssrc.json | 8 ++--- .../io-assist-angular-editable/package.json | 2 +- .../io-assist/scripts/io-assist-ng-build.js | 33 +++++++++++++++++++ .../src/app/io-assist/styles.css | 11 ++++++- 5 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 templates/io-assist-angular-editable/src/app/io-assist/scripts/io-assist-ng-build.js diff --git a/templates/io-assist-angular-editable/.gitignore b/templates/io-assist-angular-editable/.gitignore index 854acd5..931f4f1 100644 --- a/templates/io-assist-angular-editable/.gitignore +++ b/templates/io-assist-angular-editable/.gitignore @@ -11,6 +11,9 @@ npm-debug.log yarn-error.log +# Generated by src/app/io-assist/scripts/io-assist-ng-build.js (npm run build) +/src/app/io-assist/files + # IDEs and editors .idea/ .project diff --git a/templates/io-assist-angular-editable/.postcssrc.json b/templates/io-assist-angular-editable/.postcssrc.json index 9674304..e092dc7 100644 --- a/templates/io-assist-angular-editable/.postcssrc.json +++ b/templates/io-assist-angular-editable/.postcssrc.json @@ -1,5 +1,5 @@ { - "plugins": { - "@tailwindcss/postcss": {} - } -} \ No newline at end of file + "plugins": { + "@tailwindcss/postcss": {} + } +} diff --git a/templates/io-assist-angular-editable/package.json b/templates/io-assist-angular-editable/package.json index c8048d4..67023c1 100644 --- a/templates/io-assist-angular-editable/package.json +++ b/templates/io-assist-angular-editable/package.json @@ -4,7 +4,7 @@ "scripts": { "ng": "ng", "start": "ng serve --port 4103", - "build": "ng build", + "build": "node src/app/io-assist/scripts/io-assist-ng-build.js", "watch": "ng build --watch --configuration development", "test": "ng test" }, diff --git a/templates/io-assist-angular-editable/src/app/io-assist/scripts/io-assist-ng-build.js b/templates/io-assist-angular-editable/src/app/io-assist/scripts/io-assist-ng-build.js new file mode 100644 index 0000000..6aa45d0 --- /dev/null +++ b/templates/io-assist-angular-editable/src/app/io-assist/scripts/io-assist-ng-build.js @@ -0,0 +1,33 @@ +/** + * Steps: + * 1. ng build — compile the Angular library + * 2. copy-fonts — copy Inter woff2 files to dist/files/ + */ + +const { execSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +const IO_ASSIST_DIR = path.resolve(__dirname, ".."); + +const run = (cmd) => { + console.log(`\n> ${cmd}`); + execSync(cmd, { stdio: "inherit" }); +}; + +const copyFonts = () => { + const fontPackageDir = path.dirname(require.resolve("@fontsource-variable/inter/wght.css")); + const srcFile = path.join(fontPackageDir, "files", "inter-latin-wght-normal.woff2"); + const destDir = path.join(IO_ASSIST_DIR, "files"); + const destFile = path.join(destDir, "inter-latin-wght-normal.woff2"); + + fs.mkdirSync(destDir, { recursive: true }); + fs.copyFileSync(srcFile, destFile); + + console.log("Copied inter-latin-wght-normal.woff2 -> src/app/io-assist/files/"); +}; + +copyFonts(); +run("ng build"); + + diff --git a/templates/io-assist-angular-editable/src/app/io-assist/styles.css b/templates/io-assist-angular-editable/src/app/io-assist/styles.css index 72a43f2..3dff2c3 100644 --- a/templates/io-assist-angular-editable/src/app/io-assist/styles.css +++ b/templates/io-assist-angular-editable/src/app/io-assist/styles.css @@ -2,4 +2,13 @@ @import 'prismjs/themes/prism-twilight.css'; @import 'tailwindcss'; @import './lib/shared/styles/_utils.css'; -@import '@fontsource-variable/inter/wght.css'; + +/* Inter Variable — latin subset only (weights 100–900, normal style) */ +@font-face { + font-family: 'Inter Variable'; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url('./files/inter-latin-wght-normal.woff2') format('woff2-variations'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} From 2557485b0f007622f09b25e358164d71858a9d51 Mon Sep 17 00:00:00 2001 From: Aleksandar Dimitrov Date: Mon, 3 Aug 2026 11:37:15 +0300 Subject: [PATCH 7/7] comment --- .../src/app/io-assist/scripts/io-assist-ng-build.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/io-assist-angular-editable/src/app/io-assist/scripts/io-assist-ng-build.js b/templates/io-assist-angular-editable/src/app/io-assist/scripts/io-assist-ng-build.js index 6aa45d0..534aa12 100644 --- a/templates/io-assist-angular-editable/src/app/io-assist/scripts/io-assist-ng-build.js +++ b/templates/io-assist-angular-editable/src/app/io-assist/scripts/io-assist-ng-build.js @@ -1,7 +1,7 @@ /** * Steps: - * 1. ng build — compile the Angular library - * 2. copy-fonts — copy Inter woff2 files to dist/files/ + * 1. copy-fonts — copy Inter woff2 files to dist/files/ + * 2. ng build — compile the Angular library */ const { execSync } = require("child_process");