mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-09-19 02:18:25 +08:00
feat: 支持聊天消息中的图片预览功能,更新Agent提示以包含图文结果输出
This commit is contained in:
@@ -95,7 +95,7 @@ services:
|
||||
- full
|
||||
|
||||
docreader:
|
||||
image: wechatopenai/weknora-docreader:main
|
||||
image: wechatopenai/weknora-docreader:latest
|
||||
container_name: WeKnora-docreader-dev
|
||||
ports:
|
||||
- "${DOCREADER_PORT:-50051}:50051"
|
||||
|
||||
@@ -208,6 +208,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- Image Preview -->
|
||||
<picturePreview :reviewImg="imagePreviewVisible" :reviewUrl="imagePreviewUrl" @closePreImg="closeImagePreview" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -216,6 +219,7 @@ import { useRouter } from 'vue-router';
|
||||
import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
import ToolResultRenderer from './ToolResultRenderer.vue';
|
||||
import picturePreview from '@/components/picture-preview.vue';
|
||||
import { getChunkByIdOnly } from '@/api/knowledge-base';
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import { useUIStore } from '@/stores/ui';
|
||||
@@ -248,6 +252,19 @@ const getLocalizedToolName = (toolName?: string | null): string => {
|
||||
// 根元素引用
|
||||
const rootElement = ref<HTMLElement | null>(null);
|
||||
|
||||
// 图片预览状态
|
||||
const imagePreviewVisible = ref(false);
|
||||
const imagePreviewUrl = ref('');
|
||||
|
||||
const openImagePreview = (url: string) => {
|
||||
imagePreviewUrl.value = url;
|
||||
imagePreviewVisible.value = true;
|
||||
};
|
||||
|
||||
const closeImagePreview = () => {
|
||||
imagePreviewVisible.value = false;
|
||||
};
|
||||
|
||||
// 浮层状态(Web/KB 共用)
|
||||
const KB_SNIPPET_LIMIT = 600;
|
||||
|
||||
@@ -923,6 +940,18 @@ const onRootClick = (e: Event) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target) return;
|
||||
|
||||
// Handle image clicks -> open preview
|
||||
if (target.tagName === 'IMG') {
|
||||
const imgEl = target as HTMLImageElement;
|
||||
const src = imgEl.getAttribute('src');
|
||||
if (src) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openImagePreview(src);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle web citation clicks
|
||||
const webEl = target.closest?.('.citation-web') as HTMLElement | null;
|
||||
if (webEl && webEl.getAttribute('data-url')) {
|
||||
@@ -1130,8 +1159,8 @@ const renderMarkdown = (content: any): string => {
|
||||
if (!html) return '';
|
||||
|
||||
return DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'code', 'pre', 'ul', 'ol', 'li', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a', 'span', 'table', 'thead', 'tbody', 'tr', 'th', 'td'],
|
||||
ALLOWED_ATTR: ['href', 'title', 'target', 'rel', 'data-tooltip', 'data-url', 'data-kb-id', 'data-chunk-id', 'data-doc', 'class', 'role', 'tabindex']
|
||||
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'code', 'pre', 'ul', 'ol', 'li', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a', 'span', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'img', 'figure', 'figcaption'],
|
||||
ALLOWED_ATTR: ['href', 'title', 'target', 'rel', 'data-tooltip', 'data-url', 'data-kb-id', 'data-chunk-id', 'data-doc', 'class', 'role', 'tabindex', 'src', 'alt', 'width', 'height', 'style']
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Markdown rendering error:', e, 'Content:', contentStr.substring(0, 100));
|
||||
@@ -1891,6 +1920,24 @@ const handleAddToKnowledge = (answerEvent: any) => {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(img) {
|
||||
max-width: 80%;
|
||||
max-height: 300px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
margin: 8px 0;
|
||||
border: 0.5px solid #e5e7eb;
|
||||
object-fit: contain;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2005,6 +2052,24 @@ const handleAddToKnowledge = (answerEvent: any) => {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(img) {
|
||||
max-width: 80%;
|
||||
max-height: 300px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
margin: 8px 0;
|
||||
border: 0.5px solid #e5e7eb;
|
||||
object-fit: contain;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, watch, computed, ref, reactive, defineProps, nextTick } from 'vue';
|
||||
import { onMounted, onBeforeUnmount, watch, computed, ref, reactive, defineProps, nextTick } from 'vue';
|
||||
import { marked } from 'marked';
|
||||
import docInfo from './docInfo.vue';
|
||||
import deepThink from './deepThink.vue';
|
||||
@@ -282,9 +282,35 @@ const handleAddToKnowledge = () => {
|
||||
MessagePlugin.info(t('chat.editorOpened') || '已打开编辑器,请选择知识库后保存');
|
||||
};
|
||||
|
||||
// 处理 markdown-content 中图片的点击事件
|
||||
const handleMarkdownImageClick = (e) => {
|
||||
const target = e.target;
|
||||
if (target && target.tagName === 'IMG') {
|
||||
const src = target.getAttribute('src');
|
||||
if (src) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
preview(src);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
processedMarkdown.value = splitMarkdownByImages(props.content);
|
||||
removeImg()
|
||||
removeImg();
|
||||
|
||||
// 为 markdown-content 中的图片添加点击事件
|
||||
nextTick(() => {
|
||||
if (parentMd.value) {
|
||||
parentMd.value.addEventListener('click', handleMarkdownImageClick, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (parentMd.value) {
|
||||
parentMd.value.removeEventListener('click', handleMarkdownImageClick, true);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@@ -396,18 +422,42 @@ onMounted(async () => {
|
||||
background: #fafafa;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(img) {
|
||||
max-width: 80%;
|
||||
max-height: 300px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
margin: 8px 0;
|
||||
border: 0.5px solid #e5e7eb;
|
||||
object-fit: contain;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ai-markdown-img {
|
||||
max-width: 80%;
|
||||
max-height: 300px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
object-fit: scale-down;
|
||||
contain: content;
|
||||
margin-left: 16px;
|
||||
object-fit: contain;
|
||||
margin: 8px 0 8px 16px;
|
||||
border: 0.5px solid #E7E7E7;
|
||||
max-width: 708px;
|
||||
height: 230px;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
.bot_msg {
|
||||
|
||||
@@ -320,6 +320,7 @@ For every retrieval attempt (Phase 1 or Phase 3), follow this exact chain:
|
||||
* **Sourced(Inline, Proximate Citations):** All factual statements must include a citation immediately after the relevant claim—within the same sentence or paragraph where the fact appears: <kb doc="..." chunk_id="..." /> or <web url="..." title="..." />.
|
||||
Citations may not be placed at the end of the answer. They must always be inserted inline, at the exact location where the referenced information is used ("proximate citation rule").
|
||||
* **Structured:** Clear hierarchy and logic.
|
||||
* **Rich Media (Markdown with Images):** When retrieved chunks contain images (indicated by the "images" field with URLs), you MUST include them in your response using standard Markdown image syntax: . Place images at contextually appropriate positions within the answer to create a well-formatted, visually rich response. Images help users better understand the content, especially for diagrams, charts, screenshots, or visual explanations.
|
||||
|
||||
### System Status
|
||||
Current Time: {{current_time}}
|
||||
@@ -399,9 +400,10 @@ For every information seeking step, strictly follow this 3-step atomic unit:
|
||||
|
||||
### Final Output Standards
|
||||
1. **Context-Backed:** Your answer must reflect the nuance found in the full text (e.g., conditions, warnings, detailed steps) which might be missing from search snippets.
|
||||
2 **Sourced(Inline, Proximate Citations):** All factual statements must include a citation immediately after the relevant claim—within the same sentence or paragraph where the fact appears: <kb doc="..." chunk_id="..." />.
|
||||
2. **Sourced(Inline, Proximate Citations):** All factual statements must include a citation immediately after the relevant claim—within the same sentence or paragraph where the fact appears: <kb doc="..." chunk_id="..." />.
|
||||
Citations may not be placed at the end of the answer. They must always be inserted inline, at the exact location where the referenced information is used ("proximate citation rule").
|
||||
3. **Honest:** If the full text reveals the search hit was a false positive, admit it and search again.
|
||||
4. **Rich Media (Markdown with Images):** When retrieved chunks contain images (indicated by the "images" field with URLs), you MUST include them in your response using standard Markdown image syntax: . Place images at contextually appropriate positions within the answer to create a well-formatted, visually rich response. Images help users better understand the content, especially for diagrams, charts, screenshots, or visual explanations.
|
||||
|
||||
### System Status
|
||||
Current Time: {{current_time}}
|
||||
|
||||
@@ -59,8 +59,8 @@ func (p *PluginChatCompletionStream) OnEvent(ctx context.Context,
|
||||
pipelineInfo(ctx, "Stream", "messages_ready", map[string]interface{}{
|
||||
"message_count": len(chatMessages),
|
||||
"system_prompt": chatMessages[0].Content,
|
||||
"user_content": chatMessages[len(chatMessages)-1].Content,
|
||||
})
|
||||
logger.Infof(ctx, "user message: %s", chatMessages[len(chatMessages)-1].Content)
|
||||
// EventBus is required for event-driven streaming
|
||||
if chatManage.EventBus == nil {
|
||||
pipelineError(ctx, "Stream", "eventbus_missing", map[string]interface{}{
|
||||
|
||||
Reference in New Issue
Block a user