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:
wizardchen
2026-06-17 22:38:07 +08:00
committed by lyingbug
parent e0d3c1dafd
commit 4e2d568149
9 changed files with 305 additions and 313 deletions
+1
View File
@@ -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', () => {
+6 -7
View File
@@ -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()}`
}, },
) )
+16 -140
View File
@@ -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>
+30 -134
View File
@@ -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);
} }
} }