Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ui/src/assets/iconfont.js

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions ui/src/components/mk-drag-upload/index.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed, useTemplateRef } from 'vue'
import { useFormItem } from 'element-plus'
import type { UploadFile, UploadFiles, UploadUserFile } from 'element-plus'
import SingleFileUpload from './SingleFileUpload.vue'
import MultipleFileUpload from './MultipleFileUpload.vue'
Expand Down Expand Up @@ -32,6 +33,9 @@ defineSlots<{
}>()

const fileList = defineModel<DragUploadFile[]>({ required: true })
/* 拖拽区边框跟随所属表单项的校验状态。 */
const { formItem } = useFormItem()
const isError = computed(() => formItem?.validateState === 'error')
const uploadingCount = computed(() => fileList.value.filter((file) => file.status === 'uploading').length)
const uploadRef = useTemplateRef<InstanceType<typeof SingleFileUpload> | InstanceType<typeof MultipleFileUpload>>('uploadRef')

Expand Down Expand Up @@ -66,6 +70,8 @@ defineExpose({ clearFiles, handleRemove, uploadingCount })
<template>
<component
:is="multiple ? MultipleFileUpload : SingleFileUpload"
class="mk-drag-upload"
:class="{ 'is-error': isError }"
v-bind="multiple ? { mode } : {}"
ref="uploadRef"
v-model="fileList"
Expand All @@ -85,3 +91,9 @@ defineExpose({ clearFiles, handleRemove, uploadingCount })
<template v-if="$slots.download" #download="{ file }"><slot name="download" :file="file" /></template>
</component>
</template>

<style scoped lang="scss">
.mk-drag-upload.is-error :deep(.el-upload-dragger) {
border-color: var(--el-color-danger);
}
</style>
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, useTemplateRef, watch } from 'vue'
import { downloadByURL } from '@/utils/common'
import { useRightSidebarStore } from '../../index'
import { createFullscreenToggle, createPdfRenderer } from './index'

Expand All @@ -9,11 +10,16 @@ const { knowledgeSource, closeRightSide } = useRightSidebarStore()
const { previewDocument, closePreview } = knowledgeSource
const { loading, error, render, destroy } = createPdfRenderer()
const fullscreen = createFullscreenToggle()
const { isFullscreen } = fullscreen

const viewerRef = useTemplateRef<HTMLElement>('viewerRef')
const pagesRef = useTemplateRef<HTMLElement>('pagesRef')

// 下载当前预览的 PDF 原文件
const downloadDocument = () => {
const document = previewDocument.value
if (document?.url) downloadByURL(document.url, document.name)
}

// 挂载及切换文档时重新绘制,卸载时释放 pdf.js 资源
onMounted(() => {
if (viewerRef.value) fullscreen.bind(viewerRef.value)
Expand Down Expand Up @@ -42,13 +48,20 @@ onBeforeUnmount(() => {
</el-button>
<h4 class="min-w-0 truncate" :title="previewDocument?.name">{{ previewDocument?.name }}</h4>
</div>
<div class="flex-align-center shrink-0">
<div class="flex-align-center gap-2 shrink-0">
<!-- 下载 -->
<MkTooltip content="下载">
<el-button text :disabled="!previewDocument?.url" @click="downloadDocument">
<MkIcon name="icon_download_outlined" size="18" />
</el-button>
</MkTooltip>
<!-- 全屏预览 -->
<MkTooltip :content="isFullscreen ? '退出全屏' : '全屏'">
<el-button text @click="fullscreen.toggle()">
<MkIcon :name="isFullscreen ? 'icon_collapse_outlined' : 'icon_expand_outlined'" size="18" />
<MkTooltip content="全屏">
<el-button text @click="fullscreen.toggle()" class="ml-0!">
<MkIcon name="icon_window-new_outlined" size="18" />
</el-button>
</MkTooltip>
<el-divider direction="vertical" class="mx-1!" />
<!-- 关闭文档预览 -->
<el-button text @click="closeRightSide()">
<MkIcon name="icon_close_outlined" size="18" />
Expand Down
3 changes: 2 additions & 1 deletion ui/src/views/VIEW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,8 @@ Chat 标题栏的新建对话按钮独立展示,不参与操作计数和收纳
### 对话导出

Chat/Portal 标题栏通过 `header/header-actions/export/ExportAction.vue` 导出当前已加载消息,不自动补查历史,
无消息、加载或生成中禁用。桌面为图标格式菜单,移动端收纳后为“更多 → 导出 → 格式”。
无消息、加载或生成中禁用。桌面为图标格式菜单,移动端收纳后为“更多 → 导出 → 格式”;子菜单优先向左展开并对齐导出行,
右侧作为空间不足时的回退,支持悬停和点击打开,展开时保留导出行高亮。
Markdown/HTML 从消息数据生成,跳过思考与工具参数;HTML 下载前清洗,HTML 与图表内容项以代码保留。
PDF/图片读取所属主面板的消息区生成预览,排除消息操作栏;沙箱 iframe 通过已有 snapshot 协议截图,
失败提示重试,不静默导出空白内容。PDF 逐页裁切下载,预览支持 PNG 下载。
Expand Down
4 changes: 2 additions & 2 deletions ui/src/workflow-canvas/component/NodeSearch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ onBeforeUnmount(() => {
</script>

<template>
<!-- 打开节点搜索 -->
<el-button text :class="{ 'bg-primary/10! text-primary!': searchVisible }" @click="openSearch">
<!-- 切换节点搜索 -->
<el-button text :class="{ 'bg-primary/10! text-primary!': searchVisible }" @click="searchVisible ? closeSearch() : openSearch()">
<MkIcon name="icon_search-outlined" size="18" />
</el-button>
<el-card v-if="searchVisible" shadow="always" class="workflow-search-container" style="--el-card-padding: 8px 12px">
Expand Down
33 changes: 15 additions & 18 deletions ui/src/workflow-canvas/nodes/application-node/details/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import DetailContainer from '@/workflow-canvas/execution-details/DetailContainer.vue'
import BaseHeader from '@/workflow-canvas/execution-details/BaseHeader.vue'
import { getFileIconUrl } from '@/utils/icon'
import { downloadByURL } from '@/utils/common'
import type { ExecutionNodeDetail } from '@/workflow-canvas/execution-details/types'

defineOptions({ name: 'ApplicationNodeDetail' })
Expand Down Expand Up @@ -34,15 +35,13 @@ defineProps<{
<p class="mb-1 text-N600">文档:</p>
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
<template v-for="(f, i) in data.document_list" :key="i">
<!-- 在新标签页打开或下载附件 -->
<a :href="f.url || undefined" target="_blank" rel="noopener noreferrer" :download="f.name" class="block min-w-0">
<el-card shadow="never" class="small cursor-pointer">
<div class="flex-align-center gap-2">
<img :src="getFileIconUrl(f?.name)" alt="" width="24" />
<span class="min-w-0 truncate" :title="f?.name">{{ f?.name }}</span>
</div>
</el-card>
</a>
<!-- 下载文档附件 -->
<el-card shadow="never" class="small cursor-pointer" @click="f.url && downloadByURL(f.url, f.name)">
<div class="flex-align-center gap-2">
<img :src="getFileIconUrl(f?.name)" alt="" width="24" />
<span class="min-w-0 truncate" :title="f?.name">{{ f?.name }}</span>
</div>
</el-card>
</template>
</div>
</div>
Expand Down Expand Up @@ -100,15 +99,13 @@ defineProps<{
<p class="mb-1 text-N600">其他:</p>
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
<template v-for="(f, i) in data.other_list" :key="i">
<!-- 在新标签页打开或下载附件 -->
<a :href="f.url || undefined" target="_blank" rel="noopener noreferrer" :download="f.name" class="block min-w-0">
<el-card shadow="never" class="small cursor-pointer">
<div class="flex-align-center gap-2">
<img :src="getFileIconUrl(f?.name)" alt="" width="24" />
<span class="min-w-0 truncate" :title="f?.name">{{ f?.name }}</span>
</div>
</el-card>
</a>
<!-- 下载其他附件 -->
<el-card shadow="never" class="small cursor-pointer" @click="f.url && downloadByURL(f.url, f.name)">
<div class="flex-align-center gap-2">
<img :src="getFileIconUrl(f?.name)" alt="" width="24" />
<span class="min-w-0 truncate" :title="f?.name">{{ f?.name }}</span>
</div>
</el-card>
</template>
</div>
</div>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is an analysis of the code changes, covering irregularities, potential issues, and optimization suggestions.

1. Functional Behavior Change

  • Shift to Explicit Download: The code changed from using standard browser behavior (opening in a new tab or downloading via a[download]) to explicitly triggering a download via a utility function downloadByURL.
    • Pros: This ensures files are always downloaded rather than opened in the browser, which solves cross-origin file viewing issues.
    • Cons: It removes the ability for users to instantly preview files (like PDFs or images) in a new tab. If this was an intentional product decision, it is fine. If not, it’s a regression.

2. Potential Issues & Risks

  • Implementation Dependency (downloadByURL):

    • Browser Compatibility: Ensure downloadByURL handles cross-origin URLs correctly. Modern browsers block the download attribute on cross-origin anchor tags. A common approach is fetch + Blob or navigating to a dedicated backend endpoint. If downloadByURL just uses a hidden <a> tag similar to the previous code but forced via JS, it will fail for cross-origin files.
    • Performance: If downloadByURL uses fetch() to get a Blob, large document lists will consume significant memory and bandwidth simultaneously if clicked rapidly. There is no apparent debouncing or loading state shown.
  • Security/Logic Gap:

    • Both templates now use @click="f.url && downloadByURL(...)".
    • If f.url is undefined/falsy, nothing happens when clicked.
    • Previously, the cursor styling (cursor-pointer) was inside the card, implying interactivity even if the link was broken. Now, you apply cursor-pointer to the wrapper unconditionally, creating a "dead zone" where the UI looks clickable but does nothing if the data is malformed. Consider adding a condition to disable interaction if !f.url.

3. Irregularities / Code Style

  • Repeated Logic & Duplication:

    • You have two separate loops (data.document_list and data.other_list) with identical markup and logic.
    • This violates DRY (Don't Repeat Yourself). If you need to change how a file item looks or behaves, you now have to update it in two places.
  • Semantic HTML Loss:

    • Replacing <a> with a div-based element (inside el-card) removes native accessibility semantics related to links. While less critical since the action isn't navigation (it's a download), it breaks right-click functionality ("Open link in tabs") if the user expects standard web behaviors.

4. Optimization Suggestions

1. Extract a Component

Since both sections render exactly the same component structure: create a reusable functional component or simple SFC.

Before: Two nearly identical template blocks.
After:

<template v-for="(f, i) in data.document_list" :key="i">
  <AttachmentItem 
    :file="f" 
    :download-action="handleDownload" 
  />
</template>

<template v-for="(f, i) in data.other_list" :key="i">
  <AttachmentItem 
    :file="f" 
    :download-action="handleDownload" 
  />
</template>

2. Better Event Handling

Instead of complex expressions in the template @click, define a handler in the script for cleaner separation of concerns and easier debugging (e.g., error handling if download fails).

Template:

<el-card shadow="never" class="small cursor-pointer" @click="handleDownload(f)">...

Script:

const handleDownload = (file: { url?: string; name?: string }) => {
  if (!file.url) return;
  try {
    downloadByURL(file.url, file.name || 'untitled_file');
  } catch (error) {
    console.error('Download failed', error);
    // trigger toast notification maybe?
  }
}

3. Conditional Rendering Improvement

Fix the "Dead Zone" issue by conditionally binding classes or rendering:

Current (Problematic):

<el-card ... class="small cursor-pointer" @click="f.url && download...">

Suggested:

<!-- Only show pointer if valid -->
<el-card 
  shadow="never" 
  :class="{ small: true, 'cursor-pointer': !!f.url }" 
  @click="f.url && handleDownload(f)"
>
  <!-- Optionally add tooltip if url missing -->
  <span v-if="!f.url">No URL provided</span>
</el-card>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { computed } from 'vue'
import DetailContainer from '@/workflow-canvas/execution-details/DetailContainer.vue'
import BaseHeader from '@/workflow-canvas/execution-details/BaseHeader.vue'
import { getFileUrl } from '@/utils/common'
import { downloadByURL, getFileUrl } from '@/utils/common'
import { getFileIconUrl } from '@/utils/icon'
import type { ExecutionNodeDetail } from '@/workflow-canvas/execution-details/types'

Expand Down Expand Up @@ -30,21 +30,18 @@ const fileList = computed<SourceFile[]>(() => (props.data?.file_list as SourceFi
<h6 class="mb-2">文件列表</h6>
<div v-if="fileList.length > 0" class="grid grid-cols-1 gap-2 sm:grid-cols-2">
<template v-for="(file, index) in fileList" :key="file.file_id || index">
<!-- 在新标签页打开或下载文件 -->
<a
:href="file.file_id ? getFileUrl(file.file_id) : undefined"
target="_blank"
rel="noopener noreferrer"
:download="file.name"
class="block min-w-0"
<!-- 下载来源文件 -->
<el-card
shadow="never"
class="small min-w-0"
:class="{ 'cursor-pointer': file.file_id }"
@click="file.file_id && downloadByURL(getFileUrl(file.file_id), file.name || '')"
>
<el-card shadow="never" class="small" :class="{ 'cursor-pointer': file.file_id }">
<div class="flex-align-center gap-2">
<img :src="getFileIconUrl(file.name || '')" alt="" width="24" />
<span class="min-w-0 truncate" :title="file.name">{{ file.name || '-' }}</span>
</div>
</el-card>
</a>
<div class="flex-align-center gap-2">
<img :src="getFileIconUrl(file.name || '')" alt="" width="24" />
<span class="min-w-0 truncate" :title="file.name">{{ file.name || '-' }}</span>
</div>
</el-card>
</template>
</div>
<template v-else>-</template>
Expand Down
52 changes: 25 additions & 27 deletions ui/src/workflow-canvas/nodes/reranker-node/details/index.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import DetailContainer from '@/workflow-canvas/execution-details/DetailContainer.vue'
import BaseHeader from '@/workflow-canvas/execution-details/BaseHeader.vue'
import { getFileUrl } from '@/utils/common'
import { downloadByURL, getFileUrl } from '@/utils/common'
import { getFileIconUrl } from '@/utils/icon'
import { DOCUMENT_RESOURCE_TYPE } from '@/api/enums'
import type { ExecutionNodeDetail } from '@/workflow-canvas/execution-details/types'
Expand All @@ -11,6 +11,12 @@ defineOptions({ name: 'RerankerNodeDetail' })
defineProps<{
data: ExecutionNodeDetail
}>()

// 来源文件按原文件名下载,网页来源打开原网址。
function openSourceDocument(name: string, fileId?: string, sourceUrl?: string) {
if (fileId) downloadByURL(getFileUrl(fileId), name.trim())
else if (sourceUrl && /^https?:\/\//i.test(sourceUrl.trim())) window.open(sourceUrl.trim(), '_blank', 'noopener,noreferrer')
}
</script>

<template>
Expand Down Expand Up @@ -50,21 +56,17 @@ defineProps<{
<MdPreview :model-value="paragraph.page_content || paragraph.metadata?.content || ''" no-img-zoom-in />

<!-- 打开来源文档 -->
<a
<el-card
v-if="paragraph.metadata?.document_name?.trim()"
:href="getFileUrl(paragraph.metadata.source_file_id) || paragraph.metadata.source_url || undefined"
target="_blank"
rel="noopener noreferrer"
:download="paragraph.metadata.document_name.trim()"
class="mt-2 block min-w-0"
shadow="never"
class="small cursor-pointer mt-2 min-w-0"
@click="openSourceDocument(paragraph.metadata.document_name, paragraph.metadata?.source_file_id, paragraph.metadata?.source_url)"
>
<el-card shadow="never" class="small cursor-pointer">
<div class="flex-align-center gap-2">
<img :src="getFileIconUrl(paragraph.metadata.document_name.trim(), paragraph.metadata.knowledge_type)" alt="" width="24" />
<span class="min-w-0 truncate" :title="paragraph.metadata.document_name.trim()">{{ paragraph.metadata.document_name.trim() }}</span>
</div>
</el-card>
</a>
<div class="flex-align-center gap-2">
<img :src="getFileIconUrl(paragraph.metadata.document_name.trim(), paragraph.metadata.knowledge_type)" alt="" width="24" />
<span class="min-w-0 truncate" :title="paragraph.metadata.document_name.trim()">{{ paragraph.metadata.document_name.trim() }}</span>
</div>
</el-card>

<template #footer>
<div class="flex-align-center w-full gap-2 border-t pt-2">
Expand Down Expand Up @@ -104,21 +106,17 @@ defineProps<{
<MdPreview :model-value="paragraph.page_content || paragraph.metadata?.content || ''" no-img-zoom-in />

<!-- 打开来源文档 -->
<a
<el-card
v-if="paragraph.metadata?.document_name?.trim()"
:href="getFileUrl(paragraph.metadata.source_file_id) || paragraph.metadata.source_url || undefined"
target="_blank"
rel="noopener noreferrer"
:download="paragraph.metadata.document_name.trim()"
class="mt-2 block min-w-0"
shadow="never"
class="small cursor-pointer mt-2 min-w-0"
@click="openSourceDocument(paragraph.metadata.document_name, paragraph.metadata?.source_file_id, paragraph.metadata?.source_url)"
>
<el-card shadow="never" class="small cursor-pointer">
<div class="flex-align-center gap-2">
<img :src="getFileIconUrl(paragraph.metadata.document_name.trim(), paragraph.metadata.knowledge_type)" alt="" width="24" />
<span class="min-w-0 truncate" :title="paragraph.metadata.document_name.trim()">{{ paragraph.metadata.document_name.trim() }}</span>
</div>
</el-card>
</a>
<div class="flex-align-center gap-2">
<img :src="getFileIconUrl(paragraph.metadata.document_name.trim(), paragraph.metadata.knowledge_type)" alt="" width="24" />
<span class="min-w-0 truncate" :title="paragraph.metadata.document_name.trim()">{{ paragraph.metadata.document_name.trim() }}</span>
</div>
</el-card>

<template #footer>
<div class="flex-align-center w-full gap-2 border-t pt-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { computed } from 'vue'
import ImageUploadCard from '@/components/mk-drag-upload/ImageUploadCard.vue'
import DetailContainer from '@/workflow-canvas/execution-details/DetailContainer.vue'
import BaseHeader from '@/workflow-canvas/execution-details/BaseHeader.vue'
import { getFileUrl } from '@/utils/common'
import { downloadByURL, getFileUrl } from '@/utils/common'
import { getFileIconUrl } from '@/utils/icon'
import { DOCUMENT_RESOURCE_TYPE, KNOWLEDGE_HIT_UNIT_TYPE } from '@/api/enums'
import type { DocumentResourceType, KnowledgeHitTestAsset, KnowledgeType } from '@/api/types'
Expand Down Expand Up @@ -56,6 +56,12 @@ function getParagraphImageUrl(paragraph: SearchParagraph) {
if (!fileId || paragraph.content?.includes(`/oss/file/${fileId}`)) return ''
return getFileUrl(fileId)
}

// 来源文件按原文件名下载,网页来源打开原网址。
function openSourceDocument(name: string, fileId?: string, sourceUrl?: string) {
if (fileId) downloadByURL(getFileUrl(fileId), name.trim())
else if (sourceUrl && /^https?:\/\//i.test(sourceUrl.trim())) window.open(sourceUrl.trim(), '_blank', 'noopener,noreferrer')
}
</script>

<template>
Expand Down Expand Up @@ -120,32 +126,27 @@ function getParagraphImageUrl(paragraph: SearchParagraph) {
<MdPreview :model-value="paragraph.content" no-img-zoom-in />

<!-- 打开来源文档 -->

<a
<el-card
v-if="paragraph.document_name?.trim()"
:href="paragraph.meta?.source_file_id || paragraph.meta?.source_url || undefined"
target="_blank"
rel="noopener noreferrer"
:download="paragraph.document_name.trim()"
class="mt-2 block min-w-0"
shadow="never"
class="small cursor-pointer mt-2 min-w-0"
@click="openSourceDocument(paragraph.document_name, paragraph.meta?.source_file_id, paragraph.meta?.source_url)"
>
<el-card shadow="never" class="small cursor-pointer">
<div class="flex-align-center gap-2">
<img
:src="
getFileIconUrl(
paragraph.document_name.trim(),
paragraph.knowledge_type,
paragraph.document_resource_type === DOCUMENT_RESOURCE_TYPE.IMAGE,
)
"
alt=""
class="size-6 shrink-0"
/>
<span class="min-w-0 truncate" :title="paragraph.document_name.trim()">{{ paragraph.document_name.trim() }}</span>
</div>
</el-card>
</a>
<div class="flex-align-center gap-2">
<img
:src="
getFileIconUrl(
paragraph.document_name.trim(),
paragraph.knowledge_type,
paragraph.document_resource_type === DOCUMENT_RESOURCE_TYPE.IMAGE,
)
"
alt=""
class="size-6 shrink-0"
/>
<span class="min-w-0 truncate" :title="paragraph.document_name.trim()">{{ paragraph.document_name.trim() }}</span>
</div>
</el-card>

<template #footer>
<div class="flex-align-center w-full gap-2 border-t pt-2">

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the code review with identified irregularities, potential issues, and optimization suggestions:

1. Critical Logic/Performance Issue: Repeated trim() Calls

  • Issue: paragraph.document_name?.trim() is called 6 times in the template (and once in the script). This is inefficient and bad practice. If document_name is long, this creates unnecessary function call overhead.
  • Fix: Compute it once or use a local variable/constant. However, since this is in a v-for loop (implied by paragraph), you cannot create a top-level constant. The best Vue 3 approach is to ensure the data is clean before entering the template, or accept the minor overhead if strictly necessary. Ideally, trim in the backend or when fetching data. If not possible, at least reduce redundancy where feasible.

2. Accessibility (a11y) Regression

  • Issue: The previous implementation used an <a> tag, which is semantically correct for links/downloads and accessible via keyboard/keyboard navigation (tabindex). The new <el-card @click="..."> is a <div> under the hood. It is not keyboard-accessible by default. Users using keyboards cannot trigger the action.
  • Fix: Add @keydown.enter="openSourceDocument(...)", @keydown.space="...", tabindex="0", and role="button" to the el-card. Or, keep <a> and style it like a card.

3. Security: URL Validation Weakness

  • Issue: The regex /^https?:\/\//i.test(sourceUrl.trim()) only checks the prefix. It does not validate if the URL is safe (e.g., blocks javascript: or malformed URIs that might bypass simple prefix checks in edge cases). While better than nothing, URL.canParse() (if available) or a more robust validation is preferred. Also, .trim() is called twice on sourceUrl.
  • Optimization: Validate once and store safely. Consider whitelisting domains if applicable.

4. Potential Bug: Missing fileId vs sourceUrl Priority

  • Issue: In openSourceDocument, if both fileId and sourceUrl are present, it only downloads the file. Is this intended? Previously, the <a> tag prioritized source_file_id too, so behavior is consistent, but ensure this matches business logic.
  • Note: No change needed if intentional.

5. Code Quality & Readability

  • Suggestion: The getFileIconUrl call inside the template is heavy. While not easily avoidable without refactoring the component structure, be aware that it runs on every render.
  • Suggestion: The openSourceDocument function mixes concerns (download logic + window.open logic). Keep it as is for now, but consider separating UI event handling from business logic if the component grows.

✅ Recommended Refactored Code Template Section:

<!-- In <script setup> -->
// Add a helper to reduce trim calls if possible, but primarily fix a11y in template.

// In <template>
<el-card
  v-if="paragraph.document_name?.trim()"
  shadow="never"
  class="small cursor-pointer mt-2 min-w-0 focus-visible:ring-2 focus-visible:ring-blue-500"
  role="button"
  tabindex="0"
  :aria-label="`Open ${paragraph.document_name.trim()}`"
  @click="handleOpenSource(paragraph)"
  @keydown.enter.prevent="handleOpenSource(paragraph)"
  @keydown.space.prevent="handleOpenSource(paragraph)"
>
  <div class="flex-align-center gap-2">
    <img
      :src="getFileIconUrl(
        paragraph.document_name.trim(),
        paragraph.knowledge_type,
        paragraph.document_resource_type === DOCUMENT_RESOURCE_TYPE.IMAGE,
      )"
      alt=""
      class="size-6 shrink-0"
    />
    <!-- Use computed property or v-text if name is very long -->
    <span class="min-w-0 truncate" :title="paragraph.document_name.trim()">
      {{ paragraph.document_name.trim() }}
    </span>
  </div>
</el-card>

✅ Recommended Refactored Script Function:

/**
 * Opens source document.
 * Prioritizes file download if fileId exists, otherwise opens URL.
 */
function handleOpenSource(paragraph: SearchParagraph) {
  const name = paragraph.document_name?.trim() || '';
  if (!name) return; // Safety check, though v-if should prevent clicking empty names

  const fileId = paragraph.meta?.source_file_id;
  const sourceUrl = paragraph.meta?.source_url?.trim();

  if (fileId) {
    const url = getFileUrl(fileId);
    if (url) {
      downloadByURL(url, name);
    }
  } else if (sourceUrl) {
    /**
     * Security Note: Ensure sourceUrl is trusted or validated.
     * Regex prevents common malicious schemes, but doesn't protect against all phishing.
     */
    if (/^https?:\/\//i.test(sourceUrl)) {
      window.open(sourceUrl, '_blank', 'noopener,noreferrer');
    } else {
      console.warn('Invalid source URL provided:', sourceUrl);
    }
  }
}

Summary of Changes:

  1. Added Keyboard Accessibility (tabindex, role, @keydown).
  2. Extracted handler handleOpenSource for cleaner separation and reusability.
  3. Added null safety inside the handler for FileId/Url.
  4. Added warning log for invalid URLs to help debugging.
  5. Removed redundant trim() where logically safe (in the new handler).

Expand Down
Loading
Loading