feat(chat): improve chat layout and suggested questions UX
Refine input field, session sidebar, suggested questions refresh, and embed channel preview for a cleaner chat creation experience.
This commit is contained in:
@@ -1,12 +1,6 @@
|
||||
<template>
|
||||
<t-drawer
|
||||
:visible="visible"
|
||||
:header="title || $t('embedPublish.preview')"
|
||||
size="720px"
|
||||
:footer="false"
|
||||
class="embed-preview-drawer"
|
||||
@close="emit('update:visible', false)"
|
||||
>
|
||||
<t-drawer :visible="visible" :header="title || $t('embedPublish.preview')" size="720px" :footer="false"
|
||||
:z-index="2600" class="embed-preview-drawer" @close="emit('update:visible', false)">
|
||||
<div class="preview-body">
|
||||
<template v-if="mode === 'iframe'">
|
||||
<p class="preview-hint">{{ $t('embedPublish.previewIframeHint') }}</p>
|
||||
@@ -19,13 +13,7 @@
|
||||
</div>
|
||||
<div class="device-frame__screen">
|
||||
<t-loading v-if="!iframeSrc || !iframeReady" size="small" :text="$t('embedPublish.previewLoading')" />
|
||||
<iframe
|
||||
v-else
|
||||
:key="iframeSrc"
|
||||
:src="iframeSrc"
|
||||
class="preview-iframe"
|
||||
allow="clipboard-write"
|
||||
/>
|
||||
<iframe v-else :key="iframeSrc" :src="iframeSrc" class="preview-iframe" allow="clipboard-write" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -38,25 +26,15 @@
|
||||
<div class="widget-mock-page__line" />
|
||||
<div class="widget-mock-page__line widget-mock-page__line--short" />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="widget-launcher"
|
||||
:style="{ background: primaryColor || 'var(--td-brand-color)' }"
|
||||
<button type="button" class="widget-launcher" :style="{ background: primaryColor || 'var(--td-brand-color)' }"
|
||||
:aria-label="widgetOpen ? $t('common.close') : $t('embedPublish.preview')"
|
||||
@click="widgetOpen = !widgetOpen"
|
||||
>
|
||||
@click="widgetOpen = !widgetOpen">
|
||||
<t-icon :name="widgetOpen ? 'close' : 'chat'" />
|
||||
</button>
|
||||
<transition name="widget-panel">
|
||||
<div v-show="widgetOpen" class="widget-panel">
|
||||
<iframe
|
||||
v-if="iframeSrc && iframeReady"
|
||||
:key="`widget-${iframeSrc}`"
|
||||
:src="iframeSrc"
|
||||
class="preview-iframe"
|
||||
:title="title || $t('embedPublish.preview')"
|
||||
allow="clipboard-write"
|
||||
/>
|
||||
<iframe v-if="iframeSrc && iframeReady" :key="`widget-${iframeSrc}`" :src="iframeSrc"
|
||||
class="preview-iframe" :title="title || $t('embedPublish.preview')" allow="clipboard-write" />
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@@ -172,9 +150,17 @@ watch(() => props.visible, async (open) => {
|
||||
border-radius: 50%;
|
||||
background: var(--td-component-stroke);
|
||||
|
||||
&:nth-child(1) { background: #ff5f57; }
|
||||
&:nth-child(2) { background: #febc2e; }
|
||||
&:nth-child(3) { background: #28c840; }
|
||||
&:nth-child(1) {
|
||||
background: #ff5f57;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
background: #febc2e;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
background: #28c840;
|
||||
}
|
||||
}
|
||||
|
||||
&__url {
|
||||
@@ -289,23 +275,51 @@ watch(() => props.visible, async (open) => {
|
||||
}
|
||||
|
||||
.pos-bottom-right {
|
||||
.widget-launcher { right: 20px; bottom: 20px; }
|
||||
.widget-panel { right: 20px; bottom: 80px; }
|
||||
.widget-launcher {
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
.widget-panel {
|
||||
right: 20px;
|
||||
bottom: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.pos-bottom-left {
|
||||
.widget-launcher { left: 20px; bottom: 20px; }
|
||||
.widget-panel { left: 20px; bottom: 80px; }
|
||||
.widget-launcher {
|
||||
left: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
.widget-panel {
|
||||
left: 20px;
|
||||
bottom: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.pos-top-right {
|
||||
.widget-launcher { right: 20px; top: 20px; }
|
||||
.widget-panel { right: 20px; top: 80px; }
|
||||
.widget-launcher {
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.widget-panel {
|
||||
right: 20px;
|
||||
top: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.pos-top-left {
|
||||
.widget-launcher { left: 20px; top: 20px; }
|
||||
.widget-panel { left: 20px; top: 80px; }
|
||||
.widget-launcher {
|
||||
left: 20px;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.widget-panel {
|
||||
left: 20px;
|
||||
top: 80px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -2152,195 +2152,199 @@ defineExpose({
|
||||
|
||||
<!-- 控制栏(放在 rich-input-container 内,相对输入框边框定位) -->
|
||||
<div class="control-bar" :class="{ 'is-embedded': embeddedMode }">
|
||||
<!-- 左侧控制按钮 -->
|
||||
<div class="control-left" v-if="!embeddedMode">
|
||||
<!-- Agent 模式切换按钮 -->
|
||||
<div ref="agentModeButtonRef" class="control-btn agent-mode-btn" :class="{
|
||||
'is-normal': !isCustomAgent && !isAgentEnabled,
|
||||
'is-agent': !isCustomAgent && isAgentEnabled,
|
||||
'is-custom': isCustomAgent
|
||||
}" @click.stop="toggleAgentModeSelector">
|
||||
<span class="agent-mode-text">
|
||||
{{ selectedAgent.name || (isAgentEnabled ? $t('input.agentMode') : $t('input.normalMode')) }}
|
||||
</span>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor" class="dropdown-arrow"
|
||||
:class="{ 'rotate': showAgentModeSelector }">
|
||||
<path d="M2.5 4.5L6 8L9.5 4.5H2.5Z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Agent 选择器下拉菜单 -->
|
||||
<AgentSelector :visible="showAgentModeSelector" :anchorEl="agentModeButtonRef" :currentAgentId="selectedAgentId"
|
||||
:agents="enabledAgents" @close="closeAgentModeSelector" @select="handleSelectAgent" />
|
||||
|
||||
<!-- WebSearch 开关按钮 -->
|
||||
<t-tooltip placement="top" theme="light" :popupProps="{ overlayClassName: 'input-field-tooltip' }">
|
||||
<template #content>
|
||||
<div v-if="isWebSearchDisabledByAgent" class="tooltip-with-link">
|
||||
<span>{{ $t('input.webSearchDisabledByAgent') }}</span>
|
||||
<a href="#" @click.prevent="handleGoToAgentSettings('websearch')">{{ $t('input.goToAgentSettings') }}</a>
|
||||
</div>
|
||||
<span v-else-if="isWebSearchConfigured">{{ isWebSearchEnabled ? $t('input.webSearch.toggleOff') :
|
||||
$t('input.webSearch.toggleOn') }}</span>
|
||||
<div v-else class="tooltip-with-link">
|
||||
<span>{{ $t('input.webSearch.notConfigured') }}</span>
|
||||
<a href="#" @click.prevent="handleGoToWebSearchSettings">{{ $t('input.goToSettings') }}</a>
|
||||
</div>
|
||||
</template>
|
||||
<div class="control-btn websearch-btn" :class="{
|
||||
'active': isWebSearchEnabled && isWebSearchConfigured,
|
||||
'disabled': !isWebSearchConfigured || isWebSearchDisabledByAgent
|
||||
}" @click.stop="toggleWebSearch">
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
class="control-icon websearch-icon" :class="{ 'active': isWebSearchEnabled && isWebSearchConfigured }">
|
||||
<circle cx="9" cy="9" r="7" stroke="currentColor" stroke-width="1.2" fill="none" />
|
||||
<path d="M 9 2 A 3.5 7 0 0 0 9 16" stroke="currentColor" stroke-width="1.2" fill="none" />
|
||||
<path d="M 9 2 A 3.5 7 0 0 1 9 16" stroke="currentColor" stroke-width="1.2" fill="none" />
|
||||
<line x1="2.94" y1="5.5" x2="15.06" y2="5.5" stroke="currentColor" stroke-width="1.2"
|
||||
stroke-linecap="round" />
|
||||
<line x1="2.94" y1="12.5" x2="15.06" y2="12.5" stroke="currentColor" stroke-width="1.2"
|
||||
stroke-linecap="round" />
|
||||
<!-- 左侧控制按钮 -->
|
||||
<div class="control-left" v-if="!embeddedMode">
|
||||
<!-- Agent 模式切换按钮 -->
|
||||
<div ref="agentModeButtonRef" class="control-btn agent-mode-btn" :class="{
|
||||
'is-normal': !isCustomAgent && !isAgentEnabled,
|
||||
'is-agent': !isCustomAgent && isAgentEnabled,
|
||||
'is-custom': isCustomAgent
|
||||
}" @click.stop="toggleAgentModeSelector">
|
||||
<span class="agent-mode-text">
|
||||
{{ selectedAgent.name || (isAgentEnabled ? $t('input.agentMode') : $t('input.normalMode')) }}
|
||||
</span>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor" class="dropdown-arrow"
|
||||
:class="{ 'rotate': showAgentModeSelector }">
|
||||
<path d="M2.5 4.5L6 8L9.5 4.5H2.5Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</t-tooltip>
|
||||
|
||||
<!-- 图片上传按钮 -->
|
||||
<t-tooltip placement="top" theme="light" :popupProps="{ overlayClassName: 'input-field-tooltip' }">
|
||||
<template #content>
|
||||
<div v-if="!isImageUploadEnabledByAgent" class="tooltip-with-link">
|
||||
<span>{{ $t('input.imageUploadDisabledByAgent') }}</span>
|
||||
<a href="#" @click.prevent="handleGoToAgentSettings('model')">{{ $t('input.goToAgentSettings') }}</a>
|
||||
</div>
|
||||
<span v-else>{{ $t('chat.imageUploadTooltip') }}</span>
|
||||
</template>
|
||||
<div class="control-btn image-upload-btn" :class="{
|
||||
'active': uploadedImages.length > 0,
|
||||
'disabled': !isImageUploadEnabledByAgent
|
||||
}" @click.stop="isImageUploadEnabledByAgent && triggerImageUpload()">
|
||||
<svg width="18" height="18" viewBox="0 0 1024 1024" fill="currentColor" class="control-icon">
|
||||
<path
|
||||
d="M896 128H128c-35.3 0-64 28.7-64 64v640c0 35.3 28.7 64 64 64h768c35.3 0 64-28.7 64-64V192c0-35.3-28.7-64-64-64zM128 832V192h768l0.1 640H128z" />
|
||||
<path d="M352 448a96 96 0 1 0 0-192 96 96 0 0 0 0 192z" />
|
||||
<path d="M128 768l224-288 160 160 192-256L896 640v128H128z" />
|
||||
</svg>
|
||||
<span v-if="uploadedImages.length > 0" class="image-count">{{ uploadedImages.length }}</span>
|
||||
</div>
|
||||
</t-tooltip>
|
||||
<!-- Agent 选择器下拉菜单 -->
|
||||
<AgentSelector :visible="showAgentModeSelector" :anchorEl="agentModeButtonRef"
|
||||
:currentAgentId="selectedAgentId" :agents="enabledAgents" @close="closeAgentModeSelector"
|
||||
@select="handleSelectAgent" />
|
||||
|
||||
<!-- 附件上传按钮 -->
|
||||
<t-tooltip placement="top" theme="light" :popupProps="{ overlayClassName: 'input-field-tooltip' }">
|
||||
<template #content>
|
||||
<span>{{ uploadedAttachments.length > 0 ? $t('chat.attachmentWithCount', {
|
||||
count: uploadedAttachments.length
|
||||
}) : $t('chat.attachmentUploadTooltip') }}</span>
|
||||
</template>
|
||||
<div class="control-btn attachment-upload-btn" :class="{ 'active': uploadedAttachments.length > 0 }"
|
||||
@click.stop="attachmentUploadRef?.triggerFileSelect()">
|
||||
<!-- 回形针图标 -->
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" stroke-linejoin="round" class="control-icon">
|
||||
<path
|
||||
d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
|
||||
</svg>
|
||||
<span v-if="uploadedAttachments.length > 0" class="attachment-count">{{ uploadedAttachments.length }}</span>
|
||||
</div>
|
||||
</t-tooltip>
|
||||
|
||||
<!-- @ 知识库/文件选择按钮 -->
|
||||
<t-tooltip placement="top" theme="light" :popupProps="{ overlayClassName: 'input-field-tooltip' }">
|
||||
<template #content>
|
||||
<div v-if="isKnowledgeBaseDisabledByAgent" class="tooltip-with-link">
|
||||
<span>{{ $t('input.kbDisabledByAgent') }}</span>
|
||||
<a href="#" @click.prevent="handleGoToAgentSettings('knowledge')">{{ $t('input.goToAgentSettings') }}</a>
|
||||
</div>
|
||||
<span v-else>{{ allSelectedItems.length > 0 ? $t('input.knowledgeBaseWithCount', {
|
||||
count:
|
||||
allSelectedItems.length
|
||||
}) : $t('input.knowledgeBase') }}</span>
|
||||
</template>
|
||||
<div ref="atButtonRef" class="control-btn kb-btn" data-guide="chat-kb-mention" :class="{
|
||||
'active': allSelectedItems.length > 0,
|
||||
'disabled': isKnowledgeBaseDisabledByAgent
|
||||
}" @click.stop @mousedown.prevent="triggerMention">
|
||||
<svg width="18" height="18" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
class="control-icon at-icon">
|
||||
<circle cx="10" cy="10" r="3.5" stroke="currentColor" stroke-width="1.8" />
|
||||
<path
|
||||
d="M13.5 10V11.5C13.5 12.163 13.7634 12.7989 14.2322 13.2678C14.7011 13.7366 15.337 14 16 14C16.663 14 17.2989 13.7366 17.7678 13.2678C18.2366 12.7989 18.5 12.163 18.5 11.5V10C18.5 7.74566 17.6045 5.58365 16.0104 3.98959C14.4163 2.39553 12.2543 1.5 10 1.5C7.74566 1.5 5.58365 2.39553 3.98959 3.98959C2.39553 5.58365 1.5 7.74566 1.5 10C1.5 12.2543 2.39553 14.4163 3.98959 16.0104C5.58365 17.6045 7.74566 18.5 10 18.5H12"
|
||||
stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<span v-if="allSelectedItems.length > 0" class="kb-count">{{ allSelectedItems.length }}</span>
|
||||
</div>
|
||||
</t-tooltip>
|
||||
|
||||
<!-- 模型显示 -->
|
||||
<t-tooltip :content="isModelLockedByAgent ? $t('input.modelLockedByAgent') : ''"
|
||||
:disabled="!isModelLockedByAgent">
|
||||
<div class="model-display" :class="{ 'agent-controlled': isModelLockedByAgent }">
|
||||
<div ref="modelButtonRef" class="model-selector-trigger" @click.stop="toggleModelSelector">
|
||||
<span class="model-selector-name">
|
||||
{{ selectedModelDisplayName }}
|
||||
</span>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor" class="model-dropdown-arrow"
|
||||
:class="{ 'rotate': showModelSelector }">
|
||||
<path d="M2.5 4.5L6 8L9.5 4.5H2.5Z" />
|
||||
<!-- WebSearch 开关按钮 -->
|
||||
<t-tooltip placement="top" theme="light" :popupProps="{ overlayClassName: 'input-field-tooltip' }">
|
||||
<template #content>
|
||||
<div v-if="isWebSearchDisabledByAgent" class="tooltip-with-link">
|
||||
<span>{{ $t('input.webSearchDisabledByAgent') }}</span>
|
||||
<a href="#" @click.prevent="handleGoToAgentSettings('websearch')">{{ $t('input.goToAgentSettings')
|
||||
}}</a>
|
||||
</div>
|
||||
<span v-else-if="isWebSearchConfigured">{{ isWebSearchEnabled ? $t('input.webSearch.toggleOff') :
|
||||
$t('input.webSearch.toggleOn') }}</span>
|
||||
<div v-else class="tooltip-with-link">
|
||||
<span>{{ $t('input.webSearch.notConfigured') }}</span>
|
||||
<a href="#" @click.prevent="handleGoToWebSearchSettings">{{ $t('input.goToSettings') }}</a>
|
||||
</div>
|
||||
</template>
|
||||
<div class="control-btn websearch-btn" :class="{
|
||||
'active': isWebSearchEnabled && isWebSearchConfigured,
|
||||
'disabled': !isWebSearchConfigured || isWebSearchDisabledByAgent
|
||||
}" @click.stop="toggleWebSearch">
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
class="control-icon websearch-icon" :class="{ 'active': isWebSearchEnabled && isWebSearchConfigured }">
|
||||
<circle cx="9" cy="9" r="7" stroke="currentColor" stroke-width="1.2" fill="none" />
|
||||
<path d="M 9 2 A 3.5 7 0 0 0 9 16" stroke="currentColor" stroke-width="1.2" fill="none" />
|
||||
<path d="M 9 2 A 3.5 7 0 0 1 9 16" stroke="currentColor" stroke-width="1.2" fill="none" />
|
||||
<line x1="2.94" y1="5.5" x2="15.06" y2="5.5" stroke="currentColor" stroke-width="1.2"
|
||||
stroke-linecap="round" />
|
||||
<line x1="2.94" y1="12.5" x2="15.06" y2="12.5" stroke="currentColor" stroke-width="1.2"
|
||||
stroke-linecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</t-tooltip>
|
||||
</div>
|
||||
</t-tooltip>
|
||||
|
||||
<Teleport to="body">
|
||||
<div v-if="showModelSelector" class="model-selector-overlay" @click="closeModelSelector">
|
||||
<div class="model-selector-dropdown" :style="modelDropdownStyle" @click.stop>
|
||||
<div class="model-selector-header">
|
||||
<span>{{ $t('conversationSettings.models.chatGroupLabel') }}</span>
|
||||
<button class="model-selector-add" type="button" @click="handleModelChange('__add_model__')">
|
||||
<span class="add-icon">+</span>
|
||||
<span class="add-text">{{ $t('input.addModel') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="model-selector-content">
|
||||
<div v-for="model in availableModels" :key="model.id" class="model-option"
|
||||
:class="{ selected: model.id === selectedModelId }" @click="handleModelChange(model.id || '')">
|
||||
<div class="model-option-main">
|
||||
<span class="model-option-name">{{ modelDisplayName(model) }}</span>
|
||||
<span v-if="model.display_name" class="model-option-raw-name">{{ model.name }}</span>
|
||||
<span v-if="model.source === 'remote'" class="model-badge-remote">{{ $t('input.remote') }}</span>
|
||||
<span v-else-if="model.parameters?.parameter_size" class="model-badge-local">
|
||||
{{ model.parameters.parameter_size }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="model.description" class="model-option-desc">
|
||||
{{ model.description }}
|
||||
</div>
|
||||
<!-- 图片上传按钮 -->
|
||||
<t-tooltip placement="top" theme="light" :popupProps="{ overlayClassName: 'input-field-tooltip' }">
|
||||
<template #content>
|
||||
<div v-if="!isImageUploadEnabledByAgent" class="tooltip-with-link">
|
||||
<span>{{ $t('input.imageUploadDisabledByAgent') }}</span>
|
||||
<a href="#" @click.prevent="handleGoToAgentSettings('model')">{{ $t('input.goToAgentSettings') }}</a>
|
||||
</div>
|
||||
<div v-if="availableModels.length === 0" class="model-option empty">
|
||||
{{ $t('input.noModel') }}
|
||||
<span v-else>{{ $t('chat.imageUploadTooltip') }}</span>
|
||||
</template>
|
||||
<div class="control-btn image-upload-btn" :class="{
|
||||
'active': uploadedImages.length > 0,
|
||||
'disabled': !isImageUploadEnabledByAgent
|
||||
}" @click.stop="isImageUploadEnabledByAgent && triggerImageUpload()">
|
||||
<svg width="18" height="18" viewBox="0 0 1024 1024" fill="currentColor" class="control-icon">
|
||||
<path
|
||||
d="M896 128H128c-35.3 0-64 28.7-64 64v640c0 35.3 28.7 64 64 64h768c35.3 0 64-28.7 64-64V192c0-35.3-28.7-64-64-64zM128 832V192h768l0.1 640H128z" />
|
||||
<path d="M352 448a96 96 0 1 0 0-192 96 96 0 0 0 0 192z" />
|
||||
<path d="M128 768l224-288 160 160 192-256L896 640v128H128z" />
|
||||
</svg>
|
||||
<span v-if="uploadedImages.length > 0" class="image-count">{{ uploadedImages.length }}</span>
|
||||
</div>
|
||||
</t-tooltip>
|
||||
|
||||
<!-- 附件上传按钮 -->
|
||||
<t-tooltip placement="top" theme="light" :popupProps="{ overlayClassName: 'input-field-tooltip' }">
|
||||
<template #content>
|
||||
<span>{{ uploadedAttachments.length > 0 ? $t('chat.attachmentWithCount', {
|
||||
count: uploadedAttachments.length
|
||||
}) : $t('chat.attachmentUploadTooltip') }}</span>
|
||||
</template>
|
||||
<div class="control-btn attachment-upload-btn" :class="{ 'active': uploadedAttachments.length > 0 }"
|
||||
@click.stop="attachmentUploadRef?.triggerFileSelect()">
|
||||
<!-- 回形针图标 -->
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" stroke-linejoin="round" class="control-icon">
|
||||
<path
|
||||
d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
|
||||
</svg>
|
||||
<span v-if="uploadedAttachments.length > 0" class="attachment-count">{{ uploadedAttachments.length
|
||||
}}</span>
|
||||
</div>
|
||||
</t-tooltip>
|
||||
|
||||
<!-- @ 知识库/文件选择按钮 -->
|
||||
<t-tooltip placement="top" theme="light" :popupProps="{ overlayClassName: 'input-field-tooltip' }">
|
||||
<template #content>
|
||||
<div v-if="isKnowledgeBaseDisabledByAgent" class="tooltip-with-link">
|
||||
<span>{{ $t('input.kbDisabledByAgent') }}</span>
|
||||
<a href="#" @click.prevent="handleGoToAgentSettings('knowledge')">{{ $t('input.goToAgentSettings')
|
||||
}}</a>
|
||||
</div>
|
||||
<span v-else>{{ allSelectedItems.length > 0 ? $t('input.knowledgeBaseWithCount', {
|
||||
count:
|
||||
allSelectedItems.length
|
||||
}) : $t('input.knowledgeBase') }}</span>
|
||||
</template>
|
||||
<div ref="atButtonRef" class="control-btn kb-btn" data-guide="chat-kb-mention" :class="{
|
||||
'active': allSelectedItems.length > 0,
|
||||
'disabled': isKnowledgeBaseDisabledByAgent
|
||||
}" @click.stop @mousedown.prevent="triggerMention">
|
||||
<svg width="18" height="18" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
class="control-icon at-icon">
|
||||
<circle cx="10" cy="10" r="3.5" stroke="currentColor" stroke-width="1.8" />
|
||||
<path
|
||||
d="M13.5 10V11.5C13.5 12.163 13.7634 12.7989 14.2322 13.2678C14.7011 13.7366 15.337 14 16 14C16.663 14 17.2989 13.7366 17.7678 13.2678C18.2366 12.7989 18.5 12.163 18.5 11.5V10C18.5 7.74566 17.6045 5.58365 16.0104 3.98959C14.4163 2.39553 12.2543 1.5 10 1.5C7.74566 1.5 5.58365 2.39553 3.98959 3.98959C2.39553 5.58365 1.5 7.74566 1.5 10C1.5 12.2543 2.39553 14.4163 3.98959 16.0104C5.58365 17.6045 7.74566 18.5 10 18.5H12"
|
||||
stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<span v-if="allSelectedItems.length > 0" class="kb-count">{{ allSelectedItems.length }}</span>
|
||||
</div>
|
||||
</t-tooltip>
|
||||
|
||||
<!-- 模型显示 -->
|
||||
<t-tooltip :content="isModelLockedByAgent ? $t('input.modelLockedByAgent') : ''"
|
||||
:disabled="!isModelLockedByAgent">
|
||||
<div class="model-display" :class="{ 'agent-controlled': isModelLockedByAgent }">
|
||||
<div ref="modelButtonRef" class="model-selector-trigger" @click.stop="toggleModelSelector">
|
||||
<span class="model-selector-name">
|
||||
{{ selectedModelDisplayName }}
|
||||
</span>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor" class="model-dropdown-arrow"
|
||||
:class="{ 'rotate': showModelSelector }">
|
||||
<path d="M2.5 4.5L6 8L9.5 4.5H2.5Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</t-tooltip>
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<div v-if="showModelSelector" class="model-selector-overlay" @click="closeModelSelector">
|
||||
<div class="model-selector-dropdown" :style="modelDropdownStyle" @click.stop>
|
||||
<div class="model-selector-header">
|
||||
<span>{{ $t('conversationSettings.models.chatGroupLabel') }}</span>
|
||||
<button class="model-selector-add" type="button" @click="handleModelChange('__add_model__')">
|
||||
<span class="add-icon">+</span>
|
||||
<span class="add-text">{{ $t('input.addModel') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="model-selector-content">
|
||||
<div v-for="model in availableModels" :key="model.id" class="model-option"
|
||||
:class="{ selected: model.id === selectedModelId }" @click="handleModelChange(model.id || '')">
|
||||
<div class="model-option-main">
|
||||
<span class="model-option-name">{{ modelDisplayName(model) }}</span>
|
||||
<span v-if="model.display_name" class="model-option-raw-name">{{ model.name }}</span>
|
||||
<span v-if="model.source === 'remote'" class="model-badge-remote">{{ $t('input.remote') }}</span>
|
||||
<span v-else-if="model.parameters?.parameter_size" class="model-badge-local">
|
||||
{{ model.parameters.parameter_size }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="model.description" class="model-option-desc">
|
||||
{{ model.description }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="availableModels.length === 0" class="model-option empty">
|
||||
{{ $t('input.noModel') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</Teleport>
|
||||
|
||||
<!-- 右侧控制按钮组 -->
|
||||
<div class="control-right">
|
||||
<!-- 停止按钮(仅在回复中时显示) -->
|
||||
<t-tooltip v-if="isReplying" :content="$t('input.stopGeneration')" placement="top">
|
||||
<div @click="handleStop" class="control-btn stop-btn">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<rect x="5" y="5" width="6" height="6" rx="1" />
|
||||
</svg>
|
||||
<!-- 右侧控制按钮组 -->
|
||||
<div class="control-right">
|
||||
<!-- 停止按钮(仅在回复中时显示) -->
|
||||
<t-tooltip v-if="isReplying" :content="$t('input.stopGeneration')" placement="top">
|
||||
<div @click="handleStop" class="control-btn stop-btn">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<rect x="5" y="5" width="6" height="6" rx="1" />
|
||||
</svg>
|
||||
</div>
|
||||
</t-tooltip>
|
||||
|
||||
<!-- 发送按钮 -->
|
||||
<div v-if="!isReplying" @click="createSession(query)" class="control-btn send-btn" data-guide="chat-send"
|
||||
:class="{ 'disabled': !query.length }">
|
||||
<img src="../assets/img/sending-aircraft.svg" :alt="$t('input.send')" />
|
||||
</div>
|
||||
</t-tooltip>
|
||||
|
||||
<!-- 发送按钮 -->
|
||||
<div v-if="!isReplying" @click="createSession(query)" class="control-btn send-btn" data-guide="chat-send"
|
||||
:class="{ 'disabled': !query.length }">
|
||||
<img src="../assets/img/sending-aircraft.svg" :alt="$t('input.send')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mention Selector -->
|
||||
@@ -2392,8 +2396,8 @@ const getImgSrc = (url: string) => {
|
||||
max-width: 800px;
|
||||
background: var(--td-bg-color-container, #FFF);
|
||||
border-radius: 12px;
|
||||
border: .5px solid var(--td-component-border, #E7E7E7);
|
||||
box-shadow: 0 6px 6px 0 rgba(0, 0, 0, 0.04), 0 12px 12px -1px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid var(--td-component-stroke, #dcdcdc);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04), 0 8px 16px -4px rgba(0, 0, 0, 0.06);
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--td-brand-color, #07C05F);
|
||||
@@ -2407,7 +2411,7 @@ const getImgSrc = (url: string) => {
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 6px 12px 6px;
|
||||
border-bottom: .5px solid var(--td-component-stroke, #e7e7e7);
|
||||
border-bottom: 1px solid var(--td-component-stroke, #dcdcdc);
|
||||
background: var(--td-bg-color-container, #fff);
|
||||
border-radius: 11px 11px 0 0;
|
||||
/* 与 .rich-input-container 内缘上边圆角一致(12px - 1px 边框) */
|
||||
|
||||
@@ -21,27 +21,54 @@
|
||||
<t-icon v-if="item.is_pinned" name="pin" class="submenu_pin_icon" />
|
||||
<span class="submenu_title-text">{{ item.title }}</span>
|
||||
</span>
|
||||
<t-dropdown
|
||||
v-if="!batchMode"
|
||||
:options="menuOptions"
|
||||
placement="bottom-right"
|
||||
trigger="click"
|
||||
@click="emit('menu-click', $event)"
|
||||
>
|
||||
<div @click.stop class="menu-more-wrap">
|
||||
<div v-if="!batchMode" class="session-row-menu-wrap" @click.stop>
|
||||
<button
|
||||
type="button"
|
||||
class="menu-more-wrap"
|
||||
aria-haspopup="menu"
|
||||
:aria-expanded="menuOpen"
|
||||
@click="toggleMenu"
|
||||
>
|
||||
<t-icon name="ellipsis" class="menu-more" />
|
||||
</button>
|
||||
<div v-if="menuOpen" class="session-row-menu" role="menu">
|
||||
<button
|
||||
v-for="option in menuOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
class="session-row-menu__item"
|
||||
:class="{ 'session-row-menu__item--error': option.theme === 'error' }"
|
||||
role="menuitem"
|
||||
@click="handleMenuClick(option)"
|
||||
>
|
||||
<component
|
||||
:is="option.prefixIcon"
|
||||
v-if="option.prefixIcon"
|
||||
class="session-row-menu__icon"
|
||||
/>
|
||||
<span class="session-row-menu__text">{{ option.content }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</t-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, ref } from 'vue'
|
||||
|
||||
interface SessionMenuOption {
|
||||
content: string
|
||||
value: string
|
||||
theme?: 'default' | 'success' | 'warning' | 'error' | 'primary'
|
||||
prefixIcon?: any
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
item: { id: string; path: string; title: string; is_pinned?: boolean }
|
||||
batchMode: boolean
|
||||
activePath: string
|
||||
selectedIds: string[]
|
||||
menuOptions: any[]
|
||||
menuOptions: SessionMenuOption[]
|
||||
/** 渠道文件夹下的会话(样式与聊天区会话共用文案列对齐) */
|
||||
nested?: boolean
|
||||
}>()
|
||||
@@ -53,4 +80,104 @@ const emit = defineEmits<{
|
||||
(e: 'hover-in'): void
|
||||
(e: 'hover-out'): void
|
||||
}>()
|
||||
|
||||
const menuOpen = ref(false)
|
||||
|
||||
const closeMenu = (): void => {
|
||||
menuOpen.value = false
|
||||
document.removeEventListener('click', closeMenu)
|
||||
}
|
||||
|
||||
const toggleMenu = (): void => {
|
||||
menuOpen.value = !menuOpen.value
|
||||
if (menuOpen.value) {
|
||||
document.addEventListener('click', closeMenu)
|
||||
} else {
|
||||
document.removeEventListener('click', closeMenu)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMenuClick = (option: SessionMenuOption): void => {
|
||||
closeMenu()
|
||||
emit('menu-click', { value: option.value })
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', closeMenu)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.submenu_item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.session-row-menu-wrap {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.menu-more-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.session-row-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
right: 0;
|
||||
z-index: 30;
|
||||
min-width: 132px;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--td-component-stroke);
|
||||
border-radius: 6px;
|
||||
background: var(--td-bg-color-container);
|
||||
box-shadow: var(--td-shadow-2);
|
||||
}
|
||||
|
||||
.session-row-menu__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-height: 32px;
|
||||
padding: 0 10px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
color: var(--td-text-color-primary);
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--td-bg-color-container-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.session-row-menu__item--error {
|
||||
color: var(--td-error-color);
|
||||
}
|
||||
|
||||
.session-row-menu__icon {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.session-row-menu__text {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -114,24 +114,20 @@
|
||||
<!-- 其他来源(IM / 网页嵌入):置顶一行入口,默认收起 -->
|
||||
<div v-if="visibleChannelBuckets.length" class="session-channels-section">
|
||||
<div class="timeline_header timeline_header--collapsible session-list-row session-list-row--flat"
|
||||
role="button" tabindex="0"
|
||||
@click="toggleChannelsSection"
|
||||
@keydown.enter.prevent="toggleChannelsSection"
|
||||
@keydown.space.prevent="toggleChannelsSection">
|
||||
role="button" tabindex="0" @click="toggleChannelsSection"
|
||||
@keydown.enter.prevent="toggleChannelsSection" @keydown.space.prevent="toggleChannelsSection">
|
||||
<span class="session-list-row__body">
|
||||
<span class="session-header-text">
|
||||
<span class="timeline_header-label">{{ t('menu.otherSources') }}</span>
|
||||
<t-icon class="timeline_header-chevron"
|
||||
:name="channelsSectionCollapsed ? 'chevron-right' : 'chevron-down'"
|
||||
size="10px" />
|
||||
:name="channelsSectionCollapsed ? 'chevron-right' : 'chevron-down'" size="10px" />
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<template v-if="!channelsSectionCollapsed">
|
||||
<template v-for="bucket in visibleChannelBuckets" :key="bucket.key">
|
||||
<div class="session-folder-header session-list-row session-list-row--flat"
|
||||
role="button" tabindex="0"
|
||||
@click="toggleSessionGroup(bucket.key)"
|
||||
<div class="session-folder-header session-list-row session-list-row--flat" role="button"
|
||||
tabindex="0" @click="toggleSessionGroup(bucket.key)"
|
||||
@keydown.enter.prevent="toggleSessionGroup(bucket.key)"
|
||||
@keydown.space.prevent="toggleSessionGroup(bucket.key)">
|
||||
<span class="session-list-row__body">
|
||||
@@ -151,8 +147,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<template v-if="!isSessionGroupCollapsed(bucket.key)">
|
||||
<div v-if="bucket.loading && bucket.items.length === 0"
|
||||
class="session-folder-loading">
|
||||
<div v-if="bucket.loading && bucket.items.length === 0" class="session-folder-loading">
|
||||
<div class="session-list-row session-list-row--flat">
|
||||
<span class="session-list-row__body">
|
||||
<t-loading size="small" />
|
||||
@@ -163,14 +158,14 @@
|
||||
class="submenu_item_p session-chat-row session-folder-item">
|
||||
<div class="session-list-row session-list-row--flat">
|
||||
<div class="session-list-row__body">
|
||||
<SessionSidebarRow :item="subitem" :batch-mode="batchMode"
|
||||
:active-path="currentSecondpath" :selected-ids="batchSelectedIds"
|
||||
:menu-options="buildSessionMenuOptions(subitem)"
|
||||
@navigate="gotopage(subitem.path)"
|
||||
@toggle-select="toggleBatchSelect(subitem.id)"
|
||||
@menu-click="handleSessionMenuClick($event, subitem)"
|
||||
@hover-in="mouseenteBotDownr(subitem.id)"
|
||||
@hover-out="mouseleaveBotDown" />
|
||||
<SessionSidebarRow :item="subitem" :batch-mode="batchMode"
|
||||
:active-path="currentSecondpath" :selected-ids="batchSelectedIds"
|
||||
:menu-options="buildSessionMenuOptions(subitem)"
|
||||
@navigate="gotopage(subitem.path)"
|
||||
@toggle-select="toggleBatchSelect(subitem.id)"
|
||||
@menu-click="handleSessionMenuClick($event, subitem)"
|
||||
@hover-in="mouseenteBotDownr(subitem.id)"
|
||||
@hover-out="mouseleaveBotDown" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -181,36 +176,38 @@
|
||||
|
||||
<!-- 网页对话:按日期分组,直接展示 -->
|
||||
<div class="session-web-list">
|
||||
<template v-if="webBucket?.loaded && chatGroupedSessions.length === 0">
|
||||
<div class="submenu_empty">{{ t('menu.noSessions') }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-for="group in chatGroupedSessions" :key="group.key">
|
||||
<div v-if="group.label" class="timeline_header session-list-row session-list-row--flat">
|
||||
<span class="session-list-row__body">
|
||||
<span class="timeline_header-label">{{ group.label }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-for="subitem in group.items" :key="subitem.id" class="submenu_item_p session-chat-row">
|
||||
<div class="session-list-row session-list-row--flat">
|
||||
<div class="session-list-row__body">
|
||||
<SessionSidebarRow :item="subitem" :batch-mode="batchMode"
|
||||
:active-path="currentSecondpath" :selected-ids="batchSelectedIds"
|
||||
:menu-options="buildSessionMenuOptions(subitem)"
|
||||
@navigate="gotopage(subitem.path)" @toggle-select="toggleBatchSelect(subitem.id)"
|
||||
@menu-click="handleSessionMenuClick($event, subitem)"
|
||||
@hover-in="mouseenteBotDownr(subitem.id)" @hover-out="mouseleaveBotDown" />
|
||||
<template v-if="webBucket?.loaded && chatGroupedSessions.length === 0">
|
||||
<div class="submenu_empty">{{ t('menu.noSessions') }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-for="group in chatGroupedSessions" :key="group.key">
|
||||
<div v-if="group.label" class="timeline_header session-list-row session-list-row--flat">
|
||||
<span class="session-list-row__body">
|
||||
<span class="timeline_header-label">{{ group.label }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-for="subitem in group.items" :key="subitem.id"
|
||||
class="submenu_item_p session-chat-row">
|
||||
<div class="session-list-row session-list-row--flat">
|
||||
<div class="session-list-row__body">
|
||||
<SessionSidebarRow :item="subitem" :batch-mode="batchMode"
|
||||
:active-path="currentSecondpath" :selected-ids="batchSelectedIds"
|
||||
:menu-options="buildSessionMenuOptions(subitem)"
|
||||
@navigate="gotopage(subitem.path)"
|
||||
@toggle-select="toggleBatchSelect(subitem.id)"
|
||||
@menu-click="handleSessionMenuClick($event, subitem)"
|
||||
@hover-in="mouseenteBotDownr(subitem.id)" @hover-out="mouseleaveBotDown" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="webBucket?.loading && chatGroupedSessions.length > 0"
|
||||
class="session-list-loading session-list-row session-list-row--flat">
|
||||
<span class="session-list-row__body">
|
||||
<t-loading size="small" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="webBucket?.loading && chatGroupedSessions.length > 0"
|
||||
class="session-list-loading session-list-row session-list-row--flat">
|
||||
<span class="session-list-row__body">
|
||||
<t-loading size="small" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1726,7 +1723,7 @@ const onDragHandleMouseDown = (e: MouseEvent) => {
|
||||
line-height: 20px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 4px 0;
|
||||
padding: 6px 0;
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
background: transparent;
|
||||
|
||||
@@ -112,6 +112,7 @@ export function useEmbedChatSession(options: {
|
||||
|
||||
const {
|
||||
shouldRenderAssistantMessage,
|
||||
shouldShowGlobalTypingIndicator,
|
||||
handleMsgList,
|
||||
processStreamChunk,
|
||||
} = useChatStreamHandler({
|
||||
@@ -359,6 +360,7 @@ export function useEmbedChatSession(options: {
|
||||
userHasScrolledUp,
|
||||
isFirstEnter,
|
||||
shouldRenderAssistantMessage,
|
||||
shouldShowGlobalTypingIndicator,
|
||||
getUserQuery,
|
||||
handleScroll,
|
||||
scrollToBottom,
|
||||
|
||||
@@ -8,30 +8,31 @@
|
||||
<div ref="sqContainerRef" class="suggested-questions-container">
|
||||
<!-- 骨架屏占位 -->
|
||||
<div v-if="sqLoading && suggestedQuestions.length === 0" class="suggested-questions-inner">
|
||||
<div class="suggested-questions-title"><t-skeleton animation="gradient" :row-col="[{ width: '120px', height: '18px' }]" /></div>
|
||||
<div class="suggested-questions-title"><t-skeleton animation="gradient"
|
||||
:row-col="[{ width: '120px', height: '18px' }]" /></div>
|
||||
<div class="suggested-questions-grid">
|
||||
<div v-for="n in 6" :key="'sq-skel-'+n" class="suggested-question-card sq-card-skeleton">
|
||||
<t-skeleton animation="gradient" :row-col="[{ width: '100%', height: '14px', type: 'rect' }]" />
|
||||
<div v-for="n in 6" :key="'sq-skel-' + n" class="suggested-question-card sq-card-skeleton">
|
||||
<t-skeleton animation="gradient"
|
||||
:row-col="[{ width: '100%', height: '14px', type: 'rect' }]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<transition v-else appear name="sq-slide-fade" mode="out-in"
|
||||
@before-leave="onBeforeLeave"
|
||||
@after-leave="onAfterLeave"
|
||||
@enter="onEnter"
|
||||
@after-enter="onQuestionsEntered"
|
||||
>
|
||||
<transition v-else appear name="sq-slide-fade" mode="out-in" @before-leave="onBeforeLeave"
|
||||
@after-leave="onAfterLeave" @enter="onEnter" @after-enter="onQuestionsEntered">
|
||||
<div v-if="suggestedQuestions.length > 0" :key="sqRenderKey" class="suggested-questions-inner">
|
||||
<div class="suggested-questions-title">{{ $t('chat.suggestedQuestions') }}</div>
|
||||
<div class="suggested-questions-title-row">
|
||||
<div class="suggested-questions-title">{{ $t('chat.suggestedQuestions') }}</div>
|
||||
<t-button variant="text" shape="square" size="small" class="suggested-questions-refresh"
|
||||
:loading="sqLoading" :title="$t('chat.refreshSuggestedQuestions')"
|
||||
@click="fetchSuggestedQuestions">
|
||||
<template #icon><t-icon name="refresh" /></template>
|
||||
</t-button>
|
||||
</div>
|
||||
<div class="suggested-questions-grid">
|
||||
<div
|
||||
v-for="(item, index) in suggestedQuestions"
|
||||
:key="item.question"
|
||||
class="suggested-question-card"
|
||||
:class="{ 'sq-card-visible': sqCardsRevealed }"
|
||||
<div v-for="(item, index) in suggestedQuestions" :key="item.question"
|
||||
class="suggested-question-card" :class="{ 'sq-card-visible': sqCardsRevealed }"
|
||||
:style="{ transitionDelay: sqCardsRevealed ? `${index * 50}ms` : '0ms' }"
|
||||
@click="handleSuggestedQuestionClick(item.question)"
|
||||
>
|
||||
@click="handleSuggestedQuestionClick(item.question)">
|
||||
<span class="suggested-question-text">{{ item.question }}</span>
|
||||
<span v-if="item.source === 'faq'" class="suggested-question-badge faq">FAQ</span>
|
||||
</div>
|
||||
@@ -42,18 +43,13 @@
|
||||
<InputField ref="inputFieldRef" @send-msg="sendMsg"></InputField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<ContextualGuide tour="chat" :when="showChatContextualGuide" />
|
||||
|
||||
<!-- 知识库编辑器(创建/编辑统一组件) -->
|
||||
<KnowledgeBaseEditorModal
|
||||
:visible="uiStore.showKBEditorModal"
|
||||
:mode="uiStore.kbEditorMode"
|
||||
:kb-id="uiStore.currentKBId || undefined"
|
||||
:initial-type="uiStore.kbEditorType"
|
||||
@update:visible="(val) => val ? null : uiStore.closeKBEditor()"
|
||||
@success="handleKBEditorSuccess"
|
||||
/>
|
||||
<KnowledgeBaseEditorModal :visible="uiStore.showKBEditorModal" :mode="uiStore.kbEditorMode"
|
||||
:kb-id="uiStore.currentKBId || undefined" :initial-type="uiStore.kbEditorType"
|
||||
@update:visible="(val) => val ? null : uiStore.closeKBEditor()" @success="handleKBEditorSuccess" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, nextTick, computed } from 'vue';
|
||||
@@ -80,7 +76,7 @@ const { t } = useI18n();
|
||||
const { navigateToKnowledgeBaseList } = useKnowledgeBaseCreationNavigation();
|
||||
|
||||
const showChatContextualGuide = computed(() => {
|
||||
return route.name === 'globalCreatChat' || route.name === 'kbCreatChat';
|
||||
return route.name === 'globalCreatChat' || route.name === 'kbCreatChat';
|
||||
});
|
||||
|
||||
// ===== 推荐问题 =====
|
||||
@@ -136,7 +132,7 @@ const onQuestionsEntered = () => {
|
||||
|
||||
const fetchSuggestedQuestions = async () => {
|
||||
const fetchId = ++suggestedQuestionsFetchId;
|
||||
if (suggestedQuestions.value.length === 0) sqLoading.value = true;
|
||||
sqLoading.value = true;
|
||||
try {
|
||||
const agentId = settingsStore.selectedAgentId;
|
||||
if (!agentId) return;
|
||||
@@ -193,7 +189,7 @@ async function createNewSession(value: string, modelId: string, mentionedItems:
|
||||
|
||||
// 构建 session 数据,包含 Agent 配置
|
||||
const sessionData: any = {};
|
||||
|
||||
|
||||
// 添加 Agent 配置(知识库信息在 agent_config 中)
|
||||
sessionData.agent_config = {
|
||||
enabled: true,
|
||||
@@ -220,11 +216,11 @@ async function createNewSession(value: string, modelId: string, mentionedItems:
|
||||
|
||||
const navigateToSession = async (sessionId: string, value: string, modelId: string, mentionedItems: any[], imageFiles: any[] = [], attachmentFiles: any[] = []) => {
|
||||
const now = new Date().toISOString();
|
||||
let obj = {
|
||||
title: t('createChat.newSessionTitle'),
|
||||
path: `chat/${sessionId}`,
|
||||
id: sessionId,
|
||||
isMore: false,
|
||||
let obj = {
|
||||
title: t('createChat.newSessionTitle'),
|
||||
path: `chat/${sessionId}`,
|
||||
id: sessionId,
|
||||
isMore: false,
|
||||
isNoTitle: true,
|
||||
created_at: now,
|
||||
updated_at: now
|
||||
@@ -300,8 +296,13 @@ const handleKBEditorSuccess = (kbId: string) => {
|
||||
}
|
||||
|
||||
@keyframes skeletonFadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.suggested-questions-inner {
|
||||
@@ -315,28 +316,47 @@ const handleKBEditorSuccess = (kbId: string) => {
|
||||
// 容器整体过渡:淡入 + 轻微上滑
|
||||
.sq-slide-fade-enter-active {
|
||||
transition: opacity 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.sq-slide-fade-leave-active {
|
||||
transition: opacity 0.15s cubic-bezier(0.4, 0, 1, 1),
|
||||
transform 0.15s cubic-bezier(0.4, 0, 1, 1);
|
||||
transform 0.15s cubic-bezier(0.4, 0, 1, 1);
|
||||
}
|
||||
|
||||
.sq-slide-fade-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
.sq-slide-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.suggested-questions-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.suggested-questions-title {
|
||||
font-size: 14px;
|
||||
color: var(--td-text-color-secondary);
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.suggested-questions-refresh {
|
||||
flex-shrink: 0;
|
||||
color: var(--td-text-color-secondary);
|
||||
|
||||
&:hover:not(.t-is-disabled) {
|
||||
color: var(--td-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.suggested-questions-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -358,10 +378,10 @@ const handleKBEditorSuccess = (kbId: string) => {
|
||||
opacity: 0;
|
||||
transform: translateY(8px) scale(0.97);
|
||||
transition: opacity 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
transform 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
border-color 0.2s ease,
|
||||
background 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
transform 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
border-color 0.2s ease,
|
||||
background 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
|
||||
&.sq-card-skeleton {
|
||||
opacity: 1;
|
||||
@@ -375,19 +395,38 @@ const handleKBEditorSuccess = (kbId: string) => {
|
||||
:deep(.t-skeleton) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.t-skeleton__row) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.t-skeleton__col) {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&:nth-child(1) { width: 132px; }
|
||||
&:nth-child(2) { width: 168px; }
|
||||
&:nth-child(3) { width: 116px; }
|
||||
&:nth-child(4) { width: 152px; }
|
||||
&:nth-child(5) { width: 124px; }
|
||||
&:nth-child(6) { width: 144px; }
|
||||
&:nth-child(1) {
|
||||
width: 132px;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 168px;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 116px;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
width: 152px;
|
||||
}
|
||||
|
||||
&:nth-child(5) {
|
||||
width: 124px;
|
||||
}
|
||||
|
||||
&:nth-child(6) {
|
||||
width: 144px;
|
||||
}
|
||||
}
|
||||
|
||||
&.sq-card-visible {
|
||||
@@ -443,6 +482,7 @@ const handleKBEditorSuccess = (kbId: string) => {
|
||||
width: 500px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 750px) {
|
||||
.answers-input {
|
||||
transform: translateX(-250px);
|
||||
@@ -452,6 +492,7 @@ const handleKBEditorSuccess = (kbId: string) => {
|
||||
width: 340px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.answers-input {
|
||||
transform: translateX(-250px);
|
||||
@@ -461,7 +502,6 @@ const handleKBEditorSuccess = (kbId: string) => {
|
||||
width: 300px !important;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style lang="less">
|
||||
.del-menu-popup {
|
||||
|
||||
@@ -3988,6 +3988,8 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// Wiki reader styles are for the knowledge-base document surface only.
|
||||
// Chat answer Markdown styles are centralized in components/css/chat-markdown.less.
|
||||
.wiki-reader-body {
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
|
||||
Reference in New Issue
Block a user