Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,19 @@
"peerDependencies": {
"dompurify": "^3.3.1",
"markdown-it": "^14.1.0",
"vue": "^3.3.11"
"vue": "^3.3.11",
"@tiptap/core": "^3.11.0",
"@tiptap/vue-3": "^3.11.0",
"@tiptap/pm": "^3.11.0",
"@tiptap/extension-document": "^3.11.0",
"@tiptap/extension-paragraph": "^3.11.0",
"@tiptap/extension-text": "^3.11.0",
"@tiptap/extension-history": "^3.11.0",
"@tiptap/extension-placeholder": "^3.11.0",
"@tiptap/extension-character-count": "^3.11.0"
},
"dependencies": {
"@floating-ui/dom": "^1.6.0",
"@opentiny/tiny-robot-svgs": "workspace:*",
"@opentiny/vue": "^3.20.0",
"@vueuse/core": "^13.1.0",
Expand Down
39 changes: 39 additions & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,26 @@ import History from './history'
import IconButton from './icon-button'
import { Prompt, Prompts } from './prompts'
import Sender from './sender'
import SenderCompat from './sender-compat'
import SuggestionPills, { SuggestionPillButton } from './suggestion-pills'
import SuggestionPopover from './suggestion-popover'
import ThemeProvider from './theme-provider'
import Welcome from './welcome'
import McpServerPicker from './mcp-server-picker'
import McpAddForm from './mcp-add-form'
import {
ActionButton,
SubmitButton,
ClearButton,
UploadButton,
VoiceButton,
WordCounter,
DefaultActionButtons,
} from './sender-actions'

// ============================================
// 组件类型导出
// ============================================
export * from './attachments/index.type'
export * from './bubble/index.type'
export * from './container/index.type'
Expand All @@ -30,6 +43,7 @@ export * from './history/index.type'
export * from './icon-button/index.type'
export * from './prompts/index.type'
export * from './sender/index.type'
export * from './sender-actions/index.type'
export * from './suggestion-pills/index.type'
export * from './suggestion-popover/index.type'
export * from './theme-provider/index.type'
Expand All @@ -47,6 +61,7 @@ export {
useOmitMessageFields,
} from './bubble'
export { useTheme } from './theme-provider/useTheme'
export { useSenderContext } from './sender'
export { vDropzone } from './drag-overlay/directives/vDropzone'
export { useAutoScroll, useTouchDevice } from './shared/composables'

Expand All @@ -65,13 +80,21 @@ const components = [
Prompt,
Prompts,
Sender,
SenderCompat,
SuggestionPills,
SuggestionPillButton,
SuggestionPopover,
ThemeProvider,
Welcome,
McpServerPicker,
McpAddForm,
ActionButton,
SubmitButton,
ClearButton,
UploadButton,
VoiceButton,
WordCounter,
DefaultActionButtons,
]

export default {
Expand Down Expand Up @@ -112,6 +135,8 @@ export {
Prompts as TrPrompts,
Sender,
Sender as TrSender,
SenderCompat,
SenderCompat as TrSenderCompat,
SuggestionPillButton,
SuggestionPillButton as TrSuggestionPillButton,
SuggestionPills,
Expand All @@ -126,4 +151,18 @@ export {
McpServerPicker as TrMcpServerPicker,
McpAddForm,
McpAddForm as TrMcpAddForm,
ActionButton,
ActionButton as TrActionButton,
SubmitButton,
SubmitButton as TrSubmitButton,
ClearButton,
ClearButton as TrClearButton,
UploadButton,
UploadButton as TrUploadButton,
VoiceButton,
VoiceButton as TrVoiceButton,
WordCounter,
WordCounter as TrWordCounter,
DefaultActionButtons,
DefaultActionButtons as TrDefaultActionButtons,
}
82 changes: 82 additions & 0 deletions packages/components/src/sender-actions/action-button/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<script setup lang="ts">
import { computed } from 'vue'
import { TinyTooltip } from '@opentiny/vue'
import type { ActionButtonProps } from '../types/common'
import { normalizeTooltipContent } from '../utils/tooltip'

const props = withDefaults(defineProps<ActionButtonProps>(), {
disabled: false,
active: false,
size: 32,
tooltipPlacement: 'top',
})

const tooltipRenderFn = computed(() => normalizeTooltipContent(props.tooltip))

const sizeStyle = computed(() => {
const size = typeof props.size === 'number' ? `${props.size}px` : props.size
return { fontSize: size }
})
</script>

<template>
<tiny-tooltip
v-if="props.tooltip"
:render-content="tooltipRenderFn"
:placement="props.tooltipPlacement"
effect="light"
:visible-arrow="false"
popper-class="tr-action-button-tooltip-popper"
>
<button
:class="['tr-action-button', { active: props.active }]"
:disabled="props.disabled"
@focus.capture="(event: FocusEvent) => event.stopPropagation()"
>
<!-- 优先使用插槽,如果没有插槽则使用 icon prop -->
<slot name="icon">
<component :is="props.icon" :style="sizeStyle" />
</slot>
</button>
</tiny-tooltip>

<!-- 无 tooltip 时直接渲染按钮 -->
<button v-else :class="['tr-action-button', { active: props.active }]" :disabled="props.disabled">
<slot name="icon">
<component :is="props.icon" :style="sizeStyle" />
</slot>
</button>
</template>

<style lang="less" scoped>
.tr-action-button {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 6px;
background: transparent;
cursor: pointer;
padding: 0;
transition: background-color 0.2s;
color: var(--tr-text-secondary);

&:hover:not(:disabled) {
background-color: var(--tr-sender-button-hover-bg, rgba(0, 0, 0, 0.08));
}

&:active:not(:disabled) {
background-color: var(--tr-sender-button-active-bg, rgba(0, 0, 0, 0.12));
}

&.active {
background-color: var(--tr-sender-button-active-bg, rgba(0, 0, 0, 0.12));
color: var(--tr-text-primary);
}

&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
</style>
53 changes: 53 additions & 0 deletions packages/components/src/sender-actions/clear-button/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useSenderContext } from '../../sender/context'
import { IconClear } from '@opentiny/tiny-robot-svgs'
import ActionButton from '../action-button/index.vue'
import { normalizeTooltipContent } from '../utils/tooltip'

// 从 Context 读取状态和配置
const { hasContent, clearable, clear, loading, defaultActions } = useSenderContext()

/**
* 是否禁用
*/
const isDisabled = computed(() => {
if (defaultActions.value?.clear?.disabled !== undefined) {
return defaultActions.value.clear.disabled
}
return false
})

const tooltipRenderFn = computed(() => normalizeTooltipContent(defaultActions.value?.clear?.tooltip))

const tooltipPlacement = computed(() => defaultActions.value?.clear?.tooltipPlacement ?? 'top')

/**
* 显示条件
* - clearable: 允许清空
* - hasContent: 有内容
* - !loading: 非加载中
* - !isDisabled: 非禁用
*/
const show = computed(() => clearable.value && hasContent.value && !loading.value && !isDisabled.value)

/**
* 点击处理
*/
const handleClick = () => {
if (!isDisabled.value) {
clear()
}
}
</script>

<template>
<ActionButton
v-if="show"
:icon="IconClear"
:disabled="isDisabled"
:tooltip="tooltipRenderFn"
:tooltip-placement="tooltipPlacement"
@click="handleClick"
/>
</template>
54 changes: 54 additions & 0 deletions packages/components/src/sender-actions/default-actions/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<script setup lang="ts">
import { useSenderContext } from '../../sender/context'
import ClearButton from '../clear-button/index.vue'
import SubmitButton from '../submit-button/index.vue'

const { hasContent, loading } = useSenderContext()
</script>

<template>
<div class="tr-default-action-buttons">
<Transition name="tr-slide-right">
<div v-if="hasContent || loading" class="tr-action-buttons-group">
<Transition name="tr-slide-right">
<ClearButton />
</Transition>
<div class="tr-submit-wrapper">
<SubmitButton />
</div>
</div>
</Transition>
</div>
</template>

<style lang="less" scoped>
.tr-default-action-buttons {
display: flex;
align-items: center;
gap: 12px;
min-height: var(--tr-sender-button-size-submit);

.tr-action-buttons-group {
display: flex;
align-items: center;
gap: 8px;
}

.tr-submit-wrapper {
display: flex;
align-items: center;
}
}

// 动画样式
.tr-slide-right-enter-active,
.tr-slide-right-leave-active {
transition: all 0.3s cubic-bezier(0.34, 0.69, 0.1, 1);
}

.tr-slide-right-enter-from,
.tr-slide-right-leave-to {
opacity: 0;
transform: translateX(10px);
}
</style>
23 changes: 23 additions & 0 deletions packages/components/src/sender-actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Sender Actions 组件导出
*
* 包含所有操作按钮组件:
* - ActionButton: 基础按钮
* - SubmitButton: 提交按钮
* - ClearButton: 清空按钮
* - UploadButton: 上传按钮
* - VoiceButton: 语音输入按钮
* - WordCounter: 字数统计
* - DefaultActionButtons: 默认按钮组合
*/
export { default as ActionButton } from './action-button/index.vue'
export { default as SubmitButton } from './submit-button/index.vue'
export { default as ClearButton } from './clear-button/index.vue'
export { default as UploadButton } from './upload-button/index.vue'
export { default as VoiceButton } from './voice-button/index.vue'
export { default as WordCounter } from './word-counter/index.vue'
export { default as DefaultActionButtons } from './default-actions/index.vue'

// 导出语音相关 Hook
export { useSpeechHandler } from './voice-button/useSpeechHandler'
export { WebSpeechHandler } from './voice-button/webSpeechHandler'
18 changes: 18 additions & 0 deletions packages/components/src/sender-actions/index.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Sender Actions 类型导出
*/

// 导出共享类型
export type { TooltipPlacement, TooltipContent, ActionButtonProps } from './types/common'

// 导出组件特有类型
export type { UploadButtonProps, UploadButtonEmits } from './upload-button/index.type'
export type { VoiceButtonProps, VoiceButtonEmits } from './voice-button/index.type'
export type {
SpeechConfig,
SpeechHandler,
SpeechState,
SpeechCallbacks,
SpeechHookOptions,
SpeechHandlerResult,
} from './voice-button/speech.types'
Loading
Loading