feat(frontend): polish chat suggestions, citations, and drawers
- unify suggested-question styling across chat entry points - keep citation markers inline with their preceding list content - prevent manual editor viewport shifts and synchronize drawer motion - align embedded preview and editor drawers in the body stacking context
This commit is contained in:
@@ -37,6 +37,7 @@ data/files/
|
|||||||
data/weknora.db
|
data/weknora.db
|
||||||
data/weknora.db-wal
|
data/weknora.db-wal
|
||||||
data/weknora.db-shm
|
data/weknora.db-shm
|
||||||
|
skills-lock.json
|
||||||
|
|
||||||
web/
|
web/
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<t-drawer :visible="visible" :header="title || $t('embedPublish.preview')" size="720px" :footer="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)">
|
:z-index="2600" attach="body" class="embed-preview-drawer" @close="emit('update:visible', false)">
|
||||||
<div class="preview-body">
|
<div class="preview-body">
|
||||||
<template v-if="mode === 'iframe'">
|
<template v-if="mode === 'iframe'">
|
||||||
<p class="preview-hint">{{ $t('embedPublish.previewIframeHint') }}</p>
|
<p class="preview-hint">{{ $t('embedPublish.previewIframeHint') }}</p>
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
@suggested-ease: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
|
||||||
|
.suggested-questions-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px 12px;
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
&.has-questions {
|
||||||
|
min-height: 64px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggested-questions-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggested-questions-title-row {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggested-questions-caption {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggested-questions-title {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--td-text-color-placeholder);
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggested-questions-refresh {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--td-text-color-placeholder);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s @suggested-ease, background 0.2s @suggested-ease;
|
||||||
|
|
||||||
|
:deep(.t-icon) {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
color: var(--td-text-color-secondary);
|
||||||
|
background: var(--td-bg-color-secondarycontainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
transform: scale(0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sq-refresh-spin {
|
||||||
|
animation: sq-refresh-rotate 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sq-refresh-rotate {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggested-questions-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggested-question-card {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--td-component-stroke);
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background 0.2s @suggested-ease,
|
||||||
|
border-color 0.2s @suggested-ease,
|
||||||
|
transform 0.15s @suggested-ease;
|
||||||
|
|
||||||
|
&:not(.sq-card-skeleton):hover {
|
||||||
|
background: var(--td-bg-color-secondarycontainer);
|
||||||
|
border-color: var(--td-component-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.sq-card-skeleton):active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
background: var(--td-bg-color-secondarycontainer-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sq-card-skeleton {
|
||||||
|
pointer-events: none;
|
||||||
|
cursor: default;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-color: transparent;
|
||||||
|
background: var(--td-bg-color-secondarycontainer);
|
||||||
|
|
||||||
|
:deep(.t-skeleton) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-skeleton__row) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-skeleton__col) {
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&: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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggested-question-text {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--td-text-color-secondary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: color 0.2s @suggested-ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggested-question-card:not(.sq-card-skeleton):hover .suggested-question-text {
|
||||||
|
color: var(--td-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggested-question-badge {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid var(--td-component-stroke);
|
||||||
|
background: transparent;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.6;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--td-text-color-placeholder);
|
||||||
|
}
|
||||||
@@ -150,7 +150,11 @@ const setSelectionRange = (start: number, end: number) => {
|
|||||||
if (!textarea || activeTab.value !== 'edit') {
|
if (!textarea || activeTab.value !== 'edit') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
textarea.focus()
|
// Initialization can finish while the drawer is still sliding in. A plain
|
||||||
|
// focus() makes the browser scroll the transformed textarea into view,
|
||||||
|
// which intermittently shifts the drawer away from the right edge for a
|
||||||
|
// frame. Keep keyboard focus without letting it move the viewport.
|
||||||
|
textarea.focus({ preventScroll: true })
|
||||||
textarea.setSelectionRange(start, end)
|
textarea.setSelectionRange(start, end)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -735,7 +739,6 @@ onBeforeUnmount(() => {
|
|||||||
:min-width="560"
|
:min-width="560"
|
||||||
:max-width="1280"
|
:max-width="1280"
|
||||||
storage-key="setting-drawer:width:manual-markdown-editor"
|
storage-key="setting-drawer:width:manual-markdown-editor"
|
||||||
:destroy-on-close="false"
|
|
||||||
:hide-footer="!initialLoaded"
|
:hide-footer="!initialLoaded"
|
||||||
:confirm-loading="saving && savingAction === 'publish'"
|
:confirm-loading="saving && savingAction === 'publish'"
|
||||||
:confirm-text="$t('manualEditor.actions.publish')"
|
:confirm-text="$t('manualEditor.actions.publish')"
|
||||||
@@ -1111,5 +1114,3 @@ onBeforeUnmount(() => {
|
|||||||
color: var(--td-text-color-placeholder);
|
color: var(--td-text-color-placeholder);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<teleport to="body">
|
<teleport to="body">
|
||||||
<div v-if="drawerVisible && resizable" class="setting-drawer-resize-handle"
|
<div v-if="drawerVisible && resizable" class="setting-drawer-resize-handle"
|
||||||
:class="{ 'setting-drawer-resize-handle--active': drawerResizing }" :style="{ right: `${drawerWidthPx}px` }"
|
:class="{ 'setting-drawer-resize-handle--active': drawerResizing }"
|
||||||
|
:style="{ right: `${drawerWidthPx}px`, '--setting-drawer-travel': `${drawerWidthPx}px` }"
|
||||||
role="separator" aria-orientation="vertical" @mousedown.prevent="onResizeStart">
|
role="separator" aria-orientation="vertical" @mousedown.prevent="onResizeStart">
|
||||||
<div class="setting-drawer-resize-line" />
|
<div class="setting-drawer-resize-line" />
|
||||||
</div>
|
</div>
|
||||||
</teleport>
|
</teleport>
|
||||||
<t-drawer v-model:visible="drawerVisible" :size="effectiveWidth" :z-index="2500" placement="right"
|
<t-drawer v-model:visible="drawerVisible" :size="effectiveWidth" :z-index="2500" placement="right"
|
||||||
:attach="attach"
|
attach="body" destroy-on-close
|
||||||
:destroy-on-close="destroyOnClose"
|
|
||||||
:class="['setting-drawer', { 'setting-drawer--resizing': drawerResizing }]">
|
:class="['setting-drawer', { 'setting-drawer--resizing': drawerResizing }]">
|
||||||
<!--
|
<!--
|
||||||
Custom header. We replace TDesign's default header so we can put a leading
|
Custom header. We replace TDesign's default header so we can put a leading
|
||||||
@@ -88,23 +88,6 @@ interface Props {
|
|||||||
confirmText?: string
|
confirmText?: string
|
||||||
cancelText?: string
|
cancelText?: string
|
||||||
hideFooter?: boolean
|
hideFooter?: boolean
|
||||||
/**
|
|
||||||
* Whether to destroy the drawer body content on close. Defaults to true to
|
|
||||||
* match historical behavior. Heavy editors (e.g. the markdown knowledge
|
|
||||||
* editor) pass false so the drawer panel persists across opens — this avoids
|
|
||||||
* the panel being removed/re-inserted on every open, which can cause a
|
|
||||||
* one-frame layout flicker as it slides in.
|
|
||||||
*/
|
|
||||||
destroyOnClose?: boolean
|
|
||||||
/**
|
|
||||||
* Where to render the drawer. Defaults to 'body' so the drawer escapes the
|
|
||||||
* app root, which carries `transform: translateZ(0)` for GPU compositing.
|
|
||||||
* A transformed ancestor becomes the containing block for `position: fixed`,
|
|
||||||
* which mis-anchors the overlay (it briefly appears mid-screen then snaps to
|
|
||||||
* the edge). Teleporting to <body> keeps it fixed to the viewport, matching
|
|
||||||
* the resize handle that is already teleported to body.
|
|
||||||
*/
|
|
||||||
attach?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -119,9 +102,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
confirmDisabled: false,
|
confirmDisabled: false,
|
||||||
confirmText: '',
|
confirmText: '',
|
||||||
cancelText: '',
|
cancelText: '',
|
||||||
hideFooter: false,
|
hideFooter: false
|
||||||
destroyOnClose: true,
|
|
||||||
attach: 'body'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -441,6 +422,20 @@ const handleCancel = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
/* The handle is teleported separately from TDesign's sliding panel. Move it
|
||||||
|
along the same path instead of letting it flash at the panel's final left
|
||||||
|
edge while the drawer is still entering from the right. */
|
||||||
|
animation: setting-drawer-resize-handle-in 0.28s cubic-bezier(0.38, 0, 0.24, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes setting-drawer-resize-handle-in {
|
||||||
|
from {
|
||||||
|
transform: translateX(var(--setting-drawer-travel));
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-drawer-resize-line {
|
.setting-drawer-resize-line {
|
||||||
|
|||||||
@@ -167,10 +167,37 @@ test('renderChatMarkdown inlines consecutive citation tags across newlines', ()
|
|||||||
assert.doesNotMatch(html, /<\/p>\s*<p>\s*<span class="citation citation-kb"/)
|
assert.doesNotMatch(html, /<\/p>\s*<p>\s*<span class="citation citation-kb"/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('joinCitationTagsToPreviousLine keeps citations on a new line after a list', () => {
|
test('joinCitationTagsToPreviousLine appends an indented citation to the preceding list item', () => {
|
||||||
const tag = '<kb doc="doc.pdf" chunk_id="1" />'
|
const tag = '<kb doc="阅读之星全国青少年阅读风采展示活动.pdf" chunk_id="chunk-1" />'
|
||||||
const input = `1. first\n2. second\n\n${tag}`
|
const input = [
|
||||||
assert.equal(joinCitationTagsToPreviousLine(input), `1. first\n2. second\n\n${tag}`)
|
'#### 5️⃣ 阅读之星培养基地',
|
||||||
|
'- 每个组别冠亚季军及前十强所在的学校,将获得 **"阅读之星培养基地"** 奖牌',
|
||||||
|
'',
|
||||||
|
` ${tag}`,
|
||||||
|
].join('\n')
|
||||||
|
assert.equal(
|
||||||
|
joinCitationTagsToPreviousLine(input),
|
||||||
|
[
|
||||||
|
'#### 5️⃣ 阅读之星培养基地',
|
||||||
|
`- 每个组别冠亚季军及前十强所在的学校,将获得 **"阅读之星培养基地"** 奖牌 ${tag}`,
|
||||||
|
].join('\n'),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('renderChatMarkdown renders a citation after a list item inline in that item', () => {
|
||||||
|
const renderer = createChatMarkdownRenderer({
|
||||||
|
imageRenderer: ({ href, text }) => `<img src="${href}" alt="${text}">`,
|
||||||
|
isValidImageUrl: () => true,
|
||||||
|
})
|
||||||
|
const tag = '<kb doc="阅读之星全国青少年阅读风采展示活动.pdf" chunk_id="chunk-1" />'
|
||||||
|
const html = renderChatMarkdown(`- 培养基地奖牌\n\n ${tag}`, {
|
||||||
|
renderer,
|
||||||
|
escapeMarkdown: (text) => text,
|
||||||
|
sanitizeHtml: (value) => value,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.match(html, /<li>培养基地奖牌 <span class="citation citation-kb"/)
|
||||||
|
assert.doesNotMatch(html, /<\/ul>\s*<p>\s*<span class="citation citation-kb"/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('joinCitationTagsToPreviousLine does not merge citations onto fenced code closing delimiter', () => {
|
test('joinCitationTagsToPreviousLine does not merge citations onto fenced code closing delimiter', () => {
|
||||||
|
|||||||
@@ -224,16 +224,15 @@ export function joinCitationTagsToPreviousLine(content: string): string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blank lines before citations: join to previous prose, but keep a break after lists
|
// Blank lines before citations: join to the previous content. Fenced-code
|
||||||
// or fenced-code delimiters (``` / ~~~ must stay on their own line).
|
// delimiters are the only exception because ``` / ~~~ must stay on their own line.
|
||||||
result = result.replace(/\n[ \t]*\n+([ \t]*<(?:kb|web)\b)/gi, (match, kbStart, offset, full) => {
|
result = result.replace(/\n[ \t]*\n+([ \t]*<(?:kb|web)\b)/gi, (match, kbStart, offset, full) => {
|
||||||
const before = full.slice(0, offset)
|
const before = full.slice(0, offset)
|
||||||
const lastLine = before.split('\n').filter((line) => line.trim()).pop() || ''
|
const lastLine = before.split('\n').filter((line: string) => line.trim()).pop() || ''
|
||||||
const isListItem = /^\s*(\d+\.|[-*+])\s+\S/.test(lastLine)
|
if (isFencedCodeDelimiterLine(lastLine)) {
|
||||||
if (isListItem || isFencedCodeDelimiterLine(lastLine)) {
|
|
||||||
return `\n\n${kbStart}`
|
return `\n\n${kbStart}`
|
||||||
}
|
}
|
||||||
return ` ${kbStart}`
|
return ` ${kbStart.trimStart()}`
|
||||||
})
|
})
|
||||||
|
|
||||||
// Single newline before citation when it follows text or another citation (not after a blank line)
|
// Single newline before citation when it follows text or another citation (not after a blank line)
|
||||||
@@ -243,7 +242,7 @@ export function joinCitationTagsToPreviousLine(content: string): string {
|
|||||||
if (isFencedCodeDelimiterLine(beforePart)) {
|
if (isFencedCodeDelimiterLine(beforePart)) {
|
||||||
return match
|
return match
|
||||||
}
|
}
|
||||||
return `${beforePart} ${kbStart}`
|
return `${beforePart} ${kbStart.trimStart()}`
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<div v-if="suggestedQuestionsLoading && suggestedQuestions.length === 0"
|
<div v-if="suggestedQuestionsLoading && suggestedQuestions.length === 0"
|
||||||
class="suggested-questions-inner">
|
class="suggested-questions-inner">
|
||||||
<div class="suggested-questions-title"><t-skeleton animation="gradient"
|
<div class="suggested-questions-title"><t-skeleton animation="gradient"
|
||||||
:row-col="[{ width: '120px', height: '18px' }]" /></div>
|
:row-col="[{ width: '120px', height: '14px' }]" /></div>
|
||||||
<div class="suggested-questions-grid">
|
<div class="suggested-questions-grid">
|
||||||
<div v-for="n in 6" :key="'sq-skel-' + n" class="suggested-question-card sq-card-skeleton">
|
<div v-for="n in 6" :key="'sq-skel-' + n" class="suggested-question-card sq-card-skeleton">
|
||||||
<t-skeleton animation="gradient"
|
<t-skeleton animation="gradient"
|
||||||
@@ -37,12 +37,17 @@
|
|||||||
<transition v-else appear name="sq-fade">
|
<transition v-else appear name="sq-fade">
|
||||||
<div v-if="suggestedQuestions.length > 0" class="suggested-questions-inner">
|
<div v-if="suggestedQuestions.length > 0" class="suggested-questions-inner">
|
||||||
<div class="suggested-questions-title-row">
|
<div class="suggested-questions-title-row">
|
||||||
<div class="suggested-questions-title">{{ t('chat.suggestedQuestions') }}</div>
|
<p class="suggested-questions-caption">
|
||||||
<t-button variant="text" shape="square" size="small" class="suggested-questions-refresh"
|
<span class="suggested-questions-title">{{ t('chat.suggestedQuestions') }}</span>
|
||||||
:loading="suggestedQuestionsLoading" :title="t('chat.refreshSuggestedQuestions')"
|
<button type="button" class="suggested-questions-refresh"
|
||||||
@click="fetchSuggestedQuestions">
|
:disabled="suggestedQuestionsLoading"
|
||||||
<template #icon><t-icon name="refresh" /></template>
|
:title="t('chat.refreshSuggestedQuestions')"
|
||||||
</t-button>
|
:aria-label="t('chat.refreshSuggestedQuestions')"
|
||||||
|
@click="fetchSuggestedQuestions">
|
||||||
|
<t-icon :name="suggestedQuestionsLoading ? 'loading' : 'refresh'"
|
||||||
|
:class="{ 'sq-refresh-spin': suggestedQuestionsLoading }" />
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="suggested-questions-grid">
|
<div class="suggested-questions-grid">
|
||||||
<div v-for="(item, index) in suggestedQuestions" :key="item.question"
|
<div v-for="(item, index) in suggestedQuestions" :key="item.question"
|
||||||
@@ -1078,152 +1083,23 @@ onBeforeRouteUpdate((to, from, next) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggested-questions-container {
|
@import '../../components/css/suggested-questions.less';
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 32px 16px 16px;
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
transition: min-height 0.3s ease;
|
|
||||||
|
|
||||||
&.has-questions {
|
.suggested-questions-container {
|
||||||
min-height: 80px;
|
transition: min-height 0.3s @suggested-ease;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggested-questions-inner {
|
.suggested-questions-inner {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
animation: contentFadeIn 0.3s ease-out;
|
animation: contentFadeIn 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sq-fade-enter-active,
|
.sq-fade-enter-active,
|
||||||
.sq-fade-leave-active {
|
.sq-fade-leave-active {
|
||||||
transition: opacity 0.25s ease;
|
transition: opacity 0.25s @suggested-ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sq-fade-enter-from,
|
.sq-fade-enter-from,
|
||||||
.sq-fade-leave-to {
|
.sq-fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggested-questions-title-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 2px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggested-questions-title {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--td-text-color-secondary);
|
|
||||||
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;
|
|
||||||
gap: 10px;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggested-question-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 10px 16px;
|
|
||||||
border-radius: 20px;
|
|
||||||
border: 1px solid var(--td-component-stroke);
|
|
||||||
background: var(--td-bg-color-container);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
max-width: 100%;
|
|
||||||
|
|
||||||
&.sq-card-skeleton {
|
|
||||||
pointer-events: none;
|
|
||||||
flex-shrink: 0;
|
|
||||||
border-color: transparent;
|
|
||||||
background: var(--td-bg-color-secondarycontainer);
|
|
||||||
|
|
||||||
: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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.sq-card-skeleton):hover {
|
|
||||||
border-color: var(--td-brand-color);
|
|
||||||
background: var(--td-brand-color-light);
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggested-question-text {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--td-text-color-primary);
|
|
||||||
line-height: 1.4;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggested-question-badge {
|
|
||||||
font-size: 10px;
|
|
||||||
padding: 1px 5px;
|
|
||||||
border-radius: 4px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-weight: 500;
|
|
||||||
|
|
||||||
&.faq {
|
|
||||||
background: var(--td-success-color-1);
|
|
||||||
color: var(--td-success-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<!-- 骨架屏占位 -->
|
<!-- 骨架屏占位 -->
|
||||||
<div v-if="sqLoading && suggestedQuestions.length === 0" class="suggested-questions-inner">
|
<div v-if="sqLoading && suggestedQuestions.length === 0" class="suggested-questions-inner">
|
||||||
<div class="suggested-questions-title"><t-skeleton animation="gradient"
|
<div class="suggested-questions-title"><t-skeleton animation="gradient"
|
||||||
:row-col="[{ width: '120px', height: '18px' }]" /></div>
|
:row-col="[{ width: '120px', height: '14px' }]" /></div>
|
||||||
<div class="suggested-questions-grid">
|
<div class="suggested-questions-grid">
|
||||||
<div v-for="n in 6" :key="'sq-skel-' + n" class="suggested-question-card sq-card-skeleton">
|
<div v-for="n in 6" :key="'sq-skel-' + n" class="suggested-question-card sq-card-skeleton">
|
||||||
<t-skeleton animation="gradient"
|
<t-skeleton animation="gradient"
|
||||||
@@ -21,12 +21,15 @@
|
|||||||
@after-leave="onAfterLeave" @enter="onEnter" @after-enter="onQuestionsEntered">
|
@after-leave="onAfterLeave" @enter="onEnter" @after-enter="onQuestionsEntered">
|
||||||
<div v-if="suggestedQuestions.length > 0" :key="sqRenderKey" class="suggested-questions-inner">
|
<div v-if="suggestedQuestions.length > 0" :key="sqRenderKey" class="suggested-questions-inner">
|
||||||
<div class="suggested-questions-title-row">
|
<div class="suggested-questions-title-row">
|
||||||
<div class="suggested-questions-title">{{ $t('chat.suggestedQuestions') }}</div>
|
<p class="suggested-questions-caption">
|
||||||
<t-button variant="text" shape="square" size="small" class="suggested-questions-refresh"
|
<span class="suggested-questions-title">{{ $t('chat.suggestedQuestions') }}</span>
|
||||||
:loading="sqLoading" :title="$t('chat.refreshSuggestedQuestions')"
|
<button type="button" class="suggested-questions-refresh" :disabled="sqLoading"
|
||||||
@click="fetchSuggestedQuestions">
|
:title="$t('chat.refreshSuggestedQuestions')"
|
||||||
<template #icon><t-icon name="refresh" /></template>
|
:aria-label="$t('chat.refreshSuggestedQuestions')" @click="fetchSuggestedQuestions">
|
||||||
</t-button>
|
<t-icon :name="sqLoading ? 'loading' : 'refresh'"
|
||||||
|
:class="{ 'sq-refresh-spin': sqLoading }" />
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="suggested-questions-grid">
|
<div class="suggested-questions-grid">
|
||||||
<div v-for="(item, index) in suggestedQuestions" :key="item.question"
|
<div v-for="(item, index) in suggestedQuestions" :key="item.question"
|
||||||
@@ -251,6 +254,7 @@ const handleKBEditorSuccess = (kbId: string) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
:deep(.answers-input) {
|
:deep(.answers-input) {
|
||||||
position: static;
|
position: static;
|
||||||
@@ -265,7 +269,7 @@ const handleKBEditorSuccess = (kbId: string) => {
|
|||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 0;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -285,15 +289,7 @@ const handleKBEditorSuccess = (kbId: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggested-questions-container {
|
@import '../../components/css/suggested-questions.less';
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 800px;
|
|
||||||
transition: height 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes skeletonFadeIn {
|
@keyframes skeletonFadeIn {
|
||||||
from {
|
from {
|
||||||
@@ -305,18 +301,19 @@ const handleKBEditorSuccess = (kbId: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.suggested-questions-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 16px;
|
||||||
|
transition: height 0.35s @suggested-ease;
|
||||||
|
}
|
||||||
|
|
||||||
.suggested-questions-inner {
|
.suggested-questions-inner {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
animation: skeletonFadeIn 0.3s ease-out;
|
animation: skeletonFadeIn 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 容器整体过渡:淡入 + 轻微上滑
|
|
||||||
.sq-slide-fade-enter-active {
|
.sq-slide-fade-enter-active {
|
||||||
transition: opacity 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
transition: opacity 0.35s @suggested-ease, transform 0.35s @suggested-ease;
|
||||||
transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sq-slide-fade-leave-active {
|
.sq-slide-fade-leave-active {
|
||||||
@@ -334,99 +331,18 @@ const handleKBEditorSuccess = (kbId: string) => {
|
|||||||
transform: translateY(-4px);
|
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: 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;
|
|
||||||
gap: 10px;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggested-question-card {
|
.suggested-question-card {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 10px 16px;
|
|
||||||
border-radius: 20px;
|
|
||||||
border: 1px solid var(--td-component-stroke);
|
|
||||||
background: var(--td-bg-color-container);
|
|
||||||
cursor: pointer;
|
|
||||||
max-width: 100%;
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(8px) scale(0.97);
|
transform: translateY(8px) scale(0.97);
|
||||||
transition: opacity 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
transition:
|
||||||
transform 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
opacity 0.35s @suggested-ease,
|
||||||
border-color 0.2s ease,
|
transform 0.35s @suggested-ease,
|
||||||
background 0.2s ease,
|
background 0.2s @suggested-ease,
|
||||||
box-shadow 0.2s ease;
|
border-color 0.2s @suggested-ease;
|
||||||
|
|
||||||
&.sq-card-skeleton {
|
&.sq-card-skeleton {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: none;
|
transform: none;
|
||||||
cursor: default;
|
|
||||||
pointer-events: none;
|
|
||||||
flex-shrink: 0;
|
|
||||||
border-color: transparent;
|
|
||||||
background: var(--td-bg-color-secondarycontainer);
|
|
||||||
|
|
||||||
: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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.sq-card-visible {
|
&.sq-card-visible {
|
||||||
@@ -434,32 +350,12 @@ const handleKBEditorSuccess = (kbId: string) => {
|
|||||||
transform: translateY(0) scale(1);
|
transform: translateY(0) scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.sq-card-skeleton):hover {
|
&:not(.sq-card-skeleton):active {
|
||||||
border-color: var(--td-brand-color);
|
transform: scale(0.98);
|
||||||
background: var(--td-brand-color-light);
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.suggested-question-text {
|
&.sq-card-visible:active {
|
||||||
font-size: 13px;
|
transform: scale(0.98);
|
||||||
color: var(--td-text-color-primary);
|
|
||||||
line-height: 1.4;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggested-question-badge {
|
|
||||||
font-size: 10px;
|
|
||||||
padding: 1px 5px;
|
|
||||||
border-radius: 4px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-weight: 500;
|
|
||||||
|
|
||||||
&.faq {
|
|
||||||
background: var(--td-success-color-1);
|
|
||||||
color: var(--td-success-color);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user