feat: 先进智能体功能上线
- 基于 agent-runtime 打造,默认 ReAct agent - 支持 agent 能力对接,已对接工作流、插件、知识库等 tool 能力 - 全新 agent 编排界面,支持可视化便捷配置 agent - 全新 agent 聊天界面,支持快捷操作、额外知识库选择等
This commit is contained in:
@@ -0,0 +1,459 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, watch} from 'vue';
|
||||
|
||||
import {ArrowRight, Check, Close, Collection, Plus,} from '@element-plus/icons-vue';
|
||||
import {ElPopover} from 'element-plus';
|
||||
|
||||
interface KnowledgeOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface KnowledgeView {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
disabled?: boolean;
|
||||
extraKnowledgeIds?: string[];
|
||||
knowledgeOptions?: KnowledgeOption[];
|
||||
loading?: boolean;
|
||||
selectedKnowledges?: KnowledgeView[];
|
||||
showTrigger?: boolean;
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
extraKnowledgeIds: () => [],
|
||||
knowledgeOptions: () => [],
|
||||
loading: false,
|
||||
selectedKnowledges: () => [],
|
||||
showTrigger: true,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:extraKnowledgeIds': [value: string[]];
|
||||
}>();
|
||||
|
||||
const MAX_EXTRA_KNOWLEDGE_COUNT = 3;
|
||||
const rootOpen = ref(false);
|
||||
const knowledgeOpen = ref(false);
|
||||
const selectedIdSet = computed(
|
||||
() => new Set(props.extraKnowledgeIds.map(String)),
|
||||
);
|
||||
|
||||
function setKnowledgeOpen(open: boolean) {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
knowledgeOpen.value = open;
|
||||
}
|
||||
|
||||
watch(rootOpen, (open) => {
|
||||
if (!open) {
|
||||
knowledgeOpen.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
function updateKnowledgeIds(value: string[]) {
|
||||
emit('update:extraKnowledgeIds', value);
|
||||
}
|
||||
|
||||
function isSelected(id: string) {
|
||||
return selectedIdSet.value.has(String(id));
|
||||
}
|
||||
|
||||
function isKnowledgeDisabled(id: string) {
|
||||
return (
|
||||
props.disabled ||
|
||||
props.loading ||
|
||||
(props.extraKnowledgeIds.length >= MAX_EXTRA_KNOWLEDGE_COUNT &&
|
||||
!isSelected(id))
|
||||
);
|
||||
}
|
||||
|
||||
function toggleKnowledge(id: string) {
|
||||
const normalizedId = String(id);
|
||||
const nextIds = props.extraKnowledgeIds.map(String);
|
||||
const currentIndex = nextIds.indexOf(normalizedId);
|
||||
if (currentIndex !== -1) {
|
||||
nextIds.splice(currentIndex, 1);
|
||||
updateKnowledgeIds(nextIds);
|
||||
return;
|
||||
}
|
||||
if (nextIds.length >= MAX_EXTRA_KNOWLEDGE_COUNT) {
|
||||
return;
|
||||
}
|
||||
nextIds.push(normalizedId);
|
||||
updateKnowledgeIds(nextIds);
|
||||
}
|
||||
|
||||
function removeKnowledge(id: string) {
|
||||
updateKnowledgeIds(
|
||||
props.extraKnowledgeIds.filter((item) => String(item) !== String(id)),
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="showTrigger || selectedKnowledges.length > 0"
|
||||
class="chat-capability"
|
||||
>
|
||||
<div v-if="selectedKnowledges.length > 0" class="chat-capability__chips">
|
||||
<span
|
||||
v-for="knowledge in selectedKnowledges"
|
||||
:key="knowledge.id"
|
||||
class="chat-capability__chip"
|
||||
>
|
||||
<Collection class="chat-capability__chip-icon" />
|
||||
<span class="chat-capability__chip-label">{{ knowledge.title }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="chat-capability__chip-remove"
|
||||
aria-label="移除知识库"
|
||||
:disabled="disabled"
|
||||
@click="removeKnowledge(knowledge.id)"
|
||||
>
|
||||
<Close />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ElPopover
|
||||
v-if="showTrigger"
|
||||
v-model:visible="rootOpen"
|
||||
placement="top-start"
|
||||
:show-arrow="false"
|
||||
:width="120"
|
||||
:offset="8"
|
||||
popper-class="chat-capability-popper"
|
||||
trigger="click"
|
||||
>
|
||||
<template #reference>
|
||||
<button
|
||||
type="button"
|
||||
class="chat-capability__trigger"
|
||||
:class="{ 'is-open': rootOpen }"
|
||||
:disabled="disabled"
|
||||
aria-label="添加能力"
|
||||
title="添加能力"
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<div class="chat-capability__menu" @mouseleave="setKnowledgeOpen(false)">
|
||||
<button
|
||||
type="button"
|
||||
class="chat-capability__menu-item"
|
||||
:class="{ 'is-open': knowledgeOpen }"
|
||||
:disabled="disabled"
|
||||
@click="setKnowledgeOpen(true)"
|
||||
@focus="setKnowledgeOpen(true)"
|
||||
@mouseenter="setKnowledgeOpen(true)"
|
||||
>
|
||||
<Collection class="chat-capability__menu-icon" />
|
||||
<span>知识库</span>
|
||||
<ArrowRight class="chat-capability__menu-arrow" />
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="knowledgeOpen"
|
||||
class="chat-capability__knowledge-panel"
|
||||
@mouseenter="setKnowledgeOpen(true)"
|
||||
@mouseleave="setKnowledgeOpen(false)"
|
||||
>
|
||||
<div class="chat-capability__knowledge-head">
|
||||
<span>选择知识库</span>
|
||||
<span>最多 3 个</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="knowledgeOptions.length > 0"
|
||||
class="chat-capability__knowledge-list"
|
||||
>
|
||||
<button
|
||||
v-for="item in knowledgeOptions"
|
||||
:key="item.value"
|
||||
type="button"
|
||||
class="chat-capability__knowledge-item"
|
||||
:class="{
|
||||
'is-active': isSelected(item.value),
|
||||
'is-disabled': isKnowledgeDisabled(item.value),
|
||||
}"
|
||||
:disabled="isKnowledgeDisabled(item.value)"
|
||||
@click.stop="toggleKnowledge(item.value)"
|
||||
>
|
||||
<span>{{ item.label }}</span>
|
||||
<Check
|
||||
v-if="isSelected(item.value)"
|
||||
class="chat-capability__knowledge-check"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="chat-capability__empty">
|
||||
{{ loading ? '加载中' : '暂无可选知识库' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElPopover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chat-capability {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chat-capability__chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chat-capability__chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 220px;
|
||||
min-height: 28px;
|
||||
gap: 6px;
|
||||
padding: 0 8px;
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
border: 1px solid var(--el-color-primary-light-7);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.chat-capability__chip-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.chat-capability__chip-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chat-capability__chip-remove,
|
||||
.chat-capability__trigger,
|
||||
.chat-capability__menu-item,
|
||||
.chat-capability__knowledge-item {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.chat-capability__chip-remove {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.chat-capability__chip-remove:hover:not(:disabled),
|
||||
.chat-capability__chip-remove:focus-visible {
|
||||
background: var(--el-color-primary-light-8);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.chat-capability__chip-remove :deep(.el-icon),
|
||||
.chat-capability__chip-remove svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.chat-capability__trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
transition:
|
||||
background-color 0.16s ease,
|
||||
color 0.16s ease;
|
||||
}
|
||||
|
||||
.chat-capability__trigger:hover:not(:disabled),
|
||||
.chat-capability__trigger:focus-visible,
|
||||
.chat-capability__trigger.is-open {
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-fill-color-light);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.chat-capability__trigger:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.chat-capability__trigger svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.chat-capability__menu,
|
||||
.chat-capability__knowledge-panel {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.chat-capability__menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chat-capability__menu-item {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
padding: 0 8px;
|
||||
color: var(--el-text-color-primary);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.chat-capability__menu-item:hover:not(:disabled),
|
||||
.chat-capability__menu-item:focus-visible,
|
||||
.chat-capability__menu-item.is-open {
|
||||
background: var(--el-fill-color-light);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.chat-capability__menu-item:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.chat-capability__menu-icon,
|
||||
.chat-capability__menu-arrow {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.chat-capability__knowledge-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 32px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.chat-capability__knowledge-panel {
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: calc(100% + 4px);
|
||||
width: 296px;
|
||||
background: var(--el-bg-color-overlay);
|
||||
border-radius: 14px;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
box-sizing: border-box;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.chat-capability__knowledge-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
max-height: 260px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.chat-capability__knowledge-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
gap: 12px;
|
||||
padding: 0 10px;
|
||||
color: var(--el-text-color-primary);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.chat-capability__knowledge-item span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chat-capability__knowledge-item:hover:not(:disabled),
|
||||
.chat-capability__knowledge-item:focus-visible,
|
||||
.chat-capability__knowledge-item.is-active {
|
||||
background: var(--el-fill-color-light);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.chat-capability__knowledge-item.is-active {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.chat-capability__knowledge-item:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.chat-capability__knowledge-check {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.chat-capability__empty {
|
||||
padding: 16px 10px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
:global(.chat-capability-popper) {
|
||||
width: 120px !important;
|
||||
min-width: 120px !important;
|
||||
overflow: visible !important;
|
||||
padding: 4px !important;
|
||||
border-radius: 12px !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:global(.chat-capability-popper .chat-capability__menu) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:global(.chat-capability-popper.el-popper) {
|
||||
transition:
|
||||
opacity 0.08s ease,
|
||||
transform 0.08s ease !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,138 @@
|
||||
<script setup lang="ts">
|
||||
import type {ChatInputTriggerItem} from './input-triggers/types';
|
||||
|
||||
import {Collection} from '@element-plus/icons-vue';
|
||||
|
||||
defineProps<{
|
||||
activeIndex?: number;
|
||||
groupLabel?: string;
|
||||
items?: ChatInputTriggerItem[];
|
||||
keyword?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [item: ChatInputTriggerItem, index: number];
|
||||
setActive: [index: number];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chat-input-trigger-panel">
|
||||
<div v-if="groupLabel" class="chat-input-trigger-panel__head">
|
||||
{{ groupLabel }}
|
||||
</div>
|
||||
<div
|
||||
v-if="items && items.length > 0"
|
||||
class="chat-input-trigger-panel__list"
|
||||
>
|
||||
<button
|
||||
v-for="(item, index) in items"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="chat-input-trigger-panel__item"
|
||||
:class="{ 'is-active': index === activeIndex }"
|
||||
:disabled="item.disabled"
|
||||
@mouseenter="emit('setActive', index)"
|
||||
@mousedown.prevent="emit('select', item, index)"
|
||||
>
|
||||
<component
|
||||
:is="item.icon || Collection"
|
||||
class="chat-input-trigger-panel__icon"
|
||||
/>
|
||||
<span class="chat-input-trigger-panel__content">
|
||||
<span class="chat-input-trigger-panel__label">{{ item.label }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="chat-input-trigger-panel__empty">
|
||||
{{ keyword ? '没有匹配项' : '暂无可选项' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chat-input-trigger-panel {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
background: var(--el-fill-color-extra-light);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 18px;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.chat-input-trigger-panel__head {
|
||||
min-height: 32px;
|
||||
padding: 0 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 32px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.chat-input-trigger-panel__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.chat-input-trigger-panel__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
gap: 10px;
|
||||
padding: 0 10px;
|
||||
color: var(--el-text-color-primary);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.chat-input-trigger-panel__item:hover:not(:disabled),
|
||||
.chat-input-trigger-panel__item.is-active {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
.chat-input-trigger-panel__item:focus-visible {
|
||||
outline: 2px solid var(--el-color-primary-light-5);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.chat-input-trigger-panel__item:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.chat-input-trigger-panel__icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex: none;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.chat-input-trigger-panel__content {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.chat-input-trigger-panel__label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chat-input-trigger-panel__empty {
|
||||
padding: 18px 10px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
import type {Component} from 'vue';
|
||||
|
||||
export type ChatInputTriggerSymbol = '#' | '$' | '/' | '@';
|
||||
|
||||
export interface ChatInputTriggerItem {
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
icon?: Component;
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface ChatInputTriggerGroup {
|
||||
items: ChatInputTriggerItem[];
|
||||
label: string;
|
||||
symbol: ChatInputTriggerSymbol;
|
||||
}
|
||||
|
||||
export interface ChatInputTriggerMatch {
|
||||
keyword: string;
|
||||
start: number;
|
||||
symbol: ChatInputTriggerSymbol;
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import type {Ref} from 'vue';
|
||||
import {computed, nextTick, ref} from 'vue';
|
||||
|
||||
import type {ChatInputTriggerGroup, ChatInputTriggerMatch, ChatInputTriggerSymbol,} from './types';
|
||||
|
||||
const TRIGGER_SYMBOLS = new Set<ChatInputTriggerSymbol>(['#', '$', '/', '@']);
|
||||
const TRIGGER_BOUNDARY = /[\s,。!?;:,.!?;:()[\]{}]/;
|
||||
|
||||
export interface ChatInputTriggerOptions {
|
||||
disabled: Ref<boolean>;
|
||||
groups: Ref<ChatInputTriggerGroup[]>;
|
||||
inputRef: Ref<undefined | unknown>;
|
||||
text: Ref<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理聊天输入框快捷触发状态。
|
||||
*
|
||||
* @param options 触发器依赖
|
||||
* @return 输入触发状态与操作方法
|
||||
*/
|
||||
export function useChatInputTrigger(options: ChatInputTriggerOptions) {
|
||||
const activeMatch = ref<ChatInputTriggerMatch>();
|
||||
const activeIndex = ref(0);
|
||||
|
||||
const activeGroup = computed(() => {
|
||||
if (!activeMatch.value) {
|
||||
return undefined;
|
||||
}
|
||||
return options.groups.value.find(
|
||||
(group) => group.symbol === activeMatch.value?.symbol,
|
||||
);
|
||||
});
|
||||
|
||||
const visibleItems = computed(() => {
|
||||
const group = activeGroup.value;
|
||||
if (!group) {
|
||||
return [];
|
||||
}
|
||||
const keyword = activeMatch.value?.keyword.trim().toLowerCase() || '';
|
||||
if (!keyword) {
|
||||
return group.items;
|
||||
}
|
||||
return group.items.filter((item) => {
|
||||
const label = item.label.toLowerCase();
|
||||
const description = item.description?.toLowerCase() || '';
|
||||
return label.includes(keyword) || description.includes(keyword);
|
||||
});
|
||||
});
|
||||
|
||||
const activePanel = computed(() => {
|
||||
const group = activeGroup.value;
|
||||
if (!activeMatch.value || !group) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
groupLabel: group.label,
|
||||
keyword: activeMatch.value.keyword,
|
||||
symbol: activeMatch.value.symbol,
|
||||
};
|
||||
});
|
||||
|
||||
function getTextareaElement() {
|
||||
const rawRef = options.inputRef.value as
|
||||
| undefined
|
||||
| { textarea?: HTMLTextAreaElement };
|
||||
return rawRef?.textarea;
|
||||
}
|
||||
|
||||
function close() {
|
||||
activeMatch.value = undefined;
|
||||
activeIndex.value = 0;
|
||||
}
|
||||
|
||||
function sync() {
|
||||
if (options.disabled.value) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
const textarea = getTextareaElement();
|
||||
const cursor = textarea?.selectionStart ?? options.text.value.length;
|
||||
const match = findTriggerMatch(options.text.value, cursor);
|
||||
if (!match) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
activeMatch.value = match;
|
||||
activeIndex.value = Math.min(
|
||||
activeIndex.value,
|
||||
Math.max(visibleItems.value.length - 1, 0),
|
||||
);
|
||||
}
|
||||
|
||||
async function replaceTriggerText(replacement = '') {
|
||||
const match = activeMatch.value;
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
const textarea = getTextareaElement();
|
||||
const cursor = textarea?.selectionStart ?? options.text.value.length;
|
||||
const before = options.text.value.slice(0, match.start);
|
||||
const after = options.text.value.slice(cursor);
|
||||
const nextText = `${before}${replacement}${after}`;
|
||||
const nextCursor = before.length + replacement.length;
|
||||
options.text.value = nextText;
|
||||
close();
|
||||
await nextTick();
|
||||
textarea?.focus();
|
||||
textarea?.setSelectionRange(nextCursor, nextCursor);
|
||||
}
|
||||
|
||||
function move(delta: number) {
|
||||
const total = visibleItems.value.length;
|
||||
if (!activeMatch.value || total === 0) {
|
||||
return;
|
||||
}
|
||||
activeIndex.value = (activeIndex.value + delta + total) % total;
|
||||
}
|
||||
|
||||
function setActiveIndex(index: number) {
|
||||
if (index < 0 || index >= visibleItems.value.length) {
|
||||
return;
|
||||
}
|
||||
activeIndex.value = index;
|
||||
}
|
||||
|
||||
function findTriggerMatch(
|
||||
text: string,
|
||||
cursor: number,
|
||||
): ChatInputTriggerMatch | undefined {
|
||||
const beforeCursor = text.slice(0, cursor);
|
||||
for (let index = beforeCursor.length - 1; index >= 0; index--) {
|
||||
const char = beforeCursor[index];
|
||||
if (!char) {
|
||||
continue;
|
||||
}
|
||||
if (TRIGGER_BOUNDARY.test(char)) {
|
||||
return undefined;
|
||||
}
|
||||
if (!TRIGGER_SYMBOLS.has(char as ChatInputTriggerSymbol)) {
|
||||
continue;
|
||||
}
|
||||
const previous = index === 0 ? '' : beforeCursor[index - 1];
|
||||
if (previous && !TRIGGER_BOUNDARY.test(previous)) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
keyword: beforeCursor.slice(index + 1),
|
||||
start: index,
|
||||
symbol: char as ChatInputTriggerSymbol,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
activeIndex,
|
||||
activePanel,
|
||||
close,
|
||||
move,
|
||||
replaceTriggerText,
|
||||
setActiveIndex,
|
||||
sync,
|
||||
visibleItems,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user