(null)
const userHasScrolledUp = ref(false)
const SCROLL_BOTTOM_THRESHOLD = 80
@@ -130,6 +133,8 @@ export function useEmbedChatSession(options: {
isAgentStreamSession,
scrollToBottom,
onReplyComplete: notifyEmbedReceived,
+ onTurnComplete: options.onTurnComplete,
+ onAfterMsgList: () => options.onMessagesLoaded?.(messagesList),
onError: embedToast,
isFirstEnter,
scrollContainer,
@@ -298,6 +303,8 @@ export function useEmbedChatSession(options: {
? `/api/v1/embed/${options.channelId}/agent-chat`
: `/api/v1/embed/${options.channelId}/knowledge-chat`
+ const suggestionAttribution = pendingSuggestionAttribution
+ pendingSuggestionAttribution = null
await startStream({
session_id: options.sessionId.value,
knowledge_base_ids: options.kbIds,
@@ -312,6 +319,7 @@ export function useEmbedChatSession(options: {
images: imageAttachments.length > 0 ? imageAttachments : undefined,
attachment_uploads: attachmentUploads.length > 0 ? attachmentUploads : undefined,
query: outboundQuery,
+ suggestion_attribution: suggestionAttribution || undefined,
method: 'POST',
url: endpoint,
embed_token: options.token,
@@ -391,5 +399,8 @@ export function useEmbedChatSession(options: {
onClickScrollToBottom,
sendMsg,
handleStopGeneration,
+ setSuggestionAttribution: (suggestionSetId: string, questionId: string) => {
+ pendingSuggestionAttribution = { suggestion_set_id: suggestionSetId, question_id: questionId }
+ },
}
}
diff --git a/frontend/src/i18n/embed.ts b/frontend/src/i18n/embed.ts
index a28f63223..2bf31d71c 100644
--- a/frontend/src/i18n/embed.ts
+++ b/frontend/src/i18n/embed.ts
@@ -77,6 +77,8 @@ const messages = {
"newChat": "新对话",
"suggestedQuestions": "你可以这样问我",
"suggestedQuestionsLoading": "正在加载推荐问题...",
+ "followUpQuestions": "接下来可以继续问",
+ "refreshSuggestedQuestions": "换一批推荐问题",
"inputPlaceholder": "请输入您的消息...",
"send": "发送",
"thinking": "思考中...",
@@ -550,6 +552,8 @@ const messages = {
"newChat": "New Chat",
"suggestedQuestions": "You can ask me",
"suggestedQuestionsLoading": "Loading suggestions...",
+ "followUpQuestions": "Continue with a follow-up",
+ "refreshSuggestedQuestions": "Refresh suggestions",
"inputPlaceholder": "Enter your message...",
"send": "Send",
"thinking": "Thinking...",
@@ -1008,6 +1012,8 @@ const koEmbedPublish = {
},
chat: {
suggestedQuestions: '이렇게 물어보세요',
+ followUpQuestions: '이어서 질문해 보세요',
+ refreshSuggestedQuestions: '추천 질문 새로고침',
imageTooMany: '이미지는 최대 5장까지 업로드할 수 있습니다',
imageTypeSizeError: 'JPG/PNG/GIF/WEBP만 지원하며, 각 파일은 10MB 이하여야 합니다',
imageReadFailed: '이미지를 읽지 못했습니다',
@@ -1091,6 +1097,8 @@ const ruEmbedPublish = {
},
chat: {
suggestedQuestions: 'Вы можете спросить так',
+ followUpQuestions: 'Продолжите уточняющим вопросом',
+ refreshSuggestedQuestions: 'Обновить предложения',
imageTooMany: 'Можно загрузить не более 5 изображений',
imageTypeSizeError: 'Поддерживаются только JPG/PNG/GIF/WEBP, каждый файл до 10 МБ',
imageReadFailed: 'Не удалось прочитать изображение',
diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts
index 43901927b..4a59c1972 100755
--- a/frontend/src/i18n/locales/en-US.ts
+++ b/frontend/src/i18n/locales/en-US.ts
@@ -3081,6 +3081,7 @@ export default {
title: 'Chat',
newChat: 'New Chat',
suggestedQuestions: 'You can ask me',
+ followUpQuestions: 'Continue with a follow-up',
suggestedQuestionsLoading: 'Loading suggestions...',
refreshSuggestedQuestions: 'Refresh suggestions',
inputPlaceholder: 'Enter your message...',
@@ -5487,6 +5488,41 @@ export default {
capability: 'Extensions',
integration: 'Publish & Integrations',
},
+ questionSuggestions: {
+ navLabel: 'Question suggestions',
+ title: 'Conversation question suggestions',
+ description: 'Configure starters and contextual follow-ups in one agent-owned policy. Channels may hide them but cannot override the policy.',
+ startersTitle: 'Conversation starters',
+ followUpsTitle: 'After-answer follow-ups',
+ enableStarters: 'Show starter questions',
+ enableStartersDesc: 'Shown before the first user message from curated, knowledge, or mixed sources.',
+ enableFollowUps: 'Generate follow-up questions',
+ enableFollowUpsDesc: 'Generated asynchronously after every completed answer. Enabling this adds model usage.',
+ sourceMode: 'Source mode',
+ count: 'Question count',
+ curatedItems: 'Curated questions',
+ curatedItemsDesc: 'Used for starters and prioritized in hybrid mode.',
+ addItem: 'Add question',
+ model: 'Generation model',
+ modelDesc: 'Uses the model from the completed turn when empty.',
+ advancedSettings: 'Advanced generation settings',
+ advancedSettingsDesc: 'Context, question types, instructions, and display rules',
+ displayRules: 'Display and fallback rules',
+ contextTurns: 'Context turns',
+ categories: 'Question types',
+ instruction: 'Additional instruction',
+ suppressFallback: 'Hide after fallback answers',
+ suppressQuestion: 'Hide when the answer ends with a question',
+ knowledgeFallback: 'Use knowledge candidates if generation fails',
+ allowRegenerate: 'Allow users to regenerate',
+ modeCurated: 'Curated',
+ modeKnowledge: 'Knowledge',
+ modeGenerated: 'Generated',
+ modeHybrid: 'Hybrid',
+ categoryClarify: 'Clarify',
+ categoryDeepen: 'Deepen',
+ categoryAction: 'Next step',
+ },
placeholders: {
available: 'Available variables: ',
clickToInsert: '(click to insert)',
diff --git a/frontend/src/i18n/locales/ko-KR.ts b/frontend/src/i18n/locales/ko-KR.ts
index ca0b6ae2e..5aeda5035 100755
--- a/frontend/src/i18n/locales/ko-KR.ts
+++ b/frontend/src/i18n/locales/ko-KR.ts
@@ -895,6 +895,7 @@ export default {
title: "대화",
newChat: "새 대화",
suggestedQuestions: "이렇게 물어보세요",
+ followUpQuestions: "이어서 질문해 보세요",
suggestedQuestionsLoading: "추천 질문 로딩 중...",
refreshSuggestedQuestions: "추천 질문 새로고침",
inputPlaceholder: "메시지를 입력하세요...",
@@ -5498,6 +5499,24 @@ export default {
capability: '기능 확장',
integration: '게시 및 통합',
},
+ questionSuggestions: {
+ navLabel: '질문 추천', title: '대화 질문 추천',
+ description: '대화 시작 질문과 답변 후 후속 질문을 에이전트 정책으로 통합 설정합니다.',
+ startersTitle: '시작 질문', followUpsTitle: '답변 후 후속 질문',
+ enableStarters: '시작 질문 표시', enableStartersDesc: '첫 질문 전에 운영 설정 또는 지식 기반 질문을 표시합니다.',
+ enableFollowUps: '후속 질문 생성', enableFollowUpsDesc: '완료된 답변마다 비동기로 생성하며 추가 모델 사용량이 발생합니다.',
+ sourceMode: '콘텐츠 소스', count: '표시 개수', curatedItems: '운영 설정 질문',
+ curatedItemsDesc: '혼합 모드에서 우선 표시됩니다.', addItem: '질문 추가',
+ model: '생성 모델', modelDesc: '비워 두면 현재 대화 모델을 사용합니다.',
+ advancedSettings: '고급 생성 설정',
+ advancedSettingsDesc: '컨텍스트, 질문 유형, 생성 지침 및 표시 규칙',
+ displayRules: '표시 및 폴백 규칙',
+ contextTurns: '컨텍스트 턴 수', categories: '질문 유형', instruction: '추가 생성 지침',
+ suppressFallback: '대체 답변 뒤에는 숨기기', suppressQuestion: '답변이 질문으로 끝나면 숨기기',
+ knowledgeFallback: '생성 실패 시 지식 후보 사용', allowRegenerate: '새 질문 묶음 허용',
+ modeCurated: '운영 설정', modeKnowledge: '지식 기반', modeGenerated: '모델 생성', modeHybrid: '혼합',
+ categoryClarify: '명확화', categoryDeepen: '심화', categoryAction: '다음 단계',
+ },
placeholders: {
available: '사용 가능한 변수: ',
clickToInsert: '(클릭하여 삽입)',
diff --git a/frontend/src/i18n/locales/ru-RU.ts b/frontend/src/i18n/locales/ru-RU.ts
index 4b16f73a2..c66c25d6d 100755
--- a/frontend/src/i18n/locales/ru-RU.ts
+++ b/frontend/src/i18n/locales/ru-RU.ts
@@ -3663,6 +3663,7 @@ export default {
title: 'Диалог',
newChat: 'Новый чат',
suggestedQuestions: 'Вы можете спросить меня',
+ followUpQuestions: 'Продолжите уточняющим вопросом',
suggestedQuestionsLoading: 'Загрузка предложений...',
refreshSuggestedQuestions: 'Обновить предложения',
inputPlaceholder: 'Введите ваше сообщение...',
@@ -4998,6 +4999,24 @@ export default {
capability: 'Расширения',
integration: 'Публикация и интеграция',
},
+ questionSuggestions: {
+ navLabel: 'Рекомендуемые вопросы', title: 'Рекомендации вопросов в диалоге',
+ description: 'Единая политика агента для стартовых и контекстных вопросов после ответа.',
+ startersTitle: 'Стартовые вопросы', followUpsTitle: 'Вопросы после ответа',
+ enableStarters: 'Показывать стартовые вопросы', enableStartersDesc: 'Показываются до первого сообщения пользователя.',
+ enableFollowUps: 'Генерировать уточнения', enableFollowUpsDesc: 'Создаются асинхронно после каждого полного ответа и расходуют модель.',
+ sourceMode: 'Источник', count: 'Количество', curatedItems: 'Редакторские вопросы',
+ curatedItemsDesc: 'Имеют приоритет в гибридном режиме.', addItem: 'Добавить вопрос',
+ model: 'Модель генерации', modelDesc: 'Если не выбрана, используется модель текущего ответа.',
+ advancedSettings: 'Расширенные настройки генерации',
+ advancedSettingsDesc: 'Контекст, типы вопросов, инструкции и правила показа',
+ displayRules: 'Правила показа и резерва',
+ contextTurns: 'Ходы контекста', categories: 'Типы вопросов', instruction: 'Дополнительная инструкция',
+ suppressFallback: 'Скрывать после резервного ответа', suppressQuestion: 'Скрывать, если ответ заканчивается вопросом',
+ knowledgeFallback: 'Использовать базу знаний при ошибке', allowRegenerate: 'Разрешить обновление',
+ modeCurated: 'Редакторские', modeKnowledge: 'База знаний', modeGenerated: 'Модель', modeHybrid: 'Гибрид',
+ categoryClarify: 'Уточнение', categoryDeepen: 'Углубление', categoryAction: 'Следующий шаг',
+ },
placeholders: {
available: 'Доступные переменные: ',
clickToInsert: '(нажмите для вставки)',
diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts
index 5c824dc89..35bba61be 100755
--- a/frontend/src/i18n/locales/zh-CN.ts
+++ b/frontend/src/i18n/locales/zh-CN.ts
@@ -893,6 +893,7 @@ export default {
title: "对话",
newChat: "新对话",
suggestedQuestions: "你可以这样问我",
+ followUpQuestions: "接下来可以继续问",
suggestedQuestionsLoading: "正在加载推荐问题...",
refreshSuggestedQuestions: "换一批推荐问题",
inputPlaceholder: "请输入您的消息...",
@@ -5504,6 +5505,41 @@ export default {
capability: "能力扩展",
integration: "发布集成",
},
+ questionSuggestions: {
+ navLabel: "问题推荐",
+ title: "对话问题推荐",
+ description: "统一配置开场问题与回答后的上下文追问;渠道可关闭展示,但不能改写智能体策略。",
+ startersTitle: "开场推荐",
+ followUpsTitle: "回答后推荐",
+ enableStarters: "展示开场问题",
+ enableStartersDesc: "在用户首次提问前展示,可使用运营配置、知识库或混合来源。",
+ enableFollowUps: "生成回答后推荐",
+ enableFollowUpsDesc: "每次完整回答结束后异步生成,不阻塞主回答。启用后会产生额外模型调用。",
+ sourceMode: "内容来源",
+ count: "展示数量",
+ curatedItems: "运营配置问题",
+ curatedItemsDesc: "用于开场推荐;混合模式下优先展示。",
+ addItem: "添加问题",
+ model: "生成模型",
+ modelDesc: "留空时使用本轮对话模型。",
+ advancedSettings: "高级生成设置",
+ advancedSettingsDesc: "上下文、问题类型、生成要求与展示规则",
+ displayRules: "展示与兜底规则",
+ contextTurns: "上下文轮数",
+ categories: "问题类型",
+ instruction: "附加生成要求",
+ suppressFallback: "兜底回答后不展示",
+ suppressQuestion: "回答本身以提问结尾时不展示",
+ knowledgeFallback: "模型生成失败时使用知识库候选",
+ allowRegenerate: "允许用户换一批",
+ modeCurated: "运营配置",
+ modeKnowledge: "知识库",
+ modeGenerated: "模型生成",
+ modeHybrid: "混合",
+ categoryClarify: "澄清",
+ categoryDeepen: "深入",
+ categoryAction: "下一步",
+ },
placeholders: {
available: "可用变量:",
clickToInsert: "(点击插入)",
diff --git a/frontend/src/views/agent/AgentEditorModal.vue b/frontend/src/views/agent/AgentEditorModal.vue
index bec0e240b..98e6d8f4a 100644
--- a/frontend/src/views/agent/AgentEditorModal.vue
+++ b/frontend/src/views/agent/AgentEditorModal.vue
@@ -825,6 +825,187 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $t('agentEditor.questionSuggestions.enableStartersDesc') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $t('agentEditor.questionSuggestions.curatedItemsDesc') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('agentEditor.questionSuggestions.addItem') }}
+
+
+
+
+
+
+
+
+
+
+
{{ $t('agentEditor.questionSuggestions.enableFollowUpsDesc') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $t('agentEditor.questionSuggestions.modelDesc') }}
+
+
+ formData.config.question_suggestions.follow_ups.model_id = val"
+ @add-model="handleAddModel('summary')" />
+
+
+
+
+ {{ $t('agentEditor.questionSuggestions.advancedSettings') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('agentEditor.questionSuggestions.suppressFallback') }}
+ {{ $t('agentEditor.questionSuggestions.suppressQuestion') }}
+ {{ $t('agentEditor.questionSuggestions.knowledgeFallback') }}
+ {{ $t('agentEditor.questionSuggestions.allowRegenerate') }}
+
+
+
+
+
+
+
{
suggestedQuestionsFetchId++;
@@ -309,6 +324,54 @@ const handleSuggestedQuestionClick = (question) => {
}
};
+const resolveAssistantMessageId = (message) => message?.id || message?.assistant_message_id;
+
+const loadFollowUpSuggestions = async (message, ensure = false, regenerate = false) => {
+ const messageId = resolveAssistantMessageId(message);
+ const targetSessionId = session_id.value;
+ if (!messageId || !targetSessionId || message.suggestionsDismissed) return;
+ message.suggestionLoading = true;
+ try {
+ let response = ensure
+ ? await ensureMessageSuggestions(targetSessionId, messageId, regenerate)
+ : await getMessageSuggestions(targetSessionId, messageId);
+ let set = response?.data;
+ for (let attempt = 0; set?.status === 'generating' && attempt < 120; attempt++) {
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ if (session_id.value !== targetSessionId || message.suggestionsDismissed) return;
+ response = await getMessageSuggestions(targetSessionId, messageId);
+ set = response?.data;
+ }
+ message.suggestionSet = set?.status === 'ready' ? set : null;
+ } catch (error) {
+ if (ensure) console.warn('[FollowUpSuggestions] Failed to generate:', error);
+ message.suggestionSet = null;
+ } finally {
+ message.suggestionLoading = false;
+ nextTick(() => scrollToBottom());
+ }
+};
+
+const recordSuggestionEvent = (message, set, eventType, questionId = '') => {
+ if (!set?.id) return;
+ void recordMessageSuggestionEvent(session_id.value, set.id, eventType, questionId).catch(() => undefined);
+};
+
+const handleFollowUpSelect = (message, item) => {
+ recordSuggestionEvent(message, message.suggestionSet, 'click', item.id);
+ pendingSuggestionAttribution = {
+ suggestion_set_id: message.suggestionSet.id,
+ question_id: item.id,
+ };
+ if (inputFieldRef.value?.triggerSend) inputFieldRef.value.triggerSend(item.text);
+ else sendMsg(item.text);
+};
+
+const dismissSuggestions = (message, set) => {
+ message.suggestionsDismissed = true;
+ recordSuggestionEvent(message, set, 'dismiss');
+};
+
// 防抖包装,切换知识库/文件时300ms内不重复请求
const debouncedFetchSuggestions = () => {
if (historyLoading.value || messagesList.length > 0) return;
@@ -466,6 +529,11 @@ const {
scrollContainer,
debug: import.meta.env.DEV,
onAfterMsgList: async () => {
+ for (const message of messagesList) {
+ if (message.role === 'assistant' && message.is_completed && message.suggestionSet === undefined) {
+ void loadFollowUpSuggestions(message, false);
+ }
+ }
const lastMessage = messagesList[messagesList.length - 1];
if (lastMessage && !lastMessage.is_completed) {
isReplying.value = true;
@@ -507,6 +575,9 @@ const {
attachStreamDebugToMessage(message);
pendingStreamDebug.value = null;
},
+ onTurnComplete: (message) => {
+ void loadFollowUpSuggestions(message, true);
+ },
});
const showGlobalTypingIndicator = computed(() =>
@@ -668,6 +739,8 @@ const sendMsg = async (value, modelId = '', mentionedItems = [], imageFiles = []
const requestMcpServiceIds = agentEnabled ? mcpServiceIds : [];
const requestSkillNames = agentEnabled ? skillNames : [];
+ const suggestionAttribution = pendingSuggestionAttribution;
+ pendingSuggestionAttribution = null;
await startStream({
session_id: session_id.value,
knowledge_base_ids: kbIds,
@@ -684,6 +757,7 @@ const sendMsg = async (value, modelId = '', mentionedItems = [], imageFiles = []
images: imageAttachments.length > 0 ? imageAttachments : undefined,
attachment_uploads: attachmentUploads.length > 0 ? attachmentUploads : undefined,
query: value,
+ suggestion_attribution: suggestionAttribution || undefined,
method: 'POST',
url: endpoint,
});
diff --git a/frontend/src/views/embed/EmbedChatCore.vue b/frontend/src/views/embed/EmbedChatCore.vue
index 71f55ebc5..7ac6087da 100644
--- a/frontend/src/views/embed/EmbedChatCore.vue
+++ b/frontend/src/views/embed/EmbedChatCore.vue
@@ -69,6 +69,14 @@
:embed-session-sig="sessionSig"
:embed-visitor-id="visitorId"
/>
+ handleFollowUpSelect(session, item)"
+ @regenerate="loadFollowUpSuggestions(session, true, true)"
+ @impression="(set) => recordFollowUpEvent(set, 'impression')"
+ @dismiss="(set) => dismissFollowUps(session, set)" />
@@ -107,13 +115,22 @@