Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<script setup lang="ts">
import type { SenderProps, SenderRef } from "@antdv-next/x";

import { App } from "antdv-next";
import { ref } from "vue";

const slotConfig: SenderProps["slotConfig"] = [
{ type: "text", value: 'Translate "' },
{
type: "content",
key: "text",
props: { defaultValue: "Hello World", placeholder: "Enter text" },
formatResult: (value: any) => `[${value}]`,
},
{ type: "text", value: '" from ' },
{
type: "select",
key: "sourceLang",
props: {
defaultValue: "English",
options: ["English", "Chinese", "Japanese"],
placeholder: "Select language",
},
formatResult: (value: any) => `{${value}}`,
},
{ type: "text", value: " to " },
{
type: "select",
key: "targetLang",
props: {
defaultValue: "Chinese",
options: ["English", "Chinese", "Japanese"],
placeholder: "Select language",
},
formatResult: (value: any) => `{${value}}`,
},
{ type: "text", value: "." },
];

const { message } = App.useApp();
const senderRef = ref<SenderRef>();
const value = ref("");

const getValue = () => {
value.value = senderRef.value?.getValue().value ?? "";
};

const onSubmit: SenderProps["onSubmit"] = nextValue => {
value.value = nextValue;
message.success(`Sent: ${nextValue}`);
};
</script>

<template>
<a-flex vertical :gap="16">
<a-button style="width: fit-content" @click="getValue">
Get Value
</a-button>
<ax-sender
ref="senderRef"
:auto-size="{ minRows: 2, maxRows: 4 }"
placeholder="Enter to send"
:slot-config="slotConfig"
:on-submit="onSubmit"
/>
<div>
<strong>getValue() result:</strong>
{{ value || '(click "Get Value" to see formatted result)' }}
</div>
</a-flex>
</template>

<docs lang="zh-CN">
通过 `formatResult` 格式化所有词槽类型的最终输出,包括可编辑的 `content` 词槽。
</docs>

<docs lang="en-US">
Use `formatResult` to format the final output of every slot type, including editable `content` slots.
</docs>
11 changes: 6 additions & 5 deletions packages/docs/src/pages/components/sender/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ description: A chat input component for sending messages.
<demo src="./demo/basic.vue">Basic Usage</demo>
<demo src="./demo/switch.vue">Feature Toggle</demo>
<demo src="./demo/slot-filling.vue">Slot Mode</demo>
<demo src="./demo/slot-format-result.vue">Slot Format Result</demo>
<demo src="./demo/ref-action.vue">Instance Methods</demo>
<demo src="./demo/submit-type.vue">Submit Methods</demo>
<demo src="./demo/speech.vue">Voice Input</demo>
Expand Down Expand Up @@ -144,11 +145,11 @@ interface SenderFocusOptions extends FocusOptions {

#### SlotConfigType

| Property | Description | Type |
| ------------ | ------------------------------------------------------------ | ----------------------------------------------------------------- |
| type | Node type, determines the rendering component type, required | `'text' \| 'input' \| 'select' \| 'tag' \| 'content' \| 'custom'` |
| key | Unique identifier, can be omitted when type is `text` | `string` |
| formatResult | Format the final result | `(value: any) => string` |
| Property | Description | Type |
| ------------ | ----------------------------------------------------------------------- | ----------------------------------------------------------------- |
| type | Node type, determines the rendering component type, required | `'text' \| 'input' \| 'select' \| 'tag' \| 'content' \| 'custom'` |
| key | Unique identifier, can be omitted when type is `text` | `string` |
| formatResult | Format the final result; applies to all slot types, including `content` | `(value: any) => string` |

##### text node properties

Expand Down
11 changes: 6 additions & 5 deletions packages/docs/src/pages/components/sender/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ description: 用于聊天的输入框组件。
<demo src="./demo/basic.vue">基本用法</demo>
<demo src="./demo/switch.vue">功能开关</demo>
<demo src="./demo/slot-filling.vue">词槽模式</demo>
<demo src="./demo/slot-format-result.vue">词槽格式化</demo>
<demo src="./demo/ref-action.vue">实例方法</demo>
<demo src="./demo/submit-type.vue">提交方式</demo>
<demo src="./demo/speech.vue">语音输入</demo>
Expand Down Expand Up @@ -144,11 +145,11 @@ interface SenderFocusOptions extends FocusOptions {

#### SlotConfigType

| 属性 | 说明 | 类型 |
| ------------ | --------------------------------- | ----------------------------------------------------------------- |
| type | 节点类型,决定渲染组件类型,必填 | `'text' \| 'input' \| 'select' \| 'tag' \| 'content' \| 'custom'` |
| key | 唯一标识,type 为 `text` 时可省略 | `string` |
| formatResult | 格式化最终结果 | `(value: any) => string` |
| 属性 | 说明 | 类型 |
| ------------ | -------------------------------------------------- | ----------------------------------------------------------------- |
| type | 节点类型,决定渲染组件类型,必填 | `'text' \| 'input' \| 'select' \| 'tag' \| 'content' \| 'custom'` |
| key | 唯一标识,type 为 `text` 时可省略 | `string` |
| formatResult | 格式化最终结果,对所有词槽类型(含 `content`)生效 | `(value: any) => string` |

##### text 节点属性

Expand Down
95 changes: 95 additions & 0 deletions packages/x/components/sender/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,101 @@ describe("Sender", () => {
);
});

it("should format content slots using the latest DOM text", async () => {
const formatResult = vi.fn((value: any) => `[formatted:${value}]`);
const wrapper = mount(Sender, {
props: {
slotConfig: [
{
type: "content",
key: "content",
props: { defaultValue: "Initial Value" },
formatResult,
},
],
},
});
await wrapper.vm.$nextTick();

const contentSlot = wrapper.find<HTMLElement>(".antd-sender-slot-content");
contentSlot.element.innerText = "Edited Value";
await wrapper.find(".antd-sender-input-slot").trigger("input");

expect(formatResult).toHaveBeenLastCalledWith("Edited Value");
expect((wrapper.vm as any).getValue()).toEqual(
expect.objectContaining({
value: " [formatted:Edited Value] ",
slotConfig: [
expect.objectContaining({
key: "content",
type: "content",
value: "[formatted:Edited Value]",
}),
],
}),
);
});

it("should return raw content slot text without formatResult", async () => {
const wrapper = mount(Sender, {
props: {
slotConfig: [
{
type: "content",
key: "content",
props: { defaultValue: "Content Value" },
},
],
},
});
await wrapper.vm.$nextTick();

expect((wrapper.vm as any).getValue().value).toBe(" Content Value ");
expect((wrapper.vm as any).getValue().slotConfig).toEqual([
expect.objectContaining({
key: "content",
type: "content",
value: "Content Value",
}),
]);
});

it("should format content and other slot types together", async () => {
const wrapper = mount(Sender, {
props: {
slotConfig: [
{ type: "text", value: 'Translate "' },
{
type: "content",
key: "content",
props: { defaultValue: "Hello" },
formatResult: (value: any) => `[${value}]`,
},
{ type: "text", value: '" from ' },
{
type: "select",
key: "language",
props: {
defaultValue: "English",
options: ["English", "Chinese"],
},
formatResult: (value: any) => `{${value}}`,
},
],
},
});
await wrapper.vm.$nextTick();

const result = (wrapper.vm as any).getValue();
expect(result.value).toBe('Translate " [Hello] " from {English}');
expect(result.slotConfig).toEqual(
expect.arrayContaining([
expect.objectContaining({ type: "content", value: "[Hello]" }),
expect.objectContaining({ type: "select", value: "{English}" }),
]),
);
});

it("should support slot insert and getValue on ref", async () => {
const wrapper = mount(Sender, {
props: {
Expand Down
7 changes: 5 additions & 2 deletions packages/x/components/sender/components/SlotTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,11 +211,14 @@ export default defineComponent({
}

const config = nodeInfo.slotConfig;
if (!config || config.type === "content") {
if (!config) {
return element.innerText || "";
}

const rawValue = slotValues.value[nodeInfo.slotKey];
const rawValue =
config.type === "content"
? element.innerText || ""
: slotValues.value[nodeInfo.slotKey];
const formatted = config.formatResult?.(rawValue);
return formatted ?? stringifyValue(rawValue);
}
Expand Down
Loading