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
12 changes: 10 additions & 2 deletions ui/src/conversation-panel/main/chat-panel/index.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue'
import ChatHeader from '../../components/header/chat/index.vue'
import MessageList from '../../components/message-list/index.vue'
import MessageInput from '../../components/message-input/index.vue'
Expand All @@ -16,20 +17,27 @@ const list = useConversationListStore()
const input = useMessageInputStore()
// 拖拽到面板任意处 → 交给 message-input 接收文件
const handleDrop = (e: DragEvent) => input.addFiles(e.dataTransfer?.files)
// 开启免责声明时在输入框下方居中显示,与显示设置预览一致
const disclaimerText = computed(() => (list.appInfo.value?.disclaimer ? list.appInfo.value.disclaimer_value : ''))
</script>
<template>
<!-- 带输入框的主面板:header + message-list + message-input -->
<main class="mk-conversation-main" @drop.prevent="handleDrop" @dragover.prevent>
<!-- 头部可被替换(如 debug 用 debug header);默认普通 chat header -->
<slot name="header"><ChatHeader :shareable="shareable" @share="emit('share')" /></slot>
<slot name="header">
<ChatHeader :shareable="shareable" @share="emit('share')" />
</slot>
<!-- 聊天背景铺满标题栏以下的消息区与输入区,与显示设置预览一致 -->
<div
class="flex-column min-h-0 flex-1 bg-cover bg-center"
:style="{ backgroundImage: list.appInfo.value?.chat_background ? `url(${JSON.stringify(list.appInfo.value.chat_background)})` : undefined }"
>
<MessageList :key="list.currentChatId.value" :app-name="list.appInfo.value?.name || 'AI 助手'"></MessageList>

<footer v-if="!list.isShared.value" class="mk-conversation-content flex-center shrink-0 p-4"><MessageInput /></footer>
<footer v-if="!list.isShared.value" class="mk-conversation-content flex-col-center shrink-0 gap-2 p-4">
<MessageInput />
<p v-if="disclaimerText" class="wrap-anywhere text-center text-sm text-N500 -mb-1">{{ disclaimerText }}</p>
</footer>
</div>
</main>
</template>

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.

Review

1. Correctness / Type safety

disclaimerText computed is fragile

const disclaimerText = computed(() => (list.appInfo.value?.disclaimer ? list.appInfo.value.disclaimer_value : ''))
  • If disclaimer is truthy but disclaimer_value is null/undefined, the computed returns undefined (type becomes string | undefined), not ''. Add ?? ''.
  • Backend flags are often 0/1 or "true"; truthiness may not match intent. Normalize explicitly if the API is not strictly boolean.
  • Whitespace-only values will pass v-if. Trim.
  • Optional-chaining is used on .disclaimer but not on .disclaimer_value — TS narrowing happens to cover this today, but it's brittle if the expression is refactored.

Suggested:

const disclaimerText = computed(() => {
  const info = list.appInfo.value
  return info?.disclaimer ? (info.disclaimer_value ?? '').trim() : ''
})

Duplicated business rule — the comment says this must match the “display settings preview”. That rule now lives in two places and will drift. Extract it into a shared helper/composable (e.g. useDisclaimerText() / getDisclaimerText(appInfo)) and reuse it in the preview component.

2. Template / store access

  • list.appInfo.value?.… and list.currentChatId.value are used in the template. That only works if list is a plain object holding refs. If list is ever converted to a Pinia/composable store with auto-unwrapped refs, all of these silently break (.value becomes undefined). Consider consistently exposing plain computed values instead, e.g. const backgroundImage = computed(...).
  • :style runs JSON.stringify(...) on every render. Cheap, but a computed is cleaner and gives you one place to sanitize.
  • JSON.stringify is not a CSS sanitizer. url(${JSON.stringify(url)}) will produce invalid CSS for URLs containing newlines/CRs (JSON escapes them to \n, which CSS interprets as a literal n). Prefer validating/encoding:
    const backgroundImage = computed(() => {
      const url = list.appInfo.value?.chat_background
      return url ? `url("${encodeURI(url).replace(/"/g, '%22')}")` : undefined
    })
    Also consider that a background-image value coming from server data is a mild CSS-injection surface; restricting to http(s):/data:image is safer.
  • <MessageList ...></MessageList> can be self-closed for brevity.

3. Layout / UX

  • Verify flex-col-center exists in your utility layer. If it doesn't (or resolves to display:flex; align-items:center; justify-content:center without flex-direction: column), the input and disclaimer will sit side by side.
  • In a column, justify-center on the footer would center the input vertically — check that flex-col-center doesn't unintentionally add vertical centering now that the footer has two children.
  • text-N500 over a user-provided chat_background image may have poor contrast. Consider a subtle backdrop (bg-surface/60, a blur, or a text-shadow) for readability.
  • -mb-1 is a magic offset to counteract p-4; if the padding changes, the alignment breaks. Prefer adjusting the footer padding or using explicit spacing.
  • Consider select-text on the <p> if the disclaimer contains links/legal text users may want to copy, and role="note" for semantics.
  • {{ disclaimerText }} is auto-escaped — good. If the content is ever meant to be rich text (markdown/HTML), do not switch to v-html without sanitizing.
  • Note the disclaimer lives inside <main @drop>, so dropping a file on the disclaimer also adds an attachment. Probably intended, but worth confirming.

4. Code style / minor

  • import { computed } from 'vue' — fine, unless the project uses unplugin-auto-import; then this import is redundant and may fail the lint rule. Check sibling components for consistency.
  • Slot reformatting is purely cosmetic — no issue.
  • Long :style line exceeds typical prettier width; a computed would also fix readability.
  • Naming: disclaimer_value is snake_case (backend shape) mixed with camelCase locals. Consider mapping to camelCase at the store boundary.

Suggested revision

const disclaimerText = computed(() => {
  const info = list.appInfo.value
  return info?.disclaimer ? (info.disclaimer_value ?? '').trim() : ''
})

const backgroundImage = computed(() => {
  const url = list.appInfo.value?.chat_background
  return url ? `url("${encodeURI(url).replace(/"/g, '%22')}")` : undefined
})
<footer v-if="!list.isShared.value" class="mk-conversation-content flex-col-center shrink-0 gap-2 p-4">
  <MessageInput />
  <p v-if="disclaimerText" role="note" class="wrap-anywhere text-center text-sm text-N500">
    {{ disclaimerText }}
  </p>
</footer>

11 changes: 10 additions & 1 deletion ui/src/conversation-panel/main/portal-chat-panel/index.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useShareLinkStoreOptional } from '../../components/message-list/share/index'
import PortalHeader from '../../components/header/portal/index.vue'
import MessageList from '../../components/message-list/index.vue'
Expand All @@ -16,6 +17,11 @@ const portalList = usePortalConversationListStore()

// 拖拽到面板任意处 → 交给 message-input 接收文件
const handleDrop = (e: DragEvent) => input.addFiles(e.dataTransfer?.files)

// 当前智能体开启免责声明时在输入框下方居中显示,与显示设置预览一致
const disclaimerText = computed(() =>
portalList.currentApplicationId.value && list.appInfo.value?.disclaimer ? list.appInfo.value.disclaimer_value : '',
)
</script>
<template>
<!-- 门户对话主面板:header + message-list + message-input -->
Expand All @@ -27,6 +33,9 @@ const handleDrop = (e: DragEvent) => input.addFiles(e.dataTransfer?.files)
<div v-else class="flex-center min-h-0 flex-1">
<MkEmpty v-if="portalList.isApplicationGroupsLoaded.value" description="暂无可用智能体" />
</div>
<footer v-if="!list.isShared.value" class="mk-conversation-content flex-center shrink-0 p-4"><MessageInput /></footer>
<footer v-if="!list.isShared.value" class="mk-conversation-content flex-col-center shrink-0 gap-2 p-4">
<MessageInput />
<p v-if="disclaimerText" class="wrap-anywhere text-center text-sm text-N500 -mb-1">{{ disclaimerText }}</p>
</footer>
</main>
</template>

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.

Review

1. Logic / correctness

  • currentApplicationId is used only as a truthiness gate – it doesn't actually select the disclaimer, yet it silently suppresses the text when the id is falsy (0, '', undefined). Either make the dependency explicit (e.g. gate on appInfo.id === currentApplicationId) or drop the check entirely. As written, it reads like a bug magnet.
  • Possible stale text when switching agents – appInfo may still hold the previous agent's data during the transition, so users could briefly see another agent's disclaimer. Consider resetting/validating against the current agent, or reading the disclaimer from the same source that renders the current conversation.
  • Empty/whitespace disclaimer_value – if disclaimer is true but the value is "" or " ", the <p> still renders (in the whitespace case). .trim() before returning.
  • Return type is string | undefined, since disclaimer_value may be optional. It works with v-if, but if you want a clean string contract, coalesce:
    const disclaimerText = computed(() => {
      const info = list.appInfo.value
      if (!portalList.currentApplicationId.value || !info?.disclaimer) return ''
      return info.disclaimer_value?.trim() ?? ''
    })
    This also avoids repeatedly poking .value and keeps the truthiness check shallow.

2. Template / CSS

  • flex-center + flex-col-center on the same element is suspicious – the two utilities likely both set display:flex/align-items, so the result depends on CSS order/specificity. Use only flex-col-center (or flex flex-col items-center gap-2) to avoid a silently order-dependent layout.
  • Input width regression risk – flex-col-center presumably means align-items: center, so <MessageInput /> may no longer stretch to full width the way it did under the row-oriented flex-center. Add w-full / self-stretch to <MessageInput /> (or items-stretch on the footer and only center the <p>), and verify on wide screens.
  • -mb-1 is a hack – negative margins combined with the parent's p-4 are hard to reason about and can clip/overflow when the footer participates in a flex layout with min-h-0. Prefer adjusting padding (px-4 pt-4 pb-3) or use mt-1 on the <p>.
  • Verify these class names exist/case-match your token system: flex-col-center, wrap-anywhere (Tailwind has break-words / [overflow-wrap:anywhere]), text-N500 (tokens are often lowercase, e.g. text-n500). Typos here fail silently.
  • Multi-line disclaimers – if the legal text contains \n, it will collapse. Add whitespace-pre-line if line breaks are expected.
  • Accessibility/semantics – consider role="note" (or aria-live="polite" if the text can change while visible) on the <p>, and text-balance/max-w-prose so long disclaimers don't span the full panel width awkwardly.

3. Readability / minor

  • Extract a small DisclaimerText component or at least an i18n key if the string ever needs localization (currently dynamic, so likely fine, but the surrounding patterns are worth checking).
  • disclaimer_value (snake_case) vs disclaimer (camelCase) is an API-shape inconsistency; if you have a mapper layer, normalize it there rather than in the component.
  • The comment is helpful; consider noting why currentApplicationId is part of the condition (that's the non-obvious part).

Suggested version

const disclaimerText = computed(() => {
  const info = list.appInfo.value
  if (!portalList.currentApplicationId.value || !info?.disclaimer) return ''
  return info.disclaimer_value?.trim() ?? ''
})
<footer
  v-if="!list.isShared.value"
  class="mk-conversation-content flex-col-center shrink-0 gap-2 px-4 pt-4 pb-3"
>
  <MessageInput class="w-full" />
  <p
    v-if="disclaimerText"
    class="wrap-anywhere text-center text-sm text-N500 whitespace-pre-line"
    role="note"
  >
    {{ disclaimerText }}
  </p>
</footer>

Main risks to verify before merging: the flex-center/flex-col-center combination, MessageInput losing full width, and the existence of the wrap-anywhere / text-N500 classes.

Loading