From 7d3a994057f91f0c121d1ab48336f4cdeccb78f5 Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Tue, 5 May 2026 16:55:27 +0800 Subject: [PATCH 1/3] init: chat kit commit --- .github/workflows/auto-publish.yml | 6 +- .github/workflows/dispatch-publish.yml | 6 +- .github/workflows/pr-ci-build.yml | 5 +- .github/workflows/pr-ci-publish-packages.yml | 1 + package.json | 3 +- packages/chat/README.md | 190 +++++ packages/chat/package.json | 53 ++ packages/chat/src/components/ChatFooter.vue | 14 + packages/chat/src/components/ChatHeader.vue | 122 ++++ packages/chat/src/components/ChatLayout.vue | 71 ++ .../chat/src/components/ChatMessageList.vue | 159 +++++ packages/chat/src/components/ChatSender.vue | 200 ++++++ packages/chat/src/components/ChatWelcome.vue | 43 ++ .../attachments/ChatAttachments.vue | 36 + .../chat/src/components/attachments/index.ts | 3 + .../attachments/useChatAttachments.ts | 90 +++ .../src/components/feedback/ChatFeedback.vue | 165 +++++ .../components/feedback/ChatUsagePanel.vue | 123 ++++ .../components/feedback/actions/copyAction.ts | 32 + .../components/feedback/actions/editAction.ts | 36 + .../src/components/feedback/actions/index.ts | 3 + .../feedback/actions/refreshAction.ts | 54 ++ .../chat/src/components/feedback/index.ts | 3 + .../components/feedback/useChatFeedback.ts | 253 +++++++ .../feedback/useFeedbackVisibility.ts | 109 +++ .../src/components/feedback/useUsageInfo.ts | 46 ++ .../src/components/history/ChatHistory.vue | 39 ++ .../components/history/ChatHistoryContent.vue | 15 + .../components/history/ChatHistoryList.vue | 136 ++++ .../history/ChatHistoryManageButton.vue | 77 ++ .../history/ChatHistoryNewSession.vue | 66 ++ .../components/history/ChatHistoryPanel.vue | 180 +++++ .../components/history/ChatHistorySearch.vue | 49 ++ .../components/history/ChatHistoryToolbar.vue | 29 + packages/chat/src/components/history/index.ts | 8 + .../src/components/history/useHistoryState.ts | 33 + packages/chat/src/components/index.ts | 11 + .../chat/src/components/mcp/ChatMcpPanel.vue | 302 ++++++++ .../chat/src/components/mcp/McpTrigger.vue | 77 ++ packages/chat/src/components/mcp/index.ts | 4 + .../chat/src/components/mcp/useMcpManager.ts | 184 +++++ .../model-selector/ModelSelector.vue | 169 +++++ .../src/components/model-selector/index.ts | 4 + .../model-selector/useFloatingDropdown.ts | 91 +++ .../model-selector/useKeyboardNavigation.ts | 126 ++++ .../model-selector/useModelSelector.ts | 71 ++ .../page-regions/ChatDefaultBodyRegion.vue | 73 ++ .../page-regions/ChatDefaultFooterRegion.vue | 74 ++ .../page-regions/ChatDefaultHeaderRegion.vue | 35 + .../page-regions/ChatPageContent.vue | 95 +++ .../chat/src/components/page-regions/index.ts | 4 + .../renderers/AttachmentsRenderer.vue | 43 ++ .../renderers/EditInputRenderer.vue | 238 +++++++ .../components/renderers/ErrorRenderer.vue | 99 +++ .../renderers/MarkStreamRenderer.vue | 154 ++++ .../components/renderers/ToolCallRenderer.vue | 109 +++ .../renderers/ToolCallsRenderer.vue | 21 + .../chat/src/components/renderers/index.ts | 6 + .../shared/ConditionalThemeProvider.vue | 34 + .../src/components/useDefaultBubbleConfig.ts | 86 +++ packages/chat/src/components/useSlotFilter.ts | 15 + .../workspace/ChatWorkspaceLayout.vue | 231 ++++++ .../workspace/ChatWorkspaceLeftSheet.vue | 79 +++ .../workspace/ChatWorkspaceRightEmpty.vue | 63 ++ .../workspace/ChatWorkspaceRightPanel.vue | 121 ++++ .../workspace/ChatWorkspaceRightSheet.vue | 123 ++++ .../workspace/ChatWorkspaceSidebar.vue | 25 + .../workspace/ChatWorkspaceSidebarRail.vue | 83 +++ .../workspace/ChatWorkspaceSidebarShell.vue | 146 ++++ .../components/workspace/WorkspaceShell.vue | 315 +++++++++ .../chat/src/components/workspace/index.ts | 9 + .../workspace/useWorkspaceRegion.ts | 60 ++ .../components/workspace/workspaceUtils.ts | 37 + .../chat/src/entry/RootBootstrapProvider.vue | 54 ++ packages/chat/src/entry/TrChat.vue | 36 + packages/chat/src/entry/TrChatPage.vue | 165 +++++ packages/chat/src/entry/TrChatProvider.vue | 69 ++ packages/chat/src/entry/TrChatRoot.vue | 36 + .../src/entry/createRootBootstrapState.ts | 295 ++++++++ packages/chat/src/entry/index.ts | 6 + packages/chat/src/index.ts | 113 +++ packages/chat/src/internal.ts | 9 + .../runtime/config/createRuntimeFromConfig.ts | 662 ++++++++++++++++++ packages/chat/src/runtime/config/index.ts | 2 + .../runtime/config/resolveProviderRuntime.ts | 50 ++ .../src/runtime/config/trchatConfigEntry.ts | 68 ++ .../useTrChatConfigRuntimeResolution.ts | 56 ++ .../chat/src/runtime/core/messageIdentity.ts | 65 ++ .../chat/src/runtime/core/normalizeRuntime.ts | 150 ++++ .../src/runtime/engine/chatMessageState.ts | 75 ++ .../src/runtime/engine/chatRenderMessages.ts | 55 ++ packages/chat/src/runtime/engine/index.ts | 24 + .../src/runtime/engine/useChatConversation.ts | 233 ++++++ .../chat/src/runtime/engine/useChatKit.ts | 389 ++++++++++ .../src/runtime/engine/useChatMessages.ts | 87 +++ .../chat/src/runtime/engine/useChatRequest.ts | 190 +++++ .../chat/src/runtime/features/featureTypes.ts | 100 +++ packages/chat/src/runtime/features/index.ts | 22 + .../chat/src/runtime/features/registry.ts | 272 +++++++ packages/chat/src/runtime/transport/index.ts | 1 + .../transport/openaiCompatibleTransport.ts | 227 ++++++ packages/chat/src/shared/attachments.ts | 86 +++ .../chat/src/shared/context/chatUiContext.ts | 409 +++++++++++ packages/chat/src/shared/context/index.ts | 146 ++++ packages/chat/src/shared/messages/index.ts | 111 +++ packages/chat/src/shared/utils/iconMap.ts | 48 ++ packages/chat/src/shared/utils/index.ts | 3 + packages/chat/src/shared/utils/props.ts | 8 + packages/chat/src/shared/utils/typeGuards.ts | 35 + packages/chat/src/styles/drawer.css | 33 + packages/chat/src/styles/index.css | 11 + packages/chat/src/styles/layout.css | 299 ++++++++ packages/chat/src/styles/mcp-trigger.css | 86 +++ packages/chat/src/styles/model-selector.css | 173 +++++ packages/chat/src/styles/tokens.css | 271 +++++++ packages/chat/src/types/component.ts | 345 +++++++++ packages/chat/src/types/config.ts | 130 ++++ packages/chat/src/types/core.ts | 175 +++++ packages/chat/src/types/index.ts | 124 ++++ packages/chat/src/types/message.ts | 75 ++ packages/chat/src/types/model.ts | 9 + packages/chat/src/types/runtime.ts | 154 ++++ packages/chat/src/types/workspace.ts | 38 + packages/chat/tsconfig.json | 25 + packages/chat/vite.config.ts | 49 ++ packages/svgs/src/assets/bailian.svg | 1 + packages/svgs/src/assets/claude.svg | 1 + packages/svgs/src/assets/deepseek.svg | 1 + packages/svgs/src/assets/gemini.svg | 1 + packages/svgs/src/assets/info.svg | 7 + packages/svgs/src/assets/menu-open.svg | 14 + packages/svgs/src/assets/modelscope.svg | 1 + packages/svgs/src/assets/ollama.svg | 1 + packages/svgs/src/assets/openai.svg | 1 + packages/svgs/src/assets/openrouter.svg | 1 + packages/svgs/src/assets/panel-left-close.svg | 8 + .../svgs/src/assets/panel-right-close.svg | 8 + 137 files changed, 12265 insertions(+), 9 deletions(-) create mode 100644 packages/chat/README.md create mode 100644 packages/chat/package.json create mode 100644 packages/chat/src/components/ChatFooter.vue create mode 100644 packages/chat/src/components/ChatHeader.vue create mode 100644 packages/chat/src/components/ChatLayout.vue create mode 100644 packages/chat/src/components/ChatMessageList.vue create mode 100644 packages/chat/src/components/ChatSender.vue create mode 100644 packages/chat/src/components/ChatWelcome.vue create mode 100644 packages/chat/src/components/attachments/ChatAttachments.vue create mode 100644 packages/chat/src/components/attachments/index.ts create mode 100644 packages/chat/src/components/attachments/useChatAttachments.ts create mode 100644 packages/chat/src/components/feedback/ChatFeedback.vue create mode 100644 packages/chat/src/components/feedback/ChatUsagePanel.vue create mode 100644 packages/chat/src/components/feedback/actions/copyAction.ts create mode 100644 packages/chat/src/components/feedback/actions/editAction.ts create mode 100644 packages/chat/src/components/feedback/actions/index.ts create mode 100644 packages/chat/src/components/feedback/actions/refreshAction.ts create mode 100644 packages/chat/src/components/feedback/index.ts create mode 100644 packages/chat/src/components/feedback/useChatFeedback.ts create mode 100644 packages/chat/src/components/feedback/useFeedbackVisibility.ts create mode 100644 packages/chat/src/components/feedback/useUsageInfo.ts create mode 100644 packages/chat/src/components/history/ChatHistory.vue create mode 100644 packages/chat/src/components/history/ChatHistoryContent.vue create mode 100644 packages/chat/src/components/history/ChatHistoryList.vue create mode 100644 packages/chat/src/components/history/ChatHistoryManageButton.vue create mode 100644 packages/chat/src/components/history/ChatHistoryNewSession.vue create mode 100644 packages/chat/src/components/history/ChatHistoryPanel.vue create mode 100644 packages/chat/src/components/history/ChatHistorySearch.vue create mode 100644 packages/chat/src/components/history/ChatHistoryToolbar.vue create mode 100644 packages/chat/src/components/history/index.ts create mode 100644 packages/chat/src/components/history/useHistoryState.ts create mode 100644 packages/chat/src/components/index.ts create mode 100644 packages/chat/src/components/mcp/ChatMcpPanel.vue create mode 100644 packages/chat/src/components/mcp/McpTrigger.vue create mode 100644 packages/chat/src/components/mcp/index.ts create mode 100644 packages/chat/src/components/mcp/useMcpManager.ts create mode 100644 packages/chat/src/components/model-selector/ModelSelector.vue create mode 100644 packages/chat/src/components/model-selector/index.ts create mode 100644 packages/chat/src/components/model-selector/useFloatingDropdown.ts create mode 100644 packages/chat/src/components/model-selector/useKeyboardNavigation.ts create mode 100644 packages/chat/src/components/model-selector/useModelSelector.ts create mode 100644 packages/chat/src/components/page-regions/ChatDefaultBodyRegion.vue create mode 100644 packages/chat/src/components/page-regions/ChatDefaultFooterRegion.vue create mode 100644 packages/chat/src/components/page-regions/ChatDefaultHeaderRegion.vue create mode 100644 packages/chat/src/components/page-regions/ChatPageContent.vue create mode 100644 packages/chat/src/components/page-regions/index.ts create mode 100644 packages/chat/src/components/renderers/AttachmentsRenderer.vue create mode 100644 packages/chat/src/components/renderers/EditInputRenderer.vue create mode 100644 packages/chat/src/components/renderers/ErrorRenderer.vue create mode 100644 packages/chat/src/components/renderers/MarkStreamRenderer.vue create mode 100644 packages/chat/src/components/renderers/ToolCallRenderer.vue create mode 100644 packages/chat/src/components/renderers/ToolCallsRenderer.vue create mode 100644 packages/chat/src/components/renderers/index.ts create mode 100644 packages/chat/src/components/shared/ConditionalThemeProvider.vue create mode 100644 packages/chat/src/components/useDefaultBubbleConfig.ts create mode 100644 packages/chat/src/components/useSlotFilter.ts create mode 100644 packages/chat/src/components/workspace/ChatWorkspaceLayout.vue create mode 100644 packages/chat/src/components/workspace/ChatWorkspaceLeftSheet.vue create mode 100644 packages/chat/src/components/workspace/ChatWorkspaceRightEmpty.vue create mode 100644 packages/chat/src/components/workspace/ChatWorkspaceRightPanel.vue create mode 100644 packages/chat/src/components/workspace/ChatWorkspaceRightSheet.vue create mode 100644 packages/chat/src/components/workspace/ChatWorkspaceSidebar.vue create mode 100644 packages/chat/src/components/workspace/ChatWorkspaceSidebarRail.vue create mode 100644 packages/chat/src/components/workspace/ChatWorkspaceSidebarShell.vue create mode 100644 packages/chat/src/components/workspace/WorkspaceShell.vue create mode 100644 packages/chat/src/components/workspace/index.ts create mode 100644 packages/chat/src/components/workspace/useWorkspaceRegion.ts create mode 100644 packages/chat/src/components/workspace/workspaceUtils.ts create mode 100644 packages/chat/src/entry/RootBootstrapProvider.vue create mode 100644 packages/chat/src/entry/TrChat.vue create mode 100644 packages/chat/src/entry/TrChatPage.vue create mode 100644 packages/chat/src/entry/TrChatProvider.vue create mode 100644 packages/chat/src/entry/TrChatRoot.vue create mode 100644 packages/chat/src/entry/createRootBootstrapState.ts create mode 100644 packages/chat/src/entry/index.ts create mode 100644 packages/chat/src/index.ts create mode 100644 packages/chat/src/internal.ts create mode 100644 packages/chat/src/runtime/config/createRuntimeFromConfig.ts create mode 100644 packages/chat/src/runtime/config/index.ts create mode 100644 packages/chat/src/runtime/config/resolveProviderRuntime.ts create mode 100644 packages/chat/src/runtime/config/trchatConfigEntry.ts create mode 100644 packages/chat/src/runtime/config/useTrChatConfigRuntimeResolution.ts create mode 100644 packages/chat/src/runtime/core/messageIdentity.ts create mode 100644 packages/chat/src/runtime/core/normalizeRuntime.ts create mode 100644 packages/chat/src/runtime/engine/chatMessageState.ts create mode 100644 packages/chat/src/runtime/engine/chatRenderMessages.ts create mode 100644 packages/chat/src/runtime/engine/index.ts create mode 100644 packages/chat/src/runtime/engine/useChatConversation.ts create mode 100644 packages/chat/src/runtime/engine/useChatKit.ts create mode 100644 packages/chat/src/runtime/engine/useChatMessages.ts create mode 100644 packages/chat/src/runtime/engine/useChatRequest.ts create mode 100644 packages/chat/src/runtime/features/featureTypes.ts create mode 100644 packages/chat/src/runtime/features/index.ts create mode 100644 packages/chat/src/runtime/features/registry.ts create mode 100644 packages/chat/src/runtime/transport/index.ts create mode 100644 packages/chat/src/runtime/transport/openaiCompatibleTransport.ts create mode 100644 packages/chat/src/shared/attachments.ts create mode 100644 packages/chat/src/shared/context/chatUiContext.ts create mode 100644 packages/chat/src/shared/context/index.ts create mode 100644 packages/chat/src/shared/messages/index.ts create mode 100644 packages/chat/src/shared/utils/iconMap.ts create mode 100644 packages/chat/src/shared/utils/index.ts create mode 100644 packages/chat/src/shared/utils/props.ts create mode 100644 packages/chat/src/shared/utils/typeGuards.ts create mode 100644 packages/chat/src/styles/drawer.css create mode 100644 packages/chat/src/styles/index.css create mode 100644 packages/chat/src/styles/layout.css create mode 100644 packages/chat/src/styles/mcp-trigger.css create mode 100644 packages/chat/src/styles/model-selector.css create mode 100644 packages/chat/src/styles/tokens.css create mode 100644 packages/chat/src/types/component.ts create mode 100644 packages/chat/src/types/config.ts create mode 100644 packages/chat/src/types/core.ts create mode 100644 packages/chat/src/types/index.ts create mode 100644 packages/chat/src/types/message.ts create mode 100644 packages/chat/src/types/model.ts create mode 100644 packages/chat/src/types/runtime.ts create mode 100644 packages/chat/src/types/workspace.ts create mode 100644 packages/chat/tsconfig.json create mode 100644 packages/chat/vite.config.ts create mode 100644 packages/svgs/src/assets/bailian.svg create mode 100644 packages/svgs/src/assets/claude.svg create mode 100644 packages/svgs/src/assets/deepseek.svg create mode 100644 packages/svgs/src/assets/gemini.svg create mode 100644 packages/svgs/src/assets/info.svg create mode 100644 packages/svgs/src/assets/menu-open.svg create mode 100644 packages/svgs/src/assets/modelscope.svg create mode 100644 packages/svgs/src/assets/ollama.svg create mode 100644 packages/svgs/src/assets/openai.svg create mode 100644 packages/svgs/src/assets/openrouter.svg create mode 100644 packages/svgs/src/assets/panel-left-close.svg create mode 100644 packages/svgs/src/assets/panel-right-close.svg diff --git a/.github/workflows/auto-publish.yml b/.github/workflows/auto-publish.yml index a291194cb..106715f22 100644 --- a/.github/workflows/auto-publish.yml +++ b/.github/workflows/auto-publish.yml @@ -52,9 +52,9 @@ jobs: - name: Install dependencies run: pnpm i --no-frozen-lockfile - # 步骤7: 构建组件 - - name: Run Build Components - run: pnpm build + # 步骤7: 构建发布包 + - name: Run Build Packages + run: pnpm build:packages - name: Parse Publish tag id: parse_tag diff --git a/.github/workflows/dispatch-publish.yml b/.github/workflows/dispatch-publish.yml index bf60c4677..253b13447 100644 --- a/.github/workflows/dispatch-publish.yml +++ b/.github/workflows/dispatch-publish.yml @@ -70,9 +70,9 @@ jobs: - name: Install dependencies run: pnpm i --no-frozen-lockfile - # 步骤7: 构建组件 - - name: Run Build Components - run: pnpm build + # 步骤7: 构建发布包 + - name: Run Build Packages + run: pnpm build:packages # 步骤8: 发布组件到NPM - name: Publish components diff --git a/.github/workflows/pr-ci-build.yml b/.github/workflows/pr-ci-build.yml index 6e675b09a..294270416 100644 --- a/.github/workflows/pr-ci-build.yml +++ b/.github/workflows/pr-ci-build.yml @@ -39,8 +39,8 @@ jobs: - name: Install dependencies run: pnpm i --no-frozen-lockfile - - name: Build components - run: pnpm build + - name: Build packages + run: pnpm build:packages - name: Build docs run: pnpm -F docs build @@ -55,6 +55,7 @@ jobs: name: build-${{ github.event.pull_request.head.sha || github.sha }} path: | packages/components/dist + packages/chat/dist packages/kit/dist packages/svgs/dist docs/dist diff --git a/.github/workflows/pr-ci-publish-packages.yml b/.github/workflows/pr-ci-publish-packages.yml index 5f4eed1b8..e1837125d 100644 --- a/.github/workflows/pr-ci-publish-packages.yml +++ b/.github/workflows/pr-ci-publish-packages.yml @@ -51,6 +51,7 @@ jobs: run: | npx pkg-pr-new publish \ ./packages/components \ + ./packages/chat \ ./packages/kit \ ./packages/svgs \ --pnpm \ diff --git a/package.json b/package.json index 030f0fdcc..b4d21a56a 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,10 @@ "postbuild:docs": "node scripts/copy-playground.js packages/playground/dist docs/dist/playground", "dev:playground": "pnpm -F @opentiny/tiny-robot-playground dev", "build:playground": "pnpm -F @opentiny/tiny-robot-playground build", - "build:deps": "pnpm -F @opentiny/tiny-robot-* -F \"!@opentiny/tiny-robot-playground\" build", + "build:deps": "pnpm -F @opentiny/tiny-robot-* -F \"!@opentiny/tiny-robot-playground\" -F \"!@opentiny/tiny-robot-chat\" build", "dev:components": "pnpm build:deps && pnpm -F @opentiny/tiny-robot dev", "build:components": "pnpm build:deps && pnpm -F @opentiny/tiny-robot build", + "build:packages": "pnpm build:components && pnpm -F @opentiny/tiny-robot-chat build", "dev:kit": "pnpm -F @opentiny/tiny-robot-kit dev", "build:kit": "pnpm -F @opentiny/tiny-robot-kit build", "build:svgs": "pnpm -F @opentiny/tiny-robot-svgs build", diff --git a/packages/chat/README.md b/packages/chat/README.md new file mode 100644 index 000000000..fcc516edb --- /dev/null +++ b/packages/chat/README.md @@ -0,0 +1,190 @@ +# @opentiny/tiny-robot-chat + +`@opentiny/tiny-robot-chat` 是 TinyRobot 提供的高级聊天 UI 包,基于 `@opentiny/tiny-robot`(基础组件库)和 `@opentiny/tiny-robot-kit`(数据层工具包)构建。 +它提供开箱即用的完整聊天页面,也支持逐步深入的白盒定制。 + +## 安装 + +```bash +pnpm add @opentiny/tiny-robot-chat +``` + +需要同时安装 peer dependencies: + +```bash +pnpm add @opentiny/tiny-robot @opentiny/tiny-robot-kit vue markstream-vue +``` + +## 基本用法 + +### 引入样式 + +```ts +import '@opentiny/tiny-robot-chat/style' +``` + +### 开箱即用(TrChat) + +传入一个 `TrChatConfig` 配置对象,即可渲染完整聊天页面: + +```vue + + + +``` + +## 三层入口 + +包提供三个官方入口层级,按定制深度递进: + +### 1. TrChat — 黑盒入口 + +适合快速接入,传入 `TrChatConfig` 即可。 + +```vue + +``` + +### 2. TrChat.Root + TrChat.Page — 白盒页面 + +适合需要自己创建 runtime 但保留官方页面组合的场景。 + +```vue + + + +``` + +使用 `createRuntimeFromConfig(config)` 从配置创建 runtime: + +```ts +import { createRuntimeFromConfig } from '@opentiny/tiny-robot-chat' + +const { runtime, ui, dispose } = createRuntimeFromConfig(config) +``` + +### 3. TrChat.Root + primitives — 细粒度组合 + +适合需要完全控制页面结构的场景,使用公开的 primitive 组件自由拼装。 + +```vue + + + + + + + + + + +``` + +### 高级:TrChat.Provider — 自定义 transport + +适合需要使用包的 UI 和聊天编排能力,但自带 transport / 数据层的团队。 + +```vue + + + + + + + + + +``` + +## 配置域 + +`TrChatConfig` 按功能域组织: + +| 配置域 | 职责 | +| --- | --- | +| `request` | 模型列表、默认模型、transport 配置 | +| `conversation` | 初始消息、持久化策略 | +| `ui` | 品牌、欢迎区、外观、内容布局、i18n 文案 | +| `sender` | 输入框占位符、模式、字数限制、语音 | +| `attachments` | 附件上传、列表配置 | +| `messages` | 消息操作、feedback、渲染器、transform | +| `history` | 历史记录启用、默认展开 | +| `workspace` | 工作区视图、左右区域配置 | +| `lifecycle` | `beforeSend` / `afterReceive` / `error` 钩子 | + +## 公开组件 + +| 组件 | 说明 | +| --- | --- | +| `TrChat` | 黑盒入口(含 `.Root` / `.Page` / `.Provider` 等子组件) | +| `TrChat.Layout` | 聊天布局容器 | +| `TrChat.WorkspaceLayout` | 工作区布局(含侧边栏) | +| `TrChat.Header` | 聊天头部 | +| `TrChat.Welcome` | 欢迎区 | +| `TrChat.MessageList` | 消息列表 | +| `TrChat.Footer` | 底部区域 | +| `TrChat.Sender` | 输入框 | +| `TrChat.Attachments` | 附件区 | +| `TrChat.History` | 历史记录 | +| `TrChatFeedback` | 消息反馈(独立导出) | +| `TrMcpTrigger` | MCP 触发器(独立导出) | + +## 验证命令 + +```bash +# 类型检查 +pnpm -F @opentiny/tiny-robot-chat type-check + +# 构建 +pnpm -F @opentiny/tiny-robot-chat build +``` + +## 目录结构 + +``` +src/ + entry/ # 入口组件(TrChat、TrChatRoot、TrChatPage、TrChatProvider) + components/ # UI 组件 + page-regions/ # 默认页面区域(Header/Body/Footer Region、ChatPageContent) + workspace/ # 工作区布局(WorkspaceShell、LeftSheet、RightSheet 等) + attachments/ # 附件 + feedback/ # 消息反馈 + history/ # 历史记录 + mcp/ # MCP 触发器和面板 + model-selector/ # 模型选择器 + renderers/ # 消息渲染器(Error、Edit、ToolCalls、Markdown 等) + shared/ # 共享组件(ConditionalThemeProvider) + runtime/ + config/ # createRuntimeFromConfig、config entry、provider resolution + core/ # normalizeRuntime、messageIdentity + engine/ # useChatKit、useChatConversation、useChatMessages、useChatRequest + transport/ # openaiCompatibleTransport + features/ # feature registry + shared/ + context/ # chatUiContext、injection keys + messages/ # i18n 文案(CHAT_MESSAGES) + utils/ # 工具函数 + types/ # 类型定义(component、config、runtime、message、workspace 等) + styles/ # CSS 样式 +``` diff --git a/packages/chat/package.json b/packages/chat/package.json new file mode 100644 index 000000000..dc230ac93 --- /dev/null +++ b/packages/chat/package.json @@ -0,0 +1,53 @@ +{ + "name": "@opentiny/tiny-robot-chat", + "version": "0.4.2", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./internal": { + "import": "./dist/internal.js", + "types": "./dist/internal.d.ts" + }, + "./style": { + "default": "./dist/style.css" + } + }, + "files": [ + "dist" + ], + "sideEffects": [ + "./dist/style.css" + ], + "scripts": { + "dev": "vue-tsc && vite build --watch", + "build": "vue-tsc && vite build", + "type-check": "vue-tsc --noEmit" + }, + "peerDependencies": { + "vue": "^3.3.11", + "markstream-vue": "^0.0.9-beta.0", + "@opentiny/tiny-robot": "workspace:*", + "@opentiny/tiny-robot-kit": "workspace:*" + }, + "dependencies": { + "@floating-ui/dom": "^1.7.5", + "@opentiny/tiny-robot-svgs": "workspace:*", + "@vueuse/core": "^13.1.0", + "markdown-it": "^14.1.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.5.2", + "less": "^4.2.2", + "typescript": "^5.2.2", + "vite": "^5.0.8", + "vite-plugin-dts": "^4.5.3", + "vue": "^3.3.11", + "vue-tsc": "^2.2.8" + } +} diff --git a/packages/chat/src/components/ChatFooter.vue b/packages/chat/src/components/ChatFooter.vue new file mode 100644 index 000000000..d6082fa8a --- /dev/null +++ b/packages/chat/src/components/ChatFooter.vue @@ -0,0 +1,14 @@ + + + diff --git a/packages/chat/src/components/ChatHeader.vue b/packages/chat/src/components/ChatHeader.vue new file mode 100644 index 000000000..53289c8e6 --- /dev/null +++ b/packages/chat/src/components/ChatHeader.vue @@ -0,0 +1,122 @@ + + + diff --git a/packages/chat/src/components/ChatLayout.vue b/packages/chat/src/components/ChatLayout.vue new file mode 100644 index 000000000..fcabf5fa7 --- /dev/null +++ b/packages/chat/src/components/ChatLayout.vue @@ -0,0 +1,71 @@ + + + diff --git a/packages/chat/src/components/ChatMessageList.vue b/packages/chat/src/components/ChatMessageList.vue new file mode 100644 index 000000000..84d1f4685 --- /dev/null +++ b/packages/chat/src/components/ChatMessageList.vue @@ -0,0 +1,159 @@ + + + diff --git a/packages/chat/src/components/ChatSender.vue b/packages/chat/src/components/ChatSender.vue new file mode 100644 index 000000000..43a80c79c --- /dev/null +++ b/packages/chat/src/components/ChatSender.vue @@ -0,0 +1,200 @@ + + + diff --git a/packages/chat/src/components/ChatWelcome.vue b/packages/chat/src/components/ChatWelcome.vue new file mode 100644 index 000000000..0712956c9 --- /dev/null +++ b/packages/chat/src/components/ChatWelcome.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/packages/chat/src/components/attachments/ChatAttachments.vue b/packages/chat/src/components/attachments/ChatAttachments.vue new file mode 100644 index 000000000..1f647855d --- /dev/null +++ b/packages/chat/src/components/attachments/ChatAttachments.vue @@ -0,0 +1,36 @@ + + + diff --git a/packages/chat/src/components/attachments/index.ts b/packages/chat/src/components/attachments/index.ts new file mode 100644 index 000000000..4728afbc3 --- /dev/null +++ b/packages/chat/src/components/attachments/index.ts @@ -0,0 +1,3 @@ +export { default as ChatAttachments } from './ChatAttachments.vue' +export { useChatAttachments } from './useChatAttachments' +export type { UseChatAttachmentsReturn } from './useChatAttachments' diff --git a/packages/chat/src/components/attachments/useChatAttachments.ts b/packages/chat/src/components/attachments/useChatAttachments.ts new file mode 100644 index 000000000..064142d8f --- /dev/null +++ b/packages/chat/src/components/attachments/useChatAttachments.ts @@ -0,0 +1,90 @@ +import { getCurrentScope, onScopeDispose, ref, type Ref } from 'vue' +import type { Attachment } from '@opentiny/tiny-robot' +import { detectFileType } from '@/shared/attachments' +import type { UseChatAttachmentsOptions } from '@/types' + +function normalizeAttachment(file: File): Attachment { + return { + rawFile: file, + url: URL.createObjectURL(file), + name: file.name, + size: file.size, + fileType: detectFileType(file), + status: 'success', + } +} + +export function useChatAttachments(options: UseChatAttachmentsOptions = {}) { + const items = ref([...(options.initialItems ?? [])]) + const ownedObjectUrls = new Set() + + function registerOwnedObjectUrl(item: Attachment) { + if (typeof item.url === 'string' && item.url) { + ownedObjectUrls.add(item.url) + } + } + + function revokeOwnedObjectUrl(item: Attachment) { + if (typeof item.url !== 'string' || !ownedObjectUrls.has(item.url)) { + return + } + + URL.revokeObjectURL(item.url) + ownedObjectUrls.delete(item.url) + } + + function revokeRemovedItems(nextItems: Attachment[]) { + const nextUrls = new Set( + nextItems.map((item) => item.url).filter((url): url is string => typeof url === 'string' && url.length > 0), + ) + + for (const item of items.value) { + if (typeof item.url === 'string' && !nextUrls.has(item.url)) { + revokeOwnedObjectUrl(item) + } + } + } + + function addFiles(files: File[]) { + const nextItems = files.map(normalizeAttachment) + nextItems.forEach(registerOwnedObjectUrl) + items.value.push(...nextItems) + } + + function setItems(nextItems: Attachment[]) { + revokeRemovedItems(nextItems) + items.value = [...nextItems] + } + + function removeItem(target: Attachment) { + revokeOwnedObjectUrl(target) + items.value = items.value.filter((item) => item !== target) + } + + function clear() { + items.value.forEach(revokeOwnedObjectUrl) + items.value = [] + } + + if (getCurrentScope()) { + onScopeDispose(() => { + clear() + }) + } + + return { + items, + addFiles, + setItems, + removeItem, + clear, + } +} + +export interface UseChatAttachmentsReturn { + items: Ref + addFiles: (files: File[]) => void + setItems: (items: Attachment[]) => void + removeItem: (item: Attachment) => void + clear: () => void +} diff --git a/packages/chat/src/components/feedback/ChatFeedback.vue b/packages/chat/src/components/feedback/ChatFeedback.vue new file mode 100644 index 000000000..21ad1913c --- /dev/null +++ b/packages/chat/src/components/feedback/ChatFeedback.vue @@ -0,0 +1,165 @@ + + + + + diff --git a/packages/chat/src/components/feedback/ChatUsagePanel.vue b/packages/chat/src/components/feedback/ChatUsagePanel.vue new file mode 100644 index 000000000..4c789997c --- /dev/null +++ b/packages/chat/src/components/feedback/ChatUsagePanel.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/packages/chat/src/components/feedback/actions/copyAction.ts b/packages/chat/src/components/feedback/actions/copyAction.ts new file mode 100644 index 000000000..2746e67d0 --- /dev/null +++ b/packages/chat/src/components/feedback/actions/copyAction.ts @@ -0,0 +1,32 @@ +import { useClipboard } from '@vueuse/core' +import type { ComputedRef } from 'vue' +import type { ChatMessageActionDefinition, ChatMessageActionRole, ChatRuntime } from '@/types' + +interface CopyActionOptions { + role: ChatMessageActionRole + label: string + content: ComputedRef + runtime: ChatRuntime | null + primaryMessageId: ComputedRef +} + +export function createCopyAction(options: CopyActionOptions): ChatMessageActionDefinition { + const { copy } = useClipboard() + const { role, label, content, runtime, primaryMessageId } = options + + return { + id: 'copy', + label, + icon: 'copy', + placement: 'actions', + roles: [role], + order: 100, + onClick: async () => { + if (runtime && primaryMessageId.value) { + await runtime.message.copy(primaryMessageId.value) + return + } + copy(content.value) + }, + } +} diff --git a/packages/chat/src/components/feedback/actions/editAction.ts b/packages/chat/src/components/feedback/actions/editAction.ts new file mode 100644 index 000000000..c1b2ea3e6 --- /dev/null +++ b/packages/chat/src/components/feedback/actions/editAction.ts @@ -0,0 +1,36 @@ +import { h } from 'vue' +import { TrIconButton } from '@opentiny/tiny-robot' +import { IconEditPen } from '@opentiny/tiny-robot-svgs' +import type { ChatMessageActionDefinition, ChatRuntime } from '@/types' + +interface EditActionFallbackRuntime { + startEditMessage: (messageIndex: number) => void +} + +interface EditActionOptions { + label: string + runtime: ChatRuntime | null + fallbackRuntime: EditActionFallbackRuntime | null +} + +export function createEditAction(options: EditActionOptions): ChatMessageActionDefinition { + const { label, runtime, fallbackRuntime } = options + + return { + id: 'edit', + label, + icon: h(TrIconButton, { icon: IconEditPen }), + placement: 'actions', + roles: ['user'], + order: 200, + onClick: (context) => { + if (runtime && context.messageId) { + runtime.message.startEdit(context.messageId) + return + } + if (fallbackRuntime && context.messageIndex !== undefined) { + fallbackRuntime.startEditMessage(context.messageIndex) + } + }, + } +} diff --git a/packages/chat/src/components/feedback/actions/index.ts b/packages/chat/src/components/feedback/actions/index.ts new file mode 100644 index 000000000..c087d476d --- /dev/null +++ b/packages/chat/src/components/feedback/actions/index.ts @@ -0,0 +1,3 @@ +export { createCopyAction } from './copyAction' +export { createEditAction } from './editAction' +export { createRefreshAction } from './refreshAction' diff --git a/packages/chat/src/components/feedback/actions/refreshAction.ts b/packages/chat/src/components/feedback/actions/refreshAction.ts new file mode 100644 index 000000000..2c7bd2659 --- /dev/null +++ b/packages/chat/src/components/feedback/actions/refreshAction.ts @@ -0,0 +1,54 @@ +import type { ComputedRef } from 'vue' +import type { ChatMessageActionDefinition, ChatErrorInfo, ChatRuntime } from '@/types' + +interface RefreshActionFallbackRuntime { + lastError: ComputedRef + retry: () => Promise + regenerate: (messageIndex?: number) => Promise +} + +interface RefreshActionOptions { + label: string + runtime: ChatRuntime | null + fallbackRuntime: RefreshActionFallbackRuntime | null + primaryMessageId: ComputedRef + isStreaming: ComputedRef + lastUserContent: ComputedRef +} + +export function createRefreshAction(options: RefreshActionOptions): ChatMessageActionDefinition { + const { label, runtime, fallbackRuntime, primaryMessageId, isStreaming, lastUserContent } = options + + return { + id: 'refresh', + label, + icon: 'refresh', + placement: 'actions', + roles: ['assistant'], + order: 200, + when: () => + Boolean((runtime && primaryMessageId.value) || (fallbackRuntime && !isStreaming.value && lastUserContent.value)), + onClick: async (context) => { + if (runtime && context.messageId) { + const viewState = runtime.message.getViewState(context.messageId) + if (viewState?.error?.retryable) { + await runtime.conversation.retry(context.messageId) + return + } + await runtime.conversation.regenerate(context.messageId) + return + } + + if (!fallbackRuntime || isStreaming.value || !lastUserContent.value) { + return + } + + if (fallbackRuntime.lastError.value?.retryable) { + await fallbackRuntime.retry() + return + } + + await fallbackRuntime.regenerate(context.messageIndex) + }, + } +} diff --git a/packages/chat/src/components/feedback/index.ts b/packages/chat/src/components/feedback/index.ts new file mode 100644 index 000000000..8ba8d2b26 --- /dev/null +++ b/packages/chat/src/components/feedback/index.ts @@ -0,0 +1,3 @@ +export { default as ChatFeedback } from './ChatFeedback.vue' +export { useChatFeedback } from './useChatFeedback' +export type { UseChatFeedbackOptions, UsageInfo } from './useChatFeedback' diff --git a/packages/chat/src/components/feedback/useChatFeedback.ts b/packages/chat/src/components/feedback/useChatFeedback.ts new file mode 100644 index 000000000..18128bdd2 --- /dev/null +++ b/packages/chat/src/components/feedback/useChatFeedback.ts @@ -0,0 +1,253 @@ +import { computed, type ComputedRef, type Ref } from 'vue' +import type { BubbleMessage, FeedbackProps } from '@opentiny/tiny-robot' +import type { ChatMessage } from '@opentiny/tiny-robot-kit' +import { useResolvedChatMessages } from '@/shared/messages' +import { getChatRenderSourceMessage, unwrapChatRenderMessages } from '@/runtime/engine/chatRenderMessages' +import { ensureRuntimeMessageId } from '@/runtime/core/messageIdentity' +import type { + ChatErrorInfo, + ChatMessageActionContext, + ChatMessageActionDefinition, + ChatMessageActionsInput, + ChatMessageActionsMode, + ChatRuntime, + ChatStatus, +} from '@/types' +import { createCopyAction, createEditAction, createRefreshAction } from './actions' +import { useFeedbackVisibility } from './useFeedbackVisibility' +import { useUsageInfo } from './useUsageInfo' +import type { UsageInfo } from './useUsageInfo' + +export type { UsageInfo } + +interface ChatFeedbackFallbackRuntime { + activeConversationId: Readonly> + messages: ComputedRef + status: ComputedRef + lastError: ComputedRef + startEditMessage: (messageIndex: number) => void + isMessageEditing: (messageIndex: number) => boolean + retry: () => Promise + regenerate: (messageIndex?: number) => Promise +} + +export interface UseChatFeedbackOptions { + messages: BubbleMessage[] + messageIndexes: number[] + role?: string + enabled?: boolean + runtime?: ChatRuntime | null + fallbackRuntime?: ChatFeedbackFallbackRuntime | null + messageActions?: ChatMessageActionsInput + messageActionsMode?: ChatMessageActionsMode +} + +export function useRuntimeFeedbackEnabled(options: { enabled?: boolean; runtime?: ChatRuntime | null }) { + return computed(() => options.enabled ?? options.runtime?.message.config?.feedback?.enabled ?? true) +} + +export function useChatFeedback(options: UseChatFeedbackOptions) { + const { messages, messageIndexes, role, fallbackRuntime = null, runtime = null } = options + + // --- Base derived state --- + + const sourceMessages = computed(() => unwrapChatRenderMessages(messages as unknown as ChatMessage[])) + const chatMessages = useResolvedChatMessages() + + const runtimeActionMode = computed(() => runtime?.message.config?.actionMode) + const messageActionMode = computed( + () => options.messageActionsMode ?? runtimeActionMode.value ?? 'append', + ) + + const primaryMessage = computed(() => + getChatRenderSourceMessage(sourceMessages.value[sourceMessages.value.length - 1]), + ) + + const messageIds = computed(() => + sourceMessages.value + .map((message) => ensureRuntimeMessageId(message)) + .filter((messageId): messageId is string => Boolean(messageId)), + ) + + const primaryMessageId = computed(() => + primaryMessage.value ? ensureRuntimeMessageId(primaryMessage.value) : undefined, + ) + + const primaryViewState = computed(() => + runtime && primaryMessageId.value ? runtime.message.getViewState(primaryMessageId.value) : undefined, + ) + + const lastContent = computed(() => { + const last = [...sourceMessages.value].reverse().find((m) => m.role === 'assistant' || !m.role) + if (!last?.content) return '' + return typeof last.content === 'string' ? last.content : JSON.stringify(last.content) + }) + + const lastUserContent = computed(() => { + if (!fallbackRuntime) return '' + const allMessages = fallbackRuntime.messages.value + const firstIndex = messageIndexes[0] ?? 0 + for (let index = firstIndex - 1; index >= 0; index--) { + if (allMessages[index]?.role === 'user') { + const content = allMessages[index].content + return typeof content === 'string' ? content : '' + } + } + return '' + }) + + const userContent = computed(() => { + const userMessage = sourceMessages.value.find((m) => m.role === 'user') + if (!userMessage?.content) return '' + return typeof userMessage.content === 'string' ? userMessage.content : JSON.stringify(userMessage.content) + }) + + const isStreaming = computed(() => + primaryViewState.value + ? primaryViewState.value.status === 'streaming' || primaryViewState.value.status === 'pending' + : fallbackRuntime + ? fallbackRuntime.status.value === 'streaming' || fallbackRuntime.status.value === 'submitted' + : false, + ) + + const actionContext = computed(() => ({ + role, + messages: sourceMessages.value, + messageIds: messageIds.value, + messageIndexes, + message: primaryMessage.value as ChatMessage | undefined, + messageIndex: messageIndexes[messageIndexes.length - 1], + messageId: primaryMessageId.value, + runtime, + conversationId: runtime ? undefined : (fallbackRuntime?.activeConversationId.value ?? undefined), + })) + + // --- Visibility --- + + const runtimeFeedbackEnabled = useRuntimeFeedbackEnabled({ enabled: options.enabled, runtime }) + + const { shouldRender, isEditing, hasError, isPendingAssistantTurn } = useFeedbackVisibility({ + role, + messages: sourceMessages, + primaryMessage, + primaryMessageId, + messageIndexes, + runtime, + fallbackRuntime, + runtimeFeedbackEnabled, + }) + + // --- Built-in actions --- + + const builtInActions = computed(() => { + if (role === 'user') { + return [ + createCopyAction({ + role: 'user', + label: chatMessages.value.feedback.copy, + content: userContent, + runtime, + primaryMessageId, + }), + createEditAction({ label: chatMessages.value.feedback.edit, runtime, fallbackRuntime }), + ] + } + + if (role === 'assistant') { + return [ + createCopyAction({ + role: 'assistant', + label: chatMessages.value.feedback.copy, + content: lastContent, + runtime, + primaryMessageId, + }), + createRefreshAction({ + label: chatMessages.value.feedback.regenerate, + runtime, + fallbackRuntime, + primaryMessageId, + isStreaming, + lastUserContent, + }), + ] + } + + return [] + }) + + // --- Custom actions --- + + const customActions = computed(() => { + const { messageActions } = options + if (messageActions) { + return typeof messageActions === 'function' ? messageActions(actionContext.value) : messageActions + } + if (runtime?.message.getActions && primaryMessageId.value) { + return runtime.message.getActions(primaryMessageId.value) ?? [] + } + return [] + }) + + // --- Resolved actions --- + + const resolvedActions = computed(() => { + const actionSource = + messageActionMode.value === 'replace' ? customActions.value : [...builtInActions.value, ...customActions.value] + + const deduped = new Map() + actionSource.forEach((action) => deduped.set(action.id, action)) + + return [...deduped.values()] + .filter((action) => { + const actionRoles = action.roles + if (actionRoles?.length && (!role || !actionRoles.includes(role as never))) return false + if (action.when && action.when(actionContext.value) === false) return false + return true + }) + .sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER)) + }) + + const feedbackActions = computed(() => + resolvedActions.value + .filter((action) => (action.placement ?? 'actions') === 'actions') + .map((action) => ({ name: action.id, label: action.label, icon: action.icon })), + ) + + const feedbackOperations = computed(() => + resolvedActions.value + .filter((action) => action.placement === 'operations') + .map((action) => ({ name: action.id, label: action.label })), + ) + + function getActionDefinition(actionId: string, placement?: 'actions' | 'operations') { + return resolvedActions.value.find( + (action) => action.id === actionId && (placement === undefined || (action.placement ?? 'actions') === placement), + ) + } + + // --- Usage info --- + + const usageInfo = useUsageInfo({ role, primaryMessage, isStreaming }) + + return { + shouldRender, + isEditing, + hasError, + isPendingAssistantTurn, + feedbackActions, + feedbackOperations, + getActionDefinition, + actionContext, + messageIds, + userContent, + usageInfo, + } +} + +/** + * @deprecated Use `useChatFeedback` directly — `fallbackRuntime` is now an optional param. + */ +export function useChatFeedbackWithFallbackRuntime(options: UseChatFeedbackOptions) { + return useChatFeedback(options) +} diff --git a/packages/chat/src/components/feedback/useFeedbackVisibility.ts b/packages/chat/src/components/feedback/useFeedbackVisibility.ts new file mode 100644 index 000000000..5c6a1f609 --- /dev/null +++ b/packages/chat/src/components/feedback/useFeedbackVisibility.ts @@ -0,0 +1,109 @@ +import { computed } from 'vue' +import type { ComputedRef } from 'vue' +import type { ChatMessage } from '@opentiny/tiny-robot-kit' +import { getChatMessageError, isChatMessageEditing } from '@/runtime/engine/chatMessageState' +import type { ChatRuntime, ChatStatus } from '@/types' + +interface FallbackRuntime { + messages: ComputedRef + status: ComputedRef + isMessageEditing: (messageIndex: number) => boolean +} + +export interface UseFeedbackVisibilityOptions { + role?: string + enabled?: boolean + messages: ComputedRef + primaryMessage: ComputedRef + primaryMessageId: ComputedRef + messageIndexes: number[] + runtime: ChatRuntime | null + fallbackRuntime: FallbackRuntime | null + runtimeFeedbackEnabled: ComputedRef +} + +export interface UseFeedbackVisibilityReturn { + isEditing: ComputedRef + isPendingAssistantTurn: ComputedRef + hasError: ComputedRef + shouldRender: ComputedRef +} + +export function useFeedbackVisibility(options: UseFeedbackVisibilityOptions): UseFeedbackVisibilityReturn { + const { + role, + messages, + primaryMessage, + primaryMessageId, + messageIndexes, + runtime, + fallbackRuntime, + runtimeFeedbackEnabled, + } = options + + const primarySourceError = computed(() => getChatMessageError(primaryMessage.value)) + const primarySourceEditing = computed(() => isChatMessageEditing(primaryMessage.value)) + const primarySourceStreaming = computed(() => Boolean(primaryMessage.value?.loading)) + + const latestAssistantIndex = computed(() => { + if (!fallbackRuntime) return undefined + const allMessages = fallbackRuntime.messages.value + for (let index = allMessages.length - 1; index >= 0; index--) { + if (allMessages[index]?.role === 'assistant') return index + } + return undefined + }) + + const isEditing = computed(() => { + if (primaryMessageId.value && runtime) { + const runtimeEditing = runtime.message.getViewState(primaryMessageId.value)?.editing + if (runtimeEditing !== undefined) return Boolean(runtimeEditing) + } + + if (primarySourceEditing.value) return true + + if (!fallbackRuntime) return false + const primaryMessageIndex = messageIndexes[0] + if (primaryMessageIndex === undefined) return false + return Boolean(fallbackRuntime.isMessageEditing(primaryMessageIndex)) + }) + + const isPendingAssistantTurn = computed(() => { + if (role !== 'assistant') return false + + if (primaryMessageId.value && runtime) { + const status = runtime.message.getViewState(primaryMessageId.value)?.status + if (status !== undefined) return status === 'pending' || status === 'streaming' + } + + if (primarySourceStreaming.value) return true + + if (!fallbackRuntime) return false + const latestIndex = latestAssistantIndex.value + if (latestIndex === undefined) return false + return messageIndexes.includes(latestIndex) && fallbackRuntime.status.value !== 'ready' + }) + + const hasError = computed(() => { + if (primaryMessageId.value && runtime) { + const runtimeError = runtime.message.getViewState(primaryMessageId.value)?.error + if (runtimeError !== undefined) return Boolean(runtimeError) + } + + if (primarySourceError.value) return true + + return messages.value.some((message) => Boolean(getChatMessageError(message))) + }) + + const shouldRender = computed(() => { + if (!runtimeFeedbackEnabled.value) return false + if (isEditing.value) return false + if (role === 'assistant') { + if (hasError.value) return false + if (isPendingAssistantTurn.value) return false + } + return true + }) + + return { isEditing, isPendingAssistantTurn, hasError, shouldRender } +} diff --git a/packages/chat/src/components/feedback/useUsageInfo.ts b/packages/chat/src/components/feedback/useUsageInfo.ts new file mode 100644 index 000000000..6e2f04fb9 --- /dev/null +++ b/packages/chat/src/components/feedback/useUsageInfo.ts @@ -0,0 +1,46 @@ +import { computed } from 'vue' +import type { ComputedRef } from 'vue' +import type { ChatMessage } from '@opentiny/tiny-robot-kit' + +export interface UsageInfo { + model?: string + finishReason?: string | null + createdAt?: number + promptTokens?: number + completionTokens?: number + totalTokens?: number +} + +interface UseUsageInfoOptions { + role?: string + primaryMessage: ComputedRef + isStreaming: ComputedRef +} + +export function useUsageInfo(options: UseUsageInfoOptions): ComputedRef { + return computed(() => { + if (options.role !== 'assistant') return null + + if (options.isStreaming.value) return null + + const message = options.primaryMessage.value + if (!message) return null + + const metadata = message.metadata + if (!metadata) return null + + const usage = metadata.usage as + | { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number } + | undefined + if (!usage) return null + + return { + model: metadata.model as string | undefined, + finishReason: (metadata.choices as Array<{ finish_reason?: string | null }> | undefined)?.[0]?.finish_reason, + createdAt: metadata.createdAt as number | undefined, + promptTokens: usage.prompt_tokens, + completionTokens: usage.completion_tokens, + totalTokens: usage.total_tokens, + } + }) +} diff --git a/packages/chat/src/components/history/ChatHistory.vue b/packages/chat/src/components/history/ChatHistory.vue new file mode 100644 index 000000000..1a691cd38 --- /dev/null +++ b/packages/chat/src/components/history/ChatHistory.vue @@ -0,0 +1,39 @@ + + + diff --git a/packages/chat/src/components/history/ChatHistoryContent.vue b/packages/chat/src/components/history/ChatHistoryContent.vue new file mode 100644 index 000000000..496e53d8e --- /dev/null +++ b/packages/chat/src/components/history/ChatHistoryContent.vue @@ -0,0 +1,15 @@ + + + diff --git a/packages/chat/src/components/history/ChatHistoryList.vue b/packages/chat/src/components/history/ChatHistoryList.vue new file mode 100644 index 000000000..0b5cf87d8 --- /dev/null +++ b/packages/chat/src/components/history/ChatHistoryList.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/packages/chat/src/components/history/ChatHistoryManageButton.vue b/packages/chat/src/components/history/ChatHistoryManageButton.vue new file mode 100644 index 000000000..3073a7de7 --- /dev/null +++ b/packages/chat/src/components/history/ChatHistoryManageButton.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/packages/chat/src/components/history/ChatHistoryNewSession.vue b/packages/chat/src/components/history/ChatHistoryNewSession.vue new file mode 100644 index 000000000..c7acf6996 --- /dev/null +++ b/packages/chat/src/components/history/ChatHistoryNewSession.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/packages/chat/src/components/history/ChatHistoryPanel.vue b/packages/chat/src/components/history/ChatHistoryPanel.vue new file mode 100644 index 000000000..0a50717d4 --- /dev/null +++ b/packages/chat/src/components/history/ChatHistoryPanel.vue @@ -0,0 +1,180 @@ + + + + + diff --git a/packages/chat/src/components/history/ChatHistorySearch.vue b/packages/chat/src/components/history/ChatHistorySearch.vue new file mode 100644 index 000000000..06ba7d0b7 --- /dev/null +++ b/packages/chat/src/components/history/ChatHistorySearch.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/packages/chat/src/components/history/ChatHistoryToolbar.vue b/packages/chat/src/components/history/ChatHistoryToolbar.vue new file mode 100644 index 000000000..0ee27ee16 --- /dev/null +++ b/packages/chat/src/components/history/ChatHistoryToolbar.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/packages/chat/src/components/history/index.ts b/packages/chat/src/components/history/index.ts new file mode 100644 index 000000000..f72537357 --- /dev/null +++ b/packages/chat/src/components/history/index.ts @@ -0,0 +1,8 @@ +export { default as ChatHistory } from './ChatHistory.vue' +export { default as ChatHistoryContent } from './ChatHistoryContent.vue' +export { default as ChatHistoryNewSession } from './ChatHistoryNewSession.vue' +export { default as ChatHistoryManageButton } from './ChatHistoryManageButton.vue' +export { default as ChatHistoryList } from './ChatHistoryList.vue' +export { default as ChatHistoryPanel } from './ChatHistoryPanel.vue' +export { default as ChatHistorySearch } from './ChatHistorySearch.vue' +export { default as ChatHistoryToolbar } from './ChatHistoryToolbar.vue' diff --git a/packages/chat/src/components/history/useHistoryState.ts b/packages/chat/src/components/history/useHistoryState.ts new file mode 100644 index 000000000..f0e402c8f --- /dev/null +++ b/packages/chat/src/components/history/useHistoryState.ts @@ -0,0 +1,33 @@ +import { ref } from 'vue' + +export function useHistoryState() { + const isManagementMode = ref(false) + const searchQuery = ref('') + const selectedItems = ref([]) + + function toggleItemSelection(id: string) { + const idx = selectedItems.value.indexOf(id) + if (idx === -1) selectedItems.value.push(id) + else selectedItems.value.splice(idx, 1) + } + + function selectAll(ids: string[]) { + selectedItems.value = [...ids] + } + + function clearSelection() { + selectedItems.value = [] + searchQuery.value = '' + } + + return { + isManagementMode, + searchQuery, + selectedItems, + toggleItemSelection, + selectAll, + clearSelection, + } +} + +export type UseHistoryStateReturn = ReturnType diff --git a/packages/chat/src/components/index.ts b/packages/chat/src/components/index.ts new file mode 100644 index 000000000..e7a527d7e --- /dev/null +++ b/packages/chat/src/components/index.ts @@ -0,0 +1,11 @@ +export { default as ChatLayout } from './ChatLayout.vue' +export { default as ChatHeader } from './ChatHeader.vue' +export { default as ChatWelcome } from './ChatWelcome.vue' +export { default as ChatMessageList } from './ChatMessageList.vue' +export { default as ChatFooter } from './ChatFooter.vue' +export { default as ChatSender } from './ChatSender.vue' +export { default as ChatProvider } from '@/entry/TrChatProvider.vue' +export { useDefaultBubbleConfig } from './useDefaultBubbleConfig' +export { useSlotFilter } from './useSlotFilter' +export type { UseDefaultBubbleConfigOptions } from './useDefaultBubbleConfig' +export * from './page-regions' diff --git a/packages/chat/src/components/mcp/ChatMcpPanel.vue b/packages/chat/src/components/mcp/ChatMcpPanel.vue new file mode 100644 index 000000000..3aa724084 --- /dev/null +++ b/packages/chat/src/components/mcp/ChatMcpPanel.vue @@ -0,0 +1,302 @@ + + + + + diff --git a/packages/chat/src/components/mcp/McpTrigger.vue b/packages/chat/src/components/mcp/McpTrigger.vue new file mode 100644 index 000000000..51ce69bc6 --- /dev/null +++ b/packages/chat/src/components/mcp/McpTrigger.vue @@ -0,0 +1,77 @@ + + + diff --git a/packages/chat/src/components/mcp/index.ts b/packages/chat/src/components/mcp/index.ts new file mode 100644 index 000000000..5d463b166 --- /dev/null +++ b/packages/chat/src/components/mcp/index.ts @@ -0,0 +1,4 @@ +export { default as ChatMcpPanel } from './ChatMcpPanel.vue' +export { default as McpTrigger } from './McpTrigger.vue' +export { useMcpManager } from './useMcpManager' +export type { UseMcpManagerBridge, UseMcpManagerOptions, UseMcpManagerReturn } from './useMcpManager' diff --git a/packages/chat/src/components/mcp/useMcpManager.ts b/packages/chat/src/components/mcp/useMcpManager.ts new file mode 100644 index 000000000..e48aa9d23 --- /dev/null +++ b/packages/chat/src/components/mcp/useMcpManager.ts @@ -0,0 +1,184 @@ +import { computed, ref } from 'vue' +import type { MaybePromise, Tool, ToolCall } from '@opentiny/tiny-robot-kit' +import type { PluginInfo, PluginTool } from '@opentiny/tiny-robot' + +export interface McpToolContext { + plugin: PluginInfo + tool: PluginTool + installedPlugins: PluginInfo[] +} + +export interface McpPluginContext { + installedPlugins: PluginInfo[] +} + +export interface UseMcpManagerBridge { + getTools?: (context: McpPluginContext) => MaybePromise + callTool?: (toolCall: ToolCall, context: McpToolContext) => MaybePromise> + onPluginToggle?: (plugin: PluginInfo, enabled: boolean, context: McpPluginContext) => MaybePromise + onToolToggle?: (plugin: PluginInfo, toolId: string, enabled: boolean, context: McpPluginContext) => MaybePromise + onPluginCreate?: (type: 'form' | 'code', data: unknown, context: McpPluginContext) => MaybePromise + onPluginDelete?: (plugin: PluginInfo, context: McpPluginContext) => MaybePromise +} + +export interface UseMcpManagerOptions { + initialPlugins?: PluginInfo[] + bridge?: UseMcpManagerBridge +} + +function clonePlugins(plugins: PluginInfo[]): PluginInfo[] { + return plugins.map((plugin) => ({ + ...plugin, + tools: plugin.tools.map((tool) => ({ ...tool })), + })) +} + +function createPluginContext(installedPlugins: PluginInfo[]): McpPluginContext { + return { + installedPlugins, + } +} + +function normalizeToolResult(result: string | Record): string { + return typeof result === 'string' ? result : JSON.stringify(result) +} + +function createMissingBridgeError(toolName: string): string { + return JSON.stringify({ + error: `No MCP bridge configured for tool "${toolName}"`, + }) +} + +export function useMcpManager(options: UseMcpManagerOptions = {}) { + const installedPlugins = ref(clonePlugins(options.initialPlugins ?? [])) + const bridge = options.bridge + + const getTools = async (): Promise => { + if (bridge?.getTools) { + return await bridge.getTools(createPluginContext(installedPlugins.value)) + } + + const tools: Tool[] = [] + + for (const plugin of installedPlugins.value) { + if (!plugin.enabled) { + continue + } + + for (const tool of plugin.tools) { + if (!tool.enabled) { + continue + } + + tools.push({ + type: 'function', + function: { + name: `${plugin.id}__${tool.id}`, + description: tool.description, + parameters: { + type: 'object', + properties: {}, + required: [], + }, + }, + }) + } + } + + return tools + } + + const callTool = async (toolCall: ToolCall): Promise => { + const toolName = toolCall.function.name + const [pluginId, toolId] = toolName.split('__') + + const plugin = installedPlugins.value.find((item) => item.id === pluginId) + if (!plugin) { + return JSON.stringify({ error: `Plugin not found: ${pluginId}` }) + } + + const tool = plugin.tools.find((item) => item.id === toolId) + if (!tool) { + return JSON.stringify({ error: `Tool not found: ${toolId}` }) + } + + if (!bridge?.callTool) { + return createMissingBridgeError(toolName) + } + + return normalizeToolResult( + await bridge.callTool(toolCall, { + plugin, + tool, + installedPlugins: installedPlugins.value, + }), + ) + } + + async function handlePluginToggle(plugin: PluginInfo, enabled: boolean) { + const target = installedPlugins.value.find((item) => item.id === plugin.id) + if (target) { + target.enabled = enabled + if (!enabled) { + target.tools.forEach((tool) => { + tool.enabled = false + }) + } + } + + await bridge?.onPluginToggle?.(plugin, enabled, createPluginContext(installedPlugins.value)) + } + + async function handleToolToggle(plugin: PluginInfo, toolId: string, enabled: boolean) { + const target = installedPlugins.value.find((item) => item.id === plugin.id) + if (target) { + const tool = target.tools.find((item) => item.id === toolId) + if (tool) { + tool.enabled = enabled + } + } + + await bridge?.onToolToggle?.(plugin, toolId, enabled, createPluginContext(installedPlugins.value)) + } + + async function handlePluginCreate(type: 'form' | 'code', data: unknown) { + const plugin = await bridge?.onPluginCreate?.(type, data, createPluginContext(installedPlugins.value)) + if (!plugin) { + return + } + + const nextPlugin = clonePlugins([plugin])[0] + const existingIndex = installedPlugins.value.findIndex((item) => item.id === nextPlugin.id) + if (existingIndex >= 0) { + installedPlugins.value.splice(existingIndex, 1, nextPlugin) + } else { + installedPlugins.value.push(nextPlugin) + } + } + + async function handlePluginDelete(plugin: PluginInfo) { + const index = installedPlugins.value.findIndex((item) => item.id === plugin.id) + if (index >= 0) { + installedPlugins.value.splice(index, 1) + } + + await bridge?.onPluginDelete?.(plugin, createPluginContext(installedPlugins.value)) + } + + const activeCount = computed(() => { + return installedPlugins.value.filter((plugin) => plugin.enabled).length + }) + + return { + installedPlugins, + getTools, + callTool, + handlePluginToggle, + handleToolToggle, + handlePluginCreate, + handlePluginDelete, + activeCount, + } +} + +export type UseMcpManagerReturn = ReturnType diff --git a/packages/chat/src/components/model-selector/ModelSelector.vue b/packages/chat/src/components/model-selector/ModelSelector.vue new file mode 100644 index 000000000..62e4f7ac0 --- /dev/null +++ b/packages/chat/src/components/model-selector/ModelSelector.vue @@ -0,0 +1,169 @@ + + + diff --git a/packages/chat/src/components/model-selector/index.ts b/packages/chat/src/components/model-selector/index.ts new file mode 100644 index 000000000..89e11d7b7 --- /dev/null +++ b/packages/chat/src/components/model-selector/index.ts @@ -0,0 +1,4 @@ +export { default as ModelSelector } from './ModelSelector.vue' +export { useModelSelector } from './useModelSelector' +export { useFloatingDropdown } from './useFloatingDropdown' +export { useKeyboardNavigation } from './useKeyboardNavigation' diff --git a/packages/chat/src/components/model-selector/useFloatingDropdown.ts b/packages/chat/src/components/model-selector/useFloatingDropdown.ts new file mode 100644 index 000000000..63fb3b4ad --- /dev/null +++ b/packages/chat/src/components/model-selector/useFloatingDropdown.ts @@ -0,0 +1,91 @@ +import { ref, nextTick, onScopeDispose, watch } from 'vue' +import { computePosition, flip, shift, offset, autoUpdate } from '@floating-ui/dom' +import { onClickOutside, useEventListener } from '@vueuse/core' + +export function useFloatingDropdown( + referenceEl: ReturnType>, + floatingEl: ReturnType>, +) { + const isOpen = ref(false) + let cleanupAutoUpdate: (() => void) | null = null + + const updatePosition = async () => { + if (!referenceEl.value || !floatingEl.value) return + + const { x, y } = await computePosition(referenceEl.value, floatingEl.value, { + placement: 'bottom-start', + strategy: 'absolute', + middleware: [offset(8), flip({ padding: 8 }), shift({ padding: 8 })], + }) + + const dpr = window.devicePixelRatio || 1 + const roundedX = Math.round(x * dpr) / dpr + const roundedY = Math.round(y * dpr) / dpr + + Object.assign(floatingEl.value.style, { + left: '0', + top: '0', + transform: `translate(${roundedX}px, ${roundedY}px)`, + }) + } + + const startAutoUpdate = () => { + if (!referenceEl.value || !floatingEl.value) return + cleanupAutoUpdate = autoUpdate(referenceEl.value, floatingEl.value, updatePosition) + } + + const stopAutoUpdate = () => { + cleanupAutoUpdate?.() + cleanupAutoUpdate = null + } + + onClickOutside( + floatingEl, + () => { + if (!isOpen.value) { + return + } + + isOpen.value = false + }, + { + ignore: [referenceEl], + }, + ) + + if (typeof document !== 'undefined') { + useEventListener(document, 'keydown', (event: KeyboardEvent) => { + if (!isOpen.value) { + return + } + + if (event.key === 'Escape') { + event.preventDefault() + isOpen.value = false + referenceEl.value?.focus() + } + }) + } + + watch(isOpen, async (newVal) => { + if (newVal) { + await nextTick() + updatePosition() + startAutoUpdate() + } else { + stopAutoUpdate() + } + }) + + watch([referenceEl, floatingEl], () => { + if (!isOpen.value) { + stopAutoUpdate() + } + }) + + onScopeDispose(() => { + stopAutoUpdate() + }) + + return { isOpen, updatePosition } +} diff --git a/packages/chat/src/components/model-selector/useKeyboardNavigation.ts b/packages/chat/src/components/model-selector/useKeyboardNavigation.ts new file mode 100644 index 000000000..6cd03f4ef --- /dev/null +++ b/packages/chat/src/components/model-selector/useKeyboardNavigation.ts @@ -0,0 +1,126 @@ +import { ref, type Ref } from 'vue' +import { useMagicKeys, onKeyStroke, whenever } from '@vueuse/core' + +export interface UseKeyboardNavigationOptions { + /** + * 是否启用键盘导航 + */ + enabled?: Ref | boolean + /** + * 项目数量 + */ + itemCount: Ref | number + /** + * 是否禁用某个项目 + */ + isItemDisabled?: (index: number) => boolean + /** + * 选择某个项目后的回调 + */ + onSelect?: (index: number) => void + /** + * 关闭列表时的回调 + */ + onClose?: () => void + /** + * 是否可以循环导航 + */ + loop?: boolean +} + +export interface UseKeyboardNavigationReturn { + /** + * 当前高亮索引 + */ + highlightedIndex: Ref + /** + * 重置高亮 + */ + reset: () => void + /** + * 设置高亮索引 + */ + setHighlightedIndex: (index: number) => void +} + +export function useKeyboardNavigation(options: UseKeyboardNavigationOptions): UseKeyboardNavigationReturn { + const { enabled = true, itemCount, isItemDisabled, onSelect, onClose, loop = false } = options + + const highlightedIndex = ref(0) + const { ArrowUp, ArrowDown } = useMagicKeys() + + const isEnabled = () => { + if (typeof enabled === 'boolean') return enabled + return enabled.value + } + + const getItemCount = () => { + if (typeof itemCount === 'number') return itemCount + return itemCount.value + } + + const reset = () => { + highlightedIndex.value = 0 + } + + const setHighlightedIndex = (index: number) => { + const count = getItemCount() + if (count <= 0) { + highlightedIndex.value = 0 + return + } + highlightedIndex.value = Math.max(0, Math.min(index, count - 1)) + } + + const switchHighlightedIndex = (isNext: boolean) => { + const count = getItemCount() + if (count <= 0) return + + let target = highlightedIndex.value + do { + if (isNext) { + target = loop ? (target + 1) % count : Math.min(target + 1, count - 1) + } else { + target = loop ? (target - 1 + count) % count : Math.max(target - 1, 0) + } + } while (isItemDisabled?.(target) && target !== highlightedIndex.value) + highlightedIndex.value = target + } + + whenever( + () => ArrowUp.value && isEnabled(), + () => { + switchHighlightedIndex(false) + }, + ) + + whenever( + () => ArrowDown.value && isEnabled(), + () => { + switchHighlightedIndex(true) + }, + ) + + onKeyStroke('Enter', (event) => { + if (!isEnabled()) return + + const count = getItemCount() + if (count > 0 && highlightedIndex.value < count) { + event.preventDefault() + onSelect?.(highlightedIndex.value) + } + }) + + onKeyStroke('Escape', (event) => { + if (!isEnabled()) return + + event.preventDefault() + onClose?.() + }) + + return { + highlightedIndex, + reset, + setHighlightedIndex, + } +} diff --git a/packages/chat/src/components/model-selector/useModelSelector.ts b/packages/chat/src/components/model-selector/useModelSelector.ts new file mode 100644 index 000000000..626277a10 --- /dev/null +++ b/packages/chat/src/components/model-selector/useModelSelector.ts @@ -0,0 +1,71 @@ +import { computed, toValue, watchEffect, type MaybeRefOrGetter, type Ref } from 'vue' +import type { ModelOption } from '@/types' +import { getProviderIcon } from '@/shared/utils/iconMap' + +export interface UseModelSelectorOptions { + currentModel: Ref + models: MaybeRefOrGetter + onChange?: (model: ModelOption) => void +} + +interface CommitModelOptions { + notifyChange?: boolean +} + +export function useModelSelector(options: UseModelSelectorOptions) { + const models = computed(() => toValue(options.models)) + + const currentModelOption = computed(() => { + return models.value.find((model) => model.value === options.currentModel.value) + }) + + const currentProvider = computed(() => { + return currentModelOption.value ? getProviderIcon(currentModelOption.value) : null + }) + + function canSelectModel(model: ModelOption) { + return !model.disabled + } + + function commitModel(model: ModelOption, commitOptions: CommitModelOptions = {}) { + const { notifyChange = true } = commitOptions + + if (!canSelectModel(model)) { + return false + } + + options.currentModel.value = model.value + + if (notifyChange) { + options.onChange?.(model) + } + + return true + } + + watchEffect(() => { + if (models.value.length === 0) { + return + } + + const selectedModel = currentModelOption.value + const isCurrentModelSelectable = selectedModel ? canSelectModel(selectedModel) : false + + if (!isCurrentModelSelectable) { + const fallbackModel = models.value.find((model) => canSelectModel(model)) + if (fallbackModel) { + commitModel(fallbackModel) + } + } + }) + + function selectModel(model: ModelOption, options?: CommitModelOptions) { + commitModel(model, options) + } + + return { + currentModelOption, + currentProvider, + selectModel, + } +} diff --git a/packages/chat/src/components/page-regions/ChatDefaultBodyRegion.vue b/packages/chat/src/components/page-regions/ChatDefaultBodyRegion.vue new file mode 100644 index 000000000..e96b6506f --- /dev/null +++ b/packages/chat/src/components/page-regions/ChatDefaultBodyRegion.vue @@ -0,0 +1,73 @@ + + + diff --git a/packages/chat/src/components/page-regions/ChatDefaultFooterRegion.vue b/packages/chat/src/components/page-regions/ChatDefaultFooterRegion.vue new file mode 100644 index 000000000..fd86fda48 --- /dev/null +++ b/packages/chat/src/components/page-regions/ChatDefaultFooterRegion.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/packages/chat/src/components/page-regions/ChatDefaultHeaderRegion.vue b/packages/chat/src/components/page-regions/ChatDefaultHeaderRegion.vue new file mode 100644 index 000000000..39aaeb827 --- /dev/null +++ b/packages/chat/src/components/page-regions/ChatDefaultHeaderRegion.vue @@ -0,0 +1,35 @@ + + + diff --git a/packages/chat/src/components/page-regions/ChatPageContent.vue b/packages/chat/src/components/page-regions/ChatPageContent.vue new file mode 100644 index 000000000..7c775ac5e --- /dev/null +++ b/packages/chat/src/components/page-regions/ChatPageContent.vue @@ -0,0 +1,95 @@ + + + diff --git a/packages/chat/src/components/page-regions/index.ts b/packages/chat/src/components/page-regions/index.ts new file mode 100644 index 000000000..9b932e165 --- /dev/null +++ b/packages/chat/src/components/page-regions/index.ts @@ -0,0 +1,4 @@ +export { default as ChatDefaultHeaderRegion } from './ChatDefaultHeaderRegion.vue' +export { default as ChatDefaultBodyRegion } from './ChatDefaultBodyRegion.vue' +export { default as ChatDefaultFooterRegion } from './ChatDefaultFooterRegion.vue' +export { default as ChatPageContent } from './ChatPageContent.vue' diff --git a/packages/chat/src/components/renderers/AttachmentsRenderer.vue b/packages/chat/src/components/renderers/AttachmentsRenderer.vue new file mode 100644 index 000000000..e62c06b81 --- /dev/null +++ b/packages/chat/src/components/renderers/AttachmentsRenderer.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/packages/chat/src/components/renderers/EditInputRenderer.vue b/packages/chat/src/components/renderers/EditInputRenderer.vue new file mode 100644 index 000000000..766d6dfcd --- /dev/null +++ b/packages/chat/src/components/renderers/EditInputRenderer.vue @@ -0,0 +1,238 @@ + + +