|
|
|
@ -69,29 +69,21 @@ |
|
|
|
>{{ q }}</t-tag> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 消息列表 --> |
|
|
|
<!-- 消息列表(TDesign AI Chat) --> |
|
|
|
<div class="msg-area chat-msg-area" ref="msgAreaRef"> |
|
|
|
<div v-for="(m, i) in messages" :key="i" :class="['msg', m.role, m.streaming ? 'streaming' : '']"> |
|
|
|
<div class="msg-content"> |
|
|
|
<!-- 编辑模式 --> |
|
|
|
<template v-if="editingIndex === i"> |
|
|
|
<t-textarea |
|
|
|
v-model="editingText" |
|
|
|
:autosize="{ minRows: 2, maxRows: 4 }" |
|
|
|
@keydown.enter.exact.prevent="submitEdit(i)" |
|
|
|
/> |
|
|
|
<div class="edit-actions"> |
|
|
|
<t-button theme="primary" size="small" @click="submitEdit(i)" :disabled="isSending || !editingText.trim()">重新发送</t-button> |
|
|
|
<t-button variant="outline" size="small" @click="cancelEdit">取消</t-button> |
|
|
|
<span class="edit-hint">将清除此条之后的全部对话并重发</span> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
|
|
|
|
<!-- 正常显示模式 --> |
|
|
|
<template v-else> |
|
|
|
<chat-list |
|
|
|
:data="chatData" |
|
|
|
layout="both" |
|
|
|
:auto-scroll="false" |
|
|
|
:is-stream-load="false" |
|
|
|
:text-loading="false" |
|
|
|
class="chat-list" |
|
|
|
> |
|
|
|
<template #content="{ index }"> |
|
|
|
<div class="chat-custom-content"> |
|
|
|
<!-- MCP 工具调用 --> |
|
|
|
<div v-if="m.role === 'assistant' && m.toolCalls && m.toolCalls.length" class="mcp-tool-calls"> |
|
|
|
<div v-for="(tc, j) in m.toolCalls" :key="j" class="mcp-tool-item"> |
|
|
|
<div v-if="messages[index]?.toolCalls?.length" class="mcp-tool-calls"> |
|
|
|
<div v-for="(tc, j) in messages[index].toolCalls" :key="j" class="mcp-tool-item"> |
|
|
|
<div class="mcp-tool-header"> |
|
|
|
<span class="mcp-tool-icon">{{ tc.status === 'running' ? '⏳' : '✅' }}</span> |
|
|
|
<span class="mcp-tool-name">{{ tc.tool }}</span> |
|
|
|
@ -99,60 +91,79 @@ |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<!-- AI 回复(Markdown 渲染) --> |
|
|
|
<div v-if="m.role === 'assistant' && m.content" class="msg-bubble markdown-body" v-html="renderMarkdown(m.content)"></div> |
|
|
|
<!-- AI 思考中 --> |
|
|
|
<div v-else-if="m.role === 'assistant'" class="msg-bubble"><span class="thinking">正在思考</span></div> |
|
|
|
<!-- 用户消息 --> |
|
|
|
<div v-else class="msg-bubble">{{ m.content }}</div> |
|
|
|
|
|
|
|
<!-- 引用来源 --> |
|
|
|
<MessageSources v-if="m.role === 'assistant' && m.sources && m.sources.length" :sources="m.sources" /> |
|
|
|
|
|
|
|
<!-- 推荐问题(suggest-message-list) --> |
|
|
|
<div class="suggest-row" v-if="i === messages.length - 1 && currentSuggestions.length && !isSending"> |
|
|
|
<span class="suggest-label">💡 推荐问题</span> |
|
|
|
<t-tag |
|
|
|
v-for="q in currentSuggestions" |
|
|
|
:key="q" |
|
|
|
theme="default" |
|
|
|
variant="light" |
|
|
|
class="quick-tag" |
|
|
|
@click="useQuickQuestion(q)" |
|
|
|
>{{ q }}</t-tag> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 消息操作栏 --> |
|
|
|
<div class="msg-tools"> |
|
|
|
<span>{{ m.time }}</span> |
|
|
|
<t-button v-if="m.role === 'assistant' && m.content" variant="text" size="small" @click="copyMessage(m.content)">复制</t-button> |
|
|
|
<t-button v-if="m.role === 'assistant' && m.content && !m.streaming" variant="text" size="small" @click="regenerate(i)">重新生成</t-button> |
|
|
|
<t-button v-if="m.role === 'user' && !m.streaming" variant="text" size="small" @click="startEditMessage(i)">编辑</t-button> |
|
|
|
<t-button v-if="m.error" variant="text" size="small" @click="retryLast">重试</t-button> |
|
|
|
<template v-if="m.role === 'assistant' && m.content && !m.streaming"> |
|
|
|
<t-button variant="text" size="small" :style="{ color: m.feedback === 'up' ? 'var(--td-brand-color)' : '' }" @click="submitFeedback(m.id, 'up')" title="有帮助">👍</t-button> |
|
|
|
<t-button variant="text" size="small" :style="{ color: m.feedback === 'down' ? 'var(--td-error-color)' : '' }" @click="submitFeedback(m.id, 'down')" title="没帮助">👎</t-button> |
|
|
|
</template> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<!-- 编辑模式 --> |
|
|
|
<template v-if="editingIndex === index"> |
|
|
|
<t-textarea |
|
|
|
v-model="editingText" |
|
|
|
:autosize="{ minRows: 2, maxRows: 4 }" |
|
|
|
@keydown.enter.exact.prevent="submitEdit(index)" |
|
|
|
/> |
|
|
|
<div class="edit-actions"> |
|
|
|
<t-button theme="primary" size="small" @click="submitEdit(index)" :disabled="isSending || !editingText.trim()">重新发送</t-button> |
|
|
|
<t-button variant="outline" size="small" @click="cancelEdit">取消</t-button> |
|
|
|
<span class="edit-hint">将清除此条之后的全部对话并重发</span> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
|
|
|
|
<!-- 正常显示模式 --> |
|
|
|
<template v-else> |
|
|
|
<!-- AI 回复(Markdown 渲染) --> |
|
|
|
<div v-if="messages[index]?.role === 'assistant' && messages[index]?.content" class="markdown-body" v-html="renderMarkdown(messages[index].content)"></div> |
|
|
|
<!-- AI 思考中 --> |
|
|
|
<div v-else-if="messages[index]?.role === 'assistant'" class="thinking"><span>正在思考</span></div> |
|
|
|
<!-- 用户消息 --> |
|
|
|
<div v-else class="user-text">{{ messages[index]?.content }}</div> |
|
|
|
|
|
|
|
<!-- 引用来源 --> |
|
|
|
<MessageSources v-if="messages[index]?.role === 'assistant' && messages[index]?.sources?.length" :sources="messages[index].sources" /> |
|
|
|
|
|
|
|
<!-- 推荐问题 --> |
|
|
|
<div class="suggest-row" v-if="isLastAssistant(index, messages) && currentSuggestions.length && !isSending"> |
|
|
|
<span class="suggest-label">💡 推荐问题</span> |
|
|
|
<t-tag |
|
|
|
v-for="q in currentSuggestions" |
|
|
|
:key="q" |
|
|
|
theme="default" |
|
|
|
variant="light" |
|
|
|
class="quick-tag" |
|
|
|
@click="useQuickQuestion(q)" |
|
|
|
>{{ q }}</t-tag> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
|
|
|
|
<template #actions="{ index }"> |
|
|
|
<chat-actionbar |
|
|
|
v-if="messages[index]?.role === 'assistant' && messages[index]?.content && !messages[index]?.streaming" |
|
|
|
:is-good="messages[index]?.feedback === 'up'" |
|
|
|
:is-bad="messages[index]?.feedback === 'down'" |
|
|
|
:action-bar="['copy', 'replay', 'good', 'bad']" |
|
|
|
@operation="(val) => onMessageAction(val, index)" |
|
|
|
/> |
|
|
|
<div v-else-if="messages[index]?.role === 'user' && !messages[index]?.streaming" class="msg-extra-actions"> |
|
|
|
<t-button variant="text" size="small" @click="startEditMessage(index)">编辑</t-button> |
|
|
|
</div> |
|
|
|
<div v-else-if="messages[index]?.error" class="msg-extra-actions"> |
|
|
|
<t-button variant="text" size="small" @click="retryLast">重试</t-button> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
</chat-list> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 输入区 --> |
|
|
|
<footer class="chat-composer"> |
|
|
|
<div class="composer-box"> |
|
|
|
<t-textarea |
|
|
|
v-model="userInput" |
|
|
|
placeholder="输入问题,Enter 发送,Shift + Enter 换行" |
|
|
|
:autosize="{ minRows: 2, maxRows: 4 }" |
|
|
|
:disabled="isSending" |
|
|
|
@keydown.enter.exact.prevent="send" |
|
|
|
/> |
|
|
|
<t-button theme="primary" class="send-btn" @click="send" :disabled="isSending || !userInput.trim()"> |
|
|
|
{{ isSending ? '发送中' : '发送' }} |
|
|
|
</t-button> |
|
|
|
</div> |
|
|
|
<chat-sender |
|
|
|
v-model="userInput" |
|
|
|
:loading="isSending" |
|
|
|
:disabled="isSending" |
|
|
|
:send-btn-disabled="!userInput.trim()" |
|
|
|
placeholder="输入问题,Enter 发送,Shift + Enter 换行" |
|
|
|
:textarea-props="{ autosize: { minRows: 2, maxRows: 4 } }" |
|
|
|
@send="send" |
|
|
|
@stop="abortChat" |
|
|
|
/> |
|
|
|
</footer> |
|
|
|
</section> |
|
|
|
|
|
|
|
@ -184,6 +195,12 @@ |
|
|
|
|
|
|
|
<script setup lang="ts"> |
|
|
|
import { ref, computed, nextTick, onMounted, onBeforeUnmount } from 'vue' |
|
|
|
import { |
|
|
|
ChatList, |
|
|
|
ChatSender, |
|
|
|
ChatActionbar, |
|
|
|
} from '@tdesign-vue-next/chat' |
|
|
|
import '@tdesign-vue-next/chat/es/style/index.css' |
|
|
|
import { chatSync, chatRagSync, chatSSEUrl, chatRagSSEUrl, ragSources, fetchSuggestions } from '@/api/chat' |
|
|
|
import { getRoleList } from '@/api/role' |
|
|
|
import { getActiveModelConfig } from '@/api/model-config' |
|
|
|
@ -193,6 +210,8 @@ import { toast } from '@/utils/toast' |
|
|
|
import { readSSEStream, readSSEStreamWithEvents } from '@/utils/sse' |
|
|
|
import { renderMarkdown } from '@/utils/markdown' |
|
|
|
import { useCategoryStore } from '@/stores/category' |
|
|
|
import { toChatData, isLastAssistant } from '@/utils/chatAdapter' |
|
|
|
import type { ChatMessage } from '@/utils/chatAdapter' |
|
|
|
import MessageSources from '@/components/MessageSources.vue' |
|
|
|
|
|
|
|
const categoryStore = useCategoryStore() |
|
|
|
@ -243,18 +262,6 @@ let sseAbortController: AbortController | null = null |
|
|
|
const currentSuggestions = ref<string[]>([]) // suggest-message-list |
|
|
|
|
|
|
|
// ==================== 消息列表 ==================== |
|
|
|
interface ChatMessage { |
|
|
|
id: string |
|
|
|
role: 'user' | 'assistant' |
|
|
|
content: string |
|
|
|
streaming: boolean |
|
|
|
time: string |
|
|
|
sources?: any[] |
|
|
|
toolCalls?: any[] |
|
|
|
error?: boolean |
|
|
|
feedback?: string | null |
|
|
|
} |
|
|
|
|
|
|
|
const messages = ref<ChatMessage[]>([ |
|
|
|
{ |
|
|
|
id: generateMsgId(), |
|
|
|
@ -265,6 +272,8 @@ const messages = ref<ChatMessage[]>([ |
|
|
|
}, |
|
|
|
]) |
|
|
|
|
|
|
|
const chatData = computed(() => toChatData(messages.value)) |
|
|
|
|
|
|
|
// ==================== 计算属性 ==================== |
|
|
|
const currentRole = computed(() => roles.value.find(r => r.key === selectedRole.value) || roles.value[0] || FALLBACK_ROLE) |
|
|
|
|
|
|
|
@ -440,6 +449,7 @@ async function send(): Promise<void> { |
|
|
|
await readSSEStreamWithEvents(url, { |
|
|
|
onMessage: async (chunk: string) => { |
|
|
|
assistantMsg.content += chunk |
|
|
|
messages.value = [...messages.value] |
|
|
|
await scrollToBottom() |
|
|
|
}, |
|
|
|
onToolCallStart: (data: any) => { |
|
|
|
@ -449,10 +459,12 @@ async function send(): Promise<void> { |
|
|
|
onToolCallResult: (data: any) => { |
|
|
|
const tc = assistantMsg.toolCalls!.find(t => t.tool === data.tool && t.status === 'running') |
|
|
|
if (tc) { tc.status = 'done'; tc.result = data.result; tc.latencyMs = data.latencyMs } |
|
|
|
messages.value = [...messages.value] |
|
|
|
scrollToBottom() |
|
|
|
}, |
|
|
|
onError: (data: any) => { |
|
|
|
assistantMsg.content += '\n\n⚠️ ' + (data.message || '工具调用出错') |
|
|
|
messages.value = [...messages.value] |
|
|
|
}, |
|
|
|
onDone: () => {}, |
|
|
|
}, undefined, signal) |
|
|
|
@ -464,6 +476,7 @@ async function send(): Promise<void> { |
|
|
|
const url = chatSSEUrl(text, cid, roleId) |
|
|
|
await readSSEStream(url, async (chunk: string) => { |
|
|
|
assistantMsg.content += chunk |
|
|
|
messages.value = [...messages.value] |
|
|
|
await scrollToBottom() |
|
|
|
}, () => {}, undefined, signal) |
|
|
|
} |
|
|
|
@ -487,6 +500,7 @@ async function send(): Promise<void> { |
|
|
|
} finally { |
|
|
|
assistantMsg.streaming = false |
|
|
|
isSending.value = false |
|
|
|
messages.value = [...messages.value] |
|
|
|
|
|
|
|
// 拉取推荐问题(suggest-message-list);AbortError / 异常时不拉取 |
|
|
|
if (!sseAbortController?.signal.aborted && assistantMsg.content && !assistantMsg.error) { |
|
|
|
@ -499,7 +513,40 @@ async function send(): Promise<void> { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// ==================== 停止生成 ==================== |
|
|
|
function abortChat(): void { |
|
|
|
if (sseAbortController) { |
|
|
|
sseAbortController.abort() |
|
|
|
} |
|
|
|
isSending.value = false |
|
|
|
const last = messages.value[messages.value.length - 1] |
|
|
|
if (last && last.role === 'assistant' && last.streaming) { |
|
|
|
last.streaming = false |
|
|
|
if (!last.content) last.content = '已取消' |
|
|
|
messages.value = [...messages.value] |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// ==================== 消息操作 ==================== |
|
|
|
function onMessageAction(action: string, index: number): void { |
|
|
|
const msg = messages.value[index] |
|
|
|
if (!msg) return |
|
|
|
switch (action) { |
|
|
|
case 'copy': |
|
|
|
copyMessage(msg.content) |
|
|
|
break |
|
|
|
case 'replay': |
|
|
|
regenerate(index) |
|
|
|
break |
|
|
|
case 'good': |
|
|
|
submitFeedback(msg.id, 'up') |
|
|
|
break |
|
|
|
case 'bad': |
|
|
|
submitFeedback(msg.id, 'down') |
|
|
|
break |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function retryLast(): void { |
|
|
|
if (!lastUserInput.value || isSending.value) return |
|
|
|
userInput.value = lastUserInput.value |
|
|
|
@ -605,7 +652,7 @@ async function submitFeedback(msgId: string, type: 'up' | 'down'): Promise<void> |
|
|
|
feedbackType: 'THUMBS_UP', |
|
|
|
}) |
|
|
|
toast('感谢反馈 👍', 'success') |
|
|
|
} catch (e) { |
|
|
|
} catch (e: any) { |
|
|
|
// 失败时回滚乐观更新 |
|
|
|
msg.feedback = wasActive ? 'up' : null |
|
|
|
console.error('反馈提交失败:', e) |
|
|
|
@ -641,7 +688,7 @@ async function confirmFeedbackReason(): Promise<void> { |
|
|
|
reasonComment, |
|
|
|
}) |
|
|
|
toast('感谢反馈,我们会持续改进', 'success') |
|
|
|
} catch (e) { |
|
|
|
} catch (e: any) { |
|
|
|
// 失败时回滚 |
|
|
|
msg.feedback = null |
|
|
|
console.error('反馈提交失败:', e) |
|
|
|
@ -736,34 +783,55 @@ onBeforeUnmount(() => { |
|
|
|
.quick-row { display: flex; gap: 8px; padding: 10px 16px; flex-wrap: wrap; border-bottom: 1px solid var(--td-border-level-1-color); } |
|
|
|
.quick-tag { cursor: pointer; } |
|
|
|
|
|
|
|
/* ==================== 消息区 ==================== */ |
|
|
|
/* ==================== 消息区(TDesign ChatList 容器) ==================== */ |
|
|
|
.msg-area { flex: 1; overflow-y: auto; padding: 16px; } |
|
|
|
.msg { margin-bottom: 16px; } |
|
|
|
.msg.user .msg-content { display: flex; flex-direction: column; align-items: flex-end; } |
|
|
|
.msg.user .msg-bubble { |
|
|
|
background: var(--td-brand-color); color: var(--td-text-color-anti); border-radius: 12px 12px 4px 12px; |
|
|
|
padding: 10px 14px; max-width: 70%; font-size: 14px; line-height: 1.6; white-space: pre-wrap; |
|
|
|
} |
|
|
|
.msg.assistant .msg-bubble { |
|
|
|
background: var(--td-bg-color-secondarycontainer); color: var(--td-text-color-primary); border-radius: 12px 12px 12px 4px; |
|
|
|
padding: 12px 16px; max-width: 85%; font-size: 14px; line-height: 1.7; |
|
|
|
} |
|
|
|
.msg.streaming .msg-content::after { content: ''; } |
|
|
|
.thinking { color: var(--td-text-color-placeholder); font-style: italic; animation: blink 1s infinite; } |
|
|
|
.chat-list { |
|
|
|
--td-chat-item-max-width: 85%; |
|
|
|
} |
|
|
|
.chat-list :deep(.t-chat__list) { |
|
|
|
gap: 16px; |
|
|
|
} |
|
|
|
.chat-list :deep(.t-chat__inner) { |
|
|
|
width: 100%; |
|
|
|
} |
|
|
|
.chat-list :deep(.t-chat__content) { |
|
|
|
max-width: 85%; |
|
|
|
} |
|
|
|
.chat-list :deep(.t-chat__detail) { |
|
|
|
padding: 12px 16px; |
|
|
|
border-radius: 12px; |
|
|
|
background: var(--td-bg-color-secondarycontainer); |
|
|
|
} |
|
|
|
.chat-list :deep(.t-chat__inner.user .t-chat__detail) { |
|
|
|
background: var(--td-brand-color); |
|
|
|
color: var(--td-text-color-anti); |
|
|
|
} |
|
|
|
.chat-list :deep(.t-chat__actions-margin) { |
|
|
|
margin-top: 6px; |
|
|
|
} |
|
|
|
|
|
|
|
/* 自定义内容区 */ |
|
|
|
.chat-custom-content { font-size: 14px; line-height: 1.7; } |
|
|
|
.chat-custom-content .user-text { |
|
|
|
white-space: pre-wrap; |
|
|
|
} |
|
|
|
.chat-custom-content .thinking { |
|
|
|
color: var(--td-text-color-placeholder); font-style: italic; animation: blink 1s infinite; |
|
|
|
} |
|
|
|
@keyframes blink { 50% { opacity: 0.5; } } |
|
|
|
|
|
|
|
/* Markdown 在气泡内 */ |
|
|
|
.msg-bubble :deep(h1) { font-size: 1.3em; margin: 12px 0 6px; } |
|
|
|
.msg-bubble :deep(h2) { font-size: 1.15em; margin: 10px 0 4px; } |
|
|
|
.msg-bubble :deep(h3) { font-size: 1.05em; margin: 8px 0 4px; } |
|
|
|
.msg-bubble :deep(p) { margin: 4px 0; } |
|
|
|
.msg-bubble :deep(ul), .msg-bubble :deep(ol) { padding-left: 20px; margin: 6px 0; } |
|
|
|
.msg-bubble :deep(table) { border-collapse: collapse; margin: 8px 0; width: 100%; } |
|
|
|
.msg-bubble :deep(th), .msg-bubble :deep(td) { border: 1px solid var(--td-border-level-2-color); padding: 6px 10px; text-align: left; font-size: 13px; } |
|
|
|
.msg-bubble :deep(th) { background: var(--td-bg-color-secondarycontainer); font-weight: 600; } |
|
|
|
.msg-bubble :deep(code) { background: rgba(0,0,0,.06); padding: 2px 6px; border-radius: 3px; font-size: 13px; } |
|
|
|
.msg-bubble :deep(pre) { background: var(--color-code-block-bg); color: var(--color-code-block-text); padding: 12px; border-radius: 8px; overflow-x: auto; } |
|
|
|
.msg-bubble :deep(pre code) { background: none; padding: 0; color: inherit; } |
|
|
|
.chat-custom-content :deep(h1) { font-size: 1.3em; margin: 12px 0 6px; } |
|
|
|
.chat-custom-content :deep(h2) { font-size: 1.15em; margin: 10px 0 4px; } |
|
|
|
.chat-custom-content :deep(h3) { font-size: 1.05em; margin: 8px 0 4px; } |
|
|
|
.chat-custom-content :deep(p) { margin: 4px 0; } |
|
|
|
.chat-custom-content :deep(ul), .chat-custom-content :deep(ol) { padding-left: 20px; margin: 6px 0; } |
|
|
|
.chat-custom-content :deep(table) { border-collapse: collapse; margin: 8px 0; width: 100%; } |
|
|
|
.chat-custom-content :deep(th), .chat-custom-content :deep(td) { border: 1px solid var(--td-border-level-2-color); padding: 6px 10px; text-align: left; font-size: 13px; } |
|
|
|
.chat-custom-content :deep(th) { background: var(--td-bg-color-secondarycontainer); font-weight: 600; } |
|
|
|
.chat-custom-content :deep(code) { background: rgba(0,0,0,.06); padding: 2px 6px; border-radius: 3px; font-size: 13px; } |
|
|
|
.chat-custom-content :deep(pre) { background: var(--color-code-block-bg); color: var(--color-code-block-text); padding: 12px; border-radius: 8px; overflow-x: auto; } |
|
|
|
.chat-custom-content :deep(pre code) { background: none; padding: 0; color: inherit; } |
|
|
|
|
|
|
|
/* ==================== MCP 工具调用 ==================== */ |
|
|
|
.mcp-tool-calls { margin-bottom: 8px; } |
|
|
|
@ -774,16 +842,16 @@ onBeforeUnmount(() => { |
|
|
|
.mcp-tool-latency { font-size: 11px; color: var(--td-text-color-placeholder); margin-left: auto; } |
|
|
|
|
|
|
|
/* ==================== 消息操作 ==================== */ |
|
|
|
.msg-tools { display: flex; align-items: center; gap: 6px; margin-top: 6px; font-size: 12px; color: var(--td-text-color-placeholder); } |
|
|
|
.msg-tools span { margin-right: auto; } |
|
|
|
.msg-extra-actions { display: inline-flex; } |
|
|
|
.edit-actions { display: flex; align-items: center; gap: 8px; margin-top: 8px; } |
|
|
|
.edit-hint { font-size: 12px; color: var(--td-text-color-placeholder); } |
|
|
|
|
|
|
|
/* ==================== 推荐问题 ==================== */ |
|
|
|
.suggest-row { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; align-items: center; } |
|
|
|
.suggest-label { font-size: 12px; color: var(--td-text-color-placeholder); } |
|
|
|
|
|
|
|
/* ==================== 输入区 ==================== */ |
|
|
|
.chat-composer { padding: 12px 16px; border-top: 1px solid var(--td-border-level-1-color); } |
|
|
|
.composer-box { display: flex; gap: 10px; align-items: flex-end; } |
|
|
|
.composer-box :deep(.t-textarea__inner) { min-height: 44px; } |
|
|
|
.send-btn { flex-shrink: 0; height: 44px; } |
|
|
|
|
|
|
|
@media (max-width: 768px) { |
|
|
|
.chat-sidebar { display: none; } |
|
|
|
|